feat(docker, firewall): Add stateful network connection check and optimize NAT rules
continuous-integration/drone/push Build is passing

This adds an IsConnected method to verify if a container is already connected to a network with the expected IP, preventing redundant operations. In reconcileIPs, it skips reconnections if the state is correct. In applyNATRule, MASQUERADE is now applied in the same namespace as DNAT (container or host) for consistent and accurate rule application.
This commit is contained in:
gyurix
2026-06-15 23:39:58 +02:00
parent bf94206849
commit 246346f8b1
4 changed files with 95 additions and 13 deletions
+28 -8
View File
@@ -98,6 +98,13 @@ func (o *Orchestrator) reconcileIPs(ctx context.Context, cfg *config.NetworksCon
ipCfg.ContainerName, containerName)
}
// Stateful check: verify container already has the correct IP on this network
if o.dockerClient.IsConnected(ctx, containerName, networkName, ipStr) {
logger.Debug("FIREWALL: container %s already connected to %s with IP %s, skipping",
containerName, networkName, ipStr)
continue
}
logger.Info("FIREWALL: connecting container %s to network %s with IP %s",
containerName, networkName, ipStr)
@@ -228,6 +235,7 @@ func (o *Orchestrator) applyNATRule(ctx context.Context, cfg *config.NetworksCon
// Try to insert rules inside the container namespace via nsenter
usedContainer := false
var containerPID int
if selector != "" {
pid, err := o.dockerClient.GetContainerPID(ctx, selector)
if err == nil {
@@ -238,6 +246,7 @@ func (o *Orchestrator) applyNATRule(ctx context.Context, cfg *config.NetworksCon
logger.Info("FIREWALL: DNAT rule inserted in container %s: target=%s proto=%s port=%s",
selector, targetIP, proto, port)
usedContainer = true
containerPID = pid
}
} else {
logger.Warn("FIREWALL: cannot get PID for container %s: %v, trying host rules", selector, err)
@@ -257,23 +266,34 @@ func (o *Orchestrator) applyNATRule(ctx context.Context, cfg *config.NetworksCon
// Always add MASQUERADE on POSTROUTING so return traffic from the
// DNAT target can route back through the same interface.
// This mirrors the old shell script behavior where POSTROUTING
// was always set alongside PREROUTING DNAT rules.
// Required regardless of whether DNAT was in container namespace or host.
// If DNAT was in a container namespace, apply POSTROUTING in the same namespace.
// If DNAT was on the host, apply POSTROUTING on the host.
if targetIP != "" {
masqComment := comment + "-masq"
targetSubnet := ""
// Use the target's /24 subnet as the source CIDR for masquerade
if strings.Contains(targetIP, ".") {
targetSubnet = targetIP[:strings.LastIndex(targetIP, ".")] + ".0/24"
}
if targetSubnet != "" {
logger.Info("FIREWALL: inserting POSTROUTING MASQUERADE for %s", targetSubnet)
if err := o.iptablesMgr.InsertPostroutingMasquerade(targetSubnet, proto, port, masqComment); err != nil {
logger.Error("FIREWALL: failed to insert POSTROUTING MASQUERADE: %v", err)
if usedContainer && containerPID > 0 {
// Apply in container namespace alongside the DNAT rule
logger.Info("FIREWALL: POSTROUTING MASQUERADE in container PID %d", containerPID)
if err := o.iptablesMgr.InsertPostroutingMasqueradeInContainer(containerPID, targetSubnet, proto, port, masqComment); err != nil {
logger.Error("FIREWALL: failed to insert container POSTROUTING MASQUERADE: %v", err)
} else {
logger.Info("FIREWALL: container POSTROUTING MASQUERADE inserted: subnet=%s proto=%s port=%s",
targetSubnet, proto, port)
}
} else {
logger.Info("FIREWALL: POSTROUTING MASQUERADE inserted: subnet=%s proto=%s port=%s",
targetSubnet, proto, port)
// Apply on host POSTROUTING
logger.Info("FIREWALL: POSTROUTING MASQUERADE on host")
if err := o.iptablesMgr.InsertPostroutingMasquerade(targetSubnet, proto, port, masqComment); err != nil {
logger.Error("FIREWALL: failed to insert host POSTROUTING MASQUERADE: %v", err)
} else {
logger.Info("FIREWALL: host POSTROUTING MASQUERADE inserted: subnet=%s proto=%s port=%s",
targetSubnet, proto, port)
}
}
}
}