Keep set references and rule tracking consistent when a routing rule fails

This commit is contained in:
Viktor Liu
2026-08-21 12:32:36 +02:00
parent dd54240f42
commit 4b70772a4a
3 changed files with 55 additions and 3 deletions
+45 -3
View File
@@ -322,6 +322,49 @@ func TestIptablesFilterDestinationSetRequiresIPSet(t *testing.T) {
require.ErrorContains(t, err, "requires ipset") require.ErrorContains(t, err, "requires ipset")
} }
// TestIptablesNatRuleDropsSourceSetOnDestinationFailure covers a marking rule
// whose source set is created but whose destination set is not: the source
// reference has to go back, or the set it created stays in the kernel with a
// count nothing will ever drop.
func TestIptablesNatRuleDropsSourceSetOnDestinationFailure(t *testing.T) {
manager, err := Create(ifaceMock, iface.DefaultMTU)
require.NoError(t, err)
require.NoError(t, manager.Init(nil))
defer func() {
require.NoError(t, manager.Close(nil))
}()
sourceSet := fw.NewPrefixSet([]netip.Prefix{
netip.MustParsePrefix("100.0.0.0/16"),
netip.MustParsePrefix("10.10.0.0/16"),
})
destSet := fw.NewDomainSet(domain.List{"example.org"})
// Poison the destination set's name so its hash:net creation fails after
// the source set has already been created.
poisoned := manager.family4.ipsetName(destSet.HashedName())
require.NoError(t, ipset.Create(poisoned, ipset.TypeHashIP, ipset.CreateOptions{}))
t.Cleanup(func() {
if err := ipset.Destroy(poisoned); err != nil {
t.Logf("destroy poisoned set %s: %v", poisoned, err)
}
})
pair := fw.RouterPair{
ID: "nat-source-set-test",
Source: fw.Network{Set: sourceSet},
Destination: fw.Network{Set: destSet},
Masquerade: true,
Dynamic: true,
}
require.Error(t, manager.AddNatRule(pair), "the destination set must fail to be created")
_, ok := manager.family4.ipsetCounter.Get(manager.family4.ipsetName(sourceSet.HashedName()))
require.False(t, ok, "the source set reference must be released")
}
// TestIptablesNatRuleReAddKeepsSetReferences re-adds the same NAT rule the way // TestIptablesNatRuleReAddKeepsSetReferences re-adds the same NAT rule the way
// a repeated network-map update does. The marking rule's set references must not // a repeated network-map update does. The marking rule's set references must not
// grow, or RemoveNatRule can never drop the count to zero and the set stays in // grow, or RemoveNatRule can never drop the count to zero and the set stays in
@@ -461,13 +504,12 @@ func TestIptablesCloseRemovesAllState(t *testing.T) {
TranslatedAddress: netip.MustParseAddr("10.20.0.44"), TranslatedAddress: netip.MustParseAddr("10.20.0.44"),
TranslatedPort: fw.Port{Values: []uint16{80}}, TranslatedPort: fw.Port{Values: []uint16{80}},
} }
dnatRule, err := manager.AddDNATRule(dnat) _, err = manager.AddDNATRule(dnat)
require.NoError(t, err, "add dnat rule") require.NoError(t, err, "add dnat rule")
require.NotEqual(t, before, snapshotIptables(t, ipv4Client), "the manager must have installed state") require.NotEqual(t, before, snapshotIptables(t, ipv4Client), "the manager must have installed state")
require.NoError(t, manager.DeleteDNATRule(dnatRule), "delete dnat rule") // Everything above stays in place, so Close is what has to remove it.
require.NoError(t, manager.DisableRouting(), "disable routing")
require.NoError(t, manager.Close(nil), "close") require.NoError(t, manager.Close(nil), "close")
after := snapshotIptables(t, ipv4Client) after := snapshotIptables(t, ipv4Client)
@@ -231,6 +231,7 @@ func (r *family) addNatRule(pair firewall.RouterPair) (err error) {
} }
destExp, err := r.applyNetwork("-d", pair.Destination, nil) destExp, err := r.applyNetwork("-d", pair.Destination, nil)
if err != nil { if err != nil {
r.dropSourceMatch(sourceExp)
return fmt.Errorf("apply network -d: %w", err) return fmt.Errorf("apply network -d: %w", err)
} }
@@ -158,7 +158,13 @@ func (r *family) queueNatRule(pair firewall.RouterPair, exprs []expr.Any) {
if _, exists := r.rules[ruleID]; exists { if _, exists := r.rules[ruleID]; exists {
if err := r.removeNatRule(pair); err != nil { if err := r.removeNatRule(pair); err != nil {
// The rule this replaces may still be in the kernel. Keep tracking
// it and skip the new one: overwriting the entry would leave the old
// rule installed with nothing that can find it again, while keeping
// it lets the next update retry the whole replacement.
log.Errorf("replace prerouting rule %s: %v", ruleID, err) log.Errorf("replace prerouting rule %s: %v", ruleID, err)
r.dropNetworkMatch(exprs)
return
} }
} }
@@ -360,7 +366,10 @@ func (r *family) queueLegacyRouteRule(pair firewall.RouterPair, exprs []expr.Any
if _, exists := r.rules[ruleID]; exists { if _, exists := r.rules[ruleID]; exists {
if err := r.removeLegacyRouteRule(pair); err != nil { if err := r.removeLegacyRouteRule(pair); err != nil {
// Keep the old rule tracked instead of losing it, as in queueNatRule.
log.Errorf("replace legacy forwarding rule %s: %v", ruleID, err) log.Errorf("replace legacy forwarding rule %s: %v", ruleID, err)
r.dropNetworkMatch(exprs)
return
} }
} }