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 / leveva / tests / etrace_proptest.rs
7.5 kB 186 lines
1//! Property-based fuzzing of `ETRACE` at the [`dispatch`](leveva::command::dispatch) 2//! layer. ETRACE is **fully oper-gated** (the whole command needs `ACL_TRACE`), so the 3//! fuzzing surface is: the privilege gate (no `trace` → exactly `[481]`); the privileged 4//! sweep (one `708` per seeded local person in folded-nick order, then one `759`); the 5//! privileged single-target lookup (a registered nick → `[708, 759]`, otherwise `[759]` 6//! only); and arbitrary args / nick / privilege — none of which may panic the handler or 7//! emit a non-ETRACE numeric. 8//! 9//! - **`gate_then_sweep`** — a no-arg `ETRACE`: a requester *without* `ACL_TRACE` yields 10//! exactly `[481]`; *with* it, exactly one `708` per seeded person (folded order) then 11//! one closing `759`. 12//! - **`target_resolves_or_terminator`** — a privileged `ETRACE <target>`: a registered 13//! nick → `[708, 759]` (the `708` naming the target), any other token → `[759]` only. 14//! - **`arbitrary_never_panics`** — arbitrary printable args / nick / privilege never panic 15//! and emit only `{481, 708, 759}`. 16 17use std::sync::Arc; 18 19use leveva::channel::Channels; 20use leveva::command::{dispatch, Registered}; 21use leveva::config::{Admin, OperPrivilege}; 22use leveva::registry::Registry; 23use leveva::server::{Counters, ServerContext}; 24use leveva::{casemap, Message, Sid, Uid, UserMode}; 25use proptest::prelude::*; 26use tokio::sync::mpsc::unbounded_channel; 27 28const SERVER: &str = "leveva.test"; 29 30fn ctx() -> Arc<ServerContext> { 31 Arc::new(ServerContext { 32 name: SERVER.to_string(), 33 description: "T".to_string(), 34 network: "TestNet".to_string(), 35 version: "leveva-0.0.0", 36 created: "FIXED".to_string(), 37 motd: None, 38 counters: Counters::default(), 39 registry: Registry::new(), 40 channels: Channels::new(), 41 whowas: leveva::whowas::WhowasHistory::default(), 42 uids: leveva::uid::UidGenerator::new(Sid::try_from("0ABC").unwrap()), 43 default_user_modes: 0, 44 default_channel_modes: 0, 45 operators: Vec::new(), 46 stats_conf: leveva::server::StatsConf::default(), 47 klines: leveva::kline::KlineStore::new(), 48 dlines: leveva::dline::DlineStore::new(), 49 net: leveva::s2s::Network::new(), 50 peers: leveva::s2s::PeerLinks::new(), 51 monitors: leveva::monitor::Monitors::new(), 52 conn_limits: leveva::connlimit::ConnLimits::new(), 53 linking: leveva::link::LinkingSet::new(), 54 knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 55 knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 56 resvs: leveva::resv::ResvStore::new(), 57 history: leveva::history::History::disabled(), 58 auth: leveva::server::AuthConfig::default(), 59 admin: Admin::default(), 60 }) 61} 62 63/// A distinct UID for seed index `i` (≤ 19, the strategy cap): `0ABCAAAA{A..T}`. 64fn uid_for(i: usize) -> Uid { 65 Uid::try_from(format!("0ABCAAAA{}", (b'A' + i as u8) as char).as_str()).unwrap() 66} 67 68/// Seed `nick` (index `i`) with `is_oper` into the registry; return its UID. 69fn seed(ctx: &ServerContext, i: usize, nick: &str, is_oper: bool) -> Uid { 70 let uid = uid_for(i); 71 let (tx, _rx) = unbounded_channel(); 72 ctx.registry 73 .try_claim(&uid, nick, "u", "h", "r", tx) 74 .expect("free nick"); 75 if is_oper { 76 ctx.registry.set_modes(&uid, UserMode::Oper.bit()); 77 } 78 uid 79} 80 81fn client(nick: &str, trace_priv: bool) -> Registered { 82 Registered { 83 uid: uid_for(0), 84 nick: nick.to_string(), 85 user: "u".into(), 86 host: "127.0.0.1".into(), 87 realname: "R".into(), 88 modes: 0, 89 privileges: if trace_priv { 90 vec![OperPrivilege::Trace] 91 } else { 92 Vec::new() 93 }, 94 caps: Default::default(), 95 } 96} 97 98fn codes(ms: &[Message]) -> Vec<&str> { 99 ms.iter().map(|m| m.command()).collect() 100} 101 102/// A parse-safe nick token. The first entry is always the requester. 103fn members() -> impl Strategy<Value = Vec<String>> { 104 prop::collection::vec("[a-zA-Z][a-zA-Z0-9]{0,5}", 1..8) 105} 106 107/// Dedup `raw` by folded nick (first wins), preserving order — registry claims are keyed 108/// by folded nick, so collisions would otherwise fail the seed. 109fn dedup(raw: &[String]) -> Vec<String> { 110 let mut seen = std::collections::HashSet::new(); 111 raw.iter() 112 .filter(|n| seen.insert(casemap::fold(n))) 113 .cloned() 114 .collect() 115} 116 117proptest! { 118 #![proptest_config(ProptestConfig::with_cases(512))] 119 120 /// A no-arg `ETRACE`: unprivileged → `[481]`; privileged → one `708` per person 121 /// (folded order) then one `759`. 122 #[test] 123 fn gate_then_sweep(raw in members(), trace_priv in any::<bool>()) { 124 let people = dedup(&raw); 125 let ctx = ctx(); 126 for (i, nick) in people.iter().enumerate() { 127 // Oper-status here only flips the 708 Oper/User label, not membership. 128 seed(&ctx, i, nick, i % 2 == 0); 129 } 130 let me = client(&people[0], trace_priv); 131 let r = dispatch(&mut me.clone(), &Message::parse("ETRACE").unwrap(), &ctx); 132 let c = codes(&r); 133 134 if !trace_priv { 135 prop_assert_eq!(&c, &["481"]); 136 } else { 137 // One 708 per seeded person, folded order, then a single trailing 759. 138 let mut folded: Vec<String> = people.iter().map(|n| casemap::fold(n)).collect(); 139 folded.sort(); 140 let mut want: Vec<&str> = vec!["708"; folded.len()]; 141 want.push("759"); 142 prop_assert_eq!(&c, &want); 143 // The 708 nick column follows folded order; the 759 names the server. 144 let got_nicks: Vec<String> = 145 r[..r.len() - 1].iter().map(|m| casemap::fold(&m.params()[3])).collect(); 146 prop_assert_eq!(got_nicks, folded); 147 prop_assert_eq!(r.last().unwrap().params()[1].as_str(), SERVER); 148 } 149 } 150 151 /// A privileged `ETRACE <target>`: registered nick → `[708, 759]`; else `[759]`. 152 #[test] 153 fn target_resolves_or_terminator(raw in members(), target in "[a-zA-Z0-9]{1,8}") { 154 let people = dedup(&raw); 155 let ctx = ctx(); 156 for (i, nick) in people.iter().enumerate() { 157 seed(&ctx, i, nick, false); 158 } 159 let me = client(&people[0], true); // privileged 160 let line = format!("ETRACE {target}"); 161 let r = dispatch(&mut me.clone(), &Message::parse(&line).unwrap(), &ctx); 162 let c = codes(&r); 163 164 if let Some(nick) = people.iter().find(|n| casemap::fold(n) == casemap::fold(&target)) { 165 prop_assert_eq!(&c, &["708", "759"]); 166 prop_assert_eq!(casemap::fold(&r[0].params()[3]), casemap::fold(nick)); 167 } else { 168 prop_assert_eq!(&c, &["759"]); 169 } 170 } 171 172 /// Arbitrary args / nick / privilege never panic and emit only ETRACE numerics. 173 #[test] 174 fn arbitrary_never_panics( 175 nick in "[a-zA-Z][a-zA-Z0-9]{0,8}", 176 trace_priv in any::<bool>(), 177 args in prop::collection::vec("[a-zA-Z0-9.*?_-]{0,8}", 0..4), 178 ) { 179 let ctx = ctx(); 180 seed(&ctx, 0, &nick, false); 181 let line = format!("ETRACE {}", args.join(" ")); 182 let me = client(&nick, trace_priv); 183 let r = dispatch(&mut me.clone(), &Message::parse(line.trim()).unwrap(), &ctx); 184 prop_assert!(codes(&r).iter().all(|x| matches!(*x, "481" | "708" | "759"))); 185 } 186}