Files
Parth SarthiandgVisor bot 6ccc20c026 Nftables: Support NFTA_CHAIN_COUNTERS.
Chain Counters count the number
  of packets or bytes the BaseChain evaluated.

PiperOrigin-RevId: 968741662
2026-08-21 16:02:20 -07:00

1947 lines
63 KiB
Go

// Copyright 2024 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package nftables
import (
"fmt"
"math"
"slices"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/atomicbitops"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/rand"
"gvisor.dev/gvisor/pkg/sentry/socket/netlink/nlmsg"
"gvisor.dev/gvisor/pkg/syserr"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
//
// Interface-Related Methods
//
// CheckPrerouting checks at the Prerouting hook if the packet should continue traversing the stack.
func (nf *NFTables) CheckPrerouting(pkt *stack.PacketBuffer, route *stack.Route, af stack.AddressFamily) bool {
return nf.checkHook(pkt, route, af, stack.NFPrerouting)
}
// CheckInput checks at the Input hook if the packet should continue traversing the stack.
func (nf *NFTables) CheckInput(pkt *stack.PacketBuffer, route *stack.Route, af stack.AddressFamily) bool {
return nf.checkHook(pkt, route, af, stack.NFInput)
}
// CheckForward checks at the Forward hook if the packet should continue traversing the stack.
func (nf *NFTables) CheckForward(pkt *stack.PacketBuffer, route *stack.Route, af stack.AddressFamily) bool {
return nf.checkHook(pkt, route, af, stack.NFForward)
}
// CheckOutput checks at the Output hook if the packet should continue traversing the stack.
func (nf *NFTables) CheckOutput(pkt *stack.PacketBuffer, route *stack.Route, af stack.AddressFamily) bool {
return nf.checkHook(pkt, route, af, stack.NFOutput)
}
// CheckPostrouting checks at the Postrouting hook if the packet should continue traversing the stack.
func (nf *NFTables) CheckPostrouting(pkt *stack.PacketBuffer, route *stack.Route, af stack.AddressFamily) bool {
return nf.checkHook(pkt, route, af, stack.NFPostrouting)
}
// CheckIngress checks at the Ingress hook if the packet should continue traversing the stack.
func (nf *NFTables) CheckIngress(pkt *stack.PacketBuffer, route *stack.Route, af stack.AddressFamily) bool {
return nf.checkHook(pkt, route, af, stack.NFIngress)
}
// CheckEgress checks at the Egress hook if the packet should continue traversing the stack.
func (nf *NFTables) CheckEgress(pkt *stack.PacketBuffer, route *stack.Route, af stack.AddressFamily) bool {
return nf.checkHook(pkt, route, af, stack.NFEgress)
}
// checkHook returns true if the packet should continue traversing the stack or false
// if the packet should be dropped.
// If NFTables is not enabled, the packet is always allowed to continue traversing the stack.
func (nf *NFTables) checkHook(pkt *stack.PacketBuffer, route *stack.Route, af stack.AddressFamily, hook stack.NFHook) bool {
if !IsNFTablesEnabled() {
return true
}
v, err := nf.EvaluateHook(af, hook, pkt, route)
if err != nil {
return false
}
return v.Code == VC(linux.NF_ACCEPT)
}
// isTerminalCode returns if the hook evaluation should terminate now.
func isTerminalCode(code uint32) bool {
// NF_ACCEPT should continue the evaluation of the other base chains.
switch code {
case VC(linux.NF_DROP), VC(linux.NF_STOLEN), VC(linux.NF_QUEUE):
return true
}
return false
}
//
// Core Evaluation Functions
//
// evaluateNATBaseChains evaluates the packet through the NAT base chains;
// if NAT has not been configured for this packet.
// Reserves a no-op NAT if no rules matched.
// Returns error and whether NAT is/was configured.
func (nf *NFTables) evaluateNATBaseChains(pkt *stack.PacketBuffer, route *stack.Route, hook stack.NFHook, family stack.AddressFamily, regs *registerSet) (*syserr.AnnotatedError, bool) {
natType := stack.NfHookToNATType(hook)
if natType == stack.NATUnknown {
return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, fmt.Sprintf("invalid hook: %s for NATType: %s", hook, natType)), false
}
// Check if NAT has already been configured for this packet.
// If so, skip the evaluation of the NAT base chains.
if pkt.IsNATConfigured(natType) {
return nil, true
}
baseChains, err := nf.getBaseChainsForEvaluation(family, hook, true /* natChains */)
if err != nil {
return err, false
}
evalCtx := opEvalCtx{
pkt: pkt,
route: route,
hook: hook,
nftState: nf,
}
for _, bc := range baseChains {
if _, dormant := bc.table.flagSet[TableFlagDormant]; dormant {
continue
}
// Evaluate the packet through the base chain.
if err := bc.evaluate(regs, evalCtx); err != nil {
return err, false
}
// If the verdict is a terminal code, NAT will not be configured for this
// packet.
if isTerminalCode(regs.Verdict().Code) {
return nil, false
}
// If the current base chain configured NAT for this packet, work is done.
if pkt.IsNATConfigured(natType) {
return nil, true
}
}
// If none of the base chains configured NAT for this packet, reserve a no-op NAT.
// As NAT rules should be only evaluated once for a connection, a no-op NAT must be reserved
// to ensure the packet skips the NAT base chains again.
if !pkt.ConfigureNoopNAT(natType) {
return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, fmt.Sprintf("failed to reserve no-op NAT for packet: %v", pkt)), false
}
return nil, true
}
// evaluateNAT evaluates then applies NAT to the packet.
func (nf *NFTables) evaluateNAT(pkt *stack.PacketBuffer, route *stack.Route, hook stack.NFHook, family stack.AddressFamily, regs *registerSet) *syserr.AnnotatedError {
if !pkt.IsConnTrackConfigured() {
log.Warningf("ConnTrack should be configured before NAT")
return nil
}
err, natConfigured := nf.evaluateNATBaseChains(pkt, route, hook, family, regs)
if err != nil {
return err
}
// If NAT is not configured for this packet, nothing to do.
if !natConfigured {
return nil
}
// Apply NAT to the packet;
// i.e. rewrite the packet headers and update the conntrack state.
_ = stack.NFTApplyNAT(pkt, hook, route)
regs.verdict.Code = VC(linux.NF_ACCEPT)
return nil
}
// extraEvaluator is a struct that holds a priority and a function to call during
// functions other than base chain evaluations.
type extraEvaluator struct {
priority int
handle func() *syserr.AnnotatedError
}
// getExtraEvaluators returns a list of extra evaluators beside the nft chains.
func (nf *NFTables) getExtraEvaluators(family stack.AddressFamily, hook stack.NFHook, pkt *stack.PacketBuffer, route *stack.Route, regs *registerSet) []*extraEvaluator {
var e []*extraEvaluator
// Get ConnTrack evaluator.
connTrackHookPriority, connTrackOk := stack.NfConnTrackPriority(hook)
if nf.connTrack != nil && connTrackOk {
e = append(e, &extraEvaluator{
priority: connTrackHookPriority,
handle: func() *syserr.AnnotatedError {
switch hook {
case stack.NFPrerouting, stack.NFOutput:
nf.connTrack.GetConnAndUpdatePkt(pkt, hook == stack.NFOutput /* skipChecksumValidation */)
case stack.NFInput, stack.NFPostrouting:
if !pkt.FinalizeConnTrack() {
return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, fmt.Sprintf("failed to finalize connTrack for packet: %v", pkt))
}
}
return nil
}})
}
// Get NAT evaluator.
natHookPriority, natOk := stack.NfNATPriority(hook)
if nf.natEnabled && natOk {
e = append(e, &extraEvaluator{
priority: natHookPriority,
handle: func() *syserr.AnnotatedError {
return nf.evaluateNAT(pkt, route, hook, family, regs)
},
})
}
// Sort the evaluators by priority.
slices.SortFunc(e, func(a, b *extraEvaluator) int {
return a.priority - b.priority
})
return e
}
func (nf *NFTables) getBaseChainsForEvaluation(family stack.AddressFamily, hook stack.NFHook, natChains bool) ([]*Chain, *syserr.AnnotatedError) {
getBaseChains := func(hf *hookFunctionStack) []*Chain {
if natChains {
return hf.natBaseChains
}
return hf.baseChains
}
switch family {
case stack.IP:
return getBaseChains(&nf.ip4InetBaseChains[hook]), nil
case stack.IP6:
return getBaseChains(&nf.ip6InetBaseChains[hook]), nil
case stack.Inet:
// A packet can be of only IP or IP6 address family.
return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "inet address family is not supported for hook evaluation")
}
if nf.filters[family] == nil {
return nil, nil
}
filter := nf.filters[family]
if filter.hfStacks[hook] == nil {
return nil, nil
}
return getBaseChains(filter.hfStacks[hook]), nil
}
// EvaluateHook evaluates a packet using the rules of the given hook for the
// given address family, returning a netfilter verdict and modifying the packet
// in place.
// Returns an error if address family or hook is invalid or they don't match.
// TODO(b/345684870): Consider removing error case if we never return an error.
func (nf *NFTables) EvaluateHook(family stack.AddressFamily, hook stack.NFHook, pkt *stack.PacketBuffer, route *stack.Route) (Verdict, *syserr.AnnotatedError) {
// Note: none of the other evaluate functions are public because they require
// jumping to different chains in the same table, so all chains, rules, and
// operations must be tied to a table. Thus, calling evaluate for standalone
// chains, rules, or operations can be misleading and dangerous.
// Ensures address family is valid.
if err := validateAddressFamily(family); err != nil {
return Verdict{}, err
}
// Ensures hook is valid.
if err := validateHook(hook, family); err != nil {
return Verdict{}, err
}
baseChains, err := nf.getBaseChainsForEvaluation(family, hook, false /* natChains */)
if err != nil {
return Verdict{}, err
}
// Create a new register set for the evaluation.
regs := newRegisterSet()
// Evaluates packet through all base chains for given hook in priority order.
extraEvaluators := nf.getExtraEvaluators(family, hook, pkt, route, &regs)
ei := 0 // Extra evaluator iterator.
numExtraHooks := len(extraEvaluators)
ci := 0 // Base chain iterator.
numBaseChains := len(baseChains)
// If there's nothing to evaluate, return accept.
if numBaseChains == 0 && numExtraHooks == 0 {
return Verdict{Code: VC(linux.NF_ACCEPT)}, nil
}
var lastEvaluatedChain *Chain
evalCtx := opEvalCtx{
pkt: pkt,
route: route,
hook: hook,
nftState: nf,
}
// Evaluate base chains and extra evaluators in priority order.
for ei < numExtraHooks || ci < numBaseChains {
selectExtra := (ci == numBaseChains) || (ei < numExtraHooks && extraEvaluators[ei].priority < baseChains[ci].GetBaseChainInfo().Priority.GetValue())
if selectExtra {
if err := extraEvaluators[ei].handle(); err != nil {
return Verdict{}, err
}
ei++
} else {
bc := baseChains[ci]
ci++
// Doesn't evaluate chain if it's table is flagged as dormant.
if _, dormant := bc.table.flagSet[TableFlagDormant]; dormant {
continue
}
if err := bc.evaluate(&regs, evalCtx); err != nil {
return Verdict{}, err
}
lastEvaluatedChain = bc
}
// Terminates immediately on netfilter terminal verdicts.
if v := regs.Verdict(); isTerminalCode(v.Code) {
return v, nil
}
}
// Returns policy verdict of the last base chain evaluated if no terminal
// verdict was issued.
switch regs.Verdict().Code {
case VC(linux.NFT_CONTINUE), VC(linux.NFT_RETURN):
if lastEvaluatedChain != nil && lastEvaluatedChain.GetBaseChainInfo().PolicyDrop {
return Verdict{Code: VC(linux.NF_DROP)}, nil
}
return Verdict{Code: VC(linux.NF_ACCEPT)}, nil
case VC(linux.NF_ACCEPT):
return Verdict{Code: VC(linux.NF_ACCEPT)}, nil
}
panic(fmt.Sprintf("unexpected verdict from hook evaluation: %s", VerdictCodeToString(regs.Verdict().Code)))
}
// evaluateFromRule is a helper function for Chain.evaluate that evaluates the
// packet through the rules in the chain starting at the specified rule index.
func (c *Chain) evaluateFromRule(rIdx int, jumpDepth int, regs *registerSet, evalCtx opEvalCtx) *syserr.AnnotatedError {
if jumpDepth >= nestedJumpLimit {
return syserr.NewAnnotatedError(syserr.ErrTooManyLinks, fmt.Sprintf("exceeded nested jump limit of %d", nestedJumpLimit))
}
// Resets verdict to continue for the next rule.
regs.verdict.Code = VC(linux.NFT_CONTINUE)
// Evaluates all rules in the chain (breaking on terminal verdicts).
evalLoop:
for ; rIdx < len(c.rules); rIdx++ {
rule := c.rules[rIdx]
evalCtx.rule = rule
if err := rule.evaluate(regs, evalCtx); err != nil {
return err
}
// Continues evaluation at target chains for jump and goto verdicts.
jumped := false
v := regs.Verdict()
switch v.Code {
case VC(linux.NFT_JUMP):
jumpDepth++
jumped = true
fallthrough
case VC(linux.NFT_GOTO):
nextChain := v.Chain
if nextChain == nil {
return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "chain not found for jump/goto verdict")
}
if err := nextChain.evaluateFromRule(0, jumpDepth, regs, evalCtx); err != nil {
return err
}
// Ends evaluation for goto (and continues evaluation for jump).
if !jumped {
break evalLoop
}
jumpDepth--
}
// Update verdict after jumps.
v = regs.Verdict()
// Only continues evaluation for Continue and Break verdicts.
switch v.Code {
case VC(linux.NFT_RETURN):
regs.verdict.Code = VC(linux.NFT_CONTINUE)
return nil
case VC(linux.NFT_BREAK):
// Resets verdict for next rule (after breaking from a single operation).
regs.verdict.Code = VC(linux.NFT_CONTINUE)
case VC(linux.NFT_CONTINUE):
// Goes to next rule.
continue
default:
// Break evaluation for all the netfilter verdicts.
break evalLoop
}
}
return nil
}
// evaluate for Chain evaluates the packet through the chain's rules and returns
// the verdict and modifies the packet in place.
func (c *Chain) evaluate(regs *registerSet, evalCtx opEvalCtx) *syserr.AnnotatedError {
if c.counter != nil {
c.counter.Add(1, uint64(evalCtx.pkt.Size()))
}
return c.evaluateFromRule(0, 0, regs, evalCtx)
}
// evaluate evaluates the rule on the given packet and register set, changing
// the register set and possibly the packet in place.
// The verdict in regs.Verdict() may be an nf table internal verdict or a
// netfilter terminal verdict.
func (r *Rule) evaluate(regs *registerSet, evalCtx opEvalCtx) *syserr.AnnotatedError {
for _, op := range r.ops {
op.evaluate(regs, evalCtx)
if regs.Verdict().Code != VC(linux.NFT_CONTINUE) {
break
}
}
return nil
}
//
// Top-Level NFTables Functions
// Note: Provides wrapper functions for the creation and deletion of tables,
// chains, and rules for convenience.
//
// NewNFTables creates a new NFTables state object using the given clock for
// timing operations.
// Note: Expects random number generator to be initialized with a seed.
func NewNFTables(stack *stack.Stack, clock tcpip.Clock, rng rand.RNG) *NFTables {
if clock == nil {
panic("nftables state must be initialized with a non-nil clock")
}
if rng.Reader == nil {
panic("nftables state must be initialized with a non-nil random number generator")
}
return &NFTables{stack: stack, clock: clock, startTime: clock.Now(), rng: rng, tableHandleCounter: atomicbitops.Uint64{}, genid: 1}
}
// GetGenID returns the generation ID for the NFTables object.
func (nf *NFTables) GetGenID() uint32 {
return nf.genid
}
// SetGenID sets the generation ID for the NFTables object.
func (nf *NFTables) SetGenID(genid uint32) {
nf.genid = genid
}
// PruneUnused deletes unused/non-referenced internal objects.
func (nf *NFTables) PruneUnused() {
for _, filter := range nf.filters {
if filter == nil {
continue
}
for _, tab := range filter.tables {
tab.pruneUnused()
}
}
}
// CanCommitChains checks that all the new bounded chains are valid.
// Ref: net/netfilter/nf_tables_api.c:nf_tables_commit()
func (nf *NFTables) CanCommitChains(newChains []*Chain) *syserr.AnnotatedError {
for _, chain := range newChains {
if !chain.IsAnonymousChain() {
continue
}
if !chain.IsBound() {
return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "anonymous chains must have jump/goto reference")
}
if chain.GetChainUse() > 1 {
return syserr.NewAnnotatedError(syserr.ErrBusy, "anonymous chain already in use")
}
}
return nil
}
// Flush clears all data for all address families or
// a specific family/table if filtered.
// It skips tables that are not owned by the given owner.
func (nf *NFTables) Flush(familyFilter stack.AddressFamily, attrs map[uint16]nlmsg.BytesView, owner uint32) *syserr.AnnotatedError {
for family := range stack.NumAFs {
if familyFilter != stack.Unspec &&
stack.AddressFamily(family) != familyFilter {
continue
}
afFilter := nf.filters[family]
if afFilter == nil {
continue
}
var attrName *string = nil
if nameBytes, ok := attrs[linux.NFTA_TABLE_NAME]; ok {
name := nameBytes.String()
attrName = &name
}
var tablesToDelete []TableInfo
for name, table := range afFilter.tables {
// Caller cannot delete a table they do not own.
if table.HasOwner() && table.GetOwner() != owner {
continue
}
if attrName != nil && *attrName != table.GetName() {
continue
}
for _, chain := range table.chains {
if err := nf.removeChainFromCache(chain); err != nil {
return err
}
}
table.destroy()
tablesToDelete = append(tablesToDelete, TableInfo{Name: name, Handle: table.GetHandle()})
}
for _, tableData := range tablesToDelete {
delete(afFilter.tables, tableData.Name)
delete(afFilter.tableHandles, tableData.Handle)
}
}
return nil
}
// FlushAddressFamily clears ruleset and all data for the given address family,
// returning an error if the address family is invalid.
func (nf *NFTables) FlushAddressFamily(family stack.AddressFamily) *syserr.AnnotatedError {
// Ensures address family is valid.
if err := validateAddressFamily(family); err != nil {
return err
}
nf.filters[family] = nil
return nil
}
// GetAddressFamilyTables returns the tables for the given address family.
func (nf *NFTables) GetAddressFamilyTables(family stack.AddressFamily) map[string]*Table {
afFilter := nf.filters[family]
if afFilter == nil {
// An empty map is safe to iterate over.
return nil
}
return afFilter.tables
}
// GetTable validates the inputs and gets a table if it exists, error otherwise.
func (nf *NFTables) GetTable(family stack.AddressFamily, tableName string, portID uint32) (*Table, *syserr.AnnotatedError) {
// Ensures address family is valid.
if err := validateAddressFamily(family); err != nil {
return nil, err
}
// Checks if the table map for the address family has been initialized.
if nf.filters[family] == nil || nf.filters[family].tables == nil {
return nil, syserr.NewAnnotatedError(syserr.ErrNoFileOrDir, fmt.Sprintf("table map for address family %v has no tables", family))
}
// Gets the corresponding table map for the address family.
tableMap := nf.filters[family].tables
// Checks if a table with the name exists.
t, exists := tableMap[tableName]
if !exists {
return nil, syserr.NewAnnotatedError(syserr.ErrNoFileOrDir, fmt.Sprintf("table %s not found for address family %v", tableName, family))
}
// If the table has an owner, it must match the Netlink portID of the calling process.
// User space processes only have non-zero port ids.
// Only the kernel can have a zero port id.
if t.HasOwner() && portID != 0 && portID != t.GetOwner() {
return nil, syserr.NewAnnotatedError(syserr.ErrNotPermitted, fmt.Sprintf("table %s has owner %d, which does not match the Netlink portID of the calling process %d", tableName, t.GetOwner(), portID))
}
return t, nil
}
// GetTableByHandle validates the inputs and gets a table by its handle and family if it exists,
// error otherwise.
func (nf *NFTables) GetTableByHandle(family stack.AddressFamily, handle uint64, portID uint32) (*Table, *syserr.AnnotatedError) {
// Ensures address family is valid.
if err := validateAddressFamily(family); err != nil {
return nil, err
}
// Checks if the table handle map for the address family has been initialized.
if nf.filters[family] == nil || nf.filters[family].tableHandles == nil {
return nil, syserr.NewAnnotatedError(syserr.ErrNoFileOrDir, fmt.Sprintf("table handle map for address family %v has no tables", family))
}
// Gets the corresponding table map for the address family.
tableHandleMap := nf.filters[family].tableHandles
// Checks if a table with the name exists.
t, exists := tableHandleMap[handle]
if !exists {
return nil, syserr.NewAnnotatedError(syserr.ErrNoFileOrDir, fmt.Sprintf("table with handle %d not found for address family %v", handle, family))
}
// If the table has an owner, it must match the Netlink portID of the calling process.
// User space processes only have non-zero port ids.
// Only the kernel can have a zero port id.
if t.HasOwner() && portID != 0 && portID != t.GetOwner() {
return nil, syserr.NewAnnotatedError(syserr.ErrNotPermitted, fmt.Sprintf("table with handle %d has owner %d, which does not match the Netlink portID of the calling process %d", handle, t.GetOwner(), portID))
}
return t, nil
}
// AddTable makes a new table for the specified address family, returning an
// error if the address family is invalid. Can return an error if a table by the
// same name already exists if errorOnDuplicate is true. Can be used to get an
// existing table by the same name if errorOnDuplicate is false.
// Note: if the table already exists, the existing table is returned without any
// modifications.
// Note: Table initialized as not dormant.
func (nf *NFTables) AddTable(family stack.AddressFamily, name string,
errorOnDuplicate bool) (*Table, *syserr.AnnotatedError) {
// Ensures address family is valid.
if err := validateAddressFamily(family); err != nil {
return nil, err
}
// Initializes filter if first table for the address family.
if nf.filters[family] == nil {
nf.filters[family] = &addressFamilyFilter{
family: family,
nftState: nf,
tables: make(map[string]*Table),
tableHandles: make(map[uint64]*Table),
hfStacks: make(map[stack.NFHook]*hookFunctionStack),
}
}
// Gets the corresponding table map for the address family.
tableMap := nf.filters[family].tables
tableHandleMap := nf.filters[family].tableHandles
// Checks if a table with the same name already exists. If so, returns the
// existing table (unless errorOnDuplicate is true).
if existingTable, exists := tableMap[name]; exists {
if errorOnDuplicate {
return nil, syserr.NewAnnotatedError(syserr.ErrExists, fmt.Sprintf("table %s already exists in address family %v", name, family))
}
return existingTable, nil
}
// Creates the new table and add it to the table map.
t := &Table{
name: name,
afFilter: nf.filters[family],
chains: make(map[string]*Chain),
chainHandles: make(map[uint64]*Chain),
flagSet: make(map[TableFlag]struct{}),
handle: nf.getNewTableHandle(),
handleCounter: atomicbitops.Uint64{},
sets: make(map[string]*nftSet),
setHandles: make(map[uint64]*nftSet),
}
tableMap[name] = t
tableHandleMap[t.handle] = t
return t, nil
}
// getNewTableHandle returns a new table handle for the NFTables object.
func (nf *NFTables) getNewTableHandle() uint64 {
return nf.tableHandleCounter.Add(1)
}
// CreateTable makes a new table for the specified address family like AddTable
// but also returns an error if a table by the same name already exists.
// Note: this interface mirrors the difference between the create and add
// commands within the nft binary.
func (nf *NFTables) CreateTable(family stack.AddressFamily, name string) (*Table, *syserr.AnnotatedError) {
return nf.AddTable(family, name, true)
}
// DeleteTable deletes the specified table from the NFTables object returning
// true if the table was deleted and false if the table doesn't exist. Returns
// an error if the address family is invalid.
func (nf *NFTables) DeleteTable(family stack.AddressFamily, tableName string) (bool, *syserr.AnnotatedError) {
// Ensures address family is valid.
if err := validateAddressFamily(family); err != nil {
return false, err
}
// Gets and checks the table.
t, err := nf.GetTable(family, tableName, 0)
if err != nil {
return false, err
}
for _, c := range t.chains {
if err := nf.removeChainFromCache(c); err != nil {
return false, err
}
}
t.destroy()
// Deletes the table from the table map and from the table handle map.
delete(nf.filters[family].tables, tableName)
delete(nf.filters[family].tableHandles, t.handle)
return true, nil
}
// GetChain validates the inputs and gets a chain if it exists, error otherwise.
func (nf *NFTables) GetChain(family stack.AddressFamily, tableName string, chainName string) (*Chain, *syserr.AnnotatedError) {
// Gets and checks the table.
t, err := nf.GetTable(family, tableName, 0)
if err != nil {
return nil, err
}
return t.GetChain(chainName)
}
// AddChainToTable makes a new chain for the corresponding table and adds it to the
// chain map and hook function list, returning an error if the address family is
// invalid or the table doesn't exist. Can return an error if a chain by the
// same name already exists if errorOnDuplicate is true. Can be used to get an
// existing chain by the same name if errorOnDuplicate is false.
// Note: if the chain already exists, the existing chain is returned without any
// modifications.
// Note: if the chain is not a base chain, info should be nil.
func (nf *NFTables) AddChainToTable(tab *Table, chainName string, bcInfo *BaseChainInfo, comment string, errorOnDuplicate bool, chainFlags uint32, udata []byte, policy uint8) (*Chain, *syserr.AnnotatedError) {
// Add the chain to the table, appending, by priority, to the stack of base
// chains for the hook.
chain, err := tab.addChain(chainName, bcInfo, comment, errorOnDuplicate)
if err != nil {
return nil, err
}
chain.SetFlags(uint8(chainFlags))
if udata != nil {
if err := chain.SetUserData(udata); err != nil {
return nil, err
}
}
if chain.IsBaseChain() {
info := chain.GetBaseChainInfo()
if info == nil {
return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, fmt.Sprintf("base chain info is nil for chain %s", chainName))
}
info.PolicyDrop = policy == linux.NF_DROP
if err := nf.addChainToCache(chain); err != nil {
return nil, err
}
// Initialize ConnTrack and NAT when the first nat expression is added to the rule.
// No locks are needed as each update happens
// in a new copy.
if bcInfo.BcType == BaseChainTypeNat {
nf.InitConnTrackOnce()
nf.SetNATState(true /*enabled*/)
}
}
return chain, nil
}
// getNewHandle returns a new handle for a chain or rule.
func (t *Table) getNewHandle() uint64 {
return t.handleCounter.Add(1)
}
// DeleteChainFromTable deletes the specified chain from the NFTables object returning
// true if the chain was deleted and false if the chain doesn't exist. Returns
// an error if the address family is invalid or the table doesn't exist.
func (nf *NFTables) DeleteChainFromTable(tab *Table, chainName string) (bool, *syserr.AnnotatedError) {
// Checks if the chain exists.
c, exists := tab.chains[chainName]
if !exists {
return false, nil
}
if err := nf.removeChainFromCache(c); err != nil {
return false, err
}
return tab.deleteChain(c), nil
}
// TableCount returns the number of tables in the NFTables object.
func (nf *NFTables) TableCount() int {
return len(nf.filters)
}
// removeChainFromCache removes the specified chain from the cache.
// The cache is composed of base chains.
func (nf *NFTables) removeChainFromCache(c *Chain) *syserr.AnnotatedError {
if !c.IsBaseChain() {
return nil
}
hook := c.GetBaseChainInfo().Hook
af := c.GetAddressFamily()
switch af {
case stack.IP:
return nf.ip4InetBaseChains[hook].detachBaseChain(c)
case stack.IP6:
return nf.ip6InetBaseChains[hook].detachBaseChain(c)
case stack.Inet:
if err := nf.ip4InetBaseChains[hook].detachBaseChain(c); err != nil {
return err
}
return nf.ip6InetBaseChains[hook].detachBaseChain(c)
}
return nil
}
// addChainToCache adds the base chain to the cache.
// The cache is just a merged list of base chains for IPv4-inet and IPv6-inet
// address families. The list is sorted by priority.
func (nf *NFTables) addChainToCache(c *Chain) *syserr.AnnotatedError {
if !c.IsBaseChain() {
return nil
}
hook := c.GetBaseChainInfo().Hook
af := c.GetAddressFamily()
switch af {
case stack.IP:
return nf.ip4InetBaseChains[hook].attachBaseChain(c)
case stack.IP6:
return nf.ip6InetBaseChains[hook].attachBaseChain(c)
case stack.Inet:
if err := nf.ip4InetBaseChains[hook].attachBaseChain(c); err != nil {
return err
}
if err := nf.ip6InetBaseChains[hook].attachBaseChain(c); err != nil {
return err
}
}
return nil
}
// InitConnTrackOnce initializes the conntrack.
// No need to lock the mutex as each NFTables update
// happens in a new copy of the NFTables struct.
func (nf *NFTables) InitConnTrackOnce() {
if nf.connTrack == nil {
nf.connTrack, nf.connTrackReaper = stack.NewConnTrackWithReaper(nf.clock, &nf.rng, nil /* seed */)
}
}
// GetConnTrack returns the conntrack.
func (nf *NFTables) GetConnTrack() *stack.ConnTrack {
return nf.connTrack
}
// SetNATState sets the NAT state of the NFTables object.
// Every new connection needs to go through the NAT configuration
// even if there are no NAT rules to apply.
// This variable is used to skip these NAT configurations
// for the new connections and gets only
// enabled when a NAT chain is added.
func (nf *NFTables) SetNATState(enabled bool) {
nf.natEnabled = enabled
}
//
// Table Functions
//
// GetName returns the name of the table.
func (t *Table) GetName() string {
return t.name
}
// GetAddressFamily returns the address family of the table.
func (t *Table) GetAddressFamily() stack.AddressFamily {
return t.afFilter.family
}
// GetHandle returns the handle of the table.
func (t *Table) GetHandle() uint64 {
return t.handle
}
// GetOwner returns the owner of the table.
func (t *Table) GetOwner() uint32 {
return t.owner
}
// SetOwner sets the owner of the table. If the table already has an owner, it
// is not updated.
func (t *Table) SetOwner(nlpid uint32) *syserr.AnnotatedError {
// This should only be called once, when setting the owner of a table for the first time.
if t.HasOwner() {
return syserr.NewAnnotatedError(syserr.ErrNotSupported, fmt.Sprintf("table %s already has an owner", t.name))
}
t.flagSet[TableFlagOwner] = struct{}{}
t.owner = nlpid
return nil
}
// HasOwner returns whether the table has an owner.
func (t *Table) HasOwner() bool {
_, ok := t.flagSet[TableFlagOwner]
return ok
}
// GetUserData returns the user data of the table.
func (t *Table) GetUserData() []byte {
return t.userData
}
// HasUserData returns whether the table has user data.
func (t *Table) HasUserData() bool {
return t.userData != nil
}
// SetUserData sets the user data of the table.
func (t *Table) SetUserData(data []byte) *syserr.AnnotatedError {
// User data should only be set once.
if t.userData != nil {
return syserr.NewAnnotatedError(syserr.ErrNotSupported, fmt.Sprintf("table %s already has user data", t.name))
}
t.userData = make([]byte, len(data))
copy(t.userData, data)
return nil
}
// IsDormant returns whether the table is dormant.
func (t *Table) IsDormant() bool {
_, dormant := t.flagSet[TableFlagDormant]
return dormant
}
// SetDormant sets the dormant flag for the table.
func (t *Table) SetDormant(dormant bool) {
if dormant {
t.flagSet[TableFlagDormant] = struct{}{}
} else {
delete(t.flagSet, TableFlagDormant)
}
}
// GetLinuxFlagSet returns the flag set of the table.
// Although user flags map to uint8 space, internal flags could eventually be
// supported, which together map to a uint32 space.
func (t *Table) GetLinuxFlagSet() (uint32, *syserr.AnnotatedError) {
var flags uint32 = 0
for flag := range t.flagSet {
switch flag {
case TableFlagDormant:
flags |= linux.NFT_TABLE_F_DORMANT
case TableFlagOwner:
flags |= linux.NFT_TABLE_F_OWNER
default:
return 0, syserr.NewAnnotatedError(syserr.ErrNotSupported, fmt.Sprintf("unsupported flag %v", flag))
}
}
return flags, nil
}
// GetLinuxUserFlagSet returns the user flag set of the table.
func (t *Table) GetLinuxUserFlagSet() (uint8, *syserr.AnnotatedError) {
flags, err := t.GetLinuxFlagSet()
if err != nil {
return 0, err
}
return uint8(flags & linux.NFT_TABLE_F_MASK), nil
}
// GetChain returns the chain with the specified name if it exists, error
// otherwise.
func (t *Table) GetChain(chainName string) (*Chain, *syserr.AnnotatedError) {
// Checks if a chain with the name exists.
c, exists := t.chains[chainName]
if !exists {
return nil, syserr.NewAnnotatedError(syserr.ErrNoFileOrDir, fmt.Sprintf("chain %s not found for table %s", chainName, t.name))
}
return c, nil
}
// GetChainByHandle returns the chain with the specified handle if it exists, error otherwise.
func (t *Table) GetChainByHandle(chainHandle uint64) (*Chain, *syserr.AnnotatedError) {
// Checks if a chain with the handle exists. We don't support transactions/generations of tables
// or chains, so those checks are not needed.
c, exists := t.chainHandles[chainHandle]
if !exists {
return nil, syserr.NewAnnotatedError(syserr.ErrNoFileOrDir, fmt.Sprintf("chain %d not found for table %s", chainHandle, t.name))
}
return c, nil
}
// GetChainByID returns the chain with the specified transaction ID if it exists.
func (t *Table) GetChainByID(chainID uint32) (*Chain, *syserr.AnnotatedError) {
c, exists := t.chainIDs[chainID]
if !exists {
return nil, syserr.NewAnnotatedError(syserr.ErrNoFileOrDir, fmt.Sprintf("chain ID %d not found for table %s", chainID, t.name))
}
return c, nil
}
// RegisterChainID registers a temporary batch transaction chain ID.
func (t *Table) RegisterChainID(chainID uint32, c *Chain) {
if t.chainIDs == nil {
t.chainIDs = make(map[uint32]*Chain)
}
// Ref: net/netfilter/nf_tables_api.c:nft_trans_chain_add()
// Linux allows overwriting the chain ID.
// No need to check for duplicates.
t.chainIDs[chainID] = c
}
// ClearChainIDs clears all temporary transaction chain IDs in the table.
func (t *Table) ClearChainIDs() {
t.chainIDs = nil
}
// pruneUnused removes unused/unreferenced table objects.
func (t *Table) pruneUnused() {
// Garbage collect unbounded chains.
// Unbounded chains are anonymous but have no references.
// So, no rule can hit them.
for {
var toDelete []*Chain
for _, c := range t.chains {
if c.IsAnonymousChain() && !c.IsBound() {
toDelete = append(toDelete, c)
}
}
if len(toDelete) == 0 {
break
}
for _, c := range toDelete {
t.deleteChain(c)
}
}
// Clear temporary transaction chain IDs.
t.ClearChainIDs()
}
// GetChains returns a map of all chains for the table.
func (t *Table) GetChains() map[string]*Chain {
return t.chains
}
// addChain makes a new chain for the table. Can return an error if a chain by
// the same name already exists if errorOnDuplicate is true.
func (t *Table) addChain(name string, info *BaseChainInfo, comment string, errorOnDuplicate bool) (*Chain, *syserr.AnnotatedError) {
// Checks if a chain with the same name already exists. If so, returns the
// existing chain (unless errorOnDuplicate is true).
if existingChain, exists := t.chains[name]; exists {
if errorOnDuplicate {
return nil, syserr.NewAnnotatedError(syserr.ErrExists, fmt.Sprintf("chain %s already exists for table %s", name, t.name))
}
return existingChain, nil
}
// Creates a new chain.
c := &Chain{
name: name,
table: t,
baseChainInfo: info,
comment: comment,
handleToRule: make(map[uint64]*Rule),
}
// Sets the base chain info if it's a base chain (and validates it).
if info != nil {
if err := c.setBaseChainInfo(info); err != nil {
return nil, err
}
}
// Only assign a chain handle after error checks.
c.handle = t.getNewHandle()
// Adds the chain to the chain map (after successfully doing everything else).
t.chains[name] = c
t.chainHandles[c.handle] = c
return c, nil
}
// deleteChain deletes the specified chain from the table returning true if the
// chain was deleted and false if the chain doesn't exist.
func (t *Table) deleteChain(c *Chain) bool {
// Detaches the chain from the pipeline if it's a base chain.
if c.baseChainInfo != nil {
hfStack := t.afFilter.hfStacks[c.baseChainInfo.Hook]
if err := hfStack.detachBaseChain(c); err != nil {
panic(fmt.Sprintf("failed to detach base chain %s from hook %v: %v", c.GetName(), c.baseChainInfo.Hook, err))
}
if len(hfStack.baseChains) == 0 && len(hfStack.natBaseChains) == 0 {
delete(t.afFilter.hfStacks, c.baseChainInfo.Hook)
}
}
// Deletes chain.
delete(t.chains, c.name)
delete(t.chainHandles, c.handle)
c.destroy()
return true
}
// deleteSet deletes the specified set from the table returning true if the
// set was deleted and false if the set doesn't exist.
func (t *Table) deleteSet(s *nftSet) bool {
if _, ok := t.sets[s.name]; !ok {
return false
}
delete(t.sets, s.name)
delete(t.setHandles, s.handle)
s.destroy()
return true
}
func (t *Table) destroy() {
// Destroy all rules in the table first to handle any chain and set use.
for _, c := range t.chains {
c.DeleteAllRules()
}
// Destroy all sets in the table before chains so that any element-level
// operations or verdicts referencing chains release their chainUse count.
for _, s := range t.sets {
t.deleteSet(s)
}
// Destroy all chains in the table.
for _, c := range t.chains {
t.deleteChain(c)
}
t.name = ""
t.chains = nil
t.chainHandles = nil
t.sets = nil
t.setHandles = nil
t.flagSet = nil
t.userData = nil
t.afFilter = nil
t.handle = 0
t.handleCounter.Store(0)
t.owner = 0
}
// ChainCount returns the number of chains in the table.
func (t *Table) ChainCount() int {
return len(t.chains)
}
// RenameChain renames an existing chain in the table.
// Ref: net/netfilter/nf_tables_api.c:nf_tables_updchain()
func (t *Table) RenameChain(c *Chain, newName string) *syserr.AnnotatedError {
currName := c.name
if currName == newName {
return nil
}
// Check for existing chain with a same name.
if existingChain, exists := t.chains[newName]; exists {
if existingChain != c {
return syserr.NewAnnotatedError(syserr.ErrExists, fmt.Sprintf("chain %s already exists for table %s", newName, t.name))
}
return nil
}
delete(t.chains, currName)
c.name = newName
t.chains[newName] = c
return nil
}
//
// Chain Functions
//
// GetName returns the name of the chain.
func (c *Chain) GetName() string {
return c.name
}
// SetName sets the name of the chain. This should only be called on
// a chain that is not yet attached to a table.
func (c *Chain) SetName(name string) *syserr.AnnotatedError {
if c.table != nil {
return syserr.NewAnnotatedError(syserr.ErrNotSupported, fmt.Sprintf("Cannot change the name of chain %s that is already attached to table %s", c.name, c.table.name))
}
c.name = name
return nil
}
// GetAddressFamily returns the address family of the chain.
func (c *Chain) GetAddressFamily() stack.AddressFamily {
return c.table.GetAddressFamily()
}
// GetTable returns the table that the chain belongs to.
func (c *Chain) GetTable() *Table {
return c.table
}
// GetHandle returns the handle of the chain.
func (c *Chain) GetHandle() uint64 {
return c.handle
}
// GetFlags returns the flags of the chain.
func (c *Chain) GetFlags() uint8 {
return c.flags
}
// SetFlags sets the flags of the chain.
func (c *Chain) SetFlags(flags uint8) {
c.flags = flags
}
// GetUserData returns the user data of the chain.
func (c *Chain) GetUserData() []byte {
return c.userData
}
// SetUserData sets the user data of the chain.
func (c *Chain) SetUserData(data []byte) *syserr.AnnotatedError {
// User data should only be set once.
if c.userData != nil {
return syserr.NewAnnotatedError(syserr.ErrNotSupported, fmt.Sprintf("chain %s already has user data", c.name))
}
c.userData = make([]byte, len(data))
copy(c.userData, data)
return nil
}
// HasUserData returns whether the chain has user data.
func (c *Chain) HasUserData() bool {
return c.userData != nil
}
// IsBaseChain returns whether the chain is a base chain.
func (c *Chain) IsBaseChain() bool {
return c.baseChainInfo != nil
}
// IsAnonymousChain returns true if the chain is anonymous.
func (c *Chain) IsAnonymousChain() bool {
return (c.flags&linux.NFT_CHAIN_BINDING != 0)
}
// IsBound returns true if the chain is an anonymous chain currently bound to a rule.
func (c *Chain) IsBound() bool {
return c.IsAnonymousChain() && c.GetChainUse() > 0
}
// IncrementChainUse increments the chain use value of the chain.
func (c *Chain) IncrementChainUse() bool {
if c.chainUse == math.MaxUint32 {
return false
}
c.chainUse++
return true
}
// GetBaseChainInfo returns the base chain info of the chain.
// Note: Returns nil if the chain is not a base chain.
func (c *Chain) GetBaseChainInfo() *BaseChainInfo {
return c.baseChainInfo
}
// SetPolicy sets the policy for a base chain.
func (c *Chain) SetPolicy(policy uint8) *syserr.AnnotatedError {
if !c.IsBaseChain() {
return syserr.NewAnnotatedError(syserr.ErrNotSupported, fmt.Sprintf("chain %s is not a base chain", c.name))
}
if policy != linux.NF_DROP && policy != linux.NF_ACCEPT {
return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, fmt.Sprintf("invalid policy %d", policy))
}
c.baseChainInfo.PolicyDrop = (policy == linux.NF_DROP)
return nil
}
// SetBaseChainInfo attaches the specified chain to the netfilter pipeline (and
// detaches the chain from the pipeline if it was previously attached to a
// different hook) by setting the base chain info for the chain, returning an
// error if the base chain info is invalid.
func (c *Chain) setBaseChainInfo(info *BaseChainInfo) *syserr.AnnotatedError {
// Ensures base chain info is valid if it's a base chain.
if err := validateBaseChainInfo(info, c.GetAddressFamily()); err != nil {
return err
}
hfStacks := c.table.afFilter.hfStacks
// Detaches the chain if it was previously attached to a different hook.
if c.baseChainInfo != nil && c.baseChainInfo.Hook != info.Hook {
oldHfStack := hfStacks[c.baseChainInfo.Hook]
if err := oldHfStack.detachBaseChain(c); err != nil {
return err
}
}
// Initializes hook function stack (and its slice of base chains) if
// first base chain for this hook (for the given address family).
if hfStacks[info.Hook] == nil {
hfStacks[info.Hook] = &hookFunctionStack{}
}
// Sets the base chain info and attaches to the pipeline.
c.baseChainInfo = info
hfStacks[info.Hook].attachBaseChain(c)
return nil
}
// GetChainUse returns the chain use value of the chain.
func (c *Chain) GetChainUse() uint32 {
return c.chainUse
}
// GetComment returns the comment of the chain.
func (c *Chain) GetComment() string {
return c.comment
}
// SetComment sets the comment of the chain.
func (c *Chain) SetComment(comment string) {
c.comment = comment
}
// GetRules returns the rules of the chain.
func (c *Chain) GetRules() []*Rule {
return c.rules
}
// Counter returns the chain's counter.
func (c *Chain) Counter() *ChainCounter {
return c.counter
}
// SetCounter sets or replaces the chain's counter.
func (c *Chain) SetCounter(counter *ChainCounter) {
c.counter = counter
}
// RegisterRule assigns the chain to the rule and adds the rule to the chain's
// rule list at the given index.
// Valid indices are -1 (append) and [0, len]. Errors on invalid index.
// This also checks that the operations in the rule comply with the chain.
// Checks done:
// - All jump and goto operations have a valid target chain.
// - Loop checking for jump and goto operations.
// - TODO(b/345684870): Add more checks as more operations are supported.
// TODO - b/434244017: Update rules to be in a linked list for faster insertion and deletion.
func (c *Chain) RegisterRule(rule *Rule, index int) *syserr.AnnotatedError {
// Error checks like these are not part of the nf_tables_api.c. Rather they are error
// checked here for completeness for unit tests. Netfilter sockets should never attempt to register
// the exact same rule struct twice.
if rule.chain != nil {
return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "rule chain is malformed")
}
if index < -1 || index > c.RuleCount() {
return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, fmt.Sprintf("invalid index %d for rule registration with %d rule(s)", index, c.RuleCount()))
}
// Checks if there are loops from all jump and goto operations in the rule.
for _, op := range rule.ops {
if err := op.checkCompatibility(&opCompatCtx{chain: c}); err != nil {
return err
}
isJumpOrGoto, nextChain := isJumpOrGotoOperation(op)
if !isJumpOrGoto {
continue
}
if nextChain == nil {
return syserr.NewAnnotatedError(syserr.ErrNoFileOrDir, "jump or goto operation does not have a target chain")
}
if err := nextChain.checkLoops(c, 0); err != nil {
return err
}
}
// Assigns chain to rule and adds rule to chain's rule list at given index with the given handle.
rule.chain = c
rule.handle = c.table.getNewHandle()
c.handleToRule[rule.handle] = rule
// Adds the rule to the chain's rule list at the correct index.
if index == -1 || index == c.RuleCount() {
c.rules = append(c.rules, rule)
} else {
c.rules = slices.Insert(c.rules, index, rule)
}
return nil
}
// RegisterBeforeExistingRule registers the new rule before the existing rule.
func (c *Chain) RegisterBeforeExistingRule(newRule *Rule, oldRule *Rule) *syserr.AnnotatedError {
index, err := c.GetRuleIndex(oldRule)
if err != nil {
return err
}
return c.RegisterRule(newRule, index)
}
// RegisterAfterExistingRule registers the new rule after the existing rule.
func (c *Chain) RegisterAfterExistingRule(newRule *Rule, oldRule *Rule) *syserr.AnnotatedError {
index, err := c.GetRuleIndex(oldRule)
if err != nil {
return err
}
return c.RegisterRule(newRule, index+1)
}
// UnregisterRuleByIndex removes the rule at the given index from the chain's rule list
// and un-assigns the chain from the rule then returns the unregistered rule.
// Valid indices are -1 (pop) and [0, len-1]. Errors on invalid index.
func (c *Chain) UnregisterRuleByIndex(index int) (*Rule, *syserr.AnnotatedError) {
rule, err := c.GetRule(index)
if err != nil {
return nil, err
}
if index == -1 {
index = c.RuleCount() - 1
}
c.rules = append(c.rules[:index], c.rules[index+1:]...)
rule.chain = nil
delete(c.handleToRule, rule.handle)
return rule, nil
}
// GetRule returns the rule at the given index in the chain's rule list.
// Valid indices are -1 (last) and [0, len-1]. Errors on invalid index.
func (c *Chain) GetRule(index int) (*Rule, *syserr.AnnotatedError) {
if index < -1 || index > c.RuleCount()-1 || (index == -1 && c.RuleCount() == 0) {
return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, fmt.Sprintf("invalid index %d for rule retrieval with %d rule(s)", index, c.RuleCount()))
}
if index == -1 {
return c.rules[c.RuleCount()-1], nil
}
return c.rules[index], nil
}
// GetRuleByHandle returns the rule with the specified handle from the chain's rule list.
// Errors on rule not found.
func (c *Chain) GetRuleByHandle(handle uint64) (*Rule, *syserr.AnnotatedError) {
rule, exists := c.handleToRule[handle]
if !exists {
return nil, syserr.NewAnnotatedError(syserr.ErrNoFileOrDir, fmt.Sprintf("rule with handle %d not found for chain %s", handle, c.name))
}
return rule, nil
}
// GetRuleIndex returns the index of the rule in the chain's rule list.
func (c *Chain) GetRuleIndex(r *Rule) (int, *syserr.AnnotatedError) {
for i, rule := range c.rules {
if rule == r {
return i, nil
}
}
return -1, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, fmt.Sprintf("index of rule with handle %d not found for chain %s", r.handle, c.name))
}
// RuleCount returns the number of rules in the chain.
func (c *Chain) RuleCount() int {
return len(c.rules)
}
//
// Loop Checking Helper Functions
//
// isJumpOrGotoOperation returns whether the operation is an
// immediate operation that sets the verdict register
// to a jump or goto verdict, returns pointer to the target chain if so.
func isJumpOrGotoOperation(op operation) (bool, *Chain) {
imm, ok := op.(*immediate)
if !ok {
return false, nil
}
if imm.dataType != linux.NFT_DATA_VERDICT {
return false, nil
}
verdict := imm.verdict
if verdict.Code != VC(linux.NFT_JUMP) && verdict.Code != VC(linux.NFT_GOTO) {
return false, nil
}
return true, verdict.Chain
}
// checkLoops detects if there are any loops via jumps and gotos between chains
// by tracing all immediate operations starting from the destination chain
// of a jump or goto operation and checking that no jump or goto operations lead
// back to the original source chain.
// Note: this loop checking is done whenever a rule is registered to a chain.
func (c *Chain) checkLoops(source *Chain, depth int) *syserr.AnnotatedError {
// Depth is checked here to prevent invalid rules from being registered. This implicitly
// checks if we revisit the same chain more than once in a loop.
// From linux/net/netfilter/nf_tables_api.c:nft_chain_validate
if depth >= nestedJumpLimit {
return syserr.NewAnnotatedError(syserr.ErrTooManyLinks, fmt.Sprintf("chain %s has exceeded the nested jump limit of %d", c.name, nestedJumpLimit))
}
// Jumping to the same chain is not allowed and although implicitly checked, we explcitly
// check it here for clarity.
if c == source {
return syserr.NewAnnotatedError(syserr.ErrTooManyLinks, fmt.Sprintf("chain %s cannot jump to itself", c.name))
}
for _, rule := range c.rules {
for _, op := range rule.ops {
isJumpOrGoto, nextChain := isJumpOrGotoOperation(op)
if !isJumpOrGoto {
continue
}
if nextChain == nil {
return syserr.NewAnnotatedError(syserr.ErrNoFileOrDir, "jump or goto operation does not have a target chain")
}
// Depth is incremented regardless if the verdict is a NFT_JUMP or NFT_GOTO.
// From net/netfilter/nft_immediate.c:nft_immediate_validate
depth++
if err := nextChain.checkLoops(source, depth); err != nil {
return err
}
depth--
}
}
return nil
}
//
// Rule Functions
//
// addOperation adds an operation to the rule. Adding operations is only allowed
// before the rule is registered to a chain. Returns an error if the operation
// is nil or if the rule is already registered to a chain.
func (r *Rule) addOperation(op operation) *syserr.AnnotatedError {
// From net/netfilter/nf_tables_api.c:nft_expr_type
if op == nil {
return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "operation is nil")
}
// Netfilter sockets should not try to register operations to rules that
// have already been registered to a chain. Instead, old rules should be unregistered
// and new rules should be created.
if r.chain != nil {
return syserr.NewAnnotatedError(syserr.ErrNotSupported, "cannot add operation to a rule that is already registered to a chain")
}
r.ops = append(r.ops, op)
return nil
}
// AddOpFromExprInfo adds an operation to the rule given the expression information.
func (r *Rule) AddOpFromExprInfo(nf *NFTables, tab *Table, exprInfo ExprInfo) *syserr.AnnotatedError {
// Centralized here so that operations can do their own validation when being created.
var op operation
var err *syserr.AnnotatedError
exprOpType := ToOpType(exprInfo.ExprName)
switch exprOpType {
case OpTypeImmediate:
if op, err = initImmediate(tab, exprInfo); err != nil {
return err
}
case OpTypePayload:
if op, err = initPayload(tab, exprInfo); err != nil {
return err
}
case OpTypeBitwise:
if op, err = initBitwise(tab, exprInfo); err != nil {
return err
}
case OpTypeMeta:
if op, err = initMeta(tab, exprInfo); err != nil {
return err
}
case OpTypeComparison:
if op, err = initComparison(tab, exprInfo); err != nil {
return err
}
case OpTypeCounter:
if op, err = initCounter(exprInfo); err != nil {
return err
}
case OpTypeNAT:
if op, err = initNATOp(tab, exprInfo); err != nil {
return err
}
case OpTypeLookup:
if op, err = initLookup(tab, exprInfo); err != nil {
return err
}
case OpTypeFIB:
if op, err = initFIB(tab, exprInfo); err != nil {
return err
}
case OpTypeCT:
if op, err = initCT(tab, exprInfo); err != nil {
return err
}
case OpTypeMasq:
if op, err = initMasqOp(tab, exprInfo); err != nil {
return err
}
case OpTypeMatch:
if op, err = initMatch(tab, exprInfo); err != nil {
return err
}
case OpTypeTarget:
if op, err = initTarget(tab, exprInfo); err != nil {
return err
}
default:
return syserr.NewAnnotatedError(syserr.ErrNoFileOrDir, fmt.Sprintf("unknown expression type not found: %s", exprInfo.ExprName))
}
if exprOpType == OpTypeCT || exprOpType == OpTypeNAT || exprOpType == OpTypeMasq || exprOpType == OpTypeMatch || exprOpType == OpTypeTarget {
// NAT, Masq, and xtables operations require connection tracking.
nf.InitConnTrackOnce()
}
return r.addOperation(op)
}
// GetChain returns the chain that the rule is registered to.
func (r *Rule) GetChain() *Chain {
return r.chain
}
// GetHandle returns the handle of the rule.
func (r *Rule) GetHandle() uint64 {
return r.handle
}
// SetUserData sets the user data of the rule.
func (r *Rule) SetUserData(data []byte) *syserr.AnnotatedError {
if r.udata != nil {
return syserr.NewAnnotatedError(syserr.ErrNotSupported, "rule already has user data")
}
r.udata = make([]byte, len(data))
copy(r.udata, data)
return nil
}
// HasUserData returns whether the rule has user data.
func (r *Rule) HasUserData() bool {
return r.udata != nil
}
// GetUserData returns the user data of the rule.
func (r *Rule) GetUserData() []byte {
return r.udata
}
// GetAddressFamily returns the address family of the rule.
func (r *Rule) GetAddressFamily() stack.AddressFamily {
return r.chain.GetAddressFamily()
}
// GetOperations returns the list of operations in the rule.
func (r *Rule) GetOperations() []operation {
return r.ops
}
// appendBaseChain appends a base chain to the list in ascending priority order.
// Returns false if the chain is not a base chain.
func appendBaseChain(list []*Chain, newBaseChain *Chain) ([]*Chain, bool) {
if newBaseChain.baseChainInfo == nil {
return nil, false
}
if len(list) == 0 {
return []*Chain{newBaseChain}, true
}
pos, _ := slices.BinarySearchFunc(list, newBaseChain, func(a, b *Chain) int {
return a.baseChainInfo.Priority.GetValue() - b.baseChainInfo.Priority.GetValue()
})
return slices.Insert(list, pos, newBaseChain), true
}
// removeBaseChain removes a base chain with the specified pointer from the list.
// Returns false if the chain doesn't exist.
func removeBaseChain(list []*Chain, c *Chain) ([]*Chain, bool) {
idx := slices.IndexFunc(list, func(chain *Chain) bool {
return chain == c
})
if idx == -1 {
return list, false
}
return slices.Delete(list, idx, idx+1), true
}
//
// Private hookFunctionStack functions
//
// attachBaseChain adds an (assumed/previously checked) base chain to the stack,
// maintaining ascending priority ordering.
// Note: assumes stack and base chains slice are initialized and is base chain.
func (hfStack *hookFunctionStack) attachBaseChain(chain *Chain) *syserr.AnnotatedError {
ok := false
if chain.baseChainInfo.BcType == BaseChainTypeNat {
hfStack.natBaseChains, ok = appendBaseChain(hfStack.natBaseChains, chain)
} else {
hfStack.baseChains, ok = appendBaseChain(hfStack.baseChains, chain)
}
if ok {
return nil
}
return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, fmt.Sprintf("failed to append base chain: %s", chain.name))
}
// detachBaseChain removes a base chain with the specified pointer from the stack,
// returning an error if the chain doesn't exist.
// Note: assumes stack is initialized.
func (hfStack *hookFunctionStack) detachBaseChain(c *Chain) *syserr.AnnotatedError {
ok := false
if hfStack.baseChains, ok = removeBaseChain(hfStack.baseChains, c); ok {
return nil
}
if hfStack.natBaseChains, ok = removeBaseChain(hfStack.natBaseChains, c); ok {
return nil
}
return syserr.NewAnnotatedError(syserr.ErrNoFileOrDir, fmt.Sprintf("failed to detach base chain: %s, chain not found", c.GetName()))
}
//
// NFTables Parser functions
//
// ParseExpr parses the expression attributes and returns the expression information.
func ParseExpr(attrs nlmsg.AttrsView) (*ExprInfo, *syserr.AnnotatedError) {
exprAttrs, ok := NfParse(attrs)
if !ok {
return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "failed to parse attributes for expression")
}
exprNameBytes, ok := exprAttrs[linux.NFTA_EXPR_NAME]
if !ok {
return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "NFTA_EXPR_NAME attribute is malformed or not found")
}
// exprData holds the expression data for a specific operation.
exprData := nlmsg.AttrsView{}
// Only assign exprData if the data is present. Later validation will
// check if it is needed for the specific operation type.
// From linux/net/netfilter/nf_tables_api.c: nf_tables_expr_parse
if exprDataBytes, ok := exprAttrs[linux.NFTA_EXPR_DATA]; ok {
exprData = nlmsg.AttrsView(exprDataBytes)
}
return &ExprInfo{
ExprName: exprNameBytes.String(),
ExprData: exprData,
}, nil
}
// ParseNestedExprs parses the nested expression attributes and
// returns the expression information.
func ParseNestedExprs(nestedAttrBytes nlmsg.AttrsView, maxExprs int) ([]ExprInfo, *syserr.AnnotatedError) {
// Netlink message structure for rule expressions (NFTA_RULE_EXPRESSIONS):
//
// [ NFTA_RULE_EXPRESSIONS (Outer Array Container) ]
// ├── [ NFTA_LIST_ELEM (Element Wrapper #1) ]
// │ ├── [ NFTA_EXPR_NAME ] (e.g., "counter")
// │ └── [ NFTA_EXPR_DATA ] (Expression-specific attributes)
// │
// └── [ NFTA_LIST_ELEM (Element Wrapper #2) ]
// ├── [ NFTA_EXPR_NAME ] (e.g., "cmp")
// └── [ NFTA_EXPR_DATA ] (Expression-specific attributes)
var exprInfos []ExprInfo
numExprs := 0
for !nestedAttrBytes.Empty() {
hdr, value, rest, ok := nestedAttrBytes.ParseFirst()
if !ok {
return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "failed to parse list attribute for rules")
}
nestedAttrBytes = rest
if hdr.Type&linux.NLA_TYPE_MASK != linux.NFTA_LIST_ELEM {
return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "parsed attribute is not of type NFTA_LIST_ELEM")
}
if numExprs == maxExprs {
return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "too many expressions specified for rule")
}
numExprs++
exprInfo, err := ParseExpr(value)
if err != nil {
return nil, err
}
exprInfos = append(exprInfos, *exprInfo)
}
return exprInfos, nil
}
// DecrementChainUse decrements the chain use value of the chain.
func (c *Chain) DecrementChainUse() {
if c.chainUse > 0 {
c.chainUse--
} else {
log.BugTraceback(fmt.Errorf("chain: %s has chainUse underflow", c.name))
}
}
func (r *Rule) destroy() {
for _, op := range r.ops {
op.destroy()
}
r.chain = nil
r.ops = nil
r.handle = 0
r.udata = nil
}
// deleteRuleAtIndex deletes the rule at the index from the chain.
func (c *Chain) deleteRuleAtIndex(index int) *syserr.AnnotatedError {
if index < 0 || index >= len(c.rules) {
return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, fmt.Sprintf("invalid rule index %d for chain %s", index, c.name))
}
rule := c.rules[index]
rule.destroy()
_, err := c.UnregisterRuleByIndex(index)
return err
}
// DeleteRule removes the rule from the chain.
// Ref: net/netfilter/nf_tables_api.c:nft_delrule
func (c *Chain) DeleteRule(rule *Rule) *syserr.AnnotatedError {
index, err := c.GetRuleIndex(rule)
if err != nil {
return err
}
return c.deleteRuleAtIndex(index)
}
// DeleteAllRules removes all rules from the chain.
// Ref: net/netfilter/nf_tables_api.c:nft_delrule_by_chain
func (c *Chain) DeleteAllRules() {
for _, rule := range c.rules {
rule.destroy()
}
c.rules = nil
c.handleToRule = make(map[uint64]*Rule)
}
func (c *Chain) destroy() {
if c.GetChainUse() != 0 {
log.BugTraceback(fmt.Errorf("chain: %s has chainUse %d != 0", c.name, c.GetChainUse()))
}
c.DeleteAllRules()
c.name = ""
c.flags = 0
c.table = nil
c.baseChainInfo = nil
c.handleToRule = nil
c.rules = nil
c.userData = nil
c.comment = ""
c.handle = 0
}
// DeleteRule handles netlink request: NFT_MSG_DELRULE / NFT_MSG_DESTROYRULE.
// Ref: net/netfilter/nf_tables_api.c:nf_tables_delrule
func (nf *NFTables) DeleteRule(attrs map[uint16]nlmsg.BytesView, family stack.AddressFamily, msgType linux.NfTableMsgType, ms *nlmsg.MessageSet) *syserr.AnnotatedError {
// Get the table from the attributes.
tabNameBytes, ok := attrs[linux.NFTA_RULE_TABLE]
if !ok {
return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "NFTA_RULE_TABLE attribute is malformed or not found")
}
tab, err := nf.GetTable(family, tabNameBytes.String(), uint32(ms.PortID))
if err != nil {
if err.GetError() == syserr.ErrNoFileOrDir && msgType == linux.NFT_MSG_DESTROYRULE {
return nil
}
return err
}
// Find if a chain is specified.
var chain *Chain
if chainNameBytes, ok := attrs[linux.NFTA_RULE_CHAIN]; ok {
chain, err = tab.GetChain(chainNameBytes.String())
if err != nil {
if err.GetError() == syserr.ErrNoFileOrDir && msgType == linux.NFT_MSG_DESTROYRULE {
return nil
}
return err
}
}
if chain == nil {
// Delete all rules in all chains of the table.
for _, c := range tab.GetChains() {
if c.GetFlags()&linux.NFT_CHAIN_BINDING != 0 {
continue
}
c.DeleteAllRules()
}
return nil
}
// Delete rule in the specified chain.
{
if chain.GetFlags()&linux.NFT_CHAIN_BINDING != 0 {
return syserr.NewAnnotatedError(syserr.ErrNotSupported, "operation not supported on binding chains")
}
// If a rule is specified, delete it.
if handleBytes, ok := attrs[linux.NFTA_RULE_HANDLE]; ok {
ruleHandle, ok := handleBytes.Uint64()
if !ok {
return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "rule handle attribute is malformed or not found")
}
rule, err := chain.GetRuleByHandle(nlmsg.NetToHostU64(ruleHandle))
if err != nil {
if err.GetError() == syserr.ErrNoFileOrDir && msgType == linux.NFT_MSG_DESTROYRULE {
return nil
}
return err
}
return chain.DeleteRule(rule)
}
// Delete all rules in chain to clean up operations and object references.
chain.DeleteAllRules()
}
return nil
}
// chainCountersPolicy is the NLA policy for chain counter attributes.
// Ref: net/netfilter/nf_tables_api.c:nft_counter_policy
var chainCountersPolicy = []NlaPolicy{
linux.NFTA_COUNTER_PACKETS: {nlaType: linux.NLA_U64},
linux.NFTA_COUNTER_BYTES: {nlaType: linux.NLA_U64},
}
// ParseChainCounter parses a nested NFTA_CHAIN_COUNTERS attribute.
func ParseChainCounter(data nlmsg.BytesView) (*ChainCounter, *syserr.AnnotatedError) {
attrs, err := NfParseWithOpts(nlmsg.AttrsView(data), &NfParseOpts{
Policy: chainCountersPolicy,
})
if err != nil {
return nil, err
}
pkts, okP := AttrNetToHost[uint64](linux.NFTA_COUNTER_PACKETS, attrs)
bytes, okB := AttrNetToHost[uint64](linux.NFTA_COUNTER_BYTES, attrs)
if !okP || !okB {
return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "NFTA_COUNTER_PACKETS and NFTA_COUNTER_BYTES are required in NFTA_CHAIN_COUNTERS")
}
return newChainCounter(bytes, pkts), nil
}