上一篇看到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. 20135320赵瀚青LINUX内核分析第四周学习笔记

    赵瀚青原创作品转载请注明出处<Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 概述 本周的内容主要是讲解系 ...

  2. Animal_human_kp人脸与马脸迁移学习GitHub 论文实现

    Interspecies Knowledge Transfer for Facial Keypoint Detection关键点检测   Github地址:Interspecies Knowledge ...

  3. [小问题笔记(三)] SVN树冲突(Tree Conflict),文件不能提交的解决办法

    传说中SVN的树冲突是由不同开发者删除文件,移动文件神马的造成的. 我们遇到的情况是: 开发人员小B移动了项目中几个文件然后提交.开发人员小L更新项目至最新版本. 获取到移动后的文件则显示文件已被修改 ...

  4. 51nod 1187 寻找分数

    本文版权归ljh2000和博客园共有,欢迎转载,但须保留此声明,并给出原文链接,谢谢合作. 本文作者:ljh2000 作者博客:http://www.cnblogs.com/ljh2000-jump/ ...

  5. Linux shell常用命令

    1. sz 和 rz  sz命令发送文件到本地: # sz filename rz命令本地上传文件到服务器: # rz 执行该命令后,在弹出框中选择要上传的文件即可.

  6. 关于HashTable,HashMap和TreeMap的几点心得

    刚开始看到HashTable,HashMap和TreeMap的时候比较晕,觉得作用差不多,但是到实际运用的时候又发现有许多差别的.于是自己搜索了一些相关资料来学习,以下就是我的学习沉淀. java为数 ...

  7. linux sed 批量替换字符串

    Linux下批量替换多个文件中的字符串的简单方法.用sed命令可以批量替换多个文件中的字符串. 命令如下: sed -i "s/原字符串/新字符串/g" `grep 原字符串 -r ...

  8. 到底啥是平台,到底啥是中台?李鬼太多,不得不说(ZT)

    (1)哪些不是中台,而是应该叫平台 做开发,有所谓的三层技术架构:前端展示层.中间逻辑层.后端数据层.我们现在讲的中台不在这个维度上. 做开发,还有所谓的技术中间件.一开始我们没有中间件的概念,只有操 ...

  9. 2.spring cloud eureka client配置

    红色加粗内容表示修改部分 1.把server项目打成jar包并启动 在项目根目录cmd执行  mvn clean package -Dmaven.test.skip=true mavne仓库地址建议 ...

  10. commons-fileupload实现上传进度条的显示

    本文将使用   apache fileupload   ,spring MVC   jquery 实现一个带进度条的多文件上传, 由于fileupload 的局限,暂不能实现每个上传文件都显示进度条, ...