use super::*; use crate::config::{UpstreamConfig, UpstreamType}; use crate::crypto::{AesCtr, SecureRandom}; use crate::protocol::constants::ProtoTag; use crate::proxy::route_mode::{RelayRouteMode, RouteRuntimeController}; use crate::stats::Stats; use crate::stream::{BufferPool, CryptoReader, CryptoWriter}; use crate::transport::UpstreamManager; use std::fs; use std::io::Write; use std::path::Path; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Duration; use tokio::io::duplex; use tokio::net::TcpListener; fn make_crypto_reader(reader: R) -> CryptoReader where R: tokio::io::AsyncRead + Unpin, { let key = [0u8; 32]; let iv = 0u128; CryptoReader::new(reader, AesCtr::new(&key, iv)) } fn make_crypto_writer(writer: W) -> CryptoWriter where W: tokio::io::AsyncWrite + Unpin, { let key = [0u8; 32]; let iv = 0u128; CryptoWriter::new(writer, AesCtr::new(&key, iv), 8 * 1024) } fn nonempty_line_count(text: &str) -> usize { text.lines().filter(|line| !line.trim().is_empty()).count() } #[test] fn unknown_dc_log_is_deduplicated_per_dc_idx() { let _guard = unknown_dc_test_lock() .lock() .expect("unknown dc test lock must be available"); clear_unknown_dc_log_cache_for_testing(); assert!(should_log_unknown_dc(777)); assert!( !should_log_unknown_dc(777), "same unknown dc_idx must not be logged repeatedly" ); assert!( should_log_unknown_dc(778), "different unknown dc_idx must still be loggable" ); } #[test] fn unknown_dc_log_respects_distinct_limit() { let _guard = unknown_dc_test_lock() .lock() .expect("unknown dc test lock must be available"); clear_unknown_dc_log_cache_for_testing(); for dc in 1..=UNKNOWN_DC_LOG_DISTINCT_LIMIT { assert!( should_log_unknown_dc(dc as i16), "expected first-time unknown dc_idx to be loggable" ); } assert!( !should_log_unknown_dc(i16::MAX), "distinct unknown dc_idx entries above limit must not be logged" ); } #[test] fn unknown_dc_log_fails_closed_when_dedup_lock_is_poisoned() { let poisoned = Arc::new(std::sync::Mutex::new(std::collections::HashSet::::new())); let poisoned_for_thread = poisoned.clone(); let _ = std::thread::spawn(move || { let _guard = poisoned_for_thread .lock() .expect("poison setup lock must be available"); panic!("intentional poison for fail-closed regression"); }) .join(); assert!( !should_log_unknown_dc_with_set(poisoned.as_ref(), 4242), "poisoned unknown-DC dedup lock must fail closed" ); } #[test] fn unsafe_unknown_dc_log_path_does_not_consume_dedup_slot() { let _guard = unknown_dc_test_lock() .lock() .expect("unknown dc test lock must be available"); clear_unknown_dc_log_cache_for_testing(); let dc_idx: i16 = 31_123; let mut cfg = ProxyConfig::default(); cfg.general.unknown_dc_file_log_enabled = true; cfg.general.unknown_dc_log_path = Some("../telemt-unknown-dc-unsafe.log".to_string()); let _ = get_dc_addr_static(dc_idx, &cfg).expect("fallback routing must still work"); assert!( should_log_unknown_dc(dc_idx), "rejected unsafe log path must not consume unknown-dc dedup entry" ); } #[test] fn stress_unknown_dc_log_concurrent_unique_churn_respects_cap() { let _guard = unknown_dc_test_lock() .lock() .expect("unknown dc test lock must be available"); clear_unknown_dc_log_cache_for_testing(); let accepted = Arc::new(AtomicUsize::new(0)); let mut workers = Vec::new(); // Adversarial model: many concurrent peers rotate dc_idx values rapidly. for worker in 0..16usize { let accepted = Arc::clone(&accepted); workers.push(std::thread::spawn(move || { let base = (worker * 2048) as i32; for offset in 0..512i32 { let raw = base + offset; let dc = (raw % i16::MAX as i32) as i16; if should_log_unknown_dc(dc) { accepted.fetch_add(1, Ordering::Relaxed); } } })); } for worker in workers { worker.join().expect("worker thread must not panic"); } assert_eq!( accepted.load(Ordering::Relaxed), UNKNOWN_DC_LOG_DISTINCT_LIMIT, "concurrent unique churn must never admit more than the configured distinct cap" ); } #[test] fn light_fuzz_unknown_dc_log_mixed_duplicates_never_exceeds_cap() { let _guard = unknown_dc_test_lock() .lock() .expect("unknown dc test lock must be available"); clear_unknown_dc_log_cache_for_testing(); // Deterministic xorshift sequence for reproducible mixed duplicate fuzzing. let mut s: u64 = 0xA5A5_5A5A_C3C3_3C3C; let mut admitted = 0usize; for _ in 0..20_000 { s ^= s << 7; s ^= s >> 9; s ^= s << 8; let dc = (s as i16).wrapping_sub(i16::MAX / 2); if should_log_unknown_dc(dc) { admitted += 1; } } assert!( admitted <= UNKNOWN_DC_LOG_DISTINCT_LIMIT, "mixed-duplicate fuzzed inputs must not admit more than cap" ); } #[test] fn scope_hint_accepts_ascii_alnum_and_dash_within_limit() { assert_eq!(validated_scope_hint("scope_alpha-1"), Some("alpha-1")); assert_eq!(validated_scope_hint("scope_AZ09"), Some("AZ09")); } #[test] fn scope_hint_rejects_invalid_or_oversized_values() { assert_eq!(validated_scope_hint("plain_user"), None); assert_eq!(validated_scope_hint("scope_"), None); assert_eq!(validated_scope_hint("scope_a/b"), None); assert_eq!(validated_scope_hint("scope_bad space"), None); assert_eq!(validated_scope_hint("scope_bad.dot"), None); let oversized = format!("scope_{}", "a".repeat(MAX_SCOPE_HINT_LEN + 1)); assert_eq!(validated_scope_hint(&oversized), None); } #[test] fn unknown_dc_log_path_sanitizer_rejects_parent_traversal_inputs() { assert!( sanitize_unknown_dc_log_path("../unknown-dc.txt").is_none(), "parent traversal paths must be rejected" ); assert!( sanitize_unknown_dc_log_path("logs/../unknown-dc.txt").is_none(), "embedded parent traversal must be rejected" ); assert!( sanitize_unknown_dc_log_path("./../unknown-dc.txt").is_none(), "relative parent traversal must be rejected" ); } #[test] fn unknown_dc_log_path_sanitizer_accepts_absolute_paths_with_existing_parent() { let absolute = std::env::temp_dir().join("unknown-dc.txt"); let absolute_str = absolute .to_str() .expect("temp absolute path must be valid UTF-8"); let sanitized = sanitize_unknown_dc_log_path(absolute_str) .expect("absolute paths with existing parent must be accepted"); assert_eq!(sanitized.resolved_path, absolute); } #[test] fn unknown_dc_log_path_sanitizer_rejects_absolute_parent_traversal() { assert!( sanitize_unknown_dc_log_path("/tmp/../etc/passwd").is_none(), "absolute parent traversal must be rejected" ); } #[test] fn unknown_dc_log_path_sanitizer_accepts_safe_relative_path() { let base = std::env::current_dir() .expect("cwd must be available") .join("target") .join(format!("telemt-unknown-dc-log-{}", std::process::id())); fs::create_dir_all(&base).expect("temp test directory must be creatable"); let candidate = base.join("unknown-dc.txt"); let candidate_relative = format!("target/telemt-unknown-dc-log-{}/unknown-dc.txt", std::process::id()); let sanitized = sanitize_unknown_dc_log_path(&candidate_relative) .expect("safe relative path with existing parent must be accepted"); assert_eq!(sanitized.resolved_path, candidate); } #[test] fn unknown_dc_log_path_sanitizer_rejects_empty_or_dot_only_inputs() { assert!( sanitize_unknown_dc_log_path("").is_none(), "empty path must be rejected" ); assert!( sanitize_unknown_dc_log_path(".").is_none(), "dot-only path without filename must be rejected" ); } #[test] fn unknown_dc_log_path_sanitizer_accepts_directory_only_as_filename_projection() { let sanitized = sanitize_unknown_dc_log_path("target/") .expect("directory-only input is interpreted as filename projection in current sanitizer"); assert!( sanitized.resolved_path.ends_with("target"), "directory-only input should resolve to canonical parent plus filename projection" ); } #[test] fn unknown_dc_log_path_sanitizer_accepts_dot_prefixed_relative_path() { let rel_dir = format!("target/telemt-unknown-dc-dot-{}", std::process::id()); let abs_dir = std::env::current_dir() .expect("cwd must be available") .join(&rel_dir); fs::create_dir_all(&abs_dir).expect("dot-prefixed test directory must be creatable"); let rel_candidate = format!("./{rel_dir}/unknown-dc.log"); let expected = abs_dir.join("unknown-dc.log"); let sanitized = sanitize_unknown_dc_log_path(&rel_candidate) .expect("dot-prefixed safe path must be accepted"); assert_eq!(sanitized.resolved_path, expected); } #[test] fn light_fuzz_unknown_dc_path_parentdir_inputs_always_rejected() { let mut s: u64 = 0xD00D_BAAD_1234_5678; for _ in 0..4096 { s ^= s << 7; s ^= s >> 9; s ^= s << 8; let a = (s as usize) % 32; let b = ((s >> 8) as usize) % 32; let candidate = format!("target/{a}/../{b}/unknown-dc.log"); assert!( sanitize_unknown_dc_log_path(&candidate).is_none(), "parent-dir candidate must be rejected: {candidate}" ); } } #[test] fn unknown_dc_log_path_sanitizer_rejects_nonexistent_parent_directory() { let rel_candidate = format!( "target/telemt-unknown-dc-missing-{}/nested/unknown-dc.txt", std::process::id() ); assert!( sanitize_unknown_dc_log_path(&rel_candidate).is_none(), "path with missing parent must be rejected to avoid implicit directory creation" ); } #[cfg(unix)] #[test] fn unknown_dc_log_path_sanitizer_accepts_symlinked_parent_inside_workspace() { use std::os::unix::fs::symlink; let base = std::env::current_dir() .expect("cwd must be available") .join("target") .join(format!("telemt-unknown-dc-log-symlink-internal-{}", std::process::id())); let real_parent = base.join("real_parent"); fs::create_dir_all(&real_parent).expect("real parent dir must be creatable"); let symlink_parent = base.join("internal_link"); let _ = fs::remove_file(&symlink_parent); symlink(&real_parent, &symlink_parent).expect("internal symlink must be creatable"); let rel_candidate = format!( "target/telemt-unknown-dc-log-symlink-internal-{}/internal_link/unknown-dc.txt", std::process::id() ); let sanitized = sanitize_unknown_dc_log_path(&rel_candidate) .expect("symlinked parent that resolves inside workspace must be accepted"); assert!( sanitized.resolved_path.starts_with(&real_parent), "sanitized path must resolve to canonical internal parent" ); } #[cfg(unix)] #[test] fn unknown_dc_log_path_sanitizer_accepts_symlink_parent_escape_as_canonical_path() { use std::os::unix::fs::symlink; let base = std::env::current_dir() .expect("cwd must be available") .join("target") .join(format!("telemt-unknown-dc-log-symlink-{}", std::process::id())); fs::create_dir_all(&base).expect("symlink test directory must be creatable"); let symlink_parent = base.join("escape_link"); let _ = fs::remove_file(&symlink_parent); symlink("/tmp", &symlink_parent).expect("symlink parent must be creatable"); let rel_candidate = format!( "target/telemt-unknown-dc-log-symlink-{}/escape_link/unknown-dc.txt", std::process::id() ); let sanitized = sanitize_unknown_dc_log_path(&rel_candidate) .expect("symlinked parent must canonicalize to target path"); assert!( sanitized.resolved_path.starts_with(Path::new("/tmp")), "sanitized path must resolve to canonical symlink target" ); } #[cfg(unix)] #[test] fn unknown_dc_log_path_revalidation_rejects_symlinked_target_escape() { use std::os::unix::fs::symlink; let base = std::env::current_dir() .expect("cwd must be available") .join("target") .join(format!("telemt-unknown-dc-target-link-{}", std::process::id())); fs::create_dir_all(&base).expect("target-link base must be creatable"); let outside = std::env::temp_dir().join(format!("telemt-outside-{}", std::process::id())); let _ = fs::remove_file(&outside); fs::write(&outside, "outside").expect("outside file must be writable"); let linked_target = base.join("unknown-dc.log"); let _ = fs::remove_file(&linked_target); symlink(&outside, &linked_target).expect("target symlink must be creatable"); let rel_candidate = format!( "target/telemt-unknown-dc-target-link-{}/unknown-dc.log", std::process::id() ); let sanitized = sanitize_unknown_dc_log_path(&rel_candidate) .expect("candidate should sanitize before final revalidation"); assert!( !unknown_dc_log_path_is_still_safe(&sanitized), "final revalidation must reject symlinked target escape" ); } #[cfg(unix)] #[test] fn unknown_dc_open_append_rejects_symlink_target_with_nofollow() { use std::os::unix::fs::symlink; let base = std::env::current_dir() .expect("cwd must be available") .join("target") .join(format!("telemt-unknown-dc-nofollow-{}", std::process::id())); fs::create_dir_all(&base).expect("nofollow base must be creatable"); let outside = std::env::temp_dir().join(format!( "telemt-unknown-dc-nofollow-outside-{}.log", std::process::id() )); let _ = fs::remove_file(&outside); fs::write(&outside, "outside\n").expect("outside file must be writable"); let linked_target = base.join("unknown-dc.log"); let _ = fs::remove_file(&linked_target); symlink(&outside, &linked_target).expect("symlink target must be creatable"); let err = open_unknown_dc_log_append(&linked_target) .expect_err("O_NOFOLLOW open must fail for symlink target"); assert_eq!( err.raw_os_error(), Some(libc::ELOOP), "symlink target must be rejected with ELOOP when O_NOFOLLOW is applied" ); } #[cfg(unix)] #[test] fn unknown_dc_open_append_rejects_broken_symlink_target_with_nofollow() { use std::os::unix::fs::symlink; let base = std::env::current_dir() .expect("cwd must be available") .join("target") .join(format!("telemt-unknown-dc-broken-link-{}", std::process::id())); fs::create_dir_all(&base).expect("broken-link base must be creatable"); let linked_target = base.join("unknown-dc.log"); let _ = fs::remove_file(&linked_target); symlink(base.join("missing-target.log"), &linked_target) .expect("broken symlink target must be creatable"); let err = open_unknown_dc_log_append(&linked_target) .expect_err("O_NOFOLLOW open must fail for broken symlink target"); assert_eq!( err.raw_os_error(), Some(libc::ELOOP), "broken symlink target must be rejected with ELOOP when O_NOFOLLOW is applied" ); } #[cfg(unix)] #[test] fn adversarial_unknown_dc_open_append_symlink_flip_never_writes_outside_file() { use std::os::unix::fs::symlink; let base = std::env::current_dir() .expect("cwd must be available") .join("target") .join(format!("telemt-unknown-dc-symlink-flip-{}", std::process::id())); fs::create_dir_all(&base).expect("symlink-flip base must be creatable"); let outside = std::env::temp_dir().join(format!( "telemt-unknown-dc-symlink-flip-outside-{}.log", std::process::id() )); fs::write(&outside, "outside-baseline\n").expect("outside baseline file must be writable"); let outside_before = fs::read_to_string(&outside).expect("outside baseline must be readable"); let target = base.join("unknown-dc.log"); let _ = fs::remove_file(&target); for step in 0..1024usize { let _ = fs::remove_file(&target); if step % 2 == 0 { symlink(&outside, &target).expect("symlink creation in flip loop must succeed"); } if let Ok(mut file) = open_unknown_dc_log_append(&target) { writeln!(file, "dc_idx={step}").expect("append on regular file must succeed"); } } let outside_after = fs::read_to_string(&outside).expect("outside file must remain readable"); assert_eq!( outside_after, outside_before, "outside file must never be modified under symlink-flip adversarial churn" ); } #[test] fn unknown_dc_open_append_creates_regular_file() { let base = std::env::current_dir() .expect("cwd must be available") .join("target") .join(format!("telemt-unknown-dc-open-{}", std::process::id())); fs::create_dir_all(&base).expect("open test base must be creatable"); let target = base.join("unknown-dc.log"); let _ = fs::remove_file(&target); { let mut file = open_unknown_dc_log_append(&target) .expect("regular target must be creatable with append open"); writeln!(file, "dc_idx=1234").expect("append write must succeed"); } let meta = fs::symlink_metadata(&target).expect("created target metadata must be readable"); assert!(meta.file_type().is_file(), "target must be a regular file"); assert!( !meta.file_type().is_symlink(), "regular target open path must not produce symlink artifacts" ); } #[test] fn stress_unknown_dc_open_append_regular_file_preserves_line_integrity() { let base = std::env::current_dir() .expect("cwd must be available") .join("target") .join(format!("telemt-unknown-dc-open-stress-{}", std::process::id())); fs::create_dir_all(&base).expect("stress open base must be creatable"); let target = base.join("unknown-dc.log"); let _ = fs::remove_file(&target); let writes = 2048usize; for idx in 0..writes { let mut file = open_unknown_dc_log_append(&target) .expect("stress append open on regular file must succeed"); writeln!(file, "dc_idx={idx}").expect("stress append write must succeed"); } let content = fs::read_to_string(&target).expect("stress output file must be readable"); assert_eq!( nonempty_line_count(&content), writes, "regular-file append stress must preserve one logical line per write" ); } #[test] fn unknown_dc_log_path_revalidation_accepts_regular_existing_target() { let base = std::env::current_dir() .expect("cwd must be available") .join("target") .join(format!("telemt-unknown-dc-safe-target-{}", std::process::id())); fs::create_dir_all(&base).expect("safe target base must be creatable"); let target = base.join("unknown-dc.log"); fs::write(&target, "seed\n").expect("safe target seed write must succeed"); let rel_candidate = format!( "target/telemt-unknown-dc-safe-target-{}/unknown-dc.log", std::process::id() ); let sanitized = sanitize_unknown_dc_log_path(&rel_candidate) .expect("safe candidate must sanitize"); assert!( unknown_dc_log_path_is_still_safe(&sanitized), "revalidation must allow safe existing regular files" ); } #[test] fn unknown_dc_log_path_revalidation_rejects_deleted_parent_after_sanitize() { let base = std::env::current_dir() .expect("cwd must be available") .join("target") .join(format!("telemt-unknown-dc-vanish-parent-{}", std::process::id())); fs::create_dir_all(&base).expect("vanish-parent base must be creatable"); let rel_candidate = format!( "target/telemt-unknown-dc-vanish-parent-{}/unknown-dc.log", std::process::id() ); let sanitized = sanitize_unknown_dc_log_path(&rel_candidate) .expect("candidate must sanitize before parent deletion"); fs::remove_dir_all(&base).expect("test parent directory must be removable"); assert!( !unknown_dc_log_path_is_still_safe(&sanitized), "revalidation must fail when sanitized parent disappears before write" ); } #[cfg(unix)] #[test] fn unknown_dc_log_path_revalidation_rejects_parent_swapped_to_symlink() { use std::os::unix::fs::symlink; let parent = std::env::current_dir() .expect("cwd must be available") .join("target") .join(format!("telemt-unknown-dc-parent-swap-{}", std::process::id())); fs::create_dir_all(&parent).expect("parent-swap test parent must be creatable"); let rel_candidate = format!( "target/telemt-unknown-dc-parent-swap-{}/unknown-dc.log", std::process::id() ); let sanitized = sanitize_unknown_dc_log_path(&rel_candidate) .expect("candidate must sanitize before parent swap"); let moved = parent.with_extension("bak"); let _ = fs::remove_dir_all(&moved); fs::rename(&parent, &moved).expect("parent must be movable for swap simulation"); symlink("/tmp", &parent).expect("symlink replacement for parent must be creatable"); assert!( !unknown_dc_log_path_is_still_safe(&sanitized), "revalidation must fail when canonical parent is swapped to a symlinked target" ); } #[cfg(unix)] #[test] fn adversarial_check_then_symlink_flip_is_blocked_by_nofollow_open() { use std::os::unix::fs::symlink; let parent = std::env::current_dir() .expect("cwd must be available") .join("target") .join(format!("telemt-unknown-dc-check-open-race-{}", std::process::id())); fs::create_dir_all(&parent).expect("check-open-race parent must be creatable"); let target = parent.join("unknown-dc.log"); fs::write(&target, "seed\n").expect("seed target file must be writable"); let rel_candidate = format!( "target/telemt-unknown-dc-check-open-race-{}/unknown-dc.log", std::process::id() ); let sanitized = sanitize_unknown_dc_log_path(&rel_candidate) .expect("candidate must sanitize"); assert!( unknown_dc_log_path_is_still_safe(&sanitized), "precondition: target should initially pass revalidation" ); let outside = std::env::temp_dir().join(format!( "telemt-unknown-dc-check-open-race-outside-{}.log", std::process::id() )); fs::write(&outside, "outside\n").expect("outside file must be writable"); fs::remove_file(&target).expect("target removal before flip must succeed"); symlink(&outside, &target).expect("target symlink flip must be creatable"); let err = open_unknown_dc_log_append(&sanitized.resolved_path) .expect_err("nofollow open must fail after symlink flip between check and open"); assert_eq!( err.raw_os_error(), Some(libc::ELOOP), "symlink flip in check/open window must be neutralized by O_NOFOLLOW" ); } #[tokio::test] async fn unknown_dc_absolute_log_path_writes_one_entry() { let _guard = unknown_dc_test_lock() .lock() .expect("unknown dc test lock must be available"); clear_unknown_dc_log_cache_for_testing(); let dc_idx: i16 = 31_001; let file_path = std::env::temp_dir().join(format!( "telemt-unknown-dc-abs-{}-{}.log", std::process::id(), dc_idx )); let _ = fs::remove_file(&file_path); let mut cfg = ProxyConfig::default(); cfg.general.unknown_dc_file_log_enabled = true; cfg.general.unknown_dc_log_path = Some( file_path .to_str() .expect("temp file path must be valid UTF-8") .to_string(), ); let _ = get_dc_addr_static(dc_idx, &cfg).expect("fallback routing must still work"); let mut content = None; for _ in 0..20 { if let Ok(text) = fs::read_to_string(&file_path) { content = Some(text); break; } tokio::time::sleep(Duration::from_millis(15)).await; } let text = content.expect("absolute unknown-DC log path must produce exactly one log write"); assert!( text.contains(&format!("dc_idx={dc_idx}")), "absolute unknown-DC integration log must contain requested dc_idx" ); } #[tokio::test] async fn unknown_dc_safe_relative_log_path_writes_one_entry() { let _guard = unknown_dc_test_lock() .lock() .expect("unknown dc test lock must be available"); clear_unknown_dc_log_cache_for_testing(); let dc_idx: i16 = 31_002; let rel_dir = format!("target/telemt-unknown-dc-int-{}", std::process::id()); let rel_file = format!("{rel_dir}/unknown-dc.log"); let abs_dir = std::env::current_dir() .expect("cwd must be available") .join(&rel_dir); fs::create_dir_all(&abs_dir).expect("integration test log directory must be creatable"); let abs_file = abs_dir.join("unknown-dc.log"); let _ = fs::remove_file(&abs_file); let mut cfg = ProxyConfig::default(); cfg.general.unknown_dc_file_log_enabled = true; cfg.general.unknown_dc_log_path = Some(rel_file); let _ = get_dc_addr_static(dc_idx, &cfg).expect("fallback routing must still work"); let mut content = None; for _ in 0..20 { if let Ok(text) = fs::read_to_string(&abs_file) { content = Some(text); break; } tokio::time::sleep(Duration::from_millis(15)).await; } let text = content.expect("safe relative path must produce exactly one log write"); assert!( text.contains(&format!("dc_idx={dc_idx}")), "unknown-DC integration log must contain requested dc_idx" ); } #[tokio::test] async fn unknown_dc_same_index_burst_writes_only_once() { let _guard = unknown_dc_test_lock() .lock() .expect("unknown dc test lock must be available"); clear_unknown_dc_log_cache_for_testing(); let dc_idx: i16 = 31_010; let rel_dir = format!("target/telemt-unknown-dc-same-{}", std::process::id()); let rel_file = format!("{rel_dir}/unknown-dc.log"); let abs_dir = std::env::current_dir().unwrap().join(&rel_dir); fs::create_dir_all(&abs_dir).expect("same-index log directory must be creatable"); let abs_file = abs_dir.join("unknown-dc.log"); let _ = fs::remove_file(&abs_file); let mut cfg = ProxyConfig::default(); cfg.general.unknown_dc_file_log_enabled = true; cfg.general.unknown_dc_log_path = Some(rel_file); for _ in 0..64 { let _ = get_dc_addr_static(dc_idx, &cfg).expect("fallback routing must still work"); } let mut content = None; for _ in 0..30 { if let Ok(text) = fs::read_to_string(&abs_file) { content = Some(text); break; } tokio::time::sleep(Duration::from_millis(10)).await; } let text = content.expect("same-index burst must produce at least one log write"); assert_eq!( nonempty_line_count(&text), 1, "same unknown dc index must be deduplicated to one file line" ); } #[tokio::test] async fn unknown_dc_distinct_burst_is_hard_capped_on_file_writes() { let _guard = unknown_dc_test_lock() .lock() .expect("unknown dc test lock must be available"); clear_unknown_dc_log_cache_for_testing(); let rel_dir = format!("target/telemt-unknown-dc-cap-{}", std::process::id()); let rel_file = format!("{rel_dir}/unknown-dc.log"); let abs_dir = std::env::current_dir().unwrap().join(&rel_dir); fs::create_dir_all(&abs_dir).expect("cap log directory must be creatable"); let abs_file = abs_dir.join("unknown-dc.log"); let _ = fs::remove_file(&abs_file); let mut cfg = ProxyConfig::default(); cfg.general.unknown_dc_file_log_enabled = true; cfg.general.unknown_dc_log_path = Some(rel_file); for i in 0..(UNKNOWN_DC_LOG_DISTINCT_LIMIT + 128) { let dc_idx = 20_000i16.wrapping_add(i as i16); let _ = get_dc_addr_static(dc_idx, &cfg).expect("fallback routing must still work"); } let mut final_text = String::new(); for _ in 0..80 { if let Ok(text) = fs::read_to_string(&abs_file) { final_text = text; if nonempty_line_count(&final_text) >= UNKNOWN_DC_LOG_DISTINCT_LIMIT { break; } } tokio::time::sleep(Duration::from_millis(10)).await; } let line_count = nonempty_line_count(&final_text); assert!( line_count > 0, "distinct unknown-dc burst must write at least one line" ); assert!( line_count <= UNKNOWN_DC_LOG_DISTINCT_LIMIT, "distinct unknown-dc writes must stay within dedup hard cap" ); } #[cfg(unix)] #[tokio::test] async fn unknown_dc_symlinked_target_escape_is_not_written_integration() { use std::os::unix::fs::symlink; let _guard = unknown_dc_test_lock() .lock() .expect("unknown dc test lock must be available"); clear_unknown_dc_log_cache_for_testing(); let base = std::env::current_dir() .expect("cwd must be available") .join("target") .join(format!("telemt-unknown-dc-no-write-link-{}", std::process::id())); fs::create_dir_all(&base).expect("integration symlink base must be creatable"); let outside = std::env::temp_dir().join(format!( "telemt-unknown-dc-outside-{}.log", std::process::id() )); fs::write(&outside, "baseline\n").expect("outside baseline file must be writable"); let linked_target = base.join("unknown-dc.log"); let _ = fs::remove_file(&linked_target); symlink(&outside, &linked_target).expect("symlink target must be creatable"); let rel_file = format!( "target/telemt-unknown-dc-no-write-link-{}/unknown-dc.log", std::process::id() ); let dc_idx: i16 = 31_050; let mut cfg = ProxyConfig::default(); cfg.general.unknown_dc_file_log_enabled = true; cfg.general.unknown_dc_log_path = Some(rel_file); let before = fs::read_to_string(&outside).expect("must read baseline outside file"); let _ = get_dc_addr_static(dc_idx, &cfg).expect("fallback routing must still work"); tokio::time::sleep(Duration::from_millis(80)).await; let after = fs::read_to_string(&outside).expect("must read outside file after attempt"); assert_eq!( after, before, "symlink target escape must not be written by unknown-DC logging" ); } #[test] fn fallback_dc_never_panics_with_single_dc_list() { let mut cfg = ProxyConfig::default(); cfg.network.prefer = 6; cfg.network.ipv6 = Some(true); cfg.default_dc = Some(42); let addr = get_dc_addr_static(999, &cfg).expect("fallback dc must resolve safely"); let expected = SocketAddr::new(TG_DATACENTERS_V6[0], TG_DATACENTER_PORT); assert_eq!(addr, expected); } #[tokio::test] async fn direct_relay_abort_midflight_releases_route_gauge() { let tg_listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let tg_addr = tg_listener.local_addr().unwrap(); let tg_accept_task = tokio::spawn(async move { let (stream, _) = tg_listener.accept().await.unwrap(); let _hold_stream = stream; tokio::time::sleep(Duration::from_secs(60)).await; }); let stats = Arc::new(Stats::new()); let mut config = ProxyConfig::default(); config .dc_overrides .insert("2".to_string(), vec![tg_addr.to_string()]); let config = Arc::new(config); let upstream_manager = Arc::new(UpstreamManager::new( vec![UpstreamConfig { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, }, weight: 1, enabled: true, scopes: String::new(), selected_scope: String::new(), }], 1, 1, 1, 1, false, stats.clone(), )); let rng = Arc::new(SecureRandom::new()); let buffer_pool = Arc::new(BufferPool::new()); let route_runtime = Arc::new(RouteRuntimeController::new(RelayRouteMode::Direct)); let route_snapshot = route_runtime.snapshot(); let (server_side, client_side) = duplex(64 * 1024); let (server_reader, server_writer) = tokio::io::split(server_side); let client_reader = make_crypto_reader(server_reader); let client_writer = make_crypto_writer(server_writer); let success = HandshakeSuccess { user: "abort-direct-user".to_string(), dc_idx: 2, proto_tag: ProtoTag::Intermediate, dec_key: [0u8; 32], dec_iv: 0, enc_key: [0u8; 32], enc_iv: 0, peer: "127.0.0.1:50000".parse().unwrap(), is_tls: false, }; let relay_task = tokio::spawn(handle_via_direct( client_reader, client_writer, success, upstream_manager, stats.clone(), config, buffer_pool, rng, route_runtime.subscribe(), route_snapshot, 0xabad1dea, )); let started = tokio::time::timeout(Duration::from_secs(2), async { loop { if stats.get_current_connections_direct() == 1 { break; } tokio::time::sleep(Duration::from_millis(10)).await; } }) .await; assert!(started.is_ok(), "direct relay must increment route gauge before abort"); relay_task.abort(); let joined = relay_task.await; assert!(joined.is_err(), "aborted direct relay task must return join error"); tokio::time::sleep(Duration::from_millis(20)).await; assert_eq!( stats.get_current_connections_direct(), 0, "route gauge must be released when direct relay task is aborted mid-flight" ); drop(client_side); tg_accept_task.abort(); let _ = tg_accept_task.await; } #[tokio::test] async fn direct_relay_cutover_midflight_releases_route_gauge() { let tg_listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let tg_addr = tg_listener.local_addr().unwrap(); let tg_accept_task = tokio::spawn(async move { let (stream, _) = tg_listener.accept().await.unwrap(); let _hold_stream = stream; tokio::time::sleep(Duration::from_secs(60)).await; }); let stats = Arc::new(Stats::new()); let mut config = ProxyConfig::default(); config .dc_overrides .insert("2".to_string(), vec![tg_addr.to_string()]); let config = Arc::new(config); let upstream_manager = Arc::new(UpstreamManager::new( vec![UpstreamConfig { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, }, weight: 1, enabled: true, scopes: String::new(), selected_scope: String::new(), }], 1, 1, 1, 1, false, stats.clone(), )); let rng = Arc::new(SecureRandom::new()); let buffer_pool = Arc::new(BufferPool::new()); let route_runtime = Arc::new(RouteRuntimeController::new(RelayRouteMode::Direct)); let route_snapshot = route_runtime.snapshot(); let (server_side, client_side) = duplex(64 * 1024); let (server_reader, server_writer) = tokio::io::split(server_side); let client_reader = make_crypto_reader(server_reader); let client_writer = make_crypto_writer(server_writer); let success = HandshakeSuccess { user: "cutover-direct-user".to_string(), dc_idx: 2, proto_tag: ProtoTag::Intermediate, dec_key: [0u8; 32], dec_iv: 0, enc_key: [0u8; 32], enc_iv: 0, peer: "127.0.0.1:50002".parse().unwrap(), is_tls: false, }; let relay_task = tokio::spawn(handle_via_direct( client_reader, client_writer, success, upstream_manager, stats.clone(), config, buffer_pool, rng, route_runtime.subscribe(), route_snapshot, 0xface_cafe, )); tokio::time::timeout(Duration::from_secs(2), async { loop { if stats.get_current_connections_direct() == 1 { break; } tokio::time::sleep(Duration::from_millis(10)).await; } }) .await .expect("direct relay must increment route gauge before cutover"); assert!( route_runtime.set_mode(RelayRouteMode::Middle).is_some(), "cutover must advance route generation" ); let relay_result = tokio::time::timeout(Duration::from_secs(6), relay_task) .await .expect("direct relay must terminate after cutover") .expect("direct relay task must not panic"); assert!( relay_result.is_err(), "cutover should terminate direct relay session" ); assert!( matches!( relay_result, Err(ProxyError::Proxy(ref msg)) if msg == ROUTE_SWITCH_ERROR_MSG ), "client-visible cutover error must stay generic and avoid route-internal metadata" ); assert_eq!( stats.get_current_connections_direct(), 0, "route gauge must be released when direct relay exits on cutover" ); drop(client_side); tg_accept_task.abort(); let _ = tg_accept_task.await; } #[tokio::test] async fn direct_relay_cutover_storm_multi_session_keeps_generic_errors_and_releases_gauge() { let session_count = 6usize; let tg_listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let tg_addr = tg_listener.local_addr().unwrap(); let tg_accept_task = tokio::spawn(async move { let mut held_streams = Vec::with_capacity(session_count); for _ in 0..session_count { let (stream, _) = tg_listener.accept().await.unwrap(); held_streams.push(stream); } tokio::time::sleep(Duration::from_secs(60)).await; drop(held_streams); }); let stats = Arc::new(Stats::new()); let mut config = ProxyConfig::default(); config .dc_overrides .insert("2".to_string(), vec![tg_addr.to_string()]); let config = Arc::new(config); let upstream_manager = Arc::new(UpstreamManager::new( vec![UpstreamConfig { upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None, }, weight: 1, enabled: true, scopes: String::new(), selected_scope: String::new(), }], 1, 1, 1, 1, false, stats.clone(), )); let rng = Arc::new(SecureRandom::new()); let buffer_pool = Arc::new(BufferPool::new()); let route_runtime = Arc::new(RouteRuntimeController::new(RelayRouteMode::Direct)); let route_snapshot = route_runtime.snapshot(); let mut relay_tasks = Vec::with_capacity(session_count); let mut client_sides = Vec::with_capacity(session_count); for idx in 0..session_count { let (server_side, client_side) = duplex(64 * 1024); client_sides.push(client_side); let (server_reader, server_writer) = tokio::io::split(server_side); let client_reader = make_crypto_reader(server_reader); let client_writer = make_crypto_writer(server_writer); let success = HandshakeSuccess { user: format!("cutover-storm-direct-user-{idx}"), dc_idx: 2, proto_tag: ProtoTag::Intermediate, dec_key: [0u8; 32], dec_iv: 0, enc_key: [0u8; 32], enc_iv: 0, peer: SocketAddr::new( std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), 51000 + idx as u16, ), is_tls: false, }; relay_tasks.push(tokio::spawn(handle_via_direct( client_reader, client_writer, success, upstream_manager.clone(), stats.clone(), config.clone(), buffer_pool.clone(), rng.clone(), route_runtime.subscribe(), route_snapshot, 0xA000_0000 + idx as u64, ))); } tokio::time::timeout(Duration::from_secs(4), async { loop { if stats.get_current_connections_direct() == session_count as u64 { break; } tokio::time::sleep(Duration::from_millis(10)).await; } }) .await .expect("all direct sessions must become active before cutover storm"); let route_runtime_flipper = route_runtime.clone(); let flipper = tokio::spawn(async move { for step in 0..64u32 { let mode = if (step & 1) == 0 { RelayRouteMode::Middle } else { RelayRouteMode::Direct }; let _ = route_runtime_flipper.set_mode(mode); tokio::time::sleep(Duration::from_millis(15)).await; } }); for relay_task in relay_tasks { let relay_result = tokio::time::timeout(Duration::from_secs(10), relay_task) .await .expect("direct relay task must finish under cutover storm") .expect("direct relay task must not panic"); assert!( matches!( relay_result, Err(ProxyError::Proxy(ref msg)) if msg == ROUTE_SWITCH_ERROR_MSG ), "storm-cutover termination must remain generic for all direct sessions" ); } flipper.abort(); let _ = flipper.await; assert_eq!( stats.get_current_connections_direct(), 0, "direct route gauge must return to zero after cutover storm" ); drop(client_sides); tg_accept_task.abort(); let _ = tg_accept_task.await; } #[test] fn prefer_v6_override_matrix_prefers_matching_family_then_degrades_safely() { let dc_idx: i16 = 2; let mut cfg_a = ProxyConfig::default(); cfg_a.network.prefer = 6; cfg_a.network.ipv6 = Some(true); cfg_a.dc_overrides.insert( dc_idx.to_string(), vec![ "203.0.113.90:443".to_string(), "[2001:db8::90]:443".to_string(), ], ); let a = get_dc_addr_static(dc_idx, &cfg_a).expect("v6+v4 override set must resolve"); assert!(a.is_ipv6(), "prefer_v6 should choose v6 override when present"); let mut cfg_b = ProxyConfig::default(); cfg_b.network.prefer = 6; cfg_b.network.ipv6 = Some(true); cfg_b.dc_overrides .insert(dc_idx.to_string(), vec!["203.0.113.91:443".to_string()]); let b = get_dc_addr_static(dc_idx, &cfg_b).expect("v4-only override must still resolve"); assert!(b.is_ipv4(), "when no v6 override exists, v4 override must be used"); let mut cfg_c = ProxyConfig::default(); cfg_c.network.prefer = 6; cfg_c.network.ipv6 = Some(true); let c = get_dc_addr_static(dc_idx, &cfg_c).expect("table fallback must resolve"); assert_eq!( c, SocketAddr::new(TG_DATACENTERS_V6[(dc_idx as usize) - 1], TG_DATACENTER_PORT), "without overrides, prefer_v6 path must resolve from static v6 datacenter table" ); } #[test] fn prefer_v6_override_matrix_ignores_invalid_entries_and_keeps_fail_closed_fallback() { let dc_idx: i16 = 3; let mut cfg = ProxyConfig::default(); cfg.network.prefer = 6; cfg.network.ipv6 = Some(true); cfg.dc_overrides.insert( dc_idx.to_string(), vec![ "not-an-addr".to_string(), "also:bad".to_string(), "203.0.113.55:443".to_string(), ], ); let addr = get_dc_addr_static(dc_idx, &cfg).expect("at least one valid override must keep resolution alive"); assert_eq!(addr, "203.0.113.55:443".parse::().unwrap()); } #[test] fn stress_prefer_v6_override_matrix_is_deterministic_under_mixed_inputs() { for idx in 1..=5i16 { let mut cfg = ProxyConfig::default(); cfg.network.prefer = 6; cfg.network.ipv6 = Some(true); cfg.dc_overrides.insert( idx.to_string(), vec![ format!("203.0.113.{}:443", 100 + idx), format!("[2001:db8::{}]:443", 100 + idx), ], ); let first = get_dc_addr_static(idx, &cfg).expect("first lookup must resolve"); let second = get_dc_addr_static(idx, &cfg).expect("second lookup must resolve"); assert_eq!(first, second, "override resolution must stay deterministic for dc {idx}"); assert!(first.is_ipv6(), "dc {idx}: v6 override should be preferred"); } }