Skip to content

Commit

Permalink
Merge pull request #2 from DefGuard/wgapi
Browse files Browse the repository at this point in the history
Add wgapi module
  • Loading branch information
dzania authored Sep 8, 2023
2 parents 94bf824 + ccb6e19 commit 5abf8f9
Show file tree
Hide file tree
Showing 5 changed files with 336 additions and 1 deletion.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
5 changes: 4 additions & 1 deletion src/bsd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
19 changes: 19 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -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),
}
113 changes: 113 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<Peer>
}

/// 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},
};
197 changes: 197 additions & 0 deletions src/wgapi.rs
Original file line number Diff line number Diff line change
@@ -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<UnixStream> {
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<Host> {
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);
}
}

0 comments on commit 5abf8f9

Please sign in to comment.