Proof of concept mechanical port of ircnet/ircd to Rust as part of a bit about C being insecure for network services
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(leveva): per-user KNOCK throttle (charybdis knock_delay) — P11 slice 209

Closes the last documented gap in the KNOCK throttle family (slices 198/199/200).
Slice 200's per-channel cooldown stops a chorus spamming one channel's operators;
it did nothing about one user spraying KNOCKs across many *fresh* channels. This
adds charybdis's per-user `knock_delay` (default 300s).

- Reuse `KnockThrottle` as a generic per-key cooldown; second `ServerContext`
field `knock_user_throttle` keyed by the knocker's UID (folds injectively).
- New `cooling`/`arm` peek/arm split: the command peeks both throttles and arms
neither unless the knock is delivered (charybdis updates its clocks on success
only). `admit` retained; a unit test pins `admit == cooling-then-arm`.
- `KNOCK_DELAY_USER_SECS = 300` const beside `KNOCK_DELAY_CHANNEL_SECS`.
- Command gate answers `712 ERR_TOOMANYKNOCK` if either window is cooling,
notifying nobody; per-user peeked first.

Tests: store units (cooling-does-not-arm, window-not-permanent, admit==split);
command units (second knock to a fresh channel is 712 with no 710; per-user is
per-user not global; per-channel test rewritten with distinct knockers); new
`knock_user_throttle_proptest.rs` (clock-tracking + arbitrary-key totality).
New `testutil::run_as` dispatches as a named client. `knock_user_throttle`
added to the 60 hand-rolled ServerContext test literals (slice-206 churn).

Gate: cargo test -p leveva green (1949 lib + all knock proptests/golden/s2s);
clippy -p leveva --tests clean; build --workspace 0 warnings.

