2026-03-05 组会

2026.03.05 组会-Tor 客户端选择 Rendezvous Point (RP) 机制汇报

核心问题:

Tor 客户端在建立隐藏服务连接时,是如何选择 Rendezvous Point (RP) 的?

分析基于 circuitbuild.c 源码。


问题背景

在 v3 Hidden Service 连接过程中,客户端需要:

  1. 构建到 RP 的电路
  2. 通知隐藏服务连接该 RP
  3. 通过 RP 汇合形成 6-hop 通路

从逻辑上看,RP 是一个特殊角色节点。

但从源码实现上看,RP 并不是一种特殊类型。


核心结论

在当前实现中:

RP 本质上就是这条 internal 电路的“exit”节点。

换句话说:

1
RP == chosen_exit(当 purpose = C_ESTABLISH_REND)

因此:

  • 如果未指定 exit,则随机选择一个节点作为 RP
  • 如果指定了 exit_ei,则该节点直接成为 RP

即:

1
指定 exit == 指定 RP

前提条件:

  • 电路用途为 CIRCUIT_PURPOSE_C_ESTABLISH_REND
  • 电路必须是 internal

源码路径分析

客户端建立 RP 电路的调用链如下:

1
2
3
4
5
6
7
origin_circuit_init()

onion_pick_cpath_exit()

choose_good_exit_server()

router_choose_random_node()

下面逐步说明。


Step 1:internal 标记的来源

1
2
3
4
5
6
7
8
9
origin_circuit_t *
origin_circuit_init(uint8_t purpose, int flags)
{
circ->build_state->is_internal =
((flags & CIRCLAUNCH_IS_INTERNAL) ? 1 : 0);

circ->base_.purpose = purpose;
return circ;
}

关键点:

  • is_internal 由 flags 决定
  • 建立 RP 电路时必须设置 CIRCLAUNCH_IS_INTERNAL
  • 否则后续会触发断言

internal 电路的含义是:

不用于访问外网,而用于内部目的(如 HS 或目录)


Step 2:决定最后一跳

1
2
3
4
5
6
7
8
9
10
11
12
// onion_pick_cpath_exit()

if (exit_ei) {
exit_ei = extend_info_dup(exit_ei);
} else {
const node_t *node =
choose_good_exit_server(circ, flags, state->is_internal);

exit_ei = extend_info_from_node(node, ...);
}

state->chosen_exit = exit_ei;

逻辑非常清晰:

  • 如果调用方给了 exit_ei
  • 则直接使用该节点
  • 否则调用选择逻辑

对于 rendezvous 电路来说:

1
chosen_exit == RP

Step 3:RP 的实际选择逻辑

1
2
3
4
5
6
7
8
9
10
11
// choose_good_exit_server(origin_circuit_t *circ,
// router_crn_flags_t flags, int is_internal)

case CIRCUIT_PURPOSE_C_ESTABLISH_REND:
tor_assert_nonfatal(is_internal);
flags |= CRN_FOR_HS;
FALLTHROUGH;

case CIRCUIT_PURPOSE_C_GENERAL:
if (is_internal)
return router_choose_random_node(NULL, options->ExcludeNodes, flags);

逐条解释:

  1. 必须是 internal 电路
  2. 标记该节点用于 Hidden Service
  3. 调用 router_choose_random_node() 随机选择

因此可以得出结论:

RP 是在满足过滤条件的节点中随机选出的一个普通 relay。

它不是特殊类型节点。


七、RP 电路的结构

客户端侧电路结构:

1
guard → middle → RP

这是一条三跳 internal 电路。

RP 只是最后一跳。

当隐藏服务连接到该 RP 后,形成:

1
2
client:  guard → middle → RP
service: guard → middle → RP

最终构成 6-hop 通路。


八、设计思想分析

为什么 Tor 这样设计?

原因很简单:

  • RP 不需要访问外网
  • 它只是汇合点
  • 因此不需要单独设计一种“RP 类型节点”

实现上直接复用 exit 选择逻辑。

只是在逻辑层面限制为 internal。


九、实验启示

对于源码修改或实验:

如果希望固定 RP:

  • 可以在建立电路时指定 exit_ei
  • 无需修改随机选择函数

如果希望统计 RP 行为:

  • 可以在 choose_good_exit_server() 中添加日志

十、总结

本次分析说明:

  1. RP 不是特殊节点类型
  2. RP 是 internal 电路的 chosen_exit
  3. 未指定时随机选择
  4. 指定 exit 等价于指定 RP

一句话概括:

在客户端建立 Rendezvous 电路时,RP 就是这条 internal 电路的最后一跳。


十一、6-hop 电路结构解析

在 v3 Hidden Service 连接完成后,客户端与隐藏服务之间形成如下结构:

客户端侧电路:

1
guard_c → middle_c → RP

隐藏服务侧电路:

1
guard_s → middle_s → RP

合并后形成完整 6-hop 通路:

1
2
3
4
5
client

guard_c → middle_c → RP ← middle_s ← guard_s

service

注意:

  • RP 是双方电路的汇合点
  • RP 不知道 client 或 service 的真实身份
  • 两侧电路在密码学上彼此独立

因此:

6-hop 结构本质上是两条 3-hop internal 电路在 RP 处拼接。


十二、Tor Proposal 中的原文描述

在 Tor Proposal 224(Next-Generation Hidden Services)中,对 rendezvous 机制的描述如下:

“The client builds a circuit to a randomly chosen rendezvous point (RP) and sends a RENDEZVOUS1 cell.”

以及:

“The service builds a circuit to the rendezvous point and sends a RENDEZVOUS2 cell to complete the handshake.”

关键点可以提炼为:

  1. Client 随机选择 RP
  2. Client 先建立到 RP 的电路
  3. Service 再建立到同一个 RP 的电路
  4. 双方在 RP 处完成握手

这与我们在源码中看到的逻辑完全一致:

  • RP 是随机选取
  • RP 是 internal 电路的最后一跳
  • 双方各自构建独立电路

十三、Proposal 与源码的对应关系

Proposal 描述 源码实现
Client randomly chooses RP router_choose_random_node()
Client builds circuit to RP CIRCUIT_PURPOSE_C_ESTABLISH_REND
Service connects to RP 服务端构建 internal 电路
Rendezvous handshake RP 处 relay cell 交换

可以看到:

Proposal 是协议层描述,源码是工程实现。

RP 并不是协议层的特殊节点类型。

它只是一个普通 relay,被选作汇合点。


十四、结构总结

结合协议与源码,可以得到完整理解:

  1. RP 是普通 relay
  2. RP 通过 internal 电路被选出
  3. 6-hop 是两条 3-hop 电路拼接
  4. 指定 exit 等价于指定 RP

从实现角度看:

Tor 没有专门的 RP 类型或 RP 选择函数。

RP 只是 exit 选择逻辑在特定 purpose 下的复用。


以上为结合 Proposal 与源码的完整机制说明。