@mark1russell7/harness 0.3.0 → 0.4.1
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 +27 -10
- package/build/index.d.ts +139 -10
- package/build/index.js +253 -27
- package/build/index.js.map +1 -1
- package/image/native/src/bin/client.rs +63 -3
- package/image/native/src/bin/inject.rs +48 -6
- package/package.json +3 -3
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
//! timeout makes the server answer only when the timeout runs out, so its reply can be the one in
|
|
16
16
|
//! flight when a fault lands.
|
|
17
17
|
//! stream: then wait for --go, and send the request --count times, each when the reply to the one
|
|
18
|
-
//! before it
|
|
19
|
-
//! whose outcome is one (loss, reordering).
|
|
18
|
+
//! before it is whole and --interval-ms has passed: a distribution of reply times, for a fault
|
|
19
|
+
//! whose outcome is one (loss, reordering). A reply is whole at its CRLF, or for a bulk string
|
|
20
|
+
//! after its length: so `GET big` keeps a long reply's many segments in flight, and the replied
|
|
21
|
+
//! line stamps its last byte. It ends with a done line.
|
|
20
22
|
|
|
21
23
|
use prober::{errno_name, monotonic_ns, quantity};
|
|
22
24
|
use serde_json::{json, Value};
|
|
@@ -87,6 +89,40 @@ fn readable(fd: i32, ms: i32) -> std::io::Result<bool> {
|
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
/// The whole length of a Redis reply from its first bytes, or None while its first line is not
|
|
93
|
+
/// all there: a simple string, an error or an integer ends at its CRLF, and a bulk string after
|
|
94
|
+
/// its length and one more CRLF (a null one, of length -1, at its first CRLF).
|
|
95
|
+
fn reply_length(head: &[u8]) -> Option<usize> {
|
|
96
|
+
let line = head.windows(2).position(|pair| pair == b"\r\n")? + 2;
|
|
97
|
+
if head.first() != Some(&b'$') {
|
|
98
|
+
return Some(line);
|
|
99
|
+
}
|
|
100
|
+
let length: i64 = std::str::from_utf8(&head[1..line - 2]).ok()?.parse().ok()?;
|
|
101
|
+
Some(if length < 0 { line } else { line + length as usize + 2 })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// The reply a stream waits for: its first bytes until its length is known, and how much came.
|
|
105
|
+
#[derive(Default)]
|
|
106
|
+
struct Reply {
|
|
107
|
+
head: Vec<u8>,
|
|
108
|
+
length: Option<usize>,
|
|
109
|
+
got: usize,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
impl Reply {
|
|
113
|
+
/// Takes the bytes of one read, and says whether the reply is whole.
|
|
114
|
+
fn take(&mut self, bytes: &[u8]) -> bool {
|
|
115
|
+
self.got += bytes.len();
|
|
116
|
+
if self.length.is_none() {
|
|
117
|
+
// a bulk string's first line is its length: 64 bytes hold any
|
|
118
|
+
let room = 64usize.saturating_sub(self.head.len());
|
|
119
|
+
self.head.extend_from_slice(&bytes[..bytes.len().min(room)]);
|
|
120
|
+
self.length = reply_length(&self.head);
|
|
121
|
+
}
|
|
122
|
+
matches!(self.length, Some(length) if self.got >= length)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
90
126
|
/// what in TCP_INFO is worth a line when it changes: the timers' own state, never the ages
|
|
91
127
|
fn watched(info: &libc::tcp_info) -> [u32; 7] {
|
|
92
128
|
[
|
|
@@ -186,6 +222,7 @@ fn run() -> Result<(), Box<dyn Error>> {
|
|
|
186
222
|
// stream mode: requests sent, whether one waits for its reply, and when the next may go
|
|
187
223
|
let mut sent: u32 = 0;
|
|
188
224
|
let mut awaiting = false;
|
|
225
|
+
let mut reply = Reply::default();
|
|
189
226
|
let mut next_at: u64 = 0;
|
|
190
227
|
loop {
|
|
191
228
|
if readable(fd, tick)? {
|
|
@@ -196,8 +233,10 @@ fn run() -> Result<(), Box<dyn Error>> {
|
|
|
196
233
|
}
|
|
197
234
|
Ok(n) => {
|
|
198
235
|
emit("data", json!({ "n": n, "text": String::from_utf8_lossy(&buf[..n.min(40)]) }));
|
|
199
|
-
if awaiting {
|
|
236
|
+
if awaiting && reply.take(&buf[..n]) {
|
|
237
|
+
emit("replied", json!({ "n": quantity(reply.got as u32, "B") }));
|
|
200
238
|
awaiting = false;
|
|
239
|
+
reply = Reply::default();
|
|
201
240
|
next_at = monotonic_ns() + args.interval.as_nanos() as u64;
|
|
202
241
|
}
|
|
203
242
|
}
|
|
@@ -261,3 +300,24 @@ fn main() {
|
|
|
261
300
|
std::process::exit(1);
|
|
262
301
|
}
|
|
263
302
|
}
|
|
303
|
+
|
|
304
|
+
#[cfg(test)]
|
|
305
|
+
mod tests {
|
|
306
|
+
use super::*;
|
|
307
|
+
|
|
308
|
+
#[test]
|
|
309
|
+
fn a_reply_is_whole_at_its_crlf_or_after_its_bulk_length() {
|
|
310
|
+
assert_eq!(reply_length(b"+PONG\r\n"), Some(7));
|
|
311
|
+
assert_eq!(reply_length(b"+PON"), None);
|
|
312
|
+
assert_eq!(reply_length(b"$5\r\nhello\r\n"), Some(11));
|
|
313
|
+
assert_eq!(reply_length(b"$-1\r\n"), Some(5));
|
|
314
|
+
assert_eq!(reply_length(b"$1000000\r\nxx"), Some(1_000_012));
|
|
315
|
+
// a long reply in reads of any size: whole only at its last byte
|
|
316
|
+
let mut reply = Reply::default();
|
|
317
|
+
assert!(!reply.take(b"$10\r"));
|
|
318
|
+
assert!(!reply.take(b"\n01234"));
|
|
319
|
+
assert!(!reply.take(b"56789\r"));
|
|
320
|
+
assert!(reply.take(b"\n"));
|
|
321
|
+
assert_eq!(reply.got, 17);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -30,6 +30,12 @@
|
|
|
30
30
|
//! `--at NS` holds the commit until that instant on CLOCK_MONOTONIC, the clock every stamp is on.
|
|
31
31
|
//! A stall's end is timed that way, from inside the VM: the exec only has to arrive before it, and
|
|
32
32
|
//! its 190 ms never reach the stall's length.
|
|
33
|
+
//!
|
|
34
|
+
//! `--via DEV` puts a filter rule in the FORWARD chain, for a router: its INPUT and OUTPUT see only
|
|
35
|
+
//! what the router itself sends and receives. The rule faces one side by the device toward it:
|
|
36
|
+
//! what comes from the peer arrives on DEV (`-i DEV -s PEER`), and what goes to it leaves on DEV
|
|
37
|
+
//! (`-o DEV -d PEER`). A REJECT then answers from the router: a reset spoofs the address the
|
|
38
|
+
//! segment was going to, and an ICMP error comes from the router's own address on DEV.
|
|
33
39
|
|
|
34
40
|
use prober::monotonic_ns;
|
|
35
41
|
use serde_json::json;
|
|
@@ -63,13 +69,15 @@ fn until(at: u64) -> Result<(), Box<dyn Error>> {
|
|
|
63
69
|
}
|
|
64
70
|
}
|
|
65
71
|
|
|
66
|
-
/// The options after the positional arguments: `--at NS`, `--direction D`, `--code C`, `--above N
|
|
72
|
+
/// The options after the positional arguments: `--at NS`, `--direction D`, `--code C`, `--above N`,
|
|
73
|
+
/// `--via DEV`.
|
|
67
74
|
#[derive(Default)]
|
|
68
75
|
struct Options {
|
|
69
76
|
at: Option<u64>,
|
|
70
77
|
direction: Option<String>,
|
|
71
78
|
code: Option<String>,
|
|
72
79
|
above: Option<u32>,
|
|
80
|
+
via: Option<String>,
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
fn options(args: &[String]) -> Result<Options, Box<dyn Error>> {
|
|
@@ -82,6 +90,7 @@ fn options(args: &[String]) -> Result<Options, Box<dyn Error>> {
|
|
|
82
90
|
"--direction" => out.direction = Some(value.clone()),
|
|
83
91
|
"--code" => out.code = Some(value.clone()),
|
|
84
92
|
"--above" => out.above = Some(value.parse()?),
|
|
93
|
+
"--via" => out.via = Some(device(value)?.to_string()),
|
|
85
94
|
other => return Err(format!("unknown option {other}").into()),
|
|
86
95
|
}
|
|
87
96
|
}
|
|
@@ -91,12 +100,18 @@ fn options(args: &[String]) -> Result<Options, Box<dyn Error>> {
|
|
|
91
100
|
/// The iptables rules of an action that is a filter rule, or None for an action that is not.
|
|
92
101
|
fn rules(action: &str, peer: Ipv4Addr, opts: &Options) -> Result<String, Box<dyn Error>> {
|
|
93
102
|
let op = |set: &str| if action == set { "-I" } else { "-D" };
|
|
103
|
+
// what comes from the peer, and what goes to it: at an end, the INPUT and OUTPUT of its own
|
|
104
|
+
// sockets; at a router, what it forwards in and out of the device toward the peer
|
|
105
|
+
let (from, to) = match opts.via.as_deref() {
|
|
106
|
+
None => (format!("INPUT -s {peer}/32"), format!("OUTPUT -d {peer}/32")),
|
|
107
|
+
Some(dev) => (format!("FORWARD -i {dev} -s {peer}/32"), format!("FORWARD -o {dev} -d {peer}/32")),
|
|
108
|
+
};
|
|
94
109
|
Ok(match action {
|
|
95
110
|
"blackhole" | "clear" => {
|
|
96
111
|
let op = op("blackhole");
|
|
97
112
|
let direction = opts.direction.as_deref().unwrap_or("both");
|
|
98
|
-
let input = format!("{op}
|
|
99
|
-
let output = format!("{op}
|
|
113
|
+
let input = format!("{op} {from} -j DROP\n");
|
|
114
|
+
let output = format!("{op} {to} -j DROP\n");
|
|
100
115
|
match direction {
|
|
101
116
|
"both" => format!("{input}{output}"),
|
|
102
117
|
"in" => input,
|
|
@@ -106,7 +121,7 @@ fn rules(action: &str, peer: Ipv4Addr, opts: &Options) -> Result<String, Box<dyn
|
|
|
106
121
|
}
|
|
107
122
|
"rst" | "clear-rst" => {
|
|
108
123
|
let op = op("rst");
|
|
109
|
-
format!("{op}
|
|
124
|
+
format!("{op} {from} -p tcp -j REJECT --reject-with tcp-reset\n")
|
|
110
125
|
}
|
|
111
126
|
"reject" | "clear-reject" => {
|
|
112
127
|
let op = op("reject");
|
|
@@ -117,14 +132,14 @@ fn rules(action: &str, peer: Ipv4Addr, opts: &Options) -> Result<String, Box<dyn
|
|
|
117
132
|
"admin" => "icmp-admin-prohibited",
|
|
118
133
|
other => return Err(format!("unknown code {other}").into()),
|
|
119
134
|
};
|
|
120
|
-
format!("{op}
|
|
135
|
+
format!("{op} {from} -p tcp -j REJECT --reject-with {with}\n")
|
|
121
136
|
}
|
|
122
137
|
// the long segments drop where they arrive: in the sender's own namespace, an OUTPUT drop
|
|
123
138
|
// would fail its sends with EPERM rather than lose them on the way
|
|
124
139
|
"mtu" | "clear-mtu" => {
|
|
125
140
|
let op = op("mtu");
|
|
126
141
|
let above = opts.above.ok_or("--above N")?;
|
|
127
|
-
format!("{op}
|
|
142
|
+
format!("{op} {from} -p tcp -m length --length {}:65535 -j DROP\n", above + 1)
|
|
128
143
|
}
|
|
129
144
|
other => return Err(format!("unknown action {other}").into()),
|
|
130
145
|
})
|
|
@@ -323,6 +338,33 @@ mod tests {
|
|
|
323
338
|
assert!(rules("reject", peer, &Options::default()).is_err());
|
|
324
339
|
}
|
|
325
340
|
|
|
341
|
+
#[test]
|
|
342
|
+
fn a_router_gets_forward_rules_that_face_one_device() {
|
|
343
|
+
let peer: Ipv4Addr = "10.0.0.2".parse().unwrap();
|
|
344
|
+
let via = |direction: Option<&str>| Options {
|
|
345
|
+
direction: direction.map(Into::into),
|
|
346
|
+
via: Some("eth0".into()),
|
|
347
|
+
..Options::default()
|
|
348
|
+
};
|
|
349
|
+
assert_eq!(
|
|
350
|
+
rules("blackhole", peer, &via(None)).unwrap(),
|
|
351
|
+
"-I FORWARD -i eth0 -s 10.0.0.2/32 -j DROP\n-I FORWARD -o eth0 -d 10.0.0.2/32 -j DROP\n"
|
|
352
|
+
);
|
|
353
|
+
assert_eq!(rules("clear", peer, &via(Some("out"))).unwrap(), "-D FORWARD -o eth0 -d 10.0.0.2/32 -j DROP\n");
|
|
354
|
+
assert_eq!(
|
|
355
|
+
rules("rst", peer, &via(None)).unwrap(),
|
|
356
|
+
"-I FORWARD -i eth0 -s 10.0.0.2/32 -p tcp -j REJECT --reject-with tcp-reset\n"
|
|
357
|
+
);
|
|
358
|
+
let host = Options { code: Some("host".into()), ..via(None) };
|
|
359
|
+
assert_eq!(
|
|
360
|
+
rules("clear-reject", peer, &host).unwrap(),
|
|
361
|
+
"-D FORWARD -i eth0 -s 10.0.0.2/32 -p tcp -j REJECT --reject-with icmp-host-unreachable\n"
|
|
362
|
+
);
|
|
363
|
+
// a device name is letters, digits, dots and dashes: nothing else reaches the rules
|
|
364
|
+
assert!(options(&["--via".into(), "eth0;x".into()]).is_err());
|
|
365
|
+
assert_eq!(options(&["--via".into(), "eth1".into()]).unwrap().via.as_deref(), Some("eth1"));
|
|
366
|
+
}
|
|
367
|
+
|
|
326
368
|
#[test]
|
|
327
369
|
fn http_requests_take_only_a_method_a_path_and_a_length() {
|
|
328
370
|
assert!(http_path("/proxies/redis/toxics").is_ok());
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/harness",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"description": "A fault-injection harness for connection timers: a cell runs a client against a stock target in Docker, through a topology of routers and proxies, injects a fault, measures the kernel's own records and judges a prediction.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"fault-injection",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"node": ">=22.19"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@mark1russell7/prober": "^0.1
|
|
60
|
-
"@mark1russell7/quantity": "^0.
|
|
59
|
+
"@mark1russell7/prober": "^0.3.1",
|
|
60
|
+
"@mark1russell7/quantity": "^0.2.0",
|
|
61
61
|
"testcontainers": "12.2.0"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|