就想到会有人抬杠,时钟精度要求是为了防重放,自己蠢怪协议。这就属于懂一点,但又不多,然后急于炫耀「我比你懂」。
来仔细讲讲防重放是怎么回事:
什么是重放攻击?
重放攻击指链路上的攻击者,通过抓取留存客户端曾经发送过的数据包,稍后再次发送给服务端的行为。
对于翻墙协议来说,主要是通过重放观察服务端的响应,来判断是否是特定协议的代理服务器。
最早的 shadowsocks 原版协议,由于没有对数据进行完整性校验,所以通过构造几个微调过的数据包,观察服务端的反应,可以比较精准的判断是否为 shadowsocks 服务器。在升级为 AEAD 后,已经不存在该问题。
但是防重放攻击的问题就一直被社区记了下来,后续的各种协议均对重放攻击进行了严格防护。然而从我的观察来看,没有明确的证据证明,gfw 对其他协议有通过重放攻击进行探测并封锁。
事实上,绝大多数正常的协议,对于重放请求,要么是直接允许(如 HTTP),要么是产生一个明确的拒绝消息(如 TLS 会回退 1-RTT 的 Server Hello)。反而就只有翻墙代理协议会非常暴力的直接打断 TCP 连接,这反而构成了一种特征。
这些问题讨论起来很复杂也还有争议,我们先回到怎样防重放的问题上。
怎样防重放攻击?
很简单且常见的一种方式,就是客户端每次请求产生一个随机数(64 或 128bit,碰撞概率极低),然后服务端对于已经出现过的随机数,便认为该请求是被重放的,进入相应的处理流程。(请求是经过了强加密且有完整性保护的,没有密钥的攻击者无法修改该随机数)
很容易想到,服务器需要维护一个数据结构来存储已经出现过的随机数,这个数据结构不可能无限增大,于是有两种方案
- 限制数据结构总长度,先进先出,一段时间以前出现过的随机数就不管了。
这种模式下,可能几天前的数据包重放后无法被阻止,具体时间长度取决于内存占用和请求的数量。
- 要求客户端在产生随机数的同时,报告当前的时间戳,如果时间戳已经超过特定的范围(比方说 VMess 和 ss-2022 采用的 30s),则视为无效请求
那么,服务端只需保留最近 30s 内出现过的随机数即可。而且无论是多久以前的请求重放都是无效的。
所以实际上,时间戳的目的是帮助服务器节省内存占用。如果放宽时间限制到 24 小时,那么服务端也改为保留 24 小时内的随机数即可,效果上完全等价。
那么 30s 和 24h 的内存占用差多少呢?
假设服务器每秒接受 100 个新连接(这个标准已经不低了),使用 Bloom Filter 数据结构,24h 滚动存储的内存开销约为 30MB。
我相信 30MB 对于现代服务器来说,不是什么需要考虑的事情。所以即使使用时间戳限制的方案,30s 这个值也纯属拍脑袋决定的,放宽到 24h,就可以先解决 80% 的问题。
那么是否只有这两种方案呢?当然不是,比如最常见的 TLS 协议也支持防重放,且客户端完全不依赖时钟,通过 session ticket 实现,只是首次连接的开销是 1-RTT,稍微浪费了一点点。
(TLS 的证书有效期对时钟的要求,实际上是证书链校验系统的业务需求,而非协议本身的需求,如果是静态证书指纹验证,则完全和时钟无关)
总结
综上所述,使用 30s 的时间窗口去解决重放攻击问题,是一个非常暴力且业余的设计,旨在解决一个可能不存在的问题,让双端凭空增加了对时钟的依赖,实践中具体可能的坑有:
- 服务端和客户端均需要保证有 ntp 或其他方式保证时钟精准
- 时钟错误时,客户端看不到任何错误信息,一般用户难以发现是时钟不准导致的问题。
- 客户端必须存在精准时钟方可连接,如果一个设备不存在 RTC,那么开机后时钟需要依赖 ntp 初始化,而如果该代理又是该设备的唯一外网,则进入死循环。