client: support running as non-root (#94)

shared(wg): use netlink instead of execve calls to "ip"
hostsfile: write to hostsfile in-place
This commit is contained in:
Jake McGinty 2021-06-10 22:57:47 +09:00 committed by GitHub
parent 6a60643d7d
commit 449b4b8278
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 417 additions and 200 deletions

67
Cargo.lock generated
View file

@ -153,13 +153,13 @@ dependencies = [
name = "client"
version = "1.3.1"
dependencies = [
"anyhow",
"colored",
"dialoguer",
"hostsfile",
"indoc",
"ipnetwork",
"lazy_static",
"libc",
"log",
"regex",
"serde",
@ -382,10 +382,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hostsfile"
version = "1.0.1"
dependencies = [
"tempfile",
]
version = "1.1.0"
[[package]]
name = "http"
@ -576,6 +573,54 @@ dependencies = [
"winapi",
]
[[package]]
name = "netlink-packet-core"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac48279d5062bdf175bdbcb6b58ff1d6b0ecd54b951f7a0ff4bc0550fe903ccb"
dependencies = [
"anyhow",
"byteorder",
"libc",
"netlink-packet-utils",
]
[[package]]
name = "netlink-packet-route"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da1bf86b4324996fb58f8e17752b2a06176f4c5efc013928060ac94a3a329b71"
dependencies = [
"anyhow",
"bitflags",
"byteorder",
"libc",
"netlink-packet-core",
"netlink-packet-utils",
]
[[package]]
name = "netlink-packet-utils"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c2afb159d0e3ac700e85f0df25b8438b99d43ed0c0b685242fcdf1b5673e54d"
dependencies = [
"anyhow",
"byteorder",
"paste",
"thiserror",
]
[[package]]
name = "netlink-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d61c5374735aa0cd07cb7fd820b656062b187b5588d79517f72956b57c6de9ef"
dependencies = [
"libc",
"log",
]
[[package]]
name = "nom"
version = "5.1.2"
@ -636,6 +681,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "paste"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58"
[[package]]
name = "peeking_take_while"
version = "0.1.2"
@ -930,12 +981,17 @@ dependencies = [
name = "shared"
version = "1.3.1"
dependencies = [
"anyhow",
"colored",
"dialoguer",
"indoc",
"ipnetwork",
"lazy_static",
"libc",
"log",
"netlink-packet-core",
"netlink-packet-route",
"netlink-sys",
"publicip",
"regex",
"serde",
@ -943,6 +999,7 @@ dependencies = [
"toml",
"url",
"wgctrl",
"wgctrl-sys",
]
[[package]]

View file

@ -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"] }

View file

@ -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::{
@ -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)?;
@ -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();
}

View file

@ -1,3 +1,4 @@
use anyhow::{anyhow, bail};
use colored::*;
use dialoguer::{Confirm, Input};
use hostsfile::HostsBuilder;
@ -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},
@ -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(())
}
@ -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()?;
@ -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.");
@ -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.");
@ -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)
@ -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.");
@ -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
@ -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>, _>>()?;
@ -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);
}
}
@ -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,

View file

