From 8b6f479eddb04a9a2d2548c3e0c4011b31aba46d Mon Sep 17 00:00:00 2001 From: dzania Date: Thu, 7 Sep 2023 12:45:39 +0200 Subject: [PATCH 1/7] Add wgapi module --- src/lib.rs | 1 + src/wgapi.rs | 197 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 src/wgapi.rs diff --git a/src/lib.rs b/src/lib.rs index d2b5777..762a53f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod host; pub mod key; pub mod net; pub mod netlink; +pub mod wgapi; #[macro_use] extern crate log; 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); + } +} From 498c370d4811dba37c94f4726e72873199faaa89 Mon Sep 17 00:00:00 2001 From: dzania Date: Thu, 7 Sep 2023 12:52:10 +0200 Subject: [PATCH 2/7] fix imports --- src/bsd/mod.rs | 5 ++++- src/lib.rs | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) 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/lib.rs b/src/lib.rs index 762a53f..8e88715 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,9 @@ +#[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; From 04a7dade9d3aca5d3b4ab54d6f22f3aeaff4ee3f Mon Sep 17 00:00:00 2001 From: dzania Date: Thu, 7 Sep 2023 13:22:39 +0200 Subject: [PATCH 3/7] add custom error and setup interface method --- Cargo.toml | 3 ++ src/error.rs | 19 +++++++++ src/lib.rs | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 src/error.rs diff --git a/Cargo.toml b/Cargo.toml index 982aefe..06cff2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,9 @@ edition = "2021" [dependencies] base64 = "0.13" log = "0.4" +thiserror = "1.0.48" +tokio = "1.32.0" +boringtun = { version = "0.4", optional = true } [target.'cfg(target_os = "freebsd")'.dependencies] nix = { version = "0.26", features = ["ioctl", "socket"] } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..03b1edf --- /dev/null +++ b/src/error.rs @@ -0,0 +1,19 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum DefguardWireguardError { + #[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 8e88715..facaa83 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,114 @@ 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::DefguardWireguardError; + +#[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<(), DefguardWireguardError> { + 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<(), DefguardWireguardError> { + 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<(), DefguardWireguardError> { + 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}, +}; From fb546ebe08f82c74c19f3ae02320856cfe0da75d Mon Sep 17 00:00:00 2001 From: dzania Date: Thu, 7 Sep 2023 13:39:51 +0200 Subject: [PATCH 4/7] rename error enum --- src/error.rs | 2 +- src/lib.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/error.rs b/src/error.rs index 03b1edf..818a5e7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,7 +1,7 @@ use thiserror::Error; #[derive(Debug, Error)] -pub enum DefguardWireguardError { +pub enum WireguardError { #[error("Interface setup error: {0}")] Interface(String), #[cfg(feature = "boringtun")] diff --git a/src/lib.rs b/src/lib.rs index facaa83..dc29226 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,7 +19,7 @@ extern crate log; use std::{process::Command, str::FromStr}; use wgapi::WGApi; -use crate::error::DefguardWireguardError; +use crate::error::WireguardError; #[derive(Debug, Clone)] pub struct InterfaceConfiguration { @@ -37,7 +37,7 @@ pub struct InterfaceConfiguration { /// /// * `name` - Interface name #[cfg(feature = "boringtun")] -pub fn create_interface_userspace(ifname: &str) -> Result<(), DefguardWireguardError> { +pub fn create_interface_userspace(ifname: &str) -> Result<(), WireguardError> { let enable_drop_privileges = true; let config = DeviceConfig::default(); @@ -62,7 +62,7 @@ pub fn create_interface_userspace(ifname: &str) -> Result<(), DefguardWireguardE /// /// * `interface` - Interface name /// * `addr` - Address to assign to interface -pub fn assign_addr(ifname: &str, addr: &IpAddrMask) -> Result<(), DefguardWireguardError> { +pub fn assign_addr(ifname: &str, addr: &IpAddrMask) -> Result<(), WireguardError> { if cfg!(target_os = "linux") { #[cfg(target_os = "linux")] netlink::address_interface(ifname, addr)?; @@ -86,7 +86,7 @@ pub fn setup_interface( ifname: &str, userspace: bool, config: &InterfaceConfiguration, -) -> Result<(), DefguardWireguardError> { +) -> Result<(), WireguardError> { if userspace { #[cfg(feature = "boringtun")] create_interface_userspace(ifname)?; From 8b8b3e294341e0b1a50db5aa96dd4c485fba63b6 Mon Sep 17 00:00:00 2001 From: dzania Date: Thu, 7 Sep 2023 13:56:38 +0200 Subject: [PATCH 5/7] add missing doc --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index dc29226..b1013df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ 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, From f0006b21e1307a82e848149dde0be45e9133a7e1 Mon Sep 17 00:00:00 2001 From: Artur Kantorczyk <64010779+dzania@users.noreply.github.com> Date: Thu, 7 Sep 2023 19:42:45 +0200 Subject: [PATCH 6/7] Update Cargo.toml Co-authored-by: Adam --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 06cff2a..534239a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] base64 = "0.13" log = "0.4" -thiserror = "1.0.48" +thiserror = "1.0" tokio = "1.32.0" boringtun = { version = "0.4", optional = true } From ccb6e19fd92d6da41c172f538502a3b44798870f Mon Sep 17 00:00:00 2001 From: Artur Kantorczyk <64010779+dzania@users.noreply.github.com> Date: Thu, 7 Sep 2023 19:42:54 +0200 Subject: [PATCH 7/7] Update Cargo.toml Co-authored-by: Adam --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 534239a..1d478d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" base64 = "0.13" log = "0.4" thiserror = "1.0" -tokio = "1.32.0" +tokio = "1.32" boringtun = { version = "0.4", optional = true } [target.'cfg(target_os = "freebsd")'.dependencies]