13个版本
0.1.12 | 2023年12月12日 |
---|---|
0.1.11 | 2023年11月15日 |
#1 in #混淆
191 每月下载量
用于 4 个crate(直接使用3个)
78KB
1.5K SLoC
为sosistab2提供的混淆UDP传输
sosistab2-obfsudp
是一个基于UDP的混淆、不可靠、面向连接的协议,旨在作为sosistab2的后端“管道”使用。
它允许服务器监听管道,客户端连接到服务器并建立管道,使用类似于TCP的API,但管道本身携带的不可靠数据报文大小可达64 KB。
混淆的目的是实现
- 抵抗被动分析:给定一个数据包跟踪,很难确定哪些数据包是
obfsudp
数据包。 - 抵抗主动探测:给定一个
host:port
,以及对其所有数据包的观察,很难设计一个测试来确认是否托管了obfsudp
服务器。 - 抵抗数据包篡改和注入:上述两种情况即使在攻击者可以任意篡改和注入自己的数据包的情况下也成立。
规范
加密格式
obfsudp发送的所有UDP数据包都使用ChaCha20-Poly1305进行加密,看起来是均匀随机的,并隐藏原始数据包长度。
加密到密钥 k
的加密数据包如下所示
- 使用密钥
k
和密文n
加密的ChaCha20-Poly1305- 1字节:填充长度
padlen
padlen
字节:任意填充字节- 可变长度:消息
- 1字节:填充长度
- 12字节:随机密文
n
,为每个数据包单独选择
我们记为使用密钥 k
和随机密文加密消息 m
为 seal(k, m)
,解密为 open(k, m)
初始握手
初始握手帧使用从服务器的X25519公钥派生的密钥对称加密。这里的意图是只有知道服务器公钥 server_pk
(即不是ISP窃听者)的人才能派生这些密钥
hs_c2s
:客户端到服务器的握手密钥,通过以下方式派生:hs_c2s = derive_key("sosistab-2-c2s", server_pk)
,其中derive_key
是BLAKE3定义的密钥派生函数。hs_s2c
:服务端到服务器的握手密钥,通过以下方式派生:hs_s2c = derive_key("sosistab-2-s2c", server_pk)
。
握手帧是stdcode编码和加密的Rust枚举的表示
pub enum HandshakeFrame {
ClientHello {
long_pk: x25519_dalek::PublicKey,
eph_pk: x25519_dalek::PublicKey,
version: u64,
timestamp: u64,
},
ServerHello {
long_pk: x25519_dalek::PublicKey,
eph_pk: x25519_dalek::PublicKey,
resume_token: Bytes,
client_commitment: [u8; 32],
},
Finalize {
resume_token: Bytes,
metadata: String,
},
}
建立obfsudp
连接的握手有三个阶段:客户端你好、服务端你好和完成。
客户端你好
客户端获得两个X25519密钥对
long_sk
,long_pk
:一个“长期”密钥对,用于唯一标识客户端。如果客户端不需要通过此握手进行身份验证,则可以随机生成。eph_sk
,eph_pk
:一个临时密钥对。必须新鲜随机生成。
然后向服务器发送seal(hs_c2s, client_hello)
,其中client_hello
包含密钥对,version = 4
和timestamp
是当前Unix时间戳(以秒为单位)。
服务端你好
服务器响应一个包含ServerHello的消息
long_pk
:其长期、已知的X25519公钥eph_pk
:其临时的X25519公钥resume_token
:“cookie”,允许服务器重建所有初始化会话所需的信息。目前,这些数据如下,加密方式仅限于服务器自身解密sess_key
:通过客户端密钥和服务端密钥之间的三重ECDH派生的对称密钥timestamp
:当前Unix时间戳version
:设置为4
client_commitment
:blake3(client_hello)
会话密钥通过以下方式派生:sess_key = blake3_keyed(key = blake3(metadata), shared_secret)
,其中metadata
是ClientHello中的metadata
字段,shared_secret
是从三重ECDH密钥交换派生的共享密钥。
此时,服务器不会将信息保存到任何表格或类似的结构中。它会丢弃所有计算出的值,并继续处理更多传入的包。
服务器必须小心拒绝重放的ClientHello。这是通过以下方式完成的
- 记住最后60秒的ClientHello,并拒绝任何重复项
- 拒绝任何早于或晚于当前时间30秒的ClientHello
完成
客户端收到ServerHello并验证
long_pk
是预期的值client_commitment
与其自身匹配,计算方式为blake3(client_hello)
随后,它使用三重-ECDH计算会话密钥 sess_key
,并发送包含以下内容的 Finalize 消息:
resume_token
:来自 ServerHello 的resume_token
metadata
:与此连接相关的任意元数据。这通常用于指示哪个高级 sosistab2 Multiplex 这根管道属于。
此时,客户端和服务器已就相同的 sess_key
达成一致。双方现在推导出
up_key = blake3_keyed(key = "upload--------------------------", sess_key)
:上传密钥dn_key = blake3_keyed(key = "download------------------------", sess_key)
:下载密钥
“稳态”帧
一旦会话建立,双方都有了对方的主机:端口,以及上传对称密钥和下载对称密钥。他们现在发送格式为 (u64, SessionFrame)
的帧,以正确的密钥(例如,一个上传消息 m
将以 seal(up_key, m)
)进行 stdcode-序列化和加密
pub enum SessionFrame {
Data {
seqno: u64,
body: Bytes,
},
Parity {
data_frame_first: u64,
data_count: u8,
parity_count: u8,
parity_index: u8,
pad_size: u16,
body: Bytes,
},
Acks {
acks: Vec<(u64, u32)>,
naks: Vec<u64>,
},
}
附加到 SessionFrame 上的 u64
在每个发送的包中增加 1,接收方必须使用它来实施重放攻击预防。
我们分别讨论三种类型的帧
数据帧
每当需要发送数据报时,它将被 分片 并编码为一个或多个 Data
帧中。这是因为我们必须支持多达 64 KiB 的数据报,这比互联网链路上的典型 MTU 大得多。
每个数据报都分成以下格式的片段
- 1 字节:这是哪个片段
- 1 字节:总片段数
- 最多 1340 字节:片段的内容
例如,12345 字节的数据报将被分成 10 个片段,第一个片段包含头部 00 19
和数据报的前 1340 字节,第二个片段包含头部 09 19
和下一个 1340 字节,以此类推。这些片段然后将分别编码为 Data
帧的主体。
校验帧
校验帧用于前向纠错(FEC),基于 8 位里德-所罗门。需要注意的是,数据发送者 不 预先选择哪种里德-所罗门编码(例如,将 5 个数据帧扩展到 5 个数据 + 3 个校验)。相反,它只需要在决定发送校验帧后选择编码,并且在此点动态更改编码。
这体现在校验帧的格式中。每个校验帧实际上“声明”某些数据帧为里德-所罗门编码组的一部分
data_frame_first
:用于校验计算的第一个数据帧的序列号。data_count
:包含在校验计算中的数据帧数量。parity_count
:该组的总校验帧数。parity_index
:这个校验帧的索引。(例如,0 表示这是第一个校验帧)pad_size
:所有帧必须填充到的统一大小,用于Reed-Solomon解码(注意,RS要求每个数据和校验包大小相同)body
:校验数据本身。
确认帧
因为 obfsudp
是一个不可靠的数据报传输,所以确认信息不用于重传或拥塞控制。相反,它们仅用于帮助另一端测量连接质量。确认帧的格式如下
acks
:一个向量,表示发送方接收到的确认的seqno
,与 从接收到该seqno
到发送确认之间经过的毫秒数相关联。包括后者数字允许数据包接收者自由延迟和批量确认,而不会影响发送方的ping测量。=naks
:一个向量,表示发送方认为永远丢失的seqno
,通过诸如后续seqno
已确认到达的数量这样的启发式算法计算得出。
依赖关系
~24–64MB
~1M SLoC