Shadowsocks 源码解释

对于科学上网这种事,我向来一丝不苟。

但本po的主题并不是如何科学上网,而是深入了解proxy背后的原理。

Preface

去年shadowsocks在V2EX刚发布的时候,我就已经开始留意这个项目。当时还在用Goagent,但有时候速度确实不咋的,而且重新配置的话会比较麻烦。9月份入手VPS之后开始折腾PPTP VPN,效果相当不稳定。不久后OpenVPN也开始受到干扰了。看来必须寻找比较小众的方式,避免躺枪。因此,初试shadowsocks(python版),速度或者稳定性都相当好,一直用到现在,未出现过什么问题。配置也很简单,唯一的门槛就是需要国外的Linux 主机(VPS)。现在shadowsocks项目已经发展到多语言跨平台了,社区也比较活跃,主要原因是项目架构简单,代码精简易于维护。

一周前开始学习python,主要是想用python写一个爬虫。大概用了4天的课余时间把 《Dive Into Python3》过了一遍,了解基本语法。结合文档看大牛的源码是很好的学习方式,所谓learn by doing嘛。不得不说,shadowsocks的源码真心简洁,再看一下SOCK5协议的报文格式,并没有花很多时间。貌似说了不少废话,现在入正题= =!

Socks5

首先介绍一下socks5协议: SOCKS协议位于传输层(TCP/UDP等)与应用层之间,其工作流程为

  1. client向proxy发出请求信息,用以协商传输方式
  2. proxy作出应答
  3. client接到应答后向proxy发送目的主机(destination server)的ip和port
  4. proxy评估该目的主机地址,返回自身IP和port,此时C/P连接建立。
  5. proxy与dst server连接
  6. proxy将client发出的信息传到server,将server返回的信息转发到client。代理完成

client连接proxy的第一个报文信息,进行认证机制协商

version nmethod methods
1 Bytes 1 Bytes 1 to 255 Bytes

一般是 hex: 05 01 00 即:版本5,1种认证方式,NO AUTHENTICATION REQUIRED(无需认证 0x00)

proxy从METHODS字段中选中一个字节(一种认证机制),并向Client发送响应报文:

version methods
1 1

一般是 hex: 05 00 即:版本5,无需认证

认证机制相关的子协商完成后,Client提交转发请求:

VER CMD RSV ATYP DST.ADDR DST.PORT
1 1 0x00 1 variable 2

前3个一般是 hex: 05 01 00 地址类型可以是 * 0x01 IPv4地址 * 0x03 FQDN(全称域名) * 0x04 IPv6地址

对应不同的地址类型,地址信息格式也不同: * IPv4地址,这里是big-endian序的4字节数据 * FQDN,比如”www.nsfocus.net”,这里将是:0F 77 77 77 2E 6E 73 66 6F 63 75 73 2E 6E 65 74。注意,第一字节是长度域 * IPv6地址,这里是16字节数据。

proxy评估来自Client的转发请求并发送响应报文

VER REP RSV ATYP BND.ADDR BND.PORT
1 1(response) 0x00 1 variable 2

Proxy可以靠DST.ADDR、DST.PORT、SOCKSCLIENT.ADDR、SOCKSCLIENT.PORT进行评估,以决定建立到转发目的地的TCP连接还是拒绝转发。若允许则响应包的REP为0,非0则表示失败(拒绝转发或未能成功建立到转发目的地的TCP连接)。

shadowsocks source code

源代码方面,主要是由socks5转发模块和加密解密模块组成

转发模块感觉比较简单,但是个人觉得有几点需要注意的地方,或者说我自己不太明白。(python菜,请谅解)

1
2
3
4
5
6
with open('config.json', 'rb') as f:
    config = json.load(f)
SERVER = config['server']
REMOTE_PORT = config['server_port']
PORT = config['local_port']
KEY = config['password']

client

  • 从main开始,读取配置。这里为什么要用二进制的方式打开json文件呢?
  • 解释命令行参数 optlist, args = getopt.getopt(sys.argv[1:], 's:p:k:l:'),这个跟Linux的getopt函数差不多,可以自己man一下。
  • 设置logging等级和信息,生成密文表(包括加密解密)。
  • 运行 ThreadingTCPServer 实例。从定义看, class ThreadingTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer) 子类化ThreadingTCPServer(多重继承继承ThreadingMixIn类和TCPServer类),即使用多线程处理TCP请求(Mix-in class to handle each request in a new thread),同时设置其类属性allow_reuse_address。这里绑定的地址是(”,PORT),意思是该套接字对于本机的任何地址都是可达的。BaseRequestHandler 则由 Socks5Server 子类实现。由于 SocketServer module 包含了很多socket programming的细节,所以代码看起来相当简洁。
  • 每当有请求到达,调用 handle 函数,主要是建立proxy到client和server的连接,然后调用 handle_tcp 函数来转发TCP数据包(包括client to server或相反方向),对于server来说,proxy是完全透明的。当然,这里client和proxy之间的数据交互需要通过加密传输。
  • 对于handle_tcp而言,它需要同时处理两个socket(client & server),这里使用了I/O multiplexing的方式,选择select作为实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def handle_tcp(self, sock, remote):
    try:
        fdset = [sock, remote]
        while True:
            r, w, e = select.select(fdset, [], [])
            if sock in r:
                data = sock.recv(4096)
                if len(data) <= 0:
                    break
                result = send_all(remote, self.decrypt(data))
                if result < len(data):
                    raise Exception('failed to send all data')
            if remote in r:
                data = remote.recv(4096)
                if len(data) <= 0:
                    break
                result = send_all(sock, self.encrypt(data))
                if result < len(data):
                    raise Exception('failed to send all data')

    finally:
        sock.close()
        remote.close()

对于高性能并发服务器而言,这是一种非常重要的手段。当然还是其他实现方式,例如poll,epoll,kqueue等。这里由于文件描述符数量较小,所以分别也不大了。更详细的信息可以看 C10K problem

server

服务端代码与客户端差不多,主要是数据报文的解释和转发问题。主要是处理好在client端发送过来的自定数据格式,转发到目的地址server,再将返回的数据转发给client。

更多的细节我都在源码上注释了,有兴趣可以看看。

整个架构图大概这样:(就不要吐槽画的有多丑了= =)

arch


Reference:

Socks5详解(RFC)

Comments