Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_steel/src/srv/admin/guard.rs

4.8 KiB, 61 runs

created by r1870400018:10910, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Per-IP rate-limit / blacklist guard for Steel.
2//!
3//! A thin wrapper around `fe2o3_net::guard::addr::AddressGuard` that fixes the generic
4//! parameters to Steel's defaults and exposes a `new_shared` builder. Referenced from
5//! `AdminState`, fed by the TCP accept loop in `srv/server.rs`, and rendered by the admin
6//! dashboard's Security view.
7//!
8//! The guard is intentionally wired in the TCP accept path rather than deeper in the HTTPS
9//! handler so a blacklisted attacker costs the server only a SYN/ACK -- no TLS handshake,
10//! no HTTP parse, no application dispatch.
11//!
12//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
13//! Anthropic Claude
14
15use oxedyne_fe2o3_core::prelude::*;
16use oxedyne_fe2o3_hash::{
17 hash::HashScheme,
18 map::ShardMap,
19};
20use oxedyne_fe2o3_iop_hash::api::HashForm;
21use oxedyne_fe2o3_net::guard::addr::{
22 AddressGuard,
23 AddressLog,
24};
25
26use std::{
27 collections::BTreeMap,
28 sync::{
29 Arc,
30 atomic::AtomicUsize,
31 },
32 time::Duration,
33};
34
35pub const GUARD_SHARDS: usize = 16;
36pub const GUARD_RING: usize = 64;
37pub const GUARD_SALT_LEN: usize = 8;
38// Fixed salt bytes for the shard hasher, static because the guard map is
39// in-memory only.
40pub const GUARD_SALT: [u8; GUARD_SALT_LEN] = [
41 0x9a, 0x5b, 0x11, 0xe7, 0xaa, 0x3c, 0x80, 0x42,
42];
43
44// Guard thresholds an operator may override from the `addr_guard` config block.
45// The request ceiling is more permissive than shield's 30 because HTTP clients
46// burst heavily on page loads.
47pub const DEFAULT_RPS_MAX: u64 = 50;
48pub const DEFAULT_TINT_MIN: Duration = Duration::from_millis(100);
49pub const DEFAULT_TSUNSET_BASE: Duration = Duration::from_secs(60);
50pub const DEFAULT_TSUNSET_SPREAD: Duration = Duration::from_secs(240);
51pub const DEFAULT_BLIST_CNT: u16 = 6;
52
53pub const DEFAULT_SNAPSHOT_CAP: usize = 256;
54
55/// The caller-supplied extension payload is `()`: Steel does not need to carry
56/// shield-style proof-of-work negotiation on top of the state machine.
57pub type SteelAddressGuard = AddressGuard<
58 GUARD_SHARDS,
59 BTreeMap<HashForm, AddressLog<GUARD_RING, ()>>,
60 HashScheme,
61 GUARD_SALT_LEN,
62 GUARD_RING,
63 (),
64>;
65
66/// Deserialised from the `addr_guard` block in Steel's `ServerConfig` and applied
67/// when the guard is constructed at startup.
68///
69/// Every field has a meaningful default (the `DEFAULT_*` consts above), so a
70/// deployment that omits the `addr_guard` block altogether gets the same
71/// thresholds as the pre-config version of this module.
72#[derive(Clone, Debug)]
73pub struct AddrGuardSettings {
74 pub rps_max: u64, // average requests per second before downgrade to Throttle
75 pub tint_min: Duration, // minimum interval between allowed requests while throttled
76 pub tsunset_base: Duration, // base throttle cooldown
77 pub tsunset_spread: Duration, // jitter added to `tsunset_base`, spreading cooldown expiry
78 pub blist_cnt: u16, // throttle episodes before auto-blacklisting
79 pub conn_max: usize, // concurrent connections from one IP; 0 disables the cap
80 pub decay_after: Duration, // quiet spell before throttle history decays; 0 disables
81}
82
83impl Default for AddrGuardSettings {
84 fn default() -> Self {
85 Self {
86 rps_max: DEFAULT_RPS_MAX,
87 tint_min: DEFAULT_TINT_MIN,
88 tsunset_base: DEFAULT_TSUNSET_BASE,
89 tsunset_spread: DEFAULT_TSUNSET_SPREAD,
90 blist_cnt: DEFAULT_BLIST_CNT,
91 conn_max: 0, // inert until a deployment opts in
92 decay_after: Duration::ZERO, // no decay until a deployment opts in
93 }
94 }
95}
96
97pub fn new_shared() -> Outcome<Arc<SteelAddressGuard>> {
98 new_shared_with(AddrGuardSettings::default())
99}
100
101/// The shard count, ring length and hasher salt are fixed compile-time
102/// parameters; only the runtime thresholds are operator adjustable.
103pub fn new_shared_with(settings: AddrGuardSettings) -> Outcome<Arc<SteelAddressGuard>> {
104 let amap = res!(ShardMap::<
105 GUARD_SHARDS,
106 GUARD_SALT_LEN,
107 AddressLog<GUARD_RING, ()>,
108 BTreeMap<HashForm, AddressLog<GUARD_RING, ()>>,
109 HashScheme,
110 >::new(
111 GUARD_SHARDS as u32,
112 GUARD_SALT,
113 BTreeMap::new(),
114 res!(HashScheme::try_from("Seahash")),
115 ));
116 let guard = AddressGuard {
117 amap,
118 arps_max: settings.rps_max,
119 tint_min: settings.tint_min,
120 tsunset_base: settings.tsunset_base,
121 tsunset_spread: settings.tsunset_spread,
122 blist_cnt: settings.blist_cnt,
123 conn_max: settings.conn_max,
124 live_total: Arc::new(AtomicUsize::new(0)),
125 decay_after: settings.decay_after,
126 };
127 Ok(Arc::new(guard))
128}