diff --git a/Cargo.lock b/Cargo.lock index 09d3a6b7..1f8c2e92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11653,6 +11653,7 @@ dependencies = [ "tty7-core", "tty7-mobile-client", "tty7-mobile-proto", + "url", ] [[package]] diff --git a/crates/tty7-core/src/core/config.rs b/crates/tty7-core/src/core/config.rs index 387d1150..a74c72e3 100644 --- a/crates/tty7-core/src/core/config.rs +++ b/crates/tty7-core/src/core/config.rs @@ -383,7 +383,8 @@ pub struct Config { pub clipboard_trim_trailing_spaces: bool, pub copy_on_select: bool, /// Optional HTTP/SOCKS proxy for tty7's *own* update checks and release - /// downloads; when set it overrides the system proxy and the environment. + /// downloads, and (HTTP only) the mobile gateway's relay; when set it is + /// tried before the system proxy and the environment. /// Programs running in a pane are unaffected — they inherit their proxy /// from their own environment, as in any other terminal. /// diff --git a/crates/tty7-core/src/daemon/install/proxy.rs b/crates/tty7-core/src/daemon/install/proxy.rs index b9a91b98..4b7d79e8 100644 --- a/crates/tty7-core/src/daemon/install/proxy.rs +++ b/crates/tty7-core/src/daemon/install/proxy.rs @@ -1,6 +1,8 @@ //! Resolve an effective HTTP(S)/SOCKS proxy for tty7's *own* downloads. //! -//! Scope: the update check and the release / remote-server asset downloads. +//! Scope: the update check and the release / remote-server asset downloads, +//! and the mobile gateway's connection to its relay (which takes the URLs from +//! [`system_url`] and [`env_url`] rather than a `ureq::Proxy`). //! Programs running inside a pane are deliberately untouched — they inherit //! whatever their environment says, exactly like in any other terminal. //! @@ -61,18 +63,44 @@ fn parse_manual(value: &str) -> Option { proxy_from_url(&normalize_manual(value)?, &[]).ok() } -#[cfg(windows)] fn system_proxy(target_url: &str) -> Option { - windows::system_proxy(target_url) + let (url, no_proxy) = system_proxy_url(target_url)?; + proxy_from_url(&url, &no_proxy).ok() +} + +/// The platform's system proxy for `target_url`, as a URL: what [`resolve`] +/// uses, for a client that is not `ureq`. The platform's bypass list is not +/// applied — the caller knows where it is going. +pub fn system_url(target_url: &str) -> Option { + system_proxy_url(target_url).map(|(url, _)| url) +} + +/// The proxy the environment names for `target_url`, as a URL. `NO_PROXY` is +/// not applied, as in [`system_url`]. +pub fn env_url(target_url: &str) -> Option { + let names: &[&str] = if target_scheme(target_url) == "https" { + &["HTTPS_PROXY", "https_proxy", "ALL_PROXY", "all_proxy"] + } else { + &["HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy"] + }; + names + .iter() + .filter_map(|name| std::env::var(name).ok()) + .find_map(|value| normalize_manual(&value)) +} + +#[cfg(windows)] +fn system_proxy_url(target_url: &str) -> Option<(String, Vec)> { + windows::system_proxy_url(target_url) } #[cfg(target_os = "macos")] -fn system_proxy(target_url: &str) -> Option { - macos::system_proxy(target_url) +fn system_proxy_url(target_url: &str) -> Option<(String, Vec)> { + macos::system_proxy_url(target_url) } #[cfg(not(any(windows, target_os = "macos")))] -fn system_proxy(_target_url: &str) -> Option { +fn system_proxy_url(_target_url: &str) -> Option<(String, Vec)> { None } @@ -186,14 +214,13 @@ fn parse_windows_proxy_server(server: &str, target_scheme: &str) -> Option Option { + pub fn system_proxy_url(target_url: &str) -> Option<(String, Vec)> { let key = RegKey::predef(HKEY_CURRENT_USER) .open_subkey(INTERNET_SETTINGS) .ok()?; @@ -213,13 +240,13 @@ mod windows { .collect(); let proxy_url = parse_windows_proxy_server(&server, target_scheme(target_url))?; - proxy_from_url(&proxy_url, &no_proxy).ok() + Some((proxy_url, no_proxy)) } } #[cfg(target_os = "macos")] mod macos { - use super::{proxy_from_url, target_scheme}; + use super::target_scheme; // Via `system_configuration`'s re-export, so these are the same // core-foundation types `get_proxies` hands back. `daemon::pane` keeps its // own, newer core-foundation; the two never exchange values. @@ -235,7 +262,6 @@ mod macos { kSCPropNetProxiesHTTPSProxy, kSCPropNetProxiesSOCKSEnable, kSCPropNetProxiesSOCKSPort, kSCPropNetProxiesSOCKSProxy, }; - use ureq::Proxy; /// The proxy settings dictionary `SCDynamicStore` hands back. type Proxies = CFDictionary; @@ -277,7 +303,7 @@ mod macos { } } - pub fn system_proxy(target_url: &str) -> Option { + pub fn system_proxy_url(target_url: &str) -> Option<(String, Vec)> { let store = SCDynamicStoreBuilder::new("tty7").build(); let proxies = store.get_proxies()?; @@ -292,7 +318,7 @@ mod macos { .into_iter() .find_map(|kind| read_proxy(&proxies, kind))?; - proxy_from_url(&proxy_url, &read_exceptions(&proxies)).ok() + Some((proxy_url, read_exceptions(&proxies))) } fn read_proxy(proxies: &Proxies, kind: Kind) -> Option { diff --git a/crates/tty7-gateway/Cargo.toml b/crates/tty7-gateway/Cargo.toml index 6b043622..b0383fd9 100644 --- a/crates/tty7-gateway/Cargo.toml +++ b/crates/tty7-gateway/Cargo.toml @@ -12,7 +12,9 @@ name = "tty7-gateway" path = "src/main.rs" [dependencies] -tty7-core = { path = "../tty7-core" } +# `remote-install` for its proxy resolution, which the relay dial shares with +# tty7's downloads. +tty7-core = { path = "../tty7-core", features = ["remote-install"] } tty7-mobile-proto = { path = "../tty7-mobile-proto", features = ["tokio"] } anyhow.workspace = true @@ -31,6 +33,8 @@ iroh = "1.2" iroh-mdns-address-lookup = "0.5" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "io-util"] } getrandom = "0.3" +# iroh takes its proxy as a `Url`. +url = "2" # Draws the pairing code as a QR code right in the terminal. qrcode = { version = "0.14", default-features = false } diff --git a/crates/tty7-gateway/src/lib.rs b/crates/tty7-gateway/src/lib.rs index c381503f..1d0e91ce 100644 --- a/crates/tty7-gateway/src/lib.rs +++ b/crates/tty7-gateway/src/lib.rs @@ -8,6 +8,7 @@ pub mod daemon; pub mod poller; +pub mod route; pub mod serve; pub mod service; pub mod state; diff --git a/crates/tty7-gateway/src/route.rs b/crates/tty7-gateway/src/route.rs new file mode 100644 index 00000000..0a8cabdb --- /dev/null +++ b/crates/tty7-gateway/src/route.rs @@ -0,0 +1,151 @@ +//! How the gateway gets out to its relay: straight, or through a proxy. +//! +//! The relay is what lets a phone on another network reach this machine at +//! all — it carries the first packets and coordinates hole punching — and iroh +//! dials it with its own resolver and its own TCP, not through whatever proxy +//! the system is set to. On a machine whose network only works through a local +//! proxy, that dial fails and nothing says so: the gateway still serves phones +//! on its own network, and every other phone times out. +//! +//! So the gateway lists the ways out it knows of, the ones people have set up +//! first, and keeps the first one the relay actually answers on. + +use std::fmt; + +use tty7_core::core::config::config_path; +use tty7_core::daemon::install::proxy; +use url::Url; + +/// One way to reach the relay. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Route { + Direct, + /// Through an HTTP proxy, with `CONNECT`. + Proxy(Url), +} + +impl Route { + pub fn proxy(&self) -> Option<&Url> { + match self { + Route::Direct => None, + Route::Proxy(url) => Some(url), + } + } +} + +impl fmt::Display for Route { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Route::Direct => f.write_str("direct"), + // Never the userinfo: this ends up in logs. + Route::Proxy(url) => match url.port_or_known_default() { + Some(port) => write!(f, "proxy {}:{port}", url.host_str().unwrap_or("?")), + None => write!(f, "proxy {}", url.host_str().unwrap_or("?")), + }, + } + } +} + +/// Every way out worth trying, most likely first: tty7's own proxy setting, +/// then the system's, then the environment's, then none. Read afresh on each +/// call — a proxy is switched on and off far more often than the gateway +/// restarts. +pub fn routes() -> Vec { + // Relays are dialed over https; the proxy that applies is that scheme's. + const TARGET: &str = "https://relay.invalid/"; + candidates([ + manual_proxy().as_deref().and_then(proxy::normalize_manual), + proxy::system_url(TARGET), + proxy::env_url(TARGET), + ]) +} + +/// `http_proxy` from `config.json`, read on its own: loading the whole +/// `Config` may migrate the file, which is the GUI's business, not the +/// gateway's. +fn manual_proxy() -> Option { + let text = std::fs::read(config_path("config.json")?).ok()?; + let config: serde_json::Value = serde_json::from_slice(&text).ok()?; + config.get("http_proxy")?.as_str().map(str::to_owned) +} + +fn candidates(proxies: impl IntoIterator>) -> Vec { + let mut routes = Vec::new(); + for url in proxies.into_iter().flatten() { + // iroh tunnels through a proxy with `CONNECT`; a SOCKS one it cannot + // use, and the relay would stay out of reach through it. + let Some(url) = Url::parse(&url) + .ok() + .filter(|u| u.scheme() == "http" || u.scheme() == "https") + else { + continue; + }; + let route = Route::Proxy(url); + if !routes.contains(&route) { + routes.push(route); + } + } + routes.push(Route::Direct); + routes +} + +#[cfg(test)] +mod tests { + use super::*; + + fn proxy(url: &str) -> Route { + Route::Proxy(Url::parse(url).unwrap()) + } + + #[test] + fn with_no_proxy_anywhere_the_only_way_is_direct() { + assert_eq!(candidates([None, None, None]), [Route::Direct]); + } + + #[test] + fn proxies_come_first_in_order_and_direct_last() { + assert_eq!( + candidates([ + Some("http://10.0.0.1:3128".into()), + None, + Some("http://127.0.0.1:7890".into()), + ]), + [ + proxy("http://10.0.0.1:3128"), + proxy("http://127.0.0.1:7890"), + Route::Direct + ] + ); + } + + #[test] + fn the_same_proxy_named_twice_is_tried_once() { + // The usual case: the system proxy and `HTTPS_PROXY` both point at the + // same local proxy. + assert_eq!( + candidates([ + None, + Some("http://127.0.0.1:7890".into()), + Some("http://127.0.0.1:7890".into()), + ]), + [proxy("http://127.0.0.1:7890"), Route::Direct] + ); + } + + #[test] + fn socks_proxies_are_skipped() { + assert_eq!( + candidates([Some("socks5://127.0.0.1:1080".into()), None, None]), + [Route::Direct] + ); + } + + #[test] + fn a_route_prints_without_credentials() { + assert_eq!( + proxy("http://user:secret@127.0.0.1:7890").to_string(), + "proxy 127.0.0.1:7890" + ); + assert_eq!(Route::Direct.to_string(), "direct"); + } +} diff --git a/crates/tty7-gateway/src/service.rs b/crates/tty7-gateway/src/service.rs index afc273f5..6ccce4b9 100644 --- a/crates/tty7-gateway/src/service.rs +++ b/crates/tty7-gateway/src/service.rs @@ -10,19 +10,32 @@ use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; use std::sync::Arc; use std::sync::mpsc as std_mpsc; use std::thread::JoinHandle; -use std::time::Duration; +use std::time::{Duration, Instant}; use anyhow::{Context as _, Result}; use iroh::endpoint::{BindOpts, presets}; -use iroh::{Endpoint, SecretKey}; +use iroh::{Endpoint, SecretKey, Watcher as _}; use iroh_mdns_address_lookup::MdnsAddressLookup; use tokio::sync::oneshot; use tty7_mobile_proto::{ALPN, MDNS_SERVICE, PairCode}; use crate::daemon::{Daemon, hostname}; +use crate::route::{self, Route}; use crate::serve::{self, Backend as _}; use crate::state::{Reachable, State, Status, failed, running}; +/// How long a relay may stay out of reach before the gateway tries other ways +/// out to it. Long enough for a healthy one to connect, which takes a second +/// or two. +const RELAY_WAIT: Duration = Duration::from_secs(10); +/// How often, while no way out reaches a relay, the gateway tries them again. +const RELAY_RETRY: Duration = Duration::from_secs(60); +/// How often the relay's state is looked at. +const RELAY_POLL: Duration = Duration::from_secs(2); +/// How long a way out being tried has to reach a relay. +const PROBE_WAIT: Duration = Duration::from_secs(8); +const PROBE_POLL: Duration = Duration::from_millis(250); + /// A gateway running on its own thread. Dropping it stops it. pub struct Running { stop: Option>, @@ -92,7 +105,7 @@ pub fn start(state: State) -> Result { } } -async fn run(state: State, ready: std_mpsc::Sender>, stop: oneshot::Receiver<()>) { +async fn run(state: State, ready: std_mpsc::Sender>, mut stop: oneshot::Receiver<()>) { // Another gateway already serving this config dir owns `status.json`; one // that could not take the lock must not overwrite what it says. let lock = match state.lock_serve() { @@ -102,20 +115,16 @@ async fn run(state: State, ready: std_mpsc::Sender>, stop: oneshot::R return; } }; + // The most likely way out, taken without waiting to see: phones on this + // network need none, and a better one is found while they are served. + let mut route = routes().await.into_iter().next().unwrap_or(Route::Direct); let started = async { - let endpoint = bind(state.secret_key()?, state.port()).await?; - if let Some(port) = endpoint - .bound_sockets() - .iter() - .map(SocketAddr::port) - .find(|&p| p != 0) - && state.port() != Some(port) - { - state.set_port(port).context("remembering the port")?; - } - anyhow::Ok(endpoint) + let key = state.secret_key()?; + let endpoint = bind(key.clone(), state.port(), &route).await?; + remember_port(&endpoint, &state)?; + anyhow::Ok((key, endpoint)) }; - let endpoint = match started.await { + let (key, mut endpoint) = match started.await { Ok(up) => up, Err(e) => { let _ = state.set_status(&failed(&e)); @@ -124,7 +133,7 @@ async fn run(state: State, ready: std_mpsc::Sender>, stop: oneshot::R } }; let id = endpoint.id().to_string(); - log::info!("mobile gateway listening as {id}"); + log::info!("mobile gateway listening as {id} (relay: {route})"); let _ = state.set_status(&running(&id)); let _ = ready.send(Ok(())); @@ -135,44 +144,165 @@ async fn run(state: State, ready: std_mpsc::Sender>, stop: oneshot::R log::warn!("{}", serve::server_down(&daemon.hostname(), &e)); } - // Keep the addresses a pairing code carries current. The local ones are - // known at once and are all a phone on the same network needs; the relay - // only arrives once the endpoint gets online, which on a network that - // blocks the relays is never — a code must not wait for it. - let addrs = tokio::spawn({ - let (endpoint, state) = (endpoint.clone(), state.clone()); - async move { - loop { - let addr = endpoint.addr(); - let reachable = Reachable { - relay: addr.relay_urls().next().map(|u| u.to_string()), - addrs: addr.ip_addrs().map(|a| a.to_string()).collect(), - }; - if state.reachable() != reachable { - let _ = state.set_reachable(&reachable); - } - tokio::time::sleep(Duration::from_secs(5)).await; + loop { + let addrs = tokio::spawn(keep_reachable(endpoint.clone(), state.clone())); + let switch = tokio::select! { + () = serve::run(endpoint.clone(), state.clone(), daemon.clone()) => None, + _ = &mut stop => None, + better = better_route(&endpoint, &route) => Some(better), + }; + addrs.abort(); + let _ = addrs.await; + let Some(better) = switch else { break }; + // iroh takes its proxy when an endpoint is built, so a new way out is + // a new endpoint: the same key on the same port, so phones' pairing + // codes still reach it. Only a relay that has stayed out of reach gets + // here, so what this drops is at most connections on this network, + // which the phone makes again. + log::info!("mobile gateway: its relay answers {better}, not {route} — switching"); + // Every handle on the old endpoint has to be gone before its port is + // free to bind again. + endpoint.close().await; + drop(endpoint); + let rebound = async { + let endpoint = bind(key.clone(), state.port(), &better).await?; + remember_port(&endpoint, &state)?; + anyhow::Ok(endpoint) + }; + match rebound.await { + Ok(new) => { + endpoint = new; + route = better; + } + Err(e) => { + log::warn!("mobile gateway: {e:#}"); + let _ = state.set_status(&failed(&e)); + drop(lock); + return; } } - }); - - tokio::select! { - () = serve::run(endpoint.clone(), state.clone(), daemon) => {} - _ = stop => {} } - addrs.abort(); endpoint.close().await; let _ = state.set_status(&Status::Stopped); drop(lock); log::info!("mobile gateway stopped"); } +/// Keeps the port a restart should bind again, so the addresses in phones' +/// pairing codes still reach this machine. +fn remember_port(endpoint: &Endpoint, state: &State) -> Result<()> { + if let Some(port) = endpoint + .bound_sockets() + .iter() + .map(SocketAddr::port) + .find(|&p| p != 0) + && state.port() != Some(port) + { + state.set_port(port).context("remembering the port")?; + } + Ok(()) +} + +/// Keeps the addresses a pairing code carries current. The local ones are +/// known at once and are all a phone on the same network needs. The relay is +/// only written down once it is connected: the one iroh picks by latency is +/// not necessarily one it can reach, and a code naming it sends a phone +/// somewhere this machine is not. +async fn keep_reachable(endpoint: Endpoint, state: State) { + loop { + let reachable = Reachable { + relay: connected_relay(&endpoint), + addrs: endpoint.addr().ip_addrs().map(|a| a.to_string()).collect(), + }; + if state.reachable() != reachable { + let _ = state.set_reachable(&reachable); + } + tokio::time::sleep(Duration::from_secs(5)).await; + } +} + +fn connected_relay(endpoint: &Endpoint) -> Option { + endpoint + .home_relay_status() + .get() + .iter() + .find(|status| status.is_connected()) + .map(|status| status.url().to_string()) +} + +/// Every way out to try, most likely first. Reading them touches the config +/// file and, on macOS, the system configuration store. +async fn routes() -> Vec { + tokio::task::spawn_blocking(route::routes) + .await + .unwrap_or_else(|_| vec![Route::Direct]) +} + +/// Resolves, never, while `endpoint`'s relay is up. Once it has been out of +/// reach for a while, tries every other way out on an endpoint of its own and +/// resolves with the first that reaches a relay — and if none does, looks +/// again every so often: a proxy is started, a network changes. +async fn better_route(endpoint: &Endpoint, current: &Route) -> Route { + let mut down_for = RELAY_WAIT; + let mut said = false; + loop { + relay_down_for(endpoint, down_for).await; + for route in routes().await.into_iter().filter(|r| r != current) { + if reaches_relay(&route).await { + return route; + } + } + if !said { + log::warn!( + "mobile gateway: no relay answers ({current}, or any proxy tty7 knows of) — \ + phones on other networks cannot reach this machine" + ); + said = true; + } + down_for = RELAY_RETRY; + } +} + +/// Resolves once `endpoint` has gone `span` without a connected relay. +async fn relay_down_for(endpoint: &Endpoint, span: Duration) { + let mut since = Instant::now(); + loop { + tokio::time::sleep(RELAY_POLL).await; + if connected_relay(endpoint).is_some() { + since = Instant::now(); + } else if since.elapsed() >= span { + return; + } + } +} + +/// Whether a relay answers by `route`, asked on a throwaway endpoint so the +/// one phones use is not disturbed. +async fn reaches_relay(route: &Route) -> bool { + let mut builder = Endpoint::builder(presets::N0).clear_address_lookup(); + if let Some(proxy) = route.proxy() { + builder = builder.proxy_url(proxy.clone()); + } + let Ok(probe) = builder.bind().await else { + return false; + }; + let reached = tokio::time::timeout(PROBE_WAIT, async { + while connected_relay(&probe).is_none() { + tokio::time::sleep(PROBE_POLL).await; + } + }) + .await + .is_ok(); + probe.close().await; + reached +} + /// Binds the gateway's endpoint on the port it had last time, so the /// addresses in phones' pairing codes still reach it, and advertises it on the /// local network. Each of those is given up, with a note, rather than let it /// keep the gateway from starting: the port may be taken, and multicast may be -/// off. -async fn bind(key: SecretKey, port: Option) -> Result { +/// off. Its relay is reached by `route`. +async fn bind(key: SecretKey, port: Option, route: &Route) -> Result { let mut attempts = Vec::new(); if let Some(port) = port { attempts.extend([(port, true), (port, false)]); @@ -181,7 +311,7 @@ async fn bind(key: SecretKey, port: Option) -> Result { let mut failures = Vec::new(); for (port, mdns) in attempts { - let builder = Endpoint::builder(presets::N0) + let mut builder = Endpoint::builder(presets::N0) .secret_key(key.clone()) .alpns(vec![ALPN.to_vec()]) .clear_ip_transports() @@ -191,6 +321,9 @@ async fn bind(key: SecretKey, port: Option) -> Result { SocketAddr::from((Ipv6Addr::UNSPECIFIED, port)), BindOpts::default().set_is_required(false), )?; + if let Some(proxy) = route.proxy() { + builder = builder.proxy_url(proxy.clone()); + } let builder = match mdns { true => builder.address_lookup(MdnsAddressLookup::builder().service_name(MDNS_SERVICE)), false => builder, diff --git a/mobile/src-tauri/Cargo.lock b/mobile/src-tauri/Cargo.lock index f15aaea7..55181ac5 100644 --- a/mobile/src-tauri/Cargo.lock +++ b/mobile/src-tauri/Cargo.lock @@ -948,7 +948,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c54e03a951783e8b327515db3f2a2fd0e3bed362a96b066f341ce66ed49b4ead" dependencies = [ "data-encoding", - "syn 3.0.6", + "syn 1.0.109", ] [[package]] @@ -1080,7 +1080,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1301,7 +1301,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1644,8 +1644,8 @@ dependencies = [ "libc", "log", "rustversion", - "windows-link 0.2.1", - "windows-result 0.4.1", + "windows-link 0.1.3", + "windows-result 0.3.4", ] [[package]] @@ -2080,7 +2080,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.61.2", ] [[package]] @@ -2300,9 +2300,9 @@ checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" [[package]] name = "iroh" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2f8d1cfffc83efe39a1031aab423ce09cb8048071baba550508931e9a81ce46" +checksum = "885787b892b5e2507c701f132ecbd45d2bad4bbb75157a19427087c16dadb833" dependencies = [ "backon", "blake3", @@ -2350,9 +2350,9 @@ dependencies = [ [[package]] name = "iroh-base" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd1efd1aaecd68d4d1b0727a30c5811f43fb822843e48f65f6e5db3b7256cc9" +checksum = "6ae9092be76c9f5429776ab3394f5a5a78dc19dc2524267e884a5b9781546c3f" dependencies = [ "curve25519-dalek", "data-encoding", @@ -2439,9 +2439,9 @@ dependencies = [ [[package]] name = "iroh-relay" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beb2294a9749d6a25fd7cd8bcf0fccd932f20716d4f967d85d3f23c7135ae6a2" +checksum = "ee116a8233f84980574bb8d11e4517503080cd2c3605bafca04ee9c35d708bd9" dependencies = [ "blake3", "bytes", @@ -3268,7 +3268,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3302,7 +3302,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate 3.5.0", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn 2.0.119", @@ -4233,7 +4233,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4291,7 +4291,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4951,6 +4951,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", + "quote", "unicode-ident", ] @@ -5374,10 +5375,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6243,7 +6244,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6689,8 +6690,8 @@ dependencies = [ "log", "serde", "thiserror 2.0.21", - "windows 0.62.2", - "windows-core 0.62.2", + "windows 0.61.3", + "windows-core 0.61.2", ] [[package]]