Skip to main content

microsandbox_network/
conn.rs

1//! Connection tracker: manages smoltcp TCP sockets for the poll loop.
2//!
3//! Creates sockets on SYN detection, tracks connection lifecycle, relays data
4//! between smoltcp sockets and proxy task channels, and cleans up closed
5//! connections.
6
7use std::collections::{HashMap, HashSet};
8use std::net::SocketAddr;
9
10use bytes::Bytes;
11use smoltcp::iface::{SocketHandle, SocketSet};
12use smoltcp::socket::tcp;
13use smoltcp::wire::IpListenEndpoint;
14use tokio::sync::mpsc;
15
16//--------------------------------------------------------------------------------------------------
17// Constants
18//--------------------------------------------------------------------------------------------------
19
20/// TCP socket receive buffer size (64 KiB).
21const TCP_RX_BUF_SIZE: usize = 65536;
22
23/// TCP socket transmit buffer size (64 KiB).
24const TCP_TX_BUF_SIZE: usize = 65536;
25
26/// Default max concurrent connections.
27const DEFAULT_MAX_CONNECTIONS: usize = 256;
28
29/// Capacity of the mpsc channels between the poll loop and proxy tasks.
30const CHANNEL_CAPACITY: usize = 32;
31
32/// Buffer size for reading from smoltcp sockets.
33const RELAY_BUF_SIZE: usize = 16384;
34
35//--------------------------------------------------------------------------------------------------
36// Types
37//--------------------------------------------------------------------------------------------------
38
39/// Tracks TCP connections between guest and proxy tasks.
40///
41/// Each guest TCP connection maps to a smoltcp socket and a pair of channels
42/// connecting it to a tokio proxy task. The tracker handles:
43///
44/// - **Socket creation** — on SYN detection, before smoltcp processes the frame.
45/// - **Data relay** — shuttles bytes between smoltcp sockets and channels.
46/// - **Lifecycle detection** — identifies newly-established connections for
47///   proxy spawning.
48/// - **Cleanup** — removes closed sockets from the socket set.
49pub struct ConnectionTracker {
50    /// Active connections keyed by smoltcp socket handle.
51    connections: HashMap<SocketHandle, Connection>,
52    /// Secondary index for O(1) duplicate-SYN detection by (src, dst) 4-tuple.
53    connection_keys: HashSet<(SocketAddr, SocketAddr)>,
54    /// Max concurrent connections (from NetworkConfig).
55    max_connections: usize,
56}
57
58/// Maximum number of poll iterations to attempt flushing remaining data
59/// after the proxy task has exited before force-aborting the socket.
60const DEFERRED_CLOSE_LIMIT: u16 = 64;
61
62/// Internal state for a single tracked TCP connection.
63struct Connection {
64    /// Guest source address (from the guest's SYN).
65    src: SocketAddr,
66    /// Original destination (from the guest's SYN).
67    dst: SocketAddr,
68    /// Sends data from smoltcp socket to proxy task (guest → server).
69    to_proxy: mpsc::Sender<Bytes>,
70    /// Receives data from proxy task to write to smoltcp socket (server → guest).
71    from_proxy: mpsc::Receiver<Bytes>,
72    /// Proxy-side channel ends, held until the connection is ESTABLISHED.
73    /// Taken by [`ConnectionTracker::take_new_connections()`].
74    proxy_channels: Option<ProxyChannels>,
75    /// Whether a proxy task has been spawned for this connection.
76    proxy_spawned: bool,
77    /// Partial data from proxy that couldn't be fully written to smoltcp socket.
78    write_buf: Option<(Bytes, usize)>,
79    /// Counter for deferred close attempts (prevents stalling forever).
80    close_attempts: u16,
81}
82
83/// Proxy-side channel ends, created at socket creation time and taken when
84/// the connection becomes ESTABLISHED.
85struct ProxyChannels {
86    /// Receive data from smoltcp socket (guest → proxy task).
87    from_smoltcp: mpsc::Receiver<Bytes>,
88    /// Send data to smoltcp socket (proxy task → guest).
89    to_smoltcp: mpsc::Sender<Bytes>,
90}
91
92/// Information for spawning a proxy task for a newly established connection.
93///
94/// Returned by [`ConnectionTracker::take_new_connections()`]. The poll loop
95/// passes this to the proxy task spawner.
96pub struct NewConnection {
97    /// Original destination the guest was connecting to.
98    pub dst: SocketAddr,
99    /// Receive data from smoltcp socket (guest → proxy task).
100    pub from_smoltcp: mpsc::Receiver<Bytes>,
101    /// Send data to smoltcp socket (proxy task → guest).
102    pub to_smoltcp: mpsc::Sender<Bytes>,
103}
104
105//--------------------------------------------------------------------------------------------------
106// Methods
107//--------------------------------------------------------------------------------------------------
108
109impl ConnectionTracker {
110    /// Create a new tracker with the given connection limit.
111    pub fn new(max_connections: Option<usize>) -> Self {
112        Self {
113            connections: HashMap::new(),
114            connection_keys: HashSet::new(),
115            max_connections: max_connections.unwrap_or(DEFAULT_MAX_CONNECTIONS),
116        }
117    }
118
119    /// Returns `true` if a tracked socket already exists for this exact
120    /// connection (same source AND destination). O(1) via HashSet lookup.
121    pub fn has_socket_for(&self, src: &SocketAddr, dst: &SocketAddr) -> bool {
122        self.connection_keys.contains(&(*src, *dst))
123    }
124
125    /// Create a smoltcp TCP socket for an incoming SYN and register it.
126    ///
127    /// The socket is put into LISTEN state on the destination IP + port so
128    /// smoltcp will complete the three-way handshake when it processes the
129    /// SYN frame. Binding to the specific destination IP (not just port)
130    /// prevents socket dispatch ambiguity when multiple connections target
131    /// different IPs on the same port.
132    ///
133    /// Returns `false` if at `max_connections` limit.
134    pub fn create_tcp_socket(
135        &mut self,
136        src: SocketAddr,
137        dst: SocketAddr,
138        sockets: &mut SocketSet<'_>,
139    ) -> bool {
140        if self.connections.len() >= self.max_connections {
141            return false;
142        }
143
144        // Create smoltcp TCP socket with buffers.
145        let rx_buf = tcp::SocketBuffer::new(vec![0u8; TCP_RX_BUF_SIZE]);
146        let tx_buf = tcp::SocketBuffer::new(vec![0u8; TCP_TX_BUF_SIZE]);
147        let mut socket = tcp::Socket::new(rx_buf, tx_buf);
148
149        // Listen on the specific destination IP + port. With any_ip mode,
150        // binding to the IP ensures the correct socket accepts each SYN
151        // when multiple connections target the same port on different IPs.
152        let listen_endpoint = IpListenEndpoint {
153            addr: Some(dst.ip().into()),
154            port: dst.port(),
155        };
156        if socket.listen(listen_endpoint).is_err() {
157            return false;
158        }
159
160        let handle = sockets.add(socket);
161
162        // Create channel pairs for proxy task communication.
163        //
164        // smoltcp → proxy (guest sends data, proxy relays to server):
165        let (to_proxy_tx, to_proxy_rx) = mpsc::channel(CHANNEL_CAPACITY);
166        // proxy → smoltcp (server sends data, proxy relays to guest):
167        let (from_proxy_tx, from_proxy_rx) = mpsc::channel(CHANNEL_CAPACITY);
168
169        self.connection_keys.insert((src, dst));
170        self.connections.insert(
171            handle,
172            Connection {
173                src,
174                dst,
175                to_proxy: to_proxy_tx,
176                from_proxy: from_proxy_rx,
177                proxy_channels: Some(ProxyChannels {
178                    from_smoltcp: to_proxy_rx,
179                    to_smoltcp: from_proxy_tx,
180                }),
181                proxy_spawned: false,
182                write_buf: None,
183                close_attempts: 0,
184            },
185        );
186
187        true
188    }
189
190    /// Relay data between smoltcp sockets and proxy task channels.
191    ///
192    /// For each connection with a spawned proxy:
193    /// - Reads data from the smoltcp socket and sends it to the proxy channel.
194    /// - Receives data from the proxy channel and writes it to the smoltcp socket.
195    pub fn relay_data(&mut self, sockets: &mut SocketSet<'_>) {
196        let mut relay_buf = [0u8; RELAY_BUF_SIZE];
197
198        for (&handle, conn) in &mut self.connections {
199            if !conn.proxy_spawned {
200                continue;
201            }
202
203            let socket = sockets.get_mut::<tcp::Socket>(handle);
204
205            // Detect proxy task exit: when the proxy drops its channel
206            // ends, close the smoltcp socket so the guest gets a FIN.
207            if conn.to_proxy.is_closed() {
208                write_proxy_data(socket, conn);
209                if conn.write_buf.is_none() {
210                    socket.close();
211                } else {
212                    // Abort if we've been trying to flush for too long
213                    // (guest stopped reading, socket send buffer full).
214                    conn.close_attempts += 1;
215                    if conn.close_attempts >= DEFERRED_CLOSE_LIMIT {
216                        socket.abort();
217                    }
218                }
219                continue;
220            }
221
222            // smoltcp → proxy: read from socket, send via channel.
223            while socket.can_recv() {
224                match socket.recv_slice(&mut relay_buf) {
225                    Ok(n) if n > 0 => {
226                        let data = Bytes::copy_from_slice(&relay_buf[..n]);
227                        if conn.to_proxy.try_send(data).is_err() {
228                            break;
229                        }
230                    }
231                    _ => break,
232                }
233            }
234
235            // proxy → smoltcp: write pending data, then drain channel.
236            write_proxy_data(socket, conn);
237        }
238    }
239
240    /// Collect newly-established connections that need proxy tasks.
241    ///
242    /// Returns a list of [`NewConnection`] structs containing the channel ends
243    /// for the proxy task. The poll loop is responsible for spawning the task.
244    pub fn take_new_connections(&mut self, sockets: &mut SocketSet<'_>) -> Vec<NewConnection> {
245        let mut new = Vec::new();
246
247        for (&handle, conn) in &mut self.connections {
248            if conn.proxy_spawned {
249                continue;
250            }
251
252            let socket = sockets.get::<tcp::Socket>(handle);
253            if socket.state() == tcp::State::Established {
254                conn.proxy_spawned = true;
255
256                if let Some(channels) = conn.proxy_channels.take() {
257                    new.push(NewConnection {
258                        dst: conn.dst,
259                        from_smoltcp: channels.from_smoltcp,
260                        to_smoltcp: channels.to_smoltcp,
261                    });
262                }
263            }
264        }
265
266        new
267    }
268
269    /// Remove closed connections and their sockets.
270    ///
271    /// Only removes sockets in the `Closed` state. Sockets in `TimeWait`
272    /// are left for smoltcp to handle naturally (2*MSL timer), preventing
273    /// delayed duplicate segments from being accepted by a reused port.
274    pub fn cleanup_closed(&mut self, sockets: &mut SocketSet<'_>) {
275        let keys = &mut self.connection_keys;
276        self.connections.retain(|&handle, conn| {
277            let socket = sockets.get::<tcp::Socket>(handle);
278            if matches!(socket.state(), tcp::State::Closed) {
279                keys.remove(&(conn.src, conn.dst));
280                sockets.remove(handle);
281                false
282            } else {
283                true
284            }
285        });
286    }
287}
288
289//--------------------------------------------------------------------------------------------------
290// Functions
291//--------------------------------------------------------------------------------------------------
292
293/// Try to write proxy data to the smoltcp socket.
294fn write_proxy_data(socket: &mut tcp::Socket<'_>, conn: &mut Connection) {
295    // First, try to finish writing any pending partial data.
296    if let Some((data, offset)) = &mut conn.write_buf {
297        if socket.can_send() {
298            match socket.send_slice(&data[*offset..]) {
299                Ok(written) => {
300                    *offset += written;
301                    if *offset >= data.len() {
302                        conn.write_buf = None;
303                    }
304                }
305                Err(_) => return,
306            }
307        } else {
308            return;
309        }
310    }
311
312    // Then drain the channel.
313    while conn.write_buf.is_none() {
314        match conn.from_proxy.try_recv() {
315            Ok(data) => {
316                if socket.can_send() {
317                    match socket.send_slice(&data) {
318                        Ok(written) if written < data.len() => {
319                            conn.write_buf = Some((data, written));
320                        }
321                        Err(_) => {
322                            conn.write_buf = Some((data, 0));
323                        }
324                        _ => {}
325                    }
326                } else {
327                    conn.write_buf = Some((data, 0));
328                }
329            }
330            Err(_) => break,
331        }
332    }
333}