Markey's home

Shadowsocks翻墙原理探究

2019/02/28 Share

Shadowsocks翻墙原理探究

Shadowsocks 是目前公认的科学上网神器,由 clowwindy 开发,它的出现极大地便利了广大程序猿们,让我们能够绕过 GFW 方便地访问 google 或 youtube 等国际大型网站。它的核心原理是基于 Socks5 代理协议的网络数据加密传输,具体在下面会做进一步介绍。

本文会以如下目录依次展开:

  • GFW
  • Socks5协议
  • Shadowsocks工作原理
  • Shadowsocks源码解析

GFW

全称是 Great Firewall of China,国内一般称作“防火墙”或俗称“墙”,正是因为这个东西的存在,让整个中国大陆区域内的网络受其控制,成为了众所周知的“局域网”。

防火墙采用技术手段主要有如下几种:

  1. DNS污染/劫持

    我们都知道在互联网中访问一个站点前,会先通过 DNS 解析将域名解析成对应的 IP,然后才能通过 IP 进行 HTTP 访问。而 DNS 劫持即在 DNS 解析阶段动手脚。由于 DNS 协议是基于 UDP 的协议,该协议具有无状态、不可靠传输的特点,因此只要是先收到了响应就会抛弃之后的响应。

    GFW 就利用了这个特性。它会在主要的 DNS 流量出口进行检测,若发现黑名单域名,就会伪装成域名服务器向客户端发回虚假回应,导致客户端请求到虚假的 IP。

  2. IP封锁
    客户端在解析得到 IP 后,向服务端请求的过程中会经过一系列路由的转发,在路由器转发的过程中会根据路由表中存储的表项来决定下一跳的路由器或主机,选择的下一跳地址会根据路由协议来决定。

    早期使用的是ACL(访问控制列表)来进行IP黑名单限制,现在更高效的路由扩散技术来进行对特定的IP进行封锁。

    早期路由器都是采用静态路由协议,每一条路由需要进行人工来配置路由表项,或者配置一些策略,在决定路由转发,这时可以通过检测,对相应要封锁的IP配置一条错误的路由,将之牵引到一个不做任何操作的服务器(黑洞服务器),此服务器所要做的就是丢包,这样便无声息封锁掉了。动态路由协议的出现可以更高效的进行屏蔽,动态路由协议可以让路由器通过交换路由表信息来动态更新路由表,并通过寻址算法来决定最优化的路径。因此可以通过动态路由协议的路由重分发功能将错误的信息散播到整个网络,从而达到屏蔽目的。

  3. IP端口黑名单
    该手段可以结合上边提到的IP封锁技术,将封锁精确到具体的端口,使该IP的具体端口接收不到请求,从而达到更细粒度的封锁。经常被封锁的端口如下:

    1. SSH的TCP协议22端口
    2. HTTP的80端口
    3. PPTP类型VPN使用的TCP协议1723端口,L2TP类型VPN使用的UDP协议1701端口,IPSec类型VPN使用的UDP协议500端口和4500端口,OpenVPN默认使用的TCP协议和UDP协议的1194端口
    4. TLS/SSL/HTTPS的TCP协议443端口
    5. Squid Cache的TCP协议3128端口
  4. 无状态TCP连接重置

    TCP三次握手

    TCP连接会有三次握手,此种攻击方式利用了该特点来进行攻击,gfw会对特定IP的所有数据包进行监控,会对特定黑名单动作进行监控(如TLS加密连接),当进行TCP连接时,会在TCP连接的第二部SYNC-ACK阶段,伪装成客户端和服务器同时向真实的客户端和服务器发送RESET重置,以很低的成本来达到切断双方连接的目的。与丢弃客户机的包相比,在丢包后客户机会不断的发起重试,这样会加重黑洞服务器的负担,利用TCP连接重置来断开连接,客户机也不必发送ACK来确认,这样成本就要低得多。

  5. TCP协议关键字阻断
    该手段在无状态TCP连接重置手段之上,加入了关键字过滤功能,当协议的头部包含特定的关键字便对其连接进行重置,比如HTTP协议、ED2K协议等等。

  6. 深度包检测
    深度数据包检测(Deep packet inspection,DPI)是一种于应用层对网络上传递的数据进行侦测与处理的技术,被广泛用于入侵检测、流量分析及数据挖掘。就字面意思考虑,所谓“深度”是相对于普通的报文检测而言的——相较普通的报文检测,DPI可对报文内容和协议特征进行检测。
    基于必要的硬件设施、适宜的检测模型及相应的模式匹配算法,gfw能够精确且快速地从实时网络环境中判别出有悖于预期标准的可疑流量,并对此及时作出审查者所期望的应对措施。


Socks5协议

Socks5协议是一种应用层的代理协议,在OSI七层模型中位于会话层

与Socks4相比,它多出了如下几点特性:

  • 比Socks4更加安全,增加了用户鉴权
  • 支持UDP协议
  • 地址方面支持域名及IPV6
  • SOCKS工作在比HTTP代理更低的层级
  • 工作方式更加透明,不会重写或解释报头

Socks5工作原理

参考论文

我们都知道计算机网络模型常用的有如下两种,理解了它们,有助于我们理解 Socks5 的工作原理:

网络模型

TCP/IP 模型的应用层对应 OSI 模型的前三层,链路层对应 OSI 的最后两层。计算机在网络中进行通信的时候,请求方、服务方都会有一个数据封装、解封的过程,如下:

1
2
3
4
5
6
7
8
9
10
11
          请求方                                   接收方
应用层 --> data data --> 应用层
| | ∧ ∧
∨ ∨ | |
传输层 --> segment segment --> 传输层
| | ∧ ∧
∨ ∨ | |
网络层 --> package package --> 网络层
| | ∧ ∧
∨ ∨ | |
链路层 --> frame >>传输>> frame --> 链路层

流程大概是应用层要发起一个请求会先将要传输的数据及协议(即 data)传递给传输层,传输层收到后,会将数据分片(segment),并加上 TCP/UDP 的协议头,之后在进入网络层时再将传输层的分片加上 IP 头,最后进入链路层加上帧头和帧尾完成数据的封装,开始传递。

在接收端则是完全相反的一套流程,就不再赘述。

而 Socks5 则是属于 TCP/IP 模型中应用层协议,用 Socks5 请求的流程大概如下:

  1. 在应用层先将数据添加 Socks5 头部,发送给传输层
  2. 传输层将 Socks5 协议数据分片,添加 TCP/UDP 头部分发给网络层
  3. 网络层将 TCP/UDP 协议数据添加 IP 协议头,发往链路层
  4. 链路层添加帧头与尾,将数据封装成帧发往接收方

基于TCP的Socks5

参考维基百科

Socks5协商:即客户端与服务端确认验证方式的一次交互。

先由客户端发起第一次握手,进行版本、方法的选择:

1
2
3
4
5
+----+----------+----------+
|VER | NMETHODS | METHODS |
+----+----------+----------+
| 1 | 1 | 1 to 255 |
+----+----------+----------+
  • VER是SOCKS版本,这里应该是0x05;
  • NMETHODS是METHODS部分的长度;
  • METHODS是客户端支持的认证方式列表,每个方法占1字节。当前的定义是:
    • 0x00 不需要认证
    • 0x01 GSSAPI
    • 0x02 用户名、密码认证
    • 0x03 - 0x7F由IANA分配(保留)
    • 0x80 - 0xFE为私人方法保留
    • 0xFF 无可接受的方法

服务端应当在客户端提供的方法中选择一个,并返回:

1
2
3
4
5
+----+---------+
|VER | METHODS |
+----+---------+
| 1 | 1 |
+----+---------+

认证结束后,客户端就可以继续发起对请求信息的握手,格式如下:

1
2
3
4
5
+----+-----+-------+------+----------+----------+
|VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
+----+-----+-------+------+----------+----------+
| 1 | 1 | 0x00 | 1 | Variable | 2 |
+----+-----+-------+------+----------+----------+
  • VER是SOCKS版本,这里应该是0x05;
  • CMD是SOCK的命令码
    • 0x01表示CONNECT请求
    • 0x02表示BIND请求,指双向连接,比如FTP协议,一个连接用于发送命令指令,另外一个连接用于传输数据
    • 0x03表示UDP转发
  • RSV 0x00,保留
  • ATYP DST.ADDR类型
    • 0x01 IPv4地址,DST.ADDR部分4字节长度
    • 0x03 域名,DST.ADDR部分第一个字节为域名长度,DST.ADDR剩余的内容为域名,没有\0结尾
    • 0x04 IPv6地址,16个字节长度
  • DST.ADDR 目的地址
  • DST.PORT 网络字节序表示的目的端口

