Skip to content

tcp_info version handling (#5) plus a batch of decode, cli, and platform fixes#8

Open
svczero wants to merge 19 commits into
runZeroInc:mainfrom
svczero:conniver-fixes
Open

tcp_info version handling (#5) plus a batch of decode, cli, and platform fixes#8
svczero wants to merge 19 commits into
runZeroInc:mainfrom
svczero:conniver-fixes

Conversation

@svczero

@svczero svczero commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Rebased on main after 662f6a7: since you already fixed the #5 shadow your way, I dropped my version of it and reworked the couple of tests that leaned on it to your linuxKernelVersion / no-arg API. What is left is the batch of other bugs found while digging into #5, none of which the shadow fix touches. One commit per fix so any can be dropped on its own. Builds/vets/tests green on your main; I ran the riskier ones against a live kernel (notes at the end).

the two bugs the shadow was hiding

Your #5 fix makes version injection actually take effect, which is what makes these testable. Both are still live on main:

  • v4.9 size 148 -> 168: an append-only struct can't drop below v4.6's 160; 148 was notsent_bytes's offset. As the getsockopt optlen, 148 means the kernel writes 148 bytes and min_rtt/data_segs_in/data_segs_out/delivery_rate come back Valid:true, Value:0.
  • gate the v6.7 RTO fields under _6_7, not _6_2: total_rto* came back Valid:true, Value:0 on 6.2-6.6 kernels (_6_7 was set and never read).
  • hermetic version matrix: pins each version, asserts the size table is non-decreasing and ends at sizeof(RawTCPInfo), and checks each field is Valid iff its introduction version <= the pin. Each new test was confirmed to fail against its bug first. It bumps the test pin from 6.2 to 6.7, which the RTO fix requires.

tcpinfo decode

  • last_data_* is kernel milliseconds (jiffies_to_msecs), not microseconds, so LastRxAt/LastTxAt/LastRxAckAt were 1000x too small
  • total_rto_time prom_help said nanoseconds; the value is milliseconds
  • bitfield decode per byte order: wscale/app-limited/fastopen assumed little-endian, so s390x/ppc64/mips got swapped nibbles. I have no big-endian box to run it on, so worth a second look
  • windows: report current RTT, not RTTMin (linux and darwin report current)
  • darwin: snd_wnd (bytes) was going into TxWindowSegs (a segment count); leave it 0
  • darwin: init RxOptions like TxOptions so it marshals [] not null

httpstat / get

  • don't log.Fatalf on one failed address in ConnectDone: it aborts before the dialer tries the rest, so a dual-stack host on a single-stack network dies for a host that is actually reachable
  • reset per-hop stats and wait for the Closed callback, so -L redirects stop printing the previous hop's numbers and fast single requests stop missing them
  • get example: drain the body before Close so the conn can be pooled

pkg/kernel, pkg/os

  • windows RtlGetVersion instead of the manifest-shimmed GetVersion (caps at 6.2.9200, so unmanifested binaries report Win8 on Win10/11)
  • fix the windows VersionInfo doc comments to match the code
  • match the kernel test build tag to the code (vet/test failed to compile on solaris and friends)
  • darwin/freebsd GetOperatingSystem returned the CPU arch (utsname.Machine), not the OS name (Sysname)
  • detect cgroup v2 containers (a v2 0::/ read as host)
  • check scanner.Err in getValueFromOsRelease (an oversized os-release line made it report "Linux" with a nil error)

wrap.go

  • defer finishIO so a panicking wrapped conn doesn't leak inFlight and deadlock Close

Verified live on Ubuntu 26.04 / kernel 7.0: the full suite plus the live-socket decode, the ConnectDone dual-stack abort (dies before, connects after), and the last_data_* fix (LastRxAt 2ms -> 2s after a 2s idle).

svczero added 19 commits July 6, 2026 00:15
the table had v4.9 smaller than v4.6 (160), which an append-only struct can never
be. 148 is the offset of notsent_bytes, a v4.6 field; v4.9 adds tcpi_delivery_rate
ending at 168. this size is the optlen passed to getsockopt, so 148 meant the
kernel wrote 148 bytes and min_rtt, data_segs_in/out and delivery_rate came back a
silent zero with Valid:true.
total_rto, total_rto_recoveries and total_rto_time landed in v6.7 but Unpack
marked them valid from _6_2 on, while _6_7 was set and never read. on a 6.2 to 6.6
kernel those three fields sit past the struct the kernel actually wrote, so they
came back Valid:true, Value:0. move them under _6_7 and give the flag a reader.
the value is the kernel's tcpi_total_rto_time, which is milliseconds (see the
struct comment), but the prometheus help text said nanoseconds, so a dashboard
trusting the help would be off by six orders of magnitude.
TxWindowScale, RxWindowScale and the app-limited/fastopen flags are C bitfields,
whose member order depends on endianness: the first member sits in the low bits
on little-endian and the high bits on big-endian. Unpack assumed little-endian,
so on s390x, ppc64 and mips the window scales came back swapped and the flags
shifted. read them per byte order.
ToInfo() surfaced RTTMin as the cross-platform Info.RTT, so windows reported the
historical minimum where linux and darwin report the most-recent RTT. a link
with 50ms current and 5ms minimum showed 5ms. use s.RTT to match the field's
meaning and the other platforms.
darwin ToInfo put tcpi_snd_wnd, the send window in bytes, into TxWindowSegs,
which is meant to be a congestion window in segments. xnu has no per-segment
cwnd, so leave the field 0 rather than reporting a byte count as segments.
TxWindowBytes already carries the byte value.
TxOptions is set to an empty slice up front while RxOptions is only appended to,
so with no tcp options one marshals to [] and the other to null. give RxOptions
the same empty-slice start.
ConnectDone fires once per address the dialer tries, with that address's error.
calling log.Fatalf there kills the process on the first failure, before the
dialer reaches the remaining addresses, so a dual-stack host on a single-stack
network (or one whose first record is filtered) dies with "unable to connect"
for a host the stdlib would have reached. only record the connect time and print
on success; client.Do() below already reports a genuine total failure.
the final diagnostics read a package-global connConn that setConniver only ever
assigned and never cleared, and read it right after CloseIdleConnections without
waiting for the async Closed callback. a -L redirect chain printed the previous
hop's remote/RTT/byte counts, and a fast single request could print before the
callback populated anything at all (DisableKeepAlives keeps the conn out of the
pool, so CloseIdleConnections cannot force the close). reset the recorded conn at
the start of each visit() and arm a ready channel the callback closes, then wait
on that channel before reading, so a hop only ever prints its own connection.
the example closed the body without reading it, so the connection could not be
pooled and the CloseIdleConnections call that follows had nothing to close,
which is the opposite of what its comment says. copy the body to io.Discard
first.
GetVersion() is compatibility-shimmed to cap at 6.2.9200 for any process without
a supportedOS manifest, which these binaries do not ship, so GetKernelVersion
reported windows 8 on windows 10 and 11 with a nil error. RtlGetVersion is not
capped that way and returns the true major, minor and build.
the kvi/major/minor/build comments described a 6.1.7601.17592 to 6/1/7601/17592
mapping the code never produces: kvi is the BuildLabEx string, and the other
three come from the GetVersion DWORD (major is the low byte, minor the second
byte, build the high word). there is no revision field. describe what it does.
kernel_unix_test.go was tagged !windows && !aix, which includes solaris, illumos,
plan9 and js/wasm, where the package defines no VersionInfo, so go vet and go
test failed to compile there even though go build was fine. give the test the
same positive constraint the code uses.
GetOperatingSystem read utsname.Machine, which is the architecture (arm64,
x86_64), so it never returned "Darwin" or "FreeBSD" and disagreed with its own
doc comment and the linux and windows siblings. Sysname is the OS name field.
the check keyed on the cgroup v1 layout: a v2 container's /proc/1/cgroup is the
single line "0::/", which ends in ":/" and read as host, so IsContainerized
returned false on docker 20.10+, ubuntu 22.04+, fedora, rhel 9 and the rest of
the v2 world. add the v2 signals (/.dockerenv, /run/.containerenv, a docker /
lxc / kubepods / containerd / machine path) and keep the v1 logic.
the scan loop ignored scanner.Err(), so an os-release with a line past bufio's
64KiB token limit, or a mid-read i/o error, stopped the scan early and the
function returned an empty value with a nil error, which GetOperatingSystem then
reported as the hardcoded "Linux". return the error instead.
Read and Write only called finishIO on the normal return path, so if the
underlying conn's Read or Write panicked and was recovered upstream, inFlight
never came back down and Close blocked forever in its drain loop. defer finishIO
right after beginIO in both, so the counter is always released.
the kernel emits tcpi_last_data_sent, tcpi_last_data_recv and tcpi_last_ack_recv
via jiffies_to_msecs(), so they are milliseconds, but Unpack scaled them with the
shared microsecond multiplier like rto/ato/rtt. that made LastTxAt, LastRxAt and
LastRxAckAt come out 1000x too small on every linux host. give those three
time.Millisecond and leave the genuinely-microsecond fields alone. last_ack_sent
is never populated by the kernel so it stays 0 either way.
with the shadow gone, adaptToKernelVersion takes the version as an argument, so
the tests pin one instead of reading the host, and the shipped bitfield tests
move to 6.7.0 so their all-fields-present expectations match a version that now
takes effect. three new tests cover what a single host data point never could:
the size table must be non-decreasing and end at sizeof(RawTCPInfo) (catches the
v4.9=148 shrink), each field is Valid iff its introduction version <= the pin
(catches the 6.6 vs 6.7 RTO mix-up), and a pinned version drives the output no
matter what host runs the test. each was checked to fail against its bug first.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant