Compare commits

...

10 Commits

Author SHA1 Message Date
Kevin Yue
6119976027 Release 2.3.0 2024-05-20 21:31:26 +08:00
Kevin Yue
a286b5e418 feat: improve client certificate authentication 2024-05-20 09:08:47 -04:00
Kevin Yue
882ab4001d chore: improve error message 2024-05-19 22:30:40 +08:00
Kevin Yue
52b6fa6fbd feat: support client certificate authentication (related #363) 2024-05-19 18:44:07 +08:00
Kevin Yue
3bb115bd2d Merge branch 'main' into dev 2024-05-19 10:23:00 +08:00
Kevin Yue
a01c55e38d fix: do not panic when failed to start service (fix #362) 2024-05-19 10:19:21 +08:00
Kevin Yue
af51bc257b feat: add the --reconnect-timeout option 2024-05-19 09:59:25 +08:00
Kevin Yue
90a8c11acb feat: add disable_ipv6 option (related #364) 2024-05-19 09:04:45 +08:00
Kevin Yue
92b858884c fix: check executable for file 2024-05-10 10:26:45 -04:00
Kevin Yue
159673652c Refactor prelogin.rs to use default labels for username and password 2024-05-09 01:48:02 -04:00
23 changed files with 614 additions and 76 deletions

View File

@ -1,6 +1,7 @@
{
"cSpell.words": [
"authcookie",
"badssl",
"bincode",
"chacha",
"clientos",
@ -25,7 +26,9 @@
"LOGNAME",
"oneshot",
"openconnect",
"pkcs",
"pkexec",
"pkey",
"Prelogin",
"prelogon",
"prelogonuserauthcookie",
@ -35,6 +38,7 @@
"rspc",
"servercert",
"specta",
"sslkey",
"sysinfo",
"tanstack",
"tauri",

32
Cargo.lock generated
View File

@ -252,6 +252,12 @@ version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -564,7 +570,7 @@ dependencies = [
[[package]]
name = "common"
version = "2.2.1"
version = "2.3.0"
dependencies = [
"is_executable",
]
@ -1430,7 +1436,7 @@ dependencies = [
[[package]]
name = "gpapi"
version = "2.2.1"
version = "2.3.0"
dependencies = [
"anyhow",
"base64 0.21.5",
@ -1440,6 +1446,8 @@ dependencies = [
"log",
"md5",
"open",
"openssl",
"pem",
"redact-engine",
"regex",
"reqwest",
@ -1462,7 +1470,7 @@ dependencies = [
[[package]]
name = "gpauth"
version = "2.2.1"
version = "2.3.0"
dependencies = [
"anyhow",
"clap",
@ -1483,7 +1491,7 @@ dependencies = [
[[package]]
name = "gpclient"
version = "2.2.1"
version = "2.3.0"
dependencies = [
"anyhow",
"clap",
@ -1505,7 +1513,7 @@ dependencies = [
[[package]]
name = "gpgui-helper"
version = "2.2.1"
version = "2.3.0"
dependencies = [
"anyhow",
"clap",
@ -1523,7 +1531,7 @@ dependencies = [
[[package]]
name = "gpservice"
version = "2.2.1"
version = "2.3.0"
dependencies = [
"anyhow",
"axum",
@ -2537,7 +2545,7 @@ dependencies = [
[[package]]
name = "openconnect"
version = "2.2.1"
version = "2.3.0"
dependencies = [
"cc",
"common",
@ -2670,6 +2678,16 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]]
name = "pem"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae"
dependencies = [
"base64 0.22.1",
"serde",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"

View File

@ -5,7 +5,7 @@ members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth", "apps/g
[workspace.package]
rust-version = "1.70"
version = "2.2.1"
version = "2.3.0"
authors = ["Kevin Yue <k3vinyue@gmail.com>"]
homepage = "https://github.com/yuezk/GlobalProtect-openconnect"
edition = "2021"
@ -22,6 +22,8 @@ is_executable = "1.0"
log = "0.4"
regex = "1"
reqwest = { version = "0.11", features = ["native-tls-vendored", "json"] }
openssl = "0.10"
pem = "3"
roxmltree = "0.18"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

View File

@ -13,6 +13,7 @@ A GUI for GlobalProtect VPN, based on OpenConnect, supports the SSO authenticati
- [x] Support both SSO and non-SSO authentication
- [x] Support the FIDO2 authentication (e.g., YubiKey)
- [x] Support authentication using default browser
- [x] Support client certificate authentication
- [x] Support multiple portals
- [x] Support gateway selection
- [x] Support connect gateway directly
@ -74,7 +75,7 @@ sudo apt-get install globalprotect-openconnect
>
> For Linux Mint, you might need to import the GPG key with: `sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 7937C393082992E5D6E4A60453FC26B43838D761` if you encountered an error `gpg: keyserver receive failed: General error`.
#### **Ubuntu 24.04**
#### **Ubuntu 24.04 and later**
The `libwebkit2gtk-4.0-37` package was [removed](https://bugs.launchpad.net/ubuntu/+source/webkit2gtk/+bug/2061914) from its repo, before [the issue](https://github.com/yuezk/GlobalProtect-openconnect/issues/351) gets resolved, you need to install them manually:

View File

@ -1,4 +1,4 @@
use std::{fs, sync::Arc};
use std::{cell::RefCell, fs, sync::Arc};
use clap::Args;
use common::vpn_utils::find_csd_wrapper;
@ -13,7 +13,7 @@ use gpapi::{
auth_launcher::SamlAuthLauncher,
users::{get_non_root_user, get_user_by_name},
},
utils::shutdown_signal,
utils::{request::RequestIdentityError, shutdown_signal},
GP_USER_AGENT,
};
use inquire::{Password, PasswordDisplayMode, Select, Text};
@ -42,14 +42,29 @@ pub(crate) struct ConnectArgs {
)]
hip: bool,
#[arg(
short,
long,
help = "Use SSL client certificate file in pkcs#8 (.pem) or pkcs#12 (.p12, .pfx) format"
)]
certificate: Option<String>,
#[arg(short = 'k', long, help = "Use SSL private key file in pkcs#8 (.pem) format")]
sslkey: Option<String>,
#[arg(short = 'p', long, help = "The key passphrase of the private key")]
key_password: Option<String>,
#[arg(long, help = "Same as the '--csd-user' option in the openconnect command")]
csd_user: Option<String>,
#[arg(long, help = "Same as the '--csd-wrapper' option in the openconnect command")]
csd_wrapper: Option<String>,
#[arg(long, default_value = "300", help = "Reconnection retry timeout in seconds")]
reconnect_timeout: u32,
#[arg(short, long, help = "Request MTU from server (legacy servers only)")]
mtu: Option<u32>,
#[arg(long, help = "Do not ask for IPv6 connectivity")]
disable_ipv6: bool,
#[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")]
user_agent: String,
@ -82,11 +97,16 @@ impl ConnectArgs {
pub(crate) struct ConnectHandler<'a> {
args: &'a ConnectArgs,
shared_args: &'a SharedArgs,
latest_key_password: RefCell<Option<String>>,
}
impl<'a> ConnectHandler<'a> {
pub(crate) fn new(args: &'a ConnectArgs, shared_args: &'a SharedArgs) -> Self {
Self { args, shared_args }
Self {
args,
shared_args,
latest_key_password: Default::default(),
}
}
fn build_gp_params(&self) -> GpParams {
@ -95,10 +115,45 @@ impl<'a> ConnectHandler<'a> {
.client_os(ClientOs::from(&self.args.os))
.os_version(self.args.os_version())
.ignore_tls_errors(self.shared_args.ignore_tls_errors)
.certificate(self.args.certificate.clone())
.sslkey(self.args.sslkey.clone())
.key_password(self.latest_key_password.borrow().clone())
.build()
}
pub(crate) async fn handle(&self) -> anyhow::Result<()> {
self.latest_key_password.replace(self.args.key_password.clone());
loop {
let Err(err) = self.handle_impl().await else {
return Ok(());
};
let Some(root_cause) = err.root_cause().downcast_ref::<RequestIdentityError>() else {
return Err(err);
};
match root_cause {
RequestIdentityError::NoKey => {
eprintln!("ERROR: No private key found in the certificate file");
eprintln!("ERROR: Please provide the private key file using the `-k` option");
return Ok(());
}
RequestIdentityError::NoPassphrase(cert_type) | RequestIdentityError::DecryptError(cert_type) => {
// Decrypt the private key error, ask for the key password
let message = format!("Enter the {} passphrase:", cert_type);
let password = Password::new(&message)
.without_confirmation()
.with_display_mode(PasswordDisplayMode::Masked)
.prompt()?;
self.latest_key_password.replace(Some(password));
}
}
}
}
pub(crate) async fn handle_impl(&self) -> anyhow::Result<()> {
let server = self.args.server.as_str();
let as_gateway = self.args.as_gateway;
@ -213,9 +268,14 @@ impl<'a> ConnectHandler<'a> {
let vpn = Vpn::builder(gateway, cookie)
.script(self.args.script.clone())
.user_agent(self.args.user_agent.clone())
.certificate(self.args.certificate.clone())
.sslkey(self.args.sslkey.clone())
.key_password(self.latest_key_password.borrow().clone())
.csd_uid(csd_uid)
.csd_wrapper(csd_wrapper)
.reconnect_timeout(self.args.reconnect_timeout)
.mtu(mtu)
.disable_ipv6(self.args.disable_ipv6)
.build()?;
let vpn = Arc::new(vpn);

View File

@ -38,10 +38,15 @@ impl VpnTaskContext {
let vpn = match Vpn::builder(req.gateway().server(), args.cookie())
.script(args.vpnc_script())
.user_agent(args.user_agent())
.os(args.openconnect_os())
.certificate(args.certificate())
.sslkey(args.sslkey())
.key_password(args.key_password())
.csd_uid(args.csd_uid())
.csd_wrapper(args.csd_wrapper())
.reconnect_timeout(args.reconnect_timeout())
.mtu(args.mtu())
.os(args.openconnect_os())
.disable_ipv6(args.disable_ipv6())
.build()
{
Ok(vpn) => vpn,

View File

@ -1,5 +1,11 @@
# Changelog
## 2.3.0 - 2024-05-20
- Support client certificate authentication (fix [#363](https://github.com/yuezk/GlobalProtect-openconnect/issues/363))
- Support `--disable-ipv6`, `--reconnect-timeout` parameters (related: [#364](https://github.com/yuezk/GlobalProtect-openconnect/issues/364))
- Use default labels if label fields are missing in prelogin response (fix [#357](https://github.com/yuezk/GlobalProtect-openconnect/issues/357))
## 2.2.1 - 2024-05-07
- GUI: Restore the default browser auth implementation (fix [#360](https://github.com/yuezk/GlobalProtect-openconnect/issues/360))

View File

@ -1,7 +1,6 @@
use is_executable::IsExecutable;
use std::path::Path;
use std::{io, path::Path};
pub use is_executable::is_executable;
use is_executable::IsExecutable;
const VPNC_SCRIPT_LOCATIONS: [&str; 6] = [
"/usr/local/share/vpnc-scripts/vpnc-script",
@ -39,3 +38,17 @@ pub fn find_vpnc_script() -> Option<String> {
pub fn find_csd_wrapper() -> Option<String> {
find_executable(&CSD_WRAPPER_LOCATIONS)
}
/// If file exists, check if it is executable
pub fn check_executable(file: &str) -> Result<(), io::Error> {
let path = Path::new(file);
if path.exists() && !path.is_executable() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("{} is not executable", file),
));
}
Ok(())
}

View File

@ -9,6 +9,8 @@ anyhow.workspace = true
base64.workspace = true
log.workspace = true
reqwest.workspace = true
openssl.workspace = true
pem.workspace = true
roxmltree.workspace = true
serde.workspace = true
specta.workspace = true

View File

@ -156,11 +156,7 @@ fn build_csd_token(cookie: &str) -> anyhow::Result<String> {
}
pub async fn hip_report(gateway: &str, cookie: &str, csd_wrapper: &str, gp_params: &GpParams) -> anyhow::Result<()> {
let client = Client::builder()
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
.user_agent(gp_params.user_agent())
.build()?;
let client = Client::try_from(gp_params)?;
let md5 = build_csd_token(cookie)?;
info!("Submit HIP report md5: {}", md5);

View File

@ -21,10 +21,7 @@ pub async fn gateway_login(gateway: &str, cred: &Credential, gp_params: &GpParam
let gateway = remove_url_scheme(&url);
let login_url = format!("{}/ssl-vpn/login.esp", url);
let client = Client::builder()
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
.user_agent(gp_params.user_agent())
.build()?;
let client = Client::try_from(gp_params)?;
let mut params = cred.to_params();
let extra_params = gp_params.to_params();

View File

@ -1,9 +1,11 @@
use std::collections::HashMap;
use log::info;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use specta::Type;
use crate::GP_USER_AGENT;
use crate::{utils::request::create_identity, GP_USER_AGENT};
#[derive(Debug, Serialize, Deserialize, Clone, Type, Default)]
pub enum ClientOs {
@ -51,6 +53,9 @@ pub struct GpParams {
client_version: Option<String>,
computer: String,
ignore_tls_errors: bool,
certificate: Option<String>,
sslkey: Option<String>,
key_password: Option<String>,
// Used for MFA
input_str: Option<String>,
otp: Option<String>,
@ -142,6 +147,9 @@ pub struct GpParamsBuilder {
client_version: Option<String>,
computer: String,
ignore_tls_errors: bool,
certificate: Option<String>,
sslkey: Option<String>,
key_password: Option<String>,
}
impl GpParamsBuilder {
@ -156,6 +164,9 @@ impl GpParamsBuilder {
client_version: Default::default(),
computer,
ignore_tls_errors: false,
certificate: Default::default(),
sslkey: Default::default(),
key_password: Default::default(),
}
}
@ -194,6 +205,21 @@ impl GpParamsBuilder {
self
}
pub fn certificate<T: Into<Option<String>>>(&mut self, certificate: T) -> &mut Self {
self.certificate = certificate.into();
self
}
pub fn sslkey<T: Into<Option<String>>>(&mut self, sslkey: T) -> &mut Self {
self.sslkey = sslkey.into();
self
}
pub fn key_password<T: Into<Option<String>>>(&mut self, password: T) -> &mut Self {
self.key_password = password.into();
self
}
pub fn build(&self) -> GpParams {
GpParams {
is_gateway: self.is_gateway,
@ -203,6 +229,9 @@ impl GpParamsBuilder {
client_version: self.client_version.clone(),
computer: self.computer.clone(),
ignore_tls_errors: self.ignore_tls_errors,
certificate: self.certificate.clone(),
sslkey: self.sslkey.clone(),
key_password: self.key_password.clone(),
input_str: Default::default(),
otp: Default::default(),
}
@ -214,3 +243,22 @@ impl Default for GpParamsBuilder {
Self::new()
}
}
impl TryFrom<&GpParams> for Client {
type Error = anyhow::Error;
fn try_from(value: &GpParams) -> Result<Self, Self::Error> {
let mut builder = Client::builder()
.danger_accept_invalid_certs(value.ignore_tls_errors)
.user_agent(&value.user_agent);
if let Some(cert) = value.certificate.as_deref() {
info!("Using client certificate authentication...");
let identity = create_identity(cert, value.sslkey.as_deref(), value.key_password.as_deref())?;
builder = builder.identity(identity);
}
let client = builder.build()?;
Ok(client)
}
}

View File

@ -88,10 +88,7 @@ pub async fn retrieve_config(portal: &str, cred: &Credential, gp_params: &GpPara
let server = remove_url_scheme(&portal);
let url = format!("{}/global-protect/getconfig.esp", portal);
let client = Client::builder()
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
.user_agent(gp_params.user_agent())
.build()?;
let client = Client::try_from(gp_params)?;
let mut params = cred.to_params();
let extra_params = gp_params.to_params();

View File

@ -114,10 +114,7 @@ pub async fn prelogin(portal: &str, gp_params: &GpParams) -> anyhow::Result<Prel
params.retain(|k, _| REQUIRED_PARAMS.iter().any(|required_param| required_param == k));
let client = Client::builder()
.danger_accept_invalid_certs(gp_params.ignore_tls_errors())
.user_agent(user_agent)
.build()?;
let client = Client::try_from(gp_params)?;
let res = client
.post(&prelogin_url)
@ -181,22 +178,24 @@ fn parse_res_xml(res_xml: &str, is_gateway: bool) -> anyhow::Result<Prelogin> {
return Ok(Prelogin::Saml(saml_prelogin));
}
let label_username = xml::get_child_text(&doc, "username-label");
let label_password = xml::get_child_text(&doc, "password-label");
// Check if the prelogin response is standard login
if label_username.is_some() && label_password.is_some() {
let auth_message =
xml::get_child_text(&doc, "authentication-message").unwrap_or(String::from("Please enter the login credentials"));
let standard_prelogin = StandardPrelogin {
region,
is_gateway,
auth_message,
label_username: label_username.unwrap(),
label_password: label_password.unwrap(),
};
let label_username = xml::get_child_text(&doc, "username-label").unwrap_or_else(|| {
info!("Username label has no value, using default");
String::from("Username")
});
let label_password = xml::get_child_text(&doc, "password-label").unwrap_or_else(|| {
info!("Password label has no value, using default");
String::from("Password")
});
Ok(Prelogin::Standard(standard_prelogin))
} else {
Err(anyhow!("Invalid prelogin response"))
}
let auth_message =
xml::get_child_text(&doc, "authentication-message").unwrap_or(String::from("Please enter the login credentials"));
let standard_prelogin = StandardPrelogin {
region,
is_gateway,
auth_message,
label_username,
label_password,
};
Ok(Prelogin::Standard(standard_prelogin))
}

View File

@ -32,10 +32,15 @@ pub struct ConnectArgs {
cookie: String,
vpnc_script: Option<String>,
user_agent: Option<String>,
os: Option<ClientOs>,
certificate: Option<String>,
sslkey: Option<String>,
key_password: Option<String>,
csd_uid: u32,
csd_wrapper: Option<String>,
reconnect_timeout: u32,
mtu: u32,
os: Option<ClientOs>,
disable_ipv6: bool,
}
impl ConnectArgs {
@ -45,9 +50,14 @@ impl ConnectArgs {
vpnc_script: None,
user_agent: None,
os: None,
certificate: None,
sslkey: None,
key_password: None,
csd_uid: 0,
csd_wrapper: None,
reconnect_timeout: 300,
mtu: 0,
disable_ipv6: false,
}
}
@ -67,6 +77,18 @@ impl ConnectArgs {
self.os.as_ref().map(|os| os.to_openconnect_os().to_string())
}
pub fn certificate(&self) -> Option<String> {
self.certificate.clone()
}
pub fn sslkey(&self) -> Option<String> {
self.sslkey.clone()
}
pub fn key_password(&self) -> Option<String> {
self.key_password.clone()
}
pub fn csd_uid(&self) -> u32 {
self.csd_uid
}
@ -75,9 +97,17 @@ impl ConnectArgs {
self.csd_wrapper.clone()
}
pub fn reconnect_timeout(&self) -> u32 {
self.reconnect_timeout
}
pub fn mtu(&self) -> u32 {
self.mtu
}
pub fn disable_ipv6(&self) -> bool {
self.disable_ipv6
}
}
#[derive(Debug, Deserialize, Serialize, Type)]
@ -109,11 +139,6 @@ impl ConnectRequest {
self
}
pub fn with_mtu(mut self, mtu: u32) -> Self {
self.args.mtu = mtu;
self
}
pub fn with_user_agent<T: Into<Option<String>>>(mut self, user_agent: T) -> Self {
self.args.user_agent = user_agent.into();
self
@ -124,6 +149,36 @@ impl ConnectRequest {
self
}
pub fn with_certificate<T: Into<Option<String>>>(mut self, certificate: T) -> Self {
self.args.certificate = certificate.into();
self
}
pub fn with_sslkey<T: Into<Option<String>>>(mut self, sslkey: T) -> Self {
self.args.sslkey = sslkey.into();
self
}
pub fn with_key_password<T: Into<Option<String>>>(mut self, key_password: T) -> Self {
self.args.key_password = key_password.into();
self
}
pub fn with_reconnect_timeout(mut self, reconnect_timeout: u32) -> Self {
self.args.reconnect_timeout = reconnect_timeout;
self
}
pub fn with_mtu(mut self, mtu: u32) -> Self {
self.args.mtu = mtu;
self
}
pub fn with_disable_ipv6(mut self, disable_ipv6: bool) -> Self {
self.args.disable_ipv6 = disable_ipv6;
self
}
pub fn gateway(&self) -> &Gateway {
self.info.gateway()
}

View File

@ -8,6 +8,7 @@ pub mod env_file;
pub mod lock_file;
pub mod openssl;
pub mod redact;
pub mod request;
#[cfg(feature = "tauri")]
pub mod window;

View File

@ -0,0 +1,140 @@
use std::{borrow::Cow, fs};
use anyhow::bail;
use log::warn;
use openssl::pkey::PKey;
use pem::parse_many;
use reqwest::Identity;
#[derive(Debug, thiserror::Error)]
pub enum RequestIdentityError {
#[error("Failed to find the private key")]
NoKey,
#[error("No passphrase provided")]
NoPassphrase(&'static str),
#[error("Failed to decrypt private key")]
DecryptError(&'static str),
}
/// Create an identity object from a certificate and key
/// The file is expected to be the PKCS#8 PEM or PKCS#12 format
/// When using a PKCS#12 file, the key is NOT required, but a passphrase is required
pub fn create_identity(cert: &str, key: Option<&str>, passphrase: Option<&str>) -> anyhow::Result<Identity> {
if cert.ends_with(".p12") || cert.ends_with(".pfx") {
create_identity_from_pkcs12(cert, passphrase)
} else {
create_identity_from_pem(cert, key, passphrase)
}
}
fn create_identity_from_pem(cert: &str, key: Option<&str>, passphrase: Option<&str>) -> anyhow::Result<Identity> {
let cert_pem = fs::read(cert).map_err(|err| anyhow::anyhow!("Failed to read certificate file: {}", err))?;
// Use the certificate as the key if no key is provided
let key_pem_file = match key {
Some(key) => Cow::Owned(fs::read(key).map_err(|err| anyhow::anyhow!("Failed to read key file: {}", err))?),
None => Cow::Borrowed(&cert_pem),
};
// Find the private key in the pem file
let key_pem = parse_many(key_pem_file.as_ref())?
.into_iter()
.find(|pem| pem.tag().ends_with("PRIVATE KEY"))
.ok_or(RequestIdentityError::NoKey)?;
// The key pem could be encrypted, so we need to decrypt it
let decrypted_key_pem = if key_pem.tag().ends_with("ENCRYPTED PRIVATE KEY") {
let passphrase = passphrase.ok_or_else(|| {
warn!("Key is encrypted but no passphrase provided");
RequestIdentityError::NoPassphrase("PEM")
})?;
let pem_content = pem::encode(&key_pem);
let key = PKey::private_key_from_pem_passphrase(pem_content.as_bytes(), passphrase.as_bytes()).map_err(|err| {
warn!("Failed to decrypt key: {}", err);
RequestIdentityError::DecryptError("PEM")
})?;
key.private_key_to_pem_pkcs8()?
} else {
pem::encode(&key_pem).into()
};
let identity = Identity::from_pkcs8_pem(&cert_pem, &decrypted_key_pem)?;
Ok(identity)
}
fn create_identity_from_pkcs12(pkcs12: &str, passphrase: Option<&str>) -> anyhow::Result<Identity> {
let pkcs12 = fs::read(pkcs12)?;
let Some(passphrase) = passphrase else {
bail!(RequestIdentityError::NoPassphrase("PKCS#12"));
};
let identity = Identity::from_pkcs12_der(&pkcs12, passphrase)?;
Ok(identity)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_identity_from_pem_requires_passphrase() {
let cert = "tests/files/badssl.com-client.pem";
let identity = create_identity_from_pem(cert, None, None);
assert!(identity.is_err());
assert!(identity.unwrap_err().to_string().contains("No passphrase provided"));
}
#[test]
fn create_identity_from_pem_with_passphrase() {
let cert = "tests/files/badssl.com-client.pem";
let passphrase = "badssl.com";
let identity = create_identity_from_pem(cert, None, Some(passphrase));
assert!(identity.is_ok());
}
#[test]
fn create_identity_from_pem_unencrypted_key() {
let cert = "tests/files/badssl.com-client-unencrypted.pem";
let identity = create_identity_from_pem(cert, None, None);
println!("{:?}", identity);
assert!(identity.is_ok());
}
#[test]
fn create_identity_from_pem_cert_and_encrypted_key() {
let cert = "tests/files/badssl.com-client.pem";
let key = "tests/files/badssl.com-client.pem";
let passphrase = "badssl.com";
let identity = create_identity_from_pem(cert, Some(key), Some(passphrase));
assert!(identity.is_ok());
}
#[test]
fn create_identity_from_pem_cert_and_encrypted_key_no_passphrase() {
let cert = "tests/files/badssl.com-client.pem";
let key = "tests/files/badssl.com-client.pem";
let identity = create_identity_from_pem(cert, Some(key), None);
assert!(identity.is_err());
assert!(identity.unwrap_err().to_string().contains("No passphrase provided"));
}
#[test]
fn create_identity_from_pem_cert_and_unencrypted_key() {
let cert = "tests/files/badssl.com-client.pem";
let key = "tests/files/badssl.com-client-unencrypted.pem";
let identity = create_identity_from_pem(cert, Some(key), None);
assert!(identity.is_ok());
}
}

View File

@ -0,0 +1,62 @@
Bag Attributes
localKeyID: AE DC 75 2E 97 28 71 D8 1E 9A 7F 1E 5A AA F4 2E D3 6D 2C 8B
subject=/C=US/ST=California/L=San Francisco/O=BadSSL/CN=BadSSL Client Certificate
issuer=/C=US/ST=California/L=San Francisco/O=BadSSL/CN=BadSSL Client Root Certificate Authority
-----BEGIN CERTIFICATE-----
MIIEnTCCAoWgAwIBAgIJAPfJjkenM2ooMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp
c2NvMQ8wDQYDVQQKDAZCYWRTU0wxMTAvBgNVBAMMKEJhZFNTTCBDbGllbnQgUm9v
dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjQwNTE3MTc1OTMyWhcNMjYwNTE3
MTc1OTMyWjBvMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQG
A1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0GA1UECgwGQmFkU1NMMSIwIAYDVQQDDBlC
YWRTU0wgQ2xpZW50IENlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAxzdfEeseTs/rukjly6MSLHM+Rh0enA3Ai4Mj2sdl31x3SbPoen08
utVhjPmlxIUdkiMG4+ffe7N+JtDLG75CaxZp9CxytX7kywooRBJsRnQhmQPca8MR
WAJBIz+w/L+3AFkTIqWBfyT+1VO8TVKPkEpGdLDovZOmzZAASi9/sj+j6gM7AaCi
DeZTf2ES66abA5pOp60Q6OEdwg/vCUJfarhKDpi9tj3P6qToy9Y4DiBUhOct4MG8
w5XwmKAC+Vfm8tb7tMiUoU0yvKKOcL6YXBXxB2kPcOYxYNobXavfVBEdwSrjQ7i/
s3o6hkGQlm9F7JPEuVgbl/Jdwa64OYIqjQIDAQABoy0wKzAJBgNVHRMEAjAAMBEG
CWCGSAGG+EIBAQQEAwIHgDALBgNVHQ8EBAMCBeAwDQYJKoZIhvcNAQELBQADggIB
AE6iDW5Lv5I0bJY6TGxJUoB4rcsbbtEP4O4MT14GP7j7I48V09VBG9yjskYze0Ls
Xb9mQpEpPyQLTDJIWu/ic/y5SMnelCjUxmfl37cfNLJajQZxc4FDEUSemrPKpEkB
UzHNkxw9LSzqsyxnQmMIGoN+ZNCFoV7s5pekzPfgZj5+s7a+oiF/AzhOWZzF7vaM
aclX7KCeENQV+q0giDjsGIHI6BevUHYkglocEqff+rIDHjjLxHLPooflV50M+ifc
4uJdHgG8hwKxd1uf3LImUsquiBrW5CO6KCgwLrtQNe11pQHpY0urZxK/tnAj7QtD
v/O1ryd/3+b0Gx14TyulMtcaLHsE94ppwjcxpYGNcyH+M39OMihuR2aqmkrqcZd/
VWop1cNwZgPtCNVvfivRpX52NLI5I0eMfs6jeTMr719hdAby3akoiNLN3YNKrdrp
pyRz/sUFGO8AHHECXA15KTeMBNfZnO32ZAZ4jHyyDBO1A5f9iDbErhXfIpeRCrCO
gM9MLuO4YEMG1Skp+qaw7SIaG+oi2t4lbVRr3LOv0Hfkjjb7bVjfWSwLBPH/gv0E
ZL6G0p7PjeoCh4obS3Y1yxfNlPR6RQwWl1wve+Nkmf5sDCmgr3P0512ZuvqkbKkB
/syiAWDsYzFuq2Ntv2ljTYPEPwXEIQcpsagDRL6WzoLR
-----END CERTIFICATE-----
Bag Attributes
localKeyID: AE DC 75 2E 97 28 71 D8 1E 9A 7F 1E 5A AA F4 2E D3 6D 2C 8B
Key Attributes: <No Attributes>
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDHN18R6x5Oz+u6
SOXLoxIscz5GHR6cDcCLgyPax2XfXHdJs+h6fTy61WGM+aXEhR2SIwbj5997s34m
0MsbvkJrFmn0LHK1fuTLCihEEmxGdCGZA9xrwxFYAkEjP7D8v7cAWRMipYF/JP7V
U7xNUo+QSkZ0sOi9k6bNkABKL3+yP6PqAzsBoKIN5lN/YRLrppsDmk6nrRDo4R3C
D+8JQl9quEoOmL22Pc/qpOjL1jgOIFSE5y3gwbzDlfCYoAL5V+by1vu0yJShTTK8
oo5wvphcFfEHaQ9w5jFg2htdq99UER3BKuNDuL+zejqGQZCWb0Xsk8S5WBuX8l3B
rrg5giqNAgMBAAECggEAVRB/t9b9igmeTlzyQpHPIMvUu3uTpm742JmWpcSe61FA
XmhDzInNdLnIfbnb3p44kj4Coy5PbzKlm01sbNxA4BkiBPE1yen1J/2eU/LJ6QuN
jRjo9drFfR75UWPQ3xu9uJhQY2rocLILXmvy69FlG+ebThh8SPbTMtNaTFMb47An
pk2FrW9+rzPswbklOxls/SDt78usRvfAjslm73IdBTOrbceF+GmYs3/SXz1gu05p
LxY2rhC8piBlqnD/QbXBahZbhjb9SkDFn2typMFZKkJIIKDJaOI2E9tIlZ97/0nZ
txqchMty8IuU9YYAfLXCmj2IEfnvLtL7thLfKLuWAQKBgQDyXBpEgKFzfy2a1AI0
+1qL/u5UN14l7S6/wmyDTgVMXwoxhwPRXWD5PutQ8D6tMfC/y4AYt3OXg1blCvLD
XysNj5SK+dpmQR0SyeWjd9zwxJAXvx0McJefCYd86YGcGhJsuX5bkHIeQlEc6df7
yoqr1480VQx/+Fk1i6Zr0EIUFQKBgQDSbalUOfXZh2EVRQEgf3VoPlxAiwGGQcVT
i+pbjMG3pOwmkVyJZusGtN5HN4Oi7n1oiyfMYGsszKQ5j4TDBGS70pNUzhTv3Vn8
0Vsfz0arJRqJxviiv4FfDmsYXwObNKwOjR+LEn1NUPkOYOLdz1lDuWOu11LE90Dy
Q6hg8WwCmQKBgQDTy5lI9AAjpqh7/XpQQrhGT2qHPjuQeU25Vnbt6GjI7OVDkvHL
LQdpyYprGQgs4s+5TGWNNARYC/cMAh1Ujv5Yw3jUWrR5V73IhZeg20bBQYWKuwDv
thVKblFw377cZAxl51R9QCX6O4oW8mRFLiMxORd0bD6YNrf/CyNMZJraYQKBgAE7
o0JbFJWxtV/qh5cpKAb0VpYKOngO6pkSuMzQhlINJVUUhPZJJBdl9+dy69KIkzOJ
nTIVXotkp5GuxZhe7jgrg7F7g6PkKCLTFzWYgVF/ZihoggxyEs/7xaTe6aZ/KILt
UMH/2bwaPVtYNfwWuu8qpurfWBzPVhIVU2c+AuQBAoGAXMbw10vyiznlhyMFw5kx
SzlBMqJBLJkzQBtpvXuT0lqqxTSNC3N4WxgVOLCHa6HqXiB0790YL8/RWunsXTk2
c7ugThP6iMPNVAycWkIF4vvHTwZ9RCSmEQabRaqGGLz/bhLL3fi3lPGCR+iW2Dxq
GTH3fhaM/pZZGdIC75x/69Y=
-----END PRIVATE KEY-----

View File

@ -0,0 +1,64 @@
Bag Attributes
localKeyID: AE DC 75 2E 97 28 71 D8 1E 9A 7F 1E 5A AA F4 2E D3 6D 2C 8B
subject=/C=US/ST=California/L=San Francisco/O=BadSSL/CN=BadSSL Client Certificate
issuer=/C=US/ST=California/L=San Francisco/O=BadSSL/CN=BadSSL Client Root Certificate Authority
-----BEGIN CERTIFICATE-----
MIIEnTCCAoWgAwIBAgIJAPfJjkenM2ooMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp
c2NvMQ8wDQYDVQQKDAZCYWRTU0wxMTAvBgNVBAMMKEJhZFNTTCBDbGllbnQgUm9v
dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjQwNTE3MTc1OTMyWhcNMjYwNTE3
MTc1OTMyWjBvMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQG
A1UEBwwNU2FuIEZyYW5jaXNjbzEPMA0GA1UECgwGQmFkU1NMMSIwIAYDVQQDDBlC
YWRTU0wgQ2xpZW50IENlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAxzdfEeseTs/rukjly6MSLHM+Rh0enA3Ai4Mj2sdl31x3SbPoen08
utVhjPmlxIUdkiMG4+ffe7N+JtDLG75CaxZp9CxytX7kywooRBJsRnQhmQPca8MR
WAJBIz+w/L+3AFkTIqWBfyT+1VO8TVKPkEpGdLDovZOmzZAASi9/sj+j6gM7AaCi
DeZTf2ES66abA5pOp60Q6OEdwg/vCUJfarhKDpi9tj3P6qToy9Y4DiBUhOct4MG8
w5XwmKAC+Vfm8tb7tMiUoU0yvKKOcL6YXBXxB2kPcOYxYNobXavfVBEdwSrjQ7i/
s3o6hkGQlm9F7JPEuVgbl/Jdwa64OYIqjQIDAQABoy0wKzAJBgNVHRMEAjAAMBEG
CWCGSAGG+EIBAQQEAwIHgDALBgNVHQ8EBAMCBeAwDQYJKoZIhvcNAQELBQADggIB
AE6iDW5Lv5I0bJY6TGxJUoB4rcsbbtEP4O4MT14GP7j7I48V09VBG9yjskYze0Ls
Xb9mQpEpPyQLTDJIWu/ic/y5SMnelCjUxmfl37cfNLJajQZxc4FDEUSemrPKpEkB
UzHNkxw9LSzqsyxnQmMIGoN+ZNCFoV7s5pekzPfgZj5+s7a+oiF/AzhOWZzF7vaM
aclX7KCeENQV+q0giDjsGIHI6BevUHYkglocEqff+rIDHjjLxHLPooflV50M+ifc
4uJdHgG8hwKxd1uf3LImUsquiBrW5CO6KCgwLrtQNe11pQHpY0urZxK/tnAj7QtD
v/O1ryd/3+b0Gx14TyulMtcaLHsE94ppwjcxpYGNcyH+M39OMihuR2aqmkrqcZd/
VWop1cNwZgPtCNVvfivRpX52NLI5I0eMfs6jeTMr719hdAby3akoiNLN3YNKrdrp
pyRz/sUFGO8AHHECXA15KTeMBNfZnO32ZAZ4jHyyDBO1A5f9iDbErhXfIpeRCrCO
gM9MLuO4YEMG1Skp+qaw7SIaG+oi2t4lbVRr3LOv0Hfkjjb7bVjfWSwLBPH/gv0E
ZL6G0p7PjeoCh4obS3Y1yxfNlPR6RQwWl1wve+Nkmf5sDCmgr3P0512ZuvqkbKkB
/syiAWDsYzFuq2Ntv2ljTYPEPwXEIQcpsagDRL6WzoLR
-----END CERTIFICATE-----
Bag Attributes
localKeyID: AE DC 75 2E 97 28 71 D8 1E 9A 7F 1E 5A AA F4 2E D3 6D 2C 8B
Key Attributes: <No Attributes>
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIET6L0Ht/lYgCAggA
MBQGCCqGSIb3DQMHBAi1Xo+JdQ6XvwSCBMgX20Fk3/GzptJ0zjl7ZqX2G3J4LIkM
E5qJ4yv2WUkCCOWqz5DjlSrRz4kdCYHqnM1/qyrLa1UWWJlNQ9lBHTE+yp0vtAC/
ajQfKt3RFyGxblp6nEKJI7kvhmQHDbITilmVEpcZPbci3gi7asQI3bRSLaHwGtbH
DY+8hJ8lZQMRjYGDyGb99qEdYnMMRMW+b44lIRASe6W3EUfrvJlp+OUqRA7hJzn2
yha9Zo8KWo9fA9UZDFFKNlXakg76+1HymB+uqvZl14xHHfwhlKPaqzmCb8MUtt7e
YJDB9I3y8aHKExXPbRk04bbY5G9o6WdWslDUY4axOZuhUXyn0h6cTZn//qmsjcBH
499+j55vW6W7vkMfurt/pmLIBWC9kDWPZVLizbfXxWiWvRmQvKPfzO5TU8oObYyJ
qUVjb3Vpa/WPrF5APUVd/DDofurgzdOkmDGomONPvSHxahHSyEZsxpnl52GD6uU/
i3oa5qLE9uA1QjyX6wyN9SU5wE2FZKTJwwRJwW4+s4T/2eJjhuJez5q1xhSCes4A
A2pufAAY/ctQSmCCKCTW+EkrXtcezx66fkgPpNK/m6bz5KGJkA4QXjl8A05PDAFE
Z68VOX/T0IGfXc2BbPgP0u+WpCvvO2cW/pU4sjcwOMxFuT1Bn3TwmDLTZ+zba1rE
zFRMMCz/8SKq3I+VkzQ66ureEz0RLwk07JVzE9AJUEm+zCFUdoIaz09OMGVqtf4a
V+UgupH0QlffmRNJKQtXPuj6Wjfa43GLaCnN/cpXXq8+2o81dLTsCbEsYu+8DRjC
B0iyjzdqgjBBYurIEwEc4iGtPt4Y+4rgAJcpEUgwvWii37xyutOC9V7ansvd6zg3
WXiX5Ktj/qS0EzM33WtZfx7jygJIf1MvxrJU+D+HgGii1mHaZ6bHxMX3QGpRsEvh
IzBx16XvoHcXARZJG91bC+K1sJ6e05L1PevS7gj4heJTEhtmvABUrn9O1n5fZWPj
Q81zRDgplMO7r8aBW/pE+sj4VSTMg0Xu0nlqqvQoWxr9YFcJm0+I9fHQPxewnRus
sBZoiTqnWqbTr+uRATRUAp+hU03S4jGZwbzH4ylL2hr/TshGVJk/olBsULAfIiHa
dA5H258IEwAoFO6zgI9AvqmTFo3Mnpqb/AS/HuDmmS/3Ud1EF8hFsMLPcV0JdSTY
Dl4xgZ6j6jOUlTN5Yt6To2Zg3Q9Bm6qytFaffEP66Jl5aWhksI31Fz/ihzn5wfx9
xh91U8+kGVNrpYHlo5y3FR/ywSXynLkJffCbfUciEaTDv9i0JppoIVXyFqcMofHe
GUsWTCozAW3O8MwpLaJxcNcfRq0DWziIdiDgbF2tPoCqnNxXtLYSPpdt3jNDcPcx
U0Z6ep6FnAXiujtQRSRSP3Ssq23098BxDSM9+eashFOmSbSClAEEn/THRxTp/gMh
zmD8kpX1zN1Cm/lerTGjrGjnkXcQ7LY76/+C1uT+tQbw5LjmCfFEYTFtnFyYFlF1
GiXFokh9SdLaCzW4vmZok85Fe+7VZ7BAchBTfTIMKlXKmeouf3YVYJ8glPsinrjb
cB2pKv3tVrdQwo3moYDwSsDgkd7BNKKHDVdY2O6NgX4/Fyd6pZt7ZAphyC1giEqg
pPo=
-----END ENCRYPTED PRIVATE KEY-----

View File

@ -14,12 +14,16 @@ pub(crate) struct ConnectOptions {
pub script: *const c_char,
pub os: *const c_char,
pub certificate: *const c_char,
pub sslkey: *const c_char,
pub key_password: *const c_char,
pub servercert: *const c_char,
pub csd_uid: u32,
pub csd_wrapper: *const c_char,
pub reconnect_timeout: u32,
pub mtu: u32,
pub disable_ipv6: u32,
}
#[link(name = "vpn")]

View File

@ -63,7 +63,11 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback)
INFO("OS: %s", options->os);
INFO("CSD_USER: %d", options->csd_uid);
INFO("CSD_WRAPPER: %s", options->csd_wrapper);
INFO("CERTIFICATE: %s", options->certificate);
INFO("SSLKEY: %s", options->sslkey);
INFO("RECONNECT_TIMEOUT: %d", options->reconnect_timeout);
INFO("MTU: %d", options->mtu);
INFO("DISABLE_IPV6: %d", options->disable_ipv6);
vpninfo = openconnect_vpninfo_new(options->user_agent, validate_peer_cert, NULL, NULL, print_progress, NULL);
@ -78,6 +82,11 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback)
openconnect_set_protocol(vpninfo, "gp");
openconnect_set_hostname(vpninfo, options->server);
openconnect_set_cookie(vpninfo, options->cookie);
openconnect_set_client_cert(vpninfo, options->certificate, options->sslkey);
if (options->key_password) {
openconnect_set_key_password(vpninfo, options->key_password);
}
if (options->os) {
openconnect_set_reported_os(vpninfo, options->os);
@ -103,6 +112,10 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback)
openconnect_set_reqmtu(vpninfo, mtu);
}
if (options->disable_ipv6) {
openconnect_disable_ipv6(vpninfo);
}
g_cmd_pipe_fd = openconnect_setup_cmd_pipe(vpninfo);
if (g_cmd_pipe_fd < 0)
{
@ -132,7 +145,7 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback)
while (1)
{
int ret = openconnect_mainloop(vpninfo, 300, 10);
int ret = openconnect_mainloop(vpninfo, options->reconnect_timeout, 10);
if (ret)
{

View File

@ -15,12 +15,16 @@ typedef struct vpn_options
const char *script;
const char *os;
const char *certificate;
const char *sslkey;
const char *key_password;
const char *servercert;
const uid_t csd_uid;
const char *csd_wrapper;
const int reconnect_timeout;
const int mtu;
const int disable_ipv6;
} vpn_options;
int vpn_connect(const vpn_options *options, vpn_connected_callback callback);
@ -35,7 +39,7 @@ static char *format_message(const char *format, va_list args)
int len = vsnprintf(NULL, 0, format, args_copy);
va_end(args_copy);
char *buffer = malloc(len + 1);
char *buffer = (char*)malloc(len + 1);
if (buffer == NULL)
{
return NULL;

View File

@ -4,7 +4,7 @@ use std::{
sync::{Arc, RwLock},
};
use common::vpn_utils::{find_vpnc_script, is_executable};
use common::vpn_utils::{check_executable, find_vpnc_script};
use log::info;
use crate::ffi;
@ -18,12 +18,16 @@ pub struct Vpn {
script: CString,
os: CString,
certificate: Option<CString>,
sslkey: Option<CString>,
key_password: Option<CString>,
servercert: Option<CString>,
csd_uid: u32,
csd_wrapper: Option<CString>,
reconnect_timeout: u32,
mtu: u32,
disable_ipv6: bool,
callback: OnConnectedCallback,
}
@ -61,13 +65,18 @@ impl Vpn {
user_agent: self.user_agent.as_ptr(),
script: self.script.as_ptr(),
os: self.os.as_ptr(),
certificate: Self::option_to_ptr(&self.certificate),
sslkey: Self::option_to_ptr(&self.sslkey),
key_password: Self::option_to_ptr(&self.key_password),
servercert: Self::option_to_ptr(&self.servercert),
csd_uid: self.csd_uid,
csd_wrapper: Self::option_to_ptr(&self.csd_wrapper),
reconnect_timeout: self.reconnect_timeout,
mtu: self.mtu,
disable_ipv6: self.disable_ipv6 as u32,
}
}
@ -80,23 +89,23 @@ impl Vpn {
}
#[derive(Debug)]
pub struct VpnError<'a> {
message: &'a str,
pub struct VpnError {
message: String,
}
impl<'a> VpnError<'a> {
fn new(message: &'a str) -> Self {
impl VpnError {
fn new(message: String) -> Self {
Self { message }
}
}
impl fmt::Display for VpnError<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
impl fmt::Display for VpnError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for VpnError<'_> {}
impl std::error::Error for VpnError {}
pub struct VpnBuilder {
server: String,
@ -106,10 +115,16 @@ pub struct VpnBuilder {
user_agent: Option<String>,
os: Option<String>,
certificate: Option<String>,
sslkey: Option<String>,
key_password: Option<String>,
csd_uid: u32,
csd_wrapper: Option<String>,
reconnect_timeout: u32,
mtu: u32,
disable_ipv6: bool,
}
impl VpnBuilder {
@ -122,10 +137,16 @@ impl VpnBuilder {
user_agent: None,
os: None,
certificate: None,
sslkey: None,
key_password: None,
csd_uid: 0,
csd_wrapper: None,
reconnect_timeout: 300,
mtu: 0,
disable_ipv6: false,
}
}
@ -144,6 +165,21 @@ impl VpnBuilder {
self
}
pub fn certificate<T: Into<Option<String>>>(mut self, certificate: T) -> Self {
self.certificate = certificate.into();
self
}
pub fn sslkey<T: Into<Option<String>>>(mut self, sslkey: T) -> Self {
self.sslkey = sslkey.into();
self
}
pub fn key_password<T: Into<Option<String>>>(mut self, key_password: T) -> Self {
self.key_password = key_password.into();
self
}
pub fn csd_uid(mut self, csd_uid: u32) -> Self {
self.csd_uid = csd_uid;
self
@ -154,26 +190,32 @@ impl VpnBuilder {
self
}
pub fn reconnect_timeout(mut self, reconnect_timeout: u32) -> Self {
self.reconnect_timeout = reconnect_timeout;
self
}
pub fn mtu(mut self, mtu: u32) -> Self {
self.mtu = mtu;
self
}
pub fn build(self) -> Result<Vpn, VpnError<'static>> {
pub fn disable_ipv6(mut self, disable_ipv6: bool) -> Self {
self.disable_ipv6 = disable_ipv6;
self
}
pub fn build(self) -> Result<Vpn, VpnError> {
let script = match self.script {
Some(script) => {
if !is_executable(&script) {
return Err(VpnError::new("vpnc script is not executable"));
}
check_executable(&script).map_err(|e| VpnError::new(e.to_string()))?;
script
}
None => find_vpnc_script().ok_or_else(|| VpnError::new("Failed to find vpnc-script"))?,
None => find_vpnc_script().ok_or_else(|| VpnError::new(String::from("Failed to find vpnc-script")))?,
};
if let Some(csd_wrapper) = &self.csd_wrapper {
if !is_executable(csd_wrapper) {
return Err(VpnError::new("CSD wrapper is not executable"));
}
check_executable(csd_wrapper).map_err(|e| VpnError::new(e.to_string()))?;
}
let user_agent = self.user_agent.unwrap_or_default();
@ -185,13 +227,18 @@ impl VpnBuilder {
user_agent: Self::to_cstring(&user_agent),
script: Self::to_cstring(&script),
os: Self::to_cstring(&os),
certificate: None,
certificate: self.certificate.as_deref().map(Self::to_cstring),
sslkey: self.sslkey.as_deref().map(Self::to_cstring),
key_password: self.key_password.as_deref().map(Self::to_cstring),
servercert: None,
csd_uid: self.csd_uid,
csd_wrapper: self.csd_wrapper.as_deref().map(Self::to_cstring),
reconnect_timeout: self.reconnect_timeout,
mtu: self.mtu,
disable_ipv6: self.disable_ipv6,
callback: Default::default(),
})