Skip to main content

netget/server/openvpn/
mod.rs

1//! OpenVPN **control-channel server**.
2//!
3//! # What this is
4//!
5//! A server that speaks the OpenVPN UDP control channel: the wire format, the
6//! reliability layer that runs underneath it, a real TLS session carried inside
7//! `P_CONTROL_V1` packets, and the key-method-2 exchange that follows the
8//! handshake. A genuine `openvpn` client completes its TLS handshake against
9//! this server, sends its options string and `--auth-user-pass` credentials, and
10//! is told whether it is admitted — by the model, not by a hardcoded rule.
11//!
12//! ```text
13//! client ──> P_CONTROL_HARD_RESET_CLIENT_V2
14//! server ──> P_CONTROL_HARD_RESET_SERVER_V2        (only if the model accepts)
15//! client ──> P_CONTROL_V1 * n   TLS ClientHello
16//! server ──> P_CONTROL_V1 * n   ServerHello, certificate, Finished
17//! client ──> P_CONTROL_V1       key method 2: key material, options, user/pass
18//! server ──> P_CONTROL_V1       key method 2 answer (only if the model accepts)
19//! client ──> P_CONTROL_V1       PUSH_REQUEST                    <-- stops here
20//! ```
21//!
22//! # What this is not
23//!
24//! **It is not a VPN and never carries traffic.** `PUSH_REQUEST` is not
25//! answered, no data-channel keys are derived from the exchanged key material,
26//! there is no TUN device, and every `P_DATA_*` packet is dropped. A real client
27//! gets as far as asking for its configuration and then times out. Use
28//! `wireguard` for a tunnel.
29//!
30//! There used to be a `crypto` module here — AES-256-GCM and ChaCha20-Poly1305
31//! wrappers plus a `derive_data_keys`. It is deleted rather than kept for a
32//! future data channel, because **it was not OpenVPN's key derivation and a real
33//! client could never have decrypted anything it produced.** OpenVPN keys the
34//! data channel with the TLS 1.0 PRF over the key-method-2 random material from
35//! both peers; that function used HKDF-SHA256 with the invented label
36//! `"OpenVPN data channel keys"`, and said so in its own doc comment ("For MVP,
37//! we use a simplified HKDF-based approach"). Nothing called it, in `src/` or in
38//! `tests/`.
39//!
40//! 222 lines of plausible, unreachable, non-interoperable crypto is the exact
41//! shape that cost `wireguard` its Stable rating: it makes the protocol read as
42//! nearly finished to anyone skimming, and whoever eventually wires it up
43//! inherits a tunnel that silently talks to nobody. A data channel here starts
44//! with the PRF from RFC 5246 §5 and the `key method 2` material this server
45//! already parses — not with this file.
46//!
47//! What it *is* good for is what the control channel gives you without a
48//! tunnel: it identifies who probes UDP/1194, which OpenVPN build they run
49//! (their `IV_*` peer info), what options they expect, and — because the TLS
50//! session is real — the username and password they were going to authenticate
51//! with.
52//!
53//! # Client trust
54//!
55//! The control-channel certificate is self-signed and generated per run. A
56//! client trusts it with OpenVPN 2.6+'s `--peer-fingerprint`, whose value this
57//! server logs at startup. Nothing is written to disk and there is no shipped
58//! key.
59//!
60//! # Unsupported client options
61//!
62//! `--tls-auth`, `--tls-crypt` and `--tls-crypt-v2` wrap or displace the
63//! reliability fields this server reads. Rather than mis-parse them into
64//! plausible-looking nonsense, such frames are detected and refused with an
65//! explicit log line (see [`packet::ControlFrame::is_plain_reset`] and
66//! [`packet::Opcode::is_tls_crypt_v2`]).
67
68pub mod actions;
69pub mod keymethod;
70pub mod packet;
71pub mod peer;
72pub mod reliable;
73pub mod session;
74pub mod tls_channel;
75
76use crate::llm::action_helper::call_llm;
77use crate::llm::actions::protocol_trait::ActionResult;
78use crate::llm::ollama_client::OllamaClient;
79use crate::logging::emit::Log;
80use crate::protocol::Event;
81use crate::server::connection::ConnectionId;
82use crate::state::app_state::AppState;
83use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
84use crate::utils::clock::Instant;
85use crate::utils::wire_failure::WireFailure;
86use actions::{OpenvpnProtocol, OPENVPN_KEY_EXCHANGE_EVENT, OPENVPN_PEER_RESET_EVENT};
87use anyhow::{Context, Result};
88use keymethod::ClientKeyMethod2;
89use packet::{ControlFrame, DataFrame};
90use peer::{Peer, PeerAdmission, PeerManager};
91use session::{ControlSession, SessionEvent, SessionManager};
92use std::net::SocketAddr;
93use std::sync::Arc;
94use tokio::net::UdpSocket;
95use tokio::sync::mpsc;
96use tracing::{debug, error, trace, warn};
97
98/// Maximum number of peers tracked at once. Beyond this, resets are ignored.
99const MAX_PEERS: usize = 100;
100
101/// A peer with no traffic for this long is forgotten.
102const PEER_IDLE_TIMEOUT_SECS: u64 = 120;
103
104/// How often the idle sweep runs.
105const SWEEP_INTERVAL_SECS: u64 = 30;
106
107/// How often the reliability layer is asked whether anything needs
108/// retransmitting. Must be well below the first retransmission delay.
109const RETRANSMIT_TICK_MS: u64 = 250;
110
111/// OpenVPN control-channel server.
112pub struct OpenvpnServer {
113    socket: Arc<UdpSocket>,
114    local_addr: SocketAddr,
115    peer_manager: Arc<PeerManager>,
116    sessions: Arc<SessionManager>,
117    /// This server's OpenVPN session id, freshly random per run.
118    server_session_id: u64,
119    tls_config: Arc<rustls::ServerConfig>,
120    llm_client: Arc<OllamaClient>,
121}
122
123impl OpenvpnServer {
124    /// Bind the UDP socket and start serving.
125    ///
126    /// Returns only once the socket is bound and the control-channel TLS
127    /// configuration exists, so either failure surfaces as `Err` and
128    /// `server_startup` can mark the server `Error` instead of reporting a
129    /// server that is not listening as `Running`.
130    pub async fn spawn_with_llm_actions(
131        bind_addr: SocketAddr,
132        llm_client: Arc<OllamaClient>,
133        app_state: Arc<AppState>,
134        server_id: crate::state::ServerId,
135        status_tx: mpsc::UnboundedSender<String>,
136    ) -> Result<SocketAddr> {
137        // Certificate generation and root-store work are synchronous and can be
138        // slow enough to matter; keep them off the runtime's worker.
139        let tls = tokio::task::spawn_blocking(tls_channel::build_control_channel_tls)
140            .await
141            .context("TLS setup task for the OpenVPN control channel panicked")??;
142
143        let socket = UdpSocket::bind(bind_addr)
144            .await
145            .with_context(|| format!("Failed to bind OpenVPN UDP socket on {}", bind_addr))?;
146        let local_addr = socket.local_addr()?;
147
148        let server_session_id = rand::random::<u64>();
149
150        let log = Log::new(Some(&status_tx));
151        log.info(format!(
152            "OpenVPN control-channel server listening on {} (session id {:016x})",
153            local_addr, server_session_id
154        ));
155        // Printed so an operator can actually connect: the certificate is
156        // self-signed and generated per run, so this is the only value a client
157        // can pin it by.
158        log.info(format!(
159            "OpenVPN control channel peer fingerprint SHA256={}",
160            tls.fingerprint
161        ));
162        log.warn(
163            "OpenVPN carries no traffic: the control channel completes a TLS handshake and the \
164             key method 2 exchange, but PUSH_REQUEST is not answered, no data channel keys are \
165             derived and there is no TUN device, so no tunnel is ever established. Use WireGuard \
166             for a real VPN.",
167        );
168
169        let server = Arc::new(OpenvpnServer {
170            socket: Arc::new(socket),
171            local_addr,
172            peer_manager: Arc::new(PeerManager::new()),
173            sessions: Arc::new(SessionManager::new()),
174            server_session_id,
175            tls_config: tls.config,
176            llm_client,
177        });
178
179        // `register_server_task` keeps exactly one handle per server, so the
180        // receive loop, the idle sweep and the retransmission timer run inside a
181        // single task joined by `select!`. Registering several would silently
182        // drop all but the last handle and leak those loops past `stop_server`.
183        let loop_server = server.clone();
184        let loop_state = app_state.clone();
185        let loop_status = status_tx.clone();
186        let accept_handle = tokio::spawn(async move {
187            tokio::select! {
188                res = loop_server.clone().recv_loop(loop_state.clone(), server_id, loop_status.clone()) => {
189                    if let Err(e) = res {
190                        error!("OpenVPN receive loop stopped: {}", e);
191                    }
192                }
193                _ = loop_server.clone().sweep_loop(loop_state, server_id) => {}
194                _ = loop_server.retransmit_loop() => {}
195            }
196        });
197
198        app_state
199            .register_server_task(server_id, accept_handle)
200            .await;
201
202        log.info(format!("OpenVPN server ready on {}", local_addr));
203        Ok(local_addr)
204    }
205
206    /// Receive and dispatch UDP datagrams.
207    async fn recv_loop(
208        self: Arc<Self>,
209        app_state: Arc<AppState>,
210        server_id: crate::state::ServerId,
211        status_tx: mpsc::UnboundedSender<String>,
212    ) -> Result<()> {
213        let mut buf = vec![0u8; packet::MAX_PACKET_SIZE];
214
215        loop {
216            let (len, peer_addr) = self.socket.recv_from(&mut buf).await?;
217            let datagram = &buf[..len];
218
219            trace!("OpenVPN: {} bytes from {}", len, peer_addr);
220
221            let (opcode, key_id) = match packet::parse_opcode_byte(datagram) {
222                Ok(v) => v,
223                Err(e) => {
224                    debug!("OpenVPN: undecodable datagram from {}: {}", peer_addr, e);
225                    continue;
226                }
227            };
228
229            if opcode.is_tls_crypt_v2() {
230                Log::new(Some(&status_tx)).warn(format!(
231                    "OpenVPN: {} uses tls-crypt-v2 ({:?}), which this server cannot decode; ignoring",
232                    peer_addr, opcode
233                ));
234                continue;
235            }
236
237            if opcode.is_data() {
238                self.handle_data(datagram, peer_addr, &status_tx).await;
239                continue;
240            }
241
242            let frame = match ControlFrame::parse(datagram) {
243                Ok(f) => f,
244                Err(e) => {
245                    debug!("OpenVPN: malformed {:?} from {}: {}", opcode, peer_addr, e);
246                    continue;
247                }
248            };
249
250            if opcode.is_client_reset() {
251                self.clone()
252                    .handle_client_reset(
253                        frame,
254                        peer_addr,
255                        key_id,
256                        app_state.clone(),
257                        server_id,
258                        status_tx.clone(),
259                    )
260                    .await;
261            } else if opcode.is_ack() {
262                self.handle_ack(frame, peer_addr).await;
263            } else {
264                self.clone()
265                    .handle_control(
266                        frame,
267                        peer_addr,
268                        app_state.clone(),
269                        server_id,
270                        status_tx.clone(),
271                    )
272                    .await;
273            }
274        }
275    }
276
277    /// Drop peers that have gone quiet, so a scan cannot grow the peer table
278    /// without bound and hold the `MAX_PEERS` slots forever.
279    async fn sweep_loop(
280        self: Arc<Self>,
281        app_state: Arc<AppState>,
282        server_id: crate::state::ServerId,
283    ) {
284        let mut ticker = tokio::time::interval(std::time::Duration::from_secs(SWEEP_INTERVAL_SECS));
285        ticker.tick().await; // fires immediately; skip it
286
287        loop {
288            ticker.tick().await;
289            let expired = self
290                .peer_manager
291                .remove_idle_peers(std::time::Duration::from_secs(PEER_IDLE_TIMEOUT_SECS))
292                .await;
293            for peer in expired {
294                debug!("OpenVPN: forgetting idle peer {}", peer.addr);
295                self.sessions.remove(&peer.addr).await;
296                if peer.admission == PeerAdmission::Accepted {
297                    app_state
298                        .close_connection_on_server(server_id, peer.connection_id)
299                        .await;
300                }
301            }
302        }
303    }
304
305    /// Drive the reliability layer.
306    ///
307    /// The control channel is a reliable layer over UDP: a control packet that
308    /// is not acknowledged has to go out again, or a TLS flight spread over
309    /// several datagrams stalls forever on the first loss. This is the only
310    /// thing that makes multi-packet control messages work at all.
311    async fn retransmit_loop(self: Arc<Self>) {
312        let mut ticker =
313            tokio::time::interval(std::time::Duration::from_millis(RETRANSMIT_TICK_MS));
314        loop {
315            ticker.tick().await;
316            let (datagrams, dead) = self.sessions.drain_all(Instant::now()).await;
317            for (addr, datagram) in datagrams {
318                if let Err(e) = self.socket.send_to(&datagram, addr).await {
319                    warn!("OpenVPN: retransmission to {} failed: {}", addr, e);
320                }
321            }
322            for addr in dead {
323                debug!(
324                    "OpenVPN: {} stopped acknowledging control packets; dropping its session",
325                    addr
326                );
327                self.sessions.remove(&addr).await;
328            }
329        }
330    }
331
332    /// Handle `P_CONTROL_HARD_RESET_CLIENT_V1/V2`: the first packet of a
333    /// handshake, and the first of the two points at which policy is decided.
334    async fn handle_client_reset(
335        self: Arc<Self>,
336        frame: ControlFrame,
337        peer_addr: SocketAddr,
338        key_id: u8,
339        app_state: Arc<AppState>,
340        server_id: crate::state::ServerId,
341        status_tx: mpsc::UnboundedSender<String>,
342    ) {
343        // A plain reset carries nothing after its packet id. Anything else means
344        // the client wrapped the control channel with --tls-auth or --tls-crypt,
345        // in which case the fields we just read are not the fields it wrote.
346        if !frame.is_plain_reset() {
347            Log::new(Some(&status_tx)).warn(format!(
348                "OpenVPN: reset from {} is not a plain reset ({} payload bytes, {} ACKs); \
349                 the client is probably using --tls-auth or --tls-crypt, which is not \
350                 supported. Ignoring rather than mis-parsing it.",
351                peer_addr,
352                frame.payload.len(),
353                frame.ack_packet_ids.len()
354            ));
355            return;
356        }
357
358        let client_packet_id = frame.packet_id.unwrap_or(0);
359
360        // Resets are retransmitted every couple of seconds until the client
361        // hears back, and an LLM decision takes longer than that. Reuse the
362        // existing peer so one handshake costs at most one model call.
363        if let Some(existing) = self.peer_manager.get_peer(&peer_addr).await {
364            if existing.session_id == frame.session_id {
365                match existing.admission {
366                    PeerAdmission::Deciding => {
367                        trace!(
368                            "OpenVPN: reset retransmit from {} while a decision is in flight",
369                            peer_addr
370                        );
371                    }
372                    PeerAdmission::Accepted => {
373                        // The client missed our reply; put it back on the wire.
374                        self.sessions
375                            .with(&peer_addr, |s| s.on_client_reset_retransmit())
376                            .await;
377                        self.flush_peer(peer_addr).await;
378                    }
379                    PeerAdmission::Rejected => {
380                        trace!(
381                            "OpenVPN: ignoring reset retransmit from rejected {}",
382                            peer_addr
383                        );
384                    }
385                }
386                self.peer_manager.touch(&peer_addr).await;
387                return;
388            }
389            // Different session id from the same address: the client restarted.
390            debug!("OpenVPN: {} restarted with a new session id", peer_addr);
391            self.peer_manager.remove_peer(&peer_addr).await;
392            self.sessions.remove(&peer_addr).await;
393        }
394
395        if self.peer_manager.count().await >= MAX_PEERS {
396            Log::new(Some(&status_tx)).warn(format!(
397                "OpenVPN: peer table full ({}), ignoring reset from {}",
398                MAX_PEERS, peer_addr
399            ));
400            return;
401        }
402
403        let connection_id = ConnectionId::new(app_state.get_next_unified_id().await);
404        let peer = Peer::new(connection_id, peer_addr, frame.session_id, key_id);
405        self.peer_manager.add_peer(peer).await;
406
407        Log::new(Some(&status_tx)).info(format!(
408            "OpenVPN reset from {} (session {:016x}, {:?})",
409            peer_addr, frame.session_id, frame.opcode
410        ));
411
412        // Ask the model before answering. The decision can take seconds, so it
413        // runs off the receive loop.
414        let this = self.clone();
415        // Tracked, not detached: stop_server must abort this task too.
416        let task_owner = app_state.clone();
417        task_owner
418            .spawn_server_task(server_id, async move {
419                this.decide_and_answer(
420                    peer_addr,
421                    connection_id,
422                    frame,
423                    key_id,
424                    client_packet_id,
425                    app_state,
426                    server_id,
427                    status_tx,
428                )
429                .await;
430            })
431            .await;
432    }
433
434    /// Raise `openvpn_peer_reset` and act on the answer.
435    ///
436    /// Fails closed: only an explicit `accept_peer` produces a reply. A
437    /// `reject_peer`, an empty answer, or an LLM error all leave the peer
438    /// unanswered, and the three outcomes are logged distinctly so a refusal is
439    /// never confused with silence.
440    ///
441    /// **Silence is the protocol's refusal, not a missing feature.** Before the
442    /// TLS control channel exists OpenVPN has exactly one server-to-client
443    /// message, `P_CONTROL_HARD_RESET_SERVER_V2`, and sending it *is* admitting
444    /// the peer — there is no NAK and no error packet at this point in the
445    /// handshake. A real OpenVPN server drops what it will not admit (that is
446    /// what an HMAC failure under `--tls-auth` does). So answering on backend
447    /// failure would be the fail-open bug, not a fix for it: there is no reply
448    /// that means "try again later", only one that means "you are in".
449    ///
450    /// What the peer cannot be told, the operator is. Every outcome is logged
451    /// with a stable `decision=` token — `model_accept`, `model_reject`,
452    /// `fail_closed_no_action`, `fail_closed_llm_error` — mirroring
453    /// `src/server/radius/`, so `decision=fail_closed_` greps out every peer
454    /// the model did not actually answer for. The LLM-error line additionally
455    /// carries the [`WireFailure`] class, which is the distinction a protocol
456    /// with two error codes would have put on the wire, and the full error,
457    /// which never leaves the log.
458    #[allow(clippy::too_many_arguments)]
459    async fn decide_and_answer(
460        &self,
461        peer_addr: SocketAddr,
462        connection_id: ConnectionId,
463        frame: ControlFrame,
464        key_id: u8,
465        client_packet_id: u32,
466        app_state: Arc<AppState>,
467        server_id: crate::state::ServerId,
468        status_tx: mpsc::UnboundedSender<String>,
469    ) {
470        let event = Event::new(
471            &OPENVPN_PEER_RESET_EVENT,
472            serde_json::json!({
473                "peer_addr": peer_addr.to_string(),
474                "client_session_id": format!("{:016x}", frame.session_id),
475                "key_id": key_id,
476                "reset_type": format!("{:?}", frame.opcode),
477                "packet_id": client_packet_id,
478                "peer_count": self.peer_manager.count().await,
479            }),
480        );
481
482        let protocol = OpenvpnProtocol::new();
483        let outcome = call_llm(
484            &self.llm_client,
485            &app_state,
486            server_id,
487            Some(connection_id),
488            &event,
489            &protocol,
490        )
491        .await;
492
493        let log = Log::new(Some(&status_tx));
494        let decision = match outcome {
495            Ok(result) => {
496                for message in &result.messages {
497                    log.info(message);
498                }
499                extract_decision(&result.protocol_results, actions::PEER_DECISION_RESULT)
500            }
501            Err(e) => {
502                // The error is rendered here and nowhere else: the peer gets no
503                // bytes at all, so nothing can leak, and the operator needs the
504                // whole chain. The class is what a protocol with a retryable
505                // error code would have signalled instead.
506                let class = match WireFailure::classify(&e) {
507                    WireFailure::Overloaded => "overloaded",
508                    WireFailure::Unavailable => "unavailable",
509                };
510                log.error(format!(
511                    "OpenVPN {} decision=fail_closed_llm_error class={} - leaving it \
512                     unanswered (the protocol has no reply that is not an admission): {}",
513                    peer_addr, class, e
514                ));
515                None
516            }
517        };
518
519        match decision {
520            Some(Decision::Accept { reason }) => {
521                self.accept_peer(
522                    peer_addr,
523                    connection_id,
524                    frame.session_id,
525                    key_id,
526                    client_packet_id,
527                    reason,
528                    &app_state,
529                    server_id,
530                    &status_tx,
531                )
532                .await;
533            }
534            Some(Decision::Reject { reason }) => {
535                self.peer_manager
536                    .set_admission(&peer_addr, PeerAdmission::Rejected)
537                    .await;
538                log.info(format!(
539                    "OpenVPN {} decision=model_reject - refused, no reply sent ({})",
540                    peer_addr,
541                    reason.as_deref().unwrap_or("no reason given")
542                ));
543            }
544            None => {
545                // Distinct from an explicit rejection on purpose: this is the
546                // "nothing usable came back" path, and it must not fall through
547                // to answering the peer.
548                self.peer_manager
549                    .set_admission(&peer_addr, PeerAdmission::Rejected)
550                    .await;
551                log.warn(format!(
552                    "OpenVPN {} decision=fail_closed_no_action - the model produced neither \
553                     accept_peer nor reject_peer; leaving it unanswered",
554                    peer_addr
555                ));
556            }
557        }
558    }
559
560    /// Open a control session and send `P_CONTROL_HARD_RESET_SERVER_V2`.
561    #[allow(clippy::too_many_arguments)]
562    async fn accept_peer(
563        &self,
564        peer_addr: SocketAddr,
565        connection_id: ConnectionId,
566        client_session_id: u64,
567        key_id: u8,
568        client_packet_id: u32,
569        reason: Option<String>,
570        app_state: &Arc<AppState>,
571        server_id: crate::state::ServerId,
572        status_tx: &mpsc::UnboundedSender<String>,
573    ) {
574        let control = match ControlSession::new(
575            connection_id,
576            client_session_id,
577            self.server_session_id,
578            key_id,
579            client_packet_id,
580            self.tls_config.clone(),
581        ) {
582            Ok(s) => s,
583            Err(e) => {
584                // No session means no reply, which is the fail-closed outcome
585                // again: better an unanswered peer than an admitted one whose
586                // control channel we cannot actually run.
587                self.peer_manager
588                    .set_admission(&peer_addr, PeerAdmission::Rejected)
589                    .await;
590                Log::new(Some(status_tx)).error(format!(
591                    "OpenVPN {} decision=fail_closed_no_control_channel - could not start a \
592                     control session, so nothing was sent: {}",
593                    peer_addr, e
594                ));
595                return;
596            }
597        };
598
599        self.sessions.insert(peer_addr, control).await;
600        self.peer_manager
601            .update_peer(&peer_addr, |p| {
602                p.admission = PeerAdmission::Accepted;
603            })
604            .await;
605
606        let sent = self.flush_peer(peer_addr).await;
607        if sent == 0 {
608            Log::new(Some(status_tx)).error(format!(
609                "OpenVPN: reply to {} could not be sent; the peer is left unanswered",
610                peer_addr
611            ));
612            self.sessions.remove(&peer_addr).await;
613            self.peer_manager
614                .set_admission(&peer_addr, PeerAdmission::Rejected)
615                .await;
616            return;
617        }
618
619        let now = crate::utils::clock::Instant::now();
620        app_state
621            .add_connection_to_server(
622                server_id,
623                ConnectionState {
624                    id: connection_id,
625                    remote_addr: peer_addr,
626                    local_addr: self.local_addr,
627                    bytes_sent: sent,
628                    bytes_received: 0,
629                    packets_sent: 1,
630                    packets_received: 1,
631                    last_activity: now,
632                    status: ConnectionStatus::Active,
633                    status_changed_at: now,
634                    protocol_info: ProtocolConnectionInfo::empty(),
635                },
636            )
637            .await;
638        let _ = status_tx.send("__UPDATE_UI__".to_string());
639
640        Log::new(Some(status_tx)).info(format!(
641            "OpenVPN {} decision=model_accept - answered with HARD_RESET_SERVER_V2 ({} bytes){}",
642            peer_addr,
643            sent,
644            reason.map(|r| format!(" - {}", r)).unwrap_or_default()
645        ));
646    }
647
648    /// Send whatever this peer's control session has queued. Returns the number
649    /// of bytes that actually went out.
650    ///
651    /// The session lock is taken, the datagrams are copied out, and the guard is
652    /// dropped before a single byte touches the socket — the lock is never held
653    /// across the `await`.
654    async fn flush_peer(&self, peer_addr: SocketAddr) -> u64 {
655        let datagrams = self
656            .sessions
657            .with(&peer_addr, |s| s.drain_datagrams(Instant::now()))
658            .await
659            .unwrap_or_default();
660
661        let mut sent = 0u64;
662        for datagram in &datagrams {
663            match self.socket.send_to(datagram, peer_addr).await {
664                Ok(n) => sent = sent.saturating_add(n as u64),
665                Err(e) => {
666                    warn!("OpenVPN: send to {} failed: {}", peer_addr, e);
667                    break;
668                }
669            }
670        }
671        if sent > 0 {
672            self.peer_manager
673                .update_peer(&peer_addr, |p| p.record_sent(sent))
674                .await;
675        }
676        sent
677    }
678
679    /// Handle `P_CONTROL_V1`: acknowledge it, feed its payload to the peer's TLS
680    /// session, and act on whatever that produces.
681    async fn handle_control(
682        self: Arc<Self>,
683        frame: ControlFrame,
684        peer_addr: SocketAddr,
685        app_state: Arc<AppState>,
686        server_id: crate::state::ServerId,
687        status_tx: mpsc::UnboundedSender<String>,
688    ) {
689        match self.peer_manager.get_peer(&peer_addr).await {
690            Some(p) if p.admission == PeerAdmission::Accepted => {}
691            Some(_) => {
692                trace!("OpenVPN: control packet from unanswered peer {}", peer_addr);
693                return;
694            }
695            None => {
696                trace!("OpenVPN: control packet from unknown peer {}", peer_addr);
697                return;
698            }
699        }
700
701        let payload_len = frame.payload.len();
702        let hint = describe_tls_payload(&frame.payload);
703        trace!(
704            "OpenVPN: control payload from {}: {}",
705            peer_addr,
706            hex_prefix(&frame.payload, 64)
707        );
708
709        let events = match self
710            .sessions
711            .with(&peer_addr, |s| s.on_control_frame(frame))
712            .await
713        {
714            Some(events) => events,
715            None => {
716                trace!("OpenVPN: control packet from {} has no session", peer_addr);
717                return;
718            }
719        };
720
721        let (first_control, connection_id) = self
722            .peer_manager
723            .update_peer_returning(&peer_addr, |p| {
724                p.record_received(payload_len as u64);
725                let first = !p.saw_control_payload;
726                p.saw_control_payload = true;
727                (first, Some(p.connection_id))
728            })
729            .await
730            .unwrap_or((false, None));
731
732        // Keep the rail's counters and `last_activity` moving for as long as the peer is
733        // really there. Nothing else updated them, so an accepted peer was drawn as 0B in
734        // both directions for its whole handshake.
735        if let Some(connection_id) = connection_id {
736            app_state
737                .update_connection_stats(
738                    server_id,
739                    connection_id,
740                    Some(payload_len as u64),
741                    None,
742                    Some(1),
743                    None,
744                )
745                .await;
746        }
747
748        if first_control && payload_len > 0 {
749            Log::new(Some(&status_tx)).info(format!(
750                "OpenVPN: {} sent {} ({} bytes); feeding it to the control-channel TLS session",
751                peer_addr, hint, payload_len
752            ));
753        }
754
755        // Acknowledge and answer before doing anything that can block.
756        self.flush_peer(peer_addr).await;
757
758        for event in events {
759            self.clone()
760                .handle_session_event(event, peer_addr, &app_state, server_id, &status_tx)
761                .await;
762        }
763    }
764
765    /// React to something the control session produced.
766    async fn handle_session_event(
767        self: Arc<Self>,
768        event: SessionEvent,
769        peer_addr: SocketAddr,
770        app_state: &Arc<AppState>,
771        server_id: crate::state::ServerId,
772        status_tx: &mpsc::UnboundedSender<String>,
773    ) {
774        let log = Log::new(Some(status_tx));
775        match event {
776            SessionEvent::TlsEstablished { version, cipher } => {
777                log.info(format!(
778                    "OpenVPN {} control channel TLS handshake completed ({}, {})",
779                    peer_addr, version, cipher
780                ));
781            }
782            SessionEvent::ClientKeyExchange(km2) => {
783                let connection_id = self
784                    .sessions
785                    .with(&peer_addr, |s| s.connection_id)
786                    .await
787                    .unwrap_or_else(|| ConnectionId::new(0));
788
789                log.info(format!(
790                    "OpenVPN {} sent key method 2: username {:?}, {} bytes of peer info",
791                    peer_addr,
792                    km2.username,
793                    km2.peer_info.len()
794                ));
795                // The password is what a honeypot is here to capture, but it is
796                // still a credential: it goes to the model and to DEBUG, never
797                // to INFO or to the wire.
798                debug!(
799                    "OpenVPN {} credentials: username={:?} password={:?} options={:?}",
800                    peer_addr, km2.username, km2.password, km2.options
801                );
802
803                let this = self.clone();
804                let state = app_state.clone();
805                let status = status_tx.clone();
806                // Tracked, not detached: stop_server must abort this task too.
807                let task_owner = app_state.clone();
808                task_owner
809                    .spawn_server_task(server_id, async move {
810                        this.decide_key_exchange(
811                            peer_addr,
812                            connection_id,
813                            km2,
814                            state,
815                            server_id,
816                            status,
817                        )
818                        .await;
819                    })
820                    .await;
821            }
822            SessionEvent::ControlMessage(message) => {
823                if message.starts_with("PUSH_REQUEST") {
824                    log.warn(format!(
825                        "OpenVPN {} sent PUSH_REQUEST, which this server does not answer: it \
826                         derives no data channel keys and has no TUN device, so the client will \
827                         time out here rather than build a tunnel",
828                        peer_addr
829                    ));
830                } else {
831                    log.info(format!(
832                        "OpenVPN {} control message: {}",
833                        peer_addr,
834                        crate::utils::truncate::truncate_for_log(&message, 200)
835                    ));
836                }
837            }
838            SessionEvent::TlsFailed(reason) => {
839                log.warn(format!(
840                    "OpenVPN {} control-channel TLS session failed: {}",
841                    peer_addr, reason
842                ));
843                // rustls may have queued an alert; send it, then let the idle
844                // sweep collect the session.
845                self.flush_peer(peer_addr).await;
846            }
847        }
848    }
849
850    /// Raise `openvpn_client_key_exchange` and act on the answer.
851    ///
852    /// Fails closed exactly like the reset decision: only `accept_key_exchange`
853    /// causes the server's key-method-2 answer to be written. A refusal, an
854    /// empty answer and an LLM error all leave the client with nothing, and are
855    /// logged under distinct `decision=` tokens.
856    ///
857    /// Silence is again the refusal. OpenVPN's `AUTH_FAILED` is a control
858    /// message the client only looks for **after** it has read the server's
859    /// key-method-2 answer; sent before it, the client parses it as that answer
860    /// and reports a protocol error rather than a rejection. So there is no
861    /// refusal message available at this point that the peer would understand,
862    /// and sending our key material to say "no" would be the fail-open bug in a
863    /// new place.
864    async fn decide_key_exchange(
865        self: Arc<Self>,
866        peer_addr: SocketAddr,
867        connection_id: ConnectionId,
868        km2: Box<ClientKeyMethod2>,
869        app_state: Arc<AppState>,
870        server_id: crate::state::ServerId,
871        status_tx: mpsc::UnboundedSender<String>,
872    ) {
873        // `pre_master`, `random1` and `random2` are key material and are
874        // deliberately absent from the event: they are secrets, and no decision
875        // can be made from 112 random bytes.
876        let event = Event::new(
877            &OPENVPN_KEY_EXCHANGE_EVENT,
878            serde_json::json!({
879                "peer_addr": peer_addr.to_string(),
880                "username": km2.username,
881                "password": km2.password,
882                "has_credentials": !km2.username.is_empty(),
883                "options": km2.options,
884                "peer_info": keymethod::peer_info_map(&km2.peer_info),
885            }),
886        );
887
888        let protocol = OpenvpnProtocol::new();
889        let outcome = call_llm(
890            &self.llm_client,
891            &app_state,
892            server_id,
893            Some(connection_id),
894            &event,
895            &protocol,
896        )
897        .await;
898
899        let log = Log::new(Some(&status_tx));
900        let decision = match outcome {
901            Ok(result) => {
902                for message in &result.messages {
903                    log.info(message);
904                }
905                extract_decision(
906                    &result.protocol_results,
907                    actions::KEY_EXCHANGE_DECISION_RESULT,
908                )
909            }
910            Err(e) => {
911                let class = match WireFailure::classify(&e) {
912                    WireFailure::Overloaded => "overloaded",
913                    WireFailure::Unavailable => "unavailable",
914                };
915                log.error(format!(
916                    "OpenVPN {} decision=fail_closed_llm_error class={} - key exchange left \
917                     unanswered (no message at this point in the handshake means anything but \
918                     'admitted'): {}",
919                    peer_addr, class, e
920                ));
921                None
922            }
923        };
924
925        match decision {
926            Some(Decision::Accept { reason }) => {
927                let written = self
928                    .sessions
929                    .with(&peer_addr, |s| s.write_key_method_2_answer(&km2.options))
930                    .await;
931                match written {
932                    Some(Ok(options)) => {
933                        self.flush_peer(peer_addr).await;
934                        log.info(format!(
935                            "OpenVPN {} decision=model_accept - answered key method 2 with \
936                             options {:?}{}",
937                            peer_addr,
938                            crate::utils::truncate::truncate_for_log(&options, 160),
939                            reason.map(|r| format!(" - {}", r)).unwrap_or_default()
940                        ));
941                    }
942                    Some(Err(e)) => log.error(format!(
943                        "OpenVPN {} decision=fail_closed_write_error - the key method 2 answer \
944                         could not be written: {}",
945                        peer_addr, e
946                    )),
947                    None => log.warn(format!(
948                        "OpenVPN {} decision=fail_closed_no_session - the control session was \
949                         gone before the key method 2 answer could be written",
950                        peer_addr
951                    )),
952                }
953            }
954            Some(Decision::Reject { reason }) => {
955                log.info(format!(
956                    "OpenVPN {} decision=model_reject - key exchange refused, nothing sent ({})",
957                    peer_addr,
958                    reason.as_deref().unwrap_or("no reason given")
959                ));
960                self.sessions.remove(&peer_addr).await;
961            }
962            None => {
963                log.warn(format!(
964                    "OpenVPN {} decision=fail_closed_no_action - the model produced neither \
965                     accept_key_exchange nor reject_key_exchange; the key exchange is left \
966                     unanswered",
967                    peer_addr
968                ));
969                self.sessions.remove(&peer_addr).await;
970            }
971        }
972    }
973
974    /// Handle `P_ACK_V1` from a client: clear what it acknowledges from the
975    /// retransmission queue.
976    async fn handle_ack(&self, frame: ControlFrame, peer_addr: SocketAddr) {
977        trace!(
978            "OpenVPN: ACK from {} for {:?}",
979            peer_addr,
980            frame.ack_packet_ids
981        );
982        self.sessions
983            .with(&peer_addr, |s| s.on_ack_frame(&frame))
984            .await;
985        self.peer_manager.touch(&peer_addr).await;
986    }
987
988    /// Handle `P_DATA_V1/V2`.
989    ///
990    /// The key-method-2 exchange carries the material a real server would expand
991    /// into data-channel keys, but this server derives none: there is no data
992    /// channel and no TUN device to carry the plaintext to. Every data packet is
993    /// therefore unopenable by construction. Say so once per peer instead of
994    /// pretending to decrypt.
995    async fn handle_data(
996        &self,
997        datagram: &[u8],
998        peer_addr: SocketAddr,
999        status_tx: &mpsc::UnboundedSender<String>,
1000    ) {
1001        let frame = match DataFrame::parse(datagram) {
1002            Ok(f) => f,
1003            Err(e) => {
1004                debug!("OpenVPN: malformed data packet from {}: {}", peer_addr, e);
1005                return;
1006            }
1007        };
1008
1009        let first = self
1010            .peer_manager
1011            .update_peer_returning(&peer_addr, |p| {
1012                p.record_received(frame.payload.len() as u64);
1013                let first = !p.saw_data_packet;
1014                p.saw_data_packet = true;
1015                first
1016            })
1017            .await
1018            .unwrap_or(false);
1019
1020        debug!(
1021            "OpenVPN: dropping {:?} from {} ({} ciphertext bytes, peer id {:?}): no data \
1022             channel keys exist",
1023            frame.opcode,
1024            peer_addr,
1025            frame.payload.len(),
1026            frame.peer_id
1027        );
1028
1029        if first {
1030            Log::new(Some(status_tx)).warn(format!(
1031                "OpenVPN: {} sent a data packet, but this server derives no data channel keys, \
1032                 so it cannot be decrypted and is dropped",
1033                peer_addr
1034            ));
1035        }
1036    }
1037
1038    /// Peers currently answered by this server.
1039    pub async fn accepted_peers(&self) -> Vec<SocketAddr> {
1040        self.peer_manager
1041            .get_all_peers()
1042            .await
1043            .into_iter()
1044            .filter(|p| p.admission == PeerAdmission::Accepted)
1045            .map(|p| p.addr)
1046            .collect()
1047    }
1048}
1049
1050/// What the model decided.
1051enum Decision {
1052    Accept { reason: Option<String> },
1053    Reject { reason: Option<String> },
1054}
1055
1056/// Pull the first accept/reject decision with the given result name out of the
1057/// executed action results.
1058///
1059/// Returns `None` when the model produced neither, which callers must treat as
1060/// a refusal rather than a default.
1061fn extract_decision(results: &[ActionResult], wanted: &str) -> Option<Decision> {
1062    fn walk(results: &[ActionResult], wanted: &str, out: &mut Option<Decision>) {
1063        for result in results {
1064            if out.is_some() {
1065                return;
1066            }
1067            match result {
1068                ActionResult::Custom { name, data } if name == wanted => {
1069                    let reason = data
1070                        .get("reason")
1071                        .and_then(|v| v.as_str())
1072                        .map(|s| s.to_string());
1073                    match data.get("accept").and_then(|v| v.as_bool()) {
1074                        Some(true) => *out = Some(Decision::Accept { reason }),
1075                        Some(false) => *out = Some(Decision::Reject { reason }),
1076                        None => {}
1077                    }
1078                }
1079                ActionResult::Multiple(inner) => walk(inner, wanted, out),
1080                _ => {}
1081            }
1082        }
1083    }
1084
1085    let mut out = None;
1086    walk(results, wanted, &mut out);
1087    out
1088}
1089
1090/// Best-effort description of a TLS record, for logs only.
1091fn describe_tls_payload(payload: &[u8]) -> &'static str {
1092    match payload.first() {
1093        Some(0x16) => "a TLS handshake record (ClientHello)",
1094        Some(0x14) => "a TLS change-cipher-spec record",
1095        Some(0x15) => "a TLS alert record",
1096        Some(0x17) => "a TLS application-data record",
1097        Some(_) => "a non-TLS control payload",
1098        None => "an empty control payload",
1099    }
1100}
1101
1102/// Hex-dump at most `limit` bytes, for TRACE logging.
1103fn hex_prefix(data: &[u8], limit: usize) -> String {
1104    let shown = &data[..data.len().min(limit)];
1105    let mut s = String::with_capacity(shown.len() * 2 + 16);
1106    for b in shown {
1107        s.push_str(&format!("{:02x}", b));
1108    }
1109    if data.len() > shown.len() {
1110        s.push_str(&format!("... ({} bytes total)", data.len()));
1111    }
1112    s
1113}