Skip to content

[Windows] Proxy rules stop being applied after hours of uptime: stale port-decision cache leaks traffic direct #206

Description

@rsyuzyov

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

  1. Add a rule curl.exe → PROXY (any SOCKS5/HTTP proxy with a distinguishable exit IP).
  2. 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.
  3. Poll the exit address in a loop, e.g. every 5 minutes: curl.exe https://ifconfig.me
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions