continuous-integration/drone/push Build is passing
Implement FindContainerName method on DockerAPI that attempts exact match first, then falls back to prefix-based matching (e.g., extracting prefix before dash like "service-" in "service-abc") to replicate the old shell script's `grep $D"-"` behavior. Update firewall orchestrator to use this resolution before connecting containers to networks, improving robustness when container names vary from configured selectors.
225 lines
7.5 KiB
Go
225 lines
7.5 KiB
Go
package firewall
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"net"
|
|
"strconv"
|
|
"time"
|
|
|
|
"firewall_containers/network-go/config"
|
|
"firewall_containers/network-go/docker"
|
|
"firewall_containers/network-go/iptables"
|
|
"firewall_containers/network-go/resolver"
|
|
)
|
|
|
|
// Orchestrator reconciles the networks.json configuration into Docker networks
|
|
// and iptables firewall rules
|
|
type Orchestrator struct {
|
|
dockerClient docker.DockerAPI
|
|
iptablesMgr iptables.IPTablesAPI
|
|
resolver *resolver.Resolver
|
|
debug bool
|
|
}
|
|
|
|
// NewOrchestrator creates a new firewall orchestrator
|
|
func NewOrchestrator(dockerClient docker.DockerAPI, iptablesMgr iptables.IPTablesAPI, cfg *config.NetworksConfig) *Orchestrator {
|
|
return &Orchestrator{
|
|
dockerClient: dockerClient,
|
|
iptablesMgr: iptablesMgr,
|
|
resolver: resolver.NewResolver(cfg),
|
|
}
|
|
}
|
|
|
|
// ReconcileAll runs the full reconciliation: networks, container connections, and firewall rules
|
|
func (o *Orchestrator) ReconcileAll(ctx context.Context, cfg *config.NetworksConfig) {
|
|
log.Println("FIREWALL: starting full reconciliation")
|
|
|
|
// Update resolver with latest config
|
|
o.resolver.SetConfig(cfg)
|
|
|
|
// Step 0: Enable IP forwarding (may fail in containers with read-only fs)
|
|
if err := o.iptablesMgr.EnsureIPForward(); err != nil {
|
|
log.Printf("FIREWALL: WARNING could not enable ip_forward: %v", err)
|
|
} else {
|
|
log.Println("FIREWALL: IP forwarding enabled")
|
|
}
|
|
|
|
// Step 1: Ensure all defined networks exist
|
|
o.reconcileNetworks(ctx, cfg)
|
|
|
|
// Step 2: Connect containers to networks with assigned IPs
|
|
o.reconcileIPs(ctx, cfg)
|
|
|
|
// Step 3: Reconcile firewall policies
|
|
o.reconcilePolicies(ctx, cfg)
|
|
|
|
log.Println("FIREWALL: full reconciliation completed")
|
|
}
|
|
|
|
// reconcileNetworks creates Docker networks if they don't exist
|
|
func (o *Orchestrator) reconcileNetworks(ctx context.Context, cfg *config.NetworksConfig) {
|
|
for name, netCfg := range cfg.Networks {
|
|
log.Printf("FIREWALL: ensuring network %s (%s, subnet=%s, gateway=%s)", name, netCfg.NetworkName, netCfg.Subnet, netCfg.Gateway)
|
|
if err := o.dockerClient.EnsureNetwork(ctx, netCfg); err != nil {
|
|
log.Printf("FIREWALL: ERROR ensuring network %s: %v", name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// reconcileIPs connects containers to networks with their assigned IPs
|
|
func (o *Orchestrator) reconcileIPs(ctx context.Context, cfg *config.NetworksConfig) {
|
|
for ipStr, ipCfg := range cfg.IPs {
|
|
networkName := findNetworkForIP(cfg, ipStr)
|
|
if networkName == "" {
|
|
log.Printf("FIREWALL: WARNING no network found for IP %s (container=%s)", ipStr, ipCfg.ContainerName)
|
|
continue
|
|
}
|
|
|
|
// Resolve the actual container name, with fallback to fuzzy matching
|
|
// (old shell script behavior: docker ps | grep $D"-")
|
|
containerName, err := o.dockerClient.FindContainerName(ctx, ipCfg.ContainerName, ipCfg.Selector)
|
|
if err != nil {
|
|
log.Printf("FIREWALL: WARNING container %s (selector=%s) not found: %v, trying connection anyway", ipCfg.ContainerName, ipCfg.Selector, err)
|
|
containerName = ipCfg.ContainerName
|
|
}
|
|
|
|
log.Printf("FIREWALL: connecting container %s to network %s with IP %s", containerName, networkName, ipStr)
|
|
|
|
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
if err := o.dockerClient.WaitForContainerRunning(waitCtx, containerName, 10*time.Second); err != nil {
|
|
log.Printf("FIREWALL: WARNING container %s not running yet: %v, connecting anyway", containerName, err)
|
|
}
|
|
cancel()
|
|
|
|
if err := o.dockerClient.ConnectContainer(ctx, containerName, networkName, ipStr); err != nil {
|
|
log.Printf("FIREWALL: ERROR connecting container %s to %s: %v", containerName, networkName, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// reconcilePolicies translates PolicyConfig entries into iptables rules
|
|
func (o *Orchestrator) reconcilePolicies(ctx context.Context, cfg *config.NetworksConfig) {
|
|
for i, policy := range cfg.Policies {
|
|
log.Printf("FIREWALL: processing policy[%d]", i)
|
|
|
|
proto := policy.Proto
|
|
if proto == "" {
|
|
proto = "tcp"
|
|
}
|
|
port := strconv.Itoa(policy.Port)
|
|
|
|
// Build comment for iptables (matches shell script's NAME-COMMENT pattern)
|
|
comment := policy.ServiceName
|
|
if policy.Name != "" {
|
|
comment = policy.Name + "-" + policy.ServiceName
|
|
}
|
|
|
|
// CASE 1: Rule with "from" field — this is a FORWARD ACCEPT rule
|
|
if policy.From != "" {
|
|
o.applyForwardRule(ctx, cfg, policy, proto, port, comment)
|
|
continue
|
|
}
|
|
|
|
// CASE 2: Rule with "nat" field — this is a DNAT/MASQUERADE rule
|
|
if policy.Nat != "" {
|
|
o.applyNATRule(ctx, cfg, policy, proto, port, comment)
|
|
continue
|
|
}
|
|
|
|
// Unhandled pattern
|
|
log.Printf("FIREWALL: policy[%d] unhandled pattern — service=%s container=%s selector=%s from=%s to=%s port=%d proto=%s nat=%s",
|
|
i, policy.ServiceName, policy.ContainerName, policy.Selector, policy.From, policy.To, policy.Port, policy.Proto, policy.Nat)
|
|
}
|
|
}
|
|
|
|
func (o *Orchestrator) applyForwardRule(ctx context.Context, cfg *config.NetworksConfig, policy config.PolicyConfig, proto, port, comment string) {
|
|
sourceIP := o.resolveIP(policy.From)
|
|
targetIP := ""
|
|
if policy.To != "" {
|
|
targetIP = o.resolveIP(policy.To)
|
|
}
|
|
|
|
// Determine the chain: use DOCKER-USER (iptables-legacy) or FORWARD
|
|
chain := "FORWARD"
|
|
if o.iptablesMgr.Binary() == "/usr/sbin/iptables-legacy" {
|
|
chain = "DOCKER-USER"
|
|
}
|
|
|
|
// Ensure established/related rule exists at the top
|
|
if err := o.iptablesMgr.EnsureEstablishedRelated(chain); err != nil {
|
|
log.Printf("FIREWALL: ERROR ensuring established/related rule in %s: %v", chain, err)
|
|
}
|
|
|
|
// Insert the FORWARD ACCEPT rule
|
|
if err := o.iptablesMgr.InsertForwardAccept(chain, sourceIP, targetIP, proto, "", port, comment); err != nil {
|
|
log.Printf("FIREWALL: ERROR inserting FORWARD ACCEPT rule: %v", err)
|
|
}
|
|
}
|
|
|
|
func (o *Orchestrator) applyNATRule(ctx context.Context, cfg *config.NetworksConfig, policy config.PolicyConfig, proto, port, comment string) {
|
|
selector := policy.Selector
|
|
to := policy.To
|
|
|
|
// Resolve "to" as target IP
|
|
targetIP := o.resolveIP(to)
|
|
|
|
if targetIP == "" {
|
|
log.Printf("FIREWALL: WARNING cannot resolve target %s for nat policy", to)
|
|
return
|
|
}
|
|
|
|
if policy.Nat == "dnat" {
|
|
// Get the container PID for nsenter
|
|
pid, err := o.dockerClient.GetContainerPID(ctx, selector)
|
|
if err != nil {
|
|
log.Printf("FIREWALL: WARNING cannot get PID for container %s: %v, trying host rules", selector, err)
|
|
// Fall back to host-level PREROUTING
|
|
if policy.Iface != "" {
|
|
if err := o.iptablesMgr.InsertPreroutingRuleOnInterface(policy.Iface, proto, port, targetIP, port, comment); err != nil {
|
|
log.Printf("FIREWALL: ERROR inserting interface PREROUTING rule: %v", err)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Insert DNAT PREROUTING inside container namespace
|
|
if err := o.iptablesMgr.InsertPreroutingRuleInContainer(pid, "0.0.0.0/0", proto, port, targetIP, port, comment); err != nil {
|
|
log.Printf("FIREWALL: ERROR inserting container PREROUTING rule: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// resolveIP resolves a name or IP string to an IP address using networks.json config
|
|
func (o *Orchestrator) resolveIP(name string) string {
|
|
// If it's already an IP, return it as CIDR
|
|
if config.IsIP(name) {
|
|
return config.ToCIDR(name)
|
|
}
|
|
|
|
// Use the resolver which looks up from networks.json
|
|
ips := o.resolver.Resolve(name)
|
|
if len(ips) > 0 {
|
|
return ips[0]
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// findNetworkForIP finds the network name that contains the given IP in its subnet
|
|
func findNetworkForIP(cfg *config.NetworksConfig, ip string) string {
|
|
for _, netCfg := range cfg.Networks {
|
|
subnet, err := netCfg.ParseCIDR()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
parsedIP := net.ParseIP(ip)
|
|
if parsedIP == nil {
|
|
continue
|
|
}
|
|
if subnet.Contains(parsedIP) {
|
|
return netCfg.NetworkName
|
|
}
|
|
}
|
|
return ""
|
|
} |