shadowsocks-libev源码分析

shadowsocks-libev源码分析

1.什么是shadowsocks?

shadowsocks-libev是一个轻量的socks5代理,可用于伪装ip访问服务器,底层基于libev事件驱动库,非常轻量高效

2.shadowsocks协议解析

类型 目标ip地址 目标端口 数据
长度(Byte) 1 可变 2 可变

ATYP: 协议类型

        * 0x01 ipv4的ip地址
        * 0x03 域名
        * 0x04 ipv6的ip地址

当目标服务器使用域名时

类型 域名长度 域名 目标端口 数据
长度(Byte) 1 1 2 2 可变

3.shadowsocks-libev核心文件

1
2
ss-server 	#服务端运行程序
ss-local #客户端运行文程序

4.shadowsocks-libev源码解析

shadowsocks-libev分为客户端和服务端,因此我们需要两份代码,一个客户端代码一个服务段代码。

4.1.ss-server(服务端)

1.我们先看shadowsocks-libev ss-server服务端程序开始监听客户端连接的程序代码段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
if (mode != UDP_ONLY) {
int num_listen_ctx = 0;
for (int i = 0; i < server_num; i++) {
const char *host = server_addr[i].host;
const char *port = server_addr[i].port ? server_addr[i].port : server_port;

if (plugin != NULL) {
host = plugin_host;
}

if (host && ss_is_ipv6addr(host))
LOGI("tcp server listening at [%s]:%s", host, port);
else
LOGI("tcp server listening at %s:%s", host ? host : "0.0.0.0", port);

// Bind to port
int listenfd;
listenfd = create_and_bind(host, port, mptcp);
if (listenfd == -1) {
continue;
}
if (listen(listenfd, SSMAXCONN) == -1) {
ERROR("listen()");
continue;
}
setfastopen(listenfd);
setnonblocking(listenfd);
listen_ctx_t *listen_ctx = &listen_ctx_list[i];

// Setup proxy context
listen_ctx->timeout = atoi(timeout);
listen_ctx->fd = listenfd;
listen_ctx->iface = iface;
listen_ctx->loop = loop;

ev_io_init(&listen_ctx->io, accept_cb, listenfd, EV_READ);
//开始监听客户端的连接,accept_cb为ss-server接受客户端请求连接的回调函数
ev_io_start(loop, &listen_ctx->io);

num_listen_ctx++;

if (plugin != NULL)
break;
}

if (num_listen_ctx == 0) {
FATAL("failed to listen on any address");
}
}

我们可以看到这里服务端绑定本机接口并开始监听,等待来自客户端的连接。

2.我们来看看客户端连接ss-server 后的回调函数accept_cb

1
2
3
4
5
6
7
8
9
    listen_ctx_t *listener = (listen_ctx_t *)w;
int serverfd = accept(listener->fd, NULL, NULL);
if (serverfd == -1) {
ERROR("accept");
return;
}
...
server_t *server = new_server(serverfd, listener);
ev_io_start(EV_A_ & server->recv_ctx->io);

这里回调函数accept_cb函数调用了我们常用的accept函数接收客户端的连接,得到和客户端数据交互的socket,并使用new_server函数创建了一个新的任务,开始等待接收客户端发来的数据

3.我们继续看new_server函数干了什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
static server_t *
new_server(int fd, listen_ctx_t *listener)
{
if (verbose) {
server_conn++;
LOGI("new connection from client, %d opened client connections", server_conn);
}

server_t *server;
server = ss_malloc(sizeof(server_t));

memset(server, 0, sizeof(server_t));

server->recv_ctx = ss_malloc(sizeof(server_ctx_t));
server->send_ctx = ss_malloc(sizeof(server_ctx_t));
server->buf = ss_malloc(sizeof(buffer_t));
memset(server->recv_ctx, 0, sizeof(server_ctx_t));
memset(server->send_ctx, 0, sizeof(server_ctx_t));
balloc(server->buf, SOCKET_BUF_SIZE);
server->fd = fd;
server->recv_ctx->server = server;
server->recv_ctx->connected = 0;
server->send_ctx->server = server;
server->send_ctx->connected = 0;
server->stage = STAGE_INIT;
server->frag = 0;
server->query = NULL;
server->listen_ctx = listener;
server->remote = NULL;

server->e_ctx = ss_malloc(sizeof(cipher_ctx_t));
server->d_ctx = ss_malloc(sizeof(cipher_ctx_t));
crypto->ctx_init(crypto->cipher, server->e_ctx, 1);
crypto->ctx_init(crypto->cipher, server->d_ctx, 0);

int timeout = max(MIN_TCP_IDLE_TIMEOUT, server->listen_ctx->timeout);
ev_io_init(&server->recv_ctx->io, server_recv_cb, fd, EV_READ);
ev_io_init(&server->send_ctx->io, server_send_cb, fd, EV_WRITE);
ev_timer_init(&server->recv_ctx->watcher, server_timeout_cb,
timeout, timeout);

cork_dllist_add(&connections, &server->entries);

return server;
}

