美文网首页iOS程序员
shadowsocks源码分析之UDP协议

shadowsocks源码分析之UDP协议

作者: 1d96ba4c1912 | 来源:发表于2018-04-01 16:46 被阅读413次

    shadowsocks中的UDP协议转发用的应该比较少,网上的资料也非常少。
    但为了对shadowsocks做一个比较完整的分析,我还是读了这部分源码,这次总结一下,算是对shadowsocks源码的一个完结。

    注:该篇文章用到了前一篇TCP协议文章的知识,建议先去看TCP协议实现,然后再看本文。

    shadowsocks源码分析之TCP协议

    socks5协议中的UDP流程

    第一步:跟上一篇TCP协议一样,先进行握手,不再赘述。

    第二步:握手完成以后发送给代理服务器目标服务器地址时,CMD命令值设置为03,即为请求转发UDP协议。代理服务器返回的响应信息中,BND.ADDR跟BND.PORT就是给客户端分配的UDP地址,后面客户端会把所有的UDP数据包都发送到这个地址。
    注:这两步都是在TCP连接上进行的。

    第三步:开始发送数据,socks5协议要求所有的UDP数据包都带有如下头部:

    字段名 说明
    RSV 占用2个字节,保留字节,默认设置为00。
    FRAG 占用1个字节,表示当前数据包位于整个数据中的位置,下面细说。
    ATYP DST.ADDR类型,其中0x01表示IPv4地址,此时DST.ADDR部分4字节长度。0x03则表示域名,此时DST.ADDR部分第一个字节为域名长度,DST.ADDR剩余的内容为域名,没有\0结尾。0x04表示IPv6地址,此时DST.ADDR部分16个字节长度。
    DST.ADDR 目标服务地址
    DST.PORT 网络字节序表示的目标服务的端口

    注:由于UDP是没有连接的,因此需要每次都带上目标服务地址,这很好理解。

    下面详细说说FRAG字段。
    首先如果FRAG设置为0,则表示该数据包是独立的。
    其次,如果一次性发送的数据很多,自然是要拆分成多个数据包进行发送,拆包以后,FRAG占中一个字节,协议规定最高位用来表示数据流是否结束,也就是说如果值为1xxx xxxx则表示这就是最后一个数据包了。这样的话只用1-127表示数据包在整个数据流中的位置。

    TCP协议中也会拆包,只是协议本身帮我们做好了数据的组装,在UDP中这部分逻辑就需要我们自己来写了。socks5协议论文中描述了如何进行数据的组合,简单翻译如下:

    设置一个队列跟超时计时器,当收到数据包时先把它放在队列中。如果超时以后最后一个数据包还没有到,则丢弃。另外如果收到的数据包中FRAG字段的值小于当前正在处理的数据包的最大值,则丢弃。论文中建议超时时间设置不小于5秒。

    论文中建议用户最好不要去实现FRAG功能。后面可以看到shadowsocks中就没有实现该功能。
    好了,理论部分说完下面就该上代码了。

    shadowsocks中UDP的使用

    名词说明:

    1. client:请求来源,一般来说是浏览器。
    2. ss-local:本地代理服务。
    3. ss-server:远端代理服务。
    4. 目标服务:数据包真正要发往的服务器。

    上一篇文章中使用的是go语言的shadowsocks,但是该版本并没有实现UDP协议转发,本文就python版的shadowsocks源码进行分析,目前我也就发现该版支持UDP协议转发。这次就不准备看全量代码了,只分析部分片段。

    握手过程跟TCP一样,这里不再赘述,直接看握手完成以后,交换目标服务器地址的代码。

    由于UDP数据转发是建立的TCP连接之上的,因此源码在tcprelay.py文件中,如下:

    #取出收到数据中的CMD字段
    cmd = common.ord(data[1])
    #如果是UDP的请求,则进行UDP处理
    if cmd == CMD_UDP_ASSOCIATE:
        #拼接返回头信息,包括VER + REP + RESV + ATYP + BND.ADDR + BND.PORT
        if self._local_sock.family == socket.AF_INET6:
            header = b'\x05\x00\x00\x04'
        else:
            header = b'\x05\x00\x00\x01'
        addr, port = self._local_sock.getsockname()[:2]
        #后面接受UDP数据包的地址,IP PORT
        addr_to_send = socket.inet_pton(self._local_sock.family,
                                        addr)
        port_to_send = struct.pack('>H', port)
        #返回给client
        self._write_to_sock(header + addr_to_send + port_to_send,
                            self._local_sock)
        return
    

    返回数据的详细格式可以参考上一篇文章

    这步完成以后,就可以看交换数据部分了。关于数据交换,shadowsock中采用的方式是如果是ss-local与client进行交互,则严格尊重socks5协议的头信息,也就是说数据包前面会加上RSV + FRAG + ATYP + DST.ADDR + DST.PORT几个字段,而在ss-local跟ss-server之间进行交互时,却是跟TCP一致,采用ATYP + DST.ADDR + DST.PORT加数据的方式,这点要切记。

    这里要提前说明一下,不要被前一篇文章中我分析的源码逻辑误导。
    在go版的shadowsocks中,源码的组织形式是分两块,一个是ss-local的读与写,一个是ss-server的读与写。
    但在python版shadowsocks中虽然也是分了两个板块,却是按数据流向分的。把client到ss-local与ss-local到ss-server放在一个板块取名为_handle_server,把目标服务器到ss-server跟ss-server到ss-local放在了一个板块取名为_handle_client。

    先来看下_handle_server的代码:

    def _handle_server(self):
        server = self._server_socket
        #从socks读取数据,data为读取数据,r_addr为数据发送方的地址
        data, r_addr = server.recvfrom(BUF_SIZE)
        if not data:
            logging.debug('UDP handle_server: data is empty')
        if self._stat_callback:
            self._stat_callback(self._listen_port, len(data))
        #is_local表示是ss-local,也就是说这里的数据来源是client
        if self._is_local:
            #这里如果frag不为0则直接丢弃,说明它并没有实现协议中的拆包功能
            frag = common.ord(data[2])
            if frag != 0:
                logging.warn('drop a message since frag is not 0')
                return
            else:
                #截取数据,即ATYP + ADDR + PORT + 数据
                #丢弃掉RSV跟FRAG两个字段
                data = data[3:]
        else:
            #这里说明是ss-server,数据来自于ss-local,所以先进行解密
            data = encrypt.encrypt_all(self._password, self._method, 0, data)
            #失败则返回
            if not data:
                logging.debug('UDP handle_server: data is empty after decrypt')
                return
        #解析头信息,返回ATYP + ADDR + PORT
        header_result = parse_header(data)
        #失败返回
        if header_result is None:
            return
        addrtype, dest_addr, dest_port, header_length = header_result
    
        if self._is_local:
            #如果是ss-local,则取ss-server的地址
            server_addr, server_port = self._get_a_server()
        else:
            #如果是ss-server,则取数据包中的目标地址
            server_addr, server_port = dest_addr, dest_port
        #解析地址
        addrs = self._dns_cache.get(server_addr, None)
        if addrs is None:
            addrs = socket.getaddrinfo(server_addr, server_port, 0,
                                       socket.SOCK_DGRAM, socket.SOL_UDP)
            if not addrs:
                # drop
                return
            else:
                self._dns_cache[server_addr] = addrs
    
        af, socktype, proto, canonname, sa = addrs[0]
        key = client_key(r_addr, af)
        client = self._cache.get(key, None)
        #如果第一次转发,初始化连接
        if not client:
            if self._forbidden_iplist:
                if common.to_str(sa[0]) in self._forbidden_iplist:
                    logging.debug('IP %s is in forbidden list, drop' %
                                  common.to_str(sa[0]))
                    # drop
                    return
            client = socket.socket(af, socktype, proto)
            client.setblocking(False)
            self._cache[key] = client
            self._client_fd_to_server_addr[client.fileno()] = r_addr
    
            self._sockets.add(client.fileno())
            self._eventloop.add(client, eventloop.POLL_IN, self)
    
        #ss-local发送数据给ss-server,因此对数据进行加密
        if self._is_local:
            data = encrypt.encrypt_all(self._password, self._method, 1, data)
            if not data:
                return
        else:
            #ss-server需要把原生数据发送给目标服务器,截取出原生数据
            data = data[header_length:]
        if not data:
            return
        #进行转发
        try:
            client.sendto(data, (server_addr, server_port))
        except IOError as e:
            err = eventloop.errno_from_exception(e)
            if err in (errno.EINPROGRESS, errno.EAGAIN):
                pass
            else:
                shell.print_exception(e)
    

    最后再看下_handle_client的代码,注意它的数据流量是反过来的:

    def _handle_client(self, sock):
        #从socks读取数据,data为读取数据,r_addr为数据发送方的地址
        data, r_addr = sock.recvfrom(BUF_SIZE)
        if not data:
            logging.debug('UDP handle_client: data is empty')
            return
        #设置了一个状态回调函数,可忽略
        if self._stat_callback:
            self._stat_callback(self._listen_port, len(data))
        #表示是ss-server读取到目标服务发来的数据
        if not self._is_local:
            addrlen = len(r_addr[0])
            if addrlen > 255:
                # drop
                return
            #打包数据,加入头,整体下来就是ATYP + ADDR + PROT + 数据
            data = pack_addr(r_addr[0]) + struct.pack('>H', r_addr[1]) + data
            #由于是发往ss-server的,因此要加密,注意这里入参1表示加密
            response = encrypt.encrypt_all(self._password, self._method, 1,
                                           data)
            #失败则返回
            if not response:
                return
        else:
            #这个判断表示是ss-local收到ss-server发来的数据,需要先解密
            #注意这里入参,0表示解密
            data = encrypt.encrypt_all(self._password, self._method, 0,
                                       data)
            #失败则返回
            if not data:
                return
            #解析头数据,就是ATYP + ADDR + PORT
            header_result = parse_header(data)
            if header_result is None:
                return
            #拼接上协议UDP协议头部的RSV + FRAG
            response = b'\x00\x00\x00' + data
        #转发数据给client
        client_addr = self._client_fd_to_server_addr.get(sock.fileno())
        if client_addr:
            self._server_socket.sendto(response, client_addr)
        else:
            # this packet is from somewhere else we know
            # simply drop that packet
            pass
    

    上面最坑的就是加解密方法encrypt.encrypt_all,居然通过入参是0或1来判断进行解密还是加密,我读代码的时候被这个方法名误导了很久。


    后记:
    关于shadowsocks的源码分析就到这里,其实原论文除了TCP跟UDP以外,还有一种BIND的连接方式,是用来转发FTP协议之类的需要双通道的场景,应该比UDP还要不常用,就不再分析了,有机会再交流吧。

    相关文章

      网友评论

      • 凌晨海岸:本文原创凌晨海岸

            2019-2020年灾难预警与避免方法分析

        如何避免陨石陨落(预测正常运转下的接触时间和地点;不断为地球磁场充能让地球公转快一点或者慢一点;利用超大功率电磁炮远距离轰击陨石粉碎陨石光解陨石;)

        如何避免大规模战争(进行圣人相关理念和思想宣传)

        如何避免大规模海啸(近月点减小月球和陨石对地球产生的潮汐力多植树减小冲击力和缓冲风力,修建湖泊修建大坝修建固水固土固砂系统锁住陆地水分防止暴雨冲刷大地;)

        如何避免大规模瘟疫(保证环境清洁卫生;使用无污染材料;注意日常杀菌消毒工作;注意保证某个地区人口密度不要过大;保证通风透气;预测瘟疫性质医疗队提前做好相关准备;)

        如何避免大规模地震(地震伴随海啸;近期不要使用大规模杀伤性武器实验和试验;慢慢梳散几大预知地震中心的人类;减少深度挖煤挖矿和石油钻井采集;种植开采人造清洁能源;)

        如何避免地球磁场紊乱(南北极磁场对调;避免过度开采铁矿石等相关磁性资源;)

        如何避免火山爆发(海水因为大量进入岩浆层致使地球内部膨胀强大的内部气压会撑破地壳导致火山爆发;持续观察活性火山,渐渐疏散各火山口的居民;)

        如何避免拜物主义引起的系列问题(地球物质资源大部分聚集到人类种族会导致自然界其它物种的生存压力;按需获得相关物质资源避免过度浪费;避免大规模生产低质量的产品;尽早研发和生产人造蛋白质人造脂肪人造钙铁锌硒维生素等作为直接食物;从而避免伤害生态中其它动植物保证其它物种数量和多样性比如蜜蜂等;)
      • 小慕汐:你好,对UDP这里有一些疑问,能否帮忙解答呢,可以的话留个QQ
        1d96ba4c1912:@嘟嘟小灰 866825822 可以加这个讨论组
      • Sevenuncle:shadowsocks 服务端默认是upd开启的吗,

      本文标题:shadowsocks源码分析之UDP协议

      本文链接:https://www.haomeiwen.com/subject/fgovcftx.html