use dbus::{ channel::{MatchingReceiver, Token}, message::MatchRule, nonblock::SyncConnection, }; use dbus_crossroads::{Context, Crossroads, MethodErr}; use defguard_wireguard_rs::{key::Key, net::IpAddrMask, WireguardInterfaceApi}; use log::{debug, error, info, warn}; use rand::Rng; use std::{ collections::{hash_map::Entry, HashMap}, marker::PhantomData, net::SocketAddr, ops::DerefMut, path::PathBuf, str::FromStr, sync::Arc, time::SystemTime, }; use tokio::{net::TcpListener, sync::RwLock, task}; use crate::{daemon::*, daemon_config::*, daemon_network::*}; pub async fn start_dbus( state: Arc>, config_path: PathBuf, ) -> Result<(Arc, Token), DaemonError> { let mut cr = Crossroads::new(); let if_token = cr.register("de.a.maesch", move |b| { //b.signal::<(String, String), _>("Proposal", ("network", "peer_data")); let state_rem_network = state.clone(); let config_path_rem_network = config_path.clone(); let state_add_peer = state.clone(); let config_path_add_peer = config_path.clone(); b.method_with_cr_async( "AddNetwork", ("name", "key", "ip", "listen_port", "maesch_port"), ("success",), move |ctx, _, args: (String, String, String, u16, u16)| { debug!("Received AddNetwork"); handle_add_network(ctx, state.clone(), config_path.clone(), args) }, ); b.method_with_cr_async( "RemoveNetwork", ("name",), ("success",), move |ctx, _, args: (String,)| { debug!("Received RemoveNetwork"); handle_remove_network( ctx, state_rem_network.clone(), config_path_rem_network.clone(), args, ) }, ); b.method_with_cr_async( "AddPeer", ( "network", "key", "psk", "use_hostnames", "endpoint", "maesch_endpoint", "ips", ), ("success",), move |ctx, _, args: ( String, String, String, bool, String, String, Vec<(String, String)>, )| { debug!("Received AddPeer"); handle_add_peer( ctx, state_add_peer.clone(), config_path_add_peer.clone(), args, ) }, ); }); cr.insert("/de/a/maesch", &[if_token], ()); // drive dbus interface let (res, c) = dbus_tokio::connection::new_system_sync()?; cr.set_async_support(Some(( c.clone(), Box::new(|x| { tokio::spawn(x); }), ))); let _ = tokio::spawn(print_error(async { res.await; Result::::Err("lost connection to dbus!") })); let receive_token = c.start_receive( MatchRule::new_method_call(), Box::new(move |msg, conn| { cr.handle_message(msg, conn).unwrap(); true }), ); c.request_name("de.a.maesch", false, true, false).await?; Ok((c, receive_token)) } async fn handle_add_peer( mut ctx: Context, state: Arc>, config_path: PathBuf, (nw_name, key, may_psk, use_hostnames, may_endpoint, may_mäsch_endpoint, allowed_ips): ( String, String, String, bool, String, String, Vec<(String, String)>, ), ) -> PhantomData<(bool,)> { let key = match Key::from_str(&key) { Ok(k) => k, Err(e) => { warn!("AddPeer with bad key: {e}"); return ctx.reply(Err(MethodErr::invalid_arg(&e))); } }; let psk = match may_psk.as_str() { "" => None, _ => match Key::from_str(&may_psk) { Ok(k) => Some(k), Err(e) => { warn!("AddPeer with bad pre-shared key: {e}"); return ctx.reply(Err(MethodErr::invalid_arg(&e))); } }, }; // use_hostnames is already a boolean, so i guess we require it to be set? let endpoint = match may_endpoint.as_str() { "" => None, _ => match SocketAddr::from_str(&may_endpoint) { Ok(addr) => Some(Endpoint::Ip(addr)), Err(_) => { let mut parts_it = may_endpoint.split(':'); match (parts_it.next(), parts_it.next(), parts_it.next()) { (Some(domain), Some(port), None) if let Ok(port) = port.parse() => { Some(Endpoint::Domain(domain.to_owned(), port)) } _ => { warn!("AddPeer with bad endpoint: {may_endpoint}"); return ctx.reply(Err(MethodErr::invalid_arg("Could not parse endpoint"))); } } } }, }; let mäsch_endpoint = match SocketAddr::from_str(&may_mäsch_endpoint) { Ok(addr) => addr, Err(e) => { warn!("AddPeer with bad mäsch endpoint: {e}"); return ctx.reply(Err(MethodErr::invalid_arg(&e))); } }; // let may_allowed_ips = mapM (\(addr, hostname) -> (, if hostname == "" then Nothing else Just hostname) <$> readMaybe addr) allowed_ips let may_allowed_ips: Result)>, _> = allowed_ips .into_iter() .map(|(addr, hostname)| { addr.parse() .map(|ip_mask| (ip_mask, if hostname == "" { None } else { Some(hostname) })) }) .collect(); let allowed_ips = match may_allowed_ips { Ok(v) => v, Err(e) => { warn!("AddPeer with bad allowed ips: {e}"); return ctx.reply(Err(MethodErr::invalid_arg(&e))); } }; let mut state_rw_guard = state.write().await; if !state_rw_guard.conf.networks.contains_key(&nw_name) { warn!("AddPeer for non-existent network"); return ctx.reply(Err(MethodErr::invalid_arg("bad network"))); }; let allowed_ips_without_domains = allowed_ips.iter().map(|(ip, _)| ip.clone()).collect(); let wg_api = &state_rw_guard .nw_handles .get(&nw_name) .expect("state.conf.networks and state.nw_handles desynced") .0; match add_peer( wg_api, key.clone(), psk.clone(), endpoint.clone(), allowed_ips_without_domains, ) .await { Ok(_) => info!("Added peer"), Err(e) => { warn!("AddPeer failed: {e}"); return ctx.reply(Err(MethodErr::invalid_arg(&e))); } }; match state_rw_guard .conf .networks .get_mut(&nw_name) .expect("state.conf.networks changed while lock was held") .peers .entry(key) { Entry::Vacant(e) => { e.insert(PeerConfig { psk, ips: allowed_ips, use_hostnames, endpoint, last_changed: SystemTime::now(), known_to: vec![], mäsch_endpoint, }); } Entry::Occupied(e) => { let r = e.into_mut(); r.psk = psk; r.ips = allowed_ips; r.use_hostnames = use_hostnames; r.endpoint = endpoint; r.last_changed = SystemTime::now(); r.mäsch_endpoint = mäsch_endpoint; } }; match write_config(&state_rw_guard.conf, &config_path) { Ok(_) => info!("Synced config"), Err(e) => { error!("Couldn't sync config: {e}"); return ctx.reply(Err(MethodErr::failed(&e))); } }; return ctx.reply(Ok((true,))); } async fn handle_remove_network( mut ctx: Context, state: Arc>, config_path: PathBuf, (name,): (String,), ) -> PhantomData<(bool,)> { let mut state_rw_guard = state.write().await; if let Some(_) = state_rw_guard.conf.networks.remove(&name) { let (wg_api, h) = state_rw_guard .nw_handles .remove(&name) .expect("state.conf.networks and state.nw_handles desynced"); h.abort(); let ri = wg_api.remove_interface(); let _ = h.await; match write_config(&state_rw_guard.conf, &config_path) { Ok(_) => info!("Synced config"), Err(e) => { error!("Couldn't sync config: {e}"); return ctx.reply(Err(MethodErr::failed(&e))); } }; drop(state_rw_guard); match ri { Ok(_) => info!("Removed network: {name}"), Err(e) => { error!("Removing network: {e}"); return ctx.reply(Err(MethodErr::failed(&e))); } }; } else { warn!("Tried to remove non-existent network: {name}"); return ctx.reply(Err(MethodErr::invalid_arg("bad network"))); } ctx.reply(Ok((true,))) } async fn handle_add_network( mut ctx: Context, state: Arc>, config_path: PathBuf, (name, may_key, may_ip, may_lp, may_mp): (String, String, String, u16, u16), ) -> PhantomData<(bool,)> { let mut state_rw_guard = state.write().await; let state_rw = state_rw_guard.deref_mut(); // Scary! let prev_entry = state_rw.conf.networks.remove(&name); let key = if may_key.as_str() == "" { prev_entry .as_ref() .map(|nw| nw.privkey.clone()) .unwrap_or_else(|| Key::new(rand::thread_rng().gen()).to_string()) } else { may_key }; // we store the ip as the original string, but should validate it regardless let (ip, ip_string) = match may_ip.as_str() { "" => match prev_entry.as_ref() { Some(e) => ( IpAddrMask::from_str(&e.address) .expect("Impossible: old config broken!") .ip, e.address.clone(), ), None => { warn!("AddNetwork with no ip"); prev_entry.map(|pe| state_rw.conf.networks.insert(name, pe)); return ctx.reply(Err(MethodErr::invalid_arg("ip required"))); } }, _ => match IpAddrMask::from_str(&may_ip) { Ok(ip_mask) => (ip_mask.ip, may_ip), Err(_) => { warn!("AddNetwork with bad ip"); prev_entry.map(|pe| state_rw.conf.networks.insert(name, pe)); return ctx.reply(Err(MethodErr::invalid_arg("invalid ip"))); } }, }; let lp = if may_lp == 0 { prev_entry .as_ref() .map(|nw| nw.listen_port) .unwrap_or(25565) } else { may_lp }; let mp = if may_mp == 0 { prev_entry.as_ref().map(|nw| nw.mäsch_port).unwrap_or(51820) } else { may_mp }; let (wg_api, hostnames) = match add_network( name.clone(), key.clone(), ip_string.clone(), lp, &HashMap::new(), ) .await { Ok(v) => v, Err(e) => { warn!("AddNetwork couldn't add network: {e}"); prev_entry.map(|pe| state_rw.conf.networks.insert(name, pe)); return ctx.reply(Err(MethodErr::failed(&e))); } }; let listener = match TcpListener::bind((ip, mp)).await { Ok(l) => l, Err(e) => { let _ = wg_api.remove_interface(); warn!("AddNetwork couldn't start listener: {e}"); prev_entry.map(|pe| state_rw.conf.networks.insert(name, pe)); return ctx.reply(Err(MethodErr::failed(&e))); } }; let h = task::spawn(print_error(run_network( state.clone(), listener, name.clone(), ))); state_rw.nw_handles.insert(name.clone(), (wg_api, h)); state_rw.conf.networks.insert( name, Network { privkey: key, address: ip_string, listen_port: lp, peers: prev_entry.map_or_else(|| HashMap::new(), |nw: Network| nw.peers), mäsch_port: mp, }, ); // NOTE this _is_ thread-safe, as we hold an exclusive write handle to the state. match write_config(&state_rw.conf, &config_path) { Ok(_) => info!("Synced config"), Err(e) => error!("Couldn't sync config: {e}"), } // similarly, this is still racy w.r.t. other processes running on the system, but at least // no other thread from this program should be able to concurrently call this match sync_hostnames(&hostnames) { Ok(_) => (), Err(e) => error!("Failed to sync hostnames to disk: {e}"), }; ctx.reply(Ok((true,))) }