Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
61 changes: 52 additions & 9 deletions client/firewall/iptables/router_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,16 +259,21 @@ func (r *router) AddNatRule(pair firewall.RouterPair) error {
}
}

if !pair.Masquerade {
return nil
}

if err := r.addNatRule(pair); err != nil {
return fmt.Errorf("add nat rule: %w", err)
}
if pair.Masquerade {
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)
if err := r.addNatRule(firewall.GetInversePair(pair)); err != nil {
return fmt.Errorf("add inverse nat rule: %w", err)
}
} else {
// Insert a RETURN rule at the head of the postrouting NAT chain for this
// destination, 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)
}
}

r.updateState()
Expand All @@ -286,6 +291,19 @@ 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 {
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 no-masquerade return rule: %w", err)
}
delete(r.rules, ruleKey)
if err := r.decrementSetCounter(rule); err != nil {
return fmt.Errorf("decrement ipset counter: %w", err)
}
} else {
log.Debugf("no-masquerade postrouting rule %s not found", ruleKey)
}
}

if err := r.removeLegacyRouteRule(pair); err != nil {
Expand Down Expand Up @@ -502,6 +520,31 @@ 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. This ensures 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 _, exists := r.rules[ruleKey]; exists {
return nil
}
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)
}

rule := append(destExp, "-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
r.updateState()
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
51 changes: 51 additions & 0 deletions client/firewall/nftables/router_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,14 @@ 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 a return verdict in the postrouting NAT chain for this destination.
// This prevents the exit node's catch-all masquerade rule (which marks all wt0
// traffic with PreroutingFwmarkMasquerade) from rewriting the source IP for
// routes that explicitly have masquerade disabled.
if err := r.addNoMasqPostRoutingRule(pair); err != nil {
return fmt.Errorf("add no-masquerade postrouting rule: %w", err)
}
}

if err := r.conn.Flush(); err != nil {
Expand Down Expand Up @@ -763,6 +771,35 @@ 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 return fires before the blanket masquerade rule because
// InsertRule places it at chain position 0.
func (r *router) addNoMasqPostRoutingRule(pair firewall.RouterPair) error {
destExp, err := r.applyNetwork(pair.Destination, nil, false)
if err != nil {
return fmt.Errorf("apply destination: %w", err)
}

exprs := append(destExp, &expr.Verdict{Kind: expr.VerdictReturn})

ruleKey := firewall.GenKey(firewall.NoMasqPostRoutingFormat, pair)
if _, exists := r.rules[ruleKey]; exists {
return nil
}

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 +1412,20 @@ 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 {
ruleKey := firewall.GenKey(firewall.NoMasqPostRoutingFormat, pair)
if rule, exists := r.rules[ruleKey]; exists {
if err := r.conn.DelRule(rule); err != nil {
merr = multierror.Append(merr, fmt.Errorf("remove no-masquerade postrouting rule: %w", err))
} else {
delete(r.rules, ruleKey)
if err := r.decrementSetCounter(rule); err != nil {
merr = multierror.Append(merr, fmt.Errorf("decrement set counter for no-masq rule: %w", err))
}
}
} else {
log.Debugf("no-masquerade postrouting rule %s not found", ruleKey)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

if err := r.removeLegacyRouteRule(pair); err != nil {
Expand Down