diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..2d33829 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,16 @@ +changelog: + exclude: + labels: + - ignore-for-release + categories: + - title: Breaking Changes 🛠 + labels: + - Semver-Major + - breaking-change + - title: Exciting New Features 🎉 + labels: + - Semver-Minor + - enhancement + - title: Other Changes + labels: + - "*" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..10f00ef --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,18 @@ +name: Create a new GitHub release +on: + push: + tags: + - v*.*.* + +jobs: + create-release: + runs-on: self-hosted + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Create release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + draft: true + generate_release_notes: true diff --git a/.gitignore b/.gitignore index ea8c4bf..c403c34 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.idea/ diff --git a/Cargo.lock b/Cargo.lock index 14d68f9..5da0d38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,6 +86,23 @@ dependencies = [ "syn", ] +[[package]] +name = "defguard_wireguard_rs" +version = "0.2.0" +dependencies = [ + "base64", + "log", + "netlink-packet-core", + "netlink-packet-generic", + "netlink-packet-route", + "netlink-packet-utils", + "netlink-packet-wireguard", + "netlink-sys", + "nix", + "thiserror", + "x25519-dalek", +] + [[package]] name = "fiat-crypto" version = "0.2.1" @@ -303,18 +320,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.48" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.48" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", @@ -333,23 +350,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "wireguard_rs" -version = "0.1.0" -dependencies = [ - "base64", - "log", - "netlink-packet-core", - "netlink-packet-generic", - "netlink-packet-route", - "netlink-packet-utils", - "netlink-packet-wireguard", - "netlink-sys", - "nix", - "thiserror", - "x25519-dalek", -] - [[package]] name = "x25519-dalek" version = "2.0.0" diff --git a/Cargo.toml b/Cargo.toml index 7a47d5a..8b5ecea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,14 @@ [package] -name = "wireguard_rs" -version = "0.1.0" +name = "defguard_wireguard_rs" +version = "0.2.0" edition = "2021" - +description = "A unified multi-platform high-level API for managing WireGuard interfaces" +license = "Apache-2.0" +readme = "README.md" +homepage = "https://github.com/DefGuard/wireguard-rs" +repository = "https://github.com/DefGuard/wireguard-rs" +keywords = ["wireguard", "network", "vpn"] +categories = ["network-programming"] [dependencies] base64 = "0.21" diff --git a/README.md b/README.md index 393967d..965c132 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ defguard

