Tor 中的 SENDME 机制详解(基于 Proposal 289 与源码实现)
背景与动机
Tor 使用 窗口流控 (window-based flow control) 防止一端无限推送数据:
发送端维护
package_window
(还能发多少 DATA)。接收端维护
deliver_window
(还能收多少 DATA)。当接收端
deliver_window
降到阈值,就向对端发送 电路级 SENDME,对端据此为自己的package_window
补窗。
为防伪造/提前/重复 SENDME,Tor 在 Proposal 289 – Authenticated SENDMEs 中引入 v1 SENDME:在 SENDME 里携带 摘要 (digest),该摘要与“刚好位于 SENDME 边界的那一格 DATA cell 的加/解密状态”绑定,只有真正见过该 DATA 的一方才能给出正确摘要。
参考规范:
torspec proposals:
proposals/289-authenticated-sendmes.txt
tor-spec:
tor-spec.txt
的 Flow control & SENDME 部分
要点:只有在“边界点(每 inc 个数据)” 才会记录/期待摘要。提前发的 SENDME 在对端 没有可匹配摘要 → 校验失败 → 关路。
端到端流程(按方向拆解)
A 端(接收 DATA → 可能发送电路级 SENDME)
收到 DATA、递减窗口
sendme_circuit_data_received(circ, layer_hint)
负责将deliver_window
—,并记录日志。判断是否该发电路级 SENDME
sendme_circuit_consider_sending(circ, layer_hint)
:当deliver_window <= CIRCWINDOW_START - sendme_inc
时,进入发送分支;当前实现还包含“不得一次性连续发多个 SENDME”的保护(否则摘要会复用,导致对端不匹配)。构造并发送 SENDME(含摘要)
send_circuit_level_sendme(circ, layer_hint, digest)
:选择发送版本:
get_emit_min_version()
;v1:
build_cell_payload_v1(cell_digest, payload)
将摘要写入 payload;通过
relay_send_command_from_edge(...)
发出RELAY_COMMAND_SENDME
。
A 端需要先从电路密码状态取到 摘要:
客户端(有
layer_hint
):cpath_get_sendme_digest(layer_hint)
其他场景(无
layer_hint
):relay_crypto_get_sendme_digest(&TO_OR_CIRCUIT(circ)->crypto)
B 端(发送 DATA → 记录摘要 → 接收/校验 A 的 SENDME)
发送 DATA 时记录“边界摘要”
只有当“下一步预计会收到 SENDME”(即到达 inc 边界)才记录:加密后发送:
sendme_record_sending_cell_digest(circ, cpath)
统一入口:
sendme_record_cell_digest_on_circ(circ, cpath)
内部逻辑:
- `circuit_sent_cell_for_sendme(circ, cpath)` → 判断是否到达 SENDME 边界;
- 若到达,取摘要:
- 有 `cpath`:`cpath_sendme_record_cell_digest(cpath, true/false)`
- 无 `cpath`:`relay_crypto_record_sendme_digest(&TO_OR_CIRCUIT(circ)->crypto, dir)`
- 通过 `record_cell_digest_on_circ(circ, digest)` 将 **摘要 push 到** `**circ->sendme_last_digests**`(FIFO)。
收到 A 的电路级 SENDME
入口:sendme_process_circuit_level(layer_hint, circ, payload, len)
:第一步 校验:
sendme_is_valid(circ, payload, len)
;通过后:
固定窗口算法:
sendme_process_circuit_level_impl(layer_hint, circ)
→**package_window += CIRCWINDOW_INCREMENT**
;或拥塞控制:
congestion_control_dispatch_cc_alg(cc, circ)
;
成功后通常会触发 恢复读取:
circuit_resume_edge_reading(circ, layer_hint)
(允许继续从应用读数据、打包为 DATA)。
校验原理与“提前 SENDME”失败路径
摘要如何形成
摘要来源于电路的滚动加/解密状态:
cpath_sendme_record_cell_digest(cpath, dir)
relay_crypto_record_sendme_digest(&TO_OR_CIRCUIT(circ)->crypto, dir)
只在边界(预计下一个 cell 是 SENDME)时记录;
存入
circ->sendme_last_digests
(FIFO)。
发送端 B 的校验(收到 SENDME 时)
sendme_is_valid(circ, cell_payload, cell_payload_len)
:
解析版本:payload 为空→v0;否则走 trunnel 解析得出版本(v1 有摘要)。
弹出期望摘要:
circ_digest = pop_first_cell_digest(circ)
- 若返回
NULL
:说明 B 端并未到达边界、尚未入队摘要 ⇒ 提前/伪造 SENDME ⇒ 判定非法,返回 false;
- 若返回
v1 比对摘要:
cell_v1_is_valid(cell, circ_digest)
→v1_digest_matches(circ_digest, cell_digest)
做字节级比较;不等即非法;通过:返回 true。
sendme_process_circuit_level(...)
在 sendme_is_valid(...)
为 false 时返回 -END_CIRC_REASON_TORPROTOCOL
,上层据此关闭电路。
结论:A 端“提前发” SENDME 时,B 端大概率 没有入队摘要,
pop_first_cell_digest()
拿到NULL
,校验失败 → 关路。即使 B 端恰好有摘要,若 A 端重复/错位地使用摘要,也会在v1_digest_matches()
处失败。
关键函数索引与注释(按功能分组)
A 端:接收 DATA → 考虑发送 SENDME
1 | int sendme_circuit_data_received(circuit_t *circ, crypt_path_t *layer_hint); |
sendme_circuit_data_received
:deliver_window--
,到阈值触发后续。sendme_circuit_consider_sending
:边界判断、回填deliver_window
、取 digest、发 SENDME;并保护“不要一次性多发”。send_circuit_level_sendme
:决定 v0/v1,构 payload 并经relay_send_command_from_edge
发送。
B 端:发送 DATA → 记录摘要(边界时)
1 | void sendme_record_cell_digest_on_circ(circuit_t *circ, crypt_path_t *cpath); |
sendme_record_sending_cell_digest
/sendme_record_cell_digest_on_circ
:到边界才入队摘要。circuit_sendme_cell_is_next
:根据窗口 +inc
判“下一格是否 SENDME”。
B 端:接收/校验 SENDME → 回填窗口
1 | int sendme_process_circuit_level(crypt_path_t *layer_hint, circuit_t *circ, |
sendme_is_valid
:核心校验,包括版本检查、弹出队首摘要、(v1)摘要比对。失败返回 false。sendme_process_circuit_level
:校验失败则返回-END_CIRC_REASON_TORPROTOCOL
;成功则固定窗口或拥塞控制回填。sendme_process_circuit_level_impl
:固定窗口算法中package_window += CIRCWINDOW_INCREMENT
,并做上界保护。