Describe the bug
On Windows, connections of processes covered by a PROXY rule gradually start going out
directly, bypassing the proxy, after ProxyBridge has been running for a few hours.
Restarting ProxyBridge restores correct behaviour for another few hours.
It starts as occasional single connections ("flapping": the same process is proxied in
one request and direct in the next) and degrades over uptime until a browser session is
effectively permanently direct.
The cause is the per-port decision fast-path cache in packet_processor
(port_decided_bitmap / port_direct_bitmap): a DIRECT verdict cached for a source
port outlives the connection that produced it and is then inherited by an unrelated
process that reuses the same ephemeral port.
Environment
- OS: Windows 11 Pro 24H2 (26200)
- ProxyBridge:
dev @ d773d3d (4.0.13-Beta)
- Platform: Windows
- Rules: ~20 enabled per-process rules →
PROXY (SOCKS5), plus a LAN range → DIRECT;
Localhost via Proxy disabled
To reproduce
- Add a rule
curl.exe → PROXY (any SOCKS5/HTTP proxy with a distinguishable exit IP).
- Leave a chatty process that is not proxied running — anything that opens many
short-lived connections (a browser with a disabled rule, local dev tooling, an
IPC-heavy editor). Each of its connections caches a DIRECT verdict on its ephemeral
port.
- Poll the exit address in a loop, e.g. every 5 minutes:
curl.exe https://ifconfig.me
- Within a few hours some replies return the host WAN address instead of the proxy
exit. The share of leaked connections grows with uptime.
The leak is easiest to see with an instrumented build that logs (a) the rule verdict for
every SYN and (b) every fast-path hit on a cached DIRECT.
What actually happens
Eviction of the decision cache is incomplete. port_clear() is only reached from the
outbound FIN/RST branches:
if (port_is_decided(sp))
{
if (tcp_header->Fin || tcp_header->Rst)
port_clear(sp);
if (port_is_direct(sp))
{
WinDivertSend(windivert_handle, packet, packet_len, NULL, &addr);
continue;
}
}
so an entry survives whenever the close does not pass through that branch:
- the server closes first — the
FIN arrives on the inbound branch, which never
touches the bitmaps;
- loopback IPC and abortive teardowns are not seen by the filter at all.
The entry then stays set until the process restarts. When Windows reuses that ephemeral
port for a different process, the first SYN hits the fast path, is sent unmodified and
check_process_rule() never runs — a PROXY rule is silently skipped.
Measured on an instrumented build over 63 h: 232 056 ports were freed by Windows
while their cached verdict was still set and no FIN/RST had reached the filter. Cache
fill grew from ~2 400 to ~57 000 of 65 536 ports.
Note the SYN path already anticipates port recycling — it evicts the PID cache
there:
if (tcp_header->Syn && !tcp_header->Ack)
remove_cached_pid(ip_header->SrcAddr, sp, FALSE);
but leaves the decision cache stale, so the wrong-app problem is only half closed.
Expected behaviour
A new connection is matched against the rules that apply to its process, regardless of
which process previously used the ephemeral port.
Additional context
- The IPv6 branch shares the same bitmaps and has no eviction, so a verdict cached for an
IPv4 connection can be inherited by an IPv6 one reusing the port number.
- Evicting the decision cache on a fresh
SYN (next to the existing remove_cached_pid)
fixes it: 63 h uptime, ~1.4 M connection events, zero leaks, exit IP stable across 757
probes. Happy to send a PR against dev.
Describe the bug
On Windows, connections of processes covered by a
PROXYrule gradually start going outdirectly, bypassing the proxy, after ProxyBridge has been running for a few hours.
Restarting ProxyBridge restores correct behaviour for another few hours.
It starts as occasional single connections ("flapping": the same process is proxied in
one request and direct in the next) and degrades over uptime until a browser session is
effectively permanently direct.
The cause is the per-port decision fast-path cache in
packet_processor(
port_decided_bitmap/port_direct_bitmap): aDIRECTverdict cached for a sourceport outlives the connection that produced it and is then inherited by an unrelated
process that reuses the same ephemeral port.
Environment
dev@d773d3d(4.0.13-Beta)PROXY(SOCKS5), plus a LAN range →DIRECT;Localhost via Proxy disabled
To reproduce
curl.exe → PROXY(any SOCKS5/HTTP proxy with a distinguishable exit IP).short-lived connections (a browser with a disabled rule, local dev tooling, an
IPC-heavy editor). Each of its connections caches a
DIRECTverdict on its ephemeralport.
curl.exe https://ifconfig.meexit. The share of leaked connections grows with uptime.
The leak is easiest to see with an instrumented build that logs (a) the rule verdict for
every
SYNand (b) every fast-path hit on a cachedDIRECT.What actually happens
Eviction of the decision cache is incomplete.
port_clear()is only reached from theoutbound
FIN/RSTbranches:so an entry survives whenever the close does not pass through that branch:
FINarrives on the inbound branch, which nevertouches the bitmaps;
The entry then stays set until the process restarts. When Windows reuses that ephemeral
port for a different process, the first
SYNhits the fast path, is sent unmodified andcheck_process_rule()never runs — aPROXYrule is silently skipped.Measured on an instrumented build over 63 h: 232 056 ports were freed by Windows
while their cached verdict was still set and no
FIN/RSThad reached the filter. Cachefill grew from ~2 400 to ~57 000 of 65 536 ports.
Note the
SYNpath already anticipates port recycling — it evicts the PID cachethere:
but leaves the decision cache stale, so the wrong-app problem is only half closed.
Expected behaviour
A new connection is matched against the rules that apply to its process, regardless of
which process previously used the ephemeral port.
Additional context
IPv4 connection can be inherited by an IPv6 one reusing the port number.
SYN(next to the existingremove_cached_pid)fixes it: 63 h uptime, ~1.4 M connection events, zero leaks, exit IP stable across 757
probes. Happy to send a PR against
dev.