mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-12 17:59:06 +02:00
* 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
250 lines
6.9 KiB
Go
250 lines
6.9 KiB
Go
//go:build !android
|
|
|
|
package nftables
|
|
|
|
import (
|
|
"fmt"
|
|
"net/netip"
|
|
|
|
"github.com/coreos/go-iptables/iptables"
|
|
"github.com/google/nftables"
|
|
"github.com/hashicorp/go-multierror"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
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/internal/routemanager/ipfwdstate"
|
|
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
|
)
|
|
|
|
const (
|
|
tableNat = "nat"
|
|
tableMangle = "mangle"
|
|
tableRaw = "raw"
|
|
tableSecurity = "security"
|
|
|
|
chainNameNatPrerouting = "PREROUTING"
|
|
chainNameRoutingFw = "netbird-rt-fwd"
|
|
chainNameRoutingNat = "netbird-rt-postrouting"
|
|
chainNameRoutingRdr = "netbird-rt-redirect"
|
|
chainNameNATOutput = "netbird-nat-output"
|
|
chainNameForward = "FORWARD"
|
|
chainNameMangleForward = "netbird-mangle-forward"
|
|
|
|
// Peer ACL chain names.
|
|
chainNameInputRules = "netbird-acl-input-rules"
|
|
chainNameInputFilter = "netbird-acl-input-filter"
|
|
chainNameForwardFilter = "netbird-acl-forward-filter"
|
|
chainNameManglePrerouting = "netbird-mangle-prerouting"
|
|
chainNameManglePostrouting = "netbird-mangle-postrouting"
|
|
|
|
flushError = "flush: %w"
|
|
|
|
firewalldTableName = "firewalld"
|
|
|
|
userDataAcceptForwardRuleIif = "frwacceptiif"
|
|
userDataAcceptForwardRuleOif = "frwacceptoif"
|
|
userDataAcceptInputRule = "inputaccept"
|
|
|
|
dnatSuffix firewall.RuleID = "_dnat"
|
|
snatSuffix firewall.RuleID = "_snat"
|
|
|
|
// ipv4TCPHeaderSize is the minimum IPv4 (20) + TCP (20) header size for MSS calculation.
|
|
ipv4TCPHeaderSize = 40
|
|
// ipv6TCPHeaderSize is the minimum IPv6 (40) + TCP (20) header size for MSS calculation.
|
|
ipv6TCPHeaderSize = 60
|
|
|
|
// maxPrefixesSet 1638 prefixes start to fail, taking some margin
|
|
maxPrefixesSet = 1500
|
|
refreshRulesMapError = "refresh rules map: %w"
|
|
)
|
|
|
|
var (
|
|
errFilterTableNotFound = fmt.Errorf("'filter' table not found")
|
|
)
|
|
|
|
type setInput struct {
|
|
set firewall.Set
|
|
prefixes []netip.Prefix
|
|
}
|
|
|
|
// family holds the per-address-family nftables state. One instance
|
|
// handles route ACLs, peer ACLs, NAT, DNAT, and MSS clamping for a
|
|
// single family; the top-level Manager owns one for v4 and another
|
|
// for v6. The name predates the peer-ACL absorption; it's effectively
|
|
// the per-family backend now.
|
|
type family struct {
|
|
conn *nftables.Conn
|
|
workTable *nftables.Table
|
|
filterTable *nftables.Table
|
|
chains map[string]*nftables.Chain
|
|
|
|
// filters holds peer + route filter rules keyed by content hash.
|
|
// AddFilterRule writes here; DeleteFilterRule looks up by id.
|
|
filters map[firewall.RuleID]*Rule
|
|
|
|
// rules holds NAT, DNAT, and external accept rules (auxiliary
|
|
// plumbing that isn't a filter rule).
|
|
rules map[firewall.RuleID]*nftables.Rule
|
|
|
|
// Peer ACL chain handles.
|
|
chainInputRules *nftables.Chain
|
|
chainPrerouting *nftables.Chain
|
|
routingFwChainName string
|
|
|
|
ipsetCounter *refcounter.Counter[string, setInput, *nftables.Set]
|
|
|
|
af addrFamily
|
|
wgIface iFaceMapper
|
|
ipFwdState *ipfwdstate.IPForwardingState
|
|
legacyManagement bool
|
|
mtu uint16
|
|
}
|
|
|
|
func newFamily(workTable *nftables.Table, wgIface iFaceMapper, mtu uint16) *family {
|
|
r := &family{
|
|
conn: &nftables.Conn{},
|
|
workTable: workTable,
|
|
chains: make(map[string]*nftables.Chain),
|
|
filters: make(map[firewall.RuleID]*Rule),
|
|
rules: make(map[firewall.RuleID]*nftables.Rule),
|
|
routingFwChainName: chainNameRoutingFw,
|
|
af: familyForAddr(workTable.Family == nftables.TableFamilyIPv4),
|
|
wgIface: wgIface,
|
|
ipFwdState: ipfwdstate.NewIPForwardingState(wgIface.Name()),
|
|
mtu: mtu,
|
|
}
|
|
|
|
r.ipsetCounter = refcounter.New(
|
|
r.createIpSet,
|
|
r.deleteIpSet,
|
|
)
|
|
|
|
var err error
|
|
r.filterTable, err = r.loadFilterTable()
|
|
if err != nil {
|
|
log.Debugf("ip filter table not found: %v", err)
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
func (r *family) init(workTable *nftables.Table) error {
|
|
r.workTable = workTable
|
|
|
|
if err := r.removeAcceptFilterRules(); err != nil {
|
|
log.Errorf("failed to clean up rules from filter table: %s", err)
|
|
}
|
|
|
|
if err := r.createContainers(); err != nil {
|
|
return fmt.Errorf("create containers: %w", err)
|
|
}
|
|
|
|
if err := r.setupDataPlaneMark(); err != nil {
|
|
log.Errorf("failed to set up data plane mark: %v", err)
|
|
}
|
|
|
|
if err := r.createDefaultChains(); err != nil {
|
|
return fmt.Errorf("create default acl chains: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Reset cleans existing nftables filter table rules from the system
|
|
func (r *family) Reset() error {
|
|
// clear without deleting the ipsets, the nf table will be deleted by the caller
|
|
r.ipsetCounter.Clear()
|
|
|
|
var merr *multierror.Error
|
|
|
|
if err := r.removeAcceptFilterRules(); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("remove accept filter rules: %w", err))
|
|
}
|
|
|
|
if err := firewalld.UntrustInterface(r.wgIface.Name()); err != nil {
|
|
merr = multierror.Append(merr, err)
|
|
}
|
|
|
|
if err := r.removeNatPreroutingRules(); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("remove filter prerouting rules: %w", err))
|
|
}
|
|
|
|
return nberrors.FormatErrorOrNil(merr)
|
|
}
|
|
|
|
func (r *family) loadFilterTable() (*nftables.Table, error) {
|
|
tables, err := r.conn.ListTablesOfFamily(r.af.tableFamily)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list tables: %w", err)
|
|
}
|
|
|
|
for _, table := range tables {
|
|
if table.Name == "filter" {
|
|
return table, nil
|
|
}
|
|
}
|
|
|
|
return nil, errFilterTableNotFound
|
|
}
|
|
|
|
func hookName(hook *nftables.ChainHook) string {
|
|
if hook == nil {
|
|
return "unknown"
|
|
}
|
|
switch *hook {
|
|
case *nftables.ChainHookForward:
|
|
return chainNameForward
|
|
case *nftables.ChainHookInput:
|
|
return chainNameInput
|
|
default:
|
|
return fmt.Sprintf("hook(%d)", *hook)
|
|
}
|
|
}
|
|
|
|
func familyName(family nftables.TableFamily) string {
|
|
switch family {
|
|
case nftables.TableFamilyIPv4:
|
|
return "ip"
|
|
case nftables.TableFamilyIPv6:
|
|
return "ip6"
|
|
case nftables.TableFamilyINet:
|
|
return "inet"
|
|
default:
|
|
return fmt.Sprintf("family(%d)", family)
|
|
}
|
|
}
|
|
|
|
func (r *family) iptablesProto() iptables.Protocol {
|
|
if r.af.tableFamily == nftables.TableFamilyIPv6 {
|
|
return iptables.ProtocolIPv6
|
|
}
|
|
return iptables.ProtocolIPv4
|
|
}
|
|
|
|
func (r *family) refreshRulesMap() error {
|
|
var merr *multierror.Error
|
|
newRules := make(map[firewall.RuleID]*nftables.Rule)
|
|
for _, chain := range r.chains {
|
|
rules, err := r.conn.GetRules(chain.Table, chain)
|
|
if err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("list rules for chain %s: %w", chain.Name, err))
|
|
// preserve existing entries for this chain since we can't verify their state
|
|
for k, v := range r.rules {
|
|
if v.Chain != nil && v.Chain.Name == chain.Name {
|
|
newRules[k] = v
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
for _, rule := range rules {
|
|
if len(rule.UserData) > 0 {
|
|
newRules[firewall.RuleID(rule.UserData)] = rule
|
|
}
|
|
}
|
|
}
|
|
r.rules = newRules
|
|
return nberrors.FormatErrorOrNil(merr)
|
|
}
|