shadow-socks白皮书

标签: shadowsocks 代理

Shadowsocks: 一个安全socks5代理

S.D.T
January 4, 2019

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]

有三种类型的地址类型被定义:

  1. 0x01: host是一个4-byte的IPV4地址。
  2. 0x03: host是一个变长字符串。第一个byte是一个变长字符串长度, 接下来是一个最长255-byte的域名。
  3. 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-gcmaes-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的最大长度为 $$ 2^{14}-1 $$ bytes。

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 向插件传递参数

插件通过环境变量接收参数

  1. 4个必须参数环境变量为:
SS_REMOTE_HOST 远端插件服务host
SS_REMOTE_PORT 远端插件服务port
SS_LOCAL_HOST 本地ss服务或插件host
SS_LOCAL_PORT 本地ss服务或插件port
  1. 1个可选环境变量 SS_PLUGIN_OPTIONS。如果插件有个性化参数需要传递可以放在这个环境变量里。值例如 obfs=http;obfs-host=www.baidu.com,如果值里有分号和等号需要进行backslash转义。

4.4 与PT的兼容性

为了使用tor project的插件,有2种方法可行:

  1. fork tor的插件,按照SIP003进行修改
  2. 对PT进行适配以符合SIP003

4.5 插件的许可

所有插件必须运行在独立的进程中,插件开发者可以选择任何许可证。对插件提供者来说并不存在GPL许可的限制。

4.6 插件的限制

  1. 插件不支持互相串联。ss启动的时候只有一个插件被启动。如果真的想这样,那么就按照SIP003来实现一个plugin-over-plugin
  2. 只有TCP数据被转发,UDP目前还不支持

4.7 插件样例工程

  1. 流量混淆, SIP003 标准插件 simple-obfs
  2. 基于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(#开图),最后的/可以省掉。

例子:

  1. ss://[email protected]:8888#Example1
  2. ss://[email protected]:8888/?plugin=obfs-local%3Bobfs%3Dhttp#Example2

6. 官方ss具体实现

6.1 服务端实现

  1. shadowsocks python

  2. shadowsocks-libev C

  3. shadowsocks-go Go

  4. go-shadowsocks2 Go

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

6.2 客户端

6.2.1 客户端功能对比

build:   __BUILD_VERSION__