Cell phone hotspot to OpenWRT One as backup to main ISP

{ EDIT: Updated Hotspot Monitor to fix boot-time operation, and to ensure operation under all circumstances. }

{ EDIT 2: Changed verbiage printed to the log }

{EDIT 3: Revised the procedure to make it a bit easier to set up}

I first tried Travelmate, but it caused CPU spikes. I then tried MultiWAN Manager, but it was slow, clunky and kept misconfiguring the connections.

So I scripted my own.

This script monitors for a certain SSID on radio0 (2.4 GHz). That SSID comes from a cell phone hotspot. If your main ISP goes down, you can feed data to/from your LAN through your cell phone’s hotspot.

The script monitors periodically (using the main 2.4GHz WiFi) for the TARGET_SSID. When the script sees the TARGET_SSID, it enables the wireless network which is set up to receive the cell phone hotspot WiFi signal; and when it no longer sees that TARGET_SSID, it disables that wireless network and its associated interface, and brings radio0 back up so the 2.4 GHz signal remains stable.

This is due to a bug that causes radio0 (2.4GHz) to go down and stay down after you stop the hotspot signal, usually requiring a manual restart of radio0. There’s also a bug wherein the wireless network set up to receive the hotspot signal loses the interface it’s assigned to when the hotspot WiFi is disabled… this script assigns that interface just before bringing up the wireless network set up to receive the hotspot signal.

DO NOT use radio1 (5GHz) for receiving a hotspot signal. It just doesn’t work. radio1 goes down when it tries, requiring a manual restart of radio1. Set your cellphone’s hotspot to only transmit on 2.4GHz.

This problem relates to bandwidth… the 20MHz wide 2.4GHz radio0 has sufficient bandwidth to both receive WiFi signals and transmit WiFi signals. The 80MHz wide 5 GHz radio1 does not.

The script below goes into your /etc/rc.local file, accessible in the LuCI interface at System >> Startup >> Local Startup. Put it below the two comment lines, but above the ‘exit 0’ line.

Network >> Interfaces >> Devices tab:
Click 'Add device configuration' button

General device options tab:
Device type: Network device
Existing device: type: phy0-sta0
MTU: 1500
MAC address: blank
TX queue length: blank
Enable IPv6: automatic (disabled)

Advanced device options tab:
Enable promiscuous mode: automatic (disabled)
Reverse path filter: disabled
Accept local: automatic (disabled)
Send ICMP redirects: automatic (disabled)
Honor gratuitous ARP: automatic (disabled)
Drop gratuitous ARP: automatic (disabled)
Neighbour cache validity: blank
State neighbour cache timeout: blank
Minimum ARP validity time: blank
Enable multicast support: automatic (enabled)
Force IGMP version: No enforcement
Force MLD version: No enforcement



Network >> Interfaces:
Click the “Add new interface” button.

Add new interface… dialog box:
Name: {CHOOSE A NAME} I chose hotspot.
Protocol: DHCP client
Device: phy0-sta0
(you might have to directly type phy0-sta0 in, rather than select it from a list)

Click the “Create Interface” button.

Back in the list of interfaces, find the interface you just created, and click its “Edit” button.

General Settings tab:
Protocol: DHCP client
Device: phy0-sta0
Disable this interface: unchecked
Bring up on boot: unchecked
Hostname to send when requesting DHCP: Do not send a hostname

Advanced Settings tab:
Force link: unchecked
Use broadcast flag: unchecked
Preferred client ID: Automatic
Client ID to send when requesting DHCP: blank
Vendor Class to send when requesting DHCP: blank
Use default gateway: checked
Use DNS servers advertised by peer: unchecked
Use custom DNS servers: 1.1.1.1; 1.0.0.1; 9.9.9.9; 149.112.112.112
NOTE: If you’re using DoH (DNS-over-HTTPS) to reroute DNS requests to HTTPS, leave Advanced Settings >> Use custom DNS Servers blank.
DNS weight: 0
Use gateway metric: 100
Multi-Path TCP: Off
Override IPv4 routing table: blank
Override IPv6 routing table: blank
Delegate IPv6 prefixes: checked
IPv6 assignment length: disabled
IPv6 prefix filter: blank
IPv6 suffix: blank
IPv6 preference: blank

Firewall Settings tab:
Create/Assign firewall-zone: wan + wan6

DHCP Server tab:
No DHCP Server configured for this interface.

Click the “Save” button.



Back at Network >> Interfaces, click the “Add new interface…” button again.

Add new interface…: Name: {CHOOSE A NAME} I chose hotspot6.
Add new interface…: Protocol: DHCPv6 client
Add new interface…: Device: Alias Interface: @{whatever you named the first interface} Mine would be: Alias Interface: “@hotspot”

Click the “Create Interface” button.

Back in the list of interfaces, find the interface you just created, and click its “Edit” button.

