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![ (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![ (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(¤t_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::(&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 { 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 { 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 { 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::>(); 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 { self.calls.fetch_add(1, Ordering::SeqCst); Err(UpdateTransportError::RequestFailed) } } struct RejectingSignatureVerifier; impl PackageSignatureVerifier for RejectingSignatureVerifier { fn verify(&self, _path: &Path) -> Result { Err(SignatureVerifierError) } } #[cfg(windows)] struct ExactMetadataTransport { body: Vec, calls: AtomicUsize, } #[cfg(windows)] impl ExactMetadataTransport { fn new(body: Vec) -> Self { Self { body, calls: AtomicUsize::new(0), } } } #[cfg(windows)] impl UpdateTransport for ExactMetadataTransport { fn get( &self, request: &UpdateTransportRequest, ) -> Result { 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); } } }