Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/bin/sh
- #DEBUG= # comment/uncomment to disable/enable debug mode
- # name: ddwrt-bind-static-routes-to-wan.sh
- # version: 3.2.0, 12-nov-2024, by eibgrad
- # purpose: route specific ip(s)/domain(s) through wan
- # script type: startup (autostart)
- # installation:
- # 1. enable jffs2 (administration->jffs2)
- # 2. enable syslogd (services->services->system log)
- # 3. use shell (telnet/ssh) to execute one of the following commands:
- # curl -kLs bit.ly/ddwrt-installer|tr -d '\r'|sh -s gnxtZuqg startup
- # or
- # wget -qO - bit.ly/ddwrt-installer|tr -d '\r'|sh -s gnxtZuqg startup
- # 4. modify options (minimally STATIC_ROUTES) using vi editor:
- # vi /jffs/etc/config/ddwrt-bind-static-routes-to-wan.startup
- # 5. reboot
- #
- # compatibility/limitations/restrictions:
- # * due to a known bug (see https://svn.dd-wrt.com/ticket/7798), you will
- # NOT be able to designate specific dns servers for name resolution
- # unless you also install the entware version of nslookup, specifically
- # the bind-nslookup package; otherwise, name resolution will *always*
- # use the dns server(s) configured w/ the wan/isp, regardless how you
- # choose to configure the DNS_SERVER option.
- # ------------------------------ BEGIN OPTIONS ------------------------------- #
- STATIC_ROUTES='
- #myhostname.duckdns.org # laptop
- #myhostname2.duckdns.org # smartphone
- #myhostname3.duckdns.org # tablet
- #171.190.59.0/24 # workplace (cidr notation only)
- #230.139.191.67 # vacation home
- #215.126.219.216 # local wifi cafe
- '
- # time (in secs) between checks for domain name updates
- # (caution: the lower the value, the greater the risk of banning)
- # (lower values may benefit from dns randomization (see below))
- CHECK_INTERVAL=300
- # optional: some servers update faster and/or more reliably than others
- DNS_SERVER='' # NOT specified (system default)
- #DNS_SERVER='localhost' # dnsmasq (local caching dns proxy)
- #DNS_SERVER='1.1.1.1' # cloudflare
- #DNS_SERVER='8.8.8.8' # google
- #DNS_SERVER='9.9.9.9' # quad9
- #DNS_SERVER='duckdns.org' # recommended when using duckdns ddns
- #DNS_SERVER='1.1.1.1 8.8.8.8 9.9.9.9' # multiple DNS servers
- # uncomment/comment to enable/disable randomization of dns servers
- # (always valid, but only useful w/ multiple dns servers)
- # (caution: increasingly slows name resolution as you add more domain
- # names (at least one (1) additional second per domain name))
- #SW_RANDOMIZE_DNS=
- # uncomment/comment to retain/delete expired (stale) static routes
- #SW_RETAIN_EXPIRED_ROUTES=
- # ------------------------------- END OPTIONS -------------------------------- #
- # ---------------------- DO NOT CHANGE BELOW THIS LINE ----------------------- #
- # set internal field separator to newline
- OIFS="$IFS"; IFS=$'\n'
- # ------------------------------ BEGIN STARTUP ------------------------------- #
- # function startup() # entry-point by system startup
- startup() {
- (
- [ ${DEBUG+x} ] && set -x
- # base specification for all temporary files
- TFILE="/tmp/$(basename $0)"
- # protection against possible reentrancy (esp. w/ x86 platform)
- LOCK="$TFILE.lock"
- # must match alternate routing table used by built-in pbr
- TID='10'
- # function dom_fname( domain )
- dom_fname() { echo "$TFILE.${1//./_}"; }
- # function query_dns( domain [server] )
- query_dns() {
- local nslookup
- # establish source for nslookup utility
- # (see https://svn.dd-wrt.com/ticket/7798)
- [ -f /opt/bin/nslookup ] && \
- nslookup='/opt/bin/nslookup' || nslookup="$(which nslookup)"
- # caution: may return multiple ivp4 addresses; caller is responsible
- # for filtering results to suit needs
- $nslookup $1 $2 2>/dev/null | awk '
- /^Name:/,
- 0 {if (/^Addr.*/) {print $0}}
- ' | awk '
- match($0,/([0-9]{1,3}\.){3}[0-9]{1,3}/) {
- print substr($0, RSTART, RLENGTH)
- }'
- }
- # function get_ip( domain )
- get_ip() {
- local i ip_list dns_server="$(echo $DNS_SERVER)"
- # handle unspecified dns server
- if [ ! "$dns_server" ]; then
- # query may return multiple ipv4 addresses
- ip_list="$(query_dns $1 | awk '{print $1}')"
- [ "$ip_list" ] && { echo $ip_list; return 0; } || return 1
- fi
- # optional: randomize multiple dns servers
- if [ ${SW_RANDOMIZE_DNS+x} ]; then
- sleep 1 # increases the effectiveness of randomization
- dns_server="$(echo $dns_server | awk '
- BEGIN {srand()}
- {
- for (i = 1; i <= NF; i++) {
- r = int(rand() * NF) + 1
- x = $r; $r = $i; $i = x
- }
- print
- }')"
- fi
- for i in ${dns_server// /$'\n'}; do
- # query may return multiple ip addresses
- ip_list="$(query_dns $1 $i | awk '{print $1}')"
- [ "$ip_list" ] && { echo $ip_list; return 0; }
- done
- return 1
- }
- # function add_route( ip )
- add_route() {
- if ! ip route | grep -q "^${1//./\\.} "; then
- if ip route add $1 via $wan_gw; then
- routing_change=
- echo "info: route added (table main): $1"
- fi
- fi
- if [ $(ip route show table $TID | wc -l) -gt 0 ]; then
- if ! ip route show table $TID | grep -q "^${1//./\\.} "; then
- if ip route add $1 via $wan_gw table $TID; then
- routing_change=
- echo "info: route added (table $TID): $1"
- fi
- fi
- fi
- }
- # function delete_route( ip )
- delete_route() {
- if ip route | grep -q "^${1//./\\.} "; then
- if ip route del $1 via $wan_gw; then
- routing_change=
- echo "info: route deleted (table main): $1"
- fi
- fi
- if [ $(ip route show table $TID | wc -l) -gt 0 ]; then
- if ip route show table $TID | grep -q "^${1//./\\.} "; then
- if ip route del $1 via $wan_gw table $TID; then
- routing_change=
- echo "info: route deleted (table $TID): $1"
- fi
- fi
- fi
- }
- # reject additional instances
- mkdir $LOCK &>/dev/null || exit 0
- # catch unexpected exit and cleanup
- trap "rmdir $LOCK; exit 0" SIGHUP SIGINT SIGTERM
- # wait for wan availability
- until ping -qc1 -W3 8.8.8.8 &>/dev/null; do sleep 10; done
- # quit if we fail to validate all static routes
- validate_static_routes || { echo 'info: exiting on fatal error(s)'; exit 1; }
- # initialize domain tracking files
- # record #1: prior results of name resolution
- # record #2: current results of name resolution
- if [ ! ${SW_RETAIN_EXPIRED_ROUTES+x} ]; then
- for ip_dom in $STATIC_ROUTES; do
- # skip comments and blank lines
- echo $ip_dom | grep -Eq '^[[:space:]]*(#|$)' && continue
- # isolate ip|domain (treat the rest as comments)
- ip_dom="$(echo $ip_dom | awk '{print $1}')"
- # skip explicit ip address
- ip_address $ip_dom && continue
- # assume failed prior name resolution
- echo '255.255.255.255' > $(dom_fname $ip_dom)
- done
- fi
- # periodically update routing table(s)
- while :; do
- wan_ip="$(nvram get wan_ipaddr)"
- wan_gw="$(ip route | awk '/^default/{print $3}')"
- static_routes_added=''
- unset routing_change
- for ip_dom in $STATIC_ROUTES; do
- # skip comments and blank lines
- echo $ip_dom | grep -Eq '^[[:space:]]*(#|$)' && continue
- # isolate ip|domain (treat the rest as comments)
- ip_dom="$(echo $ip_dom | awk '{print $1}')"
- # NOT a domain name; handle explicit ip address
- if ip_address $ip_dom; then
- add_route $ip_dom
- if [ ! ${SW_RETAIN_EXPIRED_ROUTES+x} ]; then
- static_routes_added="$ip_dom $static_routes_added"
- fi
- continue
- fi
- # skip duplicate domain names (when tracking)
- if [ ! ${SW_RETAIN_EXPIRED_ROUTES+x} ]; then
- [ $(cat $(dom_fname $ip_dom) | wc -l) -gt 1 ] && continue
- fi
- # resolve domain name (may return multiple ipv4 addresses)
- ip_list="$(get_ip $ip_dom)"
- # add each ip address to the routing table(s)
- if [ "$ip_list" ]; then
- for ip in ${ip_list// /$'\n'}; do
- # ignore self-references (unlikey, but just in case)
- [ "$ip" == "$wan_ip" ] && continue
- add_route $ip
- if [ ! ${SW_RETAIN_EXPIRED_ROUTES+x} ]; then
- static_routes_added="$ip $static_routes_added"
- fi
- done
- else
- echo "error: cannot resolve $ip_dom"
- fi
- [ ${SW_RETAIN_EXPIRED_ROUTES+x} ] && continue
- if [ "$ip_list" ]; then
- # sort ip addresses to facilitate subsequent comparisons
- ip_list="$(echo $ip_list | xargs -n1 | \
- sort -unt. -k1,1 -k2,2 -k3,3 -k4,4 | xargs)"
- # save current name resolution
- echo $ip_list >> $(dom_fname $ip_dom)
- else
- # save prior name resolution as current name resolution
- head -n1 $(dom_fname $ip_dom) >> $(dom_fname $ip_dom)
- fi
- done
- # optional: delete expired/stale static routes
- if [ ! ${SW_RETAIN_EXPIRED_ROUTES+x} ]; then
- for ip_dom in $STATIC_ROUTES; do
- # skip comments and blank lines
- echo $ip_dom | grep -Eq '^[[:space:]]*(#|$)' && continue
- # isolate ip|domain (treat the rest as comments)
- ip_dom="$(echo $ip_dom | awk '{print $1}')"
- # skip explicit ip address
- ip_address $ip_dom && continue
- # obtain prior name resolution from tracking file
- ip_list="$(head -n1 $(dom_fname $ip_dom))"
- # skip failed prior name resolution
- [ "$ip_list" == '255.255.255.255' ] && continue
- # delete any static routes no longer in use
- for ip in ${ip_list// /$'\n'}; do
- echo $static_routes_added | grep -Eq "${ip//./\\.}" \
- || delete_route $ip
- done
- done
- # make current name resolution prior name resolution for next pass
- for ip_dom in $STATIC_ROUTES; do
- # skip comments and blank lines
- echo $ip_dom | grep -Eq '^[[:space:]]*(#|$)' && continue
- # isolate ip|domain (treat the rest as comments)
- ip_dom="$(echo $ip_dom | awk '{print $1}')"
- # skip explicit ip address
- ip_address $ip_dom && continue
- # skip duplicate domain names (when tracking)
- [ $(cat $(dom_fname $ip_dom) | wc -l) -le 1 ] && continue
- # remove prior name resolution from tracking file
- sed -i '1d' $(dom_fname $ip_dom)
- done
- fi
- # force routing system to recognize changes
- [ ${routing_change+x} ] && ip route flush cache
- # wait awhile and repeat
- sleep $CHECK_INTERVAL
- done
- ) 2>&1 | logger -t "$(basename $0 | grep -Eo '^.{0,23}')[$$]" &
- } # end startup
- # ------------------------------- END STARTUP -------------------------------- #
- # function ip_address( string )
- ip_address() {
- echo $1 | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}(|/[0-9]{1,2})$'
- }
- # function validate_static_routes()
- validate_static_routes() {
- local ip_dom err_found
- # function _ip_address( string )
- _ip_address() {
- local i subnet netmask
- # note: validation is NOT absolute, only best effort
- # verify subnet/netmask for basic syntax/format
- ip_address $1 || return 1
- # isolate subnet from netmask (if any)
- subnet="$(echo $1 | sed -r 's:^([^/]*).*$:\1:')"
- # verify each octet of subnet for valid range (0-255)
- for i in ${subnet//./$'\n'}; do [ $i -gt 255 ] && return 1; done
- # isolate netmask (if any) from subnet
- netmask="$(echo $1 | sed -rn 's:^.*/(.*)$:\1:p')"
- # verify netmask (if any) for valid range (0-32)
- [ $netmask ] && [ $netmask -gt 32 ] && return 1
- return 0
- }
- for ip_dom in $STATIC_ROUTES; do
- # skip comments and blank lines
- echo $ip_dom | grep -Eq '^[[:space:]]*(#|$)' && continue
- # isolate ip|domain (treat the rest as comments)
- ip_dom="$(echo $ip_dom | awk '{print $1}')"
- # validate ip address
- _ip_address $ip_dom && continue
- # validate domain name
- echo $ip_dom | \
- grep -Eq '^([a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]\.)+[a-zA-Z]{2,}$' \
- && continue
- echo "fatal error: invalid static route: $ip_dom"
- err_found=
- done
- [ ${err_found+x} ] && return 1 || return 0
- }
- # function usage()
- usage() {
- echo "Usage: $(basename $0) [option...]"
- echo
- echo ' Options (stackable)'
- echo ' db1 enable debug mode'
- echo ' db0 disable debug mode'
- echo ' validate validate static routes (syntactically only)'
- echo ' version version information'
- echo ' help this usage information'
- echo
- }
- # script was called by system startup
- true <> /dev/tty || { startup; exit 0; }
- # script was called from command-line (i.e., interactively)
- # handle no options
- [ "$1" ] || { usage; exit 0; }
- # handle -h|--help option
- for opt; do case $opt in help) usage; exit 0;; esac; done
- # handle command-line options (stackable)
- while [ $# -gt 0 ]; do
- case "$1" in
- db1) sed -ri '2 s/^#(DEBUG)/\1/' $0; echo 'info: debug mode enabled';;
- db0) sed -ri '2 s/^(DEBUG)/#\1/' $0; echo 'info: debug mode disabled';;
- validate) validate_static_routes && echo 'info: all static routes are valid';;
- version) echo "info: $(awk -F: '/^#.*version:/{print $2; exit}' $0)";;
- *) echo "error: unknown option: $1";;
- esac
- shift
- done
- exit 0
Add Comment
Please, Sign In to add comment