Shadowsocks: 一个安全socks5代理
1. 概述
Shadowsocks (后面简称ss)是一个基于SOCKS5的安全代理。
客户端 <---> ss-client <--[加密传输]--> SS-server <---> 受限主机
ss-client 扮演了传统上的SOCKS5代理服务的功能,ss-client加密并转发客户端的数据到ss-server,ss-server将会解密并转发数据到远端受限的主机。受限主机的响应将会被ss-server加密并转发给ss-client, 最后客户端解密并发送数据到发起请求的客户端。
1.1 ss的地址/寻址方式
代表ss服务的地址和SOCKS5的地址格式保持一致:
[1-byte type][variable-length host][2-byte port]
有三种类型的地址类型被定义:
- 0x01: host是一个4-byte的IPV4地址。
- 0x03: host是一个变长字符串。第一个byte是一个变长字符串长度, 接下来是一个最长255-byte的域名。
- 0x04: host是一个IPV6地址。
最后的2-byte port是一个大端模式的无符号整形(big-endian unsigned integer)。
1.2 TCP数据寻址
ss-client初始化一个TCP连接到ss-server, 然后ss-client发送加密的数据流到ss-server, 数据流格式如下:
[target address][payload]
以上内容会被客户端加密,加密由ss-server和ss-client配置的加密算法决定(双方算法必须保持一致)。
ss-server收到ss-client发送过来的加密数据,解密之后解析其中的目标地址,并与之简历一个新的TCP连接并转发payload到目标地址。ss-server随后会收到目标地址发送的响应,ss-server把响应内容加密并回传给ss-client,这个过程一致重复直到ss-client关闭连接。
1.3 UDP数据寻址
2. 数据流加密
数据流加密算法只提供数据保密性,而数据的完整性和正确性并不能保证。用户尽可能使用AEAD算法。
下面的算法提供了合理的保密性。
名称 | Key Size | IV Length |
---|---|---|
aes-128-ctr | 16 | 16 |
aes-192-ctr | 24 | 16 |
aes-256-ctr | 32 | 16 |
aes-128-cfb | 16 | 16 |
aes-192-cfb | 24 | 16 |
aes-256-cfb | 32 | 16 |
camellia-128-cfb | 16 | 16 |
camellia-192-cfb | 24 | 16 |
camellia-256-cfb | 32 | 16 |
chacha20-ietf | 32 | 12 |
2.1 数据流加解密
stream_encrypt是一个这样的加密函数:它接收一个密钥(secret key),一个初始向量(init vector),一条数据(message), 函数输出一条与数据想通长度的密文(ciphertext),过程表示如下:
stream_encrypt(secret_key, IV, message) => ciphertext
stream_decrypt是一个解密函数,它还原原始的数据(original message),过程如下:
stream_decrypt(secret_key, IV, ciphertext) => message
secret key可以是用户指定,也可以从一个(用户的)密码生成。secret key的生成遵从 OpenSSl里的 EVP_bytesToKe,详情可以参考 https://wiki.openssl.org/index.php/Manual:EVP_BytesToKey(3)
2.2 TCP
密文流由一个随机生成的IV紧跟着被加密的payload data组成:
[IV][encrypted payload]
2.3 UDP
[IV][encrypted payload]
3. AEAD 密码
AEAD, (这篇讲的更好点)代表 Authenticated Encryption with Associated Data。AEAD密码同事提供了保密性,完整性和真实性。这种算法在现代硬件上具有卓越的性能和效率。用户在任何时候应该尽量选择AEAD密码。
下表的AEAD密码是我们推荐的,相关的ss实现必须支持chacha20-ietf-poly1305
。在具备AES硬件加速的设备上的ss实现必须支持aes-128-gcm
, aes-192-gcm
和aes-256-gcm
。
Name | Key Size | Salt Size | Nonce Size | Tag Size |
---|---|---|---|---|
chacha20-ietf-poly1305 | 32 | 32 | 12 | 16 |
aes-256-gcm | 32 | 32 | 12 | 16 |
aes-192-gcm | 24 | 24 | 12 | 16 |
aes-128-gcm | 16 | 16 | 12 | 16 |
ss如何使用AEAD密码在 SIP004中做了规范,并在 SIP007中做了修订。
3.1 key的生成
主key可以是用户直接输入的,也可以从用户密码生成。生成算法依旧沿用OpenSSL的EVP_BytesToKey(3)
。
HKDF_SHA1是这样一个函数:接收一个secret key, 一个非保密salt, 一个字符串,最终输出一个密码学上来说高强度的subkey, 即使输入的secrey key比较弱。
HKDF_SHA1(secret key, salt, info_string) => subkey
info_string在应用上下文与subkey强绑定。
我们利用函数HKDF_SHA1利用预先双方共享的主key(我觉得可以简单理解为ss-client的密码)生成了每次会话的临时subkey, salt 需要保证在主key不变的情况下保持唯一性。
3.2 经验证的加解密 (Authenticated Encryption/Decryption)
AE_encrypt 是一个函数,它接收一个secret key, 一个非保密的一次性随机字符串(nonce),一条message, 最后输出密文和认证标签(authentication tag)。使用同一key的每次调用中,一次性随机串必须保持唯一(也就是对于同一个key调用函数时,nonce必须不一样)。
AE_encrypt(secret key, nonce, message) => (ciphertext, tag)
AE_decrypt是一个函数,它接收一个secrey key, 非保密一次性字符串(nonce), 密文(ciphertext), 认证标签(authentication tag),最终输出原始数据。如果任何一个数据被篡改,解密就会失败。
AE_decrypt(secret key, nonce, ciphertext, tag) => message
3.3 TCP
AEAD 加密的TCP数据流由一个随机盐值开头(用来生成per-session subkey), 后面跟随着任意数量的加密数据块,每个块拥有如下结构:
[encrypted payload length][length tag][encrypted payload][payload tag]
开头的payload length是2-byte的大端无符号整形,被限定在最大0x3FFF。最高两位保留,目前总是被设置为0。因此payload的最大长度为
3.4 UDP
一个AEAD加密的UDP包结构如下:
[salt][encrypted payload][tag]
salt被用来生成per-session的subkey,salt必须保证随机性和唯一性。每个UDP包使用salt生成的subkey和全0的nonce独立加解密。
4. 传输插件
4.1 架构概述
ss的插件和tor的 Pluggable Transport(简称PT)类似,但是和PT里的SOCKS5代理不同的是, ss的每个SIP003插件都作为一个tunnel(或者叫做local port forwarding)。这个设计的目的是为了避免PT里每连接参数(per-connection arguments), 从而使得ss的插件容易实现。
+-----------+ +--------------------------+ | SS Client +-- Local Loopback --+ Plugin Client (Tunnel) +--+ +-----------+ +--------------------------+ | | Public Internet (Obfuscated/Transformed traffic) ==> | | +-----------+ +--------------------------+ | | SS Server +-- Local Loopback --+ Plugin Server (Tunnel) +--+ +-----------+ +--------------------------+
4.2 插件的生命周期
和PT类似,插件的client/server作为ss client/server的子进程存在。
如果插件中有错误发生,那么作为插件的子进程就会退出(exit),并给出error code。然后ss父进程也会停止(SIGCHLD)。
当用户停止了ss client/server时,插件自然也被终止。
4.3 向插件传递参数
插件通过环境变量接收参数
- 4个必须参数环境变量为:
SS_REMOTE_HOST | 远端插件服务host |
---|---|
SS_REMOTE_PORT | 远端插件服务port |
SS_LOCAL_HOST | 本地ss服务或插件host |
SS_LOCAL_PORT | 本地ss服务或插件port |
- 1个可选环境变量 SS_PLUGIN_OPTIONS。如果插件有个性化参数需要传递可以放在这个环境变量里。值例如
obfs=http;obfs-host=www.baidu.com
,如果值里有分号和等号需要进行backslash转义。
4.4 与PT的兼容性
为了使用tor project的插件,有2种方法可行:
- fork tor的插件,按照SIP003进行修改
- 对PT进行适配以符合SIP003
4.5 插件的许可
所有插件必须运行在独立的进程中,插件开发者可以选择任何许可证。对插件提供者来说并不存在GPL许可的限制。
4.6 插件的限制
- 插件不支持互相串联。ss启动的时候只有一个插件被启动。如果真的想这样,那么就按照SIP003来实现一个plugin-over-plugin
- 只有TCP数据被转发,UDP目前还不支持
4.7 插件样例工程
- 流量混淆, SIP003 标准插件 simple-obfs
- 基于SIP003实现的ss https://github.com/shadowsocks/shadowsocks-libev
5. URL scheme
ss的url模式遵循 RFC3986标准
SS-URI = "ss://" userinfo "@" hostname ":" port ["/"] ["?"plugin] ["#" tag]
userinfo = websafe-base64-encode-utf8(method ":" password)
url最后的/必须加上,如果有插件。但如果只有tag(#开图),最后的/可以省掉。
例子:
ss://[email protected]:8888#Example1
ss://[email protected]:8888/?plugin=obfs-local%3Bobfs%3Dhttp#Example2
6. 官方ss具体实现
6.1 服务端实现
-
shadowsocks python
6.1.1 服务端功能比较
ss | ss-libev | ss-go | go-ss2 | |
---|---|---|---|---|
TCP fast open | Y | Y | N | N |
Multiuser | Y | Y | Y | N |
Management API | Y | Y | N | N |
Redirect mode | N | Y | N | Y |
Tunnel mode | Y | Y | N | Y |
UDP Relay | Y | Y | Y | Y |
AEAD ciphers | Y | Y | N | Y |
Plugin | N | Y | N | N |