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:
1 parent
cfab38824d
commit
ce64264027
10 files changed
+419
-141
No files matched your search
@@ -22,8 +22,11 @@
|
||||
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::dataplane::PluginError;
|
||||
|
||||
use crate::identity::NetworkId;
|
||||
|
||||
use super::keys::WgPublicKey;
|
||||
@@ -37,12 +40,76 @@ pub const OVERLAY_PREFIX_LEN: u8 = 64;
|
||||
/// Prefix length of one member's address inside the overlay.
|
||||
pub const OVERLAY_HOST_PREFIX_LEN: u8 = 128;
|
||||
|
||||
/// Default IPv4 overlay range: RFC 6598 shared address space.
|
||||
/// An IPv4 range the overlay can be derived into.
|
||||
///
|
||||
/// Deliberately not RFC 1918, so it rarely collides with the home or office
|
||||
/// network the machine is already on. It can collide with a carrier-grade NAT
|
||||
/// that uses the same range, which is why it is configurable.
|
||||
pub const DEFAULT_IPV4_RANGE: (Ipv4Addr, u8) = (Ipv4Addr::new(100, 64, 0, 0), 10);
|
||||
/// Every member of a network must be configured with the same one, because
|
||||
/// addresses are derived from it. See [`crate::dataplane::wireguard::plugin::WireguardConfig::ipv4_range`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Ipv4Range {
|
||||
/// Base address of the range.
|
||||
pub base: Ipv4Addr,
|
||||
/// Prefix length, at most 30 so there is room for hosts.
|
||||
pub prefix_len: u8,
|
||||
}
|
||||
|
||||
impl Ipv4Range {
|
||||
/// Builds a range, rejecting one with no room for hosts.
|
||||
pub fn new(base: Ipv4Addr, prefix_len: u8) -> Result<Self, PluginError> {
|
||||
if prefix_len > 30 {
|
||||
return Err(PluginError::Rejected(format!(
|
||||
"a /{prefix_len} has no room for hosts; use /30 or larger"
|
||||
)));
|
||||
}
|
||||
Ok(Self { base, prefix_len })
|
||||
}
|
||||
|
||||
/// Whether an address falls inside the range.
|
||||
pub fn contains(&self, address: Ipv4Addr) -> bool {
|
||||
let host_bits = 32 - u32::from(self.prefix_len);
|
||||
let mask = if host_bits >= 32 {
|
||||
0
|
||||
} else {
|
||||
u32::MAX << host_bits
|
||||
};
|
||||
u32::from(address) & mask == u32::from(self.base) & mask
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Ipv4Range {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}/{}", self.base, self.prefix_len)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Ipv4Range {
|
||||
type Err = PluginError;
|
||||
|
||||
fn from_str(text: &str) -> Result<Self, Self::Err> {
|
||||
let (base, prefix) = text.split_once('/').ok_or_else(|| {
|
||||
PluginError::Rejected(format!(
|
||||
"`{text}` is not an address with a prefix, for example 10.77.0.0/16"
|
||||
))
|
||||
})?;
|
||||
let base = base.parse().map_err(|err| {
|
||||
PluginError::Rejected(format!("`{base}` is not an IPv4 address: {err}"))
|
||||
})?;
|
||||
let prefix_len = prefix.parse().map_err(|err| {
|
||||
PluginError::Rejected(format!("`{prefix}` is not a prefix length: {err}"))
|
||||
})?;
|
||||
Self::new(base, prefix_len)
|
||||
}
|
||||
}
|
||||
|
||||
/// RFC 6598 shared address space, offered only as a reference point.
|
||||
///
|
||||
/// **Not a default, and usually a bad choice.** Tailscale uses exactly this
|
||||
/// range, and so does carrier-grade NAT, so a machine running either will
|
||||
/// collide with it. There is no IPv4 range that is free on every host, which
|
||||
/// is why the IPv4 overlay has no default at all and must be configured.
|
||||
pub const RFC6598_SHARED_RANGE: Ipv4Range = Ipv4Range {
|
||||
base: Ipv4Addr::new(100, 64, 0, 0),
|
||||
prefix_len: 10,
|
||||
};
|
||||
|
||||
fn push_lp(out: &mut Vec<u8>, bytes: &[u8]) {
|
||||
let len = u32::try_from(bytes.len()).unwrap_or(u32::MAX);
|
||||
@@ -107,9 +174,9 @@ pub fn overlay_address(network: NetworkId, key: &WgPublicKey) -> Ipv6Addr {
|
||||
pub fn overlay_address_v4(
|
||||
network: NetworkId,
|
||||
key: &WgPublicKey,
|
||||
range: (Ipv4Addr, u8),
|
||||
range: Ipv4Range,
|
||||
) -> Option<Ipv4Addr> {
|
||||
let (base, prefix_len) = range;
|
||||
let Ipv4Range { base, prefix_len } = range;
|
||||
if prefix_len > 32 {
|
||||
return None;
|
||||
}
|
||||
@@ -185,14 +252,14 @@ mod tests {
|
||||
#[test]
|
||||
fn ipv4_addresses_land_inside_the_range_and_avoid_its_edges() {
|
||||
let id = network("v4");
|
||||
let range = DEFAULT_IPV4_RANGE;
|
||||
let range = RFC6598_SHARED_RANGE;
|
||||
for byte in 0..64u8 {
|
||||
let key = WgPublicKey::from_bytes([byte; 32]);
|
||||
let addr = overlay_address_v4(id, &key, range).unwrap();
|
||||
let raw = u32::from(addr);
|
||||
assert_eq!(
|
||||
raw & 0xffc0_0000,
|
||||
u32::from(range.0),
|
||||
u32::from(range.base),
|
||||
"outside 100.64.0.0/10"
|
||||
);
|
||||
// Never the network address and never the broadcast address.
|
||||
@@ -201,12 +268,25 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_range_parses_and_prints_round_trip() {
|
||||
let range: Ipv4Range = "10.77.0.0/16".parse().unwrap();
|
||||
assert_eq!(range.to_string(), "10.77.0.0/16");
|
||||
assert!(range.contains("10.77.3.4".parse().unwrap()));
|
||||
assert!(!range.contains("10.78.3.4".parse().unwrap()));
|
||||
|
||||
assert!("10.77.0.0".parse::<Ipv4Range>().is_err());
|
||||
assert!("nonsense/16".parse::<Ipv4Range>().is_err());
|
||||
assert!("10.77.0.0/zz".parse::<Ipv4Range>().is_err());
|
||||
assert!("10.77.0.0/31".parse::<Ipv4Range>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv4_derivation_is_deterministic_and_scoped_like_ipv6() {
|
||||
let key = WgPublicKey::from_bytes([9u8; 32]);
|
||||
let first = network("one");
|
||||
let second = network("two");
|
||||
let range = DEFAULT_IPV4_RANGE;
|
||||
let range = RFC6598_SHARED_RANGE;
|
||||
|
||||
assert_eq!(
|
||||
overlay_address_v4(first, &key, range),
|
||||
@@ -223,7 +303,11 @@ mod tests {
|
||||
// A different range moves everybody.
|
||||
assert_ne!(
|
||||
overlay_address_v4(first, &key, range),
|
||||
overlay_address_v4(first, &key, (Ipv4Addr::new(10, 0, 0, 0), 8))
|
||||
overlay_address_v4(
|
||||
first,
|
||||
&key,
|
||||
Ipv4Range::new(Ipv4Addr::new(10, 0, 0, 0), 8).unwrap()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -231,14 +315,16 @@ mod tests {
|
||||
fn a_range_with_no_room_yields_nothing() {
|
||||
let id = network("tiny");
|
||||
let key = WgPublicKey::from_bytes([1u8; 32]);
|
||||
// /31 and /32 have no usable host addresses.
|
||||
assert!(overlay_address_v4(id, &key, (Ipv4Addr::new(10, 0, 0, 0), 31)).is_none());
|
||||
assert!(overlay_address_v4(id, &key, (Ipv4Addr::new(10, 0, 0, 1), 32)).is_none());
|
||||
assert!(overlay_address_v4(id, &key, (Ipv4Addr::new(10, 0, 0, 0), 33)).is_none());
|
||||
// /31 and /32 have no usable host addresses, so they are refused.
|
||||
assert!(Ipv4Range::new(Ipv4Addr::new(10, 0, 0, 0), 31).is_err());
|
||||
assert!(Ipv4Range::new(Ipv4Addr::new(10, 0, 0, 1), 32).is_err());
|
||||
assert!(Ipv4Range::new(Ipv4Addr::new(10, 0, 0, 0), 33).is_err());
|
||||
// A /30 has two usable addresses.
|
||||
assert!(overlay_address_v4(id, &key, (Ipv4Addr::new(10, 0, 0, 0), 30)).is_some());
|
||||
let small = Ipv4Range::new(Ipv4Addr::new(10, 0, 0, 0), 30).unwrap();
|
||||
assert!(overlay_address_v4(id, &key, small).is_some());
|
||||
// A /0 must not overflow.
|
||||
assert!(overlay_address_v4(id, &key, (Ipv4Addr::UNSPECIFIED, 0)).is_some());
|
||||
let everything = Ipv4Range::new(Ipv4Addr::UNSPECIFIED, 0).unwrap();
|
||||
assert!(overlay_address_v4(id, &key, everything).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user