runnel-rs 0.2.3

A Rust proxy and tunnel toolbox with WireGuard-style, TUN, SOCKS, and TLS-based transports.
Documentation
use anyhow::{Context, Result};
use clap::Args;
use std::{
    fs,
    io::Write,
    path::{Path, PathBuf},
};

#[derive(Clone, Debug, Args)]
pub struct CertArgs {
    #[arg(long, default_value = "server.crt")]
    pub cert: PathBuf,
    #[arg(long, default_value = "server.key")]
    pub key: PathBuf,
    #[arg(long = "name", num_args = 1.., value_delimiter = ',')]
    pub names: Vec<String>,
}

pub fn run(args: CertArgs) -> Result<()> {
    if args.names.is_empty() {
        anyhow::bail!("certificate names are required; pass --name or set them in --config");
    }
    let certified = rcgen::generate_simple_self_signed(args.names.clone())
        .context("failed to generate self-signed certificate")?;

    fs::write(&args.cert, certified.cert.pem())
        .with_context(|| format!("failed to write {}", args.cert.display()))?;
    write_private_key_file(&args.key, &certified.key_pair.serialize_pem())
        .with_context(|| format!("failed to write {}", args.key.display()))?;

    println!(
        "generated certificate at {} and key at {}",
        args.cert.display(),
        args.key.display()
    );
    Ok(())
}

fn write_private_key_file(path: &Path, contents: &str) -> Result<()> {
    #[cfg(unix)]
    {
        use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};

        let mut options = fs::OpenOptions::new();
        options
            .write(true)
            .create(true)
            .truncate(true)
            .mode(0o600)
            .custom_flags(libc::O_NOFOLLOW);
        let mut file = options.open(path)?;
        file.set_permissions(fs::Permissions::from_mode(0o600))?;
        file.write_all(contents.as_bytes())?;
        file.flush()?;
        Ok(())
    }

    #[cfg(not(unix))]
    {
        fs::write(path, contents)?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[cfg(unix)]
    #[test]
    fn private_key_file_is_written_with_owner_only_permissions() {
        use std::os::unix::fs::PermissionsExt;

        let path = std::env::temp_dir().join(format!(
            "runnel-key-perms-{}-{}.key",
            std::process::id(),
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        ));

        write_private_key_file(&path, "secret\n").expect("key should be written");
        let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
        let contents = fs::read_to_string(&path).unwrap();
        let _ = fs::remove_file(&path);

        assert_eq!(mode, 0o600);
        assert_eq!(contents, "secret\n");
    }
}