切换深浅色
第26章 Shadowsocks:从加密转发流到2022版
26.1 地址头与转发流:它没有把整个SOCKS5握手搬到远端
Shadowsocks通常与一个本地SOCKS入口配合使用,但本地入口协议和两端代理之间的协议必须分开。浏览器与本地程序协商SOCKS方法,指定目标;本地程序把目标地址和应用字节放进Shadowsocks的加密传输。远端无需再次接收浏览器的05 01 00方法列表,也不靠一份完整SOCKS CONNECT回复组织返回流。
本章将三代格式分开:历史流密码格式、通常称为2017 AEAD的格式,以及SIP022定义的Shadowsocks 2022。规范材料固定于shadowsocks/shadowsocks-org提交34598d65054dad975d330ff9d7317b0d41cf1efd(2026-07-12);涉及具体实现时使用前述Xray提交0888beb。协议名相同不代表这些代际能够互通。
传统请求解密后的逻辑内容从目标地址开始:
text
地址类型1 || 地址(可变长) || 端口2(大端) || 应用字节……这里沿用SOCKS5的地址编码:IPv4类型01后接4字节地址,域名类型03后接1字节长度和域名字节,IPv6类型04后接16字节地址。它仅复用了地址结构,没有前面的SOCKS版本、命令和保留字节。
例如example.com:443后接hello,共20字节:
text
03 0b 65 78 61 6d 70 6c 65 2e 63 6f 6d 01 bb 68 65 6c 6c 6f
|------------- 地址与端口15字节 -------------| |---5字节---|十六进制中的03与0b属于结构,example.com占11字节,01 bb表示443。IPv4地址头只有7字节,IPv6地址头19字节,域名地址头则是4加域名字节数。这与VLESS无Addon时还要携带版本、UUID、命令等字段的设计不同。
传统TCP响应的解密内容直接是目标返回的应用字节,通常没有再重复目标地址。一个代理TCP连接对应一个目标TCP会话,两方向各有加密状态。要同时访问多个目标,通常建立多个这样的连接,或在外部另加复用机制;地址头本身没有HTTP/2式流号。
最早的流密码格式在每个方向先发送IV,再发送等长密文。以历史aes-128-ctr的结构为例:16字节IV之后是对目标头及应用字节连续加密得到的密文。随机IV使相同明文不必总对应相同开头,但这套格式本身没有AEAD Tag。能隐藏内容与能检测篡改是不同性质;CTR密文中的可控改位会改变相应明文字节,接收方没有天然的认证失败信号。
官方已将流密码文档标为历史教育用途,并明确弃用这类缺乏完整性与真实性保护的方案。本章讨论它,是为了理解后来为什么要增加认证长度块、认证负载块及重放约束,而不是把旧方法列为现行可选推荐。固定Xray的旧Shadowsocks包也只在其getCipher分支列出若干AEAD方法,不能因为协议历史存在,就假定这个版本仍支持全部旧流密码。
2017 AEAD保留“目标地址在解密请求开头”的思路,却把运输改成带认证的分块。2022又增加独立握手头、时间和请求关联。后续必须按各代真正的记录层读取,不能找到一个正确地址头就忽略外面的密钥、长度和Nonce状态。
历史结构见固定stream.md;传统地址及请求/响应处理见Xrayprotocol.go,其实际支持方法见config.go。
26.2 2017 AEAD TCP:长度和内容分别认证
TCP只提供字节流,接收端必须知道下一段密文有多长才能验证并解密。直接公开长度虽然容易解析,却会把应用分块长度暴露在这一层。2017 AEAD格式把长度自身也加密认证:先处理固定18字节的长度密文,再根据解出的长度读取下一块内容密文。
一个方向的完整结构是:
text
Salt S字节
[长度密文2 || 长度Tag16] [内容密文L || 内容Tag16]
[长度密文2 || 长度Tag16] [内容密文L || 内容Tag16] ……Salt是公开随机值,用于从共享主密钥派生本方向子密钥;不是IV与Nonce的可随意互换名称。aes-128-gcm的Salt为16字节,aes-256-gcm和chacha20-ietf-poly1305为32字节;这三个方法的Nonce都是12字节,Tag都是16字节。对端从配置预先知道方法,报文开头没有再用一份明文算法列表进行协商。
长度明文是2字节大端整数,上限0x3fff,即16383;最高两位必须为0。它数的是下一内容块的明文字节,不包括Tag。接收端读取18字节、用当前Nonce验证长度,成功后才按L+16继续读取并验证内容。认证失败不能把尚未可信的长度当作合法数据继续转发。
每个方向的Nonce是独立的96位小端计数器,第一项从全0开始,每次AEAD操作后加1。因此第一个长度块用0,第一个内容块用1;第二个长度块用2,第二个内容块用3。Nonce没有逐块显式写在网络上,接收方靠相同的操作顺序维护它。不是“一个分块只增长一次”,更不是沿用VLESS Encryption的大端预增计数规则。
| AEAD操作 | 长度明文 | Nonce的前几个字节 | 本次输出长度 |
|---|---|---|---|
| 首块长度 | 00 14(20字节) | 00 00 00… | 18 |
| 首块内容 | 目标头与hello共20字节 | 01 00 00… | 36 |
| 第二块长度 | 00 05(假定5字节) | 02 00 00… | 18 |
| 第二块内容 | 5字节应用数据 | 03 00 00… | 21 |
构造一次aes-256-gcm请求,若26.1节20字节内容放进同一个块,线上Shadowsocks字节为Salt32+长度块18+内容块36=86字节。它不含TCP/IP头,也不含服务器回应。若应用或实现拆成更多块,每新增一个内容块会新增一对长度/内容Tag及2字节长度,总固定增加34字节;因此记录拆分会影响开销。
TCP分段不是这些认证块的边界。一段TCP可以只带长度密文的前几字节,接收端必须等待;一段也可以合并多个已完整分块。AEAD块提供密码校验和解析边界,TCP提供有序运输;两者共同工作,不能用一次Socket.Read返回多少字节决定L。
响应方向使用自己的Salt和子密钥,从Nonce0重新计数,解密结果直接是目标返回数据。不同Salt使两方向不必共享相同的AEAD密钥/Nonce组合;但传统格式没有2022版那种明确的响应类型、时间与请求Salt关联头,其安全和重放边界还需另行讨论。
最后,Tag验证只能说明这块数据在该密钥和Nonce下通过认证。TCP截断若恰好发生在完整块之后,基础AEAD块格式没有一条必然存在的认证结束消息来证明后面没有被删掉的内容。网站TLS关闭通知、HTTP Content-Length等上层结束条件仍有各自作用。
格式基线见固定aead.md;Xray的AEAD包装与Nonce调用见config.go及common/crypto/auth.go。协议允许的最大块长不表示所有实现每次都生成这么大的块。
26.3 随机Salt为什么不能带来前向保密
2017格式有两级密钥。长期共享的主密钥K来自配置;每个方向发送的Salt再决定该方向的子密钥Ks。对于传统口令输入,常见兼容路径沿用基于MD5链的EVP_BytesToKey式处理,而不是把口令原始字符直接作为AES-256的32字节密钥。
固定Xray对所列16/32字节方法采用这样的链:D1为MD5(口令字节),D2为MD5(D1||口令字节),按需要连接D1、D2取得主密钥。16字节方法取D1,32字节方法取D1||D2。这里的MD5链计算很快,没有现代口令派生常用的高内存或高迭代成本;一段容易猜测的口令不会因它变成长密钥就获得同等熵。
在取得主密钥后,2017子密钥按HKDF-SHA1派生,info是ASCII字符串ss-subkey:
text
PRK = HMAC-SHA1(key=Salt, data=K)
T1 = HMAC-SHA1(key=PRK, data="ss-subkey" || 01)
T2 = HMAC-SHA1(key=PRK, data=T1 || "ss-subkey" || 02)
Ks = (T1 || T2 || …)的前k字节k是方法要求的密钥字节数。16字节子密钥只需截取T1;32字节则取T1的20字节与T2的前12字节。这一过程与TLS的HKDF-Expand-Label结构不同,不能擅自添加tls13前缀,也不是用Salt作为AEAD的逐记录Nonce。
Salt保证不同会话有机会获得不同子密钥,Nonce计数器保证同一子密钥下每次操作使用不同Nonce。二者分别解决密钥区分和密钥内的Nonce唯一性。若同一主密钥下Salt重复,两条方向流会导出同样子密钥,计数器又都从0开始,产生危险的密钥/Nonce复用。因此规范要求Salt在该主密钥整个使用期内保持唯一;随机生成只是实现这个目标的方法,碰撞概率仍依Salt位数和使用量增长。
对于独立均匀b位Salt,q次生成时发生至少一对碰撞的近似概率,在概率很小时约为q(q−1)/2^(b+1)。这个公式不证明某实现随机源质量,也不替代重复检测。它还说明,128位Salt与256位Salt不能只用“都是随机字符串”判断为完全相同的规模边界。
Salt不是秘密。观察者可以记录Salt和密文;如果之后获得主密钥K,就能重算Ks,并从Nonce0起解开历史流。因此没有前向保密。每次换Salt、每包换Nonce、删除某个会话Ks,都无法阻止持有长期K的人重新派生它。要改变这个性质,需要引入不能从长期K和公开历史材料恢复的秘密来源,而不是再增加一个公开随机字段。
同样不能把HKDF描述成能让弱口令“变得不可猜”。攻击者若能枚举口令,便可按MD5链、Salt和HKDF导出候选Ks,再用截获的AEAD Tag验证猜测。HKDF用于从已有秘密和上下文派生密钥,不是缓慢的口令猜测防护函数。SHA1的普通碰撞问题也不等于这里的HMAC立即失效;分析应针对实际构造,而不是按散列名字直接跳到安全结论。
这些区别为2022版的变化提供了理由:直接要求足够熵的固定长度PSK,换用明确的BLAKE3派生上下文,同时增加重放与方向约束。不过2022仍以PSK和公开Salt派生会话密钥,故也没有由此获得前向保密。
2017派生与口令兼容路径见固定Xrayconfig.go,HKDF定义见RFC 5869;2022密钥输入要求见SIP022第2节。
26.4 2017 AEAD UDP:每个数据报独立派生和认证
UDP已经提供数据报边界,不需要先解一个2字节长度块再知道报文结束位置。2017 AEAD UDP因而采用另一种结构:
text
随机Salt S || AEAD密文[地址类型、地址、端口、UDP负载] || Tag16每个数据报重新生成Salt,按相同HKDF规则导出自己的子密钥,并使用全0的12字节Nonce完成一次AEAD操作。全0并不违反“同一密钥下Nonce不可重复”的条件,前提是Salt唯一使子密钥也按设计分开;若Salt在同一主密钥下重复,Nonce也再次为0,风险就出现了。不能仅比较不同包的Nonce数值而忽略它们所用的密钥。
例如把26.1节目标头15字节和hello5字节当作一个UDP负载构造,aes-256-gcm的Shadowsocks报文长度为32+15+5+16=68字节。与同样20字节逻辑内容的TCP单块86字节相比,少了18字节认证长度块。但UDP还有自己的不可靠交付、路径MTU和应用恢复问题;这18字节差值不能独自证明哪种承载更快。
目标头属于每个数据报的加密内容。客户端请求中它指定要发到哪个UDP目标;响应中相应地址字段描述返回数据所关联的目标端点,客户端再把地址语义与负载交给本地UDP代理逻辑。不同于传统TCP响应,不能在UDP回程无条件省掉地址头,因为同一个客户端关联可能需要辨别来自不同目标的数据。
本地SOCKS5 UDP头与此结构也不是同一个东西。浏览器或应用交给本地入口的数据可能先有RSV、FRAG、ATYP等SOCKS字段;本地程序解析后再形成Shadowsocks目标头。Shadowsocks线上格式没有因此自动带上完整SOCKS保留字和分片字段。内层地址格式相近,外层会话与转发责任仍不同。
丢失一个2017 UDP数据报不会推进或破坏后一个数据报的AEAD计数,因为它们本来各用独立Salt和Nonce0。接收端可以独立验证后到的包,但也不因此得到有序交付、可靠重传或接收确认。若应用要求这些性质,需要由应用或另一层协议提供。DNS可能自行重试,实时媒体可能接受部分丢失,都不是基础Shadowsocks AEAD替它们完成了可靠性。
重复一个原封不动的有效数据报,Tag仍然有效。2017基础格式没有2022的会话ID、包ID与滑动窗口,故单凭AEAD无法识别“这是一个旧包被再次发送”。某些实现增加Salt去重等措施,具体缓存、生命周期和接收方向应查实现;不能把它们当作这份报文结构天然携带的防重放证明。
UDP报文再加IP/UDP头后还要受路径MTU限制。假设目标地址头15字节、Salt32、Tag16,不计其他隧道包装,Shadowsocks层就已比应用负载多63字节;一份原本接近路径可用上限的应用数据可能需要减小,或遭遇分片、丢弃和相关恢复。IPv4、IPv6及额外承载的空间不同,不能把某个固定“最大UDP负载”推广到所有部署。
固定Xray的EncodeUDPPacket先添加随机Salt、地址和负载,再由AEADCipher完成保护;解码验证后恢复目标及UDP语义。该实现使用自己的缓冲区,故协议在字节层允许的大小与具体读写路径能接纳的大小还须分开。UDP格式来自固定aead.md,对应路径见protocol.go和config.go。
26.5 Shadowsocks 2022:独立的头、派生与UDP会话
2022版不是给2017报文换一个AEAD名字。它改变密钥输入、TCP开头、响应关联、最大块长和UDP结构。若一端按2017方式读取首个18字节长度块,另一端却发送2022固定头,两端即使都使用AES-GCM也不能互通。
SIP022要求直接提供足够熵、固定长度的PSK,禁止继续从普通口令派生密钥。Base64只是这些原始字节的配置表示。必需方法2022-blake3-aes-128-gcm和2022-blake3-aes-256-gcm分别使用16、32字节PSK,TCP Salt与PSK等长。子密钥使用BLAKE3的derive_key模式,固定上下文为shadowsocks 2022 session subkey,输入材料为PSK原始字节连接Salt,输出所需16或32字节。它不是普通BLAKE3散列,也不是第22章所用动态上下文。
TCP请求先发送Salt,再发送两个分别认证加密的头块:
| 头块 | 明文字段 | 明文长度 |
|---|---|---|
| 请求固定头 | 类型00、Unix秒时间戳8大端、下一头块长度2大端 | 11 |
| 请求可变头 | SOCKS格式目标地址与端口、Padding长度2大端、Padding、首批应用数据 | 可变 |
| 响应固定头 | 类型01、时间戳8大端、本请求Salt S、首批负载长度2大端 | 11+S |
请求固定头的长度指向整个可变头块,包含目标、Padding长度字段、Padding和初始负载。两块各加16字节Tag。客户端方向Nonce为12字节小端计数,固定头用0,可变头用1;随后普通长度/负载对从2、3继续。基础块最大明文长度扩展为65535,不再是2017的16383。
后续普通数据仍按“认证长度2+Tag16,再认证负载L+Tag16”组织。请求可变头已经携带了它自己的目标和首批应用数据,不能再无条件给这块前面添加2017式长度块,否则会多解释一次结构。
响应使用新的Salt和子密钥。响应固定头在Nonce0下加密,其中包含客户端请求Salt,客户端必须核对它确实对应这次请求;首批响应负载用Nonce1。响应固定头已经提供首批长度,不另加一份首块长度密文。后续再按普通长度/负载成对推进。这样响应不再只是一个没有请求关联字段的独立加密流。
请求不能既没有初始负载、又没有Padding;这种情况须拒绝。Padding长度可为0到900,但首批数据为空时必须添加非零长度Padding。头及Padding必须在规定头块内结束,不能借一个很大的Padding长度越过已认证的块边界。规范还要求把Salt和头缓冲为一次Socket写入,以减少可预测写入形状;一次Write仍不保证网络只产生一个TCP段。
对于读取Salt及固定头,SIP022另外规定单次读取和统一处理失败可观察行为,以避免探测者根据逐字节输入后何时关闭推断解析器消耗量。这是入口抗探测策略,不是对TCP短读保证的改写:合法TCP仍可短读,具体实现必须处理这种情况下的接受策略或失败表现,不能宣称协议让短读不再发生。固定Xray的相应路径及失败关闭策略可在stream.go和ResetTCPConn定位。
UDP的改动更大。必需AES方法先形成16字节分离头:8字节会话ID加8字节大端包ID,并用PSK的AES单块运算隐藏它;正文另外用会话子密钥AEAD保护。
text
线上UDP = AES_PSK[会话ID8 || 包ID8]
|| AEAD_Ks[主头 || UDP负载] || Tag16
Ks = BLAKE3派生(固定session上下文, PSK || 会话ID8)
Nonce = 分离头原始明文的字节[4:16]Nonce因此是会话ID后4字节连接完整包ID8,不能拿已加密的分离头截取Nonce。每个会话的包ID从0递增,接收端可由分离头恢复所需状态;包无需像TCP那样先把前一包交付,才能知道当前Nonce。
| UDP正文方向 | AEAD保护的主头,之后接应用负载 |
|---|---|
| 客户端到服务端 | 类型00、时间8大端、Padding长度2大端、Padding、SOCKS地址与端口 |
| 服务端到客户端 | 类型01、时间8大端、客户端会话ID8、Padding长度2大端、Padding、SOCKS地址与端口 |
服务器回程生成自己的会话ID和包ID,不得直接把客户端会话ID当作自己的ID。正文额外回显客户端会话ID,把这组回程状态关联到客户端原会话。服务端依据客户端会话ID路由,记录最后一个通过验证的客户端地址,使会话有机会跨客户端网络地址变化继续;这不是无条件穿越任何NAT或防火墙的保证。
因此2022的UDP不必像2017那样每个包重新生成16/32字节Salt并重新建立完整独立状态,但要保存会话及包号窗口,并承担超时清理。减少某些重复派生与传输字段,换来更明确的状态管理和重放规则;既没有把UDP变成可靠流,也没有加入公钥前向保密交换。
SIP022还定义可选ChaCha系列方法。其TCP可替换相应AEAD;2022-blake3-chacha20-poly1305的UDP却改用24字节随机Nonce和直接PSK的XChaCha20-Poly1305,会话ID、包ID并入被加密主头,不能套用上述AES分离头公式。支持哪种可选方法必须查实现,不能把所有名字都视为同样的包格式。
规范基线见固定SIP022。与之对应的Xray结构可读kdf.go、stream.go、packet.go和cipher.go;规范要求与这些局部源码对应关系不等于已经完成全部互通合规测试。
26.6 防重放需要状态,身份扩展解决另一类问题
2022版的TCP头把时间、方向和请求关联纳入认证内容,但时间本身仍不足以防止短时间内重复。服务端须先验证解密头及其时间,超过当前时间30秒的消息按规范作为重放拒绝;对仍在时间范围内的请求,再检查Salt是否已出现,并保存接收的Salt至少覆盖规定的60秒记录需求。
这两部分分工明确:时间限制旧报文何时还可能有效,Salt集合阻止窗口内同一请求再次成立。只检查时间,会接受短时间重复;只在有限时间保存Salt而不检查时间,清理后很旧的请求又可能有效。时钟严重偏差会让合法请求失败,这也是协议状态依赖,而不是AES-GCM解密能力的问题。
固定Xray的TCP入口使用60秒间隔的MapFilter,内部持锁完成查重与插入,并维护两代集合。清理按检查触发,集合按代轮换,不是为每个Salt设置恰好60秒后执行的独立定时器。规范还禁止使用可能误报的Bloom过滤器作为2022的Salt存储。Bloom节省空间却可能误拒绝新连接,在这里不是可随意替换的同等语义。
TCP响应头中的类型01和请求Salt回显又提供另一个约束:客户端必须确认这份响应关联到自己的当前请求。方向标识使请求不能直接按响应语义使用,Salt关联防止把另一条独立响应挪过来冒充。它们与服务端请求Salt去重各自覆盖不同问题,不能删掉一个后声称还有Tag就足够。
UDP不能按到达顺序简单递增一个隐式接收计数,因为网络允许乱序。2022让包ID显式进入可恢复的头,再给每个会话维护滑动窗口。窗口记录一定范围内哪些包ID已经接收:新的较大ID推进窗口,窗口内未见过的较小ID仍可接收,重复或过旧ID拒绝。它防止重复交付,不负责等待缺失ID,更不替丢包重传。
例如某会话依次收到ID 10、12、11、12,前三个在允许窗口内且验证通过时可以接收;第二次12应拒绝。若来了一个伪造的极大包号,不能在其AEAD与主头语义验证前就永久推进窗口,否则攻击者可能把随后合法包挤成“过旧”。SIP022明确要求成功验证头之前不更新窗口。固定Xray相关路径将检查与最终Add分开,具体并发调用的完整性质仍需结合调度审查;这里只确认其字段和所列调用顺序,不冒称完成并发防重放证明。
会话状态还必须保存足够久。规范要求UDP关联至少记忆60秒,以免旧会话过早遗忘后被重复创建;回程也需考虑服务器重启产生新服务端会话ID的情况。状态被重启清空、集群节点各自维护窗口、同一PSK请求被分散到不共享集合的实例,都会改变整体防重放边界。规范中的完整重放保护是在相应状态和接受规则成立的前提下,不是仅靠密文自身自动发生。
多用户身份属于另一项需求。若一台服务器共用一个端口,却想为不同用户使用不同PSK,需要有效地找到应使用哪一把密钥。SIP023在2022基础上添加可扩展身份头EIH;它不是SIP022每个基础请求都固定携带的UUID字段。
TCP的EIH位于Salt与AEAD头块之间,每层16字节。当前层iPSK与Salt经固定上下文shadowsocks 2022 identity subkey派生身份子密钥,用AES单块加密下一层PSK的BLAKE3散列前16字节。接收者解开后查散列表,确定下一用户或下一中转目标,移除自己处理的身份头,再传递余下结构。最终协议头和应用内容使用最后一层用户PSK的会话子密钥保护。
这个设计支持“中转知道下一层、最终服务器区分用户”,但不能把身份头的AES单块加密单独当成AEAD认证。有效性仍要结合后面的受认证协议内容与各层接受规则判断;知道某层身份密钥,也不自然等于知道最终用户PSK。多层身份材料同样没有增加临时前向保密交换。
UDP EIH位于分离头与正文密文之间,按当前iPSK加密“下一层PSK散列前16字节异或原始分离头”。每层处理者还需把分离头解密并以下一层PSK重新加密;不能仅剪掉16字节就把原来的加密分离头原样转发。TCP和UDP的身份处理因此具有不同的数据路径。
以上要求见固定SIP022及SIP023。实现证据为Xraymapfilter.go、replay.go、packet.go与身份派生。
26.7 两份可复算的密文,和它们不能证明的流量性质
下面用公开构造材料把2017与2022的TCP起始字节分别算出。它们是本书独立计算的说明例,不是官方测试向量或实际抓包。两例都使用AES-256-GCM,原始共享密钥取递增字节00到1f,Salt取20到3f;这些材料完全公开,不能作为实际秘密密钥。
text
K = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
S = 202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f2017例直接把K作为主密钥输入,不走口令MD5转换。HKDF-SHA1得到32字节子密钥:
text
a4c3fd751f89596fa35314637662a00db30339ceeff662c094023a217a476417目标example.com:443与hello合为20字节首块,长度明文为00 14。Nonce0保护长度,Nonce1按小端表示为01后接11个00。下面两行分别是完整18字节长度密文含Tag、完整36字节内容密文含Tag:
text
c3c6390611251358d00f4f1d8da86bbd9562
cd76d042b06842cc2ec7c3a990d5ff88c16d85eadcd40923c4b7fe98859f3392bb955d5c前面再接32字节S,就是完整86字节起始流。观察者能数出字节和传输时序,却不能仅从这份密文直接读到example.com;持有K的人则能从公开S重算子密钥。这里没有握手临时秘密,所以长期K泄漏后的历史可解密性也可直接从计算依赖看出。
2022例使用同样原始K/S,却因派生方法和上下文不同,子密钥变为:
text
374fca03e4dae7f998fd7e59c1edfcc8e3197f4db1c19ca1671be3b66a92ddda固定头用类型00、时间戳1700000000秒及可变头长度22。这个旧时间只为确定性构造,当前真实服务端应因时间差拒绝,不能用本例作为可重放的有效现行请求。可变头包含目标15字节、Padding长度00 00及hello5字节;有首批负载,故零Padding在规范上允许。
text
固定头明文11字节:00000000006553f1000016
可变头明文22字节:030b6578616d706c652e636f6d01bb000068656c6c6f两块依次使用Nonce0和Nonce1,输出如下27及38字节密文含Tag:
text
aa4efbd2198e0f750c826ce3c9f6838fc47ed1b6492ab3ee99cab7
e906b5d46f86e2a0c6ab7b88332a973b30190955be6c74a5c569f4dc65f624b394123fa7e4e5加S后总长97字节。基础SIP022中本例没有身份头;若加入SIP023,每个TCP EIH另占16字节。不能把这97字节当成所有2022请求的固定长度:目标、首批负载、Padding及身份层都会改变它。
本书内部用手工HMAC展开与密码库HKDF核对2017子密钥,用BLAKE3 derive_key计算2022子密钥;分别加密、解密两块并验证改动Tag后拒绝。没有使用该计算代替Xray互通。固定Xray的2022写入路径在首批负载小于900字节时还会选择非零Padding,因此它不会按默认策略直接生成这里的零Padding示例。2017固定Xray又会在WriteTCPRequest先写地址头,随后另写hello;按两块计算本例可为120字节,而不是规范允许合并时的86。规范构造与具体Write路径必须分开计数。
随机外观也应这样审慎理解。AEAD密文的内容不应呈现可利用的明文规律,但网络流仍暴露IP、端口、连接寿命、字节量、方向、时间和失败行为。实现的首读、缓冲、Padding、错误关闭策略都可能形成可观察特征。“没有固定明文魔数”不能证明整条代理流与任意随机流量在所有探测方式下不可区分。
SIP022把抗探测作为设计目标,并规定相应写入和错误行为;这些是应遵守的机制,不是本书已完成的不可区分性实验。也不能从两例首批长度差11字节,就推断2022在所有网络上性能更差或更好:2017目标分块、2022首批数据合并和后续块的实际大小都会影响总成本。
派生与记录参数对应固定2017 AEAD规范、SIP022及Xray的传统请求写入、2022写入。
26.8 按代际比较身份、重放与信任
Shadowsocks把代理地址与应用数据放进共享密钥保护的通道,通常不另发一个VLESS式UUID认证头。接收端能验证合法AEAD内容,意味着发送者具有相应密钥能力,但不能据此区分持有同一PSK的多个人。共享一把密钥也共享其泄漏和冒充范围;换成更长的Salt不会为每个持有者建立独立身份。
| 机制 | 历史流密码 | 2017 AEAD | 2022基础版 |
|---|---|---|---|
| 内容完整性 | 基础格式没有认证Tag | 每块/数据报AEAD | AEAD,加明确头部语义 |
| TCP初始头 | 加密目标地址 | 目标在认证内容块中 | 独立固定头、可变头 |
| 方向与响应关联 | 无2022式字段 | 无2022式字段 | 类型与响应回显请求Salt |
| TCP防重放 | 基础格式不提供完整规则 | 需结合实现的额外措施 | 时间检查及规定Salt集合 |
| UDP防重放 | 基础格式不提供 | 基础AEAD本身不提供 | 时间、会话与包ID窗口 |
| 主要密钥来源 | 常见为口令兼容派生 | 传统主密钥/口令兼容派生 | 直接提供固定长度高熵PSK |
| 前向保密 | 不由该结构提供 | 不提供 | 不提供 |
SIP023再把身份或中转选择引入2022,但应与基础2022分开标注。它允许把最终用户密钥与入口身份密钥分离,而不是让一个裸UUID承担全部密码保护。知道入口iPSK的人不必知道每个用户uPSK;最终服务器根据身份头找到用户材料后,仍须处理并验证对应的受保护协议内容。
格式层面的密钥长度也不等于秘密的熵。固定Xray的ParseKey会先尝试标准Base64解码,失败时把输入当原始字节,再验证长度。这是它的配置兼容行为;它没有把短口令用MD5扩成2022密钥,也没有证明长度恰好16或32的可打印字符串具有足够不可预测性。规范要求的高熵仍取决于材料如何产生和保管。
与VLESS相比,二者最根本的区别是职责分配。基础VLESS明确定义身份标识、命令与目标,但把机密性及密码认证交给外部安全层或可选Encryption。Shadowsocks的代际格式直接把目标和内容放入自身的共享密钥加密结构。不能拿“裸VLESS”与“自带AEAD的Shadowsocks”比较后得出完整部署强弱,也不能忽略VLESS+TLS、REALITY或Encryption各自不同的秘密与信任模型。
PSK方案可以在TCP建连后直接发送受保护请求,不必先执行证书和临时公钥握手。因此它减少的是某类初始握手依赖,并非把任何路径的总RTT降成0。远端仍需收到目标、解析域名、建立目标连接,网站TLS也有自己的交互。2022若为了合并首批数据短暂等待,应用交互时机也会进入延迟;开销小与等待少不是同一项指标。
长期PSK泄漏后,可以从记录的Salt或UDP会话ID重算历史子密钥;拥有PSK的人也具备构造新合法流量的能力。2022的时间和窗口主要约束重放旧字节,不阻止已经持钥的人生成新时间戳、新Salt和新包号。它们不等于撤销凭据或证明持钥者的现实身份。
服务器对目标的可见性则与多数正向代理一致:它至少知道所请求地址,并在目标没有网站TLS等端到端保护时能读取应用明文。增加传输插件或把Shadowsocks放进其他隧道,会改变外观、额外信任和开销,应重新标出终结点。普通HTTP CDN不能凭借“支持HTTPS”就直接终结并理解一条任意原始Shadowsocks流;需要相容的HTTP承载或其他明确架构。
判断某个实际客户端的“SS节点”,至少要落实到具体方法及代际、密钥原始字节语义、TCP/UDP格式、防重放状态、是否使用EIH,以及外部承载。这些条件确定后,才可以讨论它与前面完整VLESS组合的安全和性能差异。下一章的VMess采用另一种身份和请求加密组织,不能从同为UUID配置就把它与VLESS视为同一格式。
本节密钥解析实现见固定kdf.go,身份扩展边界见SIP023。表中规范性质不等于本书完成了每个软件、所有并发和重启组合的审计。