[client] Unify peer and route ACL filtering with multi-source rules (#6322)

* Unify peer and route ACL filtering with multi-source peer rules

* Remove partial userspace firewall mode and open foreign chains via a table-less allower

* Snapshot iptables rule maps before persisting state

* Scope userspace firewall wildcard source rules per address family

* Install nftables peer filter and mangle rules in a single transaction

* Share the iptables jump rule spec between install and cleanup

* Fix legacy ACL source wildcard and keep rollback tracking on delete failure

* Fix CI: recognize multi-value port set lookups in tests and correct PeerIP lint suppression

* Fall back to per-prefix filter rules when ipset is unavailable

* Annotate legacy PeerIP usages in ACL tests and fix import formatting

* Keep firewall rule bookkeeping in step with the kernel on replace and teardown

* Release the routing reference when the route manager shuts down

* Keep set references and rule tracking consistent when a routing rule fails
This commit is contained in:
Viktor Liu
2026-09-03 10:02:50 +02:00
committed by GitHub
parent 6aaeed744e
commit 778b3b3264
81 changed files with 9046 additions and 6686 deletions
@@ -1,37 +0,0 @@
//go:build !windows
package uspfilter
import (
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/firewall/firewalld"
"github.com/netbirdio/netbird/client/internal/statemanager"
)
// Close cleans up the firewall manager by removing all rules and closing trackers
func (m *Manager) Close(stateManager *statemanager.Manager) error {
m.mutex.Lock()
defer m.mutex.Unlock()
m.resetState()
if m.nativeFirewall != nil {
return m.nativeFirewall.Close(stateManager)
}
if err := firewalld.UntrustInterface(m.wgIface.Name()); err != nil {
log.Warnf("failed to untrust interface in firewalld: %v", err)
}
return nil
}
// AllowNetbird allows netbird interface traffic
func (m *Manager) AllowNetbird() error {
if m.nativeFirewall != nil {
return m.nativeFirewall.AllowNetbird()
}
if err := firewalld.TrustInterface(m.wgIface.Name()); err != nil {
log.Warnf("failed to trust interface in firewalld: %v", err)
}
return nil
}
-17
View File
@@ -1,17 +0,0 @@
package common
import (
wgdevice "golang.zx2c4.com/wireguard/device"
"github.com/netbirdio/netbird/client/iface/device"
"github.com/netbirdio/netbird/client/iface/wgaddr"
)
// IFaceMapper defines subset methods of interface required for manager
type IFaceMapper interface {
Name() string
SetFilter(device.PacketFilter) error
Address() wgaddr.Address
GetWGDevice() *wgdevice.Device
GetDevice() *device.FilteredDevice
}
+367 -279
View File
@@ -5,7 +5,6 @@ import (
"encoding/binary"
"errors"
"fmt"
"net"
"net/netip"
"os"
"slices"
@@ -20,14 +19,18 @@ import (
"github.com/google/uuid"
"github.com/hashicorp/go-multierror"
log "github.com/sirupsen/logrus"
wgdevice "golang.zx2c4.com/wireguard/device"
nberrors "github.com/netbirdio/netbird/client/errors"
"github.com/netbirdio/netbird/client/firewall/firewalld"
firewall "github.com/netbirdio/netbird/client/firewall/manager"
"github.com/netbirdio/netbird/client/firewall/uspfilter/common"
"github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
"github.com/netbirdio/netbird/client/firewall/uspfilter/forwarder"
nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log"
"github.com/netbirdio/netbird/client/iface/device"
"github.com/netbirdio/netbird/client/iface/netstack"
"github.com/netbirdio/netbird/client/iface/wgaddr"
nbid "github.com/netbirdio/netbird/client/internal/acl/id"
nftypes "github.com/netbirdio/netbird/client/internal/netflow/types"
"github.com/netbirdio/netbird/client/internal/statemanager"
@@ -58,7 +61,10 @@ const (
// EnvDisableMSSClamping disables TCP MSS clamping for forwarded traffic.
EnvDisableMSSClamping = "NB_DISABLE_MSS_CLAMPING"
// EnvForceUserspaceRouter forces userspace routing even if native routing is available.
// EnvForceUserspaceRouter is a deprecated alias for
// NB_FORCE_USERSPACE_FIREWALL: the userspace firewall always routes in
// userspace, so forcing one forces the other. Kept for backward
// compatibility.
EnvForceUserspaceRouter = "NB_FORCE_USERSPACE_ROUTER"
// EnvEnableLocalForwarding enables forwarding of local traffic to the native stack for internal (non-NetBird) interfaces.
@@ -70,14 +76,20 @@ const (
EnvEnableNetstackLocalForwarding = "NB_ENABLE_NETSTACK_LOCAL_FORWARDING"
)
var errNatNotSupported = errors.New("nat not supported with userspace firewall")
// errNotSupported is returned by firewall operations that only make sense with
// a kernel firewall (kernel NAT/DNAT, eBPF) and are not implemented in
// userspace mode, where they should not be called.
var errNotSupported = errors.New("not supported with userspace firewall")
// RuleSet is a set of rules grouped by a string key
type RuleSet map[string]PeerRule
// peerRules is the canonical list-based storage for peer ACL rules.
// Drop and accept rules live in separate slices; drop-before-accept
// ordering comes from consulting the deny slice (and its index) before
// the accept one.
type peerRules []*PeerRule
type RouteRules []*RouteRule
type routeRules []*RouteRule
func (r RouteRules) Sort() {
func (r routeRules) Sort() {
slices.SortStableFunc(r, func(a, b *RouteRule) int {
// Deny rules come first
if a.action == firewall.ActionDrop && b.action != firewall.ActionDrop {
@@ -86,22 +98,74 @@ func (r RouteRules) Sort() {
if a.action != firewall.ActionDrop && b.action == firewall.ActionDrop {
return 1
}
return strings.Compare(a.id, b.id)
return strings.Compare(string(a.id), string(b.id))
})
}
// peerRuleSpec carries the parameters that define a peer filter rule,
// threaded together through the build path so the builders take a single
// argument instead of a long parameter list.
type peerRuleSpec struct {
mgmtID []byte
sources []netip.Prefix
ipLayer gopacket.LayerType
proto firewall.Protocol
sPort *firewall.Port
dPort *firewall.Port
action firewall.Action
}
// Iface is the network interface the userspace firewall attaches to: the
// methods of the WireGuard device it actually uses.
type Iface interface {
Name() string
Address() wgaddr.Address
SetFilter(device.PacketFilter) error
GetWGDevice() *wgdevice.Device
}
// InterfaceAllower opens the NetBird interface in the host firewall so it
// doesn't drop traffic the userspace firewall handles, without taking over
// packet filtering. Implementations (nftables, iptables, firewalld, the windows
// netsh rule) are selected per platform and injected into Create; Apply runs at
// creation and Close on teardown.
type InterfaceAllower interface {
Apply() error
Close() error
}
// Config holds the dependencies and options for the userspace firewall.
type Config struct {
// IFace is the overlay interface the filter attaches to.
IFace Iface
// InterfaceAllower opens the NetBird interface in foreign kernel filter
// chains so the kernel doesn't drop traffic the userspace firewall handles.
// Nil in netstack mode, on non-Linux platforms without a backend, or when
// neither nftables nor iptables is available. firewalld trust is applied by
// the manager regardless, since firewalld owns its own chains and we cannot
// insert into them.
InterfaceAllower InterfaceAllower
// DisableServerRoutes indicates whether server routes are disabled.
DisableServerRoutes bool
FlowLogger nftypes.FlowLogger
MTU uint16
}
// Manager userspace firewall manager
type Manager struct {
outgoingRules map[netip.Addr]RuleSet
incomingDenyRules map[netip.Addr]RuleSet
incomingRules map[netip.Addr]RuleSet
routeRules RouteRules
routeRulesMap map[nbid.RuleID]*RouteRule
decoders sync.Pool
wgIface common.IFaceMapper
nativeFirewall firewall.Manager
decoders sync.Pool
wgIface Iface
ifaceAllower InterfaceAllower
mutex sync.RWMutex
mutex sync.RWMutex
incomingDenyRules peerRules
incomingAcceptRules peerRules
incomingDenyIndex peerRuleIndex
incomingAcceptIndex peerRuleIndex
peerRulesMap map[nbid.RuleID]*PeerRule
routeRules routeRules
routeRulesMap map[nbid.RuleID]*RouteRule
// indicates whether server routes are disabled
disableServerRoutes bool
@@ -219,24 +283,6 @@ func (d *decoder) decodeTransport(proto layers.IPProtocol, payload []byte) bool
return true
}
// Create userspace firewall manager constructor
func Create(iface common.IFaceMapper, disableServerRoutes bool, flowLogger nftypes.FlowLogger, mtu uint16) (*Manager, error) {
return create(iface, nil, disableServerRoutes, flowLogger, mtu)
}
func CreateWithNativeFirewall(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableServerRoutes bool, flowLogger nftypes.FlowLogger, mtu uint16) (*Manager, error) {
if nativeFirewall == nil {
return nil, errors.New("native firewall is nil")
}
mgr, err := create(iface, nativeFirewall, disableServerRoutes, flowLogger, mtu)
if err != nil {
return nil, err
}
return mgr, nil
}
func parseCreateEnv() (bool, bool, bool) {
var disableConntrack, enableLocalForwarding, disableMSSClamping bool
var err error
@@ -267,7 +313,7 @@ func parseCreateEnv() (bool, bool, bool) {
return disableConntrack, enableLocalForwarding, disableMSSClamping
}
func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableServerRoutes bool, flowLogger nftypes.FlowLogger, mtu uint16) (*Manager, error) {
func Create(cfg Config) (_ *Manager, err error) {
disableConntrack, enableLocalForwarding, disableMSSClamping := parseCreateEnv()
m := &Manager{
@@ -290,65 +336,133 @@ func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableSe
return d
},
},
nativeFirewall: nativeFirewall,
outgoingRules: make(map[netip.Addr]RuleSet),
incomingDenyRules: make(map[netip.Addr]RuleSet),
incomingRules: make(map[netip.Addr]RuleSet),
wgIface: iface,
wgIface: cfg.IFace,
ifaceAllower: cfg.InterfaceAllower,
localipmanager: newLocalIPManager(),
disableServerRoutes: disableServerRoutes,
disableServerRoutes: cfg.DisableServerRoutes,
stateful: !disableConntrack,
logger: nblog.NewFromLogrus(log.StandardLogger()),
flowLogger: flowLogger,
flowLogger: cfg.FlowLogger,
netstack: netstack.IsEnabled(),
localForwarding: enableLocalForwarding,
peerRulesMap: make(map[nbid.RuleID]*PeerRule),
routeRulesMap: make(map[nbid.RuleID]*RouteRule),
dnatMappings: make(map[netip.Addr]netip.Addr),
portDNATRules: []portDNATRule{},
netstackServices: make(map[serviceKey]struct{}),
mtu: mtu,
mtu: cfg.MTU,
}
m.routingEnabled.Store(false)
// Release the allower (and its monitor) if setup fails after it was wired in.
defer func() {
if err != nil {
m.closeAllowerOnError()
}
}()
if !disableMSSClamping {
m.mssClampEnabled = true
if mtu > ipv4TCPHeaderMinSize {
m.mssClampValueIPv4 = mtu - ipv4TCPHeaderMinSize
}
if mtu > ipv6TCPHeaderMinSize {
m.mssClampValueIPv6 = mtu - ipv6TCPHeaderMinSize
}
m.enableMSSClamping(cfg.MTU)
}
if err := m.localipmanager.UpdateLocalIPs(iface); err != nil {
if err := m.localipmanager.UpdateLocalIPs(cfg.IFace); err != nil {
return nil, fmt.Errorf("update local IPs: %w", err)
}
m.fragments = newFragmentTracker(m.logger)
if disableConntrack {
log.Info("conntrack is disabled")
} else {
m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout, m.logger, flowLogger)
m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, m.logger, flowLogger)
m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, m.logger, flowLogger)
}
m.setupConntrack(disableConntrack)
if m.netstack && m.localForwarding {
if err := m.initForwarder(); err != nil {
log.Errorf("failed to initialize forwarder: %v", err)
}
}
if err := iface.SetFilter(m); err != nil {
if err := cfg.IFace.SetFilter(m); err != nil {
m.fragments.Close()
return nil, fmt.Errorf("set filter: %w", err)
}
m.openHostFirewall(cfg.IFace.Name())
return m, nil
}
// closeAllowerOnError releases the allower (and its monitor) when Create fails
// after the allower was wired in.
func (m *Manager) closeAllowerOnError() {
if m.ifaceAllower == nil {
return
}
if err := m.ifaceAllower.Close(); err != nil {
log.Warnf("close interface allower after failed firewall setup: %v", err)
}
}
// enableMSSClamping enables MSS clamping and computes the per-family clamp values.
func (m *Manager) enableMSSClamping(mtu uint16) {
m.mssClampEnabled = true
if mtu > ipv4TCPHeaderMinSize {
m.mssClampValueIPv4 = mtu - ipv4TCPHeaderMinSize
}
if mtu > ipv6TCPHeaderMinSize {
m.mssClampValueIPv6 = mtu - ipv6TCPHeaderMinSize
}
}
// setupConntrack initializes the stateful trackers unless conntrack is disabled.
func (m *Manager) setupConntrack(disabled bool) {
if disabled {
log.Info("conntrack is disabled")
return
}
m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout, m.logger, m.flowLogger)
m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, m.logger, m.flowLogger)
m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, m.logger, m.flowLogger)
}
// openHostFirewall opens the NetBird interface in the kernel firewall so it
// doesn't drop traffic the userspace firewall handles. Best-effort: failures
// here shouldn't prevent the firewall from coming up.
func (m *Manager) openHostFirewall(ifaceName string) {
if m.ifaceAllower != nil {
if err := m.ifaceAllower.Apply(); err != nil {
log.Errorf("failed to allow netbird interface traffic: %v", err)
}
}
// firewalld owns its own chains we can't insert into, so trust the interface
// there in addition to the allower. Netstack has no kernel interface.
if !m.netstack {
if err := firewalld.TrustInterface(ifaceName); err != nil {
log.Warnf("failed to trust interface in firewalld: %v", err)
}
}
}
// Close cleans up the firewall manager: removes rules, closes trackers, and
// closes the interface allower.
func (m *Manager) Close(*statemanager.Manager) error {
m.mutex.Lock()
defer m.mutex.Unlock()
m.resetState()
var merr *multierror.Error
if m.ifaceAllower != nil {
if err := m.ifaceAllower.Close(); err != nil {
merr = multierror.Append(merr, fmt.Errorf("close interface allower: %w", err))
}
}
if !m.netstack {
if err := firewalld.UntrustInterface(m.wgIface.Name()); err != nil {
merr = multierror.Append(merr, fmt.Errorf("untrust interface in firewalld: %w", err))
}
}
return nberrors.FormatErrorOrNil(merr)
}
// blockInvalidRouted installs drop rules for traffic to the wg overlay that
// arrives via the routing path. v4 and v6 are independent: a v6 install
// failure leaves v4 protection in place (and vice versa) so the returned
// slice always contains whatever was successfully installed, even on error.
// Callers must persist the slice so DisableRouting can clean partial state.
func (m *Manager) blockInvalidRouted(iface common.IFaceMapper) ([]firewall.Rule, error) {
func (m *Manager) blockInvalidRouted(iface Iface) ([]firewall.Rule, error) {
wgPrefix := iface.Address().Network
log.Debugf("blocking invalid routed traffic for %s", wgPrefix)
@@ -359,7 +473,7 @@ func (m *Manager) blockInvalidRouted(iface common.IFaceMapper) ([]firewall.Rule,
}
var rules []firewall.Rule
v4Rule, err := m.addRouteFiltering(
v4Rule, err := m.addRouteRule(
nil,
sources,
firewall.Network{Prefix: wgPrefix},
@@ -375,7 +489,7 @@ func (m *Manager) blockInvalidRouted(iface common.IFaceMapper) ([]firewall.Rule,
if v6Net.IsValid() {
log.Debugf("blocking invalid routed traffic for %s", v6Net)
v6Rule, err := m.addRouteFiltering(
v6Rule, err := m.addRouteRule(
nil,
sources,
firewall.Network{Prefix: v6Net},
@@ -396,20 +510,14 @@ func (m *Manager) blockInvalidRouted(iface common.IFaceMapper) ([]firewall.Rule,
}
func (m *Manager) determineRouting() error {
var disableUspRouting, forceUserspaceRouter bool
var err error
var disableUspRouting bool
if val := os.Getenv(EnvDisableUserspaceRouting); val != "" {
var err error
disableUspRouting, err = strconv.ParseBool(val)
if err != nil {
log.Warnf("failed to parse %s: %v", EnvDisableUserspaceRouting, err)
}
}
if val := os.Getenv(EnvForceUserspaceRouter); val != "" {
forceUserspaceRouter, err = strconv.ParseBool(val)
if err != nil {
log.Warnf("failed to parse %s: %v", EnvForceUserspaceRouter, err)
}
}
switch {
case disableUspRouting:
@@ -424,26 +532,11 @@ func (m *Manager) determineRouting() error {
log.Info("server routes are disabled")
case forceUserspaceRouter:
m.routingEnabled.Store(true)
m.nativeRouter.Store(false)
log.Info("userspace routing is forced")
case !m.netstack && m.nativeFirewall != nil:
// if the OS supports routing natively, then we don't need to filter/route ourselves
// netstack mode won't support native routing as there is no interface
m.routingEnabled.Store(true)
m.nativeRouter.Store(true)
log.Info("native routing is enabled")
default:
m.routingEnabled.Store(true)
m.nativeRouter.Store(false)
log.Info("userspace routing enabled by default")
log.Info("userspace routing enabled")
}
if m.routingEnabled.Load() && !m.nativeRouter.Load() {
@@ -509,96 +602,118 @@ func (m *Manager) IsStateful() bool {
return m.stateful
}
func (m *Manager) AddNatRule(pair firewall.RouterPair) error {
if m.nativeRouter.Load() && m.nativeFirewall != nil {
return m.nativeFirewall.AddNatRule(pair)
}
func (m *Manager) AddNatRule(firewall.RouterPair) error {
// userspace routed packets are always SNATed to the inbound direction
// TODO: implement outbound SNAT
return nil
}
// RemoveNatRule removes a routing firewall rule
func (m *Manager) RemoveNatRule(pair firewall.RouterPair) error {
if m.nativeRouter.Load() && m.nativeFirewall != nil {
return m.nativeFirewall.RemoveNatRule(pair)
}
func (m *Manager) RemoveNatRule(firewall.RouterPair) error {
return nil
}
// AddPeerFiltering rule to the firewall
//
// If comment argument is empty firewall manager should set
// rule ID as comment for the rule
func (m *Manager) AddPeerFiltering(
// addPeerRule installs an input-chain rule that matches packets
// by source only. Called from AddFilterRule when the caller doesn't
// specify a destination. Sources are expected to share one address
// family; the family selects the ipLayer so the ICMP variant matches
// what the decoder produces.
func (m *Manager) addPeerRule(
id []byte,
ip net.IP,
sources []netip.Prefix,
proto firewall.Protocol,
sPort *firewall.Port,
dPort *firewall.Port,
action firewall.Action,
_ string,
) ([]firewall.Rule, error) {
// TODO: fix in upper layers
i, ok := netip.AddrFromSlice(ip)
if !ok {
return nil, fmt.Errorf("invalid IP: %s", ip)
}
i = i.Unmap()
r := PeerRule{
id: uuid.New().String(),
mgmtId: id,
ip: i,
ipLayer: layers.LayerTypeIPv6,
matchByIP: true,
drop: action == firewall.ActionDrop,
}
if i.Is4() {
r.ipLayer = layers.LayerTypeIPv4
}
if s := r.ip.String(); s == "0.0.0.0" || s == "::" {
r.matchByIP = false
}
r.sPort = sPort
r.dPort = dPort
r.protoLayer = protoToLayer(proto, r.ipLayer)
m.mutex.Lock()
var targetMap map[netip.Addr]RuleSet
if r.drop {
targetMap = m.incomingDenyRules
} else {
targetMap = m.incomingRules
}
if _, ok := targetMap[r.ip]; !ok {
targetMap[r.ip] = make(RuleSet)
}
targetMap[r.ip][r.id] = r
m.mutex.Unlock()
return []firewall.Rule{&r}, nil
}
func (m *Manager) AddRouteFiltering(
id []byte,
sources []netip.Prefix,
destination firewall.Network,
proto firewall.Protocol,
sPort, dPort *firewall.Port,
action firewall.Action,
) (firewall.Rule, error) {
m.mutex.Lock()
defer m.mutex.Unlock()
return m.addRouteFiltering(id, sources, destination, proto, sPort, dPort, action)
// Sources are a single family; normalize v4-mapped prefixes to plain
// v4 and pick the matching IP layer. A /0 source matches any address
// of its own family only, mirroring the kernel backends.
normalized := make([]netip.Prefix, len(sources))
ipLayer := layers.LayerTypeIPv4
for i, p := range sources {
normalized[i] = firewall.UnmapPrefix(p)
if normalized[i].Addr().Is6() {
ipLayer = layers.LayerTypeIPv6
}
}
spec := peerRuleSpec{
mgmtID: id,
sources: normalized,
ipLayer: ipLayer,
proto: proto,
sPort: sPort,
dPort: dPort,
action: action,
}
return m.addOnePeerRule(spec), nil
}
func (m *Manager) addRouteFiltering(
// addOnePeerRule builds and registers a single-family peer rule, or
// returns the existing rule when one with the same content key is
// already installed. The caller must hold m.mutex. The content key is
// the shared GenerateRuleID with an empty destination, so peer rules
// dedup the same way route rules and the kernel backends do; it is
// order-independent, so callers passing the same sources in any order
// dedup to one rule.
//
// There is no refcount: a content key is installed once and deleted on
// the first DeleteFilterRule for that key. The caller must therefore
// key its own tracking by the returned rule id so add and delete stay
// balanced per content key; the acl manager does this via
// peerRulesPairs.
func (m *Manager) addOnePeerRule(spec peerRuleSpec) *PeerRule {
ruleID := nbid.GenerateRuleID(spec.sources, firewall.Network{}, spec.proto, spec.sPort, spec.dPort, spec.action)
if existing, ok := m.peerRulesMap[ruleID]; ok {
return existing
}
rule := m.buildPeerRule(ruleID, spec)
m.registerPeerRule(rule)
return rule
}
func (m *Manager) buildPeerRule(ruleID nbid.RuleID, spec peerRuleSpec) *PeerRule {
r := &PeerRule{
id: ruleID,
mgmtId: spec.mgmtID,
sources: spec.sources,
action: spec.action,
srcPort: spec.sPort,
dstPort: spec.dPort,
}
r.sourceAddrs = make(map[netip.Addr]struct{}, len(spec.sources))
for _, p := range spec.sources {
if p.Bits() == p.Addr().BitLen() {
r.sourceAddrs[p.Addr()] = struct{}{}
}
}
r.protoLayer = protoToLayer(spec.proto, spec.ipLayer)
return r
}
// registerPeerRule records a freshly built peer rule in the matching
// slice, index, and dedup map. The caller must hold m.mutex.
func (m *Manager) registerPeerRule(r *PeerRule) {
if r.action == firewall.ActionDrop {
m.incomingDenyRules = append(m.incomingDenyRules, r)
m.incomingDenyIndex.add(r)
} else {
m.incomingAcceptRules = append(m.incomingAcceptRules, r)
m.incomingAcceptIndex.add(r)
}
m.peerRulesMap[r.id] = r
}
// AddFilterRule is the unified entry point for both peer (input chain)
// and route (forward chain) filtering rules. The destination
// distinguishes the two semantics: a zero Network installs an
// input-side rule that matches by source only; a set Network installs
// a forward-side rule that also matches the destination.
func (m *Manager) AddFilterRule(
id []byte,
sources []netip.Prefix,
destination firewall.Network,
@@ -606,19 +721,49 @@ func (m *Manager) addRouteFiltering(
sPort, dPort *firewall.Port,
action firewall.Action,
) (firewall.Rule, error) {
if m.nativeRouter.Load() && m.nativeFirewall != nil {
return m.nativeFirewall.AddRouteFiltering(id, sources, destination, proto, sPort, dPort, action)
if len(sources) == 0 {
return nil, firewall.ErrNoSources
}
ruleKey := nbid.GenerateRouteRuleKey(sources, destination, proto, sPort, dPort, action)
if destination.IsZero() {
return m.addPeerRule(id, sources, proto, sPort, dPort, action)
}
if existingRule, ok := m.routeRulesMap[ruleKey]; ok {
m.mutex.Lock()
defer m.mutex.Unlock()
return m.addRouteRule(id, sources, destination, proto, sPort, dPort, action)
}
// DeleteFilterRule deletes a filtering rule. The rule's underlying type
// is used to route to the correct internal path.
func (m *Manager) DeleteFilterRule(rule firewall.Rule) error {
m.mutex.Lock()
defer m.mutex.Unlock()
if r, ok := rule.(*PeerRule); ok {
return m.deletePeerRuleLocked(r)
}
// Anything else is a route rule (matched on the forward path).
return m.deleteRouteRule(rule)
}
func (m *Manager) addRouteRule(
id []byte,
sources []netip.Prefix,
destination firewall.Network,
proto firewall.Protocol,
sPort, dPort *firewall.Port,
action firewall.Action,
) (firewall.Rule, error) {
ruleID := nbid.GenerateRuleID(sources, destination, proto, sPort, dPort, action)
if existingRule, ok := m.routeRulesMap[ruleID]; ok {
return existingRule, nil
}
rule := RouteRule{
// TODO: consolidate these IDs
id: string(ruleKey),
id: ruleID,
mgmtId: id,
sources: sources,
dstSet: destination.Set,
@@ -633,78 +778,58 @@ func (m *Manager) addRouteFiltering(
m.routeRules = append(m.routeRules, &rule)
m.routeRules.Sort()
m.routeRulesMap[ruleKey] = &rule
m.routeRulesMap[ruleID] = &rule
return &rule, nil
}
func (m *Manager) DeleteRouteRule(rule firewall.Rule) error {
m.mutex.Lock()
defer m.mutex.Unlock()
return m.deleteRouteRule(rule)
}
func (m *Manager) deleteRouteRule(rule firewall.Rule) error {
if m.nativeRouter.Load() && m.nativeFirewall != nil {
return m.nativeFirewall.DeleteRouteRule(rule)
ruleID := rule.ID()
trimmed, _, ok := removeRuleByID(m.routeRules, ruleID)
if !ok {
return fmt.Errorf("route rule not found: %s", ruleID)
}
ruleKey := nbid.RuleID(rule.ID())
if _, ok := m.routeRulesMap[ruleKey]; !ok {
return fmt.Errorf("route rule not found: %s", ruleKey)
}
idx := slices.IndexFunc(m.routeRules, func(r *RouteRule) bool {
return r.id == string(ruleKey)
})
if idx < 0 {
return fmt.Errorf("route rule not found in slice: %s", ruleKey)
}
m.routeRules = slices.Delete(m.routeRules, idx, idx+1)
delete(m.routeRulesMap, ruleKey)
m.routeRules = trimmed
delete(m.routeRulesMap, ruleID)
return nil
}
// DeletePeerRule from the firewall by rule definition
func (m *Manager) DeletePeerRule(rule firewall.Rule) error {
m.mutex.Lock()
defer m.mutex.Unlock()
// deletePeerRuleLocked removes a peer rule from the matching slice,
// index, and dedup map. The caller must hold m.mutex.
func (m *Manager) deletePeerRuleLocked(r *PeerRule) error {
target, index := &m.incomingAcceptRules, &m.incomingAcceptIndex
if r.action == firewall.ActionDrop {
target, index = &m.incomingDenyRules, &m.incomingDenyIndex
}
r, ok := rule.(*PeerRule)
trimmed, stored, ok := removeRuleByID(*target, r.id)
if !ok {
return fmt.Errorf("delete rule: invalid rule type: %T", rule)
}
var sourceMap map[netip.Addr]RuleSet
if r.drop {
sourceMap = m.incomingDenyRules
} else {
sourceMap = m.incomingRules
}
if ruleset, ok := sourceMap[r.ip]; ok {
if _, exists := ruleset[r.id]; !exists {
return fmt.Errorf("delete rule: no rule with such id: %v", r.id)
}
delete(ruleset, r.id)
if len(ruleset) == 0 {
delete(sourceMap, r.ip)
}
} else {
return fmt.Errorf("delete rule: no rule with such id: %v", r.id)
}
*target = trimmed
index.remove(stored)
delete(m.peerRulesMap, r.id)
return nil
}
// SetLegacyManagement doesn't need to be implemented for this manager
func (m *Manager) SetLegacyManagement(isLegacy bool) error {
if m.nativeFirewall == nil {
return nil
// removeRuleByID removes the first rule whose id matches ruleID from
// rules, preserving order. It returns the trimmed slice, the removed
// rule, and whether a match was found.
func removeRuleByID[S ~[]T, T firewall.Rule](rules S, ruleID firewall.RuleID) (S, T, bool) {
idx := slices.IndexFunc(rules, func(r T) bool { return r.ID() == ruleID })
var removed T
if idx < 0 {
return rules, removed, false
}
return m.nativeFirewall.SetLegacyManagement(isLegacy)
removed = rules[idx]
return slices.Delete(rules, idx, idx+1), removed, true
}
// SetLegacyManagement is a no-op for the userspace firewall: it only matters
// when an old management server can't send route firewall rules, which the
// userspace router doesn't rely on.
func (m *Manager) SetLegacyManagement(bool) error {
return nil
}
// Flush doesn't need to be implemented for this manager
@@ -713,11 +838,14 @@ func (m *Manager) Flush() error { return nil }
// resetState clears all firewall rules and closes connection trackers.
// Must be called with m.mutex held.
func (m *Manager) resetState() {
clear(m.outgoingRules)
clear(m.incomingDenyRules)
clear(m.incomingRules)
m.incomingDenyRules = m.incomingDenyRules[:0]
m.incomingAcceptRules = m.incomingAcceptRules[:0]
m.incomingDenyIndex.reset()
m.incomingAcceptIndex.reset()
clear(m.peerRulesMap)
clear(m.routeRulesMap)
m.routeRules = m.routeRules[:0]
m.blockRules = nil
m.udpHookOut.Store(nil)
m.tcpHookOut.Store(nil)
@@ -751,21 +879,15 @@ func (m *Manager) resetState() {
}
}
// SetupEBPFProxyNoTrack creates notrack rules for eBPF proxy loopback traffic.
func (m *Manager) SetupEBPFProxyNoTrack(proxyPort, wgPort uint16) error {
if m.nativeFirewall == nil {
return nil
}
return m.nativeFirewall.SetupEBPFProxyNoTrack(proxyPort, wgPort)
// SetupEBPFProxyNoTrack is not supported by the userspace firewall: eBPF isn't
// used in userspace mode, so this should never be called.
func (m *Manager) SetupEBPFProxyNoTrack(uint16, uint16) error {
return errNotSupported
}
// UpdateSet updates the rule destinations associated with the given set
// by merging the existing prefixes with the new ones, then deduplicating.
func (m *Manager) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error {
if m.nativeRouter.Load() && m.nativeFirewall != nil {
return m.nativeFirewall.UpdateSet(set, prefixes)
}
m.mutex.Lock()
defer m.mutex.Unlock()
@@ -863,11 +985,11 @@ func (m *Manager) extractIPs(d *decoder) (srcIP, dstIP netip.Addr) {
case layers.LayerTypeIPv4:
src, _ := netip.AddrFromSlice(d.ip4.SrcIP)
dst, _ := netip.AddrFromSlice(d.ip4.DstIP)
return src, dst
return src.Unmap(), dst.Unmap()
case layers.LayerTypeIPv6:
src, _ := netip.AddrFromSlice(d.ip6.SrcIP)
dst, _ := netip.AddrFromSlice(d.ip6.DstIP)
return src, dst
return src.Unmap(), dst.Unmap()
default:
return netip.Addr{}, netip.Addr{}
}
@@ -1622,20 +1744,12 @@ func (m *Manager) peerACLsBlock(srcIP netip.Addr, d *decoder, packetData []byte)
return nil, false
}
if mgmtId, filter, ok := validateRule(srcIP, packetData, m.incomingDenyRules[srcIP], d); ok {
if mgmtId, filter, ok := m.incomingDenyIndex.match(srcIP, d); ok {
return mgmtId, filter
}
if mgmtId, filter, ok := validateRule(srcIP, packetData, m.incomingRules[srcIP], d); ok {
if mgmtId, filter, ok := m.incomingAcceptIndex.match(srcIP, d); ok {
return mgmtId, filter
}
if mgmtId, filter, ok := validateRule(srcIP, packetData, m.incomingRules[netip.IPv4Unspecified()], d); ok {
return mgmtId, filter
}
if mgmtId, filter, ok := validateRule(srcIP, packetData, m.incomingRules[netip.IPv6Unspecified()], d); ok {
return mgmtId, filter
}
return nil, true
}
@@ -1656,39 +1770,6 @@ func portsMatch(rulePort *firewall.Port, packetPort uint16) bool {
return false
}
func validateRule(ip netip.Addr, packetData []byte, rules map[string]PeerRule, d *decoder) ([]byte, bool, bool) {
payloadLayer := d.decoded[1]
for _, rule := range rules {
if rule.matchByIP && ip.Compare(rule.ip) != 0 {
continue
}
if rule.protoLayer == layerTypeAll {
return rule.mgmtId, rule.drop, true
}
if !protoLayerMatches(rule.protoLayer, payloadLayer) {
continue
}
switch payloadLayer {
case layers.LayerTypeTCP:
if portsMatch(rule.sPort, uint16(d.tcp.SrcPort)) && portsMatch(rule.dPort, uint16(d.tcp.DstPort)) {
return rule.mgmtId, rule.drop, true
}
case layers.LayerTypeUDP:
if portsMatch(rule.sPort, uint16(d.udp.SrcPort)) && portsMatch(rule.dPort, uint16(d.udp.DstPort)) {
return rule.mgmtId, rule.drop, true
}
case layers.LayerTypeICMPv4, layers.LayerTypeICMPv6:
return rule.mgmtId, rule.drop, true
}
}
return nil, false, false
}
// routeACLsPass returns true if the packet is allowed by the route ACLs
func (m *Manager) routeACLsPass(srcIP, dstIP netip.Addr, protoLayer gopacket.LayerType, srcPort, dstPort uint16) ([]byte, bool) {
m.mutex.RLock()
@@ -1765,10 +1846,13 @@ func (m *Manager) EnableRouting() error {
}
rules, err := m.blockInvalidRouted(m.wgIface)
// Persist whatever was installed even on partial failure, so DisableRouting
// can clean it up later.
m.blockRules = rules
if err != nil {
// Roll back so forwarding can't stay active without the full set of
// block rules.
if derr := m.disableRouting(); derr != nil {
log.Warnf("roll back routing after block rule failure: %v", derr)
}
return fmt.Errorf("block invalid routed: %w", err)
}
@@ -1779,6 +1863,10 @@ func (m *Manager) DisableRouting() error {
m.mutex.Lock()
defer m.mutex.Unlock()
return m.disableRouting()
}
func (m *Manager) disableRouting() error {
fwder := m.forwarder.Load()
if fwder == nil {
return nil
+75 -49
View File
@@ -94,7 +94,7 @@ func BenchmarkCoreFiltering(b *testing.B) {
stateful: false,
setupFunc: func(m *Manager) {
// Single rule allowing all traffic
_, err := m.AddPeerFiltering(nil, net.ParseIP("0.0.0.0"), fw.ProtocolALL, nil, nil, fw.ActionAccept, "")
_, err := m.AddFilterRule(nil, pfx(net.ParseIP("0.0.0.0")), fw.Network{}, fw.ProtocolALL, nil, nil, fw.ActionAccept)
require.NoError(b, err)
},
desc: "Baseline: Single 'allow all' rule without connection tracking",
@@ -114,15 +114,13 @@ func BenchmarkCoreFiltering(b *testing.B) {
// Add explicit rules matching return traffic pattern
for i := 0; i < 1000; i++ { // Simulate realistic ruleset size
ip := generateRandomIPs(1)[0]
_, err := m.AddPeerFiltering(
_, err := m.AddFilterRule(
nil,
ip,
pfx(ip), fw.Network{},
fw.ProtocolTCP,
&fw.Port{Values: []uint16{uint16(1024 + i)}},
&fw.Port{Values: []uint16{80}},
fw.ActionAccept,
"",
)
fw.ActionAccept)
require.NoError(b, err)
}
},
@@ -133,15 +131,13 @@ func BenchmarkCoreFiltering(b *testing.B) {
stateful: true,
setupFunc: func(m *Manager) {
// Add some basic rules but rely on state for established connections
_, err := m.AddPeerFiltering(
_, err := m.AddFilterRule(
nil,
net.ParseIP("0.0.0.0"),
pfx(net.ParseIP("0.0.0.0")), fw.Network{},
fw.ProtocolTCP,
nil,
nil,
fw.ActionDrop,
"",
)
fw.ActionDrop)
require.NoError(b, err)
},
desc: "Connection tracking with established connections",
@@ -168,9 +164,12 @@ func BenchmarkCoreFiltering(b *testing.B) {
}
// Create manager and basic setup
manager, _ := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(b, err)
defer b.Cleanup(func() {
require.NoError(b, manager.Close(nil))
})
@@ -208,9 +207,12 @@ func BenchmarkStateScaling(b *testing.B) {
for _, count := range connCounts {
b.Run(fmt.Sprintf("conns_%d", count), func(b *testing.B) {
manager, _ := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(b, err)
b.Cleanup(func() {
require.NoError(b, manager.Close(nil))
})
@@ -251,9 +253,12 @@ func BenchmarkEstablishmentOverhead(b *testing.B) {
for _, sc := range scenarios {
b.Run(sc.name, func(b *testing.B) {
manager, _ := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(b, err)
b.Cleanup(func() {
require.NoError(b, manager.Close(nil))
})
@@ -409,9 +414,12 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
for _, sc := range scenarios {
b.Run(sc.name, func(b *testing.B) {
manager, _ := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(b, err)
b.Cleanup(func() {
require.NoError(b, manager.Close(nil))
})
@@ -536,9 +544,12 @@ func BenchmarkLongLivedConnections(b *testing.B) {
require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK"))
}
manager, _ := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(b, err)
defer b.Cleanup(func() {
require.NoError(b, manager.Close(nil))
})
@@ -546,7 +557,7 @@ func BenchmarkLongLivedConnections(b *testing.B) {
// Setup initial state based on scenario
if sc.rules {
// Single rule to allow all return traffic from port 80
_, err := manager.AddPeerFiltering(nil, net.ParseIP("0.0.0.0"), fw.ProtocolTCP, &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept, "")
_, err := manager.AddFilterRule(nil, pfx(net.ParseIP("0.0.0.0")), fw.Network{}, fw.ProtocolTCP, &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept)
require.NoError(b, err)
}
@@ -619,9 +630,12 @@ func BenchmarkShortLivedConnections(b *testing.B) {
require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK"))
}
manager, _ := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(b, err)
defer b.Cleanup(func() {
require.NoError(b, manager.Close(nil))
})
@@ -629,7 +643,7 @@ func BenchmarkShortLivedConnections(b *testing.B) {
// Setup initial state based on scenario
if sc.rules {
// Single rule to allow all return traffic from port 80
_, err := manager.AddPeerFiltering(nil, net.ParseIP("0.0.0.0"), fw.ProtocolTCP, &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept, "")
_, err := manager.AddFilterRule(nil, pfx(net.ParseIP("0.0.0.0")), fw.Network{}, fw.ProtocolTCP, &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept)
require.NoError(b, err)
}
@@ -730,16 +744,19 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) {
require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK"))
}
manager, _ := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(b, err)
defer b.Cleanup(func() {
require.NoError(b, manager.Close(nil))
})
// Setup initial state based on scenario
if sc.rules {
_, err := manager.AddPeerFiltering(nil, net.ParseIP("0.0.0.0"), fw.ProtocolTCP, &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept, "")
_, err := manager.AddFilterRule(nil, pfx(net.ParseIP("0.0.0.0")), fw.Network{}, fw.ProtocolTCP, &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept)
require.NoError(b, err)
}
@@ -810,15 +827,18 @@ func BenchmarkParallelShortLivedConnections(b *testing.B) {
require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK"))
}
manager, _ := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(b, err)
defer b.Cleanup(func() {
require.NoError(b, manager.Close(nil))
})
if sc.rules {
_, err := manager.AddPeerFiltering(nil, net.ParseIP("0.0.0.0"), fw.ProtocolTCP, &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept, "")
_, err := manager.AddFilterRule(nil, pfx(net.ParseIP("0.0.0.0")), fw.Network{}, fw.ProtocolTCP, &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept)
require.NoError(b, err)
}
@@ -931,7 +951,7 @@ func BenchmarkRouteACLs(b *testing.B) {
for _, r := range rules {
dst := fw.Network{Prefix: r.dest}
_, err := manager.AddRouteFiltering(nil, r.sources, dst, r.proto, nil, r.port, fw.ActionAccept)
_, err := manager.AddFilterRule(nil, r.sources, dst, r.proto, nil, r.port, fw.ActionAccept)
if err != nil {
b.Fatal(err)
}
@@ -1014,9 +1034,11 @@ func BenchmarkMSSClamping(b *testing.B) {
for _, sc := range scenarios {
b.Run(sc.name, func(b *testing.B) {
manager, err := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(b, err)
defer func() {
require.NoError(b, manager.Close(nil))
@@ -1079,9 +1101,11 @@ func BenchmarkMSSClampingOverhead(b *testing.B) {
for _, sc := range scenarios {
b.Run(sc.name, func(b *testing.B) {
manager, err := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(b, err)
defer func() {
require.NoError(b, manager.Close(nil))
@@ -1134,9 +1158,11 @@ func BenchmarkMSSClampingMemory(b *testing.B) {
for _, sc := range scenarios {
b.Run(sc.name, func(b *testing.B) {
manager, err := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(b, err)
defer func() {
require.NoError(b, manager.Close(nil))
+45 -47
View File
@@ -32,7 +32,7 @@ func TestPeerACLFiltering(t *testing.T) {
},
}
manager, err := Create(ifaceMock, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(t, err)
require.NotNil(t, manager)
@@ -496,40 +496,32 @@ func TestPeerACLFiltering(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
if tc.ruleAction == fw.ActionDrop {
// add general accept rule for the same IP to test drop rule precedence
rules, err := manager.AddPeerFiltering(
rules, err := manager.AddFilterRule(
nil,
net.ParseIP(tc.ruleIP),
pfx(net.ParseIP(tc.ruleIP)), fw.Network{},
fw.ProtocolALL,
nil,
nil,
fw.ActionAccept,
"",
)
fw.ActionAccept)
require.NoError(t, err)
require.NotEmpty(t, rules)
require.NotNil(t, rules)
t.Cleanup(func() {
for _, rule := range rules {
require.NoError(t, manager.DeletePeerRule(rule))
}
require.NoError(t, manager.DeleteFilterRule(rules))
})
}
rules, err := manager.AddPeerFiltering(
rules, err := manager.AddFilterRule(
nil,
net.ParseIP(tc.ruleIP),
pfx(net.ParseIP(tc.ruleIP)), fw.Network{},
tc.ruleProto,
tc.ruleSrcPort,
tc.ruleDstPort,
tc.ruleAction,
"",
)
tc.ruleAction)
require.NoError(t, err)
require.NotEmpty(t, rules)
require.NotNil(t, rules)
t.Cleanup(func() {
for _, rule := range rules {
require.NoError(t, manager.DeletePeerRule(rule))
}
require.NoError(t, manager.DeleteFilterRule(rules))
})
packet := createTestPacket(t, tc.srcIP, tc.dstIP, tc.proto, tc.srcPort, tc.dstPort)
@@ -557,7 +549,7 @@ func TestPeerACLFilteringIPv6(t *testing.T) {
},
}
manager, err := Create(ifaceMock, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, manager.Close(nil)) })
@@ -652,14 +644,24 @@ func TestPeerACLFilteringIPv6(t *testing.T) {
shouldBeBlocked: false,
},
{
name: "IPv6: v4 wildcard ICMP rule matches ICMPv6 via protoLayerMatches",
name: "IPv6: v4 wildcard ICMP rule does not match ICMPv6",
srcIP: "fd00::1",
dstIP: "fd00::100",
proto: fw.ProtocolICMP,
ruleIP: "0.0.0.0",
ruleProto: fw.ProtocolICMP,
ruleAction: fw.ActionAccept,
shouldBeBlocked: false,
shouldBeBlocked: true,
},
{
name: "IPv4: v6 wildcard ICMP rule does not match ICMPv4",
srcIP: "100.10.0.1",
dstIP: "100.10.0.100",
proto: fw.ProtocolICMP,
ruleIP: "::",
ruleProto: fw.ProtocolICMP,
ruleAction: fw.ActionAccept,
shouldBeBlocked: true,
},
}
@@ -672,22 +674,18 @@ func TestPeerACLFilteringIPv6(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.ruleAction == fw.ActionDrop {
rules, err := manager.AddPeerFiltering(nil, net.ParseIP(tc.ruleIP), fw.ProtocolALL, nil, nil, fw.ActionAccept, "")
rules, err := manager.AddFilterRule(nil, pfx(net.ParseIP(tc.ruleIP)), fw.Network{}, fw.ProtocolALL, nil, nil, fw.ActionAccept)
require.NoError(t, err)
t.Cleanup(func() {
for _, rule := range rules {
require.NoError(t, manager.DeletePeerRule(rule))
}
require.NoError(t, manager.DeleteFilterRule(rules))
})
}
rules, err := manager.AddPeerFiltering(nil, net.ParseIP(tc.ruleIP), tc.ruleProto, nil, tc.ruleDstPort, tc.ruleAction, "")
rules, err := manager.AddFilterRule(nil, pfx(net.ParseIP(tc.ruleIP)), fw.Network{}, tc.ruleProto, nil, tc.ruleDstPort, tc.ruleAction)
require.NoError(t, err)
require.NotEmpty(t, rules)
require.NotNil(t, rules)
t.Cleanup(func() {
for _, rule := range rules {
require.NoError(t, manager.DeletePeerRule(rule))
}
require.NoError(t, manager.DeleteFilterRule(rules))
})
packet := createTestPacket(t, tc.srcIP, tc.dstIP, tc.proto, tc.srcPort, tc.dstPort)
@@ -800,7 +798,7 @@ func setupRoutedManager(tb testing.TB, network string) *Manager {
},
}
manager, err := Create(ifaceMock, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(tb, err)
require.NoError(tb, manager.EnableRouting())
require.NotNil(tb, manager)
@@ -1405,7 +1403,7 @@ func TestRouteACLFiltering(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
if tc.rule.action == fw.ActionDrop {
// add general accept rule to test drop rule
rule, err := manager.AddRouteFiltering(
rule, err := manager.AddFilterRule(
nil,
[]netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
fw.Network{Prefix: netip.MustParsePrefix("0.0.0.0/0")},
@@ -1415,13 +1413,13 @@ func TestRouteACLFiltering(t *testing.T) {
fw.ActionAccept,
)
require.NoError(t, err)
require.NotNil(t, rule)
require.NotEmpty(t, rule)
t.Cleanup(func() {
require.NoError(t, manager.DeleteRouteRule(rule))
require.NoError(t, manager.DeleteFilterRule(rule))
})
}
rule, err := manager.AddRouteFiltering(
rule, err := manager.AddFilterRule(
nil,
tc.rule.sources,
tc.rule.dest,
@@ -1431,10 +1429,10 @@ func TestRouteACLFiltering(t *testing.T) {
tc.rule.action,
)
require.NoError(t, err)
require.NotNil(t, rule)
require.NotEmpty(t, rule)
t.Cleanup(func() {
require.NoError(t, manager.DeleteRouteRule(rule))
require.NoError(t, manager.DeleteFilterRule(rule))
})
srcIP := netip.MustParseAddr(tc.srcIP)
@@ -1602,9 +1600,9 @@ func TestRouteACLOrder(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var rules []fw.Rule
var addedRules []fw.Rule
for _, r := range tc.rules {
rule, err := manager.AddRouteFiltering(
rule, err := manager.AddFilterRule(
nil,
r.sources,
r.dest,
@@ -1615,12 +1613,12 @@ func TestRouteACLOrder(t *testing.T) {
)
require.NoError(t, err)
require.NotNil(t, rule)
rules = append(rules, rule)
addedRules = append(addedRules, rule)
}
t.Cleanup(func() {
for _, rule := range rules {
require.NoError(t, manager.DeleteRouteRule(rule))
for _, rule := range addedRules {
require.NoError(t, manager.DeleteFilterRule(rule))
}
})
@@ -1646,7 +1644,7 @@ func TestRouteACLSet(t *testing.T) {
},
}
manager, err := Create(ifaceMock, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, manager.Close(nil))
@@ -1655,7 +1653,7 @@ func TestRouteACLSet(t *testing.T) {
set := fw.NewDomainSet(domain.List{"example.org"})
// Add rule that uses the set (initially empty)
rule, err := manager.AddRouteFiltering(
rule, err := manager.AddFilterRule(
nil,
[]netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
fw.Network{Set: set},
@@ -1689,7 +1687,7 @@ func TestRouteACLFilteringIPv6(t *testing.T) {
manager := setupRoutedManager(t, "10.10.0.100/16")
v6Dst := netip.MustParsePrefix("fd00:dead:beef::/48")
_, err := manager.AddRouteFiltering(
_, err := manager.AddFilterRule(
nil,
[]netip.Prefix{netip.MustParsePrefix("fd00::/16")},
fw.Network{Prefix: v6Dst},
@@ -1700,7 +1698,7 @@ func TestRouteACLFilteringIPv6(t *testing.T) {
)
require.NoError(t, err)
_, err = manager.AddRouteFiltering(
_, err = manager.AddFilterRule(
nil,
[]netip.Prefix{netip.MustParsePrefix("fd00::/16")},
fw.Network{Prefix: netip.MustParsePrefix("fd00:dead:beef:1::/64")},
@@ -29,7 +29,7 @@ func TestAddRouteFilteringReturnsExistingRule(t *testing.T) {
destination := fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")}
// Add rule first time
rule1, err := manager.AddRouteFiltering(
rule1, err := manager.AddFilterRule(
[]byte("policy-1"),
sources,
destination,
@@ -42,7 +42,7 @@ func TestAddRouteFilteringReturnsExistingRule(t *testing.T) {
require.NotNil(t, rule1)
// Add the same rule again
rule2, err := manager.AddRouteFiltering(
rule2, err := manager.AddFilterRule(
[]byte("policy-1"),
sources,
destination,
@@ -74,7 +74,7 @@ func TestAddRouteFilteringDifferentRulesGetDifferentIDs(t *testing.T) {
sources := []netip.Prefix{netip.MustParsePrefix("100.64.1.0/24")}
// Add first rule
rule1, err := manager.AddRouteFiltering(
rule1, err := manager.AddFilterRule(
[]byte("policy-1"),
sources,
fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
@@ -86,7 +86,7 @@ func TestAddRouteFilteringDifferentRulesGetDifferentIDs(t *testing.T) {
require.NoError(t, err)
// Add different rule (different destination)
rule2, err := manager.AddRouteFiltering(
rule2, err := manager.AddFilterRule(
[]byte("policy-2"),
sources,
fw.Network{Prefix: netip.MustParsePrefix("192.168.2.0/24")}, // Different!
@@ -115,7 +115,7 @@ func TestRouteRuleUpdateDoesNotCauseGap(t *testing.T) {
sources := []netip.Prefix{netip.MustParsePrefix("100.64.1.0/24")}
destination := fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")}
rule1, err := manager.AddRouteFiltering(
rule1, err := manager.AddFilterRule(
[]byte("policy-1"),
sources,
destination,
@@ -132,7 +132,7 @@ func TestRouteRuleUpdateDoesNotCauseGap(t *testing.T) {
require.True(t, pass, "Traffic should pass with rule in place")
// Re-add same rule (simulates network map update)
rule2, err := manager.AddRouteFiltering(
rule2, err := manager.AddFilterRule(
[]byte("policy-1"),
sources,
destination,
@@ -147,7 +147,7 @@ func TestRouteRuleUpdateDoesNotCauseGap(t *testing.T) {
// won't delete rule1 during cleanup. If IDs differed, deleting rule1
// would remove the only matching rule and cause a traffic gap.
if rule1.ID() != rule2.ID() {
err = manager.DeleteRouteRule(rule1)
err = manager.DeleteFilterRule(rule1)
require.NoError(t, err)
}
@@ -156,6 +156,59 @@ func TestRouteRuleUpdateDoesNotCauseGap(t *testing.T) {
"Traffic should still pass after rule update - no gap should occur")
}
// TestBlockInvalidRoutedDualStack verifies that when the interface has an
// IPv6 overlay address, blockInvalidRouted installs a block rule for both
// the v4 and v6 WG prefixes and that routed traffic to the v6 prefix is
// denied. The v4-only soft-skip path is covered by
// TestBlockInvalidRoutedIdempotent, whose mock leaves IPv6Net invalid.
func TestBlockInvalidRoutedDualStack(t *testing.T) {
ctrl := gomock.NewController(t)
dev := mocks.NewMockDevice(ctrl)
dev.EXPECT().MTU().Return(1500, nil).AnyTimes()
wgNet := netip.MustParsePrefix("100.64.0.1/16")
wgNet6 := netip.MustParsePrefix("fd00:1234::1/64")
ifaceMock := &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
AddressFunc: func() wgaddr.Address {
return wgaddr.Address{
IP: wgNet.Addr(),
Network: wgNet,
IPv6: wgNet6.Addr(),
IPv6Net: wgNet6,
}
},
GetDeviceFunc: func() *device.FilteredDevice {
return &device.FilteredDevice{Device: dev}
},
GetWGDeviceFunc: func() *wgdevice.Device {
return &wgdevice.Device{}
},
}
manager, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, manager.Close(nil))
})
rules, err := manager.blockInvalidRouted(ifaceMock)
require.NoError(t, err)
require.Len(t, rules, 2, "dual-stack interface must produce a v4 and a v6 block rule")
manager.mutex.RLock()
ruleCount := len(manager.routeRules)
manager.mutex.RUnlock()
assert.Equal(t, 2, ruleCount, "should have one block rule per family")
// v6 routed traffic to the WG prefix must be denied.
srcIP := netip.MustParseAddr("2001:db8::1")
dstIP := netip.MustParseAddr("fd00:1234::50")
_, pass := manager.routeACLsPass(srcIP, dstIP, layers.LayerTypeTCP, 12345, 80)
assert.False(t, pass, "block rule should deny routed traffic to the v6 WG prefix")
}
// TestBlockInvalidRoutedIdempotent verifies that blockInvalidRouted creates
// exactly one drop rule for the WireGuard network prefix, and calling it again
// returns the same rule without duplicating.
@@ -182,7 +235,7 @@ func TestBlockInvalidRoutedIdempotent(t *testing.T) {
},
}
manager, err := Create(ifaceMock, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, manager.Close(nil))
@@ -245,7 +298,7 @@ func TestBlockRuleNotAccumulatedOnRepeatedEnableRouting(t *testing.T) {
},
}
manager, err := Create(ifaceMock, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, manager.Close(nil))
@@ -274,7 +327,7 @@ func TestRouteRuleCountStableAcrossUpdates(t *testing.T) {
// Simulate 5 network map updates with the same route rule
for i := 0; i < 5; i++ {
rule, err := manager.AddRouteFiltering(
rule, err := manager.AddFilterRule(
[]byte("policy-1"),
sources,
destination,
@@ -304,7 +357,7 @@ func TestDeleteRouteRuleAfterIdempotentAdd(t *testing.T) {
destination := fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")}
// Add same rule twice
rule1, err := manager.AddRouteFiltering(
rule1, err := manager.AddFilterRule(
[]byte("policy-1"),
sources,
destination,
@@ -315,7 +368,7 @@ func TestDeleteRouteRuleAfterIdempotentAdd(t *testing.T) {
)
require.NoError(t, err)
rule2, err := manager.AddRouteFiltering(
rule2, err := manager.AddFilterRule(
[]byte("policy-1"),
sources,
destination,
@@ -329,7 +382,7 @@ func TestDeleteRouteRuleAfterIdempotentAdd(t *testing.T) {
require.Equal(t, rule1.ID(), rule2.ID(), "Should return same rule ID")
// Delete using first reference
err = manager.DeleteRouteRule(rule1)
err = manager.DeleteFilterRule(rule1)
require.NoError(t, err)
// Verify traffic no longer passes
@@ -364,7 +417,7 @@ func setupTestManager(t *testing.T) *Manager {
},
}
manager, err := Create(ifaceMock, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(t, err)
require.NoError(t, manager.EnableRouting())
+81 -110
View File
@@ -78,18 +78,19 @@ func TestManagerCreate(t *testing.T) {
SetFilterFunc: func(device.PacketFilter) error { return nil },
}
m, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
m, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: nbiface.DefaultMTU})
if err != nil {
t.Errorf("failed to create Manager: %v", err)
return
}
t.Cleanup(func() { require.NoError(t, m.Close(nil)) })
if m == nil {
t.Error("Manager is nil")
}
}
func TestManagerAddPeerFiltering(t *testing.T) {
func TestManagerAddFilterRule(t *testing.T) {
isSetFilterCalled := false
ifaceMock := &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error {
@@ -98,18 +99,19 @@ func TestManagerAddPeerFiltering(t *testing.T) {
},
}
m, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
m, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: nbiface.DefaultMTU})
if err != nil {
t.Errorf("failed to create Manager: %v", err)
return
}
t.Cleanup(func() { require.NoError(t, m.Close(nil)) })
ip := net.ParseIP("192.168.1.1")
proto := fw.ProtocolTCP
port := &fw.Port{Values: []uint16{80}}
action := fw.ActionDrop
rule, err := m.AddPeerFiltering(nil, ip, proto, nil, port, action, "")
rule, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, proto, nil, port, action)
if err != nil {
t.Errorf("failed to add filtering: %v", err)
return
@@ -131,74 +133,47 @@ func TestManagerDeleteRule(t *testing.T) {
SetFilterFunc: func(device.PacketFilter) error { return nil },
}
m, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
m, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: nbiface.DefaultMTU})
if err != nil {
t.Errorf("failed to create Manager: %v", err)
return
}
t.Cleanup(func() { require.NoError(t, m.Close(nil)) })
ip := netip.MustParseAddr("192.168.1.1")
proto := fw.ProtocolTCP
port := &fw.Port{Values: []uint16{80}}
action := fw.ActionDrop
rule2, err := m.AddPeerFiltering(nil, ip.AsSlice(), proto, nil, port, action, "")
rule2, err := m.AddFilterRule(nil, pfx(ip.AsSlice()), fw.Network{}, proto, nil, port, action)
if err != nil {
t.Errorf("failed to add filtering: %v", err)
return
}
// Check rules exist in appropriate maps
for _, r := range rule2 {
peerRule, ok := r.(*PeerRule)
if !ok {
t.Errorf("rule should be a PeerRule")
continue
}
// Check if rule exists in deny or allow maps based on action
var found bool
if peerRule.drop {
_, found = m.incomingDenyRules[ip][r.ID()]
} else {
_, found = m.incomingRules[ip][r.ID()]
}
if !found {
t.Errorf("rule2 is not in the expected rules map")
peerRule, ok := rule2.(*PeerRule)
require.True(t, ok, "rule should be a peer rule")
inMap := func() bool {
if peerRule.action == fw.ActionDrop {
return findRuleByID(m.incomingDenyRules, ip, rule2.ID())
}
return findRuleByID(m.incomingAcceptRules, ip, rule2.ID())
}
for _, r := range rule2 {
err = m.DeletePeerRule(r)
if err != nil {
t.Errorf("failed to delete rule: %v", err)
return
}
}
require.True(t, inMap(), "rule2 should be in the expected rules list")
// Check rules are removed from appropriate maps
for _, r := range rule2 {
peerRule, ok := r.(*PeerRule)
if !ok {
t.Errorf("rule should be a PeerRule")
continue
}
// Check if rule is removed from deny or allow maps based on action
var found bool
if peerRule.drop {
_, found = m.incomingDenyRules[ip][r.ID()]
} else {
_, found = m.incomingRules[ip][r.ID()]
}
if found {
t.Errorf("rule2 should be removed from the rules map")
}
}
require.NoError(t, m.DeleteFilterRule(rule2), "failed to delete rule")
require.False(t, inMap(), "rule2 should be removed from the rules list")
}
func TestSetUDPPacketHook(t *testing.T) {
manager, err := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, nbiface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: nbiface.DefaultMTU})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, manager.Close(nil)) })
@@ -220,9 +195,11 @@ func TestSetUDPPacketHook(t *testing.T) {
}
func TestSetTCPPacketHook(t *testing.T) {
manager, err := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, nbiface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: nbiface.DefaultMTU})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, manager.Close(nil)) })
@@ -250,7 +227,7 @@ func TestPeerRuleLifecycleDenyRules(t *testing.T) {
SetFilterFunc: func(device.PacketFilter) error { return nil },
}
m, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
m, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: nbiface.DefaultMTU})
require.NoError(t, err)
defer func() {
require.NoError(t, m.Close(nil))
@@ -260,36 +237,34 @@ func TestPeerRuleLifecycleDenyRules(t *testing.T) {
addr := netip.MustParseAddr("192.168.1.1")
// Add multiple deny rules for different ports
rule1, err := m.AddPeerFiltering(nil, ip, fw.ProtocolTCP, nil,
&fw.Port{Values: []uint16{22}}, fw.ActionDrop, "")
rule1, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{22}}, fw.ActionDrop)
require.NoError(t, err)
rule2, err := m.AddPeerFiltering(nil, ip, fw.ProtocolTCP, nil,
&fw.Port{Values: []uint16{80}}, fw.ActionDrop, "")
rule2, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionDrop)
require.NoError(t, err)
m.mutex.RLock()
denyCount := len(m.incomingDenyRules[addr])
denyCount := countRulesForAddr(m.incomingDenyRules, addr)
m.mutex.RUnlock()
require.Equal(t, 2, denyCount, "Should have exactly 2 deny rules")
// Delete the first deny rule
err = m.DeletePeerRule(rule1[0])
err = m.DeleteFilterRule(rule1)
require.NoError(t, err)
m.mutex.RLock()
denyCount = len(m.incomingDenyRules[addr])
denyCount = countRulesForAddr(m.incomingDenyRules, addr)
m.mutex.RUnlock()
require.Equal(t, 1, denyCount, "Should have 1 deny rule after deleting first")
// Delete the second deny rule
err = m.DeletePeerRule(rule2[0])
err = m.DeleteFilterRule(rule2)
require.NoError(t, err)
m.mutex.RLock()
_, exists := m.incomingDenyRules[addr]
exists := countRulesForAddr(m.incomingDenyRules, addr) > 0
m.mutex.RUnlock()
require.False(t, exists, "Deny rules IP entry should be cleaned up when empty")
require.False(t, exists, "Deny rules should be cleaned up when empty")
}
// TestPeerRuleAddAndDeleteDontLeak verifies that repeatedly adding and deleting
@@ -299,7 +274,7 @@ func TestPeerRuleAddAndDeleteDontLeak(t *testing.T) {
SetFilterFunc: func(device.PacketFilter) error { return nil },
}
m, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
m, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: nbiface.DefaultMTU})
require.NoError(t, err)
defer func() {
require.NoError(t, m.Close(nil))
@@ -311,27 +286,21 @@ func TestPeerRuleAddAndDeleteDontLeak(t *testing.T) {
// Simulate 10 network map updates: add rule, delete old, add new
for i := 0; i < 10; i++ {
// Add a deny rule
rules, err := m.AddPeerFiltering(nil, ip, fw.ProtocolTCP, nil,
&fw.Port{Values: []uint16{22}}, fw.ActionDrop, "")
rules, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{22}}, fw.ActionDrop)
require.NoError(t, err)
// Add an allow rule
allowRules, err := m.AddPeerFiltering(nil, ip, fw.ProtocolTCP, nil,
&fw.Port{Values: []uint16{80}}, fw.ActionAccept, "")
allowRules, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept)
require.NoError(t, err)
// Delete them (simulating ACL manager cleanup)
for _, r := range rules {
require.NoError(t, m.DeletePeerRule(r))
}
for _, r := range allowRules {
require.NoError(t, m.DeletePeerRule(r))
}
require.NoError(t, m.DeleteFilterRule(rules))
require.NoError(t, m.DeleteFilterRule(allowRules))
}
m.mutex.RLock()
denyCount := len(m.incomingDenyRules[addr])
allowCount := len(m.incomingRules[addr])
denyCount := countRulesForAddr(m.incomingDenyRules, addr)
allowCount := countRulesForAddr(m.incomingAcceptRules, addr)
m.mutex.RUnlock()
require.Equal(t, 0, denyCount, "No deny rules should remain after cleanup")
@@ -345,7 +314,7 @@ func TestMixedAllowDenyRulesSameIP(t *testing.T) {
SetFilterFunc: func(device.PacketFilter) error { return nil },
}
m, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
m, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: nbiface.DefaultMTU})
require.NoError(t, err)
defer func() {
require.NoError(t, m.Close(nil))
@@ -354,41 +323,39 @@ func TestMixedAllowDenyRulesSameIP(t *testing.T) {
ip := net.ParseIP("192.168.1.1")
// Add allow rule for port 80
allowRule, err := m.AddPeerFiltering(nil, ip, fw.ProtocolTCP, nil,
&fw.Port{Values: []uint16{80}}, fw.ActionAccept, "")
allowRule, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept)
require.NoError(t, err)
// Add deny rule for port 22
denyRule, err := m.AddPeerFiltering(nil, ip, fw.ProtocolTCP, nil,
&fw.Port{Values: []uint16{22}}, fw.ActionDrop, "")
denyRule, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{22}}, fw.ActionDrop)
require.NoError(t, err)
addr := netip.MustParseAddr("192.168.1.1")
m.mutex.RLock()
allowCount := len(m.incomingRules[addr])
denyCount := len(m.incomingDenyRules[addr])
allowCount := countRulesForAddr(m.incomingAcceptRules, addr)
denyCount := countRulesForAddr(m.incomingDenyRules, addr)
m.mutex.RUnlock()
require.Equal(t, 1, allowCount, "Should have 1 allow rule")
require.Equal(t, 1, denyCount, "Should have 1 deny rule")
// Delete allow rule should not affect deny rule
err = m.DeletePeerRule(allowRule[0])
err = m.DeleteFilterRule(allowRule)
require.NoError(t, err)
m.mutex.RLock()
denyCountAfter := len(m.incomingDenyRules[addr])
denyCountAfter := countRulesForAddr(m.incomingDenyRules, addr)
m.mutex.RUnlock()
require.Equal(t, 1, denyCountAfter, "Deny rule should still exist after deleting allow rule")
// Delete deny rule
err = m.DeletePeerRule(denyRule[0])
err = m.DeleteFilterRule(denyRule)
require.NoError(t, err)
m.mutex.RLock()
_, denyExists := m.incomingDenyRules[addr]
_, allowExists := m.incomingRules[addr]
denyExists := countRulesForAddr(m.incomingDenyRules, addr) > 0
allowExists := countRulesForAddr(m.incomingAcceptRules, addr) > 0
m.mutex.RUnlock()
require.False(t, denyExists, "Deny rules should be empty")
@@ -400,7 +367,7 @@ func TestManagerReset(t *testing.T) {
SetFilterFunc: func(device.PacketFilter) error { return nil },
}
m, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
m, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: nbiface.DefaultMTU})
if err != nil {
t.Errorf("failed to create Manager: %v", err)
return
@@ -411,7 +378,7 @@ func TestManagerReset(t *testing.T) {
port := &fw.Port{Values: []uint16{80}}
action := fw.ActionDrop
_, err = m.AddPeerFiltering(nil, ip, proto, nil, port, action, "")
_, err = m.AddFilterRule(nil, pfx(ip), fw.Network{}, proto, nil, port, action)
if err != nil {
t.Errorf("failed to add filtering: %v", err)
return
@@ -423,7 +390,7 @@ func TestManagerReset(t *testing.T) {
return
}
if len(m.outgoingRules) != 0 || len(m.incomingRules) != 0 || len(m.incomingDenyRules) != 0 {
if len(m.incomingAcceptRules) != 0 || len(m.incomingDenyRules) != 0 {
t.Errorf("rules are not empty")
}
}
@@ -439,7 +406,7 @@ func TestNotMatchByIP(t *testing.T) {
},
}
m, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
m, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: nbiface.DefaultMTU})
if err != nil {
t.Errorf("failed to create Manager: %v", err)
return
@@ -449,7 +416,7 @@ func TestNotMatchByIP(t *testing.T) {
proto := fw.ProtocolUDP
action := fw.ActionAccept
_, err = m.AddPeerFiltering(nil, ip, proto, nil, nil, action, "")
_, err = m.AddFilterRule(nil, pfx(ip), fw.Network{}, proto, nil, nil, action)
if err != nil {
t.Errorf("failed to add filtering: %v", err)
return
@@ -502,7 +469,7 @@ func TestRemovePacketHook(t *testing.T) {
}
// creating manager instance
manager, err := Create(iface, false, flowLogger, nbiface.DefaultMTU)
manager, err := Create(Config{IFace: iface, FlowLogger: flowLogger, MTU: nbiface.DefaultMTU})
if err != nil {
t.Fatalf("Failed to create Manager: %s", err)
}
@@ -519,9 +486,11 @@ func TestRemovePacketHook(t *testing.T) {
}
func TestProcessOutgoingHooks(t *testing.T) {
manager, err := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, nbiface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: nbiface.DefaultMTU})
require.NoError(t, err)
manager.udpTracker.Close()
@@ -606,7 +575,7 @@ func TestUSPFilterCreatePerformance(t *testing.T) {
ifaceMock := &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}
manager, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
manager, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: nbiface.DefaultMTU})
require.NoError(t, err)
time.Sleep(time.Second)
@@ -621,7 +590,7 @@ func TestUSPFilterCreatePerformance(t *testing.T) {
start := time.Now()
for i := 0; i < testMax; i++ {
port := &fw.Port{Values: []uint16{uint16(1000 + i)}}
_, err = manager.AddPeerFiltering(nil, ip, "tcp", nil, port, fw.ActionAccept, "")
_, err = manager.AddFilterRule(nil, pfx(ip), fw.Network{}, "tcp", nil, port, fw.ActionAccept)
require.NoError(t, err, "failed to add rule")
}
@@ -631,9 +600,11 @@ func TestUSPFilterCreatePerformance(t *testing.T) {
}
func TestStatefulFirewall_UDPTracking(t *testing.T) {
manager, err := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, nbiface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: nbiface.DefaultMTU})
require.NoError(t, err)
manager.udpTracker.Close() // Close the existing tracker
@@ -845,7 +816,7 @@ func TestUpdateSetMerge(t *testing.T) {
SetFilterFunc: func(device.PacketFilter) error { return nil },
}
manager, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
manager, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: nbiface.DefaultMTU})
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, manager.Close(nil))
@@ -858,7 +829,7 @@ func TestUpdateSetMerge(t *testing.T) {
netip.MustParsePrefix("192.168.1.0/24"),
}
rule, err := manager.AddRouteFiltering(
rule, err := manager.AddFilterRule(
nil,
[]netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
fw.Network{Set: set},
@@ -931,7 +902,7 @@ func TestUpdateSetDeduplication(t *testing.T) {
SetFilterFunc: func(device.PacketFilter) error { return nil },
}
manager, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
manager, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: nbiface.DefaultMTU})
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, manager.Close(nil))
@@ -939,7 +910,7 @@ func TestUpdateSetDeduplication(t *testing.T) {
set := fw.NewDomainSet(domain.List{"example.org"})
rule, err := manager.AddRouteFiltering(
rule, err := manager.AddFilterRule(
nil,
[]netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
fw.Network{Set: set},
@@ -1051,7 +1022,7 @@ func TestMSSClamping(t *testing.T) {
},
}
manager, err := Create(ifaceMock, false, flowLogger, 1280)
manager, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: 1280})
require.NoError(t, err)
defer func() {
require.NoError(t, manager.Close(nil))
@@ -1243,7 +1214,7 @@ func TestShouldForward(t *testing.T) {
return wgaddr.Address{IP: wgIP, Network: netip.PrefixFrom(wgIP, 24)}
}
manager, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
manager, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: nbiface.DefaultMTU})
require.NoError(t, err)
defer func() {
require.NoError(t, manager.Close(nil))
@@ -1358,7 +1329,7 @@ func TestShouldForward(t *testing.T) {
// Re-create manager to pick up the new address with IPv6
require.NoError(t, manager.Close(nil))
manager, err = Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
manager, err = Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: nbiface.DefaultMTU})
require.NoError(t, err)
v6Cases := []struct {
@@ -12,6 +12,7 @@ import (
"time"
log "github.com/sirupsen/logrus"
wgdevice "golang.zx2c4.com/wireguard/device"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
@@ -22,9 +23,9 @@ import (
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
"github.com/netbirdio/netbird/client/firewall/uspfilter/common"
"github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log"
"github.com/netbirdio/netbird/client/iface/wgaddr"
nftypes "github.com/netbirdio/netbird/client/internal/netflow/types"
)
@@ -40,6 +41,12 @@ const (
envForceTCPRACK = "NB_FORCE_TCP_RACK"
)
// IFace provides the WireGuard device and overlay addresses the forwarder needs.
type IFace interface {
GetWGDevice() *wgdevice.Device
Address() wgaddr.Address
}
type Forwarder struct {
logger *nblog.Logger
flowLogger nftypes.FlowLogger
@@ -58,7 +65,7 @@ type Forwarder struct {
pingSemaphore chan struct{}
}
func New(iface common.IFaceMapper, logger *nblog.Logger, flowLogger nftypes.FlowLogger, netstack bool, mtu uint16) (*Forwarder, error) {
func New(iface IFace, logger *nblog.Logger, flowLogger nftypes.FlowLogger, netstack bool, mtu uint16) (*Forwarder, error) {
s := stack.New(stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{
ipv4.NewProtocol,
+14 -14
View File
@@ -39,7 +39,7 @@ func newFragmentTestManager(tb testing.TB) *Manager {
},
}
m, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
m, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: nbiface.DefaultMTU})
require.NoError(tb, err)
require.NoError(tb, m.UpdateLocalIPs())
tb.Cleanup(func() { require.NoError(tb, m.Close(nil)) })
@@ -175,8 +175,8 @@ func normalUDPPacket(tb testing.TB, dstPort uint16, payloadLen int) []byte {
func allowUDP(tb testing.TB, m *Manager, dstPort uint16) {
tb.Helper()
_, err := m.AddPeerFiltering(nil, net.ParseIP(fragTestSrc), fw.ProtocolUDP, nil,
&fw.Port{Values: []uint16{dstPort}}, fw.ActionAccept, "")
_, err := m.AddFilterRule(nil, pfx(net.ParseIP(fragTestSrc)), fw.Network{}, fw.ProtocolUDP, nil,
&fw.Port{Values: []uint16{dstPort}}, fw.ActionAccept)
require.NoError(tb, err)
}
@@ -228,8 +228,8 @@ func TestFragment_DeniedFirstDropsTrailing(t *testing.T) {
// the overlap lands on real header bytes (the flags at byte 13).
func TestFragment_OverlappingHeaderDropped(t *testing.T) {
m := newFragmentTestManager(t)
_, err := m.AddPeerFiltering(nil, net.ParseIP(fragTestSrc), fw.ProtocolTCP, nil,
&fw.Port{Values: []uint16{8080}}, fw.ActionAccept, "")
_, err := m.AddFilterRule(nil, pfx(net.ParseIP(fragTestSrc)), fw.Network{}, fw.ProtocolTCP, nil,
&fw.Port{Values: []uint16{8080}}, fw.ActionAccept)
require.NoError(t, err)
// First fragment: TCP header (20) + 12 data = 32 bytes -> headerEnd = 4 octets.
@@ -290,8 +290,8 @@ func TestFragment_TinyFirstDropped(t *testing.T) {
// trailing fragments inherit the verdict.
func TestFragment_TCPFirstFragment(t *testing.T) {
m := newFragmentTestManager(t)
_, err := m.AddPeerFiltering(nil, net.ParseIP(fragTestSrc), fw.ProtocolTCP, nil,
&fw.Port{Values: []uint16{8080}}, fw.ActionAccept, "")
_, err := m.AddFilterRule(nil, pfx(net.ParseIP(fragTestSrc)), fw.Network{}, fw.ProtocolTCP, nil,
&fw.Port{Values: []uint16{8080}}, fw.ActionAccept)
require.NoError(t, err)
// TCP header (20) + 12 data = 32 bytes -> headerEnd = 4 octets.
@@ -308,8 +308,8 @@ func TestFragment_TCPFirstFragment(t *testing.T) {
// bytes would satisfy a UDP header but falls short of the 20-byte TCP header.
func TestFragment_TCPTinyFirstDropped(t *testing.T) {
m := newFragmentTestManager(t)
_, err := m.AddPeerFiltering(nil, net.ParseIP(fragTestSrc), fw.ProtocolTCP, nil,
&fw.Port{Values: []uint16{8080}}, fw.ActionAccept, "")
_, err := m.AddFilterRule(nil, pfx(net.ParseIP(fragTestSrc)), fw.Network{}, fw.ProtocolTCP, nil,
&fw.Port{Values: []uint16{8080}}, fw.ActionAccept)
require.NoError(t, err)
tiny := trailingFragmentTo(t, fragTestDst, layers.IPProtocolTCP, 0x7777, 0, true, 12)
@@ -353,7 +353,7 @@ func TestFragment_RouteACL(t *testing.T) {
m.routingEnabled.Store(true)
m.nativeRouter.Store(false)
_, err := m.AddRouteFiltering(
_, err := m.AddFilterRule(
[]byte("rt-1"),
[]netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
fw.Network{Prefix: netip.MustParsePrefix("198.51.100.0/24")},
@@ -511,8 +511,8 @@ func TestFragmentV6_TrailingWithoutFirstDropped(t *testing.T) {
// through.
func TestFragmentV6_AllowedFirstPassesTrailing(t *testing.T) {
m := newFragmentTestManager(t)
_, err := m.AddPeerFiltering(nil, net.ParseIP(fragTestSrcV6), fw.ProtocolUDP, nil,
&fw.Port{Values: []uint16{8080}}, fw.ActionAccept, "")
_, err := m.AddFilterRule(nil, pfx(net.ParseIP(fragTestSrcV6)), fw.Network{}, fw.ProtocolUDP, nil,
&fw.Port{Values: []uint16{8080}}, fw.ActionAccept)
require.NoError(t, err)
// First fragment: UDP header (8) + 32 data = 40 octets -> headerEnd = 5.
@@ -531,8 +531,8 @@ func TestFragmentV6_AllowedFirstPassesTrailing(t *testing.T) {
// exhaust the verdict table.
func TestFragmentV6_AtomicNotCached(t *testing.T) {
m := newFragmentTestManager(t)
_, err := m.AddPeerFiltering(nil, net.ParseIP(fragTestSrcV6), fw.ProtocolUDP, nil,
&fw.Port{Values: []uint16{8080}}, fw.ActionAccept, "")
_, err := m.AddFilterRule(nil, pfx(net.ParseIP(fragTestSrcV6)), fw.Network{}, fw.ProtocolUDP, nil,
&fw.Port{Values: []uint16{8080}}, fw.ActionAccept)
require.NoError(t, err)
atomic := fragmentUDPv6(t, 0xA70301C, 8080, 16, false)
@@ -9,7 +9,6 @@ import (
log "github.com/sirupsen/logrus"
nberrors "github.com/netbirdio/netbird/client/errors"
"github.com/netbirdio/netbird/client/internal/statemanager"
)
type action string
@@ -20,35 +19,20 @@ const (
firewallRuleName = "Netbird"
)
// Close cleans up the firewall manager by removing all rules and closing trackers
func (m *Manager) Close(*statemanager.Manager) error {
m.mutex.Lock()
defer m.mutex.Unlock()
m.resetState()
if !isWindowsFirewallReachable() {
return nil
}
var merr *multierror.Error
if isFirewallRuleActive(firewallRuleName) {
if err := manageFirewallRule(firewallRuleName, deleteRule); err != nil {
merr = multierror.Append(merr, fmt.Errorf("remove windows firewall rule: %w", err))
}
}
if isFirewallRuleActive(firewallRuleName + "-v6") {
if err := manageFirewallRule(firewallRuleName+"-v6", deleteRule); err != nil {
merr = multierror.Append(merr, fmt.Errorf("remove windows v6 firewall rule: %w", err))
}
}
return nberrors.FormatErrorOrNil(merr)
// WindowsInterfaceAllower opens the NetBird interface in the Windows firewall
// via netsh advfirewall rules. It implements InterfaceAllower for the userspace
// firewall on Windows.
type WindowsInterfaceAllower struct {
iface Iface
}
// AllowNetbird allows netbird interface traffic
func (m *Manager) AllowNetbird() error {
// NewWindowsInterfaceAllower builds the Windows netsh-based interface allower.
func NewWindowsInterfaceAllower(iface Iface) *WindowsInterfaceAllower {
return &WindowsInterfaceAllower{iface: iface}
}
// Apply adds inbound-allow netsh rules for the interface's addresses.
func (a *WindowsInterfaceAllower) Apply() error {
if !isWindowsFirewallReachable() {
return nil
}
@@ -60,13 +44,13 @@ func (m *Manager) AllowNetbird() error {
"enable=yes",
"action=allow",
"profile=any",
"localip="+m.wgIface.Address().IP.String(),
"localip="+a.iface.Address().IP.String(),
); err != nil {
return err
}
}
if v6 := m.wgIface.Address().IPv6; v6.IsValid() && !isFirewallRuleActive(firewallRuleName+"-v6") {
if v6 := a.iface.Address().IPv6; v6.IsValid() && !isFirewallRuleActive(firewallRuleName+"-v6") {
if err := manageFirewallRule(firewallRuleName+"-v6",
addRule,
"dir=in",
@@ -82,8 +66,27 @@ func (m *Manager) AllowNetbird() error {
return nil
}
func manageFirewallRule(ruleName string, action action, extraArgs ...string) error {
// Close removes the netsh rules added by Apply.
func (a *WindowsInterfaceAllower) Close() error {
if !isWindowsFirewallReachable() {
return nil
}
var merr *multierror.Error
if isFirewallRuleActive(firewallRuleName) {
if err := manageFirewallRule(firewallRuleName, deleteRule); err != nil {
merr = multierror.Append(merr, fmt.Errorf("remove windows firewall rule: %w", err))
}
}
if isFirewallRuleActive(firewallRuleName + "-v6") {
if err := manageFirewallRule(firewallRuleName+"-v6", deleteRule); err != nil {
merr = multierror.Append(merr, fmt.Errorf("remove windows v6 firewall rule: %w", err))
}
}
return nberrors.FormatErrorOrNil(merr)
}
func manageFirewallRule(ruleName string, action action, extraArgs ...string) error {
args := []string{"advfirewall", "firewall", string(action), "rule", "name=" + ruleName}
if action == addRule {
args = append(args, extraArgs...)
+1 -3
View File
@@ -7,8 +7,6 @@ import (
"sync/atomic"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/firewall/uspfilter/common"
)
// localIPSnapshot is an immutable snapshot of local IP addresses, swapped
@@ -60,7 +58,7 @@ func processInterface(iface net.Interface, ips map[netip.Addr]struct{}, addresse
}
// UpdateLocalIPs rebuilds the local IP snapshot and swaps it in atomically.
func (m *localIPManager) UpdateLocalIPs(iface common.IFaceMapper) (err error) {
func (m *localIPManager) UpdateLocalIPs(iface Iface) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
+12 -23
View File
@@ -487,19 +487,13 @@ func incrementalUpdate(oldChecksum uint16, oldBytes, newBytes []byte) uint16 {
}
// AddDNATRule adds outbound DNAT rule for forwarding external traffic to NetBird network.
func (m *Manager) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
if m.nativeFirewall == nil {
return nil, errNatNotSupported
}
return m.nativeFirewall.AddDNATRule(rule)
func (m *Manager) AddDNATRule(firewall.ForwardRule) (firewall.Rule, error) {
return nil, errNotSupported
}
// DeleteDNATRule deletes outbound DNAT rule.
func (m *Manager) DeleteDNATRule(rule firewall.Rule) error {
if m.nativeFirewall == nil {
return errNatNotSupported
}
return m.nativeFirewall.DeleteDNATRule(rule)
func (m *Manager) DeleteDNATRule(firewall.Rule) error {
return errNotSupported
}
// addPortRedirection adds a port redirection rule.
@@ -521,7 +515,6 @@ func (m *Manager) addPortRedirection(targetIP netip.Addr, protocol gopacket.Laye
}
// AddInboundDNAT adds an inbound DNAT rule redirecting traffic from NetBird peers to local services.
// TODO: also delegate to nativeFirewall when available for kernel WG mode
func (m *Manager) AddInboundDNAT(localAddr netip.Addr, protocol firewall.Protocol, originalPort, translatedPort uint16) error {
var layerType gopacket.LayerType
switch protocol {
@@ -567,20 +560,16 @@ func (m *Manager) RemoveInboundDNAT(localAddr netip.Addr, protocol firewall.Prot
return m.removePortRedirection(localAddr, layerType, originalPort, translatedPort)
}
// AddOutputDNAT delegates to the native firewall if available.
func (m *Manager) AddOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, originalPort, translatedPort uint16) error {
if m.nativeFirewall == nil {
return fmt.Errorf("output DNAT not supported without native firewall")
}
return m.nativeFirewall.AddOutputDNAT(localAddr, protocol, originalPort, translatedPort)
// AddOutputDNAT is not supported by the userspace firewall: it backs kernel DNS
// redirection, but userspace DNS is served in-process on the gVisor netstack, so
// this should never be called.
func (m *Manager) AddOutputDNAT(netip.Addr, firewall.Protocol, uint16, uint16) error {
return errNotSupported
}
// RemoveOutputDNAT delegates to the native firewall if available.
func (m *Manager) RemoveOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, originalPort, translatedPort uint16) error {
if m.nativeFirewall == nil {
return nil
}
return m.nativeFirewall.RemoveOutputDNAT(localAddr, protocol, originalPort, translatedPort)
// RemoveOutputDNAT is a no-op for the userspace firewall (see AddOutputDNAT).
func (m *Manager) RemoveOutputDNAT(netip.Addr, firewall.Protocol, uint16, uint16) error {
return nil
}
// translateInboundPortDNAT applies port-specific DNAT translation to inbound packets.
+25 -15
View File
@@ -64,9 +64,11 @@ func BenchmarkDNATTranslation(b *testing.B) {
for _, sc := range scenarios {
b.Run(sc.name, func(b *testing.B) {
manager, err := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(b, err)
defer func() {
require.NoError(b, manager.Close(nil))
@@ -124,9 +126,11 @@ func BenchmarkDNATTranslation(b *testing.B) {
// BenchmarkDNATConcurrency tests DNAT performance under concurrent load
func BenchmarkDNATConcurrency(b *testing.B) {
manager, err := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(b, err)
defer func() {
require.NoError(b, manager.Close(nil))
@@ -196,9 +200,11 @@ func BenchmarkDNATScaling(b *testing.B) {
for _, count := range mappingCounts {
b.Run(fmt.Sprintf("mappings_%d", count), func(b *testing.B) {
manager, err := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(b, err)
defer func() {
require.NoError(b, manager.Close(nil))
@@ -308,9 +314,11 @@ func BenchmarkChecksumUpdate(b *testing.B) {
// BenchmarkDNATMemoryAllocations checks for memory allocations in DNAT operations
func BenchmarkDNATMemoryAllocations(b *testing.B) {
manager, err := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(b, err)
defer func() {
require.NoError(b, manager.Close(nil))
@@ -481,9 +489,11 @@ func BenchmarkPortDNAT(b *testing.B) {
for _, sc := range scenarios {
b.Run(sc.name, func(b *testing.B) {
manager, err := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(b, err)
defer func() {
require.NoError(b, manager.Close(nil))
+10 -6
View File
@@ -13,9 +13,11 @@ import (
// TestPortDNATBasic tests basic port DNAT functionality
func TestPortDNATBasic(t *testing.T) {
manager, err := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(t, err)
defer func() {
require.NoError(t, manager.Close(nil))
@@ -49,9 +51,11 @@ func TestPortDNATBasic(t *testing.T) {
// TestPortDNATMultipleRules tests multiple port DNAT rules
func TestPortDNATMultipleRules(t *testing.T) {
manager, err := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(t, err)
defer func() {
require.NoError(t, manager.Close(nil))
+20 -12
View File
@@ -15,9 +15,11 @@ import (
// TestDNATTranslationCorrectness verifies DNAT translation works correctly
func TestDNATTranslationCorrectness(t *testing.T) {
manager, err := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(t, err)
defer func() {
require.NoError(t, manager.Close(nil))
@@ -104,9 +106,11 @@ func parsePacket(t testing.TB, packetData []byte) *decoder {
// TestDNATMappingManagement tests adding/removing DNAT mappings
func TestDNATMappingManagement(t *testing.T) {
manager, err := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(t, err)
defer func() {
require.NoError(t, manager.Close(nil))
@@ -152,9 +156,11 @@ func TestDNATMappingManagement(t *testing.T) {
}
func TestInboundPortDNAT(t *testing.T) {
manager, err := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(t, err)
defer func() {
require.NoError(t, manager.Close(nil))
@@ -202,9 +208,11 @@ func TestInboundPortDNAT(t *testing.T) {
}
func TestInboundPortDNATNegative(t *testing.T) {
manager, err := Create(&IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}, false, flowLogger, iface.DefaultMTU)
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(t, err)
defer func() {
require.NoError(t, manager.Close(nil))
@@ -0,0 +1,333 @@
//go:build uspbench
package uspfilter
import (
"fmt"
"io"
"math/rand"
"net"
"net/netip"
"runtime"
"testing"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
fw "github.com/netbirdio/netbird/client/firewall/manager"
"github.com/netbirdio/netbird/client/iface"
"github.com/netbirdio/netbird/client/iface/device"
"github.com/netbirdio/netbird/client/iface/wgaddr"
)
// BenchmarkPeerACLMatch measures the per-packet cost of the peer ACL
// matcher (peerACLsBlock) across realistic shapes: M distinct policy
// rules, each with K source peers in its set.
//
// With the reverse-source index, miss cost is independent of M and
// hit cost grows only with the number of rules touching a single
// srcIP, not with total rule count.
func BenchmarkPeerACLMatch(b *testing.B) {
shapes := []struct{ M, K int }{
{1, 100}, {10, 100}, {50, 100}, {100, 100}, {100, 1000},
}
families := []struct {
name string
v6 bool
}{{"v4", false}, {"v6", true}}
for _, fam := range families {
for _, s := range shapes {
b.Run(fmt.Sprintf("%s/M=%d/K=%d/hit", fam.name, s.M, s.K), func(b *testing.B) {
runPeerACLBench(b, s.M, s.K, true, fam.v6)
})
b.Run(fmt.Sprintf("%s/M=%d/K=%d/miss", fam.name, s.M, s.K), func(b *testing.B) {
runPeerACLBench(b, s.M, s.K, false, fam.v6)
})
}
}
}
func runPeerACLBench(b *testing.B, m, k int, hit, v6 bool) {
log.SetOutput(io.Discard) // keep manager logs out of the benchmark output
// Miss packets are dropped, so they always traverse the full peer
// ACL matcher (every bucket) without short-circuiting and without
// touching conntrack. Disable conntrack for the miss case so it
// measures the matcher, not established-state lookups. The hit case
// keeps conntrack on: an accepted packet reaches trackInbound, which
// needs the trackers conntrack creates.
if !hit {
b.Setenv("NB_DISABLE_CONNTRACK", "1")
}
bits := 32
genPkt := generatePacket
addrs := uniqueAddrs
if v6 {
bits = 128
genPkt = generatePacket6
addrs = uniqueAddrs6
}
// dstIP must be a local IP so filterInbound takes the local-traffic
// path (handleLocalTraffic → peerACLsBlock) we are measuring; an
// address the manager doesn't own would be treated as routed and
// short-circuit before the peer matcher.
dstIP := addrs(1, 2)[0]
mockAddr := wgaddr.Address{IP: dstIP, Network: netip.PrefixFrom(dstIP, bits)}
if v6 {
// The local-IP manager needs a valid v4 address too; expose the v6
// dst as the interface's IPv6 so IsLocalIP recognizes it.
mockAddr = wgaddr.Address{
IP: netip.MustParseAddr("100.64.0.1"),
Network: netip.MustParsePrefix("100.64.0.0/16"),
IPv6: dstIP,
IPv6Net: netip.PrefixFrom(dstIP, bits),
}
}
manager, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
AddressFunc: func() wgaddr.Address { return mockAddr },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(b, err)
b.Cleanup(func() { require.NoError(b, manager.Close(nil)) })
// Generate M policies × K source peers, all distinct.
all := addrs(m*k, 1)
for i := 0; i < m; i++ {
sources := make([]netip.Prefix, k)
for j, a := range all[i*k : (i+1)*k] {
sources[j] = netip.PrefixFrom(a, bits)
}
_, err := manager.AddFilterRule(
nil, sources, fw.Network{}, fw.ProtocolTCP, nil,
&fw.Port{Values: []uint16{uint16(80 + i)}},
fw.ActionAccept)
require.NoError(b, err)
}
// Hit: cycle through real sources, picking the matching policy's port.
// Miss: a source from a disjoint range, port 80 (matches no policy).
var pktFn func(i int) []byte
if hit {
pktFn = func(i int) []byte {
policy := i % m
src := all[policy*k+(i%k)]
return genPkt(b, src.AsSlice(), dstIP.AsSlice(),
uint16(1024+i%60000), uint16(80+policy), layers.IPProtocolTCP)
}
} else {
miss := addrs(4096, 99)
pktFn = func(i int) []byte {
return genPkt(b, miss[i%len(miss)].AsSlice(), dstIP.AsSlice(),
uint16(1024+i%60000), 80, layers.IPProtocolTCP)
}
}
// Pre-build a pool to avoid allocations dominating the measurement.
pool := make([][]byte, 1024)
for i := range pool {
pool[i] = pktFn(i)
}
// Confirm the matcher is actually exercised: a hit packet must be
// allowed and a miss packet dropped. Without this the benchmark
// could silently time the routed early-return instead.
require.Equal(b, !hit, manager.filterInbound(pool[0], 0),
"benchmark must reach the peer ACL matcher")
b.ResetTimer()
for i := 0; i < b.N; i++ {
manager.filterInbound(pool[i%len(pool)], 0)
}
}
// BenchmarkPeerACLIndexMemory reports the resident memory cost of
// the source-keyed index across realistic deployment shapes. Two
// dimensions matter: (M, K), the number of policies × peers-per-policy,
// and overlap, the fraction of peers shared between policies.
//
// The output uses ReportMetric("bytes/rule") so the cost can be
// compared across shapes directly. Total bytes = bytes/rule * M.
func BenchmarkPeerACLIndexMemory(b *testing.B) {
cases := []struct {
name string
M, K int
overlapFrac float64 // 0 = disjoint per-policy sources, 1 = all share the same pool
}{
{"M=10/K=100/disjoint", 10, 100, 0},
{"M=100/K=100/disjoint", 100, 100, 0},
{"M=100/K=1000/disjoint", 100, 1000, 0},
{"M=100/K=1000/overlap=0.5", 100, 1000, 0.5},
{"M=100/K=1000/overlap=1.0", 100, 1000, 1.0},
{"M=1000/K=100/overlap=1.0", 1000, 100, 1.0},
}
for _, c := range cases {
b.Run(c.name, func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
mgr, err := Create(Config{
IFace: &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
},
FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(b, err)
populateIndexedRules(b, mgr, c.M, c.K, c.overlapFrac)
runtime.GC()
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
before := ms.HeapAlloc
// Drop the manager's external roots so we can isolate
// the index cost. We hold the manager itself live; the
// index is what we measure on the second pass.
mgr.incomingAcceptIndex.reset()
mgr.incomingDenyIndex.reset()
mgr.incomingAcceptRules = mgr.incomingAcceptRules[:0]
mgr.incomingDenyRules = mgr.incomingDenyRules[:0]
runtime.GC()
runtime.ReadMemStats(&ms)
after := ms.HeapAlloc
delta := int64(before) - int64(after)
if delta < 0 {
delta = 0
}
b.ReportMetric(float64(delta)/float64(c.M), "bytes/rule")
b.ReportMetric(float64(delta), "bytes/total")
require.NoError(b, mgr.Close(nil))
}
})
}
}
func populateIndexedRules(b *testing.B, mgr *Manager, m, k int, overlapFrac float64) {
b.Helper()
pool := uniqueAddrs(k+m*k, 1) // big enough universe
sharedLen := int(float64(k) * overlapFrac)
if sharedLen > k {
sharedLen = k
}
shared := pool[:sharedLen]
uniquePool := pool[sharedLen:]
for i := 0; i < m; i++ {
sources := make([]netip.Prefix, 0, k)
for _, a := range shared {
sources = append(sources, netip.PrefixFrom(a, 32))
}
// each policy gets (k-sharedLen) addresses unique to it from the unique pool
unique := uniquePool[i*(k-sharedLen) : (i+1)*(k-sharedLen)]
for _, a := range unique {
sources = append(sources, netip.PrefixFrom(a, 32))
}
_, err := mgr.AddFilterRule(
nil, sources, fw.Network{}, fw.ProtocolTCP, nil,
&fw.Port{Values: []uint16{uint16(80 + i)}},
fw.ActionAccept)
require.NoError(b, err)
}
}
// uniqueAddrs returns n distinct addrs. Seeds 1, 2 are used for
// policy sources / dst; seed 99 puts misses in 10/8.
func uniqueAddrs(n int, seed int64) []netip.Addr {
out := make([]netip.Addr, 0, n)
seen := make(map[netip.Addr]struct{}, n)
r := rand.New(rand.NewSource(seed))
miss := seed == 99
for len(out) < n {
var b [4]byte
if miss {
b[0] = 10
b[1] = byte(r.Intn(256))
} else {
b[0] = 100
b[1] = byte(64 + r.Intn(63))
}
b[2] = byte(r.Intn(256))
b[3] = byte(1 + r.Intn(254))
a := netip.AddrFrom4(b)
if _, ok := seen[a]; ok {
continue
}
seen[a] = struct{}{}
out = append(out, a)
}
return out
}
// uniqueAddrs6 mirrors uniqueAddrs for IPv6: sources come from the ULA
// range fd00::/8, the miss set (seed 99) from 2001:db8::/32 so it is
// disjoint from any source.
func uniqueAddrs6(n int, seed int64) []netip.Addr {
out := make([]netip.Addr, 0, n)
seen := make(map[netip.Addr]struct{}, n)
r := rand.New(rand.NewSource(seed))
miss := seed == 99
for len(out) < n {
var b [16]byte
if miss {
b[0], b[1], b[2], b[3] = 0x20, 0x01, 0x0d, 0xb8
} else {
b[0] = 0xfd
}
for x := 8; x < 16; x++ {
b[x] = byte(r.Intn(256))
}
a := netip.AddrFrom16(b)
if _, ok := seen[a]; ok {
continue
}
seen[a] = struct{}{}
out = append(out, a)
}
return out
}
// generatePacket6 builds an IPv6 TCP/UDP packet, mirroring
// generatePacket for the v4 case.
func generatePacket6(b *testing.B, srcIP, dstIP net.IP, srcPort, dstPort uint16, protocol layers.IPProtocol) []byte {
b.Helper()
ipv6 := &layers.IPv6{
Version: 6,
HopLimit: 64,
NextHeader: protocol,
SrcIP: srcIP,
DstIP: dstIP,
}
var transportLayer gopacket.SerializableLayer
switch protocol {
case layers.IPProtocolTCP:
tcp := &layers.TCP{
SrcPort: layers.TCPPort(srcPort),
DstPort: layers.TCPPort(dstPort),
SYN: true,
}
require.NoError(b, tcp.SetNetworkLayerForChecksum(ipv6))
transportLayer = tcp
case layers.IPProtocolUDP:
udp := &layers.UDP{
SrcPort: layers.UDPPort(srcPort),
DstPort: layers.UDPPort(dstPort),
}
require.NoError(b, udp.SetNetworkLayerForChecksum(ipv6))
transportLayer = udp
}
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{ComputeChecksums: true, FixLengths: true}
require.NoError(b, gopacket.SerializeLayers(buf, opts, ipv6, transportLayer, gopacket.Payload("test")))
return buf.Bytes()
}
@@ -0,0 +1,150 @@
package uspfilter
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
fw "github.com/netbirdio/netbird/client/firewall/manager"
nbiface "github.com/netbirdio/netbird/client/iface"
"github.com/netbirdio/netbird/client/iface/device"
)
func newTestManager(t *testing.T) *Manager {
t.Helper()
ifaceMock := &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
}
m, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: nbiface.DefaultMTU})
require.NoError(t, err, "create manager")
t.Cleanup(func() { require.NoError(t, m.Close(nil)) })
return m
}
// TestAddPeerFiltering_DeduplicatesIdenticalRules verifies that adding
// the same peer rule twice does not create two backing rules. The acl
// manager keys its own cache, but the firewall backend must be
// idempotent on its own so a double-apply cannot leak rules, matching
// the route path and the kernel backends.
func TestAddPeerFiltering_DeduplicatesIdenticalRules(t *testing.T) {
m := newTestManager(t)
ip := net.ParseIP("192.168.1.1")
proto := fw.ProtocolTCP
port := &fw.Port{Values: []uint16{80}}
action := fw.ActionDrop
first, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, proto, nil, port, action)
require.NoError(t, err, "first add")
second, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, proto, nil, port, action)
require.NoError(t, err, "second add")
assert.Equal(t, first.ID(), second.ID(), "duplicate add should return the same rule id")
assert.Len(t, m.incomingDenyRules, 1, "duplicate add must not create a second backing rule")
}
// TestDeletePeerFiltering_NoRefcountSingleDeleteRemoves locks the
// backend's owner accounting for the same-owner case: a content key
// installed twice by the same owner registers one owner claim, so the
// first DeleteFilterRule removes the rule. Owner counting only kicks
// in for distinct management rule IDs (see the peer owner tests); the
// acl manager keys its tracking per (policy, content) and deletes once
// per key, so adds and deletes stay balanced.
func TestDeletePeerFiltering_NoRefcountSingleDeleteRemoves(t *testing.T) {
m := newTestManager(t)
ip := net.ParseIP("192.168.1.1")
proto := fw.ProtocolTCP
port := &fw.Port{Values: []uint16{80}}
action := fw.ActionDrop
first, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, proto, nil, port, action)
require.NoError(t, err, "first add")
second, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, proto, nil, port, action)
require.NoError(t, err, "second add")
require.Equal(t, first.ID(), second.ID(), "dedup to one rule")
require.Len(t, m.incomingDenyRules, 1, "still one backing rule after duplicate add")
require.NoError(t, m.DeleteFilterRule(first), "delete once")
assert.Empty(t, m.incomingDenyRules, "single delete removes the backing rule (no refcount)")
assert.NotContains(t, m.peerRulesMap, first.ID(), "dedup map entry cleared")
}
// TestAddPeerFiltering_DeterministicID verifies the peer rule id is a
// content hash, not a random UUID: identical inputs produce the same id
// across independent managers. A random id breaks caller-side dedup.
func TestAddPeerFiltering_DeterministicID(t *testing.T) {
ip := net.ParseIP("10.0.0.5")
proto := fw.ProtocolUDP
port := &fw.Port{Values: []uint16{53}}
action := fw.ActionAccept
m1 := newTestManager(t)
r1, err := m1.AddFilterRule(nil, pfx(ip), fw.Network{}, proto, nil, port, action)
require.NoError(t, err)
m2 := newTestManager(t)
r2, err := m2.AddFilterRule(nil, pfx(ip), fw.Network{}, proto, nil, port, action)
require.NoError(t, err)
assert.Equal(t, r1.ID(), r2.ID(), "same inputs must produce the same rule id")
}
// TestAddPeerFiltering_DistinctRulesNotDeduped verifies that rules
// differing only by port are kept separate.
func TestAddPeerFiltering_DistinctRulesNotDeduped(t *testing.T) {
m := newTestManager(t)
ip := net.ParseIP("192.168.1.1")
proto := fw.ProtocolTCP
action := fw.ActionAccept
r80, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, proto, nil, &fw.Port{Values: []uint16{80}}, action)
require.NoError(t, err)
r443, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, proto, nil, &fw.Port{Values: []uint16{443}}, action)
require.NoError(t, err)
assert.NotEqual(t, r80.ID(), r443.ID(), "different ports must produce different rule ids")
assert.Len(t, m.incomingAcceptRules, 2, "distinct rules must both be stored")
}
// TestAddPeerFiltering_SourceVsDestPortNotDeduped verifies that a rule
// matching on source port and one matching on destination port for the
// same selector do not collide: the port lands in a different slot, so
// the content key must differ.
func TestAddPeerFiltering_SourceVsDestPortNotDeduped(t *testing.T) {
m := newTestManager(t)
ip := net.ParseIP("192.168.1.1")
proto := fw.ProtocolTCP
port := &fw.Port{Values: []uint16{80}}
action := fw.ActionAccept
dPortRule, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, proto, nil, port, action)
require.NoError(t, err)
sPortRule, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, proto, port, nil, action)
require.NoError(t, err)
assert.NotEqual(t, dPortRule.ID(), sPortRule.ID(), "source-port and dest-port matches must produce different rule ids")
}
// TestAddFilterRule_EmptySourcesRejected verifies that an empty source
// list is rejected rather than treated as "match any". "Match any" must
// be an explicit /0, so a zeroed list can never silently widen a rule to
// every source.
func TestAddFilterRule_EmptySourcesRejected(t *testing.T) {
m := newTestManager(t)
proto := fw.ProtocolTCP
port := &fw.Port{Values: []uint16{80}}
_, err := m.AddFilterRule(nil, nil, fw.Network{}, proto, nil, port, fw.ActionAccept)
require.ErrorIs(t, err, fw.ErrNoSources, "empty sources must be rejected")
assert.Empty(t, m.incomingAcceptRules, "no rule should be stored for empty sources")
}
@@ -0,0 +1,105 @@
package uspfilter
import (
"net"
"net/netip"
"testing"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
fw "github.com/netbirdio/netbird/client/firewall/manager"
nbiface "github.com/netbirdio/netbird/client/iface"
"github.com/netbirdio/netbird/client/iface/device"
"github.com/netbirdio/netbird/client/iface/wgaddr"
)
func newV6TestManager(t *testing.T, localV6 string) *Manager {
t.Helper()
ifaceMock := &IFaceMock{
SetFilterFunc: func(device.PacketFilter) error { return nil },
AddressFunc: func() wgaddr.Address {
return wgaddr.Address{
IP: netip.MustParseAddr("100.10.0.100"),
Network: netip.MustParsePrefix("100.10.0.0/16"),
IPv6: netip.MustParseAddr(localV6),
IPv6Net: netip.MustParsePrefix("fd00::/64"),
}
},
}
m, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: nbiface.DefaultMTU})
require.NoError(t, err, "create manager")
t.Cleanup(func() { require.NoError(t, m.Close(nil)) })
return m
}
func v6UDPPacket(t *testing.T, src, dst string, dstPort uint16) []byte {
t.Helper()
ip6 := &layers.IPv6{
Version: 6,
HopLimit: 64,
NextHeader: layers.IPProtocolUDP,
SrcIP: net.ParseIP(src),
DstIP: net.ParseIP(dst),
}
udp := &layers.UDP{SrcPort: 51334, DstPort: layers.UDPPort(dstPort)}
require.NoError(t, udp.SetNetworkLayerForChecksum(ip6))
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{ComputeChecksums: true, FixLengths: true}
require.NoError(t, gopacket.SerializeLayers(buf, opts, ip6, udp, gopacket.Payload("test")))
return buf.Bytes()
}
// TestPeerACL_IPv6HostRule verifies the source index resolves /128 v6
// rules: a matching v6 source is accepted, a non-matching one is
// denied by the default. This is the end-to-end proof that the index
// is not v4-only.
func TestPeerACL_IPv6HostRule(t *testing.T) {
m := newV6TestManager(t, "fd00::100")
src := net.ParseIP("fd00::1")
_, err := m.AddFilterRule(nil, pfx(src), fw.Network{}, fw.ProtocolUDP, nil, &fw.Port{Values: []uint16{53}}, fw.ActionAccept)
require.NoError(t, err, "add v6 accept rule")
require.False(t, m.filterInbound(v6UDPPacket(t, "fd00::1", "fd00::100", 53), 0),
"v6 packet from the allowed /128 source must be accepted")
require.True(t, m.filterInbound(v6UDPPacket(t, "fd00::2", "fd00::100", 53), 0),
"v6 packet from an unlisted source must be denied by default")
}
// TestPeerACL_IPv6IndexBuckets verifies that v6 sources land in the
// right index bucket: a /128 in bySource keyed by its address, and
// coarser prefixes (including ::/0) in the nonHost slice.
func TestPeerACL_IPv6IndexBuckets(t *testing.T) {
m := newV6TestManager(t, "fd00::100")
port := &fw.Port{Values: []uint16{53}}
host := netip.MustParseAddr("fd00::1")
_, err := m.AddFilterRule(nil, []netip.Prefix{netip.PrefixFrom(host, 128)}, fw.Network{}, fw.ProtocolUDP, nil, port, fw.ActionAccept)
require.NoError(t, err)
assert.Contains(t, m.incomingAcceptIndex.bySource, host, "/128 v6 source must be indexed by address")
_, err = m.AddFilterRule(nil, []netip.Prefix{netip.MustParsePrefix("fd00:dead::/64")}, fw.Network{}, fw.ProtocolUDP, nil, port, fw.ActionAccept)
require.NoError(t, err)
require.Len(t, m.incomingAcceptIndex.nonHost, 1, "coarser v6 prefix must land in nonHost")
_, err = m.AddFilterRule(nil, []netip.Prefix{netip.MustParsePrefix("::/0")}, fw.Network{}, fw.ProtocolUDP, nil, port, fw.ActionAccept)
require.NoError(t, err)
require.Len(t, m.incomingAcceptIndex.nonHost, 2, "::/0 source must also land in nonHost")
}
// TestPeerACL_IPv4MappedSourceNormalized verifies a v4-mapped v6
// source prefix is normalized to v4 so a plain v4 packet matches it.
func TestPeerACL_IPv4MappedSourceNormalized(t *testing.T) {
m := newTestManager(t)
mapped := netip.MustParseAddr("::ffff:192.168.1.1")
_, err := m.AddFilterRule(nil, []netip.Prefix{netip.PrefixFrom(mapped, mapped.BitLen())}, fw.Network{}, fw.ProtocolUDP, nil, &fw.Port{Values: []uint16{53}}, fw.ActionAccept)
require.NoError(t, err)
v4 := netip.MustParseAddr("192.168.1.1")
assert.Contains(t, m.incomingAcceptIndex.bySource, v4, "v4-mapped v6 source must be indexed as plain v4")
}
@@ -0,0 +1,104 @@
package uspfilter
import (
"net"
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
fw "github.com/netbirdio/netbird/client/firewall/manager"
)
// peerACLCheck decodes the packet and runs it through the peer ACLs,
// returning the attributed management rule id and the drop verdict.
func peerACLCheck(t *testing.T, m *Manager, packet []byte) ([]byte, bool) {
t.Helper()
d := m.decoders.Get().(*decoder)
defer m.decoders.Put(d)
require.NoError(t, d.decodePacket(packet))
src, _ := m.extractIPs(d)
return m.peerACLsBlock(src, d, packet)
}
// TestPeerACL_MultiValuePortMatchesEachListedPort guards the multi-value
// port path: a rule listing several discrete destination ports must
// match a packet to each listed port and drop one that is not listed.
// Management currently splits a multi-port policy into one rule per port
// (and the wire format carries a single port), so this list shape is not
// emitted today; the test locks correct matching in case that changes.
func TestPeerACL_MultiValuePortMatchesEachListedPort(t *testing.T) {
m := newTestManager(t)
src := net.ParseIP("192.168.1.1")
ports := &fw.Port{Values: []uint16{80, 443}}
_, err := m.AddFilterRule(nil, pfx(src), fw.Network{}, fw.ProtocolTCP, nil, ports, fw.ActionAccept)
require.NoError(t, err, "add multi-value port rule")
for _, p := range []uint16{80, 443} {
_, blocked := peerACLCheck(t, m, createTestPacket(t, "192.168.1.1", "10.0.0.2", fw.ProtocolTCP, 12345, p))
assert.False(t, blocked, "packet to listed port %d must match the rule", p)
}
_, blocked := peerACLCheck(t, m, createTestPacket(t, "192.168.1.1", "10.0.0.2", fw.ProtocolTCP, 12345, 8080))
assert.True(t, blocked, "packet to a port not in the list must not match the rule")
}
// TestPeerACL_MatchAnyIsFamilyScoped verifies that a /0 source matches
// only packets of its own family: 0.0.0.0/0 must not match IPv6 packets
// and ::/0 must not match IPv4 packets, matching kernel backend
// semantics.
func TestPeerACL_MatchAnyIsFamilyScoped(t *testing.T) {
m := newTestManager(t)
v4Packet := createTestPacket(t, "10.0.0.1", "10.0.0.2", fw.ProtocolUDP, 12345, 53)
v6Packet := v6UDPPacket(t, "fd00::1", "fd00::100", 53)
v4Any := []netip.Prefix{netip.PrefixFrom(netip.IPv4Unspecified(), 0)}
rule, err := m.AddFilterRule(nil, v4Any, fw.Network{}, fw.ProtocolALL, nil, nil, fw.ActionAccept)
require.NoError(t, err, "add v4 /0 rule")
_, blocked := peerACLCheck(t, m, v4Packet)
assert.False(t, blocked, "0.0.0.0/0 must match IPv4 packets")
_, blocked = peerACLCheck(t, m, v6Packet)
assert.True(t, blocked, "0.0.0.0/0 must not match IPv6 packets")
require.NoError(t, m.DeleteFilterRule(rule))
v6Any := []netip.Prefix{netip.PrefixFrom(netip.IPv6Unspecified(), 0)}
_, err = m.AddFilterRule(nil, v6Any, fw.Network{}, fw.ProtocolALL, nil, nil, fw.ActionAccept)
require.NoError(t, err, "add v6 /0 rule")
_, blocked = peerACLCheck(t, m, v6Packet)
assert.False(t, blocked, "::/0 must match IPv6 packets")
_, blocked = peerACLCheck(t, m, v4Packet)
assert.True(t, blocked, "::/0 must not match IPv4 packets")
}
// TestRouteACL_MixedFamilyZeroSourcesStayFamilySafe verifies the route
// path keeps per-prefix family matching when a single rule carries both
// 0.0.0.0/0 and ::/0 sources, as blockInvalidRouted does.
func TestRouteACL_MixedFamilyZeroSourcesStayFamilySafe(t *testing.T) {
m := newTestManager(t)
sources := []netip.Prefix{
netip.PrefixFrom(netip.IPv4Unspecified(), 0),
netip.PrefixFrom(netip.IPv6Unspecified(), 0),
}
_, err := m.AddFilterRule(nil, sources, fw.Network{Prefix: netip.MustParsePrefix("10.0.0.0/24")},
fw.ProtocolALL, nil, nil, fw.ActionAccept)
require.NoError(t, err)
_, err = m.AddFilterRule(nil, sources, fw.Network{Prefix: netip.MustParsePrefix("fd00:1::/64")},
fw.ProtocolALL, nil, nil, fw.ActionAccept)
require.NoError(t, err)
v4Src := netip.MustParseAddr("192.168.1.1")
v6Src := netip.MustParseAddr("fd00::1")
_, pass := m.routeACLsPass(v4Src, netip.MustParseAddr("10.0.0.5"), 255, 0, 0)
assert.True(t, pass, "v4 source must match the v4 destination rule via 0.0.0.0/0")
_, pass = m.routeACLsPass(v6Src, netip.MustParseAddr("fd00:1::5"), 255, 0, 0)
assert.True(t, pass, "v6 source must match the v6 destination rule via ::/0")
}
+140
View File
@@ -0,0 +1,140 @@
package uspfilter
import (
"net/netip"
"slices"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
firewall "github.com/netbirdio/netbird/client/firewall/manager"
)
// peerRuleIndex is the source-side dispatcher consulted on the packet
// hot path. It splits rules into two buckets by the shape of their
// source list:
//
// - bySource: every source is a host prefix (/32 for v4, /128 for
// v6). Keyed by the concrete source address, so a hit guarantees
// the source filter passes and the matcher goes straight to
// proto/port checks. This is the common case for peer ACLs.
// - nonHost: any source list with a prefix coarser than a host,
// including a /0 "match any". Walked linearly with a per-rule
// Contains() check. Expected small or empty for typical peer ACLs.
//
// Maintained incrementally by add/remove, never rebuilt.
type peerRuleIndex struct {
bySource map[netip.Addr][]*PeerRule
nonHost []*PeerRule
}
func (i *peerRuleIndex) add(r *PeerRule) {
if hasNonHostSource(r) {
i.nonHost = append(i.nonHost, r)
return
}
if i.bySource == nil {
i.bySource = make(map[netip.Addr][]*PeerRule)
}
for a := range r.sourceAddrs {
i.bySource[a] = append(i.bySource[a], r)
}
}
func (i *peerRuleIndex) remove(r *PeerRule) {
if hasNonHostSource(r) {
i.nonHost = slices.DeleteFunc(i.nonHost, eqRule(r))
return
}
if i.bySource == nil {
return
}
for a := range r.sourceAddrs {
entries := slices.DeleteFunc(i.bySource[a], eqRule(r))
if len(entries) == 0 {
delete(i.bySource, a)
} else {
i.bySource[a] = entries
}
}
}
func (i *peerRuleIndex) reset() {
i.bySource = nil
i.nonHost = i.nonHost[:0]
}
// match returns the first rule matching src and the decoded packet.
// Host rules are found by direct map lookup; nonHost rules run a
// per-rule source Contains() check. Containment is family-scoped, so
// a /0 source matches every address of its own family only (0.0.0.0/0
// never matches v6 sources and ::/0 never matches v4). Within either
// bucket the matcher runs the proto/port filter.
func (i *peerRuleIndex) match(src netip.Addr, d *decoder) ([]byte, bool, bool) {
payloadLayer := d.decoded[1]
for _, rule := range i.bySource[src] {
if id, drop, ok := matchProto(rule, d, payloadLayer); ok {
return id, drop, true
}
}
for _, rule := range i.nonHost {
if !prefixesContain(rule.sources, src) {
continue
}
if id, drop, ok := matchProto(rule, d, payloadLayer); ok {
return id, drop, true
}
}
return nil, false, false
}
func eqRule(target *PeerRule) func(*PeerRule) bool {
return func(p *PeerRule) bool { return p == target }
}
// hasNonHostSource reports whether the rule has any source prefix
// that is not a single host address. Called only at add/remove time,
// not on the packet path.
func hasNonHostSource(r *PeerRule) bool {
for _, p := range r.sources {
if p.Bits() != p.Addr().BitLen() {
return true
}
}
return false
}
// matchProto applies the proto/port half of a rule against the
// decoded packet. Source matching is the caller's responsibility.
func matchProto(rule *PeerRule, d *decoder, payloadLayer gopacket.LayerType) ([]byte, bool, bool) {
drop := rule.action == firewall.ActionDrop
if rule.protoLayer == layerTypeAll {
return rule.mgmtId, drop, true
}
if !protoLayerMatches(rule.protoLayer, payloadLayer) {
return nil, false, false
}
switch payloadLayer {
case layers.LayerTypeTCP:
if portsMatch(rule.srcPort, uint16(d.tcp.SrcPort)) && portsMatch(rule.dstPort, uint16(d.tcp.DstPort)) {
return rule.mgmtId, drop, true
}
case layers.LayerTypeUDP:
if portsMatch(rule.srcPort, uint16(d.udp.SrcPort)) && portsMatch(rule.dstPort, uint16(d.udp.DstPort)) {
return rule.mgmtId, drop, true
}
case layers.LayerTypeICMPv4, layers.LayerTypeICMPv6:
return rule.mgmtId, drop, true
}
return nil, false, false
}
func prefixesContain(sources []netip.Prefix, src netip.Addr) bool {
for _, p := range sources {
if p.Contains(src) {
return true
}
}
return false
}
+31 -12
View File
@@ -10,24 +10,43 @@ import (
// PeerRule to handle management of rules
type PeerRule struct {
id string
mgmtId []byte
ip netip.Addr
ipLayer gopacket.LayerType
matchByIP bool
protoLayer gopacket.LayerType
sPort *firewall.Port
dPort *firewall.Port
drop bool
id firewall.RuleID
mgmtId []byte
// sources is the canonical list of source prefixes this rule
// matches against.
sources []netip.Prefix
// sourceAddrs is a fast-path membership set for host-prefix
// sources (/32 v4, /128 v6). Populated alongside sources;
// consulted before falling back to prefix scan.
sourceAddrs map[netip.Addr]struct{}
protoLayer gopacket.LayerType
srcPort *firewall.Port
dstPort *firewall.Port
action firewall.Action
}
// matchesSource reports whether the given source address is covered
// by this rule's source list. Prefix containment is family-scoped, so
// a /0 source matches every address of its own family only.
func (r *PeerRule) matchesSource(src netip.Addr) bool {
if _, ok := r.sourceAddrs[src]; ok {
return true
}
for _, p := range r.sources {
if p.Contains(src) {
return true
}
}
return false
}
// ID returns the rule id
func (r *PeerRule) ID() string {
func (r *PeerRule) ID() firewall.RuleID {
return r.id
}
type RouteRule struct {
id string
id firewall.RuleID
mgmtId []byte
sources []netip.Prefix
dstSet firewall.Set
@@ -39,6 +58,6 @@ type RouteRule struct {
}
// ID returns the rule id
func (r *RouteRule) ID() string {
func (r *RouteRule) ID() firewall.RuleID {
return r.id
}
@@ -0,0 +1,50 @@
package uspfilter
import (
"net"
"net/netip"
firewall "github.com/netbirdio/netbird/client/firewall/manager"
)
// countRulesForAddr reports how many rules in the given slice match
// the supplied source address.
func countRulesForAddr(rules peerRules, src netip.Addr) int {
n := 0
for _, r := range rules {
if r.matchesSource(src) {
n++
}
}
return n
}
// findRuleByID returns true if the rules slice contains a rule with
// the given id whose source set covers src.
func findRuleByID(rules peerRules, src netip.Addr, id firewall.RuleID) bool {
for _, r := range rules {
if r.id == id && r.matchesSource(src) {
return true
}
}
return false
}
// pfx converts a single net.IP into the []netip.Prefix form
// AddFilterRule expects. A nil or unspecified address becomes a /0
// ("match any") prefix in the matching family; any other address
// becomes its /32 (or /128) host prefix.
func pfx(ip net.IP) []netip.Prefix {
if ip == nil {
return []netip.Prefix{netip.PrefixFrom(netip.IPv4Unspecified(), 0)}
}
if ip.IsUnspecified() {
if ip.To4() != nil {
return []netip.Prefix{netip.PrefixFrom(netip.IPv4Unspecified(), 0)}
}
return []netip.Prefix{netip.PrefixFrom(netip.IPv6Unspecified(), 0)}
}
a, _ := netip.AddrFromSlice(ip)
a = a.Unmap()
return []netip.Prefix{netip.PrefixFrom(a, a.BitLen())}
}
+8
View File
@@ -285,6 +285,14 @@ func (m *Manager) TracePacket(packetData []byte, direction fw.RuleDirection) *Pa
trace.SourceIP = srcIP
trace.DestinationIP = dstIP
// A fragment or otherwise truncated packet has no transport layer.
// The inbound datapath drops these via isValidPacket; the tracer must
// guard explicitly since every downstream stage reads d.decoded[1].
if len(d.decoded) < 2 {
trace.AddResult(StageReceived, "Packet has no transport layer (fragment or unsupported protocol)", false)
return trace
}
// Determine protocol and ports
switch d.decoded[1] {
case layers.LayerTypeTCP:
+11 -11
View File
@@ -45,7 +45,7 @@ func TestTracePacket(t *testing.T) {
},
}
m, err := Create(ifaceMock, false, flowLogger, iface.DefaultMTU)
m, err := Create(Config{IFace: ifaceMock, FlowLogger: flowLogger, MTU: iface.DefaultMTU})
require.NoError(t, err)
if !statefulMode {
@@ -97,7 +97,7 @@ func TestTracePacket(t *testing.T) {
proto := fw.ProtocolTCP
port := &fw.Port{Values: []uint16{80}}
action := fw.ActionAccept
_, err := m.AddPeerFiltering(nil, ip, proto, nil, port, action, "")
_, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, proto, nil, port, action)
require.NoError(t, err)
},
packetBuilder: func() *PacketBuilder {
@@ -121,7 +121,7 @@ func TestTracePacket(t *testing.T) {
proto := fw.ProtocolTCP
port := &fw.Port{Values: []uint16{80}}
action := fw.ActionDrop
_, err := m.AddPeerFiltering(nil, ip, proto, nil, port, action, "")
_, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, proto, nil, port, action)
require.NoError(t, err)
},
packetBuilder: func() *PacketBuilder {
@@ -150,7 +150,7 @@ func TestTracePacket(t *testing.T) {
proto := fw.ProtocolTCP
port := &fw.Port{Values: []uint16{80}}
action := fw.ActionAccept
_, err := m.AddPeerFiltering(nil, ip, proto, nil, port, action, "")
_, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, proto, nil, port, action)
require.NoError(t, err)
},
packetBuilder: func() *PacketBuilder {
@@ -178,7 +178,7 @@ func TestTracePacket(t *testing.T) {
proto := fw.ProtocolTCP
port := &fw.Port{Values: []uint16{80}}
action := fw.ActionAccept
_, err := m.AddPeerFiltering(nil, ip, proto, nil, port, action, "")
_, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, proto, nil, port, action)
require.NoError(t, err)
},
packetBuilder: func() *PacketBuilder {
@@ -205,7 +205,7 @@ func TestTracePacket(t *testing.T) {
src := netip.PrefixFrom(netip.AddrFrom4([4]byte{1, 1, 1, 1}), 32)
dst := netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 168, 17, 2}), 32)
_, err := m.AddRouteFiltering(nil, []netip.Prefix{src}, fw.Network{Prefix: dst}, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept)
_, err := m.AddFilterRule(nil, []netip.Prefix{src}, fw.Network{Prefix: dst}, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept)
require.NoError(t, err)
},
packetBuilder: func() *PacketBuilder {
@@ -231,7 +231,7 @@ func TestTracePacket(t *testing.T) {
src := netip.PrefixFrom(netip.AddrFrom4([4]byte{1, 1, 1, 1}), 32)
dst := netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 168, 17, 2}), 32)
_, err := m.AddRouteFiltering(nil, []netip.Prefix{src}, fw.Network{Prefix: dst}, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionDrop)
_, err := m.AddFilterRule(nil, []netip.Prefix{src}, fw.Network{Prefix: dst}, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionDrop)
require.NoError(t, err)
},
packetBuilder: func() *PacketBuilder {
@@ -332,7 +332,7 @@ func TestTracePacket(t *testing.T) {
ip := net.ParseIP("1.1.1.1")
proto := fw.ProtocolICMP
action := fw.ActionAccept
_, err := m.AddPeerFiltering(nil, ip, proto, nil, nil, action, "")
_, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, proto, nil, nil, action)
require.NoError(t, err)
},
packetBuilder: func() *PacketBuilder {
@@ -355,7 +355,7 @@ func TestTracePacket(t *testing.T) {
ip := net.ParseIP("1.1.1.1")
proto := fw.ProtocolICMP
action := fw.ActionDrop
_, err := m.AddPeerFiltering(nil, ip, proto, nil, nil, action, "")
_, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, proto, nil, nil, action)
require.NoError(t, err)
},
packetBuilder: func() *PacketBuilder {
@@ -379,7 +379,7 @@ func TestTracePacket(t *testing.T) {
proto := fw.ProtocolUDP
port := &fw.Port{Values: []uint16{53}}
action := fw.ActionAccept
_, err := m.AddPeerFiltering(nil, ip, proto, nil, port, action, "")
_, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, proto, nil, port, action)
require.NoError(t, err)
},
packetBuilder: func() *PacketBuilder {
@@ -423,7 +423,7 @@ func TestTracePacket(t *testing.T) {
proto := fw.ProtocolTCP
port := &fw.Port{Values: []uint16{80}}
action := fw.ActionDrop
_, err := m.AddPeerFiltering(nil, ip, proto, nil, port, action, "")
_, err := m.AddFilterRule(nil, pfx(ip), fw.Network{}, proto, nil, port, action)
require.NoError(t, err)
},
packetBuilder: func() *PacketBuilder {