mirror of
				https://github.com/yuezk/GlobalProtect-openconnect.git
				synced 2025-05-20 07:26:58 -04:00 
			
		
		
		
	fix: disconnect VPN when sleep
This commit is contained in:
		
							
								
								
									
										5
									
								
								.github/workflows/build.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.github/workflows/build.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -14,6 +14,11 @@ on: | |||||||
|       - release/* |       - release/* | ||||||
|     tags: |     tags: | ||||||
|       - v*.*.* |       - v*.*.* | ||||||
|  |  | ||||||
|  | concurrency: | ||||||
|  |   group: ${{ github.workflow }}-${{ github.ref }} | ||||||
|  |   cancel-in-progress: true | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   # Include arm64 if ref is a tag |   # Include arm64 if ref is a tag | ||||||
|   setup-matrix: |   setup-matrix: | ||||||
|   | |||||||
							
								
								
									
										1910
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1910
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										18
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								Cargo.toml
									
									
									
									
									
								
							| @@ -1,7 +1,13 @@ | |||||||
| [workspace] | [workspace] | ||||||
| resolver = "2" | resolver = "2" | ||||||
|  |  | ||||||
| members = ["crates/*", "apps/gpclient", "apps/gpservice", "apps/gpauth", "apps/gpgui-helper/src-tauri"] | members = [ | ||||||
|  |   "crates/*", | ||||||
|  |   "apps/gpclient", | ||||||
|  |   "apps/gpservice", | ||||||
|  |   "apps/gpauth", | ||||||
|  |   "apps/gpgui-helper/src-tauri", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [workspace.package] | [workspace.package] | ||||||
| rust-version = "1.70" | rust-version = "1.70" | ||||||
| @@ -30,11 +36,11 @@ serde = { version = "1.0", features = ["derive"] } | |||||||
| serde_json = "1.0" | serde_json = "1.0" | ||||||
| sysinfo = "0.29" | sysinfo = "0.29" | ||||||
| tempfile = "3.8" | tempfile = "3.8" | ||||||
| tokio = { version = "1", features = ["full"] } | tokio = { version = "1" } | ||||||
| tokio-util = "0.7" | tokio-util = "0.7" | ||||||
| url = "2.4" | url = "2.4" | ||||||
| urlencoding = "2.1.3" | urlencoding = "2.1.3" | ||||||
| axum = "0.7" | axum = "0.8" | ||||||
| futures = "0.3" | futures = "0.3" | ||||||
| futures-util = "0.3" | futures-util = "0.3" | ||||||
| tokio-tungstenite = "0.20.1" | tokio-tungstenite = "0.20.1" | ||||||
| @@ -44,9 +50,9 @@ thiserror = "1" | |||||||
| redact-engine = "0.1" | redact-engine = "0.1" | ||||||
| compile-time = "0.2" | compile-time = "0.2" | ||||||
| serde_urlencoded = "0.7" | serde_urlencoded = "0.7" | ||||||
| md5="0.7" | md5 = "0.7" | ||||||
| sha256="1" | sha256 = "1" | ||||||
| which="6" | which = "7" | ||||||
|  |  | ||||||
| # Tauri dependencies | # Tauri dependencies | ||||||
| tauri = { version = "1.5" } | tauri = { version = "1.5" } | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								Makefile
									
									
									
									
									
								
							| @@ -117,6 +117,10 @@ install: | |||||||
| 		install -Dm755 .build/gpgui/gpgui_*/gpgui $(DESTDIR)/usr/bin/gpgui; \ | 		install -Dm755 .build/gpgui/gpgui_*/gpgui $(DESTDIR)/usr/bin/gpgui; \ | ||||||
| 	fi | 	fi | ||||||
|  |  | ||||||
|  | 	# Install the disconnect hooks | ||||||
|  | 	install -Dm755 packaging/files/usr/lib/NetworkManager/dispatcher.d/pre-down.d/gpclient.down $(DESTDIR)/usr/lib/NetworkManager/dispatcher.d/pre-down.d/gpclient.down | ||||||
|  | 	install -Dm755 packaging/files/usr/lib/NetworkManager/dispatcher.d/gpclient-nm-hook $(DESTDIR)/usr/lib/NetworkManager/dispatcher.d/gpclient-nm-hook | ||||||
|  |  | ||||||
| 	install -Dm644 packaging/files/usr/share/applications/gpgui.desktop $(DESTDIR)/usr/share/applications/gpgui.desktop | 	install -Dm644 packaging/files/usr/share/applications/gpgui.desktop $(DESTDIR)/usr/share/applications/gpgui.desktop | ||||||
| 	install -Dm644 packaging/files/usr/share/icons/hicolor/scalable/apps/gpgui.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/gpgui.svg | 	install -Dm644 packaging/files/usr/share/icons/hicolor/scalable/apps/gpgui.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/gpgui.svg | ||||||
| 	install -Dm644 packaging/files/usr/share/icons/hicolor/32x32/apps/gpgui.png $(DESTDIR)/usr/share/icons/hicolor/32x32/apps/gpgui.png | 	install -Dm644 packaging/files/usr/share/icons/hicolor/32x32/apps/gpgui.png $(DESTDIR)/usr/share/icons/hicolor/32x32/apps/gpgui.png | ||||||
| @@ -133,6 +137,9 @@ uninstall: | |||||||
| 	rm -f $(DESTDIR)/usr/bin/gpgui-helper | 	rm -f $(DESTDIR)/usr/bin/gpgui-helper | ||||||
| 	rm -f $(DESTDIR)/usr/bin/gpgui | 	rm -f $(DESTDIR)/usr/bin/gpgui | ||||||
|  |  | ||||||
|  | 	rm -f $(DESTDIR)/usr/lib/NetworkManager/dispatcher.d/pre-down.d/gpclient.down | ||||||
|  | 	rm -f $(DESTDIR)/usr/lib/NetworkManager/dispatcher.d/gpclient-nm-hook | ||||||
|  |  | ||||||
| 	rm -f $(DESTDIR)/usr/share/applications/gpgui.desktop | 	rm -f $(DESTDIR)/usr/share/applications/gpgui.desktop | ||||||
| 	rm -f $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/gpgui.svg | 	rm -f $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/gpgui.svg | ||||||
| 	rm -f $(DESTDIR)/usr/share/icons/hicolor/32x32/apps/gpgui.png | 	rm -f $(DESTDIR)/usr/share/icons/hicolor/32x32/apps/gpgui.png | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ clap.workspace = true | |||||||
| env_logger.workspace = true | env_logger.workspace = true | ||||||
| inquire = "0.6.2" | inquire = "0.6.2" | ||||||
| log.workspace = true | log.workspace = true | ||||||
| tokio.workspace = true | tokio = { workspace = true, features = ["rt-multi-thread"] } | ||||||
| sysinfo.workspace = true | sysinfo.workspace = true | ||||||
| serde_json.workspace = true | serde_json.workspace = true | ||||||
| whoami.workspace = true | whoami.workspace = true | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ use tempfile::NamedTempFile; | |||||||
|  |  | ||||||
| use crate::{ | use crate::{ | ||||||
|   connect::{ConnectArgs, ConnectHandler}, |   connect::{ConnectArgs, ConnectHandler}, | ||||||
|   disconnect::DisconnectHandler, |   disconnect::{DisconnectArgs, DisconnectHandler}, | ||||||
|   launch_gui::{LaunchGuiArgs, LaunchGuiHandler}, |   launch_gui::{LaunchGuiArgs, LaunchGuiHandler}, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -23,7 +23,7 @@ enum CliCommand { | |||||||
|   #[command(about = "Connect to a portal server")] |   #[command(about = "Connect to a portal server")] | ||||||
|   Connect(Box<ConnectArgs>), |   Connect(Box<ConnectArgs>), | ||||||
|   #[command(about = "Disconnect from the server")] |   #[command(about = "Disconnect from the server")] | ||||||
|   Disconnect, |   Disconnect(DisconnectArgs), | ||||||
|   #[command(about = "Launch the GUI")] |   #[command(about = "Launch the GUI")] | ||||||
|   LaunchGui(LaunchGuiArgs), |   LaunchGui(LaunchGuiArgs), | ||||||
| } | } | ||||||
| @@ -81,7 +81,7 @@ impl Cli { | |||||||
|  |  | ||||||
|     match &self.command { |     match &self.command { | ||||||
|       CliCommand::Connect(args) => ConnectHandler::new(args, &shared_args).handle().await, |       CliCommand::Connect(args) => ConnectHandler::new(args, &shared_args).handle().await, | ||||||
|       CliCommand::Disconnect => DisconnectHandler::new().handle(), |       CliCommand::Disconnect(args) => DisconnectHandler::new(args).handle().await, | ||||||
|       CliCommand::LaunchGui(args) => LaunchGuiHandler::new(args).handle().await, |       CliCommand::LaunchGui(args) => LaunchGuiHandler::new(args).handle().await, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -84,11 +84,11 @@ pub(crate) struct ConnectArgs { | |||||||
|   #[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")] |   #[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")] | ||||||
|   user_agent: String, |   user_agent: String, | ||||||
|  |  | ||||||
|   #[arg(long, default_value = "Linux")] |   #[arg(long, value_enum, default_value_t = ConnectArgs::default_os())] | ||||||
|   os: Os, |   os: Os, | ||||||
|  |  | ||||||
|   #[arg(long)] |   #[arg(long, default_value_t = ConnectArgs::default_os_version())] | ||||||
|   os_version: Option<String>, |   os_version: String, | ||||||
|  |  | ||||||
|   #[arg(long, help = "Disable DTLS and ESP")] |   #[arg(long, help = "Disable DTLS and ESP")] | ||||||
|   no_dtls: bool, |   no_dtls: bool, | ||||||
| @@ -110,12 +110,16 @@ pub(crate) struct ConnectArgs { | |||||||
| } | } | ||||||
|  |  | ||||||
| impl ConnectArgs { | impl ConnectArgs { | ||||||
|   fn os_version(&self) -> String { |   fn default_os() -> Os { | ||||||
|     if let Some(os_version) = &self.os_version { |     if cfg!(target_os = "macos") { | ||||||
|       return os_version.to_owned(); |       Os::Mac | ||||||
|  |     } else { | ||||||
|  |       Os::Linux | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|     match self.os { |   fn default_os_version() -> String { | ||||||
|  |     match ConnectArgs::default_os() { | ||||||
|       Os::Linux => format!("Linux {}", whoami::distro()), |       Os::Linux => format!("Linux {}", whoami::distro()), | ||||||
|       Os::Windows => String::from("Microsoft Windows 11 Pro , 64-bit"), |       Os::Windows => String::from("Microsoft Windows 11 Pro , 64-bit"), | ||||||
|       Os::Mac => String::from("Apple Mac OS X 13.4.0"), |       Os::Mac => String::from("Apple Mac OS X 13.4.0"), | ||||||
| @@ -142,7 +146,7 @@ impl<'a> ConnectHandler<'a> { | |||||||
|     GpParams::builder() |     GpParams::builder() | ||||||
|       .user_agent(&self.args.user_agent) |       .user_agent(&self.args.user_agent) | ||||||
|       .client_os(ClientOs::from(&self.args.os)) |       .client_os(ClientOs::from(&self.args.os)) | ||||||
|       .os_version(self.args.os_version()) |       .os_version(self.args.os_version.clone()) | ||||||
|       .ignore_tls_errors(self.shared_args.ignore_tls_errors) |       .ignore_tls_errors(self.shared_args.ignore_tls_errors) | ||||||
|       .certificate(self.args.certificate.clone()) |       .certificate(self.args.certificate.clone()) | ||||||
|       .sslkey(self.args.sslkey.clone()) |       .sslkey(self.args.sslkey.clone()) | ||||||
| @@ -355,7 +359,7 @@ impl<'a> ConnectHandler<'a> { | |||||||
|           .saml_request(prelogin.saml_request()) |           .saml_request(prelogin.saml_request()) | ||||||
|           .user_agent(&self.args.user_agent) |           .user_agent(&self.args.user_agent) | ||||||
|           .os(self.args.os.as_str()) |           .os(self.args.os.as_str()) | ||||||
|           .os_version(Some(&self.args.os_version())) |           .os_version(Some(&self.args.os_version)) | ||||||
|           .hidpi(self.args.hidpi) |           .hidpi(self.args.hidpi) | ||||||
|           .fix_openssl(self.shared_args.fix_openssl) |           .fix_openssl(self.shared_args.fix_openssl) | ||||||
|           .ignore_tls_errors(self.shared_args.ignore_tls_errors) |           .ignore_tls_errors(self.shared_args.ignore_tls_errors) | ||||||
|   | |||||||
| @@ -1,31 +1,63 @@ | |||||||
| use crate::GP_CLIENT_LOCK_FILE; | use crate::GP_CLIENT_LOCK_FILE; | ||||||
|  | use clap::Args; | ||||||
|  | use gpapi::utils::lock_file::gpservice_lock_info; | ||||||
| use log::{info, warn}; | use log::{info, warn}; | ||||||
| use std::fs; | use std::{fs, str::FromStr, thread, time::Duration}; | ||||||
| use sysinfo::{Pid, ProcessExt, Signal, System, SystemExt}; | use sysinfo::{Pid, ProcessExt, Signal, System, SystemExt}; | ||||||
|  |  | ||||||
| pub(crate) struct DisconnectHandler; | #[derive(Args)] | ||||||
|  | pub struct DisconnectArgs { | ||||||
|  |   #[arg( | ||||||
|  |     long, | ||||||
|  |     required = false, | ||||||
|  |     help = "The time in seconds to wait for the VPN connection to disconnect" | ||||||
|  |   )] | ||||||
|  |   wait: Option<u64>, | ||||||
|  | } | ||||||
|  |  | ||||||
| impl DisconnectHandler { | pub struct DisconnectHandler<'a> { | ||||||
|   pub(crate) fn new() -> Self { |   args: &'a DisconnectArgs, | ||||||
|     Self | } | ||||||
|  |  | ||||||
|  | impl<'a> DisconnectHandler<'a> { | ||||||
|  |   pub fn new(args: &'a DisconnectArgs) -> Self { | ||||||
|  |     Self { args } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   pub(crate) fn handle(&self) -> anyhow::Result<()> { |   pub async fn handle(&self) -> anyhow::Result<()> { | ||||||
|     if fs::metadata(GP_CLIENT_LOCK_FILE).is_err() { |     // Try to disconnect the CLI client | ||||||
|       warn!("PID file not found, maybe the client is not running"); |     if let Ok(c) = fs::read_to_string(GP_CLIENT_LOCK_FILE) { | ||||||
|       return Ok(()); |       send_signal(c.trim(), Signal::Interrupt).unwrap_or_else(|err| { | ||||||
|  |         warn!("Failed to send signal to client: {}", err); | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Try to disconnect the GUI service | ||||||
|  |     if let Ok(c) = gpservice_lock_info().await { | ||||||
|  |       send_signal(&c.pid.to_string(), Signal::User1).unwrap_or_else(|err| { | ||||||
|  |         warn!("Failed to send signal to service: {}", err); | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // sleep, to give the client and service time to disconnect | ||||||
|  |     if let Some(wait) = self.args.wait { | ||||||
|  |       thread::sleep(Duration::from_secs(wait)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     let pid = fs::read_to_string(GP_CLIENT_LOCK_FILE)?; |     Ok(()) | ||||||
|     let pid = pid.trim().parse::<usize>()?; |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn send_signal(pid: &str, signal: Signal) -> anyhow::Result<()> { | ||||||
|   let s = System::new_all(); |   let s = System::new_all(); | ||||||
|  |   let pid = Pid::from_str(pid)?; | ||||||
|  |  | ||||||
|     if let Some(process) = s.process(Pid::from(pid)) { |   if let Some(process) = s.process(pid) { | ||||||
|       info!("Found process {}, killing...", pid); |     info!("Found process {}, sending signal...", pid); | ||||||
|       if process.kill_with(Signal::Interrupt).is_none() { |  | ||||||
|  |     if process.kill_with(signal).is_none() { | ||||||
|       warn!("Failed to kill process {}", pid); |       warn!("Failed to kill process {}", pid); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   Ok(()) |   Ok(()) | ||||||
|   } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ gpapi = { path = "../../crates/gpapi" } | |||||||
| openconnect = { path = "../../crates/openconnect" } | openconnect = { path = "../../crates/openconnect" } | ||||||
| clap.workspace = true | clap.workspace = true | ||||||
| anyhow.workspace = true | anyhow.workspace = true | ||||||
| tokio.workspace = true | tokio = { workspace = true, features = ["rt-multi-thread"] } | ||||||
| tokio-util.workspace = true | tokio-util.workspace = true | ||||||
| axum = { workspace = true, features = ["ws"] } | axum = { workspace = true, features = ["ws"] } | ||||||
| futures.workspace = true | futures.workspace = true | ||||||
|   | |||||||
| @@ -30,7 +30,8 @@ struct Cli { | |||||||
|  |  | ||||||
| impl Cli { | impl Cli { | ||||||
|   async fn run(&mut self, redaction: Arc<Redaction>) -> anyhow::Result<()> { |   async fn run(&mut self, redaction: Arc<Redaction>) -> anyhow::Result<()> { | ||||||
|     let lock_file = Arc::new(LockFile::new(GP_SERVICE_LOCK_FILE)); |     let pid = std::process::id(); | ||||||
|  |     let lock_file = Arc::new(LockFile::new(GP_SERVICE_LOCK_FILE, pid)); | ||||||
|  |  | ||||||
|     if lock_file.check_health().await { |     if lock_file.check_health().await { | ||||||
|       bail!("Another instance of the service is already running"); |       bail!("Another instance of the service is already running"); | ||||||
| @@ -48,9 +49,17 @@ impl Cli { | |||||||
|  |  | ||||||
|     let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(4); |     let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(4); | ||||||
|     let shutdown_tx_clone = shutdown_tx.clone(); |     let shutdown_tx_clone = shutdown_tx.clone(); | ||||||
|     let vpn_task_token = vpn_task.cancel_token(); |     let vpn_task_cancel_token = vpn_task.cancel_token(); | ||||||
|     let server_token = ws_server.cancel_token(); |     let server_token = ws_server.cancel_token(); | ||||||
|  |  | ||||||
|  |     #[cfg(unix)] | ||||||
|  |     { | ||||||
|  |       let vpn_ctx = vpn_task.context(); | ||||||
|  |       let ws_ctx = ws_server.context(); | ||||||
|  |  | ||||||
|  |       tokio::spawn(async move { signals::handle_signals(vpn_ctx, ws_ctx).await }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     let vpn_task_handle = tokio::spawn(async move { vpn_task.start(server_token).await }); |     let vpn_task_handle = tokio::spawn(async move { vpn_task.start(server_token).await }); | ||||||
|     let ws_server_handle = tokio::spawn(async move { ws_server.start(shutdown_tx_clone).await }); |     let ws_server_handle = tokio::spawn(async move { ws_server.start(shutdown_tx_clone).await }); | ||||||
|  |  | ||||||
| @@ -82,7 +91,7 @@ impl Cli { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     vpn_task_token.cancel(); |     vpn_task_cancel_token.cancel(); | ||||||
|     let _ = tokio::join!(vpn_task_handle, ws_server_handle); |     let _ = tokio::join!(vpn_task_handle, ws_server_handle); | ||||||
|  |  | ||||||
|     lock_file.unlock()?; |     lock_file.unlock()?; | ||||||
| @@ -125,6 +134,54 @@ fn init_logger() -> Arc<Redaction> { | |||||||
|   redaction |   redaction | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[cfg(unix)] | ||||||
|  | mod signals { | ||||||
|  |   use std::sync::Arc; | ||||||
|  |  | ||||||
|  |   use log::{info, warn}; | ||||||
|  |  | ||||||
|  |   use crate::vpn_task::VpnTaskContext; | ||||||
|  |   use crate::ws_server::WsServerContext; | ||||||
|  |  | ||||||
|  |   const DISCONNECTED_PID_FILE: &str = "/tmp/gpservice_disconnected.pid"; | ||||||
|  |  | ||||||
|  |   pub async fn handle_signals(vpn_ctx: Arc<VpnTaskContext>, ws_ctx: Arc<WsServerContext>) { | ||||||
|  |     use gpapi::service::event::WsEvent; | ||||||
|  |     use tokio::signal::unix::{signal, Signal, SignalKind}; | ||||||
|  |  | ||||||
|  |     let (mut user_sig1, mut user_sig2) = match || -> anyhow::Result<(Signal, Signal)> { | ||||||
|  |       let user_sig1 = signal(SignalKind::user_defined1())?; | ||||||
|  |       let user_sig2 = signal(SignalKind::user_defined2())?; | ||||||
|  |       Ok((user_sig1, user_sig2)) | ||||||
|  |     }() { | ||||||
|  |       Ok(signals) => signals, | ||||||
|  |       Err(err) => { | ||||||
|  |         warn!("Failed to create signal: {}", err); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     loop { | ||||||
|  |       tokio::select! { | ||||||
|  |         _ = user_sig1.recv() => { | ||||||
|  |           info!("Received SIGUSR1 signal"); | ||||||
|  |           if vpn_ctx.disconnect().await { | ||||||
|  |             // Write the PID to a dedicated file to indicate that the VPN task is disconnected via SIGUSR1 | ||||||
|  |             let pid = std::process::id(); | ||||||
|  |             if let Err(err) = tokio::fs::write(DISCONNECTED_PID_FILE, pid.to_string()).await { | ||||||
|  |               warn!("Failed to write PID to file: {}", err); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         _ = user_sig2.recv() => { | ||||||
|  |           info!("Received SIGUSR2 signal"); | ||||||
|  |           ws_ctx.send_event(WsEvent::ResumeConnection).await; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| async fn launch_gui(envs: Option<HashMap<String, String>>, api_key: Vec<u8>, mut minimized: bool) { | async fn launch_gui(envs: Option<HashMap<String, String>>, api_key: Vec<u8>, mut minimized: bool) { | ||||||
|   loop { |   loop { | ||||||
|     let gui_launcher = GuiLauncher::new(env!("CARGO_PKG_VERSION"), &api_key) |     let gui_launcher = GuiLauncher::new(env!("CARGO_PKG_VERSION"), &api_key) | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| use std::{ | use std::{ | ||||||
|   borrow::Cow, |  | ||||||
|   fs::{File, Permissions}, |   fs::{File, Permissions}, | ||||||
|   io::BufReader, |   io::BufReader, | ||||||
|   ops::ControlFlow, |   ops::ControlFlow, | ||||||
| @@ -12,7 +11,7 @@ use anyhow::bail; | |||||||
| use axum::{ | use axum::{ | ||||||
|   body::Bytes, |   body::Bytes, | ||||||
|   extract::{ |   extract::{ | ||||||
|     ws::{self, CloseFrame, Message, WebSocket}, |     ws::{self, CloseFrame, Message, Utf8Bytes, WebSocket}, | ||||||
|     State, WebSocketUpgrade, |     State, WebSocketUpgrade, | ||||||
|   }, |   }, | ||||||
|   http::StatusCode, |   http::StatusCode, | ||||||
| @@ -137,7 +136,7 @@ async fn handle_socket(mut socket: WebSocket, ctx: Arc<WsServerContext>) { | |||||||
|  |  | ||||||
|     let close_msg = Message::Close(Some(CloseFrame { |     let close_msg = Message::Close(Some(CloseFrame { | ||||||
|       code: ws::close_code::NORMAL, |       code: ws::close_code::NORMAL, | ||||||
|       reason: Cow::from("Goodbye"), |       reason: Utf8Bytes::from("Goodbye"), | ||||||
|     })); |     })); | ||||||
|  |  | ||||||
|     if let Err(err) = sender.send(close_msg).await { |     if let Err(err) = sender.send(close_msg).await { | ||||||
|   | |||||||
| @@ -87,7 +87,7 @@ impl VpnTaskContext { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   pub async fn disconnect(&self) { |   pub async fn disconnect(&self) -> bool { | ||||||
|     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..."); |       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() { | ||||||
| @@ -98,9 +98,13 @@ impl VpnTaskContext { | |||||||
|       // Wait for the VPN to be disconnected |       // Wait for the VPN to be disconnected | ||||||
|       disconnect_rx.await.ok(); |       disconnect_rx.await.ok(); | ||||||
|       info!("VPN disconnected"); |       info!("VPN disconnected"); | ||||||
|  |  | ||||||
|  |       true | ||||||
|     } else { |     } else { | ||||||
|       info!("VPN is not connected, skip disconnect"); |       info!("VPN is not connected, skip disconnect"); | ||||||
|       self.vpn_state_tx.send(VpnState::Disconnected).ok(); |       self.vpn_state_tx.send(VpnState::Disconnected).ok(); | ||||||
|  |  | ||||||
|  |       false | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -143,6 +147,10 @@ impl VpnTask { | |||||||
|     server_cancel_token.cancel(); |     server_cancel_token.cancel(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn context(&self) -> Arc<VpnTaskContext> { | ||||||
|  |     return Arc::clone(&self.ctx); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   async fn recv(&mut self) { |   async fn recv(&mut self) { | ||||||
|     while let Some(req) = self.ws_req_rx.recv().await { |     while let Some(req) = self.ws_req_rx.recv().await { | ||||||
|       tokio::spawn(process_ws_req(req, self.ctx.clone())); |       tokio::spawn(process_ws_req(req, self.ctx.clone())); | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ impl WsConnection { | |||||||
|  |  | ||||||
|   pub async fn send_event(&self, event: &WsEvent) -> anyhow::Result<()> { |   pub async fn send_event(&self, event: &WsEvent) -> anyhow::Result<()> { | ||||||
|     let encrypted = self.crypto.encrypt(event)?; |     let encrypted = self.crypto.encrypt(event)?; | ||||||
|     let msg = Message::Binary(encrypted); |     let msg = Message::Binary(encrypted.into()); | ||||||
|  |  | ||||||
|     self.tx.send(msg).await?; |     self.tx.send(msg).await?; | ||||||
|  |  | ||||||
| @@ -29,7 +29,7 @@ impl WsConnection { | |||||||
|  |  | ||||||
|   pub fn recv_msg(&self, msg: Message) -> ControlFlow<(), WsRequest> { |   pub fn recv_msg(&self, msg: Message) -> ControlFlow<(), WsRequest> { | ||||||
|     match msg { |     match msg { | ||||||
|       Message::Binary(data) => match self.crypto.decrypt(data) { |       Message::Binary(data) => match self.crypto.decrypt(data.into()) { | ||||||
|         Ok(ws_req) => ControlFlow::Continue(ws_req), |         Ok(ws_req) => ControlFlow::Continue(ws_req), | ||||||
|         Err(err) => { |         Err(err) => { | ||||||
|           info!("Failed to decrypt message: {}", err); |           info!("Failed to decrypt message: {}", err); | ||||||
|   | |||||||
| @@ -113,6 +113,10 @@ impl WsServer { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   pub fn context(&self) -> Arc<WsServerContext> { | ||||||
|  |     Arc::clone(&self.ctx) | ||||||
|  |   } | ||||||
|  |  | ||||||
|   pub fn cancel_token(&self) -> CancellationToken { |   pub fn cancel_token(&self) -> CancellationToken { | ||||||
|     self.cancel_token.clone() |     self.cancel_token.clone() | ||||||
|   } |   } | ||||||
| @@ -124,7 +128,7 @@ impl WsServer { | |||||||
|         warn!("Failed to start WS server: {}", err); |         warn!("Failed to start WS server: {}", err); | ||||||
|         let _ = shutdown_tx.send(()).await; |         let _ = shutdown_tx.send(()).await; | ||||||
|         return; |         return; | ||||||
|       }, |       } | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     tokio::select! { |     tokio::select! { | ||||||
| @@ -149,7 +153,7 @@ impl WsServer { | |||||||
|  |  | ||||||
|     info!("WS server listening on port: {}", port); |     info!("WS server listening on port: {}", port); | ||||||
|  |  | ||||||
|     self.lock_file.lock(port.to_string())?; |     self.lock_file.lock(&port.to_string())?; | ||||||
|  |  | ||||||
|     Ok(listener) |     Ok(listener) | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ serde.workspace = true | |||||||
| specta.workspace = true | specta.workspace = true | ||||||
| specta-macros.workspace = true | specta-macros.workspace = true | ||||||
| urlencoding.workspace = true | urlencoding.workspace = true | ||||||
| tokio.workspace = true | tokio = { workspace = true, features = ["process", "signal", "macros"] } | ||||||
| serde_json.workspace = true | serde_json.workspace = true | ||||||
| whoami.workspace = true | whoami.workspace = true | ||||||
| tempfile.workspace = true | tempfile.workspace = true | ||||||
| @@ -32,6 +32,9 @@ md5.workspace = true | |||||||
| sha256.workspace = true | sha256.workspace = true | ||||||
| which.workspace = true | which.workspace = true | ||||||
|  |  | ||||||
|  | # Pin the version of home because the latest version requires Rust 1.81 | ||||||
|  | home = "=0.5.9" | ||||||
|  |  | ||||||
| tauri = { workspace = true, optional = true } | tauri = { workspace = true, optional = true } | ||||||
| clap = { workspace = true, optional = true } | clap = { workspace = true, optional = true } | ||||||
| open = { version = "5", optional = true } | open = { version = "5", optional = true } | ||||||
|   | |||||||
| @@ -9,4 +9,5 @@ pub enum WsEvent { | |||||||
|   ActiveGui, |   ActiveGui, | ||||||
|   /// External authentication data |   /// External authentication data | ||||||
|   AuthData(String), |   AuthData(String), | ||||||
|  |   ResumeConnection, | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,10 +1,9 @@ | |||||||
| use tokio::fs; | use super::lock_file::gpservice_lock_info; | ||||||
|  |  | ||||||
| use crate::GP_SERVICE_LOCK_FILE; |  | ||||||
|  |  | ||||||
| async fn read_port() -> anyhow::Result<String> { | async fn read_port() -> anyhow::Result<String> { | ||||||
|   let port = fs::read_to_string(GP_SERVICE_LOCK_FILE).await?; |   let lock_info = gpservice_lock_info().await?; | ||||||
|   Ok(port.trim().to_string()) |  | ||||||
|  |   Ok(lock_info.port.to_string()) | ||||||
| } | } | ||||||
|  |  | ||||||
| pub async fn http_endpoint() -> anyhow::Result<String> { | pub async fn http_endpoint() -> anyhow::Result<String> { | ||||||
|   | |||||||
| @@ -1,19 +1,24 @@ | |||||||
| use std::path::PathBuf; | use std::path::PathBuf; | ||||||
|  |  | ||||||
|  | use thiserror::Error; | ||||||
|  | use tokio::fs; | ||||||
|  |  | ||||||
| pub struct LockFile { | pub struct LockFile { | ||||||
|   path: PathBuf, |   path: PathBuf, | ||||||
|  |   pid: u32, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl LockFile { | impl LockFile { | ||||||
|   pub fn new<P: Into<PathBuf>>(path: P) -> Self { |   pub fn new<P: Into<PathBuf>>(path: P, pid: u32) -> Self { | ||||||
|     Self { path: path.into() } |     Self { path: path.into(), pid } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   pub fn exists(&self) -> bool { |   pub fn exists(&self) -> bool { | ||||||
|     self.path.exists() |     self.path.exists() | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   pub fn lock(&self, content: impl AsRef<[u8]>) -> anyhow::Result<()> { |   pub fn lock(&self, content: &str) -> anyhow::Result<()> { | ||||||
|  |     let content = format!("{}:{}", self.pid, content); | ||||||
|     std::fs::write(&self.path, content)?; |     std::fs::write(&self.path, content)?; | ||||||
|     Ok(()) |     Ok(()) | ||||||
|   } |   } | ||||||
| @@ -37,3 +42,87 @@ impl LockFile { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[derive(Error, Debug)] | ||||||
|  | pub enum LockFileError { | ||||||
|  |   #[error("Failed to read lock file: {0}")] | ||||||
|  |   IoError(#[from] std::io::Error), | ||||||
|  |  | ||||||
|  |   #[error("Invalid lock file format: expected 'pid:port'")] | ||||||
|  |   InvalidFormat, | ||||||
|  |  | ||||||
|  |   #[error("Invalid PID value: {0}")] | ||||||
|  |   InvalidPid(std::num::ParseIntError), | ||||||
|  |  | ||||||
|  |   #[error("Invalid port value: {0}")] | ||||||
|  |   InvalidPort(std::num::ParseIntError), | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub struct LockInfo { | ||||||
|  |   pub pid: u32, | ||||||
|  |   pub port: u32, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl LockInfo { | ||||||
|  |   async fn from_file(path: impl AsRef<std::path::Path>) -> Result<Self, LockFileError> { | ||||||
|  |     let content = fs::read_to_string(path).await?; | ||||||
|  |     Self::parse(&content) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   fn parse(content: &str) -> Result<Self, LockFileError> { | ||||||
|  |     let mut parts = content.trim().split(':'); | ||||||
|  |  | ||||||
|  |     let pid = parts | ||||||
|  |       .next() | ||||||
|  |       .ok_or(LockFileError::InvalidFormat)? | ||||||
|  |       .parse() | ||||||
|  |       .map_err(LockFileError::InvalidPid)?; | ||||||
|  |  | ||||||
|  |     let port = parts | ||||||
|  |       .next() | ||||||
|  |       .ok_or(LockFileError::InvalidFormat)? | ||||||
|  |       .parse() | ||||||
|  |       .map_err(LockFileError::InvalidPort)?; | ||||||
|  |  | ||||||
|  |     // Ensure there are no extra parts after pid:port | ||||||
|  |     if parts.next().is_some() { | ||||||
|  |       return Err(LockFileError::InvalidFormat); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Ok(Self { pid, port }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub async fn gpservice_lock_info() -> Result<LockInfo, LockFileError> { | ||||||
|  |   LockInfo::from_file(crate::GP_SERVICE_LOCK_FILE).await | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[cfg(test)] | ||||||
|  | mod tests { | ||||||
|  |   use super::*; | ||||||
|  |  | ||||||
|  |   #[test] | ||||||
|  |   fn test_parse_valid_input() { | ||||||
|  |     let info = LockInfo::parse("1234:8080").unwrap(); | ||||||
|  |     assert_eq!(info.pid, 1234); | ||||||
|  |     assert_eq!(info.port, 8080); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   #[test] | ||||||
|  |   fn test_parse_invalid_format() { | ||||||
|  |     assert!(matches!( | ||||||
|  |       LockInfo::parse("123:456:789"), | ||||||
|  |       Err(LockFileError::InvalidFormat) | ||||||
|  |     )); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   #[test] | ||||||
|  |   fn test_parse_invalid_numbers() { | ||||||
|  |     assert!(matches!(LockInfo::parse("abc:8080"), Err(LockFileError::InvalidPid(_)))); | ||||||
|  |  | ||||||
|  |     assert!(matches!( | ||||||
|  |       LockInfo::parse("1234:abc"), | ||||||
|  |       Err(LockFileError::InvalidPort(_)) | ||||||
|  |     )); | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -10,6 +10,10 @@ install: | |||||||
| 		install -Dm755 artifacts/usr/bin/gpgui $(DESTDIR)/usr/bin/gpgui; \ | 		install -Dm755 artifacts/usr/bin/gpgui $(DESTDIR)/usr/bin/gpgui; \ | ||||||
| 	fi | 	fi | ||||||
|  |  | ||||||
|  | 	# Install the disconnect hooks | ||||||
|  | 	install -Dm755 artifacts/usr/lib/NetworkManager/dispatcher.d/pre-down.d/gpclient.down $(DESTDIR)/usr/lib/NetworkManager/dispatcher.d/pre-down.d/gpclient.down | ||||||
|  | 	install -Dm755 artifacts/usr/lib/NetworkManager/dispatcher.d/gpclient-nm-hook $(DESTDIR)/usr/lib/NetworkManager/dispatcher.d/gpclient-nm-hook | ||||||
|  |  | ||||||
| 	install -Dm644 artifacts/usr/share/applications/gpgui.desktop $(DESTDIR)/usr/share/applications/gpgui.desktop | 	install -Dm644 artifacts/usr/share/applications/gpgui.desktop $(DESTDIR)/usr/share/applications/gpgui.desktop | ||||||
| 	install -Dm644 artifacts/usr/share/icons/hicolor/scalable/apps/gpgui.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/gpgui.svg | 	install -Dm644 artifacts/usr/share/icons/hicolor/scalable/apps/gpgui.svg $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/gpgui.svg | ||||||
| 	install -Dm644 artifacts/usr/share/icons/hicolor/32x32/apps/gpgui.png $(DESTDIR)/usr/share/icons/hicolor/32x32/apps/gpgui.png | 	install -Dm644 artifacts/usr/share/icons/hicolor/32x32/apps/gpgui.png $(DESTDIR)/usr/share/icons/hicolor/32x32/apps/gpgui.png | ||||||
| @@ -26,6 +30,9 @@ uninstall: | |||||||
| 	rm -f $(DESTDIR)/usr/bin/gpgui-helper | 	rm -f $(DESTDIR)/usr/bin/gpgui-helper | ||||||
| 	rm -f $(DESTDIR)/usr/bin/gpgui | 	rm -f $(DESTDIR)/usr/bin/gpgui | ||||||
|  |  | ||||||
|  | 	rm -f $(DESTDIR)/usr/lib/NetworkManager/dispatcher.d/pre-down.d/gpclient.down | ||||||
|  | 	rm -f $(DESTDIR)/usr/lib/NetworkManager/dispatcher.d/gpclient-nm-hook | ||||||
|  |  | ||||||
| 	rm -f $(DESTDIR)/usr/share/applications/gpgui.desktop | 	rm -f $(DESTDIR)/usr/share/applications/gpgui.desktop | ||||||
| 	rm -f $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/gpgui.svg | 	rm -f $(DESTDIR)/usr/share/icons/hicolor/scalable/apps/gpgui.svg | ||||||
| 	rm -f $(DESTDIR)/usr/share/icons/hicolor/32x32/apps/gpgui.png | 	rm -f $(DESTDIR)/usr/share/icons/hicolor/32x32/apps/gpgui.png | ||||||
|   | |||||||
							
								
								
									
										26
									
								
								packaging/files/usr/lib/NetworkManager/dispatcher.d/gpclient-nm-hook
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										26
									
								
								packaging/files/usr/lib/NetworkManager/dispatcher.d/gpclient-nm-hook
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | #!/bin/sh | ||||||
|  |  | ||||||
|  | # Resume the VPN connection if the network comes back up | ||||||
|  |  | ||||||
|  | set -e | ||||||
|  |  | ||||||
|  | PIDFILE=/tmp/gpservice_disconnected.pid | ||||||
|  |  | ||||||
|  | resume_vpn() { | ||||||
|  |   if [ -f $PIDFILE ]; then | ||||||
|  |     PID=$(cat $PIDFILE) | ||||||
|  |  | ||||||
|  |     # Always remove the PID file | ||||||
|  |     rm $PIDFILE | ||||||
|  |  | ||||||
|  |     # Ensure the PID is a gpservice process | ||||||
|  |     if ps -p $PID -o comm= | grep -q gpservice; then | ||||||
|  |       # Send a USR2 signal to the gpclient process to resume the VPN connection | ||||||
|  |       kill -USR2 $PID | ||||||
|  |     fi | ||||||
|  |   fi | ||||||
|  | } | ||||||
|  |  | ||||||
|  | if [ "$2" = "up" ]; then | ||||||
|  |   resume_vpn | ||||||
|  | fi | ||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | #!/bin/sh | ||||||
|  |  | ||||||
|  | set -e | ||||||
|  |  | ||||||
|  | # Disconnect the VPN connection before the network goes down | ||||||
|  | /usr/bin/gpclient disconnect --wait 3 | ||||||
| @@ -55,6 +55,13 @@ make build OFFLINE=@OFFLINE@ BUILD_FE=0 | |||||||
| %{_datadir}/icons/hicolor/scalable/apps/gpgui.svg | %{_datadir}/icons/hicolor/scalable/apps/gpgui.svg | ||||||
| %{_datadir}/polkit-1/actions/com.yuezk.gpgui.policy | %{_datadir}/polkit-1/actions/com.yuezk.gpgui.policy | ||||||
|  |  | ||||||
|  | %dir /usr/lib/NetworkManager | ||||||
|  | %dir /usr/lib/NetworkManager/dispatcher.d | ||||||
|  | %dir /usr/lib/NetworkManager/dispatcher.d/pre-down.d | ||||||
|  |  | ||||||
|  | /usr/lib/NetworkManager/dispatcher.d/pre-down.d/gpclient.down | ||||||
|  | /usr/lib/NetworkManager/dispatcher.d/gpclient-nm-hook | ||||||
|  |  | ||||||
| %dir %{_datadir}/icons/hicolor | %dir %{_datadir}/icons/hicolor | ||||||
| %dir %{_datadir}/icons/hicolor/32x32 | %dir %{_datadir}/icons/hicolor/32x32 | ||||||
| %dir %{_datadir}/icons/hicolor/32x32/apps | %dir %{_datadir}/icons/hicolor/32x32/apps | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user