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}