General Settings tab:
Protocol: DHCPv6 client
Device: @{whatever you named the first interface} Mine would be: @hotspot
General Settings: Disable this interface: unchecked
Bring up on boot: checked
Request IPv6-address: try
Request IPv6-prefix: Automatic
Extend prefix: unchecked
Do not send a Release when restarting: checked

Advanced Settings tab:
Force link: unchecked
Preferred client ID: Automatic
Client ID to send when requesting DHCP: blank
Use default gateway: checked
Use DNS servers advertised by peer: unchecked
Use custom DNS servers: 1.1.1.1; 1.0.0.1; 9.9.9.9; 149.112.112.112
NOTE: If you’re using DoH (DNS-over-HTTPS) to reroute DNS requests to HTTPS, leave Advanced Settings >> Use custom DNS Servers blank.
DNS weight: 0
Use gateway metric: 100
Multi-Path TCP: Off
Override IPv4 routing table: blank
Override IPv6 routing table: blank
IPv6 source routing: checked
Delegate IPv6 prefixes: checked
IPv6 assignment length: disabled
IPv6 prefix filter: blank
IPv6 suffix: blank
IPv6 preference: blank

Firewall Settings tab:
Create/Assign firewall-zone: wan + wan6

DHCP Server tab:
No DHCP Server configured for this interface.



Network >> Wireless
Find the 2.4 GHz radio (radio0) and click the 'Add' button

Device Configuration >> General Setup tab:
Operating frequency >> Mode: AX
Operating frequency >> Channel: auto
Width: 20 MHz
Allow legacy 802.11b rates: unchecked
Maximum transmit power: driver default
Country Code: {YOUR COUNTRY}

Device Configuration >> Advanced Settings tab:
Coverage cell density: Disabled
Distance Optimization: blank
Fragmentation Threshold: blank
RTS/CTS Threshold: blank
Force 40MHz mode: unchecked
Beacon Interval: 300
Rx LDPC: checked
Tx LDPC: checked

