shadowsocks客户端源码分析

Author Avatar
WoodyXiong 4月 29, 2018
  • 在其它设备中阅读本文章

前言

学软件那么久最喜欢的一个开源项目就是shadowsocks了,虽然这个项目相对于其他的大型开源项目小得多,但是这个项目有中国特色社会主义情节在内,然后又有很多大神一起打造了shadowsocks项目圈,包括go语言的、路由器的、Linux客户端、Windows客户端、Android客户端、Iphone客户端等,甚至出现了shadowsocksr变种以及shadowsocksrr,这两种都是从shadowsocks衍生而来的产物,当时众多默默无闻的大神一起为项目作出贡献。

shadowsocks使用的是Apache License,开源免费,国内众多开发者在国外服务搭建自己的shadowsocks,更有人商用shadowsocks,给国内小白用户搭建代理。这更加增加了网络的透明度,在开发者看来,可以通过shadowsocks更加方便的访问谷歌,获取世界各地碰到的bug信息。多年以后,可能这样自由的软件还会被继续ban掉,但是人们心中崇尚自由的精神永远不会消失。

原理

ss示意图

Shadowsocks项目主要分成sslocalssserver,这是一个客户端和一个服务端,客户端放在本地,服务端架设在服务器中。

sslocal介绍

sslocal分成两个部分,第一个部分是socks5服务端,它负责监听本地的请求,另外一个部分是信息发送端,它负责向远程的ssserver发送数据包。

比如我想访问谷歌,首先需要配置浏览器的socks5代理指向本地或者局域网内的socks5服务端,然后socks5服务端将包进行加密,从信息发送端发送给远程的ssserver

ssserver介绍

ssserver也分成两个部分,一个是信息接收端,它是一个服务器,负责接收sslocal发来的加密数据,另外一个是请求内容端,它负责向目标服务器发送请求并将数据返回回去。

继续上述的访问谷歌的操作,ssserver信息接收端接收到sslocal发送的数据之后将数据进行解密,然后用请求内容端将需要发送的数据发送给谷歌服务器,因为ssserver是部署在国外,不受限制,然后将收到的数据原路返回即可到socks5客户端

使用wireshark分析shadowsocks

为了更加清楚的看清shadowsocks的底层传输格式,由于我已经比较熟悉http的报文格式,所以通过浏览器的socks5代理来与shadowsocks进行连接,然后使用wireshark分析底层的数据包。

参考文档 SOCKS5代理原理探索 socks5 rfc

wireshark分析shadowsocks

以下的序号为上图左侧的抓包序号。

7-9 tcp的三次握手

10 socks5客户端socks5服务端协商认证阶段

name VER NMETHODS METHODS
size 1 1 1
data 05 01 00
  • VER表示socks的版本,默认为5
  • NMETHODS字段是 METHODS字段占用的字节数
  • METHODS字段的每一个字节表示一种认证方式,表示客户端支持的全部认证方式。

11 服务端接收到客户端的请求之后,之后将认证方式发送给客户端

name VER METHOD
size 1 1
data 05 00
  • VER表示socks的版本,默认为5
  • METHOD表示认证的方式,00表示无需用户密码认证

13 socks5客户端指导shadowsocks连接需要访问的服务器

name VER CMD RSV ATYP DST.ADDR DST.PORT
size 1 1 1 1 Variable 2
data 05 01 00 03 115.114.113.112(已转成ascll) 0050
  • VER表示socks的版本,默认为5
  • CMDcommand的缩写,表示连接的方式
    • 0x01:建立 TCP 连接
    • 0x03:关联 UDP 请求
  • RSV保留字段,值为 0x00
  • ATYPaddress type 的缩写,取值为
    • 0x01:IPv4
    • 0x03:域名
    • 0x04:IPv6

15 当socks5服务端与远程的需要访问的服务器建立连接之后,发送以下数据

name VER REP RSV ATYP DST.ADDR DST.PORT
size 1 1 1 1 Variable 2
data 05 00 00 01 0.0.0.0(已转成ascll) 0050
  • REP表示返回的连接状态
    • 0x00: 连接成功
    • 其他状态参考rfc1928

17 纯正的http的Request Headers

