mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-17 12:09:58 +00:00
501 lines
13 KiB
Go
501 lines
13 KiB
Go
//go:build !android
|
|
|
|
package nftables
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/google/nftables"
|
|
"github.com/google/nftables/binaryutil"
|
|
"github.com/google/nftables/expr"
|
|
"github.com/hashicorp/go-multierror"
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.org/x/sys/unix"
|
|
|
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
|
nbnet "github.com/netbirdio/netbird/client/net"
|
|
)
|
|
|
|
func (r *family) AddNatRule(pair firewall.RouterPair) error {
|
|
if err := r.refreshRulesMap(); err != nil {
|
|
return fmt.Errorf(refreshRulesMapError, err)
|
|
}
|
|
|
|
if r.legacyManagement {
|
|
log.Warnf("This peer is connected to a NetBird Management service with an older version. Allowing all traffic for %s", pair.Destination)
|
|
if err := r.addLegacyRouteRule(pair); err != nil {
|
|
r.rollbackRules(pair)
|
|
return fmt.Errorf("add legacy routing rule: %w", err)
|
|
}
|
|
}
|
|
|
|
if pair.Masquerade {
|
|
if err := r.addNatRule(pair); err != nil {
|
|
r.rollbackRules(pair)
|
|
return fmt.Errorf("add nat rule: %w", err)
|
|
}
|
|
|
|
if err := r.addNatRule(firewall.GetInversePair(pair)); err != nil {
|
|
r.rollbackRules(pair)
|
|
return fmt.Errorf("add inverse nat rule: %w", err)
|
|
}
|
|
}
|
|
|
|
if err := r.conn.Flush(); err != nil {
|
|
r.rollbackRules(pair)
|
|
return fmt.Errorf("insert rules for %s: %w", pair.Destination, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// rollbackRules cleans up unflushed rules and their set counters after a flush failure.
|
|
func (r *family) rollbackRules(pair firewall.RouterPair) {
|
|
keys := []firewall.RuleID{
|
|
pair.GenKey(firewall.ForwardingFormat),
|
|
pair.GenKey(firewall.PreroutingFormat),
|
|
firewall.GetInversePair(pair).GenKey(firewall.PreroutingFormat),
|
|
}
|
|
for _, key := range keys {
|
|
rule, ok := r.rules[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if err := r.decrementSetCounter(rule); err != nil {
|
|
log.Warnf("rollback set counter for %s: %v", key, err)
|
|
}
|
|
delete(r.rules, key)
|
|
}
|
|
}
|
|
|
|
// addNatRule inserts a nftables rule to the conn client flush queue
|
|
func (r *family) addNatRule(pair firewall.RouterPair) error {
|
|
sourceExp, err := r.applyNetwork(pair.Source, nil, true)
|
|
if err != nil {
|
|
return fmt.Errorf("apply source: %w", err)
|
|
}
|
|
|
|
destExp, err := r.applyNetwork(pair.Destination, nil, false)
|
|
if err != nil {
|
|
r.dropNetworkMatch(sourceExp)
|
|
return fmt.Errorf("apply destination: %w", err)
|
|
}
|
|
|
|
op := expr.CmpOpEq
|
|
if pair.Inverse {
|
|
op = expr.CmpOpNeq
|
|
}
|
|
|
|
exprs := []expr.Any{
|
|
&expr.Meta{
|
|
Key: expr.MetaKeyIIFNAME,
|
|
Register: 1,
|
|
},
|
|
&expr.Cmp{
|
|
Op: op,
|
|
Register: 1,
|
|
Data: ifname(r.wgIface.Name()),
|
|
},
|
|
}
|
|
// We only care about NEW connections to mark them and later identify them in the postrouting chain for masquerading.
|
|
// Masquerading will take care of the conntrack state, which means we won't need to mark established connections.
|
|
exprs = append(exprs, getCtNewExprs()...)
|
|
|
|
exprs = append(exprs, sourceExp...)
|
|
exprs = append(exprs, destExp...)
|
|
|
|
var markValue uint32 = nbnet.PreroutingFwmarkMasquerade
|
|
if pair.Inverse {
|
|
markValue = nbnet.PreroutingFwmarkMasqueradeReturn
|
|
}
|
|
|
|
exprs = append(exprs,
|
|
&expr.Immediate{
|
|
Register: 1,
|
|
Data: binaryutil.NativeEndian.PutUint32(markValue),
|
|
},
|
|
&expr.Meta{
|
|
Key: expr.MetaKeyMARK,
|
|
SourceRegister: true,
|
|
Register: 1,
|
|
},
|
|
)
|
|
|
|
ruleID := pair.GenKey(firewall.PreroutingFormat)
|
|
|
|
if _, exists := r.rules[ruleID]; exists {
|
|
if err := r.removeNatRule(pair); err != nil {
|
|
r.dropNetworkMatch(sourceExp)
|
|
r.dropNetworkMatch(destExp)
|
|
return fmt.Errorf("remove prerouting rule: %w", err)
|
|
}
|
|
}
|
|
|
|
// Ensure nat rules come first, so the mark can be overwritten.
|
|
// Currently overwritten by the dst-type LOCAL rules for redirected traffic.
|
|
r.rules[ruleID] = r.conn.InsertRule(&nftables.Rule{
|
|
Table: r.workTable,
|
|
Chain: r.chains[chainNameManglePrerouting],
|
|
Exprs: exprs,
|
|
UserData: []byte(ruleID),
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *family) addPostroutingRules() {
|
|
// First masquerade rule for traffic coming in from WireGuard interface
|
|
exprs := []expr.Any{
|
|
// Match on the first fwmark
|
|
&expr.Meta{
|
|
Key: expr.MetaKeyMARK,
|
|
Register: 1,
|
|
},
|
|
&expr.Cmp{
|
|
Op: expr.CmpOpEq,
|
|
Register: 1,
|
|
Data: binaryutil.NativeEndian.PutUint32(nbnet.PreroutingFwmarkMasquerade),
|
|
},
|
|
|
|
// We need to exclude the loopback interface as this changes the ebpf proxy port
|
|
&expr.Meta{
|
|
Key: expr.MetaKeyOIFNAME,
|
|
Register: 1,
|
|
},
|
|
&expr.Cmp{
|
|
Op: expr.CmpOpNeq,
|
|
Register: 1,
|
|
Data: ifname("lo"),
|
|
},
|
|
&expr.Counter{},
|
|
&expr.Masq{},
|
|
}
|
|
|
|
r.conn.AddRule(&nftables.Rule{
|
|
Table: r.workTable,
|
|
Chain: r.chains[chainNameRoutingNat],
|
|
Exprs: exprs,
|
|
})
|
|
|
|
// Second masquerade rule for traffic going out through WireGuard interface
|
|
exprs2 := []expr.Any{
|
|
// Match on the second fwmark
|
|
&expr.Meta{
|
|
Key: expr.MetaKeyMARK,
|
|
Register: 1,
|
|
},
|
|
&expr.Cmp{
|
|
Op: expr.CmpOpEq,
|
|
Register: 1,
|
|
Data: binaryutil.NativeEndian.PutUint32(nbnet.PreroutingFwmarkMasqueradeReturn),
|
|
},
|
|
|
|
// Match WireGuard interface
|
|
&expr.Meta{
|
|
Key: expr.MetaKeyOIFNAME,
|
|
Register: 1,
|
|
},
|
|
&expr.Cmp{
|
|
Op: expr.CmpOpEq,
|
|
Register: 1,
|
|
Data: ifname(r.wgIface.Name()),
|
|
},
|
|
&expr.Counter{},
|
|
&expr.Masq{},
|
|
}
|
|
|
|
r.conn.AddRule(&nftables.Rule{
|
|
Table: r.workTable,
|
|
Chain: r.chains[chainNameRoutingNat],
|
|
Exprs: exprs2,
|
|
})
|
|
}
|
|
|
|
// addMSSClampingRules adds MSS clamping rules to prevent fragmentation for forwarded traffic.
|
|
func (r *family) addMSSClampingRules() error {
|
|
overhead := uint16(ipv4TCPHeaderSize)
|
|
if r.af.tableFamily == nftables.TableFamilyIPv6 {
|
|
overhead = ipv6TCPHeaderSize
|
|
}
|
|
if r.mtu <= overhead {
|
|
log.Debugf("MTU %d too small for MSS clamping (overhead %d), skipping", r.mtu, overhead)
|
|
return nil
|
|
}
|
|
mss := r.mtu - overhead
|
|
|
|
exprsOut := []expr.Any{
|
|
&expr.Meta{
|
|
Key: expr.MetaKeyOIFNAME,
|
|
Register: 1,
|
|
},
|
|
&expr.Cmp{
|
|
Op: expr.CmpOpEq,
|
|
Register: 1,
|
|
Data: ifname(r.wgIface.Name()),
|
|
},
|
|
&expr.Meta{
|
|
Key: expr.MetaKeyL4PROTO,
|
|
Register: 1,
|
|
},
|
|
&expr.Cmp{
|
|
Op: expr.CmpOpEq,
|
|
Register: 1,
|
|
Data: []byte{unix.IPPROTO_TCP},
|
|
},
|
|
&expr.Payload{
|
|
DestRegister: 1,
|
|
Base: expr.PayloadBaseTransportHeader,
|
|
Offset: 13,
|
|
Len: 1,
|
|
},
|
|
&expr.Bitwise{
|
|
DestRegister: 1,
|
|
SourceRegister: 1,
|
|
Len: 1,
|
|
Mask: []byte{0x02},
|
|
Xor: []byte{0x00},
|
|
},
|
|
&expr.Cmp{
|
|
Op: expr.CmpOpNeq,
|
|
Register: 1,
|
|
Data: []byte{0x00},
|
|
},
|
|
&expr.Counter{},
|
|
&expr.Exthdr{
|
|
DestRegister: 1,
|
|
Type: 2,
|
|
Offset: 2,
|
|
Len: 2,
|
|
Op: expr.ExthdrOpTcpopt,
|
|
},
|
|
&expr.Cmp{
|
|
Op: expr.CmpOpGt,
|
|
Register: 1,
|
|
Data: binaryutil.BigEndian.PutUint16(uint16(mss)),
|
|
},
|
|
&expr.Immediate{
|
|
Register: 1,
|
|
Data: binaryutil.BigEndian.PutUint16(uint16(mss)),
|
|
},
|
|
&expr.Exthdr{
|
|
SourceRegister: 1,
|
|
Type: 2,
|
|
Offset: 2,
|
|
Len: 2,
|
|
Op: expr.ExthdrOpTcpopt,
|
|
},
|
|
}
|
|
|
|
r.conn.AddRule(&nftables.Rule{
|
|
Table: r.workTable,
|
|
Chain: r.chains[chainNameMangleForward],
|
|
Exprs: exprsOut,
|
|
})
|
|
|
|
return r.conn.Flush()
|
|
}
|
|
|
|
func (r *family) addLegacyRouteRule(pair firewall.RouterPair) error {
|
|
sourceExp, err := r.applyNetwork(pair.Source, nil, true)
|
|
if err != nil {
|
|
return fmt.Errorf("apply source: %w", err)
|
|
}
|
|
|
|
destExp, err := r.applyNetwork(pair.Destination, nil, false)
|
|
if err != nil {
|
|
r.dropNetworkMatch(sourceExp)
|
|
return fmt.Errorf("apply destination: %w", err)
|
|
}
|
|
|
|
var exprs []expr.Any
|
|
exprs = append(exprs, sourceExp...)
|
|
exprs = append(exprs, destExp...)
|
|
exprs = append(exprs,
|
|
&expr.Counter{},
|
|
&expr.Verdict{Kind: expr.VerdictAccept},
|
|
)
|
|
|
|
ruleID := pair.GenKey(firewall.ForwardingFormat)
|
|
|
|
if _, exists := r.rules[ruleID]; exists {
|
|
if err := r.removeLegacyRouteRule(pair); err != nil {
|
|
r.dropNetworkMatch(sourceExp)
|
|
r.dropNetworkMatch(destExp)
|
|
return fmt.Errorf("remove legacy routing rule: %w", err)
|
|
}
|
|
}
|
|
|
|
r.rules[ruleID] = r.conn.AddRule(&nftables.Rule{
|
|
Table: r.workTable,
|
|
Chain: r.chains[chainNameRoutingFw],
|
|
Exprs: exprs,
|
|
UserData: []byte(ruleID),
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// removeLegacyRouteRule removes a legacy routing rule for mgmt servers pre route acls
|
|
func (r *family) removeLegacyRouteRule(pair firewall.RouterPair) error {
|
|
ruleID := pair.GenKey(firewall.ForwardingFormat)
|
|
|
|
rule, exists := r.rules[ruleID]
|
|
if !exists {
|
|
return nil
|
|
}
|
|
|
|
return r.deleteLegacyRuleEntry(ruleID, rule)
|
|
}
|
|
|
|
// deleteLegacyRuleEntry removes one legacy forwarding rule and drops its
|
|
// ipset references. It also clears stale entries that never got a handle.
|
|
func (r *family) deleteLegacyRuleEntry(ruleID firewall.RuleID, rule *nftables.Rule) error {
|
|
if rule.Handle == 0 {
|
|
log.Warnf("legacy forwarding rule %s has no handle, removing stale entry", ruleID)
|
|
if err := r.decrementSetCounter(rule); err != nil {
|
|
log.Warnf("decrement set counter for stale rule %s: %v", ruleID, err)
|
|
}
|
|
delete(r.rules, ruleID)
|
|
return nil
|
|
}
|
|
|
|
if err := r.conn.DelRule(rule); err != nil {
|
|
return fmt.Errorf("remove legacy forwarding rule %s: %w", ruleID, err)
|
|
}
|
|
|
|
delete(r.rules, ruleID)
|
|
|
|
if err := r.decrementSetCounter(rule); err != nil {
|
|
return fmt.Errorf("decrement set counter: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetLegacyManagement returns the route manager's legacy management mode
|
|
func (r *family) GetLegacyManagement() bool {
|
|
return r.legacyManagement
|
|
}
|
|
|
|
// SetLegacyManagement sets the route manager to use legacy management mode
|
|
func (r *family) SetLegacyManagement(isLegacy bool) {
|
|
r.legacyManagement = isLegacy
|
|
}
|
|
|
|
// RemoveAllLegacyRouteRules removes all legacy routing rules for mgmt servers pre route acls
|
|
func (r *family) RemoveAllLegacyRouteRules() error {
|
|
if err := r.refreshRulesMap(); err != nil {
|
|
return fmt.Errorf(refreshRulesMapError, err)
|
|
}
|
|
|
|
var merr *multierror.Error
|
|
for k, rule := range r.rules {
|
|
if !strings.HasPrefix(string(k), firewall.ForwardingFormatPrefix) {
|
|
continue
|
|
}
|
|
if err := r.deleteLegacyRuleEntry(k, rule); err != nil {
|
|
merr = multierror.Append(merr, err)
|
|
}
|
|
}
|
|
return nberrors.FormatErrorOrNil(merr)
|
|
}
|
|
|
|
func (r *family) removeNatPreroutingRules() error {
|
|
table := &nftables.Table{
|
|
Name: tableNat,
|
|
Family: r.af.tableFamily,
|
|
}
|
|
chain := &nftables.Chain{
|
|
Name: chainNameNatPrerouting,
|
|
Table: table,
|
|
Hooknum: nftables.ChainHookPrerouting,
|
|
Priority: nftables.ChainPriorityNATDest,
|
|
Type: nftables.ChainTypeNAT,
|
|
}
|
|
rules, err := r.conn.GetRules(table, chain)
|
|
if err != nil {
|
|
return fmt.Errorf("get rules from nat table: %w", err)
|
|
}
|
|
|
|
var merr *multierror.Error
|
|
|
|
// Delete rules that have our UserData suffix
|
|
for _, rule := range rules {
|
|
if len(rule.UserData) == 0 || !strings.HasSuffix(string(rule.UserData), string(dnatSuffix)) {
|
|
continue
|
|
}
|
|
if err := r.conn.DelRule(rule); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("delete rule %s: %w", rule.UserData, err))
|
|
}
|
|
}
|
|
|
|
if err := r.conn.Flush(); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf(flushError, err))
|
|
}
|
|
return nberrors.FormatErrorOrNil(merr)
|
|
}
|
|
|
|
func (r *family) RemoveNatRule(pair firewall.RouterPair) error {
|
|
if err := r.refreshRulesMap(); err != nil {
|
|
return fmt.Errorf(refreshRulesMapError, err)
|
|
}
|
|
|
|
var merr *multierror.Error
|
|
|
|
if pair.Masquerade {
|
|
if err := r.removeNatRule(pair); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("remove prerouting rule: %w", err))
|
|
}
|
|
|
|
if err := r.removeNatRule(firewall.GetInversePair(pair)); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("remove inverse prerouting rule: %w", err))
|
|
}
|
|
}
|
|
|
|
if err := r.removeLegacyRouteRule(pair); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("remove legacy routing rule: %w", err))
|
|
}
|
|
|
|
// Set counters are decremented in the sub-methods above before flush. If flush fails,
|
|
// counters will be off until the next successful removal or refresh cycle.
|
|
if err := r.conn.Flush(); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("flush remove nat rules %s: %w", pair.Destination, err))
|
|
}
|
|
|
|
return nberrors.FormatErrorOrNil(merr)
|
|
}
|
|
|
|
func (r *family) removeNatRule(pair firewall.RouterPair) error {
|
|
ruleID := pair.GenKey(firewall.PreroutingFormat)
|
|
|
|
rule, exists := r.rules[ruleID]
|
|
if !exists {
|
|
log.Debugf("prerouting rule %s not found", ruleID)
|
|
return nil
|
|
}
|
|
|
|
if rule.Handle == 0 {
|
|
log.Warnf("prerouting rule %s has no handle, removing stale entry", ruleID)
|
|
if err := r.decrementSetCounter(rule); err != nil {
|
|
log.Warnf("decrement set counter for stale rule %s: %v", ruleID, err)
|
|
}
|
|
delete(r.rules, ruleID)
|
|
return nil
|
|
}
|
|
|
|
if err := r.conn.DelRule(rule); err != nil {
|
|
return fmt.Errorf("remove prerouting rule %s -> %s: %w", pair.Source, pair.Destination, err)
|
|
}
|
|
|
|
log.Debugf("removed prerouting rule %s -> %s", pair.Source, pair.Destination)
|
|
|
|
delete(r.rules, ruleID)
|
|
|
|
if err := r.decrementSetCounter(rule); err != nil {
|
|
return fmt.Errorf("decrement set counter: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|