Post your sanitized post-cfg.sh

let’s create a “central” repo with different post-cfg.sh examples for Route10
I’ll start with mine, that adds CNAME records

#!/bin/sh

#Add CNAME records

for cname in ex1 ex2 ex3 ex4; do
uci add dhcp cname
uci set dhcp.@cname[-1].cname=“${cname}.local.lan”
uci set dhcp.@cname[-1].target=“test.local.lan”
done

#Commit changes and restart services

uci commit dhcp
/etc/init.d/dnsmasq restart

my alternate motive
im trying to figure out a way to assign the DoH settings from /etc/config/https-dns-proxy to individual vlans. example lan_2 to only use listen_port ‘5055’ (doh.opendns.com) and lan_3 to only use listen_port ‘5054’ (cloudflare-dns.com)
my experiments failed with

uci delete https-dns-proxy.@https-dns-proxy[0]
uci delete https-dns-proxy.@https-dns-proxy[1]

uci set dhcp.@dhcp[0].list.@dhcp_option[1]=“option:dns-server,127.0.0.1#5055”
uci set dhcp.@dhcp[1].list.@dhcp_option[1]=“option:dns-server,127.0.0.1#5054”

uci set dhcp.@lan_2.dns_service=‘1’
uci set dhcp.@lan_3.dns_service=‘1’

if anyone has ideas, im open to try them.

3 Likes

I’d also caution those willing to explore unless you’re absolutely confident with modifying your config. The last thing we want is to bombard Alta with follow up help requests.

3 Likes

I have mine configured as rc.local but roughly same thing. I have a couple things in hear, installing speedtest for testing directly on router and also policy based routing out a vpn, adding checks to make sure its up.

#!/bin/ash

# === Persistent Installation Path ===
SPEEDTEST_BIN="/usr/bin/speedtest"
SPEEDTEST_DIR="/cfg/speedtest"
SPEEDTEST_URL="https://install.speedtest.net/app/cli/ookla-speedtest-1.2.0-linux-$(uname -m).tgz"

# === Check if speedtest is already installed ===
if [ ! -f "$SPEEDTEST_BIN" ]; then
    echo "Speedtest not found, downloading..."
    mkdir -p "$SPEEDTEST_DIR"
    cd "$SPEEDTEST_DIR"
    wget -q "$SPEEDTEST_URL"
    tar -xvzf ookla-speedtest-*.tgz
    if [ -f "$SPEEDTEST_DIR/speedtest" ]; then
        mv "$SPEEDTEST_DIR/speedtest" "$SPEEDTEST_BIN"
        chmod +x "$SPEEDTEST_BIN"
        echo "Speedtest installed successfully."
    else
        echo "Error: Speedtest binary not found after extraction!"
    fi
else
    echo "Speedtest already installed."
fi

# === WireGuard Interface ===
if ! uci show network | grep -q "^network.wg0="; then
    uci set network.wg0='interface'
    uci set network.wg0.proto='wireguard'
    uci set network.wg0.private_key=’'
    uci set network.wg0.addresses=''
    uci set network.wg0.peerdns='0'
    uci add_list network.wg0.dns=''
fi

# === WireGuard Peer ===
if ! uci show network | grep -q "network.@wireguard_wg0.*.public_key=''"; then
    uci add network wireguard_wg0
    uci set network.@wireguard_wg0[-1].public_key=''
    uci set network.@wireguard_wg0[-1].allowed_ips='0.0.0.0/0'
    uci set network.@wireguard_wg0[-1].endpoint_host=''
    uci set network.@wireguard_wg0[-1].endpoint_port='51820'
    uci set network.@wireguard_wg0[-1].persistent_keepalive='25'
fi

uci commit network
/etc/init.d/network reload
ifup wg0

# === Firewall Zone for WireGuard ===
if ! uci show firewall | grep -q "firewall.wg0_zone"; then
    uci set firewall.wg0_zone="zone"
    uci set firewall.wg0_zone.name="wg0"
    uci set firewall.wg0_zone.network="wg0"
    uci set firewall.wg0_zone.input="ACCEPT"
    uci set firewall.wg0_zone.output="ACCEPT"
    uci set firewall.wg0_zone.forward="REJECT"
    uci set firewall.wg0_zone.masq="1"
    uci set firewall.wg0_zone.mtu_fix="1"
fi

if ! uci get firewall.@zone[2].network | grep -q 'wg0'; then
    uci add_list firewall.@zone[2].network='wg0'
