Report an allocated IPv4 address that is not on the host

IPv6 works end to end between two machines; IPv4 silently did not, and the
agent said nothing useful about why.

Allocation moved the address from something derivable before startup to
something agreed at run time, so an interface configured by an earlier
`tun-setup` carries a different address than the one allocated. The kernel
then sends packets with that stale source and every peer drops them as not
belonging to us — correct behaviour, invisible cause. Meanwhile pings to
our own allocated address fall into the tunnel and land in the "nobody
owns this" counter.

The agent now checks whether its allocated address is assigned anywhere on
the host — by binding a UDP socket to it, which needs no privileges and no
platform code — and reports the exact `ip address add` command until it
is, mentioning that another address of the range has to go.

`tun-setup` no longer prints a derived IPv4 address, because that number
is now wrong by construction. It says the agent will print the real one.

The unroutable counter keeps one destination as a sample, in status output
too. A bare count says something is wrong; the address says what.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
tsunagi
2026-09-21 13:53:42 +01:00
co-authored by Claude Opus 5
parent 84c06c6cac
commit 8b333455f1
9 changed files with 162 additions and 18 deletions
+42
View File
@@ -504,6 +504,48 @@ async fn a_joining_member_adopts_the_range_the_network_already_uses() {
b.shutdown().await;
}
#[tokio::test]
async fn an_allocated_address_missing_from_the_host_is_reported() {
let discovery = SharedMemoryDiscovery::new();
let (name, secret) = network("wg-missing-address");
// The in-memory interface never carries the address, which is exactly
// the situation of a real interface the operator has not configured yet.
// Left unsaid, packets leave with the wrong source and every peer drops
// them, which looks like a broken network rather than a missing command.
let a = WgAgent::spawn(&discovery, "ta").await;
let b = WgAgent::spawn(&discovery, "tb").await;
let mut events = a.agent.subscribe();
let network_id = a.agent.join_network(&name, &secret).await.unwrap();
b.agent.join_network(&name, &secret).await.unwrap();
a.wait_for_tunnels(network_id, 1).await;
let reason = wait_event(&mut events, |event| match event {
Event::PluginError { reason, .. } if reason.contains("not on any") => Some(reason.clone()),
_ => None,
})
.await;
let allocated = a
.plugin
.overview(network_id)
.unwrap()
.overlay_address_v4
.unwrap();
assert!(
reason.contains(&allocated.to_string()),
"unexpected: {reason}"
);
assert!(
reason.contains("ip address add"),
"must name the fix: {reason}"
);
a.shutdown().await;
b.shutdown().await;
}
#[tokio::test]
async fn an_address_is_kept_across_a_restart() {
let discovery = SharedMemoryDiscovery::new();