Skip to content

Add SOCKS5 proxy support (socks5:// and socks5h://)#1299

Merged
quinnj merged 13 commits into
masterfrom
codex/http-socks5-support
Jun 12, 2026
Merged

Add SOCKS5 proxy support (socks5:// and socks5h://)#1299
quinnj merged 13 commits into
masterfrom
codex/http-socks5-support

Conversation

@quinnj

@quinnj quinnj commented Jun 11, 2026

Copy link
Copy Markdown
Member

Summary

Adds SOCKS5 proxy transport to the client, configured the same way as HTTP proxies:

  • socks5:// and socks5h:// URLs work with ProxyURL, ProxyConfig, the proxy= request kwarg, and ALL_PROXY-style environment variables. Following Go's net/http behavior, both schemes send domain targets to the proxy for remote resolution.
  • Username/password credentials come from the proxy URL userinfo (percent-decoded) and are negotiated via RFC 1929 username/password auth when the proxy selects it.
  • The SOCKS handshake runs on the pooled TCP connection before anything else, so plain HTTP/1.1, HTTPS (h1 and ALPN h2), and WebSockets all ride the proxied stream; connect_timeout bounds the handshake via the existing connect deadline.
  • Requests over SOCKS write origin-form targets and never attach Proxy-Authorization; SOCKS plans get distinct pool keys and proxied connections are pooled/reused normally.
  • SOCKS handshake failures surface as HTTP.ConnectError (with the SOCKS error as cause), keeping the "HTTP-typed exceptions only" public error contract; handshake timeouts surface as HTTP.TimeoutError. Neither is retried, matching CONNECT-tunnel failure behavior.

Hardening that came out of review:

  • strict SOCKS proxy URL authority/port parsing (socks5://::1:1080, hex/signed ports, etc. are rejected)
  • _new_conn_tls! now closes the underlying TCP connection when the TLS handshake fails instead of leaking the fd until GC (covers direct, CONNECT-tunnel, and SOCKS paths)

Dependencies

Requires Reseau >= 1.3.0, which includes JuliaServices/Reseau.jl#123 (Reseau.SOCKS) and is available in General.

Tests

  • proxy URL parsing and plan-mode coverage for socks5/socks5h, including no_proxy bypass, case-insensitive schemes, bracketed IPv6 proxy hosts, default ports, and malformed-authority rejection
  • end-to-end: plain h1 through a scripted SOCKS proxy (with RFC 1929 auth), HTTPS h1 and ALPN h2 over bridged SOCKS streams, WebSockets over SOCKS, pool reuse across sequential requests through one handshake
  • failure modes: command rejection, auth-method mismatch, empty-username URLs (rejected before any proxy I/O), and a stalled proxy hitting connect_timeout
  • full local suite green on macOS (aarch64) against registered Reseau v1.3.0; test/http_client_proxy_tests.jl standalone: 162 passing

Co-authored by Codex

🤖 Generated with Claude Code

quinnj and others added 4 commits June 10, 2026 18:17
Route requests through SOCKS5 proxies configured with ProxyURL,
ProxyConfig, or ALL_PROXY-style environment variables. Both socks5:// and
socks5h:// follow Go's net/http behavior and pass domain targets to the
proxy for resolution. Username/password credentials come from the proxy
URL userinfo (percent-decoded) and are negotiated with RFC 1929
username/password auth when the proxy selects it.

The SOCKS handshake is performed by Reseau.SOCKS.connect! on the pooled
TCP connection before TLS, HTTP/1.1, HTTP/2, or WebSocket traffic flows,
and connect_timeout bounds the handshake via the existing connect
deadline. Plain-HTTP requests over SOCKS write origin-form targets and
never attach Proxy-Authorization.

Co-authored by Codex
The 250ms request/read/write timeouts were tight enough to flake on
loaded CI machines; the test only asserts that configured timeouts
propagate into the request context, so the exact value just needs to be
distinctive.

Co-authored by Codex
- Reject malformed non-bracketed SOCKS proxy authorities (e.g.
  "socks5://::1:1080") instead of silently treating the whole string as
  a host with the default port.
- Parse SOCKS proxy ports strictly as decimal; "0x50", "+80", and other
  tryparse-isms are rejected.
- Wrap SOCKS handshake failures (AuthenticationError, ReplyError,
  ProtocolError, TargetAddressError) in HTTP.ConnectError so callers see
  HTTP-typed exceptions per the public error contract; timeouts and EOFs
  keep their existing classifications and none of these are retried,
  matching CONNECT-tunnel behavior.
- Close the underlying TCP connection when the TLS handshake fails in
  _new_conn_tls! instead of leaking the fd until GC; this covers direct,
  CONNECT-tunnel, and SOCKS TLS paths.
- Expand test coverage: parse edge cases (case-insensitive schemes,
  bracketed IPv6 proxy hosts, default ports, path/query stripping,
  malformed authorities and ports), no_proxy bypass for SOCKS plans,
  ConnectError wrapping for command rejection / auth-method mismatch /
  empty username, connect_timeout enforcement against a stalled proxy,
  HTTPS h1 over a SOCKS-bridged TLS stream, and connection pool reuse
  across sequential requests through one SOCKS handshake.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reseau.SOCKS ships in JuliaServices/Reseau.jl#123 and is not registered
yet; pin Reseau to that PR head so CI dependency resolution succeeds.
Revert this commit once Reseau >= 1.2.1 is registered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
quinnj added a commit to JuliaServices/Reseau.jl that referenced this pull request Jun 11, 2026
The SOCKS support layer is a new feature on top of the released 1.2.1,
and HTTP.jl's SOCKS proxy support (JuliaWeb/HTTP.jl#1299) declares
compat against the version that first ships Reseau.SOCKS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
quinnj and others added 3 commits June 10, 2026 19:13
Reseau#123 now bumps its version to 1.3.0 (matching the Reseau = "1.3"
compat here) and merges Reseau main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Windows environment variables are case-insensitive, so clearing
"all_proxy" after setting "ALL_PROXY" deleted the value and
ProxyFromEnvironment saw no proxy at all. Clear the lowercase variants
first and set ALL_PROXY last.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.97959% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 87.02%. Comparing base (f3e6efe) to head (64497f7).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/http_proxy.jl 98.43% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1299      +/-   ##
==========================================
+ Coverage   86.91%   87.02%   +0.10%     
==========================================
  Files          28       28              
  Lines       10915    11017     +102     
==========================================
+ Hits         9487     9587     +100     
- Misses       1428     1430       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

findlast(==('@'), authority) dispatches to the generic Function method,
which the Julia 1.13 juliac trim verifier rejects as an unresolved call
(the SOCKS parser is reachable from ProxyFromEnvironment in trimmed
binaries). Scan code units with the existing _find_last_url_byte helper
like the rest of the URL parsing code. Verified locally with the trim
compile tests under 1.13.0-rc1: 63/63 pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
quinnj added a commit to JuliaServices/Reseau.jl that referenced this pull request Jun 11, 2026
* feat(socks): add SOCKS5 connect helper

Add an internal SOCKS module that performs a SOCKS5 CONNECT handshake over an existing TCP.Conn, including no-auth and username/password auth, target encoding, reply parsing, typed errors, and deadline handling.

Cover FQDN, IPv4, IPv6, auth negotiation, command failures, pre-I/O target validation, and stalled proxy deadlines with focused Reseau tests.

* fix(socks): validate targets and credentials before proxy I/O

- Reject zone-scoped IPv6 targets ("[fe80::1%en0]:80") up front: macOS
  inet_pton accepts them while embedding the zone index into the address
  bytes, and on other platforms they leaked to the proxy as FQDN targets.
- Reject bracketed non-IPv6 hosts ("[1:2:3]:80") instead of sending them
  as domain names.
- Validate username/password (presence, 1-255 byte lengths, password
  requires username) in connect! before any bytes reach the proxy.
- Parse target ports strictly as decimal ("0x50", "+80", " 80" no longer
  accepted).
- Drop a throwaway SocketAddrV6 construction in bound-reply parsing.
- Document deadline overwrite semantics, thrown error types, and the
  RFC 1928/1929 scope on the module and connect! docstrings.
- Expand test coverage: protocol-error replies (bad versions, non-zero
  RSV, unknown ATYP), auth subnegotiation failures, unsupported method
  selections, proxy EOF mid-handshake, no-auth selected despite offered
  credentials, username-only auth, 255-byte credential/FQDN boundaries,
  IPv4 bound replies, and pre-I/O validation of targets and credentials.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(hostresolvers): reject zone-scoped literals in _parse_ipv6_literal

macOS inet_pton(AF_INET6) accepts "fe80::1%en0" and embeds the zone index
into the returned address bytes, while Linux and Windows reject zoned
input. Reject '%' before calling inet_pton so results are
platform-independent; callers that support zones already split them off
via _split_host_zone first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Bump version to 1.3.0

The SOCKS support layer is a new feature on top of the released 1.2.1,
and HTTP.jl's SOCKS proxy support (JuliaWeb/HTTP.jl#1299) declares
compat against the version that first ships Reseau.SOCKS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(socks): add a trim-safe compile workload

Exercise the SOCKS5 CONNECT handshake (scripted loopback proxy, no
second task) plus pre-I/O target validation and error formatting in a
--trim=safe compiled binary, so trim regressions in the SOCKS layer are
caught in-repo instead of by downstream consumers. HTTP.jl's trim suite
already reaches SOCKS.connect! through its proxy plumbing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Comment thread .github/workflows/ci.yml Outdated
Comment thread src/http_proxy.jl Outdated
@quinnj quinnj merged commit 94c71fe into master Jun 12, 2026
8 checks passed
@quinnj quinnj deleted the codex/http-socks5-support branch June 12, 2026 18:32
@quinnj quinnj mentioned this pull request Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant