mirror of
https://github.com/yuezk/GlobalProtect-openconnect.git
synced 2025-04-02 18:31:50 -04:00
Support HIP report (#309)
This commit is contained in:
parent
662e4d0b8a
commit
db9249bd61
8
Cargo.lock
generated
8
Cargo.lock
generated
@ -1431,6 +1431,7 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"dotenvy_macro",
|
"dotenvy_macro",
|
||||||
"log",
|
"log",
|
||||||
|
"md5",
|
||||||
"open",
|
"open",
|
||||||
"redact-engine",
|
"redact-engine",
|
||||||
"regex",
|
"regex",
|
||||||
@ -1438,6 +1439,7 @@ dependencies = [
|
|||||||
"roxmltree",
|
"roxmltree",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
"specta",
|
"specta",
|
||||||
"specta-macros",
|
"specta-macros",
|
||||||
"tauri",
|
"tauri",
|
||||||
@ -2226,6 +2228,12 @@ version = "0.7.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "md5"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.7.1"
|
version = "2.7.1"
|
||||||
|
@ -43,6 +43,8 @@ thiserror = "1"
|
|||||||
redact-engine = "0.1"
|
redact-engine = "0.1"
|
||||||
dotenvy_macro = "0.15"
|
dotenvy_macro = "0.15"
|
||||||
compile-time = "0.2"
|
compile-time = "0.2"
|
||||||
|
serde_urlencoded = "0.7"
|
||||||
|
md5="0.7"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 'z' # Optimize for size
|
opt-level = 'z' # Optimize for size
|
||||||
|
@ -32,11 +32,13 @@ impl VpnTaskContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let info = req.info().clone();
|
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 args = req.args();
|
||||||
let vpn = Vpn::builder(req.gateway().server(), args.cookie())
|
let vpn = Vpn::builder(req.gateway().server(), args.cookie())
|
||||||
.user_agent(args.user_agent())
|
.user_agent(args.user_agent())
|
||||||
.script(args.vpnc_script())
|
.script(args.vpnc_script())
|
||||||
|
.csd_uid(args.csd_uid())
|
||||||
|
.csd_wrapper(args.csd_wrapper())
|
||||||
.os(args.openconnect_os())
|
.os(args.openconnect_os())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@ -73,7 +75,9 @@ impl VpnTaskContext {
|
|||||||
|
|
||||||
pub async fn disconnect(&self) {
|
pub async fn disconnect(&self) {
|
||||||
if let Some(disconnect_rx) = self.disconnect_rx.write().await.take() {
|
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() {
|
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();
|
self.vpn_state_tx.send(VpnState::Disconnecting).ok();
|
||||||
vpn.disconnect()
|
vpn.disconnect()
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,8 @@ url.workspace = true
|
|||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
dotenvy_macro.workspace = true
|
dotenvy_macro.workspace = true
|
||||||
uzers.workspace = true
|
uzers.workspace = true
|
||||||
|
serde_urlencoded.workspace = true
|
||||||
|
md5.workspace = true
|
||||||
|
|
||||||
tauri = { workspace = true, optional = true }
|
tauri = { workspace = true, optional = true }
|
||||||
clap = { workspace = true, optional = true }
|
clap = { workspace = true, optional = true }
|
||||||
|
178
crates/gpapi/src/gateway/hip.rs
Normal file
178
crates/gpapi/src/gateway/hip.rs
Normal file
@ -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<String> {
|
||||||
|
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 <ip-address>
|
||||||
|
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<bool> {
|
||||||
|
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<String> {
|
||||||
|
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<bool> {
|
||||||
|
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<HashMap<String, String>> {
|
||||||
|
let cookie_params = serde_urlencoded::from_str::<HashMap<String, String>>(cookie)?;
|
||||||
|
let params = params
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||||
|
.chain(cookie_params)
|
||||||
|
.collect::<HashMap<String, String>>();
|
||||||
|
|
||||||
|
Ok(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute md5 for fields except authcookie,preferred-ip,preferred-ipv6
|
||||||
|
fn build_csd_token(cookie: &str) -> anyhow::Result<String> {
|
||||||
|
let mut cookie_params = serde_urlencoded::from_str::<Vec<(String, String)>>(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
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
mod login;
|
mod login;
|
||||||
mod parse_gateways;
|
mod parse_gateways;
|
||||||
|
pub mod hip;
|
||||||
|
|
||||||
pub use login::*;
|
pub use login::*;
|
||||||
pub(crate) use parse_gateways::*;
|
pub(crate) use parse_gateways::*;
|
||||||
|
@ -83,6 +83,18 @@ impl GpParams {
|
|||||||
self.prefer_default_browser
|
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> {
|
pub(crate) fn to_params(&self) -> HashMap<&str, &str> {
|
||||||
let mut params: HashMap<&str, &str> = HashMap::new();
|
let mut params: HashMap<&str, &str> = HashMap::new();
|
||||||
let client_os = self.client_os.as_str();
|
let client_os = self.client_os.as_str();
|
||||||
|
94
crates/gpapi/src/process/hip_launcher.rs
Normal file
94
crates/gpapi/src/process/hip_launcher.rs
Normal file
@ -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<String> {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,9 @@
|
|||||||
pub(crate) mod command_traits;
|
pub(crate) mod command_traits;
|
||||||
|
|
||||||
pub mod users;
|
|
||||||
pub mod auth_launcher;
|
pub mod auth_launcher;
|
||||||
#[cfg(feature = "browser-auth")]
|
#[cfg(feature = "browser-auth")]
|
||||||
pub mod browser_authenticator;
|
pub mod browser_authenticator;
|
||||||
pub mod gui_launcher;
|
pub mod gui_launcher;
|
||||||
|
pub mod hip_launcher;
|
||||||
pub mod service_launcher;
|
pub mod service_launcher;
|
||||||
|
pub mod users;
|
||||||
|
@ -23,6 +23,11 @@ pub fn get_non_root_user() -> anyhow::Result<User> {
|
|||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_current_user() -> anyhow::Result<User> {
|
||||||
|
let current_user = whoami::username();
|
||||||
|
get_user_by_name(¤t_user)
|
||||||
|
}
|
||||||
|
|
||||||
fn get_real_user() -> anyhow::Result<User> {
|
fn get_real_user() -> anyhow::Result<User> {
|
||||||
// Read the UID from SUDO_UID or PKEXEC_UID environment variable if available.
|
// Read the UID from SUDO_UID or PKEXEC_UID environment variable if available.
|
||||||
let uid = match env::var("SUDO_UID") {
|
let uid = match env::var("SUDO_UID") {
|
||||||
|
@ -32,6 +32,8 @@ pub struct ConnectArgs {
|
|||||||
cookie: String,
|
cookie: String,
|
||||||
vpnc_script: Option<String>,
|
vpnc_script: Option<String>,
|
||||||
user_agent: Option<String>,
|
user_agent: Option<String>,
|
||||||
|
csd_uid: u32,
|
||||||
|
csd_wrapper: Option<String>,
|
||||||
os: Option<ClientOs>,
|
os: Option<ClientOs>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,6 +44,8 @@ impl ConnectArgs {
|
|||||||
vpnc_script: None,
|
vpnc_script: None,
|
||||||
user_agent: None,
|
user_agent: None,
|
||||||
os: None,
|
os: None,
|
||||||
|
csd_uid: 0,
|
||||||
|
csd_wrapper: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,6 +64,14 @@ impl ConnectArgs {
|
|||||||
pub fn openconnect_os(&self) -> Option<String> {
|
pub fn openconnect_os(&self) -> Option<String> {
|
||||||
self.os.as_ref().map(|os| os.to_openconnect_os().to_string())
|
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<String> {
|
||||||
|
self.csd_wrapper.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Type)]
|
#[derive(Debug, Deserialize, Serialize, Type)]
|
||||||
@ -81,6 +93,16 @@ impl ConnectRequest {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_csd_uid(mut self, csd_uid: u32) -> Self {
|
||||||
|
self.args.csd_uid = csd_uid;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_csd_wrapper<T: Into<Option<String>>>(mut self, csd_wrapper: T) -> Self {
|
||||||
|
self.args.csd_wrapper = csd_wrapper.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_user_agent<T: Into<Option<String>>>(mut self, user_agent: T) -> Self {
|
pub fn with_user_agent<T: Into<Option<String>>>(mut self, user_agent: T) -> Self {
|
||||||
self.args.user_agent = user_agent.into();
|
self.args.user_agent = user_agent.into();
|
||||||
self
|
self
|
||||||
|
@ -143,6 +143,9 @@ int vpn_connect(const vpn_options *options, vpn_connected_callback callback)
|
|||||||
void vpn_disconnect()
|
void vpn_disconnect()
|
||||||
{
|
{
|
||||||
char cmd = OC_CMD_CANCEL;
|
char cmd = OC_CMD_CANCEL;
|
||||||
|
|
||||||
|
INFO("Stopping VPN connection: %d", g_cmd_pipe_fd);
|
||||||
|
|
||||||
if (write(g_cmd_pipe_fd, &cmd, 1) < 0)
|
if (write(g_cmd_pipe_fd, &cmd, 1) < 0)
|
||||||
{
|
{
|
||||||
ERROR("Failed to write to command pipe, VPN connection may not be stopped");
|
ERROR("Failed to write to command pipe, VPN connection may not be stopped");
|
||||||
|
Loading…
Reference in New Issue
Block a user