ss-libev 源码解析local篇(2):ss_local和socks5客户端握手
来源:互联网 发布:json字符串转化成数组 编辑:程序博客网 时间:2024/06/05 20:10
上一篇说到ss-libev创建listen_ctx_t对象用于监听客户端连接,调用accept_cb处理来自客户端的新连接,创建server_t对象用于处理和客户端之间的交互。本篇分析来自客户端的SOCK5连接的建立以及传输数据的过程。
首先,回忆一下使用new_server()函数创建server_t对象时,注册了客户端连接的读写事件的回调:
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);
即,对于客户端和ss-local server之间的TCP连接,当local server的accepted fd上有可读事件时,即客户端发送数据过来了,server_recv_cb被调用;当local server的accepted fd上有可写事件时,即可以向客户端发送数据时(一般是准备发送数据时start,当写缓冲可用时触发),server_send_cb被调用。这是libev异步io的用法,具体可以查询libev。注意这两句只是注册了事件回调,还没有启用监听。在accept_cb里面,new_server之后,随即调用了 ev_io_start(EV_A_ & server->recv_ctx->io); 启用了server->recv_ctx->io上面注册的事件即读事件的监听,当客户端有数据发过来时,server_recv_cb即被调用。而server->send_ctx->io的监听暂时还没启用,因为此刻还不可能有数据可写,要等到远程服务器返回转发的数据之后才会启用监听。好了,先分析server_recv_cb。
static void server_recv_cb(EV_P_ ev_io *w, int revents) 分析
几个数据结构
- server_ctx_t
typedef struct server_ctx { ev_io io; int connected; struct server *server;} server_ctx_t;
- server_t
typedef struct server { int fd; int stage; cipher_ctx_t *e_ctx; cipher_ctx_t *d_ctx; struct server_ctx *recv_ctx; struct server_ctx *send_ctx; struct listen_ctx *listener; struct remote *remote; buffer_t *buf; buffer_t *abuf; ev_timer delayed_connect_watcher; struct cork_dllist_item entries;} server_t;
- remote_t
typedef struct remote { int fd; int direct; int addr_len; uint32_t counter; buffer_t *buf; struct remote_ctx *recv_ctx; struct remote_ctx *send_ctx; struct server *server; struct sockaddr_storage addr;} remote_t;
- buffer_t
typedef struct buffer { size_t idx; size_t len; size_t capacity; char *data;} buffer_t;
其中 server_t 上篇已经说过,客户端每来一个新的TCP请求,都会生成一个server_t,可以认为server_t描述了客户端和ss-local server的交互状态。server_t包含了server_ctx和buffer_t对象,并引用了remote对象。server_ctx_t用来具体处理客户端和local server之间的数据读写,因此server中包含了两个server_ctx:recv_ctx和send_ctx。server_ctx_t的结构上篇也说了,主要包含ev_io对象,以及connected标志,并且还指向了他所属于的server。remote是用于描述ss-local server和远程ss-server之间的交互,暂且不说。buffer_t是一个缓冲区结构,server包含两个缓冲区,分别是buf和abuf。另外server还有一个stage标志,用于描述自身所处的状态,在new_server里面被初始化为STAGE_INIT。所有的状态描述如下:
#define STAGE_ERROR -1 /* Error detected */#define STAGE_INIT 0 /* Initial stage */#define STAGE_HANDSHAKE 1 /* Handshake with client */#define STAGE_PARSE 2 /* Parse the header */#define STAGE_RESOLVE 4 /* Resolve the hostname */#define STAGE_WAIT 5 /* Wait for more data */#define STAGE_STREAM 6 /* Stream between client and server */
参数解析
libev的io回调函数,会把io对象传入。而server_recv_cb实际需要处理server等对象。在函数开头,利用结构体里面的指针位置,解出需要的指针:
server_ctx_t *server_recv_ctx = (server_ctx_t *)w;server_t *server = server_recv_ctx->server;remote_t *remote = server->remote;
随后,根据是否已经创建remote,判断使用哪个buf
if (remote == NULL) { buf = server->buf;} else { buf = remote->buf;}
如果已经创建了remote,则使用remote的buf,将数据读入其中,后面会看到这是因为此时已经是要传送具体的数据了,所以直接把数据读取到remote的buf中。
读取数据
既然server_recv_cb是可读事件的回调,所以被调用时就可以读取数据了。但这儿要注意的是server的stage,如果是STAGE_WAIT则不读取,这个wait后面再说。
r = recv(server->fd, buf->data + buf->len, BUF_SIZE - buf->len, 0);
r是成功读取到的数据的字节数,如果为0,表示读取到了一个EOF,即连接已经close,没有数据可读了。因为这儿是读取从客户端发送过来的数据,因此是客户端主动断开了连接。此时ss-local要做的是关闭和释放server和remote,本次代理交互结束。
if (r == 0) { // connection closed close_and_free_remote(EV_A_ remote); close_and_free_server(EV_A_ server); return; }
如果r==-1,则有可能是出错了,也需要结束本次代理,但因为是非阻塞io,如果errno == EAGAIN 或 errno == EWOULDBLOCK,则表示现在没有数据需要再试。如果r>0,则表示读取到了r字节数据,记录在buf的len中,并继续执行。
buf->len += r;
SOCKS5 方法选择 (in STAGE_INIT)
上文提到,一个新的server_t,stage初始化为STAGE_INIT,因此在server_recv_cb中,会先处理这个状态。在STAGE_INIT中,处理SOCK5握手的第一个请求,即method select请求,按照SOCK5规范,客户端先向SOCK5服务端查询支持的认证方式,最常用的是匿名和用户名密码认证,客户端在请求头里面将他希望使用的认证方式都列出来,服务端选择他要用的方式并返回响应。具体可参阅SOKC5 RFC1928。因为ss-local是在本地运行的,其实认证方式并没有太大的意义,所以无论客户端请求什么样的方式,ss-local都只返回匿名认证,也就是不认证直接通过,这样就没有后续的验证环节了。
struct method_select_response response;response.ver = SVERSION;response.method = 0;char *send_buf = (char *)&response;send(server->fd, send_buf, sizeof(response), 0);server->stage = STAGE_HANDSHAKE;
如这段代码所示,ss-local直接给客户端发送了一个0X0500,表示我这是匿名认证你来握手吧,然后将server的stage设置为STAGE_HANDSHAKE。
发送method select reponse之后,有个tcp粘包处理,因为tcp是流协议,没有消息边界,一次recv出来的数据可能超过了本条消息的长度,因此ss-local在这儿做了个处理:
if (method->ver == SVERSION && method_len < (int)(buf->len)) { memmove(buf->data, buf->data + method_len , buf->len - method_len); buf->len -= method_len; continue;}
此处method_len为SOCKS5方法选择请求的消息长度,根据方法数而变化,如果方法数为1,则长度为3,方法数为2,则长度为4,即长度为方法数+2。如果读取到的数据超过消息长度,则把超出的部分数据移动到buf的前端,并把buf的len设置为剩余数据的长度。这样相当于清除掉了已经处理过的方法选择消息,保留了多读取到的内容。不过我认为这种情况应该是不会发生的,因为SOCKS5握手阶段的消息都是一应一答,如果服务端不返回method select response,客户端应该不会进一步发送其他消息。TCP粘包一般发生在连续发送数据时。可以认为ss-local的处理比较严谨,可以看成是一种防御性编程。上面没提到的是,在处理方法选择请求时,ss-local同样处理了断包的情况,即如果收到的数据长度不满足消息的长度则直接返回。因为上面列出的数据读取代码,总是将数据添加到buf->data + buf->len处,所以断包的情况自然能处理好。不过ss-local只是对请求的长度进行检查,实际并不检测其内容,所以客户端只要发一个长度为3的任意包,也能通过这一阶段,进入下面的握手。
SOCKS5握手(in STAGE_HANDSHAKE)
SOCK5匿名认证成功之后,客户端就可以发送具体的请求细节了,ss-local称之为握手阶段。具体就是客户端发送一个SOCKS5请求,粘一段RFC的内容:
+----+-----+-------+------+----------+----------+ |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | +----+-----+-------+------+----------+----------+ | 1 | 1 | X'00' | 1 | Variable | 2 | +----+-----+-------+------+----------+----------+ Where: o VER protocol version: X'05' o CMD o CONNECT X'01' o BIND X'02' o UDP ASSOCIATE X'03' o RSV RESERVED o ATYP address type of following address o IP V4 address: X'01' o DOMAINNAME: X'03' o IP V6 address: X'04' o DST.ADDR desired destination address o DST.PORT desired destination port in network octet order
其中cmd为1是tcp转发请求,3是udp联合转发请求,ss-local只支持这两个cmd。
先来看代码:
else if (server->stage == STAGE_HANDSHAKE || server->stage == STAGE_PARSE) { struct socks5_request *request = (struct socks5_request *)buf->data; size_t request_len = sizeof(struct socks5_request); struct sockaddr_in sock_addr; memset(&sock_addr, 0, sizeof(sock_addr)); if (buf->len < request_len) { return; }
在server的stage为STAGE_HANDSHAKE或STAGE_PARSE时,将读入的数据视为socks5 request处理,STAGE_PARSE后面再说,此处会判断读取的buf是否满足request的长度,如果不满足就返回等待数据继续读入。注意这儿定义了一个sockaddr_in对象sock_addr并设置为0,这个sock_addr会在socks5_response中返回给客户端。继续往后看,如果request->cmd为3,则会在sock_addr中填充udp转发的监听地址和端口:
if (request->cmd == 3) { udp_assc = 1; socklen_t addr_len = sizeof(sock_addr); getsockname(udp_fd, (struct sockaddr *)&sock_addr, &addr_len); if (verbose) { LOGI("udp assc request accepted"); } }
这个udp_fd,就是上一篇中说的init_udprelay返回的fd。如果cmd是1,则sock_addr则不会有任何处理,保持0。如果cmd不是3也不是1,则ss-local会返回一个表示cmd不支持的response,其rep字段填0x07表示不支持cmd,并关闭这个代理连接。如果不是不支持,则代码继续往下,走到Fake reply。
// Fake reply if (server->stage == STAGE_HANDSHAKE) { struct socks5_response response; response.ver = SVERSION; response.rep = 0; response.rsv = 0; response.atyp = 1; buffer_t resp_to_send; buffer_t *resp_buf = &resp_to_send; balloc(resp_buf, BUF_SIZE); memcpy(resp_buf->data, &response, sizeof(struct socks5_response)); memcpy(resp_buf->data + sizeof(struct socks5_response), &sock_addr.sin_addr, sizeof(sock_addr.sin_addr)); memcpy(resp_buf->data + sizeof(struct socks5_response) + sizeof(sock_addr.sin_addr), &sock_addr.sin_port, sizeof(sock_addr.sin_port)); int reply_size = sizeof(struct socks5_response) + sizeof(sock_addr.sin_addr) + sizeof(sock_addr.sin_port); int s = send(server->fd, resp_buf->data, reply_size, 0); bfree(resp_buf); if (s < reply_size) { LOGE("failed to send fake reply"); close_and_free_remote(EV_A_ remote); close_and_free_server(EV_A_ server); return; } if (udp_assc) { close_and_free_remote(EV_A_ remote); close_and_free_server(EV_A_ server); return; } }
为啥叫fake reply呢,因为按照正常流程,客户端发过来的请求包含了域名或者ip地址,socks5服务端需要去实际连接目的服务器才知道是否能代理,如果不能代理,比如根本访问不了,则会在socks5 response中的rep中填入相应的错误码,如0x04 Host unreachable。而ss-local的处理是直接认为可以访问,返回可成功代理的response,也就是0x0500 0001加后上sock_addr中的地址和端口号,sock_addr是socks5服务器访问目的地址使用的ip地址和端口。对于tcp的情况,这个地址和端口号对于客户端没什么用,可以全为0,并且这儿是fake replay,此时连接没有建立,所以只能填0。且通过代码看,sock_addr确实初始化为0了,所以这儿填的全是0。如果是udp,则不全为0了,最后两位端口号是真实的端口号,即udp_fd对应的端口号。ss-local处理udp转发的方式是建立一个全局的udp监听端口,在fake replay的时候直接把这个端口发给客户端,客户端就可以往此端口发送数据了。注意在fake reply最后,如果是udp_assc的情况,直接把tcp连接断开了,这点不符合SOCKS5规范,不过在最新版中已经改掉了。在SOCKS5规范中,如果udp assc的tcp断开,客户端会认为udp端口不再可用,需要重新请求。
正常来说,发送fake reply之后,握手就结束了,不过STAGE_HANDSHAKE中还有一些处理,为避免本篇太长,留下篇再说。
- ss-libev 源码解析local篇(2):ss_local和socks5客户端握手
- ss-libev 源码解析local篇(1): ss_local的启动,客户端连入
- ss-libev 源码解析local篇(4): server_recv_cb之STAGE_STREAM
- ss-libev 源码解析local篇(3): server_recv_cb之SNI和STAGE_PARSE
- ss-libev 源码解析local篇(5):ss-local之remote_send_cb
- ss-libev 源码解析udp篇 (2)
- ss-libev 源码解析udp篇 (1)
- ss-libev 源码解析udp篇 (3)
- ss-libev 源码解析udp篇 (4)
- Libev源码解析
- libev源码解析——定时器监视器和组织形式
- vultr+ss-libev+hiwifi
- libev源码解析——总览
- libev源码解析——调度策略
- libev源码解析——定时器原理
- libev源码解析——监视器(watcher)结构和组织形式
- libev源码解析——I/O模型
- socks5解析地址
- java冒泡排序
- C++ 二级指针
- 谢谢关注,烦请移步至我的个人站点www.samyoc.com
- Meteor changePassword
- 关于vue.js安装那点事
- ss-libev 源码解析local篇(2):ss_local和socks5客户端握手
- ORA-12638: 身份证明检索失败
- 数组中的重复数字
- 配置PostgreSQL实现TCP/IP访问连接
- python 内置运算
- 405错误 400错误
- VUE初学笔记
- RAII机制实现自动锁
- Logger日志级别及设置方法 说明