Dual-WAN failover on OpenWrt, in one screen and about five minutes.
wansentry does not implement failover. It generates the mwan3 configuration
that implements failover, from ten fields on a single LuCI page, and then owns
what it generated. Pick a primary uplink, pick a backup, name two health-check
hosts, apply. Provided both uplinks already exist as configured network
interfaces, nothing else has to be visited.
It is VPN aware, and that is not a checkbox. If your router steers some
traffic through a VPN using policy-based routing, turning on failover can
silently defeat those policies. Nothing breaks and nothing is logged: traffic
keeps flowing and quietly stops going where you sent it, which on a country
exit means the affected devices begin appearing in the wrong country.
wansentry detects that arrangement, generates the mwan3 rules that prevent
it, and tells you on screen which policies it protected and which it could not.
It also fails over between VPN tunnels. If pbr sends some traffic into a tunnel, list two or more tunnels in order and that traffic moves to the next one when the first dies, comes back when it recovers, and is blocked outright while every tunnel is down (a killswitch), so it can never leak out the ordinary internet. Which traffic enters a tunnel stays pbr's decision. See VPN failover.
And it tells you. A failover that works is quiet, so the first you hear of a dead primary is usually when the backup dies too. Optional email on every down/up transition, plus a daily report of every path. See Email notifications.
There is a guide to the whole subject in docs/VPN-AND-POLICY-ROUTING.md: how to tell whether your own tunnel survives a switchover, two settings that look like tidy-ups and are not, and the part that has nothing to do with your VPN and derails more setups than anything else, which is DNS. It ends with the commands to measure your own router rather than trust the numbers in it.
Status: working, and failover is verified on hardware. A live switchover under load has been measured on two independent uplink types: a wired primary failing over to cellular, and the same primary failing over to a second broadband line on separate Wi-Fi. Both switched in 13-14 s against a 15 s detection threshold and failed back in 31 s against a 30 s recovery threshold, confirmed by interface byte counters rather than status output. Config generation, the ownership refusal, rollback and service reconciliation are verified too, as is coexistence with policy-based routing and a VPN: measured on a bench built to replicate a real production stack rather than a clean two-WAN rig, because a clean rig cannot reproduce any of it. See Known limitations for what remains, chiefly that this does not solve DNS, and that IPv6 is generated but has never been failover-tested on a live IPv6 line.
One file, every device. The package is noarch, and the same .apk is
verified installing on both aarch64 and arm_cortex-a15 hardware.
wget -O luci-app-wansentry-1.2.3-r1.apk https://github.com/VolanticSystems/luci-app-wansentry/releases/download/v1.2.3/luci-app-wansentry-1.2.3-r1.apk
apk add --allow-untrusted ./luci-app-wansentry-1.2.3-r1.apk
Needs OpenWrt 25.12 or newer and mwan3. Both uplinks must already exist as
working network interfaces. --allow-untrusted is required because this is not
signed by the OpenWrt build key.
Prefer to build it yourself? See Building from source below.
The page, taken on the author's own router: a GL-MT6000 running pbr with
three policies and three OpenVPN tunnels to the US. The pbr banner names the
policies it will protect, then four tabs under one Save & Apply: Status, WAN
config, VPN config and Notifications. The Status tab shows every uplink and
tunnel with what it is connected to (address and gateway for an uplink, the
server for a tunnel) and which one is carrying the traffic. Folded at the
bottom are the complete mwan3 configuration it will write, updated as you edit,
and the known limitations. The backup uplink here is a USB-tethered phone,
unplugged when this was taken, which is why it shows DOWN:
The VPN config tab on the same router. Three tunnels in failover order, and the killswitch that blocks tunnel traffic rather than let it leak out the ordinary internet when every tunnel is down:
A notification email. Every alert and the daily report look like this, with a plain-text version for mail clients that do not show HTML:
And the hand-off. This is what you get if /etc/config/mwan3 already contains
anything wansentry did not write. Every field is disabled, the foreign sections
are named, and nothing is generated:
mwan3 is the right engine for this. It is actively maintained, and every sharp edge in dual-WAN failover (probe hysteresis, multi-target tracking, conntrack flushing on switchover, hotplug integration) is something it has already iterated on for a decade. That is not the problem.
The problem is that mwan3 is a general policy-routing tool, and its LuCI app exposes that generality: separate configuration tabs for Interfaces, Members, Policies and Rules, plus globals, and further tabs for status. A pure failover setup needs six sections across five of those screens, and none of the concepts it makes you learn are ones you asked about. The recurring forum complaint is not that mwan3 does not work; it is that the documentation is "huge and complex" for what people consider a basic two-uplink job.
Several projects have answered that by reimplementing failover from scratch,
starting around 2010: simplefailover, Adze1502/mwan,
GTANAdam/openwrt-wan-failover-script, belliash/wanmonitor and others. Most
are now unmaintained, and the one adjacent tool with real traction,
br101/pingcheck, does detection only and deliberately leaves failover to
something else. Dates and current status for each are tabulated in
docs/DESIGN.md section 2, so you can check that reading
rather than take it on trust.
The conclusion wansentry draws from that history is that the gap worth filling is a simpler configuration path onto mwan3, not another failover mechanism. That is a judgment, not a measurement, and it is the premise the whole package rests on.
luci-app-mwan3 |
luci-app-wansentry |
|
|---|---|---|
| Scope | Every mwan3 feature: load balancing, weights, arbitrary policies and rules | Failover only: one primary and one backup uplink, plus an ordered list of VPN tunnels |
| Screens for a working setup | Several: Interfaces, Members, Policies, Rules, plus status | 1 |
| Fields | Every mwan3 option, on every section | 10 for WAN failover; VPN failover and email each on a tab of their own |
| Configuration model | You write mwan3 config | wansentry generates it, and shows you exactly what it wrote |
| Backend | mwan3 | mwan3 (same engine, same package) |
| Coexistence | It is the mwan3 editor | Refuses to touch mwan3 config it did not create |
| Policy routing and VPNs | Not its job: you write whatever rules you need | Detects pbr policies and generates the mwan3 exclusions that keep them working |
They are not really competitors. Use wansentry to get failover working; if you
later need load balancing or per-destination policies, install
luci-app-mwan3 and configure it there.
What "hands off" actually looks like, because "steps aside" was too soft a phrase for it. The moment wansentry sees an mwan3 section it did not write, it stops managing mwan3 entirely: the settings form goes read-only, a banner names the foreign sections, and applying is refused rather than merged. The service-side reconciler follows the same rule and leaves the mwan3 service alone at boot. Nothing you configured elsewhere is modified or removed, but this is a clean handover, not graceful coexistence, and you will find a read-only page rather than a page that quietly defers.
What counts as "ours", precisely, since the whole safety promise rests on it. A section is wansentry's only if both of these hold:
- its type is one of
interface,member,policyorrule, and - it either carries
option wansentry '1'or is namedwansentry_*.
A section of any other type is foreign no matter what it is called, so an
mwan3 config carrying, say, a notify section is not ours to manage even if
somebody named it wansentry_notify. That rule is implemented twice, in the
browser and in the init script, and the two are held to agreement by
tests/ownership-suite.sh, which exists because they have drifted before.
Two interface sections (tracking options mapped from the form), two member
sections at metric 1 and 2, one failover policy, and one default rule, and the
complete minimal mwan3 failover configuration and not one section more.
The only exception is mwan3's own globals section, which wansentry adds
(with mmx_mask) if and only if the system does not already have one; a
stock mwan3 install ships it, so normally nothing is added. The
screen renders the exact text before you apply it. Regeneration is idempotent:
applying an unchanged configuration produces zero uci operations.
- Any one health-check host answering keeps an uplink up. ICMP-hostile upstreams are common; demanding that every target respond turns one of them into a phantom outage.
- Recovery is slower than failure. The recovery threshold defaults higher than the failure threshold, because flapping back onto a shaky primary is worse than staying on a working backup a little longer.
- A returning uplink has to prove itself before it gets traffic back. mwan3 defaults to assuming an interface that comes back is working; wansentry generates the opposite. Pull a cable and both are fine, because the link and the internet return together. Power-cycle a modem and they do not: carrier is back in seconds, the service behind it takes far longer, and assuming otherwise hands traffic to an uplink that cannot carry it. Measured, that costs a second outage of roughly nine seconds. This is why the failover test everybody runs, pulling the cable, does not catch it.
- Conntrack is flushed on every transition by default. Established connections carry per-flow NAT and routing-mark state that still points at the uplink that just failed; until those entries expire they keep steering traffic down the dead path, so they are cleared on the switch.
- If both uplinks are marked offline, traffic falls through to the kernel routing table rather than being blackholed. A tracking false positive should degrade to plain routing, not to a total outage.
- IPv6 follows IPv4, or is left alone. An uplink's IPv6 partner (
wan6besidewan) is generated alongside it, so a failover never moves IPv4 and leaves IPv6 routing out the dead uplink. Nothing is asked: the partner is derived from the interface name, the way OpenWrt itself pairs them.
Each of these is argued, with sources, in docs/DESIGN.md.
(The banner this describes is at the top of the first screenshot above.)
This is the case most failover guides skip, and it is the one where getting it wrong is hardest to notice.
pbr and mwan3 both do policy routing, and mwan3 wins. Their fwmarks do not
collide (pbr masks on 0x00ff0000, mwan3 on 0x00003f00), but their ip rule
priorities do: mwan3 installs at 1001-3002 and pbr at 29995-30000, and rules are
evaluated in ascending order. So when both have marked a packet, mwan3's table
is consulted first and pbr's policy never runs.
Nothing breaks when this happens. Traffic keeps flowing, both packages report success, and neither logs a thing. Your traffic just stops going where you sent it. If you use pbr for a country exit, the affected devices quietly start appearing in the wrong country.
wansentry detects pbr and generates an mwan3 rule for each range your policies
claim, carrying use_policy 'default' and ordered ahead of the catch-all, so
mwan3 stands aside for that traffic and pbr's decision survives. Everything
outside your pbr ranges still fails over normally. The settings screen tells you
which policies it protected, and names any it could not.
The division of labor that results is the right one: pbr decides what enters the tunnel, wansentry decides which uplink the tunnel rides on.
Your VPN probably does not need restarting. Measured on OpenVPN 2.7 against
a server that negotiates peer-id, a nobind client roams to the new uplink
mid-session: zero packets lost on a clean link failure, and about ten seconds on
a dead-upstream failure, which is mwan3's own detection time rather than the
tunnel's. ping-restart 120 never fires because nothing restarts. A planned
"restart the tunnel on switchover" feature was dropped once that was measured,
because it would have been a disruption fixing a problem that does not occur.
Older OpenVPN, a server without float, and WireGuard are all plausibly
different and none of them has been measured. For those, and for anything else
that needs a nudge when the uplink changes, drop an executable script in
/etc/wansentry.d/; it is run with WANSENTRY_OLD and WANSENTRY_NEW set.
There is deliberately no "run this command on switchover" field in the web UI,
because that is remote command execution handed to whoever holds the failover
ACL.
If your router runs a VPN, read docs/VPN-AND-POLICY-ROUTING.md. It covers this, why DNS is the most likely reason a working failover looks broken, how to tell whether your own tunnel survives a switchover, two settings that look like tidy-ups and are not, and a set of commands for measuring your own router rather than trusting the numbers here. The engineering rationale is in docs/DESIGN.md section 11.
For traffic that pbr already sends into a VPN tunnel. List the tunnels in the
order they should be used; the screen generates one tracked mwan3 interface per
tunnel, members at metric 1, 2, 3, and a policy whose last resort is
unreachable. Every pbr policy that points at one of the tunnels is mirrored
as an mwan3 rule to that policy, in pbr's own order, so a narrower pbr policy
ahead of it (a LAN-to-LAN exception, say) still wins exactly as it does in pbr.
pbr's policies stay enabled underneath: if mwan3 is ever stopped, pbr's strict
enforcement is still a killswitch.
Measured on a bench with three real OpenVPN tunnels to three real servers
(VPN-FAILOVER-BENCH-2026-10-08.md in the parent repository), using network
namespace clients and per-tunnel packet counters as the oracle:
| fault | result |
|---|---|
| first tunnel's server dies | traffic on the second tunnel; ~17 s of loss |
| second dies too | traffic on the third |
| all three dead | tunnel traffic blocked; zero connections leaked out a WAN address; the rest of the LAN unaffected |
| servers return | back on the first tunnel once it passes its recovery checks |
| a long-lived connection across a tunnel death | survives, on the next tunnel |
| the primary WAN dies under the tunnels | tunnels re-establish over the backup WAN by themselves; traffic back on the first tunnel in ~40 s |
| a tunnel's OpenVPN process is killed | instant switch; back on it in ~21 s after procd restarts it |
| reboot | everything back in under a minute, no intervention |
Repeated on the author's own router, a GL-MT6000 with three tunnels to three US sites: each tunnel killed in turn moved the traffic to the next in about 20 s, with all three dead not one tunnel connection left by a WAN address, and restoring them brought the traffic back to the first. The rest of the LAN was unaffected throughout. A reboot and a WAN failure under the tunnels were tested on the bench only.
Two things this needed that are not obvious, both handled for you:
- A tunnel with no default route of its own cannot be an mwan3 member.
mwan3 builds each interface's routing table by copying that device's routes
out of the main table, and a tunnel set up with
route-nopullhas none, so its table is empty. A hotplug script gives each failover tunnel a high-metric default route in the main table (the ordinary default still wins), so any tunnel type works without editing its config. - Connections stay pinned to a dead tunnel. mwan3 keeps each connection
on the member it started on. Without help a live connection never moves off
a dead tunnel. mwan3's own
flush_conntrackwould fix that globally, dropping every connection on the router; instead a hook forgets only the connections pinned to the tunnel that died (needs theconntrackpackage, a dependency).
Off by default. Set a recipient and an SMTP server, then press Send test email. A login is optional, as in a mail client: tick Server requires authentication to give one. Without it most servers accept mail only for their own mailboxes, which suits sending to an address on the same server. With notifications on:
- one email when any uplink or tunnel wansentry manages goes down, and one when it comes back, saying what is carrying the traffic now. These follow mwan3's own verdicts, so its thresholds already filter a flapping link. A "back up" is sent only after a "down" was, so restarting mwan3 (a Save & Apply, say) sends nothing;
- one email at boot instead of one per path coming up;
- optionally, off by default, a daily report of every path at an hour you choose. A backup that died weeks ago and has never been needed since shows up here, every day, until it is fixed, and a daily email that stops arriving is itself the alarm that the mail path is broken. A path that is not connected at all, such as a USB phone unplugged because it is being used as a phone, is listed but not counted as down.
Sent with msmtp (a dependency), with a 30 s network timeout, and never in a
way that can hold up the failover it reports (see DESIGN.md section 17). The
SMTP password, if you set one, is stored in plain text in
/etc/config/wansentry; use an account made for this.
DNS. dnsmasq merges upstream resolvers from every interface that is up and picks between them without regard to failover state, so name resolution can still be attempted through the uplink that just died. mwan3 does not solve this either. wansentry shows the failure mode and the dnsmasq workaround on screen rather than pretending the problem does not exist.
Three suites, 146 checks, no failures. Nothing reimplements the logic it tests: an earlier suite kept its own copy of the ownership arithmetic and every one of those checks passed against a reconciler that was known to be broken, because the copy was right when the shipped code was not.
| suite | checks | what it does |
|---|---|---|
tests/generator-suite.js |
80 | loads the real browser module under Node and classifies the same configurations the shell suite drives, plus the pbr exclusion logic. Needs no router; runs on every push. |
tests/ownership-suite.sh |
29 | drives the installed init script on a sandbox router, with configurations chosen because the two halves of the ownership rule could read them differently. |
tests/hardware-suite.sh |
37 | the ownership gate, arming, reconciler restart discipline, the security cases, pbr coexistence and the switchover hooks. |
Every check names, in a comment written before the assertion, the smallest edit to the product that turns it red. That is not a house style: running those sabotages is what found two checks in this round that could not fail at all, one of which was the check meant to catch non-determinism.
The pbr group skips loudly when pbr is not installed, and the run reports
the number of checks that did not run. A group that skips silently reads as a
passing group to anyone scanning the tail of the output.
The first two exist because the ownership rule is implemented twice and a rule implemented twice drifts. They use the same fixtures, in the same notation, so the two implementations are held to one answer. Details in CONTRIBUTING.md.
Prefer to compile it yourself, or running an OpenWrt release older than
25.12 where the .apk above will not install? Build it with the OpenWrt
SDK for your release, then install the resulting package.
Build:
# from an OpenWrt SDK tree matching your device's release
./scripts/feeds update -a
./scripts/feeds install -a
git clone https://github.com/VolanticSystems/luci-app-wansentry.git \
package/luci-app-wansentry
make package/luci-app-wansentry/compile V=s
Install on the device (copy the built package over first):
apk add --allow-untrusted /tmp/luci-app-wansentry-*.apk # pulls in mwan3
--allow-untrusted is required because a locally built package is not signed
by an OpenWrt repository key. On releases still using opkg, use
opkg install ./luci-app-wansentry_*.ipk instead.
Then Network → WAN and VPN Failover.
Removing the package does not tear down failover. The generated mwan3
configuration stays in /etc/config/mwan3 and mwan3 keeps running it, so the
router carries on failing over exactly as before. That is deliberate: removing
a configuration front end should not drop your connections or silently change
how the router routes.
If you want the failover configuration gone as well, remove the sections wansentry created before or after removing the package. They are all marked, so they are easy to find:
uci show mwan3 | grep "wansentry='1'"
for s in $(uci show mwan3 | grep "wansentry='1'" | cut -d. -f2 | cut -d= -f1 | sort -u); do
uci delete mwan3.$s
done
uci commit mwan3
/etc/init.d/mwan3 stop && /etc/init.d/mwan3 disable
Nothing else wansentry installed survives removal.
Documented up front rather than left to be discovered.
- IPv6 is generated but has not been failover-tested. When an uplink has an
IPv6 partner (
wan6besidewan), both halves are generated so IPv6 fails over with IPv4. The test suite checks exactly what is written, but there was no IPv6 line anywhere in development to carry a live failover, so what is claimed is that the configuration is correct, not that mwan3 acts on it correctly. VPN failover is IPv4 only. - Detection is not instant, and that is deliberate. An uplink is declared
down only after
interval x failure thresholdseconds, five and three by default, so expect roughly 15 s before traffic moves, and roughly 30 s before it moves back (interval x recovery threshold). Measured on hardware at 13-14 s and 31 s. Shortening the interval detects faster and flaps more; the defaults favor not flapping. - Existing connections are dropped on a switchover when conntrack flushing is on, which is the default. That is the point: entries pinned to the dead uplink would otherwise keep steering traffic down it. But mwan3's flush is global rather than per-uplink, so connections on the healthy uplink are dropped too.
- DNS is not solved, and cannot be solved here. dnsmasq merges upstream resolvers from every interface that is up and does not consult failover state, so name resolution can still be attempted through a dead uplink. mwan3 has the same gap. wansentry surfaces the failure mode and the dnsmasq workaround on screen instead of pretending otherwise. This is the most likely reason a switchover that worked correctly looks broken from the sofa, so it is worth fixing before you need it: docs/VPN-AND-POLICY-ROUTING.md section 3.
- Failover only, by design. No load balancing, no per-destination policy routing. If you need those, use mwan3 directly; wansentry deliberately refuses to grow into a second mwan3 UI.
- A new uplink will look broken until you add it here. While failover is armed, mwan3 steers traffic through its own routing tables by firewall mark. An interface that exists but is not named as your primary or backup is not in that model, so traffic bound to it gets marked onto a managed table and fails, even though the link itself is perfectly healthy. Symptom: you add a second WAN, it associates and gets a DHCP lease, and then cannot reach anything. The fix is to select it as the backup on this screen and apply, or to switch failover off while you test the new link. This is mwan3 working as designed, not a fault in the interface.
- wansentry only touches what it created. It will not modify or remove mwan3 configuration it did not generate, and the service-side reconciler applies the same rule. An existing hand-built mwan3 setup is left alone, and wansentry will decline to manage it rather than adopt it.
luci-app-appflow is a
per-application traffic dashboard: it uses netifyd's deep packet inspection to
show which applications and which devices are using the connection, live and
over the past hour. Same author, same OpenWrt release. It answers "what is on
this link", where wansentry answers "keep the link up"; the two are
independent and neither requires the other.
Apache-2.0, see LICENSE.



