@mark1russell7/prober 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,11 +17,21 @@ The kernel program records these events:
17
17
  - TIME_WAIT sockets and request sockets.
18
18
  - Each keepalive probe or zero-window probe that `tcp_write_wakeup` sends.
19
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.
20
21
  - The state of the retransmit timer at each retransmission.
21
22
  - Each time that the timer wheel fires the keepalive timer or the retransmit timer of a socket (`timer`).
22
23
 
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`.
24
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
+
25
35
  A `timer` record has three more fields:
26
36
 
27
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.
@@ -64,7 +74,7 @@ The Go and C collectors are in the repository for the comparison. They are not i
64
74
 
65
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:
66
76
 
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.
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.
68
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.
69
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.
70
80
 
package/bpf/prober.bpf.c CHANGED
@@ -5,14 +5,15 @@
5
5
  *
6
6
  * Its core was built and verified first: the state changes, the owner at
7
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:
8
+ * socks, with the socket cookie, the namespace and the cgroup. The harness adds five things:
9
9
  * - each probe tcp_write_wakeup sends, as keepalive or zero-window;
10
10
  * - every segment received on an established socket, so a frozen peer's ACKs show;
11
11
  * - the connection's retransmit-timer state on each retransmission, so the tail loss probe (its
12
12
  * pending event is the loss probe) parts from the RTO timer's retransmissions, which alone
13
- * count toward tcp_retries2;
13
+ * count toward tcp_retries2; with it the SACK scoreboard and the reordering state;
14
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. */
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. */
16
17
  #include "vmlinux.h"
17
18
  #include <bpf/bpf_helpers.h>
18
19
  #include <bpf/bpf_tracing.h>
@@ -95,6 +96,12 @@ static __always_inline void fill_tcp(struct ev *e, const struct sock *sk)
95
96
  e->tcp.snd_una = BPF_CORE_READ(tp, snd_una);
96
97
  e->tcp.snd_nxt = BPF_CORE_READ(tp, snd_nxt);
97
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);
98
105
  }
99
106
 
100
107
  SEC("tp_btf/inet_sock_set_state")
@@ -287,6 +294,21 @@ int BPF_PROG(on_rx, struct sock *sk, struct sk_buff *skb)
287
294
  return 0;
288
295
  }
289
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
+
290
312
  SEC("tp_btf/tcp_send_reset")
291
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); }
292
314
 
package/bpf/prober.h CHANGED
@@ -17,6 +17,7 @@ enum {
17
17
  EV_PROBE, /* a probe sent by tcp_write_wakeup: keepalive or zero window */
18
18
  EV_RX, /* a segment received on an established socket (tcp_probe) */
19
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 */
20
21
  };
21
22
 
22
23
  /* what EV_PROBE's extra says the probe was */
@@ -35,9 +36,17 @@ struct tcp_now {
35
36
  __u32 rto; /* icsk_rto, in jiffies */
36
37
  __u32 srtt_us; /* the smoothed RTT, in microseconds */
37
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 */
38
46
  };
39
47
 
40
- /* the segment a record is about: retransmitted, or received */
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 */
41
50
  struct seg {
42
51
  __u32 seq, ack, len, flags;
43
52
  };
package/build/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Quantity } from "@mark1russell7/quantity";
2
2
  //#region src/index.d.ts
3
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"];
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
5
  /** A record kind is one kind of record that the kernel side writes. */
6
6
  export type RecordKind = (typeof RECORD_KINDS)[number];
7
7
  /** These are the lines of the collector itself: its start, its clock track, its failure, and a record that it cannot read. */
@@ -40,10 +40,31 @@ export interface KernelRecord {
40
40
  readonly probes_out: number;
41
41
  readonly rto: Quantity;
42
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;
43
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
+ */
44
64
  readonly seg?: {
45
65
  readonly seq: number;
46
66
  readonly ack?: number;
67
+ readonly flags?: number;
47
68
  readonly len: Quantity;
48
69
  };
49
70
  readonly [field: string]: unknown;
package/build/index.js CHANGED
@@ -14,7 +14,8 @@ const RECORD_KINDS = [
14
14
  "rst_v4",
15
15
  "probe",
16
16
  "rx",
17
- "timer"
17
+ "timer",
18
+ "tx"
18
19
  ];
19
20
  //#endregion
20
21
  export { RECORD_KINDS };
@@ -1 +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"}
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
@@ -11,14 +11,14 @@ use std::borrow::Cow;
11
11
  use std::net::Ipv4Addr;
12
12
 
13
13
  /// `struct ev`'s size in bytes. In order: seven u64, five u32, seven i32, two u16, two u32,
14
- /// `struct tcp_now` (nine u32), `struct seg` (four u32), two u32 for a timer and a 16-byte comm.
15
- /// Every field sits on its own alignment, so the C compiler adds no padding.
16
- pub const SIZE: usize = 192;
14
+ /// `struct tcp_now` (fifteen u32), `struct seg` (four u32), two u32 for a timer and a 16-byte
15
+ /// comm. Every field sits on its own alignment, so the C compiler adds no padding.
16
+ pub const SIZE: usize = 216;
17
17
 
18
18
  /// The event kinds, in `prober.h`'s order, from 1.
19
- const KINDS: [&str; 14] = [
19
+ const KINDS: [&str; 15] = [
20
20
  "state", "accept", "retrans", "rst_tx", "rst_rx", "error", "destroy", "tw_enter", "tw_kill",
21
- "req_new", "rst_v4", "probe", "rx", "timer",
21
+ "req_new", "rst_v4", "probe", "rx", "timer", "tx",
22
22
  ];
23
23
 
24
24
  /// ICSK_TIME_LOSS_PROBE in include/net/inet_connection_sock.h: the tail loss probe is armed
@@ -39,6 +39,13 @@ pub struct TcpNow {
39
39
  pub snd_una: u32,
40
40
  pub snd_nxt: u32,
41
41
  pub rcv_nxt: u32,
42
+ /// the SACK scoreboard and the reordering state, as `tcp_info` names them
43
+ pub sacked_out: u32,
44
+ pub lost_out: u32,
45
+ pub reordering: u32,
46
+ pub reord_seen: u32,
47
+ pub dsack_dups: u32,
48
+ pub rcv_ooopack: u32,
42
49
  }
43
50
 
44
51
  #[derive(Debug, Clone, PartialEq)]
@@ -138,6 +145,12 @@ impl Ev {
138
145
  snd_una: f.u32(),
139
146
  snd_nxt: f.u32(),
140
147
  rcv_nxt: f.u32(),
148
+ sacked_out: f.u32(),
149
+ lost_out: f.u32(),
150
+ reordering: f.u32(),
151
+ reord_seen: f.u32(),
152
+ dsack_dups: f.u32(),
153
+ rcv_ooopack: f.u32(),
141
154
  };
142
155
  let seg = Seg { seq: f.u32(), ack: f.u32(), len: f.u32(), flags: f.u32() };
143
156
  let (behind, late) = (f.u32(), f.u32());
@@ -206,11 +219,17 @@ impl Ev {
206
219
  #[derive(Serialize)]
207
220
  pub struct TcpOut {
208
221
  pub backoff: u32,
222
+ pub dsack_dups: u32,
223
+ pub lost: u32,
209
224
  pub pending: u32,
210
225
  pub probes_out: u32,
211
226
  pub rcv_nxt: u32,
227
+ pub rcv_ooopack: u32,
228
+ pub reord_seen: u32,
229
+ pub reordering: u32,
212
230
  pub retransmits: u32,
213
231
  pub rto: Quantity,
232
+ pub sacked: u32,
214
233
  pub snd_nxt: u32,
215
234
  pub snd_una: u32,
216
235
  pub srtt: Quantity,
@@ -334,6 +353,12 @@ impl<'a> From<&'a Ev> for Record<'a> {
334
353
  snd_una: t.snd_una,
335
354
  snd_nxt: t.snd_nxt,
336
355
  rcv_nxt: t.rcv_nxt,
356
+ sacked: t.sacked_out,
357
+ lost: t.lost_out,
358
+ reordering: t.reordering,
359
+ reord_seen: t.reord_seen,
360
+ dsack_dups: t.dsack_dups,
361
+ rcv_ooopack: t.rcv_ooopack,
337
362
  }),
338
363
  seg: e.seg.as_ref().map(|s| SegOut {
339
364
  seq: s.seq,
@@ -546,16 +571,28 @@ impl Ev {
546
571
  if let Some(t) = &self.tcp {
547
572
  out.extend_from_slice(b",\"tcp\":{\"backoff\":");
548
573
  push_u64(out, t.backoff.into());
574
+ out.extend_from_slice(b",\"dsack_dups\":");
575
+ push_u64(out, t.dsack_dups.into());
576
+ out.extend_from_slice(b",\"lost\":");
577
+ push_u64(out, t.lost_out.into());
549
578
  out.extend_from_slice(b",\"pending\":");
550
579
  push_u64(out, t.pending.into());
551
580
  out.extend_from_slice(b",\"probes_out\":");
552
581
  push_u64(out, t.probes_out.into());
553
582
  out.extend_from_slice(b",\"rcv_nxt\":");
554
583
  push_u64(out, t.rcv_nxt.into());
584
+ out.extend_from_slice(b",\"rcv_ooopack\":");
585
+ push_u64(out, t.rcv_ooopack.into());
586
+ out.extend_from_slice(b",\"reord_seen\":");
587
+ push_u64(out, t.reord_seen.into());
588
+ out.extend_from_slice(b",\"reordering\":");
589
+ push_u64(out, t.reordering.into());
555
590
  out.extend_from_slice(b",\"retransmits\":");
556
591
  push_u64(out, t.retransmits.into());
557
592
  out.extend_from_slice(b",\"rto\":");
558
593
  push_quantity(out, t.rto, "jiffy");
594
+ out.extend_from_slice(b",\"sacked\":");
595
+ push_u64(out, t.sacked_out.into());
559
596
  out.extend_from_slice(b",\"snd_nxt\":");
560
597
  push_u64(out, t.snd_nxt.into());
561
598
  out.extend_from_slice(b",\"snd_una\":");
@@ -610,7 +647,7 @@ mod tests {
610
647
  b.extend_from_slice(&6379_u16.to_ne_bytes());
611
648
  b.extend_from_slice(&1_u32.to_ne_bytes());
612
649
  b.extend_from_slice(&1_u32.to_ne_bytes());
613
- for v in [retransmits, pending, 1, 0, 102, 80, 10, 16, 20] {
650
+ for v in [retransmits, pending, 1, 0, 102, 80, 10, 16, 20, 2, 1, 3, 0, 1, 4] {
614
651
  b.extend_from_slice(&v.to_ne_bytes());
615
652
  }
616
653
  for v in [10_u32, 0, 6, 0] {
@@ -637,6 +674,18 @@ mod tests {
637
674
  assert_eq!(json["why"], "rto");
638
675
  assert_eq!(json["src"], "172.21.0.3:41978");
639
676
  assert_eq!(json["tcp"]["rto"], serde_json::json!({ "value": 102.0, "unit": "jiffy" }));
677
+ // the SACK scoreboard and the reordering state, by the names of tcp_info
678
+ assert_eq!((&json["tcp"]["sacked"], &json["tcp"]["lost"]), (&serde_json::json!(2), &serde_json::json!(1)));
679
+ assert_eq!((&json["tcp"]["reordering"], &json["tcp"]["rcv_ooopack"]), (&serde_json::json!(3), &serde_json::json!(4)));
680
+ }
681
+
682
+ #[test]
683
+ fn says_what_a_write_gave_the_socket() {
684
+ let ev = Ev::decode(&raw(15, 0, 0)).unwrap();
685
+ assert_eq!(ev.kind_name(), "tx");
686
+ let json = serde_json::to_value(Record::from(&ev)).unwrap();
687
+ // a write has its first sequence number and its length, but no ack and no flags
688
+ assert_eq!(json["seg"], serde_json::json!({ "len": { "value": 6.0, "unit": "B" }, "seq": 10 }));
640
689
  }
641
690
 
642
691
  #[test]
@@ -670,7 +719,7 @@ mod tests {
670
719
 
671
720
  #[test]
672
721
  fn refuses_a_record_of_the_wrong_size() {
673
- assert!(Ev::decode(&[0u8; 12]).unwrap_err().contains("192 bytes"));
722
+ assert!(Ev::decode(&[0u8; 12]).unwrap_err().contains("216 bytes"));
674
723
  }
675
724
 
676
725
  /// every whole record of the identity fixture, which the Go and C collectors are held to
@@ -691,6 +740,6 @@ mod tests {
691
740
  );
692
741
  checked += 1;
693
742
  }
694
- assert_eq!(checked, 46);
743
+ assert_eq!(checked, 48);
695
744
  }
696
745
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/prober",
4
- "version": "0.1.0",
4
+ "version": "0.3.0",
5
5
  "description": "An eBPF prober for TCP: a CO-RE kernel program and its Rust collector, which record what the kernel did to each socket, with the record contract in TypeScript.",
6
6
  "keywords": [
7
7
  "ebpf",
@@ -51,7 +51,7 @@
51
51
  "node": ">=22.19"
52
52
  },
53
53
  "dependencies": {
54
- "@mark1russell7/quantity": "^0.1.0"
54
+ "@mark1russell7/quantity": "^0.2.0"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "zod": "^4.1.0"