Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 88 additions & 8 deletions client/firewall/iptables/router_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,16 +259,25 @@ func (r *router) AddNatRule(pair firewall.RouterPair) error {
}
}

if !pair.Masquerade {
return nil
}
if pair.Masquerade {
if err := r.addNatRule(pair); err != nil {
return fmt.Errorf("add nat rule: %w", err)
}

if err := r.addNatRule(pair); err != nil {
return fmt.Errorf("add nat rule: %w", err)
}
if err := r.addNatRule(firewall.GetInversePair(pair)); err != nil {
return fmt.Errorf("add inverse nat rule: %w", err)
}
} else {
// Insert RETURN rules at the head of the postrouting NAT chain for both directions,
// preventing the exit node's catch-all masquerade from rewriting the source IP
// for routes with masquerade disabled.
if err := r.addNoMasqPostRoutingRule(pair); err != nil {
return fmt.Errorf("add no-masquerade postrouting rule: %w", err)
}

if err := r.addNatRule(firewall.GetInversePair(pair)); err != nil {
return fmt.Errorf("add inverse nat rule: %w", err)
if err := r.addNoMasqPostRoutingRule(firewall.GetInversePair(pair)); err != nil {
return fmt.Errorf("add inverse no-masquerade postrouting rule: %w", err)
}
}

r.updateState()
Expand All @@ -286,6 +295,14 @@ func (r *router) RemoveNatRule(pair firewall.RouterPair) error {
if err := r.removeNatRule(firewall.GetInversePair(pair)); err != nil {
return fmt.Errorf("remove inverse nat rule: %w", err)
}
} else {
if err := r.removeNoMasqPostRoutingRule(pair); err != nil {
return err
}

if err := r.removeNoMasqPostRoutingRule(firewall.GetInversePair(pair)); err != nil {
return err
}
}

if err := r.removeLegacyRouteRule(pair); err != nil {
Expand Down Expand Up @@ -502,6 +519,69 @@ func (r *router) cleanupDataPlaneMark() error {
return nberrors.FormatErrorOrNil(merr)
}

// addNoMasqPostRoutingRule inserts a RETURN rule at position 1 of the postrouting
// NAT chain for the given destination. The mark match scopes the rule to exit-node
// traffic only, ensuring routes with masquerade=false are not masqueraded by the
// exit node's catch-all mark-based masquerade rule.
func (r *router) addNoMasqPostRoutingRule(pair firewall.RouterPair) error {
ruleKey := firewall.GenKey(firewall.NoMasqPostRoutingFormat, pair)
if rule, exists := r.rules[ruleKey]; exists {
if err := r.iptablesClient.DeleteIfExists(tableNat, chainRTNAT, rule...); err != nil {
return fmt.Errorf("remove existing no-masq rule before reinstall: %w", err)
}
if err := r.decrementSetCounter(rule); err != nil {
log.Warnf("decrement set counter for reinstalled rule %s: %v", ruleKey, err)
}
delete(r.rules, ruleKey)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

destExp, err := r.applyNetwork("-d", pair.Destination, nil)
if err != nil {
return fmt.Errorf("apply destination: %w", err)
}

// Select the correct fwmark based on traffic direction:
// forward (wt0→eth0) uses PreroutingFwmarkMasquerade,
// inverse (eth0→wt0) uses PreroutingFwmarkMasqueradeReturn.
markValue := uint32(nbnet.PreroutingFwmarkMasquerade)
if pair.Inverse {
markValue = nbnet.PreroutingFwmarkMasqueradeReturn
}

// Match exit-node mark + destination, then RETURN to skip the blanket masquerade.
rule := []string{"-m", "mark", "--mark", fmt.Sprintf("%#x", markValue)}
rule = append(rule, destExp...)
rule = append(rule, "-j", "RETURN")

if err := r.iptablesClient.Insert(tableNat, chainRTNAT, 1, rule...); err != nil {
return fmt.Errorf("add no-masquerade return rule for %s: %w", pair.Destination, err)
}

r.rules[ruleKey] = rule
return nil
}

func (r *router) removeNoMasqPostRoutingRule(pair firewall.RouterPair) error {
ruleKey := firewall.GenKey(firewall.NoMasqPostRoutingFormat, pair)

rule, exists := r.rules[ruleKey]
if !exists {
log.Debugf("no-masquerade postrouting rule %s not found", ruleKey)
return nil
}

if err := r.iptablesClient.DeleteIfExists(tableNat, chainRTNAT, rule...); err != nil {
return fmt.Errorf("remove no-masquerade return rule %s: %w", ruleKey, err)
}
delete(r.rules, ruleKey)

if err := r.decrementSetCounter(rule); err != nil {
return fmt.Errorf("decrement ipset counter for %s: %w", ruleKey, err)
}

return nil
}

func (r *router) addPostroutingRules() error {
// First rule for outbound masquerade
rule1 := []string{
Expand Down
9 changes: 5 additions & 4 deletions client/firewall/manager/firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import (
)

const (
ForwardingFormatPrefix = "netbird-fwd-"
ForwardingFormat = "netbird-fwd-%s-%t"
PreroutingFormat = "netbird-prerouting-%s-%t"
NatFormat = "netbird-nat-%s-%t"
ForwardingFormatPrefix = "netbird-fwd-"
ForwardingFormat = "netbird-fwd-%s-%t"
PreroutingFormat = "netbird-prerouting-%s-%t"
NatFormat = "netbird-nat-%s-%t"
NoMasqPostRoutingFormat = "netbird-no-masq-postrouting-%s-%t"
)

// Rule abstraction should be implemented by each firewall manager
Expand Down
102 changes: 102 additions & 0 deletions client/firewall/nftables/router_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,19 @@ func (r *router) AddNatRule(pair firewall.RouterPair) error {
if err := r.addNatRule(firewall.GetInversePair(pair)); err != nil {
return fmt.Errorf("add inverse nat rule: %w", err)
}
} else {
// Insert return verdicts in the postrouting NAT chain for both traffic directions.
// This prevents the exit node's catch-all masquerade rules from rewriting the
// source IP for routes that explicitly have masquerade disabled.
// Forward direction: wt0→eth0 traffic marked with PreroutingFwmarkMasquerade.
// Inverse direction: eth0→wt0 traffic marked with PreroutingFwmarkMasqueradeReturn.
if err := r.addNoMasqPostRoutingRule(pair); err != nil {
return fmt.Errorf("add no-masquerade postrouting rule: %w", err)
}

if err := r.addNoMasqPostRoutingRule(firewall.GetInversePair(pair)); err != nil {
return fmt.Errorf("add inverse no-masquerade postrouting rule: %w", err)
}
}

if err := r.conn.Flush(); err != nil {
Expand All @@ -678,6 +691,8 @@ func (r *router) rollbackRules(pair firewall.RouterPair) {
firewall.GenKey(firewall.ForwardingFormat, pair),
firewall.GenKey(firewall.PreroutingFormat, pair),
firewall.GenKey(firewall.PreroutingFormat, firewall.GetInversePair(pair)),
firewall.GenKey(firewall.NoMasqPostRoutingFormat, pair),
firewall.GenKey(firewall.NoMasqPostRoutingFormat, firewall.GetInversePair(pair)),
}
for _, key := range keys {
rule, ok := r.rules[key]
Expand Down Expand Up @@ -763,6 +778,56 @@ func (r *router) addNatRule(pair firewall.RouterPair) error {
return nil
}

// addNoMasqPostRoutingRule inserts a return verdict at the head of the postrouting
// NAT chain for the given destination. This ensures that when an exit node
// (0.0.0.0/0, masquerade=true) is active alongside routes with masquerade=false,
// the exit node's catch-all mark rule does not cause those destinations to be
// masqueraded. The mark match scopes the rule to exit-node traffic only.
// InsertRule places it at chain position 0, before the blanket masquerade rule.
func (r *router) addNoMasqPostRoutingRule(pair firewall.RouterPair) error {
ruleKey := firewall.GenKey(firewall.NoMasqPostRoutingFormat, pair)
if _, exists := r.rules[ruleKey]; exists {
if err := r.removeNoMasqPostRoutingRule(pair); err != nil {
return fmt.Errorf("remove existing no-masq rule before reinstall: %w", err)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

destExp, err := r.applyNetwork(pair.Destination, nil, false)
if err != nil {
return fmt.Errorf("apply destination: %w", err)
}

// Select the correct fwmark based on traffic direction:
// forward (wt0→eth0) uses PreroutingFwmarkMasquerade,
// inverse (eth0→wt0) uses PreroutingFwmarkMasqueradeReturn.
markValue := uint32(nbnet.PreroutingFwmarkMasquerade)
if pair.Inverse {
markValue = nbnet.PreroutingFwmarkMasqueradeReturn
}

// Match only packets carrying the exit-node masquerade mark, then match the
// destination, then return — skipping the blanket masquerade rule below.
exprs := []expr.Any{
&expr.Meta{Key: expr.MetaKeyMARK, Register: 1},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: binaryutil.NativeEndian.PutUint32(markValue),
},
}
exprs = append(exprs, destExp...)
exprs = append(exprs, &expr.Verdict{Kind: expr.VerdictReturn})

r.rules[ruleKey] = r.conn.InsertRule(&nftables.Rule{
Table: r.workTable,
Chain: r.chains[chainNameRoutingNat],
Exprs: exprs,
UserData: []byte(ruleKey),
})

return nil
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// addPostroutingRules adds the masquerade rules
func (r *router) addPostroutingRules() {
// First masquerade rule for traffic coming in from WireGuard interface
Expand Down Expand Up @@ -1375,6 +1440,14 @@ func (r *router) RemoveNatRule(pair firewall.RouterPair) error {
if err := r.removeNatRule(firewall.GetInversePair(pair)); err != nil {
merr = multierror.Append(merr, fmt.Errorf("remove inverse prerouting rule: %w", err))
}
} else {
if err := r.removeNoMasqPostRoutingRule(pair); err != nil {
merr = multierror.Append(merr, err)
}

if err := r.removeNoMasqPostRoutingRule(firewall.GetInversePair(pair)); err != nil {
merr = multierror.Append(merr, err)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

if err := r.removeLegacyRouteRule(pair); err != nil {
Expand Down Expand Up @@ -1423,6 +1496,35 @@ func (r *router) removeNatRule(pair firewall.RouterPair) error {
return nil
}

func (r *router) removeNoMasqPostRoutingRule(pair firewall.RouterPair) error {
ruleKey := firewall.GenKey(firewall.NoMasqPostRoutingFormat, pair)

rule, exists := r.rules[ruleKey]
if !exists {
log.Debugf("no-masquerade postrouting rule %s not found", ruleKey)
return nil
}

if rule.Handle == 0 {
log.Warnf("no-masquerade postrouting rule %s has no handle, removing stale entry", ruleKey)
if err := r.decrementSetCounter(rule); err != nil {
log.Warnf("decrement set counter for stale no-masq rule %s: %v", ruleKey, err)
}
delete(r.rules, ruleKey)
return nil
}

if err := r.deleteNftRule(rule, ruleKey); err != nil {
return fmt.Errorf("remove no-masquerade postrouting rule: %w", err)
}

if err := r.decrementSetCounter(rule); err != nil {
return fmt.Errorf("decrement set counter for no-masq rule: %w", err)
}

return nil
}

// refreshRulesMap rebuilds the rule map from the kernel. This removes stale entries
// (e.g. from failed flushes) and updates handles for all existing rules.
func (r *router) refreshRulesMap() error {
Expand Down
Loading