netget-tool 0.3.5

Network protocol stubbing toolkit: ~280 protocol servers and clients whose responses come from an LLM, a script, a static rule or a person.
use crate::llm::actions::{
    client_trait::{Client, ClientActionResult},
    protocol_trait::Protocol,
    ActionDefinition, ParameterDefinition, StartupExamples,
};
use crate::protocol::{
    metadata::{DevelopmentState, ProtocolMetadataV2},
    ConnectContext, EventType,
};
use crate::server::diameter::{
    actions::{action, identity_parameters, parameter, timer_parameters},
    codec::{self, Request},
};
use crate::state::AppState;
use anyhow::{bail, ensure, Result};
use serde_json::{json, Value};
use std::sync::LazyLock;
#[derive(Default)]
pub struct DiameterClientProtocol;
impl DiameterClientProtocol {
    pub fn new() -> Self {
        Self
    }
}
fn send() -> ActionDefinition {
    action("send_diameter_aa","Send one stateless NASREQ PAP AAR on the negotiated TCP peer, with fresh correlated IDs and Session-Id. No automatic retry or account fallback.",vec![parameter("username","string","Required nonempty UTF8<=255bytes",true),parameter("password","string","UTF8<=128bytes; required types1/3, excluded type2; redacted in action logs",false),parameter("auth_request_type","number","1authenticate-only,2authorize-only,3combined(default)",false),parameter("nas_identifier","string","Optional NAS identifier, UTF8 at most255bytes without NUL",false),parameter("nas_port","number","Optional NAS port number in the unsigned32bit range",false)],json!({"type":"send_diameter_aa","username":"alice","password":"replace-me","auth_request_type":3}))
}
fn disconnect() -> ActionDefinition {
    action("disconnect","Send bounded native DPR/DPA then close; cancel pending AAA and handlers. Removal closes immediately without waiting for peer.",vec![],json!({"type":"disconnect"}))
}
fn all() -> Vec<ActionDefinition> {
    vec![send(), disconnect()]
}
pub static CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
    EventType::new("diameter_connected","TCP and CER/CEA NASREQ capability exchange completed; peer identity is consistent but unauthenticated clear TCP.",send().example.clone()).with_parameters(vec![parameter("peer","object","origin_host,origin_realm,remote_addr",true)]).with_actions(all())
});
pub static AA_EVENT: LazyLock<EventType> = LazyLock::new(|| {
    EventType::new("diameter_aa_result","Correlated stateless NASREQ result; accepted only for2001 with agreed stateless mode and supported mandatory AVPs. Returned attributes are not applied to a device.",disconnect().example.clone()).with_parameters(vec![parameter("request","object","Credential-free username/auth_request_type/NAS fields and Session-Id",true),parameter("reply","object","result_code,accepted,stateless,reply_messages,filter_ids,service_type,session_timeout",true)]).with_actions(all())
});
pub static ERROR_EVENT: LazyLock<EventType> = LazyLock::new(|| {
    EventType::new(
        "diameter_error",
        "Peer/session deadline, framing or correlation failed; no implicit replay.",
        disconnect().example.clone(),
    )
    .with_parameters(vec![parameter(
        "error",
        "string",
        "Safe bounded category without credential reflection",
        true,
    )])
    .with_actions(all())
});
pub fn parse_action(value: Value) -> Result<Request> {
    let mut value = codec::owned_json(value)?;
    ensure!(
        value["type"] == "send_diameter_aa",
        "Diameter client request action"
    );
    value
        .as_object_mut()
        .context("Diameter action object")?
        .remove("type");
    let request: Request = serde_json::from_value(value)?;
    request.validate()?;
    Ok(request)
}
use anyhow::Context;
impl Protocol for DiameterClientProtocol {
    fn protocol_name(&self) -> &'static str {
        "DIAMETER"
    }
    fn stack_name(&self) -> &'static str {
        "ETH>IP>TCP>DIAMETER"
    }
    fn keywords(&self) -> Vec<&'static str> {
        vec!["diameter", "nasreq"]
    }
    fn description(&self) -> &'static str {
        "Diameter negotiated TCP peer and stateless NASREQ typed client"
    }
    fn example_prompt(&self) -> &'static str {
        "Authenticate alice against a Diameter NASREQ test peer"
    }
    fn group_name(&self) -> &'static str {
        "Network Management"
    }
    fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
        all()
    }
    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
        vec![]
    }
    fn get_event_types(&self) -> Vec<EventType> {
        vec![
            CONNECTED_EVENT.clone(),
            AA_EVENT.clone(),
            ERROR_EVENT.clone(),
        ]
    }
    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
        let mut p = identity_parameters();
        p.extend(timer_parameters());
        p
    }
    fn metadata(&self) -> ProtocolMetadataV2 {
        ProtocolMetadataV2::builder().state(DevelopmentState::Experimental).well_known_port(3868).request_only("Native CER/CEA/watchdog/disconnect and typed AAA responses; no arbitrary unsolicited payload action").max_inbound_bytes(codec::MAX_FRAME_BYTES).implementation("Native bounded RFC6733 negotiated TCP peer lifecycle and RFC7155 stateless NASREQ UTF8 PAP requests/typed results").llm_control("Common typed connected/result/error handlers, shared memory/access recording and bounded command injection independent of parked handlers").e2e_testing("Required unchanged python-diameter0.9.0 Node and fiorix/go-diameter4.5.0 public SDK server roles; real codec golden, response correlation/errors/state refusal/bounds/cancellation").notes("Experimental selected stateless NASREQ application1, not full RFC compliance. Trusted clear TCP3868 only; no TLS/DTLS/SCTP, peer certificate authentication or integrity. Connect completes TCP/CER/CEA; negotiated origin host/realm supply direct NASREQ destination; identity consistency is not cryptographic authentication. Native watchdog and DPR/DPA remain responsive while handlers are parked. Auth-Session-State=1 required on successful AAA; only result2001 accepted after header/IDs/session/application/type/origin/mandatory-AVP validation.1authenticate-only,2authorize-only(no password),3combined; no stateful sessions/multiround/CHAP/EAP, prior authentication binding, STR/ASR/RAR/accounting/agents/routing/vendor applications/grouped policy/failover or replay. One pending AAA,16commands,32queued events/actions,followupdepth8;16KiBframe/64AVPs,512byteSession-Id,255byteusername/NAS identity,128bytepassword,1024byte reply text,8reply/filter values. Client result events omit password; original action values remain intact on wire and redacted in injected logs. Service-Type1..19,Filter-Id,Session-Timeout reported without device enforcement. Constructed action JSON16KiB/4096nodes/depth16 preflighted before copying. Whole-frame/connect/watchdog answer90s,handler30s,watchdog idle30s default configurable1..300s;write10s. Caller command timeout alone does not cancel wire operation; disconnect/removal cancels it and handlers. No protocol domain store, durable AAA session, fuzz or production-capture claim.").build()
    }
    fn get_startup_examples(&self) -> StartupExamples {
        let base = json!({"type":"open_client","base_stack":"diameter","remote_addr":"127.0.0.1:3868","startup_params":{"origin_host":"client.example","origin_realm":"example"}});
        let mut llm = base.clone();
        llm["instruction"] = json!("Request one stateless PAP authentication for alice");
        let mut script = base.clone();
        script["event_handlers"] = json!([{"event_pattern":"diameter_connected","handler":{"type":"script","language":"python","code":"import json,sys\njson.load(sys.stdin)\nprint(json.dumps({'actions':[{'type':'send_diameter_aa','username':'alice','password':'replace-me'}]}))"}}]);
        let mut fixed = base;
        fixed["event_handlers"] = json!([{"event_pattern":"diameter_connected","handler":{"type":"static","actions":[send().example]}}]);
        StartupExamples::new(llm, script, fixed)
    }
}
impl Client for DiameterClientProtocol {
    fn connect(
        &self,
        ctx: ConnectContext,
    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
    {
        Box::pin(super::DiameterClient::connect(ctx))
    }
    fn execute_action(&self, value: Value) -> Result<ClientActionResult> {
        let value = codec::owned_json(value)?;
        if value["type"] == "disconnect" {
            ensure!(
                value.as_object().is_some_and(|m| m.len() == 1),
                "Diameter disconnect only type"
            );
            return Ok(ClientActionResult::Disconnect);
        }
        if value["type"] != "send_diameter_aa" {
            bail!("Unknown Diameter client action");
        }
        parse_action(value.clone())?;
        Ok(ClientActionResult::Custom {
            name: "diameter_command".into(),
            data: value,
        })
    }
}