trifid/nebula-ffi/src/lib.rs

233 lines
7.7 KiB
Rust

//! # nebula-ffi
//! nebula-ffi is a crate for interacting with the Nebula project via a `CGo` compatability layer.
//! It provides support for running a Nebula VPN directly from a Rust binary, liken to how the default
//! `nebula` binary functions.
//! ## Versioning
//! `nebula-ffi` is automatically updated for every single release or commit made on the slackhq/nebula repository.
//! To build against a **specific release:**
//! ```toml
//! [dependencies]
//! nebula-ffi = { version = "1.7.2" } # for Nebula 1.7.2
//! ```
//! To build against a **specific commit:**
//! ```toml
//! [dependencies]
//! nebula-ffi = { version = "1.7.2+83b6dc7" } # for commit 83b6dc7, which happened *after* the 1.7.2 release
//! ```
//! This versioning is an artifact of the build process for nebula-ffi and how Cargo versioning works.
#![warn(clippy::pedantic)]
#![warn(clippy::nursery)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::expect_used)]
#![deny(missing_docs)]
#![deny(clippy::missing_errors_doc)]
#![deny(clippy::missing_panics_doc)]
#![deny(clippy::missing_safety_doc)]
#[allow(non_upper_case_globals)]
#[allow(non_camel_case_types)]
#[allow(non_snake_case)]
#[allow(missing_docs)]
#[allow(unused)]
#[allow(clippy::derive_partial_eq_without_eq)]
pub mod generated {
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
}
use std::error::Error;
use std::ffi::{c_char, CString};
use std::fmt::{Display, Formatter};
use std::path::{Path};
use generated::GoString;
impl From<&str> for GoString {
#[allow(clippy::cast_possible_wrap)]
#[allow(clippy::expect_used)]
fn from(value: &str) -> Self {
let c_str = CString::new(value).expect("CString::new() failed");
let ptr = c_str.as_ptr();
let go_string = GoString {
p: ptr,
n: c_str.as_bytes().len() as isize
};
go_string
}
}
#[allow(clippy::expect_used)]
fn cstring_to_string(cstr_ptr: *mut c_char) -> String {
let cstr = unsafe { CString::from_raw(cstr_ptr) };
cstr.to_string_lossy().to_string()
}
/// An instance of a Nebula Control and Config object. This struct does not actually store any information - it is stored in globals in the Go wrapper, however this struct is used to provide typesafety.
pub struct NebulaInstance {}
impl NebulaInstance {
/// Creates a new `NebulaInstance` by calling `NebulaSetup` with the given config path and configTest parameters.
/// # Errors
/// This function will return errors in many, many circumstances. It is not fully documented the scope of errors that may occur.
/// # Panics
/// This function will panic if memory is corrupted while communicating with Go.
pub fn new(config_path: &Path, config_test: bool) -> Result<Self, Box<dyn Error>> {
let mut config_path_bytes = unsafe { config_path.display().to_string().as_bytes_mut().to_vec() };
config_path_bytes.push(0u8);
let config_test_u8 = u8::from(config_test);
let res;
unsafe {
res = generated::NebulaSetup(config_path_bytes.as_mut_ptr().cast::<c_char>(), config_test_u8);
}
let res = cstring_to_string(res);
if res == "NO_FAILURE" {
Ok(Self {})
} else {
Err(NebulaError::from_string(&res).into())
}
}
/// Reloads the Nebula configuration file. Not all configuration values can be reloaded, some may require a full stop and re-setup of Nebula to be applied.
/// # Errors
/// This function will return an error if an error is returned by the CGOFFI layer.
pub fn reload_config(&self) -> Result<(), Box<dyn Error>> {
let res;
unsafe {
res = generated::NebulaReloadConfig();
}
let res = cstring_to_string(res);
if res == "NO_FAILURE" {
Ok(())
} else {
Err(NebulaError::from_string(&res).into())
}
}
/// Starts the Nebula packet listener server if it is not already running.
/// # Errors
/// This function will return an error if an error is returned by the CGOFFI layer.
pub fn start(&self) -> Result<(), Box<dyn Error>> {
let res;
unsafe {
res = generated::NebulaStart();
}
let res = cstring_to_string(res);
if res == "NO_FAILURE" {
Ok(())
} else {
Err(NebulaError::from_string(&res).into())
}
}
/// Signals nebula to shutdown, returns after the shutdown is complete
/// # Errors
/// This function will return an error if an error is returned by the CGOFFI layer.
pub fn stop(&self) -> Result<(), Box<dyn Error>> {
let res;
unsafe {
res = generated::NebulaStop();
}
let res = cstring_to_string(res);
if res == "NO_FAILURE" {
Ok(())
} else {
Err(NebulaError::from_string(&res).into())
}
}
/// This function blocks until a SIGTERM or SIGINT is received, then stops the Nebula packet listener server if it is not already stopped.
/// # Errors
/// This function will return an error if an error is returned by the CGOFFI layer.
pub fn block_and_stop(&self) -> Result<(), Box<dyn Error>> {
let res;
unsafe {
res = generated::NebulaShutdownBlock();
}
let res = cstring_to_string(res);
if res == "NO_FAILURE" {
Ok(())
} else {
Err(NebulaError::from_string(&res).into())
}
}
/// Asks the UDP listener to rebind it's listener. Mainly used on mobile clients when interfaces change
/// # Errors
/// This function will return an error if an error is returned by the CGOFFI layer.
pub fn rebind_udp(&self) -> Result<(), Box<dyn Error>> {
let res;
unsafe {
res = generated::NebulaRebindUDP();
}
let res = cstring_to_string(res);
if res == "NO_FAILURE" {
Ok(())
} else {
Err(NebulaError::from_string(&res).into())
}
}
}
#[derive(Debug)]
/// An enumeration of all known errors that can be returned by Nebula. As errors are passed by string, this can be a little bit janky, but it works most of the time.
pub enum NebulaError {
/// Returned by nebula when the TUN/TAP device already exists
DeviceOrResourceBusy {
/// The complete error string returned by the Nebula wrapper
error_str: String
},
/// An unknown error that the error parser couldn't figure out how to parse.
Unknown {
/// The complete error string returned by the Nebula wrapper
error_str: String
},
/// Occurs if you call a function before NebulaSetup has been called
NebulaNotSetup {
/// The complete error string returned by the Nebula wrapper
error_str: String
}
}
impl Display for NebulaError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::DeviceOrResourceBusy { error_str } => write!(f, "device or resource busy ({error_str})"),
Self::Unknown { error_str } => write!(f, "unknown error ({error_str})"),
Self::NebulaNotSetup { error_str } => write!(f, "attempted to call a function depending on global variable before NebulaSetup ({error_str})")
}
}
}
impl Error for NebulaError {}
impl NebulaError {
/// Converts a string error from the Nebula CGO wrapper into a Rust strongtyped error.
#[must_use]
pub fn from_string(string: &str) -> Self {
if string.starts_with("device or resource busy") {
Self::DeviceOrResourceBusy { error_str: string.to_string() }
} else if string.starts_with("NebulaSetup has not yet been called") {
Self::NebulaNotSetup { error_str: string.to_string() }
} else {
Self::Unknown { error_str: string.to_string() }
}
}
}