Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add wgapi module #2

Merged
merged 7 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.48"
dzania marked this conversation as resolved.
Show resolved Hide resolved
tokio = "1.32.0"
dzania marked this conversation as resolved.
Show resolved Hide resolved
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);
}
}