use colored::Colorize;
use hostsfile::HostsBuilder;
use ipnet::IpNet;
use std::{
fs::{self, File, Permissions},
io,
net::{IpAddr, Ipv6Addr},
os::unix::fs::PermissionsExt,
path::Path,
time::Duration,
};
use wireguard_control::InterfaceName;
pub mod interface_config;
#[cfg(target_os = "linux")]
mod netlink;
pub mod peer;
pub mod prompts;
pub mod types;
pub mod wg;
pub use anyhow::Error;
pub use types::*;
pub const REDEEM_TRANSITION_WAIT: Duration = Duration::from_secs(5);
pub const PERSISTENT_KEEPALIVE_INTERVAL_SECS: u16 = 25;
pub const INNERNET_PUBKEY_HEADER: &str = "X-Innernet-Server-Key";
pub const DEFAULT_HOSTS_PATH: &str = "/etc/hosts";
pub fn ensure_dirs_exist(dirs: &[&Path]) -> Result<(), WrappedIoError> {
for dir in dirs {
match fs::create_dir(dir).with_path(dir) {
Ok(()) => {
log::debug!("created dir {}", dir.to_string_lossy());
std::fs::set_permissions(dir, Permissions::from_mode(0o700)).with_path(dir)?;
},
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
},
Err(e) => {
return Err(e);
},
}
}
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(())
}
pub fn chmod(file: &File, new_mode: u32) -> Result<bool, io::Error> {
let metadata = file.metadata()?;
let mut permissions = metadata.permissions();
let mode = permissions.mode() & 0o777;
let updated = if mode != new_mode {
permissions.set_mode(new_mode);
file.set_permissions(permissions)?;
true
} else {
false
};
Ok(updated)
}
#[cfg(any(target_os = "macos", target_os = "openbsd"))]
pub fn _get_local_addrs() -> Result<impl Iterator<Item = std::net::IpAddr>, io::Error> {
use std::net::Ipv4Addr;
use nix::net::if_::InterfaceFlags;
let addrs = nix::ifaddrs::getifaddrs()?
.filter(|addr| {
addr.flags.contains(InterfaceFlags::IFF_UP)
&& !addr.flags.intersects(
InterfaceFlags::IFF_LOOPBACK
| InterfaceFlags::IFF_POINTOPOINT
| InterfaceFlags::IFF_PROMISC,
)
})
.filter_map(|interface_addr| {
interface_addr.address.and_then(|addr| {
if let Some(sockaddr_in) = addr.as_sockaddr_in() {
Some(IpAddr::V4(Ipv4Addr::from(sockaddr_in.ip())))
} else {
addr.as_sockaddr_in6()
.map(|sockaddr_in6| IpAddr::V6(sockaddr_in6.ip()))
}
})
});
Ok(addrs)
}
#[cfg(target_os = "linux")]
pub use netlink::get_local_addrs as _get_local_addrs;
pub fn get_local_addrs() -> Result<impl Iterator<Item = std::net::IpAddr>, io::Error> {
fn is_unicast_global(ip: &Ipv6Addr) -> bool {
!((ip.segments()[0] & 0xff00) == 0xff00 || ip.is_loopback()
|| ip.is_unspecified()
|| ((ip.segments()[0] == 0x2001) && (ip.segments()[1] == 0xdb8)) || (ip.segments()[0] & 0xffc0) == 0xfe80 || (ip.segments()[0] & 0xfe00) == 0xfc00) }
Ok(_get_local_addrs()?
.filter(|ip| {
ip.is_ipv4()
|| matches!(ip,
IpAddr::V6(v6) if is_unicast_global(v6))
})
.take(10))
}
pub trait IpNetExt {
fn is_assignable(&self, ip: &IpAddr) -> bool;
}
impl IpNetExt for IpNet {
fn is_assignable(&self, ip: &IpAddr) -> bool {
self.contains(ip)
&& match self {
IpNet::V4(_) => {
self.prefix_len() >= 31 || (ip != &self.network() && ip != &self.broadcast())
},
IpNet::V6(_) => self.prefix_len() >= 127 || ip != &self.network(),
}
}
}
pub fn update_hosts_file(
interface: &InterfaceName,
opts: &HostsOpts,
peers: impl IntoIterator<Item = impl AsRef<Peer>>,
) -> Result<(), WrappedIoError> {
if opts.no_write_hosts {
return Ok(());
}
let mut hosts_builder = HostsBuilder::new(format!("innernet {interface}"));
for peer in peers {
let peer = peer.as_ref();
let peer_hostname = if let Some(suffix) = &opts.host_suffix {
if suffix.is_empty() {
peer.contents.name.to_string()
} else {
format!("{}.{}", peer.contents.name, suffix)
}
} else {
format!("{}.{}.wg", peer.contents.name, interface)
};
hosts_builder.add_hostname(peer.contents.ip, peer_hostname);
}
match hosts_builder
.write_to(opts.hosts_path.as_path())
.with_path(opts.hosts_path.as_path())
{
Ok(has_written) if has_written => {
log::info!(
"updated {} with the latest peers.",
opts.hosts_path.to_string_lossy().yellow()
)
},
Ok(_) => {},
Err(e) => log::warn!("failed to update hosts ({})", e),
};
Ok(())
}