Skip to content

feat: bind peer traffic to one interface, with fixed ports and a /network API - #210

Merged
baairon merged 2 commits into
mainfrom
claude/sweet-galileo-nlxwv2
Nov 4, 2026
Merged

baairon merged 2 commits into
mainfrom
claude/sweet-galileo-nlxwv2

Conversation

@baairon

@baairon baairon commented Nov 4, 2026 •

Copy link
Copy Markdown
Owner

What and why

Closes #164.

webtorrent binds nothing to any interface, so a VPN that drops hands the swarm and the search sites the real address. This binds every socket the process opens to one interface's address and fails closed when that interface is gone, with no TUI changes and nothing tied to a particular VPN.

  • --interface <name|ip> (TORLINK_INTERFACE) binds peers, trackers, DHT, web seeds and the search fetches to the interface's IPv4 address. webtorrent has no option for this, so src/util/bind.ts injects the address at the one place every library passes through: Node's net/dgram prototypes, plus utp-native's own bind. Installed only when an interface is configured; inert otherwise.
  • While the interface is gone, new sockets get an address this host does not hold (an RFC 5737 range not assigned locally) and die with EADDRNOTAVAIL instead of leaving through the default route. A 2 s watch tears the engine down on a drop and re-attaches every torrent when the interface is back, on its new address if that changed, after a 3 s settle so a bouncing link does not re-hash a seedbox each time. Queue items never change state, so the TUI needs nothing new: a stalled download reads as stalled.
  • --peer-port fixes the TCP/uTP listen port (the one to forward), --dht-port the DHT port; the two may never be equal, since uTP already holds the peer port's UDP side. Bound, LSD, UPnP, NAT-PMP and WebRTC peers stay off: each would use another interface, and a mapping request would fight the one the interface's gateway keeps alive.
  • Flags ride on the environment like TORLINK_NO_UTP, so the TUI, a --daemon child and search all see them, and a restart after torlnk update replays argv. search --interface exits 1 with a message when the interface is down.
  • serve gains GET /network (address, online, listen port, incoming peer count as proof a forwarded port works, and the client's own last error) and POST /network to change interface or ports in place, so an external driver that reads a forwarded port from its VPN client can hand it over without a restart. /downloads carries the same status.

Verified on Linux beyond the suite: serve --interface lo --peer-port 51413 puts every peer socket on 127.0.0.1 and moves the port through POST /network; on a veth interface, all sockets sit on its address, vanish on link down while the download stays listed, and come back on a new address with the fixed port; an unknown interface boots paused with a hint listing the real ones.

Limits stated in the README: IPv4 only (webtorrent and the DHT open udp4 sockets), DNS through the OS resolver, and the git/npm children of torlnk update unbound.

Checklist

  • npm run typecheck is clean
  • npm test passes
  • New logic has a test (vitest; mock node built-ins for platform code)
  • If I added a key, I updated both HELP_GROUPS and footerHints in src/ui/keymap.ts (no key added; no TUI changes)
  • If I added a Store field, I updated makeStore in scripts/render-previews-impl.tsx (no Store field added)
  • OS-touching code works on Windows, macOS, and Linux (the shim relies on Node behavior shared by all three; the full matrix, ubuntu/macos/windows × Node 22/24, is green on this PR)
  • One concern, with a Conventional Commits title (feat: / fix: / docs: / chore:)

Generated by Claude Code

…work API (#164)

webtorrent binds nothing to any interface, so a VPN that drops hands the
swarm and the search sites the real address. Instead of polling an
adapter and hoping to pause in time, every socket the process opens is
bound to the interface's address through Node's own net/dgram prototypes
(and utp-native's bind, which opens its socket natively): peers,
trackers, DHT, web seeds and the search fetches. While the interface is
gone, new sockets get an unroutable address and fail closed. A 2 s watch
tears the engine down when the interface disappears and re-attaches
every torrent when it is back, on its new address if that changed; the
queue's items never change state, so the TUI needs nothing new and a
stalled download simply reads as stalled.

--interface <name|ip>, --peer-port and --dht-port ride on the
environment (TORLINK_INTERFACE, TORLINK_PEER_PORT, TORLINK_DHT_PORT),
the way TORLINK_NO_UTP does, so the TUI, a --daemon child and search all
see them, and a restart after `torlnk update` replays the flags. The DHT
may never share the peer port, where uTP already listens. Bound, LSD,
UPnP, NAT-PMP and WebRTC peers stay off: each would use another
interface, and a mapping request would fight the one the interface's
owner keeps alive.

serve gains GET /network (address, online, listen port, and the count of
peers that dialed in, which is the proof a forwarded port works) and
POST /network to change the interface or ports in place, so a script
that reads a forwarded port from a VPN client can hand it over without a
restart. Nothing is tied to a particular VPN: torlink only knows the
interface. IPv4 only for now, since webtorrent and the DHT open udp4
sockets.
…lient errors

Re-attaching after a drop re-verifies every seed on disk, so a link that
bounces for a few seconds would have re-hashed a seedbox each time; the
return now settles for 3 s first, and anything added meanwhile waits
with the rest.

The fail-closed address was a fixed 192.0.2.1, and containers do hand
out that range (the sandbox this was built in sat on 192.0.2.2/24). It
is now whichever RFC 5737 range this host does not hold at the moment
the interface goes down.

webtorrent's own errors (a fixed port already taken, uTP that would not
start) were swallowed, leaving torrents at zero peers with nothing in
the log. They are reported on /network and by the daemons, a destroyed
client is dropped so the next add or a POST /network with a free port
builds a fresh one.
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

vu1nz Security Review

0 finding(s) in PR #?

No security issues found.

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

0 finding(s)

No findings.

@baairon
baairon merged commit cbe07d6 into main Nov 4, 2026
9 checks passed
@baairon
baairon deleted the claude/sweet-galileo-nlxwv2 branch November 4, 2026 06:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Add VPN Network Interface Check / Kill-Switch to prevent IP leaks

1 participant