Skip to main content

netget/server/tor_relay/
mod.rs

1//! Tor Relay (OR protocol) server - **partial, not interoperable with real Tor**.
2//!
3//! The link handshake stops after VERSIONS (no CERTS/AUTH_CHALLENGE/NETINFO), relay cell
4//! digests are neither computed nor verified, and there is no EXTEND. See `CLAUDE.md` in this
5//! directory for the full list of non-conformances. What is implemented:
6//! - **Length-driven cell framing**: VERSIONS (2-byte circuit id, variable length) and
7//!   commands >= 128 are framed by their length field rather than assumed to be 514 bytes,
8//!   and the read is cancellation-safe inside the `select!`
9//! - **ntor handshake** for circuit creation (CREATE2/CREATED2) with Curve25519 DH
10//! - **Relay cell encryption** using AES-128-CTR with forward/backward keys
11//! - **Circuit management** with crypto state and stream multiplexing
12//! - **Exit relay functionality** (BEGIN, DATA, END, CONNECTED)
13//! - **Bidirectional data forwarding** (TCP ↔ Tor client) via background tasks
14//! - **SENDME flow control** at circuit and stream levels (tor-spec compliant)
15//! - **Bandwidth tracking** per circuit and aggregate statistics
16//! - **Channel-based architecture** for concurrent stream handling
17//! - **LLM notification** on circuit creation and on unimplemented RELAY commands. The data
18//!   path itself (BEGIN/DATA/END/SENDME, exit target selection) is decided in Rust, and no
19//!   exit policy is enforced.
20//!
21//! ## Architecture
22//!
23//! ```text
24//! TLS Connection (TorRelaySession)
25//!     │
26//!     ├─ tokio::select! loop
27//!     │   ├─ Read TLS stream → Parse cells
28//!     │   └─ Write from channel ← Forwarder tasks
29//!     │
30//!     ├─ CircuitManager (shared across connections)
31//!     │   ├─ Circuit 1 (crypto + streams)
32//!     │   │   ├─ Stream 1 → TCP connection
33//!     │   │   ├─ Stream 2 → TCP connection
34//!     │   │   └─ Flow control windows
35//!     │   ├─ Circuit 2 (crypto + streams)
36//!     │   └─ Statistics tracking
37//!     │
38//!     └─ Background forwarder tasks (per stream)
39//!         └─ TCP → Encrypt → Channel → TLS
40//! ```
41//!
42//! ## Cell Processing Flow
43//!
44//! 1. **CREATE2**: ntor handshake → CREATED2 response
45//! 2. **RELAY/BEGIN**: Parse target → TCP connect → CONNECTED response
46//! 3. **RELAY/DATA**: Decrypt → Forward to TCP (client→server) OR Encrypt ← TCP (server→client)
47//! 4. **RELAY/END**: Close TCP connection
48//! 5. **RELAY/SENDME**: Update flow control windows
49//!
50//! ## Flow Control (SENDME)
51//!
52//! - Circuit-level: 1000 cell window, SENDME every 100 cells
53//! - Stream-level: 500 cell window, SENDME every 50 cells
54//! - Automatic SENDME generation on receive thresholds
55//! - Package window prevents overwhelming next hop
56//!
57//! **Status**: Experimental. Not a usable relay; see `CLAUDE.md`.
58
59pub mod actions;
60pub mod circuit;
61pub mod stream;
62
63use anyhow::{Context, Result};
64use std::net::SocketAddr;
65use std::sync::Arc;
66use tokio::sync::{mpsc, Mutex};
67use tracing::{debug, error, info, trace, warn};
68
69#[cfg(unix)]
70use crate::llm::action_helper::call_llm;
71#[cfg(unix)]
72use crate::llm::ollama_client::OllamaClient;
73#[cfg(unix)]
74use crate::llm::ActionResult;
75#[cfg(unix)]
76use crate::protocol::Event;
77#[cfg(unix)]
78use crate::server::TorRelayProtocol;
79#[cfg(unix)]
80use crate::state::app_state::AppState;
81#[cfg(unix)]
82use actions::{TOR_RELAY_CIRCUIT_CREATED_EVENT, TOR_RELAY_RELAY_CELL_EVENT};
83#[cfg(unix)]
84use circuit::{CircuitId, CircuitManager, StreamId};
85#[cfg(unix)]
86use stream::{build_relay_cell, connect_to_target, end_reason, parse_begin_target, relay_command};
87
88#[cfg(unix)]
89use crate::logging::emit::Log;
90use tokio::io::{AsyncReadExt, AsyncWriteExt};
91use tokio::net::{TcpListener, TcpStream};
92use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer};
93use tokio_rustls::rustls::ServerConfig;
94use tokio_rustls::TlsAcceptor;
95
96/// The write half of a peer's TLS stream, shared between the session loop and the dashboard's
97/// peer-command task (`server::peer_support`).
98#[cfg(unix)]
99type SharedWrite = Arc<Mutex<tokio::io::WriteHalf<tokio_rustls::server::TlsStream<TcpStream>>>>;
100
101/// Tor Relay server - handles OR protocol connections
102pub struct TorRelayServer;
103
104#[cfg(unix)]
105impl TorRelayServer {
106    /// Spawn Tor Relay server with LLM action integration
107    pub async fn spawn_with_llm_actions(
108        listen_addr: SocketAddr,
109        llm_client: OllamaClient,
110        app_state: Arc<AppState>,
111        status_tx: mpsc::UnboundedSender<String>,
112        server_id: crate::state::ServerId,
113    ) -> Result<SocketAddr> {
114        // Generate self-signed TLS certificate for OR protocol
115        let (cert, key) = generate_tls_certificate()?;
116
117        // Configure TLS acceptor
118        // `ring`, which is the provider this tree compiles rustls with: `Cargo.toml` pins
119        // `rustls`/`tokio-rustls` to `default-features = false, features = ["ring", ...]`
120        // because the default `aws_lc_rs` is a C library that does not build for wasm32, and
121        // every other TLS site here (`tls`, `dot`, `proxy`, `openvpn`, the binary's own
122        // `install_default`) uses `ring` for that reason. Naming `aws_lc_rs` here did not
123        // select a different provider - it did not compile at all, so `--features tor` was a
124        // hard build error on its own and only ever got as far as a feature set that happened
125        // to turn the `aws_lc_rs` feature back on.
126        let crypto_provider = tokio_rustls::rustls::crypto::ring::default_provider();
127        let tls_config = ServerConfig::builder_with_provider(Arc::new(crypto_provider))
128            // TLS 1.2 as well as 1.3. A real Tor relay accepts both -- the link
129            // specification's minimum is 1.2 -- and restricted to 1.3 alone this server
130            // rejected every peer that sent a 1.2 ClientHello with
131            // `SupportedVersionsExtensionRequired`, which is rustls saying "this hello has
132            // no supported_versions extension" and reads like a peer bug rather than our
133            // own configuration.
134            .with_protocol_versions(&[
135                &tokio_rustls::rustls::version::TLS13,
136                &tokio_rustls::rustls::version::TLS12,
137            ])
138            .expect("Valid TLS protocol versions")
139            .with_no_client_auth()
140            .with_single_cert(vec![cert], key)
141            .context("Failed to create TLS config")?;
142
143        let acceptor = TlsAcceptor::from(Arc::new(tls_config));
144
145        // Bind TCP listener
146        let listener = TcpListener::bind(listen_addr)
147            .await
148            .context("Failed to bind Tor Relay server")?;
149
150        let actual_addr = listener
151            .local_addr()
152            .context("Failed to get local address")?;
153
154        Log::new(Some(&status_tx)).info(format!(
155            "Tor Relay (OR protocol) server listening on {}",
156            actual_addr
157        ));
158
159        // Create circuit manager (shared across all connections)
160        let circuit_manager = Arc::new(CircuitManager::new());
161
162        // Publish the relay identity.
163        //
164        // The ntor handshake mixes the relay's identity fingerprint (ID) and its onion
165        // public key (B) into `secret_input`, so a peer that does not know both cannot
166        // derive the same key material — it gets a CREATED2 it can do nothing with. A real
167        // relay publishes them in its router descriptor. This one generates a fresh onion
168        // key per process and used to print only the fingerprint, which meant **no peer
169        // could ever complete a circuit with it**. Both are public values by design.
170        let fingerprint = circuit_manager.identity_fingerprint();
171        let onion_key = *circuit_manager.onion_public_key().as_bytes();
172        Log::new(Some(&status_tx)).info(format!("Relay fingerprint: {}", hex::encode(fingerprint)));
173        Log::new(Some(&status_tx)).info(format!("Relay onion key: {}", hex::encode(onion_key)));
174
175        let protocol = Arc::new(TorRelayProtocol::new());
176
177        // Spawn connection handler
178        let task_registrar = app_state.clone();
179        let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
180        let accept_handle = tokio::spawn(async move {
181            loop {
182                match crate::server::accept_bounded::accept_bounded(
183                    &listener,
184                    &limiter,
185                    CONNECTION_CAP_REFUSAL,
186                    "Tor Relay",
187                    Some(&status_tx),
188                )
189                .await
190                {
191                    Ok((stream, remote_addr, permit)) => {
192                        let connection_id = crate::server::connection::ConnectionId::new(
193                            app_state.get_next_unified_id().await,
194                        );
195                        Log::new(Some(&status_tx)).debug(format!(
196                            "Tor Relay connection {} from {}",
197                            connection_id, remote_addr
198                        ));
199
200                        let llm_clone = llm_client.clone();
201                        let state_clone = app_state.clone();
202                        let status_clone = status_tx.clone();
203                        let acceptor_clone = acceptor.clone();
204                        let protocol_clone = protocol.clone();
205                        let circuit_mgr_clone = circuit_manager.clone();
206
207                        // Tracked, not detached: stop_server must abort this task too.
208                        let task_owner = app_state.clone();
209                        task_owner
210                            .spawn_server_task(server_id, async move {
211                                // Held for the life of the connection, so the cap counts live
212                                // peers rather than accepts.
213                                let _permit = permit;
214                                if let Err(e) = handle_tor_relay_connection(
215                                    stream,
216                                    connection_id,
217                                    server_id,
218                                    remote_addr,
219                                    acceptor_clone,
220                                    llm_clone,
221                                    state_clone,
222                                    status_clone,
223                                    protocol_clone,
224                                    circuit_mgr_clone,
225                                )
226                                .await
227                                {
228                                    error!("Tor Relay connection error: {}", e);
229                                }
230                            })
231                            .await;
232                    }
233                    Err(e) => {
234                        Log::new(Some(&status_tx))
235                            .error(format!("Failed to accept Tor Relay connection: {}", e));
236                    }
237                }
238            }
239        });
240
241        task_registrar
242            .register_server_task(server_id, accept_handle)
243            .await;
244
245        Ok(actual_addr)
246    }
247}
248
249#[cfg(not(unix))]
250impl TorRelayServer {
251    pub async fn spawn_with_llm_actions(
252        _listen_addr: SocketAddr,
253        _llm_client: OllamaClient,
254        _app_state: Arc<AppState>,
255        _status_tx: mpsc::UnboundedSender<String>,
256        _server_id: crate::state::ServerId,
257    ) -> Result<SocketAddr> {
258        anyhow::bail!("Tor Relay feature not enabled")
259    }
260}
261
262/// Generate self-signed TLS certificate for OR protocol
263fn generate_tls_certificate() -> Result<(CertificateDer<'static>, PrivateKeyDer<'static>)> {
264    use rcgen::{CertificateParams, KeyPair};
265
266    let mut params = CertificateParams::new(vec!["tor-relay.local".to_string()])?;
267    params.distinguished_name = rcgen::DistinguishedName::new();
268    params
269        .distinguished_name
270        .push(rcgen::DnType::CommonName, "NetGet Tor Relay");
271
272    let key_pair = KeyPair::generate()?;
273    let cert = params.self_signed(&key_pair)?;
274
275    let cert_der = CertificateDer::from(cert.der().to_vec());
276    let key_der = PrivateKeyDer::Pkcs8(key_pair.serialize_der().into());
277
278    Ok((cert_der, key_der))
279}
280
281/// Handle individual Tor Relay connection.
282///
283/// Owns the connection's bookkeeping: the `AppState` entry the dashboard draws the peer row
284/// from, and the peer handle that row's `[ message ]` / `[ disconnect ]` buttons need. Both
285/// are released here on **every** exit, including a failed TLS handshake, so nothing is left
286/// listed as live after the socket has gone.
287#[allow(clippy::too_many_arguments)]
288async fn handle_tor_relay_connection(
289    stream: TcpStream,
290    connection_id: crate::server::connection::ConnectionId,
291    server_id: crate::state::ServerId,
292    remote_addr: SocketAddr,
293    acceptor: TlsAcceptor,
294    llm_client: OllamaClient,
295    app_state: Arc<AppState>,
296    status_tx: mpsc::UnboundedSender<String>,
297    protocol: Arc<TorRelayProtocol>,
298    circuit_manager: Arc<CircuitManager>,
299) -> Result<()> {
300    // Track the connection. Nothing did this before, so a relay with peers on it showed an
301    // empty peer list, no byte counters, and no row for the dashboard to hang a
302    // "[ message this peer ]" button on.
303    let local_addr = stream
304        .local_addr()
305        .unwrap_or_else(|_| "0.0.0.0:0".parse().expect("0.0.0.0:0 parses"));
306    let now = crate::utils::clock::Instant::now();
307    app_state
308        .add_connection_to_server(
309            server_id,
310            crate::state::server::ConnectionState {
311                id: connection_id,
312                remote_addr,
313                local_addr,
314                bytes_sent: 0,
315                bytes_received: 0,
316                packets_sent: 0,
317                packets_received: 0,
318                last_activity: now,
319                status: crate::state::server::ConnectionStatus::Active,
320                status_changed_at: now,
321                protocol_info: crate::state::server::ProtocolConnectionInfo::empty(),
322            },
323        )
324        .await;
325    let _ = status_tx.send("__UPDATE_UI__".to_string());
326
327    let result = run_tor_relay_connection(
328        stream,
329        connection_id,
330        server_id,
331        remote_addr,
332        acceptor,
333        llm_client,
334        app_state.clone(),
335        status_tx.clone(),
336        protocol,
337        circuit_manager,
338    )
339    .await;
340
341    // Every exit path lands here - a TLS handshake that never completed, a clean close, a cell
342    // error that returned `Err`, an injected disconnect. Idempotent with the peer task's own
343    // close path.
344    app_state
345        .remove_peer_handle(server_id, connection_id.as_u32())
346        .await;
347    app_state
348        .update_connection_status(
349            server_id,
350            connection_id,
351            crate::state::server::ConnectionStatus::Closed,
352        )
353        .await;
354    let _ = status_tx.send("__UPDATE_UI__".to_string());
355
356    result
357}
358
359/// The TLS handshake and the session itself. Split out so its `?` paths cannot skip the
360/// teardown in [`handle_tor_relay_connection`].
361#[allow(clippy::too_many_arguments)]
362async fn run_tor_relay_connection(
363    stream: TcpStream,
364    connection_id: crate::server::connection::ConnectionId,
365    server_id: crate::state::ServerId,
366    remote_addr: SocketAddr,
367    acceptor: TlsAcceptor,
368    llm_client: OllamaClient,
369    app_state: Arc<AppState>,
370    status_tx: mpsc::UnboundedSender<String>,
371    protocol: Arc<TorRelayProtocol>,
372    circuit_manager: Arc<CircuitManager>,
373) -> Result<()> {
374    // Perform TLS handshake
375    let tls_stream = match acceptor.accept(stream).await {
376        Ok(s) => {
377            Log::new(Some(&status_tx)).info(format!("TLS handshake completed for {}", remote_addr));
378            s
379        }
380        Err(e) => {
381            // Handshake failure is a client-side condition, not a server error: WARN.
382            Log::new(Some(&status_tx))
383                .warn(format!("TLS handshake failed for {}: {}", remote_addr, e));
384            return Err(e.into());
385        }
386    };
387
388    // Split the TLS stream (never clone it). The reader stays with the session's `select!`
389    // loop; the write half is shared with the peer-command task through an `Arc<Mutex<..>>`,
390    // which is the only lock either side takes and is never held across an `.await` that does
391    // anything but the write itself. Note the difference from `spawn_stream_forwarder` below:
392    // there a lock is held over a whole *exit* TcpStream whose task parks in `read()`, which
393    // is why only the read half is taken there. Here nothing reads through the mutex.
394    let (reader, writer) = tokio::io::split(tls_stream);
395    let write_half: SharedWrite = Arc::new(Mutex::new(writer));
396
397    // Peer messaging: registered BEFORE the first cell is read. A relay says nothing until the
398    // peer sends VERSIONS, and a manual `*` rule parks the events this connection raises for
399    // minutes - the operator has to be able to reach, or hang up, a connection that is waiting
400    // on their own answer.
401    let peer_rx = crate::server::peer_support::register_peer_channel(
402        &app_state,
403        server_id,
404        connection_id.as_u32(),
405    )
406    .await;
407    crate::server::peer_support::spawn_peer_command_task(
408        peer_rx,
409        protocol.clone(),
410        app_state.clone(),
411        server_id,
412        connection_id.as_u32(),
413        write_half.clone(),
414        status_tx.clone(),
415    );
416
417    // Create channel for outgoing cells
418    let (outgoing_tx, outgoing_rx) = mpsc::unbounded_channel();
419
420    let mut session = TorRelaySession {
421        reader,
422        write_half: write_half.clone(),
423        connection_id,
424        server_id,
425        remote_addr,
426        llm_client,
427        app_state,
428        status_tx,
429        protocol,
430        circuit_manager,
431        outgoing_tx,
432        outgoing_rx,
433    };
434
435    let result = session.handle().await;
436
437    // Make the FIN immediate rather than waiting for the peer task to drop its clone.
438    let _ = write_half.lock().await.shutdown().await;
439    result
440}
441
442/// Tor Relay session handler
443struct TorRelaySession {
444    /// Read half of the TLS stream, owned outright by the `select!` loop.
445    reader: tokio::io::ReadHalf<tokio_rustls::server::TlsStream<TcpStream>>,
446    /// Write half, shared with the dashboard's peer-command task.
447    write_half: SharedWrite,
448    connection_id: crate::server::connection::ConnectionId,
449    server_id: crate::state::ServerId,
450    remote_addr: SocketAddr,
451    llm_client: OllamaClient,
452    app_state: Arc<AppState>,
453    status_tx: mpsc::UnboundedSender<String>,
454    protocol: Arc<TorRelayProtocol>,
455    circuit_manager: Arc<CircuitManager>,
456    /// Channel for sending outgoing cells (from forwarder tasks)
457    outgoing_tx: mpsc::UnboundedSender<Vec<u8>>,
458    outgoing_rx: mpsc::UnboundedReceiver<Vec<u8>>,
459}
460
461impl TorRelaySession {
462    /// Handle Tor Relay session - read cells and process.
463    ///
464    /// Two properties this loop has to hold, both of which it used to break:
465    ///
466    /// 1. **Framing follows the cell, not a fixed size.** It used to `read_exact` into a
467    ///    514-byte buffer, so the first thing a real Tor client sends - an 11-byte VERSIONS
468    ///    cell - blocked it forever and `tor` logged `died in state handshaking`. Any
469    ///    variable-length cell (VERSIONS, or command >= 128) did the same.
470    /// 2. **The read is cancellation-safe.** `read_exact` is not: dropping it mid-cell when
471    ///    the `outgoing_rx` branch of the `select!` wins discards the bytes already taken
472    ///    off the TLS stream, permanently desynchronising the connection. `read` is
473    ///    cancel-safe, so bytes are accumulated in `inbuf` and cells are framed out of it.
474    async fn handle(&mut self) -> Result<()> {
475        debug!("Tor Relay session started for {}", self.remote_addr);
476
477        let mut inbuf: Vec<u8> = Vec::with_capacity(4096);
478        let mut read_buf = vec![0u8; 4096];
479        // False until the peer has sent anything at all; see FIRST_CELL_READ_TIMEOUT.
480        let mut seen_cell = false;
481        // Only the first cell of a connection may be a VERSIONS cell, and only a VERSIONS
482        // cell uses a 2-byte circuit id (tor-spec 3). After it, framing is unambiguous.
483        let mut expecting_versions = true;
484
485        loop {
486            // Drain every complete cell already buffered before waiting for more bytes.
487            while let Some(cell) = take_cell(&mut inbuf, expecting_versions) {
488                expecting_versions = false;
489
490                match cell {
491                    FramedCell::Variable {
492                        command, payload, ..
493                    } => {
494                        if let Some(response) = self.handle_variable_cell(command, &payload) {
495                            self.write_counted(&response).await?;
496                        }
497                    }
498                    FramedCell::Fixed { circuit_id, raw } => {
499                        let Some(cell_info) = parse_tor_cell(&raw) else {
500                            warn!("Failed to parse Tor cell from {}", self.remote_addr);
501                            continue;
502                        };
503                        debug!(
504                            "Tor cell: type={}, circuit_id={}",
505                            cell_info.cell_type, cell_info.circuit_id
506                        );
507
508                        match self.handle_cell(cell_info, &raw).await {
509                            Ok(Some(response)) => {
510                                self.write_counted(&response).await?;
511                                debug!("Sent response cell ({} bytes)", response.len());
512                            }
513                            Ok(None) => {}
514                            Err(e) => {
515                                error!("Failed to handle cell: {}", e);
516                                let destroy = self.create_destroy_cell(circuit_id);
517                                self.write_counted(&destroy).await?;
518                                return Err(e);
519                            }
520                        }
521                    }
522                }
523            }
524
525            // Re-arm the idle deadline for the wait that follows. It is set here, immediately
526            // before the `select!`, rather than kept running across the loop body — and that is
527            // deliberate: `handle_cell` above awaits the model, and may park the cell for a
528            // human at the dashboard (`src/state/intercepts.rs`, 300s by default). Arming the
529            // clock only over the wait means none of that time is ever counted as idle, so an
530            // answer that takes minutes cannot close the connection it is an answer for.
531            let idle_bound = if seen_cell {
532                IDLE_BETWEEN_CELLS_TIMEOUT
533            } else {
534                FIRST_CELL_READ_TIMEOUT
535            };
536            let idle_deadline = tokio::time::sleep(idle_bound);
537            tokio::pin!(idle_deadline);
538
539            tokio::select! {
540                // Nothing at all from the peer, and nothing to send it, for the whole bound.
541                _ = &mut idle_deadline => {
542                    Log::new(Some(&self.status_tx)).info(format!(
543                        "Tor Relay peer {} sent nothing for {}s; closing idle connection",
544                        self.remote_addr,
545                        idle_bound.as_secs()
546                    ));
547                    return Ok(());
548                }
549
550                // Read incoming bytes from the TLS stream. `read` is cancel-safe: if the
551                // other branch wins, nothing has been consumed.
552                read_result = self.reader.read(&mut read_buf) => {
553                    match read_result {
554                        Ok(0) => {
555                            Log::new(Some(&self.status_tx)).info(format!(
556                                "Tor Relay connection closed by {}",
557                                self.remote_addr
558                            ));
559                            return Ok(());
560                        }
561                        Ok(n) => {
562                            seen_cell = true;
563                            trace!("Received {} bytes from {}", n, self.remote_addr);
564                            // The rail's down-counter and the peer row's last_activity read
565                            // these; nothing updated them before, so every relay connection
566                            // showed 0/0 however much traffic crossed it.
567                            self.app_state
568                                .update_connection_stats(
569                                    self.server_id,
570                                    self.connection_id,
571                                    Some(n as u64),
572                                    None,
573                                    Some(1),
574                                    None,
575                                )
576                                .await;
577                            inbuf.extend_from_slice(&read_buf[..n]);
578                        }
579                        Err(e) => {
580                            error!("Failed to read Tor cell from {}: {}", self.remote_addr, e);
581                            return Err(e.into());
582                        }
583                    }
584                }
585
586                // Send outgoing cells from forwarder tasks
587                Some(cell) = self.outgoing_rx.recv() => {
588                    trace!("Sending outgoing cell ({} bytes)", cell.len());
589                    self.write_counted(&cell).await?;
590                }
591            }
592        }
593    }
594
595    /// Write one cell to the peer and count it.
596    ///
597    /// The guard is dropped before the stats update, so nothing awaits `AppState` while
598    /// holding the write half - the peer-command task needs the same lock to inject a cell or
599    /// a disconnect. A cell must reach the wire whole: a short write desynchronises the peer's
600    /// framing for the rest of the connection, which is why the lock is taken around
601    /// `write_all` + `flush` together rather than per call.
602    async fn write_counted(&self, data: &[u8]) -> Result<()> {
603        {
604            let mut writer = self.write_half.lock().await;
605            writer.write_all(data).await?;
606            writer.flush().await?;
607        }
608        self.app_state
609            .update_connection_stats(
610                self.server_id,
611                self.connection_id,
612                None,
613                Some(data.len() as u64),
614                None,
615                Some(1),
616            )
617            .await;
618        Ok(())
619    }
620
621    /// Handle a variable-length cell.
622    ///
623    /// VERSIONS is answered so that a peer's first cell no longer goes unacknowledged. The
624    /// rest of the link handshake (CERTS, AUTH_CHALLENGE, NETINFO) is still not implemented,
625    /// so a real Tor client gets this far and then gives up - which is a great deal better
626    /// than the reader blocking forever, but is still not interoperability.
627    fn handle_variable_cell(&self, command: u8, payload: &[u8]) -> Option<Vec<u8>> {
628        match command {
629            CELL_COMMAND_VERSIONS => {
630                let offered: Vec<u16> = payload
631                    .chunks_exact(2)
632                    .map(|c| u16::from_be_bytes([c[0], c[1]]))
633                    .collect();
634                Log::new(Some(&self.status_tx))
635                    .info(format!("Received VERSIONS cell offering {:?}", offered));
636
637                if !offered.contains(&LINK_PROTOCOL_VERSION) {
638                    warn!(
639                        "Peer offered link versions {:?}, none of which is {}",
640                        offered, LINK_PROTOCOL_VERSION
641                    );
642                }
643
644                // VERSIONS always uses a 2-byte circuit id (tor-spec 3).
645                let mut cell = Vec::with_capacity(7);
646                cell.extend_from_slice(&0u16.to_be_bytes());
647                cell.push(CELL_COMMAND_VERSIONS);
648                cell.extend_from_slice(&2u16.to_be_bytes());
649                cell.extend_from_slice(&LINK_PROTOCOL_VERSION.to_be_bytes());
650                Some(cell)
651            }
652            _ => {
653                warn!(
654                    "Ignoring variable-length cell command {} ({} byte payload): the link \
655                     handshake past VERSIONS is not implemented",
656                    command,
657                    payload.len()
658                );
659                None
660            }
661        }
662    }
663
664    /// Handle individual cell based on type
665    async fn handle_cell(
666        &mut self,
667        cell_info: TorCellInfo,
668        cell_data: &[u8],
669    ) -> Result<Option<Vec<u8>>> {
670        match cell_info.cell_type.as_str() {
671            "CREATE2" => self.handle_create2(cell_info.circuit_id, cell_data).await,
672            "RELAY" | "RELAY_EARLY" => self.handle_relay(cell_info.circuit_id, cell_data).await,
673            "DESTROY" => {
674                debug!(
675                    "Received DESTROY for circuit {}",
676                    cell_info.circuit_id.as_u32()
677                );
678                self.circuit_manager
679                    .destroy_circuit(cell_info.circuit_id)
680                    .await;
681                Ok(None)
682            }
683            "PADDING" => {
684                trace!("Received PADDING cell");
685                Ok(None)
686            }
687            _ => {
688                warn!("Unhandled cell type: {}", cell_info.cell_type);
689                Ok(None)
690            }
691        }
692    }
693
694    /// Handle CREATE2 cell
695    async fn handle_create2(
696        &mut self,
697        circuit_id: CircuitId,
698        cell_data: &[u8],
699    ) -> Result<Option<Vec<u8>>> {
700        debug!("Processing CREATE2 for circuit {}", circuit_id.as_u32());
701
702        // Parse CREATE2 cell:
703        // CircID (4) | Command (1) | HTYPE (2) | HLEN (2) | HDATA (HLEN)
704        if cell_data.len() < 9 {
705            return Err(anyhow::anyhow!("CREATE2 cell too short"));
706        }
707
708        let htype = u16::from_be_bytes([cell_data[5], cell_data[6]]);
709        let hlen = u16::from_be_bytes([cell_data[7], cell_data[8]]);
710
711        if htype != 0x0002 {
712            // ntor
713            return Err(anyhow::anyhow!("Unsupported handshake type: {}", htype));
714        }
715
716        if hlen != 84 {
717            // ntor client handshake is 84 bytes (ID:20 + B:32 + X:32)
718            return Err(anyhow::anyhow!("Invalid ntor handshake length: {}", hlen));
719        }
720
721        // Extract client's X (last 32 bytes of handshake data)
722        let handshake_start = 9;
723        let handshake_end = handshake_start + hlen as usize;
724        if cell_data.len() < handshake_end {
725            return Err(anyhow::anyhow!("Handshake data incomplete"));
726        }
727
728        let handshake_data = &cell_data[handshake_start..handshake_end];
729        // ntor handshake: ID(20) + B(32) + X(32)
730        let client_x: [u8; 32] = handshake_data[52..84].try_into()?;
731
732        // Perform ntor handshake via circuit manager
733        let (y, auth) = self
734            .circuit_manager
735            .handle_create2(circuit_id, client_x)
736            .await?;
737
738        Log::new(Some(&self.status_tx)).info(format!(
739            "Circuit {} created successfully",
740            circuit_id.as_u32()
741        ));
742
743        // Log relay statistics (summary: file-only)
744        let stats = self.circuit_manager.get_relay_stats().await;
745        Log::new(Some(&self.status_tx)).debug(format!(
746            "Relay stats: {} circuits, {} streams, sent={} received={}",
747            stats.total_circuits,
748            stats.total_streams,
749            stats.total_bytes_sent,
750            stats.total_bytes_received
751        ));
752
753        // Send event to LLM
754        let event = Event::new(
755            &TOR_RELAY_CIRCUIT_CREATED_EVENT,
756            serde_json::json!({
757                "circuit_id": format!("0x{:08x}", circuit_id.as_u32()),
758                "client_ip": self.remote_addr.ip().to_string(),
759            }),
760        );
761
762        // Act on what the LLM decides. Discarding this result was why the circuit-created
763        // event had no effect at all.
764        match call_llm(
765            &self.llm_client,
766            &self.app_state,
767            self.server_id,
768            Some(self.connection_id),
769            &event,
770            self.protocol.as_ref(),
771        )
772        .await
773        {
774            Ok(execution_result) => {
775                for message in execution_result.messages {
776                    let _ = self.status_tx.send(message);
777                }
778                for protocol_result in execution_result.protocol_results {
779                    match protocol_result {
780                        // A DESTROY chosen here replaces the CREATED2 we were about to send.
781                        ActionResult::Output(data) => {
782                            info!(
783                                "Tor relay circuit 0x{:08x}: CREATE2 answered by the model's \
784                                 own cell decision=model_answer",
785                                circuit_id.as_u32()
786                            );
787                            return Ok(Some(data));
788                        }
789                        ActionResult::CloseConnection => {
790                            info!(
791                                "Tor relay circuit 0x{:08x}: model closed the connection on \
792                                 CREATE2 decision=model_reject",
793                                circuit_id.as_u32()
794                            );
795                            self.circuit_manager.destroy_circuit(circuit_id).await;
796                            return Err(anyhow::anyhow!("LLM requested close"));
797                        }
798                        _ => {}
799                    }
800                }
801            }
802            Err(e) => {
803                // CREATED2 is admission: it completes the ntor handshake and hands the peer a
804                // circuit it will build on. The model was asked whether to admit this one and
805                // gave no answer, so admitting it anyway would turn a backend outage into
806                // "every circuit accepted". Tear it down instead, in Tor's own vocabulary:
807                // DESTROY (tor-spec 5.4) with reason 2 INTERNAL — the fault is the relay's,
808                // not the client's and not a protocol violation. The circuit's crypto state
809                // is dropped too, so what this relay believes and what the peer was told agree.
810                //
811                // Non-fatal to the connection: a DESTROY is the wire answer, so WARN.
812                Log::new(Some(&self.status_tx)).warn(format!(
813                    "Tor relay circuit 0x{:08x}: LLM call failed on CREATE2 ({}) - sent DESTROY \
814                     (reason 2 INTERNAL) decision=fail_closed_llm_error",
815                    circuit_id.as_u32(),
816                    e
817                ));
818                self.circuit_manager.destroy_circuit(circuit_id).await;
819                return Ok(Some(self.create_destroy_cell_with_reason(
820                    circuit_id,
821                    DESTROY_REASON_INTERNAL,
822                )));
823            }
824        }
825
826        info!(
827            "Tor relay circuit 0x{:08x}: CREATED2 sent decision=model_answer",
828            circuit_id.as_u32()
829        );
830
831        // Build CREATED2 response
832        // CircID (4) | Command (1) | HLEN (2) | HDATA (HLEN)
833        let mut response = Vec::with_capacity(71);
834        response.extend_from_slice(&circuit_id.to_bytes());
835        response.push(11); // CREATED2 command (tor-spec 3; 10 is CREATE2)
836        response.extend_from_slice(&64u16.to_be_bytes()); // HLEN = 64 (Y:32 + AUTH:32)
837        response.extend_from_slice(&y);
838        response.extend_from_slice(&auth);
839
840        // Pad to 514 bytes
841        response.resize(514, 0);
842
843        Ok(Some(response))
844    }
845
846    /// Handle RELAY cell
847    async fn handle_relay(
848        &mut self,
849        circuit_id: CircuitId,
850        cell_data: &[u8],
851    ) -> Result<Option<Vec<u8>>> {
852        trace!("Processing RELAY cell for circuit {}", circuit_id.as_u32());
853
854        // Extract relay payload (skip CircID:4 + Command:1)
855        let mut relay_payload = cell_data[5..514].to_vec();
856
857        // Decrypt relay cell
858        self.circuit_manager
859            .decrypt_relay_cell(circuit_id, &mut relay_payload)
860            .await?;
861
862        // Track bytes received from client (entire cell)
863        let _ = self.circuit_manager.record_received(circuit_id, 509).await;
864
865        // Track RELAY cell for circuit-level flow control - send SENDME if needed
866        let send_circuit_sendme = self
867            .circuit_manager
868            .record_relay_received(circuit_id)
869            .await
870            .unwrap_or(false);
871        if send_circuit_sendme {
872            debug!(
873                "Sending circuit-level SENDME for circuit {}",
874                circuit_id.as_u32()
875            );
876            let sendme_cell = build_relay_cell(
877                circuit_id.as_u32(),
878                0, // Stream ID 0 for circuit-level SENDME
879                relay_command::SENDME,
880                &[],
881            );
882            let mut encrypted = sendme_cell.clone();
883            self.circuit_manager
884                .encrypt_relay_cell(circuit_id, &mut encrypted[5..514])
885                .await?;
886            let _ = self.outgoing_tx.send(encrypted);
887        }
888
889        // Parse relay cell header:
890        // Command (1) | Recognized (2) | StreamID (2) | Digest (4) | Length (2) | Data (Length)
891        if relay_payload.len() < 11 {
892            return Err(anyhow::anyhow!("RELAY cell too short"));
893        }
894
895        let relay_cmd = relay_payload[0];
896        let recognized = u16::from_be_bytes([relay_payload[1], relay_payload[2]]);
897        let stream_id_u16 = u16::from_be_bytes([relay_payload[3], relay_payload[4]]);
898        let stream_id = StreamId::new(stream_id_u16);
899        let length = u16::from_be_bytes([relay_payload[9], relay_payload[10]]) as usize;
900
901        // `length` comes from the decrypted payload, so it is whatever the peer's ciphertext
902        // decrypts to - up to 65535 - while the payload is only 509 bytes. Slicing on it
903        // unchecked panicked the connection task on essentially any malformed or
904        // wrongly-keyed cell.
905        if 11 + length > relay_payload.len() {
906            warn!(
907                "RELAY cell on circuit {} declares {} bytes of data but only {} remain; dropping",
908                circuit_id.as_u32(),
909                length,
910                relay_payload.len().saturating_sub(11)
911            );
912            return Ok(None);
913        }
914        let data = &relay_payload[11..11 + length];
915
916        // Check if this cell is for us (recognized should be 0)
917        if recognized != 0 {
918            // Not for us, would normally forward but we're endpoint
919            trace!("RELAY cell not recognized, dropping");
920            return Ok(None);
921        }
922
923        let relay_cmd_name = match relay_cmd {
924            relay_command::BEGIN => "BEGIN",
925            relay_command::DATA => "DATA",
926            relay_command::END => "END",
927            relay_command::CONNECTED => "CONNECTED",
928            relay_command::SENDME => "SENDME",
929            relay_command::EXTEND => "EXTEND",
930            relay_command::EXTENDED => "EXTENDED",
931            relay_command::TRUNCATE => "TRUNCATE",
932            relay_command::TRUNCATED => "TRUNCATED",
933            relay_command::DROP => "DROP",
934            relay_command::RESOLVE => "RESOLVE",
935            relay_command::RESOLVED => "RESOLVED",
936            relay_command::BEGIN_DIR => "BEGIN_DIR",
937            _ => "UNKNOWN",
938        };
939
940        debug!(
941            "RELAY cell: command={} ({}), stream={}, length={}",
942            relay_cmd,
943            relay_cmd_name,
944            stream_id.as_u16(),
945            length
946        );
947
948        // Handle specific relay commands
949        match relay_cmd {
950            relay_command::BEGIN => {
951                return self.handle_begin_cell(circuit_id, stream_id, data).await;
952            }
953            relay_command::BEGIN_DIR => {
954                return self.handle_begin_dir_cell(circuit_id, stream_id).await;
955            }
956            relay_command::DATA => {
957                return self.handle_data_cell(circuit_id, stream_id, data).await;
958            }
959            relay_command::END => {
960                return self.handle_end_cell(circuit_id, stream_id, data).await;
961            }
962            relay_command::SENDME => {
963                return self.handle_sendme_cell(circuit_id, stream_id).await;
964            }
965            _ => {
966                // For other commands, send event to LLM
967                let event = Event::new(
968                    &TOR_RELAY_RELAY_CELL_EVENT,
969                    serde_json::json!({
970                        "circuit_id": format!("0x{:08x}", circuit_id.as_u32()),
971                        "relay_command": relay_cmd_name,
972                        "stream_id": stream_id.as_u16(),
973                        "length": length,
974                        "client_ip": self.remote_addr.ip().to_string(),
975                    }),
976                );
977
978                // Get LLM response for how to handle this
979                match call_llm(
980                    &self.llm_client,
981                    &self.app_state,
982                    self.server_id,
983                    Some(self.connection_id),
984                    &event,
985                    self.protocol.as_ref(),
986                )
987                .await
988                {
989                    Ok(execution_result) => {
990                        for message in execution_result.messages {
991                            let _ = self.status_tx.send(message);
992                        }
993                        // Execute protocol actions
994                        for protocol_result in execution_result.protocol_results {
995                            match protocol_result {
996                                ActionResult::Output(data) => {
997                                    debug!(
998                                        "Tor relay circuit 0x{:08x}: RELAY {} answered \
999                                         decision=model_answer",
1000                                        circuit_id.as_u32(),
1001                                        relay_cmd_name
1002                                    );
1003                                    return Ok(Some(data));
1004                                }
1005                                ActionResult::CloseConnection => {
1006                                    info!(
1007                                        "Tor relay circuit 0x{:08x}: model closed the \
1008                                         connection on RELAY {} decision=model_reject",
1009                                        circuit_id.as_u32(),
1010                                        relay_cmd_name
1011                                    );
1012                                    return Err(anyhow::anyhow!("LLM requested close"));
1013                                }
1014                                _ => {}
1015                            }
1016                        }
1017                        debug!(
1018                            "Tor relay circuit 0x{:08x}: RELAY {} left unanswered by the model \
1019                             decision=model_silent",
1020                            circuit_id.as_u32(),
1021                            relay_cmd_name
1022                        );
1023                    }
1024                    Err(e) => {
1025                        // This branch is reached only for RELAY commands the relay does not
1026                        // implement itself (EXTEND, TRUNCATE, RESOLVE, DROP, ...), so the
1027                        // model was the entire answer. Swallowing the error left the peer
1028                        // waiting on a circuit that would never say anything again.
1029                        //
1030                        // Tor's own vocabulary for "this relay cannot carry on with your
1031                        // circuit" is DESTROY (tor-spec 5.4), with reason 2 INTERNAL: an
1032                        // error at the relay, not at the client and not a protocol violation.
1033                        // It needs no relay-cell encryption, so it is still deliverable when
1034                        // the circuit crypto is the thing that is unhappy, and it is the one
1035                        // answer that cannot be mistaken for the EXTENDED/RESOLVED/TRUNCATED
1036                        // the peer asked for. There is no "try again" cell here to express an
1037                        // overload with, so overload and outage are answered identically and
1038                        // distinguished only in the log.
1039                        // Non-fatal: a DESTROY cell (reason 2 INTERNAL) is the wire answer, so WARN.
1040                        Log::new(Some(&self.status_tx)).warn(format!(
1041                            "Tor relay circuit 0x{:08x}: LLM call failed on RELAY {} ({}) - sent \
1042                             DESTROY (reason 2 INTERNAL) decision=fail_closed_llm_error",
1043                            circuit_id.as_u32(),
1044                            relay_cmd_name,
1045                            e
1046                        ));
1047                        if crate::llm::is_overload_error(&e) {
1048                            warn!(
1049                                "Tor relay circuit {} destroyed: LLM capacity exhausted",
1050                                circuit_id.as_u32()
1051                            );
1052                        }
1053                        // Drop the circuit's own state too, so the DESTROY we send and what
1054                        // this relay believes are the same thing.
1055                        self.circuit_manager.destroy_circuit(circuit_id).await;
1056                        return Ok(Some(self.create_destroy_cell_with_reason(
1057                            circuit_id,
1058                            DESTROY_REASON_INTERNAL,
1059                        )));
1060                    }
1061                }
1062            }
1063        }
1064
1065        // Default: no response
1066        Ok(None)
1067    }
1068
1069    /// Handle BEGIN cell - establish TCP connection to target
1070    async fn handle_begin_cell(
1071        &mut self,
1072        circuit_id: CircuitId,
1073        stream_id: StreamId,
1074        data: &[u8],
1075    ) -> Result<Option<Vec<u8>>> {
1076        // Parse target address
1077        let target = parse_begin_target(data)?;
1078
1079        Log::new(Some(&self.status_tx)).info(format!(
1080            "BEGIN stream {} on circuit {} to {}",
1081            stream_id.as_u16(),
1082            circuit_id.as_u32(),
1083            target
1084        ));
1085
1086        // Create stream in circuit manager.
1087        //
1088        // A refusal here is a per-stream condition, not a per-circuit one: the peer has hit
1089        // MAX_STREAMS_PER_CIRCUIT, or reused a live stream id. Answer END/RESOURCE_LIMIT and
1090        // keep the circuit up. Propagating with `?` would take the whole connection down
1091        // through the DESTROY path in `handle()`, which hands a peer that opens too many
1092        // streams a way to kill everything else riding the same circuit.
1093        if let Err(e) = self
1094            .circuit_manager
1095            .create_stream(circuit_id, stream_id, target.clone())
1096            .await
1097        {
1098            Log::new(Some(&self.status_tx)).warn(format!(
1099                "Refusing BEGIN stream {} on circuit {}: {}",
1100                stream_id.as_u16(),
1101                circuit_id.as_u32(),
1102                e
1103            ));
1104            let end_cell = build_relay_cell(
1105                circuit_id.as_u32(),
1106                stream_id.as_u16(),
1107                relay_command::END,
1108                &[end_reason::RESOURCE_LIMIT],
1109            );
1110            let mut encrypted = end_cell.clone();
1111            self.circuit_manager
1112                .encrypt_relay_cell(circuit_id, &mut encrypted[5..514])
1113                .await?;
1114            let _ = self.circuit_manager.record_sent(circuit_id, 509).await;
1115            return Ok(Some(encrypted));
1116        }
1117
1118        // Attempt to connect to target
1119        match connect_to_target(&target).await {
1120            Ok(tcp_stream) => {
1121                Log::new(Some(&self.status_tx)).info(format!(
1122                    "Connected to {} for stream {}",
1123                    target,
1124                    stream_id.as_u16()
1125                ));
1126
1127                // Store TCP connection in stream
1128                self.circuit_manager
1129                    .set_stream_active(circuit_id, stream_id, tcp_stream)
1130                    .await?;
1131
1132                // Build CONNECTED response
1133                let connected_cell = build_relay_cell(
1134                    circuit_id.as_u32(),
1135                    stream_id.as_u16(),
1136                    relay_command::CONNECTED,
1137                    &[],
1138                );
1139
1140                // Encrypt response
1141                let mut encrypted = connected_cell.clone();
1142                self.circuit_manager
1143                    .encrypt_relay_cell(circuit_id, &mut encrypted[5..514])
1144                    .await?;
1145
1146                // Track bytes sent to client
1147                let _ = self.circuit_manager.record_sent(circuit_id, 509).await;
1148
1149                // Start forwarding task for this stream
1150                self.spawn_stream_forwarder(circuit_id, stream_id, self.outgoing_tx.clone())
1151                    .await?;
1152
1153                Ok(Some(encrypted))
1154            }
1155            Err(e) => {
1156                // Non-fatal: an END cell (CONNECT_REFUSED) is sent to the client, so WARN.
1157                Log::new(Some(&self.status_tx))
1158                    .warn(format!("Failed to connect to {}: {}", target, e));
1159
1160                // Close stream
1161                let _ = self
1162                    .circuit_manager
1163                    .close_stream(circuit_id, stream_id)
1164                    .await;
1165
1166                // Build END response with error reason
1167                let end_cell = build_relay_cell(
1168                    circuit_id.as_u32(),
1169                    stream_id.as_u16(),
1170                    relay_command::END,
1171                    &[end_reason::CONNECT_REFUSED],
1172                );
1173
1174                // Encrypt response
1175                let mut encrypted = end_cell.clone();
1176                self.circuit_manager
1177                    .encrypt_relay_cell(circuit_id, &mut encrypted[5..514])
1178                    .await?;
1179
1180                // Track bytes sent to client
1181                let _ = self.circuit_manager.record_sent(circuit_id, 509).await;
1182
1183                Ok(Some(encrypted))
1184            }
1185        }
1186    }
1187
1188    /// Handle BEGIN_DIR cell - create directory stream for serving consensus
1189    async fn handle_begin_dir_cell(
1190        &mut self,
1191        circuit_id: CircuitId,
1192        stream_id: StreamId,
1193    ) -> Result<Option<Vec<u8>>> {
1194        Log::new(Some(&self.status_tx)).info(format!(
1195            "BEGIN_DIR stream {} on circuit {} (directory request over circuit)",
1196            stream_id.as_u16(),
1197            circuit_id.as_u32()
1198        ));
1199
1200        // Create directory stream in circuit manager
1201        self.circuit_manager
1202            .create_directory_stream(circuit_id, stream_id)
1203            .await?;
1204
1205        info!(
1206            "Directory stream {} ready on circuit {}",
1207            stream_id.as_u16(),
1208            circuit_id.as_u32()
1209        );
1210
1211        // Build CONNECTED response (same as normal stream)
1212        let connected_cell = build_relay_cell(
1213            circuit_id.as_u32(),
1214            stream_id.as_u16(),
1215            relay_command::CONNECTED,
1216            &[],
1217        );
1218
1219        // Encrypt response
1220        let mut encrypted = connected_cell.clone();
1221        self.circuit_manager
1222            .encrypt_relay_cell(circuit_id, &mut encrypted[5..514])
1223            .await?;
1224
1225        Ok(Some(encrypted))
1226    }
1227
1228    /// Handle DATA cell - forward data to TCP connection or handle directory request
1229    async fn handle_data_cell(
1230        &mut self,
1231        circuit_id: CircuitId,
1232        stream_id: StreamId,
1233        data: &[u8],
1234    ) -> Result<Option<Vec<u8>>> {
1235        trace!(
1236            "DATA cell for stream {} ({} bytes)",
1237            stream_id.as_u16(),
1238            data.len()
1239        );
1240
1241        // Track DATA cell for stream-level flow control - send SENDME if needed
1242        let send_stream_sendme = self
1243            .circuit_manager
1244            .record_stream_data_received(circuit_id, stream_id)
1245            .await
1246            .unwrap_or(false);
1247
1248        if send_stream_sendme {
1249            debug!(
1250                "Sending stream-level SENDME for stream {}",
1251                stream_id.as_u16()
1252            );
1253            let sendme_cell = build_relay_cell(
1254                circuit_id.as_u32(),
1255                stream_id.as_u16(),
1256                relay_command::SENDME,
1257                &[],
1258            );
1259            let mut encrypted = sendme_cell.clone();
1260            self.circuit_manager
1261                .encrypt_relay_cell(circuit_id, &mut encrypted[5..514])
1262                .await?;
1263            let _ = self.circuit_manager.record_sent(circuit_id, 509).await;
1264            let _ = self.outgoing_tx.send(encrypted);
1265        }
1266
1267        // Check if this is a directory stream (BEGIN_DIR)
1268        if self
1269            .circuit_manager
1270            .is_directory_stream(circuit_id, stream_id)
1271            .await?
1272        {
1273            return self
1274                .handle_directory_data(circuit_id, stream_id, data)
1275                .await;
1276        }
1277
1278        // Get the write half of this stream's TCP connection
1279        if let Some(connection) = self
1280            .circuit_manager
1281            .get_stream_writer(circuit_id, stream_id)
1282            .await?
1283        {
1284            // Write data to TCP connection
1285            let mut conn = connection.lock().await;
1286            if let Err(e) = conn.write_all(data).await {
1287                error!("Failed to write to stream {}: {}", stream_id.as_u16(), e);
1288
1289                // Close stream
1290                drop(conn); // Release lock before closing
1291                let _ = self
1292                    .circuit_manager
1293                    .close_stream(circuit_id, stream_id)
1294                    .await;
1295
1296                // Send END cell
1297                let end_cell = build_relay_cell(
1298                    circuit_id.as_u32(),
1299                    stream_id.as_u16(),
1300                    relay_command::END,
1301                    &[end_reason::MISC],
1302                );
1303
1304                let mut encrypted = end_cell.clone();
1305                self.circuit_manager
1306                    .encrypt_relay_cell(circuit_id, &mut encrypted[5..514])
1307                    .await?;
1308
1309                // Track bytes sent to client
1310                let _ = self.circuit_manager.record_sent(circuit_id, 509).await;
1311
1312                return Ok(Some(encrypted));
1313            }
1314
1315            trace!(
1316                "Forwarded {} bytes to stream {}",
1317                data.len(),
1318                stream_id.as_u16()
1319            );
1320        } else {
1321            warn!("Stream {} not found or not active", stream_id.as_u16());
1322        }
1323
1324        Ok(None)
1325    }
1326
1327    /// Handle directory stream DATA - accumulate HTTP request and respond
1328    async fn handle_directory_data(
1329        &mut self,
1330        circuit_id: CircuitId,
1331        stream_id: StreamId,
1332        data: &[u8],
1333    ) -> Result<Option<Vec<u8>>> {
1334        // Append data to directory request buffer
1335        self.circuit_manager
1336            .append_directory_data(circuit_id, stream_id, data)
1337            .await?;
1338
1339        // Get accumulated request
1340        let request_data = self
1341            .circuit_manager
1342            .get_directory_request(circuit_id, stream_id)
1343            .await?;
1344
1345        // Check if we have a complete HTTP request (ends with \r\n\r\n)
1346        if let Some(pos) = request_data.windows(4).position(|w| w == b"\r\n\r\n") {
1347            // Extract HTTP request line
1348            let request_str = String::from_utf8_lossy(&request_data[..pos]);
1349            let first_line = request_str.lines().next().unwrap_or("");
1350
1351            debug!("Directory request: {}", first_line);
1352
1353            // Parse HTTP method and path
1354            let parts: Vec<&str> = first_line.split_whitespace().collect();
1355            let (_method, path) = if parts.len() >= 2 {
1356                (parts[0], parts[1])
1357            } else {
1358                ("GET", "/")
1359            };
1360
1361            // Generate consensus response
1362            // TODO: Call LLM to generate dynamic consensus based on path and instruction
1363            let consensus = if path.contains("consensus") {
1364                Self::generate_test_consensus()
1365            } else {
1366                b"HTTP/1.0 404 Not Found\r\n\r\n".to_vec()
1367            };
1368
1369            info!(
1370                "Serving {} response ({} bytes) for directory path: {}",
1371                if path.contains("consensus") {
1372                    "consensus"
1373                } else {
1374                    "404"
1375                },
1376                consensus.len(),
1377                path
1378            );
1379
1380            // Send response as DATA cells
1381            self.send_directory_response(circuit_id, stream_id, &consensus)
1382                .await?;
1383
1384            // Close stream after response
1385            let end_cell = build_relay_cell(
1386                circuit_id.as_u32(),
1387                stream_id.as_u16(),
1388                relay_command::END,
1389                &[end_reason::DONE],
1390            );
1391
1392            let mut encrypted = end_cell.clone();
1393            self.circuit_manager
1394                .encrypt_relay_cell(circuit_id, &mut encrypted[5..514])
1395                .await?;
1396
1397            return Ok(Some(encrypted));
1398        }
1399
1400        Ok(None)
1401    }
1402
1403    /// Send directory response data as multiple DATA cells
1404    async fn send_directory_response(
1405        &mut self,
1406        circuit_id: CircuitId,
1407        stream_id: StreamId,
1408        data: &[u8],
1409    ) -> Result<()> {
1410        const MAX_RELAY_PAYLOAD: usize = 498; // tor-spec: relay cell payload max
1411
1412        for chunk in data.chunks(MAX_RELAY_PAYLOAD) {
1413            let data_cell = build_relay_cell(
1414                circuit_id.as_u32(),
1415                stream_id.as_u16(),
1416                relay_command::DATA,
1417                chunk,
1418            );
1419
1420            let mut encrypted = data_cell.clone();
1421            self.circuit_manager
1422                .encrypt_relay_cell(circuit_id, &mut encrypted[5..514])
1423                .await?;
1424
1425            self.outgoing_tx.send(encrypted)?;
1426            let _ = self.circuit_manager.record_sent(circuit_id, 509).await;
1427        }
1428
1429        Ok(())
1430    }
1431
1432    /// Handle END cell - close stream
1433    async fn handle_end_cell(
1434        &mut self,
1435        circuit_id: CircuitId,
1436        stream_id: StreamId,
1437        data: &[u8],
1438    ) -> Result<Option<Vec<u8>>> {
1439        let reason = if data.is_empty() {
1440            end_reason::DONE
1441        } else {
1442            data[0]
1443        };
1444
1445        Log::new(Some(&self.status_tx)).debug(format!(
1446            "END stream {} (reason: {})",
1447            stream_id.as_u16(),
1448            reason
1449        ));
1450
1451        // Close stream
1452        let _ = self
1453            .circuit_manager
1454            .close_stream(circuit_id, stream_id)
1455            .await;
1456
1457        Ok(None)
1458    }
1459
1460    /// Handle SENDME cell - update flow control windows
1461    async fn handle_sendme_cell(
1462        &mut self,
1463        circuit_id: CircuitId,
1464        stream_id: StreamId,
1465    ) -> Result<Option<Vec<u8>>> {
1466        if stream_id.as_u16() == 0 {
1467            // Circuit-level SENDME
1468            debug!(
1469                "Received circuit-level SENDME for circuit {}",
1470                circuit_id.as_u32()
1471            );
1472            let _ = self
1473                .circuit_manager
1474                .process_circuit_sendme(circuit_id)
1475                .await;
1476        } else {
1477            // Stream-level SENDME
1478            debug!(
1479                "Received stream-level SENDME for stream {}",
1480                stream_id.as_u16()
1481            );
1482            let _ = self
1483                .circuit_manager
1484                .process_stream_sendme(circuit_id, stream_id)
1485                .await;
1486        }
1487        Ok(None)
1488    }
1489
1490    /// Generate a test consensus document for Arti bootstrap
1491    /// Returns HTTP response with minimal but valid consensus
1492    fn generate_test_consensus() -> Vec<u8> {
1493        use crate::utils::clock::{SystemTime, UNIX_EPOCH};
1494
1495        // Get current time for valid-after/until
1496        let now = SystemTime::now()
1497            .duration_since(UNIX_EPOCH)
1498            .unwrap()
1499            .as_secs();
1500        let hour = now - (now % 3600); // Round down to hour
1501
1502        // Format timestamps (RFC 3339 style for Tor)
1503        let valid_after = chrono::DateTime::from_timestamp(hour as i64, 0)
1504            .unwrap()
1505            .format("%Y-%m-%d %H:%M:%S")
1506            .to_string();
1507        let fresh_until = chrono::DateTime::from_timestamp((hour + 3600) as i64, 0)
1508            .unwrap()
1509            .format("%Y-%m-%d %H:%M:%S")
1510            .to_string();
1511        let valid_until = chrono::DateTime::from_timestamp((hour + 10800) as i64, 0)
1512            .unwrap()
1513            .format("%Y-%m-%d %H:%M:%S")
1514            .to_string();
1515
1516        let consensus_body = format!(
1517            "network-status-version 3\n\
1518             vote-status consensus\n\
1519             consensus-method 35\n\
1520             valid-after {}\n\
1521             fresh-until {}\n\
1522             valid-until {}\n\
1523             voting-delay 300 300\n\
1524             client-versions 0.4.7.0-alpha-dev,0.4.8.0\n\
1525             server-versions 0.4.7.0-alpha-dev,0.4.8.0\n\
1526             known-flags Authority BadExit Exit Fast Guard HSDir NoEdConsensus Running Stable StaleDesc Sybil V2Dir Valid\n\
1527             params CircuitPriorityHalflifeMsec=30000 DoSCircuitCreationEnabled=1 DoSConnectionEnabled=1\n\
1528             r TestRelay1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= 127.0.0.1 9001 127.0.0.1 9030\n\
1529             s Exit Fast Guard HSDir Running Stable V2Dir Valid\n\
1530             v Tor 0.4.8.0\n\
1531             w Bandwidth=5000\n\
1532             p accept 1-65535\n\
1533             r TestRelay2 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB= 127.0.0.2 9001 127.0.0.2 9030\n\
1534             s Exit Fast Guard HSDir Running Stable V2Dir Valid\n\
1535             v Tor 0.4.8.0\n\
1536             w Bandwidth=5000\n\
1537             p accept 1-65535\n\
1538             r TestRelay3 CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC= 127.0.0.3 9001 127.0.0.3 9030\n\
1539             s Exit Fast Guard HSDir Running Stable V2Dir Valid\n\
1540             v Tor 0.4.8.0\n\
1541             w Bandwidth=5000\n\
1542             p accept 1-65535\n\
1543             r TestRelay4 DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD= 127.0.0.4 9001 127.0.0.4 9030\n\
1544             s Exit Fast Guard HSDir Running Stable V2Dir Valid\n\
1545             v Tor 0.4.8.0\n\
1546             w Bandwidth=5000\n\
1547             p accept 1-65535\n\
1548             directory-footer\n\
1549             bandwidth-weights Wbd=0 Wbe=0 Wbg=4096 Wbm=10000 Wdb=10000 Web=10000 Wed=10000 Wee=10000 Weg=10000 Wem=10000 Wgb=10000 Wgd=0 Wgg=5920 Wgm=5920 Wmb=10000 Wmd=0 Wme=0 Wmg=4096 Wmm=10000\n\
1550             directory-signature 0000000000000000000000000000000000000000 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n\
1551             -----BEGIN SIGNATURE-----\n\
1552             AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n\
1553             -----END SIGNATURE-----\n",
1554            valid_after, fresh_until, valid_until
1555        );
1556
1557        // Build HTTP response
1558        let http_response = format!(
1559            "HTTP/1.0 200 OK\r\n\
1560             Content-Type: text/plain\r\n\
1561             Content-Length: {}\r\n\
1562             \r\n\
1563             {}",
1564            consensus_body.len(),
1565            consensus_body
1566        );
1567
1568        http_response.into_bytes()
1569    }
1570
1571    /// Spawn background task to forward data from TCP connection back to Tor client
1572    async fn spawn_stream_forwarder(
1573        &self,
1574        circuit_id: CircuitId,
1575        stream_id: StreamId,
1576        outgoing_tx: mpsc::UnboundedSender<Vec<u8>>,
1577    ) -> Result<()> {
1578        // Read half only. Holding a lock over the whole TcpStream here parked the
1579        // client-to-target write path forever, because this task waits in `read()`.
1580        let connection = self
1581            .circuit_manager
1582            .get_stream_reader(circuit_id, stream_id)
1583            .await?
1584            .ok_or_else(|| anyhow::anyhow!("Stream not found"))?;
1585
1586        let circuit_mgr = self.circuit_manager.clone();
1587        let status_tx = self.status_tx.clone();
1588
1589        // Tracked, not detached: stop_server must abort this task too.
1590        let task_owner = self.app_state.clone();
1591        task_owner
1592            .spawn_server_task(self.server_id, async move {
1593                let mut buffer = vec![0u8; 498]; // Max relay cell data size
1594
1595                loop {
1596                    let bytes_read = {
1597                        let mut conn = connection.lock().await;
1598                        match conn.read(&mut buffer).await {
1599                            Ok(0) => {
1600                                // EOF - connection closed
1601                                debug!("Stream {} EOF from target", stream_id.as_u16());
1602                                break;
1603                            }
1604                            Ok(n) => n,
1605                            Err(e) => {
1606                                error!("Failed to read from stream {}: {}", stream_id.as_u16(), e);
1607                                break;
1608                            }
1609                        }
1610                    };
1611
1612                    // Build DATA relay cell
1613                    let mut data_cell = build_relay_cell(
1614                        circuit_id.as_u32(),
1615                        stream_id.as_u16(),
1616                        relay_command::DATA,
1617                        &buffer[..bytes_read],
1618                    );
1619
1620                    // Encrypt relay cell payload
1621                    if let Err(e) = circuit_mgr
1622                        .encrypt_relay_cell(circuit_id, &mut data_cell[5..514])
1623                        .await
1624                    {
1625                        error!(
1626                            "Failed to encrypt DATA cell for stream {}: {}",
1627                            stream_id.as_u16(),
1628                            e
1629                        );
1630                        break;
1631                    }
1632
1633                    // Track bytes sent to client
1634                    let _ = circuit_mgr.record_sent(circuit_id, 509).await;
1635
1636                    // Send encrypted cell through channel
1637                    if let Err(e) = outgoing_tx.send(data_cell) {
1638                        error!(
1639                            "Failed to send DATA cell for stream {}: {}",
1640                            stream_id.as_u16(),
1641                            e
1642                        );
1643                        break;
1644                    }
1645
1646                    trace!(
1647                        "Forwarded {} bytes from stream {} back to client",
1648                        bytes_read,
1649                        stream_id.as_u16()
1650                    );
1651                }
1652
1653                // Send END cell when stream closes
1654                let mut end_cell = build_relay_cell(
1655                    circuit_id.as_u32(),
1656                    stream_id.as_u16(),
1657                    relay_command::END,
1658                    &[end_reason::DONE],
1659                );
1660
1661                // Encrypt END cell
1662                if let Ok(_) = circuit_mgr
1663                    .encrypt_relay_cell(circuit_id, &mut end_cell[5..514])
1664                    .await
1665                {
1666                    let _ = circuit_mgr.record_sent(circuit_id, 509).await;
1667                    let _ = outgoing_tx.send(end_cell);
1668                }
1669
1670                // Close stream
1671                let _ = circuit_mgr.close_stream(circuit_id, stream_id).await;
1672                Log::new(Some(&status_tx)).debug(format!("Stream {} closed", stream_id.as_u16()));
1673            })
1674            .await;
1675
1676        Ok(())
1677    }
1678
1679    /// Create a DESTROY cell (tor-spec 5.4) carrying `reason`.
1680    fn create_destroy_cell_with_reason(&self, circuit_id: CircuitId, reason: u8) -> Vec<u8> {
1681        let mut cell = Vec::with_capacity(FIXED_CELL_LEN);
1682        cell.extend_from_slice(&circuit_id.to_bytes());
1683        cell.push(4); // DESTROY command
1684        cell.push(reason);
1685        cell.resize(FIXED_CELL_LEN, 0); // Pad to 514 bytes
1686        cell
1687    }
1688
1689    /// Create DESTROY cell for a malformed or unprocessable cell.
1690    fn create_destroy_cell(&self, circuit_id: CircuitId) -> Vec<u8> {
1691        self.create_destroy_cell_with_reason(circuit_id, DESTROY_REASON_PROTOCOL)
1692    }
1693}
1694
1695/// Tor cell information
1696#[derive(Debug, Clone)]
1697struct TorCellInfo {
1698    circuit_id: CircuitId,
1699    cell_type: String,
1700}
1701
1702/// The link protocol version this relay frames cells for: 4-byte circuit ids, 514-byte
1703/// fixed cells.
1704/// How long to wait for a peer's first cell after the TLS handshake.
1705///
1706/// The Tor link protocol is client-speaks-first once TLS is up: the VERSIONS cell is the first
1707/// thing a client sends and the relay says nothing before it. A peer that has completed TLS and
1708/// then gone quiet has begun no link handshake, so it gets the short bound — and this is the
1709/// one an unauthenticated flood lives under, since it never reaches the idle bound below.
1710const FIRST_CELL_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
1711
1712/// How long to wait for a *further* cell once the link is up.
1713///
1714/// Fifteen minutes, derived from Tor's own answer to the same question. `KeepalivePeriod`
1715/// defaults to five minutes: a relay sends a PADDING cell that often on an open connection
1716/// precisely so an idle-but-live link keeps proving it is live through firewalls that would
1717/// otherwise drop it. Three of those periods with neither a cell nor a padding keepalive means
1718/// the peer is gone, not quiet. (Tor's `CircuitIdleTimeout` — an hour — is about circuits, not
1719/// about a connection that is producing nothing at all, so it is not the number to copy here.)
1720const IDLE_BETWEEN_CELLS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(900);
1721
1722/// Concurrent inbound connections this relay admits.
1723const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
1724
1725/// What a peer over [`MAX_CONNECTIONS`] is told before the socket closes.
1726///
1727/// The cap is applied on the raw TCP accept, before the TLS handshake, so the only vocabulary
1728/// available is TLS's: a plaintext fatal alert record — content type 21, legacy record version
1729/// 0x0303, length 2, level `fatal`(2), description `internal_error`(80). TLS has no "server
1730/// busy" alert and the Tor link protocol has nothing at all that may precede VERSIONS, so this
1731/// is as close as either layer comes. A client reports a fatal alert rather than a bare reset.
1732const CONNECTION_CAP_REFUSAL: &[u8] = &[0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x50];
1733
1734const LINK_PROTOCOL_VERSION: u16 = 4;
1735/// tor-spec 3, command 7. Variable length, and the only cell with a 2-byte circuit id.
1736const CELL_COMMAND_VERSIONS: u8 = 7;
1737/// Fixed cell size for link protocol v4: 4 (circuit id) + 1 (command) + 509 (payload).
1738const FIXED_CELL_LEN: usize = 514;
1739
1740/// tor-spec 5.4 DESTROY reason: "a protocol error occurred".
1741const DESTROY_REASON_PROTOCOL: u8 = 1;
1742/// tor-spec 5.4 DESTROY reason: "internal error at the relay". The relay tearing a circuit
1743/// down because of a fault of its own, which is exactly what an LLM backend failure is.
1744const DESTROY_REASON_INTERNAL: u8 = 2;
1745
1746/// One cell framed off the wire.
1747#[derive(Debug)]
1748enum FramedCell {
1749    /// A fixed-length v4 cell. `raw` is all 514 bytes, so the existing handlers can keep
1750    /// indexing it directly.
1751    Fixed { circuit_id: CircuitId, raw: Vec<u8> },
1752    /// A variable-length cell: VERSIONS (7), or any command >= 128 (VPADDING, CERTS,
1753    /// AUTH_CHALLENGE, AUTHENTICATE, AUTHORIZE).
1754    Variable {
1755        #[allow(dead_code)]
1756        circuit_id: u32,
1757        command: u8,
1758        payload: Vec<u8>,
1759    },
1760}
1761
1762/// Frame one cell out of `buf`, consuming its bytes. `None` means more bytes are needed.
1763///
1764/// `expecting_versions` must be true only for the first cell of a connection. tor-spec 3
1765/// allows VERSIONS only there, and gives it a 2-byte circuit id while every other cell in
1766/// link protocol v4 has a 4-byte one — so the command byte sits at a different offset and
1767/// the two layouts are only distinguishable because of that rule.
1768fn take_cell(buf: &mut Vec<u8>, expecting_versions: bool) -> Option<FramedCell> {
1769    if expecting_versions && buf.len() >= 3 && buf[2] == CELL_COMMAND_VERSIONS {
1770        // CircID (2) | Command (1) | Length (2) | Payload (Length)
1771        if buf.len() < 5 {
1772            return None;
1773        }
1774        let length = u16::from_be_bytes([buf[3], buf[4]]) as usize;
1775        if buf.len() < 5 + length {
1776            return None;
1777        }
1778        let circuit_id = u16::from_be_bytes([buf[0], buf[1]]) as u32;
1779        let payload = buf[5..5 + length].to_vec();
1780        buf.drain(..5 + length);
1781        return Some(FramedCell::Variable {
1782            circuit_id,
1783            command: CELL_COMMAND_VERSIONS,
1784            payload,
1785        });
1786    }
1787
1788    if buf.len() < 5 {
1789        return None;
1790    }
1791    let circuit_id = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
1792    let command = buf[4];
1793
1794    // Commands 128 and above are variable-length in every link protocol version.
1795    if command >= 128 {
1796        if buf.len() < 7 {
1797            return None;
1798        }
1799        let length = u16::from_be_bytes([buf[5], buf[6]]) as usize;
1800        if buf.len() < 7 + length {
1801            return None;
1802        }
1803        let payload = buf[7..7 + length].to_vec();
1804        buf.drain(..7 + length);
1805        return Some(FramedCell::Variable {
1806            circuit_id,
1807            command,
1808            payload,
1809        });
1810    }
1811
1812    if buf.len() < FIXED_CELL_LEN {
1813        return None;
1814    }
1815    let raw = buf[..FIXED_CELL_LEN].to_vec();
1816    buf.drain(..FIXED_CELL_LEN);
1817    Some(FramedCell::Fixed {
1818        circuit_id: CircuitId::new(circuit_id),
1819        raw,
1820    })
1821}
1822
1823/// Parse Tor cell header
1824///
1825/// Tor v4 cell format:
1826/// - Circuit ID: 4 bytes
1827/// - Command: 1 byte
1828/// - Payload: 509 bytes (variable-length cells have length field)
1829///
1830/// Command types (tor-spec.txt section 3). Note these are the *real* numbers: an earlier
1831/// version of this table was shifted by one from command 7 onwards, so a client's CREATE2
1832/// (10) was read as CREATED2 and dropped as "unhandled", and this relay's own CREATED2
1833/// replies went out with the CREATE2 command byte.
1834/// - 0: PADDING
1835/// - 1: CREATE (obsolete)
1836/// - 2: CREATED (obsolete)
1837/// - 3: RELAY
1838/// - 4: DESTROY
1839/// - 5: CREATE_FAST (obsolete)
1840/// - 6: CREATED_FAST (obsolete)
1841/// - 7: VERSIONS (variable length - NOT handled, see below)
1842/// - 8: NETINFO
1843/// - 9: RELAY_EARLY
1844/// - 10: CREATE2
1845/// - 11: CREATED2
1846/// - 12: PADDING_NEGOTIATE
1847///
1848/// Commands 128+ (VPADDING, CERTS, AUTH_CHALLENGE, AUTHENTICATE, AUTHORIZE) and VERSIONS are
1849/// variable-length cells. They are framed by `take_cell` and dispatched to
1850/// `handle_variable_cell` before reaching here, so this function only ever sees the
1851/// fixed-length 514-byte form.
1852fn parse_tor_cell(data: &[u8]) -> Option<TorCellInfo> {
1853    if data.len() < 5 {
1854        return None;
1855    }
1856
1857    // Extract circuit ID (4 bytes, big-endian)
1858    let circuit_id_bytes: [u8; 4] = data[0..4].try_into().ok()?;
1859    let circuit_id = CircuitId::from_bytes(&circuit_id_bytes);
1860
1861    // Extract command byte
1862    let command = data[4];
1863
1864    // Map command to cell type
1865    let cell_type = match command {
1866        0 => "PADDING",
1867        1 => "CREATE",
1868        2 => "CREATED",
1869        3 => "RELAY",
1870        4 => "DESTROY",
1871        5 => "CREATE_FAST",
1872        6 => "CREATED_FAST",
1873        7 => "VERSIONS",
1874        8 => "NETINFO",
1875        9 => "RELAY_EARLY",
1876        10 => "CREATE2",
1877        11 => "CREATED2",
1878        12 => "PADDING_NEGOTIATE",
1879        _ => "UNKNOWN",
1880    };
1881
1882    Some(TorCellInfo {
1883        circuit_id,
1884        cell_type: cell_type.to_string(),
1885    })
1886}