In-kernel validation
Truncated packets and illegal TCP flag combinations (NULL, SYN+FIN, SYN+RST, FIN+RST, XMAS) are dropped at the NIC. Unrelated traffic — SSH, everything off your listen ports — passes straight through, untouched.
XDP · eBPF · Rust
equinox validates and load-balances traffic inside the NIC driver with XDP — before a packet ever reaches your host stack. No userspace proxy in the hot path, no extra hop, no per-connection overhead.
Everything you need to put a kernel-speed shield in front of your services.
Truncated packets and illegal TCP flag combinations (NULL, SYN+FIN, SYN+RST, FIN+RST, XMAS) are dropped at the NIC. Unrelated traffic — SSH, everything off your listen ports — passes straight through, untouched.
Consistent hashing with a 65,537-entry table
keeps flows pinned to backends even as the set
changes. Packets are DNAT'd and re-transmitted
with XDP_TX — no userspace round
trip.
Routing tables are double-buffered. The control plane fills a standby buffer and flips a single atomic value to switch over. The XDP program is never detached; traffic never stops.
Per-source-IP sliding windows count requests and malformed packets. Cross the limit and the source is blacklisted in-kernel for five minutes — all of it happening in the data plane, not after the fact.
Backends are TCP-probed every reload. A crashed instance is evicted from the table within a cycle and re-added automatically when it recovers. No traffic black-holed.
Find backends statically, through the Docker socket, or via DNS. Ports are optional and override per route. Edit the config and it reloads live.
Set one address and get Prometheus
/metrics — routed and
dropped-by-reason counters, backend gauges —
plus a /healthz readiness probe.
Off by default, zero setup to skip it.
Attaches in native driver mode where the NIC supports it (Intel, Mellanox, virtio-net) and falls back to SKB mode everywhere else. One config line to force a mode.
A traditional load balancer copies every packet into userspace, makes a decision, and copies it back. equinox decides inside the NIC driver.
skb allocation — the
kernel hasn't paid for the packet yet.
XDP_TX straight back out the wire.
Run it in Docker (recommended) or directly on the host —
pick a path and drop a config.yaml next to
it. Edit the file live to hot-reload.
# 1. Pull the image
$ docker pull ghcr.io/you/equinox:latest
# 2. Run it — zero config, starts on the baked-in default
$ docker run --rm --network host --privileged \
ghcr.io/you/equinox:latest
# To customise: mount the current dir. A config.yaml template is
# seeded for you on first run — edit it and it hot-reloads.
$ docker run --rm --network host --privileged \
-v "$(pwd):/app" ghcr.io/typicallhavok/equinox:latest
# …or bring up the shield + demo backends together
$ docker compose up --build
No volume flag needed to start — the image ships
a default config. The interface is auto-detected
from the default route (override with
-e IFACE=eth0), and
--cap-add NET_ADMIN SYS_ADMIN BPF
works in place of --privileged on
most kernels.
# 1. Toolchain (one-time)
$ rustup toolchain install stable
$ rustup toolchain install nightly --component rust-src
$ cargo install bpf-linker
# 2. Build the control plane (eBPF is built automatically)
$ cargo build --release --package l4
# 3. Run as root — needs CAP_NET_ADMIN + CAP_SYS_ADMIN
$ sudo RUST_LOG=info ./target/release/l4 --config config.yaml
--iface is auto-detected from the
default route; override with
--iface eth0. Linux only — eBPF/XDP
is a kernel feature.
A single YAML file, read on start and re-read on every
edit. Here is a complete example with every block; only
gateway and discovery are
required.
gateway:
listen_ports: [80, 443] # ports to shield; all else passes through
xdp_mode: "auto" # auto | skb | drv | hw
discovery:
strategy: "docker" # static | docker | dns
sync_interval_ms: 3000 # re-discover this often
drop_unmatched: false # drop vs. pass when no backend
network: "equinox_backends"
# static_routes: [{ ip: "172.18.0.10", port: 3000 }]
protection: # per-source-IP abuse limits
enabled: true
rate_limit_per_sec: 5000
malformed_limit: 20
window_ms: 1000
block_duration_secs: 300 # blacklist for 5 min
health_check: # evict dead backends
enabled: true
timeout_ms: 500
# observability: # opt-in; omit to keep off
# metrics_addr: "0.0.0.0:9100"
| Setting | Default | What it does |
|---|---|---|
listen_ports |
80, 443 | Ports intercepted & validated; everything else passes straight to the host. |
xdp_mode |
auto |
Attach mode. auto tries
native and falls back to SKB.
|
strategy |
— |
Where backends come from:
static,
docker socket, or
dns.
|
sync_interval_ms |
3000 | How often discovery re-runs to pick up scaled or crashed instances. |
drop_unmatched |
false | Drop validated packets with no backend instead of passing them. |
protection.enabled |
true | In-kernel per-source rate & malformed limiting with timed blacklist. |
block_duration_secs
|
300 | How long a tripped source stays blacklisted. |
health_check.enabled
|
true | TCP-probe backends each reload; only healthy ones get traffic. |
metrics_addr |
off |
Set it to expose Prometheus
/metrics +
/healthz.
|
Linux + Docker. Apache-2.0 control plane, GPL-2.0 data plane.