Files
abrown38andClaude Opus 5.5 9863dd1cdd Quotas: carry what was used when a rule counts differently
model.QuotaCarry says what becomes of a quota's use when its rule
changes how it counts; the firewall's apply follows it, comparing with
the rules in force: per address to a total starts at the addresses' sum,
wider groups add up their addresses, and what can't be carried (a total
to per address, per source to per destination, finer groups) starts
again, with the engine warning quota_restarts on that change. Before,
per source to per destination kept the sources' use as destinations',
and regrouping left old entries behind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:57:18 +01:00

854 lines
25 KiB
Go

// Package nft runs the nft command: it checks and applies rendered
// rulesets, reads counters and dynamic sets back as JSON, and maps nft's
// error messages to the objects they came from.
//
// The nft command is used rather than a netlink library: it checks a
// ruleset without applying it (nft -c), applies a whole script in one
// transaction, and its errors are readable.
package nft
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/netip"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
"housecarl/internal/model"
"housecarl/internal/render"
"housecarl/internal/trace"
)
// Runner runs nft.
type Runner struct {
// Command is how nft is run: ["nft"] (the default), or a wrapper such
// as ["docker", "run", "--rm", "-i", "--cap-add", "NET_ADMIN", "IMAGE",
// "nft"] in tests.
Command []string
}
// Wrapper reads HOUSECARL_NFT, a command line that runs nft (tests point
// it at a container), or returns ["nft"].
func Wrapper() []string {
if v := strings.TrimSpace(os.Getenv("HOUSECARL_NFT")); v != "" {
return strings.Fields(v)
}
return []string{"nft"}
}
func (r *Runner) cmd(ctx context.Context, args ...string) *exec.Cmd {
c := r.Command
if len(c) == 0 {
c = []string{"nft"}
}
return exec.CommandContext(ctx, c[0], append(append([]string{}, c[1:]...), args...)...)
}
// run runs nft with args, script on stdin.
func (r *Runner) run(ctx context.Context, script string, args ...string) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel()
c := r.cmd(ctx, args...)
if script != "" {
c.Stdin = strings.NewReader(script)
}
var out, errb bytes.Buffer
c.Stdout, c.Stderr = &out, &errb
if err := c.Run(); err != nil {
msg := strings.TrimSpace(errb.String())
if msg == "" {
msg = err.Error()
}
return out.Bytes(), &Error{Output: msg}
}
return out.Bytes(), nil
}
// Error is nft's complaint.
type Error struct {
Output string
// Object is what the first failing line came from, when known.
Object string
// Message is the first error line, without the file and position.
Message string
}
func (e *Error) Error() string {
if e.Object != "" && e.Message != "" {
return e.Object + ": " + e.Message
}
if e.Message != "" {
return e.Message
}
return e.Output
}
// errLine matches "/dev/stdin:12:3-10: Error: …".
var errLine = regexp.MustCompile(`(?m)^[^:\n]*:(\d+):[0-9-]+: Error: (.*)$`)
// explain fills in Object and Message from nft's output and the rendered
// ruleset.
func explain(err error, rs *render.Ruleset) error {
e, ok := err.(*Error)
if !ok {
return err
}
if m := errLine.FindStringSubmatch(e.Output); m != nil {
line, _ := strconv.Atoi(m[1])
e.Message = strings.TrimSpace(m[2])
e.Object = rs.Object(line)
}
return e
}
// Check has nft check a ruleset without applying it.
func (r *Runner) Check(ctx context.Context, rs *render.Ruleset) error {
_, err := r.run(ctx, rs.Text, "-c", "-f", "/dev/stdin")
return explain(err, rs)
}
// Apply applies a ruleset (one transaction: all or nothing).
func (r *Runner) Apply(ctx context.Context, rs *render.Ruleset) error {
_, err := r.run(ctx, rs.Text, "-f", "/dev/stdin")
return explain(err, rs)
}
// ListTable returns Housecarl's table as nft JSON (nil if it doesn't
// exist).
func (r *Runner) ListTable(ctx context.Context) ([]byte, error) {
out, err := r.run(ctx, "", "-j", "list", "table", "inet", render.Table)
if err != nil {
if e, ok := err.(*Error); ok && strings.Contains(e.Output, "No such file or directory") {
return nil, nil
}
return nil, err
}
return out, nil
}
// ListRuleset returns the whole ruleset as nft JSON (the importer's
// input on this machine).
func (r *Runner) ListRuleset(ctx context.Context) ([]byte, error) {
return r.run(ctx, "", "-j", "list", "ruleset")
}
// TableText returns a table as an nft script that recreates it.
func (r *Runner) TableText(ctx context.Context, family, name string) (string, error) {
out, err := r.run(ctx, "", "list", "table", family, name)
if err != nil {
return "", err
}
return "table " + family + " " + name + " {}\ndelete table " + family + " " + name + "\n" + string(out), nil
}
// ApplyScript applies an nft script (a takeover's rollback).
func (r *Runner) ApplyScript(ctx context.Context, script string) error {
_, err := r.run(ctx, script, "-f", "/dev/stdin")
return err
}
// DeleteTable deletes a table (hc takeover: the ruleset Housecarl
// replaces).
func (r *Runner) DeleteTable(ctx context.Context, family, name string) error {
_, err := r.run(ctx, "", "delete", "table", family, name)
return err
}
// AddElement adds an entry to a dynamic set's family part, with a timeout
// in seconds (0: the set's default).
func (r *Runner) AddElement(ctx context.Context, set, addr string, timeout int) error {
a, err := model.ParseAddress(addr)
if err != nil {
return err
}
t := ""
if timeout > 0 {
t = fmt.Sprintf(" timeout %ds", timeout)
}
_, err = r.run(ctx, fmt.Sprintf("add element inet %s %s { %s%s }\n", render.Table, render.SetName(set, a.IsV6()), a, t), "-f", "/dev/stdin")
return err
}
// DeleteElement removes an entry from a dynamic set.
func (r *Runner) DeleteElement(ctx context.Context, set, addr string) error {
a, err := model.ParseAddress(addr)
if err != nil {
return err
}
_, err = r.run(ctx, fmt.Sprintf("delete element inet %s %s { %s }\n", render.Table, render.SetName(set, a.IsV6()), a), "-f", "/dev/stdin")
return err
}
// ---- reading JSON
// Table is what Housecarl reads back from its table: counters by mark
// and the dynamic sets' entries.
type Table struct {
// Counters by the comment's mark ("r12", "f3", "b-established",
// "p-input"); several nftables rules of one object add up.
Counters map[string]Counter
// Elements by set name (both families together).
Elements map[string][]render.Element
// Quotas: the rules' quotas' used bytes (quota_r12), and the
// per-address quotas' entries by set (quota_r12_v4).
QuotaUsage
// DeviceSets are the per-device traffic counters by set
// (stats_up_v4, stats_down_v6 …): each address's packets and bytes.
DeviceSets map[string][]render.Element
// Since is when the counters started (housecarld's first apply).
Since time.Time
// Tables are all the machine's tables ("inet main"), when the whole
// ruleset was read.
Tables []string
// Fallback: the table is the boot fallback (render.Fallback), not a
// ruleset housecarld applied.
Fallback bool
}
// QuotaUsage is what the quotas used: kept across reloads, and saved in
// the database so a reboot doesn't reset it.
type QuotaUsage struct {
Quotas map[string]int64 `json:"quotas,omitempty"`
QuotaSets map[string][]render.Element `json:"quota_sets,omitempty"`
}
// merge takes the larger usage of each quota and entry from o (what was
// saved before a reboot, when the kernel's count started again).
func (u *QuotaUsage) merge(o *QuotaUsage) {
if o == nil {
return
}
if u.Quotas == nil {
u.Quotas = map[string]int64{}
}
if u.QuotaSets == nil {
u.QuotaSets = map[string][]render.Element{}
}
for k, v := range o.Quotas {
if v > u.Quotas[k] {
u.Quotas[k] = v
}
}
for set, es := range o.QuotaSets {
for _, e := range es {
found := false
for i := range u.QuotaSets[set] {
if u.QuotaSets[set][i].Addr == e.Addr {
found = true
if e.Used > u.QuotaSets[set][i].Used {
u.QuotaSets[set][i].Used = e.Used
}
}
}
if !found {
u.QuotaSets[set] = append(u.QuotaSets[set], e)
}
}
}
}
// Counter is packets and bytes.
type Counter struct {
Packets uint64 `json:"packets"`
Bytes uint64 `json:"bytes"`
}
type jsonDoc struct {
Nftables []map[string]json.RawMessage `json:"nftables"`
}
type jsonRule struct {
Comment string `json:"comment"`
Expr []json.RawMessage `json:"expr"`
}
type jsonSet struct {
Name string `json:"name"`
Elem []json.RawMessage `json:"elem"`
}
// jsonQuota is a quota's state: a named quota ({"bytes", "used"}, in
// bytes) or one in an element ({"val", "val_unit", "used", "used_unit"}).
type jsonQuota struct {
Name string `json:"name"`
Used int64 `json:"used"`
UsedUnit string `json:"used_unit"`
}
func (q jsonQuota) bytes() int64 {
if m, ok := model.ByteUnits[q.UsedUnit]; ok {
return q.Used * m
}
return q.Used
}
// ParseTable reads `nft -j list table inet housecarl`.
func ParseTable(data []byte) (*Table, error) {
t := &Table{Counters: map[string]Counter{}, Elements: map[string][]render.Element{}, DeviceSets: map[string][]render.Element{},
QuotaUsage: QuotaUsage{Quotas: map[string]int64{}, QuotaSets: map[string][]render.Element{}}}
if len(data) == 0 {
return t, nil
}
var doc jsonDoc
if err := json.Unmarshal(data, &doc); err != nil {
return nil, fmt.Errorf("reading nft's JSON: %w", err)
}
for _, item := range doc.Nftables {
if raw, ok := item["rule"]; ok {
var r jsonRule
if json.Unmarshal(raw, &r) != nil || !strings.HasPrefix(r.Comment, "hc:") {
continue
}
mark, _, _ := strings.Cut(strings.TrimPrefix(r.Comment, "hc:"), " ")
if strings.HasPrefix(mark, render.FallbackMark+"-") {
t.Fallback = true
}
for _, e := range r.Expr {
var c struct {
Counter *Counter `json:"counter"`
}
if json.Unmarshal(e, &c) == nil && c.Counter != nil {
cur := t.Counters[mark]
cur.Packets += c.Counter.Packets
cur.Bytes += c.Counter.Bytes
t.Counters[mark] = cur
}
}
}
if raw, ok := item["quota"]; ok {
var q jsonQuota
if json.Unmarshal(raw, &q) == nil && strings.HasPrefix(q.Name, "quota_r") {
t.Quotas[q.Name] = q.bytes()
}
}
if raw, ok := item["set"]; ok {
var s jsonSet
if json.Unmarshal(raw, &s) != nil {
continue
}
if strings.HasPrefix(s.Name, "quota_r") {
for _, e := range s.Elem {
var w struct {
Elem struct {
Val string `json:"val"`
Quota *jsonQuota `json:"quota"`
} `json:"elem"`
}
if json.Unmarshal(e, &w) == nil && w.Elem.Quota != nil {
t.QuotaSets[s.Name] = append(t.QuotaSets[s.Name], render.Element{Addr: w.Elem.Val, Used: w.Elem.Quota.bytes()})
}
}
continue
}
if render.IsDeviceSet(s.Name) {
for _, e := range s.Elem {
var w struct {
Elem struct {
Val string `json:"val"`
Expires int `json:"expires"`
Counter *Counter `json:"counter"`
} `json:"elem"`
}
if json.Unmarshal(e, &w) == nil && w.Elem.Counter != nil {
t.DeviceSets[s.Name] = append(t.DeviceSets[s.Name], render.Element{Addr: w.Elem.Val, Timeout: w.Elem.Expires, Packets: w.Elem.Counter.Packets, Bytes: w.Elem.Counter.Bytes})
}
}
continue
}
name := strings.TrimSuffix(strings.TrimSuffix(s.Name, "_v4"), "_v6")
if name == s.Name || strings.HasPrefix(s.Name, "meter_") || strings.HasPrefix(s.Name, "conn_") {
continue
}
for _, e := range s.Elem {
if el, ok := parseElem(e); ok {
t.Elements[name] = append(t.Elements[name], el)
}
}
}
}
return t, nil
}
// parseElem reads a set element: "10.0.0.5", or {"elem": {"val":
// "10.0.0.5", "timeout": 1800, "expires": 1799}}.
func parseElem(raw json.RawMessage) (render.Element, bool) {
var s string
if json.Unmarshal(raw, &s) == nil {
return render.Element{Addr: s}, true
}
var w struct {
Elem struct {
Val json.RawMessage `json:"val"`
Expires int `json:"expires"`
} `json:"elem"`
}
if json.Unmarshal(raw, &w) != nil || json.Unmarshal(w.Elem.Val, &s) != nil {
return render.Element{}, false
}
return render.Element{Addr: s, Timeout: w.Elem.Expires}, true
}
// ---- the firewall
// Firewall is the engine's Firewall: it renders configurations and has
// nft check and apply them, carrying dynamic sets' entries over.
type Firewall struct {
Runner *Runner
// Interfaces lists the interfaces that exist now (nil: assume all).
Interfaces func() map[string]bool
// BootFile is where the confirmed ruleset is saved for the boot unit
// (housecarl-ruleset.service; "" for none).
BootFile string
// Shadow is shadow mode: the ruleset changes nothing, beside another
// firewall (render.Options; docs/shadow.md). ShadowBefore runs its
// filter chains before that one's.
Shadow, ShadowBefore bool
// Replacing the table resets its counters: base keeps the totals of
// the tables replaced since housecarld started.
mu sync.Mutex
base map[string]Counter
since time.Time
// restore is the quota usage saved before housecarld (or the machine)
// stopped, put back by the next apply.
restore *QuotaUsage
// adjust are quota changes the next apply makes (AdjustQuota).
adjust []QuotaAdjust
// quotaRules are the rules with a quota in the ruleset in force, by
// ID: what their usage was counted by (nil before the first apply).
quotaRules map[int64]*model.Rule
// flowDevs are the flowtable's devices in the ruleset in force, by
// name, with their interface index then (0: left out, missing).
flowDevs map[string]int
}
// ifIndex is an interface's index, 0 if it doesn't exist (tests change it).
var ifIndex = func(name string) int {
if i, err := net.InterfaceByName(name); err == nil {
return i.Index
}
return 0
}
// FlowtableStale reports whether the flowtable in force no longer matches
// the machine: a device of st's flowtable appeared, went, or came back as
// a new interface (ppp0 after PPPoE reconnects; the kernel drops a
// device from the flowtable when it goes). Applying again fixes it.
func (f *Firewall) FlowtableStale(st *model.State) bool {
f.mu.Lock()
defer f.mu.Unlock()
if f.flowDevs == nil {
return false
}
for _, d := range st.Settings.FlowtableDevices {
if ifIndex(d) != f.flowDevs[d] {
return true
}
}
return false
}
// RestoreQuotas has the next apply put this saved quota usage back where
// the kernel's count is lower (after a reboot, the boot ruleset starts
// every quota at zero).
func (f *Firewall) RestoreQuotas(u *QuotaUsage) {
f.mu.Lock()
defer f.mu.Unlock()
f.restore = u
}
// QuotaAdjust takes Give bytes off what a quota used (never below zero):
// the named quota Name, or the entries of the per-address sets Name_v4
// and Name_v6, Addr's alone if given (the set's key, as listed).
type QuotaAdjust struct {
Name string
Addr string
Give int64
}
// AdjustQuota has the next apply change what a quota used.
func (f *Firewall) AdjustQuota(a QuotaAdjust) {
f.mu.Lock()
defer f.mu.Unlock()
f.adjust = append(f.adjust, a)
}
// apply takes what a's say off u. An entry with nothing left used is
// dropped, so its quota starts again with its next packet.
func (u *QuotaUsage) apply(adj []QuotaAdjust) {
less := func(used, give int64) int64 {
if give >= used {
return 0
}
return used - give
}
for _, a := range adj {
if used, ok := u.Quotas[a.Name]; ok {
u.Quotas[a.Name] = less(used, a.Give)
}
for _, set := range []string{a.Name + "_v4", a.Name + "_v6"} {
var kept []render.Element
for _, e := range u.QuotaSets[set] {
if a.Addr == "" || e.Addr == a.Addr {
e.Used = less(e.Used, a.Give)
}
if e.Used > 0 {
kept = append(kept, e)
}
}
if _, ok := u.QuotaSets[set]; ok {
u.QuotaSets[set] = kept
}
}
}
}
// carry keeps what quotas used when their rules now count differently
// (model.QuotaCarry): per address to a total, the sum; to wider groups,
// the groups' sums; what can't be carried starts again. was are the
// rules in force before (nil: nothing to compare with).
func (u *QuotaUsage) carry(was map[int64]*model.Rule, st *model.State) {
for _, r := range st.Rules {
old := was[r.ID]
carry, _ := model.QuotaCarry(old, r)
if carry == model.QuotaKept {
continue
}
name := render.QuotaName(r.ID)
sets := []string{name + "_v4", name + "_v6"}
switch carry {
case model.QuotaSummed:
var sum int64
for _, set := range sets {
for _, e := range u.QuotaSets[set] {
sum += e.Used
}
}
if u.Quotas == nil {
u.Quotas = map[string]int64{}
}
u.Quotas[name] = sum
case model.QuotaRegrouped:
v4, v6 := r.Groups()
for _, set := range sets {
byKey := map[string]int64{}
var order []string
for _, e := range u.QuotaSets[set] {
a, err := netip.ParseAddr(e.Addr)
if err != nil {
continue
}
bits := v4
if a.Is6() {
bits = v6
}
p, _ := a.Prefix(bits)
k := p.Addr().String()
if _, ok := byKey[k]; !ok {
order = append(order, k)
}
byKey[k] += e.Used
}
if len(order) == 0 {
continue
}
var es []render.Element
for _, k := range order {
es = append(es, render.Element{Addr: k, Used: byKey[k]})
}
u.QuotaSets[set] = es
}
continue
}
// What was counted the other way goes: a total, or the entries.
if carry != model.QuotaSummed {
delete(u.Quotas, name)
}
for _, set := range sets {
delete(u.QuotaSets, set)
}
}
}
// ResetQuota starts a rule's quota again: the named quota, or the
// per-address quota sets (both families).
func (f *Firewall) ResetQuota(ctx context.Context, ruleID int64, perAddress bool) error {
name := render.QuotaName(ruleID)
if !perAddress {
_, err := f.Runner.run(ctx, "", "reset", "quota", "inet", render.Table, name)
return err
}
for _, fam := range []string{"_v4", "_v6"} {
if _, err := f.Runner.run(ctx, "", "flush", "set", "inet", render.Table, name+fam); err != nil {
if e, ok := err.(*Error); ok && strings.Contains(e.Output, "No such file or directory") {
continue // the rule has no such family
}
return err
}
}
return nil
}
func (f *Firewall) options() render.Options {
o := render.Options{Shadow: f.Shadow, ShadowBefore: f.ShadowBefore}
if f.Interfaces != nil {
o.Interfaces = f.Interfaces()
}
return o
}
// Render builds the ruleset for st as Apply would.
func (f *Firewall) Render(st *model.State) *render.Ruleset { return render.Render(st, f.options()) }
// RenderEnforcing builds the ruleset for st as Apply would outside shadow
// mode.
func (f *Firewall) RenderEnforcing(st *model.State) *render.Ruleset {
o := f.options()
o.Shadow, o.ShadowBefore = false, false
return render.Render(st, o)
}
// Check implements engine.Firewall. In shadow mode it checks the
// enforcing ruleset too, so that leaving shadow mode can't fail.
func (f *Firewall) Check(ctx context.Context, st *model.State) error {
if err := f.Runner.Check(ctx, render.Render(st, f.options())); err != nil {
return err
}
if f.Shadow {
if err := f.Runner.Check(ctx, f.RenderEnforcing(st)); err != nil {
return fmt.Errorf("the ruleset for when shadow mode is off: %w", err)
}
}
return nil
}
// Apply implements engine.Firewall.
func (f *Firewall) Apply(ctx context.Context, st *model.State) error {
f.mu.Lock()
defer f.mu.Unlock()
opt := f.options()
var old *Table
if data, err := f.Runner.ListTable(ctx); err == nil {
if t, err := ParseTable(data); err == nil {
opt.Elements = t.Elements
old = t
}
}
usage := QuotaUsage{}
if old != nil {
usage = old.QuotaUsage
}
usage.merge(f.restore)
usage.carry(f.quotaRules, st)
usage.apply(f.adjust)
f.adjust = nil
opt.Quotas, opt.QuotaSets = usage.Quotas, usage.QuotaSets
if old != nil {
opt.DeviceSets = old.DeviceSets
}
if err := f.Runner.Apply(ctx, render.Render(st, opt)); err != nil {
return err
}
f.restore = nil
f.quotaRules = map[int64]*model.Rule{}
for _, r := range st.Rules {
if r.Quota != nil {
c, q := *r, *r.Quota // only the quota and the grouping are read
c.Quota = &q
f.quotaRules[r.ID] = &c
}
}
f.flowDevs = map[string]int{}
for _, d := range st.Settings.FlowtableDevices {
if opt.Interfaces == nil || opt.Interfaces[d] {
f.flowDevs[d] = ifIndex(d)
}
}
if f.base == nil {
f.base, f.since = map[string]Counter{}, time.Now()
}
if old != nil {
for k, c := range old.Counters {
b := f.base[k]
b.Packets += c.Packets
b.Bytes += c.Bytes
f.base[k] = b
}
}
pruneCounters(f.base, st)
return nil
}
// pruneCounters forgets the totals of objects st no longer has (deleted,
// or replaced by an import with new IDs), so they aren't listed beside
// the current ones. A disabled object keeps its totals for when it comes
// back; built-ins and policies are always kept.
func pruneCounters(base map[string]Counter, st *model.State) {
ids := map[string]bool{}
for _, r := range st.Rules {
ids[fmt.Sprintf("r%d", r.ID)] = true
}
for _, x := range st.Forwards {
ids[fmt.Sprintf("f%d", x.ID)] = true
}
for _, x := range st.SNATs {
ids[fmt.Sprintf("s%d", x.ID)] = true
}
for _, x := range st.Redirects {
ids[fmt.Sprintf("d%d", x.ID)] = true
}
for mark := range base {
obj, _, _ := strings.Cut(mark, "-") // f3-hairpin, f3-6 are f3's
if len(obj) < 2 || !strings.ContainsRune("rfsd", rune(obj[0])) || strings.Trim(obj[1:], "0123456789") != "" {
continue // b-established, p-input
}
if !ids[obj] {
delete(base, mark)
}
}
}
// SaveBoot writes the ruleset of st for the boot unit, and the fallback
// it loads if that fails (FallbackFile), atomically. The boot ruleset has
// no flowtable: it loads before the network comes up, when its devices
// (ppp0, wg0) don't exist yet and nftables would refuse the whole file;
// housecarld adds it once they do. In shadow mode both are shadow mode's:
// the fallback then filters nothing.
func (f *Firewall) SaveBoot(st *model.State) error {
if f.BootFile == "" {
return nil
}
if err := os.MkdirAll(filepath.Dir(f.BootFile), 0o750); err != nil {
return err
}
fallback := render.Fallback(st)
if f.Shadow {
fallback = render.ShadowFallback()
}
if err := writeAtomic(FallbackFile(f.BootFile), fallback); err != nil {
return err
}
rs := render.Render(st, render.Options{Interfaces: map[string]bool{}, Shadow: f.Shadow, ShadowBefore: f.ShadowBefore})
return writeAtomic(f.BootFile, rs.Text)
}
// FallbackFile is where the fallback for a boot ruleset is saved:
// ruleset.nft's is ruleset-fallback.nft beside it.
func FallbackFile(boot string) string {
return strings.TrimSuffix(boot, ".nft") + "-fallback.nft"
}
func writeAtomic(path, text string) error {
tmp := path + ".tmp"
if err := os.WriteFile(tmp, []byte(text), 0o640); err != nil {
return err
}
return os.Rename(tmp, path)
}
// Table reads Housecarl's table back, with counters totalled since
// housecarld started (across reloads).
func (f *Firewall) Table(ctx context.Context) (*Table, error) {
f.mu.Lock()
defer f.mu.Unlock()
data, err := f.Runner.ListTable(ctx)
if err != nil {
return nil, err
}
if data == nil {
return nil, ErrNoTable
}
t, err := ParseTable(data)
if err != nil {
return nil, err
}
for k, b := range f.base {
c := t.Counters[k]
c.Packets += b.Packets
c.Bytes += b.Bytes
t.Counters[k] = c
}
t.Since = f.since
return t, nil
}
// ErrNoTable: Housecarl's table isn't in the ruleset.
var ErrNoTable = errors.New("there is no table inet housecarl: Housecarl's ruleset isn't in force")
// AddElement adds an entry to a dynamic set.
func (f *Firewall) AddElement(ctx context.Context, set, addr string, timeout int) error {
return f.Runner.AddElement(ctx, set, addr, timeout)
}
// DeleteElement removes an entry from a dynamic set.
func (f *Firewall) DeleteElement(ctx context.Context, set, addr string) error {
return f.Runner.DeleteElement(ctx, set, addr)
}
// ListRuleset returns the machine's whole ruleset as JSON.
func (f *Firewall) ListRuleset(ctx context.Context) ([]byte, error) { return f.Runner.ListRuleset(ctx) }
// TableText returns another table as a script that recreates it.
func (f *Firewall) TableText(ctx context.Context, family, name string) (string, error) {
return f.Runner.TableText(ctx, family, name)
}
// ApplyScript applies an nft script.
func (f *Firewall) ApplyScript(ctx context.Context, script string) error {
return f.Runner.ApplyScript(ctx, script)
}
// Trace follows packets: it starts `nft monitor trace`, puts the tracing
// table (script) in, and passes each line to line until ctx ends; then
// it takes the table out again, whatever happened.
func (f *Firewall) Trace(ctx context.Context, script string, line func(string)) error {
c := f.Runner.cmd(ctx, "monitor", "trace")
out, err := c.StdoutPipe()
if err != nil {
return err
}
var errb bytes.Buffer
c.Stderr = &errb
if err := c.Start(); err != nil {
return err
}
defer f.Runner.run(context.Background(), "", "delete", "table", "inet", trace.Table)
if err := f.Runner.ApplyScript(ctx, script); err != nil {
c.Process.Kill()
c.Wait()
return fmt.Errorf("starting the trace: %w", err)
}
sc := bufio.NewScanner(out)
sc.Buffer(make([]byte, 64<<10), 1<<20)
for sc.Scan() {
line(sc.Text())
}
err = c.Wait()
if ctx.Err() != nil {
return nil // stopped, or its time was up
}
if err != nil && errb.Len() > 0 {
return fmt.Errorf("nft monitor trace: %s", strings.TrimSpace(errb.String()))
}
return err
}
// DeleteTable deletes another table (takeover).
func (f *Firewall) DeleteTable(ctx context.Context, family, name string) error {
if name == render.Table && family == "inet" {
return fmt.Errorf("that is Housecarl's own table")
}
return f.Runner.DeleteTable(ctx, family, name)
}