diff options
-rw-r--r-- | de.a.maesch.conf | 9 | ||||
-rw-r--r-- | src/daemon.rs | 279 |
2 files changed, 162 insertions, 126 deletions
diff --git a/de.a.maesch.conf b/de.a.maesch.conf new file mode 100644 index 0000000..38cbfed --- /dev/null +++ b/de.a.maesch.conf @@ -0,0 +1,9 @@ +<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" + "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> +<busconfig> + <policy user="root"> + <allow own="de.a.maesch"/> + <allow send_destination="de.a.maesch"/> + <allow send_interface="de.a.maesch"/> + </policy> +</busconfig> diff --git a/src/daemon.rs b/src/daemon.rs index e1404a0..34e4474 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -2,19 +2,21 @@ use atomic_write_file::AtomicWriteFile; use base64::prelude::*; use core::net::SocketAddr; use dbus::{channel::MatchingReceiver, message::MatchRule}; -use dbus_crossroads::{Crossroads, MethodErr}; +use dbus_crossroads::{Crossroads, MethodErr, Context}; use defguard_wireguard_rs::{ host::Peer, key::Key, net::IpAddrMask, InterfaceConfiguration, WGApi, WireguardInterfaceApi, }; use futures::future; -use log::{info, warn}; +use log::{debug, info, warn}; use rand::Rng; use serde::{Deserialize, Serialize}; use std::{ collections::{BTreeSet, HashMap}, fs::File, io::{ErrorKind, Read, Write}, + ops::DerefMut, net::ToSocketAddrs, + marker::PhantomData, sync::Arc, time::SystemTime, }; @@ -91,16 +93,17 @@ struct Config { networks: HashMap<String, Network>, } +// TODO das überschreibt änderungen an /etc/hosts während der runtime :( struct State { conf: Config, - apis: HashMap<String, WGApi>, + nw_handles: HashMap<String, (WGApi, task::JoinHandle<()>)>, hostfile: Option<(String, BTreeSet<String>)>, } impl Drop for State { fn drop(&mut self) { - for api in self.apis.values() { - let _ = api.remove_interface(); + for (wg_api, _) in self.nw_handles.values() { + let _ = wg_api.remove_interface(); } } } @@ -140,42 +143,165 @@ pub fn daemon() -> Result<(), DaemonError> { } }; - let mut state = State { - conf: Config::default(), - apis: HashMap::new(), + let state = Arc::new(RwLock::new(State { + conf: config, + nw_handles: HashMap::new(), hostfile, - }; + })); + + let rt = Builder::new_current_thread().enable_all().build()?; + rt.block_on(init_networks(state))?; + + Ok(()) +} - for (name, nw) in &config.networks { - add_network( - &mut state, +async fn init_networks(state: Arc<RwLock<State>>) -> Result<(), DaemonError> { + let mut state_rw_guard = state.write().await; + let state_rw = state_rw_guard.deref_mut(); + + for (name, nw) in &state_rw.conf.networks { + let wg_api = add_network( + &mut state_rw.hostfile, name.clone(), nw.privkey.clone(), nw.address.clone(), nw.listen_port, &nw.peers, - )?; - info!("loaded configuration for {0}", name); + ).await?; + + let addr = IpAddrMask::from_str(&nw.address)?.ip; + let h = task::spawn(make_fatal(run_network( + state.clone(), + TcpListener::bind((addr, nw.mäsch_port)).await?, + name.clone(), + ))); + + state_rw.nw_handles.insert(name.clone(), (wg_api, h)); + + debug!("loaded configuration for {0}", name); } info!("loaded all existing configurations"); - state.conf = config; + drop(state_rw_guard); - let state = Arc::new(RwLock::new(state)); + let mut cr = Crossroads::new(); + let state_ref = state.clone(); - let rt = Builder::new_current_thread().enable_all().build()?; - rt.block_on(run_listeners(state))?; + let if_token = cr.register("de.a.maesch", move |b| { + b.signal::<(String, String), _>("Proposal", ("network", "peer_data")); + 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_ref.clone(), args) + }, + ); + }); - Ok(()) + cr.insert("/de/a/maesch", &[if_token], ()); + + 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(make_fatal(async { + res.await; + Result::<!, &'static str>::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?; + + future::pending::<!>().await + + //c.stop_receive(receive_token); } -fn add_network( - state: &mut State, +async fn handle_add_network( + mut ctx: Context, + state: Arc<RwLock<State>>, + (name, may_key, may_ip, may_lp, may_mp): (String, String, String, u16, u16), +) -> PhantomData<(bool,)> { + // 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(), + _ => { + warn!("AddNetwork with bad key"); + 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) { + 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 { 25565 } else { may_lp }; + let mp = if may_mp == 0 { 51820 } else { may_mp }; + + let mut state_rw_guard = state.write().await; + let state_rw = state_rw_guard.deref_mut(); + + let wg_api = match add_network( + &mut state_rw.hostfile, + name.clone(), + key.to_string(), + ip_string, + lp, + &HashMap::new(), + ).await { + Ok(wg_api) => wg_api, + Err(e) => { + warn!("AddNetwork couldn't add network: {e}"); + return ctx.reply(Err(MethodErr::failed(&e))) + }, + }; + + // TODO ins wg_api + 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(make_fatal(run_network( + state.clone(), + listener, + name.clone(), + ))); + + state_rw.nw_handles.insert(name.clone(), (wg_api, h)); + + ctx.reply(Ok((true,))) +} + +async fn add_network( + hostfile: &mut Option<(String, BTreeSet<String>)>, name: String, privkey: String, address: String, port: u16, peers: &HashMap<Key, PeerConfig>, -) -> Result<(), DaemonError> { +) -> Result<WGApi, DaemonError> { let wg = WGApi::new(name.clone(), false)?; let defguard_peers = peers .iter() @@ -211,7 +337,7 @@ fn add_network( peers: defguard_peers, })?; - if let Some((hosts_str, hosts)) = &mut state.hostfile { + if let Some((hosts_str, hosts)) = hostfile { peers .values() .map(|peer| { @@ -234,115 +360,15 @@ fn add_network( .count(); } - state.apis.insert(name, wg); - - if let Some((hosts_str, _)) = &state.hostfile { - info!("proposed next hosts file: {hosts_str}"); + if let Some((hosts_str, _)) = hostfile { + debug!("writing 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<RwLock<State>>) -> 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| { - 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, - )| { - let state_ref = state_ref.clone(); - 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::<!, &'static str>::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 + Ok(wg) } async fn make_fatal<E: std::fmt::Display, O, F: std::future::Future<Output = Result<O, E>>>( @@ -351,6 +377,7 @@ async fn make_fatal<E: std::fmt::Display, O, F: std::future::Future<Output = Res match f.await { Err(e) => { eprintln!("oh no: {e}"); + // TODO das soll lieber die tokio runtime beenden... std::process::exit(1); } _ => (), |