Skip to content

Dual-WAN load balancing

A small but unusual application of the splitting engine: instead of splitting across VPN tunnels for privacy, you split across raw WAN links for bandwidth and resilience. The same kernel-side numgen round-robin works, the same per-connection affinity holds, the same failover semantics apply.

A home or small office has two ISP uplinks — for example a fibre line and a 4G LTE backup — and wants:

  • Both uplinks used in parallel when both are healthy (cost-effective, twice the aggregate bandwidth).
  • Per-connection affinity so each TCP flow sticks to one link (no broken sessions).
  • Automatic failover when one link dies — surviving link takes everything.
  • No leakage through the broken link if it comes back up briefly with corrupted state.

Any Linux box with two NICs (or one NIC + a USB Ethernet adapter). Mini-PC works well.

Terminal window
sudo ./scripts/install-gateway.sh --profile vps # no WiFi AP needed

Network mode: primary (The Gateway is the LAN’s main router).

network:
mode: primary
wan_iface: eth0 # primary WAN (fibre)
lan_iface: eth2
wan_secondary_iface: eth1 # secondary WAN (LTE)
dhcp:
enabled: true
interface: eth2
range_start: "192.168.10.100"
range_end: "192.168.10.250"
gateway: "192.168.10.1"
dns: "192.168.10.1"

The Gateway’s splitting engine operates on endpoints — objects with a type (wireguard, openvpn, socks5, direct), an interface, a weight, and a route-table ID. For dual-WAN balancing, we register two direct endpoints, one per WAN interface, via the splitting API:

Terminal window
curl -X POST https://gateway.lan:8080/api/v1/splitting/endpoints \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{ "name":"wan-fibre", "type":"direct", "interface":"eth0", "weight":7 }'
curl -X POST https://gateway.lan:8080/api/v1/splitting/endpoints \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{ "name":"wan-lte", "type":"direct", "interface":"eth1", "weight":3 }'

Weights (1–10) bias the round-robin: 7/3 above means 70% of new connections use the fibre, 30% use the LTE. The Gateway installs the necessary ip rule / per-endpoint routing table; you don’t need to write those by hand.

Note that this is a more advanced setup than the VPN-pool case — direct endpoints don’t have the kill-switch protection that tunnel endpoints get. Pair with fail_policy: fail-open (this isn’t privacy, it’s bandwidth).

Once endpoints are registered, activate the engine. It distributes new connections across registered endpoints at the configured privacy level; existing connections stay on their original link via conntrack affinity:

Terminal window
# Set the splitting privacy level — 1 means session-affinity round-robin
# with no padding / cover / shaping overhead. Right for bandwidth balancing.
curl -X PUT https://gateway.lan:8080/api/v1/splitting/level \
-H "Authorization: Bearer $API_KEY" \
-d '{"level":1}'
# Activate — refuses with 409 if no endpoints are registered
curl -X POST https://gateway.lan:8080/api/v1/splitting/activate \
-H "Authorization: Bearer $API_KEY"

Privacy level 1 is the right choice here — per-session affinity so a single TCP flow doesn’t bounce between links mid-connection, but no padding / cover / shaping overhead. Level 0 disables splitting entirely; levels 2+ start adding overhead suited to a VPN-pool case, not a WAN-balance one.

When one WAN dies:

  1. Its latency probe fails N times → the splitting engine marks the endpoint unhealthy within ~10 seconds.
  2. All new connections route to the surviving link (the slot map drops the unhealthy endpoint).
  3. Existing connections on the dead link will eventually time out — applications retry on the surviving link.
  4. When the dead link recovers, the probe succeeds → the endpoint is re-added to the slot map at its full weight on the next rotation cycle.

Failover requires no manual intervention. No script, no cron, no monit. The reconciler does the right thing.

Per-app stickiness for problematic services

Section titled “Per-app stickiness for problematic services”

Some apps don’t tolerate per-connection routing variance — banking sites that lock a session to a source IP, for instance. A routing policy pins the matched domain to one link, bypassing the splitter for that destination. Routing policies run before the splitter so any matched packet is marked + routed before the splitter sees it; no extra precedence work is needed here.

Terminal window
curl -X POST https://gateway.lan:8080/api/v1/routing/policies \
-H "Authorization: Bearer $API_KEY" -d '{
"name": "bank-pin-fibre",
"priority": 110,
"enabled": true,
"source_type": "domain",
"source_value": "*.bank.example",
"hops": [{"type":"interface","target":"eth0"}],
"fail_policy": "fail-open"
}'
curl -X POST https://gateway.lan:8080/api/v1/routing/policies/apply \
-H "Authorization: Bearer $API_KEY"

The bank-pin-fibre policy wins on domain match before traffic ever reaches the splitter. Everything else falls through to the round-robin pool.

Scenario Result
Both WANs healthy New connections split 70/30 across fibre / LTE. Established connections stick to their link.
Fibre goes down Within ~10s, all new connections route via LTE. Existing fibre flows time out and retry.
Fibre flaps (down → up → down) The endpoint is marked unhealthy → recovered → unhealthy. The splitter’s hysteresis stops thrashing — minimum 60s healthy before re-adding.
Both WANs down No internet (obviously). Kill-switch never engaged because fail_policy: fail-open for this profile — but there’s nothing to route to anyway.

This isn’t channel bonding (a single TCP flow split across two links). That requires both endpoints to cooperate. This is per-connection load balancing — each flow uses one link, but new flows distribute across links. For ~95% of household / small-office traffic, this is what you want and it actually works.

Once the dual-WAN split is healthy, what to do next:

  1. Wrap a VPN around it. Layer privacy classes and tunnel pools so failover applies to both the upstream link and the egress provider. See traffic splitting.
  2. Watch the right alerts. Enable tunnel-flap and tunnel-down so a flaky uplink pages on-call instead of silently bleeding traffic to the worse link. See Alerts.
  3. Pin sensitive flows. Use a higher-priority policy for banking / work to force them onto the more trusted link regardless of weighting. See Routing policies.
  • Traffic splitting — the engine that powers the per-connection distribution, just with WAN interfaces in place of VPN tunnels.
  • VPN tunnels — register a raw WAN interface as a tunnel of type interface.
  • Routing policies — pinning a domain to one link with higher priority than the balanced pool.