From 11c4ae39d238056e67a3435ba8c48b6c59855e38 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Wed, 21 May 2025 18:08:04 +0300 Subject: [PATCH 1/4] (Windows) Fix false-positive detection of Portmaster UI processes Problem: System browsers launched from the Portmaster UI (e.g., when a user clicks a link) may be incorrectly detected as Portmaster UI child processes. Solution: The Tauri UI app now sets the PORTMASTER_UI_WEBVIEW_PROCESS environment variable for all child WebView processes. Portmaster-core uses this variable to accurately determine if a process is truly related to the Portmaster UI. --- desktop/tauri/src-tauri/src/window.rs | 29 +++++++++++++++++++++++++++ service/process/profile.go | 29 ++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/desktop/tauri/src-tauri/src/window.rs b/desktop/tauri/src-tauri/src/window.rs index 43c03d46..50bfb4af 100644 --- a/desktop/tauri/src-tauri/src/window.rs +++ b/desktop/tauri/src-tauri/src/window.rs @@ -9,6 +9,8 @@ use crate::{portmaster::PortmasterExt, traymenu}; const LIGHT_PM_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_light_512.png"); const DARK_PM_ICON: &[u8] = include_bytes!("../../../../assets/data/icons/pm_dark_512.png"); +const CUSTOM_ENVVAR_FOR_WEBVIEW_PROCESS: &str = "PORTMASTER_UI_WEBVIEW_PROCESS"; + /// Either returns the existing "main" window or creates a new one. /// /// The window is not automatically shown (i.e it starts hidden). @@ -24,6 +26,7 @@ pub fn create_main_window(app: &AppHandle) -> Result { } else { debug!("[tauri] creating main window"); + do_before_window_create(); // required operations before window creation let res = WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into())) .title("Portmaster") .visible(false) @@ -31,6 +34,7 @@ pub fn create_main_window(app: &AppHandle) -> Result { .min_inner_size(800.0, 600.0) .theme(Some(Theme::Dark)) .build(); + do_after_window_create(); // required operations after window creation match res { Ok(win) => { @@ -69,6 +73,8 @@ pub fn create_splash_window(app: &AppHandle) -> Result { let _ = window.show(); Ok(window) } else { + + do_before_window_create(); // required operations before window creation let window = WebviewWindowBuilder::new(app, "splash", WebviewUrl::App("index.html".into())) .center() .closable(false) @@ -78,6 +84,7 @@ pub fn create_splash_window(app: &AppHandle) -> Result { .title("Portmaster") .inner_size(600.0, 250.0) .build()?; + do_after_window_create(); // required operations after window creation set_window_icon(&window); let _ = window.request_user_attention(Some(UserAttentionType::Informational)); @@ -118,6 +125,28 @@ pub fn set_window_icon(window: &WebviewWindow) { }; } +/// This function must be called before the window is created. +/// +/// Temporarily sets the environment variable `PORTMASTER_WEBVIEW_UI_PROCESS` to "true". +/// This ensures that any child process (i.e., the WebView process) spawned during window creation +/// will inherit this environment variable. This allows portmaster-core to detect that the process +/// is a child WebView of the main process. +/// +/// IMPORTANT: After the window is created, you must call `do_after_window_create()` to remove +/// the environment variable from the main process environment. +pub fn do_before_window_create() { + std::env::set_var(CUSTOM_ENVVAR_FOR_WEBVIEW_PROCESS, "true"); +} + +/// This function must be called after the window is created. +/// +/// Removes the `PORTMASTER_WEBVIEW_UI_PROCESS` environment variable from the main process. +/// This ensures that only the child WebView process has the variable set, and the main process +/// does not retain it. +pub fn do_after_window_create() { + std::env::remove_var(CUSTOM_ENVVAR_FOR_WEBVIEW_PROCESS); +} + /// Opens a window for the tauri application. /// /// If the main window has already been created, it is instructed to diff --git a/service/process/profile.go b/service/process/profile.go index 7653bd72..8217aa9b 100644 --- a/service/process/profile.go +++ b/service/process/profile.go @@ -118,6 +118,8 @@ func (p *Process) IsPortmasterUi(ctx context.Context) bool { var previousPid int proc := p + hasPmWebviewEnvVar := false + for i := 0; i < checkLevels; i++ { if proc.Pid == UnidentifiedProcessID || proc.Pid == SystemProcessID { break @@ -125,7 +127,32 @@ func (p *Process) IsPortmasterUi(ctx context.Context) bool { realPath, err := filepath.EvalSymlinks(proc.Path) if err == nil && realPath == module.portmasterUIPath { - return true + if runtime.GOOS != "windows" { + return true + } + + // On Windows, avoid false positive detection of the Portmaster UI. + // For example: + // There may be cases where a system browser is launched from the Portmaster UI, + // making it a child of the Portmaster UI process (e.g., user clicked a link in the UI). + // To ensure that 'p' is the actual Portmaster UI process, we check for the presence + // of the 'PORTMASTER_UI_WEBVIEW_PROCESS' environment variable in the process and its parents. + // If the env var is set, we are a child (WebView window) of the Portmaster UI process. + // Otherwise, the process was launched by the Portmaster UI, but should not be trusted as the Portmaster UI process. + if i == 0 { + return true // We are the main Portmaster UI process. + } + if hasPmWebviewEnvVar { + return true // We are a WebView window of the Portmaster UI process. + } + // The process was launched by the Portmaster UI, but should not be trusted as the Portmaster UI process. + log.Tracer(ctx).Warning(fmt.Sprintf("process: %d '%s' is a child of the Portmaster UI, but does not have the PORTMASTER_UI_WEBVIEW_PROCESS environment variable set. Ignoring.", p.Pid, p.Path)) + return false + } + + // Check if the process has the environment variable set. + if _, ok := proc.Env["PORTMASTER_UI_WEBVIEW_PROCESS"]; ok { + hasPmWebviewEnvVar = true } if i < checkLevels-1 { // no need to check parent if we are at the last level From 3b91aa06ba2eae78943b92a21c3a57d87a4c1c28 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Wed, 21 May 2025 18:12:50 +0300 Subject: [PATCH 2/4] Enhance default connection settings for Portmaster UI profile to block all connections, ensuring only necessary connections to Portmaster Core are allowed. --- service/profile/special.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/service/profile/special.go b/service/profile/special.go index 55b466ec..b7674668 100644 --- a/service/profile/special.go +++ b/service/profile/special.go @@ -237,14 +237,20 @@ func createSpecialProfile(profileID string, path string) *Profile { Source: SourceLocal, PresentationPath: path, Config: map[string]interface{}{ + // Block all connections by default for the Portmaster UI profile, + // since the only required connections are to the Portmaster Core, + // which are fast-tracked. + // + // This ensures that any unexpected connections — + // possibly made by the internal WebView implementation — + // are blocked. CfgOptionDefaultActionKey: DefaultActionBlockValue, - CfgOptionBlockScopeInternetKey: false, - CfgOptionBlockScopeLANKey: false, - CfgOptionBlockScopeLocalKey: false, - CfgOptionBlockP2PKey: false, + CfgOptionBlockScopeInternetKey: true, + CfgOptionBlockScopeLANKey: true, + CfgOptionBlockScopeLocalKey: true, + CfgOptionBlockP2PKey: true, CfgOptionBlockInboundKey: true, CfgOptionEndpointsKey: []string{ - "+ Localhost", "+ .safing.io", }, }, From fbc93cc09fbcadf4290bf0fa289b274af053a5c8 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Thu, 22 May 2025 15:30:05 +0300 Subject: [PATCH 3/4] Add more descriptive comments + minor improvements --- service/process/profile.go | 13 ++++++++++++- service/profile/special.go | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/service/process/profile.go b/service/process/profile.go index 8217aa9b..4ade9a0d 100644 --- a/service/process/profile.go +++ b/service/process/profile.go @@ -135,6 +135,12 @@ func (p *Process) IsPortmasterUi(ctx context.Context) bool { // For example: // There may be cases where a system browser is launched from the Portmaster UI, // making it a child of the Portmaster UI process (e.g., user clicked a link in the UI). + // In this case, the parent process tree may look like this: + // Portmaster.exe + // ├─ WebView (PM UI) + // │ └─ WebView (PM UI child) + // └─ System Web Browser ... + // // To ensure that 'p' is the actual Portmaster UI process, we check for the presence // of the 'PORTMASTER_UI_WEBVIEW_PROCESS' environment variable in the process and its parents. // If the env var is set, we are a child (WebView window) of the Portmaster UI process. @@ -146,11 +152,16 @@ func (p *Process) IsPortmasterUi(ctx context.Context) bool { return true // We are a WebView window of the Portmaster UI process. } // The process was launched by the Portmaster UI, but should not be trusted as the Portmaster UI process. - log.Tracer(ctx).Warning(fmt.Sprintf("process: %d '%s' is a child of the Portmaster UI, but does not have the PORTMASTER_UI_WEBVIEW_PROCESS environment variable set. Ignoring.", p.Pid, p.Path)) + log.Tracer(ctx).Warningf("process: %d '%s' is a child of the Portmaster UI, but does not have the PORTMASTER_UI_WEBVIEW_PROCESS environment variable set. Ignoring.", p.Pid, p.Path) return false } // Check if the process has the environment variable set. + // + // It is OK to check for the existence of the environment variable in all + // processes in the parent chain (on all loop iterations). This increases the + // chance of correct detection, even if a child or grandchild WebView process + // did not inherit the environment variable for some reason. if _, ok := proc.Env["PORTMASTER_UI_WEBVIEW_PROCESS"]; ok { hasPmWebviewEnvVar = true } diff --git a/service/profile/special.go b/service/profile/special.go index b7674668..cfff90bc 100644 --- a/service/profile/special.go +++ b/service/profile/special.go @@ -245,7 +245,7 @@ func createSpecialProfile(profileID string, path string) *Profile { // possibly made by the internal WebView implementation — // are blocked. CfgOptionDefaultActionKey: DefaultActionBlockValue, - CfgOptionBlockScopeInternetKey: true, + CfgOptionBlockScopeInternetKey: false, // This is stronger than the rules, and thus must be false in order to access safing.io. CfgOptionBlockScopeLANKey: true, CfgOptionBlockScopeLocalKey: true, CfgOptionBlockP2PKey: true, From 531d147936a264d57dd52c60c3e4e5f54823e9a3 Mon Sep 17 00:00:00 2001 From: Alexandr Stelnykovych Date: Thu, 22 May 2025 15:40:06 +0300 Subject: [PATCH 4/4] Improve logging message format --- service/process/profile.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/process/profile.go b/service/process/profile.go index 4ade9a0d..5b3de094 100644 --- a/service/process/profile.go +++ b/service/process/profile.go @@ -152,7 +152,7 @@ func (p *Process) IsPortmasterUi(ctx context.Context) bool { return true // We are a WebView window of the Portmaster UI process. } // The process was launched by the Portmaster UI, but should not be trusted as the Portmaster UI process. - log.Tracer(ctx).Warningf("process: %d '%s' is a child of the Portmaster UI, but does not have the PORTMASTER_UI_WEBVIEW_PROCESS environment variable set. Ignoring.", p.Pid, p.Path) + log.Tracer(ctx).Warningf("process: %d %q is a child of the Portmaster UI, but does not have the PORTMASTER_UI_WEBVIEW_PROCESS environment variable set. Ignoring.", p.Pid, p.Path) return false }