What's new

Simple script for DNS fallback

  • SNBForums Code of Conduct

    SNBForums is a community for everyone, no matter what their level of experience.

    Please be tolerant and patient of others, especially newcomers. We are all here to share and learn!

    The rules are simple: Be patient, be nice, be helpful or be gone!

cyruz

Occasional Visitor
Hello guys, I just want to share this small script I made to set a DNS fallback when using a resolver inside your network, different from the router itself (AdGuard, PiHole or whatever).

The main target of this setup is to be able to monitor the queries from the network devices on our main resolver (no proxying from the router) and fall back to the router internal resolver in case of unavailability.

The idea is to poll the ARP table to check for the reachability of the resolver and, if not reachable, assign the resolver IP to a sub-interface of the main interface of the router (br0:1) and add a piece of configuration to dnsmasq. The only issue I found so far, is the client-side ARP table update timing. From my test on a MacOS Sonoma, I had variable results, with the arp table updated in around ~1 minute. In the worst case a reconnection should fix everything.

The setup:
  1. Set the router DHCP server to release only the DNS resolver IP and disable "Advertise router's IP in addition to user-specified DNS".

  2. Due to the router being the fallback, set the WAN DNS to an external DNS of your liking.

The script:
  1. Write the following script to /jffs/scripts/dns-fallback, adjust the RESOLVER_MAC and RESOLVER_IP variables (and any other variable, if you need) and make it executable with chmod 755 /jffs/scripts/dns-fallback

    Bash:
    #!/bin/sh
    # dns-fallback
    # ------------
    # Poll for the availability of the DNS resolver through the ARP table,
    # if not available, assign the resolver IP to a router subinterface and restart dnsmasq,
    # if available, revert everything and let the DNS resolver do its job.
    # * The script must be executable and its path should be "/jffs/scripts/dns-fallback".
    # * The variables should be adjusted to your liking.
    # * To run this script with cron (recommended) add the following line to "/jffs/scripts/services-start":
    #     [ -x /jffs/scripts/dns-fallback ] && cru a dns-fallback "* * * * * /jffs/scripts/dns-fallback > /dev/null"
    #   Adjust the '* * * * *' part to your liking. Suggested values are '*/5 * * * *' or '*/10 * * * *'.
    # * To run this script continuosly, un-comment the while loop and the sleep and
    #   add the following line to "/jffs/script/services-start":
    #     [ -x /jffs/scripts/dns-fallback ] && /jffs/scripts/dns-fallback > /dev/null &
    # * Logging enabled by default, empty "LOG_FILE" variable to disable it.
    # * Email notification disabled by default, set "MAIL_NOTIFY" variable to 1 to enable it. Tested with "Brevo".
    # ------------
    
    # Variables
    # ---------
    RESOLVER_MAC=02:42:c0:a8:01:64
    RESOLVER_IP=192.168.1.100
    NETMASK=255.255.255.0
    INTERFACE=br0
    SUB_IF=:1
    POLLING_TIME=60s
    CONF_FILE=/jffs/configs/dnsmasq.conf.add
    LOCK_FILE=/tmp/dns-fallback.lock
    LOG_FILE=/tmp/dns-fallback.log
    LOG_MAX_SIZE=1024000
    MAIL_NOTIFY=0
    MAIL_SMTP=smtp-relay.brevo.com:587
    MAIL_USER=someuser@icloud.com
    MAIL_PASSWORD="mypassword"
    MAIL_FROM=someuser@icloud.com
    MAIL_NAME="ASUS GT-AX11000"
    MAIL_NAME_FROM="asus-gt-ax11000@local.lan"
    MAIL_TO=someuser@icloud.com
    MAIL_SUBJECT="DNS fallback notifier"
    # ---------
    
    mail() {
        [ "${MAIL_NOTIFY}" -eq "1" ] && sendmail -S"${MAIL_SMTP}" -f"${MAIL_FROM}" ${MAIL_TO} -au"${MAIL_USER}" -ap"${MAIL_PASSWORD}" << EOM
    Subject: ${MAIL_SUBJECT}
    From: \"${MAIL_NAME}\" <${MAIL_NAME_FROM}>
    Date: `date -R`
    
    $@
    EOM
    }
    
    log() {
        [ -n "${LOG_FILE}" ] && echo "[`date '+%F %H:%M:%S'`] ${SCRIPT} :: $@" | tee -a ${LOG_FILE}
    }
    
    # Quit if the script is already running.
    SCRIPT=`basename $0`
    PID=`pidof ${SCRIPT}`
    [ "`echo ${PID} | wc -w`" -gt "1" ] && log "script already running!" && exit
    trap '' HUP INT QUIT ABRT TERM TSTP
    touch ${LOCK_FILE}
    
    # Delete the log file if the size is greater than max.
    [ "`wc -c ${LOG_FILE} | tr -dc '0-9'`" -gt "${LOG_MAX_SIZE}" ] && rm -f ${LOG_FILE}
    
    SED_PATTERN="^(no-dhcp-)?interface=${INTERFACE}${SUB_IF}"
    #while true; do
        (
            flock -x 200
            ping -I ${INTERFACE} -c 1 ${RESOLVER_IP} > /dev/null && sleep 1s
            if arp -D -i ${INTERFACE} | grep -i ${RESOLVER_MAC} > /dev/null; then
                log "resolver found"
                if grep -E "${SED_PATTERN}" ${CONF_FILE} > /dev/null 2>&1; then
                    log "<${CONF_FILE}> present - disabling router dnsmasq on ${INTERFACE}${SUB_IF}"
                    ifconfig ${INTERFACE}${SUB_IF} down && log "interface ${INTERFACE}${SUB_IF} disabled"
                    sed -i -E "/${SED_PATTERN}/d" ${CONF_FILE} && log "dnsmasq configuration addition removed from <${CONF_FILE}>"
                    [ -s ${CONF_FILE} ] || rm -f ${CONF_FILE} && log "<${CONF_FILE}> is empty - removed"
                    sleep 1s && service restart_dnsmasq && log "dnsmasq service restarted"
                    mail "Resolver found, deconfigured router dsnmasq resolver."
                fi
            else
                log "resolver not found - fallback to router dnsmasq"
                if ! grep -E "${SED_PATTERN}" ${CONF_FILE} > /dev/null 2>&1; then
                    log "dnsmasq configuration addition not present - appending it to <${CONF_FILE}>"
                    echo "interface=${INTERFACE}${SUB_IF}" >> ${CONF_FILE}
                    echo "no-dhcp-interface=${INTERFACE}${SUB_IF}" >> ${CONF_FILE}
                    ifconfig ${INTERFACE}${SUB_IF} ${RESOLVER_IP} netmask ${NETMASK} up && log "interface ${INTERFACE}${SUB_IF} configured with IP: ${RESOLVER_IP} and NETMA
                    sleep 1s && service restart_dnsmasq && log "dnsmasq service restarted"
                    mail "Resolver not found, configured router dnsmasq resolver."
                fi
            fi
            log "###"
        ) 200>"${LOCK_FILE}"
    #    sleep ${POLLING_TIME}
    #done

  2. Add the following line to /jffs/scripts/services-start:

    Bash:
    [ -x /jffs/scripts/dns-fallback ] && cru a dns-fallback "* * * * * /jffs/scripts/dns-fallback > /dev/null"

    Adjust * * * * * (every minute) to your liking. Suggested values are */5 * * * * (every 5 minutes) or */10 * * * * (every 10 minutes).

