use anyhow::{anyhow, Context, Result};
use base64::Engine as _;
use std::io::Write as _;
use std::net::IpAddr;
use std::path::Path;
use crate::config::{make_config_dir, ServicePort};
use crate::net::generate_unused_subnet;
use serde::Serialize;
use x25519_dalek::{PublicKey, StaticSecret};
mod runtime;
pub use runtime::LocalWg;
const WIREGUARD_LISTEN_PORT: i32 = 51820;
#[derive(Debug, Serialize, Clone)]
pub struct WireguardKeypair {
private: String,
public: String,
}
impl WireguardKeypair {
pub fn new() -> Result<WireguardKeypair> {
let secret = StaticSecret::random_from_rng(rand_core::OsRng);
let public = PublicKey::from(&secret);
Ok(WireguardKeypair {
private: encode_key(secret.as_bytes()),
public: encode_key(public.as_bytes()),
})
}
pub fn private_bytes(&self) -> Result<[u8; 32]> {
decode_key(&self.private).context("failed to decode wg private key")
}
pub fn public_bytes(&self) -> Result<[u8; 32]> {
decode_key(&self.public).context("failed to decode wg public key")
}
}
fn encode_key(bytes: &[u8]) -> String {
base64::engine::general_purpose::STANDARD.encode(bytes)
}
fn decode_key(s: &str) -> Result<[u8; 32]> {
let raw = base64::engine::general_purpose::STANDARD.decode(s.trim())?;
raw.as_slice()
.try_into()
.map_err(|_| anyhow::anyhow!("expected 32-byte key, got {}", raw.len()))
}
#[derive(Debug, Serialize, Clone)]
pub struct WireguardHost {
pub name: String,
pub address: IpAddr,
pub endpoint: Option<IpAddr>,
pub listenport: i32,
pub keypair: WireguardKeypair,
}
#[derive(Debug, Serialize, Clone)]
pub struct WireguardDevice {
pub name: String,
pub interface: WireguardHost,
pub peer: WireguardHost,
}
impl WireguardDevice {
pub fn config(&self) -> Result<String> {
let wg_template = include_str!("../../files/wg0.conf.j2");
let mut context = tera::Context::new();
context.insert("wireguard_device", &self);
let empty_rules: Vec<ServicePort> = Vec::new();
context.insert("services", &empty_rules);
tera::Tera::one_off(wg_template, &context, false)
.context("Failed to write wireguard config")
}
pub fn config_with_services(&self, services: &[ServicePort]) -> Result<String> {
let wg_template = include_str!("../../files/wg0.conf.j2");
let mut context = tera::Context::new();
context.insert("wireguard_device", &self);
context.insert("services", &services);
tera::Tera::one_off(wg_template, &context, false)
.context("Failed to write wireguard config for multiple services")
}
pub fn write_locally(&self, service_name: &str, services: &[ServicePort]) -> Result<()> {
let wg_config_path = make_config_dir(service_name)?.join(format!("{}.conf", service_name));
let mut f = std::fs::File::create(&wg_config_path)?;
f.write_all(self.config_with_services(services)?.as_bytes())?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct WireguardManager {
pub wg_local_ip: IpAddr,
pub wg_local_device: WireguardDevice,
pub wg_remote_ip: IpAddr,
pub wg_remote_device: WireguardDevice,
}
impl WireguardManager {
pub fn new(service_name: &str) -> Result<WireguardManager> {
let wg_subnet = generate_unused_subnet()?;
let s = wg_subnet.hosts().collect::<Vec<IpAddr>>();
let wg_local_ip = s[0];
let wg_local_name = pick_local_iface_name()?;
let wg_local_keypair = WireguardKeypair::new()?;
let wg_local_host = WireguardHost {
name: wg_local_name.to_owned(),
address: wg_local_ip,
endpoint: None,
listenport: 0,
keypair: wg_local_keypair,
};
let wg_remote_ip = s[1];
let wg_remote_name = format!("innisfree-{}-remote", service_name);
let wg_remote_keypair = WireguardKeypair::new()?;
let wg_remote_host = WireguardHost {
name: wg_remote_name.to_owned(),
address: wg_remote_ip,
endpoint: None,
listenport: WIREGUARD_LISTEN_PORT,
keypair: wg_remote_keypair,
};
let wg_local_device = WireguardDevice {
name: wg_local_name,
interface: wg_local_host.clone(),
peer: wg_remote_host.clone(),
};
let wg_remote_device = WireguardDevice {
name: wg_remote_name,
interface: wg_remote_host,
peer: wg_local_host,
};
Ok(WireguardManager {
wg_local_ip,
wg_local_device,
wg_remote_ip,
wg_remote_device,
})
}
}
fn pick_local_iface_name() -> Result<String> {
const MAX_SLOTS: u32 = 100;
for n in 0..MAX_SLOTS {
let candidate = format!("innisfree{n}");
if !Path::new(&format!("/sys/class/net/{candidate}")).exists() {
return Ok(candidate);
}
}
Err(anyhow!(
"no free interface name in innisfree[0..{MAX_SLOTS}); \
clean up stale ones with `sudo ip link delete <name>`"
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_generation() -> anyhow::Result<()> {
let wg_hosts = _generate_hosts()?;
let wg_device = WireguardDevice {
name: "foo1".to_string(),
interface: wg_hosts[0].clone(),
peer: wg_hosts[1].clone(),
};
let wg_config = wg_device.config()?;
assert!(wg_config.contains("Interface"));
assert!(wg_config.contains("PrivateKey = "));
assert!(!wg_config.contains(&wg_hosts[0].keypair.public));
assert!(wg_config.contains(&wg_hosts[0].keypair.private));
assert!(wg_config.contains(&wg_hosts[1].keypair.public));
assert!(!wg_config.contains(&wg_hosts[1].keypair.private));
assert!(!wg_config.contains("/"));
assert!(!wg_config.contains(r"/"));
Ok(())
}
fn _generate_hosts() -> Result<Vec<WireguardHost>> {
let kp1 = WireguardKeypair::new()?;
let h1 = WireguardHost {
name: "foo1".to_string(),
address: "127.0.0.1".parse()?,
endpoint: Some("1.1.1.1".parse()?),
listenport: 80,
keypair: kp1,
};
let kp2 = WireguardKeypair::new()?;
let h2 = WireguardHost {
name: "foo2".to_string(),
address: "127.0.0.1".parse()?,
endpoint: None,
listenport: 80,
keypair: kp2,
};
let wg_hosts: Vec<WireguardHost> = vec![h1, h2];
Ok(wg_hosts)
}
#[test]
fn host_generation() -> anyhow::Result<()> {
let wg_hosts = _generate_hosts()?;
assert_eq!(wg_hosts[0].name, "foo1");
assert_eq!(wg_hosts[1].name, "foo2");
Ok(())
}
#[test]
fn device_generation() -> anyhow::Result<()> {
let wg_hosts = _generate_hosts()?;
let wg_device = WireguardDevice {
name: "foo".to_string(),
interface: wg_hosts[0].clone(),
peer: wg_hosts[1].clone(),
};
assert_eq!(wg_device.name, "foo");
assert_eq!(wg_hosts[0].name, "foo1");
Ok(())
}
#[test]
fn host_cloning() -> anyhow::Result<()> {
let wg_hosts = _generate_hosts()?;
let wg_h1 = &wg_hosts[0];
let wg_h2 = &wg_hosts[1];
let wg_device = WireguardDevice {
name: "foo".to_string(),
interface: wg_h1.clone(),
peer: wg_h2.clone(),
};
assert_eq!(wg_device.name, "foo");
assert_eq!(wg_hosts[0].name, "foo1");
assert_eq!(wg_device.interface.keypair.public, wg_h1.keypair.public);
assert_eq!(wg_device.interface.keypair.private, wg_h1.keypair.private);
Ok(())
}
#[test]
fn device_cloning() -> anyhow::Result<()> {
let wg_hosts = _generate_hosts()?;
let wg_h1 = &wg_hosts[0];
let wg_h2 = &wg_hosts[1];
let wg_device = WireguardDevice {
name: "foo".to_string(),
interface: wg_h1.clone(),
peer: wg_h2.clone(),
};
let wg_device2 = wg_device.clone();
assert_eq!(
wg_device.interface.keypair.public,
wg_device2.interface.keypair.public
);
assert_eq!(
wg_device.interface.keypair.private,
wg_device2.interface.keypair.private
);
Ok(())
}
#[test]
fn pubkey_generation() -> anyhow::Result<()> {
let privkey_b64 = "yPgz26A4S6RcniNaikFZrc0C0SyCW1moXmDP7AMeimE=";
let secret = StaticSecret::from(decode_key(privkey_b64)?);
let public = encode_key(PublicKey::from(&secret).as_bytes());
assert_eq!(public, "ISRq2SHZQDnSfV0VlmMEP4MbwfExE/iNHzthMQ7eNmY=");
Ok(())
}
#[test]
fn key_generation() -> anyhow::Result<()> {
let kp = WireguardKeypair::new()?;
assert!(!kp.public.ends_with('\n'));
assert!(!kp.private.ends_with('\n'));
assert!(!kp.public.contains("/"));
assert!(!kp.public.contains(r"/"));
assert!(!kp.private.contains("/"));
assert!(!kp.private.contains(r"/"));
Ok(())
}
#[test]
fn create_manager() -> anyhow::Result<()> {
let mgr = WireguardManager::new("foo-service")?;
let local_ip: IpAddr = "10.50.0.1".parse()?;
assert!(mgr.wg_local_ip == local_ip);
let remote_ip: IpAddr = "10.50.0.2".parse()?;
assert!(mgr.wg_remote_ip == remote_ip);
Ok(())
}
}