Exit nodes on Windows: use a member as exit node via destination bypass routes
Choosing an exit node on Windows did nothing: the using side was refused, because Windows has no per-user routing and the agent's own traffic would loop into the tunnel it carries. Now the overlay interface gets 0.0.0.0/1 and 128.0.0.0/1 routes (and ::/1, 8000::/1 as an IPv6 blackhole), and every underlay address the agent talks to directly gets a /32 route through the physical default gateway, marked with route metric 7373 so stale ones are found and removed. Bypass routes are written before the halves and removed after them; a failed step removes what was written. All routes are in the active store. The addresses come from the runtime: each network runtime hands the interface the verified direct IPv4 paths of its control connections and the relay hosts in use (Interface::set_underlay, refreshed every dial round); relay names are resolved when the rules are applied. ExitHostRules::needs_bypass (true only on Windows) gates it, so Linux and macOS plans never carry or re-apply on path changes. ExitHostPlan gains `bypass` and `bypass_hosts`; networks leaving forget their entry. Limits: a path that appears after the last apply goes through the tunnel until the next apply; data-plane-only addresses are not collected separately (the data links ride the same iroh paths). Nothing was run on Windows. Checks: cargo fmt --check, cargo clippy --locked --workspace --all-targets -D warnings and cargo test --locked --workspace --all-targets passed on macOS; scripts and ordering are unit-tested with an injected runner and resolver, and Interface::set_underlay with the mock host. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
637915600e
commit
7c176e0b74
8 files changed
+659
-45
No files matched your search
@@ -800,6 +800,11 @@ impl Agent {
|
||||
// itself stays, because the agent owns it and other networks may
|
||||
// still be using it.
|
||||
self.inner.routes.remove_network(network_id);
|
||||
if let Some(interface) = self.inner.interface.get() {
|
||||
interface
|
||||
.set_underlay(network_id, Vec::new(), Vec::new())
|
||||
.await;
|
||||
}
|
||||
if let Some(interface) = self.inner.interface.get()
|
||||
&& let Err(err) = interface.sync_addresses().await
|
||||
{
|
||||
|
||||
@@ -712,6 +712,34 @@ impl Runtime {
|
||||
self.start_dials();
|
||||
// Last, so it carries what this round learned.
|
||||
self.introduce_peers();
|
||||
self.refresh_underlay().await;
|
||||
}
|
||||
|
||||
/// Hands the interface the underlay destinations this network depends
|
||||
/// on: the verified direct IPv4 paths of every control connection, and
|
||||
/// the relays in use. Only hosts that cannot exempt the agent from an
|
||||
/// exit node's default route read it.
|
||||
async fn refresh_underlay(&self) {
|
||||
let Some(interface) = self.params.interface.clone() else {
|
||||
return;
|
||||
};
|
||||
let mut addresses = Vec::new();
|
||||
let mut hosts = Vec::new();
|
||||
for session in self.sessions.values() {
|
||||
for path in snapshot_connection(&session.conn).paths {
|
||||
match path.remote {
|
||||
PathAddr::Ip(std::net::SocketAddr::V4(socket)) => addresses.push(*socket.ip()),
|
||||
PathAddr::Relay(url) => hosts.extend(relay_host(&url)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
for url in self.params.adapter.snapshot().relay_urls {
|
||||
hosts.extend(relay_host(&url));
|
||||
}
|
||||
interface
|
||||
.set_underlay(self.network_id, addresses, hosts)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn add_candidate(&mut self, candidate: Candidate) {
|
||||
@@ -2448,3 +2476,48 @@ mod tests {
|
||||
assert_eq!(chosen_restored.copied(), Some("wg"));
|
||||
}
|
||||
}
|
||||
|
||||
/// The host name of a relay URL such as `https://relay.example:443/`.
|
||||
fn relay_host(url: &str) -> Option<String> {
|
||||
let rest = url.split_once("://").map_or(url, |(_, rest)| rest);
|
||||
let authority = rest.split(['/', '?', '#']).next()?;
|
||||
let host = authority.rsplit_once('@').map_or(authority, |(_, h)| h);
|
||||
let host = if host.starts_with('[') {
|
||||
host.split_once(']')?.0.trim_start_matches('[')
|
||||
} else {
|
||||
host.split(':').next()?
|
||||
};
|
||||
(!host.is_empty()
|
||||
&& host.len() <= 253
|
||||
&& host
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == ':'))
|
||||
.then(|| host.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod relay_host_tests {
|
||||
use super::relay_host;
|
||||
|
||||
#[test]
|
||||
fn the_host_is_taken_out_of_a_relay_url() {
|
||||
assert_eq!(
|
||||
relay_host("https://relay.example.com./").as_deref(),
|
||||
Some("relay.example.com.")
|
||||
);
|
||||
assert_eq!(
|
||||
relay_host("https://Euw1-1.relay.iroh.network:443/x").as_deref(),
|
||||
Some("euw1-1.relay.iroh.network")
|
||||
);
|
||||
assert_eq!(
|
||||
relay_host("http://192.0.2.7:3340").as_deref(),
|
||||
Some("192.0.2.7")
|
||||
);
|
||||
assert_eq!(
|
||||
relay_host("https://[2001:db8::1]:443/").as_deref(),
|
||||
Some("2001:db8::1")
|
||||
);
|
||||
assert_eq!(relay_host("https:///").as_deref(), None);
|
||||
assert_eq!(relay_host("https://bad host/").as_deref(), None);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user