From 90ec4ca086c6731b02e3b9190817aad274a21616 Mon Sep 17 00:00:00 2001 From: dokril Date: Wed, 22 Jul 2026 16:17:00 +0300 Subject: [PATCH] Add ProxiFyre firewall rule management and bump version to 1.1.0 --- .cargo/config.toml | 4 ++ package-lock.json | 4 +- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- .../cleanup/uninstall-managed-components.ps1 | 12 ++++ src-tauri/src/commands.rs | 19 ++++-- src-tauri/src/component_detection.rs | 33 ++++++++++ src-tauri/src/lib.rs | 1 + src-tauri/src/proxifyre_ownership.rs | 3 +- src-tauri/src/proxifyre_runtime.rs | 56 ++++++++++++++--- src-tauri/src/proxifyre_scripts.rs | 44 ++++++++++++- src-tauri/tauri.conf.json | 2 +- src-tauri/tests/command_tests.rs | 63 +++++++++++++++++++ src-tauri/tests/proxifyre_ownership_tests.rs | 29 +++++++++ src/api/tauriCommands.ts | 4 ++ src/app/App.tsx | 31 ++++++++- 17 files changed, 289 insertions(+), 22 deletions(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..eeb1e04 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,4 @@ +# `cargo run` / `tauri dev` should control the components installed by ProxyWarden, +# not copies that happen to exist beside target\debug\proxywarden.exe. +[env] +PROXYWARDEN_DEV_INSTALL_ROOT = { value = 'C:\Program Files\ProxyWarden', force = false } diff --git a/package-lock.json b/package-lock.json index d2babf8..bf42fec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "proxywarden", - "version": "1.0.3", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "proxywarden", - "version": "1.0.3", + "version": "1.1.0", "dependencies": { "@fontsource-variable/jetbrains-mono": "^5.2.8", "@tauri-apps/api": "^2.0.0", diff --git a/package.json b/package.json index 974a5ac..b02b8fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "proxywarden", - "version": "1.0.3", + "version": "1.1.0", "private": true, "type": "module", "description": "Standalone Windows desktop proxy management app for ProxyWarden.", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ca37d6c..9f65409 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2314,7 +2314,7 @@ dependencies = [ [[package]] name = "proxywarden" -version = "1.0.3" +version = "1.1.0" dependencies = [ "base64 0.22.1", "percent-encoding", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b1e1b02..91872d5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxywarden" -version = "1.0.3" +version = "1.1.0" description = "Standalone Windows desktop proxy management app for ProxyWarden." authors = ["ProxyWarden"] edition = "2021" diff --git a/src-tauri/bundled/cleanup/uninstall-managed-components.ps1 b/src-tauri/bundled/cleanup/uninstall-managed-components.ps1 index c1dc31b..d277c01 100644 --- a/src-tauri/bundled/cleanup/uninstall-managed-components.ps1 +++ b/src-tauri/bundled/cleanup/uninstall-managed-components.ps1 @@ -172,6 +172,17 @@ function Remove-SafeDirectory([string]$Path, [string]$Root) { return $true } +function Remove-ManagedFirewallRules { + $removed = @() + foreach ($name in @("ProxyWarden.ProxiFyre.Inbound", "ProxyWarden.ProxiFyre.Outbound")) { + $rule = Get-NetFirewallRule -Name $name -ErrorAction SilentlyContinue + if ($null -eq $rule) { continue } + $rule | Remove-NetFirewallRule -ErrorAction Stop + $removed += $name + } + return $removed +} + function Get-InstalledProgram([string]$Pattern) { $paths = @( "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*", @@ -220,6 +231,7 @@ try { $details.removedProxiFyreServices = Remove-ManagedService -Names @("ProxiFyreService", "ProxiFyre") -InstallRoot $root -UninstallExe (Join-Path $proxifyreDir "ProxiFyre.exe") $details.removedSingBoxServices = Remove-ManagedService -Names @("ProxyWardenSingBox") -InstallRoot $root -UninstallExe (Join-Path $singboxDir "ProxyWardenSingBox.exe") + $details.removedProxiFyreFirewallRules = Remove-ManagedFirewallRules $details.removedProxiFyreDir = Remove-SafeDirectory $proxifyreDir $root $details.removedSingBoxDir = Remove-SafeDirectory $singboxDir $root diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 1a2bd2f..798f7f4 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -18,13 +18,15 @@ pub use crate::configuration_use_case::{ }; pub use crate::proxifyre_runtime::wrap_elevated_package_script; use crate::proxifyre_runtime::{ - build_proxifyre_setup_status_for_install_dir, control_proxifyre_service, - install_proxifyre_component, proxifyre_install_dir_for_app, read_proxifyre_setup_progress, - singbox_install_dir_for_app, uninstall_proxifyre_component, ServiceControlAction, + build_proxifyre_setup_status_for_install_dir, configure_proxifyre_firewall, + control_proxifyre_service, install_proxifyre_component, proxifyre_install_dir_for_app, + read_proxifyre_setup_progress, singbox_install_dir_for_app, uninstall_proxifyre_component, + ServiceControlAction, }; pub use crate::proxifyre_scripts::{ - install_proxifyre_script, install_proxifyre_script_for_target, - install_proxifyre_script_with_bundle, uninstall_proxifyre_script, + configure_proxifyre_firewall_script, install_proxifyre_script, + install_proxifyre_script_for_target, install_proxifyre_script_with_bundle, + uninstall_proxifyre_script, }; pub use crate::proxy_apply::{ apply_profiles_with_services, apply_profiles_with_services_and_detection, @@ -288,6 +290,13 @@ pub async fn install_proxifyre( .map_err(background_task_error)? } +#[tauri::command] +pub async fn configure_proxifyre_firewall_rules(app: tauri::AppHandle) -> Result<(), CommandError> { + tauri::async_runtime::spawn_blocking(move || configure_proxifyre_firewall(&app)) + .await + .map_err(background_task_error)? +} + #[tauri::command] pub async fn uninstall_proxifyre( app: tauri::AppHandle, diff --git a/src-tauri/src/component_detection.rs b/src-tauri/src/component_detection.rs index 42f715f..1535720 100644 --- a/src-tauri/src/component_detection.rs +++ b/src-tauri/src/component_detection.rs @@ -12,6 +12,8 @@ use std::{ pub const PROXYWARDEN_COMPONENTS_DIR_NAME: &str = "components"; pub const PROXIFYRE_COMPONENT_DIR_NAME: &str = "ProxiFyre"; pub const SINGBOX_COMPONENT_DIR_NAME: &str = "sing-box"; +#[cfg(debug_assertions)] +const PROXYWARDEN_DEV_INSTALL_ROOT_ENV: &str = "PROXYWARDEN_DEV_INSTALL_ROOT"; #[derive(Debug, Clone, PartialEq, Eq)] pub enum ProxyfierEngine { @@ -132,11 +134,42 @@ pub fn detect_proxyfier_install() -> Option { } pub fn app_install_dir_from_current_exe() -> Option { + #[cfg(debug_assertions)] + if let Some(path) = env::var_os(PROXYWARDEN_DEV_INSTALL_ROOT_ENV) + .filter(|value| !value.is_empty()) + .map(PathBuf::from) + { + return Some(path); + } + env::current_exe() .ok() .and_then(|path| path.parent().map(Path::to_path_buf)) } +#[cfg(all(test, debug_assertions))] +mod tests { + use super::*; + + #[test] + #[cfg(debug_assertions)] + fn debug_component_roots_follow_configured_install() { + let install_root = PathBuf::from( + env::var(PROXYWARDEN_DEV_INSTALL_ROOT_ENV) + .expect("Cargo dev config should define the installed ProxyWarden root"), + ); + + assert_eq!( + default_proxifyre_install_dir(), + proxifyre_install_dir_from_app_dir(&install_root) + ); + assert_eq!( + default_singbox_install_dir(), + singbox_install_dir_from_app_dir(&install_root) + ); + } +} + pub fn component_root_from_app_dir(app_dir: &Path) -> PathBuf { app_dir.join(PROXYWARDEN_COMPONENTS_DIR_NAME) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c69a8b0..f37d1d0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -57,6 +57,7 @@ pub fn run() { commands::start_proxifyre_service, commands::stop_proxifyre_service, commands::install_proxifyre, + commands::configure_proxifyre_firewall_rules, commands::uninstall_proxifyre, commands::start_singbox_service, commands::stop_singbox_service, diff --git a/src-tauri/src/proxifyre_ownership.rs b/src-tauri/src/proxifyre_ownership.rs index 7086882..a53a505 100644 --- a/src-tauri/src/proxifyre_ownership.rs +++ b/src-tauri/src/proxifyre_ownership.rs @@ -68,7 +68,8 @@ pub fn verify_managed_proxifyre_install( marker_path.display() ) })?; - let marker: ProxiFyreInstallMarker = serde_json::from_str(&marker_text).map_err(|error| { + let marker_text = marker_text.strip_prefix('\u{feff}').unwrap_or(&marker_text); + let marker: ProxiFyreInstallMarker = serde_json::from_str(marker_text).map_err(|error| { format!( "marker установки {} содержит некорректный JSON: {error}", marker_path.display() diff --git a/src-tauri/src/proxifyre_runtime.rs b/src-tauri/src/proxifyre_runtime.rs index 98d969b..18164bb 100644 --- a/src-tauri/src/proxifyre_runtime.rs +++ b/src-tauri/src/proxifyre_runtime.rs @@ -6,7 +6,7 @@ use crate::clock::{Clock, SystemClock}; use crate::command_dto::*; use crate::component_detection::{ - detect_proxyfier_install, proxifyre_install_dir_from_app_dir, + app_install_dir_from_current_exe, detect_proxyfier_install, proxifyre_install_dir_from_app_dir, proxyfier_component_from_detection, singbox_install_dir_from_app_dir, DetectedProxyfier, }; use crate::elevated_scripts; @@ -17,12 +17,15 @@ use crate::powershell::{ }; use crate::process::command_no_window; use crate::proxifyre_ownership::verify_managed_proxifyre_install; -use crate::proxifyre_scripts::{install_proxifyre_script_for_target, uninstall_proxifyre_script}; +use crate::proxifyre_scripts::{ + configure_proxifyre_firewall_script, install_proxifyre_script_for_target, + uninstall_proxifyre_script, +}; use crate::safe_fs; use crate::storage::{default_config_root, JsonStorage}; use serde::Deserialize; +use std::fs; use std::path::{Path, PathBuf}; -use std::{env, fs}; use tauri::Manager; pub(crate) fn control_proxifyre_service( @@ -389,6 +392,7 @@ exit 4 enum ProxiFyrePackageAction { Install, Uninstall, + ConfigureFirewall, } impl ProxiFyrePackageAction { @@ -396,6 +400,7 @@ impl ProxiFyrePackageAction { match self { ProxiFyrePackageAction::Install => "proxifyre_install_failed", ProxiFyrePackageAction::Uninstall => "proxifyre_uninstall_failed", + ProxiFyrePackageAction::ConfigureFirewall => "proxifyre_firewall_failed", } } @@ -403,6 +408,7 @@ impl ProxiFyrePackageAction { match self { ProxiFyrePackageAction::Install => "установить", ProxiFyrePackageAction::Uninstall => "удалить", + ProxiFyrePackageAction::ConfigureFirewall => "настроить Windows Firewall для", } } @@ -410,6 +416,7 @@ impl ProxiFyrePackageAction { match self { ProxiFyrePackageAction::Install => "install", ProxiFyrePackageAction::Uninstall => "uninstall", + ProxiFyrePackageAction::ConfigureFirewall => "firewall", } } @@ -417,6 +424,7 @@ impl ProxiFyrePackageAction { match self { ProxiFyrePackageAction::Install => "install", ProxiFyrePackageAction::Uninstall => "uninstall", + ProxiFyrePackageAction::ConfigureFirewall => "firewall", } } @@ -424,6 +432,9 @@ impl ProxiFyrePackageAction { match self { ProxiFyrePackageAction::Install => "Готовлю установку ProxiFyre.", ProxiFyrePackageAction::Uninstall => "Готовлю удаление ProxiFyre и сетевого драйвера.", + ProxiFyrePackageAction::ConfigureFirewall => { + "Готовлю правила Windows Firewall для ProxiFyre." + } } } @@ -431,6 +442,9 @@ impl ProxiFyrePackageAction { match self { ProxiFyrePackageAction::Install => "ProxiFyre и сетевой драйвер готовы.", ProxiFyrePackageAction::Uninstall => "ProxiFyre и сетевой драйвер удалены.", + ProxiFyrePackageAction::ConfigureFirewall => { + "Правила Windows Firewall для ProxiFyre добавлены." + } } } } @@ -477,6 +491,36 @@ pub(crate) fn install_proxifyre_component( )) } +pub(crate) fn configure_proxifyre_firewall(app: &tauri::AppHandle) -> Result<(), CommandError> { + let Some(detected) = detect_proxyfier_install() else { + return Err(CommandError::new( + ProxiFyrePackageAction::ConfigureFirewall.error_code(), + "ProxiFyre не найден на компьютере.", + )); + }; + + let expected_install_dir = proxifyre_install_dir_for_app(app)?; + verify_managed_proxifyre_install( + &detected.install_dir, + &detected.executable_path, + &expected_install_dir, + ) + .map_err(|reason| { + CommandError::new( + ProxiFyrePackageAction::ConfigureFirewall.error_code(), + format!("Настройка Windows Firewall заблокирована: {reason}"), + ) + })?; + + let script = configure_proxifyre_firewall_script(&detected.executable_path); + let artifact_dir = default_config_root().join("state"); + run_elevated_package_script( + ProxiFyrePackageAction::ConfigureFirewall, + script, + &artifact_dir, + ) +} + fn bundled_proxifyre_asset_dir(app: &tauri::AppHandle) -> Option { let mut candidates = Vec::new(); if let Ok(resource_dir) = app.path().resource_dir() { @@ -492,10 +536,8 @@ fn bundled_proxifyre_asset_dir(app: &tauri::AppHandle) -> Option { } fn app_install_dir(app: &tauri::AppHandle) -> Result { - if let Ok(exe_path) = env::current_exe() { - if let Some(parent) = exe_path.parent() { - return Ok(parent.to_path_buf()); - } + if let Some(app_dir) = app_install_dir_from_current_exe() { + return Ok(app_dir); } app.path().resource_dir().map_err(|error| { diff --git a/src-tauri/src/proxifyre_scripts.rs b/src-tauri/src/proxifyre_scripts.rs index f9e12b7..0ded1b4 100644 --- a/src-tauri/src/proxifyre_scripts.rs +++ b/src-tauri/src/proxifyre_scripts.rs @@ -14,6 +14,8 @@ const NDISAPI_PINNED_RELEASE_TAG: &str = "v3.6.2"; const NDISAPI_PINNED_INSTALLER_VERSION: &str = "3.6.2.1"; const VC_REDIST_X64_URL: &str = "https://aka.ms/vc14/vc_redist.x64.exe"; const VC_REDIST_X86_URL: &str = "https://aka.ms/vc14/vc_redist.x86.exe"; +pub const PROXIFYRE_FIREWALL_INBOUND_RULE: &str = "ProxyWarden.ProxiFyre.Inbound"; +pub const PROXIFYRE_FIREWALL_OUTBOUND_RULE: &str = "ProxyWarden.ProxiFyre.Outbound"; pub fn install_proxifyre_script(generated_config_path: &Path) -> String { install_proxifyre_script_with_bundle(generated_config_path, None) @@ -453,14 +455,15 @@ pub fn install_proxifyre_script_for_target( } $markerPath = Join-Path $targetDir 'proxywarden-component.json' - [ordered]@{ + $markerJson = [ordered]@{ manager = 'ProxyWarden' component = 'proxifyre' serviceName = 'ProxiFyreService' installedAt = (Get-Date).ToString('o') installRoot = $targetDir packetFilterInstalledByProxyWarden = (-not $packetFilterAlreadyInstalled) - } | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $markerPath -Encoding UTF8 + } | ConvertTo-Json -Depth 4 + [IO.File]::WriteAllText($markerPath, $markerJson, [Text.UTF8Encoding]::new($false)) Write-ProxyWardenProgress 'install' 'proxifyre' 'running' 90 'Устанавливаю и запускаю службу ProxiFyre.' Push-Location $targetDir @@ -483,6 +486,38 @@ pub fn install_proxifyre_script_for_target( script } +pub fn configure_proxifyre_firewall_script(executable_path: &Path) -> String { + let executable_path = escape_powershell_single(&executable_path.display().to_string()); + format!( + r#" +$exePath = '{executable_path}' +if (-not (Test-Path -LiteralPath $exePath -PathType Leaf)) {{ + throw "ProxiFyre.exe не найден по подтвержденному пути: $exePath" +}} + +$ruleSpecs = @( + @{{ Name = '{PROXIFYRE_FIREWALL_INBOUND_RULE}'; DisplayName = 'ProxyWarden: ProxiFyre (входящие)'; Direction = 'Inbound' }}, + @{{ Name = '{PROXIFYRE_FIREWALL_OUTBOUND_RULE}'; DisplayName = 'ProxyWarden: ProxiFyre (исходящие)'; Direction = 'Outbound' }} +) + +foreach ($rule in $ruleSpecs) {{ + Get-NetFirewallRule -Name $rule.Name -ErrorAction SilentlyContinue | + Remove-NetFirewallRule -ErrorAction Stop + New-NetFirewallRule ` + -Name $rule.Name ` + -DisplayName $rule.DisplayName ` + -Group 'ProxyWarden' ` + -Program $exePath ` + -Direction $rule.Direction ` + -Action Allow ` + -Profile Any ` + -Enabled True ` + -ErrorAction Stop | Out-Null +}} +"#, + ) +} + pub fn uninstall_proxifyre_script( detected: Option<&DetectedProxyfier>, ownership: &ManagedProxiFyreOwnership, @@ -619,6 +654,11 @@ pub fn uninstall_proxifyre_script( sc.exe delete $service.Name | Out-Null } + foreach ($firewallRuleName in @('ProxyWarden.ProxiFyre.Inbound', 'ProxyWarden.ProxiFyre.Outbound')) { + Get-NetFirewallRule -Name $firewallRuleName -ErrorAction SilentlyContinue | + Remove-NetFirewallRule -ErrorAction Stop + } + if (-not [string]::IsNullOrWhiteSpace($installDir) -and (Test-Path -LiteralPath $installDir)) { Remove-Item -LiteralPath $installDir -Recurse -Force } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 5851969..ada75a6 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "ProxyWarden", - "version": "1.0.3", + "version": "1.1.0", "identifier": "ru.dokops.proxywarden.windows", "build": { "beforeDevCommand": "npm run dev", diff --git a/src-tauri/tests/command_tests.rs b/src-tauri/tests/command_tests.rs index 3d7ca57..a3969dc 100644 --- a/src-tauri/tests/command_tests.rs +++ b/src-tauri/tests/command_tests.rs @@ -287,6 +287,11 @@ fn proxifyre_install_script_uses_resilient_download_helpers() { )); assert!(script.contains("github.com/wiresock/ndisapi/releases/download")); assert!(script.contains("github.com/wiresock/proxifyre/releases/download")); + assert!(script.contains( + "[IO.File]::WriteAllText($markerPath, $markerJson, [Text.UTF8Encoding]::new($false))" + )); + assert!(!script + .contains("ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $markerPath -Encoding UTF8")); let packet_filter_step = script .find("Write-ProxyWardenProgress 'install' 'packet-filter'") .expect("packet filter install step should be present"); @@ -342,6 +347,52 @@ fn proxifyre_install_script_prefers_bundled_assets_before_downloads() { cleanup(&root); } +#[test] +fn proxifyre_firewall_script_scopes_rules_to_managed_executable() { + let executable = + PathBuf::from(r"C:\Program Files\Proxy'Warden\components\ProxiFyre\ProxiFyre.exe"); + let script = commands::configure_proxifyre_firewall_script(&executable); + + assert!(script.contains( + "$exePath = 'C:\\Program Files\\Proxy''Warden\\components\\ProxiFyre\\ProxiFyre.exe'" + )); + assert!(script.contains("ProxyWarden.ProxiFyre.Inbound")); + assert!(script.contains("ProxyWarden.ProxiFyre.Outbound")); + assert!(script.contains("-Program $exePath")); + assert!(script.contains("Get-NetFirewallRule -Name $rule.Name")); + assert!(!script.contains("Get-NetFirewallRule -DisplayName")); +} + +#[test] +#[cfg(windows)] +fn proxifyre_firewall_script_parses_as_powershell() { + let root = test_root("proxifyre-firewall-script"); + fs::create_dir_all(&root).expect("test root should be created"); + let script = commands::configure_proxifyre_firewall_script( + &root.join("ProxiFyre").join("ProxiFyre.exe"), + ); + let script_path = root.join("firewall.ps1"); + fs::write(&script_path, script).expect("script should be written"); + + let escaped_path = script_path.display().to_string().replace('\'', "''"); + let parser = format!( + "$tokens = $null; $errors = $null; [System.Management.Automation.Language.Parser]::ParseFile('{escaped_path}', [ref]$tokens, [ref]$errors) | Out-Null; if ($errors.Count -gt 0) {{ $errors | ForEach-Object {{ $_.Message }}; exit 1 }}" + ); + let output = ProcessCommand::new("powershell") + .args(["-NoProfile", "-NonInteractive", "-Command", &parser]) + .output() + .expect("powershell parser should run"); + + assert!( + output.status.success(), + "firewall script should parse\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + + cleanup(&root); +} + #[test] #[cfg(windows)] fn proxifyre_uninstall_script_parses_as_powershell() { @@ -410,6 +461,8 @@ fn proxifyre_uninstall_script_removes_packet_filter_after_proxifyre() { assert!(script.contains("Start-Process -FilePath 'msiexec.exe'")); assert!(script.contains("ArgumentList @('/x', $productCode, '/qn', '/norestart'")); assert!(script.contains("Uninstall-MsiProgram $packetFilter 'Windows Packet Filter'")); + assert!(script.contains("ProxyWarden.ProxiFyre.Inbound")); + assert!(script.contains("ProxyWarden.ProxiFyre.Outbound")); let proxifyre_step = script .find("Write-ProxyWardenProgress 'uninstall' 'proxifyre'") .expect("proxifyre uninstall step should be present"); @@ -419,6 +472,16 @@ fn proxifyre_uninstall_script_removes_packet_filter_after_proxifyre() { assert!(proxifyre_step < packet_filter_step); } +#[test] +fn proxywarden_uninstall_hook_removes_only_managed_firewall_rules() { + let script = include_str!("../bundled/cleanup/uninstall-managed-components.ps1"); + + assert!(script.contains("ProxyWarden.ProxiFyre.Inbound")); + assert!(script.contains("ProxyWarden.ProxiFyre.Outbound")); + assert!(script.contains("Get-NetFirewallRule -Name $name")); + assert!(!script.contains("Get-NetFirewallRule -DisplayName")); +} + #[test] fn proxifyre_uninstall_script_leaves_shared_packet_filter_installed() { let detected = DetectedProxyfier { diff --git a/src-tauri/tests/proxifyre_ownership_tests.rs b/src-tauri/tests/proxifyre_ownership_tests.rs index 1ce88c9..09bf77c 100644 --- a/src-tauri/tests/proxifyre_ownership_tests.rs +++ b/src-tauri/tests/proxifyre_ownership_tests.rs @@ -18,6 +18,35 @@ fn accepts_matching_managed_install_and_returns_packet_filter_ownership() { assert!(ownership.remove_packet_filter); } +#[test] +fn accepts_windows_powershell_utf8_bom_marker() { + let fixture = ManagedInstallFixture::new("utf8-bom"); + let marker = json!({ + "manager": "ProxyWarden", + "component": "proxifyre", + "serviceName": "ProxiFyreService", + "installRoot": fixture.install_dir, + "packetFilterInstalledByProxyWarden": false + }); + let mut marker_bytes = vec![0xEF, 0xBB, 0xBF]; + marker_bytes.extend(serde_json::to_vec_pretty(&marker).expect("marker should serialize")); + fs::write( + fixture.install_dir.join("proxywarden-component.json"), + marker_bytes, + ) + .expect("BOM marker should be written"); + + let ownership = verify_managed_proxifyre_install( + &fixture.install_dir, + &fixture.executable_path, + &fixture.install_dir, + ) + .expect("Windows PowerShell BOM marker should prove ownership"); + + assert_eq!(ownership.service_name, "ProxiFyreService"); + assert!(!ownership.remove_packet_filter); +} + #[test] fn rejects_install_outside_expected_managed_directory() { let fixture = ManagedInstallFixture::new("unexpected-root"); diff --git a/src/api/tauriCommands.ts b/src/api/tauriCommands.ts index 223591e..230b02b 100644 --- a/src/api/tauriCommands.ts +++ b/src/api/tauriCommands.ts @@ -270,6 +270,10 @@ export function installProxiFyre(): Promise { return invoke("install_proxifyre"); } +export function configureProxiFyreFirewallRules(): Promise { + return invoke("configure_proxifyre_firewall_rules"); +} + export function uninstallProxiFyre(): Promise { return invoke("uninstall_proxifyre"); } diff --git a/src/app/App.tsx b/src/app/App.tsx index 7a61924..f283eb3 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -18,6 +18,7 @@ import { } from "lucide-react"; import { applyConfiguration, + configureProxiFyreFirewallRules, fetchSingBoxSubscription, forgetSingBoxSubscription, generateSingBoxConfig, @@ -591,7 +592,7 @@ export function App() { async function installProxiFyrePackage() { const confirmed = window.confirm( - "Установить ProxiFyre? ProxyWarden запросит права администратора, скачает ProxiFyre, Windows Packet Filter и при необходимости Visual C++ Runtime, затем создаст и запустит Windows-службу.", + "Установить ProxiFyre? ProxyWarden запросит права администратора, установит ProxiFyre, Windows Packet Filter и при необходимости Visual C++ Runtime, затем создаст и запустит Windows-службу. После установки приложение отдельно предложит добавить правила Windows Firewall.", ); if (!confirmed) return; @@ -621,6 +622,34 @@ export function App() { title: "ProxiFyre установлен", text: proxyfierDetails(component, false), }); + + const firewallConfirmed = window.confirm( + "Добавить разрешающие правила Windows Firewall для установленного ProxiFyre.exe? Будут созданы отдельные входящее и исходящее правила только для подтвержденного пути программы. Windows снова запросит права администратора.", + ); + if (firewallConfirmed) { + try { + await configureProxiFyreFirewallRules(); + const firewallProgress = await getProxiFyreSetupProgress(); + setSetupProgress(firewallProgress); + showNotice({ + kind: "success", + title: "Windows Firewall настроен", + text: "Входящее и исходящее правила добавлены для установленного ProxiFyre.exe.", + }); + } catch (error) { + showNotice({ + kind: "error", + title: "Windows Firewall не настроен", + text: `ProxiFyre установлен, но правила не добавлены. ${errorMessage(error)}`, + }); + } + } else { + showNotice({ + kind: "info", + title: "Правила Firewall пропущены", + text: "ProxiFyre установлен без правил Windows Firewall. Их можно будет добавить повторной установкой компонента.", + }); + } } catch (error) { void getProxiFyreSetupProgress() .then(setSetupProgress)