fix(client): stop leaking a socket+task per bypassed UDP flow

start_udp_bypass_session (the TUN-mode path for UDP from apps/IPs the user
has excluded from the tunnel) spawned a separate task to read from the
physical-interface-bound socket, holding its own Arc<UdpSocket> clone.
Nothing ever cancelled that task when the outer function returned (e.g.
once session_rx closed) - it just kept running, and its socket clone kept
the OS fd alive, for the lifetime of the process. Every distinct bypassed
UDP flow (any excluded app's DNS query, game session, etc.) leaked one
socket and one task permanently.

The sibling function right below it, start_udp_session, already does this
correctly: one tokio::select! loop combining both directions in a single
task that exits (and drops the socket) as soon as either side closes.
Rewrote start_udp_bypass_session to match that pattern instead of
spawning a detached reader task.
This commit is contained in:
ospab 2026-07-18 17:46:14 +03:00
parent 51b947e6ff
commit dbf923fb16
1 changed files with 25 additions and 18 deletions

View File

@ -138,27 +138,34 @@ async fn start_udp_bypass_session(
let _ = crate::tunnel::proxy::bind_socket_to_interface(&socket, name);
}
let socket = Arc::new(socket);
let socket_rx = socket.clone();
// Spawn a task to read from physical socket and send back to smoltcp
let tx_clone = smoltcp_tx.clone();
tokio::spawn(async move {
use futures::SinkExt;
let mut buf = [0u8; 65536];
loop {
match socket_rx.recv_from(&mut buf).await {
Ok((n, peer)) => {
let mut lock = tx_clone.lock().await;
let _ = lock.send((buf[..n].to_vec(), peer, client_src)).await;
// A single select! loop over both directions, rather than spawning a
// separate task for the read side, so the whole session - physical
// socket included - is torn down the moment this function returns
// (e.g. when session_rx closes). The previous spawned-task version left
// that task (and its Arc<UdpSocket> clone, keeping the OS socket fd
// alive) running forever after this function returned: nothing ever
// cancelled it, so every bypassed UDP flow (any excluded app/IP in TUN
// mode) leaked one socket + one task for the lifetime of the process.
use futures::SinkExt;
let mut buf = [0u8; 65536];
loop {
tokio::select! {
outbound = session_rx.recv() => {
match outbound {
Some((payload, dst)) => { socket.send_to(&payload, dst).await?; }
None => break,
}
}
inbound = socket.recv_from(&mut buf) => {
match inbound {
Ok((n, peer)) => {
let mut lock = smoltcp_tx.lock().await;
let _ = lock.send((buf[..n].to_vec(), peer, client_src)).await;
}
Err(_) => break,
}
Err(_) => break,
}
}
});
while let Some((payload, dst)) = session_rx.recv().await {
socket.send_to(&payload, dst).await?;
}
Ok(())