#![allow(deprecated)]
use super::vpn::{VpnConfig, VpnKind};
use crate::api::models::error::ConnectionError;
use std::convert::TryFrom;
use std::net::Ipv4Addr;
use uuid::Uuid;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VpnRoute {
pub dest: String,
pub prefix: u32,
pub next_hop: Option<String>,
pub metric: Option<u32>,
}
impl VpnRoute {
#[must_use]
pub fn new(dest: impl Into<String>, prefix: u32) -> Self {
Self {
dest: dest.into(),
prefix,
next_hop: None,
metric: None,
}
}
#[must_use]
pub fn next_hop(mut self, gateway: impl Into<String>) -> Self {
self.next_hop = Some(gateway.into());
self
}
#[must_use]
pub fn metric(mut self, metric: u32) -> Self {
self.metric = Some(metric);
self
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OpenVpnAuthType {
Password,
Tls,
PasswordTls,
StaticKey,
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct OpenVpnConfig {
pub name: String,
pub remote: String,
pub port: u16,
pub tcp: bool,
pub auth_type: Option<OpenVpnAuthType>,
pub auth: Option<String>,
pub cipher: Option<String>,
pub dns: Option<Vec<String>>,
pub mtu: Option<u32>,
pub uuid: Option<Uuid>,
pub ca_cert: Option<String>,
pub client_cert: Option<String>,
pub client_key: Option<String>,
pub key_password: Option<String>,
pub username: Option<String>,
pub password: Option<String>,
pub compression: Option<OpenVpnCompression>,
pub proxy: Option<OpenVpnProxy>,
pub tls_auth_key: Option<String>,
pub tls_auth_direction: Option<u8>,
pub tls_crypt: Option<String>,
pub tls_crypt_v2: Option<String>,
pub tls_version_min: Option<String>,
pub tls_version_max: Option<String>,
pub tls_cipher: Option<String>,
pub remote_cert_tls: Option<String>,
pub verify_x509_name: Option<(String, String)>,
pub crl_verify: Option<String>,
pub redirect_gateway: bool,
pub routes: Vec<VpnRoute>,
pub ping: Option<u32>,
pub ping_exit: Option<u32>,
pub ping_restart: Option<u32>,
pub reneg_seconds: Option<u32>,
pub connect_timeout: Option<u32>,
pub data_ciphers: Option<String>,
pub data_ciphers_fallback: Option<String>,
pub ncp_disable: bool,
}
impl OpenVpnConfig {
pub fn new(name: impl Into<String>, remote: impl Into<String>, port: u16, tcp: bool) -> Self {
Self {
name: name.into(),
remote: remote.into(),
port,
tcp,
auth_type: None,
auth: None,
cipher: None,
dns: None,
mtu: None,
uuid: None,
ca_cert: None,
client_cert: None,
client_key: None,
key_password: None,
username: None,
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,
redirect_gateway: false,
routes: Vec::new(),
ping: None,
ping_exit: None,
ping_restart: None,
reneg_seconds: None,
connect_timeout: None,
data_ciphers: None,
data_ciphers_fallback: None,
ncp_disable: false,
}
}
#[must_use]
pub fn with_auth_type(mut self, auth_type: OpenVpnAuthType) -> Self {
self.auth_type = Some(auth_type);
self
}
#[must_use]
pub fn with_auth(mut self, auth: impl Into<String>) -> Self {
self.auth = Some(auth.into());
self
}
#[must_use]
pub fn with_cipher(mut self, cipher: impl Into<String>) -> Self {
self.cipher = Some(cipher.into());
self
}
#[must_use]
pub fn with_dns(mut self, dns: Vec<String>) -> Self {
self.dns = Some(dns);
self
}
#[must_use]
pub fn with_mtu(mut self, mtu: u32) -> Self {
self.mtu = Some(mtu);
self
}
#[must_use]
pub fn with_uuid(mut self, uuid: Uuid) -> Self {
self.uuid = Some(uuid);
self
}
#[must_use]
pub fn with_ca_cert(mut self, path: impl Into<String>) -> Self {
self.ca_cert = Some(path.into());
self
}
#[must_use]
pub fn with_client_cert(mut self, path: impl Into<String>) -> Self {
self.client_cert = Some(path.into());
self
}
#[must_use]
pub fn with_client_key(mut self, path: impl Into<String>) -> Self {
self.client_key = Some(path.into());
self
}
#[must_use]
pub fn with_key_password(mut self, password: impl Into<String>) -> Self {
self.key_password = Some(password.into());
self
}
#[must_use]
pub fn with_username(mut self, username: impl Into<String>) -> Self {
self.username = Some(username.into());
self
}
#[must_use]
pub fn with_password(mut self, password: impl Into<String>) -> Self {
self.password = Some(password.into());
self
}
#[must_use]
pub fn with_compression(mut self, compression: OpenVpnCompression) -> Self {
self.compression = Some(compression);
self
}
#[must_use]
pub fn with_proxy(mut self, proxy: OpenVpnProxy) -> Self {
self.proxy = Some(proxy);
self
}
#[must_use]
pub fn with_tls_auth(mut self, key_path: impl Into<String>, direction: Option<u8>) -> Self {
self.tls_auth_key = Some(key_path.into());
self.tls_auth_direction = direction;
self
}
#[must_use]
pub fn with_tls_crypt(mut self, key_path: impl Into<String>) -> Self {
self.tls_crypt = Some(key_path.into());
self
}
#[must_use]
pub fn with_tls_crypt_v2(mut self, key_path: impl Into<String>) -> Self {
self.tls_crypt_v2 = Some(key_path.into());
self
}
#[must_use]
pub fn with_tls_version_min(mut self, version: impl Into<String>) -> Self {
self.tls_version_min = Some(version.into());
self
}
#[must_use]
pub fn with_tls_version_max(mut self, version: impl Into<String>) -> Self {
self.tls_version_max = Some(version.into());
self
}
#[must_use]
pub fn with_tls_cipher(mut self, cipher: impl Into<String>) -> Self {
self.tls_cipher = Some(cipher.into());
self
}
#[must_use]
pub fn with_remote_cert_tls(mut self, cert_type: impl Into<String>) -> Self {
self.remote_cert_tls = Some(cert_type.into());
self
}
#[must_use]
pub fn with_verify_x509_name(
mut self,
name: impl Into<String>,
name_type: impl Into<String>,
) -> Self {
self.verify_x509_name = Some((name.into(), name_type.into()));
self
}
#[must_use]
pub fn with_crl_verify(mut self, path: impl Into<String>) -> Self {
self.crl_verify = Some(path.into());
self
}
#[must_use]
pub fn with_redirect_gateway(mut self, redirect: bool) -> Self {
self.redirect_gateway = redirect;
self
}
#[must_use]
pub fn with_routes(mut self, routes: Vec<VpnRoute>) -> Self {
self.routes = routes;
self
}
#[must_use]
pub fn with_ping(mut self, seconds: u32) -> Self {
self.ping = Some(seconds);
self
}
#[must_use]
pub fn with_ping_exit(mut self, seconds: u32) -> Self {
self.ping_exit = Some(seconds);
self
}
#[must_use]
pub fn with_ping_restart(mut self, seconds: u32) -> Self {
self.ping_restart = Some(seconds);
self
}
#[must_use]
pub fn with_reneg_seconds(mut self, seconds: u32) -> Self {
self.reneg_seconds = Some(seconds);
self
}
#[must_use]
pub fn with_connect_timeout(mut self, seconds: u32) -> Self {
self.connect_timeout = Some(seconds);
self
}
#[must_use]
pub fn with_data_ciphers(mut self, ciphers: impl Into<String>) -> Self {
self.data_ciphers = Some(ciphers.into());
self
}
#[must_use]
pub fn with_data_ciphers_fallback(mut self, cipher: impl Into<String>) -> Self {
self.data_ciphers_fallback = Some(cipher.into());
self
}
#[must_use]
pub fn with_ncp_disable(mut self, disable: bool) -> Self {
self.ncp_disable = disable;
self
}
}
fn ipv4_netmask_to_prefix(netmask: Ipv4Addr) -> u32 {
let mut prefix = 0u32;
for byte in netmask.octets() {
if byte == 0xff {
prefix += 8;
} else if byte == 0 {
break;
} else {
let mut b = byte;
while b & 0x80 != 0 {
prefix += 1;
b <<= 1;
}
break;
}
}
prefix
}
pub(crate) fn vpn_route_from_parser(
r: crate::core::ovpn_parser::parser::Route,
) -> Result<VpnRoute, ConnectionError> {
let dest = r.network.to_string();
let prefix = r.netmask.map(ipv4_netmask_to_prefix).unwrap_or(32);
if prefix > 32 {
return Err(ConnectionError::InvalidAddress(format!(
"invalid route netmask for destination {dest}"
)));
}
let next_hop = r.gateway.map(|g| g.to_string());
Ok(VpnRoute {
dest,
prefix,
next_hop,
metric: None,
})
}
impl TryFrom<crate::core::ovpn_parser::parser::OvpnFile> for OpenVpnConfig {
type Error = ConnectionError;
fn try_from(f: crate::core::ovpn_parser::parser::OvpnFile) -> Result<Self, Self::Error> {
use crate::core::ovpn_parser::parser::{AllowCompress, CertSource, Compress};
let first_remote = f
.remotes
.into_iter()
.next()
.ok_or_else(|| ConnectionError::InvalidGateway("no remote in .ovpn file".into()))?;
let tcp = first_remote
.proto
.as_deref()
.map(|p: &str| p.starts_with("tcp"))
.unwrap_or_else(|| {
f.proto
.as_deref()
.map(|p: &str| p.starts_with("tcp"))
.unwrap_or(false)
});
let compression = match (f.compress, f.allow_compress) {
(Some(Compress::Algorithm(ref s)), _) => Some(match s.as_str() {
"lz4" => OpenVpnCompression::Lz4,
"lz4-v2" => OpenVpnCompression::Lz4V2,
_ => OpenVpnCompression::Yes,
}),
(Some(Compress::Stub | Compress::StubV2), _) => Some(OpenVpnCompression::No),
(None, Some(AllowCompress::No)) => Some(OpenVpnCompression::No),
_ => None,
};
let has_client_cert_pair = f.cert.is_some() && f.key.is_some();
let auth_type = match (f.auth_user_pass, has_client_cert_pair) {
(true, true) => Some(OpenVpnAuthType::PasswordTls),
(true, false) => Some(OpenVpnAuthType::Password),
(false, true) => Some(OpenVpnAuthType::Tls),
(false, false) => None,
};
let cert_path = |src: CertSource, field: &str| -> Result<String, ConnectionError> {
match src {
CertSource::File(p) => Ok(p),
CertSource::Inline(_) => Err(ConnectionError::VpnFailed(format!(
"inline <{field}> blocks require OpenVpnBuilder::from_ovpn_file() \
or from_ovpn_str() which persists them via the cert store; \
TryFrom<OvpnFile> cannot handle inline certs"
))),
}
};
let routes: Vec<VpnRoute> = f
.routes
.into_iter()
.map(vpn_route_from_parser)
.collect::<Result<_, _>>()?;
let redirect_gateway = f.redirect_gateway.is_some();
let data_ciphers = if f.data_ciphers.is_empty() {
None
} else {
Some(f.data_ciphers.join(":"))
};
Ok(OpenVpnConfig {
name: String::new(),
remote: first_remote.host,
port: first_remote.port.unwrap_or(1194),
tcp,
auth_type,
auth: f.auth,
cipher: f.cipher,
dns: None,
mtu: None,
uuid: None,
ca_cert: f.ca.map(|s| cert_path(s, "ca")).transpose()?,
client_cert: f.cert.map(|s| cert_path(s, "cert")).transpose()?,
client_key: f.key.map(|s| cert_path(s, "key")).transpose()?,
key_password: None,
username: None,
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,
redirect_gateway,
routes,
ping: None,
ping_exit: None,
ping_restart: None,
reneg_seconds: None,
connect_timeout: None,
data_ciphers,
data_ciphers_fallback: None,
ncp_disable: false,
})
}
}
impl super::vpn::sealed::Sealed for OpenVpnConfig {}
impl VpnConfig for OpenVpnConfig {
fn vpn_kind(&self) -> VpnKind {
VpnKind::Plugin
}
fn name(&self) -> &str {
&self.name
}
fn dns(&self) -> Option<&[String]> {
self.dns.as_deref()
}
fn mtu(&self) -> Option<u32> {
self.mtu
}
fn uuid(&self) -> Option<Uuid> {
self.uuid
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OpenVpnCompression {
No,
#[deprecated(note = "comp-lzo is deprecated upstream. Use Lz4V2 or No instead.")]
Lzo,
Lz4,
Lz4V2,
Yes,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OpenVpnProxy {
Http {
server: String,
port: u16,
username: Option<String>,
password: Option<String>,
retry: bool,
},
Socks {
server: String,
port: u16,
retry: bool,
},
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ovpn_parser::parser::parse_ovpn;
fn ovpn_with_remote(extra: &str) -> String {
format!("remote vpn.example.com 1194 udp\n{extra}")
}
#[test]
fn try_from_auth_user_pass_with_file_certs_infers_password_tls() {
let input = ovpn_with_remote(
"auth-user-pass\ncert /etc/openvpn/client.crt\nkey /etc/openvpn/client.key",
);
let ovpn = parse_ovpn(&input).unwrap();
let config = OpenVpnConfig::try_from(ovpn).unwrap();
assert_eq!(config.auth_type, Some(OpenVpnAuthType::PasswordTls));
}
#[test]
fn try_from_auth_user_pass_without_certs_infers_password() {
let input = ovpn_with_remote("auth-user-pass");
let ovpn = parse_ovpn(&input).unwrap();
let config = OpenVpnConfig::try_from(ovpn).unwrap();
assert_eq!(config.auth_type, Some(OpenVpnAuthType::Password));
}
#[test]
fn try_from_no_auth_user_pass_with_file_certs_infers_tls() {
let input = ovpn_with_remote("cert /etc/openvpn/client.crt\nkey /etc/openvpn/client.key");
let ovpn = parse_ovpn(&input).unwrap();
let config = OpenVpnConfig::try_from(ovpn).unwrap();
assert_eq!(config.auth_type, Some(OpenVpnAuthType::Tls));
}
#[test]
fn try_from_no_auth_user_pass_no_certs_infers_none() {
let input = ovpn_with_remote("");
let ovpn = parse_ovpn(&input).unwrap();
let config = OpenVpnConfig::try_from(ovpn).unwrap();
assert_eq!(config.auth_type, None);
}
#[test]
fn try_from_inline_cert_returns_error() {
let input = ovpn_with_remote("<cert>\nCERTPEM\n</cert>\n<key>\nKEYPEM\n</key>");
let ovpn = parse_ovpn(&input).unwrap();
let result = OpenVpnConfig::try_from(ovpn);
assert!(
result.is_err(),
"inline certs should be rejected by TryFrom"
);
}
#[test]
fn try_from_cert_only_without_auth_user_pass_does_not_infer_tls() {
let input = ovpn_with_remote("cert /etc/openvpn/client.crt");
let ovpn = parse_ovpn(&input).unwrap();
let config = OpenVpnConfig::try_from(ovpn).unwrap();
assert_eq!(config.auth_type, None);
}
#[test]
fn try_from_cert_only_with_auth_user_pass_infers_password_not_password_tls() {
let input = ovpn_with_remote("auth-user-pass\ncert /etc/openvpn/client.crt");
let ovpn = parse_ovpn(&input).unwrap();
let config = OpenVpnConfig::try_from(ovpn).unwrap();
assert_eq!(config.auth_type, Some(OpenVpnAuthType::Password));
}
}