服务端收到后,应当返回对应的应答:

1
2
3
4
5
+----+-----+-------+------+----------+----------+
|VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
+----+-----+-------+------+----------+----------+
| 1 | 1 | 0x00 | 1 | Variable | 2 |
+----+-----+-------+------+----------+----------+
  • VER是SOCKS版本,这里应该是0x05;
  • REP应答字段
    • 0x00表示成功
    • 0x01普通SOCKS服务器连接失败
    • 0x02现有规则不允许连接
    • 0x03网络不可达
    • 0x04主机不可达
    • 0x05连接被拒
    • 0x06TTL超时
    • 0x07不支持的命令
    • 0x08不支持的地址类型
    • 0x09 - 0xFF未定义
  • RSV 0x00,保留
  • ATYP BND.ADDR类型
    • 0x01 IPv4地址,DST.ADDR部分4字节长度
    • 0x03域名,DST.ADDR部分第一个字节为域名长度,DST.ADDR剩余的内容为域名,没有\0结尾
    • 0x04 IPv6地址,16个字节长度
  • BND.ADDR 服务器绑定的地址
  • BND.PORT 网络字节序表示的服务器绑定的端口

在方法、请求握手完成后,还可以进行用户名、密码认证,此处就不在赘述,可上维基百科中查阅。


Shadowsocks工作原理

终于说到Shadowsocks工作原理了

Shadowsocks的工作方式与普通的Socks5代理的不同之处在于,它是一个客户端-服务端模型,而普通的Socks5代理则是只有一个Socks代理服务器,如下图:

1
Socks5客户端  <---Socks5--->   Socks5服务器  <---正常请求--->  目标主机

Socks 5客户端在与Socks 5服务器交互的整个过程是有可能暴露在整个互联网中的,因此很容易被监控到,根据协议特征也可以很容易识别出来,若采取普通的Socks 5代理方式的话,若用于翻墙去看外边的世界,这种方式很容易被墙,代理服务器的IP极容易被加入黑名单,也就导致此代理的寿终正寝,因此一种新的方式Shadowsocks出现了。

而Shadowsocks则处理的更加巧妙,为了防止与代理服务器的交互过程暴露,需要在本地起一个Socks5服务,让客户端发起的请求都与本地Socks5服务进行交互,再经过数据加密,传输到代理服务器上去,代理服务器也通过Socks5协议进行解析,并向目标服务器发起请求。流程大致如下图:

1
2
3
4
5
6
7
Socks5客户端  <---Socks5--->   sslocal  

|
密文
|

ssserver <---正常请求---> 目标主机

其他方面基本都一样,主要的不同之处在于Shadowsocks将Socks5服务端拆解成两部分:

  • 本地Socks5服务:用于监听客户端发起的请求,对于客户端来说是完全透明的,就相当于Socks5服务器

  • 远程Socks5服务:用于监听本地发起的Socks5请求,解析请求、发送给目标服务器,并将相应返回给本地Socks5服务

  • 本地 - 远程:本地与远程的交互过程都是加密后进行的。本地将Socks5请求准备好后,先加密发送给远程Socks5服务,远程收到后也先进行解密再进行代理,之后将收到的响应数据包加密返回给本地


部分源码分析

Shadowsocks 原版是 python 写的,之后又不断涌现了 C++、GO 等多个版本,由于本人只对 js 比较熟悉,因此以 shadowsocks-js 为例

官方的 shadowsocks-nodejs 由于 nodejs 的内存占用问题,已经停止维护了,没有以它为例,本例可以说是它的一个替代方案。

