@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 +21 -0
- package/README.md +111 -2
- package/bpf/prober.bpf.c +372 -0
- package/bpf/prober.h +59 -0
- package/build/index.d.ts +52 -0
- package/build/index.js +22 -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 +696 -0
- package/package.json +74 -3
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
//! The record the kernel side writes (`bpf/prober.h`'s `struct ev`), decoded field by field
|
|
2
|
+
//! without `unsafe`, and the JSON line the harness reads.
|
|
3
|
+
//!
|
|
4
|
+
//! A line's keys are in byte order, as serde_json writes any object it holds as a map. The
|
|
5
|
+
//! structs below declare their fields in that order and serialize straight to the writer: no map
|
|
6
|
+
//! is built per record, and the Go and C collectors write the same bytes (the identity fixture).
|
|
7
|
+
|
|
8
|
+
use crate::{quantity, Quantity};
|
|
9
|
+
use serde::{Serialize, Serializer};
|
|
10
|
+
use std::borrow::Cow;
|
|
11
|
+
use std::net::Ipv4Addr;
|
|
12
|
+
|
|
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;
|
|
17
|
+
|
|
18
|
+
/// The event kinds, in `prober.h`'s order, from 1.
|
|
19
|
+
const KINDS: [&str; 14] = [
|
|
20
|
+
"state", "accept", "retrans", "rst_tx", "rst_rx", "error", "destroy", "tw_enter", "tw_kill",
|
|
21
|
+
"req_new", "rst_v4", "probe", "rx", "timer",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/// ICSK_TIME_LOSS_PROBE in include/net/inet_connection_sock.h: the tail loss probe is armed
|
|
25
|
+
const ICSK_TIME_LOSS_PROBE: u32 = 5;
|
|
26
|
+
|
|
27
|
+
/// `prober.h`'s TIMER_OWNED: a timer record's flag that the socket's owner held the socket
|
|
28
|
+
const TIMER_OWNED: u32 = 0x100;
|
|
29
|
+
|
|
30
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
31
|
+
pub struct TcpNow {
|
|
32
|
+
pub retransmits: u32,
|
|
33
|
+
pub pending: u32,
|
|
34
|
+
pub backoff: u32,
|
|
35
|
+
pub probes_out: u32,
|
|
36
|
+
/// icsk_rto, in jiffies
|
|
37
|
+
pub rto: u32,
|
|
38
|
+
pub srtt_us: u32,
|
|
39
|
+
pub snd_una: u32,
|
|
40
|
+
pub snd_nxt: u32,
|
|
41
|
+
pub rcv_nxt: u32,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
45
|
+
pub struct Seg {
|
|
46
|
+
pub seq: u32,
|
|
47
|
+
pub ack: u32,
|
|
48
|
+
pub len: u32,
|
|
49
|
+
pub flags: u32,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
53
|
+
pub struct Ev {
|
|
54
|
+
pub mono: u64,
|
|
55
|
+
pub tai: u64,
|
|
56
|
+
pub cookie: u64,
|
|
57
|
+
pub nscookie: u64,
|
|
58
|
+
pub cgid: u64,
|
|
59
|
+
pub skaddr: u64,
|
|
60
|
+
pub ino: u64,
|
|
61
|
+
pub kind: u32,
|
|
62
|
+
pub netns: u32,
|
|
63
|
+
pub saddr: Ipv4Addr,
|
|
64
|
+
pub daddr: Ipv4Addr,
|
|
65
|
+
pub extra: u32,
|
|
66
|
+
pub pid: i32,
|
|
67
|
+
pub fd: i32,
|
|
68
|
+
pub oldst: i32,
|
|
69
|
+
pub newst: i32,
|
|
70
|
+
pub state: i32,
|
|
71
|
+
pub err: i32,
|
|
72
|
+
pub owner: i32,
|
|
73
|
+
pub sport: u16,
|
|
74
|
+
pub dport: u16,
|
|
75
|
+
pub tcp: Option<TcpNow>,
|
|
76
|
+
pub seg: Option<Seg>,
|
|
77
|
+
/// a timer: how many jiffies behind its wheel slot the CPU ran it
|
|
78
|
+
pub behind: u32,
|
|
79
|
+
/// and how many after the expiry its owner asked for
|
|
80
|
+
pub late: u32,
|
|
81
|
+
/// the task's name as the kernel holds it: up to 16 bytes, not always UTF-8
|
|
82
|
+
pub comm_raw: [u8; 16],
|
|
83
|
+
pub comm_len: usize,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Reads native-endian fields in order from a record's bytes.
|
|
87
|
+
struct Fields<'a> {
|
|
88
|
+
bytes: &'a [u8],
|
|
89
|
+
at: usize,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
impl<'a> Fields<'a> {
|
|
93
|
+
fn take<const N: usize>(&mut self) -> [u8; N] {
|
|
94
|
+
let mut out = [0u8; N];
|
|
95
|
+
out.copy_from_slice(&self.bytes[self.at..self.at + N]);
|
|
96
|
+
self.at += N;
|
|
97
|
+
out
|
|
98
|
+
}
|
|
99
|
+
fn u64(&mut self) -> u64 {
|
|
100
|
+
u64::from_ne_bytes(self.take())
|
|
101
|
+
}
|
|
102
|
+
fn u32(&mut self) -> u32 {
|
|
103
|
+
u32::from_ne_bytes(self.take())
|
|
104
|
+
}
|
|
105
|
+
fn i32(&mut self) -> i32 {
|
|
106
|
+
i32::from_ne_bytes(self.take())
|
|
107
|
+
}
|
|
108
|
+
fn u16(&mut self) -> u16 {
|
|
109
|
+
u16::from_ne_bytes(self.take())
|
|
110
|
+
}
|
|
111
|
+
/// an IPv4 address the kernel holds in network order
|
|
112
|
+
fn addr(&mut self) -> Ipv4Addr {
|
|
113
|
+
Ipv4Addr::from(self.take::<4>())
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
impl Ev {
|
|
118
|
+
/// Decodes one ring-buffer record, or says why it cannot.
|
|
119
|
+
pub fn decode(bytes: &[u8]) -> Result<Ev, String> {
|
|
120
|
+
if bytes.len() != SIZE {
|
|
121
|
+
return Err(format!("a record is {SIZE} bytes, not {}", bytes.len()));
|
|
122
|
+
}
|
|
123
|
+
let mut f = Fields { bytes, at: 0 };
|
|
124
|
+
let (mono, tai, cookie, nscookie, cgid, skaddr, ino) =
|
|
125
|
+
(f.u64(), f.u64(), f.u64(), f.u64(), f.u64(), f.u64(), f.u64());
|
|
126
|
+
let (kind, netns, saddr, daddr, extra) = (f.u32(), f.u32(), f.addr(), f.addr(), f.u32());
|
|
127
|
+
let (pid, fd, oldst, newst, state, err, owner) =
|
|
128
|
+
(f.i32(), f.i32(), f.i32(), f.i32(), f.i32(), f.i32(), f.i32());
|
|
129
|
+
let (sport, dport) = (f.u16(), f.u16());
|
|
130
|
+
let (has_tcp, has_seg) = (f.u32(), f.u32());
|
|
131
|
+
let tcp = TcpNow {
|
|
132
|
+
retransmits: f.u32(),
|
|
133
|
+
pending: f.u32(),
|
|
134
|
+
backoff: f.u32(),
|
|
135
|
+
probes_out: f.u32(),
|
|
136
|
+
rto: f.u32(),
|
|
137
|
+
srtt_us: f.u32(),
|
|
138
|
+
snd_una: f.u32(),
|
|
139
|
+
snd_nxt: f.u32(),
|
|
140
|
+
rcv_nxt: f.u32(),
|
|
141
|
+
};
|
|
142
|
+
let seg = Seg { seq: f.u32(), ack: f.u32(), len: f.u32(), flags: f.u32() };
|
|
143
|
+
let (behind, late) = (f.u32(), f.u32());
|
|
144
|
+
let comm_raw = f.take::<16>();
|
|
145
|
+
let comm_len = comm_raw.iter().position(|&b| b == 0).unwrap_or(comm_raw.len());
|
|
146
|
+
Ok(Ev {
|
|
147
|
+
mono,
|
|
148
|
+
tai,
|
|
149
|
+
cookie,
|
|
150
|
+
nscookie,
|
|
151
|
+
cgid,
|
|
152
|
+
skaddr,
|
|
153
|
+
ino,
|
|
154
|
+
kind,
|
|
155
|
+
netns,
|
|
156
|
+
saddr,
|
|
157
|
+
daddr,
|
|
158
|
+
extra,
|
|
159
|
+
pid,
|
|
160
|
+
fd,
|
|
161
|
+
oldst,
|
|
162
|
+
newst,
|
|
163
|
+
state,
|
|
164
|
+
err,
|
|
165
|
+
owner,
|
|
166
|
+
sport,
|
|
167
|
+
dport,
|
|
168
|
+
tcp: (has_tcp != 0).then_some(tcp),
|
|
169
|
+
seg: (has_seg != 0).then_some(seg),
|
|
170
|
+
behind,
|
|
171
|
+
late,
|
|
172
|
+
comm_raw,
|
|
173
|
+
comm_len,
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// The task's name, its broken UTF-8 replaced as `String::from_utf8_lossy` replaces it.
|
|
178
|
+
pub fn comm(&self) -> Cow<'_, str> {
|
|
179
|
+
String::from_utf8_lossy(&self.comm_raw[..self.comm_len])
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
pub fn kind_name(&self) -> &'static str {
|
|
183
|
+
(self.kind as usize)
|
|
184
|
+
.checked_sub(1)
|
|
185
|
+
.and_then(|at| KINDS.get(at))
|
|
186
|
+
.copied()
|
|
187
|
+
.unwrap_or("unknown")
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// What a retransmission was: the tail loss probe, the RTO timer's, or one from ACK
|
|
191
|
+
/// processing. Only the RTO timer's count toward tcp_retries2; the first of them starts the
|
|
192
|
+
/// give-up's budget.
|
|
193
|
+
pub fn retransmission(&self) -> Option<&'static str> {
|
|
194
|
+
let tcp = self.tcp.as_ref()?;
|
|
195
|
+
Some(if tcp.pending == ICSK_TIME_LOSS_PROBE {
|
|
196
|
+
"tlp"
|
|
197
|
+
} else if tcp.pending == 0 && tcp.retransmits > 0 {
|
|
198
|
+
"rto"
|
|
199
|
+
} else {
|
|
200
|
+
"fast"
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// The record's TCP state, its keys in byte order.
|
|
206
|
+
#[derive(Serialize)]
|
|
207
|
+
pub struct TcpOut {
|
|
208
|
+
pub backoff: u32,
|
|
209
|
+
pub pending: u32,
|
|
210
|
+
pub probes_out: u32,
|
|
211
|
+
pub rcv_nxt: u32,
|
|
212
|
+
pub retransmits: u32,
|
|
213
|
+
pub rto: Quantity,
|
|
214
|
+
pub snd_nxt: u32,
|
|
215
|
+
pub snd_una: u32,
|
|
216
|
+
pub srtt: Quantity,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// The record's segment, its keys in byte order.
|
|
220
|
+
#[derive(Serialize)]
|
|
221
|
+
pub struct SegOut {
|
|
222
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
223
|
+
pub ack: Option<u32>,
|
|
224
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
225
|
+
pub flags: Option<u32>,
|
|
226
|
+
pub len: Quantity,
|
|
227
|
+
pub seq: u32,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// An address and port, written as `a.b.c.d:port` without building a string first.
|
|
231
|
+
pub struct Endpoint(pub Ipv4Addr, pub u16);
|
|
232
|
+
|
|
233
|
+
impl Serialize for Endpoint {
|
|
234
|
+
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
235
|
+
serializer.collect_str(&format_args!("{}:{}", self.0, self.1))
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// One JSON line, its keys in byte order.
|
|
240
|
+
#[derive(Serialize)]
|
|
241
|
+
pub struct Record<'a> {
|
|
242
|
+
/// a reset sent with no segment to answer: an active reset
|
|
243
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
244
|
+
pub active: Option<bool>,
|
|
245
|
+
/// a timer: how many jiffies behind its wheel slot it ran
|
|
246
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
247
|
+
pub behind: Option<Quantity>,
|
|
248
|
+
pub cg: u64,
|
|
249
|
+
pub comm: Cow<'a, str>,
|
|
250
|
+
pub cookie: u64,
|
|
251
|
+
pub dst: Endpoint,
|
|
252
|
+
pub err: i32,
|
|
253
|
+
pub ev: &'static str,
|
|
254
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
255
|
+
pub fd: Option<i32>,
|
|
256
|
+
/// a timer: how many jiffies after its expiry it ran
|
|
257
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
258
|
+
pub late: Option<Quantity>,
|
|
259
|
+
pub mono: u64,
|
|
260
|
+
pub netns: u32,
|
|
261
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
262
|
+
pub new: Option<i32>,
|
|
263
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
264
|
+
pub old: Option<i32>,
|
|
265
|
+
/// a timer: whether the socket's owner held the socket as it ran
|
|
266
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
267
|
+
pub owned: Option<bool>,
|
|
268
|
+
pub pid: i32,
|
|
269
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
270
|
+
pub seg: Option<SegOut>,
|
|
271
|
+
pub src: Endpoint,
|
|
272
|
+
pub state: i32,
|
|
273
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
274
|
+
pub tai: Option<u64>,
|
|
275
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
276
|
+
pub tcp: Option<TcpOut>,
|
|
277
|
+
/// a probe: keepalive, window or other; a timer: keepalive, retransmit or other
|
|
278
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
279
|
+
pub what: Option<&'static str>,
|
|
280
|
+
/// a retransmission: tlp, rto or fast
|
|
281
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
282
|
+
pub why: Option<&'static str>,
|
|
283
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
284
|
+
pub x: Option<u32>,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
impl<'a> From<&'a Ev> for Record<'a> {
|
|
288
|
+
fn from(e: &'a Ev) -> Record<'a> {
|
|
289
|
+
let name = e.kind_name();
|
|
290
|
+
// a connect or an accept names the task that owns the socket, and its fd
|
|
291
|
+
let has_owner = e.owner != 0;
|
|
292
|
+
let state_change = name == "state";
|
|
293
|
+
let timer = name == "timer";
|
|
294
|
+
Record {
|
|
295
|
+
ev: name,
|
|
296
|
+
mono: e.mono,
|
|
297
|
+
tai: (e.tai != 0).then_some(e.tai),
|
|
298
|
+
cookie: e.cookie,
|
|
299
|
+
netns: e.netns,
|
|
300
|
+
cg: e.cgid,
|
|
301
|
+
pid: e.pid,
|
|
302
|
+
comm: e.comm(),
|
|
303
|
+
src: Endpoint(e.saddr, e.sport),
|
|
304
|
+
dst: Endpoint(e.daddr, e.dport),
|
|
305
|
+
state: e.state,
|
|
306
|
+
err: e.err,
|
|
307
|
+
old: state_change.then_some(e.oldst),
|
|
308
|
+
new: state_change.then_some(e.newst),
|
|
309
|
+
fd: has_owner.then_some(e.fd),
|
|
310
|
+
behind: timer.then(|| quantity(e.behind, "jiffy")),
|
|
311
|
+
late: timer.then(|| quantity(e.late, "jiffy")),
|
|
312
|
+
owned: timer.then_some(e.extra & TIMER_OWNED != 0),
|
|
313
|
+
why: if name == "retrans" { e.retransmission() } else { None },
|
|
314
|
+
// a timer's kind is its extra's low byte, beside the owned flag
|
|
315
|
+
what: match (name, if timer { e.extra & 0xff } else { e.extra }) {
|
|
316
|
+
("probe" | "timer", 1) => Some("keepalive"),
|
|
317
|
+
("probe", 2) => Some("window"),
|
|
318
|
+
("timer", 2) => Some("retransmit"),
|
|
319
|
+
("probe" | "timer", _) => Some("other"),
|
|
320
|
+
_ => None,
|
|
321
|
+
},
|
|
322
|
+
active: (name == "rst_tx").then_some(e.extra == 1),
|
|
323
|
+
x: match name {
|
|
324
|
+
"error" | "tw_enter" | "tw_kill" | "req_new" | "rst_v4" => Some(e.extra),
|
|
325
|
+
_ => None,
|
|
326
|
+
},
|
|
327
|
+
tcp: e.tcp.as_ref().map(|t| TcpOut {
|
|
328
|
+
retransmits: t.retransmits,
|
|
329
|
+
pending: t.pending,
|
|
330
|
+
backoff: t.backoff,
|
|
331
|
+
probes_out: t.probes_out,
|
|
332
|
+
rto: quantity(t.rto, "jiffy"),
|
|
333
|
+
srtt: quantity(t.srtt_us, "us"),
|
|
334
|
+
snd_una: t.snd_una,
|
|
335
|
+
snd_nxt: t.snd_nxt,
|
|
336
|
+
rcv_nxt: t.rcv_nxt,
|
|
337
|
+
}),
|
|
338
|
+
seg: e.seg.as_ref().map(|s| SegOut {
|
|
339
|
+
seq: s.seq,
|
|
340
|
+
ack: (name == "rx").then_some(s.ack),
|
|
341
|
+
len: quantity(s.len, "B"),
|
|
342
|
+
flags: (name == "rx").then_some(s.flags),
|
|
343
|
+
}),
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ---------------------------------------------------------------------------------------------
|
|
349
|
+
// the line by hand: what `Record` serializes, written straight into a buffer
|
|
350
|
+
// ---------------------------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
fn push_u64(out: &mut Vec<u8>, mut v: u64) {
|
|
353
|
+
let mut digits = [0u8; 20];
|
|
354
|
+
let mut at = digits.len();
|
|
355
|
+
loop {
|
|
356
|
+
at -= 1;
|
|
357
|
+
digits[at] = b'0' + (v % 10) as u8;
|
|
358
|
+
v /= 10;
|
|
359
|
+
if v == 0 {
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
out.extend_from_slice(&digits[at..]);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
fn push_i64(out: &mut Vec<u8>, v: i64) {
|
|
367
|
+
if v < 0 {
|
|
368
|
+
out.push(b'-');
|
|
369
|
+
}
|
|
370
|
+
push_u64(out, v.unsigned_abs());
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/// One byte of a JSON string as serde_json writes it: \" \\ \b \f \n \r \t, other control
|
|
374
|
+
/// characters as \u00xx in lowercase hex, everything else as it is.
|
|
375
|
+
fn push_escaped(out: &mut Vec<u8>, c: u8) {
|
|
376
|
+
const HEX: &[u8; 16] = b"0123456789abcdef";
|
|
377
|
+
match c {
|
|
378
|
+
b'"' => out.extend_from_slice(b"\\\""),
|
|
379
|
+
b'\\' => out.extend_from_slice(b"\\\\"),
|
|
380
|
+
0x08 => out.extend_from_slice(b"\\b"),
|
|
381
|
+
0x0c => out.extend_from_slice(b"\\f"),
|
|
382
|
+
b'\n' => out.extend_from_slice(b"\\n"),
|
|
383
|
+
b'\r' => out.extend_from_slice(b"\\r"),
|
|
384
|
+
b'\t' => out.extend_from_slice(b"\\t"),
|
|
385
|
+
c if c < 0x20 => {
|
|
386
|
+
out.extend_from_slice(b"\\u00");
|
|
387
|
+
out.push(HEX[(c >> 4) as usize]);
|
|
388
|
+
out.push(HEX[(c & 0xf) as usize]);
|
|
389
|
+
}
|
|
390
|
+
c => out.push(c),
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/// Raw bytes as a JSON string, decoded as `String::from_utf8_lossy` decodes them: each maximal
|
|
395
|
+
/// prefix of a would-be UTF-8 sequence that cannot complete becomes one U+FFFD, and decoding
|
|
396
|
+
/// resumes at the byte that broke it. The Go and C collectors run this same loop.
|
|
397
|
+
fn push_lossy_quoted(out: &mut Vec<u8>, b: &[u8]) {
|
|
398
|
+
const REPLACEMENT: &[u8] = "\u{FFFD}".as_bytes();
|
|
399
|
+
out.push(b'"');
|
|
400
|
+
let mut i = 0;
|
|
401
|
+
while i < b.len() {
|
|
402
|
+
let c = b[i];
|
|
403
|
+
if c < 0x80 {
|
|
404
|
+
push_escaped(out, c);
|
|
405
|
+
i += 1;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
let (need, lo, hi) = match c {
|
|
409
|
+
0xc2..=0xdf => (1, 0x80, 0xbf),
|
|
410
|
+
0xe0 => (2, 0xa0, 0xbf),
|
|
411
|
+
0xed => (2, 0x80, 0x9f),
|
|
412
|
+
0xe1..=0xef => (2, 0x80, 0xbf),
|
|
413
|
+
0xf0 => (3, 0x90, 0xbf),
|
|
414
|
+
0xf4 => (3, 0x80, 0x8f),
|
|
415
|
+
0xf1..=0xf3 => (3, 0x80, 0xbf),
|
|
416
|
+
_ => {
|
|
417
|
+
out.extend_from_slice(REPLACEMENT);
|
|
418
|
+
i += 1;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
let mut j = i + 1;
|
|
423
|
+
let mut whole = true;
|
|
424
|
+
for k in 0..need {
|
|
425
|
+
let (first, last) = if k == 0 { (lo, hi) } else { (0x80, 0xbf) };
|
|
426
|
+
match b.get(j) {
|
|
427
|
+
Some(&next) if (first..=last).contains(&next) => j += 1,
|
|
428
|
+
_ => {
|
|
429
|
+
whole = false;
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if whole {
|
|
435
|
+
out.extend_from_slice(&b[i..j]);
|
|
436
|
+
} else {
|
|
437
|
+
out.extend_from_slice(REPLACEMENT);
|
|
438
|
+
}
|
|
439
|
+
i = j;
|
|
440
|
+
}
|
|
441
|
+
out.push(b'"');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/// `{"unit":"…","value":N.0}`: every amount a record carries is a whole u32, which serde writes
|
|
445
|
+
/// as a float
|
|
446
|
+
fn push_quantity(out: &mut Vec<u8>, value: u32, unit: &str) {
|
|
447
|
+
out.extend_from_slice(b"{\"unit\":\"");
|
|
448
|
+
out.extend_from_slice(unit.as_bytes());
|
|
449
|
+
out.extend_from_slice(b"\",\"value\":");
|
|
450
|
+
push_u64(out, value.into());
|
|
451
|
+
out.extend_from_slice(b".0}");
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/// `"a.b.c.d:port"`
|
|
455
|
+
fn push_endpoint(out: &mut Vec<u8>, addr: Ipv4Addr, port: u16) {
|
|
456
|
+
out.push(b'"');
|
|
457
|
+
for (k, octet) in addr.octets().into_iter().enumerate() {
|
|
458
|
+
if k > 0 {
|
|
459
|
+
out.push(b'.');
|
|
460
|
+
}
|
|
461
|
+
push_u64(out, octet.into());
|
|
462
|
+
}
|
|
463
|
+
out.push(b':');
|
|
464
|
+
push_u64(out, port.into());
|
|
465
|
+
out.push(b'"');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
impl Ev {
|
|
469
|
+
/// Appends the record's JSON line, the bytes `Record` serializes to, written by hand: its keys
|
|
470
|
+
/// in byte order, straight into the buffer, building nothing per record. The Go and C
|
|
471
|
+
/// collectors write it the same way, and the tests hold it to `Record`'s output.
|
|
472
|
+
pub fn append_line(&self, out: &mut Vec<u8>) {
|
|
473
|
+
let name = self.kind_name();
|
|
474
|
+
out.push(b'{');
|
|
475
|
+
if name == "rst_tx" {
|
|
476
|
+
out.extend_from_slice(if self.extra == 1 { b"\"active\":true," } else { b"\"active\":false," });
|
|
477
|
+
}
|
|
478
|
+
if name == "timer" {
|
|
479
|
+
out.extend_from_slice(b"\"behind\":");
|
|
480
|
+
push_quantity(out, self.behind, "jiffy");
|
|
481
|
+
out.push(b',');
|
|
482
|
+
}
|
|
483
|
+
out.extend_from_slice(b"\"cg\":");
|
|
484
|
+
push_u64(out, self.cgid);
|
|
485
|
+
out.extend_from_slice(b",\"comm\":");
|
|
486
|
+
push_lossy_quoted(out, &self.comm_raw[..self.comm_len]);
|
|
487
|
+
out.extend_from_slice(b",\"cookie\":");
|
|
488
|
+
push_u64(out, self.cookie);
|
|
489
|
+
out.extend_from_slice(b",\"dst\":");
|
|
490
|
+
push_endpoint(out, self.daddr, self.dport);
|
|
491
|
+
out.extend_from_slice(b",\"err\":");
|
|
492
|
+
push_i64(out, self.err.into());
|
|
493
|
+
out.extend_from_slice(b",\"ev\":\"");
|
|
494
|
+
out.extend_from_slice(name.as_bytes());
|
|
495
|
+
out.push(b'"');
|
|
496
|
+
if self.owner != 0 {
|
|
497
|
+
out.extend_from_slice(b",\"fd\":");
|
|
498
|
+
push_i64(out, self.fd.into());
|
|
499
|
+
}
|
|
500
|
+
if name == "timer" {
|
|
501
|
+
out.extend_from_slice(b",\"late\":");
|
|
502
|
+
push_quantity(out, self.late, "jiffy");
|
|
503
|
+
}
|
|
504
|
+
out.extend_from_slice(b",\"mono\":");
|
|
505
|
+
push_u64(out, self.mono);
|
|
506
|
+
out.extend_from_slice(b",\"netns\":");
|
|
507
|
+
push_u64(out, self.netns.into());
|
|
508
|
+
if name == "state" {
|
|
509
|
+
out.extend_from_slice(b",\"new\":");
|
|
510
|
+
push_i64(out, self.newst.into());
|
|
511
|
+
out.extend_from_slice(b",\"old\":");
|
|
512
|
+
push_i64(out, self.oldst.into());
|
|
513
|
+
}
|
|
514
|
+
if name == "timer" {
|
|
515
|
+
out.extend_from_slice(if self.extra & TIMER_OWNED != 0 {
|
|
516
|
+
b",\"owned\":true".as_slice()
|
|
517
|
+
} else {
|
|
518
|
+
b",\"owned\":false"
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
out.extend_from_slice(b",\"pid\":");
|
|
522
|
+
push_i64(out, self.pid.into());
|
|
523
|
+
if let Some(s) = &self.seg {
|
|
524
|
+
out.extend_from_slice(b",\"seg\":{");
|
|
525
|
+
if name == "rx" {
|
|
526
|
+
out.extend_from_slice(b"\"ack\":");
|
|
527
|
+
push_u64(out, s.ack.into());
|
|
528
|
+
out.extend_from_slice(b",\"flags\":");
|
|
529
|
+
push_u64(out, s.flags.into());
|
|
530
|
+
out.push(b',');
|
|
531
|
+
}
|
|
532
|
+
out.extend_from_slice(b"\"len\":");
|
|
533
|
+
push_quantity(out, s.len, "B");
|
|
534
|
+
out.extend_from_slice(b",\"seq\":");
|
|
535
|
+
push_u64(out, s.seq.into());
|
|
536
|
+
out.push(b'}');
|
|
537
|
+
}
|
|
538
|
+
out.extend_from_slice(b",\"src\":");
|
|
539
|
+
push_endpoint(out, self.saddr, self.sport);
|
|
540
|
+
out.extend_from_slice(b",\"state\":");
|
|
541
|
+
push_i64(out, self.state.into());
|
|
542
|
+
if self.tai != 0 {
|
|
543
|
+
out.extend_from_slice(b",\"tai\":");
|
|
544
|
+
push_u64(out, self.tai);
|
|
545
|
+
}
|
|
546
|
+
if let Some(t) = &self.tcp {
|
|
547
|
+
out.extend_from_slice(b",\"tcp\":{\"backoff\":");
|
|
548
|
+
push_u64(out, t.backoff.into());
|
|
549
|
+
out.extend_from_slice(b",\"pending\":");
|
|
550
|
+
push_u64(out, t.pending.into());
|
|
551
|
+
out.extend_from_slice(b",\"probes_out\":");
|
|
552
|
+
push_u64(out, t.probes_out.into());
|
|
553
|
+
out.extend_from_slice(b",\"rcv_nxt\":");
|
|
554
|
+
push_u64(out, t.rcv_nxt.into());
|
|
555
|
+
out.extend_from_slice(b",\"retransmits\":");
|
|
556
|
+
push_u64(out, t.retransmits.into());
|
|
557
|
+
out.extend_from_slice(b",\"rto\":");
|
|
558
|
+
push_quantity(out, t.rto, "jiffy");
|
|
559
|
+
out.extend_from_slice(b",\"snd_nxt\":");
|
|
560
|
+
push_u64(out, t.snd_nxt.into());
|
|
561
|
+
out.extend_from_slice(b",\"snd_una\":");
|
|
562
|
+
push_u64(out, t.snd_una.into());
|
|
563
|
+
out.extend_from_slice(b",\"srtt\":");
|
|
564
|
+
push_quantity(out, t.srtt_us, "us");
|
|
565
|
+
out.push(b'}');
|
|
566
|
+
}
|
|
567
|
+
if name == "probe" || name == "timer" {
|
|
568
|
+
let kind = if name == "timer" { self.extra & 0xff } else { self.extra };
|
|
569
|
+
out.extend_from_slice(match (name, kind) {
|
|
570
|
+
(_, 1) => b",\"what\":\"keepalive\"".as_slice(),
|
|
571
|
+
("probe", 2) => b",\"what\":\"window\"",
|
|
572
|
+
(_, 2) => b",\"what\":\"retransmit\"",
|
|
573
|
+
_ => b",\"what\":\"other\"",
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
if name == "retrans" {
|
|
577
|
+
if let Some(why) = self.retransmission() {
|
|
578
|
+
out.extend_from_slice(b",\"why\":\"");
|
|
579
|
+
out.extend_from_slice(why.as_bytes());
|
|
580
|
+
out.push(b'"');
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if matches!(name, "error" | "tw_enter" | "tw_kill" | "req_new" | "rst_v4") {
|
|
584
|
+
out.extend_from_slice(b",\"x\":");
|
|
585
|
+
push_u64(out, self.extra.into());
|
|
586
|
+
}
|
|
587
|
+
out.push(b'}');
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
#[cfg(test)]
|
|
592
|
+
mod tests {
|
|
593
|
+
use super::*;
|
|
594
|
+
|
|
595
|
+
/// a record as the kernel side lays it out, built field by field in `struct ev`'s order
|
|
596
|
+
fn raw(kind: u32, retransmits: u32, pending: u32) -> Vec<u8> {
|
|
597
|
+
let mut b = Vec::with_capacity(SIZE);
|
|
598
|
+
for v in [1_000_u64, 0, 42, 7, 9, 0xffff, 0] {
|
|
599
|
+
b.extend_from_slice(&v.to_ne_bytes());
|
|
600
|
+
}
|
|
601
|
+
b.extend_from_slice(&kind.to_ne_bytes());
|
|
602
|
+
b.extend_from_slice(&4026532000_u32.to_ne_bytes());
|
|
603
|
+
b.extend_from_slice(&[172, 21, 0, 3]);
|
|
604
|
+
b.extend_from_slice(&[172, 21, 0, 2]);
|
|
605
|
+
b.extend_from_slice(&0_u32.to_ne_bytes());
|
|
606
|
+
for v in [100_i32, -1, 0, 0, 1, 0, 0] {
|
|
607
|
+
b.extend_from_slice(&v.to_ne_bytes());
|
|
608
|
+
}
|
|
609
|
+
b.extend_from_slice(&41978_u16.to_ne_bytes());
|
|
610
|
+
b.extend_from_slice(&6379_u16.to_ne_bytes());
|
|
611
|
+
b.extend_from_slice(&1_u32.to_ne_bytes());
|
|
612
|
+
b.extend_from_slice(&1_u32.to_ne_bytes());
|
|
613
|
+
for v in [retransmits, pending, 1, 0, 102, 80, 10, 16, 20] {
|
|
614
|
+
b.extend_from_slice(&v.to_ne_bytes());
|
|
615
|
+
}
|
|
616
|
+
for v in [10_u32, 0, 6, 0] {
|
|
617
|
+
b.extend_from_slice(&v.to_ne_bytes());
|
|
618
|
+
}
|
|
619
|
+
for v in [2_u32, 10] {
|
|
620
|
+
b.extend_from_slice(&v.to_ne_bytes());
|
|
621
|
+
}
|
|
622
|
+
let mut comm = [0u8; 16];
|
|
623
|
+
comm[..6].copy_from_slice(b"client");
|
|
624
|
+
b.extend_from_slice(&comm);
|
|
625
|
+
b
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
#[test]
|
|
629
|
+
fn decodes_a_record_and_names_the_rto_timers_retransmission() {
|
|
630
|
+
let ev = Ev::decode(&raw(3, 1, 0)).unwrap();
|
|
631
|
+
assert_eq!(ev.kind_name(), "retrans");
|
|
632
|
+
assert_eq!(ev.saddr, Ipv4Addr::new(172, 21, 0, 3));
|
|
633
|
+
assert_eq!((ev.sport, ev.dport), (41978, 6379));
|
|
634
|
+
assert_eq!(ev.comm(), "client");
|
|
635
|
+
assert_eq!(ev.retransmission(), Some("rto"));
|
|
636
|
+
let json = serde_json::to_value(Record::from(&ev)).unwrap();
|
|
637
|
+
assert_eq!(json["why"], "rto");
|
|
638
|
+
assert_eq!(json["src"], "172.21.0.3:41978");
|
|
639
|
+
assert_eq!(json["tcp"]["rto"], serde_json::json!({ "value": 102.0, "unit": "jiffy" }));
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
#[test]
|
|
643
|
+
fn tells_the_tail_loss_probe_from_the_rto_timer() {
|
|
644
|
+
// the loss probe is armed (pending 5) and no RTO retransmission has happened
|
|
645
|
+
assert_eq!(Ev::decode(&raw(3, 0, ICSK_TIME_LOSS_PROBE)).unwrap().retransmission(), Some("tlp"));
|
|
646
|
+
// a retransmission in ACK processing, with the RTO timer armed (pending 1)
|
|
647
|
+
assert_eq!(Ev::decode(&raw(3, 0, 1)).unwrap().retransmission(), Some("fast"));
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
#[test]
|
|
651
|
+
fn says_how_far_behind_its_wheel_slot_a_sockets_timer_ran() {
|
|
652
|
+
// extra, after seven u64, the kind, the namespace and two addresses: the retransmit timer
|
|
653
|
+
let mut bytes = raw(14, 0, 0);
|
|
654
|
+
bytes[72..76].copy_from_slice(&2_u32.to_ne_bytes());
|
|
655
|
+
let ev = Ev::decode(&bytes).unwrap();
|
|
656
|
+
assert_eq!(ev.kind_name(), "timer");
|
|
657
|
+
let json = serde_json::to_value(Record::from(&ev)).unwrap();
|
|
658
|
+
assert_eq!(json["what"], "retransmit");
|
|
659
|
+
assert_eq!(json["behind"], serde_json::json!({ "value": 2.0, "unit": "jiffy" }));
|
|
660
|
+
assert_eq!(json["late"], serde_json::json!({ "value": 10.0, "unit": "jiffy" }));
|
|
661
|
+
assert_eq!(json["owned"], false);
|
|
662
|
+
// the same timer, finding its socket in its owner's hands
|
|
663
|
+
bytes[72..76].copy_from_slice(&(1_u32 | TIMER_OWNED).to_ne_bytes());
|
|
664
|
+
let held = serde_json::to_value(Record::from(&Ev::decode(&bytes).unwrap())).unwrap();
|
|
665
|
+
assert_eq!((&held["what"], &held["owned"]), (&serde_json::json!("keepalive"), &serde_json::json!(true)));
|
|
666
|
+
// no other kind says either
|
|
667
|
+
let other = serde_json::to_value(Record::from(&Ev::decode(&raw(3, 1, 0)).unwrap())).unwrap();
|
|
668
|
+
assert!(other.get("behind").is_none() && other.get("late").is_none() && other.get("owned").is_none());
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
#[test]
|
|
672
|
+
fn refuses_a_record_of_the_wrong_size() {
|
|
673
|
+
assert!(Ev::decode(&[0u8; 12]).unwrap_err().contains("192 bytes"));
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/// every whole record of the identity fixture, which the Go and C collectors are held to
|
|
677
|
+
const FIXTURE: &[u8] = include_bytes!("../../fixtures/records.bin");
|
|
678
|
+
|
|
679
|
+
#[test]
|
|
680
|
+
fn writes_by_hand_exactly_what_serde_and_from_utf8_lossy_write() {
|
|
681
|
+
let mut checked = 0;
|
|
682
|
+
for chunk in FIXTURE.chunks_exact(SIZE) {
|
|
683
|
+
let ev = Ev::decode(chunk).unwrap();
|
|
684
|
+
let mut by_hand = Vec::new();
|
|
685
|
+
ev.append_line(&mut by_hand);
|
|
686
|
+
let by_serde = serde_json::to_vec(&Record::from(&ev)).unwrap();
|
|
687
|
+
assert_eq!(
|
|
688
|
+
String::from_utf8_lossy(&by_hand),
|
|
689
|
+
String::from_utf8_lossy(&by_serde),
|
|
690
|
+
"record {checked}"
|
|
691
|
+
);
|
|
692
|
+
checked += 1;
|
|
693
|
+
}
|
|
694
|
+
assert_eq!(checked, 46);
|
|
695
|
+
}
|
|
696
|
+
}
|