1use super::codec::{self, Identity, Reply};
2use crate::llm::actions::{
3 protocol_trait::{ActionResult, Protocol, Server},
4 ActionDefinition, Parameter, ParameterDefinition, StartupExamples,
5};
6use crate::protocol::{
7 metadata::{DevelopmentState, ProtocolMetadataV2},
8 EventType, SpawnContext,
9};
10use crate::state::AppState;
11use anyhow::{bail, ensure, Result};
12use serde_json::{json, Value};
13use std::sync::LazyLock;
14#[derive(Default)]
15pub struct DiameterProtocol;
16impl DiameterProtocol {
17 pub fn new() -> Self {
18 Self
19 }
20}
21pub fn parameter(name: &str, kind: &str, description: &str, required: bool) -> Parameter {
22 Parameter {
23 name: name.into(),
24 type_hint: kind.into(),
25 description: description.into(),
26 required,
27 }
28}
29pub fn action(
30 name: &str,
31 description: &str,
32 parameters: Vec<Parameter>,
33 example: Value,
34) -> ActionDefinition {
35 ActionDefinition {
36 name: name.into(),
37 description: description.into(),
38 parameters,
39 example,
40 log_template: Some(crate::protocol::log_template::LogTemplate::new().with_info(
41 match name {
42 "respond_diameter_aa" => {
43 "Diameter stateless NASREQ authentication/authorization reply"
44 }
45 "send_diameter_aa" => {
46 "Diameter stateless NASREQ authentication/authorization requested"
47 }
48 "disconnect" => "Diameter peer disconnect and cancel pending work",
49 _ => "Diameter typed action",
50 },
51 )),
52 }
53}
54fn respond() -> ActionDefinition {
55 action("respond_diameter_aa","Choose a stateless NASREQ PAP authentication/authorization verdict. Native transport supplies correlated Session-Id/peer identities and Auth-Session-State=1. No account store or device policy application.",vec![parameter("reply","object","verdict:accept|reject(default)|error; optional reply_messages/filter_ids<=8 each, UTF8<=1024bytes; service_type1..19; session_timeout:u32. Values are reported to the peer, not applied to a NAS.",true)],json!({"type":"respond_diameter_aa","reply":{"verdict":"reject"}}))
56}
57pub static AA_EVENT: LazyLock<EventType> = LazyLock::new(|| {
58 EventType::new("diameter_aa_request","Selected RFC7155 stateless NASREQ AAR (application1): PAP authenticate-only1, authorize-only2, authenticate-and-authorize3. Input password intentionally goes to the chosen common handler; incidental diagnostics are private.",respond().example.clone()).with_parameters(vec![parameter("request","object","username,password(if authentication),auth_request_type,nas_identifier,nas_port,session_id,origin_host,origin_realm,source_addr. No asserted earlier authentication binding.",true)]).with_actions(vec![respond()])
59});
60pub fn duration_parameter(name: &str, default: u64, description: &str) -> ParameterDefinition {
61 ParameterDefinition {
62 name: name.into(),
63 type_hint: "number".into(),
64 description: description.into(),
65 required: false,
66 example: json!(default),
67 default: Some(json!(default)),
68 }
69}
70pub fn identity_parameters() -> Vec<ParameterDefinition> {
71 vec![
72 ParameterDefinition {
73 name: "origin_host".into(),
74 type_hint: "string".into(),
75 description:
76 "Required local Diameter identity, ASCII letters/digits/dot/hyphen1..255bytes"
77 .into(),
78 required: true,
79 example: json!("netget.example"),
80 default: None,
81 },
82 ParameterDefinition {
83 name: "origin_realm".into(),
84 type_hint: "string".into(),
85 description: "Required served/local realm, selected ASCII identity1..255bytes".into(),
86 required: true,
87 example: json!("example"),
88 default: None,
89 },
90 ]
91}
92pub fn timer_parameters() -> Vec<ParameterDefinition> {
93 vec![
94 duration_parameter(
95 "io_timeout_seconds",
96 codec::DEFAULT_IO_SECONDS,
97 "Whole frame/connect/read deadline1..300seconds;10s writes",
98 ),
99 duration_parameter(
100 "handler_timeout_seconds",
101 codec::DEFAULT_HANDLER_SECONDS,
102 "Common handler deadline1..300seconds; base peer traffic remains independent",
103 ),
104 duration_parameter(
105 "watchdog_interval_seconds",
106 codec::DEFAULT_WATCHDOG_SECONDS,
107 "Native watchdog idle interval1..300seconds; answer deadline uses io_timeout_seconds",
108 ),
109 ]
110}
111pub fn identity(params: &crate::protocol::StartupParams) -> Result<Identity> {
112 let id = Identity {
113 host: params.get_string("origin_host")?,
114 realm: params.get_string("origin_realm")?,
115 };
116 id.validate()?;
117 Ok(id)
118}
119impl Protocol for DiameterProtocol {
120 fn protocol_name(&self) -> &'static str {
121 "DIAMETER"
122 }
123 fn stack_name(&self) -> &'static str {
124 "ETH>IP>TCP>DIAMETER"
125 }
126 fn keywords(&self) -> Vec<&'static str> {
127 vec!["diameter", "nasreq"]
128 }
129 fn description(&self) -> &'static str {
130 "Diameter TCP base peers and stateless NASREQ PAP authentication/authorization"
131 }
132 fn example_prompt(&self) -> &'static str {
133 "Run a Diameter NASREQ endpoint with deterministic denial policy"
134 }
135 fn group_name(&self) -> &'static str {
136 "Network Management"
137 }
138 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
139 vec![]
140 }
141 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
142 vec![respond()]
143 }
144 fn get_event_types(&self) -> Vec<EventType> {
145 vec![AA_EVENT.clone()]
146 }
147 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
148 let mut p = identity_parameters();
149 p.extend(timer_parameters());
150 p.push(ParameterDefinition{name:"llm_fallback".into(),type_hint:"boolean".into(),description:"Opt unmatched credential events into model calls; explicit handlers always run. Defaultfalse rejects unmatched AAA.".into(),required:false,example:json!(true),default:Some(json!(codec::DEFAULT_LLM_FALLBACK))});
151 p
152 }
153 fn metadata(&self) -> ProtocolMetadataV2 {
154 ProtocolMetadataV2::builder().state(DevelopmentState::Experimental).well_known_port(3868).answers_on_failure().request_only("Diameter AAA answers and native peer control messages require a negotiated TCP peer; no arbitrary unsolicited payload API").max_inbound_bytes(codec::MAX_FRAME_BYTES).implementation("Native bounded RFC6733 version1 TCP headers/typed AVPs, CER/CEA, DWR/DWA, DPR/DPA; selected RFC7155 stateless NASREQ AAR/AAA application1").llm_control("Common static/script/manual/model typed verdicts, shared memory/access log; native peer lifecycle and safe fail-closed errors").e2e_testing("Required unchanged python-diameter0.9.0 Node and fiorix/go-diameter4.5.0 public SDK peers; literal real wire codec assertions, selected functional exchanges and lifecycle/failure/bounds/cancellation checks").notes("Experimental selected subset, not full RFC compliance. Trusted clear TCP3868 only: no TLS/DTLS/SCTP, peer certificate authentication or integrity. CER advertises only NASREQ application1 and no inband security; identities checked for connection consistency, not cryptographic identity. Auth-Session-State=NO_STATE_MAINTAINED(1) required; no durable account, policy or accounting store. UTF8 PAP authentication-only1, authorization-only2 and combined3; no claimed previous-session authentication binding. No stateful/multiround/CHAP/MSCHAP/EAP, STR/ASR/RAR, accounting, vendor-specific application, grouped policy, agents/proxy/relay/routing/failover or automatic replay. Optional unhandled AVPs ignored; mandatory unhandled AVPs reject with Failed-AVP; typed selected success/rejection/error outputs. Session-Id<=512bytes, username/NAS identity<=255, password<=128,16KiB frames/64AVPs,256connections,one AAA handler per peer; repeated concurrent AAA rejected busy. Handler JSON16KiB/4096nodes/depth16. Shared access record precedes any AAA acceptance. Input passwords intentionally in selected handler event/shared access retention; common incidental diagnostics hide reflections. Service-Type/Filter-Id/Session-Timeout are returned assertions, no device enforcement. Base control remains responsive while a handler is parked; whole-frame/connect and watchdog deadlines90s,handler30s,watchdogidle30s default configurable1..300s;writes10s. No fuzz/production-capture claim.").build()
155 }
156 fn get_startup_examples(&self) -> StartupExamples {
157 let base = json!({"type":"open_server","base_stack":"diameter","port":3868,"startup_params":{"origin_host":"server.example","origin_realm":"example"}});
158 let mut llm = base.clone();
159 llm["startup_params"]["llm_fallback"] = json!(true);
160 llm["instruction"] = json!("Reject every NASREQ authentication or authorization");
161 let mut script = base.clone();
162 script["event_handlers"] = json!([{"event_pattern":"diameter_aa_request","handler":{"type":"script","language":"python","code":"import json,sys\njson.load(sys.stdin)\nprint(json.dumps({'actions':[{'type':'respond_diameter_aa','reply':{'verdict':'reject'}}]}))"}}]);
163 let mut fixed = base;
164 fixed["event_handlers"] = json!([{"event_pattern":"diameter_aa_request","handler":{"type":"static","actions":[respond().example]}}]);
165 StartupExamples::new(llm, script, fixed)
166 }
167}
168impl Server for DiameterProtocol {
169 fn spawn(
170 &self,
171 ctx: SpawnContext,
172 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
173 {
174 Box::pin(super::DiameterServer::spawn(ctx))
175 }
176 fn execute_action(&self, value: Value) -> Result<ActionResult> {
177 let value = codec::owned_json(value)?;
178 ensure!(
179 value
180 .as_object()
181 .is_some_and(|m| m.len() == 2 && m.contains_key("reply")),
182 "Diameter action type/reply only"
183 );
184 if value["type"] != "respond_diameter_aa" {
185 bail!("Unknown Diameter server action");
186 }
187 let reply: Reply = serde_json::from_value(value["reply"].clone())?;
188 reply.validate()?;
189 Ok(ActionResult::Custom {
190 name: "respond_diameter_aa".into(),
191 data: serde_json::to_value(reply)?,
192 })
193 }
194}