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:
@@ -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"]
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 (
|
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")
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user