use atomic_write_file::AtomicWriteFile; use base64::prelude::*; use core::net::SocketAddr; use dbus::{channel::MatchingReceiver, message::MatchRule}; use dbus_crossroads::{Crossroads, MethodErr}; use defguard_wireguard_rs::{ host::Peer, key::Key, net::IpAddrMask, InterfaceConfiguration, WGApi, WireguardInterfaceApi, }; use futures::future; use log::{info, warn}; use rand::Rng; use serde::{Deserialize, Serialize}; use std::{ collections::{BTreeSet, HashMap}, fs::File, io::{ErrorKind, Read, Write}, net::ToSocketAddrs, sync::Arc, time::SystemTime, }; use thiserror::Error; use tokio::{net::TcpListener, runtime::Builder, sync::RwLock, task}; 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}")] IpMaskParse(#[from] defguard_wireguard_rs::net::IpAddrParseError), #[error("{0}")] WgInterfaceError(#[from] defguard_wireguard_rs::error::WireguardInterfaceError), #[error("{0}")] DbusError(#[from] dbus::Error), } #[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 { 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, last_changed: SystemTime, known_to: Vec, mäsch_endpoint: SocketAddr, } fn default_wg_port() -> u16 { 51820 } #[derive(Serialize, Deserialize)] struct Network { 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: HashMap, mäsch_port: u16, } #[derive(Serialize, Deserialize, Default)] struct Config { networks: HashMap, } struct State { conf: Config, apis: HashMap, hostfile: Option<(String, BTreeSet)>, } impl Drop for State { fn drop(&mut self) { for api in self.apis.values() { let _ = api.remove_interface(); } } } 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 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 } }; let mut state = State { conf: Config::default(), apis: HashMap::new(), hostfile, }; for (name, nw) in &config.networks { add_network(&mut state, name.clone(), nw.privkey.clone(), nw.address.clone(), nw.listen_port, &nw.peers)?; info!("loaded configuration for {0}", name); } info!("loaded all existing configurations"); state.conf = config; let state = Arc::new(RwLock::new(state)); let rt = Builder::new_current_thread().enable_all().build()?; rt.block_on(run_listeners(state))?; Ok(()) } fn add_network(state: &mut State, name: String, privkey: String, address: String, port: u16, peers: &HashMap) -> Result<(), DaemonError> { let wg = WGApi::new(name.clone(), false)?; let defguard_peers = peers .iter() .map(|(peer_key, p)| Peer { public_key: peer_key.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: name.clone(), prvkey: privkey, address: address, port: port as u32, peers: defguard_peers, })?; if let Some((hosts_str, hosts)) = &mut state.hostfile { peers .values() .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(); } state.apis.insert(name, wg); if let Some((hosts_str, _)) = &state.hostfile { info!("proposed next hosts file: {hosts_str}"); let mut f = AtomicWriteFile::open("/etc/hosts")?; f.write(hosts_str.as_bytes())?; f.commit()?; } Ok(()) } async fn run_listeners(state: Arc>) -> Result<(), DaemonError> { for (name, nw) in &state.read().await.conf.networks { let addr = IpAddrMask::from_str(&nw.address)?.ip; let listener = TcpListener::bind((addr, nw.mäsch_port)).await?; task::spawn(make_fatal(run_network( state.clone(), listener, name.clone(), ))); } let mut cr = Crossroads::new(); let state_ref = state.clone(); let if_token = cr.register("de.69owo.maesch", move |b| { let state_ref = state_ref.clone(); b.signal::<(String, String), _>("Proposal", ("network", "peer_data")); b.method_with_cr_async("AddNetwork", ("name", "key", "ip", "listen_port", "maesch_port"), ("success",), move |mut ctx, cr, (name, may_key, may_ip, may_lp, may_mp): (String, String, String, u16, u16)| async move { // NOTE: this is kinda stupid: we convert to a string later anyways, as thats what // defguard_wg takes... let key = Key::new(match may_key.as_str() { "" => rand::thread_rng().gen(), _ => match BASE64_STANDARD.decode(may_key) { Ok(v) if v.len() == 32 => v.try_into().unwrap(), _ => return ctx.reply(Err(MethodErr::invalid_arg("bad key"))) }, }); // we store the ip as the original string, but should validate it regardless let (ip, ip_string) = match may_ip.as_str() { "" => todo!(), _ => match IpAddrMask::from_str(&may_ip) { Err(_) => return ctx.reply(Err(MethodErr::invalid_arg("invalid ip"))), Ok(ip_mask) => (ip_mask.ip, may_ip), }, }; let lp = if may_lp == 0 { 25565 } else { may_lp }; let mp = if may_mp == 0 { 51820 } else { may_mp }; let mut st_wr = state_ref.write().await; match add_network(&mut st_wr, name, key.to_string(), ip_string, lp, &HashMap::new()) { Ok(_) => (), Err(e) => return ctx.reply(Err(MethodErr::failed(&e))), }; //let listener = TcpListener::bind((ip, mp)).await?; ctx.reply(Ok((true,))) }); }); cr.insert("/de/69owo/maesch", &[if_token], state.clone()); let (res, c) = dbus_tokio::connection::new_session_sync()?; let _ = tokio::spawn(make_fatal(async { res.await; Result::::Err("lost connection to dbus!") })); c.start_receive( MatchRule::new_method_call(), Box::new(move |msg, conn| { cr.handle_message(msg, conn).unwrap(); true }), ); c.request_name("de.69owo.maesch", true, true, false).await?; future::pending::().await } async fn make_fatal>>( f: F, ) -> () { match f.await { Err(e) => { eprintln!("oh no: {e}"); std::process::exit(1); } _ => (), }; } async fn run_network( state: Arc>, sock: TcpListener, nw_name: String, ) -> Result<(), DaemonError> { Ok(()) }