3264 lines
98 KiB
Go
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
|
|
}
|