From db9249bd614f889e1722d78a7c6ac596e767e901 Mon Sep 17 00:00:00 2001 From: Kevin Yue Date: Mon, 5 Feb 2024 05:35:45 -0500 Subject: [PATCH] Support HIP report (#309) --- Cargo.lock | 8 + Cargo.toml | 2 + apps/gpservice/src/vpn_task.rs | 6 +- crates/gpapi/Cargo.toml | 2 + crates/gpapi/src/gateway/hip.rs | 178 +++++++++++++++++++++++ crates/gpapi/src/gateway/mod.rs | 1 + crates/gpapi/src/gp_params.rs | 12 ++ crates/gpapi/src/process/hip_launcher.rs | 94 ++++++++++++ crates/gpapi/src/process/mod.rs | 3 +- crates/gpapi/src/process/users.rs | 5 + crates/gpapi/src/service/request.rs | 22 +++ crates/openconnect/src/ffi/vpn.c | 3 + 12 files changed, 334 insertions(+), 2 deletions(-) create mode 100644 crates/gpapi/src/gateway/hip.rs create mode 100644 crates/gpapi/src/process/hip_launcher.rs diff --git a/Cargo.lock b/Cargo.lock index 600a7db..95c9c94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1431,6 +1431,7 @@ dependencies = [ "clap", "dotenvy_macro", "log", + "md5", "open", "redact-engine", "regex", @@ -1438,6 +1439,7 @@ dependencies = [ "roxmltree", "serde", "serde_json", + "serde_urlencoded", "specta", "specta-macros", "tauri", @@ -2226,6 +2228,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.7.1" diff --git a/Cargo.toml b/Cargo.toml index 7ce3ab7..78570bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,8 @@ thiserror = "1" redact-engine = "0.1" dotenvy_macro = "0.15" compile-time = "0.2" +serde_urlencoded = "0.7" +md5="0.7" [profile.release] opt-level = 'z' # Optimize for size diff --git a/apps/gpservice/src/vpn_task.rs b/apps/gpservice/src/vpn_task.rs index 55424a1..ad19e47 100644 --- a/apps/gpservice/src/vpn_task.rs +++ b/apps/gpservice/src/vpn_task.rs @@ -32,11 +32,13 @@ impl VpnTaskContext { } let info = req.info().clone(); - let vpn_handle = self.vpn_handle.clone(); + let vpn_handle = Arc::clone(&self.vpn_handle); let args = req.args(); let vpn = Vpn::builder(req.gateway().server(), args.cookie()) .user_agent(args.user_agent()) .script(args.vpnc_script()) + .csd_uid(args.csd_uid()) + .csd_wrapper(args.csd_wrapper()) .os(args.openconnect_os()) .build(); @@ -73,7 +75,9 @@ impl VpnTaskContext { pub async fn disconnect(&self) { if let Some(disconnect_rx) = self.disconnect_rx.write().await.take() { + info!("Disconnecting VPN..."); if let Some(vpn) = self.vpn_handle.read().await.as_ref() { + info!("VPN is connected, start disconnecting..."); self.vpn_state_tx.send(VpnState::Disconnecting).ok(); vpn.disconnect() } diff --git a/crates/gpapi/Cargo.toml b/crates/gpapi/Cargo.toml index ee8fa37..c75fb8d 100644 --- a/crates/gpapi/Cargo.toml +++ b/crates/gpapi/Cargo.toml @@ -25,6 +25,8 @@ url.workspace = true regex.workspace = true dotenvy_macro.workspace = true uzers.workspace = true +serde_urlencoded.workspace = true +md5.workspace = true tauri = { workspace = true, optional = true } clap = { workspace = true, optional = true } diff --git a/crates/gpapi/src/gateway/hip.rs b/crates/gpapi/src/gateway/hip.rs new file mode 100644 index 0000000..7cc7548 --- /dev/null +++ b/crates/gpapi/src/gateway/hip.rs @@ -0,0 +1,178 @@ +use std::collections::HashMap; + +use log::{info, warn}; +use reqwest::Client; +use roxmltree::Document; + +use crate::{gp_params::GpParams, process::hip_launcher::HipLauncher, utils::normalize_server}; + +struct HipReporter<'a> { + server: String, + cookie: &'a str, + md5: &'a str, + csd_wrapper: &'a str, + gp_params: &'a GpParams, + client: Client, +} + +impl HipReporter<'_> { + async fn report(&self) -> anyhow::Result<()> { + let client_ip = self.retrieve_client_ip().await?; + + let hip_needed = match self.check_hip(&client_ip).await { + Ok(hip_needed) => hip_needed, + Err(err) => { + warn!("Failed to check HIP: {}", err); + return Ok(()); + } + }; + + if !hip_needed { + info!("HIP report not needed"); + return Ok(()); + } + + info!("HIP report needed, generating report..."); + let report = self.generate_report(&client_ip).await?; + + if let Err(err) = self.submit_hip(&client_ip, &report).await { + warn!("Failed to submit HIP report: {}", err); + } + + Ok(()) + } + + async fn retrieve_client_ip(&self) -> anyhow::Result { + let config_url = format!("{}/ssl-vpn/getconfig.esp", self.server); + let mut params: HashMap<&str, &str> = HashMap::new(); + + params.insert("client-type", "1"); + params.insert("protocol-version", "p1"); + params.insert("internal", "no"); + params.insert("ipv6-support", "yes"); + params.insert("clientos", self.gp_params.client_os()); + params.insert("hmac-algo", "sha1,md5,sha256"); + params.insert("enc-algo", "aes-128-cbc,aes-256-cbc"); + + if let Some(os_version) = self.gp_params.os_version() { + params.insert("os-version", os_version); + } + if let Some(client_version) = self.gp_params.client_version() { + params.insert("app-version", client_version); + } + + let params = merge_cookie_params(self.cookie, ¶ms)?; + + let res = self.client.post(&config_url).form(¶ms).send().await?; + let res_xml = res.error_for_status()?.text().await?; + let doc = Document::parse(&res_xml)?; + + // Get + let ip = doc + .descendants() + .find(|n| n.has_tag_name("ip-address")) + .and_then(|n| n.text()) + .ok_or_else(|| anyhow::anyhow!("ip-address not found"))?; + + Ok(ip.to_string()) + } + + async fn check_hip(&self, client_ip: &str) -> anyhow::Result { + let url = format!("{}/ssl-vpn/hipreportcheck.esp", self.server); + let mut params = HashMap::new(); + + params.insert("client-role", "global-protect-full"); + params.insert("client-ip", client_ip); + params.insert("md5", self.md5); + + let params = merge_cookie_params(self.cookie, ¶ms)?; + let res = self.client.post(&url).form(¶ms).send().await?; + let res_xml = res.error_for_status()?.text().await?; + + is_hip_needed(&res_xml) + } + + async fn generate_report(&self, client_ip: &str) -> anyhow::Result { + let launcher = HipLauncher::new(self.csd_wrapper) + .cookie(self.cookie) + .md5(self.md5) + .client_ip(client_ip) + .client_os(self.gp_params.client_os()) + .client_version(self.gp_params.client_version()); + + launcher.launch().await + } + + async fn submit_hip(&self, client_ip: &str, report: &str) -> anyhow::Result<()> { + let url = format!("{}/ssl-vpn/hipreport.esp", self.server); + + let mut params = HashMap::new(); + params.insert("client-role", "global-protect-full"); + params.insert("client-ip", client_ip); + params.insert("report", report); + + let params = merge_cookie_params(self.cookie, ¶ms)?; + let res = self.client.post(&url).form(¶ms).send().await?; + let res_xml = res.error_for_status()?.text().await?; + + info!("HIP check response: {}", res_xml); + + Ok(()) + } +} + +fn is_hip_needed(res_xml: &str) -> anyhow::Result { + let doc = Document::parse(res_xml)?; + + let hip_needed = doc + .descendants() + .find(|n| n.has_tag_name("hip-report-needed")) + .and_then(|n| n.text()) + .ok_or_else(|| anyhow::anyhow!("hip-report-needed not found"))?; + + Ok(hip_needed == "yes") +} + +fn merge_cookie_params(cookie: &str, params: &HashMap<&str, &str>) -> anyhow::Result> { + let cookie_params = serde_urlencoded::from_str::>(cookie)?; + let params = params + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .chain(cookie_params) + .collect::>(); + + Ok(params) +} + +// Compute md5 for fields except authcookie,preferred-ip,preferred-ipv6 +fn build_csd_token(cookie: &str) -> anyhow::Result { + let mut cookie_params = serde_urlencoded::from_str::>(cookie)?; + cookie_params.retain(|(k, _)| k != "authcookie" && k != "preferred-ip" && k != "preferred-ipv6"); + + let token = serde_urlencoded::to_string(cookie_params)?; + let md5 = format!("{:x}", md5::compute(token)); + + Ok(md5) +} + +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 md5 = build_csd_token(cookie)?; + + info!("Submit HIP report md5: {}", md5); + + let reporter = HipReporter { + server: normalize_server(gateway)?, + cookie, + md5: &md5, + csd_wrapper, + gp_params, + client, + }; + + reporter.report().await +} diff --git a/crates/gpapi/src/gateway/mod.rs b/crates/gpapi/src/gateway/mod.rs index ab84223..b768269 100644 --- a/crates/gpapi/src/gateway/mod.rs +++ b/crates/gpapi/src/gateway/mod.rs @@ -1,5 +1,6 @@ mod login; mod parse_gateways; +pub mod hip; pub use login::*; pub(crate) use parse_gateways::*; diff --git a/crates/gpapi/src/gp_params.rs b/crates/gpapi/src/gp_params.rs index 0c2a205..03322ac 100644 --- a/crates/gpapi/src/gp_params.rs +++ b/crates/gpapi/src/gp_params.rs @@ -83,6 +83,18 @@ impl GpParams { self.prefer_default_browser } + pub fn client_os(&self) -> &str { + self.client_os.as_str() + } + + pub fn os_version(&self) -> Option<&str> { + self.os_version.as_deref() + } + + pub fn client_version(&self) -> Option<&str> { + self.client_version.as_deref() + } + pub(crate) fn to_params(&self) -> HashMap<&str, &str> { let mut params: HashMap<&str, &str> = HashMap::new(); let client_os = self.client_os.as_str(); diff --git a/crates/gpapi/src/process/hip_launcher.rs b/crates/gpapi/src/process/hip_launcher.rs new file mode 100644 index 0000000..02ba227 --- /dev/null +++ b/crates/gpapi/src/process/hip_launcher.rs @@ -0,0 +1,94 @@ +use std::process::Stdio; + +use anyhow::bail; +use tokio::process::Command; + +pub struct HipLauncher<'a> { + program: &'a str, + cookie: Option<&'a str>, + client_ip: Option<&'a str>, + md5: Option<&'a str>, + client_os: Option<&'a str>, + client_version: Option<&'a str>, +} + +impl<'a> HipLauncher<'a> { + pub fn new(program: &'a str) -> Self { + Self { + program, + cookie: None, + client_ip: None, + md5: None, + client_os: None, + client_version: None, + } + } + + pub fn cookie(mut self, cookie: &'a str) -> Self { + self.cookie = Some(cookie); + self + } + + pub fn client_ip(mut self, client_ip: &'a str) -> Self { + self.client_ip = Some(client_ip); + self + } + + pub fn md5(mut self, md5: &'a str) -> Self { + self.md5 = Some(md5); + self + } + + pub fn client_os(mut self, client_os: &'a str) -> Self { + self.client_os = Some(client_os); + self + } + + pub fn client_version(mut self, client_version: Option<&'a str>) -> Self { + self.client_version = client_version; + self + } + + pub async fn launch(&self) -> anyhow::Result { + let mut cmd = Command::new(self.program); + + if let Some(cookie) = self.cookie { + cmd.arg("--cookie").arg(cookie); + } + + if let Some(client_ip) = self.client_ip { + cmd.arg("--client-ip").arg(client_ip); + } + + if let Some(md5) = self.md5 { + cmd.arg("--md5").arg(md5); + } + + if let Some(client_os) = self.client_os { + cmd.arg("--client-os").arg(client_os); + } + + if let Some(client_version) = self.client_version { + cmd.env("APP_VERSION", client_version); + } + + let output = cmd + .kill_on_drop(true) + .stdout(Stdio::piped()) + .spawn()? + .wait_with_output() + .await?; + + if let Some(exit_status) = output.status.code() { + if exit_status != 0 { + bail!("HIP report generation failed with exit code {}", exit_status); + } + + let report = String::from_utf8(output.stdout)?; + + Ok(report) + } else { + bail!("HIP report generation failed"); + } + } +} diff --git a/crates/gpapi/src/process/mod.rs b/crates/gpapi/src/process/mod.rs index cb9db31..acb3555 100644 --- a/crates/gpapi/src/process/mod.rs +++ b/crates/gpapi/src/process/mod.rs @@ -1,8 +1,9 @@ pub(crate) mod command_traits; -pub mod users; pub mod auth_launcher; #[cfg(feature = "browser-auth")] pub mod browser_authenticator; pub mod gui_launcher; +pub mod hip_launcher; pub mod service_launcher; +pub mod users; diff --git a/crates/gpapi/src/process/users.rs b/crates/gpapi/src/process/users.rs index 0fc51ef..265f1d4 100644 --- a/crates/gpapi/src/process/users.rs +++ b/crates/gpapi/src/process/users.rs @@ -23,6 +23,11 @@ pub fn get_non_root_user() -> anyhow::Result { Ok(user) } +pub fn get_current_user() -> anyhow::Result { + let current_user = whoami::username(); + get_user_by_name(¤t_user) +} + fn get_real_user() -> anyhow::Result { // Read the UID from SUDO_UID or PKEXEC_UID environment variable if available. let uid = match env::var("SUDO_UID") { diff --git a/crates/gpapi/src/service/request.rs b/crates/gpapi/src/service/request.rs index e80f68e..a457197 100644 --- a/crates/gpapi/src/service/request.rs +++ b/crates/gpapi/src/service/request.rs @@ -32,6 +32,8 @@ pub struct ConnectArgs { cookie: String, vpnc_script: Option, user_agent: Option, + csd_uid: u32, + csd_wrapper: Option, os: Option, } @@ -42,6 +44,8 @@ impl ConnectArgs { vpnc_script: None, user_agent: None, os: None, + csd_uid: 0, + csd_wrapper: None, } } @@ -60,6 +64,14 @@ impl ConnectArgs { pub fn openconnect_os(&self) -> Option { self.os.as_ref().map(|os| os.to_openconnect_os().to_string()) } + + pub fn csd_uid(&self) -> u32 { + self.csd_uid + } + + pub fn csd_wrapper(&self) -> Option { + self.csd_wrapper.clone() + } } #[derive(Debug, Deserialize, Serialize, Type)] @@ -81,6 +93,16 @@ impl ConnectRequest { self } + pub fn with_csd_uid(mut self, csd_uid: u32) -> Self { + self.args.csd_uid = csd_uid; + self + } + + pub fn with_csd_wrapper>>(mut self, csd_wrapper: T) -> Self { + self.args.csd_wrapper = csd_wrapper.into(); + self + } + pub fn with_user_agent>>(mut self, user_agent: T) -> Self { self.args.user_agent = user_agent.into(); self diff --git a/crates/openconnect/src/ffi/vpn.c b/crates/openconnect/src/ffi/vpn.c index 265a4e2..f4e6423 100644 --- a/crates/openconnect/src/ffi/vpn.c +++ b/crates/openconnect/src/ffi/vpn.c @@ -143,6 +143,9 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback) void vpn_disconnect() { char cmd = OC_CMD_CANCEL; + + INFO("Stopping VPN connection: %d", g_cmd_pipe_fd); + if (write(g_cmd_pipe_fd, &cmd, 1) < 0) { ERROR("Failed to write to command pipe, VPN connection may not be stopped");