简单粗暴的tcp发送报文,需要向远程服务器发送什么就发送什么

纯正的http的Request Headers

21 纯正的http的接收报文
远程服务器向本地发送什么,就返回给socks5客户端什么,也是简单粗暴

27 由于远程需要访问的服务器断开连接,所以socks5服务端socks5客户端也断开连接

源码分析

以下就是惊险刺激的源码分析了,目前分析的源码是shadowsocks2.9.1

因为sslocalssserver在写法上比较类似,都是接收数据+转发,所以在这里我们就只分析sslocal

以下贴出sslocal.py的主要的代码

def main():
    # 检测Python版本是否支持
    shell.check_python()

    # 如果是已经将python转为exe文件则进入如下配置
    if hasattr(sys, "frozen") and sys.frozen in \
            ("windows_exe", "console_exe"):
        p = os.path.dirname(os.path.abspath(sys.executable))
        os.chdir(p)

    # 获取配置
    config = shell.get_config(True)

    if not config.get('dns_ipv6', False):
        asyncdns.IPV6_CONNECTION_SUPPORT = False

    # 守护进程设置
    daemon.daemon_exec(config)

    try:
        # 录入日志
        logging.info("starting local at %s:%d" %
                     (config['local_address'], config['local_port']))

        # 生成dns寻址器
        dns_resolver = asyncdns.DNSResolver()

        # 生成tcp服务器
        tcp_server = tcprelay.TCPRelay(config, dns_resolver, True)

        # 生成udp服务器
        udp_server = udprelay.UDPRelay(config, dns_resolver, True)

        # 生成loop循环结构
        loop = eventloop.EventLoop()

        # 将以上的东西加入进loop循环结构
        dns_resolver.add_to_loop(loop)
        tcp_server.add_to_loop(loop)
        udp_server.add_to_loop(loop)

        def handler(signum, _):
            logging.warn('received SIGQUIT, doing graceful shutting down..')
            tcp_server.close(next_tick=True)
            udp_server.close(next_tick=True)
        signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), handler)

        def int_handler(signum, _):
            sys.exit(1)
        signal.signal(signal.SIGINT, int_handler)

        daemon.set_user(config.get('user', None))

        # 循环运行
        loop.run()
    except Exception as e:
        shell.print_exception(e)
        sys.exit(1)

if __name__ == '__main__':
    main()

代码的前半部分都是一些获取配置的,最主要的代码都在try...catch结构中。其中主要的东西有loop循环结构dns寻址器tcp服务器udp服务器

loop循环结构的代码在eventloop.py中,其中封装了三种IO复用函数epollkqueueselect,并且提供了统一的接口。

初始化dns寻址器的代码在asyncdns.py中,主要作用是生成dns的相关配置

初始化tcp服务器的代码在tcprelay.py中,主要作用是通过配置文件对ip和端口号进行tcp的监听

初始化udp服务器的代码在udprelay.py中,主要作用是通过配置文件对ip和端口号进行udp的监听

整个程序通过loop.run()函数对客户端进行监听,具体代码如下

def run(self):
    events = []
    while not self._stopping:
        asap = False
        try:
            events = self.poll(TIMEOUT_PRECISION)
        except (OSError, IOError) as e:
            if errno_from_exception(e) in (errno.EPIPE, errno.EINTR):
                # EPIPE: Happens when the client closes the connection
                # EINTR: Happens when received a signal
                # handles them as soon as possible
                asap = True
                logging.debug('poll:%s', e)
            else:
                logging.error('poll:%s', e)
                traceback.print_exc()
                continue

        for sock, fd, event in events:
            handler = self._fdmap.get(fd, None)
            if handler is not None:
                handler = handler[1]
                try:
                    handler.handle_event(sock, fd, event)
                except (OSError, IOError) as e:
                    shell.print_exception(e)
        now = time.time()
        if asap or now - self._last_time >= TIMEOUT_PRECISION:
            for callback in self._periodic_callbacks:
                callback()
            self._last_time = now

整个服务器在events = self.poll(TIMEOUT_PRECISION)中进行阻塞对客户端进行监听,如果有客户端连接或者发送信息,则进入for sock, fd, event in events:循环结构。

