本周末花了点时间阅读了一下 shadowsocks 2.8.1 版和 shadowsocks-go 1.1.4 版的源码。

原本工作原理是知道的,这里通过阅读源码,增加了对 eventloop 的网络模型和 Go 语言的 goroutine 模型的认知。两者比较起来,Python 版代码不记测试共计 4000 余行,Go 版本包括测试一共才 2000 多行代码。当然,主要原因是 Go 版本功能要少一点,比如 UDP 的支持,和 TCP Fast Open 特性支持。从理解上而言,Go 版本要远远好于 Python 版本。

基本原理

shadowsocks 的基本工作原理并不复杂。shadowsocks 包括 local 和 server 两个程序。local 运行在用户自己的机器上,server 运行在墙外的服务器上。正常工作模式下,local 通常会监听本地 1080 端口,提供 socks v5 协议接口。在用户本机进程和 local 的 1080 端口建立 TCP 连接之后,首先会发送一个 hello 包,或者叫 handshake 握手包。local 程序接收到这个包之后,进行简单的包数据检查之后就返回一个确认包。本机进程收到确认的包之后,会再发送一个请求包,包的主要内容就是目标服务的地址和端口号。local 程序接收到请求包之后,会和 server 程序建立一个 TCP 连接,连接建立之后会把上面的目标服务的地址和端口号传给 server。这个连接是穿墙的关键,连接里面传输的数据都是经过加密的,最常用的就是 aes-256-cfb。local 程序会对请求包返回一个确认的包。然后本机进程就开始向 local 传输实际的数据,local 接收到之后加密继续传给 server。server 接收到之后把数据解密出来,然后和目标服务建立 TCP 连接,并把解密后的数据发送出去。然后接收数据就是上述的反过来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
                                        great firewall
+
|
|
+----------+ | +-----------+
| | | | |
socks5 | local | encryption | server | raw data
<------------> 1080 | | <-----------+----------> | | <----------->
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
+----------+ | +-----------+
|
|
|
+

为了理解以上内容,强烈建议阅读一下 socks v5 协议的 RFC 1928。不长,一共才 9 页,定义的包格式也很少。

shadowsocks

shadowsocks Python 版本主要包括 Eventloop、TCPRelay、UDPRelay 和 DNSResolver 这几个模块。我们主要介绍 TCP 的模式,UDP 不做过多介绍。

shadowsocks 主要的工作流程就是先进行配置处理,然后针对每个需要监听的端口建立一个 TCPRelay 和 UDPRelay,并添加到 eventloop 里。然后启动 eventloop 的循环。当 eventloop 接收到事件时,将事件分发,调用对应的 handle_event 进行处理。对于每个建立的客户端发起的 TCP 连接,都会新建一个 TCPRelayHandler 进行处理。

在这里,local 和 server 使用的是同一个 TCPRelay 类,他们的处理流程都统一了起来。但是就是因为如此,代码的理解上反而显的不是那么清晰。

Eventloop

shadowsocks 最早期的版本是基于线程的模型处理并发连接的。由于种种原因,线程模型在频繁建立连接、高并发的情况下效率并不高。现在的版本是基于 eventloop 的处理模型。shadowsocks 里使用的 eventloop 是基于 epoll 模型的封装,把 select 和 kqueue 都封装成了 epoll 模型的接口。

eventloop 最重要的方法 run 里的逻辑,就是典型的 epoll 处理方式。这里强烈建议去理解一下 epoll 的工作模型。这里很简单,接收到 event 之后,调用对应的 handle_event 方法进行处理。

TCPRelay

TCPRelay 里有个概念就是 _server_socket,表示的是监听端口的 socket。然后看 TCPRelay 的 handle_event 逻辑就分为了两块,如果是 _server_socket,那么就只有客户端请求建立连接的事件,_server_socket 负责 accept 之后创建新的 TCPRelayHandler;如果不是,那么说明是客户端连接的读写事件,直接分发到对应的 TCPRelayHandler 调用 handle_event 进行处理。

