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
+119 -23
View File
@@ -27,7 +27,7 @@
//! what it announced.
use std::collections::HashMap;
use std::net::Ipv6Addr;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::time::Duration;
@@ -42,7 +42,7 @@ use crate::dataplane::transport::{SharedLink, TransportError};
use crate::identity::NetworkId;
use super::keys::{WgPublicKey, WgSecretKey};
use super::overlay::overlay_address;
use super::overlay::{overlay_address, overlay_address_v4};
use super::packet::IpHeader;
use super::tun::TunDevice;
@@ -109,6 +109,9 @@ struct Peer {
endpoint_id: EndpointId,
public_key: WgPublicKey,
overlay: Ipv6Addr,
/// The IPv4 address this peer owns, when the overlay is dual stack and
/// nobody else derived the same one.
overlay_v4: Mutex<Option<Ipv4Addr>>,
tunn: Mutex<Tunn>,
link: SharedLink,
counters: Arc<PeerCounters>,
@@ -168,6 +171,13 @@ pub struct PeerSummary {
pub public_key: WgPublicKey,
/// The overlay address this agent derived for it.
pub overlay_address: Ipv6Addr,
/// Its IPv4 overlay address, when the overlay is dual stack.
///
/// `None` with `ipv4_conflict` set means another member derived the same
/// address and won it; that peer is still fully reachable over IPv6.
pub overlay_address_v4: Option<Ipv4Addr>,
/// Whether this peer lost an IPv4 address to a derivation collision.
pub ipv4_conflict: bool,
/// Whether the tunnel has handshaken.
pub health: PeerHealth,
/// Traffic counters.
@@ -182,11 +192,15 @@ struct Inner {
network: NetworkId,
private_key: WgSecretKey,
tun: Arc<dyn TunDevice>,
/// The IPv4 overlay range, when the overlay is dual stack.
ipv4_range: Option<(Ipv4Addr, u8)>,
peers: RwLock<HashMap<WgPublicKey, Arc<Peer>>>,
routes: RwLock<HashMap<Ipv6Addr, WgPublicKey>>,
/// Both families, so one lookup routes any packet.
routes: RwLock<HashMap<IpAddr, WgPublicKey>>,
next_index: AtomicU32,
unroutable: AtomicU64,
multicast: AtomicU64,
ipv4_conflicts: AtomicU64,
}
impl std::fmt::Debug for Inner {
@@ -207,16 +221,23 @@ pub struct WireguardDevice {
impl WireguardDevice {
/// Starts a device on top of `tun`.
pub fn start(network: NetworkId, private_key: WgSecretKey, tun: Arc<dyn TunDevice>) -> Self {
pub fn start(
network: NetworkId,
private_key: WgSecretKey,
tun: Arc<dyn TunDevice>,
ipv4_range: Option<(Ipv4Addr, u8)>,
) -> Self {
let inner = Arc::new(Inner {
network,
private_key,
tun,
ipv4_range,
peers: RwLock::new(HashMap::new()),
routes: RwLock::new(HashMap::new()),
next_index: AtomicU32::new(1),
unroutable: AtomicU64::new(0),
multicast: AtomicU64::new(0),
ipv4_conflicts: AtomicU64::new(0),
});
let reader = tokio::spawn(read_from_os(Arc::clone(&inner)));
@@ -263,10 +284,12 @@ impl WireguardDevice {
);
let overlay = overlay_address(self.inner.network, &public_key);
let overlay_v4 = self.claim_ipv4(&public_key);
let peer = Arc::new(Peer {
endpoint_id,
public_key,
overlay,
overlay_v4: Mutex::new(overlay_v4),
tunn: Mutex::new(tunn),
link,
counters: Arc::new(PeerCounters::default()),
@@ -279,7 +302,10 @@ impl WireguardDevice {
}
write_lock(&self.inner.peers).insert(public_key, Arc::clone(&peer));
write_lock(&self.inner.routes).insert(overlay, public_key);
write_lock(&self.inner.routes).insert(IpAddr::V6(overlay), public_key);
if let Some(v4) = overlay_v4 {
write_lock(&self.inner.routes).insert(IpAddr::V4(v4), public_key);
}
// Start the handshake now instead of waiting for the next timer tick,
// so the tunnel is usable as soon as the link exists.
@@ -287,13 +313,64 @@ impl WireguardDevice {
Ok(())
}
/// Decides which IPv4 address a new peer gets, if any.
///
/// IPv4 has far too little room for a derived address to be collision
/// free. When two members derive the same one, the member whose public
/// key sorts lower keeps it — a rule every member computes identically,
/// so they all agree on the outcome without talking about it. The other
/// member simply has no IPv4 address; it is still fully reachable over
/// IPv6, which never collides.
fn claim_ipv4(&self, public_key: &WgPublicKey) -> Option<Ipv4Addr> {
let range = self.inner.ipv4_range?;
let wanted = overlay_address_v4(self.inner.network, public_key, range)?;
let holder = read_lock(&self.inner.routes)
.get(&IpAddr::V4(wanted))
.copied();
let Some(holder) = holder else {
return Some(wanted);
};
if holder == *public_key {
return Some(wanted);
}
self.inner.ipv4_conflicts.fetch_add(1, Ordering::Relaxed);
if holder.as_bytes() <= public_key.as_bytes() {
// The peer already holding it wins.
return None;
}
// The newcomer wins; take the address away from the other peer.
if let Some(loser) = read_lock(&self.inner.peers).get(&holder).cloned() {
let mut slot = match loser.overlay_v4.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
*slot = None;
}
Some(wanted)
}
/// Removes a peer and stops its tunnel.
pub fn remove_peer(&self, public_key: &WgPublicKey) {
if let Some(peer) = write_lock(&self.inner.peers).remove(public_key) {
write_lock(&self.inner.routes).remove(&peer.overlay);
let mut routes = write_lock(&self.inner.routes);
routes.remove(&IpAddr::V6(peer.overlay));
let v4 = match peer.overlay_v4.lock() {
Ok(guard) => *guard,
Err(poisoned) => *poisoned.into_inner(),
};
if let Some(v4) = v4 {
routes.remove(&IpAddr::V4(v4));
}
}
}
/// How many IPv4 derivation collisions have been resolved.
pub fn ipv4_conflicts(&self) -> u64 {
self.inner.ipv4_conflicts.load(Ordering::Relaxed)
}
/// Removes every peer whose key is not in `keep`.
pub fn retain_peers(&self, keep: &[WgPublicKey]) {
let stale: Vec<WgPublicKey> = read_lock(&self.inner.peers)
@@ -315,14 +392,22 @@ impl WireguardDevice {
pub fn peers(&self) -> Vec<PeerSummary> {
let mut peers: Vec<PeerSummary> = read_lock(&self.inner.peers)
.values()
.map(|peer| PeerSummary {
endpoint_id: peer.endpoint_id,
public_key: peer.public_key,
overlay_address: peer.overlay,
health: peer.health(),
stats: peer.stats(),
path: peer.link.path_description(),
max_datagram: peer.link.max_datagram_size(),
.map(|peer| {
let overlay_v4 = match peer.overlay_v4.lock() {
Ok(guard) => *guard,
Err(poisoned) => *poisoned.into_inner(),
};
PeerSummary {
endpoint_id: peer.endpoint_id,
public_key: peer.public_key,
overlay_address: peer.overlay,
overlay_address_v4: overlay_v4,
ipv4_conflict: overlay_v4.is_none() && self.inner.ipv4_range.is_some(),
health: peer.health(),
stats: peer.stats(),
path: peer.link.path_description(),
max_datagram: peer.link.max_datagram_size(),
}
})
.collect();
peers.sort_by_key(|peer| peer.public_key);
@@ -415,9 +500,8 @@ async fn read_from_os(inner: Arc<Inner>) {
};
// Route by destination: only the peer that owns that overlay address
// may receive it.
let Some(destination) = IpHeader::parse(&packet).and_then(|header| header.v6_destination())
else {
// may receive it. Both families go through the same table.
let Some(destination) = IpHeader::parse(&packet).map(|header| header.destination()) else {
inner.unroutable.fetch_add(1, Ordering::Relaxed);
continue;
};
@@ -488,9 +572,11 @@ async fn read_from_link(inner: Arc<Inner>, peer: Arc<Peer>) {
match tunn.decapsulate(None, input.unwrap_or(&[]), &mut scratch) {
TunnResult::WriteToNetwork(out) => Outcome::ToNetwork(out.len()),
TunnResult::WriteToTunnelV6(out, source) => {
Outcome::ToTunnel(out.len(), Some(source))
Outcome::ToTunnel(out.len(), IpAddr::V6(source))
}
TunnResult::WriteToTunnelV4(out, source) => {
Outcome::ToTunnel(out.len(), IpAddr::V4(source))
}
TunnResult::WriteToTunnelV4(out, _) => Outcome::ToTunnel(out.len(), None),
TunnResult::Done => Outcome::Done,
TunnResult::Err(err) => {
tracing::trace!(?err, "wireguard decapsulation failed");
@@ -508,9 +594,19 @@ async fn read_from_link(inner: Arc<Inner>, peer: Arc<Peer>) {
}
Outcome::ToTunnel(len, source) => {
let payload = Bytes::copy_from_slice(&scratch[..len]);
// Enforce address ownership: a peer may only send from the
// address derived for its own key.
if source != Some(peer.overlay) {
// Enforce address ownership: a peer may only send from an
// address derived for its own key, in either family.
let owned = match source {
IpAddr::V6(addr) => addr == peer.overlay,
IpAddr::V4(addr) => {
let held = match peer.overlay_v4.lock() {
Ok(guard) => *guard,
Err(poisoned) => *poisoned.into_inner(),
};
held == Some(addr)
}
};
if !owned {
peer.counters
.dropped_wrong_source
.fetch_add(1, Ordering::Relaxed);
@@ -538,7 +634,7 @@ async fn read_from_link(inner: Arc<Inner>, peer: Arc<Peer>) {
enum Outcome {
ToNetwork(usize),
ToTunnel(usize, Option<Ipv6Addr>),
ToTunnel(usize, IpAddr),
Done,
Failed,
}
+3 -1
View File
@@ -54,7 +54,9 @@ pub use announcement::{ValidatedAnnouncement, WgAnnouncement};
pub use config::{Cidr, DEFAULT_INTERFACE_PREFIX, MAX_INTERFACE_NAME_LEN, interface_name};
pub use device::{PeerHealth, PeerStats, PeerSummary, WireguardDevice};
pub use keys::{WgPublicKey, WgSecretKey};
pub use overlay::{OVERLAY_PREFIX_LEN, overlay_address, overlay_prefix};
pub use overlay::{
DEFAULT_IPV4_RANGE, OVERLAY_PREFIX_LEN, overlay_address, overlay_address_v4, overlay_prefix,
};
pub use packet::IpHeader;
pub use plugin::{
DEFAULT_MTU, MIN_MTU, NetworkOverview, PeerOverview, WIREGUARD_OVERHEAD, WIREGUARD_PROTOCOL,
+110 -1
View File
@@ -20,7 +20,7 @@
//! but it cannot choose to collide with an existing member's address without
//! finding a hash preimage.
use std::net::Ipv6Addr;
use std::net::{Ipv4Addr, Ipv6Addr};
use sha2::{Digest, Sha256};
@@ -37,6 +37,13 @@ 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.
///
/// 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);
fn push_lp(out: &mut Vec<u8>, bytes: &[u8]) {
let len = u32::try_from(bytes.len()).unwrap_or(u32::MAX);
out.extend_from_slice(&len.to_be_bytes());
@@ -85,6 +92,49 @@ pub fn overlay_address(network: NetworkId, key: &WgPublicKey) -> Ipv6Addr {
Ipv6Addr::from(octets)
}
/// The IPv4 address a member with `key` has in `network`.
///
/// # Why this is weaker than the IPv6 derivation
///
/// A 64 bit interface identifier makes an IPv6 collision impossible in
/// practice. IPv4 has nothing like that much room, so two members *can* derive
/// the same address. In a `/10` with 50 members the chance is roughly 0.03%,
/// which is small but real, so it is detected and resolved rather than
/// assumed away — see [`super::device`]. IPv6 remains the address that always
/// works.
///
/// Returns `None` when the range has no room for hosts.
pub fn overlay_address_v4(
network: NetworkId,
key: &WgPublicKey,
range: (Ipv4Addr, u8),
) -> Option<Ipv4Addr> {
let (base, prefix_len) = range;
if prefix_len > 32 {
return None;
}
let host_bits = 32 - u32::from(prefix_len);
// A usable range needs a network address, a broadcast address and at
// least one host between them.
if host_bits < 2 {
return None;
}
let hash = digest("ipv4", network, Some(key));
let raw = u32::from_be_bytes([hash[0], hash[1], hash[2], hash[3]]);
let usable = (1u64 << host_bits) - 2;
let offset = (u64::from(raw) % usable) + 1;
let mask = if host_bits == 32 {
0
} else {
u32::MAX << host_bits
};
let network_part = u32::from(base) & mask;
Some(Ipv4Addr::from(network_part | offset as u32))
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
@@ -132,6 +182,65 @@ mod tests {
assert_ne!(overlay_prefix(first), overlay_prefix(second));
}
#[test]
fn ipv4_addresses_land_inside_the_range_and_avoid_its_edges() {
let id = network("v4");
let range = DEFAULT_IPV4_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),
"outside 100.64.0.0/10"
);
// Never the network address and never the broadcast address.
assert_ne!(raw & 0x003f_ffff, 0);
assert_ne!(raw & 0x003f_ffff, 0x003f_ffff);
}
}
#[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;
assert_eq!(
overlay_address_v4(first, &key, range),
overlay_address_v4(first, &key, range)
);
assert_ne!(
overlay_address_v4(first, &key, range),
overlay_address_v4(second, &key, range)
);
assert_ne!(
overlay_address_v4(first, &key, range),
overlay_address_v4(first, &WgPublicKey::from_bytes([10u8; 32]), range)
);
// 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))
);
}
#[test]
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());
// A /30 has two usable addresses.
assert!(overlay_address_v4(id, &key, (Ipv4Addr::new(10, 0, 0, 0), 30)).is_some());
// A /0 must not overflow.
assert!(overlay_address_v4(id, &key, (Ipv4Addr::UNSPECIFIED, 0)).is_some());
}
#[test]
fn addresses_are_never_the_subnet_router_anycast_address() {
let id = network("anycast");
+16
View File
@@ -65,6 +65,22 @@ impl IpHeader {
IpHeader::V4 { .. } => None,
}
}
/// The destination, whichever family it is.
pub fn destination(&self) -> std::net::IpAddr {
match self {
IpHeader::V4 { destination, .. } => std::net::IpAddr::V4(*destination),
IpHeader::V6 { destination, .. } => std::net::IpAddr::V6(*destination),
}
}
/// The source, whichever family it is.
pub fn source(&self) -> std::net::IpAddr {
match self {
IpHeader::V4 { source, .. } => std::net::IpAddr::V4(*source),
IpHeader::V6 { source, .. } => std::net::IpAddr::V6(*source),
}
}
}
#[cfg(test)]
+41 -3
View File
@@ -25,7 +25,7 @@
//! A failure here is reported and retried. It never stops the control plane.
use std::collections::{BTreeSet, HashMap};
use std::net::IpAddr;
use std::net::{IpAddr, Ipv4Addr};
use std::path::PathBuf;
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;
@@ -43,7 +43,9 @@ use super::announcement::{ValidatedAnnouncement, WgAnnouncement};
use super::config::{DEFAULT_INTERFACE_PREFIX, interface_name};
use super::device::{PeerSummary, WireguardDevice};
use super::keys::{WgPublicKey, WgSecretKey};
use super::overlay::{OVERLAY_PREFIX_LEN, overlay_address, overlay_prefix};
use super::overlay::{
DEFAULT_IPV4_RANGE, OVERLAY_PREFIX_LEN, overlay_address, overlay_address_v4, overlay_prefix,
};
use super::store::WgKeyStore;
use super::tun::{TunFactory, TunRequest};
@@ -86,6 +88,12 @@ pub struct WireguardConfig {
pub keepalive: Option<u16>,
/// Interface MTU. See [`DEFAULT_MTU`].
pub mtu: u32,
/// IPv4 overlay range, or `None` for an IPv6-only overlay.
///
/// IPv6 addresses are derived collision-free; IPv4 ones cannot be, so a
/// collision is detected and resolved deterministically instead. See
/// `docs/wireguard.md`.
pub ipv4_range: Option<(Ipv4Addr, u8)>,
/// How long to coalesce changes before reconciling.
pub reconcile_debounce: Duration,
/// How often to reconcile anyway, which is also when a packet interface
@@ -101,6 +109,7 @@ impl WireguardConfig {
interface_prefix: DEFAULT_INTERFACE_PREFIX.to_string(),
keepalive: Some(25),
mtu: DEFAULT_MTU,
ipv4_range: Some(DEFAULT_IPV4_RANGE),
reconcile_debounce: Duration::from_millis(200),
reconcile_interval: Duration::from_secs(15),
}
@@ -120,6 +129,12 @@ impl WireguardConfig {
self
}
/// Sets the IPv4 overlay range, or disables IPv4 with `None`.
pub fn with_ipv4_range(mut self, range: Option<(Ipv4Addr, u8)>) -> Self {
self.ipv4_range = range;
self
}
/// Sets the reconciliation timings.
pub fn with_reconcile(mut self, debounce: Duration, interval: Duration) -> Self {
self.reconcile_debounce = debounce;
@@ -150,6 +165,10 @@ pub struct NetworkOverview {
pub overlay_prefix: IpAddr,
/// Prefix length of the overlay subnet.
pub overlay_prefix_len: u8,
/// This agent's IPv4 overlay address, when the overlay is dual stack.
pub overlay_address_v4: Option<Ipv4Addr>,
/// The IPv4 overlay range in use.
pub ipv4_range: Option<(Ipv4Addr, u8)>,
/// Peers this agent knows about.
pub peers: Vec<PeerOverview>,
/// Unicast packets the operating system sent to an address no peer owns.
@@ -174,6 +193,8 @@ pub struct PeerOverview {
pub public_key: WgPublicKey,
/// The overlay address derived for it locally.
pub overlay_address: IpAddr,
/// Its IPv4 overlay address, once a tunnel exists and it won the address.
pub overlay_address_v4: Option<Ipv4Addr>,
/// Whether a data plane link to it exists.
pub has_link: bool,
/// The running tunnel, once there is a link.
@@ -309,6 +330,9 @@ impl WireguardPlugin {
endpoint_id: *endpoint_id,
public_key: announcement.public_key,
overlay_address: IpAddr::V6(announcement.overlay_address),
overlay_address_v4: tunnels
.get(&announcement.public_key)
.and_then(|tunnel| tunnel.overlay_address_v4),
has_link: state.links.contains_key(endpoint_id),
tunnel: tunnels.get(&announcement.public_key).cloned(),
})
@@ -323,6 +347,12 @@ impl WireguardPlugin {
overlay_address: IpAddr::V6(overlay_address(network, &state.key.public())),
overlay_prefix: IpAddr::V6(overlay_prefix(network)),
overlay_prefix_len: OVERLAY_PREFIX_LEN,
overlay_address_v4: self
.worker
.config
.ipv4_range
.and_then(|range| overlay_address_v4(network, &state.key.public(), range)),
ipv4_range: self.worker.config.ipv4_range,
peers,
unroutable_packets: state
.device
@@ -437,10 +467,18 @@ impl Worker {
name: name.clone(),
address: overlay_address(network, &key.public()),
prefix_len: OVERLAY_PREFIX_LEN,
address_v4: self.config.ipv4_range.and_then(|range| {
overlay_address_v4(network, &key.public(), range).map(|address| (address, range.1))
}),
mtu: self.config.mtu,
};
let tun = self.tun_factory.create(request).await?;
let device = Arc::new(WireguardDevice::start(network, key, tun));
let device = Arc::new(WireguardDevice::start(
network,
key,
tun,
self.config.ipv4_range,
));
let mut shared = self.lock_shared();
if let Some(state) = shared.networks.get_mut(&network) {
+30 -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::Ipv6Addr;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::sync::Arc;
use bytes::Bytes;
@@ -29,6 +29,8 @@ 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)>,
/// Interface MTU.
pub mtu: u32,
}
@@ -381,7 +383,7 @@ mod system {
/// reason: duplicate address detection can never finish with no carrier,
/// and the address would stay tentative and unusable.
pub fn setup_commands(request: &TunRequest, user: &str) -> Vec<String> {
vec![
let mut commands = vec![
format!(
"sudo ip tuntap add dev {} mode tun user {user}",
request.name
@@ -398,7 +400,16 @@ mod system {
"sudo ip -6 address add {}/{} dev {} nodad",
request.address, request.prefix_len, request.name
),
]
];
if let Some((address, prefix_len)) = request.address_v4 {
// IPv4 is not sensitive to carrier the way IPv6 is, so it needs
// no extra settings.
commands.push(format!(
"sudo ip address add {address}/{prefix_len} dev {}",
request.name
));
}
commands
}
fn current_user() -> String {
@@ -578,7 +589,8 @@ fd559caf9652cb86321feac65c73bd84 05 40 00 08 tsun0
name: "tsun0".into(),
address: "fd00::1".parse().unwrap(),
prefix_len: 64,
mtu: 1100,
address_v4: Some(("100.64.1.2".parse().unwrap(), 10)),
mtu: 1280,
};
let commands = setup_commands(&request, "someone");
@@ -586,11 +598,24 @@ fd559caf9652cb86321feac65c73bd84 05 40 00 08 tsun0
// must survive losing carrier, and it must not wait for duplicate
// address detection that can never complete.
let joined = commands.join("\n");
let up = joined.find("link set dev tsun0 mtu 1100 up").unwrap();
let up = joined.find("link set dev tsun0 mtu 1280 up").unwrap();
let keep = joined.find("keep_addr_on_down=1").unwrap();
let add = joined.find("address add fd00::1/64").unwrap();
assert!(up < keep && keep < add, "wrong order:\n{joined}");
assert!(joined.contains("nodad"));
assert!(joined.contains("user someone"));
// IPv4 needs no carrier tricks, just the address.
assert!(joined.contains("ip address add 100.64.1.2/10 dev tsun0"));
// An IPv6-only overlay says nothing about IPv4.
let v6_only = TunRequest {
address_v4: None,
..request
};
assert!(
!setup_commands(&v6_only, "someone")
.join("\n")
.contains("100.64")
);
}
}