【Mongoose笔记】socks5 服务器
简介
Mongoose 笔记系列用于记录学习 Mongoose 的一些内容。
Mongoose 是一个 C/C++ 的网络库。它为 TCP、UDP、HTTP、WebSocket、MQTT 实现了事件驱动的、非阻塞的 API。
项目地址:
https://github.com/cesanta/mongoose
学习
下面学习一下 Mongoose 项目代码中的 socks5-server 示例程序 ,这个示例程序是一个简易的 SOCKS5 代理服务器,只实现了SOCKS5 TCP 服务的一个子集,这个示例程序允许客户端通过这个SOCKS5 代理服务器与所需的 TCP 服务器交换数据,无需用户认证。
示例程序代码如下:
// Copyright (c) 2020 Cesanta Software Limited
// All rights reserved
//
// Example socks5 server. To test,
// 1. Run `make` to start this server on port 1080
// 2. Run `curl`#include "mongoose.h"static const char *s_lsn = "tcp://localhost:1080"; // Listening addressenum {VERSION = 5, // Socks protocol versionSTATE_HANDSHAKE = 0, // Connection state: in handshakeSTATE_REQUEST = 1, // Connection state: connectingSTATE_ESTABLISHED = 2, // Connection state: establishedHANDSHAKE_NOAUTH = 0, // Handshake method - no authenticationHANDSHAKE_GSSAPI = 1, // Handshake method - GSSAPI authHANDSHAKE_USERPASS = 2, // Handshake method - user/password authHANDSHAKE_FAILURE = 0xff, // Handshake method - failureCMD_CONNECT = 1, // Command: CONNECTCMD_BIND = 2, // Command: BINDCMD_UDP_ASSOCIATE = 3, // Command: UDP ASSOCIATEADDR_TYPE_IPV4 = 1, // Address type: IPv4ADDR_TYPE_DOMAIN = 3, // Address type: Domain nameADDR_TYPE_IPV6 = 4, // Address type: IPv6RESP_SUCCESS = 0, // Response: successRESP_FAILURE = 1, // Response: failureRESP_NOT_ALLOWED = 2, // Response statusRESP_NET_UNREACHABLE = 3, // Response statusRESP_HOST_UNREACHABLE = 4, // Response statusRESP_CONN_REFUSED = 5, // Response statusRESP_TTL_EXPIRED = 6, // Response statusRESP_CMD_NOT_SUPPORTED = 7, // Response statusRESP_ADDR_NOT_SUPPORTED = 8, // Response status
};// https://www.ietf.org/rfc/rfc1928.txt paragraph 3, handle client handshake
// +----+----------+----------+
// |VER | NMETHODS | METHODS |
// +----+----------+----------+
// | 1 | 1 | 1 to 255 |
// +----+----------+----------+
static void handshake(struct mg_connection *c) {struct mg_iobuf *r = &c->recv;if (r->buf[0] != VERSION) {c->is_closing = 1;} else if (r->len > 2 && (size_t) r->buf[1] + 2 <= r->len) {/* https://www.ietf.org/rfc/rfc1928.txt paragraph 3 */uint8_t reply[2] = {VERSION, HANDSHAKE_FAILURE};int i;for (i = 2; i < r->buf[1] + 2; i++) {// TODO(lsm): support other auth methodsif (r->buf[i] == HANDSHAKE_NOAUTH) reply[1] = r->buf[i];}mg_iobuf_del(r, 0, 2 + r->buf[1]);mg_send(c, reply, sizeof(reply));c->data[0] = STATE_REQUEST;}
}static void disband(struct mg_connection *c) {struct mg_connection *c2 = (struct mg_connection *) c->fn_data;if (c2 != NULL) {c2->is_draining = 1;c2->fn_data = NULL;}c->is_draining = 1;c->fn_data = NULL;
}static void exchange(struct mg_connection *c) {struct mg_connection *c2 = (struct mg_connection *) c->fn_data;if (c2 != NULL) {mg_send(c2, c->recv.buf, c->recv.len);mg_iobuf_del(&c->recv, 0, c->recv.len);} else {c->is_draining = 1;}
}static void fn2(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {if (ev == MG_EV_READ) {exchange(c);} else if (ev == MG_EV_CLOSE) {disband(c);}(void) ev_data;(void) fn_data;
}// Request, https://www.ietf.org/rfc/rfc1928.txt paragraph 4
// +----+-----+-------+------+----------+----------+
// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
// +----+-----+-------+------+----------+----------+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
// +----+-----+-------+------+----------+----------+
static void request(struct mg_connection *c) {struct mg_iobuf *r = &c->recv;uint8_t *p = r->buf, addr_len = 4, reply = RESP_SUCCESS;int ver, cmd, atyp;char addr[1024];if (r->len < 8) return; // return if not fully buffered. min DST.ADDR is 2ver = p[0];cmd = p[1];atyp = p[3];// TODO(lsm): support other commandsif (ver != VERSION || cmd != CMD_CONNECT) {reply = RESP_CMD_NOT_SUPPORTED;} else if (atyp == ADDR_TYPE_IPV4) {addr_len = 4;if (r->len < (size_t) addr_len + 6) return; /* return if not buffered */snprintf(addr, sizeof(addr), "tcp://%d.%d.%d.%d:%d", p[4], p[5], p[6], p[7],p[8] << 8 | p[9]);c->fn_data = mg_connect(c->mgr, addr, fn2, c);} else if (atyp == ADDR_TYPE_IPV6) {addr_len = 16;if (r->len < (size_t) addr_len + 6) return; /* return if not buffered */snprintf(addr, sizeof(addr), "tcp://[%x:%x:%x:%x:%x:%x:%x:%x]:%d",p[4] << 8 | p[5], p[6] << 8 | p[7], p[8] << 8 | p[9],p[10] << 8 | p[11], p[12] << 8 | p[13], p[14] << 8 | p[15],p[16] << 8 | p[17], p[18] << 8 | p[19], p[20] << 8 | p[21]);c->fn_data = mg_connect(c->mgr, addr, fn2, c);} else if (atyp == ADDR_TYPE_DOMAIN) {addr_len = p[4] + 1;if (r->len < (size_t) addr_len + 6) return; /* return if not buffered */snprintf(addr, sizeof(addr), "tcp://%.*s:%d", p[4], p + 5,p[4 + addr_len] << 8 | p[4 + addr_len + 1]);c->fn_data = mg_connect(c->mgr, addr, fn2, c);} else {reply = RESP_ADDR_NOT_SUPPORTED;}// Reply, https://www.ietf.org/rfc/rfc1928.txt paragraph 5//// +----+-----+-------+------+----------+----------+// |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |// +----+-----+-------+------+----------+----------+// | 1 | 1 | X'00' | 1 | Variable | 2 |// +----+-----+-------+------+----------+----------+{uint8_t buf[] = {VERSION, reply, 0};mg_send(c, buf, sizeof(buf));}mg_send(c, r->buf + 3, addr_len + 1 + 2);mg_iobuf_del(r, 0, 6 + addr_len); // Remove request from the input streamc->data[0] = STATE_ESTABLISHED; // Mark ourselves as connected
}static void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {if (ev == MG_EV_READ) {// We use the first label byte as a stateif (c->data[0] == STATE_HANDSHAKE) handshake(c);if (c->data[0] == STATE_REQUEST) request(c);if (c->data[0] == STATE_ESTABLISHED) exchange(c);} else if (ev == MG_EV_CLOSE) {disband(c);}(void) fn_data;(void) ev_data;
}int main(void) {struct mg_mgr mgr; // Event managermg_log_set(MG_LL_DEBUG); // Set log levelmg_mgr_init(&mgr); // Initialise event managermg_listen(&mgr, s_lsn, fn, NULL); // Create client connectionwhile (true) mg_mgr_poll(&mgr, 1000); // Infinite event loopmg_mgr_free(&mgr); // Free resourcesreturn 0;
}
下面从main
函数开始分析代码。
定义变量,struct mg_mgr
是用于保存所有活动连接的事件管理器。
struct mg_mgr mgr; // Event manager
设置 Mongoose 日志记录级别,设置等级为 MG_LL_DEBUG
。
mg_log_set(MG_LL_DEBUG); // Set log level
初始化一个事件管理器,也就是将上面定义的struct mg_mgr
变量 mgr
中的数据进行初始化。
mg_mgr_init(&mgr); // Initialise event manager
启动 SOCKS5 服务器,通过 mg_listen
函数创建一个监听连接,监听地址s_lsn
。其中fn
是事件处理函数。
mg_listen(&mgr, s_lsn, fn, NULL); // Create client connection
其中s_lsn
是一个全局静态变量,值为tcp://localhost:1080
。
SOCKS5 使用 TCP 传输,默认端口为 1080。
static const char *s_lsn = "tcp://localhost:1080"; // Listening address
然后是事件循环,mg_mgr_poll
遍历所有连接,接受新连接,发送和接收数据,关闭连接,并为各个事件调用事件处理函数。
while (true) mg_mgr_poll(&mgr, 1000); // Infinite event loop
调用 mg_mgr_free
关闭所有连接,释放所有资源。
mg_mgr_free(&mgr); // Free resources
分析完main
函数后,我们看下事件处理函数fn
的代码。
这个 SOCKS5 服务器的工作过程分为3个阶段,分别为握手(handshake),请求(request),交换(exchange)。当前的状态被记录在data
中。
判断是否收到MG_EV_READ
事件,当有从套接字socket接收到数据时,就会发送MG_EV_READ
事件。当接收到MG_EV_READ
事件后,判断当前的状态是处于什么阶段,就进入对应的状态处理函数中。
static void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {if (ev == MG_EV_READ) {// We use the first label byte as a stateif (c->data[0] == STATE_HANDSHAKE) handshake(c);if (c->data[0] == STATE_REQUEST) request(c);if (c->data[0] == STATE_ESTABLISHED) exchange(c);} else if (ev == MG_EV_CLOSE) {disband(c);}(void) fn_data;(void) ev_data;
}
首先看下握手阶段(Handshake phase)。
代码中的注释部分是客户端握手消息的格式。
// https://www.ietf.org/rfc/rfc1928.txt paragraph 3, handle client handshake
// +----+----------+----------+
// |VER | NMETHODS | METHODS |
// +----+----------+----------+
// | 1 | 1 | 1 to 255 |
// +----+----------+----------+
static void handshake(struct mg_connection *c) {
定义一个指针变量r
指向c->recv
IO 缓冲区,接收到的数据会放入其中。
struct mg_iobuf *r = &c->recv;
检测客户端的版本是否是 SOCKS5,如果不是则关闭连接。将c->is_closing
置 1 将立即关闭并释放连接。
if (r->buf[0] != VERSION) {c->is_closing = 1;}
判断接收到的数据长度是否满足要求,用于判断接收到的数据是否包含整个客户端的握手消息。其中r->len > 2
是因为握手消息的格式分为 3 个部分,所以长度一定会大于 2;然后r->buf[1]
是NMETHODS
字段,表示METHODS
字段的数量,再加上VER
和NMETHODS
这两个字段的长度各为 1 ,所以整个握手包的长度为r->buf[1] + 2
,因此完整的握手消息需要r->buf[1] + 2 <= r->len
。
} else if (r->len > 2 && (size_t) r->buf[1] + 2 <= r->len) {
定义应答消息。
/* https://www.ietf.org/rfc/rfc1928.txt paragraph 3 */uint8_t reply[2] = {VERSION, HANDSHAKE_FAILURE};
应答消息的格式如下:
+----+--------+|VER | METHOD |+----+--------+| 1 | 1 |+----+--------+
遍历握手消息,寻找支持的认证方法。这个程序目前只支持无需认证这个一种方法。
int i;for (i = 2; i < r->buf[1] + 2; i++) {// TODO(lsm): support other auth methodsif (r->buf[i] == HANDSHAKE_NOAUTH) reply[1] = r->buf[i];}
调用mg_iobuf_del
函数清除已处理过的接收数据。mg_iobuf_del
函数用于删除从 offset
开始的 len
个字节,并移动剩余的字节。
mg_iobuf_del(r, 0, 2 + r->buf[1]);
将要回复应答的数据发送出去。mg_send
函数将把size
字节的data
数据追加到输出缓冲区,以便稍后由事件管理器发送。
mg_send(c, reply, sizeof(reply));
最后修改状态,表示进入请求阶段。
c->data[0] = STATE_REQUEST;}
}
接下来看下请求阶段(Request phase)。
代码中的注释部分是客户端请求消息的格式。
// Request, https://www.ietf.org/rfc/rfc1928.txt paragraph 4
// +----+-----+-------+------+----------+----------+
// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
// +----+-----+-------+------+----------+----------+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
// +----+-----+-------+------+----------+----------+
static void request(struct mg_connection *c) {
定义了一些变量。其中定义了一个指针变量r
指向c->recv
IO 缓冲区,接收到的数据会放入其中。
struct mg_iobuf *r = &c->recv;uint8_t *p = r->buf, addr_len = 4, reply = RESP_SUCCESS;int ver, cmd, atyp;char addr[1024];
判断接收到的数据长度是否小于 8,完整的数据包需要大于等于 8。
if (r->len < 8) return; // return if not fully buffered. min DST.ADDR is 2
给变量赋值,分别代表版本,命令和地址类型。
ver = p[0];cmd = p[1];atyp = p[3];
判断客户端的版本是否是 SOCKS5,是否是连接请求,如果不是则回复表示命令不支持。
这个程序目前只支持连接请求这一种请求。
// TODO(lsm): support other commandsif (ver != VERSION || cmd != CMD_CONNECT) {reply = RESP_CMD_NOT_SUPPORTED;}
判断地址类型是否是 IP V4。如果是则地址长度为 4,然后判断接收到的数据长度是否是一个完整数据包,如果不是则返回。接着将 IP 地址与端口号格式化为指定格式放入addr
中。最后调用mg_connect
创建连接,fn2
是事件处理函数。
} else if (atyp == ADDR_TYPE_IPV4) {addr_len = 4;if (r->len < (size_t) addr_len + 6) return; /* return if not buffered */snprintf(addr, sizeof(addr), "tcp://%d.%d.%d.%d:%d", p[4], p[5], p[6], p[7],p[8] << 8 | p[9]);c->fn_data = mg_connect(c->mgr, addr, fn2, c);}
判断地址类型是否是 IP V6。如果是则地址长度为 16,然后判断接收到的数据长度是否是一个完整数据包,如果不是则返回。接着将 IP 地址与端口号格式化为指定格式放入addr
中。最后调用mg_connect
创建连接。
} else if (atyp == ADDR_TYPE_IPV6) {addr_len = 16;if (r->len < (size_t) addr_len + 6) return; /* return if not buffered */snprintf(addr, sizeof(addr), "tcp://[%x:%x:%x:%x:%x:%x:%x:%x]:%d",p[4] << 8 | p[5], p[6] << 8 | p[7], p[8] << 8 | p[9],p[10] << 8 | p[11], p[12] << 8 | p[13], p[14] << 8 | p[15],p[16] << 8 | p[17], p[18] << 8 | p[19], p[20] << 8 | p[21]);c->fn_data = mg_connect(c->mgr, addr, fn2, c);}
判断地址类型是否是域名。如果是则设置地址长度为p[4] + 1
,地址类型为域名时,DST.ADDR
字段的第一字节是长度域,然后判断接收到的数据长度是否是一个完整数据包,如果不是则返回。接着将域名与端口号格式化为指定格式放入addr
中。最后调用mg_connect
创建连接。
} else if (atyp == ADDR_TYPE_DOMAIN) {addr_len = p[4] + 1;if (r->len < (size_t) addr_len + 6) return; /* return if not buffered */snprintf(addr, sizeof(addr), "tcp://%.*s:%d", p[4], p + 5,p[4 + addr_len] << 8 | p[4 + addr_len + 1]);c->fn_data = mg_connect(c->mgr, addr, fn2, c);}
其他的地址类型不支持。
} else {reply = RESP_ADDR_NOT_SUPPORTED;}
代码中的注释部分是应答消息的格式。
填入应答数据的前 3 个字段,使用mg_send
函数将数据放入发送缓冲区。
// Reply, https://www.ietf.org/rfc/rfc1928.txt paragraph 5//// +----+-----+-------+------+----------+----------+// |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |// +----+-----+-------+------+----------+----------+// | 1 | 1 | X'00' | 1 | Variable | 2 |// +----+-----+-------+------+----------+----------+{uint8_t buf[] = {VERSION, reply, 0};mg_send(c, buf, sizeof(buf));}
将应答消息的剩余部分放入发送缓冲区,并调用mg_iobuf_del
清除已处理过的接收数据。最后修改状态,表示进入交换阶段。
mg_send(c, r->buf + 3, addr_len + 1 + 2);mg_iobuf_del(r, 0, 6 + addr_len); // Remove request from the input streamc->data[0] = STATE_ESTABLISHED; // Mark ourselves as connected
}
接下来看下交换阶段(Exchange phase)。交换阶段要做的事就是将来自客户端的数据转发到服务器,将来自服务器的数据转发到客户端。这里通过exchange
函数来实现该功能。
如果c->fn_data
不为空,将收到的数据发送到另外一端,并在接收缓冲区清除这段数据。否则就将is_draining
置位,这样会发送剩余数据,然后关闭并释放。
static void exchange(struct mg_connection *c) {struct mg_connection *c2 = (struct mg_connection *) c->fn_data;if (c2 != NULL) {mg_send(c2, c->recv.buf, c->recv.len);mg_iobuf_del(&c->recv, 0, c->recv.len);} else {c->is_draining = 1;}
}
示例程序中一共有两处调用exchange
函数,一个是fn
函数中,用于将来自客户端的数据转发到服务器。
if (c->data[0] == STATE_ESTABLISHED) exchange(c);
另一个是在请求阶段时调用mg_connect
函数创建连接的事件处理函数fn2
。用于将接收到来自服务器的数据转发到客户端。
static void fn2(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {if (ev == MG_EV_READ) {exchange(c);} else if (ev == MG_EV_CLOSE) {disband(c);}(void) ev_data;(void) fn_data;
}
在fn
和fn2
函数中,如果收到MG_EV_CLOSE
事件,表示连接关闭,调用disband
函数进行关闭处理。
} else if (ev == MG_EV_CLOSE) {disband(c);}
将两边连接的is_draining
置位,刷新缓冲区然后关闭并释放,然后将fn_data
置NULL
。
static void disband(struct mg_connection *c) {struct mg_connection *c2 = (struct mg_connection *) c->fn_data;if (c2 != NULL) {c2->is_draining = 1;c2->fn_data = NULL;}c->is_draining = 1;c->fn_data = NULL;
}
socks5-server 的示例程序代码就都解析完了,下面实际运行一下 socks5-server 程序进行测试验证。
打开示例程序,编译并运行:
pi@raspberrypi:~ $ cd Desktop/study/mongoose/examples/socks5-server/
pi@raspberrypi:~/Desktop/study/mongoose/examples/socks5-server $ make
cc ../../mongoose.c -I../.. -W -Wall -DMG_ENABLE_LINES -o example main.c
./example
137183022 3 net.c:182:mg_listen 1 0x4 tcp://localhost:1080
使用 curl 命令行工具来进行测试,指定 HTTP 请求通过localhost:1080
的 socks5 代理发出。
pi@raspberrypi:~ $ curl -x socks5h://localhost:1080 -k -X GET http://www.baidu.com
<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=http://s1.bdstatic.com/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn"></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=http://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');</script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>©2017 Baidu <a href=http://www.baidu.com/duty/>使用百度前必读</a> <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a> 京ICP证030173号 <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>
看下我们的 SOCKS5 代理服务器的日志:
pi@raspberrypi:~ $ cd Desktop/study/mongoose/examples/socks5-server/
pi@raspberrypi:~/Desktop/study/mongoose/examples/socks5-server $ make
cc ../../mongoose.c -I../.. -W -Wall -DMG_ENABLE_LINES -o example main.c
./example
137183022 3 net.c:182:mg_listen 1 0x4 tcp://localhost:1080
137185e28 3 sock.c:417:accept_conn 2 0x5 accepted 127.0.0.1.63234 -> 127.0.0.1.1080
137185e29 3 sock.c:273:read_conn 2 0x5 snd 0/0 rcv 0/2048 n=4 err=0
137185e29 3 sock.c:284:write_conn 2 0x5 snd 2/2048 rcv 0/2048 n=2 err=0
137185e29 3 sock.c:273:read_conn 2 0x5 snd 0/2048 rcv 0/2048 n=20 err=0
137185e29 3 net.c:159:mg_connect 3 0xffffffffffffffff tcp://www.baidu.com:80
137185e29 3 net.c:159:mg_connect 4 0xffffffffffffffff udp://8.8.8.8:53
137185e2a 3 sock.c:146:mg_send 4 0x6 0:0 31 err 0
137185e2a 3 sock.c:284:write_conn 2 0x5 snd 20/2048 rcv 0/2048 n=20 err=0
137185e2a 3 sock.c:273:read_conn 2 0x5 snd 0/2048 rcv 0/2048 n=77 err=0
137185e57 3 sock.c:273:read_conn 4 0x6 snd 0/0 rcv 0/2048 n=90 err=9
137185e58 3 dns.c:165:dns_cb 3 www.a.shifen.com is 14.215.177.38
137185e58 3 sock.c:361:mg_connect_resol 3 0x7 -> 14.215.177.38:80 pend
137185e69 3 sock.c:284:write_conn 3 0x7 snd 77/2048 rcv 0/0 n=77 err=115
137185e7f 3 sock.c:273:read_conn 3 0x7 snd 0/2048 rcv 0/2048 n=2048 err=115
137185e7f 3 sock.c:273:read_conn 3 0x7 snd 0/2048 rcv 0/2048 n=733 err=115
137185e7f 3 sock.c:284:write_conn 2 0x5 snd 2781/4096 rcv 0/2048 n=2781 err=115
137185e82 3 sock.c:273:read_conn 2 0x5 snd 0/4096 rcv 0/2048 n=-1 err=115
137185e83 3 net.c:136:mg_close_conn 2 0x5 closed
13718626b 3 net.c:136:mg_close_conn 3 0x7 closed
【参考资料】
examples/socks5-server
Documentation
rfc1928
本文链接:https://blog.csdn.net/u012028275/article/details/129769177