client: support running as non-root (#94)
shared(wg): use netlink instead of execve calls to "ip" hostsfile: write to hostsfile in-placepull/121/head
parent
6a60643d7d
commit
449b4b8278
|
@ -153,13 +153,13 @@ dependencies = [
|
||||||
name = "client"
|
name = "client"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"colored",
|
"colored",
|
||||||
"dialoguer",
|
"dialoguer",
|
||||||
"hostsfile",
|
"hostsfile",
|
||||||
"indoc",
|
"indoc",
|
||||||
"ipnetwork",
|
"ipnetwork",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
|
||||||
"log",
|
"log",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -382,10 +382,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hostsfile"
|
name = "hostsfile"
|
||||||
version = "1.0.1"
|
version = "1.1.0"
|
||||||
dependencies = [
|
|
||||||
"tempfile",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
|
@ -576,6 +573,54 @@ dependencies = [
|
||||||
"winapi",
|
"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]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "5.1.2"
|
version = "5.1.2"
|
||||||
|
@ -636,6 +681,12 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paste"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "peeking_take_while"
|
name = "peeking_take_while"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
@ -930,12 +981,17 @@ dependencies = [
|
||||||
name = "shared"
|
name = "shared"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"colored",
|
"colored",
|
||||||
"dialoguer",
|
"dialoguer",
|
||||||
"indoc",
|
"indoc",
|
||||||
"ipnetwork",
|
"ipnetwork",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
|
"netlink-packet-core",
|
||||||
|
"netlink-packet-route",
|
||||||
|
"netlink-sys",
|
||||||
"publicip",
|
"publicip",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -943,6 +999,7 @@ dependencies = [
|
||||||
"toml",
|
"toml",
|
||||||
"url",
|
"url",
|
||||||
"wgctrl",
|
"wgctrl",
|
||||||
|
"wgctrl-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -14,13 +14,13 @@ name = "innernet"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = "1"
|
||||||
colored = "2"
|
colored = "2"
|
||||||
dialoguer = "0.8"
|
dialoguer = "0.8"
|
||||||
hostsfile = { path = "../hostsfile" }
|
hostsfile = { path = "../hostsfile" }
|
||||||
indoc = "1"
|
indoc = "1"
|
||||||
ipnetwork = { git = "https://github.com/mcginty/ipnetwork" } # pending https://github.com/achanda/ipnetwork/pull/129
|
ipnetwork = { git = "https://github.com/mcginty/ipnetwork" } # pending https://github.com/achanda/ipnetwork/pull/129
|
||||||
lazy_static = "1"
|
lazy_static = "1"
|
||||||
libc = "0.2"
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
regex = { version = "1", default-features = false, features = ["std"] }
|
regex = { version = "1", default-features = false, features = ["std"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use colored::*;
|
use anyhow::bail;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use shared::{ensure_dirs_exist, Cidr, IoErrorContext, Peer, WrappedIoError, CLIENT_DATA_DIR};
|
use shared::{ensure_dirs_exist, Cidr, IoErrorContext, Peer, WrappedIoError, CLIENT_DATA_DIR};
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -35,13 +35,7 @@ impl DataStore {
|
||||||
.open(path)
|
.open(path)
|
||||||
.with_path(path)?;
|
.with_path(path)?;
|
||||||
|
|
||||||
if shared::chmod(&file, 0o600).with_path(path)? {
|
shared::warn_on_dangerous_mode(path).with_path(path)?;
|
||||||
println!(
|
|
||||||
"{} updated permissions for {} to 0600.",
|
|
||||||
"[!]".yellow(),
|
|
||||||
path.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut json = String::new();
|
let mut json = String::new();
|
||||||
file.read_to_string(&mut json).with_path(path)?;
|
file.read_to_string(&mut json).with_path(path)?;
|
||||||
|
@ -94,9 +88,7 @@ impl DataStore {
|
||||||
for new_peer in current_peers.iter() {
|
for new_peer in current_peers.iter() {
|
||||||
if let Some(existing_peer) = peers.iter_mut().find(|p| p.ip == new_peer.ip) {
|
if let Some(existing_peer) = peers.iter_mut().find(|p| p.ip == new_peer.ip) {
|
||||||
if existing_peer.public_key != new_peer.public_key {
|
if existing_peer.public_key != new_peer.public_key {
|
||||||
return Err(
|
bail!("PINNING ERROR: New peer has same IP but different public key.");
|
||||||
"PINNING ERROR: New peer has same IP but different public key.".into(),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
*existing_peer = new_peer.clone();
|
*existing_peer = new_peer.clone();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use anyhow::{anyhow, bail};
|
||||||
use colored::*;
|
use colored::*;
|
||||||
use dialoguer::{Confirm, Input};
|
use dialoguer::{Confirm, Input};
|
||||||
use hostsfile::HostsBuilder;
|
use hostsfile::HostsBuilder;
|
||||||
|
@ -6,10 +7,10 @@ use shared::{
|
||||||
interface_config::InterfaceConfig, prompts, AddAssociationOpts, AddCidrOpts, AddPeerOpts,
|
interface_config::InterfaceConfig, prompts, AddAssociationOpts, AddCidrOpts, AddPeerOpts,
|
||||||
Association, AssociationContents, Cidr, CidrTree, DeleteCidrOpts, EndpointContents,
|
Association, AssociationContents, Cidr, CidrTree, DeleteCidrOpts, EndpointContents,
|
||||||
InstallOpts, Interface, IoErrorContext, NetworkOpt, Peer, RedeemContents, RenamePeerOpts,
|
InstallOpts, Interface, IoErrorContext, NetworkOpt, Peer, RedeemContents, RenamePeerOpts,
|
||||||
State, CLIENT_CONFIG_DIR, REDEEM_TRANSITION_WAIT,
|
State, WrappedIoError, CLIENT_CONFIG_DIR, REDEEM_TRANSITION_WAIT,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
fmt,
|
fmt, io,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
thread,
|
thread,
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
|
@ -245,7 +246,9 @@ fn update_hosts_file(
|
||||||
&format!("{}.{}.wg", peer.contents.name, interface),
|
&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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -272,7 +275,7 @@ fn install(
|
||||||
|
|
||||||
let target_conf = CLIENT_CONFIG_DIR.join(&iface).with_extension("conf");
|
let target_conf = CLIENT_CONFIG_DIR.join(&iface).with_extension("conf");
|
||||||
if target_conf.exists() {
|
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()?;
|
let iface = iface.parse()?;
|
||||||
|
@ -423,11 +426,10 @@ fn fetch(
|
||||||
|
|
||||||
if !interface_up {
|
if !interface_up {
|
||||||
if !bring_up_interface {
|
if !bring_up_interface {
|
||||||
return Err(format!(
|
bail!(
|
||||||
"Interface is not up. Use 'innernet up {}' instead",
|
"Interface is not up. Use 'innernet up {}' instead",
|
||||||
interface
|
interface
|
||||||
)
|
);
|
||||||
.into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("bringing up the interface.");
|
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)
|
.filter(|p| p.name == old_name)
|
||||||
.map(|p| p.id)
|
.map(|p| p.id)
|
||||||
.next()
|
.next()
|
||||||
.ok_or("Peer not found.")?;
|
.ok_or(anyhow!("Peer not found."))?;
|
||||||
|
|
||||||
let _ = api.http_form("PUT", &format!("/admin/peers/{}", id), peer_request)?;
|
let _ = api.http_form("PUT", &format!("/admin/peers/{}", id), peer_request)?;
|
||||||
log::info!("Peer renamed.");
|
log::info!("Peer renamed.");
|
||||||
|
@ -687,11 +689,11 @@ fn add_association(interface: &InterfaceName, opts: AddAssociationOpts) -> Resul
|
||||||
let cidr1 = cidrs
|
let cidr1 = cidrs
|
||||||
.iter()
|
.iter()
|
||||||
.find(|c| &c.name == cidr1)
|
.find(|c| &c.name == cidr1)
|
||||||
.ok_or(format!("can't find cidr '{}'", cidr1))?;
|
.ok_or(anyhow!("can't find cidr '{}'", cidr1))?;
|
||||||
let cidr2 = cidrs
|
let cidr2 = cidrs
|
||||||
.iter()
|
.iter()
|
||||||
.find(|c| &c.name == cidr2)
|
.find(|c| &c.name == cidr2)
|
||||||
.ok_or(format!("can't find cidr '{}'", cidr2))?;
|
.ok_or(anyhow!("can't find cidr '{}'", cidr2))?;
|
||||||
(cidr1, cidr2)
|
(cidr1, cidr2)
|
||||||
} else if let Some((cidr1, cidr2)) = prompts::add_association(&cidrs[..])? {
|
} else if let Some((cidr1, cidr2)) = prompts::add_association(&cidrs[..])? {
|
||||||
(cidr1, cidr2)
|
(cidr1, cidr2)
|
||||||
|
@ -824,16 +826,18 @@ fn show(
|
||||||
let devices = interfaces
|
let devices = interfaces
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|name| {
|
.filter_map(|name| {
|
||||||
DataStore::open(&name)
|
match DataStore::open(&name) {
|
||||||
.and_then(|store| {
|
Ok(store) => {
|
||||||
Ok((
|
let device = Device::get(&name, network.backend).with_str(name.as_str_lossy());
|
||||||
Device::get(&name, network.backend).with_str(name.as_str_lossy())?,
|
Some(device.map(|device| (device, store)))
|
||||||
store,
|
},
|
||||||
))
|
// Skip WireGuard interfaces that aren't managed by innernet.
|
||||||
})
|
Err(e) if e.kind() == io::ErrorKind::NotFound => None,
|
||||||
.ok()
|
// 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() {
|
if devices.is_empty() {
|
||||||
log::info!("No innernet networks currently running.");
|
log::info!("No innernet networks currently running.");
|
||||||
|
@ -846,7 +850,7 @@ fn show(
|
||||||
let me = peers
|
let me = peers
|
||||||
.iter()
|
.iter()
|
||||||
.find(|p| p.public_key == device_info.public_key.as_ref().unwrap().to_base64())
|
.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
|
let mut peer_states = device_info
|
||||||
.peers
|
.peers
|
||||||
|
@ -858,7 +862,7 @@ fn show(
|
||||||
peer,
|
peer,
|
||||||
info: Some(info),
|
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>, _>>()?;
|
.collect::<Result<Vec<PeerState>, _>>()?;
|
||||||
|
@ -987,6 +991,9 @@ fn main() {
|
||||||
if let Err(e) = run(opt) {
|
if let Err(e) = run(opt) {
|
||||||
println!();
|
println!();
|
||||||
log::error!("{}\n", e);
|
log::error!("{}\n", e);
|
||||||
|
if let Some(e) = e.downcast_ref::<WrappedIoError>() {
|
||||||
|
util::permissions_helptext(e);
|
||||||
|
}
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -998,10 +1005,6 @@ fn run(opt: Opts) -> Result<(), Error> {
|
||||||
interface: None,
|
interface: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
if unsafe { libc::getuid() } != 0 && !matches!(command, Command::Completions { .. }) {
|
|
||||||
return Err("innernet must run as root.".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
match command {
|
match command {
|
||||||
Command::Install {
|
Command::Install {
|
||||||
invite,
|
invite,
|
||||||
|
|
|
@ -1,19 +1,27 @@
|
||||||
use crate::{ClientError, Error};
|
use crate::{ClientError, Error};
|
||||||
use colored::*;
|
use colored::*;
|
||||||
|
use indoc::eprintdoc;
|
||||||
use log::{Level, LevelFilter};
|
use log::{Level, LevelFilter};
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use shared::{interface_config::ServerInfo, INNERNET_PUBKEY_HEADER};
|
use shared::{interface_config::ServerInfo, WrappedIoError, INNERNET_PUBKEY_HEADER};
|
||||||
use std::time::Duration;
|
use std::{io, time::Duration};
|
||||||
use ureq::{Agent, AgentBuilder};
|
use ureq::{Agent, AgentBuilder};
|
||||||
|
|
||||||
static LOGGER: Logger = Logger;
|
static LOGGER: Logger = Logger;
|
||||||
struct 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 {
|
impl log::Log for Logger {
|
||||||
fn enabled(&self, metadata: &log::Metadata) -> bool {
|
fn enabled(&self, metadata: &log::Metadata) -> bool {
|
||||||
metadata.level() <= log::max_level()
|
metadata.level() <= log::max_level()
|
||||||
&& (log::max_level() == LevelFilter::Trace
|
&& (log::max_level() == LevelFilter::Trace || target_is_base(metadata.target()))
|
||||||
|| metadata.target().starts_with("shared::")
|
|
||||||
|| metadata.target() == "innernet")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log(&self, record: &log::Record) {
|
fn log(&self, record: &log::Record) {
|
||||||
|
@ -25,7 +33,7 @@ impl log::Log for Logger {
|
||||||
Level::Debug => "[D]".blue(),
|
Level::Debug => "[D]".blue(),
|
||||||
Level::Trace => "[T]".purple(),
|
Level::Trace => "[T]".purple(),
|
||||||
};
|
};
|
||||||
if record.level() <= LevelFilter::Debug && record.target() != "innernet" {
|
if record.level() <= LevelFilter::Debug && !target_is_base(record.target()) {
|
||||||
println!(
|
println!(
|
||||||
"{} {} {}",
|
"{} {} {}",
|
||||||
level_str,
|
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> {
|
pub struct Api<'a> {
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
server: &'a ServerInfo,
|
server: &'a ServerInfo,
|
||||||
|
|
|
@ -5,7 +5,6 @@ edition = "2018"
|
||||||
license = "UNLICENSED"
|
license = "UNLICENSED"
|
||||||
name = "hostsfile"
|
name = "hostsfile"
|
||||||
publish = false
|
publish = false
|
||||||
version = "1.0.1"
|
version = "1.1.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tempfile = "3"
|
|
||||||
|
|
|
@ -1,12 +1,4 @@
|
||||||
use std::{
|
use std::{collections::HashMap, fmt, fs::OpenOptions, io::{self, BufRead, BufReader, ErrorKind, Write}, net::IpAddr, path::{Path, PathBuf}, result};
|
||||||
collections::HashMap,
|
|
||||||
fmt,
|
|
||||||
fs::{self, File, OpenOptions},
|
|
||||||
io::{BufRead, BufReader, Write},
|
|
||||||
net::IpAddr,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
result,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub type Result<T> = result::Result<T, Box<dyn std::error::Error>>;
|
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
|
/// 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.
|
/// 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) {
|
let hosts_file = if cfg!(unix) {
|
||||||
PathBuf::from("/etc/hosts")
|
PathBuf::from("/etc/hosts")
|
||||||
} else if cfg!(windows) {
|
} else if cfg!(windows) {
|
||||||
|
@ -124,21 +121,24 @@ impl HostsBuilder {
|
||||||
// the location depends on the environment variable %WinDir%.
|
// the location depends on the environment variable %WinDir%.
|
||||||
format!(
|
format!(
|
||||||
"{}\\System32\\Drivers\\Etc\\hosts",
|
"{}\\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 {
|
} 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() {
|
if !hosts_file.exists() {
|
||||||
return Err(Box::new(Error(format!(
|
return Err(ErrorKind::NotFound.into());
|
||||||
"hosts file {:?} missing",
|
|
||||||
&hosts_file
|
|
||||||
))));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
/// 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
|
/// 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).
|
/// 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 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);
|
||||||
|
@ -179,49 +179,44 @@ impl HostsBuilder {
|
||||||
lines.len()
|
lines.len()
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
return Err(Box::new(Error(format!(
|
return Err(io::Error::new(
|
||||||
"start or end marker missing in {:?}",
|
io::ErrorKind::InvalidData,
|
||||||
&hosts_path
|
format!("start or end marker missing in {:?}", &hosts_path),
|
||||||
))));
|
));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// The tempfile should be in the same filesystem as the hosts file.
|
let mut s = vec![];
|
||||||
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)?;
|
|
||||||
|
|
||||||
for line in &lines[..insert] {
|
for line in &lines[..insert] {
|
||||||
writeln!(&mut file, "{}", line)?;
|
writeln!(&mut s, "{}", line)?;
|
||||||
}
|
}
|
||||||
if !self.hostname_map.is_empty() {
|
if !self.hostname_map.is_empty() {
|
||||||
writeln!(&mut file, "{}", begin_marker)?;
|
writeln!(&mut s, "{}", begin_marker)?;
|
||||||
for (ip, hostnames) in &self.hostname_map {
|
for (ip, hostnames) in &self.hostname_map {
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
// windows only allows one hostname per line
|
// windows only allows one hostname per line
|
||||||
for hostname in hostnames {
|
for hostname in hostnames {
|
||||||
writeln!(&mut file, "{} {}", ip, hostname)?;
|
writeln!(&mut s, "{} {}", ip, hostname)?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// assume the same format as Unix
|
// 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..] {
|
for line in &lines[insert..] {
|
||||||
writeln!(&mut file, "{}", line)?;
|
writeln!(&mut s, "{}", line)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move the file atomically to avoid a partial state.
|
OpenOptions::new()
|
||||||
fs::rename(&temp_path, &hosts_path)?;
|
.create(true)
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(hosts_path)?
|
||||||
|
.write_all(&s)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ name = "innernet-server"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = "1"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
colored = "2"
|
colored = "2"
|
||||||
dialoguer = "0.8"
|
dialoguer = "0.8"
|
||||||
|
|
|
@ -229,7 +229,7 @@ mod tests {
|
||||||
let old_peer = DatabasePeer::get(&server.db.lock(), test::DEVELOPER1_PEER_ID)?;
|
let old_peer = DatabasePeer::get(&server.db.lock(), test::DEVELOPER1_PEER_ID)?;
|
||||||
|
|
||||||
let change = PeerContents {
|
let change = PeerContents {
|
||||||
name: "new-peer-name".parse()?,
|
name: "new-peer-name".parse().unwrap(),
|
||||||
..old_peer.contents.clone()
|
..old_peer.contents.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
use anyhow::anyhow;
|
||||||
use db::DatabaseCidr;
|
use db::DatabaseCidr;
|
||||||
use dialoguer::{theme::ColorfulTheme, Input};
|
use dialoguer::{theme::ColorfulTheme, Input};
|
||||||
use indoc::printdoc;
|
use indoc::printdoc;
|
||||||
|
@ -63,7 +64,7 @@ fn populate_database(conn: &Connection, db_init_data: DbInitData) -> Result<(),
|
||||||
parent: None,
|
parent: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.map_err(|_| "failed to create root CIDR".to_string())?;
|
.map_err(|_| anyhow!("failed to create root CIDR"))?;
|
||||||
|
|
||||||
let server_cidr = DatabaseCidr::create(
|
let server_cidr = DatabaseCidr::create(
|
||||||
&conn,
|
&conn,
|
||||||
|
@ -73,12 +74,12 @@ fn populate_database(conn: &Connection, db_init_data: DbInitData) -> Result<(),
|
||||||
parent: Some(root_cidr.id),
|
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(
|
let _me = DatabasePeer::create(
|
||||||
&conn,
|
&conn,
|
||||||
PeerContents {
|
PeerContents {
|
||||||
name: SERVER_NAME.parse()?,
|
name: SERVER_NAME.parse().map_err(|e: &str| anyhow!(e))?,
|
||||||
ip: db_init_data.our_ip,
|
ip: db_init_data.our_ip,
|
||||||
cidr_id: server_cidr.id,
|
cidr_id: server_cidr.id,
|
||||||
public_key: db_init_data.public_key_base64,
|
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,
|
invite_expires: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.map_err(|_| "failed to create innernet peer.".to_string())?;
|
.map_err(|_| anyhow!("failed to create innernet peer."))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -99,7 +100,7 @@ pub fn init_wizard(conf: &ServerConfig, opts: InitializeOpts) -> Result<(), Erro
|
||||||
let theme = ColorfulTheme::default();
|
let theme = ColorfulTheme::default();
|
||||||
|
|
||||||
shared::ensure_dirs_exist(&[conf.config_dir(), conf.database_dir()]).map_err(|_| {
|
shared::ensure_dirs_exist(&[conf.config_dir(), conf.database_dir()]).map_err(|_| {
|
||||||
format!(
|
anyhow!(
|
||||||
"Failed to create config and database directories {}",
|
"Failed to create config and database directories {}",
|
||||||
"(are you not running as root?)".bold()
|
"(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 {
|
let endpoint: Endpoint = if let Some(endpoint) = opts.external_endpoint {
|
||||||
endpoint
|
endpoint
|
||||||
} else if opts.auto_external_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()
|
SocketAddr::new(ip, 51820).into()
|
||||||
} else {
|
} else {
|
||||||
prompts::ask_endpoint()?
|
prompts::ask_endpoint()?
|
||||||
|
@ -152,7 +153,7 @@ pub fn init_wizard(conf: &ServerConfig, opts: InitializeOpts) -> Result<(), Erro
|
||||||
.with_prompt("Listen port")
|
.with_prompt("Listen port")
|
||||||
.default(51820)
|
.default(51820)
|
||||||
.interact()
|
.interact()
|
||||||
.map_err(|_| "failed to get listen port.")?
|
.map_err(|_| anyhow!("failed to get listen port."))?
|
||||||
};
|
};
|
||||||
|
|
||||||
let our_ip = root_cidr
|
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 database_path = conf.database_path(&name);
|
||||||
let conn = create_database(&database_path).map_err(|_| {
|
let conn = create_database(&database_path).map_err(|_| {
|
||||||
format!(
|
anyhow!(
|
||||||
"failed to create database {}",
|
"failed to create database {}",
|
||||||
"(are you not running as root?)".bold()
|
"(are you not running as root?)".bold()
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use anyhow::{anyhow, bail};
|
||||||
use colored::*;
|
use colored::*;
|
||||||
use dialoguer::Confirm;
|
use dialoguer::Confirm;
|
||||||
use hyper::{http, server::conn::AddrStream, Body, Request, Response};
|
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(
|
fn open_database_connection(
|
||||||
interface: &InterfaceName,
|
interface: &InterfaceName,
|
||||||
conf: &ServerConfig,
|
conf: &ServerConfig,
|
||||||
) -> Result<rusqlite::Connection, Box<dyn std::error::Error>> {
|
) -> Result<rusqlite::Connection, Error> {
|
||||||
let database_path = conf.database_path(&interface);
|
let database_path = conf.database_path(&interface);
|
||||||
if !Path::new(&database_path).exists() {
|
if !Path::new(&database_path).exists() {
|
||||||
return Err(format!(
|
bail!(
|
||||||
"no database file found at {}",
|
"no database file found at {}",
|
||||||
database_path.to_string_lossy()
|
database_path.to_string_lossy()
|
||||||
)
|
);
|
||||||
.into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let conn = Connection::open(&database_path)?;
|
let conn = Connection::open(&database_path)?;
|
||||||
|
@ -337,7 +337,7 @@ fn rename_peer(
|
||||||
let mut db_peer = DatabasePeer::list(&conn)?
|
let mut db_peer = DatabasePeer::list(&conn)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|p| p.name == old_name)
|
.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)?;
|
let _peer = db_peer.update(&conn, peer_request)?;
|
||||||
} else {
|
} else {
|
||||||
println!("exited without creating peer.");
|
println!("exited without creating peer.");
|
||||||
|
|
|
@ -221,7 +221,7 @@ pub fn peer_contents(
|
||||||
let public_key = KeyPair::generate().public;
|
let public_key = KeyPair::generate().public;
|
||||||
|
|
||||||
Ok(PeerContents {
|
Ok(PeerContents {
|
||||||
name: name.parse()?,
|
name: name.parse().map_err(|e: &str| anyhow!(e))?,
|
||||||
ip: ip_str.parse()?,
|
ip: ip_str.parse()?,
|
||||||
cidr_id,
|
cidr_id,
|
||||||
public_key: public_key.to_base64(),
|
public_key: public_key.to_base64(),
|
||||||
|
|
|
@ -7,11 +7,13 @@ publish = false
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = "1"
|
||||||
colored = "2.0"
|
colored = "2.0"
|
||||||
dialoguer = "0.8"
|
dialoguer = "0.8"
|
||||||
indoc = "1"
|
indoc = "1"
|
||||||
ipnetwork = { git = "https://github.com/mcginty/ipnetwork" } # pending https://github.com/achanda/ipnetwork/pull/129
|
ipnetwork = { git = "https://github.com/mcginty/ipnetwork" } # pending https://github.com/achanda/ipnetwork/pull/129
|
||||||
lazy_static = "1"
|
lazy_static = "1"
|
||||||
|
libc = "0.2"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
publicip = { path = "../publicip" }
|
publicip = { path = "../publicip" }
|
||||||
regex = "1"
|
regex = "1"
|
||||||
|
@ -20,3 +22,9 @@ structopt = "0.3"
|
||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
url = "2"
|
url = "2"
|
||||||
wgctrl = { path = "../wgctrl-rs" }
|
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" }
|
||||||
|
|
|
@ -116,14 +116,7 @@ impl InterfaceConfig {
|
||||||
|
|
||||||
pub fn from_interface(interface: &InterfaceName) -> Result<Self, Error> {
|
pub fn from_interface(interface: &InterfaceName) -> Result<Self, Error> {
|
||||||
let path = Self::build_config_file_path(interface)?;
|
let path = Self::build_config_file_path(interface)?;
|
||||||
let file = File::open(&path).with_path(&path)?;
|
crate::warn_on_dangerous_mode(&path).with_path(&path)?;
|
||||||
if crate::chmod(&file, 0o600)? {
|
|
||||||
println!(
|
|
||||||
"{} updated permissions for {} to 0600.",
|
|
||||||
"[!]".yellow(),
|
|
||||||
path.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Self::from_file(path)
|
Self::from_file(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use colored::*;
|
pub use anyhow::Error;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use std::{
|
use std::{
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
|
@ -9,6 +9,8 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod interface_config;
|
pub mod interface_config;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
mod netlink;
|
||||||
pub mod prompts;
|
pub mod prompts;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod wg;
|
pub mod wg;
|
||||||
|
@ -26,8 +28,6 @@ lazy_static! {
|
||||||
pub const PERSISTENT_KEEPALIVE_INTERVAL_SECS: u16 = 25;
|
pub const PERSISTENT_KEEPALIVE_INTERVAL_SECS: u16 = 25;
|
||||||
pub const INNERNET_PUBKEY_HEADER: &str = "X-Innernet-Server-Key";
|
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> {
|
pub fn ensure_dirs_exist(dirs: &[&Path]) -> Result<(), WrappedIoError> {
|
||||||
for dir in dirs {
|
for dir in dirs {
|
||||||
match fs::create_dir(dir).with_path(dir) {
|
match fs::create_dir(dir).with_path(dir) {
|
||||||
|
@ -35,20 +35,29 @@ pub fn ensure_dirs_exist(dirs: &[&Path]) -> Result<(), WrappedIoError> {
|
||||||
return Err(e);
|
return Err(e);
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
let target_file = File::open(dir).with_path(dir)?;
|
warn_on_dangerous_mode(dir).with_path(dir)?;
|
||||||
if chmod(&target_file, 0o700).with_path(dir)? {
|
|
||||||
println!(
|
|
||||||
"{} updated permissions for {} to 0700.",
|
|
||||||
"[!]".yellow(),
|
|
||||||
dir.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
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
|
/// Updates the permissions of a file or directory. Returns `Ok(true)` if
|
||||||
/// permissions had to be changed, `Ok(false)` if permissions were already
|
/// permissions had to be changed, `Ok(false)` if permissions were already
|
||||||
/// correct.
|
/// correct.
|
||||||
|
|
|
@ -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()),
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ use crate::{
|
||||||
AddCidrOpts, AddPeerOpts, Association, Cidr, CidrContents, CidrTree, DeleteCidrOpts, Endpoint,
|
AddCidrOpts, AddPeerOpts, Association, Cidr, CidrContents, CidrTree, DeleteCidrOpts, Endpoint,
|
||||||
Error, Hostname, Peer, PeerContents, RenamePeerOpts, PERSISTENT_KEEPALIVE_INTERVAL_SECS,
|
Error, Hostname, Peer, PeerContents, RenamePeerOpts, PERSISTENT_KEEPALIVE_INTERVAL_SECS,
|
||||||
};
|
};
|
||||||
|
use anyhow::anyhow;
|
||||||
use colored::*;
|
use colored::*;
|
||||||
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
|
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
|
||||||
use ipnetwork::IpNetwork;
|
use ipnetwork::IpNetwork;
|
||||||
|
@ -21,7 +22,7 @@ pub fn add_cidr(cidrs: &[Cidr], request: &AddCidrOpts) -> Result<Option<CidrCont
|
||||||
cidrs
|
cidrs
|
||||||
.iter()
|
.iter()
|
||||||
.find(|cidr| &cidr.name == parent_name)
|
.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 {
|
} else {
|
||||||
choose_cidr(cidrs, "Parent CIDR")?
|
choose_cidr(cidrs, "Parent CIDR")?
|
||||||
};
|
};
|
||||||
|
@ -74,7 +75,7 @@ pub fn delete_cidr(cidrs: &[Cidr], peers: &[Peer], request: &DeleteCidrOpts) ->
|
||||||
cidrs
|
cidrs
|
||||||
.iter()
|
.iter()
|
||||||
.find(|cidr| &cidr.name == name)
|
.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 {
|
} else {
|
||||||
let cidr_index = Select::with_theme(&*THEME)
|
let cidr_index = Select::with_theme(&*THEME)
|
||||||
.with_prompt("Delete CIDR")
|
.with_prompt("Delete CIDR")
|
||||||
|
@ -92,7 +93,7 @@ pub fn delete_cidr(cidrs: &[Cidr], peers: &[Peer], request: &DeleteCidrOpts) ->
|
||||||
{
|
{
|
||||||
Ok(cidr.id)
|
Ok(cidr.id)
|
||||||
} else {
|
} else {
|
||||||
Err("Canceled".into())
|
Err(anyhow!("Canceled"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,7 +194,7 @@ pub fn add_peer(
|
||||||
leaves
|
leaves
|
||||||
.iter()
|
.iter()
|
||||||
.find(|cidr| &cidr.name == parent_name)
|
.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 {
|
} else {
|
||||||
choose_cidr(&leaves[..], "Eligible CIDRs for peer")?
|
choose_cidr(&leaves[..], "Eligible CIDRs for peer")?
|
||||||
};
|
};
|
||||||
|
@ -241,7 +242,7 @@ pub fn add_peer(
|
||||||
} else {
|
} else {
|
||||||
Input::with_theme(&*THEME)
|
Input::with_theme(&*THEME)
|
||||||
.with_prompt("Invite expires after")
|
.with_prompt("Invite expires after")
|
||||||
.default("14d".parse()?)
|
.default("14d".parse().map_err(|s: &str| anyhow!(s))?)
|
||||||
.interact()?
|
.interact()?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -287,7 +288,7 @@ pub fn rename_peer(
|
||||||
eligible_peers
|
eligible_peers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|p| &p.name == name)
|
.find(|p| &p.name == name)
|
||||||
.ok_or_else(|| format!("Peer '{}' does not exist", name))?
|
.ok_or_else(|| anyhow!("Peer '{}' does not exist", name))?
|
||||||
.clone()
|
.clone()
|
||||||
} else {
|
} else {
|
||||||
let peer_index = Select::with_theme(&*THEME)
|
let peer_index = Select::with_theme(&*THEME)
|
||||||
|
|
|
@ -4,6 +4,7 @@ use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::{self, Display, Formatter},
|
fmt::{self, Display, Formatter},
|
||||||
|
io,
|
||||||
net::{IpAddr, SocketAddr, ToSocketAddrs},
|
net::{IpAddr, SocketAddr, ToSocketAddrs},
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
path::Path,
|
path::Path,
|
||||||
|
@ -121,14 +122,14 @@ impl fmt::Display for Endpoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Endpoint {
|
impl Endpoint {
|
||||||
pub fn resolve(&self) -> Result<SocketAddr, String> {
|
pub fn resolve(&self) -> Result<SocketAddr, io::Error> {
|
||||||
let mut addrs = self
|
let mut addrs = self.to_string().to_socket_addrs()?;
|
||||||
.to_string()
|
addrs.next().ok_or_else(|| {
|
||||||
.to_socket_addrs()
|
io::Error::new(
|
||||||
.map_err(|e| e.to_string())?;
|
io::ErrorKind::AddrNotAvailable,
|
||||||
addrs
|
"failed to resolve address".to_string(),
|
||||||
.next()
|
)
|
||||||
.ok_or_else(|| "failed to resolve address".to_string())
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
106
shared/src/wg.rs
106
shared/src/wg.rs
|
@ -1,13 +1,11 @@
|
||||||
use crate::{Error, IoErrorContext, NetworkOpt};
|
use crate::{Error, IoErrorContext, NetworkOpt};
|
||||||
use ipnetwork::IpNetwork;
|
use ipnetwork::IpNetwork;
|
||||||
use std::{
|
use std::net::{IpAddr, SocketAddr};
|
||||||
net::{IpAddr, SocketAddr},
|
|
||||||
process::{self, Command},
|
|
||||||
};
|
|
||||||
use wgctrl::{Backend, Device, DeviceUpdate, InterfaceName, PeerConfigBuilder};
|
use wgctrl::{Backend, Device, DeviceUpdate, InterfaceName, PeerConfigBuilder};
|
||||||
|
|
||||||
fn cmd(bin: &str, args: &[&str]) -> Result<process::Output, Error> {
|
#[cfg(target_os = "macos")]
|
||||||
let output = Command::new(bin).args(args).output()?;
|
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!("cmd: {} {}", bin, args.join(" "));
|
||||||
log::debug!("status: {:?}", output.status.code());
|
log::debug!("status: {:?}", output.status.code());
|
||||||
log::trace!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
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() {
|
if output.status.success() {
|
||||||
Ok(output)
|
Ok(output)
|
||||||
} else {
|
} else {
|
||||||
Err(format!(
|
Err(anyhow::anyhow!(
|
||||||
"failed to run {} {} command: {}",
|
"failed to run {} {} command: {}",
|
||||||
bin,
|
bin,
|
||||||
args.join(" "),
|
args.join(" "),
|
||||||
String::from_utf8_lossy(&output.stderr)
|
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(),
|
&addr.ip().to_string(),
|
||||||
"alias",
|
"alias",
|
||||||
],
|
],
|
||||||
)?;
|
)
|
||||||
|
.map(|_output| ())
|
||||||
} else {
|
} else {
|
||||||
cmd(
|
cmd(
|
||||||
"ifconfig",
|
"ifconfig",
|
||||||
&[&real_interface, "inet6", &addr.to_string(), "alias"],
|
&[&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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub fn set_addr(interface: &InterfaceName, addr: IpNetwork) -> Result<(), Error> {
|
pub use super::netlink::set_addr;
|
||||||
let interface = interface.to_string();
|
|
||||||
cmd(
|
#[cfg(target_os = "linux")]
|
||||||
"ip",
|
pub use super::netlink::set_up;
|
||||||
&["address", "replace", &addr.to_string(), "dev", &interface],
|
|
||||||
)?;
|
|
||||||
cmd(
|
|
||||||
"ip",
|
|
||||||
&["link", "set", "mtu", "1420", "up", "dev", &interface],
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn up(
|
pub fn up(
|
||||||
interface: &InterfaceName,
|
interface: &InterfaceName,
|
||||||
|
@ -88,6 +85,7 @@ pub fn up(
|
||||||
.set_private_key(wgctrl::Key::from_base64(&private_key).unwrap())
|
.set_private_key(wgctrl::Key::from_base64(&private_key).unwrap())
|
||||||
.apply(interface, network.backend)?;
|
.apply(interface, network.backend)?;
|
||||||
set_addr(interface, address)?;
|
set_addr(interface, address)?;
|
||||||
|
set_up(interface, 1420)?;
|
||||||
if !network.no_routing {
|
if !network.no_routing {
|
||||||
add_route(interface, address)?;
|
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.
|
/// 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
|
/// Returns an error if the process doesn't exit successfully, otherwise returns
|
||||||
/// true if the route was changed, false if the route already exists.
|
/// 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> {
|
pub fn add_route(interface: &InterfaceName, cidr: IpNetwork) -> Result<bool, Error> {
|
||||||
if cfg!(target_os = "macos") {
|
let real_interface =
|
||||||
let real_interface =
|
wgctrl::backends::userspace::resolve_tun(interface).with_str(interface.to_string())?;
|
||||||
wgctrl::backends::userspace::resolve_tun(interface).with_str(interface.to_string())?;
|
let output = cmd(
|
||||||
let output = cmd(
|
"route",
|
||||||
"route",
|
&[
|
||||||
&[
|
"-n",
|
||||||
"-n",
|
"add",
|
||||||
"add",
|
if cidr.is_ipv4() { "-inet" } else { "-inet6" },
|
||||||
if cidr.is_ipv4() { "-inet" } else { "-inet6" },
|
&cidr.to_string(),
|
||||||
&cidr.to_string(),
|
"-interface",
|
||||||
"-interface",
|
&real_interface,
|
||||||
&real_interface,
|
],
|
||||||
],
|
)?;
|
||||||
)?;
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
if !output.status.success() {
|
||||||
if !output.status.success() {
|
Err(anyhow::anyhow!(
|
||||||
Err(format!(
|
"failed to add route for device {} ({}): {}",
|
||||||
"failed to add route for device {} ({}): {}",
|
&interface, real_interface, stderr
|
||||||
&interface, real_interface, stderr
|
))
|
||||||
)
|
|
||||||
.into())
|
|
||||||
} else {
|
|
||||||
Ok(!stderr.contains("File exists"))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// TODO(mcginty): use the netlink interface on linux to modify routing table.
|
Ok(!stderr.contains("File exists"))
|
||||||
let _ = cmd(
|
|
||||||
"ip",
|
|
||||||
&[
|
|
||||||
"route",
|
|
||||||
"add",
|
|
||||||
&IpNetwork::new(cidr.network(), cidr.prefix())?.to_string(),
|
|
||||||
"dev",
|
|
||||||
&interface.to_string(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
Ok(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub use super::netlink::add_route;
|
||||||
|
|
|
@ -165,7 +165,7 @@ impl InterfaceName {
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
/// Returns a pointer to the inner byte buffer for FFI calls.
|
/// 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()
|
self.0.as_ptr()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue