From 814b1bba55f8f2297e7a95525c547afcb6d1abf4 Mon Sep 17 00:00:00 2001 From: Parth Sarthi Date: Tue, 5 May 2026 00:22:58 -0700 Subject: [PATCH] Nftables: Added FIB op support. PiperOrigin-RevId: 910485751 --- pkg/abi/linux/nf_tables.go | 35 ++ pkg/tcpip/network/ipv4/ipv4.go | 4 +- pkg/tcpip/nftables/BUILD | 1 + pkg/tcpip/nftables/nft_fib.go | 475 ++++++++++++++++++ pkg/tcpip/nftables/nft_lookup.go | 2 +- pkg/tcpip/nftables/nftables.go | 8 +- pkg/tcpip/nftables/nftables_test.go | 6 +- pkg/tcpip/nftables/nftables_types.go | 6 + pkg/tcpip/stack/packet_buffer.go | 5 + runsc/boot/loader.go | 2 +- test/nftables/nftables_validation.go | 207 ++++++++ .../linux/socket_netlink_netfilter.cc | 86 ++++ 12 files changed, 829 insertions(+), 8 deletions(-) create mode 100644 pkg/tcpip/nftables/nft_fib.go diff --git a/pkg/abi/linux/nf_tables.go b/pkg/abi/linux/nf_tables.go index 726f1f822c..d9b80e1c37 100644 --- a/pkg/abi/linux/nf_tables.go +++ b/pkg/abi/linux/nf_tables.go @@ -674,3 +674,38 @@ const ( NFTA_LOOKUP_MAX = __NFTA_LOOKUP_MAX - 1 ) const NFT_LOOKUP_F_INV = uint32(1 << 0) + +// NfTable fib expression netlink attributes. +// These correspond to enum values in include/uapi/linux/netfilter/nf_tables.h. +const ( + NFTA_FIB_UNSPEC uint16 = iota + NFTA_FIB_DREG + NFTA_FIB_RESULT + NFTA_FIB_FLAGS + __NFTA_FIB_MAX +) + +const NFTA_FIB_MAX = __NFTA_FIB_MAX - 1 + +// NfTable fib result types. +// These correspond to enum values in include/uapi/linux/netfilter/nf_tables.h. +const ( + NFT_FIB_RESULT_UNSPEC = iota + NFT_FIB_RESULT_OIF + NFT_FIB_RESULT_OIFNAME + NFT_FIB_RESULT_ADDRTYPE + __NFT_FIB_RESULT_MAX +) + +const NFT_FIB_RESULT_MAX = __NFT_FIB_RESULT_MAX - 1 + +// NfTable fib flags. +// These correspond to enum values in include/uapi/linux/netfilter/nf_tables.h. +const ( + NFTA_FIB_F_SADDR = 1 << 0 + NFTA_FIB_F_DADDR = 1 << 1 + NFTA_FIB_F_MARK = 1 << 2 + NFTA_FIB_F_IIF = 1 << 3 + NFTA_FIB_F_OIF = 1 << 4 + NFTA_FIB_F_PRESENT = 1 << 5 +) diff --git a/pkg/tcpip/network/ipv4/ipv4.go b/pkg/tcpip/network/ipv4/ipv4.go index f357eb48d9..56dcf3a4e5 100644 --- a/pkg/tcpip/network/ipv4/ipv4.go +++ b/pkg/tcpip/network/ipv4/ipv4.go @@ -970,8 +970,10 @@ func (e *endpoint) HandlePacket(pkt *stack.PacketBuffer) { } } + nicID := e.nic.ID() // Loopback traffic skips the prerouting chain. - inNicName := stk.FindNICNameFromID(e.nic.ID()) + inNicName := stk.FindNICNameFromID(nicID) + pkt.InputNICID = nicID if ok := stk.IPTables().CheckPrerouting(pkt, e, inNicName); !ok { // iptables is telling us to drop the packet. stats.IPTablesPreroutingDropped.Increment() diff --git a/pkg/tcpip/nftables/BUILD b/pkg/tcpip/nftables/BUILD index 50557561ae..abee79bb91 100644 --- a/pkg/tcpip/nftables/BUILD +++ b/pkg/tcpip/nftables/BUILD @@ -20,6 +20,7 @@ go_library( "nft_byteorder.go", "nft_comparison.go", "nft_counter.go", + "nft_fib.go", "nft_immediate.go", "nft_last.go", "nft_lookup.go", diff --git a/pkg/tcpip/nftables/nft_fib.go b/pkg/tcpip/nftables/nft_fib.go new file mode 100644 index 0000000000..33a8e1d93e --- /dev/null +++ b/pkg/tcpip/nftables/nft_fib.go @@ -0,0 +1,475 @@ +// Copyright 2026 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 ( + "encoding/binary" + "fmt" + + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/sentry/socket/netlink/nlmsg" + "gvisor.dev/gvisor/pkg/syserr" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +// fib is an operation that queries the routing table. +type fib struct { + result int + flags uint32 + dregIdx int +} + +// ipv4AddrType determines the address type of the given IPv4 address. +// nicID represents the constraint interface ID. +// rt is the route to 'addr' (if one was found). +func (op *fib) ipv4AddrType(st *stack.Stack, addr tcpip.Address, nicID tcpip.NICID, rt *stack.Route) uint32 { + switch { + case addr == header.IPv4Any || addr == header.IPv4Broadcast: + return uint32(linux.RTN_BROADCAST) + case header.IsV4MulticastAddress(addr): + return uint32(linux.RTN_MULTICAST) + case st.IsSubnetBroadcast(nicID, header.IPv4ProtocolNumber, addr): + return uint32(linux.RTN_BROADCAST) + case st.CheckLocalAddress(0 /*nicID*/, header.IPv4ProtocolNumber, addr) != 0: + // Pass 0 as NICID otherwise the CheckLocalAddress will always return RTN_LOCAL. + return uint32(linux.RTN_LOCAL) + case rt != nil: + return uint32(linux.RTN_UNICAST) + default: + return uint32(linux.RTN_UNREACHABLE) + } +} + +// ipv6AddrType determines the address type of the given IPv6 address. +// nicID represents the constraint interface ID. +// rt is the route to 'addr' (if one was found). +func (op *fib) ipv6AddrType(st *stack.Stack, addr tcpip.Address, nicID tcpip.NICID, rt *stack.Route) uint32 { + switch { + case header.IsV6MulticastAddress(addr): + return uint32(linux.RTN_MULTICAST) + case addr == header.IPv6Any: + // Matches Linux [net/ipv6/netfilter/nft_fib_ipv6.c]:[__nft_fib6_eval_type]() + return uint32(linux.RTN_UNSPEC) + case st.CheckLocalAddress(0 /*nicID*/, header.IPv6ProtocolNumber, addr) != 0: + // Pass 0 as NICID otherwise the CheckLocalAddress will always return RTN_LOCAL. + return uint32(linux.RTN_LOCAL) + case rt != nil: + return uint32(linux.RTN_UNICAST) + default: + return uint32(linux.RTN_UNREACHABLE) + } +} + +func (op *fib) getAddrType(netProto tcpip.NetworkProtocolNumber, st *stack.Stack, addr tcpip.Address, nicID tcpip.NICID, rt *stack.Route) uint32 { + switch netProto { + case header.IPv4ProtocolNumber: + return op.ipv4AddrType(st, addr, nicID, rt) + case header.IPv6ProtocolNumber: + return op.ipv6AddrType(st, addr, nicID, rt) + default: + return uint32(linux.RTN_UNSPEC) + } +} + +// Ref: net/netfilter/nft_fib.c +func (op *fib) storeResult(regs *registerSet, nicID tcpip.NICID, st *stack.Stack) { + // Just set the boolean result if the flag is set. + if op.flags&linux.NFTA_FIB_F_PRESENT != 0 { + if nicID != 0 { + regs.data[op.dregIdx] = uint8(1) + } else { + regs.data[op.dregIdx] = uint8(0) + } + return + } + + switch op.result { + case linux.NFT_FIB_RESULT_OIF: + binary.NativeEndian.PutUint32(regs.data[op.dregIdx:], uint32(nicID)) + case linux.NFT_FIB_RESULT_OIFNAME: + name := st.FindNICNameFromID(nicID) + startIdx, endIdx := op.dregIdx, op.dregIdx+linux.IFNAMSIZ + clear(regs.data[startIdx:endIdx]) + copy(regs.data[startIdx:endIdx], name) + } +} + +func (op *fib) validatePktHeader(pkt *stack.PacketBuffer) bool { + switch pkt.NetworkProtocolNumber { + case header.IPv4ProtocolNumber: + hdr := pkt.NetworkHeader().Slice() + if len(hdr) < header.IPv4MinimumSize { + return false + } + case header.IPv6ProtocolNumber: + hdr := pkt.NetworkHeader().Slice() + if len(hdr) < header.IPv6MinimumSize { + return false + } + } + return true +} + +func (op *fib) getSrcDstAddr(pkt *stack.PacketBuffer) (tcpip.Address, tcpip.Address, bool) { + switch pkt.NetworkProtocolNumber { + case header.IPv4ProtocolNumber: + hdr := pkt.NetworkHeader().Slice() + iph := header.IPv4(hdr) + return iph.SourceAddress(), iph.DestinationAddress(), true + case header.IPv6ProtocolNumber: + hdr := pkt.NetworkHeader().Slice() + iph := header.IPv6(hdr) + return iph.SourceAddress(), iph.DestinationAddress(), true + } + return tcpip.Address{}, tcpip.Address{}, false +} + +// getOrFindRoute returns a route from the stack. If the route is found in the +// evalCtx, it is returned directly. Otherwise, the route is found using +// FindRoute. +func (op *fib) getOrFindRoute(evalCtx opEvalCtx, srcAddr, dstAddr tcpip.Address, netProto tcpip.NetworkProtocolNumber, nicID tcpip.NICID, dAddr bool) (rt *stack.Route, release func(), err tcpip.Error) { + if dAddr && evalCtx.route != nil { + return evalCtx.route, func() {}, nil + } + rt, err = evalCtx.nftState.stack.FindRoute(nicID, srcAddr, dstAddr, netProto, false) + if err != nil { + return nil, func() {}, err + } + return rt, rt.Release, nil +} + +// Ref: net/ipv[4|6]/netfilter/nft_fib_ipv[4|6].c]:nft_fib[4|6]_eval() +func (op *fib) evaluateOIF(regs *registerSet, evalCtx opEvalCtx) { + hook := evalCtx.hook + pkt := evalCtx.pkt + st := evalCtx.nftState.stack + if hook == stack.NFPrerouting || hook == stack.NFInput || hook == stack.NFIngress { + nic, err := st.GetNICByID(pkt.InputNICID) + if err == nil && nic.IsLoopback() { + op.storeResult(regs, pkt.InputNICID, st) + return + } + } + + // When: + // 1. nicID == 0, means no OIF/IIF constraint was specified, and FIB will + // just deduce and store the interface from the route lookup. + // 2. nicID != 0, FIB will verify if the calculated route's interface matches + // this constraint, and only store the result if they match. + nicID := tcpip.NICID(0) + if op.flags&linux.NFTA_FIB_F_OIF != 0 { + if evalCtx.route != nil { + nicID = evalCtx.route.OutgoingNIC() + } + } else if op.flags&linux.NFTA_FIB_F_IIF != 0 { + nicID = pkt.InputNICID + } + + if !op.validatePktHeader(evalCtx.pkt) { + regs.verdict = stack.NFVerdict{Code: VC(linux.NFT_BREAK)} + return + } + srcAddr, dstAddr, ok := op.getSrcDstAddr(pkt) + if !ok { + regs.verdict = stack.NFVerdict{Code: VC(linux.NFT_BREAK)} + return + } + + dAddr := op.flags&linux.NFTA_FIB_F_DADDR != 0 + if !dAddr { + srcAddr, dstAddr = dstAddr, srcAddr + } else { + if evalCtx.hook == stack.NFForward && op.flags&linux.NFTA_FIB_F_IIF != 0 { + log.Warningf("fib FORWARD hook with IIF flag is not fully supported.") + // TODO: b/530282592 - In Linux, if hook is FORWARD and IIF is set, + // the lookup's input interface is set to the output interface. + // However, gVisor's FindRoute does not consider the input interface + // for finding routes. + } + } + addr := dstAddr + + netProto := pkt.NetworkProtocolNumber + switch netProto { + case header.IPv4ProtocolNumber: + hdr := pkt.NetworkHeader().Slice() + iph := header.IPv4(hdr) + if op.isV4Zeronet(iph.SourceAddress()) { + if iph.DestinationAddress() == header.IPv4Broadcast || header.IsV4LinkLocalMulticastAddress(iph.DestinationAddress()) { + op.storeResult(regs, pkt.NICID, st) + return + } + } + if srcAddr == header.IPv4Broadcast || header.IsV4MulticastAddress(srcAddr) || op.isV4Zeronet(srcAddr) { + srcAddr = tcpip.Address{} + } + case header.IPv6ProtocolNumber: + hdr := pkt.NetworkHeader().Slice() + iph := header.IPv6(hdr) + if op.ipv6SkipICMP(pkt, iph.SourceAddress(), iph.DestinationAddress()) { + op.storeResult(regs, pkt.InputNICID, st) + return + } + } + + // Set the default result to 0 now and only set the regs based on the + // the result of route lookup. + binary.NativeEndian.PutUint32(regs.data[op.dregIdx:], uint32(0)) + + rt, release, err := op.getOrFindRoute(evalCtx, srcAddr, dstAddr, netProto, nicID, dAddr) + defer release() + if err != nil { + return + } + + addrType := op.getAddrType(netProto, st, addr, nicID, rt) + if addrType == linux.RTN_LOCAL { + return + } + + // Just need to set the result to NICID if it is not 0. + if nicID == 0 { + op.storeResult(regs, rt.NICID(), st) + return + } + if rt.OutgoingNIC() == nicID { + op.storeResult(regs, nicID, st) + return + } +} + +// Ref: net/ipv6/netfilter/nft_fib_ipv6.c:nft_fib_v6_skip_icmpv6]() +func (op *fib) ipv6SkipICMP(pkt *stack.PacketBuffer, saddr, daddr tcpip.Address) bool { + if pkt.TransportProtocolNumber != header.ICMPv6ProtocolNumber { + return false + } + if saddr != header.IPv6Any { + return false + } + return header.IsV6LinkLocalUnicastAddress(daddr) || header.IsV6LinkLocalMulticastAddress(daddr) +} + +// Ref: include/linux/in.h:ipv4_is_zeronet() +func (op *fib) isV4Zeronet(addr tcpip.Address) bool { + slice := addr.AsSlice() + return slice[0] == 0 && slice[1] == 0 && slice[2] == 0 && slice[3] == 0 +} + +// Ref: net/ipv4/netfilter/nft_fib_ipv[4|6].c:nft_fib[4|6]_eval_type() +func (op *fib) evaluateAddrType(regs *registerSet, evalCtx opEvalCtx) { + pkt := evalCtx.pkt + if !op.validatePktHeader(pkt) { + regs.verdict = stack.NFVerdict{Code: VC(linux.NFT_BREAK)} + return + } + + srcAddr, dstAddr, ok := op.getSrcDstAddr(pkt) + if !ok { + regs.verdict = stack.NFVerdict{Code: VC(linux.NFT_BREAK)} + return + } + + dAddr := op.flags&linux.NFTA_FIB_F_DADDR != 0 + if !dAddr { + srcAddr, dstAddr = dstAddr, srcAddr + } + addr := dstAddr + + st := evalCtx.nftState.stack + netProto := pkt.NetworkProtocolNumber + + nicID := tcpip.NICID(0) + if op.flags&linux.NFTA_FIB_F_IIF != 0 { + nicID = pkt.InputNICID + } else if op.flags&linux.NFTA_FIB_F_OIF != 0 { + if evalCtx.route != nil { + nicID = evalCtx.route.OutgoingNIC() + } + } + + // Find route to determine address type. + rt, release, _ := op.getOrFindRoute(evalCtx, srcAddr, dstAddr, netProto, nicID, dAddr) + defer release() + + addrType := op.getAddrType(netProto, st, addr, nicID, rt) + binary.NativeEndian.PutUint32(regs.data[op.dregIdx:], addrType) +} + +// evaluate implements operation.evaluate. +// Ref: net/netfilter/nft_fib.c:nft_fib_eval() +func (op *fib) evaluate(regs *registerSet, evalCtx opEvalCtx) { + switch op.result { + case linux.NFT_FIB_RESULT_ADDRTYPE: + op.evaluateAddrType(regs, evalCtx) + default: + op.evaluateOIF(regs, evalCtx) + } +} + +// GetExprName implements operation's ExprName interface. +func (op *fib) GetExprName() string { + return OpTypeFIB.String() +} + +// Dump implements operation's Dump interface. +func (op *fib) Dump() ([]byte, *syserr.AnnotatedError) { + m := &nlmsg.Message{} + m.PutAttr(linux.NFTA_FIB_DREG, nlmsg.PutU32(formatRegIdxForDump(op.dregIdx))) + m.PutAttr(linux.NFTA_FIB_RESULT, nlmsg.PutU32(uint32(op.result))) + m.PutAttr(linux.NFTA_FIB_FLAGS, nlmsg.PutU32(op.flags)) + return m.Buffer(), nil +} + +// deepCopy implements operation's deepCopy interface. +func (op *fib) deepCopy() operation { + opCopy := &fib{} + opCopy.result = op.result + opCopy.flags = op.flags + opCopy.dregIdx = op.dregIdx + return opCopy +} + +// checkCompatibility implements operation.checkCompatibility. +// Ref: net/netfilter/nft_fib.c:nft_fib_validate() +func (op *fib) checkCompatibility(cCtx *opCompatCtx) *syserr.AnnotatedError { + c := cCtx.chain + if c == nil { + return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "cannot validate fib operation on a rule without a chain") + } + if c.baseChainInfo == nil { + // Accept case. + return nil + } + hook := c.baseChainInfo.Hook + switch op.result { + case linux.NFT_FIB_RESULT_OIF, linux.NFT_FIB_RESULT_OIFNAME: + switch hook { + case stack.NFPrerouting, stack.NFInput, stack.NFForward: + return nil + } + return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "fib result OIF/OIFNAME only valid in PREROUTING, INPUT, FORWARD") + case linux.NFT_FIB_RESULT_ADDRTYPE: + if op.flags&linux.NFTA_FIB_F_IIF != 0 { + switch hook { + case stack.NFPrerouting, stack.NFInput, stack.NFForward: + return nil + } + return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "fib result ADDRTYPE with IIF only valid in PREROUTING, INPUT, FORWARD") + } + switch hook { + case stack.NFInput, stack.NFOutput, stack.NFForward, stack.NFPrerouting, stack.NFPostrouting: + return nil + } + return syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "fib result ADDRTYPE without IIF/OIF only valid in INPUT, OUTPUT, FORWARD, PREROUTING, POSTROUTING") + } + return nil +} + +// newFIB creates a new fib operation. +// Ref: net/netfilter/nft_fib.c:nft_fib_init() +func newFIB(result int, flags uint32, dreg uint8) (*fib, *syserr.AnnotatedError) { + if isVerdictRegister(dreg) { + return nil, syserr.NewAnnotatedError( + syserr.ErrInvalidArgument, "fib operation does not support verdict register as destination register", + ) + } + + if flags == 0 { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "fib flags cannot be zero") + } + + if flags&linux.NFTA_FIB_F_MARK != 0 { + log.Warningf("fib mark flag is not supported for routing lookup in gVisor") + } + + if (flags & (linux.NFTA_FIB_F_SADDR | linux.NFTA_FIB_F_DADDR)) == (linux.NFTA_FIB_F_SADDR | linux.NFTA_FIB_F_DADDR) { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "fib flags cannot have both SADDR and DADDR") + } + if (flags & (linux.NFTA_FIB_F_IIF | linux.NFTA_FIB_F_OIF)) == (linux.NFTA_FIB_F_IIF | linux.NFTA_FIB_F_OIF) { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "fib flags cannot have both IIF and OIF") + } + if (flags & (linux.NFTA_FIB_F_SADDR | linux.NFTA_FIB_F_DADDR)) == 0 { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "fib flags must have at least SADDR or DADDR") + } + + resLen := 0 + switch result { + case linux.NFT_FIB_RESULT_OIF: + if flags&linux.NFTA_FIB_F_OIF != 0 { + return nil, syserr.NewAnnotatedError( + syserr.ErrInvalidArgument, + "fib result OIF/OIFNAME cannot be used with OIF flag", + ) + } + resLen = 4 // size of int + case linux.NFT_FIB_RESULT_OIFNAME: + if flags&linux.NFTA_FIB_F_OIF != 0 { + return nil, syserr.NewAnnotatedError( + syserr.ErrInvalidArgument, + "fib result OIF/OIFNAME cannot be used with OIF flag", + ) + } + resLen = linux.IFNAMSIZ + case linux.NFT_FIB_RESULT_ADDRTYPE: + resLen = 4 // size of uint32 + default: + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, fmt.Sprintf("unknown fib result: %d", result)) + } + + dataLen := resLen + dregIdx, err := regNumToIdx(dreg, dataLen) + if err != nil { + return nil, err + } + + return &fib{result: result, flags: flags, dregIdx: dregIdx}, nil +} + +const nftFibFlagsAll = linux.NFTA_FIB_F_SADDR | linux.NFTA_FIB_F_DADDR | + linux.NFTA_FIB_F_MARK | linux.NFTA_FIB_F_IIF | linux.NFTA_FIB_F_OIF | + linux.NFTA_FIB_F_PRESENT + +// Ref: net/netfilter/nft_fib.c:nft_fib_policy +var nftFibPolicy = []NlaPolicy{ + {nlaType: linux.NLA_U32, validator: AttrMaskValidator[uint32](nftFibFlagsAll)}, + {nlaType: linux.NLA_U32}, + {nlaType: linux.NLA_U32}, +} + +// Ref: net/netfilter/nft_fib.c:nft_fib_init() +func initFIB(tab *Table, exprInfo ExprInfo) (operation, *syserr.AnnotatedError) { + attrs, ok := NfParseWithOpts(exprInfo.ExprData, &NfParseOpts{ + Policy: nftFibPolicy, + }) + if !ok { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "failed to parse fib expression data") + } + dreg, ok := AttrNetToHost[uint32](linux.NFTA_FIB_DREG, attrs) + if !ok { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "failed to parse NFTA_FIB_DREG") + } + result, ok := AttrNetToHost[uint32](linux.NFTA_FIB_RESULT, attrs) + if !ok { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "failed to parse NFTA_FIB_RESULT") + } + flags, ok := AttrNetToHost[uint32](linux.NFTA_FIB_FLAGS, attrs) + if !ok { + return nil, syserr.NewAnnotatedError(syserr.ErrInvalidArgument, "failed to parse NFTA_FIB_FLAGS") + } + return newFIB(int(result), flags, uint8(dreg)) +} diff --git a/pkg/tcpip/nftables/nft_lookup.go b/pkg/tcpip/nftables/nft_lookup.go index d14ae96529..8f67bb2e79 100644 --- a/pkg/tcpip/nftables/nft_lookup.go +++ b/pkg/tcpip/nftables/nft_lookup.go @@ -83,7 +83,7 @@ func (op *lookupOp) evaluate(regs *registerSet, evalCtx opEvalCtx) { } func (op *lookupOp) GetExprName() string { - return "lookup" + return OpTypeLookup.String() } // Dump implements operation.Dump. diff --git a/pkg/tcpip/nftables/nftables.go b/pkg/tcpip/nftables/nftables.go index 7eba8910fc..47a4544c83 100644 --- a/pkg/tcpip/nftables/nftables.go +++ b/pkg/tcpip/nftables/nftables.go @@ -417,14 +417,14 @@ func (r *Rule) evaluate(regs *registerSet, evalCtx opEvalCtx) *syserr.AnnotatedE // 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(clock tcpip.Clock, rng rand.RNG) *NFTables { +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{clock: clock, startTime: clock.Now(), rng: rng, tableHandleCounter: atomicbitops.Uint64{}, genid: 1} + 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. @@ -1401,6 +1401,10 @@ func (r *Rule) AddOpFromExprInfo(tab *Table, exprInfo ExprInfo) *syserr.Annotate if op, err = initLookup(tab, exprInfo); err != nil { return err } + case OpTypeFIB: + if op, err = initFIB(tab, exprInfo); err != nil { + return err + } default: return syserr.NewAnnotatedError(syserr.ErrNoFileOrDir, fmt.Sprintf("Nftables: Unknown expression type not found: %s", exprInfo.ExprName)) diff --git a/pkg/tcpip/nftables/nftables_test.go b/pkg/tcpip/nftables/nftables_test.go index bfc9c5483a..1bf4324d29 100644 --- a/pkg/tcpip/nftables/nftables_test.go +++ b/pkg/tcpip/nftables/nftables_test.go @@ -2407,7 +2407,7 @@ func TestEvaluateLast(t *testing.T) { // Sets up an NFTables object with a base chain and fake manual clock. fakeClock := faketime.NewManualClock() fixedRNG := rand.RNGFrom(&fixedReader{}) - nf := NewNFTables(fakeClock, fixedRNG) + nf := NewNFTables(nil /* stack */, fakeClock, fixedRNG) tab, err := nf.AddTable(arbitraryFamily, "test", false) if err != nil { t.Fatalf("unexpected error for AddTable: %v", err) @@ -2954,7 +2954,7 @@ func TestEvaluateMetaLoad(t *testing.T) { t.Run(test.tname, func(t *testing.T) { // Sets up an NFTables object with a base chain and fake manual clock. // Using Manual Clock sets time.Now to Unix Epoch which fixes rng seed! - nf := NewNFTables(fakeClock, rand.RNGFrom(&fixedReader{})) + nf := NewNFTables(nil /* stack */, fakeClock, rand.RNGFrom(&fixedReader{})) tab, err := nf.AddTable(arbitraryFamily, "test", false) if err != nil { @@ -3753,7 +3753,7 @@ func packetResultString(initial, final *stack.PacketBuffer) string { func newNFTablesStd() *NFTables { stdClock := tcpip.NewStdClock() fixedRNG := rand.RNGFrom(&fixedReader{}) - return NewNFTables(stdClock, fixedRNG) + return NewNFTables(nil /* stack */, stdClock, fixedRNG) } // mustCreateImmediate wraps the newImmediate function for brevity. diff --git a/pkg/tcpip/nftables/nftables_types.go b/pkg/tcpip/nftables/nftables_types.go index 5d85f40e72..8ef2daac86 100644 --- a/pkg/tcpip/nftables/nftables_types.go +++ b/pkg/tcpip/nftables/nftables_types.go @@ -261,6 +261,7 @@ type NFTables struct { connTrack *stack.ConnTrack // Conntrack object for tracking connections. connTrackReaper tcpip.Timer // Reaper timer for reaping timed out connections. natEnabled bool // Whether the nat module is enabled. + stack *stack.Stack // Parent stack object. } // Ensures NFTables implements the NFTablesInterface. @@ -754,6 +755,7 @@ var ( _ operation = (*metaSet)(nil) _ operation = (*natOp)(nil) _ operation = (*lookupOp)(nil) + _ operation = (*fib)(nil) ) // OpType represents the type of operation. @@ -784,6 +786,8 @@ const ( OpTypeNAT // OpTypeLookup is the lookup operation type. OpTypeLookup + // OpTypeFIB is the FIB operation type. + OpTypeFIB // OpTypeUnknown is the unknown operation type. OpTypeUnknown ) @@ -801,6 +805,7 @@ var opTypeStrings = []string{ OpTypeMeta: "meta", OpTypeNAT: "nat", OpTypeLookup: "lookup", + OpTypeFIB: "fib", OpTypeUnknown: "unknown", } @@ -1382,6 +1387,7 @@ func (nf *NFTables) DeepCopy() *NFTables { connTrack: nf.connTrack, connTrackReaper: nf.connTrackReaper, natEnabled: nf.natEnabled, + stack: nf.stack, } nftCopy.tableHandleCounter.Store(nf.tableHandleCounter.Load()) diff --git a/pkg/tcpip/stack/packet_buffer.go b/pkg/tcpip/stack/packet_buffer.go index 5288802ee0..de8cc3ebd2 100644 --- a/pkg/tcpip/stack/packet_buffer.go +++ b/pkg/tcpip/stack/packet_buffer.go @@ -158,6 +158,10 @@ type PacketBuffer struct { // NICID is the ID of the last interface the network packet was handled at. NICID tcpip.NICID + // InputNICID is the ID of the interface that the network packet + // was received on. + InputNICID tcpip.NICID + // RXChecksumValidated indicates that checksum verification may be // safely skipped. RXChecksumValidated bool @@ -397,6 +401,7 @@ func (pk *PacketBuffer) Clone() *PacketBuffer { newPk.TransportProtocolNumber = pk.TransportProtocolNumber newPk.PktType = pk.PktType newPk.NICID = pk.NICID + newPk.InputNICID = pk.InputNICID newPk.RXChecksumValidated = pk.RXChecksumValidated newPk.NetworkPacketInfo = pk.NetworkPacketInfo newPk.tuple = pk.tuple diff --git a/runsc/boot/loader.go b/runsc/boot/loader.go index af40ba805f..d8f0687992 100644 --- a/runsc/boot/loader.go +++ b/runsc/boot/loader.go @@ -1746,7 +1746,7 @@ func (c *sandboxNetstackCreator) newEmptySandboxNetworkStack() (*netstack.Stack, }), c.uid.UniqueID()) if nftables.IsNFTablesEnabled() { - s.Stack.SetNFTables(nftables.NewNFTables(c.clock, s.Stack.SecureRNG())) + s.Stack.SetNFTables(nftables.NewNFTables(s.Stack, c.clock, s.Stack.SecureRNG())) } // Enable SACK Recovery. diff --git a/test/nftables/nftables_validation.go b/test/nftables/nftables_validation.go index 040ed618cb..2df1fff0bc 100644 --- a/test/nftables/nftables_validation.go +++ b/test/nftables/nftables_validation.go @@ -32,6 +32,7 @@ var validationTests = []TestCase{ &tcpDNAT{}, &tcpSNAT{}, &mapTest{}, + &fibTest{}, } func init() { @@ -699,3 +700,209 @@ func (t *mapTest) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error { func (*mapTest) Timeout() time.Duration { return 30 * time.Second } + +// fibTest tests installs Nftables rules such that: +// 1. Incoming packets to port 9005 match a FIB lookup. +// 2. If matched, the packet is accepted. Otherwise dropped. +type fibTest struct{ containerCase } + +var _ TestCase = (*fibTest)(nil) + +func (*fibTest) Name() string { + return "fibTest" +} + +// ContainerAction implements TestCase.ContainerAction. +func (t *fibTest) ContainerAction(ctx context.Context, ip net.IP, ipv6 bool) error { + if ipv6 { + log.Warningf("fibTest is not supported for IPv6 yet.") + return nil + } + + // Find the interface dynamically. + targetIface, ok := netutils.GetNonLoopbackInterface() + if !ok { + return fmt.Errorf("no non-loopback interface found") + } + log.Infof("FIB test using interface: %s (Index: %d)", targetIface.Name, targetIface.Index) + + // install_rules + { + cmds := [][]string{ + {"add", "table", "inet", "filter"}, + // Policy set to drop by default; if FIB fails, packet should be dropped. + {"add", "chain", "inet", "filter", "input", "{ type filter hook input priority 0; policy drop; }"}, + // Accept connections to 8995 to bypass drop policy for the test setup sync + {"add", "rule", "inet", "filter", "input", "tcp", "dport", "8995", "accept"}, + // Try to trigger all the FIB paths. + {"add", "rule", "inet", "filter", "input", "udp", "dport", "9005", + "fib", "saddr", ".", "iif", "oif", fmt.Sprintf("%d", targetIface.Index), + "fib", "saddr", ".", "iif", "oifname", targetIface.Name, + "fib", "saddr", ".", "iif", "oif", "exists", + "fib", "saddr", ".", "iif", "oifname", "exists", + "fib", "saddr", "type", "unicast", + "fib", "daddr", "type", "local", + "accept"}, + } + + for _, cmd := range cmds { + if err := nftCmd(cmd); err != nil { + return fmt.Errorf("nft cmd: %v, failed with error: %v", cmd, err) + } + } + log.Infof("fibTest: NFT rules installed successfully") + } + + // Verify that the UDP packets from outside the container can reach port 9005. + // This verifies that the FIB rules are evaluated correctly. + udpListener, err := net.ListenPacket("udp", "0.0.0.0:9005") + if err != nil { + return fmt.Errorf("UDP net.ListenPacket failed on 9005: %v", err) + } + defer udpListener.Close() + log.Infof("fibTest: Listening for UDP on port 9005") + + udpErrCh := make(chan error, 1) + go func() { + buf := make([]byte, 1024) + for { + udpListener.SetReadDeadline(time.Now().Add(30 * time.Second)) + n, _, err := udpListener.ReadFrom(buf) + if err != nil { + udpErrCh <- fmt.Errorf("failed receiving remote UDP packet: %v", err) + return + } + if string(buf[:n]) == "remote_test" { + break + } + } + udpErrCh <- nil + }() + + // Sync setup with LocalAction. + { + syncListener, err := net.Listen("tcp", "0.0.0.0:8995") + if err != nil { + return fmt.Errorf("net.Listen failed on 8995: %v", err) + } + log.Infof("fibTest: Listening for sync connection on port 8995") + + syncErrCh := make(chan error, 1) + go func() { + defer syncListener.Close() + conn, err := syncListener.Accept() + if err != nil { + syncErrCh <- err + return + } + defer conn.Close() + syncErrCh <- nil + }() + + select { + case err := <-syncErrCh: + if err != nil { + return err + } + log.Infof("fibTest: Sync with LocalAction successful") + case <-time.After(15 * time.Second): + syncListener.Close() + return fmt.Errorf("timeout waiting for client TCP connection on 8995") + } + } + + // Wait for remote UDP + select { + case err := <-udpErrCh: + if err != nil { + return err + } + log.Infof("fibTest: Received remote UDP packet 'remote_test'") + case <-time.After(15 * time.Second): + return fmt.Errorf("timeout waiting for remote client UDP connection on 9005") + } + + // verify UDP negative logic (Local Loopback) + // Packets sent to itself (127.0.0.1) should fail the FIB interface check + // and be dropped by the default policy. + log.Infof("fibTest: Starting negative test (Local Loopback to 127.0.0.1:9005)") + conn, err := net.Dial("udp", "127.0.0.1:9005") + if err != nil { + return fmt.Errorf("local UDP dial failed: %v", err) + } + + if _, err := conn.Write([]byte("local_test")); err != nil { + conn.Close() + return fmt.Errorf("failed to write local UDP packet: %v", err) + } + conn.Close() + + // Read loop to verify local_test doesn't arrive. + buf := make([]byte, 1024) + udpListener.SetReadDeadline(time.Now().Add(1 * time.Second)) + for { + n, _, err := udpListener.ReadFrom(buf) + if err != nil { + if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + log.Infof("fibTest: Success! Local UDP packet was dropped (Timed out waiting for packet)") + break + } + return fmt.Errorf("unexpected error waiting for local UDP: %v", err) + } + if string(buf[:n]) == "local_test" { + return fmt.Errorf("local UDP transmission to 9005 succeeded when it should have been dropped") + } + } + return nil +} + +// LocalAction implements TestCase.LocalAction. +func (t *fibTest) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error { + if ipv6 { + return nil + } + + // 1. Sync with ContainerAction by connecting to port 8995. + log.Infof("fibTest (LocalAction): Attempting to sync with container on port 8995") + { + addr := net.JoinHostPort(ip.String(), "8995") + var err error + var conn net.Conn + for i := 0; i < 10; i++ { + conn, err = net.Dial("tcp", addr) + if err == nil { + conn.Close() + break + } + time.Sleep(1 * time.Second) + } + if err != nil { + return fmt.Errorf("sync dial failed: %v", err) + } + log.Infof("fibTest (LocalAction): Sync successful") + } + + // 2. Validate that remote UDP packets reach Container.. + log.Infof("fibTest (LocalAction): Sending remote UDP packets to container") + { + conn, err := net.Dial("udp", net.JoinHostPort(ip.String(), "9005")) + if err != nil { + return fmt.Errorf("udp dial failed: %v", err) + } + defer conn.Close() + + // Send multiple times to ensure delivery over UDP + for i := 0; i < 10; i++ { + conn.Write([]byte("remote_test")) + time.Sleep(1 * time.Second) + } + log.Infof("fibTest (LocalAction): Finished sending remote UDP packets") + } + + return nil +} + +// Timeout implements TestCase.Timeout. +func (*fibTest) Timeout() time.Duration { + return 90 * time.Second +} diff --git a/test/syscalls/linux/socket_netlink_netfilter.cc b/test/syscalls/linux/socket_netlink_netfilter.cc index 94ce9f9cb7..f85e81e43f 100644 --- a/test/syscalls/linux/socket_netlink_netfilter.cc +++ b/test/syscalls/linux/socket_netlink_netfilter.cc @@ -3983,6 +3983,92 @@ INSTANTIATE_TEST_SUITE_P( return info.param.test_name; }); +std::vector GetFibRuleTestParams() { + return { + RuleWithExprTestParams{ + .test_name = "MissingDreg", + .expr_name = "fib", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_FIB_RESULT, NFT_FIB_RESULT_OIF) + .U32Attr(NFTA_FIB_FLAGS, NFTA_FIB_F_SADDR), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "MissingResult", + .expr_name = "fib", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_FIB_DREG, NFT_REG_1) + .U32Attr(NFTA_FIB_FLAGS, NFTA_FIB_F_SADDR), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "MissingFlags", + .expr_name = "fib", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_FIB_DREG, NFT_REG_1) + .U32Attr(NFTA_FIB_RESULT, NFT_FIB_RESULT_OIF), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "BothSaddrDaddr", + .expr_name = "fib", + .expr_attrs = + NlNestedAttr() + .U32Attr(NFTA_FIB_DREG, NFT_REG_1) + .U32Attr(NFTA_FIB_RESULT, NFT_FIB_RESULT_OIF) + .U32Attr(NFTA_FIB_FLAGS, NFTA_FIB_F_SADDR | NFTA_FIB_F_DADDR), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "BothOifIif", + .expr_name = "fib", + .expr_attrs = + NlNestedAttr() + .U32Attr(NFTA_FIB_DREG, NFT_REG_1) + .U32Attr(NFTA_FIB_RESULT, NFT_FIB_RESULT_OIF) + .U32Attr(NFTA_FIB_FLAGS, + NFTA_FIB_F_SADDR | NFTA_FIB_F_IIF | NFTA_FIB_F_OIF), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "InvalidResult", + .expr_name = "fib", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_FIB_DREG, NFT_REG_1) + .U32Attr(NFTA_FIB_RESULT, 999) + .U32Attr(NFTA_FIB_FLAGS, NFTA_FIB_F_SADDR), + .expected_error_no = EINVAL}, + RuleWithExprTestParams{ + .test_name = "ValidOIF", + .expr_name = "fib", + .expr_attrs = NlNestedAttr() + .U32Attr(NFTA_FIB_DREG, NFT_REG_1) + .U32Attr(NFTA_FIB_RESULT, NFT_FIB_RESULT_OIF) + .U32Attr(NFTA_FIB_FLAGS, NFTA_FIB_F_SADDR), + .expected_error_no = 0}, + RuleWithExprTestParams{ + .test_name = "ValidOIFNAME", + .expr_name = "fib", + .expr_attrs = + NlNestedAttr() + .U32Attr(NFTA_FIB_DREG, NFT_REG_1) + .U32Attr(NFTA_FIB_RESULT, NFT_FIB_RESULT_OIFNAME) + .U32Attr(NFTA_FIB_FLAGS, NFTA_FIB_F_DADDR | NFTA_FIB_F_IIF), + .expected_error_no = 0}, + RuleWithExprTestParams{ + .test_name = "ValidADDRTYPE", + .expr_name = "fib", + .expr_attrs = + NlNestedAttr() + .U32Attr(NFTA_FIB_DREG, NFT_REG_1) + .U32Attr(NFTA_FIB_RESULT, NFT_FIB_RESULT_ADDRTYPE) + .U32Attr(NFTA_FIB_FLAGS, NFTA_FIB_F_SADDR | NFTA_FIB_F_IIF), + .expected_error_no = 0}, + }; +} + +INSTANTIATE_TEST_SUITE_P(FibRuleTest, AddRuleWithExprTest, + /*param_generator=*/ValuesIn(GetFibRuleTestParams()), + /*param_name_generator=*/ + [](const TestParamInfo& info) { + return info.param.test_name; + }); + } // namespace } // namespace testing