Skip to content

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:

/interface/wifi/radio/print detail

Read the bands= field. Radio 0 (wifi1) reports 5ghz-a/n/ac/ax; radio 1 (wifi2) reports 2ghz-g/n/ax.

How the failover worksΒΆ

Three mechanisms stacked, in order of subtlety:

  1. Distance-based routing. Two static default routes, distance 1 (TCI) and distance 2 (ZiTel). Only the lowest-distance reachable route is active.
  2. Pinned probe routes. A /32 route 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.
  3. Blackhole backstops. A second /32 per probe at distance 250. When a WAN's gateway goes unreachable its pinned route deactivates, and without a backstop the probe falls through to 0.0.0.0/0 and answers over the other WAN β€” so netwatch reports the dead link as healthy. The blackhole drops the probe instead, producing an honest down.

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 !LAN rather than WAN, so interfaces that do not exist yet are denied by default.
  • No output chain rules β€” RouterOS defaults that chain to accept.
  • The DHCP client replies rule 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.

/interface/wifi/set wifi2 configuration.mode=station disabled=no

Scan to confirm the SSID verbatim and check signal quality:

/interface/wifi/scan wifi2 duration=10

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:

/interface/wifi/monitor wifi2 once

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ΒΆ

/ip/route/print where dst-address=0.0.0.0/0
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.

/tool/netwatch/print

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ΒΆ

/tool/traceroute 1.1.1.1 count=1

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ΒΆ

/log/print where message~"WAN1"

TestingΒΆ

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ΒΆ

  1. Power off the TCI modem
  2. Confirm traffic moves to ZiTel (~15 s)
  3. Power the modem back on and do nothing
  4. 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.

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ΒΆ

/export file=dualwan-final
/system/backup/save name=dualwan-final
scp admin@192.168.8.1:dualwan-final.rsc ~/

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.