这里可以看到new_server函数设置了接收数据后的回调函数server_recv_cb,和发送数据的处理函数server_send_cb

1
2
ev_io_init(&server->recv_ctx->io, server_recv_cb, fd, EV_READ);
ev_io_init(&server->send_ctx->io, server_send_cb, fd, EV_WRITE);

4.我们继续看server_recv_cb函数如何对数据进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
server_ctx_t *server_recv_ctx = (server_ctx_t *)w;
server_t *server = server_recv_ctx->server;
remote_t *remote = NULL;

buffer_t *buf = server->buf;

if (server->stage == STAGE_STREAM) {
remote = server->remote;
buf = remote->buf;

// Only timer the watcher if a valid connection is established
ev_timer_again(EV_A_ & server->recv_ctx->watcher);
}

ssize_t r = recv(server->fd, buf->data, SOCKET_BUF_SIZE, 0);

if (r == 0) {
// connection closed
close_and_free_remote(EV_A_ remote);
close_and_free_server(EV_A_ server);
return;
} else if (r == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// no data
// continue to wait for recv
return;
} else {
ERROR("server recv");
close_and_free_remote(EV_A_ remote);
close_and_free_server(EV_A_ server);
return;
}
}

// Ignore any new packet if the server is stopped
if (server->stage == STAGE_STOP) {
return;
}

tx += r;
buf->len = r;
//对数据包进行解密
int err = crypto->decrypt(buf, server->d_ctx, SOCKET_BUF_SIZE);

