Skip to content

Commit

Permalink
client: support running as non-root (#94)
Browse files Browse the repository at this point in the history
shared(wg): use netlink instead of execve calls to "ip"
hostsfile: write to hostsfile in-place
  • Loading branch information
Jake McGinty authored Jun 10, 2021
1 parent 6a60643 commit 449b4b8
Show file tree
Hide file tree
Showing 20 changed files with 417 additions and 200 deletions.
67 changes: 62 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ name = "innernet"
path = "src/main.rs"

[dependencies]
anyhow = "1"
colored = "2"
dialoguer = "0.8"
hostsfile = { path = "../hostsfile" }
indoc = "1"
ipnetwork = { git = "https://github.com/mcginty/ipnetwork" } # pending https://github.com/achanda/ipnetwork/pull/129
lazy_static = "1"
libc = "0.2"
log = "0.4"
regex = { version = "1", default-features = false, features = ["std"] }
serde = { version = "1.0", features = ["derive"] }
Expand Down
14 changes: 3 additions & 11 deletions client/src/data_store.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::Error;
use colored::*;
use anyhow::bail;
use serde::{Deserialize, Serialize};
use shared::{ensure_dirs_exist, Cidr, IoErrorContext, Peer, WrappedIoError, CLIENT_DATA_DIR};
use std::{
Expand Down Expand Up @@ -35,13 +35,7 @@ impl DataStore {
.open(path)
.with_path(path)?;

if shared::chmod(&file, 0o600).with_path(path)? {
println!(
"{} updated permissions for {} to 0600.",
"[!]".yellow(),
path.display()
);
}
shared::warn_on_dangerous_mode(path).with_path(path)?;

let mut json = String::new();
file.read_to_string(&mut json).with_path(path)?;
Expand Down Expand Up @@ -94,9 +88,7 @@ impl DataStore {
for new_peer in current_peers.iter() {
if let Some(existing_peer) = peers.iter_mut().find(|p| p.ip == new_peer.ip) {
if existing_peer.public_key != new_peer.public_key {
return Err(
"PINNING ERROR: New peer has same IP but different public key.".into(),
);
bail!("PINNING ERROR: New peer has same IP but different public key.");
} else {
*existing_peer = new_peer.clone();
}
Expand Down
53 changes: 28 additions & 25 deletions client/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use anyhow::{anyhow, bail};
use colored::*;
use dialoguer::{Confirm, Input};
use hostsfile::HostsBuilder;
Expand All @@ -6,10 +7,10 @@ use shared::{
interface_config::InterfaceConfig, prompts, AddAssociationOpts, AddCidrOpts, AddPeerOpts,
Association, AssociationContents, Cidr, CidrTree, DeleteCidrOpts, EndpointContents,
InstallOpts, Interface, IoErrorContext, NetworkOpt, Peer, RedeemContents, RenamePeerOpts,
State, CLIENT_CONFIG_DIR, REDEEM_TRANSITION_WAIT,
State, WrappedIoError, CLIENT_CONFIG_DIR, REDEEM_TRANSITION_WAIT,
};
use std::{
fmt,
fmt, io,
path::{Path, PathBuf},
thread,
time::{Duration, SystemTime},
Expand Down Expand Up @@ -245,7 +246,9 @@ fn update_hosts_file(
&format!("{}.{}.wg", peer.contents.name, interface),
);
}
hosts_builder.write_to(hosts_path)?;
if let Err(e) = hosts_builder.write_to(&hosts_path).with_path(hosts_path) {
log::warn!("failed to update hosts ({})", e);
}

Ok(())
}
Expand All @@ -272,7 +275,7 @@ fn install(

let target_conf = CLIENT_CONFIG_DIR.join(&iface).with_extension("conf");
if target_conf.exists() {
return Err("An interface with this name already exists in innernet.".into());
bail!("An interface with this name already exists in innernet.");
}

let iface = iface.parse()?;
Expand Down Expand Up @@ -423,11 +426,10 @@ fn fetch(

if !interface_up {
if !bring_up_interface {
return Err(format!(
bail!(
"Interface is not up. Use 'innernet up {}' instead",
interface
)
.into());
);
}

log::info!("bringing up the interface.");
Expand Down Expand Up @@ -647,7 +649,7 @@ fn rename_peer(interface: &InterfaceName, opts: RenamePeerOpts) -> Result<(), Er
.filter(|p| p.name == old_name)
.map(|p| p.id)
.next()
.ok_or("Peer not found.")?;
.ok_or(anyhow!("Peer not found."))?;

let _ = api.http_form("PUT", &format!("/admin/peers/{}", id), peer_request)?;
log::info!("Peer renamed.");
Expand Down Expand Up @@ -687,11 +689,11 @@ fn add_association(interface: &InterfaceName, opts: AddAssociationOpts) -> Resul
let cidr1 = cidrs
.iter()
.find(|c| &c.name == cidr1)
.ok_or(format!("can't find cidr '{}'", cidr1))?;
.ok_or(anyhow!("can't find cidr '{}'", cidr1))?;
let cidr2 = cidrs
.iter()
.find(|c| &c.name == cidr2)
.ok_or(format!("can't find cidr '{}'", cidr2))?;
.ok_or(anyhow!("can't find cidr '{}'", cidr2))?;
(cidr1, cidr2)
} else if let Some((cidr1, cidr2)) = prompts::add_association(&cidrs[..])? {
(cidr1, cidr2)
Expand Down Expand Up @@ -824,16 +826,18 @@ fn show(
let devices = interfaces
.into_iter()
.filter_map(|name| {
DataStore::open(&name)
.and_then(|store| {
Ok((
Device::get(&name, network.backend).with_str(name.as_str_lossy())?,
store,
))
})
.ok()
match DataStore::open(&name) {
Ok(store) => {
let device = Device::get(&name, network.backend).with_str(name.as_str_lossy());
Some(device.map(|device| (device, store)))
},
// Skip WireGuard interfaces that aren't managed by innernet.
Err(e) if e.kind() == io::ErrorKind::NotFound => None,
// Error on interfaces that *are* managed by innernet but are not readable.
Err(e) => Some(Err(e)),
}
})
.collect::<Vec<_>>();
.collect::<Result<Vec<_>, _>>()?;

if devices.is_empty() {
log::info!("No innernet networks currently running.");
Expand All @@ -846,7 +850,7 @@ fn show(
let me = peers
.iter()
.find(|p| p.public_key == device_info.public_key.as_ref().unwrap().to_base64())
.ok_or("missing peer info")?;
.ok_or(anyhow!("missing peer info"))?;

let mut peer_states = device_info
.peers
Expand All @@ -858,7 +862,7 @@ fn show(
peer,
info: Some(info),
}),
None => Err(format!("peer {} isn't an innernet peer.", public_key)),
None => Err(anyhow!("peer {} isn't an innernet peer.", public_key)),
}
})
.collect::<Result<Vec<PeerState>, _>>()?;
Expand Down Expand Up @@ -987,6 +991,9 @@ fn main() {
if let Err(e) = run(opt) {
println!();
log::error!("{}\n", e);
if let Some(e) = e.downcast_ref::<WrappedIoError>() {
util::permissions_helptext(e);
}
std::process::exit(1);
}
}
Expand All @@ -998,10 +1005,6 @@ fn run(opt: Opts) -> Result<(), Error> {
interface: None,
});

if unsafe { libc::getuid() } != 0 && !matches!(command, Command::Completions { .. }) {
return Err("innernet must run as root.".into());
}

match command {
Command::Install {
invite,
Expand Down
55 changes: 49 additions & 6 deletions client/src/util.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
use crate::{ClientError, Error};
use colored::*;
use indoc::eprintdoc;
use log::{Level, LevelFilter};
use serde::{de::DeserializeOwned, Serialize};
use shared::{interface_config::ServerInfo, INNERNET_PUBKEY_HEADER};
use std::time::Duration;
use shared::{interface_config::ServerInfo, WrappedIoError, INNERNET_PUBKEY_HEADER};
use std::{io, time::Duration};
use ureq::{Agent, AgentBuilder};

static LOGGER: Logger = Logger;
struct Logger;

const BASE_MODULES: &[&str] = &["innernet", "shared"];

fn target_is_base(target: &str) -> bool {
BASE_MODULES
.iter()
.any(|module| module == &target || target.starts_with(&format!("{}::", module)))
}

impl log::Log for Logger {
fn enabled(&self, metadata: &log::Metadata) -> bool {
metadata.level() <= log::max_level()
&& (log::max_level() == LevelFilter::Trace
|| metadata.target().starts_with("shared::")
|| metadata.target() == "innernet")
&& (log::max_level() == LevelFilter::Trace || target_is_base(metadata.target()))
}

fn log(&self, record: &log::Record) {
Expand All @@ -25,7 +33,7 @@ impl log::Log for Logger {
Level::Debug => "[D]".blue(),
Level::Trace => "[T]".purple(),
};
if record.level() <= LevelFilter::Debug && record.target() != "innernet" {
if record.level() <= LevelFilter::Debug && !target_is_base(record.target()) {
println!(
"{} {} {}",
level_str,
Expand Down Expand Up @@ -94,6 +102,41 @@ pub fn human_size(bytes: u64) -> String {
}
}

pub fn permissions_helptext(e: &WrappedIoError) {
if e.raw_os_error() == Some(1) {
let current_exe = std::env::current_exe()
.ok()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "<innernet path>".into());
eprintdoc!(
"{}: innernet can't access the device info.
You either need to run innernet as root, or give innernet CAP_NET_ADMIN capabilities:
sudo setcap cap_net_admin+eip {}
",
"ERROR".bold().red(),
current_exe
);
} else if e.kind() == io::ErrorKind::PermissionDenied {
eprintdoc!(
"{}: innernet can't access its config/data folders.
You either need to run innernet as root, or give the user/group running innernet permissions
to access {config} and {data}.
For non-root permissions, it's recommended to create an \"innernet\" group, and run for example:
sudo chgrp -R innernet {config} {data}
sudo chmod -R g+rwX {config} {data}
",
"ERROR".bold().red(),
config = shared::CLIENT_CONFIG_DIR.to_string_lossy(),
data = shared::CLIENT_DATA_DIR.to_string_lossy(),
);
}
}

pub struct Api<'a> {
agent: Agent,
server: &'a ServerInfo,
Expand Down
Loading

0 comments on commit 449b4b8

Please sign in to comment.