Skip to content

feat(event): client-side SOCKS5 proxy support at the io layer - #885

Merged
ithewei merged 12 commits into
masterfrom
feat-socks5-client
Sep 21, 2026
Merged

ithewei merged 12 commits into
masterfrom
feat-socks5-client

Conversation

@ithewei

@ithewei ithewei commented Sep 20, 2026 •

Copy link
Copy Markdown
Owner

What

Adds client-side proxy support at the io layer, hooked into hio_connect(), so any client built on it (TcpClient, HttpClient, ...) can connect through a proxy. The framework is protocol-tagged (proxy_protocol_e); SOCKS5 (RFC 1928 + RFC 1929 username/password auth) is the first and currently only implemented protocol.

Only the client side (dial out through a proxy) is added; the server side stays an example (examples/socks5_proxy_server.c), since a proxy server is an application, not an io capability.

Design

The socket is created for the proxy address, so hio_connect() dials the proxy directly (a single socket, no fd recreation). The proxy handshake then runs before the connection is handed to the upper layer — mirroring how SSL is done at the io layer:

create socket [proxy] -> TCP connect [proxy]
                      -> proxy handshake (SOCKS5 CONNECT to target)
                      -> (optional) TLS handshake against the target -> connect_cb
  • event/hloop.h: proxy_protocol_e { NONE, SOCKS5 } + proxy_setting_t { protocol, proxy_host, proxy_port, target_host, target_port, username, password }. The setting carries the final target the proxy should CONNECT to plus optional credentials.
  • event/socks5.{h,c}: internal proxy_conn_t runtime state (copied setting + fragmentation-safe read accumulator) + SOCKS5 request builders. Target is sent as ATYP=domain for hostnames, ATYP=ipv4/ipv6 for IP literals.
  • event/nio.c: socks5_handshake state machine (method negotiation → [user/pass auth] → CONNECT <target>:<port> → reply), driven non-blockingly via hio_add, exactly like ssl_client_handshake (raw recv into an internal buffer; never touches io->read_cb). proxy_handshake_start() dispatches by protocol. Handshake sends use a dedicated raw send() (no upper-layer write_cb, no hssl_write on a not-yet-created SSL handle). TLS SNI falls back to proxy_setting.target_host when the socket is the proxy.
  • event/hevent.{h,c}: io->proxy field, hio_set_proxy(io, setting) (copies the setting, validates the protocol), init/cleanup.
  • evpp/Channel.h, evpp/TcpClient.h: setProxy(proxy_setting_t*). TcpClient::createsocket() is called with the proxy address; the normal (async-DNS) connect path resolves and dials the proxy, and the target is delivered via setProxy.

API

// C — create the socket for the PROXY, then set the target + auth:
hio_t* io = hio_create_socket(loop, proxy_host, proxy_port, HIO_TYPE_TCP, HIO_CLIENT_SIDE);
proxy_setting_t s;
memset(&s, 0, sizeof(s));
s.protocol = PROXY_PROTOCOL_SOCKS5;
strcpy(s.target_host, "target.example.com"); s.target_port = 1234;
// optional: strcpy(s.username, ...); strcpy(s.password, ...);
hio_set_proxy(io, &s);          // before hio_connect
hio_connect(io);
// C++ — createsocket(PROXY), setProxy(target):
proxy_setting_t s;
strcpy(s.target_host, "target.example.com"); s.target_port = 1234;
tcpClient.createsocket(1080, "127.0.0.1");   // proxy
tcpClient.setProxy(&s);

Scope / platform

  • Protocol: SOCKS5 only (proxy_protocol_e leaves room for more).
  • Auth: no-auth + username/password (RFC 1929). No GSSAPI.
  • Covered by epoll / kqueue / wepoll (all use nio.c). The legacy IOCP backend (overlapio.c) is not wired — it is no longer maintained and superseded by wepoll on Windows.

Testing

  • make libhv ✅, make socks5_client_test ✅.
  • End-to-end against libhv's own socks5_proxy_server and a fragmenting fake proxy (2-byte replies split byte-by-byte with delay):
    • IPv4 target (ATYP=1) → echo round-trips ✅
    • hostname target (localhost, resolved by the proxy, ATYP=domain) → ✅
    • username/password auth proxy → ✅
    • fragmented method/auth replies → ✅ (no data loss; no onWriteComplete before onConnection)

