use std::net::SocketAddr;
use foundation_wireguard::{NetworkId, SeedBits, WgSeed};
use super::config::{ContainerConfig, PortMapping, PortProtocol};
pub const ENV_WG_SECRET: &str = "WG_SECRET";
pub const ENV_WG_NETWORK: &str = "WG_NETWORK";
pub const ENV_WG_SEED_ENDPOINTS: &str = "WG_SEED_ENDPOINTS";
pub const ENV_WG_RELAY: &str = "WG_RELAY";
#[derive(Debug, Clone)]
pub struct WgNetworkSecret {
pub seed: WgSeed,
pub network_id: NetworkId,
}
impl WgNetworkSecret {
#[must_use]
pub fn seed_base64url(&self) -> String {
self.seed.to_base64url()
}
#[must_use]
pub fn network_id_hex(&self) -> String {
self.network_id.to_hex()
}
pub fn save_to_file(&self, path: &str) -> std::io::Result<()> {
let content = format!(
"{}=\"{}\"\n{}=\"{}\"\n",
ENV_WG_SECRET,
self.seed_base64url(),
ENV_WG_NETWORK,
self.network_id_hex(),
);
if let Some(parent) = std::path::Path::new(path).parent() {
let _ = std::fs::create_dir_all(parent);
}
std::fs::write(path, content)
}
#[must_use]
pub fn load_from_file(path: &str) -> Option<Self> {
let content = std::fs::read_to_string(path).ok()?;
let mut seed_str = None;
let mut net_str = None;
for line in content.lines() {
if let Some((key, val)) = line.split_once('=') {
let val = val.trim().trim_matches('"');
match key.trim() {
ENV_WG_SECRET => seed_str = Some(val.to_string()),
ENV_WG_NETWORK => net_str = Some(val.to_string()),
_ => {}
}
}
}
let seed_str = seed_str?;
let seed = foundation_wireguard::WgSeed::from_base64url(&seed_str).ok()?;
let network_id = if let Some(hex) = net_str {
foundation_wireguard::NetworkId::from_hex(&hex).ok()?
} else {
seed.derive_network_id()
};
Some(Self { seed, network_id })
}
}
#[must_use]
pub fn generate_network_secret() -> WgNetworkSecret {
let seed = WgSeed::generate(SeedBits::Bits256)
.expect("OS random source unavailable — cannot generate network secret");
let network_id = seed.derive_network_id();
WgNetworkSecret { seed, network_id }
}
#[must_use]
pub fn generate_network_secret_with_id(network_id: NetworkId) -> WgNetworkSecret {
let seed = WgSeed::generate(SeedBits::Bits256)
.expect("OS random source unavailable");
WgNetworkSecret {
seed,
network_id,
}
}
pub trait WireguardInjector {
fn inject_wireguard(
self,
secret: &WgNetworkSecret,
seed_endpoints: &[SocketAddr],
relay_enabled: bool,
) -> Self;
}
impl WireguardInjector for ContainerConfig {
fn inject_wireguard(
mut self,
secret: &WgNetworkSecret,
seed_endpoints: &[SocketAddr],
relay_enabled: bool,
) -> Self {
self.env.push((
ENV_WG_SECRET.to_string(),
secret.seed_base64url(),
));
self.env.push((
ENV_WG_NETWORK.to_string(),
secret.network_id_hex(),
));
if !seed_endpoints.is_empty() {
let eps: Vec<String> = seed_endpoints.iter().map(|a| a.to_string()).collect();
self.env.push((
ENV_WG_SEED_ENDPOINTS.to_string(),
eps.join(","),
));
}
if relay_enabled {
self.env.push((ENV_WG_RELAY.to_string(), "true".to_string()));
}
self
}
}
pub fn designate_seed(config: &mut ContainerConfig, wg_port: u16) {
config.ports.push(super::config::PortMapping {
container_port: wg_port,
host_port: None, protocol: PortProtocol::Udp,
});
}
pub fn designate_relay(config: &mut ContainerConfig, relay_port: u16, wg_port: u16) {
config.ports.push(super::config::PortMapping {
container_port: relay_port,
host_port: None,
protocol: PortProtocol::Udp,
});
config.ports.push(super::config::PortMapping {
container_port: wg_port,
host_port: None,
protocol: PortProtocol::Udp,
});
config.env.push((ENV_WG_RELAY.to_string(), "true".to_string()));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_produces_valid_secret() {
let secret = generate_network_secret();
assert_eq!(secret.seed.bits(), SeedBits::Bits256);
assert_eq!(secret.seed_base64url().len(), 43); assert_eq!(secret.network_id_hex().len(), 32); }
#[test]
fn injector_adds_env_vars() {
let secret = generate_network_secret();
let ep: SocketAddr = "10.0.0.1:51820".parse().expect("addr");
let config = ContainerConfig::new("test-image")
.inject_wireguard(&secret, &[ep], false);
let has_secret = config.env.iter().any(|(k, _)| k == ENV_WG_SECRET);
let has_network = config.env.iter().any(|(k, _)| k == ENV_WG_NETWORK);
let has_endpoints = config.env.iter().any(|(k, v)| k == ENV_WG_SEED_ENDPOINTS && v.contains("51820"));
assert!(has_secret, "WG_SECRET missing");
assert!(has_network, "WG_NETWORK missing");
assert!(has_endpoints, "WG_SEED_ENDPOINTS missing or wrong value");
}
#[test]
fn injector_no_endpoints_when_empty() {
let secret = generate_network_secret();
let config = ContainerConfig::new("test-image")
.inject_wireguard(&secret, &[], true);
let has_endpoints = config.env.iter().any(|(k, _)| k == ENV_WG_SEED_ENDPOINTS);
assert!(!has_endpoints, "WG_SEED_ENDPOINTS should not be set when empty");
}
#[test]
fn designate_relay_adds_ports_and_env() {
let secret = generate_network_secret();
let mut config = ContainerConfig::new("relay-node")
.inject_wireguard(&secret, &[], true);
designate_relay(&mut config, 51821, 51820);
let has_relay_env = config.env.iter().any(|(k, v)| k == ENV_WG_RELAY && v == "true");
assert!(has_relay_env, "WG_RELAY=true missing");
let udp_ports: Vec<_> = config.ports.iter()
.filter(|p| p.protocol == PortProtocol::Udp)
.collect();
assert_eq!(udp_ports.len(), 2);
}
}