233 lines
7.7 KiB
Rust
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() }
|
|
}
|
|
}
|
|
} |