@mark1russell7/prober 0.0.0-stage → 0.2.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,73 @@
1
+ import { Quantity } from "@mark1russell7/quantity";
2
+ //#region src/index.d.ts
3
+ /** These are the kinds of record, in the sequence of the enum in `bpf/prober.h` and of `KINDS` in the Rust decoder. */
4
+ export declare const RECORD_KINDS: readonly ["state", "accept", "retrans", "rst_tx", "rst_rx", "error", "destroy", "tw_enter", "tw_kill", "req_new", "rst_v4", "probe", "rx", "timer", "tx"];
5
+ /** A record kind is one kind of record that the kernel side writes. */
6
+ export type RecordKind = (typeof RECORD_KINDS)[number];
7
+ /** These are the lines of the collector itself: its start, its clock track, its failure, and a record that it cannot read. */
8
+ export type CollectorLine = "begin" | "clock" | "fail" | "undecodable";
9
+ /**
10
+ * A kernel record is one record of the prober: what the kernel did to a socket. The eBPF side
11
+ * stamps it on CLOCK_MONOTONIC, the clock that all containers on the VM share (`native/src/record.rs`).
12
+ */
13
+ export interface KernelRecord {
14
+ readonly ev: RecordKind | CollectorLine | string;
15
+ readonly mono: number;
16
+ readonly cookie?: number;
17
+ readonly netns?: number;
18
+ readonly src?: string;
19
+ readonly dst?: string;
20
+ readonly state?: number;
21
+ /** This number is the `sk_err` of the socket at the time of the record: the errno of an error report. */
22
+ readonly err?: number;
23
+ /** This value is the cause of a retransmission: the tail loss probe, the RTO timer, or a fast retransmission from ACK processing. */
24
+ readonly why?: "tlp" | "rto" | "fast";
25
+ /** This value is the kind of a probe that `tcp_write_wakeup` sent, or of a socket timer that fired. */
26
+ readonly what?: "keepalive" | "window" | "retransmit" | "other";
27
+ /**
28
+ * For a socket timer that fired, `behind` is the delay of the CPU after the slot of the timer
29
+ * wheel. Only a stalled CPU causes that delay. `late` is the time after the expiry of the timer,
30
+ * with the rounding of the wheel.
31
+ */
32
+ readonly behind?: Quantity;
33
+ readonly late?: Quantity;
34
+ /** This flag tells that a socket timer fired while the owner of the socket held it. Keepalive then tries again HZ/20 later. */
35
+ readonly owned?: boolean;
36
+ readonly tcp?: {
37
+ readonly retransmits: number;
38
+ readonly pending: number;
39
+ readonly backoff: number;
40
+ readonly probes_out: number;
41
+ readonly rto: Quantity;
42
+ readonly srtt: Quantity;
43
+ readonly snd_una?: number;
44
+ readonly snd_nxt?: number;
45
+ readonly rcv_nxt?: number;
46
+ /**
47
+ * These counts are the SACK scoreboard and the reordering state, as `tcp_info` names them. A
48
+ * record of version 0.1 does not have them. `sacked` and `lost` are the segments that the peer
49
+ * SACKed and that the kernel marked lost. `reordering` is the reordering degree of the kernel,
50
+ * in segments, and `reord_seen` counts the reorderings that it detected. `dsack_dups` counts
51
+ * the DSACK blocks that arrived, and `rcv_ooopack` the segments that arrived out of order.
52
+ */
53
+ readonly sacked?: number;
54
+ readonly lost?: number;
55
+ readonly reordering?: number;
56
+ readonly reord_seen?: number;
57
+ readonly dsack_dups?: number;
58
+ readonly rcv_ooopack?: number;
59
+ };
60
+ /**
61
+ * This record is the segment of a record. For `rx`, it has the acknowledgment and the flags. For
62
+ * `tx`, it is the write: its first sequence number and its length.
63
+ */
64
+ readonly seg?: {
65
+ readonly seq: number;
66
+ readonly ack?: number;
67
+ readonly flags?: number;
68
+ readonly len: Quantity;
69
+ };
70
+ readonly [field: string]: unknown;
71
+ }
72
+ //#endregion
73
+ //# sourceMappingURL=index.d.ts.map
package/build/index.js ADDED
@@ -0,0 +1,23 @@
1
+ //#region src/index.ts
2
+ /** These are the kinds of record, in the sequence of the enum in `bpf/prober.h` and of `KINDS` in the Rust decoder. */
3
+ const RECORD_KINDS = [
4
+ "state",
5
+ "accept",
6
+ "retrans",
7
+ "rst_tx",
8
+ "rst_rx",
9
+ "error",
10
+ "destroy",
11
+ "tw_enter",
12
+ "tw_kill",
13
+ "req_new",
14
+ "rst_v4",
15
+ "probe",
16
+ "rx",
17
+ "timer",
18
+ "tx"
19
+ ];
20
+ //#endregion
21
+ export { RECORD_KINDS };
22
+
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["// @mark1russell7/prober: the instrument that records what each layer did to a connection. Its\n// first source is the kernel: bpf/prober.bpf.c, a libbpf CO-RE program, and the collector in\n// native/ that prints each record as a JSON line. This module is the record contract for the\n// readers of those lines.\n\nimport type { Quantity } from \"@mark1russell7/quantity\";\n\n/** These are the kinds of record, in the sequence of the enum in `bpf/prober.h` and of `KINDS` in the Rust decoder. */\nexport const RECORD_KINDS = [\n \"state\",\n \"accept\",\n \"retrans\",\n \"rst_tx\",\n \"rst_rx\",\n \"error\",\n \"destroy\",\n \"tw_enter\",\n \"tw_kill\",\n \"req_new\",\n \"rst_v4\",\n \"probe\",\n \"rx\",\n \"timer\",\n \"tx\",\n] as const;\n\n/** A record kind is one kind of record that the kernel side writes. */\nexport type RecordKind = (typeof RECORD_KINDS)[number];\n\n/** These are the lines of the collector itself: its start, its clock track, its failure, and a record that it cannot read. */\nexport type CollectorLine = \"begin\" | \"clock\" | \"fail\" | \"undecodable\";\n\n/**\n * A kernel record is one record of the prober: what the kernel did to a socket. The eBPF side\n * stamps it on CLOCK_MONOTONIC, the clock that all containers on the VM share (`native/src/record.rs`).\n */\nexport interface KernelRecord {\n readonly ev: RecordKind | CollectorLine | string;\n readonly mono: number;\n readonly cookie?: number;\n readonly netns?: number;\n readonly src?: string;\n readonly dst?: string;\n readonly state?: number;\n /** This number is the `sk_err` of the socket at the time of the record: the errno of an error report. */\n readonly err?: number;\n /** This value is the cause of a retransmission: the tail loss probe, the RTO timer, or a fast retransmission from ACK processing. */\n readonly why?: \"tlp\" | \"rto\" | \"fast\";\n /** This value is the kind of a probe that `tcp_write_wakeup` sent, or of a socket timer that fired. */\n readonly what?: \"keepalive\" | \"window\" | \"retransmit\" | \"other\";\n /**\n * For a socket timer that fired, `behind` is the delay of the CPU after the slot of the timer\n * wheel. Only a stalled CPU causes that delay. `late` is the time after the expiry of the timer,\n * with the rounding of the wheel.\n */\n readonly behind?: Quantity;\n readonly late?: Quantity;\n /** This flag tells that a socket timer fired while the owner of the socket held it. Keepalive then tries again HZ/20 later. */\n readonly owned?: boolean;\n readonly tcp?: {\n readonly retransmits: number;\n readonly pending: number;\n readonly backoff: number;\n readonly probes_out: number;\n readonly rto: Quantity;\n readonly srtt: Quantity;\n readonly snd_una?: number;\n readonly snd_nxt?: number;\n readonly rcv_nxt?: number;\n /**\n * These counts are the SACK scoreboard and the reordering state, as `tcp_info` names them. A\n * record of version 0.1 does not have them. `sacked` and `lost` are the segments that the peer\n * SACKed and that the kernel marked lost. `reordering` is the reordering degree of the kernel,\n * in segments, and `reord_seen` counts the reorderings that it detected. `dsack_dups` counts\n * the DSACK blocks that arrived, and `rcv_ooopack` the segments that arrived out of order.\n */\n readonly sacked?: number;\n readonly lost?: number;\n readonly reordering?: number;\n readonly reord_seen?: number;\n readonly dsack_dups?: number;\n readonly rcv_ooopack?: number;\n };\n /**\n * This record is the segment of a record. For `rx`, it has the acknowledgment and the flags. For\n * `tx`, it is the write: its first sequence number and its length.\n */\n readonly seg?: {\n readonly seq: number;\n readonly ack?: number;\n readonly flags?: number;\n readonly len: Quantity;\n };\n readonly [field: string]: unknown;\n}\n"],"mappings":";;AAQA,MAAa,eAAe;CAC1B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF"}
Binary file
@@ -0,0 +1,206 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "bitflags"
7
+ version = "2.13.2"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
10
+
11
+ [[package]]
12
+ name = "cc"
13
+ version = "1.6.0"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630"
16
+ dependencies = [
17
+ "find-msvc-tools",
18
+ "shlex",
19
+ ]
20
+
21
+ [[package]]
22
+ name = "cfg-if"
23
+ version = "1.0.5"
24
+ source = "registry+https://github.com/rust-lang/crates.io-index"
25
+ checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
26
+
27
+ [[package]]
28
+ name = "cfg_aliases"
29
+ version = "0.2.2"
30
+ source = "registry+https://github.com/rust-lang/crates.io-index"
31
+ checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
32
+
33
+ [[package]]
34
+ name = "find-msvc-tools"
35
+ version = "0.1.14"
36
+ source = "registry+https://github.com/rust-lang/crates.io-index"
37
+ checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
38
+
39
+ [[package]]
40
+ name = "itoa"
41
+ version = "1.0.18"
42
+ source = "registry+https://github.com/rust-lang/crates.io-index"
43
+ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
44
+
45
+ [[package]]
46
+ name = "libbpf-rs"
47
+ version = "0.27.2"
48
+ source = "registry+https://github.com/rust-lang/crates.io-index"
49
+ checksum = "ffef943d6690772a293abe80412ca438c227867cac93448af2518166df1b1a7b"
50
+ dependencies = [
51
+ "bitflags",
52
+ "libbpf-sys",
53
+ "libc",
54
+ "vsprintf",
55
+ ]
56
+
57
+ [[package]]
58
+ name = "libbpf-sys"
59
+ version = "1.7.0+v1.7.0"
60
+ source = "registry+https://github.com/rust-lang/crates.io-index"
61
+ checksum = "a109478760b2900aa2a6f2087e9d0de1d9c535b1758602af2845d5d2ccfaed7c"
62
+ dependencies = [
63
+ "cc",
64
+ "nix",
65
+ "pkg-config",
66
+ ]
67
+
68
+ [[package]]
69
+ name = "libc"
70
+ version = "0.2.190"
71
+ source = "registry+https://github.com/rust-lang/crates.io-index"
72
+ checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
73
+
74
+ [[package]]
75
+ name = "memchr"
76
+ version = "2.8.3"
77
+ source = "registry+https://github.com/rust-lang/crates.io-index"
78
+ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
79
+
80
+ [[package]]
81
+ name = "nix"
82
+ version = "0.31.3"
83
+ source = "registry+https://github.com/rust-lang/crates.io-index"
84
+ checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
85
+ dependencies = [
86
+ "bitflags",
87
+ "cfg-if",
88
+ "cfg_aliases",
89
+ "libc",
90
+ ]
91
+
92
+ [[package]]
93
+ name = "pkg-config"
94
+ version = "0.3.34"
95
+ source = "registry+https://github.com/rust-lang/crates.io-index"
96
+ checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
97
+
98
+ [[package]]
99
+ name = "prober"
100
+ version = "0.1.0"
101
+ dependencies = [
102
+ "libbpf-rs",
103
+ "libc",
104
+ "serde",
105
+ "serde_json",
106
+ ]
107
+
108
+ [[package]]
109
+ name = "proc-macro2"
110
+ version = "1.0.107"
111
+ source = "registry+https://github.com/rust-lang/crates.io-index"
112
+ checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
113
+ dependencies = [
114
+ "unicode-ident",
115
+ ]
116
+
117
+ [[package]]
118
+ name = "quote"
119
+ version = "1.0.47"
120
+ source = "registry+https://github.com/rust-lang/crates.io-index"
121
+ checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
122
+ dependencies = [
123
+ "proc-macro2",
124
+ ]
125
+
126
+ [[package]]
127
+ name = "serde"
128
+ version = "1.0.229"
129
+ source = "registry+https://github.com/rust-lang/crates.io-index"
130
+ checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
131
+ dependencies = [
132
+ "serde_core",
133
+ "serde_derive",
134
+ ]
135
+
136
+ [[package]]
137
+ name = "serde_core"
138
+ version = "1.0.229"
139
+ source = "registry+https://github.com/rust-lang/crates.io-index"
140
+ checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
141
+ dependencies = [
142
+ "serde_derive",
143
+ ]
144
+
145
+ [[package]]
146
+ name = "serde_derive"
147
+ version = "1.0.229"
148
+ source = "registry+https://github.com/rust-lang/crates.io-index"
149
+ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
150
+ dependencies = [
151
+ "proc-macro2",
152
+ "quote",
153
+ "syn",
154
+ ]
155
+
156
+ [[package]]
157
+ name = "serde_json"
158
+ version = "1.0.151"
159
+ source = "registry+https://github.com/rust-lang/crates.io-index"
160
+ checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
161
+ dependencies = [
162
+ "itoa",
163
+ "memchr",
164
+ "serde",
165
+ "serde_core",
166
+ "zmij",
167
+ ]
168
+
169
+ [[package]]
170
+ name = "shlex"
171
+ version = "2.0.1"
172
+ source = "registry+https://github.com/rust-lang/crates.io-index"
173
+ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
174
+
175
+ [[package]]
176
+ name = "syn"
177
+ version = "3.0.6"
178
+ source = "registry+https://github.com/rust-lang/crates.io-index"
179
+ checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
180
+ dependencies = [
181
+ "proc-macro2",
182
+ "quote",
183
+ "unicode-ident",
184
+ ]
185
+
186
+ [[package]]
187
+ name = "unicode-ident"
188
+ version = "1.0.26"
189
+ source = "registry+https://github.com/rust-lang/crates.io-index"
190
+ checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
191
+
192
+ [[package]]
193
+ name = "vsprintf"
194
+ version = "2.0.0"
195
+ source = "registry+https://github.com/rust-lang/crates.io-index"
196
+ checksum = "aec2f81b75ca063294776b4f7e8da71d1d5ae81c2b1b149c8d89969230265d63"
197
+ dependencies = [
198
+ "cc",
199
+ "libc",
200
+ ]
201
+
202
+ [[package]]
203
+ name = "zmij"
204
+ version = "1.0.23"
205
+ source = "registry+https://github.com/rust-lang/crates.io-index"
206
+ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
@@ -0,0 +1,30 @@
1
+ [package]
2
+ name = "prober"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ publish = false
6
+ description = "The prober's native side: the record contract, the clock, and the collector for the kernel source"
7
+
8
+ [lib]
9
+ name = "prober"
10
+ path = "src/lib.rs"
11
+
12
+ # the collector loads the kernel source, so it alone needs libbpf: the harness's tools use the
13
+ # library without it
14
+ [[bin]]
15
+ name = "prober"
16
+ path = "src/bin/prober.rs"
17
+ required-features = ["collector"]
18
+
19
+ [features]
20
+ collector = ["dep:libbpf-rs"]
21
+
22
+ [dependencies]
23
+ libbpf-rs = { version = "=0.27.2", optional = true }
24
+ libc = "0.2"
25
+ serde = { version = "1", features = ["derive"] }
26
+ serde_json = "1"
27
+
28
+ [profile.release]
29
+ lto = true
30
+ codegen-units = 1
@@ -0,0 +1,161 @@
1
+ //! prober [--object PATH] NETNS... | prober --decode FILE
2
+ //!
3
+ //! The collector for the prober's kernel source. It loads `prober.bpf.o`, watches
4
+ //! the network namespaces given by their inode numbers, and prints every record the kernel writes
5
+ //! as one JSON line. Every second it also prints the clock track (CLOCK_TAI beside CLOCK_MONOTONIC)
6
+ //! and how many records the ring buffer had to drop, which must stay at zero.
7
+
8
+ use prober::record::{self, Ev};
9
+ use prober::{monotonic_ns, tai_ns};
10
+ use libbpf_rs::{MapCore, MapFlags, ObjectBuilder, RingBufferBuilder};
11
+ use serde_json::json;
12
+ use std::cell::RefCell;
13
+ use std::error::Error;
14
+ use std::ffi::CString;
15
+ use std::io::{BufWriter, Stdout, Write};
16
+ use std::path::Path;
17
+ use std::rc::Rc;
18
+ use std::time::Duration;
19
+
20
+ const OBJECT: &str = "/opt/harness/prober.bpf.o";
21
+ const TRACEFS: &str = "/sys/kernel/tracing";
22
+
23
+ /// Classic tracepoints (the connect and accept syscalls) attach through tracefs, which a
24
+ /// container does not mount. A privileged one may mount it for itself.
25
+ fn mount_tracefs() -> Result<(), Box<dyn Error>> {
26
+ if Path::new(TRACEFS).join("events").exists() {
27
+ return Ok(());
28
+ }
29
+ let (source, target) = (CString::new("tracefs")?, CString::new(TRACEFS)?);
30
+ // SAFETY: every pointer is a NUL-terminated string that outlives the call; data may be null
31
+ let status = unsafe {
32
+ libc::mount(source.as_ptr(), target.as_ptr(), source.as_ptr(), 0, std::ptr::null())
33
+ };
34
+ if status != 0 {
35
+ return Err(format!("mount tracefs: {}", std::io::Error::last_os_error()).into());
36
+ }
37
+ Ok(())
38
+ }
39
+
40
+ fn line(out: &Rc<RefCell<BufWriter<Stdout>>>, value: &serde_json::Value) {
41
+ let mut out = out.borrow_mut();
42
+ let _ = serde_json::to_writer(&mut *out, value);
43
+ let _ = out.write_all(b"\n");
44
+ }
45
+
46
+ /// A record's line, written by hand into a buffer the caller reuses, then out in one write.
47
+ fn record(out: &Rc<RefCell<BufWriter<Stdout>>>, ev: &Ev, buffer: &mut Vec<u8>) {
48
+ buffer.clear();
49
+ ev.append_line(buffer);
50
+ buffer.push(b'\n');
51
+ let _ = out.borrow_mut().write_all(buffer);
52
+ }
53
+
54
+ /// Prints the line for every record in a file of raw records, as the ring buffer would hand them
55
+ /// over: how the three collectors are held to one contract byte for byte.
56
+ fn decode_file(path: &str) -> Result<(), Box<dyn Error>> {
57
+ let data = std::fs::read(path).map_err(|error| format!("{path}: {error}"))?;
58
+ let out = Rc::new(RefCell::new(BufWriter::with_capacity(1 << 16, std::io::stdout())));
59
+ let mut buffer = Vec::with_capacity(1024);
60
+ for chunk in data.chunks(record::SIZE) {
61
+ match Ev::decode(chunk) {
62
+ Ok(ev) => record(&out, &ev, &mut buffer),
63
+ Err(why) => line(&out, &json!({ "ev": "undecodable", "why": why })),
64
+ }
65
+ }
66
+ out.borrow_mut().flush()?;
67
+ Ok(())
68
+ }
69
+
70
+ fn run() -> Result<(), Box<dyn Error>> {
71
+ let mut args: Vec<String> = std::env::args().skip(1).collect();
72
+ if let Some(at) = args.iter().position(|arg| arg == "--decode") {
73
+ return decode_file(args.get(at + 1).ok_or("--decode needs a file")?);
74
+ }
75
+ let mut object = OBJECT.to_string();
76
+ if let Some(at) = args.iter().position(|arg| arg == "--object") {
77
+ object = args.get(at + 1).ok_or("--object needs a path")?.clone();
78
+ args.drain(at..=at + 1);
79
+ }
80
+ let watched: Vec<u32> = args
81
+ .iter()
82
+ .map(|arg| arg.parse::<u32>().map_err(|_| format!("{arg} is not a netns inode")))
83
+ .collect::<Result<_, _>>()?;
84
+ if watched.is_empty() {
85
+ return Err("name at least one network namespace to watch, by its inode".into());
86
+ }
87
+
88
+ mount_tracefs()?;
89
+ let obj = ObjectBuilder::default().open_file(&object)?.load()?;
90
+ let want = obj.maps().find(|map| map.name() == "want").ok_or("the object has no map want")?;
91
+ for netns in &watched {
92
+ want.update(&netns.to_ne_bytes(), &[1u8], MapFlags::ANY)?;
93
+ }
94
+ let mut links = Vec::new();
95
+ for prog in obj.progs_mut() {
96
+ let name = prog.name().to_string_lossy().into_owned();
97
+ links.push(prog.attach().map_err(|error| format!("attach {name}: {error}"))?);
98
+ }
99
+
100
+ let out = Rc::new(RefCell::new(BufWriter::with_capacity(1 << 16, std::io::stdout())));
101
+ let records = Rc::new(RefCell::new(0u64));
102
+ let ring = obj.maps().find(|map| map.name() == "rb").ok_or("the object has no map rb")?;
103
+ let mut builder = RingBufferBuilder::new();
104
+ {
105
+ let (out, records) = (Rc::clone(&out), Rc::clone(&records));
106
+ let mut buffer = Vec::with_capacity(1024);
107
+ builder.add(&ring, move |data: &[u8]| {
108
+ match Ev::decode(data) {
109
+ Ok(ev) => {
110
+ *records.borrow_mut() += 1;
111
+ record(&out, &ev, &mut buffer);
112
+ }
113
+ Err(why) => line(&out, &json!({ "ev": "undecodable", "why": why })),
114
+ }
115
+ 0
116
+ })?;
117
+ }
118
+ let ring = builder.build()?;
119
+ let lost = obj.maps().find(|map| map.name() == "lost").ok_or("the object has no map lost")?;
120
+ let dropped = || -> u64 {
121
+ lost.lookup_percpu(&0u32.to_ne_bytes(), MapFlags::ANY)
122
+ .ok()
123
+ .flatten()
124
+ .map(|cpus| {
125
+ cpus.iter()
126
+ .filter_map(|bytes| bytes.get(..8))
127
+ .map(|bytes| u64::from_ne_bytes(bytes.try_into().unwrap_or([0; 8])))
128
+ .sum()
129
+ })
130
+ .unwrap_or(0)
131
+ };
132
+
133
+ line(
134
+ &out,
135
+ &json!({ "ev": "begin", "mono": monotonic_ns(), "tai": tai_ns(), "netns": watched, "programs": links.len() }),
136
+ );
137
+ out.borrow_mut().flush()?;
138
+ let mut next = monotonic_ns() + 1_000_000_000;
139
+ loop {
140
+ // woken only when 256 KiB are waiting, so drain on every 20 ms tick as well
141
+ ring.poll(Duration::from_millis(20))?;
142
+ ring.consume()?;
143
+ let now = monotonic_ns();
144
+ if now >= next {
145
+ let count = *records.borrow();
146
+ line(
147
+ &out,
148
+ &json!({ "ev": "clock", "mono": now, "tai": tai_ns(), "records": count, "lost": dropped() }),
149
+ );
150
+ next += 1_000_000_000;
151
+ }
152
+ out.borrow_mut().flush()?;
153
+ }
154
+ }
155
+
156
+ fn main() {
157
+ if let Err(error) = run() {
158
+ println!("{}", json!({ "ev": "fail", "mono": monotonic_ns(), "why": error.to_string() }));
159
+ std::process::exit(1);
160
+ }
161
+ }
@@ -0,0 +1,75 @@
1
+ //! The prober's native side:
2
+ //! - the record contract: the C struct the kernel source writes (`record`), and the JSON line it
3
+ //! becomes;
4
+ //! - the clock, the quantities and the errno names, which the harness's tools share;
5
+ //! - with the `collector` feature, the `prober` binary, which loads `bpf/prober.bpf.c` and prints
6
+ //! every record the kernel writes as one JSON line.
7
+ //!
8
+ //! Every instant printed is CLOCK_MONOTONIC in nanoseconds: eBPF stamps with that clock, and every
9
+ //! container on the VM shares it. Every amount is a quantity,
10
+ //! `{ "value": …, "unit": … }`, in @mark1russell7/quantity's canonical units.
11
+
12
+ pub mod record;
13
+
14
+ use serde::Serialize;
15
+
16
+ fn clock_ns(clock: libc::clockid_t) -> u64 {
17
+ let mut now = libc::timespec { tv_sec: 0, tv_nsec: 0 };
18
+ // SAFETY: clock_gettime only writes the timespec it is handed, which lives on this stack
19
+ let status = unsafe { libc::clock_gettime(clock, &mut now) };
20
+ assert_eq!(status, 0, "clock_gettime({clock}) failed");
21
+ now.tv_sec as u64 * 1_000_000_000 + now.tv_nsec as u64
22
+ }
23
+
24
+ /// CLOCK_MONOTONIC, the clock the kernel source stamps with.
25
+ pub fn monotonic_ns() -> u64 {
26
+ clock_ns(libc::CLOCK_MONOTONIC)
27
+ }
28
+
29
+ /// CLOCK_TAI, for the clock track: REALTIME steps on this VM, TAI does not jump relative to it.
30
+ pub fn tai_ns() -> u64 {
31
+ clock_ns(libc::CLOCK_TAI)
32
+ }
33
+
34
+ /// An amount and its canonical unit, as @mark1russell7/quantity writes one; its keys in byte order, as
35
+ /// serde_json writes any object it holds as a map.
36
+ #[derive(Serialize, Clone, Copy, Debug, PartialEq)]
37
+ pub struct Quantity {
38
+ pub unit: &'static str,
39
+ pub value: f64,
40
+ }
41
+
42
+ pub fn quantity(value: impl Into<f64>, unit: &'static str) -> Quantity {
43
+ Quantity { value: value.into(), unit }
44
+ }
45
+
46
+ /// The name of an errno, as the harness's predictions name the error a detector raises.
47
+ pub fn errno_name(code: i32) -> String {
48
+ match code {
49
+ libc::ETIMEDOUT => "ETIMEDOUT".into(),
50
+ libc::ECONNRESET => "ECONNRESET".into(),
51
+ libc::ECONNREFUSED => "ECONNREFUSED".into(),
52
+ libc::EHOSTUNREACH => "EHOSTUNREACH".into(),
53
+ libc::ENETUNREACH => "ENETUNREACH".into(),
54
+ libc::EPIPE => "EPIPE".into(),
55
+ libc::ECONNABORTED => "ECONNABORTED".into(),
56
+ other => format!("E{other}"),
57
+ }
58
+ }
59
+
60
+ #[cfg(test)]
61
+ mod tests {
62
+ use super::*;
63
+
64
+ #[test]
65
+ fn names_the_errors_a_detector_raises() {
66
+ assert_eq!(errno_name(libc::ETIMEDOUT), "ETIMEDOUT");
67
+ assert_eq!(errno_name(9999), "E9999");
68
+ }
69
+
70
+ #[test]
71
+ fn reads_a_monotonic_clock_that_never_steps_back() {
72
+ let first = monotonic_ns();
73
+ assert!(monotonic_ns() >= first);
74
+ }
75
+ }