@mark1russell7/harness 0.14.4 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,633 @@
1
+ //! ctfull target PORT [--fill M]
2
+ //! ctfull client --max M --plan full|reset|fin [--settle-s N] HOST PORT [--mode M] [--max-s N]
3
+ //!
4
+ //! This program is the two ends of the conntrack cells. Each line is JSON with an `ev` and a `t` on
5
+ //! CLOCK_MONOTONIC. The client makes its flows on loopback, thus each flow is one entry of the table
6
+ //! of its own network namespace, and no entry goes to the table of the host.
7
+ //!
8
+ //! The target listens on PORT. With `--fill M`, it holds M flows on its own loopback, so that its
9
+ //! namespace reads M, and it writes a `count` line each second. It reads its count after each connect,
10
+ //! because the rule that starts conntrack in the namespace can come after its start. A flow that does
11
+ //! not raise the count closes with a reset.
12
+ //!
13
+ //! The client does these steps:
14
+ //!
15
+ //! 1. It writes `ready`, and it waits until the count of its namespace is 0: the entries of the run
16
+ //! before it go at a scan. It writes `settled`, or `unsettled` at the end of `--settle-s`.
17
+ //! 2. It holds M flows, and it writes `full`. Then it waits, so that the last flow is assured.
18
+ //! 3. `--plan full` makes connect M + 1, and it writes `stall` with its errno, its time and the
19
+ //! rise of the two counters of the kernel. It then closes each flow with a reset.
20
+ //! 4. `--plan reset` writes `closing`, closes each flow with a reset, and writes `drained` when the
21
+ //! count is 0 or at the end of the wait: the first fall, the zero and the lowest count, from a
22
+ //! poll of 1 ms.
23
+ //! 5. `--plan fin` closes each flow with a FIN from each side, and it writes `closed` with the count.
24
+ //! It makes connect M + 1 with the poll on, and it writes `stall` with the lowest count and the
25
+ //! count after the connect.
26
+
27
+ use harness_native::emit;
28
+ use prober::{errno_name, monotonic_ns};
29
+ use serde_json::{json, Value};
30
+ use std::error::Error;
31
+ use std::net::{Ipv4Addr, SocketAddr, TcpListener, TcpStream};
32
+ use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
33
+ use std::process::ExitCode;
34
+ use std::sync::atomic::{AtomicBool, Ordering};
35
+ use std::sync::{Arc, Mutex, MutexGuard};
36
+ use std::thread::JoinHandle;
37
+ use std::time::Duration;
38
+
39
+ const COUNT: &str = "/proc/sys/net/netfilter/nf_conntrack_count";
40
+ const NETSTAT: &str = "/proc/net/netstat";
41
+ /// the subsystem of ctnetlink in nfnetlink (`NFNL_SUBSYS_CTNETLINK`)
42
+ const CTNETLINK: u16 = 1;
43
+ /// the message of ctnetlink that dumps the statistics of each CPU (`IPCTNL_MSG_CT_GET_STATS_CPU`)
44
+ const GET_STATS_CPU: u16 = 4;
45
+ /// the attribute of the field drop in each message of that dump (`CTA_STATS_DROP`), a big-endian u32
46
+ const STATS_DROP: u16 = 10;
47
+ /// the length of the header of a netlink message, and of the header of nfnetlink after it
48
+ const NLMSG_HEADER: usize = 16;
49
+ const NFGENMSG: usize = 4;
50
+ /// the type bits of an attribute, without the flags of nesting and byte order
51
+ const ATTRIBUTE_TYPE: u16 = 0x3fff;
52
+ /// the wait after the last flow of a fill and after each side closes, in milliseconds
53
+ const SETTLE_MS: u64 = 50;
54
+ /// the time between two reads of the count while a step waits for it, in milliseconds
55
+ const WAIT_MS: u64 = 100;
56
+ /// the longest wait for the count to reach 0 after a reset, in seconds: 10 s and one scan of 60 s, with a margin
57
+ const DRAIN_S: u64 = 90;
58
+ const SECOND_NS: u64 = 1_000_000_000;
59
+
60
+ fn sleep_ms(time: u64) {
61
+ std::thread::sleep(Duration::from_millis(time));
62
+ }
63
+
64
+ fn locked(held: &Mutex<Vec<TcpStream>>) -> MutexGuard<'_, Vec<TcpStream>> {
65
+ held.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
66
+ }
67
+
68
+ /// Raises the soft limit of open files to the hard limit: a full table holds two sockets for each flow.
69
+ fn raise_files() -> std::io::Result<()> {
70
+ let mut limit = libc::rlimit { rlim_cur: 0, rlim_max: 0 };
71
+ // SAFETY: the record is on this stack, and the call writes only that record
72
+ if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) } != 0 {
73
+ return Err(std::io::Error::last_os_error());
74
+ }
75
+ limit.rlim_cur = limit.rlim_max;
76
+ // SAFETY: the record is on this stack, and the call only reads it
77
+ if unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &limit) } != 0 {
78
+ return Err(std::io::Error::last_os_error());
79
+ }
80
+ Ok(())
81
+ }
82
+
83
+ /// Reads the count of the table of this namespace.
84
+ fn count() -> Result<u64, Box<dyn Error>> {
85
+ Ok(std::fs::read_to_string(COUNT)?.trim().parse()?)
86
+ }
87
+
88
+ /// Gives the request of the dump of the conntrack statistics of each CPU: a netlink header, then a nfnetlink header.
89
+ fn stats_request(seq: u32) -> Vec<u8> {
90
+ let length = (NLMSG_HEADER + NFGENMSG) as u32;
91
+ let mut message = Vec::with_capacity(NLMSG_HEADER + NFGENMSG);
92
+ message.extend_from_slice(&length.to_ne_bytes());
93
+ message.extend_from_slice(&((CTNETLINK << 8) | GET_STATS_CPU).to_ne_bytes());
94
+ message.extend_from_slice(&((libc::NLM_F_REQUEST | libc::NLM_F_DUMP) as u16).to_ne_bytes());
95
+ message.extend_from_slice(&seq.to_ne_bytes());
96
+ message.extend_from_slice(&0u32.to_ne_bytes());
97
+ // the family AF_UNSPEC, the version NFNETLINK_V0, and the resource 0
98
+ message.extend_from_slice(&[0, 0, 0, 0]);
99
+ message
100
+ }
101
+
102
+ fn u16_at(bytes: &[u8], at: usize) -> Option<u16> {
103
+ Some(u16::from_ne_bytes(bytes.get(at..at + 2)?.try_into().ok()?))
104
+ }
105
+
106
+ fn u32_at(bytes: &[u8], at: usize) -> Option<u32> {
107
+ Some(u32::from_ne_bytes(bytes.get(at..at + 4)?.try_into().ok()?))
108
+ }
109
+
110
+ /// Netlink pads each message and each attribute to four bytes.
111
+ fn aligned(length: usize) -> usize {
112
+ (length + 3) & !3
113
+ }
114
+
115
+ /// Gives the field drop of the attributes of one message of the dump.
116
+ fn drop_of(attributes: &[u8]) -> Result<u64, String> {
117
+ let mut at = 0;
118
+ while at + 4 <= attributes.len() {
119
+ let length = usize::from(u16_at(attributes, at).ok_or("a short attribute")?);
120
+ let kind = u16_at(attributes, at + 2).ok_or("a short attribute")? & ATTRIBUTE_TYPE;
121
+ if length < 4 || at + length > attributes.len() {
122
+ return Err(format!("an attribute of {length} bytes at byte {at} of {}", attributes.len()));
123
+ }
124
+ if kind == STATS_DROP {
125
+ let value = attributes.get(at + 4..at + 8).ok_or("a short field drop")?;
126
+ return Ok(u64::from(u32::from_be_bytes(value.try_into().map_err(|_| "a short field drop")?)));
127
+ }
128
+ at += aligned(length);
129
+ }
130
+ Err("a message of the statistics with no field drop".into())
131
+ }
132
+
133
+ /// What one datagram of the dump gave: the sum of the field drop of its messages, and whether the dump ended.
134
+ #[derive(Debug, PartialEq)]
135
+ struct Part {
136
+ drops: u64,
137
+ done: bool,
138
+ }
139
+
140
+ /// Reads one datagram of the dump: messages of the statistics of a CPU, an error, or the end of the dump.
141
+ fn stats_part(datagram: &[u8]) -> Result<Part, String> {
142
+ let mut drops = 0;
143
+ let mut at = 0;
144
+ while at + NLMSG_HEADER <= datagram.len() {
145
+ let length = u32_at(datagram, at).ok_or("a short header")? as usize;
146
+ let kind = u16_at(datagram, at + 4).ok_or("a short header")?;
147
+ if length < NLMSG_HEADER || at + length > datagram.len() {
148
+ return Err(format!("a message of {length} bytes at byte {at} of {}", datagram.len()));
149
+ }
150
+ let message = &datagram[at..at + length];
151
+ if i32::from(kind) == libc::NLMSG_DONE {
152
+ return Ok(Part { drops, done: true });
153
+ }
154
+ if i32::from(kind) == libc::NLMSG_ERROR {
155
+ let code = i32::from_ne_bytes(message.get(NLMSG_HEADER..NLMSG_HEADER + 4).ok_or("a short error")?.try_into().map_err(|_| "a short error")?);
156
+ if code != 0 {
157
+ return Err(std::io::Error::from_raw_os_error(-code).to_string());
158
+ }
159
+ } else if kind == (CTNETLINK << 8) | GET_STATS_CPU {
160
+ drops += drop_of(message.get(NLMSG_HEADER + NFGENMSG..).ok_or("a message with no nfnetlink header")?)?;
161
+ }
162
+ at += aligned(length);
163
+ }
164
+ Ok(Part { drops, done: false })
165
+ }
166
+
167
+ fn os_error(what: &str) -> String {
168
+ format!("{what}: {}", std::io::Error::last_os_error())
169
+ }
170
+
171
+ /// Reads the sum of the field drop of the conntrack statistics of this namespace over ctnetlink. A kernel
172
+ /// without CONFIG_NF_CONNTRACK_PROCFS has no /proc/net/stat/nf_conntrack, and the kernel of the runners is
173
+ /// such a kernel. Each message of nfnetlink needs NET_ADMIN in the namespace.
174
+ fn stat_drops() -> Result<u64, String> {
175
+ // SAFETY: the call takes no pointer
176
+ let fd = unsafe { libc::socket(libc::AF_NETLINK, libc::SOCK_RAW | libc::SOCK_CLOEXEC, libc::NETLINK_NETFILTER) };
177
+ if fd < 0 {
178
+ return Err(os_error("no netlink socket"));
179
+ }
180
+ // SAFETY: the descriptor is new, and only this owner closes it
181
+ let socket = unsafe { OwnedFd::from_raw_fd(fd) };
182
+ // the kernel answers each request, and this limit stops a wait that does not end
183
+ let limit = libc::timeval { tv_sec: 5, tv_usec: 0 };
184
+ // SAFETY: the option value is on this stack, passed with its exact size
185
+ let status = unsafe {
186
+ libc::setsockopt(
187
+ socket.as_raw_fd(),
188
+ libc::SOL_SOCKET,
189
+ libc::SO_RCVTIMEO,
190
+ (&limit as *const libc::timeval).cast(),
191
+ std::mem::size_of::<libc::timeval>() as libc::socklen_t,
192
+ )
193
+ };
194
+ if status != 0 {
195
+ return Err(os_error("no limit on the netlink socket"));
196
+ }
197
+ let request = stats_request(1);
198
+ // SAFETY: the buffer is the request, passed with its exact size. An unconnected netlink socket sends to the kernel
199
+ if unsafe { libc::send(socket.as_raw_fd(), request.as_ptr().cast(), request.len(), 0) } < 0 {
200
+ return Err(os_error("the request did not go"));
201
+ }
202
+ let mut buffer = vec![0u8; 65_536];
203
+ let mut total = 0;
204
+ loop {
205
+ // SAFETY: the call writes at most the length of the buffer into it
206
+ let got = unsafe { libc::recv(socket.as_raw_fd(), buffer.as_mut_ptr().cast(), buffer.len(), 0) };
207
+ if got < 0 {
208
+ return Err(os_error("no answer to the request"));
209
+ }
210
+ if got == 0 {
211
+ return Err("the dump ended with no NLMSG_DONE".into());
212
+ }
213
+ let part = stats_part(&buffer[..got as usize])?;
214
+ total += part.drops;
215
+ if part.done {
216
+ return Ok(total);
217
+ }
218
+ }
219
+ }
220
+
221
+ /// Gives one counter of the TcpExt lines of /proc/net/netstat: a line of names, then a line of values.
222
+ fn tcp_ext(text: &str, field: &str) -> Option<u64> {
223
+ let mut rows = text.lines().filter(|line| line.starts_with("TcpExt:"));
224
+ let at = rows.next()?.split_whitespace().position(|name| name == field)?;
225
+ rows.next()?.split_whitespace().nth(at)?.parse().ok()
226
+ }
227
+
228
+ /// Reads the two counters of a stalled connect: the entries that the table refused, and the sends that failed.
229
+ fn counters() -> Result<(u64, u64), Box<dyn Error>> {
230
+ let drops = stat_drops().map_err(|error| format!("no conntrack statistics over netlink: {error}"))?;
231
+ let failed = tcp_ext(&std::fs::read_to_string(NETSTAT)?, "TCPRetransFail").ok_or("no TCPRetransFail in netstat")?;
232
+ Ok((drops, failed))
233
+ }
234
+
235
+ /// Closes a connection with a reset: SO_LINGER with a time of 0, then close(2).
236
+ fn reset(stream: TcpStream) -> std::io::Result<()> {
237
+ let linger = libc::linger { l_onoff: 1, l_linger: 0 };
238
+ // SAFETY: the option value is on this stack, passed with its exact size
239
+ let status = unsafe {
240
+ libc::setsockopt(
241
+ stream.as_raw_fd(),
242
+ libc::SOL_SOCKET,
243
+ libc::SO_LINGER,
244
+ (&linger as *const libc::linger).cast(),
245
+ std::mem::size_of::<libc::linger>() as libc::socklen_t,
246
+ )
247
+ };
248
+ if status == 0 { Ok(()) } else { Err(std::io::Error::last_os_error()) }
249
+ }
250
+
251
+ fn errno_of(error: &std::io::Error) -> String {
252
+ error.raw_os_error().map_or_else(|| error.to_string(), errno_name)
253
+ }
254
+
255
+ /// Accepts each connection of a listener on its own thread, and holds it.
256
+ fn hold_accepted(listener: TcpListener) -> Arc<Mutex<Vec<TcpStream>>> {
257
+ let accepted = Arc::new(Mutex::new(Vec::new()));
258
+ let held = Arc::clone(&accepted);
259
+ std::thread::spawn(move || {
260
+ for stream in listener.incoming().flatten() {
261
+ locked(&held).push(stream);
262
+ }
263
+ });
264
+ accepted
265
+ }
266
+
267
+ /// Holds flows on the loopback of the target until its namespace reads `max`. It never connects at `max`.
268
+ fn fill_neighbor(address: SocketAddr, max: u64, held: &mut Vec<TcpStream>) -> Result<(), Box<dyn Error>> {
269
+ loop {
270
+ let before = count()?;
271
+ if before >= max {
272
+ return Ok(());
273
+ }
274
+ let stream = TcpStream::connect(address)?;
275
+ if count()? > before {
276
+ held.push(stream);
277
+ } else {
278
+ // the rule of the namespace is not in yet, and this flow has no entry
279
+ reset(stream)?;
280
+ sleep_ms(WAIT_MS);
281
+ }
282
+ }
283
+ }
284
+
285
+ fn target(args: &[String]) -> Result<(), Box<dyn Error>> {
286
+ let port: u16 = args.first().ok_or("ctfull target PORT [--fill M]")?.parse()?;
287
+ let fill: Option<u64> = match args.get(1).map(String::as_str) {
288
+ None => None,
289
+ Some("--fill") => Some(args.get(2).ok_or("--fill needs a value")?.parse()?),
290
+ Some(other) => return Err(format!("no flag {other}").into()),
291
+ };
292
+ raise_files()?;
293
+ let listener = TcpListener::bind((Ipv4Addr::UNSPECIFIED, port))?;
294
+ let _accepted = hold_accepted(listener);
295
+ emit("listening", json!({}));
296
+ let mut held = Vec::new();
297
+ let address = SocketAddr::from((Ipv4Addr::LOCALHOST, port));
298
+ loop {
299
+ if let Some(max) = fill {
300
+ fill_neighbor(address, max, &mut held)?;
301
+ emit("count", json!({ "count": count()?, "flows": held.len() }));
302
+ }
303
+ sleep_ms(1_000);
304
+ }
305
+ }
306
+
307
+ #[derive(Clone, Copy, Debug, PartialEq)]
308
+ enum Way {
309
+ Full,
310
+ Reset,
311
+ Fin,
312
+ }
313
+
314
+ #[derive(Debug)]
315
+ struct Plan {
316
+ max: usize,
317
+ way: Way,
318
+ settle_s: u64,
319
+ }
320
+
321
+ fn plan_of(args: &[String]) -> Result<Plan, Box<dyn Error>> {
322
+ let mut max: Option<usize> = None;
323
+ let mut way = None;
324
+ let mut settle_s: u64 = 200;
325
+ let mut places = 0;
326
+ let mut rest = args.iter();
327
+ while let Some(arg) = rest.next() {
328
+ let mut value = || rest.next().ok_or_else(|| format!("{arg} needs a value"));
329
+ match arg.as_str() {
330
+ "--max" => max = Some(value()?.parse()?),
331
+ "--plan" => {
332
+ way = Some(match value()?.as_str() {
333
+ "full" => Way::Full,
334
+ "reset" => Way::Reset,
335
+ "fin" => Way::Fin,
336
+ other => return Err(format!("no plan named {other}").into()),
337
+ })
338
+ }
339
+ "--settle-s" => settle_s = value()?.parse()?,
340
+ // the harness gives these flags to each client, and the plan does not read them
341
+ "--mode" | "--max-s" => {
342
+ value()?;
343
+ }
344
+ other if other.starts_with("--") => return Err(format!("no flag {other}").into()),
345
+ // the host and the port of the target: the flows of the client stay on its loopback
346
+ _ => places += 1,
347
+ }
348
+ }
349
+ match (max, way, places) {
350
+ (Some(max), Some(way), 2) => Ok(Plan { max, way, settle_s }),
351
+ _ => Err("ctfull client --max M --plan full|reset|fin [--settle-s N] HOST PORT".into()),
352
+ }
353
+ }
354
+
355
+ /// Waits until the count of the namespace is 0. It says if the count got there.
356
+ fn settled(wait_s: u64) -> Result<bool, Box<dyn Error>> {
357
+ let start = monotonic_ns();
358
+ loop {
359
+ let now = count()?;
360
+ let waited = monotonic_ns() - start;
361
+ if now == 0 {
362
+ emit("settled", json!({ "waited": waited }));
363
+ return Ok(true);
364
+ }
365
+ if waited > wait_s * SECOND_NS {
366
+ emit("unsettled", json!({ "count": now, "waited": waited }));
367
+ return Ok(false);
368
+ }
369
+ sleep_ms(WAIT_MS);
370
+ }
371
+ }
372
+
373
+ /// what a poll of the count saw
374
+ #[derive(Clone, Copy, Debug, PartialEq)]
375
+ struct Seen {
376
+ first_fall: Option<u64>,
377
+ zero: Option<u64>,
378
+ lowest: u64,
379
+ }
380
+
381
+ impl Seen {
382
+ fn new(level: u64) -> Self {
383
+ Seen { first_fall: None, zero: None, lowest: level }
384
+ }
385
+
386
+ fn read(&mut self, level: u64, at: u64, now: u64) {
387
+ if now < level && self.first_fall.is_none() {
388
+ self.first_fall = Some(at);
389
+ }
390
+ if now == 0 && self.zero.is_none() {
391
+ self.zero = Some(at);
392
+ }
393
+ self.lowest = self.lowest.min(now);
394
+ }
395
+ }
396
+
397
+ /// Reads the count each millisecond, until `stop` or the count of 0.
398
+ fn poll(level: u64, stop: Arc<AtomicBool>) -> JoinHandle<Result<Seen, String>> {
399
+ std::thread::spawn(move || {
400
+ let mut seen = Seen::new(level);
401
+ while !stop.load(Ordering::Relaxed) && seen.zero.is_none() {
402
+ let now = count().map_err(|error| error.to_string())?;
403
+ seen.read(level, monotonic_ns(), now);
404
+ sleep_ms(1);
405
+ }
406
+ Ok(seen)
407
+ })
408
+ }
409
+
410
+ fn seen_of(watch: JoinHandle<Result<Seen, String>>) -> Result<Seen, Box<dyn Error>> {
411
+ Ok(watch.join().map_err(|_| "the poll of the count stopped")??)
412
+ }
413
+
414
+ /// Makes connect M + 1 and gives the line of its end, with the rise of the two counters.
415
+ fn stalled_connect(address: SocketAddr) -> Result<(Value, Option<TcpStream>), Box<dyn Error>> {
416
+ let (drops, failed) = counters()?;
417
+ let start = monotonic_ns();
418
+ let result = TcpStream::connect(address);
419
+ let elapsed = monotonic_ns() - start;
420
+ let (drops_after, failed_after) = counters()?;
421
+ let (errno, stream) = match result {
422
+ Ok(stream) => ("none".to_string(), Some(stream)),
423
+ Err(error) => (errno_of(&error), None),
424
+ };
425
+ let line = json!({
426
+ "errno": errno,
427
+ "elapsed": elapsed,
428
+ "drops": drops_after - drops,
429
+ "retrans_fail": failed_after - failed,
430
+ });
431
+ Ok((line, stream))
432
+ }
433
+
434
+ fn extend(line: Value, more: Value) -> Value {
435
+ let mut joined = line;
436
+ if let (Some(joined), Value::Object(more)) = (joined.as_object_mut(), more) {
437
+ joined.extend(more);
438
+ }
439
+ joined
440
+ }
441
+
442
+ fn reset_all(streams: Vec<TcpStream>) -> std::io::Result<()> {
443
+ for stream in streams {
444
+ reset(stream)?;
445
+ }
446
+ Ok(())
447
+ }
448
+
449
+ fn client(args: &[String]) -> Result<(), Box<dyn Error>> {
450
+ let plan = plan_of(args)?;
451
+ raise_files()?;
452
+ emit("ready", json!({}));
453
+ if !settled(plan.settle_s)? {
454
+ return Ok(());
455
+ }
456
+ let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))?;
457
+ let address = listener.local_addr()?;
458
+ let accepted = hold_accepted(listener);
459
+ let mut held = Vec::with_capacity(plan.max);
460
+ let mut failed = None;
461
+ while held.len() < plan.max {
462
+ match TcpStream::connect(address) {
463
+ Ok(stream) => held.push(stream),
464
+ Err(error) => {
465
+ failed = Some(errno_of(&error));
466
+ break;
467
+ }
468
+ }
469
+ }
470
+ let level = count()?;
471
+ emit("full", json!({ "flows": held.len(), "count": level, "errno": failed }));
472
+ if failed.is_some() {
473
+ reset_all(held)?;
474
+ emit("done", json!({}));
475
+ return Ok(());
476
+ }
477
+ sleep_ms(SETTLE_MS);
478
+ match plan.way {
479
+ Way::Full => {
480
+ let (line, stream) = stalled_connect(address)?;
481
+ emit("stall", line);
482
+ held.extend(stream);
483
+ reset_all(held)?;
484
+ }
485
+ Way::Reset => {
486
+ let stop = Arc::new(AtomicBool::new(false));
487
+ emit("closing", json!({}));
488
+ reset_all(held)?;
489
+ let watch = poll(level, Arc::clone(&stop));
490
+ let end = monotonic_ns() + DRAIN_S * SECOND_NS;
491
+ while !watch.is_finished() && monotonic_ns() < end {
492
+ sleep_ms(WAIT_MS);
493
+ }
494
+ stop.store(true, Ordering::Relaxed);
495
+ let seen = seen_of(watch)?;
496
+ emit("drained", json!({ "first_fall": seen.first_fall, "zero": seen.zero, "lowest": seen.lowest }));
497
+ }
498
+ Way::Fin => {
499
+ drop(held);
500
+ sleep_ms(SETTLE_MS);
501
+ locked(&accepted).clear();
502
+ sleep_ms(SETTLE_MS);
503
+ let closed = count()?;
504
+ emit("closed", json!({ "count": closed }));
505
+ let stop = Arc::new(AtomicBool::new(false));
506
+ let watch = poll(closed, Arc::clone(&stop));
507
+ let (line, stream) = stalled_connect(address)?;
508
+ stop.store(true, Ordering::Relaxed);
509
+ let seen = seen_of(watch)?;
510
+ let after = count()?;
511
+ emit("stall", extend(line, json!({ "lowest": seen.lowest, "after": after })));
512
+ if let Some(stream) = stream {
513
+ reset(stream)?;
514
+ }
515
+ }
516
+ }
517
+ emit("done", json!({}));
518
+ Ok(())
519
+ }
520
+
521
+ fn main() -> ExitCode {
522
+ let args: Vec<String> = std::env::args().skip(1).collect();
523
+ let result = match args.first().map(String::as_str) {
524
+ Some("target") => target(&args[1..]),
525
+ Some("client") => client(&args[1..]),
526
+ _ => Err("ctfull target PORT [--fill M] | ctfull client [flags] HOST PORT".into()),
527
+ };
528
+ match result {
529
+ Ok(()) => ExitCode::SUCCESS,
530
+ Err(error) => {
531
+ // the harness reads the lines of this program, thus a run with no end names its cause
532
+ emit("error", json!({ "what": error.to_string() }));
533
+ eprintln!("{error}");
534
+ ExitCode::FAILURE
535
+ }
536
+ }
537
+ }
538
+
539
+ #[cfg(test)]
540
+ mod tests {
541
+ use super::*;
542
+
543
+ fn strings(words: &[&str]) -> Vec<String> {
544
+ words.iter().map(|each| each.to_string()).collect()
545
+ }
546
+
547
+ #[test]
548
+ fn a_plan_reads_its_flags_and_skips_the_flags_of_the_harness() {
549
+ let args = strings(&["--max", "1024", "--plan", "fin", "--settle-s", "70", "10.0.0.2", "6392", "--mode", "idle", "--max-s", "90"]);
550
+ let plan = plan_of(&args).expect("a plan");
551
+ assert_eq!(plan.max, 1024);
552
+ assert_eq!(plan.way, Way::Fin);
553
+ assert_eq!(plan.settle_s, 70);
554
+ assert!(plan_of(&strings(&["--no-such"])).is_err());
555
+ assert!(plan_of(&strings(&["--max", "4", "h", "1"])).is_err());
556
+ assert!(plan_of(&strings(&["--max", "4", "--plan", "soon", "h", "1"])).is_err());
557
+ }
558
+
559
+ fn message(kind: u16, payload: &[u8]) -> Vec<u8> {
560
+ let mut bytes = Vec::new();
561
+ bytes.extend_from_slice(&((NLMSG_HEADER + payload.len()) as u32).to_ne_bytes());
562
+ bytes.extend_from_slice(&kind.to_ne_bytes());
563
+ bytes.extend_from_slice(&0u16.to_ne_bytes());
564
+ bytes.extend_from_slice(&1u32.to_ne_bytes());
565
+ bytes.extend_from_slice(&0u32.to_ne_bytes());
566
+ bytes.extend_from_slice(payload);
567
+ while bytes.len() % 4 != 0 {
568
+ bytes.push(0);
569
+ }
570
+ bytes
571
+ }
572
+
573
+ fn attribute(kind: u16, value: u32) -> Vec<u8> {
574
+ [8u16.to_ne_bytes().as_slice(), &kind.to_ne_bytes(), &value.to_be_bytes()].concat()
575
+ }
576
+
577
+ /// One message of the dump: the nfnetlink header with the CPU, then the fields found, drop and early_drop.
578
+ fn stats(cpu: u16, drop: u32) -> Vec<u8> {
579
+ let payload = [vec![0, 0], cpu.to_be_bytes().to_vec(), attribute(2, 99), attribute(STATS_DROP, drop), attribute(11, 5)].concat();
580
+ message((CTNETLINK << 8) | GET_STATS_CPU, &payload)
581
+ }
582
+
583
+ #[test]
584
+ fn the_request_dumps_the_statistics_of_each_cpu() {
585
+ let request = stats_request(7);
586
+ assert_eq!(request.len(), 20);
587
+ assert_eq!(u32_at(&request, 0), Some(20));
588
+ assert_eq!(u16_at(&request, 4), Some(0x0104));
589
+ assert_eq!(u16_at(&request, 6), Some(0x0301));
590
+ assert_eq!(u32_at(&request, 8), Some(7));
591
+ assert_eq!(&request[16..], &[0, 0, 0, 0]);
592
+ }
593
+
594
+ #[test]
595
+ fn the_dump_sums_the_field_drop_of_each_cpu_until_its_end() {
596
+ let done = message(libc::NLMSG_DONE as u16, &0i32.to_ne_bytes());
597
+ assert_eq!(stats_part(&[stats(0, 10), stats(1, 6), done].concat()), Ok(Part { drops: 16, done: true }));
598
+ assert_eq!(stats_part(&stats(3, 4)), Ok(Part { drops: 4, done: false }));
599
+ let refused = message(libc::NLMSG_ERROR as u16, &[(-libc::EPERM).to_ne_bytes().as_slice(), &stats_request(1)[..16]].concat());
600
+ assert_eq!(stats_part(&refused), Err(std::io::Error::from_raw_os_error(libc::EPERM).to_string()));
601
+ let acked = message(libc::NLMSG_ERROR as u16, &[0i32.to_ne_bytes().as_slice(), &stats_request(1)[..16]].concat());
602
+ assert_eq!(stats_part(&acked), Ok(Part { drops: 0, done: false }));
603
+ let mut short = stats(0, 10);
604
+ short.truncate(30);
605
+ assert!(stats_part(&short).is_err());
606
+ let no_drop = message((CTNETLINK << 8) | GET_STATS_CPU, &[vec![0, 0, 0, 0], attribute(2, 99)].concat());
607
+ assert_eq!(stats_part(&no_drop), Err("a message of the statistics with no field drop".to_string()));
608
+ }
609
+
610
+ #[test]
611
+ fn a_counter_of_netstat_comes_from_the_second_tcpext_line() {
612
+ let text = "TcpExt: SyncookiesSent TCPRetransFail\nTcpExt: 3 6\nIpExt: InNoRoutes\nIpExt: 0\n";
613
+ assert_eq!(tcp_ext(text, "TCPRetransFail"), Some(6));
614
+ assert_eq!(tcp_ext(text, "InNoRoutes"), None);
615
+ }
616
+
617
+ #[test]
618
+ fn a_poll_keeps_the_first_fall_the_zero_and_the_lowest() {
619
+ let mut seen = Seen::new(4);
620
+ seen.read(4, 10, 4);
621
+ assert_eq!(seen, Seen { first_fall: None, zero: None, lowest: 4 });
622
+ seen.read(4, 20, 3);
623
+ seen.read(4, 30, 4);
624
+ seen.read(4, 40, 0);
625
+ seen.read(4, 50, 0);
626
+ assert_eq!(seen, Seen { first_fall: Some(20), zero: Some(40), lowest: 0 });
627
+ }
628
+
629
+ #[test]
630
+ fn a_table_with_no_room_stalls_with_its_name() {
631
+ assert_eq!(errno_of(&std::io::Error::from_raw_os_error(libc::ETIMEDOUT)), "ETIMEDOUT");
632
+ }
633
+ }
@@ -293,6 +293,12 @@ fn signal_number(name: &str) -> Result<libc::c_int, Box<dyn Error>> {
293
293
  })
