Skip to main content

netget/client/diameter/
actions.rs

1use crate::llm::actions::{
2    client_trait::{Client, ClientActionResult},
3    protocol_trait::Protocol,
4    ActionDefinition, ParameterDefinition, StartupExamples,
5};
6use crate::protocol::{
7    metadata::{DevelopmentState, ProtocolMetadataV2},
8    ConnectContext, EventType,
9};
10use crate::server::diameter::{
11    actions::{action, identity_parameters, parameter, timer_parameters},
12    codec::{self, Request},
13};
14use crate::state::AppState;
15use anyhow::{bail, ensure, Result};
16use serde_json::{json, Value};
17use std::sync::LazyLock;
18#[derive(Default)]
19pub struct DiameterClientProtocol;
20impl DiameterClientProtocol {
21    pub fn new() -> Self {
22        Self
23    }
24}
25fn send() -> ActionDefinition {
26    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}))
27}
28fn disconnect() -> ActionDefinition {
29    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"}))
30}
31fn all() -> Vec<ActionDefinition> {
32    vec![send(), disconnect()]
33}
34pub static CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
35    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())
36});
37pub static AA_EVENT: LazyLock<EventType> = LazyLock::new(|| {
38    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())
39});
40pub static ERROR_EVENT: LazyLock<EventType> = LazyLock::new(|| {
41    EventType::new(
42        "diameter_error",
43        "Peer/session deadline, framing or correlation failed; no implicit replay.",
44        disconnect().example.clone(),
45    )
46    .with_parameters(vec![parameter(
47        "error",
48        "string",
49        "Safe bounded category without credential reflection",
50        true,
51    )])
52    .with_actions(all())
53});
54pub fn parse_action(value: Value) -> Result<Request> {
55    let mut value = codec::owned_json(value)?;
56    ensure!(
57        value["type"] == "send_diameter_aa",
58        "Diameter client request action"
59    );
60    value
61        .as_object_mut()
62        .context("Diameter action object")?
63        .remove("type");
64    let request: Request = serde_json::from_value(value)?;
65    request.validate()?;
66    Ok(request)
67}
68use anyhow::Context;
69impl Protocol for DiameterClientProtocol {
70    fn protocol_name(&self) -> &'static str {
71        "DIAMETER"
72    }
73    fn stack_name(&self) -> &'static str {
74        "ETH>IP>TCP>DIAMETER"
75    }
76    fn keywords(&self) -> Vec<&'static str> {
77        vec!["diameter", "nasreq"]
78    }
79    fn description(&self) -> &'static str {
80        "Diameter negotiated TCP peer and stateless NASREQ typed client"
81    }
82    fn example_prompt(&self) -> &'static str {
83        "Authenticate alice against a Diameter NASREQ test peer"
84    }
85    fn group_name(&self) -> &'static str {
86        "Network Management"
87    }
88    fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
89        all()
90    }
91    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
92        vec![]
93    }
94    fn get_event_types(&self) -> Vec<EventType> {
95        vec![
96            CONNECTED_EVENT.clone(),
97            AA_EVENT.clone(),
98            ERROR_EVENT.clone(),
99        ]
100    }
101    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
102        let mut p = identity_parameters();
103        p.extend(timer_parameters());
104        p
105    }
106    fn metadata(&self) -> ProtocolMetadataV2 {
107        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()
108    }
109    fn get_startup_examples(&self) -> StartupExamples {
110        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"}});
111        let mut llm = base.clone();
112        llm["instruction"] = json!("Request one stateless PAP authentication for alice");
113        let mut script = base.clone();
114        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'}]}))"}}]);
115        let mut fixed = base;
116        fixed["event_handlers"] = json!([{"event_pattern":"diameter_connected","handler":{"type":"static","actions":[send().example]}}]);
117        StartupExamples::new(llm, script, fixed)
118    }
119}
120impl Client for DiameterClientProtocol {
121    fn connect(
122        &self,
123        ctx: ConnectContext,
124    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
125    {
126        Box::pin(super::DiameterClient::connect(ctx))
127    }
128    fn execute_action(&self, value: Value) -> Result<ClientActionResult> {
129        let value = codec::owned_json(value)?;
130        if value["type"] == "disconnect" {
131            ensure!(
132                value.as_object().is_some_and(|m| m.len() == 1),
133                "Diameter disconnect only type"
134            );
135            return Ok(ClientActionResult::Disconnect);
136        }
137        if value["type"] != "send_diameter_aa" {
138            bail!("Unknown Diameter client action");
139        }
140        parse_action(value.clone())?;
141        Ok(ClientActionResult::Custom {
142            name: "diameter_command".into(),
143            data: value,
144        })
145    }
146}