Skip to content

feat: L4 load balancer with nftables DNAT#26

Open
Frando wants to merge 4 commits intomainfrom
feat/load-balancer
Open

feat: L4 load balancer with nftables DNAT#26
Frando wants to merge 4 commits intomainfrom
feat/load-balancer

Conversation

@Frando
Copy link
Copy Markdown
Member

@Frando Frando commented Apr 11, 2026

Adds L4 load balancing to routers using nftables DNAT, matching kube-proxy nftables mode behavior. A balancer creates a virtual IP on a router that distributes incoming connections across backend devices using round-robin or random selection.

The implementation lives in a self-contained balancer.rs module (~500 LOC) following the split-impl pattern. Router and RouterBuilder methods are defined inside balancer.rs via split impl blocks. The glue outside the module is ~10 lines total: one field on RouterData, one setup call in wiring.rs, and re-exports in lib.rs.

Under the hood, each balancer generates a table ip lb with per-balancer chains using numgen inc mod N (round-robin) or numgen random mod N (random). Session affinity uses nftables dynamic maps with configurable timeouts. A postrouting masquerade rule handles same-subnet return traffic so backends reply through the router's conntrack rather than directly to the client at L2.

let dc = lab.add_router("dc").build().await?;
let web1 = lab.add_device("web1").iface("eth0", dc.id()).build().await?;
let web2 = lab.add_device("web2").iface("eth0", dc.id()).build().await?;

dc.add_balancer(
    BalancerConfig::new("web", "198.18.1.100".parse()?, 80)
        .backend(web1.id(), 8080)
        .backend(web2.id(), 8080)
        .round_robin(),
).await?;

// Dynamic backend management (rolling updates)
dc.remove_lb_backend("web", web1.id()).await?;
dc.add_lb_backend("web", web3.id(), 8080).await?;

Supports TCP, UDP, and both. Conntrack flush is best-effort (works when conntrack binary is available, silently skips otherwise). 7 tests cover round-robin distribution, backend add/remove, and UDP balancing.

Frando and others added 4 commits April 11, 2026 15:32
Add BalancerConfig, LbAlgorithm, LbProtocol, and SessionAffinity types
for configuring L4 load balancers on routers. The balancer creates a VIP
on the router's downstream bridge and generates nftables DNAT rules that
distribute connections across backend devices using numgen (matching
kube-proxy nftables mode).

The implementation is self-contained in balancer.rs with split impl
blocks on Router and RouterBuilder. External glue is limited to a
balancers field on RouterData (core.rs), a setup hook (wiring.rs),
and module registration (lib.rs).

Includes integration tests for round-robin distribution, backend
add/remove, and UDP balancing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix the UDP echo server closures to avoid returning Result from
fire-and-forget spawn calls, and remove an unused intermediate VIP
variable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add meta l4proto prefix to DNAT rules so nftables has transport protocol
context for the inet_service (port) mapping. Add a postrouting
masquerade chain with ct status dnat to handle same-subnet backends
where the response would otherwise bypass conntrack and miss the reverse
DNAT.

Also fix test compilation issues and adjust unit test assertions for the
updated rule format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CI environments may not have the conntrack binary installed. The flush
is a performance optimization, not a correctness requirement. Log and
continue instead of returning an error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Frando Frando force-pushed the feat/load-balancer branch from 39c06eb to 7990829 Compare April 11, 2026 14:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant