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 forwarding plane 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 forwarded via DSR
with XDP_TX — only the MAC is
rewritten, 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.
Direct Server Return means the load balancer is never in the return path — backends reply straight to the client. For Docker, equinox automatically injects the VIP into each container's loopback. For bare metal, a one-line script does it. Zero backend changes.
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. Clone the repo
$ git clone https://github.com/typicallhavok/equinox
$ cd equinox
# 2. Create your config (required — no bundled default)
$ cp config.example.yaml config.yaml # then edit it
# 3. Recommended — compose runs the forwarding plane + demo backends
$ docker compose up --build
# …or run the published image directly, mounting your config
$ docker pull typicallhavok/equinox:latest
$ docker run --rm --network host --privileged \
-v "$(pwd)/config.yaml:/app/config.yaml" typicallhavok/equinox:latest
A config at /app/config.yaml is
required — the container exits with an error if
it's missing. 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. Clone the repo
$ git clone https://github.com/typicallhavok/equinox
$ cd equinox
# 2. Toolchain (one-time)
$ rustup toolchain install stable
$ rustup toolchain install nightly --component rust-src
$ cargo install bpf-linker
# 3. Build the control plane (eBPF is built automatically)
$ cargo build --release --package l4
# 4. Create your config, then run as root (CAP_NET_ADMIN + CAP_SYS_ADMIN)
$ cp config.example.yaml config.yaml # then edit it
$ 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. It's required — there's no bundled default. Only
gateway and discovery are
mandatory: point discovery at a Docker
network (strategy: docker) or list backend
IPs (strategy: static). For every field,
see the
full configuration reference.
gateway:
listen_ports: [80, 443] # ports to load-balance; all else passes through
vip: "10.0.0.100" # Virtual IP for DSR
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.