Files
gyurix 3172023254
continuous-integration/drone/push Build is passing
fix: update iptables binary paths to use /usr/sbin instead of /sbin
Standardize iptables paths across firewall scripts and Go code to ensure compatibility with systems where iptables is located in /usr/sbin. This affects both legacy and non-legacy iptables binaries.
2026-06-15 16:34:24 +02:00

7.9 KiB

network-go — Docker Network & Firewall Manager

A Go-based replacement for the firewall/firewall-add shell script. It watches /etc/user/config/networks.json for changes and reconciles Docker networks, container connections, and iptables firewall rules — all via the Docker SDK and nsenter (no Docker CLI calls).

Project Structure

network-go/
├── main.go                  Entry point: watches config, orchestrates reconciliation
├── config/
│   └── config.go            Parses /etc/user/config/networks.json into typed structs
├── docker/
│   └── docker.go            Docker SDK wrapper: network create, container connect,
│                            container PID for nsenter, route management
├── firewall/
│   └── firewall.go          Orchestrator: translates policies → iptables rules
├── iptables/
│   └── iptables.go          Manages iptables CLI: PREROUTING DNAT, POSTROUTING MASQUERADE,
│                            FORWARD ACCEPT, nsenter for container network namespaces
├── resolver/
│   └── resolver.go          Resolves names → IPs using networks.json config only
├── watcher/
│   └── watcher.go           Periodic file change detection via MD5 hash polling
├── go.mod / go.sum          Module definition with Docker SDK dependency
└── .gitignore

Docker Run — Required Environment & Volumes

This program is designed to run inside a Docker container with the following setup:

docker run -d \
  --name network-go \
  --network host \
  --cap-add NET_ADMIN \
  --cap-add SYS_ADMIN \
  --pid host \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /etc/user/config:/etc/user/config \
  -v /proc/sys/net/ipv4/ip_forward:/proc/sys/net/ipv4/ip_forward \
  network-go

Required Volumes

Mount Purpose
/var/run/docker.sock:/var/run/docker.sock Docker SDK communication (create networks, inspect containers, get PIDs for nsenter)
/etc/user/config:/etc/user/config Access to networks.json configuration file

Required Flags

Flag Purpose
--network host Run in the host network namespace so iptables rules apply to the host
--pid host Access host PIDs for nsenter -t <pid> into container network namespaces
--cap-add NET_ADMIN Required to manipulate iptables rules
--cap-add SYS_ADMIN Required for nsenter to enter other containers' namespaces

Environment Variables

Variable Default Description
NETWORKS_CONFIG_PATH /etc/user/config/networks.json Path to the configuration file (inside the container)
WATCH_PERIOD_SECONDS 30 Polling interval in seconds for config file changes
DEBUG false Enable debug output (1 or true)
DOCKER_HOST (empty = /var/run/docker.sock) Docker daemon socket — automatically used by Docker SDK

Why --network host and --pid host?

The program uses nsenter to enter other containers' network namespaces and insert iptables rules. This requires:

  1. Host PID namespace (--pid host) — to know the PID of the target container (obtained via Docker SDK ContainerInspect().State.Pid)
  2. Host network namespace (--network host) — so the program can also manage host-level iptables chains (DOCKER-USER, FORWARD, POSTROUTING)
  3. NET_ADMIN capability — iptables manipulation requires this Linux capability
  4. SYS_ADMIN capabilitynsenter needs this to switch namespaces

Without these, the program cannot:

  • List/manage host iptables rules
  • Insert PREROUTING/POSTROUTING rules inside other containers
  • Add routes to container network namespaces

Configuration — /etc/user/config/networks.json

