From 8ad6ff132aa377f3d925c48da20b16c333e37e3c Mon Sep 17 00:00:00 2001 From: Franz Heinzmann Date: Tue, 21 May 2024 19:00:53 +0200 Subject: [PATCH 1/7] tests(iroh-gossip): fix `net` smoke test (#2314) ## Description * Use shared test setup code from `iroh_net` * Fix stupid typo ## Notes & open questions ## Change checklist - [x] Self-review. - [x] Documentation updates if relevant. - [x] Tests if relevant. - [x] All breaking changes documented. --- iroh-gossip/Cargo.toml | 2 +- iroh-gossip/src/net.rs | 146 +++++------------------------------------ 2 files changed, 17 insertions(+), 131 deletions(-) diff --git a/iroh-gossip/Cargo.toml b/iroh-gossip/Cargo.toml index 9e4b329478..de0c0d2b5b 100644 --- a/iroh-gossip/Cargo.toml +++ b/iroh-gossip/Cargo.toml @@ -32,7 +32,7 @@ iroh-base = { version = "0.16.0", path = "../iroh-base" } # net dependencies (optional) futures-lite = { version = "2.3", optional = true } -iroh-net = { path = "../iroh-net", version = "0.16.0", optional = true, default-features = false } +iroh-net = { path = "../iroh-net", version = "0.16.0", optional = true, default-features = false, features = ["test-utils"] } tokio = { version = "1", optional = true, features = ["io-util", "sync", "rt", "macros", "net", "fs"] } tokio-util = { version = "0.7.8", optional = true, features = ["codec"] } genawaiter = { version = "0.99.1", default-features = false, features = ["futures03"] } diff --git a/iroh-gossip/src/net.rs b/iroh-gossip/src/net.rs index e415e68c69..97f554cda7 100644 --- a/iroh-gossip/src/net.rs +++ b/iroh-gossip/src/net.rs @@ -653,6 +653,7 @@ fn decode_peer_data(peer_data: &PeerData) -> anyhow::Result { mod test { use std::time::Duration; + use iroh_net::key::SecretKey; use iroh_net::relay::{RelayMap, RelayMode}; use tokio::spawn; use tokio::time::timeout; @@ -661,10 +662,15 @@ mod test { use super::*; - async fn create_endpoint(relay_map: RelayMap) -> anyhow::Result { + async fn create_endpoint( + rng: &mut rand_chacha::ChaCha12Rng, + relay_map: RelayMap, + ) -> anyhow::Result { Endpoint::builder() + .secret_key(SecretKey::generate_with_rng(rng)) .alpns(vec![GOSSIP_ALPN.to_vec()]) .relay_mode(RelayMode::Custom(relay_map)) + .insecure_skip_relay_cert_verify(true) .bind(0) .await } @@ -688,16 +694,15 @@ mod test { } #[tokio::test] - #[ignore = "flaky"] async fn gossip_net_smoke() { + let mut rng = rand_chacha::ChaCha12Rng::seed_from_u64(1); let _guard = iroh_test::logging::setup(); - let (relay_map, relay_url, cleanup) = util::run_relay_and_stun([127, 0, 0, 1].into()) - .await - .unwrap(); + let (relay_map, relay_url, _guard) = + iroh_net::test_utils::run_relay_server().await.unwrap(); - let ep1 = create_endpoint(relay_map.clone()).await.unwrap(); - let ep2 = create_endpoint(relay_map.clone()).await.unwrap(); - let ep3 = create_endpoint(relay_map.clone()).await.unwrap(); + let ep1 = create_endpoint(&mut rng, relay_map.clone()).await.unwrap(); + let ep2 = create_endpoint(&mut rng, relay_map.clone()).await.unwrap(); + let ep3 = create_endpoint(&mut rng, relay_map.clone()).await.unwrap(); let addr1 = AddrInfo { relay_url: Some(relay_url.clone()), direct_addresses: Default::default(), @@ -722,8 +727,8 @@ mod test { let cancel = CancellationToken::new(); let tasks = [ spawn(endpoint_loop(ep1.clone(), go1.clone(), cancel.clone())), - spawn(endpoint_loop(ep2.clone(), go3.clone(), cancel.clone())), - spawn(endpoint_loop(ep3.clone(), go2.clone(), cancel.clone())), + spawn(endpoint_loop(ep2.clone(), go2.clone(), cancel.clone())), + spawn(endpoint_loop(ep3.clone(), go3.clone(), cancel.clone())), ]; debug!("----- adding peers ----- "); @@ -739,7 +744,7 @@ mod test { go2.join(topic, vec![pi1]).await.unwrap().await.unwrap(); go3.join(topic, vec![pi1]).await.unwrap().await.unwrap(); - let len = 10; + let len = 2; // subscribe nodes 2 and 3 to the topic let mut stream2 = go2.subscribe(topic).await.unwrap(); @@ -814,124 +819,5 @@ mod test { .unwrap() .unwrap(); } - drop(cleanup); - } - - // This is copied from iroh-net/src/hp/magicsock/conn.rs - // TODO: Move into a public test_utils module in iroh-net? - mod util { - use std::net::{IpAddr, SocketAddr}; - - use anyhow::Result; - use iroh_net::{ - key::SecretKey, - relay::{RelayMap, RelayUrl}, - stun::{is, parse_binding_request, response}, - }; - use tokio::sync::oneshot; - use tracing::{debug, info, trace}; - - /// A drop guard to clean up test infrastructure. - /// - /// After dropping the test infrastructure will asynchronously shutdown and release its - /// resources. - // Nightly sees the sender as dead code currently, but we only rely on Drop of the - // sender. - #[derive(Debug)] - #[allow(dead_code)] - pub(crate) struct CleanupDropGuard(pub(crate) oneshot::Sender<()>); - - /// Runs a relay server with STUN enabled suitable for tests. - /// - /// The returned `Url` is the url of the relay server in the returned [`RelayMap`], it - /// is always `Some` as that is how the [`Endpoint::connect`] API expects it. - /// - /// [`Endpoint::connect`]: crate::endpoint::Endpoint - pub(crate) async fn run_relay_and_stun( - stun_ip: IpAddr, - ) -> Result<(RelayMap, RelayUrl, CleanupDropGuard)> { - let server_key = SecretKey::generate(); - let server = iroh_net::relay::http::ServerBuilder::new("127.0.0.1:0".parse().unwrap()) - .secret_key(Some(server_key)) - .tls_config(None) - .spawn() - .await?; - - let http_addr = server.addr(); - info!("relay listening on {:?}", http_addr); - - let (stun_addr, stun_drop_guard) = serve(stun_ip).await?; - let relay_url: RelayUrl = format!("http://localhost:{}", http_addr.port()) - .parse() - .unwrap(); - let m = RelayMap::default_from_node(relay_url.clone(), stun_addr.port()); - - let (tx, rx) = oneshot::channel(); - tokio::spawn(async move { - let _stun_cleanup = stun_drop_guard; // move into this closure - - // Wait until we're dropped or receive a message. - rx.await.ok(); - server.shutdown().await; - }); - - Ok((m, relay_url, CleanupDropGuard(tx))) - } - - /// Sets up a simple STUN server. - async fn serve(ip: IpAddr) -> Result<(SocketAddr, CleanupDropGuard)> { - let pc = tokio::net::UdpSocket::bind((ip, 0)).await?; - let mut addr = pc.local_addr()?; - match addr.ip() { - IpAddr::V4(ip) => { - if ip.octets() == [0, 0, 0, 0] { - addr.set_ip("127.0.0.1".parse().unwrap()); - } - } - _ => unreachable!("using ipv4"), - } - - info!("STUN listening on {}", addr); - let (s, r) = oneshot::channel(); - tokio::task::spawn(async move { - run_stun(pc, r).await; - }); - - Ok((addr, CleanupDropGuard(s))) - } - - async fn run_stun(pc: tokio::net::UdpSocket, mut done: oneshot::Receiver<()>) { - let mut buf = vec![0u8; 64 << 10]; - loop { - trace!("read loop"); - tokio::select! { - _ = &mut done => { - debug!("shutting down"); - break; - } - res = pc.recv_from(&mut buf) => match res { - Ok((n, addr)) => { - trace!("read packet {}bytes from {}", n, addr); - let pkt = &buf[..n]; - if !is(pkt) { - debug!("received non STUN pkt"); - continue; - } - if let Ok(txid) = parse_binding_request(pkt) { - debug!("received binding request"); - - let res = response(txid, addr); - if let Err(err) = pc.send_to(&res, addr).await { - eprintln!("STUN server write failed: {:?}", err); - } - } - } - Err(err) => { - eprintln!("failed to read: {:?}", err); - } - } - } - } - } } } From b412927e8578c1bfa78bcd07772520a0eb25b615 Mon Sep 17 00:00:00 2001 From: Friedel Ziegelmayer Date: Tue, 21 May 2024 19:04:46 +0200 Subject: [PATCH 2/7] feat(iroh)!: remove node events (#2274) These seem to be mostly unused by consumers, and add complexity. ## Breaking Changes - remove: - `iroh::node::Event` - `iroh::node::Node::subscribe` ## Notes - [x] Still need to figure out how to migrate the tests --------- Co-authored-by: Ruediger Klaehn --- iroh/src/node.rs | 82 +---------------------------- iroh/src/node/builder.rs | 66 ++++++++++++++---------- iroh/src/node/rpc.rs | 9 +--- iroh/tests/gc.rs | 65 +++++++++-------------- iroh/tests/provide.rs | 108 +-------------------------------------- 5 files changed, 66 insertions(+), 264 deletions(-) diff --git a/iroh/src/node.rs b/iroh/src/node.rs index 05227992e8..88665a8c72 100644 --- a/iroh/src/node.rs +++ b/iroh/src/node.rs @@ -2,8 +2,6 @@ //! //! A node is a server that serves various protocols. //! -//! You can monitor what is happening in the node using [`Node::subscribe`]. -//! //! To shut down the node, call [`Node::shutdown`]. use std::fmt::Debug; use std::net::SocketAddr; @@ -11,7 +9,7 @@ use std::path::Path; use std::sync::Arc; use anyhow::{anyhow, Result}; -use futures_lite::{future::Boxed as BoxFuture, FutureExt, StreamExt}; +use futures_lite::StreamExt; use iroh_base::key::PublicKey; use iroh_blobs::downloader::Downloader; use iroh_blobs::store::Store as BaoStore; @@ -19,7 +17,6 @@ use iroh_net::util::AbortingJoinHandle; use iroh_net::{endpoint::LocalEndpointsStream, key::SecretKey, Endpoint}; use quic_rpc::transport::flume::FlumeConnection; use quic_rpc::RpcClient; -use tokio::sync::{mpsc, RwLock}; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; use tokio_util::task::LocalPoolHandle; @@ -35,38 +32,6 @@ mod rpc_status; pub use self::builder::{Builder, DiscoveryConfig, GcPolicy, StorageConfig}; pub use self::rpc_status::RpcStatus; -type EventCallback = Box BoxFuture<()> + 'static + Sync + Send>; - -#[derive(Default, derive_more::Debug, Clone)] -struct Callbacks(#[debug("..")] Arc>>); - -impl Callbacks { - async fn push(&self, cb: EventCallback) { - self.0.write().await.push(cb); - } - - #[allow(dead_code)] - async fn send(&self, event: Event) { - let cbs = self.0.read().await; - for cb in &*cbs { - cb(event.clone()).await; - } - } -} - -impl iroh_blobs::provider::EventSender for Callbacks { - fn send(&self, event: iroh_blobs::provider::Event) -> BoxFuture<()> { - let this = self.clone(); - async move { - let cbs = this.0.read().await; - for cb in &*cbs { - cb(Event::ByteProvide(event.clone())).await; - } - } - .boxed() - } -} - /// A server which implements the iroh node. /// /// Clients can connect to this server and requests hashes from it. @@ -91,9 +56,6 @@ struct NodeInner { secret_key: SecretKey, cancel_token: CancellationToken, controller: FlumeConnection, - #[debug("callbacks: Sender>")] - cb_sender: mpsc::Sender BoxFuture<()> + Send + Sync + 'static>>, - callbacks: Callbacks, #[allow(dead_code)] gc_task: Option>, #[debug("rt")] @@ -102,15 +64,6 @@ struct NodeInner { downloader: Downloader, } -/// Events emitted by the [`Node`] informing about the current status. -#[derive(Debug, Clone)] -pub enum Event { - /// Events from the iroh-blobs transfer protocol. - ByteProvide(iroh_blobs::provider::Event), - /// Events from database - Db(iroh_blobs::store::Event), -} - /// In memory node. pub type MemNode = Node; @@ -177,18 +130,6 @@ impl Node { self.inner.secret_key.public() } - /// Subscribe to [`Event`]s emitted from the node, informing about connections and - /// progress. - /// - /// Warning: The callback must complete quickly, as otherwise it will block ongoing work. - pub async fn subscribe BoxFuture<()> + Send + Sync + 'static>( - &self, - cb: F, - ) -> Result<()> { - self.inner.cb_sender.send(Box::new(cb)).await?; - Ok(()) - } - /// Returns a handle that can be used to do RPC calls to the node internally. pub fn controller(&self) -> crate::client::MemRpcClient { RpcClient::new(self.inner.controller.clone()) @@ -319,23 +260,7 @@ mod tests { let _drop_guard = node.cancel_token().drop_guard(); - let (r, mut s) = mpsc::channel(1); - node.subscribe(move |event| { - let r = r.clone(); - async move { - if let Event::ByteProvide(iroh_blobs::provider::Event::TaggedBlobAdded { - hash, - .. - }) = event - { - r.send(hash).await.ok(); - } - } - .boxed() - }) - .await?; - - let got_hash = tokio::time::timeout(Duration::from_secs(1), async move { + let _got_hash = tokio::time::timeout(Duration::from_secs(1), async move { let mut stream = node .controller() .server_streaming(BlobAddPathRequest { @@ -364,9 +289,6 @@ mod tests { .context("timeout")? .context("get failed")?; - let event_hash = s.recv().await.expect("missing add tagged blob event"); - assert_eq!(got_hash, event_hash); - Ok(()) } diff --git a/iroh/src/node/builder.rs b/iroh/src/node/builder.rs index addec034d4..61a53f2828 100644 --- a/iroh/src/node/builder.rs +++ b/iroh/src/node/builder.rs @@ -27,19 +27,18 @@ use quic_rpc::{ RpcServer, ServiceEndpoint, }; use serde::{Deserialize, Serialize}; -use tokio::sync::mpsc; use tokio_util::{sync::CancellationToken, task::LocalPoolHandle}; use tracing::{debug, error, error_span, info, trace, warn, Instrument}; use crate::{ client::RPC_ALPN, docs_engine::Engine, - node::{Event, NodeInner}, + node::NodeInner, rpc_protocol::{Request, Response, RpcService}, util::{fs::load_secret_key, path::IrohPaths}, }; -use super::{rpc, rpc_status::RpcStatus, Callbacks, EventCallback, Node}; +use super::{rpc, rpc_status::RpcStatus, Node}; pub const PROTOCOLS: [&[u8]; 3] = [iroh_blobs::protocol::ALPN, GOSSIP_ALPN, DOCS_ALPN]; @@ -69,7 +68,7 @@ const MAX_STREAMS: u64 = 10; /// /// The returned [`Node`] is awaitable to know when it finishes. It can be terminated /// using [`Node::shutdown`]. -#[derive(Debug)] +#[derive(derive_more::Debug)] pub struct Builder where D: Map, @@ -88,6 +87,9 @@ where docs_store: iroh_docs::store::fs::Store, #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify: bool, + /// Callback to register when a gc loop is done + #[debug("callback")] + gc_done_callback: Option>, } /// Configuration for storage. @@ -135,6 +137,7 @@ impl Default for Builder { node_discovery: Default::default(), #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify: false, + gc_done_callback: None, } } } @@ -160,6 +163,7 @@ impl Builder { node_discovery: Default::default(), #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify: false, + gc_done_callback: None, } } } @@ -222,6 +226,7 @@ where node_discovery: self.node_discovery, #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify: false, + gc_done_callback: self.gc_done_callback, }) } @@ -242,6 +247,7 @@ where node_discovery: self.node_discovery, #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify: self.insecure_skip_relay_cert_verify, + gc_done_callback: self.gc_done_callback, } } @@ -267,6 +273,7 @@ where node_discovery: self.node_discovery, #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify: self.insecure_skip_relay_cert_verify, + gc_done_callback: self.gc_done_callback, }) } @@ -337,6 +344,13 @@ where self } + /// Register a callback for when GC is done. + #[cfg(any(test, feature = "test-utils"))] + pub fn register_gc_done_cb(mut self, cb: Box) -> Self { + self.gc_done_callback.replace(cb); + self + } + /// Whether to log the SSL pre-master key. /// /// If `true` and the `SSLKEYLOGFILE` environment variable is the path to a file this @@ -352,7 +366,7 @@ where /// This will create the underlying network server and spawn a tokio task accepting /// connections. The returned [`Node`] can be used to control the task as well as /// get information about it. - pub async fn spawn(self) -> Result> { + pub async fn spawn(mut self) -> Result> { trace!("spawning node"); let lp = LocalPoolHandle::new(num_cpus::get()); @@ -406,7 +420,6 @@ where let endpoint = endpoint.bind(bind_port).await?; trace!("created quinn endpoint"); - let (cb_sender, cb_receiver) = mpsc::channel(8); let cancel_token = CancellationToken::new(); debug!("rpc listening on: {:?}", self.rpc_endpoint.local_addr()); @@ -427,12 +440,13 @@ where ); let sync_db = sync.sync.clone(); - let callbacks = Callbacks::default(); let gc_task = if let GcPolicy::Interval(gc_period) = self.gc_policy { tracing::info!("Starting GC task with interval {:?}", gc_period); let db = self.blobs_store.clone(); - let callbacks = callbacks.clone(); - let task = lp.spawn_pinned(move || Self::gc_loop(db, sync_db, gc_period, callbacks)); + let gc_done_callback = self.gc_done_callback.take(); + + let task = + lp.spawn_pinned(move || Self::gc_loop(db, sync_db, gc_period, gc_done_callback)); Some(task.into()) } else { None @@ -446,8 +460,6 @@ where secret_key: self.secret_key, controller, cancel_token, - callbacks: callbacks.clone(), - cb_sender, gc_task, rt: lp.clone(), sync, @@ -464,8 +476,6 @@ where async move { Self::run( ep, - callbacks, - cb_receiver, handler, self.rpc_endpoint, internal_rpc, @@ -508,8 +518,6 @@ where #[allow(clippy::too_many_arguments)] async fn run( server: Endpoint, - callbacks: Callbacks, - mut cb_receiver: mpsc::Receiver, handler: rpc::Handler, rpc: E, internal_rpc: impl ServiceEndpoint, @@ -586,10 +594,6 @@ where } }); }, - // Handle new callbacks - Some(cb) = cb_receiver.recv() => { - callbacks.push(cb).await; - } else => break, } } @@ -609,7 +613,7 @@ where db: D, ds: iroh_docs::actor::SyncHandle, gc_period: Duration, - callbacks: Callbacks, + done_cb: Option>, ) { let mut live = BTreeSet::new(); tracing::debug!("GC loop starting {:?}", gc_period); @@ -623,14 +627,11 @@ where // do delay before the two phases of GC tokio::time::sleep(gc_period).await; tracing::debug!("Starting GC"); - callbacks - .send(Event::Db(iroh_blobs::store::Event::GcStarted)) - .await; live.clear(); let doc_hashes = match ds.content_hashes().await { Ok(hashes) => hashes, Err(err) => { - tracing::error!("Error getting doc hashes: {}", err); + tracing::warn!("Error getting doc hashes: {}", err); continue 'outer; } }; @@ -680,9 +681,9 @@ where } } } - callbacks - .send(Event::Db(iroh_blobs::store::Event::GcCompleted)) - .await; + if let Some(ref cb) = done_cb { + cb(); + } } } } @@ -719,7 +720,7 @@ async fn handle_connection( iroh_blobs::provider::handle_connection( connection, node.db.clone(), - node.callbacks.clone(), + MockEventSender, node.rt.clone(), ) .await @@ -776,3 +777,12 @@ fn make_rpc_endpoint( Ok((rpc_endpoint, actual_rpc_port)) } + +#[derive(Debug, Clone)] +struct MockEventSender; + +impl iroh_blobs::provider::EventSender for MockEventSender { + fn send(&self, _event: iroh_blobs::provider::Event) -> futures_lite::future::Boxed<()> { + Box::pin(std::future::ready(())) + } +} diff --git a/iroh/src/node/rpc.rs b/iroh/src/node/rpc.rs index f7640f5ff2..0c50f7ed33 100644 --- a/iroh/src/node/rpc.rs +++ b/iroh/src/node/rpc.rs @@ -52,7 +52,7 @@ use crate::rpc_protocol::{ NodeWatchResponse, Request, RpcService, SetTagOption, }; -use super::{Event, NodeInner}; +use super::NodeInner; const HEALTH_POLL_WAIT: Duration = Duration::from_secs(1); /// Chunk size for getting blobs over RPC @@ -761,13 +761,6 @@ impl Handler { tag: tag.clone(), }) .await?; - self.inner - .callbacks - .send(Event::ByteProvide( - iroh_blobs::provider::Event::TaggedBlobAdded { hash, format, tag }, - )) - .await; - Ok(()) } diff --git a/iroh/tests/gc.rs b/iroh/tests/gc.rs index e0899fb2fb..4c3c3fc26f 100644 --- a/iroh/tests/gc.rs +++ b/iroh/tests/gc.rs @@ -6,7 +6,6 @@ use std::{ use anyhow::Result; use bao_tree::{blake3, io::sync::Outboard, ChunkRanges}; use bytes::Bytes; -use futures_lite::FutureExt; use iroh::node::{self, Node}; use rand::RngCore; @@ -38,55 +37,40 @@ pub fn simulate_remote(data: &[u8]) -> (blake3::Hash, Cursor) { } /// Wrap a bao store in a node that has gc enabled. -async fn wrap_in_node(bao_store: S, gc_period: Duration) -> Node +async fn wrap_in_node(bao_store: S, gc_period: Duration) -> (Node, flume::Receiver<()>) where S: iroh_blobs::store::Store, { let doc_store = iroh_docs::store::Store::memory(); - node::Builder::with_db_and_store(bao_store, doc_store, iroh::node::StorageConfig::Mem) - .gc_policy(iroh::node::GcPolicy::Interval(gc_period)) - .spawn() - .await - .unwrap() -} - -async fn attach_db_events( - node: &Node, -) -> flume::Receiver { - let (db_send, db_recv) = flume::unbounded(); - node.subscribe(move |ev| { - let db_send = db_send.clone(); - async move { - if let iroh::node::Event::Db(ev) = ev { - db_send.into_send_async(ev).await.ok(); - } - } - .boxed() - }) - .await - .unwrap(); - db_recv + let (gc_send, gc_recv) = flume::unbounded(); + let node = + node::Builder::with_db_and_store(bao_store, doc_store, iroh::node::StorageConfig::Mem) + .gc_policy(iroh::node::GcPolicy::Interval(gc_period)) + .register_gc_done_cb(Box::new(move || { + gc_send.send(()).ok(); + })) + .spawn() + .await + .unwrap(); + (node, gc_recv) } async fn gc_test_node() -> ( Node, iroh_blobs::store::mem::Store, - flume::Receiver, + flume::Receiver<()>, ) { let bao_store = iroh_blobs::store::mem::Store::new(); - let node = wrap_in_node(bao_store.clone(), Duration::from_millis(500)).await; - let db_recv = attach_db_events(&node).await; - (node, bao_store, db_recv) + let (node, gc_recv) = wrap_in_node(bao_store.clone(), Duration::from_millis(500)).await; + (node, bao_store, gc_recv) } -async fn step(evs: &flume::Receiver) { +async fn step(evs: &flume::Receiver<()>) { + // drain the event queue, we want a new GC while evs.try_recv().is_ok() {} + // wait for several GC cycles for _ in 0..3 { - while let Ok(ev) = evs.recv_async().await { - if let iroh_blobs::store::Event::GcCompleted = ev { - break; - } - } + evs.recv_async().await.unwrap(); } } @@ -246,7 +230,7 @@ mod file { let _ = tracing_subscriber::fmt::try_init(); let dir = testdir!(); let bao_store = iroh_blobs::store::fs::Store::load(dir.join("store")).await?; - let node = wrap_in_node(bao_store.clone(), Duration::from_secs(10)).await; + let (node, _) = wrap_in_node(bao_store.clone(), Duration::from_secs(10)).await; let client = node.client(); let doc = client.docs.create().await?; let author = client.authors.create().await?; @@ -289,8 +273,7 @@ mod file { let outboard_path = outboard_path(dir.clone()); let bao_store = iroh_blobs::store::fs::Store::load(dir.clone()).await?; - let node = wrap_in_node(bao_store.clone(), Duration::from_millis(100)).await; - let evs = attach_db_events(&node).await; + let (node, evs) = wrap_in_node(bao_store.clone(), Duration::from_millis(100)).await; let data1 = create_test_data(10000000); let tt1 = bao_store .import_bytes(data1.clone(), BlobFormat::Raw) @@ -452,8 +435,7 @@ mod file { let outboard_path = outboard_path(dir.clone()); let bao_store = iroh_blobs::store::fs::Store::load(dir.clone()).await?; - let node = wrap_in_node(bao_store.clone(), Duration::from_millis(10)).await; - let evs = attach_db_events(&node).await; + let (node, evs) = wrap_in_node(bao_store.clone(), Duration::from_millis(10)).await; let data1: Bytes = create_test_data(10000000); let (_entry, tt1) = simulate_download_partial(&bao_store, data1.clone()).await?; @@ -484,8 +466,7 @@ mod file { let dir = testdir!(); let bao_store = iroh_blobs::store::fs::Store::load(dir.clone()).await?; - let node = wrap_in_node(bao_store.clone(), Duration::from_secs(1)).await; - let evs = attach_db_events(&node).await; + let (node, evs) = wrap_in_node(bao_store.clone(), Duration::from_secs(1)).await; let mut deleted = Vec::new(); let mut live = Vec::new(); diff --git a/iroh/tests/provide.rs b/iroh/tests/provide.rs index dcec072ca9..a4f005fe58 100644 --- a/iroh/tests/provide.rs +++ b/iroh/tests/provide.rs @@ -5,15 +5,14 @@ use std::{ time::{Duration, Instant}, }; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result}; use bytes::Bytes; use futures_lite::FutureExt; -use iroh::node::{Builder, Event}; +use iroh::node::Builder; use iroh_base::node_addr::AddrInfoOptions; use iroh_net::{defaults::default_relay_map, key::SecretKey, NodeAddr, NodeId}; use quic_rpc::transport::misc::DummyServerEndpoint; use rand::RngCore; -use tokio::sync::mpsc; use bao_tree::{blake3, ChunkNum, ChunkRanges}; use iroh_blobs::{ @@ -24,7 +23,6 @@ use iroh_blobs::{ Stats, }, protocol::{GetRequest, RangeSpecSeq}, - provider, store::{MapMut, Store}, BlobFormat, Hash, }; @@ -225,17 +223,6 @@ where let node = test_node(mdb.clone()).spawn().await?; - let (events_sender, mut events_recv) = mpsc::unbounded_channel(); - - node.subscribe(move |event| { - let events_sender = events_sender.clone(); - async move { - events_sender.send(event).ok(); - } - .boxed() - }) - .await?; - let addrs = node.local_endpoint_addresses().await?; let (secret_key, peer) = get_options(node.node_id(), addrs); let request = GetRequest::all(collection_hash); @@ -251,66 +238,11 @@ where assert_eq!(expected, got); } - // We have to wait for the completed event before shutting down the node. - let events = tokio::time::timeout(Duration::from_secs(30), async move { - let mut events = Vec::new(); - while let Some(event) = events_recv.recv().await { - match event { - Event::ByteProvide(provider::Event::TransferCompleted { .. }) - | Event::ByteProvide(provider::Event::TransferAborted { .. }) => { - events.push(event); - break; - } - _ => events.push(event), - } - } - events - }) - .await - .expect("duration expired"); - node.shutdown().await?; - assert_events(events, num_blobs + 1); - Ok(()) } -fn assert_events(events: Vec, num_blobs: usize) { - let num_basic_events = 4; - let num_total_events = num_basic_events + num_blobs; - assert_eq!( - events.len(), - num_total_events, - "missing events, only got {:#?}", - events - ); - assert!(matches!( - events[0], - Event::ByteProvide(provider::Event::ClientConnected { .. }) - )); - assert!(matches!( - events[1], - Event::ByteProvide(provider::Event::GetRequestReceived { .. }) - )); - assert!(matches!( - events[2], - Event::ByteProvide(provider::Event::TransferHashSeqStarted { .. }) - )); - for (i, event) in events[3..num_total_events - 1].iter().enumerate() { - match event { - Event::ByteProvide(provider::Event::TransferBlobCompleted { index, .. }) => { - assert_eq!(*index, i as u64); - } - _ => panic!("unexpected event {:?}", event), - } - } - assert!(matches!( - events.last().unwrap(), - Event::ByteProvide(provider::Event::TransferCompleted { .. }) - )); -} - #[tokio::test] async fn test_server_close() { // Prepare a Provider transferring a file. @@ -323,47 +255,11 @@ async fn test_server_close() { let node_addr = node.local_endpoint_addresses().await.unwrap(); let peer_id = node.node_id(); - let (events_sender, mut events_recv) = mpsc::unbounded_channel(); - node.subscribe(move |event| { - let events_sender = events_sender.clone(); - async move { - events_sender.send(event).ok(); - } - .boxed() - }) - .await - .unwrap(); let (secret_key, peer) = get_options(peer_id, node_addr); let request = GetRequest::all(hash); let (_collection, _children, _stats) = run_collection_get_request(secret_key, peer, request) .await .unwrap(); - - // Unwrap the JoinHandle, then the result of the Provider - tokio::time::timeout(Duration::from_secs(10), async move { - loop { - tokio::select! { - biased; - maybe_event = events_recv.recv() => { - match maybe_event { - Some(event) => match event { - Event::ByteProvide(provider::Event::TransferCompleted { .. }) => { - return node.shutdown().await; - }, - Event::ByteProvide(provider::Event::TransferAborted { .. }) => { - break Err(anyhow!("transfer aborted")); - } - _ => (), - } - None => break Err(anyhow!("events ended")), - } - } - } - } - }) - .await - .expect("supervisor timeout") - .expect("supervisor failed"); } /// create an in memory test database containing the given entries and an iroh collection of all entries From 6d1a6dd6a9f825aa6fe434cd5098d2fb8684ae14 Mon Sep 17 00:00:00 2001 From: Friedel Ziegelmayer Date: Wed, 22 May 2024 14:41:38 +0200 Subject: [PATCH 3/7] feat(iroh-net)!: Implement http proxy support (#2298) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Headers are based on how `curl` and env variables on`reqwest`. Setting any of - `HTTP_PROXY` - `http_proxy` - `HTTPS_PROXY` - `https_proxy` will make all relay code use these to proxy outgoing connections. Closes #2295 ## Breaking Changes - Added `iroh_net::endpoint::Builder::proxy_url` - Added `iroh_net::endpoint::Builder::proxy_from_env` - Added `iroh_net::relay::http::ClientError::Proxy` enum variant ## TODOs - [x] config & parsing env variables - [x] the todos in the code - [x] https proxy - [x] testing: tested manually on two machines using `squid` --- Cargo.lock | 629 ++++++++++++++++---------- iroh-cli/Cargo.toml | 2 +- iroh-metrics/Cargo.toml | 2 +- iroh-net/Cargo.toml | 3 +- iroh-net/src/endpoint.rs | 68 +++ iroh-net/src/magicsock.rs | 15 + iroh-net/src/magicsock/relay_actor.rs | 6 +- iroh-net/src/relay/client.rs | 11 +- iroh-net/src/relay/http.rs | 1 + iroh-net/src/relay/http/client.rs | 196 +++++--- iroh-net/src/relay/http/streams.rs | 292 ++++++++++++ iroh-net/src/relay/server.rs | 7 +- iroh-net/src/util.rs | 2 + iroh-net/src/util/chain.rs | 138 ++++++ iroh/src/node/builder.rs | 1 + 15 files changed, 1059 insertions(+), 314 deletions(-) create mode 100644 iroh-net/src/relay/http/streams.rs create mode 100644 iroh-net/src/util/chain.rs diff --git a/Cargo.lock b/Cargo.lock index 1a7774d317..3caa10894b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,47 +78,48 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -126,9 +127,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arc-swap" @@ -200,7 +201,7 @@ checksum = "7378575ff571966e99a744addeff0bff98b8ada0dedf1956d59e634db95eaac1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", "synstructure 0.13.1", ] @@ -223,7 +224,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -234,7 +235,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -246,6 +247,12 @@ dependencies = [ "critical-section", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "attohttpc" version = "0.24.1" @@ -259,9 +266,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" @@ -328,7 +335,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -346,10 +353,10 @@ dependencies = [ "hyper 1.3.1", "hyper-util", "pin-project-lite", - "rustls", + "rustls 0.21.12", "rustls-pemfile 2.1.2", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tower", "tower-service", ] @@ -411,9 +418,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64-url" @@ -522,9 +529,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" dependencies = [ "serde", ] @@ -574,9 +581,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.95" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" [[package]] name = "cfg-if" @@ -667,7 +674,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.1", + "strsim", ] [[package]] @@ -679,7 +686,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -707,9 +714,9 @@ checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "colored" @@ -748,9 +755,9 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] @@ -884,9 +891,9 @@ checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] @@ -912,9 +919,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" @@ -1027,14 +1034,14 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] name = "darling" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" dependencies = [ "darling_core", "darling_macro", @@ -1042,27 +1049,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", - "syn 2.0.60", + "strsim", + "syn 2.0.65", ] [[package]] name = "darling_macro" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -1072,7 +1079,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -1080,9 +1087,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "der" @@ -1131,7 +1138,7 @@ checksum = "5fe87ce4529967e0ba1dcf8450bab64d97dfd5010a6256187ffe2e43e6f0e049" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -1161,7 +1168,7 @@ checksum = "2bba3e9872d7c58ce7ef0fcf1844fcc3e23ef2a58377b50df35dd98e42a5726e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", "unicode-xid", ] @@ -1244,7 +1251,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -1330,9 +1337,9 @@ dependencies = [ [[package]] name = "either" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" [[package]] name = "elliptic-curve" @@ -1389,7 +1396,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -1402,7 +1409,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -1422,7 +1429,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -1448,9 +1455,9 @@ checksum = "76a5aa24577083f8190ad401e376b55887c7cd9083ae95d83ceec5d28ea78125" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1494,9 +1501,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "fd-lock" @@ -1521,9 +1528,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38793c55593b33412e3ae40c2c9781ffaa6f438f6f8c10f24e71846fbd7ae01e" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "flume" @@ -1585,9 +1592,9 @@ dependencies = [ [[package]] name = "futures-buffered" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de8419e65098e54c06f5ae8a130a79e8ba2e391ff995d260ca5d77ea72ab2fe3" +checksum = "02dcae03ee5afa5ea17b1aebc793806b8ddfc6dc500e0b8e8e1eb30b9dad22c0" dependencies = [ "futures-core", "futures-util", @@ -1662,7 +1669,7 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ - "fastrand 2.0.2", + "fastrand 2.1.0", "futures-core", "futures-io", "parking", @@ -1677,7 +1684,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -1760,9 +1767,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "js-sys", @@ -1835,15 +1842,15 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" dependencies = [ + "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "futures-util", "http 1.1.0", "indexmap 2.2.6", "slab", @@ -1879,9 +1886,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", @@ -1889,11 +1896,11 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692eaaf7f7607518dd3cef090f1474b61edc5301d8012f09579920df68b725ee" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -1962,13 +1969,13 @@ dependencies = [ "once_cell", "rand", "ring 0.16.20", - "rustls", + "rustls 0.21.12", "rustls-pemfile 1.0.4", "serde", "thiserror", "tinyvec", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tracing", "url", ] @@ -1988,12 +1995,12 @@ dependencies = [ "parking_lot", "rand", "resolv-conf", - "rustls", + "rustls 0.21.12", "serde", "smallvec", "thiserror", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tracing", ] @@ -2010,12 +2017,12 @@ dependencies = [ "futures-util", "hickory-proto", "hickory-resolver", - "rustls", + "rustls 0.21.12", "serde", "thiserror", "time", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tokio-util", "tracing", ] @@ -2191,7 +2198,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.4", + "h2 0.4.5", "http 1.1.0", "http-body 1.0.0", "httparse", @@ -2212,9 +2219,26 @@ dependencies = [ "futures-util", "http 0.2.12", "hyper 0.14.28", - "rustls", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.3.1", + "hyper-util", + "rustls 0.22.4", + "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.25.0", + "tower-service", ] [[package]] @@ -2224,6 +2248,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" dependencies = [ "bytes", + "futures-channel", "futures-util", "http 1.1.0", "http-body 1.0.0", @@ -2231,6 +2256,9 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", + "tower", + "tower-service", + "tracing", ] [[package]] @@ -2319,7 +2347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "serde", ] @@ -2337,12 +2365,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "indoc" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" - [[package]] name = "inout" version = "0.1.3" @@ -2363,9 +2385,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", ] @@ -2379,7 +2401,7 @@ dependencies = [ "socket2", "widestring", "windows-sys 0.48.0", - "winreg", + "winreg 0.50.0", ] [[package]] @@ -2515,7 +2537,7 @@ dependencies = [ "redb 1.5.1", "redb 2.1.0", "reflink-copy", - "rustls", + "rustls 0.21.12", "self_cell", "serde", "serde_json", @@ -2563,7 +2585,7 @@ dependencies = [ "rand", "ratatui", "regex", - "reqwest", + "reqwest 0.12.4", "rustyline", "serde", "serde_with", @@ -2611,13 +2633,13 @@ dependencies = [ "rcgen 0.12.1", "redb 2.1.0", "regex", - "rustls", + "rustls 0.21.12", "rustls-pemfile 1.0.4", "serde", "struct_iterable", "strum 0.26.2", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tokio-rustls-acme", "tokio-stream", "tokio-util", @@ -2721,7 +2743,7 @@ dependencies = [ "hyper-util", "once_cell", "prometheus-client", - "reqwest", + "reqwest 0.12.4", "serde", "struct_iterable", "time", @@ -2737,6 +2759,7 @@ dependencies = [ "anyhow", "axum", "backoff", + "base64 0.22.1", "bytes", "clap", "criterion", @@ -2785,12 +2808,12 @@ dependencies = [ "rand_core", "rcgen 0.11.3", "regex", - "reqwest", + "reqwest 0.12.4", "ring 0.17.8", "rtnetlink", - "rustls", + "rustls 0.21.12", "rustls-pemfile 1.0.4", - "rustls-webpki", + "rustls-webpki 0.101.7", "serde", "serde_json", "serde_with", @@ -2803,7 +2826,7 @@ dependencies = [ "thiserror", "time", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tokio-rustls-acme", "tokio-util", "toml", @@ -2811,7 +2834,7 @@ dependencies = [ "tracing-subscriber", "url", "watchable", - "webpki-roots", + "webpki-roots 0.25.4", "windows 0.51.1", "wmi", "x509-parser 0.15.1", @@ -2843,7 +2866,7 @@ dependencies = [ "iroh-quinn-udp", "pin-project-lite", "rustc-hash", - "rustls", + "rustls 0.21.12", "thiserror", "tokio", "tracing", @@ -2859,7 +2882,7 @@ dependencies = [ "rand", "ring 0.16.20", "rustc-hash", - "rustls", + "rustls 0.21.12", "rustls-native-certs", "slab", "thiserror", @@ -2901,6 +2924,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.10.5" @@ -2945,9 +2974,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libm" @@ -2973,9 +3002,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" @@ -2999,7 +3028,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -3084,9 +3113,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", ] @@ -3313,11 +3342,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -3356,9 +3384,9 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", @@ -3367,9 +3395,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -3403,7 +3431,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -3563,9 +3591,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pem" @@ -3573,7 +3601,7 @@ version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "serde", ] @@ -3623,7 +3651,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -3654,7 +3682,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -3679,7 +3707,7 @@ dependencies = [ "ed25519-dalek", "mainline", "rand", - "reqwest", + "reqwest 0.11.27", "self_cell", "simple-dns", "thiserror", @@ -3716,9 +3744,9 @@ checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7" [[package]] name = "plotters" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" dependencies = [ "num-traits", "plotters-backend", @@ -3729,15 +3757,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" +checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" [[package]] name = "plotters-svg" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" dependencies = [ "plotters-backend", ] @@ -3760,7 +3788,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -3944,9 +3972,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" dependencies = [ "unicode-ident", ] @@ -3971,7 +3999,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -4124,29 +4152,29 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80" +checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" dependencies = [ "bitflags 2.5.0", "cassowary", "compact_str", "crossterm", - "indoc", "itertools 0.12.1", "lru", "paste", "stability", "strum 0.26.2", "unicode-segmentation", + "unicode-truncate", "unicode-width", ] [[package]] name = "raw-cpuid" -version = "11.0.1" +version = "11.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d86a7c4638d42c44551f4791a20e687dbb4c3de1f33c43dd71e355cd429def1" +checksum = "e29830cbb1290e404f24c73af91c5d8d631ce7e128691e9477556b540cd01ecd" dependencies = [ "bitflags 2.5.0", ] @@ -4244,22 +4272,22 @@ dependencies = [ [[package]] name = "ref-cast" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4846d4c50d1721b1a3bef8af76924eef20d5e723647333798c1b519b3a9473f" +checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fddb4f8d99b0a2ebafc65a87a69a7b9875e4b1ae1f00db265d300ef7f28bccc" +checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -4338,7 +4366,7 @@ dependencies = [ "http 0.2.12", "http-body 0.4.6", "hyper 0.14.28", - "hyper-rustls", + "hyper-rustls 0.24.2", "ipnet", "js-sys", "log", @@ -4346,7 +4374,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls", + "rustls 0.21.12", "rustls-pemfile 1.0.4", "serde", "serde_json", @@ -4354,14 +4382,55 @@ dependencies = [ "sync_wrapper 0.1.2", "system-configuration 0.5.1", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.25.4", + "winreg 0.50.0", +] + +[[package]] +name = "reqwest" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.3.1", + "hyper-rustls 0.26.0", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.22.4", + "rustls-pemfile 2.1.2", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "tokio", + "tokio-rustls 0.25.0", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", - "winreg", + "webpki-roots 0.26.1", + "winreg 0.52.0", ] [[package]] @@ -4455,9 +4524,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -4498,16 +4567,30 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.11" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring 0.17.8", - "rustls-webpki", + "rustls-webpki 0.101.7", "sct", ] +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring 0.17.8", + "rustls-pki-types", + "rustls-webpki 0.102.4", + "subtle", + "zeroize", +] + [[package]] name = "rustls-native-certs" version = "0.6.3" @@ -4535,15 +4618,15 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.5.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" @@ -4555,11 +4638,22 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "rustls-webpki" +version = "0.102.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +dependencies = [ + "ring 0.17.8", + "rustls-pki-types", + "untrusted 0.9.0", +] + [[package]] name = "rustversion" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "rusty-fork" @@ -4598,9 +4692,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "salsa20" @@ -4661,11 +4755,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "core-foundation", "core-foundation-sys", "libc", @@ -4674,9 +4768,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", @@ -4684,24 +4778,24 @@ dependencies = [ [[package]] name = "self_cell" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba" +checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.198" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" dependencies = [ "serde_derive", ] @@ -4736,20 +4830,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -4768,9 +4862,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -4798,11 +4892,11 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.8.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c85f8e96d1d6857f13768fcbd895fcb06225510022a2774ed8b5150581847b0" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", @@ -4816,14 +4910,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.8.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8b3a576c4eb2924262d5951a3b737ccaf16c931e39a2810c36f9a7e25575557" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -4967,9 +5061,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", @@ -5058,7 +5152,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" dependencies = [ "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -5079,12 +5173,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "strsim" version = "0.11.1" @@ -5111,7 +5199,7 @@ dependencies = [ "proc-macro2", "quote", "struct_iterable_internal", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -5129,7 +5217,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -5140,7 +5228,7 @@ checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -5171,7 +5259,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -5184,7 +5272,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -5193,7 +5281,7 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0adebf9fb8fba5c39ee34092b0383f247e4d1255b98fcffec94b4b797b85b677" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "bounded-integer", "byteorder", "crc", @@ -5246,9 +5334,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106" dependencies = [ "proc-macro2", "quote", @@ -5298,7 +5386,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -5370,7 +5458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", - "fastrand 2.0.2", + "fastrand 2.1.0", "rustix", "windows-sys 0.52.0", ] @@ -5384,7 +5472,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -5403,22 +5491,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -5516,7 +5604,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -5525,7 +5613,18 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls", + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", "tokio", ] @@ -5545,16 +5644,16 @@ dependencies = [ "pem", "proc-macro2", "rcgen 0.12.1", - "reqwest", + "reqwest 0.11.27", "ring 0.17.8", - "rustls", + "rustls 0.21.12", "serde", "serde_json", "thiserror", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "url", - "webpki-roots", + "webpki-roots 0.25.4", "x509-parser 0.16.0", ] @@ -5587,39 +5686,38 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "futures-util", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "pin-project-lite", "slab", "tokio", - "tracing", ] [[package]] name = "toml" -version = "0.8.12" +version = "0.8.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" dependencies = [ "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.12", + "toml_edit 0.22.13", ] [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] @@ -5637,15 +5735,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.12" +version = "0.22.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" +checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" dependencies = [ "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.6", + "winnow 0.6.8", ] [[package]] @@ -5741,7 +5839,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -5863,11 +5961,21 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +[[package]] +name = "unicode-truncate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226" +dependencies = [ + "itertools 0.12.1", + "unicode-width", +] + [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "unicode-xid" @@ -5938,9 +6046,9 @@ dependencies = [ [[package]] name = "waker-fn" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" [[package]] name = "walkdir" @@ -5994,7 +6102,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", "wasm-bindgen-shared", ] @@ -6028,7 +6136,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6067,6 +6175,15 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +[[package]] +name = "webpki-roots" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "whoami" version = "1.5.1" @@ -6205,7 +6322,7 @@ checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -6216,7 +6333,7 @@ checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -6227,7 +6344,7 @@ checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -6238,7 +6355,7 @@ checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] @@ -6400,9 +6517,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" dependencies = [ "memchr", ] @@ -6417,6 +6534,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "wmi" version = "0.13.3" @@ -6512,22 +6639,22 @@ checksum = "edb37266251c28b03d08162174a91c3a092e3bd4f476f8205ee1c507b78b7bdc" [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.65", ] [[package]] diff --git a/iroh-cli/Cargo.toml b/iroh-cli/Cargo.toml index 112a977e2d..91ab81394e 100644 --- a/iroh-cli/Cargo.toml +++ b/iroh-cli/Cargo.toml @@ -48,7 +48,7 @@ postcard = "1.0.8" quic-rpc = { version = "0.9.0", features = ["flume-transport", "quinn-transport"] } rand = "0.8.5" ratatui = "0.26.2" -reqwest = { version = "0.11.19", default-features = false, features = ["json", "rustls-tls"] } +reqwest = { version = "0.12.4", default-features = false, features = ["json", "rustls-tls"] } rustyline = "12.0.0" serde = { version = "1.0.197", features = ["derive"] } serde_with = "3.7.0" diff --git a/iroh-metrics/Cargo.toml b/iroh-metrics/Cargo.toml index 3670038ed6..6789a13d7d 100644 --- a/iroh-metrics/Cargo.toml +++ b/iroh-metrics/Cargo.toml @@ -22,7 +22,7 @@ hyper = { version = "1", features = ["server", "http1"] } hyper-util = { version = "0.1.1", features = ["tokio"] } once_cell = "1.17.0" prometheus-client = { version = "0.22.0", optional = true } -reqwest = { version = "0.11.19", default-features = false, features = ["json", "rustls-tls"] } +reqwest = { version = "0.12.4", default-features = false, features = ["json", "rustls-tls"] } serde = { version = "1.0", features = ["derive"] } struct_iterable = "0.1" time = { version = "0.3.21", features = ["serde-well-known"] } diff --git a/iroh-net/Cargo.toml b/iroh-net/Cargo.toml index 9b52533a49..9d0f7cdc36 100644 --- a/iroh-net/Cargo.toml +++ b/iroh-net/Cargo.toml @@ -19,6 +19,7 @@ workspace = true axum = { version = "0.7.4", optional = true } aead = { version = "0.5.2", features = ["bytes"] } anyhow = { version = "1" } +base64 = "0.22.1" backoff = "0.4.0" bytes = "1" netdev = "0.25" @@ -54,7 +55,7 @@ quinn-udp = { package = "iroh-quinn-udp", version = "0.4" } rand = "0.8" rand_core = "0.6.4" rcgen = "0.11" -reqwest = { version = "0.11.19", default-features = false, features = ["rustls-tls"] } +reqwest = { version = "0.12.4", default-features = false, features = ["rustls-tls"] } ring = "0.17" rustls = { version = "0.21.11", default-features = false, features = ["dangerous_configuration"] } serde = { version = "1", features = ["derive", "rc"] } diff --git a/iroh-net/src/endpoint.rs b/iroh-net/src/endpoint.rs index 130db7e70d..e5cf7936ec 100644 --- a/iroh-net/src/endpoint.rs +++ b/iroh-net/src/endpoint.rs @@ -16,6 +16,7 @@ use derive_more::Debug; use futures_lite::{Stream, StreamExt}; use tokio_util::sync::{CancellationToken, WaitForCancellationFuture}; use tracing::{debug, info_span, trace, warn}; +use url::Url; use crate::{ config, @@ -58,6 +59,7 @@ pub struct Builder { concurrent_connections: Option, keylog: bool, discovery: Option>, + proxy_url: Option, /// Path for known peers. See [`Builder::peers_data_path`]. peers_path: Option, dns_resolver: Option, @@ -75,6 +77,7 @@ impl Default for Builder { concurrent_connections: Default::default(), keylog: Default::default(), discovery: Default::default(), + proxy_url: None, peers_path: None, dns_resolver: None, #[cfg(any(test, feature = "test-utils"))] @@ -100,6 +103,23 @@ impl Builder { self } + /// Set an explicit proxy url to proxy all HTTP(S) traffic through. + pub fn proxy_url(mut self, url: Url) -> Self { + self.proxy_url.replace(url); + self + } + + /// Set the proxy url from the environment, in this order: + /// + /// - `HTTP_PROXY` + /// - `http_proxy` + /// - `HTTPS_PROXY` + /// - `https_proxy` + pub fn proxy_from_env(mut self) -> Self { + self.proxy_url = proxy_url_from_env(); + self + } + /// If *keylog* is `true` and the KEYLOGFILE environment variable is present it will be /// considered a filename to which the TLS pre-master keys are logged. This can be useful /// to be able to decrypt captured traffic for debugging purposes. @@ -225,6 +245,7 @@ impl Builder { relay_map, nodes_path: self.peers_path, discovery: self.discovery, + proxy_url: self.proxy_url, dns_resolver, #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify: self.insecure_skip_relay_cert_verify, @@ -831,6 +852,53 @@ fn try_send_rtt_msg(conn: &quinn::Connection, magic_ep: &Endpoint) { } } +/// Read a proxy url from the environemnt, in this order +/// +/// - `HTTP_PROXY` +/// - `http_proxy` +/// - `HTTPS_PROXY` +/// - `https_proxy` +fn proxy_url_from_env() -> Option { + if let Some(url) = std::env::var("HTTP_PROXY") + .ok() + .and_then(|s| s.parse::().ok()) + { + if is_cgi() { + warn!("HTTP_PROXY environment variable ignored in CGI"); + } else { + return Some(url); + } + } + if let Some(url) = std::env::var("http_proxy") + .ok() + .and_then(|s| s.parse::().ok()) + { + return Some(url); + } + if let Some(url) = std::env::var("HTTPS_PROXY") + .ok() + .and_then(|s| s.parse::().ok()) + { + return Some(url); + } + if let Some(url) = std::env::var("https_proxy") + .ok() + .and_then(|s| s.parse::().ok()) + { + return Some(url); + } + + None +} + +/// Check if we are being executed in a CGI context. +/// +/// If so, a malicious client can send the `Proxy:` header, and it will +/// be in the `HTTP_PROXY` env var. So we don't use it :) +fn is_cgi() -> bool { + std::env::var_os("REQUEST_METHOD").is_some() +} + // TODO: These tests could still be flaky, lets fix that: // https://github.com/n0-computer/iroh/issues/1183 #[cfg(test)] diff --git a/iroh-net/src/magicsock.rs b/iroh-net/src/magicsock.rs index 8243f4191d..f5c745dbc7 100644 --- a/iroh-net/src/magicsock.rs +++ b/iroh-net/src/magicsock.rs @@ -46,6 +46,7 @@ use tokio_util::sync::CancellationToken; use tracing::{ debug, error, error_span, info, info_span, instrument, trace, trace_span, warn, Instrument, }; +use url::Url; use watchable::Watchable; use crate::{ @@ -117,6 +118,9 @@ pub(super) struct Options { /// configuration. pub dns_resolver: DnsResolver, + /// Proxy configuration. + pub proxy_url: Option, + /// Skip verification of SSL certificates from relay servers /// /// May only be used in tests. @@ -132,6 +136,7 @@ impl Default for Options { relay_map: RelayMap::empty(), nodes_path: None, discovery: None, + proxy_url: None, dns_resolver: crate::dns::default_resolver().clone(), #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify: false, @@ -170,6 +175,9 @@ pub(super) struct MagicSock { relay_actor_sender: mpsc::Sender, /// String representation of the node_id of this node. me: String, + /// Proxy + proxy_url: Option, + /// Used for receiving relay messages. relay_recv_receiver: flume::Receiver, /// Stores wakers, to be called when relay_recv_ch receives new data. @@ -249,6 +257,11 @@ impl MagicSock { self.my_relay.get() } + /// Get the current proxy configuration. + pub fn proxy_url(&self) -> Option<&Url> { + self.proxy_url.as_ref() + } + /// Sets the relay node with the best latency. /// /// If we are not connected to any relay nodes, set this to `None`. @@ -1283,6 +1296,7 @@ impl Handle { discovery, nodes_path, dns_resolver, + proxy_url, #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify, } = opts; @@ -1341,6 +1355,7 @@ impl Handle { me, port: AtomicU16::new(port), secret_key, + proxy_url, local_addrs: std::sync::RwLock::new((ipv4_addr, ipv6_addr)), closing: AtomicBool::new(false), closed: AtomicBool::new(false), diff --git a/iroh-net/src/magicsock/relay_actor.rs b/iroh-net/src/magicsock/relay_actor.rs index ca46d2109e..ecae432cf3 100644 --- a/iroh-net/src/magicsock/relay_actor.rs +++ b/iroh-net/src/magicsock/relay_actor.rs @@ -479,7 +479,11 @@ impl RelayActor { let url1 = url.clone(); // building a client dials the relay - let builder = relay::http::ClientBuilder::new(url1.clone()) + let mut builder = relay::http::ClientBuilder::new(url1.clone()); + if let Some(url) = self.msock.proxy_url() { + builder = builder.proxy_url(url.clone()); + } + let builder = builder .address_family_selector(move || { let ipv6_reported = ipv6_reported.clone(); Box::pin(async move { ipv6_reported.load(Ordering::Relaxed) }) diff --git a/iroh-net/src/relay/client.rs b/iroh-net/src/relay/client.rs index cbee0aea0a..bf0e069bfc 100644 --- a/iroh-net/src/relay/client.rs +++ b/iroh-net/src/relay/client.rs @@ -8,12 +8,13 @@ use bytes::Bytes; use futures_lite::StreamExt; use futures_sink::Sink; use futures_util::sink::SinkExt; -use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::io::AsyncWrite; use tokio::sync::mpsc; use tokio_util::codec::{FramedRead, FramedWrite}; use tracing::{debug, info_span, trace, Instrument}; use super::codec::PER_CLIENT_READ_QUEUE_DEPTH; +use super::http::streams::{MaybeTlsStreamReader, MaybeTlsStreamWriter}; use super::{ codec::{ write_frame, DerpCodec, Frame, MAX_PACKET_SIZE, PER_CLIENT_SEND_QUEUE_DEPTH, @@ -63,7 +64,7 @@ impl ClientReceiver { } } -type RelayReader = FramedRead, DerpCodec>; +type RelayReader = FramedRead; #[derive(derive_more::Debug)] pub struct InnerClient { @@ -247,7 +248,7 @@ impl ClientWriter { pub struct ClientBuilder { secret_key: SecretKey, reader: RelayReader, - writer: FramedWrite, DerpCodec>, + writer: FramedWrite, local_addr: SocketAddr, } @@ -255,8 +256,8 @@ impl ClientBuilder { pub fn new( secret_key: SecretKey, local_addr: SocketAddr, - reader: Box, - writer: Box, + reader: MaybeTlsStreamReader, + writer: MaybeTlsStreamWriter, ) -> Self { Self { secret_key, diff --git a/iroh-net/src/relay/http.rs b/iroh-net/src/relay/http.rs index 5ea0aee188..e73da2de73 100644 --- a/iroh-net/src/relay/http.rs +++ b/iroh-net/src/relay/http.rs @@ -3,6 +3,7 @@ //! mod client; mod server; +pub(crate) mod streams; pub use self::client::{Client, ClientBuilder, ClientError, ClientReceiver}; pub use self::server::{Server, ServerBuilder, TlsAcceptor, TlsConfig}; diff --git a/iroh-net/src/relay/http/client.rs b/iroh-net/src/relay/http/client.rs index 339a50e432..39c9302bd4 100644 --- a/iroh-net/src/relay/http/client.rs +++ b/iroh-net/src/relay/http/client.rs @@ -5,16 +5,18 @@ use std::net::{IpAddr, SocketAddr}; use std::sync::Arc; use std::time::Duration; -use anyhow::bail; +use base64::{engine::general_purpose::URL_SAFE, Engine as _}; use bytes::Bytes; use futures_lite::future::Boxed as BoxFuture; +use http_body_util::Empty; use hyper::body::Incoming; use hyper::header::UPGRADE; -use hyper::upgrade::{Parts, Upgraded}; +use hyper::upgrade::Parts; use hyper::Request; +use hyper_util::rt::TokioIo; use rand::Rng; use rustls::client::Resumption; -use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite}; +use tokio::io::{AsyncRead, AsyncWrite}; use tokio::net::TcpStream; use tokio::sync::{mpsc, oneshot}; use tokio::task::JoinSet; @@ -24,13 +26,17 @@ use url::Url; use crate::dns::{DnsResolver, ResolverExt}; use crate::key::{PublicKey, SecretKey}; +use crate::relay::http::streams::{downcast_upgrade, MaybeTlsStream}; use crate::relay::RelayUrl; use crate::relay::{ client::Client as RelayClient, client::ClientBuilder as RelayClientBuilder, client::ClientReceiver as RelayClientReceiver, ReceivedMessage, }; +use crate::util::chain; use crate::util::AbortingJoinHandle; +use super::streams::ProxyStream; + const DIAL_NODE_TIMEOUT: Duration = Duration::from_millis(1500); const PING_TIMEOUT: Duration = Duration::from_secs(5); const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); @@ -87,6 +93,9 @@ pub enum ClientError { /// The connection failed to upgrade #[error("failed to upgrade connection: {0}")] Upgrade(String), + /// The connection failed to proxy + #[error("failed to proxy connection: {0}")] + Proxy(String), /// The relay [`super::client::Client`] failed to build #[error("failed to build relay client: {0}")] Build(String), @@ -159,6 +168,7 @@ struct Actor { pings: PingTracker, ping_tasks: JoinSet<()>, dns_resolver: DnsResolver, + proxy_url: Option, } #[derive(Default, Debug)] @@ -200,6 +210,8 @@ pub struct ClientBuilder { /// Allow self-signed certificates from relay servers #[cfg(any(test, feature = "test-utils"))] insecure_skip_cert_verify: bool, + /// HTTP Proxy + proxy_url: Option, } impl std::fmt::Debug for ClientBuilder { @@ -224,6 +236,7 @@ impl ClientBuilder { url: url.into(), #[cfg(any(test, feature = "test-utils"))] insecure_skip_cert_verify: false, + proxy_url: None, } } @@ -275,6 +288,12 @@ impl ClientBuilder { self } + /// Set an explicit proxy url to proxy all HTTP(S) traffic through. + pub fn proxy_url(mut self, url: Url) -> Self { + self.proxy_url.replace(url); + self + } + /// Build the [`Client`] pub fn build(self, key: SecretKey, dns_resolver: DnsResolver) -> (Client, ClientReceiver) { // TODO: review TLS config @@ -316,6 +335,7 @@ impl ClientBuilder { url: self.url, tls_connector, dns_resolver, + proxy_url: self.proxy_url, }; let (msg_sender, inbox) = mpsc::channel(64); @@ -762,18 +782,6 @@ impl Actor { .and_then(|s| rustls::ServerName::try_from(s).ok()) } - fn url_port(&self) -> Option { - if let Some(port) = self.url.port() { - return Some(port); - } - - match self.url.scheme() { - "http" => Some(80), - "https" => Some(443), - _ => None, - } - } - fn use_https(&self) -> bool { // only disable https if we are explicitly dialing a http url if self.url.scheme() == "http" { @@ -782,14 +790,22 @@ impl Actor { true } - async fn dial_url(&self) -> Result { - debug!(%self.url, "dial url"); + async fn dial_url(&self) -> Result { + if let Some(ref proxy) = self.proxy_url { + let stream = self.dial_url_proxy(proxy.clone()).await?; + Ok(ProxyStream::Proxied(stream)) + } else { + let stream = self.dial_url_direct().await?; + Ok(ProxyStream::Raw(stream)) + } + } + async fn dial_url_direct(&self) -> Result { + debug!(%self.url, "dial url"); let prefer_ipv6 = self.prefer_ipv6().await; let dst_ip = resolve_host(&self.dns_resolver, &self.url, prefer_ipv6).await?; - let port = self - .url_port() + let port = url_port(&self.url) .ok_or_else(|| ClientError::InvalidUrl("missing url port".into()))?; let addr = SocketAddr::new(dst_ip, port); @@ -808,6 +824,102 @@ impl Actor { Ok(tcp_stream) } + async fn dial_url_proxy( + &self, + proxy_url: Url, + ) -> Result, MaybeTlsStream>, ClientError> { + debug!(%self.url, %proxy_url, "dial url via proxy"); + + // Resolve proxy DNS + let prefer_ipv6 = self.prefer_ipv6().await; + let proxy_ip = resolve_host(&self.dns_resolver, &proxy_url, prefer_ipv6).await?; + + let proxy_port = url_port(&proxy_url) + .ok_or_else(|| ClientError::Proxy("missing proxy url port".into()))?; + let proxy_addr = SocketAddr::new(proxy_ip, proxy_port); + + debug!(%proxy_addr, "connecting to proxy"); + + let tcp_stream = tokio::time::timeout(DIAL_NODE_TIMEOUT, async move { + TcpStream::connect(proxy_addr).await + }) + .await + .map_err(|_| ClientError::ConnectTimeout)? + .map_err(ClientError::DialIO)?; + + tcp_stream.set_nodelay(true)?; + + // Setup TLS if necessary + let io = if proxy_url.scheme() == "http" { + MaybeTlsStream::Raw(tcp_stream) + } else { + let hostname = proxy_url + .host_str() + .and_then(|s| rustls::ServerName::try_from(s).ok()) + .ok_or_else(|| ClientError::InvalidUrl("No tls servername for proxy url".into()))?; + let tls_stream = self.tls_connector.connect(hostname, tcp_stream).await?; + MaybeTlsStream::Tls(tls_stream) + }; + let io = TokioIo::new(io); + + let target_host = self + .url + .host_str() + .ok_or_else(|| ClientError::Proxy("missing proxy host".into()))?; + + let port = + url_port(&self.url).ok_or_else(|| ClientError::Proxy("invalid target port".into()))?; + + // Establish Proxy Tunnel + let mut req_builder = Request::builder() + .uri(format!("{}:{}", target_host, port)) + .method("CONNECT") + .header("Host", target_host) + .header("Proxy-Connection", "Keep-Alive"); + if !proxy_url.username().is_empty() { + // Passthrough authorization + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Proxy-Authorization + debug!( + "setting proxy-authorization: username={}", + proxy_url.username() + ); + let to_encode = format!( + "{}:{}", + proxy_url.username(), + proxy_url.password().unwrap_or_default() + ); + let encoded = URL_SAFE.encode(to_encode); + req_builder = req_builder.header("Proxy-Authorization", format!("Basic {}", encoded)); + } + let req = req_builder.body(Empty::::new())?; + + debug!("Sending proxy request: {:?}", req); + + let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await?; + tokio::task::spawn(async move { + if let Err(err) = conn.with_upgrades().await { + error!("Proxy connection failed: {:?}", err); + } + }); + + let res = sender.send_request(req).await?; + if !res.status().is_success() { + return Err(ClientError::Proxy(format!( + "failed to connect to proxy: {}", + res.status(), + ))); + } + + let upgraded = hyper::upgrade::on(res).await?; + let Ok(Parts { io, read_buf, .. }) = upgraded.downcast::>() else { + return Err(ClientError::Proxy("invalid upgrade".to_string())); + }; + + let res = chain::chain(std::io::Cursor::new(read_buf), io.into_inner()); + + Ok(res) + } + /// Reports whether IPv4 dials should be slightly /// delayed to give IPv6 a better chance of winning dial races. /// Implementations should only return true if IPv6 is expected @@ -882,38 +994,6 @@ async fn resolve_host( } } -fn downcast_upgrade( - upgraded: Upgraded, -) -> anyhow::Result<( - Box, - Box, -)> { - match upgraded.downcast::>() { - Ok(Parts { read_buf, io, .. }) => { - let (reader, writer) = tokio::io::split(io.into_inner()); - // Prepend data to the reader to avoid data loss - let reader = std::io::Cursor::new(read_buf).chain(reader); - - Ok((Box::new(reader), Box::new(writer))) - } - Err(upgraded) => { - if let Ok(Parts { read_buf, io, .. }) = - upgraded.downcast::>>() - { - let (reader, writer) = tokio::io::split(io.into_inner()); - // Prepend data to the reader to avoid data loss - let reader = std::io::Cursor::new(read_buf).chain(reader); - - return Ok((Box::new(reader), Box::new(writer))); - } - - bail!( - "could not downcast the upgraded connection to a TcpStream or client::TlsStream" - ) - } - } -} - /// Used to allow self signed certificates in tests #[cfg(any(test, feature = "test-utils"))] struct NoCertVerifier; @@ -933,9 +1013,21 @@ impl rustls::client::ServerCertVerifier for NoCertVerifier { } } +fn url_port(url: &Url) -> Option { + if let Some(port) = url.port() { + return Some(port); + } + + match url.scheme() { + "http" => Some(80), + "https" => Some(443), + _ => None, + } +} + #[cfg(test)] mod tests { - use anyhow::Result; + use anyhow::{bail, Result}; use crate::dns::default_resolver; diff --git a/iroh-net/src/relay/http/streams.rs b/iroh-net/src/relay/http/streams.rs new file mode 100644 index 0000000000..7910049683 --- /dev/null +++ b/iroh-net/src/relay/http/streams.rs @@ -0,0 +1,292 @@ +use std::{ + net::SocketAddr, + pin::Pin, + task::{Context, Poll}, +}; + +use anyhow::{bail, Result}; +use bytes::Bytes; +use hyper::upgrade::{Parts, Upgraded}; +use hyper_util::rt::TokioIo; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + net::TcpStream, +}; + +use crate::util::chain; + +pub enum MaybeTlsStreamReader { + Raw(chain::Chain, tokio::io::ReadHalf>), + Tls( + chain::Chain< + std::io::Cursor, + tokio::io::ReadHalf>, + >, + ), + #[cfg(test)] + Mem(tokio::io::ReadHalf), +} + +impl AsyncRead for MaybeTlsStreamReader { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + match &mut *self { + Self::Raw(stream) => Pin::new(stream).poll_read(cx, buf), + Self::Tls(stream) => Pin::new(stream).poll_read(cx, buf), + #[cfg(test)] + Self::Mem(stream) => Pin::new(stream).poll_read(cx, buf), + } + } +} + +pub enum MaybeTlsStreamWriter { + Raw(tokio::io::WriteHalf), + Tls(tokio::io::WriteHalf>), + #[cfg(test)] + Mem(tokio::io::WriteHalf), +} + +impl AsyncWrite for MaybeTlsStreamWriter { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + match &mut *self { + Self::Raw(stream) => Pin::new(stream).poll_write(cx, buf), + Self::Tls(stream) => Pin::new(stream).poll_write(cx, buf), + #[cfg(test)] + Self::Mem(stream) => Pin::new(stream).poll_write(cx, buf), + } + } + + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + match &mut *self { + Self::Raw(stream) => Pin::new(stream).poll_flush(cx), + Self::Tls(stream) => Pin::new(stream).poll_flush(cx), + #[cfg(test)] + Self::Mem(stream) => Pin::new(stream).poll_flush(cx), + } + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + match &mut *self { + Self::Raw(stream) => Pin::new(stream).poll_shutdown(cx), + Self::Tls(stream) => Pin::new(stream).poll_shutdown(cx), + #[cfg(test)] + Self::Mem(stream) => Pin::new(stream).poll_shutdown(cx), + } + } + + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[std::io::IoSlice<'_>], + ) -> Poll> { + match &mut *self { + Self::Raw(stream) => Pin::new(stream).poll_write_vectored(cx, bufs), + Self::Tls(stream) => Pin::new(stream).poll_write_vectored(cx, bufs), + #[cfg(test)] + Self::Mem(stream) => Pin::new(stream).poll_write_vectored(cx, bufs), + } + } +} + +pub fn downcast_upgrade( + upgraded: Upgraded, +) -> Result<(MaybeTlsStreamReader, MaybeTlsStreamWriter)> { + match upgraded.downcast::>() { + Ok(Parts { read_buf, io, .. }) => { + let inner = io.into_inner(); + let (reader, writer) = tokio::io::split(inner); + // Prepend data to the reader to avoid data loss + let reader = chain::chain(std::io::Cursor::new(read_buf), reader); + Ok(( + MaybeTlsStreamReader::Raw(reader), + MaybeTlsStreamWriter::Raw(writer), + )) + } + Err(upgraded) => { + if let Ok(Parts { read_buf, io, .. }) = + upgraded.downcast::>>() + { + let inner = io.into_inner(); + let (reader, writer) = tokio::io::split(inner); + // Prepend data to the reader to avoid data loss + let reader = chain::chain(std::io::Cursor::new(read_buf), reader); + + return Ok(( + MaybeTlsStreamReader::Tls(reader), + MaybeTlsStreamWriter::Tls(writer), + )); + } + + bail!( + "could not downcast the upgraded connection to a TcpStream or client::TlsStream" + ) + } + } +} + +pub enum ProxyStream { + Raw(TcpStream), + Proxied(chain::Chain, MaybeTlsStream>), +} + +impl AsyncRead for ProxyStream { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + match &mut *self { + Self::Raw(stream) => Pin::new(stream).poll_read(cx, buf), + Self::Proxied(stream) => Pin::new(stream).poll_read(cx, buf), + } + } +} + +impl AsyncWrite for ProxyStream { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + match &mut *self { + Self::Raw(stream) => Pin::new(stream).poll_write(cx, buf), + Self::Proxied(stream) => Pin::new(stream.get_mut().1).poll_write(cx, buf), + } + } + + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + match &mut *self { + Self::Raw(stream) => Pin::new(stream).poll_flush(cx), + Self::Proxied(stream) => Pin::new(stream.get_mut().1).poll_flush(cx), + } + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + match &mut *self { + Self::Raw(stream) => Pin::new(stream).poll_shutdown(cx), + Self::Proxied(stream) => Pin::new(stream.get_mut().1).poll_shutdown(cx), + } + } + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[std::io::IoSlice<'_>], + ) -> Poll> { + match &mut *self { + Self::Raw(stream) => Pin::new(stream).poll_write_vectored(cx, bufs), + Self::Proxied(stream) => Pin::new(stream.get_mut().1).poll_write_vectored(cx, bufs), + } + } +} + +impl ProxyStream { + pub fn local_addr(&self) -> std::io::Result { + match self { + Self::Raw(s) => s.local_addr(), + Self::Proxied(s) => s.get_ref().1.local_addr(), + } + } + + pub fn peer_addr(&self) -> std::io::Result { + match self { + Self::Raw(s) => s.peer_addr(), + Self::Proxied(s) => s.get_ref().1.peer_addr(), + } + } +} + +pub enum MaybeTlsStream { + Raw(TcpStream), + Tls(tokio_rustls::client::TlsStream), +} + +impl AsyncRead for MaybeTlsStream { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + match &mut *self { + Self::Raw(stream) => Pin::new(stream).poll_read(cx, buf), + Self::Tls(stream) => Pin::new(stream).poll_read(cx, buf), + } + } +} + +impl AsyncWrite for MaybeTlsStream { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + match &mut *self { + Self::Raw(stream) => Pin::new(stream).poll_write(cx, buf), + Self::Tls(stream) => Pin::new(stream).poll_write(cx, buf), + } + } + + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + match &mut *self { + Self::Raw(stream) => Pin::new(stream).poll_flush(cx), + Self::Tls(stream) => Pin::new(stream).poll_flush(cx), + } + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + match &mut *self { + Self::Raw(stream) => Pin::new(stream).poll_shutdown(cx), + Self::Tls(stream) => Pin::new(stream).poll_shutdown(cx), + } + } + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[std::io::IoSlice<'_>], + ) -> Poll> { + match &mut *self { + Self::Raw(stream) => Pin::new(stream).poll_write_vectored(cx, bufs), + Self::Tls(stream) => Pin::new(stream).poll_write_vectored(cx, bufs), + } + } +} + +impl MaybeTlsStream { + pub fn local_addr(&self) -> std::io::Result { + match self { + Self::Raw(s) => s.local_addr(), + Self::Tls(s) => s.get_ref().0.local_addr(), + } + } + + pub fn peer_addr(&self) -> std::io::Result { + match self { + Self::Raw(s) => s.peer_addr(), + Self::Tls(s) => s.get_ref().0.peer_addr(), + } + } +} diff --git a/iroh-net/src/relay/server.rs b/iroh-net/src/relay/server.rs index dd4c8d6265..38493c4601 100644 --- a/iroh-net/src/relay/server.rs +++ b/iroh-net/src/relay/server.rs @@ -427,6 +427,7 @@ mod tests { use crate::relay::{ client::ClientBuilder, codec::{recv_frame, Frame, FrameType}, + http::streams::{MaybeTlsStreamReader, MaybeTlsStreamWriter}, types::ClientInfo, ReceivedMessage, }; @@ -570,13 +571,15 @@ mod tests { fn make_test_client(secret_key: SecretKey) -> (tokio::io::DuplexStream, ClientBuilder) { let (client, server) = tokio::io::duplex(10); let (client_reader, client_writer) = tokio::io::split(client); + let client_reader = MaybeTlsStreamReader::Mem(client_reader); + let client_writer = MaybeTlsStreamWriter::Mem(client_writer); ( server, ClientBuilder::new( secret_key, "127.0.0.1:0".parse().unwrap(), - Box::new(client_reader), - Box::new(client_writer), + client_reader, + client_writer, ), ) } diff --git a/iroh-net/src/util.rs b/iroh-net/src/util.rs index 58165a9b8d..e94655b51f 100644 --- a/iroh-net/src/util.rs +++ b/iroh-net/src/util.rs @@ -10,6 +10,8 @@ use std::{ use futures_lite::future::Boxed as BoxFuture; use futures_util::{future::Shared, FutureExt}; +pub mod chain; + /// A join handle that owns the task it is running, and aborts it when dropped. #[derive(Debug, derive_more::Deref)] pub struct AbortingJoinHandle { diff --git a/iroh-net/src/util/chain.rs b/iroh-net/src/util/chain.rs new file mode 100644 index 0000000000..f4b683f541 --- /dev/null +++ b/iroh-net/src/util/chain.rs @@ -0,0 +1,138 @@ +//! IO utilitiy to chain `AsyncRead`s together. + +// Based on tokios chain implementation, that doesn't make the concrete type public. + +use std::fmt; +use std::io; +use std::pin::Pin; +use std::task::ready; +use std::task::{Context, Poll}; + +use pin_project::pin_project; +use tokio::io::{AsyncBufRead, AsyncRead, ReadBuf}; + +/// Stream for the [`chain`] method. +#[must_use = "streams do nothing unless polled"] +#[pin_project] +pub struct Chain { + #[pin] + first: T, + #[pin] + second: U, + done_first: bool, +} + +/// Chain two `AsyncRead`s together. +pub fn chain(first: T, second: U) -> Chain +where + T: AsyncRead, + U: AsyncRead, +{ + Chain { + first, + second, + done_first: false, + } +} + +impl Chain +where + T: AsyncRead, + U: AsyncRead, +{ + /// Gets references to the underlying readers in this `Chain`. + pub fn get_ref(&self) -> (&T, &U) { + (&self.first, &self.second) + } + + /// Gets mutable references to the underlying readers in this `Chain`. + /// + /// Care should be taken to avoid modifying the internal I/O state of the + /// underlying readers as doing so may corrupt the internal state of this + /// `Chain`. + pub fn get_mut(&mut self) -> (&mut T, &mut U) { + (&mut self.first, &mut self.second) + } + + /// Gets pinned mutable references to the underlying readers in this `Chain`. + /// + /// Care should be taken to avoid modifying the internal I/O state of the + /// underlying readers as doing so may corrupt the internal state of this + /// `Chain`. + pub fn get_pin_mut(self: Pin<&mut Self>) -> (Pin<&mut T>, Pin<&mut U>) { + let me = self.project(); + (me.first, me.second) + } + + /// Consumes the `Chain`, returning the wrapped readers. + pub fn into_inner(self) -> (T, U) { + (self.first, self.second) + } +} + +impl fmt::Debug for Chain +where + T: fmt::Debug, + U: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Chain") + .field("t", &self.first) + .field("u", &self.second) + .finish() + } +} + +impl AsyncRead for Chain +where + T: AsyncRead, + U: AsyncRead, +{ + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let me = self.project(); + + if !*me.done_first { + let rem = buf.remaining(); + ready!(me.first.poll_read(cx, buf))?; + if buf.remaining() == rem { + *me.done_first = true; + } else { + return Poll::Ready(Ok(())); + } + } + me.second.poll_read(cx, buf) + } +} + +impl AsyncBufRead for Chain +where + T: AsyncBufRead, + U: AsyncBufRead, +{ + fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let me = self.project(); + + if !*me.done_first { + match ready!(me.first.poll_fill_buf(cx)?) { + [] => { + *me.done_first = true; + } + buf => return Poll::Ready(Ok(buf)), + } + } + me.second.poll_fill_buf(cx) + } + + fn consume(self: Pin<&mut Self>, amt: usize) { + let me = self.project(); + if !*me.done_first { + me.first.consume(amt) + } else { + me.second.consume(amt) + } + } +} diff --git a/iroh/src/node/builder.rs b/iroh/src/node/builder.rs index 61a53f2828..833071ac05 100644 --- a/iroh/src/node/builder.rs +++ b/iroh/src/node/builder.rs @@ -391,6 +391,7 @@ where let endpoint = Endpoint::builder() .secret_key(self.secret_key.clone()) + .proxy_from_env() .alpns(PROTOCOLS.iter().map(|p| p.to_vec()).collect()) .keylog(self.keylog) .transport_config(transport_config) From d81308933f39dc5a448609863402159ee72091ca Mon Sep 17 00:00:00 2001 From: Divma <26765164+divagant-martian@users.noreply.github.com> Date: Wed, 22 May 2024 11:24:38 -0500 Subject: [PATCH 4/7] feat(iroh-net)!: improve dns behaviour by staggering requests (#2313) ## Description Improves dns behaviour by staggering concurrent queries. Other changes include: - Remove unnecessary trait bounds from `ResolverExt` functions. - Unify dns capabilities under `ResolverExt`. - Add helper fn `stagger_call` with a minimal test. - Add staggering versions of the `ResolverExt` functions. - Use staggered options and turn on tests ## Breaking Changes - `iroh_net::dns::node_info::lookup_by_domain` moved to `iroh_net::ResolverExt::lookup_by_domain` ## Notes & open questions Whether test are less prone to fail randomly is something I can't test locally (no windows) so I'm trusting ci here. ## Change checklist - [x] Self-review. - [x] Documentation updates if relevant. - [x] Tests if relevant. - [x] All breaking changes documented. --- iroh-dns-server/examples/resolve.rs | 2 +- iroh-dns-server/src/lib.rs | 9 +- iroh-net/src/discovery.rs | 6 +- iroh-net/src/discovery/dns.rs | 9 +- iroh-net/src/dns.rs | 251 +++++++++++++++++++++++++++- iroh-net/src/dns/node_info.rs | 23 +-- iroh-net/src/netcheck/reportgen.rs | 16 +- 7 files changed, 269 insertions(+), 47 deletions(-) diff --git a/iroh-dns-server/examples/resolve.rs b/iroh-dns-server/examples/resolve.rs index b9464ab38c..e660c6335c 100644 --- a/iroh-dns-server/examples/resolve.rs +++ b/iroh-dns-server/examples/resolve.rs @@ -57,7 +57,7 @@ async fn main() -> anyhow::Result<()> { TxtAttrs::::lookup_by_id(&resolver, &node_id, origin).await? } Command::Domain { domain } => { - TxtAttrs::::lookup_by_domain(&resolver, &domain).await? + TxtAttrs::::lookup_by_name(&resolver, &domain).await? } }; println!("resolved node {}", resolved.node_id()); diff --git a/iroh-dns-server/src/lib.rs b/iroh-dns-server/src/lib.rs index 95e09bfc1e..117b2c6d9c 100644 --- a/iroh-dns-server/src/lib.rs +++ b/iroh-dns-server/src/lib.rs @@ -22,10 +22,7 @@ mod tests { }; use iroh_net::{ discovery::pkarr_publish::PkarrRelayClient, - dns::{ - node_info::{lookup_by_id, NodeInfo}, - DnsResolver, - }, + dns::{node_info::NodeInfo, DnsResolver, ResolverExt}, key::SecretKey, }; use pkarr::{PkarrClient, SignedPacket}; @@ -168,7 +165,7 @@ mod tests { pkarr.publish(&signed_packet).await?; let resolver = test_resolver(nameserver); - let res = lookup_by_id(&resolver, &node_id, origin).await?; + let res = resolver.lookup_by_id(&node_id, origin).await?; assert_eq!(res.node_id, node_id); assert_eq!(res.info.relay_url.map(Url::from), Some(relay_url)); @@ -204,7 +201,7 @@ mod tests { // resolve via DNS from our server, which will lookup from our DHT let resolver = test_resolver(nameserver); - let res = lookup_by_id(&resolver, &node_id, origin).await?; + let res = resolver.lookup_by_id(&node_id, origin).await?; assert_eq!(res.node_id, node_id); assert_eq!(res.info.relay_url.map(Url::from), Some(relay_url)); diff --git a/iroh-net/src/discovery.rs b/iroh-net/src/discovery.rs index 0fba47ca29..0b935621c2 100644 --- a/iroh-net/src/discovery.rs +++ b/iroh-net/src/discovery.rs @@ -562,7 +562,7 @@ mod test_dns_pkarr { use crate::{ discovery::pkarr_publish::PkarrPublisher, - dns::node_info::{lookup_by_id, NodeInfo}, + dns::{node_info::NodeInfo, ResolverExt}, relay::{RelayMap, RelayMode}, test_utils::{ dns_server::{create_dns_resolver, run_dns_server}, @@ -590,7 +590,7 @@ mod test_dns_pkarr { state.upsert(signed_packet)?; let resolver = create_dns_resolver(nameserver)?; - let resolved = lookup_by_id(&resolver, &node_info.node_id, &origin).await?; + let resolved = resolver.lookup_by_id(&node_info.node_id, &origin).await?; assert_eq!(resolved, node_info.into()); @@ -620,7 +620,7 @@ mod test_dns_pkarr { publisher.update_addr_info(&addr_info); // wait until our shared state received the update from pkarr publishing dns_pkarr_server.on_node(&node_id, timeout).await?; - let resolved = lookup_by_id(&resolver, &node_id, &origin).await?; + let resolved = resolver.lookup_by_id(&node_id, &origin).await?; let expected = NodeAddr { info: addr_info, diff --git a/iroh-net/src/discovery/dns.rs b/iroh-net/src/discovery/dns.rs index 76f487456f..13706deb5d 100644 --- a/iroh-net/src/discovery/dns.rs +++ b/iroh-net/src/discovery/dns.rs @@ -5,11 +5,13 @@ use futures_lite::stream::Boxed as BoxStream; use crate::{ discovery::{Discovery, DiscoveryItem}, - dns, Endpoint, NodeId, + dns::ResolverExt, + Endpoint, NodeId, }; /// The n0 testing DNS node origin pub const N0_DNS_NODE_ORIGIN: &str = "dns.iroh.link"; +const DNS_STAGGERING_MS: &[u64] = &[200, 300]; /// DNS node discovery /// @@ -53,8 +55,9 @@ impl Discovery for DnsDiscovery { let resolver = ep.dns_resolver().clone(); let origin_domain = self.origin_domain.clone(); let fut = async move { - let node_addr = - dns::node_info::lookup_by_id(&resolver, &node_id, &origin_domain).await?; + let node_addr = resolver + .lookup_by_id_staggered(&node_id, &origin_domain, DNS_STAGGERING_MS) + .await?; Ok(DiscoveryItem { provenance: "dns", last_updated: None, diff --git a/iroh-net/src/dns.rs b/iroh-net/src/dns.rs index 068166bb54..1ac64c2f7f 100644 --- a/iroh-net/src/dns.rs +++ b/iroh-net/src/dns.rs @@ -1,12 +1,19 @@ //! This module exports a DNS resolver, which is also the default resolver used in the //! [`crate::Endpoint`] if no custom resolver is configured. +//! +//! It also exports [`ResolverExt`]: A extension trait over [`DnsResolver`] to perform DNS queries +//! by ipv4, ipv6, name and node_id. See the [`node_info`] module documentation for details on how +//! iroh node records are structured. +use std::fmt::Write; use std::net::{IpAddr, Ipv6Addr}; use std::time::Duration; use anyhow::Result; -use futures_lite::Future; +use futures_lite::{Future, StreamExt}; use hickory_resolver::{AsyncResolver, IntoName, TokioAsyncResolver}; +use iroh_base::key::NodeId; +use iroh_base::node_addr::NodeAddr; use once_cell::sync::Lazy; pub mod node_info; @@ -78,14 +85,14 @@ fn create_default_resolver() -> Result { /// Extension trait to [`DnsResolver`]. pub trait ResolverExt { /// Perform an ipv4 lookup with a timeout. - fn lookup_ipv4( + fn lookup_ipv4( &self, host: N, timeout: Duration, ) -> impl Future>>; /// Perform an ipv6 lookup with a timeout. - fn lookup_ipv6( + fn lookup_ipv6( &self, host: N, timeout: Duration, @@ -97,10 +104,85 @@ pub trait ResolverExt { host: N, timeout: Duration, ) -> impl Future>>; + + /// Looks up node info by DNS name. + fn lookup_by_name(&self, name: &str) -> impl Future>; + + /// Looks up node info by [`NodeId`] and origin domain name. + fn lookup_by_id( + &self, + node_id: &NodeId, + origin: &str, + ) -> impl Future>; + + /// Perform an ipv4 lookup with a timeout in a staggered fashion. + /// + /// From the moment this function is called, each lookup is scheduled after the delays in + /// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls + /// at T+0ms, T+200ms and T+300ms. The `timeout` is applied to each call individually. The + /// result of the first successful call is returned, or a summary of all errors otherwise. + fn lookup_ipv4_staggered( + &self, + host: N, + timeout: Duration, + delays_ms: &[u64], + ) -> impl Future>>; + + /// Perform an ipv6 lookup with a timeout in a staggered fashion. + /// + /// From the moment this function is called, each lookup is scheduled after the delays in + /// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls + /// at T+0ms, T+200ms and T+300ms. The `timeout` is applied to each call individually. The + /// result of the first successful call is returned, or a summary of all errors otherwise. + fn lookup_ipv6_staggered( + &self, + host: N, + timeout: Duration, + delays_ms: &[u64], + ) -> impl Future>>; + + /// Race an ipv4 and ipv6 lookup with a timeout in a staggered fashion. + /// + /// From the moment this function is called, each lookup is scheduled after the delays in + /// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls + /// at T+0ms, T+200ms and T+300ms. The `timeout` is applied as stated in + /// [`Self::lookup_ipv4_ipv6`]. The result of the first successful call is returned, or a + /// summary of all errors otherwise. + fn lookup_ipv4_ipv6_staggered( + &self, + host: N, + timeout: Duration, + delays_ms: &[u64], + ) -> impl Future>>; + + /// Looks up node info by DNS name in a staggered fashion. + /// + /// From the moment this function is called, each lookup is scheduled after the delays in + /// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls + /// at T+0ms, T+200ms and T+300ms. The result of the first successful call is returned, or a + /// summary of all errors otherwise. + fn lookup_by_name_staggered( + &self, + name: &str, + delays_ms: &[u64], + ) -> impl Future>; + + /// Looks up node info by [`NodeId`] and origin domain name. + /// + /// From the moment this function is called, each lookup is scheduled after the delays in + /// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls + /// at T+0ms, T+200ms and T+300ms. The result of the first successful call is returned, or a + /// summary of all errors otherwise. + fn lookup_by_id_staggered( + &self, + node_id: &NodeId, + origin: &str, + delays_ms: &[u64], + ) -> impl Future>; } impl ResolverExt for DnsResolver { - async fn lookup_ipv4( + async fn lookup_ipv4( &self, host: N, timeout: Duration, @@ -109,7 +191,7 @@ impl ResolverExt for DnsResolver { Ok(addrs.into_iter().map(|ip| IpAddr::V4(ip.0))) } - async fn lookup_ipv6( + async fn lookup_ipv6( &self, host: N, timeout: Duration, @@ -142,9 +224,103 @@ impl ResolverExt for DnsResolver { } } } + + /// Looks up node info by DNS name. + /// + /// The resource records returned for `name` must either contain an [`node_info::IROH_TXT_NAME`] TXT + /// record or be a CNAME record that leads to an [`node_info::IROH_TXT_NAME`] TXT record. + async fn lookup_by_name(&self, name: &str) -> Result { + let attrs = node_info::TxtAttrs::::lookup_by_name(self, name).await?; + let info: node_info::NodeInfo = attrs.into(); + Ok(info.into()) + } + + /// Looks up node info by [`NodeId`] and origin domain name. + async fn lookup_by_id(&self, node_id: &NodeId, origin: &str) -> Result { + let attrs = + node_info::TxtAttrs::::lookup_by_id(self, node_id, origin).await?; + let info: node_info::NodeInfo = attrs.into(); + Ok(info.into()) + } + + /// Perform an ipv4 lookup with a timeout in a staggered fashion. + /// + /// From the moment this function is called, each lookup is scheduled after the delays in + /// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls + /// at T+0ms, T+200ms and T+300ms. The `timeout` is applied to each call individually. The + /// result of the first successful call is returned, or a summary of all errors otherwise. + async fn lookup_ipv4_staggered( + &self, + host: N, + timeout: Duration, + delays_ms: &[u64], + ) -> Result> { + let f = || self.lookup_ipv4(host.clone(), timeout); + stagger_call(f, delays_ms).await + } + + /// Perform an ipv6 lookup with a timeout in a staggered fashion. + /// + /// From the moment this function is called, each lookup is scheduled after the delays in + /// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls + /// at T+0ms, T+200ms and T+300ms. The `timeout` is applied to each call individually. The + /// result of the first successful call is returned, or a summary of all errors otherwise. + async fn lookup_ipv6_staggered( + &self, + host: N, + timeout: Duration, + delays_ms: &[u64], + ) -> Result> { + let f = || self.lookup_ipv6(host.clone(), timeout); + stagger_call(f, delays_ms).await + } + + /// Race an ipv4 and ipv6 lookup with a timeout in a staggered fashion. + /// + /// From the moment this function is called, each lookup is scheduled after the delays in + /// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls + /// at T+0ms, T+200ms and T+300ms. The `timeout` is applied as stated in + /// [`Self::lookup_ipv4_ipv6`]. The result of the first successful call is returned, or a + /// summary of all errors otherwise. + async fn lookup_ipv4_ipv6_staggered( + &self, + host: N, + timeout: Duration, + delays_ms: &[u64], + ) -> Result> { + let f = || self.lookup_ipv4_ipv6(host.clone(), timeout); + stagger_call(f, delays_ms).await + } + + /// Looks up node info by DNS name in a staggered fashion. + /// + /// From the moment this function is called, each lookup is scheduled after the delays in + /// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls + /// at T+0ms, T+200ms and T+300ms. The result of the first successful call is returned, or a + /// summary of all errors otherwise. + async fn lookup_by_name_staggered(&self, name: &str, delays_ms: &[u64]) -> Result { + let f = || self.lookup_by_name(name); + stagger_call(f, delays_ms).await + } + + /// Looks up node info by [`NodeId`] and origin domain name. + /// + /// From the moment this function is called, each lookup is scheduled after the delays in + /// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls + /// at T+0ms, T+200ms and T+300ms. The result of the first successful call is returned, or a + /// summary of all errors otherwise. + async fn lookup_by_id_staggered( + &self, + node_id: &NodeId, + origin: &str, + delays_ms: &[u64], + ) -> Result { + let f = || self.lookup_by_id(node_id, origin); + stagger_call(f, delays_ms).await + } } -/// Helper enum to give a unified type to the iterators of [`ResolverExt::lookup_ipv4_ipv6`] +/// Helper enum to give a unified type to the iterators of [`ResolverExt::lookup_ipv4_ipv6`]. enum LookupIter { Ipv4(A), Ipv6(B), @@ -163,11 +339,53 @@ impl, B: Iterator> Iterator for Lookup } } +/// Staggers calls to the future F with the given delays. +/// +/// The first call is performed immediately. The first call to succeed generates an Ok result +/// ignoring any previous error. If all calls fail, an error sumarizing all errors is returned. +async fn stagger_call Fut, Fut: Future>>( + f: F, + delays_ms: &[u64], +) -> Result { + let mut calls = futures_buffered::FuturesUnorderedBounded::new(delays_ms.len() + 1); + // NOTE: we add the 0 delay here to have a uniform set of futures. This is more performant than + // using alternatives that allow futures of different types. + for delay in std::iter::once(&0u64).chain(delays_ms) { + let delay = std::time::Duration::from_millis(*delay); + let fut = f(); + let staggered_fut = async move { + tokio::time::sleep(delay).await; + fut.await + }; + calls.push(staggered_fut) + } + + let mut errors = vec![]; + while let Some(call_result) = calls.next().await { + match call_result { + Ok(t) => return Ok(t), + Err(e) => errors.push(e), + } + } + + anyhow::bail!( + "no calls succeed: [ {}]", + errors.into_iter().fold(String::new(), |mut summary, e| { + write!(summary, "{e} ").expect("infallible"); + summary + }) + ) +} + #[cfg(test)] pub(crate) mod tests { + use std::sync::atomic::AtomicUsize; + use crate::defaults::NA_RELAY_HOSTNAME; use super::*; + const TIMEOUT: Duration = Duration::from_secs(5); + const STAGGERING_DELAYS: &[u64] = &[200, 300]; #[tokio::test] #[cfg_attr(target_os = "windows", ignore = "flaky")] @@ -181,16 +399,33 @@ pub(crate) mod tests { } #[tokio::test] - #[cfg_attr(target_os = "windows", ignore = "flaky")] async fn test_dns_lookup_ipv4_ipv6() { let _logging = iroh_test::logging::setup(); let resolver = default_resolver(); let res: Vec<_> = resolver - .lookup_ipv4_ipv6(NA_RELAY_HOSTNAME, Duration::from_secs(5)) + .lookup_ipv4_ipv6_staggered(NA_RELAY_HOSTNAME, TIMEOUT, STAGGERING_DELAYS) .await .unwrap() .collect(); assert!(!res.is_empty()); dbg!(res); } + + #[tokio::test] + async fn stagger_basic() { + let _logging = iroh_test::logging::setup(); + const CALL_RESULTS: &[Result] = &[Err(2), Ok(3), Ok(5), Ok(7)]; + static DONE_CALL: AtomicUsize = AtomicUsize::new(0); + let f = || { + let r_pos = DONE_CALL.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + async move { + tracing::info!(r_pos, "call"); + CALL_RESULTS[r_pos].map_err(|e| anyhow::anyhow!("{e}")) + } + }; + + let delays = [1000, 15]; + let result = stagger_call(f, &delays).await.unwrap(); + assert_eq!(result, 5) + } } diff --git a/iroh-net/src/dns/node_info.rs b/iroh-net/src/dns/node_info.rs index 4e34107674..16bf532cf4 100644 --- a/iroh-net/src/dns/node_info.rs +++ b/iroh-net/src/dns/node_info.rs @@ -62,27 +62,6 @@ pub enum IrohAttr { Addr, } -/// Looks up node info by DNS name. -/// -/// The resource records returned for `name` must either contain an [`IROH_TXT_NAME`] TXT -/// record or be a CNAME record that leads to an [`IROH_TXT_NAME`] TXT record. -pub async fn lookup_by_domain(resolver: &TokioAsyncResolver, name: &str) -> Result { - let attrs = TxtAttrs::::lookup_by_domain(resolver, name).await?; - let info: NodeInfo = attrs.into(); - Ok(info.into()) -} - -/// Looks up node info by [`NodeId`] and origin domain name. -pub async fn lookup_by_id( - resolver: &TokioAsyncResolver, - node_id: &NodeId, - origin: &str, -) -> Result { - let attrs = TxtAttrs::::lookup_by_id(resolver, node_id, origin).await?; - let info: NodeInfo = attrs.into(); - Ok(info.into()) -} - /// Encodes a [`NodeId`] in [`z-base-32`] encoding. /// /// [z-base-32]: https://philzimmermann.com/docs/human-oriented-base-32-encoding.txt @@ -300,7 +279,7 @@ impl TxtAttrs { } /// Looks up attributes by DNS name. - pub async fn lookup_by_domain(resolver: &TokioAsyncResolver, name: &str) -> Result { + pub async fn lookup_by_name(resolver: &TokioAsyncResolver, name: &str) -> Result { let name = Name::from_str(name)?; TxtAttrs::lookup(resolver, name).await } diff --git a/iroh-net/src/netcheck/reportgen.rs b/iroh-net/src/netcheck/reportgen.rs index 2085610bde..665ea66e54 100644 --- a/iroh-net/src/netcheck/reportgen.rs +++ b/iroh-net/src/netcheck/reportgen.rs @@ -73,6 +73,9 @@ const ENOUGH_NODES: usize = 3; const DNS_TIMEOUT: Duration = Duration::from_secs(3); +/// Delay used to perform staggered dns queries. +const DNS_STAGGERING_MS: &[u64] = &[200, 300]; + /// Holds the state for a single invocation of [`netcheck::Client::get_report`]. /// /// Dropping this will cancel the actor and stop the report generation. @@ -192,7 +195,7 @@ struct Actor { /// /// This is essentially the summary of all the work the [`Actor`] is doing. outstanding_tasks: OutstandingTasks, - /// The DNS resolver to use for probes that need to resolve DNS records + /// The DNS resolver to use for probes that need to resolve DNS records. dns_resolver: DnsResolver, } @@ -945,7 +948,10 @@ async fn get_relay_addr( ProbeProto::StunIpv4 | ProbeProto::IcmpV4 => match relay_node.url.host() { Some(url::Host::Domain(hostname)) => { debug!(?proto, %hostname, "Performing DNS A lookup for relay addr"); - match dns_resolver.lookup_ipv4(hostname, DNS_TIMEOUT).await { + match dns_resolver + .lookup_ipv4_staggered(hostname, DNS_TIMEOUT, DNS_STAGGERING_MS) + .await + { Ok(mut addrs) => addrs .next() .map(ip::to_canonical) @@ -962,7 +968,10 @@ async fn get_relay_addr( ProbeProto::StunIpv6 | ProbeProto::IcmpV6 => match relay_node.url.host() { Some(url::Host::Domain(hostname)) => { debug!(?proto, %hostname, "Performing DNS AAAA lookup for relay addr"); - match dns_resolver.lookup_ipv6(hostname, DNS_TIMEOUT).await { + match dns_resolver + .lookup_ipv6_staggered(hostname, DNS_TIMEOUT, DNS_STAGGERING_MS) + .await + { Ok(mut addrs) => addrs .next() .map(ip::to_canonical) @@ -1316,7 +1325,6 @@ mod tests { // // TODO: Not sure what about IPv6 pings using sysctl. #[tokio::test] - #[cfg_attr(target_os = "windows", ignore = "flaky")] async fn test_icmpk_probe_eu_relayer() { let _logging_guard = iroh_test::logging::setup(); let pinger = Pinger::new(); From e41d1d9b6bee6129a58a0760d3410bc38d9abe19 Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 22 May 2024 18:30:12 +0000 Subject: [PATCH 5/7] fix(iroh-gossip): do not drop existing connection on incoming one (#2318) Fixes https://github.com/n0-computer/iroh/issues/2307 --- iroh-gossip/src/net.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/iroh-gossip/src/net.rs b/iroh-gossip/src/net.rs index 97f554cda7..4083e3a113 100644 --- a/iroh-gossip/src/net.rs +++ b/iroh-gossip/src/net.rs @@ -615,11 +615,13 @@ async fn connection_loop( loop { tokio::select! { biased; - msg = send_rx.recv() => { - match msg { - None => break, - Some(msg) => write_message(&mut send, &mut send_buf, &msg).await?, - } + // If `send_rx` is closed, + // stop selecting it but don't quit. + // We are not going to use connection for sending anymore, + // but the other side may still want to use it to + // send data to us. + Some(msg) = send_rx.recv(), if !send_rx.is_closed() => { + write_message(&mut send, &mut send_buf, &msg).await? } msg = read_message(&mut recv, &mut recv_buf) => { From 98d45f3b862f48e89be8e5b5d2ec1b15ae6fdf9f Mon Sep 17 00:00:00 2001 From: Asmir Avdicevic Date: Wed, 22 May 2024 22:05:09 +0200 Subject: [PATCH 6/7] feat: iroh-perf (#2186) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description In progress thing for what should later be `iroh-perf`. For now you can run with `cargo run --release -- iroh` and `cargo run --release -- quinn` to do a simple comparison benchmark. More knobs are available for tuning with `--help`. There's a much more stark difference on macOS compared to running on Linux. Next steps bumping to test against `quinn 0.11` Sample output: ``` iroh (macOS) Client 0 stats: Connect time: 13.788583ms Overall download stats: Transferred 1073741824 bytes on 1 streams in 5.47s (187.27 MiB/s) Time to first byte (TTFB): 1ms Total chunks: 130319 Average chunk time: 41.936ms Average chunk size: 8.04KiB Stream download metrics: │ Throughput │ Duration ──────┼───────────────┼────────── AVG │ 187.31 MiB/s │ 5.47s P0 │ 187.25 MiB/s │ 5.46s P10 │ 187.37 MiB/s │ 5.47s P50 │ 187.37 MiB/s │ 5.47s P90 │ 187.37 MiB/s │ 5.47s P100 │ 187.37 MiB/s │ 5.47s ``` ``` quinn (macOS) Client 0 stats: Connect time: 1.81875ms Overall download stats: Transferred 1073741824 bytes on 1 streams in 2.85s (358.92 MiB/s) Time to first byte (TTFB): 61.168ms Total chunks: 47412 Average chunk time: 60.176ms Average chunk size: 22.12KiB Stream download metrics: │ Throughput │ Duration ──────┼───────────────┼────────── AVG │ 358.88 MiB/s │ 2.85s P0 │ 358.75 MiB/s │ 2.85s P10 │ 359.00 MiB/s │ 2.85s P50 │ 359.00 MiB/s │ 2.85s P90 │ 359.00 MiB/s │ 2.85s P100 │ 359.00 MiB/s │ 2.85s ``` ``` iroh (linux) Client 0 stats: Connect time: 3.83554ms Overall download stats: Transferred 1073741824 bytes on 1 streams in 1.16s (884.52 MiB/s) Time to first byte (TTFB): 2.014ms Total chunks: 38046 Average chunk time: 30.408ms Average chunk size: 27.55KiB Stream download metrics: │ Throughput │ Duration ──────┼───────────────┼────────── AVG │ 884.75 MiB/s │ 1.16s P0 │ 884.50 MiB/s │ 1.16s P10 │ 885.00 MiB/s │ 1.16s P50 │ 885.00 MiB/s │ 1.16s P90 │ 885.00 MiB/s │ 1.16s P100 │ 885.00 MiB/s │ 1.16s ``` ``` quinn (linux) Client 0 stats: Connect time: 1.22085ms Overall download stats: Transferred 1073741824 bytes on 1 streams in 994.57ms (1029.59 MiB/s) Time to first byte (TTFB): 1.483ms Total chunks: 32809 Average chunk time: 30.296ms Average chunk size: 31.96KiB Stream download metrics: │ Throughput │ Duration ──────┼───────────────┼────────── AVG │ 1030.50 MiB/s │ 993.00ms P0 │ 1030.00 MiB/s │ 993.00ms P10 │ 1031.00 MiB/s │ 993.00ms P50 │ 1031.00 MiB/s │ 993.00ms P90 │ 1031.00 MiB/s │ 993.00ms P100 │ 1031.00 MiB/s │ 993.00ms ``` ## Notes & open questions ## Change checklist - [ ] Self-review. - [ ] Documentation updates if relevant. - [ ] Tests if relevant. --- .github/workflows/ci.yml | 2 +- .github/workflows/tests.yaml | 2 +- Cargo.lock | 52 ++++++ iroh-net/bench/Cargo.toml | 4 + iroh-net/bench/src/bin/bulk.rs | 218 +++++++---------------- iroh-net/bench/src/iroh.rs | 232 ++++++++++++++++++++++++ iroh-net/bench/src/lib.rs | 312 +++++++++++++++++++-------------- iroh-net/bench/src/quinn.rs | 285 ++++++++++++++++++++++++++++++ iroh-net/bench/src/s2n.rs | 5 + iroh-net/bench/src/stats.rs | 41 ++++- 10 files changed, 861 insertions(+), 292 deletions(-) create mode 100644 iroh-net/bench/src/iroh.rs create mode 100644 iroh-net/bench/src/quinn.rs create mode 100644 iroh-net/bench/src/s2n.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fb3cf843e..0022321c76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,7 +115,7 @@ jobs: # uses: obi1kenobi/cargo-semver-checks-action@v2 uses: n0-computer/cargo-semver-checks-action@feat-baseline with: - package: iroh, iroh-base, iroh-blobs, iroh-cli, iroh-dns-server, iroh-gossip, iroh-metrics, iroh-net, iroh-docs + package: iroh, iroh-base, iroh-blobs, iroh-cli, iroh-dns-server, iroh-gossip, iroh-metrics, iroh-net, iroh-net-bench, iroh-docs baseline-rev: ${{ env.HEAD_COMMIT_SHA }} use-cache: false diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index badd64f42d..f82ecabe1f 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -23,7 +23,7 @@ env: RUSTFLAGS: -Dwarnings RUSTDOCFLAGS: -Dwarnings SCCACHE_CACHE_SIZE: "50G" - CRATES_LIST: "iroh,iroh-blobs,iroh-gossip,iroh-metrics,iroh-net,iroh-docs,iroh-test,iroh-cli,iroh-dns-server" + CRATES_LIST: "iroh,iroh-blobs,iroh-gossip,iroh-metrics,iroh-net,iroh-net-bench,iroh-docs,iroh-test,iroh-cli,iroh-dns-server" jobs: build_and_test_nix: diff --git a/Cargo.lock b/Cargo.lock index 3caa10894b..31aac2e02a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2850,6 +2850,10 @@ dependencies = [ "clap", "hdrhistogram", "iroh-net", + "quinn", + "rcgen 0.11.3", + "rustls 0.21.12", + "socket2", "tokio", "tracing", "tracing-subscriber", @@ -4064,6 +4068,54 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quinn" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cc2c5017e4b43d5995dcea317bc46c1e09404c0a9664d2908f7f02dfe943d75" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.21.12", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "141bf7dfde2fbc246bfd3fe12f2455aa24b0fbd9af535d8c86c7bd1381ff2b1a" +dependencies = [ + "bytes", + "rand", + "ring 0.16.20", + "rustc-hash", + "rustls 0.21.12", + "rustls-native-certs", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "055b4e778e8feb9f93c4e439f71dc2156ef13360b432b799e179a8c4cdf0b1d7" +dependencies = [ + "bytes", + "libc", + "socket2", + "tracing", + "windows-sys 0.48.0", +] + [[package]] name = "quote" version = "1.0.36" diff --git a/iroh-net/bench/Cargo.toml b/iroh-net/bench/Cargo.toml index 00f7067014..b5c7048e0a 100644 --- a/iroh-net/bench/Cargo.toml +++ b/iroh-net/bench/Cargo.toml @@ -10,7 +10,11 @@ anyhow = "1.0.22" bytes = "1" hdrhistogram = { version = "7.2", default-features = false } iroh-net = { path = ".." } +quinn = "0.10" +rcgen = "0.11.1" +rustls = { version = "0.21.0", default-features = false, features = ["quic"] } clap = { version = "4", features = ["derive"] } tokio = { version = "1.0.1", features = ["rt", "sync"] } tracing = "0.1" tracing-subscriber = { version = "0.3.0", default-features = false, features = ["env-filter", "fmt", "ansi", "time", "local-time"] } +socket2 = "0.5" diff --git a/iroh-net/bench/src/bin/bulk.rs b/iroh-net/bench/src/bin/bulk.rs index f5de15ea39..5c7e956249 100644 --- a/iroh-net/bench/src/bin/bulk.rs +++ b/iroh-net/bench/src/bin/bulk.rs @@ -1,38 +1,42 @@ -use std::{ - sync::{Arc, Mutex}, - time::Instant, -}; - -use anyhow::{Context, Result}; +use anyhow::Result; use clap::Parser; -use iroh_net::{ - endpoint::{self, Connection}, - Endpoint, NodeAddr, -}; -use tokio::sync::Semaphore; -use tracing::{info, trace}; - -use iroh_net_bench::{ - configure_tracing_subscriber, connect_client, drain_stream, rt, send_data_on_stream, - server_endpoint, - stats::{Stats, TransferResult}, - Opt, -}; + +use iroh_net_bench::{configure_tracing_subscriber, iroh, quinn, rt, s2n, Commands, Opt}; fn main() { - let opt = Opt::parse(); + let cmd = Commands::parse(); configure_tracing_subscriber(); + match cmd { + Commands::Iroh(opt) => { + if let Err(e) = run_iroh(opt) { + eprintln!("failed: {e:#}"); + } + } + Commands::Quinn(opt) => { + if let Err(e) = run_quinn(opt) { + eprintln!("failed: {e:#}"); + } + } + Commands::S2n(opt) => { + if let Err(e) = run_s2n(opt) { + eprintln!("failed: {e:#}"); + } + } + } +} + +pub fn run_iroh(opt: Opt) -> Result<()> { let server_span = tracing::error_span!("server"); let runtime = rt(); let (server_addr, endpoint) = { let _guard = server_span.enter(); - server_endpoint(&runtime, &opt) + iroh::server_endpoint(&runtime, &opt) }; let server_thread = std::thread::spawn(move || { let _guard = server_span.entered(); - if let Err(e) = runtime.block_on(server(endpoint, opt)) { + if let Err(e) = runtime.block_on(iroh::server(endpoint, opt)) { eprintln!("server failed: {e:#}"); } }); @@ -43,7 +47,7 @@ fn main() { handles.push(std::thread::spawn(move || { let _guard = tracing::error_span!("client", id).entered(); let runtime = rt(); - match runtime.block_on(client(server_addr, opt)) { + match runtime.block_on(iroh::client(server_addr, opt)) { Ok(stats) => Ok(stats), Err(e) => { eprintln!("client failed: {e:#}"); @@ -62,153 +66,53 @@ fn main() { } server_thread.join().expect("server thread"); -} - -async fn server(endpoint: Endpoint, opt: Opt) -> Result<()> { - let mut server_tasks = Vec::new(); - - // Handle only the expected amount of clients - for _ in 0..opt.clients { - let handshake = endpoint.accept().await.unwrap(); - let connection = handshake.await.context("handshake failed")?; - - server_tasks.push(tokio::spawn(async move { - loop { - let (mut send_stream, mut recv_stream) = match connection.accept_bi().await { - Err(endpoint::ConnectionError::ApplicationClosed(_)) => break, - Err(e) => { - eprintln!("accepting stream failed: {e:?}"); - break; - } - Ok(stream) => stream, - }; - trace!("stream established"); - - tokio::spawn(async move { - drain_stream(&mut recv_stream, opt.read_unordered).await?; - send_data_on_stream(&mut send_stream, opt.download_size).await?; - Ok::<_, anyhow::Error>(()) - }); - } - - if opt.stats { - println!("\nServer connection stats:\n{:#?}", connection.stats()); - } - })); - } - - // Await all the tasks. We have to do this to prevent the runtime getting dropped - // and all server tasks to be cancelled - for handle in server_tasks { - if let Err(e) = handle.await { - eprintln!("Server task error: {e:?}"); - }; - } Ok(()) } -async fn client(server_addr: NodeAddr, opt: Opt) -> Result { - let (endpoint, connection) = connect_client(server_addr, opt).await?; - - let start = Instant::now(); - - let connection = Arc::new(connection); - - let mut stats = ClientStats::default(); - let mut first_error = None; - - let sem = Arc::new(Semaphore::new(opt.max_streams)); - let results = Arc::new(Mutex::new(Vec::new())); - for _ in 0..opt.streams { - let permit = sem.clone().acquire_owned().await.unwrap(); - let results = results.clone(); - let connection = connection.clone(); - tokio::spawn(async move { - let result = - handle_client_stream(connection, opt.upload_size, opt.read_unordered).await; - info!("stream finished: {:?}", result); - results.lock().unwrap().push(result); - drop(permit); - }); - } +pub fn run_quinn(opt: Opt) -> Result<()> { + let server_span = tracing::error_span!("server"); + let runtime = rt(); + let (server_addr, endpoint) = { + let _guard = server_span.enter(); + quinn::server_endpoint(&runtime, &opt) + }; - // Wait for remaining streams to finish - let _ = sem.acquire_many(opt.max_streams as u32).await.unwrap(); + let server_thread = std::thread::spawn(move || { + let _guard = server_span.entered(); + if let Err(e) = runtime.block_on(quinn::server(endpoint, opt)) { + eprintln!("server failed: {e:#}"); + } + }); - for result in results.lock().unwrap().drain(..) { - match result { - Ok((upload_result, download_result)) => { - stats.upload_stats.stream_finished(upload_result); - stats.download_stats.stream_finished(download_result); - } - Err(e) => { - if first_error.is_none() { - first_error = Some(e); + let mut handles = Vec::new(); + for id in 0..opt.clients { + handles.push(std::thread::spawn(move || { + let _guard = tracing::error_span!("client", id).entered(); + let runtime = rt(); + match runtime.block_on(quinn::client(server_addr, opt)) { + Ok(stats) => Ok(stats), + Err(e) => { + eprintln!("client failed: {e:#}"); + Err(e) } } - } - } - - stats.upload_stats.total_duration = start.elapsed(); - stats.download_stats.total_duration = start.elapsed(); - - // Explicit close of the connection, since handles can still be around due - // to `Arc`ing them - connection.close(0u32.into(), b"Benchmark done"); - - endpoint.close(0u32.into(), b"").await?; - - if opt.stats { - println!("\nClient connection stats:\n{:#?}", connection.stats()); + })); } - match first_error { - None => Ok(stats), - Some(e) => Err(e), + for (id, handle) in handles.into_iter().enumerate() { + // We print all stats at the end of the test sequentially to avoid + // them being garbled due to being printed concurrently + if let Ok(stats) = handle.join().expect("client thread") { + stats.print(id); + } } -} - -async fn handle_client_stream( - connection: Arc, - upload_size: u64, - read_unordered: bool, -) -> Result<(TransferResult, TransferResult)> { - let start = Instant::now(); - - let (mut send_stream, mut recv_stream) = connection - .open_bi() - .await - .context("failed to open stream")?; - - send_data_on_stream(&mut send_stream, upload_size).await?; - - let upload_result = TransferResult::new(start.elapsed(), upload_size); - - let start = Instant::now(); - let size = drain_stream(&mut recv_stream, read_unordered).await?; - let download_result = TransferResult::new(start.elapsed(), size as u64); - Ok((upload_result, download_result)) -} + server_thread.join().expect("server thread"); -#[derive(Default)] -struct ClientStats { - upload_stats: Stats, - download_stats: Stats, + Ok(()) } -impl ClientStats { - pub fn print(&self, client_id: usize) { - println!(); - println!("Client {client_id} stats:"); - - if self.upload_stats.total_size != 0 { - self.upload_stats.print("upload"); - } - - if self.download_stats.total_size != 0 { - self.download_stats.print("download"); - } - } +pub fn run_s2n(_opt: s2n::Opt) -> Result<()> { + unimplemented!() } diff --git a/iroh-net/bench/src/iroh.rs b/iroh-net/bench/src/iroh.rs new file mode 100644 index 0000000000..a359be35b2 --- /dev/null +++ b/iroh-net/bench/src/iroh.rs @@ -0,0 +1,232 @@ +use std::{ + net::SocketAddr, + time::{Duration, Instant}, +}; + +use anyhow::{Context, Result}; +use bytes::Bytes; +use iroh_net::{ + endpoint::{Connection, ConnectionError, RecvStream, SendStream, TransportConfig}, + relay::RelayMode, + Endpoint, NodeAddr, +}; +use tracing::trace; + +use crate::{ + client_handler, stats::TransferResult, ClientStats, ConnectionSelector, EndpointSelector, Opt, +}; + +pub const ALPN: &[u8] = b"n0/iroh-net-bench/0"; + +/// Creates a server endpoint which runs on the given runtime +pub fn server_endpoint(rt: &tokio::runtime::Runtime, opt: &Opt) -> (NodeAddr, Endpoint) { + let _guard = rt.enter(); + rt.block_on(async move { + let ep = Endpoint::builder() + .alpns(vec![ALPN.to_vec()]) + .relay_mode(RelayMode::Disabled) + .transport_config(transport_config(opt.max_streams, opt.initial_mtu)) + .bind(0) + .await + .unwrap(); + let addr = ep.local_addr(); + let addr = SocketAddr::new("127.0.0.1".parse().unwrap(), addr.0.port()); + let addr = NodeAddr::new(ep.node_id()).with_direct_addresses([addr]); + (addr, ep) + }) +} + +/// Create and run a client +pub async fn client(server_addr: NodeAddr, opt: Opt) -> Result { + let client_start = std::time::Instant::now(); + let (endpoint, connection) = connect_client(server_addr, opt).await?; + let client_connect_time = client_start.elapsed(); + let mut res = client_handler( + EndpointSelector::Iroh(endpoint), + ConnectionSelector::Iroh(connection), + opt, + ) + .await?; + res.connect_time = client_connect_time; + Ok(res) +} + +/// Create a client endpoint and client connection +pub async fn connect_client(server_addr: NodeAddr, opt: Opt) -> Result<(Endpoint, Connection)> { + let endpoint = Endpoint::builder() + .alpns(vec![ALPN.to_vec()]) + .relay_mode(RelayMode::Disabled) + .transport_config(transport_config(opt.max_streams, opt.initial_mtu)) + .bind(0) + .await + .unwrap(); + + // TODO: We don't support passing client transport config currently + // let mut client_config = quinn::ClientConfig::new(Arc::new(crypto)); + // client_config.transport_config(Arc::new(transport_config(&opt))); + + let connection = endpoint + .connect(server_addr, ALPN) + .await + .context("unable to connect")?; + trace!("connected"); + + Ok((endpoint, connection)) +} + +pub fn transport_config(max_streams: usize, initial_mtu: u16) -> TransportConfig { + // High stream windows are chosen because the amount of concurrent streams + // is configurable as a parameter. + let mut config = TransportConfig::default(); + config.max_concurrent_uni_streams(max_streams.try_into().unwrap()); + config.initial_mtu(initial_mtu); + + // TODO: reenable when we upgrade quinn version + // let mut acks = quinn::AckFrequencyConfig::default(); + // acks.ack_eliciting_threshold(10u32.into()); + // config.ack_frequency_config(Some(acks)); + + config +} + +async fn drain_stream( + stream: &mut RecvStream, + read_unordered: bool, +) -> Result<(usize, Duration, u64)> { + let mut read = 0; + + let download_start = Instant::now(); + let mut first_byte = true; + let mut ttfb = download_start.elapsed(); + + let mut num_chunks: u64 = 0; + + if read_unordered { + while let Some(chunk) = stream.read_chunk(usize::MAX, false).await? { + if first_byte { + ttfb = download_start.elapsed(); + first_byte = false; + } + read += chunk.bytes.len(); + num_chunks += 1; + } + } else { + // These are 32 buffers, for reading approximately 32kB at once + #[rustfmt::skip] + let mut bufs = [ + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + ]; + + while let Some(n) = stream.read_chunks(&mut bufs[..]).await? { + if first_byte { + ttfb = download_start.elapsed(); + first_byte = false; + } + read += bufs.iter().take(n).map(|buf| buf.len()).sum::(); + num_chunks += 1; + } + } + + Ok((read, ttfb, num_chunks)) +} + +async fn send_data_on_stream(stream: &mut SendStream, stream_size: u64) -> Result<()> { + const DATA: &[u8] = &[0xAB; 1024 * 1024]; + let bytes_data = Bytes::from_static(DATA); + + let full_chunks = stream_size / (DATA.len() as u64); + let remaining = (stream_size % (DATA.len() as u64)) as usize; + + for _ in 0..full_chunks { + stream + .write_chunk(bytes_data.clone()) + .await + .context("failed sending data")?; + } + + if remaining != 0 { + stream + .write_chunk(bytes_data.slice(0..remaining)) + .await + .context("failed sending data")?; + } + + stream.finish().await.context("failed finishing stream")?; + + Ok(()) +} + +pub async fn handle_client_stream( + connection: &Connection, + upload_size: u64, + read_unordered: bool, +) -> Result<(TransferResult, TransferResult)> { + let start = Instant::now(); + + let (mut send_stream, mut recv_stream) = connection + .open_bi() + .await + .context("failed to open stream")?; + + send_data_on_stream(&mut send_stream, upload_size).await?; + + let upload_result = TransferResult::new(start.elapsed(), upload_size, Duration::default(), 0); + + let start = Instant::now(); + let (size, ttfb, num_chunks) = drain_stream(&mut recv_stream, read_unordered).await?; + let download_result = TransferResult::new(start.elapsed(), size as u64, ttfb, num_chunks); + + Ok((upload_result, download_result)) +} + +/// Take the provided endpoint and run the server +pub async fn server(endpoint: Endpoint, opt: Opt) -> Result<()> { + let mut server_tasks = Vec::new(); + + // Handle only the expected amount of clients + for _ in 0..opt.clients { + let handshake = endpoint.accept().await.unwrap(); + let connection = handshake.await.context("handshake failed")?; + + server_tasks.push(tokio::spawn(async move { + loop { + let (mut send_stream, mut recv_stream) = match connection.accept_bi().await { + Err(ConnectionError::ApplicationClosed(_)) => break, + Err(e) => { + eprintln!("accepting stream failed: {e:?}"); + break; + } + Ok(stream) => stream, + }; + trace!("stream established"); + + tokio::spawn(async move { + drain_stream(&mut recv_stream, opt.read_unordered).await?; + send_data_on_stream(&mut send_stream, opt.download_size).await?; + Ok::<_, anyhow::Error>(()) + }); + } + + if opt.stats { + println!("\nServer connection stats:\n{:#?}", connection.stats()); + } + })); + } + + // Await all the tasks. We have to do this to prevent the runtime getting dropped + // and all server tasks to be cancelled + for handle in server_tasks { + if let Err(e) = handle.await { + eprintln!("Server task error: {e:?}"); + }; + } + + Ok(()) +} diff --git a/iroh-net/bench/src/lib.rs b/iroh-net/bench/src/lib.rs index 25c049ce8c..b92665e77e 100644 --- a/iroh-net/bench/src/lib.rs +++ b/iroh-net/bench/src/lib.rs @@ -1,143 +1,32 @@ -use std::{net::SocketAddr, num::ParseIntError, str::FromStr}; - -use anyhow::{Context, Result}; -use bytes::Bytes; +use std::{ + num::ParseIntError, + str::FromStr, + sync::{Arc, Mutex}, + time::Instant, +}; + +use anyhow::Result; use clap::Parser; -use iroh_net::endpoint::{self, Connection, RecvStream, SendStream}; -use iroh_net::{relay::RelayMode, Endpoint, NodeAddr}; +use stats::Stats; use tokio::runtime::{Builder, Runtime}; -use tracing::trace; +use tokio::sync::Semaphore; +use tracing::info; +pub mod iroh; +pub mod quinn; +pub mod s2n; pub mod stats; -pub const ALPN: &[u8] = b"n0/iroh-net-bench/0"; - -pub fn configure_tracing_subscriber() { - tracing::subscriber::set_global_default( - tracing_subscriber::FmtSubscriber::builder() - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) - .finish(), - ) - .unwrap(); -} - -/// Creates a server endpoint which runs on the given runtime -pub fn server_endpoint(rt: &tokio::runtime::Runtime, opt: &Opt) -> (NodeAddr, Endpoint) { - let _guard = rt.enter(); - rt.block_on(async move { - let ep = Endpoint::builder() - .alpns(vec![ALPN.to_vec()]) - .relay_mode(RelayMode::Disabled) - .transport_config(transport_config(opt)) - .bind(0) - .await - .unwrap(); - let addr = ep.local_addr(); - let addr = SocketAddr::new("127.0.0.1".parse().unwrap(), addr.0.port()); - let addr = NodeAddr::new(ep.node_id()).with_direct_addresses([addr]); - (addr, ep) - }) -} - -/// Create a client endpoint and client connection -pub async fn connect_client(server_addr: NodeAddr, opt: Opt) -> Result<(Endpoint, Connection)> { - let endpoint = Endpoint::builder() - .alpns(vec![ALPN.to_vec()]) - .relay_mode(RelayMode::Disabled) - .transport_config(transport_config(&opt)) - .bind(0) - .await - .unwrap(); - - // TODO: We don't support passing client transport config currently - // let mut client_config = quinn::ClientConfig::new(Arc::new(crypto)); - // client_config.transport_config(Arc::new(transport_config(&opt))); - - let connection = endpoint - .connect(server_addr, ALPN) - .await - .context("unable to connect")?; - trace!("connected"); - - Ok((endpoint, connection)) -} - -pub async fn drain_stream(stream: &mut RecvStream, read_unordered: bool) -> Result { - let mut read = 0; - - if read_unordered { - while let Some(chunk) = stream.read_chunk(usize::MAX, false).await? { - read += chunk.bytes.len(); - } - } else { - // These are 32 buffers, for reading approximately 32kB at once - #[rustfmt::skip] - let mut bufs = [ - Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), - Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), - Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), - Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), - Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), - Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), - Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), - Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), - ]; - - while let Some(n) = stream.read_chunks(&mut bufs[..]).await? { - read += bufs.iter().take(n).map(|buf| buf.len()).sum::(); - } - } - - Ok(read) -} - -pub async fn send_data_on_stream(stream: &mut SendStream, stream_size: u64) -> Result<()> { - const DATA: &[u8] = &[0xAB; 1024 * 1024]; - let bytes_data = Bytes::from_static(DATA); - - let full_chunks = stream_size / (DATA.len() as u64); - let remaining = (stream_size % (DATA.len() as u64)) as usize; - - for _ in 0..full_chunks { - stream - .write_chunk(bytes_data.clone()) - .await - .context("failed sending data")?; - } - - if remaining != 0 { - stream - .write_chunk(bytes_data.slice(0..remaining)) - .await - .context("failed sending data")?; - } - - stream.finish().await.context("failed finishing stream")?; - - Ok(()) -} - -pub fn rt() -> Runtime { - Builder::new_current_thread().enable_all().build().unwrap() -} - -pub fn transport_config(opt: &Opt) -> endpoint::TransportConfig { - // High stream windows are chosen because the amount of concurrent streams - // is configurable as a parameter. - let mut config = endpoint::TransportConfig::default(); - config.max_concurrent_uni_streams(opt.max_streams.try_into().unwrap()); - config.initial_mtu(opt.initial_mtu); - - // TODO: reenable when we upgrade quinn version - // let mut acks = quinn::AckFrequencyConfig::default(); - // acks.ack_eliciting_threshold(10u32.into()); - // config.ack_frequency_config(Some(acks)); - - config +#[derive(Parser, Debug, Clone, Copy)] +#[clap(name = "bulk")] +pub enum Commands { + Iroh(Opt), + Quinn(Opt), + S2n(s2n::Opt), } #[derive(Parser, Debug, Clone, Copy)] -#[clap(name = "bulk")] +#[clap(name = "options")] pub struct Opt { /// The total number of clients which should be created #[clap(long = "clients", short = 'c', default_value = "1")] @@ -171,6 +60,67 @@ pub struct Opt { pub initial_mtu: u16, } +pub enum EndpointSelector { + Iroh(iroh_net::Endpoint), + Quinn(::quinn::Endpoint), +} + +impl EndpointSelector { + pub async fn close(self) -> Result<()> { + match self { + EndpointSelector::Iroh(endpoint) => { + endpoint.close(0u32.into(), b"").await?; + } + EndpointSelector::Quinn(endpoint) => { + endpoint.close(0u32.into(), b""); + } + } + Ok(()) + } +} + +pub enum ConnectionSelector { + Iroh(iroh_net::endpoint::Connection), + Quinn(::quinn::Connection), +} + +impl ConnectionSelector { + pub fn stats(&self) { + match self { + ConnectionSelector::Iroh(connection) => { + println!("{:#?}", connection.stats()); + } + ConnectionSelector::Quinn(connection) => { + println!("{:#?}", connection.stats()); + } + } + } + + pub fn close(&self, error_code: u32, reason: &[u8]) { + match self { + ConnectionSelector::Iroh(connection) => { + connection.close(error_code.into(), reason); + } + ConnectionSelector::Quinn(connection) => { + connection.close(error_code.into(), reason); + } + } + } +} + +pub fn configure_tracing_subscriber() { + tracing::subscriber::set_global_default( + tracing_subscriber::FmtSubscriber::builder() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .finish(), + ) + .unwrap(); +} + +pub fn rt() -> Runtime { + Builder::new_current_thread().enable_all().build().unwrap() +} + fn parse_byte_size(s: &str) -> Result { let s = s.trim(); @@ -192,3 +142,101 @@ fn parse_byte_size(s: &str) -> Result { Ok(base * multiplier) } + +#[derive(Default)] +pub struct ClientStats { + upload_stats: Stats, + download_stats: Stats, + connect_time: std::time::Duration, +} + +impl ClientStats { + pub fn print(&self, client_id: usize) { + println!(); + println!("Client {client_id} stats:"); + + let ct = self.connect_time.as_nanos() as f64 / 1_000_000.0; + println!("Connect time: {ct}ms"); + + if self.upload_stats.total_size != 0 { + self.upload_stats.print("upload"); + } + + if self.download_stats.total_size != 0 { + self.download_stats.print("download"); + } + } +} + +/// Take the provided endpoint and run the client benchmark +pub async fn client_handler( + endpoint: EndpointSelector, + connection: ConnectionSelector, + opt: Opt, +) -> Result { + let start = Instant::now(); + + let connection = Arc::new(connection); + + let mut stats = ClientStats::default(); + let mut first_error = None; + + let sem = Arc::new(Semaphore::new(opt.max_streams)); + let results = Arc::new(Mutex::new(Vec::new())); + for _ in 0..opt.streams { + let permit = sem.clone().acquire_owned().await.unwrap(); + let results = results.clone(); + let connection = connection.clone(); + tokio::spawn(async move { + let result = match &*connection { + ConnectionSelector::Iroh(connection) => { + iroh::handle_client_stream(connection, opt.upload_size, opt.read_unordered) + .await + } + ConnectionSelector::Quinn(connection) => { + quinn::handle_client_stream(connection, opt.upload_size, opt.read_unordered) + .await + } + }; + // handle_client_stream(connection, opt.upload_size, opt.read_unordered).await; + info!("stream finished: {:?}", result); + results.lock().unwrap().push(result); + drop(permit); + }); + } + + // Wait for remaining streams to finish + let _ = sem.acquire_many(opt.max_streams as u32).await.unwrap(); + + stats.upload_stats.total_duration = start.elapsed(); + stats.download_stats.total_duration = start.elapsed(); + + for result in results.lock().unwrap().drain(..) { + match result { + Ok((upload_result, download_result)) => { + stats.upload_stats.stream_finished(upload_result); + stats.download_stats.stream_finished(download_result); + } + Err(e) => { + if first_error.is_none() { + first_error = Some(e); + } + } + } + } + + // Explicit close of the connection, since handles can still be around due + // to `Arc`ing them + connection.close(0u32, b"Benchmark done"); + + endpoint.close().await?; + + if opt.stats { + println!("\nClient connection stats:\n{:#?}", connection.stats()); + } + + match first_error { + None => Ok(stats), + Some(e) => Err(e), + } +} diff --git a/iroh-net/bench/src/quinn.rs b/iroh-net/bench/src/quinn.rs new file mode 100644 index 0000000000..b2c5e1c07b --- /dev/null +++ b/iroh-net/bench/src/quinn.rs @@ -0,0 +1,285 @@ +use std::{ + net::SocketAddr, + sync::Arc, + time::{Duration, Instant}, +}; + +use anyhow::{Context, Result}; +use bytes::Bytes; +use quinn::{Connection, Endpoint, RecvStream, SendStream, TokioRuntime, TransportConfig}; +use socket2::{Domain, Protocol, Socket, Type}; +use tracing::{trace, warn}; + +use crate::{ + client_handler, stats::TransferResult, ClientStats, ConnectionSelector, EndpointSelector, Opt, +}; + +/// Derived from the iroh-net udp SOCKET_BUFFER_SIZE +const SOCKET_BUFFER_SIZE: usize = 7 << 20; +pub const ALPN: &[u8] = b"n0/quinn-bench/0"; + +/// Creates a server endpoint which runs on the given runtime +pub fn server_endpoint(rt: &tokio::runtime::Runtime, opt: &Opt) -> (SocketAddr, quinn::Endpoint) { + let secret_key = iroh_net::key::SecretKey::generate(); + let crypto = + iroh_net::tls::make_server_config(&secret_key, vec![ALPN.to_vec()], false).unwrap(); + + let transport = transport_config(opt.max_streams, opt.initial_mtu); + + let mut server_config = quinn::ServerConfig::with_crypto(Arc::new(crypto)); + server_config.transport_config(Arc::new(transport)); + + let addr = SocketAddr::new("127.0.0.1".parse().unwrap(), 0); + + let socket = bind_socket(addr).unwrap(); + + let _guard = rt.enter(); + rt.block_on(async move { + let ep = quinn::Endpoint::new( + Default::default(), + Some(server_config), + socket, + Arc::new(TokioRuntime), + ) + .unwrap(); + let addr = ep.local_addr().unwrap(); + (addr, ep) + }) +} + +/// Create and run a client +pub async fn client(server_addr: SocketAddr, opt: Opt) -> Result { + let client_start = std::time::Instant::now(); + let (endpoint, connection) = connect_client(server_addr, opt).await?; + let client_connect_time = client_start.elapsed(); + let mut res = client_handler( + EndpointSelector::Quinn(endpoint), + ConnectionSelector::Quinn(connection), + opt, + ) + .await?; + res.connect_time = client_connect_time; + Ok(res) +} + +/// Create a client endpoint and client connection +pub async fn connect_client( + server_addr: SocketAddr, + opt: Opt, +) -> Result<(::quinn::Endpoint, Connection)> { + let secret_key = iroh_net::key::SecretKey::generate(); + let tls_client_config = + iroh_net::tls::make_client_config(&secret_key, None, vec![ALPN.to_vec()], false)?; + let mut config = quinn::ClientConfig::new(Arc::new(tls_client_config)); + + let transport = transport_config(opt.max_streams, opt.initial_mtu); + + // let mut config = quinn::ClientConfig::new(Arc::new(crypto)); + config.transport_config(Arc::new(transport)); + + let addr = SocketAddr::new("127.0.0.1".parse().unwrap(), 0); + + let socket = bind_socket(addr).unwrap(); + + let ep = + quinn::Endpoint::new(Default::default(), None, socket, Arc::new(TokioRuntime)).unwrap(); + let connection = ep + .connect_with(config, server_addr, "local")? + .await + .context("connecting")?; + Ok((ep, connection)) +} + +pub fn transport_config(max_streams: usize, initial_mtu: u16) -> TransportConfig { + // High stream windows are chosen because the amount of concurrent streams + // is configurable as a parameter. + let mut config = TransportConfig::default(); + config.max_concurrent_uni_streams(max_streams.try_into().unwrap()); + config.initial_mtu(initial_mtu); + + // TODO: reenable when we upgrade quinn version + // let mut acks = quinn::AckFrequencyConfig::default(); + // acks.ack_eliciting_threshold(10u32.into()); + // config.ack_frequency_config(Some(acks)); + + config +} + +fn bind_socket(addr: SocketAddr) -> Result { + let socket = Socket::new(Domain::for_address(addr), Type::DGRAM, Some(Protocol::UDP)) + .context("create socket")?; + + if addr.is_ipv6() { + socket.set_only_v6(false).context("set_only_v6")?; + } + + socket + .bind(&socket2::SockAddr::from(addr)) + .context("binding endpoint")?; + socket + .set_send_buffer_size(SOCKET_BUFFER_SIZE) + .context("send buffer size")?; + socket + .set_recv_buffer_size(SOCKET_BUFFER_SIZE) + .context("recv buffer size")?; + + let buf_size = socket.send_buffer_size().context("send buffer size")?; + if buf_size < SOCKET_BUFFER_SIZE { + warn!( + "Unable to set desired send buffer size. Desired: {}, Actual: {}", + SOCKET_BUFFER_SIZE, buf_size + ); + } + + let buf_size = socket.recv_buffer_size().context("recv buffer size")?; + if buf_size < SOCKET_BUFFER_SIZE { + warn!( + "Unable to set desired recv buffer size. Desired: {}, Actual: {}", + SOCKET_BUFFER_SIZE, buf_size + ); + } + + Ok(socket.into()) +} + +async fn drain_stream( + stream: &mut RecvStream, + read_unordered: bool, +) -> Result<(usize, Duration, u64)> { + let mut read = 0; + + let download_start = Instant::now(); + let mut first_byte = true; + let mut ttfb = download_start.elapsed(); + + let mut num_chunks: u64 = 0; + + if read_unordered { + while let Some(chunk) = stream.read_chunk(usize::MAX, false).await? { + if first_byte { + ttfb = download_start.elapsed(); + first_byte = false; + } + read += chunk.bytes.len(); + num_chunks += 1; + } + } else { + // These are 32 buffers, for reading approximately 32kB at once + #[rustfmt::skip] + let mut bufs = [ + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + Bytes::new(), Bytes::new(), Bytes::new(), Bytes::new(), + ]; + + while let Some(n) = stream.read_chunks(&mut bufs[..]).await? { + if first_byte { + ttfb = download_start.elapsed(); + first_byte = false; + } + read += bufs.iter().take(n).map(|buf| buf.len()).sum::(); + num_chunks += 1; + } + } + + Ok((read, ttfb, num_chunks)) +} + +async fn send_data_on_stream(stream: &mut SendStream, stream_size: u64) -> Result<()> { + const DATA: &[u8] = &[0xAB; 1024 * 1024]; + let bytes_data = Bytes::from_static(DATA); + + let full_chunks = stream_size / (DATA.len() as u64); + let remaining = (stream_size % (DATA.len() as u64)) as usize; + + for _ in 0..full_chunks { + stream + .write_chunk(bytes_data.clone()) + .await + .context("failed sending data")?; + } + + if remaining != 0 { + stream + .write_chunk(bytes_data.slice(0..remaining)) + .await + .context("failed sending data")?; + } + + stream.finish().await.context("failed finishing stream")?; + + Ok(()) +} + +pub async fn handle_client_stream( + connection: &Connection, + upload_size: u64, + read_unordered: bool, +) -> Result<(TransferResult, TransferResult)> { + let start = Instant::now(); + + let (mut send_stream, mut recv_stream) = connection + .open_bi() + .await + .context("failed to open stream")?; + + send_data_on_stream(&mut send_stream, upload_size).await?; + + let upload_result = TransferResult::new(start.elapsed(), upload_size, Duration::default(), 0); + + let start = Instant::now(); + let (size, ttfb, num_chunks) = drain_stream(&mut recv_stream, read_unordered).await?; + let download_result = TransferResult::new(start.elapsed(), size as u64, ttfb, num_chunks); + + Ok((upload_result, download_result)) +} + +/// Take the provided endpoint and run the server +pub async fn server(endpoint: Endpoint, opt: Opt) -> Result<()> { + let mut server_tasks = Vec::new(); + + // Handle only the expected amount of clients + for _ in 0..opt.clients { + let handshake = endpoint.accept().await.unwrap(); + let connection = handshake.await.context("handshake failed")?; + + server_tasks.push(tokio::spawn(async move { + loop { + let (mut send_stream, mut recv_stream) = match connection.accept_bi().await { + Err(::quinn::ConnectionError::ApplicationClosed(_)) => break, + Err(e) => { + eprintln!("accepting stream failed: {e:?}"); + break; + } + Ok(stream) => stream, + }; + trace!("stream established"); + + tokio::spawn(async move { + drain_stream(&mut recv_stream, opt.read_unordered).await?; + send_data_on_stream(&mut send_stream, opt.download_size).await?; + Ok::<_, anyhow::Error>(()) + }); + } + + if opt.stats { + println!("\nServer connection stats:\n{:#?}", connection.stats()); + } + })); + } + + // Await all the tasks. We have to do this to prevent the runtime getting dropped + // and all server tasks to be cancelled + for handle in server_tasks { + if let Err(e) = handle.await { + eprintln!("Server task error: {e:?}"); + }; + } + + Ok(()) +} diff --git a/iroh-net/bench/src/s2n.rs b/iroh-net/bench/src/s2n.rs new file mode 100644 index 0000000000..cd5ddb5ed5 --- /dev/null +++ b/iroh-net/bench/src/s2n.rs @@ -0,0 +1,5 @@ +use clap::Parser; + +#[derive(Parser, Debug, Clone, Copy)] +#[clap(name = "s2n")] +pub struct Opt {} diff --git a/iroh-net/bench/src/stats.rs b/iroh-net/bench/src/stats.rs index 6bc4856dca..17816523f3 100644 --- a/iroh-net/bench/src/stats.rs +++ b/iroh-net/bench/src/stats.rs @@ -23,6 +23,21 @@ impl Stats { .throughput_hist .record(stream_result.throughput as u64) .unwrap(); + self.stream_stats + .ttfb_hist + .record(stream_result.ttfb.as_nanos() as u64) + .unwrap(); + self.stream_stats + .chunk_time + .record( + stream_result.duration.as_nanos() as u64 / std::cmp::max(stream_result.chunks, 1), + ) + .unwrap(); + self.stream_stats.chunks += stream_result.chunks; + self.stream_stats + .chunk_size + .record(stream_result.avg_chunk_size) + .unwrap(); } pub fn print(&self, stat_name: &str) { @@ -35,6 +50,16 @@ impl Stats { throughput_bps(self.total_duration, self.total_size) / 1024.0 / 1024.0 ); + let avg_ttfb = self.stream_stats.ttfb_hist.mean() / 1_000.0; + println!("Time to first byte (TTFB): {avg_ttfb}ms\n"); + + let chunks = self.stream_stats.chunks; + println!("Total chunks: {chunks}\n"); + let avg_chunk_time = self.stream_stats.chunk_time.mean() / 1_000.0; + println!("Average chunk time: {avg_chunk_time}ms\n"); + let avg_chunk_size = self.stream_stats.chunk_size.mean() / 1024.0; + println!("Average chunk size: {avg_chunk_size:.2}KiB\n"); + println!("Stream {stat_name} metrics:\n"); println!(" │ Throughput │ Duration "); @@ -62,6 +87,10 @@ impl Stats { pub struct StreamStats { pub duration_hist: Histogram, pub throughput_hist: Histogram, + pub ttfb_hist: Histogram, + pub chunk_time: Histogram, + pub chunks: u64, + pub chunk_size: Histogram, } impl Default for StreamStats { @@ -69,6 +98,10 @@ impl Default for StreamStats { Self { duration_hist: Histogram::::new(3).unwrap(), throughput_hist: Histogram::::new(3).unwrap(), + ttfb_hist: Histogram::::new(3).unwrap(), + chunk_time: Histogram::::new(3).unwrap(), + chunks: 0, + chunk_size: Histogram::::new(3).unwrap(), } } } @@ -78,15 +111,21 @@ pub struct TransferResult { pub duration: Duration, pub size: u64, pub throughput: f64, + pub ttfb: Duration, + pub chunks: u64, + pub avg_chunk_size: u64, } impl TransferResult { - pub fn new(duration: Duration, size: u64) -> Self { + pub fn new(duration: Duration, size: u64, ttfb: Duration, chunks: u64) -> Self { let throughput = throughput_bps(duration, size); TransferResult { duration, size, throughput, + ttfb, + chunks, + avg_chunk_size: size / std::cmp::max(chunks, 1), } } } From d635d93ace4b1375c7dfeb194b5ee8e4651c810c Mon Sep 17 00:00:00 2001 From: Asmir Avdicevic Date: Wed, 22 May 2024 22:52:46 +0200 Subject: [PATCH 7/7] feat(cli): add metrics server to iroh doctor (#2292) ## Description Folks try to look at metrics while doctor alone is running (note this clashes by default with the iroh node if both are running locally on port :9090). Also kind of inconvenient given if you want to look at a running iroh node when doctor is running. Suggestions welcome, maybe default off metrics on `doctor`? Also most metrics are not really used in the `doctor` path, but we can fix that as we figure out what's cool to measure. ## Breaking Changes ## Notes & open questions ## Change checklist - [x] Self-review. - [ ] Documentation updates if relevant. - [ ] Tests if relevant. - [ ] All breaking changes documented. --- iroh-cli/src/commands.rs | 10 ++++++++-- iroh-cli/src/commands/doctor.rs | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/iroh-cli/src/commands.rs b/iroh-cli/src/commands.rs index 9fd279be12..2e53bd3e0e 100644 --- a/iroh-cli/src/commands.rs +++ b/iroh-cli/src/commands.rs @@ -37,7 +37,7 @@ pub(crate) struct Cli { #[clap(long, global = true)] start: bool, - /// Port to serve metrics on. -1 to disable. + /// Port to serve metrics on. Disabled by default. #[clap(long)] pub(crate) metrics_port: Option, } @@ -184,7 +184,13 @@ impl Cli { .await } Commands::Doctor { command } => { - let config = NodeConfig::load(self.config.as_deref()).await?; + let mut config = NodeConfig::load(self.config.as_deref()).await?; + if let Some(metrics_port) = self.metrics_port { + config.metrics_addr = match metrics_port { + MetricsPort::Disabled => None, + MetricsPort::Port(port) => Some(([127, 0, 0, 1], port).into()), + }; + } self::doctor::run(command, &config).await } } diff --git a/iroh-cli/src/commands/doctor.rs b/iroh-cli/src/commands/doctor.rs index 01445e2d33..156bb4dd9d 100644 --- a/iroh-cli/src/commands/doctor.rs +++ b/iroh-cli/src/commands/doctor.rs @@ -1068,7 +1068,8 @@ fn inspect_ticket(ticket: &str, zbase32: bool) -> anyhow::Result<()> { pub async fn run(command: Commands, config: &NodeConfig) -> anyhow::Result<()> { let data_dir = iroh_data_root()?; let _guard = crate::logging::init_terminal_and_file_logging(&config.file_logs, &data_dir)?; - match command { + let metrics_fut = super::start::start_metrics_server(config.metrics_addr); + let cmd_res = match command { Commands::Report { stun_host, stun_port, @@ -1200,7 +1201,11 @@ pub async fn run(command: Commands, config: &NodeConfig) -> anyhow::Result<()> { Ok(()) } + }; + if let Some(metrics_fut) = metrics_fut { + metrics_fut.abort(); } + cmd_res } async fn run_plotter(