Skip to content
filippo-agentPublic

About

Minimal two-WAN failover daemon for OpenWrt (a focused mwan3 alternative)

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

simplewan

A deliberately small two-WAN failover daemon for OpenWrt — a focused alternative to mwan3 when all you want is: use the primary uplink while it's healthy, fail over to the backup when it isn't, and switch back after it has been healthy for a while.

No load balancing, no per-flow policies, no firewall marks, no iptables/nftables. Just ICMP probes bound to each interface and a reordering of the IPv4 default routes.

Safety invariant

simplewan is designed so it can never cause an outage that wouldn't happen without it:

  • It only ever reorders existing default routes. It never installs a blackhole/unreachable route and never removes the last default route.
  • If no WAN looks healthy (e.g. your single ping target is globally down), it does nothing — it holds the last-good selection. Worst case it degrades to "no failover", never to "no connectivity".
  • The daemon is never in the packet path. If simplewand crashes or is killed, the routes it last set simply remain, and traffic keeps flowing.

How it works

  1. One ICMP echo target is pinged out of each WAN every 2 s, with the socket bound to that WAN's device (SO_BINDTODEVICE). Binding is essential: it lets the daemon test a WAN that is not currently the default route, so a demoted-but-recovering link can be observed coming back.
  2. Health comes from a sliding ~60 s window (fixed in the daemon, not configurable):
    • a dead link is caught fast — 5 consecutive misses (~10 s) → offline;
    • high packet loss is caught over the window — ≥ 10 % loss → offline, and back online once loss drops below 10 % again;
    • a device that is absent or has no route yet (e.g. PPPoE still negotiating at boot) is treated as a clean "down" and validated from scratch when it returns, rather than carrying stale failures.
  3. Selection: the primary wins while healthy. Failing over to the backup (the current WAN went unhealthy) is immediate. Switching back to the primary waits until it has been healthy for recovery_time seconds — but only after a real failover; at boot the primary is adopted as soon as it validates (~10 s after its link is up).
  4. The selected WAN keeps its base metric (read from netifd, so the resting state matches what netifd installs and needs no correction); the other WAN is demoted above it only when necessary. Metric changes are make-before-break (add the new metric, then drop the old) so there is never a moment with no default route for a live WAN.
  5. A netlink route monitor re-asserts this whenever netifd re-adds a default route (DHCP/PPPoE renewals, or the primary's PPPoE link coming up at boot).

Only IPv4 is handled for now.

Requirements

  • Both WANs must be in a masqueraded firewall zone (standard wan zone), or failover traffic won't be NATed.
  • Set the two WANs' route metrics in the normal Network config so the primary is lower (e.g. wan = 10, wan2 = 20). That ordering is how you designate primary vs backup at the routing level; simplewan reads those metrics rather than defining its own.
  • The daemon runs as root (needs raw ICMP sockets); procd handles that.

Configuration (/etc/config/simplewan)

config globals 'globals'
	option enabled       '1'
	option primary       'wan'      # preferred while healthy
	option backup        'wan2'     # failover target
	option ping_target   '1.1.1.1'  # single IPv4 target
	option recovery_time '300'      # healthy seconds before switching back
	option flush_conntrack '1'

config notify 'notify'
	option enabled '0'
	option postmark_token ''
	option mail_from 'router@example.com'
	option mail_to 'you@example.com'
	option subject_prefix '[simplewan]'

recovery_time is the only numeric knob; probe cadence and the dead-link/loss thresholds are fixed in the daemon. The config file is installed mode 0600 because it holds the Postmark token.

Email notifications (Postmark)

When configured, simplewan emails on every selection change (failover and switch-back) via the Postmark API. Sending is asynchronous and time-bounded, so a slow or failing Postmark request can never delay a routing change. mail_from must be a verified Postmark sender.

LuCI

luci-app-simplewan adds a status page (per-WAN health, active upstream, loss, RTT) and a configuration form under Network → SimpleWAN Failover. The status page reads /var/run/simplewan/status.json through a small ucode rpcd backend.

Building and the feed

The daemon is a static Go binary, cross-compiled with the go command on your PATH (see the go directive in go.mod for the required version) rather than the OpenWrt packages feed's Go toolchain. Build the packages with an OpenWrt SDK for your target (Turris Omnia = mvebu/cortexa9, package arch arm_cortex-a9_vfpv3-d16):

scripts/build-feed.sh /path/to/openwrt-sdk

The package format follows the SDK: OpenWrt 25.12 and later use apk, 24.10 and earlier use opkg.

The GitHub Actions workflow (.github/workflows/feed.yml) does the same for each supported release series and publishes a signed feed per series to GitHub Pages on each v* tag:

OpenWrt Package manager Feed
25.12 apk https://filippo-agent.github.io/simplewan/25.12/packages.adb
24.10 opkg https://filippo-agent.github.io/simplewan/24.10

Signing

Each feed index is signed, with a different key per package manager. The public keys are committed in feed/ and also published at the root of the site:

  • apk: feed/simplewan-apk.pem, an ECDSA P-256 key. The secret key lives only in the APK_SIGN_KEY repository Actions secret.
  • opkg: feed/simplewan-feed.pub, a usign key (fingerprint 913328a568e78b46). The secret key lives only in the USIGN_SECRET repository Actions secret.

Neither secret is ever in the repo or the published site. Whoever holds them can publish packages the router will trust automatically.

Installing from the feed (OpenWrt 25.12, apk)

# trust the feed's signing key
wget -O /etc/apk/keys/simplewan-apk.pem \
	https://filippo-agent.github.io/simplewan/simplewan-apk.pem

# add the feed
echo 'https://filippo-agent.github.io/simplewan/25.12/packages.adb' \
	>> /etc/apk/repositories.d/customfeeds.list

apk update
apk add simplewan luci-app-simplewan

Compare the downloaded key against the one committed in this repository.

Installing from the feed (OpenWrt 24.10, opkg)

# trust the feed's signing key
echo 'RWSRMyilaOeLRofZWUzeLc9CEK8XGijN6sv5UJ32hIlNX021r7nLPJ7I' \
	> /etc/opkg/keys/913328a568e78b46

# add the feed
echo 'src/gz simplewan https://filippo-agent.github.io/simplewan/24.10' \
	>> /etc/opkg/customfeeds.conf

opkg update
opkg install simplewan luci-app-simplewan

Installing one-off (without adding the feed)

Download the packages and install them directly. Update the package lists first so dependencies (ca-bundle, kmod-nf-conntrack-netlink, luci-base) resolve from the official feeds. The exact filenames track the version; the current ones are listed in each feed's index.

On 25.12, apk verifies individual packages against the trusted keys, and packages installed by path are not covered by the feed's signed index, so they need --allow-untrusted:

cd /tmp
apk update
wget https://filippo-agent.github.io/simplewan/25.12/simplewan-0.2.3-r1.apk
wget https://filippo-agent.github.io/simplewan/25.12/luci-app-simplewan-0.2.3-r1.apk
apk add --allow-untrusted ./simplewan-0.2.3-r1.apk ./luci-app-simplewan-0.2.3-r1.apk

On 24.10, opkg only verifies feed indexes, not individual packages installed by path:

cd /tmp
opkg update
wget https://filippo-agent.github.io/simplewan/24.10/simplewan_0.2.3-r1_arm_cortex-a9_vfpv3-d16.ipk
wget https://filippo-agent.github.io/simplewan/24.10/luci-app-simplewan_0.2.3-r1_all.ipk
opkg install ./simplewan_0.2.3-r1_arm_cortex-a9_vfpv3-d16.ipk ./luci-app-simplewan_0.2.3-r1_all.ipk

Surviving firmware upgrades

/etc/config/simplewan is a conffile, so sysupgrade preserves your settings. The packages themselves are not reinstalled by a sysupgrade, and the official ImageBuilder/ASU servers cannot include them in an image. With owut, leave them out of the build explicitly:

owut upgrade -r simplewan,luci-app-simplewan

and reinstall them from the feed for the new release afterwards. When upgrading from 24.10 to 25.12, the package manager changes from opkg to apk, so set up the 25.12 apk feed (or install the .apks one-off) after the upgrade. Until then routing follows the netifd metrics, so the primary WAN stays preferred but there is no failover.

Status

Early. The Go daemon and its logic are unit-tested and the CI feed build is validated end-to-end; the on-router behaviour (device resolution, probing, route reordering) has not yet been exercised on real hardware.

About

Minimal two-WAN failover daemon for OpenWrt (a focused mwan3 alternative)

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages