420d13e4bb7d7e6b3d65e2733182d644
Shadowsocks Probe I - Socks5 与 EventLoop 事件分发

#define 爱国 科学

最近 Apple Store 在大陆下架了所有 VPN 应用。然而日常的爱国上网已经成为了刚需。这也就是促使我阅读 Shadowsocks 源码的原因。希望后期可以自行编写移动设备的 Client 端而努力。

Shadowsocks 的原理初探

关于 Shadowsocks 的原理,有一张经典的图示解释的十分清晰:
whats-shadowsocks-041
(该图引用自 vc2tea · 写给非专业人士看的 Shadowsocks 简介
Shadowsocks 是将原先的 ssh 创建的 Sock5 协议分成了 Server 端和 Client 端,这是一种类 ssh tunnel 的解决方案。
客户端发出的 Socks5 协议与 SS Local 进行通信以后,由于 SS Local 是当前使用端或是一个路由器越过 GFW,与 SS Server 进行通信,避免了 GFW 的分析干扰问题。并且在 SS Local 和 SS Server 两端可通过各种各样的加密方式进行通信,并且经过 GFW 的网络包就是很普通的 TCP 包,没有特征码,GFW 也无法对其数据进行解密。SS Server 对数据进行解密,还原请求并触发,在以相同的通信方式回传 SS Local。
一句话总结就是 Shadowsocks 可以加密数据包并伪装成常规的 TCP 包,从而达到数据交互。

Socks5 协议

Shadowsocks 源码分析——协议与结构 这篇文中讲述了 Socks5 协议的三个过程:握手阶段建立连接传输阶段。再具体一些可将其扩展成这么一个工作流:

  1. Client 向 Proxy 发出请求信息,用以写上传输方式;
  2. Proxy 做出应答;
  3. Client 接到应答后向 Proxy 发送 Destination Server (很多书中称之为目的主机)的 IP 和 Port;
  4. Proxy 来评估 Destination Server 的主机地址,并返回自身的 IP 和 Port,此时 C/P 的链接建立;
  5. Proxy 和 Dst Server 链接;
  6. Proxy 将 Client 发出的信息传至 Server,将 Server 响应的信息转发给 Client,完成整个代理过程。
    在 Client 连接 Proxy 的时候,通过第一个报文信息来协商认证,比如其中的信息包括:是否使用用户名/密码方式进行认证等等。以下是格式信息,数字表示对应字段占用的 Byte 值:
  • +----+----------+----------+
  • |VER | NMETHODS | METHODS |
  • +----+----------+----------+
  • | 1 | 1 | 1~255 |
  • +----+----------+----------+
  • VER:是当前协议的版本号,这里是 5
  • NMETHODS:是 METHODS 字段占用的 Byte 数;
  • METHOD:每一个字节表示一种认证方式,表示客户端支持的全部认证方式。
    Proxy 在收到客户端请求后,检查是否有认证方式,并返回一下格式的消息:
  • +----+--------+
  • |VER | METHOD |
  • +----+--------+
  • | 1 | 1 |
  • +----+--------+

对于 Shadowsocks 而言,只有两种可能:

  • 0x05 0x00:告诉 Client 采用无认证方式来建立连接;
  • 0x05 0xff:客户端的任意一种认证方式 Proxy 都不支持。
    handshake-time
    在握手之后,Client 会向 Proxy 发送请求,格式如下:
  • +----+-----+-------+------+----------+----------+
  • |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
  • +----+-----+-------+------+----------+----------+
  • | 1 | 1 | 1 | 1 | Variable | 2 |
  • +----+-----+-------+------+----------+----------+
  • CMD:一些配置标识,Shadowsocks 只用到了以下两种:
  • 0x01:建立 TCP 连接;
  • 0x03:关联 UDP 请求;
  • RSV:保留字段,值为 0x00
  • ATYPaddress type 的缩写,取值为:
  • 0x01:IPv4;
  • 0x03:域名;
  • 0x04:IPv6
  • DST.ADDRdestination address 的缩写,取值会随着 ATYP 变化:
  • ATYP == 0x01:4 个字节的 IPv4 地址;
  • ATYP == 0x03:1 个字节表示域名长度,紧随其后的是对应的域名;
  • ATYP == 0x04:16 个字节的 IPv6 地址;
  • DST.PORT 字段:目的服务器端口号。
    在收到请求后,Proxy 也会对应的返回如下格式的消息:
  • +----+-----+-------+------+----------+----------+
  • |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
  • +----+-----+-------+------+----------+----------+
  • | 1 | 1 | 1 | 1 | Variable | 2 |
  • +----+-----+-------+------+----------+----------+

REP 字段是用来告知 Client 请求的处理情况,正常情况下 Shadowsocks 会将其填充为 0x00,否则直接断开连接。其他的字段含义均同发送包的字段含义相同。
在万事具备之后,Socks5 协议就完成了自身的主要实名,在握手和建立连接之后,Socks5 的 Proxy 服务器就只做简单的消息转发。我们以通过 Shadowsocks 代理来访问 apple.com:80 为例,整个过程如下图所示:
visit-apple.com.re
而信息的传输过程可能是这样的:

  • # 握手阶段
  • # 无验证最简单的握手
  • client -> ss: 0x05 0x01 0x00
  • ss -> client: 0x05 0x00
  • # 建立连接
  • # b'apple.com' 表示 'apple.com' 的 ASCII 码
  • client -> ss: 0x05 0x01 0x00 0x03 0x0a b'apple.com' 0x00 0x50
  • ss -> client: 0x05 0x00 0x00 0x01 0x00 0x00 0x00 0x00 0x10 0x10
  • # 传输阶段
  • client -> ss -> remote
  • remote -> ss -> client
  • ...

Shadowsocks 模块划分

在真正深入到源码之前,先看看各个模块的主要功能划分:

  • .(shadowsocks)
  • ├── __init__.py
  • ├── asyncdns.py # 实现了简单的异步 DNS 查询
  • ├── common.py # 提供一些工具函数,重要的是解析 Socks5 请求
  • ├── crypto # 封装加密库的调用
  • │ ├── __init__.py
  • │ ├── openssl.py
  • │ ├── rc4_md5.py
  • │ ├── sodium.py
  • │ ├── table.py
  • │ └── util.py
  • ├── daemon.py # 用于实现守护进程(daemon)
  • ├── encrypt.py # 提供统一的加密和解密接口
  • ├── eventloop.py # 封装了 IO 常用方法 epoll, kqueue 和 select ,提供统一接口
  • ├── local.py # shadowsocks 客户端入口 - sslocal 命令
  • ├── lru_cache.py # LRU Cache,说白了就是限时缓存,过量删除的一种机制
  • ├── manager.py # 总控入口,用于组织组件逻辑
  • ├── server.py # shadowsocks 服务端 - ssserver 命令
  • ├── shell.py # shell 命令封装包
  • ├── tcprelay.py # 核心部分,实现整个 Socks5 协议,负责 TCP 代理部分
  • └── udprelay.py # 负责 UDP 代理实现

Shadowsocks 利用 Socks5 协议来进行数据传输,而增加的一个过程就是对 TCP 包的数据加密。这里我们称之为能爱国上网的 Socks5 代理:
patriotic-networ-2
在加密解密过程中,数据经过 sslocal 加密后转发给 ssserver,这是过程中最重要的环节。然后我们开始对细节进行剖析。

server.py 一个通往爱国的大门

我们从 server.py 这个入口函数开始看起,这样也便于把握整体代码的流程。
```python
def main():
# 配置代码
# 检测 python 版本
shell.check_python()
# 从命令行中获得配置参数
config = shell.get_config(False)
# 根据配置决定要不要以 daemon 的方式运行
daemon.daemon_exec(config)
# 端口加密模式输出 log
...
tcp_servers = []
udp_servers = []
# dns 服务器配置
if 'dns_server' in config: # allow override settings in resolv.conf
dns_resolver = asyncdns.DNSResolver(config['dns_server'],
config['prefer_ipv6'])
else:
dns_resolver = asyncdns.DNSResolver(prefer_ipv6=config['prefer_ipv6'])
# 将 port password 存入缓存,从配置字典中删除
port_password = config['port_password']
del config['port_password']
# 从配置中读入每一组配置信息
for port, password in port_password.items():

  • a_config = config.copy()
  • a_config['server_port'] = int(port)
  • a_config['password'] = password
  • logging.info("starting server at %s:%d" %
top Created with Sketch.