Skip to main content

netget/server/tor_relay/
stream.rs

1//! Tor Relay Stream Management
2//!
3//! Handles TCP streams within circuits for exit functionality
4
5use anyhow::{Context, Result};
6use std::collections::HashMap;
7use std::net::SocketAddr;
8use std::sync::Arc;
9use tokio::net::TcpStream;
10use tokio::sync::Mutex;
11use tracing::{debug, trace};
12
13use super::circuit::StreamId;
14
15/// Stream state within a circuit
16#[derive(Debug)]
17pub enum StreamState {
18    /// Stream is being established
19    Connecting,
20    /// Stream is active and forwarding data
21    Active {
22        /// Read half of the TCP connection to the destination, used only by the forwarder
23        /// task. It is deliberately a *separate* lock from `writer`: the forwarder parks in
24        /// `read()` waiting for the target, and while it held one lock over the whole
25        /// `TcpStream` the client-to-target direction could never acquire it to write. The
26        /// exit stream deadlocked on its first RELAY/DATA cell, every time.
27        reader: Arc<Mutex<tokio::net::tcp::OwnedReadHalf>>,
28        /// Write half, used by `handle_data_cell` for client-to-target data.
29        writer: Arc<Mutex<tokio::net::tcp::OwnedWriteHalf>>,
30        /// Bytes sent to destination
31        bytes_sent: u64,
32        /// Bytes received from destination
33        bytes_received: u64,
34        /// Package window (decremented on DATA send, incremented on SENDME receive)
35        package_window: u16,
36        /// Deliver window (decremented on DATA receive, send SENDME at threshold)
37        deliver_window: u16,
38        /// Count of DATA cells received (for SENDME triggering)
39        data_cells_received: u16,
40    },
41    /// Directory stream (BEGIN_DIR) - serves directory documents over circuit
42    Directory {
43        /// Accumulated HTTP request data
44        request_data: Vec<u8>,
45        /// Package window
46        package_window: u16,
47        /// Deliver window
48        deliver_window: u16,
49        /// Count of DATA cells received
50        data_cells_received: u16,
51    },
52    /// Stream is closing
53    Closing,
54    /// Stream is closed
55    Closed,
56}
57
58/// Stream window constants (from tor-spec.txt)
59pub const STREAM_WINDOW_START: u16 = 500;
60pub const STREAM_WINDOW_INCREMENT: u16 = 50;
61
62/// How many streams one circuit may hold open at once.
63///
64/// Each active stream is one outbound TCP connection to an address the peer chose, plus a
65/// forwarder task. The stream id is 16 bits, so without a cap a single circuit can ask for
66/// 65,535 of them — a file-descriptor exhaustion against this host and, pointed at one
67/// victim, an outbound connection flood with a 1:65,535 amplification from the attacker's
68/// side. Real Tor clients multiplex a handful of streams per circuit; 64 is generous.
69pub const MAX_STREAMS_PER_CIRCUIT: usize = 64;
70
71/// Stream information
72#[derive(Debug)]
73pub struct Stream {
74    pub id: StreamId,
75    pub target: String,
76    pub state: StreamState,
77    pub created_at: crate::utils::clock::Instant,
78}
79
80impl Stream {
81    /// Create new stream in connecting state
82    pub fn new(id: StreamId, target: String) -> Self {
83        Self {
84            id,
85            target,
86            state: StreamState::Connecting,
87            created_at: crate::utils::clock::Instant::now(),
88        }
89    }
90
91    /// Set stream as active with TCP connection
92    pub fn set_active(&mut self, connection: TcpStream) {
93        let (reader, writer) = connection.into_split();
94        self.state = StreamState::Active {
95            reader: Arc::new(Mutex::new(reader)),
96            writer: Arc::new(Mutex::new(writer)),
97            bytes_sent: 0,
98            bytes_received: 0,
99            package_window: STREAM_WINDOW_START,
100            deliver_window: STREAM_WINDOW_START,
101            data_cells_received: 0,
102        };
103    }
104
105    /// Set stream as directory stream (BEGIN_DIR)
106    pub fn set_directory(&mut self) {
107        self.state = StreamState::Directory {
108            request_data: Vec::new(),
109            package_window: STREAM_WINDOW_START,
110            deliver_window: STREAM_WINDOW_START,
111            data_cells_received: 0,
112        };
113    }
114
115    /// Check if stream is active
116    pub fn is_active(&self) -> bool {
117        matches!(self.state, StreamState::Active { .. })
118    }
119
120    /// Check if stream is directory stream
121    pub fn is_directory(&self) -> bool {
122        matches!(self.state, StreamState::Directory { .. })
123    }
124
125    /// Write half of the TCP connection, if active (client -> target).
126    pub fn writer(&self) -> Option<Arc<Mutex<tokio::net::tcp::OwnedWriteHalf>>> {
127        match &self.state {
128            StreamState::Active { writer, .. } => Some(writer.clone()),
129            _ => None,
130        }
131    }
132
133    /// Read half of the TCP connection, if active (target -> client).
134    pub fn reader(&self) -> Option<Arc<Mutex<tokio::net::tcp::OwnedReadHalf>>> {
135        match &self.state {
136            StreamState::Active { reader, .. } => Some(reader.clone()),
137            _ => None,
138        }
139    }
140
141    /// Record bytes sent
142    pub fn record_sent(&mut self, bytes: u64) {
143        if let StreamState::Active { bytes_sent, .. } = &mut self.state {
144            *bytes_sent += bytes;
145        }
146    }
147
148    /// Record bytes received
149    pub fn record_received(&mut self, bytes: u64) {
150        if let StreamState::Active { bytes_received, .. } = &mut self.state {
151            *bytes_received += bytes;
152        }
153    }
154
155    /// Close stream
156    pub fn close(&mut self) {
157        self.state = StreamState::Closed;
158    }
159
160    /// Record DATA cell received - returns true if SENDME should be sent
161    pub fn record_data_received(&mut self) -> bool {
162        match &mut self.state {
163            StreamState::Active {
164                deliver_window,
165                data_cells_received,
166                ..
167            }
168            | StreamState::Directory {
169                deliver_window,
170                data_cells_received,
171                ..
172            } => {
173                *deliver_window = deliver_window.saturating_sub(1);
174                *data_cells_received += 1;
175
176                // Send SENDME every STREAM_WINDOW_INCREMENT cells
177                if *data_cells_received >= STREAM_WINDOW_INCREMENT {
178                    *data_cells_received = 0;
179                    *deliver_window += STREAM_WINDOW_INCREMENT;
180                    return true;
181                }
182            }
183            _ => {}
184        }
185        false
186    }
187
188    /// Process received SENDME - increment package window
189    pub fn process_sendme(&mut self) {
190        match &mut self.state {
191            StreamState::Active { package_window, .. }
192            | StreamState::Directory { package_window, .. } => {
193                *package_window += STREAM_WINDOW_INCREMENT;
194                trace!(
195                    "Stream {} package window increased to {}",
196                    self.id.as_u16(),
197                    package_window
198                );
199            }
200            _ => {}
201        }
202    }
203
204    /// Check if we can send DATA (package window > 0)
205    pub fn can_send_data(&self) -> bool {
206        match &self.state {
207            StreamState::Active { package_window, .. }
208            | StreamState::Directory { package_window, .. } => *package_window > 0,
209            _ => false,
210        }
211    }
212
213    /// Decrement package window when sending DATA
214    pub fn consume_package_window(&mut self) {
215        match &mut self.state {
216            StreamState::Active { package_window, .. }
217            | StreamState::Directory { package_window, .. } => {
218                *package_window = package_window.saturating_sub(1);
219            }
220            _ => {}
221        }
222    }
223
224    /// Append HTTP request data for directory stream
225    pub fn append_request_data(&mut self, data: &[u8]) {
226        if let StreamState::Directory { request_data, .. } = &mut self.state {
227            request_data.extend_from_slice(data);
228        }
229    }
230
231    /// Get accumulated HTTP request data for directory stream
232    pub fn get_request_data(&self) -> Option<&[u8]> {
233        if let StreamState::Directory { request_data, .. } = &self.state {
234            Some(request_data)
235        } else {
236            None
237        }
238    }
239}
240
241/// Stream manager for a circuit
242#[derive(Debug)]
243pub struct StreamManager {
244    streams: HashMap<StreamId, Stream>,
245    next_stream_id: u16,
246}
247
248impl StreamManager {
249    /// Create new stream manager
250    pub fn new() -> Self {
251        Self {
252            streams: HashMap::new(),
253            next_stream_id: 1,
254        }
255    }
256
257    /// Allocate new stream ID
258    pub fn allocate_stream_id(&mut self) -> StreamId {
259        let id = StreamId::new(self.next_stream_id);
260        self.next_stream_id = self.next_stream_id.wrapping_add(1);
261        if self.next_stream_id == 0 {
262            self.next_stream_id = 1; // Skip 0
263        }
264        id
265    }
266
267    /// Create new stream
268    pub fn create_stream(&mut self, id: StreamId, target: String) -> Result<()> {
269        if self.streams.contains_key(&id) {
270            return Err(anyhow::anyhow!("Stream ID already exists"));
271        }
272
273        // Refused before the socket is opened, so a peer cannot turn one circuit into
274        // thousands of outbound connections. The caller answers with END/RESOURCE_LIMIT,
275        // which is tor-spec's own word for this.
276        if self.streams.len() >= MAX_STREAMS_PER_CIRCUIT {
277            return Err(anyhow::anyhow!(
278                "circuit already holds {} streams, the per-circuit limit",
279                MAX_STREAMS_PER_CIRCUIT
280            ));
281        }
282
283        let stream = Stream::new(id, target);
284        self.streams.insert(id, stream);
285        debug!("Created stream {} in circuit", id.as_u16());
286
287        Ok(())
288    }
289
290    /// Get mutable stream
291    pub fn get_mut(&mut self, id: StreamId) -> Option<&mut Stream> {
292        self.streams.get_mut(&id)
293    }
294
295    /// Get stream
296    pub fn get(&self, id: StreamId) -> Option<&Stream> {
297        self.streams.get(&id)
298    }
299
300    /// Remove stream
301    pub fn remove(&mut self, id: StreamId) -> Option<Stream> {
302        self.streams.remove(&id)
303    }
304
305    /// Get all active stream IDs
306    pub fn active_streams(&self) -> Vec<StreamId> {
307        self.streams
308            .iter()
309            .filter(|(_, s)| s.is_active())
310            .map(|(id, _)| *id)
311            .collect()
312    }
313
314    /// Get stream count
315    pub fn count(&self) -> usize {
316        self.streams.len()
317    }
318}
319
320/// Parse target address from BEGIN cell data
321/// Format: "host:port\0" (null-terminated)
322pub fn parse_begin_target(data: &[u8]) -> Result<String> {
323    // Find null terminator
324    let end = data.iter().position(|&b| b == 0).unwrap_or(data.len());
325    let target_str = std::str::from_utf8(&data[..end]).context("Invalid UTF-8 in BEGIN target")?;
326
327    // Validate format (should be host:port)
328    if !target_str.contains(':') {
329        return Err(anyhow::anyhow!(
330            "Invalid BEGIN target format: {}",
331            target_str
332        ));
333    }
334
335    Ok(target_str.to_string())
336}
337
338/// How long a BEGIN may hold the whole session waiting for its exit connection.
339///
340/// `handle_begin_cell` awaits this inline in the cell-processing loop, so until it returns
341/// the session reads no further cells and writes none of the ones its forwarder tasks have
342/// queued. Without a bound that is the OS connect timeout — around 75 seconds on macOS — and
343/// the address is chosen by the peer, so one BEGIN to a blackholed address (any unroutable
344/// RFC 5737 or RFC 1918 address will do) freezes the connection for the whole of it. Ten
345/// seconds is well past any reachable host and short enough that the stall is a nuisance
346/// rather than a denial of service.
347pub const TARGET_CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
348
349/// Establish TCP connection to target.
350///
351/// **The destination is whatever the peer put in its BEGIN cell, and nothing restricts it** —
352/// see this protocol's `metadata().notes`, which says so in as many words. The bound here is
353/// on *time*, not on where the connection may go.
354pub async fn connect_to_target(target: &str) -> Result<TcpStream> {
355    debug!("Connecting to target: {}", target);
356
357    // Parse target as SocketAddr or resolve hostname
358    let connect = async {
359        if let Ok(addr) = target.parse::<SocketAddr>() {
360            // Direct IP:port connection
361            TcpStream::connect(addr).await
362        } else {
363            // Hostname:port - let tokio resolve
364            TcpStream::connect(target).await
365        }
366    };
367
368    let stream = tokio::time::timeout(TARGET_CONNECT_TIMEOUT, connect)
369        .await
370        .map_err(|_| {
371            anyhow::anyhow!(
372                "timed out after {}s connecting to {}",
373                TARGET_CONNECT_TIMEOUT.as_secs(),
374                target
375            )
376        })??;
377
378    debug!("Connected to {}", target);
379    Ok(stream)
380}
381
382/// Build RELAY cell response
383/// Format: Command (1) | Recognized (2) | StreamID (2) | Digest (4) | Length (2) | Data (Length)
384pub fn build_relay_cell(circuit_id: u32, stream_id: u16, command: u8, data: &[u8]) -> Vec<u8> {
385    let mut cell = Vec::with_capacity(514);
386
387    // Circuit ID (4 bytes)
388    cell.extend_from_slice(&circuit_id.to_be_bytes());
389
390    // RELAY command (1 byte)
391    cell.push(3); // RELAY
392
393    // Relay cell header (11 bytes)
394    cell.push(command); // Relay command
395    cell.extend_from_slice(&0u16.to_be_bytes()); // Recognized = 0
396    cell.extend_from_slice(&stream_id.to_be_bytes()); // Stream ID
397    cell.extend_from_slice(&[0u8; 4]); // Digest (filled by encryption)
398    cell.extend_from_slice(&(data.len() as u16).to_be_bytes()); // Length
399
400    // Data
401    cell.extend_from_slice(data);
402
403    // Pad to 514 bytes
404    cell.resize(514, 0);
405
406    cell
407}
408
409/// RELAY cell commands (from tor-spec.txt)
410pub mod relay_command {
411    pub const BEGIN: u8 = 1;
412    pub const DATA: u8 = 2;
413    pub const END: u8 = 3;
414    pub const CONNECTED: u8 = 4;
415    pub const SENDME: u8 = 5;
416    pub const EXTEND: u8 = 6;
417    pub const EXTENDED: u8 = 7;
418    pub const TRUNCATE: u8 = 8;
419    pub const TRUNCATED: u8 = 9;
420    pub const DROP: u8 = 10;
421    pub const RESOLVE: u8 = 11;
422    pub const RESOLVED: u8 = 12;
423    pub const BEGIN_DIR: u8 = 13;
424    pub const EXTEND2: u8 = 14;
425    pub const EXTENDED2: u8 = 15;
426}
427
428/// END cell reason codes
429pub mod end_reason {
430    pub const MISC: u8 = 1;
431    pub const RESOLVE_FAILED: u8 = 2;
432    pub const CONNECT_REFUSED: u8 = 3;
433    pub const EXIT_POLICY: u8 = 4;
434    pub const DESTROY: u8 = 5;
435    pub const DONE: u8 = 6;
436    pub const TIMEOUT: u8 = 7;
437    pub const NO_ROUTE: u8 = 8;
438    pub const HIBERNATING: u8 = 9;
439    pub const INTERNAL: u8 = 10;
440    pub const RESOURCE_LIMIT: u8 = 11;
441    pub const CONNECT_RESET: u8 = 12;
442    pub const TOR_PROTOCOL: u8 = 13;
443}