diff --git a/README.md b/README.md index 73d1198..9ef0505 100644 --- a/README.md +++ b/README.md @@ -174,8 +174,18 @@ sudo sysctl -qw net.ipv6.conf.tsunjwc6dcrtmo5.keep_addr_on_down=1 sudo ip -6 address add fd80:1210:f724:f620:d1bb:f982:3b6e:19bd/64 dev tsunjwc6dcrtmo5 nodad ``` -If you asked for IPv4 with `--ipv4-range`, `tun-setup` adds an `ip address add` -line for it too. +With IPv4 enabled, `tun-setup` adds an `ip address add` line for the overlay +IPv4 address too — but only once there is one to print. Unlike the IPv6 +address, the IPv4 address is not derived from the keys: it is allocated at run +time and signed (see [docs/sync-model.md](docs/sync-model.md)), so it exists +only after the agent has run once. `tun-setup` then reads it back out of +`state.sqlite`, which does not disturb a running agent, and includes it from +then on. Until then the first `tsunagi up` prints the exact command for the +address it was given. + +That IPv4 line needs no `keep_addr_on_down` and no `nodad`: Linux keeps IPv4 +addresses on an interface that loses carrier, and IPv4 has no duplicate +address detection to stall. Adding it once is enough. The MTU is 1280 because that is the minimum IPv6 requires (RFC 8200). Linux disables IPv6 entirely on an interface below it — the per-device diff --git a/docs/wireguard.md b/docs/wireguard.md index 30b1284..fa99dfa 100644 --- a/docs/wireguard.md +++ b/docs/wireguard.md @@ -271,7 +271,13 @@ async fn main() -> Result<()> { attached it has no carrier, and Linux then flushes its IPv6 addresses. The setup printed by `tsunagi tun-setup` sets it; the agent checks the address is present *and usable* — not tentative, not DAD-failed — before attaching, and - reports what it actually found. + reports what it actually found. This applies to IPv6 only: IPv4 addresses + survive carrier loss, so the overlay IPv4 address is added once and stays. +* **The overlay IPv4 address is not derivable, so `tun-setup` cannot print it + on a fresh state directory.** It is allocated and signed at run time, so the + first `tsunagi up` is what names it; afterwards `tun-setup` reads it back + from `state.sqlite` — a read that takes no directory lock and so does not + disturb a running agent — and includes the `ip address add` line. * **The agent cannot assign the overlay address itself.** The `tun` crate sets addresses through an IPv4-only ioctl, so the IPv6 overlay address must come from `ip -6 address add` or an equivalent. The agent verifies the address is diff --git a/src/bin/tsunagi.rs b/src/bin/tsunagi.rs index e1b8973..4c2b91f 100644 --- a/src/bin/tsunagi.rs +++ b/src/bin/tsunagi.rs @@ -344,6 +344,31 @@ async fn run(command: Command) -> Result<(), Box> { } } +/// The IPv4 address this agent has already been allocated, if any. +/// +/// Read straight from the mandatory state store. Opening it for reading does +/// not take the directory lock, so this works while the agent is running. +/// Records are verified here too: the database is not a trust boundary. +fn allocated_ipv4( + paths: &StoragePaths, + network: tsunagi::NetworkId, +) -> Result, Box> { + use tsunagi::state::RecordBody; + use tsunagi::storage::StateStore; + + let store = StateStore::open(paths.state_db())?; + let author = store.load_or_create_device_identity()?.endpoint_id(); + for record in store.signed_records(network)? { + if record.author != *author.as_bytes() || record.verify(network).is_err() { + continue; + } + if let RecordBody::Ipv4Claim { address, range } = record.body { + return Ok(Some((address, range.prefix_len))); + } + } + Ok(None) +} + /// Path of the local control socket for a state directory. fn control_socket(paths: &StoragePaths, override_path: Option<&PathBuf>) -> PathBuf { match override_path { @@ -406,12 +431,23 @@ async fn tun_setup(args: TunSetupArgs) -> Result<(), Box> println!("# Network {name} ({network})"); println!("# Interface {interface}, address {address}/{OVERLAY_PREFIX_LEN}, mtu {mtu}"); - if ipv4_range.is_some() { - println!( - "# IPv4 is allocated once the agent runs and agrees with its peers, so it\n\ - # cannot be printed here. Start `tsunagi up`; it prints the exact\n\ - # `ip address add` command for the address it was given." - ); + // The IPv4 address is allocated at run time, so it can only be shown once + // the agent has one. Reading the state store does not disturb a running + // agent: the directory lock belongs to the agent, not to this reader. + let allocated_v4 = ipv4_range.and_then(|_| allocated_ipv4(&paths, network).ok().flatten()); + match (ipv4_range, allocated_v4) { + (Some(_), Some((address, prefix_len))) => { + println!("# IPv4 overlay address {address}/{prefix_len}, allocated and signed"); + } + (Some(_), None) => { + println!( + "# IPv4 is allocated once the agent runs and agrees with its peers, so\n\ + # there is nothing to print yet. Start `tsunagi up`: it prints the exact\n\ + # `ip address add` command for the address it was given, and this\n\ + # command will include it from then on." + ); + } + (None, _) => {} } println!("# Run once as root; then run `tsunagi up` as {user}."); println!( @@ -426,9 +462,14 @@ async fn tun_setup(args: TunSetupArgs) -> Result<(), Box> println!("sudo ip link set dev {interface} mtu {mtu} up"); println!("sudo sysctl -qw net.ipv6.conf.{interface}.keep_addr_on_down=1"); println!("sudo ip -6 address add {address}/{OVERLAY_PREFIX_LEN} dev {interface} nodad"); + if let Some((address, prefix_len)) = allocated_v4 { + // IPv4 addresses are not flushed when an interface loses carrier, so + // this one needs none of the treatment IPv6 does. + println!("sudo ip address add {address}/{prefix_len} dev {interface}"); + } println!("\n# To check it afterwards:"); - println!("ip -6 addr show dev {interface}"); + println!("ip addr show dev {interface}"); println!("\n# To remove it again:"); println!("sudo ip link del dev {interface}"); Ok(()) diff --git a/tests/multi_peer.rs b/tests/multi_peer.rs index 14d4723..b42011e 100644 --- a/tests/multi_peer.rs +++ b/tests/multi_peer.rs @@ -42,12 +42,19 @@ async fn four_agents_form_a_mesh_and_exchange_distinguishable_messages() { } // Each peer announced its own hostname, so sessions are distinguishable. - let status = agents[0].agent.network_status(network_id).await.unwrap(); - let hostnames: HashSet = status - .peers - .iter() - .filter_map(|peer| peer.hostname.clone()) - .collect(); + // A peer counts as connected as soon as its session is authenticated, which + // can be a round before its announcement carrying the hostname arrives, so + // this waits for the hostnames rather than reading them straight away. + let hostnames: HashSet = wait_until("three distinct peer hostnames", || async { + let status = agents[0].agent.network_status(network_id).await.ok()?; + let hostnames: HashSet = status + .peers + .iter() + .filter_map(|peer| peer.hostname.clone()) + .collect(); + (hostnames.len() >= 3).then_some(hostnames) + }) + .await; assert_eq!( hostnames.len(), 3,