研究

2026-03-05 组会

2026.03.05 组会-改动Tor客户端源码固定RP节点

核心问题:

通过仅少量改动Tor客户端源码实现固定RP节点到受控制的Exit节点


问题背景

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

  1. 随机选择一个RP节点,然后构建到 RP 的电路
  2. 连接一个Introduce节点,然后通过Introduce节点通知隐藏服务RP节点的信息
  3. 隐藏服务建立连接RP的三跳电路
  4. 通过 RP 汇合形成 6-hop 通路

从逻辑上看,RP 是一个特殊角色节点,但从源码实现上看,RP 并不是一种特殊类型,而是普通的Relay被选中赋予逻辑上的“会合点”含义。


可行性分析

  1. RP可固定:在Proposal的逻辑实现当中,客户端随机选择了一个Relay节点作为RP,然后形成了一个到RP的三跳电路,从结构上来说,可以把RP看作是这条Internal电路的“Exit”节点,这意味着可以通过固定普通Exit节点的方式来固定RP节点。
  2. 流量可控:RP节点同时也可以视为隐藏服务连接到RP节点的三跳电路的Exit节点,因此可以通过控制固定的RPoutbuf的方式来进行流量控制从而暴露隐藏服务。
  3. 更严格的猜想:由于Stream级别的流控要到端点才进行处理,所以可能甚至RP是没有对XON进行处理的,会直接转发到隐藏服务进行处理,那么很有可能甚至不需要对RP进行固定和处理,任意的RP直接通过客户端拉流量的方式就能直接暴露隐藏服务。

源码分析

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

1
2
3
4
5
6
7
8
9
circuit_launch_by_extend_info()

circuit_establish_circuit()

onion_pick_cpath_exit()

choose_good_exit_server()

router_choose_random_node()

而客户端建立不同电路的调用链如下:

1
2
3
4
5
6
7
8
9
circuit_launch_by_extend_info()

circuit_establish_circuit()

onion_pick_cpath_exit()

choose_good_exit_server()

choose_good_exit_server_general()

因此可以进行小改动,让client像选普通exit节点一样选择RP节点,从而实现在torrc里面直接配置就能固定RP

源码改动

如何选择节点的关键部分在下面的函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//circuitbuild.c

/** Return a pointer to a suitable router to be the exit node for the
* circuit of purpose <b>purpose</b> that we're about to build (or NULL
* if no router is suitable).
*
* For general-purpose circuits, pass it off to
* choose_good_exit_server_general()
*
* For client-side rendezvous circuits, choose a random node, weighted
* toward the preferences in 'options'.
*/
static const node_t *
choose_good_exit_server(origin_circuit_t *circ,
router_crn_flags_t flags, int is_internal)
{
const or_options_t *options = get_options();
flags |= CRN_NEED_DESC;

switch (TO_CIRCUIT(circ)->purpose) {
case CIRCUIT_PURPOSE_C_HSDIR_GET:
case CIRCUIT_PURPOSE_S_HSDIR_POST:
case CIRCUIT_PURPOSE_HS_VANGUARDS:
case CIRCUIT_PURPOSE_C_ESTABLISH_REND:
/* For these three, we want to pick the exit like a middle hop,
* since it should be random. */
tor_assert_nonfatal(is_internal);
/* We want to avoid picking certain nodes for HS purposes. */
flags |= CRN_FOR_HS;
FALLTHROUGH;
case CIRCUIT_PURPOSE_CONFLUX_UNLINKED:
case CIRCUIT_PURPOSE_C_GENERAL:
if (is_internal) /* pick it like a middle hop */
return router_choose_random_node(NULL, options->ExcludeNodes, flags);
else
return choose_good_exit_server_general(flags);
}
log_warn(LD_BUG,"Unhandled purpose %d", TO_CIRCUIT(circ)->purpose);
tor_fragile_assert();
return NULL;
}

选择RP节点的时候会调用router_choose_random_node函数,而正常选择exit节点的时候会调用choose_good_exit_server_general函数。因此可以在这个分支做改动,在发现是CIRCUIT_PROPOSE_C_REND的时候直接调用新加入的choose_good_exit_server_from_routerset函数来选择一个符合torrc配置中正常exit节点的节点作为RP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// circuitbuild.c

/** Return a pointer to a suitable router to be the exit node for the
* circuit of purpose <b>purpose</b> that we're about to build (or NULL
* if no router is suitable).
*
* For general-purpose circuits, pass it off to
* choose_good_exit_server_general()
*
* For client-side rendezvous circuits, choose a random node, weighted
* toward the preferences in 'options'.
*/
static const node_t *
choose_good_exit_server(origin_circuit_t *circ,
router_crn_flags_t flags, int is_internal)
{
const or_options_t *options = get_options();
flags |= CRN_NEED_DESC;

switch (TO_CIRCUIT(circ)->purpose) {
case CIRCUIT_PURPOSE_C_HSDIR_GET:
case CIRCUIT_PURPOSE_S_HSDIR_POST:
case CIRCUIT_PURPOSE_HS_VANGUARDS:
case CIRCUIT_PURPOSE_C_ESTABLISH_REND:
/* For these three, we want to pick the exit like a middle hop,
* since it should be random. */
tor_assert_nonfatal(is_internal);
/* We want to avoid picking certain nodes for HS purposes. */
flags |= CRN_FOR_HS;
/* BORING TEST */
/* If ExitNodes is configured, force the client rendezvous point to be
* chosen from that set as well, including IP-based routerset entries. */
if (TO_CIRCUIT(circ)->purpose == CIRCUIT_PURPOSE_C_ESTABLISH_REND &&
options->ExitNodes) {
const node_t *node = choose_good_exit_server_from_routerset(
options->ExitNodes, options->ExcludeExitNodesUnion_, flags);
if (!node) {
log_warn(LD_CIRC,
"No nodes in ExitNodes%s seem usable as a rendezvous "
"point: can't choose a rendezvous point.",
options->ExcludeExitNodesUnion_ ?
", except possibly those excluded by your configuration, " :
"");
}
return node;
}
/* BORING TEST */
FALLTHROUGH;
case CIRCUIT_PURPOSE_CONFLUX_UNLINKED:
case CIRCUIT_PURPOSE_C_GENERAL:
if (is_internal) /* pick it like a middle hop */
return router_choose_random_node(NULL, options->ExcludeNodes, flags);
else
return choose_good_exit_server_general(flags);
}
log_warn(LD_BUG,"Unhandled purpose %d", TO_CIRCUIT(circ)->purpose);
tor_fragile_assert();
return NULL;
}

