Make the overlay dual stack

Every member now also derives an IPv4 address, from the same inputs as its
IPv6 one, into 100.64.0.0/10 by default. The range is configurable and IPv4
can be turned off with --no-ipv4.

IPv4 is honestly weaker than IPv6 here and the code says so. A 64 bit
interface identifier makes an IPv6 collision impossible in practice; IPv4
has nothing like that room, and in a /10 with 50 members two will derive the
same address about 0.03% of the time. A mesh with no coordinator cannot
allocate around that, so a collision is detected and resolved instead: the
member whose public key sorts lower keeps the address, a rule every member
computes identically and therefore agrees on. The other keeps IPv6 and is
flagged in the status. IPv6 always works; IPv4 almost always works and
degrades predictably.

Routing and address-ownership enforcement now cover both families: a packet
goes to the peer that owns its destination, and a decrypted packet is
dropped unless its source is an address derived for the peer that sent it,
IPv4 included.

Six new tests, among them a real IPv4 packet crossing a tunnel next to an
IPv6 one, a spoofed IPv4 source being dropped, and an IPv6-only overlay.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
tsunagi
2026-09-21 13:00:35 +01:00
co-authored by Claude Opus 5
parent be459e5bd0
commit cfab38824d
12 changed files with 638 additions and 46 deletions
+14 -2
View File
@@ -136,6 +136,8 @@ pub struct OverlayReport {
pub mtu: u32,
/// This agent's overlay address.
pub address: String,
/// This agent's IPv4 overlay address, when the overlay is dual stack.
pub address_v4: Option<String>,
/// The subnet every member shares.
pub prefix: String,
/// Prefix length of that subnet.
@@ -155,6 +157,8 @@ pub struct OverlayPeerReport {
pub public_key: String,
/// Its overlay address.
pub address: String,
/// Its IPv4 overlay address, when it has one.
pub address_v4: Option<String>,
/// Seconds since the last WireGuard handshake.
///
/// `None` means the tunnel has never handshaken and cannot carry traffic.
@@ -229,10 +233,14 @@ impl StatusReport {
let up = overlay.peers.iter().filter(|peer| peer.is_up()).count();
let _ = writeln!(
out,
" overlay {} {}/{} mtu {} {}/{} tunnel(s) up",
" overlay {} {}/{}{} mtu {} {}/{} tunnel(s) up",
overlay.interface,
overlay.address,
overlay.prefix_len,
match &overlay.address_v4 {
Some(v4) => format!(" and {v4}"),
None => String::new(),
},
overlay.mtu,
up,
overlay.peers.len()
@@ -240,9 +248,13 @@ impl StatusReport {
for peer in &overlay.peers {
let _ = writeln!(
out,
" {} {} {} tx {} rx {}{} {}",
" {} {}{} {} tx {} rx {}{} {}",
&peer.public_key[..8.min(peer.public_key.len())],
peer.address,
match &peer.address_v4 {
Some(v4) => format!(" / {v4}"),
None => String::new(),
},
match peer.handshake_secs_ago {
Some(secs) => format!("handshake {secs}s ago"),
None => "NOT HANDSHAKEN".to_string(),