mirror of
https://github.com/telemt/telemt.git
synced 2026-10-31 21:02:01 +03:00
Hardened listener reload + config persistence + SYN Limit startup safety
This commit is contained in:
+20
-96
@@ -16,12 +16,10 @@
|
||||
//! | `general` | `telemetry` / `me_*_policy` | Applied immediately |
|
||||
//! | `network` | `dns_overrides` | Applied immediately |
|
||||
//! | `access` | All user/quota fields | Effective immediately |
|
||||
//! | `server.listeners` | `synlimit*` for existing endpoints | Netfilter rules reconciled immediately |
|
||||
//!
|
||||
//! Fields that require re-binding sockets (`server.listeners`, legacy
|
||||
//! `server.port`, `censorship.*`, `network.*`, `use_middle_proxy`) are **not**
|
||||
//! applied, except for SYN limiter fields on unchanged listener endpoints; a
|
||||
//! warning is emitted.
|
||||
//! applied; a warning is emitted. SYN limiter rules are process-owned and are
|
||||
//! reconciled only during privileged startup.
|
||||
//! Non-hot changes are never mixed into the runtime config snapshot.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
@@ -36,9 +34,11 @@ use tracing::{error, info, warn};
|
||||
|
||||
use super::load::{LoadedConfig, ProxyConfig};
|
||||
use crate::config::{
|
||||
CidrRateLimitKey, ListenerConfig, LogLevel, MeBindStaleMode, MeFloorMode, MeSocksKdfPolicy,
|
||||
MeTelemetryLevel, MeWriterPickMode, SynLimitMode,
|
||||
CidrRateLimitKey, LogLevel, MeBindStaleMode, MeFloorMode, MeSocksKdfPolicy, MeTelemetryLevel,
|
||||
MeWriterPickMode,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use crate::config::{ListenerConfig, SynLimitMode};
|
||||
|
||||
const HOT_RELOAD_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||
|
||||
@@ -133,22 +133,6 @@ pub struct HotFields {
|
||||
pub user_max_unique_ips_global_each: usize,
|
||||
pub user_max_unique_ips_mode: crate::config::UserMaxUniqueIpsMode,
|
||||
pub user_max_unique_ips_window_secs: u64,
|
||||
pub listener_synlimit: Vec<ListenerSynLimitHotFields>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ListenerSynLimitHotFields {
|
||||
pub ip: IpAddr,
|
||||
pub port: Option<u16>,
|
||||
pub synlimit: SynLimitMode,
|
||||
pub synlimit_seconds: u32,
|
||||
pub synlimit_hitcount: u32,
|
||||
pub synlimit_burst: u32,
|
||||
pub synlimit_ios_seconds: u32,
|
||||
pub synlimit_ios_hitcount: u32,
|
||||
pub synlimit_ios_burst: u32,
|
||||
pub synlimit_hashlimit_expire_ms: u32,
|
||||
pub synlimit_hashlimit_size: u32,
|
||||
}
|
||||
|
||||
impl HotFields {
|
||||
@@ -278,30 +262,6 @@ impl HotFields {
|
||||
user_max_unique_ips_global_each: cfg.access.user_max_unique_ips_global_each,
|
||||
user_max_unique_ips_mode: cfg.access.user_max_unique_ips_mode,
|
||||
user_max_unique_ips_window_secs: cfg.access.user_max_unique_ips_window_secs,
|
||||
listener_synlimit: cfg
|
||||
.server
|
||||
.listeners
|
||||
.iter()
|
||||
.map(ListenerSynLimitHotFields::from_listener)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ListenerSynLimitHotFields {
|
||||
fn from_listener(listener: &ListenerConfig) -> Self {
|
||||
Self {
|
||||
ip: listener.ip,
|
||||
port: listener.port,
|
||||
synlimit: listener.synlimit,
|
||||
synlimit_seconds: listener.synlimit_seconds,
|
||||
synlimit_hitcount: listener.synlimit_hitcount,
|
||||
synlimit_burst: listener.synlimit_burst,
|
||||
synlimit_ios_seconds: listener.synlimit_ios_seconds,
|
||||
synlimit_ios_hitcount: listener.synlimit_ios_hitcount,
|
||||
synlimit_ios_burst: listener.synlimit_ios_burst,
|
||||
synlimit_hashlimit_expire_ms: listener.synlimit_hashlimit_expire_ms,
|
||||
synlimit_hashlimit_size: listener.synlimit_hashlimit_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -348,18 +308,7 @@ fn listeners_equal(
|
||||
lhs: &[crate::config::ListenerConfig],
|
||||
rhs: &[crate::config::ListenerConfig],
|
||||
) -> bool {
|
||||
if lhs.len() != rhs.len() {
|
||||
return false;
|
||||
}
|
||||
lhs.iter().zip(rhs.iter()).all(|(a, b)| {
|
||||
a.ip == b.ip
|
||||
&& a.port == b.port
|
||||
&& a.client_mss == b.client_mss
|
||||
&& a.announce == b.announce
|
||||
&& a.announce_ip == b.announce_ip
|
||||
&& a.proxy_protocol == b.proxy_protocol
|
||||
&& a.reuse_allow == b.reuse_allow
|
||||
})
|
||||
serde_json::to_value(lhs).ok() == serde_json::to_value(rhs).ok()
|
||||
}
|
||||
|
||||
fn resolve_default_link_port(cfg: &ProxyConfig) -> u16 {
|
||||
@@ -608,8 +557,6 @@ fn overlay_hot_fields(old: &ProxyConfig, new: &ProxyConfig) -> ProxyConfig {
|
||||
cfg.access.user_max_unique_ips_global_each = new.access.user_max_unique_ips_global_each;
|
||||
cfg.access.user_max_unique_ips_mode = new.access.user_max_unique_ips_mode;
|
||||
cfg.access.user_max_unique_ips_window_secs = new.access.user_max_unique_ips_window_secs;
|
||||
overlay_listener_synlimit_fields(&mut cfg.server.listeners, &new.server.listeners);
|
||||
|
||||
if cfg.rebuild_runtime_user_auth().is_err() {
|
||||
cfg.runtime_user_auth = None;
|
||||
}
|
||||
@@ -617,26 +564,6 @@ fn overlay_hot_fields(old: &ProxyConfig, new: &ProxyConfig) -> ProxyConfig {
|
||||
cfg
|
||||
}
|
||||
|
||||
fn overlay_listener_synlimit_fields(old: &mut [ListenerConfig], new: &[ListenerConfig]) {
|
||||
if old.len() != new.len() {
|
||||
return;
|
||||
}
|
||||
for (old_listener, new_listener) in old.iter_mut().zip(new.iter()) {
|
||||
if old_listener.ip != new_listener.ip || old_listener.port != new_listener.port {
|
||||
continue;
|
||||
}
|
||||
old_listener.synlimit = new_listener.synlimit;
|
||||
old_listener.synlimit_seconds = new_listener.synlimit_seconds;
|
||||
old_listener.synlimit_hitcount = new_listener.synlimit_hitcount;
|
||||
old_listener.synlimit_burst = new_listener.synlimit_burst;
|
||||
old_listener.synlimit_ios_seconds = new_listener.synlimit_ios_seconds;
|
||||
old_listener.synlimit_ios_hitcount = new_listener.synlimit_ios_hitcount;
|
||||
old_listener.synlimit_ios_burst = new_listener.synlimit_ios_burst;
|
||||
old_listener.synlimit_hashlimit_expire_ms = new_listener.synlimit_hashlimit_expire_ms;
|
||||
old_listener.synlimit_hashlimit_size = new_listener.synlimit_hashlimit_size;
|
||||
}
|
||||
}
|
||||
|
||||
/// Warn if any non-hot fields changed (require restart).
|
||||
fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: bool) {
|
||||
let mut warned = false;
|
||||
@@ -913,13 +840,6 @@ fn log_changes(
|
||||
);
|
||||
}
|
||||
|
||||
if old_hot.listener_synlimit != new_hot.listener_synlimit {
|
||||
info!(
|
||||
"config reload: server.listeners SYN limiter updated ({} listeners)",
|
||||
new_hot.listener_synlimit.len()
|
||||
);
|
||||
}
|
||||
|
||||
if old_hot.desync_all_full != new_hot.desync_all_full {
|
||||
info!(
|
||||
"config reload: desync_all_full: {} → {}",
|
||||
@@ -1338,6 +1258,7 @@ fn reload_config(
|
||||
let LoadedConfig {
|
||||
config: new_cfg,
|
||||
source_files,
|
||||
source_contents: _,
|
||||
rendered_hash,
|
||||
} = loaded;
|
||||
let next_manifest = WatchManifest::from_source_files(&source_files);
|
||||
@@ -1731,7 +1652,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listener_synlimit_extended_fields_are_hot() {
|
||||
fn listener_synlimit_fields_are_process_owned() {
|
||||
let mut old = sample_config();
|
||||
old.server.listeners.push(ListenerConfig {
|
||||
ip: "0.0.0.0".parse().unwrap(),
|
||||
@@ -1765,14 +1686,17 @@ mod tests {
|
||||
let applied = overlay_hot_fields(&old, &new);
|
||||
let listener = &applied.server.listeners[0];
|
||||
assert_eq!(applied.server.port, old.server.port);
|
||||
assert_eq!(listener.synlimit_seconds, 120);
|
||||
assert_eq!(listener.synlimit_hitcount, 96);
|
||||
assert_eq!(listener.synlimit_burst, 2);
|
||||
assert_eq!(listener.synlimit_ios_seconds, 2);
|
||||
assert_eq!(listener.synlimit_ios_hitcount, 18);
|
||||
assert_eq!(listener.synlimit_ios_burst, 36);
|
||||
assert_eq!(listener.synlimit_hashlimit_expire_ms, 90_000);
|
||||
assert_eq!(listener.synlimit_hashlimit_size, 65_536);
|
||||
assert_eq!(listener.synlimit_seconds, old.server.listeners[0].synlimit_seconds);
|
||||
assert_eq!(
|
||||
listener.synlimit_hitcount,
|
||||
old.server.listeners[0].synlimit_hitcount
|
||||
);
|
||||
assert_eq!(listener.synlimit_burst, old.server.listeners[0].synlimit_burst);
|
||||
assert_eq!(
|
||||
listener.synlimit_hashlimit_size,
|
||||
old.server.listeners[0].synlimit_hashlimit_size
|
||||
);
|
||||
assert!(classify_config_changes(&old, &new).restart_required);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+75
-7
@@ -1,6 +1,6 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
@@ -34,8 +34,8 @@ use self::normalize::{
|
||||
pub(crate) use self::runtime_auth::UserAuthSnapshot;
|
||||
use self::strict_keys::handle_unknown_config_keys;
|
||||
use self::validation::{
|
||||
normalize_upstream_family_policy, validate_logging_config, validate_network_cfg,
|
||||
validate_upstreams,
|
||||
normalize_upstream_family_policy, validate_listener_runtime_profiles,
|
||||
validate_logging_config, validate_network_cfg, validate_upstreams,
|
||||
};
|
||||
|
||||
const MAX_ME_WRITER_CMD_CHANNEL_CAPACITY: usize = 16_384;
|
||||
@@ -49,12 +49,27 @@ const MAX_MAX_CLIENT_FRAME_BYTES: usize = 16 * 1024 * 1024;
|
||||
const MAX_API_REQUEST_BODY_LIMIT_BYTES: usize = 1024 * 1024;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Validated config plus the exact recursive source snapshot used to build it.
|
||||
pub(crate) struct LoadedConfig {
|
||||
/// Validated and normalized effective configuration.
|
||||
pub(crate) config: ProxyConfig,
|
||||
/// Canonical paths participating in the recursive include graph.
|
||||
pub(crate) source_files: Vec<PathBuf>,
|
||||
/// Raw source bytes keyed by canonical source path.
|
||||
pub(crate) source_contents: BTreeMap<PathBuf, String>,
|
||||
/// Legacy hash of the include-expanded rendered snapshot.
|
||||
pub(crate) rendered_hash: u64,
|
||||
}
|
||||
|
||||
/// Raw recursive source graph captured before typed deserialization.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ConfigSourceGraph {
|
||||
/// Raw source bytes keyed by canonical source path.
|
||||
pub(crate) source_contents: BTreeMap<PathBuf, String>,
|
||||
/// Include-expanded TOML used for typed deserialization.
|
||||
pub(crate) rendered: String,
|
||||
}
|
||||
|
||||
/// Main runtime configuration loaded from TOML.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ProxyConfig {
|
||||
@@ -120,14 +135,64 @@ impl ProxyConfig {
|
||||
Self::load_with_metadata(path).map(|loaded| loaded.config)
|
||||
}
|
||||
|
||||
/// Loads typed configuration together with its source and rendered metadata.
|
||||
pub(crate) fn load_with_metadata<P: AsRef<Path>>(path: P) -> Result<LoadedConfig> {
|
||||
Self::load_with_source_overrides(path, &BTreeMap::new())
|
||||
}
|
||||
|
||||
/// Loads a typed snapshot while replacing selected captured source documents.
|
||||
pub(crate) fn load_with_source_overrides<P: AsRef<Path>>(
|
||||
path: P,
|
||||
source_overrides: &BTreeMap<PathBuf, String>,
|
||||
) -> Result<LoadedConfig> {
|
||||
let graph = Self::read_source_graph_with_overrides(path, source_overrides)?;
|
||||
Self::load_source_graph(graph)
|
||||
}
|
||||
|
||||
/// Captures the raw include graph without requiring typed config validity.
|
||||
pub(crate) fn read_source_graph<P: AsRef<Path>>(path: P) -> Result<ConfigSourceGraph> {
|
||||
Self::read_source_graph_with_overrides(path, &BTreeMap::new())
|
||||
}
|
||||
|
||||
/// Captures a raw include graph with in-memory source replacements.
|
||||
pub(crate) fn read_source_graph_with_overrides<P: AsRef<Path>>(
|
||||
path: P,
|
||||
source_overrides: &BTreeMap<PathBuf, String>,
|
||||
) -> Result<ConfigSourceGraph> {
|
||||
let path = path.as_ref();
|
||||
let content =
|
||||
std::fs::read_to_string(path).map_err(|e| ProxyError::Config(e.to_string()))?;
|
||||
let normalized_path = normalize_config_path(path);
|
||||
let content = source_overrides
|
||||
.get(&normalized_path)
|
||||
.cloned()
|
||||
.map(Ok)
|
||||
.unwrap_or_else(|| std::fs::read_to_string(path))
|
||||
.map_err(|e| ProxyError::Config(e.to_string()))?;
|
||||
let base_dir = path.parent().unwrap_or(Path::new("."));
|
||||
let mut source_files = BTreeSet::new();
|
||||
source_files.insert(normalize_config_path(path));
|
||||
let processed = preprocess_includes(&content, base_dir, 0, &mut source_files)?;
|
||||
source_files.insert(normalized_path.clone());
|
||||
let mut source_contents = BTreeMap::new();
|
||||
source_contents.insert(normalized_path, content.clone());
|
||||
let processed = preprocess_includes(
|
||||
&content,
|
||||
base_dir,
|
||||
0,
|
||||
&mut source_files,
|
||||
&mut source_contents,
|
||||
source_overrides,
|
||||
)?;
|
||||
|
||||
Ok(ConfigSourceGraph {
|
||||
source_contents,
|
||||
rendered: processed,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_source_graph(graph: ConfigSourceGraph) -> Result<LoadedConfig> {
|
||||
let ConfigSourceGraph {
|
||||
source_contents,
|
||||
rendered: processed,
|
||||
} = graph;
|
||||
let source_files: BTreeSet<PathBuf> = source_contents.keys().cloned().collect();
|
||||
|
||||
let parsed_toml: toml::Value =
|
||||
toml::from_str(&processed).map_err(|e| ProxyError::Config(e.to_string()))?;
|
||||
@@ -1349,6 +1414,7 @@ impl ProxyConfig {
|
||||
listener.announce = Some(ip.to_string());
|
||||
}
|
||||
}
|
||||
validate_listener_runtime_profiles(&config)?;
|
||||
|
||||
// Migration: show_link (top-level) → general.links.show.
|
||||
if !config.show_link.is_empty() && config.general.links.show.is_empty() {
|
||||
@@ -1387,6 +1453,7 @@ impl ProxyConfig {
|
||||
Ok(LoadedConfig {
|
||||
config,
|
||||
source_files: source_files.into_iter().collect(),
|
||||
source_contents,
|
||||
rendered_hash: hash_rendered_snapshot(&processed),
|
||||
})
|
||||
}
|
||||
@@ -1458,6 +1525,7 @@ impl ProxyConfig {
|
||||
}
|
||||
|
||||
crate::network::dns_overrides::validate_entries(&self.network.dns_overrides)?;
|
||||
validate_listener_runtime_profiles(self)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -27,6 +27,8 @@ pub(super) fn preprocess_includes(
|
||||
base_dir: &Path,
|
||||
depth: u8,
|
||||
source_files: &mut BTreeSet<PathBuf>,
|
||||
source_contents: &mut BTreeMap<PathBuf, String>,
|
||||
source_overrides: &BTreeMap<PathBuf, String>,
|
||||
) -> Result<String> {
|
||||
if depth > 10 {
|
||||
return Err(ProxyError::Config("Include depth > 10".into()));
|
||||
@@ -39,15 +41,23 @@ pub(super) fn preprocess_includes(
|
||||
if let Some(rest) = rest.strip_prefix('=') {
|
||||
let path_str = rest.trim().trim_matches('"');
|
||||
let resolved = base_dir.join(path_str);
|
||||
source_files.insert(normalize_config_path(&resolved));
|
||||
let included = std::fs::read_to_string(&resolved)
|
||||
let normalized = normalize_config_path(&resolved);
|
||||
source_files.insert(normalized.clone());
|
||||
let included = source_overrides
|
||||
.get(&normalized)
|
||||
.cloned()
|
||||
.map(Ok)
|
||||
.unwrap_or_else(|| std::fs::read_to_string(&resolved))
|
||||
.map_err(|e| ProxyError::Config(e.to_string()))?;
|
||||
source_contents.insert(normalized, included.clone());
|
||||
let included_dir = resolved.parent().unwrap_or(base_dir);
|
||||
output.push_str(&preprocess_includes(
|
||||
&included,
|
||||
included_dir,
|
||||
depth + 1,
|
||||
source_files,
|
||||
source_contents,
|
||||
source_overrides,
|
||||
)?);
|
||||
output.push('\n');
|
||||
continue;
|
||||
|
||||
@@ -3,7 +3,9 @@ use tracing::warn;
|
||||
|
||||
use crate::error::{ProxyError, Result};
|
||||
|
||||
use super::super::types::{LoggingConfig, LoggingDestination, NetworkConfig, UpstreamType};
|
||||
use super::super::types::{
|
||||
LoggingConfig, LoggingDestination, NetworkConfig, SynLimitMode, UpstreamType,
|
||||
};
|
||||
use super::ProxyConfig;
|
||||
|
||||
pub(super) fn validate_network_cfg(net: &mut NetworkConfig) -> Result<()> {
|
||||
@@ -90,6 +92,77 @@ pub(super) fn validate_upstreams(config: &ProxyConfig) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn validate_listener_runtime_profiles(config: &ProxyConfig) -> Result<()> {
|
||||
for (index, listener) in config.server.listeners.iter().enumerate() {
|
||||
let supported = if cfg!(target_os = "linux") {
|
||||
matches!(
|
||||
listener.synlimit,
|
||||
SynLimitMode::Off | SynLimitMode::Iptables | SynLimitMode::Nftables
|
||||
)
|
||||
} else if cfg!(target_os = "freebsd") {
|
||||
matches!(listener.synlimit, SynLimitMode::Off | SynLimitMode::Pf)
|
||||
} else {
|
||||
listener.synlimit == SynLimitMode::Off
|
||||
};
|
||||
if !supported {
|
||||
let backend = match listener.synlimit {
|
||||
SynLimitMode::Off => "off",
|
||||
SynLimitMode::Iptables => "iptables",
|
||||
SynLimitMode::Nftables => "nftables",
|
||||
SynLimitMode::Pf => "pf",
|
||||
};
|
||||
let supported = if cfg!(target_os = "linux") {
|
||||
"off, iptables, nftables"
|
||||
} else if cfg!(target_os = "freebsd") {
|
||||
"off, pf"
|
||||
} else {
|
||||
"off"
|
||||
};
|
||||
return Err(ProxyError::Config(format!(
|
||||
"server.listeners[{index}].synlimit backend {backend} is unsupported on this platform; supported values: {supported}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let Some(bulk_mss) = config
|
||||
.server
|
||||
.client_mss_bulk_value()
|
||||
.map_err(|error| ProxyError::Config(format!("server.client_mss_bulk {error}")))?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
if !cfg!(target_os = "linux") {
|
||||
return Err(ProxyError::Config(
|
||||
"server.client_mss_bulk is supported only on Linux".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut participants = 0usize;
|
||||
for (index, listener) in config.server.listeners.iter().enumerate() {
|
||||
let handshake_mss = listener
|
||||
.effective_client_mss(&config.server)
|
||||
.map_err(|error| {
|
||||
ProxyError::Config(format!("server.listeners[{index}].client_mss {error}"))
|
||||
})?;
|
||||
let Some(handshake_mss) = handshake_mss else {
|
||||
continue;
|
||||
};
|
||||
participants = participants.saturating_add(1);
|
||||
if bulk_mss <= handshake_mss {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"server.client_mss_bulk ({bulk_mss}) must be greater than the effective handshake MSS ({handshake_mss}) for server.listeners[{index}]"
|
||||
)));
|
||||
}
|
||||
}
|
||||
if participants == 0 {
|
||||
return Err(ProxyError::Config(
|
||||
"server.client_mss_bulk requires an effective client_mss on at least one listener"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn normalize_upstream_family_policy(config: &mut ProxyConfig) {
|
||||
for (idx, upstream) in config.upstreams.iter_mut().enumerate() {
|
||||
if matches!(upstream.ipv4, Some(false)) && upstream.prefer == Some(4) {
|
||||
|
||||
@@ -5,5 +5,6 @@ pub mod hot_reload;
|
||||
mod load;
|
||||
mod types;
|
||||
|
||||
pub(crate) use load::{ConfigSourceGraph, LoadedConfig};
|
||||
pub use load::ProxyConfig;
|
||||
pub use types::*;
|
||||
|
||||
@@ -62,6 +62,7 @@ fn synlimit_synfix_defaults_are_loaded_for_listener() {
|
||||
assert_eq!(listener.synlimit_hashlimit_size, 32_768);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "freebsd")]
|
||||
#[test]
|
||||
fn synlimit_pf_mode_is_loaded_for_listener() {
|
||||
let cfg = load_config_from_temp_toml(
|
||||
@@ -82,6 +83,30 @@ fn synlimit_pf_mode_is_loaded_for_listener() {
|
||||
assert_eq!(cfg.server.listeners[0].synlimit, SynLimitMode::Pf);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "freebsd"))]
|
||||
#[test]
|
||||
fn synlimit_pf_mode_is_rejected_off_freebsd() {
|
||||
let toml = r#"
|
||||
[censorship]
|
||||
tls_domain = "example.com"
|
||||
|
||||
[access.users]
|
||||
user = "00000000000000000000000000000000"
|
||||
|
||||
[[server.listeners]]
|
||||
ip = "0.0.0.0"
|
||||
port = 443
|
||||
synlimit = "pf"
|
||||
"#;
|
||||
let dir = std::env::temp_dir();
|
||||
let path = dir.join("telemt_synlimit_pf_unsupported_test.toml");
|
||||
std::fs::write(&path, toml).unwrap();
|
||||
let err = ProxyConfig::load(&path).unwrap_err().to_string();
|
||||
|
||||
assert!(err.contains("backend pf is unsupported on this platform"));
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synlimit_synfix_zero_values_are_rejected() {
|
||||
for (field, expected) in [
|
||||
@@ -1742,11 +1767,12 @@ fn client_mss_presets_and_listener_override_are_resolved() {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn client_mss_custom_value_is_accepted() {
|
||||
let toml = r#"
|
||||
[server]
|
||||
client_mss = "4096"
|
||||
client_mss = "92"
|
||||
client_mss_bulk = "1400"
|
||||
|
||||
[censorship]
|
||||
@@ -1760,11 +1786,83 @@ fn client_mss_custom_value_is_accepted() {
|
||||
std::fs::write(&path, toml).unwrap();
|
||||
let cfg = ProxyConfig::load(&path).unwrap();
|
||||
|
||||
assert_eq!(cfg.server.client_mss_value(), Ok(Some(4096)));
|
||||
assert_eq!(cfg.server.client_mss_value(), Ok(Some(92)));
|
||||
assert_eq!(cfg.server.client_mss_bulk_value(), Ok(Some(1400)));
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn client_mss_bulk_requires_a_larger_bulk_profile_and_handshake_participant() {
|
||||
for (name, server, expected) in [
|
||||
(
|
||||
"without_handshake",
|
||||
"client_mss_bulk = \"1400\"",
|
||||
"requires an effective client_mss",
|
||||
),
|
||||
(
|
||||
"equal",
|
||||
"client_mss = \"1400\"\nclient_mss_bulk = \"1400\"",
|
||||
"must be greater than the effective handshake MSS",
|
||||
),
|
||||
(
|
||||
"inverted",
|
||||
"client_mss = \"1500\"\nclient_mss_bulk = \"1400\"",
|
||||
"must be greater than the effective handshake MSS",
|
||||
),
|
||||
] {
|
||||
let toml = format!(
|
||||
"[server]\n{server}\n\n[censorship]\ntls_domain = \"example.com\"\n\n[access.users]\nuser = \"00000000000000000000000000000000\"\n"
|
||||
);
|
||||
let dir = std::env::temp_dir();
|
||||
let path = dir.join(format!("telemt_client_mss_bulk_{name}_test.toml"));
|
||||
std::fs::write(&path, toml).unwrap();
|
||||
let err = ProxyConfig::load(&path).unwrap_err().to_string();
|
||||
|
||||
assert!(err.contains(expected), "unexpected error: {err}");
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn client_mss_bulk_allows_explicit_listener_opt_out() {
|
||||
let toml = r#"
|
||||
[server]
|
||||
client_mss = "92"
|
||||
client_mss_bulk = "1400"
|
||||
|
||||
[[server.listeners]]
|
||||
ip = "0.0.0.0"
|
||||
port = 443
|
||||
|
||||
[[server.listeners]]
|
||||
ip = "::"
|
||||
port = 443
|
||||
client_mss = ""
|
||||
|
||||
[censorship]
|
||||
tls_domain = "example.com"
|
||||
|
||||
[access.users]
|
||||
user = "00000000000000000000000000000000"
|
||||
"#;
|
||||
let dir = std::env::temp_dir();
|
||||
let path = dir.join("telemt_client_mss_bulk_listener_opt_out_test.toml");
|
||||
std::fs::write(&path, toml).unwrap();
|
||||
let cfg = ProxyConfig::load(&path).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
cfg.server.listeners[0].effective_client_mss(&cfg.server),
|
||||
Ok(Some(92))
|
||||
);
|
||||
assert_eq!(
|
||||
cfg.server.listeners[1].effective_client_mss(&cfg.server),
|
||||
Ok(None)
|
||||
);
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_mss_out_of_range_is_rejected() {
|
||||
for value in ["87", "4097"] {
|
||||
|
||||
+5
-5
@@ -1623,11 +1623,11 @@ pub struct ServerConfig {
|
||||
#[serde(default)]
|
||||
pub client_mss: Option<String>,
|
||||
|
||||
/// Client-facing TCP MSS for bulk traffic when initial-response fragmentation
|
||||
/// is enabled on Linux. The listener uses this MSS from connection setup;
|
||||
/// `client_mss` becomes the fragment size for the authenticated FakeTLS
|
||||
/// response. Empty/omitted keeps `client_mss` connection-wide. Uses the same
|
||||
/// preset/integer grammar as `client_mss`.
|
||||
/// Experimental Linux-only bulk MSS used with best-effort userspace
|
||||
/// chunking of the authenticated FakeTLS response. TCP offloads, loss, and
|
||||
/// retransmission may coalesce write boundaries. Empty or omitted keeps
|
||||
/// `client_mss` connection-wide. Uses the same preset/integer grammar as
|
||||
/// `client_mss`.
|
||||
#[serde(default)]
|
||||
pub client_mss_bulk: Option<String>,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user