diff --git a/Cargo.toml b/Cargo.toml index 982aefe..1d478d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,9 @@ edition = "2021" [dependencies] base64 = "0.13" log = "0.4" +thiserror = "1.0" +tokio = "1.32" +boringtun = { version = "0.4", optional = true } [target.'cfg(target_os = "freebsd")'.dependencies] nix = { version = "0.26", features = ["ioctl", "socket"] } diff --git a/src/bsd/mod.rs b/src/bsd/mod.rs index f020f64..43bca0c 100644 --- a/src/bsd/mod.rs +++ b/src/bsd/mod.rs @@ -11,7 +11,10 @@ use self::{ timespec::{pack_timespec, unpack_timespec}, wgio::{WgDataIo, WgIoError}, }; -use super::{Host, IpAddrMask, Peer}; +use crate::{ + host::{Host, Peer}, + net::IpAddrMask, +}; // nvlist key names static NV_LISTEN_PORT: &str = "listen-port"; diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..818a5e7 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,19 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum WireguardError { + #[error("Interface setup error: {0}")] + Interface(String), + #[cfg(feature = "boringtun")] + #[error("BorningTun error")] + BorningTun(boringtun::device::Error), + #[error("Command execution failed")] + CommandExecutionFailed(#[from] std::io::Error), + #[error("WireGuard key error")] + KeyDecode(#[from] base64::DecodeError), + + #[error("Command returned error status")] + CommandExecutionError { stderr: String }, + #[error("IP address/mask error")] + IpAddrMask(#[from] crate::net::IpAddrParseError), +} diff --git a/src/lib.rs b/src/lib.rs index d2b5777..b1013df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,120 @@ +#[cfg(target_os = "freebsd")] +pub mod bsd; pub mod host; pub mod key; pub mod net; +#[cfg(target_os = "linux")] pub mod netlink; +pub mod wgapi; +pub mod error; + +#[cfg(feature = "boringtun")] +use boringtun::{ + device::drop_privileges::drop_privileges, + device::{DeviceConfig, DeviceHandle}, +}; #[macro_use] extern crate log; + +use std::{process::Command, str::FromStr}; +use wgapi::WGApi; +use crate::error::WireguardError; + +/// Wireguard Interface configuration +#[derive(Debug, Clone)] +pub struct InterfaceConfiguration { + pub name: String, + pub prvkey: String, + pub address: String, + pub port: u32, + pub peers: Vec +} + +/// Creates wireguard interface using userspace implementation. +/// https://github.com/cloudflare/boringtun +/// +/// # Arguments +/// +/// * `name` - Interface name +#[cfg(feature = "boringtun")] +pub fn create_interface_userspace(ifname: &str) -> Result<(), WireguardError> { + let enable_drop_privileges = true; + + let config = DeviceConfig::default(); + + let mut device_handle = DeviceHandle::new(ifname, config).map_err(GatewayError::BorningTun)?; + + if enable_drop_privileges { + if let Err(e) = drop_privileges() { + error!("Failed to drop privileges: {:?}", e); + } + } + + tokio::spawn(async move { + device_handle.wait(); + }); + Ok(()) +} + +/// Assigns address to interface. +/// +/// # Arguments +/// +/// * `interface` - Interface name +/// * `addr` - Address to assign to interface +pub fn assign_addr(ifname: &str, addr: &IpAddrMask) -> Result<(), WireguardError> { + if cfg!(target_os = "linux") { + #[cfg(target_os = "linux")] + netlink::address_interface(ifname, addr)?; + } else if cfg!(target_os = "macos") { + // On macOS, interface is point-to-point and requires a pair of addresses + let address_string = addr.ip.to_string(); + Command::new("ifconfig") + .args([ifname, &address_string, &address_string]) + .output()?; + } else { + Command::new("ifconfig") + .args([ifname, &addr.to_string()]) + .output()?; + } + + Ok(()) +} + +/// Helper method performing interface configuration +pub fn setup_interface( + ifname: &str, + userspace: bool, + config: &InterfaceConfiguration, +) -> Result<(), WireguardError> { + if userspace { + #[cfg(feature = "boringtun")] + create_interface_userspace(ifname)?; + } else { + #[cfg(target_os = "linux")] + netlink::create_interface(ifname)?; + } + + let address = IpAddrMask::from_str(&config.address)?; + assign_addr(ifname, &address)?; + let key = config.prvkey.as_str().try_into()?; + let mut host = Host::new(config.port as u16, key); + for peercfg in &config.peers { + let key: Key = peercfg.public_key.clone(); + let mut peer = Peer::new(key.clone()); + peer.set_allowed_ips(peercfg.allowed_ips.clone()); + + host.peers.insert(key, peer); + } + let api = WGApi::new(ifname.into(), userspace); + api.write_host(&host)?; + + Ok(()) +} + +pub use { + host::{Host, Peer}, + key::Key, + net::{IpAddrMask, IpAddrParseError}, +}; diff --git a/src/wgapi.rs b/src/wgapi.rs new file mode 100644 index 0000000..97e18ed --- /dev/null +++ b/src/wgapi.rs @@ -0,0 +1,197 @@ +use std::{ + io::{self, BufRead, BufReader, Read, Write}, + os::unix::net::UnixStream, + time::Duration, +}; + +#[cfg(target_os = "freebsd")] +use crate::bsd::{delete_peer, get_host, set_host, set_peer}; +use crate::host::{Host, Peer}; +#[cfg(target_os = "linux")] +use crate::netlink::{delete_peer, get_host, set_host, set_peer}; + +pub struct WGApi { + ifname: String, + userspace: bool, +} + +impl WGApi { + #[must_use] + pub fn new(ifname: String, userspace: bool) -> Self { + Self { ifname, userspace } + } + + fn socket(&self) -> io::Result { + let path = format!("/var/run/wireguard/{}.sock", self.ifname); + let socket = UnixStream::connect(path)?; + socket.set_read_timeout(Some(Duration::new(3, 0)))?; + Ok(socket) + } + + // FIXME: currenty other errors are ignored and result in 0 being returned. + fn parse_errno(buf: impl Read) -> u32 { + let reader = BufReader::new(buf); + for line_result in reader.lines() { + let line = match line_result { + Ok(line) => line, + Err(err) => { + error!("Error parsing buffer line: {err}"); + continue; + } + }; + if let Some((keyword, value)) = line.split_once('=') { + if keyword == "errno" { + return value.parse().unwrap_or_default(); + } + } + } + 0 + } + + pub fn read_host(&self) -> io::Result { + debug!("Reading host interface info"); + if self.userspace { + let mut socket = self.socket()?; + socket.write_all(b"get=1\n\n")?; + Host::parse_uapi(socket) + } else { + #[cfg(target_os = "freebsd")] + { + // FIXME: use proper error + get_host(&self.ifname).map_err(|err| { + io::Error::new(io::ErrorKind::Other, format!("kernel support error: {err}")) + }) + } + #[cfg(target_os = "linux")] + { + get_host(&self.ifname) + } + #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] + Err(io::Error::new( + io::ErrorKind::Other, + "kernel support is not available on this platform", + )) + } + } + + pub fn write_host(&self, host: &Host) -> io::Result<()> { + if self.userspace { + let mut socket = self.socket()?; + socket.write_all(b"set=1\n")?; + socket.write_all(host.as_uapi().as_bytes())?; + socket.write_all(b"\n")?; + + if Self::parse_errno(socket) != 0 { + Err(io::Error::new( + io::ErrorKind::InvalidData, + "write configuration error", + )) + } else { + Ok(()) + } + } else { + #[cfg(target_os = "freebsd")] + { + // FIXME: use proper error + set_host(&self.ifname, host).map_err(|err| { + io::Error::new(io::ErrorKind::Other, format!("kernel support error: {err}")) + }) + } + #[cfg(target_os = "linux")] + { + set_host(&self.ifname, host) + } + #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] + Err(io::Error::new( + io::ErrorKind::Other, + "kernel support is not available on this platform", + )) + } + } + + pub fn write_peer(&self, peer: &Peer) -> io::Result<()> { + if self.userspace { + let mut socket = self.socket()?; + socket.write_all(b"set=1\n")?; + socket.write_all(peer.as_uapi_update().as_bytes())?; + socket.write_all(b"\n")?; + + if Self::parse_errno(socket) != 0 { + Err(io::Error::new( + io::ErrorKind::InvalidData, + "write configuration error", + )) + } else { + Ok(()) + } + } else { + #[cfg(target_os = "freebsd")] + { + // FIXME: use proper error + set_peer(&self.ifname, peer).map_err(|err| { + io::Error::new(io::ErrorKind::Other, format!("kernel support error: {err}")) + }) + } + #[cfg(target_os = "linux")] + { + set_peer(&self.ifname, peer) + } + #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] + Err(io::Error::new( + io::ErrorKind::Other, + "kernel support is not available on this platform", + )) + } + } + + pub fn delete_peer(&self, peer: &Peer) -> io::Result<()> { + if self.userspace { + let mut socket = self.socket()?; + socket.write_all(b"set=1\n")?; + socket.write_all(peer.as_uapi_remove().as_bytes())?; + socket.write_all(b"\n")?; + + if Self::parse_errno(socket) != 0 { + Err(io::Error::new( + io::ErrorKind::InvalidData, + "write configuration error", + )) + } else { + Ok(()) + } + } else { + #[cfg(target_os = "freebsd")] + { + // FIXME: use proper error + delete_peer(&self.ifname, peer).map_err(|err| { + io::Error::new(io::ErrorKind::Other, format!("kernel support error: {err}")) + }) + } + #[cfg(target_os = "linux")] + { + delete_peer(&self.ifname, peer) + } + #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] + Err(io::Error::new( + io::ErrorKind::Other, + "kernel support is not available on this platform", + )) + } + } +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use super::*; + + #[test] + fn test_parse_errno() { + let buf = Cursor::new(b"errno=0\n"); + assert_eq!(WGApi::parse_errno(buf), 0); + + let buf = Cursor::new(b"errno=12345\n"); + assert_eq!(WGApi::parse_errno(buf), 12345); + } +}