Reported: with an upstream proxy configured, Google flags the session because
the server's address and the proxy's exit address differ, and YouTube
geolocates to the server rather than the proxy exit. Both follow from the same
defect in three places — an outbound rule that says Proxy was being satisfied
by connecting DIRECTLY whenever the proxy could not be used:
- router.rs, UdpSessionRouter::send_to: action Proxy with no UDP proxy
established fell through to the direct socket.
- outbound.rs, connect_target: any protocol string other than exactly
"socks5"/"http" hit a `_ =>` arm that connected directly. A typo, or just
"SOCKS5", silently un-proxied ALL TCP.
- outbound.rs, connect_udp_target: non-socks5 upstream returned
UdpProxySocket::Direct, with a comment noting HTTP cannot carry UDP —
correct in itself, but the chosen fallback leaks.
The visible symptom is precisely what these produce: TCP goes through the
proxy while UDP (QUIC — which is what YouTube uses) leaves from the server,
so one session presents two exit addresses.
All three now fail closed. A rule asking for the proxy is never honoured by
sending in the clear: a dropped flow is visible and debuggable, a
deanonymising leak is neither. Protocol matching is also case-insensitive
now, and the SOCKS5 UDP ASSOCIATE failure warns unconditionally rather than
only under `debug`, which is why this could go unnoticed.
Behaviour change: with an HTTP upstream, or a broken SOCKS5 UDP ASSOCIATE,
UDP now fails instead of leaking. Where that is genuinely wanted, it must be
stated in the config as an explicit udp rule with action "direct".
Also carries the relay key-sync diagnostics: a 404 there means the URL is
missing the panel's secret webpath (the API is nested under it, not at /api),
which the previous bare "HTTP 404" gave no way to work out.
Matches a real report: traffic counters move (the OSTP tunnel handshakes
fine) but sites don't open, or take very long - on a freshly deployed
DigitalOcean droplet in Amsterdam.
connect_target's fallback path handed the raw "host:port" string straight
to TcpStream::connect, which resolves and tries addresses internally but
shares ONE 10s timeout across the WHOLE attempt (all resolved addresses,
not per-address). Some VPS hosts assign the machine an IPv6 address that
the OS prefers by RFC 6724 ordering but that has no actually-working
outbound route - the connect doesn't get refused, it just hangs. With a
single shared budget, that one dead IPv6 candidate eats the entire 10s and
the working IPv4 candidate is never even attempted: every dual-stack
destination (i.e. most popular sites) times out, while IPv4-only
destinations work fine.
New connect_direct() resolves target itself via lookup_host, sorts IPv4
candidates first, and tries each with its own 3s budget (still bounded
overall by the original 10s outer timeout as a backstop) so a hung IPv6
attempt can't starve the IPv4 fallback of a chance.
Added tests: IPv4-first sort ordering (and stability within a family), a
successful connect against a live local listener, and a refused-port
connect failing well under the timeout (proving failures aren't
needlessly slow). Scoped to connect_target's direct-connect paths; the
SOCKS5/HTTP outbound-proxy paths and the fallback/camouflage TCP proxy
(which targets a fixed admin-configured local address, not arbitrary
dual-stack hostnames) are unaffected.