The tcpstats kernel module provides per-socket TCP statistics via a character device (/dev/tcp_stats). It includes a filter parser that accepts filter strings like local_port=443 exclude=listen,timewait to control which sockets are reported.
The kmod builds cleanly on FreeBSD 14.3-RELEASE, 14.4-RELEASE, and 15.0-RELEASE using the system cc (clang) and /usr/src/sys kernel headers.
cd kmod/tcpstats
make clean all
# produces tcpstats.koDual-compile parser: compiles as both _KERNEL code inside the kmod and as userspace code for testing. Supports directives:
local_port=443,8443/remote_port=80-- port filtering (up to 8 per direction)exclude=listen,timewait/include_state=established-- TCP state filteringlocal_addr=10.0.0.0/24/remote_addr=fe80::/10-- IPv4/IPv6 CIDR filteringipv4_only/ipv6_only-- address family flagsformat=compact|full-- output format selectionfields=state,rtt,buffers-- field group selection
All string operations use bounded variants (strlcpy, strnlen, snprintf). No strcpy, strcat, or sprintf. Input is validated for length and printable ASCII before parsing.
Fifteen test targets run on the FreeBSD VM, covering correctness, memory safety, performance, kernel module compilation, tooling, and live kernel module integration testing.
Verified on all VMs from a single nix run .#kmod-test-freebsd -- live_all invocation:
| Target | FreeBSD 14.3 | FreeBSD 14.4 | FreeBSD 15.0 | What it tests |
|---|---|---|---|---|
| unit | PASS (78/78) | PASS (78/78) | PASS (78/78) | Functional correctness: positive parsing, error rejection, value verification |
| memcheck | PASS (0 errors) | PASS (0 errors) | PASS (0 errors) | Valgrind memcheck: leak detection, use-after-free, uninitialized reads |
| asan | PASS | PASS | PASS | AddressSanitizer + UBSan: buffer overflows, use-after-free, undefined behavior |
| ubsan | PASS | PASS | PASS | UndefinedBehaviorSanitizer standalone: signed overflow, shift errors, null derefs |
| bench | PASS | PASS | PASS | Performance benchmark: 1M iterations across 11 workloads |
| callgrind | PASS | PASS | PASS | Callgrind CPU profiling: instruction-level hotspot analysis |
| kmod | PASS | PASS | PASS | Kernel module compilation: tcpstats.ko produced with -Werror |
| bench_read | PASS | PASS | PASS | Read-path microbenchmark compilation (7 workloads incl. concurrent readers) |
| gen_conn | PASS | PASS | PASS | Loopback connection generator compilation (up to 500K connections) |
| live_smoke | PASS | PASS | PASS | Kmod lifecycle: load, read /dev/tcpstats, verify sysctl tree, unload |
| live_bench | PASS | PASS | PASS | Read-path benchmark at 1K/10K/100K connections (7 workloads + concurrent readers) |
| live_stats | PASS | PASS | PASS | Sysctl counter invariant: visited == emitted + sum(skipped) |
| live_dtrace | PASS | PASS | PASS | DTrace SDT probes register and fire (7 probes via KDTRACE_HOOKS) |
| live_dos | PASS | PASS | PASS | DoS protections: EMFILE limit, read timeout partial results, EINTR signal |
Measured on FreeBSD 14.3-RELEASE, 1M iterations per workload, compiled with -O2:
| Workload | NS/CALL | CALLS/SEC |
|---|---|---|
| empty | 3.2 | 313M |
| single_port | 54.5 | 18.3M |
| multi_port (8 ports) | 193.9 | 5.2M |
| exclude_states (4 states) | 137.1 | 7.3M |
| ipv4_cidr | 141.2 | 7.1M |
| ipv6_compressed | 146.4 | 6.8M |
| ipv6_full (8 groups) | 135.8 | 7.4M |
| complex_combo | 323.3 | 3.1M |
| worst_case (all features) | 820.5 | 1.2M |
| uppercase_stress | 317.3 | 3.2M |
All workloads complete well under 1 microsecond per call.
Callgrind profiling identified five hotspots that were optimized, yielding a 1.6x-2.5x speedup across all workloads:
| Optimization | Ir saved | Technique |
|---|---|---|
Fix strlcpy shim |
~513M (10.7%) | BSD guard on #ifndef strlcpy so FreeBSD uses native strlcpy instead of snprintf shim |
| Merge printable check + copy | ~300M (6.3%) | Single-pass validation and buffer copy instead of separate loop + strlcpy |
| Inline port/prefix conversion | ~521M (10.9%) | Manual val = val * 10 + (c - '0') replaces strtoul (avoids locale lookups) |
| First-char directive dispatch | ~300M (6.3%) | switch (key[0]) skips irrelevant strcmp calls |
| ASCII-only tolower | ~178M (3.7%) | TSF_TOLOWER macro via c | 0x20 bypasses locale-aware __sbtolower |
Before/after comparison (FreeBSD 15.0-RELEASE):
| Workload | Before (ns) | After (ns) | Speedup |
|---|---|---|---|
| single_port | 162.8 | 64.7 | 2.5x |
| multi_port | 429.1 | 227.1 | 1.9x |
| ipv4_cidr | 351.7 | 159.4 | 2.2x |
| worst_case | 1436.9 | 851.6 | 1.7x |
A nix orchestrator (kmod-test-freebsd) handles the full workflow: rsync source to the VM, install dependencies, compile, and run tests. Works on fresh FreeBSD installs with no manual setup.
# Full suite on default VM (192.168.122.41)
nix run .#kmod-test-freebsd
# Individual target
nix run .#kmod-test-freebsd -- unit
nix run .#kmod-test-freebsd -- bench
nix run .#kmod-test-freebsd -- kmod
# Live integration tests (require root, load/unload kmod)
nix run .#kmod-test-freebsd -- live_all
nix run .#kmod-test-freebsd -- live_smoke
nix run .#kmod-test-freebsd -- live_bench
nix run .#kmod-test-freebsd -- live_dos
# Custom host
FREEBSD_HOST=root@192.168.122.27 nix run .#kmod-test-freebsd
# Direct on VM (after rsync)
ssh root@192.168.122.41 'sh /root/tcpstats-reader/kmod/tcpstats/test/run-tests-freebsd.sh all'| Target | Description |
|---|---|
unit |
Compile + run 78 unit tests |
memcheck |
Valgrind memcheck (leak/error detection) |
asan |
AddressSanitizer + UBSan |
ubsan |
UndefinedBehaviorSanitizer standalone |
bench |
Performance benchmark (1M iterations, 11 workloads) |
callgrind |
Callgrind CPU profiling + annotation |
kmod |
Build kernel module (tcpstats.ko) |
bench_read |
Compile read-path microbenchmark |
gen_conn |
Compile loopback connection generator |
all |
All of the above sequentially |
live_smoke |
Kmod lifecycle: load, read, verify sysctl, unload |
live_bench |
Read-path benchmark at 1K/10K/100K connections |
live_stats |
Sysctl counter invariant validation (-DTCPSTATS_STATS) |
live_dtrace |
DTrace SDT probe registration + firing (-DTCPSTATS_DTRACE), skips if dtrace unavailable |
live_dos |
DoS protection: EMFILE, read timeout, EINTR tests |
live_all |
All live_* targets sequentially (requires root) |
Takes a fresh FreeBSD install to a fully working build + test environment:
- Bootstraps
pkgif not installed - Fetches and installs kernel source tree (
src.txz) if/usr/src/sysis missing - Installs packages:
valgrind,perl5(forcallgrind_annotate)
FreeBSD base system already provides: cc (clang), make, sanitizers, strlcpy, strsep, bsd.kmod.mk.
| Variable | Default | Description |
|---|---|---|
FREEBSD_HOST |
root@192.168.122.41 |
SSH target for the FreeBSD VM (15.0: .41, 14.4: .85, 14.3: .27) |
FREEBSD_KMOD_DIR |
/root/tcpstats-reader/kmod |
Remote directory for kmod source |
CC |
cc |
Compiler (in run-tests-freebsd.sh) |
BENCH_ITERS |
1000000 |
Benchmark iteration count |
The parser tests also run on the Linux nix host (useful for CI):
nix run .#kmod-test-unit # gcc, unit tests
nix run .#kmod-test-asan # gcc, ASan + UBSan
nix run .#kmod-test-ubsan # gcc, UBSan standalone
nix run .#kmod-test-memcheck # gcc + valgrind
nix run .#kmod-test-bench # gcc, benchmark
nix run .#kmod-test-callgrind # gcc + valgrind callgrind
nix run .#kmod-test-all # all of the abovekmod/tcpstats/
Makefile Kernel module build (bsd.kmod.mk)
tcp_statsdev.c Kernel module: char device, ioctl, profiles
tcp_statsdev.h Shared header: ioctl commands, filter struct
tcp_statsdev_filter.c Dual-compile filter string parser
tcp_statsdev_filter.h Parser API: tsf_parse_filter_string()
test/
test_filter_parse.c 78 unit tests (positive + negative + value verification)
bench_filter_parse.c Benchmark harness (11 workloads, configurable iterations)
fuzz_filter_parse.c AFL++ / libFuzzer dual harness
read_tcpstats.c Manual test program for /dev/tcp_stats ioctl
freebsd-pkg-setup.sh Idempotent FreeBSD setup (pkg, kernel src, valgrind)
bench_read_tcpstats.c Read-path microbenchmark (7 workloads, concurrent readers)
gen_connections.c Loopback connection generator (up to 500K connections)
test_dos_limits.c DoS protection validation (EMFILE, timeout, EINTR)
run-tests-freebsd.sh POSIX sh test runner (15 targets incl. 6 live)
Makefile Build read_tcpstats test program
tools/
decode_tcpstats.py Python decoder for /dev/tcp_stats binary output
nix/
kmod-tests.nix Nix targets: kmod-test-{unit,memcheck,asan,ubsan,bench,callgrind,all,freebsd}
All 15 test targets pass on all three versions, including the 6 live kernel module integration tests (live_all).
| Version | Arch | Kernel source | Offline tests | Live tests | Kmod build |
|---|---|---|---|---|---|
| 14.3-RELEASE | amd64 | Fetched via src.txz |
9/9 PASS | 5/5 PASS | PASS |
| 14.4-RELEASE | amd64 | Fetched via src.txz |
9/9 PASS | 5/5 PASS | PASS |
| 15.0-RELEASE | amd64 | Pre-installed | 9/9 PASS | 5/5 PASS | PASS |
| Target | FreeBSD 14.3 | FreeBSD 14.4 | FreeBSD 15.0 | Notes |
|---|---|---|---|---|
live_smoke |
PASS | PASS | PASS | kmod load/read/sysctl/unload |
live_bench |
PASS | PASS | PASS | 1K/10K/100K connections, ~3M rec/s at 10K |
live_stats |
PASS | PASS | PASS | Invariant holds: visited == emitted + skipped |
live_dtrace |
PASS | PASS | PASS | SDT probes register and fire at runtime (7 probes) |
live_dos |
PASS | PASS | PASS | EMFILE, timeout (partial results), EINTR all verified |
DTrace SDT probes initially compiled but did not register at runtime. The root cause was that KDTRACE_HOOKS -- which gates the real SDT macro implementation in sys/sys/sdt.h -- is defined in the GENERIC kernel config but not in DEFAULTS. Out-of-tree bsd.kmod.mk builds generate opt_global.h only from DEFAULTS, so all SDT_PROVIDER_DEFINE/SDT_PROBE_DEFINE* calls silently expanded to no-ops.
The fix adds -DKDTRACE_HOOKS automatically in the Makefile when -DTCPSTATS_DTRACE is detected in CFLAGS, plus a #error guard in tcp_statsdev.c to catch future misconfigurations at compile time. This is safe because GENERIC kernels (the standard FreeBSD kernel) already have KDTRACE_HOOKS enabled; the define only controls whether sdt.h macros emit linker set entries.
Changes made:
| File | Change |
|---|---|
kmod/tcpstats/Makefile |
.if !empty(CFLAGS:M*TCPSTATS_DTRACE*) conditional auto-adds -DKDTRACE_HOOKS |
kmod/tcpstats/tcp_statsdev.c |
#error guard if TCPSTATS_DTRACE set without KDTRACE_HOOKS |
kmod/tcpstats/test/run-tests-freebsd.sh |
live_dtrace now FAILs (not skips) if probes don't register |
Verification (both FreeBSD 14.3 and 15.0):
-
Build with DTrace -- ELF sections present:
readelf -S tcpstats.ko | grep sdt [ 4] set_sdt_tracepoint_set PROGBITS ... 000240 [ 8] set_sdt_providers_set PROGBITS ... 000008 [10] set_sdt_probes_set PROGBITS ... 000038 [12] set_sdt_argtypes_set PROGBITS ... 000060 -
Runtime -- all 7 probes registered under
tcpstatsprovider:dtrace -l -P tcpstats ID PROVIDER MODULE FUNCTION NAME 73670 tcpstats tcpstats read entry 73671 tcpstats tcpstats read done 73672 tcpstats tcpstats filter skip 73673 tcpstats tcpstats filter match 73674 tcpstats tcpstats fill done 73675 tcpstats tcpstats profile create 73676 tcpstats tcpstats profile destroy -
Probes fire under load (
live_dtracetest, 100 connections, 2 reads):read:done 2 read:entry 2 filter:match 858 -
Production build (no
-DTCPSTATS_DTRACE) -- zero SDT sections:readelf -S tcpstats.ko | grep sdt # (empty output)
All 13 items from the performance/security plan are implemented and verified via live_all on both FreeBSD 14.3 and 15.0. Details in ../design/10-performance-security.md (analysis) and ../../archive/perf-security-log.md (implementation log).
| # | Category | Item | Status | Verified by |
|---|---|---|---|---|
| 1 | Critical | Concurrent reader limit (max 32, EBUSY) | DONE | live_bench concurrent reader workload |
| 2 | Critical | Fix destroy_dev() under sx xlock |
DONE | live_smoke load/unload cycles |
| 3 | Critical | Read iteration timeout (default 5s) | DONE | live_dos timeout sub-test |
| 4 | Critical | Signal-interruptible reads (EINTR) | DONE | live_dos EINTR sub-test |
| 5 | High | Cache getsbinuptime() per read() |
DONE | live_bench throughput numbers |
| 6 | High | field_mask gating (9 field groups) | DONE | live_bench field_mask workloads |
| 7 | High | IPv6 address filtering | DONE | live_bench ipv4_only/ipv6_only workloads |
| 8 | Medium | DTrace SDT probes (compile-time) | DONE | live_dtrace (probes register and fire at runtime) |
| 9 | Medium | Sysctl stats counters (two-tier) | DONE | live_stats invariant validation |
| 10 | Medium | Read-path microbenchmark | DONE | live_bench (7 workloads) |
| 11 | Medium | Loopback connection generator | DONE | live_bench (1K/10K/100K scales) |
| 12 | Medium | Max open fds cap (default 16, EMFILE) | DONE | live_dos EMFILE sub-test |
| 13 | Medium | Per-fd rate limiting (default 0/off) | DONE | sysctl tunable available |
| Protection | Default | Sysctl | Behavior |
|---|---|---|---|
| Max open fds | 16 | dev.tcpstats.max_open_fds |
Returns EMFILE when exceeded |
| Max concurrent readers | 32 | dev.tcpstats.max_concurrent_readers |
Returns EBUSY when exceeded |
| Read iteration timeout | 5000ms | dev.tcpstats.max_read_duration_ms |
Returns partial results on timeout |
| Per-fd rate limiting | 0 (off) | dev.tcpstats.min_read_interval_ms |
Returns EBUSY if too frequent |
| Signal-interruptible reads | Always | -- | SIGPENDING checked every 256 sockets, returns EINTR |
| Voluntary preemption | Always | -- | kern_yield(PRI_USER) every 256 sockets |
destroy_dev() is no longer called under sx xlock. Profile deletion is split into tcpstats_profile_detach() (under lock) and tcpstats_profile_destroy_unlocked() (outside lock), preventing deadlock when readers have open fds.
Measured via live_bench with bench_read_tcpstats, no filter, 4MB buffer:
| Scale | FreeBSD 14.3 | FreeBSD 15.0 | Notes |
|---|---|---|---|
| 1K connections | 275.9 us avg, 7.3M rec/s | ~280 us avg, ~7.1M rec/s | L2/L3 hot |
| 10K connections | 4.4 ms avg, 3.0M rec/s | 4.7 ms avg, 2.8M rec/s | L3 hot |
| 100K connections | ~4.2 ms (13K rec buffer limit) | ~4.2 ms (13K rec buffer limit) | 4MB buffer caps at ~13K records |
Concurrent reader scaling (10K connections, 5 reads each):
| Threads | FreeBSD 14.3 wall time | FreeBSD 15.0 wall time |
|---|---|---|
| 1 | 2.1 ms | 2.3 ms |
| 4 | 2.8 ms | 2.5 ms |
| 16 | 9.0 ms | 4.2 ms |
- Cached
getsbinuptime(): called once perread(), passed totcpstats_fill_record()-- eliminates N-1 timecounter reads - field_mask gating: 9 field groups individually gated by
if (field_mask & TSR_FIELDS_*)-- skips pointer chases for unneeded fields - IPv6 address filtering:
tsf_match_v6_prefix()for both local and remote addresses, completing the previously half-built feature
- DTrace SDT probes (
-DTCPSTATS_DTRACE): 7 probe points (read entry/done, filter skip/match, fill done, profile create/destroy) - Sysctl statistics (
-DTCPSTATS_STATS): 13 per-socket hot-loop counters (visited, emitted, 6x skip reasons, timing, errors) - Tier 1 always-on counters:
reads_total,active_fds,opens_total(outside hot loop, negligible cost)
test/bench_read_tcpstats.c-- read-path microbenchmark with 7 workloads including concurrent readerstest/gen_connections.c-- loopback connection generator (up to 500K connections) for populating connection tables
- Fuzz testing on FreeBSD -- run
fuzz_filter_parse.cwith AFL++ or libFuzzer on the VM to find edge cases the unit tests miss Kmod load/unload testing-- DONE:live_smoketargetIoctl integration tests-- DONE:live_benchandlive_statstargets cover full pipelinePerformance & security hardening-- DONE: all 13 plan items implemented and verified on both VMsDTrace SDT probe registration-- DONE: fixed by auto-adding-DKDTRACE_HOOKSin Makefile when-DTCPSTATS_DTRACEis set- Filter parser:
strlcpyshim for Linux -- replace thesnprintf-based shim with a properstrlcpyimplementation for non-BSD platforms (currently only affects Linux CI performance, not correctness)
- FreeBSD platform parser in Rust -- implement
src/platform/freebsd.rsto parsenet.inet.tcp.pcblistsysctl output, matching the macOS parser architecture - kern.file PID join -- FreeBSD pcblist doesn't include PID; join with
kern.filesysctl to attribute sockets to processes - tcpstats enrichment -- use the kmod's per-socket data to supplement sysctl fields (congestion window, RTT, retransmits)
- CI pipeline -- automate
nix run .#kmod-test-freebsdin CI (requires FreeBSD VM runner or bhyve-in-CI setup)
- ARM64 FreeBSD -- test on aarch64 FreeBSD (Raspberry Pi, AWS Graviton)
Kernel module packaging-- DONE:net/tcpstats-kmodandnet/tcpstats-readerFreeBSD ports, tested on all 3 VMs. See port-testing.mdDTrace + stats profiling-- DONE:live_dtraceandlive_statstargetsLive socket filtering benchmarks-- DONE:live_benchtarget (1K/10K/100K connections)