From 296cd7b4963e59955ec4b3eebe740b54e07b69b7 Mon Sep 17 00:00:00 2001 From: Jake McGinty Date: Wed, 7 Apr 2021 17:00:52 +0900 Subject: [PATCH] client: changeable hosts file settings Introduces `--hosts-path [PATH]` and `--no-write-hosts` options in `innernet`. This could be further improved to have a persistent setting in a config file i.e. /etc/innernet.conf (which doesn't currently exist). Fixes #6 --- client/src/main.rs | 79 +++++++++++++++++++++++++++++++++++--------- hostsfile/src/lib.rs | 30 +++++++++-------- 2 files changed, 80 insertions(+), 29 deletions(-) diff --git a/client/src/main.rs b/client/src/main.rs index c37e1b9..64994cd 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -30,11 +30,33 @@ struct Opt { command: Option, } +#[derive(Debug, StructOpt)] +struct HostsOpt { + /// The path to write hosts to. + #[structopt(long, default_value = "/etc/hosts")] + hosts_path: PathBuf, + + /// Don't write to any hosts files. This takes precedence over --hosts-path. + #[structopt(long)] + no_write_hosts: bool, +} + +impl From for Option { + fn from(opt: HostsOpt) -> Self { + (!opt.no_write_hosts).then(|| opt.hosts_path) + } +} + #[derive(Debug, StructOpt)] enum Command { /// Install a new innernet config. #[structopt(alias = "redeem")] - Install { config: PathBuf }, + Install { + config: PathBuf, + + #[structopt(flatten)] + hosts: HostsOpt, + }, /// Enumerate all innernet connections. #[structopt(alias = "list")] @@ -60,11 +82,19 @@ enum Command { #[structopt(long, default_value = "60")] interval: u64, + #[structopt(flatten)] + hosts: HostsOpt, + interface: Interface, }, /// Fetch and update your local interface with the latest peer list. - Fetch { interface: Interface }, + Fetch { + interface: Interface, + + #[structopt(flatten)] + hosts: HostsOpt, + }, /// Bring down the interface (equivalent to "wg-quick down [interface]") Down { interface: Interface }, @@ -125,7 +155,7 @@ impl std::error::Error for ClientError { } } -fn update_hosts_file(interface: &str, peers: &Vec) -> Result<(), Error> { +fn update_hosts_file(interface: &str, hosts_path: PathBuf, peers: &Vec) -> Result<(), Error> { println!( "{} updating {} with the latest peers.", "[*]".dimmed(), @@ -139,12 +169,12 @@ fn update_hosts_file(interface: &str, peers: &Vec) -> Result<(), Error> { &format!("{}.{}.wg", peer.contents.name, interface), ); } - hosts_builder.write()?; + hosts_builder.write_to(hosts_path)?; Ok(()) } -fn install(invite: &Path) -> Result<(), Error> { +fn install(invite: &Path, hosts_file: Option) -> Result<(), Error> { let theme = ColorfulTheme::default(); shared::ensure_dirs_exist(&[*CLIENT_CONFIG_PATH])?; let mut config = InterfaceConfig::from_file(invite)?; @@ -205,7 +235,7 @@ fn install(invite: &Path) -> Result<(), Error> { .set_private_key(keypair.private) .apply(&iface)?; - fetch(&iface, false)?; + fetch(&iface, false, hosts_file)?; if Confirm::with_theme(&theme) .with_prompt(&format!( @@ -228,8 +258,12 @@ fn install(invite: &Path) -> Result<(), Error> { {systemctl_enable}{interface} - See the documentation for more detailed instruction on managing your interface - and your network. + By default, innernet will write to your /etc/hosts file for peer name + resolution. To disable this behavior, use the --no-write-hosts or --write-hosts [PATH] + options. + + See the manpage or innernet GitHub repo for more detailed instruction on managing your + interface and network. Have fun! ", star = "[*]".dimmed(), @@ -241,9 +275,13 @@ fn install(invite: &Path) -> Result<(), Error> { Ok(()) } -fn up(interface: &str, loop_interval: Option) -> Result<(), Error> { +fn up( + interface: &str, + loop_interval: Option, + hosts_path: Option, +) -> Result<(), Error> { loop { - fetch(interface, true)?; + fetch(interface, true, hosts_path.clone())?; match loop_interval { Some(interval) => thread::sleep(interval), None => break, @@ -253,7 +291,11 @@ fn up(interface: &str, loop_interval: Option) -> Result<(), Error> { Ok(()) } -fn fetch(interface: &str, bring_up_interface: bool) -> Result<(), Error> { +fn fetch( + interface: &str, + bring_up_interface: bool, + hosts_path: Option, +) -> Result<(), Error> { let config = InterfaceConfig::from_interface(interface)?; let interface_up = if let Ok(interfaces) = DeviceInfo::enumerate() { interfaces.iter().any(|name| name == interface) @@ -349,7 +391,9 @@ fn fetch(interface: &str, bring_up_interface: bool) -> Result<(), Error> { if device_config_changed { device_config_builder.apply(&interface)?; - update_hosts_file(interface, &peers)?; + if let Some(path) = hosts_path { + update_hosts_file(interface, path, &peers)?; + } println!( "\n{} updated interface {}\n", @@ -724,18 +768,23 @@ fn run(opt: Opt) -> Result<(), Error> { }); match command { - Command::Install { config } => install(&config)?, + Command::Install { config, hosts } => install(&config, hosts.into())?, Command::Show { short, tree, interface, } => show(short, tree, interface)?, - Command::Fetch { interface } => fetch(&interface, false)?, + Command::Fetch { interface, hosts } => fetch(&interface, false, hosts.into())?, Command::Up { interface, daemon, + hosts, interval, - } => up(&interface, daemon.then(|| Duration::from_secs(interval)))?, + } => up( + &interface, + daemon.then(|| Duration::from_secs(interval)), + hosts.into(), + )?, Command::Down { interface } => wg::down(&interface)?, Command::AddPeer { interface } => add_peer(&interface)?, Command::AddCidr { interface } => add_cidr(&interface)?, diff --git a/hostsfile/src/lib.rs b/hostsfile/src/lib.rs index f36aee4..f22ae40 100644 --- a/hostsfile/src/lib.rs +++ b/hostsfile/src/lib.rs @@ -1,7 +1,7 @@ use std::{ collections::HashMap, fmt, - fs::{self, File}, + fs::{self, File, OpenOptions}, io::{BufRead, BufReader, Write}, net::IpAddr, path::{Path, PathBuf}, @@ -127,18 +127,20 @@ impl HostsBuilder { /// Inserts a new section to the specified hosts file. If there is a section with the same tag /// name already, it will be replaced with the new list instead. - pub fn write_to>(&self, hosts_file: P) -> Result<()> { - let hosts_file = hosts_file.as_ref(); + pub fn write_to>(&self, hosts_path: P) -> Result<()> { + let hosts_path = hosts_path.as_ref(); let begin_marker = format!("# DO NOT EDIT {} BEGIN", &self.tag); let end_marker = format!("# DO NOT EDIT {} END", &self.tag); - let mut lines = match File::open(hosts_file) { - Ok(file) => BufReader::new(file) - .lines() - .map(|line| line.unwrap()) - .collect::>(), - _ => Vec::new(), - }; + let hosts_file = OpenOptions::new() + .create(true) + .read(true) + .write(true) + .open(hosts_path)?; + let mut lines = BufReader::new(hosts_file) + .lines() + .map(|line| line.unwrap()) + .collect::>(); let begin = lines.iter().position(|line| line.trim() == begin_marker); let end = lines.iter().position(|line| line.trim() == end_marker); @@ -160,20 +162,20 @@ impl HostsBuilder { _ => { return Err(Box::new(Error(format!( "start or end marker missing in {:?}", - &hosts_file + &hosts_path )))); }, }; // The tempfile should be in the same filesystem as the hosts file. - let hosts_dir = hosts_file + let hosts_dir = hosts_path .parent() .expect("hosts file must be an absolute file path"); let temp_dir = tempfile::Builder::new().tempdir_in(hosts_dir)?; let temp_path = temp_dir.path().join("hosts"); // Copy the existing hosts file to preserve permissions. - fs::copy(&hosts_file, &temp_path)?; + fs::copy(&hosts_path, &temp_path)?; let mut file = File::create(&temp_path)?; @@ -192,7 +194,7 @@ impl HostsBuilder { } // Move the file atomically to avoid a partial state. - fs::rename(&temp_path, &hosts_file)?; + fs::rename(&temp_path, &hosts_path)?; Ok(()) }