mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-23 07:09:08 +02:00
[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:
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
+34
-31
@@ -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...)
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user