use std::time::Instant;
use super::{h2::MAX_HEADER_LIST_SIZE, parser::H2Error};
const DEFAULT_MAX_RST_STREAM_PER_WINDOW: u32 = 2000;
const DEFAULT_MAX_RST_STREAM_LIFETIME: u64 = 200_000;
const DEFAULT_MAX_RST_STREAM_ABUSIVE_LIFETIME: u64 = 1000;
const DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME: u64 = 10_000;
const DEFAULT_MAX_PING_PER_WINDOW: u32 = 2000;
const DEFAULT_MAX_PING_LIFETIME: u32 = 200_000;
const DEFAULT_MAX_SETTINGS_PER_WINDOW: u32 = 1000;
const DEFAULT_MAX_SETTINGS_LIFETIME: u32 = 200_000;
const DEFAULT_MAX_EMPTY_DATA_PER_WINDOW: u32 = 2000;
const DEFAULT_MAX_WINDOW_UPDATE_STREAM0_PER_WINDOW: u32 = 2000;
const WINDOW_UPDATE_STREAM0_CREDIT_PER_DATA_FRAME: u64 = 2;
const DEFAULT_MAX_CONTINUATION_FRAMES: u32 = 20;
const DEFAULT_MAX_HEADER_TABLE_SIZE: u32 = 65536;
const DEFAULT_MAX_HEADER_FIELDS: u32 = 128;
const DEFAULT_STREAM_REFUSAL_PERCENT: u32 = 50;
const FLOOD_WINDOW_DURATION: std::time::Duration = std::time::Duration::from_secs(1);
const DEFAULT_MAX_GLITCH_COUNT: u32 = 2000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct H2FloodConfig {
max_rst_stream_per_window: u32,
max_ping_per_window: u32,
max_settings_per_window: u32,
max_empty_data_per_window: u32,
max_window_update_stream0_per_window: u32,
max_continuation_frames: u32,
max_glitch_count: u32,
max_rst_stream_lifetime: u64,
max_rst_stream_abusive_lifetime: u64,
max_rst_stream_emitted_lifetime: u64,
max_header_list_size: u32,
max_header_table_size: u32,
max_header_fields: u32,
stream_refusal_percent: u32,
}
impl Default for H2FloodConfig {
fn default() -> Self {
Self {
max_rst_stream_per_window: DEFAULT_MAX_RST_STREAM_PER_WINDOW,
max_ping_per_window: DEFAULT_MAX_PING_PER_WINDOW,
max_settings_per_window: DEFAULT_MAX_SETTINGS_PER_WINDOW,
max_empty_data_per_window: DEFAULT_MAX_EMPTY_DATA_PER_WINDOW,
max_window_update_stream0_per_window: DEFAULT_MAX_WINDOW_UPDATE_STREAM0_PER_WINDOW,
max_continuation_frames: DEFAULT_MAX_CONTINUATION_FRAMES,
max_glitch_count: DEFAULT_MAX_GLITCH_COUNT,
max_rst_stream_lifetime: DEFAULT_MAX_RST_STREAM_LIFETIME,
max_rst_stream_abusive_lifetime: DEFAULT_MAX_RST_STREAM_ABUSIVE_LIFETIME,
max_rst_stream_emitted_lifetime: DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME,
max_header_list_size: MAX_HEADER_LIST_SIZE as u32,
max_header_table_size: DEFAULT_MAX_HEADER_TABLE_SIZE,
max_header_fields: DEFAULT_MAX_HEADER_FIELDS,
stream_refusal_percent: DEFAULT_STREAM_REFUSAL_PERCENT,
}
}
}
impl H2FloodConfig {
#[allow(clippy::too_many_arguments)]
pub fn new(
max_rst_stream_per_window: u32,
max_ping_per_window: u32,
max_settings_per_window: u32,
max_empty_data_per_window: u32,
max_window_update_stream0_per_window: u32,
max_continuation_frames: u32,
max_glitch_count: u32,
max_rst_stream_lifetime: u64,
max_rst_stream_abusive_lifetime: u64,
max_rst_stream_emitted_lifetime: u64,
max_header_list_size: u32,
max_header_table_size: u32,
max_header_fields: u32,
stream_refusal_percent: u32,
) -> Self {
let config = Self {
max_rst_stream_per_window: max_rst_stream_per_window.max(1),
max_ping_per_window: max_ping_per_window.max(1),
max_settings_per_window: max_settings_per_window.max(1),
max_empty_data_per_window: max_empty_data_per_window.max(1),
max_window_update_stream0_per_window: max_window_update_stream0_per_window.max(1),
max_continuation_frames: max_continuation_frames.max(1),
max_glitch_count: max_glitch_count.max(1),
max_rst_stream_lifetime: max_rst_stream_lifetime.max(1),
max_rst_stream_abusive_lifetime: max_rst_stream_abusive_lifetime.max(1),
max_rst_stream_emitted_lifetime: max_rst_stream_emitted_lifetime.max(1),
max_header_list_size: max_header_list_size.max(1),
max_header_table_size: max_header_table_size.max(1),
max_header_fields: max_header_fields.max(1),
stream_refusal_percent,
};
debug_assert!(
config.max_rst_stream_per_window >= 1
&& config.max_ping_per_window >= 1
&& config.max_settings_per_window >= 1
&& config.max_empty_data_per_window >= 1
&& config.max_window_update_stream0_per_window >= 1
&& config.max_continuation_frames >= 1
&& config.max_glitch_count >= 1,
"every u32 flood threshold must be clamped to >= 1"
);
debug_assert!(
config.max_rst_stream_lifetime >= 1
&& config.max_rst_stream_abusive_lifetime >= 1
&& config.max_rst_stream_emitted_lifetime >= 1
&& config.max_header_list_size >= 1
&& config.max_header_table_size >= 1
&& config.max_header_fields >= 1,
"every lifetime/size flood threshold must be clamped to >= 1"
);
config
}
#[allow(clippy::too_many_arguments)]
pub fn from_optional(
max_rst_stream_per_window: Option<u32>,
max_ping_per_window: Option<u32>,
max_settings_per_window: Option<u32>,
max_empty_data_per_window: Option<u32>,
max_window_update_stream0_per_window: Option<u32>,
max_continuation_frames: Option<u32>,
max_glitch_count: Option<u32>,
max_rst_stream_lifetime: Option<u64>,
max_rst_stream_abusive_lifetime: Option<u64>,
max_rst_stream_emitted_lifetime: Option<u64>,
max_header_list_size: Option<u32>,
max_header_table_size: Option<u32>,
max_header_fields: Option<u32>,
stream_refusal_percent: Option<u32>,
) -> Self {
let defaults = Self::default();
Self::new(
max_rst_stream_per_window.unwrap_or(defaults.max_rst_stream_per_window),
max_ping_per_window.unwrap_or(defaults.max_ping_per_window),
max_settings_per_window.unwrap_or(defaults.max_settings_per_window),
max_empty_data_per_window.unwrap_or(defaults.max_empty_data_per_window),
max_window_update_stream0_per_window
.unwrap_or(defaults.max_window_update_stream0_per_window),
max_continuation_frames.unwrap_or(defaults.max_continuation_frames),
max_glitch_count.unwrap_or(defaults.max_glitch_count),
max_rst_stream_lifetime.unwrap_or(defaults.max_rst_stream_lifetime),
max_rst_stream_abusive_lifetime.unwrap_or(defaults.max_rst_stream_abusive_lifetime),
max_rst_stream_emitted_lifetime.unwrap_or(defaults.max_rst_stream_emitted_lifetime),
max_header_list_size.unwrap_or(defaults.max_header_list_size),
max_header_table_size.unwrap_or(defaults.max_header_table_size),
max_header_fields.unwrap_or(defaults.max_header_fields),
stream_refusal_percent.unwrap_or(defaults.stream_refusal_percent),
)
}
pub fn max_rst_stream_per_window(&self) -> u32 {
self.max_rst_stream_per_window
}
pub fn max_ping_per_window(&self) -> u32 {
self.max_ping_per_window
}
pub fn max_settings_per_window(&self) -> u32 {
self.max_settings_per_window
}
pub fn max_empty_data_per_window(&self) -> u32 {
self.max_empty_data_per_window
}
pub fn max_window_update_stream0_per_window(&self) -> u32 {
self.max_window_update_stream0_per_window
}
pub fn max_continuation_frames(&self) -> u32 {
self.max_continuation_frames
}
pub fn max_glitch_count(&self) -> u32 {
self.max_glitch_count
}
pub fn max_rst_stream_lifetime(&self) -> u64 {
self.max_rst_stream_lifetime
}
pub fn max_rst_stream_abusive_lifetime(&self) -> u64 {
self.max_rst_stream_abusive_lifetime
}
pub fn max_rst_stream_emitted_lifetime(&self) -> u64 {
self.max_rst_stream_emitted_lifetime
}
pub fn max_header_list_size(&self) -> u32 {
self.max_header_list_size
}
pub fn max_header_table_size(&self) -> u32 {
self.max_header_table_size
}
pub fn max_header_fields(&self) -> u32 {
self.max_header_fields
}
pub fn stream_refusal_percent(&self) -> u32 {
self.stream_refusal_percent
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct H2FloodViolation {
pub error: H2Error,
pub reason: &'static str,
pub metric_key: &'static str,
pub count: u64,
pub threshold: u64,
}
#[derive(Debug)]
pub(super) struct H2FloodDetector {
rst_stream_count: u32,
total_rst_received_lifetime: u64,
total_abusive_rst_received_lifetime: u64,
total_rst_streams_emitted_lifetime: u64,
streams_opened: u64,
window_update_stream0_credit: u64,
data_frames_recent: u64,
ping_count: u32,
total_ping_received_lifetime: u32,
settings_count: u32,
total_settings_received_lifetime: u32,
empty_data_count: u32,
window_update_stream0_count: u32,
continuation_count: u32,
accumulated_header_size: u32,
glitch_count: u32,
last_pre_response_rst_at: Option<Instant>,
window_start: Instant,
config: H2FloodConfig,
}
impl H2FloodDetector {
pub(super) fn new(config: H2FloodConfig, now: Instant) -> Self {
debug_assert!(
config.max_rst_stream_per_window >= 1
&& config.max_ping_per_window >= 1
&& config.max_settings_per_window >= 1
&& config.max_empty_data_per_window >= 1
&& config.max_window_update_stream0_per_window >= 1
&& config.max_continuation_frames >= 1
&& config.max_glitch_count >= 1,
"flood detector must be constructed with validated (>= 1) per-window thresholds"
);
debug_assert!(
config.max_rst_stream_lifetime >= 1
&& config.max_rst_stream_abusive_lifetime >= 1
&& config.max_rst_stream_emitted_lifetime >= 1
&& config.max_header_list_size >= 1
&& config.max_header_table_size >= 1
&& config.max_header_fields >= 1,
"flood detector must be constructed with validated (>= 1) lifetime/size thresholds"
);
let detector = Self {
rst_stream_count: 0,
total_rst_received_lifetime: 0,
total_abusive_rst_received_lifetime: 0,
total_rst_streams_emitted_lifetime: 0,
streams_opened: 0,
window_update_stream0_credit: 0,
data_frames_recent: 0,
ping_count: 0,
total_ping_received_lifetime: 0,
settings_count: 0,
total_settings_received_lifetime: 0,
empty_data_count: 0,
window_update_stream0_count: 0,
continuation_count: 0,
accumulated_header_size: 0,
glitch_count: 0,
last_pre_response_rst_at: None,
window_start: now,
config,
};
detector.debug_assert_invariants();
detector
}
pub(super) fn config(&self) -> H2FloodConfig {
self.config
}
pub(super) fn accumulated_header_size(&self) -> u32 {
self.accumulated_header_size
}
pub(super) fn total_rst_received_lifetime(&self) -> u64 {
self.total_rst_received_lifetime
}
pub(super) fn total_rst_streams_emitted_lifetime(&self) -> u64 {
self.total_rst_streams_emitted_lifetime
}
pub(super) fn record_stream_opened(&mut self) {
let before = self.streams_opened;
self.streams_opened = self.streams_opened.saturating_add(1);
debug_assert!(
self.streams_opened > before || before == u64::MAX,
"streams-opened counter must advance (or already be saturated)"
);
self.debug_assert_invariants();
}
pub(super) fn record_data_frames_sent(&mut self, frames: u64) {
let before = self.window_update_stream0_credit;
self.data_frames_recent = self.data_frames_recent.saturating_add(frames);
let cap = self.window_update_stream0_credit_cap();
self.window_update_stream0_credit = self
.window_update_stream0_credit
.saturating_add(frames.saturating_mul(WINDOW_UPDATE_STREAM0_CREDIT_PER_DATA_FRAME))
.min(cap);
debug_assert!(
self.window_update_stream0_credit >= before.min(cap),
"sending DATA must never take credit away below the cap"
);
debug_assert!(
self.window_update_stream0_credit <= cap,
"the stored stream-0 WINDOW_UPDATE credit must stay within its cap"
);
self.debug_assert_invariants();
}
fn window_update_stream0_credit_cap(&self) -> u64 {
u64::from(self.config.max_window_update_stream0_per_window)
.saturating_add(self.data_frames_recent)
.saturating_mul(WINDOW_UPDATE_STREAM0_CREDIT_PER_DATA_FRAME)
}
fn exceeds_floor_and_ratio(
count: u64,
floor: u64,
ratio_count: u64,
streams_opened: u64,
) -> bool {
count > floor && ratio_count > streams_opened
}
fn pre_response_resets(&self) -> u64 {
self.total_abusive_rst_received_lifetime
.saturating_add(self.total_rst_streams_emitted_lifetime)
}
pub(super) fn record_rst_received(
&mut self,
response_started: bool,
answered: bool,
now: Instant,
) -> Option<H2FloodViolation> {
debug_assert!(
response_started || !answered,
"a stream reset before its response is never an answered one"
);
if answered {
self.record_stream_opened();
}
if !response_started {
self.last_pre_response_rst_at = Some(now);
}
debug_assert!(
response_started || self.last_pre_response_rst_at == Some(now),
"a pre-response reset must stamp the soft-refusal clock"
);
self.record_rst_lifetime(response_started)
}
pub(super) fn refuses_new_streams(&self, now: Instant) -> bool {
let percent = u128::from(self.config.stream_refusal_percent);
if percent == 0 {
return false;
}
let recent = self
.last_pre_response_rst_at
.is_some_and(|at| now.saturating_duration_since(at) < FLOOD_WINDOW_DURATION);
let past_soft_floor = u128::from(self.total_abusive_rst_received_lifetime) * 100
> u128::from(self.config.max_rst_stream_abusive_lifetime) * percent;
let refuses = recent && past_soft_floor && self.pre_response_resets() > self.streams_opened;
debug_assert!(
!refuses || self.total_abusive_rst_received_lifetime > 0,
"a refusal needs at least one pre-response reset"
);
debug_assert!(
!refuses || self.last_pre_response_rst_at.is_some(),
"a refusal needs the time of the last pre-response reset"
);
refuses
}
fn record_rst_lifetime(&mut self, response_started: bool) -> Option<H2FloodViolation> {
let total_before = self.total_rst_received_lifetime;
let abusive_before = self.total_abusive_rst_received_lifetime;
self.total_rst_received_lifetime = self.total_rst_received_lifetime.saturating_add(1);
if !response_started {
self.total_abusive_rst_received_lifetime =
self.total_abusive_rst_received_lifetime.saturating_add(1);
}
debug_assert!(
self.total_rst_received_lifetime >= total_before,
"lifetime RST counter must be monotonic non-decreasing"
);
debug_assert_eq!(
self.total_abusive_rst_received_lifetime > abusive_before,
!response_started,
"abusive RST counter advances iff the RST is pre-response-start"
);
debug_assert!(
self.total_abusive_rst_received_lifetime <= self.total_rst_received_lifetime,
"abusive RST count is a subset of total received RST count"
);
self.debug_assert_invariants();
if Self::exceeds_floor_and_ratio(
self.total_rst_received_lifetime,
self.config.max_rst_stream_lifetime,
self.total_rst_received_lifetime
.saturating_sub(self.total_abusive_rst_received_lifetime),
self.streams_opened,
) {
return Some(H2FloodViolation {
error: H2Error::EnhanceYourCalm,
reason: "Rapid Reset: lifetime RST_STREAM",
metric_key: "h2.flood.violation.rst_stream_lifetime",
count: self.total_rst_received_lifetime,
threshold: self.config.max_rst_stream_lifetime,
});
}
if Self::exceeds_floor_and_ratio(
self.total_abusive_rst_received_lifetime,
self.config.max_rst_stream_abusive_lifetime,
self.pre_response_resets(),
self.streams_opened,
) {
return Some(H2FloodViolation {
error: H2Error::EnhanceYourCalm,
reason: "Rapid Reset: lifetime pre-response RST_STREAM",
metric_key: "h2.flood.violation.rst_stream_pre_response_lifetime",
count: self.total_abusive_rst_received_lifetime,
threshold: self.config.max_rst_stream_abusive_lifetime,
});
}
None
}
pub(super) fn record_rst_emitted(&mut self) -> Option<H2FloodViolation> {
let before = self.total_rst_streams_emitted_lifetime;
self.total_rst_streams_emitted_lifetime =
self.total_rst_streams_emitted_lifetime.saturating_add(1);
debug_assert!(
self.total_rst_streams_emitted_lifetime > before || before == u64::MAX,
"emitted-RST lifetime counter must advance (or already be saturated)"
);
self.debug_assert_invariants();
if Self::exceeds_floor_and_ratio(
self.total_rst_streams_emitted_lifetime,
self.config.max_rst_stream_emitted_lifetime,
self.pre_response_resets(),
self.streams_opened,
) {
return Some(H2FloodViolation {
error: H2Error::EnhanceYourCalm,
reason: "MadeYouReset: lifetime server-emitted RST_STREAM",
metric_key: "h2.flood.violation.rst_stream_emitted_lifetime",
count: self.total_rst_streams_emitted_lifetime,
threshold: self.config.max_rst_stream_emitted_lifetime,
});
}
None
}
pub(super) fn record_rst_stream_window(&mut self) {
let before = self.rst_stream_count;
self.rst_stream_count += 1;
debug_assert_eq!(
self.rst_stream_count,
before + 1,
"per-window RST_STREAM counter must advance by exactly one per inbound RST"
);
self.debug_assert_invariants();
}
pub(super) fn record_ping_frame(&mut self) {
let count_before = self.ping_count;
let lifetime_before = self.total_ping_received_lifetime;
self.ping_count += 1;
self.total_ping_received_lifetime = self.total_ping_received_lifetime.saturating_add(1);
debug_assert_eq!(
self.ping_count,
count_before + 1,
"per-window PING counter must advance by one per non-ACK PING"
);
debug_assert!(
self.total_ping_received_lifetime > lifetime_before || lifetime_before == u32::MAX,
"lifetime PING counter must advance (or already be saturated)"
);
self.debug_assert_invariants();
}
pub(super) fn record_settings_frame(&mut self) {
let count_before = self.settings_count;
let lifetime_before = self.total_settings_received_lifetime;
self.settings_count += 1;
self.total_settings_received_lifetime =
self.total_settings_received_lifetime.saturating_add(1);
debug_assert_eq!(
self.settings_count,
count_before + 1,
"per-window SETTINGS counter must advance by one per non-ACK SETTINGS"
);
debug_assert!(
self.total_settings_received_lifetime > lifetime_before || lifetime_before == u32::MAX,
"lifetime SETTINGS counter must advance (or already be saturated)"
);
self.debug_assert_invariants();
}
pub(super) fn record_empty_data_frame(&mut self) {
let before = self.empty_data_count;
self.empty_data_count += 1;
debug_assert_eq!(
self.empty_data_count,
before + 1,
"empty-DATA flood counter must advance by exactly one per empty frame"
);
self.debug_assert_invariants();
}
pub(super) fn record_window_update_stream0(&mut self) {
let count_before = self.window_update_stream0_count;
let credit_before = self.window_update_stream0_credit;
if self.window_update_stream0_credit > 0 {
self.window_update_stream0_credit -= 1;
} else {
self.window_update_stream0_count = self.window_update_stream0_count.saturating_add(1);
}
debug_assert_eq!(
credit_before > 0,
self.window_update_stream0_credit < credit_before,
"a credited stream-0 WINDOW_UPDATE must spend exactly one unit of credit"
);
debug_assert_eq!(
credit_before == 0,
self.window_update_stream0_count > count_before || count_before == u32::MAX,
"an unsolicited stream-0 WINDOW_UPDATE must advance the flood counter"
);
self.debug_assert_invariants();
}
#[cfg(test)]
pub(super) fn glitch_count(&self) -> u32 {
self.glitch_count
}
pub(super) fn record_glitch(&mut self) {
let before = self.glitch_count;
self.glitch_count += 1;
debug_assert!(
self.glitch_count > before || before == u32::MAX,
"glitch counter must advance (or already be saturated)"
);
self.debug_assert_invariants();
}
pub(super) fn record_continuation_frame(&mut self, payload_len: u32) {
let cont_count_before = self.continuation_count;
let acc_size_before = self.accumulated_header_size;
self.continuation_count += 1;
self.accumulated_header_size = self.accumulated_header_size.saturating_add(payload_len);
debug_assert_eq!(
self.continuation_count,
cont_count_before + 1,
"CONTINUATION per-block counter must advance by one per frame"
);
debug_assert!(
self.accumulated_header_size >= acc_size_before,
"accumulated header size must not shrink within a header block"
);
self.debug_assert_invariants();
}
pub(super) fn begin_header_block_if_new(&mut self, initial_fragment_len: u32) {
if self.continuation_count == 0 {
self.accumulated_header_size = initial_fragment_len;
}
self.debug_assert_invariants();
}
fn maybe_reset_window(&mut self, now: Instant) {
if now.saturating_duration_since(self.window_start) >= FLOOD_WINDOW_DURATION {
let (rst_before, ping_before, settings_before) =
(self.rst_stream_count, self.ping_count, self.settings_count);
let (empty_before, wu0_before, glitch_before) = (
self.empty_data_count,
self.window_update_stream0_count,
self.glitch_count,
);
self.rst_stream_count /= 2;
self.ping_count /= 2;
self.settings_count /= 2;
self.empty_data_count /= 2;
self.window_update_stream0_count /= 2;
self.data_frames_recent /= 2;
self.window_update_stream0_credit = self
.window_update_stream0_credit
.min(self.window_update_stream0_credit_cap());
self.glitch_count /= 2;
self.window_start = now;
debug_assert_eq!(self.rst_stream_count, rst_before / 2, "RST count halves");
debug_assert_eq!(self.ping_count, ping_before / 2, "PING count halves");
debug_assert_eq!(
self.settings_count,
settings_before / 2,
"SETTINGS count halves"
);
debug_assert_eq!(
self.empty_data_count,
empty_before / 2,
"empty-DATA count halves"
);
debug_assert_eq!(
self.window_update_stream0_count,
wu0_before / 2,
"stream-0 WINDOW_UPDATE count halves"
);
debug_assert_eq!(self.glitch_count, glitch_before / 2, "glitch count halves");
debug_assert!(
now.saturating_duration_since(self.window_start) < FLOOD_WINDOW_DURATION,
"window_start must be refreshed to the caller's now after decay"
);
}
self.debug_assert_invariants();
}
pub(super) fn check_flood(&mut self, now: Instant) -> Option<H2FloodViolation> {
self.maybe_reset_window(now);
fn flag(
reason: &'static str,
metric_key: &'static str,
count: u32,
threshold: u32,
) -> Option<H2FloodViolation> {
if count > threshold {
Some(H2FloodViolation {
error: H2Error::EnhanceYourCalm,
reason,
metric_key,
count: count as u64,
threshold: threshold as u64,
})
} else {
None
}
}
let violation = flag(
"RST_STREAM",
"h2.flood.violation.rst_stream_window",
self.rst_stream_count,
self.config.max_rst_stream_per_window,
)
.or_else(|| {
flag(
"PING",
"h2.flood.violation.ping_window",
self.ping_count,
self.config.max_ping_per_window,
)
})
.or_else(|| {
flag(
"PING lifetime",
"h2.flood.violation.ping_lifetime",
self.total_ping_received_lifetime,
DEFAULT_MAX_PING_LIFETIME,
)
})
.or_else(|| {
flag(
"SETTINGS",
"h2.flood.violation.settings_window",
self.settings_count,
self.config.max_settings_per_window,
)
})
.or_else(|| {
flag(
"SETTINGS lifetime",
"h2.flood.violation.settings_lifetime",
self.total_settings_received_lifetime,
DEFAULT_MAX_SETTINGS_LIFETIME,
)
})
.or_else(|| {
flag(
"empty DATA",
"h2.flood.violation.empty_data_window",
self.empty_data_count,
self.config.max_empty_data_per_window,
)
})
.or_else(|| {
flag(
"CONTINUATION",
"h2.flood.violation.continuation_per_block",
self.continuation_count,
self.config.max_continuation_frames,
)
})
.or_else(|| {
flag(
"WINDOW_UPDATE stream 0",
"h2.flood.violation.window_update_stream0_window",
self.window_update_stream0_count,
self.config.max_window_update_stream0_per_window,
)
})
.or_else(|| {
flag(
"accumulated header size",
"h2.flood.violation.header_size_per_block",
self.accumulated_header_size,
self.config.max_header_list_size,
)
})
.or_else(|| {
flag(
"glitch",
"h2.flood.violation.glitch_window",
self.glitch_count,
self.config.max_glitch_count,
)
});
debug_assert!(
violation
.as_ref()
.is_none_or(|v| v.error == H2Error::EnhanceYourCalm && v.count > v.threshold),
"a flood violation must be EnhanceYourCalm with count strictly above threshold"
);
violation
}
pub(super) fn reset_continuation(&mut self) {
self.continuation_count = 0;
self.accumulated_header_size = 0;
debug_assert_eq!(
self.continuation_count, 0,
"continuation_count must be zero after a block completes"
);
debug_assert_eq!(
self.accumulated_header_size, 0,
"accumulated_header_size must be zero after a block completes"
);
self.debug_assert_invariants();
}
#[cfg(debug_assertions)]
fn check_invariants(&self) {
debug_assert!(
self.total_abusive_rst_received_lifetime <= self.total_rst_received_lifetime,
"abusive RST count is a subset of total received RST count"
);
}
#[inline]
fn debug_assert_invariants(&self) {
#[cfg(debug_assertions)]
self.check_invariants();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn soft_refusal_config(abusive_floor: u64, percent: Option<u32>) -> H2FloodConfig {
H2FloodConfig::from_optional(
None,
None,
None,
None,
None,
None,
None,
None,
Some(abusive_floor),
None,
None,
None,
None,
percent,
)
}
#[test]
fn test_soft_refusal_engages_past_half_the_pre_response_floor() {
let base = Instant::now();
let mut detector = H2FloodDetector::new(soft_refusal_config(100, None), base);
for n in 1..=50 {
assert!(detector.record_rst_received(false, false, base).is_none());
assert!(
!detector.refuses_new_streams(base),
"{n} pre-response resets are not past half the floor"
);
}
assert!(detector.record_rst_received(false, false, base).is_none());
assert!(
detector.refuses_new_streams(base),
"51 pre-response resets are past half the floor of 100"
);
}
#[test]
fn test_soft_refusal_lifts_once_pre_response_resets_stop() {
let base = Instant::now();
let mut detector = H2FloodDetector::new(soft_refusal_config(100, None), base);
for _ in 0..60 {
assert!(detector.record_rst_received(false, false, base).is_none());
}
assert!(detector.refuses_new_streams(base), "premise: refusing");
let later = base + FLOOD_WINDOW_DURATION;
assert!(
!detector.refuses_new_streams(later),
"one window without a pre-response reset ends the refusal"
);
assert!(detector.record_rst_received(false, false, later).is_none());
assert!(
detector.refuses_new_streams(later),
"a new pre-response reset past the soft threshold refuses again"
);
}
#[test]
fn test_soft_refusal_follows_the_pre_response_ratio() {
let base = Instant::now();
let mut detector = H2FloodDetector::new(soft_refusal_config(100, None), base);
for _ in 0..90 {
detector.record_stream_opened();
}
for _ in 0..90 {
assert!(detector.record_rst_received(false, false, base).is_none());
}
assert!(
!detector.refuses_new_streams(base),
"90 pre-response resets against 90 answered streams are not refused"
);
assert!(detector.record_rst_received(false, false, base).is_none());
assert!(
detector.refuses_new_streams(base),
"one more pre-response reset than answered streams is"
);
}
#[test]
fn test_soft_refusal_leaves_the_hard_limit_unchanged() {
let base = Instant::now();
let floor = 100;
let mut detector = H2FloodDetector::new(soft_refusal_config(floor, None), base);
let mut tripped_at = None;
for n in 1..=floor + 1 {
if let Some(violation) = detector.record_rst_received(false, false, base) {
assert_eq!(violation.error, H2Error::EnhanceYourCalm);
assert_eq!(
violation.metric_key,
"h2.flood.violation.rst_stream_pre_response_lifetime"
);
tripped_at = Some(n);
break;
}
if n > floor / 2 {
assert!(detector.refuses_new_streams(base), "refusing at {n}");
}
}
assert_eq!(
tripped_at,
Some(floor + 1),
"the pre-response cap trips just past its floor, soft state or not"
);
let config = H2FloodConfig::default();
let mut detector = H2FloodDetector::new(config, base);
for _ in 0..=config.max_rst_stream_per_window() {
detector.record_rst_stream_window();
}
assert!(
matches!(
detector.check_flood(base),
Some(H2FloodViolation {
error: H2Error::EnhanceYourCalm,
metric_key: "h2.flood.violation.rst_stream_window",
..
})
),
"the RST_STREAM rate limit still trips past its threshold"
);
}
#[test]
fn test_soft_refusal_disabled_at_zero_and_inert_at_hundred() {
let base = Instant::now();
for percent in [0, 100, 250] {
let mut detector = H2FloodDetector::new(soft_refusal_config(100, Some(percent)), base);
for _ in 0..100 {
assert!(detector.record_rst_received(false, false, base).is_none());
}
assert!(
!detector.refuses_new_streams(base),
"percent {percent} must not refuse below the cap"
);
}
}
#[test]
fn test_soft_refusal_ignores_the_rst_rate() {
let base = Instant::now();
let config = H2FloodConfig::default();
let mut detector = H2FloodDetector::new(config, base);
for _ in 0..config.max_rst_stream_per_window() {
detector.record_rst_stream_window();
assert!(detector.record_rst_received(true, true, base).is_none());
}
assert!(
detector.check_flood(base).is_none(),
"premise: at the rate limit"
);
assert!(
!detector.refuses_new_streams(base),
"resets after a response, however fast, are not refused"
);
}
#[test]
fn test_flood_detector_no_flood_below_threshold() {
let base = Instant::now();
let config = H2FloodConfig::default();
let mut detector = H2FloodDetector::new(config, base);
assert!(detector.check_flood(base).is_none());
detector.rst_stream_count = config.max_rst_stream_per_window;
detector.ping_count = config.max_ping_per_window;
detector.settings_count = config.max_settings_per_window;
detector.empty_data_count = config.max_empty_data_per_window;
detector.continuation_count = config.max_continuation_frames;
detector.glitch_count = config.max_glitch_count;
assert!(detector.check_flood(base).is_none());
}
#[test]
fn test_flood_detector_detects_rapid_reset() {
let base = Instant::now();
let config = H2FloodConfig::default();
let mut detector = H2FloodDetector::new(config, base);
detector.rst_stream_count = config.max_rst_stream_per_window + 1;
assert!(matches!(
detector.check_flood(base),
Some(H2FloodViolation {
error: H2Error::EnhanceYourCalm,
..
})
));
}
#[test]
fn test_flood_detector_detects_ping_flood() {
let base = Instant::now();
let config = H2FloodConfig::default();
let mut detector = H2FloodDetector::new(config, base);
detector.ping_count = config.max_ping_per_window + 1;
assert!(matches!(
detector.check_flood(base),
Some(H2FloodViolation {
error: H2Error::EnhanceYourCalm,
..
})
));
}
#[test]
fn test_flood_detector_detects_settings_flood() {
let base = Instant::now();
let config = H2FloodConfig::default();
let mut detector = H2FloodDetector::new(config, base);
detector.settings_count = config.max_settings_per_window + 1;
assert!(matches!(
detector.check_flood(base),
Some(H2FloodViolation {
error: H2Error::EnhanceYourCalm,
..
})
));
}
#[test]
fn test_flood_detector_detects_empty_data_flood() {
let base = Instant::now();
let config = H2FloodConfig::default();
let mut detector = H2FloodDetector::new(config, base);
detector.empty_data_count = config.max_empty_data_per_window + 1;
assert!(matches!(
detector.check_flood(base),
Some(H2FloodViolation {
error: H2Error::EnhanceYourCalm,
..
})
));
}
#[test]
fn test_flood_detector_detects_continuation_flood() {
let base = Instant::now();
let config = H2FloodConfig::default();
let mut detector = H2FloodDetector::new(config, base);
detector.continuation_count = config.max_continuation_frames + 1;
assert!(matches!(
detector.check_flood(base),
Some(H2FloodViolation {
error: H2Error::EnhanceYourCalm,
..
})
));
}
#[test]
fn test_flood_detector_detects_header_size_flood() {
let base = Instant::now();
let config = H2FloodConfig::default();
let mut detector = H2FloodDetector::new(config, base);
detector.accumulated_header_size = MAX_HEADER_LIST_SIZE as u32 + 1;
assert!(matches!(
detector.check_flood(base),
Some(H2FloodViolation {
error: H2Error::EnhanceYourCalm,
..
})
));
}
#[test]
fn test_flood_detector_detects_glitch_flood() {
let base = Instant::now();
let config = H2FloodConfig::default();
let mut detector = H2FloodDetector::new(config, base);
detector.glitch_count = config.max_glitch_count + 1;
assert!(matches!(
detector.check_flood(base),
Some(H2FloodViolation {
error: H2Error::EnhanceYourCalm,
..
})
));
}
#[test]
fn test_flood_detector_custom_thresholds() {
let base = Instant::now();
let config = H2FloodConfig {
max_rst_stream_per_window: 5,
max_ping_per_window: 10,
max_settings_per_window: 3,
max_empty_data_per_window: 8,
max_continuation_frames: 2,
max_glitch_count: 15,
..H2FloodConfig::default()
};
let mut detector = H2FloodDetector::new(config, base);
detector.rst_stream_count = 5;
assert!(detector.check_flood(base).is_none());
detector.rst_stream_count = 6;
assert!(matches!(
detector.check_flood(base),
Some(H2FloodViolation {
error: H2Error::EnhanceYourCalm,
..
})
));
}
#[test]
fn test_flood_detector_reset_continuation() {
let base = Instant::now();
let config = H2FloodConfig::default();
let mut detector = H2FloodDetector::new(config, base);
detector.continuation_count = 15;
detector.accumulated_header_size = 30000;
detector.reset_continuation();
assert_eq!(detector.continuation_count, 0);
assert_eq!(detector.accumulated_header_size, 0);
}
#[test]
fn test_flood_detector_half_decay_on_window_expiry() {
let base = Instant::now();
let config = H2FloodConfig::default();
let mut detector = H2FloodDetector::new(config, base);
detector.rst_stream_count = 80;
detector.ping_count = 60;
detector.settings_count = 40;
detector.empty_data_count = 20;
detector.window_update_stream0_count = 90;
detector.glitch_count = 50;
let _ = detector.check_flood(base + FLOOD_WINDOW_DURATION);
assert_eq!(detector.rst_stream_count, 40);
assert_eq!(detector.ping_count, 30);
assert_eq!(detector.settings_count, 20);
assert_eq!(detector.empty_data_count, 10);
assert_eq!(detector.window_update_stream0_count, 45);
assert_eq!(detector.glitch_count, 25);
}
#[test]
fn test_flood_detector_window_update_stream0_trips_at_threshold() {
let base = Instant::now();
let config = H2FloodConfig {
max_window_update_stream0_per_window: 5,
..H2FloodConfig::default()
};
let mut detector = H2FloodDetector::new(config, base);
detector.window_update_stream0_count = 5;
assert!(detector.check_flood(base).is_none());
detector.window_update_stream0_count = 6;
let violation = detector
.check_flood(base)
.expect("WINDOW_UPDATE stream-0 flood must trip above threshold");
assert_eq!(violation.error, H2Error::EnhanceYourCalm);
assert_eq!(violation.reason, "WINDOW_UPDATE stream 0");
assert_eq!(
violation.metric_key,
"h2.flood.violation.window_update_stream0_window"
);
assert_eq!(violation.count, 6);
assert_eq!(violation.threshold, 5);
}
#[test]
fn test_flood_detector_window_update_stream0_honours_default() {
let detector = H2FloodDetector::new(H2FloodConfig::default(), Instant::now());
assert_eq!(
detector.config.max_window_update_stream0_per_window,
DEFAULT_MAX_WINDOW_UPDATE_STREAM0_PER_WINDOW
);
assert_eq!(detector.window_update_stream0_count, 0);
}
#[test]
fn test_flood_detector_decay_prevents_flood() {
let base = Instant::now();
let config = H2FloodConfig {
max_rst_stream_per_window: 10,
..H2FloodConfig::default()
};
let mut detector = H2FloodDetector::new(config, base);
detector.rst_stream_count = 12;
assert!(matches!(
detector.check_flood(base),
Some(H2FloodViolation {
error: H2Error::EnhanceYourCalm,
..
})
));
detector.rst_stream_count = 12;
assert!(detector.check_flood(base + FLOOD_WINDOW_DURATION).is_none());
}
#[test]
fn test_flood_detector_lifetime_rst_cap_triggers_enhance_your_calm() {
let mut detector = H2FloodDetector::new(H2FloodConfig::default(), Instant::now());
for _ in 0..DEFAULT_MAX_RST_STREAM_LIFETIME {
assert!(detector.record_rst_lifetime(true).is_none());
}
assert_eq!(
detector.total_rst_received_lifetime,
DEFAULT_MAX_RST_STREAM_LIFETIME
);
assert_eq!(detector.total_abusive_rst_received_lifetime, 0);
assert!(matches!(
detector.record_rst_lifetime(true),
Some(H2FloodViolation {
error: H2Error::EnhanceYourCalm,
..
})
));
}
#[test]
fn test_flood_detector_abusive_rst_cap_triggers_first() {
let mut detector = H2FloodDetector::new(H2FloodConfig::default(), Instant::now());
for _ in 0..DEFAULT_MAX_RST_STREAM_ABUSIVE_LIFETIME {
assert!(detector.record_rst_lifetime(false).is_none());
}
assert_eq!(
detector.total_abusive_rst_received_lifetime,
DEFAULT_MAX_RST_STREAM_ABUSIVE_LIFETIME
);
assert!(matches!(
detector.record_rst_lifetime(false),
Some(H2FloodViolation {
error: H2Error::EnhanceYourCalm,
..
})
));
}
#[test]
fn test_flood_detector_emitted_rst_below_threshold_is_clean() {
let mut detector = H2FloodDetector::new(H2FloodConfig::default(), Instant::now());
for _ in 0..DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME {
assert!(detector.record_rst_emitted().is_none());
}
assert_eq!(
detector.total_rst_streams_emitted_lifetime,
DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME
);
}
#[test]
fn test_flood_detector_emitted_rst_cap_triggers_made_you_reset() {
let mut detector = H2FloodDetector::new(H2FloodConfig::default(), Instant::now());
for _ in 0..DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME {
assert!(detector.record_rst_emitted().is_none());
}
let violation = detector
.record_rst_emitted()
.expect("emitting past the cap should produce a violation");
assert!(matches!(
violation,
H2FloodViolation {
error: H2Error::EnhanceYourCalm,
reason: "MadeYouReset: lifetime server-emitted RST_STREAM",
..
}
));
assert_eq!(violation.count, DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME + 1);
assert_eq!(violation.threshold, DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME);
}
#[test]
fn test_flood_detector_emitted_rst_counter_does_not_decay() {
let base = Instant::now();
let mut detector = H2FloodDetector::new(H2FloodConfig::default(), base);
for _ in 0..10 {
detector.record_rst_emitted();
}
let _ = detector.check_flood(base + FLOOD_WINDOW_DURATION);
assert_eq!(detector.total_rst_streams_emitted_lifetime, 10);
}
#[test]
fn test_flood_violation_metric_keys_are_unique_and_namespaced() {
fn key_from_rst_lifetime(response_started: bool) -> &'static str {
let mut detector = H2FloodDetector::new(H2FloodConfig::default(), Instant::now());
loop {
if let Some(v) = detector.record_rst_lifetime(response_started) {
return v.metric_key;
}
}
}
fn key_from_rst_emitted() -> &'static str {
let mut detector = H2FloodDetector::new(H2FloodConfig::default(), Instant::now());
loop {
if let Some(v) = detector.record_rst_emitted() {
return v.metric_key;
}
}
}
fn key_from_check_flood(setup: impl FnOnce(&mut H2FloodDetector)) -> &'static str {
let now = Instant::now();
let mut detector = H2FloodDetector::new(H2FloodConfig::default(), now);
setup(&mut detector);
detector
.check_flood(now)
.expect("setup should always trip a flood")
.metric_key
}
let keys: [&'static str; 13] = [
key_from_rst_lifetime(true),
key_from_rst_lifetime(false),
key_from_rst_emitted(),
key_from_check_flood(|d| d.rst_stream_count = u32::MAX),
key_from_check_flood(|d| d.ping_count = u32::MAX),
key_from_check_flood(|d| d.total_ping_received_lifetime = u32::MAX),
key_from_check_flood(|d| d.settings_count = u32::MAX),
key_from_check_flood(|d| d.total_settings_received_lifetime = u32::MAX),
key_from_check_flood(|d| d.empty_data_count = u32::MAX),
key_from_check_flood(|d| d.continuation_count = u32::MAX),
key_from_check_flood(|d| d.window_update_stream0_count = u32::MAX),
key_from_check_flood(|d| d.accumulated_header_size = u32::MAX),
key_from_check_flood(|d| d.glitch_count = u32::MAX),
];
for key in keys {
assert!(
key.starts_with("h2.flood.violation."),
"metric key {key} is missing the h2.flood.violation. prefix",
);
}
let mut deduped = keys.to_vec();
deduped.sort_unstable();
deduped.dedup();
assert_eq!(
deduped.len(),
keys.len(),
"metric keys must be unique across violation kinds; collisions: {keys:?}",
);
let mut published = crate::metrics::names::h2::FLOOD_VIOLATION_KEYS.to_vec();
published.sort_unstable();
assert_eq!(
deduped, published,
"names::h2::FLOOD_VIOLATION_KEYS (read by `sozu top`) must list exactly the emitted keys",
);
}
#[test]
fn test_flood_detector_response_started_rst_not_abusive() {
let mut detector = H2FloodDetector::new(H2FloodConfig::default(), Instant::now());
for _ in 0..(DEFAULT_MAX_RST_STREAM_ABUSIVE_LIFETIME + 100) {
assert!(detector.record_rst_lifetime(true).is_none());
}
assert_eq!(detector.total_abusive_rst_received_lifetime, 0);
assert_eq!(
detector.total_rst_received_lifetime,
DEFAULT_MAX_RST_STREAM_ABUSIVE_LIFETIME + 100
);
}
#[test]
fn test_flood_detector_minority_pre_response_cancels_never_trip() {
let mut detector = H2FloodDetector::new(H2FloodConfig::default(), Instant::now());
for stream in 0..100_000u64 {
detector.record_stream_opened();
if stream % 10 == 0 {
assert!(
detector.record_rst_lifetime(false).is_none(),
"cancel #{} of {} streams must not trip",
detector.total_abusive_rst_received_lifetime,
detector.streams_opened
);
}
}
assert_eq!(detector.total_abusive_rst_received_lifetime, 10_000);
}
#[test]
fn test_flood_detector_rapid_reset_trips_just_past_the_floor() {
let mut detector = H2FloodDetector::new(H2FloodConfig::default(), Instant::now());
for _ in 0..DEFAULT_MAX_RST_STREAM_ABUSIVE_LIFETIME {
assert!(detector.record_rst_lifetime(false).is_none());
}
let violation = detector
.record_rst_lifetime(false)
.expect("a reset of every opened stream must trip past the floor");
assert_eq!(
violation.metric_key,
"h2.flood.violation.rst_stream_pre_response_lifetime"
);
assert_eq!(violation.count, DEFAULT_MAX_RST_STREAM_ABUSIVE_LIFETIME + 1);
}
#[test]
fn test_flood_detector_pre_response_cap_is_half_of_streams_opened() {
let mut detector = H2FloodDetector::new(H2FloodConfig::default(), Instant::now());
for _ in 0..5000 {
detector.record_stream_opened();
}
for _ in 0..5000 {
assert!(detector.record_rst_lifetime(false).is_none());
}
assert!(
detector.record_rst_lifetime(false).is_some(),
"5001 pre-response resets beside 5000 answered streams is more than half"
);
}
#[test]
fn test_flood_detector_pre_response_cap_trips_only_above_half() {
let mut detector = H2FloodDetector::new(H2FloodConfig::default(), Instant::now());
for round in 0..10_000 {
for _ in 0..3 {
detector.record_stream_opened();
}
for _ in 0..2 {
assert!(
detector.record_rst_lifetime(false).is_none(),
"40% pre-response cancels must not trip (round {round})"
);
}
}
let mut flood = H2FloodDetector::new(H2FloodConfig::default(), Instant::now());
let mut tripped = false;
for _ in 0..10_000 {
for _ in 0..2 {
flood.record_stream_opened();
}
for _ in 0..3 {
tripped |= flood.record_rst_lifetime(false).is_some();
}
}
assert!(tripped, "60% pre-response cancels must trip past the floor");
}
#[test]
fn test_flood_detector_emitted_cap_scales_with_streams_opened() {
let mut detector = H2FloodDetector::new(H2FloodConfig::default(), Instant::now());
for stream in 0..400_000u64 {
detector.record_stream_opened();
if stream % 20 == 0 {
assert!(detector.record_rst_emitted().is_none());
}
}
assert_eq!(detector.total_rst_streams_emitted_lifetime, 20_000);
let mut flood = H2FloodDetector::new(H2FloodConfig::default(), Instant::now());
for _ in 0..DEFAULT_MAX_RST_STREAM_EMITTED_LIFETIME {
assert!(flood.record_rst_emitted().is_none());
}
assert!(
flood.record_rst_emitted().is_some(),
"one provoked reset per opened stream must trip past the floor"
);
}
#[test]
fn test_flood_detector_pre_response_caps_share_one_count() {
let config = H2FloodConfig {
max_rst_stream_abusive_lifetime: 10,
max_rst_stream_emitted_lifetime: 10,
..H2FloodConfig::default()
};
let mut detector = H2FloodDetector::new(config, Instant::now());
for _ in 0..100 {
detector.record_stream_opened();
}
let mut tripped = false;
for _ in 0..60 {
tripped |= detector.record_rst_lifetime(false).is_some();
tripped |= detector.record_rst_emitted().is_some();
}
assert!(
tripped,
"120 pre-response resets beside 100 answered streams is more than half"
);
}
#[test]
fn test_flood_detector_window_updates_answering_data_are_not_counted() {
let now = Instant::now();
let mut detector = H2FloodDetector::new(H2FloodConfig::default(), now);
for _ in 0..4096 {
detector.record_data_frames_sent(1);
detector.record_window_update_stream0();
assert!(detector.check_flood(now).is_none());
}
assert_eq!(detector.window_update_stream0_count, 0);
}
#[test]
fn test_flood_detector_chained_proxy_window_updates_are_not_counted() {
let now = Instant::now();
let mut detector = H2FloodDetector::new(H2FloodConfig::default(), now);
for _ in 0..10_000 {
detector.record_data_frames_sent(32);
detector.record_window_update_stream0();
}
assert!(detector.check_flood(now).is_none());
assert_eq!(detector.window_update_stream0_count, 0);
}
#[test]
fn test_flood_detector_unsolicited_window_updates_still_trip() {
let now = Instant::now();
let mut detector = H2FloodDetector::new(H2FloodConfig::default(), now);
for _ in 0..DEFAULT_MAX_WINDOW_UPDATE_STREAM0_PER_WINDOW {
detector.record_window_update_stream0();
assert!(detector.check_flood(now).is_none());
}
detector.record_window_update_stream0();
let violation = detector
.check_flood(now)
.expect("an unsolicited stream-0 WINDOW_UPDATE flood must trip");
assert_eq!(
violation.metric_key,
"h2.flood.violation.window_update_stream0_window"
);
}
#[test]
fn test_flood_detector_window_update_credit_is_spent_once() {
let now = Instant::now();
let mut detector = H2FloodDetector::new(H2FloodConfig::default(), now);
detector.record_data_frames_sent(10);
for _ in 0..(10 * WINDOW_UPDATE_STREAM0_CREDIT_PER_DATA_FRAME) {
detector.record_window_update_stream0();
}
assert_eq!(detector.window_update_stream0_count, 0);
assert_eq!(detector.window_update_stream0_credit, 0);
detector.record_window_update_stream0();
assert_eq!(detector.window_update_stream0_count, 1);
}
#[test]
fn test_flood_detector_window_update_credit_is_capped() {
let now = Instant::now();
let mut detector = H2FloodDetector::new(H2FloodConfig::default(), now);
detector.record_data_frames_sent(1_000_000);
assert_eq!(detector.window_update_stream0_credit, 2_000_000);
let mut at = now;
for _ in 0..32 {
at += FLOOD_WINDOW_DURATION;
assert!(detector.check_flood(at).is_none());
}
let floor = u64::from(DEFAULT_MAX_WINDOW_UPDATE_STREAM0_PER_WINDOW)
* WINDOW_UPDATE_STREAM0_CREDIT_PER_DATA_FRAME;
assert_eq!(detector.window_update_stream0_credit, floor);
for _ in 0..floor {
detector.record_window_update_stream0();
}
assert_eq!(detector.window_update_stream0_count, 0);
detector.record_window_update_stream0();
assert_eq!(detector.window_update_stream0_count, 1);
}
#[test]
fn test_flood_detector_window_update_credit_covers_data_in_flight() {
let now = Instant::now();
let config = H2FloodConfig {
max_window_update_stream0_per_window: 100,
..H2FloodConfig::default()
};
let mut detector = H2FloodDetector::new(config, now);
detector.record_data_frames_sent(960);
for _ in 0..960 {
detector.record_window_update_stream0();
}
assert!(detector.check_flood(now).is_none());
assert_eq!(detector.window_update_stream0_count, 0);
}
#[test]
fn test_flood_config_default_values() {
let config = H2FloodConfig::default();
assert_eq!(config.max_rst_stream_per_window, 2000);
assert_eq!(config.max_ping_per_window, 2000);
assert_eq!(config.max_settings_per_window, 1000);
assert_eq!(config.max_empty_data_per_window, 2000);
assert_eq!(config.max_window_update_stream0_per_window, 2000);
assert_eq!(config.max_continuation_frames, 20);
assert_eq!(config.max_header_fields, 128);
assert_eq!(config.max_glitch_count, 2000);
assert_eq!(config.max_rst_stream_lifetime, 200_000);
assert_eq!(config.max_rst_stream_abusive_lifetime, 1000);
assert_eq!(config.max_rst_stream_emitted_lifetime, 10_000);
assert_eq!(config.max_header_list_size, MAX_HEADER_LIST_SIZE as u32);
}
#[test]
fn test_flood_config_default_matches_constants() {
let config = H2FloodConfig::default();
assert_eq!(
config.max_rst_stream_per_window,
DEFAULT_MAX_RST_STREAM_PER_WINDOW
);
assert_eq!(config.max_ping_per_window, DEFAULT_MAX_PING_PER_WINDOW);
assert_eq!(
config.max_settings_per_window,
DEFAULT_MAX_SETTINGS_PER_WINDOW
);
assert_eq!(
config.max_empty_data_per_window,
DEFAULT_MAX_EMPTY_DATA_PER_WINDOW
);
assert_eq!(
config.max_continuation_frames,
DEFAULT_MAX_CONTINUATION_FRAMES
);
assert_eq!(config.max_glitch_count, DEFAULT_MAX_GLITCH_COUNT);
}
#[test]
fn test_flood_config_equality() {
let config_a = H2FloodConfig::default();
let config_b = H2FloodConfig::default();
assert_eq!(config_a, config_b);
let config_c = H2FloodConfig {
max_rst_stream_per_window: 1,
..H2FloodConfig::default()
};
assert_ne!(config_a, config_c);
}
#[test]
fn test_flood_config_from_optional_all_none_is_default() {
let config = H2FloodConfig::from_optional(
None, None, None, None, None, None, None, None, None, None, None, None, None, None,
);
assert_eq!(config, H2FloodConfig::default());
}
#[test]
fn test_flood_config_from_optional_maps_each_knob_to_its_own_field() {
let config = H2FloodConfig::from_optional(
Some(11),
Some(12),
Some(13),
Some(14),
Some(15),
Some(16),
Some(17),
Some(18),
Some(19),
Some(20),
Some(21),
Some(22),
Some(23),
Some(24),
);
assert_eq!(config.max_rst_stream_per_window, 11);
assert_eq!(config.max_ping_per_window, 12);
assert_eq!(config.max_settings_per_window, 13);
assert_eq!(config.max_empty_data_per_window, 14);
assert_eq!(config.max_window_update_stream0_per_window, 15);
assert_eq!(config.max_continuation_frames, 16);
assert_eq!(config.max_glitch_count, 17);
assert_eq!(config.max_rst_stream_lifetime, 18);
assert_eq!(config.max_rst_stream_abusive_lifetime, 19);
assert_eq!(config.max_rst_stream_emitted_lifetime, 20);
assert_eq!(config.max_header_list_size, 21);
assert_eq!(config.max_header_table_size, 22);
assert_eq!(config.max_header_fields, 23);
assert_eq!(config.stream_refusal_percent, 24);
}
#[test]
fn test_flood_config_from_optional_clamps_every_zero_to_one() {
let config = H2FloodConfig::from_optional(
Some(0),
Some(0),
Some(0),
Some(0),
Some(0),
Some(0),
Some(0),
Some(0),
Some(0),
Some(0),
Some(0),
Some(0),
Some(0),
Some(0),
);
assert_eq!(config.max_rst_stream_per_window, 1);
assert_eq!(config.max_ping_per_window, 1);
assert_eq!(config.max_settings_per_window, 1);
assert_eq!(config.max_empty_data_per_window, 1);
assert_eq!(config.max_window_update_stream0_per_window, 1);
assert_eq!(config.max_continuation_frames, 1);
assert_eq!(config.max_glitch_count, 1);
assert_eq!(config.max_rst_stream_lifetime, 1);
assert_eq!(config.max_rst_stream_abusive_lifetime, 1);
assert_eq!(config.max_rst_stream_emitted_lifetime, 1);
assert_eq!(config.max_header_list_size, 1);
assert_eq!(config.max_header_table_size, 1);
assert_eq!(config.max_header_fields, 1);
assert_eq!(config.stream_refusal_percent, 0);
}
#[test]
fn test_flood_config_accessors_read_their_own_field() {
let config = H2FloodConfig::from_optional(
Some(11),
Some(12),
Some(13),
Some(14),
Some(15),
Some(16),
Some(17),
Some(18),
Some(19),
Some(20),
Some(21),
Some(22),
Some(23),
Some(24),
);
assert_eq!(config.max_rst_stream_per_window(), 11);
assert_eq!(config.max_ping_per_window(), 12);
assert_eq!(config.max_settings_per_window(), 13);
assert_eq!(config.max_empty_data_per_window(), 14);
assert_eq!(config.max_window_update_stream0_per_window(), 15);
assert_eq!(config.max_continuation_frames(), 16);
assert_eq!(config.max_glitch_count(), 17);
assert_eq!(config.max_rst_stream_lifetime(), 18);
assert_eq!(config.max_rst_stream_abusive_lifetime(), 19);
assert_eq!(config.max_rst_stream_emitted_lifetime(), 20);
assert_eq!(config.max_header_list_size(), 21);
assert_eq!(config.max_header_table_size(), 22);
assert_eq!(config.max_header_fields(), 23);
assert_eq!(config.stream_refusal_percent(), 24);
}
}