Files
ProxyWarden/src-tauri/src/privileged_runtime.rs
T
dokril efda8eb98f
CI / Windows baseline (push) Canceled after 0s
Release v2.0.0
2026-09-10 20:59:52 +03:00

2929 lines
114 KiB
Rust

//! 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<dyn PrivilegedBundleVerifier + Send + Sync>;
type SharedUpdateTransport = Arc<dyn UpdateTransport + Send + Sync>;
type SharedSignatureVerifier = Arc<dyn PackageSignatureVerifier + Send + Sync>;
/// 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<ComponentPackageService>,
storage_paths: StoragePaths,
bundle_verifier: SharedBundleVerifier,
startup_session_id: String,
app_root: Option<PathBuf>,
}
impl LocalPrivilegedPlanResolver {
pub fn production(startup_session_id: &str) -> Result<Self, PrivilegedRunnerFailure> {
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<ComponentPackageService>,
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<ComponentPackageService>,
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<ComponentPackageService>,
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<Option<PlannedPackageFingerprint>, 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::<Result<Vec<_>, _>>()
.map_err(|_| PrivilegedRunnerFailure::PackageVerificationFailed)?;
Ok(Some(planned_leased_package(
InstalledPackageSource::Bundled,
&leases.iter().collect::<Vec<_>>(),
)?))
}
}
impl PrivilegedPlanResolver for LocalPrivilegedPlanResolver {
fn resolve(
&self,
action: PrivilegedAction,
) -> Result<ResolvedActionPlan, PrivilegedRunnerFailure> {
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<ResolvedActionPlan, PrivilegedRunnerFailure> {
self.resolve_cutover_action_with_inventory(action, live_inventory(action))
}
fn resolve_cutover_action_with_inventory(
&self,
action: PrivilegedAction,
inventory: ComponentInventory,
) -> Result<ResolvedActionPlan, PrivilegedRunnerFailure> {
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<ResolvedActionPlan, PrivilegedRunnerFailure> {
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<ComponentPackageService>,
storage_paths: StoragePaths,
bundle_verifier: SharedBundleVerifier,
update_transport: Option<SharedUpdateTransport>,
signature_verifier: SharedSignatureVerifier,
package_staging_parent: PathBuf,
verify_staging_app_root: bool,
enforce_cutover_journal: bool,
pending: Mutex<Option<PreparedAction>>,
}
impl SystemPrivilegedRuntime {
pub fn production() -> Result<Self, PrivilegedRunnerFailure> {
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<ComponentPackageService>,
storage_paths: StoragePaths,
package_staging_parent: PathBuf,
bundle_verifier: SharedBundleVerifier,
update_transport: Option<SharedUpdateTransport>,
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<PreparedInput, PrivilegedRunnerFailure> {
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<PrivilegedPackageLease, PrivilegedRunnerFailure> {
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<PrivilegedPackageLease, PrivilegedRunnerFailure> {
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<SealedCutoverStore, PrivilegedRunnerFailure> {
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<Option<String>, 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<ResolvedActionPlan, PrivilegedRunnerFailure> {
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<String> {
(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<String> {
stopped_cutover_route_lifecycle_exception(
action,
phase,
execution_mode,
original_service_state,
config_fingerprint,
)
}
impl PrivilegedPlanResolver for SystemPrivilegedRuntime {
fn resolve(
&self,
action: PrivilegedAction,
) -> Result<ResolvedActionPlan, PrivilegedRunnerFailure> {
self.resolve_internal(action, None)
}
fn resolve_elevated(
&self,
action: PrivilegedAction,
requested_plan: &ResolvedActionPlan,
) -> Result<ResolvedActionPlan, PrivilegedRunnerFailure> {
self.resolve_internal(action, Some(requested_plan))
}
}
impl PrivilegedActionRunner for SystemPrivilegedRuntime {
fn run(
&self,
action: PrivilegedAction,
plan: &ResolvedActionPlan,
) -> Result<PrivilegedMutationResult, PrivilegedRunnerFailure> {
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<PrivilegedPackageLease>,
packet_filter: Box<PrivilegedPackageLease>,
vc_runtime: Box<PrivilegedPackageLease>,
config: GeneratedConfigLease,
packet_filter_present: bool,
vc_runtime_present: bool,
}
struct PreparedTerminalCutoverRetirement {
app_root: PathBuf,
expected_phase: Option<CutoverPhase>,
expectation: CutoverTerminalRetirementExpectation,
}
struct PreparedSystemCutover {
mode: PreparedCutoverMode,
store: Option<SealedCutoverStore>,
new: Option<PreparedNewSystemCutover>,
retirement: Option<PreparedTerminalCutoverRetirement>,
host: Option<SystemCutoverHost>,
user_evidence: Option<ComponentCutoverUserEvidence>,
}
impl PreparedSystemCutover {
fn execute(mut self) -> Result<PrivilegedMutationResult, PrivilegedRunnerFailure> {
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<u64, PrivilegedRunnerFailure> {
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<SystemProxifyreCutoverCandidateWriter>,
native: SystemProxifyreNativeHost,
_proxifyre_lease: Option<Box<PrivilegedPackageLease>>,
packet_filter_lease: Option<Box<PrivilegedPackageLease>>,
vc_runtime_lease: Option<Box<PrivilegedPackageLease>>,
_config_lease: Option<GeneratedConfigLease>,
captured_packet_filter_present: Option<bool>,
captured_vc_runtime_present: Option<bool>,
last_observed_identity: Option<safe_fs::StableObjectIdentity>,
reboot_required: bool,
}
impl SystemCutoverHost {
#[allow(clippy::too_many_arguments)]
fn new(
plan: &ProxifyreCutoverPlan,
candidate: PreparedProxifyreCutoverCandidate,
native: SystemProxifyreNativeHost,
proxifyre_lease: Box<PrivilegedPackageLease>,
packet_filter_lease: Box<PrivilegedPackageLease>,
vc_runtime_lease: Box<PrivilegedPackageLease>,
config_lease: GeneratedConfigLease,
packet_filter_present: bool,
vc_runtime_present: bool,
) -> Result<Self, PrivilegedRunnerFailure> {
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<Self, PrivilegedRunnerFailure> {
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<StateFingerprint, CutoverHostError> {
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<StateFingerprint, CutoverHostError> {
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<Option<MutationAuthorityEvidence>, 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<LegacyBeforeState, CutoverHostError> {
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<StateFingerprint, CutoverHostError> {
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<Option<safe_fs::StableObjectIdentity>, CutoverHostError> {
Ok(self.last_observed_identity.clone())
}
fn expected_effect(
&mut self,
operation: &CutoverOperation,
journal: &CutoverJournal,
) -> Result<StateFingerprint, CutoverHostError> {
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<Option<MutationAuthorityEvidence>, 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<StateFingerprint, CutoverHostError> {
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, CutoverHostError> {
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<RuntimeConfigVerification, ProxifyreNativeHostError>,
) -> 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<RuntimeConfigVerification, ProxifyreNativeHostError>,
) -> Result<(), CutoverHostError> {
classify_cutover_runtime_config(result)
}
type CandidateObservationResult =
Result<(StateFingerprint, Option<safe_fs::StableObjectIdentity>), 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<StateFingerprint, CutoverHostError> {
let value =
serde_json::to_vec(&(operation, state)).map_err(|_| CutoverHostError::ObservationFailed)?;
Ok(StateFingerprint::digest(domain, &value))
}
enum PreparedInput {
None,
Cutover(Box<PreparedSystemCutover>),
InstallProxifyre {
proxifyre: Box<PrivilegedPackageLease>,
packet_filter: Box<PrivilegedPackageLease>,
vc_runtime: Box<PrivilegedPackageLease>,
},
UpdateProxifyre(Box<PrivilegedPackageLease>),
StartProxifyre(GeneratedConfigLease),
InstallSingBox {
runtime: Box<PrivilegedPackageLease>,
wrapper: Box<PrivilegedPackageLease>,
},
UpdateSingBox(Box<PrivilegedPackageLease>),
StartSingBox(GeneratedConfigLease),
}
impl PreparedInput {
fn package_fingerprint(
&self,
) -> Result<Option<PlannedPackageFingerprint>, 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<u8>,
sha256: String,
}
impl GeneratedConfigLease {
fn open(path: &Path, component: ManagedComponent) -> Result<Self, PrivilegedRunnerFailure> {
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<PrivilegedMutationResult, PrivilegedRunnerFailure> {
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<crate::privileged_jobs::ActivationAcknowledgement> {
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<PathBuf, PrivilegedRunnerFailure> {
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<CutoverExternalMutationStatus> {
installed_app_root()
.ok()
.map(|app_root| verify_cutover_external_mutation_status(&app_root))
}
fn read_local_cutover_observation(
storage_paths: &StoragePaths,
) -> Option<ComponentCutoverObservation> {
JsonStorage::new(storage_paths.root.clone())
.read_component_cutover_observation()
.ok()
.flatten()
}
fn read_local_cutover_user_evidence(
storage_paths: &StoragePaths,
) -> Result<ComponentCutoverUserEvidence, PrivilegedRunnerFailure> {
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<LegacyServiceState>,
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<PrivilegedCutoverContext, PrivilegedRunnerFailure> {
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<PrivilegedCutoverContext, PrivilegedRunnerFailure> {
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<PrivilegedCutoverContext, PrivilegedRunnerFailure> {
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<LegacyServiceState, PrivilegedRunnerFailure> {
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<String> {
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<String, PrivilegedRunnerFailure> {
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<PlannedPackageFingerprint, PrivilegedRunnerFailure> {
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<StrictProxifyreProxy>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct StrictProxifyreProxy {
app_names: Vec<String>,
socks5_proxy_endpoint: String,
supported_protocols: Vec<String>,
}
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::<std::net::Ipv6Addr>().is_ok()
} else {
!host.contains([':', '[', ']']) && safe_nonempty_text(host)
};
valid_host
&& port.bytes().all(|byte| byte.is_ascii_digit())
&& port.parse::<u16>().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<String, Value>, ()> {
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<String, Value>, keys: &[&str]) -> Result<(), ()> {
if object.keys().any(|key| !keys.contains(&key.as_str())) {
return Err(());
}
Ok(())
}
fn require_text(object: &Map<String, Value>, 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))
}