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

1326 lines
48 KiB
Rust

use proxywarden_lib::adapters::singbox::{
SingBoxAdapter, SingBoxCheckResult, SingBoxConfigChecker, SingBoxConfigError,
SingBoxGenerationRequest,
};
use proxywarden_lib::component_catalog::ComponentId as CatalogComponentId;
use proxywarden_lib::component_cutover::{
ComponentCutoverObservation, ComponentCutoverUserEvidence, CutoverDisplayState,
CutoverExecutionMode, CutoverHostError, CutoverPhase, LegacyServiceState, StateFingerprint,
CUTOVER_OBSERVATION_SCHEMA_VERSION, CUTOVER_USER_EVIDENCE_SCHEMA_VERSION,
};
use proxywarden_lib::component_inventory::{
component_inventory_fingerprint_for_cutover, CandidateRole, ComponentCandidate,
ComponentClassification, ComponentInventory, MarkerEvidence, ServiceEvidence,
};
use proxywarden_lib::component_packages::{
ComponentPackageService, ComponentUpdateObservation, ComponentUpdatesState,
GithubReleaseDigestProof, PackageCacheManifest, PackageSignatureVerifier,
PrivilegedBundleVerificationError, PrivilegedBundleVerifier, PrivilegedCachedUpdatePlan,
SignaturePublisher, SignatureVerifierError, TrustedGithubReleaseObservation, UpdateRequestKind,
UpdateTransport, UpdateTransportError, UpdateTransportRequest, UpdateTransportResponse,
COMPONENT_UPDATES_STATE_SCHEMA_VERSION, PACKAGE_CACHE_MANIFEST_FILENAME,
PACKAGE_CACHE_MANIFEST_SCHEMA_VERSION,
};
use proxywarden_lib::models::{LocalSingBoxConfig, SubscriptionCache};
use proxywarden_lib::privileged_jobs::{
cutover_recovery_probe_fingerprint, ManagedComponent, PrivilegedAction, PrivilegedActionRunner,
PrivilegedCutoverMode, PrivilegedPlanResolver, PrivilegedRunnerFailure,
};
use proxywarden_lib::privileged_runtime::{
classify_cutover_runtime_config_for_tests, dispatch_candidate_observation_for_tests,
shared_service_name_path_ownership_for_tests,
stopped_cutover_route_lifecycle_exception_for_tests, validate_generated_config_bytes_for_tests,
LocalPrivilegedPlanResolver, SystemPrivilegedRuntime,
};
use proxywarden_lib::proxifyre_runtime::{ProxifyreNativeHostError, RuntimeConfigVerification};
use proxywarden_lib::safe_fs;
use proxywarden_lib::storage::StoragePaths;
use proxywarden_lib::subscription::parse_subscription_body;
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use std::fs;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use uuid::Uuid;
#[test]
fn current_verification_distinguishes_exact_config_mismatch_from_ambiguous_read_failure() {
assert_eq!(
classify_cutover_runtime_config_for_tests(Ok(RuntimeConfigVerification::Match)),
Ok(())
);
assert_eq!(
classify_cutover_runtime_config_for_tests(Ok(RuntimeConfigVerification::Mismatch)),
Err(CutoverHostError::VerificationFailed),
"successfully read mismatching bytes may trigger the sealed rollback path"
);
assert_eq!(
classify_cutover_runtime_config_for_tests(Err(ProxifyreNativeHostError)),
Err(CutoverHostError::ObservationFailed),
"ACL, open, read, or revalidation failures remain ambiguous and must not trigger rollback"
);
}
#[test]
fn restart_recovery_dispatches_pending_candidate_observation_without_live_package_writer() {
let live_calls = AtomicUsize::new(0);
let recovered_calls = AtomicUsize::new(0);
let expected_fingerprint = StateFingerprint::digest("test:recovered-candidate", b"expected");
let expected_identity = safe_fs::StableObjectIdentity {
volume_serial_number: 7,
file_id: 11,
kind: safe_fs::StableObjectKind::RegularFile,
size: 13,
};
let observed = dispatch_candidate_observation_for_tests(
false,
|| {
live_calls.fetch_add(1, Ordering::SeqCst);
Err(CutoverHostError::ObservationFailed)
},
|| {
recovered_calls.fetch_add(1, Ordering::SeqCst);
Ok((
expected_fingerprint.clone(),
Some(expected_identity.clone()),
))
},
)
.expect("package-free recovery observation");
assert_eq!(live_calls.load(Ordering::SeqCst), 0);
assert_eq!(recovered_calls.load(Ordering::SeqCst), 1);
assert_eq!(observed.0, expected_fingerprint);
assert_eq!(observed.1, Some(expected_identity));
}
#[test]
fn shared_service_name_is_owned_by_exact_binary_path_not_scm_record_existence() {
assert_eq!(
shared_service_name_path_ownership_for_tests(true, false),
(true, false),
"the legacy binary owns the shared SCM name before cutover and after rollback"
);
assert_eq!(
shared_service_name_path_ownership_for_tests(false, true),
(false, true),
"the current binary owns the same SCM name after cutover"
);
assert_eq!(
shared_service_name_path_ownership_for_tests(false, false),
(false, false),
"a missing or foreign path is not silently assigned to either owner"
);
assert_eq!(
shared_service_name_path_ownership_for_tests(true, true),
(false, false),
"an ambiguous dual match is never authorized"
);
}
#[test]
fn fresh_bundled_plans_are_exact_ordered_and_make_zero_transport_calls() {
let workspace = TestDirectory::new();
let paths = StoragePaths::new(workspace.path().join("storage"));
let packages = package_service(&paths);
let transport = Arc::new(CountingTransport::default());
let bundle = Arc::new(CountingBundleVerifier::default());
let runtime = SystemPrivilegedRuntime::from_parts_for_tests(
Some(packages),
paths,
workspace.path().join("staging"),
bundle.clone(),
Some(transport.clone()),
Arc::new(RejectingSignatureVerifier),
);
let proxifyre = runtime
.resolve(PrivilegedAction::InstallProxifyre)
.expect("fresh ProxiFyre plan");
let proxifyre_package = proxifyre.package.expect("ProxiFyre package");
assert_eq!(proxifyre_package.version, "2.4.0");
assert_eq!(
proxifyre_package
.assets
.iter()
.map(|asset| (asset.component_id, asset.version.as_str()))
.collect::<Vec<_>>(),
vec![
(CatalogComponentId::Proxifyre, "2.4.0"),
(CatalogComponentId::WindowsPacketFilter, "3.6.2"),
(CatalogComponentId::VcRuntime, "14.51.36247.0"),
]
);
let singbox = runtime
.resolve(PrivilegedAction::InstallSingBox)
.expect("fresh sing-box plan");
let singbox_package = singbox.package.expect("sing-box package");
assert_eq!(singbox_package.version, "1.13.19");
assert_eq!(
singbox_package
.assets
.iter()
.map(|asset| (asset.component_id, asset.version.as_str()))
.collect::<Vec<_>>(),
vec![
(CatalogComponentId::SingBox, "1.13.19"),
(CatalogComponentId::Winsw, "2.12.0"),
]
);
assert_eq!(transport.calls.load(Ordering::SeqCst), 0);
assert_eq!(bundle.calls.load(Ordering::SeqCst), 5);
}
#[test]
fn local_resolver_is_stateless_and_rebuilds_the_same_plan() {
let workspace = TestDirectory::new();
let paths = StoragePaths::new(workspace.path().join("storage"));
let bundle = Arc::new(CountingBundleVerifier::default());
let resolver = LocalPrivilegedPlanResolver::from_parts_for_tests(
Some(package_service(&paths)),
paths,
bundle.clone(),
);
let first = resolver
.resolve(PrivilegedAction::InstallProxifyre)
.expect("first resolve");
let second = resolver
.resolve(PrivilegedAction::InstallProxifyre)
.expect("second resolve");
assert_eq!(first, second);
assert_eq!(bundle.calls.load(Ordering::SeqCst), 6);
}
#[test]
fn deleted_or_corrupt_ux_cache_converges_through_new_hint_then_asset_free_recovery() {
let workspace = TestDirectory::new();
let paths = StoragePaths::new(workspace.path().join("storage"));
let bundle = Arc::new(CountingBundleVerifier::default());
let resolver = LocalPrivilegedPlanResolver::from_parts_for_tests_with_startup_session(
Some(package_service(&paths)),
paths.clone(),
bundle.clone(),
"11111111-1111-4111-8111-111111111111",
);
let missing_inventory = ComponentInventory {
component_id: proxywarden_lib::models::ComponentId::Proxyfier,
candidates: Vec::new(),
selected: None,
issues: Vec::new(),
};
let missing_cache = resolver
.resolve_cutover_action_with_inventory_for_tests(
PrivilegedAction::CutoverProxifyre,
missing_inventory.clone(),
)
.expect("missing cache emits a side-effect-free new hint");
assert_eq!(
missing_cache
.cutover_context
.as_ref()
.expect("cutover context")
.mode,
PrivilegedCutoverMode::New
);
assert_eq!(
missing_cache
.package
.as_ref()
.expect("new hint exact bundle")
.assets
.len(),
3
);
assert!(!paths
.generated_dir
.join("proxifyre-app-config.json")
.exists());
fs::create_dir_all(&paths.state_dir).expect("state directory");
fs::write(&paths.component_cutover_observation_file, b"{").expect("corrupt UX observation");
let corrupt_cache = resolver
.resolve_cutover_action_with_inventory_for_tests(
PrivilegedAction::CutoverProxifyre,
missing_inventory.clone(),
)
.expect("corrupt cache emits the same safe new hint");
assert_eq!(missing_cache, corrupt_cache);
let cutover_id = "22222222-2222-4222-8222-222222222222";
let observation = ComponentCutoverObservation {
schema_version: CUTOVER_OBSERVATION_SCHEMA_VERSION,
component: "proxifyre".to_string(),
cutover_id: cutover_id.to_string(),
state: CutoverDisplayState::AwaitingNextStart,
phase: CutoverPhase::LegacyQuarantined,
original_service_state: LegacyServiceState::Stopped,
legacy_version: "2.2.1".to_string(),
bundled_version: "2.4.0".to_string(),
operation_fingerprint: "a".repeat(64),
transaction_fingerprint: "b".repeat(64),
evidence_fingerprint: None,
next_start_verified: false,
route_smoke_confirmed: false,
legacy_path_label: "legacy-proxifyre".to_string(),
current_path_label: "current-proxifyre".to_string(),
can_recover: false,
can_cleanup: false,
disabled_code: None,
updated_at_epoch_seconds: 10,
};
fs::write(
&paths.component_cutover_observation_file,
serde_json::to_vec(&observation).expect("encode observation"),
)
.expect("restored redacted observation");
let recovery = resolver
.resolve_cutover_action_with_inventory_for_tests(
PrivilegedAction::CutoverProxifyre,
missing_inventory,
)
.expect("recovery plan");
assert!(recovery.package.is_none());
assert_eq!(
recovery
.cutover_context
.as_ref()
.expect("recovery context")
.mode,
PrivilegedCutoverMode::Recovery
);
assert_eq!(bundle.calls.load(Ordering::SeqCst), 6);
}
#[test]
fn non_absent_cutover_uses_canonical_probe_without_trusting_local_observation() {
for cache_case in ["missing", "corrupt", "stale-valid"] {
let workspace = TestDirectory::new();
let paths = StoragePaths::new(workspace.path().join("storage"));
fs::create_dir_all(&paths.state_dir).expect("state directory");
match cache_case {
"missing" => {}
"corrupt" => {
fs::write(&paths.component_cutover_observation_file, b"{")
.expect("corrupt observation");
}
"stale-valid" => {
let observation = ComponentCutoverObservation {
schema_version: CUTOVER_OBSERVATION_SCHEMA_VERSION,
component: "proxifyre".to_string(),
cutover_id: "22222222-2222-4222-8222-222222222222".to_string(),
state: CutoverDisplayState::Complete,
phase: CutoverPhase::ComponentComplete,
original_service_state: LegacyServiceState::Stopped,
legacy_version: "2.2.1".to_string(),
bundled_version: "2.4.0".to_string(),
operation_fingerprint: "a".repeat(64),
transaction_fingerprint: "b".repeat(64),
evidence_fingerprint: None,
next_start_verified: false,
route_smoke_confirmed: false,
legacy_path_label: "legacy-proxifyre".to_string(),
current_path_label: "current-proxifyre".to_string(),
can_recover: false,
can_cleanup: false,
disabled_code: None,
updated_at_epoch_seconds: 10,
};
fs::write(
&paths.component_cutover_observation_file,
serde_json::to_vec(&observation).expect("encode observation"),
)
.expect("stale terminal observation");
}
_ => unreachable!(),
}
let bundle = Arc::new(CountingBundleVerifier::default());
let resolver = LocalPrivilegedPlanResolver::from_parts_for_tests_with_cutover_app_root(
Some(package_service(&paths)),
paths,
bundle.clone(),
"11111111-1111-4111-8111-111111111111",
workspace.path().join("untrusted-test-app-root"),
);
let inventory = ComponentInventory {
component_id: proxywarden_lib::models::ComponentId::Proxyfier,
candidates: Vec::new(),
selected: None,
issues: Vec::new(),
};
let inventory_fingerprint = component_inventory_fingerprint_for_cutover(&inventory);
let plan = resolver
.resolve_cutover_action_with_inventory_for_tests(
PrivilegedAction::CutoverProxifyre,
inventory,
)
.expect("canonical sealed recovery probe");
let context = plan.cutover_context.expect("probe context");
assert_eq!(context.mode, PrivilegedCutoverMode::Recovery);
assert_eq!(context.cutover_id, None);
assert_eq!(context.initiating_startup_session_id, None);
assert_eq!(context.user_evidence, None);
assert_eq!(
context.evidence_fingerprint,
cutover_recovery_probe_fingerprint(&inventory_fingerprint)
);
assert!(plan.package.is_none());
assert_eq!(bundle.calls.load(Ordering::SeqCst), 0);
}
}
#[test]
fn non_start_actions_do_not_require_generated_config_and_are_domain_separated() {
let workspace = TestDirectory::new();
let paths = StoragePaths::new(workspace.path().join("storage"));
let resolver = LocalPrivilegedPlanResolver::from_parts_for_tests(
Some(package_service(&paths)),
paths,
Arc::new(CountingBundleVerifier::default()),
);
let stop = resolver
.resolve(PrivilegedAction::StopProxifyre)
.expect("stop plan without generated config");
let uninstall = resolver
.resolve(PrivilegedAction::UninstallProxifyre)
.expect("uninstall plan without generated config");
assert_ne!(
stop.configuration_fingerprint,
uninstall.configuration_fingerprint
);
assert_eq!(
resolver.resolve(PrivilegedAction::StartProxifyre),
Err(PrivilegedRunnerFailure::PreconditionFailed)
);
}
#[cfg(windows)]
#[test]
fn missing_or_corrupt_catalog_only_blocks_package_actions() {
let workspace = TestDirectory::new();
let missing_root = workspace.path().join("missing-components");
let corrupt_root = workspace.path().join("corrupt-components");
fs::create_dir(&corrupt_root).expect("create corrupt bundle root");
fs::write(corrupt_root.join("catalog.json"), b"{").expect("write corrupt catalog");
for (case, bundle_root) in [("missing", missing_root), ("corrupt", corrupt_root)] {
let paths = StoragePaths::new(workspace.path().join(format!("storage-{case}")));
assert!(
ComponentPackageService::open(&bundle_root, &paths).is_err(),
"fixture catalog must be unavailable"
);
safe_fs::write_restricted_atomic(
&paths.generated_dir.join("proxifyre-app-config.json"),
&proxifyre_config_bytes(),
)
.expect("restricted generated source");
let bundle = Arc::new(CountingBundleVerifier::default());
let local =
LocalPrivilegedPlanResolver::from_parts_for_tests(None, paths.clone(), bundle.clone());
let system = SystemPrivilegedRuntime::from_parts_for_tests(
None,
paths,
workspace
.path()
.join(format!("app-{case}"))
.join(".proxywarden-package-staging"),
bundle.clone(),
None,
Arc::new(RejectingSignatureVerifier),
);
for action in [
PrivilegedAction::StartProxifyre,
PrivilegedAction::StopProxifyre,
PrivilegedAction::UninstallSingBox,
PrivilegedAction::ConfigureProxifyreFirewall,
] {
assert!(local.resolve(action).is_ok(), "local {case}: {action:?}");
assert!(system.resolve(action).is_ok(), "system {case}: {action:?}");
}
for action in [
PrivilegedAction::InstallProxifyre,
PrivilegedAction::InstallSingBox,
PrivilegedAction::UpdateProxifyre,
PrivilegedAction::UpdateSingBox,
] {
assert_eq!(
local.resolve(action),
Err(PrivilegedRunnerFailure::PackageVerificationFailed),
"local {case}: {action:?}"
);
assert_eq!(
system.resolve(action),
Err(PrivilegedRunnerFailure::PackageVerificationFailed),
"system {case}: {action:?}"
);
}
assert_eq!(bundle.calls.load(Ordering::SeqCst), 0);
}
}
#[test]
fn pending_action_is_consumed_on_plan_mismatch_without_transport_or_host_work() {
let workspace = TestDirectory::new();
let paths = StoragePaths::new(workspace.path().join("storage"));
let transport = Arc::new(CountingTransport::default());
let runtime = SystemPrivilegedRuntime::from_parts_for_tests(
Some(package_service(&paths)),
paths,
workspace.path().join("staging"),
Arc::new(CountingBundleVerifier::default()),
Some(transport.clone()),
Arc::new(RejectingSignatureVerifier),
);
let plan = runtime
.resolve(PrivilegedAction::StopProxifyre)
.expect("prepare stop");
let mut changed = plan.clone();
changed.inventory_fingerprint = "f".repeat(64);
assert_eq!(
runtime.run(PrivilegedAction::StopProxifyre, &changed),
Err(PrivilegedRunnerFailure::PreconditionFailed)
);
assert_eq!(
runtime.run(PrivilegedAction::StopProxifyre, &plan),
Err(PrivilegedRunnerFailure::PreconditionFailed)
);
assert_eq!(transport.calls.load(Ordering::SeqCst), 0);
}
#[test]
fn stopped_cutover_allows_only_legacy_quarantined_start_stop_route_window() {
let config = "a".repeat(64);
for action in [
PrivilegedAction::StartProxifyre,
PrivilegedAction::StopProxifyre,
] {
assert_eq!(
stopped_cutover_route_lifecycle_exception_for_tests(
action,
CutoverPhase::LegacyQuarantined,
CutoverExecutionMode::AwaitingUser,
LegacyServiceState::Stopped,
&config,
),
Some(config.clone())
);
}
for (action, phase, mode, original_state) in [
(
PrivilegedAction::InstallProxifyre,
CutoverPhase::LegacyQuarantined,
CutoverExecutionMode::AwaitingUser,
LegacyServiceState::Stopped,
),
(
PrivilegedAction::StartProxifyre,
CutoverPhase::LegacyQuarantined,
CutoverExecutionMode::AwaitingUser,
LegacyServiceState::Running,
),
(
PrivilegedAction::StartProxifyre,
CutoverPhase::NextStartVerified,
CutoverExecutionMode::AwaitingUser,
LegacyServiceState::Stopped,
),
(
PrivilegedAction::StopProxifyre,
CutoverPhase::LegacyQuarantined,
CutoverExecutionMode::RollingBack,
LegacyServiceState::Stopped,
),
] {
assert_eq!(
stopped_cutover_route_lifecycle_exception_for_tests(
action,
phase,
mode,
original_state,
&config,
),
None,
"unexpected lifecycle exception for {action:?}/{phase:?}/{mode:?}/{original_state:?}"
);
}
}
#[test]
fn cleanup_plan_requires_user_evidence_from_the_same_normal_app_session() {
let workspace = TestDirectory::new();
let paths = StoragePaths::new(workspace.path().join("storage"));
fs::create_dir_all(&paths.state_dir).expect("state directory");
let current_inventory = managed_current_proxifyre_inventory();
let current_inventory_fingerprint =
component_inventory_fingerprint_for_cutover(&current_inventory);
let cutover_id = "22222222-2222-4222-8222-222222222222";
let observation = ComponentCutoverObservation {
schema_version: CUTOVER_OBSERVATION_SCHEMA_VERSION,
component: "proxifyre".to_string(),
cutover_id: cutover_id.to_string(),
state: CutoverDisplayState::AwaitingNextStart,
phase: CutoverPhase::LegacyQuarantined,
original_service_state: LegacyServiceState::Stopped,
legacy_version: "2.2.1".to_string(),
bundled_version: "2.4.0".to_string(),
operation_fingerprint: "a".repeat(64),
transaction_fingerprint: "b".repeat(64),
evidence_fingerprint: None,
next_start_verified: false,
route_smoke_confirmed: false,
legacy_path_label: "legacy-proxifyre".to_string(),
current_path_label: "current-proxifyre".to_string(),
can_recover: false,
can_cleanup: false,
disabled_code: None,
updated_at_epoch_seconds: 10,
};
fs::write(
&paths.component_cutover_observation_file,
serde_json::to_vec(&observation).expect("encode observation"),
)
.expect("write observation");
let expected_session = "11111111-1111-4111-8111-111111111111";
let resolver = LocalPrivilegedPlanResolver::from_parts_for_tests_with_startup_session(
None,
paths.clone(),
Arc::new(CountingBundleVerifier::default()),
expected_session,
);
let mut evidence = ComponentCutoverUserEvidence {
schema_version: CUTOVER_USER_EVIDENCE_SCHEMA_VERSION,
cutover_id: cutover_id.to_string(),
startup_session_id: "33333333-3333-4333-8333-333333333333".to_string(),
current_inventory_fingerprint,
route_smoke_confirmed: true,
observed_at_epoch_seconds: 11,
confirmed_at_epoch_seconds: Some(12),
};
fs::write(
&paths.component_cutover_user_evidence_file,
serde_json::to_vec(&evidence).expect("encode foreign-session evidence"),
)
.expect("write foreign-session evidence");
assert_eq!(
resolver.resolve_cutover_action_with_inventory_for_tests(
PrivilegedAction::CleanupProxifyreQuarantine,
current_inventory.clone(),
),
Err(PrivilegedRunnerFailure::CutoverStateConflict)
);
evidence.startup_session_id = expected_session.to_string();
fs::write(
&paths.component_cutover_user_evidence_file,
serde_json::to_vec(&evidence).expect("encode same-session evidence"),
)
.expect("write same-session evidence");
let cleanup = resolver
.resolve_cutover_action_with_inventory_for_tests(
PrivilegedAction::CleanupProxifyreQuarantine,
current_inventory,
)
.expect("same-session cleanup plan");
assert!(cleanup.package.is_none());
assert_eq!(
cleanup
.cutover_context
.as_ref()
.expect("cleanup context")
.mode,
PrivilegedCutoverMode::Cleanup
);
}
#[test]
fn generated_config_rejects_weak_acl_before_schema_or_runner() {
let workspace = TestDirectory::new();
let paths = StoragePaths::new(workspace.path().join("storage"));
fs::create_dir_all(&paths.generated_dir).expect("generated directory");
fs::write(
paths.generated_dir.join("proxifyre-app-config.json"),
proxifyre_config_bytes(),
)
.expect("ordinary file");
let resolver = LocalPrivilegedPlanResolver::from_parts_for_tests(
Some(package_service(&paths)),
paths,
Arc::new(CountingBundleVerifier::default()),
);
assert_eq!(
resolver.resolve(PrivilegedAction::StartProxifyre),
Err(PrivilegedRunnerFailure::PreconditionFailed)
);
}
#[cfg(windows)]
#[test]
fn elevated_resolve_holds_exact_generated_source_and_mismatch_drops_it() {
let workspace = TestDirectory::new();
let paths = StoragePaths::new(workspace.path().join("storage"));
let generated = paths.generated_dir.join("proxifyre-app-config.json");
safe_fs::write_restricted_atomic(&generated, &proxifyre_config_bytes())
.expect("restricted generated source");
let runtime = SystemPrivilegedRuntime::from_parts_for_tests(
Some(package_service(&paths)),
paths,
workspace.path().join("staging"),
Arc::new(CountingBundleVerifier::default()),
Some(Arc::new(CountingTransport::default())),
Arc::new(RejectingSignatureVerifier),
);
let plan = runtime
.resolve(PrivilegedAction::StartProxifyre)
.expect("leased start plan");
assert!(
fs::write(&generated, proxifyre_config_bytes()).is_err(),
"deny-share-write lease must block source replacement"
);
let mut changed = plan.clone();
changed.configuration_fingerprint = "f".repeat(64);
assert_eq!(
runtime.run(PrivilegedAction::StartProxifyre, &changed),
Err(PrivilegedRunnerFailure::PreconditionFailed)
);
fs::write(&generated, proxifyre_config_bytes()).expect("lease released after mismatch");
}
#[test]
fn strict_generated_schema_rejects_unknown_and_dangerous_fields() {
let proxifyre = serde_json::from_slice::<Value>(&proxifyre_config_bytes()).expect("ProxiFyre");
assert!(validate_generated_config_bytes_for_tests(
ManagedComponent::Proxifyre,
&serde_json::to_vec(&proxifyre).expect("encode")
));
let mut unknown_root = proxifyre.clone();
unknown_root["command"] = json!("calc.exe");
assert!(!validate_generated_config_bytes_for_tests(
ManagedComponent::Proxifyre,
&serde_json::to_vec(&unknown_root).expect("encode")
));
let mut unknown_proxy = proxifyre;
unknown_proxy["proxies"][0]["password"] = json!("must-not-pass");
assert!(!validate_generated_config_bytes_for_tests(
ManagedComponent::Proxifyre,
&serde_json::to_vec(&unknown_proxy).expect("encode")
));
let singbox = singbox_config_value();
assert!(validate_generated_config_bytes_for_tests(
ManagedComponent::SingBox,
&serde_json::to_vec(&singbox).expect("encode")
));
let mut unknown_outbound = singbox.clone();
unknown_outbound["outbounds"][0]["executable"] = json!("foreign.exe");
assert!(!validate_generated_config_bytes_for_tests(
ManagedComponent::SingBox,
&serde_json::to_vec(&unknown_outbound).expect("encode")
));
let mut path_option = singbox;
path_option["outbounds"][0]["tls"]["certificate_path"] = json!("C:\\foreign.pem");
assert!(!validate_generated_config_bytes_for_tests(
ManagedComponent::SingBox,
&serde_json::to_vec(&path_option).expect("encode")
));
}
#[test]
fn production_subscription_adapter_outputs_are_authorized_for_all_supported_types() {
let bodies = [
json!({ "outbounds": [{
"type": "vless", "tag": "vless", "server": "vless.example.test",
"server_port": 443, "uuid": "11111111-1111-1111-1111-111111111111",
"flow": "", "packet_encoding": "xudp", "tls": { "enabled": true }
}]}),
json!({ "outbounds": [{
"type": "vmess", "tag": "vmess", "server": "vmess.example.test",
"server_port": 443, "uuid": "22222222-2222-2222-2222-222222222222",
"security": "auto", "tls": { "enabled": true },
"transport": { "type": "ws" }
}]}),
json!({ "outbounds": [{
"type": "trojan", "tag": "trojan", "server": "trojan.example.test",
"server_port": 443, "password": "trojan-secret", "tls": { "enabled": true }
}]}),
json!({ "outbounds": [{
"type": "shadowsocks", "tag": "shadowsocks", "server": "ss.example.test",
"server_port": 8388, "method": "aes-256-gcm", "password": "ss-secret"
}]}),
json!({ "outbounds": [{
"type": "hysteria2", "tag": "hysteria2", "server": "hy2.example.test",
"server_port": 443, "password": "hy2-secret",
"obfs": { "type": "salamander", "password": "obfs-secret" },
"tls": { "enabled": true }
}]}),
];
for body in bodies {
let generated = generated_from_subscription(&body.to_string());
assert!(
validate_generated_config_bytes_for_tests(
ManagedComponent::SingBox,
&serde_json::to_vec(&generated).expect("encode generated config")
),
"production adapter output was rejected: {generated}"
);
}
}
#[test]
fn raw_vless_and_trojan_ws_tls_without_server_name_are_authorized() {
for kind in ["vless", "trojan"] {
let credential = if kind == "vless" {
json!({ "uuid": "33333333-3333-3333-3333-333333333333" })
} else {
json!({ "password": "trojan-secret" })
};
let mut outbound = json!({
"type": kind,
"tag": format!("{kind}-ws"),
"server": format!("{kind}.example.test"),
"server_port": 443,
"tls": { "enabled": true },
"transport": {
"type": "ws",
"path": "/socket",
"headers": { "Host": "front.example.test" }
}
});
outbound
.as_object_mut()
.expect("outbound object")
.extend(credential.as_object().expect("credential object").clone());
let generated =
generated_from_subscription(&json!({ "outbounds": [outbound] }).to_string());
assert!(validate_generated_config_bytes_for_tests(
ManagedComponent::SingBox,
&serde_json::to_vec(&generated).expect("encode generated config")
));
}
}
#[test]
fn singbox_authorization_rejects_dangerous_passthrough_and_unsupported_shapes() {
let base = generated_from_subscription(
&json!({ "outbounds": [{
"type": "vless", "tag": "vless-ws", "server": "vless.example.test",
"server_port": 443, "uuid": "44444444-4444-4444-4444-444444444444",
"tls": { "enabled": true },
"transport": { "type": "ws" }
}]})
.to_string(),
);
let mut rejected = Vec::new();
for key in [
"command",
"service",
"executable",
"plugin",
"multiplex",
"detour",
"certificate_path",
"config_path",
] {
let mut value = base.clone();
value["outbounds"][0][key] = json!(r"C:\foreign\payload.exe");
rejected.push((key, value));
}
let mut unsupported_transport = base.clone();
unsupported_transport["outbounds"][0]["transport"]["type"] = json!("grpc");
rejected.push(("non-ws transport", unsupported_transport));
let mut foreign_header = base.clone();
foreign_header["outbounds"][0]["transport"]["headers"] = json!({ "Authorization": "secret" });
rejected.push(("foreign WS header", foreign_header));
let mut control_path = base.clone();
control_path["outbounds"][0]["transport"]["path"] = json!("/ws\ncontrol");
rejected.push(("control character in WS path", control_path));
let mut control_host = base.clone();
control_host["outbounds"][0]["transport"]["headers"] =
json!({ "Host": "front.example.test\r\nInjected: value" });
rejected.push(("control character in Host", control_host));
let mut shadowsocks_transport = generated_from_subscription(
&json!({ "outbounds": [{
"type": "shadowsocks", "tag": "ss", "server": "ss.example.test",
"server_port": 8388, "method": "aes-256-gcm", "password": "secret"
}]})
.to_string(),
);
shadowsocks_transport["outbounds"][0]["transport"] = json!({ "type": "ws" });
rejected.push(("transport on shadowsocks", shadowsocks_transport));
let mut wrong_obfs = generated_from_subscription(
&json!({ "outbounds": [{
"type": "hysteria2", "tag": "hy2", "server": "hy2.example.test",
"server_port": 443, "password": "secret",
"obfs": { "type": "other", "password": "obfs-secret" }
}]})
.to_string(),
);
wrong_obfs["outbounds"][0]["obfs"]["type"] = json!("other");
rejected.push(("non-salamander obfs", wrong_obfs));
for (label, value) in rejected {
assert!(
!validate_generated_config_bytes_for_tests(
ManagedComponent::SingBox,
&serde_json::to_vec(&value).expect("encode rejected config")
),
"dangerous shape was authorized: {label}"
);
}
}
#[cfg(windows)]
#[test]
fn cached_update_resolve_holds_exact_origin_and_locked_staging_until_one_shot_run() {
let workspace = TestDirectory::new();
let paths = StoragePaths::new(workspace.path().join("storage"));
let bytes = b"synthetic cached sing-box update";
let cached = seed_singbox_cache(&paths, bytes);
let transport = Arc::new(ExactMetadataTransport::new(metadata_for(&cached)));
let staging_parent = workspace
.path()
.join("app")
.join(".proxywarden-package-staging");
fs::create_dir_all(staging_parent.parent().expect("staging app root"))
.expect("create staging app root");
let packages = package_service(&paths);
assert_eq!(
packages
.plan_cached_update(CatalogComponentId::SingBox)
.expect("select trusted cached update"),
cached
);
let runtime = SystemPrivilegedRuntime::from_parts_for_tests(
Some(packages),
paths,
staging_parent.clone(),
Arc::new(CountingBundleVerifier::default()),
Some(transport.clone()),
Arc::new(RejectingSignatureVerifier),
);
let plan = runtime
.resolve(PrivilegedAction::UpdateSingBox)
.expect("prepare cached update");
assert_eq!(transport.calls.load(Ordering::SeqCst), 1);
let package = plan.package.as_ref().expect("cached package fingerprint");
let origin = package
.origin_proof
.as_ref()
.expect("official origin proof");
assert_eq!(package.version, cached.version);
assert_eq!(origin.repository, cached.independent_proof.repository);
assert_eq!(origin.release_id, cached.independent_proof.release_id);
assert_eq!(origin.asset_id, cached.independent_proof.asset_id);
assert_eq!(
origin.sha256_from_api,
cached.independent_proof.sha256_from_api
);
let staged_directory = only_child(&staging_parent);
let staged_asset = staged_directory.join(&cached.independent_proof.asset_name);
safe_fs::verify_path_protected_for_owner_admin_system(&staged_directory)
.expect("sealed staged directory");
safe_fs::verify_path_protected_for_owner_admin_system(&staged_asset)
.expect("sealed staged asset");
assert!(
fs::write(&staged_asset, b"replacement").is_err(),
"pending package lease must deny source replacement"
);
let mut mismatch = plan.clone();
mismatch.inventory_fingerprint = "f".repeat(64);
assert_eq!(
runtime.run(PrivilegedAction::UpdateSingBox, &mismatch),
Err(PrivilegedRunnerFailure::PreconditionFailed)
);
assert!(
fs::read_dir(&staging_parent)
.expect("read staging parent")
.next()
.is_none(),
"mismatch must drop the lease and remove its staging directory"
);
assert_eq!(
runtime.run(PrivilegedAction::UpdateSingBox, &plan),
Err(PrivilegedRunnerFailure::PreconditionFailed)
);
assert_eq!(transport.calls.load(Ordering::SeqCst), 1);
}
fn package_service(paths: &StoragePaths) -> ComponentPackageService {
ComponentPackageService::open(bundled_root(), paths).expect("open bundled package service")
}
fn bundled_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("bundled/components")
}
fn proxifyre_config_bytes() -> Vec<u8> {
serde_json::to_vec(&json!({
"logLevel": "Info",
"bypassLan": true,
"proxies": [{
"appNames": ["browser.exe"],
"socks5ProxyEndpoint": "127.0.0.1:1080",
"supportedProtocols": ["TCP", "UDP"]
}]
}))
.expect("encode ProxiFyre config")
}
fn managed_current_proxifyre_inventory() -> ComponentInventory {
let root = PathBuf::from(r"C:\Program Files\ProxyWarden\components\ProxiFyre");
let executable = root.join("ProxiFyre.exe");
ComponentInventory {
component_id: proxywarden_lib::models::ComponentId::Proxyfier,
candidates: vec![ComponentCandidate {
component_id: proxywarden_lib::models::ComponentId::Proxyfier,
classification: ComponentClassification::ManagedCurrent,
role: CandidateRole::Current,
root,
executable_path: Some(executable.clone()),
binary_version: Some("2.4.0".to_string()),
service: Some(ServiceEvidence {
name: "ProxiFyreService".to_string(),
status: "Stopped".to_string(),
path_name: Some(executable.to_string_lossy().into_owned()),
executable_path: Some(executable),
path_matches_candidate: true,
binary_version: Some("2.4.0".to_string()),
}),
marker: MarkerEvidence::Valid,
issues: Vec::new(),
}],
selected: Some(0),
issues: Vec::new(),
}
}
fn singbox_config_value() -> Value {
json!({
"log": { "disabled": false, "level": "info", "timestamp": true },
"inbounds": [{
"type": "mixed",
"tag": "proxywarden-mixed-in",
"listen": "127.0.0.1",
"listen_port": 1080,
"users": [],
"set_system_proxy": false
}],
"outbounds": [
{
"type": "vless",
"tag": "vpn",
"server": "edge.example.test",
"server_port": 443,
"uuid": "11111111-1111-1111-1111-111111111111",
"flow": "xtls-rprx-vision",
"packet_encoding": "xudp",
"tls": {
"enabled": true,
"server_name": "edge.example.test",
"utls": { "enabled": true, "fingerprint": "chrome" },
"reality": {
"enabled": true,
"public_key": "fixture-public-key",
"short_id": "fixture-short-id"
}
}
},
{ "type": "direct", "tag": "direct" },
{ "type": "block", "tag": "block" }
],
"route": {
"rules": [{ "ip_is_private": true, "outbound": "direct" }],
"final": "vpn"
}
})
}
fn generated_from_subscription(body: &str) -> Value {
let parsed = parse_subscription_body(body).expect("parse subscription fixture");
let selected_server_id = parsed
.servers
.first()
.expect("supported subscription server")
.id
.clone();
let config = LocalSingBoxConfig {
selected_server_id: Some(selected_server_id),
..LocalSingBoxConfig::default()
};
let cache = SubscriptionCache {
config: parsed.config,
servers: parsed.servers,
user_info: Default::default(),
fetched_at: "test-fixture".to_string(),
};
let generated = SingBoxAdapter::default()
.generate_config(
SingBoxGenerationRequest::new(&config, &cache, None),
&NeverConfigChecker,
)
.expect("generate production sing-box config");
serde_json::from_str(&generated.contents).expect("decode generated sing-box config")
}
struct NeverConfigChecker;
impl SingBoxConfigChecker for NeverConfigChecker {
fn check_config(
&self,
_binary_path: &Path,
_config_json: &str,
) -> Result<SingBoxCheckResult, SingBoxConfigError> {
panic!("binary check is unreachable without a binary path")
}
}
#[cfg(windows)]
fn seed_singbox_cache(paths: &StoragePaths, bytes: &[u8]) -> PrivilegedCachedUpdatePlan {
let version = "1.14.0";
let asset_name = format!("sing-box-{version}-windows-amd64.zip");
let sha256 = format!("{:x}", Sha256::digest(bytes));
let proof = GithubReleaseDigestProof {
repository: "SagerNet/sing-box".to_string(),
release_id: 700,
asset_id: 701,
stable_tag: format!("v{version}"),
asset_name: asset_name.clone(),
size: bytes.len() as u64,
sha256_from_api: sha256.clone(),
verified_signatures: Vec::new(),
};
let plan = PrivilegedCachedUpdatePlan {
component_id: CatalogComponentId::SingBox,
version: version.to_string(),
independent_proof: proof.clone(),
};
let component_root = paths.packages_dir.join(plan.component_id.as_str());
let version_root = component_root.join(version);
fs::create_dir_all(&version_root).expect("create cached package directories");
let asset_path = version_root.join(&asset_name);
fs::write(&asset_path, bytes).expect("write cached package");
let manifest_path = version_root.join(PACKAGE_CACHE_MANIFEST_FILENAME);
let manifest = PackageCacheManifest {
schema_version: PACKAGE_CACHE_MANIFEST_SCHEMA_VERSION,
component_id: plan.component_id,
version: version.to_string(),
asset_name,
sha256,
size: bytes.len() as u64,
independent_proof: proof.clone(),
};
fs::write(
&manifest_path,
serde_json::to_vec_pretty(&manifest).expect("encode cache manifest"),
)
.expect("write cache manifest");
let trusted = TrustedGithubReleaseObservation {
repository: proof.repository.clone(),
release_id: proof.release_id,
asset_id: proof.asset_id,
stable_tag: proof.stable_tag.clone(),
asset_name: proof.asset_name.clone(),
size: proof.size,
sha256_from_api: proof.sha256_from_api.clone(),
};
let state = ComponentUpdatesState {
schema_version: COMPONENT_UPDATES_STATE_SCHEMA_VERSION,
observations: vec![ComponentUpdateObservation {
component_id: plan.component_id,
checked_at_unix: 1,
latest_known_version: version.to_string(),
trusted_releases: vec![trusted],
}],
};
fs::create_dir_all(&paths.state_dir).expect("create update state directory");
fs::write(
&paths.component_updates_file,
serde_json::to_vec_pretty(&state).expect("encode update state"),
)
.expect("write update state");
for path in [
paths.packages_dir.as_path(),
component_root.as_path(),
version_root.as_path(),
manifest_path.as_path(),
asset_path.as_path(),
paths.state_dir.as_path(),
paths.component_updates_file.as_path(),
] {
safe_fs::protect_path_for_owner_admin_system(path).expect("protect trusted cache fixture");
}
plan
}
#[cfg(windows)]
fn metadata_for(plan: &PrivilegedCachedUpdatePlan) -> Vec<u8> {
let proof = &plan.independent_proof;
serde_json::to_vec(&json!({
"id": proof.release_id,
"tag_name": proof.stable_tag,
"draft": false,
"prerelease": false,
"assets": [{
"id": proof.asset_id,
"name": proof.asset_name,
"size": proof.size,
"digest": format!("sha256:{}", proof.sha256_from_api),
"browser_download_url": format!(
"https://github.com/{}/releases/download/{}/{}",
proof.repository, proof.stable_tag, proof.asset_name
)
}]
}))
.expect("encode GitHub metadata")
}
#[cfg(windows)]
fn only_child(parent: &Path) -> PathBuf {
let children = fs::read_dir(parent)
.expect("read parent")
.map(|entry| entry.expect("read child").path())
.collect::<Vec<_>>();
assert_eq!(children.len(), 1, "expected one staged lease");
children.into_iter().next().expect("staged child")
}
#[derive(Default)]
struct CountingBundleVerifier {
calls: AtomicUsize,
}
impl PrivilegedBundleVerifier for CountingBundleVerifier {
fn verify(
&self,
bundled_root: &Path,
catalog_path: &Path,
asset_path: &Path,
) -> Result<(), PrivilegedBundleVerificationError> {
self.calls.fetch_add(1, Ordering::SeqCst);
if catalog_path == bundled_root.join("catalog.json") && asset_path.starts_with(bundled_root)
{
Ok(())
} else {
Err(PrivilegedBundleVerificationError)
}
}
}
#[derive(Default)]
struct CountingTransport {
calls: AtomicUsize,
}
impl UpdateTransport for CountingTransport {
fn get(
&self,
_request: &UpdateTransportRequest,
) -> Result<UpdateTransportResponse, UpdateTransportError> {
self.calls.fetch_add(1, Ordering::SeqCst);
Err(UpdateTransportError::RequestFailed)
}
}
struct RejectingSignatureVerifier;
impl PackageSignatureVerifier for RejectingSignatureVerifier {
fn verify(&self, _path: &Path) -> Result<SignaturePublisher, SignatureVerifierError> {
Err(SignatureVerifierError)
}
}
#[cfg(windows)]
struct ExactMetadataTransport {
body: Vec<u8>,
calls: AtomicUsize,
}
#[cfg(windows)]
impl ExactMetadataTransport {
fn new(body: Vec<u8>) -> Self {
Self {
body,
calls: AtomicUsize::new(0),
}
}
}
#[cfg(windows)]
impl UpdateTransport for ExactMetadataTransport {
fn get(
&self,
request: &UpdateTransportRequest,
) -> Result<UpdateTransportResponse, UpdateTransportError> {
self.calls.fetch_add(1, Ordering::SeqCst);
assert_eq!(request.kind, UpdateRequestKind::GithubReleaseMetadata);
assert_eq!(
request.url,
"https://api.github.com/repos/SagerNet/sing-box/releases/latest"
);
Ok(UpdateTransportResponse {
status: 200,
location: None,
content_length: Some(self.body.len() as u64),
body: Box::new(Cursor::new(self.body.clone())),
})
}
}
struct TestDirectory {
path: PathBuf,
target_root: PathBuf,
}
impl TestDirectory {
fn new() -> Self {
let target_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("target");
fs::create_dir_all(&target_root).expect("create Cargo target directory");
let target_root = fs::canonicalize(target_root).expect("canonical target directory");
let path = target_root.join(format!("privileged-runtime-tests-{}", Uuid::new_v4()));
fs::create_dir(&path).expect("create isolated test directory");
Self { path, target_root }
}
fn path(&self) -> &Path {
&self.path
}
}
impl Drop for TestDirectory {
fn drop(&mut self) {
let exact_parent = self.path.parent() == Some(self.target_root.as_path());
let valid_name = self
.path
.file_name()
.and_then(|name| name.to_str())
.and_then(|name| name.strip_prefix("privileged-runtime-tests-"))
.is_some_and(|id| Uuid::parse_str(id).is_ok());
if exact_parent
&& valid_name
&& self.path.is_absolute()
&& safe_fs::ensure_no_reparse_ancestors(&self.path).is_ok()
{
let _ = fs::remove_dir_all(&self.path);
}
}
}