192 lines
6.3 KiB
Go
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
|
|
}
|