From 04322b699ebacc2823900aa72b70540eaeec6275 Mon Sep 17 00:00:00 2001 From: gyurix Date: Tue, 16 Jun 2026 00:28:13 +0200 Subject: [PATCH] feat(iptables): make rule insertions idempotent and robust Add lineExistsInContainer helper to check for existing rules before insertion, making InsertPreroutingRuleInContainer and InsertPostroutingMasqueradeInContainer idempotent. Change cleanup errors from fatal to warnings for better fault tolerance. --- network-go/iptables/iptables.go | 49 +++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/network-go/iptables/iptables.go b/network-go/iptables/iptables.go index 86f9896..c096dbb 100644 --- a/network-go/iptables/iptables.go +++ b/network-go/iptables/iptables.go @@ -397,13 +397,48 @@ func (m *Manager) DeleteForwardAccept(chain, comment string) error { return nil } +// lineExistsInContainer checks if a line matching the patterns exists inside a container namespace +func (m *Manager) lineExistsInContainer(pid int, table, chain string, patterns ...string) bool { + iptPath := m.binary + + nsenterArgs := []string{"-t", fmt.Sprintf("%d", pid), "-n", "--", iptPath, "-w", "-n", "-t", table, "-L", chain} + cmd := exec.Command("nsenter", nsenterArgs...) + output, err := cmd.Output() + if err != nil { + logger.Debug("IPTABLES: lineExistsInContainer list failed for PID %d chain %s: %v", pid, chain, err) + return false + } + + for _, line := range strings.Split(string(output), "\n") { + matchesAll := true + for _, pattern := range patterns { + if !strings.Contains(line, pattern) { + matchesAll = false + break + } + } + if matchesAll { + return true + } + } + return false +} + // InsertPreroutingRuleInContainer inserts a DNAT PREROUTING rule inside a container namespace func (m *Manager) InsertPreroutingRuleInContainer(pid int, sourceIP, proto, sourcePort, targetIP, targetPort, comment string) error { logger.Info("IPTABLES: inserting PREROUTING DNAT rule in container PID %d: src=%s proto=%s dport=%s -> %s:%s comment=%q", pid, sourceIP, proto, sourcePort, targetIP, targetPort, comment) + + // Idempotent check: if the rule already exists, skip + if m.lineExistsInContainer(pid, "nat", "PREROUTING", "DNAT", sourcePort, targetIP, comment) { + logger.Debug("IPTABLES: PREROUTING DNAT rule already exists in container PID %d (pattern=%s -> %s), skipping", pid, sourcePort, targetIP) + return nil + } + + // Clean up any stale/duplicate rules first patterns := []string{"DNAT", sourcePort, targetIP, targetPort, comment} if err := m.deleteMatchingLinesInContainer(pid, "nat", "PREROUTING", patterns...); err != nil { - return fmt.Errorf("failed to delete old container PREROUTING rules: %w", err) + logger.Warn("IPTABLES: failed to delete old container PREROUTING rules: %v (continuing)", err) } args := []string{ @@ -421,9 +456,17 @@ func (m *Manager) InsertPreroutingRuleInContainer(pid int, sourceIP, proto, sour func (m *Manager) InsertPostroutingMasqueradeInContainer(pid int, sourceCIDR, proto, sourcePort, comment string) error { logger.Info("IPTABLES: inserting POSTROUTING MASQUERADE rule in container PID %d: src=%s proto=%s sport=%s comment=%q", pid, sourceCIDR, proto, sourcePort, comment) + + // Idempotent check: if the rule already exists, skip + if m.lineExistsInContainer(pid, "nat", "POSTROUTING", "MASQUERADE", comment, sourceCIDR) { + logger.Debug("IPTABLES: POSTROUTING MASQUERADE rule already exists in container PID %d (src=%s comment=%s), skipping", pid, sourceCIDR, comment) + return nil + } + + // Clean up any stale/duplicate rules first patterns := []string{"MASQUERADE", comment, sourceCIDR, sourcePort} if err := m.deleteMatchingLinesInContainer(pid, "nat", "POSTROUTING", patterns...); err != nil { - return fmt.Errorf("failed to delete old container POSTROUTING rules: %w", err) + logger.Warn("IPTABLES: failed to delete old container POSTROUTING rules: %v (continuing)", err) } args := []string{ @@ -435,4 +478,4 @@ func (m *Manager) InsertPostroutingMasqueradeInContainer(pid int, sourceCIDR, pr "-j", "MASQUERADE", } return m.runInContainer(pid, "nat", args...) -} \ No newline at end of file +}