go-firewall/backup.go
2026-07-08 15:54:48 -05:00

275 lines
9.5 KiB
Go

package firewall
import (
"context"
"encoding/json"
"fmt"
"io"
"strings"
)
// This file makes a Backup portable: it can be serialized to and from JSON so a
// snapshot taken on one host can be replayed on another (or persisted to disk).
//
// The rule/NAT-rule structs are plain data, but their enum fields (Action,
// Family, Protocol, ...) are iota-based uint8 values. Left to encoding/json
// those would marshal as bare numbers, which are unreadable and would silently
// change meaning if a constant were ever reordered. They are therefore given
// MarshalJSON/UnmarshalJSON implementations that carry the canonical, stable
// string name each type's String method already emits. The two generic helpers
// below keep that boilerplate to one line per type.
// marshalEnum renders an enum value as its canonical string name.
func marshalEnum[T ~uint8](v T, str func(T) string) ([]byte, error) {
return json.Marshal(str(v))
}
// unmarshalEnum parses an enum value from its canonical string name via parse.
func unmarshalEnum[T ~uint8](data []byte, parse func(string) (T, error)) (T, error) {
var zero T
var s string
if err := json.Unmarshal(data, &s); err != nil {
return zero, err
}
return parse(s)
}
// MarshalJSON renders the action as its stable name (e.g. "accept").
func (a Action) MarshalJSON() ([]byte, error) { return marshalEnum(a, Action.String) }
// UnmarshalJSON parses an action from its stable name. The sentinel "invalid"
// (ActionInvalid) is accepted on the wire for round-trip fidelity even though
// ParseAction rejects it as caller input.
func (a *Action) UnmarshalJSON(data []byte) error {
v, err := unmarshalEnum(data, func(s string) (Action, error) {
if strings.EqualFold(strings.TrimSpace(s), "invalid") {
return ActionInvalid, nil
}
return ParseAction(s)
})
if err != nil {
return err
}
*a = v
return nil
}
// MarshalJSON renders the family as its stable name (e.g. "ipv4").
func (f Family) MarshalJSON() ([]byte, error) { return marshalEnum(f, Family.String) }
// UnmarshalJSON parses a family from its stable name.
func (f *Family) UnmarshalJSON(data []byte) error {
v, err := unmarshalEnum(data, ParseFamily)
if err != nil {
return err
}
*f = v
return nil
}
// MarshalJSON renders the protocol as its stable name (e.g. "tcp").
func (p Protocol) MarshalJSON() ([]byte, error) { return marshalEnum(p, Protocol.String) }
// UnmarshalJSON parses a protocol from its stable name. An unrecognized token is
// rejected rather than silently widening the rule to ProtocolAny; only the
// wildcard spellings ("any" or empty) resolve to ProtocolAny.
func (p *Protocol) UnmarshalJSON(data []byte) error {
v, err := unmarshalEnum(data, func(s string) (Protocol, error) {
proto := GetProtocol(s)
t := strings.TrimSpace(s)
if proto == ProtocolAny && t != "" && !strings.EqualFold(t, "any") {
return 0, fmt.Errorf("unknown protocol %q", s)
}
return proto, nil
})
if err != nil {
return err
}
*p = v
return nil
}
// MarshalJSON renders the connection-state set as a comma-joined name list.
func (s ConnState) MarshalJSON() ([]byte, error) { return marshalEnum(s, ConnState.String) }
// UnmarshalJSON parses a connection-state set from a comma-joined name list.
func (s *ConnState) UnmarshalJSON(data []byte) error {
v, err := unmarshalEnum(data, func(tok string) (ConnState, error) { return ParseConnState(tok) })
if err != nil {
return err
}
*s = v
return nil
}
// MarshalJSON renders the rate unit as its stable name (e.g. "minute").
func (u RateUnit) MarshalJSON() ([]byte, error) { return marshalEnum(u, RateUnit.String) }
// UnmarshalJSON parses a rate unit from its stable name.
func (u *RateUnit) UnmarshalJSON(data []byte) error {
v, err := unmarshalEnum(data, ParseRateUnit)
if err != nil {
return err
}
*u = v
return nil
}
// MarshalJSON renders the NAT kind as its stable name (e.g. "dnat").
func (k NATKind) MarshalJSON() ([]byte, error) { return marshalEnum(k, NATKind.String) }
// UnmarshalJSON parses a NAT kind from its stable name.
func (k *NATKind) UnmarshalJSON(data []byte) error {
v, err := unmarshalEnum(data, ParseNATKind)
if err != nil {
return err
}
*k = v
return nil
}
// MarshalJSON renders the direction as its stable name (e.g. "input").
func (d Direction) MarshalJSON() ([]byte, error) { return marshalEnum(d, Direction.String) }
// UnmarshalJSON parses a direction from its stable name.
func (d *Direction) UnmarshalJSON(data []byte) error {
v, err := unmarshalEnum(data, ParseDirection)
if err != nil {
return err
}
*d = v
return nil
}
// MarshalJSON renders the set type as its stable name (e.g. "hash:net").
func (t SetType) MarshalJSON() ([]byte, error) { return marshalEnum(t, SetType.String) }
// UnmarshalJSON parses a set type from its stable name.
func (t *SetType) UnmarshalJSON(data []byte) error {
v, err := unmarshalEnum(data, ParseSetType)
if err != nil {
return err
}
*t = v
return nil
}
// captureBackupState fills a backup's DefaultPolicy and AddressSets from the
// backend, for the backends that advertise those features. It is shared by every
// Backup implementation so a snapshot captures the full managed state — not just
// filter/NAT rules — without each backend re-probing capabilities. A backend that
// advertises neither feature leaves both fields nil, so the snapshot is unchanged.
func captureBackupState(ctx context.Context, mgr Manager, zoneName string, b *Backup) error {
caps := mgr.Capabilities()
if caps.DefaultPolicy {
// Best-effort: a backend that cannot report a single coherent default policy
// (iptables with out-of-band divergent IPv4/IPv6 chain policies) captures none
// rather than failing the whole backup. A genuine read error would already have
// surfaced from the GetRules call the Backup makes before this, which reads the
// same source; leaving the field nil then makes Restore leave the policy as it
// finds it.
if policy, err := mgr.GetDefaultPolicy(ctx, zoneName); err == nil {
b.DefaultPolicy = policy
}
}
if caps.AddressSets {
sets, err := mgr.GetAddressSets(ctx)
if err != nil {
return err
}
b.AddressSets = sets
}
return nil
}
// restoreBackupSets recreates a backup's address sets so a set-referencing rule
// (@set) resolves on Restore. It runs before the filter rules are re-added. Only
// backends that advertise AddressSets act. cleanFirst removes each set before
// recreating it: a caller whose restore has already cleared the rules that could
// reference a set (a container backend that flushes its table/anchor) passes true
// so the set is rebuilt from a clean slate — needed for nftables, whose
// AddAddressSet is a no-op on an existing set and so would not reconcile its
// entries. A caller whose old rules are still loaded when sets are recreated
// (a tag/rewrite backend) passes false and relies on AddAddressSet's own
// idempotent create-or-repopulate (ipset -exist, pfctl -T add).
func restoreBackupSets(ctx context.Context, mgr Manager, b *Backup, cleanFirst bool) error {
if b == nil || !mgr.Capabilities().AddressSets {
return nil
}
for _, set := range b.AddressSets {
if cleanFirst {
if err := mgr.RemoveAddressSet(ctx, set.Name); err != nil {
return err
}
}
if err := mgr.AddAddressSet(ctx, set); err != nil {
return err
}
}
return nil
}
// applyBackupPolicy re-asserts a backup's default policy, after the rules are
// restored (the policy is independent of rule order). Only backends that
// advertise DefaultPolicy act, and a nil snapshot policy (a backend that captured
// none) is left unchanged.
func applyBackupPolicy(ctx context.Context, mgr Manager, zoneName string, b *Backup) error {
if b == nil || b.DefaultPolicy == nil || !mgr.Capabilities().DefaultPolicy {
return nil
}
return mgr.SetDefaultPolicy(ctx, zoneName, b.DefaultPolicy)
}
// WriteBackup encodes backup as JSON to w so it can be persisted to disk (or
// moved to another host) and later replayed with ReadBackup or RestoreReader.
//
// The encoding is portable: enum fields are written as their stable string
// names, so a backup survives a reordering of the library's iota constants and
// is readable on a host running a different version of the library. Per-rule
// counters (Packets/Bytes) are carried through for the record but are ignored
// when a backup is restored (they are not part of rule identity).
//
// f, _ := os.Create("backup.json")
// _ = firewall.WriteBackup(f, backup)
func WriteBackup(w io.Writer, backup *Backup) error {
if backup == nil {
return fmt.Errorf("backup cannot be nil")
}
if w == nil {
return fmt.Errorf("writer cannot be nil")
}
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(backup)
}
// ReadBackup decodes a backup previously written by WriteBackup from r.
//
// f, _ := os.Open("backup.json")
// backup, _ := firewall.ReadBackup(f)
// _ = mgr.Restore(ctx, zone, backup)
func ReadBackup(r io.Reader) (*Backup, error) {
if r == nil {
return nil, fmt.Errorf("reader cannot be nil")
}
var backup Backup
if err := json.NewDecoder(r).Decode(&backup); err != nil {
return nil, err
}
return &backup, nil
}
// RestoreReader reads a backup from r and applies it via mgr.Restore. It is the
// streaming counterpart of Manager.Restore: a caller can replay a backup
// straight from a file or network reader without first buffering it into a
// *Backup. A read error is returned before any rules are touched.
//
// f, _ := os.Open("backup.json")
// _ = firewall.RestoreReader(ctx, mgr, zone, f)
func RestoreReader(ctx context.Context, mgr Manager, zoneName string, r io.Reader) error {
backup, err := ReadBackup(r)
if err != nil {
return err
}
return mgr.Restore(ctx, zoneName, backup)
}