1pub mod actions;
4pub mod tls;
5
6use crate::logging::emit::Log;
7use crate::protocol::SpawnContext;
8use crate::server::{
9 accept_bounded::{accept_bounded, ConnectionLimiter, DEFAULT_MAX_CONNECTIONS},
10 connection::ConnectionId,
11 radius::{packet, packet::RadiusPacket, RadiusServer},
12};
13use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
14use anyhow::{ensure, Context, Result};
15use std::{net::SocketAddr, sync::Arc, time::Duration};
16use tokio::{io::AsyncWriteExt, net::TcpListener, sync::mpsc};
17
18pub const IDLE_TIMEOUT: Duration = Duration::from_secs(300);
20pub const MAX_IDLE_TIMEOUT_SECS: u64 = 86_400;
21pub const MAX_IN_FLIGHT: usize = 32;
23
24pub async fn spawn(ctx: SpawnContext) -> Result<SocketAddr> {
25 let params = ctx.startup_params.as_ref();
26 let get = |k: &str| -> Result<Option<String>> {
27 Ok(params
28 .map(|p| p.get_optional_string(k))
29 .transpose()?
30 .flatten())
31 };
32 let config = tls::server_config(
33 get("certificate_file")?.as_deref(),
34 get("private_key_file")?.as_deref(),
35 get("ca_file")?.as_deref(),
36 )?;
37 let mutual = get("ca_file")?.is_some();
38 let secret = get("shared_secret")?.unwrap_or_else(|| tls::DEFAULT_SECRET.to_string());
39 ensure!(!secret.is_empty(), "shared_secret must not be empty");
40 let secret = Arc::new(secret.into_bytes());
41 let idle_secs = params
42 .map(|p| p.get_optional_u64("idle_timeout_secs"))
43 .transpose()?
44 .flatten()
45 .unwrap_or(IDLE_TIMEOUT.as_secs());
46 ensure!(
47 (1..=MAX_IDLE_TIMEOUT_SECS).contains(&idle_secs),
48 "idle_timeout_secs must be between 1 and {MAX_IDLE_TIMEOUT_SECS}"
49 );
50 let idle = Duration::from_secs(idle_secs);
51 let listener = TcpListener::bind(ctx.legacy_listen_addr())
52 .await
53 .context("RadSec failed to bind")?;
54 let local = listener.local_addr()?;
55 Log::new(Some(&ctx.status_tx)).info(format!(
56 "RadSec listening on {local} ({})",
57 if mutual {
58 "client certificates required"
59 } else {
60 "no client certificate required"
61 }
62 ));
63 let acceptor = tokio_rustls::TlsAcceptor::from(config);
64 let state = ctx.state.clone();
65 let server_id = ctx.server_id;
66 let accept = tokio::spawn(async move {
67 let limiter = ConnectionLimiter::new(DEFAULT_MAX_CONNECTIONS);
68 loop {
69 let Ok((stream, peer, permit)) =
70 accept_bounded(&listener, &limiter, b"", "RadSec", Some(&ctx.status_tx)).await
71 else {
72 break;
73 };
74 let id = ConnectionId::new(ctx.state.get_next_unified_id().await);
75 let now = crate::utils::clock::Instant::now();
76 ctx.state
77 .add_connection_to_server(
78 server_id,
79 ConnectionState {
80 id,
81 remote_addr: peer,
82 local_addr: local,
83 bytes_sent: 0,
84 bytes_received: 0,
85 packets_sent: 0,
86 packets_received: 0,
87 last_activity: now,
88 status: ConnectionStatus::Active,
89 status_changed_at: now,
90 protocol_info: ProtocolConnectionInfo::empty(),
91 },
92 )
93 .await;
94 let child = ctx.clone();
95 let acceptor = acceptor.clone();
96 let secret = secret.clone();
97 ctx.state
98 .spawn_server_task(server_id, async move {
99 let _permit = permit;
100 let log = Log::new(Some(&child.status_tx));
101 match tokio::time::timeout(tls::HANDSHAKE_TIMEOUT, acceptor.accept(stream))
102 .await
103 {
104 Ok(Ok(tls)) => {
105 if let Err(e) = session(&child, id, peer, tls, secret, idle).await {
106 log.warn(format!(
107 "RadSec connection {id} from {peer} ended: {e:#}"
108 ));
109 }
110 }
111 Ok(Err(e)) => log.warn(format!("RadSec handshake with {peer} failed: {e}")),
112 Err(_) => log.warn(format!("RadSec handshake with {peer} timed out")),
113 }
114 child
115 .state
116 .update_connection_status(server_id, id, ConnectionStatus::Closed)
117 .await;
118 let _ = child.status_tx.send("__UPDATE_UI__".into());
119 })
120 .await;
121 }
122 });
123 state.register_server_task(server_id, accept).await;
124 Ok(local)
125}
126
127async fn session(
130 ctx: &SpawnContext,
131 id: ConnectionId,
132 peer: SocketAddr,
133 tls: tokio_rustls::server::TlsStream<tokio::net::TcpStream>,
134 secret: Arc<Vec<u8>>,
135 idle: Duration,
136) -> Result<()> {
137 let (reader, mut writer) = tokio::io::split(tls);
138 let (frames_tx, mut frames) = mpsc::channel(8);
139 let reader_task = tokio::spawn(packet::read_frames(reader, frames_tx));
140 let (replies_tx, mut replies) = mpsc::channel::<Vec<u8>>(MAX_IN_FLIGHT);
141 let slots = Arc::new(tokio::sync::Semaphore::new(MAX_IN_FLIGHT));
142 let mut answering = tokio::task::JoinSet::new();
143 let result = loop {
144 tokio::select! {
145 frame = tokio::time::timeout(idle, frames.recv()) => {
146 let frame = match frame {
147 Err(_) => break Ok(()),
148 Ok(None) => break Ok(()),
149 Ok(Some(Err(e))) if e.kind() == std::io::ErrorKind::UnexpectedEof => break Ok(()),
150 Ok(Some(Err(e))) => break Err(anyhow::anyhow!(e)),
151 Ok(Some(Ok(f))) => f,
152 };
153 ctx.state
154 .update_connection_stats(ctx.server_id, id, Some(frame.len() as u64), None, Some(1), None)
155 .await;
156 let request = match RadiusPacket::decode(&frame) {
157 Ok(p) => p,
158 Err(e) => {
159 Log::new(Some(&ctx.status_tx)).warn(format!("RadSec dropped a packet from {peer}: {e}"));
160 continue;
161 }
162 };
163 if let Err(e) = packet::verify_request_message_authenticator(&frame, &secret) {
164 Log::new(Some(&ctx.status_tx)).warn(format!(
165 "RadSec dropped {} id={} from {peer}: {e}",
166 packet::code_name(request.code),
167 request.identifier
168 ));
169 continue;
170 }
171 if request.code == packet::CODE_ACCOUNTING_REQUEST {
172 if let Err(e) = packet::verify_accounting_request(&request, &secret) {
173 Log::new(Some(&ctx.status_tx)).warn(format!(
174 "RadSec dropped Accounting-Request id={} from {peer}: {e}", request.identifier
175 ));
176 continue;
177 }
178 }
179 let Ok(slot) = slots.clone().acquire_owned().await else { break Ok(()) };
180 let (llm, state, status, secret, tx) =
181 (ctx.llm_client.clone(), ctx.state.clone(), ctx.status_tx.clone(), secret.clone(), replies_tx.clone());
182 let server_id = ctx.server_id;
183 answering.spawn(async move {
184 let _slot = slot;
185 if let Some(reply) =
186 RadiusServer::answer(request, peer, Some(id), llm, state, status, server_id, secret).await
187 {
188 let _ = tx.send(reply).await;
189 }
190 });
191 }
192 Some(reply) = replies.recv() => {
193 writer.write_all(&reply).await?;
194 writer.flush().await?;
195 ctx.state
196 .update_connection_stats(ctx.server_id, id, None, Some(reply.len() as u64), None, Some(1))
197 .await;
198 }
199 Some(_) = answering.join_next(), if !answering.is_empty() => {}
200 }
201 };
202 drop(replies_tx);
204 while answering.join_next().await.is_some() {}
205 while let Ok(reply) = replies.try_recv() {
206 let _ = writer.write_all(&reply).await;
207 }
208 let _ = writer.shutdown().await;
209 reader_task.abort();
210 result
211}