It's basically a hack and as such, I'm aware it can break anytime. If you have any suggestion, please feel free to share it.

---
EDIT: 20.01.2024 - rev 0.2 - implemented dave14305 suggestions
EDIT: 25.01.2024 - rev 0.3 - implemented Martinski suggestions
EDIT: 27.01.2024 - rev 0.4 - run through cru
EDIT: 27.01.2024 - rev 0.5 - implemented SomeWhereOverTheRainBow suggestions
EDIT: 27.01.2024 - rev 0.6 - added log limit management & modified script test presence with -x
EDIT: 16.02.2024 - rev 0.7 - added email notifications
---
 
Last edited:
Hello dave, thanks for the feedback.

1. The dnsmasq.conf changes are necessary otherwise dnsmasq will not listen on the newly configured interface.
2. I updated the code with sed!

I changed the name to dns-fallback, just for consistency.
 
..
If you have any suggestion, please feel free to share it.
Just a couple of suggestions:

1) The "-P" parameter used with the grep commands is *not* valid for the F/W built-in grep tool.

The "-P" option is available in the GNU grep version so it works if you have Entware installed, your PATH environment variable is set up to include the Entware paths *before* the F/W built-in paths, *and* you have explicitly installed the "grep" Entware package. Since undoubtedly there would be users for whom those 3 conditions will not all be true, I'd recommend making your script more portable for ASUS routers by replacing the "-P" with the "-E" option (which is valid for the built-in grep).

For example:
Bash:
grep -E "$SED_PATTERN" $CONF_FILE

