@mark1russell7/harness 0.14.2 → 0.14.4
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.
- package/README.md +42 -5
- package/build/{cell-BccWKiXh.d.ts → cell-DZm8zXN4.d.ts} +354 -10
- package/build/{image-Lg8LL6C3.js → image-EZ8ioZ3L.js} +161 -11
- package/build/image-EZ8ioZ3L.js.map +1 -0
- package/build/index.d.ts +742 -107
- package/build/index.js +2403 -164
- package/build/index.js.map +1 -1
- package/build/schema.d.ts +1 -1
- package/build/schema.js +1 -1
- package/image/Dockerfile +1 -1
- package/image/amqp/client-cluster.mjs +68 -31
- package/image/hikari/ClientHikari.java +81 -6
- package/image/kafka/AdminKafka.java +117 -0
- package/image/kafka/ClientKafka.java +234 -0
- package/image/kafka/Dockerfile +23 -0
- package/image/kafka/jars.sha256 +5 -0
- package/image/mongo/client-mongo.mjs +5 -3
- package/image/native/src/bin/inject.rs +40 -13
- package/image/native/src/bin/ports.rs +374 -0
- package/image/native/src/bin/queue.rs +553 -0
- package/image/node/client-sentinel.mjs +28 -12
- package/image/pg/client-pg.mjs +31 -4
- package/package.json +5 -1
- package/build/image-Lg8LL6C3.js.map +0 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# The harness's client of Apache Kafka: kafka-clients 4.3.2 on Temurin 21.0.12.1. It runs as a
|
|
2
|
+
# container in the client's network namespace, as the harness's own TCP client does. It prints the
|
|
3
|
+
# same JSON lines. AdminKafka is the admin of a lab: it makes each topic, reads the state of a
|
|
4
|
+
# partition, and reads the whole partition after a run.
|
|
5
|
+
# lab.ts stages the build context: this folder, under harness/image/kafka. The build gets each jar
|
|
6
|
+
# from Maven Central and checks its SHA-256 (jars.sha256), thus a build cannot get another jar. The
|
|
7
|
+
# jars are kafka-clients and each runtime dependency of its POM.
|
|
8
|
+
FROM eclipse-temurin:21.0.12.1_1-jdk-alpine@sha256:0bfc69a4758a86710e5c474032d28400a8bd00874766f9e8b1642ac2fd293159 AS build
|
|
9
|
+
WORKDIR /build
|
|
10
|
+
COPY harness/image/kafka/jars.sha256 ./
|
|
11
|
+
RUN wget -q https://repo1.maven.org/maven2/org/apache/kafka/kafka-clients/4.3.2/kafka-clients-4.3.2.jar \
|
|
12
|
+
&& wget -q https://repo1.maven.org/maven2/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar \
|
|
13
|
+
&& wget -q https://repo1.maven.org/maven2/com/github/luben/zstd-jni/1.5.6-10/zstd-jni-1.5.6-10.jar \
|
|
14
|
+
&& wget -q https://repo1.maven.org/maven2/at/yawk/lz4/lz4-java/1.11.4/lz4-java-1.11.4.jar \
|
|
15
|
+
&& wget -q https://repo1.maven.org/maven2/org/xerial/snappy/snappy-java/1.1.10.11/snappy-java-1.1.10.11.jar \
|
|
16
|
+
&& sha256sum -c jars.sha256
|
|
17
|
+
COPY harness/image/kafka/ClientKafka.java harness/image/kafka/AdminKafka.java ./
|
|
18
|
+
RUN javac --release 21 -cp "kafka-clients-4.3.2.jar:slf4j-api-1.7.36.jar" -d classes ClientKafka.java AdminKafka.java
|
|
19
|
+
|
|
20
|
+
FROM eclipse-temurin:21.0.12.1_1-jre-alpine@sha256:51ab5e3302e7141ce665ca3ea85e8b5cd648eafbc3c0c90dd79d6537684e4555
|
|
21
|
+
WORKDIR /opt/harness/kafka
|
|
22
|
+
COPY --from=build /build/kafka-clients-4.3.2.jar /build/slf4j-api-1.7.36.jar /build/zstd-jni-1.5.6-10.jar /build/lz4-java-1.11.4.jar /build/snappy-java-1.1.10.11.jar ./lib/
|
|
23
|
+
COPY --from=build /build/classes ./classes
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
edaf0818414a61a98d1d2e58d6966be8c9f780cef6ded2ffbab10dd841b2bfb8 kafka-clients-4.3.2.jar
|
|
2
|
+
d3ef575e3e4979678dc01bf1dcce51021493b4d11fb7f1be8ad982877c16a1c0 slf4j-api-1.7.36.jar
|
|
3
|
+
b6c3237e24b8252a5a9c3a0e1ae30ef8f323412e74bd358548b7a9e527d676df zstd-jni-1.5.6-10.jar
|
|
4
|
+
58c8e0b813960d2a248e050c353baea73139b48a2ffc55382d42c69707e17325 lz4-java-1.11.4.jar
|
|
5
|
+
58d245ec749fc608ecb175e934a16f2840442c5742aed91002cfd4cb9c6c7aee snappy-java-1.1.10.11.jar
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// client-mongo HOST PORT --mode idle|inflight|stream [--hosts H:P,H:P] [--set NAME] [--op write|read-long]
|
|
2
|
-
// [--interval-ms N] [--max-s N] [--go PATH] [--w majority|1] [--before] [--options JSON]
|
|
2
|
+
// [--interval-ms N] [--max-s N] [--go PATH] [--w majority|1] [--before] [--from N] [--options JSON]
|
|
3
3
|
//
|
|
4
4
|
// The client of a MongoDB cell: the driver mongodb 7.7.0 on Node 24.21.0, against a replica set.
|
|
5
5
|
// Each event is a JSON line with `ev` and `t`, the time on CLOCK_MONOTONIC in nanoseconds
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
// write, waits for the file --go, sends one more operation into the fault, and holds the
|
|
29
29
|
// connections after its end. stream sends a write each --interval-ms after the reply before, until
|
|
30
30
|
// --max-s or SIGTERM. --before starts the client into a fault that holds already: its first
|
|
31
|
-
// selection meets it.
|
|
31
|
+
// selection meets it. --from starts the numbers of a stream at N: a recovery drill starts a second
|
|
32
|
+
// client after a restore, and its numbers follow the numbers of the first client.
|
|
32
33
|
|
|
33
34
|
import { randomUUID } from "node:crypto";
|
|
34
35
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -57,6 +58,7 @@ const max = Number(flag("--max-s") ?? 120) * 1000;
|
|
|
57
58
|
const go = flag("--go") ?? "/tmp/go";
|
|
58
59
|
const w = flag("--w") === "1" ? 1 : "majority";
|
|
59
60
|
const before = argv.includes("--before");
|
|
61
|
+
const start = Number(flag("--from") ?? 1);
|
|
60
62
|
const options = JSON.parse(flag("--options") ?? "{}");
|
|
61
63
|
|
|
62
64
|
// ---- the two clocks: the wall clock of a log line of mongod goes onto the monotonic clock by them
|
|
@@ -242,7 +244,7 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
242
244
|
|
|
243
245
|
if (mode === "stream") {
|
|
244
246
|
emit("ready", { at: first });
|
|
245
|
-
for (let n =
|
|
247
|
+
for (let n = start; !stopped; n += 1) {
|
|
246
248
|
await operate(n);
|
|
247
249
|
await sleep(interval);
|
|
248
250
|
}
|
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
//! - `resp ADDR:PORT ARG...`: one command to a Redis-protocol server, for example
|
|
40
40
|
//! `SENTINEL FAILOVER mymaster` to a Sentinel. The stamps bracket the write of the command and
|
|
41
41
|
//! the first line of its reply, so a planned failover starts at a known instant.
|
|
42
|
+
//! - `silence` / `clear-silence`: each packet of this namespace drops, in the three chains at one
|
|
43
|
+
//! commit, and no peer is named. The links stay up and ARP goes on, so a router in this namespace
|
|
44
|
+
//! goes silent, and its neighbors can detect that only by their own timers.
|
|
42
45
|
//! - `query ADDR:PORT USER DATABASE SQL`: one simple query to a server of the PostgreSQL protocol
|
|
43
46
|
//! that trusts the user, for example `PAUSE` on the console of PgBouncer. The stamps bracket the
|
|
44
47
|
//! write of the query and its ReadyForQuery, after the login, so the end of a PAUSE is known.
|
|
@@ -88,6 +91,30 @@ fn until(at: u64) -> Result<(), Box<dyn Error>> {
|
|
|
88
91
|
}
|
|
89
92
|
}
|
|
90
93
|
|
|
94
|
+
/// The rules of a namespace that goes silent, or of the end of the silence.
|
|
95
|
+
fn silence(action: &str) -> String {
|
|
96
|
+
let op = if action == "silence" { "-I" } else { "-D" };
|
|
97
|
+
["INPUT", "OUTPUT", "FORWARD"].map(|chain| format!("{op} {chain} -j DROP\n")).concat()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Commits rules in one iptables-restore at an instant, between two stamps.
|
|
101
|
+
fn commit(body: &str, at: Option<u64>) -> Result<(u64, u64), Box<dyn Error>> {
|
|
102
|
+
let script = format!("*filter\n{body}COMMIT\n");
|
|
103
|
+
if let Some(at) = at {
|
|
104
|
+
until(at)?;
|
|
105
|
+
}
|
|
106
|
+
let t0 = monotonic_ns();
|
|
107
|
+
let mut child =
|
|
108
|
+
Command::new("iptables-restore").arg("--noflush").stdin(Stdio::piped()).spawn()?;
|
|
109
|
+
child.stdin.take().ok_or("iptables-restore took no stdin")?.write_all(script.as_bytes())?;
|
|
110
|
+
let status = child.wait()?;
|
|
111
|
+
let t1 = monotonic_ns();
|
|
112
|
+
if !status.success() {
|
|
113
|
+
return Err(format!("iptables-restore exited {status}").into());
|
|
114
|
+
}
|
|
115
|
+
Ok((t0, t1))
|
|
116
|
+
}
|
|
117
|
+
|
|
91
118
|
/// The options after the positional arguments: `--at NS`, `--direction D`, `--code C`, `--above N`,
|
|
92
119
|
/// `--via DEV`.
|
|
93
120
|
#[derive(Default)]
|
|
@@ -559,6 +586,12 @@ fn run() -> Result<(), Box<dyn Error>> {
|
|
|
559
586
|
println!("{}", json!({ "action": action, "device": dev, "spec": spec, "t0": t0, "t1": t1 }));
|
|
560
587
|
Ok(())
|
|
561
588
|
}
|
|
589
|
+
"silence" | "clear-silence" => {
|
|
590
|
+
let opts = options(&args[1..])?;
|
|
591
|
+
let (t0, t1) = commit(&silence(action), opts.at)?;
|
|
592
|
+
println!("{}", json!({ "action": action, "t0": t0, "t1": t1 }));
|
|
593
|
+
Ok(())
|
|
594
|
+
}
|
|
562
595
|
_ => {
|
|
563
596
|
// parsed, never pasted: only addresses reach the rules
|
|
564
597
|
let list = peers(args.get(1).ok_or("and the peer's address")?)?;
|
|
@@ -567,19 +600,7 @@ fn run() -> Result<(), Box<dyn Error>> {
|
|
|
567
600
|
for peer in &list {
|
|
568
601
|
body.push_str(&rules(action, *peer, &opts)?);
|
|
569
602
|
}
|
|
570
|
-
let
|
|
571
|
-
if let Some(at) = opts.at {
|
|
572
|
-
until(at)?;
|
|
573
|
-
}
|
|
574
|
-
let t0 = monotonic_ns();
|
|
575
|
-
let mut child =
|
|
576
|
-
Command::new("iptables-restore").arg("--noflush").stdin(Stdio::piped()).spawn()?;
|
|
577
|
-
child.stdin.take().ok_or("iptables-restore took no stdin")?.write_all(script.as_bytes())?;
|
|
578
|
-
let status = child.wait()?;
|
|
579
|
-
let t1 = monotonic_ns();
|
|
580
|
-
if !status.success() {
|
|
581
|
-
return Err(format!("iptables-restore exited {status}").into());
|
|
582
|
-
}
|
|
603
|
+
let (t0, t1) = commit(&body, opts.at)?;
|
|
583
604
|
let named: Vec<String> = list.iter().map(ToString::to_string).collect();
|
|
584
605
|
println!("{}", json!({ "action": action, "peer": named.join(","), "t0": t0, "t1": t1 }));
|
|
585
606
|
Ok(())
|
|
@@ -624,6 +645,12 @@ mod tests {
|
|
|
624
645
|
assert!(rules("reject", peer, &Options::default()).is_err());
|
|
625
646
|
}
|
|
626
647
|
|
|
648
|
+
#[test]
|
|
649
|
+
fn a_silence_drops_each_chain_and_its_end_takes_the_rules_out() {
|
|
650
|
+
assert_eq!(silence("silence"), "-I INPUT -j DROP\n-I OUTPUT -j DROP\n-I FORWARD -j DROP\n");
|
|
651
|
+
assert_eq!(silence("clear-silence"), "-D INPUT -j DROP\n-D OUTPUT -j DROP\n-D FORWARD -j DROP\n");
|
|
652
|
+
}
|
|
653
|
+
|
|
627
654
|
#[test]
|
|
628
655
|
fn a_router_gets_forward_rules_that_face_one_device() {
|
|
629
656
|
let peer: Ipv4Addr = "10.0.0.2".parse().unwrap();
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
//! ports target PORT
|
|
2
|
+
//! ports client [--rounds N] [--close reset|client|target] [--destinations N] [--settle-ms N]
|
|
3
|
+
//! [--retry-ms N] [--wait-s N] HOST PORT [--mode M] [--max-s N]
|
|
4
|
+
//!
|
|
5
|
+
//! This program is the two ends of the port range cells: a target that accepts each connection,
|
|
6
|
+
//! and a client that connects until its range has no port. Each line is JSON with an `ev` and a `t`
|
|
7
|
+
//! on CLOCK_MONOTONIC.
|
|
8
|
+
//!
|
|
9
|
+
//! The target listens on PORT and on PORT + 2, the two destinations of a cell. It serves one command
|
|
10
|
+
//! on PORT + 1, its control port: `close COUNT` waits until it holds COUNT connections, and closes them.
|
|
11
|
+
//!
|
|
12
|
+
//! The client does these steps in each round:
|
|
13
|
+
//!
|
|
14
|
+
//! 1. It connects to each destination until a connect fails, and it holds each connection. A `round`
|
|
15
|
+
//! line gives the ports of each destination, the errno of the failed connect and its time.
|
|
16
|
+
//! 2. `--close reset` closes each connection with a reset. The target then closes its sockets.
|
|
17
|
+
//! 3. `--close client` closes each connection with a FIN, and it waits `--settle-ms`. The target
|
|
18
|
+
//! then closes its sockets, between the lines `closing` and `closed`. The client connects again
|
|
19
|
+
//! each `--retry-ms`, until it holds each port or `--wait-s` passes. A `freed` line gives each port.
|
|
20
|
+
//! It then closes those connections with a reset, so that the next run finds each port free.
|
|
21
|
+
//! 4. `--close target` makes the target close first. The client reads the FIN of each connection,
|
|
22
|
+
//! closes it, and waits `--settle-ms`.
|
|
23
|
+
//!
|
|
24
|
+
//! The control connection takes a port of the range. Its destination is not a destination of the
|
|
25
|
+
//! search, thus it does not change a count. It has SO_LINGER 0 from its connect, so that its close is a
|
|
26
|
+
//! reset and holds no port. A FIN at the exit of the client holds that port in TIME_WAIT for 60 s. A
|
|
27
|
+
//! range of 3 has only 2 ports for a search. Thus the third run of an environment had no control port.
|
|
28
|
+
|
|
29
|
+
use prober::{errno_name, monotonic_ns};
|
|
30
|
+
use serde_json::{json, Value};
|
|
31
|
+
use std::error::Error;
|
|
32
|
+
use std::io::{BufRead, BufReader, Read, Write};
|
|
33
|
+
use std::net::{Ipv4Addr, SocketAddrV4, TcpListener, TcpStream};
|
|
34
|
+
use std::os::fd::AsRawFd;
|
|
35
|
+
use std::sync::{Arc, Mutex, MutexGuard};
|
|
36
|
+
use std::time::Duration;
|
|
37
|
+
|
|
38
|
+
/// the most connections that one search holds: a guard for a range that the harness did not set
|
|
39
|
+
const LIMIT: usize = 256;
|
|
40
|
+
/// how long `close COUNT` waits for the accept of the last connection, in milliseconds
|
|
41
|
+
const ACCEPT_WAIT_MS: u64 = 2_000;
|
|
42
|
+
|
|
43
|
+
fn emit(event: &str, fields: Value) {
|
|
44
|
+
let mut line = json!({ "ev": event, "t": monotonic_ns() });
|
|
45
|
+
if let (Some(line), Value::Object(fields)) = (line.as_object_mut(), fields) {
|
|
46
|
+
line.extend(fields);
|
|
47
|
+
}
|
|
48
|
+
println!("{line}");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn locked(held: &Mutex<Vec<TcpStream>>) -> MutexGuard<'_, Vec<TcpStream>> {
|
|
52
|
+
held.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Accepts each connection of a port on its own thread.
|
|
56
|
+
fn listen(port: u16, held: &Arc<Mutex<Vec<TcpStream>>>) -> std::io::Result<()> {
|
|
57
|
+
let listener = TcpListener::bind((Ipv4Addr::UNSPECIFIED, port))?;
|
|
58
|
+
let held = Arc::clone(held);
|
|
59
|
+
std::thread::spawn(move || {
|
|
60
|
+
for stream in listener.incoming().flatten() {
|
|
61
|
+
locked(&held).push(stream);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
Ok(())
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Waits until the target holds `count` connections, and closes each one. It gives the count it closed.
|
|
68
|
+
fn close_held(held: &Mutex<Vec<TcpStream>>, count: usize) -> usize {
|
|
69
|
+
for _ in 0..ACCEPT_WAIT_MS {
|
|
70
|
+
if locked(held).len() >= count {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
std::thread::sleep(Duration::from_millis(1));
|
|
74
|
+
}
|
|
75
|
+
let mut held = locked(held);
|
|
76
|
+
let closed = held.len();
|
|
77
|
+
held.clear();
|
|
78
|
+
closed
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fn serve(stream: TcpStream, held: &Mutex<Vec<TcpStream>>) -> Result<(), Box<dyn Error>> {
|
|
82
|
+
stream.set_nodelay(true)?;
|
|
83
|
+
let mut writer = stream.try_clone()?;
|
|
84
|
+
for line in BufReader::new(stream).lines() {
|
|
85
|
+
let line = line?;
|
|
86
|
+
let mut words = line.split_whitespace();
|
|
87
|
+
let reply = match (words.next(), words.next().and_then(|word| word.parse::<usize>().ok())) {
|
|
88
|
+
(Some("close"), Some(count)) => json!({ "closed": close_held(held, count) }),
|
|
89
|
+
_ => json!({ "refused": line }),
|
|
90
|
+
};
|
|
91
|
+
writeln!(writer, "{reply}")?;
|
|
92
|
+
}
|
|
93
|
+
Ok(())
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fn target(args: &[String]) -> Result<(), Box<dyn Error>> {
|
|
97
|
+
let port: u16 = args.first().ok_or("ports target PORT")?.parse()?;
|
|
98
|
+
let held = Arc::new(Mutex::new(Vec::new()));
|
|
99
|
+
listen(port, &held)?;
|
|
100
|
+
listen(port + 2, &held)?;
|
|
101
|
+
let control = TcpListener::bind((Ipv4Addr::UNSPECIFIED, port + 1))?;
|
|
102
|
+
emit("listening", json!({ "control": port + 1 }));
|
|
103
|
+
for stream in control.incoming() {
|
|
104
|
+
if let Err(error) = serve(stream?, &held) {
|
|
105
|
+
emit("control-error", json!({ "error": error.to_string() }));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
Ok(())
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
112
|
+
enum Close {
|
|
113
|
+
Reset,
|
|
114
|
+
Client,
|
|
115
|
+
Target,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#[derive(Debug)]
|
|
119
|
+
struct Plan {
|
|
120
|
+
host: String,
|
|
121
|
+
port: u16,
|
|
122
|
+
rounds: u32,
|
|
123
|
+
close: Close,
|
|
124
|
+
destinations: u16,
|
|
125
|
+
settle_ms: u64,
|
|
126
|
+
retry_ms: u64,
|
|
127
|
+
wait_s: u64,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fn plan_of(args: &[String]) -> Result<Plan, Box<dyn Error>> {
|
|
131
|
+
let mut plan = Plan {
|
|
132
|
+
host: String::new(),
|
|
133
|
+
port: 0,
|
|
134
|
+
rounds: 1,
|
|
135
|
+
close: Close::Reset,
|
|
136
|
+
destinations: 1,
|
|
137
|
+
settle_ms: 50,
|
|
138
|
+
retry_ms: 10,
|
|
139
|
+
wait_s: 80,
|
|
140
|
+
};
|
|
141
|
+
let mut places = Vec::new();
|
|
142
|
+
let mut rest = args.iter();
|
|
143
|
+
while let Some(arg) = rest.next() {
|
|
144
|
+
let mut value = || rest.next().ok_or_else(|| format!("{arg} needs a value"));
|
|
145
|
+
match arg.as_str() {
|
|
146
|
+
"--rounds" => plan.rounds = value()?.parse()?,
|
|
147
|
+
"--close" => {
|
|
148
|
+
plan.close = match value()?.as_str() {
|
|
149
|
+
"reset" => Close::Reset,
|
|
150
|
+
"client" => Close::Client,
|
|
151
|
+
"target" => Close::Target,
|
|
152
|
+
other => return Err(format!("no way to close named {other}").into()),
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
"--destinations" => plan.destinations = value()?.parse()?,
|
|
156
|
+
"--settle-ms" => plan.settle_ms = value()?.parse()?,
|
|
157
|
+
"--retry-ms" => plan.retry_ms = value()?.parse()?,
|
|
158
|
+
"--wait-s" => plan.wait_s = value()?.parse()?,
|
|
159
|
+
// the harness gives these flags to each client, and the plan does not read them
|
|
160
|
+
"--mode" | "--max-s" => {
|
|
161
|
+
value()?;
|
|
162
|
+
}
|
|
163
|
+
other if other.starts_with("--") => return Err(format!("no flag {other}").into()),
|
|
164
|
+
other => places.push(other.to_string()),
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if let [host, port] = places.as_slice() {
|
|
168
|
+
plan.host = host.clone();
|
|
169
|
+
plan.port = port.parse()?;
|
|
170
|
+
return Ok(plan);
|
|
171
|
+
}
|
|
172
|
+
Err("ports client [flags] HOST PORT".into())
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// the control connection of the client
|
|
176
|
+
struct Control {
|
|
177
|
+
reader: BufReader<TcpStream>,
|
|
178
|
+
writer: TcpStream,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
impl Control {
|
|
182
|
+
fn ask(&mut self, command: &str) -> Result<Value, Box<dyn Error>> {
|
|
183
|
+
writeln!(self.writer, "{command}")?;
|
|
184
|
+
let mut line = String::new();
|
|
185
|
+
self.reader.read_line(&mut line)?;
|
|
186
|
+
Ok(serde_json::from_str(&line)?)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Closes a connection with a reset: SO_LINGER with a time of 0, then close(2).
|
|
191
|
+
fn reset(stream: TcpStream) -> std::io::Result<()> {
|
|
192
|
+
abortive(&stream)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// Sets SO_LINGER with a time of 0, so that each later close of the socket is a reset.
|
|
196
|
+
fn abortive(stream: &TcpStream) -> std::io::Result<()> {
|
|
197
|
+
let linger = libc::linger { l_onoff: 1, l_linger: 0 };
|
|
198
|
+
// SAFETY: the option value is on this stack, passed with its exact size
|
|
199
|
+
let status = unsafe {
|
|
200
|
+
libc::setsockopt(
|
|
201
|
+
stream.as_raw_fd(),
|
|
202
|
+
libc::SOL_SOCKET,
|
|
203
|
+
libc::SO_LINGER,
|
|
204
|
+
(&linger as *const libc::linger).cast(),
|
|
205
|
+
std::mem::size_of::<libc::linger>() as libc::socklen_t,
|
|
206
|
+
)
|
|
207
|
+
};
|
|
208
|
+
if status == 0 { Ok(()) } else { Err(std::io::Error::last_os_error()) }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
fn errno_of(error: &std::io::Error) -> String {
|
|
212
|
+
error.raw_os_error().map_or_else(|| error.to_string(), errno_name)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/// Connects to one destination until a connect fails, and writes the `round` line.
|
|
216
|
+
fn search(index: u32, destination: SocketAddrV4, held: &mut Vec<TcpStream>) -> Result<(), Box<dyn Error>> {
|
|
217
|
+
let mut ports = Vec::new();
|
|
218
|
+
let (errno, refusal) = loop {
|
|
219
|
+
if ports.len() == LIMIT {
|
|
220
|
+
break ("none".to_string(), 0);
|
|
221
|
+
}
|
|
222
|
+
let before = monotonic_ns();
|
|
223
|
+
match TcpStream::connect(destination) {
|
|
224
|
+
Ok(stream) => {
|
|
225
|
+
ports.push(stream.local_addr()?.port());
|
|
226
|
+
held.push(stream);
|
|
227
|
+
}
|
|
228
|
+
Err(error) => break (errno_of(&error), monotonic_ns() - before),
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
emit(
|
|
232
|
+
"round",
|
|
233
|
+
json!({ "index": index, "destination": destination.port(), "ports": ports, "errno": errno, "refusal": refusal }),
|
|
234
|
+
);
|
|
235
|
+
Ok(())
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// Connects again each `retry_ms` until the client holds `count` ports, or the wait ends.
|
|
239
|
+
fn reclaim(plan: &Plan, destination: SocketAddrV4, count: usize) -> Result<Vec<TcpStream>, Box<dyn Error>> {
|
|
240
|
+
let mut again = Vec::new();
|
|
241
|
+
let mut tries = 0u32;
|
|
242
|
+
let end = monotonic_ns() + plan.wait_s * 1_000_000_000;
|
|
243
|
+
while again.len() < count && monotonic_ns() < end {
|
|
244
|
+
match TcpStream::connect(destination) {
|
|
245
|
+
Ok(stream) => {
|
|
246
|
+
emit("freed", json!({ "port": stream.local_addr()?.port(), "tries": tries }));
|
|
247
|
+
again.push(stream);
|
|
248
|
+
}
|
|
249
|
+
Err(error) if error.raw_os_error() == Some(libc::EADDRNOTAVAIL) => {
|
|
250
|
+
tries += 1;
|
|
251
|
+
std::thread::sleep(Duration::from_millis(plan.retry_ms));
|
|
252
|
+
}
|
|
253
|
+
Err(error) => {
|
|
254
|
+
emit("failed", json!({ "errno": errno_of(&error) }));
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
Ok(again)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
fn close_round(plan: &Plan, first: SocketAddrV4, control: &mut Control, held: Vec<TcpStream>) -> Result<(), Box<dyn Error>> {
|
|
263
|
+
let count = held.len();
|
|
264
|
+
let settle = Duration::from_millis(plan.settle_ms);
|
|
265
|
+
match plan.close {
|
|
266
|
+
Close::Reset => {
|
|
267
|
+
for stream in held {
|
|
268
|
+
reset(stream)?;
|
|
269
|
+
}
|
|
270
|
+
control.ask(&format!("close {count}"))?;
|
|
271
|
+
}
|
|
272
|
+
Close::Client => {
|
|
273
|
+
drop(held);
|
|
274
|
+
std::thread::sleep(settle);
|
|
275
|
+
emit("closing", json!({}));
|
|
276
|
+
let reply = control.ask(&format!("close {count}"))?;
|
|
277
|
+
emit("closed", reply);
|
|
278
|
+
let again = reclaim(plan, first, count)?;
|
|
279
|
+
let reclaimed = again.len();
|
|
280
|
+
for stream in again {
|
|
281
|
+
reset(stream)?;
|
|
282
|
+
}
|
|
283
|
+
control.ask(&format!("close {reclaimed}"))?;
|
|
284
|
+
}
|
|
285
|
+
Close::Target => {
|
|
286
|
+
control.ask(&format!("close {count}"))?;
|
|
287
|
+
for mut stream in held {
|
|
288
|
+
let mut sink = [0u8; 16];
|
|
289
|
+
// the read ends at the FIN of the target, and the close of the stream sends the FIN of the client
|
|
290
|
+
stream.read(&mut sink)?;
|
|
291
|
+
}
|
|
292
|
+
std::thread::sleep(settle);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
Ok(())
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
fn client(args: &[String]) -> Result<(), Box<dyn Error>> {
|
|
299
|
+
let plan = plan_of(args)?;
|
|
300
|
+
let ip: Ipv4Addr = plan.host.parse()?;
|
|
301
|
+
let stream = TcpStream::connect((ip, plan.port + 1))?;
|
|
302
|
+
// a reset at each close, the exit included: the port of this connection is free for the next run
|
|
303
|
+
abortive(&stream)?;
|
|
304
|
+
stream.set_nodelay(true)?;
|
|
305
|
+
let mut control = Control { reader: BufReader::new(stream.try_clone()?), writer: stream };
|
|
306
|
+
let destinations: Vec<SocketAddrV4> = (0..plan.destinations).map(|at| SocketAddrV4::new(ip, plan.port + 2 * at)).collect();
|
|
307
|
+
emit("ready", json!({}));
|
|
308
|
+
for index in 0..plan.rounds {
|
|
309
|
+
let mut held = Vec::new();
|
|
310
|
+
for destination in &destinations {
|
|
311
|
+
search(index, *destination, &mut held)?;
|
|
312
|
+
}
|
|
313
|
+
close_round(&plan, destinations[0], &mut control, held)?;
|
|
314
|
+
}
|
|
315
|
+
emit("done", json!({}));
|
|
316
|
+
Ok(())
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
fn main() -> Result<(), Box<dyn Error>> {
|
|
320
|
+
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
321
|
+
match args.first().map(String::as_str) {
|
|
322
|
+
Some("target") => target(&args[1..]),
|
|
323
|
+
Some("client") => client(&args[1..]),
|
|
324
|
+
_ => Err("ports target PORT | ports client [flags] HOST PORT".into()),
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#[cfg(test)]
|
|
329
|
+
mod tests {
|
|
330
|
+
use super::*;
|
|
331
|
+
|
|
332
|
+
#[test]
|
|
333
|
+
fn a_plan_reads_its_flags_and_skips_the_flags_of_the_harness() {
|
|
334
|
+
let args: Vec<String> = ["--rounds", "50", "--close", "client", "--destinations", "2", "10.0.0.2", "6392", "--mode", "idle", "--max-s", "90"]
|
|
335
|
+
.iter()
|
|
336
|
+
.map(|each| each.to_string())
|
|
337
|
+
.collect();
|
|
338
|
+
let plan = plan_of(&args).expect("a plan");
|
|
339
|
+
assert_eq!(plan.host, "10.0.0.2");
|
|
340
|
+
assert_eq!(plan.port, 6392);
|
|
341
|
+
assert_eq!(plan.rounds, 50);
|
|
342
|
+
assert_eq!(plan.close, Close::Client);
|
|
343
|
+
assert_eq!(plan.destinations, 2);
|
|
344
|
+
assert_eq!(plan.retry_ms, 10);
|
|
345
|
+
assert!(plan_of(&["--no-such".to_string()]).is_err());
|
|
346
|
+
assert!(plan_of(&["--close".to_string(), "soon".to_string(), "h".to_string(), "1".to_string()]).is_err());
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
#[test]
|
|
350
|
+
fn a_range_with_no_free_port_is_refused_with_its_name() {
|
|
351
|
+
assert_eq!(errno_of(&std::io::Error::from_raw_os_error(libc::EADDRNOTAVAIL)), "EADDRNOTAVAIL");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
#[test]
|
|
355
|
+
fn the_control_connection_closes_with_a_reset_and_holds_no_port() {
|
|
356
|
+
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("a listener");
|
|
357
|
+
let stream = TcpStream::connect(listener.local_addr().expect("an address")).expect("a connect");
|
|
358
|
+
abortive(&stream).expect("SO_LINGER");
|
|
359
|
+
let mut linger = libc::linger { l_onoff: 0, l_linger: -1 };
|
|
360
|
+
let mut size = std::mem::size_of::<libc::linger>() as libc::socklen_t;
|
|
361
|
+
// SAFETY: the buffer is on this stack, passed with its exact size
|
|
362
|
+
let status = unsafe {
|
|
363
|
+
libc::getsockopt(
|
|
364
|
+
stream.as_raw_fd(),
|
|
365
|
+
libc::SOL_SOCKET,
|
|
366
|
+
libc::SO_LINGER,
|
|
367
|
+
(&mut linger as *mut libc::linger).cast(),
|
|
368
|
+
&mut size,
|
|
369
|
+
)
|
|
370
|
+
};
|
|
371
|
+
assert_eq!(status, 0);
|
|
372
|
+
assert_eq!((linger.l_onoff, linger.l_linger), (1, 0));
|
|
373
|
+
}
|
|
374
|
+
}
|