@ -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) {
@ -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,
@ -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,

View file

@ -5,7 +5,6 @@ edition = "2018"
license = "UNLICENSED"
name = "hostsfile"
publish = false
version = "1.0.1"
version = "1.1.0"
[dependencies]
tempfile = "3"

View file

@ -1,12 +1,4 @@
use std::{
collections::HashMap,
fmt,
fs::{self, File, OpenOptions},
io::{BufRead, BufReader, Write},
net::IpAddr,
path::{Path, PathBuf},
result,
};
use std::{collections::HashMap, fmt, fs::OpenOptions, io::{self, BufRead, BufReader, ErrorKind, Write}, net::IpAddr, path::{Path, PathBuf}, result};
pub type Result<T> = result::Result<T, Box<dyn std::error::Error>>;
@ -115,7 +107,12 @@ impl HostsBuilder {
/// Inserts a new section to the system's default 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(&self) -> Result<()> {
pub fn write(&self) -> io::Result<()> {
self.write_to(&Self::default_path()?)
}
/// Returns the default hosts path based on the current OS.
pub fn default_path() -> io::Result<PathBuf> {
let hosts_file = if cfg!(unix) {
PathBuf::from("/etc/hosts")
} else if cfg!(windows) {
@ -124,21 +121,24 @@ impl HostsBuilder {
// the location depends on the environment variable %WinDir%.
format!(
"{}\\System32\\Drivers\\Etc\\hosts",
std::env::var("WinDir")?
std::env::var("WinDir").map_err(|_| io::Error::new(
ErrorKind::Other,
"WinDir environment variable missing".to_owned()
))?
),
)
} else {
return Err(Box::new(Error("unsupported operating system.".to_owned())));
return Err(io::Error::new(
ErrorKind::Other,
"unsupported operating system.".to_owned(),
));
};
if !hosts_file.exists() {
return Err(Box::new(Error(format!(
"hosts file {:?} missing",
&hosts_file
))));
return Err(ErrorKind::NotFound.into());
}
self.write_to(&hosts_file)
Ok(hosts_file)
}
/// Inserts a new section to the specified hosts file. If there is a section with the same tag
@ -146,7 +146,7 @@ impl HostsBuilder {
///
/// On Windows, the format of one hostname per line will be used, all other systems will use
/// the same format as Unix and Unix-like systems (i.e. allow multiple hostnames per line).
pub fn write_to<P: AsRef<Path>>(&self, hosts_path: P) -> Result<()> {
pub fn write_to<P: AsRef<Path>>(&self, hosts_path: P) -> io::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);
@ -179,49 +179,44 @@ impl HostsBuilder {
lines.len()
},
_ => {
return Err(Box::new(Error(format!(
"start or end marker missing in {:?}",
&hosts_path
))));
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("start or end marker missing in {:?}", &hosts_path),
));
},
};
// The tempfile should be in the same filesystem as the 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_path, &temp_path)?;
let mut file = File::create(&temp_path)?;
let mut s = vec![];
for line in &lines[..insert] {
writeln!(&mut file, "{}", line)?;
writeln!(&mut s, "{}", line)?;
}
if !self.hostname_map.is_empty() {
writeln!(&mut file, "{}", begin_marker)?;
writeln!(&mut s, "{}", begin_marker)?;
for (ip, hostnames) in &self.hostname_map {
if cfg!(windows) {
// windows only allows one hostname per line
for hostname in hostnames {
writeln!(&mut file, "{} {}", ip, hostname)?;
writeln!(&mut s, "{} {}", ip, hostname)?;
}
} else {
// assume the same format as Unix
writeln!(&mut file, "{} {}", ip, hostnames.join(" "))?;
writeln!(&mut s, "{} {}", ip, hostnames.join(" "))?;
}
}
writeln!(&mut file, "{}", end_marker)?;
writeln!(&mut s, "{}", end_marker)?;
}
for line in &lines[insert..] {
writeln!(&mut file, "{}", line)?;
writeln!(&mut s, "{}", line)?;
}
// Move the file atomically to avoid a partial state.
fs::rename(&temp_path, &hosts_path)?;
OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(true)
.open(hosts_path)?
.write_all(&s)?;
Ok(())
}

View file

@ -13,6 +13,7 @@ name = "innernet-server"
path = "src/main.rs"
[dependencies]
anyhow = "1"
bytes = "1"
colored = "2"
dialoguer = "0.8"

View file

