fix(gateway): reach the relay through a proxy when that is the only way out (#1058)

* fix(mobile): bump iroh to 1.3.0 so a stuck relay cannot stall direct dials

iroh 1.2.0 sends a connection's first datagrams to every known path one
after another inside the remote-state actor, awaiting each. When the relay
is unreachable its send queue fills and the actor blocks, so handshake
packets for a direct path that does work (a mesh VPN address, a public
IPv6) queue behind it and the dial times out. 1.3.0 sends to all paths
concurrently with a bounded wait (n0-computer/iroh#4512).

The desktop workspace was already on 1.3.0; the app has its own lockfile
and was left behind.

* fix(gateway): reach the relay through a proxy when that is the only way out

iroh dials its relay with its own resolver and its own TCP, ignoring the
system proxy. On a machine whose network only works through a local proxy
(Clash and the like, system-proxy or TUN mode alike) that dial never
succeeds, and nothing says so: phones on the same network still connect,
while a phone on cellular times out, because without a relay nothing
coordinates hole punching and the home router drops unsolicited inbound
packets.

The gateway now lists the ways out it knows of, most likely first:
tty7's own http_proxy setting, the system proxy, the environment's, then
direct. It starts on the first without waiting, and when the relay stays
unreachable for 10s it tries the others on a throwaway endpoint, switching
to the first that reaches a relay (same key, same port, so pairing codes
keep working). While none does, it looks again every minute.

A pairing code now names the relay only once it is actually connected;
before, it named whichever relay iroh picked by latency, reachable or not.

The platform proxy readers in daemon::install::proxy now also hand back
the proxy as a URL, for a client that is not ureq.

Claude-Session: https://claude.ai/code/session_018wE9ZRyxgWW2f55VSy9FvZ
This commit is contained in:
l0ng-ai
2026-10-01 00:16:06 +08:00
committed by GitHub
parent 96870ebcd4
commit d7dad45fa8
8 changed files with 397 additions and 79 deletions
Generated
+1
View File
@@ -11653,6 +11653,7 @@ dependencies = [
"tty7-core",
"tty7-mobile-client",
"tty7-mobile-proto",
"url",
]
[[package]]
+2 -1
View File
@@ -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.
///
+40 -14
View File
@@ -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> {
proxy_from_url(&normalize_manual(value)?, &[]).ok()
}
#[cfg(windows)]
fn system_proxy(target_url: &str) -> Option<Proxy> {
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<String> {
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<String> {
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<String>)> {
windows::system_proxy_url(target_url)
}
#[cfg(target_os = "macos")]
fn system_proxy(target_url: &str) -> Option<Proxy> {
macos::system_proxy(target_url)
fn system_proxy_url(target_url: &str) -> Option<(String, Vec<String>)> {
macos::system_proxy_url(target_url)
}
#[cfg(not(any(windows, target_os = "macos")))]
fn system_proxy(_target_url: &str) -> Option<Proxy> {
fn system_proxy_url(_target_url: &str) -> Option<(String, Vec<String>)> {
None
}
@@ -186,14 +214,13 @@ fn parse_windows_proxy_server(server: &str, target_scheme: &str) -> Option<Strin
#[cfg(windows)]
mod windows {
use super::{parse_windows_proxy_server, proxy_from_url, target_scheme};
use ureq::Proxy;
use super::{parse_windows_proxy_server, target_scheme};
use winreg::RegKey;
use winreg::enums::HKEY_CURRENT_USER;
const INTERNET_SETTINGS: &str = r"Software\Microsoft\Windows\CurrentVersion\Internet Settings";
pub fn system_proxy(target_url: &str) -> Option<Proxy> {
pub fn system_proxy_url(target_url: &str) -> Option<(String, Vec<String>)> {
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<CFString, CFType>;
@@ -277,7 +303,7 @@ mod macos {
}
}
pub fn system_proxy(target_url: &str) -> Option<Proxy> {
pub fn system_proxy_url(target_url: &str) -> Option<(String, Vec<String>)> {
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<String> {
+5 -1
View File
@@ -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 }
+1
View File
@@ -8,6 +8,7 @@
pub mod daemon;
pub mod poller;
pub mod route;
pub mod serve;
pub mod service;
pub mod state;
+151
View File
@@ -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<Route> {
// 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<String> {
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<Item = Option<String>>) -> Vec<Route> {
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");
}
}
+175 -42
View File
@@ -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<oneshot::Sender<()>>,
@@ -92,7 +105,7 @@ pub fn start(state: State) -> Result<Running> {
}
}
async fn run(state: State, ready: std_mpsc::Sender<Result<()>>, stop: oneshot::Receiver<()>) {
async fn run(state: State, ready: std_mpsc::Sender<Result<()>>, 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<Result<()>>, 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<Result<()>>, 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<Result<()>>, 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<String> {
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<Route> {
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<u16>) -> Result<Endpoint> {
/// off. Its relay is reached by `route`.
async fn bind(key: SecretKey, port: Option<u16>, route: &Route) -> Result<Endpoint> {
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<u16>) -> Result<Endpoint> {
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<u16>) -> Result<Endpoint> {
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,
+22 -21
View File
@@ -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]]