mirror of
				https://github.com/yuezk/GlobalProtect-openconnect.git
				synced 2025-05-20 07:26:58 -04:00 
			
		
		
		
	refactor: read extra args from gp.conf
This commit is contained in:
		| @@ -17,6 +17,11 @@ ring = "0.16" | ||||
| data-encoding = "2.3" | ||||
| log = "0.4" | ||||
| is_executable = "1.0" | ||||
| configparser = "3.0" | ||||
| shlex = "1.0" | ||||
| anyhow = "1.0" | ||||
| tempfile = "3.8" | ||||
| lexopt = "0.3.0" | ||||
|  | ||||
| [build-dependencies] | ||||
| cc = "1.0" | ||||
|   | ||||
| @@ -1,21 +1,24 @@ | ||||
| use log::{debug, info, trace, warn}; | ||||
| use std::ffi::c_void; | ||||
| use std::ffi::{c_char, c_int, c_void}; | ||||
| use tokio::sync::mpsc; | ||||
|  | ||||
| #[repr(C)] | ||||
| #[derive(Debug, Copy, Clone)] | ||||
| pub(crate) struct Options { | ||||
|     pub server: *const std::os::raw::c_char, | ||||
|     pub cookie: *const std::os::raw::c_char, | ||||
|     pub script: *const std::os::raw::c_char, | ||||
|     pub user_agent: *const std::os::raw::c_char, | ||||
|     pub server: *const c_char, | ||||
|     pub cookie: *const c_char, | ||||
|     pub user_agent: *const c_char, | ||||
|     pub user_data: *mut c_void, | ||||
|  | ||||
|     pub script: *const c_char, | ||||
|     pub certificate: *const c_char, | ||||
|     pub servercert: *const c_char, | ||||
| } | ||||
|  | ||||
| #[link(name = "vpn")] | ||||
| extern "C" { | ||||
|     #[link_name = "vpn_connect"] | ||||
|     pub(crate) fn connect(options: *const Options) -> std::os::raw::c_int; | ||||
|     pub(crate) fn connect(options: *const Options) -> c_int; | ||||
|  | ||||
|     #[link_name = "vpn_disconnect"] | ||||
|     pub(crate) fn disconnect(); | ||||
| @@ -33,7 +36,7 @@ extern "C" fn on_vpn_connected(value: i32, sender: *mut c_void) { | ||||
| // level: 0 = error, 1 = info, 2 = debug, 3 = trace | ||||
| // map the error level log in openconnect to the warning level | ||||
| #[no_mangle] | ||||
| extern "C" fn vpn_log(level: i32, message: *const std::os::raw::c_char) { | ||||
| extern "C" fn vpn_log(level: i32, message: *const c_char) { | ||||
|     let message = unsafe { std::ffi::CStr::from_ptr(message) }; | ||||
|     let message = message.to_str().unwrap_or("Invalid log message"); | ||||
|     // Strip the trailing newline | ||||
|   | ||||
							
								
								
									
										164
									
								
								gpcommon/src/vpn/gpconf.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								gpcommon/src/vpn/gpconf.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,164 @@ | ||||
| use anyhow::{Error, Ok, Result}; | ||||
| use configparser::ini::Ini; | ||||
| use lexopt::Parser; | ||||
| use log::warn; | ||||
|  | ||||
| const GPCONF_PATH: &str = "/etc/gpservice/gp.conf"; | ||||
| const DEFAULT_SECTION: &str = "*"; | ||||
| const PROP_OPENCONNECT_ARGS: &str = "openconnect-args"; | ||||
|  | ||||
| /// A struct representing the CLI arguments for the `openconnect` command. | ||||
| /// Supports most of the options from the `openconnect` command line. | ||||
| #[derive(Debug, Default)] | ||||
| pub(crate) struct OpenconnectArgs { | ||||
|     script: Option<String>, | ||||
|     certificate: Option<String>, | ||||
|     servercert: Option<String>, | ||||
| } | ||||
|  | ||||
| impl OpenconnectArgs { | ||||
|     pub fn script(&self) -> Option<String> { | ||||
|         self.script.to_owned() | ||||
|     } | ||||
|  | ||||
|     pub fn certificate(&self) -> Option<String> { | ||||
|         self.certificate.to_owned() | ||||
|     } | ||||
|  | ||||
|     pub fn servercert(&self) -> Option<String> { | ||||
|         self.servercert.to_owned() | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Read the `gp.conf` file and return the `openconnect` arguments for the given server. | ||||
| /// If the server is not found, the default section is used. | ||||
| pub(crate) fn read_conf(server: &str) -> Result<OpenconnectArgs> { | ||||
|     read_conf_from(GPCONF_PATH, server) | ||||
| } | ||||
|  | ||||
| /// Private function to read the `openconnect` arguments for the given server from a given path. | ||||
| /// Make it easy to write tests. | ||||
| fn read_conf_from(path: &str, server: &str) -> Result<OpenconnectArgs> { | ||||
|     let mut config = Ini::new(); | ||||
|  | ||||
|     config.set_default_section(DEFAULT_SECTION); | ||||
|     config.set_multiline(true); | ||||
|  | ||||
|     config.load(path).map_err(Error::msg)?; | ||||
|  | ||||
|     let default_openconnect_config = config | ||||
|         .get(DEFAULT_SECTION, PROP_OPENCONNECT_ARGS) | ||||
|         .unwrap_or_default(); | ||||
|     let server_openconnect_config = config | ||||
|         .get(server, PROP_OPENCONNECT_ARGS) | ||||
|         .unwrap_or(default_openconnect_config); | ||||
|  | ||||
|     let args = shlex::split(&server_openconnect_config).unwrap_or_default(); | ||||
|     parse_args(&args) | ||||
| } | ||||
|  | ||||
| fn parse_args(args: &Vec<String>) -> Result<OpenconnectArgs> { | ||||
|     use lexopt::prelude::*; | ||||
|  | ||||
|     let mut parser = Parser::from_args(args); | ||||
|  | ||||
|     let mut script: Option<String> = None; | ||||
|     let mut certificate: Option<String> = None; | ||||
|     let mut servercert: Option<String> = None; | ||||
|     while let Some(arg) = parser.next()? { | ||||
|         match arg { | ||||
|             Long("script") | Short('s') => { | ||||
|                 script = Some(parser.value()?.parse()?); | ||||
|             } | ||||
|             Long("certificate") | Short('c') => { | ||||
|                 certificate = Some(parser.value()?.parse()?); | ||||
|             } | ||||
|             Long("servercert") => { | ||||
|                 servercert = Some(parser.value()?.parse()?); | ||||
|             } | ||||
|             _ => { | ||||
|                 warn!("Ignoring unknown argument: {}", arg.unexpected()); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(OpenconnectArgs { | ||||
|         script, | ||||
|         certificate, | ||||
|         servercert, | ||||
|     }) | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use std::io::Write; | ||||
|     use tempfile::NamedTempFile; | ||||
|  | ||||
|     use super::*; | ||||
|  | ||||
|     // Macro to create a temporary file with the given content. | ||||
|     macro_rules! tempfile { | ||||
|         ($content:expr) => {{ | ||||
|             let mut file = NamedTempFile::new().unwrap(); | ||||
|             write!(file, $content).unwrap(); | ||||
|             file | ||||
|         }}; | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_config_not_found() { | ||||
|         let args = read_conf_from("non-existent-file", "server"); | ||||
|         assert!(args.is_err()); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_default_config() { | ||||
|         let file = tempfile!( | ||||
|             r#" | ||||
| [*] | ||||
| openconnect-args=--script=/script.sh | ||||
| "# | ||||
|         ); | ||||
|         let path = file.path().to_str().unwrap(); | ||||
|         let args = read_conf_from(path, "any server").unwrap(); | ||||
|  | ||||
|         assert_eq!(args.script, Some("/script.sh".to_string())); | ||||
|         assert_eq!(args.certificate, None); | ||||
|         assert_eq!(args.servercert, None); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_server_config() { | ||||
|         let file = tempfile!( | ||||
|             r#" | ||||
| [*] | ||||
| openconnect-args=--script=/script.sh | ||||
|  | ||||
| [server] | ||||
| openconnect-args=--certificate=/cert.pem | ||||
| "# | ||||
|         ); | ||||
|         let path = file.path().to_str().unwrap(); | ||||
|         let args = read_conf_from(path, "server").unwrap(); | ||||
|  | ||||
|         assert_eq!(args.script, None); | ||||
|         assert_eq!(args.certificate, Some("/cert.pem".to_string())); | ||||
|         assert_eq!(args.servercert, None); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_ignore_unknown_args() { | ||||
|         let file = tempfile!( | ||||
|             r#" | ||||
| [*] | ||||
| openconnect-args=--script=/script.sh --unknown-arg -c /cert.pem | ||||
| "# | ||||
|         ); | ||||
|         let path = file.path().to_str().unwrap(); | ||||
|         let args = read_conf_from(path, "server").unwrap(); | ||||
|  | ||||
|         assert_eq!(args.script, Some("/script.sh".to_string())); | ||||
|         assert_eq!(args.certificate, Some("/cert.pem".to_string())); | ||||
|         assert_eq!(args.servercert, None); | ||||
|     } | ||||
| } | ||||
| @@ -1,13 +1,15 @@ | ||||
| use crate::vpn::vpnc_script::find_default_vpnc_script; | ||||
| use self::vpn_options::VpnOptions; | ||||
| use log::{debug, info, warn}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::ffi::{c_void, CString}; | ||||
| use std::ffi::c_void; | ||||
| use std::sync::Arc; | ||||
| use std::thread; | ||||
| use tokio::sync::watch; | ||||
| use tokio::sync::{mpsc, Mutex}; | ||||
|  | ||||
| mod ffi; | ||||
| mod gpconf; | ||||
| mod vpn_options; | ||||
| mod vpnc_script; | ||||
|  | ||||
| #[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)] | ||||
| @@ -51,30 +53,6 @@ impl StatusHolder { | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct VpnOptions { | ||||
|     server: CString, | ||||
|     cookie: CString, | ||||
|     script: CString, | ||||
|     user_agent: CString, | ||||
| } | ||||
|  | ||||
| impl VpnOptions { | ||||
|     fn as_oc_options(&self, user_data: *mut c_void) -> ffi::Options { | ||||
|         ffi::Options { | ||||
|             server: self.server.as_ptr(), | ||||
|             cookie: self.cookie.as_ptr(), | ||||
|             script: self.script.as_ptr(), | ||||
|             user_agent: self.user_agent.as_ptr(), | ||||
|             user_data, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn to_cstr(value: &str) -> CString { | ||||
|         CString::new(value.to_string()).expect("Failed to convert to CString") | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Default)] | ||||
| pub(crate) struct Vpn { | ||||
|     status_holder: Arc<Mutex<StatusHolder>>, | ||||
| @@ -92,23 +70,18 @@ impl Vpn { | ||||
|         cookie: &str, | ||||
|         user_agent: &str, | ||||
|     ) -> Result<(), Box<dyn std::error::Error>> { | ||||
|         let script = match find_default_vpnc_script() { | ||||
|             Some(script) => { | ||||
|                 debug!("Using default vpnc-script: {}", script); | ||||
|                 script | ||||
|             } | ||||
|             None => { | ||||
|                 return Err("Failed to find default vpnc-script".into()); | ||||
|             } | ||||
|         }; | ||||
|         let mut options_builder = VpnOptions::builder(); | ||||
|         let mut options_builder = options_builder | ||||
|             .server(server) | ||||
|             .cookie(cookie) | ||||
|             .user_agent(user_agent); | ||||
|  | ||||
|         if let Ok(openconnect_args) = gpconf::read_conf(server) { | ||||
|             info!("Found openconnect args in /etc/gpservice/gp.conf"); | ||||
|             options_builder = options_builder.with_openconnect_args(openconnect_args); | ||||
|         } | ||||
|         // Save the VPN options so we can use them later, e.g. reconnect | ||||
|         *self.vpn_options.lock().await = Some(VpnOptions { | ||||
|             server: VpnOptions::to_cstr(server), | ||||
|             cookie: VpnOptions::to_cstr(cookie), | ||||
|             script: VpnOptions::to_cstr(script), | ||||
|             user_agent: VpnOptions::to_cstr(user_agent), | ||||
|         }); | ||||
|         *self.vpn_options.lock().await = Some(options_builder.build()); | ||||
|  | ||||
|         let vpn_options = self.vpn_options.clone(); | ||||
|         let status_holder = self.status_holder.clone(); | ||||
|   | ||||
| @@ -67,6 +67,17 @@ int vpn_connect(const vpn_options *options) | ||||
|     openconnect_set_hostname(vpninfo, options->server); | ||||
|     openconnect_set_cookie(vpninfo, options->cookie); | ||||
|  | ||||
|     if (options->certificate) | ||||
|     { | ||||
|         INFO("Setting client certificate: %s", options->certificate); | ||||
|         openconnect_set_client_cert(vpninfo, options->certificate, NULL); | ||||
|     } | ||||
|  | ||||
|     if (options->servercert) { | ||||
|         INFO("Setting server certificate: %s", options->servercert); | ||||
|         openconnect_set_system_trust(vpninfo, 0); | ||||
|     } | ||||
|  | ||||
|     g_cmd_pipe_fd = openconnect_setup_cmd_pipe(vpninfo); | ||||
|     if (g_cmd_pipe_fd < 0) | ||||
|     { | ||||
|   | ||||
| @@ -7,9 +7,12 @@ typedef struct vpn_options | ||||
| { | ||||
|     const char *server; | ||||
|     const char *cookie; | ||||
|     const char *script; | ||||
|     const char *user_agent; | ||||
|     void *user_data; | ||||
|  | ||||
|     const char *script; | ||||
|     const char *certificate; | ||||
|     const char *servercert; | ||||
| } vpn_options; | ||||
|  | ||||
| int vpn_connect(const vpn_options *options); | ||||
|   | ||||
							
								
								
									
										93
									
								
								gpcommon/src/vpn/vpn_options.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								gpcommon/src/vpn/vpn_options.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | ||||
| use super::{ffi, gpconf::OpenconnectArgs, vpnc_script::find_default_vpnc_script}; | ||||
| use std::ffi::{c_char, c_void, CString}; | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub(crate) struct VpnOptions { | ||||
|     server: CString, | ||||
|     cookie: CString, | ||||
|     user_agent: CString, | ||||
|  | ||||
|     script: CString, | ||||
|     certificate: Option<CString>, | ||||
|     servercert: Option<CString>, | ||||
| } | ||||
|  | ||||
| impl VpnOptions { | ||||
|     pub fn builder() -> VpnOptionsBuilder { | ||||
|         VpnOptionsBuilder::default() | ||||
|     } | ||||
|  | ||||
|     pub fn as_oc_options(&self, user_data: *mut c_void) -> ffi::Options { | ||||
|         ffi::Options { | ||||
|             server: self.server.as_ptr(), | ||||
|             cookie: self.cookie.as_ptr(), | ||||
|             user_agent: self.user_agent.as_ptr(), | ||||
|             user_data, | ||||
|  | ||||
|             script: self.script.as_ptr(), | ||||
|             certificate: Self::option_as_ptr(&self.certificate), | ||||
|             servercert: Self::option_as_ptr(&self.servercert), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn option_as_ptr(value: &Option<CString>) -> *const c_char { | ||||
|         match value { | ||||
|             Some(value) => value.as_ptr(), | ||||
|             None => std::ptr::null(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Default)] | ||||
| pub(crate) struct VpnOptionsBuilder { | ||||
|     server: String, | ||||
|     cookie: String, | ||||
|     user_agent: String, | ||||
|     openconnect_args: OpenconnectArgs, | ||||
| } | ||||
|  | ||||
| impl VpnOptionsBuilder { | ||||
|     pub fn server(&mut self, server: &str) -> &mut Self { | ||||
|         self.server = server.to_string(); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn cookie(&mut self, cookie: &str) -> &mut Self { | ||||
|         self.cookie = cookie.to_string(); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn user_agent(&mut self, user_agent: &str) -> &mut Self { | ||||
|         self.user_agent = user_agent.to_string(); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn with_openconnect_args(&mut self, openconnect_args: OpenconnectArgs) -> &mut Self { | ||||
|         self.openconnect_args = openconnect_args; | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     fn to_cstr(value: &str) -> CString { | ||||
|         CString::new(value.to_string()).expect("Failed to convert to CString") | ||||
|     } | ||||
|  | ||||
|     pub fn build(&self) -> VpnOptions { | ||||
|         let openconnect_args = &self.openconnect_args; | ||||
|         let script = openconnect_args | ||||
|             .script() | ||||
|             .or_else(|| find_default_vpnc_script().map(|s| s.to_string())) | ||||
|             .map(|s| Self::to_cstr(&s)) | ||||
|             .unwrap_or_default(); | ||||
|         let certificate = openconnect_args.certificate().map(|s| Self::to_cstr(&s)); | ||||
|         let servercert = openconnect_args.servercert().map(|s| Self::to_cstr(&s)); | ||||
|  | ||||
|         VpnOptions { | ||||
|             server: Self::to_cstr(&self.server), | ||||
|             cookie: Self::to_cstr(&self.cookie), | ||||
|             user_agent: Self::to_cstr(&self.user_agent), | ||||
|             script, | ||||
|             certificate, | ||||
|             servercert, | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user