Files
tsunagi/crates/tsunagi-core/src/overlay/interface.rs
T
UltradesuandClaude Sonnet 5.5 7c176e0b74 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>
2026-10-03 13:43:01 +01:00

1624 lines
60 KiB
Rust

//! The one interface an agent owns, and the loop that moves packets across it.
//!
//! Outbound, a packet read from the interface is looked up in the
//! [`RoutingTable`] and handed to whatever can carry it to that peer.
//! Inbound, a packet a protocol has decrypted is checked against the same
//! table and written to the interface.
//!
//! Neither direction knows which protocol is involved, and that is the
//! point: the interface, the addresses on it and the decision of whose
//! packet this is all belong here, so several protocols can be carrying
//! traffic at once without any of them owning the thing they are carrying it
//! for.
//!
//! # What is counted rather than hidden
//!
//! A packet with nowhere to go, a packet from a peer that does not hold its
//! source address, a multicast packet the operating system emitted anyway:
//! each is dropped and counted, with a sample kept for the first kind,
//! because "the overlay is quiet" and "the overlay is dropping everything"
//! look identical without them.
use std::net::{IpAddr, Ipv4Addr};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use bytes::Bytes;
use iroh::EndpointId;
use tokio::task::JoinHandle;
use crate::identity::NetworkId;
use super::exit::{ExitRulesReport, RuleSetReport};
use super::hostrules::{
BroadcastHostRules, BroadcastRulesPlan, BroadcastRulesReport, ExitHostPlan, ExitHostReport,
RuleOutcome, offer_detail,
};
use super::packet::IpHeader;
use super::router::{Route, RoutingTable};
use super::tun::{TunDevice, TunFactory, TunRequest};
use super::{Cidr, OverlayError};
/// Carries one packet to a peer, by whatever protocol currently can.
///
/// Implemented by the layer that knows which protocols are live for which
/// peers. The interface does not: it asks for a packet to reach a peer and
/// is told whether that was possible.
pub trait PacketCarrier: Send + Sync + 'static {
/// Carries a packet, answering `false` if nothing can right now.
fn carry(&self, route: Route, packet: Bytes) -> bool;
}
/// Why a decrypted packet was not written to the interface.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Rejected {
/// It was not a packet this agent could read.
Malformed,
/// The overlay is IPv4; anything else has no owner here.
WrongFamily,
/// The sending peer does not hold the source address it used.
WrongSource,
/// Broadcast disabled, outside this domain, or malformed/non-UDP.
BroadcastDenied,
/// The packet terminates outside this host's network address/domain.
WrongDestination,
}
/// Counters for one interface.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Counters {
/// Packets handed to a protocol.
pub sent: u64,
/// Broadcast copies handed to recipient tunnels.
pub broadcast_sent: u64,
/// Broadcast packets admitted to this host.
pub broadcast_received: u64,
/// Broadcasts refused by domain policy or packet validation.
pub broadcast_dropped: u64,
/// Packets written to the operating system.
pub received: u64,
/// Packets for an address nobody in any network holds.
pub unroutable: u64,
/// One such destination, so the number can be acted on.
pub unroutable_sample: Option<IpAddr>,
/// Packets nothing could carry, though their destination was known.
pub undeliverable: u64,
/// Unsupported multicast or unspecified-destination packets.
pub multicast: u64,
/// Packets from a peer that does not hold the source address used.
pub wrong_source: u64,
/// Decrypted packets addressed outside the receiving network's local host.
pub wrong_destination: u64,
/// Packets that could not be read at all.
pub malformed: u64,
}
#[derive(Debug, Default)]
struct Tally {
sent: AtomicU64,
broadcast_sent: AtomicU64,
broadcast_received: AtomicU64,
broadcast_dropped: AtomicU64,
received: AtomicU64,
unroutable: AtomicU64,
undeliverable: AtomicU64,
multicast: AtomicU64,
wrong_source: AtomicU64,
wrong_destination: AtomicU64,
malformed: AtomicU64,
sample: std::sync::Mutex<Option<IpAddr>>,
}
impl Tally {
fn snapshot(&self) -> Counters {
Counters {
sent: self.sent.load(Ordering::Relaxed),
broadcast_sent: self.broadcast_sent.load(Ordering::Relaxed),
broadcast_received: self.broadcast_received.load(Ordering::Relaxed),
broadcast_dropped: self.broadcast_dropped.load(Ordering::Relaxed),
received: self.received.load(Ordering::Relaxed),
unroutable: self.unroutable.load(Ordering::Relaxed),
unroutable_sample: match self.sample.lock() {
Ok(guard) => *guard,
Err(poisoned) => *poisoned.into_inner(),
},
undeliverable: self.undeliverable.load(Ordering::Relaxed),
multicast: self.multicast.load(Ordering::Relaxed),
wrong_source: self.wrong_source.load(Ordering::Relaxed),
wrong_destination: self.wrong_destination.load(Ordering::Relaxed),
malformed: self.malformed.load(Ordering::Relaxed),
}
}
fn note_unroutable(&self, destination: IpAddr) {
self.unroutable.fetch_add(1, Ordering::Relaxed);
// The first one is kept. A later sample would keep overwriting the
// one a reader is looking at.
let mut guard = match self.sample.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
if guard.is_none() {
*guard = Some(destination);
}
}
}
/// The overlay interface.
#[derive(Debug)]
pub struct Interface {
device: Arc<dyn TunDevice>,
factory: Arc<dyn TunFactory>,
name: String,
mtu: u32,
routes: Arc<RoutingTable>,
tally: Arc<Tally>,
/// What was last applied to the host, so an unchanged table is not
/// re-applied on every pass.
applied: std::sync::Mutex<Vec<Cidr>>,
task: std::sync::Mutex<Option<JoinHandle<()>>>,
sync_lock: Arc<tokio::sync::Mutex<()>>,
/// The host rules for this interface, when the factory manages the host.
host_rules: Option<Arc<dyn BroadcastHostRules>>,
/// What those rules last reported, `None` while none are installed.
rules_report: std::sync::Mutex<Option<BroadcastRulesReport>>,
/// What the exit-node rules were last asked for, and what came of it.
exit_state: std::sync::Mutex<ExitState>,
/// Where each network's runtime reaches its peers and relays outside the
/// overlay; read only by hosts that cannot exempt the agent from the
/// exit node's default route.
underlay: std::sync::Mutex<std::collections::HashMap<NetworkId, Underlay>>,
}
/// Underlay destinations one network's runtime currently depends on.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
struct Underlay {
addresses: std::collections::BTreeSet<std::net::Ipv4Addr>,
hosts: std::collections::BTreeSet<String>,
}
/// The exit-node host rules as this interface last left them.
#[derive(Debug, Default)]
struct ExitState {
/// Whether what an earlier run left behind has been taken away. Done once,
/// at the first sync, whether or not anything is wanted now: a firewall
/// rule that forwards a range must not outlive the switch that offered it.
cleaned: bool,
/// What was last applied, `None` while nothing is.
plan: Option<ExitHostPlan>,
/// What came of it.
report: Option<ExitHostReport>,
}
impl ExitState {
/// Whether every step of the last apply took.
fn is_ok(&self) -> bool {
self.report.as_ref().is_some_and(|report| {
report.offer.iter().all(|(_, outcome)| outcome.is_applied())
&& report.client.as_ref().is_none_or(RuleOutcome::is_applied)
})
}
}
impl Interface {
/// Creates the interface and starts moving packets.
pub async fn start(
factory: Arc<dyn TunFactory>,
name: impl Into<String>,
mtu: u32,
routes: Arc<RoutingTable>,
carrier: Arc<dyn PacketCarrier>,
) -> Result<Self, OverlayError> {
let name = name.into();
let host_rules = factory.host_rules();
let device = factory.create(TunRequest::bare(name.clone(), mtu)).await?;
// The name the operating system actually gave the interface. On Linux
// and Windows that is the one we asked for; on macOS the kernel assigns
// the utun unit, so the real name differs and everything that
// configures the host for this interface afterwards — the broadcast
// route and firewall, the reconfigure on an address change — has to use
// the real one, not the requested `tsun…`.
let name = device.name().to_string();
let tally = Arc::new(Tally::default());
let task = {
let device = Arc::clone(&device);
let routes = Arc::clone(&routes);
let tally = Arc::clone(&tally);
tokio::spawn(async move {
while let Some(packet) = device.recv().await {
let Some(header) = IpHeader::parse(&packet) else {
tally.malformed.fetch_add(1, Ordering::Relaxed);
continue;
};
let destination = header.destination();
if let (IpAddr::V4(source), IpAddr::V4(destination)) =
(header.source(), destination)
&& routes.is_broadcast(destination)
{
let recipients = super::broadcast::valid_udp(&packet)
.then(|| routes.broadcast_recipients(source, destination))
.flatten();
if let Some(recipients) = recipients {
for &route in recipients.iter() {
if carrier.carry(route, packet.clone()) {
tally.sent.fetch_add(1, Ordering::Relaxed);
tally.broadcast_sent.fetch_add(1, Ordering::Relaxed);
} else {
tally.undeliverable.fetch_add(1, Ordering::Relaxed);
}
}
} else {
tally.broadcast_dropped.fetch_add(1, Ordering::Relaxed);
}
continue;
}
if is_multicast_or_broadcast(destination) {
// The operating system emits these on any interface.
// The overlay is a set of point-to-point tunnels and
// has nowhere to put them; expected, not a fault.
tally.multicast.fetch_add(1, Ordering::Relaxed);
continue;
}
let Some(route) = routes.route_v4(destination) else {
tally.note_unroutable(destination);
continue;
};
if carrier.carry(route, packet) {
tally.sent.fetch_add(1, Ordering::Relaxed);
} else {
tally.undeliverable.fetch_add(1, Ordering::Relaxed);
}
}
})
};
Ok(Self {
device,
factory,
name,
mtu,
routes,
tally,
applied: std::sync::Mutex::new(Vec::new()),
task: std::sync::Mutex::new(Some(task)),
sync_lock: Arc::new(tokio::sync::Mutex::new(())),
host_rules,
rules_report: std::sync::Mutex::new(None),
exit_state: std::sync::Mutex::new(ExitState::default()),
underlay: std::sync::Mutex::new(std::collections::HashMap::new()),
})
}
/// Brings the broadcast route and firewall allowance in line with the
/// routing table: installed while some network has broadcast on and an
/// address, removed when none does.
///
/// Called after the addresses are synced, because the route's preferred
/// source has to be on the interface already. A plan that has not changed
/// is not re-applied, so this is cheap to call whenever the table moves.
/// It never fails: the outcome is kept for [`Self::broadcast_rules`].
pub async fn sync_broadcast_rules(&self) {
let Some(rules) = &self.host_rules else {
return;
};
let _guard = self.sync_lock.lock().await;
let desired =
self.routes
.broadcast_host_target()
.map(|(source, range)| BroadcastRulesPlan {
interface: self.name.clone(),
source,
range,
});
let current = self.current_rules_report();
match (desired, current) {
(None, None) => {}
(None, Some(_)) => {
rules.clear(&self.name).await;
self.store_rules_report(None);
tracing::info!(interface = %self.name, "broadcast host rules removed");
}
(Some(plan), Some(report)) if report.plan == plan => {}
(Some(plan), _) => {
let report = rules.apply(&plan).await;
if report.is_ok() {
tracing::info!(
interface = %self.name,
source = %plan.source,
"broadcast host rules applied"
);
} else {
tracing::warn!(
interface = %self.name,
"broadcast host rules are incomplete: {}",
report.summary()
);
}
self.store_rules_report(Some(report));
}
}
}
/// Brings the host's exit-node rules in line with the routing table.
///
/// Idempotent, and cheap when nothing changed. It never fails: the
/// outcome is kept for [`Self::exit_rules`]. The first call also takes
/// away whatever an earlier run left, even when nothing is wanted now.
pub async fn sync_exit_rules(&self) {
let Some(exit) = self
.host_rules
.as_ref()
.and_then(|rules| rules.exit_rules())
else {
return;
};
let _guard = self.sync_lock.lock().await;
let mut ranges: Vec<_> = self
.routes
.exit_offers()
.into_iter()
.map(|(_, range)| range)
.collect();
ranges.sort_by_key(|range| (range.base, range.prefix_len));
ranges.dedup();
let client = self.routes.exit_via().is_some();
let overlay = self.routes.overlay_ranges();
let (bypass, bypass_hosts) = if client && exit.needs_bypass() {
self.underlay_targets(&overlay)
} else {
(Vec::new(), Vec::new())
};
let wanted = ExitHostPlan {
interface: self.name.clone(),
offer: ranges,
client,
overlay,
bypass,
bypass_hosts,
};
let first = !std::mem::replace(&mut self.exit_state().cleaned, true);
if first {
exit.clear(&self.name).await;
}
let (planned, ok) = {
let state = self.exit_state();
(state.plan.clone(), state.is_ok())
};
if wanted.is_empty() {
if planned.is_some() {
exit.clear(&self.name).await;
let mut state = self.exit_state();
state.plan = None;
state.report = None;
tracing::info!(interface = %self.name, "exit node host rules removed");
}
return;
}
if planned.as_ref() == Some(&wanted) && ok {
return;
}
let report = exit.apply(&wanted).await;
for (range, outcome) in &report.offer {
match outcome {
RuleOutcome::Applied => tracing::info!(
interface = %self.name,
%range,
"exit node firewall rules applied"
),
RuleOutcome::Failed(reason) => tracing::warn!(
interface = %self.name,
%range,
"exit node firewall rules are incomplete: {reason}"
),
}
}
if report.offer.iter().any(|(_, outcome)| outcome.is_applied())
&& report.forwarding == Some(false)
{
tracing::warn!(
interface = %self.name,
"kernel forwarding is off for this interface, so nothing is forwarded for the \
exit node yet: {}",
if cfg!(target_os = "windows") {
"`Set-NetIPInterface -Forwarding Enabled` on the interfaces involved"
} else if cfg!(target_os = "macos") {
"`sysctl -w net.inet.ip.forwarding=1`, set again after every reboot"
} else {
"`sysctl -w net.ipv4.ip_forward=1` (or `net.ipv4.conf.<interface>.forwarding=1`), \
and set it in sysctl.d to keep it"
}
);
}
match &report.client {
Some(RuleOutcome::Applied) => tracing::info!(
interface = %self.name,
"this device's traffic is routed through its exit node"
),
Some(RuleOutcome::Failed(reason)) => tracing::warn!(
interface = %self.name,
"cannot route this device's traffic through the exit node: {reason}"
),
None => {}
}
let mut state = self.exit_state();
state.plan = Some(wanted);
state.report = Some(report);
}
/// Tells the interface where a network's runtime reaches peers and
/// relays outside the overlay. When the host needs those kept out of the
/// exit node's default route and the set changed, the rules follow.
pub async fn set_underlay(
&self,
network: NetworkId,
addresses: impl IntoIterator<Item = std::net::Ipv4Addr>,
hosts: impl IntoIterator<Item = String>,
) {
let new = Underlay {
addresses: addresses.into_iter().collect(),
hosts: hosts.into_iter().collect(),
};
let changed = {
let mut map = match self.underlay.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
if new == Underlay::default() {
map.remove(&network).is_some()
} else {
map.insert(network, new.clone()) != Some(new)
}
};
let needed = changed
&& self.routes.exit_via().is_some()
&& self
.host_rules
.as_ref()
.and_then(|rules| rules.exit_rules())
.is_some_and(|exit| exit.needs_bypass());
if needed {
self.sync_exit_rules().await;
}
}
/// Every underlay destination worth keeping direct, minus anything that
/// is the overlay itself, loopback or link-local.
fn underlay_targets(
&self,
overlay: &[crate::state::Ipv4Range],
) -> (Vec<std::net::Ipv4Addr>, Vec<String>) {
let map = match self.underlay.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
let mut addresses = std::collections::BTreeSet::new();
let mut hosts = std::collections::BTreeSet::new();
for underlay in map.values() {
addresses.extend(underlay.addresses.iter().copied().filter(|address| {
!address.is_loopback()
&& !address.is_link_local()
&& !address.is_multicast()
&& !address.is_unspecified()
&& !address.is_broadcast()
&& !overlay.iter().any(|range| range.contains(*address))
}));
hosts.extend(underlay.hosts.iter().cloned());
}
(addresses.into_iter().collect(), hosts.into_iter().collect())
}
fn exit_state(&self) -> std::sync::MutexGuard<'_, ExitState> {
match self.exit_state.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
}
}
/// What the exit-node rules of a network are doing now.
///
/// `offer` is `Some` while the network offers this agent as an exit node,
/// `client` while this device sends its traffic through a member of it.
pub fn exit_rules(&self, network: NetworkId) -> ExitRulesReport {
let Some((policy, range)) = self.routes.exit_settings(network) else {
return ExitRulesReport::default();
};
let unavailable = match self
.host_rules
.as_ref()
.map(|rules| rules.exit_rules().is_some())
{
None => Some(
"this overlay interface is not on the host (no TUN device), so there is \
nothing to forward to",
),
Some(false) => Some("exit nodes are not supported on this platform yet"),
Some(true) => None,
};
let failed = |detail: &str| RuleSetReport {
ok: false,
detail: detail.to_string(),
};
let state = self.exit_state();
let mut out = ExitRulesReport::default();
if policy.offer {
out.offer = Some(match (unavailable, range) {
(Some(why), _) => failed(why),
(None, None) => failed("waiting for the network to agree an address range"),
(None, Some(range)) => {
match state
.report
.as_ref()
.and_then(|report| report.offer.iter().find(|(r, _)| *r == range))
{
Some((_, outcome)) => RuleSetReport {
ok: outcome.is_applied(),
detail: offer_detail(outcome, &range),
},
None => failed("not applied yet"),
}
}
});
if unavailable.is_none() {
out.forwarding = state.report.as_ref().and_then(|report| report.forwarding);
}
}
if policy.via.is_some()
&& self
.routes
.exit_via()
.is_some_and(|(selected, _)| selected == network)
{
out.client = Some(match unavailable {
Some(why) => failed(why),
None => match state
.report
.as_ref()
.and_then(|report| report.client.as_ref())
{
Some(RuleOutcome::Applied) => RuleSetReport {
ok: true,
detail: "this device's traffic is routed through the exit node".to_string(),
},
Some(RuleOutcome::Failed(reason)) => failed(reason),
None => failed("not applied yet"),
},
});
}
out
}
/// Whether this agent can really act as an exit node in a network: the
/// rules members' traffic needs are in place. Kernel forwarding is not
/// part of it, because the agent never turns that on itself.
pub fn exit_offer_ready(&self, network: NetworkId) -> bool {
let Some((policy, Some(range))) = self.routes.exit_settings(network) else {
return false;
};
policy.offer
&& self.exit_state().report.as_ref().is_some_and(|report| {
report
.offer
.iter()
.any(|(r, outcome)| *r == range && outcome.is_applied())
})
}
/// What the broadcast host rules achieved, `None` when none are installed
/// or this interface has no host to configure.
pub fn broadcast_rules(&self) -> Option<BroadcastRulesReport> {
self.current_rules_report()
}
fn current_rules_report(&self) -> Option<BroadcastRulesReport> {
match self.rules_report.lock() {
Ok(guard) => guard.clone(),
Err(poisoned) => poisoned.into_inner().clone(),
}
}
fn store_rules_report(&self, report: Option<BroadcastRulesReport>) {
match self.rules_report.lock() {
Ok(mut guard) => *guard = report,
Err(poisoned) => *poisoned.into_inner() = report,
}
}
/// Brings the addresses on the host in line with the routing table.
///
/// Called whenever a network agrees a different address for this agent.
/// The interface is never recreated for it: that would drop every tunnel
/// riding on it for the sake of one address.
pub async fn sync_addresses(&self) -> Result<(), OverlayError> {
let _guard = self.sync_lock.lock().await;
let wanted = self.wanted_addresses();
{
let applied = match self.applied.lock() {
Ok(guard) => guard.clone(),
Err(poisoned) => poisoned.into_inner().clone(),
};
if applied == wanted {
return Ok(());
}
}
// Every address this agent holds, in every network it is in. The
// plan is exhaustive by contract, so this is also what takes an
// address off the interface when a network is left or stopped.
let request = TunRequest {
name: self.name.clone(),
addresses: wanted.clone(),
mtu: self.mtu,
};
self.factory.reconfigure(request).await?;
match self.applied.lock() {
Ok(mut guard) => *guard = wanted,
Err(poisoned) => *poisoned.into_inner() = wanted,
}
Ok(())
}
/// The interface name the operating system gave.
pub fn name(&self) -> &str {
self.device.name()
}
/// The interface MTU.
pub fn mtu(&self) -> u32 {
self.device.mtu()
}
/// Whether this interface exists on the host.
///
/// `false` with an in-memory device, where tunnels run and packets move
/// but the operating system knows nothing about any of it. Anything that
/// would configure the host — a route, a resolver setting, a complaint
/// that an address is missing from a link — has to ask first.
pub fn on_host(&self) -> bool {
self.factory.on_host()
}
/// Removes the interface from the host.
pub async fn remove(&self) {
// The route goes with the interface; the firewall rule does not, so
// it is taken out explicitly while the interface is still known.
if let Some(rules) = &self.host_rules {
rules.clear(&self.name).await;
self.store_rules_report(None);
let touched = {
let state = self.exit_state();
state.cleaned || state.plan.is_some()
};
if let (true, Some(exit)) = (touched, rules.exit_rules()) {
exit.clear(&self.name).await;
let mut state = self.exit_state();
state.plan = None;
state.report = None;
}
}
// The packet loop holds the device open, and it ends only when the
// device reports end of stream — which a real interface never does
// while it exists. Stop it directly before destroying the interface.
let task = match self.task.lock() {
Ok(mut guard) => guard.take(),
Err(poisoned) => poisoned.into_inner().take(),
};
if let Some(task) = task {
task.abort();
let _ = task.await;
}
self.factory.destroy(self.device.name()).await;
}
/// The counters as they stand.
pub fn counters(&self) -> Counters {
self.tally.snapshot()
}
/// Writes a packet a protocol decrypted to the operating system.
///
/// The source is checked against what the network agreed that peer
/// holds. A protocol proved *who* sent the packet; only this level knows
/// what that member is entitled to say.
pub async fn deliver(
&self,
network: NetworkId,
peer: EndpointId,
packet: Bytes,
) -> Result<(), Rejected> {
let Some(header) = IpHeader::parse(&packet) else {
self.tally.malformed.fetch_add(1, Ordering::Relaxed);
return Err(Rejected::Malformed);
};
let IpAddr::V4(source) = header.source() else {
self.tally.wrong_source.fetch_add(1, Ordering::Relaxed);
return Err(Rejected::WrongFamily);
};
if !self.routes.may_send_from(network, peer, source) {
self.tally.wrong_source.fetch_add(1, Ordering::Relaxed);
return Err(Rejected::WrongSource);
}
let broadcast = match header.destination() {
IpAddr::V4(destination) if self.routes.is_broadcast(destination) => {
if !super::broadcast::valid_udp(&packet)
|| !self.routes.accepts_broadcast(network, peer, destination)
{
self.tally.broadcast_dropped.fetch_add(1, Ordering::Relaxed);
return Err(Rejected::BroadcastDenied);
}
true
}
IpAddr::V4(destination) if self.routes.is_local_destination(network, destination) => {
false
}
// This agent is an exit node of this network: a member's packet
// for the internet goes to the host, which masquerades it.
IpAddr::V4(destination) if self.routes.accepts_exit_traffic(network, destination) => {
false
}
_ => {
self.tally.wrong_destination.fetch_add(1, Ordering::Relaxed);
return Err(Rejected::WrongDestination);
}
};
// Remote broadcasts terminate here. Only local TUN ingress can fan out.
if self.device.send(packet).await.is_ok() {
if broadcast {
self.tally
.broadcast_received
.fetch_add(1, Ordering::Relaxed);
}
self.tally.received.fetch_add(1, Ordering::Relaxed);
}
Ok(())
}
/// The addresses the interface should be carrying, from the table.
pub fn wanted_addresses(&self) -> Vec<Cidr> {
self.routes
.local_addresses()
.into_iter()
.filter_map(|(address, prefix_len)| Cidr::new(address.into(), prefix_len).ok())
.collect()
}
}
impl Drop for Interface {
fn drop(&mut self) {
if let Ok(mut guard) = self.task.lock()
&& let Some(task) = guard.take()
{
task.abort();
}
}
}
impl RoutingTable {
/// The route for a destination, when it is one the overlay carries.
fn route_v4(&self, destination: IpAddr) -> Option<Route> {
match destination {
IpAddr::V4(address) => self.route(address),
// The overlay is IPv4. An IPv6 packet has no owner here, and
// counting it as unroutable is the truth rather than a fault.
IpAddr::V6(_) => None,
}
}
}
/// Whether an address is one the overlay has nowhere to send.
fn is_multicast_or_broadcast(address: IpAddr) -> bool {
match address {
IpAddr::V4(address) => {
address.is_multicast() || address.is_broadcast() || address == Ipv4Addr::UNSPECIFIED
}
IpAddr::V6(address) => address.is_multicast(),
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
#[tokio::test]
async fn an_in_memory_interface_says_it_is_not_on_the_host() {
// Everything that would configure the operating system for this
// interface asks first, and the answer here is no: the name is real
// to the agent and to nothing else. A real `tsun0` belonging to
// another agent looks identical from here, so acting on the name
// alone would configure that one.
let interface = Interface::start(
Arc::new(crate::overlay::MemoryTunFactory::new()),
"tsun0",
1280,
Arc::new(RoutingTable::new()),
Arc::new(Recorder::default()),
)
.await
.unwrap();
assert_eq!(interface.name(), "tsun0");
assert!(!interface.on_host());
interface.remove().await;
}
use crate::identity::{NetworkKeys, NetworkName, NetworkSecret};
use crate::overlay::broadcast::BroadcastPolicy;
use crate::overlay::hostrules::{MockHostRules, RuleOutcome};
use crate::overlay::provision::{ManagedTunFactory, MockProvisioner};
use crate::overlay::router::{ExitPolicy, NetworkRoutes};
use crate::overlay::tun::{MemoryTun, MemoryTunFactory};
fn network(name: &str) -> NetworkId {
NetworkKeys::derive(
&NetworkName::new(name).unwrap(),
&NetworkSecret::from_bytes(vec![7u8; 32]).unwrap(),
)
.network_id()
}
fn peer(seed: u8) -> EndpointId {
iroh::SecretKey::from_bytes(&[seed; 32]).public()
}
fn addr(last: u8) -> Ipv4Addr {
Ipv4Addr::new(10, 13, 37, last)
}
fn ipv4(source: Ipv4Addr, destination: Ipv4Addr, payload: &[u8]) -> Bytes {
let mut packet = vec![0u8; 20];
packet[0] = 0x45;
let total = (20 + payload.len()) as u16;
packet[2..4].copy_from_slice(&total.to_be_bytes());
packet[9] = 17;
packet[12..16].copy_from_slice(&source.octets());
packet[16..20].copy_from_slice(&destination.octets());
packet.extend_from_slice(payload);
Bytes::from(packet)
}
#[tokio::test]
async fn broadcast_ingress_is_validated_and_remote_delivery_never_refloods() {
use super::super::broadcast::BroadcastPolicy;
use std::collections::HashSet;
let (interface, device, routes, carrier, id) = interface(false).await;
let enabled = BroadcastPolicy {
enabled: true,
peers: HashSet::from([peer(2)]),
};
routes.set_broadcast(id, enabled.clone());
let packet = ipv4(addr(2), Ipv4Addr::BROADCAST, &[0, 1, 0, 2, 0, 8, 0, 0]);
interface
.deliver(id, peer(2), packet.clone())
.await
.unwrap();
assert_eq!(device.pop_to_os().await.unwrap(), packet);
assert!(
carrier.carried().is_empty(),
"remote ingress never originates a fanout"
);
assert_eq!(
interface.deliver(id, peer(3), packet.clone()).await,
Err(Rejected::WrongSource)
);
assert_eq!(
interface
.deliver(id, peer(2), ipv4(addr(2), Ipv4Addr::BROADCAST, b"bad"))
.await,
Err(Rejected::BroadcastDenied)
);
routes.set_broadcast(
id,
BroadcastPolicy {
enabled: false,
..enabled
},
);
assert_eq!(
interface.deliver(id, peer(2), packet).await,
Err(Rejected::BroadcastDenied)
);
// An authenticated sender cannot use TUN as a gateway to a physical LAN.
assert_eq!(
interface
.deliver(
id,
peer(2),
ipv4(
addr(2),
"192.168.1.255".parse().unwrap(),
&[0, 1, 0, 2, 0, 8, 0, 0]
)
)
.await,
Err(Rejected::WrongDestination)
);
device.push_from_os(ipv4(
addr(1),
Ipv4Addr::BROADCAST,
&[0, 1, 0, 2, 0, 8, 0, 0],
));
wait_for(&interface, |c| c.broadcast_dropped.saturating_sub(2)).await;
assert!(carrier.carried().is_empty());
assert_eq!(interface.counters().received, 1);
interface.remove().await;
}
/// Records what it was asked to carry, and can refuse.
#[derive(Debug, Default)]
struct Recorder {
carried: std::sync::Mutex<Vec<(Route, Bytes)>>,
refuse: bool,
}
impl PacketCarrier for Recorder {
fn carry(&self, route: Route, packet: Bytes) -> bool {
if self.refuse {
return false;
}
match self.carried.lock() {
Ok(mut guard) => guard.push((route, packet)),
Err(poisoned) => poisoned.into_inner().push((route, packet)),
}
true
}
}
impl Recorder {
fn carried(&self) -> Vec<(Route, Bytes)> {
match self.carried.lock() {
Ok(guard) => guard.clone(),
Err(poisoned) => poisoned.into_inner().clone(),
}
}
}
async fn interface(
refuse: bool,
) -> (
Interface,
Arc<MemoryTun>,
Arc<RoutingTable>,
Arc<Recorder>,
NetworkId,
) {
let id = network("interface");
let routes = Arc::new(RoutingTable::new());
routes
.set_network(
id,
NetworkRoutes {
exit: Default::default(),
broadcast: Default::default(),
range: Some("10.13.37.0/24".parse().unwrap()),
local: Some(addr(1)),
peers: vec![(addr(2), peer(2)), (addr(3), peer(3))],
},
)
.unwrap();
let carrier = Arc::new(Recorder {
carried: std::sync::Mutex::new(Vec::new()),
refuse,
});
let factory = Arc::new(MemoryTunFactory::new());
let interface = Interface::start(
Arc::clone(&factory) as Arc<dyn TunFactory>,
"tsuntest",
1280,
Arc::clone(&routes),
Arc::clone(&carrier) as Arc<dyn PacketCarrier>,
)
.await
.unwrap();
// The factory keeps what it made, which is how a test reaches the
// device the interface is using without exposing it.
let device = factory.device("tsuntest").expect("the device was created");
(interface, device, routes, carrier, id)
}
/// Waits for a counter to move, so the interface's own task has run.
async fn wait_for(interface: &Interface, pick: impl Fn(&Counters) -> u64) {
for _ in 0..400 {
if pick(&interface.counters()) > 0 {
return;
}
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
panic!("the counter never moved: {:?}", interface.counters());
}
#[tokio::test]
async fn a_packet_leaving_the_interface_goes_to_the_peer_that_owns_it() {
let (interface, device, _routes, carrier, id) = interface(false).await;
device.push_from_os(ipv4(addr(1), addr(2), b"hello"));
wait_for(&interface, |counters| counters.sent).await;
let carried = carrier.carried();
assert_eq!(carried.len(), 1);
assert_eq!(carried[0].0.peer, peer(2));
assert_eq!(carried[0].0.network, id);
assert_eq!(&carried[0].1[20..], b"hello");
}
#[tokio::test]
async fn a_packet_for_nobody_is_counted_with_a_sample_and_not_sent() {
// Counted rather than dropped quietly: without the number and an
// example of it, "the overlay is quiet" and "the overlay is dropping
// everything" look the same.
let (interface, device, _routes, carrier, _id) = interface(false).await;
let nowhere = Ipv4Addr::new(10, 13, 37, 200);
device.push_from_os(ipv4(addr(1), nowhere, b"lost"));
wait_for(&interface, |counters| counters.unroutable).await;
assert_eq!(
interface.counters().unroutable_sample,
Some(IpAddr::V4(nowhere))
);
assert!(carrier.carried().is_empty(), "nothing was flooded");
}
#[tokio::test]
async fn multicast_is_expected_and_never_carried() {
let (interface, device, _routes, carrier, _id) = interface(false).await;
device.push_from_os(ipv4(addr(1), Ipv4Addr::new(224, 0, 0, 251), b"mdns"));
wait_for(&interface, |counters| counters.multicast).await;
assert_eq!(interface.counters().unroutable, 0, "not a routing failure");
assert!(carrier.carried().is_empty());
}
#[tokio::test]
async fn a_packet_nothing_can_carry_is_counted_separately() {
// Distinct from unroutable: the destination is known, there is just
// no live protocol for it. The two want different answers.
let (interface, device, _routes, _carrier, _id) = interface(true).await;
device.push_from_os(ipv4(addr(1), addr(2), b"no link"));
wait_for(&interface, |counters| counters.undeliverable).await;
let counters = interface.counters();
assert_eq!(counters.sent, 0);
assert_eq!(counters.unroutable, 0);
}
#[tokio::test]
async fn a_decrypted_packet_from_its_owner_reaches_the_operating_system() {
let (interface, device, _routes, _carrier, id) = interface(false).await;
interface
.deliver(id, peer(2), ipv4(addr(2), addr(1), b"inbound"))
.await
.unwrap();
let written = tokio::time::timeout(std::time::Duration::from_secs(5), device.pop_to_os())
.await
.expect("the packet reaches the interface")
.unwrap();
assert_eq!(&written[20..], b"inbound");
assert_eq!(interface.counters().received, 1);
}
#[tokio::test]
async fn a_peer_cannot_send_from_an_address_it_does_not_hold() {
let (interface, _device, _routes, _carrier, id) = interface(false).await;
// Another member's address.
assert_eq!(
interface
.deliver(id, peer(2), ipv4(addr(3), addr(1), b"spoofed"))
.await,
Err(Rejected::WrongSource)
);
// And this agent's own.
assert_eq!(
interface
.deliver(id, peer(2), ipv4(addr(1), addr(1), b"spoofed"))
.await,
Err(Rejected::WrongSource)
);
assert_eq!(interface.counters().wrong_source, 2);
assert_eq!(interface.counters().received, 0);
}
#[tokio::test]
async fn an_unreadable_or_wrong_family_packet_is_refused_without_panicking() {
let (interface, _device, _routes, _carrier, id) = interface(false).await;
assert_eq!(
interface.deliver(id, peer(2), Bytes::new()).await,
Err(Rejected::Malformed)
);
assert_eq!(
interface
.deliver(id, peer(2), Bytes::from_static(&[0xff; 8]))
.await,
Err(Rejected::Malformed)
);
// An IPv6 packet: readable, but the overlay has no owner for it.
let mut six = vec![0u8; 40];
six[0] = 0x60;
assert_eq!(
interface.deliver(id, peer(2), Bytes::from(six)).await,
Err(Rejected::WrongFamily)
);
}
#[tokio::test]
async fn the_wanted_addresses_follow_the_table() {
let (interface, _device, routes, _carrier, id) = interface(false).await;
assert_eq!(
interface.wanted_addresses(),
vec![Cidr::new(addr(1).into(), 24).unwrap()]
);
// The network agreed a different address for us.
routes
.set_network(
id,
NetworkRoutes {
exit: Default::default(),
broadcast: Default::default(),
range: Some("10.13.37.0/24".parse().unwrap()),
local: Some(addr(9)),
peers: vec![(addr(2), peer(2))],
},
)
.unwrap();
assert_eq!(
interface.wanted_addresses(),
vec![Cidr::new(addr(9).into(), 24).unwrap()]
);
}
async fn managed_interface(
rules: &MockHostRules,
broadcast_enabled: bool,
) -> (Interface, Arc<RoutingTable>, NetworkId) {
let id = network("rules");
let routes = Arc::new(RoutingTable::new());
routes
.set_network(
id,
NetworkRoutes {
exit: Default::default(),
broadcast: BroadcastPolicy {
enabled: broadcast_enabled,
..Default::default()
},
range: Some("10.13.37.0/24".parse().unwrap()),
local: Some(addr(1)),
peers: vec![(addr(2), peer(2))],
},
)
.unwrap();
let factory = Arc::new(
ManagedTunFactory::new(Arc::new(MockProvisioner::default()))
.with_host_rules(Arc::new(rules.clone())),
);
let interface = Interface::start(
factory as Arc<dyn TunFactory>,
"tsunrules",
1280,
Arc::clone(&routes),
Arc::new(Recorder::default()) as Arc<dyn PacketCarrier>,
)
.await
.unwrap();
(interface, routes, id)
}
#[tokio::test]
async fn host_rules_follow_broadcast_policy_address_and_interface_lifetime() {
let rules = MockHostRules::new();
let (interface, routes, id) = managed_interface(&rules, true).await;
assert!(
interface.broadcast_rules().is_none(),
"nothing before a sync"
);
interface.sync_broadcast_rules().await;
let installed = rules.installed();
assert_eq!(installed["tsunrules"].source, addr(1));
assert_eq!(
installed["tsunrules"].range,
"10.13.37.0/24".parse().unwrap()
);
assert!(interface.broadcast_rules().unwrap().is_ok());
// Unchanged: not applied a second time.
interface.sync_broadcast_rules().await;
interface.sync_broadcast_rules().await;
assert_eq!(rules.calls(), ["apply:tsunrules"]);
// A new address replaces the plan, it does not stack a second one.
routes
.set_network(
id,
NetworkRoutes {
exit: Default::default(),
broadcast: BroadcastPolicy::default(),
range: Some("10.13.37.0/24".parse().unwrap()),
local: Some(addr(9)),
peers: vec![(addr(2), peer(2))],
},
)
.unwrap();
interface.sync_broadcast_rules().await;
assert_eq!(rules.installed()["tsunrules"].source, addr(9));
assert_eq!(rules.installed().len(), 1);
// Turning broadcast off takes the rules away.
routes.set_broadcast(
id,
BroadcastPolicy {
enabled: false,
..Default::default()
},
);
interface.sync_broadcast_rules().await;
assert!(rules.installed().is_empty());
assert!(interface.broadcast_rules().is_none());
// And on again puts them back; leaving the agent removes them once more.
routes.set_broadcast(id, BroadcastPolicy::default());
interface.sync_broadcast_rules().await;
assert_eq!(rules.installed().len(), 1);
interface.remove().await;
assert!(rules.installed().is_empty(), "shutdown clears the host");
assert!(interface.broadcast_rules().is_none());
}
#[tokio::test]
async fn host_rules_are_not_installed_while_broadcast_is_off() {
let rules = MockHostRules::new();
let (interface, _routes, _id) = managed_interface(&rules, false).await;
interface.sync_broadcast_rules().await;
assert!(rules.installed().is_empty());
assert!(rules.calls().is_empty(), "the host was not even asked");
interface.remove().await;
}
#[tokio::test]
async fn a_failed_firewall_step_is_reported_and_does_not_undo_the_route() {
let rules = MockHostRules::new();
rules.fail_firewall("iptables not found");
let (interface, _routes, _id) = managed_interface(&rules, true).await;
interface.sync_broadcast_rules().await;
let report = interface.broadcast_rules().unwrap();
assert!(report.route.is_applied());
assert_eq!(
report.firewall,
RuleOutcome::Failed("iptables not found".into())
);
assert!(!report.is_ok());
// Not retried on every sync: the plan is the same.
interface.sync_broadcast_rules().await;
assert_eq!(rules.calls(), ["apply:tsunrules"]);
interface.remove().await;
}
#[tokio::test]
async fn an_interface_without_a_host_has_no_host_rules() {
let (interface, _device, _routes, _carrier, _id) = interface(false).await;
interface.sync_broadcast_rules().await;
assert!(interface.broadcast_rules().is_none());
interface.remove().await;
}
const INTERNET: Ipv4Addr = Ipv4Addr::new(93, 184, 216, 34);
fn offering() -> ExitPolicy {
ExitPolicy {
offer: true,
via: None,
}
}
fn using(exit: EndpointId) -> ExitPolicy {
ExitPolicy {
offer: false,
via: Some(exit),
}
}
#[tokio::test]
async fn internet_traffic_goes_to_the_exit_node_and_only_once_one_is_chosen() {
let (interface, device, routes, carrier, id) = interface(false).await;
device.push_from_os(ipv4(addr(1), INTERNET, b"before"));
wait_for(&interface, |counters| counters.unroutable).await;
assert!(carrier.carried().is_empty(), "no exit node, no route");
routes.set_exit(id, using(peer(2)));
device.push_from_os(ipv4(addr(1), INTERNET, b"after"));
wait_for(&interface, |counters| counters.sent).await;
let carried = carrier.carried();
assert_eq!(carried.len(), 1);
assert_eq!(carried[0].0.peer, peer(2));
assert_eq!(&carried[0].1[20..], b"after");
}
#[tokio::test]
async fn the_exit_node_may_answer_with_any_source_and_nobody_else_may() {
let (interface, device, routes, _carrier, id) = interface(false).await;
routes.set_exit(id, using(peer(2)));
interface
.deliver(id, peer(2), ipv4(INTERNET, addr(1), b"reply"))
.await
.unwrap();
let written = tokio::time::timeout(std::time::Duration::from_secs(5), device.pop_to_os())
.await
.expect("the reply reaches the interface")
.unwrap();
assert_eq!(&written[20..], b"reply");
// Another member cannot use the internet's addresses.
assert_eq!(
interface
.deliver(id, peer(3), ipv4(INTERNET, addr(1), b"spoof"))
.await,
Err(Rejected::WrongSource)
);
// Nor can the exit node speak for a member.
assert_eq!(
interface
.deliver(id, peer(2), ipv4(addr(3), addr(1), b"spoof"))
.await,
Err(Rejected::WrongSource)
);
// And what it sends still has to be for this agent.
assert_eq!(
interface
.deliver(id, peer(2), ipv4(INTERNET, addr(200), b"elsewhere"))
.await,
Err(Rejected::WrongDestination)
);
assert_eq!(interface.counters().received, 1);
}
#[tokio::test]
async fn an_exit_node_hands_a_members_internet_packet_to_the_host_only_while_offering() {
let (interface, device, routes, _carrier, id) = interface(false).await;
// Not offering: dropped exactly as before.
assert_eq!(
interface
.deliver(id, peer(2), ipv4(addr(2), INTERNET, b"request"))
.await,
Err(Rejected::WrongDestination)
);
assert_eq!(interface.counters().received, 0);
routes.set_exit(id, offering());
interface
.deliver(id, peer(2), ipv4(addr(2), INTERNET, b"request"))
.await
.unwrap();
let written = tokio::time::timeout(std::time::Duration::from_secs(5), device.pop_to_os())
.await
.expect("the request reaches the interface")
.unwrap();
assert_eq!(&written[20..], b"request");
// The source must still be the sender's own address.
assert_eq!(
interface
.deliver(id, peer(2), ipv4(addr(3), INTERNET, b"spoof"))
.await,
Err(Rejected::WrongSource)
);
assert_eq!(
interface
.deliver(id, peer(2), ipv4(INTERNET, INTERNET, b"spoof"))
.await,
Err(Rejected::WrongSource)
);
// Offering does not make this a door to the overlay or to this host.
for destination in [addr(3), addr(200), Ipv4Addr::LOCALHOST] {
assert_eq!(
interface
.deliver(id, peer(2), ipv4(addr(2), destination, b"no"))
.await,
Err(Rejected::WrongDestination),
"{destination}"
);
}
assert_eq!(interface.counters().received, 1);
}
#[tokio::test]
async fn the_underlay_reaches_the_rules_only_for_hosts_that_need_it_and_only_while_using_one() {
let rules = MockHostRules::new();
let (interface, routes, id) = managed_interface(&rules, false).await;
let relay = || vec!["relay.example".to_string()];
let direct = || {
vec![
Ipv4Addr::new(138, 201, 61, 182),
Ipv4Addr::new(10, 13, 37, 9), // the overlay itself
Ipv4Addr::LOCALHOST,
Ipv4Addr::new(169, 254, 1, 1),
]
};
// A host that does not ask never gets them, so Linux and macOS do not
// re-apply their rules whenever a path moves.
routes.set_exit(id, using(peer(2)));
interface.set_underlay(id, direct(), relay()).await;
interface.sync_exit_rules().await;
let plan = rules.exit().installed().unwrap();
assert!(plan.client);
assert!(plan.bypass.is_empty() && plan.bypass_hosts.is_empty());
// A host that does gets the usable ones, and follows changes.
rules.exit().set_needs_bypass(true);
interface.set_underlay(id, direct(), relay()).await;
interface.sync_exit_rules().await;
let plan = rules.exit().installed().unwrap();
assert_eq!(plan.bypass, vec![Ipv4Addr::new(138, 201, 61, 182)]);
assert_eq!(plan.bypass_hosts, relay());
let applies = || {
rules
.exit()
.calls()
.iter()
.filter(|c| c.starts_with("apply"))
.count()
};
let before = applies();
interface.set_underlay(id, direct(), relay()).await;
assert_eq!(applies(), before, "an unchanged set is not applied again");
interface
.set_underlay(id, vec![Ipv4Addr::new(5, 6, 7, 8)], relay())
.await;
assert_eq!(applies(), before + 1);
assert_eq!(
rules.exit().installed().unwrap().bypass,
vec![Ipv4Addr::new(5, 6, 7, 8)]
);
// Not using an exit node: nothing to keep direct.
routes.set_exit(id, Default::default());
interface.sync_exit_rules().await;
interface
.set_underlay(id, vec![Ipv4Addr::new(1, 1, 1, 1)], relay())
.await;
assert!(rules.exit().installed().is_none());
}
#[tokio::test]
async fn the_exit_rules_follow_the_routing_table_and_leave_with_the_interface() {
let rules = MockHostRules::new();
let (interface, routes, id) = managed_interface(&rules, false).await;
let range: crate::state::Ipv4Range = "10.13.37.0/24".parse().unwrap();
// Nothing wanted: nothing installed, but what a crashed run left is
// taken away once.
interface.sync_exit_rules().await;
interface.sync_exit_rules().await;
assert_eq!(rules.exit().calls(), ["clear:tsunrules"]);
assert!(rules.exit().installed().is_none());
assert!(!interface.exit_offer_ready(id));
assert_eq!(interface.exit_rules(id), ExitRulesReport::default());
// Offering: the range is masqueraded, and the agent can say so.
routes.set_exit(id, offering());
assert!(!interface.exit_offer_ready(id), "not applied yet");
let waiting = interface.exit_rules(id).offer.unwrap();
assert!(!waiting.ok);
assert!(waiting.detail.contains("not applied yet"), "{waiting:?}");
interface.sync_exit_rules().await;
assert_eq!(rules.exit().installed().unwrap().offer, vec![range]);
assert!(interface.exit_offer_ready(id));
let report = interface.exit_rules(id);
assert!(report.offer.as_ref().unwrap().ok);
assert_eq!(report.forwarding, Some(true));
assert!(report.client.is_none());
// Unchanged: not applied again.
interface.sync_exit_rules().await;
assert_eq!(
rules
.exit()
.calls()
.iter()
.filter(|call| call.starts_with("apply"))
.count(),
1
);
// Forwarding off in the kernel: still installed, still ready, and said.
rules.exit().set_forwarding(Some(false));
routes.set_exit(
id,
ExitPolicy {
offer: true,
via: Some(peer(2)),
},
);
interface.sync_exit_rules().await;
assert!(rules.exit().installed().unwrap().client);
assert!(interface.exit_offer_ready(id));
let report = interface.exit_rules(id);
assert_eq!(report.forwarding, Some(false));
assert!(report.offer.unwrap().ok);
assert!(report.client.unwrap().ok);
// Turned off: removed, and no longer announced as ready.
routes.set_exit(id, ExitPolicy::default());
interface.sync_exit_rules().await;
assert!(rules.exit().installed().is_none());
assert!(!interface.exit_offer_ready(id));
assert_eq!(interface.exit_rules(id), ExitRulesReport::default());
routes.set_exit(id, offering());
interface.sync_exit_rules().await;
assert!(rules.exit().installed().is_some());
interface.remove().await;
assert!(
rules.exit().installed().is_none(),
"gone with the interface"
);
}
#[tokio::test]
async fn a_failed_exit_step_is_reported_and_the_agent_does_not_claim_to_be_an_exit_node() {
let rules = MockHostRules::new();
rules.exit().fail_with(Some("iptables needs root".into()));
let (interface, routes, id) = managed_interface(&rules, false).await;
routes.set_exit(id, offering());
interface.sync_exit_rules().await;
assert!(!interface.exit_offer_ready(id));
let offer = interface.exit_rules(id).offer.unwrap();
assert!(!offer.ok);
assert!(offer.detail.contains("iptables needs root"), "{offer:?}");
// It is tried again, not given up on, the next time it is asked.
rules.exit().fail_with(None);
interface.sync_exit_rules().await;
assert!(interface.exit_offer_ready(id));
interface.remove().await;
}
#[tokio::test]
async fn an_interface_without_a_host_cannot_be_an_exit_node_and_says_why() {
let (interface, _device, routes, _carrier, id) = interface(false).await;
routes.set_exit(
id,
ExitPolicy {
offer: true,
via: Some(peer(2)),
},
);
interface.sync_exit_rules().await;
assert!(!interface.exit_offer_ready(id));
let report = interface.exit_rules(id);
let offer = report.offer.unwrap();
assert!(!offer.ok);
assert!(offer.detail.contains("not on the host"), "{offer:?}");
assert!(!report.client.unwrap().ok);
interface.remove().await;
}
#[tokio::test]
async fn a_network_waiting_for_its_range_has_nothing_to_masquerade_yet() {
let rules = MockHostRules::new();
let (interface, routes, _id) = managed_interface(&rules, false).await;
let waiting = network("waiting");
routes
.set_network(
waiting,
NetworkRoutes {
exit: offering(),
..Default::default()
},
)
.unwrap();
interface.sync_exit_rules().await;
assert!(rules.exit().installed().is_none());
let offer = interface.exit_rules(waiting).offer.unwrap();
assert!(offer.detail.contains("address range"), "{offer:?}");
interface.remove().await;
}
}