@mark1russell7/harness 0.1.0 → 0.3.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.
- package/README.md +63 -11
- package/build/index.d.ts +407 -30
- package/build/index.js +979 -271
- package/build/index.js.map +1 -1
- package/image/Dockerfile +2 -1
- package/image/native/src/bin/client.rs +47 -8
- package/image/native/src/bin/inject.rs +280 -33
- package/image/router/Dockerfile +8 -0
- package/image/router/ctwatch.py +66 -0
- package/image/router/mbx.sh +61 -0
- package/image/router/resetter.py +104 -0
- package/package.json +6 -2
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The reset at expiry: the form of a middlebox that resets the two ends when it forgets a flow.
|
|
3
|
+
|
|
4
|
+
It listens to the DESTROY events of conntrack in this namespace. For each destroyed TCP flow that
|
|
5
|
+
it saw carry traffic, and never saw closed (FIN or RST), it sends a reset to each end. Each reset
|
|
6
|
+
has the sequence number that its end expects: the last acknowledgment number that the end sent,
|
|
7
|
+
read by a raw sniffer on the interfaces, because conntrack events carry no sequence numbers.
|
|
8
|
+
Each line is JSON on the monotonic clock: {"ev": "reset", "t": ..., "flow": ..., "to": [...]}.
|
|
9
|
+
|
|
10
|
+
resetter.py IFACE [IFACE...]
|
|
11
|
+
"""
|
|
12
|
+
import argparse, json, re, socket, struct, subprocess, threading, time
|
|
13
|
+
|
|
14
|
+
p = argparse.ArgumentParser()
|
|
15
|
+
p.add_argument("ifaces", nargs="+")
|
|
16
|
+
a = p.parse_args()
|
|
17
|
+
|
|
18
|
+
plock = threading.Lock()
|
|
19
|
+
flock = threading.Lock()
|
|
20
|
+
flows = {} # (src, sport, dst, dport) -> {"ack", "closed"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def emit(**fields):
|
|
24
|
+
with plock:
|
|
25
|
+
print(json.dumps({"t": time.clock_gettime_ns(time.CLOCK_MONOTONIC), **fields}), flush=True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def sniff(ifname):
|
|
29
|
+
s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(0x0800))
|
|
30
|
+
s.bind((ifname, 0))
|
|
31
|
+
while True:
|
|
32
|
+
pkt = s.recv(65535)
|
|
33
|
+
ip = pkt[14:]
|
|
34
|
+
if len(ip) < 40 or ip[0] >> 4 != 4 or ip[9] != 6:
|
|
35
|
+
continue
|
|
36
|
+
ihl = (ip[0] & 0xF) * 4
|
|
37
|
+
src, dst = socket.inet_ntoa(ip[12:16]), socket.inet_ntoa(ip[16:20])
|
|
38
|
+
tcp = ip[ihl:]
|
|
39
|
+
sport, dport, _seq, ack = struct.unpack("!HHII", tcp[:12])
|
|
40
|
+
fl = tcp[13]
|
|
41
|
+
with flock:
|
|
42
|
+
f = flows.setdefault((src, sport, dst, dport), {"ack": None, "closed": False})
|
|
43
|
+
if fl & 0x10:
|
|
44
|
+
f["ack"] = ack
|
|
45
|
+
if fl & 0x05:
|
|
46
|
+
f["closed"] = True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
raw = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def csum(b):
|
|
53
|
+
if len(b) % 2:
|
|
54
|
+
b += b"\0"
|
|
55
|
+
s = sum(struct.unpack("!%dH" % (len(b) // 2), b))
|
|
56
|
+
while s >> 16:
|
|
57
|
+
s = (s & 0xFFFF) + (s >> 16)
|
|
58
|
+
return ~s & 0xFFFF
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def send_rst(src, sport, dst, dport, seq):
|
|
62
|
+
tcp = struct.pack("!HHIIBBHHH", sport, dport, seq, 0, 5 << 4, 0x04, 0, 0, 0)
|
|
63
|
+
pseudo = socket.inet_aton(src) + socket.inet_aton(dst) + struct.pack("!BBH", 0, 6, len(tcp))
|
|
64
|
+
tcp = tcp[:16] + struct.pack("!H", csum(pseudo + tcp)) + tcp[18:]
|
|
65
|
+
iph = struct.pack(
|
|
66
|
+
"!BBHHHBBH4s4s", 0x45, 0, 20 + len(tcp), 0, 0x4000, 64, 6, 0, socket.inet_aton(src), socket.inet_aton(dst)
|
|
67
|
+
)
|
|
68
|
+
raw.sendto(iph + tcp, (dst, 0))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
PAT = re.compile(r"src=(\S+) dst=(\S+) sport=(\d+) dport=(\d+) .*?src=(\S+) dst=(\S+) sport=(\d+) dport=(\d+)")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main():
|
|
75
|
+
for i in a.ifaces:
|
|
76
|
+
threading.Thread(target=sniff, args=(i,), daemon=True).start()
|
|
77
|
+
proc = subprocess.Popen(
|
|
78
|
+
["conntrack", "-E", "-e", "DESTROY", "-p", "tcp"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1
|
|
79
|
+
)
|
|
80
|
+
emit(ev="resetter", ifaces=a.ifaces)
|
|
81
|
+
for line in proc.stdout:
|
|
82
|
+
m = PAT.search(line)
|
|
83
|
+
if not m:
|
|
84
|
+
continue
|
|
85
|
+
c, s, cp, sp, s2, r, sp2, rp = m.groups()
|
|
86
|
+
cp, sp, sp2, rp = int(cp), int(sp), int(sp2), int(rp)
|
|
87
|
+
k_cs, k_sr = (c, cp, s, sp), (s2, sp2, r, rp)
|
|
88
|
+
with flock:
|
|
89
|
+
fs = {k: dict(flows[k]) for k in (k_cs, (s, sp, c, cp), (r, rp, s2, sp2), k_sr) if k in flows}
|
|
90
|
+
if not fs or any(f["closed"] for f in fs.values()):
|
|
91
|
+
continue
|
|
92
|
+
seq_c = fs.get(k_cs, {}).get("ack") # the RCV.NXT of the client
|
|
93
|
+
seq_s = fs.get(k_sr, {}).get("ack") # the RCV.NXT of the server, on the translated side
|
|
94
|
+
sent = []
|
|
95
|
+
if seq_c is not None:
|
|
96
|
+
send_rst(s, sp, c, cp, seq_c)
|
|
97
|
+
sent.append("client")
|
|
98
|
+
if seq_s is not None:
|
|
99
|
+
send_rst(r, rp, s2, sp2, seq_s)
|
|
100
|
+
sent.append("server")
|
|
101
|
+
emit(ev="reset", flow=f"{c}:{cp}>{s}:{sp}", nat=f"{r}:{rp}", to=sent)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
main()
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@mark1russell7/harness",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"description": "A fault-injection harness for connection timers: a cell runs a client against a stock target in Docker, injects a fault, measures the kernel's own records and judges a prediction.",
|
|
4
|
+
"version": "0.3.0",
|
|
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",
|
|
8
8
|
"chaos-engineering",
|
|
@@ -45,6 +45,10 @@
|
|
|
45
45
|
"image/node/client-ioredis.mjs",
|
|
46
46
|
"image/node/package.json",
|
|
47
47
|
"image/node/npm-lock.json",
|
|
48
|
+
"image/router/Dockerfile",
|
|
49
|
+
"image/router/mbx.sh",
|
|
50
|
+
"image/router/ctwatch.py",
|
|
51
|
+
"image/router/resetter.py",
|
|
48
52
|
"README.md",
|
|
49
53
|
"LICENSE"
|
|
50
54
|
],
|