@mark1russell7/prober 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mark Russell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,112 @@
1
- # Temporary Holding Version
1
+ # @mark1russell7/prober
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An eBPF prober for TCP. It records what the kernel did to each socket, and eBPF stamps each record on CLOCK_MONOTONIC, the clock that all containers on a VM share.
4
+
5
+ The package has three parts:
6
+
7
+ - **`bpf/prober.bpf.c`**, in C: a libbpf CO-RE program. The image of [`@mark1russell7/harness`](https://www.npmjs.com/package/@mark1russell7/harness) compiles it against the BTF of the kernel that it operates on.
8
+ - **`native/`**, in Rust, on libbpf-rs: the record decoder and the `prober` collector.
9
+ - **`src/`**, in TypeScript: the record contract for the readers of the lines of the collector, `KernelRecord` and `RECORD_KINDS`.
10
+
11
+ ## The records
12
+
13
+ The kernel program records these events:
14
+
15
+ - Each TCP state change, and the owner of the socket at connect and at accept.
16
+ - Retransmissions, resets, error reports and the destruction of a socket.
17
+ - TIME_WAIT sockets and request sockets.
18
+ - Each keepalive probe or zero-window probe that `tcp_write_wakeup` sends.
19
+ - Each segment that an established socket receives (`tcp_probe`).
20
+ - The state of the retransmit timer at each retransmission.
21
+ - Each time that the timer wheel fires the keepalive timer or the retransmit timer of a socket (`timer`).
22
+
23
+ The state of the retransmit timer tells the tail loss probe from the retransmissions of the RTO timer. Only the RTO timer counts toward `tcp_retries2`.
24
+
25
+ A `timer` record has three more fields:
26
+
27
+ - **`behind`** is the number of jiffies between the slot of the timer on the wheel and the time that the CPU fired it. The wheel fires a slot in its tick. Thus a timer behind its slot shows a delay of its CPU, for example a vCPU that the host stopped.
28
+ - **`late`** is the number of jiffies after the expiry that the owner of the timer set. It is the rounding of the wheel.
29
+ - **`owned`** tells that the owner of the socket held the socket when the timer fired. A keepalive timer then sends nothing, and it tries again HZ/20 later.
30
+
31
+ To find the slot, `timer_expire_entry` keeps, for each CPU, the timer that the wheel fires and its slot. The function of the timer then finds itself there.
32
+
33
+ Each amount in a record is a quantity in a canonical unit of [`@mark1russell7/quantity`](https://www.npmjs.com/package/@mark1russell7/quantity). An RTO or the delay of a timer is in `jiffy`, a round trip is in `us`, and a segment is in `B`.
34
+
35
+ ## The collector
36
+
37
+ The Rust crate has two parts:
38
+
39
+ - The library has the record decoder, which reads the C struct field by field with no `unsafe`. It also has the clock, the quantities and the names of the errno values, which the tools of the harness share.
40
+ - The `collector` feature adds the `prober` binary. It loads the kernel program and watches the network namespaces that it gets by inode. It prints each record as one JSON line.
41
+
42
+ Each second, the collector also prints the clock track and the number of records that the ring buffer dropped.
43
+
44
+ ## The contract, written three times
45
+
46
+ The C enum that the kernel side stamps, the names of the Rust decoder and `RECORD_KINDS` must agree in sequence. Otherwise, a record reads as the wrong kind. `src/index.test.ts` reads the other two and compares them.
47
+
48
+ The line of a record is fixed to the byte:
49
+
50
+ - Its keys are in byte sequence, as serde_json writes each object that it holds.
51
+ - An amount is `{"unit":…,"value":N.0}`.
52
+ - Strings have the escapes of serde_json.
53
+ - A task name that is not UTF-8 decodes as `String::from_utf8_lossy` of Rust decodes it.
54
+
55
+ ## Three collectors
56
+
57
+ Three collectors load the same `prober.bpf.o` and print the same lines:
58
+
59
+ - `native/`, Rust on libbpf-rs: the collector that the harness uses
60
+ - `go/`, Go on cilium/ebpf v0.22: pure Go, with no cgo, in one static binary
61
+ - `c/prober.c`, C on libbpf 1.6
62
+
63
+ The Go and C collectors are in the repository for the comparison. They are not in the npm package. A decision to keep them or to remove them is open.
64
+
65
+ Each collector has a `--decode FILE` mode, which reads raw records from a file and not from the kernel. This mode holds the three to one contract:
66
+
67
+ - `fixtures/records.bin` has 46 raw records, and one record that is cut short. The records have each kind, each optional field, the extremes of each integer, the escapes of JSON and each way to break UTF-8. `src/fixture.ts` makes the file, and its test keeps the committed bytes the same as the output.
68
+ - The Rust test compares the writer of the crate with the output of serde, record by record. It also compares the names with the lossy decoder of the standard library.
69
+ - The comparison image (`image/Dockerfile`) decodes the fixture with the three collectors. The build stops unless each line is the same, byte for byte.
70
+
71
+ Then `image/compare.sh` starts the three collectors live in one privileged container. A loopback storm of short connections (`c/storm.c`) gives each record that the life of a connection has.
72
+
73
+ ## The results
74
+
75
+ These results are from 2026-10-11, on kernel 6.6.87.2 under Docker Desktop, with 16 CPUs. Each collector writes its line by hand into one buffer, and it writes the buffer one time. The first versions did not do this, and that difference was larger than the difference between the languages.
76
+
77
+ | | Rust | Go | C |
78
+ | ------------------------------------------------- | --------------------------- | --------------------------- | --------------------------- |
79
+ | The decoder and the writer alone, on raw records | 3.34 M a second | 2.49 M a second | 3.46 M a second |
80
+ | The three on one storm: the same records | 39,839 | 39,839, none different | 39,839, none different |
81
+ | Alone, 20,000 connections, 3 rounds: records lost | none (0.65–0.78 M a second) | none (0.70–0.75 M a second) | none (0.61–0.67 M a second) |
82
+ | CPU for those ≈ 407,000 records | 240–330 ms | 400–440 ms | 260–290 ms |
83
+ | Peak memory (with the 16 MiB ring) | 34 MB | 85–91 MB | 33 MB |
84
+ | The start: load, verify, attach | 1.0–1.1 s | 1.0–1.2 s | 1.2 s |
85
+ | Binary | 0.8 MB (libbpf inside) | 3.6 MB (static) | 49 KB (and libbpf.so) |
86
+ | A clean release build | 20 s | 8 s | 1 s |
87
+
88
+ - Rust and C have the same cost when they have the same loop. Go uses approximately 50 % more CPU and two times the memory for its runtime. It gives a static binary with no C toolchain.
89
+ - The start is the verifier and the attach of the kernel, which are the same in each language.
90
+ - The first versions show the cost of the loop. With serde, which made a map for each record, Rust dropped a third of the records of a heavy storm. Go made and sorted an object for each record, and it dropped more than half. C used `fprintf` one time for each field. It kept up with a slower storm only, and it dropped up to a quarter of a faster storm.
91
+
92
+ To start the comparison again, use this command:
93
+
94
+ ```sh
95
+ LOOM_PROBER_COMPARE=1 pnpm --filter @mark1russell7/prober exec vitest run src/compare.test.ts
96
+ ```
97
+
98
+ The test holds what does not change with the load of the host. These are the bytes, and the same records with none lost under a light storm. It prints the numbers of the heavy storm.
99
+
100
+ ## Installation
101
+
102
+ ```sh
103
+ pnpm add @mark1russell7/prober zod
104
+ ```
105
+
106
+ The package uses `@mark1russell7/quantity`, and `quantity` uses `zod` as a peer dependency. Thus the application installs `zod`. The npm package has the kernel program, the Rust crate and the fixture, thus an image can compile them from `node_modules`.
107
+
108
+ ## Tests
109
+
110
+ - `pnpm --filter @mark1russell7/prober test` compares the contract in C, Rust and TypeScript, and it holds the fixture to its generator.
111
+ - `cargo test --features collector` in the build of the image of the harness tests the record decoder. It also tests the names of tail loss probes, RTO retransmissions and fast retransmissions.
112
+ - The cells of [`@mark1russell7/harness`](https://www.npmjs.com/package/@mark1russell7/harness) measure each interval from these records. The `TCP_INFO` samples of the client agree with them within one sample, and a run with records that the collector dropped is void.
@@ -0,0 +1,372 @@
1
+ // SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
2
+ /* prober.bpf.c - the prober's kernel source: a libbpf CO-RE program that records
3
+ * what the kernel does to every TCP socket in the network namespaces it watches, each record
4
+ * stamped on CLOCK_MONOTONIC, which every container on the VM shares.
5
+ *
6
+ * Its core was built and verified first: the state changes, the owner at
7
+ * connect and accept, retransmissions, resets, error reports, destruction, TIME_WAIT and request
8
+ * socks, with the socket cookie, the namespace and the cgroup. The harness adds four things:
9
+ * - each probe tcp_write_wakeup sends, as keepalive or zero-window;
10
+ * - every segment received on an established socket, so a frozen peer's ACKs show;
11
+ * - the connection's retransmit-timer state on each retransmission, so the tail loss probe (its
12
+ * pending event is the loss probe) parts from the RTO timer's retransmissions, which alone
13
+ * count toward tcp_retries2;
14
+ * - each run of a socket's keepalive or retransmit timer, with how far behind its wheel slot it
15
+ * ran, so a run that a stalled vCPU delayed tells itself apart from the kernel's own timing. */
16
+ #include "vmlinux.h"
17
+ #include <bpf/bpf_helpers.h>
18
+ #include <bpf/bpf_tracing.h>
19
+ #include <bpf/bpf_core_read.h>
20
+ #include <bpf/bpf_endian.h>
21
+ #include "prober.h"
22
+
23
+ char LICENSE[] SEC("license") = "Dual BSD/GPL";
24
+
25
+ struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 1 << 24); } rb SEC(".maps");
26
+ struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 256); __type(key, __u32); __type(value, __u8); } want SEC(".maps");
27
+ struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 65536); __type(key, __u32); __type(value, __s32); } cfd SEC(".maps");
28
+ struct { __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); __uint(max_entries, 1); __type(key, __u32); __type(value, __u64); } lost SEC(".maps");
29
+
30
+ /* the pid-namespace level Docker Desktop's containers sit at, as measured */
31
+ const volatile __u32 pidns_level = 3;
32
+ const volatile __u8 all_ns = 0;
33
+
34
+ static __always_inline bool ok_ns(__u32 ns) { return all_ns || bpf_map_lookup_elem(&want, &ns); }
35
+
36
+ /* Wake the reader only when 256 KiB are waiting: a wakeup per record cost about 3 us per event
37
+ * (as measured). The reader also drains the ring every 20 ms. */
38
+ static __always_inline long wake(void)
39
+ {
40
+ return bpf_ringbuf_query(&rb, BPF_RB_AVAIL_DATA) >= (1 << 18) ? BPF_RB_FORCE_WAKEUP : BPF_RB_NO_WAKEUP;
41
+ }
42
+
43
+ static __always_inline __s32 docker_pid(void)
44
+ {
45
+ struct task_struct *t = (struct task_struct *)bpf_get_current_task();
46
+ struct pid *p = BPF_CORE_READ(t, group_leader, thread_pid);
47
+ unsigned int lvl = BPF_CORE_READ(p, level);
48
+ struct upid up = {};
49
+ if (lvl < pidns_level)
50
+ return -1;
51
+ bpf_probe_read_kernel(&up, sizeof(up), &p->numbers[pidns_level]);
52
+ return up.nr;
53
+ }
54
+
55
+ static __always_inline struct ev *reserve(void)
56
+ {
57
+ struct ev *e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
58
+ if (!e) {
59
+ __u32 k = 0; __u64 *n = bpf_map_lookup_elem(&lost, &k);
60
+ if (n) (*n)++;
61
+ return 0;
62
+ }
63
+ __builtin_memset(e, 0, sizeof(*e));
64
+ e->mono = bpf_ktime_get_ns();
65
+ e->pid = docker_pid();
66
+ bpf_get_current_comm(e->comm, sizeof(e->comm));
67
+ return e;
68
+ }
69
+
70
+ static __always_inline void fill_sk(struct ev *e, const struct sock *sk)
71
+ {
72
+ e->netns = BPF_CORE_READ(sk, __sk_common.skc_net.net, ns.inum);
73
+ e->nscookie = BPF_CORE_READ(sk, __sk_common.skc_net.net, net_cookie);
74
+ e->cgid = BPF_CORE_READ(sk, sk_cgrp_data.cgroup, kn, id);
75
+ e->saddr = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
76
+ e->daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr);
77
+ e->sport = BPF_CORE_READ(sk, __sk_common.skc_num);
78
+ e->dport = bpf_ntohs(BPF_CORE_READ(sk, __sk_common.skc_dport));
79
+ e->state = BPF_CORE_READ(sk, __sk_common.skc_state);
80
+ e->err = BPF_CORE_READ(sk, sk_err);
81
+ }
82
+
83
+ /* the retransmit timer's state and the sequence space, as the kernel holds them now */
84
+ static __always_inline void fill_tcp(struct ev *e, const struct sock *sk)
85
+ {
86
+ const struct inet_connection_sock *icsk = (const struct inet_connection_sock *)sk;
87
+ const struct tcp_sock *tp = (const struct tcp_sock *)sk;
88
+ e->has_tcp = 1;
89
+ e->tcp.retransmits = BPF_CORE_READ(icsk, icsk_retransmits);
90
+ e->tcp.pending = BPF_CORE_READ(icsk, icsk_pending);
91
+ e->tcp.backoff = BPF_CORE_READ(icsk, icsk_backoff);
92
+ e->tcp.probes_out = BPF_CORE_READ(icsk, icsk_probes_out);
93
+ e->tcp.rto = BPF_CORE_READ(icsk, icsk_rto);
94
+ e->tcp.srtt_us = BPF_CORE_READ(tp, srtt_us) >> 3;
95
+ e->tcp.snd_una = BPF_CORE_READ(tp, snd_una);
96
+ e->tcp.snd_nxt = BPF_CORE_READ(tp, snd_nxt);
97
+ e->tcp.rcv_nxt = BPF_CORE_READ(tp, rcv_nxt);
98
+ }
99
+
100
+ SEC("tp_btf/inet_sock_set_state")
101
+ int BPF_PROG(on_state, const struct sock *sk, const int oldstate, const int newstate)
102
+ {
103
+ if (sk->sk_protocol != IPPROTO_TCP || !ok_ns(sk->__sk_common.skc_net.net->ns.inum))
104
+ return 0;
105
+ struct ev *e = reserve();
106
+ if (!e) return 0;
107
+ e->kind = EV_STATE; e->tai = bpf_ktime_get_tai_ns();
108
+ e->cookie = bpf_get_socket_cookie((void *)sk);
109
+ e->skaddr = (__u64)sk;
110
+ fill_sk(e, sk);
111
+ e->oldst = oldstate; e->newst = newstate;
112
+ if (oldstate == 7 && newstate == 2) { /* connect(): the owner and its fd */
113
+ __u32 tid = (__u32)bpf_get_current_pid_tgid();
114
+ __s32 *fd = bpf_map_lookup_elem(&cfd, &tid);
115
+ e->fd = fd ? *fd : -1; e->owner = 1;
116
+ }
117
+ bpf_ringbuf_submit(e, wake());
118
+ return 0;
119
+ }
120
+
121
+ SEC("tracepoint/syscalls/sys_enter_connect")
122
+ int on_connect_enter(struct trace_event_raw_sys_enter *ctx)
123
+ {
124
+ __u32 tid = (__u32)bpf_get_current_pid_tgid(); __s32 fd = (__s32)ctx->args[0];
125
+ bpf_map_update_elem(&cfd, &tid, &fd, BPF_ANY);
126
+ return 0;
127
+ }
128
+
129
+ SEC("tracepoint/syscalls/sys_exit_connect")
130
+ int on_connect_exit(struct trace_event_raw_sys_exit *ctx)
131
+ {
132
+ __u32 tid = (__u32)bpf_get_current_pid_tgid();
133
+ bpf_map_delete_elem(&cfd, &tid);
134
+ return 0;
135
+ }
136
+
137
+ static __always_inline int accept_exit(long ret)
138
+ {
139
+ if (ret < 0) return 0;
140
+ struct task_struct *t = (struct task_struct *)bpf_get_current_task();
141
+ struct file **fdt = BPF_CORE_READ(t, files, fdt, fd);
142
+ struct file *f = 0;
143
+ bpf_probe_read_kernel(&f, sizeof(f), &fdt[ret]);
144
+ struct socket *so = BPF_CORE_READ(f, private_data);
145
+ struct sock *sk = BPF_CORE_READ(so, sk);
146
+ __u32 ns = BPF_CORE_READ(sk, __sk_common.skc_net.net, ns.inum);
147
+ if (!ok_ns(ns) || BPF_CORE_READ(sk, sk_protocol) != IPPROTO_TCP) return 0;
148
+ struct ev *e = reserve();
149
+ if (!e) return 0;
150
+ e->kind = EV_ACCEPT;
151
+ e->cookie = BPF_CORE_READ(sk, __sk_common.skc_cookie.counter);
152
+ fill_sk(e, sk);
153
+ e->fd = (__s32)ret; e->owner = 1;
154
+ e->ino = BPF_CORE_READ(f, f_inode, i_ino);
155
+ bpf_ringbuf_submit(e, wake());
156
+ return 0;
157
+ }
158
+ SEC("tracepoint/syscalls/sys_exit_accept4")
159
+ int on_accept4_exit(struct trace_event_raw_sys_exit *ctx) { return accept_exit(ctx->ret); }
160
+ SEC("tracepoint/syscalls/sys_exit_accept")
161
+ int on_accept_exit(struct trace_event_raw_sys_exit *ctx) { return accept_exit(ctx->ret); }
162
+
163
+ static __always_inline struct ev *sk_record(const struct sock *sk, __u32 kind, __u32 extra)
164
+ {
165
+ if (!ok_ns(BPF_CORE_READ(sk, __sk_common.skc_net.net, ns.inum)))
166
+ return 0;
167
+ struct ev *e = reserve();
168
+ if (!e) return 0;
169
+ e->kind = kind;
170
+ e->cookie = bpf_get_socket_cookie((void *)sk);
171
+ fill_sk(e, sk);
172
+ e->extra = extra;
173
+ return e;
174
+ }
175
+
176
+ static __always_inline int sk_event(const struct sock *sk, __u32 kind, __u32 extra)
177
+ {
178
+ struct ev *e = sk_record(sk, kind, extra);
179
+ if (e) bpf_ringbuf_submit(e, wake());
180
+ return 0;
181
+ }
182
+
183
+ SEC("tp_btf/tcp_retransmit_skb")
184
+ int BPF_PROG(on_retrans, const struct sock *sk, const struct sk_buff *skb)
185
+ {
186
+ struct ev *e = sk_record(sk, EV_RETRANS, 0);
187
+ if (!e) return 0;
188
+ fill_tcp(e, sk);
189
+ e->has_seg = 1;
190
+ bpf_probe_read_kernel(&e->seg.seq, sizeof(e->seg.seq), skb->cb); /* TCP_SKB_CB(skb)->seq */
191
+ e->seg.len = BPF_CORE_READ(skb, len);
192
+ bpf_ringbuf_submit(e, wake());
193
+ return 0;
194
+ }
195
+
196
+ /* keepalive's probes and the zero-window ones both go out through tcp_write_wakeup. The SNMP
197
+ * counters are an anonymous enum, which CO-RE cannot relocate by name; the image's build takes
198
+ * vmlinux.h from the kernel it runs on, so these constants are that kernel's own. */
199
+ SEC("fentry/tcp_write_wakeup")
200
+ int BPF_PROG(on_probe, struct sock *sk, int mib)
201
+ {
202
+ __u32 what = PROBE_OTHER;
203
+ if (mib == LINUX_MIB_TCPKEEPALIVE) what = PROBE_KEEPALIVE;
204
+ else if (mib == LINUX_MIB_TCPWINPROBE) what = PROBE_WINDOW;
205
+ struct ev *e = sk_record(sk, EV_PROBE, what);
206
+ if (!e) return 0;
207
+ fill_tcp(e, sk);
208
+ bpf_ringbuf_submit(e, wake());
209
+ return 0;
210
+ }
211
+
212
+ /* The timer each CPU's wheel is running now, and the wheel slot it is running it from.
213
+ * call_timer_fn traces timer_expire_entry and then calls the timer's own function, on the same
214
+ * CPU, so a socket's timer function finds its own wheel slot here. */
215
+ struct expiring { __u64 timer, baseclk, now; };
216
+ struct { __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); __uint(max_entries, 1); __type(key, __u32); __type(value, struct expiring); } expiring SEC(".maps");
217
+
218
+ SEC("tp_btf/timer_expire_entry")
219
+ int BPF_PROG(on_timer_expire, struct timer_list *timer, unsigned long baseclk)
220
+ {
221
+ __u32 k = 0;
222
+ struct expiring *x = bpf_map_lookup_elem(&expiring, &k);
223
+ if (x) { x->timer = (__u64)timer; x->baseclk = baseclk; x->now = bpf_jiffies64(); }
224
+ return 0;
225
+ }
226
+
227
+ /* A socket's own timer, run from the wheel. `behind` is how many jiffies after its slot the CPU
228
+ * ran it: the wheel runs a slot in the tick it falls due, so any lag is the CPU's or the VM's, a
229
+ * vCPU the host descheduled, which no timer's rounding explains. `late` is how many after the
230
+ * expiry its owner asked for, the rounding included. Whether the socket's owner held it says
231
+ * whether the keepalive timer will put itself off by HZ/20. The socket is the timer's container,
232
+ * read through CO-RE like any probe read, its cookie as accept's is. */
233
+ static __always_inline int timer_fired(__u64 skaddr, const struct timer_list *t, __u32 what)
234
+ {
235
+ __u32 k = 0;
236
+ struct expiring *x = bpf_map_lookup_elem(&expiring, &k);
237
+ if (!x || x->timer != (__u64)t) return 0;
238
+ const struct sock *sk = (const struct sock *)skaddr;
239
+ if (!ok_ns(BPF_CORE_READ(sk, __sk_common.skc_net.net, ns.inum))) return 0;
240
+ struct ev *e = reserve();
241
+ if (!e) return 0;
242
+ e->kind = EV_TIMER;
243
+ e->cookie = BPF_CORE_READ(sk, __sk_common.skc_cookie.counter);
244
+ fill_sk(e, sk);
245
+ fill_tcp(e, sk);
246
+ /* a socket its owner holds as the timer runs: keepalive tries again HZ/20 later */
247
+ e->extra = what | (BPF_CORE_READ(sk, sk_lock.owned) ? TIMER_OWNED : 0);
248
+ __u64 expires = BPF_CORE_READ(t, expires);
249
+ e->behind = (__u32)(x->now - x->baseclk);
250
+ e->late = x->now > expires ? (__u32)(x->now - expires) : 0;
251
+ bpf_ringbuf_submit(e, wake());
252
+ return 0;
253
+ }
254
+
255
+ SEC("fentry/tcp_keepalive_timer")
256
+ int BPF_PROG(on_keepalive_timer, struct timer_list *t)
257
+ {
258
+ return timer_fired((__u64)t - bpf_core_field_offset(struct sock, sk_timer), t, TIMER_KEEPALIVE);
259
+ }
260
+
261
+ /* the retransmit timer: the RTO's, the tail loss probe's and the zero-window probe's */
262
+ SEC("fentry/tcp_write_timer")
263
+ int BPF_PROG(on_write_timer, struct timer_list *t)
264
+ {
265
+ return timer_fired((__u64)t - bpf_core_field_offset(struct inet_connection_sock, icsk_retransmit_timer),
266
+ t, TIMER_RETRANSMIT);
267
+ }
268
+
269
+ /* every segment that reaches an established socket: tcp_rcv_established's first act */
270
+ SEC("tp_btf/tcp_probe")
271
+ int BPF_PROG(on_rx, struct sock *sk, struct sk_buff *skb)
272
+ {
273
+ struct ev *e = sk_record(sk, EV_RX, 0);
274
+ if (!e) return 0;
275
+ fill_tcp(e, sk);
276
+ struct tcphdr th = {};
277
+ bpf_probe_read_kernel(&th, sizeof(th), BPF_CORE_READ(skb, data));
278
+ __u32 len = BPF_CORE_READ(skb, len);
279
+ __u32 head = (__u32)th.doff * 4;
280
+ e->has_seg = 1;
281
+ e->seg.seq = bpf_ntohl(th.seq);
282
+ e->seg.ack = bpf_ntohl(th.ack_seq);
283
+ e->seg.len = len > head ? len - head : 0;
284
+ e->seg.flags = ((__u32)th.fin) | ((__u32)th.syn << 1) | ((__u32)th.rst << 2) | ((__u32)th.psh << 3) |
285
+ ((__u32)th.ack << 4);
286
+ bpf_ringbuf_submit(e, wake());
287
+ return 0;
288
+ }
289
+
290
+ SEC("tp_btf/tcp_send_reset")
291
+ int BPF_PROG(on_rst_tx, const struct sock *sk, const struct sk_buff *skb) { return sk_event(sk, EV_RST_TX, skb == 0); }
292
+
293
+ SEC("tp_btf/tcp_receive_reset")
294
+ int BPF_PROG(on_rst_rx, struct sock *sk) { return sk_event(sk, EV_RST_RX, 0); }
295
+
296
+ /* tcp_write_err is inlined on the measured kernel, so the error report stands for it */
297
+ SEC("tp_btf/inet_sk_error_report")
298
+ int BPF_PROG(on_error, const struct sock *sk)
299
+ {
300
+ if (sk->sk_protocol != IPPROTO_TCP) return 0;
301
+ struct ev *e = sk_record(sk, EV_ERROR, BPF_CORE_READ((struct inet_connection_sock *)sk, icsk_pending));
302
+ if (!e) return 0;
303
+ fill_tcp(e, sk);
304
+ bpf_ringbuf_submit(e, wake());
305
+ return 0;
306
+ }
307
+
308
+ SEC("tp_btf/tcp_destroy_sock")
309
+ int BPF_PROG(on_destroy, struct sock *sk) { return sk_event(sk, EV_DESTROY, 0); }
310
+
311
+ SEC("fentry/tcp_time_wait")
312
+ int BPF_PROG(on_tw_enter, struct sock *sk, int state, int timeo) { return sk_event(sk, EV_TW_ENTER, state); }
313
+
314
+ SEC("fentry/inet_csk_reqsk_queue_hash_add")
315
+ int BPF_PROG(on_req_new, struct sock *sk, struct request_sock *req, unsigned long timeout)
316
+ {
317
+ if (!ok_ns(BPF_CORE_READ(sk, __sk_common.skc_net.net, ns.inum)))
318
+ return 0;
319
+ struct ev *e = reserve();
320
+ if (!e) return 0;
321
+ e->kind = EV_REQ_NEW;
322
+ e->cookie = bpf_get_socket_cookie((void *)req); /* the child inherits this cookie */
323
+ e->extra = (__u32)bpf_get_socket_cookie((void *)sk); /* the listener (low 32 bits) */
324
+ e->netns = BPF_CORE_READ(sk, __sk_common.skc_net.net, ns.inum);
325
+ e->saddr = BPF_CORE_READ(req, __req_common.skc_rcv_saddr);
326
+ e->daddr = BPF_CORE_READ(req, __req_common.skc_daddr);
327
+ e->sport = BPF_CORE_READ(req, __req_common.skc_num);
328
+ e->dport = bpf_ntohs(BPF_CORE_READ(req, __req_common.skc_dport));
329
+ bpf_ringbuf_submit(e, wake());
330
+ return 0;
331
+ }
332
+
333
+ SEC("fentry/inet_twsk_kill")
334
+ int BPF_PROG(on_tw_kill, struct inet_timewait_sock *tw)
335
+ {
336
+ __u32 ns = BPF_CORE_READ(tw, __tw_common.skc_net.net, ns.inum);
337
+ if (!ok_ns(ns)) return 0;
338
+ struct ev *e = reserve();
339
+ if (!e) return 0;
340
+ e->kind = EV_TW_KILL; e->netns = ns;
341
+ e->cookie = BPF_CORE_READ(tw, __tw_common.skc_cookie.counter);
342
+ e->saddr = BPF_CORE_READ(tw, __tw_common.skc_rcv_saddr);
343
+ e->daddr = BPF_CORE_READ(tw, __tw_common.skc_daddr);
344
+ e->sport = BPF_CORE_READ(tw, __tw_common.skc_num);
345
+ e->dport = bpf_ntohs(BPF_CORE_READ(tw, __tw_common.skc_dport));
346
+ e->extra = BPF_CORE_READ(tw, tw_substate);
347
+ bpf_ringbuf_submit(e, wake());
348
+ return 0;
349
+ }
350
+
351
+ SEC("fentry/tcp_v4_send_reset")
352
+ int BPF_PROG(on_rst_v4, const struct sock *sk, struct sk_buff *skb)
353
+ {
354
+ unsigned char *head = BPF_CORE_READ(skb, head);
355
+ __u16 nh = BPF_CORE_READ(skb, network_header), th = BPF_CORE_READ(skb, transport_header);
356
+ /* the socket's netns when there is one (skb->dev is NULL for an RST answered by a listener) */
357
+ __u32 ns = sk ? BPF_CORE_READ(sk, __sk_common.skc_net.net, ns.inum) : BPF_CORE_READ(skb, dev, nd_net.net, ns.inum);
358
+ if (!ok_ns(ns)) return 0;
359
+ struct iphdr ip = {}; struct tcphdr tcp = {};
360
+ bpf_probe_read_kernel(&ip, sizeof(ip), head + nh);
361
+ bpf_probe_read_kernel(&tcp, sizeof(tcp), head + th);
362
+ struct ev *e = reserve();
363
+ if (!e) return 0;
364
+ e->kind = EV_RST_V4; e->netns = ns;
365
+ e->state = sk ? BPF_CORE_READ(sk, __sk_common.skc_state) : 0;
366
+ e->cookie = sk ? BPF_CORE_READ(sk, __sk_common.skc_cookie.counter) : 0;
367
+ e->saddr = ip.daddr; e->daddr = ip.saddr; /* the RST goes back to the sender */
368
+ e->sport = bpf_ntohs(tcp.dest); e->dport = bpf_ntohs(tcp.source);
369
+ e->extra = ((__u32)tcp.syn) | ((__u32)tcp.ack << 1) | ((__u32)tcp.fin << 2) | ((__u32)tcp.rst << 3);
370
+ bpf_ringbuf_submit(e, wake());
371
+ return 0;
372
+ }
package/bpf/prober.h ADDED
@@ -0,0 +1,59 @@
1
+ /* prober.h - the record the kernel side writes and user space prints */
2
+ #ifndef HARNESS_PROBER_H
3
+ #define HARNESS_PROBER_H
4
+
5
+ enum {
6
+ EV_STATE = 1, /* a TCP state change */
7
+ EV_ACCEPT, /* a connection accepted, with its owner's fd */
8
+ EV_RETRANS, /* a segment retransmitted: the tail loss probe, the RTO timer's, or a fast one */
9
+ EV_RST_TX, /* a reset sent from a socket (no skb: an active reset) */
10
+ EV_RST_RX, /* a reset received */
11
+ EV_ERROR, /* an error reported to the socket's owner: sk_err */
12
+ EV_DESTROY, /* the socket destroyed */
13
+ EV_TW_ENTER, /* into TIME_WAIT */
14
+ EV_TW_KILL, /* a TIME_WAIT socket killed */
15
+ EV_REQ_NEW, /* a request sock born (a SYN accepted onto a listener) */
16
+ EV_RST_V4, /* a reset sent by tcp_v4_send_reset, with or without a socket */
17
+ EV_PROBE, /* a probe sent by tcp_write_wakeup: keepalive or zero window */
18
+ EV_RX, /* a segment received on an established socket (tcp_probe) */
19
+ EV_TIMER, /* a socket's own timer run from the wheel: its keepalive, or its retransmit timer */
20
+ };
21
+
22
+ /* what EV_PROBE's extra says the probe was */
23
+ enum { PROBE_OTHER = 0, PROBE_KEEPALIVE = 1, PROBE_WINDOW = 2 };
24
+
25
+ /* which timer EV_TIMER's extra says ran, in its low byte, and TIMER_OWNED when the socket's owner
26
+ * held the socket as it ran: the keepalive timer then tries again HZ/20 later, sending nothing */
27
+ enum { TIMER_KEEPALIVE = 1, TIMER_RETRANSMIT = 2, TIMER_OWNED = 0x100 };
28
+
29
+ /* the connection's TCP state when the record was taken */
30
+ struct tcp_now {
31
+ __u32 retransmits; /* icsk_retransmits: the RTO timer's retransmissions this episode */
32
+ __u32 pending; /* icsk_pending: which retransmit-timer event is armed */
33
+ __u32 backoff; /* icsk_backoff */
34
+ __u32 probes_out; /* icsk_probes_out */
35
+ __u32 rto; /* icsk_rto, in jiffies */
36
+ __u32 srtt_us; /* the smoothed RTT, in microseconds */
37
+ __u32 snd_una, snd_nxt, rcv_nxt;
38
+ };
39
+
40
+ /* the segment a record is about: retransmitted, or received */
41
+ struct seg {
42
+ __u32 seq, ack, len, flags;
43
+ };
44
+
45
+ struct ev {
46
+ __u64 mono, tai, cookie, nscookie, cgid, skaddr, ino;
47
+ __u32 kind, netns, saddr, daddr, extra;
48
+ __s32 pid, fd, oldst, newst, state, err, owner;
49
+ __u16 sport, dport;
50
+ __u32 has_tcp, has_seg;
51
+ struct tcp_now tcp;
52
+ struct seg seg;
53
+ /* EV_TIMER: how many jiffies behind its wheel slot the CPU ran the timer, which no timer's
54
+ * rounding explains (a stalled vCPU does), and how many after its own expiry */
55
+ __u32 behind, late;
56
+ char comm[16];
57
+ };
58
+
59
+ #endif
@@ -0,0 +1,52 @@
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"];
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
+ };
44
+ readonly seg?: {
45
+ readonly seq: number;
46
+ readonly ack?: number;
47
+ readonly len: Quantity;
48
+ };
49
+ readonly [field: string]: unknown;
50
+ }
51
+ //#endregion
52
+ //# sourceMappingURL=index.d.ts.map
package/build/index.js ADDED
@@ -0,0 +1,22 @@
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
+ ];
19
+ //#endregion
20
+ export { RECORD_KINDS };
21
+
22
+ //# 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] 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 };\n readonly seg?: { readonly seq: number; readonly ack?: number; readonly len: Quantity };\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;AACF"}
Binary file