Dual WAN Active-Passive Failover — hAP ax lite + Irancell¶
Build runbook for a MikroTik hAP ax lite: TCI fiber as primary, Irancell TD-LTE as backup, both uplinks wired, with automatic failover and automatic failback.
Adapted from active-passive-ha.md, which documents the same failover design on a hAP ax³. Read that one for the reasoning behind each mechanism — this file is the executable sequence, with the hardware differences worked in.
Port and radio plan¶
| Interface | Role |
|---|---|
ether1 |
WAN1 — TCI fiber (primary, distance 1) |
ether2 |
WAN2 — Irancell TD-LTE (backup, distance 2) |
ether3, ether4 |
LAN — bridge ports |
wifi1 |
LAN — client AP, 2.4 GHz, dedicated |
Both uplinks on cable means the single 2.4 GHz radio serves clients only. No station mode, no virtual AP, no shared channel, no halved throughput — the radio picks its own channel and runs at full duty.
Only two wired LAN ports remain. The hAP ax lite has four ports total and two are now uplinks. If more than two wired devices are needed, put a cheap unmanaged switch on
ether4.
Differences from the reference build¶
| Reference build (ax³) | This build (ax lite) | |
|---|---|---|
| Device | hAP ax³ | hAP ax lite (L41G-2axD) |
| Radios | 2 (2.4 + 5 GHz) | 1 — 2.4 GHz only |
| Ethernet ports | 5 | 4 |
| WAN2 attachment | WiFi station (2.4 GHz) | ether2, wired |
| Client AP | 5 GHz, dedicated radio | 2.4 GHz, dedicated radio |
| WAN2 uplink | ZiTel, 192.168.0.1 |
Irancell — verify, never assume |
| LAN subnet | 192.168.8.0/24 |
192.168.20.0/24 |
| WAN2 naming | *-ZiTel |
*-Irancell |
| Connection-flush match | plain substring | escaped-dot regex (Step 8) |
Wiring WAN2 also removes a whole class of failure the reference build had to design
around: no SSID to read, no signal level to worry about, no wireless flapping feeding
false failovers. Link state on ether2 is unambiguous.
Why 192.168.20.0/24 and not 192.168.8.0/24¶
Irancell distributes many Huawei LTE CPEs (B310 / B315 / B525 / B535) whose default
LAN is 192.168.8.1 — identical to the reference build's LAN. Reusing it would
overlap the modem's own subnet and break routing in a way no failover config can
repair.
Other Irancell units are ZTE on 192.168.1.1, which collides with nothing but does
break the connection-flush pattern if written as a plain substring.
192.168.20.0/24 avoids both. If you choose differently, adjust every command below.
Placeholders¶
Fill these in from Step 0 before running anything.
| Placeholder | Meaning |
|---|---|
<TCI-GW> |
TCI fiber modem gateway (typically 192.168.100.1) |
<IRANCELL-GW> |
Irancell modem gateway (192.168.1.1 or 192.168.8.1) |
<AP-SSID> / <AP-PASS> |
client WiFi name and passphrase |
Step 0 — Discovery¶
Run before changing anything.
Record and confirm:
- Board name — expect
hAP ax lite - RouterOS version — see the upgrade note below
- Package — must show
wifi-qcom, meaning the/interface/wifimenu applies. If it showswirelessinstead, this document's wireless syntax is wrong and you need the legacy/interface/wirelesscommands. - Port count — expect
ether1–ether4. Ifether5exists, add it to the bridge in Step 2. - Radio count — expect a single
wifi1. There is nowifi2on this board.
Upgrade first¶
Every command here is written for RouterOS 7.13+. Older releases differ in ways
that will bite you — notably the blackhole route flag and parts of the
/interface/wifi menu. Upgrade before starting:
Modem gateways¶
From a laptop plugged into each modem in turn:
Record <TCI-GW> and <IRANCELL-GW>, then confirm all three subnets differ — both
modems and the planned 192.168.20.0/24. Overlapping subnets are the one failure that
cannot be worked around later.
Step 1 — Clean reset¶
Before running it:
- Move the laptop to ether3 or ether4.
ether1andether2both become uplinks and leave the bridge. - Use a cable, not WiFi. The reset wipes the radio config, and with only one radio there is no second band to recover on.
- Have WinBox ready. A
no-defaultsreset leaves no IP, so reconnect via Neighbors → click the MAC address, useradmin, blank password. MAC-telnet and SSH will not work; this is WinBox-only. - Leave both modem cables unplugged until Step 4 — the firewall lands first.
Step 2 — LAN bridge, addressing, DHCP¶
/system/identity/set name=hAP-friend
/user/set admin password="CHANGE-ME"
/interface/bridge/add name=bridge-lan comment="LAN"
/interface/bridge/port
add bridge=bridge-lan interface=ether3
add bridge=bridge-lan interface=ether4
/ip/address/add address=192.168.20.1/24 interface=bridge-lan comment="LAN gateway"
/ip/pool/add name=pool-lan ranges=192.168.20.10-192.168.20.254
/ip/dhcp-server/add name=dhcp-lan interface=bridge-lan address-pool=pool-lan \
lease-time=1h disabled=no
/ip/dhcp-server/network/add address=192.168.20.0/24 gateway=192.168.20.1 \
dns-server=192.168.20.1 comment="clients resolve via router"
/ip/dns/set servers=1.1.1.1,9.9.9.9 allow-remote-requests=yes
Only ether3 and ether4 — ether1 and ether2 are the two uplinks. wifi1
joins the bridge in Step 7, once it's configured as an AP.
DNS forwarders must not be 8.8.8.8 or 8.8.4.4; both are pinned to specific WANs in
Step 6, and a forwarder on a pinned address dies whenever that WAN does. Clients get
192.168.20.1 so nothing on the LAN depends on a probe address.
Verify:
Renew the laptop's lease, then SSH to 192.168.20.1 and drop WinBox.
Step 3 — Interface lists, NAT, firewall¶
All of this lands before either uplink is connected, so the router is never exposed. Both WAN members go in now, since both are known ethernet ports.
/interface/list/add name=WAN comment="uplinks"
/interface/list/add name=LAN comment="trusted"
/interface/list/member
add list=LAN interface=bridge-lan
add list=WAN interface=ether1 comment="WAN1 TCI fiber"
add list=WAN interface=ether2 comment="WAN2 Irancell TD-LTE"
/ip/firewall/nat
add chain=srcnat out-interface-list=WAN action=masquerade comment="NAT out any WAN"
/ip/firewall/filter
add chain=input action=accept connection-state=established,related,untracked \
comment="accept established,related,untracked"
add chain=input action=drop connection-state=invalid comment="drop invalid"
add chain=input action=accept protocol=icmp comment="accept ICMP"
add chain=input action=accept dst-address=127.0.0.1 comment="accept loopback"
add chain=input action=drop in-interface-list=!LAN comment="drop everything not from LAN"
add chain=forward action=accept connection-state=established,related,untracked \
comment="accept established,related,untracked"
add chain=forward action=drop connection-state=invalid comment="drop invalid"
add chain=forward action=drop connection-state=new connection-nat-state=!dstnat \
in-interface-list=WAN comment="drop new from WAN not dstnat"
add chain=forward action=accept in-interface-list=LAN out-interface-list=WAN \
comment="LAN to WAN"
add chain=forward action=drop comment="drop everything else"
Everything references the lists, so one masquerade rule and one forward rule cover both uplinks.
The input catch-all matches !LAN rather than WAN, so interfaces that do not exist
yet are denied by default. No output rules — RouterOS defaults that chain to accept.
Fasttrack is deliberately omitted. Fasttracked connections bypass the routing decision, which is precisely what breaks distance-based failover — traffic keeps riding the dead link until conntrack expires. The ax lite's dual-core 800 MHz CPU handles a home fiber line without it, though it has less headroom than the ax³.
Management hardening¶
/ip/neighbor/discovery-settings/set discover-interface-list=LAN
/tool/mac-server/set allowed-interface-list=LAN
/tool/mac-server/mac-winbox/set allowed-interface-list=LAN
/ip/service/set ssh address=192.168.20.0/24
/ip/service/set winbox address=192.168.20.0/24
/ip/service/disable telnet,ftp,api,api-ssl
Step 4 — Both uplinks¶
/ip/dhcp-client/add interface=ether1 add-default-route=no use-peer-dns=no \
use-peer-ntp=no disabled=no comment="WAN1 TCI fiber"
/ip/dhcp-client/add interface=ether2 add-default-route=no use-peer-dns=no \
use-peer-ntp=no disabled=no comment="WAN2 Irancell"
add-default-route=no is required on both — netwatch cannot disable a dynamically
added route, so both default routes must be static.
use-peer-dns=no keeps either modem from overwriting the DNS forwarders.
Now plug in: TCI fiber → ether1, Irancell → ether2. Then:
Expect both status=bound, with gateway=<TCI-GW> and gateway=<IRANCELL-GW>
respectively. Confirm the leases land in the subnets you recorded in Step 0.
If either stays searching... — this happened on the reference build:
/ip/firewall/filter/add chain=input action=accept protocol=udp dst-port=68 \
in-interface-list=WAN place-before=4 comment="DHCP client replies"
Only add it if a lease genuinely fails; it opens a port that is otherwise closed.
Step 5 — Verify each uplink independently¶
Before building failover, prove both paths reach the internet. Add the pinned probe routes first — they let you test the backup while the primary carries traffic.
/ip/route/add dst-address=8.8.8.8/32 gateway=<TCI-GW> comment="probe-WAN1-TCI"
/ip/route/add dst-address=8.8.4.4/32 gateway=<IRANCELL-GW> comment="probe-WAN2-Irancell"
Test each:
Both must return replies. Expect the Irancell path to show noticeably higher latency than fiber — that's normal for TD-LTE.
Confirm each probe genuinely uses its own uplink:
Hop 0 must be <TCI-GW> and <IRANCELL-GW> respectively. If both show the same
gateway, the pin failed and everything built on top will monitor one link twice.
Step 6 — Default routes and blackhole backstops¶
# default routes
/ip/route/add dst-address=0.0.0.0/0 gateway=<TCI-GW> distance=1 \
comment="WAN1-TCI-default"
/ip/route/add dst-address=0.0.0.0/0 gateway=<IRANCELL-GW> distance=2 \
comment="WAN2-Irancell-default"
# blackhole backstops - MANDATORY, see Traps
/ip/route/add dst-address=8.8.8.8/32 distance=250 comment="probe-WAN1-blackhole" blackhole
/ip/route/add dst-address=8.8.4.4/32 distance=250 comment="probe-WAN2-blackhole" blackhole
blackhole is a valueless flag in RouterOS 7.23. Both type=blackhole and
blackhole=yes are rejected; place it last with no value.
Route comments are load-bearing — the netwatch scripts locate routes by comment string.
Verify:
Expect the distance-1 default flagged As, the distance-2 default plain s (standby),
both /32 probes As, and both blackholes present without A.
If the WAN2 probe misbehaves¶
On the reference build the original WAN2 probe 1.0.0.1 answered 10/10 manual pings
at 0% loss while netwatch scored 27 of 27 tests as failed — Cloudflare
deprioritises ICMP and it did not survive netwatch's probe pattern through CGNAT. If
8.8.4.4 shows the same behaviour over Irancell in Step 9, try 9.9.9.9 and repoint
both the probe and blackhole routes to match.
Step 7 — Client AP on wifi1¶
The radio is dedicated to clients, so this is a plain AP — no station mode, no virtual interface, and it chooses its own channel.
/interface/wifi/set wifi1 configuration.mode=ap configuration.ssid="<AP-SSID>" \
security.authentication-types=wpa2-psk,wpa3-psk \
security.passphrase="<AP-PASS>" \
channel.band=2ghz-ax channel.width=20mhz disabled=no
/interface/bridge/port/add bridge=bridge-lan interface=wifi1 comment="2.4GHz AP"
Pick a clean channel. Scan the neighbourhood first:
Note which channels the strongest neighbours occupy, then pin one of the three non-overlapping options — 2412 (ch 1), 2437 (ch 6), or 2462 (ch 11):
width=20mhz is deliberate. 40 MHz on 2.4 GHz consumes most of the band and, in
any typical apartment, causes more contention than the extra width recovers. On a
single-radio device serving all clients, 20 MHz is the more reliable choice.
wpa2-psk,wpa3-psk is WPA2/WPA3 transition mode; drop wpa3-psk if an older device
refuses to associate.
Country defaults to Latvia and governs legal channels and transmit power. Find the
accepted spelling with tab completion — plain Iran is rejected:
Verify:
Expect state: running. The R flag appears once a client associates. Connect a phone
and confirm it gets a 192.168.20.x address with working internet.
Step 8 — Failover scripts¶
The regex differs from the reference build and this matters.
Plain substring matching is unsafe here. If Irancell is 192.168.1.0/24, the pattern
"192.168.1" also matches 192.168.100.46, and an unescaped . is a regex wildcard
that happily matches the 0. Escaping the dots makes 192\.168\.1\. fail to match
192.168.100.46, which is the behaviour we need.
/system/script/add name=wan1-down policy=read,write,test source={
:log warning "WAN1 TCI DOWN - failing over to Irancell";
/ip route disable [find comment="WAN1-TCI-default"];
/ip firewall connection remove [find where reply-dst-address~"192\\.168\\.100\\."];
}
/system/script/add name=wan1-up policy=read,write,test source={
:log info "WAN1 TCI UP - failing back from Irancell";
/ip route enable [find comment="WAN1-TCI-default"];
/ip firewall connection remove [find where reply-dst-address~"192\\.168\\.1\\."];
}
/system/script/add name=wan2-down policy=read,write,test source={
:log warning "WAN2 Irancell DOWN - no backup uplink available";
}
/system/script/add name=wan2-up policy=read,write,test source={
:log info "WAN2 Irancell UP - backup uplink ready";
}
Adjust both patterns to the actual modem subnets. The wan1-down pattern must match
the fiber subnet; the wan1-up pattern must match the Irancell subnet.
Keep the flush scoped. A blanket remove [find] wipes the entire connection table
including the SSH session to the router, disconnecting you on every failover.
Test by hand before wiring netwatch¶
Expect WAN1-TCI-default flagged Xs and WAN2-Irancell-default promoted to As.
Confirm internet still works from a client, then:
Step 9 — Netwatch¶
/tool/netwatch/add name="WAN1-TCI" host=8.8.8.8 type=icmp interval=10s timeout=2s \
packet-count=10 packet-interval=500ms thr-loss-percent=100 \
down-script="/system script run wan1-down" \
up-script="/system script run wan1-up" \
comment="TCI fiber health" disabled=no
/tool/netwatch/add name="WAN2-Irancell" host=8.8.4.4 type=icmp interval=10s timeout=2s \
packet-count=10 packet-interval=500ms thr-loss-percent=100 \
down-script="/system script run wan2-down" \
up-script="/system script run wan2-up" \
comment="Irancell health (log only)" disabled=no
Named scripts only. Inline down-script=":log warning \"...\"" with escaped quotes
corrupted the entry on the reference build, leaving a stray test-script="" that made
it report down permanently regardless of probe results.
Scripts are referenced as /system script run <name> — netwatch's script fields hold
code, not references.
The WAN2-Irancell monitor only logs and changes no routes: Irancell is already the
lowest-priority path with nothing to demote it to. Its value is telling you the backup
has silently died before you need it.
Those thresholds are anti-flap tuning, not cosmetics. Netwatch defaults produced three false failovers in seven minutes on the reference build — and the backup monitor flapped even with all thresholds stripped and no scripts attached, proving the links themselves have ordinary transient loss. Each test now spans ~5 s across 10 packets and fails only at 100% loss. Cost: ~15–20 s detection instead of ~10 s.
Verify both entries reach up and that their since timestamps stay steady across
several prints:
If a since timestamp keeps resetting, the link is flapping and the thresholds need
loosening further — not tightening.
Step 10 — NTP, testing, backup¶
NTP¶
/system/ntp/client/set enabled=yes servers=0.pool.ntp.org,1.pool.ntp.org
/system/clock/set time-zone-name=Asia/Tehran
Both DHCP clients run use-peer-ntp=no, so nothing else sets the clock. Without this
the log timestamps drift and outage postmortems are worthless.
Test A — cable pull¶
Unplug the fiber from ether1. Two mechanisms fire together: the connected route
disappears so the distance-1 default goes inactive on its own, and netwatch records a
down. Traffic should be on Irancell within ~15 s. Plug it back and confirm failback.
Test B — simulated uplink loss¶
The case link-state detection cannot see: cable up, lease intact, modem healthy, but no internet beyond it.
/ip/firewall/filter/add chain=output action=drop dst-address=8.8.8.8 \
out-interface=ether1 place-before=0 comment="TEST fiber loss"
# wait 30s, check /ip/route/print, then remove
/ip/firewall/filter/remove [find comment="TEST fiber loss"]
Reference build result: down → failover → up → failback in 18 seconds.
Test C — real modem power-cycle¶
- Power off the TCI modem
- Confirm traffic moves to Irancell (~15 s)
- Power the modem back on and do nothing
- Failback occurs automatically
Wait 3–5 minutes before judging failback. A fiber ONT cold boot must complete link training, DHCP, and upstream PPPoE before it passes traffic. Netwatch reacts within ~15 s of the fiber genuinely working, not of it being powered on. The reference build went down at 22:37:22 and recovered automatically at 22:42:00 — concluding it was broken after 60 seconds would have been wrong.
Backup¶
Store it off the device — a reset destroys it.
Operations¶
Which uplink is active?¶
| Flags | Gateway | Meaning |
|---|---|---|
As |
<TCI-GW> |
TCI fiber carrying traffic (normal) |
As |
<IRANCELL-GW> |
Irancell carrying traffic (failed over) |
Xs |
<TCI-GW> |
fiber disabled by wan1-down — it is down |
s |
<IRANCELL-GW> |
Irancell idle in standby |
Only one route ever carries A.
Uplink health¶
WAN1-TCI down while WAN2-Irancell is up means you are running on the backup with no
further fallback.
Definitive path check¶
Hop 0 is the gateway actually in use. Do not test with 8.8.8.8 or 8.8.4.4 — both
are pinned and always report their own WAN regardless of which is active.
Failover history¶
Traps¶
Carried over from the reference build. None are recoverable from a config export.
1. Blackhole routes are mandatory¶
Without them, powering off a modem deactivates that WAN's pinned probe route, so
netwatch's pings fall through to 0.0.0.0/0 and answer over the other WAN. Netwatch
reports the dead link as up for the whole outage.
Traffic still moves to the backup by link-level failover, but no down is recorded —
and with no down there is no down → up edge, so the up script never fires and the
router stays on the backup permanently.
Symptom: default route stuck at Xs while /ping 8.8.8.8 succeeds and the probe route
shows As.
2. Validate the probe target¶
A target that answers manual pings perfectly can still fail every netwatch test. See Step 6.
3. Netwatch defaults flap¶
Three false failovers in seven minutes. Each flap flushes the connection table and bounces traffic between uplinks — worse than the outages it protects against. See Step 9.
4. Inline netwatch scripts corrupt the entry¶
Always use named scripts. See Step 9.
5. Only two wired LAN ports¶
ether1 and ether2 are uplinks. If more than two wired devices are needed, add an
unmanaged switch on ether4 — do not repurpose an uplink port.
RouterOS 7.23 syntax notes¶
| Wrong | Right |
|---|---|
type=blackhole |
blackhole (valueless flag, place last) |
blackhole=yes |
blackhole |
thr-max-rtt=1s |
thr-max=1s |
configuration.country=Iran |
tab-complete — long ISO-style names |
Checklist¶
- Step 0 — board confirmed
hAP ax lite,wifi-qcompackage, port count noted - RouterOS upgraded to 7.13+
- All three subnets confirmed distinct
-
<IRANCELL-GW>verified (not assumed) - Reset performed over cable, laptop on ether3/ether4
- LAN up on ether3–4, SSH to
192.168.20.1working - Firewall and NAT in place before either uplink connected
- TCI on
ether1, Irancell onether2, both leasesbound - Both probes ping successfully
- Traceroute confirms each probe uses its own gateway
- All six routes present, both blackholes flagged
- AP running on a clean channel (1, 6, or 11) at 20 MHz
- Clients getting
192.168.20.xover WiFi with internet - Scripts use escaped-dot regex matching the real subnets
- Scripts tested manually before netwatch
- Both netwatch entries
upand stable - Test A passes (cable pull)
- Test B passes (simulated loss)
- Test C passes (modem power-cycle, waited 3–5 min)
- NTP synchronised
-
/exportdownloaded off the device