tcprelay.py 这个文件最上方,有一段注释是描述的客户端连接建立的全部过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# as sslocal:
# stage 0 SOCKS hello received from local, send hello to local
# stage 1 addr received from local, query DNS for remote
# stage 2 UDP assoc
# stage 3 DNS resolved, connect to remote
# stage 4 still connecting, more data from local received
# stage 5 remote connected, piping local and remote

# as ssserver:
# stage 0 just jump to stage 1
# stage 1 addr received from local, query DNS for remote
# stage 3 DNS resolved, connect to remote
# stage 4 still connecting, more data from local received
# stage 5 remote connected, piping local and remote

在 TCPRelayHandler 里,就是按照如下定义的 stage 的流程运行的。

1
2
3
4
5
6
7
STAGE_INIT = 0
STAGE_ADDR = 1
STAGE_UDP_ASSOC = 2
STAGE_DNS = 3
STAGE_CONNECTING = 4
STAGE_STREAM = 5
STAGE_DESTROYED = -1

先看 handle_event,里面的逻辑分成了 remote_sock 和 local_sock 两部分。先从 local_sock 开始。从客户端建立 TCP 连接之后,TCPRelayHandler 创建的时候,local_sock 就存在了,由于是客户端主动建立的连接,数据也都是客户端先发起的,所以先从 local_sock 的可读事件开始。记住,我们目前处于 STAGE_INIT 状态。在进入_on_local_read之后,紧守着is_local_stage两个变量,就可以按照上面基本原理里面说的工作流程,将状态机运行起来了。

eventloop 模型比较让人讨厌的就是要不停的循环,然后进入各自的 handle_event 里去思考流程,比较麻烦。

shadowsocks-go

我们再来看看 Go 语言版本基于 goroutine 协程模型。

Go 语言版本的 local 和 server 也是十分相似的。

以 local 程序开始介绍。从 main 开始,处理各种配置的问题;然后到 run 运行起来,建立服务端的 socket,监听端口,循环接收来自客户端的连接请求;然后调用 handleConnection,并建立 goroutine,处理该连接的所有逻辑;在 handleConnection 里,所有逻辑就十分的简单了,全部是同步的方式,从上到下,一步一步的,最后建立上行和下行的两个 PipeThenClose。

main 就没什么好说的了,就是初始化配置,各种参数检查,然后跳到 run 开始运行。run 里面最主要的就是一个 for 的无限循环,不停的 accept 连接,然后 go handleConnection(conn) 开启新的 goroutine 协程并发的执行。

handleConnection 的逻辑分为 4 块。第一块就是到 handShake,无非就是按照 socks v5 协议要求接收一个包,返回一个包。第二块是到 getRequest,这里也是按照 socks v5 协议要求,接收一个包,这个包里主要的内容就是目标服务的地址和端口号。第三块是到 createServerConn,这里返回的 remote 是一个和远端的 server 建立的连接,是一个 ss.Conn 的类型,这个 Conn 类型是在标准库 net.Conn interface 的基础上进行的封装,实现了 Read 和 Write 接口。自己实现的 Read 接口会从 src 读数据之后并解密后返回,Write 接口会把数据加密后写入到 socket 中。最后第四块就是两个 PipeThenClose,自己这个 goroutine 和新开启的 goroutine 并发的从本地到远端、远端到本地的上下行的数据传输。

server 的执行流程和上述也是一样的,main 里面处理配置之后到 run 开始执行。run 里面无限循环接收请求,建立连接,然后go handleConnection(ss.NewConn(conn, cipher.Copy())) 开启新的 goroutine。需要注意的是这里传入的是 ss.Conn 类型的客户端连接,他的 Read 和 Write 接口是有解密和加密的逻辑的。handleConnection 方法的逻辑也十分相似了,除了没有第一步的握手阶段。

总结

总得看来,shadowsocks 并不复杂。Python 版本的 eventloop 和 Go 版本的 goroutine,两种模型相比较,个人觉得 goroutine 明显更加容易理解。

在理解上述内容之前,需要读者对于 socket 编程、epoll 模型、socks v5 的协议内容和 Go 语言要有一定的了解。