diff --git a/rp/src/cli.rs b/rp/src/cli.rs index f7371a6..26742e9 100644 --- a/rp/src/cli.rs +++ b/rp/src/cli.rs @@ -3,6 +3,11 @@ use std::{iter::Peekable, net::SocketAddr}; use crate::exchange::{ExchangeOptions, ExchangePeer}; +/// The different commands supported by the `rp` binary. +/// [GenKey](crate::cli::Command::GenKey), [PubKey](crate::cli::Command::PubKey), +/// [Exchange](crate::cli::Command::Exchange) and +/// [ExchangeConfig](crate::cli::Command::ExchangeConfig) +/// contain information specific to the respective command. pub enum Command { GenKey { private_keys_dir: PathBuf, @@ -18,6 +23,10 @@ pub enum Command { Help, } +/// The different command types supported by the `rp` binary. +/// This enum is exclusively used in [fatal] and when calling [fatal] and is therefore +/// limited to the command types that can fail. E.g., the help command can not fail and is therefore +/// not part of the [CommandType]-enum. enum CommandType { GenKey, PubKey, @@ -25,12 +34,24 @@ enum CommandType { ExchangeConfig, } +/// This structure captures the result of parsing the arguments to the `rp` binary. +/// A new [Cli] is created by calling [Cli::parse] with the appropriate arguments. #[derive(Default)] pub struct Cli { + /// whether the output should be verbose. pub verbose: bool, + /// the command specified by the given arguments. pub command: Option, } +/// Processes a fatal error when parsing cli arguments. +/// It *always* returns an [Err(String)], where such that the contained [String] explains +/// the parsing error, including the provided `note`. +/// +/// # Generic Parameters +/// the generic parameter `T` is given to make the [Result]-type compatible with the respective +/// return type of the calling function. +/// fn fatal(note: &str, command: Option) -> Result { match command { Some(command) => match command { @@ -44,6 +65,9 @@ fn fatal(note: &str, command: Option) -> Result { } impl ExchangePeer { + /// Parses peer parameters given to the `rp` binary in the context of an `exchange` operation. + /// It returns a result with either [ExchangePeer] that contains the parameters of the peer + /// or an error describing why the arguments could not be parsed. pub fn parse(args: &mut &mut Peekable>) -> Result { let mut peer = ExchangePeer::default(); @@ -126,6 +150,9 @@ impl ExchangePeer { } impl ExchangeOptions { + /// Parses the arguments given to the `rp` binary *if the `exchange` operation is given*. + /// It returns a result with either [ExchangeOptions] that contains the result of parsing the + /// arguments or an error describing why the arguments could not be parsed. pub fn parse(mut args: &mut Peekable>) -> Result { let mut options = ExchangeOptions::default(); @@ -191,6 +218,9 @@ impl ExchangeOptions { } impl Cli { + /// Parses the arguments given to the `rp` binary. It returns a result with either + /// a [Cli] that contains the result of parsing the arguments or an error describing + /// why the arguments could not be parsed. pub fn parse(mut args: Peekable>) -> Result { let mut cli = Cli::default(); diff --git a/rp/src/exchange.rs b/rp/src/exchange.rs index 757ed66..28e985d 100644 --- a/rp/src/exchange.rs +++ b/rp/src/exchange.rs @@ -11,21 +11,35 @@ use anyhow::Result; #[cfg(any(target_os = "linux", target_os = "freebsd"))] use crate::key::WG_B64_LEN; +/// Used to define a peer for the rosenpass connection that consists of +/// a directory for storing public keys and optionally an IP address and port of the endpoint, +/// for how long the connection should be kept alive and a list of allowed IPs for the peer. #[derive(Default, Deserialize)] pub struct ExchangePeer { + /// Directory where public keys are stored pub public_keys_dir: PathBuf, + /// The IP address of the endpoint pub endpoint: Option, + /// For how long to keep the connection alive pub persistent_keepalive: Option, + /// The IPs that are allowed for this peer. pub allowed_ips: Option, } +/// Options for the exchange operation of the `rp` binary. #[derive(Default, Deserialize)] pub struct ExchangeOptions { + /// Whether the cli output should be verbose. pub verbose: bool, + /// path to the directory where private keys are stored. pub private_keys_dir: PathBuf, + /// The link rosenpass should run as. If None is given [exchange] will use `"rosenpass0"` instead. pub dev: Option, + /// The IP-address rosenpass should run under pub ip: Option, + /// The IP-address and port that the rosenpass [AppServer](rosenpass::app_server::AppServer) should use. pub listen: Option, + /// Other peers a connection should be initialized to pub peers: Vec, } @@ -48,8 +62,11 @@ mod netlink { use netlink_packet_wireguard::nlas::WgDeviceAttrs; use rtnetlink::Handle; + /// Creates a netlink named `link_name` and changes the state to up. It returns the index + /// of the interface in the list of interfaces as the result or an error if any of the + ///operations of creating the link or changing its state to up fails. pub async fn link_create_and_up(rtnetlink: &Handle, link_name: String) -> Result { - // add the link + // add the link, equivalent to `ip link add type wireguard` rtnetlink .link() .add() @@ -57,7 +74,8 @@ mod netlink { .execute() .await?; - // retrieve the link to be able to up it + // retrieve the link to be able to up it, equivalent to `ip link show` and then + // using the link shown that is identified by `link_name`. let link = rtnetlink .link() .get() @@ -69,7 +87,7 @@ mod netlink { .0 .unwrap()?; - // up the link + // up the link, equivalent to `ip link set dev up` rtnetlink .link() .set(link.header.index) @@ -80,12 +98,16 @@ mod netlink { Ok(link.header.index) } + /// Deletes a link using rtnetlink. The link is specified using its index in the list of links. pub async fn link_cleanup(rtnetlink: &Handle, index: u32) -> Result<()> { rtnetlink.link().del(index).execute().await?; Ok(()) } + /// Deletes a link using rtnetlink. The link is specified using its index in the list of links. + /// In contrast to [link_cleanup], this function create a new socket connection to netlink and + /// *ignores errors* that occur during deletion. pub async fn link_cleanup_standalone(index: u32) -> Result<()> { let (connection, rtnetlink, _) = rtnetlink::new_connection()?; tokio::spawn(connection); @@ -138,6 +160,9 @@ mod netlink { } } +/// A wrapper for a list of cleanup handlers that can be used in an asynchronous context +/// to clean up after the usage of rosenpass or if the `rp` binary is interrupted with ctrl+c +/// or a `SIGINT` signal in general. #[derive(Clone)] #[cfg(any(target_os = "linux", target_os = "freebsd"))] struct CleanupHandlers( @@ -146,19 +171,27 @@ struct CleanupHandlers( #[cfg(any(target_os = "linux", target_os = "freebsd"))] impl CleanupHandlers { + /// Creates a new list of [CleanupHandlers]. fn new() -> Self { CleanupHandlers(Arc::new(::futures::lock::Mutex::new(vec![]))) } + /// Enqueues a new cleanup handler in the form of a [Future]. async fn enqueue(&self, handler: Pin> + Send>>) { self.0.lock().await.push(Box::pin(handler)) } + /// Runs all cleanup handlers. Following the documentation of [futures::future::try_join_all]: + /// If any cleanup handler returns an error then all other cleanup handlers will be canceled and + /// an error will be returned immediately. If all cleanup handlers complete successfully, + /// however, then the returned future will succeed with a Vec of all the successful results. async fn run(self) -> Result, Error> { futures::future::try_join_all(self.0.lock().await.deref_mut()).await } } +/// Sets up the rosenpass link and wireguard and configures both with the configuration specified by +/// `options`. #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub async fn exchange(options: ExchangeOptions) -> Result<()> { use std::fs; @@ -182,6 +215,8 @@ pub async fn exchange(options: ExchangeOptions) -> Result<()> { let link_name = options.dev.clone().unwrap_or("rosenpass0".to_string()); let link_index = netlink::link_create_and_up(&rtnetlink, link_name.clone()).await?; + // set up a list of (initiallc empty) cleanup handlers that are to be run if + // ctrl-c is hit or generally a `SIGINT` signal is received and always in the end. let cleanup_handlers = CleanupHandlers::new(); let final_cleanup_handlers = (&cleanup_handlers).clone(); @@ -198,6 +233,8 @@ pub async fn exchange(options: ExchangeOptions) -> Result<()> { .expect("Failed to clean up"); })?; + // run `ip address add dev ` and enqueue + // `ip address del dev ` as a cleanup if let Some(ip) = options.ip { let dev = options.dev.clone().unwrap_or("rosenpass0".to_string()); Command::new("ip") @@ -244,6 +281,7 @@ pub async fn exchange(options: ExchangeOptions) -> Result<()> { netlink::wg_set(&mut genetlink, link_index, attr).await?; + // set up the rosenpass AppServer let pqsk = options.private_keys_dir.join("pqsk"); let pqpk = options.private_keys_dir.join("pqpk"); @@ -271,6 +309,7 @@ pub async fn exchange(options: ExchangeOptions) -> Result<()> { anyhow::Error::msg(format!("NativeUnixBrokerConfigBaseBuilderError: {:?}", e)) } + // configure everything per peer for peer in options.peers { let wgpk = peer.public_keys_dir.join("wgpk"); let pqpk = peer.public_keys_dir.join("pqpk"); @@ -318,7 +357,8 @@ pub async fn exchange(options: ExchangeOptions) -> Result<()> { peer.endpoint.map(|x| x.to_string()), )?; - // Configure routes + // Configure routes, equivalent to `ip route replace dev ` and set up + // the cleanup as `ip route del `. if let Some(allowed_ips) = peer.allowed_ips { Command::new("ip") .arg("route") diff --git a/rp/src/key.rs b/rp/src/key.rs index 0ffdd42..4f98bb8 100644 --- a/rp/src/key.rs +++ b/rp/src/key.rs @@ -14,6 +14,7 @@ use rosenpass_cipher_traits::Kem; use rosenpass_ciphers::kem::StaticKem; use rosenpass_secret_memory::{file::StoreSecret as _, Public, Secret}; +/// The length of wireguard keys as a length in base 64 encoding. pub const WG_B64_LEN: usize = 32 * 5 / 3; #[cfg(not(target_family = "unix"))] @@ -24,6 +25,14 @@ pub fn genkey(_: &Path) -> Result<()> { )) } +/// Generates a new symmetric keys for wireguard and asymmetric keys for rosenpass +/// in the provided `private_keys_dir`. +/// +/// It checks whether the directory `private_keys_dir` points to exists and creates it otherwise. +/// If it exists, it ensures that the permission is set to 0700 and aborts otherwise. If the +/// directory is newly created, the appropriate permissions are set. +/// +/// Already existing keys are not overwritten. #[cfg(target_family = "unix")] pub fn genkey(private_keys_dir: &Path) -> Result<()> { if private_keys_dir.exists() { @@ -70,6 +79,11 @@ pub fn genkey(private_keys_dir: &Path) -> Result<()> { Ok(()) } +/// Creates a new directory under `public_keys_dir` and stores the public keys for rosenpass and for +/// wireguard that correspond to the private keys in `private_keys_dir` in `public_keys_dir`. +/// +/// If `public_keys_dir` already exists, the wireguard private key or the rosenpass public key +/// are not present in `private_keys_dir`, an error is returned. pub fn pubkey(private_keys_dir: &Path, public_keys_dir: &Path) -> Result<()> { if public_keys_dir.exists() { return Err(anyhow!("Directory {:?} already exists", public_keys_dir)); @@ -90,9 +104,11 @@ pub fn pubkey(private_keys_dir: &Path, public_keys_dir: &Path) -> Result<()> { Public::from_slice(public.as_bytes()) }; + // store the wireguard public key wgpk.store_b64::(public_wgpk)?; wgpk.zeroize(); + // copy the pq-public key to the public directory fs::copy(private_pqpk, public_pqpk)?; Ok(())