275 lines
9.5 KiB
Go
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)
|
|
}
|