//! # 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 generated::GoString; use std::error::Error; use std::ffi::{c_char, CString}; use std::fmt::{Display, Formatter}; use std::path::Path; 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> { 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::(), 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> { 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> { 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> { 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> { 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> { 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(), } } } }