ss-libev 源码解析local篇(3): server_recv_cb之SNI和STAGE_PARSE

来源:互联网 发布:上海嘉桥数据咨询公司 编辑:程序博客网 时间:2024/05/16 12:49

上一篇看到STAGE_HANDSHAKE中的处理,到发出fake reply。这之后会从socks5 request中解析出remote addr and port,即客户端实际想要访问的服务器地址和端口。根据request->atyp,有三种情况,atyp为1,请求中带的是ipv4的地址,为3是域名,为4是ipv6地址。ss-local会把remote addr & port填入server->abuf中,可以认为abuf中是要发送到ss-server的内容。

            buffer_t *abuf = server->abuf;            abuf->idx = 0;            abuf->len = 0;            abuf->data[abuf->len++] = request->atyp;

abuf第一个字节,填入地址类型,即atyp,1,3 or 4。之后会根据地址类型填入地址和端口号,以ipv4为例:

if (atyp == 1) {                // IP V4                size_t in_addr_len = sizeof(struct in_addr);                if (buf->len < request_len + in_addr_len + 2) {                    return;                }                memcpy(abuf->data + abuf->len, buf->data + 4, in_addr_len + 2);                abuf->len += in_addr_len + 2;}

此时的buf是server->buf,因为正在处理socks5 request,buf中自带了地址和端口,所以这儿是直接从buf拷贝到abuf。buf->data+4是因为要跳过请求的前4个字节,从第5字节开始是地址和端口。域名和ipv6的情况也类似,可以认为此时的abuf就是buf的第4字节开始的内容。之所以要分开处理,是因为还要解析出host, ip,端口给acl使用。
在这之后,会进入sni阶段。

SNI: 从http/https中探测出要访问的域名

            size_t abuf_len  = abuf->len;            int sni_detected = 0;            if (atyp == 1 || atyp == 4) {                char *hostname = NULL;                uint16_t p = ntohs(*(uint16_t *)(abuf->data + abuf->len - 2));                int ret    = 0;                if (p == http_protocol->default_port)                    ret = http_protocol->parse_packet(buf->data + 3 + abuf->len,                                                      buf->len - 3 - abuf->len, &hostname);                else if (p == tls_protocol->default_port)                    ret = tls_protocol->parse_packet(buf->data + 3 + abuf->len,                                                     buf->len - 3 - abuf->len, &hostname);                if (ret == -1 && buf->len < BUF_SIZE) {                    server->stage = STAGE_PARSE;                    return;                } else if (ret > 0) {                    sni_detected = 1;                    // Reconstruct address buffer                    abuf->len               = 0;                    abuf->data[abuf->len++] = 3;                    abuf->data[abuf->len++] = ret;                    memcpy(abuf->data + abuf->len, hostname, ret);                    abuf->len += ret;                    p          = htons(p);                    memcpy(abuf->data + abuf->len, &p, 2);                    abuf->len += 2;                    if (acl || verbose || logDetail) {                        memcpy(host, hostname, ret);                        host[ret] = '\0';                        //wh                        if(logDetail){                            if(server->dest_host!=NULL){                                ss_free(server->dest_host);                            }                            server->dest_host = (char*)malloc(sizeof(host));                            memcpy(server->dest_host, host, sizeof(host));                            if(verbose){                                LOGI("reset host by parse [%s:%s]",server->dest_host,server->dest_port);                            }                        }                        //wh                    }                    ss_free(hostname);                }            }

这一段代码的主要目的是从http头或https的tls握手中解析出http/https请求要访问的域名。首先,浏览器访问url时,会先调用系统的dns解析将域名解析为ip地址,所以socks5客户端收到的就是包含ip地址的请求,当然也可能还有包含域名的请求,比如在socks5之前又使用了一个http代理。在某些网络环境中,系统解析出来的ip地址可能是不正确的,比如被dns劫持了等,ss-local在这儿会去分析http的头或者https的tls握手内容,分析出hostname,然后修改abuf,将原本是ip地址类型的abuf,修改为域名类型的abuf,这样socks5服务器将会去解析这个域名,从而避免DNS污染。下面具体分析这段代码。
首先,只对atyp为1或4,即ip地址的情况进行处理,开始先从abuf中解析出端口号,如果端口号为80或443则分别进行http/https的探测。探测的内容为,buf中buf->data + 3 + abuf->len开始的位置,长度为buf->len - 3 - abuf->len。回忆一下buf的结构,前4个字节加上后面的地址和端口,buf->data+3的位置是atyp,而abuf是相当于buf中从atyp往后的内容,所以buf->data + 3 + abuf->len相当于buf中整个socks5请求消息体的后面。而长度buf->len - 3 - abuf->len相当于buf中刨去socks5请求体剩余内容的长度。这是什么意思呢?我们一路看过来,此时是客户端给ss-local发送一个socks5请求,并没有其他内容,那解析的是啥?别急继续看。首先我们看下parse_packet的返回值,大于0表示解析出的hostname的长度,-1表示Incomplete request即要分析的内容是不完整的。那么一开始就只能返回-1,因为此时消息体后啥都没有。而ret返回-1的结果就是server->stage进入STAGE_PARSE状态并从server_recv_cb函数返回,等待下一次server_recv_cb的调用。

STAGE_PARSE:数据再次读入和分析

server进入STAGE_PARSE后,因为之前已经发送了fake reply,此时客户端会认为已经可以发送实际要代理的数据了,对于http会发送http消息体,其中最开始的是http头,对于https会先进行tls握手。而根据recv的代码,新来的数据总是添加到当前buf内容的最后,即buf->data+buf->len,回忆一下:

r = recv(server->fd, buf->data + buf->len, BUF_SIZE - buf->len, 0);

因为之前的步骤并没有清除buf,此时buf中是SOCKS5 request的内容,即4字节+地址端口。新来的数据就被添加到这个request之后。而上一篇我们看到,处理STAGE_PARSE和处理STAGE_HANDSHAKE是同一段代码:

else if (server->stage == STAGE_HANDSHAKE || server->stage == STAGE_PARSE)

也就是说,在STAGE_PARSE状态,我们会重新分析一遍整个buf,(当然不会再发送fake reply,因为已有状态区分),然后代码继续走到SNI的部分,此时parse_packet就有可能返回一个大于0的数了,表示解析出了hostname,如果读入的内容还是不够,则继续ret==-1的退出当前函数,等待更多数据的到来。其实ret除了大于0或-1,还有其他值,ss-local这儿没有具体处理,只是在ret为-1时还检查了一下buf->len:

                if (ret == -1 && buf->len < BUF_SIZE) {                    server->stage = STAGE_PARSE;                    return;                }

即如果buf->len大于BUF_SIZE则不会继续进入STAGE_PARSE了,而是跳过SNI部分,进入正式的STAGE_STREAM状态。这是为了防止一直解析不出hostname。毕竟ss-local只是根据端口号就去探测,没准探测的数据并不是http/s的,这种情况parse_packet会返回表示其他错误的ret,ss-local采取这种方法统一处理了各种不成功的可能,总之就是我解析到BUF_SIZE为止,如果还不行就放弃SNI。
当然后ret为大于0的数时,表示SNI成功,探测到hostname。此时ss-local就会重建abuf的内容:

                   // Reconstruct address buffer                    abuf->len               = 0;                    abuf->data[abuf->len++] = 3;                    abuf->data[abuf->len++] = ret;                    memcpy(abuf->data + abuf->len, hostname, ret);                    abuf->len += ret;                    p          = htons(p);                    memcpy(abuf->data + abuf->len, &p, 2);                    abuf->len += 2;

abuf第一个字节填3表示是域名,第二个字节填ret,因为ret就是hostname的长度,之后填入hostname,之后填入端口号。
SNI完成或者放弃后,代码继续:

           server->stage = STAGE_STREAM;            buf->len -= (3 + abuf_len);            if (buf->len > 0) {                memmove(buf->data, buf->data + 3 + abuf_len, buf->len);            }

首先将状态设置为STAGE_STREAM表示要转发实际的数据了。buf->len -= (3 + abuf_len);实际就是将buf中的SOCKS5 request消息清除,如果buf->len>0,表示除了request还有其他内容,比如SNI成功的情况,这个其他内容就是http头或者tls握手,需要保留这些内容,因为通过memmove移动到buf头部。这儿3 + abuf_len就相当于request的长度。
下面是acl的部分,这部分简单说下吧。acl其实就是白名单了,白名单里面的域名ip不通过ss代理,而是直接发送到目的服务器,这儿的处理是生成一个remote:

remote = create_remote(server->listener, (struct sockaddr *)&storage);                        if (remote != NULL)                            remote->direct = 1;

storage的地址是目的服务器的地址,所以说这种情况remote就不对应ss-server了,而是直接对应目的服务器,remote->direct = 1表示这个remote是直连的,会和ss-server的代理有区别。
继续往下,如果acl没符合,则就建立一个到远端ss-server的remote。

            // Not match ACL            if (remote == NULL) {                remote = create_remote(server->listener, NULL);            }

这个server->listener就是启动ss-local时创建的那个全局唯一的listen_ctx_t对象,create_remote里面会用到listener里面存储的remote_addr即ss-server服务器地址来创建到ss-server的socket连接,对于create_remote和里面调用的new_remote在分析remote时再说吧。这儿就知道创建了一个到ss-server的remote对象就可以了。继续,

if (!remote->direct) {                int err = crypto->encrypt(abuf, server->e_ctx, BUF_SIZE);

这是将abuf进行加密。继续,

            if (buf->len > 0) {                memcpy(remote->buf->data, buf->data, buf->len);                remote->buf->len = buf->len;            }            server->remote = remote;            remote->server = server;

如果buf去掉request之后还有内容(见上面),则把这些数据拷贝到remote的buf中,至于为什么这么做,回忆之前的代码,server_recv_cb一开始读取数据的时候,如果有remote存在,则读取到remote的buf中,因为这儿已经创建了remote,下一次再读取就使用remote的buf,所以先将这部分待处理的数据放到remote的buf中。然后是将server和remote互相关联起来,方便以后使用。
整个handshake & parse的过程就结束了,在退出server_recv_cb之前,ss-local还启动了一个timer:

ev_timer_start(EV_A_ & server->delayed_connect_watcher);

这个timer是做啥用的呢?刚刚说过,如果buf中有内容就copy到remote的buf中等待下一次处理,但是如果没有下一次了呢?如果buf中的内容就是本次tcp转发的所有数据了,server_recv_cb下一次被调用就是客户端主动close了,这样就会在recv到0之后直接关闭连接,这样remote buf中的数据就全丢失了。。于是乎,ss-local在本次退出server_recv_cb时启动了这个timer,这个timer是在new_server中建立的:

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

0.05秒后会调用delayed_connect_cb:

static voiddelayed_connect_cb(EV_P_ ev_timer *watcher, int revents){    server_t *server = cork_container_of(watcher, server_t,                                         delayed_connect_watcher);    server->stage = STAGE_WAIT;    server_recv_cb(EV_A_ & server->recv_ctx->io, revents);}

很简单,将server设置为wait状态,然后立刻主动调用server_recv_cb。这样回到server_recv_cb一开始的地方:

if (server->stage != STAGE_WAIT) {        r = recv(server->fd, buf->data + buf->len, BUF_SIZE - buf->len, 0);        .....    } else {        server->stage = STAGE_STREAM;    }

不读取数据,直接进入STAGE_STREAM去处理已有的数据。
下一篇具体分析STAGE_STREAM的过程。