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:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user