mirror of
https://github.com/telemt/telemt.git
synced 2026-06-18 17:08:29 +03:00
Refactor and enhance security in proxy and handshake modules
- Updated `direct_relay_security_tests.rs` to ensure sanitized paths are correctly validated against resolved paths. - Added tests for symlink handling in `unknown_dc_log_path_revalidation` to prevent symlink target escape vulnerabilities. - Modified `handshake.rs` to use a more robust hashing strategy for eviction offsets, improving the eviction logic in `auth_probe_record_failure_with_state`. - Introduced new tests in `handshake_security_tests.rs` to validate eviction logic under various conditions, ensuring low fail streak entries are prioritized for eviction. - Simplified `route_mode.rs` by removing unnecessary atomic mode tracking, streamlining the transition logic in `RouteRuntimeController`. - Enhanced `route_mode_security_tests.rs` with comprehensive tests for mode transitions and their effects on session states, ensuring consistency under concurrent modifications. - Cleaned up `emulator.rs` by removing unused ALPN extension handling, improving code clarity and maintainability.
This commit is contained in:
@@ -7,6 +7,7 @@ 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};
|
||||
@@ -182,7 +183,7 @@ fn unknown_dc_log_path_sanitizer_accepts_absolute_paths_with_existing_parent() {
|
||||
|
||||
let sanitized = sanitize_unknown_dc_log_path(absolute_str)
|
||||
.expect("absolute paths with existing parent must be accepted");
|
||||
assert_eq!(sanitized, absolute);
|
||||
assert_eq!(sanitized.resolved_path, absolute);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -206,7 +207,7 @@ fn unknown_dc_log_path_sanitizer_accepts_safe_relative_path() {
|
||||
|
||||
let sanitized = sanitize_unknown_dc_log_path(&candidate_relative)
|
||||
.expect("safe relative path with existing parent must be accepted");
|
||||
assert_eq!(sanitized, candidate);
|
||||
assert_eq!(sanitized.resolved_path, candidate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -226,7 +227,7 @@ 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.ends_with("target"),
|
||||
sanitized.resolved_path.ends_with("target"),
|
||||
"directory-only input should resolve to canonical parent plus filename projection"
|
||||
);
|
||||
}
|
||||
@@ -243,7 +244,7 @@ fn unknown_dc_log_path_sanitizer_accepts_dot_prefixed_relative_path() {
|
||||
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, expected);
|
||||
assert_eq!(sanitized.resolved_path, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -300,7 +301,7 @@ fn unknown_dc_log_path_sanitizer_accepts_symlinked_parent_inside_workspace() {
|
||||
let sanitized = sanitize_unknown_dc_log_path(&rel_candidate)
|
||||
.expect("symlinked parent that resolves inside workspace must be accepted");
|
||||
assert!(
|
||||
sanitized.starts_with(&real_parent),
|
||||
sanitized.resolved_path.starts_with(&real_parent),
|
||||
"sanitized path must resolve to canonical internal parent"
|
||||
);
|
||||
}
|
||||
@@ -328,11 +329,304 @@ fn unknown_dc_log_path_sanitizer_accepts_symlink_parent_escape_as_canonical_path
|
||||
let sanitized = sanitize_unknown_dc_log_path(&rel_candidate)
|
||||
.expect("symlinked parent must canonicalize to target path");
|
||||
assert!(
|
||||
sanitized.starts_with(Path::new("/tmp")),
|
||||
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()
|
||||
@@ -499,6 +793,53 @@ async fn unknown_dc_distinct_burst_is_hard_capped_on_file_writes() {
|
||||
);
|
||||
}
|
||||
|
||||
#[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();
|
||||
|
||||
Reference in New Issue
Block a user