Skip to main content

netget/server/radsec/
actions.rs

1//! RadSec (RFC 6614) offers the model exactly RADIUS's vocabulary: the same events, the same
2//! reply actions, the same fail-closed rule. Only the transport differs.
3use crate::llm::actions::{
4    protocol_trait::{ActionResult, Protocol, Server},
5    ActionDefinition, ParameterDefinition, StartupExamples,
6};
7use crate::protocol::{EventType, SpawnContext};
8use crate::server::radius::RadiusProtocol;
9use crate::state::app_state::AppState;
10use anyhow::Result;
11use serde_json::{json, Value};
12
13#[derive(Default)]
14pub struct RadsecProtocol;
15impl RadsecProtocol {
16    pub fn new() -> Self {
17        Self
18    }
19}
20
21pub(crate) fn tls_parameters(server: bool) -> Vec<ParameterDefinition> {
22    let own = if server { "server" } else { "client" };
23    vec![
24        ParameterDefinition {
25            name: "certificate_file".into(),
26            type_hint: "string".into(),
27            description: format!(
28                "PEM file with this {own}'s certificate chain{}",
29                if server {
30                    "; with private_key_file. Omit both for a fresh self-signed certificate"
31                } else {
32                    "; with private_key_file, presented when the server asks for one"
33                }
34            ),
35            required: false,
36            example: json!(format!("/etc/netget/radsec-{own}.pem")),
37            default: None,
38        },
39        ParameterDefinition {
40            name: "private_key_file".into(),
41            type_hint: "string".into(),
42            description: format!("PEM file with the private key of certificate_file ({own} side)"),
43            required: false,
44            example: json!(format!("/etc/netget/radsec-{own}.key")),
45            default: None,
46        },
47        ParameterDefinition {
48            name: "ca_file".into(),
49            type_hint: "string".into(),
50            description: if server {
51                "PEM file of the CA client certificates must chain to; when set, a client without one is refused at the handshake".into()
52            } else {
53                "PEM file of the CA the server's certificate must chain to (required: the server is always verified)".into()
54            },
55            required: !server,
56            example: json!("/etc/netget/radsec-ca.pem"),
57            default: None,
58        },
59        ParameterDefinition {
60            name: "shared_secret".into(),
61            type_hint: "string".into(),
62            description: "RADIUS shared secret inside the TLS session; RFC 6614 fixes it as radsec"
63                .into(),
64            required: false,
65            example: json!("radsec"),
66            default: Some(json!(super::tls::DEFAULT_SECRET)),
67        },
68    ]
69}
70
71impl Protocol for RadsecProtocol {
72    fn protocol_name(&self) -> &'static str {
73        "RadSec"
74    }
75    fn stack_name(&self) -> &'static str {
76        "ETH>IP>TCP>TLS>RadSec"
77    }
78    fn keywords(&self) -> Vec<&'static str> {
79        vec!["radsec", "radius over tls", "radius/tls", "rfc 6614"]
80    }
81    fn description(&self) -> &'static str {
82        "RadSec server: RADIUS authentication and accounting over TLS (RFC 6614), mutual TLS when a CA is given"
83    }
84    fn get_async_actions(&self, state: &AppState) -> Vec<ActionDefinition> {
85        RadiusProtocol::new().get_async_actions(state)
86    }
87    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
88        RadiusProtocol::new().get_sync_actions()
89    }
90    fn get_event_types(&self) -> Vec<EventType> {
91        RadiusProtocol::new().get_event_types()
92    }
93    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
94        let mut p = tls_parameters(true);
95        p.push(ParameterDefinition {
96            name: "idle_timeout_secs".into(),
97            type_hint: "number".into(),
98            description: "Seconds a connection may stay silent before it is closed (1..=86400)"
99                .into(),
100            required: false,
101            example: json!(600),
102            default: Some(json!(super::IDLE_TIMEOUT.as_secs())),
103        });
104        p
105    }
106    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
107        use crate::protocol::metadata::*;
108        ProtocolMetadataV2::builder()
109            .state(DevelopmentState::Experimental)
110            .privilege_requirement(PrivilegeRequirement::None)
111            .well_known_port(2083)
112            .implementation("RFC 6614 over tokio-rustls: RADIUS packets framed by their own Length field on a TLS stream, answered by the RADIUS server's decision path (RadiusServer::answer), so the codec, the Response Authenticator and the fail-closed rule are RADIUS's own; mutual TLS when ca_file is set")
113            .llm_control("The same as RADIUS: the authorization decision (Accept, Reject, Challenge), the reply attributes, and acknowledging accounting")
114            .e2e_testing("tests/server/radsec: radsecproxy (C) and FreeRADIUS proxying to NetGet as a TLS home server, each driven by radclient through it; raw TLS for framing, bounds and the client-certificate requirement")
115            .notes("FAILS CLOSED as RADIUS does: no usable answer is an Access-Reject logged decision=fail_closed_*. Requests on one connection are answered concurrently (32 at a time) and may be answered out of order, which RFC 6614 allows. No retransmission handling is needed over TCP; a duplicate identifier is answered again. A length outside 20..=4096 closes the connection: a stream cannot be resynchronised. Status-Server keepalives are answered like any other request.")
116            .request_only("Every packet NetGet writes answers a RADIUS request; no CoA or Disconnect-Request is sent")
117            .answers_on_failure()
118            .max_inbound_bytes(crate::server::radius::packet::MAX_PACKET_LEN)
119            .build()
120    }
121    fn example_prompt(&self) -> &'static str {
122        "RadSec server on 2083 that accepts alice with password wonderland and rejects everyone else"
123    }
124    fn get_startup_examples(&self) -> StartupExamples {
125        let base = json!({"type":"open_server","base_stack":"radsec","port":2083,
126            "instruction":"Accept alice with password wonderland; reject everyone else"});
127        let mut scripted = base.clone();
128        scripted["event_handlers"] = json!([{"event_pattern":"radius_access_request","handler":{"type":"script","language":"python",
129            "code":"respond([{'type': 'send_access_accept'}] if event['user_name'] == 'alice' and event.get('password') == 'wonderland' else [{'type': 'send_access_reject', 'reply_message': 'denied'}])"}}]);
130        let mut static_example = base.clone();
131        static_example["event_handlers"] = json!([{"event_pattern":"radius_access_request","handler":{"type":"static","actions":[{"type":"send_access_reject","reply_message":"This server denies everyone"}]}}]);
132        StartupExamples::new(base, scripted, static_example)
133    }
134    fn group_name(&self) -> &'static str {
135        "Application"
136    }
137}
138
139impl Server for RadsecProtocol {
140    fn spawn(
141        &self,
142        ctx: SpawnContext,
143    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
144    {
145        Box::pin(super::spawn(ctx))
146    }
147    fn execute_action(&self, v: Value) -> Result<ActionResult> {
148        RadiusProtocol::new().execute_action(v)
149    }
150}