上一篇看到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 void
delayed_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的过程。

ss-libev 源码解析local篇(3): server_recv_cb之SNI和STAGE_PARSE的更多相关文章

  1. ss-libev 源码解析local篇(4): server_recv_cb之STAGE_STREAM

    继续探索server_recv_cb,我们已经来到了STAGE_STREAM状态.如果在0.05秒的timer来之前客户端就有数据过来,server_recv_cb被调用,此时已经在stream状态就 ...

  2. ss-libev 源码解析local篇(1): ss_local的启动,客户端连入

    学习研究ss-libev的一点记录(基于版本3.0.6) ss_local主要代码在local.c中,如果作为一个库编译,可通过start_ss_local_server启动local server. ...

  3. ss-libev 源码解析local篇(5):ss-local之remote_send_cb

    remote_send_cb这个回调函数的工作是将从客户端收取来的数据转发给ss-server.在之前阅读server_recv_cb代码时可以看到,在STAGE_STREAM阶段有几种可能都会开启r ...

  4. jQuery2.x源码解析(缓存篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 缓存是jQuery中的又一核心设计,jQuery ...

  5. jQuery2.x源码解析(构建篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 笔者阅读了园友艾伦 Aaron的系列博客< ...

  6. jQuery2.x源码解析(设计篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 这一篇笔者主要以设计的角度探索jQuery的源代 ...

  7. jQuery2.x源码解析(回调篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 通过艾伦的博客,我们能看出,jQuery的pro ...

  8. Shiro源码解析-Session篇

    上一篇Shiro源码解析-登录篇中提到了在登录验证成功后有对session的处理,但未详细分析,本文对此部分源码详细分析下. 1. 分析切入点:DefaultSecurityManger的login方 ...

  9. myBatis源码解析-类型转换篇(5)

    前言 开始分析Type包前,说明下使用场景.数据构建语句使用PreparedStatement,需要输入的是jdbc类型,但我们一般写的是java类型.同理,数据库结果集返回的是jdbc类型,而我们需 ...

随机推荐

  1. 20145327 《Java程序设计》第八周学习总结

    20145327 <Java程序设计>第八周学习总结 教材学习内容总结 NIO使用频道(channel)来衔接数据节点,在处理数据时,NIO可以让你设定缓冲区(Buffer)容量,在缓冲区 ...

  2. 20145329 吉东云《Java程序设计》第二周学习总结

    教材学习内容总结 第三章 基础语法 基本类型 1.整数(short.int.long) 2.字节(byte),可表示-128~127的整数 3.浮点数(float/double),主要储存小数数值 4 ...

  3. mysql对数据库的备份和还原

    在对mysql数据库的某个数据库进行备份时,使用 mysqldump命令来进行操作 mysqldump -u root -p db_database_name > /[your_path.mys ...

  4. Elasticsearch之停用词

    前提 什么是倒排索引? Elasticsearch之分词器的作用 Elasticsearch之分词器的工作流程 Elasticsearch的停用词 1.有些词在文本中出现的频率非常高,但是对文本所携带 ...

  5. input[type="file"]的样式以及文件名的显示

    如何美化input[type="file"] 基本思路是: (1)首先在 input 外层套一个 div : (2)将 div 和 input 设置为一样大小(width和heig ...

  6. .Net HttpClient form-data格式请求

    var multipartFormDataContent = new MultipartFormDataContent(); multipartFormDataContent.Add(new Stri ...

  7. easyui combobox 拼音检索快捷选择输入

    easyui combobox 拼音检索快捷选择输入 效果如图   $.ajax({ url: UserActionUrl + '?action=listuserworktype', dataType ...

  8. LightOJ 1356 Prime Independence(质因数分解+最大独立集+Hopcroft-Carp)

    http://lightoj.com/login_main.php?url=volume_showproblem.php?problem=1356 题意: 给出n个数,问最多能选几个数,使得该集合中的 ...

  9. hiho 有序01字符串 dp

    题目1 : 有序01字符串 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 对于一个01字符串,你每次可以将一个0修改成1,或者将一个1修改成0.那么,你最少需要修改多少 ...

  10. 【Python】 \uxxxx转中文

    背景 写Python接口自动化过程中,使用到邮件发送测试结果详情,邮件呈现出来的内容为 \uxxxx ,不是中文 接收到的邮件内容: 成功: 110 失败: 1 失败的用例如下 : [(u'\u752 ...