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

233 lines
7.8 KiB
Markdown

# 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:
```bash
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 capability**`nsenter` 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`
```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:
```bash
nsenter -t $(docker inspect --format {{.State.Pid}} $NAME) -n -- /sbin/iptables-legacy -t nat -I PREROUTING ...
```
The Go implementation does the same:
```go
// 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
```bash
# 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