294
294
  }
295
295
 
296
+ /// tells if the kernel drops this signal to this pid: the first process of a namespace gets no STOP
297
+ /// and no KILL from a process in that namespace, and kill(2) still returns 0
298
+ fn dropped(pid: libc::pid_t, sig: libc::c_int) -> bool {
299
+ pid == 1 && (sig == libc::SIGSTOP || sig == libc::SIGKILL)
300
+ }
301
+
296
302
  /// a process name: letters, digits, dots, dashes and underscores, as /proc/PID/comm holds it
297
303
  fn process_name(name: &str) -> Result<&str, Box<dyn Error>> {
298
304
  if !name.is_empty()
@@ -339,6 +345,10 @@ fn signal(args: &[String]) -> Result<(), Box<dyn Error>> {
339
345
  if pids.is_empty() {
340
346
  return Err(format!("no process {name} in this process namespace").into());
341
347
  }
348
+ // a fault that cannot land must not give stamps: run the server as the child of a shell
349
+ if pids.iter().any(|pid| dropped(*pid, sig)) {
350
+ return Err(format!("{name} is the first process of its namespace: the kernel drops {}", args[2]).into());
351
+ }
342
352
  if let Some(at) = opts.at {
343
353
  until(at)?;
344
354
  }
@@ -705,6 +715,12 @@ mod tests {
705
715
  assert!(zombie("812 (valkey-server) Z 1 812 812 0 -1"));
706
716
  assert!(!zombie("813 (valkey-server) S 1 813 813 0 -1"));
707
717
  assert!(!zombie("814 (a) Z) S 1"));
718
+ // the first process of a namespace: STOP and KILL do not reach it, the other signals can
719
+ assert!(dropped(1, libc::SIGSTOP));
720
+ assert!(dropped(1, libc::SIGKILL));
721
+ assert!(!dropped(1, libc::SIGCONT));
722
+ assert!(!dropped(1, libc::SIGTERM));
723
+ assert!(!dropped(7, libc::SIGSTOP));
708
724
  }
709
725
 
710
726
  #[test]