主要功能代码都在 lib 目录下:

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
26
27
28
29
30
31
32
.
├── bin
│   ├── localssjs // 本地服务启动命令
│   └── serverssjs // 远程服务启动命令
├── config.json // ss配置
├── lib
│   ├── auth.js // Socks5用户校验相关方法
│   ├── cli.js // 命令行启动工具
│   ├── createUDPRelay.js // UDP代理相关
│   ├── daemon.js // 守护进程相关
│   ├── defaultConfig.js // 默认ss配置
│   ├── encryptor.js // 加密、解密相关方法
│   ├── filter.js
│   ├── gfwlistUtils.js // 防火墙相关方法,用于PAC切换
│   ├── logger.js
│   ├── pacServer.js // PAC服务
│   ├── pid.js
│   ├── recordMemoryUsage.js // 内存记录
│   ├── ssLocal.js // 本地Socks5服务
│   ├── ssServer.js // 远程Socks5服务
│   └── utils.js
├── logs
│   ├── local.log
│   └── server.log
├── node_modules
├── pac
│   ├── gfwlist.txt
│   └── user.txt
├── package.json
├── src
├── test
└── vendor

核心代码就只有 ssLocal.js、ssServer.js,下面会分别讲解其作用

ssLocal.js 只有400多行,还算比较容易看懂,其核心方法如下

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
* 处理 method 握手
*/
function handleMethod(connection, data, authInfo) {
...处理 method 握手响应头部
/**
* 返回响应
*/
connection.write(buf);

return method === 0 ? 1 : 3;
}

function fetchUsernamePassword(data) {
...获取用户名及密码,用于Socks5身份校验
}

function responseAuth(success, connection) {
...响应身份校验
}

function usernamePasswordAuthetication(connection, data, authInfo) {
...调用上述两方法,处理身份校验
}

/**
* 处理 request 握手,并链接远程Socks5服务器,添加事件监听
*/
function handleRequest(
connection, data, _ref,
dstInfo, onConnect, onDestroy,
isClientConnected
) {
...准备响应数据
if (isUDPRelay) {
...单独针对UDP请求处理
}
/**
* 将客户端发送的数据进行加密,用于之后传输给远程服务器
*/
tmp = (0, _encryptor.createCipher)(password, method, data.slice(3)); // skip VER, CMD, RSV
cipher = tmp.cipher;
cipheredData = tmp.data;

/**
* 连接远程服务器
*/
var clientToRemote = (0, _net.connect)(clientOptions, function () {
logger.warn('Local server has connected remote server.');
onConnect();
});

/**
* 收到远程服务器的响应后,先解密再传递给客户端
*/
clientToRemote.on('data', function (remoteData) {
...对远程服务器返回的数据解密,并返回给客户端
});

/**
* 完成 request 握手
*/
connection.write(repBuf);
}

/**
* 处理客户端与本地服务的链接,判断不同的阶段分别进行处理
* 0:method握手阶段
* 1:request握手阶段
* 2:数据传输阶段
* 3:Socks5用户校验阶段
*/
function handleConnection(config, connection) {
/**
* 处理来自客户端的请求,不同阶段根据不同方式处理
*/
connection.on('data', function (data) {
switch (stage) {
case 0: ...处理method握手
case 1: ...处理request握手
case 2: ...将数据传输给远程服务
case 3: ...处理用户校验
default:
}
});
}

function createServer(config) {
...启动本地服务,添加事件监听
}

ssServer.js 代码相对来说更简单,只有不到300行,核心方法只有三个

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 链接目标服务器,并将响应的数据加密返回给本地
*/
function createClientToDst(
connection, data, password,
method, onConnect, onDestroy,
isLocalConnected
) {
// 链接目标服务器
var clientToDst = (0, _net.connect)(clientOptions, onConnect);

clientToDst.on('data', function (clientData) {
...对目标服务器的响应加密,再返回本地服务
});
}

/**
* 处理本地服务发起的请求,根据不同阶段分别处理
* 0:未连接目标服务器
* 1:已连接目标服务器
*/
function handleConnection(config, connection) {
connection.on('data', function (chunck) {
...解密本地服务的数据

switch (stage) {
case 0: ...调用上一个方法链接目标服务器
case 1: ...将本地服务的请求数据发送给目标服务器
default:
}
});
}

/**
* 启动远程服务
*/
function createServer(config) {
...启动远程服务,添加事件监听
}
CATALOG
  1. 1. Shadowsocks翻墙原理探究
    1. 1.1. GFW
    2. 1.2. Socks5协议
      1. 1.2.1. Socks5工作原理
      2. 1.2.2. 基于TCP的Socks5
    3. 1.3. Shadowsocks工作原理
    4. 1.4. 部分源码分析