Make the IPv4 overlay opt-in and detect a range mismatch

100.64.0.0/10 was a bad default: it is exactly Tailscale's range, and
carrier-grade NAT's. There is no IPv4 range that is free on every host, so
there is now no default at all — IPv4 is off until --ipv4-range names one.
IPv6 is unaffected and still works out of the box, because a ULA derived
from the network id collides with essentially nothing.

The more serious problem this exposed: the range is an input to the address
derivation, and each agent derives every peer's address itself. Two members
configured with different ranges would therefore derive different addresses
for each other and IPv4 would silently misroute. So the range now travels
in the announcement — not as a request and never trusted, only so the
mismatch is seen. A peer whose range disagrees gets no IPv4 address here,
keeps working over IPv6, and the reason is reported with both ranges named.

The announcement format goes to version 2. postcard is not
self-describing, so an older peer cannot read it; the version check already
catches that and now says which side needs updating.

The (Ipv4Addr, u8) tuple that had spread across six modules is now an
Ipv4Range with validation, Display and FromStr, so a bad --ipv4-range is
refused with a reason instead of being accepted and misbehaving later. It
is also rejected when passed without --wireguard rather than ignored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
tsunagi
2026-09-21 13:11:09 +01:00
co-authored by Claude Opus 5
parent cfab38824d
commit ce64264027
10 changed files with 419 additions and 141 deletions
+9 -5
View File
@@ -12,7 +12,7 @@
//! * `SystemTun`, behind the `tun-device` feature, is a real TUN interface.
//! Creating one needs `CAP_NET_ADMIN` on Linux or the equivalent elsewhere.
use std::net::{Ipv4Addr, Ipv6Addr};
use std::net::Ipv6Addr;
use std::sync::Arc;
use bytes::Bytes;
@@ -29,8 +29,10 @@ pub struct TunRequest {
pub address: Ipv6Addr,
/// Prefix length of the overlay subnet, so the OS routes it here.
pub prefix_len: u8,
/// The IPv4 overlay address and its prefix length, when dual stack.
pub address_v4: Option<(Ipv4Addr, u8)>,
/// The IPv4 overlay address this host answers to, when dual stack.
pub address_v4: Option<std::net::Ipv4Addr>,
/// Prefix length of the IPv4 overlay range.
pub prefix_len_v4: u8,
/// Interface MTU.
pub mtu: u32,
}
@@ -401,7 +403,8 @@ mod system {
request.address, request.prefix_len, request.name
),
];
if let Some((address, prefix_len)) = request.address_v4 {
if let Some(address) = request.address_v4 {
let prefix_len = request.prefix_len_v4;
// IPv4 is not sensitive to carrier the way IPv6 is, so it needs
// no extra settings.
commands.push(format!(
@@ -589,7 +592,8 @@ fd559caf9652cb86321feac65c73bd84 05 40 00 08 tsun0
name: "tsun0".into(),
address: "fd00::1".parse().unwrap(),
prefix_len: 64,
address_v4: Some(("100.64.1.2".parse().unwrap(), 10)),
address_v4: Some("100.64.1.2".parse().unwrap()),
prefix_len_v4: 10,
mtu: 1280,
};
let commands = setup_commands(&request, "someone");