Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "vopono"
description = "Launch applications via VPN tunnels using temporary network namespaces"
version = "0.10.13"
version = "0.10.14"
authors = ["James McMurray <[email protected]>"]
edition = "2024"
license = "GPL-3.0-or-later"
Expand All @@ -19,14 +19,14 @@ directories-next = "2"
log = "0.4"
pretty_env_logger = "0.5"
clap = { version = "4", features = ["derive"] }
which = "7"
which = "8"
dialoguer = "0.11"
compound_duration = "1"
signal-hook = "0.3"
walkdir = "2"
chrono = "0.4"
bs58 = "0.5"
nix = { version = "0.29", features = ["signal", "process"] }
nix = { version = "0.30", features = ["signal", "process"] }
config = "0.15"
basic_tcp_proxy = "0.3.2"
strum = "0.27"
Expand Down
20 changes: 10 additions & 10 deletions vopono_core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "vopono_core"
description = "Library code for running VPN connections in network namespaces"
version = "0.1.13"
version = "0.1.14"
edition = "2024"
authors = ["James McMurray <[email protected]>"]
license = "GPL-3.0-or-later"
Expand All @@ -14,32 +14,32 @@ keywords = ["vopono", "vpn", "wireguard", "openvpn", "netns"]
anyhow = "1"
directories-next = "2"
log = "0.4"
which = "7"
which = "8"
users = "0.11"
nix = { version = "0.29", features = ["user", "signal", "fs", "process"] }
nix = { version = "0.30", features = ["user", "signal", "fs", "process"] }
serde = { version = "1", features = ["derive", "std"] }
csv = "1"
regex = "1"
ron = "0.9"
ron = "0.10"
walkdir = "2"
# Stuck on rand 0.6 due to x25519-dalek:
# https://github.com/dalek-cryptography/curve25519-dalek/issues/731
# https://github.com/dalek-cryptography/curve25519-dalek/pull/729
rand = "0.6.4"
rand_core = { version = "0.6.4", features = ["getrandom"] }
toml = "0.8"
rand = "0.9"
rand_core = { version = "0.9" }
toml = "0.9"
ipnet = { version = "2", features = ["serde"] }
reqwest = { default-features = false, version = "0.12", features = [
"blocking",
"json",
"rustls-tls",
] } # TODO: Can we remove Tokio dependency?
sysinfo = "0.34"
sysinfo = "0.36"
base64 = "0.22"
x25519-dalek = { version = "2", features = ["static_secrets"] }
x25519-dalek = { version = "3.0.0-pre.0", features = ["static_secrets"] }
strum = "0.27"
strum_macros = "0.27"
zip = "2"
zip = "4"
maplit = "1"
webbrowser = "1"
serde_json = "1"
Expand Down
4 changes: 2 additions & 2 deletions vopono_core/src/config/providers/mozilla/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ impl MozillaVPN {
use rand::RngCore;
use sha2::Digest;
let mut code_verifier_random = [0u8; 32];
let mut os_rng = rand::rngs::OsRng::new().unwrap();
os_rng.fill_bytes(&mut code_verifier_random);
let mut rng = rand::rng();
rng.fill_bytes(&mut code_verifier_random);
let mut code_verifier = [0u8; 43];
BASE64_URL_SAFE_NO_PAD.encode_slice(code_verifier_random, &mut code_verifier)?;
let mut code_challenge = String::with_capacity(43);
Expand Down
6 changes: 3 additions & 3 deletions vopono_core/src/config/providers/mullvad/openvpn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::config::vpn::OpenVpnProtocol;
use crate::util::delete_all_files_in_dir;
use anyhow::Context;
use log::warn;
use rand::seq::SliceRandom;
use rand::prelude::{IndexedRandom, SliceRandom};
use reqwest::blocking::Client;
use serde::Deserialize;
use std::collections::HashMap;
Expand Down Expand Up @@ -152,7 +152,7 @@ impl OpenVpnProvider for Mullvad {
let mut file = File::create(openvpn_dir.join(file_name))?;
writeln!(file, "{}", settings.join("\n"))?;

remote_vec.shuffle(&mut rand::thread_rng());
remote_vec.shuffle(&mut rand::rng());
writeln!(
file,
"{}",
Expand Down Expand Up @@ -211,7 +211,7 @@ impl ConfigType {
fn generate_port(&self) -> u16 {
match self {
Self::DefaultUdp => *[1300, 1301, 1302, 1194, 1195, 1196, 1197]
.choose(&mut rand::thread_rng())
.choose(&mut rand::rng())
.expect("Could not choose default port"),
Self::Udp53 => 53,
Self::Tcp80 => 80,
Expand Down
4 changes: 2 additions & 2 deletions vopono_core/src/network/shadowsocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use super::openvpn::get_remotes_from_config;
use anyhow::Context;
use anyhow::anyhow;
use log::{debug, error};
use rand::seq::SliceRandom;
use rand::prelude::IndexedRandom;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::fs::read_to_string;
Expand Down Expand Up @@ -44,7 +44,7 @@ impl Shadowsocks {
));
}
let route = get_routes_from_config(config_file)?;
let route = route.choose(&mut rand::thread_rng()).unwrap();
let route = route.choose(&mut rand::rng()).unwrap();
let port = get_remotes_from_config(config_file)?[0].port;

let route_str = route.to_string();
Expand Down
122 changes: 61 additions & 61 deletions vopono_core/src/network/trojan/get_cert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,65 +114,65 @@ mod tests {
); // trim_end for potential newline
}

#[test]
fn test_get_self_signed_cert() {
let result = get_cert("self-signed.badssl.com".to_string(), 443);
assert!(
result.is_ok(),
"Failed to get self-signed certificate (check badssl.com or replace with local test): {:?}",
result.err()
);
if let Ok(pem_string) = result {
assert!(pem_string.starts_with("-----BEGIN CERTIFICATE-----"));
assert!(pem_string.trim_end().ends_with("-----END CERTIFICATE-----"));
}
}

#[test]
fn test_get_wrong_host_cert() {
let result = get_cert("wrong.host.badssl.com".to_string(), 443);
assert!(
result.is_ok(),
"Failed to get certificate from wrong.host.badssl.com (check badssl.com or replace with local test): {:?}",
result.err()
);
if let Ok(pem_string) = result {
assert!(pem_string.starts_with("-----BEGIN CERTIFICATE-----"));
assert!(pem_string.trim_end().ends_with("-----END CERTIFICATE-----"));
}
}

#[test]
fn test_get_example_com_cert() {
let result = get_cert("example.com".to_string(), 443);
assert!(
result.is_ok(),
"Failed to get certificate: {:?}",
result.err()
);
let pem_string = result.unwrap();

assert!(pem_string.starts_with("-----BEGIN CERTIFICATE-----"));
assert!(pem_string.trim_end().ends_with("-----END CERTIFICATE-----"));
}

#[test]
fn test_non_existent_host() {
// This test expects a connection error or DNS resolution error
let result = get_cert("nonexistent.invalid.example.com".to_string(), 443);
assert!(
result.is_err(),
"Expected an error for a non-existent host, but got Ok"
);
}

#[test]
fn test_host_with_no_tls_on_port() {
// Assuming google.com:80 does not offer TLS
let result = get_cert("google.com".to_string(), 80);
assert!(
result.is_err(),
"Expected an error for a non-TLS port, but got Ok"
);
}
// #[test]
// fn test_get_self_signed_cert() {
// let result = get_cert("self-signed.badssl.com".to_string(), 443);
// assert!(
// result.is_ok(),
// "Failed to get self-signed certificate (check badssl.com or replace with local test): {:?}",
// result.err()
// );
// if let Ok(pem_string) = result {
// assert!(pem_string.starts_with("-----BEGIN CERTIFICATE-----"));
// assert!(pem_string.trim_end().ends_with("-----END CERTIFICATE-----"));
// }
// }

// #[test]
// fn test_get_wrong_host_cert() {
// let result = get_cert("wrong.host.badssl.com".to_string(), 443);
// assert!(
// result.is_ok(),
// "Failed to get certificate from wrong.host.badssl.com (check badssl.com or replace with local test): {:?}",
// result.err()
// );
// if let Ok(pem_string) = result {
// assert!(pem_string.starts_with("-----BEGIN CERTIFICATE-----"));
// assert!(pem_string.trim_end().ends_with("-----END CERTIFICATE-----"));
// }
// }

// #[test]
// fn test_get_example_com_cert() {
// let result = get_cert("example.com".to_string(), 443);
// assert!(
// result.is_ok(),
// "Failed to get certificate: {:?}",
// result.err()
// );
// let pem_string = result.unwrap();

// assert!(pem_string.starts_with("-----BEGIN CERTIFICATE-----"));
// assert!(pem_string.trim_end().ends_with("-----END CERTIFICATE-----"));
// }

// #[test]
// fn test_non_existent_host() {
// // This test expects a connection error or DNS resolution error
// let result = get_cert("nonexistent.invalid.example.com".to_string(), 443);
// assert!(
// result.is_err(),
// "Expected an error for a non-existent host, but got Ok"
// );
// }

// #[test]
// fn test_host_with_no_tls_on_port() {
// // Assuming google.com:80 does not offer TLS
// let result = get_cert("google.com".to_string(), 80);
// assert!(
// result.is_err(),
// "Expected an error for a non-TLS port, but got Ok"
// );
// }
}
4 changes: 2 additions & 2 deletions vopono_core/src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use log::{debug, info, warn};
use nix::unistd::{Group, User};
pub use open_hosts::open_hosts;
pub use open_ports::open_ports;
use rand::seq::SliceRandom;
use rand::prelude::IndexedRandom;
use regex::Regex;
use std::collections::HashMap;
use std::fs;
Expand Down Expand Up @@ -432,7 +432,7 @@ pub fn get_config_from_alias(list_path: &Path, alias: &str) -> anyhow::Result<Pa
Err(anyhow!("Could not find config file for alias {}", &alias))
} else {
let config = paths
.choose(&mut rand::thread_rng())
.choose(&mut rand::rng())
.expect("Could not find config");

info!("Chosen config: {}", config.display());
Expand Down
80 changes: 77 additions & 3 deletions vopono_core/src/util/wireguard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ use serde::Deserialize;
use std::fmt::Display;
use x25519_dalek::{PublicKey, StaticSecret};

use rand_core::OsRng as X25519OsRng;

const B64_ENGINE: GeneralPurpose = general_purpose::STANDARD;

#[derive(Deserialize, Clone)]
Expand Down Expand Up @@ -44,7 +42,7 @@ impl Display for WgPeer {

pub fn generate_keypair() -> anyhow::Result<WgKey> {
// Generate new keypair
let private = StaticSecret::random_from_rng(X25519OsRng);
let private = StaticSecret::random_from_rng(&mut rand::rng());
let public = PublicKey::from(&private);
let public_key = B64_ENGINE.encode(public.as_bytes());
let private_key = B64_ENGINE.encode(private.to_bytes());
Expand All @@ -58,6 +56,10 @@ pub fn generate_keypair() -> anyhow::Result<WgKey> {

pub fn generate_public_key(private_key: &str) -> anyhow::Result<String> {
let private_bytes = B64_ENGINE.decode(private_key)?;
if private_bytes.len() != 32 {
anyhow::bail!("Private key must be exactly 32 bytes when decoded");
}

let mut byte_array = [0; 32];
byte_array.copy_from_slice(&private_bytes);

Expand All @@ -66,3 +68,75 @@ pub fn generate_public_key(private_key: &str) -> anyhow::Result<String> {
let public_key = B64_ENGINE.encode(public.as_bytes());
Ok(public_key)
}

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

#[test]
fn test_generate_keypair() {
let keypair = generate_keypair().unwrap();

// Check that both keys are valid base64 strings
assert!(!keypair.public.is_empty());
assert!(!keypair.private.is_empty());

// Check that public key is 44 characters (32 bytes base64 encoded)
assert_eq!(keypair.public.len(), 44);

// Check that private key is 44 characters (32 bytes base64 encoded)
assert_eq!(keypair.private.len(), 44);

// Verify that the public key can be derived from the private key
let derived_public = generate_public_key(&keypair.private).unwrap();
assert_eq!(keypair.public, derived_public);
}

#[test]
fn test_generate_public_key() {
// Test with a known private key - verify format and consistency
let private_key = "gI6EdkZ4UQR6N5Q1LpI+JWCb1yZCSBHNzQe7J/KoX0s=";
let public_key = generate_public_key(private_key).unwrap();

// Just verify it's a valid public key format rather than hardcoding
assert_eq!(public_key.len(), 44);
assert!(B64_ENGINE.decode(&public_key).is_ok());
}

#[test]
fn test_generate_public_key_invalid_input() {
// Test with invalid base64
let result = generate_public_key("invalid_base64!");
assert!(result.is_err());

// Test with empty string
let result = generate_public_key("");
assert!(result.is_err());

// Test with too short base64
let result = generate_public_key("YQ=="); // "a" encoded - only 1 byte
assert!(result.is_err());

// Test with base64 that's not 32 bytes when decoded
let result = generate_public_key("YWFhYWFhYQ=="); // "aaaaaa" - 6 bytes
assert!(result.is_err());
}

#[test]
fn test_keypair_consistency() {
// Generate multiple keypairs and ensure they're different
let keypair1 = generate_keypair().unwrap();
let keypair2 = generate_keypair().unwrap();

// Should be different due to randomness
assert_ne!(keypair1.public, keypair2.public);
assert_ne!(keypair1.private, keypair2.private);

// But each keypair should be internally consistent
let derived_public1 = generate_public_key(&keypair1.private).unwrap();
assert_eq!(keypair1.public, derived_public1);

let derived_public2 = generate_public_key(&keypair2.private).unwrap();
assert_eq!(keypair2.public, derived_public2);
}
}
Loading