Skip to main content

netget/server/tls/
mod.rs

1//! TLS server implementation
2//!
3//! Provides a generic TLS transport layer that allows the LLM to implement
4//! custom application protocols on top of encrypted connections.
5
6pub mod actions;
7
8use anyhow::{Context, Result};
9use bytes::Bytes;
10use std::collections::HashMap;
11use std::net::SocketAddr;
12use std::sync::Arc;
13use tokio::io::{AsyncReadExt, AsyncWriteExt};
14use tokio::net::{TcpListener, TcpStream};
15use tokio::sync::{mpsc, Mutex};
16use tokio_rustls::TlsAcceptor;
17use tracing::{debug, error, info};
18
19use super::connection::ConnectionId;
20use crate::llm::action_helper::call_llm;
21use crate::llm::ollama_client::OllamaClient;
22use crate::llm::ActionResult;
23use crate::logging::emit::Log;
24use crate::protocol::Event;
25use crate::server::TlsProtocol;
26use crate::state::app_state::AppState;
27use crate::utils::WireFailure;
28use actions::{TLS_CONNECTION_OPENED_EVENT, TLS_DATA_RECEIVED_EVENT};
29
30/// How long a peer has to complete the TLS handshake.
31///
32/// **One constant used to govern this wait and the wait for the first application record**, on
33/// the reasoning that from the peer's side they are one condition: it holds a socket and has
34/// produced nothing usable. They are not one condition, because they face different peers.
35/// This one faces a peer that has opened a TCP socket and not yet sent a ClientHello; the
36/// other faces a peer that has *completed* a handshake. A single number cannot be right for
37/// both, and the number that was right here was wrong there.
38///
39/// Nothing on this side of the split has changed. `acceptor.accept()` was once unbounded, so a
40/// peer that connected and never sent a ClientHello held a task and a rustls state machine for
41/// as long as it liked — cheaper for an attacker than a completed connection. Every real
42/// client sends ClientHello immediately and finishes in one round-trip, and NetGet's own TLS
43/// client is no exception: `src/client/tls/mod.rs` runs `TlsConnector::connect` inside its own
44/// `connect()`, before any model turn or keystroke, so a minute is far beyond what any peer
45/// worth waiting for needs, even on a bad link. Nothing in this phase involves the model, so
46/// the deadline may cover the whole of it.
47///
48/// Overridable per server with `handshake_timeout_secs`.
49const HANDSHAKE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
50
51/// How long a handshaked peer has to send its first *application* record.
52///
53/// **This was [`HANDSHAKE_READ_TIMEOUT`]'s 60 seconds, and the peer it faces is a different
54/// one.** The handshake is done here: the peer has proved it speaks TLS and has paid for a
55/// rustls state machine of its own. What it has not done is say anything on top — and TLS is a
56/// carrier, so whether it *should* have by now is a property of the application riding on it,
57/// which this server does not know.
58///
59/// What it does know is who is usually there. `src/client/tls/mod.rs` completes the handshake
60/// inside `connect()` and then writes **no application bytes at all** unprompted: every byte
61/// comes from an action, and a client created from the dashboard's `[ + tls client ]` is
62/// routed `tls_client_connected` → static-with-no-actions and then `*` → manual
63/// (`src/tui/modal/form.rs`). So it connects, handshakes, is answered with nothing, and waits
64/// for a person to type into `[ send message ]`. At 60 seconds this server hung up on the
65/// operator's own client while they were still looking at it — and, unlike the handshake
66/// phase, there was nothing the peer could have done about it.
67///
68/// 300 seconds is the window a `manual` rule gives a human to answer one event
69/// (`src/state/intercepts.rs`), which is the number this product already uses for how long
70/// someone might take, and it is also this server's own [`IDLE_AFTER_DATA_TIMEOUT`] — so a
71/// hand-driven session is now bounded the same way before its first record as after it.
72///
73/// What it costs: a peer that completed a handshake and then said nothing holds a socket, a
74/// task, an `AppState` row and one of [`MAX_CONNECTIONS`] slots for 300 seconds rather than
75/// 60 — a fivefold rise in how long one such slot is held, not a removal of the bound, and a
76/// peer over the cap is still answered [`CONNECTION_CAP_REFUSAL`]. Note that it is also
77/// strictly more expensive for the attacker than the handshake phase, which is untouched: to
78/// reach this bound at all it must complete a real TLS handshake. A listener exposed to
79/// strangers should set `first_byte_timeout_secs` low; 60 is the old value and remains a sound
80/// choice for one.
81const FIRST_RECORD_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
82
83/// The three read deadlines for one listener, resolved from its startup parameters.
84///
85/// Carried as one value so threading them from `spawn_with_llm_actions` into the connection
86/// task costs one argument rather than three.
87#[derive(Clone, Copy)]
88struct ReadDeadlines {
89    /// [`HANDSHAKE_READ_TIMEOUT`], or this server's `handshake_timeout_secs`.
90    handshake: std::time::Duration,
91    /// [`FIRST_RECORD_READ_TIMEOUT`], or this server's `first_byte_timeout_secs`.
92    first_byte: std::time::Duration,
93    /// [`IDLE_AFTER_DATA_TIMEOUT`], or this server's `idle_timeout_secs`.
94    idle: std::time::Duration,
95}
96
97impl ReadDeadlines {
98    fn resolve(
99        handshake_secs: Option<u64>,
100        first_byte_secs: Option<u64>,
101        idle_secs: Option<u64>,
102    ) -> Self {
103        Self {
104            handshake: handshake_secs
105                .map(std::time::Duration::from_secs)
106                .unwrap_or(HANDSHAKE_READ_TIMEOUT),
107            first_byte: first_byte_secs
108                .map(std::time::Duration::from_secs)
109                .unwrap_or(FIRST_RECORD_READ_TIMEOUT),
110            idle: idle_secs
111                .map(std::time::Duration::from_secs)
112                .unwrap_or(IDLE_AFTER_DATA_TIMEOUT),
113        }
114    }
115}
116
117/// How long to wait for a *further* record once the peer has sent application data.
118///
119/// TLS is a carrier, not an application: whatever rides on it decides what "idle" means, and
120/// this server cannot know. Five minutes is therefore chosen to sit well above any
121/// request/response turnaround an operator would run over it and well below an unbounded hold.
122/// Because the clock counts only *silence* — see the `activity` check at the read site — a peer
123/// waiting on a slow answer of ours is never counted against it.
124///
125/// Overridable per server with `idle_timeout_secs`, for the same reason the two before it are:
126/// the right value is a property of whatever rides on this carrier, and only the operator
127/// knows that.
128const IDLE_AFTER_DATA_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
129
130/// Concurrent connections this server admits.
131const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
132
133/// Most application data this server will hold for one connection while an answer is in flight.
134///
135/// `ConnectionData::queued_data` exists because a peer may keep writing while the model is
136/// being asked what to say — the Idle → Processing → Accumulating machine every protocol here
137/// hand-rolls. It had no ceiling, so the peer decided how much memory one connection cost, and
138/// it decided it *while the answer it is waiting for has not arrived*: the window is a whole
139/// LLM round-trip, or, where a `manual` rule parks the record for a human
140/// (`src/state/intercepts.rs`), **300 seconds by default**, times
141/// [`MAX_CONNECTIONS`]. Neither authentication nor a model call stands in front of it: the
142/// first record a stranger sends opens the window.
143///
144/// 1 MiB is 64 maximum-size TLS records (RFC 8446 caps a plaintext record at 2^14 bytes), and
145/// the number is taken from the protocol's own framing rather than from a guess about the
146/// application: the queue holds what arrived *while one answer was being composed*, and sixty
147/// four full records is already far more than any request a model is going to be asked to read.
148/// Past that the peer is not waiting for an answer, it is filling memory.
149pub const MAX_QUEUED_BYTES: usize = 1024 * 1024;
150
151/// How many further octets are read and **discarded** after the queue cap is exceeded, so the
152/// peer can finish writing and then read the alert.
153///
154/// A peer refused here is by definition mid-stream. Closing a socket with unread data in the
155/// receive queue sends `RST`, which discards the bytes already written along with it — so the
156/// close_notify record below would never arrive and the peer would see a bare connection reset
157/// instead. This is nginx's `lingering_close`; nothing is buffered, the octets are counted and
158/// dropped, and a peer that keeps writing past either bound gets the abrupt close it earned.
159const LINGER_DRAIN_BYTES: usize = 8 * 1024 * 1024;
160
161/// Wall-clock bound on that drain, so a peer trickling one octet at a time cannot hold the
162/// connection open by staying under [`LINGER_DRAIN_BYTES`].
163const LINGER_DRAIN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
164
165/// What a peer over [`MAX_CONNECTIONS`] is told before the socket closes.
166///
167/// A plaintext fatal alert record: content type 21 (alert), legacy record version 0x0303,
168/// length 2, level `fatal`(2), description `internal_error`(80). TLS defines no "server busy"
169/// alert — RFC 8446's closest is `internal_error`, which is what a server sends when it cannot
170/// proceed for reasons unrelated to the peer — so that is the honest choice. A client reports
171/// "received fatal alert: internal_error" instead of a bare connection reset, which is the
172/// difference between an operator having a reason and guessing at one.
173const CONNECTION_CAP_REFUSAL: &[u8] = &[0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x50];
174
175/// The `decision=` token for an LLM call that returned `Err`.
176///
177/// TLS has exactly one failure shape on the wire - a close_notify alert - so an overloaded
178/// backend and a broken one cannot be told apart by the peer. They are told apart in the log
179/// instead: the token is stable, so `grep 'decision=fail_closed_'` finds every connection that
180/// was hung up on because nothing usable was produced, and the `_overloaded` suffix marks the
181/// transient half. The error itself is logged, never written to the socket.
182fn llm_error_decision(err: &anyhow::Error) -> &'static str {
183    match WireFailure::classify(err) {
184        WireFailure::Overloaded => "fail_closed_llm_error_overloaded",
185        WireFailure::Unavailable => "fail_closed_llm_error_unavailable",
186    }
187}
188
189/// Connection state for LLM processing
190#[derive(Debug, Clone, PartialEq)]
191enum ConnectionState {
192    Idle,
193    Processing,
194    Accumulating,
195}
196
197/// Per-connection data for LLM handling
198struct ConnectionData {
199    state: ConnectionState,
200    queued_data: Vec<u8>,
201    write_half: Arc<Mutex<tokio::io::WriteHalf<tokio_rustls::server::TlsStream<TcpStream>>>>,
202}
203
204/// TLS server that listens for incoming connections
205pub struct TlsServer;
206
207impl TlsServer {
208    /// Spawn the TLS server with integrated LLM actions
209    #[allow(clippy::too_many_arguments)]
210    pub async fn spawn_with_llm_actions(
211        listen_addr: SocketAddr,
212        llm_client: OllamaClient,
213        app_state: Arc<AppState>,
214        status_tx: mpsc::UnboundedSender<String>,
215        send_first: bool,
216        server_id: crate::state::ServerId,
217        tls_config: Option<Arc<rustls::ServerConfig>>,
218        handshake_timeout_secs: Option<u64>,
219        first_byte_timeout_secs: Option<u64>,
220        idle_timeout_secs: Option<u64>,
221    ) -> Result<SocketAddr> {
222        // All three bounds are tunable because their right value is a property of who is on
223        // the other end and what rides on this carrier, which only the operator knows. The
224        // first-record default serves NetGet's own TLS client waiting on a human; a listener
225        // exposed to strangers wants it much lower.
226        let deadlines = ReadDeadlines::resolve(
227            handshake_timeout_secs,
228            first_byte_timeout_secs,
229            idle_timeout_secs,
230        );
231        // Create TLS configuration (use provided or generate default)
232        let tls_config = if let Some(config) = tls_config {
233            config
234        } else {
235            crate::server::tls_cert_manager::generate_default_tls_config()
236                .context("Failed to generate TLS configuration")?
237        };
238
239        // Create and bind TCP listener
240        let listener = TcpListener::bind(listen_addr)
241            .await
242            .context("Failed to bind TLS TCP listener")?;
243        let local_addr = listener.local_addr()?;
244        Log::new(Some(&status_tx)).info(format!(
245            "TLS server (action-based) listening on {}",
246            local_addr
247        ));
248
249        let connections = Arc::new(Mutex::new(HashMap::new()));
250        let protocol = Arc::new(TlsProtocol::new());
251        let acceptor = TlsAcceptor::from(tls_config);
252
253        // Spawn accept loop
254        let task_registrar = app_state.clone();
255        let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
256        let accept_handle = tokio::spawn(async move {
257            loop {
258                match crate::server::accept_bounded::accept_bounded(
259                    &listener,
260                    &limiter,
261                    CONNECTION_CAP_REFUSAL,
262                    "TLS",
263                    Some(&status_tx),
264                )
265                .await
266                {
267                    Ok((stream, remote_addr, permit)) => {
268                        let connection_id =
269                            ConnectionId::new(app_state.get_next_unified_id().await);
270                        let local_addr_conn = stream.local_addr().unwrap_or(local_addr);
271                        debug!("TLS TCP connection from {}", remote_addr);
272
273                        let acceptor = acceptor.clone();
274                        let llm_client_clone = llm_client.clone();
275                        let app_state_clone = app_state.clone();
276                        let status_tx_clone = status_tx.clone();
277                        let connections_clone = connections.clone();
278                        let protocol_clone = protocol.clone();
279
280                        // Tracked, not detached: stop_server must abort this task too.
281                        let task_owner = app_state.clone();
282                        task_owner
283                            .spawn_server_task(server_id, async move {
284                                // Held for the life of the connection, so the cap counts live
285                                // peers rather than accepts.
286                                let _permit = permit;
287
288                                // Perform TLS handshake, bounded. This awaits the peer's
289                                // ClientHello and the rest of its side of the handshake; nothing in
290                                // it involves the model, so the deadline may cover the whole of it.
291                                let handshake = tokio::time::timeout(
292                                    deadlines.handshake,
293                                    acceptor.accept(stream),
294                                );
295                                let tls_stream = match handshake.await {
296                                    Err(_) => {
297                                        Log::new(Some(&status_tx_clone)).warn(format!(
298                                            "TLS handshake with {} produced nothing within {}s; \
299                                         closing",
300                                            remote_addr,
301                                            deadlines.handshake.as_secs()
302                                        ));
303                                        return;
304                                    }
305                                    Ok(Ok(stream)) => stream,
306                                    Ok(Err(e)) => {
307                                        // Handshake failure ends this connection but is a
308                                        // client-side condition, not a server error: WARN.
309                                        Log::new(Some(&status_tx_clone)).warn(format!(
310                                            "TLS handshake failed with {}: {}",
311                                            remote_addr, e
312                                        ));
313                                        return;
314                                    }
315                                };
316
317                                Log::new(Some(&status_tx_clone))
318                                    .debug(format!("TLS handshake complete with {}", remote_addr));
319
320                                info!(
321                                    "Accepted TLS connection {} from {}",
322                                    connection_id, remote_addr
323                                );
324
325                                // Split stream
326                                let (read_half, write_half) = tokio::io::split(tls_stream);
327                                let write_half_arc = Arc::new(Mutex::new(write_half));
328
329                                // TLS is the one server here whose read loop runs *concurrently*
330                                // with the answer: `handle_data_with_actions` is spawned and the
331                                // loop goes straight back to reading. So a peer whose record is
332                                // parked for a human, or is waiting on the model, sits inside the
333                                // read deadline while that happens — and closing it there would be
334                                // exactly the live-transfer eviction this project learned about
335                                // from TFTP. This tracks work in flight so the read site can tell
336                                // "the peer is silent" from "the peer is waiting for us".
337                                let activity = Arc::new(
338                                    crate::server::accept_bounded::ConnectionActivity::new(),
339                                );
340
341                                // Add connection to ServerInstance
342                                use crate::state::server::{
343                                    ConnectionState as ServerConnectionState, ConnectionStatus,
344                                    ProtocolConnectionInfo,
345                                };
346                                let now = crate::utils::clock::Instant::now();
347                                let conn_state = ServerConnectionState {
348                                    id: connection_id,
349                                    remote_addr,
350                                    local_addr: local_addr_conn,
351                                    bytes_sent: 0,
352                                    bytes_received: 0,
353                                    packets_sent: 0,
354                                    packets_received: 0,
355                                    last_activity: now,
356                                    status: ConnectionStatus::Active,
357                                    status_changed_at: now,
358                                    protocol_info: ProtocolConnectionInfo::new(serde_json::json!({
359                                        "state": "Idle",
360                                        "tls_handshake": "complete"
361                                    })),
362                                };
363                                app_state_clone
364                                    .add_connection_to_server(server_id, conn_state)
365                                    .await;
366                                let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
367
368                                // Register the connection HERE, before either task is spawned.
369                                //
370                                // This used to be the first thing the banner task did, racing the
371                                // reader task spawned immediately after it, and
372                                // handle_data_with_actions returns silently when the connection
373                                // is not in the map. TLS loses that race almost every time: the
374                                // handshake reads from the socket, so application data sent right
375                                // behind the client's Finished is already buffered inside rustls
376                                // and the reader's first read() returns it without ever waiting on
377                                // the I/O driver. 15 of 16 clients that wrote at handshake
378                                // completion had their request dropped with no response and no log
379                                // line.
380                                // A `send_first` server registers it `Processing`:
381                                // `tls_connection_opened` is being answered, and application data
382                                // that arrives meanwhile queues behind it instead of raising a
383                                // concurrent model call. Without `send_first` no connect event is
384                                // raised, so it starts `Idle`.
385                                connections_clone.lock().await.insert(
386                                    connection_id,
387                                    ConnectionData {
388                                        state: if send_first {
389                                            ConnectionState::Processing
390                                        } else {
391                                            ConnectionState::Idle
392                                        },
393                                        queued_data: Vec::new(),
394                                        write_half: write_half_arc.clone(),
395                                    },
396                                );
397
398                                // Peer messaging: the dashboard's "message this peer" /
399                                // "disconnect this peer" inject actions into THIS connection
400                                // through the same executor the LLM path uses, and the bytes
401                                // go out over the same `Arc<Mutex<WriteHalf<TlsStream>>>` the
402                                // session writes through - so an injected `send_tls_data` is
403                                // encrypted by rustls exactly like a modelled one.
404                                //
405                                // Registered here, before the banner task and before the
406                                // reader, for the same reason the connection map entry is:
407                                // a `manual` rule can park the very first record for a human
408                                // (300s by default), and that is precisely the window in
409                                // which an operator needs to reach the peer. Registering it
410                                // from inside the reader would lose the same race the
411                                // comment above describes.
412                                let peer_rx = crate::server::peer_support::register_peer_channel(
413                                    &app_state_clone,
414                                    server_id,
415                                    connection_id.as_u32(),
416                                )
417                                .await;
418                                crate::server::peer_support::spawn_peer_command_task(
419                                    peer_rx,
420                                    protocol_clone.clone(),
421                                    app_state_clone.clone(),
422                                    server_id,
423                                    connection_id.as_u32(),
424                                    write_half_arc.clone(),
425                                    status_tx_clone.clone(),
426                                );
427
428                                // `tls_connection_opened` is raised only for a `send_first`
429                                // server, as `tcp_connection_opened` is: the peer is owed a
430                                // greeting and nothing is read until the event is answered. A
431                                // server that speaks second pays no model call per connection.
432                                // See `handle_connection_opened`.
433                                let (opened_tx, opened_rx) = tokio::sync::oneshot::channel::<()>();
434                                // Dropped by the reader on every exit path, which tells the
435                                // connect task its peer is gone.
436                                let (reader_alive_tx, reader_alive_rx) =
437                                    tokio::sync::oneshot::channel::<()>();
438                                if send_first {
439                                    let llm_client_for_conn = llm_client_clone.clone();
440                                    let app_state_for_conn = app_state_clone.clone();
441                                    let status_tx_for_conn = status_tx_clone.clone();
442                                    let connections_for_conn = connections_clone.clone();
443                                    let write_half_for_conn = write_half_arc.clone();
444                                    let protocol_for_conn = protocol_clone.clone();
445                                    let opened_busy = activity.busy();
446                                    // Tracked, not detached: stop_server must abort this task too.
447                                    let task_owner = app_state_clone.clone();
448                                    task_owner
449                                        .spawn_server_task(server_id, async move {
450                                            let _opened_busy = opened_busy;
451                                            Self::handle_connection_opened(
452                                                connection_id,
453                                                server_id,
454                                                send_first,
455                                                opened_tx,
456                                                reader_alive_rx,
457                                                llm_client_for_conn,
458                                                app_state_for_conn,
459                                                status_tx_for_conn,
460                                                connections_for_conn,
461                                                write_half_for_conn,
462                                                protocol_for_conn,
463                                            )
464                                            .await;
465                                        })
466                                        .await;
467                                } else {
468                                    drop(opened_tx);
469                                    drop(reader_alive_rx);
470                                }
471
472                                // Spawn reader task
473                                let llm_client_for_read = llm_client_clone.clone();
474                                let app_state_for_read = app_state_clone.clone();
475                                let status_tx_for_read = status_tx_clone.clone();
476                                let connections_for_read = connections_clone.clone();
477                                let protocol_for_read = protocol_clone.clone();
478                                let activity_for_read = Arc::clone(&activity);
479                                // The reader decides the queue cap, so it needs the write half
480                                // to say so: the refusal is a TLS record, and rustls will only
481                                // produce one through this stream.
482                                let write_half_for_read = write_half_arc.clone();
483                                // Tracked, not detached: stop_server must abort this task too.
484                                let task_owner = app_state_clone.clone();
485                                task_owner
486                                    .spawn_server_task(server_id, async move {
487                                        let _reader_alive = reader_alive_tx;
488                                        let mut buffer = vec![0u8; 8192];
489                                        let mut read_half = read_half;
490                                        let mut seen_data = false;
491
492                                        // `send_first`: nothing is read until the connect event
493                                        // has been answered. Otherwise reads proceed and queue
494                                        // behind it (the connection starts `Processing`).
495                                        if send_first {
496                                            let _ = opened_rx.await;
497                                        } else {
498                                            drop(opened_rx);
499                                        }
500
501                                        loop {
502                                            let read_timeout = if seen_data {
503                                                deadlines.idle
504                                            } else {
505                                                deadlines.first_byte
506                                            };
507                                            // Re-arm rather than close whenever the deadline expires
508                                            // while an answer is still being produced: a peer waiting
509                                            // on us is not idle.
510                                            let read = loop {
511                                                match tokio::time::timeout(
512                                                    read_timeout,
513                                                    read_half.read(&mut buffer),
514                                                )
515                                                .await
516                                                {
517                                                    Ok(read) => break Some(read),
518                                                    Err(_) => {
519                                                        if activity_for_read.idle_for().is_none() {
520                                                            continue;
521                                                        }
522                                                        Log::new(Some(&status_tx_for_read)).info(
523                                                            format!(
524                                                    "TLS connection {connection_id} sent nothing \
525                                                     for {}s; closing idle connection",
526                                                    read_timeout.as_secs()
527                                                ),
528                                                        );
529                                                        break None;
530                                                    }
531                                                }
532                                            };
533                                            let Some(read) = read else {
534                                                connections_for_read
535                                                    .lock()
536                                                    .await
537                                                    .remove(&connection_id);
538                                                app_state_for_read
539                                                    .close_connection_on_server(
540                                                        server_id,
541                                                        connection_id,
542                                                    )
543                                                    .await;
544                                                let _ = status_tx_for_read
545                                                    .send("__UPDATE_UI__".to_string());
546                                                break;
547                                            };
548                                            match read {
549                                                Ok(0) => {
550                                                    // Connection closed
551                                                    connections_for_read
552                                                        .lock()
553                                                        .await
554                                                        .remove(&connection_id);
555                                                    app_state_for_read
556                                                        .close_connection_on_server(
557                                                            server_id,
558                                                            connection_id,
559                                                        )
560                                                        .await;
561                                                    Log::new(Some(&status_tx_for_read)).info(
562                                                        format!(
563                                                            "TLS connection {connection_id} closed"
564                                                        ),
565                                                    );
566                                                    let _ = status_tx_for_read
567                                                        .send("__UPDATE_UI__".to_string());
568                                                    break;
569                                                }
570                                                Ok(n) => {
571                                                    seen_data = true;
572
573                                                    // Bound the queue **before** these bytes are
574                                                    // handed to anything that would keep them.
575                                                    // The size compared is the one the peer has
576                                                    // already committed to — what is queued plus
577                                                    // what this read delivered — not what is left
578                                                    // over after some part of it has been
579                                                    // subtracted, which is the NATS `HPUB`
580                                                    // mistake the project CLAUDE.md records.
581                                                    let queued_state = {
582                                                        let conns =
583                                                            connections_for_read.lock().await;
584                                                        conns.get(&connection_id).map(|c| {
585                                                            (c.state.clone(), c.queued_data.len())
586                                                        })
587                                                    };
588                                                    if let Some((
589                                                        ConnectionState::Processing,
590                                                        queued,
591                                                    )) = queued_state
592                                                    {
593                                                        if queued.saturating_add(n)
594                                                            > MAX_QUEUED_BYTES
595                                                        {
596                                                            Self::refuse_queued_data_overflow(
597                                                                connection_id,
598                                                                server_id,
599                                                                queued.saturating_add(n),
600                                                                &mut read_half,
601                                                                &write_half_for_read,
602                                                                &connections_for_read,
603                                                                &app_state_for_read,
604                                                                &status_tx_for_read,
605                                                            )
606                                                            .await;
607                                                            break;
608                                                        }
609                                                    }
610
611                                                    let data = Bytes::copy_from_slice(&buffer[..n]);
612
613                                                    // Keep the rail's down/up counters and
614                                                    // `last_activity` moving. Nothing else updates
615                                                    // them for TLS, so every connection was drawn
616                                                    // with 0B in both directions however much it
617                                                    // carried.
618                                                    app_state_for_read
619                                                        .update_connection_stats(
620                                                            server_id,
621                                                            connection_id,
622                                                            Some(n as u64),
623                                                            None,
624                                                            Some(1),
625                                                            None,
626                                                        )
627                                                        .await;
628
629                                                    // Data summary + full payload are FileOnly: the
630                                                    // tls_data_received event template renders the
631                                                    // equivalent lines to the TUI, so streaming the
632                                                    // payload here too would duplicate it and load the
633                                                    // unbounded status channel.
634                                                    let log = Log::new(Some(&status_tx_for_read));
635                                                    if data.iter().all(|&b| {
636                                                        b.is_ascii_graphic()
637                                                            || b.is_ascii_whitespace()
638                                                    }) {
639                                                        let data_str =
640                                                            String::from_utf8_lossy(&data);
641                                                        let preview = if data_str.len() > 100 {
642                                                            format!("{}...", &data_str[..100])
643                                                        } else {
644                                                            data_str.to_string()
645                                                        };
646                                                        log.debug(format!(
647                                                            "TLS received {} bytes on {}: {}",
648                                                            n, connection_id, preview
649                                                        ));
650                                                        log.trace(format!(
651                                                            "TLS data (text): {:?}",
652                                                            data_str
653                                                        ));
654                                                    } else {
655                                                        log.debug(format!(
656                                                    "TLS received {} bytes on {} (binary data)",
657                                                    n, connection_id
658                                                ));
659                                                        log.trace(format!(
660                                                            "TLS data (hex): {}",
661                                                            hex::encode(&data)
662                                                        ));
663                                                    }
664
665                                                    // Handle data in separate task
666                                                    let llm_clone = llm_client_for_read.clone();
667                                                    let state_clone = app_state_for_read.clone();
668                                                    let status_clone = status_tx_for_read.clone();
669                                                    let conns_clone = connections_for_read.clone();
670                                                    let protocol_clone = protocol_for_read.clone();
671                                                    // Marked busy *before* the task is spawned, so
672                                                    // there is no window in which the read deadline
673                                                    // could see the connection as idle while an answer
674                                                    // is on its way.
675                                                    activity_for_read.begin_work();
676                                                    let activity_for_handler =
677                                                        Arc::clone(&activity_for_read);
678                                                    // Tracked, not detached: stop_server must abort this task too.
679                                                    let task_owner = app_state_for_read.clone();
680                                                    task_owner
681                                                        .spawn_server_task(server_id, async move {
682                                                            Self::handle_data_with_actions(
683                                                                connection_id,
684                                                                server_id,
685                                                                data,
686                                                                llm_clone,
687                                                                state_clone,
688                                                                status_clone,
689                                                                conns_clone,
690                                                                protocol_clone,
691                                                            )
692                                                            .await;
693                                                            activity_for_handler.end_work();
694                                                        })
695                                                        .await;
696                                                }
697                                                Err(e) => {
698                                                    Log::new(Some(&status_tx_for_read)).error(
699                                                        format!(
700                                                            "Read error on {}: {}",
701                                                            connection_id, e
702                                                        ),
703                                                    );
704                                                    connections_for_read
705                                                        .lock()
706                                                        .await
707                                                        .remove(&connection_id);
708                                                    // Close it in AppState as well. Dropping only the
709                                                    // map entry left the connection drawn as Active
710                                                    // for the life of the server, and every later
711                                                    // stat update targeted a peer that was gone.
712                                                    app_state_for_read
713                                                        .close_connection_on_server(
714                                                            server_id,
715                                                            connection_id,
716                                                        )
717                                                        .await;
718                                                    let _ = status_tx_for_read
719                                                        .send("__UPDATE_UI__".to_string());
720                                                    break;
721                                                }
722                                            }
723                                        }
724
725                                        // Every exit from the read loop — EOF, the idle
726                                        // deadline, a read error — lands here, so the handle
727                                        // goes away with the connection rather than leaving
728                                        // the rail offering a dead peer. The peer-command
729                                        // task ends when the handle is dropped, releasing its
730                                        // clone of the write half. Idempotent with
731                                        // `peer_support`'s own close path, which runs when an
732                                        // injected `close_connection` half-closes from
733                                        // outside this task.
734                                        app_state_for_read
735                                            .remove_peer_handle(server_id, connection_id.as_u32())
736                                            .await;
737                                    })
738                                    .await;
739                            })
740                            .await;
741                    }
742                    Err(e) => {
743                        Log::new(Some(&status_tx)).error(format!("Accept error: {}", e));
744                        break;
745                    }
746                }
747            }
748        });
749
750        task_registrar
751            .register_server_task(server_id, accept_handle)
752            .await;
753
754        Ok(local_addr)
755    }
756
757    /// Answer `tls_connection_opened` for a new connection, then release whatever the peer
758    /// sent while it was being answered. The TLS twin of `tcp`'s function of the same name,
759    /// called only for a `send_first` server, whose peer is owed a greeting: no bytes is WARN
760    /// `decision=model_silent`, and a backend failure closes with close_notify
761    /// (`decision=fail_closed_llm_error_*`). The branch for a server without `send_first` is
762    /// kept so the function stays correct if it is ever called for one.
763    ///
764    /// `opened` is signalled once the answer is on the wire; a `send_first` reader waits for
765    /// it. `reader_alive` resolves when the reader ends, which abandons the call.
766    #[allow(clippy::too_many_arguments)]
767    async fn handle_connection_opened(
768        connection_id: ConnectionId,
769        server_id: crate::state::ServerId,
770        send_first: bool,
771        opened: tokio::sync::oneshot::Sender<()>,
772        reader_alive: tokio::sync::oneshot::Receiver<()>,
773        llm_client: OllamaClient,
774        app_state: Arc<AppState>,
775        status_tx: mpsc::UnboundedSender<String>,
776        connections: Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
777        write_half: Arc<Mutex<tokio::io::WriteHalf<tokio_rustls::server::TlsStream<TcpStream>>>>,
778        protocol: Arc<TlsProtocol>,
779    ) {
780        let log = Log::new(Some(&status_tx));
781        let event = Event::new(
782            &TLS_CONNECTION_OPENED_EVENT,
783            crate::protocol::event_type::connect_event_data(),
784        );
785
786        let answer = tokio::select! {
787            answer = call_llm(
788                &llm_client,
789                &app_state,
790                server_id,
791                Some(connection_id),
792                &event,
793                protocol.as_ref(),
794            ) => answer,
795            _ = reader_alive => {
796                log.debug(format!(
797                    "TLS connection {connection_id} closed before its connect event was \
798                     answered: decision=peer_left_before_answer"
799                ));
800                return;
801            }
802        };
803
804        match answer {
805            Ok(execution_result) => {
806                debug!("LLM TLS connect-event response received");
807                for msg in execution_result.messages {
808                    let _ = status_tx.send(msg);
809                }
810
811                let mut wrote = false;
812                for protocol_result in execution_result.protocol_results {
813                    match protocol_result {
814                        ActionResult::Output(output_data) => {
815                            let mut write = write_half.lock().await;
816                            if let Err(e) = write.write_all(&output_data).await {
817                                log.error(format!("Failed to send greeting: {}", e));
818                            } else {
819                                drop(write);
820                                log.debug(format!(
821                                    "TLS sent {} bytes to {}",
822                                    output_data.len(),
823                                    connection_id
824                                ));
825                                app_state
826                                    .update_connection_stats(
827                                        server_id,
828                                        connection_id,
829                                        None,
830                                        Some(output_data.len() as u64),
831                                        None,
832                                        Some(1),
833                                    )
834                                    .await;
835                                wrote = true;
836                            }
837                        }
838                        ActionResult::CloseConnection => {
839                            connections.lock().await.remove(&connection_id);
840                            if let Err(e) = write_half.lock().await.shutdown().await {
841                                debug!("TLS shutdown on {} returned: {}", connection_id, e);
842                            }
843                            log.info(format!(
844                                "Closed TLS connection {connection_id} on connect: \
845                                 decision=model_close"
846                            ));
847                            let _ = opened.send(());
848                            return;
849                        }
850                        _ => {}
851                    }
852                }
853
854                if wrote {
855                    log.debug(format!(
856                        "TLS connection {connection_id} greeted: decision=model_answer"
857                    ));
858                } else if send_first {
859                    log.warn(format!(
860                        "TLS connection {connection_id} decision=model_silent: send_first was \
861                         requested but the model produced no greeting"
862                    ));
863                } else {
864                    log.debug(format!(
865                        "TLS connection {connection_id} connect: decision=model_no_actions"
866                    ));
867                }
868            }
869            Err(e) if send_first => {
870                // A `send_first` server owes the peer a greeting; without one the peer waits
871                // for a banner that will never come. Close with a close_notify alert so it
872                // reads EOF instead. The full error goes to the log only.
873                let decision = llm_error_decision(&e);
874                error!(
875                    "TLS connection {} decision={}: LLM call failed generating greeting: {:#}",
876                    connection_id, decision, e
877                );
878                log.warn(format!(
879                    "TLS connection {connection_id} decision={decision}: no greeting \
880                     generated, closing with close_notify"
881                ));
882                {
883                    let mut write = write_half.lock().await;
884                    if let Err(shutdown_err) = write.shutdown().await {
885                        debug!(
886                            "TLS shutdown on {} returned: {}",
887                            connection_id, shutdown_err
888                        );
889                    }
890                }
891                connections.lock().await.remove(&connection_id);
892                app_state
893                    .remove_peer_handle(server_id, connection_id.as_u32())
894                    .await;
895                app_state
896                    .close_connection_on_server(server_id, connection_id)
897                    .await;
898                let _ = opened.send(());
899                return;
900            }
901            Err(e) => {
902                // Nobody asked this server to speak first, so the peer has been failed by
903                // nothing yet; its own data gets the data path's answer.
904                log.warn(format!(
905                    "TLS connect event for {connection_id} failed: \
906                     decision=connect_event_failed error={e}"
907                ));
908            }
909        }
910
911        let _ = opened.send(());
912
913        let queued = {
914            let mut conns = connections.lock().await;
915            let Some(conn) = conns.get_mut(&connection_id) else {
916                return;
917            };
918            conn.state = ConnectionState::Idle;
919            !conn.queued_data.is_empty()
920        };
921        if queued {
922            Self::handle_data_with_actions(
923                connection_id,
924                server_id,
925                Bytes::new(),
926                llm_client,
927                app_state,
928                status_tx,
929                connections,
930                protocol,
931            )
932            .await;
933        }
934    }
935
936    /// Refuse a connection whose queued application data has passed [`MAX_QUEUED_BYTES`].
937    ///
938    /// **The alert is `close_notify`, and it is not the alert this deserves.** TLS has a
939    /// description for exactly this condition — `record_overflow(22)` — and rustls 0.23 cannot
940    /// send it: `CommonState::send_fatal_alert` is `pub(crate)`, and the only alert the public
941    /// API will emit is `send_close_notify`, which is what `poll_shutdown` on a
942    /// `tokio_rustls` stream calls. Writing the seven raw bytes of a `record_overflow` alert to
943    /// the TCP socket underneath is not an alternative: after the handshake every record is
944    /// encrypted, so a plaintext one is a protocol violation the peer must reject, and it would
945    /// arrive as garbage rather than as a reason. (The connection **cap** refusal higher up in
946    /// this file *is* a raw plaintext alert, and legitimately so — it is written before any
947    /// handshake has happened, when nothing is encrypted yet.)
948    ///
949    /// So the distinction the wire cannot carry is carried by the log, which is the rule the
950    /// project CLAUDE.md states for every deliberately-silent protocol: `decision=` names what
951    /// happened, and `grep decision=fail_closed` finds it.
952    #[allow(clippy::too_many_arguments)]
953    async fn refuse_queued_data_overflow<R>(
954        connection_id: ConnectionId,
955        server_id: crate::state::ServerId,
956        would_be: usize,
957        read_half: &mut R,
958        write_half: &Arc<Mutex<tokio::io::WriteHalf<tokio_rustls::server::TlsStream<TcpStream>>>>,
959        connections: &Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
960        app_state: &Arc<AppState>,
961        status_tx: &mpsc::UnboundedSender<String>,
962    ) where
963        R: tokio::io::AsyncRead + Unpin,
964    {
965        Log::new(Some(status_tx)).warn(format!(
966            "TLS connection {connection_id} decision=fail_closed_queued_data_overflow: peer \
967             queued {would_be} bytes while an answer was in flight, limit is {MAX_QUEUED_BYTES}; \
968             closing with close_notify (TLS record_overflow is not reachable through rustls)"
969        ));
970
971        {
972            let mut write = write_half.lock().await;
973            if let Err(e) = write.shutdown().await {
974                debug!(
975                    "TLS shutdown on {} after overflow returned: {}",
976                    connection_id, e
977                );
978            }
979        }
980
981        // Drain before the socket is dropped, or the close is an RST and the alert just
982        // written is discarded with it.
983        let mut drained = 0usize;
984        let mut scratch = vec![0u8; 64 * 1024];
985        let _ = tokio::time::timeout(LINGER_DRAIN_TIMEOUT, async {
986            while drained < LINGER_DRAIN_BYTES {
987                match read_half.read(&mut scratch).await {
988                    Ok(0) | Err(_) => break,
989                    Ok(n) => drained = drained.saturating_add(n),
990                }
991            }
992        })
993        .await;
994        debug!("TLS {connection_id}: drained {drained} bytes after refusing the queue");
995
996        connections.lock().await.remove(&connection_id);
997        app_state
998            .remove_peer_handle(server_id, connection_id.as_u32())
999            .await;
1000        app_state
1001            .close_connection_on_server(server_id, connection_id)
1002            .await;
1003        let _ = status_tx.send("__UPDATE_UI__".to_string());
1004    }
1005
1006    /// Handle data received on a connection with LLM actions
1007    async fn handle_data_with_actions(
1008        connection_id: ConnectionId,
1009        server_id: crate::state::ServerId,
1010        data: Bytes,
1011        llm_client: OllamaClient,
1012        app_state: Arc<AppState>,
1013        status_tx: mpsc::UnboundedSender<String>,
1014        connections: Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
1015        protocol: Arc<TlsProtocol>,
1016    ) {
1017        // Check connection state
1018        let current_state = {
1019            let conns = connections.lock().await;
1020            if let Some(conn_data) = conns.get(&connection_id) {
1021                conn_data.state.clone()
1022            } else {
1023                // Never silent. A miss here means the connection was torn down between the
1024                // read and this lookup (peer reset, or close_this_connection on another
1025                // task) - legitimate, but indistinguishable from a registration race, which
1026                // is exactly what made the bitcoin accept-order bug so hard to find: the
1027                // read loop logged "received N bytes" and then nothing at all.
1028                debug!(
1029                    "TLS connection {} is no longer registered; dropping {} received bytes",
1030                    connection_id,
1031                    data.len()
1032                );
1033                return;
1034            }
1035        };
1036
1037        // If processing, queue the data — up to the cap, and never past it.
1038        //
1039        // The reader loop refuses and tears the connection down before it ever hands over data
1040        // that would cross [`MAX_QUEUED_BYTES`], so this is the second half of the same bound
1041        // rather than a different one: the reader's check reads the queue length a moment
1042        // before this runs, and with a handler task in between the two there is a window in
1043        // which another read could arrive. Refusing to extend here is what makes the `Vec`
1044        // itself unable to exceed the cap, whatever that window does.
1045        if current_state == ConnectionState::Processing {
1046            let mut refused = None;
1047            connections
1048                .lock()
1049                .await
1050                .entry(connection_id)
1051                .and_modify(|conn| {
1052                    let would_be = conn.queued_data.len().saturating_add(data.len());
1053                    if would_be > MAX_QUEUED_BYTES {
1054                        refused = Some(would_be);
1055                    } else {
1056                        conn.queued_data.extend_from_slice(&data);
1057                    }
1058                });
1059            match refused {
1060                Some(would_be) => Log::new(Some(&status_tx)).warn(format!(
1061                    "TLS connection {connection_id} decision=fail_closed_queued_data_overflow: \
1062                     dropped {} bytes that would have taken the queue to {would_be}, limit is \
1063                     {MAX_QUEUED_BYTES}",
1064                    data.len()
1065                )),
1066                None => Log::new(Some(&status_tx)).debug(format!(
1067                    "Queued {} bytes for {}",
1068                    data.len(),
1069                    connection_id
1070                )),
1071            }
1072            return;
1073        }
1074
1075        // Merge any queued data with new data.
1076        //
1077        // The lock was released after the state check above, so the reader task
1078        // may have removed this connection in the meantime (client disconnected).
1079        // Unwrapping here panicked the task on that race, and a panicked task
1080        // leaves the server reporting Running.
1081        let mut all_data = {
1082            let mut conns = connections.lock().await;
1083            let Some(conn_data) = conns.get_mut(&connection_id) else {
1084                return; // Connection closed while we were waiting for the lock
1085            };
1086            conn_data.state = ConnectionState::Processing;
1087            let mut merged = conn_data.queued_data.clone();
1088            merged.extend_from_slice(&data);
1089            conn_data.queued_data.clear();
1090            Bytes::from(merged)
1091        };
1092
1093        loop {
1094            // Get write_half for context
1095            let write_half = {
1096                let conns = connections.lock().await;
1097                conns.get(&connection_id).map(|c| c.write_half.clone())
1098            };
1099
1100            let Some(write_half) = write_half else {
1101                debug!(
1102                    "TLS connection {} went away before its response could be written",
1103                    connection_id
1104                );
1105                return;
1106            };
1107
1108            // Format data for event parameter. The encoding is reported
1109            // alongside so the model can echo binary back through
1110            // send_tls_data with encoding="hex" instead of sending the ASCII
1111            // hex digits.
1112            let printable = all_data
1113                .iter()
1114                .all(|&b| b.is_ascii_graphic() || b.is_ascii_whitespace());
1115            let (data_str, encoding) = if printable {
1116                (String::from_utf8_lossy(&all_data).to_string(), "utf8")
1117            } else {
1118                (hex::encode(&all_data), "hex")
1119            };
1120
1121            // Create data received event
1122            let event = Event::new(
1123                &TLS_DATA_RECEIVED_EVENT,
1124                serde_json::json!({
1125                    "data": data_str,
1126                    "encoding": encoding
1127                }),
1128            );
1129
1130            // Call LLM
1131            match call_llm(
1132                &llm_client,
1133                &app_state,
1134                server_id,
1135                Some(connection_id),
1136                &event,
1137                protocol.as_ref(),
1138            )
1139            .await
1140            {
1141                Ok(execution_result) => {
1142                    debug!("LLM TLS response received");
1143
1144                    // Display messages
1145                    for msg in execution_result.messages {
1146                        let _ = status_tx.send(msg);
1147                    }
1148
1149                    // Handle protocol results
1150                    let mut should_close = false;
1151                    let mut should_wait = false;
1152                    let mut acted = false;
1153
1154                    for protocol_result in execution_result.protocol_results {
1155                        match protocol_result {
1156                            ActionResult::Output(output_data) => {
1157                                acted = true;
1158                                let mut write = write_half.lock().await;
1159                                let log = Log::new(Some(&status_tx));
1160                                if let Err(e) = write.write_all(&output_data).await {
1161                                    log.error(format!("Failed to send response: {}", e));
1162                                } else {
1163                                    // Sent-data summary + payload are FileOnly: the
1164                                    // send_tls_data action template already reports the send
1165                                    // to the TUI.
1166                                    if output_data
1167                                        .iter()
1168                                        .all(|&b| b.is_ascii_graphic() || b.is_ascii_whitespace())
1169                                    {
1170                                        let data_str = String::from_utf8_lossy(&output_data);
1171                                        let preview = if data_str.len() > 100 {
1172                                            format!("{}...", &data_str[..100])
1173                                        } else {
1174                                            data_str.to_string()
1175                                        };
1176                                        log.debug(format!(
1177                                            "TLS sent {} bytes to {}: {}",
1178                                            output_data.len(),
1179                                            connection_id,
1180                                            preview
1181                                        ));
1182                                        log.trace(format!("TLS sent (text): {:?}", data_str));
1183                                    } else {
1184                                        log.debug(format!(
1185                                            "TLS sent {} bytes to {} (binary data)",
1186                                            output_data.len(),
1187                                            connection_id
1188                                        ));
1189                                        log.trace(format!(
1190                                            "TLS sent (hex): {}",
1191                                            hex::encode(&output_data)
1192                                        ));
1193                                    }
1194                                    log.debug(format!(
1195                                        "Sent {} bytes to {}",
1196                                        output_data.len(),
1197                                        connection_id
1198                                    ));
1199                                    app_state
1200                                        .update_connection_stats(
1201                                            server_id,
1202                                            connection_id,
1203                                            None,
1204                                            Some(output_data.len() as u64),
1205                                            None,
1206                                            Some(1),
1207                                        )
1208                                        .await;
1209                                }
1210                            }
1211                            ActionResult::CloseConnection => {
1212                                acted = true;
1213                                should_close = true;
1214                            }
1215                            ActionResult::WaitForMore => {
1216                                acted = true;
1217                                should_wait = true;
1218                            }
1219                            _ => {}
1220                        }
1221                    }
1222
1223                    // An answer that carries no output, no close and no wait leaves the
1224                    // peer holding an unanswered request. That is a legitimate "say nothing"
1225                    // for a transport with no reply obligation, so the connection stays open -
1226                    // but it is logged distinctly from a backend failure, which hangs up.
1227                    if !acted {
1228                        Log::new(Some(&status_tx)).warn(format!(
1229                            "TLS connection {connection_id} decision=model_no_action: model \
1230                             produced no usable action, nothing sent"
1231                        ));
1232                    }
1233
1234                    // Handle wait_for_more
1235                    if should_wait {
1236                        connections
1237                            .lock()
1238                            .await
1239                            .entry(connection_id)
1240                            .and_modify(|conn| conn.state = ConnectionState::Accumulating);
1241                        Log::new(Some(&status_tx))
1242                            .debug(format!("Waiting for more data from {connection_id}"));
1243                        return;
1244                    }
1245
1246                    // Handle close_connection.
1247                    //
1248                    // Dropping the map entry alone left the socket open forever
1249                    // (the reader task kept blocking on read and the client never
1250                    // saw a close), so close_this_connection did not close
1251                    // anything. Shut the TLS write half down to send close_notify
1252                    // and let the peer's read return EOF.
1253                    if should_close {
1254                        connections.lock().await.remove(&connection_id);
1255                        if let Err(e) = write_half.lock().await.shutdown().await {
1256                            debug!("TLS shutdown on {} returned: {}", connection_id, e);
1257                        }
1258                        Log::new(Some(&status_tx))
1259                            .info(format!("Closed TLS connection {connection_id}"));
1260                        return;
1261                    }
1262
1263                    // Check for queued data
1264                    let has_queued = {
1265                        let conns = connections.lock().await;
1266                        conns
1267                            .get(&connection_id)
1268                            .map(|c| !c.queued_data.is_empty())
1269                            .unwrap_or(false)
1270                    };
1271
1272                    if has_queued {
1273                        // Take the queue and make it the next iteration's payload.
1274                        // Leaving it in place re-sent the SAME bytes to the model on
1275                        // every pass and never emptied the queue: one response per
1276                        // iteration, forever, for a single line of input.
1277                        let queued = {
1278                            let mut conns = connections.lock().await;
1279                            match conns.get_mut(&connection_id) {
1280                                Some(conn) => std::mem::take(&mut conn.queued_data),
1281                                None => return,
1282                            }
1283                        };
1284                        if queued.is_empty() {
1285                            connections
1286                                .lock()
1287                                .await
1288                                .entry(connection_id)
1289                                .and_modify(|conn| conn.state = ConnectionState::Idle);
1290                            return;
1291                        }
1292                        all_data = Bytes::from(queued);
1293                    } else {
1294                        // Go to Idle state
1295                        connections
1296                            .lock()
1297                            .await
1298                            .entry(connection_id)
1299                            .and_modify(|conn| conn.state = ConnectionState::Idle);
1300                        return;
1301                    }
1302                }
1303                Err(e) => {
1304                    // The full error goes to the log and the status stream; the peer gets a
1305                    // close_notify alert carrying nothing about why.
1306                    let decision = llm_error_decision(&e);
1307                    error!(
1308                        "TLS connection {} decision={}: LLM call failed for received data: {:#}",
1309                        connection_id, decision, e
1310                    );
1311                    Log::new(Some(&status_tx)).warn(format!(
1312                        "TLS connection {connection_id} decision={decision}: no response \
1313                         generated, closing with close_notify"
1314                    ));
1315
1316                    // Say something on the wire instead of resetting to Idle in silence.
1317                    //
1318                    // TLS carries no application-level error - the application protocol here
1319                    // is whatever the handler invents, so there is no reply we could phrase in
1320                    // it. What TLS does have is the alert protocol, and `shutdown()` emits a
1321                    // real close_notify alert record (not just a FIN), which every TLS client
1322                    // surfaces as a clean end of stream immediately rather than blocking until
1323                    // its own timeout.
1324                    //
1325                    // A *fatal* `internal_error` alert would be more precise, but rustls 0.23
1326                    // keeps `CommonState::send_fatal_alert` `pub(crate)`; close_notify is the
1327                    // strongest in-spec signal reachable through its public API. Forging a
1328                    // plaintext alert record onto the TCP socket underneath would violate
1329                    // TLS 1.3 record protection, so it is not done.
1330                    {
1331                        let mut write = write_half.lock().await;
1332                        if let Err(shutdown_err) = write.shutdown().await {
1333                            debug!(
1334                                "TLS shutdown on {} returned: {}",
1335                                connection_id, shutdown_err
1336                            );
1337                        }
1338                    }
1339                    connections.lock().await.remove(&connection_id);
1340                    // The reader task is still parked in `read()` and will only notice once
1341                    // the peer closes its own side, so retire the peer handle here rather
1342                    // than leaving the rail offering a connection this task has already
1343                    // ended. Idempotent with the reader's own removal.
1344                    app_state
1345                        .remove_peer_handle(server_id, connection_id.as_u32())
1346                        .await;
1347                    app_state
1348                        .close_connection_on_server(server_id, connection_id)
1349                        .await;
1350                    Log::new(Some(&status_tx)).info(format!(
1351                        "Closed TLS connection {connection_id} after LLM error"
1352                    ));
1353                    return;
1354                }
1355            }
1356        }
1357    }
1358}