Files
firewall_containers/network-go/implementation.md
gyurix f9513cd98a
continuous-integration/drone/push Build is failing
fix(resolver): strip all dashes when matching container names
Instead of prefix matching on the part before the first dash, now strip
all dashes from both the lookup name and the stored container/selector
names and compare exactly. This improves matching accuracy for names
containing multiple dashes or dashes in varying positions.
2026-06-15 11:55:00 +02:00

7.8 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", "--",
    "/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