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..5b3de094 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,43 @@ 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). + // 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. + // 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).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 + } + + // 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 } if i < checkLevels-1 { // no need to check parent if we are at the last level diff --git a/service/profile/special.go b/service/profile/special.go index 55b466ec..cfff90bc 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: false, // This is stronger than the rules, and thus must be false in order to access safing.io. + CfgOptionBlockScopeLANKey: true, + CfgOptionBlockScopeLocalKey: true, + CfgOptionBlockP2PKey: true, CfgOptionBlockInboundKey: true, CfgOptionEndpointsKey: []string{ - "+ Localhost", "+ .safing.io", }, },