Skip to main content

netget/server/diameter/
actions.rs

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}