1use std::collections::HashSet;
14use std::future::Future;
15use std::net::{IpAddr, SocketAddr};
16use std::path::PathBuf;
17use std::sync::Arc;
18use std::time::{Duration, Instant};
19
20use iroh::PublicKey;
21use iroh_base::CustomAddr;
22use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
23use tokio::net::{TcpStream, lookup_host};
24use tokio::sync::mpsc;
25use tokio::time::{sleep, timeout};
26use tokio_rustls::client::TlsStream;
27use tokio_util::sync::CancellationToken;
28use tracing::warn;
29
30use crate::error::{Result, SyncwebError};
31
32use super::RelayConfig;
33use super::certs::{self, RelayCerts};
34use super::pool;
35use super::protocol::{
36 MAX_BYTE_FIELD, Message, PROTOCOL_NAME, read_data_frame, read_message, write_data_frame, write_message,
37};
38use super::transport::{Backend, IncomingPacket, Session, SyncthingRelayTransport, own_addr, peer_addr};
39
40const DEFAULT_RELAY_PORT: u16 = 22_067;
42const SESSION_QUEUE: usize = 1024;
44const RELAY_REFRESH: Duration = Duration::from_mins(30);
46const POOL_RETRY: Duration = Duration::from_secs(30);
48const CONTROL_IDLE: Duration = Duration::from_mins(1);
50const JOIN_BACKOFF: Duration = Duration::from_secs(2);
52const MAX_DIAL_ATTEMPTS: usize = 4;
54const UNKNOWN_NODE: [u8; 32] = [0_u8; 32];
56
57#[derive(Clone, Debug)]
59#[non_exhaustive]
60pub struct RelayManagerOptions {
61 pub config: RelayConfig,
63 pub data_dir: PathBuf,
65}
66
67impl RelayManagerOptions {
68 #[must_use]
70 pub const fn new(config: RelayConfig, data_dir: PathBuf) -> Self {
71 Self { config, data_dir }
72 }
73}
74
75#[derive(Clone)]
78pub struct RelayManager {
79 backend: Arc<Backend>,
80 config: RelayConfig,
81 cancel: CancellationToken,
82}
83
84impl std::fmt::Debug for RelayManager {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 f.debug_struct("RelayManager").finish_non_exhaustive()
87 }
88}
89
90impl RelayManager {
91 pub fn start(options: RelayManagerOptions, own_node: PublicKey) -> Result<Self> {
99 let certs = Arc::new(RelayCerts::load_or_create(&options.data_dir)?);
100 let connector = certs::tls_connector(&certs)?;
101 let backend = Arc::new(Backend::new(certs, connector, options.config.timeout));
102 backend.update_local_addr(own_addr(&own_node, backend.certs.relay_id()));
105 let cancel = CancellationToken::new();
106 let _spawned = tokio::spawn(supervisor(
107 backend.clone(),
108 options.config.clone(),
109 own_node,
110 cancel.clone(),
111 ));
112 Ok(Self {
113 backend,
114 config: options.config,
115 cancel,
116 })
117 }
118
119 #[must_use]
121 pub fn transport(&self) -> SyncthingRelayTransport {
122 SyncthingRelayTransport::new(self.backend.clone())
123 }
124
125 #[must_use]
127 pub fn local_addr(&self) -> CustomAddr {
128 self.backend.local_addr()
129 }
130
131 #[must_use]
133 pub fn relay_id(&self) -> [u8; 32] {
134 *self.backend.certs.relay_id()
135 }
136
137 pub fn remember_node(&self, relay_id: &[u8; 32], node: PublicKey) {
140 self.backend.remember_node(*relay_id, *node.as_bytes());
141 }
142
143 pub async fn test_relay(&self, url: &str) -> Result<TestRelayReport> {
152 let started = Instant::now();
153 let (host, port, token) = parse_relay_url(url)?;
154 let mut stream = control_connect(&self.backend, url, &host, port).await?;
155 limited(
156 self.config.timeout,
157 write_message(
158 &mut stream,
159 &Message::JoinRelayRequest {
160 token: token.unwrap_or_default(),
161 },
162 ),
163 )
164 .await?;
165 let reply = limited(self.config.timeout, read_message(&mut stream)).await?;
166 match &reply {
167 Message::Response {
168 code: Message::RESPONSE_SUCCESS | Message::RESPONSE_ALREADY_CONNECTED,
169 ..
170 } => {}
171 Message::Response { code, message } => {
172 return Err(SyncwebError::RelayUnreachable {
173 reasons: format!("{url} rejected our registration (code {code}): {message}"),
174 });
175 }
176 Message::RelayFull => {
177 return Err(SyncwebError::RelayUnreachable {
178 reasons: format!("{url} is full"),
179 });
180 }
181 Message::Ping
182 | Message::Pong
183 | Message::JoinRelayRequest { .. }
184 | Message::JoinSessionRequest { .. }
185 | Message::ConnectRequest { .. }
186 | Message::SessionInvitation { .. } => {
187 return Err(unexpected_reply(&reply, "unexpected reply to join request"));
188 }
189 }
190 Ok(TestRelayReport {
191 url: url.to_owned(),
192 own_relay_id: *self.backend.certs.relay_id(),
193 elapsed: started.elapsed(),
194 })
195 }
196
197 #[doc(hidden)]
207 pub fn dial_peer_for_test(&self, peer: [u8; 32], node: PublicKey) -> Result<()> {
208 dial_peer(&self.backend, peer, *node.as_bytes())
209 }
210
211 pub async fn shutdown(&self) {
213 self.cancel.cancel();
214 self.backend.cancel.cancel();
215 sleep(Duration::from_millis(50)).await;
216 }
217}
218
219#[derive(Clone, Debug, Eq, PartialEq)]
221#[non_exhaustive]
222pub struct TestRelayReport {
223 pub url: String,
225 pub own_relay_id: [u8; 32],
227 pub elapsed: Duration,
229}
230
231impl std::fmt::Display for TestRelayReport {
232 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233 write!(
234 f,
235 "relay {} reachable in {:?} (device {})",
236 self.url,
237 self.elapsed,
238 device_id(self.own_relay_id)
239 )
240 }
241}
242
243fn unexpected_reply(reply: &Message, context: &str) -> SyncwebError {
245 SyncwebError::RelayDecode(format!("{context}: {reply:?}"))
246}
247
248pub(crate) fn start_peer_dial(backend: Arc<Backend>, peer: [u8; 32], node: [u8; 32]) {
252 if !backend.dials.lock().insert(peer) {
253 return;
254 }
255 tokio::spawn(async move {
256 let result = dial_peer(&backend, peer, node);
257 if result.is_ok() {
258 for _ in 0..150 {
262 if backend.sessions.read().contains_key(&peer) {
263 break;
264 }
265 sleep(Duration::from_millis(100)).await;
266 }
267 }
268 backend.dials.lock().remove(&peer);
269 if let Err(error) = result {
270 warn!(%error, "relay dial to peer failed");
271 }
272 });
273}
274
275fn dial_peer(backend: &Arc<Backend>, peer: [u8; 32], node: [u8; 32]) -> Result<()> {
283 if peer == [0_u8; 32] {
284 return Err(SyncwebError::RelayDecode("peer device id is empty".to_owned()));
285 }
286 backend.remember_node(peer, node);
289 let mut failures = Vec::new();
290 for _ in 0..MAX_DIAL_ATTEMPTS {
291 let Some(url) = backend.next_relay() else {
292 failures.push("no relays configured".to_owned());
293 break;
294 };
295 let sender = backend.control.read().get(&url).cloned();
296 match sender {
297 Some(control) => match control.send(Message::ConnectRequest { id: peer.to_vec() }) {
298 Ok(()) => return Ok(()),
299 Err(_) => failures.push(format!("{url}: registration connection closed")),
300 },
301 None => failures.push(format!("{url}: not registered")),
302 }
303 }
304 Err(SyncwebError::RelayUnreachable {
305 reasons: failures.join("; "),
306 })
307}
308
309struct SessionSpec {
311 key: Vec<u8>,
312 address: Vec<u8>,
313 port: u16,
314}
315
316async fn join_loop(backend: Arc<Backend>, url: String, cancel: CancellationToken) {
319 loop {
320 if cancel.is_cancelled() {
321 return;
322 }
323 let (host, port, token) = match parse_relay_url(&url) {
324 Ok(parts) => parts,
325 Err(error) => {
326 warn!(%url, %error, "invalid relay url");
327 return;
328 }
329 };
330 if let Err(error) = serve_registration(&backend, &url, &host, port, token).await {
331 warn!(%url, %error, "relay registration dropped");
332 }
333 sleep_with_cancel(cancel.clone(), JOIN_BACKOFF).await;
334 }
335}
336
337async fn serve_registration(
340 backend: &Arc<Backend>,
341 url: &str,
342 host: &str,
343 port: u16,
344 token: Option<String>,
345) -> Result<()> {
346 let mut stream = control_connect(backend, url, host, port).await?;
347 limited(
348 backend.timeout,
349 write_message(
350 &mut stream,
351 &Message::JoinRelayRequest {
352 token: token.unwrap_or_default(),
353 },
354 ),
355 )
356 .await?;
357 let reply = limited(backend.timeout, read_message(&mut stream)).await?;
358 match &reply {
359 Message::Response {
360 code: Message::RESPONSE_SUCCESS | Message::RESPONSE_ALREADY_CONNECTED,
361 ..
362 } => {}
363 Message::Response { code, message } => {
364 return Err(SyncwebError::RelayUnreachable {
365 reasons: format!("{url} rejected registration (code {code}): {message}"),
366 });
367 }
368 Message::RelayFull => {
369 return Err(SyncwebError::RelayUnreachable {
370 reasons: format!("{url} is full"),
371 });
372 }
373 Message::Ping
374 | Message::Pong
375 | Message::JoinRelayRequest { .. }
376 | Message::JoinSessionRequest { .. }
377 | Message::ConnectRequest { .. }
378 | Message::SessionInvitation { .. } => {
379 return Err(unexpected_reply(&reply, "unexpected reply to join"));
380 }
381 }
382
383 let (mut reader, mut writer) = tokio::io::split(stream);
388 let (tx, mut rx) = mpsc::unbounded_channel::<Message>();
389 backend.control.write().insert(url.to_owned(), tx.clone());
390 let control_tx = tx.clone();
391 let writer_tx = tx;
392 let writer_backend = backend.clone();
393 let writer_url = url.to_owned();
394 let writer_cancel = backend.cancel.clone();
395 tokio::spawn(async move {
396 loop {
397 tokio::select! {
398 () = writer_cancel.cancelled() => break,
399 incoming = rx.recv() => {
400 let Some(message) = incoming else { break };
401 if write_message(&mut writer, &message).await.is_err() {
402 break;
403 }
404 }
405 }
406 }
407 let mut control = writer_backend.control.write();
408 if control
409 .get(&writer_url)
410 .is_some_and(|existing| existing.same_channel(&writer_tx))
411 {
412 control.remove(&writer_url);
413 }
414 });
415 loop {
416 let message = match limited(backend.timeout.max(CONTROL_IDLE), read_message(&mut reader)).await {
417 Err(error) => return Err(error),
418 Ok(message) => message,
419 };
420 match &message {
421 Message::Ping => {
422 let _ = control_tx.send(Message::Pong);
423 }
424 Message::Pong | Message::RelayFull => {}
425 Message::SessionInvitation {
426 from: inviter,
427 key,
428 address,
429 port: invite_port,
430 ..
431 } => {
432 let from = try_into_32(inviter)?;
433 let node = backend.node_ids.read().get(&from).copied().unwrap_or(UNKNOWN_NODE);
434 let spec = SessionSpec {
435 key: key.clone(),
436 address: address.clone(),
437 port: *invite_port,
438 };
439 tokio::spawn(inbound_session(backend.clone(), url.to_owned(), from, node, spec));
440 }
441 Message::JoinRelayRequest { .. }
442 | Message::JoinSessionRequest { .. }
443 | Message::Response { .. }
444 | Message::ConnectRequest { .. } => {
445 return Err(unexpected_reply(
446 &message,
447 "unexpected message on registration connection",
448 ));
449 }
450 }
451 }
452}
453
454async fn inbound_session(backend: Arc<Backend>, url: String, peer: [u8; 32], node: [u8; 32], spec: SessionSpec) {
457 if let Err(error) = establish_session(&backend, &url, peer, node, spec).await {
458 warn!(%url, %error, "inbound session failed");
459 }
460}
461
462async fn establish_session(
464 backend: &Arc<Backend>,
465 url: &str,
466 peer: [u8; 32],
467 node: [u8; 32],
468 spec: SessionSpec,
469) -> Result<()> {
470 let addrs = session_addrs(url, spec.address.as_slice(), spec.port).await?;
471 let mut last_error = None;
472 for addr in addrs {
473 match join_session(backend, url, peer, node, addr, &spec.key).await {
474 Ok(()) => return Ok(()),
475 Err(error) => last_error = Some(error),
476 }
477 }
478 Err(last_error.unwrap_or_else(|| SyncwebError::RelayUnreachable {
479 reasons: format!("{url}: no session address was usable"),
480 }))
481}
482
483async fn join_session(
485 backend: &Arc<Backend>,
486 url: &str,
487 peer: [u8; 32],
488 node: [u8; 32],
489 addr: SocketAddr,
490 key: &[u8],
491) -> Result<()> {
492 if key.len() > MAX_BYTE_FIELD {
493 return Err(SyncwebError::RelayDecode(
494 "session key exceeds the protocol limit".to_owned(),
495 ));
496 }
497 let mut stream = limited_io(url, backend.timeout, TcpStream::connect(addr)).await?;
498 limited(
499 backend.timeout,
500 write_message(&mut stream, &Message::JoinSessionRequest { key: key.to_vec() }),
501 )
502 .await?;
503 let reply = limited(backend.timeout, read_message(&mut stream)).await?;
504 match &reply {
505 Message::Response {
506 code: Message::RESPONSE_SUCCESS,
507 ..
508 } => {}
509 Message::Response { code, message } => {
510 return Err(SyncwebError::RelayUnreachable {
511 reasons: format!("{url}: session join rejected (code {code}): {message}"),
512 });
513 }
514 Message::Ping
515 | Message::Pong
516 | Message::JoinRelayRequest { .. }
517 | Message::JoinSessionRequest { .. }
518 | Message::ConnectRequest { .. }
519 | Message::SessionInvitation { .. }
520 | Message::RelayFull => {
521 return Err(unexpected_reply(&reply, "unexpected reply to session join"));
522 }
523 }
524 register_session(backend, peer, node, stream);
525 Ok(())
526}
527
528fn register_session(backend: &Arc<Backend>, peer: [u8; 32], node: [u8; 32], stream: TcpStream) {
530 let (read_half, write_half) = stream.into_split();
531 let (tx, rx) = mpsc::channel::<bytes::Bytes>(SESSION_QUEUE);
532 let cancel = backend.cancel.clone();
533 let session = Arc::new(Session {
534 peer,
535 peer_node: node,
536 queue: tx,
537 cancel: cancel.clone(),
538 });
539 {
540 let mut sessions = backend.sessions.write();
541 if let Some(previous) = sessions.insert(peer, session.clone()) {
542 previous.cancel.cancel();
543 }
544 }
545 tokio::spawn(run_session_reader(backend.clone(), session.clone(), read_half));
546 tokio::spawn(run_session_writer(backend.clone(), session, write_half, rx, cancel));
547}
548
549async fn run_session_reader(backend: Arc<Backend>, session: Arc<Session>, mut read_half: OwnedReadHalf) {
551 let remote = peer_addr(session.peer, session.peer_node);
552 let local = backend.local_addr();
553 while let Ok(frame) = read_data_frame(&mut read_half).await {
554 let Some(tx) = backend.ingress.lock().clone() else {
555 continue;
556 };
557 if tx
558 .send(IncomingPacket {
559 remote: remote.clone(),
560 local: local.clone(),
561 data: bytes::Bytes::from(frame),
562 })
563 .is_err()
564 {
565 break;
566 }
567 }
568 remove_session(&backend, session.peer, &session);
569}
570
571async fn run_session_writer(
573 backend: Arc<Backend>,
574 session: Arc<Session>,
575 mut write_half: OwnedWriteHalf,
576 mut rx: mpsc::Receiver<bytes::Bytes>,
577 cancel: CancellationToken,
578) {
579 loop {
580 let data = tokio::select! {
581 () = cancel.cancelled() => break,
582 data = rx.recv() => match data {
583 Some(packet) => packet,
584 None => break,
585 },
586 };
587 if write_data_frame(&mut write_half, &data).await.is_err() {
588 break;
589 }
590 }
591 remove_session(&backend, session.peer, &session);
592}
593
594fn remove_session(backend: &Backend, peer: [u8; 32], session: &Arc<Session>) {
596 let mut sessions = backend.sessions.write();
597 if sessions
598 .get(&peer)
599 .is_some_and(|existing| Arc::ptr_eq(existing, session))
600 {
601 sessions.remove(&peer);
602 }
603}
604
605async fn supervisor(backend: Arc<Backend>, config: RelayConfig, own_node: PublicKey, cancel: CancellationToken) {
608 let mut running: HashSet<String> = HashSet::new();
609 let mut pool_seen = false;
610 loop {
611 if cancel.is_cancelled() {
612 return;
613 }
614 let urls = match relay_list(&config, pool_seen).await {
615 Ok(urls) => urls,
616 Err(error) => {
617 warn!(%error, "relay list unavailable");
618 sleep_with_cancel(cancel.clone(), POOL_RETRY).await;
619 continue;
620 }
621 };
622 pool_seen = true;
623 let addr = own_addr(&own_node, backend.certs.relay_id());
624 let stored = urls.clone();
625 *backend.relays.write() = stored;
626 backend.update_local_addr(addr);
627 for url in urls {
628 if running.insert(url.clone()) {
629 tokio::spawn(join_loop(backend.clone(), url, cancel.clone()));
630 }
631 }
632 sleep_with_cancel(cancel.clone(), RELAY_REFRESH).await;
633 }
634}
635
636async fn relay_list(config: &RelayConfig, pool_seen: bool) -> Result<Vec<String>> {
639 if !config.relay_urls.is_empty() {
640 return Ok(config.relay_urls.clone());
641 }
642 if !config.auto_fallback {
643 return Ok(Vec::new());
644 }
645 match pool::fetch_relay_pool(config.timeout.max(Duration::from_secs(5))).await {
646 Ok(urls) if urls.is_empty() => Err(SyncwebError::RelayUnreachable {
647 reasons: "relay pool returned no relays".to_owned(),
648 }),
649 Ok(urls) => Ok(urls),
650 Err(error) => {
651 if !pool_seen {
652 warn!(%error, "initial relay pool fetch failed");
653 }
654 Err(error)
655 }
656 }
657}
658
659async fn sleep_with_cancel(cancel: CancellationToken, duration: Duration) {
661 tokio::select! {
662 () = cancel.cancelled() => {}
663 () = sleep(duration) => {}
664 }
665}
666
667async fn control_connect(backend: &Backend, url: &str, host: &str, port: u16) -> Result<TlsStream<TcpStream>> {
669 let socket = resolve(host, port)
670 .await
671 .ok_or_else(|| SyncwebError::RelayUnreachable {
672 reasons: format!("{url}: host {host} did not resolve"),
673 })?;
674 let name = certs::server_name_for(host)?;
675 let tcp = limited_io(url, backend.timeout, TcpStream::connect(socket)).await?;
676 let tls = limited_io(url, backend.timeout, backend.connector.connect(name, tcp)).await?;
677 if let Some(named) = tls.get_ref().1.alpn_protocol()
678 && named != PROTOCOL_NAME
679 {
680 return Err(SyncwebError::RelayUnreachable {
681 reasons: format!(
682 "{url}: relay negotiated unexpected ALPN protocol {:?}",
683 String::from_utf8_lossy(named)
684 ),
685 });
686 }
687 Ok(tls)
688}
689
690async fn resolve(host: &str, port: u16) -> Option<SocketAddr> {
692 lookup_host((host, port)).await.ok()?.next()
693}
694
695async fn session_addrs(url: &str, address: &[u8], invite_port: u16) -> Result<Vec<SocketAddr>> {
698 let port = if invite_port == 0 {
699 DEFAULT_RELAY_PORT
700 } else {
701 invite_port
702 };
703 if address.is_empty() || address.iter().all(|byte| *byte == 0) {
704 let (host, _token, _port) = parse_relay_url(url)?;
705 let addresses = lookup_host((host.as_str(), port)).await?.collect::<Vec<SocketAddr>>();
706 if addresses.is_empty() {
707 return Err(SyncwebError::RelayUnreachable {
708 reasons: format!("{url}: session host {host} did not resolve"),
709 });
710 }
711 Ok(addresses)
712 } else {
713 Ok(vec![SocketAddr::new(ip_from_bytes(address)?, port)])
714 }
715}
716
717fn ip_from_bytes(bytes: &[u8]) -> Result<IpAddr> {
719 match bytes.len() {
720 4 => Ok(IpAddr::from(try_into_array::<4>(bytes)?)),
721 16 => Ok(IpAddr::from(try_into_array::<16>(bytes)?)),
722 length => Err(SyncwebError::RelayDecode(format!(
723 "invitation address has invalid length {length}"
724 ))),
725 }
726}
727
728fn try_into_array<const N: usize>(bytes: &[u8]) -> Result<[u8; N]> {
730 bytes.try_into().map_err(|error| {
731 SyncwebError::RelayDecode(format!("byte field has unexpected length {}: {error}", bytes.len()))
732 })
733}
734
735fn try_into_32(bytes: &[u8]) -> Result<[u8; 32]> {
737 try_into_array(bytes)
738}
739
740fn parse_relay_url(url: &str) -> Result<(String, u16, Option<String>)> {
742 let parsed = url::Url::parse(url).map_err(|error| SyncwebError::RelayBadAddress(format!("{url}: {error}")))?;
743 match parsed.scheme() {
744 "relay" | "tcp" => {}
745 _ => return Err(SyncwebError::RelayBadScheme),
746 }
747 let host = parsed
748 .host_str()
749 .filter(|host| !host.is_empty())
750 .ok_or_else(|| SyncwebError::RelayBadAddress(url.to_owned()))?;
751 let port = parsed.port().unwrap_or(DEFAULT_RELAY_PORT);
752 let token = parsed
753 .query_pairs()
754 .find(|(key, _)| key == "token")
755 .map(|(_, value)| value.into_owned());
756 Ok((host.to_owned(), port, token))
757}
758
759async fn limited<T>(duration: Duration, future: impl Future<Output = Result<T>>) -> Result<T> {
762 timeout(duration, future).await.map_or_else(
763 |_| {
764 Err(SyncwebError::RelayUnreachable {
765 reasons: "relay operation timed out".to_owned(),
766 })
767 },
768 std::convert::identity,
769 )
770}
771
772async fn limited_io<T>(
775 context: &str,
776 duration: Duration,
777 future: impl Future<Output = std::io::Result<T>>,
778) -> Result<T> {
779 match timeout(duration, future).await {
780 Err(_) => Err(SyncwebError::RelayUnreachable {
781 reasons: format!("{context}: operation timed out"),
782 }),
783 Ok(Err(error)) => Err(SyncwebError::RelayUnreachable {
784 reasons: format!("{context}: {error}"),
785 }),
786 Ok(Ok(value)) => Ok(value),
787 }
788}
789
790#[must_use]
792fn device_id(bytes: [u8; 32]) -> String {
793 use std::fmt::Write as _;
794 let mut out = String::with_capacity(64);
795 for (index, byte) in bytes.iter().enumerate() {
796 if index == 8 || index == 16 || index == 24 {
797 out.push('-');
798 }
799 let _ = write!(out, "{byte:02X}");
800 }
801 out
802}