add documentation for the rp crate

This commit is contained in:
David Niehues
2024-12-11 11:23:31 +01:00
parent d4350195eb
commit cca02dc8d1
3 changed files with 90 additions and 4 deletions

View File

@@ -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<Command>,
}
/// 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<T>(note: &str, command: Option<CommandType>) -> Result<T, String> {
match command {
Some(command) => match command {
@@ -44,6 +65,9 @@ fn fatal<T>(note: &str, command: Option<CommandType>) -> Result<T, String> {
}
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<impl Iterator<Item = String>>) -> Result<Self, String> {
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<impl Iterator<Item = String>>) -> Result<Self, String> {
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<impl Iterator<Item = String>>) -> Result<Self, String> {
let mut cli = Cli::default();

View File

@@ -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<SocketAddr>,
/// For how long to keep the connection alive
pub persistent_keepalive: Option<u32>,
/// The IPs that are allowed for this peer.
pub allowed_ips: Option<String>,
}
/// 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<String>,
/// The IP-address rosenpass should run under
pub ip: Option<String>,
/// The IP-address and port that the rosenpass [AppServer](rosenpass::app_server::AppServer) should use.
pub listen: Option<SocketAddr>,
/// Other peers a connection should be initialized to
pub peers: Vec<ExchangePeer>,
}
@@ -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<u32> {
// add the link
// add the link, equivalent to `ip link add <link_name> 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 <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<Box<dyn Future<Output = Result<(), Error>> + 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<Vec<()>, 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 <ip> dev <dev>` and enqueue
// `ip address del <ip> dev <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 <allowed_ips> dev <dev>` and set up
// the cleanup as `ip route del <allowed_ips>`.
if let Some(allowed_ips) = peer.allowed_ips {
Command::new("ip")
.arg("route")

View File

@@ -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::<WG_B64_LEN, _>(public_wgpk)?;
wgpk.zeroize();
// copy the pq-public key to the public directory
fs::copy(private_pqpk, public_pqpk)?;
Ok(())