mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-16 19:49:56 +00:00
347 lines
11 KiB
Go
347 lines
11 KiB
Go
//go:build !android
|
|
|
|
package iptables
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/go-multierror"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
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) createContainers() error {
|
|
for _, chainInfo := range []struct {
|
|
chain string
|
|
table string
|
|
}{
|
|
{chainRTFwdIn, tableFilter},
|
|
{chainRTFwdOut, tableFilter},
|
|
{chainRTPre, tableMangle},
|
|
{chainRTNAT, tableNat},
|
|
{chainRTRdr, tableNat},
|
|
{chainRTMSSClamp, tableMangle},
|
|
} {
|
|
// Fallback: clear chains that survived an unclean shutdown.
|
|
if ok, _ := r.iptablesClient.ChainExists(chainInfo.table, chainInfo.chain); ok {
|
|
if err := r.iptablesClient.ClearAndDeleteChain(chainInfo.table, chainInfo.chain); err != nil {
|
|
log.Warnf("clear stale chain %s in %s: %v", chainInfo.chain, chainInfo.table, err)
|
|
}
|
|
}
|
|
if err := r.iptablesClient.NewChain(chainInfo.table, chainInfo.chain); err != nil {
|
|
return fmt.Errorf("create chain %s in table %s: %w", chainInfo.chain, chainInfo.table, err)
|
|
}
|
|
}
|
|
|
|
if err := r.insertEstablishedRule(chainRTFwdIn); err != nil {
|
|
return fmt.Errorf("insert established rule: %w", err)
|
|
}
|
|
|
|
if err := r.insertEstablishedRule(chainRTFwdOut); err != nil {
|
|
return fmt.Errorf("insert established rule: %w", err)
|
|
}
|
|
|
|
if err := r.addPostroutingRules(); err != nil {
|
|
return fmt.Errorf("add static nat rules: %w", err)
|
|
}
|
|
|
|
if err := r.addJumpRules(); err != nil {
|
|
return fmt.Errorf("add jump rules: %w", err)
|
|
}
|
|
|
|
if err := r.addMSSClampingRules(); err != nil {
|
|
log.Errorf("failed to add MSS clamping rules: %s", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *family) addJumpRules() error {
|
|
// Jump to nat chain
|
|
natRule := jumpRuleSpec(chainRTNAT)
|
|
if err := r.iptablesClient.Insert(tableNat, chainPostrouting, 1, natRule...); err != nil {
|
|
return fmt.Errorf("add nat postrouting jump rule: %w", err)
|
|
}
|
|
r.rules[jumpNATPost] = natRule
|
|
|
|
// Jump to mangle prerouting chain
|
|
preRule := jumpRuleSpec(chainRTPre)
|
|
if err := r.iptablesClient.Insert(tableMangle, chainPrerouting, 1, preRule...); err != nil {
|
|
return fmt.Errorf("add mangle prerouting jump rule: %w", err)
|
|
}
|
|
r.rules[jumpManglePre] = preRule
|
|
|
|
// Jump to nat prerouting chain
|
|
rdrRule := jumpRuleSpec(chainRTRdr)
|
|
if err := r.iptablesClient.Insert(tableNat, chainPrerouting, 1, rdrRule...); err != nil {
|
|
return fmt.Errorf("add nat prerouting jump rule: %w", err)
|
|
}
|
|
r.rules[jumpNATPre] = rdrRule
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *family) setupDataPlaneMark() error {
|
|
var merr *multierror.Error
|
|
preRule := []string{
|
|
"-i", r.wgIface.Name(),
|
|
"-m", "conntrack", "--ctstate", "NEW",
|
|
"-j", "CONNMARK", "--set-mark", fmt.Sprintf("%#x", nbnet.DataPlaneMarkIn),
|
|
}
|
|
|
|
if err := r.iptablesClient.AppendUnique(tableMangle, chainPrerouting, preRule...); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("add mangle prerouting rule: %w", err))
|
|
} else {
|
|
r.rules[markManglePre] = preRule
|
|
}
|
|
|
|
postRule := []string{
|
|
"-o", r.wgIface.Name(),
|
|
"-m", "conntrack", "--ctstate", "NEW",
|
|
"-j", "CONNMARK", "--set-mark", fmt.Sprintf("%#x", nbnet.DataPlaneMarkOut),
|
|
}
|
|
|
|
if err := r.iptablesClient.AppendUnique(tableMangle, chainPostrouting, postRule...); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("add mangle postrouting rule: %w", err))
|
|
} else {
|
|
r.rules[markManglePost] = postRule
|
|
}
|
|
|
|
return nberrors.FormatErrorOrNil(merr)
|
|
}
|
|
|
|
// seedInitialEntries adds default rules to the entries map. Rules are
|
|
// inserted at position 1, so the order here is reversed.
|
|
//
|
|
// Existing FORWARD policy decides outbound traffic towards our
|
|
// interface. If FORWARD policy is "drop", we add an
|
|
// established/related rule to allow return traffic for inbound rules.
|
|
func (r *family) seedInitialEntries() {
|
|
established := getConntrackEstablished()
|
|
|
|
r.appendToEntries(chainInput, []string{"-i", r.wgIface.Name(), "-j", "DROP"})
|
|
r.appendToEntries(chainInput, []string{"-i", r.wgIface.Name(), "-j", chainACLInput})
|
|
r.appendToEntries(chainInput, append([]string{"-i", r.wgIface.Name()}, established...))
|
|
|
|
r.appendToEntries(chainForward, []string{"-i", r.wgIface.Name(), "-j", "DROP"})
|
|
r.appendToEntries(chainForward, []string{"-o", r.wgIface.Name(), "-j", chainRTFwdOut})
|
|
r.appendToEntries(chainForward, []string{"-i", r.wgIface.Name(), "-j", chainRTFwdIn})
|
|
|
|
// Mangle FORWARD guard: when external DNAT redirects traffic from
|
|
// the wg interface, it traverses FORWARD instead of INPUT,
|
|
// bypassing ACL rules. ACCEPT rules in filter FORWARD can be
|
|
// inserted above ours. Mangle runs before filter, so these guard
|
|
// rules enforce the ACL mark check where it cannot be overridden.
|
|
r.appendToEntries(mangleForwardKey, []string{
|
|
"-i", r.wgIface.Name(),
|
|
"-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED",
|
|
"-j", "ACCEPT",
|
|
})
|
|
r.appendToEntries(mangleForwardKey, []string{
|
|
"-i", r.wgIface.Name(),
|
|
"-m", "conntrack", "--ctstate", "DNAT",
|
|
"-m", "mark", "!", "--mark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkRedirected),
|
|
"-j", "DROP",
|
|
})
|
|
}
|
|
|
|
func (r *family) seedInitialOptionalEntries() {
|
|
r.optionalEntries[chainForward] = []entry{
|
|
{
|
|
spec: []string{"-m", "mark", "--mark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkRedirected), "-j", "ACCEPT"},
|
|
position: 2,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (r *family) appendToEntries(chain chainKey, spec ruleSpec) {
|
|
r.entries[chain] = append(r.entries[chain], spec)
|
|
}
|
|
|
|
func (r *family) createDefaultChains() error {
|
|
if err := r.iptablesClient.NewChain(tableFilter, chainACLInput); err != nil {
|
|
return fmt.Errorf("create %s chain: %w", chainACLInput, err)
|
|
}
|
|
|
|
for chain, rules := range r.entries {
|
|
// mangle FORWARD guard rules are handled separately below
|
|
if chain == mangleForwardKey {
|
|
continue
|
|
}
|
|
for _, rule := range rules {
|
|
if err := r.iptablesClient.InsertUnique(tableFilter, string(chain), 1, rule...); err != nil {
|
|
return fmt.Errorf("insert jump rule into %s: %w", chain, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
for chain, entries := range r.optionalEntries {
|
|
for _, entry := range entries {
|
|
if err := r.iptablesClient.InsertUnique(tableFilter, string(chain), entry.position, entry.spec...); err != nil {
|
|
log.Errorf("failed to insert optional entry %v: %v", entry.spec, err)
|
|
continue
|
|
}
|
|
r.entries[chain] = append(r.entries[chain], entry.spec)
|
|
}
|
|
}
|
|
clear(r.optionalEntries)
|
|
|
|
// Insert mangle FORWARD guard rules to prevent external DNAT bypass.
|
|
for _, rule := range r.entries[mangleForwardKey] {
|
|
if err := r.iptablesClient.AppendUnique(tableMangle, chainForward, rule...); err != nil {
|
|
log.Errorf("failed to add mangle FORWARD guard rule: %v", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *family) cleanUpDefaultForwardRules() error {
|
|
var merr *multierror.Error
|
|
|
|
// cleanJumpRules removes the OUTPUT jump to NETBIRD-NAT-OUTPUT among
|
|
// the others, so the chain below deletes cleanly instead of failing
|
|
// with "device or resource busy".
|
|
if err := r.cleanJumpRules(); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("clean jump rules: %w", err))
|
|
}
|
|
|
|
for _, chainInfo := range []struct {
|
|
chain string
|
|
table string
|
|
}{
|
|
{chainRTFwdIn, tableFilter},
|
|
{chainRTFwdOut, tableFilter},
|
|
{chainRTPre, tableMangle},
|
|
{chainRTNAT, tableNat},
|
|
{chainRTRdr, tableNat},
|
|
{chainNATOutput, tableNat},
|
|
{chainRTMSSClamp, tableMangle},
|
|
} {
|
|
ok, err := r.iptablesClient.ChainExists(chainInfo.table, chainInfo.chain)
|
|
if err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("check chain %s in table %s: %w", chainInfo.chain, chainInfo.table, err))
|
|
continue
|
|
}
|
|
if ok {
|
|
if err := r.iptablesClient.ClearAndDeleteChain(chainInfo.table, chainInfo.chain); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("clear and delete chain %s in table %s: %w", chainInfo.chain, chainInfo.table, err))
|
|
}
|
|
}
|
|
}
|
|
|
|
return nberrors.FormatErrorOrNil(merr)
|
|
}
|
|
|
|
func (r *family) cleanJumpRules() error {
|
|
// locations maps each jump rule to the built-in table and chain it
|
|
// was inserted into, plus the netbird chain it targets.
|
|
locations := map[firewall.RuleID]struct{ table, chain, target string }{
|
|
jumpNATPost: {tableNat, chainPostrouting, chainRTNAT},
|
|
jumpManglePre: {tableMangle, chainPrerouting, chainRTPre},
|
|
jumpNATPre: {tableNat, chainPrerouting, chainRTRdr},
|
|
jumpMSSClamp: {tableMangle, chainForward, chainRTMSSClamp},
|
|
jumpNATOutput: {tableNat, chainOutput, chainNATOutput},
|
|
}
|
|
|
|
var merr *multierror.Error
|
|
for ruleID, loc := range locations {
|
|
rule, exists := r.rules[ruleID]
|
|
if !exists {
|
|
// Untracked (e.g. fresh start after an unclean shutdown with no
|
|
// restored state): if the target chain survived, remove the stale
|
|
// jump to it so the chain can be deleted.
|
|
ok, err := r.iptablesClient.ChainExists(loc.table, loc.target)
|
|
if err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("check chain %s in table %s: %w", loc.target, loc.table, err))
|
|
continue
|
|
}
|
|
if !ok {
|
|
continue
|
|
}
|
|
rule = jumpRuleSpec(loc.target)
|
|
}
|
|
if err := r.iptablesClient.DeleteIfExists(loc.table, loc.chain, rule...); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("delete rule from chain %s in table %s: %w", loc.chain, loc.table, err))
|
|
continue
|
|
}
|
|
delete(r.rules, ruleID)
|
|
}
|
|
return nberrors.FormatErrorOrNil(merr)
|
|
}
|
|
|
|
// jumpRuleSpec builds the iptables rule spec that jumps to target. Create
|
|
// and cleanup sites share it so the installed and deleted specs cannot drift.
|
|
func jumpRuleSpec(target string) []string {
|
|
return []string{"-j", target}
|
|
}
|
|
|
|
func (r *family) cleanAclChains() error {
|
|
var merr *multierror.Error
|
|
|
|
if err := r.cleanInputAclChain(); err != nil {
|
|
merr = multierror.Append(merr, err)
|
|
}
|
|
|
|
for _, rule := range r.entries[mangleForwardKey] {
|
|
if err := r.iptablesClient.DeleteIfExists(tableMangle, chainForward, rule...); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("delete mangle %s guard rule %v: %w", chainForward, rule, err))
|
|
}
|
|
}
|
|
|
|
return nberrors.FormatErrorOrNil(merr)
|
|
}
|
|
|
|
func (r *family) cleanInputAclChain() error {
|
|
ok, err := r.iptablesClient.ChainExists(tableFilter, chainACLInput)
|
|
if err != nil {
|
|
return fmt.Errorf("check chain %s: %w", chainACLInput, err)
|
|
}
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
var merr *multierror.Error
|
|
for _, rule := range r.entries[chainInput] {
|
|
if err := r.iptablesClient.DeleteIfExists(tableFilter, chainInput, rule...); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("delete %s rule %v: %w", chainInput, rule, err))
|
|
}
|
|
}
|
|
|
|
for _, rule := range r.entries[chainForward] {
|
|
if err := r.iptablesClient.DeleteIfExists(tableFilter, chainForward, rule...); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("delete %s rule %v: %w", chainForward, rule, err))
|
|
}
|
|
}
|
|
|
|
if err := r.iptablesClient.ClearAndDeleteChain(tableFilter, chainACLInput); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("clear and delete %s chain: %w", chainACLInput, err))
|
|
}
|
|
|
|
return nberrors.FormatErrorOrNil(merr)
|
|
}
|
|
|
|
func (r *family) cleanupDataPlaneMark() error {
|
|
var merr *multierror.Error
|
|
if preRule, exists := r.rules[markManglePre]; exists {
|
|
if err := r.iptablesClient.DeleteIfExists(tableMangle, chainPrerouting, preRule...); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("remove mangle prerouting rule: %w", err))
|
|
} else {
|
|
delete(r.rules, markManglePre)
|
|
}
|
|
}
|
|
|
|
if postRule, exists := r.rules[markManglePost]; exists {
|
|
if err := r.iptablesClient.DeleteIfExists(tableMangle, chainPostrouting, postRule...); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("remove mangle postrouting rule: %w", err))
|
|
} else {
|
|
delete(r.rules, markManglePost)
|
|
}
|
|
}
|
|
|
|
return nberrors.FormatErrorOrNil(merr)
|
|
}
|