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

3264 lines
98 KiB
Go

package firewall
import (
"bytes"
"context"
"errors"
"fmt"
"hash/fnv"
"net"
"net/netip"
"sort"
"strings"
"sync"
"sync/atomic"
"github.com/google/nftables"
"github.com/google/nftables/binaryutil"
"github.com/google/nftables/expr"
"github.com/google/nftables/userdata"
"go4.org/netipx"
"golang.org/x/sys/unix"
)
const (
// NFTDefaultTable is the table name used when no rule prefix is supplied.
NFTDefaultTable = "go_firewall"
// nftMeterSetSize is the dynamic-set size nft itself gives a meter, kept so a
// set this backend creates is indistinguishable from one nft would have made.
nftMeterSetSize = 65535
// nftCommentMax is the longest rule comment nftables stores (its own limit).
nftCommentMax = 128
// nftLogPrefixMax is the longest log prefix the kernel's nf_log accepts.
nftLogPrefixMax = 127
)
// NFT manages firewall rules through the nftables netlink API. To avoid
// clobbering rules owned by other tooling, every rule this backend creates lives
// in a private `inet` table (named after the rule prefix) with its own input and
// output base chains. Writes are scoped to that table; reads also report rules
// found in other tables so callers can see the whole ruleset.
type NFT struct {
// table is the nftables table this backend owns.
table string
// mu guards the ensured/natEnsured flags so concurrent callers do not race on
// the one-time table/chain setup.
mu sync.Mutex
// ensured records whether the private table/chains have been created this
// session, so the setup runs only once.
ensured bool
// natEnsured records the same for the nat base chains, which are created
// lazily only when a NAT rule is first written.
natEnsured bool
}
// nftSetID hands out the transaction-local identifiers an anonymous set is
// referenced by within a batch. Only uniqueness inside one batch matters, so a
// process-wide counter is sufficient; it also keeps the library's own
// auto-allocation (which only runs for a set with ID 0) out of the picture.
var nftSetID atomic.Uint32
// nextSetID returns the next anonymous-set identifier.
func (f *NFT) nextSetID() uint32 {
return nftSetID.Add(1)
}
// sanitizeNFTName reduces an arbitrary prefix to a valid nftables identifier
// (letters, digits and underscores), falling back to the default when nothing
// usable remains.
func sanitizeNFTName(prefix string) string {
var b strings.Builder
for _, r := range prefix {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '_':
b.WriteRune(r)
case r == '-' || r == ' ' || r == '.':
b.WriteRune('_')
}
}
name := strings.Trim(b.String(), "_")
if name == "" {
return NFTDefaultTable
}
// An nftables identifier must begin with a letter; a digit-led prefix would
// make every later table/chain creation fail.
if c := name[0]; c >= '0' && c <= '9' {
name = "fw_" + name
}
return name
}
// nftConn returns a transaction scope for one operation. Despite the name this
// opens nothing: nftables.New only allocates, and the netlink socket is dialed
// and closed per operation inside the library either way, so there is no
// connection here worth caching on the backend.
//
// What the value does carry is a batch: staged additions and deletions
// accumulate on it until Flush sends and clears them all. That is why each
// operation takes its own rather than sharing one — a Flush on a shared scope
// would commit whatever another caller had half-staged.
func nftConn() (*nftables.Conn, error) {
return nftables.New()
}
// NewNFT constructs an nftables-backed Manager, deriving the private table name
// from rulePrefix and verifying nf_tables is reachable over netlink.
func NewNFT(ctx context.Context, rulePrefix string) (*NFT, error) {
nft := &NFT{table: sanitizeNFTName(rulePrefix)}
// Confirm the nf_tables subsystem answers. Opening the socket alone proves
// little, so list the ruleset's tables: that fails without the kernel module
// or the privileges every later operation needs.
c, err := nftConn()
if err != nil {
return nil, fmt.Errorf("unable to open the nftables netlink socket: %s", err)
}
if _, err := c.ListTables(); err != nil {
return nil, fmt.Errorf("unable to list the nftables ruleset: %s", err)
}
return nft, nil
}
// Type returns the manager type.
func (f *NFT) Type() string {
return NFTType
}
// Capabilities returns the set of features this backend can express.
func (f *NFT) Capabilities() Capabilities {
return Capabilities{
Output: true,
Forward: true,
IPv6: true,
PortPair: true,
ConnState: true,
InterfaceMatch: true,
Logging: true,
RateLimit: true,
ConnLimit: true,
NAT: true,
RuleOrdering: true,
DefaultPolicy: true,
RuleCounters: true,
AddressSets: true,
Comments: true,
Negation: true,
RejectAction: true,
FamilyWithoutAddress: true,
}
}
// GetZone reports no zone; nftables has no interface-to-zone mapping in this model.
func (f *NFT) GetZone(ctx context.Context, iface string) (zoneName string, err error) {
return "", nil
}
// tableRef returns the netlink handle for this backend's private inet table.
func (f *NFT) tableRef() *nftables.Table {
return &nftables.Table{Family: nftables.TableFamilyINet, Name: f.table}
}
// chainRef returns the netlink handle for one of the private table's chains.
// Only the name and table identify a chain for rule operations, so the hook
// properties are left unset here; ensureTable owns their definition.
func (f *NFT) chainRef(name string) *nftables.Chain {
return &nftables.Chain{Name: name, Table: f.tableRef()}
}
// nftFilterChains lists the private table's filter base chains, in the order a
// read enumerates them.
var nftFilterChains = []string{"input", "output", "forward"}
// nftNATChains lists the private table's nat base chains, in read order.
var nftNATChains = []string{"prerouting", "postrouting"}
// directionForChain returns the rule direction a filter base-chain name maps
// to (the inverse of chainForDirection).
func (f *NFT) directionForChain(chain string) Direction {
switch chain {
case "output":
return DirOutput
case "forward":
return DirForward
}
return DirInput
}
// chainForDirection returns the filter base-chain name a rule of the given
// direction lives in.
func (f *NFT) chainForDirection(d Direction) string {
switch d {
case DirOutput:
return "output"
case DirForward:
return "forward"
}
return "input"
}
// familyName renders a netlink table family as the keyword nft prints, so a
// foreign rule's recorded table reads the way an operator would write it
// ("inet filter"). The container-runtime table check parses this same form.
func (f *NFT) familyName(fam nftables.TableFamily) string {
switch fam {
case nftables.TableFamilyIPv4:
return "ip"
case nftables.TableFamilyIPv6:
return "ip6"
case nftables.TableFamilyINet:
return "inet"
case nftables.TableFamilyARP:
return "arp"
case nftables.TableFamilyBridge:
return "bridge"
case nftables.TableFamilyNetdev:
return "netdev"
}
return "unknown"
}
// familyForTable returns the family every row of a single-family table matches.
// An ip or ip6 table is the family qualifier for its own rules, so those rows
// carry no nfproto match of their own; an inet table (this backend's own, and
// the arp/bridge/netdev families) settles nothing, reporting false.
func (f *NFT) familyForTable(tbl *nftables.Table) (Family, bool) {
if tbl == nil {
return FamilyAny, false
}
switch tbl.Family {
case nftables.TableFamilyIPv4:
return IPv4, true
case nftables.TableFamilyIPv6:
return IPv6, true
}
return FamilyAny, false
}
// isNotExist reports whether a netlink error means the object is simply absent,
// which every read path treats as "nothing there yet" rather than a failure.
func (f *NFT) isNotExist(err error) bool {
return errors.Is(err, unix.ENOENT)
}
// -----------------------------------------------------------------------------
// Expression encoding
// -----------------------------------------------------------------------------
// nftAnonSet is an anonymous constant set an encoded rule references. It must be
// created in the same netlink batch as the rule, before the rule's own message.
type nftAnonSet struct {
set *nftables.Set
elements []nftables.SetElement
}
// nftEncoded is a marshalled rule: the chain it belongs in, the expression list
// the kernel stores, the comment as nftables user data, any anonymous sets the
// expressions reference, and the named dynamic set a per-source connection limit
// counts in. Encoding is pure — nothing here touches the kernel — so a caller
// can inspect the result before committing it.
type nftEncoded struct {
chain string
exprs []expr.Any
userData []byte
anonSets []nftAnonSet
// meterSet is the named dynamic set a per-source connection-limit rule
// counts in; it outlives the batch and is created separately.
meterSet *nftables.Set
}
// ifnameBytes renders an interface name as the comparison operand nftables
// expects: a trailing '*' is a prefix match, compared against just the leading
// characters, and anything else is an exact match against the fixed 16-byte
// (IFNAMSIZ) NUL-padded buffer.
func (f *NFT) ifnameBytes(name string) []byte {
if strings.HasSuffix(name, "*") {
return []byte(strings.TrimSuffix(name, "*"))
}
b := make([]byte, unix.IFNAMSIZ)
copy(b, name)
return b
}
// ifnameString reverses ifnameBytes: a full-width operand is an exact name with
// its NUL padding trimmed, a short one a prefix match rendered back with '*'.
func (f *NFT) ifnameString(data []byte) string {
if len(data) == unix.IFNAMSIZ {
return string(bytes.TrimRight(data, "\x00"))
}
return string(bytes.TrimRight(data, "\x00")) + "*"
}
// nfprotoByte returns the NFPROTO constant a family pins to.
func (f *NFT) nfprotoByte(fam Family) byte {
if fam == IPv6 {
return unix.NFPROTO_IPV6
}
return unix.NFPROTO_IPV4
}
// familyForNFProto reverses nfprotoByte.
func (f *NFT) familyForNFProto(b byte) (Family, bool) {
switch b {
case unix.NFPROTO_IPV4:
return IPv4, true
case unix.NFPROTO_IPV6:
return IPv6, true
}
return FamilyAny, false
}
// addrField describes where a source or destination address sits in the network
// header of the given family, which is what a payload load must name.
func addrField(fam Family, source bool) (offset, length uint32) {
if fam == IPv6 {
if source {
return 8, 16
}
return 24, 16
}
if source {
return 12, 4
}
return 16, 4
}
// ipProtoByte returns the IP protocol number nftables matches a protocol by.
func (f *NFT) ipProtoByte(p Protocol) (byte, bool) {
switch p {
case TCP:
return unix.IPPROTO_TCP, true
case UDP:
return unix.IPPROTO_UDP, true
case ICMP:
return unix.IPPROTO_ICMP, true
case ICMPv6:
return unix.IPPROTO_ICMPV6, true
case SCTP:
return unix.IPPROTO_SCTP, true
case GRE:
return unix.IPPROTO_GRE, true
case ESP:
return unix.IPPROTO_ESP, true
case AH:
return unix.IPPROTO_AH, true
}
return 0, false
}
// protocolForByte reverses ipProtoByte, returning ProtocolAny for a protocol the
// Rule model has no field for.
func (f *NFT) protocolForByte(b byte) Protocol {
switch b {
case unix.IPPROTO_TCP:
return TCP
case unix.IPPROTO_UDP:
return UDP
case unix.IPPROTO_ICMP:
return ICMP
case unix.IPPROTO_ICMPV6:
return ICMPv6
case unix.IPPROTO_SCTP:
return SCTP
case unix.IPPROTO_GRE:
return GRE
case unix.IPPROTO_ESP:
return ESP
case unix.IPPROTO_AH:
return AH
}
return ProtocolAny
}
// nftConnStates maps the library's connection-state bits onto the kernel's ct
// state bits, which do not share an ordering: the library counts from new,
// netfilter from invalid.
var nftConnStates = []struct {
state ConnState
bit uint32
}{
{StateNew, 0x08},
{StateEstablished, 0x02},
{StateRelated, 0x04},
{StateInvalid, 0x01},
}
// ctStateMask renders a connection-state set as the kernel bitmask a ct state
// match tests against.
func (f *NFT) ctStateMask(s ConnState) uint32 {
var mask uint32
for _, cs := range nftConnStates {
if s&cs.state != 0 {
mask |= cs.bit
}
}
return mask
}
// connStateForMask reverses ctStateMask, reporting false when the mask carries a
// state the Rule model cannot hold (untracked, say), so the row stays opaque
// rather than being silently narrowed to the states that did map.
func (f *NFT) connStateForMask(mask uint32) (ConnState, bool) {
var state ConnState
var covered uint32
for _, cs := range nftConnStates {
if mask&cs.bit != 0 {
state |= cs.state
covered |= cs.bit
}
}
if covered != mask {
return 0, false
}
return state, true
}
// rateUnitSeconds returns the number of seconds a rate unit spans, which is how
// nftables expresses a limit's interval.
func rateUnitSeconds(u RateUnit) uint64 {
switch u {
case PerMinute:
return 60
case PerHour:
return 3600
case PerDay:
return 86400
}
return 1
}
// rateUnitForSeconds reverses rateUnitSeconds.
func (f *NFT) rateUnitForSeconds(s uint64) (RateUnit, bool) {
switch s {
case 1:
return PerSecond, true
case 60:
return PerMinute, true
case 3600:
return PerHour, true
case 86400:
return PerDay, true
}
return PerSecond, false
}
// addrBytes renders an address or CIDR as the network-header operand a match
// compares against: the raw address bytes plus, for a prefix, the mask to apply
// first. A host address yields a nil mask.
func (f *NFT) addrBytes(fam Family, addr string) (value, mask []byte, err error) {
width := 4
if fam == IPv6 {
width = 16
}
if _, ipnet, cerr := net.ParseCIDR(addr); cerr == nil {
ones, bits := ipnet.Mask.Size()
if bits/8 != width {
return nil, nil, fmt.Errorf("address %q does not match the rule's family", addr)
}
network := ipnet.IP.To16()
if width == 4 {
network = ipnet.IP.To4()
}
if ones == bits {
// A host prefix is the address itself; no masking needed.
return network, nil, nil
}
return network, ipnet.Mask, nil
}
ip := net.ParseIP(addr)
if ip == nil {
return nil, nil, fmt.Errorf("invalid address %q", addr)
}
if width == 4 {
v4 := ip.To4()
if v4 == nil {
return nil, nil, fmt.Errorf("address %q is not IPv4", addr)
}
return v4, nil, nil
}
if ip.To4() != nil {
return nil, nil, fmt.Errorf("address %q is not IPv6", addr)
}
return ip.To16(), nil, nil
}
// cmpOp returns the comparison a match uses, inverted when the value was negated.
func (f *NFT) cmpOp(neg bool) expr.CmpOp {
if neg {
return expr.CmpOpNeq
}
return expr.CmpOpEq
}
// encodeAddr appends the expressions matching a source or destination address.
// A named set is referenced by a lookup; an address or CIDR loads the header
// field and compares it, masking first when the value is a prefix.
func (f *NFT) encodeAddr(exprs []expr.Any, fam Family, value string, source bool) ([]expr.Any, error) {
neg, bare := splitAddrNeg(strings.TrimSpace(value))
offset, length := addrField(fam, source)
exprs = append(exprs, &expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseNetworkHeader,
Offset: offset,
Len: length,
})
if isSetRef(value) {
exprs = append(exprs, &expr.Lookup{
SourceRegister: 1,
SetName: bare,
Invert: neg,
})
return exprs, nil
}
val, mask, err := f.addrBytes(fam, bare)
if err != nil {
return nil, err
}
if mask != nil {
// A prefix match is the masked field compared against the network
// address. nft shortens a byte-aligned prefix to a narrower payload load
// instead; both forms are accepted on read, and this one covers every
// prefix length with a single shape.
exprs = append(exprs, &expr.Bitwise{
SourceRegister: 1,
DestRegister: 1,
Len: uint32(len(val)),
Mask: mask,
Xor: make([]byte, len(val)),
})
}
exprs = append(exprs, &expr.Cmp{Op: f.cmpOp(neg), Register: 1, Data: val})
return exprs, nil
}
// portElements renders port ranges as the elements of an anonymous set. An
// interval element is the inclusive start plus an end marker at the exclusive
// upper bound, which is how nftables stores a span.
func (f *NFT) portElements(specs []PortRange, interval bool) []nftables.SetElement {
var elems []nftables.SetElement
for _, p := range specs {
elems = append(elems, nftables.SetElement{Key: binaryutil.BigEndian.PutUint16(p.Start)})
if interval {
elems = append(elems, nftables.SetElement{
Key: binaryutil.BigEndian.PutUint16(p.End + 1),
IntervalEnd: true,
})
}
}
return elems
}
// encodePorts appends the expressions matching a source or destination port. A
// single discrete port compares directly, a single span is a range, and a list
// becomes an anonymous set (an interval set when any member is a span).
func (f *NFT) encodePorts(enc *nftEncoded, specs []PortRange, source bool) {
offset := uint32(2)
if source {
offset = 0
}
enc.exprs = append(enc.exprs, &expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseTransportHeader,
Offset: offset,
Len: 2,
})
if len(specs) == 1 {
p := specs[0]
if p.Start == p.End {
enc.exprs = append(enc.exprs, &expr.Cmp{
Op: expr.CmpOpEq, Register: 1, Data: binaryutil.BigEndian.PutUint16(p.Start),
})
return
}
enc.exprs = append(enc.exprs, &expr.Range{
Op: expr.CmpOpEq,
Register: 1,
FromData: binaryutil.BigEndian.PutUint16(p.Start),
ToData: binaryutil.BigEndian.PutUint16(p.End),
})
return
}
interval := false
for _, p := range specs {
if p.Start != p.End {
interval = true
break
}
}
set := &nftables.Set{
Table: f.tableRef(),
ID: f.nextSetID(),
Name: "__set%d",
Anonymous: true,
Constant: true,
Interval: interval,
KeyType: nftables.TypeInetService,
}
enc.anonSets = append(enc.anonSets, nftAnonSet{set: set, elements: f.portElements(specs, interval)})
enc.exprs = append(enc.exprs, &expr.Lookup{SourceRegister: 1, SetName: set.Name, SetID: set.ID})
}
// encodeTCPUDP appends the both-transports protocol match: an anonymous set of
// the two protocol numbers, which keeps a TCPUDP rule a single nftables row.
func (f *NFT) encodeTCPUDP(enc *nftEncoded) {
set := &nftables.Set{
Table: f.tableRef(),
ID: f.nextSetID(),
Name: "__set%d",
Anonymous: true,
Constant: true,
KeyType: nftables.TypeInetProto,
}
enc.anonSets = append(enc.anonSets, nftAnonSet{set: set, elements: []nftables.SetElement{
{Key: []byte{unix.IPPROTO_TCP}},
{Key: []byte{unix.IPPROTO_UDP}},
}})
enc.exprs = append(enc.exprs,
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
&expr.Lookup{SourceRegister: 1, SetName: set.Name, SetID: set.ID},
)
}
// validateRule reports whether the rule is valid for nftables, applying the
// universal Rule.validate and then this encoder's shape constraints. The
// encoding entry points run it before marshalling; MarshalRule itself is a pure
// encoder. Unlike the other backends it does not reject TCPUDP: nft carries both
// transports in one row.
func (f *NFT) validateRule(r *Rule) error {
if err := r.validate(); err != nil {
return err
}
// A comment rides in the rule's user data and a log prefix in the log
// expression; both are length-capped by nftables and the kernel.
if len(r.Comment) > nftCommentMax {
return fmt.Errorf("an nftables comment may not exceed %d bytes", nftCommentMax)
}
if len(r.LogPrefix) > nftLogPrefixMax {
return fmt.Errorf("an nftables log prefix may not exceed %d bytes", nftLogPrefixMax)
}
// A per-source connection limit counts in a family-typed meter (its key is
// the source address field), so a FamilyAny rule must be expanded to concrete
// families by the caller before reaching the marshaller.
if r.ConnLimit != nil && r.ConnLimit.PerSource && r.impliedFamily() == FamilyAny {
return fmt.Errorf("a per-source connection limit counts in a family-typed meter; the caller must expand the rule to concrete families first")
}
// A connection-state match must name states nftables knows.
if r.State != 0 && f.ctStateMask(r.State) == 0 {
return fmt.Errorf("no valid connection state was provided")
}
// The rule must carry a valid verdict.
switch r.Action {
case Accept, Drop, Reject:
default:
return fmt.Errorf("no valid action was provided")
}
return nil
}
// MarshalRule encodes a filter rule as the nftables expression list the kernel
// stores, plus the anonymous sets and meter set it references. It is a pure
// encoder: callers run validateRule first.
func (f *NFT) MarshalRule(r *Rule) (*nftEncoded, error) {
enc := &nftEncoded{chain: f.chainForDirection(r.Direction)}
fam := r.impliedFamily()
// Interface match.
if r.InInterface != "" {
enc.exprs = append(enc.exprs,
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: f.ifnameBytes(r.InInterface)},
)
}
if r.OutInterface != "" {
enc.exprs = append(enc.exprs,
&expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: f.ifnameBytes(r.OutInterface)},
)
}
// Family pin. In an inet table a network-header offset means different fields
// in the two families, so every rule that resolves to a concrete family states
// it — not only the address-less ones. Without the guard an IPv4 source-address
// load also matches inside an IPv6 source address.
if fam != FamilyAny {
enc.exprs = append(enc.exprs,
&expr.Meta{Key: expr.MetaKeyNFPROTO, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{f.nfprotoByte(fam)}},
)
}
// Address matches, honoring negation and named-set references.
var err error
if r.Source != "" {
if enc.exprs, err = f.encodeAddr(enc.exprs, fam, r.Source, true); err != nil {
return nil, err
}
}
if r.Destination != "" {
if enc.exprs, err = f.encodeAddr(enc.exprs, fam, r.Destination, false); err != nil {
return nil, err
}
}
// Protocol and port matches. A TCPUDP rule pins both transports with an
// anonymous set and matches its ports through the shared transport-header
// offsets, which is valid precisely because l4proto is constrained to
// port-carrying protocols; that keeps the rule a single row needing no
// fan-out. Every other protocol names itself before its ports.
srcSpecs := r.SourcePortSpecs()
hasPorts := r.HasPorts() || len(srcSpecs) > 0
switch {
case r.Proto == TCPUDP:
f.encodeTCPUDP(enc)
case r.Proto != ProtocolAny:
pb, ok := f.ipProtoByte(r.Proto)
if !ok {
return nil, fmt.Errorf("unsupported protocol %s", r.Proto)
}
enc.exprs = append(enc.exprs,
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{pb}},
)
case hasPorts:
// A port match loads the transport header, so it needs a protocol to load
// it from. Rule.validate rejects this at the entry points; the encoder
// repeats it because the internal split paths marshal directly.
return nil, fmt.Errorf("a port match requires a protocol")
}
if r.HasPorts() {
f.encodePorts(enc, r.PortSpecs(), false)
}
if len(srcSpecs) > 0 {
f.encodePorts(enc, srcSpecs, true)
}
if r.Proto.IsICMP() && r.ICMPType != nil {
// The message type is the first byte of the ICMP header.
enc.exprs = append(enc.exprs,
&expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseTransportHeader,
Offset: 0,
Len: 1,
},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{*r.ICMPType}},
)
}
// Connection-tracking state: the state register is masked to the requested
// states and matches when any of them is set.
if r.State != 0 {
enc.exprs = append(enc.exprs,
&expr.Ct{Key: expr.CtKeySTATE, Register: 1},
&expr.Bitwise{
SourceRegister: 1,
DestRegister: 1,
Len: 4,
Mask: binaryutil.NativeEndian.PutUint32(f.ctStateMask(r.State)),
Xor: []byte{0, 0, 0, 0},
},
&expr.Cmp{Op: expr.CmpOpNeq, Register: 1, Data: []byte{0, 0, 0, 0}},
)
}
// Rate limit: the statement matches only while under the rate, so over-rate
// packets fall through to later rules rather than taking this rule's verdict.
if r.RateLimit != nil {
burst := uint32(r.RateLimit.Burst)
if burst == 0 {
burst = netfilterDefaultBurst
}
enc.exprs = append(enc.exprs, &expr.Limit{
Type: expr.LimitTypePkts,
Rate: uint64(r.RateLimit.Rate),
Unit: expr.LimitTime(rateUnitSeconds(r.RateLimit.Unit)),
Burst: burst,
})
}
// Connection limit. Per-source counting keys the count on the source address
// in a named dynamic set — the set is family-typed, so a FamilyAny rule was
// fanned out into one row per family before reaching here.
if r.ConnLimit != nil {
if r.ConnLimit.PerSource {
name := f.meterName(enc.chain, r)
keyType := nftables.TypeIPAddr
if fam == IPv6 {
keyType = nftables.TypeIP6Addr
}
enc.meterSet = &nftables.Set{
Table: f.tableRef(),
Name: name,
KeyType: keyType,
Dynamic: true,
Size: nftMeterSetSize,
}
offset, length := addrField(fam, true)
enc.exprs = append(enc.exprs,
&expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseNetworkHeader,
Offset: offset,
Len: length,
},
&expr.Dynset{
SrcRegKey: 1,
SetName: name,
Operation: uint32(unix.NFT_DYNSET_OP_ADD),
Exprs: []expr.Any{
&expr.Connlimit{Count: uint32(r.ConnLimit.Count), Flags: expr.NFT_CONNLIMIT_F_INV},
},
},
)
} else {
enc.exprs = append(enc.exprs, &expr.Connlimit{
Count: uint32(r.ConnLimit.Count),
Flags: expr.NFT_CONNLIMIT_F_INV,
})
}
}
// Logging, emitted just before the verdict so the packet is logged and then
// the action is applied.
if r.Log {
lg := &expr.Log{Level: expr.LogLevelWarning}
if r.LogPrefix != "" {
lg.Key = 1 << unix.NFTA_LOG_PREFIX
lg.Data = []byte(r.LogPrefix)
}
enc.exprs = append(enc.exprs, lg)
}
// A counter so GetRules can report per-rule packet/byte statistics. The
// counter has no effect on matching and is ignored when comparing rules.
enc.exprs = append(enc.exprs, &expr.Counter{})
// Verdict. validateRule has already rejected an invalid action.
switch r.Action {
case Accept:
enc.exprs = append(enc.exprs, &expr.Verdict{Kind: expr.VerdictAccept})
case Drop:
enc.exprs = append(enc.exprs, &expr.Verdict{Kind: expr.VerdictDrop})
case Reject:
// The inet table's reject default: an ICMPX port-unreachable, which the
// kernel renders per family.
enc.exprs = append(enc.exprs, &expr.Reject{
Type: unix.NFT_REJECT_ICMPX_UNREACH,
Code: unix.NFT_REJECT_ICMPX_PORT_UNREACH,
})
}
// An optional user comment, stored as nftables user data. Unlike the textual
// interface this has no quoting, so any comment within the length cap round
// trips verbatim.
if r.Comment != "" {
enc.userData = userdata.AppendString(nil, userdata.TypeComment, r.Comment)
}
return enc, nil
}
// validateNAT reports whether the NAT rule is valid for nftables, applying the
// universal NATRule.validate and then this encoder's constraints. The encoding
// entry points run it before marshalling; MarshalNATRule is a pure encoder.
func (f *NFT) validateNAT(r *NATRule) error {
if err := r.validate(); err != nil {
return err
}
// nft's snat expression maps only to an address; a source-port translation has
// no representation here (iptables emits it as --to-source addr:port).
if r.Kind == SNAT && r.ToPort != 0 {
return fmt.Errorf("nftables snat does not translate the source port: %w", ErrUnsupportedNAT)
}
return nil
}
// MarshalNATRule encodes a NAT rule as an expression list, returning it in the
// chain it belongs in (prerouting for destination NAT, postrouting for source
// NAT). It is a pure encoder: callers run validateNAT first.
func (f *NFT) MarshalNATRule(r *NATRule) (*nftEncoded, error) {
enc := &nftEncoded{chain: "prerouting"}
if r.Kind.isSource() {
enc.chain = "postrouting"
}
fam := r.impliedFamily()
// Interface, bound to the NAT direction: outbound for source NAT, inbound
// for destination NAT.
if r.Interface != "" {
key := expr.MetaKeyIIFNAME
if r.Kind.isSource() {
key = expr.MetaKeyOIFNAME
}
enc.exprs = append(enc.exprs,
&expr.Meta{Key: key, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: f.ifnameBytes(r.Interface)},
)
}
// Family pin, for the same reason as a filter rule.
if fam != FamilyAny {
enc.exprs = append(enc.exprs,
&expr.Meta{Key: expr.MetaKeyNFPROTO, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{f.nfprotoByte(fam)}},
)
}
var err error
if r.Source != "" {
if enc.exprs, err = f.encodeAddr(enc.exprs, fam, r.Source, true); err != nil {
return nil, err
}
}
if r.Destination != "" {
if enc.exprs, err = f.encodeAddr(enc.exprs, fam, r.Destination, false); err != nil {
return nil, err
}
}
if r.Proto != ProtocolAny {
pb, ok := f.ipProtoByte(r.Proto)
if !ok {
return nil, fmt.Errorf("unsupported protocol %s", r.Proto)
}
enc.exprs = append(enc.exprs,
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{pb}},
)
}
if r.HasPorts() {
f.encodePorts(enc, r.PortSpecs(), false)
}
// The translation. An address goes into register 1 and a port into register
// 2, which the nat expression then names; redirect takes only a port and
// reads it from register 1.
switch r.Kind {
case DNAT, SNAT:
natType := expr.NATTypeDestNAT
if r.Kind == SNAT {
natType = expr.NATTypeSourceNAT
}
n := &expr.NAT{Type: natType, Family: uint32(f.nfprotoByte(fam))}
if r.ToAddress != "" {
val, mask, aerr := f.addrBytes(fam, r.ToAddress)
if aerr != nil {
return nil, aerr
}
if mask != nil {
return nil, fmt.Errorf("a nat translation target must be a single address, not a prefix")
}
enc.exprs = append(enc.exprs, &expr.Immediate{Register: 1, Data: val})
n.RegAddrMin, n.RegAddrMax = 1, 1
}
if r.ToPort != 0 {
enc.exprs = append(enc.exprs, &expr.Immediate{
Register: 2, Data: binaryutil.BigEndian.PutUint16(r.ToPort),
})
n.RegProtoMin, n.RegProtoMax = 2, 2
}
enc.exprs = append(enc.exprs, n)
case Redirect:
rd := &expr.Redir{}
if r.ToPort != 0 {
enc.exprs = append(enc.exprs, &expr.Immediate{
Register: 1, Data: binaryutil.BigEndian.PutUint16(r.ToPort),
})
rd.RegisterProtoMin, rd.RegisterProtoMax = 1, 1
rd.Flags = unix.NF_NAT_RANGE_PROTO_SPECIFIED
}
enc.exprs = append(enc.exprs, rd)
case Masquerade:
enc.exprs = append(enc.exprs, &expr.Masq{})
}
return enc, nil
}
// meterName derives the dynamic-set name a per-source connection-limit rule
// counts in. The name is a hash of the rule's identity (chain, family, match,
// limit and verdict) so two distinct per-source rules never share counting
// state, while a re-add — or a split's re-add — of the same rule deterministically
// reuses its set. The comment is excluded, as it is not part of rule identity.
func (f *NFT) meterName(chain string, r *Rule) string {
h := fnv.New64a()
_, _ = fmt.Fprintf(h, "%s|%d|%d|%s|%s|%d|%s|%s|%v|%d|%d|%d",
chain, r.impliedFamily(), r.Proto, r.Source, r.Destination,
r.State, FormatPortRanges(r.PortSpecs(), ","), FormatPortRanges(r.SourcePortSpecs(), ","),
r.Log, r.Action, r.ConnLimit.Count, r.Priority)
return fmt.Sprintf("cl%016x", h.Sum64())
}
// perSourceFamilySplit reports whether a rule must be fanned out into one row
// per family before marshalling: a per-source connection limit counts in a
// family-typed dynamic set, so a FamilyAny rule has no single nftables row —
// the family analog of the DirAny fan-out.
func (f *NFT) perSourceFamilySplit(r *Rule) bool {
return r.perSourceLimited() && r.impliedFamily() == FamilyAny
}
// -----------------------------------------------------------------------------
// Expression decoding
// -----------------------------------------------------------------------------
// nftSetContents is a set's definition together with its elements, which a
// lookup expression must be resolved against to recover the values it matches.
type nftSetContents struct {
set *nftables.Set
elements []nftables.SetElement
}
// nftSetReader resolves the sets a rule's lookups reference, caching each read
// so decoding a chain full of set-referencing rules costs one dump per set.
type nftSetReader struct {
conn *nftables.Conn
cache map[string]*nftSetContents
// staged holds sets that exist only in an in-flight batch, keyed by the
// transaction-local identifier a lookup names them by. Every anonymous set
// shares the same placeholder name until the kernel resolves it, so an
// encoding can only be decoded back through its identifiers.
staged map[uint32]*nftSetContents
}
// newSetReader returns a set resolver bound to a netlink connection.
func newSetReader(c *nftables.Conn) *nftSetReader {
return &nftSetReader{conn: c, cache: make(map[string]*nftSetContents)}
}
// newStagedSetReader returns a resolver over the anonymous sets encoded rules
// carry, so an encoding can be decoded back without ever reaching the kernel.
func newStagedSetReader(encs ...*nftEncoded) *nftSetReader {
s := &nftSetReader{staged: make(map[uint32]*nftSetContents)}
for _, enc := range encs {
for _, as := range enc.anonSets {
s.staged[as.set.ID] = &nftSetContents{set: as.set, elements: as.elements}
}
}
return s
}
// resolve reads the set a lookup names. A set staged in the current batch is
// named by its transaction-local identifier; one already in the ruleset is named
// by the name the kernel assigned it.
func (s *nftSetReader) resolve(tbl *nftables.Table, lk *expr.Lookup) (*nftSetContents, error) {
if lk.SetID != 0 {
if sc, ok := s.staged[lk.SetID]; ok {
return sc, nil
}
}
if s.conn == nil {
return nil, fmt.Errorf("set %q is not available", lk.SetName)
}
key := fmt.Sprintf("%d|%s|%s", tbl.Family, tbl.Name, lk.SetName)
if c, ok := s.cache[key]; ok {
return c, nil
}
set, err := s.conn.GetSetByName(tbl, lk.SetName)
if err != nil {
return nil, err
}
elems, err := s.conn.GetSetElements(set)
if err != nil {
return nil, err
}
c := &nftSetContents{set: set, elements: elems}
s.cache[key] = c
return c, nil
}
// portRangesFromSet reconstructs the port ranges a set holds. A plain set lists
// discrete ports; an interval set lists boundary markers, each span being an
// inclusive start followed by an end marker at the exclusive upper bound.
func (f *NFT) portRangesFromSet(sc *nftSetContents) ([]PortRange, error) {
type elem struct {
port uint16
end bool
}
var elems []elem
for _, e := range sc.elements {
if len(e.Key) != 2 {
return nil, fmt.Errorf("unsupported port set element width %d", len(e.Key))
}
elems = append(elems, elem{port: binaryutil.BigEndian.Uint16(e.Key), end: e.IntervalEnd})
}
sort.Slice(elems, func(i, j int) bool {
if elems[i].port == elems[j].port {
return !elems[i].end && elems[j].end
}
return elems[i].port < elems[j].port
})
var specs []PortRange
if !sc.set.Interval {
for _, e := range elems {
specs = append(specs, PortRange{Start: e.port, End: e.port})
}
return specs, nil
}
open := false
var start uint16
for _, e := range elems {
if !e.end {
start, open = e.port, true
continue
}
if !open {
// A leading end marker closes the span below the first element, which
// carries no range of its own.
continue
}
if e.port == 0 {
return nil, fmt.Errorf("invalid interval end 0 in port set")
}
specs = append(specs, PortRange{Start: start, End: e.port - 1})
open = false
}
if open {
// An unterminated span runs to the top of the port space.
specs = append(specs, PortRange{Start: start, End: 65535})
}
return specs, nil
}
// protocolFromSet decodes an l4proto lookup. The only set this backend writes,
// and the only one a single Proto field can hold, is the both-transports pair;
// anything else belongs to a foreign rule whose coverage the model cannot carry,
// so it is rejected rather than narrowed to one member.
func (f *NFT) protocolFromSet(sc *nftSetContents) (Protocol, error) {
if len(sc.elements) != 2 {
return ProtocolAny, fmt.Errorf("unsupported l4proto set of %d members", len(sc.elements))
}
var got [2]Protocol
for i, e := range sc.elements {
if len(e.Key) != 1 {
return ProtocolAny, fmt.Errorf("unsupported l4proto set element width %d", len(e.Key))
}
got[i] = f.protocolForByte(e.Key[0])
}
if (got[0] == TCP && got[1] == UDP) || (got[0] == UDP && got[1] == TCP) {
return TCPUDP, nil
}
return ProtocolAny, fmt.Errorf("unsupported l4proto set")
}
// maskPrefixLen returns the prefix length a contiguous network mask represents,
// reporting false for a non-contiguous mask the address model cannot render.
func (f *NFT) maskPrefixLen(mask []byte) (int, bool) {
ones, bits := net.IPMask(mask).Size()
if bits == 0 {
return 0, false
}
return ones, true
}
// addrFromPayload reconstructs the address or CIDR a network-header match names.
// It accepts both shapes nftables stores: a full-width load with an optional
// mask, and the shortened load nft emits for a byte-aligned prefix.
func (f *NFT) addrFromPayload(fam Family, p *expr.Payload, mask, data []byte) (string, error) {
width := 4
if fam == IPv6 {
width = 16
}
if int(p.Len) > width || len(data) != int(p.Len) {
return "", fmt.Errorf("unsupported address match width %d", p.Len)
}
full := make([]byte, width)
copy(full, data)
ip := net.IP(full)
switch {
case int(p.Len) < width:
// A shortened load compares only the leading bytes: a byte-aligned prefix.
return (&net.IPNet{IP: ip, Mask: net.CIDRMask(int(p.Len)*8, width*8)}).String(), nil
case mask != nil:
ones, ok := f.maskPrefixLen(mask)
if !ok {
return "", fmt.Errorf("unsupported non-contiguous address mask")
}
if ones == width*8 {
return ip.String(), nil
}
return (&net.IPNet{IP: ip, Mask: net.CIDRMask(ones, width*8)}).String(), nil
default:
return ip.String(), nil
}
}
// nftDecoder walks a rule's expression list, pairing each value-producing
// expression (a meta, payload or ct load) with the test that follows it.
type nftDecoder struct {
f *NFT
sets *nftSetReader
tbl *nftables.Table
fam Family
// load is the most recent value-producing expression, awaiting its test.
load expr.Any
// mask is a pending bitwise mask applied to the loaded value.
mask []byte
}
// setPort stores a decoded port match, keeping the single-port form for a lone
// discrete port so it round-trips against rules built that way.
func (f *NFT) setPort(r *Rule, specs []PortRange, source bool) {
single := len(specs) == 1 && specs[0].Start == specs[0].End
if source {
if single {
r.SourcePort = specs[0].Start
} else {
r.SourcePorts = specs
}
return
}
if single {
r.Port = specs[0].Start
} else {
r.Ports = specs
}
}
// applyCmp folds a comparison into the rule, interpreting it against the value
// the preceding load produced.
func (d *nftDecoder) applyCmp(r *Rule, c *expr.Cmp) error {
neg := c.Op == expr.CmpOpNeq
switch l := d.load.(type) {
case *expr.Meta:
switch l.Key {
case expr.MetaKeyNFPROTO:
if len(c.Data) != 1 {
return fmt.Errorf("unsupported nfproto match")
}
fam, ok := d.f.familyForNFProto(c.Data[0])
if !ok {
return fmt.Errorf("unsupported nfproto %d", c.Data[0])
}
r.Family, d.fam = fam, fam
case expr.MetaKeyL4PROTO:
if len(c.Data) != 1 {
return fmt.Errorf("unsupported l4proto match")
}
p := d.f.protocolForByte(c.Data[0])
if p == ProtocolAny {
return fmt.Errorf("unsupported l4proto %d", c.Data[0])
}
r.Proto = p
case expr.MetaKeyIIFNAME:
r.InInterface = d.f.ifnameString(c.Data)
case expr.MetaKeyOIFNAME:
r.OutInterface = d.f.ifnameString(c.Data)
default:
return fmt.Errorf("unsupported meta key %d", l.Key)
}
case *expr.Payload:
switch l.Base {
case expr.PayloadBaseNetworkHeader:
if d.fam == FamilyAny {
// Without a family guard a network-header offset is ambiguous
// between the two families in an inet table.
return fmt.Errorf("address match without a family qualifier")
}
soff, _ := addrField(d.fam, true)
doff, _ := addrField(d.fam, false)
addr, aerr := d.f.addrFromPayload(d.fam, l, d.mask, c.Data)
if aerr != nil {
return aerr
}
if neg {
addr = "!" + addr
}
switch l.Offset {
case soff:
r.Source = addr
case doff:
r.Destination = addr
default:
return fmt.Errorf("unsupported network header offset %d", l.Offset)
}
case expr.PayloadBaseTransportHeader:
switch {
case l.Offset == 2 && l.Len == 2:
d.f.setPort(r, []PortRange{{Start: binaryutil.BigEndian.Uint16(c.Data), End: binaryutil.BigEndian.Uint16(c.Data)}}, false)
case l.Offset == 0 && l.Len == 2:
d.f.setPort(r, []PortRange{{Start: binaryutil.BigEndian.Uint16(c.Data), End: binaryutil.BigEndian.Uint16(c.Data)}}, true)
case l.Offset == 0 && l.Len == 1:
// The ICMP message type.
if !r.Proto.IsICMP() {
return fmt.Errorf("an icmp type match requires an icmp protocol")
}
r.ICMPType = Ptr(c.Data[0])
default:
return fmt.Errorf("unsupported transport header offset %d width %d", l.Offset, l.Len)
}
default:
return fmt.Errorf("unsupported payload base %d", l.Base)
}
case *expr.Ct:
if l.Key != expr.CtKeySTATE {
return fmt.Errorf("unsupported ct key %d", l.Key)
}
if d.mask == nil || len(d.mask) != 4 || c.Op != expr.CmpOpNeq {
return fmt.Errorf("unsupported ct state match")
}
state, ok := d.f.connStateForMask(binaryutil.NativeEndian.Uint32(d.mask))
if !ok {
return fmt.Errorf("unsupported ct state mask")
}
r.State = state
default:
return fmt.Errorf("comparison without a value to compare")
}
return nil
}
// applyLookup folds a set lookup into the rule.
func (d *nftDecoder) applyLookup(r *Rule, lk *expr.Lookup) error {
switch l := d.load.(type) {
case *expr.Meta:
if l.Key != expr.MetaKeyL4PROTO {
return fmt.Errorf("unsupported meta set lookup")
}
sc, err := d.sets.resolve(d.tbl, lk)
if err != nil {
return err
}
p, err := d.f.protocolFromSet(sc)
if err != nil {
return err
}
r.Proto = p
case *expr.Payload:
switch l.Base {
case expr.PayloadBaseNetworkHeader:
if d.fam == FamilyAny {
return fmt.Errorf("address set lookup without a family qualifier")
}
soff, _ := addrField(d.fam, true)
doff, _ := addrField(d.fam, false)
name := lk.SetName
if lk.Invert {
name = "!" + name
}
switch l.Offset {
case soff:
r.Source = name
case doff:
r.Destination = name
default:
return fmt.Errorf("unsupported network header offset %d", l.Offset)
}
case expr.PayloadBaseTransportHeader:
if l.Len != 2 || (l.Offset != 0 && l.Offset != 2) {
return fmt.Errorf("unsupported port set lookup")
}
sc, err := d.sets.resolve(d.tbl, lk)
if err != nil {
return err
}
specs, err := d.f.portRangesFromSet(sc)
if err != nil {
return err
}
if len(specs) == 0 {
return fmt.Errorf("empty port set")
}
d.f.setPort(r, specs, l.Offset == 0)
default:
return fmt.Errorf("unsupported payload base %d", l.Base)
}
default:
return fmt.Errorf("set lookup without a value to look up")
}
return nil
}
// applyRange folds a range test into the rule; nftables uses one for a single
// port span.
func (d *nftDecoder) applyRange(r *Rule, rg *expr.Range) error {
l, ok := d.load.(*expr.Payload)
if !ok || l.Base != expr.PayloadBaseTransportHeader || l.Len != 2 {
return fmt.Errorf("unsupported range match")
}
if l.Offset != 0 && l.Offset != 2 {
return fmt.Errorf("unsupported range offset %d", l.Offset)
}
if rg.Op != expr.CmpOpEq || len(rg.FromData) != 2 || len(rg.ToData) != 2 {
return fmt.Errorf("unsupported range match")
}
d.f.setPort(r, []PortRange{{
Start: binaryutil.BigEndian.Uint16(rg.FromData),
End: binaryutil.BigEndian.Uint16(rg.ToData),
}}, l.Offset == 0)
return nil
}
// applyDynset folds a per-source connection limit into the rule. The dynamic set
// is keyed on the source address loaded just before it, which is what pins the
// rule's family.
func (d *nftDecoder) applyDynset(r *Rule, ds *expr.Dynset) error {
var cl *expr.Connlimit
for _, inner := range ds.Exprs {
if c, ok := inner.(*expr.Connlimit); ok {
cl = c
}
}
if cl == nil {
return fmt.Errorf("unsupported dynamic set statement")
}
if cl.Flags&expr.NFT_CONNLIMIT_F_INV == 0 {
return fmt.Errorf("unsupported under-limit connection count")
}
l, ok := d.load.(*expr.Payload)
if !ok || l.Base != expr.PayloadBaseNetworkHeader {
return fmt.Errorf("unsupported connection-limit key")
}
if d.fam == FamilyAny {
return fmt.Errorf("connection-limit key without a family qualifier")
}
soff, slen := addrField(d.fam, true)
if l.Offset != soff || l.Len != slen {
return fmt.Errorf("unsupported connection-limit key offset %d", l.Offset)
}
r.ConnLimit = &ConnLimit{Count: uint(cl.Count), PerSource: true}
r.meterSet = ds.SetName
return nil
}
// UnmarshalRule decodes a chain row's expression list into a filter rule. It
// returns an error for any row carrying a construct the model cannot hold, so
// the caller can keep the row as an opaque slot rather than misrepresenting it.
func (f *NFT) UnmarshalRule(nr *nftables.Rule, chain string, sets *nftSetReader, tbl *nftables.Table) (*Rule, error) {
r := &Rule{Direction: f.directionForChain(chain)}
d := &nftDecoder{f: f, sets: sets, tbl: tbl}
// A row in a single-family table states no nfproto of its own: the table is
// the qualifier, so seed the family from it. Without this an operator's
// `table ip filter` rule matching an address (or an address set) has no family
// to read its network-header offsets against and the whole row goes opaque.
if fam, ok := f.familyForTable(tbl); ok {
r.Family, d.fam = fam, fam
}
for _, e := range nr.Exprs {
switch v := e.(type) {
case *expr.Meta, *expr.Payload, *expr.Ct:
d.load, d.mask = v, nil
case *expr.Bitwise:
d.mask = v.Mask
case *expr.Cmp:
if err := d.applyCmp(r, v); err != nil {
return nil, err
}
case *expr.Lookup:
if err := d.applyLookup(r, v); err != nil {
return nil, err
}
case *expr.Range:
if err := d.applyRange(r, v); err != nil {
return nil, err
}
case *expr.Dynset:
if err := d.applyDynset(r, v); err != nil {
return nil, err
}
case *expr.Connlimit:
if v.Flags&expr.NFT_CONNLIMIT_F_INV == 0 {
return nil, fmt.Errorf("unsupported under-limit connection count")
}
r.ConnLimit = &ConnLimit{Count: uint(v.Count)}
case *expr.Limit:
if v.Type != expr.LimitTypePkts || v.Over {
return nil, fmt.Errorf("unsupported limit statement")
}
unit, ok := f.rateUnitForSeconds(uint64(v.Unit))
if !ok {
return nil, fmt.Errorf("unsupported rate unit %d", v.Unit)
}
// nftables applies a default burst of 5 packets to every limit and
// reports it back even when none was requested, so the default reads
// as unset.
r.RateLimit = &RateLimit{Rate: uint(v.Rate), Unit: unit, Burst: normBurst(uint(v.Burst))}
case *expr.Log:
r.Log = true
if len(v.Data) > 0 {
r.LogPrefix = string(v.Data)
}
case *expr.Counter:
r.Packets, r.Bytes = v.Packets, v.Bytes
case *expr.Verdict:
switch v.Kind {
case expr.VerdictAccept:
r.Action = Accept
case expr.VerdictDrop:
r.Action = Drop
default:
return nil, fmt.Errorf("unsupported verdict %d", v.Kind)
}
case *expr.Reject:
r.Action = Reject
default:
return nil, fmt.Errorf("unsupported expression %T", e)
}
}
if r.Action == ActionInvalid {
return nil, fmt.Errorf("no valid action was provided")
}
// The comment rides in the rule's user data rather than its expressions.
if comment, ok := userdata.GetString(nr.UserData, userdata.TypeComment); ok {
r.Comment = comment
}
return r, nil
}
// UnmarshalNATRule decodes a nat chain row's expression list into a NAT rule.
func (f *NFT) UnmarshalNATRule(nr *nftables.Rule, sets *nftSetReader, tbl *nftables.Table) (*NATRule, error) {
r := &NATRule{}
// The NAT matches reuse the filter decoder, which works against a Rule; the
// shared fields are copied across once the walk is done.
match := &Rule{}
d := &nftDecoder{f: f, sets: sets, tbl: tbl}
immediates := map[uint32][]byte{}
// The table settles the family for a single-family table's rows, as in
// UnmarshalRule; a nat expression naming its own family overrides it below.
if fam, ok := f.familyForTable(tbl); ok {
match.Family, d.fam = fam, fam
}
for _, e := range nr.Exprs {
switch v := e.(type) {
case *expr.Meta, *expr.Payload, *expr.Ct:
d.load, d.mask = v, nil
case *expr.Bitwise:
d.mask = v.Mask
case *expr.Cmp:
if err := d.applyCmp(match, v); err != nil {
return nil, err
}
case *expr.Lookup:
if err := d.applyLookup(match, v); err != nil {
return nil, err
}
case *expr.Range:
if err := d.applyRange(match, v); err != nil {
return nil, err
}
case *expr.Immediate:
immediates[v.Register] = v.Data
case *expr.Counter:
// A foreign nat rule may carry a counter; it is not part of the model.
case *expr.NAT:
r.Kind = DNAT
if v.Type == expr.NATTypeSourceNAT {
r.Kind = SNAT
}
if fam, ok := f.familyForNFProto(byte(v.Family)); ok {
r.Family = fam
}
if v.RegAddrMin != 0 {
data, ok := immediates[v.RegAddrMin]
if !ok {
return nil, fmt.Errorf("nat address register %d was never loaded", v.RegAddrMin)
}
r.ToAddress = net.IP(data).String()
}
if v.RegProtoMin != 0 {
data, ok := immediates[v.RegProtoMin]
if !ok || len(data) != 2 {
return nil, fmt.Errorf("nat port register %d was never loaded", v.RegProtoMin)
}
r.ToPort = binaryutil.BigEndian.Uint16(data)
}
case *expr.Redir:
r.Kind = Redirect
if v.RegisterProtoMin != 0 {
data, ok := immediates[v.RegisterProtoMin]
if !ok || len(data) != 2 {
return nil, fmt.Errorf("redirect port register %d was never loaded", v.RegisterProtoMin)
}
r.ToPort = binaryutil.BigEndian.Uint16(data)
}
case *expr.Masq:
r.Kind = Masquerade
default:
return nil, fmt.Errorf("unsupported expression %T", e)
}
}
if r.Kind == NATInvalid {
return nil, fmt.Errorf("no nat action was provided")
}
// Carry the decoded matches across. The interface is direction-bound, so
// whichever side the match named is the rule's interface.
if match.Family != FamilyAny {
r.Family = match.Family
}
r.Source, r.Destination = match.Source, match.Destination
r.Proto = match.Proto
r.Port, r.Ports = match.Port, match.Ports
if match.InInterface != "" {
r.Interface = match.InInterface
} else if match.OutInterface != "" {
r.Interface = match.OutInterface
}
if r.Family == FamilyAny {
r.Family = r.impliedFamily()
}
return r, nil
}
// -----------------------------------------------------------------------------
// Reading
// -----------------------------------------------------------------------------
// listChain returns the chain's rules with their nftables handles, 1:1 with its
// physical rows. A row the model cannot parse is kept as an opaque slot — a nil
// rule with its handle — so a rewrite deletes only rows the model understands
// and the position math stays aligned; GetRules and the dedup scans skip the
// nil entries.
func (f *NFT) listChain(c *nftables.Conn, sets *nftSetReader, chain string) (rules []*Rule, handles []uint64, err error) {
tbl := f.tableRef()
rows, err := c.GetRules(tbl, f.chainRef(chain))
if err != nil {
// A missing table or chain simply means there are no rules yet.
if f.isNotExist(err) {
return nil, nil, nil
}
return nil, nil, err
}
for _, nr := range rows {
rule, perr := f.UnmarshalRule(nr, chain, sets, tbl)
if perr != nil {
rules = append(rules, nil)
handles = append(handles, nr.Handle)
continue
}
// Rules live in this backend's own table; membership in the library's
// private table is what sets HasPrefix, so record the table and flag it
// as carrying the prefix.
rule.table = f.table
rule.HasPrefix = true
rules = append(rules, rule)
handles = append(handles, nr.Handle)
}
return rules, handles, nil
}
// listNATChain is listChain for the nat base chains.
func (f *NFT) listNATChain(c *nftables.Conn, sets *nftSetReader, chain string) (rules []*NATRule, handles []uint64, err error) {
tbl := f.tableRef()
rows, err := c.GetRules(tbl, f.chainRef(chain))
if err != nil {
if f.isNotExist(err) {
return nil, nil, nil
}
return nil, nil, err
}
for _, nr := range rows {
rule, perr := f.UnmarshalNATRule(nr, sets, tbl)
if perr != nil {
rules = append(rules, nil)
handles = append(handles, nr.Handle)
continue
}
rule.table = f.table
rule.HasPrefix = true
rules = append(rules, rule)
handles = append(handles, nr.Handle)
}
return rules, handles, nil
}
// foreignChains returns every chain in the ruleset that is not in this backend's
// own table, paired with the table it belongs to. Chains are dumped once per
// family rather than once per table.
func (f *NFT) foreignChains(c *nftables.Conn) ([]*nftables.Chain, error) {
tables, err := c.ListTables()
if err != nil {
return nil, err
}
families := map[nftables.TableFamily]bool{}
keep := map[string]bool{}
for _, t := range tables {
if t.Family == nftables.TableFamilyINet && t.Name == f.table {
continue
}
// A container runtime's table is out of scope entirely: it is the
// runtime's to reconcile, and this backend could not remove it anyway
// (mutations are scoped to our own table), so reporting it would only make
// Sync try, no-op, and over-count removed on every run.
if isContainerRuntimeTable(f.familyName(t.Family) + " " + t.Name) {
continue
}
families[t.Family] = true
keep[fmt.Sprintf("%d|%s", t.Family, t.Name)] = true
}
var out []*nftables.Chain
for fam := range families {
chains, cerr := c.ListChainsOfTableFamily(fam)
if cerr != nil {
return nil, cerr
}
for _, ch := range chains {
if !keep[fmt.Sprintf("%d|%s", ch.Table.Family, ch.Table.Name)] {
continue
}
if isContainerRuntimeChain(ch.Name) {
continue
}
out = append(out, ch)
}
}
return out, nil
}
// listForeignRules walks the ruleset and returns best-effort parsed rules that
// live outside this backend's own inet table. Because arbitrary foreign tables
// use constructs the Rule model cannot represent, any row that fails to decode
// is skipped rather than erroring the whole read. This gives callers visibility
// of rules in other tables alongside the library's own.
func (f *NFT) listForeignRules(c *nftables.Conn, sets *nftSetReader) ([]*Rule, error) {
chains, err := f.foreignChains(c)
if err != nil {
// No ruleset (or netlink unavailable for listing): nothing foreign to report.
return nil, nil
}
var rules []*Rule
for _, ch := range chains {
rows, rerr := c.GetRules(ch.Table, ch)
if rerr != nil {
continue
}
for _, nr := range rows {
rule, perr := f.UnmarshalRule(nr, ch.Name, sets, ch.Table)
if perr != nil || rule == nil {
continue
}
if rule.isContainerRuntime() {
continue
}
// A rule from another table: record where it came from; it is not ours,
// so HasPrefix stays false.
rule.table = f.familyName(ch.Table.Family) + " " + ch.Table.Name
rules = append(rules, rule)
}
}
return rules, nil
}
// listForeignNATRules is listForeignRules for NAT rules.
func (f *NFT) listForeignNATRules(c *nftables.Conn, sets *nftSetReader) ([]*NATRule, error) {
chains, err := f.foreignChains(c)
if err != nil {
return nil, nil
}
var rules []*NATRule
for _, ch := range chains {
rows, rerr := c.GetRules(ch.Table, ch)
if rerr != nil {
continue
}
for _, nr := range rows {
rule, perr := f.UnmarshalNATRule(nr, sets, ch.Table)
if perr != nil || rule == nil {
continue
}
if rule.isHairpinMasquerade() {
continue
}
rule.table = f.familyName(ch.Table.Family) + " " + ch.Table.Name
rules = append(rules, rule)
}
}
return rules, nil
}
// listOwnRules returns the library's own filter rules from its private table, one
// rule per physical chain row. A read does not create the table; listChain returns
// nothing when the table does not yet exist. nftables' inet table stores a
// family-agnostic rule as one unpinned row and a both-transports rule as one
// l4proto-set row, so UnmarshalRule reports FamilyAny and TCPUDP straight off the
// row that carries them; nothing is collapsed here. Number per direction (input
// then output chain) so each rule's Number matches the InsertRule/MoveRule
// position within its chain.
func (f *NFT) listOwnRules(c *nftables.Conn, sets *nftSetReader) ([]*Rule, error) {
var rules []*Rule
for _, chain := range nftFilterChains {
chainRules, _, cerr := f.listChain(c, sets, chain)
if cerr != nil {
return nil, cerr
}
// Opaque (nil) rows stay in the chain but are not reportable rules.
for _, r := range chainRules {
if r != nil {
rules = append(rules, r)
}
}
}
numberByDirection(rules)
return rules, nil
}
// listOwnNATRules returns the library's own NAT rules from its private table, one
// rule per physical chain row.
func (f *NFT) listOwnNATRules(c *nftables.Conn, sets *nftSetReader) ([]*NATRule, error) {
var rules []*NATRule
for _, chain := range nftNATChains {
chainRules, _, cerr := f.listNATChain(c, sets, chain)
if cerr != nil {
return nil, cerr
}
for _, r := range chainRules {
if r != nil {
rules = append(rules, r)
}
}
}
// The nat chains live in the same inet table, so a family-agnostic translation is
// one unpinned row that reads back as FamilyAny; nothing is collapsed here. Number
// per nat chain (prerouting then postrouting) so each rule's Number matches the
// InsertNATRule/MoveNATRule position within its chain.
numberNATByChain(rules)
return rules, nil
}
// GetRules returns the existing filter rules from the zone.
func (f *NFT) GetRules(ctx context.Context, zoneName string) (rules []*Rule, err error) {
c, err := nftConn()
if err != nil {
return nil, err
}
sets := newSetReader(c)
// The library's own rules, then foreign rules from every other table.
rules, err = f.listOwnRules(c, sets)
if err != nil {
return nil, err
}
foreign, ferr := f.listForeignRules(c, sets)
if ferr != nil {
return nil, ferr
}
rules = append(rules, foreign...)
return rules, nil
}
// GetNATRules returns the existing NAT rules from the zone.
func (f *NFT) GetNATRules(ctx context.Context, zoneName string) (rules []*NATRule, err error) {
c, err := nftConn()
if err != nil {
return nil, err
}
sets := newSetReader(c)
rules, err = f.listOwnNATRules(c, sets)
if err != nil {
return nil, err
}
foreign, ferr := f.listForeignNATRules(c, sets)
if ferr != nil {
return nil, ferr
}
rules = append(rules, foreign...)
return rules, nil
}
// -----------------------------------------------------------------------------
// Table and chain setup
// -----------------------------------------------------------------------------
// nftBaseChain describes one of the private table's base chains.
type nftBaseChain struct {
name string
chType nftables.ChainType
hook *nftables.ChainHook
priority *nftables.ChainPriority
}
// nftFilterBaseChains defines the filter hooks the private table installs.
var nftFilterBaseChains = []nftBaseChain{
{"input", nftables.ChainTypeFilter, nftables.ChainHookInput, nftables.ChainPriorityFilter},
{"output", nftables.ChainTypeFilter, nftables.ChainHookOutput, nftables.ChainPriorityFilter},
{"forward", nftables.ChainTypeFilter, nftables.ChainHookForward, nftables.ChainPriorityFilter},
}
// nftNATBaseChains defines the nat hooks, created lazily on first NAT write.
var nftNATBaseChains = []nftBaseChain{
{"prerouting", nftables.ChainTypeNAT, nftables.ChainHookPrerouting, nftables.ChainPriorityNATDest},
{"postrouting", nftables.ChainTypeNAT, nftables.ChainHookPostrouting, nftables.ChainPriorityNATSource},
}
// ensureTable creates the private table and its filter base chains if they do
// not already exist. Adding an existing table or chain re-asserts it rather than
// failing, so re-running is safe.
//
// The chain definitions deliberately leave the policy unset: re-adding an
// existing base chain re-asserts the named properties, so stating "accept" here
// would revert a default-drop policy a prior SetDefaultPolicy set. A base chain
// created without a policy defaults to accept (the intended initial default),
// and omitting it leaves any existing policy untouched.
func (f *NFT) ensureTable(ctx context.Context) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.ensured {
return nil
}
c, err := nftConn()
if err != nil {
return err
}
tbl := c.AddTable(f.tableRef())
for _, bc := range nftFilterBaseChains {
c.AddChain(&nftables.Chain{
Name: bc.name,
Table: tbl,
Type: bc.chType,
Hooknum: bc.hook,
Priority: bc.priority,
})
}
if err := c.Flush(); err != nil {
return fmt.Errorf("failed to set up nftables table %s: %s", f.table, err)
}
f.ensured = true
return nil
}
// ensureNATChains creates the private table's nat base chains (prerouting for
// destination NAT, postrouting for source NAT) if they do not already exist. It
// is called lazily the first time a NAT rule is written so filter-only use never
// installs nat hooks.
func (f *NFT) ensureNATChains(ctx context.Context) error {
if err := f.ensureTable(ctx); err != nil {
return err
}
f.mu.Lock()
defer f.mu.Unlock()
if f.natEnsured {
return nil
}
c, err := nftConn()
if err != nil {
return err
}
tbl := f.tableRef()
accept := nftables.ChainPolicyAccept
for _, bc := range nftNATBaseChains {
c.AddChain(&nftables.Chain{
Name: bc.name,
Table: tbl,
Type: bc.chType,
Hooknum: bc.hook,
Priority: bc.priority,
Policy: &accept,
})
}
if err := c.Flush(); err != nil {
return fmt.Errorf("failed to set up nftables nat chains for %s: %s", f.table, err)
}
f.natEnsured = true
return nil
}
// -----------------------------------------------------------------------------
// Placement
// -----------------------------------------------------------------------------
// registerSets stages the sets an encoded rule references. Anonymous sets are
// created in the same batch as the rule that looks them up; the named meter set
// outlives any single rule, so it is created only when absent.
func (f *NFT) registerSets(c *nftables.Conn, enc *nftEncoded) error {
for _, as := range enc.anonSets {
if err := c.AddSet(as.set, as.elements); err != nil {
return err
}
}
if enc.meterSet != nil {
_, err := c.GetSetByName(f.tableRef(), enc.meterSet.Name)
switch {
case err == nil:
// The set already counts for this rule; reusing it keeps the counting
// state a re-add is meant to inherit.
case f.isNotExist(err):
if aerr := c.AddSet(enc.meterSet, nil); aerr != nil {
return aerr
}
default:
return err
}
}
return nil
}
// placeRule stages an encoded rule for addition at the 0-based physical index
// insPos. nftables positions a new rule relative to an existing one's handle, so
// an index inside the chain inserts before the row currently there and an index
// at or past the end appends. A negative index appends too, which is how AddRule
// asks for a plain append.
func (f *NFT) placeRule(c *nftables.Conn, enc *nftEncoded, insPos int, handles []uint64) error {
if err := f.registerSets(c, enc); err != nil {
return err
}
nr := &nftables.Rule{
Table: f.tableRef(),
Chain: f.chainRef(enc.chain),
Exprs: enc.exprs,
UserData: enc.userData,
}
if insPos >= 0 && insPos < len(handles) {
nr.Position = handles[insPos]
c.InsertRule(nr)
return nil
}
c.AddRule(nr)
return nil
}
// ruleExists reports whether existing already contains a rule matching r.
// Opaque (nil) rows never match.
func (f *NFT) ruleExists(existing []*Rule, r *Rule) bool {
for _, e := range existing {
if e != nil && e.EqualForDedup(r, true) {
return true
}
}
return false
}
// natRuleExists is ruleExists for NAT rules. Opaque (nil) rows never match.
func (f *NFT) natRuleExists(existing []*NATRule, r *NATRule) bool {
for _, e := range existing {
if e != nil && e.EqualForDedup(r) {
return true
}
}
return false
}
// physicalIndex maps a 1-based position over the modeled (non-nil) rows to the
// 0-based physical row index it lands at, keeping opaque rows pinned in place;
// a position past the last modeled row maps to the end of the chain.
func physicalIndex[T any](rows []*T, position int) int {
seen := 0
for i, e := range rows {
if e == nil {
continue
}
seen++
if seen == position {
return i
}
}
return len(rows)
}
// -----------------------------------------------------------------------------
// Filter rule mutations
// -----------------------------------------------------------------------------
// nextSurvivingHandle returns the handle of the first row after index i that a
// removal keeps, or 0 when every later row is being deleted (so a replacement
// belongs at the end of the chain).
func (f *NFT) nextSurvivingHandle(matched []bool, handles []uint64, i int) uint64 {
for j := i + 1; j < len(handles); j++ {
if !matched[j] {
return handles[j]
}
}
return 0
}
// removeCovered deletes every chain row the target covers and re-adds each
// merged row's untargeted remainder (see splitMergedRow) in the row's own slot,
// so coverage the caller never named survives in place. The deletions and the
// replacements ride one netlink transaction, so the chain is never briefly
// missing the coverage it keeps. It reports whether any row was deleted.
func (f *NFT) removeCovered(c *nftables.Conn, chain string, rules []*Rule, handles []uint64, r *Rule) (bool, error) {
matched := make([]bool, len(rules))
splits := make([][]*Rule, len(rules))
deleted := false
for i, e := range rules {
if e == nil || !e.EqualForRemoval(r, true) {
continue
}
matched[i] = true
deleted = true
// A concrete target that matched a multi-state row would drop coverage
// the caller never asked to remove: an unpinned inet row covers both
// families, and an l4proto-set row both transports.
splits[i] = splitMergedRow(e, r)
}
if !deleted {
return false, nil
}
tbl := f.tableRef()
ch := f.chainRef(chain)
for i := range rules {
if !matched[i] {
continue
}
if err := c.DelRule(&nftables.Rule{Table: tbl, Chain: ch, Handle: handles[i]}); err != nil {
return false, err
}
}
// Each remainder takes its row's slot: it is inserted before the first row
// the removal keeps, which is exactly where the deleted row sat. Walking the
// rows in ascending order keeps several remainders in their original order.
for i := range rules {
if !matched[i] {
continue
}
before := f.nextSurvivingHandle(matched, handles, i)
for _, s := range splits[i] {
// A remainder is synthesized here rather than supplied by a caller, so
// it takes the check an entry point would have run.
if verr := f.validateRule(s); verr != nil {
return false, verr
}
enc, merr := f.MarshalRule(s)
if merr != nil {
return false, merr
}
if err := f.registerSets(c, enc); err != nil {
return false, err
}
nr := &nftables.Rule{Table: tbl, Chain: ch, Exprs: enc.exprs, UserData: enc.userData}
if before != 0 {
nr.Position = before
c.InsertRule(nr)
} else {
c.AddRule(nr)
}
}
}
if err := c.Flush(); err != nil {
return false, err
}
// A deleted per-source connection-limit row leaves its counting set behind;
// drop each removed row's meter set now that its rule is gone. The delete is
// best-effort: one that fails (a surviving row — foreign, or a remainder
// re-added above — still references the set) leaves the set in place, which
// is the correct outcome.
f.sweepMeterSets(rules, matched)
return true, nil
}
// sweepMeterSets removes the counting sets of the rows a removal deleted. Each
// delete runs in its own transaction so one still-referenced set does not block
// the rest.
func (f *NFT) sweepMeterSets(rules []*Rule, matched []bool) {
for i, e := range rules {
if !matched[i] || e == nil || e.meterSet == "" {
continue
}
c, err := nftConn()
if err != nil {
return
}
c.DelSet(&nftables.Set{Table: f.tableRef(), Name: e.meterSet})
_ = c.Flush()
}
}
// insertRule places a rule in its chain, at a 1-based position over the modeled
// rows or, for a negative position, appended.
func (f *NFT) insertRule(ctx context.Context, zoneName string, position int, r *Rule) error {
if err := f.ensureTable(ctx); err != nil {
return err
}
// A DirAny rule fans out into an input row plus its role-swapped output row;
// place each in its own chain at the requested position.
if r.Direction == DirAny {
for _, sub := range expandDirections(r) {
if err := f.insertRule(ctx, zoneName, position, sub); err != nil {
return err
}
}
return nil
}
// A FamilyAny per-source connection limit has no single row (its meter set is
// family-typed): fan out into a v4 row and a v6 row, each with its own set.
if f.perSourceFamilySplit(r) {
for _, sub := range expandFamilies(r) {
if err := f.insertRule(ctx, zoneName, position, sub); err != nil {
return err
}
}
return nil
}
// Verify the rule is valid for nftables. The fan-outs above run first:
// validateRule asserts a per-source connection limit already carries a
// concrete family.
if err := f.validateRule(r); err != nil {
return err
}
c, err := nftConn()
if err != nil {
return err
}
// A family-agnostic set reference is pinned to the set's own family.
r, err = f.resolveSetRefFamily(ctx, c, r)
if err != nil {
return err
}
enc, err := f.MarshalRule(r)
if err != nil {
return err
}
// Skip if an equivalent rule already exists.
existing, handles, err := f.listChain(c, newSetReader(c), enc.chain)
if err != nil {
return err
}
if f.ruleExists(existing, r) {
return nil
}
insPos := -1
if position >= 1 {
// A position counts the modeled rows, matching GetRules' numbering;
// physicalIndex maps it past any opaque rows to the chain's real index.
insPos = physicalIndex(existing, position)
}
if err := f.placeRule(c, enc, insPos, handles); err != nil {
return err
}
return c.Flush()
}
// AddRule adds a rule to the zone.
func (f *NFT) AddRule(ctx context.Context, zoneName string, r *Rule) error {
return f.insertRule(ctx, zoneName, -1, r)
}
// InsertRule inserts rule before the given 1-based position. position <= 0 is
// treated as 1 (prepend); a position larger than the current rule count appends
// the rule. Normalizing here keeps insertRule's -1 sentinel reserved for
// AddRule's plain append.
func (f *NFT) InsertRule(ctx context.Context, zoneName string, position int, r *Rule) error {
if position <= 0 {
position = 1
}
return f.insertRule(ctx, zoneName, position, r)
}
// MoveRule moves an existing rule to the given 1-based position.
func (f *NFT) MoveRule(ctx context.Context, zoneName string, r *Rule, position int) error {
if position <= 0 {
position = 1
}
// A DirAny rule occupies a slot in both chains; move each half to the requested
// position within its own chain.
if r.Direction == DirAny {
if err := f.ensureTable(ctx); err != nil {
return err
}
for _, sub := range expandDirections(r) {
if err := f.MoveRule(ctx, zoneName, sub, position); err != nil {
return err
}
}
return nil
}
// A FamilyAny per-source connection limit occupies one row per family; move
// each to the requested position, mirroring the insertRule fan-out.
if f.perSourceFamilySplit(r) {
if err := f.ensureTable(ctx); err != nil {
return err
}
for _, sub := range expandFamilies(r) {
if err := f.MoveRule(ctx, zoneName, sub, position); err != nil {
return err
}
}
return nil
}
// Verify the rule is valid for nftables; the fan-outs above have already split
// a per-source connection limit per family.
if err := f.validateRule(r); err != nil {
return err
}
if err := f.ensureTable(ctx); err != nil {
return err
}
c, err := nftConn()
if err != nil {
return err
}
// A family-agnostic set reference is pinned to the set's own family so the
// re-add below marshals; the rows it targets are already pinned on read.
r, err = f.resolveSetRefFamily(ctx, c, r)
if err != nil {
return err
}
chain := f.chainForDirection(r.Direction)
rules, handles, err := f.listChain(c, newSetReader(c), chain)
if err != nil {
return err
}
// The target's current position is its first matching row's position among
// the modeled rows; moving it there is a no-op.
firstLogical := -1
logical := 0
for _, e := range rules {
if e == nil {
continue
}
logical++
if firstLogical < 0 && e.EqualForRemoval(r, true) {
firstLogical = logical
}
}
if firstLogical < 0 || position == firstLogical {
return nil
}
// nftables has no native move. Delete every row the target covers — a
// FamilyAny or TCPUDP target spans rows the chain may hold separately, so all
// of them relocate — but a concrete target that matched a merged row must not
// take the untargeted coverage with it: removeCovered re-adds each merged
// row's remainder in its own slot, and only the targeted rule moves.
if _, err := f.removeCovered(c, chain, rules, handles, r); err != nil {
return err
}
// The rewrite changed the chain's handles, so read it back before placing the
// rule at its new position. Flush emptied the batch, so the same scope takes
// the follow-up placement; the set reader is rebuilt because its cache
// predates the rewrite.
after, afterHandles, err := f.listChain(c, newSetReader(c), chain)
if err != nil {
return err
}
enc, err := f.MarshalRule(r)
if err != nil {
return err
}
if err := f.placeRule(c, enc, physicalIndex(after, position), afterHandles); err != nil {
return err
}
return c.Flush()
}
// RemoveRule removes a rule from the zone.
func (f *NFT) RemoveRule(ctx context.Context, zoneName string, r *Rule) error {
if err := f.ensureTable(ctx); err != nil {
return err
}
// A DirAny target removes both its input row and its role-swapped output row,
// each from its own chain.
if r.Direction == DirAny {
for _, sub := range expandDirections(r) {
if err := f.RemoveRule(ctx, zoneName, sub); err != nil {
return err
}
}
return nil
}
// Verify the rule is valid for nftables. Only the encoding paths run
// validateRule: a removal target is matched against parsed rows, never
// encoded, and RemoveRule has no per-family fan-out, so a FamilyAny
// per-source connection limit is a legitimate target here.
if err := r.validate(); err != nil {
return err
}
c, err := nftConn()
if err != nil {
return err
}
chain := f.chainForDirection(r.Direction)
rules, handles, err := f.listChain(c, newSetReader(c), chain)
if err != nil {
return err
}
// Delete every row the target covers, not just the first: a FamilyAny target
// clears both an unpinned row and any family-pinned rows it spans, and a TCPUDP
// target clears both transports. A concrete-family target still removes only its
// own family — see EqualForRemoval; removeCovered re-adds each merged row's
// untargeted remainder in the row's own slot.
_, err = f.removeCovered(c, chain, rules, handles, r)
return err
}
// -----------------------------------------------------------------------------
// NAT rule mutations
// -----------------------------------------------------------------------------
// removeCoveredNAT is removeCovered for NAT rules: it deletes every chain row
// the target covers and re-adds each dual-family row's untargeted family
// remainder (see splitNATDualRow) in the row's own slot, so family coverage the
// caller never named survives in place. Family is the only axis a NAT rule
// spans, so a concrete-family target never splits a row it matches.
func (f *NFT) removeCoveredNAT(c *nftables.Conn, chain string, rules []*NATRule, handles []uint64, r *NATRule) (bool, error) {
matched := make([]bool, len(rules))
splits := make([]*NATRule, len(rules))
deleted := false
for i, e := range rules {
if e == nil || !e.EqualForRemoval(r) {
continue
}
matched[i] = true
deleted = true
// A concrete target that matched a genuine dual-family row (an unpinned
// inet row covering both) would drop the family the caller did not name.
splits[i] = splitNATDualRow(e, r)
}
if !deleted {
return false, nil
}
tbl := f.tableRef()
ch := f.chainRef(chain)
for i := range rules {
if !matched[i] {
continue
}
if err := c.DelRule(&nftables.Rule{Table: tbl, Chain: ch, Handle: handles[i]}); err != nil {
return false, err
}
}
for i := range rules {
if !matched[i] || splits[i] == nil {
continue
}
// A remainder is synthesized here rather than supplied by a caller, so it
// takes the check an entry point would have run.
if verr := f.validateNAT(splits[i]); verr != nil {
return false, verr
}
enc, merr := f.MarshalNATRule(splits[i])
if merr != nil {
return false, merr
}
if err := f.registerSets(c, enc); err != nil {
return false, err
}
nr := &nftables.Rule{Table: tbl, Chain: ch, Exprs: enc.exprs, UserData: enc.userData}
if before := f.nextSurvivingHandle(matched, handles, i); before != 0 {
nr.Position = before
c.InsertRule(nr)
} else {
c.AddRule(nr)
}
}
return true, c.Flush()
}
// addNATRule places a NAT rule in its chain, at a 1-based position over the
// modeled rows or, for a negative position, appended.
func (f *NFT) addNATRule(ctx context.Context, position int, r *NATRule) error {
// Verify the rule is valid for nftables.
if err := f.validateNAT(r); err != nil {
return err
}
if err := f.ensureNATChains(ctx); err != nil {
return err
}
c, err := nftConn()
if err != nil {
return err
}
// A family-agnostic set reference is pinned to the set's own family.
r, err = f.resolveNATSetRefFamily(ctx, c, r)
if err != nil {
return err
}
enc, err := f.MarshalNATRule(r)
if err != nil {
return err
}
existing, handles, err := f.listNATChain(c, newSetReader(c), enc.chain)
if err != nil {
return err
}
if f.natRuleExists(existing, r) {
return nil
}
insPos := -1
if position >= 1 {
insPos = physicalIndex(existing, position)
}
if err := f.placeRule(c, enc, insPos, handles); err != nil {
return err
}
return c.Flush()
}
// AddNATRule adds a NAT rule to the zone.
func (f *NFT) AddNATRule(ctx context.Context, zoneName string, r *NATRule) error {
return f.addNATRule(ctx, -1, r)
}
// InsertNATRule inserts a NAT rule before the given 1-based position within its
// nat chain. position <= 0 is treated as 1; a position larger than the chain's
// current rule count appends the rule.
func (f *NFT) InsertNATRule(ctx context.Context, zoneName string, position int, r *NATRule) error {
if position <= 0 {
position = 1
}
return f.addNATRule(ctx, position, r)
}
// MoveNATRule moves an existing NAT rule to the given 1-based position within
// its nat chain. position <= 0 is treated as 1; a position larger than the
// chain's current rule count moves the rule to the end.
func (f *NFT) MoveNATRule(ctx context.Context, zoneName string, r *NATRule, position int) error {
if position <= 0 {
position = 1
}
// Verify the rule is valid for nftables.
if err := f.validateNAT(r); err != nil {
return err
}
if err := f.ensureNATChains(ctx); err != nil {
return err
}
c, err := nftConn()
if err != nil {
return err
}
r, err = f.resolveNATSetRefFamily(ctx, c, r)
if err != nil {
return err
}
enc, err := f.MarshalNATRule(r)
if err != nil {
return err
}
rules, handles, err := f.listNATChain(c, newSetReader(c), enc.chain)
if err != nil {
return err
}
// The target's current position is its first matched row's position among the
// modeled rows; moving it there is a no-op.
firstLogical := -1
logical := 0
for _, e := range rules {
if e == nil {
continue
}
logical++
if firstLogical < 0 && e.EqualForRemoval(r) {
firstLogical = logical
}
}
if firstLogical < 0 || position == firstLogical {
return nil
}
// nftables has no native move; see MoveRule for why the covered rows are
// deleted and each dual row's untargeted family re-added in its own slot.
if _, err := f.removeCoveredNAT(c, enc.chain, rules, handles, r); err != nil {
return err
}
// Flush emptied the batch, so the same scope takes the follow-up placement
// against the handles the rewrite left behind.
after, afterHandles, err := f.listNATChain(c, newSetReader(c), enc.chain)
if err != nil {
return err
}
if err := f.placeRule(c, enc, physicalIndex(after, position), afterHandles); err != nil {
return err
}
return c.Flush()
}
// RemoveNATRule removes a NAT rule from the zone.
func (f *NFT) RemoveNATRule(ctx context.Context, zoneName string, r *NATRule) error {
// Verify the rule is valid for nftables.
if err := r.validate(); err != nil {
return err
}
if err := f.ensureNATChains(ctx); err != nil {
return err
}
chain := "prerouting"
if r.Kind.isSource() {
chain = "postrouting"
}
c, err := nftConn()
if err != nil {
return err
}
rules, handles, err := f.listNATChain(c, newSetReader(c), chain)
if err != nil {
return err
}
// Delete every matching row (see RemoveRule): a FamilyAny NAT target must clear
// both the unpinned row it names and any family-pinned rows it covers, while a
// concrete-family target removes only its own family. A concrete-family target
// that matched a genuine dual-family row re-adds the untargeted family in the
// row's slot, so a bare masquerade/redirect does not silently stop translating
// the other family.
_, err = f.removeCoveredNAT(c, chain, rules, handles, r)
return err
}
// -----------------------------------------------------------------------------
// Default policy
// -----------------------------------------------------------------------------
// chainPolicies reads the policy of the private table's filter base chains. A
// chain that does not exist yet has no policy to report and is left as
// ActionInvalid.
func (f *NFT) chainPolicies(c *nftables.Conn) (map[string]Action, error) {
out := map[string]Action{}
chains, err := c.ListChainsOfTableFamily(nftables.TableFamilyINet)
if err != nil {
if f.isNotExist(err) {
return out, nil
}
return nil, err
}
for _, ch := range chains {
if ch.Table.Name != f.table || ch.Policy == nil {
continue
}
switch *ch.Policy {
case nftables.ChainPolicyAccept:
out[ch.Name] = Accept
case nftables.ChainPolicyDrop:
out[ch.Name] = Drop
}
}
return out, nil
}
// GetDefaultPolicy returns the default action applied to packets that match no rule.
func (f *NFT) GetDefaultPolicy(ctx context.Context, zoneName string) (*DefaultPolicy, error) {
c, err := nftConn()
if err != nil {
return nil, err
}
policies, err := f.chainPolicies(c)
if err != nil {
return nil, err
}
return &DefaultPolicy{
Input: policies["input"],
Output: policies["output"],
Forward: policies["forward"],
}, nil
}
// SetDefaultPolicy sets the policy of the named directions. nftables chain
// policies may only be accept or drop; reject is not expressible. Re-adding the
// base chain with a policy is how a policy is changed, so its hook properties
// are restated alongside.
func (f *NFT) SetDefaultPolicy(ctx context.Context, zoneName string, policy *DefaultPolicy) error {
if policy == nil {
return fmt.Errorf("policy cannot be nil")
}
if err := f.ensureTable(ctx); err != nil {
return err
}
wanted := map[string]Action{
"input": policy.Input,
"output": policy.Output,
"forward": policy.Forward,
}
c, err := nftConn()
if err != nil {
return err
}
tbl := f.tableRef()
for _, bc := range nftFilterBaseChains {
action, ok := wanted[bc.name]
if !ok || action == ActionInvalid {
continue
}
var pol nftables.ChainPolicy
switch action {
case Accept:
pol = nftables.ChainPolicyAccept
case Drop:
pol = nftables.ChainPolicyDrop
default:
return fmt.Errorf("nftables chain policy may only be accept or drop")
}
c.AddChain(&nftables.Chain{
Name: bc.name,
Table: tbl,
Type: bc.chType,
Hooknum: bc.hook,
Priority: bc.priority,
Policy: &pol,
})
}
return c.Flush()
}
// -----------------------------------------------------------------------------
// Address sets
// -----------------------------------------------------------------------------
// setKeyType returns the nftables key type a family's addresses are stored as.
func (f *NFT) setKeyType(family Family) (nftables.SetDatatype, error) {
switch family {
case IPv6:
return nftables.TypeIP6Addr, nil
case IPv4, FamilyAny:
// An nftables set carries a single address type, so an unspecified family
// resolves to IPv4.
return nftables.TypeIPAddr, nil
}
return nftables.SetDatatype{}, fmt.Errorf("a set requires a concrete ip family: %w", ErrUnsupportedSet)
}
// familyForKeyType reverses setKeyType.
func (f *NFT) familyForKeyType(t nftables.SetDatatype) Family {
switch t.Name {
case "ipv6_addr":
return IPv6
case "ipv4_addr":
return IPv4
}
return FamilyAny
}
// addrFromKey decodes a set element key as an address.
func (f *NFT) addrFromKey(key []byte) (netip.Addr, bool) {
return netip.AddrFromSlice(key)
}
// addressSetEntries reconstructs the entries a set holds. A plain set lists
// discrete addresses; an interval set lists boundary markers, each span an
// inclusive start and an exclusive end, reported back as a CIDR when the span is
// exactly one and as a "lo-hi" range otherwise.
func (f *NFT) addressSetEntries(sc *nftSetContents) []string {
type elem struct {
addr netip.Addr
end bool
}
var elems []elem
for _, e := range sc.elements {
addr, ok := f.addrFromKey(e.Key)
if !ok {
continue
}
elems = append(elems, elem{addr: addr, end: e.IntervalEnd})
}
sort.Slice(elems, func(i, j int) bool {
if elems[i].addr == elems[j].addr {
return !elems[i].end && elems[j].end
}
return elems[i].addr.Less(elems[j].addr)
})
var entries []string
if !sc.set.Interval {
for _, e := range elems {
entries = append(entries, e.addr.String())
}
return entries
}
open := false
var start netip.Addr
for _, e := range elems {
if !e.end {
start, open = e.addr, true
continue
}
if !open {
continue
}
// The stored end is exclusive; the span runs to the address below it.
last := e.addr.Prev()
if !last.IsValid() || last.Less(start) {
open = false
continue
}
rng := netipx.IPRangeFrom(start, last)
if p, ok := rng.Prefix(); ok {
entries = append(entries, p.String())
} else {
entries = append(entries, rng.String())
}
open = false
}
return entries
}
// setElements renders an entry — an address, a CIDR or a "lo-hi" range — as the
// element(s) a set stores it as. An interval set records the inclusive start and
// an end marker at the exclusive upper bound.
func (f *NFT) setElements(entry string, interval bool) ([]nftables.SetElement, error) {
entry = strings.TrimSpace(entry)
var from, to netip.Addr
switch {
case strings.Contains(entry, "/"):
p, err := netip.ParsePrefix(entry)
if err != nil {
return nil, fmt.Errorf("invalid set entry %q: %s", entry, err)
}
rng := netipx.RangeOfPrefix(p.Masked())
from, to = rng.From(), rng.To()
case strings.Contains(entry, "-"):
rng, err := netipx.ParseIPRange(entry)
if err != nil {
return nil, fmt.Errorf("invalid set entry %q: %s", entry, err)
}
from, to = rng.From(), rng.To()
default:
addr, err := netip.ParseAddr(entry)
if err != nil {
return nil, fmt.Errorf("invalid set entry %q: %s", entry, err)
}
from, to = addr, addr
}
if !interval {
if from != to {
return nil, fmt.Errorf("set entry %q spans a range, which requires an interval set", entry)
}
return []nftables.SetElement{{Key: from.AsSlice()}}, nil
}
end := to.Next()
if !end.IsValid() {
return nil, fmt.Errorf("set entry %q reaches the end of the address space", entry)
}
return []nftables.SetElement{
{Key: from.AsSlice()},
{Key: end.AsSlice(), IntervalEnd: true},
}, nil
}
// getAddressSet reads a single nftables set as an AddressSet, or nil if it does
// not exist.
func (f *NFT) getAddressSet(c *nftables.Conn, name string) (*AddressSet, error) {
set, err := c.GetSetByName(f.tableRef(), name)
if err != nil {
// A missing set is a no-op for the callers that probe with it; any other
// failure must surface rather than reading as "not there", or a Backup
// would silently capture fewer sets than exist.
if f.isNotExist(err) {
return nil, nil
}
return nil, err
}
// A dynamic set is connection-limit counting state, not an address set;
// report it as not-found so no caller manages it as data.
if set.Dynamic {
return nil, nil
}
elems, err := c.GetSetElements(set)
if err != nil {
return nil, err
}
out := &AddressSet{Name: name, Family: f.familyForKeyType(set.KeyType)}
if set.Interval {
out.Type = SetHashNet
}
out.Entries = f.addressSetEntries(&nftSetContents{set: set, elements: elems})
return out, nil
}
// GetAddressSets returns the address sets managed by this backend.
func (f *NFT) GetAddressSets(ctx context.Context) ([]*AddressSet, error) {
if err := f.ensureTable(ctx); err != nil {
return nil, err
}
c, err := nftConn()
if err != nil {
return nil, err
}
sets, err := c.GetSets(f.tableRef())
if err != nil {
return nil, err
}
result := make([]*AddressSet, 0, len(sets))
for _, s := range sets {
// A dynamic set is a per-source connection-limit meter, rule mechanics
// rather than a caller-managed address set; reporting one would let a
// Backup/Restore or a caller's sweep manage counting state as data. An
// anonymous set is a rule's own inline literal, likewise not data.
if s.Dynamic || s.Anonymous {
continue
}
detail, derr := f.getAddressSet(c, s.Name)
if derr != nil {
return nil, derr
}
if detail == nil {
continue
}
result = append(result, detail)
}
return result, nil
}
// GetAddressSet returns a single address set by name, or an error if it does not exist.
func (f *NFT) GetAddressSet(ctx context.Context, name string) (*AddressSet, error) {
if err := f.ensureTable(ctx); err != nil {
return nil, err
}
c, err := nftConn()
if err != nil {
return nil, err
}
set, err := f.getAddressSet(c, name)
if err != nil {
return nil, err
}
if set == nil {
return nil, fmt.Errorf("address set %q not found", name)
}
return set, nil
}
// setMatches reports whether an existing set's definition matches a requested
// family/type, so AddAddressSet can tell a harmless re-add of an identical set
// apart from a genuine type/family conflict, which must surface as an error
// rather than being silently swallowed. A nil existing set never matches.
func (f *NFT) setMatches(existing *AddressSet, wantFamily Family, wantType SetType) bool {
if existing == nil {
return false
}
if wantFamily == FamilyAny {
wantFamily = IPv4
}
return existing.Family == wantFamily && existing.Type == wantType
}
// AddAddressSet creates an address set. Adding a set that already exists (by name)
// with the same definition is a no-op.
func (f *NFT) AddAddressSet(ctx context.Context, set *AddressSet) error {
if set == nil || set.Name == "" {
return fmt.Errorf("an address set requires a name")
}
if err := f.ensureTable(ctx); err != nil {
return err
}
keyType, err := f.setKeyType(set.Family)
if err != nil {
return err
}
c, err := nftConn()
if err != nil {
return err
}
// An existing set is only acceptable when it is the same set; a differing
// definition is a genuine conflict.
if existing, gerr := f.getAddressSet(c, set.Name); gerr != nil {
return gerr
} else if existing != nil {
if !f.setMatches(existing, set.Family, set.Type) {
return fmt.Errorf("address set %q already exists with a different definition", set.Name)
}
return nil
}
interval := set.Type == SetHashNet
var elems []nftables.SetElement
for _, e := range set.Entries {
els, eerr := f.setElements(e, interval)
if eerr != nil {
return eerr
}
elems = append(elems, els...)
}
if err := c.AddSet(&nftables.Set{
Table: f.tableRef(),
Name: set.Name,
KeyType: keyType,
Interval: interval,
}, elems); err != nil {
return err
}
return c.Flush()
}
// RemoveAddressSet removes an address set by name.
func (f *NFT) RemoveAddressSet(ctx context.Context, name string) error {
if err := f.ensureTable(ctx); err != nil {
return err
}
c, err := nftConn()
if err != nil {
return err
}
c.DelSet(&nftables.Set{Table: f.tableRef(), Name: name})
if err := c.Flush(); err != nil {
if f.isNotExist(err) {
return nil
}
return err
}
return nil
}
// setEntryElements resolves the set an entry is being written to and renders the
// entry as its element(s), so an interval set's boundary markers match the set's
// actual definition rather than a guess.
func (f *NFT) setEntryElements(c *nftables.Conn, name, entry string) (*nftables.Set, []nftables.SetElement, error) {
set, err := c.GetSetByName(f.tableRef(), name)
if err != nil {
if f.isNotExist(err) {
return nil, nil, fmt.Errorf("address set %q not found", name)
}
return nil, nil, err
}
elems, err := f.setElements(entry, set.Interval)
if err != nil {
return nil, nil, err
}
return set, elems, nil
}
// AddAddressSetEntry adds an entry to the named set.
func (f *NFT) AddAddressSetEntry(ctx context.Context, name, entry string) error {
if err := f.ensureTable(ctx); err != nil {
return err
}
c, err := nftConn()
if err != nil {
return err
}
set, elems, err := f.setEntryElements(c, name, entry)
if err != nil {
return err
}
if err := c.SetAddElements(set, elems); err != nil {
return err
}
return c.Flush()
}
// RemoveAddressSetEntry removes an entry from the named set.
func (f *NFT) RemoveAddressSetEntry(ctx context.Context, name, entry string) error {
if err := f.ensureTable(ctx); err != nil {
return err
}
c, err := nftConn()
if err != nil {
return err
}
set, elems, err := f.setEntryElements(c, name, entry)
if err != nil {
return err
}
if err := c.SetDeleteElements(set, elems); err != nil {
return err
}
return c.Flush()
}
// setRefFamily resolves the single family of the named address set(s) a rule
// references through the shared resolver core, reading each set from this
// backend's own store — nft named sets are not kernel ipsets, so the ipset
// resolver cannot see them. A named set is family-typed, so a family-agnostic
// set-referencing rule is pinned to the set's own family rather than rejected:
// its rows could never match the other family anyway.
func (f *NFT) setRefFamily(c *nftables.Conn, source, destination string) (Family, error) {
return setRefFamilyFrom(func(name string) (Family, bool, error) {
set, err := f.getAddressSet(c, name)
if err != nil {
return FamilyAny, false, err
}
if set == nil {
return FamilyAny, false, nil
}
return set.Family, true, nil
}, source, destination)
}
// resolveSetRefFamily returns r pinned to its referenced set's family when the
// rule is family-agnostic and names a set; every other rule passes through
// unchanged. Callers resolve before marshalling, so MarshalRule never sees an
// unpinned set reference.
func (f *NFT) resolveSetRefFamily(ctx context.Context, c *nftables.Conn, r *Rule) (*Rule, error) {
return resolveSetRefRule(r, func(source, destination string) (Family, error) {
return f.setRefFamily(c, source, destination)
})
}
// resolveNATSetRefFamily is resolveSetRefFamily for NAT rules.
func (f *NFT) resolveNATSetRefFamily(ctx context.Context, c *nftables.Conn, r *NATRule) (*NATRule, error) {
return resolveSetRefNAT(r, func(source, destination string) (Family, error) {
return f.setRefFamily(c, source, destination)
})
}
// -----------------------------------------------------------------------------
// Backup and lifecycle
// -----------------------------------------------------------------------------
// Backup captures the filter and NAT rules in this backend's private table.
func (f *NFT) Backup(ctx context.Context, zoneName string) (*Backup, error) {
c, err := nftConn()
if err != nil {
return nil, err
}
sets := newSetReader(c)
// Read the private table directly rather than GetRules: Restore refills only
// this table, so the backup must not pull in rules from foreign tables (they
// would be re-added into the wrong table on Restore).
rules, err := f.listOwnRules(c, sets)
if err != nil {
return nil, err
}
natRules, err := f.listOwnNATRules(c, sets)
if err != nil {
return nil, err
}
backup := &Backup{Rules: rules, NATRules: natRules}
if err := captureBackupState(ctx, f, zoneName, backup); err != nil {
return nil, err
}
return backup, nil
}
// Restore replaces the managed rules with the contents of a Backup.
func (f *NFT) Restore(ctx context.Context, zoneName string, backup *Backup) error {
if backup == nil {
return fmt.Errorf("backup cannot be nil")
}
if err := f.ensureTable(ctx); err != nil {
return err
}
if err := f.ensureNATChains(ctx); err != nil {
return err
}
// Clear the modeled rows by handle rather than flushing the table: an
// unmodeled row (a foreign construct hand-added into the private table) is
// invisible to Backup, so a flush would destroy state the snapshot cannot
// reproduce.
if err := f.clearModeledRows(ctx); err != nil {
return err
}
// Recreate the sets on a clean slate before the rules that reference them. The
// clear above removed every modeled rule, so no modeled rule holds a set
// reference and each set can be removed and rebuilt; the clean rebuild is
// required because AddAddressSet is a no-op on an existing set and would not
// otherwise restore a flushed set's elements. An unmodeled row that still
// references a set surfaces here as a delete-set error rather than being
// silently destroyed.
if err := restoreBackupSets(ctx, f, backup, true); err != nil {
return err
}
for _, r := range backup.Rules {
if err := f.AddRule(ctx, zoneName, r); err != nil {
return err
}
}
for _, r := range backup.NATRules {
if err := f.AddNATRule(ctx, zoneName, r); err != nil {
return err
}
}
return applyBackupPolicy(ctx, f, zoneName, backup)
}
// clearModeledRows deletes every modeled rule row from the private table's
// filter and nat chains by handle, in one netlink transaction, leaving unmodeled
// (opaque) rows in place. Chain hooks and policies are untouched. The cleared
// rows' per-source meter sets are swept afterwards, best-effort, so a restore
// does not strand counting state; a rule the restore re-adds auto-creates its
// set again.
func (f *NFT) clearModeledRows(ctx context.Context) error {
c, err := nftConn()
if err != nil {
return err
}
sets := newSetReader(c)
tbl := f.tableRef()
staged := false
var staleRules []*Rule
var staleMatched []bool
for _, chain := range nftFilterChains {
rules, handles, lerr := f.listChain(c, sets, chain)
if lerr != nil {
return lerr
}
ch := f.chainRef(chain)
for i, e := range rules {
if e == nil {
continue
}
if derr := c.DelRule(&nftables.Rule{Table: tbl, Chain: ch, Handle: handles[i]}); derr != nil {
return derr
}
staged = true
staleRules = append(staleRules, e)
staleMatched = append(staleMatched, true)
}
}
for _, chain := range nftNATChains {
rules, handles, lerr := f.listNATChain(c, sets, chain)
if lerr != nil {
return lerr
}
ch := f.chainRef(chain)
for i, e := range rules {
if e == nil {
continue
}
if derr := c.DelRule(&nftables.Rule{Table: tbl, Chain: ch, Handle: handles[i]}); derr != nil {
return derr
}
staged = true
}
}
if !staged {
return nil
}
if err := c.Flush(); err != nil {
return err
}
f.sweepMeterSets(staleRules, staleMatched)
return nil
}
// Reload is a no-op; nftables applies changes immediately, so there is nothing to reload.
func (f *NFT) Reload(ctx context.Context) error {
return nil
}
// Close closes the connection to the manager.
func (f *NFT) Close(ctx context.Context) error {
return nil
}