Skip to content

Commit f0abede

Browse files
authored
feat(iroh): Enable applications to establish 0-RTT connections (#3163)
## Description Implements necessary APIs to make use of 0-RTT QUIC connections. 0-RTT allows you to skip a round-trip in case you have connected to a known endpoint ahead of time, and stored the given TLS session ticket. With this PR, we by default will cache up to 8 session tickets per endpoint you connect to, and remember up to 32 endpoints maximum. This cache only lives in-memory. We might add customization to the `EndpointBuilder` in the future to allow for customizing this cache (allowing you to persist it), but that obviously has security implications, so will need careful consideration. This PR enables using 0-RTT via the `Endpoint::connect_with_opts` function, which - unlike `Endpoint::connect` - returns a `Connecting`, a state prior to a full `Connection`. By calling `Connecting::into_0rtt` you can attempt to turn this connection into a full 0-RTT connection. However, security caveats apply. See that function's documentation for details. Migration guide: ```rs let connection = endpoint.connect_with(node_addr, alpn, transport_config).await?; ``` to ```rs let connection = endpoint.connect_with_opts( node_addr, alpn, ConnectOptions::new().with_transport_config(transport_config), ) .await? .await?; // second await for Connecting -> Connection ``` Closes #3146 ## Breaking Changes - `iroh::Endpoint::connect_with` was removed, and `iroh::Endpoint::connect_with_opts` was added instead, but returning an `iroh::endpoint::Connecting` instead of an `iroh::endpoint::Connection`, allowing use of QUIC's 0-RTT feature. - `iroh::endpoint::Connection::into_0rtt` now returns `iroh::endpoint::ZeroRttAccepted` (among other things), instead of `iroh_quinn::ZeroRttAccepted`. This wrapper is equivalent in functionality, but makes sure we're not depending on API-breaking changes in quinn and can keep a discovery task alive for as long as needed, until a connection is established. ## Change checklist - [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 a4fcaaa commit f0abede

File tree

5 files changed

+437
-89
lines changed

5 files changed

+437
-89
lines changed

iroh/src/discovery.rs

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -398,15 +398,15 @@ impl DiscoveryTask {
398398
}
399399
};
400400
let mut on_first_tx = Some(on_first_tx);
401-
debug!("discovery: start");
401+
debug!("starting");
402402
loop {
403403
match stream.next().await {
404404
Some(Ok(r)) => {
405405
if r.node_addr.is_empty() {
406-
debug!(provenance = %r.provenance, "discovery: empty address found");
406+
debug!(provenance = %r.provenance, "empty address found");
407407
continue;
408408
}
409-
debug!(provenance = %r.provenance, addr = ?r.node_addr, "discovery: new address found");
409+
debug!(provenance = %r.provenance, addr = ?r.node_addr, "new address found");
410410
ep.add_node_addr_with_source(r.node_addr, r.provenance).ok();
411411
if let Some(tx) = on_first_tx.take() {
412412
tx.send(Ok(())).ok();
@@ -443,13 +443,14 @@ mod tests {
443443

444444
use anyhow::Context;
445445
use iroh_base::SecretKey;
446+
use quinn::{IdleTimeout, TransportConfig};
446447
use rand::Rng;
447448
use testresult::TestResult;
448449
use tokio_util::task::AbortOnDropHandle;
449450
use tracing_test::traced_test;
450451

451452
use super::*;
452-
use crate::RelayMode;
453+
use crate::{endpoint::ConnectOptions, RelayMode};
453454

454455
type InfoStore = HashMap<NodeId, (Option<RelayUrl>, BTreeSet<SocketAddr>, u64)>;
455456

@@ -507,15 +508,14 @@ mod tests {
507508
endpoint: Endpoint,
508509
node_id: NodeId,
509510
) -> Option<BoxStream<Result<DiscoveryItem>>> {
510-
let addr_info = match self.resolve_wrong {
511-
false => self.shared.nodes.lock().unwrap().get(&node_id).cloned(),
512-
true => {
513-
let ts = system_time_now() - 100_000;
514-
let port: u16 = rand::thread_rng().gen_range(10_000..20_000);
515-
// "240.0.0.0/4" is reserved and unreachable
516-
let addr: SocketAddr = format!("240.0.0.1:{port}").parse().unwrap();
517-
Some((None, BTreeSet::from([addr]), ts))
518-
}
511+
let addr_info = if self.resolve_wrong {
512+
let ts = system_time_now() - 100_000;
513+
let port: u16 = rand::thread_rng().gen_range(10_000..20_000);
514+
// "240.0.0.0/4" is reserved and unreachable
515+
let addr: SocketAddr = format!("240.0.0.1:{port}").parse().unwrap();
516+
Some((None, BTreeSet::from([addr]), ts))
517+
} else {
518+
self.shared.nodes.lock().unwrap().get(&node_id).cloned()
519519
};
520520
let stream = match addr_info {
521521
Some((url, addrs, ts)) => {
@@ -636,10 +636,9 @@ mod tests {
636636
disco.add(disco3);
637637
new_endpoint(secret, disco).await
638638
};
639-
let ep1_addr = NodeAddr::new(ep1.node_id());
640639
// wait for out address to be updated and thus published at least once
641640
ep1.node_addr().await?;
642-
let _conn = ep2.connect(ep1_addr, TEST_ALPN).await?;
641+
let _conn = ep2.connect(ep1.node_id(), TEST_ALPN).await?;
643642
Ok(())
644643
}
645644

@@ -659,10 +658,19 @@ mod tests {
659658
let disco = ConcurrentDiscovery::from_services(vec![Box::new(disco1)]);
660659
new_endpoint(secret, disco).await
661660
};
662-
let ep1_addr = NodeAddr::new(ep1.node_id());
663661
// wait for out address to be updated and thus published at least once
664662
ep1.node_addr().await?;
665-
let res = ep2.connect(ep1_addr, TEST_ALPN).await;
663+
664+
// 10x faster test via a 3s idle timeout instead of the 30s default
665+
let mut config = TransportConfig::default();
666+
config.keep_alive_interval(Some(Duration::from_secs(1)));
667+
config.max_idle_timeout(Some(IdleTimeout::try_from(Duration::from_secs(3))?));
668+
let opts = ConnectOptions::new().with_transport_config(Arc::new(config));
669+
670+
let res = ep2
671+
.connect_with_opts(ep1.node_id(), TEST_ALPN, opts)
672+
.await? // -> Connecting works
673+
.await; // -> Connection is expected to fail
666674
assert!(res.is_err());
667675
Ok(())
668676
}

0 commit comments

Comments
 (0)