Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -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"

Expand Down
123 changes: 123 additions & 0 deletions docs/cn/socks5.md
Original file line number Diff line number Diff line change
@@ -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
```
15 changes: 15 additions & 0 deletions event/hevent.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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;
Comment on lines +500 to +503
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;
Comment on lines +508 to +510
}

void hio_del_connect_timer(hio_t* io) {
if (io->connect_timer) {
htimer_del(io->connect_timer);
Expand Down
4 changes: 4 additions & 0 deletions event/hevent.h
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
45 changes: 45 additions & 0 deletions event/hloop.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines +370 to +390

// 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
Expand Down
Loading
Loading