REF-059 - Align the VPN guard across the three product families¶
Summary¶
The "VPN guard" (verify P2P traffic egresses through the bound VPN/tunnel interface, and fail closed otherwise) exists at three different maturity levels across the products:
- eMuleBB (eD2k, C++/MFC): full guard - startup gate, runtime monitor, bind-loss monitor, CIDR allow-list, and (FEAT-121) a sequential STUN-then-HTTP egress check, all fail-closed.
- emulebb-rust (eD2k): only the STUN egress primitive (FEAT-121) plus
IP_UNICAST_IFegress pinning on the data-plane sockets. No guard wiring (no startup gate / runtime monitor / CIDR gate). - qBittorrentBB (libtorrent, C++): the STUN primitive in the libtorrent fork (FEAT-121, still unwired and unbuilt), plus the binding rules (WebUI on X_LOCAL_IP, BT/DHT on the tunnel). No egress guard.
This item is to review the guard in depth and converge on one consistent mechanism and policy across all three, then close the gaps (rust and qBittorrentBB currently have no egress gate at all).
This is backlog / non-blocking: the eMuleBB guard is verified working (FEAT-121). Captured now so the cross-product alignment is not lost.
Goals¶
- One documented guard model (enable, behavior, fail-closed semantics) shared by all three families, with per-family deviations justified explicitly.
- Wire an egress guard into emulebb-rust and qBittorrentBB (currently absent), reusing the FEAT-121 STUN primitives already present in each.
- Resolve the divergences and weaknesses listed below.
- A shared spec for the allow-list semantics and the STUN/HTTP gate ordering.
Background analysis (eMuleBB guard, as of FEAT-121)¶
Enablement¶
VpnGuardMode = Blockenables it (Offdisables) -VpnGuardSeams::ParseModePreferenceText.- Requires a resolved bind interface:
BuildProbeContextneeds a non-empty active bind interface,BARR_Resolved, a non-zeroIP_UNICAST_IFindex, and a bind address. No resolved tunnel -> no probe. VpnGuardAllowedPublicIpCidrs- comma/space/semicolon list of public IPv4 CIDRs, each validated as public (IsPublicIpv4Address+IsPublicIpv4RangeOnlyreject private/reserved), so a private range cannot be allow-listed.- Companion prefs
BlockNetworkWhenBindUnavailableAtStartupandExitOnBindInterfaceLossform the bind-presence half of the guard.
Behavior¶
- Startup (
OnStartupTimerstage 4): runs the egress probe viaShouldRunStartupProbeand holds startup (P2P sockets not opened) until a verdict postsUM_STARTUP_NEXT_STAGE. Empty allow-list -> IP check skipped (approved); fail ->BlockStartupNetworkingForSession(app runs, P2P off). - Runtime (
CheckVpnGuardHttpMonitor, periodic + onOnBindInterfaceChanged): re-runs the cycle; out-of-range egress ->ExitForVpnGuardFailure->ExitForBindLoss->WM_CLOSE(app exits). - Bind-loss monitor (separate): watches the bound interface's presence; if it vanishes -> exit. Two guards: "interface gone" (presence) and "egress IP wrong" (the probe).
- Connection gating:
CanUseP2PConnectionCommands/CanPostStartupAutoConnectkeep connect + auto-connect disabled until the guard is satisfied/armed. - Fail-closed, asymmetric by design: startup failure blocks P2P (app keeps running); runtime failure exits. Both via one shared pause-all-P2P path.
STUN vs HTTP (FEAT-121)¶
- Sequential, AND-gated: STUN (UDP) is the gate (proves the UDP data-plane
egress - KAD / source-reask - exits via the tunnel; fast, no Host-header
quirks), then HTTP (TCP) is the confirmer (proves the TCP egress; runs only
if STUN passed). Both bound +
IP_UNICAST_IF-pinned +connect()-filtered, multi-server raced. Either failing fails the cycle closed.
CIDR allow-listing¶
IsPublicIpv4Allowedmasks the reflexive IP (host order viaToComparableIpv4) against each allowed prefix - standard CIDR containment.
Divergences and weaknesses to resolve¶
- Guard absent in rust + qBittorrentBB. Only eMuleBB gates; the others have the STUN primitive but no startup gate / runtime monitor / CIDR check.
- Empty allow-list silently disables the IP gate (only interface binding remains). Consider treating empty as a hard block, or warn loudly.
- Stale CIDRs fail closed on a legitimate tunnel (observed: default test CIDRs did not include the connected hide.me server's egress). Operational, but worth a self-update / discovery story.
- DNS is unbounded on the eMuleBB probes (blocking
getaddrinfo); a hung resolver could delay the gate (mitigated only by multi-server fan-out). rust / libtorrent bound the whole probe; eMuleBB does not bound DNS. - Sequential latency: worst-case ~STUN 5s + HTTP 5s before startup proceeds.
- Startup vs runtime asymmetry (block vs exit) - confirm this is the intended policy for all three.
- Loser-probe cancellation differs: rust + libtorrent eager-cancel on first win; eMuleBB threads self-terminate at their bounded timeout.
- Server-list duplication: the STUN server set is hand-synced in three places
(
StunProbeSeams, rustDEFAULT_STUN_SERVERS, libtorrentdefault_servers). - libtorrent STUN is unwired and unbuilt (verify on next qBittorrentBB build).
Acceptance Criteria¶
- [ ] A written cross-product VPN-guard spec (enable / behavior / fail-closed / STUN-vs-HTTP / allow-list) with justified per-family deviations.
- [ ] emulebb-rust and qBittorrentBB gain an egress guard reusing their FEAT-121 STUN primitives, or an explicit decision recording why not.
- [ ] The divergences/weaknesses above are each resolved or explicitly accepted.
- [ ] Decision on empty-allow-list semantics (skip vs hard block).
Release Note¶
Post-0.7.3 cross-product hardening/alignment work; targets the 0.8.x line. No behavior change in 0.7.3.