use atomic_write_file::AtomicWriteFile; use core::net::SocketAddr; use defguard_wireguard_rs::{host::Peer, InterfaceConfiguration, WGApi, WireguardInterfaceApi}; use defguard_wireguard_rs::{key::Key, net::IpAddrMask}; use log::{info, warn}; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; use std::fs::File; use std::io::{ErrorKind, Read, Write}; use std::net::{TcpListener, ToSocketAddrs}; use thiserror::Error; use xdg::BaseDirectories; use std::str::FromStr; #[derive(Debug, Error)] pub enum DaemonError { #[error("{0}")] Io(#[from] std::io::Error), #[error("{0}")] XdgBase(#[from] xdg::BaseDirectoriesError), // TODO hier wärs nett zu unterscheiden was decoded wurde #[error("{0}")] Decoding(#[from] serde_json::Error), #[error("{0}")] WgInterfaceError(#[from] defguard_wireguard_rs::error::WireguardInterfaceError), } #[derive(Serialize, Deserialize, Clone)] enum Endpoint { Ip(SocketAddr), Domain(String, u16), } // subset of defguard_wireguard_rs::host::Peer, with hostname added #[derive(Serialize, Deserialize)] struct PeerConfig { pubkey: Key, psk: Option, ips: Vec<(IpAddrMask, Option)>, // if false: the hostnames are kept around for sharing, but we personally do not use them use_hostnames: bool, endpoint: Option, } fn default_wg_port() -> u16 { 51820 } #[derive(Serialize, Deserialize)] struct Network { name: String, privkey: String, // this really should be a different type, but this is what defguard takes... address: String, #[serde(default = "default_wg_port")] listen_port: u16, peers: Vec, } #[derive(Serialize, Deserialize, Default)] struct Config { networks: Vec, } pub fn daemon() -> Result<(), DaemonError> { let config_path = BaseDirectories::with_prefix("mäsch")?.place_state_file("daemon.json")?; let config: Config = match File::open(config_path) { Ok(f) => serde_json::from_reader(f)?, Err(e) => match e.kind() { ErrorKind::NotFound => Config::default(), _ => Err(e)?, }, }; info!("read config"); //let networks = vec![Network { // name: "kek".to_string(), // privkey: "OK9WQudPVO5rXxcdxdtTzRmJzVu+KuqLMstYsZd8mWE=".to_string(), // address: "1.2.3.4".to_string(), // listen_port: 5221, // peers: vec![PeerConfig { // pubkey: Key::from_str("Osrxi/bRVK+FQit7YMbIgSaOWmRDOZQoh/7ddV4eEE8=").unwrap(), // psk: Some(Key::from_str("wFiG3II9ivYBn+xjLGChC0PjNlbOibZ1K6pmspPD0Hg=").unwrap()), // use_hostnames: true, // endpoint: Some(Endpoint::Domain("alex.69owo.de".to_string(), 12456)), // ips: vec![(IpAddrMask::from_str("5.4.3.2/24").unwrap(), Some("blah.blub".to_owned()))], // }], //}]; // TODO call wg.remove_interface on program exit using a drop impl on an 'Interface' struct // containing the Network and WGApi let mut hostfile = match File::open("/etc/hosts") { Ok(mut f) => { let mut r = String::new(); f.read_to_string(&mut r)?; let seen_hostnames: BTreeSet = r .lines() .map(|l| { l.split_whitespace() .take_while(|dom| dom.chars().next().unwrap() != '#') .skip(1) }) .flatten() .map(|dom| dom.to_owned()) .collect(); Some((r, seen_hostnames)) } Err(e) => { warn!("failed to read /etc/hosts: {e}"); None } }; for nw in config.networks { let wg = WGApi::new(nw.name.clone(), false)?; let defguard_peers = nw .peers .iter() .map(|p| Peer { public_key: p.pubkey.clone(), preshared_key: p.psk.clone(), protocol_version: None, endpoint: p .endpoint .clone() .map(|e| match e { Endpoint::Ip(ep) => Some(ep), Endpoint::Domain(s, p) => (s, p) .to_socket_addrs() .ok() .map(|mut it| it.next()) .flatten(), }) .flatten(), last_handshake: None, tx_bytes: 0, rx_bytes: 0, persistent_keepalive_interval: None, allowed_ips: p.ips.iter().map(|(ip_mask, _)| ip_mask.clone()).collect(), }) .collect(); wg.create_interface()?; wg.configure_interface(&InterfaceConfiguration { name: nw.name.clone(), prvkey: nw.privkey, address: nw.address, port: nw.listen_port as u32, peers: defguard_peers, })?; if let Some((hosts_str, hosts)) = &mut hostfile { nw.peers .iter() .map(|peer| { if peer.use_hostnames { peer.ips .iter() .map(|(mask, may_dom)| { if let Some(dom) = may_dom && hosts.insert(dom.clone()) { hosts_str.push_str(&format!("{}", mask.ip)); hosts_str.push('\t'); hosts_str.push_str(&dom); hosts_str.push('\n'); } }) .count(); } }) .count(); } info!("loaded configuration for {0}", nw.name); } info!("loaded all existing configurations"); if let Some((hosts_str, _)) = &hostfile { info!("proposed next hosts file: {hosts_str}"); let mut f = AtomicWriteFile::open("/etc/hosts")?; f.write(hosts_str.as_bytes())?; f.commit(); } // TODO open dbus & network interfaces Ok(()) }