fi

# === Custom Firewall Rule ===
if ! uci show firewall | grep -q ""; then
    uci add firewall rule
    uci set firewall.@rule[-1].name=''
    uci set firewall.@rule[-1].src=''
    uci set firewall.@rule[-1].src_ip=''
    uci set firewall.@rule[-1].dest='wan'
    uci set firewall.@rule[-1].proto='all'
    uci set firewall.@rule[-1].target='REJECT'
    uci set firewall.@rule[-1].enabled='1'
fi

uci commit firewall
/etc/init.d/firewall restart

# === Routing Table and Rules ===
grep -q "wgroute" /etc/iproute2/rt_tables || echo "200 wgroute" >> /etc/iproute2/rt_tables

ip route show table wgroute | grep -q "^default" || ip route add default dev wg0 table wgroute

ip rule | grep -q "from 10.14.66.0/24.*table wgroute" || ip rule add from 10.14.66.0/24 table wgroute priority 300
ip rule | grep -q "to 10.14.1.0/24.*lookup main" || ip rule add to 10.14.1.0/24 lookup main

# === VPN Route Checker Script ===
cat << 'EOF' > /root/vpn-route-check.sh
#!/bin/ash

INTERFACE="10.14.66.1"
EXPECTED_CITY="Stockholm"
VPN_IF="wg0"
ROUTING_TABLE="wgroute"
LOG_TAG="VPNRouteCheck"