Docs / examples

  • examples/socks5_client_test.c (pure C, io layer), docs/cn/socks5.md, build wiring for Makefile/CMake/Bazel.

Add SOCKS5 (RFC 1928 + RFC 1929 user/pass auth) client proxy support hooked
into hio_connect(), so any client built on it (TcpClient, HttpClient, ...)
can connect through a SOCKS5 proxy. Only the client side (dial out through a
proxy); the server side remains an example (examples/socks5_proxy_server.c).

- event/socks5.{h,c}: socks5_setting_t (user config, like unpack_setting_t /
  reconn_setting_t) + internal socks5_conn_t runtime state; request builders.
- event/nio.c: socks5_handshake state machine (method negotiation ->
  [user/pass auth] -> CONNECT <domain>:<port> -> reply), driven non-blockingly
  via hio_add like the SSL handshake. hio_connect() dials the proxy instead of
  the target and records the target (sent as a domain, ATYP=domain, so the
  proxy resolves it). SOCKS5 runs before the optional SSL handshake.
- event/hevent.{h,c}: io->socks5 field, hio_set_socks5(io, setting) (copies
  the setting), init/cleanup.
- event/hloop.h: hio_set_socks5 declaration.
- evpp/Channel.h, evpp/TcpClient.h: setSocks5Proxy(socks5_setting_t*); the
  TcpClient skips client-side DNS for a hostname target when a proxy is set
  (the proxy resolves it) and stores the config as socks5_setting_t* (mirrors
  reconn_setting/unpack_setting).
- examples/socks5_client_test.cpp + docs/cn/socks5.md + build wiring
  (Makefile/CMake/Bazel headers).

Covered by epoll/kqueue/wepoll (nio.c); the legacy IOCP backend is not wired
(it is no longer maintained, superseded by wepoll on Windows).

Verified end-to-end against libhv's own socks5_proxy_server: IPv4 target,
hostname target (resolved by the proxy), and username/password auth all
round-trip through the proxy.

Co-authored-by: TRAE CLI <traecli@bytedance.com>
Copilot AI lite review requested due to automatic review settings September 20, 2026 08:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved critical and moderate issues affect backend support, address-family handling, buffer safety, API correctness, and build integration.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 4 High severity · 2 Medium severity · 1 Low severity

Open (7)
What changed in this PR

Adds client-side SOCKS5 proxy support at the io layer, including authentication, C++ APIs, documentation, and an example.

Changes:

  • Implements nonblocking SOCKS5 negotiation and CONNECT handshakes.
  • Integrates proxy configuration with C and C++ clients.
  • Adds public headers, build metadata, documentation, and an example.
File Summary
Makefile.vars Registers the SOCKS5 header.
Makefile Adds the client example target.
examples/​socks5_client_test.cpp Demonstrates proxied TCP connections.
evpp/​TcpClient.h Adds proxy configuration and hostname handling.
evpp/​Channel.h Exposes the SOCKS5 setter.
event/​socks5.h Defines SOCKS5 configuration and helpers.
event/​socks5.c Builds SOCKS5 protocol messages.
event/​nio.c Implements the nonblocking handshake.
event/​hloop.h Declares the C proxy API.
event/​hevent.h Adds per-connection proxy state.
event/​hevent.c Manages proxy state lifecycle.
docs/​cn/​socks5.md Documents configuration and usage.
cmake/​vars.cmake Registers the public header for CMake.
BUILD.bazel Registers the public header for Bazel.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread event/hloop.h Outdated
Comment on lines +347 to +357
// SOCKS5 proxy (client side). When set, hio_connect() dials the proxy at
// setting->host:port and performs a SOCKS5 handshake (RFC 1928), issuing a
// CONNECT to the io's original target (sent as a domain name, ATYP=domain, so
// the proxy resolves it). After the handshake succeeds the connection is
// transparent and (if SSL was enabled) the TLS handshake runs against the
// target. Because it hooks hio_connect, all clients built on it (TcpClient,
// HttpClient, ...) can use it. The setting is copied. Pass an empty username
// for no auth, or a username/password for RFC 1929 auth.
// NOTE: set before hio_connect().
struct socks5_setting_s;
HV_EXPORT int hio_set_socks5(hio_t* io, struct socks5_setting_s* setting);
Comment thread event/nio.c Outdated
Comment thread event/nio.c Outdated
Comment thread event/socks5.h Outdated
Comment on lines +35 to +39
typedef struct socks5_setting_s {
char host[256]; // proxy host
int port; // proxy port
char username[256]; // empty => no auth
char password[256];
Comment thread Makefile Outdated

ifeq ($(WITH_EVPP), yes)
EXAMPLES += nmap
EXAMPLES += nmap socks5_client_test
Comment thread evpp/TcpClient.h Outdated
Comment thread docs/cn/socks5.md Outdated
- socket address family: with a SOCKS5 proxy, the socket was created with the
  target's family but hio_connect() repointed peeraddr to the proxy, which can
  resolve to a different family (EAFNOSUPPORT). Now recreate the fd with the
  proxy's family (detach/close/attach) when it differs from the target family.
- auth request buffer: enlarge the handshake buffer to 640 bytes; an RFC 1929
  username/password request can be up to 513 bytes and overran the 512 buffer.
- move socks5_setting_t (public config) into hloop.h; socks5.h is now an
  internal header (not installed). This keeps the public API self-contained in
  hloop.h and stops exporting an internal header.
- C init: document that C callers must zero socks5_setting_t (memset / = {0});
  C++ keeps a default constructor. (no extra _init function; memset is enough)
- examples: add socks5_client_test to CMake (was Make-only).
- docs: fix the C++ API declaration (setSocks5Proxy is on SocketChannel).

Still epoll/kqueue/wepoll only (nio.c); legacy IOCP backend not wired.

Co-authored-by: TRAE CLI <traecli@bytedance.com>
Copilot AI review requested due to automatic review settings September 20, 2026 15:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved handshake, socket-management, backend, DNS, and build-integration issues remain.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 3 High severity · 3 Medium severity · 1 Low severity

Open (7)
Resolved since last review (3)
Previously missed (4)

In code that hasn't changed since last review

Medium severity Handle partial nonblocking SOCKS5 writes

event/​nio.c:261

send() on a nonblocking socket may write fewer than n bytes or return EAGAIN; treating any short write as a handshake failure can reject valid connections under backpressure. Queue/retain the unsent bytes and resume on HV_WRITE rather than requiring one complete write.

This issue also appears in the following locations of the same file:

  • line 287
  • line 305
Medium severity Buffer fragmented SOCKS5 method replies

event/​nio.c:268

A TCP read is not guaranteed to return both bytes of this SOCKS5 method reply. If the proxy fragments the reply, this consumes the first byte and the n < 2 check closes a valid connection instead of waiting for the remaining byte. Accumulate the fixed-length reply (and preserve partial bytes) before parsing it.

This issue also appears on line 294 of the same file.

Medium severity Validate the RFC 1929 reply version

event/​nio.c:297

RFC 1929 requires the subnegotiation reply version to be 0x01, but this check validates only the status byte. A malformed or incompatible proxy returning another version with status 0 would be accepted and the client would continue parsing the stream as SOCKS5.

Medium severity Preserve the original hostname for proxy resolution

event/​nio.c:619

For a raw hio_create_socket(loop, "hostname", ...) client, the original hostname is not stored in hio_t; only the synchronously resolved peer address is retained. Unless the caller separately calls hio_set_hostname, this fallback sends the resolved IP to the proxy, so the proxy does not perform the documented proxy-side hostname resolution. Preserve the original host or make that requirement explicit.

Comment thread event/nio.c Outdated
Comment thread event/nio.c Outdated
Comment thread docs/cn/socks5.md Outdated
Address the deeper review findings:

- Fragmentation: the 2-byte method/auth replies (and the variable CONNECT
  reply) are now accumulated in socks5_conn_t (rbuf/rlen/want) until a full
  step is available, so a reply split across TCP segments no longer fails the
  handshake. Previously a short read was treated as fatal.
- read_cb hijack (regression fix): drive the handshake via
  hio_add(io, socks5_handshake, HV_READ) doing raw recv into the accumulator,
  exactly like ssl_client_handshake, and NEVER touch io->read_cb. The earlier
  hio_readbytes/hio_setcb_read approach overwrote (and then cleared) the
  upper-layer Channel's read_cb, so after the tunnel came up user data was
  dropped and onMessage never fired. Handoff now just hio_del(HV_READ) and runs
  the SSL handshake / connect_cb with read_cb intact.
- send: handshake requests go through hio_write (handles partial writes),
  instead of a bare send() whose short write was treated as fatal.
- ATYP: an IPv4/IPv6 literal target is now sent as ATYP=1/4 (raw address) per
  RFC 1928; only real hostnames use ATYP=domain. Previously '127.0.0.1'/'::1'
  were sent as domains, which strict proxies reject and '::1' cannot resolve.
- remove dead code socks5_connect_reply_len().

Verified end-to-end (echo round-trip) with: real libhv proxy; a fake proxy
that splits the method reply and the auth reply 1+delay+1 byte; and
username/password auth with a domain target. All deliver data correctly now.

Co-authored-by: TRAE CLI <traecli@bytedance.com>
Copilot AI review requested due to automatic review settings September 20, 2026 17:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The SOCKS5 handshake write path has a critical runtime issue, with additional protocol, target-host, and build/documentation problems unresolved.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 4 High severity · 3 Medium severity · 1 Low severity

Open (8)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Preserve proxy target hostname separately from SNI

event/​nio.c:655

io->hostname is the SNI field, not a guaranteed copy of the original connect target. In particular, hio_create_socket() resolves a hostname and stores only the resulting peer address, so a generic C client using hio_create_socket(loop, "name", ...) still performs local DNS and this fallback sends the resolved IP instead of an ATYP=domain request; a caller that sets a custom SNI can also make the proxy connect to the wrong host. Preserve the target hostname separately (or add an explicit target-host API) rather than reusing the SNI field.

Comment thread event/nio.c Outdated
ithewei and others added 2 commits September 21, 2026 02:08
- send handshake packets via a dedicated raw send() instead of hio_write():
  hio_write leaked handshake bytes (incl. credentials) to the upper-layer
  write_cb before onConnection, dispatched to hssl_write() with a not-yet-
  created SSL handle for TLS targets, and could clobber the handshake read
  handler by enqueueing on EAGAIN. Short write/error is treated as fatal
  (handshake runs on a fresh connection with an empty send buffer, <=513B).
- examples/BUILD.bazel: add socks5_client_test target (guarded by with_evpp).
- docs/cn/socks5.md: remove duplicated typedef block; note that a hostname
  proxy resolves synchronously on the loop thread.

Co-authored-by: TRAE CLI <traecli@bytedance.com>
The SOCKS5 proxy support lives at the io (C) layer, so the example is more
appropriate as plain C using hio_set_socks5() directly rather than the C++
TcpClient wrapper. This also drops the with_evpp guard in all three build
systems (Makefile/CMake/Bazel), since the C example has no C++ dependency.

Co-authored-by: TRAE CLI <traecli@bytedance.com>
Copilot AI review requested due to automatic review settings September 20, 2026 18:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread event/nio.c
int n = socks5_build_method_request(s5, buf);
if (socks5_send(io, buf, n) != 0) { socks5_fail(io); return; }
socks5_expect(io, S5_RECV_METHOD, 2);
hio_add(io, socks5_handshake, HV_READ);
Comment thread examples/socks5_client_test.c Outdated
hio_create_socket(host) resolves the name immediately and only keeps the
numeric sockaddr, so hio_connect() would fall back to that literal (ATYP=1)
and a proxy-only-resolvable name would fail locally. Create the socket with a
loopback placeholder and set the real target via hio_set_hostname(): the
handshake then sends CONNECT <hostname> (ATYP=domain), and hio_connect()
recreates the fd with the proxy family as needed. Verified against a fake
proxy: domain target -> atyp=3, ipv4 literal -> atyp=1.

Co-authored-by: TRAE CLI <traecli@bytedance.com>
Copilot AI review requested due to automatic review settings September 20, 2026 18:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved critical and moderate issues remain in hostname handling, handshake writes, socket preservation, and protocol validation.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 5 High severity · 3 Medium severity

Open (8)
Resolved since last review (1)
Previously missed (2)

In code that hasn't changed since last review

Medium severity Handle partial and blocked nonblocking handshake writes

event/​nio.c:285

A nonblocking send() is allowed to return a short count (or EAGAIN), even for these small messages. Treating any result other than len as fatal can send a partial method/auth/CONNECT request and close otherwise valid connections under a constrained send buffer or transient backpressure. Track the unsent handshake bytes and resume on HV_WRITE, or use an equivalent nonblocking write path that preserves handshake ordering.

This issue also appears on line 327 of the same file.

Medium severity SOCKS5 uses resolved address instead of original hostname

event/​nio.c:674

When the caller uses the low-level API as documented (hio_create_socket(loop, "example.com", ...) followed by hio_set_socks5), hio_create_socket has already resolved the name and does not populate io->hostname. This fallback therefore sends the resolved numeric address instead of the original domain, so the proxy does not perform the promised proxy-side DNS resolution. Preserve the original host for SOCKS5 or explicitly require hio_set_hostname before hio_connect and update the public API documentation to match.

Comment thread event/nio.c Outdated
- example: clarify that hio_create_socket's port must be the real target port
  (only host is a placeholder for the address family; hio_connect takes the
  port from peeraddr).
- docs/cn/socks5.md: the target is only sent as ATYP=domain for hostnames;
  IP literals go as ATYP=ipv4/ipv6 (fix stale wording). Add a C usage section
  showing the placeholder-host + hio_set_hostname pattern so C users don't hit
  'create socket failed' by passing a proxy-only-resolvable hostname to
  hio_create_socket.

Co-authored-by: TRAE CLI <traecli@bytedance.com>
Copilot AI review requested due to automatic review settings September 20, 2026 18:41
C layer does have async DNS (event/hdns), so 'no async DNS' was wrong. The
actual reason not to pass a proxy-only-resolvable hostname to
hio_create_socket() is that this low-level API resolves synchronously via
getaddrinfo (it does not go through hdns), so such a name fails locally.

Co-authored-by: TRAE CLI <traecli@bytedance.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Unresolved moderate issues remain in transport support, handshake robustness, backend behavior, and socket handling.

Review effort: Lite
Findings: 5 High severity · 3 Medium severity

Open (8)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Handle partial nonblocking SOCKS5 handshake writes

event/​nio.c:285

Because this is a nonblocking socket, send() is allowed to return EAGAIN or fewer than len bytes even for these small messages. Treating any short write as fatal can abort valid connections (especially with a full send buffer or maximum-length credentials); retain the unwritten bytes and resume the handshake on HV_WRITE without replacing its read callback.

Copilot AI review requested due to automatic review settings September 20, 2026 18:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved moderate issues affect backend support, protocol validation, nonblocking writes, hostname bounds, and IO-array safety.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 5 High severity · 3 Medium severity · 1 Low severity

Open (9)
Previously missed (3)

In code that hasn't changed since last review

Medium severity SOCKS5 settings ignored by legacy EVENT_IOCP backend

event/​hevent.c:508

This setter succeeds on every backend, but overlapio.c's hio_connect() never consumes io->socks5; builds selecting the legacy EVENT_IOCP backend therefore silently connect without the requested proxy (and hostname targets may use the placeholder address). Since WITH_WEPOLL=OFF still permits this backend, reject the feature there or propagate an unsupported-backend error instead of returning success.

Medium severity Handle partial and interrupted nonblocking handshake sends

event/​nio.c:285

send() on this nonblocking socket may return a short count, EAGAIN, or EINTR; treating every result other than exactly len as fatal can abort otherwise valid proxy connections under backpressure or a signal. Keep the unsent handshake bytes and resume on HV_WRITE (without invoking the upper-layer write callback) before advancing the handshake state.

Medium severity Validate SOCKS5 authentication reply version

event/​nio.c:329

RFC 1929 replies include a version byte, but this branch only checks the status and accepts a malformed response with any buf[0]. Validate buf[0] == SOCKS5_AUTH_VERSION as well before proceeding to CONNECT.

Comment thread event/hloop.h Outdated
Rework client-side proxy so the socket is created for the PROXY address and
never rebuilt. Replaces socks5_setting_t/hio_set_socks5 with a protocol-tagged
proxy_setting_t + hio_set_proxy:

- hloop.h: proxy_protocol_e (NONE/SOCKS5) + proxy_setting_t
  {protocol, proxy_host, proxy_port, target_host, target_port, user, pass}.
  hio_set_proxy() validates the protocol (only SOCKS5 implemented).
- hevent: io->socks5 -> io->proxy (proxy_conn_t holds the copied setting +
  handshake accumulator); build_* read setting.target_host/target_port.
- nio.c: hio_connect() just connect()s the (proxy-bound) fd; proxy_handshake_
  start() dispatches by protocol. Removes the previous connect-time fd rebuild
  path entirely.
- evpp: Channel::setProxy / TcpClient::setProxy; TcpClient resolves the proxy
  address (numeric direct, hostname via async hdns) and connects the socket to
  the proxy, passing createsocket()'s host/port as the target.
- example rewritten to hio_create_socket(proxy) + hio_set_proxy(target); docs
  updated for the new API.

Verified end-to-end (C example + TcpClient) against libhv's socks5_proxy_server
and a fragmenting fake proxy: no-auth and user/pass auth, IPv4 and domain
targets, replies split byte-by-byte -- all deliver data, no writes before
onConnection.

Co-authored-by: TRAE CLI <traecli@bytedance.com>
Copilot AI review requested due to automatic review settings September 20, 2026 20:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread event/hevent.c
Comment on lines +500 to +503
int hio_set_proxy(hio_t* io, proxy_setting_t* setting) {
if (io == NULL || setting == NULL) return -1;
// only SOCKS5 is implemented so far
if (setting->protocol != PROXY_PROTOCOL_SOCKS5) return -1;
Comment thread evpp/TcpClient.h Outdated
Comment thread evpp/TcpClient.h Outdated
Comment thread event/hevent.c
Comment on lines +508 to +510
// copy the user config; runtime fields (state/accumulator) are filled at connect
io->proxy->setting = *setting;
return 0;
Comment thread event/hloop.h
Comment on lines +369 to +389
typedef struct proxy_setting_s {
int protocol; // proxy_protocol_e
char proxy_host[256]; // proxy host (used to create/connect the socket)
int proxy_port;
char target_host[256]; // final target the proxy should CONNECT to
int target_port;
char username[256]; // empty => no auth
char password[256];
#ifdef __cplusplus
proxy_setting_s() {
protocol = PROXY_PROTOCOL_SOCKS5;
proxy_host[0] = '\0';
proxy_port = 0;
target_host[0] = '\0';
target_port = 0;
username[0] = '\0';
password[0] = '\0';
}
#endif
} proxy_setting_t;
HV_EXPORT int hio_set_proxy(hio_t* io, proxy_setting_t* setting);
…a proxy

- TcpClient: drop the separate startConnectViaProxy path. createsocket() now
  connects to the PROXY (createsocket(proxy_port, proxy_host)) and setProxy()
  carries the target; the normal DNS/connect path resolves the proxy, so a
  single socket is used (no discard/recreate of a target socket). This also
  fixes the Copilot findings about reusing a pre-created target channel and the
  DNS-fallback connecting to the target.
- nio.c: when using a proxy over TLS, io->hostname is unset (socket is the
  proxy), so fall back to proxy_setting.target_host for hssl_set_sni_hostname
  (an explicit hio_set_hostname still wins). Fixes omitted SNI for HTTPS via
  proxy.
- hloop.h: keep proxy_host/proxy_port in proxy_setting_t for reference (SOCKS5
  path does not require them since the socket already is the proxy connection).
- example/docs updated to createsocket(proxy) + setProxy(target).

Verified end-to-end (C example + TcpClient) against libhv's socks5_proxy_server
and a fragmenting fake proxy: no-auth + user/pass auth, IPv4 + domain targets,
byte-split replies -- all deliver data, no writes before onConnection.

Co-authored-by: TRAE CLI <traecli@bytedance.com>
Copilot AI review requested due to automatic review settings September 20, 2026 20:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Resolve the public API mismatch, unsupported IOCP path, handshake timeout/validation issues, and nonblocking short-write handling.

Review effort: Lite
Findings: 4 High severity · 2 Medium severity · 1 Low severity

Open (7)
Resolved since last review (5)
Previously missed (3)

In code that hasn't changed since last review

Medium severity Nonblocking proxy writes mishandle short sends

event/​nio.c:290

send() on this nonblocking socket is allowed to return a short count or EAGAIN, especially for the up-to-513-byte username/password request. Treating every result other than exactly len as fatal can reject valid connections under a small send buffer or transient backpressure; retain the unsent bytes and resume on writability instead.

Medium severity Missing timeout for asynchronous SOCKS handshake

event/​nio.c:439

When the underlying nonblocking connect() returns 0, hio_connect() schedules nio_connect_async() without creating the connect timer. This branch then starts an asynchronous SOCKS handshake, so a proxy that accepts TCP but never replies leaves the connection open indefinitely despite connect_timeout; start/retain a timeout for the proxy handshake on this path.

Medium severity TcpClient SOCKS5 API name and type mismatch

evpp/​TcpClient.h:375

The PR API specifies TcpClient::setSocks5Proxy(socks5_setting_t*), but this change exposes setProxy(proxy_setting_t*) instead. Code using the promised method/type will not compile, and the docs/examples now describe a different API; please keep the public name and setting type consistent across the implementation, headers, and description.

Through a proxy the TLS peer is the target, so proxy_setting.target_host is
authoritative for SNI; fall back to io->hostname only when no proxy target is
set. (nio_connect_established; matches TcpClient's SNI selection.)

Co-authored-by: TRAE CLI <traecli@bytedance.com>
Copilot AI review requested due to automatic review settings September 21, 2026 03:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Unresolved API inconsistencies, IOCP bypass, handshake handling and validation defects, and a hanging example block approval.

Review effort: Lite
Findings: 4 High severity · 2 Medium severity · 1 Low severity

Open (7)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Nonblocking SOCKS5 handshake mishandles partial writes

event/​nio.c:294

This uses a raw send() on a nonblocking socket and treats any short write or EAGAIN as a fatal SOCKS5 failure. Nonblocking TCP sends are allowed to write only part of the request (the username/password request can be over 500 bytes), so valid proxy connections can fail under buffer pressure; retain the unwritten handshake bytes and resume on HV_WRITE instead of closing.

…name)

SNI must be a hostname, not an IP literal (RFC 6066). Check is_ipaddr on each
candidate independently: if the proxy target_host is numeric, skip it and fall
back to io->hostname (which may be an explicitly-set domain) rather than
dropping SNI entirely.

Co-authored-by: TRAE CLI <traecli@bytedance.com>
Copilot AI review requested due to automatic review settings September 21, 2026 11:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Unresolved proxy-handshake, unsupported-backend, API, and validation issues remain.

Review effort: Lite
Findings: 4 High severity · 2 Medium severity · 1 Low severity

Open (7)
Previously missed (2)

In code that hasn't changed since last review

Medium severity Proxy handshake mishandles partial and interrupted sends

event/​nio.c:295

send() on a nonblocking stream may return a short count, EAGAIN, or EINTR; treating every result other than exactly len as fatal can close an otherwise valid proxy connection during method/auth/CONNECT negotiation under backpressure. Preserve the unsent suffix and wait for HV_WRITE (or otherwise complete the raw send) before advancing the handshake.

Medium severity TcpClient ignores unsupported proxy configuration failures

evpp/​TcpClient.h:260

TcpClient::setProxy accepts any protocol, but SocketChannel::setProxy/hio_set_proxy can reject it (for example PROXY_PROTOCOL_NONE), and this return value is ignored. The client then proceeds to connect to remote_host—which is the proxy address—and invokes onConnection as if the target tunnel were established. Propagate or handle the failure before starting the connection.

@ithewei
ithewei merged commit 0db9c63 into master Sep 21, 2026
13 checks passed
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.

2 participants