use std::{fmt::Write, str::FromStr}; use ipnet::IpNet; use super::Key; const FIELD_PRIVATE_KEY: &str = "PrivateKey"; const FIELD_LISTEN_PORT: &str = "ListenPort"; const FIELD_FWMARK: &str = "FwMark"; const FIELD_PUBLIC_KEY: &str = "PublicKey"; const FIELD_PRE_SHARED_KEY: &str = "PresharedKey"; const FIELD_ALLOWED_IPS: &str = "AllowedIPs"; const FIELD_ENDPOINT: &str = "Endpoint"; const FIELD_PERSISTENT_KEEPALIVE: &str = "PersistentKeepalive"; // wg-quick fields const FIELD_ADDRESS: &str = "Address"; const FIELD_DNS: &str = "DNS"; macro_rules! header { ($dest:expr, $h:expr) => { writeln!($dest, "[{}]", $h).unwrap(); }; } macro_rules! field { ($dest:expr, $n:expr, $v:expr) => { writeln!($dest, "{} = {}", $n, $v).unwrap(); }; } macro_rules! field_csv { ($dest:expr, $n:expr, $v:expr) => { if !$v.is_empty() { write!($dest, "{} = ", $n).unwrap(); let mut comma = false; for e in $v.iter() { if comma { write!($dest, ", ").unwrap(); } else { comma = true; } write!($dest, "{}", e).unwrap(); } writeln!($dest).unwrap(); } }; } macro_rules! field_opt { ($dest:expr, $n:expr, $v:expr) => { if let Some(ref v) = $v { field!($dest, $n, v); } }; } #[derive(Debug, Clone)] pub struct WgInterface { pub private_key: Key, pub address: Vec, pub listen_port: Option, pub fw_mark: Option, pub dns: Option, } #[derive(Debug, Clone)] pub struct WgPeer { pub public_key: Key, pub preshared_key: Option, pub allowed_ips: Vec, pub endpoint: Option, pub keep_alive: Option, } impl WgPeer { pub fn builder(public_key: Key) -> WgPeerBuilder { WgPeerBuilder::new(public_key) } } #[derive(Debug, Clone)] pub struct WgConf { pub interface: WgInterface, pub peers: Vec, } impl WgConf { pub fn builder() -> WgConfBuilder { WgConfBuilder::new() } } #[derive(Debug)] pub struct WgPeerBuilder { pub public_key: Key, pub preshared_key: Option, pub allowed_ips: Vec, pub endpoint: Option, pub keep_alive: Option, } impl WgPeerBuilder { pub fn new(public_key: Key) -> WgPeerBuilder { WgPeerBuilder { public_key, preshared_key: None, allowed_ips: Vec::new(), endpoint: None, keep_alive: None, } } pub fn preshared_key(mut self, preshared_key: Key) -> Self { self.preshared_key = Some(preshared_key); self } pub fn allowed_ip(mut self, allowed_ip: IpNet) -> Self { self.allowed_ips.push(allowed_ip); self } pub fn allowed_ips(mut self, allowed_ips: impl IntoIterator) -> Self { self.allowed_ips.extend(allowed_ips); self } pub fn endpoint(mut self, endpoint: impl Into) -> Self { self.endpoint = Some(endpoint.into()); self } pub fn endpoint_opt(mut self, endpoint: Option>) -> Self { if let Some(endpoint) = endpoint { self.endpoint = Some(endpoint.into()); } self } pub fn keep_alive(mut self, keep_alive: u16) -> Self { self.keep_alive = Some(keep_alive); self } pub fn build(self) -> WgPeer { WgPeer { public_key: self.public_key, preshared_key: self.preshared_key, allowed_ips: self.allowed_ips, endpoint: self.endpoint, keep_alive: self.keep_alive, } } } #[derive(Debug)] pub struct WgConfBuilder { private_key: Option, address: Vec, listen_port: Option, fw_mark: Option, dns: Option, peers: Vec, } impl WgConfBuilder { pub fn new() -> Self { WgConfBuilder { private_key: None, address: Vec::new(), listen_port: None, fw_mark: None, dns: None, peers: Vec::new(), } } pub fn private_key(mut self, private_key: Key) -> Self { self.private_key = Some(private_key); self } pub fn address(mut self, address: impl Into) -> Self { self.address.push(address.into()); self } pub fn addresses(mut self, addresses: impl IntoIterator) -> Self { self.address.extend(addresses); self } pub fn listen_port(mut self, listen_port: u16) -> Self { self.listen_port = Some(listen_port); self } pub fn fw_mark(mut self, fw_mark: u32) -> Self { self.fw_mark = Some(fw_mark); self } pub fn dns(mut self, dns: impl Into) -> Self { self.dns = Some(dns.into()); self } pub fn dns_opt(mut self, dns: Option>) -> Self { if let Some(dns) = dns { self.dns = Some(dns.into()); } self } pub fn peer(mut self, peer: WgPeer) -> Self { self.peers.push(peer); self } pub fn peers(mut self, peers: impl IntoIterator) -> Self { self.peers.extend(peers); self } pub fn build(self) -> WgConf { WgConf { interface: WgInterface { private_key: self.private_key.unwrap_or_else(Key::generate_private), address: self.address, listen_port: self.listen_port, fw_mark: self.fw_mark, dns: self.dns, }, peers: self.peers, } } } #[derive(Default)] struct PartialConf { interface: Option, peers: Vec, } pub fn parse_conf(conf: &str) -> anyhow::Result { let mut iter = conf.lines().filter_map(|l| { // remove whitespace on the sides let l = l.trim(); // remove the comment let (l, _) = l.rsplit_once("#").unwrap_or((l, "")); if l.is_empty() { None } else { Some(l) } }); let mut partial = PartialConf::default(); parse_partial(&mut partial, &mut iter)?; match partial.interface { Some(interface) => Ok(WgConf { interface, peers: partial.peers, }), None => Err(anyhow::anyhow!("no interface found")), } } pub fn serialize_conf(conf: &WgConf) -> String { let mut conf_str = String::new(); header!(conf_str, "Interface"); field!(conf_str, FIELD_PRIVATE_KEY, conf.interface.private_key); field_csv!(conf_str, FIELD_ADDRESS, conf.interface.address); field_opt!(conf_str, FIELD_LISTEN_PORT, conf.interface.listen_port); field_opt!(conf_str, FIELD_FWMARK, conf.interface.fw_mark); field_opt!(conf_str, FIELD_DNS, conf.interface.dns); for peer in conf.peers.iter() { writeln!(conf_str).unwrap(); header!(conf_str, "Peer"); field!(conf_str, FIELD_PUBLIC_KEY, peer.public_key); field_opt!(conf_str, FIELD_PRE_SHARED_KEY, peer.preshared_key); field_csv!(conf_str, FIELD_ALLOWED_IPS, peer.allowed_ips); field_opt!(conf_str, FIELD_ENDPOINT, peer.endpoint); field_opt!(conf_str, FIELD_PERSISTENT_KEEPALIVE, peer.keep_alive); } conf_str } fn parse_partial<'s, I: Iterator>( cfg: &mut PartialConf, iter: &mut I, ) -> anyhow::Result<()> { match iter.next() { Some("[Interface]") => parse_interface(cfg, iter), Some("[Peer]") => parse_peer(cfg, iter), Some(line) => Err(anyhow::anyhow!("unexpected line: {}", line)), None => Err(anyhow::anyhow!("unexpected end of file")), } } fn parse_interface<'s, I: Iterator>( cfg: &mut PartialConf, iter: &mut I, ) -> anyhow::Result<()> { let mut private_key = None; let mut address = Vec::new(); let mut listen_port = None; let mut fw_mark = None; let mut dns = None; let mut peer_next = false; if cfg.interface.is_some() { anyhow::bail!("cannot have more than one interface"); } while let Some(line) = iter.next() { if line == "[Peer]" { peer_next = true; break; } let (key, value) = parse_key_value(line)?; match key { FIELD_PRIVATE_KEY => private_key = Some(value.parse()?), FIELD_LISTEN_PORT => listen_port = Some(value.parse()?), FIELD_FWMARK => fw_mark = Some(value.parse()?), FIELD_ADDRESS => address = parse_csv(value)?, FIELD_DNS => dns = Some(value.to_string()), _ => anyhow::bail!("unexpected key: {}", key), } } cfg.interface = Some(WgInterface { private_key: private_key.ok_or_else(|| anyhow::anyhow!("interface missing private key"))?, address, listen_port, fw_mark, dns, }); if peer_next { parse_peer(cfg, iter) } else { Ok(()) } } fn parse_peer<'s, I: Iterator>( cfg: &mut PartialConf, iter: &mut I, ) -> anyhow::Result<()> { let mut public_key = None; let mut preshared_key = None; let mut allowed_ips = Vec::new(); let mut endpoint = None; let mut keep_alive = None; let mut interface_next = false; let mut peer_next = false; while let Some(line) = iter.next() { if line == "[Interface]" { interface_next = true; break; } if line == "[Peer]" { peer_next = true; break; } let (key, value) = parse_key_value(line)?; match key { FIELD_PUBLIC_KEY => public_key = Some(value.parse()?), FIELD_PRE_SHARED_KEY => preshared_key = Some(value.parse()?), FIELD_ALLOWED_IPS => allowed_ips = parse_csv(value)?, FIELD_ENDPOINT => endpoint = Some(value.to_string()), FIELD_PERSISTENT_KEEPALIVE => keep_alive = Some(value.parse()?), _ => anyhow::bail!("unexpected key: {}", key), } } cfg.peers.push(WgPeer { public_key: public_key.ok_or_else(|| anyhow::anyhow!("peer missing public key"))?, preshared_key, allowed_ips, endpoint, keep_alive, }); if interface_next { parse_interface(cfg, iter) } else if peer_next { parse_peer(cfg, iter) } else { Ok(()) } } fn parse_key_value<'s>(line: &'s str) -> anyhow::Result<(&'s str, &'s str)> { line.split_once("=") .map(|(k, v)| (k.trim(), v.trim())) .ok_or_else(|| anyhow::anyhow!("invalid line: {}", line)) } fn parse_csv< 'v, T: FromStr, >( value: &'v str, ) -> anyhow::Result> { let mut values = Vec::new(); for v in value.split(',').map(str::trim) { values.push(v.parse()?); } Ok(values) } #[cfg(test)] mod tests { use std::net::Ipv4Addr; use ipnet::{IpNet, Ipv4Net}; use crate::Key; use super::{WgConfBuilder, WgPeerBuilder}; const TEST_CONF_1: &str = r#" [Interface] PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk= ListenPort = 51820 [Peer] PublicKey = xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg= Endpoint = 192.95.5.67:1234 AllowedIPs = 10.192.122.3/32, 10.192.124.1/24 [Peer] PublicKey = TrMvSoP4jYQlY6RIzBgbssQqY3vxI2Pi+y71lOWWXX0= Endpoint = [2607:5300:60:6b0::c05f:543]:2468 AllowedIPs = 10.192.122.4/32, 192.168.0.0/16 [Peer] PublicKey = gN65BkIKy1eCE9pP1wdc8ROUtkHLF2PfAqYdyYBz6EA= Endpoint = test.wireguard.com:18981 AllowedIPs = 10.10.10.230/32 PersistentKeepalive = 54 "#; const TEST_CONF_2: &str = r#" [Peer] PublicKey = xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg= Endpoint = 192.95.5.67:1234 AllowedIPs = 10.192.122.3/32, 10.192.124.1/24 [Peer] PublicKey = TrMvSoP4jYQlY6RIzBgbssQqY3vxI2Pi+y71lOWWXX0= Endpoint = [2607:5300:60:6b0::c05f:543]:2468 AllowedIPs = 10.192.122.4/32, 192.168.0.0/16 [Interface] PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk= ListenPort = 51820 [Peer] PublicKey = gN65BkIKy1eCE9pP1wdc8ROUtkHLF2PfAqYdyYBz6EA= Endpoint = test.wireguard.com:18981 AllowedIPs = 10.10.10.230/32 PersistentKeepalive = 54 "#; const TEST_CONF_3: &str = r#" [Interface] PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk= ListenPort = 51820 [Interface] PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk= ListenPort = 51821 [Peer] PublicKey = gN65BkIKy1eCE9pP1wdc8ROUtkHLF2PfAqYdyYBz6EA= Endpoint = test.wireguard.com:18981 AllowedIPs = 10.10.10.230/32 "#; const TEST_CONF_4: &str = ""; const TEST_CONF_5: &str = r#" PublicKey = 1 [Interface] PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk= ListenPort = 51820 [Peer] PublicKey = gN65BkIKy1eCE9pP1wdc8ROUtkHLF2PfAqYdyYBz6EA= Endpoint = test.wireguard.com:18981 AllowedIPs = 10.10.10.230/32 "#; const TEST_CONF_6: &str = r#" [Interface] PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk= ListenPort = 51820 Unknown = 1 [Peer] PublicKey = gN65BkIKy1eCE9pP1wdc8ROUtkHLF2PfAqYdyYBz6EA= Endpoint = test.wireguard.com:18981 AllowedIPs = 10.10.10.230/32 "#; const TEST_CONF_7: &str = r#" [Interface] PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk= ListenPort = 51820 "#; #[test] fn parse_config() { parse_config_1_and_2(TEST_CONF_1); } #[test] fn parse_config_out_of_order_interface() { parse_config_1_and_2(TEST_CONF_2); } #[test] #[should_panic] fn parse_config_duplicate_interface() { super::parse_conf(TEST_CONF_3).unwrap(); } #[test] #[should_panic] fn parse_config_empty() { super::parse_conf(TEST_CONF_4).unwrap(); } #[test] #[should_panic] fn parse_config_out_of_order_field() { super::parse_conf(TEST_CONF_5).unwrap(); } #[test] #[should_panic] fn parse_config_unkown_field() { super::parse_conf(TEST_CONF_6).unwrap(); } #[test] fn parse_config_no_peers() { let cfg = super::parse_conf(TEST_CONF_7).unwrap(); assert_eq!( "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=", cfg.interface.private_key.to_string(), ); assert_eq!(Some(51820), cfg.interface.listen_port); assert_eq!(None, cfg.interface.fw_mark); assert_eq!(0, cfg.peers.len()); } fn parse_config_1_and_2(conf_str: &str) { let cfg = super::parse_conf(conf_str).unwrap(); assert_eq!( "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=", cfg.interface.private_key.to_string() ); assert_eq!(Some(51820), cfg.interface.listen_port); assert_eq!(None, cfg.interface.fw_mark); assert_eq!(3, cfg.peers.len()); let peer = &cfg.peers[0]; assert_eq!( "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=", peer.public_key.to_string() ); assert_eq!(None, peer.preshared_key); assert_eq!(2, peer.allowed_ips.len()); assert_eq!(Some("192.95.5.67:1234"), peer.endpoint.as_deref()); assert_eq!(None, peer.keep_alive); let peer = &cfg.peers[1]; assert_eq!( "TrMvSoP4jYQlY6RIzBgbssQqY3vxI2Pi+y71lOWWXX0=", peer.public_key.to_string() ); assert_eq!(None, peer.preshared_key); assert_eq!(2, peer.allowed_ips.len()); assert_eq!( Some("[2607:5300:60:6b0::c05f:543]:2468"), peer.endpoint.as_deref() ); assert_eq!(None, peer.keep_alive); let peer = &cfg.peers[2]; assert_eq!( "gN65BkIKy1eCE9pP1wdc8ROUtkHLF2PfAqYdyYBz6EA=", peer.public_key.to_string() ); assert_eq!(None, peer.preshared_key); assert_eq!(1, peer.allowed_ips.len()); assert_eq!(Some("test.wireguard.com:18981"), peer.endpoint.as_deref()); assert_eq!(Some(54), peer.keep_alive); } #[test] fn serialize_no_peers() { let key = Key::decode("yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=").unwrap(); let conf = WgConfBuilder::new() .fw_mark(10) .listen_port(6000) .dns("dns.example.com") .address(IpNet::V4( Ipv4Net::new(Ipv4Addr::new(10, 0, 0, 5), 24).unwrap(), )) .private_key(key) .build(); let serialized = super::serialize_conf(&conf); assert_eq!( r#"[Interface] PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk= Address = 10.0.0.5/24 ListenPort = 6000 FwMark = 10 DNS = dns.example.com "#, serialized ); } #[test] fn serialize_with_peers() { let key1 = Key::decode("xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=").unwrap(); let key2 = Key::decode("TrMvSoP4jYQlY6RIzBgbssQqY3vxI2Pi+y71lOWWXX0=").unwrap(); let key3 = Key::decode("gN65BkIKy1eCE9pP1wdc8ROUtkHLF2PfAqYdyYBz6EA=").unwrap(); let conf = WgConfBuilder::new() .private_key(key1) .listen_port(51820) .dns("dns.example.com") .peer( WgPeerBuilder::new(key2) .keep_alive(10) .endpoint("test.wireguard.com:18981") .allowed_ip(ipnet::IpNet::V4( Ipv4Net::new(Ipv4Addr::new(10, 0, 0, 2), 24).unwrap(), )) .build(), ) .peer( WgPeerBuilder::new(key3) .allowed_ip(ipnet::IpNet::V4( Ipv4Net::new(Ipv4Addr::new(10, 0, 0, 3), 24).unwrap(), )) .build(), ) .build(); let serialized = super::serialize_conf(&conf); assert_eq!( r#"[Interface] PrivateKey = xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg= ListenPort = 51820 DNS = dns.example.com [Peer] PublicKey = TrMvSoP4jYQlY6RIzBgbssQqY3vxI2Pi+y71lOWWXX0= AllowedIPs = 10.0.0.2/24 Endpoint = test.wireguard.com:18981 PersistentKeepalive = 10 [Peer] PublicKey = gN65BkIKy1eCE9pP1wdc8ROUtkHLF2PfAqYdyYBz6EA= AllowedIPs = 10.0.0.3/24 "#, serialized ); } }