refactor(iptables): make rule insertion functions idempotent
continuous-integration/drone/push Build is passing

Refactored PREROUTING DNAT, POSTROUTING MASQUERADE, and FORWARD ACCEPT rule insertion to first check for existing rules before inserting. This prevents duplicate rules when run multiple times and improves reliability of firewall configuration.
This commit is contained in:
gyurix
2026-06-16 08:16:29 +02:00
parent 04322b699e
commit d1c8eaef3e
+80 -45
View File
@@ -253,13 +253,18 @@ func (m *Manager) deleteMatchingLinesInContainer(pid int, table, chain string, g
// InsertPreroutingRule inserts a DNAT PREROUTING rule on the host // InsertPreroutingRule inserts a DNAT PREROUTING rule on the host
func (m *Manager) InsertPreroutingRule(sourceIP, proto, sourcePort, targetIP, targetPort, comment string) error { func (m *Manager) InsertPreroutingRule(sourceIP, proto, sourcePort, targetIP, targetPort, comment string) error {
logger.Info("IPTABLES: inserting PREROUTING DNAT rule: src=%s proto=%s sport=%s -> dst=%s dport=%s comment=%q", logger.Info("IPTABLES: checking PREROUTING DNAT rule: src=%s proto=%s sport=%s -> dst=%s dport=%s comment=%q",
sourceIP, proto, sourcePort, targetIP, targetPort, comment) sourceIP, proto, sourcePort, targetIP, targetPort, comment)
patterns := []string{"DNAT", sourcePort, targetIP, targetPort, comment}
if err := m.deleteMatchingLines("PREROUTING", "nat", patterns...); err != nil { // Idempotent: check if rule already exists
return fmt.Errorf("failed to delete old PREROUTING rules: %w", err) existing := m.getLineNumbers("PREROUTING", "nat", comment, "DNAT", targetIP, targetPort)
if len(existing) > 0 {
logger.Debug("IPTABLES: PREROUTING DNAT rule already exists (lines=%v), skipping", existing)
return nil
} }
logger.Info("IPTABLES: inserting PREROUTING DNAT rule: src=%s proto=%s sport=%s -> dst=%s dport=%s comment=%q",
sourceIP, proto, sourcePort, targetIP, targetPort, comment)
args := []string{ args := []string{
"-w", "-t", "nat", "-I", "PREROUTING", "-w", "-t", "nat", "-I", "PREROUTING",
"-d", sourceIP, "-d", sourceIP,
@@ -323,13 +328,18 @@ func (m *Manager) InsertPostroutingMasquerade(sourceCIDR, proto, sourcePort, com
// InsertPostroutingMasqueradeForTarget inserts a MASQUERADE POSTROUTING rule for a target // InsertPostroutingMasqueradeForTarget inserts a MASQUERADE POSTROUTING rule for a target
func (m *Manager) InsertPostroutingMasqueradeForTarget(targetCIDR, proto, targetPort, comment string) error { func (m *Manager) InsertPostroutingMasqueradeForTarget(targetCIDR, proto, targetPort, comment string) error {
logger.Info("IPTABLES: inserting POSTROUTING MASQUERADE rule for target: dst=%s proto=%s dport=%s comment=%q", logger.Info("IPTABLES: checking POSTROUTING MASQUERADE rule for target: dst=%s proto=%s dport=%s comment=%q",
targetCIDR, proto, targetPort, comment) targetCIDR, proto, targetPort, comment)
patterns := []string{"MASQUERADE", comment, targetCIDR, targetPort}
if err := m.deleteMatchingLines("POSTROUTING", "nat", patterns...); err != nil { // Idempotent: check if rule already exists
return fmt.Errorf("failed to delete old POSTROUTING rules: %w", err) existing := m.getLineNumbers("POSTROUTING", "nat", comment, "MASQUERADE", targetCIDR)
if len(existing) > 0 {
logger.Debug("IPTABLES: POSTROUTING MASQUERADE for target already exists (lines=%v), skipping", existing)
return nil
} }
logger.Info("IPTABLES: inserting POSTROUTING MASQUERADE rule for target: dst=%s proto=%s dport=%s comment=%q",
targetCIDR, proto, targetPort, comment)
args := []string{ args := []string{
"-w", "-t", "nat", "-I", "POSTROUTING", "-w", "-t", "nat", "-I", "POSTROUTING",
"-d", targetCIDR, "-d", targetCIDR,
@@ -343,10 +353,12 @@ func (m *Manager) InsertPostroutingMasqueradeForTarget(targetCIDR, proto, target
// InsertForwardAccept inserts a FORWARD ACCEPT rule on the host // InsertForwardAccept inserts a FORWARD ACCEPT rule on the host
func (m *Manager) InsertForwardAccept(chain, sourceIP, targetIP, proto, sourcePort, targetPort, comment string) error { func (m *Manager) InsertForwardAccept(chain, sourceIP, targetIP, proto, sourcePort, targetPort, comment string) error {
logger.Info("IPTABLES: inserting FORWARD ACCEPT rule: chain=%s src=%s dst=%s proto=%s sport=%s dport=%s comment=%q", logger.Info("IPTABLES: checking FORWARD ACCEPT rule: chain=%s src=%s dst=%s proto=%s sport=%s dport=%s comment=%q",
chain, sourceIP, targetIP, proto, sourcePort, targetPort, comment) chain, sourceIP, targetIP, proto, sourcePort, targetPort, comment)
// Idempotent: check if rule already exists
var grepPatterns []string var grepPatterns []string
grepPatterns = append(grepPatterns, proto) grepPatterns = append(grepPatterns, comment, proto)
if sourceIP != "" { if sourceIP != "" {
grepPatterns = append(grepPatterns, sourceIP) grepPatterns = append(grepPatterns, sourceIP)
} }
@@ -359,11 +371,14 @@ func (m *Manager) InsertForwardAccept(chain, sourceIP, targetIP, proto, sourcePo
if targetPort != "" { if targetPort != "" {
grepPatterns = append(grepPatterns, targetPort) grepPatterns = append(grepPatterns, targetPort)
} }
existing := m.getLineNumbers(chain, "", grepPatterns...)
if err := m.deleteMatchingLines(chain, "", grepPatterns...); err != nil { if len(existing) > 0 {
return fmt.Errorf("failed to delete old FORWARD rules: %w", err) logger.Debug("IPTABLES: FORWARD ACCEPT rule already exists in %s (lines=%v), skipping", chain, existing)
return nil
} }
logger.Info("IPTABLES: inserting FORWARD ACCEPT rule: chain=%s src=%s dst=%s proto=%s sport=%s dport=%s comment=%q",
chain, sourceIP, targetIP, proto, sourcePort, targetPort, comment)
args := []string{"-w", "-I", chain, "-p", proto} args := []string{"-w", "-I", chain, "-p", proto}
if sourceIP != "" { if sourceIP != "" {
args = append(args, "-s", sourceIP) args = append(args, "-s", sourceIP)
@@ -397,31 +412,17 @@ func (m *Manager) DeleteForwardAccept(chain, comment string) error {
return nil return nil
} }
// lineExistsInContainer checks if a line matching the patterns exists inside a container namespace // checkContainerChainExists lists rules inside a container namespace and returns
func (m *Manager) lineExistsInContainer(pid int, table, chain string, patterns ...string) bool { // the output, or an error if nsenter/iptables fails inside the container.
func (m *Manager) checkContainerChainExists(pid int, table, chain string) (string, error) {
iptPath := m.binary iptPath := m.binary
nsenterArgs := []string{"-t", fmt.Sprintf("%d", pid), "-n", "--", iptPath, "-w", "-n", "-t", table, "-L", chain} nsenterArgs := []string{"-t", fmt.Sprintf("%d", pid), "-n", "--", iptPath, "-w", "-n", "-t", table, "-L", chain}
cmd := exec.Command("nsenter", nsenterArgs...) cmd := exec.Command("nsenter", nsenterArgs...)
output, err := cmd.Output() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
logger.Debug("IPTABLES: lineExistsInContainer list failed for PID %d chain %s: %v", pid, chain, err) return "", fmt.Errorf("nsenter iptables list failed for PID %d chain %s/%s: %w (output: %s)", pid, table, chain, err, strings.TrimSpace(string(output)))
return false
} }
return string(output), nil
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 // InsertPreroutingRuleInContainer inserts a DNAT PREROUTING rule inside a container namespace
@@ -429,16 +430,34 @@ func (m *Manager) InsertPreroutingRuleInContainer(pid int, sourceIP, proto, sour
logger.Info("IPTABLES: inserting PREROUTING DNAT rule in container PID %d: src=%s proto=%s dport=%s -> %s:%s comment=%q", 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) pid, sourceIP, proto, sourcePort, targetIP, targetPort, comment)
// Idempotent check: if the rule already exists, skip // First, try to list the chain inside the container to check state
if m.lineExistsInContainer(pid, "nat", "PREROUTING", "DNAT", sourcePort, targetIP, comment) { output, err := m.checkContainerChainExists(pid, "nat", "PREROUTING")
logger.Debug("IPTABLES: PREROUTING DNAT rule already exists in container PID %d (pattern=%s -> %s), skipping", pid, sourcePort, targetIP) if err != nil {
// Cannot inspect container iptables — return error so caller falls back to host
logger.Warn("IPTABLES: cannot check container PREROUTING (PID %d): %v", pid, err)
return fmt.Errorf("cannot check container iptables: %w", err)
}
// Idempotent check: scan existing rules for matching patterns
ruleExists := false
for _, line := range strings.Split(output, "\n") {
if strings.Contains(line, "DNAT") &&
strings.Contains(line, sourcePort) &&
strings.Contains(line, targetIP) &&
strings.Contains(line, comment) {
ruleExists = true
break
}
}
if ruleExists {
logger.Info("IPTABLES: PREROUTING DNAT rule already exists in container PID %d (dport=%s -> %s), skipping", pid, sourcePort, targetIP)
return nil return nil
} }
// Clean up any stale/duplicate rules first // Rule doesn't exist — clean up stale/duplicate rules then insert
patterns := []string{"DNAT", sourcePort, targetIP, targetPort, comment} patterns := []string{"DNAT", sourcePort, targetIP, targetPort, comment}
if err := m.deleteMatchingLinesInContainer(pid, "nat", "PREROUTING", patterns...); err != nil { if delErr := m.deleteMatchingLinesInContainer(pid, "nat", "PREROUTING", patterns...); delErr != nil {
logger.Warn("IPTABLES: failed to delete old container PREROUTING rules: %v (continuing)", err) logger.Debug("IPTABLES: stale PREROUTING cleanup in container PID %d: %v", pid, delErr)
} }
args := []string{ args := []string{
@@ -457,16 +476,32 @@ func (m *Manager) InsertPostroutingMasqueradeInContainer(pid int, sourceCIDR, pr
logger.Info("IPTABLES: inserting POSTROUTING MASQUERADE rule in container PID %d: src=%s proto=%s sport=%s comment=%q", logger.Info("IPTABLES: inserting POSTROUTING MASQUERADE rule in container PID %d: src=%s proto=%s sport=%s comment=%q",
pid, sourceCIDR, proto, sourcePort, comment) pid, sourceCIDR, proto, sourcePort, comment)
// Idempotent check: if the rule already exists, skip // First, try to list the chain inside the container to check state
if m.lineExistsInContainer(pid, "nat", "POSTROUTING", "MASQUERADE", comment, sourceCIDR) { output, err := m.checkContainerChainExists(pid, "nat", "POSTROUTING")
logger.Debug("IPTABLES: POSTROUTING MASQUERADE rule already exists in container PID %d (src=%s comment=%s), skipping", pid, sourceCIDR, comment) if err != nil {
logger.Warn("IPTABLES: cannot check container POSTROUTING (PID %d): %v", pid, err)
return fmt.Errorf("cannot check container iptables: %w", err)
}
// Idempotent check: scan existing rules for matching patterns
ruleExists := false
for _, line := range strings.Split(output, "\n") {
if strings.Contains(line, "MASQUERADE") &&
strings.Contains(line, comment) &&
strings.Contains(line, sourceCIDR) {
ruleExists = true
break
}
}
if ruleExists {
logger.Info("IPTABLES: POSTROUTING MASQUERADE rule already exists in container PID %d (src=%s), skipping", pid, sourceCIDR)
return nil return nil
} }
// Clean up any stale/duplicate rules first // Rule doesn't exist — clean up stale/duplicate rules then insert
patterns := []string{"MASQUERADE", comment, sourceCIDR, sourcePort} patterns := []string{"MASQUERADE", comment, sourceCIDR, sourcePort}
if err := m.deleteMatchingLinesInContainer(pid, "nat", "POSTROUTING", patterns...); err != nil { if delErr := m.deleteMatchingLinesInContainer(pid, "nat", "POSTROUTING", patterns...); delErr != nil {
logger.Warn("IPTABLES: failed to delete old container POSTROUTING rules: %v (continuing)", err) logger.Debug("IPTABLES: stale POSTROUTING cleanup in container PID %d: %v", pid, delErr)
} }
args := []string{ args := []string{