当有新的socks5客户端进行访问,先进入tcprelay类的handler.handle_event(sock, fd, event)函数,在这个函数中,判断客户端是否是新连接的,如果为新连接上,则初始化TCPRelayHandler,然后将其加入到eventloop中进行循环监听。

当有已经连接过的socks5客户端发送信息则直接进入TCPRelayHandlerhandle_event函数,函数大致如下

def handle_event(self, sock, event):
    # handle all events in this handler and dispatch them to methods
    if self._stage == STAGE_DESTROYED:
        logging.debug('ignore handle_event: destroyed')
        return
    # order is important
    if sock == self._remote_sock:
        if event & eventloop.POLL_ERR:
            self._on_remote_error()
            if self._stage == STAGE_DESTROYED:
                return
        if event & (eventloop.POLL_IN | eventloop.POLL_HUP):
            # read remote
            self._on_remote_read()
            if self._stage == STAGE_DESTROYED:
                return
        if event & eventloop.POLL_OUT:
            # write remote
            self._on_remote_write()
    elif sock == self._local_sock:
        if event & eventloop.POLL_ERR:
            self._on_local_error()
            if self._stage == STAGE_DESTROYED:
                return
        if event & (eventloop.POLL_IN | eventloop.POLL_HUP):
            # read local
            self._on_local_read()
            if self._stage == STAGE_DESTROYED:
                return
        if event & eventloop.POLL_OUT:
            # write local
            self._on_local_write()
    else:
        logging.warn('unknown socket')

判断event的状态,进入相关的函数

  • 本地的socks5客户端发送信息,则进入_on_local_read()函数
  • 需要向本地的socks5客户端发送信息,则进入_on_local_write()函数
  • 远程的服务器发来信息,则进入_on_remote_read()函数
  • 需要向远程的服务器发送信息,则进入_on_remote_write()函数

首先我们分析socks5客户端sslocal的通信

我们进入tcprelay.py然后继续分析_on_local_read()函数

首先判断本身与socks5客户端的状态,由于初始化是STAGE_INIT状态,即上文的10和11数据包,sslocal随即发送05 00数据包,然后更改状态为STAGE_ADDR,即进入等待连接状态

socks5客户端像上文一样发送tcp连接请求时,连接的TCPRelayHandler的状态为STAGE_ADDR,随即进入_handle_stage_addr函数,在此函数中马上发送05 00 00 01 00 00 00 00 10 10socks5客户端,然后将刚刚收到的连接数据进行加密,保存到data_to_send变量中,接着通过_dns_resolver.resolve(self._chosen_server[0],self._handle_dns_resolved)的回调函数连接远程的ssserver,然后将加密数据发送过去。上述函数是解析ssserver地址并且回调到_handle_dns_resolved函数与ssserver连接,此时的连接状态是STAGE_CONNECTING

此时的ssserver已经跟目标服务器连接上了socks5客户端继续将数据发送给ssserver,此时sslocal进入_handle_stage_connecting函数,将需要发送的数据加密发送给ssserver

接下来分析sslocalssserver的通信

第一次sslocalssserver建立通信是在_handle_dns_resolved的回调函数中,从此TCPRelayHandlerremote_socklocal_sock相互绑定,二者互相监听互相通信。

之后通过_on_remote_read函数接收ssserver发送的数据,然后解密通过_write_to_sock发送给sslocal

ssserversslocal断开连接之后,sslocal也会进入_on_remote_read函数,发现没有接收到任何数据,即为关闭连接的状态,随后进入destroy函数断开客户端连接并且销毁自己,这样连接就结束了。

总结

由于还没有仔细弄懂python的语法,所以一开始看python的代码还是感觉挺吃力的,况且shadowsocks的注释很少,都是靠debug和输出内容大概理解作者的思路。阅读源码之前一定要看一下socks5的底层数据原理,还要看一看python的tcp/ip通信的例子,这样才会少走好多弯路。为了巩固代码的理解,顺便为了学习python,我后续会再写一个socks5客户端挂在服务器上面。在之后还会继续分析shadowsocksudp功能。

参考链接

Shadowsocks 源码分析——协议与结构

Shadowsocks 源码分析——TCP 代理

计算机网络(一) 走近socks5