2) An improvement when defining the SED_PATTERN variable:
Bash:
SED_PATTERN="^(no-dhcp-)?interface=${INTERFACE}${SUB_IF}"

My 2 cents.
 
Thanks Martinski, I didn't know about Entware, I'm fairly new on Merlin. It probably got installed as a dependency.

I'll implement your suggestion as soon as possible.
 
I noticed that for some reasons the script sometimes crashes. I can't find it running...
For this reason, I changed it to be run through cru.
 
I noticed that for some reasons the script sometimes crashes. I can't find it running...
For this reason, I changed it to be run through cru.
I see you have discovered that sometimes the script gets killed. This could happen for various reason's. There is nothing wrong with choosing "cru", but when using "cru" you want to make sure you are just doing single run through checks. Otherwise you may wind up with multiple running concurrent processes. I see you commented out the "while true; do" parts of the script. That should ensure that each cru run is a one time run through. I think * * * * * may be abit over kill, so much so that you could accidently create a nasty condition where dnsmasq gets restarted way more than you intended. Maybe add a lock file, or use flock to prevent nasty conditions from happening. You could probably get away with doing a check every 5 or 10 or even 15 minutes which will also have the same added benefit because each check will be nicely spaced out. But, you do have some fail safes such as exit on PID check greater than 1.

This is a unique little script. Please come back and share use cases. Maybe throw some debug/logging code in there that generates a record when certain parts of the script actually have to run. Run the script for a little while. Then report back here to show when your router has actually had to use it.

Here is some modifications I have added for you to try out.

Bash:
#!/bin/sh
# dns-fallback
# ------------
# Poll for the availability of the DNS resolver through the ARP table,
# if not available, assign the resolver IP to a router subinterface and restart dnsmasq,
# if available, revert everything and let the DNS resolver do its job.
# * The script must be executable and its path should be "/jffs/scripts/dns-fallback".
# * The variables should be adjusted to your liking.
# * To run this script with cron (recommended) add the following line to "/jffs/scripts/services-start":
#     [ -f /jffs/scripts/dns-fallback ] && cru a dns-fallback "* * * * * /jffs/scripts/dns-fallback > /dev/null"
# * To run this script continuosly, un-comment the while loop and the sleep and
#   add the following line to "/jffs/script/services-start":
#     [ -f /jffs/scripts/dns-fallback ] && /jffs/scripts/dns-fallback > /dev/null &
# ------------

# Variables
# ---------
RESOLVER_MAC=02:42:c0:a8:01:64
RESOLVER_IP=192.168.1.100
NETMASK=255.255.255.0
INTERFACE=br0
SUB_IF=:1
LOCK_FILE=/tmp/dns-fallback.lock
CONF_FILE=/jffs/configs/dnsmasq.conf.add
# ---------

# Quit if the script is already running.
SCRIPT=`basename $0`
PID=`pidof $SCRIPT`
[ "`echo $PID | wc -w`" -gt "1" ] && exit
trap '' HUP INT QUIT ABRT TERM TSTP
touch $LOCK_FILE

SED_PATTERN="^(no-dhcp-)?interface=${INTERFACE}${SUB_IF}"
#while true; do
    (
        flock -x 200
        ping -I ${INTERFACE} -c 1 ${RESOLVER_IP} > /dev/null
        if arp -D -i ${INTERFACE} | grep -i ${RESOLVER_MAC} > /dev/null; then
            if grep -E "${SED_PATTERN}" ${CONF_FILE} > /dev/null 2>&1; then
                ifconfig ${INTERFACE}${SUB_IF} down
                sed -i -E "/${SED_PATTERN}/d" ${CONF_FILE}
                [ -s ${CONF_FILE} ] || rm -f ${CONF_FILE}
                sleep 1s && service restart_dnsmasq
            fi
        else
            if ! grep -E "${SED_PATTERN}" ${CONF_FILE} > /dev/null 2>&1; then
                echo "interface=${INTERFACE}${SUB_IF}" >> ${CONF_FILE}
                echo "no-dhcp-interface=${INTERFACE}${SUB_IF}" >> ${CONF_FILE}
                ifconfig ${INTERFACE}${SUB_IF} ${RESOLVER_IP} netmask ${NETMASK} up
                sleep 1s && service restart_dnsmasq
            fi
        fi
    ) 200>"${LOCK_FILE}"
#    sleep ${POLLING_TIME}
#done
 
Last edited:

Sign Up For SNBForums Daily Digest

Get an update of what's new every day delivered to your mailbox. Sign up here!
Top