tokio::join! in both bridge loops waited for both I/O directions to complete. When Telegram closed an idle WebSocket (~60s timeout), the download half exited while the upload half blocked forever on reader.read(), leaking the connection and its FDs. These zombie connections accumulated until the 1024 FD limit was hit, causing accept() to fail with EMFILE for any new client.
Changes
-
Core fix —
bridge_ws/bridge_tcp: Replacetokio::join!withtokio::spawn+tokio::select!+abort(). When either direction exits, the peer task is immediately aborted and all its I/O handles (FDs) are freed:let (bytes_up, bytes_down) = tokio::select! { result = &mut upload => { let up = result.unwrap_or_else(|_| 0); download.abort(); let down = download.await.unwrap_or_else(|_| 0); (up, down) } result = &mut download => { /* symmetric */ } };
-
WS blacklist → cooldown: Permanent
WS_BLACKLISTreplaced with a 5-minuteWS_REDIRECT_COOLDOWN. A single redirect response no longer permanently bypasses WebSocket for a DC for the lifetime of the process. -
LAN IP auto-detection: When
--host 0.0.0.0is used (router deployment), the generatedtg://link now shows the machine's actual LAN IP instead of0.0.0.0, making the link usable by all devices on the network. Added--link-ipflag for explicit override. Startup emits a warning when bound to127.0.0.1(link is localhost-only). -
README: Added router deployment section documenting
--host 0.0.0.0+--link-ipusage.