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
pull/36/head
Jake McGinty 2021-04-07 17:00:52 +09:00 committed by GitHub
parent f0018c1052
commit 296cd7b496
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 80 additions and 29 deletions

View File

@ -30,11 +30,33 @@ struct Opt {
command: Option<Command>, command: Option<Command>,
} }
#[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<HostsOpt> for Option<PathBuf> {
fn from(opt: HostsOpt) -> Self {
(!opt.no_write_hosts).then(|| opt.hosts_path)
}
}
#[derive(Debug, StructOpt)] #[derive(Debug, StructOpt)]
enum Command { enum Command {
/// Install a new innernet config. /// Install a new innernet config.
#[structopt(alias = "redeem")] #[structopt(alias = "redeem")]
Install { config: PathBuf }, Install {
config: PathBuf,
#[structopt(flatten)]
hosts: HostsOpt,
},
/// Enumerate all innernet connections. /// Enumerate all innernet connections.
#[structopt(alias = "list")] #[structopt(alias = "list")]
@ -60,11 +82,19 @@ enum Command {
#[structopt(long, default_value = "60")] #[structopt(long, default_value = "60")]
interval: u64, interval: u64,
#[structopt(flatten)]
hosts: HostsOpt,
interface: Interface, interface: Interface,
}, },
/// Fetch and update your local interface with the latest peer list. /// 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]") /// Bring down the interface (equivalent to "wg-quick down [interface]")
Down { interface: Interface }, Down { interface: Interface },
@ -125,7 +155,7 @@ impl std::error::Error for ClientError {
} }
} }
fn update_hosts_file(interface: &str, peers: &Vec<Peer>) -> Result<(), Error> { fn update_hosts_file(interface: &str, hosts_path: PathBuf, peers: &Vec<Peer>) -> Result<(), Error> {
println!( println!(
"{} updating {} with the latest peers.", "{} updating {} with the latest peers.",
"[*]".dimmed(), "[*]".dimmed(),
@ -139,12 +169,12 @@ fn update_hosts_file(interface: &str, peers: &Vec<Peer>) -> Result<(), Error> {
&format!("{}.{}.wg", peer.contents.name, interface), &format!("{}.{}.wg", peer.contents.name, interface),
); );
} }
hosts_builder.write()?; hosts_builder.write_to(hosts_path)?;
Ok(()) Ok(())
} }
fn install(invite: &Path) -> Result<(), Error> { fn install(invite: &Path, hosts_file: Option<PathBuf>) -> Result<(), Error> {
let theme = ColorfulTheme::default(); let theme = ColorfulTheme::default();
shared::ensure_dirs_exist(&[*CLIENT_CONFIG_PATH])?; shared::ensure_dirs_exist(&[*CLIENT_CONFIG_PATH])?;
let mut config = InterfaceConfig::from_file(invite)?; let mut config = InterfaceConfig::from_file(invite)?;
@ -205,7 +235,7 @@ fn install(invite: &Path) -> Result<(), Error> {
.set_private_key(keypair.private) .set_private_key(keypair.private)
.apply(&iface)?; .apply(&iface)?;
fetch(&iface, false)?; fetch(&iface, false, hosts_file)?;
if Confirm::with_theme(&theme) if Confirm::with_theme(&theme)
.with_prompt(&format!( .with_prompt(&format!(
@ -228,8 +258,12 @@ fn install(invite: &Path) -> Result<(), Error> {
{systemctl_enable}{interface} {systemctl_enable}{interface}
See the documentation for more detailed instruction on managing your interface By default, innernet will write to your /etc/hosts file for peer name
and your network. 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(), star = "[*]".dimmed(),
@ -241,9 +275,13 @@ fn install(invite: &Path) -> Result<(), Error> {
Ok(()) Ok(())
} }
fn up(interface: &str, loop_interval: Option<Duration>) -> Result<(), Error> { fn up(
interface: &str,
loop_interval: Option<Duration>,
hosts_path: Option<PathBuf>,
) -> Result<(), Error> {
loop { loop {
fetch(interface, true)?; fetch(interface, true, hosts_path.clone())?;
match loop_interval { match loop_interval {
Some(interval) => thread::sleep(interval), Some(interval) => thread::sleep(interval),
None => break, None => break,
@ -253,7 +291,11 @@ fn up(interface: &str, loop_interval: Option<Duration>) -> Result<(), Error> {
Ok(()) Ok(())
} }
fn fetch(interface: &str, bring_up_interface: bool) -> Result<(), Error> { fn fetch(
interface: &str,
bring_up_interface: bool,
hosts_path: Option<PathBuf>,
) -> Result<(), Error> {
let config = InterfaceConfig::from_interface(interface)?; let config = InterfaceConfig::from_interface(interface)?;
let interface_up = if let Ok(interfaces) = DeviceInfo::enumerate() { let interface_up = if let Ok(interfaces) = DeviceInfo::enumerate() {
interfaces.iter().any(|name| name == interface) interfaces.iter().any(|name| name == interface)
@ -349,7 +391,9 @@ fn fetch(interface: &str, bring_up_interface: bool) -> Result<(), Error> {
if device_config_changed { if device_config_changed {
device_config_builder.apply(&interface)?; device_config_builder.apply(&interface)?;
update_hosts_file(interface, &peers)?; if let Some(path) = hosts_path {
update_hosts_file(interface, path, &peers)?;
}
println!( println!(
"\n{} updated interface {}\n", "\n{} updated interface {}\n",
@ -724,18 +768,23 @@ fn run(opt: Opt) -> Result<(), Error> {
}); });
match command { match command {
Command::Install { config } => install(&config)?, Command::Install { config, hosts } => install(&config, hosts.into())?,
Command::Show { Command::Show {
short, short,
tree, tree,
interface, interface,
} => 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 { Command::Up {
interface, interface,
daemon, daemon,
hosts,
interval, 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::Down { interface } => wg::down(&interface)?,
Command::AddPeer { interface } => add_peer(&interface)?, Command::AddPeer { interface } => add_peer(&interface)?,
Command::AddCidr { interface } => add_cidr(&interface)?, Command::AddCidr { interface } => add_cidr(&interface)?,

View File

@ -1,7 +1,7 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
fmt, fmt,
fs::{self, File}, fs::{self, File, OpenOptions},
io::{BufRead, BufReader, Write}, io::{BufRead, BufReader, Write},
net::IpAddr, net::IpAddr,
path::{Path, PathBuf}, 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 /// 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. /// name already, it will be replaced with the new list instead.
pub fn write_to<P: AsRef<Path>>(&self, hosts_file: P) -> Result<()> { pub fn write_to<P: AsRef<Path>>(&self, hosts_path: P) -> Result<()> {
let hosts_file = hosts_file.as_ref(); let hosts_path = hosts_path.as_ref();
let begin_marker = format!("# DO NOT EDIT {} BEGIN", &self.tag); let begin_marker = format!("# DO NOT EDIT {} BEGIN", &self.tag);
let end_marker = format!("# DO NOT EDIT {} END", &self.tag); let end_marker = format!("# DO NOT EDIT {} END", &self.tag);
let mut lines = match File::open(hosts_file) { let hosts_file = OpenOptions::new()
Ok(file) => BufReader::new(file) .create(true)
.lines() .read(true)
.map(|line| line.unwrap()) .write(true)
.collect::<Vec<_>>(), .open(hosts_path)?;
_ => Vec::new(), let mut lines = BufReader::new(hosts_file)
}; .lines()
.map(|line| line.unwrap())
.collect::<Vec<_>>();
let begin = lines.iter().position(|line| line.trim() == begin_marker); let begin = lines.iter().position(|line| line.trim() == begin_marker);
let end = lines.iter().position(|line| line.trim() == end_marker); let end = lines.iter().position(|line| line.trim() == end_marker);
@ -160,20 +162,20 @@ impl HostsBuilder {
_ => { _ => {
return Err(Box::new(Error(format!( return Err(Box::new(Error(format!(
"start or end marker missing in {:?}", "start or end marker missing in {:?}",
&hosts_file &hosts_path
)))); ))));
}, },
}; };
// The tempfile should be in the same filesystem as the hosts file. // The tempfile should be in the same filesystem as the hosts file.
let hosts_dir = hosts_file let hosts_dir = hosts_path
.parent() .parent()
.expect("hosts file must be an absolute file path"); .expect("hosts file must be an absolute file path");
let temp_dir = tempfile::Builder::new().tempdir_in(hosts_dir)?; let temp_dir = tempfile::Builder::new().tempdir_in(hosts_dir)?;
let temp_path = temp_dir.path().join("hosts"); let temp_path = temp_dir.path().join("hosts");
// Copy the existing hosts file to preserve permissions. // 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)?; let mut file = File::create(&temp_path)?;
@ -192,7 +194,7 @@ impl HostsBuilder {
} }
// Move the file atomically to avoid a partial state. // Move the file atomically to avoid a partial state.
fs::rename(&temp_path, &hosts_file)?; fs::rename(&temp_path, &hosts_path)?;
Ok(()) Ok(())
} }