@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.
@@ -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.1.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
  ],