use anyhow::{Context, anyhow};
use ipnet::IpNet;
use log::{debug, warn};
use nom::{
IResult, Parser,
branch::alt,
bytes::complete::{is_not, tag, take_while1},
character::complete::{char, line_ending, multispace0, multispace1, space0},
combinator::{all_consuming, eof, map, opt, peek},
multi::{many_till, many0},
sequence::{delimited, preceded, separated_pair, terminated},
};
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::net::IpAddr;
use std::net::SocketAddr;
use std::net::ToSocketAddrs;
use std::str::FromStr;
fn parse_key(input: &str) -> IResult<&str, &str> {
take_while1(|c: char| c.is_alphanumeric()).parse(input)
}
fn parse_value(input: &str) -> IResult<&str, &str> {
map(opt(is_not("\n\r")), |s: Option<&str>| {
s.unwrap_or("").split('#').next().unwrap_or("").trim()
})
.parse(input)
}
fn parse_key_value(input: &str) -> IResult<&str, (&str, &str)> {
separated_pair(parse_key, delimited(space0, tag("="), space0), parse_value).parse(input)
}
fn parse_section_header(input: &str) -> IResult<&str, &str> {
delimited(
char('['),
take_while1(|c: char| c.is_alphanumeric()),
char(']'),
)
.parse(input)
}
fn parse_line(input: &str) -> IResult<&str, Option<(&str, &str)>> {
alt((
map(parse_key_value, Some),
map(
(opt(preceded(char('#'), is_not("\n\r"))), line_ending),
|_| None,
),
map(preceded(char('#'), is_not("\n\r")), |_| None),
))
.parse(input)
}
#[allow(clippy::type_complexity)]
fn parse_section(input: &str) -> IResult<&str, (&str, Vec<(&str, &str)>)> {
let (input, name) = terminated(parse_section_header, multispace0).parse(input)?;
let (input, lines) = many_till(
parse_line,
peek(alt((map(parse_section_header, |_| ()), map(eof, |_| ())))),
)
.parse(input)?;
let pairs = lines.0.into_iter().flatten().collect();
Ok((input, (name, pairs)))
}
fn spc(input: &str) -> IResult<&str, ()> {
map(
many0(alt((
map(multispace1, |_| ()),
map(
terminated(
preceded(multispace0, preceded(char('#'), is_not("\n\r"))),
alt((line_ending, eof)),
),
|_| (),
),
))),
|_| (),
)
.parse(input)
}
#[allow(clippy::type_complexity)]
fn parse_config(input: &str) -> IResult<&str, Vec<(&str, Vec<(&str, &str)>)>> {
all_consuming(many0(preceded(spc, parse_section))).parse(input)
}
fn split_list_value(value: &str) -> impl Iterator<Item = &str> {
value.split(',').map(str::trim).filter(|s| !s.is_empty())
}
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq)]
pub struct WireguardInterface {
#[serde(rename = "PrivateKey")]
pub private_key: String,
#[serde(rename = "Address", deserialize_with = "de_vec_ipnet")]
pub address: Vec<IpNet>,
#[serde(rename = "DNS", deserialize_with = "de_vec_ipaddr")]
pub dns: Option<Vec<IpAddr>>,
#[serde(rename = "MTU")]
pub mtu: Option<String>,
}
impl std::fmt::Debug for WireguardInterface {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WireguardInterface")
.field("private_key", &"********".to_string())
.field("address", &self.address)
.field("dns", &self.dns)
.finish()
}
}
impl Display for WireguardConfig {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
writeln!(f, "[Interface]")?;
writeln!(f, "PrivateKey = {}", self.interface.private_key)?;
let addresses = self
.interface
.address
.iter()
.map(|a| a.to_string())
.collect::<Vec<String>>()
.join(", ");
writeln!(f, "Address = {addresses}")?;
if let Some(mtu) = &self.interface.mtu {
writeln!(f, "MTU = {mtu}")?;
}
if let Some(dns_servers) = &self.interface.dns {
for dns in dns_servers {
writeln!(f, "DNS = {dns}")?;
}
}
writeln!(f, "\n[Peer]")?;
writeln!(f, "PublicKey = {}", self.peer.public_key)?;
let allowed_ips = self
.peer
.allowed_ips
.iter()
.map(|a| a.to_string())
.collect::<Vec<String>>()
.join(", ");
writeln!(f, "AllowedIPs = {allowed_ips}")?;
writeln!(f, "Endpoint = {}", self.peer.endpoint)?;
if let Some(keepalive) = &self.peer.keepalive {
writeln!(f, "PersistentKeepalive = {keepalive}")?;
}
Ok(())
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum WireguardEndpoint {
HostnameWithPort(String, u16),
IpWithPort(SocketAddr),
}
impl WireguardEndpoint {
pub fn ip_or_hostname(&self) -> String {
match self {
WireguardEndpoint::HostnameWithPort(host, _) => host.clone(),
WireguardEndpoint::IpWithPort(addr) => addr.ip().to_string(),
}
}
pub fn port(&self) -> u16 {
match self {
WireguardEndpoint::HostnameWithPort(_, port) => *port,
WireguardEndpoint::IpWithPort(addr) => addr.port(),
}
}
pub fn resolve_ip(&self) -> anyhow::Result<IpAddr> {
match self {
WireguardEndpoint::HostnameWithPort(host, port) => {
let addr = (host.as_str(), *port)
.to_socket_addrs()
.with_context(|| format!("Failed to resolve Wireguard endpoint {host}"))?
.next()
.ok_or_else(|| anyhow!("No address found for Wireguard endpoint {host}"))?;
Ok(addr.ip())
}
WireguardEndpoint::IpWithPort(addr) => Ok(addr.ip()),
}
}
pub fn is_ip(&self) -> bool {
match self {
WireguardEndpoint::HostnameWithPort(_, _) => false,
WireguardEndpoint::IpWithPort(_) => true,
}
}
}
impl FromStr for WireguardEndpoint {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(addr) = s.parse::<SocketAddr>() {
Ok(WireguardEndpoint::IpWithPort(addr))
} else if let Some((host, port)) = s.rsplit_once(':') {
let port = port.parse::<u16>().map_err(|_| anyhow!("Invalid port"))?;
let host = host.trim_matches(|c| c == '[' || c == ']').to_string(); Ok(WireguardEndpoint::HostnameWithPort(host.to_string(), port))
} else {
Err(anyhow!("Invalid Wireguard endpoint format"))
}
}
}
impl Display for WireguardEndpoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WireguardEndpoint::HostnameWithPort(host, port) => write!(f, "{host}:{port}"),
WireguardEndpoint::IpWithPort(addr) => write!(f, "{addr}"),
}
}
}
impl Serialize for WireguardEndpoint {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for WireguardEndpoint {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse::<WireguardEndpoint>()
.map_err(serde::de::Error::custom)
}
}
#[derive(Deserialize, Debug, Serialize, PartialEq, Eq)]
pub struct WireguardPeer {
#[serde(rename = "PublicKey")]
pub public_key: String,
#[serde(rename = "AllowedIPs", deserialize_with = "de_vec_ipnet")]
pub allowed_ips: Vec<IpNet>,
#[serde(rename = "Endpoint")]
pub endpoint: WireguardEndpoint,
#[serde(rename = "PersistentKeepalive")]
pub keepalive: Option<String>,
}
#[derive(Deserialize, Debug, Serialize, PartialEq, Eq)]
pub struct WireguardConfig {
#[serde(rename = "Interface")]
pub interface: WireguardInterface,
#[serde(rename = "Peer")]
pub peer: WireguardPeer,
}
impl FromStr for WireguardConfig {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (remaining, parsed_sections) = parse_config(s.trim())
.map_err(|e| anyhow!("Failed to parse Wireguard config: {}", e))?;
if !remaining.trim().is_empty() {
return Err(anyhow!("Unexpected trailing data in config: {}", remaining));
}
let mut interface = None;
let mut peer = None;
for (section_name, kvs) in parsed_sections {
match section_name {
"Interface" => {
let mut private_key = None;
let mut addresses = Vec::new();
let mut dns_servers = Vec::new();
let mut mtu = None;
for (key, value) in kvs {
match key {
"PrivateKey" => private_key = Some(value.to_string()),
"Address" => {
addresses.extend(split_list_value(value).map(ToString::to_string))
}
"DNS" => {
dns_servers.extend(split_list_value(value).map(ToString::to_string))
}
"MTU" => mtu = Some(value.to_string()),
"PreUp" | "PostUp" | "PreDown" | "PostDown" => warn!(
"Ignoring wg-quick script option {key}; vopono manages namespace setup and does not execute config scripts"
),
"Table" | "SaveConfig" => warn!(
"Ignoring wg-quick option {key}; vopono derives namespace routes from AllowedIPs"
),
_ => debug!("Unknown key in [Interface] section: {key}"),
}
}
let parsed_addresses = addresses
.iter()
.map(|a| a.parse::<IpNet>())
.collect::<Result<Vec<_>, _>>()
.context("Failed to parse Address field")?;
let parsed_dns = dns_servers
.iter()
.map(|d| d.parse::<IpAddr>())
.collect::<Result<Vec<_>, _>>()
.context("Failed to parse DNS field")?;
interface = Some(WireguardInterface {
private_key: private_key
.context("Missing PrivateKey in [Interface] section")?,
address: parsed_addresses,
dns: if parsed_dns.is_empty() {
None
} else {
Some(parsed_dns)
},
mtu,
});
}
"Peer" => {
if peer.is_some() {
return Err(anyhow!(
"Multiple [Peer] sections are not currently supported; refusing to apply incomplete routing rules"
));
}
let mut public_key = None;
let mut allowed_ips = Vec::new();
let mut endpoint = None;
let mut keepalive = None;
for (key, value) in kvs {
match key {
"PublicKey" => public_key = Some(value.to_string()),
"AllowedIPs" => {
allowed_ips.extend(split_list_value(value).map(ToString::to_string))
}
"Endpoint" => endpoint = Some(value.to_string()),
"PersistentKeepalive" => keepalive = Some(value.to_string()),
_ => debug!("Unknown key in [Peer] section: {key}"),
}
}
let parsed_allowed_ips = allowed_ips
.iter()
.map(|a| a.parse::<IpNet>())
.collect::<Result<Vec<_>, _>>()
.context("Failed to parse AllowedIPs field")?;
let parsed_endpoint = endpoint
.context("Missing Endpoint in [Peer] section")?
.parse::<WireguardEndpoint>()
.context("Failed to parse Endpoint field")?;
peer = Some(WireguardPeer {
public_key: public_key.context("Missing PublicKey in [Peer] section")?,
allowed_ips: parsed_allowed_ips,
endpoint: parsed_endpoint,
keepalive,
});
}
_ => warn!("Unknown section in config: {section_name}"),
}
}
Ok(WireguardConfig {
interface: interface.context("Missing [Interface] section in config")?,
peer: peer.context("Missing [Peer] section in config")?,
})
}
}
pub fn de_vec_ipnet<'de, D>(deserializer: D) -> Result<Vec<IpNet>, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
match split_list_value(&raw)
.map(|x| x.parse::<IpNet>())
.collect::<Result<Vec<IpNet>, ipnet::AddrParseError>>()
{
Ok(x) => Ok(x),
Err(x) => Err(serde::de::Error::custom(anyhow!(
"Wireguard IpNet deserialisation error: {:?}",
x
))),
}
}
pub fn de_vec_ipaddr<'de, D>(deserializer: D) -> Result<Option<Vec<IpAddr>>, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = match String::deserialize(deserializer) {
Ok(s) => s,
Err(e) => {
debug!("Missing optional DNS field in Wireguard config - serde");
debug!("serde: {e:?}");
return Ok(None);
}
};
debug!("Deserializing: {raw} to Vec<IpAddr>");
match split_list_value(&raw)
.map(|x| x.parse::<IpAddr>())
.collect::<Result<Vec<IpAddr>, _>>()
{
Ok(x) if x.is_empty() => Ok(None),
Ok(x) => Ok(Some(x)),
Err(x) => Err(serde::de::Error::custom(anyhow!(
"Wireguard IpAddr deserialisation error: {:?}",
x
))),
}
}
pub fn de_socketaddr<'de, D>(deserializer: D) -> Result<std::net::SocketAddr, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
match raw.trim().to_socket_addrs() {
Ok(mut x) => Ok(x.next().unwrap()),
Err(x) => Err(serde::de::Error::custom(anyhow!(
"Wireguard IpAddr deserialisation error: {:?}",
x
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hostname_endpoint_resolves_with_its_port() {
let endpoint = "localhost:51820".parse::<WireguardEndpoint>().unwrap();
assert!(endpoint.resolve_ip().unwrap().is_loopback());
}
use std::str::FromStr;
const TEST_CONFIG_DUPLICATE_DNS: &str = r#"
# This is a comment
[Interface]
# Add IPv6 address alongside IPv4
Address = 10.200.200.2/32, fd42:42:42::2/128
PrivateKey = dGhpcyBpcyBhIGR1bW15IHByaXZhdGUga2V5ISEhISEhIQ==
DNS = 8.8.8.8
# Uncomment the IPv6 DNS server for full IPv6 support
DNS = 2001:4860:4860::8888
MTU = 1420
[Peer]
PublicKey = dGhpcyBpcyBhIGR1bW15IHB1YmxpYyBrZXkhISEhISEhIQ==
Endpoint = 199.50.100.1:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
"#;
const TEST_CONFIG_IPV6_ENDPOINT: &str = r#"
[Interface]
Address = 10.0.0.5/24
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
[Peer]
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBA=
Endpoint = [2d01:4e9:c013:c690::1]:51820
AllowedIPs = 0.0.0.0/0
"#;
const TEST_CONFIG_TRAILING_COMMAS: &str = r#"
[Interface]
Address = 10.0.0.5/24,
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
DNS = 1.1.1.1,
[Peer]
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBA=
Endpoint = 203.0.113.1:51820
AllowedIPs = 0.0.0.0/0, ::/0,
"#;
const TEST_CONFIG_BLANK_PUBLIC_KEY: &str = r#"
[Interface]
Address = 10.0.0.5/24
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
[Peer]
PublicKey =
AllowedIPs = 0.0.0.0/0
Endpoint = 203.0.113.1:51820
"#;
#[test]
fn test_parse_config_with_duplicate_dns() {
let config = WireguardConfig::from_str(TEST_CONFIG_DUPLICATE_DNS).unwrap();
assert_eq!(
config.interface.private_key,
"dGhpcyBpcyBhIGR1bW15IHByaXZhdGUga2V5ISEhISEhIQ=="
);
assert_eq!(config.interface.address.len(), 2);
assert_eq!(
config.interface.address[0],
"10.200.200.2/32".parse::<IpNet>().unwrap()
);
assert_eq!(
config.interface.address[1],
"fd42:42:42::2/128".parse::<IpNet>().unwrap()
);
let dns_servers = config.interface.dns.unwrap();
assert_eq!(dns_servers.len(), 2);
assert_eq!(dns_servers[0], "8.8.8.8".parse::<IpAddr>().unwrap());
assert_eq!(
dns_servers[1],
"2001:4860:4860::8888".parse::<IpAddr>().unwrap()
);
assert_eq!(config.peer.endpoint.to_string(), "199.50.100.1:51820");
assert_eq!(config.peer.allowed_ips.len(), 2);
assert_eq!(config.peer.keepalive, Some("25".to_string()));
}
#[test]
fn test_parse_config_with_ipv6_endpoint() {
let config = WireguardConfig::from_str(TEST_CONFIG_IPV6_ENDPOINT).unwrap();
let expected_socket_addr: SocketAddr = "[2d01:4e9:c013:c690::1]:51820".parse().unwrap();
assert_eq!(
config.peer.endpoint,
WireguardEndpoint::IpWithPort(expected_socket_addr)
);
assert_eq!(
config.peer.endpoint.to_string(),
"[2d01:4e9:c013:c690::1]:51820"
);
}
#[test]
fn test_parse_config_ignores_empty_csv_values() {
let config = WireguardConfig::from_str(TEST_CONFIG_TRAILING_COMMAS).unwrap();
assert_eq!(config.interface.address.len(), 1);
assert_eq!(config.interface.dns.unwrap().len(), 1);
assert_eq!(config.peer.allowed_ips.len(), 2);
}
#[test]
fn test_parse_config_keeps_blank_value_on_its_own_line() {
let config = WireguardConfig::from_str(TEST_CONFIG_BLANK_PUBLIC_KEY).unwrap();
assert_eq!(config.peer.public_key, "");
assert_eq!(config.peer.allowed_ips.len(), 1);
assert_eq!(config.peer.endpoint.to_string(), "203.0.113.1:51820");
}
#[test]
fn test_tostring_format() {
let config = WireguardConfig::from_str(TEST_CONFIG_DUPLICATE_DNS).unwrap();
let output = config.to_string();
assert!(output.starts_with("[Interface]"));
assert!(output.contains("Address = 10.200.200.2/32, fd42:42:42::2/128"));
assert!(output.contains("\n[Peer]\n"));
assert!(output.contains("PersistentKeepalive = 25"));
let dns_line_count = output
.lines()
.filter(|line| line.starts_with("DNS = "))
.count();
assert_eq!(dns_line_count, 2);
assert!(output.contains("DNS = 8.8.8.8"));
assert!(output.contains("DNS = 2001:4860:4860::8888"));
}
#[test]
fn test_parser_and_tostring_roundtrip() {
let original_config = WireguardConfig::from_str(TEST_CONFIG_DUPLICATE_DNS).unwrap();
let generated_string = original_config.to_string();
let roundtrip_config = WireguardConfig::from_str(&generated_string).unwrap();
assert_eq!(original_config, roundtrip_config);
let original_config_2 = WireguardConfig::from_str(TEST_CONFIG_IPV6_ENDPOINT).unwrap();
let generated_string_2 = original_config_2.to_string();
let roundtrip_config_2 = WireguardConfig::from_str(&generated_string_2).unwrap();
assert_eq!(original_config_2, roundtrip_config_2);
}
#[test]
fn test_empty_allowed_ips_parses_as_empty_vec() {
let config = r#"
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
Address = 10.0.0.5/24
[Peer]
PublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBA=
Endpoint = 199.50.100.1:51820
AllowedIPs =
"#;
let config = WireguardConfig::from_str(config).unwrap();
assert!(config.peer.allowed_ips.is_empty());
}
#[test]
fn rejects_multiple_peers_instead_of_silently_misrouting() {
let config = r#"
[Interface]
PrivateKey = private
Address = 10.0.0.2/32
[Peer]
PublicKey = first
AllowedIPs = 10.0.0.0/24
Endpoint = 192.0.2.1:51820
[Peer]
PublicKey = second
AllowedIPs = 10.1.0.0/24
Endpoint = 192.0.2.2:51820
"#;
assert!(WireguardConfig::from_str(config).is_err());
}
}