CITY=$(curl --silent --interface "$INTERFACE" http://ipinfo.io/city)

if [ "$CITY" != "$EXPECTED_CITY" ]; then
    logger -t "$LOG_TAG" "❌ City mismatch: '$CITY'. Reapplying route through $VPN_IF."
    ip route flush table $ROUTING_TABLE
    ip route add default dev "$VPN_IF" table $ROUTING_TABLE
    ip rule add from 10.14.66.0/24 table $ROUTING_TABLE priority 300
    logger -t "$LOG_TAG" "✅ Route reapplied."
else
    logger -t "$LOG_TAG" "✅ Routing through expected city: '$CITY'."
fi
EOF

chmod +x /root/vpn-route-check.sh

# === Cron Job for VPN Check ===
grep -q "/root/vpn-route-check.sh" /etc/crontabs/root || echo "* * * * * /root/vpn-route-check.sh" >> /etc/crontabs/root
/etc/init.d/cron enable
/etc/init.d/cron start

# === Custom Restart on Firewall Reload ===
grep -q "/cfg/rc.local restart" /etc/init.d/firewall || sed -i '/reload_service()/a\        /cfg/rc.local restart' /etc/init.d/firewall

exit 0
~ # 

1 Like

I have a few things in mine separated out into subscripts.. My actual post_cfg.sh is pretty small.

root@route10:/cfg# cat post-cfg.sh

#!/bin/sh
cd $(dirname $0)
for f in post-cfg.d/*.sh; do
    . $f
done

root@route10:/cfg# ls post-cfg.d/
dnsmasq.sh enableNtp.sh setupfw.sh

I enable an extra persistent hosts file with:

root@route10:/cfg# cat post-cfg.d/dnsmasq.sh

#!/bin/sh

if [ "$(uci get dhcp.@dnsmasq[0].addnhosts)" != "/cfg/hosts" ]; then
    uci add_list dhcp.@dnsmasq[0].addnhosts='/cfg/hosts'
    uci commit
    /etc/init.d/dnsmasq reload
fi

I also enable an ntp server with:
root@route10:/cfg# cat post-cfg.d/enableNtp.sh

#!/bin/sh
if [ "$(uci get system.ntp.enable_server)" != "1" ]; then
    uci set system.ntp.enable_server="1"
    uci commit system
    /etc/init.d/sysntpd reload
fi

For firewall changes I disable all the ALGs (like SIP) and enable some emerging threat blocking (a bit simpler than suricata, but still cuts way down on on the intrusion attempts, and logs any attempts to contact a “bad” site from our network).

root@route10:/cfg# cat post-cfg.d/setupfw.sh

#!/bin/sh

ipset -exist create FH_N hash:net
ipset -exist create FH_A hash:ip
ipset -exist create LAN_FH_N hash:net
ipset -exist create LAN_FH_A hash:ip
if iptables -N LOG_DROP >&/dev/null; then
    iptables -A LOG_DROP -m limit --limit 5/min -j LOG --log-prefix "IPTables-Dropped: " --log-level 4
    iptables -A LOG_DROP -j DROP
fi
if !(iptables -L input_wan_rule | grep -q FH_N); then
    iptables -A input_wan_rule -m set --match-set FH_N src -j DROP
    iptables -A input_wan_rule -m set --match-set FH_A src -j DROP
fi
if !(iptables -L forwarding_wan_rule | grep -q FH_N); then
    iptables -A forwarding_wan_rule -m set --match-set FH_N src -j DROP
    iptables -A forwarding_wan_rule -m set --match-set FH_A src -j DROP
fi
if !(iptables -L forwarding_lan_rule | grep -q LAN_FH_N); then
    iptables -A forwarding_lan_rule -m set --match-set LAN_FH_N dst -j LOG_DROP
    iptables -A forwarding_lan_rule -m set --match-set LAN_FH_A dst -j LOG_DROP
fi

#uci set firewall.@defaults[0].helper='amanda ftp RAS Q.931 irc pptp snmp tftp'
uci set firewall.@defaults[0].helper=''
uci set firewall.@defaults[0].auto_helper='0'
uci commit
/etc/init.d/firewall reload

#update every 11 minutes
if !(crontab -l | grep -q blockThreats); then
    (crontab -l ; echo '*/11 * * * * python /cfg/scripts/blockThreats.py') |crontab -
fi

Here’s my emerging threat blocking script - if anyone’s interested.

root@route10:/cfg# cat scripts/blockThreats.py

#!/usr/bin/python3
import os
import urllib
import urllib.request
import time
import calendar
import subprocess

tempFolder = '/tmp/blockThreats/'
tempIpPath = tempFolder + 'ipData'
tempNetPath = tempFolder + 'netData'
expiryPath = tempFolder + 'expiryInfo'
tempTemp = tempFolder + 'temp'
alreadyFetched = set()
freshlyFetched = set()
privateBlocks = {'10.0.0.0/8\n', '192.168.0.0/16\n', '172.16.0.0/12\n', '169.254.0.0/16\n'}
expiryTimes = {}

def readExpiryInfo():
    """
    Reads expiry times and etags from a file and populates the expiryTimes dictionary.
    """
    if os.path.exists(expiryPath):
        with open(expiryPath, 'r') as f:
            for line in f:
                split = line.split(' ', 2)
                if len(split) == 3:
                    path = split[-1][0 : -1]
                    t = float(split[0])
                    etag = split[1]
                    expiryTimes[path] = (t, etag)

def writeExpiryInfo():
    """
    Writes the expiry times from the expiryTimes dictionary to a file.
    """
    with open(expiryPath, 'w') as f:
        for path, info in expiryTimes.items():
            f.write(str(info[0]) + ' ' + info[1] + ' ' + path + '\n')

def tempPathForURL(url):
    """
    Generates a temporary file path for a given URL.
    Creates necessary directories if they do not exist.
    
    Args:
        url (str): The URL to generate the path for.
    
    Returns:
        str: The generated temporary file path.
    """
    slashesPos = url.index('//')
    if slashesPos >= 0:
        result = tempFolder + url[slashesPos+2  : ]
    os.makedirs(result[0 : result.rindex('/')], exist_ok=True)
    return result

def timeFromResponseHeader(response, key):
    """
    Extracts and converts a time value from an HTTP response header.
    
    Args:
        response (http.client.HTTPResponse): The HTTP response object.
        key (str): The header key to extract the time from.
    
    Returns:
        float: The extracted time value in seconds since the epoch.
    """
    val = response.headers[key]
    return 0 if val is None else calendar.timegm(time.strptime(val, '%a, %d %b %Y %H:%M:%S %Z'))

def fetchBlockList(url):
    """
    Fetches a block list from a given URL and saves it to a temporary path.
    Uses cached data if it is still valid based on expiry times.
    
    Args:
        url (str): The URL to fetch the block list from.
    
    Returns:
        str: The path to the fetched block list.
    """
    #print(url + '\n')
    tempPath = tempPathForURL(url)
    expireInfo = expiryTimes.get(tempPath)
    if expireInfo is not None and time.time() < expireInfo[0]:
        return tempPath
    if url in alreadyFetched:
        return tempPath
    alreadyFetched.add(url)
    req = urllib.request.Request(url=url, headers={'User-Agent': 'Mozilla/5.0'})
    mtime = 0
    if os.path.exists(tempPath):
        mtime = os.path.getmtime(tempPath)
        req.add_header('If-Modified-Since', time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(mtime)))
    try:
        with urllib.request.urlopen(req) as response:
            etag = response.headers.get('etag')
            expires = timeFromResponseHeader(response, 'Expires')
            expiryTimes[tempPath] = (expires, etag or '')
            if etag:
                if expireInfo and etag == expireInfo[1]:
                    return tempPath
            else:
                lastModified = timeFromResponseHeader(response, 'Last-Modified')
                if lastModified != 0 and lastModified <= mtime:
                    return tempPath
            data = response.read()
            with open(tempPath, mode='wb', buffering=0) as f:
                f.write(data)
            freshlyFetched.add(tempPath)
    except urllib.error.URLError as e:
        #print(e.reason)
        pass
    return tempPath

