//! Concrete privileged lifecycle coordinator. //! //! The normal process resolves a local, stateless plan. The elevated process //! rebuilds that plan while owning all verified package/config leases and lets //! the runner consume the prepared action exactly once. use crate::adapters::proxifyre::PROXIFYRE_OUTPUT_FILE; use crate::adapters::singbox::{ DEFAULT_BLOCK_OUTBOUND_TAG, DEFAULT_DIRECT_OUTBOUND_TAG, DEFAULT_MIXED_INBOUND_TAG, DEFAULT_VPN_OUTBOUND_TAG, SINGBOX_OUTPUT_FILE, }; use crate::component_catalog::ComponentId as CatalogComponentId; use crate::component_cutover::{ begin_proxifyre_cutover, candidate_operation_state_fingerprint, cleanup_proxifyre_quarantine, component_cutover_user_evidence_fingerprint, confirm_proxifyre_cleanup, cutover_terminal_record_fingerprint, cutover_transaction_fingerprint, legacy_service_snapshot_fingerprint, mark_proxifyre_next_start_verified, read_existing_sealed_cutover_journal, read_sealed_cutover_status, recover_proxifyre_cutover, retire_existing_proxifyre_cutover_terminal_state, retire_proxifyre_cutover_terminal_state, validate_component_cutover_user_evidence, verify_cutover_external_mutation_status, ComponentCutoverObservation, ComponentCutoverUserEvidence, CutoverDisplayState, CutoverError, CutoverExecutionMode, CutoverExternalMutationStatus, CutoverHost, CutoverHostError, CutoverJournal, CutoverJournalStore, CutoverOperation, CutoverPhase, CutoverRunOutcome, CutoverTerminalRetirementExpectation, LegacyBeforeState, LegacyServiceState, MutationAuthorityEvidence, PrerequisiteBeforeState, ProxifyreCutoverPlan, ProxifyreLegacyIdentity, SealedCutoverStore, StateFingerprint, SystemProxifyreCutoverFilesystem, }; use crate::component_detection::{ has_additional_matching_legacy_proxifyre_service, inventory_proxyfier, inventory_singbox, }; use crate::component_inventory::{ component_inventory_fingerprint_for_cutover, CandidateRole, ComponentClassification, ComponentInventory, LEGACY_PROXIFYRE_AUTO_CUTOVER_ROOT, LEGACY_PROXIFYRE_AUTO_CUTOVER_VERSION, }; use crate::component_packages::{ ComponentPackageService, GithubReleaseDigestProof, NativePackageSignatureVerifier, NativePrivilegedBundleVerifier, PackageSignatureVerifier, PrivilegedBundleVerifier, PrivilegedCachedUpdatePlan, PrivilegedPackageLease, PrivilegedPackageProof, ReqwestUpdateTransport, UpdateTransport, }; use crate::privileged_jobs::{ cutover_recovery_probe_fingerprint, InstalledPackageSource, ManagedComponent, PlannedAssetFingerprint, PlannedGithubOriginProof, PlannedPackageFingerprint, PrivilegedAction, PrivilegedActionRunner, PrivilegedCutoverContext, PrivilegedCutoverMode, PrivilegedMutationResult, PrivilegedPlanResolver, PrivilegedResultCode, PrivilegedRunnerFailure, ResolvedActionPlan, }; use crate::proxifyre_runtime::{ configure_proxifyre_firewall_native, expected_proxifyre_cutover_scm_effect, install_proxifyre_native, map_cutover_packet_filter_install_exit, map_proxifyre_installer_exit, mutate_proxifyre_cutover_candidate, mutate_proxifyre_cutover_scm, observe_proxifyre_cutover_scm_state, preflight_start_proxifyre_native, prepare_proxifyre_cutover_candidate, prepare_proxifyre_cutover_package, stop_proxifyre_native, uninstall_cutover_transaction_packet_filter, uninstall_proxifyre_native, update_proxifyre_native, PreparedProxifyreCutoverCandidate, ProxifyreCutoverCandidateObservation, ProxifyreCutoverCandidateWriter, ProxifyreCutoverScm, ProxifyreInstallerCommand, ProxifyreInstallerKind, ProxifyreNativeError, ProxifyreNativeHost, ProxifyreNativeHostError, ProxifyreNativeOwnership, ProxifyreNativeServiceStatus, ProxifyreNativeStartDisposition, RuntimeConfigVerification, SystemProxifyreCutoverCandidateWriter, SystemProxifyreCutoverScm, SystemProxifyreNativeHost, }; use crate::safe_fs; use crate::singbox_runtime::{ install_singbox_native, preflight_start_singbox_native, stop_singbox_native, uninstall_singbox_native, update_singbox_native, SingBoxNativeError, SingBoxNativeOutcome, SingBoxStartPreflight, SystemSingBoxNativeHost, }; use crate::storage::{JsonStorage, StoragePaths}; use serde::Deserialize; use serde_json::{json, Map, Value}; use sha2::{Digest, Sha256}; use std::fs::File; use std::io::{Read, Seek, SeekFrom}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::{SystemTime, UNIX_EPOCH}; use uuid::Uuid; const MAX_GENERATED_CONFIG_BYTES: u64 = 16 * 1024 * 1024; const PACKAGE_STAGING_DIRECTORY: &str = ".proxywarden-package-staging"; type SharedBundleVerifier = Arc; type SharedUpdateTransport = Arc; type SharedSignatureVerifier = Arc; /// Stateless resolver used by the issuing, non-elevated process. It has no /// transport field, so fresh/install/start planning cannot accidentally use /// the network. pub struct LocalPrivilegedPlanResolver { packages: Option, storage_paths: StoragePaths, bundle_verifier: SharedBundleVerifier, startup_session_id: String, app_root: Option, } impl LocalPrivilegedPlanResolver { pub fn production(startup_session_id: &str) -> Result { let startup_session_id = canonical_uuid_v4(startup_session_id) .ok_or(PrivilegedRunnerFailure::PreconditionFailed)?; let storage_paths = StoragePaths::default(); let app_root = installed_app_root()?; let packages = ComponentPackageService::open( app_root.join("bundled").join("components"), &storage_paths, ) .ok(); Ok(Self { packages, storage_paths, bundle_verifier: Arc::new(NativePrivilegedBundleVerifier), startup_session_id, app_root: Some(app_root), }) } #[cfg(debug_assertions)] #[doc(hidden)] pub fn from_parts_for_tests( packages: Option, storage_paths: StoragePaths, bundle_verifier: SharedBundleVerifier, ) -> Self { Self { packages, storage_paths, bundle_verifier, startup_session_id: Uuid::new_v4().hyphenated().to_string(), app_root: None, } } #[cfg(debug_assertions)] #[doc(hidden)] pub fn from_parts_for_tests_with_startup_session( packages: Option, storage_paths: StoragePaths, bundle_verifier: SharedBundleVerifier, startup_session_id: &str, ) -> Self { Self { packages, storage_paths, bundle_verifier, startup_session_id: canonical_uuid_v4(startup_session_id) .expect("test startup session must be a canonical UUID v4"), app_root: None, } } #[cfg(debug_assertions)] #[doc(hidden)] pub fn from_parts_for_tests_with_cutover_app_root( packages: Option, storage_paths: StoragePaths, bundle_verifier: SharedBundleVerifier, startup_session_id: &str, app_root: PathBuf, ) -> Self { Self { packages, storage_paths, bundle_verifier, startup_session_id: canonical_uuid_v4(startup_session_id) .expect("test startup session must be a canonical UUID v4"), app_root: Some(app_root), } } fn resolve_package( &self, action: PrivilegedAction, ) -> Result, PrivilegedRunnerFailure> { let bundled_ids: &[CatalogComponentId] = match action { PrivilegedAction::InstallProxifyre | PrivilegedAction::CutoverProxifyre => &[ CatalogComponentId::Proxifyre, CatalogComponentId::WindowsPacketFilter, CatalogComponentId::VcRuntime, ], PrivilegedAction::InstallSingBox => { &[CatalogComponentId::SingBox, CatalogComponentId::Winsw] } PrivilegedAction::UpdateProxifyre => { return self .packages .as_ref() .ok_or(PrivilegedRunnerFailure::PackageVerificationFailed)? .plan_cached_update(CatalogComponentId::Proxifyre) .map(|plan| Some(planned_cached_package(&plan))) .map_err(|_| PrivilegedRunnerFailure::PackageVerificationFailed); } PrivilegedAction::UpdateSingBox => { return self .packages .as_ref() .ok_or(PrivilegedRunnerFailure::PackageVerificationFailed)? .plan_cached_update(CatalogComponentId::SingBox) .map(|plan| Some(planned_cached_package(&plan))) .map_err(|_| PrivilegedRunnerFailure::PackageVerificationFailed); } _ => return Ok(None), }; let packages = self .packages .as_ref() .ok_or(PrivilegedRunnerFailure::PackageVerificationFailed)?; let leases = bundled_ids .iter() .map(|component_id| { packages.lease_bundled_for_privileged_install( *component_id, self.bundle_verifier.as_ref(), ) }) .collect::, _>>() .map_err(|_| PrivilegedRunnerFailure::PackageVerificationFailed)?; Ok(Some(planned_leased_package( InstalledPackageSource::Bundled, &leases.iter().collect::>(), )?)) } } impl PrivilegedPlanResolver for LocalPrivilegedPlanResolver { fn resolve( &self, action: PrivilegedAction, ) -> Result { if matches!( action, PrivilegedAction::CutoverProxifyre | PrivilegedAction::CleanupProxifyreQuarantine ) { return self.resolve_cutover_action(action); } let package = self.resolve_package(action)?; let configuration_fingerprint = resolve_local_configuration(action, &self.storage_paths)?; let inventory_fingerprint = component_inventory_fingerprint_for_cutover(&live_inventory(action)); Ok(ResolvedActionPlan::new( action, package, inventory_fingerprint, configuration_fingerprint, None, )) } } impl LocalPrivilegedPlanResolver { fn resolve_cutover_action( &self, action: PrivilegedAction, ) -> Result { self.resolve_cutover_action_with_inventory(action, live_inventory(action)) } fn resolve_cutover_action_with_inventory( &self, action: PrivilegedAction, inventory: ComponentInventory, ) -> Result { let inventory_fingerprint = component_inventory_fingerprint_for_cutover(&inventory); let external_status = self .app_root .as_deref() .map(verify_cutover_external_mutation_status); let cutover_infrastructure_absent = external_status == Some(CutoverExternalMutationStatus::Absent); let observation = (!cutover_infrastructure_absent) .then(|| read_local_cutover_observation(&self.storage_paths)) .flatten(); match action { PrivilegedAction::CutoverProxifyre => { if external_status .is_some_and(|status| status != CutoverExternalMutationStatus::Absent) { return Ok(ResolvedActionPlan::new( action, None, inventory_fingerprint.clone(), no_config_fingerprint(action), Some(recovery_probe_context(&inventory_fingerprint)), )); } if let Some(observation) = observation.as_ref().and_then(valid_cutover_observation) { return Ok(ResolvedActionPlan::new( action, None, inventory_fingerprint, no_config_fingerprint(action), Some(recovery_context(observation)?), )); } let original_state = validate_new_cutover_discovery(&inventory).ok(); let package = self.resolve_package(action)?; let configuration_fingerprint = match original_state { Some(_) => { GeneratedConfigLease::open( &self.storage_paths.generated_dir.join(PROXIFYRE_OUTPUT_FILE), ManagedComponent::Proxifyre, )? .sha256 } None => no_config_fingerprint(action), }; Ok(ResolvedActionPlan::new( action, package, inventory_fingerprint.clone(), configuration_fingerprint, Some(new_cutover_context( &inventory_fingerprint, original_state, &self.startup_session_id, )), )) } PrivilegedAction::CleanupProxifyreQuarantine => { let observation = observation .as_ref() .and_then(valid_cutover_observation) .ok_or(PrivilegedRunnerFailure::CutoverStateConflict)?; let user_evidence = read_local_cutover_user_evidence(&self.storage_paths)?; if user_evidence.startup_session_id != self.startup_session_id || user_evidence.current_inventory_fingerprint != inventory_fingerprint { return Err(PrivilegedRunnerFailure::CutoverStateConflict); } Ok(ResolvedActionPlan::new( action, None, inventory_fingerprint, no_config_fingerprint(action), Some(cleanup_context(observation, &user_evidence)?), )) } _ => Err(PrivilegedRunnerFailure::PreconditionFailed), } } #[cfg(debug_assertions)] #[doc(hidden)] pub fn resolve_cutover_action_with_inventory_for_tests( &self, action: PrivilegedAction, inventory: ComponentInventory, ) -> Result { self.resolve_cutover_action_with_inventory(action, inventory) } } /// Elevated resolver and one-shot runner. The pending value owns every live /// lease; a mismatch consumes and drops it before any native host is created. pub struct SystemPrivilegedRuntime { app_root: PathBuf, packages: Option, storage_paths: StoragePaths, bundle_verifier: SharedBundleVerifier, update_transport: Option, signature_verifier: SharedSignatureVerifier, package_staging_parent: PathBuf, verify_staging_app_root: bool, enforce_cutover_journal: bool, pending: Mutex>, } impl SystemPrivilegedRuntime { pub fn production() -> Result { let storage_paths = StoragePaths::default(); let app_root = installed_app_root()?; let packages = ComponentPackageService::open( app_root.join("bundled").join("components"), &storage_paths, ) .ok(); let update_transport = ReqwestUpdateTransport::new() .ok() .map(|transport| Arc::new(transport) as SharedUpdateTransport); Ok(Self { app_root: app_root.clone(), packages, storage_paths, bundle_verifier: Arc::new(NativePrivilegedBundleVerifier), update_transport, signature_verifier: Arc::new(NativePackageSignatureVerifier), package_staging_parent: app_root.join(PACKAGE_STAGING_DIRECTORY), verify_staging_app_root: true, enforce_cutover_journal: true, pending: Mutex::new(None), }) } #[cfg(debug_assertions)] #[doc(hidden)] pub fn from_parts_for_tests( packages: Option, storage_paths: StoragePaths, package_staging_parent: PathBuf, bundle_verifier: SharedBundleVerifier, update_transport: Option, signature_verifier: SharedSignatureVerifier, ) -> Self { let app_root = package_staging_parent .parent() .map(Path::to_path_buf) .unwrap_or_else(|| package_staging_parent.clone()); Self { app_root, packages, storage_paths, bundle_verifier, update_transport, signature_verifier, package_staging_parent, verify_staging_app_root: false, enforce_cutover_journal: false, pending: Mutex::new(None), } } fn prepare(&self, action: PrivilegedAction) -> Result { match action { PrivilegedAction::InstallProxifyre => Ok(PreparedInput::InstallProxifyre { proxifyre: Box::new(self.lease_bundled(CatalogComponentId::Proxifyre)?), packet_filter: Box::new( self.lease_bundled(CatalogComponentId::WindowsPacketFilter)?, ), vc_runtime: Box::new(self.lease_bundled(CatalogComponentId::VcRuntime)?), }), PrivilegedAction::InstallSingBox => Ok(PreparedInput::InstallSingBox { runtime: Box::new(self.lease_bundled(CatalogComponentId::SingBox)?), wrapper: Box::new(self.lease_bundled(CatalogComponentId::Winsw)?), }), PrivilegedAction::UpdateProxifyre => Ok(PreparedInput::UpdateProxifyre(Box::new( self.lease_cached(CatalogComponentId::Proxifyre)?, ))), PrivilegedAction::UpdateSingBox => Ok(PreparedInput::UpdateSingBox(Box::new( self.lease_cached(CatalogComponentId::SingBox)?, ))), PrivilegedAction::StartProxifyre => { Ok(PreparedInput::StartProxifyre(GeneratedConfigLease::open( &self.storage_paths.generated_dir.join(PROXIFYRE_OUTPUT_FILE), ManagedComponent::Proxifyre, )?)) } PrivilegedAction::StartSingBox => { Ok(PreparedInput::StartSingBox(GeneratedConfigLease::open( &self.storage_paths.generated_dir.join(SINGBOX_OUTPUT_FILE), ManagedComponent::SingBox, )?)) } PrivilegedAction::StopProxifyre | PrivilegedAction::ConfigureProxifyreFirewall | PrivilegedAction::UninstallProxifyre | PrivilegedAction::CutoverProxifyre | PrivilegedAction::CleanupProxifyreQuarantine | PrivilegedAction::StopSingBox | PrivilegedAction::UninstallSingBox => Ok(PreparedInput::None), } } fn lease_bundled( &self, component_id: CatalogComponentId, ) -> Result { self.packages .as_ref() .ok_or(PrivilegedRunnerFailure::PackageVerificationFailed)? .lease_bundled_for_privileged_install(component_id, self.bundle_verifier.as_ref()) .map_err(|_| PrivilegedRunnerFailure::PackageVerificationFailed) } fn lease_cached( &self, component_id: CatalogComponentId, ) -> Result { let packages = self .packages .as_ref() .ok_or(PrivilegedRunnerFailure::PackageVerificationFailed)?; let update_transport = self .update_transport .as_ref() .ok_or(PrivilegedRunnerFailure::RunnerUnavailable)?; let plan = packages .plan_cached_update(component_id) .map_err(|_| PrivilegedRunnerFailure::PackageVerificationFailed)?; let app_root = self .package_staging_parent .parent() .filter(|_| { self.package_staging_parent .file_name() .and_then(|name| name.to_str()) == Some(PACKAGE_STAGING_DIRECTORY) }) .ok_or(PrivilegedRunnerFailure::PackageVerificationFailed)?; #[cfg(debug_assertions)] if !self.verify_staging_app_root { std::fs::create_dir_all(&self.package_staging_parent) .map_err(|_| PrivilegedRunnerFailure::PackageVerificationFailed)?; safe_fs::protect_path_for_owner_admin_system(&self.package_staging_parent) .map_err(|_| PrivilegedRunnerFailure::PackageVerificationFailed)?; return packages .lease_cached_update_with_owner_protected_staging_for_tests( &plan, &self.package_staging_parent, update_transport.as_ref(), self.signature_verifier.as_ref(), ) .map_err(|_| PrivilegedRunnerFailure::PackageVerificationFailed); } if self.verify_staging_app_root { safe_fs::verify_path_under_trusted_program_files(app_root) .map_err(|_| PrivilegedRunnerFailure::PackageVerificationFailed)?; } safe_fs::create_directory_admin_owned_user_read_only(&self.package_staging_parent) .map_err(|_| PrivilegedRunnerFailure::PackageVerificationFailed)?; packages .lease_cached_update_for_privileged_install( &plan, &self.package_staging_parent, update_transport.as_ref(), self.signature_verifier.as_ref(), ) .map_err(|_| PrivilegedRunnerFailure::PackageVerificationFailed) } fn sealed_cutover_store(&self) -> Result { if !self.enforce_cutover_journal { return Err(PrivilegedRunnerFailure::RunnerUnavailable); } let storage = JsonStorage::new(self.storage_paths.root.clone()); SealedCutoverStore::prepare(&self.app_root, &storage) .map_err(|_| PrivilegedRunnerFailure::CutoverRecoveryRequired) } fn ensure_ordinary_proxifyre_action_allowed( &self, action: PrivilegedAction, ) -> Result, PrivilegedRunnerFailure> { if action.component() != ManagedComponent::Proxifyre || matches!( action, PrivilegedAction::CutoverProxifyre | PrivilegedAction::CleanupProxifyreQuarantine ) || !self.enforce_cutover_journal { return Ok(None); } match verify_cutover_external_mutation_status(&self.app_root) { CutoverExternalMutationStatus::Absent => return Ok(None), CutoverExternalMutationStatus::Active => {} CutoverExternalMutationStatus::TerminalRetirementPending | CutoverExternalMutationStatus::RolledBackRetained | CutoverExternalMutationStatus::RecoveryRequired => { return Err(PrivilegedRunnerFailure::CutoverRecoveryRequired); } } let journal = read_existing_sealed_cutover_journal(&self.app_root) .map_err(|_| PrivilegedRunnerFailure::CutoverRecoveryRequired)? .ok_or(PrivilegedRunnerFailure::CutoverRecoveryRequired)?; if let Some(config_fingerprint) = stopped_cutover_route_lifecycle_exception( action, journal.phase, journal.execution_mode, journal.plan.original_service_state, &journal.plan.config_fingerprint, ) { return Ok(Some(config_fingerprint)); } Err(PrivilegedRunnerFailure::CutoverRecoveryRequired) } fn prepare_cutover_action( &self, action: PrivilegedAction, requested_plan: Option<&ResolvedActionPlan>, ) -> Result<(ResolvedActionPlan, PreparedInput), PrivilegedRunnerFailure> { let inventory = live_inventory(action); let inventory_fingerprint = component_inventory_fingerprint_for_cutover(&inventory); let recovery_probe = recovery_probe_context(&inventory_fingerprint); let requested_recovery_probe = requested_plan .and_then(|plan| plan.cutover_context.as_ref()) .is_some_and(|context| context == &recovery_probe); let external_status = verify_cutover_external_mutation_status(&self.app_root); let existing_journal = match external_status { CutoverExternalMutationStatus::Active => Some( read_existing_sealed_cutover_journal(&self.app_root) .map_err(|_| PrivilegedRunnerFailure::CutoverRecoveryRequired)? .ok_or(PrivilegedRunnerFailure::CutoverRecoveryRequired)?, ), CutoverExternalMutationStatus::Absent => { if action != PrivilegedAction::CutoverProxifyre { return Err(PrivilegedRunnerFailure::CutoverStateConflict); } None } CutoverExternalMutationStatus::RecoveryRequired => { if action != PrivilegedAction::CutoverProxifyre { return Err(PrivilegedRunnerFailure::CutoverRecoveryRequired); } let journal = read_existing_sealed_cutover_journal(&self.app_root) .map_err(|_| PrivilegedRunnerFailure::CutoverRecoveryRequired)? .ok_or(PrivilegedRunnerFailure::CutoverRecoveryRequired)?; if journal.phase != CutoverPhase::RecoveryRequired { return Err(PrivilegedRunnerFailure::CutoverRecoveryRequired); } Some(journal) } CutoverExternalMutationStatus::TerminalRetirementPending | CutoverExternalMutationStatus::RolledBackRetained => { if action != PrivilegedAction::CutoverProxifyre { return Err(PrivilegedRunnerFailure::CutoverRecoveryRequired); } match read_existing_sealed_cutover_journal(&self.app_root) .map_err(|_| PrivilegedRunnerFailure::CutoverRecoveryRequired)? { Some(journal) => { if journal.execution_mode != CutoverExecutionMode::Finished || !matches!( journal.phase, CutoverPhase::ComponentComplete | CutoverPhase::RolledBack ) || (external_status == CutoverExternalMutationStatus::RolledBackRetained && journal.phase != CutoverPhase::RolledBack) { return Err(PrivilegedRunnerFailure::CutoverRecoveryRequired); } let expected_terminal_record_fingerprint = cutover_terminal_record_fingerprint(&journal) .map_err(|_| PrivilegedRunnerFailure::CutoverRecoveryRequired)?; let plan = ResolvedActionPlan::new( action, None, inventory_fingerprint, no_config_fingerprint(action), Some(if requested_recovery_probe { recovery_probe } else { recovery_context_from_journal(&journal)? }), ); return Ok(( plan, PreparedInput::Cutover(Box::new(PreparedSystemCutover { mode: PreparedCutoverMode::Retirement, store: None, new: None, retirement: Some(PreparedTerminalCutoverRetirement { app_root: self.app_root.clone(), expected_phase: Some(journal.phase), expectation: CutoverTerminalRetirementExpectation::Journal { cutover_id: journal.plan.cutover_id.clone(), phase: journal.phase, terminal_record_fingerprint: expected_terminal_record_fingerprint, }, }), host: None, user_evidence: None, })), )); } None => { let plan = ResolvedActionPlan::new( action, None, inventory_fingerprint, no_config_fingerprint(action), Some(recovery_probe), ); return Ok(( plan, PreparedInput::Cutover(Box::new(PreparedSystemCutover { mode: PreparedCutoverMode::Retirement, store: None, new: None, retirement: Some(PreparedTerminalCutoverRetirement { app_root: self.app_root.clone(), expected_phase: None, expectation: CutoverTerminalRetirementExpectation::EmptyInfrastructure, }), host: None, user_evidence: None, })), )); } } } }; if let Some(sealed_journal) = existing_journal { let store = self.sealed_cutover_store()?; let sealed_observation = read_sealed_cutover_status(&store) .map_err(|_| PrivilegedRunnerFailure::CutoverRecoveryRequired)? .ok_or(PrivilegedRunnerFailure::CutoverRecoveryRequired)?; let journal = store .load() .map_err(|_| PrivilegedRunnerFailure::CutoverRecoveryRequired)? .ok_or(PrivilegedRunnerFailure::CutoverRecoveryRequired)?; let observation = sealed_observation; if journal != sealed_journal || observation.cutover_id != journal.plan.cutover_id { return Err(PrivilegedRunnerFailure::CutoverRecoveryRequired); } let (mode, context, user_evidence) = match action { PrivilegedAction::CutoverProxifyre => ( PreparedCutoverMode::Recovery, if requested_recovery_probe { recovery_probe } else { recovery_context(&observation)? }, None, ), PrivilegedAction::CleanupProxifyreQuarantine => { let evidence = read_local_cutover_user_evidence(&self.storage_paths)?; validate_elevated_cleanup_evidence( &journal, &inventory_fingerprint, &evidence, unix_now_epoch_seconds()?, )?; let context = cleanup_context(&observation, &evidence)?; (PreparedCutoverMode::Cleanup, context, Some(evidence)) } _ => return Err(PrivilegedRunnerFailure::CutoverStateConflict), }; let plan = ResolvedActionPlan::new( action, None, inventory_fingerprint, no_config_fingerprint(action), Some(context), ); let host = SystemCutoverHost::recover(&journal.plan)?; return Ok(( plan, PreparedInput::Cutover(Box::new(PreparedSystemCutover { mode, store: Some(store), new: None, retirement: None, host: Some(host), user_evidence, })), )); } let initiating_startup_session_id = requested_plan .and_then(|plan| plan.cutover_context.as_ref()) .filter(|context| { context.mode == PrivilegedCutoverMode::New && context.cutover_id.is_none() && context.user_evidence.is_none() }) .and_then(|context| context.initiating_startup_session_id.as_deref()) .and_then(canonical_uuid_v4) .ok_or(PrivilegedRunnerFailure::CutoverStateConflict)?; let original_state = validate_new_cutover_discovery(&inventory)?; let proxifyre = Box::new(self.lease_bundled(CatalogComponentId::Proxifyre)?); let packet_filter = Box::new(self.lease_bundled(CatalogComponentId::WindowsPacketFilter)?); let vc_runtime = Box::new(self.lease_bundled(CatalogComponentId::VcRuntime)?); let config = GeneratedConfigLease::open( &self.storage_paths.generated_dir.join(PROXIFYRE_OUTPUT_FILE), ManagedComponent::Proxifyre, )?; let package = planned_leased_package( InstalledPackageSource::Bundled, &[ proxifyre.as_ref(), packet_filter.as_ref(), vc_runtime.as_ref(), ], )?; let context = new_cutover_context( &inventory_fingerprint, Some(original_state), &initiating_startup_session_id, ); let plan = ResolvedActionPlan::new( action, Some(package), inventory_fingerprint, config.sha256.clone(), Some(context), ); let core_plan = ProxifyreCutoverPlan::new( &self.app_root, PathBuf::from(LEGACY_PROXIFYRE_AUTO_CUTOVER_ROOT), original_state, proxifyre.proof().version.clone(), proxifyre.proof().sha256.clone(), config.sha256.clone(), plan.operation_fingerprint.clone(), initiating_startup_session_id, ); let mut native = SystemProxifyreNativeHost::from_current_exe() .map_err(|_| PrivilegedRunnerFailure::CutoverIdentityRejected)?; let native_before = native .inspect() .map_err(|_| PrivilegedRunnerFailure::CutoverIdentityRejected)?; let runtime = prepare_proxifyre_cutover_package(&proxifyre).map_err(map_proxifyre_error)?; let candidate = prepare_proxifyre_cutover_candidate( &core_plan, runtime, &config.bytes, &config.sha256, native_before.packet_filter_present, unix_now_epoch_seconds()?, ) .map_err(map_proxifyre_error)?; Ok(( plan, PreparedInput::Cutover(Box::new(PreparedSystemCutover { mode: PreparedCutoverMode::New, store: None, new: Some(PreparedNewSystemCutover { app_root: self.app_root.clone(), storage_root: self.storage_paths.root.clone(), plan: core_plan, candidate, native, proxifyre, packet_filter, vc_runtime, config, packet_filter_present: native_before.packet_filter_present, vc_runtime_present: native_before.vc_runtime_present, }), retirement: None, host: None, user_evidence: None, })), )) } fn resolve_internal( &self, action: PrivilegedAction, requested_plan: Option<&ResolvedActionPlan>, ) -> Result { let mut pending = self .pending .lock() .map_err(|_| PrivilegedRunnerFailure::OperationFailed)?; pending.take(); if matches!( action, PrivilegedAction::CutoverProxifyre | PrivilegedAction::CleanupProxifyreQuarantine ) { let (plan, input) = self.prepare_cutover_action(action, requested_plan)?; *pending = Some(PreparedAction { action, plan: plan.clone(), input, }); return Ok(plan); } let sealed_config_fingerprint = self.ensure_ordinary_proxifyre_action_allowed(action)?; let input = self.prepare(action)?; if action == PrivilegedAction::StartProxifyre && sealed_config_fingerprint .as_deref() .is_some_and(|expected| input.configuration_fingerprint(action) != expected) { return Err(PrivilegedRunnerFailure::CutoverRecoveryRequired); } let plan = ResolvedActionPlan::new( action, input.package_fingerprint()?, component_inventory_fingerprint_for_cutover(&live_inventory(action)), input.configuration_fingerprint(action), None, ); *pending = Some(PreparedAction { action, plan: plan.clone(), input, }); Ok(plan) } } fn stopped_cutover_route_lifecycle_exception( action: PrivilegedAction, phase: CutoverPhase, execution_mode: CutoverExecutionMode, original_service_state: LegacyServiceState, config_fingerprint: &str, ) -> Option { (phase == CutoverPhase::LegacyQuarantined && execution_mode == CutoverExecutionMode::AwaitingUser && original_service_state == LegacyServiceState::Stopped && matches!( action, PrivilegedAction::StartProxifyre | PrivilegedAction::StopProxifyre )) .then(|| config_fingerprint.to_string()) } #[cfg(debug_assertions)] #[doc(hidden)] pub fn stopped_cutover_route_lifecycle_exception_for_tests( action: PrivilegedAction, phase: CutoverPhase, execution_mode: CutoverExecutionMode, original_service_state: LegacyServiceState, config_fingerprint: &str, ) -> Option { stopped_cutover_route_lifecycle_exception( action, phase, execution_mode, original_service_state, config_fingerprint, ) } impl PrivilegedPlanResolver for SystemPrivilegedRuntime { fn resolve( &self, action: PrivilegedAction, ) -> Result { self.resolve_internal(action, None) } fn resolve_elevated( &self, action: PrivilegedAction, requested_plan: &ResolvedActionPlan, ) -> Result { self.resolve_internal(action, Some(requested_plan)) } } impl PrivilegedActionRunner for SystemPrivilegedRuntime { fn run( &self, action: PrivilegedAction, plan: &ResolvedActionPlan, ) -> Result { let prepared = self .pending .lock() .map_err(|_| PrivilegedRunnerFailure::OperationFailed)? .take() .ok_or(PrivilegedRunnerFailure::PreconditionFailed)?; if prepared.action != action || &prepared.plan != plan { return Err(PrivilegedRunnerFailure::PreconditionFailed); } execute_prepared(action, prepared.input) } } struct PreparedAction { action: PrivilegedAction, plan: ResolvedActionPlan, input: PreparedInput, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum PreparedCutoverMode { New, Recovery, Cleanup, Retirement, } struct PreparedNewSystemCutover { app_root: PathBuf, storage_root: PathBuf, plan: ProxifyreCutoverPlan, candidate: PreparedProxifyreCutoverCandidate, native: SystemProxifyreNativeHost, proxifyre: Box, packet_filter: Box, vc_runtime: Box, config: GeneratedConfigLease, packet_filter_present: bool, vc_runtime_present: bool, } struct PreparedTerminalCutoverRetirement { app_root: PathBuf, expected_phase: Option, expectation: CutoverTerminalRetirementExpectation, } struct PreparedSystemCutover { mode: PreparedCutoverMode, store: Option, new: Option, retirement: Option, host: Option, user_evidence: Option, } impl PreparedSystemCutover { fn execute(mut self) -> Result { let now = unix_now_epoch_seconds()?; let (outcome, reboot_required) = match self.mode { PreparedCutoverMode::New => { let prepared = self .new .take() .ok_or(PrivilegedRunnerFailure::CutoverStateConflict)?; execute_new_system_cutover(prepared, now)? } PreparedCutoverMode::Recovery => { let store = self .store .take() .ok_or(PrivilegedRunnerFailure::CutoverStateConflict)?; let mut host = self .host .take() .ok_or(PrivilegedRunnerFailure::CutoverStateConflict)?; let outcome = recover_proxifyre_cutover(&store, &mut host, now) .map_err(|error| map_cutover_error(self.mode, error))?; (outcome, host.reboot_required) } PreparedCutoverMode::Cleanup => { let store = self .store .take() .ok_or(PrivilegedRunnerFailure::CutoverStateConflict)?; let mut host = self .host .take() .ok_or(PrivilegedRunnerFailure::CutoverStateConflict)?; let evidence = self .user_evidence .as_ref() .ok_or(PrivilegedRunnerFailure::CutoverStateConflict)?; let journal = store .load() .map_err(|_| PrivilegedRunnerFailure::CutoverRecoveryRequired)? .ok_or(PrivilegedRunnerFailure::CutoverStateConflict)?; match journal.phase { CutoverPhase::LegacyQuarantined => { mark_proxifyre_next_start_verified(&store, &mut host, evidence, now) .map_err(|error| map_cutover_error(self.mode, error))?; confirm_proxifyre_cleanup(&store, evidence, now) .map_err(|error| map_cutover_error(self.mode, error))?; } CutoverPhase::NextStartVerified => { confirm_proxifyre_cleanup(&store, evidence, now) .map_err(|error| map_cutover_error(self.mode, error))?; } CutoverPhase::CleanupConfirmed => {} _ => return Err(PrivilegedRunnerFailure::CutoverStateConflict), } let outcome = cleanup_proxifyre_quarantine(&store, &mut host, now) .map_err(|error| map_cutover_error(self.mode, error))?; (outcome, host.reboot_required) } PreparedCutoverMode::Retirement => { let prepared = self .retirement .take() .ok_or(PrivilegedRunnerFailure::CutoverStateConflict)?; execute_terminal_cutover_retirement(prepared)? } }; let (changed, result_code) = match outcome { CutoverRunOutcome::AwaitingNextStart => { (true, PrivilegedResultCode::CutoverAwaitingNextStart) } CutoverRunOutcome::QuarantinePending => { (true, PrivilegedResultCode::CutoverQuarantinePending) } CutoverRunOutcome::RolledBack => (false, PrivilegedResultCode::CutoverRolledBack), CutoverRunOutcome::CleanupPending => { (true, PrivilegedResultCode::CutoverCleanupPending) } CutoverRunOutcome::Complete => (true, PrivilegedResultCode::CutoverComplete), }; Ok(PrivilegedMutationResult { activation: None, changed, reboot_required, result_code: Some(result_code), }) } } fn execute_terminal_cutover_retirement( prepared: PreparedTerminalCutoverRetirement, ) -> Result<(CutoverRunOutcome, bool), PrivilegedRunnerFailure> { if prepared.expected_phase.is_some_and(|phase| { !matches!( phase, CutoverPhase::ComponentComplete | CutoverPhase::RolledBack ) }) { return Err(PrivilegedRunnerFailure::CutoverRecoveryRequired); } retire_existing_proxifyre_cutover_terminal_state(&prepared.app_root, &prepared.expectation) .map_err(|_| PrivilegedRunnerFailure::CutoverRecoveryRequired)?; let outcome = if prepared.expected_phase == Some(CutoverPhase::ComponentComplete) { CutoverRunOutcome::Complete } else { CutoverRunOutcome::RolledBack }; Ok((outcome, false)) } fn execute_new_system_cutover( prepared: PreparedNewSystemCutover, now: u64, ) -> Result<(CutoverRunOutcome, bool), PrivilegedRunnerFailure> { let storage = JsonStorage::new(prepared.storage_root); let store = SealedCutoverStore::prepare(&prepared.app_root, &storage) .map_err(|_| PrivilegedRunnerFailure::CutoverRecoveryRequired)?; let mut host = match SystemCutoverHost::new( &prepared.plan, prepared.candidate, prepared.native, prepared.proxifyre, prepared.packet_filter, prepared.vc_runtime, prepared.config, prepared.packet_filter_present, prepared.vc_runtime_present, ) { Ok(host) => host, Err(error) => { retire_empty_new_cutover_store(&store)?; return Err(error); } }; let outcome = match begin_proxifyre_cutover(&store, &mut host, prepared.plan, now) { Ok(outcome) => outcome, Err(error) => { drop(host); preserve_journal_or_retire_empty_new_cutover(&store)?; return Err(map_cutover_error(PreparedCutoverMode::New, error)); } }; Ok((outcome, host.reboot_required)) } fn preserve_journal_or_retire_empty_new_cutover( store: &SealedCutoverStore, ) -> Result<(), PrivilegedRunnerFailure> { match store.load() { Ok(Some(_)) => Ok(()), Ok(None) => retire_empty_new_cutover_store(store), Err(_) => Err(PrivilegedRunnerFailure::CutoverRecoveryRequired), } } fn retire_empty_new_cutover_store( store: &SealedCutoverStore, ) -> Result<(), PrivilegedRunnerFailure> { retire_proxifyre_cutover_terminal_state(store) .map_err(|_| PrivilegedRunnerFailure::CutoverRecoveryRequired) } fn unix_now_epoch_seconds() -> Result { let value = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| duration.as_secs()) .map_err(|_| PrivilegedRunnerFailure::OperationFailed)?; (value > 0) .then_some(value) .ok_or(PrivilegedRunnerFailure::OperationFailed) } fn map_cutover_error(mode: PreparedCutoverMode, error: CutoverError) -> PrivilegedRunnerFailure { match error { CutoverError::AlreadyExists | CutoverError::Missing | CutoverError::StateConflict => { PrivilegedRunnerFailure::CutoverStateConflict } CutoverError::RecoveryRequired | CutoverError::Interrupted => { PrivilegedRunnerFailure::CutoverRecoveryRequired } CutoverError::InvalidPlan | CutoverError::Host(CutoverHostError::PreconditionFailed) if mode == PreparedCutoverMode::New => { PrivilegedRunnerFailure::CutoverIdentityRejected } CutoverError::InvalidPlan | CutoverError::Host(CutoverHostError::PreconditionFailed) => { PrivilegedRunnerFailure::CutoverRecoveryRequired } CutoverError::Host(_) | CutoverError::Storage(_) if mode != PreparedCutoverMode::New => { PrivilegedRunnerFailure::CutoverRecoveryRequired } CutoverError::Host(_) | CutoverError::Storage(_) => { PrivilegedRunnerFailure::OperationFailed } } } struct SystemCutoverHost { filesystem: SystemProxifyreCutoverFilesystem, scm: SystemProxifyreCutoverScm, candidate: Option, native: SystemProxifyreNativeHost, _proxifyre_lease: Option>, packet_filter_lease: Option>, vc_runtime_lease: Option>, _config_lease: Option, captured_packet_filter_present: Option, captured_vc_runtime_present: Option, last_observed_identity: Option, reboot_required: bool, } impl SystemCutoverHost { #[allow(clippy::too_many_arguments)] fn new( plan: &ProxifyreCutoverPlan, candidate: PreparedProxifyreCutoverCandidate, native: SystemProxifyreNativeHost, proxifyre_lease: Box, packet_filter_lease: Box, vc_runtime_lease: Box, config_lease: GeneratedConfigLease, packet_filter_present: bool, vc_runtime_present: bool, ) -> Result { let filesystem = SystemProxifyreCutoverFilesystem::open(plan) .map_err(|_| PrivilegedRunnerFailure::CutoverIdentityRejected)?; let scm = SystemProxifyreCutoverScm::from_current_exe() .map_err(|_| PrivilegedRunnerFailure::CutoverIdentityRejected)?; let candidate = SystemProxifyreCutoverCandidateWriter::begin(candidate) .map_err(|_| PrivilegedRunnerFailure::CutoverIdentityRejected)?; Ok(Self { filesystem, scm, candidate: Some(candidate), native, _proxifyre_lease: Some(proxifyre_lease), packet_filter_lease: Some(packet_filter_lease), vc_runtime_lease: Some(vc_runtime_lease), _config_lease: Some(config_lease), captured_packet_filter_present: Some(packet_filter_present), captured_vc_runtime_present: Some(vc_runtime_present), last_observed_identity: None, reboot_required: false, }) } fn recover(plan: &ProxifyreCutoverPlan) -> Result { Ok(Self { filesystem: SystemProxifyreCutoverFilesystem::open(plan) .map_err(|_| PrivilegedRunnerFailure::CutoverRecoveryRequired)?, scm: SystemProxifyreCutoverScm::from_current_exe() .map_err(|_| PrivilegedRunnerFailure::CutoverRecoveryRequired)?, candidate: None, native: SystemProxifyreNativeHost::from_current_exe() .map_err(|_| PrivilegedRunnerFailure::CutoverRecoveryRequired)?, _proxifyre_lease: None, packet_filter_lease: None, vc_runtime_lease: None, _config_lease: None, captured_packet_filter_present: None, captured_vc_runtime_present: None, last_observed_identity: None, reboot_required: false, }) } fn handoff_complete_candidate( &mut self, journal: &CutoverJournal, ) -> Result<(), CutoverHostError> { let Some(candidate) = self.candidate.take() else { return Ok(()); }; let (root, entries) = candidate .into_complete_candidate_leases() .map_err(|_| CutoverHostError::PreconditionFailed)?; self.filesystem .adopt_current_candidate(journal, root, entries) } fn handoff_partial_candidate_for_rollback( &mut self, journal: &CutoverJournal, ) -> Result<(), CutoverHostError> { let Some(candidate) = self.candidate.take() else { return Ok(()); }; let (root, entries) = candidate .into_partial_candidate_leases_for_rollback(journal) .map_err(|_| CutoverHostError::PreconditionFailed)?; self.filesystem .adopt_partial_current_candidate_for_rollback(journal, root, entries) } fn candidate_writer(&self) -> Result<&SystemProxifyreCutoverCandidateWriter, CutoverHostError> { self.candidate .as_ref() .ok_or(CutoverHostError::ObservationFailed) } fn candidate_writer_mut( &mut self, ) -> Result<&mut SystemProxifyreCutoverCandidateWriter, CutoverHostError> { self.candidate .as_mut() .ok_or(CutoverHostError::MutationFailed) } fn observe_prerequisite( &mut self, operation: &CutoverOperation, ) -> Result { let snapshot = self .native .inspect() .map_err(|_| CutoverHostError::ObservationFailed)?; let matches = match operation { CutoverOperation::InstallWindowsPacketFilterPrerequisite => { snapshot.packet_filter_present } CutoverOperation::InstallVcRuntimePrerequisite => snapshot.vc_runtime_present, CutoverOperation::UninstallTransactionWindowsPacketFilter => { !snapshot.packet_filter_present } _ => return Err(CutoverHostError::Unsupported), }; cutover_delegate_fingerprint( "proxywarden:privileged-cutover-prerequisite-state:v1", operation, if matches { "expected" } else { "unexpected" }, ) } fn observe_scm( &mut self, operation: &CutoverOperation, journal: &CutoverJournal, ) -> Result { observe_proxifyre_cutover_scm_state(&mut self.scm, operation, &journal.before_state.service) .map_err(|_| CutoverHostError::ObservationFailed) } fn mutate_prerequisite( &mut self, operation: &CutoverOperation, journal: &CutoverJournal, ) -> Result, CutoverHostError> { let (effect, authority_evidence) = match operation { CutoverOperation::InstallWindowsPacketFilterPrerequisite => { if journal .before_state .prerequisites .windows_packet_filter_present { return Err(CutoverHostError::MutationFailed); } let lease = self .packet_filter_lease .as_ref() .ok_or(CutoverHostError::MutationFailed)?; let code = self .native .run_installer(ProxifyreInstallerCommand::InstallPacketFilter( lease.asset_path(), )) .map_err(|_| CutoverHostError::MutationFailed)?; ( map_cutover_packet_filter_install_exit(code) .map_err(|_| CutoverHostError::MutationFailed)?, Some(MutationAuthorityEvidence::WindowsPacketFilterInstalledByTransaction), ) } CutoverOperation::InstallVcRuntimePrerequisite => { if journal.before_state.prerequisites.vc_runtime_present { return Err(CutoverHostError::MutationFailed); } let lease = self .vc_runtime_lease .as_ref() .ok_or(CutoverHostError::MutationFailed)?; let code = self .native .run_installer(ProxifyreInstallerCommand::InstallVcRuntime( lease.asset_path(), )) .map_err(|_| CutoverHostError::MutationFailed)?; ( map_proxifyre_installer_exit(ProxifyreInstallerKind::VcRuntimeInstall, code) .map_err(|_| CutoverHostError::MutationFailed)?, None, ) } CutoverOperation::UninstallTransactionWindowsPacketFilter => ( uninstall_cutover_transaction_packet_filter(journal) .map_err(|_| CutoverHostError::MutationFailed)?, None, ), _ => return Err(CutoverHostError::Unsupported), }; self.reboot_required |= effect.reboot_required; Ok(authority_evidence) } } impl CutoverHost for SystemCutoverHost { fn capture_and_lease( &mut self, plan: &ProxifyreCutoverPlan, ) -> Result { if self.candidate.is_none() || self .scm .query_current_service() .map_err(|_| CutoverHostError::PreconditionFailed)? .path_matches { return Err(CutoverHostError::PreconditionFailed); } let service = self .scm .capture_legacy_service() .map_err(|_| CutoverHostError::PreconditionFailed)?; let filesystem = self.filesystem.capture_legacy_filesystem()?; let current_components_parent = self.filesystem.capture_current_components_parent()?; let native = self .native .inspect() .map_err(|_| CutoverHostError::PreconditionFailed)?; if self.captured_packet_filter_present != Some(native.packet_filter_present) || self.captured_vc_runtime_present != Some(native.vc_runtime_present) { return Err(CutoverHostError::PreconditionFailed); } let current_candidate = self.candidate_writer()?.prepared().snapshot().clone(); let identity_fingerprint = legacy_service_snapshot_fingerprint(&service); Ok(LegacyBeforeState { identity: ProxifyreLegacyIdentity::V2_2_1ToolsPrimaryService, identity_fingerprint, service, filesystem, current_components_parent, current_candidate, prerequisites: PrerequisiteBeforeState { windows_packet_filter_present: native.packet_filter_present, vc_runtime_present: native.vc_runtime_present, }, package_fingerprint: plan.package_fingerprint.clone(), config_fingerprint: plan.config_fingerprint.clone(), additional_matching_service: has_additional_matching_legacy_proxifyre_service( &plan.legacy_root, ), }) } fn reacquire_leases(&mut self, journal: &CutoverJournal) -> Result<(), CutoverHostError> { self.filesystem.reacquire_leases(journal) } fn observe( &mut self, operation: &CutoverOperation, journal: &CutoverJournal, ) -> Result { self.last_observed_identity = None; if *operation == CutoverOperation::PromoteCurrentCandidate && self.candidate.is_some() { self.handoff_complete_candidate(journal)?; } if *operation == CutoverOperation::TombstoneCurrentCandidate && self.candidate.is_some() { self.handoff_partial_candidate_for_rollback(journal)?; } if SystemProxifyreCutoverFilesystem::handles_operation(operation) { let observed = self.filesystem.observe_operation(operation, journal)?; self.last_observed_identity = self.filesystem.observed_object_identity(); return Ok(observed); } if candidate_operation(operation) { let has_live_candidate = self.candidate.is_some(); let candidate = &mut self.candidate; let filesystem = &mut self.filesystem; let (observed, identity) = dispatch_candidate_observation( has_live_candidate, || observe_live_candidate(candidate.as_mut(), operation), || observe_recovered_candidate(filesystem, operation, journal), )?; self.last_observed_identity = identity; return Ok(observed); } if prerequisite_operation(operation) { return self.observe_prerequisite(operation); } if scm_operation(operation) { return self.observe_scm(operation, journal); } Err(CutoverHostError::Unsupported) } fn observed_object_identity( &mut self, _operation: &CutoverOperation, _journal: &CutoverJournal, ) -> Result, CutoverHostError> { Ok(self.last_observed_identity.clone()) } fn expected_effect( &mut self, operation: &CutoverOperation, journal: &CutoverJournal, ) -> Result { if SystemProxifyreCutoverFilesystem::handles_operation(operation) { return self .filesystem .expected_operation_effect(operation, journal); } if scm_operation(operation) { return expected_proxifyre_cutover_scm_effect(operation) .map_err(|_| CutoverHostError::ObservationFailed); } if candidate_operation(operation) { return candidate_operation_state_fingerprint(operation, true); } if prerequisite_operation(operation) { return cutover_delegate_fingerprint( "proxywarden:privileged-cutover-prerequisite-state:v1", operation, "expected", ); } Err(CutoverHostError::Unsupported) } fn mutate( &mut self, operation: &CutoverOperation, journal: &CutoverJournal, ) -> Result, CutoverHostError> { if SystemProxifyreCutoverFilesystem::handles_operation(operation) { self.filesystem.mutate_operation(operation, journal)?; return Ok(None); } if candidate_operation(operation) { mutate_proxifyre_cutover_candidate(self.candidate_writer_mut()?, operation) .map_err(|_| CutoverHostError::MutationFailed) .and_then(|handled| handled.then_some(()).ok_or(CutoverHostError::Unsupported))?; return Ok(None); } if prerequisite_operation(operation) { return self.mutate_prerequisite(operation, journal); } if scm_operation(operation) { match operation { CutoverOperation::StartCurrentService => self .filesystem .verify_current_runtime_read_leases(journal)?, CutoverOperation::StartLegacyService => self .filesystem .prepare_legacy_service_start_leases(journal)?, _ => {} } mutate_proxifyre_cutover_scm(&mut self.scm, operation, &journal.before_state.service) .map_err(|_| CutoverHostError::MutationFailed) .and_then(|handled| handled.then_some(()).ok_or(CutoverHostError::Unsupported))?; return Ok(None); } Err(CutoverHostError::Unsupported) } fn verify_current( &mut self, journal: &CutoverJournal, ) -> Result { let snapshot = self .native .inspect() .map_err(|_| CutoverHostError::ObservationFailed)?; let expected_service = match journal.plan.original_service_state { LegacyServiceState::Running => ProxifyreNativeServiceStatus::Running, LegacyServiceState::Stopped => ProxifyreNativeServiceStatus::Stopped, }; if snapshot.ownership != ProxifyreNativeOwnership::Managed || snapshot.service_status != expected_service || snapshot.installed_version.as_deref() != Some(&journal.plan.bundled_version) || !snapshot.install_root_trusted || !snapshot.install_root_reparse_free || !snapshot.receipt_valid || !snapshot.receipt_files_match || !snapshot.service_path_matches || !snapshot.demand_start || !snapshot.failure_recovery_disabled || !snapshot.builtin_users_start_denied || !snapshot.vc_runtime_present || !snapshot.packet_filter_present { return Err(CutoverHostError::VerificationFailed); } classify_cutover_runtime_config(self.native.verify_runtime_config( &self.native.install_root().join("app-config.json"), &journal.plan.config_fingerprint, ))?; let value = json!({ "package": journal.plan.package_fingerprint, "config": journal.plan.config_fingerprint, "candidate": journal.before_state.current_candidate.manifest_fingerprint, "service": match expected_service { ProxifyreNativeServiceStatus::Running => "running", _ => "stopped", }, }); Ok(StateFingerprint::digest( "proxywarden:privileged-cutover-current-verified:v1", value.to_string().as_bytes(), )) } fn verify_current_inventory( &mut self, _journal: &CutoverJournal, ) -> Result { StateFingerprint::from_sha256(component_inventory_fingerprint_for_cutover( &inventory_proxyfier(), )) .map_err(|_| CutoverHostError::VerificationFailed) } fn verify_rollback(&mut self, journal: &CutoverJournal) -> Result<(), CutoverHostError> { self.filesystem.verify_rollback_restored(journal)?; let legacy = self .scm .capture_legacy_service() .map_err(|_| CutoverHostError::ObservationFailed)?; let current = self .scm .query_current_service() .map_err(|_| CutoverHostError::ObservationFailed)?; let native = self .native .inspect() .map_err(|_| CutoverHostError::ObservationFailed)?; let vc_installed_by_transaction = journal.mutations.iter().any(|record| { record.direction == crate::component_cutover::MutationDirection::Forward && record.operation == CutoverOperation::InstallVcRuntimePrerequisite && record.effect.as_ref().is_some_and(|effect| { effect.disposition == crate::component_cutover::EffectDisposition::ExpectedEffect }) }); let expected_vc_runtime = journal.before_state.prerequisites.vc_runtime_present || vc_installed_by_transaction; if !legacy_service_owns_shared_name( legacy == journal.before_state.service, current.path_matches, ) || native.packet_filter_present != journal .before_state .prerequisites .windows_packet_filter_present || native.vc_runtime_present != expected_vc_runtime { return Err(CutoverHostError::VerificationFailed); } Ok(()) } fn verify_cleanup(&mut self, journal: &CutoverJournal) -> Result<(), CutoverHostError> { self.filesystem.verify_cleanup_ready(journal)?; let legacy = self .scm .query_legacy_service() .map_err(|_| CutoverHostError::ObservationFailed)?; let current = self .native .inspect() .map_err(|_| CutoverHostError::ObservationFailed)?; if !current_service_owns_shared_name(legacy.path_matches, current.service_path_matches) || current.ownership != ProxifyreNativeOwnership::Managed || !current.receipt_valid || !current.receipt_files_match { return Err(CutoverHostError::VerificationFailed); } Ok(()) } } fn classify_cutover_runtime_config( result: Result, ) -> Result<(), CutoverHostError> { match result { Ok(RuntimeConfigVerification::Match) => Ok(()), Ok(RuntimeConfigVerification::Mismatch) => Err(CutoverHostError::VerificationFailed), Err(_) => Err(CutoverHostError::ObservationFailed), } } #[cfg(debug_assertions)] #[doc(hidden)] pub fn classify_cutover_runtime_config_for_tests( result: Result, ) -> Result<(), CutoverHostError> { classify_cutover_runtime_config(result) } type CandidateObservationResult = Result<(StateFingerprint, Option), CutoverHostError>; fn dispatch_candidate_observation( has_live_candidate: bool, observe_live: impl FnOnce() -> CandidateObservationResult, observe_recovered: impl FnOnce() -> CandidateObservationResult, ) -> CandidateObservationResult { if has_live_candidate { observe_live() } else { observe_recovered() } } fn observe_live_candidate( candidate: Option<&mut SystemProxifyreCutoverCandidateWriter>, operation: &CutoverOperation, ) -> CandidateObservationResult { let observation = candidate .ok_or(CutoverHostError::ObservationFailed)? .observe_candidate(operation) .map_err(|_| CutoverHostError::ObservationFailed)?; let (state, identity) = match observation { ProxifyreCutoverCandidateObservation::Absent => ("absent", None), ProxifyreCutoverCandidateObservation::Expected(snapshot) => { ("expected", Some(snapshot.identity)) } ProxifyreCutoverCandidateObservation::Unknown => { return Err(CutoverHostError::ObservationFailed) } }; Ok(( cutover_delegate_fingerprint( "proxywarden:privileged-cutover-candidate-state:v1", operation, state, )?, identity, )) } fn observe_recovered_candidate( filesystem: &mut SystemProxifyreCutoverFilesystem, operation: &CutoverOperation, journal: &CutoverJournal, ) -> CandidateObservationResult { let observed = filesystem.observe_recovered_candidate_operation(operation, journal)?; Ok((observed, filesystem.observed_object_identity())) } #[cfg(debug_assertions)] #[doc(hidden)] pub fn dispatch_candidate_observation_for_tests( has_live_candidate: bool, observe_live: impl FnOnce() -> CandidateObservationResult, observe_recovered: impl FnOnce() -> CandidateObservationResult, ) -> CandidateObservationResult { dispatch_candidate_observation(has_live_candidate, observe_live, observe_recovered) } fn legacy_service_owns_shared_name(legacy_path_matches: bool, current_path_matches: bool) -> bool { legacy_path_matches && !current_path_matches } fn current_service_owns_shared_name(legacy_path_matches: bool, current_path_matches: bool) -> bool { !legacy_path_matches && current_path_matches } #[cfg(debug_assertions)] #[doc(hidden)] pub fn shared_service_name_path_ownership_for_tests( legacy_path_matches: bool, current_path_matches: bool, ) -> (bool, bool) { ( legacy_service_owns_shared_name(legacy_path_matches, current_path_matches), current_service_owns_shared_name(legacy_path_matches, current_path_matches), ) } fn candidate_operation(operation: &CutoverOperation) -> bool { matches!( operation, CutoverOperation::CreateCurrentCandidateRoot | CutoverOperation::WriteCurrentCandidatePackageEntry(_) | CutoverOperation::WriteCurrentCandidateConfig | CutoverOperation::WriteCurrentCandidateMarker | CutoverOperation::WriteCurrentCandidateReceipt ) } fn prerequisite_operation(operation: &CutoverOperation) -> bool { matches!( operation, CutoverOperation::InstallWindowsPacketFilterPrerequisite | CutoverOperation::InstallVcRuntimePrerequisite | CutoverOperation::UninstallTransactionWindowsPacketFilter ) } fn scm_operation(operation: &CutoverOperation) -> bool { matches!( operation, CutoverOperation::StopLegacyService | CutoverOperation::DeleteLegacyService | CutoverOperation::CreateCurrentService | CutoverOperation::SetCurrentServicePolicy(_) | CutoverOperation::SetCurrentServiceSecurity | CutoverOperation::StartCurrentService | CutoverOperation::StopCurrentService | CutoverOperation::DeleteCurrentService | CutoverOperation::CreateLegacyService | CutoverOperation::RestoreLegacyServicePolicy(_) | CutoverOperation::RestoreLegacyServiceSecurity | CutoverOperation::StartLegacyService ) } fn cutover_delegate_fingerprint( domain: &str, operation: &CutoverOperation, state: &str, ) -> Result { let value = serde_json::to_vec(&(operation, state)).map_err(|_| CutoverHostError::ObservationFailed)?; Ok(StateFingerprint::digest(domain, &value)) } enum PreparedInput { None, Cutover(Box), InstallProxifyre { proxifyre: Box, packet_filter: Box, vc_runtime: Box, }, UpdateProxifyre(Box), StartProxifyre(GeneratedConfigLease), InstallSingBox { runtime: Box, wrapper: Box, }, UpdateSingBox(Box), StartSingBox(GeneratedConfigLease), } impl PreparedInput { fn package_fingerprint( &self, ) -> Result, PrivilegedRunnerFailure> { let (source, leases): (InstalledPackageSource, Vec<&PrivilegedPackageLease>) = match self { Self::InstallProxifyre { proxifyre, packet_filter, vc_runtime, } => ( InstalledPackageSource::Bundled, vec![ proxifyre.as_ref(), packet_filter.as_ref(), vc_runtime.as_ref(), ], ), Self::UpdateProxifyre(runtime) | Self::UpdateSingBox(runtime) => { (InstalledPackageSource::Cache, vec![runtime.as_ref()]) } Self::InstallSingBox { runtime, wrapper } => ( InstalledPackageSource::Bundled, vec![runtime.as_ref(), wrapper.as_ref()], ), Self::None | Self::Cutover(_) | Self::StartProxifyre(_) | Self::StartSingBox(_) => { return Ok(None) } }; planned_leased_package(source, &leases).map(Some) } fn configuration_fingerprint(&self, action: PrivilegedAction) -> String { match self { Self::StartProxifyre(config) | Self::StartSingBox(config) => config.sha256.clone(), _ => no_config_fingerprint(action), } } } struct GeneratedConfigLease { _file: File, bytes: Vec, sha256: String, } impl GeneratedConfigLease { fn open(path: &Path, component: ManagedComponent) -> Result { let mut file = safe_fs::open_restricted_file_read_lease(path) .map_err(|_| PrivilegedRunnerFailure::PreconditionFailed)?; let mut bytes = Vec::new(); file.by_ref() .take(MAX_GENERATED_CONFIG_BYTES + 1) .read_to_end(&mut bytes) .map_err(|_| PrivilegedRunnerFailure::PreconditionFailed)?; if bytes.is_empty() || bytes.len() as u64 > MAX_GENERATED_CONFIG_BYTES || validate_generated_config(component, &bytes).is_err() { return Err(PrivilegedRunnerFailure::PreconditionFailed); } file.seek(SeekFrom::Start(0)) .map_err(|_| PrivilegedRunnerFailure::PreconditionFailed)?; let sha256 = format!("{:x}", Sha256::digest(&bytes)); Ok(Self { _file: file, bytes, sha256, }) } } fn execute_prepared( action: PrivilegedAction, input: PreparedInput, ) -> Result { match (action, input) { ( PrivilegedAction::CutoverProxifyre | PrivilegedAction::CleanupProxifyreQuarantine, PreparedInput::Cutover(cutover), ) => cutover.execute(), ( PrivilegedAction::InstallProxifyre, PreparedInput::InstallProxifyre { proxifyre, packet_filter, vc_runtime, }, ) => { let mut host = SystemProxifyreNativeHost::from_current_exe() .map_err(|_| PrivilegedRunnerFailure::OperationFailed)?; install_proxifyre_native(&mut host, &proxifyre, &packet_filter, &vc_runtime) .map(proxifyre_result) .map_err(map_proxifyre_error) } (PrivilegedAction::UpdateProxifyre, PreparedInput::UpdateProxifyre(runtime)) => { let mut host = SystemProxifyreNativeHost::from_current_exe() .map_err(|_| PrivilegedRunnerFailure::OperationFailed)?; update_proxifyre_native(&mut host, &runtime) .map(proxifyre_result) .map_err(map_proxifyre_error) } (PrivilegedAction::StartProxifyre, PreparedInput::StartProxifyre(config)) => { let mut host = SystemProxifyreNativeHost::from_current_exe() .map_err(|_| PrivilegedRunnerFailure::OperationFailed)?; match preflight_start_proxifyre_native(&mut host).map_err(map_proxifyre_error)? { ProxifyreNativeStartDisposition::AlreadyRunning => Ok(PrivilegedMutationResult { activation: None, changed: false, reboot_required: false, result_code: None, }), ProxifyreNativeStartDisposition::ReadyForConfigPromotion => { let path = host .promote_runtime_config_bytes(&config.bytes, &config.sha256) .map_err(|_| PrivilegedRunnerFailure::OperationFailed)?; let mut activation = None; let outcome = crate::proxifyre_runtime::start_proxifyre_native_observed( &mut host, &path, &config.sha256, || { activation = observe_activation(ManagedComponent::Proxifyre, &config.sha256); }, ) .map_err(map_proxifyre_error)?; let mut result = proxifyre_result(outcome); result.activation = activation; Ok(result) } } } (PrivilegedAction::StopProxifyre, PreparedInput::None) => { let mut host = SystemProxifyreNativeHost::from_current_exe() .map_err(|_| PrivilegedRunnerFailure::OperationFailed)?; stop_proxifyre_native(&mut host) .map(proxifyre_result) .map_err(map_proxifyre_error) } (PrivilegedAction::ConfigureProxifyreFirewall, PreparedInput::None) => { let mut host = SystemProxifyreNativeHost::from_current_exe() .map_err(|_| PrivilegedRunnerFailure::OperationFailed)?; configure_proxifyre_firewall_native(&mut host) .map(proxifyre_result) .map_err(map_proxifyre_error) } (PrivilegedAction::UninstallProxifyre, PreparedInput::None) => { let mut host = SystemProxifyreNativeHost::from_current_exe() .map_err(|_| PrivilegedRunnerFailure::OperationFailed)?; uninstall_proxifyre_native(&mut host) .map(proxifyre_result) .map_err(map_proxifyre_error) } (PrivilegedAction::InstallSingBox, PreparedInput::InstallSingBox { runtime, wrapper }) => { let mut host = SystemSingBoxNativeHost::from_current_exe() .map_err(|_| PrivilegedRunnerFailure::OperationFailed)?; install_singbox_native(&mut host, &runtime, &wrapper) .map(singbox_result) .map_err(map_singbox_error) } (PrivilegedAction::UpdateSingBox, PreparedInput::UpdateSingBox(runtime)) => { let mut host = SystemSingBoxNativeHost::from_current_exe() .map_err(|_| PrivilegedRunnerFailure::OperationFailed)?; update_singbox_native(&mut host, &runtime) .map(singbox_result) .map_err(map_singbox_error) } (PrivilegedAction::StartSingBox, PreparedInput::StartSingBox(config)) => { let mut host = SystemSingBoxNativeHost::from_current_exe() .map_err(|_| PrivilegedRunnerFailure::OperationFailed)?; match preflight_start_singbox_native(&mut host).map_err(map_singbox_error)? { SingBoxStartPreflight::AlreadyRunning => Ok(PrivilegedMutationResult { activation: None, changed: false, reboot_required: false, result_code: None, }), SingBoxStartPreflight::Ready => { let path = host .promote_runtime_config_bytes(&config.bytes, &config.sha256) .map_err(|_| PrivilegedRunnerFailure::OperationFailed)?; let mut activation = None; let outcome = crate::singbox_runtime::start_singbox_native_observed( &mut host, &path, &config.sha256, || { activation = observe_activation(ManagedComponent::SingBox, &config.sha256); }, ) .map_err(map_singbox_error)?; let mut result = singbox_result(outcome); result.activation = activation; Ok(result) } } } (PrivilegedAction::StopSingBox, PreparedInput::None) => { let mut host = SystemSingBoxNativeHost::from_current_exe() .map_err(|_| PrivilegedRunnerFailure::OperationFailed)?; stop_singbox_native(&mut host) .map(singbox_result) .map_err(map_singbox_error) } (PrivilegedAction::UninstallSingBox, PreparedInput::None) => { let mut host = SystemSingBoxNativeHost::from_current_exe() .map_err(|_| PrivilegedRunnerFailure::OperationFailed)?; uninstall_singbox_native(&mut host) .map(singbox_result) .map_err(map_singbox_error) } _ => Err(PrivilegedRunnerFailure::PreconditionFailed), } } fn observe_activation( component: ManagedComponent, hash: &str, ) -> Option { let service = match component { ManagedComponent::Proxifyre => crate::process::KnownWindowsService::Proxifyre, ManagedComponent::SingBox => crate::process::KnownWindowsService::SingBox, }; Some(crate::privileged_jobs::ActivationAcknowledgement { component, config_sha256: hash.to_string(), instance: crate::process::running_service_instance(service).ok()?, }) } fn proxifyre_result( outcome: crate::proxifyre_runtime::ProxifyreNativeOutcome, ) -> PrivilegedMutationResult { PrivilegedMutationResult { activation: None, changed: outcome.changed, reboot_required: outcome.reboot_required, result_code: None, } } fn singbox_result(outcome: SingBoxNativeOutcome) -> PrivilegedMutationResult { let changed = matches!( outcome, SingBoxNativeOutcome::Installed | SingBoxNativeOutcome::Updated | SingBoxNativeOutcome::Started | SingBoxNativeOutcome::Stopped | SingBoxNativeOutcome::Uninstalled ); PrivilegedMutationResult { activation: None, changed, reboot_required: false, result_code: None, } } fn map_proxifyre_error(error: ProxifyreNativeError) -> PrivilegedRunnerFailure { match error { ProxifyreNativeError::InvalidPackage | ProxifyreNativeError::InvalidArchive => { PrivilegedRunnerFailure::PackageVerificationFailed } ProxifyreNativeError::ServiceCollision => PrivilegedRunnerFailure::ServiceCollision, ProxifyreNativeError::InvalidReceipt | ProxifyreNativeError::OwnershipMismatch => { PrivilegedRunnerFailure::OwnershipMismatch } ProxifyreNativeError::ServiceRunning | ProxifyreNativeError::ServiceNotStopped | ProxifyreNativeError::ServiceContract | ProxifyreNativeError::DowngradeRejected | ProxifyreNativeError::RuntimeConfigInvalid => PrivilegedRunnerFailure::PreconditionFailed, ProxifyreNativeError::InstallerExit { .. } | ProxifyreNativeError::HostStep(_) | ProxifyreNativeError::RollbackFailed => PrivilegedRunnerFailure::OperationFailed, } } fn map_singbox_error(error: SingBoxNativeError) -> PrivilegedRunnerFailure { match error { SingBoxNativeError::InvalidPackage | SingBoxNativeError::InvalidArchive => { PrivilegedRunnerFailure::PackageVerificationFailed } SingBoxNativeError::ServiceCollision => PrivilegedRunnerFailure::ServiceCollision, SingBoxNativeError::InvalidReceipt | SingBoxNativeError::OwnershipMismatch => { PrivilegedRunnerFailure::OwnershipMismatch } SingBoxNativeError::ServiceRunning | SingBoxNativeError::ServiceNotStopped | SingBoxNativeError::ServiceContract | SingBoxNativeError::DowngradeRejected | SingBoxNativeError::RuntimeConfigInvalid => PrivilegedRunnerFailure::PreconditionFailed, SingBoxNativeError::CleanupPending => PrivilegedRunnerFailure::CleanupPending, SingBoxNativeError::HostStep(_) | SingBoxNativeError::RollbackFailed => { PrivilegedRunnerFailure::OperationFailed } } } fn installed_app_root() -> Result { let executable = std::env::current_exe().map_err(|_| PrivilegedRunnerFailure::RunnerUnavailable)?; safe_fs::verify_path_under_trusted_program_files(&executable) .map_err(|_| PrivilegedRunnerFailure::RunnerUnavailable)?; executable .parent() .map(Path::to_path_buf) .ok_or(PrivilegedRunnerFailure::RunnerUnavailable) } pub(crate) fn installed_cutover_external_status() -> Option { installed_app_root() .ok() .map(|app_root| verify_cutover_external_mutation_status(&app_root)) } fn read_local_cutover_observation( storage_paths: &StoragePaths, ) -> Option { JsonStorage::new(storage_paths.root.clone()) .read_component_cutover_observation() .ok() .flatten() } fn read_local_cutover_user_evidence( storage_paths: &StoragePaths, ) -> Result { JsonStorage::new(storage_paths.root.clone()) .read_component_cutover_user_evidence() .map_err(|_| PrivilegedRunnerFailure::CutoverStateConflict)? .ok_or(PrivilegedRunnerFailure::CutoverStateConflict) } fn valid_cutover_observation( observation: &ComponentCutoverObservation, ) -> Option<&ComponentCutoverObservation> { let parsed = Uuid::parse_str(&observation.cutover_id).ok()?; (observation.component == "proxifyre" && parsed.get_version() == Some(uuid::Version::Random) && parsed.hyphenated().to_string() == observation.cutover_id && is_sha256(&observation.operation_fingerprint) && is_sha256(&observation.transaction_fingerprint)) .then_some(observation) } fn new_cutover_context( inventory_fingerprint: &str, original_state: Option, initiating_startup_session_id: &str, ) -> PrivilegedCutoverContext { let state = match original_state { Some(LegacyServiceState::Running) => "running", Some(LegacyServiceState::Stopped) => "stopped", None => "sealed-recovery-probe", }; PrivilegedCutoverContext { mode: PrivilegedCutoverMode::New, cutover_id: None, initiating_startup_session_id: Some(initiating_startup_session_id.to_string()), evidence_fingerprint: cutover_context_fingerprint( "proxywarden:privileged-cutover-new-evidence:v1", &[ "proxifyre-2.2.1-tools-primary-topshelf", LEGACY_PROXIFYRE_AUTO_CUTOVER_ROOT, LEGACY_PROXIFYRE_AUTO_CUTOVER_VERSION, state, inventory_fingerprint, initiating_startup_session_id, ], ), user_evidence: None, } } fn recovery_context( observation: &ComponentCutoverObservation, ) -> Result { valid_cutover_observation(observation) .ok_or(PrivilegedRunnerFailure::CutoverRecoveryRequired)?; Ok(PrivilegedCutoverContext { mode: PrivilegedCutoverMode::Recovery, cutover_id: Some(observation.cutover_id.clone()), initiating_startup_session_id: None, evidence_fingerprint: observation.transaction_fingerprint.clone(), user_evidence: None, }) } fn recovery_context_from_journal( journal: &CutoverJournal, ) -> Result { Ok(PrivilegedCutoverContext { mode: PrivilegedCutoverMode::Recovery, cutover_id: Some(journal.plan.cutover_id.clone()), initiating_startup_session_id: None, evidence_fingerprint: cutover_transaction_fingerprint(journal) .map_err(|_| PrivilegedRunnerFailure::CutoverRecoveryRequired)?, user_evidence: None, }) } fn recovery_probe_context(inventory_fingerprint: &str) -> PrivilegedCutoverContext { PrivilegedCutoverContext { mode: PrivilegedCutoverMode::Recovery, cutover_id: None, initiating_startup_session_id: None, evidence_fingerprint: cutover_recovery_probe_fingerprint(inventory_fingerprint), user_evidence: None, } } fn cleanup_context( observation: &ComponentCutoverObservation, user_evidence: &ComponentCutoverUserEvidence, ) -> Result { valid_cutover_observation(observation) .filter(|value| { matches!( value.state, CutoverDisplayState::AwaitingNextStart | CutoverDisplayState::AwaitingRouteSmoke | CutoverDisplayState::CleanupReady | CutoverDisplayState::CleanupPending ) }) .ok_or(PrivilegedRunnerFailure::CutoverStateConflict)?; validate_component_cutover_user_evidence(user_evidence) .map_err(|_| PrivilegedRunnerFailure::CutoverStateConflict)?; if user_evidence.cutover_id != observation.cutover_id || !user_evidence.route_smoke_confirmed || user_evidence.confirmed_at_epoch_seconds.is_none() { return Err(PrivilegedRunnerFailure::CutoverStateConflict); } let user_evidence_fingerprint = component_cutover_user_evidence_fingerprint(user_evidence) .map_err(|_| PrivilegedRunnerFailure::CutoverStateConflict)?; Ok(PrivilegedCutoverContext { mode: PrivilegedCutoverMode::Cleanup, cutover_id: Some(observation.cutover_id.clone()), initiating_startup_session_id: None, evidence_fingerprint: cutover_context_fingerprint( "proxywarden:privileged-cutover-pending-evidence:v1", &[ &observation.transaction_fingerprint, &user_evidence_fingerprint, ], ), user_evidence: Some(user_evidence.clone()), }) } fn validate_elevated_cleanup_evidence( journal: &CutoverJournal, live_inventory_fingerprint: &str, evidence: &ComponentCutoverUserEvidence, now_epoch_seconds: u64, ) -> Result<(), PrivilegedRunnerFailure> { validate_component_cutover_user_evidence(evidence) .map_err(|_| PrivilegedRunnerFailure::CutoverStateConflict)?; if evidence.cutover_id != journal.plan.cutover_id || evidence.startup_session_id == journal.plan.created_startup_session_id || evidence.current_inventory_fingerprint != live_inventory_fingerprint || !evidence.route_smoke_confirmed || evidence.observed_at_epoch_seconds > now_epoch_seconds || evidence.confirmed_at_epoch_seconds > Some(now_epoch_seconds) || journal.next_start.as_ref().is_some_and(|sealed| { sealed.startup_session_id != evidence.startup_session_id || sealed.current_inventory_fingerprint != evidence.current_inventory_fingerprint || sealed.observed_at_epoch_seconds != evidence.observed_at_epoch_seconds }) || journal.route_smoke.as_ref().is_some_and(|sealed| { !sealed.confirmed_by_user || Some(sealed.confirmed_at_epoch_seconds) != evidence.confirmed_at_epoch_seconds }) { return Err(PrivilegedRunnerFailure::CutoverStateConflict); } Ok(()) } fn validate_new_cutover_discovery( inventory: &ComponentInventory, ) -> Result { if !inventory.issues.is_empty() || inventory.candidates.len() != 1 || has_additional_matching_legacy_proxifyre_service(Path::new( LEGACY_PROXIFYRE_AUTO_CUTOVER_ROOT, )) { return Err(PrivilegedRunnerFailure::CutoverIdentityRejected); } let candidate = inventory .selected_candidate() .filter(|candidate| { candidate.classification == ComponentClassification::ManagedLegacy && candidate.role == CandidateRole::Legacy && candidate.issues.is_empty() && normalized_path(&candidate.root) == normalized_text_path(LEGACY_PROXIFYRE_AUTO_CUTOVER_ROOT) && candidate.binary_version.as_deref() == Some(LEGACY_PROXIFYRE_AUTO_CUTOVER_VERSION) && candidate.executable_path.as_deref().is_some_and(|path| { normalized_path(path) == normalized_text_path(r"C:\Tools\ProxiFyre\ProxiFyre.exe") }) }) .ok_or(PrivilegedRunnerFailure::CutoverIdentityRejected)?; let service = candidate .service .as_ref() .filter(|service| { service.name.eq_ignore_ascii_case("ProxiFyreService") && service.path_matches_candidate && service.binary_version.as_deref() == Some(LEGACY_PROXIFYRE_AUTO_CUTOVER_VERSION) }) .ok_or(PrivilegedRunnerFailure::CutoverIdentityRejected)?; match service.status.trim().to_ascii_lowercase().as_str() { "running" => Ok(LegacyServiceState::Running), "stopped" => Ok(LegacyServiceState::Stopped), _ => Err(PrivilegedRunnerFailure::CutoverIdentityRejected), } } fn cutover_context_fingerprint(domain: &str, fields: &[&str]) -> String { let mut digest = Sha256::new(); digest.update(domain.as_bytes()); digest.update([0]); for field in fields { digest.update((field.len() as u64).to_le_bytes()); digest.update(field.as_bytes()); } format!("{:x}", digest.finalize()) } fn is_sha256(value: &str) -> bool { value.len() == 64 && value .bytes() .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) } fn canonical_uuid_v4(value: &str) -> Option { let parsed = Uuid::parse_str(value).ok()?; (parsed.get_version() == Some(uuid::Version::Random) && parsed.hyphenated().to_string() == value) .then(|| value.to_string()) } fn resolve_local_configuration( action: PrivilegedAction, storage_paths: &StoragePaths, ) -> Result { let (path, component) = match action { PrivilegedAction::StartProxifyre => ( storage_paths.generated_dir.join(PROXIFYRE_OUTPUT_FILE), ManagedComponent::Proxifyre, ), PrivilegedAction::StartSingBox => ( storage_paths.generated_dir.join(SINGBOX_OUTPUT_FILE), ManagedComponent::SingBox, ), _ => return Ok(no_config_fingerprint(action)), }; Ok(GeneratedConfigLease::open(&path, component)?.sha256) } fn no_config_fingerprint(action: PrivilegedAction) -> String { let action = match action { PrivilegedAction::InstallProxifyre => "proxifyre.install", PrivilegedAction::UpdateProxifyre => "proxifyre.update", PrivilegedAction::StartProxifyre => "proxifyre.start", PrivilegedAction::StopProxifyre => "proxifyre.stop", PrivilegedAction::ConfigureProxifyreFirewall => "proxifyre.configure-firewall", PrivilegedAction::UninstallProxifyre => "proxifyre.uninstall", PrivilegedAction::CutoverProxifyre => "proxifyre.cutover", PrivilegedAction::CleanupProxifyreQuarantine => "proxifyre.cleanup-quarantine", PrivilegedAction::InstallSingBox => "sing-box.install", PrivilegedAction::UpdateSingBox => "sing-box.update", PrivilegedAction::StartSingBox => "sing-box.start", PrivilegedAction::StopSingBox => "sing-box.stop", PrivilegedAction::UninstallSingBox => "sing-box.uninstall", }; format!( "{:x}", Sha256::digest(format!("proxywarden:privileged:no-config:v1:{action}").as_bytes()) ) } fn planned_leased_package( source: InstalledPackageSource, leases: &[&PrivilegedPackageLease], ) -> Result { let first = leases .first() .ok_or(PrivilegedRunnerFailure::PackageVerificationFailed)?; let origin_proof = match source { InstalledPackageSource::Bundled => None, InstalledPackageSource::Cache => { first.proof().independent_proof.as_ref().map(planned_origin) } }; if source == InstalledPackageSource::Cache && (leases.len() != 1 || origin_proof.is_none()) { return Err(PrivilegedRunnerFailure::PackageVerificationFailed); } Ok(PlannedPackageFingerprint { source, version: first.proof().version.clone(), assets: leases .iter() .map(|lease| planned_asset(lease.proof())) .collect(), origin_proof, }) } fn planned_cached_package(plan: &PrivilegedCachedUpdatePlan) -> PlannedPackageFingerprint { PlannedPackageFingerprint { source: InstalledPackageSource::Cache, version: plan.version.clone(), assets: vec![PlannedAssetFingerprint { component_id: plan.component_id, version: plan.version.clone(), name: plan.independent_proof.asset_name.clone(), sha256: plan.independent_proof.sha256_from_api.clone(), size: plan.independent_proof.size, }], origin_proof: Some(planned_origin(&plan.independent_proof)), } } fn planned_asset(proof: &PrivilegedPackageProof) -> PlannedAssetFingerprint { PlannedAssetFingerprint { component_id: proof.component_id, version: proof.version.clone(), name: proof.asset_name.clone(), sha256: proof.sha256.clone(), size: proof.size, } } fn planned_origin(proof: &GithubReleaseDigestProof) -> PlannedGithubOriginProof { PlannedGithubOriginProof { repository: proof.repository.clone(), stable_tag: proof.stable_tag.clone(), release_id: proof.release_id, asset_id: proof.asset_id, asset_name: proof.asset_name.clone(), size: proof.size, sha256_from_api: proof.sha256_from_api.clone(), } } fn live_inventory(action: PrivilegedAction) -> ComponentInventory { match action.component() { ManagedComponent::Proxifyre => inventory_proxyfier(), ManagedComponent::SingBox => inventory_singbox(), } } fn normalized_path(path: &Path) -> String { normalized_text_path(&path.to_string_lossy()) } fn normalized_text_path(value: &str) -> String { value.trim().replace('/', "\\").to_ascii_lowercase() } #[cfg(debug_assertions)] #[doc(hidden)] pub fn validate_generated_config_bytes_for_tests( component: ManagedComponent, bytes: &[u8], ) -> bool { validate_generated_config(component, bytes).is_ok() } fn validate_generated_config(component: ManagedComponent, bytes: &[u8]) -> Result<(), ()> { match component { ManagedComponent::Proxifyre => validate_proxifyre_config(bytes), ManagedComponent::SingBox => validate_singbox_config(bytes), } } #[derive(Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct StrictProxifyreConfig { log_level: String, bypass_lan: bool, proxies: Vec, } #[derive(Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct StrictProxifyreProxy { app_names: Vec, socks5_proxy_endpoint: String, supported_protocols: Vec, } fn validate_proxifyre_config(bytes: &[u8]) -> Result<(), ()> { let config: StrictProxifyreConfig = serde_json::from_slice(bytes).map_err(|_| ())?; if config.log_level != "Info" || !config.bypass_lan { return Err(()); } for proxy in config.proxies { if proxy.app_names.is_empty() || proxy.app_names.iter().any(|name| !safe_nonempty_text(name)) || !strict_plain_endpoint(&proxy.socks5_proxy_endpoint) || proxy.supported_protocols.is_empty() { return Err(()); } let protocol_count = proxy.supported_protocols.len(); let mut protocols = proxy.supported_protocols; protocols.sort(); protocols.dedup(); if protocols .iter() .any(|value| value != "TCP" && value != "UDP") || protocols.len() != protocol_count || protocols.len() > 2 { return Err(()); } } Ok(()) } fn strict_plain_endpoint(value: &str) -> bool { if value.contains(['/', '@', '?', '#', '\\']) || value.chars().any(char::is_whitespace) { return false; } let Some((host, port)) = value.rsplit_once(':') else { return false; }; let valid_host = if let Some(ipv6) = host .strip_prefix('[') .and_then(|host| host.strip_suffix(']')) { ipv6.parse::().is_ok() } else { !host.contains([':', '[', ']']) && safe_nonempty_text(host) }; valid_host && port.bytes().all(|byte| byte.is_ascii_digit()) && port.parse::().is_ok_and(|parsed_port| parsed_port > 0) } #[test] fn strict_proxy_endpoint_accepts_bracketed_ipv6_without_relaxing_url_rejection() { assert!(strict_plain_endpoint("[2001:db8::1]:1080")); assert!(strict_plain_endpoint("proxy.example.test:1080")); for endpoint in [ "2001:db8::1:1080", "[wrong]:1080", "host:0", "host:65536", "host:1080/path", "user@host:1080", "host:1080?x", "host :1080", ] { assert!(!strict_plain_endpoint(endpoint), "{endpoint}"); } } fn validate_singbox_config(bytes: &[u8]) -> Result<(), ()> { let value: Value = serde_json::from_slice(bytes).map_err(|_| ())?; let root = exact_object(&value, &["log", "inbounds", "outbounds", "route"])?; validate_singbox_log(root.get("log").ok_or(())?)?; validate_singbox_inbounds(root.get("inbounds").ok_or(())?)?; validate_singbox_outbounds(root.get("outbounds").ok_or(())?)?; validate_singbox_route(root.get("route").ok_or(())?) } fn validate_singbox_log(value: &Value) -> Result<(), ()> { let log = exact_object(value, &["disabled", "level", "timestamp"])?; if log.get("disabled").and_then(Value::as_bool) != Some(false) || log.get("level").and_then(Value::as_str) != Some("info") || log.get("timestamp").and_then(Value::as_bool) != Some(true) { return Err(()); } Ok(()) } fn validate_singbox_inbounds(value: &Value) -> Result<(), ()> { let inbounds = value.as_array().ok_or(())?; if inbounds.len() != 1 { return Err(()); } let inbound = exact_object( &inbounds[0], &[ "type", "tag", "listen", "listen_port", "users", "set_system_proxy", ], )?; let listen = inbound.get("listen").and_then(Value::as_str).ok_or(())?; if inbound.get("type").and_then(Value::as_str) != Some("mixed") || inbound.get("tag").and_then(Value::as_str) != Some(DEFAULT_MIXED_INBOUND_TAG) || !matches!(listen, "127.0.0.1" | "::1" | "localhost") || !valid_json_port(inbound.get("listen_port").ok_or(())?) || !inbound .get("users") .and_then(Value::as_array) .is_some_and(Vec::is_empty) || inbound.get("set_system_proxy").and_then(Value::as_bool) != Some(false) { return Err(()); } Ok(()) } fn validate_singbox_outbounds(value: &Value) -> Result<(), ()> { let outbounds = value.as_array().ok_or(())?; if outbounds.len() != 3 { return Err(()); } validate_vpn_outbound(&outbounds[0])?; validate_fixed_outbound(&outbounds[1], "direct", DEFAULT_DIRECT_OUTBOUND_TAG)?; validate_fixed_outbound(&outbounds[2], "block", DEFAULT_BLOCK_OUTBOUND_TAG) } fn validate_fixed_outbound(value: &Value, kind: &str, tag: &str) -> Result<(), ()> { let outbound = exact_object(value, &["type", "tag"])?; if outbound.get("type").and_then(Value::as_str) != Some(kind) || outbound.get("tag").and_then(Value::as_str) != Some(tag) { return Err(()); } Ok(()) } fn validate_vpn_outbound(value: &Value) -> Result<(), ()> { let outbound = value.as_object().ok_or(())?; let kind = outbound.get("type").and_then(Value::as_str).ok_or(())?; let allowed = match kind { "vless" => &[ "type", "tag", "server", "server_port", "uuid", "flow", "tls", "transport", "packet_encoding", ][..], "vmess" => &[ "type", "tag", "server", "server_port", "uuid", "security", "tls", "transport", ][..], "trojan" => &[ "type", "tag", "server", "server_port", "password", "tls", "transport", ][..], "shadowsocks" => &["type", "tag", "server", "server_port", "method", "password"][..], "hysteria2" => &[ "type", "tag", "server", "server_port", "password", "up_mbps", "down_mbps", "obfs", "tls", ][..], _ => return Err(()), }; require_allowed_keys(outbound, allowed)?; if outbound.get("tag").and_then(Value::as_str) != Some(DEFAULT_VPN_OUTBOUND_TAG) || !outbound .get("server") .and_then(Value::as_str) .is_some_and(safe_nonempty_text) || !outbound.get("server_port").is_some_and(valid_json_port) { return Err(()); } match kind { "vless" => { require_text(outbound, "uuid")?; if outbound.get("flow").is_some_and(|value| { value .as_str() .is_none_or(|text| text.chars().any(|character| character.is_control())) }) { return Err(()); } if outbound .get("packet_encoding") .is_some_and(|value| value.as_str() != Some("xudp")) { return Err(()); } } "vmess" => { require_text(outbound, "uuid")?; require_text(outbound, "security")?; } "trojan" | "hysteria2" => require_text(outbound, "password")?, "shadowsocks" => { require_text(outbound, "method")?; require_text(outbound, "password")?; } _ => return Err(()), } if let Some(tls) = outbound.get("tls") { validate_tls(tls)?; } if let Some(transport) = outbound.get("transport") { validate_transport(transport)?; } if let Some(obfs) = outbound.get("obfs") { let obfs = exact_object(obfs, &["type", "password"])?; if obfs.get("type").and_then(Value::as_str) != Some("salamander") { return Err(()); } require_text(obfs, "password")?; } for key in ["up_mbps", "down_mbps"] { if outbound .get(key) .is_some_and(|value| value.as_u64().is_none_or(|number| number == 0)) { return Err(()); } } Ok(()) } fn validate_tls(value: &Value) -> Result<(), ()> { let tls = value.as_object().ok_or(())?; require_allowed_keys( tls, &[ "enabled", "server_name", "insecure", "alpn", "utls", "reality", ], )?; if tls.get("enabled").and_then(Value::as_bool) != Some(true) || tls .get("server_name") .is_some_and(|value| !value.as_str().is_some_and(safe_nonempty_text)) || tls.get("insecure").is_some_and(|value| !value.is_boolean()) || tls.get("alpn").is_some_and(|value| { value.as_array().is_none_or(|items| { items.is_empty() || items .iter() .any(|item| !item.as_str().is_some_and(safe_nonempty_text)) }) }) { return Err(()); } if let Some(utls) = tls.get("utls") { let utls = exact_object(utls, &["enabled", "fingerprint"])?; if utls.get("enabled").and_then(Value::as_bool) != Some(true) { return Err(()); } require_text(utls, "fingerprint")?; } if let Some(reality) = tls.get("reality") { let reality = exact_object(reality, &["enabled", "public_key", "short_id"])?; if reality.get("enabled").and_then(Value::as_bool) != Some(true) { return Err(()); } require_text(reality, "public_key")?; require_text(reality, "short_id")?; } Ok(()) } fn validate_transport(value: &Value) -> Result<(), ()> { let transport = value.as_object().ok_or(())?; require_allowed_keys(transport, &["type", "path", "headers"])?; if transport.get("type").and_then(Value::as_str) != Some("ws") || transport .get("path") .is_some_and(|value| !value.as_str().is_some_and(safe_nonempty_text)) { return Err(()); } if let Some(headers) = transport.get("headers") { let headers = headers.as_object().ok_or(())?; require_allowed_keys(headers, &["Host"])?; if headers .get("Host") .is_some_and(|value| !value.as_str().is_some_and(safe_nonempty_text)) { return Err(()); } } Ok(()) } fn validate_singbox_route(value: &Value) -> Result<(), ()> { let route = exact_object(value, &["rules", "final"])?; let rules = route.get("rules").and_then(Value::as_array).ok_or(())?; if rules.len() != 1 || route.get("final").and_then(Value::as_str) != Some(DEFAULT_VPN_OUTBOUND_TAG) { return Err(()); } let rule = exact_object(&rules[0], &["ip_is_private", "outbound"])?; if rule.get("ip_is_private").and_then(Value::as_bool) != Some(true) || rule.get("outbound").and_then(Value::as_str) != Some(DEFAULT_DIRECT_OUTBOUND_TAG) { return Err(()); } Ok(()) } fn exact_object<'a>(value: &'a Value, keys: &[&str]) -> Result<&'a Map, ()> { let object = value.as_object().ok_or(())?; if object.len() != keys.len() || keys.iter().any(|key| !object.contains_key(*key)) { return Err(()); } Ok(object) } fn require_allowed_keys(object: &Map, keys: &[&str]) -> Result<(), ()> { if object.keys().any(|key| !keys.contains(&key.as_str())) { return Err(()); } Ok(()) } fn require_text(object: &Map, key: &str) -> Result<(), ()> { if object .get(key) .and_then(Value::as_str) .is_some_and(safe_nonempty_text) { Ok(()) } else { Err(()) } } fn safe_nonempty_text(value: &str) -> bool { !value.trim().is_empty() && !value.chars().any(|character| character.is_control()) } fn valid_json_port(value: &Value) -> bool { value .as_u64() .is_some_and(|port| (1..=u64::from(u16::MAX)).contains(&port)) }