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}