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 | |
| 15 | use oxedyne_fe2o3_core::prelude::*; |
| 16 | use oxedyne_fe2o3_hash::{ |
| 17 | hash::HashScheme, |
| 18 | map::ShardMap, |
| 19 | }; |
| 20 | use oxedyne_fe2o3_iop_hash::api::HashForm; |
| 21 | use oxedyne_fe2o3_net::guard::addr::{ |
| 22 | AddressGuard, |
| 23 | AddressLog, |
| 24 | }; |
| 25 | |
| 26 | use std::{ |
| 27 | collections::BTreeMap, |
| 28 | sync::{ |
| 29 | Arc, |
| 30 | atomic::AtomicUsize, |
| 31 | }, |
| 32 | time::Duration, |
| 33 | }; |
| 34 | |
| 35 | pub const GUARD_SHARDS: usize = 16; |
| 36 | pub const GUARD_RING: usize = 64; |
| 37 | pub const GUARD_SALT_LEN: usize = 8; |
| 38 | // Fixed salt bytes for the shard hasher, static because the guard map is |
| 39 | // in-memory only. |
| 40 | pub 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. |
| 47 | pub const DEFAULT_RPS_MAX: u64 = 50; |
| 48 | pub const DEFAULT_TINT_MIN: Duration = Duration::from_millis(100); |
| 49 | pub const DEFAULT_TSUNSET_BASE: Duration = Duration::from_secs(60); |
| 50 | pub const DEFAULT_TSUNSET_SPREAD: Duration = Duration::from_secs(240); |
| 51 | pub const DEFAULT_BLIST_CNT: u16 = 6; |
| 52 | |
| 53 | pub 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. |
| 57 | pub 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)] |
| 73 | pub 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 | |
| 83 | impl 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 | |
| 97 | pub 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. |
| 103 | pub 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 | } |