feat: add logging to Docker and iptables operations, fix iptables path
continuous-integration/drone/push Build is passing
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:
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
"firewall_containers/network-go/config"
|
||||
"firewall_containers/network-go/logger"
|
||||
)
|
||||
|
||||
// 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
|
||||
func NewClient() (*Client, error) {
|
||||
logger.Info("DOCKER: creating Docker client (using DOCKER_HOST env)")
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// Close closes the Docker client
|
||||
func (c *Client) Close() error {
|
||||
logger.Debug("DOCKER: closing Docker client")
|
||||
return c.cli.Close()
|
||||
}
|
||||
|
||||
// EnsureNetwork creates a Docker network if it does not already exist
|
||||
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{
|
||||
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 {
|
||||
if n.Name == netCfg.NetworkName {
|
||||
logger.Debug("DOCKER: network %q already exists", netCfg.NetworkName)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -95,26 +106,30 @@ func (c *Client) EnsureNetwork(ctx context.Context, netCfg config.NetworkConfig)
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// RemoveNetwork removes a Docker network
|
||||
func (c *Client) RemoveNetwork(ctx context.Context, networkName string) error {
|
||||
logger.Info("DOCKER: removing network %q", networkName)
|
||||
err := c.cli.NetworkRemove(ctx, networkName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove network %s: %w", networkName, err)
|
||||
}
|
||||
logger.Info("DOCKER: network %q removed", networkName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConnectContainer connects a container to a network with a specific IP
|
||||
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{
|
||||
IPAMConfig: &network.EndpointIPAMConfig{
|
||||
IPv4Address: ip,
|
||||
@@ -125,33 +140,41 @@ func (c *Client) ConnectContainer(ctx context.Context, containerName, networkNam
|
||||
if err != nil {
|
||||
// "endpoint with name ... already exists" is expected on re-reconciliation
|
||||
if strings.Contains(err.Error(), "already exists") {
|
||||
logger.Info("DOCKER: container %q already connected to network %q", containerName, networkName)
|
||||
return nil
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// DisconnectContainer disconnects a container from a network
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// InspectContainer returns the container's details
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
logger.Info("DOCKER: waiting for container %q to be running (timeout=%s)", containerName, timeout)
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
@@ -165,17 +188,21 @@ func (c *Client) WaitForContainerRunning(ctx context.Context, containerName stri
|
||||
case <-ticker.C:
|
||||
container, err := c.cli.ContainerInspect(ctx, containerName)
|
||||
if err != nil {
|
||||
logger.Debug("DOCKER: container %q inspect failed (not ready yet): %v", containerName, err)
|
||||
continue
|
||||
}
|
||||
if container.State != nil && container.State.Running {
|
||||
logger.Info("DOCKER: container %q is now running", containerName)
|
||||
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
|
||||
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)
|
||||
if err != nil {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
logger.Info("DOCKER: adding route in container %q (PID=%d): %s via %s", containerName, pid, network, gateway)
|
||||
args := []string{
|
||||
"-t", fmt.Sprintf("%d", pid),
|
||||
"-n", "--",
|
||||
@@ -204,6 +233,8 @@ func (c *Client) AddRouteInContainer(ctx context.Context, containerName, network
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -211,6 +242,8 @@ func (c *Client) AddRouteInContainer(ctx context.Context, containerName, network
|
||||
// First tries exact name match, then exact selector, then prefix matching
|
||||
// (matching the old shell script's grep $D"-" behavior).
|
||||
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
|
||||
// Docker API name filter does an exact match by default
|
||||
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 {
|
||||
cName := strings.TrimPrefix(containers[0].Names[0], "/")
|
||||
logger.Info("DOCKER: found container by exact name match: %q", cName)
|
||||
return cName, nil
|
||||
}
|
||||
|
||||
// Try exact selector match
|
||||
if selector != "" && selector != name {
|
||||
logger.Debug("DOCKER: trying selector match: %q", selector)
|
||||
containers, err = c.cli.ContainerList(ctx, container.ListOptions{
|
||||
Filters: filters.NewArgs(
|
||||
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 {
|
||||
cName := strings.TrimPrefix(containers[0].Names[0], "/")
|
||||
logger.Info("DOCKER: found container by selector match: %q", cName)
|
||||
return cName, nil
|
||||
}
|
||||
}
|
||||
@@ -249,6 +285,7 @@ func (c *Client) FindContainerName(ctx context.Context, name, selector string) (
|
||||
if strings.Contains(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{
|
||||
Filters: filters.NewArgs(
|
||||
@@ -257,16 +294,18 @@ func (c *Client) FindContainerName(ctx context.Context, name, selector string) (
|
||||
),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Debug("DOCKER: prefix list failed: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, c := range containers {
|
||||
for _, cName := range c.Names {
|
||||
cName = strings.TrimPrefix(cName, "/")
|
||||
logger.Info("DOCKER: found container by prefix match %q-: %q", prefix, cName)
|
||||
return cName, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no running container found matching name=%q selector=%q", name, selector)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package firewall
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -10,6 +9,7 @@ import (
|
||||
"firewall_containers/network-go/config"
|
||||
"firewall_containers/network-go/docker"
|
||||
"firewall_containers/network-go/iptables"
|
||||
"firewall_containers/network-go/logger"
|
||||
"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
|
||||
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
|
||||
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)
|
||||
logger.Warn("FIREWALL: could not enable ip_forward: %v", err)
|
||||
} else {
|
||||
log.Println("FIREWALL: IP forwarding enabled")
|
||||
logger.Info("FIREWALL: IP forwarding enabled")
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
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)
|
||||
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 {
|
||||
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 {
|
||||
networkName := findNetworkForIP(cfg, ipStr)
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
// (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)
|
||||
logger.Warn("FIREWALL: container %s (selector=%s) not found: %v, using config name anyway",
|
||||
ipCfg.ContainerName, ipCfg.Selector, err)
|
||||
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)
|
||||
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)
|
||||
}
|
||||
waitErr := o.dockerClient.WaitForContainerRunning(waitCtx, containerName, 10*time.Second)
|
||||
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 {
|
||||
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
|
||||
func (o *Orchestrator) reconcilePolicies(ctx context.Context, cfg *config.NetworksConfig) {
|
||||
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
|
||||
if proto == "" {
|
||||
@@ -122,6 +148,7 @@ func (o *Orchestrator) reconcilePolicies(ctx context.Context, cfg *config.Networ
|
||||
comment = policy.ServiceName
|
||||
}
|
||||
}
|
||||
logger.Debug("FIREWALL: policy[%d] comment=%q", i, comment)
|
||||
|
||||
// CASE 1: Rule with "from" field — this is a FORWARD ACCEPT rule
|
||||
if policy.From != "" {
|
||||
@@ -136,7 +163,7 @@ func (o *Orchestrator) reconcilePolicies(ctx context.Context, cfg *config.Networ
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -147,32 +174,43 @@ func (o *Orchestrator) applyForwardRule(ctx context.Context, cfg *config.Network
|
||||
if 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
|
||||
chain := "FORWARD"
|
||||
if o.iptablesMgr.Binary() == "/usr/sbin/iptables-legacy" {
|
||||
chain = "DOCKER-USER"
|
||||
}
|
||||
logger.Debug("FIREWALL: using iptables chain=%s (binary=%s)", chain, o.iptablesMgr.Binary())
|
||||
|
||||
// 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)
|
||||
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
|
||||
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) {
|
||||
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
|
||||
targetIP := o.resolveIP(to)
|
||||
logger.Debug("FIREWALL: resolved target %q -> IP=%q", to, 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
|
||||
}
|
||||
|
||||
@@ -185,26 +223,34 @@ func (o *Orchestrator) applyNATRule(ctx context.Context, cfg *config.NetworksCon
|
||||
if selector == "" {
|
||||
selector = policy.Name
|
||||
}
|
||||
logger.Debug("FIREWALL: DNAT selector=%s", selector)
|
||||
|
||||
// Try to insert rules inside the container namespace via nsenter
|
||||
usedContainer := false
|
||||
if selector != "" {
|
||||
pid, err := o.dockerClient.GetContainerPID(ctx, selector)
|
||||
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 {
|
||||
log.Printf("FIREWALL: ERROR inserting container PREROUTING rule: %v", err)
|
||||
logger.Error("FIREWALL: failed to insert container PREROUTING rule: %v", err)
|
||||
} else {
|
||||
logger.Info("FIREWALL: DNAT rule inserted in container %s: target=%s proto=%s port=%s",
|
||||
selector, targetIP, proto, port)
|
||||
usedContainer = true
|
||||
}
|
||||
} 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
|
||||
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 {
|
||||
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 {
|
||||
// If it's already an IP, return it as CIDR
|
||||
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
|
||||
ips := o.resolver.Resolve(name)
|
||||
if len(ips) > 0 {
|
||||
logger.Debug("FIREWALL: resolveIP(%q): resolved -> %s", name, ips[0])
|
||||
return ips[0]
|
||||
}
|
||||
|
||||
logger.Debug("FIREWALL: resolveIP(%q): not found", name)
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"firewall_containers/network-go/logger"
|
||||
)
|
||||
|
||||
// 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)
|
||||
func (m *Manager) detectBinary() {
|
||||
logger.Info("IPTABLES: detecting iptables binary")
|
||||
cmd := exec.Command("/usr/sbin/iptables-legacy", "-L")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err == nil && strings.Contains(string(output), "DOCKER-USER") {
|
||||
m.binary = "/usr/sbin/iptables-legacy"
|
||||
logger.Info("IPTABLES: detected iptables-legacy (DOCKER-USER chain present)")
|
||||
return
|
||||
}
|
||||
m.binary = "/usr/sbin/iptables"
|
||||
logger.Info("IPTABLES: using default iptables binary")
|
||||
}
|
||||
|
||||
// Binary returns the detected iptables binary path
|
||||
@@ -56,23 +61,29 @@ func (m *Manager) Binary() string {
|
||||
|
||||
// run executes an iptables command on the host
|
||||
func (m *Manager) run(args ...string) error {
|
||||
cmdStr := m.binary + " " + strings.Join(args, " ")
|
||||
logger.Info("IPTABLES: executing: %s", cmdStr)
|
||||
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...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
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))
|
||||
}
|
||||
logger.Debug("IPTABLES: command succeeded: %s", cmdStr)
|
||||
if len(output) > 0 {
|
||||
logger.Debug("IPTABLES: output: %s", strings.TrimSpace(string(output)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runInContainer executes an iptables command inside a container's network namespace via nsenter
|
||||
func (m *Manager) runInContainer(pid int, table string, args ...string) error {
|
||||
iptPath := "/usr/sbin/iptables-legacy"
|
||||
if !strings.Contains(m.binary, "legacy") {
|
||||
iptPath = "/usr/sbin/iptables"
|
||||
}
|
||||
// Use the same binary path that was auto-detected on the host
|
||||
// Alpine installs iptables at /usr/sbin/ not /sbin/
|
||||
iptPath := m.binary
|
||||
|
||||
fullArgs := []string{"-t", fmt.Sprintf("%d", pid), "-n", "--", iptPath}
|
||||
if table != "" {
|
||||
@@ -80,14 +91,22 @@ func (m *Manager) runInContainer(pid int, table string, args ...string) error {
|
||||
}
|
||||
fullArgs = append(fullArgs, args...)
|
||||
|
||||
cmdStr := "nsenter " + strings.Join(fullArgs, " ")
|
||||
logger.Info("IPTABLES: executing nsenter for PID %d: %s", pid, cmdStr)
|
||||
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...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
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))
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
// since this should be configured at the host level.
|
||||
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")
|
||||
output, err := cmd.CombinedOutput()
|
||||
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))
|
||||
}
|
||||
logger.Info("IPTABLES: IP forwarding enabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureEstablishedRelated inserts an ESTABLISHED,RELATED accept rule at the top of a chain
|
||||
func (m *Manager) EnsureEstablishedRelated(chain string) error {
|
||||
logger.Debug("IPTABLES: checking for ESTABLISHED,RELATED rule in %s", chain)
|
||||
checkArgs := []string{"-w", "-n", "-L", chain}
|
||||
cmd := exec.Command(m.binary, checkArgs...)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
logger.Debug("IPTABLES: could not list chain %s: %v", chain, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
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"}
|
||||
return m.run(args...)
|
||||
}
|
||||
logger.Debug("IPTABLES: ESTABLISHED,RELATED rule already exists in %s", chain)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteLine deletes a specific line number from a chain
|
||||
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}
|
||||
return m.run(args...)
|
||||
}
|
||||
|
||||
// DeleteLineInContainer deletes a specific line number from a chain inside a container namespace
|
||||
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}
|
||||
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...)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
logger.Debug("IPTABLES: getLineNumbers failed for %s: %v", chain, err)
|
||||
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
|
||||
}
|
||||
|
||||
// deleteMatchingLines deletes all lines in a chain matching the given patterns
|
||||
func (m *Manager) deleteMatchingLines(chain, table string, grepPatterns ...string) error {
|
||||
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-- {
|
||||
if err := m.DeleteLine(chain, lines[i]); err != nil {
|
||||
return err
|
||||
@@ -177,15 +210,13 @@ func (m *Manager) deleteMatchingLines(chain, table string, grepPatterns ...strin
|
||||
|
||||
// deleteMatchingLinesInContainer deletes matching lines inside a container namespace
|
||||
func (m *Manager) deleteMatchingLinesInContainer(pid int, table, chain string, grepPatterns ...string) error {
|
||||
iptPath := "/usr/sbin/iptables-legacy"
|
||||
if !strings.Contains(m.binary, "legacy") {
|
||||
iptPath = "/usr/sbin/iptables"
|
||||
}
|
||||
iptPath := m.binary
|
||||
|
||||
nsenterArgs := []string{"-t", fmt.Sprintf("%d", pid), "-n", "--", iptPath, "-w", "--line-number", "-n", "-t", table, "-L", chain}
|
||||
cmd := exec.Command("nsenter", nsenterArgs...)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
logger.Debug("IPTABLES: deleteMatchingLinesInContainer list failed for PID %d chain %s: %v", pid, chain, err)
|
||||
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-- {
|
||||
if err := m.DeleteLineInContainer(pid, table, chain, matchingLines[i]); err != nil {
|
||||
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
|
||||
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}
|
||||
if err := m.deleteMatchingLines("PREROUTING", "nat", patterns...); err != nil {
|
||||
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
|
||||
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{
|
||||
"-w", "-t", "nat", "-I", "PREROUTING",
|
||||
"-i", iface,
|
||||
@@ -248,6 +288,8 @@ func (m *Manager) InsertPreroutingRuleOnInterface(iface, proto, sourcePort, targ
|
||||
|
||||
// InsertPostroutingMasquerade inserts a MASQUERADE POSTROUTING rule on the host
|
||||
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}
|
||||
if err := m.deleteMatchingLines("POSTROUTING", "nat", patterns...); err != nil {
|
||||
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
|
||||
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}
|
||||
if err := m.deleteMatchingLines("POSTROUTING", "nat", patterns...); err != nil {
|
||||
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
|
||||
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
|
||||
grepPatterns = append(grepPatterns, proto)
|
||||
if sourceIP != "" {
|
||||
@@ -323,7 +369,11 @@ func (m *Manager) InsertForwardAccept(chain, sourceIP, targetIP, proto, sourcePo
|
||||
|
||||
// DeleteForwardAccept deletes a FORWARD ACCEPT rule by comment
|
||||
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)
|
||||
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-- {
|
||||
if err := m.DeleteLine(chain, lines[i]); err != nil {
|
||||
return err
|
||||
@@ -334,6 +384,8 @@ func (m *Manager) DeleteForwardAccept(chain, comment string) error {
|
||||
|
||||
// InsertPreroutingRuleInContainer inserts a DNAT PREROUTING rule inside a container namespace
|
||||
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}
|
||||
if err := m.deleteMatchingLinesInContainer(pid, "nat", "PREROUTING", patterns...); err != nil {
|
||||
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
|
||||
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}
|
||||
if err := m.deleteMatchingLinesInContainer(pid, "nat", "POSTROUTING", patterns...); err != nil {
|
||||
return fmt.Errorf("failed to delete old container POSTROUTING rules: %w", err)
|
||||
|
||||
@@ -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
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
@@ -12,21 +11,24 @@ import (
|
||||
"firewall_containers/network-go/docker"
|
||||
"firewall_containers/network-go/firewall"
|
||||
"firewall_containers/network-go/iptables"
|
||||
"firewall_containers/network-go/logger"
|
||||
"firewall_containers/network-go/watcher"
|
||||
)
|
||||
|
||||
// Config path - can be overridden via environment variable
|
||||
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
|
||||
const defaultWatchPeriod = 30 * time.Second
|
||||
|
||||
func getConfigPath() string {
|
||||
path := os.Getenv("NETWORKS_CONFIG_PATH")
|
||||
if path == "" {
|
||||
return defaultConfigPath
|
||||
func getEnv(key, defaultVal string) string {
|
||||
if val := os.Getenv(key); val != "" {
|
||||
return val
|
||||
}
|
||||
return path
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
func getWatchPeriod() time.Duration {
|
||||
@@ -36,7 +38,7 @@ func getWatchPeriod() time.Duration {
|
||||
}
|
||||
seconds, err := time.ParseDuration(periodStr + "s")
|
||||
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 seconds
|
||||
@@ -47,34 +49,44 @@ func getDebug() bool {
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
||||
log.Println("MAIN: starting network-go firewall container manager")
|
||||
|
||||
configPath := getConfigPath()
|
||||
configPath := getEnv("NETWORKS_CONFIG_PATH", defaultConfigPath)
|
||||
logPath := getEnv("NETWORK_GO_LOG_PATH", defaultLogPath)
|
||||
watchPeriod := getWatchPeriod()
|
||||
debug := getDebug()
|
||||
|
||||
log.Printf("MAIN: config path = %s", configPath)
|
||||
log.Printf("MAIN: watch period = %s", watchPeriod)
|
||||
log.Printf("MAIN: debug = %v", debug)
|
||||
// Initialize the dual-output logger (stdout + file)
|
||||
log := logger.NewLogger(debug, logPath)
|
||||
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)
|
||||
dockerClient, err := docker.NewClient()
|
||||
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()
|
||||
log.Info("MAIN: Docker client created")
|
||||
|
||||
// Create iptables manager
|
||||
iptablesMgr := iptables.NewManager(debug)
|
||||
log.Info("MAIN: iptables manager created (binary=%s)", iptablesMgr.Binary())
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Load initial config
|
||||
cfg, err := config.Load(configPath)
|
||||
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)
|
||||
orchestrator := firewall.NewOrchestrator(dockerClient, iptablesMgr, cfg)
|
||||
@@ -84,12 +96,14 @@ func main() {
|
||||
|
||||
// Set up file watcher to detect changes and re-reconcile
|
||||
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)
|
||||
if err != nil {
|
||||
log.Printf("MAIN: failed to reload config: %v", err)
|
||||
log.Error("MAIN: failed to reload config: %v", err)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -101,7 +115,7 @@ func main() {
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
sig := <-sigCh
|
||||
log.Printf("MAIN: received signal %v, shutting down", sig)
|
||||
log.Info("MAIN: received signal %v, shutting down", sig)
|
||||
fileWatcher.Stop()
|
||||
log.Println("MAIN: shutdown complete")
|
||||
log.Info("MAIN: shutdown complete")
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"firewall_containers/network-go/config"
|
||||
"firewall_containers/network-go/logger"
|
||||
)
|
||||
|
||||
// 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
|
||||
func NewResolver(cfg *config.NetworksConfig) *Resolver {
|
||||
return &Resolver{
|
||||
r := &Resolver{
|
||||
cfg: cfg,
|
||||
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
|
||||
func (r *Resolver) SetConfig(cfg *config.NetworksConfig) {
|
||||
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
|
||||
func (r *Resolver) SetRetries(n int) {
|
||||
r.retries = n
|
||||
logger.Debug("RESOLVER: retries set to %d", n)
|
||||
}
|
||||
|
||||
// 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
|
||||
func (r *Resolver) Resolve(name string) []string {
|
||||
if r.cfg == nil {
|
||||
logger.Debug("RESOLVER: resolve(%q): config is nil", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Debug("RESOLVER: resolving %q", name)
|
||||
var ips []string
|
||||
|
||||
// Look up by container_name and selector in the IPs section
|
||||
for _, ipCfg := range r.cfg.IPs {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// This allows "wireguardproxy-client" to match "wireguardproxyclient" (after dash stripping)
|
||||
// or "app-1"/"app-2" to match "app-x".
|
||||
if len(ips) == 0 && strings.Contains(name, "-") {
|
||||
prefix := name[:strings.Index(name, "-")]
|
||||
logger.Debug("RESOLVER: prefix matching %q with prefix %q", name, prefix)
|
||||
for _, ipCfg := range r.cfg.IPs {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -3,9 +3,10 @@ package watcher
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"firewall_containers/network-go/logger"
|
||||
)
|
||||
|
||||
// FileWatcher periodically checks a file for changes using MD5 hash
|
||||
@@ -42,7 +43,7 @@ func (fw *FileWatcher) Start() {
|
||||
// Compute initial hash
|
||||
hash, err := fw.hashFile()
|
||||
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 {
|
||||
fw.lastHash = hash
|
||||
}
|
||||
@@ -51,22 +52,22 @@ func (fw *FileWatcher) Start() {
|
||||
ticker := time.NewTicker(fw.period)
|
||||
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 {
|
||||
select {
|
||||
case <-fw.stopCh:
|
||||
log.Printf("WATCHER: stopped watching %s", fw.path)
|
||||
logger.Info("WATCHER: stopped watching %s", fw.path)
|
||||
return
|
||||
case <-ticker.C:
|
||||
hash, err := fw.hashFile()
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
if fw.onChange != nil {
|
||||
fw.onChange()
|
||||
|
||||
Reference in New Issue
Block a user