Skip to content

Commit 3a1592a

Browse files
authored
feat(iroh-relay)!: Implement new handshake protocol, refactor frame types (#3331)
## Description This PR changes the iroh relay handshake and relaying protocol. The main goals are: - Actually authenticate the client by verifying a signed challenge (previously only the client info was signed) - We also allow using TLS extracted keying material as the subject to sign and sending it in an HTTP header, if that is available. When this works (it does so most of the time), it's 1RTT faster. - Use a better encoding for messages (we now use QUIC varints for frame type, use websocket framing for the protocol) - Remove outdated and unused frames (KeepAlive, NotePreferred) There's also some secondary changes: - Conceptually, the relay handshake and relaying protocols are now split: There's a `protos::handshake` module for the handshake, and a `protos::relay` for the send/recv protocol. - I've refactored the streams we have in iroh-relay. We now use this stack: - On the client, we establish TLS streams using `MaybeTlsStream<ProxyStream>`. `MaybeTlsStream` gives us the TLS handshake and `ProxyStream` gives us HTTPS proxy support. - On the server we establish rate-limited TLS streams using `RateLimit<MaybeTlsStream>`. - On top of these two, we stack `WsBytesFramed`, which internally uses `tokio_websockets::WebSocketStream` natively and on the server and `ws_stream_wasm::WsStream` in browsers. This wrapper does the websocket framing and translates opaque `Bytes` messages into websocket bytes frames. - This `WsBytesFramed` is then directly used with the `handshake` protocol on the server and client side. - For the `send_recv` protocol, we then upgrade the server side to `RelayedStream` and the client side to the `Conn` type. - Instead of having `Frame`, `ReceivedMessage` and `SendMessage` as types, we now only have `RelayToClientMsg` and `ClientToRelayMsg` for the send/recv protocol. ## Breaking Changes In iroh: - Connections to older relays don't work anymore. In iroh-relay: - Removed `iroh_relay::client::SendMessage` and `iroh_relay::client::ReceivedMessage` in favor of `ClientToRelayMsg` and `RelayToClientMsg` respectively. - `impl Stream for Client` now produces `RelayToClientMsg` instead of `ReceivedMessage` - `Client` now `impl Sink<ClientToRelayMsg>` instead of `impl Sink<SendMessage>` - Removed `ClientBuilder::is_prober` - Moved `protos::relay::FrameType` to `protos::common::FrameType` and adjusted frame types to those of the current set of protocols ## Change checklist <!-- Remove any that are not relevant. --> - [x] Self-review. - [x] Documentation updates following the [style guide](https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text), if relevant. - [x] Tests if relevant. - [x] All breaking changes documented.
1 parent 0776687 commit 3a1592a

File tree

25 files changed

+2183
-1333
lines changed

25 files changed

+2183
-1333
lines changed

Cargo.lock

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

iroh-relay/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ toml = { version = "0.8", optional = true }
9595
tracing-subscriber = { version = "0.3", features = [
9696
"env-filter",
9797
], optional = true }
98+
blake3 = "1.8.2"
99+
serde_bytes = "0.11.17"
98100

99101
# non-wasm-in-browser dependencies
100102
[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies]

iroh-relay/proptest-regressions/protos/relay.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
# It is recommended to check this file in to source control so that
66
# everyone who runs the test benefits from these saved cases.
77
cc 9295f5287162dfb180e5826e563c2cea08b477b803ef412ff8351eb5c3eb45ef # shrinks to frame = KeepAlive
8+
cc 753aabcf8ae2b4e4a52f451d58339aab85a4b61108afdf4b9600f97b3a33bf42 # shrinks to frame = Health { problem: None }
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Seeds for failure cases proptest has generated in the past. It is
2+
# automatically read and these particular cases re-run before any
3+
# novel cases are generated.
4+
#
5+
# It is recommended to check this file in to source control so that
6+
# everyone who runs the test benefits from these saved cases.
7+
cc 8f4f94b7c917bb0f52d31b529c3d580728ea57954ca45d91768cf4ae745e6eb9 # shrinks to frame = ReceivedDatagrams { remote_node_id: PublicKey(3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29), datagrams: Datagrams { ecn: None, segment_size: Some(43846), .. } }
8+
cc e40ca61e22386f1c76f717f2a6dbba367ea05d906317b3f979b46031567edbca # shrinks to frame = SendDatagrams { dst_node_id: PublicKey(3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29), datagrams: Datagrams { ecn: None, segment_size: Some(44811), .. } }
9+
cc 435ec32fc803db22bf4688a6356878073752b58fcd0b4422876fb3ab2a622684 # shrinks to a huge frame = Health { .. }

iroh-relay/src/client.rs

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,17 @@ use tracing::warn;
2222
use tracing::{debug, event, trace, Level};
2323
use url::Url;
2424

25-
pub use self::conn::{ReceivedMessage, RecvError, SendError, SendMessage};
25+
pub use self::conn::{RecvError, SendError};
2626
#[cfg(not(wasm_browser))]
2727
use crate::dns::{DnsError, DnsResolver};
28-
use crate::{http::RELAY_PATH, protos::relay::SendError as SendRelayError, KeyCache};
28+
use crate::{
29+
http::RELAY_PATH,
30+
protos::{
31+
handshake,
32+
relay::{ClientToRelayMsg, RelayToClientMsg},
33+
},
34+
KeyCache,
35+
};
2936

3037
pub(crate) mod conn;
3138
#[cfg(not(wasm_browser))]
@@ -60,7 +67,7 @@ pub enum ConnectError {
6067
source: ws_stream_wasm::WsErr,
6168
},
6269
#[snafu(transparent)]
63-
Handshake { source: SendRelayError },
70+
Handshake { source: handshake::Error },
6471
#[snafu(transparent)]
6572
Dial { source: DialError },
6673
#[snafu(display("Unexpected status during upgrade: {code}"))]
@@ -117,8 +124,6 @@ pub struct ClientBuilder {
117124
/// Default is None
118125
#[debug("address family selector callback")]
119126
address_family_selector: Option<Arc<dyn Fn() -> bool + Send + Sync>>,
120-
/// Default is false
121-
is_prober: bool,
122127
/// Server url.
123128
url: RelayUrl,
124129
/// Allow self-signed certificates from relay servers
@@ -144,7 +149,6 @@ impl ClientBuilder {
144149
) -> Self {
145150
ClientBuilder {
146151
address_family_selector: None,
147-
is_prober: false,
148152
url: url.into(),
149153

150154
#[cfg(any(test, feature = "test-utils"))]
@@ -172,12 +176,6 @@ impl ClientBuilder {
172176
self
173177
}
174178

175-
/// Indicates this client is a prober
176-
pub fn is_prober(mut self, is: bool) -> Self {
177-
self.is_prober = is;
178-
self
179-
}
180-
181179
/// Skip the verification of the relay server's SSL certificates.
182180
///
183181
/// May only be used in tests.
@@ -202,9 +200,13 @@ impl ClientBuilder {
202200
/// Establishes a new connection to the relay server.
203201
#[cfg(not(wasm_browser))]
204202
pub async fn connect(&self) -> Result<Client, ConnectError> {
203+
use http::header::SEC_WEBSOCKET_PROTOCOL;
205204
use tls::MaybeTlsStreamBuilder;
206205

207-
use crate::protos::relay::MAX_FRAME_SIZE;
206+
use crate::{
207+
http::{CLIENT_AUTH_HEADER, RELAY_PROTOCOL_VERSION},
208+
protos::{handshake::KeyMaterialClientAuth, relay::MAX_FRAME_SIZE},
209+
};
208210

209211
let mut dial_url = (*self.url).clone();
210212
dial_url.set_path(RELAY_PATH);
@@ -240,17 +242,33 @@ impl ClientBuilder {
240242
.as_ref()
241243
.local_addr()
242244
.map_err(|_| NoLocalAddrSnafu.build())?;
243-
let (conn, response) = tokio_websockets::ClientBuilder::new()
245+
let mut builder = tokio_websockets::ClientBuilder::new()
244246
.uri(dial_url.as_str())
245247
.map_err(|_| {
246248
InvalidRelayUrlSnafu {
247249
url: dial_url.clone(),
248250
}
249251
.build()
250252
})?
253+
.add_header(
254+
SEC_WEBSOCKET_PROTOCOL,
255+
http::HeaderValue::from_static(RELAY_PROTOCOL_VERSION),
256+
)
257+
.expect("valid header name and value")
251258
.limits(tokio_websockets::Limits::default().max_payload_len(Some(MAX_FRAME_SIZE)))
252-
.connect_on(stream)
253-
.await?;
259+
// We turn off automatic flushing after a threshold (the default would be after 8KB).
260+
// This means we need to flush manually, which we do by calling `Sink::send_all` or
261+
// `Sink::send` (which calls `Sink::flush`) in the `ActiveRelayActor`.
262+
.config(tokio_websockets::Config::default().flush_threshold(usize::MAX));
263+
if let Some(client_auth) = KeyMaterialClientAuth::new(&self.secret_key, &stream) {
264+
debug!("Using TLS key export for relay client authentication");
265+
builder = builder
266+
.add_header(CLIENT_AUTH_HEADER, client_auth.into_header_value())
267+
.expect(
268+
"impossible: CLIENT_AUTH_HEADER isn't a disallowed header value for websockets",
269+
);
270+
}
271+
let (conn, response) = builder.connect_on(stream).await?;
254272

255273
if response.status() != hyper::StatusCode::SWITCHING_PROTOCOLS {
256274
UnexpectedUpgradeStatusSnafu {
@@ -291,6 +309,8 @@ impl ClientBuilder {
291309
/// Establishes a new connection to the relay server.
292310
#[cfg(wasm_browser)]
293311
pub async fn connect(&self) -> Result<Client, ConnectError> {
312+
use crate::http::RELAY_PROTOCOL_VERSION;
313+
294314
let mut dial_url = (*self.url).clone();
295315
dial_url.set_path(RELAY_PATH);
296316
// The relay URL is exchanged with the http(s) scheme in tickets and similar.
@@ -310,7 +330,9 @@ impl ClientBuilder {
310330

311331
debug!(%dial_url, "Dialing relay by websocket");
312332

313-
let (_, ws_stream) = ws_stream_wasm::WsMeta::connect(dial_url.as_str(), None).await?;
333+
let (_, ws_stream) =
334+
ws_stream_wasm::WsMeta::connect(dial_url.as_str(), Some(vec![RELAY_PROTOCOL_VERSION]))
335+
.await?;
314336
let conn = Conn::new(ws_stream, self.key_cache.clone(), &self.secret_key).await?;
315337

316338
event!(
@@ -350,49 +372,49 @@ impl Client {
350372
}
351373

352374
impl Stream for Client {
353-
type Item = Result<ReceivedMessage, RecvError>;
375+
type Item = Result<RelayToClientMsg, RecvError>;
354376

355377
fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Option<Self::Item>> {
356378
Pin::new(&mut self.conn).poll_next(cx)
357379
}
358380
}
359381

360-
impl Sink<SendMessage> for Client {
382+
impl Sink<ClientToRelayMsg> for Client {
361383
type Error = SendError;
362384

363385
fn poll_ready(
364386
mut self: Pin<&mut Self>,
365387
cx: &mut task::Context<'_>,
366388
) -> Poll<Result<(), Self::Error>> {
367-
<Conn as Sink<SendMessage>>::poll_ready(Pin::new(&mut self.conn), cx)
389+
Pin::new(&mut self.conn).poll_ready(cx)
368390
}
369391

370-
fn start_send(mut self: Pin<&mut Self>, item: SendMessage) -> Result<(), Self::Error> {
392+
fn start_send(mut self: Pin<&mut Self>, item: ClientToRelayMsg) -> Result<(), Self::Error> {
371393
Pin::new(&mut self.conn).start_send(item)
372394
}
373395

374396
fn poll_flush(
375397
mut self: Pin<&mut Self>,
376398
cx: &mut task::Context<'_>,
377399
) -> Poll<Result<(), Self::Error>> {
378-
<Conn as Sink<SendMessage>>::poll_flush(Pin::new(&mut self.conn), cx)
400+
Pin::new(&mut self.conn).poll_flush(cx)
379401
}
380402

381403
fn poll_close(
382404
mut self: Pin<&mut Self>,
383405
cx: &mut task::Context<'_>,
384406
) -> Poll<Result<(), Self::Error>> {
385-
<Conn as Sink<SendMessage>>::poll_close(Pin::new(&mut self.conn), cx)
407+
Pin::new(&mut self.conn).poll_close(cx)
386408
}
387409
}
388410

389411
/// The send half of a relay client.
390412
#[derive(Debug)]
391413
pub struct ClientSink {
392-
sink: SplitSink<Conn, SendMessage>,
414+
sink: SplitSink<Conn, ClientToRelayMsg>,
393415
}
394416

395-
impl Sink<SendMessage> for ClientSink {
417+
impl Sink<ClientToRelayMsg> for ClientSink {
396418
type Error = SendError;
397419

398420
fn poll_ready(
@@ -402,7 +424,7 @@ impl Sink<SendMessage> for ClientSink {
402424
Pin::new(&mut self.sink).poll_ready(cx)
403425
}
404426

405-
fn start_send(mut self: Pin<&mut Self>, item: SendMessage) -> Result<(), Self::Error> {
427+
fn start_send(mut self: Pin<&mut Self>, item: ClientToRelayMsg) -> Result<(), Self::Error> {
406428
Pin::new(&mut self.sink).start_send(item)
407429
}
408430

@@ -436,7 +458,7 @@ impl ClientStream {
436458
}
437459

438460
impl Stream for ClientStream {
439-
type Item = Result<ReceivedMessage, RecvError>;
461+
type Item = Result<RelayToClientMsg, RecvError>;
440462

441463
fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Option<Self::Item>> {
442464
Pin::new(&mut self.stream).poll_next(cx)

0 commit comments

Comments
 (0)