A production-grade stateful firewall, NAT and PAT targeting 100 Gbps, built with a strict two-language split:
- C owns the packet path — parsing, conntrack, NAT/PAT, ACL, routing, rate limiting, checksums, batching. Nothing in Go ever touches a packet.
- Go owns the control plane — REST API, CLI, config, policy compilation, metrics, health, audit, supervision, async event logging. Nothing in Go sits on the critical packet path.
The two meet only through shared memory (config Go→C, stats C→Go), a lock-free event ring (C→Go logging), and a Unix-domain-socket command channel (Go→C runtime commands). Config updates are applied without stopping the data plane.
Honesty rule. Every performance figure in this repo is either tied to the exact hardware it was measured on, or it is explicitly labelled NOT MEASURED. The synthetic pipeline numbers below come from an in-memory PMD on a laptop CPU — they are not NIC throughput. See
docs/PERFORMANCE.md.
| Rule | How it is enforced |
|---|---|
| C = packet processing | All per-packet code lives in c/. Go never receives an fw_buf_t. |
| Go = management/control | Go only reads shm stats, writes policy, and issues UDS commands. |
| PAT in the C fast path | fw_pat_alloc is per-core, lock-free, O(1). |
| Go creates policy, C enforces it | Go compiles YAML→ipc.Policy; C consumes it via seqlock. |
| C collects counters, Go exposes them | Per-core lock-free counters in shm; Go renders Prometheus. |
| C emits events, Go logs async | MPSC event ring drained by a Go goroutine; never inline. |
Fast-path discipline (per core, per packet): no malloc/free, no mutex on
the hot path, no printf, no synchronous logging, no syscalls, no contended
global counters. NAT/ACL/routing take a shard lock or RCU read only on a
flow-cache miss.
Data plane (C)
- DPDK PMD (gated on
HAVE_DPDK) plus tap and afpacket fallbacks so the whole stack runs and is testable on any Linux box. - Stateful conntrack: 5-tuple TCP/UDP/ICMP, per-IP / per-zone / global limits, timing-wheel expiry (no full-table scans).
- NAT: PAT/NAPT (per-core port pools, O(1) lookup, collision + exhaustion handling), SNAT, DNAT, port-forwarding, incremental RFC-1624 checksums.
- ACL (5-tuple + state), DIR-24-8-8-style LPM routing with RCU publish, token-bucket rate limiting (fixed-point TSC), flow classification, batching, RSS, CPU affinity, NUMA-aware pools, sharded/lock-free tables.
Control plane (Go)
- REST API (
/rules,/nat,/stats,/connections,/nat/translations,/health,/metrics,/config,/reload,/log,/neigh). fwctlCLI, YAML config, atomic hot reload, bearer-token auth, per-IP rate limiting, JSON audit log, async event-log consumer, supervision with watchdog + restart.- Prometheus metrics (
fw100_*), health endpoint with heartbeat freshness.
# 1. Build the C data plane + tests (Release). Sanitizer builds below.
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc)"
# 2. Build the Go control plane.
cd go && go build ./... && go vet ./... && go test ./...
go build -o ../build/firewalld ./cmd/firewalld
go build -o ../build/fwctl ./cmd/fwctl
cd ..
# 3. Run the C unit tests (9 suites) and the synthetic benchmark.
ctest --test-dir build --output-on-failure
./build/fwbench
# 4. End-to-end (needs root for TAP/netns).
sudo ./scripts/e2e.sh # control-plane integration (REST, reload, auth)
sudo ./scripts/e2e-forward.sh # real SNAT/PAT packet through the tap PMDRun the daemon against the sample config:
sudo ./build/firewalld -config configs/firewall.yaml \
-dp-bin "$PWD/build/fw100-dp" -shm /fw100-shm \
-sock /run/fw100/ctl.sock -pmd tap -cores 2 \
-api-addr 127.0.0.1:8080 -api-token secretThen talk to it:
export FW100_API_TOKEN=secret
./build/fwctl -api http://127.0.0.1:8080 status
./build/fwctl -api http://127.0.0.1:8080 stats
curl -s -H "Authorization: Bearer secret" http://127.0.0.1:8080/metricsfirewall/
├── c/ # C data plane (packet path only)
│ ├── packet/ # parser, hashing, incremental checksum
│ ├── conntrack/ # per-core conntrack + timing-wheel GC
│ ├── nat/ # NAT/PAT/SNAT/DNAT, port pools
│ ├── acl/ # 5-tuple + stateful ACL
│ ├── routing/ # LPM, neighbor table
│ ├── ratelimit/ # token buckets (fixed-point TSC)
│ ├── flowcache/ # per-core direct-mapped verdict cache
│ ├── ipc/ # shm (seqlock), event ring, UDS ctl
│ ├── dpdk/ # PMD vtable: tap / afpacket / dpdk / test
│ ├── numa/ util/ # NUMA, mempool, timing wheel
│ └── dataplane/ # pipeline glue + fw100-dp main
├── go/ # Go control plane (no packets)
│ ├── ipc/ # cgo shm/seqlock/UDS client + ABI guard
│ ├── config/ policy/ # YAML config + compile to ipc.Policy
│ ├── api/ metrics/ # REST server, Prometheus rendering
│ ├── health/ audit/ # health checker, JSON audit
│ ├── logproc/ # async event-ring consumer
│ ├── controller/ # supervision, atomic reload
│ └── cmd/ # firewalld, fwctl, abicrc
├── include/ # shared C headers (the ABI)
├── configs/ # sample + e2e YAML policies
├── tests/ benchmarks/ # C unit tests + fwbench
├── scripts/ systemd/ # ops scripts + service unit
└── docs/ # ARCHITECTURE / PERFORMANCE / OPERATIONS
Synthetic pipeline cost on this development machine (in-memory PMD, single
core, no NIC). tsc_hz ≈ 1.90 GHz.
| Scenario | cycles/pkt | Mpps (synthetic) |
|---|---|---|
| cached allow | ~166 | ~11.4 |
| cached SNAT | ~194 | ~9.8 |
| new-flow SNAT | ~428 | ~4.4 |
| drop flood | ~211 | ~9.0 |
These are NOT MEASURED on a NIC. Reaching 100 Gbps requires the DPDK PMD
and real hardware; the math and the measurement methodology are in
docs/PERFORMANCE.md.
docs/ARCHITECTURE.md— components, IPC, data flow.docs/PERFORMANCE.md— targets, math, what is and is not measured, how to reproduce.docs/OPERATIONS.md— deploy, hugepages, DPDK binding, reload, troubleshooting.
No license is applied yet. Choose and add a LICENSE file before distribution.