feat: add logging to Docker and iptables operations, fix iptables path
continuous-integration/drone/push Build is passing

- Create /var/log/network-go directory in Dockerfile for log storage
- Add comprehensive logging to Docker client creation, network management, and container operations
- Add logging to iptables rule management (list, delete, etc.)
- Fix iptables executable path resolution in deleteMatchingLinesInContainer to use configured binary path
This commit is contained in:
gyurix
2026-06-15 17:05:53 +02:00
parent 3172023254
commit 27607d1a2e
8 changed files with 401 additions and 66 deletions
+3
View File
@@ -18,6 +18,9 @@ RUN apk add --update --no-cache \
util-linux \ util-linux \
&& rm -rf /var/cache/apk/* && rm -rf /var/cache/apk/*
# Create log directory
RUN mkdir -p /var/log/network-go
COPY --from=builder /build/network-go /usr/local/bin/network-go COPY --from=builder /build/network-go /usr/local/bin/network-go
CMD ["/usr/local/bin/network-go"] CMD ["/usr/local/bin/network-go"]
+40 -1
View File
@@ -16,6 +16,7 @@ import (
"github.com/docker/docker/client" "github.com/docker/docker/client"
"firewall_containers/network-go/config" "firewall_containers/network-go/config"
"firewall_containers/network-go/logger"
) )
// DockerAPI defines the interface for Docker operations, enabling mock implementations for testing // DockerAPI defines the interface for Docker operations, enabling mock implementations for testing
@@ -42,20 +43,29 @@ var _ DockerAPI = (*Client)(nil)
// NewClient creates a new Docker client // NewClient creates a new Docker client
func NewClient() (*Client, error) { func NewClient() (*Client, error) {
logger.Info("DOCKER: creating Docker client (using DOCKER_HOST env)")
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create Docker client: %w", err) return nil, fmt.Errorf("failed to create Docker client: %w", err)
} }
ping, err := cli.Ping(context.Background())
if err != nil {
logger.Warn("DOCKER: Docker daemon ping failed: %v", err)
} else {
logger.Info("DOCKER: connected to Docker daemon (API version=%s, OS=%s)", ping.APIVersion, ping.OSType)
}
return &Client{cli: cli}, nil return &Client{cli: cli}, nil
} }
// Close closes the Docker client // Close closes the Docker client
func (c *Client) Close() error { func (c *Client) Close() error {
logger.Debug("DOCKER: closing Docker client")
return c.cli.Close() return c.cli.Close()
} }
// EnsureNetwork creates a Docker network if it does not already exist // EnsureNetwork creates a Docker network if it does not already exist
func (c *Client) EnsureNetwork(ctx context.Context, netCfg config.NetworkConfig) error { func (c *Client) EnsureNetwork(ctx context.Context, netCfg config.NetworkConfig) error {
logger.Debug("DOCKER: checking if network %q exists", netCfg.NetworkName)
existingNetworks, err := c.cli.NetworkList(ctx, network.ListOptions{ existingNetworks, err := c.cli.NetworkList(ctx, network.ListOptions{
Filters: filters.NewArgs(filters.Arg("name", netCfg.NetworkName)), Filters: filters.NewArgs(filters.Arg("name", netCfg.NetworkName)),
}) })
@@ -65,6 +75,7 @@ func (c *Client) EnsureNetwork(ctx context.Context, netCfg config.NetworkConfig)
for _, n := range existingNetworks { for _, n := range existingNetworks {
if n.Name == netCfg.NetworkName { if n.Name == netCfg.NetworkName {
logger.Debug("DOCKER: network %q already exists", netCfg.NetworkName)
return nil return nil
} }
} }
@@ -95,26 +106,30 @@ func (c *Client) EnsureNetwork(ctx context.Context, netCfg config.NetworkConfig)
Attachable: true, Attachable: true,
} }
logger.Info("DOCKER: creating network %q (subnet=%s, gateway=%s)", netCfg.NetworkName, netCfg.Subnet, netCfg.Gateway)
resp, err := c.cli.NetworkCreate(ctx, netCfg.NetworkName, createOpts) resp, err := c.cli.NetworkCreate(ctx, netCfg.NetworkName, createOpts)
if err != nil { if err != nil {
return fmt.Errorf("failed to create network %s: %w", netCfg.NetworkName, err) return fmt.Errorf("failed to create network %s: %w", netCfg.NetworkName, err)
} }
_ = resp logger.Info("DOCKER: network %q created (ID=%s)", netCfg.NetworkName, resp.ID)
return nil return nil
} }
// RemoveNetwork removes a Docker network // RemoveNetwork removes a Docker network
func (c *Client) RemoveNetwork(ctx context.Context, networkName string) error { func (c *Client) RemoveNetwork(ctx context.Context, networkName string) error {
logger.Info("DOCKER: removing network %q", networkName)
err := c.cli.NetworkRemove(ctx, networkName) err := c.cli.NetworkRemove(ctx, networkName)
if err != nil { if err != nil {
return fmt.Errorf("failed to remove network %s: %w", networkName, err) return fmt.Errorf("failed to remove network %s: %w", networkName, err)
} }
logger.Info("DOCKER: network %q removed", networkName)
return nil return nil
} }
// ConnectContainer connects a container to a network with a specific IP // ConnectContainer connects a container to a network with a specific IP
func (c *Client) ConnectContainer(ctx context.Context, containerName, networkName, ip string) error { func (c *Client) ConnectContainer(ctx context.Context, containerName, networkName, ip string) error {
logger.Info("DOCKER: connecting container %q to network %q with IP %s", containerName, networkName, ip)
endpointSettings := &network.EndpointSettings{ endpointSettings := &network.EndpointSettings{
IPAMConfig: &network.EndpointIPAMConfig{ IPAMConfig: &network.EndpointIPAMConfig{
IPv4Address: ip, IPv4Address: ip,
@@ -125,33 +140,41 @@ func (c *Client) ConnectContainer(ctx context.Context, containerName, networkNam
if err != nil { if err != nil {
// "endpoint with name ... already exists" is expected on re-reconciliation // "endpoint with name ... already exists" is expected on re-reconciliation
if strings.Contains(err.Error(), "already exists") { if strings.Contains(err.Error(), "already exists") {
logger.Info("DOCKER: container %q already connected to network %q", containerName, networkName)
return nil return nil
} }
return fmt.Errorf("failed to connect container %s to network %s: %w", containerName, networkName, err) return fmt.Errorf("failed to connect container %s to network %s: %w", containerName, networkName, err)
} }
logger.Info("DOCKER: container %q connected to network %q with IP %s", containerName, networkName, ip)
return nil return nil
} }
// DisconnectContainer disconnects a container from a network // DisconnectContainer disconnects a container from a network
func (c *Client) DisconnectContainer(ctx context.Context, containerName, networkName string) error { func (c *Client) DisconnectContainer(ctx context.Context, containerName, networkName string) error {
logger.Info("DOCKER: disconnecting container %q from network %q", containerName, networkName)
err := c.cli.NetworkDisconnect(ctx, networkName, containerName, true) err := c.cli.NetworkDisconnect(ctx, networkName, containerName, true)
if err != nil { if err != nil {
return fmt.Errorf("failed to disconnect container %s from network %s: %w", containerName, networkName, err) return fmt.Errorf("failed to disconnect container %s from network %s: %w", containerName, networkName, err)
} }
logger.Info("DOCKER: container %q disconnected from network %q", containerName, networkName)
return nil return nil
} }
// InspectContainer returns the container's details // InspectContainer returns the container's details
func (c *Client) InspectContainer(ctx context.Context, containerName string) (*types.ContainerJSON, error) { func (c *Client) InspectContainer(ctx context.Context, containerName string) (*types.ContainerJSON, error) {
logger.Debug("DOCKER: inspecting container %q", containerName)
container, err := c.cli.ContainerInspect(ctx, containerName) container, err := c.cli.ContainerInspect(ctx, containerName)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to inspect container %s: %w", containerName, err) return nil, fmt.Errorf("failed to inspect container %s: %w", containerName, err)
} }
logger.Debug("DOCKER: container %q inspected (status=%s, PID=%d)",
containerName, container.State.Status, container.State.Pid)
return &container, nil return &container, nil
} }
// WaitForContainerRunning waits until a container is in running state, with a timeout // WaitForContainerRunning waits until a container is in running state, with a timeout
func (c *Client) WaitForContainerRunning(ctx context.Context, containerName string, timeout time.Duration) error { func (c *Client) WaitForContainerRunning(ctx context.Context, containerName string, timeout time.Duration) error {
logger.Info("DOCKER: waiting for container %q to be running (timeout=%s)", containerName, timeout)
ctx, cancel := context.WithTimeout(ctx, timeout) ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel() defer cancel()
@@ -165,17 +188,21 @@ func (c *Client) WaitForContainerRunning(ctx context.Context, containerName stri
case <-ticker.C: case <-ticker.C:
container, err := c.cli.ContainerInspect(ctx, containerName) container, err := c.cli.ContainerInspect(ctx, containerName)
if err != nil { if err != nil {
logger.Debug("DOCKER: container %q inspect failed (not ready yet): %v", containerName, err)
continue continue
} }
if container.State != nil && container.State.Running { if container.State != nil && container.State.Running {
logger.Info("DOCKER: container %q is now running", containerName)
return nil return nil
} }
logger.Debug("DOCKER: container %q status=%s, not running yet", containerName, container.State.Status)
} }
} }
} }
// GetContainerPID returns the PID of a container for nsenter operations // GetContainerPID returns the PID of a container for nsenter operations
func (c *Client) GetContainerPID(ctx context.Context, containerName string) (int, error) { func (c *Client) GetContainerPID(ctx context.Context, containerName string) (int, error) {
logger.Info("DOCKER: getting PID for container %q", containerName)
cont, err := c.cli.ContainerInspect(ctx, containerName) cont, err := c.cli.ContainerInspect(ctx, containerName)
if err != nil { if err != nil {
return 0, fmt.Errorf("failed to inspect container %s: %w", containerName, err) return 0, fmt.Errorf("failed to inspect container %s: %w", containerName, err)
@@ -183,6 +210,7 @@ func (c *Client) GetContainerPID(ctx context.Context, containerName string) (int
if cont.State == nil || !cont.State.Running { if cont.State == nil || !cont.State.Running {
return 0, fmt.Errorf("container %s is not running", containerName) return 0, fmt.Errorf("container %s is not running", containerName)
} }
logger.Info("DOCKER: container %q PID=%d", containerName, cont.State.Pid)
return cont.State.Pid, nil return cont.State.Pid, nil
} }
@@ -193,6 +221,7 @@ func (c *Client) AddRouteInContainer(ctx context.Context, containerName, network
return fmt.Errorf("failed to get PID for container %s: %w", containerName, err) return fmt.Errorf("failed to get PID for container %s: %w", containerName, err)
} }
logger.Info("DOCKER: adding route in container %q (PID=%d): %s via %s", containerName, pid, network, gateway)
args := []string{ args := []string{
"-t", fmt.Sprintf("%d", pid), "-t", fmt.Sprintf("%d", pid),
"-n", "--", "-n", "--",
@@ -204,6 +233,8 @@ func (c *Client) AddRouteInContainer(ctx context.Context, containerName, network
if err != nil { if err != nil {
return fmt.Errorf("failed to add route in container %s: %w\noutput: %s", containerName, err, string(output)) return fmt.Errorf("failed to add route in container %s: %w\noutput: %s", containerName, err, string(output))
} }
logger.Info("DOCKER: route added in container %q: %s via %s", containerName, network, gateway)
logger.Debug("DOCKER: nsenter output: %s", strings.TrimSpace(string(output)))
return nil return nil
} }
@@ -211,6 +242,8 @@ func (c *Client) AddRouteInContainer(ctx context.Context, containerName, network
// First tries exact name match, then exact selector, then prefix matching // First tries exact name match, then exact selector, then prefix matching
// (matching the old shell script's grep $D"-" behavior). // (matching the old shell script's grep $D"-" behavior).
func (c *Client) FindContainerName(ctx context.Context, name, selector string) (string, error) { func (c *Client) FindContainerName(ctx context.Context, name, selector string) (string, error) {
logger.Info("DOCKER: finding container: name=%q selector=%q", name, selector)
// Try exact name match using ContainerList with a name filter // Try exact name match using ContainerList with a name filter
// Docker API name filter does an exact match by default // Docker API name filter does an exact match by default
containers, err := c.cli.ContainerList(ctx, container.ListOptions{ containers, err := c.cli.ContainerList(ctx, container.ListOptions{
@@ -221,11 +254,13 @@ func (c *Client) FindContainerName(ctx context.Context, name, selector string) (
}) })
if err == nil && len(containers) > 0 { if err == nil && len(containers) > 0 {
cName := strings.TrimPrefix(containers[0].Names[0], "/") cName := strings.TrimPrefix(containers[0].Names[0], "/")
logger.Info("DOCKER: found container by exact name match: %q", cName)
return cName, nil return cName, nil
} }
// Try exact selector match // Try exact selector match
if selector != "" && selector != name { if selector != "" && selector != name {
logger.Debug("DOCKER: trying selector match: %q", selector)
containers, err = c.cli.ContainerList(ctx, container.ListOptions{ containers, err = c.cli.ContainerList(ctx, container.ListOptions{
Filters: filters.NewArgs( Filters: filters.NewArgs(
filters.Arg("name", "^/?"+regexp.QuoteMeta(selector)+"$"), filters.Arg("name", "^/?"+regexp.QuoteMeta(selector)+"$"),
@@ -234,6 +269,7 @@ func (c *Client) FindContainerName(ctx context.Context, name, selector string) (
}) })
if err == nil && len(containers) > 0 { if err == nil && len(containers) > 0 {
cName := strings.TrimPrefix(containers[0].Names[0], "/") cName := strings.TrimPrefix(containers[0].Names[0], "/")
logger.Info("DOCKER: found container by selector match: %q", cName)
return cName, nil return cName, nil
} }
} }
@@ -249,6 +285,7 @@ func (c *Client) FindContainerName(ctx context.Context, name, selector string) (
if strings.Contains(candidate, "-") { if strings.Contains(candidate, "-") {
prefix = candidate[:strings.Index(candidate, "-")] prefix = candidate[:strings.Index(candidate, "-")]
} }
logger.Debug("DOCKER: trying prefix match: candidate=%q prefix=%q", candidate, prefix)
containers, err = c.cli.ContainerList(ctx, container.ListOptions{ containers, err = c.cli.ContainerList(ctx, container.ListOptions{
Filters: filters.NewArgs( Filters: filters.NewArgs(
@@ -257,12 +294,14 @@ func (c *Client) FindContainerName(ctx context.Context, name, selector string) (
), ),
}) })
if err != nil { if err != nil {
logger.Debug("DOCKER: prefix list failed: %v", err)
continue continue
} }
for _, c := range containers { for _, c := range containers {
for _, cName := range c.Names { for _, cName := range c.Names {
cName = strings.TrimPrefix(cName, "/") cName = strings.TrimPrefix(cName, "/")
logger.Info("DOCKER: found container by prefix match %q-: %q", prefix, cName)
return cName, nil return cName, nil
} }
} }
+73 -23
View File
@@ -2,7 +2,6 @@ package firewall
import ( import (
"context" "context"
"log"
"net" "net"
"strconv" "strconv"
"time" "time"
@@ -10,6 +9,7 @@ import (
"firewall_containers/network-go/config" "firewall_containers/network-go/config"
"firewall_containers/network-go/docker" "firewall_containers/network-go/docker"
"firewall_containers/network-go/iptables" "firewall_containers/network-go/iptables"
"firewall_containers/network-go/logger"
"firewall_containers/network-go/resolver" "firewall_containers/network-go/resolver"
) )
@@ -33,16 +33,18 @@ func NewOrchestrator(dockerClient docker.DockerAPI, iptablesMgr iptables.IPTable
// ReconcileAll runs the full reconciliation: networks, container connections, and firewall rules // ReconcileAll runs the full reconciliation: networks, container connections, and firewall rules
func (o *Orchestrator) ReconcileAll(ctx context.Context, cfg *config.NetworksConfig) { func (o *Orchestrator) ReconcileAll(ctx context.Context, cfg *config.NetworksConfig) {
log.Println("FIREWALL: starting full reconciliation") logger.Info("FIREWALL: starting full reconciliation")
logger.Debug("FIREWALL: config has %d networks, %d IPs, %d policies",
len(cfg.Networks), len(cfg.IPs), len(cfg.Policies))
// Update resolver with latest config // Update resolver with latest config
o.resolver.SetConfig(cfg) o.resolver.SetConfig(cfg)
// Step 0: Enable IP forwarding (may fail in containers with read-only fs) // Step 0: Enable IP forwarding (may fail in containers with read-only fs)
if err := o.iptablesMgr.EnsureIPForward(); err != nil { if err := o.iptablesMgr.EnsureIPForward(); err != nil {
log.Printf("FIREWALL: WARNING could not enable ip_forward: %v", err) logger.Warn("FIREWALL: could not enable ip_forward: %v", err)
} else { } else {
log.Println("FIREWALL: IP forwarding enabled") logger.Info("FIREWALL: IP forwarding enabled")
} }
// Step 1: Ensure all defined networks exist // Step 1: Ensure all defined networks exist
@@ -54,15 +56,18 @@ func (o *Orchestrator) ReconcileAll(ctx context.Context, cfg *config.NetworksCon
// Step 3: Reconcile firewall policies // Step 3: Reconcile firewall policies
o.reconcilePolicies(ctx, cfg) o.reconcilePolicies(ctx, cfg)
log.Println("FIREWALL: full reconciliation completed") logger.Info("FIREWALL: full reconciliation completed")
} }
// reconcileNetworks creates Docker networks if they don't exist // reconcileNetworks creates Docker networks if they don't exist
func (o *Orchestrator) reconcileNetworks(ctx context.Context, cfg *config.NetworksConfig) { func (o *Orchestrator) reconcileNetworks(ctx context.Context, cfg *config.NetworksConfig) {
for name, netCfg := range cfg.Networks { for name, netCfg := range cfg.Networks {
log.Printf("FIREWALL: ensuring network %s (%s, subnet=%s, gateway=%s)", name, netCfg.NetworkName, netCfg.Subnet, netCfg.Gateway) logger.Info("FIREWALL: ensuring network %s (name=%s, subnet=%s, gateway=%s)",
name, netCfg.NetworkName, netCfg.Subnet, netCfg.Gateway)
if err := o.dockerClient.EnsureNetwork(ctx, netCfg); err != nil { if err := o.dockerClient.EnsureNetwork(ctx, netCfg); err != nil {
log.Printf("FIREWALL: ERROR ensuring network %s: %v", name, err) logger.Error("FIREWALL: failed to ensure network %s: %v", name, err)
} else {
logger.Debug("FIREWALL: network %s ready", name)
} }
} }
} }
@@ -72,28 +77,46 @@ func (o *Orchestrator) reconcileIPs(ctx context.Context, cfg *config.NetworksCon
for ipStr, ipCfg := range cfg.IPs { for ipStr, ipCfg := range cfg.IPs {
networkName := findNetworkForIP(cfg, ipStr) networkName := findNetworkForIP(cfg, ipStr)
if networkName == "" { if networkName == "" {
log.Printf("FIREWALL: WARNING no network found for IP %s (container=%s)", ipStr, ipCfg.ContainerName) logger.Warn("FIREWALL: no network found for IP %s (container=%s, selector=%s)",
ipStr, ipCfg.ContainerName, ipCfg.Selector)
continue continue
} }
logger.Info("FIREWALL: resolving container name for IP %s (container=%s, selector=%s)",
ipStr, ipCfg.ContainerName, ipCfg.Selector)
// Resolve the actual container name, with fallback to fuzzy matching // Resolve the actual container name, with fallback to fuzzy matching
// (old shell script behavior: docker ps | grep $D"-") // (old shell script behavior: docker ps | grep $D"-")
containerName, err := o.dockerClient.FindContainerName(ctx, ipCfg.ContainerName, ipCfg.Selector) containerName, err := o.dockerClient.FindContainerName(ctx, ipCfg.ContainerName, ipCfg.Selector)
if err != nil { if err != nil {
log.Printf("FIREWALL: WARNING container %s (selector=%s) not found: %v, trying connection anyway", ipCfg.ContainerName, ipCfg.Selector, err) logger.Warn("FIREWALL: container %s (selector=%s) not found: %v, using config name anyway",
ipCfg.ContainerName, ipCfg.Selector, err)
containerName = ipCfg.ContainerName containerName = ipCfg.ContainerName
} else if containerName != ipCfg.ContainerName {
logger.Info("FIREWALL: container resolved: config_name=%s -> actual=%s",
ipCfg.ContainerName, containerName)
} }
log.Printf("FIREWALL: connecting container %s to network %s with IP %s", containerName, networkName, ipStr) logger.Info("FIREWALL: connecting container %s to network %s with IP %s",
containerName, networkName, ipStr)
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second) waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
if err := o.dockerClient.WaitForContainerRunning(waitCtx, containerName, 10*time.Second); err != nil { waitErr := o.dockerClient.WaitForContainerRunning(waitCtx, containerName, 10*time.Second)
log.Printf("FIREWALL: WARNING container %s not running yet: %v, connecting anyway", containerName, err)
}
cancel() cancel()
if waitErr != nil {
logger.Warn("FIREWALL: container %s not running yet: %v, connecting anyway",
containerName, waitErr)
} else {
logger.Debug("FIREWALL: container %s is running", containerName)
}
if err := o.dockerClient.ConnectContainer(ctx, containerName, networkName, ipStr); err != nil { if err := o.dockerClient.ConnectContainer(ctx, containerName, networkName, ipStr); err != nil {
log.Printf("FIREWALL: ERROR connecting container %s to %s: %v", containerName, networkName, err) logger.Error("FIREWALL: failed to connect container %s to %s: %v",
containerName, networkName, err)
} else {
logger.Info("FIREWALL: container %s connected to network %s with IP %s",
containerName, networkName, ipStr)
} }
} }
} }
@@ -101,7 +124,10 @@ func (o *Orchestrator) reconcileIPs(ctx context.Context, cfg *config.NetworksCon
// reconcilePolicies translates PolicyConfig entries into iptables rules // reconcilePolicies translates PolicyConfig entries into iptables rules
func (o *Orchestrator) reconcilePolicies(ctx context.Context, cfg *config.NetworksConfig) { func (o *Orchestrator) reconcilePolicies(ctx context.Context, cfg *config.NetworksConfig) {
for i, policy := range cfg.Policies { for i, policy := range cfg.Policies {
log.Printf("FIREWALL: processing policy[%d]", i) logger.Info("FIREWALL: processing policy[%d]", i)
logger.Debug("FIREWALL: policy[%d] details: service=%s container=%s selector=%s from=%s to=%s port=%d proto=%s nat=%s iface=%s",
i, policy.ServiceName, policy.ContainerName, policy.Selector,
policy.From, policy.To, policy.Port, policy.Proto, policy.Nat, policy.Iface)
proto := policy.Proto proto := policy.Proto
if proto == "" { if proto == "" {
@@ -122,6 +148,7 @@ func (o *Orchestrator) reconcilePolicies(ctx context.Context, cfg *config.Networ
comment = policy.ServiceName comment = policy.ServiceName
} }
} }
logger.Debug("FIREWALL: policy[%d] comment=%q", i, comment)
// CASE 1: Rule with "from" field — this is a FORWARD ACCEPT rule // CASE 1: Rule with "from" field — this is a FORWARD ACCEPT rule
if policy.From != "" { if policy.From != "" {
@@ -136,7 +163,7 @@ func (o *Orchestrator) reconcilePolicies(ctx context.Context, cfg *config.Networ
} }
// Unhandled pattern // 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", logger.Warn("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) i, policy.ServiceName, policy.ContainerName, policy.Selector, policy.From, policy.To, policy.Port, policy.Proto, policy.Nat)
} }
} }
@@ -147,32 +174,43 @@ func (o *Orchestrator) applyForwardRule(ctx context.Context, cfg *config.Network
if policy.To != "" { if policy.To != "" {
targetIP = o.resolveIP(policy.To) targetIP = o.resolveIP(policy.To)
} }
logger.Info("FIREWALL: forward rule: from=%q (IP=%s) to=%q (IP=%s) proto=%s port=%s",
policy.From, sourceIP, policy.To, targetIP, proto, port)
// Determine the chain: use DOCKER-USER (iptables-legacy) or FORWARD // Determine the chain: use DOCKER-USER (iptables-legacy) or FORWARD
chain := "FORWARD" chain := "FORWARD"
if o.iptablesMgr.Binary() == "/usr/sbin/iptables-legacy" { if o.iptablesMgr.Binary() == "/usr/sbin/iptables-legacy" {
chain = "DOCKER-USER" chain = "DOCKER-USER"
} }
logger.Debug("FIREWALL: using iptables chain=%s (binary=%s)", chain, o.iptablesMgr.Binary())
// Ensure established/related rule exists at the top // Ensure established/related rule exists at the top
if err := o.iptablesMgr.EnsureEstablishedRelated(chain); err != nil { if err := o.iptablesMgr.EnsureEstablishedRelated(chain); err != nil {
log.Printf("FIREWALL: ERROR ensuring established/related rule in %s: %v", chain, err) logger.Error("FIREWALL: failed to ensure established/related rule in %s: %v", chain, err)
} else {
logger.Debug("FIREWALL: established/related rule ensured in %s", chain)
} }
// Insert the FORWARD ACCEPT rule // Insert the FORWARD ACCEPT rule
if err := o.iptablesMgr.InsertForwardAccept(chain, sourceIP, targetIP, proto, "", port, comment); err != nil { if err := o.iptablesMgr.InsertForwardAccept(chain, sourceIP, targetIP, proto, "", port, comment); err != nil {
log.Printf("FIREWALL: ERROR inserting FORWARD ACCEPT rule: %v", err) logger.Error("FIREWALL: failed to insert FORWARD ACCEPT rule in %s: %v", chain, err)
} else {
logger.Info("FIREWALL: FORWARD ACCEPT rule inserted: chain=%s src=%s dst=%s proto=%s port=%s comment=%q",
chain, sourceIP, targetIP, proto, port, comment)
} }
} }
func (o *Orchestrator) applyNATRule(ctx context.Context, cfg *config.NetworksConfig, policy config.PolicyConfig, proto, port, comment string) { func (o *Orchestrator) applyNATRule(ctx context.Context, cfg *config.NetworksConfig, policy config.PolicyConfig, proto, port, comment string) {
to := policy.To to := policy.To
logger.Info("FIREWALL: NAT rule: to=%s proto=%s port=%s nat=%s iface=%s",
to, proto, port, policy.Nat, policy.Iface)
// Resolve "to" as target IP // Resolve "to" as target IP
targetIP := o.resolveIP(to) targetIP := o.resolveIP(to)
logger.Debug("FIREWALL: resolved target %q -> IP=%q", to, targetIP)
if targetIP == "" { if targetIP == "" {
log.Printf("FIREWALL: WARNING cannot resolve target %s for nat policy", to) logger.Warn("FIREWALL: cannot resolve target %s for nat policy", to)
return return
} }
@@ -185,26 +223,34 @@ func (o *Orchestrator) applyNATRule(ctx context.Context, cfg *config.NetworksCon
if selector == "" { if selector == "" {
selector = policy.Name selector = policy.Name
} }
logger.Debug("FIREWALL: DNAT selector=%s", selector)
// Try to insert rules inside the container namespace via nsenter // Try to insert rules inside the container namespace via nsenter
usedContainer := false usedContainer := false
if selector != "" { if selector != "" {
pid, err := o.dockerClient.GetContainerPID(ctx, selector) pid, err := o.dockerClient.GetContainerPID(ctx, selector)
if err == nil { if err == nil {
logger.Info("FIREWALL: inserting DNAT rule in container %s (PID=%d)", selector, pid)
if err := o.iptablesMgr.InsertPreroutingRuleInContainer(pid, "0.0.0.0/0", proto, port, targetIP, port, comment); err != nil { 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) logger.Error("FIREWALL: failed to insert container PREROUTING rule: %v", err)
} else { } else {
logger.Info("FIREWALL: DNAT rule inserted in container %s: target=%s proto=%s port=%s",
selector, targetIP, proto, port)
usedContainer = true usedContainer = true
} }
} else { } else {
log.Printf("FIREWALL: WARNING cannot get PID for container %s: %v, trying host rules", selector, err) logger.Warn("FIREWALL: cannot get PID for container %s: %v, trying host rules", selector, err)
} }
} }
// Fall back to host-level PREROUTING if container not used // Fall back to host-level PREROUTING if container not used
if !usedContainer && policy.Iface != "" { if !usedContainer && policy.Iface != "" {
logger.Info("FIREWALL: inserting host-level DNAT rule on interface %s", policy.Iface)
if err := o.iptablesMgr.InsertPreroutingRuleOnInterface(policy.Iface, proto, port, targetIP, port, comment); err != nil { if err := o.iptablesMgr.InsertPreroutingRuleOnInterface(policy.Iface, proto, port, targetIP, port, comment); err != nil {
log.Printf("FIREWALL: ERROR inserting interface PREROUTING rule: %v", err) logger.Error("FIREWALL: failed to insert interface PREROUTING rule on %s: %v", policy.Iface, err)
} else {
logger.Info("FIREWALL: host DNAT rule inserted: iface=%s target=%s proto=%s port=%s",
policy.Iface, targetIP, proto, port)
} }
} }
} }
@@ -214,15 +260,19 @@ func (o *Orchestrator) applyNATRule(ctx context.Context, cfg *config.NetworksCon
func (o *Orchestrator) resolveIP(name string) string { func (o *Orchestrator) resolveIP(name string) string {
// If it's already an IP, return it as CIDR // If it's already an IP, return it as CIDR
if config.IsIP(name) { if config.IsIP(name) {
return config.ToCIDR(name) result := config.ToCIDR(name)
logger.Debug("FIREWALL: resolveIP(%q): direct IP -> %s", name, result)
return result
} }
// Use the resolver which looks up from networks.json // Use the resolver which looks up from networks.json
ips := o.resolver.Resolve(name) ips := o.resolver.Resolve(name)
if len(ips) > 0 { if len(ips) > 0 {
logger.Debug("FIREWALL: resolveIP(%q): resolved -> %s", name, ips[0])
return ips[0] return ips[0]
} }
logger.Debug("FIREWALL: resolveIP(%q): not found", name)
return "" return ""
} }
+64 -10
View File
@@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"os/exec" "os/exec"
"strings" "strings"
"firewall_containers/network-go/logger"
) )
// IPTablesAPI defines the interface for iptables operations, enabling mock implementations for testing // IPTablesAPI defines the interface for iptables operations, enabling mock implementations for testing
@@ -40,13 +42,16 @@ func NewManager(debug bool) *Manager {
// detectBinary checks if iptables-legacy is available (matching shell script logic) // detectBinary checks if iptables-legacy is available (matching shell script logic)
func (m *Manager) detectBinary() { func (m *Manager) detectBinary() {
logger.Info("IPTABLES: detecting iptables binary")
cmd := exec.Command("/usr/sbin/iptables-legacy", "-L") cmd := exec.Command("/usr/sbin/iptables-legacy", "-L")
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err == nil && strings.Contains(string(output), "DOCKER-USER") { if err == nil && strings.Contains(string(output), "DOCKER-USER") {
m.binary = "/usr/sbin/iptables-legacy" m.binary = "/usr/sbin/iptables-legacy"
logger.Info("IPTABLES: detected iptables-legacy (DOCKER-USER chain present)")
return return
} }
m.binary = "/usr/sbin/iptables" m.binary = "/usr/sbin/iptables"
logger.Info("IPTABLES: using default iptables binary")
} }
// Binary returns the detected iptables binary path // Binary returns the detected iptables binary path
@@ -56,23 +61,29 @@ func (m *Manager) Binary() string {
// run executes an iptables command on the host // run executes an iptables command on the host
func (m *Manager) run(args ...string) error { func (m *Manager) run(args ...string) error {
cmdStr := m.binary + " " + strings.Join(args, " ")
logger.Info("IPTABLES: executing: %s", cmdStr)
if m.debug { if m.debug {
fmt.Printf("[IPTABLES DEBUG] %s %s\n", m.binary, strings.Join(args, " ")) logger.Debug("IPTABLES DEBUG: %s %s", m.binary, strings.Join(args, " "))
} }
cmd := exec.Command(m.binary, args...) cmd := exec.Command(m.binary, args...)
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
logger.Error("IPTABLES: command failed: %s\noutput: %s", cmdStr, strings.TrimSpace(string(output)))
return fmt.Errorf("iptables %s failed: %w\noutput: %s", strings.Join(args, " "), err, string(output)) return fmt.Errorf("iptables %s failed: %w\noutput: %s", strings.Join(args, " "), err, string(output))
} }
logger.Debug("IPTABLES: command succeeded: %s", cmdStr)
if len(output) > 0 {
logger.Debug("IPTABLES: output: %s", strings.TrimSpace(string(output)))
}
return nil return nil
} }
// runInContainer executes an iptables command inside a container's network namespace via nsenter // runInContainer executes an iptables command inside a container's network namespace via nsenter
func (m *Manager) runInContainer(pid int, table string, args ...string) error { func (m *Manager) runInContainer(pid int, table string, args ...string) error {
iptPath := "/usr/sbin/iptables-legacy" // Use the same binary path that was auto-detected on the host
if !strings.Contains(m.binary, "legacy") { // Alpine installs iptables at /usr/sbin/ not /sbin/
iptPath = "/usr/sbin/iptables" iptPath := m.binary
}
fullArgs := []string{"-t", fmt.Sprintf("%d", pid), "-n", "--", iptPath} fullArgs := []string{"-t", fmt.Sprintf("%d", pid), "-n", "--", iptPath}
if table != "" { if table != "" {
@@ -80,14 +91,22 @@ func (m *Manager) runInContainer(pid int, table string, args ...string) error {
} }
fullArgs = append(fullArgs, args...) fullArgs = append(fullArgs, args...)
cmdStr := "nsenter " + strings.Join(fullArgs, " ")
logger.Info("IPTABLES: executing nsenter for PID %d: %s", pid, cmdStr)
if m.debug { if m.debug {
fmt.Printf("[IPTABLES DEBUG] nsenter %s\n", strings.Join(fullArgs, " ")) logger.Debug("IPTABLES DEBUG: nsenter %s", strings.Join(fullArgs, " "))
} }
cmd := exec.Command("nsenter", fullArgs...) cmd := exec.Command("nsenter", fullArgs...)
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
logger.Error("IPTABLES: nsenter command failed for PID %d\noutput: %s", pid, strings.TrimSpace(string(output)))
return fmt.Errorf("nsenter iptables failed: %w\noutput: %s", err, string(output)) return fmt.Errorf("nsenter iptables failed: %w\noutput: %s", err, string(output))
} }
logger.Info("IPTABLES: nsenter command succeeded for PID %d", pid)
if len(output) > 0 {
logger.Debug("IPTABLES: nsenter output: %s", strings.TrimSpace(string(output)))
}
return nil return nil
} }
@@ -95,38 +114,47 @@ func (m *Manager) runInContainer(pid int, table string, args ...string) error {
// Logs a warning if it fails (e.g. read-only filesystem in a container), // Logs a warning if it fails (e.g. read-only filesystem in a container),
// since this should be configured at the host level. // since this should be configured at the host level.
func (m *Manager) EnsureIPForward() error { func (m *Manager) EnsureIPForward() error {
logger.Info("IPTABLES: enabling IP forwarding (echo 1 > /proc/sys/net/ipv4/ip_forward)")
cmd := exec.Command("sh", "-c", "echo 1 > /proc/sys/net/ipv4/ip_forward") cmd := exec.Command("sh", "-c", "echo 1 > /proc/sys/net/ipv4/ip_forward")
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
logger.Warn("IPTABLES: failed to enable ip_forward (expected if read-only fs): %v", err)
return fmt.Errorf("failed to enable ip_forward: %w\noutput: %s", err, string(output)) return fmt.Errorf("failed to enable ip_forward: %w\noutput: %s", err, string(output))
} }
logger.Info("IPTABLES: IP forwarding enabled")
return nil return nil
} }
// EnsureEstablishedRelated inserts an ESTABLISHED,RELATED accept rule at the top of a chain // EnsureEstablishedRelated inserts an ESTABLISHED,RELATED accept rule at the top of a chain
func (m *Manager) EnsureEstablishedRelated(chain string) error { func (m *Manager) EnsureEstablishedRelated(chain string) error {
logger.Debug("IPTABLES: checking for ESTABLISHED,RELATED rule in %s", chain)
checkArgs := []string{"-w", "-n", "-L", chain} checkArgs := []string{"-w", "-n", "-L", chain}
cmd := exec.Command(m.binary, checkArgs...) cmd := exec.Command(m.binary, checkArgs...)
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
logger.Debug("IPTABLES: could not list chain %s: %v", chain, err)
return nil return nil
} }
if !strings.Contains(string(output), "ESTABLISHED") || !strings.Contains(string(output), "RELATED") { if !strings.Contains(string(output), "ESTABLISHED") || !strings.Contains(string(output), "RELATED") {
logger.Info("IPTABLES: inserting ESTABLISHED,RELATED ACCEPT rule in %s", chain)
args := []string{"-w", "-I", chain, "-m", "state", "--state", "established,related", "-j", "ACCEPT"} args := []string{"-w", "-I", chain, "-m", "state", "--state", "established,related", "-j", "ACCEPT"}
return m.run(args...) return m.run(args...)
} }
logger.Debug("IPTABLES: ESTABLISHED,RELATED rule already exists in %s", chain)
return nil return nil
} }
// DeleteLine deletes a specific line number from a chain // DeleteLine deletes a specific line number from a chain
func (m *Manager) DeleteLine(chain string, lineNum string) error { func (m *Manager) DeleteLine(chain string, lineNum string) error {
logger.Info("IPTABLES: deleting line %s from chain %s", lineNum, chain)
args := []string{"-w", "-D", chain, lineNum} args := []string{"-w", "-D", chain, lineNum}
return m.run(args...) return m.run(args...)
} }
// DeleteLineInContainer deletes a specific line number from a chain inside a container namespace // DeleteLineInContainer deletes a specific line number from a chain inside a container namespace
func (m *Manager) DeleteLineInContainer(pid int, table, chain, lineNum string) error { func (m *Manager) DeleteLineInContainer(pid int, table, chain, lineNum string) error {
logger.Info("IPTABLES: deleting line %s from chain %s in container PID %d", lineNum, chain, pid)
args := []string{"-D", chain, lineNum} args := []string{"-D", chain, lineNum}
return m.runInContainer(pid, table, args...) return m.runInContainer(pid, table, args...)
} }
@@ -141,6 +169,7 @@ func (m *Manager) getLineNumbers(chain, table string, grepPatterns ...string) []
cmd := exec.Command(m.binary, args...) cmd := exec.Command(m.binary, args...)
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
logger.Debug("IPTABLES: getLineNumbers failed for %s: %v", chain, err)
return nil return nil
} }
@@ -161,12 +190,16 @@ func (m *Manager) getLineNumbers(chain, table string, grepPatterns ...string) []
} }
} }
} }
logger.Debug("IPTABLES: getLineNumbers chain=%s patterns=%v found=%v", chain, grepPatterns, matchingLines)
return matchingLines return matchingLines
} }
// deleteMatchingLines deletes all lines in a chain matching the given patterns // deleteMatchingLines deletes all lines in a chain matching the given patterns
func (m *Manager) deleteMatchingLines(chain, table string, grepPatterns ...string) error { func (m *Manager) deleteMatchingLines(chain, table string, grepPatterns ...string) error {
lines := m.getLineNumbers(chain, table, grepPatterns...) lines := m.getLineNumbers(chain, table, grepPatterns...)
if len(lines) > 0 {
logger.Info("IPTABLES: deleting %d matching lines from %s: %v", len(lines), chain, lines)
}
for i := len(lines) - 1; i >= 0; i-- { for i := len(lines) - 1; i >= 0; i-- {
if err := m.DeleteLine(chain, lines[i]); err != nil { if err := m.DeleteLine(chain, lines[i]); err != nil {
return err return err
@@ -177,15 +210,13 @@ func (m *Manager) deleteMatchingLines(chain, table string, grepPatterns ...strin
// deleteMatchingLinesInContainer deletes matching lines inside a container namespace // deleteMatchingLinesInContainer deletes matching lines inside a container namespace
func (m *Manager) deleteMatchingLinesInContainer(pid int, table, chain string, grepPatterns ...string) error { func (m *Manager) deleteMatchingLinesInContainer(pid int, table, chain string, grepPatterns ...string) error {
iptPath := "/usr/sbin/iptables-legacy" iptPath := m.binary
if !strings.Contains(m.binary, "legacy") {
iptPath = "/usr/sbin/iptables"
}
nsenterArgs := []string{"-t", fmt.Sprintf("%d", pid), "-n", "--", iptPath, "-w", "--line-number", "-n", "-t", table, "-L", chain} nsenterArgs := []string{"-t", fmt.Sprintf("%d", pid), "-n", "--", iptPath, "-w", "--line-number", "-n", "-t", table, "-L", chain}
cmd := exec.Command("nsenter", nsenterArgs...) cmd := exec.Command("nsenter", nsenterArgs...)
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
logger.Debug("IPTABLES: deleteMatchingLinesInContainer list failed for PID %d chain %s: %v", pid, chain, err)
return nil return nil
} }
@@ -207,6 +238,11 @@ func (m *Manager) deleteMatchingLinesInContainer(pid int, table, chain string, g
} }
} }
if len(matchingLines) > 0 {
logger.Info("IPTABLES: deleting %d matching lines from container PID %d chain %s: %v",
len(matchingLines), pid, chain, matchingLines)
}
for i := len(matchingLines) - 1; i >= 0; i-- { for i := len(matchingLines) - 1; i >= 0; i-- {
if err := m.DeleteLineInContainer(pid, table, chain, matchingLines[i]); err != nil { if err := m.DeleteLineInContainer(pid, table, chain, matchingLines[i]); err != nil {
return err return err
@@ -217,6 +253,8 @@ 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",
sourceIP, proto, sourcePort, targetIP, targetPort, comment)
patterns := []string{"DNAT", sourcePort, targetIP, targetPort, comment} patterns := []string{"DNAT", sourcePort, targetIP, targetPort, comment}
if err := m.deleteMatchingLines("PREROUTING", "nat", patterns...); err != nil { if err := m.deleteMatchingLines("PREROUTING", "nat", patterns...); err != nil {
return fmt.Errorf("failed to delete old PREROUTING rules: %w", err) return fmt.Errorf("failed to delete old PREROUTING rules: %w", err)
@@ -235,6 +273,8 @@ func (m *Manager) InsertPreroutingRule(sourceIP, proto, sourcePort, targetIP, ta
// InsertPreroutingRuleOnInterface inserts a DNAT PREROUTING rule on a specific interface // InsertPreroutingRuleOnInterface inserts a DNAT PREROUTING rule on a specific interface
func (m *Manager) InsertPreroutingRuleOnInterface(iface, proto, sourcePort, targetIP, targetPort, comment string) error { func (m *Manager) InsertPreroutingRuleOnInterface(iface, proto, sourcePort, targetIP, targetPort, comment string) error {
logger.Info("IPTABLES: inserting PREROUTING DNAT rule on interface %s: proto=%s dport=%s -> %s:%s comment=%q",
iface, proto, sourcePort, targetIP, targetPort, comment)
args := []string{ args := []string{
"-w", "-t", "nat", "-I", "PREROUTING", "-w", "-t", "nat", "-I", "PREROUTING",
"-i", iface, "-i", iface,
@@ -248,6 +288,8 @@ func (m *Manager) InsertPreroutingRuleOnInterface(iface, proto, sourcePort, targ
// InsertPostroutingMasquerade inserts a MASQUERADE POSTROUTING rule on the host // InsertPostroutingMasquerade inserts a MASQUERADE POSTROUTING rule on the host
func (m *Manager) InsertPostroutingMasquerade(sourceCIDR, proto, sourcePort, comment string) error { func (m *Manager) InsertPostroutingMasquerade(sourceCIDR, proto, sourcePort, comment string) error {
logger.Info("IPTABLES: inserting POSTROUTING MASQUERADE rule: src=%s proto=%s sport=%s comment=%q",
sourceCIDR, proto, sourcePort, comment)
patterns := []string{"MASQUERADE", comment, sourceCIDR, sourcePort} patterns := []string{"MASQUERADE", comment, sourceCIDR, sourcePort}
if err := m.deleteMatchingLines("POSTROUTING", "nat", patterns...); err != nil { if err := m.deleteMatchingLines("POSTROUTING", "nat", patterns...); err != nil {
return fmt.Errorf("failed to delete old POSTROUTING rules: %w", err) return fmt.Errorf("failed to delete old POSTROUTING rules: %w", err)
@@ -266,6 +308,8 @@ 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",
targetCIDR, proto, targetPort, comment)
patterns := []string{"MASQUERADE", comment, targetCIDR, targetPort} patterns := []string{"MASQUERADE", comment, targetCIDR, targetPort}
if err := m.deleteMatchingLines("POSTROUTING", "nat", patterns...); err != nil { if err := m.deleteMatchingLines("POSTROUTING", "nat", patterns...); err != nil {
return fmt.Errorf("failed to delete old POSTROUTING rules: %w", err) return fmt.Errorf("failed to delete old POSTROUTING rules: %w", err)
@@ -284,6 +328,8 @@ 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",
chain, sourceIP, targetIP, proto, sourcePort, targetPort, comment)
var grepPatterns []string var grepPatterns []string
grepPatterns = append(grepPatterns, proto) grepPatterns = append(grepPatterns, proto)
if sourceIP != "" { if sourceIP != "" {
@@ -323,7 +369,11 @@ func (m *Manager) InsertForwardAccept(chain, sourceIP, targetIP, proto, sourcePo
// DeleteForwardAccept deletes a FORWARD ACCEPT rule by comment // DeleteForwardAccept deletes a FORWARD ACCEPT rule by comment
func (m *Manager) DeleteForwardAccept(chain, comment string) error { func (m *Manager) DeleteForwardAccept(chain, comment string) error {
logger.Info("IPTABLES: deleting FORWARD ACCEPT rules in %s with comment=%q", chain, comment)
lines := m.getLineNumbers(chain, "", comment) lines := m.getLineNumbers(chain, "", comment)
if len(lines) > 0 {
logger.Info("IPTABLES: found %d FORWARD ACCEPT rules to delete: %v", len(lines), lines)
}
for i := len(lines) - 1; i >= 0; i-- { for i := len(lines) - 1; i >= 0; i-- {
if err := m.DeleteLine(chain, lines[i]); err != nil { if err := m.DeleteLine(chain, lines[i]); err != nil {
return err return err
@@ -334,6 +384,8 @@ func (m *Manager) DeleteForwardAccept(chain, comment string) error {
// InsertPreroutingRuleInContainer inserts a DNAT PREROUTING rule inside a container namespace // InsertPreroutingRuleInContainer inserts a DNAT PREROUTING rule inside a container namespace
func (m *Manager) InsertPreroutingRuleInContainer(pid int, sourceIP, proto, sourcePort, targetIP, targetPort, comment string) error { 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)
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 err := m.deleteMatchingLinesInContainer(pid, "nat", "PREROUTING", patterns...); err != nil {
return fmt.Errorf("failed to delete old container PREROUTING rules: %w", err) return fmt.Errorf("failed to delete old container PREROUTING rules: %w", err)
@@ -352,6 +404,8 @@ func (m *Manager) InsertPreroutingRuleInContainer(pid int, sourceIP, proto, sour
// InsertPostroutingMasqueradeInContainer inserts a MASQUERADE POSTROUTING rule inside a container namespace // InsertPostroutingMasqueradeInContainer inserts a MASQUERADE POSTROUTING rule inside a container namespace
func (m *Manager) InsertPostroutingMasqueradeInContainer(pid int, sourceCIDR, proto, sourcePort, comment string) error { 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)
patterns := []string{"MASQUERADE", comment, sourceCIDR, sourcePort} patterns := []string{"MASQUERADE", comment, sourceCIDR, sourcePort}
if err := m.deleteMatchingLinesInContainer(pid, "nat", "POSTROUTING", patterns...); err != nil { if err := m.deleteMatchingLinesInContainer(pid, "nat", "POSTROUTING", patterns...); err != nil {
return fmt.Errorf("failed to delete old container POSTROUTING rules: %w", err) return fmt.Errorf("failed to delete old container POSTROUTING rules: %w", err)
+154
View File
@@ -0,0 +1,154 @@
package logger
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"sync"
"time"
)
// Log levels
const (
LevelInfo = "INFO"
LevelWarn = "WARNING"
LevelError = "ERROR"
LevelDebug = "DEBUG"
)
// Logger provides dual-output logging to stdout and a file
type Logger struct {
infoLog *log.Logger
warnLog *log.Logger
errorLog *log.Logger
debugLog *log.Logger
debug bool
mu sync.Mutex
}
var defaultLogger *Logger
func init() {
defaultLogger = NewLogger(false, "")
}
// SetDefault sets the default logger instance
func SetDefault(l *Logger) {
defaultLogger = l
}
// Default returns the current default logger
func Default() *Logger {
return defaultLogger
}
// NewLogger creates a new dual-output logger.
// If logPath is empty, only stdout is used.
func NewLogger(debug bool, logPath string) *Logger {
flags := log.Ldate | log.Ltime | log.Lmicroseconds
var infoWriter, warnWriter, errorWriter, debugWriter io.Writer
if logPath != "" {
// Ensure directory exists
dir := filepath.Dir(logPath)
if err := os.MkdirAll(dir, 0755); err == nil {
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err == nil {
// Multi-writer: stdout + file
infoWriter = io.MultiWriter(os.Stdout, file)
warnWriter = io.MultiWriter(os.Stdout, file)
errorWriter = io.MultiWriter(os.Stderr, file)
debugWriter = io.MultiWriter(os.Stdout, file)
fmt.Fprintf(os.Stdout, "%s [%s] LOGGER: log file opened: %s\n",
time.Now().Format("2006/01/02 15:04:05.000"), LevelInfo, logPath)
} else {
fmt.Fprintf(os.Stderr, "%s [%s] LOGGER: failed to open log file %s: %v, using stdout only\n",
time.Now().Format("2006/01/02 15:04:05.000"), LevelWarn, logPath, err)
infoWriter = os.Stdout
warnWriter = os.Stdout
errorWriter = os.Stderr
debugWriter = os.Stdout
}
} else {
fmt.Fprintf(os.Stderr, "%s [%s] LOGGER: failed to create log dir %s: %v, using stdout only\n",
time.Now().Format("2006/01/02 15:04:05.000"), LevelWarn, dir, err)
infoWriter = os.Stdout
warnWriter = os.Stdout
errorWriter = os.Stderr
debugWriter = os.Stdout
}
} else {
infoWriter = os.Stdout
warnWriter = os.Stdout
errorWriter = os.Stderr
debugWriter = os.Stdout
}
return &Logger{
infoLog: log.New(infoWriter, "", flags),
warnLog: log.New(warnWriter, "", flags),
errorLog: log.New(errorWriter, "", flags),
debugLog: log.New(debugWriter, "", flags),
debug: debug,
}
}
// Info logs an INFO message
func (l *Logger) Info(format string, args ...interface{}) {
l.mu.Lock()
defer l.mu.Unlock()
l.infoLog.Printf("[%s] %s", LevelInfo, fmt.Sprintf(format, args...))
}
// Warn logs a WARNING message
func (l *Logger) Warn(format string, args ...interface{}) {
l.mu.Lock()
defer l.mu.Unlock()
l.warnLog.Printf("[%s] %s", LevelWarn, fmt.Sprintf(format, args...))
}
// Error logs an ERROR message
func (l *Logger) Error(format string, args ...interface{}) {
l.mu.Lock()
defer l.mu.Unlock()
l.errorLog.Printf("[%s] %s", LevelError, fmt.Sprintf(format, args...))
}
// Debug logs a DEBUG message (only if debug mode is enabled)
func (l *Logger) Debug(format string, args ...interface{}) {
if !l.debug {
return
}
l.mu.Lock()
defer l.mu.Unlock()
l.debugLog.Printf("[%s] %s", LevelDebug, fmt.Sprintf(format, args...))
}
// Info logs an INFO message using the default logger
func Info(format string, args ...interface{}) {
defaultLogger.Info(format, args...)
}
// Warn logs a WARNING message using the default logger
func Warn(format string, args ...interface{}) {
defaultLogger.Warn(format, args...)
}
// Error logs an ERROR message using the default logger
func Error(format string, args ...interface{}) {
defaultLogger.Error(format, args...)
}
// Debug logs a DEBUG message using the default logger
func Debug(format string, args ...interface{}) {
defaultLogger.Debug(format, args...)
}
// SetDebug enables or disables debug logging
func (l *Logger) SetDebug(debug bool) {
l.debug = debug
}
+34 -20
View File
@@ -2,7 +2,6 @@ package main
import ( import (
"context" "context"
"log"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
@@ -12,21 +11,24 @@ import (
"firewall_containers/network-go/docker" "firewall_containers/network-go/docker"
"firewall_containers/network-go/firewall" "firewall_containers/network-go/firewall"
"firewall_containers/network-go/iptables" "firewall_containers/network-go/iptables"
"firewall_containers/network-go/logger"
"firewall_containers/network-go/watcher" "firewall_containers/network-go/watcher"
) )
// Config path - can be overridden via environment variable // Config path - can be overridden via environment variable
const defaultConfigPath = "/etc/user/config/networks.json" const defaultConfigPath = "/etc/user/config/networks.json"
// Log path - can be overridden via environment variable
const defaultLogPath = "/var/log/network-go/network-go.log"
// Watch period - can be overridden via environment variable // Watch period - can be overridden via environment variable
const defaultWatchPeriod = 30 * time.Second const defaultWatchPeriod = 30 * time.Second
func getConfigPath() string { func getEnv(key, defaultVal string) string {
path := os.Getenv("NETWORKS_CONFIG_PATH") if val := os.Getenv(key); val != "" {
if path == "" { return val
return defaultConfigPath
} }
return path return defaultVal
} }
func getWatchPeriod() time.Duration { func getWatchPeriod() time.Duration {
@@ -36,7 +38,7 @@ func getWatchPeriod() time.Duration {
} }
seconds, err := time.ParseDuration(periodStr + "s") seconds, err := time.ParseDuration(periodStr + "s")
if err != nil { if err != nil {
log.Printf("MAIN: invalid WATCH_PERIOD_SECONDS=%s, using default %s", periodStr, defaultWatchPeriod) logger.Warn("MAIN: invalid WATCH_PERIOD_SECONDS=%s, using default %s", periodStr, defaultWatchPeriod)
return defaultWatchPeriod return defaultWatchPeriod
} }
return seconds return seconds
@@ -47,34 +49,44 @@ func getDebug() bool {
} }
func main() { func main() {
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) configPath := getEnv("NETWORKS_CONFIG_PATH", defaultConfigPath)
log.Println("MAIN: starting network-go firewall container manager") logPath := getEnv("NETWORK_GO_LOG_PATH", defaultLogPath)
configPath := getConfigPath()
watchPeriod := getWatchPeriod() watchPeriod := getWatchPeriod()
debug := getDebug() debug := getDebug()
log.Printf("MAIN: config path = %s", configPath) // Initialize the dual-output logger (stdout + file)
log.Printf("MAIN: watch period = %s", watchPeriod) log := logger.NewLogger(debug, logPath)
log.Printf("MAIN: debug = %v", debug) logger.SetDefault(log)
log.Info("MAIN: starting network-go firewall container manager")
log.Info("MAIN: config path = %s", configPath)
log.Info("MAIN: log path = %s", logPath)
log.Info("MAIN: watch period = %s", watchPeriod)
log.Info("MAIN: debug = %v", debug)
// Create Docker client (uses DOCKER_HOST env var automatically) // Create Docker client (uses DOCKER_HOST env var automatically)
dockerClient, err := docker.NewClient() dockerClient, err := docker.NewClient()
if err != nil { if err != nil {
log.Fatalf("MAIN: failed to create Docker client: %v", err) log.Error("MAIN: failed to create Docker client: %v", err)
os.Exit(1)
} }
defer dockerClient.Close() defer dockerClient.Close()
log.Info("MAIN: Docker client created")
// Create iptables manager // Create iptables manager
iptablesMgr := iptables.NewManager(debug) iptablesMgr := iptables.NewManager(debug)
log.Info("MAIN: iptables manager created (binary=%s)", iptablesMgr.Binary())
ctx := context.Background() ctx := context.Background()
// Load initial config // Load initial config
cfg, err := config.Load(configPath) cfg, err := config.Load(configPath)
if err != nil { if err != nil {
log.Fatalf("MAIN: failed to load initial config: %v", err) log.Error("MAIN: failed to load initial config: %v", err)
os.Exit(1)
} }
log.Info("MAIN: config loaded: %d networks, %d IPs, %d policies",
len(cfg.Networks), len(cfg.IPs), len(cfg.Policies))
// Create the firewall orchestrator (needs config for resolver) // Create the firewall orchestrator (needs config for resolver)
orchestrator := firewall.NewOrchestrator(dockerClient, iptablesMgr, cfg) orchestrator := firewall.NewOrchestrator(dockerClient, iptablesMgr, cfg)
@@ -84,12 +96,14 @@ func main() {
// Set up file watcher to detect changes and re-reconcile // Set up file watcher to detect changes and re-reconcile
onChange := func() { onChange := func() {
log.Println("MAIN: config file change detected, reloading and reconciling") log.Info("MAIN: config file change detected, reloading and reconciling")
newCfg, err := config.Load(configPath) newCfg, err := config.Load(configPath)
if err != nil { if err != nil {
log.Printf("MAIN: failed to reload config: %v", err) log.Error("MAIN: failed to reload config: %v", err)
return return
} }
log.Info("MAIN: config reloaded: %d networks, %d IPs, %d policies",
len(newCfg.Networks), len(newCfg.IPs), len(newCfg.Policies))
orchestrator.ReconcileAll(ctx, newCfg) orchestrator.ReconcileAll(ctx, newCfg)
} }
@@ -101,7 +115,7 @@ func main() {
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
sig := <-sigCh sig := <-sigCh
log.Printf("MAIN: received signal %v, shutting down", sig) log.Info("MAIN: received signal %v, shutting down", sig)
fileWatcher.Stop() fileWatcher.Stop()
log.Println("MAIN: shutdown complete") log.Info("MAIN: shutdown complete")
} }
+24 -4
View File
@@ -1,10 +1,10 @@
package resolver package resolver
import ( import (
"log"
"strings" "strings"
"firewall_containers/network-go/config" "firewall_containers/network-go/config"
"firewall_containers/network-go/logger"
) )
// Resolver resolves names to IPs using the networks.json configuration // Resolver resolves names to IPs using the networks.json configuration
@@ -15,53 +15,73 @@ type Resolver struct {
// NewResolver creates a new name resolver backed by the networks.json config // NewResolver creates a new name resolver backed by the networks.json config
func NewResolver(cfg *config.NetworksConfig) *Resolver { func NewResolver(cfg *config.NetworksConfig) *Resolver {
return &Resolver{ r := &Resolver{
cfg: cfg, cfg: cfg,
retries: 2, retries: 2,
} }
if cfg != nil {
logger.Debug("RESOLVER: created with %d IPs in config", len(cfg.IPs))
} else {
logger.Debug("RESOLVER: created with nil config")
}
return r
} }
// SetConfig updates the config used for name resolution // SetConfig updates the config used for name resolution
func (r *Resolver) SetConfig(cfg *config.NetworksConfig) { func (r *Resolver) SetConfig(cfg *config.NetworksConfig) {
r.cfg = cfg r.cfg = cfg
if cfg != nil {
logger.Debug("RESOLVER: config updated with %d IPs", len(cfg.IPs))
} else {
logger.Debug("RESOLVER: config set to nil")
}
} }
// SetRetries sets the number of retries for resolution // SetRetries sets the number of retries for resolution
func (r *Resolver) SetRetries(n int) { func (r *Resolver) SetRetries(n int) {
r.retries = n r.retries = n
logger.Debug("RESOLVER: retries set to %d", n)
} }
// Resolve resolves a name to one or more IP addresses // Resolve resolves a name to one or more IP addresses
// It looks up the name in the networks.json config by container_name and selector fields // It looks up the name in the networks.json config by container_name and selector fields
func (r *Resolver) Resolve(name string) []string { func (r *Resolver) Resolve(name string) []string {
if r.cfg == nil { if r.cfg == nil {
logger.Debug("RESOLVER: resolve(%q): config is nil", name)
return nil return nil
} }
logger.Debug("RESOLVER: resolving %q", name)
var ips []string var ips []string
// Look up by container_name and selector in the IPs section // Look up by container_name and selector in the IPs section
for _, ipCfg := range r.cfg.IPs { for _, ipCfg := range r.cfg.IPs {
if ipCfg.ContainerName == name || ipCfg.Selector == name { if ipCfg.ContainerName == name || ipCfg.Selector == name {
logger.Debug("RESOLVER: exact match for %q: container=%s selector=%s -> IP=%s",
name, ipCfg.ContainerName, ipCfg.Selector, ipCfg.IP)
ips = append(ips, ipCfg.IP) ips = append(ips, ipCfg.IP)
} }
} }
// If no exact match, try prefix matching: extract the prefix before the first dash // If no exact match, try prefix matching: extract the prefix before the first dash
// and match any container/selector starting with that prefix. // and match any container/selector starting with that prefix.
// This allows "wireguardproxy-client" to match "wireguardproxyclient" (after dash stripping) // This allows "wireguardproxy-client" to match "wireguardproxyclient" (after dash stripping)
// or "app-1"/"app-2" to match "app-x". // or "app-1"/"app-2" to match "app-x".
if len(ips) == 0 && strings.Contains(name, "-") { if len(ips) == 0 && strings.Contains(name, "-") {
prefix := name[:strings.Index(name, "-")] prefix := name[:strings.Index(name, "-")]
logger.Debug("RESOLVER: prefix matching %q with prefix %q", name, prefix)
for _, ipCfg := range r.cfg.IPs { for _, ipCfg := range r.cfg.IPs {
if strings.HasPrefix(ipCfg.ContainerName, prefix) || strings.HasPrefix(ipCfg.Selector, prefix) { if strings.HasPrefix(ipCfg.ContainerName, prefix) || strings.HasPrefix(ipCfg.Selector, prefix) {
logger.Debug("RESOLVER: prefix match for %q: %s -> IP=%s", prefix, ipCfg.ContainerName, ipCfg.IP)
ips = append(ips, ipCfg.IP) ips = append(ips, ipCfg.IP)
} }
} }
} }
if len(ips) == 0 { if len(ips) == 0 {
log.Printf("RESOLVER: no IP found for %s in networks.json config", name) logger.Warn("RESOLVER: no IP found for %q in networks.json config", name)
} else {
logger.Info("RESOLVER: %q resolved to %v", name, ips)
} }
return ips return ips
+7 -6
View File
@@ -3,9 +3,10 @@ package watcher
import ( import (
"crypto/md5" "crypto/md5"
"fmt" "fmt"
"log"
"os" "os"
"time" "time"
"firewall_containers/network-go/logger"
) )
// FileWatcher periodically checks a file for changes using MD5 hash // FileWatcher periodically checks a file for changes using MD5 hash
@@ -42,7 +43,7 @@ func (fw *FileWatcher) Start() {
// Compute initial hash // Compute initial hash
hash, err := fw.hashFile() hash, err := fw.hashFile()
if err != nil { if err != nil {
log.Printf("WATCHER: initial hash computation failed for %s: %v", fw.path, err) logger.Warn("WATCHER: initial hash computation failed for %s: %v", fw.path, err)
} else { } else {
fw.lastHash = hash fw.lastHash = hash
} }
@@ -51,22 +52,22 @@ func (fw *FileWatcher) Start() {
ticker := time.NewTicker(fw.period) ticker := time.NewTicker(fw.period)
defer ticker.Stop() defer ticker.Stop()
log.Printf("WATCHER: started watching %s every %s", fw.path, fw.period) logger.Info("WATCHER: started watching %s every %s", fw.path, fw.period)
for { for {
select { select {
case <-fw.stopCh: case <-fw.stopCh:
log.Printf("WATCHER: stopped watching %s", fw.path) logger.Info("WATCHER: stopped watching %s", fw.path)
return return
case <-ticker.C: case <-ticker.C:
hash, err := fw.hashFile() hash, err := fw.hashFile()
if err != nil { if err != nil {
log.Printf("WATCHER: failed to hash %s: %v", fw.path, err) logger.Warn("WATCHER: failed to hash %s: %v", fw.path, err)
continue continue
} }
if hash != fw.lastHash { if hash != fw.lastHash {
log.Printf("WATCHER: detected change in %s", fw.path) logger.Info("WATCHER: detected change in %s", fw.path)
fw.lastHash = hash fw.lastHash = hash
if fw.onChange != nil { if fw.onChange != nil {
fw.onChange() fw.onChange()