Synlimit V2

This commit is contained in:
Alexey
2026-06-28 12:48:43 +03:00
parent 1ee9a234d7
commit 558f352a57
11 changed files with 1268 additions and 34 deletions
+28 -3
View File
@@ -54,9 +54,14 @@ const DEFAULT_CONNTRACK_CONTROL_ENABLED: bool = true;
const DEFAULT_CONNTRACK_PRESSURE_HIGH_WATERMARK_PCT: u8 = 85;
const DEFAULT_CONNTRACK_PRESSURE_LOW_WATERMARK_PCT: u8 = 70;
const DEFAULT_CONNTRACK_DELETE_BUDGET_PER_SEC: u64 = 4096;
const DEFAULT_SYNLIMIT_SECONDS: u32 = 1;
const DEFAULT_SYNLIMIT_HITCOUNT: u32 = 1;
const DEFAULT_SYNLIMIT_BURST: u32 = 2;
const DEFAULT_SYNLIMIT_SECONDS: u32 = 60;
const DEFAULT_SYNLIMIT_HITCOUNT: u32 = 48;
const DEFAULT_SYNLIMIT_BURST: u32 = 1;
const DEFAULT_SYNLIMIT_IOS_SECONDS: u32 = 1;
const DEFAULT_SYNLIMIT_IOS_HITCOUNT: u32 = 12;
const DEFAULT_SYNLIMIT_IOS_BURST: u32 = 24;
const DEFAULT_SYNLIMIT_HASHLIMIT_EXPIRE_MS: u32 = 60_000;
const DEFAULT_SYNLIMIT_HASHLIMIT_SIZE: u32 = 32_768;
const DEFAULT_UPSTREAM_CONNECT_RETRY_ATTEMPTS: u32 = 2;
const DEFAULT_UPSTREAM_UNHEALTHY_FAIL_THRESHOLD: u32 = 5;
const DEFAULT_UPSTREAM_CONNECT_BUDGET_MS: u64 = 3000;
@@ -258,6 +263,26 @@ pub(crate) fn default_synlimit_burst() -> u32 {
DEFAULT_SYNLIMIT_BURST
}
pub(crate) fn default_synlimit_ios_seconds() -> u32 {
DEFAULT_SYNLIMIT_IOS_SECONDS
}
pub(crate) fn default_synlimit_ios_hitcount() -> u32 {
DEFAULT_SYNLIMIT_IOS_HITCOUNT
}
pub(crate) fn default_synlimit_ios_burst() -> u32 {
DEFAULT_SYNLIMIT_IOS_BURST
}
pub(crate) fn default_synlimit_hashlimit_expire_ms() -> u32 {
DEFAULT_SYNLIMIT_HASHLIMIT_EXPIRE_MS
}
pub(crate) fn default_synlimit_hashlimit_size() -> u32 {
DEFAULT_SYNLIMIT_HASHLIMIT_SIZE
}
pub(crate) fn default_prefer_4() -> u8 {
4
}
+60
View File
@@ -145,6 +145,11 @@ pub struct ListenerSynLimitHotFields {
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 {
@@ -293,6 +298,11 @@ impl ListenerSynLimitHotFields {
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,
}
}
}
@@ -620,6 +630,11 @@ fn overlay_listener_synlimit_fields(old: &mut [ListenerConfig], new: &[ListenerC
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;
}
}
@@ -1710,6 +1725,51 @@ mod tests {
assert!(!config_equal(&applied, &new));
}
#[test]
fn listener_synlimit_extended_fields_are_hot() {
let mut old = sample_config();
old.server.listeners.push(ListenerConfig {
ip: "0.0.0.0".parse().unwrap(),
port: Some(443),
client_mss: None,
synlimit: SynLimitMode::Iptables,
synlimit_seconds: 60,
synlimit_hitcount: 48,
synlimit_burst: 1,
synlimit_ios_seconds: 1,
synlimit_ios_hitcount: 12,
synlimit_ios_burst: 24,
synlimit_hashlimit_expire_ms: 60_000,
synlimit_hashlimit_size: 32_768,
announce: None,
announce_ip: None,
proxy_protocol: None,
reuse_allow: false,
});
let mut new = old.clone();
new.server.port = 8443;
new.server.listeners[0].synlimit_seconds = 120;
new.server.listeners[0].synlimit_hitcount = 96;
new.server.listeners[0].synlimit_burst = 2;
new.server.listeners[0].synlimit_ios_seconds = 2;
new.server.listeners[0].synlimit_ios_hitcount = 18;
new.server.listeners[0].synlimit_ios_burst = 36;
new.server.listeners[0].synlimit_hashlimit_expire_ms = 90_000;
new.server.listeners[0].synlimit_hashlimit_size = 65_536;
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);
}
#[test]
fn reload_applies_hot_change_on_first_observed_snapshot() {
let initial_tag = "11111111111111111111111111111111";
+112
View File
@@ -352,6 +352,11 @@ const LISTENER_CONFIG_KEYS: &[&str] = &[
"synlimit_seconds",
"synlimit_hitcount",
"synlimit_burst",
"synlimit_ios_seconds",
"synlimit_ios_hitcount",
"synlimit_ios_burst",
"synlimit_hashlimit_expire_ms",
"synlimit_hashlimit_size",
"announce",
"announce_ip",
"proxy_protocol",
@@ -2014,6 +2019,31 @@ impl ProxyConfig {
"server.listeners[{idx}].synlimit_burst must be > 0"
)));
}
if listener.synlimit_ios_seconds == 0 {
return Err(ProxyError::Config(format!(
"server.listeners[{idx}].synlimit_ios_seconds must be > 0"
)));
}
if listener.synlimit_ios_hitcount == 0 {
return Err(ProxyError::Config(format!(
"server.listeners[{idx}].synlimit_ios_hitcount must be > 0"
)));
}
if listener.synlimit_ios_burst == 0 {
return Err(ProxyError::Config(format!(
"server.listeners[{idx}].synlimit_ios_burst must be > 0"
)));
}
if listener.synlimit_hashlimit_expire_ms == 0 {
return Err(ProxyError::Config(format!(
"server.listeners[{idx}].synlimit_hashlimit_expire_ms must be > 0"
)));
}
if listener.synlimit_hashlimit_size == 0 {
return Err(ProxyError::Config(format!(
"server.listeners[{idx}].synlimit_hashlimit_size must be > 0"
)));
}
}
if config.server.accept_permit_timeout_ms > 60_000 {
@@ -2256,6 +2286,11 @@ impl ProxyConfig {
synlimit_seconds: default_synlimit_seconds(),
synlimit_hitcount: default_synlimit_hitcount(),
synlimit_burst: default_synlimit_burst(),
synlimit_ios_seconds: default_synlimit_ios_seconds(),
synlimit_ios_hitcount: default_synlimit_ios_hitcount(),
synlimit_ios_burst: default_synlimit_ios_burst(),
synlimit_hashlimit_expire_ms: default_synlimit_hashlimit_expire_ms(),
synlimit_hashlimit_size: default_synlimit_hashlimit_size(),
announce: None,
announce_ip: None,
proxy_protocol: None,
@@ -2273,6 +2308,11 @@ impl ProxyConfig {
synlimit_seconds: default_synlimit_seconds(),
synlimit_hitcount: default_synlimit_hitcount(),
synlimit_burst: default_synlimit_burst(),
synlimit_ios_seconds: default_synlimit_ios_seconds(),
synlimit_ios_hitcount: default_synlimit_ios_hitcount(),
synlimit_ios_burst: default_synlimit_ios_burst(),
synlimit_hashlimit_expire_ms: default_synlimit_hashlimit_expire_ms(),
synlimit_hashlimit_size: default_synlimit_hashlimit_size(),
announce: None,
announce_ip: None,
proxy_protocol: None,
@@ -2466,6 +2506,78 @@ mod tests {
error
}
#[test]
fn synlimit_synfix_defaults_are_loaded_for_listener() {
let cfg = load_config_from_temp_toml(
r#"
[censorship]
tls_domain = "example.com"
[access.users]
user = "00000000000000000000000000000000"
[[server.listeners]]
ip = "0.0.0.0"
port = 443
synlimit = "iptables"
"#,
);
let listener = &cfg.server.listeners[0];
assert_eq!(listener.synlimit_seconds, 60);
assert_eq!(listener.synlimit_hitcount, 48);
assert_eq!(listener.synlimit_burst, 1);
assert_eq!(listener.synlimit_ios_seconds, 1);
assert_eq!(listener.synlimit_ios_hitcount, 12);
assert_eq!(listener.synlimit_ios_burst, 24);
assert_eq!(listener.synlimit_hashlimit_expire_ms, 60_000);
assert_eq!(listener.synlimit_hashlimit_size, 32_768);
}
#[test]
fn synlimit_synfix_zero_values_are_rejected() {
for (field, expected) in [
(
"synlimit_ios_seconds",
"server.listeners[0].synlimit_ios_seconds must be > 0",
),
(
"synlimit_ios_hitcount",
"server.listeners[0].synlimit_ios_hitcount must be > 0",
),
(
"synlimit_ios_burst",
"server.listeners[0].synlimit_ios_burst must be > 0",
),
(
"synlimit_hashlimit_expire_ms",
"server.listeners[0].synlimit_hashlimit_expire_ms must be > 0",
),
(
"synlimit_hashlimit_size",
"server.listeners[0].synlimit_hashlimit_size must be > 0",
),
] {
let toml = format!(
r#"
[censorship]
tls_domain = "example.com"
[access.users]
user = "00000000000000000000000000000000"
[[server.listeners]]
ip = "0.0.0.0"
port = 443
synlimit = "iptables"
{field} = 0
"#
);
let error = load_config_error_from_temp_toml(&toml);
assert!(error.contains(expected), "{field}: {error}");
}
}
#[test]
fn serde_defaults_remain_unchanged_for_present_sections() {
let toml = r#"
+20 -5
View File
@@ -1455,9 +1455,9 @@ pub enum SynLimitMode {
/// Disable SYN limiting for this listener.
#[default]
Off,
/// Use iptables/ip6tables filter rules with the hashlimit match.
/// Use iptables/ip6tables two-tier SYN-fix rules with the hashlimit match.
Iptables,
/// Use nftables rules with per-source token-bucket meters.
/// Use nftables two-tier SYN-fix rules with per-source token-bucket meters.
Nftables,
}
@@ -2266,15 +2266,30 @@ pub struct ListenerConfig {
/// Per-listener SYN limiter mode.
#[serde(default)]
pub synlimit: SynLimitMode,
/// Token-bucket rate interval for the per-listener SYN limiter.
/// Generic SYN-fix token-bucket rate interval.
#[serde(default = "default_synlimit_seconds")]
pub synlimit_seconds: u32,
/// Token-bucket rate amount for the per-listener SYN limiter.
/// Generic SYN-fix token-bucket rate amount.
#[serde(default = "default_synlimit_hitcount")]
pub synlimit_hitcount: u32,
/// Token-bucket burst size for the per-listener SYN limiter.
/// Generic SYN-fix token-bucket burst size.
#[serde(default = "default_synlimit_burst")]
pub synlimit_burst: u32,
/// iOS-like SYN-fix token-bucket rate interval.
#[serde(default = "default_synlimit_ios_seconds")]
pub synlimit_ios_seconds: u32,
/// iOS-like SYN-fix token-bucket rate amount.
#[serde(default = "default_synlimit_ios_hitcount")]
pub synlimit_ios_hitcount: u32,
/// iOS-like SYN-fix token-bucket burst size.
#[serde(default = "default_synlimit_ios_burst")]
pub synlimit_ios_burst: u32,
/// Hashlimit entry expiration in milliseconds for iptables/ip6tables rules.
#[serde(default = "default_synlimit_hashlimit_expire_ms")]
pub synlimit_hashlimit_expire_ms: u32,
/// Hashlimit table size for iptables/ip6tables rules.
#[serde(default = "default_synlimit_hashlimit_size")]
pub synlimit_hashlimit_size: u32,
/// IP address or hostname to announce in proxy links.
/// Takes precedence over `announce_ip` if both are set.
#[serde(default)]