refactor(iptables): make rule insertion functions idempotent
continuous-integration/drone/push Build is passing
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:
@@ -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{
|
||||||
|
|||||||
Reference in New Issue
Block a user