Print the allocated IPv4 address in tun-setup

The overlay IPv4 address is not derived from the keys: it is allocated at
run time and signed, so on a fresh state directory there is nothing for
tun-setup to print. Once the agent has run, the claim is in state.sqlite,
and reading it back takes no directory lock, so tun-setup can show the
`ip address add` line while the agent is running. Records are verified on
the way out; the database is not a trust boundary.

The line needs no keep_addr_on_down and no nodad, unlike its IPv6
counterpart: Linux keeps IPv4 addresses on an interface that has lost
carrier, and IPv4 has no duplicate address detection to stall.

Also fix a race in the four-agent test. A peer counts as connected once
its session authenticates, which can precede the announcement carrying
its hostname, so reading the hostnames straight away occasionally saw
only two. It now waits for them like every other success condition.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
tsunagi
2026-09-21 14:04:44 +01:00
co-authored by Claude Opus 5
parent 8b333455f1
commit 944d98389f
4 changed files with 80 additions and 16 deletions
+48 -7
View File
@@ -344,6 +344,31 @@ async fn run(command: Command) -> Result<(), Box<dyn std::error::Error>> {
}
}
/// 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<Option<(std::net::Ipv4Addr, u8)>, Box<dyn std::error::Error>> {
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<dyn std::error::Error>>
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<dyn std::error::Error>>
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(())