@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.
- package/LICENSE +21 -0
- package/README.md +121 -2
- package/bpf/prober.bpf.c +394 -0
- package/bpf/prober.h +68 -0
- package/build/index.d.ts +73 -0
- package/build/index.js +23 -0
- package/build/index.js.map +1 -0
- package/fixtures/records.bin +0 -0
- package/native/Cargo.lock +206 -0
- package/native/Cargo.toml +30 -0
- package/native/src/bin/prober.rs +161 -0
- package/native/src/lib.rs +75 -0
- package/native/src/record.rs +745 -0
- package/package.json +74 -3
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,122 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mark1russell7/prober
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
- Each write of the owner of a socket (`tx`, at the entry of `tcp_sendmsg`), with the first sequence number of the bytes and their length. The time of a proxy is from an arrival on one of its sockets to a write on the other.
|
|
21
|
+
- The state of the retransmit timer at each retransmission.
|
|
22
|
+
- Each time that the timer wheel fires the keepalive timer or the retransmit timer of a socket (`timer`).
|
|
23
|
+
|
|
24
|
+
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`.
|
|
25
|
+
|
|
26
|
+
Each record with TCP state (`tcp`) also has the SACK scoreboard and the reordering state of the socket, by the names of `tcp_info`:
|
|
27
|
+
|
|
28
|
+
- **`sacked`** and **`lost`** are the segments that the peer SACKed and that the kernel marked lost.
|
|
29
|
+
- **`reordering`** is the reordering degree of the kernel, in segments. It starts at `tcp_reordering`, and it increases when the kernel detects reordering. **`reord_seen`** counts those detections.
|
|
30
|
+
- **`dsack_dups`** counts the DSACK blocks that arrived: segments that the peer got two times.
|
|
31
|
+
- **`rcv_ooopack`** counts the segments that arrived out of order.
|
|
32
|
+
|
|
33
|
+
A record of version 0.1 does not have these fields, and it has no `tx` records.
|
|
34
|
+
|
|
35
|
+
A `timer` record has three more fields:
|
|
36
|
+
|
|
37
|
+
- **`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.
|
|
38
|
+
- **`late`** is the number of jiffies after the expiry that the owner of the timer set. It is the rounding of the wheel.
|
|
39
|
+
- **`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.
|
|
40
|
+
|
|
41
|
+
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.
|
|
42
|
+
|
|
43
|
+
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`.
|
|
44
|
+
|
|
45
|
+
## The collector
|
|
46
|
+
|
|
47
|
+
The Rust crate has two parts:
|
|
48
|
+
|
|
49
|
+
- 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.
|
|
50
|
+
- 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.
|
|
51
|
+
|
|
52
|
+
Each second, the collector also prints the clock track and the number of records that the ring buffer dropped.
|
|
53
|
+
|
|
54
|
+
## The contract, written three times
|
|
55
|
+
|
|
56
|
+
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.
|
|
57
|
+
|
|
58
|
+
The line of a record is fixed to the byte:
|
|
59
|
+
|
|
60
|
+
- Its keys are in byte sequence, as serde_json writes each object that it holds.
|
|
61
|
+
- An amount is `{"unit":…,"value":N.0}`.
|
|
62
|
+
- Strings have the escapes of serde_json.
|
|
63
|
+
- A task name that is not UTF-8 decodes as `String::from_utf8_lossy` of Rust decodes it.
|
|
64
|
+
|
|
65
|
+
## Three collectors
|
|
66
|
+
|
|
67
|
+
Three collectors load the same `prober.bpf.o` and print the same lines:
|
|
68
|
+
|
|
69
|
+
- `native/`, Rust on libbpf-rs: the collector that the harness uses
|
|
70
|
+
- `go/`, Go on cilium/ebpf v0.22: pure Go, with no cgo, in one static binary
|
|
71
|
+
- `c/prober.c`, C on libbpf 1.6
|
|
72
|
+
|
|
73
|
+
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.
|
|
74
|
+
|
|
75
|
+
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:
|
|
76
|
+
|
|
77
|
+
- `fixtures/records.bin` has 48 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.
|
|
78
|
+
- 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.
|
|
79
|
+
- The comparison image (`image/Dockerfile`) decodes the fixture with the three collectors. The build stops unless each line is the same, byte for byte.
|
|
80
|
+
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
## The results
|
|
84
|
+
|
|
85
|
+
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.
|
|
86
|
+
|
|
87
|
+
| | Rust | Go | C |
|
|
88
|
+
| ------------------------------------------------- | --------------------------- | --------------------------- | --------------------------- |
|
|
89
|
+
| The decoder and the writer alone, on raw records | 3.34 M a second | 2.49 M a second | 3.46 M a second |
|
|
90
|
+
| The three on one storm: the same records | 39,839 | 39,839, none different | 39,839, none different |
|
|
91
|
+
| 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) |
|
|
92
|
+
| CPU for those ≈ 407,000 records | 240–330 ms | 400–440 ms | 260–290 ms |
|
|
93
|
+
| Peak memory (with the 16 MiB ring) | 34 MB | 85–91 MB | 33 MB |
|
|
94
|
+
| The start: load, verify, attach | 1.0–1.1 s | 1.0–1.2 s | 1.2 s |
|
|
95
|
+
| Binary | 0.8 MB (libbpf inside) | 3.6 MB (static) | 49 KB (and libbpf.so) |
|
|
96
|
+
| A clean release build | 20 s | 8 s | 1 s |
|
|
97
|
+
|
|
98
|
+
- 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.
|
|
99
|
+
- The start is the verifier and the attach of the kernel, which are the same in each language.
|
|
100
|
+
- 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.
|
|
101
|
+
|
|
102
|
+
To start the comparison again, use this command:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
LOOM_PROBER_COMPARE=1 pnpm --filter @mark1russell7/prober exec vitest run src/compare.test.ts
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
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.
|
|
109
|
+
|
|
110
|
+
## Installation
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
pnpm add @mark1russell7/prober zod
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
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`.
|
|
117
|
+
|
|
118
|
+
## Tests
|
|
119
|
+
|
|
120
|
+
- `pnpm --filter @mark1russell7/prober test` compares the contract in C, Rust and TypeScript, and it holds the fixture to its generator.
|
|
121
|
+
- `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.
|
|
122
|
+
- 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.
|
package/bpf/prober.bpf.c
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
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 five 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; with it the SACK scoreboard and the reordering state;
|
|
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
|
+
* - each write of the owner, so a proxy's own time shows between its two sockets. */
|
|
17
|
+
#include "vmlinux.h"
|
|
18
|
+
#include <bpf/bpf_helpers.h>
|
|
19
|
+
#include <bpf/bpf_tracing.h>
|
|
20
|
+
#include <bpf/bpf_core_read.h>
|
|
21
|
+
#include <bpf/bpf_endian.h>
|
|
22
|
+
#include "prober.h"
|
|
23
|
+
|
|
24
|
+
char LICENSE[] SEC("license") = "Dual BSD/GPL";
|
|
25
|
+
|
|
26
|
+
struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 1 << 24); } rb SEC(".maps");
|
|
27
|
+
struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 256); __type(key, __u32); __type(value, __u8); } want SEC(".maps");
|
|
28
|
+
struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 65536); __type(key, __u32); __type(value, __s32); } cfd SEC(".maps");
|
|
29
|
+
struct { __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); __uint(max_entries, 1); __type(key, __u32); __type(value, __u64); } lost SEC(".maps");
|
|
30
|
+
|
|
31
|
+
/* the pid-namespace level Docker Desktop's containers sit at, as measured */
|
|
32
|
+
const volatile __u32 pidns_level = 3;
|
|
33
|
+
const volatile __u8 all_ns = 0;
|
|
34
|
+
|
|
35
|
+
static __always_inline bool ok_ns(__u32 ns) { return all_ns || bpf_map_lookup_elem(&want, &ns); }
|
|
36
|
+
|
|
37
|
+
/* Wake the reader only when 256 KiB are waiting: a wakeup per record cost about 3 us per event
|
|
38
|
+
* (as measured). The reader also drains the ring every 20 ms. */
|
|
39
|
+
static __always_inline long wake(void)
|
|
40
|
+
{
|
|
41
|
+
return bpf_ringbuf_query(&rb, BPF_RB_AVAIL_DATA) >= (1 << 18) ? BPF_RB_FORCE_WAKEUP : BPF_RB_NO_WAKEUP;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static __always_inline __s32 docker_pid(void)
|
|
45
|
+
{
|
|
46
|
+
struct task_struct *t = (struct task_struct *)bpf_get_current_task();
|
|
47
|
+
struct pid *p = BPF_CORE_READ(t, group_leader, thread_pid);
|
|
48
|
+
unsigned int lvl = BPF_CORE_READ(p, level);
|
|
49
|
+
struct upid up = {};
|
|
50
|
+
if (lvl < pidns_level)
|
|
51
|
+
return -1;
|
|
52
|
+
bpf_probe_read_kernel(&up, sizeof(up), &p->numbers[pidns_level]);
|
|
53
|
+
return up.nr;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static __always_inline struct ev *reserve(void)
|
|
57
|
+
{
|
|
58
|
+
struct ev *e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
|
|
59
|
+
if (!e) {
|
|
60
|
+
__u32 k = 0; __u64 *n = bpf_map_lookup_elem(&lost, &k);
|
|
61
|
+
if (n) (*n)++;
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
__builtin_memset(e, 0, sizeof(*e));
|
|
65
|
+
e->mono = bpf_ktime_get_ns();
|
|
66
|
+
e->pid = docker_pid();
|
|
67
|
+
bpf_get_current_comm(e->comm, sizeof(e->comm));
|
|
68
|
+
return e;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static __always_inline void fill_sk(struct ev *e, const struct sock *sk)
|
|
72
|
+
{
|
|
73
|
+
e->netns = BPF_CORE_READ(sk, __sk_common.skc_net.net, ns.inum);
|
|
74
|
+
e->nscookie = BPF_CORE_READ(sk, __sk_common.skc_net.net, net_cookie);
|
|
75
|
+
e->cgid = BPF_CORE_READ(sk, sk_cgrp_data.cgroup, kn, id);
|
|
76
|
+
e->saddr = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
|
|
77
|
+
e->daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr);
|
|
78
|
+
e->sport = BPF_CORE_READ(sk, __sk_common.skc_num);
|
|
79
|
+
e->dport = bpf_ntohs(BPF_CORE_READ(sk, __sk_common.skc_dport));
|
|
80
|
+
e->state = BPF_CORE_READ(sk, __sk_common.skc_state);
|
|
81
|
+
e->err = BPF_CORE_READ(sk, sk_err);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* the retransmit timer's state and the sequence space, as the kernel holds them now */
|
|
85
|
+
static __always_inline void fill_tcp(struct ev *e, const struct sock *sk)
|
|
86
|
+
{
|
|
87
|
+
const struct inet_connection_sock *icsk = (const struct inet_connection_sock *)sk;
|
|
88
|
+
const struct tcp_sock *tp = (const struct tcp_sock *)sk;
|
|
89
|
+
e->has_tcp = 1;
|
|
90
|
+
e->tcp.retransmits = BPF_CORE_READ(icsk, icsk_retransmits);
|
|
91
|
+
e->tcp.pending = BPF_CORE_READ(icsk, icsk_pending);
|
|
92
|
+
e->tcp.backoff = BPF_CORE_READ(icsk, icsk_backoff);
|
|
93
|
+
e->tcp.probes_out = BPF_CORE_READ(icsk, icsk_probes_out);
|
|
94
|
+
e->tcp.rto = BPF_CORE_READ(icsk, icsk_rto);
|
|
95
|
+
e->tcp.srtt_us = BPF_CORE_READ(tp, srtt_us) >> 3;
|
|
96
|
+
e->tcp.snd_una = BPF_CORE_READ(tp, snd_una);
|
|
97
|
+
e->tcp.snd_nxt = BPF_CORE_READ(tp, snd_nxt);
|
|
98
|
+
e->tcp.rcv_nxt = BPF_CORE_READ(tp, rcv_nxt);
|
|
99
|
+
e->tcp.sacked_out = BPF_CORE_READ(tp, sacked_out);
|
|
100
|
+
e->tcp.lost_out = BPF_CORE_READ(tp, lost_out);
|
|
101
|
+
e->tcp.reordering = BPF_CORE_READ(tp, reordering);
|
|
102
|
+
e->tcp.reord_seen = BPF_CORE_READ(tp, reord_seen);
|
|
103
|
+
e->tcp.dsack_dups = BPF_CORE_READ(tp, dsack_dups);
|
|
104
|
+
e->tcp.rcv_ooopack = BPF_CORE_READ(tp, rcv_ooopack);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
SEC("tp_btf/inet_sock_set_state")
|
|
108
|
+
int BPF_PROG(on_state, const struct sock *sk, const int oldstate, const int newstate)
|
|
109
|
+
{
|
|
110
|
+
if (sk->sk_protocol != IPPROTO_TCP || !ok_ns(sk->__sk_common.skc_net.net->ns.inum))
|
|
111
|
+
return 0;
|
|
112
|
+
struct ev *e = reserve();
|
|
113
|
+
if (!e) return 0;
|
|
114
|
+
e->kind = EV_STATE; e->tai = bpf_ktime_get_tai_ns();
|
|
115
|
+
e->cookie = bpf_get_socket_cookie((void *)sk);
|
|
116
|
+
e->skaddr = (__u64)sk;
|
|
117
|
+
fill_sk(e, sk);
|
|
118
|
+
e->oldst = oldstate; e->newst = newstate;
|
|
119
|
+
if (oldstate == 7 && newstate == 2) { /* connect(): the owner and its fd */
|
|
120
|
+
__u32 tid = (__u32)bpf_get_current_pid_tgid();
|
|
121
|
+
__s32 *fd = bpf_map_lookup_elem(&cfd, &tid);
|
|
122
|
+
e->fd = fd ? *fd : -1; e->owner = 1;
|
|
123
|
+
}
|
|
124
|
+
bpf_ringbuf_submit(e, wake());
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
SEC("tracepoint/syscalls/sys_enter_connect")
|
|
129
|
+
int on_connect_enter(struct trace_event_raw_sys_enter *ctx)
|
|
130
|
+
{
|
|
131
|
+
__u32 tid = (__u32)bpf_get_current_pid_tgid(); __s32 fd = (__s32)ctx->args[0];
|
|
132
|
+
bpf_map_update_elem(&cfd, &tid, &fd, BPF_ANY);
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
SEC("tracepoint/syscalls/sys_exit_connect")
|
|
137
|
+
int on_connect_exit(struct trace_event_raw_sys_exit *ctx)
|
|
138
|
+
{
|
|
139
|
+
__u32 tid = (__u32)bpf_get_current_pid_tgid();
|
|
140
|
+
bpf_map_delete_elem(&cfd, &tid);
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static __always_inline int accept_exit(long ret)
|
|
145
|
+
{
|
|
146
|
+
if (ret < 0) return 0;
|
|
147
|
+
struct task_struct *t = (struct task_struct *)bpf_get_current_task();
|
|
148
|
+
struct file **fdt = BPF_CORE_READ(t, files, fdt, fd);
|
|
149
|
+
struct file *f = 0;
|
|
150
|
+
bpf_probe_read_kernel(&f, sizeof(f), &fdt[ret]);
|
|
151
|
+
struct socket *so = BPF_CORE_READ(f, private_data);
|
|
152
|
+
struct sock *sk = BPF_CORE_READ(so, sk);
|
|
153
|
+
__u32 ns = BPF_CORE_READ(sk, __sk_common.skc_net.net, ns.inum);
|
|
154
|
+
if (!ok_ns(ns) || BPF_CORE_READ(sk, sk_protocol) != IPPROTO_TCP) return 0;
|
|
155
|
+
struct ev *e = reserve();
|
|
156
|
+
if (!e) return 0;
|
|
157
|
+
e->kind = EV_ACCEPT;
|
|
158
|
+
e->cookie = BPF_CORE_READ(sk, __sk_common.skc_cookie.counter);
|
|
159
|
+
fill_sk(e, sk);
|
|
160
|
+
e->fd = (__s32)ret; e->owner = 1;
|
|
161
|
+
e->ino = BPF_CORE_READ(f, f_inode, i_ino);
|
|
162
|
+
bpf_ringbuf_submit(e, wake());
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
SEC("tracepoint/syscalls/sys_exit_accept4")
|
|
166
|
+
int on_accept4_exit(struct trace_event_raw_sys_exit *ctx) { return accept_exit(ctx->ret); }
|
|
167
|
+
SEC("tracepoint/syscalls/sys_exit_accept")
|
|
168
|
+
int on_accept_exit(struct trace_event_raw_sys_exit *ctx) { return accept_exit(ctx->ret); }
|
|
169
|
+
|
|
170
|
+
static __always_inline struct ev *sk_record(const struct sock *sk, __u32 kind, __u32 extra)
|
|
171
|
+
{
|
|
172
|
+
if (!ok_ns(BPF_CORE_READ(sk, __sk_common.skc_net.net, ns.inum)))
|
|
173
|
+
return 0;
|
|
174
|
+
struct ev *e = reserve();
|
|
175
|
+
if (!e) return 0;
|
|
176
|
+
e->kind = kind;
|
|
177
|
+
e->cookie = bpf_get_socket_cookie((void *)sk);
|
|
178
|
+
fill_sk(e, sk);
|
|
179
|
+
e->extra = extra;
|
|
180
|
+
return e;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
static __always_inline int sk_event(const struct sock *sk, __u32 kind, __u32 extra)
|
|
184
|
+
{
|
|
185
|
+
struct ev *e = sk_record(sk, kind, extra);
|
|
186
|
+
if (e) bpf_ringbuf_submit(e, wake());
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
SEC("tp_btf/tcp_retransmit_skb")
|
|
191
|
+
int BPF_PROG(on_retrans, const struct sock *sk, const struct sk_buff *skb)
|
|
192
|
+
{
|
|
193
|
+
struct ev *e = sk_record(sk, EV_RETRANS, 0);
|
|
194
|
+
if (!e) return 0;
|
|
195
|
+
fill_tcp(e, sk);
|
|
196
|
+
e->has_seg = 1;
|
|
197
|
+
bpf_probe_read_kernel(&e->seg.seq, sizeof(e->seg.seq), skb->cb); /* TCP_SKB_CB(skb)->seq */
|
|
198
|
+
e->seg.len = BPF_CORE_READ(skb, len);
|
|
199
|
+
bpf_ringbuf_submit(e, wake());
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* keepalive's probes and the zero-window ones both go out through tcp_write_wakeup. The SNMP
|
|
204
|
+
* counters are an anonymous enum, which CO-RE cannot relocate by name; the image's build takes
|
|
205
|
+
* vmlinux.h from the kernel it runs on, so these constants are that kernel's own. */
|
|
206
|
+
SEC("fentry/tcp_write_wakeup")
|
|
207
|
+
int BPF_PROG(on_probe, struct sock *sk, int mib)
|
|
208
|
+
{
|
|
209
|
+
__u32 what = PROBE_OTHER;
|
|
210
|
+
if (mib == LINUX_MIB_TCPKEEPALIVE) what = PROBE_KEEPALIVE;
|
|
211
|
+
else if (mib == LINUX_MIB_TCPWINPROBE) what = PROBE_WINDOW;
|
|
212
|
+
struct ev *e = sk_record(sk, EV_PROBE, what);
|
|
213
|
+
if (!e) return 0;
|
|
214
|
+
fill_tcp(e, sk);
|
|
215
|
+
bpf_ringbuf_submit(e, wake());
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* The timer each CPU's wheel is running now, and the wheel slot it is running it from.
|
|
220
|
+
* call_timer_fn traces timer_expire_entry and then calls the timer's own function, on the same
|
|
221
|
+
* CPU, so a socket's timer function finds its own wheel slot here. */
|
|
222
|
+
struct expiring { __u64 timer, baseclk, now; };
|
|
223
|
+
struct { __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); __uint(max_entries, 1); __type(key, __u32); __type(value, struct expiring); } expiring SEC(".maps");
|
|
224
|
+
|
|
225
|
+
SEC("tp_btf/timer_expire_entry")
|
|
226
|
+
int BPF_PROG(on_timer_expire, struct timer_list *timer, unsigned long baseclk)
|
|
227
|
+
{
|
|
228
|
+
__u32 k = 0;
|
|
229
|
+
struct expiring *x = bpf_map_lookup_elem(&expiring, &k);
|
|
230
|
+
if (x) { x->timer = (__u64)timer; x->baseclk = baseclk; x->now = bpf_jiffies64(); }
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* A socket's own timer, run from the wheel. `behind` is how many jiffies after its slot the CPU
|
|
235
|
+
* 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
|
|
236
|
+
* vCPU the host descheduled, which no timer's rounding explains. `late` is how many after the
|
|
237
|
+
* expiry its owner asked for, the rounding included. Whether the socket's owner held it says
|
|
238
|
+
* whether the keepalive timer will put itself off by HZ/20. The socket is the timer's container,
|
|
239
|
+
* read through CO-RE like any probe read, its cookie as accept's is. */
|
|
240
|
+
static __always_inline int timer_fired(__u64 skaddr, const struct timer_list *t, __u32 what)
|
|
241
|
+
{
|
|
242
|
+
__u32 k = 0;
|
|
243
|
+
struct expiring *x = bpf_map_lookup_elem(&expiring, &k);
|
|
244
|
+
if (!x || x->timer != (__u64)t) return 0;
|
|
245
|
+
const struct sock *sk = (const struct sock *)skaddr;
|
|
246
|
+
if (!ok_ns(BPF_CORE_READ(sk, __sk_common.skc_net.net, ns.inum))) return 0;
|
|
247
|
+
struct ev *e = reserve();
|
|
248
|
+
if (!e) return 0;
|
|
249
|
+
e->kind = EV_TIMER;
|
|
250
|
+
e->cookie = BPF_CORE_READ(sk, __sk_common.skc_cookie.counter);
|
|
251
|
+
fill_sk(e, sk);
|
|
252
|
+
fill_tcp(e, sk);
|
|
253
|
+
/* a socket its owner holds as the timer runs: keepalive tries again HZ/20 later */
|
|
254
|
+
e->extra = what | (BPF_CORE_READ(sk, sk_lock.owned) ? TIMER_OWNED : 0);
|
|
255
|
+
__u64 expires = BPF_CORE_READ(t, expires);
|
|
256
|
+
e->behind = (__u32)(x->now - x->baseclk);
|
|
257
|
+
e->late = x->now > expires ? (__u32)(x->now - expires) : 0;
|
|
258
|
+
bpf_ringbuf_submit(e, wake());
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
SEC("fentry/tcp_keepalive_timer")
|
|
263
|
+
int BPF_PROG(on_keepalive_timer, struct timer_list *t)
|
|
264
|
+
{
|
|
265
|
+
return timer_fired((__u64)t - bpf_core_field_offset(struct sock, sk_timer), t, TIMER_KEEPALIVE);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* the retransmit timer: the RTO's, the tail loss probe's and the zero-window probe's */
|
|
269
|
+
SEC("fentry/tcp_write_timer")
|
|
270
|
+
int BPF_PROG(on_write_timer, struct timer_list *t)
|
|
271
|
+
{
|
|
272
|
+
return timer_fired((__u64)t - bpf_core_field_offset(struct inet_connection_sock, icsk_retransmit_timer),
|
|
273
|
+
t, TIMER_RETRANSMIT);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* every segment that reaches an established socket: tcp_rcv_established's first act */
|
|
277
|
+
SEC("tp_btf/tcp_probe")
|
|
278
|
+
int BPF_PROG(on_rx, struct sock *sk, struct sk_buff *skb)
|
|
279
|
+
{
|
|
280
|
+
struct ev *e = sk_record(sk, EV_RX, 0);
|
|
281
|
+
if (!e) return 0;
|
|
282
|
+
fill_tcp(e, sk);
|
|
283
|
+
struct tcphdr th = {};
|
|
284
|
+
bpf_probe_read_kernel(&th, sizeof(th), BPF_CORE_READ(skb, data));
|
|
285
|
+
__u32 len = BPF_CORE_READ(skb, len);
|
|
286
|
+
__u32 head = (__u32)th.doff * 4;
|
|
287
|
+
e->has_seg = 1;
|
|
288
|
+
e->seg.seq = bpf_ntohl(th.seq);
|
|
289
|
+
e->seg.ack = bpf_ntohl(th.ack_seq);
|
|
290
|
+
e->seg.len = len > head ? len - head : 0;
|
|
291
|
+
e->seg.flags = ((__u32)th.fin) | ((__u32)th.syn << 1) | ((__u32)th.rst << 2) | ((__u32)th.psh << 3) |
|
|
292
|
+
((__u32)th.ack << 4);
|
|
293
|
+
bpf_ringbuf_submit(e, wake());
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* every write the owner makes: the bytes enter the socket here, and with TCP_NODELAY leave it in
|
|
298
|
+
* the same call. A proxy's time is from its arrival on one socket to this on the other. */
|
|
299
|
+
SEC("fentry/tcp_sendmsg")
|
|
300
|
+
int BPF_PROG(on_tx, struct sock *sk, struct msghdr *msg, size_t size)
|
|
301
|
+
{
|
|
302
|
+
struct ev *e = sk_record(sk, EV_TX, 0);
|
|
303
|
+
if (!e) return 0;
|
|
304
|
+
fill_tcp(e, sk);
|
|
305
|
+
e->has_seg = 1;
|
|
306
|
+
e->seg.seq = BPF_CORE_READ((struct tcp_sock *)sk, write_seq);
|
|
307
|
+
e->seg.len = (__u32)size;
|
|
308
|
+
bpf_ringbuf_submit(e, wake());
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
SEC("tp_btf/tcp_send_reset")
|
|
313
|
+
int BPF_PROG(on_rst_tx, const struct sock *sk, const struct sk_buff *skb) { return sk_event(sk, EV_RST_TX, skb == 0); }
|
|
314
|
+
|
|
315
|
+
SEC("tp_btf/tcp_receive_reset")
|
|
316
|
+
int BPF_PROG(on_rst_rx, struct sock *sk) { return sk_event(sk, EV_RST_RX, 0); }
|
|
317
|
+
|
|
318
|
+
/* tcp_write_err is inlined on the measured kernel, so the error report stands for it */
|
|
319
|
+
SEC("tp_btf/inet_sk_error_report")
|
|
320
|
+
int BPF_PROG(on_error, const struct sock *sk)
|
|
321
|
+
{
|
|
322
|
+
if (sk->sk_protocol != IPPROTO_TCP) return 0;
|
|
323
|
+
struct ev *e = sk_record(sk, EV_ERROR, BPF_CORE_READ((struct inet_connection_sock *)sk, icsk_pending));
|
|
324
|
+
if (!e) return 0;
|
|
325
|
+
fill_tcp(e, sk);
|
|
326
|
+
bpf_ringbuf_submit(e, wake());
|
|
327
|
+
return 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
SEC("tp_btf/tcp_destroy_sock")
|
|
331
|
+
int BPF_PROG(on_destroy, struct sock *sk) { return sk_event(sk, EV_DESTROY, 0); }
|
|
332
|
+
|
|
333
|
+
SEC("fentry/tcp_time_wait")
|
|
334
|
+
int BPF_PROG(on_tw_enter, struct sock *sk, int state, int timeo) { return sk_event(sk, EV_TW_ENTER, state); }
|
|
335
|
+
|
|
336
|
+
SEC("fentry/inet_csk_reqsk_queue_hash_add")
|
|
337
|
+
int BPF_PROG(on_req_new, struct sock *sk, struct request_sock *req, unsigned long timeout)
|
|
338
|
+
{
|
|
339
|
+
if (!ok_ns(BPF_CORE_READ(sk, __sk_common.skc_net.net, ns.inum)))
|
|
340
|
+
return 0;
|
|
341
|
+
struct ev *e = reserve();
|
|
342
|
+
if (!e) return 0;
|
|
343
|
+
e->kind = EV_REQ_NEW;
|
|
344
|
+
e->cookie = bpf_get_socket_cookie((void *)req); /* the child inherits this cookie */
|
|
345
|
+
e->extra = (__u32)bpf_get_socket_cookie((void *)sk); /* the listener (low 32 bits) */
|
|
346
|
+
e->netns = BPF_CORE_READ(sk, __sk_common.skc_net.net, ns.inum);
|
|
347
|
+
e->saddr = BPF_CORE_READ(req, __req_common.skc_rcv_saddr);
|
|
348
|
+
e->daddr = BPF_CORE_READ(req, __req_common.skc_daddr);
|
|
349
|
+
e->sport = BPF_CORE_READ(req, __req_common.skc_num);
|
|
350
|
+
e->dport = bpf_ntohs(BPF_CORE_READ(req, __req_common.skc_dport));
|
|
351
|
+
bpf_ringbuf_submit(e, wake());
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
SEC("fentry/inet_twsk_kill")
|
|
356
|
+
int BPF_PROG(on_tw_kill, struct inet_timewait_sock *tw)
|
|
357
|
+
{
|
|
358
|
+
__u32 ns = BPF_CORE_READ(tw, __tw_common.skc_net.net, ns.inum);
|
|
359
|
+
if (!ok_ns(ns)) return 0;
|
|
360
|
+
struct ev *e = reserve();
|
|
361
|
+
if (!e) return 0;
|
|
362
|
+
e->kind = EV_TW_KILL; e->netns = ns;
|
|
363
|
+
e->cookie = BPF_CORE_READ(tw, __tw_common.skc_cookie.counter);
|
|
364
|
+
e->saddr = BPF_CORE_READ(tw, __tw_common.skc_rcv_saddr);
|
|
365
|
+
e->daddr = BPF_CORE_READ(tw, __tw_common.skc_daddr);
|
|
366
|
+
e->sport = BPF_CORE_READ(tw, __tw_common.skc_num);
|
|
367
|
+
e->dport = bpf_ntohs(BPF_CORE_READ(tw, __tw_common.skc_dport));
|
|
368
|
+
e->extra = BPF_CORE_READ(tw, tw_substate);
|
|
369
|
+
bpf_ringbuf_submit(e, wake());
|
|
370
|
+
return 0;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
SEC("fentry/tcp_v4_send_reset")
|
|
374
|
+
int BPF_PROG(on_rst_v4, const struct sock *sk, struct sk_buff *skb)
|
|
375
|
+
{
|
|
376
|
+
unsigned char *head = BPF_CORE_READ(skb, head);
|
|
377
|
+
__u16 nh = BPF_CORE_READ(skb, network_header), th = BPF_CORE_READ(skb, transport_header);
|
|
378
|
+
/* the socket's netns when there is one (skb->dev is NULL for an RST answered by a listener) */
|
|
379
|
+
__u32 ns = sk ? BPF_CORE_READ(sk, __sk_common.skc_net.net, ns.inum) : BPF_CORE_READ(skb, dev, nd_net.net, ns.inum);
|
|
380
|
+
if (!ok_ns(ns)) return 0;
|
|
381
|
+
struct iphdr ip = {}; struct tcphdr tcp = {};
|
|
382
|
+
bpf_probe_read_kernel(&ip, sizeof(ip), head + nh);
|
|
383
|
+
bpf_probe_read_kernel(&tcp, sizeof(tcp), head + th);
|
|
384
|
+
struct ev *e = reserve();
|
|
385
|
+
if (!e) return 0;
|
|
386
|
+
e->kind = EV_RST_V4; e->netns = ns;
|
|
387
|
+
e->state = sk ? BPF_CORE_READ(sk, __sk_common.skc_state) : 0;
|
|
388
|
+
e->cookie = sk ? BPF_CORE_READ(sk, __sk_common.skc_cookie.counter) : 0;
|
|
389
|
+
e->saddr = ip.daddr; e->daddr = ip.saddr; /* the RST goes back to the sender */
|
|
390
|
+
e->sport = bpf_ntohs(tcp.dest); e->dport = bpf_ntohs(tcp.source);
|
|
391
|
+
e->extra = ((__u32)tcp.syn) | ((__u32)tcp.ack << 1) | ((__u32)tcp.fin << 2) | ((__u32)tcp.rst << 3);
|
|
392
|
+
bpf_ringbuf_submit(e, wake());
|
|
393
|
+
return 0;
|
|
394
|
+
}
|
package/bpf/prober.h
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
EV_TX, /* bytes the owner gave the socket to send (tcp_sendmsg): a proxy's departure */
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/* what EV_PROBE's extra says the probe was */
|
|
24
|
+
enum { PROBE_OTHER = 0, PROBE_KEEPALIVE = 1, PROBE_WINDOW = 2 };
|
|
25
|
+
|
|
26
|
+
/* which timer EV_TIMER's extra says ran, in its low byte, and TIMER_OWNED when the socket's owner
|
|
27
|
+
* held the socket as it ran: the keepalive timer then tries again HZ/20 later, sending nothing */
|
|
28
|
+
enum { TIMER_KEEPALIVE = 1, TIMER_RETRANSMIT = 2, TIMER_OWNED = 0x100 };
|
|
29
|
+
|
|
30
|
+
/* the connection's TCP state when the record was taken */
|
|
31
|
+
struct tcp_now {
|
|
32
|
+
__u32 retransmits; /* icsk_retransmits: the RTO timer's retransmissions this episode */
|
|
33
|
+
__u32 pending; /* icsk_pending: which retransmit-timer event is armed */
|
|
34
|
+
__u32 backoff; /* icsk_backoff */
|
|
35
|
+
__u32 probes_out; /* icsk_probes_out */
|
|
36
|
+
__u32 rto; /* icsk_rto, in jiffies */
|
|
37
|
+
__u32 srtt_us; /* the smoothed RTT, in microseconds */
|
|
38
|
+
__u32 snd_una, snd_nxt, rcv_nxt;
|
|
39
|
+
/* the scoreboard and the reordering state: what SACK, DSACK and out-of-order arrivals did */
|
|
40
|
+
__u32 sacked_out; /* the segments the peer SACKed */
|
|
41
|
+
__u32 lost_out; /* the segments marked lost */
|
|
42
|
+
__u32 reordering; /* the reordering degree the kernel holds, in segments: tcp_reordering at first */
|
|
43
|
+
__u32 reord_seen; /* the reorderings the kernel detected */
|
|
44
|
+
__u32 dsack_dups; /* the DSACK blocks received: segments the peer got twice */
|
|
45
|
+
__u32 rcv_ooopack; /* the segments that arrived out of order */
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/* the segment a record is about: retransmitted, or received; for EV_TX, the write: its first
|
|
49
|
+
* sequence number (write_seq) and its length */
|
|
50
|
+
struct seg {
|
|
51
|
+
__u32 seq, ack, len, flags;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
struct ev {
|
|
55
|
+
__u64 mono, tai, cookie, nscookie, cgid, skaddr, ino;
|
|
56
|
+
__u32 kind, netns, saddr, daddr, extra;
|
|
57
|
+
__s32 pid, fd, oldst, newst, state, err, owner;
|
|
58
|
+
__u16 sport, dport;
|
|
59
|
+
__u32 has_tcp, has_seg;
|
|
60
|
+
struct tcp_now tcp;
|
|
61
|
+
struct seg seg;
|
|
62
|
+
/* EV_TIMER: how many jiffies behind its wheel slot the CPU ran the timer, which no timer's
|
|
63
|
+
* rounding explains (a stalled vCPU does), and how many after its own expiry */
|
|
64
|
+
__u32 behind, late;
|
|
65
|
+
char comm[16];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
#endif
|