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中还有一些处理,为避免本篇太长,留下篇再说。

原创粉丝点击