go-firewall/livecounters_linux.go
2026-08-10 17:17:03 -05:00

192 lines
6.3 KiB
Go

package firewall
import (
"context"
"strings"
)
// Live packet/byte counters for the backends that store their rules in files but
// enforce them through iptables — iptables itself, ufw, csf and apf. The files
// carry no counts, so the counts have to come from the running ruleset, and the
// rows there are whatever the product generates for a rule rather than the rule
// itself. Each backend supplies the decode step that turns one of its rows back
// into the rule it stands for — for iptables that is the identity, for the others
// it undoes the product's own framing; the reading, the run handling and the
// matching below are the same for all four.
//
// Counters are informational (Capabilities().RuleCounters) and never part of
// rule identity, so everything here is best-effort: a missing save binary, a row
// this model cannot hold, or a rule the running ruleset does not carry simply
// leaves that rule's counters zero.
// liveRow is one counter-annotated `iptables-save -c` rule line, split into the
// parts a backend needs to decide what rule it stands for.
type liveRow struct {
// chain is the chain the line appends to.
chain string
// fields is the whole line split on whitespace, counter token first. It is
// for inspection only: a quoted log prefix does not survive the split, so
// rewrites operate on line.
fields []string
// line is the line verbatim, counter token included.
line string
// newChain reports whether this row opens a chain, so a decoder holding
// state across adjacent rows knows to drop it.
newChain bool
}
// liveSaveLines reads the live filter table for a family with counters attached.
// A missing or failing save binary yields no lines rather than an error, since a
// backend that cannot read counters still reports its rules.
func liveSaveLines(ctx context.Context, fam Family) []string {
cmd := "iptables-save"
if fam == IPv6 {
cmd = "ip6tables-save"
}
out, err := runCommand(ctx, cmd, "-c", "-t", "filter")
if err != nil {
return nil
}
return out
}
// jumpTarget returns the target of a rulespec's `-j`/`--jump` option, or the
// empty string when the line carries none.
func jumpTarget(fields []string) string {
for i, tok := range fields {
if tok == "-j" || tok == "--jump" {
if i+1 < len(fields) {
return fields[i+1]
}
return ""
}
}
return ""
}
// parseLiveRow reparses a live row as a rule in the direction its chain stands
// for. It rewrites the product's chain to the INPUT/OUTPUT/FORWARD equivalent
// and reuses the iptables rulespec parser, which also lifts the [pkts:bytes]
// prefix onto the rule. A row the model cannot hold is rejected.
func parseLiveRow(row liveRow, dir Direction, fam Family) (*Rule, bool) {
spec := strings.Replace(row.line, row.fields[1]+" "+row.chain,
row.fields[1]+" "+iptChainForDirection(dir), 1)
r, err := unmarshalIPTablesRule(spec, fam)
if err != nil {
return nil, false
}
return r, true
}
// decodeLiveRows decodes counter-annotated `iptables-save -c` output into the
// rules a backend's chains hold. decode turns one row into the rule it stands
// for and reports false for a row that stands for no rule of its own — a chain
// the backend does not surface, or one line of a multi-line expansion.
//
// A LOG line and the action line beneath it are one logical rule, but only while
// they stay physically adjacent, so a row decode rejects ends the current run
// and that run is coalesced on its own.
func decodeLiveRows(out []string, decode func(row liveRow) (*Rule, bool)) []*Rule {
var rules, run []*Rule
flush := func() {
if len(run) > 0 {
rules = append(rules, coalesceLoggedRules(run)...)
run = nil
}
}
chain := ""
for _, line := range out {
line = strings.TrimSpace(line)
// Only the counter-annotated rule lines carry a rule; the table and chain
// headers do not.
if !strings.HasPrefix(line, "[") {
continue
}
fields := strings.Fields(line)
if len(fields) < 4 || (fields[1] != "-A" && fields[1] != "--append") {
continue
}
row := liveRow{chain: fields[2], fields: fields, line: line}
if row.chain != chain {
flush()
chain, row.newChain = row.chain, true
}
r, ok := decode(row)
if !ok {
flush()
continue
}
run = append(run, r)
}
flush()
return rules
}
// applyLiveCounters attaches each live row's counters to the rule it belongs to
// and returns the rows no rule claimed. Every live row is consumed at most once,
// so duplicate rules keep distinct counts, and a rule with no live counterpart —
// an edit not yet activated by Reload, or one the backend stores but has not
// loaded — keeps zero counters. The leftovers are for a backend that has to
// claim a row on something other than rule identity (see CSF.claimDenyOutRows).
func applyLiveCounters(targets, live []*Rule) []*Rule {
if len(targets) == 0 || len(live) == 0 {
return live
}
// Match on identity first, so a rule that has a row of its own never absorbs a
// wider neighbour's count.
used := make([]bool, len(live))
matched := make([]bool, len(targets))
for ri, r := range targets {
for i, l := range live {
if used[i] || !l.Equal(r, true) {
continue
}
r.Packets, r.Bytes = l.Packets, l.Bytes
used[i], matched[ri] = true, true
break
}
}
// Then sum the rows a wider rule spans: a rule that covers more than one
// family, transport or direction is written as one row per cell, so its count
// is their total.
for ri, r := range targets {
if matched[ri] {
continue
}
for i, l := range live {
if used[i] || !r.Covers(l) {
continue
}
r.Packets += l.Packets
r.Bytes += l.Bytes
used[i] = true
}
}
var leftover []*Rule
for i, l := range live {
if !used[i] {
leftover = append(leftover, l)
}
}
return leftover
}
// countableRules returns the rules a family's live ruleset can account for: the
// ones pinned to that family, plus the family-agnostic ones. A backend that
// stores a rule dual-stack (apf's port lists, a family-agnostic hook line)
// reports it as one FamilyAny rule that both families' rulesets hold a row for,
// so it is offered to each and its counters accumulate across the two.
func countableRules(rules []*Rule, fam Family) []*Rule {
var out []*Rule
for _, r := range rules {
if r != nil && (r.Family == fam || r.Family == FamilyAny) {
out = append(out, r)
}
}
return out
}