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.

ircd.rs / docs / superpowers / plans / 2026-06-10-p11-slice70-monitor.md
7.5 kB

P11 slice 70 — IRCv3 MONITOR (online/offline watch list)#

Goal#

Implement the IRCv3 MONITOR extension in leveva: a per-connection watch list of nicks that the server proactively reports online/offline transitions for, replacing client-side ISON polling. Spec: docs/rfcs/ircv3/monitor.md. This is the next substantial IRCv3-track feature (the track note flagged it as "the next substantial one" after the message-tags follow-ons). leveva-native (the C 2.11 oracle has no MONITOR) → unit + golden + proptest, no leveva-integration differential.

Locked decisions#

  • No new capability. MONITOR is advertised via the MONITOR=<limit> ISUPPORT (005) token, not a CAP (per the spec). cap.rs/ClientCaps/SUPPORTED are untouched.
  • The watch state lives in a shared Monitors index, NOT on Registered. Keeping the per-connection list inside ctx.monitors keyed by the watcher's UID avoids touching the Registered struct (and its ~25 test-literal sites — see [[leveva-ircv3-track]] churn note). The index holds two views under one lock:
    • by_target: folded-nick → set<watcher Uid> — the reverse index for online/offline fanout.
    • by_watcher: Uid → Vec<display target> — insertion-ordered, for MONITOR L/S and disconnect cleanup. Dedup is by RFC-1459 fold; lists are ≤ limit so a linear scan is fine.
  • MONITOR_LIMIT = 100 (the ISUPPORT value).
  • Online status is read from the registry (registry.get(nick)), which is the truth for both local and remote (S2S UNICK) users. Live notifications fire only on LOCAL events (registration, disconnect, nick change) — remote online/offline transitions arriving over S2S do not yet trigger MONITOR notifications (single-server-local posture, deferred — the track's consistent stance since slice 59).

Design#

New module leveva/src/monitor.rs (pub mod monitor; in lib.rs)#

  • pub const MONITOR_LIMIT: usize = 100;
  • struct Monitors { inner: Mutex<Inner> } with Inner { by_target: HashMap<String, HashSet<Uid>>, by_watcher: HashMap<Uid, Vec<String>> }.
    • add(watcher, display) -> bool — add iff not already present (by fold) and returns true if newly added. Updates both maps.
    • contains(watcher, display) -> bool, count(watcher) -> usize.
    • remove(watcher, display) — drop from both; prune an emptied by_target set.
    • clear(watcher) — remove the watcher from every target set (pruning empties) + drop its by_watcher entry. Called on disconnect.
    • list(watcher) -> Vec<String> — the watcher's display targets, insertion order.
    • watchers_of(folded_target) -> Vec<Uid> — for fanout (sorted for determinism).
  • Free fns (the live-notify plane, called from the session/nick hooks):
    • notify_online(ctx, nick, user, host) — deliver 730 <watcher-nick> :nick!user@host to each watcher of fold(nick) (watcher nick resolved live via registry.nick_of).
    • notify_offline(ctx, nick) — deliver 731 <watcher-nick> :nick to each watcher.

New handler leveva/src/command/monitor.rs (mod monitor; + dispatch arm)#

MONITOR <modifier> [targets], one modifier per command:

  • no param → 461 ERR_NEEDMOREPARAMS.
  • + t1,t2,… — add each (valid Nick, skip invalid). Already-monitored → no-op. Past the limit → collect into a single 734 ERR_MONLISTFULL <limit> <rejected> :Monitor list is full. For the newly-added targets: online → 730 (hostmask nick!user@host), offline → 731 (bare nick), each comma-chunked within the 512 wire budget.
  • - t1,t2,… — remove each. No output.
  • C — clear all. No output.
  • L732 (comma-chunked list) then 733 :End of MONITOR list (just 733 if empty).
  • S — per target: online 730 / offline 731, comma-chunked.
  • unknown modifier → silent no-op.

Targets gathered from params()[1..] + trailing(), split on ,, empties filtered. Comma-chunking helper budgets :server NNN <nick> : + joined items ≤ MAX_CONTENT_LEN.

Live-notify hooks#

  • session.rs::finalize_registration — after a successful try_claim, call monitor::notify_online(ctx, &nick, &user, &self.host).
  • session.rs::release (the Phase::Registered arm) — monitor::notify_offline(ctx, &client.nick) then ctx.monitors.clear(&client.uid) (drop this watcher's own list).
  • command/nick.rs::nick_change (success) — notify_offline(ctx, &old_nick) then notify_online(ctx, &new, user, host) (old nick goes offline, new comes online).

Numerics (numeric.rs)#

Add RplMononline=730, RplMonoffline=731, RplMonlist=732, RplEndofmonlist=733, ErrMonlistfull=734 (variant + name() + try_from arms). is_error stays range-based (730–734 are all ≥ 600 → classified replies; harmless, 734 is a functional error reply).

ISUPPORT (isupport.rs)#

Append format!("MONITOR={}", crate::monitor::MONITOR_LIMIT) to tokens() (005 snapshots regenerate — golden_registration etc.).

Tests (TDD, inverse invariants)#

  • monitor.rs units: add/contains/count/remove/clear/list/watchers_of round-trips with inverses — remove un-monitors (watchers_of no longer lists the UID); clear drops the watcher from every target AND empties its list; a re-add after remove works; dedup keeps one entry (fold-aware: Bob/bob); pruning empties (a target with no watchers disappears); watchers_of on an unknown target is empty; clear of an unknown UID is a no-op.
  • command/monitor.rs units: + bob when bob online → 730 …:bob!u@h; when offline → 731. - bob removes (inverse: a later S no longer reports it). C clears (inverse: L → just 733). L lists added targets + 733; empty → only 733. S reports current online/offline. no-param → 461. invalid nick skipped. dedup: + bob twice keeps one (second is a no-op). limit overflow → 734 with the rejected tail (and the under-limit prefix still added).
  • live notify units (drive through the real seams): a watcher with an observable mailbox gets 730 when the monitored nick registers, 731 when it disconnects, and 731+730 across a nick change; inverse: a non-watcher's mailbox stays empty; after -/C the ex-watcher gets nothing on the next transition.
  • Golden golden_monitor.rs (real binary, two clients): alice MONITOR + bob731 (bob offline); bob registers → alice reads 730 alice :bob!…@127.0.0.1; bob quits → alice reads 731 alice :bob. MONITOR L/S snapshotted. Deterministic host → no canonicalizer rule.
  • Proptest monitor_proptest.rs (the fuzz target = the Monitors index + chunker): a model set vs the index agree on membership/count after an arbitrary add/remove/clear sequence; watchers_of ⟺ the watcher monitors the target; the comma-chunker never exceeds the wire budget and never splits a token, and re-parses to the same item set; panic-freedom on arbitrary nick bytes.

Divergences#

  • Single-server-local live notify (remote S2S transitions deferred); MONITOR is CAP-less (ISUPPORT only, per spec); 730 always carries the hostmask (spec MAY); re-adding an already-monitored target is a silent no-op (no re-report); invalid-nick targets skipped silently; unknown modifier is a no-op.

Gate#

cargo test -p leveva green (new monitor units + command units + golden + proptest + regenerated 005 snapshots); cargo clippy -p leveva --tests clean; cargo build --workspace 0 warnings.