if (err == CRYPTO_ERROR) {
report_addr(server->fd, "authentication error");
stop_server(EV_A_ server);
return;
} else if (err == CRYPTO_NEED_MORE) {
if (server->stage != STAGE_STREAM) {
server->frag++;
}
return;
}
if (server->stage == STAGE_STREAM) {
int s = send(remote->fd, remote->buf->data, remote->buf->len, 0);
if (s == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// no data, wait for send
remote->buf->idx = 0;
ev_io_stop(EV_A_ & server_recv_ctx->io);
ev_io_start(EV_A_ & remote->send_ctx->io);
} else {
ERROR("server_recv_send");
close_and_free_remote(EV_A_ remote);
close_and_free_server(EV_A_ server);
}
} else if (s < remote->buf->len) {
remote->buf->len -= s;
remote->buf->idx = s;
ev_io_stop(EV_A_ & server_recv_ctx->io);
ev_io_start(EV_A_ & remote->send_ctx->io);
}
return;
} else if (server->stage == STAGE_INIT) {

这里我们可以看到首先程序先将数据读取到缓冲区

1
ssize_t r = recv(server->fd, buf->data, SOCKET_BUF_SIZE, 0);

其次对数据包进行了解密

1
int err = crypto->decrypt(buf, server->d_ctx, SOCKET_BUF_SIZE);

然后对数据包进行了初步分析,判断数据包的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
   if (server->stage == STAGE_STOP) {
return;
}
else if (server->stage == STAGE_INIT)
{
...
}
else if (server->stage == STAGE_STREAM)
{
...
if (!need_query) {
remote_t *remote = connect_to_remote(EV_A_ & info, server);
if (remote == NULL) {
LOGE("connect error");
close_and_free_server(EV_A_ server);
return;
} else {
server->remote = remote;
remote->server = server;

// XXX: should handle buffer carefully
if (server->buf->len > 0) {
brealloc(remote->buf, server->buf->len, SOCKET_BUF_SIZE);
memcpy(remote->buf->data, server->buf->data + server->buf->idx,
server->buf->len);
remote->buf->len = server->buf->len;
remote->buf->idx = 0;
server->buf->len = 0;
server->buf->idx = 0;
}

// waiting on remote connected event
ev_io_stop(EV_A_ & server_recv_ctx->io);
ev_io_start(EV_A_ & remote->send_ctx->io);
}
}

这里我们看到当数据包类型为STAGE_STREAM类型时,首先服务器和目标服务器建立了tcp连接

1
remote_t *remote = connect_to_remote(EV_A_ & info, server);

并且如果开始停止接收数据的任务,开始处理发送任务

1
2
3
// waiting on remote connected event
ev_io_stop(EV_A_ & server_recv_ctx->io);
ev_io_start(EV_A_ & remote->send_ctx->io);

5.我们再来看看server_send_cb函数做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
static void
server_send_cb(EV_P_ ev_io *w, int revents)
{
server_ctx_t *server_send_ctx = (server_ctx_t *)w;
server_t *server = server_send_ctx->server;
remote_t *remote = server->remote;

if (remote == NULL) {
LOGE("invalid server");
close_and_free_server(EV_A_ server);
return;
}

if (server->buf->len == 0) {
// close and free
close_and_free_remote(EV_A_ remote);
close_and_free_server(EV_A_ server);
return;
} else {
// has data to send
ssize_t s = send(server->fd, server->buf->data + server->buf->idx,
server->buf->len, 0);
if (s == -1) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
ERROR("server_send_send");
close_and_free_remote(EV_A_ remote);
close_and_free_server(EV_A_ server);
}
return;
} else if (s < server->buf->len) {
// partly sent, move memory, wait for the next time to send
server->buf->len -= s;
server->buf->idx += s;
return;
} else {
// all sent out, wait for reading
server->buf->len = 0;
server->buf->idx = 0;
ev_io_stop(EV_A_ & server_send_ctx->io);
if (remote != NULL) {
ev_io_start(EV_A_ & remote->recv_ctx->io);
return;
} else {
LOGE("invalid remote");
close_and_free_remote(EV_A_ remote);
close_and_free_server(EV_A_ server);
return;
}
}
}

这里程序对缓冲区的程序进行了发送

1
2
ssize_t s = send(server->fd, server->buf->data + server->buf->idx,
server->buf->len, 0);

并且开始停止发送任务,开始处理接收任务

1
2
3
4
ev_io_stop(EV_A_ & server_send_ctx->io);
if (remote != NULL) {
ev_io_start(EV_A_ & remote->recv_ctx->io);
return;

我们从以上分析结果得出shadowsocks服务器工作的核心函数有以下几个

4.1.1.核心函数
1
2
3
4
static void remote_recv_cb(EV_P_ ev_io *w, int revents); 	//和接收来自远程服务器发来的数据包
static void remote_send_cb(EV_P_ ev_io *w,int revents); //发送数据包给远程服务器
static void server_recv_cb(EV_P_ ev_io *w, int revents); //接收来自shadowsocks客户端发送来的数据包
static void server_send_cb(EV_P_ ev_io *w,int revents); //发送数据包给shadowsock客户端
4.1.2.函数调用关系

ss-server接收到客户端的数据,处理数据的函数调用关系图

1
server_recv_cb---->decrypto---send/remote_send_cb----->recv/remote_recv_cb------>crypto---->server_send_cb

其实从函数调用关系图不难得出整个ss-server的工作流程

  • 1.接收数据
  • 2.解密数据
  • 3.将数据发送给目标服务器
  • 4.接收来自远程服务器发送的数据包
  • 5.加密数据
  • 6.将服务器发送的数据包发送给客户端

4.2.ss-local(客户端)

4.2.1.核心函数
1
2
3
4
5
void server_recv_cb(EV_P_ ev_io *w, int revents);   //接收客户端发送数据后的回调函数
void server_send_cb(EV_P_ ev_io *w, int revents); //发送给客户端数据后的回调函数
void remote_recv_cb(EV_P_ ev_io *w, int revents); //接收到来自远程服务器数据后的回调函数
void remote_send_cb(EV_P_ ev_io *w, int revents); //发送给远程服务器数据后的回调函数
void server_stream(EV_P_ ev_io *w, buffer_t *buf);

1.先看主函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
if (mode != UDP_ONLY) {
// Setup socket
int listenfd;
#ifdef HAVE_LAUNCHD
listenfd = launch_or_create(local_addr, local_port);
#else
listenfd = create_and_bind(local_addr, local_port);
#endif
if (listenfd == -1) {
FATAL("bind() error");
}
if (listen(listenfd, SOMAXCONN) == -1) {
FATAL("listen() error");
}
setnonblocking(listenfd);

listen_ctx.fd = listenfd;

ev_io_init(&listen_ctx.io, accept_cb, listenfd, EV_READ);
ev_io_start(loop, &listen_ctx.io);
}
...

这里很明显可以看清楚,客户端首先创建了socket并绑定端口开始监听连接,从函数名我们很容易看出
accept_cb为客户端接收连接的回调函数
我们耿总accept_cb函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
listen_ctx_t *listener = (listen_ctx_t *)w;  
int serverfd = accept(listener->fd, NULL, NULL);
if (serverfd == -1) {
ERROR("accept");
return; }
setnonblocking(serverfd);
int opt = 1;
setsockopt(serverfd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt));
#ifdef SO_NOSIGPIPE
setsockopt(serverfd, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt));
#endif

