fix(client): write a single NULL after USERID in SOCKS4 requests - #307
Open
monosans wants to merge 1 commit into
Open
fix(client): write a single NULL after USERID in SOCKS4 requests#307monosans wants to merge 1 commit into
monosans wants to merge 1 commit into
Conversation
`Request::write_to_buf` emitted two NULL bytes after DSTIP, labelled
`USERID` and `NULL`. Per SOCKS4 ("SOCKS: A protocol for TCP proxy across
firewalls", Ying-Da Lee, https://www.openssh.com/txt/socks4.protocol),
a CONNECT request is
VN(1) CD(1) DSTPORT(2) DSTIP(4) USERID(variable) NULL(1)
where USERID is a variable-length field terminated by a single NULL.
This connector never sends a user ID, so USERID is zero bytes long and
only its terminator belongs on the wire: 9 bytes, not 10.
The extra byte breaks SOCKS4a ("SOCKS 4A: A Simple Extension to SOCKS 4
Protocol", https://www.openssh.com/txt/socks4a.protocol), which appends
a NULL-terminated hostname after the USERID terminator. A server reads
USERID up to the first NULL (correctly empty), then reads the hostname
up to the next NULL -- the stray byte -- so the destination arrives as
an empty string. The request fails, and the real hostname plus its
terminator remain in the server's receive buffer, to be relayed into the
tunnel as the first bytes of application data.
Plain SOCKS4 requests still parse, since the destination is carried in
DSTIP, but the trailing byte is likewise left over and reaches the
target as a stray leading NULL of the tunneled stream.
The bug dates back to the connectors' introduction in hyperium#187, and the
domain path had no test coverage, so a SOCKS4a test is added alongside.
Two stale comments on the touched lines are corrected as well:
`put_u16(*port)` was labelled `IP`, and the layout diagram read
"only do IP is 0.0.0.X".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What's wrong
The SOCKS4 connector writes one byte too many in its CONNECT request.
A SOCKS4 request is an 8-byte header followed by a user ID — a variable-length string ended by a single zero byte:
— SOCKS: A protocol for TCP proxy across firewalls, Ying-Da Lee
This connector never sends a user ID, so that field is empty and only its single terminating zero byte belongs on the wire — 9 bytes in total.
Request::write_to_bufinsrc/client/legacy/connect/proxy/socks/v4/messages.rswrote two zero bytes (one commentedUSERID, one commentedNULL), so it sent 10.Why it matters
The SOCKS4a extension — the one that lets the proxy resolve a domain name — puts the hostname immediately after that terminator, itself ended by a zero byte:
— SOCKS 4A: A Simple Extension to SOCKS 4 Protocol, Ying-Da Lee
That is this connector's default mode:
local_dnsis off by default, so a domain destination is sent by name for the proxy to resolve. With an extra zero byte in front of it, the proxy reads the hostname field as an empty string, and the real hostname is never part of the request at all.Feeding both versions of the bytes to a parser that follows the spec literally:
In other words:
hyper.rs\0stays in the proxy's receive buffer and is relayed into the tunnel as if the client had sent it as application data.As an independent check, curl builds the same request with exactly one terminator: in
lib/socks.cit appendsstrlen(user) + 1bytes for the user ID (just the terminator when there is no user), thenstrlen(hostname) + 1for SOCKS4a.The change
remaining_mut()checks and the returned lengths from10to9.test_socks_v4a_with_server_resolved_domain_works. The domain path had no test at all, which is how this went unnoticed since the connectors landed in feat(client): add proxy::SocksV4 and proxy::SocksV5 connectors #187 — the existingtest_socks_v4_worksonly covers the IPv4 path, where the bug is much less visible.buf.put_u16(*port)was labelled// IP, and the layout diagram read "only do IP is 0.0.0.X".No public API change; the only behavioural difference is one fewer byte on the wire.
cargo test --all-features --test proxypasses.