新加入的choose_good_exit_server_from_routerset函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// circuitbuild.c

/* Pick a node from a configured routerset, but still enforce the normal
* suitability checks for this circuit position and purpose. */
static const node_t *
choose_good_exit_server_from_routerset(const routerset_t *pick_from,
const routerset_t *exclude_set,
router_crn_flags_t flags)
{
const node_t *node = NULL;
smartlist_t *live_nodes = smartlist_new();

tor_assert(pick_from);

routerset_get_all_nodes(live_nodes, pick_from, exclude_set, 1);
SMARTLIST_FOREACH_BEGIN(live_nodes, const node_t *, live_node) {
if (!router_can_choose_node(live_node, flags)) {
SMARTLIST_DEL_CURRENT(live_nodes, live_node);
}
} SMARTLIST_FOREACH_END(live_node);

if (smartlist_len(live_nodes) <= MAX_SANE_RESTRICTED_NODES) {
node = smartlist_choose(live_nodes);
} else {
node = node_sl_choose_by_bandwidth(live_nodes, NO_WEIGHTING);
}

smartlist_free(live_nodes);
return node;
}

这个函数从限定的pick_from集中选择仍然活跃的节点,也就是配置了指定exit节点的时候选择指定节点,未指定时随机选择符合要求的exit节点。

除此之外,需要进行修正让RP的排除检查与普通出口一致:

1
2
3
4
5
6
7
8
9
10
// circuitbuild.c warn_if_last_router_excluded()    

case CIRCUIT_PURPOSE_C_REND_JOINED:
/* BORING TEST */
/* Rendezvous points now follow the same exit exclusion set as normal
* exits so diagnostics match the enforced selection behavior. */
description = "chosen rendezvous point";
rs = options->ExcludeExitNodesUnion_;
/* BORING TEST */
break;

这样可以防止节点选择后的检查报警。

另一方面,由于HS_client_rend电路有时可能不会重新完整建路,而是复用一条已经打开的internall circuit。一旦发生这种复用,原来那条电路会被继承从而绕过上面改动的RP选择逻辑,因此需要禁用已有的internal circuit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// circuituse.c

/**
* Return true for the set of conditions for which it is OK to use
* a cannibalized circuit.
*
* Don't cannibalize for onehops, or certain purposes.
*/
static int
circuit_should_cannibalize_to_build(uint8_t purpose_to_build,
int has_extend_info,
int onehop_tunnel)
{
const or_options_t *options = get_options();

/* Do not try to cannibalize if this is a one hop circuit. */
if (onehop_tunnel) {
return 0;
}

/* Don't try to cannibalize for general purpose circuits that do not
* specify a custom exit. */
if (purpose_to_build == CIRCUIT_PURPOSE_C_GENERAL && !has_extend_info) {
return 0;
}

/* Don't cannibalize for testing circuits. We want to see if they
* complete normally. Also don't cannibalize for vanguard-purpose
* circuits, since those are specially pre-built for later
* cannibalization by the actual specific circuit types that need
* vanguards.
*/
if (purpose_to_build == CIRCUIT_PURPOSE_TESTING ||
purpose_to_build == CIRCUIT_PURPOSE_HS_VANGUARDS) {
return 0;
}

/* The server-side intro circ is not cannibalized because it only
* needs a 3 hop circuit. It is also long-lived, so it is more
* important that it have lower latency than get built fast.
*/
if (purpose_to_build == CIRCUIT_PURPOSE_S_ESTABLISH_INTRO) {
return 0;
}

/* Do not cannibalize for conflux circuits */
if (purpose_to_build == CIRCUIT_PURPOSE_CONFLUX_UNLINKED) {
return 0;
}

/* BORING TEST */
/* A client rendezvous circuit with ExitNodes configured must be built with
* a freshly chosen final hop, otherwise cannibalizing an existing internal
* circuit would keep its random endpoint and bypass the forced RP choice. */
if (purpose_to_build == CIRCUIT_PURPOSE_C_ESTABLISH_REND &&
options->ExitNodes) {
return 0;
}
/* BORING TEST */

return 1;
}

实验结果

代码改动之后随意访问一个.onion服务,可以看到6-hop电路的建立,并且选取的RP节点和普通exit节点一样:

image-20260304175957952