if (tcp_incoming_sndbuf > 0) {
setsockopt(serverfd, SOL_SOCKET, SO_SNDBUF, &tcp_incoming_sndbuf, sizeof(int));
}

if (tcp_incoming_rcvbuf > 0) {
setsockopt(serverfd, SOL_SOCKET, SO_RCVBUF, &tcp_incoming_rcvbuf, sizeof(int));
}

server_t *server = new_server(serverfd);
server->listener = listener;

ev_io_start(EV_A_ & server->recv_ctx->io);

这里发现客户端接收连接后使用new_server函数创建了一个server对象,继续跟踪new_server函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
server_t *server;  
server = ss_malloc(sizeof(server_t));

memset(server, 0, sizeof(server_t));

server->recv_ctx = ss_malloc(sizeof(server_ctx_t));
server->send_ctx = ss_malloc(sizeof(server_ctx_t));
server->buf = ss_malloc(sizeof(buffer_t));
server->abuf = ss_malloc(sizeof(buffer_t));
balloc(server->buf, SOCKET_BUF_SIZE);
balloc(server->abuf, SOCKET_BUF_SIZE);
memset(server->recv_ctx, 0, sizeof(server_ctx_t));
memset(server->send_ctx, 0, sizeof(server_ctx_t));
server->stage = STAGE_INIT;
server->recv_ctx->connected = 0;
server->send_ctx->connected = 0;
server->fd = fd;
server->recv_ctx->server = server;
server->send_ctx->server = server;

server->e_ctx = ss_malloc(sizeof(cipher_ctx_t));
server->d_ctx = ss_malloc(sizeof(cipher_ctx_t));
crypto->ctx_init(crypto->cipher, server->e_ctx, 1);
crypto->ctx_init(crypto->cipher, server->d_ctx, 0);

ev_io_init(&server->recv_ctx->io, server_recv_cb, fd, EV_READ);
ev_io_init(&server->send_ctx->io, server_send_cb, fd, EV_WRITE);

ev_timer_init(&server->delayed_connect_watcher,
delayed_connect_cb, 0.05, 0);

cork_dllist_add(&connections, &server->entries);

这里发现server->recv_ctx绑定了函数server_recv_cb和server_send_cb,这个函数从名称上不难猜测分别是服务端接收信息后的回调函数和服务端发送信息后的回调函数。
分别跟踪server_recv_cb函数和server_send_cb函数
server_recv_cb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
if (revents != EV_TIMER) {  
r = recv(server->fd, buf->data + buf->len, SOCKET_BUF_SIZE - buf->len, 0);

if (r == 0) {
// connection closed
close_and_free_remote(EV_A_ remote);
close_and_free_server(EV_A_ server);
return; } else if (r == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// no data
// continue to wait for recv return;
} else {
if (verbose)
ERROR("server_recv_cb_recv");
close_and_free_remote(EV_A_ remote);
close_and_free_server(EV_A_ server);
return; }
}
buf->len += r;
}

