mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-10-06 00:02:14 +00:00
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:
Generated
+1
@@ -11653,6 +11653,7 @@ dependencies = [
|
||||
"tty7-core",
|
||||
"tty7-mobile-client",
|
||||
"tty7-mobile-proto",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
pub mod daemon;
|
||||
pub mod poller;
|
||||
pub mod route;
|
||||
pub mod serve;
|
||||
pub mod service;
|
||||
pub mod state;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Generated
+22
-21
@@ -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]]
|
||||
|
||||
Reference in New Issue
Block a user