1use log::{debug, info, warn};
2use rand::seq::SliceRandom;
3
4use crate::circuit::{
5 begin_dir_get_via, connect_guard_circuit, extend_unless_same, Circuit, RelayMessage,
6};
7use crate::crypto::{
8 aes_ctr_crypt, ct_equal, random_bytes, relay_body_len, sha3_256, shake256, tor_mac,
9 x25519_public_from_private, x25519_shared, DigestKind, RelayCrypto,
10};
11use crate::directory::{
12 candidate_rendezvous_relays, link_spec_ipv4, parse_link_specifiers, relay_link_specifiers,
13 relay_usable_rendezvous, same_relay, select_hsdirs, serialize_link_specifiers, HsPeriodKeys,
14 LinkSpecifier, Relay,
15};
16use crate::error::{ensure, err, Result};
17use crate::tor::{
18 K_RELAY_HEADER_LEN, K_RELAY_PAYLOAD_LEN, RELAY_BEGIN, RELAY_CONNECTED, RELAY_DATA, RELAY_END,
19 RELAY_ESTABLISH_RENDEZVOUS, RELAY_INTRODUCE1, RELAY_INTRODUCE_ACK, RELAY_RENDEZVOUS2,
20 RELAY_RENDEZVOUS_ESTABLISHED, RELAY_SENDME,
21};
22use crate::util::{
23 base64_decode, base64_encode_unpadded, from_string, put_u16, put_u64, read_u16, split_ws,
24 to_string_lossy, Bytes,
25};
26use crate::{Consensus, Options};
27
28const K_HS_PROTO: &str = "tor-hs-ntor-curve25519-sha3-256-1";
29
30pub fn extract_pem_message(text: &str, begin_label: &str, end_label: &str) -> Result<String> {
31 let b = text
32 .find(begin_label)
33 .ok_or_else(|| crate::Error::new("PEM begin marker not found"))?;
34 let after_begin = text[b..]
35 .find('\n')
36 .map(|idx| b + idx)
37 .ok_or_else(|| crate::Error::new("bad PEM begin line"))?;
38 let e = text[after_begin..]
39 .find(end_label)
40 .map(|idx| after_begin + idx)
41 .ok_or_else(|| crate::Error::new("PEM end marker not found"))?;
42 Ok(text[after_begin + 1..e].to_string())
43}
44
45pub fn parse_ed25519_cert_subject(pem: &str) -> Result<Bytes> {
46 let cert = base64_decode(&extract_pem_message(
47 pem,
48 "-----BEGIN ED25519 CERT-----",
49 "-----END ED25519 CERT-----",
50 )?)?;
51 ensure(
52 cert.len() >= 1 + 1 + 4 + 1 + 32 + 1 + 64,
53 "short ed25519 cert",
54 )?;
55 ensure(cert[0] == 1, "unsupported ed25519 cert version")?;
56 Ok(cert[7..39].to_vec())
57}
58
59pub fn decrypt_descriptor_layer(
60 ciphertext: &[u8],
61 secret_data: &[u8],
62 subcredential: &[u8],
63 revision: u64,
64 constant: &str,
65) -> Result<Bytes> {
66 ensure(
67 ciphertext.len() >= 16 + 32,
68 "descriptor ciphertext too short",
69 )?;
70 let salt = &ciphertext[..16];
71 let enc = &ciphertext[16..ciphertext.len() - 32];
72 let mac = &ciphertext[ciphertext.len() - 32..];
73 let mut secret_input = secret_data.to_vec();
74 secret_input.extend_from_slice(subcredential);
75 put_u64(&mut secret_input, revision);
76 let mut kdf_in = secret_input;
77 kdf_in.extend_from_slice(salt);
78 kdf_in.extend_from_slice(constant.as_bytes());
79 let keys = shake256(&kdf_in, 32 + 16 + 32);
80 let secret_key = &keys[..32];
81 let secret_iv = &keys[32..48];
82 let mac_key = &keys[48..];
83 let mut mac_in = Bytes::new();
84 put_u64(&mut mac_in, mac_key.len() as u64);
85 mac_in.extend_from_slice(mac_key);
86 put_u64(&mut mac_in, salt.len() as u64);
87 mac_in.extend_from_slice(salt);
88 mac_in.extend_from_slice(enc);
89 ensure(
90 ct_equal(mac, &sha3_256(&mac_in)),
91 "descriptor layer MAC mismatch",
92 )?;
93 let mut plain = aes_ctr_crypt(secret_key, enc, Some(secret_iv))?;
94 while plain.last() == Some(&0) {
95 plain.pop();
96 }
97 Ok(plain)
98}
99
100#[derive(Clone, Debug, Default)]
101pub struct IntroductionPoint {
102 pub links: Vec<LinkSpecifier>,
103 pub ntor_key: Bytes,
104 pub auth_key: Bytes,
105 pub enc_key: Bytes,
106}
107
108#[derive(Clone, Debug, Default)]
109pub struct HiddenServiceDescriptor {
110 pub intros: Vec<IntroductionPoint>,
111}
112
113#[derive(Clone, Debug)]
114pub struct DescriptorFetchResult {
115 pub descriptor: HiddenServiceDescriptor,
116 pub guard: Relay,
117}
118
119pub fn intro_relay_from_descriptor(intro: &IntroductionPoint) -> Result<Relay> {
120 let hp = link_spec_ipv4(&intro.links)
121 .ok_or_else(|| crate::Error::new("intro point lacks IPv4 link specifier"))?;
122 let mut relay = Relay {
123 nickname: "intro".to_string(),
124 ip: hp.host,
125 or_port: hp.port,
126 ntor_key: intro.ntor_key.clone(),
127 ..Relay::default()
128 };
129 for ls in &intro.links {
130 if ls.spec_type == 2 && ls.data.len() == 20 {
131 relay.rsa_id = ls.data.clone();
132 } else if ls.spec_type == 3 && ls.data.len() == 32 {
133 relay.ed_id = ls.data.clone();
134 }
135 }
136 ensure(
137 relay.rsa_id.len() == 20,
138 "intro point lacks RSA identity link specifier",
139 )?;
140 ensure(
141 relay.ed_id.len() == 32,
142 "intro point lacks Ed25519 identity link specifier",
143 )?;
144 ensure(
145 relay.ntor_key.len() == 32,
146 "intro point lacks ntor onion key",
147 )?;
148 Ok(relay)
149}
150
151pub fn parse_inner_descriptor(plain: &str) -> Result<HiddenServiceDescriptor> {
152 let lines: Vec<String> = plain
153 .lines()
154 .map(|line| line.strip_suffix('\r').unwrap_or(line).to_string())
155 .collect();
156 let mut desc = HiddenServiceDescriptor::default();
157 let mut cur: Option<usize> = None;
158 let mut i = 0usize;
159 while i < lines.len() {
160 let line = &lines[i];
161 let parts = split_ws(line);
162 if parts.is_empty() {
163 i += 1;
164 continue;
165 }
166 if parts[0] == "introduction-point" && parts.len() >= 2 {
167 desc.intros.push(IntroductionPoint {
168 links: parse_link_specifiers(&base64_decode(parts[1])?)?,
169 ..IntroductionPoint::default()
170 });
171 cur = Some(desc.intros.len() - 1);
172 } else if parts[0] == "onion-key" && parts.len() >= 3 && parts[1] == "ntor" {
173 if let Some(idx) = cur {
174 desc.intros[idx].ntor_key = base64_decode(parts[2])?;
175 }
176 } else if parts[0] == "enc-key" && parts.len() >= 3 && parts[1] == "ntor" {
177 if let Some(idx) = cur {
178 desc.intros[idx].enc_key = base64_decode(parts[2])?;
179 }
180 } else if parts[0] == "auth-key" {
181 if let Some(idx) = cur {
182 let mut pem = format!("{line}\n");
183 let mut in_cert = false;
184 i += 1;
185 while i < lines.len() {
186 let cert_line = &lines[i];
187 pem.push_str(cert_line);
188 pem.push('\n');
189 if cert_line == "-----BEGIN ED25519 CERT-----" {
190 in_cert = true;
191 }
192 if cert_line == "-----END ED25519 CERT-----" {
193 break;
194 }
195 i += 1;
196 }
197 ensure(in_cert, "auth-key missing cert")?;
198 desc.intros[idx].auth_key = parse_ed25519_cert_subject(&pem)?;
199 }
200 }
201 i += 1;
202 }
203 desc.intros.retain(|intro| {
204 intro.auth_key.len() == 32
205 && intro.ntor_key.len() == 32
206 && intro.enc_key.len() == 32
207 && link_spec_ipv4(&intro.links).is_some()
208 });
209 ensure(
210 !desc.intros.is_empty(),
211 "descriptor has no usable introduction points",
212 )?;
213 Ok(desc)
214}
215
216pub fn decrypt_hs_descriptor(outer: &str, keys: &HsPeriodKeys) -> Result<HiddenServiceDescriptor> {
217 let mut revision = 0u64;
218 let mut super_pem = String::new();
219 let mut in_super = false;
220 for raw_line in outer.lines() {
221 let line = raw_line.strip_suffix('\r').unwrap_or(raw_line);
222 let parts = split_ws(line);
223 if parts.len() >= 2 && parts[0] == "revision-counter" {
224 revision = parts[1].parse()?;
225 }
226 if line == "-----BEGIN MESSAGE-----" {
227 in_super = true;
228 super_pem.push_str(line);
229 super_pem.push('\n');
230 continue;
231 }
232 if in_super {
233 super_pem.push_str(line);
234 super_pem.push('\n');
235 if line == "-----END MESSAGE-----" {
236 in_super = false;
237 }
238 }
239 }
240 ensure(
241 revision != 0 || outer.contains("revision-counter 0"),
242 "descriptor missing revision-counter",
243 )?;
244 ensure(
245 !super_pem.is_empty(),
246 "descriptor missing superencrypted blob",
247 )?;
248 let super_cipher = base64_decode(&extract_pem_message(
249 &super_pem,
250 "-----BEGIN MESSAGE-----",
251 "-----END MESSAGE-----",
252 )?)?;
253 let first_plain = decrypt_descriptor_layer(
254 &super_cipher,
255 &keys.blinded,
256 &keys.subcredential,
257 revision,
258 "hsdir-superencrypted-data",
259 )?;
260 let first = to_string_lossy(&first_plain);
261 let inner_pem =
262 extract_pem_message(&first, "-----BEGIN MESSAGE-----", "-----END MESSAGE-----")?;
263 let inner_cipher = base64_decode(&inner_pem)?;
264 let second_plain = decrypt_descriptor_layer(
265 &inner_cipher,
266 &keys.blinded,
267 &keys.subcredential,
268 revision,
269 "hsdir-encrypted-data",
270 )?;
271 parse_inner_descriptor(&to_string_lossy(&second_plain))
272}
273
274#[derive(Clone, Debug, Default)]
275pub struct HsNtorState {
276 pub x: Bytes,
277 pub x_pub: Bytes,
278 pub b: Bytes,
279 pub auth_key: Bytes,
280 pub bx: Bytes,
281 pub ntor_key_seed: Bytes,
282}
283
284#[derive(Clone, Debug, Default)]
285pub struct HsIntroPayload {
286 pub body: Bytes,
287 pub state: HsNtorState,
288}
289
290pub fn build_introduce1(
291 intro: &IntroductionPoint,
292 rp: &Relay,
293 rend_cookie: &[u8],
294 keys: &HsPeriodKeys,
295) -> Result<HsIntroPayload> {
296 ensure(rend_cookie.len() == 20, "bad rendezvous cookie")?;
297 ensure(rp.ntor_key.len() == 32, "rendezvous relay missing ntor key")?;
298 let mut header = vec![0; 20];
299 header.push(2);
300 put_u16(&mut header, 32);
301 header.extend_from_slice(&intro.auth_key);
302 header.push(0);
303
304 let mut plain = rend_cookie.to_vec();
305 plain.push(0);
306 plain.push(1);
307 put_u16(&mut plain, 32);
308 plain.extend_from_slice(&rp.ntor_key);
309 let rp_lspec = serialize_link_specifiers(&relay_link_specifiers(rp)?)?;
310 plain.extend_from_slice(&rp_lspec);
311 if plain.len() < 246 {
312 plain.resize(246, 0);
313 }
314
315 let mut st = HsNtorState::default();
316 st.x = random_bytes(32);
317 st.x_pub = x25519_public_from_private(&st.x)?;
318 st.b = intro.enc_key.clone();
319 st.auth_key = intro.auth_key.clone();
320 st.bx = x25519_shared(&st.x, &st.b)?;
321
322 let proto = from_string(K_HS_PROTO);
323 let mut intro_secret = st.bx.clone();
324 intro_secret.extend_from_slice(&intro.auth_key);
325 intro_secret.extend_from_slice(&st.x_pub);
326 intro_secret.extend_from_slice(&st.b);
327 intro_secret.extend_from_slice(&proto);
328 let mut info = from_string(format!("{K_HS_PROTO}:hs_key_expand"));
329 info.extend_from_slice(&keys.subcredential);
330 let mut kdf_in = intro_secret;
331 kdf_in.extend_from_slice(format!("{K_HS_PROTO}:hs_key_extract").as_bytes());
332 kdf_in.extend_from_slice(&info);
333 let hs_keys = shake256(&kdf_in, 64);
334 let enc_key = &hs_keys[..32];
335 let mac_key = &hs_keys[32..];
336 let encrypted = aes_ctr_crypt(enc_key, &plain, None)?;
337
338 let mut mac_msg = header.clone();
339 mac_msg.extend_from_slice(&st.x_pub);
340 mac_msg.extend_from_slice(&encrypted);
341 let mac = tor_mac(mac_key, &mac_msg);
342
343 let mut body = header;
344 body.extend_from_slice(&st.x_pub);
345 body.extend_from_slice(&encrypted);
346 body.extend_from_slice(&mac);
347 Ok(HsIntroPayload { body, state: st })
348}
349
350pub fn finish_hs_ntor(st: &mut HsNtorState, handshake_info: &[u8]) -> Result<RelayCrypto> {
351 ensure(
352 handshake_info.len() >= 64,
353 "RENDEZVOUS2 handshake too short",
354 )?;
355 let y = &handshake_info[..32];
356 let auth = &handshake_info[32..64];
357 let yx = x25519_shared(&st.x, y)?;
358 let proto = from_string(K_HS_PROTO);
359 let mut secret = yx;
360 secret.extend_from_slice(&st.bx);
361 secret.extend_from_slice(&st.auth_key);
362 secret.extend_from_slice(&st.b);
363 secret.extend_from_slice(&st.x_pub);
364 secret.extend_from_slice(y);
365 secret.extend_from_slice(&proto);
366 let ntor_key_seed = tor_mac(&secret, format!("{K_HS_PROTO}:hs_key_extract").as_bytes());
367 let verify = tor_mac(&secret, format!("{K_HS_PROTO}:hs_verify").as_bytes());
368 let mut auth_input = verify;
369 auth_input.extend_from_slice(&st.auth_key);
370 auth_input.extend_from_slice(&st.b);
371 auth_input.extend_from_slice(y);
372 auth_input.extend_from_slice(&st.x_pub);
373 auth_input.extend_from_slice(&proto);
374 auth_input.extend_from_slice(b"Server");
375 let expected = tor_mac(&auth_input, format!("{K_HS_PROTO}:hs_mac").as_bytes());
376 ensure(
377 ct_equal(auth, &expected),
378 "RENDEZVOUS2 hs-ntor auth mismatch",
379 )?;
380 st.ntor_key_seed = ntor_key_seed.clone();
381 let mut kdf_in = ntor_key_seed;
382 kdf_in.extend_from_slice(format!("{K_HS_PROTO}:hs_key_expand").as_bytes());
383 let k = shake256(&kdf_in, 128);
384 Ok(RelayCrypto::new(
385 &k[..32],
386 &k[32..64],
387 &k[64..96],
388 &k[96..128],
389 DigestKind::Sha3,
390 ))
391}
392
393pub struct RendezvousStream {
394 circ: Circuit,
395 hs: RelayCrypto,
396}
397
398impl RendezvousStream {
399 pub fn new(circ: Circuit, hs_crypto: RelayCrypto) -> Self {
400 Self {
401 circ,
402 hs: hs_crypto,
403 }
404 }
405
406 pub fn begin(&mut self, stream_id: u16, port: u16) -> Result<()> {
407 let mut target = from_string(format!(":{port}"));
408 target.push(0);
409 self.send_hs(RELAY_BEGIN, stream_id, &target)?;
410 loop {
411 let m = self.recv_hs()?;
412 if m.stream_id != stream_id {
413 continue;
414 }
415 if m.cmd == RELAY_CONNECTED {
416 return Ok(());
417 }
418 if m.cmd == RELAY_END {
419 return err("onion service stream ended before CONNECTED");
420 }
421 }
422 }
423
424 pub fn send_data(&mut self, stream_id: u16, data: &[u8]) -> Result<()> {
425 for chunk in data.chunks(K_RELAY_PAYLOAD_LEN) {
426 self.send_hs(RELAY_DATA, stream_id, chunk)?;
427 }
428 Ok(())
429 }
430
431 pub fn read_until_end(&mut self, stream_id: u16, limit: usize) -> Result<Bytes> {
432 let mut out = Bytes::new();
433 let mut circ_window = 1000;
434 let mut stream_window = 500;
435 loop {
436 let m = self.recv_hs()?;
437 if m.stream_id != stream_id {
438 continue;
439 }
440 if m.cmd == RELAY_DATA {
441 out.extend_from_slice(&m.data);
442 circ_window -= 1;
443 if circ_window <= 900 {
444 self.send_hs(RELAY_SENDME, 0, &[0, 0, 0])?;
445 circ_window += 100;
446 }
447 stream_window -= 1;
448 if stream_window <= 450 {
449 self.send_hs(RELAY_SENDME, stream_id, &[])?;
450 stream_window += 50;
451 }
452 if out.len() > limit {
453 return err("stream response too large");
454 }
455 } else if m.cmd == RELAY_END {
456 break;
457 }
458 }
459 Ok(out)
460 }
461
462 pub fn end(&mut self, stream_id: u16) -> Result<()> {
463 self.send_hs(RELAY_END, stream_id, &[6])
464 }
465
466 fn send_hs(&mut self, cmd: u8, stream_id: u16, data: &[u8]) -> Result<()> {
467 let body = self.hs.encrypt_relay(cmd, stream_id, data)?;
468 self.circ.send_raw_body(&body)
469 }
470
471 fn recv_hs(&mut self) -> Result<RelayMessage> {
472 loop {
473 let rp_plain = self.circ.recv_raw_body()?;
474 if let Some(body) = self.hs.decrypt_recognized(&rp_plain)? {
475 let len = relay_body_len(&body)?;
476 ensure(
477 K_RELAY_HEADER_LEN + len <= body.len(),
478 "relay length too large",
479 )?;
480 return Ok(RelayMessage {
481 cmd: body[0],
482 stream_id: read_u16(&body, 3)?,
483 data: body[K_RELAY_HEADER_LEN..K_RELAY_HEADER_LEN + len].to_vec(),
484 });
485 }
486 }
487 }
488}
489
490pub fn fetch_hidden_service_descriptor(
491 consensus: &Consensus,
492 keys: &HsPeriodKeys,
493 timeout_ms: i32,
494 _verbose: bool,
495) -> Result<DescriptorFetchResult> {
496 let mut srvs = Vec::new();
497 if !consensus.shared_rand_current.is_empty() {
498 srvs.push(consensus.shared_rand_current.clone());
499 }
500 if !consensus.shared_rand_previous.is_empty() {
501 srvs.push(consensus.shared_rand_previous.clone());
502 }
503 ensure(!srvs.is_empty(), "consensus has no shared-rand values")?;
504 let blinded_b64 = base64_encode_unpadded(&keys.blinded);
505 let path = format!("/tor/hs/3/{blinded_b64}");
506 let mut last_error = String::new();
507 let guards = candidate_rendezvous_relays(consensus)?;
508 for srv in srvs {
509 let mut hsdirs = select_hsdirs(
510 consensus,
511 &keys.blinded,
512 &srv,
513 keys.period_num,
514 keys.period_len,
515 )?;
516 hsdirs.shuffle(&mut rand::thread_rng());
517 let mut guard_pos = 0usize;
518 for hsdir in hsdirs {
519 let result: Result<DescriptorFetchResult> = (|| {
520 info!("fetching descriptor from HSDir {}", hsdir.nickname);
521 let mut guard = None;
522 for tries in 0..guards.len() {
523 let candidate = &guards[(guard_pos + tries) % guards.len()];
524 if candidate.ed_id != hsdir.ed_id {
525 guard = Some(candidate.clone());
526 guard_pos = (guard_pos + tries + 1) % guards.len();
527 break;
528 }
529 }
530 let guard = guard
531 .ok_or_else(|| crate::Error::new("no guard available for HSDir request"))?;
532 let body = begin_dir_get_via(&guard, &hsdir, &path, timeout_ms)?;
533 Ok(DescriptorFetchResult {
534 descriptor: decrypt_hs_descriptor(&to_string_lossy(&body), keys)?,
535 guard,
536 })
537 })();
538 match result {
539 Ok(desc) => return Ok(desc),
540 Err(e) => {
541 last_error = e.to_string();
542 debug!(
543 "descriptor fetch from HSDir {} failed: {}",
544 hsdir.nickname, last_error
545 );
546 }
547 }
548 }
549 }
550 err(format!(
551 "failed to fetch/decrypt hidden service descriptor: {last_error}"
552 ))
553}
554
555pub fn connect_onion_service(
556 opt: &Options,
557 consensus: &Consensus,
558 desc: &HiddenServiceDescriptor,
559 keys: &HsPeriodKeys,
560 rp: &Relay,
561 guard: &Relay,
562) -> Result<RendezvousStream> {
563 let rend_cookie = random_bytes(20);
564 info!(
565 "connecting to rendezvous point {} at {}:{} via guard {}",
566 rp.nickname, rp.ip, rp.or_port, guard.nickname
567 );
568 let mut rp_circ = connect_guard_circuit(guard, opt.timeout_ms)?;
569 extend_unless_same(&mut rp_circ, guard, rp)?;
570 rp_circ.send_relay(RELAY_ESTABLISH_RENDEZVOUS, 0, &rend_cookie)?;
571 loop {
572 let m = rp_circ.recv_relay()?;
573 if m.cmd == RELAY_RENDEZVOUS_ESTABLISHED {
574 break;
575 }
576 }
577
578 let mut intros = desc.intros.clone();
579 intros.shuffle(&mut rand::thread_rng());
580 let mut last_error = String::new();
581 let mut ntor_state = HsNtorState::default();
582 let mut introduced = false;
583 for intro in intros {
584 let attempt: Result<HsNtorState> = (|| {
585 let intro_relay = intro_relay_from_descriptor(&intro)?;
586 info!(
587 "sending INTRODUCE1 via intro point {}:{}",
588 intro_relay.ip, intro_relay.or_port
589 );
590 let payload = build_introduce1(&intro, rp, &rend_cookie, keys)?;
591 let mut ip_circ = connect_guard_circuit(guard, opt.timeout_ms)?;
592 extend_unless_same(&mut ip_circ, guard, &intro_relay)?;
593 ip_circ.send_relay(RELAY_INTRODUCE1, 0, &payload.body)?;
594 loop {
595 let ack = ip_circ.recv_relay()?;
596 if ack.cmd == RELAY_INTRODUCE_ACK {
597 ensure(ack.data.len() >= 2, "short INTRODUCE_ACK")?;
598 let status = read_u16(&ack.data, 0)?;
599 ensure(status == 0, format!("INTRODUCE_ACK status {status}"))?;
600 return Ok(payload.state);
601 }
602 }
603 })();
604 match attempt {
605 Ok(state) => {
606 ntor_state = state;
607 introduced = true;
608 break;
609 }
610 Err(e) => last_error = e.to_string(),
611 }
612 }
613 ensure(
614 introduced,
615 format!("all introduction points failed: {last_error}"),
616 )?;
617 info!("waiting for RENDEZVOUS2");
618 let hs_crypto = loop {
619 let m = rp_circ.recv_relay()?;
620 if m.cmd == RELAY_RENDEZVOUS2 {
621 break finish_hs_ntor(&mut ntor_state, &m.data)?;
622 }
623 };
624 let _ = consensus;
625 Ok(RendezvousStream::new(rp_circ, hs_crypto))
626}
627
628pub fn connect_onion_service_with_retries(
629 opt: &Options,
630 consensus: &Consensus,
631 desc: &HiddenServiceDescriptor,
632 keys: &HsPeriodKeys,
633 preferred_guards: &[Relay],
634) -> Result<RendezvousStream> {
635 let candidates = candidate_rendezvous_relays(consensus)?;
636 let mut guards = Vec::<Relay>::new();
637 for g in preferred_guards {
638 if relay_usable_rendezvous(g) && !guards.iter().any(|existing| same_relay(existing, g)) {
639 guards.push(g.clone());
640 }
641 }
642 for g in &candidates {
643 if !guards.iter().any(|existing| same_relay(existing, g)) {
644 guards.push(g.clone());
645 }
646 }
647 ensure(!guards.is_empty(), "no usable guard relays for rendezvous")?;
648 let mut last_error = String::new();
649 let rp_attempts = 12usize.min(candidates.len());
650 let guard_attempts = 3usize.min(guards.len());
651 for i in 0..rp_attempts {
652 for j in 0..guard_attempts {
653 let rp = &candidates[i];
654 let guard = &guards[(i + j) % guards.len()];
655 if same_relay(rp, guard) && guards.len() > 1 {
656 continue;
657 }
658 match connect_onion_service(opt, consensus, desc, keys, rp, guard) {
659 Ok(stream) => return Ok(stream),
660 Err(e) => {
661 last_error = e.to_string();
662 warn!("rendezvous attempt failed: {last_error}");
663 }
664 }
665 }
666 }
667 err(format!("all rendezvous attempts failed: {last_error}"))
668}