From e143b186a3eba9c03af20156ad3a2e729c638dc9 Mon Sep 17 00:00:00 2001 From: Hristo Venev Date: Tue, 19 Mar 2019 10:14:23 +0200 Subject: BREAKING CHANGES ifname is now passed as an argument before the config file. own_public_key is obtained from the interface. --- src/config.rs | 19 +------- src/main.rs | 144 ++++++++++++++++++++++++++++++++++++------------------- src/wg.rs | 63 ++++++++++++++++++++---- wgconfd@.service | 15 ++++++ 4 files changed, 166 insertions(+), 75 deletions(-) create mode 100644 wgconfd@.service diff --git a/src/config.rs b/src/config.rs index eab10fe..00874c2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -17,7 +17,6 @@ pub struct Source { #[serde(deny_unknown_fields)] #[derive(serde_derive::Serialize, serde_derive::Deserialize, Clone, PartialEq, Eq, Debug)] pub struct PeerConfig { - pub own_public_key: String, #[serde(default = "default_min_keepalive")] pub min_keepalive: u32, #[serde(default = "default_max_keepalive")] @@ -35,17 +34,11 @@ pub struct UpdateConfig { #[serde(deny_unknown_fields)] #[derive(serde_derive::Serialize, serde_derive::Deserialize, Clone, Debug)] pub struct Config { - pub ifname: String, - #[serde(default = "default_wg_command")] - pub wg_command: String, - #[serde(default = "default_curl_command")] - pub curl_command: String, - #[serde(flatten)] - pub peers: PeerConfig, + pub peer_config: PeerConfig, #[serde(flatten)] - pub update: UpdateConfig, + pub update_config: UpdateConfig, pub sources: Vec, } @@ -62,14 +55,6 @@ impl PeerConfig { } } -fn default_wg_command() -> String { - "wg".to_owned() -} - -fn default_curl_command() -> String { - "curl".to_owned() -} - fn default_min_keepalive() -> u32 { 10 } diff --git a/src/main.rs b/src/main.rs index 0ef17aa..f5ebb26 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,7 @@ struct Source { config: config::Source, data: Option, next_update: Instant, - backoff: Option, + backoff: Option, } impl Source { @@ -38,25 +38,31 @@ pub struct Device { update_config: config::UpdateConfig, sources: Vec, current: wg::Config, - curl_command: String, } impl Device { - pub fn new(c: config::Config) -> Device { - let dev = wg::Device::new(c.ifname, c.wg_command); - let current = wg::ConfigBuilder::new(&c.peers).build(); - Device { + pub fn new(ifname: String, c: config::Config) -> io::Result { + let dev = wg::Device::new(ifname)?; + + Ok(Device { dev, - peer_config: c.peers, - update_config: c.update, + peer_config: c.peer_config, + update_config: c.update_config, sources: c.sources.into_iter().map(Source::new).collect(), - current, - curl_command: c.curl_command, - } + current: wg::Config::default(), + }) } - fn make_config(&self, ts: SystemTime) -> (wg::Config, Vec, SystemTime) { - let mut next_update = ts + Duration::from_secs(3600); + fn refresh_period(&self) -> Duration { + Duration::from_secs(u64::from(self.update_config.refresh_period)) + } + + fn make_config( + &self, + public_key: &str, + ts: SystemTime, + ) -> (wg::Config, Vec, SystemTime) { + let mut t_cfg = ts + self.refresh_period(); let mut sources: Vec<(&Source, &proto::SourceConfig)> = vec![]; for src in self.sources.iter() { if let Some(ref data) = src.data { @@ -67,7 +73,7 @@ impl Device { if ts >= next.update_at { Some(&next.config) } else { - next_update = next_update.min(next.update_at); + t_cfg = t_cfg.min(next.update_at); None } }) @@ -76,7 +82,7 @@ impl Device { } } - let mut cfg = wg::ConfigBuilder::new(&self.peer_config); + let mut cfg = wg::ConfigBuilder::new(public_key, &self.peer_config); let mut errs = vec![]; for (src, sc) in sources.iter() { for peer in sc.servers.iter() { @@ -90,26 +96,28 @@ impl Device { } let cfg = cfg.build(); - (cfg, errs, next_update) + (cfg, errs, t_cfg) } pub fn update(&mut self) -> io::Result { - let now = Instant::now(); - let refresh = self.update_config.refresh_period; - let after_refresh = now + Duration::from_secs(u64::from(refresh)); - let mut next_update = after_refresh; + let refresh = self.refresh_period(); + let mut now = Instant::now(); + let mut t_refresh = now + refresh; + for src in self.sources.iter_mut() { if now < src.next_update { - next_update = next_update.min(src.next_update); + t_refresh = t_refresh.min(src.next_update); continue; } - let r = match fetch_source(&self.curl_command, &src.config.url) { + let r = fetch_source(&src.config.url); + now = Instant::now(); + let r = match r { Ok(r) => { eprintln!("<6>Updated [{}]", &src.config.url); src.data = Some(r); src.backoff = None; - src.next_update = after_refresh; + src.next_update = now + refresh; continue; } Err(r) => r, @@ -120,20 +128,22 @@ impl Device { let b = src.backoff.unwrap_or(if src.data.is_some() { refresh / 3 } else { - u32::min(10, refresh / 10) + Duration::from_secs(10).min(refresh / 10) }); + src.next_update = now + b; + t_refresh = t_refresh.min(src.next_update); let b = (b + b / 3).min(refresh); src.backoff = Some(b); - src.next_update = now + Duration::from_secs(u64::from(b)); - next_update = next_update.min(src.next_update); } + let now = Instant::now(); let sysnow = SystemTime::now(); - let (config, errors, upd_time) = self.make_config(sysnow); - let time_to_upd = upd_time + let public_key = self.dev.get_public_key()?; + let (config, errors, t_cfg) = self.make_config(&public_key, sysnow); + let time_to_cfg = t_cfg .duration_since(sysnow) .unwrap_or(Duration::from_secs(0)); - next_update = next_update.min(now + time_to_upd); + let t_cfg = now + time_to_cfg; if config != self.current { eprintln!("<5>Applying configuration update"); @@ -143,27 +153,49 @@ impl Device { self.dev.apply_diff(&self.current, &config)?; self.current = config; } - eprintln!("<6>Next configuration update after {:?}", time_to_upd); - Ok(next_update) + Ok(if t_cfg < t_refresh { + eprintln!("<6>Next configuration update after {:?}", time_to_cfg); + t_cfg + } else if t_refresh > now { + eprintln!("<6>Next refresh after {:?}", t_refresh.duration_since(now)); + t_refresh + } else { + now + }) } } -fn fetch_source(curl_command: &str, url: &str) -> io::Result { +fn fetch_source(url: &str) -> io::Result { + use std::env; + use std::ffi::{OsStr, OsString}; use std::process::{Command, Stdio}; - let out = Command::new(curl_command) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .arg("--fail") - .arg("--fail-early") - .arg("--") - .arg(url) - .output()?; + let curl = match env::var_os("CURL") { + None => OsString::new(), + Some(v) => v, + }; + let mut proc = Command::new(if curl.is_empty() { + OsStr::new("curl") + } else { + curl.as_os_str() + }); + + proc.stdin(Stdio::null()); + proc.stdout(Stdio::piped()); + proc.stderr(Stdio::null()); + proc.arg("--fail"); + proc.arg("--fail-early"); + proc.arg("--"); + proc.arg(url); + + let out = proc.output()?; if !out.status.success() { - return Err(io::Error::new(io::ErrorKind::Other, format!("Failed to download [{}]", url))); + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Failed to download [{}]", url), + )); } let mut de = serde_json::Deserializer::from_slice(&out.stdout); @@ -185,14 +217,20 @@ fn main() { use std::{env, process, thread}; let args: Vec = env::args().collect(); - if args.len() != 2 { + if args.len() != 3 { let arg0 = if !args.is_empty() { &args[0] } else { "wgconf" }; eprintln!("<1>Usage:"); - eprintln!("<1> {} CONFIG", arg0); + eprintln!("<1> {} IFNAME CONFIG", arg0); process::exit(1); } - let config = match load_config(&args[1]) { + let mut args = args.into_iter(); + let _ = args.next().unwrap(); + let ifname = args.next().unwrap(); + let config_path = args.next().unwrap(); + assert!(args.next().is_none()); + + let config = match load_config(&config_path) { Ok(c) => c, Err(e) => { eprintln!("<1>Failed to load config: {}", e); @@ -200,7 +238,14 @@ fn main() { } }; - let mut dev = Device::new(config); + let mut dev = match Device::new(ifname, config) { + Ok(dev) => dev, + Err(e) => { + eprintln!("<1>Failed to open device: {}", e); + process::exit(1); + } + }; + loop { let tm = match dev.update() { Ok(t) => t, @@ -210,8 +255,9 @@ fn main() { } }; let now = Instant::now(); - let sleep = tm.duration_since(now); - println!("Sleeping for {:?}", sleep); - thread::sleep(sleep); + if tm > now { + let sleep = tm.duration_since(now); + thread::sleep(sleep); + } } } diff --git a/src/wg.rs b/src/wg.rs index 94f9603..1de5574 100644 --- a/src/wg.rs +++ b/src/wg.rs @@ -8,6 +8,10 @@ use hash_map::HashMap; use std::collections::hash_map; use std::{error, fmt, io}; +use std::env; +use std::ffi::{OsStr, OsString}; +use std::process::{Command, Stdio}; + #[derive(Debug)] pub struct ConfigError { pub url: String, @@ -59,15 +63,25 @@ pub struct Config { peers: HashMap, } +impl Default for Config { + fn default() -> Config { + Config { + peers: HashMap::new(), + } + } +} + pub struct ConfigBuilder<'a> { peers: HashMap, + public_key: &'a str, pc: &'a config::PeerConfig, } impl<'a> ConfigBuilder<'a> { - pub fn new(pc: &'a config::PeerConfig) -> Self { + pub fn new(public_key: &'a str, pc: &'a config::PeerConfig) -> Self { ConfigBuilder { peers: HashMap::new(), + public_key, pc, } } @@ -144,7 +158,7 @@ impl<'a> ConfigBuilder<'a> { return; } - if p.peer.public_key == self.pc.own_public_key { + if p.peer.public_key == self.public_key { return; } @@ -169,7 +183,7 @@ impl<'a> ConfigBuilder<'a> { return; } - let ent = if p.base == self.pc.own_public_key { + let ent = if p.base == self.public_key { self.insert_with(err, s, &p.peer, |_| {}) } else { match self.peers.get_mut(&p.base) { @@ -186,18 +200,49 @@ impl<'a> ConfigBuilder<'a> { pub struct Device { ifname: String, - wg_command: String, } impl Device { - pub fn new(ifname: String, wg_command: String) -> Self { - Device { ifname, wg_command } + pub fn new(ifname: String) -> io::Result { + Ok(Device { ifname }) } - pub fn apply_diff(&mut self, old: &Config, new: &Config) -> io::Result<()> { - use std::process::{Command, Stdio}; + pub fn wg_command() -> Command { + let wg = match env::var_os("WG") { + None => OsString::new(), + Some(v) => v, + }; + + Command::new(if wg.is_empty() { + OsStr::new("wg") + } else { + wg.as_os_str() + }) + } + + pub fn get_public_key(&self) -> io::Result { + let mut proc = Device::wg_command(); + proc.stdin(Stdio::null()); + proc.stdout(Stdio::piped()); + proc.arg("show"); + proc.arg(&self.ifname); + proc.arg("public-key"); + + let r = proc.output()?; + if !r.status.success() { + return Err(io::Error::new(io::ErrorKind::Other, "Child process failed")); + } - let mut proc = Command::new(&self.wg_command); + let mut out = r.stdout; + if out.ends_with(b"\n") { + out.remove(out.len() - 1); + } + String::from_utf8(out) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid public key")) + } + + pub fn apply_diff(&mut self, old: &Config, new: &Config) -> io::Result<()> { + let mut proc = Device::wg_command(); proc.stdin(Stdio::piped()); proc.arg("set"); proc.arg(&self.ifname); diff --git a/wgconfd@.service b/wgconfd@.service new file mode 100644 index 0000000..d6e4127 --- /dev/null +++ b/wgconfd@.service @@ -0,0 +1,15 @@ +[Unit] +Description=Configure WireGuard interface +Wants=network-pre.target +Before=network-pre.target nftables.service systemd-networkd.service NetworkManager.service + +[Service] +Type=simple +CapabilityBoundingSet=CAP_NET_ADMIN +ExecStartPre=/usr/bin/wg setconf %i /etc/wireguard/%i.conf +ExecStart=/usr/local/bin/wgconfd %i /etc/wireguard/%i.json +StandardError=journal +SyslogLevelPrefix=true + +[Install] +WantedBy=multi-user.target -- cgit