//! Transfer data between two endpoints and print various stats and metrics. //! //! This example implements a transfer protocol to upload or download data between two iroh endpoints //! with a time or size limit. After the transfer finishes, statistics about the transfer and the used //! network paths are printed. //! //! It is not the typical "simple" example, yet it may be interesting to read because it uses most of //! iroh's endpoint builder options. We use it for manual testing before release, and it also runs as //! part of our CI infrastructure. //! //! You can use this example to easily test iroh connectivity between devices. Usage is straightforward: //! //! ```sh //! # Run in release mode and with all features and print available commands and options: //! cargo run --example transfer --release --all-features -- help //! //! # Run a provider endpoint on a device //! cargo run --example transfer --release --all-features -- provide //! //! # And connect to the provider endpoint from another device //! cargo run --example transfer --release --all-features -- fetch PROVIDER_ENDPOINT_ID //! ``` use std::{ fmt, fs::File, net::{SocketAddr, SocketAddrV4, SocketAddrV6}, path::{Path, PathBuf}, str::FromStr, time::{Duration, Instant}, }; use bytes::Bytes; use chrono::Local; use clap::{Parser, Subcommand, ValueEnum}; use console::Style; use data_encoding::HEXLOWER; use derive_more::{Display, From}; use indicatif::HumanBytes; use ipnet::{Ipv4Net, Ipv6Net}; use iroh::{ Endpoint, EndpointAddr, EndpointId, RelayMap, RelayMode, RelayUrl, SecretKey, TransportAddr, address_lookup::{ AddrFilter, dns::DnsAddressLookup, pkarr::{N0_DNS_PKARR_RELAY_PROD, N0_DNS_PKARR_RELAY_STAGING, PkarrPublisher}, }, dns::{DnsResolver, N0_DNS_ENDPOINT_ORIGIN_PROD, N0_DNS_ENDPOINT_ORIGIN_STAGING}, endpoint::{ BindOpts, Connection, ConnectionError, PathEvent, PathId, QuicTransportConfig, RecvStream, SendStream, VarInt, WriteError, presets, }, }; use n0_error::{Result, StackResultExt, StdResultExt, anyerr, ensure_any}; use n0_future::StreamExt; use postcard::experimental::max_size::MaxSize; use serde::{Deserialize, Serialize, Serializer}; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, task::JoinHandle, time::timeout, }; use tracing::{Instrument, Span, debug, error, info, info_span, instrument, warn}; use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt}; use url::Url; /// ALPN of our transport protocol. const TRANSFER_ALPN: &[u8] = b"n0/iroh/transfer/example/1"; const DEV_RELAY_URL: &str = "http://localhost:3340"; const DEV_PKARR_RELAY_URL: &str = "http://localhost:8080/pkarr"; const DEV_DNS_ORIGIN_DOMAIN: &str = "irohdns.example"; const DEV_DNS_SERVER: &str = "127.0.0.1:5300"; /// Connection error code for a gracefully closed connection. const GRACEFUL_CLOSE: VarInt = VarInt::from_u32(1); /// Transfer data between iroh endpoints. /// /// This is a useful example to test connection establishment and transfer speed. /// /// Note that some options are only available with optional features: /// /// --relay-only needs the `test-utils` feature /// /// --dev needs the `test-utils` feature /// /// --mdns needs the `mdns` feature /// /// To emit qlog files, enable the `qlog` feature and set the QLOGDIR /// environment variable to the path where qlog files should be written to. /// /// To enable all features, run the example with --all-features: /// /// cargo run --release --example transfer --all-features -- ARGS #[derive(Parser, Debug)] #[command(name = "transfer")] struct Cli { /// Output format. #[clap(global = true, long, value_enum, default_value_t)] output: OutputMode, #[clap(flatten)] log: LogArgs, #[command(subcommand)] command: Commands, } #[derive(Parser, Debug)] struct LogArgs { /// Save trace and qlog logs to ./logs/ #[clap(global = true, long, conflicts_with = "logs_path")] logs: bool, /// Save trace and qlog logs the specified path #[clap(global = true, long, conflicts_with = "logs")] logs_path: Option, /// Generate qlog logs. Needs --logs or --logs_path. /// /// Alternatively, set the QLOGDIR environment variable. /// /// Has no effect if the `qlog` feature is not enabled. #[clap(global = true, long)] qlog: bool, } impl LogArgs { fn init(self, command: &Commands, id: EndpointId) -> Result> { let dir = match (self.logs_path, self.logs) { (Some(path), _) => Some(path), (_, true) => Some(PathBuf::from(format!( "./logs/transfer-{command}-{}-{}", Local::now().format("%y%m%d.%H%M%S"), id.fmt_short() ))), _ => None, }; if let Some(dir) = dir { std::fs::create_dir_all(&dir) .with_context(|_| format!("failed to create log directory at {}", dir.display()))?; let tracing_file = dir.join(format!("logs-{command}")); init_tracing(Some(&tracing_file)); Ok(Some(LogSettings { dir, #[cfg(feature = "qlog")] qlog: self.qlog, })) } else { init_tracing(None); Ok(None) } } } struct LogSettings { dir: PathBuf, #[cfg(feature = "qlog")] qlog: bool, } #[derive(Clone, Copy, Default, Debug, Eq, PartialEq, clap::ValueEnum, Serialize)] enum Env { /// Use the production servers hosted by number0. Prod, /// Use the staging servers hosted by number0. #[default] Staging, /// Use localhost servers. /// /// To run the DNS server: /// cargo run --bin iroh-dns-server /// To run the relay server: /// cargo run --bin iroh-relay --features server -- --dev Dev, } impl Env { fn relay_mode(self) -> RelayMode { match self { Env::Prod => RelayMode::Default, Env::Staging => RelayMode::Staging, Env::Dev => RelayMode::Custom(RelayMap::from( RelayUrl::from_str(DEV_RELAY_URL).expect("valid url"), )), } } fn pkarr_relay_url(self) -> Url { match self { Env::Prod => N0_DNS_PKARR_RELAY_PROD.parse(), Env::Staging => N0_DNS_PKARR_RELAY_STAGING.parse(), Env::Dev => DEV_PKARR_RELAY_URL.parse(), } .expect("valid url") } fn dns_origin_domain(self) -> String { match self { Env::Prod => N0_DNS_ENDPOINT_ORIGIN_PROD.to_string(), Env::Staging => N0_DNS_ENDPOINT_ORIGIN_STAGING.to_string(), Env::Dev => DEV_DNS_ORIGIN_DOMAIN.to_string(), } } } #[derive(Serialize, Deserialize, ValueEnum, Default, Debug, Clone, Copy)] enum Mode { /// We send data to the remote, measuring our upload speed. Upload, /// We receive data from the remote, measuring our download speed. #[default] Download, /// We send and receive data in parallel. Bidi, /// We keep the connection open without sending data. Ping, } #[derive(Serialize, Deserialize, MaxSize, derive_more::Debug, Clone, Copy)] enum Length { #[debug("Size({})", HumanBytes(*_0))] Size(u64), #[debug("Duration({_0:?})")] Duration(#[serde(with = "duration_micros")] Duration), } impl Length { fn remaining(&self, start: Instant, size: usize) -> (Duration, usize) { match self { Length::Duration(limit) => (limit.saturating_sub(start.elapsed()), usize::MAX), Length::Size(limit) => (Duration::MAX, (*limit as usize).saturating_sub(size)), } } } #[derive(Debug, Serialize, Clone, Copy)] enum RequestKind { Upload, Download, } #[derive(Serialize, Deserialize, MaxSize, Debug, Clone)] enum Request { Download(Length), Upload, } impl Request { async fn read(recv: &mut RecvStream) -> Result { let header_len = recv.read_u32().await.anyerr()? as usize; ensure_any!( header_len <= Self::POSTCARD_MAX_SIZE, "received invalid header length" ); let mut buf = vec![0u8; header_len]; recv.read_exact(&mut buf).await.anyerr()?; let request = postcard::from_bytes(&buf).std_context("failed to decode request")?; debug!("received request {request:?}"); Ok(request) } async fn write(&self, send: &mut SendStream) -> Result<()> { debug!("sending request {self:?}"); let buf = postcard::to_stdvec(&self).unwrap(); send.write_u32(buf.len() as u32).await.anyerr()?; send.write_all(&buf).await.anyerr()?; Ok(()) } } #[derive(Debug, Clone, clap::Parser, Serialize)] #[serde(tag = "kind")] struct EndpointArgs { /// Set the environment for relay, pkarr, and DNS servers. /// /// If other options are set, those will override the environment defaults. #[clap(short, long, value_enum, default_value_t)] env: Env, /// Set one or more relay servers to use. #[clap(long)] relay_url: Vec, /// Authorization token sent to each `--relay-url`. /// /// Has no effect unless `--relay-url` is also set. #[clap(long)] relay_auth_token: Option, /// Disable relays completely. #[clap(long, conflicts_with = "relay_url")] no_relay: bool, /// Disable Address Lookup completely. #[clap(long, conflicts_with_all = ["pkarr_relay_url", "no_pkarr_publish", "dns_origin_domain", "no_dns_resolve"])] no_address_lookup: bool, /// If set no direct connections will be established. #[clap(long)] relay_only: bool, /// Use a custom pkarr server. #[clap(long)] pkarr_relay_url: Option, /// Disable publishing endpoint info to pkarr. #[clap(long, conflicts_with = "pkarr_relay_url")] no_pkarr_publish: bool, /// Use a custom domain when resolving endpoint info via DNS. #[clap(long)] dns_origin_domain: Option, /// Use a custom DNS server for resolving relay and endpoint info domains. #[clap(long)] dns_server: Option, /// Do not resolve endpoint info via DNS. #[clap(long)] no_dns_resolve: bool, #[clap(long)] /// Enable mDNS Address Lookup. mdns: bool, /// Set the default IPv4 bind address. #[clap(long)] bind_addr_v4: Option, /// Set additional IPv4 bind addresses. /// /// Syntax is "addr/mask:port", so e.g. "10.0.0.1/16:1234". /// The mask is used to define for which destinations this bind address is used. #[clap(long)] bind_addr_v4_additional: Vec, /// Set the default IPv6 bind address. #[clap(long)] bind_addr_v6: Option, /// Set additional IPv6 bind addresses. /// /// Syntax is "addr/mask:port", so e.g. "2001:db8::1/16:1234". /// The mask is used to define for which destinations this bind address is used. #[clap(long)] bind_addr_v6_additional: Vec, /// Disable all default bind addresses. #[clap(long)] no_default_bind: bool, /// Receive window size. /// /// This controls the maximum amount of data that may be inflight for any stream in the connection. /// Increasing this value gives higher throughput at high latencies, at the cost of /// more memory usage. /// /// The receive window is usually calculated as the product of desired throughput /// and expected maximum roundtrip latency (RTT), i.e. for a throughput of 100 MBit/s /// and a RTT of 200ms, you would set the receive window to (100 Mbit/s / 8) * 0.2s = 2.5M. /// /// The default is 1.25 MB, which gives a max throughput of 100 MBit/s at 100ms RTT. /// /// Accepts values like "5M", "2000K", etc. #[clap(long, value_parser = parse_byte_size)] receive_window: Option, } #[derive(Subcommand, Debug, derive_more::Display)] enum Commands { /// Provide data. #[display("provide")] Provide { #[clap(flatten)] endpoint_args: EndpointArgs, }, /// Fetch data. #[display("fetch")] Fetch { /// Endpoint id of the remote to connect to. remote_id: EndpointId, /// Transfer mode. #[clap(long, value_enum, default_value_t)] mode: Mode, /// Limit the transferred data size. #[clap(long, value_parser = parse_byte_size, conflicts_with = "duration")] size: Option, /// Limit the duration of the transfer, in seconds. /// /// [default: 10] #[clap(long, conflicts_with = "size")] duration: Option, /// Optionally set a relay URL for the remote. #[clap(long)] remote_relay_url: Option, /// Optionally set direct addresses for the remote. #[clap(long)] remote_direct_address: Vec, #[clap(flatten)] endpoint_args: EndpointArgs, }, } /// How long we maximally wait for a clean shutdown const SHUTDOWN_TIME: Duration = Duration::from_secs(4); #[tokio::main] async fn main() -> Result<()> { let Cli { command, output, log, } = Cli::parse(); let output = Output::new(output); // Create secret key if not set. let secret_key = match std::env::var("IROH_SECRET") { Ok(s) => SecretKey::from_str(&s) .context("Failed to parse IROH_SECRET environment variable as iroh secret key")?, Err(_) => { let s = SecretKey::generate(); output.emit(SecretGenerated { secret_key: HEXLOWER.encode(&s.to_bytes()), }); s } }; // Determine file logging path and init tracing subscriber. let log = log.init(&command, secret_key.public())?; let endpoint_args = match &command { Commands::Provide { endpoint_args } => endpoint_args, Commands::Fetch { endpoint_args, .. } => endpoint_args, }; output.emit_if_json(&endpoint_args); let endpoint = endpoint_args .clone() .bind_endpoint(secret_key, output, log.as_ref()) .await?; match run_command(command, &endpoint, output).await { Ok(()) => (), Err(err) => { error!(?err, "run_command failed"); eprintln!("{err:#}"); } } close_endpoint_with_timeout(&endpoint, output).await; if let Some(log) = log { output.emit(LogsSaved { path: log.dir.clone(), }); } Ok(()) } async fn run_command(command: Commands, endpoint: &Endpoint, output: Output) -> Result<()> { match command { Commands::Provide { endpoint_args: _ } => provide(endpoint, output).await?, Commands::Fetch { remote_id, remote_relay_url, remote_direct_address, endpoint_args: _, mode, size, duration, } => { let length = match (size, duration) { (Some(size), None) => Length::Size(size), (None, Some(duration)) => Length::Duration(Duration::from_secs(duration)), (None, None) => Length::Duration(Duration::from_secs(10)), (Some(_), Some(_)) => unreachable!("--size and --duration args are conflicting"), }; let addrs = remote_relay_url .into_iter() .map(TransportAddr::Relay) .chain(remote_direct_address.into_iter().map(TransportAddr::Ip)); let remote_addr = EndpointAddr::from_parts(remote_id, addrs); fetch(endpoint, remote_addr, length, mode, output).await? } } Ok(()) } impl EndpointArgs { async fn bind_endpoint( self, secret_key: SecretKey, output: Output, log: Option<&LogSettings>, ) -> Result { let mut builder = Endpoint::builder(presets::Minimal); if self.no_relay { // nothing to do } else if !self.relay_url.is_empty() { let token = self.relay_auth_token.clone(); let mut relay_map = RelayMap::from_iter(self.relay_url); if let Some(ref token) = token { relay_map = relay_map.with_auth_token(token); } builder = builder.relay_mode(RelayMode::Custom(relay_map)); } else { builder = builder.relay_mode(self.env.relay_mode()); }; builder = builder.secret_key(secret_key); if self.no_relay { builder = builder.addr_filter(AddrFilter::ip_only()); } else { builder = builder.addr_filter(AddrFilter::relay_only()); } if Env::Dev == self.env { #[cfg(feature = "test-utils")] { builder = builder.ca_tls_config(iroh::tls::CaTlsConfig::insecure_skip_verify()); } #[cfg(not(feature = "test-utils"))] { n0_error::bail_any!( "Must have the `test-utils` feature enabled when using the `--env=dev` flag" ) } } if !self.no_address_lookup { if !self.no_pkarr_publish { let url = self .pkarr_relay_url .unwrap_or_else(|| self.env.pkarr_relay_url()); builder = builder.address_lookup(PkarrPublisher::builder(url)); } if !self.no_dns_resolve { let domain = self .dns_origin_domain .unwrap_or_else(|| self.env.dns_origin_domain()); builder = builder.address_lookup(DnsAddressLookup::builder(domain)); } } if let Some(host) = self.dns_server { let addr = tokio::net::lookup_host(host) .await .std_context("Failed to resolve DNS server address")? .next() .std_context("Failed to resolve DNS server address")?; builder = builder.dns_resolver(DnsResolver::with_nameserver(addr)); } else if self.env == Env::Dev { let addr = DEV_DNS_SERVER.parse().expect("valid addr"); builder = builder.dns_resolver(DnsResolver::with_nameserver(addr)); } if self.relay_only || self.no_default_bind { builder = builder.clear_ip_transports(); } if let Some(addr) = self.bind_addr_v4 { builder = builder.bind_addr(addr)?; } for addr in self.bind_addr_v4_additional { let (addr, prefix_len) = parse_ipv4_net(&addr) .with_context(|_| format!("invalid bind-addr-v4-additional: {addr}"))?; builder = builder .bind_addr_with_opts(addr, BindOpts::default().set_prefix_len(prefix_len))?; } if let Some(addr) = self.bind_addr_v6 { builder = builder.bind_addr(addr)?; } for addr in self.bind_addr_v6_additional { let (addr, prefix_len) = parse_ipv6_net(&addr) .with_context(|_| format!("invalid bind-addr-v6-additional: {addr}"))?; builder = builder .bind_addr_with_opts(addr, BindOpts::default().set_prefix_len(prefix_len))?; } let mut cfg = QuicTransportConfig::builder(); // Adjust the receive window, if configured. // By default noq and iroh set the connection-level receive window to VarInt::MAX, and // the stream receive window to 1.25 MB (for a throughput of 100 MBit/s at 100ms latency). // We leave the connection-level receive window at the default and adjust the // stream-level receive window. The distinction between connection-level and stream-level // doesn't matter here because we don't have concurrent data-intensive streams // in the transfer protocol. // We also set the send_window to 2 * stream_receive_window. By default, noq sets this // to 8 * 1.25MB. The transfer protocol doesn't use concurrent data streams, so 2 times // our receive window is enough. if let Some(size) = self.receive_window { cfg = cfg.stream_receive_window(size.into()); cfg = cfg.send_window(size as u64 * 2); } #[cfg(feature = "qlog")] match (std::env::var("QLOGDIR").ok(), log) { (Some(dir), _) => cfg = cfg.qlog_from_path(dir, "transfer"), (_, Some(log)) if log.qlog => cfg = cfg.qlog_from_path(&log.dir, ""), _ => {} } #[cfg(not(feature = "qlog"))] let _ = log; builder = builder.transport_config(cfg.build()); let endpoint = builder.alpns(vec![TRANSFER_ALPN.to_vec()]).bind().await?; if self.mdns { n0_error::bail_any!( "mDNS address lookup is no longer built into iroh; use the `iroh-mdns-address-lookup` crate instead", ); } if self.relay_only { endpoint.online().await; } else if !self.no_relay { timeout(Duration::from_secs(3), endpoint.online()) .await .ok(); } let endpoint_addr = endpoint.addr(); output.emit(EndpointBound { endpoint_id: endpoint.id(), direct_addresses: endpoint_addr.ip_addrs().copied().collect(), relay_url: endpoint_addr.relay_urls().next().cloned(), }); Ok(endpoint) } } async fn provide(endpoint: &Endpoint, output: Output) -> Result<()> { for id in 0.. { // Accept incoming connections until Ctrl-C is pressed. let incoming = tokio::select! { Some(incoming) = endpoint.accept() => incoming, _ = tokio::signal::ctrl_c() => break, else => break }; // Spawn a task for each connection. tokio::spawn( async move { let accepting = match incoming.accept() { Ok(accepting) => accepting, Err(err) => { warn!("incoming connection failed: {err:#}"); // we can carry on in these cases: // this can be caused by retransmitted datagrams return; } }; match accepting.await { Ok(conn) => { info!(remote = %conn.remote_id().fmt_short(), "connection accepted"); output.emit_with_remote(conn.remote_id(), ConnectionAccepted { id }); handle_connection(conn, output).await; } Err(err) => warn!("incoming connection failed during handshake: {err:#}"), } } .instrument(info_span!("accept", id, remote = tracing::field::Empty)), ); } Ok(()) } async fn handle_connection(conn: Connection, output: Output) { let start = Instant::now(); let remote_id = conn.remote_id(); // Spawn a background task that prints connection type changes and collects stats of all paths. let stats_task = spawn_path_watcher(conn.clone(), Some(remote_id), output); // Accept incoming streams in a loop until the connection is closed by the remote. let close_reason = loop { let (send, recv) = match conn.accept_bi().await { Ok(streams) => streams, Err(err) => break err, }; tokio::spawn( async move { if let Err(err) = handle_request(remote_id, send, recv, output).await { warn!("[{}] Request failed: {err:#}", remote_id.fmt_short()); } } .instrument(Span::current()), ); }; let is_graceful = matches!( &close_reason, ConnectionError::ApplicationClosed(f) if f.error_code == GRACEFUL_CLOSE ); let error = (!is_graceful).then(|| format!("{close_reason:#}")); info!(?error, "connection closed"); output.emit_with_remote( remote_id, ConnectionClosed { error, duration: start.elapsed(), }, ); stats_task.await.expect("stats task panicked"); } #[instrument("handle", skip_all, fields(id=send.id().index()))] async fn handle_request( remote_id: EndpointId, send: SendStream, mut recv: RecvStream, output: Output, ) -> Result<()> { let request = Request::read(&mut recv) .await .context("failed to read request")?; output.emit_with_remote(remote_id, HandleRequest { request: &request }); match request { Request::Download(length) => { let stats = send_data(send, recv, length).await?; output.emit_with_remote(remote_id, UploadComplete { stats }); } Request::Upload => { let stats = drain_stream(recv, send, None).await?; output.emit_with_remote(remote_id, DownloadComplete { stats }); } } Ok(()) } async fn fetch( endpoint: &Endpoint, remote_addr: EndpointAddr, length: Length, mode: Mode, output: Output, ) -> Result<()> { // Attempt to connect, over the given ALPN. Returns a connection. let start = Instant::now(); let conn = endpoint.connect(remote_addr, TRANSFER_ALPN).await?; let remote_id = conn.remote_id(); output.emit(Connected { remote_id, duration: start.elapsed(), }); // Spawn a background task that prints connection type changes and collects stats of all paths. let stats_task = spawn_path_watcher(conn.clone(), None, output); output.emit(StartRequest { mode, length }); // Perform requests depending on the request mode. let request_fut = async { match mode { Mode::Upload => { perform_request(&conn, RequestKind::Upload, length, start, output).await? } Mode::Download => { perform_request(&conn, RequestKind::Download, length, start, output).await? } Mode::Bidi => { tokio::try_join!( perform_request(&conn, RequestKind::Download, length, start, output), perform_request(&conn, RequestKind::Upload, length, start, output), )?; } Mode::Ping => { let Length::Duration(duration) = length else { n0_error::bail_any!("--mode ping needs --duration to be set") }; tokio::time::sleep(duration).await; } } // We finished our requests. Close the connection with our graceful error code. conn.close(GRACEFUL_CLOSE, b"done"); n0_error::Ok(()) }; // Wait for the request to complete, or for the user to interrupt it with Ctrl-C let res = tokio::select! { res = request_fut => res, _ = tokio::signal::ctrl_c() => Err(anyerr!("Cancelled")) }; // If the connection hasn't been closed above, close it now. if conn.close_reason().is_none() { conn.close(0u32.into(), b"shutdown"); } let error = conn .close_reason() .filter(|reason| !matches!(reason, ConnectionError::LocallyClosed)) .map(|reason| format!("{reason:#}")); output.emit(ConnectionClosed { error, duration: start.elapsed(), }); stats_task.await.expect("stats task panicked"); res } /// Close the endpoint, with a timeout, and emit emit once done. async fn close_endpoint_with_timeout(endpoint: &Endpoint, output: Output) { let shutdown_start = Instant::now(); let timed_out = timeout(SHUTDOWN_TIME, endpoint.close()).await.is_err(); output.emit(EndpointClosed { duration: shutdown_start.elapsed(), timed_out, }); } #[instrument("request", skip_all, fields(id = tracing::field::Empty))] async fn perform_request( conn: &Connection, request_kind: RequestKind, length: Length, conn_start: Instant, output: Output, ) -> Result<()> { let (mut send, recv) = conn.open_bi().await.anyerr()?; Span::current().record("id", send.id().index()); match request_kind { RequestKind::Download => { Request::Download(length).write(&mut send).await?; let stats = drain_stream(recv, send, Some(conn_start)).await?; output.emit(DownloadComplete { stats }); } RequestKind::Upload => { Request::Upload.write(&mut send).await?; let stats = send_data(send, recv, length).await?; output.emit(UploadComplete { stats }); } } Ok(()) } /// Drain `recv`, and once done finish `send`. /// /// We use [`SendStream::finish`] as a confirmation once we fully read [`RecvStream`]. The remote will wait /// for this event and not close the connection earlire. #[instrument("drain_stream", skip_all)] async fn drain_stream( mut recv: RecvStream, mut send: SendStream, started_at: Option, ) -> Result { debug!("start"); let start = Instant::now(); let mut read = 0; let mut num_chunks: u64 = 0; let mut time_to_first_byte = None; // These are 32 buffers, for reading approximately 32kB at once let mut bufs: [Bytes; 32] = std::array::from_fn(|_| Bytes::new()); while let Some(n) = recv.read_many_chunks(&mut bufs[..]).await.anyerr()? { // Update time to first byte if still empty and started_at is set. if let (None, Some(started_at)) = (time_to_first_byte, started_at) { time_to_first_byte = Some(started_at.elapsed()); } read += bufs.iter().take(n).map(Bytes::len).sum::(); num_chunks += 1; } send.finish().anyerr()?; let stats = DownloadStats { size: read as u64, time_to_first_byte, num_chunks, duration: start.elapsed(), }; debug!(?stats, "done"); Ok(stats) } /// Send data on `send` for `length`, afterwards wait for `recv` to be closed. /// /// When done sending, we wait for [`RecvStream`] to be closed. The remote will finish its corresponding /// send stream once it has read all our data. This ensures that we don't close the connection before the remote /// has fully read our data. #[instrument("send_data", skip_all)] async fn send_data( mut send: SendStream, mut recv: RecvStream, length: Length, ) -> Result { debug!(?length, "start"); const DATA: &[u8] = &[0xAB; 1024 * 1024]; let data = Bytes::from_static(DATA); let start = Instant::now(); let mut total = 0; loop { let (remaining_time, remaining_size) = length.remaining(start, total); let chunk = if remaining_size == 0 || remaining_time == Duration::ZERO { break; } else if remaining_size < data.len() { data.slice(..remaining_size) } else { data.clone() }; total += write_chunk_timeout(&mut send, chunk, remaining_time) .await .std_context("failed to send data")?; } send.finish().std_context("failed to finish stream")?; debug!("sending finished, wait for confirmation"); recv.read_to_end(0).await.anyerr()?; let stats = UploadStats { size: total as u64, duration: start.elapsed(), }; debug!(?stats, "done"); Ok(stats) } /// Writes as much of [`Bytes`] to a [`SendStream`] as possible within `timeout`. /// /// Completes once `chunk` is fully written or after `timeout` elapses, whatever comes first. /// /// Returns the number of bytes written. async fn write_chunk_timeout( send: &mut SendStream, chunk: Bytes, timeout: Duration, ) -> Result { // This follows the pattern of [`SendStream::write_all_chunks`] but with a timeout applied. let timeout = tokio::time::sleep(timeout); tokio::pin!(timeout); let mut bufs = &mut [chunk][..]; let mut total = 0; while !bufs.is_empty() { tokio::select! { _ = &mut timeout => break, written = send.write_many_chunks(&mut bufs) => { total += written?; } } } Ok(total) } fn parse_byte_size(s: &str) -> std::result::Result { let cfg = parse_size::Config::new().with_binary(); cfg.parse_size(s) } fn spawn_path_watcher( conn: Connection, remote_id: Option, output: Output, ) -> JoinHandle<()> { let print = move |conn: &Connection| { let path = match conn.paths().iter().find(|p| p.is_selected()) { Some(p) => SelectedPath::Selected { id: p.id(), addr: p.remote_addr().clone(), rtt: p.rtt(), }, None => SelectedPath::None, }; output.emit_maybe_remote(remote_id, ConnectionTypeChanged { path }); }; tokio::spawn(async move { let mut events = conn.path_events(); print(&conn); let mut paths = Vec::new(); while let Some(event) = events.next().await { match event { PathEvent::Selected { .. } => print(&conn), PathEvent::Closed { id, remote_addr, last_stats, .. } => { paths.push(PathData { id, remote_addr, rtt: last_stats.rtt, bytes_sent: last_stats.udp_tx.bytes, bytes_recv: last_stats.udp_rx.bytes, }); } _ => {} } } output.emit_maybe_remote(remote_id, PathStats { paths }); }) } fn parse_ipv4_net(s: &str) -> Result<(SocketAddrV4, u8)> { let (net, port) = s.split_once(":").std_context("missing colon")?; let net: Ipv4Net = net.parse().std_context("invalid net")?; let port: u16 = port.parse().std_context("invalid port")?; Ok((SocketAddrV4::new(net.addr(), port), net.prefix_len())) } fn parse_ipv6_net(s: &str) -> Result<(SocketAddrV6, u8)> { let (net, port) = s.rsplit_once(":").std_context("missing colon")?; let net: Ipv6Net = net.parse().std_context("invalid net")?; let port: u16 = port.parse().std_context("invalid port")?; Ok((SocketAddrV6::new(net.addr(), port, 0, 0), net.prefix_len())) } #[derive(ValueEnum, Default, Debug, Clone, Copy)] enum OutputMode { /// Print human-readable text. #[default] Text, /// Print newline-delimited JSON. Json, } #[derive(Debug, Clone, Copy)] struct Output { mode: OutputMode, start: Instant, } impl Output { fn new(mode: OutputMode) -> Self { Self { mode, start: Instant::now(), } } fn time(&self) -> impl fmt::Display { Style::new() .dim() .italic() .apply_to(format!("{:>6.3}s", self.start.elapsed().as_secs_f32())) } fn emit(&self, event: impl Serialize + fmt::Display) { info!("{event}"); match self.mode { OutputMode::Text => println!("{event} {}", self.time()), OutputMode::Json => println!( "{}", serde_json::to_string(&Timestamped::now(event)).unwrap() ), } } fn emit_with_remote(&self, remote: EndpointId, event: impl Serialize + fmt::Display) { info!(remote=%remote.fmt_short(), "{event}"); match self.mode { OutputMode::Text => println!( "{} {event} {}", Style::new() .dim() .apply_to(format!("[{}]", remote.fmt_short())), self.time() ), OutputMode::Json => println!( "{}", serde_json::to_string(&Timestamped::now(RemoteEvent::new(remote, event))).unwrap() ), } } fn emit_maybe_remote(&self, remote: Option, event: impl Serialize + fmt::Display) { match remote { None => self.emit(event), Some(remote) => self.emit_with_remote(remote, event), } } fn emit_if_json(&self, event: &impl Serialize) { if matches!(self.mode, OutputMode::Json) { println!( "{}", serde_json::to_string(&Timestamped::now(event)).unwrap() ) } } } #[derive(Serialize, Debug, Clone, Display)] #[display("Generated a new endpoint secret. To reuse, set\n\tIROH_SECRET={secret_key}")] #[serde(tag = "kind")] struct SecretGenerated { secret_key: String, } #[derive(Serialize, Debug, Clone)] #[serde(tag = "kind")] struct EndpointBound { endpoint_id: EndpointId, direct_addresses: Vec, relay_url: Option, } impl fmt::Display for EndpointBound { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "Our endpoint id:\n\t{}", self.endpoint_id)?; writeln!(f, "Our direct addresses:")?; for addr in &self.direct_addresses { writeln!(f, "\t{addr}")?; } match &self.relay_url { Some(url) => write!(f, "Our home relay server:\t{url}")?, None => write!(f, "No home relay server found")?, } Ok(()) } } #[derive(Serialize, Debug, Clone, Display)] #[serde(tag = "kind")] #[display("Connection type changed to {path}")] struct ConnectionTypeChanged { #[serde(flatten)] path: SelectedPath, } #[derive(Serialize, Debug, Clone)] #[serde(tag = "status")] enum SelectedPath { Selected { #[serde(skip)] id: PathId, addr: TransportAddr, #[serde(with = "duration_micros")] rtt: Duration, }, None, } impl fmt::Display for SelectedPath { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Selected { addr, rtt, id } => { write!(f, "{addr:?} [id:{id}] (RTT: {})", fmt_duration(*rtt)) } Self::None => write!(f, "none"), } } } #[derive(Serialize, Debug, Clone, Display)] #[serde(tag = "kind")] #[display("Connected to {remote_id} in {}", fmt_duration(*duration))] struct Connected { remote_id: EndpointId, #[serde(with = "duration_micros")] duration: Duration, } #[derive(Serialize, Debug, Clone, Display)] #[serde(tag = "kind")] #[display("Starting {mode:?} request with {length:?}")] struct StartRequest { mode: Mode, length: Length, } #[derive(Serialize, Debug, Clone)] #[serde(tag = "kind")] struct EndpointClosed { #[serde(with = "duration_micros")] duration: Duration, timed_out: bool, } impl fmt::Display for EndpointClosed { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let duration = fmt_duration(self.duration); match self.timed_out { false => write!(f, "Shutdown took {duration}"), true => write!(f, "Shutdown timed out after {duration}",), } } } #[derive(Serialize, Debug, Clone)] struct PathData { #[serde(skip)] id: PathId, remote_addr: TransportAddr, #[serde(with = "duration_micros")] rtt: Duration, bytes_sent: u64, bytes_recv: u64, } #[derive(Serialize, Debug, Clone)] #[serde(tag = "kind")] struct PathStats { paths: Vec, } impl fmt::Display for PathStats { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Path stats:")?; for path in &self.paths { write!( f, "\n\t[{:>2}] {:?}: RTT {}, tx={}, rx={}", path.id, path.remote_addr, fmt_duration(path.rtt), path.bytes_sent, path.bytes_recv, )?; } Ok(()) } } #[derive(Serialize, Debug, Clone, Display)] #[serde(tag = "kind")] #[display("{stats}")] struct DownloadComplete { #[serde(flatten)] stats: DownloadStats, } #[derive(Serialize, Debug, Clone, Display)] #[serde(tag = "kind")] #[display("{stats}")] struct UploadComplete { #[serde(flatten)] stats: UploadStats, } #[derive(Serialize, Debug, Clone, Copy, Display)] #[display("Accepted connection (trace id: {id})")] #[serde(tag = "kind")] struct ConnectionAccepted { id: u64, } #[derive(Serialize, Debug, Clone, Display)] #[serde(tag = "kind")] #[display("Handling {request:?} request")] struct HandleRequest<'a> { request: &'a Request, } #[derive(Serialize, Debug, Clone)] #[serde(tag = "kind")] struct ConnectionClosed { #[serde(with = "duration_micros")] duration: Duration, error: Option, } impl fmt::Display for ConnectionClosed { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let time = format!("(total time: {})", fmt_duration(self.duration)); match &self.error { Some(err) => write!(f, "Connection closed with error: {err} {time}"), None => write!(f, "Connection closed {time}",), } } } #[derive(Serialize, Debug, Clone, Display)] #[display( "Downloaded: {:>10} in {}, {:>10}/s ({}{} chunks)", HumanBytes(self.size).to_string(), fmt_duration(self.duration), HumanBytes((self.size as f64 / self.duration.as_secs_f64()) as u64), self.time_to_first_byte .map(|t| format!("time to first byte {}, ", fmt_duration(t))) .unwrap_or_default(), self.num_chunks )] struct DownloadStats { size: u64, #[serde( serialize_with = "duration_micros_opt", skip_serializing_if = "Option::is_none" )] time_to_first_byte: Option, num_chunks: u64, #[serde(with = "duration_micros")] duration: Duration, } #[derive(Serialize, Debug, Clone, Display)] #[display( "Uploaded: {:>10} in {}, {:>10}/s", HumanBytes(self.size).to_string(), fmt_duration(self.duration), HumanBytes((self.size as f64 / self.duration.as_secs_f64()) as u64) )] struct UploadStats { size: u64, #[serde(with = "duration_micros")] duration: Duration, } #[derive(Serialize, Debug, Clone, Display)] #[display("Logs saved to {}", path.display())] struct LogsSaved { path: PathBuf, } #[derive(Serialize, Debug, Clone, From, Display)] #[display("[{}] {inner}", remote_id.fmt_short())] struct RemoteEvent { #[serde(flatten)] inner: T, remote_id: EndpointId, } impl RemoteEvent { fn new(remote_id: EndpointId, inner: T) -> Self { Self { remote_id, inner } } } #[derive(Serialize)] struct Timestamped { timestamp: String, #[serde(flatten)] inner: T, } impl Timestamped { fn now(inner: T) -> Self { Self { timestamp: chrono::Utc::now().to_rfc3339(), inner, } } } fn duration_micros_opt( value: &Option, serializer: S, ) -> Result { match value { Some(d) => serializer.serialize_u64(d.as_micros() as u64), None => serializer.serialize_none(), } } mod duration_micros { use std::time::Duration; use serde::{Deserialize, Deserializer, Serializer}; pub fn serialize(duration: &Duration, serializer: S) -> Result { serializer.serialize_u64(duration.as_micros() as u64) } pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { let millis = u64::deserialize(deserializer)?; Ok(Duration::from_micros(millis)) } } pub fn init_tracing(path: Option<&Path>) { let layer = tracing_subscriber::fmt::layer().with_line_number(true); if let Some(path) = path { let file = File::create(path).expect("failed to create trace log file"); let filter = EnvFilter::try_from_default_env() .unwrap_or_else(|_| EnvFilter::new("iroh=trace,transfer=trace,noq=trace")); let layer = layer.with_writer(file).with_filter(filter); tracing_subscriber::registry().with(layer).init() } else { let layer = layer .with_writer(std::io::stderr) .with_filter(EnvFilter::from_default_env()); tracing_subscriber::registry().with(layer).init() } } fn fmt_duration(d: Duration) -> impl fmt::Display { if d > Duration::from_secs(1) { format!("{:.2}s", d.as_secs_f32()) } else if d > Duration::from_millis(1) { format!("{}ms", d.as_millis()) } else { format!("{}µs", d.as_micros()) } }