cli: avoid data-dir initialization for version; create db parent dirs; redact generated passwords in CLI output
- Prevent 'nx9-wg version' from creating data directories by avoiding database initialization. - Create parent directories when an explicit --database path is provided. - Redact printed generated administrator passwords; announce file path or redact instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
commit
2ac6c81dfe
140 files changed
+31342
No files matched your search
@@ -0,0 +1,279 @@
|
||||
//! WireGuard client configuration file generator.
|
||||
|
||||
use crate::error::{Result, WireGuardError};
|
||||
use nx9_wg_core::types::client_profile::ResolvedClientProfile;
|
||||
use nx9_wg_core::types::wireguard::{Interface, Peer, PeerProfile};
|
||||
|
||||
/// Generator for standard client WireGuard configuration files (.conf).
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ClientConfigBuilder;
|
||||
|
||||
impl ClientConfigBuilder {
|
||||
/// Build a standard WireGuard client configuration string with default settings.
|
||||
pub fn build(peer: &Peer, interface: &Interface, server_host_or_ip: &str) -> Result<String> {
|
||||
Self::build_with_profile(peer, interface, server_host_or_ip, None)
|
||||
}
|
||||
|
||||
/// Build a complete standard WireGuard client configuration string with an optional resolved client profile.
|
||||
///
|
||||
/// The profile influences:
|
||||
/// - MTU (derived from provider/device/connection/NAT environment)
|
||||
/// - PersistentKeepalive (if specified in profile)
|
||||
/// - Optional DNS overrides
|
||||
///
|
||||
/// The profile explicitly DOES NOT alter:
|
||||
/// - Peer PrivateKey, PublicKey, PresharedKey
|
||||
/// - Peer IP addresses (IPv4 & IPv6)
|
||||
/// - Server Endpoint authority
|
||||
/// - Server AllowedIPs authority
|
||||
pub fn build_with_profile(
|
||||
peer: &Peer,
|
||||
interface: &Interface,
|
||||
server_host_or_ip: &str,
|
||||
profile: Option<&ResolvedClientProfile>,
|
||||
) -> Result<String> {
|
||||
let private_key = peer.private_key.as_ref().ok_or_else(|| {
|
||||
WireGuardError::Config("Peer does not have a private key stored".to_string())
|
||||
})?;
|
||||
|
||||
let mut lines = Vec::new();
|
||||
|
||||
// 1. [Interface] Section
|
||||
lines.push("[Interface]".to_string());
|
||||
lines.push(format!("PrivateKey = {}", private_key.as_str()));
|
||||
|
||||
// Address
|
||||
let mut addresses = Vec::new();
|
||||
if let Some(ref v4) = peer.address_v4 {
|
||||
addresses.push(v4.to_string());
|
||||
}
|
||||
if let Some(ref v6) = peer.address_v6 {
|
||||
addresses.push(v6.to_string());
|
||||
}
|
||||
if !addresses.is_empty() {
|
||||
lines.push(format!("Address = {}", addresses.join(", ")));
|
||||
}
|
||||
|
||||
// DNS (Profile DNS > Peer DNS > Interface DNS)
|
||||
let dns = profile
|
||||
.and_then(|p| p.dns.as_deref())
|
||||
.or(peer.dns.as_deref())
|
||||
.or(interface.dns.as_deref());
|
||||
if let Some(d) = dns.filter(|s| !s.trim().is_empty()) {
|
||||
lines.push(format!("DNS = {d}"));
|
||||
}
|
||||
|
||||
// MTU (Profile MTU > Peer MTU > Interface MTU)
|
||||
let mtu = profile.map(|p| p.mtu).or(peer.mtu).or(interface.mtu);
|
||||
if let Some(m) = mtu {
|
||||
lines.push(format!("MTU = {m}"));
|
||||
}
|
||||
|
||||
lines.push("".to_string());
|
||||
|
||||
// 2. [Peer] Section (Server)
|
||||
lines.push("[Peer]".to_string());
|
||||
lines.push(format!("PublicKey = {}", interface.public_key.as_str()));
|
||||
|
||||
if let Some(ref psk) = peer.preshared_key {
|
||||
lines.push(format!("PresharedKey = {}", psk.as_str()));
|
||||
}
|
||||
|
||||
// Endpoint
|
||||
let endpoint = if server_host_or_ip.contains(':') && !server_host_or_ip.starts_with('[') {
|
||||
// Check if already contains port
|
||||
server_host_or_ip.to_string()
|
||||
} else {
|
||||
format!("{}:{}", server_host_or_ip, interface.listen_port)
|
||||
};
|
||||
lines.push(format!("Endpoint = {endpoint}"));
|
||||
|
||||
// AllowedIPs based on Peer Profile
|
||||
let allowed_ips = match peer.profile {
|
||||
PeerProfile::FullTunnel => "0.0.0.0/0, ::/0".to_string(),
|
||||
PeerProfile::SplitTunnel => {
|
||||
let mut subnets = Vec::new();
|
||||
subnets.push(interface.address_v4.to_string());
|
||||
if let Some(ref v6) = interface.address_v6 {
|
||||
subnets.push(v6.to_string());
|
||||
}
|
||||
subnets.join(", ")
|
||||
}
|
||||
PeerProfile::Custom => {
|
||||
if peer.allowed_ips.trim().is_empty() {
|
||||
"0.0.0.0/0, ::/0".to_string()
|
||||
} else {
|
||||
peer.allowed_ips.clone()
|
||||
}
|
||||
}
|
||||
};
|
||||
lines.push(format!("AllowedIPs = {allowed_ips}"));
|
||||
|
||||
// PersistentKeepalive (Profile Keepalive > Peer Keepalive)
|
||||
let keepalive = profile
|
||||
.and_then(|p| p.persistent_keepalive)
|
||||
.or(peer.persistent_keepalive);
|
||||
if let Some(ka) = keepalive.filter(|&ka| ka > 0) {
|
||||
lines.push(format!("PersistentKeepalive = {ka}"));
|
||||
}
|
||||
|
||||
Ok(lines.join("\n") + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::Utc;
|
||||
use ipnet::IpNet;
|
||||
use nx9_wg_core::crypto::{generate_keypair, generate_preshared_key};
|
||||
use nx9_wg_core::types::wireguard::{PeerState, PeerType};
|
||||
use std::str::FromStr;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[test]
|
||||
fn test_client_config_generation_full_and_split() {
|
||||
let (srv_priv, srv_pub) = generate_keypair();
|
||||
let (peer_priv, peer_pub) = generate_keypair();
|
||||
let psk = generate_preshared_key();
|
||||
let now = Utc::now().naive_utc();
|
||||
|
||||
let iface = Interface {
|
||||
id: Uuid::new_v4(),
|
||||
name: "wg0".to_string(),
|
||||
private_key: srv_priv,
|
||||
public_key: srv_pub.clone(),
|
||||
listen_port: 51820,
|
||||
address_v4: IpNet::from_str("10.0.0.1/24").unwrap(),
|
||||
address_v6: None,
|
||||
mtu: Some(1420),
|
||||
dns: Some("1.1.1.1".to_string()),
|
||||
enabled: true,
|
||||
pre_up: None,
|
||||
post_up: None,
|
||||
pre_down: None,
|
||||
post_down: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
let mut peer = Peer {
|
||||
id: Uuid::new_v4(),
|
||||
interface_id: iface.id,
|
||||
name: "mobile-bob".to_string(),
|
||||
peer_type: PeerType::RoadWarrior,
|
||||
state: PeerState::Active,
|
||||
public_key: peer_pub,
|
||||
private_key: Some(peer_priv.clone()),
|
||||
preshared_key: Some(psk.clone()),
|
||||
endpoint: None,
|
||||
allowed_ips: "10.0.0.2/32".to_string(),
|
||||
server_allowed_ips: None,
|
||||
address_v4: Some(IpNet::from_str("10.0.0.2/32").unwrap()),
|
||||
address_v6: None,
|
||||
dns: None,
|
||||
mtu: None,
|
||||
persistent_keepalive: Some(25),
|
||||
profile: PeerProfile::FullTunnel,
|
||||
expires_at: None,
|
||||
last_handshake_at: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
// Full Tunnel
|
||||
let full_conf = ClientConfigBuilder::build(&peer, &iface, "vpn.example.com").unwrap();
|
||||
assert!(full_conf.contains(&format!("PrivateKey = {}", peer_priv.as_str())));
|
||||
assert!(full_conf.contains("Address = 10.0.0.2/32"));
|
||||
assert!(full_conf.contains("DNS = 1.1.1.1"));
|
||||
assert!(full_conf.contains("MTU = 1420"));
|
||||
assert!(full_conf.contains(&format!("PublicKey = {}", srv_pub.as_str())));
|
||||
assert!(full_conf.contains(&format!("PresharedKey = {}", psk.as_str())));
|
||||
assert!(full_conf.contains("Endpoint = vpn.example.com:51820"));
|
||||
assert!(full_conf.contains("AllowedIPs = 0.0.0.0/0, ::/0"));
|
||||
assert!(full_conf.contains("PersistentKeepalive = 25"));
|
||||
|
||||
// Split Tunnel
|
||||
peer.profile = PeerProfile::SplitTunnel;
|
||||
let split_conf = ClientConfigBuilder::build(&peer, &iface, "vpn.example.com").unwrap();
|
||||
assert!(split_conf.contains("AllowedIPs = 10.0.0.1/24"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_config_with_resolved_profile() {
|
||||
let (srv_priv, srv_pub) = generate_keypair();
|
||||
let (peer_priv, peer_pub) = generate_keypair();
|
||||
let now = Utc::now().naive_utc();
|
||||
|
||||
let iface = Interface {
|
||||
id: Uuid::new_v4(),
|
||||
name: "wg0".to_string(),
|
||||
private_key: srv_priv,
|
||||
public_key: srv_pub,
|
||||
listen_port: 51820,
|
||||
address_v4: IpNet::from_str("10.0.0.1/24").unwrap(),
|
||||
address_v6: None,
|
||||
mtu: Some(1420),
|
||||
dns: Some("1.1.1.1".to_string()),
|
||||
enabled: true,
|
||||
pre_up: None,
|
||||
post_up: None,
|
||||
pre_down: None,
|
||||
post_down: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
let peer = Peer {
|
||||
id: Uuid::new_v4(),
|
||||
interface_id: iface.id,
|
||||
name: "cgnat-peer".to_string(),
|
||||
peer_type: PeerType::RoadWarrior,
|
||||
state: PeerState::Active,
|
||||
public_key: peer_pub,
|
||||
private_key: Some(peer_priv),
|
||||
preshared_key: None,
|
||||
endpoint: None,
|
||||
allowed_ips: "10.0.0.5/32".to_string(),
|
||||
server_allowed_ips: None,
|
||||
address_v4: Some(IpNet::from_str("10.0.0.5/32").unwrap()),
|
||||
address_v6: None,
|
||||
dns: None,
|
||||
mtu: None,
|
||||
persistent_keepalive: None,
|
||||
profile: PeerProfile::FullTunnel,
|
||||
expires_at: None,
|
||||
last_handshake_at: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
let resolved_profile = ResolvedClientProfile {
|
||||
mtu: 1280,
|
||||
persistent_keepalive: Some(20),
|
||||
dns: Some("9.9.9.9".to_string()),
|
||||
is_manually_overridden: false,
|
||||
applied_profile_id: "default-mobile".to_string(),
|
||||
applied_profile_name: "Default Mobile".to_string(),
|
||||
connection_type: nx9_wg_core::types::client_profile::ConnectionType::Mobile,
|
||||
nat_type: nx9_wg_core::types::client_profile::NatType::Cgnat,
|
||||
device: Some(nx9_wg_core::types::client_profile::DeviceCategory::Android),
|
||||
provider: Some("tmobile".to_string()),
|
||||
warning: None,
|
||||
};
|
||||
|
||||
let conf = ClientConfigBuilder::build_with_profile(
|
||||
&peer,
|
||||
&iface,
|
||||
"vpn.example.com",
|
||||
Some(&resolved_profile),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(conf.contains("MTU = 1280"));
|
||||
assert!(conf.contains("PersistentKeepalive = 20"));
|
||||
assert!(conf.contains("DNS = 9.9.9.9"));
|
||||
assert!(conf.contains("Address = 10.0.0.5/32"));
|
||||
assert!(conf.contains("AllowedIPs = 0.0.0.0/0, ::/0"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user