Interface Configuration >> General Setup tab:
Mode: client
ESSID: {YOUR HOTSPOT'S SSID} Mine is Teracube.
BSSID: blank
Network: {THE NETWORK YOU JUST CREATED} In my case, it's hotspot.

Interface Configuration >> Wireless Security tab:
Encryption: WPA2-PSK/WPA3-SAE Mixed Mode (strong security)
Key: {THE SSID'S PASSPHRASE}
802.11w Management Frame Protection: Optional
802.11w maximum timeout: blank
802.11w retry timeout: blank
Operating Channel Validation: Disabled

Interface Configuration >> Advanced Settings tab:
Multi To Unicast: unchecked
Isolate Bridge Port: unchecked
Interface name: {THE DEVICE YOU CREATED} Mine would be phy0-sta0.
MAC address: driver default
Short Preamble: checked
DTIM Interval: blank
Time interval for rekeyinng GTK: blank
Disable Inactivity Polling: unchecked
Station inactivity limit: blank
Maximum allowed Listen Interval: blank
Disassociate on Low Acknowledgement: checked

Click the Save button.

Start up your cell phone’s hotspot.

Under Network >> Interfaces, for the two interfaces you created (in my case, hotspot and hotspot6), click the 'Restart' buttons.

Go to Network >> Wireless, and for the new wireless network you just created, if its button says “Enable”, click to enable the wireless network, then click the “Save & Apply” button.

Wait for the router to connect to the cellphone hotspot. You’ll see the bars in the signal strength meter for that network turn blue, and numbers denoting the signal strength will display.

In Services >> Terminal, enter:
uci show wireless | grep .device=

It will display something like this:
wireless.default_radio0.device='radio0'
wireless.default_radio1.device='radio1'
wireless.travel_2g.device='radio0'

That last entry should be the wireless network you set up to connect to the hotspot.

To be absolutely sure, in Services >> Terminal, enter:
uci show wireless.travel_2g

It will display something like this:
wireless.travel_2g=wifi-iface
wireless.travel_2g.device='radio0'
wireless.travel_2g.network='hotspot'
wireless.travel_2g.mode='sta'
wireless.travel_2g.ssid='{SSID OF YOUR HOTSPOT}'
wireless.travel_2g.encryption='sae-mixed'
wireless.travel_2g.key='{PASSPHRASE FOR THAT SSID}'

If that is your hotspot, you’ll then change the code below to:
INTERFACE=“travel_2g”
NET_INTF=“hotspot”

Go to System >> Startup >> Local Startup.

Edit the code below to reflect your TARGET_SSID, your INTERFACE, your NET_INTF, your RADIO, the maximum_sleep you want (used to reduce CPU utilization) and whether you want logging (logging=1) or not (logging=0).

To see the logs in real-time, in Services >> Terminal, you’d enter:

logread -f | grep hotspot_monitor

At System >> Startup >> Local Startup, paste the code below the first two comment lines, but before the last (exit 0) line and click the “Save” button.


## SET UP HOTSPOT MONITOR
## To read Hotspot Monitor logs, issue either:
# logread -f | grep hotspot_monitor
# while true; do clear; logread | grep "hotspot_monitor"; sleep 5; done

(
# SETTINGS
TARGET_SSID="Teracube"
INTERFACE="travel_2g"
NET_INTF="hotspot"
RADIO="radio0"
current_sleep=20
missed_checks=0
minimum_sleep=20
maximum_sleep=120
logging=1

# Give the MT7981 driver time to fully initialize on boot
sleep 30

# Log the startup
logger -t hotspot_monitor "Hotspot Monitor: $RADIO monitoring for SSID: $TARGET_SSID"

while true; do

    # Check if the interface config is currently enabled in wireless settings
    IS_ENABLED=$(uci get wireless.$INTERFACE.disabled 2>/dev/null)

    # Check if the network interface layer is actually bound and running traffic
    INTF_UP=$(ubus call network.interface.$NET_INTF status 2>/dev/null | jsonfilter -e '@.up')

    if [ "$IS_ENABLED" = "0" ]; then
        # -------------------------------------------------------------
        # IF CONNECTED: Trust UBUS network status. NEVER scan the airwaves!
        # -------------------------------------------------------------
        if [ "$INTF_UP" = "true" ]; then
            HOTSPOT_FOUND=1
        else
            HOTSPOT_FOUND=0
        fi
    else
        # -------------------------------------------------------------
        # IF DISCONNECTED: Safe to scan because the client layer is disabled!
        # -------------------------------------------------------------
        if iwinfo phy0-ap0 scan 2>/dev/null | grep -q "ESSID: .$TARGET_SSID."; then
            HOTSPOT_FOUND=1
        else
            HOTSPOT_FOUND=0
        fi
    fi

    # -------------------------------------------------------------
    # STATE ENGINE EXECUTION
    # -------------------------------------------------------------
    if [ "$HOTSPOT_FOUND" -eq 1 ]; then
        missed_checks=0

        if [ "$IS_ENABLED" = "1" ] || [ -z "$IS_ENABLED" ]; then
            if [ "$logging" = "1" ]; then
                logger -t hotspot_monitor "Hotspot Monitor: Hotspot $TARGET_SSID Found! Binding $RADIO to $NET_INTF and enabling client interface $INTERFACE."
            fi

            uci set wireless.$INTERFACE.disabled='0'
            uci set wireless.$INTERFACE.network="$NET_INTF"
            uci commit wireless
            wifi reload $RADIO

            # Force netifd to clear the operational "Disabled" state
            # This kicks the network interface back into active scanning mode.
            /sbin/ifup $NET_INTF

            missed_checks=0
            current_sleep=$minimum_sleep
            sleep 45

        else
            if [ "$logging" = "1" ]; then
                logger -t hotspot_monitor "Hotspot Monitor: Hotspot $TARGET_SSID stable. Sleeping for $current_sleep seconds."
            fi
            sleep "$current_sleep"

            current_sleep=$((current_sleep + current_sleep))
            if [ "$current_sleep" -gt "$maximum_sleep" ]; then
                current_sleep=$maximum_sleep
            fi
        fi

    else
        missed_checks=$((missed_checks + 1))
        if [ "$missed_checks" -gt 10 ]; then
            missed_checks=10
        fi

        if [ "$missed_checks" -ge 2 ]; then
            if [ "$IS_ENABLED" = "0" ]; then
                if [ "$logging" = "1" ]; then
                    logger -t hotspot_monitor "Hotspot Monitor: Hotspot $TARGET_SSID dropped! Disabling interface $INTERFACE to keep $RADIO stable."
                fi
                uci set wireless.$INTERFACE.disabled='1'
                uci commit wireless
                wifi reload $RADIO

                current_sleep=$minimum_sleep
                sleep 20
            else
                if [ "$logging" = "1" ]; then
                    logger -t hotspot_monitor "Hotspot Monitor: $RADIO monitoring for SSID: $TARGET_SSID. Sleeping for $current_sleep seconds."

                fi
                sleep "$current_sleep"

                current_sleep=$((current_sleep + 1))
                if [ "$current_sleep" -gt "$maximum_sleep" ]; then
                    current_sleep=$maximum_sleep
                fi
            fi
        else
            sleep 20
        fi
    fi
done
) &

Then to start the script, either reboot the router, or issue the command:

sh /etc/rc.local

… in Services >> Terminal.

Note that I’m using metrics… the cellphone hotspot has a metric of 100, whereas the main ISP has a metric of 1. So if both the main ISP and the cellphone hotspot are connected, data preferentially flows to/from the main ISP.

If you want to make this survive firmware updates, in the LuCI interface, go to System >> Backup / Flash Firmware >> Configuration tab, and put /etc/rc.local in that list, then click the Save button.