use std::{
fmt,
net::{IpAddr, SocketAddr},
str::FromStr,
time::Duration,
};
use super::types::{AllowedIp, WG_KEY_LEN, WgDevice, WgPeer, WgPeerBuilder};
use crate::{Connection, Error, Result, netlink::protocol::Wireguard};
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct PublicKey(pub [u8; WG_KEY_LEN]);
impl PublicKey {
pub fn from_bytes(bytes: [u8; WG_KEY_LEN]) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8; WG_KEY_LEN] {
&self.0
}
}
impl From<[u8; WG_KEY_LEN]> for PublicKey {
fn from(bytes: [u8; WG_KEY_LEN]) -> Self {
Self(bytes)
}
}
impl From<PublicKey> for [u8; WG_KEY_LEN] {
fn from(pk: PublicKey) -> Self {
pk.0
}
}
impl fmt::Debug for PublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "PublicKey({self})")
}
}
impl fmt::Display for PublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&b64_encode_32(&self.0))
}
}
impl FromStr for PublicKey {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
b64_decode_32(s)
.map(Self)
.ok_or_else(|| Error::InvalidMessage(format!("invalid WireGuard public key: {s:?}")))
}
}
fn b64_encode_32(bytes: &[u8; WG_KEY_LEN]) -> String {
const ALPHA: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(44);
for chunk in bytes.chunks(3) {
match chunk.len() {
3 => {
let n = ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8) | chunk[2] as u32;
out.push(ALPHA[((n >> 18) & 0x3f) as usize] as char);
out.push(ALPHA[((n >> 12) & 0x3f) as usize] as char);
out.push(ALPHA[((n >> 6) & 0x3f) as usize] as char);
out.push(ALPHA[(n & 0x3f) as usize] as char);
}
2 => {
let n = ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8);
out.push(ALPHA[((n >> 18) & 0x3f) as usize] as char);
out.push(ALPHA[((n >> 12) & 0x3f) as usize] as char);
out.push(ALPHA[((n >> 6) & 0x3f) as usize] as char);
out.push('=');
}
_ => unreachable!("32 % 3 = 2"),
}
}
out
}
fn b64_decode_32(s: &str) -> Option<[u8; WG_KEY_LEN]> {
let trimmed = s.trim_end_matches('=');
if trimmed.len() != 43 {
return None;
}
let mut out = [0u8; WG_KEY_LEN];
let mut buf = 0u32;
let mut bits = 0u32;
let mut written = 0usize;
for ch in trimmed.chars() {
let v = match ch {
'A'..='Z' => ch as u32 - 'A' as u32,
'a'..='z' => ch as u32 - 'a' as u32 + 26,
'0'..='9' => ch as u32 - '0' as u32 + 52,
'+' => 62,
'/' => 63,
_ => return None,
};
buf = (buf << 6) | v;
bits += 6;
if bits >= 8 {
bits -= 8;
out[written] = ((buf >> bits) & 0xff) as u8;
written += 1;
if written == 32 {
break;
}
}
}
(written == 32).then_some(out)
}
#[derive(Debug, Clone, Default)]
#[must_use = "WireguardConfig does nothing unless .diff() or .apply() is called"]
pub struct WireguardConfig {
devices: Vec<DeclaredWgDevice>,
}
impl WireguardConfig {
pub fn new() -> Self {
Self::default()
}
pub fn device(
mut self,
ifname: impl Into<String>,
f: impl FnOnce(DeclaredWgDeviceBuilder) -> DeclaredWgDeviceBuilder,
) -> Self {
let builder = f(DeclaredWgDeviceBuilder::new(ifname.into()));
self.devices.push(builder.build());
self
}
pub fn devices(&self) -> &[DeclaredWgDevice] {
&self.devices
}
pub async fn diff(&self, conn: &Connection<Wireguard>) -> Result<WireguardConfigDiff> {
let mut diff = WireguardConfigDiff::default();
for declared in &self.devices {
let current = conn.get_device_by_name(&declared.ifname).await?;
let device_changes = declared.diff_against(¤t);
if !device_changes.is_empty() {
diff.devices_to_modify
.push((declared.ifname.clone(), device_changes));
}
}
Ok(diff)
}
pub async fn apply_reconcile(
&self,
conn: &Connection<Wireguard>,
opts: crate::netlink::nftables::config::ReconcileOptions,
) -> Result<crate::netlink::nftables::config::ReconcileReport> {
let mut attempt: usize = 0;
loop {
match self.apply(conn).await {
Ok(result) => {
return Ok(crate::netlink::nftables::config::ReconcileReport {
attempts: attempt + 1,
change_count: result.total_writes(),
});
}
Err(e) if (e.is_busy() || e.is_try_again()) && attempt < opts.max_retries => {
let backoff = opts.backoff.saturating_mul(1u32 << attempt.min(10));
tokio::time::sleep(backoff).await;
attempt += 1;
continue;
}
Err(e) => return Err(e),
}
}
}
pub async fn apply(&self, conn: &Connection<Wireguard>) -> Result<WireguardApplyResult> {
let diff = self.diff(conn).await?;
let mut result = WireguardApplyResult::default();
for (ifname, changes) in &diff.devices_to_modify {
let declared = self
.devices
.iter()
.find(|d| &d.ifname == ifname)
.ok_or_else(|| {
crate::netlink::Error::InvalidMessage(format!(
"wireguard apply: diff references undeclared device `{ifname}`"
))
})?;
if changes.has_device_level_change() {
conn.set_device_by_name(ifname, |mut b| {
if let Some(k) = declared.private_key {
b = b.private_key(k);
}
if let Some(p) = declared.listen_port {
b = b.listen_port(p);
}
if let Some(fw) = declared.fwmark {
b = b.fwmark(fw);
}
b
})
.await?;
result.device_writes += 1;
}
for added in &changes.peers_to_add {
conn.set_peer_by_name(ifname, added.public_key, |b| added.apply_to_builder(b))
.await?;
result.peer_writes += 1;
}
for (pk, peer_changes) in &changes.peers_to_modify {
let declared_peer = declared
.peers
.iter()
.find(|p| &p.public_key == pk)
.ok_or_else(|| {
crate::netlink::Error::InvalidMessage(format!(
"wireguard apply: diff references undeclared peer for `{ifname}`"
))
})?;
conn.set_peer_by_name(ifname, *pk, |b| {
declared_peer.apply_changes_to_builder(b, peer_changes)
})
.await?;
result.peer_writes += 1;
}
for pk in &changes.peers_to_remove {
conn.del_peer_by_name(ifname, *pk).await?;
result.peer_removals += 1;
}
}
Ok(result)
}
pub fn client(
ifname: impl Into<String>,
private_key: [u8; WG_KEY_LEN],
server_public_key: [u8; WG_KEY_LEN],
endpoint: SocketAddr,
allowed_ips: Vec<AllowedIp>,
persistent_keepalive: Option<Duration>,
) -> Self {
let peer = DeclaredWgPeer {
public_key: server_public_key,
preshared_key: None,
endpoint: Some(endpoint),
persistent_keepalive,
allowed_ips,
};
let device = DeclaredWgDevice {
ifname: ifname.into(),
private_key: Some(private_key),
listen_port: None,
fwmark: None,
peers: vec![peer],
};
let mut cfg = WireguardConfig::new();
cfg.push_device(device);
cfg
}
pub fn from_wg_quick(ifname: impl Into<String>, contents: &str) -> Result<Self> {
parse_wg_quick(ifname.into(), contents)
}
fn push_device(&mut self, device: DeclaredWgDevice) {
self.devices.push(device);
}
}
fn parse_cidr(s: &str) -> Result<AllowedIp> {
let s = s.trim();
let (addr_str, cidr_str) = match s.split_once('/') {
Some((a, c)) => (a, Some(c)),
None => (s, None),
};
let addr: IpAddr = addr_str
.parse()
.map_err(|_| Error::InvalidMessage(format!("wireguard config: invalid IP `{addr_str}`")))?;
let max = if addr.is_ipv4() { 32 } else { 128 };
let cidr = match cidr_str {
Some(c) => c.parse::<u8>().map_err(|_| {
Error::InvalidMessage(format!("wireguard config: invalid prefix `{c}`"))
})?,
None => max,
};
if cidr > max {
return Err(Error::InvalidMessage(format!(
"wireguard config: prefix /{cidr} out of range for {} (max /{max})",
if addr.is_ipv4() { "IPv4" } else { "IPv6" }
)));
}
Ok(match addr {
IpAddr::V4(a) => AllowedIp::v4(a, cidr),
IpAddr::V6(a) => AllowedIp::v6(a, cidr),
})
}
fn parse_key(field: &str, value: &str) -> Result<[u8; WG_KEY_LEN]> {
b64_decode_32(value)
.ok_or_else(|| Error::InvalidMessage(format!("wireguard config: invalid {field} key")))
}
fn parse_wg_quick(ifname: String, contents: &str) -> Result<WireguardConfig> {
enum Section {
None,
Interface,
Peer,
}
let mut section = Section::None;
let mut device = DeclaredWgDevice {
ifname,
private_key: None,
listen_port: None,
fwmark: None,
peers: Vec::new(),
};
let mut current_peer: Option<DeclaredWgPeer> = None;
const IGNORED_INTERFACE_KEYS: &[&str] = &[
"address",
"dns",
"mtu",
"table",
"preup",
"postup",
"predown",
"postdown",
"saveconfig",
];
for (lineno, raw) in contents.lines().enumerate() {
let line = raw.split('#').next().unwrap_or("").trim();
if line.is_empty() {
continue;
}
if let Some(name) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
match name.trim().to_ascii_lowercase().as_str() {
"interface" => section = Section::Interface,
"peer" => {
if let Some(p) = current_peer.take() {
device.peers.push(p);
}
section = Section::Peer;
current_peer = Some(DeclaredWgPeer {
public_key: [0u8; WG_KEY_LEN],
preshared_key: None,
endpoint: None,
persistent_keepalive: None,
allowed_ips: Vec::new(),
});
}
other => {
return Err(Error::InvalidMessage(format!(
"wireguard config: unknown section `[{other}]` at line {}",
lineno + 1
)));
}
}
continue;
}
let (key, value) = line.split_once('=').ok_or_else(|| {
Error::InvalidMessage(format!(
"wireguard config: expected `key = value` at line {}: `{line}`",
lineno + 1
))
})?;
let key_lc = key.trim().to_ascii_lowercase();
let value = value.trim();
match section {
Section::None => {
return Err(Error::InvalidMessage(format!(
"wireguard config: key `{}` before any [Interface]/[Peer] section (line {})",
key.trim(),
lineno + 1
)));
}
Section::Interface => match key_lc.as_str() {
"privatekey" => device.private_key = Some(parse_key("PrivateKey", value)?),
"listenport" => {
device.listen_port = Some(value.parse::<u16>().map_err(|_| {
Error::InvalidMessage(format!(
"wireguard config: invalid ListenPort `{value}`"
))
})?);
}
"fwmark" => device.fwmark = Some(parse_fwmark(value)?),
k if IGNORED_INTERFACE_KEYS.contains(&k) => { }
_ => {
return Err(Error::InvalidMessage(format!(
"wireguard config: unknown [Interface] key `{}` (line {})",
key.trim(),
lineno + 1
)));
}
},
Section::Peer => {
let peer = current_peer
.as_mut()
.expect("peer section sets current_peer");
match key_lc.as_str() {
"publickey" => peer.public_key = parse_key("PublicKey", value)?,
"presharedkey" => peer.preshared_key = Some(parse_key("PresharedKey", value)?),
"endpoint" => {
peer.endpoint = Some(value.parse::<SocketAddr>().map_err(|_| {
Error::InvalidMessage(format!(
"wireguard config: Endpoint `{value}` must be IP:port \
(hostnames are not resolved)"
))
})?);
}
"allowedips" => {
for part in value.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
peer.allowed_ips.push(parse_cidr(part)?);
}
}
"persistentkeepalive" => {
if !value.eq_ignore_ascii_case("off") {
let secs = value.parse::<u16>().map_err(|_| {
Error::InvalidMessage(format!(
"wireguard config: invalid PersistentKeepalive `{value}`"
))
})?;
if secs != 0 {
peer.persistent_keepalive = Some(Duration::from_secs(secs as u64));
}
}
}
_ => {
return Err(Error::InvalidMessage(format!(
"wireguard config: unknown [Peer] key `{}` (line {})",
key.trim(),
lineno + 1
)));
}
}
}
}
}
if let Some(p) = current_peer.take() {
device.peers.push(p);
}
for (i, p) in device.peers.iter().enumerate() {
if p.public_key == [0u8; WG_KEY_LEN] {
return Err(Error::InvalidMessage(format!(
"wireguard config: [Peer] #{} is missing PublicKey",
i + 1
)));
}
}
let mut cfg = WireguardConfig::new();
cfg.push_device(device);
Ok(cfg)
}
fn parse_fwmark(s: &str) -> Result<u32> {
if s.eq_ignore_ascii_case("off") {
return Ok(0);
}
let parsed = match s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
Some(hex) => u32::from_str_radix(hex, 16),
None => s.parse::<u32>(),
};
parsed.map_err(|_| Error::InvalidMessage(format!("wireguard config: invalid FwMark `{s}`")))
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct DeclaredWgDevice {
pub ifname: String,
pub private_key: Option<[u8; WG_KEY_LEN]>,
pub listen_port: Option<u16>,
pub fwmark: Option<u32>,
pub peers: Vec<DeclaredWgPeer>,
}
impl DeclaredWgDevice {
fn diff_against(&self, current: &WgDevice) -> DeviceChanges {
let mut changes = DeviceChanges::default();
if self.private_key.is_some() {
changes.private_key_set = true;
}
if let Some(p) = self.listen_port
&& current.listen_port != Some(p)
{
changes.listen_port_set = true;
}
if let Some(fw) = self.fwmark
&& current.fwmark != Some(fw)
{
changes.fwmark_set = true;
}
let curr_keys: std::collections::HashMap<&[u8; WG_KEY_LEN], &WgPeer> =
current.peers.iter().map(|p| (&p.public_key, p)).collect();
for declared in &self.peers {
match curr_keys.get(&declared.public_key) {
None => changes.peers_to_add.push(declared.clone()),
Some(curr) => {
let pc = declared.diff_against(curr);
if !pc.is_empty() {
changes.peers_to_modify.push((declared.public_key, pc));
}
}
}
}
let declared_keys: std::collections::HashSet<&[u8; WG_KEY_LEN]> =
self.peers.iter().map(|p| &p.public_key).collect();
for curr_peer in ¤t.peers {
if !declared_keys.contains(&curr_peer.public_key) {
changes.peers_to_remove.push(curr_peer.public_key);
}
}
changes
}
}
#[derive(Debug, Clone)]
#[must_use = "builders do nothing unless built"]
pub struct DeclaredWgDeviceBuilder {
ifname: String,
private_key: Option<[u8; WG_KEY_LEN]>,
listen_port: Option<u16>,
fwmark: Option<u32>,
peers: Vec<DeclaredWgPeer>,
}
impl DeclaredWgDeviceBuilder {
fn new(ifname: String) -> Self {
Self {
ifname,
private_key: None,
listen_port: None,
fwmark: None,
peers: Vec::new(),
}
}
pub fn private_key(mut self, key: [u8; WG_KEY_LEN]) -> Self {
self.private_key = Some(key);
self
}
pub fn listen_port(mut self, port: u16) -> Self {
self.listen_port = Some(port);
self
}
pub fn fwmark(mut self, mark: u32) -> Self {
self.fwmark = Some(mark);
self
}
pub fn peer(
mut self,
public_key: [u8; WG_KEY_LEN],
f: impl FnOnce(DeclaredWgPeerBuilder) -> DeclaredWgPeerBuilder,
) -> Self {
let builder = f(DeclaredWgPeerBuilder::new(public_key));
self.peers.push(builder.build());
self
}
fn build(self) -> DeclaredWgDevice {
DeclaredWgDevice {
ifname: self.ifname,
private_key: self.private_key,
listen_port: self.listen_port,
fwmark: self.fwmark,
peers: self.peers,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct DeclaredWgPeer {
pub public_key: [u8; WG_KEY_LEN],
pub preshared_key: Option<[u8; WG_KEY_LEN]>,
pub endpoint: Option<SocketAddr>,
pub persistent_keepalive: Option<Duration>,
pub allowed_ips: Vec<AllowedIp>,
}
impl DeclaredWgPeer {
fn diff_against(&self, current: &WgPeer) -> PeerChanges {
let mut changes = PeerChanges::default();
if self.preshared_key.is_some() {
changes.preshared_key_set = true;
}
if let Some(ep) = self.endpoint
&& current.endpoint != Some(ep)
{
changes.endpoint_set = true;
}
if let Some(ka) = self.persistent_keepalive {
let want_secs: u16 = ka.as_secs().try_into().unwrap_or(u16::MAX);
if current.persistent_keepalive != Some(want_secs) {
changes.persistent_keepalive_set = true;
}
}
let mut want = self.allowed_ips.clone();
let mut have = current.allowed_ips.clone();
want.sort_by_key(|a| (a.addr, a.cidr));
have.sort_by_key(|a| (a.addr, a.cidr));
if want != have {
changes.allowed_ips_set = true;
}
changes
}
fn apply_to_builder(&self, mut b: WgPeerBuilder) -> WgPeerBuilder {
if let Some(psk) = self.preshared_key {
b = b.preshared_key(psk);
}
if let Some(ep) = self.endpoint {
b = b.endpoint(ep);
}
if let Some(ka) = self.persistent_keepalive {
let secs: u16 = ka.as_secs().try_into().unwrap_or(u16::MAX);
b = b.persistent_keepalive(secs);
}
for ip in &self.allowed_ips {
b = b.allowed_ip(*ip);
}
b = b.replace_allowed_ips();
b
}
fn apply_changes_to_builder(
&self,
mut b: WgPeerBuilder,
changes: &PeerChanges,
) -> WgPeerBuilder {
if changes.preshared_key_set
&& let Some(psk) = self.preshared_key
{
b = b.preshared_key(psk);
}
if changes.endpoint_set
&& let Some(ep) = self.endpoint
{
b = b.endpoint(ep);
}
if changes.persistent_keepalive_set
&& let Some(ka) = self.persistent_keepalive
{
let secs: u16 = ka.as_secs().try_into().unwrap_or(u16::MAX);
b = b.persistent_keepalive(secs);
}
if changes.allowed_ips_set {
for ip in &self.allowed_ips {
b = b.allowed_ip(*ip);
}
b = b.replace_allowed_ips();
}
b
}
}
#[derive(Debug, Clone)]
#[must_use = "builders do nothing unless built"]
pub struct DeclaredWgPeerBuilder {
public_key: [u8; WG_KEY_LEN],
preshared_key: Option<[u8; WG_KEY_LEN]>,
endpoint: Option<SocketAddr>,
persistent_keepalive: Option<Duration>,
allowed_ips: Vec<AllowedIp>,
}
impl DeclaredWgPeerBuilder {
fn new(public_key: [u8; WG_KEY_LEN]) -> Self {
Self {
public_key,
preshared_key: None,
endpoint: None,
persistent_keepalive: None,
allowed_ips: Vec::new(),
}
}
pub fn preshared_key(mut self, key: [u8; WG_KEY_LEN]) -> Self {
self.preshared_key = Some(key);
self
}
pub fn endpoint(mut self, addr: SocketAddr) -> Self {
self.endpoint = Some(addr);
self
}
pub fn persistent_keepalive(mut self, d: Duration) -> Self {
self.persistent_keepalive = Some(d);
self
}
pub fn allowed_ip(mut self, ip: AllowedIp) -> Self {
self.allowed_ips.push(ip);
self
}
fn build(self) -> DeclaredWgPeer {
DeclaredWgPeer {
public_key: self.public_key,
preshared_key: self.preshared_key,
endpoint: self.endpoint,
persistent_keepalive: self.persistent_keepalive,
allowed_ips: self.allowed_ips,
}
}
}
#[derive(Debug, Clone, Default)]
#[must_use = "Diffs do nothing unless passed to `.apply()` or inspected"]
pub struct WireguardConfigDiff {
pub devices_to_modify: Vec<(String, DeviceChanges)>,
}
impl WireguardConfigDiff {
pub fn is_empty(&self) -> bool {
self.devices_to_modify.is_empty()
}
pub fn change_count(&self) -> usize {
self.devices_to_modify
.iter()
.map(|(_, c)| c.change_count())
.sum()
}
}
impl fmt::Display for WireguardConfigDiff {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_empty() {
return f.write_str("WireguardConfigDiff: no changes\n");
}
writeln!(
f,
"WireguardConfigDiff: {} kernel call(s)",
self.change_count()
)?;
for (ifname, changes) in &self.devices_to_modify {
writeln!(f, " {ifname}:")?;
if changes.private_key_set {
writeln!(f, " set private_key")?;
}
if changes.listen_port_set {
writeln!(f, " set listen_port")?;
}
if changes.fwmark_set {
writeln!(f, " set fwmark")?;
}
for added in &changes.peers_to_add {
writeln!(f, " + peer {}", PublicKey(added.public_key))?;
}
for (pk, pc) in &changes.peers_to_modify {
let bits = [
pc.preshared_key_set.then_some("preshared_key"),
pc.endpoint_set.then_some("endpoint"),
pc.persistent_keepalive_set.then_some("keepalive"),
pc.allowed_ips_set.then_some("allowed_ips"),
]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join(", ");
writeln!(f, " ~ peer {} ({bits})", PublicKey(*pk))?;
}
for pk in &changes.peers_to_remove {
writeln!(f, " - peer {}", PublicKey(*pk))?;
}
}
Ok(())
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct DeviceChanges {
pub private_key_set: bool,
pub listen_port_set: bool,
pub fwmark_set: bool,
pub peers_to_add: Vec<DeclaredWgPeer>,
pub peers_to_modify: Vec<([u8; WG_KEY_LEN], PeerChanges)>,
pub peers_to_remove: Vec<[u8; WG_KEY_LEN]>,
}
impl DeviceChanges {
pub fn is_empty(&self) -> bool {
!self.has_device_level_change()
&& self.peers_to_add.is_empty()
&& self.peers_to_modify.is_empty()
&& self.peers_to_remove.is_empty()
}
pub fn has_device_level_change(&self) -> bool {
self.private_key_set || self.listen_port_set || self.fwmark_set
}
pub fn change_count(&self) -> usize {
let device_level = self.has_device_level_change() as usize;
device_level
+ self.peers_to_add.len()
+ self.peers_to_modify.len()
+ self.peers_to_remove.len()
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct PeerChanges {
pub preshared_key_set: bool,
pub endpoint_set: bool,
pub persistent_keepalive_set: bool,
pub allowed_ips_set: bool,
}
impl PeerChanges {
pub fn is_empty(&self) -> bool {
!(self.preshared_key_set
|| self.endpoint_set
|| self.persistent_keepalive_set
|| self.allowed_ips_set)
}
}
#[derive(Debug, Default)]
#[must_use = "Inspect the per-kind counters to learn what apply changed"]
pub struct WireguardApplyResult {
pub device_writes: usize,
pub peer_writes: usize,
pub peer_removals: usize,
}
impl WireguardApplyResult {
pub fn total_writes(&self) -> usize {
self.device_writes + self.peer_writes + self.peer_removals
}
}
#[allow(dead_code)]
const _PLAN_196_MARKER: Option<Error> = None;
#[cfg(test)]
mod tests {
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
time::Duration,
};
use super::*;
use crate::netlink::genl::wireguard::types::WgPeer;
fn key(byte: u8) -> [u8; WG_KEY_LEN] {
[byte; WG_KEY_LEN]
}
fn empty_device(ifname: &str) -> WgDevice {
let mut d = WgDevice::new();
d.ifname = Some(ifname.to_string());
d
}
#[test]
fn empty_config_diff_is_empty() {
let cfg = WireguardConfig::new();
assert!(cfg.devices().is_empty());
}
#[test]
fn device_builder_records_fields() {
let cfg = WireguardConfig::new().device("wg0", |d| {
d.private_key(key(0xaa)).listen_port(51820).fwmark(0xdead)
});
let dev = &cfg.devices()[0];
assert_eq!(dev.ifname, "wg0");
assert_eq!(dev.private_key, Some(key(0xaa)));
assert_eq!(dev.listen_port, Some(51820));
assert_eq!(dev.fwmark, Some(0xdead));
assert!(dev.peers.is_empty());
}
#[test]
fn peer_builder_records_fields() {
let cfg = WireguardConfig::new().device("wg0", |d| {
d.peer(key(0xbb), |p| {
p.endpoint(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)),
51820,
))
.persistent_keepalive(Duration::from_secs(25))
.allowed_ip(AllowedIp::v4(Ipv4Addr::new(10, 0, 0, 0), 24))
})
});
let peer = &cfg.devices()[0].peers[0];
assert_eq!(peer.public_key, key(0xbb));
assert_eq!(peer.persistent_keepalive, Some(Duration::from_secs(25)));
assert_eq!(peer.allowed_ips.len(), 1);
}
#[test]
fn diff_private_key_always_dirty_when_declared() {
let declared = DeclaredWgDeviceBuilder::new("wg0".into())
.private_key(key(1))
.build();
let curr = empty_device("wg0");
let changes = declared.diff_against(&curr);
assert!(changes.private_key_set);
assert!(changes.has_device_level_change());
assert_eq!(changes.change_count(), 1);
}
#[test]
fn diff_listen_port_match_emits_no_change() {
let declared = DeclaredWgDeviceBuilder::new("wg0".into())
.listen_port(51820)
.build();
let mut curr = empty_device("wg0");
curr.listen_port = Some(51820);
let changes = declared.diff_against(&curr);
assert!(
changes.is_empty(),
"matching listen_port shouldn't be dirty"
);
}
#[test]
fn diff_listen_port_mismatch_emits_change() {
let declared = DeclaredWgDeviceBuilder::new("wg0".into())
.listen_port(51820)
.build();
let mut curr = empty_device("wg0");
curr.listen_port = Some(12345);
let changes = declared.diff_against(&curr);
assert!(changes.listen_port_set);
}
#[test]
fn diff_peers_to_add() {
let declared = DeclaredWgDeviceBuilder::new("wg0".into())
.peer(key(0xbb), |p| {
p.persistent_keepalive(Duration::from_secs(25))
})
.build();
let curr = empty_device("wg0");
let changes = declared.diff_against(&curr);
assert_eq!(changes.peers_to_add.len(), 1);
assert_eq!(changes.peers_to_add[0].public_key, key(0xbb));
}
#[test]
fn diff_peers_to_remove() {
let declared = DeclaredWgDeviceBuilder::new("wg0".into()).build();
let mut curr = empty_device("wg0");
let mut p = WgPeer::new(key(0xcc));
p.endpoint = Some(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)),
51820,
));
curr.peers.push(p);
let changes = declared.diff_against(&curr);
assert_eq!(changes.peers_to_remove, vec![key(0xcc)]);
}
#[test]
fn diff_peer_endpoint_change() {
let declared = DeclaredWgDeviceBuilder::new("wg0".into())
.peer(key(0xbb), |p| {
p.endpoint(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(2, 2, 2, 2)),
51820,
))
})
.build();
let mut curr = empty_device("wg0");
let mut peer = WgPeer::new(key(0xbb));
peer.endpoint = Some(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)),
51820,
));
curr.peers.push(peer);
let changes = declared.diff_against(&curr);
assert!(
changes
.peers_to_modify
.iter()
.any(|(pk, pc)| { *pk == key(0xbb) && pc.endpoint_set })
);
}
#[test]
fn diff_peer_allowed_ips_set_difference() {
let declared = DeclaredWgDeviceBuilder::new("wg0".into())
.peer(key(0xbb), |p| {
p.allowed_ip(AllowedIp::v4(Ipv4Addr::new(10, 0, 0, 0), 24))
.allowed_ip(AllowedIp::v4(Ipv4Addr::new(10, 0, 1, 0), 24))
})
.build();
let mut curr = empty_device("wg0");
let mut peer = WgPeer::new(key(0xbb));
peer.allowed_ips = vec![AllowedIp::v4(Ipv4Addr::new(10, 0, 0, 0), 24)];
curr.peers.push(peer);
let changes = declared.diff_against(&curr);
assert!(
changes
.peers_to_modify
.iter()
.any(|(_, pc)| pc.allowed_ips_set)
);
}
#[test]
fn diff_peer_allowed_ips_order_independent() {
let declared = DeclaredWgDeviceBuilder::new("wg0".into())
.peer(key(0xbb), |p| {
p.allowed_ip(AllowedIp::v4(Ipv4Addr::new(10, 0, 0, 0), 24))
.allowed_ip(AllowedIp::v4(Ipv4Addr::new(10, 0, 1, 0), 24))
})
.build();
let mut curr = empty_device("wg0");
let mut peer = WgPeer::new(key(0xbb));
peer.allowed_ips = vec![
AllowedIp::v4(Ipv4Addr::new(10, 0, 1, 0), 24),
AllowedIp::v4(Ipv4Addr::new(10, 0, 0, 0), 24),
];
curr.peers.push(peer);
let changes = declared.diff_against(&curr);
for (_, pc) in &changes.peers_to_modify {
assert!(!pc.allowed_ips_set);
}
}
#[test]
fn public_key_round_trips_through_base64() {
let s = "fE/wpxQ6/M6OmF5j4dvbY3FbCEXc3KlBL2QqAYjE0WI=";
let pk: PublicKey = s.parse().unwrap();
assert_eq!(pk.to_string(), s);
}
#[test]
fn public_key_zero_round_trips() {
let pk = PublicKey::from_bytes([0u8; WG_KEY_LEN]);
let s = pk.to_string();
assert_eq!(s.len(), 44);
assert_eq!(s.parse::<PublicKey>().unwrap(), pk);
}
#[test]
fn public_key_max_round_trips() {
let pk = PublicKey::from_bytes([0xffu8; WG_KEY_LEN]);
let s = pk.to_string();
let back: PublicKey = s.parse().unwrap();
assert_eq!(back, pk);
}
#[test]
fn public_key_accepts_unpadded_base64() {
let padded = "fE/wpxQ6/M6OmF5j4dvbY3FbCEXc3KlBL2QqAYjE0WI=";
let unpadded = padded.trim_end_matches('=');
let from_padded: PublicKey = padded.parse().unwrap();
let from_unpadded: PublicKey = unpadded.parse().unwrap();
assert_eq!(from_padded, from_unpadded);
}
#[test]
fn public_key_rejects_wrong_length() {
assert!("AAA=".parse::<PublicKey>().is_err());
assert!("".parse::<PublicKey>().is_err());
let too_long = "fE/wpxQ6/M6OmF5j4dvbY3FbCEXc3KlBL2QqAYjE0WIAAAA=";
assert!(too_long.parse::<PublicKey>().is_err());
}
#[test]
fn public_key_rejects_non_base64_chars() {
let bad = "fE/wpxQ6/M6OmF5j4dvbY3FbCEXc3KlBL2QqAYjE0W!=";
assert!(bad.parse::<PublicKey>().is_err());
}
#[test]
fn public_key_debug_uses_display() {
let pk = PublicKey::from_bytes([0u8; WG_KEY_LEN]);
let d = format!("{pk:?}");
assert!(d.starts_with("PublicKey("));
}
#[test]
fn diff_display_empty_says_no_changes() {
let d = WireguardConfigDiff::default();
assert!(d.to_string().contains("no changes"));
}
#[test]
fn diff_display_renders_peer_add_remove() {
let declared = DeclaredWgDeviceBuilder::new("wg0".into())
.peer(key(0xbb), |p| {
p.persistent_keepalive(Duration::from_secs(25))
})
.build();
let mut curr = empty_device("wg0");
curr.peers.push(WgPeer::new(key(0xcc)));
let changes = declared.diff_against(&curr);
let mut diff = WireguardConfigDiff::default();
diff.devices_to_modify.push(("wg0".into(), changes));
let s = diff.to_string();
assert!(s.contains("wg0"));
assert!(s.contains("+ peer "));
assert!(s.contains("- peer "));
}
#[test]
fn apply_result_total_writes() {
let r = WireguardApplyResult {
device_writes: 2,
peer_writes: 3,
peer_removals: 1,
};
assert_eq!(r.total_writes(), 6);
}
#[test]
fn diff_change_count_aggregates() {
let declared = DeclaredWgDeviceBuilder::new("wg0".into())
.private_key(key(1))
.listen_port(51820)
.peer(key(0xaa), |p| {
p.persistent_keepalive(Duration::from_secs(25))
})
.build();
let curr = empty_device("wg0");
let changes = declared.diff_against(&curr);
assert_eq!(changes.change_count(), 2);
}
fn b64(byte: u8) -> String {
b64_encode_32(&key(byte))
}
#[test]
fn client_builds_single_device_single_peer() {
let cfg = WireguardConfig::client(
"wg0",
key(0x11),
key(0x22),
"203.0.113.1:51820".parse().unwrap(),
vec![AllowedIp::v4(Ipv4Addr::UNSPECIFIED, 0)],
Some(Duration::from_secs(25)),
);
let dev = &cfg.devices()[0];
assert_eq!(dev.ifname, "wg0");
assert_eq!(dev.private_key, Some(key(0x11)));
assert_eq!(dev.peers.len(), 1);
let p = &dev.peers[0];
assert_eq!(p.public_key, key(0x22));
assert_eq!(p.endpoint, Some("203.0.113.1:51820".parse().unwrap()));
assert_eq!(p.persistent_keepalive, Some(Duration::from_secs(25)));
assert_eq!(p.allowed_ips, vec![AllowedIp::v4(Ipv4Addr::UNSPECIFIED, 0)]);
}
#[test]
fn wg_quick_parses_full_profile() {
let conf = format!(
"# a client profile\n\
[Interface]\n\
PrivateKey = {}\n\
ListenPort = 51820\n\
FwMark = 0x1234\n\
Address = 10.0.0.2/24 # wg-quick only, ignored\n\
DNS = 1.1.1.1\n\
\n\
[Peer]\n\
PublicKey = {}\n\
PresharedKey = {}\n\
Endpoint = 203.0.113.1:51820\n\
AllowedIPs = 10.0.0.0/24, 192.168.1.5, ::/0\n\
PersistentKeepalive = 25\n",
b64(0x11),
b64(0x22),
b64(0x33),
);
let cfg = WireguardConfig::from_wg_quick("wg0", &conf).expect("parse");
let dev = &cfg.devices()[0];
assert_eq!(dev.ifname, "wg0");
assert_eq!(dev.private_key, Some(key(0x11)));
assert_eq!(dev.listen_port, Some(51820));
assert_eq!(dev.fwmark, Some(0x1234));
assert_eq!(dev.peers.len(), 1);
let p = &dev.peers[0];
assert_eq!(p.public_key, key(0x22));
assert_eq!(p.preshared_key, Some(key(0x33)));
assert_eq!(p.endpoint, Some("203.0.113.1:51820".parse().unwrap()));
assert_eq!(p.persistent_keepalive, Some(Duration::from_secs(25)));
assert_eq!(
p.allowed_ips,
vec![
AllowedIp::v4(Ipv4Addr::new(10, 0, 0, 0), 24),
AllowedIp::v4(Ipv4Addr::new(192, 168, 1, 5), 32),
AllowedIp::v6(std::net::Ipv6Addr::UNSPECIFIED, 0),
]
);
}
#[test]
fn wg_quick_parses_multiple_peers() {
let conf = format!(
"[Interface]\nPrivateKey = {}\n\
[Peer]\nPublicKey = {}\nAllowedIPs = 10.0.0.1/32\n\
[Peer]\nPublicKey = {}\nAllowedIPs = 10.0.0.2/32\n",
b64(0x11),
b64(0x22),
b64(0x44),
);
let cfg = WireguardConfig::from_wg_quick("wg0", &conf).expect("parse");
let dev = &cfg.devices()[0];
assert_eq!(dev.peers.len(), 2);
assert_eq!(dev.peers[0].public_key, key(0x22));
assert_eq!(dev.peers[1].public_key, key(0x44));
}
#[test]
fn wg_quick_rejects_unknown_key() {
let conf = format!("[Interface]\nPrivateKey = {}\nBogus = 1\n", b64(0x11));
let err = WireguardConfig::from_wg_quick("wg0", &conf).unwrap_err();
assert!(err.to_string().contains("unknown [Interface] key"), "{err}");
}
#[test]
fn wg_quick_rejects_hostname_endpoint() {
let conf = format!(
"[Interface]\nPrivateKey = {}\n[Peer]\nPublicKey = {}\nEndpoint = vpn.example.com:51820\n",
b64(0x11),
b64(0x22),
);
let err = WireguardConfig::from_wg_quick("wg0", &conf).unwrap_err();
assert!(err.to_string().contains("IP:port"), "{err}");
}
#[test]
fn wg_quick_rejects_peer_without_public_key() {
let conf = format!(
"[Interface]\nPrivateKey = {}\n[Peer]\nAllowedIPs = 10.0.0.1/32\n",
b64(0x11),
);
let err = WireguardConfig::from_wg_quick("wg0", &conf).unwrap_err();
assert!(err.to_string().contains("missing PublicKey"), "{err}");
}
#[test]
fn wg_quick_keepalive_off_is_none() {
let conf = format!(
"[Interface]\nPrivateKey = {}\n[Peer]\nPublicKey = {}\nPersistentKeepalive = off\n",
b64(0x11),
b64(0x22),
);
let cfg = WireguardConfig::from_wg_quick("wg0", &conf).expect("parse");
assert_eq!(cfg.devices()[0].peers[0].persistent_keepalive, None);
}
#[test]
fn parse_cidr_defaults_and_bounds() {
assert_eq!(
parse_cidr("10.0.0.1").unwrap(),
AllowedIp::v4(Ipv4Addr::new(10, 0, 0, 1), 32)
);
assert_eq!(
parse_cidr("::1").unwrap(),
AllowedIp::v6("::1".parse().unwrap(), 128)
);
assert!(
parse_cidr("10.0.0.0/33").is_err(),
"v4 prefix > 32 must fail"
);
let _ = IpAddr::V4(Ipv4Addr::UNSPECIFIED); }
}