+479 -19
+1 -1
PLAN.md
··· 90 90 | **P8 — Delete the C + retire the oracle harness ✅ DONE** (per-sub-phase: [`PLAN-P8-progress.md`](PLAN-P8-progress.md) + [`docs/progress-log/p8.md`](docs/progress-log/p8.md)) | remaining C (the ~25 variadic sender trampolines + residual data globals) + `ircd-sys` C build + `ircd-golden` (L2) + `ircd-testkit`/`cref_*` archives (L1) + the frozen reference‑C tree | With all C _logic_ already Rust (P7 exit), delete the last C: replace the variadic sender trampolines with a non‑variadic Rust sender API at every call site (faithful `wire!`/`reply_one!`/`format!` first; leveva typed messages adopted alongside the IRC senders), drop the residual data globals via the data‑symbol seam (bindgen `extern` decls resolve to `#[no_mangle]` Rust defs at link), then drop **all** C compilation from `ircd-sys` (no more `cc::Build`), retire the differential harness (`cref_*`/L1 `ircd-testkit`/`ircd-golden` L2 can no longer be built once the oracle is gone), and **migrate the load‑bearing tests into `ircd-common`** as self‑contained Rust tests (own fixtures, no `cref_` oracle). **Progress (P8a–P8r — full entries in the progress logs):** **every variadic sender trampoline is now Rust** (P8a–P8m, incl. the keystones `sendto_one`/`sendto_flag`/`esendto_*`), and the five data‑global `.o`s are dropped outright (`s_auth.o` iauth globals P8n, `send.o` P8o, `s_bsd.o` `local[]`/`highest_fd`/… P8p, `ircd.o` `me`/`client`/timers/… P8q). **P8r dropped the last C‑logic TU `support.o`** — `dgets`/`make_isupport` ported to Rust + the `ipv6string`/`minus_one` data globals defined as `#[no_mangle]` statics (`snprintf_append` is dead in Rust — WHOX is field‑by‑field — so it died with the `.o`; `irc_sprintf` is deleted not ported, in P11). **Every ircd C TU is now Rust:** the link set holds only the generated `version.o` + the L1 harness's `ctruth.o`. **P8s migrated all 115 L1 `cref_` differentials into self‑contained `ircd-common` `insta` snapshot tests** (drive only the Rust port via `link_anchor()`, no oracle; soundness via the capture chain) — the suite is green + 0 warnings. **P8t ported the last *generated* C TU `version.c` to Rust** (`ircd-common/src/version.rs`): the data globals `generation`/`creation`/`pass_version`/`infotext`/`isupport` are now `#[no_mangle]` statics via the data‑symbol seam — `generation`/`creation` sourced from crate metadata at build time (`CARGO_PKG_VERSION` + an `ircd-common/build.rs` UTC build stamp), `pass_version`/`infotext` copied verbatim. `version.c.SH` generation + the `version.o` compile/archive are gone from `ircd-sys/build.rs`; `libircd_c.a` now holds **only** the L1 harness `ctruth.o`. (`creation` is now genuinely build‑time volatile, so the golden canonicalizer masks RPL_CREATED 003, matching the existing 371 Birth‑Date rule.) **P8u (FINAL) retired the oracle.** Ran the differential suite a last time green — L1 `ircd-testkit` all pass; L2 `ircd-golden` 86 pass + only the documented reference‑C‑garbage `s_serv_stats` flake (where Rust is the *correct* side) — then **dropped all C compilation** from `ircd-sys/build.rs`: the `make` object build, the `cref_*`/`ctruth.o`/`res.o` recompiles, the `libircd_c.a` archive, and the whole‑archive link + `-lz`/`-lm`/`-lcrypt` (none needed by Rust — `pow` resolves from glibc 2.29+). `build.rs` now only runs `configure` + bindgen over the C *headers* (the struct view `ircd-common` still uses until P9–P12) and expands the install‑path Makefile vars. `ircd-testkit` (L1) + `ircd-golden` (L2) are `exclude`d from the workspace (mothballed, git‑recoverable); the `ctruth.c`/`layout.rs` drift‑net is deleted. **The workspace is now 100% Rust — zero C TUs compiled.** **P8v (literal end of C) deleted the C *source tree* itself + retired `ircd-sys`:** froze the generated `bindings.rs` into a committed `ircd-common/src/bindings.rs` (a self‑alias `extern crate self as ircd_sys;` keeps every `ircd_sys::bindings::*` path resolving, zero src churn) + the install‑path consts as literals, then deleted `ircd-sys`, the mothballed `ircd-testkit`/`ircd-golden` oracle crates, the `iauth-rs` C‑iauth differential, and **all** C source (`common/ ircd/ iauth/ support/ contrib/ cbuild/ configure .clang-format* clangformat.yml`). `ircd-rs` calls `ircd_common::ircd::c_ircd_main` directly. **Not one line of C remains in the repo.** | **MET. Final differential run green; oracle mothballed; `cargo build --workspace` 0 warnings links pure Rust; `cargo test --workspace` 695 pass / 0 fail (P8v; was 698 before deleting the C‑oracle tests); the `ircd-common` snapshot tests carry the reference‑C correctness forward. C source tree deleted — repo is 100% Rust at the file level.** | 91 91 | **P9 — std‑library cleanup ❌ RETIRED (2026‑06‑08)** | — (was: whole Rust tree) | **Skipped by decision — superseded by the `leveva` greenfield.** P9 assumed an *in‑place* idiomatic transformation of the mechanical port (`MyMalloc`/`MyFree`→`Box`/`Vec`, `dbuf` freelist→owned, raw `libc`→`std`/`nix`, `[c_char;N]`→`[u8;N]`). Because the idiomatic end‑state is now a separate greenfield crate (`leveva`), `ircd-common` is throwaway code (the oracle, deleted at parity), so polishing it earns nothing. Nothing was implemented; the row is kept for provenance. | n/a — retired. | 92 92 | **P10 — `leveva` greenfield foundations 🔶 IN PROGRESS** ([`docs/progress-log/p10.md`](docs/progress-log/p10.md)) | new `leveva` crate (lib) + `leveva-integration` differential tests | The idiomatic‑Rust product, built from scratch (not transformed from the port). **Done so far:** RFC 1459 case‑folding (`casemap`); typed identifier strings (`IrcStr`/`IrcString` + `Nick`/`ChanName`/`Uid`/`Sid`/`Cid`/`ServerName`/`UserName`/`HostName` validating newtypes via `ident_newtype!`); `Message`/`MessageBuilder`; the full `Numeric` reply/error enum (184 codes, discriminant = wire code); a generic safe `patricia` trie; glob `matching`; `mode`; KDL `config` (model/parse/password/privilege); `rustls`/`tokio` `tls` endpoint with hot reload; `ident`. The async boot path (`main.rs`) binds listeners + watches certs but currently drops accepted connections — no protocol layer yet. Idiomatic Rust allowed everywhere; pulls real crates (tokio/rustls/kdl/nom/clap). | `cargo test -p leveva` green + clippy clean per module; `leveva-integration` pins each greenfield reimplementation against `ircd-common` (the oracle) within their shared domain (documented divergences asserted explicitly). | 93 - | **P11 — `leveva` protocol layer → feature parity 🔶 IN PROGRESS** ([`docs/progress-log/p11.md`](docs/progress-log/p11.md)) | `leveva` (client/server state machine + handlers) | The bulk of the remaining work: the connection + registration state machine, the post‑registration command handlers (JOIN/PRIVMSG/MODE/KICK/TOPIC/WHO/WHOIS/…), channels + channel/user modes, the S2S link/burst protocol (UID‑based: UNICK/NJOIN/SAVE/EOB), and `leveva-iauth` (native async auth — dnsbl/socks/webproxy/pipe/ident). Each slice is built idiomatically (owned per‑connection state, typed identifiers end‑to‑end, `Result`/`thiserror`, `format!`/typed builders — **no** `sprintf`/global `buf`), unit‑tested + boot‑golden + proptest‑fuzzed, and **differentially pinned against `ircd-common`** (or `iauth-rs` for auth) wherever a pure oracle entry point exists — structural skeleton + documented divergences, since leveva emits a clean modern burst rather than a byte copy. **Done: 208 slices.** The slices span the full client command surface (registration, channels + channel/user modes, WHO/WHOIS/WHOX, OPER/STATS), the UID-based S2S link/burst/netsplit protocol (+ keepalive, live introduction, NJOIN chunking), `leveva-iauth` (dnsbl/socks/webproxy/pipe/ident), config live-rehash, a channel-mode rework, CertFP, an ongoing **IRCv3 track**, and the native cross-daemon command-skeleton differential harness; 195 = generalize the restricted-reop ban into the `enforce_reop` chokepoint so a restricted user is never auto-reopped by *any* trigger; 196 = a restricted user cannot change nick (`484`), completing the IRCnet `IsRestricted` family; 197 = close the slice-191 follow-on — a `kline-exempt` I-line (the C `FLAGS_EXEMPT`) now survives a *runtime* `TKLINE` reap, not just the registration-time K-line gate; 198 = `KNOCK` (leveva-native, charybdis-style) — a user kept out of a `+i`/`+k`/`+l` channel asks its operators for an invite (`710`/`711`), gated by the shared JOIN `check_join` so KNOCK and JOIN never disagree (banned/`+O`/open/already-member all refused), S2S propagation deferred; 199 = close that follow-on — KNOCK now propagates as `ENCAP * KNOCK` (modelled on `s2s::metadata`/`su`) so a channel operator on *any* server is notified, not just local ops (the shared `deliver_knock` fan; `712` throttle still deferred); 200 = close that last follow-on — a per-channel KNOCK flood throttle (`knock_throttle::KnockThrottle`, charybdis `knock_delay_channel`, 60s): a channel admits at most one KNOCK per window, a knock inside it is refused `712 ERR_TOOMANYKNOCK` and notifies nobody (the KNOCK family 198/199/200 is now complete); 201 = command-rate flood protection (the IRCnet "fakelag" penalty clock, deferred since the start) — every registered client's command charges `1 + len/100` onto a per-connection clock that idle-decays to real time, and a sustained flood is closed with `ERROR :Closing Link: <host> (Excess Flood)`, lighting up the previously-dead `can-flood`/`no-penalty` oper exemptions; pure `flood::FloodClock`, armed by the serve loop (the `heartbeat` opt-in precedent), kill-only (fakelag *delay* + per-command cost table deferred); 202 = close that follow-on — a per-command penalty cost table (`flood::command_penalty`): `PONG` is exempt, whole-table scans (`WHO`/`WHOIS`/`WHOWAS`/`LIST`/`NAMES`) cost +4 and channel ops (`JOIN`/`KNOCK`/`INVITE`) +2 over the base, so a flood of expensive commands trips Excess Flood faster than a flood of cheap ones (leveva-native cost tiers; the fakelag *delay* half + a pre-registration count guard remain the follow-ons); 203 = close the pre-registration guard follow-on — the slice-201 penalty clock only billed *registered* clients, so an unregistered connection could spam commands unboundedly (every unknown verb `451`-replied); now `Session::dispatch` charges the same `FloodClock` for the `Registering` phase too (after QUIT, before CAP; `PONG` exempt, no oper exemption pre-reg), closing a pre-registration flood with the oracle's exact `ERROR ... (Excess Flood)`; 204 = close the last flood follow-on but one — the kill ceiling (`flood::FLOOD_KILL_AHEAD`, was a fixed `const 60`) is now the tunable `options { flood-kill-ahead N }` (`charge` takes it as a parameter; parser rejects `0`; default 60), hot-reloaded by `REHASH` via the slice-91/175 `ConfStore` live-or-default seam (no `ServerContext` field — flood is serve-loop-armed, so `main` always seeds the live store first) — only the fakelag *delay/smoothing* half now remains deferred; 205 = **phase 13 (refinement)** opens — `REHASH` hot-reloads `options { auto-connect }`: the outgoing auto-connect master switch (was read once at boot to *spawn* the dial loop) is now read **per tick** via the slice-91/204 `ConfStore` live seam (`live_auto_connect`), so the loop always spawns but skips dialing while off and an operator can start/stop the dialer with no restart — no `AbortHandle` task-management (an idle ticker is cheap), no `ServerContext` field (slice-204 precedent, `main` seeds before spawning); 206 = enforce + REHASH-able `options { connection-accept on|off|split }` — the previously parsed-but-dead new-connection admission gate is now enforced at registration finalize (`On` accepts, `Off` refuses everyone with a bare `ERROR :Closing Link: <host> (Server is not accepting connections)`, `Split` accepts only while the net is split per the now-live `split-min-servers`/`split-min-users` thresholds), live via the canonical `live_*().unwrap_or(ctx.…)` `ConfStore` seam (a ctx field this time — the gate's read site is unit-tested, unlike 204/205 — bundled into `StatsConf` so the 62 hand-rolled `ServerContext` literals stay churn-free via `StatsConf::default()`, the slice-191 `bans` precedent); pure `connaccept::is_split`/`admits` (no `options` field remains boot-immutable); 207 = `REHASH` hot-reloads the `iauth` block (ident flag, per-probe timeout, and the dnsbl/socks/webproxy/pipe/ident module set) — the **last** boot-immutable config block — via the `ConfStore` live seam: `ConfBlocks` carries the plain `Iauth` struct while the *built* module set (`Arc<dyn Module>`, not `Debug/Eq`) is cached in a separate `ArcSwapOption` rebuilt only at seed/REHASH (`build_auth_modules` made `pub(crate)`, the server name supplied by a new boot `IRCD_NAME` OnceLock mirroring `CONFIG_PATH`), so live connections keep one shared resolver/DNS cache and a REHASH swaps the set atomically; read sites = the serve loop's auth task (modules+timeout), `resolve_username` (the `~` rule's ident flag), and `STATS a` (the 239 module list); no config block remains boot-immutable now (only the unchangeable `server {}` identity stays fixed by nature); 208 = close the **last** deferred flood follow-on — the fakelag *delay/smoothing* half (kill-only since slice 201): while the penalty clock runs more than `flood::MAX_PENALTY` (10s, the oracle's fixed `MAXPENALTY`) ahead but below the kill ceiling, the serve loop now **delays** the connection's next read (`tokio::time::sleep_until` on the `select!` read arm, output arms still live) instead of disconnecting, pacing a flooder's *input* toward one command per second while its fan-out/keepalive keep flowing — pure `FloodClock::delay_secs` + `Session::flood_delay_secs` (0 when disabled/within-budget/exempt), `charge` unchanged so all 201–204 tests hold; the flood family is now complete (only a `MAX_PENALTY` config knob remains as an optional future refinement). **Full per-slice detail (scope, mechanism, divergences, gate) for every slice lives in [`docs/progress-log/p11.md`](docs/progress-log/p11.md); the standing testing plan is [`docs/superpowers/specs/2026-06-10-leveva-testing-plan.md`](docs/superpowers/specs/2026-06-10-leveva-testing-plan.md). This row stays thin.** | Per‑slice: leveva boots and serves the slice; leveva golden snapshot + proptest green; the `leveva-integration` differential agrees with `ircd-common` on the shared skeleton with divergences asserted. Across the eventual matrix: registration, channels, WHO/WHOIS/WHOX, OPER/STATS, server burst, netsplit, rehash, restart; iauth end‑to‑end. | 93 + | **P11 — `leveva` protocol layer → feature parity 🔶 IN PROGRESS** ([`docs/progress-log/p11.md`](docs/progress-log/p11.md)) | `leveva` (client/server state machine + handlers) | The bulk of the remaining work: the connection + registration state machine, the post‑registration command handlers (JOIN/PRIVMSG/MODE/KICK/TOPIC/WHO/WHOIS/…), channels + channel/user modes, the S2S link/burst protocol (UID‑based: UNICK/NJOIN/SAVE/EOB), and `leveva-iauth` (native async auth — dnsbl/socks/webproxy/pipe/ident). Each slice is built idiomatically (owned per‑connection state, typed identifiers end‑to‑end, `Result`/`thiserror`, `format!`/typed builders — **no** `sprintf`/global `buf`), unit‑tested + boot‑golden + proptest‑fuzzed, and **differentially pinned against `ircd-common`** (or `iauth-rs` for auth) wherever a pure oracle entry point exists — structural skeleton + documented divergences, since leveva emits a clean modern burst rather than a byte copy. **Done: 209 slices.** The slices span the full client command surface (registration, channels + channel/user modes, WHO/WHOIS/WHOX, OPER/STATS), the UID-based S2S link/burst/netsplit protocol (+ keepalive, live introduction, NJOIN chunking), `leveva-iauth` (dnsbl/socks/webproxy/pipe/ident), config live-rehash, a channel-mode rework, CertFP, an ongoing **IRCv3 track**, and the native cross-daemon command-skeleton differential harness; 195 = generalize the restricted-reop ban into the `enforce_reop` chokepoint so a restricted user is never auto-reopped by *any* trigger; 196 = a restricted user cannot change nick (`484`), completing the IRCnet `IsRestricted` family; 197 = close the slice-191 follow-on — a `kline-exempt` I-line (the C `FLAGS_EXEMPT`) now survives a *runtime* `TKLINE` reap, not just the registration-time K-line gate; 198 = `KNOCK` (leveva-native, charybdis-style) — a user kept out of a `+i`/`+k`/`+l` channel asks its operators for an invite (`710`/`711`), gated by the shared JOIN `check_join` so KNOCK and JOIN never disagree (banned/`+O`/open/already-member all refused), S2S propagation deferred; 199 = close that follow-on — KNOCK now propagates as `ENCAP * KNOCK` (modelled on `s2s::metadata`/`su`) so a channel operator on *any* server is notified, not just local ops (the shared `deliver_knock` fan; `712` throttle still deferred); 200 = close that last follow-on — a per-channel KNOCK flood throttle (`knock_throttle::KnockThrottle`, charybdis `knock_delay_channel`, 60s): a channel admits at most one KNOCK per window, a knock inside it is refused `712 ERR_TOOMANYKNOCK` and notifies nobody (the KNOCK family 198/199/200 is now complete); 201 = command-rate flood protection (the IRCnet "fakelag" penalty clock, deferred since the start) — every registered client's command charges `1 + len/100` onto a per-connection clock that idle-decays to real time, and a sustained flood is closed with `ERROR :Closing Link: <host> (Excess Flood)`, lighting up the previously-dead `can-flood`/`no-penalty` oper exemptions; pure `flood::FloodClock`, armed by the serve loop (the `heartbeat` opt-in precedent), kill-only (fakelag *delay* + per-command cost table deferred); 202 = close that follow-on — a per-command penalty cost table (`flood::command_penalty`): `PONG` is exempt, whole-table scans (`WHO`/`WHOIS`/`WHOWAS`/`LIST`/`NAMES`) cost +4 and channel ops (`JOIN`/`KNOCK`/`INVITE`) +2 over the base, so a flood of expensive commands trips Excess Flood faster than a flood of cheap ones (leveva-native cost tiers; the fakelag *delay* half + a pre-registration count guard remain the follow-ons); 203 = close the pre-registration guard follow-on — the slice-201 penalty clock only billed *registered* clients, so an unregistered connection could spam commands unboundedly (every unknown verb `451`-replied); now `Session::dispatch` charges the same `FloodClock` for the `Registering` phase too (after QUIT, before CAP; `PONG` exempt, no oper exemption pre-reg), closing a pre-registration flood with the oracle's exact `ERROR ... (Excess Flood)`; 204 = close the last flood follow-on but one — the kill ceiling (`flood::FLOOD_KILL_AHEAD`, was a fixed `const 60`) is now the tunable `options { flood-kill-ahead N }` (`charge` takes it as a parameter; parser rejects `0`; default 60), hot-reloaded by `REHASH` via the slice-91/175 `ConfStore` live-or-default seam (no `ServerContext` field — flood is serve-loop-armed, so `main` always seeds the live store first) — only the fakelag *delay/smoothing* half now remains deferred; 205 = **phase 13 (refinement)** opens — `REHASH` hot-reloads `options { auto-connect }`: the outgoing auto-connect master switch (was read once at boot to *spawn* the dial loop) is now read **per tick** via the slice-91/204 `ConfStore` live seam (`live_auto_connect`), so the loop always spawns but skips dialing while off and an operator can start/stop the dialer with no restart — no `AbortHandle` task-management (an idle ticker is cheap), no `ServerContext` field (slice-204 precedent, `main` seeds before spawning); 206 = enforce + REHASH-able `options { connection-accept on|off|split }` — the previously parsed-but-dead new-connection admission gate is now enforced at registration finalize (`On` accepts, `Off` refuses everyone with a bare `ERROR :Closing Link: <host> (Server is not accepting connections)`, `Split` accepts only while the net is split per the now-live `split-min-servers`/`split-min-users` thresholds), live via the canonical `live_*().unwrap_or(ctx.…)` `ConfStore` seam (a ctx field this time — the gate's read site is unit-tested, unlike 204/205 — bundled into `StatsConf` so the 62 hand-rolled `ServerContext` literals stay churn-free via `StatsConf::default()`, the slice-191 `bans` precedent); pure `connaccept::is_split`/`admits` (no `options` field remains boot-immutable); 207 = `REHASH` hot-reloads the `iauth` block (ident flag, per-probe timeout, and the dnsbl/socks/webproxy/pipe/ident module set) — the **last** boot-immutable config block — via the `ConfStore` live seam: `ConfBlocks` carries the plain `Iauth` struct while the *built* module set (`Arc<dyn Module>`, not `Debug/Eq`) is cached in a separate `ArcSwapOption` rebuilt only at seed/REHASH (`build_auth_modules` made `pub(crate)`, the server name supplied by a new boot `IRCD_NAME` OnceLock mirroring `CONFIG_PATH`), so live connections keep one shared resolver/DNS cache and a REHASH swaps the set atomically; read sites = the serve loop's auth task (modules+timeout), `resolve_username` (the `~` rule's ident flag), and `STATS a` (the 239 module list); no config block remains boot-immutable now (only the unchangeable `server {}` identity stays fixed by nature); 208 = close the **last** deferred flood follow-on — the fakelag *delay/smoothing* half (kill-only since slice 201): while the penalty clock runs more than `flood::MAX_PENALTY` (10s, the oracle's fixed `MAXPENALTY`) ahead but below the kill ceiling, the serve loop now **delays** the connection's next read (`tokio::time::sleep_until` on the `select!` read arm, output arms still live) instead of disconnecting, pacing a flooder's *input* toward one command per second while its fan-out/keepalive keep flowing — pure `FloodClock::delay_secs` + `Session::flood_delay_secs` (0 when disabled/within-budget/exempt), `charge` unchanged so all 201–204 tests hold; the flood family is now complete (only a `MAX_PENALTY` config knob remains as an optional future refinement); 209 = close the **last** deferred KNOCK gap — the per-*user* throttle (charybdis `knock_delay`, 300s) alongside slice 200's per-*channel* one: a single knocker may deliver at most one KNOCK per window across *all* channels (keyed by UID in a second `knock_user_throttle: KnockThrottle`), so one user can no longer spray operators on many fresh channels; the command peeks both throttles (new `cooling`/`arm` split, arming neither unless the knock is delivered — charybdis updates its clocks on success only) and answers `712 ERR_TOOMANYKNOCK` if either is cooling — the KNOCK throttle family (200/209) is now complete. **Full per-slice detail (scope, mechanism, divergences, gate) for every slice lives in [`docs/progress-log/p11.md`](docs/progress-log/p11.md); the standing testing plan is [`docs/superpowers/specs/2026-06-10-leveva-testing-plan.md`](docs/superpowers/specs/2026-06-10-leveva-testing-plan.md). This row stays thin.** | Per‑slice: leveva boots and serves the slice; leveva golden snapshot + proptest green; the `leveva-integration` differential agrees with `ircd-common` on the shared skeleton with divergences asserted. Across the eventual matrix: registration, channels, WHO/WHOIS/WHOX, OPER/STATS, server burst, netsplit, rehash, restart; iauth end‑to‑end. | 94 94 | **P12 — Retire the mechanical port; `leveva` is the product ✅ DONE (2026‑06‑13)** ([`docs/progress-log/p12.md`](docs/progress-log/p12.md)) | deleted `ircd-common`/`ircd-rs` + C‑era `iauth-rs` + `leveva-integration` + the mechanical port's deployment rigging; workspace = `leveva` + `leveva-iauth` | Ran the differential suite a **final time green** (`cargo test -p leveva-integration`, skeleton‑identical to the oracle), then **deleted the mechanical port** (`ircd-common`, `ircd-rs`), its C‑era auth scaffolding (`iauth-rs`, long since subsumed by the native `leveva-iauth`), and the oracle differential crate (`leveva-integration`). The oracle tests retired with their oracle — the load‑bearing behavior was already carried by leveva's self‑contained 212‑file golden + proptest suite (verified: leveva/leveva-iauth have zero Cargo/`use` dep on the deleted crates). Also stripped the mechanical port's **deployment rigging** (`docker/Dockerfile.ircd`, the `ircd` bake target, the entire `docs/k8s/` example network) and updated the README to describe the finished strangler. This is the literal end of the strangler — same lifecycle the C tree had at P8. | **MET. Final differential run green; `ircd-common`/`ircd-rs`/`iauth-rs`/`leveva-integration` deleted; `cargo build --workspace` 0 warnings; `cargo clippy --workspace --tests` clean; `cargo test -p leveva -p leveva-iauth` green — the behavioral guarantees carry forward. The workspace is now `leveva` + `leveva-iauth`.** | 95 95 96 96 ---
+69
docs/progress-log/p11.md
··· 11619 11619 unrelated: `golden_s2s_keepalive::silent_peer_times_out_and_its_users_are_reaped` 11620 11620 fails identically on clean HEAD — a timing flake in the half-open reap path, not 11621 11621 touched by this slice.) 11622 + 11623 + ## 2026-06-16 — P11 slice 209: per-user KNOCK throttle (charybdis `knock_delay`) 11624 + 11625 + Closes the **last** documented gap in the KNOCK throttle family (slices 198/199/200). 11626 + Slice 200 added the *per-channel* cooldown (`knock_delay_channel`): one channel admits 11627 + at most one KNOCK per window, stopping a chorus spamming a single channel's operators. 11628 + It did nothing about one user spraying KNOCKs across *many* channels — each channel is 11629 + fresh, so the per-channel table admits every one and a flood of `710`s reaches a flood 11630 + of operators. charybdis's per-*user* `knock_delay` (default 300s) is the other half; 11631 + `command/knock.rs` and `knock_throttle.rs` both flagged it out of scope. Now implemented. 11632 + 11633 + ### Mechanism 11634 + 11635 + - **`KnockThrottle` reused as a generic per-key cooldown.** The store was already a 11636 + `folded-string → unix-second-hold` map with no clock of its own; it serves equally as 11637 + a per-channel table (keyed by channel) and a per-user table (keyed by UID). A UID 11638 + folds to itself (case-collapsed but injectively over `[0-9A-Z]`), so distinct UIDs 11639 + never collide. `ServerContext` now holds two instances: `knock_throttle` (per-channel) 11640 + + `knock_user_throttle` (per-user). 11641 + - **Peek/arm split.** Two new methods: `cooling(key, now) -> bool` (prune + report, no 11642 + arm) and `arm(key, now, delay)` (prune + record). The command layer *peeks* both 11643 + throttles and `arm`s them only after a knock is delivered, so a knock rejected by 11644 + *either* window consumes neither — matching charybdis, which updates `last_used`/ 11645 + `last_knock` on success only. The atomic `admit` is retained (and a unit test pins 11646 + `admit == cooling-then-arm`). 11647 + - **`KNOCK_DELAY_USER_SECS = 300`** (charybdis default), a named const beside 11648 + `KNOCK_DELAY_CHANNEL_SECS` so a future config knob is a one-liner. 11649 + - **Command gate** (`command/knock.rs`, `InviteOnly | BadKey | Full` branch): read `now` 11650 + once; `if user_throttle.cooling(uid, now) || channel_throttle.cooling(chan, now)` → 11651 + `712 ERR_TOOMANYKNOCK` (the same reply charybdis uses for both, with the channel 11652 + name), notify nobody; else deliver, propagate S2S, then `arm` both. Per-user is peeked 11653 + first so a globally-flooding user is stopped even when the target channel is fresh. 11654 + 11655 + ### Divergences 11656 + 11657 + - `KNOCK_DELAY_USER_SECS` is a const, not yet a config knob — a one-line follow-on, but 11658 + the throttle family is otherwise complete after this. 11659 + - The two-throttle gate is not atomic across the two un-synchronized mutexes; a benign 11660 + best-effort race could let two simultaneous knocks from one connection both pass. 11661 + Documented; harmless for a flood guard. 11662 + 11663 + ### Tests / fuzzing 11664 + 11665 + - **`knock_throttle.rs` units** — `cooling_does_not_arm` (a peek inserts no hold); 11666 + `arm_then_cooling_true_until_expiry` (true strictly before `now+D`, false at/after — 11667 + the window is not a permanent ban); `admit_equals_cooling_then_arm` (the atomic and 11668 + split forms agree across a clock sweep). 11669 + - **`command/knock.rs` units** — `a_users_second_knock_to_a_different_channel_is_throttled` 11670 + (the core new behavior: alice's second knock to a *fresh* invite-only #two is `712` 11671 + and #two's op gets **no** 710, even though the per-channel store would have admitted 11672 + it); `the_per_user_throttle_is_per_user_not_global` (inverse: bob knocking #two right 11673 + after alice knocked #one is delivered — one user's window never throttles another); 11674 + `the_throttle_is_per_channel_not_global` rewritten to use *distinct* knockers 11675 + (alice→#one, dave→#two) so the per-user window doesn't mask the channel-independence 11676 + claim. New `testutil::run_as(ctx, nick, line)` driver dispatches as a named client. 11677 + - **`knock_user_throttle_proptest.rs` (NEW)** — `cooling_and_arm_track_the_clock` (over a 11678 + monotone clock + arbitrary delay, a hand-tracked hold predicts `cooling` exactly; a 11679 + peeked-but-not-armed throttled step never changes the hold; idempotent peek) + 11680 + `arbitrary_uid_key_never_panics` (cooling/arm total over arbitrary keys and 11681 + saturating-edge clock/delay). Mirrors slice 200's `knock_throttle_proptest.rs`. 11682 + - Mechanical: `knock_user_throttle` added to the 58 hand-rolled `ServerContext` test 11683 + literals + the 2 in `testutil.rs` (the slice-206 churn pattern). 11684 + 11685 + **Plan:** `docs/superpowers/plans/2026-06-16-p11-slice209-knock-per-user-throttle.md`. 11686 + 11687 + **Gate:** `cargo test -p leveva` green (1949 lib tests; knock_throttle units + knock 11688 + command tests + knock_user_throttle_proptest + knock_throttle_proptest + knock_proptest 11689 + + knock_s2s_proptest + golden_knock all pass); `cargo clippy -p leveva --tests` clean; 11690 + `cargo build --workspace` 0 warnings.
+106
docs/superpowers/plans/2026-06-16-p11-slice209-knock-per-user-throttle.md
··· 1 + # P11 slice 209 — per-user KNOCK throttle (charybdis `knock_delay`) 2 + 3 + ## Goal 4 + 5 + Close the **last** documented gap in the KNOCK throttle family (slices 198/199/200). 6 + Slice 200 added the *per-channel* cooldown (charybdis `knock_delay_channel`): a single 7 + channel admits at most one KNOCK per window. That stops a chorus spamming **one** 8 + channel's operators, but it does nothing about **one** user spraying KNOCKs across 9 + **many** different channels — each channel is fresh, so each knock is delivered and a 10 + flood of 710s reaches a flood of operators. charybdis's `knock_delay` (per-*user*, 11 + default 300s) is the other half. `command/knock.rs:37` and `knock_throttle.rs:11` 12 + explicitly note it as out of scope; this slice implements it. 13 + 14 + ## Locked decisions 15 + 16 + - **Two independent cooldowns, both must pass.** A delivered knock requires the 17 + knocker's per-user window *and* the channel's per-channel window to be clear. This 18 + matches charybdis `m_knock`, which checks per-user `last_used` first then per-channel 19 + `last_knock`, and updates **neither** unless the knock is actually delivered. 20 + - **Reuse `KnockThrottle` as a generic clock-injected cooldown table.** The store is 21 + already a `folded-string → unix-second-hold` map with no clock of its own; it serves 22 + equally as a per-channel table (keyed by channel) and a per-user table (keyed by UID). 23 + A second `ServerContext` field `knock_user_throttle` holds the per-user instance. 24 + - **`KNOCK_DELAY_USER_SECS = 300`** (charybdis default `knock_delay`). A named const so a 25 + future config knob is a one-liner, exactly like `KNOCK_DELAY_CHANNEL_SECS`. 26 + - **Same 712 reply for both rejections.** charybdis answers `712 ERR_TOOMANYKNOCK` with 27 + the channel name for *both* the per-user and per-channel throttle; the wire output is 28 + identical, so the existing `too_many_knock(ctx, nick, display)` helper is reused. 29 + - **Neither window armed unless delivered.** To avoid the punitive divergence where a 30 + channel-throttled knock would still consume the user's 5-minute window, the gate 31 + *peeks* both throttles (`cooling`, no arm) and only `arm`s both after the knock is 32 + delivered. The micro-race between the two un-synchronized mutexes is benign for a 33 + best-effort flood guard and documented. 34 + 35 + ## Design 36 + 37 + `knock_throttle.rs`: 38 + - keep `admit` (atomic check+arm) for any caller that wants the combined operation; 39 + - add `cooling(&self, key, now) -> bool` — prune expired holds, return whether `key` 40 + is still within a live window (no arm); 41 + - add `arm(&self, key, now, delay)` — prune, then record `key`'s next window at 42 + `now + delay` (saturating); 43 + - broaden the module/struct docs: the table is a generic per-key cooldown (channel 44 + *or* UID); a UID folds to itself case-collapsed but injectively, so distinct UIDs 45 + never collide. 46 + 47 + `server.rs`: add `pub knock_user_throttle: KnockThrottle` + construct it in 48 + `from_config`. 49 + 50 + `command/knock.rs` deliver branch (`InviteOnly | BadKey | Full`): 51 + ``` 52 + let now = now_secs(); 53 + if ctx.knock_user_throttle.cooling(client.uid.as_str(), now) 54 + || ctx.knock_throttle.cooling(&display, now) 55 + { 56 + return vec![too_many_knock(ctx, &client.nick, &display)]; 57 + } 58 + deliver_knock(...); 59 + crate::s2s::knock::propagate(...); 60 + ctx.knock_user_throttle.arm(client.uid.as_str(), now, KNOCK_DELAY_USER_SECS); 61 + ctx.knock_throttle.arm(&display, now, KNOCK_DELAY_CHANNEL_SECS); 62 + vec![711...] 63 + ``` 64 + Per-user is peeked first so a globally-flooding user is stopped even when the target 65 + channel is fresh. 66 + 67 + ## Tests (RED first) 68 + 69 + Store-level (`knock_throttle.rs`, clock-injected — exhaustive window semantics live here): 70 + - `cooling_does_not_arm` — `cooling` twice in the window both `false` until something arms. 71 + - `arm_then_cooling_true_until_expiry` — `arm` sets a window; `cooling` true strictly 72 + before expiry, false at/after; inverse of the arm. 73 + - `admit_equals_cooling_then_arm` — the combined `admit` agrees with peek+arm. 74 + 75 + Command-level (`command/knock.rs`, real clock — same-window positive throttle, the 76 + established slice-200 pattern): 77 + - `a_users_second_knock_to_a_DIFFERENT_channel_is_throttled` — alice knocks #one 78 + (711 + 710 to its op), then alice knocks a fresh invite-only #two within her user 79 + window → 712 and **#two's op gets no 710** (the per-channel store would have admitted 80 + #two; the per-user store blocks it). The core new behavior. 81 + - `the_per_user_throttle_is_per_user_not_global` (inverse) — alice knocks #one (711), 82 + then **bob** knocks #two (711 delivered): one user's window must not throttle another's. 83 + - update `the_throttle_is_per_channel_not_global` — now that a single user is 84 + per-user-throttled, demonstrate per-channel independence with **distinct** knockers 85 + (alice→#one, dave→#two both 711) so the channel-independence claim isn't masked by 86 + the new per-user window. 87 + 88 + Fuzzing (`knock_user_throttle_proptest.rs`, NEW — mirrors `knock_throttle_proptest.rs`): 89 + - `cooling_and_arm_track_the_clock` — over a monotone clock and arbitrary delay, model 90 + the hold by hand: `cooling` is true exactly while a live armed hold has not expired; 91 + `arm` (re)sets it; a peeked-but-not-armed step never changes the hold; never panics. 92 + - `arbitrary_uid_key_never_panics` — `cooling`/`arm` over arbitrary strings and 93 + saturating-edge clock/delay values are total. 94 + 95 + ## Divergences 96 + 97 + - `KNOCK_DELAY_USER_SECS` is a const (charybdis default), not yet a config knob — a 98 + one-line follow-on, but the throttle family is otherwise complete after this. 99 + - The two-throttle gate is not atomic across both mutexes; a benign best-effort race 100 + could let two simultaneous knocks from the same connection both pass. Documented. 101 + 102 + ## Gate 103 + 104 + `cargo test -p leveva` green (knock_throttle units + knock command tests + the two new 105 + proptests + golden_knock + knock_proptest + knock_s2s_proptest unaffected); 106 + `cargo clippy -p leveva --tests` clean; `cargo build --workspace` 0 warnings.
+82 -15
leveva/src/command/knock.rs
··· 31 31 /// KNOCK <channel> <nick!user@host> [:<reason>]` to every peer (see [`crate::s2s::knock`]), so a 32 32 /// channel operator on **any** server is notified, not just local ops. 33 33 /// 34 - /// **Flood throttle (P11 slice 200):** a channel admits at most one KNOCK per cooldown window 35 - /// ([`crate::knock_throttle`]); a knock arriving while the channel is still cooling down is refused 36 - /// with `712 ERR_TOOMANYKNOCK` and notifies nobody (local ops *or* remote ops via S2S). The 37 - /// per-*user* throttle (charybdis `knock_delay`) remains out of scope. 34 + /// **Flood throttles ([`crate::knock_throttle`]):** a knock is delivered only when *both* cooldown 35 + /// windows are clear, and arms neither otherwise; a knock arriving while either is still cooling 36 + /// down is refused with `712 ERR_TOOMANYKNOCK` and notifies nobody (local ops *or* remote ops via 37 + /// S2S): 38 + /// - **per-channel** (P11 slice 200, charybdis `knock_delay_channel`) — a channel admits at most 39 + /// one KNOCK per window, throttling a chorus spamming one channel's operators; 40 + /// - **per-user** (P11 slice 209, charybdis `knock_delay`) — one knocker may deliver at most one 41 + /// KNOCK per window across *all* channels, throttling a single user spraying operators on many 42 + /// channels (the per-channel window alone would admit every fresh channel). 38 43 pub(crate) fn knock(client: &Registered, msg: &Message, ctx: &ServerContext) -> Vec<Message> { 39 44 let Some(chan) = msg 40 45 .params() ··· 108 113 } 109 114 // Invite-only / wrong-or-missing key / full: an INVITE would let them in — deliver. 110 115 Some(JoinReject::InviteOnly | JoinReject::BadKey | JoinReject::Full) => { 111 - // Flood throttle (slice 200): a channel admits at most one KNOCK per cooldown window. 112 - // A knock inside the window is refused with 712 and notifies nobody — neither the 113 - // channel's ops nor (via S2S) any remote op. 114 - if !ctx.knock_throttle.admit( 115 - &display, 116 - now_secs(), 117 - crate::knock_throttle::KNOCK_DELAY_CHANNEL_SECS, 118 - ) { 116 + // Two flood throttles, both of which must be clear for the knock to be delivered: 117 + // * per-user (slice 209, charybdis `knock_delay`) — one knocker may knock at most once 118 + // per window across ALL channels; checked first so a user spraying many channels is 119 + // stopped even when each target channel is fresh; 120 + // * per-channel (slice 200, `knock_delay_channel`) — a channel admits at most one 121 + // knock per window. 122 + // Both are *peeked* (`cooling`, no arm) so a knock rejected by either consumes neither 123 + // window — only a delivered knock arms them (charybdis updates its clocks on success 124 + // only). A rejected knock answers 712 and notifies nobody (local ops or, via S2S, remote 125 + // ops). `now` is read once and shared by both stores. 126 + let now = now_secs(); 127 + if ctx.knock_user_throttle.cooling(client.uid.as_str(), now) 128 + || ctx.knock_throttle.cooling(&display, now) 129 + { 119 130 return vec![too_many_knock(ctx, &client.nick, &display)]; 120 131 } 121 132 deliver_knock(ctx, &display, &source, reason); 122 133 // Reach operators on other servers too (P11 slice 199): broadcast the `ENCAP * KNOCK` 123 134 // carrier so a remote channel op sees the knock and can INVITE. 124 135 crate::s2s::knock::propagate(ctx, &display, &source, reason); 136 + // Delivered — arm both windows for the next knock. 137 + ctx.knock_user_throttle.arm( 138 + client.uid.as_str(), 139 + now, 140 + crate::knock_throttle::KNOCK_DELAY_USER_SECS, 141 + ); 142 + ctx.knock_throttle 143 + .arm(&display, now, crate::knock_throttle::KNOCK_DELAY_CHANNEL_SECS); 125 144 vec![Message::builder(Numeric::RplKnockdlvr) 126 145 .prefix(&ctx.name) 127 146 .param(&client.nick) ··· 376 395 let mut carol = op_member(&ctx, "carol", "#two"); 377 396 srv(&ctx, "#one", &[flag(ChanMode::InviteOnly)]); 378 397 srv(&ctx, "#two", &[flag(ChanMode::InviteOnly)]); 379 - // Knocking #one must not throttle #two — different channels, independent windows. 398 + // Knocking #one must not throttle #two — different channels, independent windows. Use 399 + // *distinct* knockers (alice→#one, dave→#two) so the per-user throttle (slice 209) doesn't 400 + // mask the channel-independence claim: alice knocking both would be per-user-throttled. 401 + assert_eq!(codes(&run(&ctx, "KNOCK #one")), &["711"]); // run's knocker is alice 402 + assert_knock_710(&delivered(&mut bob), "bob", "#one", None); 403 + let _dave = claim_observed(&ctx, "dave"); 404 + assert_eq!(codes(&run_as(&ctx, "dave", "KNOCK #two")), &["711"]); 405 + let m = delivered(&mut carol); 406 + assert_eq!(m.command(), "710"); 407 + assert_eq!(m.params(), &["carol", "#two", "dave!dave@127.0.0.1"]); 408 + } 409 + 410 + // --- Per-user throttle (slice 209): charybdis `knock_delay` ----------------------------- 411 + 412 + #[test] 413 + fn a_users_second_knock_to_a_different_channel_is_throttled() { 414 + // The core new behavior: the per-channel store would admit a fresh second channel, but the 415 + // knocker's per-user window blocks it — so one user cannot spray many channels' operators. 416 + let ctx = ctx(); 417 + let mut bob = op_member(&ctx, "bob", "#one"); 418 + let mut carol = op_member(&ctx, "carol", "#two"); 419 + srv(&ctx, "#one", &[flag(ChanMode::InviteOnly)]); 420 + srv(&ctx, "#two", &[flag(ChanMode::InviteOnly)]); 421 + // alice's first knock (#one) is delivered. 380 422 assert_eq!(codes(&run(&ctx, "KNOCK #one")), &["711"]); 381 423 assert_knock_710(&delivered(&mut bob), "bob", "#one", None); 382 - assert_eq!(codes(&run(&ctx, "KNOCK #two")), &["711"]); 383 - assert_knock_710(&delivered(&mut carol), "carol", "#two", None); 424 + // alice's second knock to a *fresh* channel #two, inside her per-user window: 712, and #two's 425 + // op gets no 710 (the side effect is suppressed, not just the ack). 426 + let r = run(&ctx, "KNOCK #two"); 427 + assert_eq!(codes(&r), &["712"]); 428 + assert_eq!(r[0].params(), &["alice", "#two"]); 429 + assert!( 430 + carol.try_recv().is_err(), 431 + "a per-user-throttled knock must deliver no 710 to the fresh channel's op" 432 + ); 433 + } 434 + 435 + #[test] 436 + fn the_per_user_throttle_is_per_user_not_global() { 437 + // Inverse: one user's window must not throttle another user. After alice knocks #one, bob 438 + // (a different knocker) knocking a fresh #two is still delivered. 439 + let ctx = ctx(); 440 + let mut opa = op_member(&ctx, "opa", "#one"); 441 + let mut opb = op_member(&ctx, "opb", "#two"); 442 + srv(&ctx, "#one", &[flag(ChanMode::InviteOnly)]); 443 + srv(&ctx, "#two", &[flag(ChanMode::InviteOnly)]); 444 + assert_eq!(codes(&run(&ctx, "KNOCK #one")), &["711"]); // alice 445 + assert_knock_710(&delivered(&mut opa), "opa", "#one", None); 446 + let _bob = claim_observed(&ctx, "bob"); 447 + assert_eq!(codes(&run_as(&ctx, "bob", "KNOCK #two")), &["711"]); 448 + let m = delivered(&mut opb); 449 + assert_eq!(m.command(), "710"); 450 + assert_eq!(m.params(), &["opb", "#two", "bob!bob@127.0.0.1"]); 384 451 } 385 452 }
+12
leveva/src/command/testutil.rs
··· 207 207 conn_limits: crate::connlimit::ConnLimits::new(), 208 208 linking: crate::link::LinkingSet::new(), 209 209 knock_throttle: crate::knock_throttle::KnockThrottle::new(), 210 + knock_user_throttle: crate::knock_throttle::KnockThrottle::new(), 210 211 }) 211 212 } 212 213 ··· 245 246 conn_limits: crate::connlimit::ConnLimits::new(), 246 247 linking: crate::link::LinkingSet::new(), 247 248 knock_throttle: crate::knock_throttle::KnockThrottle::new(), 249 + knock_user_throttle: crate::knock_throttle::KnockThrottle::new(), 248 250 }) 249 251 } 250 252 ··· 275 277 276 278 pub(crate) fn run(ctx: &ServerContext, line: &str) -> Vec<Message> { 277 279 dispatch(&mut client(), &Message::parse(line).unwrap(), ctx) 280 + } 281 + 282 + /// Like [`run`] but the sender is `nick` (its `uid_for(nick)` identity), so a test can drive a 283 + /// command as a *different* client — e.g. to show a per-user gate distinguishes knockers. 284 + pub(crate) fn run_as(ctx: &ServerContext, nick: &str, line: &str) -> Vec<Message> { 285 + let mut c = client(); 286 + c.uid = uid_for(nick); 287 + c.nick = nick.to_string(); 288 + c.user = nick.to_string(); 289 + dispatch(&mut c, &Message::parse(line).unwrap(), ctx) 278 290 } 279 291 280 292 /// Like [`run`] but the sender holds operator status (`+o`), so operator-gated paths
+79 -3
leveva/src/knock_throttle.rs
··· 7 7 //! channel is still cooling down is refused (the command layer answers `712 ERR_TOOMANYKNOCK`) and 8 8 //! notifies nobody. 9 9 //! 10 - //! This is the per-channel half of charybdis's KNOCK throttle (`knock_delay_channel`). The 11 - //! per-*user* `knock_delay` is intentionally out of scope (the deferral note was explicit: 12 - //! "a clock-keyed per-channel cooldown store"). 10 + //! The same table also backs the **per-user** KNOCK throttle (charybdis `knock_delay`, P11 slice 11 + //! 209): keyed by the knocker's UID instead of a channel, it stops a single user spraying KNOCKs 12 + //! across *many* channels (each channel fresh, so the per-channel table alone would admit every 13 + //! one). [`crate::server::ServerContext`] holds two instances — `knock_throttle` (per-channel) and 14 + //! `knock_user_throttle` (per-user) — and the command layer requires *both* windows clear before a 15 + //! knock is delivered, arming neither unless it is (charybdis updates `last_knock`/`last_used` only 16 + //! on success). The store is therefore generic: a per-key cooldown over a folded string. A UID folds 17 + //! to itself (case-collapsed but injectively over `[0-9A-Z]`), so distinct UIDs never collide. 13 18 //! 14 19 //! Like [`crate::kline`] / [`crate::autoconnect`], the store reads **no** clock: the command layer 15 20 //! reads `now` (unix seconds) once and passes it in, so the store is deterministic under test and ··· 23 28 /// The default per-channel cooldown between admitted KNOCKs, in seconds (charybdis 24 29 /// `knock_delay_channel`). A named constant so a future config knob is a one-liner. 25 30 pub const KNOCK_DELAY_CHANNEL_SECS: i64 = 60; 31 + 32 + /// The default per-*user* cooldown between admitted KNOCKs, in seconds (charybdis `knock_delay`, 33 + /// slice 209). Longer than the per-channel window: it throttles one user across *all* channels, so 34 + /// it guards against a single user spraying many channels, not a chorus on one channel. A named 35 + /// constant so a future config knob is a one-liner. 36 + pub const KNOCK_DELAY_USER_SECS: i64 = 300; 26 37 27 38 /// The shared per-channel KNOCK cooldown table. Keyed by the **case-folded** channel name 28 39 /// ([`crate::casemap::fold`]) → the unix-second instant the channel may next be knocked. A channel ··· 61 72 holds.insert(key, now.saturating_add(delay)); 62 73 true 63 74 } 75 + 76 + /// Peek whether `key` is still within a live cooldown window at unix-second `now`, **without** 77 + /// arming anything. Expired holds are pruned on every call. This is the non-mutating half of 78 + /// [`admit`](Self::admit): the command layer peeks both the per-user and per-channel throttles 79 + /// and only [`arm`](Self::arm)s them once the knock is actually delivered, so a knock rejected 80 + /// by *either* window consumes neither (charybdis updates its clocks only on success). 81 + pub fn cooling(&self, key: &str, now: i64) -> bool { 82 + let key = crate::casemap::fold(key); 83 + let mut holds = self.holds.lock().expect("knock-throttle mutex poisoned"); 84 + holds.retain(|_, &mut h| h > now); 85 + holds.contains_key(&key) 86 + } 87 + 88 + /// Arm `key`'s next cooldown window at `now + delay`, replacing any prior hold. Expired holds 89 + /// are pruned on every call so the map stays bounded to keys with a live window. Pair with 90 + /// [`cooling`](Self::cooling) for the peek-then-arm gate (see that method). 91 + pub fn arm(&self, key: &str, now: i64, delay: i64) { 92 + let key = crate::casemap::fold(key); 93 + let mut holds = self.holds.lock().expect("knock-throttle mutex poisoned"); 94 + holds.retain(|_, &mut h| h > now); 95 + // `saturating_add` keeps the function total for an absurd clock (unreachable in practice). 96 + holds.insert(key, now.saturating_add(delay)); 97 + } 64 98 } 65 99 66 100 #[cfg(test)] ··· 119 153 assert!(t.admit("#b", 1000, D), "a knock on #a must not throttle #b"); 120 154 assert!(!t.admit("#a", 1000, D)); 121 155 assert!(!t.admit("#b", 1000, D)); 156 + } 157 + 158 + // --- per-key peek/arm split (slice 209): the generic cooldown reused for the per-user 159 + // throttle. `cooling` must NOT arm; `arm` sets the window; `admit` == cooling-then-arm. ---- 160 + 161 + #[test] 162 + fn cooling_does_not_arm() { 163 + let t = KnockThrottle::new(); 164 + // Peeking an un-armed key reports "not cooling" and, crucially, leaves it un-armed: 165 + // a second peek still reports clear because nothing recorded a hold. 166 + assert!(!t.cooling("000AAAAAA", 1000)); 167 + assert!(!t.cooling("000AAAAAA", 1000)); 168 + assert!(t.holds.lock().unwrap().is_empty(), "cooling must not insert a hold"); 169 + } 170 + 171 + #[test] 172 + fn arm_then_cooling_true_until_expiry() { 173 + let t = KnockThrottle::new(); 174 + t.arm("000AAAAAA", 1000, D); 175 + // Cooling for every instant strictly before the armed hold (now + D)… 176 + assert!(t.cooling("000AAAAAA", 1000)); 177 + assert!(t.cooling("000AAAAAA", 1000 + D - 1)); 178 + // …and clear at/after it (inverse of the arm — the window is not a permanent ban). 179 + assert!(!t.cooling("000AAAAAA", 1000 + D)); 180 + assert!(!t.cooling("000AAAAAA", 1000 + D + 1)); 181 + } 182 + 183 + #[test] 184 + fn admit_equals_cooling_then_arm() { 185 + // `admit` is the atomic combination of the two split primitives — they must agree. 186 + let combined = KnockThrottle::new(); 187 + let split = KnockThrottle::new(); 188 + for &now in &[1000i64, 1000, 1000 + D - 1, 1000 + D, 1000 + 2 * D] { 189 + let a = combined.admit("#c", now, D); 190 + let b = if split.cooling("#c", now) { 191 + false 192 + } else { 193 + split.arm("#c", now, D); 194 + true 195 + }; 196 + assert_eq!(a, b, "admit vs cooling-then-arm disagree at now={now}"); 197 + } 122 198 } 123 199 124 200 #[test]
+9
leveva/src/server.rs
··· 161 161 /// refused with `712 ERR_TOOMANYKNOCK` and notifies nobody. Empty until the first delivered 162 162 /// knock. See [`crate::knock_throttle`]. 163 163 pub knock_throttle: crate::knock_throttle::KnockThrottle, 164 + /// The shared **per-user KNOCK flood throttle** (P11 slice 209, charybdis `knock_delay`) — a 165 + /// single knocker may deliver at most one KNOCK per [`KNOCK_DELAY_USER_SECS`] window across 166 + /// *all* channels (keyed by the knocker's UID), so one user cannot spray operators on many 167 + /// channels at once. Checked alongside [`Self::knock_throttle`]; a knock is delivered only when 168 + /// both windows are clear, and arms neither otherwise. See [`crate::knock_throttle`]. 169 + /// 170 + /// [`KNOCK_DELAY_USER_SECS`]: crate::knock_throttle::KNOCK_DELAY_USER_SECS 171 + pub knock_user_throttle: crate::knock_throttle::KnockThrottle, 164 172 } 165 173 166 174 /// The resolved `iauth` settings the serve loop needs to build its auth pipeline. ··· 304 312 conn_limits: crate::connlimit::ConnLimits::new(), 305 313 linking: crate::link::LinkingSet::new(), 306 314 knock_throttle: crate::knock_throttle::KnockThrottle::new(), 315 + knock_user_throttle: crate::knock_throttle::KnockThrottle::new(), 307 316 }) 308 317 } 309 318
+1
leveva/tests/account_tag_proptest.rs
··· 49 49 conn_limits: leveva::connlimit::ConnLimits::new(), 50 50 linking: leveva::link::LinkingSet::new(), 51 51 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 52 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 52 53 auth: leveva::server::AuthConfig::default(), 53 54 admin: Default::default(), 54 55 })
+1
leveva/tests/away_notify_proptest.rs
··· 48 48 conn_limits: leveva::connlimit::ConnLimits::new(), 49 49 linking: leveva::link::LinkingSet::new(), 50 50 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 51 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 51 52 auth: leveva::server::AuthConfig::default(), 52 53 admin: Default::default(), 53 54 })
+1
leveva/tests/away_proptest.rs
··· 52 52 conn_limits: leveva::connlimit::ConnLimits::new(), 53 53 linking: leveva::link::LinkingSet::new(), 54 54 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 55 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 55 56 auth: leveva::server::AuthConfig::default(), 56 57 admin: Default::default(), 57 58 })
+1
leveva/tests/bot_mode_proptest.rs
··· 52 52 conn_limits: leveva::connlimit::ConnLimits::new(), 53 53 linking: leveva::link::LinkingSet::new(), 54 54 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 55 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 55 56 auth: leveva::server::AuthConfig::default(), 56 57 admin: Default::default(), 57 58 })
+1
leveva/tests/bot_tag_proptest.rs
··· 42 42 conn_limits: leveva::connlimit::ConnLimits::new(), 43 43 linking: leveva::link::LinkingSet::new(), 44 44 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 45 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 45 46 auth: leveva::server::AuthConfig::default(), 46 47 admin: Default::default(), 47 48 })
+1
leveva/tests/cap_proptest.rs
··· 43 43 conn_limits: leveva::connlimit::ConnLimits::new(), 44 44 linking: leveva::link::LinkingSet::new(), 45 45 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 46 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 46 47 auth: leveva::server::AuthConfig::default(), 47 48 admin: Default::default(), 48 49 })
+1
leveva/tests/channel_lifecycle_proptest.rs
··· 84 84 conn_limits: leveva::connlimit::ConnLimits::new(), 85 85 linking: leveva::link::LinkingSet::new(), 86 86 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 87 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 87 88 auth: leveva::server::AuthConfig::default(), 88 89 admin: Default::default(), 89 90 })
+1
leveva/tests/channels_proptest.rs
··· 58 58 conn_limits: leveva::connlimit::ConnLimits::new(), 59 59 linking: leveva::link::LinkingSet::new(), 60 60 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 61 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 61 62 auth: leveva::server::AuthConfig::default(), 62 63 admin: Default::default(), 63 64 })
+1
leveva/tests/control_proptest.rs
··· 51 51 conn_limits: leveva::connlimit::ConnLimits::new(), 52 52 linking: leveva::link::LinkingSet::new(), 53 53 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 54 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 54 55 auth: leveva::server::AuthConfig::default(), 55 56 admin: Admin::default(), 56 57 })
+1
leveva/tests/echo_message_proptest.rs
··· 51 51 conn_limits: leveva::connlimit::ConnLimits::new(), 52 52 linking: leveva::link::LinkingSet::new(), 53 53 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 54 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 54 55 auth: leveva::server::AuthConfig::default(), 55 56 admin: Default::default(), 56 57 })
+1
leveva/tests/etrace_proptest.rs
··· 51 51 conn_limits: leveva::connlimit::ConnLimits::new(), 52 52 linking: leveva::link::LinkingSet::new(), 53 53 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 54 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 54 55 auth: leveva::server::AuthConfig::default(), 55 56 admin: Admin::default(), 56 57 })
+1
leveva/tests/extended_join_proptest.rs
··· 48 48 conn_limits: leveva::connlimit::ConnLimits::new(), 49 49 linking: leveva::link::LinkingSet::new(), 50 50 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 51 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 51 52 auth: leveva::server::AuthConfig::default(), 52 53 admin: Default::default(), 53 54 })
+1
leveva/tests/extended_monitor_proptest.rs
··· 44 44 conn_limits: leveva::connlimit::ConnLimits::new(), 45 45 linking: leveva::link::LinkingSet::new(), 46 46 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 47 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 47 48 auth: leveva::server::AuthConfig::default(), 48 49 admin: Default::default(), 49 50 })
+1
leveva/tests/feed_framing_proptest.rs
··· 61 61 conn_limits: leveva::connlimit::ConnLimits::new(), 62 62 linking: leveva::link::LinkingSet::new(), 63 63 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 64 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 64 65 auth: leveva::server::AuthConfig::default(), 65 66 admin: Default::default(), 66 67 })
+1
leveva/tests/flood_delay.rs
··· 43 43 conn_limits: leveva::connlimit::ConnLimits::new(), 44 44 linking: leveva::link::LinkingSet::new(), 45 45 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 46 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 46 47 auth: leveva::server::AuthConfig::default(), 47 48 admin: Default::default(), 48 49 })
+1
leveva/tests/help_users_proptest.rs
··· 59 59 conn_limits: leveva::connlimit::ConnLimits::new(), 60 60 linking: leveva::link::LinkingSet::new(), 61 61 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 62 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 62 63 auth: leveva::server::AuthConfig::default(), 63 64 admin: Admin::default(), 64 65 })
+1
leveva/tests/invite_notify_proptest.rs
··· 51 51 conn_limits: leveva::connlimit::ConnLimits::new(), 52 52 linking: leveva::link::LinkingSet::new(), 53 53 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 54 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 54 55 auth: leveva::server::AuthConfig::default(), 55 56 admin: Default::default(), 56 57 })
+1
leveva/tests/invite_proptest.rs
··· 73 73 conn_limits: leveva::connlimit::ConnLimits::new(), 74 74 linking: leveva::link::LinkingSet::new(), 75 75 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 76 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 76 77 auth: leveva::server::AuthConfig::default(), 77 78 admin: Default::default(), 78 79 })
+1
leveva/tests/keepalive_proptest.rs
··· 51 51 conn_limits: leveva::connlimit::ConnLimits::new(), 52 52 linking: leveva::link::LinkingSet::new(), 53 53 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 54 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 54 55 auth: leveva::server::AuthConfig::default(), 55 56 admin: Default::default(), 56 57 })
+1
leveva/tests/kick_proptest.rs
··· 62 62 conn_limits: leveva::connlimit::ConnLimits::new(), 63 63 linking: leveva::link::LinkingSet::new(), 64 64 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 65 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 65 66 auth: leveva::server::AuthConfig::default(), 66 67 admin: Default::default(), 67 68 })
+1
leveva/tests/kill_proptest.rs
··· 62 62 conn_limits: leveva::connlimit::ConnLimits::new(), 63 63 linking: leveva::link::LinkingSet::new(), 64 64 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 65 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 65 66 auth: leveva::server::AuthConfig::default(), 66 67 admin: Default::default(), 67 68 })
+1
leveva/tests/kline_proptest.rs
··· 53 53 conn_limits: leveva::connlimit::ConnLimits::new(), 54 54 linking: leveva::link::LinkingSet::new(), 55 55 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 56 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 56 57 auth: leveva::server::AuthConfig::default(), 57 58 admin: Admin::default(), 58 59 })
+1
leveva/tests/knock_proptest.rs
··· 51 51 conn_limits: leveva::connlimit::ConnLimits::new(), 52 52 linking: leveva::link::LinkingSet::new(), 53 53 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 54 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 54 55 auth: leveva::server::AuthConfig::default(), 55 56 admin: Admin::default(), 56 57 })
+63
leveva/tests/knock_user_throttle_proptest.rs
··· 1 + //! Property-based fuzzing of the **per-user** KNOCK throttle's peek/arm primitives 2 + //! ([`KnockThrottle::cooling`] / [`KnockThrottle::arm`], P11 slice 209 — charybdis `knock_delay`). 3 + //! 4 + //! Slice 200's `knock_throttle_proptest.rs` exhausts the atomic [`admit`](KnockThrottle::admit) used 5 + //! for the per-channel table. The per-user table is driven by the *split* primitives the command 6 + //! layer uses to gate two throttles at once: `cooling` peeks (no arm), `arm` commits a window only 7 + //! once a knock is actually delivered. This file pins that split contract. 8 + //! 9 + //! - **`cooling_and_arm_track_the_clock`** — over a monotone clock and arbitrary delay, a 10 + //! hand-tracked hold predicts `cooling` exactly: it is `true` iff a live armed hold has not yet 11 + //! expired. A peeked-but-not-armed step (the throttled case) never changes the hold; only `arm` 12 + //! (re)sets it. Never panics. 13 + //! - **`arbitrary_uid_key_never_panics`** — `cooling`/`arm` over arbitrary key strings (UIDs and 14 + //! non-UID edges) and saturating-edge clock/delay values are total. 15 + 16 + use leveva::knock_throttle::KnockThrottle; 17 + use proptest::prelude::*; 18 + 19 + proptest! { 20 + #![proptest_config(ProptestConfig::with_cases(2000))] 21 + 22 + /// Model the per-user gate by hand: at each (monotone) clock step we peek `cooling`; when it is 23 + /// clear we "deliver" and `arm` the window (mirroring the command layer, which arms only on a 24 + /// delivered knock). The store must agree with the hand-tracked hold on every step. 25 + #[test] 26 + fn cooling_and_arm_track_the_clock( 27 + delay in 1i64..1000, 28 + gaps in proptest::collection::vec(0i64..2000, 1..40), 29 + ) { 30 + let t = KnockThrottle::new(); 31 + let mut now = 0i64; 32 + // `None` = no live hold yet (the first peek is always clear). 33 + let mut hold: Option<i64> = None; 34 + for g in gaps { 35 + now += g; 36 + let live = matches!(hold, Some(h) if now < h); 37 + // `cooling` must report exactly whether a live hold is in force — and must NOT arm, 38 + // so calling it twice is idempotent. 39 + prop_assert_eq!(t.cooling("000AAAAAA", now), live, "cooling now={} hold={:?}", now, hold); 40 + prop_assert_eq!(t.cooling("000AAAAAA", now), live, "cooling must not arm"); 41 + if !live { 42 + // Clear → "deliver" and arm the next window (charybdis arms on success only). 43 + t.arm("000AAAAAA", now, delay); 44 + hold = Some(now.saturating_add(delay)); 45 + } 46 + // A throttled step left `hold` untouched (the inverse: a rejected knock consumes nothing). 47 + } 48 + } 49 + 50 + /// Arbitrary key strings and clock/delay values must never panic either primitive. 51 + #[test] 52 + fn arbitrary_uid_key_never_panics( 53 + key in ".{0,40}", 54 + now in any::<i64>(), 55 + delay in any::<i64>(), 56 + ) { 57 + let t = KnockThrottle::new(); 58 + let _ = t.cooling(&key, now); 59 + t.arm(&key, now, delay); 60 + // After a saturating-edge arm the peek is still total. 61 + let _ = t.cooling(&key, now); 62 + } 63 + }
+1
leveva/tests/mkpasswd_proptest.rs
··· 58 58 conn_limits: leveva::connlimit::ConnLimits::new(), 59 59 linking: leveva::link::LinkingSet::new(), 60 60 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 61 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 61 62 auth: leveva::server::AuthConfig::default(), 62 63 admin: Admin::default(), 63 64 })
+1
leveva/tests/modes_proptest.rs
··· 59 59 conn_limits: leveva::connlimit::ConnLimits::new(), 60 60 linking: leveva::link::LinkingSet::new(), 61 61 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 62 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 62 63 auth: leveva::server::AuthConfig::default(), 63 64 admin: Default::default(), 64 65 })
+1
leveva/tests/monitor_proptest.rs
··· 50 50 conn_limits: leveva::connlimit::ConnLimits::new(), 51 51 linking: leveva::link::LinkingSet::new(), 52 52 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 53 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 53 54 auth: leveva::server::AuthConfig::default(), 54 55 admin: Default::default(), 55 56 })
+1
leveva/tests/motd_reload_proptest.rs
··· 49 49 conn_limits: leveva::connlimit::ConnLimits::new(), 50 50 linking: leveva::link::LinkingSet::new(), 51 51 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 52 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 52 53 auth: leveva::server::AuthConfig::default(), 53 54 admin: Default::default(), 54 55 })
+1
leveva/tests/msgid_proptest.rs
··· 43 43 conn_limits: leveva::connlimit::ConnLimits::new(), 44 44 linking: leveva::link::LinkingSet::new(), 45 45 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 46 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 46 47 auth: leveva::server::AuthConfig::default(), 47 48 admin: Default::default(), 48 49 })
+1
leveva/tests/multi_prefix_proptest.rs
··· 41 41 conn_limits: leveva::connlimit::ConnLimits::new(), 42 42 linking: leveva::link::LinkingSet::new(), 43 43 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 44 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 44 45 auth: leveva::server::AuthConfig::default(), 45 46 admin: Default::default(), 46 47 })
+1
leveva/tests/names_list_proptest.rs
··· 58 58 conn_limits: leveva::connlimit::ConnLimits::new(), 59 59 linking: leveva::link::LinkingSet::new(), 60 60 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 61 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 61 62 auth: leveva::server::AuthConfig::default(), 62 63 admin: Default::default(), 63 64 })
+1
leveva/tests/no_implicit_names_proptest.rs
··· 41 41 conn_limits: leveva::connlimit::ConnLimits::new(), 42 42 linking: leveva::link::LinkingSet::new(), 43 43 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 44 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 44 45 auth: leveva::server::AuthConfig::default(), 45 46 admin: Default::default(), 46 47 })
+1
leveva/tests/oper_proptest.rs
··· 60 60 conn_limits: leveva::connlimit::ConnLimits::new(), 61 61 linking: leveva::link::LinkingSet::new(), 62 62 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 63 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 63 64 auth: leveva::server::AuthConfig::default(), 64 65 admin: Default::default(), 65 66 })
+1
leveva/tests/oper_tag_proptest.rs
··· 53 53 conn_limits: leveva::connlimit::ConnLimits::new(), 54 54 linking: leveva::link::LinkingSet::new(), 55 55 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 56 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 56 57 auth: leveva::server::AuthConfig::default(), 57 58 admin: Default::default(), 58 59 })
+1
leveva/tests/ping_proptest.rs
··· 48 48 conn_limits: leveva::connlimit::ConnLimits::new(), 49 49 linking: leveva::link::LinkingSet::new(), 50 50 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 51 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 51 52 auth: leveva::server::AuthConfig::default(), 52 53 admin: Default::default(), 53 54 })
+1
leveva/tests/prereg_flood_proptest.rs
··· 47 47 conn_limits: leveva::connlimit::ConnLimits::new(), 48 48 linking: leveva::link::LinkingSet::new(), 49 49 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 50 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 50 51 auth: leveva::server::AuthConfig::default(), 51 52 admin: Default::default(), 52 53 })
+1
leveva/tests/privmsg_proptest.rs
··· 55 55 conn_limits: leveva::connlimit::ConnLimits::new(), 56 56 linking: leveva::link::LinkingSet::new(), 57 57 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 58 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 58 59 auth: leveva::server::AuthConfig::default(), 59 60 admin: Default::default(), 60 61 })
+1
leveva/tests/query_routing_proptest.rs
··· 90 90 conn_limits: leveva::connlimit::ConnLimits::new(), 91 91 linking: leveva::link::LinkingSet::new(), 92 92 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 93 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 93 94 auth: leveva::server::AuthConfig::default(), 94 95 admin: Admin::default(), 95 96 });
+1
leveva/tests/redact_proptest.rs
··· 42 42 conn_limits: leveva::connlimit::ConnLimits::new(), 43 43 linking: leveva::link::LinkingSet::new(), 44 44 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 45 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 45 46 auth: leveva::server::AuthConfig::default(), 46 47 admin: Default::default(), 47 48 })
+1
leveva/tests/registration_proptest.rs
··· 44 44 conn_limits: leveva::connlimit::ConnLimits::new(), 45 45 linking: leveva::link::LinkingSet::new(), 46 46 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 47 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 47 48 auth: leveva::server::AuthConfig::default(), 48 49 admin: Default::default(), 49 50 })
+1
leveva/tests/registry_proptest.rs
··· 47 47 conn_limits: leveva::connlimit::ConnLimits::new(), 48 48 linking: leveva::link::LinkingSet::new(), 49 49 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 50 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 50 51 auth: leveva::server::AuthConfig::default(), 51 52 admin: Default::default(), 52 53 })
+1
leveva/tests/server_query_proptest.rs
··· 51 51 conn_limits: leveva::connlimit::ConnLimits::new(), 52 52 linking: leveva::link::LinkingSet::new(), 53 53 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 54 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 54 55 auth: leveva::server::AuthConfig::default(), 55 56 admin, 56 57 })
+1
leveva/tests/server_time_proptest.rs
··· 116 116 conn_limits: leveva::connlimit::ConnLimits::new(), 117 117 linking: leveva::link::LinkingSet::new(), 118 118 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 119 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 119 120 auth: leveva::server::AuthConfig::default(), 120 121 admin: Default::default(), 121 122 })
+1
leveva/tests/server_topology_proptest.rs
··· 49 49 conn_limits: leveva::connlimit::ConnLimits::new(), 50 50 linking: leveva::link::LinkingSet::new(), 51 51 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 52 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 52 53 auth: leveva::server::AuthConfig::default(), 53 54 admin: Admin::default(), 54 55 })
+1
leveva/tests/snotice_proptest.rs
··· 47 47 conn_limits: leveva::connlimit::ConnLimits::new(), 48 48 linking: leveva::link::LinkingSet::new(), 49 49 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 50 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 50 51 auth: leveva::server::AuthConfig::default(), 51 52 admin: Default::default(), 52 53 })
+1
leveva/tests/summon_proptest.rs
··· 46 46 conn_limits: leveva::connlimit::ConnLimits::new(), 47 47 linking: leveva::link::LinkingSet::new(), 48 48 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 49 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 49 50 auth: leveva::server::AuthConfig::default(), 50 51 admin: Admin::default(), 51 52 })
+1
leveva/tests/tagmsg_proptest.rs
··· 50 50 conn_limits: leveva::connlimit::ConnLimits::new(), 51 51 linking: leveva::link::LinkingSet::new(), 52 52 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 53 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 53 54 auth: leveva::server::AuthConfig::default(), 54 55 admin: Default::default(), 55 56 })
+1
leveva/tests/topic_proptest.rs
··· 62 62 conn_limits: leveva::connlimit::ConnLimits::new(), 63 63 linking: leveva::link::LinkingSet::new(), 64 64 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 65 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 65 66 auth: leveva::server::AuthConfig::default(), 66 67 admin: Default::default(), 67 68 })
+1
leveva/tests/trace_proptest.rs
··· 52 52 conn_limits: leveva::connlimit::ConnLimits::new(), 53 53 linking: leveva::link::LinkingSet::new(), 54 54 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 55 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 55 56 auth: leveva::server::AuthConfig::default(), 56 57 admin: Admin::default(), 57 58 })
+1
leveva/tests/userhost_in_names_proptest.rs
··· 43 43 conn_limits: leveva::connlimit::ConnLimits::new(), 44 44 linking: leveva::link::LinkingSet::new(), 45 45 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 46 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 46 47 auth: leveva::server::AuthConfig::default(), 47 48 admin: Default::default(), 48 49 })
+1
leveva/tests/userhost_ison_proptest.rs
··· 54 54 conn_limits: leveva::connlimit::ConnLimits::new(), 55 55 linking: leveva::link::LinkingSet::new(), 56 56 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 57 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 57 58 auth: leveva::server::AuthConfig::default(), 58 59 admin: Default::default(), 59 60 })
+1
leveva/tests/usermode_proptest.rs
··· 54 54 conn_limits: leveva::connlimit::ConnLimits::new(), 55 55 linking: leveva::link::LinkingSet::new(), 56 56 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 57 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 57 58 auth: leveva::server::AuthConfig::default(), 58 59 admin: Default::default(), 59 60 })
+1
leveva/tests/wallops_proptest.rs
··· 61 61 conn_limits: leveva::connlimit::ConnLimits::new(), 62 62 linking: leveva::link::LinkingSet::new(), 63 63 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 64 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 64 65 auth: leveva::server::AuthConfig::default(), 65 66 admin: Default::default(), 66 67 })
+1
leveva/tests/who_proptest.rs
··· 56 56 conn_limits: leveva::connlimit::ConnLimits::new(), 57 57 linking: leveva::link::LinkingSet::new(), 58 58 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 59 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 59 60 auth: leveva::server::AuthConfig::default(), 60 61 admin: Default::default(), 61 62 })
+1
leveva/tests/whois_modes_proptest.rs
··· 48 48 conn_limits: leveva::connlimit::ConnLimits::new(), 49 49 linking: leveva::link::LinkingSet::new(), 50 50 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 51 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 51 52 auth: leveva::server::AuthConfig::default(), 52 53 admin: Default::default(), 53 54 })
+1
leveva/tests/whois_proptest.rs
··· 55 55 conn_limits: leveva::connlimit::ConnLimits::new(), 56 56 linking: leveva::link::LinkingSet::new(), 57 57 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 58 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 58 59 auth: leveva::server::AuthConfig::default(), 59 60 admin: Default::default(), 60 61 })
+1
leveva/tests/whowas_proptest.rs
··· 66 66 conn_limits: leveva::connlimit::ConnLimits::new(), 67 67 linking: leveva::link::LinkingSet::new(), 68 68 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 69 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 69 70 auth: leveva::server::AuthConfig::default(), 70 71 admin: Default::default(), 71 72 });
+1
leveva/tests/whox_proptest.rs
··· 46 46 conn_limits: leveva::connlimit::ConnLimits::new(), 47 47 linking: leveva::link::LinkingSet::new(), 48 48 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 49 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 49 50 auth: leveva::server::AuthConfig::default(), 50 51 admin: Default::default(), 51 52 })