{
  "networks": {
    "smarthost-loadbalancer": {
      "network_name": "smarthost-loadbalancer",
      "subnet": "172.18.103.0/24",
      "gateway": "172.18.103.1"
    },
    "smarthost_backend-1": {
      "network_name": "smarthost_backend-1",
      "subnet": "172.18.104.0/24",
      "gateway": "172.18.104.1"
    }
  },
  "ips": {
    "172.18.103.2": {
      "ip": "172.18.103.2",
      "container_name": "smarthostloadbalancer",
      "selector": "smarthostloadbalancer",
      "service_name": "smarthost-proxy"
    },
    "172.18.104.2": {
      "ip": "172.18.104.2",
      "container_name": "smarthostbackend-1",
      "selector": "smarthostbackend-1",
      "service_name": "smarthost-proxy"
    }
  },
  "policies": [
    {
      "service_name": "smarthost-proxy",
      "container_name": "smarthost_loadbalancer",
      "selector": "smarthostloadbalancer",
      "from": "publicbackend",
      "port": 80,
      "proto": "tcp"
    },
    {
      "service_name": "smarthost-proxy",
      "container_name": "smarthost_loadbalancer",
      "selector": "smarthostloadbalancer",
      "name": "wireguardproxy",
      "iface": "wg0",
      "nat": "dnat",
      "to": "smarthostloadbalancer",
      "port": 80,
      "proto": "tcp"
    }
  ]
}

How Reconciliation Works

When the config file changes, Orchestrator.ReconcileAll() runs:

  1. Enable IP forwarding — writes 1 to /proc/sys/net/ipv4/ip_forward
  2. Ensure Docker networks — creates bridge networks with specified subnet/gateway
  3. Connect containers — attaches containers to networks with assigned static IPs
  4. Apply firewall policies — processes each policy entry into iptables rules:

Policy Types

Policy Pattern Action Target
from field present FORWARD ACCEPT rule Host chain: DOCKER-USER (iptables-legacy) or FORWARD
nat: "dnat" with iface PREROUTING DNAT on interface Host PREROUTING chain
nat: "dnat" with selector PREROUTING DNAT inside container Container namespace via nsenter

nsenter Implementation

The shell script uses:

nsenter -t $(docker inspect --format {{.State.Pid}} $NAME) -n -- /sbin/iptables-legacy -t nat -I PREROUTING ...

The Go implementation does the same:

// 1. Get container PID via Docker SDK
pid, _ := dockerClient.GetContainerPID(ctx, containerName)

// 2. Execute iptables inside container namespace via nsenter
exec.Command("nsenter", "-t", fmt.Sprintf("%d", pid), "-n", "--",
    "/usr/sbin/iptables-legacy", "-t", "nat", "-I", "PREROUTING", ...)
  • -t <pid> — target the container's PID
  • -n — enter the network namespace only
  • -- — separator, then the iptables command

Packages

config

NetworksConfig, NetworkConfig, IPConfig, PolicyConfig structs. Helpers: IsIP, ToCIDR, NetworkPrefix, ParseCIDR, Load.

docker

Docker SDK wrapper: EnsureNetwork, ConnectContainer, DisconnectContainer, GetContainerPID, AddRouteInContainer, WaitForContainerRunning, InspectContainer.

resolver

Name→IP resolution using networks.json config only. Looks up by container_name and selector fields in the ips section, with prefix fallback matching.

iptables

Auto-detects iptables-legacy vs iptables. Manages:

  • PREROUTING DNAT (host and container via nsenter)
  • POSTROUTING MASQUERADE (host and container)
  • FORWARD/DOCKER-USER ACCEPT with ESTABLISHED,RELATED
  • Rule deletion by line-number + pattern matching

firewall

Orchestrator ties all packages together. ReconcileAll() runs the full cycle. Policy→rule mapping: from → FORWARD ACCEPT, nat: dnat → PREROUTING DNAT.

watcher

FileWatcher polls a file via MD5 hash comparison. Start/Stop lifecycle.

Build & Run

# Build
cd network-go
go build -o network-go .

# Run locally (requires Docker socket access)
./network-go

# Build and run in Docker
docker build -t network-go .
docker run -d \
  --network host \
  --pid host \
  --cap-add NET_ADMIN \
  --cap-add SYS_ADMIN \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /etc/user/config:/etc/user/config \
  -e WATCH_PERIOD_SECONDS=30 \
  -e DEBUG=false \
  --name network-go \
  safebox/network-go