Script for updating IPv6 DNS and firewall rules with dynamic IPv6 prefixes

Recently, I updated my network to run with the latest OpenWRT release. My ISP supports DHCPv6-PD but it's a dynamic prefix with a very short lease time. Due to ISP network maintenance or failure the prefix would change multiple times over a year. So I decided to write a script to update my firewall rules and RA DNS server.

This script uses the set RA DNS server as reference for the old prefix. Therefore, the RA DNS server must be a local DNS server set with its global IPv6 address. I don't use ULA addresses because some Android device ignore advertised DNS servers with a ULA address, even recent newer models on recent Android versions.

Nothing worse than being unable to access my servers from the outside. Luckily I could always fall back to IPv4 to access my servers, but it still was annoying. When connecting using a domain, the IPv6 AAAA records with the old prefix was always preferred over the IPv4 A record. Aside from deploying this script, you also need some DDNS service or script running to update the IPv6 AAAA records on your domain names.

Here's the script. It can be configured for use on multiple LAN interfaces, and only works for LAN interfaces with a /64 prefix assigned. The how-to deploy is in the header comments.

#!/bin/sh

# update_prefix.sh
#
# This script is intended for use with dynamic IPv6 prefix delegation. It checks
# whether the prefix used on the specified interface has changed by comparing
# the prefix of the configured DNS server for RA. If the prefixes don't match
# it replaces all reference to the old prefix with the new prefix in the DHCP
# (RA DNS server) and firewall (all IPv6 rules) configurations.
#
# Usage: update_prefix.sh [-n] [-d <device>] [-i <interface>]
#
#        -n|--noreload     Do not reload firewall and odhcp services
#        -d|--dev name     Physical interface name (default: br-lan)
#        -i|--ifname name  Interface name in OpenWRT (default: lan)
#
# Warning: this script only works on interfaces with an assigned prefix length
# of /64. The configured DNS server for RA must be a local DNS server on the
# same interface using its global IPv6 address.
#
# For more information about this script visit:
# https://shibe.nl/2020/04/29/ziggo-openwrt-ipv6-prefix-delegation/
#
#
# Use cron to periodically execute the script. For multiple interfaces, link
# multiple commands in a single cron using "&&" and only reload the services on
# the last interface in line.
#
# Warning: when running as cron, it's possible that this scripts commits
# pending UCI changes made in the CLI.
#
# For example, run crontab and make this script run every 5 minutes:
# */5 * * * * /root/update-prefix.sh -d "br-lan" -i "lan" >/dev/null 2>&1
#
# For more information about cron jobs on OpenWRT visit:
# https://openwrt.org/docs/guide-user/base-system/cron


# Process arguments
reload=1
dev="br-lan"
ifname="lan"

while [[ $# -gt 0 ]]; do
    key="$1"

    case $key in
        -n|--noreload)
            reload=0
            shift
            ;;
        -d|--dev)
            dev="$2"
            shift
            shift
            ;;
        -i|--ifname)
            ifname="$2"
            shift
            shift
            ;;
        *)
            shift
            ;;
    esac
done

# Get currently used global IPv6 address
ip addr show dev "$dev" >/dev/null 2>&1

if [ $? -ne 0 ]; then
    echo "Specified device not found" 1>&2
    exit 1
fi

uga=$(ip addr show dev "$dev" | sed -e "s/^.*inet6 \([^ ]*\)\/.*$/\1/;t;d" | grep -v "^fd" | grep -v "^fe80" | head -1)

ip route get "$uga" >/dev/null 2>&1

if [ $? -ne 0 ]; then
    echo "No valid global IPv6 address found" 1>&2
    exit 1
fi

# Get configured DNS server
old_dns=$(uci get dhcp."$ifname".dns 2>/dev/null)

if [ $? -ne 0 ]; then
    echo "Specified interface not found" 1>&2
    exit 1
fi

# Compare the currently used global IPv6 address and DNS server prefixes
new_prefix=$(echo "$uga" | grep -o "^[^:]*:[^:]*:[^:]*:[^:]*")
old_prefix=$(echo "$old_dns" | grep -o "^[^:]*:[^:]*:[^:]*:[^:]*")

if [ "$new_prefix" == "$old_prefix" ]; then
    echo "No changes detected on interface ${ifname}"
    exit 0
fi

# Update the prefix if it has changed
new_dns="${old_dns/$old_prefix/$new_prefix}"

uci del_list dhcp."$ifname".dns="$old_dns"
uci add_list dhcp."$ifname".dns="$new_dns"

uci show firewall | grep "$old_prefix" | while read line; do
    eval "uci del_list ${line}"
    eval "uci add_list ${line/$old_prefix/$new_prefix}"
done

uci commit dhcp
uci commit firewall

logger -p notice -t update-prefix "prefix ${old_prefix}::/64 on ${ifname} updated to ${new_prefix}::/64"
echo "Prefix ${old_prefix}::/64 on ${ifname} updated to ${new_prefix}::/64"

# Reload the services
if [ $reload -eq 1 ]; then
    /etc/init.d/odhcpd reload >/dev/null 2>&1
    /etc/init.d/firewall reload >/dev/null 2>&1
else
    echo "The changes become effective after the firewall and odhcp services are reloaded"
fi

exit 0

As for changing the prefixes on the clients when a change occur, this should be done automatically by odhcpd as stated in the docs:

server: RD server for slave interfaces
a) automatic detection of prefixes, delegated prefix and default routes, MTU
b) automatic reannouncement when changes to prefixes or routes occur

Last tip, I was also very annoyed with the cron daemon making a notice in the system log everytime a cron gets executed. Luckily, this can be disabled by configuring the cronloglevel in /etc/config/system (see the system configuration docs). My scripts makes its own entry in the system log when a change has occured.

1 Like

Regarding firewall, you can use the '::1234/-64' address notation to overcome the frequently changing delegated prefixes.

3 Likes

Didn't know that, thanks! I do know that notation will not work for the announced DNS server.

If you must advertise a GUA, did you try to use an unallocated or deprecated, e.g. e000::1 and add it as an IP alias or loopback on the nameserver? A static route on the router will forward the packet to the ULA address of the nameserver.

That's actually quite clever. I thought of using a fake IPv6 global address or even using 2001:db8::/32 used for documentary purposes. I didn't want to use fake or unallocated addresses, and even db8 seems to be ignored by some Android device (like the Huawei P20). I definitely will take a look at deprecated address ranges. Do you have by any chance some references for these deprecated addresses?

I've encountered software and libraries that are really strict on which IPv6 addresses, following RFC to the letter. Fake addresses would work but but the 'perfectionist' in me prefers a cron above using fake addresses (while admitting the change of a fake address would cause any problems are almost non existent).

Straight from the source.