1use 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#[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(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#[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#[derive(derive_more::Debug, Clone)]
143pub struct ClientBuilder {
144 #[debug("address family selector callback")]
146 address_family_selector: Option<Arc<dyn Fn() -> bool + Send + Sync>>,
147 url: RelayUrl,
149 tls_config: Option<rustls::ClientConfig>,
151 proxy_url: Option<Url>,
153 secret_key: SecretKey,
155 auth_token: Option<String>,
160 #[cfg(not(wasm_browser))]
161 dns_resolver: DnsResolver,
162 key_cache: KeyCache,
164}
165
166impl ClientBuilder {
167 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 pub fn tls_client_config(mut self, tls_config: rustls::ClientConfig) -> Self {
209 self.tls_config = Some(tls_config);
210 self
211 }
212
213 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 pub fn proxy_url(mut self, url: Url) -> Self {
230 self.proxy_url.replace(url);
231 self
232 }
233
234 pub fn auth_token(mut self, token: impl Into<String>) -> Self {
245 self.auth_token = Some(token.into());
246 self
247 }
248
249 pub fn key_cache_capacity(mut self, capacity: usize) -> Self {
251 self.key_cache = KeyCache::new(capacity);
252 self
253 }
254
255 #[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 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 .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 #[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 #[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 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#[derive(Debug)]
448pub struct Client {
449 conn: Conn,
450 local_addr: Option<SocketAddr>,
451}
452
453impl Client {
454 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#[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#[derive(Debug)]
541pub struct ClientStream {
542 stream: SplitStream<Conn>,
543 local_addr: Option<SocketAddr>,
544}
545
546impl ClientStream {
547 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}