def writeSetToRestoreFile(s, f):
    for line in s:
        f.write('add bltmp ' + line)

def prepareRestores(isWan, blocklistPaths):
    """
    Prepares IP and network blocklists for restoration.

    This function reads IP addresses and network blocks from the provided blocklist paths,
    filters them based on whether they are WAN or private blocks, and writes them to temporary
    files for restoration.

    Args:
        isWan (bool): A flag indicating whether the blocks are for WAN (True) or not (False).
        blocklistPaths (list of str): A list of file paths containing the blocklists.

    Notes:
        - IP addresses are written to a temporary file specified by `tempIpPath`.
        - Network blocks are written to a temporary file specified by `tempNetPath`.
    """
    addresses = set()
    nets = set()
    for p in blocklistPaths:
        if os.path.exists(p):
            with open(p, 'r') as inp:
                for line in inp:
                    if line[0].isdigit():
                        if line.find('/') >= 0:
                            if isWan or line not in privateBlocks:
                                nets.add(line)
                        else:
                            addresses.add(line)
    with open(tempIpPath, 'w') as f:
        f.write('create bltmp hash:ip family inet hashsize 8192 maxelem 65536\n')
        writeSetToRestoreFile(addresses, f)
    with open(tempNetPath, 'w') as f:
        f.write('create bltmp hash:net family inet hashsize 2048 maxelem 65536\n')
        writeSetToRestoreFile(nets, f)

def doRestore(restorePath, ipsetName):
    with open(restorePath, 'r') as inp:
        subprocess.call(["/usr/sbin/ipset", "-!",  "restore"], stdin=inp)
    subprocess.call(["/usr/sbin/ipset", "swap",  "bltmp", ipsetName])
    subprocess.call(["/usr/sbin/ipset", "destroy",  "bltmp"])
    os.unlink(restorePath)

def anyUpdated(tempPaths):
    for p in tempPaths:
        if p in freshlyFetched:
            return True
    return False

def isEmptyIpset(setName):
    with open(tempTemp, 'w') as f:
        subprocess.call(["/usr/sbin/ipset", "list",  "--terse", setName], stdout=f)
    #result = 0
    with open(tempTemp, 'r') as f:
        info = f.read()
        result = info[-3 : -1] == ' 0'
    os.unlink(tempTemp)
    return result


def doBlockThreats(fhN, fhA, isWan, blockListURLs):
    subprocess.call(["/usr/sbin/ipset", "-exist",  "create", fhN, "hash:net"])
    subprocess.call(["/usr/sbin/ipset", "-exist",  "create", fhA, "hash:ip"])
    readExpiryInfo()
    tempPaths = [fetchBlockList(url) for url in blockListURLs]
    if anyUpdated(tempPaths) or isEmptyIpset(fhN) or isEmptyIpset(fhA):
        writeExpiryInfo()
        prepareRestores(isWan, tempPaths)
        doRestore(tempIpPath, fhA)
        doRestore(tempNetPath, fhN)

def doBlockThreatsUsingFirehol(fhN, fhA, isWan, blockLists):
    doBlockThreats(fhN, fhA, isWan, [('https://iplists.firehol.org/files/' + bl + '.netset') for bl in blockLists])

doBlockThreatsUsingFirehol("FH_N", "FH_A", True, [ 'firehol_level1', 'firehol_level2', 'firehol_webserver', 'firehol_webclient'])
doBlockThreatsUsingFirehol("LAN_FH_N", "LAN_FH_A", False, [ 'firehol_level1', 'firehol_level2', 'firehol_webclient'])
2 Likes