diff --git a/nmrs/src/api/builders/openvpn_builder.rs b/nmrs/src/api/builders/openvpn_builder.rs index 9231ca73..dd275832 100644 --- a/nmrs/src/api/builders/openvpn_builder.rs +++ b/nmrs/src/api/builders/openvpn_builder.rs @@ -60,6 +60,16 @@ pub struct OpenVpnBuilder { password: Option, compression: Option, proxy: Option, + tls_auth_key: Option, + tls_auth_direction: Option, + tls_crypt: Option, + tls_crypt_v2: Option, + tls_version_min: Option, + tls_version_max: Option, + tls_cipher: Option, + remote_cert_tls: Option, + verify_x509_name: Option<(String, String)>, + crl_verify: Option, } impl OpenVpnBuilder { @@ -85,6 +95,16 @@ impl OpenVpnBuilder { password: None, compression: None, proxy: None, + tls_auth_key: None, + tls_auth_direction: None, + tls_crypt: None, + tls_crypt_v2: None, + tls_version_min: None, + tls_version_max: None, + tls_cipher: None, + remote_cert_tls: None, + verify_x509_name: None, + crl_verify: None, } } @@ -212,6 +232,74 @@ impl OpenVpnBuilder { self } + /// Sets the TLS authentication key path and optional direction. + #[must_use] + pub fn tls_auth(mut self, key_path: impl Into, direction: Option) -> Self { + self.tls_auth_key = Some(key_path.into()); + self.tls_auth_direction = direction; + self + } + + /// Sets the TLS-Crypt key path. + #[must_use] + pub fn tls_crypt(mut self, key_path: impl Into) -> Self { + self.tls_crypt = Some(key_path.into()); + self + } + + /// Sets the TLS-Crypt-v2 key path. + #[must_use] + pub fn tls_crypt_v2(mut self, key_path: impl Into) -> Self { + self.tls_crypt_v2 = Some(key_path.into()); + self + } + + /// Sets the minimum TLS protocol version. + #[must_use] + pub fn tls_version_min(mut self, version: impl Into) -> Self { + self.tls_version_min = Some(version.into()); + self + } + + /// Sets the maximum TLS protocol version. + #[must_use] + pub fn tls_version_max(mut self, version: impl Into) -> Self { + self.tls_version_max = Some(version.into()); + self + } + + /// Sets the control channel TLS cipher suites. + #[must_use] + pub fn tls_cipher(mut self, cipher: impl Into) -> Self { + self.tls_cipher = Some(cipher.into()); + self + } + + /// Requires the remote certificate to be of a specific type. + #[must_use] + pub fn remote_cert_tls(mut self, cert_type: impl Into) -> Self { + self.remote_cert_tls = Some(cert_type.into()); + self + } + + /// Sets X.509 name verification for the remote certificate. + #[must_use] + pub fn verify_x509_name( + mut self, + name: impl Into, + name_type: impl Into, + ) -> Self { + self.verify_x509_name = Some((name.into(), name_type.into())); + self + } + + /// Sets the path to a Certificate Revocation List. + #[must_use] + pub fn crl_verify(mut self, path: impl Into) -> Self { + self.crl_verify = Some(path.into()); + self + } + /// Builds and validates the `OpenVpnConfig`. /// /// # Errors @@ -304,6 +392,16 @@ impl OpenVpnBuilder { password: self.password, compression: self.compression, proxy: self.proxy, + tls_auth_key: self.tls_auth_key, + tls_auth_direction: self.tls_auth_direction, + tls_crypt: self.tls_crypt, + tls_crypt_v2: self.tls_crypt_v2, + tls_version_min: self.tls_version_min, + tls_version_max: self.tls_version_max, + tls_cipher: self.tls_cipher, + remote_cert_tls: self.remote_cert_tls, + verify_x509_name: self.verify_x509_name, + crl_verify: self.crl_verify, }) } } diff --git a/nmrs/src/api/builders/vpn.rs b/nmrs/src/api/builders/vpn.rs index 84822aa1..85091263 100644 --- a/nmrs/src/api/builders/vpn.rs +++ b/nmrs/src/api/builders/vpn.rs @@ -224,6 +224,39 @@ pub fn build_openvpn_connection( } } + // TLS hardening options + if let Some(ref key) = config.tls_auth_key { + vpn_data.push(("tls-auth".into(), key.clone())); + if let Some(dir) = config.tls_auth_direction { + vpn_data.push(("ta-dir".into(), dir.to_string())); + } + } + if let Some(ref key) = config.tls_crypt { + vpn_data.push(("tls-crypt".into(), key.clone())); + } + if let Some(ref key) = config.tls_crypt_v2 { + vpn_data.push(("tls-crypt-v2".into(), key.clone())); + } + if let Some(ref ver) = config.tls_version_min { + vpn_data.push(("tls-version-min".into(), ver.clone())); + } + if let Some(ref ver) = config.tls_version_max { + vpn_data.push(("tls-version-max".into(), ver.clone())); + } + if let Some(ref cipher) = config.tls_cipher { + vpn_data.push(("tls-cipher".into(), cipher.clone())); + } + if let Some(ref cert_type) = config.remote_cert_tls { + vpn_data.push(("remote-cert-tls".into(), cert_type.clone())); + } + if let Some((ref name, ref name_type)) = config.verify_x509_name { + vpn_data.push(("verify-x509-name".into(), name.clone())); + vpn_data.push(("verify-x509-type".into(), name_type.clone())); + } + if let Some(ref path) = config.crl_verify { + vpn_data.push(("crl-verify".into(), path.clone())); + } + if let Some(ref proxy) = config.proxy { match proxy { OpenVpnProxy::Http { @@ -995,6 +1028,19 @@ mod tests { ); } + fn get_vpn_data_value( + settings: &HashMap<&str, HashMap<&str, Value>>, + key: &str, + ) -> Option { + let vpn = settings.get("vpn")?; + let data = vpn.get("data")?; + if let Value::Dict(dict) = data { + let val: String = dict.get::(&Value::from(key)).ok()??; + return Some(val); + } + None + } + #[test] fn openvpn_vpn_secrets_has_dict_signature() { let config = create_openvpn_config() @@ -1011,4 +1057,141 @@ mod tests { "vpn.secrets must be a{{ss}} for NetworkManager" ); } + + #[test] + fn openvpn_tls_auth_key_and_direction() { + let config = create_openvpn_config().with_tls_auth("/etc/openvpn/ta.key", Some(1)); + let opts = create_test_options(); + let settings = build_openvpn_connection(&config, &opts).unwrap(); + assert_eq!( + get_vpn_data_value(&settings, "tls-auth").as_deref(), + Some("/etc/openvpn/ta.key") + ); + assert_eq!( + get_vpn_data_value(&settings, "ta-dir").as_deref(), + Some("1") + ); + } + + #[test] + fn openvpn_tls_auth_key_without_direction() { + let config = create_openvpn_config().with_tls_auth("/etc/openvpn/ta.key", None); + let opts = create_test_options(); + let settings = build_openvpn_connection(&config, &opts).unwrap(); + assert_eq!( + get_vpn_data_value(&settings, "tls-auth").as_deref(), + Some("/etc/openvpn/ta.key") + ); + assert!(get_vpn_data_value(&settings, "ta-dir").is_none()); + } + + #[test] + fn openvpn_tls_crypt() { + let config = create_openvpn_config().with_tls_crypt("/etc/openvpn/tls-crypt.key"); + let opts = create_test_options(); + let settings = build_openvpn_connection(&config, &opts).unwrap(); + assert_eq!( + get_vpn_data_value(&settings, "tls-crypt").as_deref(), + Some("/etc/openvpn/tls-crypt.key") + ); + } + + #[test] + fn openvpn_tls_crypt_v2() { + let config = create_openvpn_config().with_tls_crypt_v2("/etc/openvpn/tls-crypt-v2.key"); + let opts = create_test_options(); + let settings = build_openvpn_connection(&config, &opts).unwrap(); + assert_eq!( + get_vpn_data_value(&settings, "tls-crypt-v2").as_deref(), + Some("/etc/openvpn/tls-crypt-v2.key") + ); + } + + #[test] + fn openvpn_tls_version_min() { + let config = create_openvpn_config().with_tls_version_min("1.2"); + let opts = create_test_options(); + let settings = build_openvpn_connection(&config, &opts).unwrap(); + assert_eq!( + get_vpn_data_value(&settings, "tls-version-min").as_deref(), + Some("1.2") + ); + } + + #[test] + fn openvpn_tls_version_max() { + let config = create_openvpn_config().with_tls_version_max("1.3"); + let opts = create_test_options(); + let settings = build_openvpn_connection(&config, &opts).unwrap(); + assert_eq!( + get_vpn_data_value(&settings, "tls-version-max").as_deref(), + Some("1.3") + ); + } + + #[test] + fn openvpn_tls_cipher() { + let config = + create_openvpn_config().with_tls_cipher("TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384"); + let opts = create_test_options(); + let settings = build_openvpn_connection(&config, &opts).unwrap(); + assert_eq!( + get_vpn_data_value(&settings, "tls-cipher").as_deref(), + Some("TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384") + ); + } + + #[test] + fn openvpn_remote_cert_tls() { + let config = create_openvpn_config().with_remote_cert_tls("server"); + let opts = create_test_options(); + let settings = build_openvpn_connection(&config, &opts).unwrap(); + assert_eq!( + get_vpn_data_value(&settings, "remote-cert-tls").as_deref(), + Some("server") + ); + } + + #[test] + fn openvpn_verify_x509_name() { + let config = create_openvpn_config().with_verify_x509_name("vpn.example.com", "name"); + let opts = create_test_options(); + let settings = build_openvpn_connection(&config, &opts).unwrap(); + assert_eq!( + get_vpn_data_value(&settings, "verify-x509-name").as_deref(), + Some("vpn.example.com") + ); + assert_eq!( + get_vpn_data_value(&settings, "verify-x509-type").as_deref(), + Some("name") + ); + } + + #[test] + fn openvpn_crl_verify() { + let config = create_openvpn_config().with_crl_verify("/etc/openvpn/crl.pem"); + let opts = create_test_options(); + let settings = build_openvpn_connection(&config, &opts).unwrap(); + assert_eq!( + get_vpn_data_value(&settings, "crl-verify").as_deref(), + Some("/etc/openvpn/crl.pem") + ); + } + + #[test] + fn openvpn_tls_options_absent_by_default() { + let config = create_openvpn_config(); + let opts = create_test_options(); + let settings = build_openvpn_connection(&config, &opts).unwrap(); + assert!(get_vpn_data_value(&settings, "tls-auth").is_none()); + assert!(get_vpn_data_value(&settings, "ta-dir").is_none()); + assert!(get_vpn_data_value(&settings, "tls-crypt").is_none()); + assert!(get_vpn_data_value(&settings, "tls-crypt-v2").is_none()); + assert!(get_vpn_data_value(&settings, "tls-version-min").is_none()); + assert!(get_vpn_data_value(&settings, "tls-version-max").is_none()); + assert!(get_vpn_data_value(&settings, "tls-cipher").is_none()); + assert!(get_vpn_data_value(&settings, "remote-cert-tls").is_none()); + assert!(get_vpn_data_value(&settings, "verify-x509-name").is_none()); + assert!(get_vpn_data_value(&settings, "crl-verify").is_none()); + } } diff --git a/nmrs/src/api/models/openvpn.rs b/nmrs/src/api/models/openvpn.rs index adee2265..017410c1 100644 --- a/nmrs/src/api/models/openvpn.rs +++ b/nmrs/src/api/models/openvpn.rs @@ -76,6 +76,27 @@ pub struct OpenVpnConfig { pub compression: Option, /// Proxy configuration. pub proxy: Option, + /// Path to TLS authentication (HMAC firewall) key file. + pub tls_auth_key: Option, + /// TLS auth direction (`0` or `1`). Only meaningful when `tls_auth_key` is set. + pub tls_auth_direction: Option, + /// Path to TLS-Crypt key file (encrypt+authenticate control channel). + pub tls_crypt: Option, + /// Path to TLS-Crypt-v2 key file (per-client TLS-Crypt). + pub tls_crypt_v2: Option, + /// Minimum TLS version (e.g. "1.2"). + pub tls_version_min: Option, + /// Maximum TLS version (e.g. "1.3"). + pub tls_version_max: Option, + /// Control channel TLS cipher suites (e.g. "TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384"). + pub tls_cipher: Option, + /// Require remote certificate to be of a specific type ("server" or "client"). + pub remote_cert_tls: Option, + /// X.509 name verification: `(name, type)` where type is e.g. "name", "subject", + /// or "name-prefix". + pub verify_x509_name: Option<(String, String)>, + /// Path to a Certificate Revocation List file. + pub crl_verify: Option, } impl OpenVpnConfig { @@ -100,6 +121,16 @@ impl OpenVpnConfig { password: None, compression: None, proxy: None, + tls_auth_key: None, + tls_auth_direction: None, + tls_crypt: None, + tls_crypt_v2: None, + tls_version_min: None, + tls_version_max: None, + tls_cipher: None, + remote_cert_tls: None, + verify_x509_name: None, + crl_verify: None, } } @@ -205,6 +236,82 @@ impl OpenVpnConfig { self.proxy = Some(proxy); self } + + /// Sets the TLS authentication key path and optional direction. + /// + /// The `--tls-auth` option adds an HMAC firewall to the control channel, + /// providing an additional layer of DoS protection. + #[must_use] + pub fn with_tls_auth(mut self, key_path: impl Into, direction: Option) -> Self { + self.tls_auth_key = Some(key_path.into()); + self.tls_auth_direction = direction; + self + } + + /// Sets the TLS-Crypt key path. + /// + /// Encrypts and authenticates the control channel with a pre-shared key, + /// providing stronger protection than `--tls-auth`. + #[must_use] + pub fn with_tls_crypt(mut self, key_path: impl Into) -> Self { + self.tls_crypt = Some(key_path.into()); + self + } + + /// Sets the TLS-Crypt-v2 key path (per-client key wrapping). + #[must_use] + pub fn with_tls_crypt_v2(mut self, key_path: impl Into) -> Self { + self.tls_crypt_v2 = Some(key_path.into()); + self + } + + /// Sets the minimum TLS protocol version (e.g. "1.2"). + #[must_use] + pub fn with_tls_version_min(mut self, version: impl Into) -> Self { + self.tls_version_min = Some(version.into()); + self + } + + /// Sets the maximum TLS protocol version (e.g. "1.3"). + #[must_use] + pub fn with_tls_version_max(mut self, version: impl Into) -> Self { + self.tls_version_max = Some(version.into()); + self + } + + /// Sets the allowed control channel TLS cipher suites. + #[must_use] + pub fn with_tls_cipher(mut self, cipher: impl Into) -> Self { + self.tls_cipher = Some(cipher.into()); + self + } + + /// Requires the remote certificate to be of a specific type ("server" or "client"). + #[must_use] + pub fn with_remote_cert_tls(mut self, cert_type: impl Into) -> Self { + self.remote_cert_tls = Some(cert_type.into()); + self + } + + /// Sets X.509 name verification for the remote certificate. + /// + /// `name_type` is one of "name", "subject", or "name-prefix". + #[must_use] + pub fn with_verify_x509_name( + mut self, + name: impl Into, + name_type: impl Into, + ) -> Self { + self.verify_x509_name = Some((name.into(), name_type.into())); + self + } + + /// Sets the path to a Certificate Revocation List for peer verification. + #[must_use] + pub fn with_crl_verify(mut self, path: impl Into) -> Self { + self.crl_verify = Some(path.into()); + self + } } impl TryFrom for OpenVpnConfig { @@ -280,6 +387,16 @@ impl TryFrom for OpenVpnConfig { password: None, compression, proxy: None, + tls_auth_key: None, + tls_auth_direction: None, + tls_crypt: None, + tls_crypt_v2: None, + tls_version_min: None, + tls_version_max: None, + tls_cipher: None, + remote_cert_tls: None, + verify_x509_name: None, + crl_verify: None, }) } } diff --git a/nmrs/src/api/models/vpn.rs b/nmrs/src/api/models/vpn.rs index c5c21a37..204e0daa 100644 --- a/nmrs/src/api/models/vpn.rs +++ b/nmrs/src/api/models/vpn.rs @@ -24,7 +24,7 @@ pub enum VpnConfiguration { /// WireGuard VPN configuration. WireGuard(WireGuardConfig), /// OpenVPN configuration - OpenVpn(OpenVpnConfig), + OpenVpn(Box), } impl From for VpnConfiguration { @@ -35,7 +35,7 @@ impl From for VpnConfiguration { impl From for VpnConfiguration { fn from(config: OpenVpnConfig) -> Self { - Self::OpenVpn(config) + Self::OpenVpn(Box::new(config)) } }