Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

# ZFW — a host firewall for ZimaOS

> **Current release:** v1.0.18 — follow-up to v1.0.17. `revert` (and therefore the Safe-Apply dead-man) never emptied the **IPv6** `DOCKER-USER` chain, which v1.0.17 started filling — so a "reverted" firewall kept silently dropping IPv6 traffic to published container ports while every status surface reported it was off. `revert` now restores that chain's stock `-j RETURN`, and `zfw status` shows it. The engine script also carried the same name-based backend guess fixed elsewhere in v1.0.17 (`case "$IPT" in *nft*)`), so on ZimaOS 1.6.2 its `revert`/`status` operated on the wrong IPv6 table. Finally, the UI, README and engine logs now say plainly that the dead-man **removes the firewall entirely** rather than restoring the previous rules.
> **Current release:** v1.0.19 — the Connections tab finally works on ZimaOS ([#1](https://github.com/chicohaager/zfw/issues/1)). ZFW read the connection table from `/proc/net/nf_conntrack` and, failing that, from `conntrack(8)`. A stock ZimaOS kernel builds with `# CONFIG_NF_CONNTRACK_PROCFS is not set` and the image ships no `conntrack` binary, so both sources were unavailable and the tab was permanently empty — on a host that was tracking hundreds of flows. ZFW now reads **ctnetlink** (`CONFIG_NF_CT_NETLINK=y`), the kernel's netlink interface, in pure Go with no new dependency. Second, `/api/conntrack` no longer answers `200 []` when the table cannot be read: an unreadable table is a `503` naming the source that failed, and the UI shows that reason instead of wrongly blaming a missing kernel module.
>
> **Previous release:** v1.0.18 — follow-up to v1.0.17. `revert` (and therefore the Safe-Apply dead-man) never emptied the **IPv6** `DOCKER-USER` chain, which v1.0.17 started filling — so a "reverted" firewall kept silently dropping IPv6 traffic to published container ports while every status surface reported it was off. `revert` now restores that chain's stock `-j RETURN`, and `zfw status` shows it. The engine script also carried the same name-based backend guess fixed elsewhere in v1.0.17 (`case "$IPT" in *nft*)`), so on ZimaOS 1.6.2 its `revert`/`status` operated on the wrong IPv6 table. Finally, the UI, README and engine logs now say plainly that the dead-man **removes the firewall entirely** rather than restoring the previous rules.
>
> **Previous release:** v1.0.17 — two ZimaOS 1.6.2 fixes. **(1)** The iptables backend is now identified by asking the binary (`iptables -V` → `nf_tables`/`legacy`) instead of pattern-matching its name. On 1.6.2 the plain `iptables` symlink drives nf_tables despite having no `nft` in its name, so whenever the Docker-FORWARD probe missed — e.g. `zfwd` starting before `dockerd` — IPv6 was pinned to the empty legacy table and the dashboard reported "IPv6 protection ✗" while `ZFW-IN6` was live. **(2)** The published-port inventory that scopes the `DOCKER-USER` default-deny now unions docker-proxy sockets with `docker ps`. With `"userland-proxy": false` there is no docker-proxy, the inventory came up empty, and the per-port default-deny was silently not emitted at all — the firewall failed open behind a green tile. **(3)** The inventory is now protocol-aware: `parseDockerPorts` used to discard UDP mappings outright, so a container publishing e.g. `8181/udp` got neither an allow rule nor a deny rule and stayed reachable from any source while its TCP sibling was filtered. Published UDP ports now get both. Also new: the IPv6 `DOCKER-USER` chain is populated (it matters the moment Docker IPv6 is enabled), and an empty inventory under `default_policy=deny` is now logged as an error instead of passing silently.

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.18
1.0.19
40 changes: 33 additions & 7 deletions internal/conntrack/conntrack.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Package conntrack reads the kernel's live connection-tracking table
// and exposes it as structured Entry records for the UI's Connections
// tab. Source-of-truth is /proc/net/nf_conntrack (always available when
// the kernel module is loaded — which it is on every ZimaOS host
// because ZFW already depends on conntrack matches). The conntrack(8)
// userland tool is tried as a fall-through only.
// tab. Source-of-truth is ctnetlink (the kernel's netlink interface);
// /proc/net/nf_conntrack and the conntrack(8) userland tool are tried as
// fall-throughs for hosts that expose them. See Read for why the order
// matters on ZimaOS.
//
// Read caps the result at a configurable limit so a host with tens of
// thousands of connections does not balloon the API response.
Expand All @@ -12,6 +12,8 @@ package conntrack
import (
"bufio"
"context"
"errors"
"fmt"
"os"
"os/exec"
"strconv"
Expand All @@ -35,15 +37,39 @@ type Entry struct {

// Read returns the live conntrack table. limit caps the result; pass
// 0 for "no cap" (use sparingly — a busy host can have 100k+ entries).
// Errors only on I/O failure; an empty table is (nil, nil).
// An empty table is (nil, nil); an unreadable one is always an error.
//
// Three sources are tried in order of availability, most universal first:
//
// 1. ctnetlink — the kernel's netlink interface. The only one a stock
// ZimaOS kernel offers (CONFIG_NF_CONNTRACK_PROCFS is off and
// conntrack(8) is not in the image), which is why the Connections tab
// was permanently empty before v1.0.19. Needs CAP_NET_ADMIN.
// 2. /proc/net/nf_conntrack — present on distro kernels that enable PROCFS.
// 3. conntrack(8) — the userland tool, if someone installed it.
//
// When every source fails the errors are joined and returned. Callers must
// not turn that into an empty list: "no connections" and "cannot read the
// connection table" are different facts, and conflating them is what made
// the UI blame a kernel module that was, in fact, tracking 877 flows.
func Read(ctx context.Context, limit int) ([]Entry, error) {
var errs []error
if entries, err := readNetlink(ctx, limit); err == nil {
return entries, nil
} else {
errs = append(errs, fmt.Errorf("ctnetlink: %w", err))
}
if entries, err := readProc(ctx, limit); err == nil {
return entries, nil
} else if entries, err := readCmd(ctx, limit); err == nil {
} else {
errs = append(errs, fmt.Errorf("/proc/net/nf_conntrack: %w", err))
}
if entries, err := readCmd(ctx, limit); err == nil {
return entries, nil
} else {
return nil, err
errs = append(errs, fmt.Errorf("conntrack(8): %w", err))
}
return nil, errors.Join(errs...)
}

// readProc parses /proc/net/nf_conntrack. The file's line format is
Expand Down
Loading
Loading