diff --git a/Makefile b/Makefile index 9e8cd0b9f..2b7f50464 100644 --- a/Makefile +++ b/Makefile @@ -94,6 +94,7 @@ EXAMPLES = hmain_test htimer_test hloop_test pipe_test \ udp_echo_server \ udp_proxy_server \ socks5_proxy_server \ + socks5_client_test \ host \ multi-acceptor-processes \ multi-acceptor-threads \ @@ -240,6 +241,9 @@ udp_proxy_server: prepare socks5_proxy_server: prepare $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS)" SRCS="examples/socks5_proxy_server.c" +socks5_client_test: prepare + $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS)" SRCS="examples/socks5_client_test.c" + host: prepare $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS)" SRCS="examples/host.c" diff --git a/docs/cn/socks5.md b/docs/cn/socks5.md new file mode 100644 index 000000000..e5828f9fb --- /dev/null +++ b/docs/cn/socks5.md @@ -0,0 +1,123 @@ +SOCKS5 代理客户端 + +在事件循环(io)层内置的客户端代理支持(目前实现 SOCKS5,RFC 1928 + RFC 1929 用户名/密码认证)。 + +设计上 socket 直接创建/连接到**代理**地址,`hio_connect()` 完成 TCP 连接后先跑代理握手(向代理发起 CONNECT 到目标),握手成功后连接对上层透明;若开启了 SSL,则在隧道之上再与目标做 TLS 握手。 + +由于挂在 `hio_connect` 上,所有基于它的客户端(`TcpClient`、`HttpClient` 等)都能直接使用。 + +> 说明: +> - 只做客户端代理(通过代理连出去),服务端见 [examples/socks5_proxy_server.c](../../examples/socks5_proxy_server.c)。 +> - 支持无认证与用户名/密码认证(不支持 GSSAPI)。 +> - 目标为域名时以 ATYP=domain 发给代理解析(客户端本地不做 DNS);为 IP 字面量时按 ATYP=ipv4/ipv6 发送。 +> - 目前仅实现 `PROXY_PROTOCOL_SOCKS5`。 + +## 配置结构 proxy_setting_t + +```c +typedef enum { + PROXY_PROTOCOL_NONE = 0, + PROXY_PROTOCOL_SOCKS5 = 1, +} proxy_protocol_e; + +typedef struct proxy_setting_s { + int protocol; // proxy_protocol_e,目前仅 SOCKS5 + char proxy_host[256]; // 代理主机(socket 连接到它) + int proxy_port; // 代理端口 + char target_host[256]; // 最终目标(代理去 CONNECT) + int target_port; + char username[256]; // 空 => 无认证 + char password[256]; +} proxy_setting_t; +``` + +> C 用户使用前请先清零:`proxy_setting_t s; memset(&s, 0, sizeof(s));`(或 `= {0}`), +> 否则 username/password 为未初始化值会导致认证方式误判。C++ 有默认构造,无需手动清零。 + +## C 接口 + +```c +// 设置代理(setting 会被拷贝);在 hio_connect() 之前调用。 +// 注意:io 必须创建到代理地址,即 hio_create_socket(loop, proxy_host, proxy_port, ...)。 +int hio_set_proxy(hio_t* io, proxy_setting_t* setting); +``` + +## C++ 接口 + +```c++ +namespace hv { + +// SocketChannel +int SocketChannel::setProxy(proxy_setting_t* setting); + +// TcpClient +void TcpClient::setProxy(proxy_setting_t* setting); + +} +``` + +## 示例 + +### C + +C 层 socket 直接建到**代理**,目标 host/port 通过 `proxy_setting_t` 传入,由代理去 CONNECT/解析: + +```c +#include "hloop.h" +#include "hbase.h" + +// socket 建到代理地址 +hio_t* io = hio_create_socket(loop, proxy_host, proxy_port, HIO_TYPE_TCP, HIO_CLIENT_SIDE); + +proxy_setting_t proxy; +memset(&proxy, 0, sizeof(proxy)); +proxy.protocol = PROXY_PROTOCOL_SOCKS5; +hv_strncpy(proxy.target_host, target_host, sizeof(proxy.target_host)); // 域名 => ATYP=domain(代理解析);IP => ATYP=ipv4/ipv6 +proxy.target_port = target_port; +// 如需认证: hv_strncpy(proxy.username, "user", ...); hv_strncpy(proxy.password, "pass", ...); +hio_set_proxy(io, &proxy); + +hio_setcb_connect(io, on_connect); +hio_setcb_close(io, on_close); +hio_connect(io); +``` + +完整示例见 [examples/socks5_client_test.c](../../examples/socks5_client_test.c)。 + +### C++ + +C++ 用 `TcpClient`:`createsocket(proxy_port, proxy_host)` 连接到**代理**,目标填在 `proxy_setting_t.target_host/target_port`。代理若是域名,由 `TcpClient` 内部异步解析(不阻塞 loop): + +```c++ +#include "TcpClient.h" +using namespace hv; + +int main() { + TcpClient cli; + cli.createsocket(1080, "127.0.0.1"); // 代理地址 + + proxy_setting_t proxy; + hv_strncpy(proxy.target_host, "target.example.com", sizeof(proxy.target_host)); // 目标(域名由代理解析) + proxy.target_port = 1234; + // 如需认证: hv_strncpy(proxy.username, "user", ...); hv_strncpy(proxy.password, "pass", ...); + cli.setProxy(&proxy); + + cli.onConnection = [](const SocketChannelPtr& channel) { + if (channel->isConnected()) channel->write("hello via socks5"); + }; + cli.onMessage = [](const SocketChannelPtr& channel, Buffer* buf) { + printf("recv: %.*s\n", (int)buf->size(), (char*)buf->data()); + }; + cli.start(); + while (1) hv_sleep(1); + return 0; +} +``` + +可用 libhv 自带的 SOCKS5 代理服务端做端到端测试: + +```sh +bin/tcp_echo_server 1234 +bin/socks5_proxy_server 1080 +bin/socks5_client_test 127.0.0.1 1080 127.0.0.1 1234 +``` diff --git a/event/hevent.c b/event/hevent.c index 25bfb8b4f..cd554f285 100644 --- a/event/hevent.c +++ b/event/hevent.c @@ -5,6 +5,7 @@ #include "herr.h" #include "unpack.h" +#include "socks5.h" uint64_t hloop_next_event_id() { static hatomic_t s_id = HATOMIC_VAR_INIT(0); @@ -135,6 +136,7 @@ void hio_ready(hio_t* io) { io->ssl_ctx = NULL; io->alloced_ssl_ctx = 0; io->hostname = NULL; + io->proxy = NULL; // context io->ctx = NULL; // private: @@ -495,6 +497,19 @@ const char* hio_get_hostname(hio_t* io) { return io->hostname; } +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; + if (io->proxy == NULL) { + HV_ALLOC_SIZEOF(io->proxy); + if (io->proxy == NULL) return -1; + } + // copy the user config; runtime fields (state/accumulator) are filled at connect + io->proxy->setting = *setting; + return 0; +} + void hio_del_connect_timer(hio_t* io) { if (io->connect_timer) { htimer_del(io->connect_timer); diff --git a/event/hevent.h b/event/hevent.h index 7f3289132..bdfe0bcf2 100644 --- a/event/hevent.h +++ b/event/hevent.h @@ -186,6 +186,10 @@ struct hio_s { void* ssl; // for hio_set_ssl void* ssl_ctx; // for hio_set_ssl_ctx char* hostname; // for hssl_set_sni_hostname + // client-side proxy: if set, hio_connect performs the proxy handshake + // (CONNECT to the target) before the connection is handed to the upper + // layer / SSL handshake. The io itself connects to the proxy address. + struct proxy_conn_s* proxy; // context void* ctx; // for hio_context / hio_set_context // private: diff --git a/event/hloop.h b/event/hloop.h index a1c89e6a3..cc6aec42b 100644 --- a/event/hloop.h +++ b/event/hloop.h @@ -344,6 +344,51 @@ HV_EXPORT hssl_ctx_t hio_get_ssl_ctx(hio_t* io); HV_EXPORT int hio_set_hostname(hio_t* io, const char* hostname); HV_EXPORT const char* hio_get_hostname(hio_t* io); +// Client-side proxy. When set, hio_connect() performs the proxy handshake +// (issuing a CONNECT to setting->target_host:target_port) before SSL / +// connect_cb; after it 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. +// +// IMPORTANT: create the io for the PROXY address, then set the target here: +// hio_create_socket(loop, proxy_host, proxy_port, ...); +// hio_set_proxy(io, &setting); // setting carries the final target + auth +// The socket connects to the proxy; the io layer only uses target_* and the +// credentials. proxy_host/proxy_port are kept in the setting for reference / +// higher-level use, but the SOCKS5 path does not require them (the socket is +// already the proxy connection). +// +// The setting is copied. Leave username empty for no auth, or set +// username/password for auth (SOCKS5 => RFC 1929). Only PROXY_PROTOCOL_SOCKS5 +// is implemented so far. +// NOTE: set before hio_connect(). +typedef enum { + PROXY_PROTOCOL_NONE = 0, + PROXY_PROTOCOL_SOCKS5 = 1, +} proxy_protocol_e; + +typedef struct proxy_setting_s { + int protocol; // proxy_protocol_e + char proxy_host[256]; // proxy host (SOCKS5: unused, socket is the proxy) + 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); + // connect timeout => hclose_cb HV_EXPORT void hio_set_connect_timeout(hio_t* io, int timeout_ms DEFAULT(HIO_DEFAULT_CONNECT_TIMEOUT)); // close timeout => hclose_cb diff --git a/event/nio.c b/event/nio.c index 4980237f4..202e14519 100644 --- a/event/nio.c +++ b/event/nio.c @@ -6,6 +6,7 @@ #include "hlog.h" #include "herr.h" #include "hthread.h" +#include "socks5.h" static void __connect_timeout_cb(htimer_t* timer) { hio_t* io = (hio_t*)timer->privdata; @@ -196,6 +197,234 @@ static void nio_accept(hio_t* io) { // hio_close(io); } +// After the transport is connected (and, if using SOCKS5, after the proxy +// handshake completed), start the SSL handshake or deliver connect_cb. +static void nio_connect_established(hio_t* io) { + if (io->io_type == HIO_TYPE_SSL) { + if (io->ssl == NULL) { + // io->ssl_ctx > g_ssl_ctx > hssl_ctx_new + hssl_ctx_t ssl_ctx = NULL; + if (io->ssl_ctx) { + ssl_ctx = io->ssl_ctx; + } else if (g_ssl_ctx) { + ssl_ctx = g_ssl_ctx; + } else { + io->ssl_ctx = ssl_ctx = hssl_ctx_new(NULL); + io->alloced_ssl_ctx = 1; + } + if (ssl_ctx == NULL) { + io->error = ERR_NEW_SSL_CTX; + hio_close(io); + return; + } + hssl_t ssl = hssl_new(ssl_ctx, io->fd); + if (ssl == NULL) { + io->error = ERR_NEW_SSL; + hio_close(io); + return; + } + io->ssl = ssl; + } + // SNI: through a proxy the TLS peer is the target, so the proxy's + // target_host is authoritative; otherwise use the explicitly-set + // io->hostname. SNI must be a hostname, not an IP literal (RFC 6066), + // so a numeric candidate is skipped and the next one is considered. + const char* sni = NULL; + if (io->proxy && io->proxy->setting.target_host[0] && !is_ipaddr(io->proxy->setting.target_host)) { + sni = io->proxy->setting.target_host; + } else if (io->hostname && !is_ipaddr(io->hostname)) { + sni = io->hostname; + } + if (sni) { + hssl_set_sni_hostname(io->ssl, sni); + } + ssl_client_handshake(io); + } + else { + // NOTE: SSL call connect_cb after handshake finished + __connect_cb(io); + } +} + +// SOCKS5 client handshake state machine (RFC 1928 + RFC 1929). +// Driven via hio_add(io, socks5_handshake, HV_READ), exactly like +// ssl_client_handshake: it does raw recv() into an internal accumulator and +// does NOT touch io->read_cb (which the upper-layer Channel owns for delivering +// user data). Bytes are buffered in s5->rbuf until a full step is available, so +// the handshake is robust to TCP fragmentation. Runs before the optional SSL +// handshake. +enum socks5_state_e { + S5_RECV_METHOD = 0, // 2 bytes: VER METHOD + S5_RECV_AUTH, // 2 bytes: VER STATUS + S5_RECV_REPLY_HEAD, // 4 bytes: VER REP RSV ATYP + S5_RECV_REPLY_ADDR, // fixed addr+port (ipv4/ipv6) + S5_RECV_REPLY_DADDR, // 1 (dlen) already known: domain + port +}; + +static void socks5_handshake(hio_t* io); + +static void socks5_fail(hio_t* io) { + if (io->error == 0) io->error = ERR_CONNECT; + hlogw("connfd=%d socks5 handshake error", io->fd); + hio_close(io); +} + +// advance to a new state that needs `want` more bytes, resetting the buffer. +static void socks5_expect(hio_t* io, int state, int want) { + proxy_conn_t* s5 = io->proxy; + s5->state = state; + s5->rlen = 0; + s5->want = want; +} + +// Raw handshake send. The SOCKS5 handshake runs immediately after the TCP +// connection to the proxy is established, when the socket send buffer is empty +// and the messages are tiny (<= 513 bytes), so a short write is not expected. +// We deliberately do NOT use hio_write() here: it would invoke the upper-layer +// write_cb (leaking handshake bytes, including credentials, to the application +// before onConnection), dispatch to hssl_write() with a not-yet-created SSL +// handle for a TLS target, and enqueue on EAGAIN via hio_add() which would +// clobber the handshake read handler. A short write or error is treated as +// fatal and closes the connection. +static int socks5_send(hio_t* io, const void* buf, int len) { + int flag = 0; +#ifdef MSG_NOSIGNAL + flag |= MSG_NOSIGNAL; +#endif + int n = send(io->fd, (const char*)buf, len, flag); + return n == len ? 0 : -1; +} + +// send the SOCKS5 CONNECT request and wait for the 4-byte reply header. +static void socks5_send_connect(hio_t* io) { + proxy_conn_t* s5 = io->proxy; + unsigned char buf[300]; + int n = socks5_build_connect_request(s5, buf); + if (n < 0) { socks5_fail(io); return; } + if (socks5_send(io, buf, n) != 0) { socks5_fail(io); return; } + socks5_expect(io, S5_RECV_REPLY_HEAD, 4); +} + +// hand off the established proxy tunnel to the upper layer: stop the handshake +// read handler, then run the SSL handshake / connect_cb. io->read_cb was never +// touched, so the upper-layer Channel read callback stays intact. +static void socks5_established(hio_t* io) { + hio_del(io, HV_READ); + nio_connect_established(io); +} + +// process one accumulated step; s5->rbuf holds exactly s5->want bytes. +static void socks5_dispatch(hio_t* io) { + proxy_conn_t* s5 = io->proxy; + unsigned char* buf = s5->rbuf; + + switch (s5->state) { + case S5_RECV_METHOD: + // VER METHOD + if (buf[0] != SOCKS5_VERSION) { socks5_fail(io); return; } + if (buf[1] == SOCKS5_AUTH_NONE) { + socks5_send_connect(io); + } else if (buf[1] == SOCKS5_AUTH_USERPASS && s5->setting.username[0]) { + unsigned char req[640]; + int n = socks5_build_auth_request(s5, req); + if (socks5_send(io, req, n) != 0) { socks5_fail(io); return; } + socks5_expect(io, S5_RECV_AUTH, 2); + } else { + socks5_fail(io); // no acceptable method + } + return; + + case S5_RECV_AUTH: + // VER STATUS (0 == success) + if (buf[1] != 0x00) { socks5_fail(io); return; } + socks5_send_connect(io); + return; + + case S5_RECV_REPLY_HEAD: { + // VER REP RSV ATYP + if (buf[0] != SOCKS5_VERSION) { socks5_fail(io); return; } + if (buf[1] != SOCKS5_REP_SUCCESS) { io->error = ERR_CONNECT; socks5_fail(io); return; } + unsigned char atyp = buf[3]; + if (atyp == SOCKS5_ATYP_IPV4) { + socks5_expect(io, S5_RECV_REPLY_ADDR, 4 + 2); // addr + port + } else if (atyp == SOCKS5_ATYP_IPV6) { + socks5_expect(io, S5_RECV_REPLY_ADDR, 16 + 2); + } else if (atyp == SOCKS5_ATYP_DOMAIN) { + // read 1 length byte + then domain+port; do it in one extra step by + // first requiring the length byte. + socks5_expect(io, S5_RECV_REPLY_DADDR, 1); + } else { + socks5_fail(io); + } + return; + } + + case S5_RECV_REPLY_ADDR: + // bound addr+port consumed; tunnel is up + socks5_established(io); + return; + + case S5_RECV_REPLY_DADDR: + // first entry: we have the 1-byte domain length -> need dlen + 2 more. + // Re-enter with the full length once available. + if (s5->want == 1) { + int dlen = buf[0]; + socks5_expect(io, S5_RECV_REPLY_DADDR, dlen + 2); + return; + } + socks5_established(io); + return; + + default: + socks5_fail(io); + return; + } +} + +// hio_add read handler: accumulate into s5->rbuf until s5->want bytes are +// available, then dispatch. Never touches io->read_cb. +static void socks5_handshake(hio_t* io) { + proxy_conn_t* s5 = io->proxy; + while (s5->rlen < s5->want) { + int need = s5->want - s5->rlen; + if (s5->want > (int)sizeof(s5->rbuf)) { socks5_fail(io); return; } + int n = recv(io->fd, (char*)s5->rbuf + s5->rlen, need, 0); + if (n == 0) { socks5_fail(io); return; } // peer closed + if (n < 0) { + int err = socket_errno(); + if (err == EAGAIN || err == EINTR) return; // wait for more + io->error = err; + socks5_fail(io); + return; + } + s5->rlen += n; + } + socks5_dispatch(io); +} + +// Kick off the SOCKS5 handshake once the TCP connection to the proxy is up. +static void socks5_handshake_start(hio_t* io) { + proxy_conn_t* s5 = io->proxy; + unsigned char buf[8]; + 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); +} + +// Dispatch the proxy handshake by protocol (only SOCKS5 implemented so far). +static void proxy_handshake_start(hio_t* io) { + switch (io->proxy->setting.protocol) { + case PROXY_PROTOCOL_SOCKS5: + socks5_handshake_start(io); + return; + default: + io->error = ERR_INVALID_PARAM; + hio_close(io); + return; + } +} + static void nio_connect(hio_t* io) { // printd("nio_connect connfd=%d\n", io->fd); socklen_t addrlen = sizeof(sockaddr_u); @@ -208,39 +437,14 @@ static void nio_connect(hio_t* io) { addrlen = sizeof(sockaddr_u); getsockname(io->fd, io->localaddr, &addrlen); - if (io->io_type == HIO_TYPE_SSL) { - if (io->ssl == NULL) { - // io->ssl_ctx > g_ssl_ctx > hssl_ctx_new - hssl_ctx_t ssl_ctx = NULL; - if (io->ssl_ctx) { - ssl_ctx = io->ssl_ctx; - } else if (g_ssl_ctx) { - ssl_ctx = g_ssl_ctx; - } else { - io->ssl_ctx = ssl_ctx = hssl_ctx_new(NULL); - io->alloced_ssl_ctx = 1; - } - if (ssl_ctx == NULL) { - io->error = ERR_NEW_SSL_CTX; - goto connect_error; - } - hssl_t ssl = hssl_new(ssl_ctx, io->fd); - if (ssl == NULL) { - io->error = ERR_NEW_SSL; - goto connect_error; - } - io->ssl = ssl; - } - if (io->hostname) { - hssl_set_sni_hostname(io->ssl, io->hostname); - } - ssl_client_handshake(io); - } - else { - // NOTE: SSL call connect_cb after handshake finished - __connect_cb(io); + // Proxy: the TCP connection is to the proxy; run the proxy handshake + // (CONNECT to the real target) before SSL / connect_cb. + if (io->proxy) { + proxy_handshake_start(io); + return; } + nio_connect_established(io); return; } @@ -646,6 +850,7 @@ int hio_close (hio_t* io) { io->ssl_ctx = NULL; } SAFE_FREE(io->hostname); + SAFE_FREE(io->proxy); if (io->io_type & HIO_TYPE_SOCKET) { closesocket(io->fd); } else if (io->io_type == HIO_TYPE_PIPE) { diff --git a/event/socks5.c b/event/socks5.c new file mode 100644 index 000000000..c924e7b69 --- /dev/null +++ b/event/socks5.c @@ -0,0 +1,76 @@ +#include "socks5.h" + +#include + +#include "hsocket.h" // is_ipv4 / is_ipv6 / inet_pton via hplatform + +// Build the SOCKS5 method-selection request. +// +----+----------+----------+ +// |VER | NMETHODS | METHODS | +// +----+----------+----------+ +// Offers NONE, plus USERPASS when auth credentials are present. +// Returns the number of bytes written. +int socks5_build_method_request(const proxy_conn_t* s5, unsigned char* buf) { + int n = 0; + buf[n++] = SOCKS5_VERSION; + if (s5->setting.username[0]) { + buf[n++] = 2; // 2 methods + buf[n++] = SOCKS5_AUTH_NONE; + buf[n++] = SOCKS5_AUTH_USERPASS; + } else { + buf[n++] = 1; // 1 method + buf[n++] = SOCKS5_AUTH_NONE; + } + return n; +} + +// Build the RFC 1929 username/password auth request. +// +----+------+----------+------+----------+ +// |VER | ULEN | UNAME | PLEN | PASSWD | +// +----+------+----------+------+----------+ +int socks5_build_auth_request(const proxy_conn_t* s5, unsigned char* buf) { + int n = 0; + int ulen = (int)strlen(s5->setting.username); + int plen = (int)strlen(s5->setting.password); + buf[n++] = SOCKS5_AUTH_VERSION; + buf[n++] = (unsigned char)ulen; + memcpy(buf + n, s5->setting.username, ulen); n += ulen; + buf[n++] = (unsigned char)plen; + memcpy(buf + n, s5->setting.password, plen); n += plen; + return n; +} + +// Build a CONNECT request. +// +----+-----+-------+------+----------+----------+ +// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | +// +----+-----+-------+------+----------+----------+ +// An IPv4/IPv6 literal target is encoded as ATYP=1/4 (raw address bytes, per +// RFC 1928); anything else is sent as ATYP=domain so the proxy resolves it. +// Returns bytes written, or -1 on error (host too long). +int socks5_build_connect_request(const proxy_conn_t* s5, unsigned char* buf) { + int n = 0; + buf[n++] = SOCKS5_VERSION; + buf[n++] = SOCKS5_CMD_CONNECT; + buf[n++] = 0x00; // RSV + + const char* target_host = s5->setting.target_host; + struct in_addr addr4; + struct in6_addr addr6; + if (inet_pton(AF_INET, target_host, &addr4) == 1) { + buf[n++] = SOCKS5_ATYP_IPV4; + memcpy(buf + n, &addr4, 4); n += 4; + } else if (inet_pton(AF_INET6, target_host, &addr6) == 1) { + buf[n++] = SOCKS5_ATYP_IPV6; + memcpy(buf + n, &addr6, 16); n += 16; + } else { + int hlen = (int)strlen(target_host); + if (hlen <= 0 || hlen > 255) return -1; + buf[n++] = SOCKS5_ATYP_DOMAIN; + buf[n++] = (unsigned char)hlen; + memcpy(buf + n, target_host, hlen); n += hlen; + } + unsigned short port = (unsigned short)s5->setting.target_port; + buf[n++] = (unsigned char)((port >> 8) & 0xFF); + buf[n++] = (unsigned char)(port & 0xFF); + return n; +} diff --git a/event/socks5.h b/event/socks5.h new file mode 100644 index 000000000..9ca2a4c80 --- /dev/null +++ b/event/socks5.h @@ -0,0 +1,52 @@ +#ifndef HV_SOCKS5_H_ +#define HV_SOCKS5_H_ + +// Internal SOCKS5 client helpers (RFC 1928 + RFC 1929 username/password auth). +// +// NOTE: this is an internal header (not installed). The public API is +// proxy_setting_t + hio_set_proxy() in hloop.h. Used internally by +// hio_connect() to run the proxy handshake; see nio.c. + +#include "hloop.h" // proxy_setting_t + +#define SOCKS5_VERSION 0x05 +#define SOCKS5_AUTH_VERSION 0x01 // username/password auth subnegotiation + +// auth methods +#define SOCKS5_AUTH_NONE 0x00 +#define SOCKS5_AUTH_USERPASS 0x02 +#define SOCKS5_AUTH_NOACCEPT 0xFF + +// commands +#define SOCKS5_CMD_CONNECT 0x01 + +// address types +#define SOCKS5_ATYP_IPV4 0x01 +#define SOCKS5_ATYP_DOMAIN 0x03 +#define SOCKS5_ATYP_IPV6 0x04 + +// reply codes (0x00 = success) +#define SOCKS5_REP_SUCCESS 0x00 + +// Internal per-connection runtime state for the proxy handshake (held on +// hio_t). Not part of the public configuration. +typedef struct proxy_conn_s { + proxy_setting_t setting; // copied proxy config (target + auth) + int state; // socks5_state_e (see nio.c) + // handshake read accumulator: SOCKS5 replies may be fragmented across TCP + // segments, so bytes are buffered here until a full message is available. + unsigned char rbuf[300]; // max reply: 4 + 1 + 255 + 2 (domain bind) + int rlen; // bytes currently in rbuf + int want; // bytes needed to complete the current step +} proxy_conn_t; + +BEGIN_EXTERN_C + +// Build SOCKS5 handshake messages into buf; return bytes written (<0 on error). +int socks5_build_method_request (const proxy_conn_t* s5, unsigned char* buf); +int socks5_build_auth_request (const proxy_conn_t* s5, unsigned char* buf); +int socks5_build_connect_request(const proxy_conn_t* s5, unsigned char* buf); + +END_EXTERN_C + +#endif // HV_SOCKS5_H_ diff --git a/evpp/Channel.h b/evpp/Channel.h index 823226224..68681b8bd 100644 --- a/evpp/Channel.h +++ b/evpp/Channel.h @@ -286,6 +286,12 @@ class SocketChannel : public Channel { return hio_set_hostname(io_, hostname.c_str()); } + // Client-side proxy; see hio_set_proxy. + int setProxy(proxy_setting_t* setting) { + if (io_ == NULL) return -1; + return hio_set_proxy(io_, setting); + } + // timeout void setConnectTimeout(int timeout_ms) { if (io_ == NULL) return; diff --git a/evpp/TcpClient.h b/evpp/TcpClient.h index 31d1f6669..c4eafd802 100644 --- a/evpp/TcpClient.h +++ b/evpp/TcpClient.h @@ -28,6 +28,7 @@ class TcpClientEventLoopTmpl { tls_setting = NULL; reconn_setting = NULL; unpack_setting = NULL; + proxy_setting = NULL; reconn_timer_id = INVALID_TIMER_ID; dns_id = INVALID_DNS_ID; reconn_success_cnt_ = 0; @@ -40,6 +41,7 @@ class TcpClientEventLoopTmpl { HV_FREE(tls_setting); HV_FREE(reconn_setting); HV_FREE(unpack_setting); + HV_FREE(proxy_setting); } const EventLoopPtr& loop() { @@ -150,6 +152,9 @@ class TcpClientEventLoopTmpl { // NOTE: Unix Domain Socket targets (remote_port < 0) carry a filesystem // path in remote_host, not a hostname; remote_addr is already set by // createsocket(), so never run DNS on them. + // NOTE: with a proxy, remote_host/remote_port ARE the proxy (that is + // what createsocket connects to); the final target lives in + // proxy_setting. So this same DNS path resolves the proxy address. if (remote_port >= 0 && !remote_host.empty() && !is_ipaddr(remote_host.c_str())) { return startResolveThenConnect(); } @@ -233,6 +238,8 @@ class TcpClientEventLoopTmpl { int startConnectWithAddr() { loop_->assertInLoopThread(); + // NOTE: with a proxy, remote_addr/remote_host is the PROXY (that is what + // we connect to); the final target lives in proxy_setting. if (channel == NULL || channel->isClosed()) { int connfd = createsocket(&remote_addr.sa); if (connfd < 0) { @@ -246,6 +253,11 @@ class TcpClientEventLoopTmpl { if (connect_timeout) { channel->setConnectTimeout(connect_timeout); } + // Proxy: the socket connects to the proxy; the proxy handshake issues + // CONNECT to the target carried in proxy_setting. + if (proxy_setting) { + channel->setProxy(proxy_setting); + } if (tls) { channel->enableSSL(); if (tls_setting) { @@ -256,8 +268,11 @@ class TcpClientEventLoopTmpl { return ret; } } - if (!is_ipaddr(remote_host.c_str())) { - channel->setHostname(remote_host); + // SNI = the TLS peer. Through a proxy the TLS peer is the target + // (proxy_setting->target_host), otherwise it is remote_host. + const char* sni = proxy_setting ? proxy_setting->target_host : remote_host.c_str(); + if (sni && sni[0] && !is_ipaddr(sni)) { + channel->setHostname(sni); } } channel->onconnect = [this]() { @@ -353,6 +368,21 @@ class TcpClientEventLoopTmpl { connect_timeout = ms; } + // Route the connection through a proxy (SOCKS5). Create the client socket + // for the PROXY (createsocket(proxy_port, proxy_host)); this setting carries + // the final target the proxy should CONNECT to. The setting is copied; set + // username/password for auth (see proxy_setting_t). + void setProxy(proxy_setting_t* setting) { + if (setting == NULL) { + HV_FREE(proxy_setting); + return; + } + if (proxy_setting == NULL) { + HV_ALLOC_SIZEOF(proxy_setting); + } + *proxy_setting = *setting; + } + void setReconnect(reconn_setting_t* setting) { if (setting == NULL) { cancelReconnectTimer(); @@ -419,6 +449,8 @@ class TcpClientEventLoopTmpl { hssl_ctx_opt_t* tls_setting; reconn_setting_t* reconn_setting; unpack_setting_t* unpack_setting; + // client-side proxy (SOCKS5), applied in startConnectWithAddr + proxy_setting_t* proxy_setting; // Callback std::function onConnection; diff --git a/examples/BUILD.bazel b/examples/BUILD.bazel index b8ca983f8..6fcf69254 100644 --- a/examples/BUILD.bazel +++ b/examples/BUILD.bazel @@ -73,6 +73,12 @@ cc_binary( deps = ["//:hv"] ) +cc_binary( + name = "socks5_client_test", + srcs = ["socks5_client_test.c"], + deps = ["//:hv"] +) + cc_binary( name = "jsonrpc_client", srcs = ["jsonrpc/jsonrpc_client.c", "jsonrpc/cJSON.c"] + glob(["jsonrpc/*.h"]), @@ -220,6 +226,7 @@ filegroup( ":udp_echo_server", ":udp_proxy_server", ":socks5_proxy_server", + ":socks5_client_test", ":jsonrpc_client", ":jsonrpc_server", ] + select({ diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 45c02c2a9..7bac59ee5 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -12,6 +12,7 @@ list(APPEND EXAMPLES udp_echo_server udp_proxy_server socks5_proxy_server + socks5_client_test host multi-acceptor-processes multi-acceptor-threads @@ -61,6 +62,9 @@ target_link_libraries(udp_proxy_server ${HV_LIBRARIES}) add_executable(socks5_proxy_server socks5_proxy_server.c) target_link_libraries(socks5_proxy_server ${HV_LIBRARIES}) +add_executable(socks5_client_test socks5_client_test.c) +target_link_libraries(socks5_client_test ${HV_LIBRARIES}) + add_executable(host host.c) target_link_libraries(host ${HV_LIBRARIES}) diff --git a/examples/socks5_client_test.c b/examples/socks5_client_test.c new file mode 100644 index 000000000..3ad3875ec --- /dev/null +++ b/examples/socks5_client_test.c @@ -0,0 +1,76 @@ +/* + * TCP client via SOCKS5 proxy (pure C, io layer). + * + * Demonstrates routing a connection through a SOCKS5 proxy with hio_set_proxy. + * The socket is created for the PROXY address; the proxy issues a CONNECT to + * the target carried in proxy_setting_t (target host sent as a domain name so + * the proxy resolves it, or as ATYP=ipv4/ipv6 for a numeric literal). + * + * @build make examples + * @test # start libhv's own SOCKS5 proxy server as the proxy: + * bin/socks5_proxy_server 1080 + * # then connect to an echo server through it: + * bin/tcp_echo_server 1234 + * bin/socks5_client_test 127.0.0.1 1080 127.0.0.1 1234 + * + * @example bin/socks5_client_test [user] [pass] + */ + +#include "hloop.h" +#include "hbase.h" + +static void on_close(hio_t* io) { + printf("disconnected: connfd=%d error=%d\n", hio_fd(io), hio_error(io)); + hloop_stop(hevent_loop(io)); +} + +static void on_message(hio_t* io, void* buf, int len) { + printf("recv: %.*s", len, (char*)buf); +} + +static void on_connect(hio_t* io) { + printf("connected through socks5 proxy, send hello\n"); + hio_setcb_read(io, on_message); + hio_read(io); + hio_write(io, "hello via socks5\n", 17); +} + +int main(int argc, char** argv) { + if (argc < 5) { + printf("Usage: %s proxy_host proxy_port target_host target_port [user] [pass]\n", argv[0]); + return -1; + } + const char* proxy_host = argv[1]; + int proxy_port = atoi(argv[2]); + const char* target_host = argv[3]; + int target_port = atoi(argv[4]); + const char* user = argc > 5 ? argv[5] : NULL; + const char* pass = argc > 6 ? argv[6] : NULL; + + hloop_t* loop = hloop_new(HLOOP_FLAG_AUTO_FREE); + // create the socket for the PROXY; the proxy handshake then CONNECTs to the + // target carried in proxy_setting below. + hio_t* io = hio_create_socket(loop, proxy_host, proxy_port, HIO_TYPE_TCP, HIO_CLIENT_SIDE); + if (io == NULL) { + printf("create socket failed\n"); + return -1; + } + + // route through the SOCKS5 proxy: carry the target + optional auth + proxy_setting_t proxy; + memset(&proxy, 0, sizeof(proxy)); + proxy.protocol = PROXY_PROTOCOL_SOCKS5; + hv_strncpy(proxy.target_host, target_host, sizeof(proxy.target_host)); + proxy.target_port = target_port; + if (user) hv_strncpy(proxy.username, user, sizeof(proxy.username)); + if (pass) hv_strncpy(proxy.password, pass, sizeof(proxy.password)); + hio_set_proxy(io, &proxy); + + hio_setcb_connect(io, on_connect); + hio_setcb_close(io, on_close); + hio_connect(io); + + hloop_run(loop); + hloop_free(&loop); + return 0; +}