Skip to main content

iroh_relay/
client.rs

1//! Exposes [`Client`], which allows to establish connections to a relay server.
2//!
3//! Based on tailscale/derp/derphttp/derphttp_client.go
4
5use std::{
6    net::SocketAddr,
7    pin::Pin,
8    sync::Arc,
9    task::{self, Poll},
10};
11
12use conn::Conn;
13use iroh_base::{RelayUrl, SecretKey};
14#[cfg(not(wasm_browser))]
15use iroh_dns::dns::{DnsError, DnsResolver};
16#[cfg(wasm_browser)]
17use n0_error::StdResultExt;
18use n0_error::{AnyError, e, stack_error};
19use n0_future::{
20    Sink, Stream,
21    split::{SplitSink, SplitStream, split},
22    time,
23};
24use tracing::{debug, trace};
25use url::Url;
26
27pub use self::conn::{RecvError, SendError};
28use crate::{
29    KeyCache,
30    http::{ProtocolVersion, RELAY_PATH},
31    protos::{
32        handshake,
33        relay::{ClientToRelayMsg, RelayToClientMsg},
34    },
35};
36
37pub(crate) mod conn;
38#[cfg(not(wasm_browser))]
39pub(crate) mod streams;
40#[cfg(not(wasm_browser))]
41mod tls;
42#[cfg(not(wasm_browser))]
43mod util;
44
45/// Connection errors.
46///
47/// `ConnectError` contains `DialError`, errors that can occur while dialing the
48/// relay, as well as errors that occur while creating or maintaining a connection.
49#[stack_error(derive, add_meta, from_sources)]
50#[allow(missing_docs)]
51#[non_exhaustive]
52pub enum ConnectError {
53    #[error("Invalid URL for websocket: {url}")]
54    InvalidWebsocketUrl { url: Url },
55    #[error("Invalid relay URL: {url}")]
56    InvalidRelayUrl { url: Url },
57    /// Error returned from the underlying WebSocket stream while establishing the connection.
58    ///
59    /// The concrete error type is `tokio_websockets::Error` on native targets and
60    /// `ws_stream_wasm::WsErr` on `wasm_browser` targets. Use [`AnyError::downcast_ref`] to
61    /// recover it. Note that the concrete downcast type is not covered by any semver
62    /// guarantees and may change between releases.
63    #[error(transparent)]
64    Websocket { source: AnyError },
65    #[error(
66        "Server replied with invalid iroh-relay version header: {}",
67        server_version.as_deref().unwrap_or("<empty>")
68    )]
69    BadVersionHeader { server_version: Option<String> },
70    #[error("Authorization token set to a string that is not a valid HTTP header value")]
71    InvalidAuthToken,
72    #[error(transparent)]
73    Handshake {
74        #[error(std_err)]
75        source: handshake::Error,
76    },
77    #[error(transparent)]
78    Dial { source: DialError },
79    #[error("Unexpected status during upgrade: {code}")]
80    UnexpectedUpgradeStatus { code: hyper::StatusCode },
81    #[error("Failed to upgrade response")]
82    Upgrade {
83        #[error(std_err)]
84        source: hyper::Error,
85    },
86    #[error("Invalid TLS servername")]
87    InvalidTlsServername {},
88    #[error("No local address available")]
89    NoLocalAddr {},
90    #[error("tls connection failed")]
91    Tls {
92        #[error(std_err)]
93        source: std::io::Error,
94    },
95    #[error(
96        "No rustls crypto provider configured while both ring and aws-lc-rs feature flags are disabled"
97    )]
98    MissingCryptoProvider,
99    #[cfg(wasm_browser)]
100    #[error("The relay protocol is not available in browsers")]
101    RelayProtoNotAvailable {},
102}
103
104/// Errors that can occur while dialing the relay server.
105#[stack_error(derive, add_meta, from_sources)]
106#[allow(missing_docs)]
107#[non_exhaustive]
108pub enum DialError {
109    #[error("Invalid target port")]
110    InvalidTargetPort {},
111    #[error(transparent)]
112    #[cfg(not(wasm_browser))]
113    Dns { source: DnsError },
114    #[error(transparent)]
115    Timeout {
116        #[error(std_err)]
117        source: time::Elapsed,
118    },
119    #[error(transparent)]
120    Io {
121        #[error(std_err)]
122        source: std::io::Error,
123    },
124    #[error("Invalid URL: {url}")]
125    InvalidUrl { url: Url },
126    #[error("Failed proxy connection: {status}")]
127    ProxyConnectInvalidStatus { status: hyper::StatusCode },
128    #[error("Invalid Proxy URL {proxy_url}")]
129    ProxyInvalidUrl { proxy_url: Url },
130    #[error("failed to establish proxy connection")]
131    ProxyConnect {
132        #[error(std_err)]
133        source: hyper::Error,
134    },
135    #[error("Invalid proxy TLS servername: {proxy_hostname}")]
136    ProxyInvalidTlsServername { proxy_hostname: String },
137    #[error("Invalid proxy target port")]
138    ProxyInvalidTargetPort {},
139}
140
141/// Build a Client.
142#[derive(derive_more::Debug, Clone)]
143pub struct ClientBuilder {
144    /// Default is None
145    #[debug("address family selector callback")]
146    address_family_selector: Option<Arc<dyn Fn() -> bool + Send + Sync>>,
147    /// Server url.
148    url: RelayUrl,
149    /// TLS verification config.
150    tls_config: Option<rustls::ClientConfig>,
151    /// HTTP Proxy
152    proxy_url: Option<Url>,
153    /// The secret key of this client.
154    secret_key: SecretKey,
155    /// Optional authorization token.
156    ///
157    /// Sent as an `Authorization: Bearer` header on native targets and as
158    /// a `?token=` query parameter under Wasm. See [`ClientBuilder::auth_token`].
159    auth_token: Option<String>,
160    #[cfg(not(wasm_browser))]
161    dns_resolver: DnsResolver,
162    /// Cache for public keys of remote endpoints.
163    key_cache: KeyCache,
164}
165
166impl ClientBuilder {
167    /// Create a new [`ClientBuilder`]
168    pub fn new(
169        url: impl Into<RelayUrl>,
170        secret_key: SecretKey,
171        #[cfg(not(wasm_browser))] dns_resolver: DnsResolver,
172    ) -> Self {
173        ClientBuilder {
174            address_family_selector: None,
175            url: url.into(),
176            tls_config: None,
177            proxy_url: None,
178            secret_key,
179            #[cfg(not(wasm_browser))]
180            dns_resolver,
181            key_cache: KeyCache::new(128),
182            auth_token: None,
183        }
184    }
185
186    /// Sets a custom TLS config.
187    ///
188    /// This is a required option.
189    ///
190    /// You can construct a [`rustls::ClientConfig`] by combining a [`rustls::crypto::CryptoProvider`]
191    /// with a [`tls::CaTlsConfig`] using [`tls::CaTlsConfig::client_config`], for example:
192    ///
193    /// ```no_run
194    /// use std::sync::Arc;
195    ///
196    /// use iroh_relay::tls::CaTlsConfig;
197    ///
198    /// let crypto_provider: rustls::crypto::CryptoProvider = todo!();
199    /// let client_config = CaTlsConfig::default().client_config(Arc::new(crypto_provider));
200    /// ```
201    ///
202    /// If you enable the tls-ring or tls-aws-lc-rs feature, you can use the enabled crypto provider
203    /// by using [`tls::default_provider`].
204    ///
205    /// [`tls::CaTlsConfig`]: crate::tls::CaTlsConfig
206    /// [`tls::CaTlsConfig::client_config`]: crate::tls::CaTlsConfig::client_config
207    /// [`tls::default_provider`]: crate::tls::default_provider
208    pub fn tls_client_config(mut self, tls_config: rustls::ClientConfig) -> Self {
209        self.tls_config = Some(tls_config);
210        self
211    }
212
213    /// Sets a callback hinting whether to prefer IPv6 when dialing the relay.
214    ///
215    /// The callback runs on each dial. When it returns `true`, IPv6 addresses
216    /// are tried first and IPv4 dials are held back slightly, biasing the
217    /// happy-eyeballs race towards IPv6; when it returns `false`, IPv4 is
218    /// preferred. Only return `true` when IPv6 is expected to work, since
219    /// otherwise the bias just delays the connection.
220    pub fn address_family_selector<S>(mut self, selector: S) -> Self
221    where
222        S: Fn() -> bool + Send + Sync + 'static,
223    {
224        self.address_family_selector = Some(Arc::new(selector));
225        self
226    }
227
228    /// Set an explicit proxy url to proxy all HTTP(S) traffic through.
229    pub fn proxy_url(mut self, url: Url) -> Self {
230        self.proxy_url.replace(url);
231        self
232    }
233
234    /// Sets an authorization token.
235    ///
236    /// On native targets, the token is sent as an `Authorization: Bearer TOKEN`
237    /// header on the WebSocket upgrade request that establishes the relay
238    /// connection. The token must be a valid HTTP header field value, if not
239    /// [`Self::connect`] will return [`ConnectError::InvalidAuthToken`].
240    ///
241    /// When compiled to WebAssembly the token is sent as a `?token=TOKEN`
242    /// query parameter on the upgrade URL, since browsers don't allow setting
243    /// headers on WebSocket requests.
244    pub fn auth_token(mut self, token: impl Into<String>) -> Self {
245        self.auth_token = Some(token.into());
246        self
247    }
248
249    /// Set the capacity of the cache for public keys.
250    pub fn key_cache_capacity(mut self, capacity: usize) -> Self {
251        self.key_cache = KeyCache::new(capacity);
252        self
253    }
254
255    /// Establishes a new connection to the relay server.
256    #[cfg(not(wasm_browser))]
257    pub async fn connect(&self) -> Result<Client, ConnectError> {
258        use http::header::{AUTHORIZATION, HeaderValue, SEC_WEBSOCKET_PROTOCOL};
259        use n0_error::StdResultExt;
260        use tls::MaybeTlsStreamBuilder;
261
262        use crate::{
263            http::CLIENT_AUTH_HEADER,
264            protos::{handshake::KeyMaterialClientAuth, relay::MAX_FRAME_SIZE},
265        };
266
267        let mut dial_url = (*self.url).clone();
268        dial_url.set_path(RELAY_PATH);
269        // The relay URL is exchanged with the http(s) scheme in tickets and similar.
270        // We need to use the ws:// or wss:// schemes when connecting with websockets, though.
271        dial_url
272            .set_scheme(match self.url.scheme() {
273                "http" => "ws",
274                "ws" => "ws",
275                _ => "wss",
276            })
277            .map_err(|_| {
278                e!(ConnectError::InvalidWebsocketUrl {
279                    url: dial_url.clone()
280                })
281            })?;
282
283        debug!(%dial_url, "Dialing relay by websocket");
284
285        let tls_config = self
286            .tls_config
287            .clone()
288            .ok_or_else(|| e!(ConnectError::MissingCryptoProvider))?;
289
290        #[allow(unused_mut)]
291        let mut builder =
292            MaybeTlsStreamBuilder::new(dial_url.clone(), self.dns_resolver.clone(), tls_config)
293                .prefer_ipv6(self.prefer_ipv6())
294                .proxy_url(self.proxy_url.clone());
295
296        let stream = builder.connect().await?;
297        let local_addr = stream
298            .as_ref()
299            .local_addr()
300            .map_err(|_| e!(ConnectError::NoLocalAddr))?;
301
302        let mut builder = tokio_websockets::ClientBuilder::new()
303            .uri(dial_url.as_str())
304            .map_err(|_| {
305                e!(ConnectError::InvalidRelayUrl {
306                    url: dial_url.clone()
307                })
308            })?
309            .add_header(
310                SEC_WEBSOCKET_PROTOCOL,
311                ProtocolVersion::all_as_header_value(),
312            )
313            .expect("valid header name and value")
314            .limits(tokio_websockets::Limits::default().max_payload_len(Some(MAX_FRAME_SIZE)))
315            // We turn off automatic flushing after a threshold (the default would be after 8KB).
316            // This means we need to flush manually, which we do by calling `Sink::send_all` or
317            // `Sink::send` (which calls `Sink::flush`) in the `ActiveRelayActor`.
318            .config(tokio_websockets::Config::default().flush_threshold(usize::MAX));
319
320        if let Some(token) = self.auth_token.as_ref() {
321            let value = HeaderValue::from_str(&format!("Bearer {token}"))
322                .map_err(|_| e!(ConnectError::InvalidAuthToken))?;
323            builder = builder
324                .add_header(AUTHORIZATION, value)
325                .expect("valid header name");
326        }
327
328        if let Some(client_auth) = KeyMaterialClientAuth::new(&self.secret_key, &stream) {
329            debug!("Using TLS key export for relay client authentication");
330            builder = builder
331                .add_header(CLIENT_AUTH_HEADER, client_auth.into_header_value())
332                .expect(
333                    "impossible: CLIENT_AUTH_HEADER isn't a disallowed header value for websockets",
334                );
335        }
336        let (conn, response) = builder.connect_on(stream).await.anyerr()?;
337
338        n0_error::ensure!(
339            response.status() == hyper::StatusCode::SWITCHING_PROTOCOLS,
340            ConnectError::UnexpectedUpgradeStatus {
341                code: response.status()
342            }
343        );
344
345        let protocol_version_str = response
346            .headers()
347            .get(SEC_WEBSOCKET_PROTOCOL)
348            .and_then(|s| s.to_str().ok());
349        let protocol_version = protocol_version_str
350            .and_then(ProtocolVersion::match_from_str)
351            .ok_or_else(|| {
352                e!(ConnectError::BadVersionHeader {
353                    server_version: protocol_version_str.map(ToOwned::to_owned)
354                })
355            })?;
356
357        let conn = Conn::new(
358            conn,
359            self.key_cache.clone(),
360            &self.secret_key,
361            protocol_version,
362        )
363        .await?;
364
365        trace!("connect done");
366
367        Ok(Client {
368            conn,
369            local_addr: Some(local_addr),
370        })
371    }
372
373    /// Reports whether IPv4 dials should be slightly
374    /// delayed to give IPv6 a better chance of winning dial races.
375    /// Implementations should only return true if IPv6 is expected
376    /// to succeed. (otherwise delaying IPv4 will delay the connection
377    /// overall)
378    #[cfg(not(wasm_browser))]
379    fn prefer_ipv6(&self) -> bool {
380        match self.address_family_selector {
381            Some(ref selector) => selector(),
382            None => false,
383        }
384    }
385
386    /// Establishes a new connection to the relay server.
387    #[cfg(wasm_browser)]
388    pub async fn connect(&self) -> Result<Client, ConnectError> {
389        use crate::http::AUTH_TOKEN_URL_QUERY_PARAM;
390
391        let mut dial_url = (*self.url).clone();
392        dial_url.set_path(RELAY_PATH);
393        // The relay URL is exchanged with the http(s) scheme in tickets and similar.
394        // We need to use the ws:// or wss:// schemes when connecting with websockets, though.
395        dial_url
396            .set_scheme(match self.url.scheme() {
397                "http" => "ws",
398                "ws" => "ws",
399                _ => "wss",
400            })
401            .map_err(|_| {
402                e!(ConnectError::InvalidWebsocketUrl {
403                    url: dial_url.clone()
404                })
405            })?;
406
407        if let Some(token) = self.auth_token.as_ref() {
408            dial_url
409                .query_pairs_mut()
410                .append_pair(AUTH_TOKEN_URL_QUERY_PARAM, token);
411        }
412
413        debug!(%dial_url, "Dialing relay by websocket");
414
415        let (ws_meta, ws_stream) = ws_stream_wasm::WsMeta::connect(
416            dial_url.as_str(),
417            Some(ProtocolVersion::all().collect()),
418        )
419        .await
420        .anyerr()?;
421
422        let protocol_version =
423            ProtocolVersion::match_from_str(&ws_meta.protocol()).ok_or_else(|| {
424                e!(ConnectError::BadVersionHeader {
425                    server_version: Some(ws_meta.protocol())
426                })
427            })?;
428
429        let conn = Conn::new(
430            ws_stream,
431            self.key_cache.clone(),
432            &self.secret_key,
433            protocol_version,
434        )
435        .await?;
436
437        trace!("connect done");
438
439        Ok(Client {
440            conn,
441            local_addr: None,
442        })
443    }
444}
445
446/// A relay client.
447#[derive(Debug)]
448pub struct Client {
449    conn: Conn,
450    local_addr: Option<SocketAddr>,
451}
452
453impl Client {
454    /// Splits the client into a sink and a stream.
455    pub fn split(self) -> (ClientStream, ClientSink) {
456        let (sink, stream) = split(self.conn);
457        (
458            ClientStream {
459                stream,
460                local_addr: self.local_addr,
461            },
462            ClientSink { sink },
463        )
464    }
465}
466
467impl Stream for Client {
468    type Item = Result<RelayToClientMsg, RecvError>;
469
470    fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Option<Self::Item>> {
471        Pin::new(&mut self.conn).poll_next(cx)
472    }
473}
474
475impl Sink<ClientToRelayMsg> for Client {
476    type Error = SendError;
477
478    fn poll_ready(
479        mut self: Pin<&mut Self>,
480        cx: &mut task::Context<'_>,
481    ) -> Poll<Result<(), Self::Error>> {
482        Pin::new(&mut self.conn).poll_ready(cx)
483    }
484
485    fn start_send(mut self: Pin<&mut Self>, item: ClientToRelayMsg) -> Result<(), Self::Error> {
486        Pin::new(&mut self.conn).start_send(item)
487    }
488
489    fn poll_flush(
490        mut self: Pin<&mut Self>,
491        cx: &mut task::Context<'_>,
492    ) -> Poll<Result<(), Self::Error>> {
493        Pin::new(&mut self.conn).poll_flush(cx)
494    }
495
496    fn poll_close(
497        mut self: Pin<&mut Self>,
498        cx: &mut task::Context<'_>,
499    ) -> Poll<Result<(), Self::Error>> {
500        Pin::new(&mut self.conn).poll_close(cx)
501    }
502}
503
504/// The send half of a relay client.
505#[derive(Debug)]
506pub struct ClientSink {
507    sink: SplitSink<Conn, ClientToRelayMsg>,
508}
509
510impl Sink<ClientToRelayMsg> for ClientSink {
511    type Error = SendError;
512
513    fn poll_ready(
514        mut self: Pin<&mut Self>,
515        cx: &mut task::Context<'_>,
516    ) -> Poll<Result<(), Self::Error>> {
517        Pin::new(&mut self.sink).poll_ready(cx)
518    }
519
520    fn start_send(mut self: Pin<&mut Self>, item: ClientToRelayMsg) -> Result<(), Self::Error> {
521        Pin::new(&mut self.sink).start_send(item)
522    }
523
524    fn poll_flush(
525        mut self: Pin<&mut Self>,
526        cx: &mut task::Context<'_>,
527    ) -> Poll<Result<(), Self::Error>> {
528        Pin::new(&mut self.sink).poll_flush(cx)
529    }
530
531    fn poll_close(
532        mut self: Pin<&mut Self>,
533        cx: &mut task::Context<'_>,
534    ) -> Poll<Result<(), Self::Error>> {
535        Pin::new(&mut self.sink).poll_close(cx)
536    }
537}
538
539/// The receive half of a relay client.
540#[derive(Debug)]
541pub struct ClientStream {
542    stream: SplitStream<Conn>,
543    local_addr: Option<SocketAddr>,
544}
545
546impl ClientStream {
547    /// Returns the local address of the client.
548    pub fn local_addr(&self) -> Option<SocketAddr> {
549        self.local_addr
550    }
551}
552
553impl Stream for ClientStream {
554    type Item = Result<RelayToClientMsg, RecvError>;
555
556    fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Option<Self::Item>> {
557        Pin::new(&mut self.stream).poll_next(cx)
558    }
559}