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::HashMap, marker::PhantomData, ops::DerefMut, path::PathBuf, str::FromStr, sync::Arc, }; 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(); 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, ) }, ); }); 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)) } pub 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,))) } // TODO make this merge correctly... pub 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(); // TODO do more with the entry... 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"); 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"); 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}"); 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}"); 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| 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,))) }