0%

Authenticating sendme cells to mitigate bandwidth attacks

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)

  1. 收到 DATA、递减窗口
    sendme_circuit_data_received(circ, layer_hint) 负责将 deliver_window —,并记录日志。

  2. 判断是否该发电路级 SENDME
    sendme_circuit_consider_sending(circ, layer_hint):当 deliver_window <= CIRCWINDOW_START - sendme_inc 时,进入发送分支;当前实现还包含“不得一次性连续发多个 SENDME”的保护(否则摘要会复用,导致对端不匹配)。

  3. 构造并发送 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)

  1. 发送 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)。
  1. 收到 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)

  1. 解析版本:payload 为空→v0;否则走 trunnel 解析得出版本(v1 有摘要)。

  2. 弹出期望摘要circ_digest = pop_first_cell_digest(circ)

    • 若返回 NULL:说明 B 端并未到达边界、尚未入队摘要提前/伪造 SENDME ⇒ 判定非法,返回 false;
  3. v1 比对摘要cell_v1_is_valid(cell, circ_digest)v1_digest_matches(circ_digest, cell_digest) 做字节级比较;不等即非法;

  4. 通过:返回 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
2
3
4
5
int sendme_circuit_data_received(circuit_t *circ, crypt_path_t *layer_hint);
void sendme_circuit_consider_sending(circuit_t *circ, crypt_path_t *layer_hint);
static int send_circuit_level_sendme(circuit_t *circ, crypt_path_t *layer_hint,
const uint8_t *cell_digest);
STATIC ssize_t build_cell_payload_v1(const uint8_t *cell_digest, uint8_t *payload);
  • sendme_circuit_data_receiveddeliver_window--,到阈值触发后续。

  • sendme_circuit_consider_sending:边界判断、回填 deliver_window、取 digest、发 SENDME;并保护“不要一次性多发”。

  • send_circuit_level_sendme:决定 v0/v1,构 payload 并经 relay_send_command_from_edge 发送。

B 端:发送 DATA → 记录摘要(边界时)

1
2
3
4
void sendme_record_cell_digest_on_circ(circuit_t *circ, crypt_path_t *cpath);
void sendme_record_sending_cell_digest(circuit_t *circ, crypt_path_t *cpath);
STATIC bool circuit_sendme_cell_is_next(int deliver_window, int sendme_inc);
static void record_cell_digest_on_circ(circuit_t *circ, const uint8_t *digest);
  • sendme_record_sending_cell_digest/sendme_record_cell_digest_on_circ:到边界才入队摘要。

  • circuit_sendme_cell_is_next:根据窗口 + inc 判“下一格是否 SENDME”。

B 端:接收/校验 SENDME → 回填窗口

1
2
3
4
5
6
7
8
9
10
int sendme_process_circuit_level(crypt_path_t *layer_hint, circuit_t *circ,
const uint8_t *cell_payload, uint16_t len);
STATIC bool sendme_is_valid(const circuit_t *circ,
const uint8_t *cell_payload, size_t len);
static uint8_t *pop_first_cell_digest(const circuit_t *circ);
static bool cell_v1_is_valid(const sendme_cell_t *cell,
const uint8_t *circ_digest);
static bool v1_digest_matches(const uint8_t *circ_digest,
const uint8_t *cell_digest);
int sendme_process_circuit_level_impl(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,并做上界保护。