Skip to content

fix: read conntrack over ctnetlink; stop reporting an unreadable table as empty (closes #1)#7

Merged
chicohaager merged 1 commit into
masterfrom
fix/conntrack-ctnetlink
Jul 10, 2026
Merged

fix: read conntrack over ctnetlink; stop reporting an unreadable table as empty (closes #1)#7
chicohaager merged 1 commit into
masterfrom
fix/conntrack-ctnetlink

Conversation

@chicohaager

Copy link
Copy Markdown
Owner

Closes #1.

The Connections tab could never work on ZimaOS

ZFW read the connection table from /proc/net/nf_conntrack, falling back to conntrack(8). A stock ZimaOS kernel is built with:

CONFIG_NF_CONNTRACK=y
# CONFIG_NF_CONNTRACK_PROCFS is not set     <- the file never appears
CONFIG_NF_CT_NETLINK=y                      <- this is what it offers instead

and the image ships no conntrack binary. Both sources were therefore unavailable — on a host that was tracking 877 flows when measured. @benhaube's own diagnosis in the issue was right on both counts (and ZFW never used lsmod, so that half of his hypothesis was moot).

ZFW now reads ctnetlink (NETLINK_NETFILTER / IPCTNL_MSG_CT_GET with NLM_F_DUMP) in pure Go via syscallno new dependency, go.mod still has none. Only CTA_TUPLE_ORIG is decoded, matching the original-direction-only rule of the existing parser. /proc and conntrack(8) stay as fall-throughs for hosts that have them.

Attribute headers are host byte order; payloads (ports, CTA_TIMEOUT) are network order. Each is decoded explicitly, and a test fails if a port is ever read little-endian — that mix-up is the classic ctnetlink bug.

The silent 200 that misdirected the report

The handler did if err != nil { entries = []Entry{} } and answered HTTP 200, so "cannot read the table" was indistinguishable from "no connections". The UI then said the kernel conntrack module was unavailable — on a host where it was running fine. That message is what sent the reporter looking at lsmod and modules.builtin.

Now: an unreadable table is a 503 naming every source that failed, logged at ERROR, and the UI prints that reason. The stale panel hint is gone.

connection table unreadable: ctnetlink: netlink: operation not permitted
/proc/net/nf_conntrack: open /proc/net/nf_conntrack: no such file or directory
conntrack(8): exec: "conntrack": executable file not found in $PATH

Tests

testdata/ctnetlink_dump.bin holds 40 real ctnetlink messages captured from a live ZimaOS 1.6.2 host (kernel 6.18.9). Only the IP addresses were rewritten in place, to documentation ranges — every length, offset and nesting is the kernel's own, so the parser is tested against reality rather than against my idea of the wire format.

Covered: TCP + UDP + IPv6 records, TCP state mapping, big-endian ports, truncation at every byte offset (must not panic), the dump limit across buffers, NLMSG_DONE, and NLMSG_ERROR (EPERM) surfacing as an error rather than an empty list. Each new guard was confirmed to fail against the pre-fix code first.

Live verification (192.168.1.143, ZimaOS 1.6.2)

  • Connections tab lists live flows: SYN_SENT, TIME_WAIT, ESTABLISHED, CLOSE_WAIT, stateless UDP, IPv6 (fe80::…:5353 → ff02::fb), and an unknown L4 protocol rendered as PROTO-2 instead of being dropped.
  • Failure path forced, not assumed: CAP_NET_ADMIN was removed from the daemon via a temporary systemd drop-in. The UI showed the reason, the daemon logged it at ERROR, HTTP was 503. The drop-in was then removed and the daemon restored.

🤖 Generated with Claude Code

…e as empty

Closes #1.

The Connections tab was permanently empty on ZimaOS. ZFW read the connection
table from /proc/net/nf_conntrack and, failing that, from conntrack(8). A stock
ZimaOS kernel is built with

    CONFIG_NF_CONNTRACK=y
    # CONFIG_NF_CONNTRACK_PROCFS is not set
    CONFIG_NF_CT_NETLINK=y

and the image ships no conntrack binary, so both sources were unavailable — on a
host that was tracking 877 flows at the time of measurement. The reporter's own
diagnosis was right: the procfs file never appears, and ctnetlink is what the
kernel offers instead.

ZFW now reads ctnetlink (NETLINK_NETFILTER / IPCTNL_MSG_CT_GET, NLM_F_DUMP) in
pure Go via syscall — no new dependency; go.mod still has none. Only
CTA_TUPLE_ORIG is decoded, matching the original-direction-only rule of the
existing parser. Attribute headers are host byte order, payloads (ports,
CTA_TIMEOUT) are network order; each is decoded explicitly, with a test that
fails if a port is read little-endian. /proc and conntrack(8) remain as
fall-throughs for hosts that have them.

Second, and the reason the reporter was sent chasing a kernel module: the
handler did `if err != nil { entries = []Entry{} }` and answered HTTP 200, so
"cannot read the table" was indistinguishable from "no connections". An
unreadable table is now a 503 naming every source that failed, logged at ERROR,
and the UI prints that reason instead of blaming a module that is running fine.
The stale panel hint ("Empty on hosts where the kernel conntrack module is
unavailable") is gone.

Tests parse 40 real ctnetlink messages captured from a live ZimaOS 1.6.2 host
(kernel 6.18.9). Only the IP addresses in that fixture were rewritten in place,
to documentation ranges — every length, offset and nesting is the kernel's own.
Coverage: TCP + UDP + IPv6, TCP state mapping, big-endian ports, truncation at
every byte offset, the dump limit across buffers, NLMSG_DONE, and NLMSG_ERROR
(EPERM) surfacing as an error rather than an empty list.

Verified on 192.168.1.143 (ZimaOS 1.6.2): the tab now lists live flows, IPv6
included, and an unknown L4 protocol renders as "PROTO-2" instead of being
dropped. The failure path was forced by removing CAP_NET_ADMIN from the daemon
via a temporary systemd drop-in: the UI showed "Could not read the connection
table: ... ctnetlink: netlink: operation not permitted ...", the daemon logged
it at ERROR, and the drop-in was removed afterwards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@chicohaager chicohaager merged commit 31d956f into master Jul 10, 2026
5 checks passed
@chicohaager chicohaager deleted the fix/conntrack-ctnetlink branch July 10, 2026 07:01
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.

Connections Tab Error

1 participant