-**wireguard-rs** is a Rust library providing a unified high-level API for managing WireGuard interfaces using native OS kernel and userspace WireGuard protocol implementations. +**defguard_wireguard_rs** is a multi-platform Rust library providing a unified high-level API for managing WireGuard interfaces using native OS kernel and userspace WireGuard protocol implementations. It can be used to create your own [WireGuard:tm:](https://www.wireguard.com/) VPN servers or clients for secure and private networking. It was developed as part of [defguard](https://github.com/defguard/defguard) security platform and used in the [gateway/server](https://github.com/defguard/gateway) as well as [desktop client](https://github.com/defguard/client). @@ -18,6 +18,11 @@ It was developed as part of [defguard](https://github.com/defguard/defguard) sec - macOS - FreeBSD +### Note on `wireguard-go` +If you intend to use the userspace WireGuard implementation you should note that currently the library assumes +that the `wireguard-go` binary will be available at runtime. There are some sanity checks when instantiating the API, +but installing it is outside the scope of this project. + ## Examples * Client: https://github.com/DefGuard/wireguard-rs/blob/main/examples/client.rs diff --git a/docs/header.png b/docs/header.png index 6e4d1b9..0a1b940 100644 Binary files a/docs/header.png and b/docs/header.png differ diff --git a/examples/client.rs b/examples/client.rs index b00c0bf..a6cf5bd 100644 --- a/examples/client.rs +++ b/examples/client.rs @@ -1,7 +1,7 @@ use std::{net::SocketAddr, str::FromStr}; -use wireguard_rs::{ - wgapi::WGApi, InterfaceConfiguration, IpAddrMask, Key, Peer, WireguardInterfaceApi, +use defguard_wireguard_rs::{ + host::Peer, key::Key, net::IpAddrMask, InterfaceConfiguration, WGApi, WireguardInterfaceApi, }; use x25519_dalek::{EphemeralSecret, PublicKey}; diff --git a/examples/server.rs b/examples/server.rs index c1e8e1b..e599d8a 100644 --- a/examples/server.rs +++ b/examples/server.rs @@ -1,7 +1,7 @@ use std::str::FromStr; -use wireguard_rs::{ - wgapi::WGApi, InterfaceConfiguration, IpAddrMask, Key, Peer, WireguardInterfaceApi, +use defguard_wireguard_rs::{ + host::Peer, key::Key, net::IpAddrMask, InterfaceConfiguration, WGApi, WireguardInterfaceApi, }; use x25519_dalek::{EphemeralSecret, PublicKey}; diff --git a/examples/userspace.rs b/examples/userspace.rs index 91fce48..5844c7a 100644 --- a/examples/userspace.rs +++ b/examples/userspace.rs @@ -1,11 +1,12 @@ +use defguard_wireguard_rs::{ + host::Peer, key::Key, net::IpAddrMask, InterfaceConfiguration, WireguardApiUserspace, + WireguardInterfaceApi, +}; use std::{ io::{stdin, stdout, Read, Write}, net::SocketAddr, str::FromStr, }; -use wireguard_rs::{ - InterfaceConfiguration, IpAddrMask, Key, Peer, WireguardApiUserspace, WireguardInterfaceApi, -}; use x25519_dalek::{EphemeralSecret, PublicKey}; fn pause() { diff --git a/src/bsd/nvlist.rs b/src/bsd/nvlist.rs index e81f897..a5de851 100644 --- a/src/bsd/nvlist.rs +++ b/src/bsd/nvlist.rs @@ -1,5 +1,5 @@ -// https://github.com/freebsd/freebsd-src/tree/main/sys/contrib/libnv -// https://github.com/freebsd/freebsd-src/blob/main/sys/sys/nv.h +//! `nvlist` implementation in Rust. + use std::{error::Error, ffi::CStr, fmt}; /// `NV_HEADER_SIZE` is for both: `nvlist_header` and `nvpair_header`. diff --git a/src/error.rs b/src/error.rs index 265f599..d575d4f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,5 @@ +//! Interface management errors + use thiserror::Error; #[derive(Debug, Error)] diff --git a/src/host.rs b/src/host.rs index dce4f1a..b59bfb2 100644 --- a/src/host.rs +++ b/src/host.rs @@ -1,3 +1,5 @@ +//! Host interface configuration + use std::{ collections::HashMap, io::{self, BufRead, BufReader, Read}, @@ -14,6 +16,7 @@ use netlink_packet_wireguard::{ use crate::{key::Key, net::IpAddrMask}; +/// WireGuard peer representation. #[derive(Debug, Default, PartialEq, Clone)] pub struct Peer { pub public_key: Key, @@ -28,6 +31,7 @@ pub struct Peer { } impl Peer { + /// Create new `Peer` with a given `public_key`. #[must_use] pub fn new(public_key: Key) -> Self { Self { @@ -153,6 +157,7 @@ impl Peer { } } +/// WireGuard host representation. #[derive(Debug, Default)] pub struct Host { pub listen_port: u16, @@ -162,6 +167,7 @@ pub struct Host { } impl Host { + /// Create new `Host` with a given `listen_port` and `private_key`. #[must_use] pub fn new(listen_port: u16, private_key: Key) -> Self { Self { diff --git a/src/key.rs b/src/key.rs index 50e0aeb..f9cb696 100644 --- a/src/key.rs +++ b/src/key.rs @@ -1,3 +1,5 @@ +//! Public key utilities + use std::{ error, fmt, hash::{Hash, Hasher}, @@ -8,6 +10,7 @@ use base64::{prelude::BASE64_STANDARD, DecodeError, Engine}; const KEY_LENGTH: usize = 32; +/// WireGuard key representation in binary form. #[derive(Clone, Default)] pub struct Key([u8; KEY_LENGTH]); @@ -31,6 +34,7 @@ impl fmt::Display for KeyError { } impl Key { + /// Create a new key from buffer. #[must_use] pub fn new(buf: [u8; KEY_LENGTH]) -> Self { Self(buf) @@ -46,6 +50,7 @@ impl Key { self.0.as_slice() } + /// Converts `Key` to `String` of lower case hexadecimal digits. #[must_use] pub fn to_lower_hex(&self) -> String { let mut hex = String::with_capacity(64); @@ -62,6 +67,11 @@ impl Key { hex } + /// Converts a text string of hexadecimal digits to `Key`. + /// + /// # Errors + /// Will return `KeyError` if text string has wrong length, + /// or contains an invalid character. pub fn decode>(hex: T) -> Result { let hex = hex.as_ref(); let length = hex.len(); diff --git a/src/lib.rs b/src/lib.rs index 686b86f..70b46cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,54 @@ +//! # `defguard_wireguard_rs` +//! +//! `defguard_wireguard_rs` is a multi-platform Rust library providing a unified high-level API +//! for managing WireGuard interfaces using native OS kernel and userspace WireGuard protocol implementations. +//! +//! It can be used to create your own [WireGuard:tm:](https://www.wireguard.com/) VPN servers or clients for secure and private networking. +//! +//! It was developed as part of [defguard](https://github.com/defguard/defguard) security platform and used in the [gateway/server](https://github.com/defguard/gateway) as well as [desktop client](https://github.com/defguard/client). +//! +//! ## Example +//! +//! ```no_run +//! use x25519_dalek::{EphemeralSecret, PublicKey}; +//! use defguard_wireguard_rs::{InterfaceConfiguration, WGApi, WireguardInterfaceApi, host::Peer}; +//! # use defguard_wireguard_rs::error::WireguardInterfaceError; +//! +//! // Create new API struct for interface +//! let ifname: String = if cfg!(target_os = "linux") || cfg!(target_os = "freebsd") { +//! "wg0".into() +//! } else { +//! "utun3".into() +//! }; +//! let wgapi = WGApi::new(ifname.clone(), false)?; +//! +//! // Create host interfaces +//! wgapi.create_interface()?; +//! +//! // Configure host interface +//! let interface_config = InterfaceConfiguration { +//! name: ifname.clone(), +//! prvkey: "AAECAwQFBgcICQoLDA0OD/Dh0sO0pZaHeGlaSzwtHg8=".to_string(), +//! address: "10.6.0.30".to_string(), +//! port: 12345, +//! peers: vec![], +//! }; +//! wgapi.configure_interface(&interface_config)?; +//! +//! // Create, add & remove peers +//! for _ in 0..32 { +//! let secret = EphemeralSecret::random(); +//! let key = PublicKey::from(&secret); +//! let peer = Peer::new(key.as_ref().try_into().unwrap()); +//! wgapi.configure_peer(&peer)?; +//! wgapi.remove_peer(&peer.public_key)?; +//! } +//! +//! // Remove host interface +//! wgapi.remove_interface()?; +//! # Ok::<(), WireguardInterfaceError>(()) +//! ``` + #[cfg(target_os = "freebsd")] pub mod bsd; pub mod error; @@ -6,7 +57,7 @@ pub mod key; pub mod net; #[cfg(target_os = "linux")] pub mod netlink; -pub mod wgapi; +mod wgapi; #[cfg(target_os = "freebsd")] mod wgapi_freebsd; @@ -21,23 +72,24 @@ extern crate log; use std::process::Output; -// public reexports +use self::{ + error::WireguardInterfaceError, + host::{Host, Peer}, + key::Key, + net::IpAddrMask, +}; + +// public re-exports +pub use wgapi::WGApi; #[cfg(target_os = "freebsd")] pub use wgapi_freebsd::WireguardApiFreebsd; #[cfg(target_os = "linux")] pub use wgapi_linux::WireguardApiLinux; #[cfg(target_family = "unix")] pub use wgapi_userspace::WireguardApiUserspace; -pub use { - self::error::WireguardInterfaceError, - host::{Host, Peer}, - key::Key, - net::{IpAddrMask, IpAddrParseError}, - wgapi::WGApi, - wireguard_interface::WireguardInterfaceApi, -}; +pub use wireguard_interface::WireguardInterfaceApi; -/// Wireguard Interface configuration +/// Host WireGuard interface configuration #[derive(Debug, Clone)] pub struct InterfaceConfiguration { pub name: String, @@ -63,8 +115,8 @@ impl TryFrom<&InterfaceConfiguration> for Host { } } -/// Util function which checks external command output status. -pub fn check_command_output_status(output: Output) -> Result<(), WireguardInterfaceError> { +/// Utility function which checks external command output status. +fn check_command_output_status(output: Output) -> Result<(), WireguardInterfaceError> { if !output.status.success() { let stdout = String::from_utf8(output.stdout).expect("Invalid UTF8 sequence in stdout"); let stderr = String::from_utf8(output.stderr).expect("Invalid UTF8 sequence in stderr"); diff --git a/src/net.rs b/src/net.rs index b96319e..8f1bef2 100644 --- a/src/net.rs +++ b/src/net.rs @@ -1,3 +1,5 @@ +//! Network address utilities + use std::{error, fmt, net::IpAddr, str::FromStr}; #[cfg(target_os = "linux")] @@ -6,6 +8,7 @@ use netlink_packet_wireguard::{ nlas::{WgAllowedIp, WgAllowedIpAttrs}, }; +/// IP address with CIDR. #[derive(Debug, PartialEq, Clone)] pub struct IpAddrMask { // IP v4 or v6 diff --git a/src/netlink.rs b/src/netlink.rs index 8947a35..20c6670 100644 --- a/src/netlink.rs +++ b/src/netlink.rs @@ -1,3 +1,5 @@ +//! Netlink utilities for controlling network interfaces on Linux + use std::{ fmt::Debug, io::ErrorKind, @@ -68,6 +70,7 @@ impl From for WireguardInterfaceError { } } +/// Wrapper `Result` type for Netlink operations pub type NetlinkResult = Result; macro_rules! get_nla_value { @@ -92,7 +95,7 @@ impl Key { } } -pub fn netlink_request_genl( +fn netlink_request_genl( mut message: GenlMessage, flags: u16, ) -> NetlinkResult>>> @@ -126,7 +129,7 @@ where netlink_request(message, flags, NETLINK_GENERIC) } -pub fn netlink_request( +fn netlink_request( message: I, flags: u16, socket: isize, @@ -280,6 +283,7 @@ fn set_address(ifindex: u32, address: &IpAddrMask) -> NetlinkResult<()> { Ok(()) } +/// Set IP address of a WireGuard network interface pub fn address_interface(ifname: &str, address: &IpAddrMask) -> NetlinkResult<()> { let mut message = LinkMessage::default(); message.nlas.push(Nla::IfName(ifname.into())); @@ -314,7 +318,7 @@ pub fn address_interface(ifname: &str, address: &IpAddrMask) -> NetlinkResult<() Ok(()) } -/// Delete WireGuard interface. +/// Delete WireGuard interface pub fn delete_interface(ifname: &str) -> NetlinkResult<()> { let mut message = LinkMessage::default(); message.nlas.push(Nla::IfName(ifname.into())); @@ -334,6 +338,7 @@ pub fn delete_interface(ifname: &str) -> NetlinkResult<()> { Ok(()) } +/// Read host interface data pub fn get_host(ifname: &str) -> NetlinkResult { debug!("Reading Netlink data for interface {ifname}"); let genlmsg = GenlMessage::from_payload(Wireguard { @@ -358,6 +363,7 @@ pub fn get_host(ifname: &str) -> NetlinkResult { Ok(host) } +/// Perform interface configuration pub fn set_host(ifname: &str, host: &Host) -> NetlinkResult<()> { let genlmsg = GenlMessage::from_payload(Wireguard { cmd: WireguardCmd::SetDevice, @@ -367,6 +373,7 @@ pub fn set_host(ifname: &str, host: &Host) -> NetlinkResult<()> { Ok(()) } +/// Save or update WireGuard peer configuration pub fn set_peer(ifname: &str, peer: &Peer) -> NetlinkResult<()> { let genlmsg = GenlMessage::from_payload(Wireguard { cmd: WireguardCmd::SetDevice, @@ -376,6 +383,7 @@ pub fn set_peer(ifname: &str, peer: &Peer) -> NetlinkResult<()> { Ok(()) } +/// Delete a WireGuard peer from interface pub fn delete_peer(ifname: &str, public_key: &Key) -> NetlinkResult<()> { let genlmsg = GenlMessage::from_payload(Wireguard { cmd: WireguardCmd::SetDevice, diff --git a/src/wgapi.rs b/src/wgapi.rs index 31e1296..f3e3474 100644 --- a/src/wgapi.rs +++ b/src/wgapi.rs @@ -1,3 +1,5 @@ +//! Shared multi-platform management API abstraction + #[cfg(target_os = "freebsd")] use crate::WireguardApiFreebsd; #[cfg(target_os = "linux")] @@ -9,9 +11,17 @@ use crate::{ WireguardInterfaceError, }; +/// Shared multi-platform WireGuard management API +/// +/// This struct adds an additional level of abstraction and can be used +/// to detect the correct API implementation for most common platforms. pub struct WGApi(Box); impl WGApi { + /// Create new instance of `WGApi`. + /// + /// # Errors + /// Will return `WireguardInterfaceError` is platform is not supported. pub fn new(ifname: String, userspace: bool) -> Result { if userspace { if cfg!(target_family = "unix") { diff --git a/src/wgapi_userspace.rs b/src/wgapi_userspace.rs index c5a0eca..f9ea601 100644 --- a/src/wgapi_userspace.rs +++ b/src/wgapi_userspace.rs @@ -23,6 +23,10 @@ pub struct WireguardApiUserspace { } impl WireguardApiUserspace { + /// Create new instance of `WireguardApiUserspace`. + /// + /// # Errors + /// Will return `WireguardInterfaceError` if `wireguard-go` can't be found. pub fn new(ifname: String) -> Result { // check that `wireguard-go` is available Command::new(USERSPACE_EXECUTABLE).arg("--version").output().map_err(|err| { @@ -37,6 +41,7 @@ impl WireguardApiUserspace { format!("/var/run/wireguard/{}.sock", self.ifname) } + /// Create UNIX socket to communicate with `wireguard-go`. fn socket(&self) -> io::Result { let path = self.socket_path(); let socket = UnixStream::connect(path)?; @@ -64,6 +69,7 @@ impl WireguardApiUserspace { 0 } + /// Read host information using user-space API. pub fn read_host(&self) -> io::Result { debug!("Reading host interface info"); let mut socket = self.socket()?; @@ -71,19 +77,20 @@ impl WireguardApiUserspace { Host::parse_uapi(socket) } + /// Write host information using user-space API. pub fn write_host(&self, host: &Host) -> io::Result<()> { 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 { + if Self::parse_errno(socket) == 0 { + Ok(()) + } else { Err(io::Error::new( io::ErrorKind::InvalidData, "write configuration error", )) - } else { - Ok(()) } } } @@ -153,10 +160,10 @@ impl WireguardInterfaceApi for WireguardApiUserspace { socket.write_all(peer.as_uapi_update().as_bytes())?; socket.write_all(b"\n")?; - if Self::parse_errno(socket) != 0 { - Err(WireguardInterfaceError::PeerConfigurationError) - } else { + if Self::parse_errno(socket) == 0 { Ok(()) + } else { + Err(WireguardInterfaceError::PeerConfigurationError) } } @@ -172,10 +179,10 @@ impl WireguardInterfaceApi for WireguardApiUserspace { )?; socket.write_all(b"\n")?; - if Self::parse_errno(socket) != 0 { - Err(WireguardInterfaceError::PeerConfigurationError) - } else { + if Self::parse_errno(socket) == 0 { Ok(()) + } else { + Err(WireguardInterfaceError::PeerConfigurationError) } }