Two independent root causes were producing auth failures on the upstream
SOCKS5 dial path:
1. `url::Url::username()` / `Url::password()` return percent-encoded
strings per the WHATWG URL spec, but the producer side already
percent-encodes the credentials when assembling the upstream URL —
so the upstream was receiving `%40` instead of `@` and authentication
silently failed for any credential containing `@ : / + = % ! space`
or non-ASCII characters. Centralize the decode in a new
`upstream_userpass` helper and route all four upstream-dial sites
through it (HTTP CONNECT → SOCKS5, HTTP CONNECT → HTTP Basic-Auth,
local SOCKS5 → HTTP Basic-Auth, local SOCKS5 → SOCKS5). The
Shadowsocks path already decoded manually and is unchanged.
2. async_socks5 0.6 issues a `write_u8` for every single-byte field of
the SOCKS5 method-selection and RFC1929 sub-negotiation. On a raw
`TcpStream` each call becomes its own TCP segment, and some upstream
SOCKS5 implementations treat this fragmented submission as a
misbehaving client and silently FIN instead of returning a status —
curl with the same credentials succeeds because it buffers each
sub-message into a single send(). Wrap the upstream socket in
`tokio::io::BufStream` (the usage pattern the async_socks5 README
shows) and enable TCP_NODELAY so flushes leave unsegmented.
Includes unit tests covering percent-decode for ASCII / special-char /
non-ASCII / no-credentials / username-only inputs, plus a trace-level
SOCKS5 handshake byte logger that can be enabled with
RUST_LOG=donutbrowser_lib::proxy_server=trace for future debugging.