Dual WAN Active-Passive Failover β hAP axΒ³ΒΆ
Automatic failover between a TCI fiber uplink (primary) and a ZiTel TD-LTE uplink (backup) on a MikroTik hAP axΒ³, configured entirely from the RouterOS CLI.
Device: MikroTik hAP axΒ³ (C53UiG+5HPaxD2HaxD)
RouterOS: 7.23.3 (stable), wifi-qcom package β uses /interface/wifi, not the
legacy /interface/wireless menu
Configured: 2026-08-07
ObjectiveΒΆ
TCI fiber carries all traffic. When it fails, traffic moves to ZiTel automatically. When it recovers, traffic moves back automatically. No manual intervention in either direction.
ArchitectureΒΆ
TCI Fiber Modem ZiTel TD-LTE
192.168.100.1 192.168.0.1
β β
[Ethernet] [WiFi 2.4 GHz]
β β
β ether1 wifi2 β (station)
ββββββββββββ¬ββββββββββββββββββββββββ
β
ββββββββββββΌββββββββββββββββββββββββ
β hAP axΒ³ β
β β
β ether1 WAN1 TCI fiber β
β wifi2 WAN2 ZiTel (2.4 GHz) β
β wifi1 AP clients (5 GHz) β
β ether2-5 LAN ports β
β bridge-lan 192.168.8.1/24 β
ββββββββββββ€ββββββββββββββββββββββββ
β
ββββββββββββββΌβββββββββββββ
ether2-5 wifi1 DHCP
(wired) (5 GHz AP) .10-.254
Address planΒΆ
| Segment | Subnet | Gateway | Notes |
|---|---|---|---|
| TCI fiber (WAN1) | 192.168.100.0/24 |
192.168.100.1 |
DHCP lease 192.168.100.46 |
| ZiTel TD-LTE (WAN2) | 192.168.0.0/24 |
192.168.0.1 |
DHCP lease 192.168.0.136 |
| LAN | 192.168.8.0/24 |
192.168.8.1 |
pool .10β.254 |
All three subnets must differ. Verify before starting β Iranian CPEs frequently ship on the same default subnet, and overlapping ranges break routing in a way no failover config can fix.
Interface rolesΒΆ
| Interface | Band / speed | Role |
|---|---|---|
ether1 |
2.5 Gb | WAN1 β TCI fiber |
ether2βether5 |
1 Gb | LAN, bridge ports |
wifi1 |
5 GHz | Client AP (huch5) |
wifi2 |
2.4 GHz | WAN2 β station to ZiTel |
wifi1 is 5 GHz and wifi2 is 2.4 GHz on this board β the reverse of the common assumption. Always confirm before assigning roles:
Read the
bands=field. Radio 0 (wifi1) reports5ghz-a/n/ac/ax; radio 1 (wifi2) reports2ghz-g/n/ax.
How the failover worksΒΆ
Three mechanisms stacked, in order of subtlety:
- Distance-based routing. Two static default routes, distance 1 (TCI) and distance 2 (ZiTel). Only the lowest-distance reachable route is active.
- Pinned probe routes. A
/32route per WAN forces its health probe out that specific uplink regardless of which default route is active. Without pinning, netwatch would follow the active route and could never detect a dead link's recovery. - Blackhole backstops. A second
/32per probe at distance 250. When a WAN's gateway goes unreachable its pinned route deactivates, and without a backstop the probe falls through to0.0.0.0/0and answers over the other WAN β so netwatch reports the dead link as healthy. The blackhole drops the probe instead, producing an honestdown.
Netwatch watches each probe and runs a script on state change: the down script
disables that WAN's default route, the up script re-enables it.
The blackhole routes are mandatory, not hardening. Without them the down event
never fires, and with no down there is no down β up edge β so failback never
happens and the router stays on the backup uplink indefinitely.
ConfigurationΒΆ
Starting point was /system reset-configuration no-defaults=yes, reconnecting over
MAC-WinBox (Neighbors tab β click the MAC address). Everything below is CLI.
1. LAN bridge, addressing, DHCPΒΆ
ether1 is deliberately excluded from the bridge β it is WAN1.
/system/identity/set name=hAP-ax3
/user/set admin password="CHANGE-ME"
/interface/bridge/add name=bridge-lan comment="LAN"
/interface/bridge/port
add bridge=bridge-lan interface=ether2
add bridge=bridge-lan interface=ether3
add bridge=bridge-lan interface=ether4
add bridge=bridge-lan interface=ether5
/ip/address/add address=192.168.8.1/24 interface=bridge-lan comment="LAN gateway"
/ip/pool/add name=pool-lan ranges=192.168.8.10-192.168.8.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.8.0/24 gateway=192.168.8.1 dns-server=192.168.8.1 comment="clients resolve via router"
/ip/dns/set servers=1.1.1.1,9.9.9.9 allow-remote-requests=yes
Clients receive 192.168.8.1 as their DNS server, and the router forwards to
1.1.1.1 / 9.9.9.9. Neither forwarder may be a pinned probe address β otherwise
DNS dies whenever that WAN does.
2. Interface lists, NAT, firewallΒΆ
Applied before any WAN is connected, so the router is never exposed with an open config. Everything references the lists, so adding the second WAN later needs no rule changes.
/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"
/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=accept protocol=udp dst-port=68 in-interface-list=WAN comment="DHCP client replies"
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"
Notes:
- The input catch-all matches
!LANrather thanWAN, so interfaces that do not exist yet are denied by default. - No
outputchain rules β RouterOS defaults that chain to accept. - The
DHCP client repliesrule was needed on this board; the WAN DHCP client would not bind without it. Drop it if your lease binds fine. - Fasttrack is deliberately omitted. Fasttracked connections bypass the routing decision, which is exactly what breaks distance-based failover β traffic keeps riding the dead link until conntrack expires. The axΒ³ routes and NATs a home fiber line comfortably without it.
3. 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.8.0/24
/ip/service/set winbox address=192.168.8.0/24
/ip/service/disable telnet,ftp,api,api-ssl
Optional key-based SSH:
ssh-keygen -t ed25519 -f ~/.ssh/mikrotik_ax3 -C "hap-ax3"
scp ~/.ssh/mikrotik_ax3.pub admin@192.168.8.1:mikrotik_ax3.pub
/user/ssh-keys/import public-key-file=mikrotik_ax3.pub user=admin
/ip/ssh/set strong-crypto=yes allow-none-crypto=no always-allow-password-login=no
Verify the key works in a second terminal before closing the current session β
always-allow-password-login=no disables password login for that user.
4. WAN1 β TCI fiber on ether1ΒΆ
/ip/dhcp-client/add interface=ether1 add-default-route=no use-peer-dns=no \
use-peer-ntp=no disabled=no comment="WAN1 TCI fiber"
add-default-route=no is required: netwatch cannot disable a dynamically added
route, so both default routes must be static.
use-peer-dns=no keeps the modem from overwriting the DNS forwarders.
5. WAN2 β ZiTel station on wifi2ΒΆ
Set mode=station and enable in one command. Enabling first would briefly broadcast
an unsecured AP, since a freshly reset radio defaults to ap mode.
Scan to confirm the SSID verbatim and check signal quality:
Observed: _mq8_2.4_ on 2412 (channel 1), WPA2-PSK, -57 dBm β comfortably above
the ~-70 dBm range where a WiFi WAN starts flapping.
/interface/wifi/set wifi2 configuration.ssid="_mq8_2.4_" \
security.authentication-types=wpa2-psk security.passphrase="ZITEL-PASSWORD"
/interface/list/member/add list=WAN interface=wifi2 comment="WAN2 ZiTel TD-LTE"
/ip/dhcp-client/add interface=wifi2 add-default-route=no use-peer-dns=no \
use-peer-ntp=no disabled=no comment="WAN2 ZiTel"
Confirm association:
Expect state: authorized and the ZiTel's BSSID as ap-address.
6. Client AP β 5 GHz on wifi1ΒΆ
/interface/wifi/set wifi1 configuration.mode=ap configuration.ssid="huch5" \
security.authentication-types=wpa2-psk,wpa3-psk \
security.passphrase="CLIENT-PASSWORD" \
channel.band=5ghz-ax channel.width=20/40/80mhz disabled=no
/interface/wifi/set wifi1 channel.frequency=5180
/interface/bridge/port/add bridge=bridge-lan interface=wifi1 comment="5GHz AP"
Pin a non-DFS channel. Left on auto it selected 5500, a DFS channel (trailing D
in the channel list). DFS requires a radar availability check before transmitting,
forces the AP to vacate on radar detection, and many clients discover DFS channels
slowly or not at all. Non-DFS options: 5180, 5200, 5220, 5240.
wpa2-psk,wpa3-psk is WPA2/WPA3 transition mode. Drop wpa3-psk if an older device
refuses to associate.
7. Routes β defaults, probes, blackholesΒΆ
# default routes
/ip/route/add dst-address=0.0.0.0/0 gateway=192.168.100.1 distance=1 \
comment="WAN1-TCI-default"
/ip/route/add dst-address=0.0.0.0/0 gateway=192.168.0.1 distance=2 \
comment="WAN2-ZiTel-default"
# pinned probe routes - never disabled
/ip/route/add dst-address=8.8.8.8/32 gateway=192.168.100.1 comment="probe-WAN1-TCI"
/ip/route/add dst-address=8.8.4.4/32 gateway=192.168.0.1 comment="probe-WAN2-ZiTel"
# blackhole backstops - prevent probes leaking to the other WAN
/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
The route comments are load-bearing β the netwatch scripts locate routes by comment string.
blackhole is a valueless flag in RouterOS 7.23. type=blackhole and
blackhole=yes are both rejected; it must be bare, and placing it last is safest.
Side effect: while a WAN is down, its probe address is unreachable from the LAN. This
is why clients are handed 192.168.8.1 for DNS and the router forwards to addresses
that are not pinned.
8. Failover scriptsΒΆ
Named scripts, not inline down-script= strings. Inline versions need nested quote
escaping on one long line, which corrupted the netwatch entry on the first attempt
(see Traps). Named scripts are also testable by hand.
/system/script/add name=wan1-down policy=read,write,test source={
:log warning "WAN1 TCI DOWN - failing over to ZiTel";
/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 ZiTel";
/ip route enable [find comment="WAN1-TCI-default"];
/ip firewall connection remove [find where reply-dst-address~"192.168.0"];
}
/system/script/add name=wan2-down policy=read,write,test source={
:log warning "WAN2 ZiTel DOWN - no backup uplink available";
}
/system/script/add name=wan2-up policy=read,write,test source={
:log info "WAN2 ZiTel UP - backup uplink ready";
}
The connection flush is scoped by WAN subnet on purpose. A blanket
remove [find] wipes the entire connection table including the SSH session to
192.168.8.1, disconnecting you on every failover. Matching reply-dst-address
hits only connections masqueraded out that WAN's address; LAN-local sessions survive.
Test them by hand before wiring netwatch:
/system/script/run wan1-down
/ip/route/print # WAN1 should be Xs, WAN2 promoted to As
/system/script/run wan1-up
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-ZiTel" 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="ZiTel health (log only)" disabled=no
Scripts are referenced as /system script run <name> β netwatch's script fields hold
code, not references.
WAN2-ZiTel only logs and deliberately changes no routes: ZiTel 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.
Thresholds are anti-flap tuning, not arbitrary. Each test spans ~5 s across 10 packets and fails only at 100% loss. See Traps.
10. 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.
OperationsΒΆ
Which uplink is active right now?ΒΆ
| Flags | Gateway | Meaning |
|---|---|---|
As |
192.168.100.1 |
TCI fiber carrying traffic (normal) |
As |
192.168.0.1 |
ZiTel carrying traffic (failed over) |
Xs |
192.168.100.1 |
fiber disabled by wan1-down β it is down |
s |
192.168.0.1 |
ZiTel idle in standby |
Only one route ever carries A.
Uplink healthΒΆ
Shows the health of both uplinks rather than which is in use. WAN1-TCI down while
WAN2-ZiTel is up means you are running on the backup with no further fallback.
Definitive path checkΒΆ
Hop 0 is the gateway actually in use. Cannot be fooled by a stale route table.
Do not use 8.8.8.8 or 8.8.4.4 to test the path β both are pinned and always
report their own WAN regardless of which is active. Use 1.1.1.1, 4.2.2.4, or
similar.
Failover historyΒΆ
TestingΒΆ
Simulated uplink loss (no hardware access needed)ΒΆ
Blackholes only the probe. Cable stays up, DHCP lease intact, modem perfectly healthy β which is precisely the failure mode link-level detection cannot see.
/ip/firewall/filter/add chain=output action=drop dst-address=8.8.8.8 \
out-interface=ether1 place-before=0 comment="TEST fiber internet loss"
# wait 30s, check /ip/route/print, then remove
/ip/firewall/filter/remove [find comment="TEST fiber internet loss"]
Verified result: event down β wan1-down β route disabled β event up β
wan1-up β route re-enabled, 18 seconds start to finish.
Real modem power-cycleΒΆ
- Power off the TCI modem
- Confirm traffic moves to ZiTel (~15 s)
- Power the modem back on and do nothing
- Failback occurs automatically
Verified result: down at 22:37:22, automatic up at 22:42:00.
Failback waits on the modem, not on netwatch. A fiber ONT cold boot must complete link training, DHCP, and upstream PPPoE before it passes traffic β 3β5 minutes is normal. Netwatch detects recovery within ~15 s of the fiber actually working, and not a moment sooner. Waiting only 60 s and concluding failback is broken is a mistake.
Behaviour summaryΒΆ
| Event | Detection | Result |
|---|---|---|
| Fiber cable unplugged | ~15 s | link-level + netwatch β ZiTel |
| Fiber modem powered off | ~15 s | blackhole forces real down β ZiTel |
| Modem alive, uplink dead | ~15 s | netwatch only β invisible to link detection |
| Fiber returns | ~15 s after it truly works | automatic failback to TCI |
| ZiTel dies | ~15 s | logged warning, no route change |
TrapsΒΆ
Four problems hit during this build. None are recoverable from a config export.
1. Blackhole routes are mandatoryΒΆ
Without them, powering off the TCI modem deactivates the pinned probe route, so
netwatch's pings to 8.8.8.8 fall through to 0.0.0.0/0 and answer over ZiTel.
Netwatch reports WAN1 as up for the entire outage. Traffic still moves to ZiTel by
link-level failover, but no down is recorded β and with no down there is no
down β up edge, so wan1-up never fires and the router stays on the backup
permanently.
Symptom: WAN1-TCI-default stuck at Xs while /ping 8.8.8.8 succeeds at 31 ms and
probe-WAN1-TCI shows As.
2. 1.0.0.1 is unusable as a netwatch targetΒΆ
The original WAN2 probe. Manual /ping 1.0.0.1 returned 10/10 replies at 0% loss,
136 ms while netwatch's Status tab showed Done Tests 27, Failed Tests 27 with
Sent 5, Response 5, Loss 0%. Netwatch was receiving replies it would not count.
Cloudflare deprioritizes ICMP and it does not survive netwatch's probe pattern through
ZiTel's CGNAT. Google DNS works reliably on both paths. Switched to 8.8.4.4.
Diagnostic that isolated it: repoint the netwatch entry at a known-good host. If it comes up, the entry is healthy and the target is at fault.
3. Netwatch defaults flap on these linksΒΆ
Three false failovers in seven minutes, each ~10 s. Critically, the ZiTel monitor flapped even with all thresholds stripped and no scripts attached β ruling out configuration as the cause. These links have ordinary transient loss, and netwatch's default sensitivity treats one bad check as an outage.
Left alone this is worse than no failover: every flap flushes the connection table and bounces traffic between uplinks.
Fix: packet-count=10 packet-interval=500ms thr-loss-percent=100 timeout=2s. Each
test becomes a ~5 s sample failing only at total loss. Cost is ~15β20 s detection
instead of ~10 s. Result: 22 minutes of complete silence on links that had been
flapping every few minutes.
4. Inline netwatch scripts corrupt the entryΒΆ
Creating a netwatch entry with escaped inline scripts β
down-script=":log warning \"...\"" β produced a stray test-script="" field. The
entry then reported down permanently regardless of probe results.
Always use named scripts and reference them with /system script run <name>.
Syntax notes for RouterOS 7.23ΒΆ
| 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 |
BackupΒΆ
The .rsc export rebuilds this configuration on a blank router. Store it off the
device β a reset destroys it.
Open itemsΒΆ
- wifi country is still
Latvia(factory default). It governs legal 5 GHz channels and transmit power. Find the accepted spelling with tab completion:/interface/wifi/set wifi1 configuration.country=ir<TAB> - No 2.4 GHz AP for clients β wifi2 is dedicated to the ZiTel uplink. A virtual AP on that radio is possible but would be locked to ZiTel's channel and halve throughput.
- IPv6 is entirely unconfigured. The failover logic is IPv4-only.