while (1) {
// local socks5 server
if (server->stage == STAGE_STREAM) {
server_stream(EV_A_ w, buf);

// all processed
return;
} else if (server->stage == STAGE_INIT) {
.....

这里看到回调函数首先调用了recv函数用于接收请求,然后通过判断server->stage参数的值,看到当server->stage=STAGE_STREAM的时候,会调用server_stream函数,从这里不容易看出server_stream函数到底做了什么,我们跟踪一下server_stream函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
server_ctx_t *server_recv_ctx = (server_ctx_t *)w;  
server_t *server = server_recv_ctx->server;
remote_t *remote = server->remote;

if (remote == NULL) {
LOGE("invalid remote");
close_and_free_server(EV_A_ server);
return; }

// insert shadowsocks header
if (!remote->direct) {
#ifdef __ANDROID__
tx += remote->buf->len;
#endif
int err = crypto->encrypt(remote->buf, server->e_ctx, SOCKET_BUF_SIZE);

if (err) {
LOGE("invalid password or cipher");
close_and_free_remote(EV_A_ remote);
close_and_free_server(EV_A_ server);
return; }

if (server->abuf) {
bprepend(remote->buf, server->abuf, SOCKET_BUF_SIZE);
bfree(server->abuf);
ss_free(server->abuf);
server->abuf = NULL;
}
}

if (!remote->send_ctx->connected) {
#ifdef __ANDROID__
if (vpn) {
int not_protect = 0; if (remote->addr.ss_family == AF_INET) { struct sockaddr_in *s = (struct sockaddr_in *)&remote->addr; if (s->sin_addr.s_addr == inet_addr("127.0.0.1")) not_protect = 1; } if (!not_protect) { if (protect_socket(remote->fd) == -1) { ERROR("protect_socket"); close_and_free_remote(EV_A_ remote); close_and_free_server(EV_A_ server); return; } } }#endif

remote->buf->idx = 0;

if (!fast_open || remote->direct) {
// connecting, wait until connected
int r = connect(remote->fd, (struct sockaddr *)&(remote->addr), remote->addr_len);

if (r == -1 && errno != CONNECT_IN_PROGRESS) {
ERROR("connect");
close_and_free_remote(EV_A_ remote);
close_and_free_server(EV_A_ server);
return; }

// wait on remote connected event
ev_io_stop(EV_A_ & server_recv_ctx->io);
ev_io_start(EV_A_ & remote->send_ctx->io);
ev_timer_start(EV_A_ & remote->send_ctx->watcher);
....

这里看到,server_stream函数首先调用了encrypto函数将数据进行了加密,然后调用connect函数连接了远程shadowsocks服务器,然后暂时禁用了接收客户端数据后的处理任务,并启动一个任务等待将数据发送给远程服务器
跟踪完server_recv_cb函数我们来看server_send_cb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
server_ctx_t *server_send_ctx = (server_ctx_t *)w;  
server_t *server = server_send_ctx->server;
remote_t *remote = server->remote;
if (server->buf->len == 0) {
// close and free
close_and_free_remote(EV_A_ remote);
close_and_free_server(EV_A_ server);
return;} else {
// has data to send
ssize_t s = send(server->fd, server->buf->data + server->buf->idx,
server->buf->len, 0);
if (s == -1) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
ERROR("server_send_cb_send");
close_and_free_remote(EV_A_ remote);
close_and_free_server(EV_A_ server);
}
return;
} else if (s < (ssize_t)(server->buf->len)) {
// partly sent, move memory, wait for the next time to send
server->buf->len -= s;
server->buf->idx += s;
return; } else {
// all sent out, wait for reading
server->buf->len = 0;
server->buf->idx = 0;
ev_io_stop(EV_A_ & server_send_ctx->io);
ev_io_start(EV_A_ & remote->recv_ctx->io);
return;

这里发现server_send_cb函数首先调用了send函数将数据返回给客户端,然后停止给客户端发送数据,等待接收来自远程服务器发送来的数据

4.2.2.函数调用关系
1
server_recv_cb-->recv-->encrypto--->remote_send_cb--->remote_recv_cb--->decrypto--->server_send_cb

我们可以总结出整个shadowsock客户端ss-local在运行时做的事情

  • 1.通过socks5协议接收来自需要代理程序发来的socks5流量包,并解包解析里面的数据
  • 2.将实际的数据封装成shadowsocks包,并将其加密发送给远程shadowsocks服务器
  • 3.等待服务器响应数据包,由于接收来自服务器的相应包也是shadowsocks包,因此首先先对包进行解密
  • 4.将解密后的数据包重新封装成socks5数据包,并将数据包返回给客户端。

4.3.shadowsocks工作原理

经过源码分析和各个函数调用关系我们不难得出shadowsocksocks的工作原理

1
client---socks5--->ss-local---crypto----->shadowsocks---->ss-server--->decrypto--->server
  • 1.客户端将数据包用socks5协议重新封装,发送给ss-local,
  • 2.ss-local将数据包重新封装成shadowsock协议格式的数据包,
  • 3.ss-local将数据包加密后发送给ss-server,
  • 4.ss-server收到数据包后将数据包进行解密,按照shadowsock协议解析出实际的payload
  • 5.ss-server开始和目标服务器建立连接,并将数据包发送给目标服务器,
  • 6.目标服务器返回数据给ss-server
  • 7.将目标服务器响应的数据封装成shadowsocks协议
  • 8.ss-server将数据包加密,通过之前和ss-local建立连接将数据包返回给ss-local,
  • 9.ss-local将数据包解密,按照shadowsocks协议规则解析出原始响应报文
  • 10.ss-local将原始响应报文封装成socks5协议发挥个客户端

5.小结

整个过程如果不深究细节,分析shadowsocks原理还是非常简单的,核心点就在于网络编程和libev的理解,因此在分析shadowsocks-libev之前一定要先理解网络编程和libev事件驱动库的使用。


shadowsocks-libev源码分析
https://dreamaccount.github.io/2022/11/29/shadowsocks-libev源码分析/
作者
404NotFound
发布于
2022年11月29日
许可协议