Sub-issue of #36. Size: Small–Medium.
What
Capture timestamps currently come from a userspace time.time() call after recv returns (src/rootwire/capture.py), so they include scheduler and interpreter latency between the packet arriving and Python reading it. Switch to kernel timestamps: enable SO_TIMESTAMPNS on the socket and read the SCM_TIMESTAMPNS ancillary message that the kernel attaches to each datagram.
How
setsockopt(SOL_SOCKET, SO_TIMESTAMPNS, 1) on the raw socket.
- Replace
sock.recv(BUFFER_SIZE) with sock.recvmsg(BUFFER_SIZE, socket.CMSG_SPACE(16)) and parse the returned ancdata. SCM_TIMESTAMPNS carries a struct timespec (tv_sec, tv_nsec as two native longs) — struct.unpack it.
- Fall back to
time.time() when no timestamp cmsg is present (some interfaces/paths don't provide one), so a missing cmsg never crashes the loop.
Precision decision to make
DecodedFrame.timestamp is a float seconds-since-epoch, and the pcap writer already documents that float µs at 2026 epoch values is lossy (tests/test_pcap.py comments on exactly this). SO_TIMESTAMPNS gives nanoseconds. Decide whether to:
- keep the
float field and accept the precision loss (smallest change), or
- carry an integer ns timestamp end-to-end (touches
frame.py, the pcap record writer, and NDJSON) for lossless capture.
I lean toward the second for a tool aiming at forensic-grade captures, but it widens the change — worth settling before implementation.
Testability (follow the #57 seam)
The cmsg-parsing logic is a pure function — extract it (_parse_timestamp(ancdata) -> float) and unit-test it with synthetic ancdata tuples, no socket required. The recvmsg wiring is exercised the same way #57 tests recv: monkeypatch the module-global socket with a fake whose recvmsg returns canned (data, ancdata, flags, addr). A real SCM_TIMESTAMPNS round trip needs a live socket and belongs in the privilege-gated integration test.
Sub-issue of #36. Size: Small–Medium.
What
Capture timestamps currently come from a userspace
time.time()call afterrecvreturns (src/rootwire/capture.py), so they include scheduler and interpreter latency between the packet arriving and Python reading it. Switch to kernel timestamps: enableSO_TIMESTAMPNSon the socket and read theSCM_TIMESTAMPNSancillary message that the kernel attaches to each datagram.How
setsockopt(SOL_SOCKET, SO_TIMESTAMPNS, 1)on the raw socket.sock.recv(BUFFER_SIZE)withsock.recvmsg(BUFFER_SIZE, socket.CMSG_SPACE(16))and parse the returnedancdata.SCM_TIMESTAMPNScarries astruct timespec(tv_sec,tv_nsecas two native longs) —struct.unpackit.time.time()when no timestamp cmsg is present (some interfaces/paths don't provide one), so a missing cmsg never crashes the loop.Precision decision to make
DecodedFrame.timestampis afloatseconds-since-epoch, and the pcap writer already documents that float µs at 2026 epoch values is lossy (tests/test_pcap.pycomments on exactly this).SO_TIMESTAMPNSgives nanoseconds. Decide whether to:floatfield and accept the precision loss (smallest change), orframe.py, the pcap record writer, and NDJSON) for lossless capture.I lean toward the second for a tool aiming at forensic-grade captures, but it widens the change — worth settling before implementation.
Testability (follow the #57 seam)
The cmsg-parsing logic is a pure function — extract it (
_parse_timestamp(ancdata) -> float) and unit-test it with syntheticancdatatuples, no socket required. Therecvmsgwiring is exercised the same way #57 testsrecv: monkeypatch the module-globalsocketwith a fake whoserecvmsgreturns canned(data, ancdata, flags, addr). A realSCM_TIMESTAMPNSround trip needs a live socket and belongs in the privilege-gated integration test.