@ -229,7 +229,7 @@ mod tests {
let old_peer = DatabasePeer::get(&server.db.lock(), test::DEVELOPER1_PEER_ID)?;
let change = PeerContents {
name: "new-peer-name".parse()?,
name: "new-peer-name".parse().unwrap(),
..old_peer.contents.clone()
};

View file

@ -1,4 +1,5 @@
use crate::*;
use anyhow::anyhow;
use db::DatabaseCidr;
use dialoguer::{theme::ColorfulTheme, Input};
use indoc::printdoc;
@ -63,7 +64,7 @@ fn populate_database(conn: &Connection, db_init_data: DbInitData) -> Result<(),
parent: None,
},
)
.map_err(|_| "failed to create root CIDR".to_string())?;
.map_err(|_| anyhow!("failed to create root CIDR"))?;
let server_cidr = DatabaseCidr::create(
&conn,
@ -73,12 +74,12 @@ fn populate_database(conn: &Connection, db_init_data: DbInitData) -> Result<(),
parent: Some(root_cidr.id),
},
)
.map_err(|_| "failed to create innernet-server CIDR".to_string())?;
.map_err(|_| anyhow!("failed to create innernet-server CIDR"))?;
let _me = DatabasePeer::create(
&conn,
PeerContents {
name: SERVER_NAME.parse()?,
name: SERVER_NAME.parse().map_err(|e: &str| anyhow!(e))?,
ip: db_init_data.our_ip,
cidr_id: server_cidr.id,
public_key: db_init_data.public_key_base64,
@ -90,7 +91,7 @@ fn populate_database(conn: &Connection, db_init_data: DbInitData) -> Result<(),
invite_expires: None,
},
)
.map_err(|_| "failed to create innernet peer.".to_string())?;
.map_err(|_| anyhow!("failed to create innernet peer."))?;
Ok(())
}
@ -99,7 +100,7 @@ pub fn init_wizard(conf: &ServerConfig, opts: InitializeOpts) -> Result<(), Erro
let theme = ColorfulTheme::default();
shared::ensure_dirs_exist(&[conf.config_dir(), conf.database_dir()]).map_err(|_| {
format!(
anyhow!(
"Failed to create config and database directories {}",
"(are you not running as root?)".bold()
)
@ -139,7 +140,7 @@ pub fn init_wizard(conf: &ServerConfig, opts: InitializeOpts) -> Result<(), Erro
let endpoint: Endpoint = if let Some(endpoint) = opts.external_endpoint {
endpoint
} else if opts.auto_external_endpoint {
let ip = publicip::get_any(Preference::Ipv4).ok_or("couldn't get external IP")?;
let ip = publicip::get_any(Preference::Ipv4).ok_or(anyhow!("couldn't get external IP"))?;
SocketAddr::new(ip, 51820).into()
} else {
prompts::ask_endpoint()?
@ -152,7 +153,7 @@ pub fn init_wizard(conf: &ServerConfig, opts: InitializeOpts) -> Result<(), Erro
.with_prompt("Listen port")
.default(51820)
.interact()
.map_err(|_| "failed to get listen port.")?
.map_err(|_| anyhow!("failed to get listen port."))?
};
let our_ip = root_cidr
@ -185,7 +186,7 @@ pub fn init_wizard(conf: &ServerConfig, opts: InitializeOpts) -> Result<(), Erro
let database_path = conf.database_path(&name);
let conn = create_database(&database_path).map_err(|_| {
format!(
anyhow!(
"failed to create database {}",
"(are you not running as root?)".bold()
)

View file

@ -1,3 +1,4 @@
use anyhow::{anyhow, bail};
use colored::*;
use dialoguer::Confirm;
use hyper::{http, server::conn::AddrStream, Body, Request, Response};
@ -261,14 +262,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
fn open_database_connection(
interface: &InterfaceName,
conf: &ServerConfig,
) -> Result<rusqlite::Connection, Box<dyn std::error::Error>> {
) -> Result<rusqlite::Connection, Error> {
let database_path = conf.database_path(&interface);
if !Path::new(&database_path).exists() {
return Err(format!(
bail!(
"no database file found at {}",
database_path.to_string_lossy()
)
.into());
);
}
let conn = Connection::open(&database_path)?;
@ -337,7 +337,7 @@ fn rename_peer(
let mut db_peer = DatabasePeer::list(&conn)?
.into_iter()
.find(|p| p.name == old_name)
.ok_or( "Peer not found.")?;
.ok_or(anyhow!("Peer not found."))?;
let _peer = db_peer.update(&conn, peer_request)?;
} else {
println!("exited without creating peer.");

View file

@ -221,7 +221,7 @@ pub fn peer_contents(
let public_key = KeyPair::generate().public;
Ok(PeerContents {
name: name.parse()?,
name: name.parse().map_err(|e: &str| anyhow!(e))?,
ip: ip_str.parse()?,
cidr_id,
public_key: public_key.to_base64(),

View file

@ -7,11 +7,13 @@ publish = false
version = "1.3.1"
[dependencies]
anyhow = "1"
colored = "2.0"
dialoguer = "0.8"
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"
publicip = { path = "../publicip" }
regex = "1"
@ -20,3 +22,9 @@ structopt = "0.3"
toml = "0.5"
url = "2"
wgctrl = { path = "../wgctrl-rs" }
[target.'cfg(target_os = "linux")'.dependencies]
netlink-sys = "0.6"
netlink-packet-core = "0.2"
netlink-packet-route = "0.7"
wgctrl-sys = { path = "../wgctrl-sys" }

View file

@ -116,14 +116,7 @@ impl InterfaceConfig {
pub fn from_interface(interface: &InterfaceName) -> Result<Self, Error> {
let path = Self::build_config_file_path(interface)?;
let file = File::open(&path).with_path(&path)?;
if crate::chmod(&file, 0o600)? {
println!(
"{} updated permissions for {} to 0600.",
"[!]".yellow(),
path.display()
);
}
crate::warn_on_dangerous_mode(&path).with_path(&path)?;
Self::from_file(path)
}

View file

@ -1,4 +1,4 @@
use colored::*;
pub use anyhow::Error;
use lazy_static::lazy_static;
use std::{
fs::{self, File},
@ -9,6 +9,8 @@ use std::{
};
pub mod interface_config;
#[cfg(target_os = "linux")]
mod netlink;
pub mod prompts;
pub mod types;
pub mod wg;
@ -26,8 +28,6 @@ lazy_static! {
pub const PERSISTENT_KEEPALIVE_INTERVAL_SECS: u16 = 25;
pub const INNERNET_PUBKEY_HEADER: &str = "X-Innernet-Server-Key";
pub type Error = Box<dyn std::error::Error>;
pub fn ensure_dirs_exist(dirs: &[&Path]) -> Result<(), WrappedIoError> {
for dir in dirs {
match fs::create_dir(dir).with_path(dir) {
@ -35,20 +35,29 @@ pub fn ensure_dirs_exist(dirs: &[&Path]) -> Result<(), WrappedIoError> {
return Err(e);
},
_ => {
let target_file = File::open(dir).with_path(dir)?;
if chmod(&target_file, 0o700).with_path(dir)? {
println!(
"{} updated permissions for {} to 0700.",
"[!]".yellow(),
dir.display()
);
}
warn_on_dangerous_mode(dir).with_path(dir)?;
},
}
}
Ok(())
}
pub fn warn_on_dangerous_mode(path: &Path) -> Result<(), io::Error> {
let file = File::open(path)?;
let metadata = file.metadata()?;
let permissions = metadata.permissions();
let mode = permissions.mode() & 0o777;
if mode & 0o007 != 0 {
log::warn!(
"{} is world-accessible (mode is {:#05o}). This is probably not what you want.",
path.to_string_lossy(),
mode
);
}
Ok(())
}
/// Updates the permissions of a file or directory. Returns `Ok(true)` if
/// permissions had to be changed, `Ok(false)` if permissions were already
/// correct.

128
shared/src/netlink.rs Normal file
View file

@ -0,0 +1,128 @@
use crate::Error;
use anyhow::anyhow;
use ipnetwork::IpNetwork;
use netlink_packet_core::{
NetlinkMessage, NetlinkPayload, NLM_F_ACK, NLM_F_CREATE, NLM_F_EXCL, NLM_F_REQUEST,
};
use netlink_packet_route::{
address, constants::*, link, route, AddressHeader, AddressMessage, LinkHeader, LinkMessage,
RouteHeader, RouteMessage, RtnlMessage, RTN_UNICAST, RT_SCOPE_LINK, RT_TABLE_MAIN,
};
use netlink_sys::{protocols::NETLINK_ROUTE, Socket, SocketAddr};
use std::io;
use wgctrl::InterfaceName;
fn if_nametoindex(interface: &InterfaceName) -> Result<u32, Error> {
match unsafe { libc::if_nametoindex(interface.as_ptr()) } {
0 => Err(anyhow!("couldn't find interface '{}'.", interface)),
index => Ok(index),
}
}
fn netlink_call(
message: RtnlMessage,
flags: Option<u16>,
) -> Result<NetlinkMessage<RtnlMessage>, io::Error> {
let mut req = NetlinkMessage::from(message);
req.header.flags = flags.unwrap_or(NLM_F_REQUEST | NLM_F_ACK | NLM_F_EXCL | NLM_F_CREATE);
req.finalize();
let mut buf = [0; 4096];
req.serialize(&mut buf);
let len = req.buffer_len();
log::debug!("netlink request: {:?}", req);
let socket = Socket::new(NETLINK_ROUTE).unwrap();
let kernel_addr = SocketAddr::new(0, 0);
socket.connect(&kernel_addr)?;
let n_sent = socket.send(&buf[..len], 0).unwrap();
if n_sent != len {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"failed to send netlink request",
));
}
let n_received = socket.recv(&mut buf[..], 0).unwrap();
let response = NetlinkMessage::<RtnlMessage>::deserialize(&buf[..n_received])
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
log::trace!("netlink response: {:?}", response);
if let NetlinkPayload::Error(e) = response.payload {
return Err(e.to_io());
}
Ok(response)
}
pub fn set_up(interface: &InterfaceName, mtu: u32) -> Result<(), Error> {
let index = if_nametoindex(interface)?;
let message = LinkMessage {
header: LinkHeader {
index,
flags: IFF_UP,
..Default::default()
},
nlas: vec![link::nlas::Nla::Mtu(mtu)],
};
netlink_call(RtnlMessage::SetLink(message), None)?;
Ok(())
}
pub fn set_addr(interface: &InterfaceName, addr: IpNetwork) -> Result<(), Error> {
let index = if_nametoindex(interface)?;
let (family, nlas) = match addr {
IpNetwork::V4(network) => {
let addr_bytes = network.ip().octets().to_vec();
(
AF_INET as u8,
vec![
address::Nla::Local(addr_bytes.clone()),
address::Nla::Address(addr_bytes),
],
)
},
IpNetwork::V6(network) => (
AF_INET6 as u8,
vec![address::Nla::Address(network.ip().octets().to_vec())],
),
};
let message = AddressMessage {
header: AddressHeader {
index,
family,
prefix_len: addr.prefix(),
scope: RT_SCOPE_UNIVERSE,
..Default::default()
},
nlas,
};
netlink_call(
RtnlMessage::NewAddress(message),
Some(NLM_F_REQUEST | NLM_F_ACK | NLM_F_REPLACE | NLM_F_CREATE),
)?;
Ok(())
}
pub fn add_route(interface: &InterfaceName, cidr: IpNetwork) -> Result<bool, Error> {
let if_index = if_nametoindex(interface)?;
let (address_family, dst) = match cidr {
IpNetwork::V4(network) => (AF_INET as u8, network.network().octets().to_vec()),
IpNetwork::V6(network) => (AF_INET6 as u8, network.network().octets().to_vec()),
};
let message = RouteMessage {
header: RouteHeader {
table: RT_TABLE_MAIN,
protocol: RTPROT_BOOT,
scope: RT_SCOPE_LINK,
kind: RTN_UNICAST,
destination_prefix_length: cidr.prefix(),
address_family,
..Default::default()
},
nlas: vec![route::Nla::Destination(dst), route::Nla::Oif(if_index)],
};
match netlink_call(RtnlMessage::NewRoute(message), None) {
Ok(_) => Ok(true),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Ok(false),
Err(e) => Err(e.into()),
}
}

View file

@ -3,6 +3,7 @@ use crate::{
AddCidrOpts, AddPeerOpts, Association, Cidr, CidrContents, CidrTree, DeleteCidrOpts, Endpoint,
Error, Hostname, Peer, PeerContents, RenamePeerOpts, PERSISTENT_KEEPALIVE_INTERVAL_SECS,
};
use anyhow::anyhow;
use colored::*;
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
use ipnetwork::IpNetwork;
@ -21,7 +22,7 @@ pub fn add_cidr(cidrs: &[Cidr], request: &AddCidrOpts) -> Result<Option<CidrCont
cidrs
.iter()
.find(|cidr| &cidr.name == parent_name)
.ok_or("No parent CIDR with that name exists.")?
.ok_or(anyhow!("No parent CIDR with that name exists."))?
} else {
choose_cidr(cidrs, "Parent CIDR")?
};
@ -74,7 +75,7 @@ pub fn delete_cidr(cidrs: &[Cidr], peers: &[Peer], request: &DeleteCidrOpts) ->
cidrs
.iter()
.find(|cidr| &cidr.name == name)
.ok_or_else(|| format!("CIDR {} doesn't exist or isn't eligible for deletion", name))?
.ok_or_else(|| anyhow!("CIDR {} doesn't exist or isn't eligible for deletion", name))?
} else {
let cidr_index = Select::with_theme(&*THEME)
.with_prompt("Delete CIDR")
@ -92,7 +93,7 @@ pub fn delete_cidr(cidrs: &[Cidr], peers: &[Peer], request: &DeleteCidrOpts) ->
{
Ok(cidr.id)
} else {
Err("Canceled".into())
Err(anyhow!("Canceled"))
}
}
@ -193,7 +194,7 @@ pub fn add_peer(
leaves
.iter()
.find(|cidr| &cidr.name == parent_name)
.ok_or("No eligible CIDR with that name exists.")?
.ok_or(anyhow!("No eligible CIDR with that name exists."))?
} else {
choose_cidr(&leaves[..], "Eligible CIDRs for peer")?
};
@ -241,7 +242,7 @@ pub fn add_peer(
} else {
Input::with_theme(&*THEME)
.with_prompt("Invite expires after")
.default("14d".parse()?)
.default("14d".parse().map_err(|s: &str| anyhow!(s))?)
.interact()?
};
@ -287,7 +288,7 @@ pub fn rename_peer(
eligible_peers
.into_iter()
.find(|p| &p.name == name)
.ok_or_else(|| format!("Peer '{}' does not exist", name))?
.ok_or_else(|| anyhow!("Peer '{}' does not exist", name))?
.clone()
} else {
let peer_index = Select::with_theme(&*THEME)

View file

@ -4,6 +4,7 @@ use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{
fmt::{self, Display, Formatter},
io,
net::{IpAddr, SocketAddr, ToSocketAddrs},
ops::Deref,
path::Path,
@ -121,14 +122,14 @@ impl fmt::Display for Endpoint {
}
impl Endpoint {
pub fn resolve(&self) -> Result<SocketAddr, String> {
let mut addrs = self
.to_string()
.to_socket_addrs()
.map_err(|e| e.to_string())?;
addrs
.next()
.ok_or_else(|| "failed to resolve address".to_string())
pub fn resolve(&self) -> Result<SocketAddr, io::Error> {
let mut addrs = self.to_string().to_socket_addrs()?;
addrs.next().ok_or_else(|| {
io::Error::new(
io::ErrorKind::AddrNotAvailable,
"failed to resolve address".to_string(),
)
})
}
}

View file

@ -1,13 +1,11 @@
use crate::{Error, IoErrorContext, NetworkOpt};
use ipnetwork::IpNetwork;
use std::{
net::{IpAddr, SocketAddr},
process::{self, Command},
};
use std::net::{IpAddr, SocketAddr};
use wgctrl::{Backend, Device, DeviceUpdate, InterfaceName, PeerConfigBuilder};
fn cmd(bin: &str, args: &[&str]) -> Result<process::Output, Error> {
let output = Command::new(bin).args(args).output()?;
#[cfg(target_os = "macos")]
fn cmd(bin: &str, args: &[&str]) -> Result<std::process::Output, Error> {
let output = std::process::Command::new(bin).args(args).output()?;
log::debug!("cmd: {} {}", bin, args.join(" "));
log::debug!("status: {:?}", output.status.code());
log::trace!("stdout: {}", String::from_utf8_lossy(&output.stdout));
@ -15,13 +13,12 @@ fn cmd(bin: &str, args: &[&str]) -> Result<process::Output, Error> {
if output.status.success() {
Ok(output)
} else {
Err(format!(
Err(anyhow::anyhow!(
"failed to run {} {} command: {}",
bin,
args.join(" "),
String::from_utf8_lossy(&output.stderr)
)
.into())
))
}
}
@ -40,30 +37,30 @@ pub fn set_addr(interface: &InterfaceName, addr: IpNetwork) -> Result<(), Error>
&addr.ip().to_string(),
"alias",
],
)?;
)
.map(|_output| ())
} else {
cmd(
"ifconfig",
&[&real_interface, "inet6", &addr.to_string(), "alias"],
)?;
)
.map(|_output| ())
}
cmd("ifconfig", &[&real_interface, "mtu", "1420"])?;
}
#[cfg(target_os = "macos")]
pub fn set_up(interface: &InterfaceName, mtu: u32) -> Result<(), Error> {
let real_interface =
wgctrl::backends::userspace::resolve_tun(interface).with_str(interface.to_string())?;
cmd("ifconfig", &[&real_interface, "mtu", &mtu.to_string()])?;
Ok(())
}
#[cfg(target_os = "linux")]
pub fn set_addr(interface: &InterfaceName, addr: IpNetwork) -> Result<(), Error> {
let interface = interface.to_string();
cmd(
"ip",
&["address", "replace", &addr.to_string(), "dev", &interface],
)?;
cmd(
"ip",
&["link", "set", "mtu", "1420", "up", "dev", &interface],
)?;
Ok(())
}
pub use super::netlink::set_addr;
#[cfg(target_os = "linux")]
pub use super::netlink::set_up;
pub fn up(
interface: &InterfaceName,
@ -88,6 +85,7 @@ pub fn up(
.set_private_key(wgctrl::Key::from_base64(&private_key).unwrap())
.apply(interface, network.backend)?;
set_addr(interface, address)?;
set_up(interface, 1420)?;
if !network.no_routing {
add_route(interface, address)?;
}
@ -120,43 +118,31 @@ pub fn down(interface: &InterfaceName, backend: Backend) -> Result<(), Error> {
/// Add a route in the OS's routing table to get traffic flowing through this interface.
/// Returns an error if the process doesn't exit successfully, otherwise returns
/// true if the route was changed, false if the route already exists.
#[cfg(target_os = "macos")]
pub fn add_route(interface: &InterfaceName, cidr: IpNetwork) -> Result<bool, Error> {
if cfg!(target_os = "macos") {
let real_interface =
wgctrl::backends::userspace::resolve_tun(interface).with_str(interface.to_string())?;
let output = cmd(
"route",
&[
"-n",
"add",
if cidr.is_ipv4() { "-inet" } else { "-inet6" },
&cidr.to_string(),
"-interface",
&real_interface,
],
)?;
let stderr = String::from_utf8_lossy(&output.stderr);
if !output.status.success() {
Err(format!(
"failed to add route for device {} ({}): {}",
&interface, real_interface, stderr
)
.into())
} else {
Ok(!stderr.contains("File exists"))
}
let real_interface =
wgctrl::backends::userspace::resolve_tun(interface).with_str(interface.to_string())?;
let output = cmd(
"route",
&[
"-n",
"add",
if cidr.is_ipv4() { "-inet" } else { "-inet6" },
&cidr.to_string(),
"-interface",
&real_interface,
],
)?;
let stderr = String::from_utf8_lossy(&output.stderr);
if !output.status.success() {
Err(anyhow::anyhow!(
"failed to add route for device {} ({}): {}",
&interface, real_interface, stderr
))
} else {
// TODO(mcginty): use the netlink interface on linux to modify routing table.
let _ = cmd(
"ip",
&[
"route",
"add",
&IpNetwork::new(cidr.network(), cidr.prefix())?.to_string(),
"dev",
&interface.to_string(),
],
);
Ok(false)
Ok(!stderr.contains("File exists"))
}
}
#[cfg(target_os = "linux")]
pub use super::netlink::add_route;

View file

@ -165,7 +165,7 @@ impl InterfaceName {
#[cfg(target_os = "linux")]
/// Returns a pointer to the inner byte buffer for FFI calls.
pub(crate) fn as_ptr(&self) -> *const c_char {
pub fn as_ptr(&self) -> *const c_char {
self.0.as_ptr()
}