eibgrad

ddwrt-bind-static-routes-to-wan.sh

Mar 11th, 2018 (edited)
2,049
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Bash 13.77 KB | None | 0 0
  1. #!/bin/sh
  2. #DEBUG= # comment/uncomment to disable/enable debug mode
  3.  
  4. #          name: ddwrt-bind-static-routes-to-wan.sh
  5. #       version: 3.2.0, 12-nov-2024, by eibgrad
  6. #       purpose: route specific ip(s)/domain(s) through wan
  7. #   script type: startup (autostart)
  8. #  installation:
  9. #    1. enable jffs2 (administration->jffs2)
  10. #    2. enable syslogd (services->services->system log)
  11. #    3. use shell (telnet/ssh) to execute one of the following commands:
  12. #         curl -kLs bit.ly/ddwrt-installer|tr -d '\r'|sh -s gnxtZuqg startup
  13. #       or
  14. #         wget -qO - bit.ly/ddwrt-installer|tr -d '\r'|sh -s gnxtZuqg startup
  15. #    4. modify options (minimally STATIC_ROUTES) using vi editor:
  16. #         vi /jffs/etc/config/ddwrt-bind-static-routes-to-wan.startup
  17. #    5. reboot
  18. #
  19. #  compatibility/limitations/restrictions:
  20. #    * due to a known bug (see https://svn.dd-wrt.com/ticket/7798), you will
  21. #      NOT be able to designate specific dns servers for name resolution
  22. #      unless you also install the entware version of nslookup, specifically
  23. #      the bind-nslookup package; otherwise, name resolution will *always*
  24. #      use the dns server(s) configured w/ the wan/isp, regardless how you
  25. #      choose to configure the DNS_SERVER option.
  26.  
  27. # ------------------------------ BEGIN OPTIONS ------------------------------- #
  28.  
  29. STATIC_ROUTES='
  30. #myhostname.duckdns.org # laptop
  31. #myhostname2.duckdns.org # smartphone
  32. #myhostname3.duckdns.org # tablet
  33. #171.190.59.0/24 # workplace (cidr notation only)
  34. #230.139.191.67 # vacation home
  35. #215.126.219.216 # local wifi cafe
  36. '
  37.  
  38. # time (in secs) between checks for domain name updates
  39. #   (caution: the lower the value, the greater the risk of banning)
  40. #   (lower values may benefit from dns randomization (see below))
  41. CHECK_INTERVAL=300
  42.  
  43. # optional: some servers update faster and/or more reliably than others
  44. DNS_SERVER='' # NOT specified (system default)
  45. #DNS_SERVER='localhost' # dnsmasq (local caching dns proxy)
  46. #DNS_SERVER='1.1.1.1' # cloudflare
  47. #DNS_SERVER='8.8.8.8' # google
  48. #DNS_SERVER='9.9.9.9' # quad9
  49. #DNS_SERVER='duckdns.org' # recommended when using duckdns ddns
  50. #DNS_SERVER='1.1.1.1 8.8.8.8 9.9.9.9' # multiple DNS servers
  51.  
  52. # uncomment/comment to enable/disable randomization of dns servers
  53. #   (always valid, but only useful w/ multiple dns servers)
  54. #   (caution: increasingly slows name resolution as you add more domain
  55. #    names (at least one (1) additional second per domain name))
  56. #SW_RANDOMIZE_DNS=
  57.  
  58. # uncomment/comment to retain/delete expired (stale) static routes
  59. #SW_RETAIN_EXPIRED_ROUTES=
  60.  
  61. # ------------------------------- END OPTIONS -------------------------------- #
  62.  
  63. # ---------------------- DO NOT CHANGE BELOW THIS LINE ----------------------- #
  64.  
  65. # set internal field separator to newline
  66. OIFS="$IFS"; IFS=$'\n'
  67.  
  68. # ------------------------------ BEGIN STARTUP ------------------------------- #
  69.  
  70. # function startup() # entry-point by system startup
  71. startup() {
  72. (
  73. [ ${DEBUG+x} ] && set -x
  74.  
  75. # base specification for all temporary files
  76. TFILE="/tmp/$(basename $0)"
  77.  
  78. # protection against possible reentrancy (esp. w/ x86 platform)
  79. LOCK="$TFILE.lock"
  80.  
  81. # must match alternate routing table used by built-in pbr
  82. TID='10'
  83.  
  84. # function dom_fname( domain )
  85. dom_fname() { echo "$TFILE.${1//./_}"; }
  86.  
  87. # function query_dns( domain [server] )
  88. query_dns() {
  89.     local nslookup
  90.  
  91.     # establish source for nslookup utility
  92.     #   (see https://svn.dd-wrt.com/ticket/7798)
  93.     [ -f /opt/bin/nslookup ] && \
  94.         nslookup='/opt/bin/nslookup' || nslookup="$(which nslookup)"
  95.  
  96.     # caution: may return multiple ivp4 addresses; caller is responsible
  97.     # for filtering results to suit needs
  98.     $nslookup $1 $2 2>/dev/null | awk '
  99.        /^Name:/,
  100.          0 {if (/^Addr.*/) {print $0}}
  101.        ' | awk '
  102.        match($0,/([0-9]{1,3}\.){3}[0-9]{1,3}/) {
  103.          print substr($0, RSTART, RLENGTH)
  104.        }'
  105. }
  106.  
  107. # function get_ip( domain )
  108. get_ip() {
  109.     local i ip_list dns_server="$(echo $DNS_SERVER)"
  110.  
  111.     # handle unspecified dns server
  112.     if [ ! "$dns_server" ]; then
  113.         # query may return multiple ipv4 addresses
  114.         ip_list="$(query_dns $1 | awk '{print $1}')"
  115.         [ "$ip_list" ] && { echo $ip_list; return 0; } || return 1
  116.     fi
  117.  
  118.     # optional: randomize multiple dns servers
  119.     if [ ${SW_RANDOMIZE_DNS+x} ]; then
  120.         sleep 1 # increases the effectiveness of randomization
  121.         dns_server="$(echo $dns_server | awk '
  122.            BEGIN {srand()}
  123.            {
  124.              for (i = 1; i <= NF; i++) {
  125.                r = int(rand() * NF) + 1
  126.                x = $r; $r = $i; $i = x
  127.              }
  128.              print
  129.            }')"
  130.     fi
  131.  
  132.     for i in ${dns_server// /$'\n'}; do
  133.         # query may return multiple ip addresses
  134.         ip_list="$(query_dns $1 $i | awk '{print $1}')"
  135.         [ "$ip_list" ] && { echo $ip_list; return 0; }
  136.     done
  137.  
  138.     return 1
  139. }
  140.  
  141. # function add_route( ip )
  142. add_route() {
  143.     if ! ip route | grep -q "^${1//./\\.} "; then
  144.         if ip route add $1 via $wan_gw; then
  145.             routing_change=
  146.             echo "info: route added (table main): $1"
  147.         fi
  148.     fi
  149.     if [ $(ip route show table $TID | wc -l) -gt 0 ]; then
  150.         if ! ip route show table $TID | grep -q "^${1//./\\.} "; then
  151.             if ip route add $1 via $wan_gw table $TID; then
  152.                 routing_change=
  153.                 echo "info: route added (table $TID): $1"
  154.             fi
  155.         fi
  156.     fi
  157. }
  158.  
  159. # function delete_route( ip )
  160. delete_route() {
  161.     if ip route | grep -q "^${1//./\\.} "; then
  162.         if ip route del $1 via $wan_gw; then
  163.             routing_change=
  164.             echo "info: route deleted (table main): $1"
  165.         fi
  166.     fi
  167.     if [ $(ip route show table $TID | wc -l) -gt 0 ]; then
  168.         if ip route show table $TID | grep -q "^${1//./\\.} "; then
  169.             if ip route del $1 via $wan_gw table $TID; then
  170.                 routing_change=
  171.                 echo "info: route deleted (table $TID): $1"
  172.             fi
  173.         fi
  174.     fi
  175. }
  176.  
  177. # reject additional instances
  178. mkdir $LOCK &>/dev/null || exit 0
  179.  
  180. # catch unexpected exit and cleanup
  181. trap "rmdir $LOCK; exit 0" SIGHUP SIGINT SIGTERM
  182.  
  183. # wait for wan availability
  184. until ping -qc1 -W3 8.8.8.8 &>/dev/null; do sleep 10; done
  185.  
  186. # quit if we fail to validate all static routes
  187. validate_static_routes || { echo 'info: exiting on fatal error(s)'; exit 1; }
  188.  
  189. # initialize domain tracking files
  190. #   record #1: prior results of name resolution
  191. #   record #2: current results of name resolution
  192. if [ ! ${SW_RETAIN_EXPIRED_ROUTES+x} ]; then
  193.     for ip_dom in $STATIC_ROUTES; do
  194.         # skip comments and blank lines
  195.         echo $ip_dom | grep -Eq '^[[:space:]]*(#|$)' && continue
  196.  
  197.         # isolate ip|domain (treat the rest as comments)
  198.         ip_dom="$(echo $ip_dom | awk '{print $1}')"
  199.  
  200.         # skip explicit ip address
  201.         ip_address $ip_dom && continue
  202.  
  203.         # assume failed prior name resolution
  204.         echo '255.255.255.255' > $(dom_fname $ip_dom)
  205.     done
  206. fi
  207.  
  208. # periodically update routing table(s)
  209. while :; do
  210.     wan_ip="$(nvram get wan_ipaddr)"
  211.     wan_gw="$(ip route | awk '/^default/{print $3}')"
  212.     static_routes_added=''
  213.     unset routing_change
  214.  
  215.     for ip_dom in $STATIC_ROUTES; do
  216.         # skip comments and blank lines
  217.         echo $ip_dom | grep -Eq '^[[:space:]]*(#|$)' && continue
  218.  
  219.         # isolate ip|domain (treat the rest as comments)
  220.         ip_dom="$(echo $ip_dom | awk '{print $1}')"
  221.  
  222.         # NOT a domain name; handle explicit ip address
  223.         if ip_address $ip_dom; then
  224.             add_route $ip_dom
  225.             if [ ! ${SW_RETAIN_EXPIRED_ROUTES+x} ]; then
  226.                 static_routes_added="$ip_dom $static_routes_added"
  227.             fi
  228.             continue
  229.         fi
  230.  
  231.         # skip duplicate domain names (when tracking)
  232.         if [ ! ${SW_RETAIN_EXPIRED_ROUTES+x} ]; then
  233.             [ $(cat $(dom_fname $ip_dom) | wc -l) -gt 1 ] && continue
  234.         fi
  235.  
  236.         # resolve domain name (may return multiple ipv4 addresses)
  237.         ip_list="$(get_ip $ip_dom)"
  238.  
  239.         # add each ip address to the routing table(s)
  240.         if [ "$ip_list" ]; then
  241.             for ip in ${ip_list// /$'\n'}; do
  242.                 # ignore self-references (unlikey, but just in case)
  243.                 [ "$ip" == "$wan_ip" ] && continue
  244.  
  245.                 add_route $ip
  246.                 if [ ! ${SW_RETAIN_EXPIRED_ROUTES+x} ]; then
  247.                     static_routes_added="$ip $static_routes_added"
  248.                 fi
  249.             done
  250.         else
  251.             echo "error: cannot resolve $ip_dom"
  252.         fi
  253.  
  254.         [ ${SW_RETAIN_EXPIRED_ROUTES+x} ] && continue
  255.  
  256.         if [ "$ip_list" ]; then
  257.             # sort ip addresses to facilitate subsequent comparisons
  258.             ip_list="$(echo $ip_list | xargs -n1 | \
  259.                sort -unt. -k1,1 -k2,2 -k3,3 -k4,4 | xargs)"
  260.             # save current name resolution
  261.             echo $ip_list >> $(dom_fname $ip_dom)
  262.         else
  263.             # save prior name resolution as current name resolution
  264.             head -n1 $(dom_fname $ip_dom) >> $(dom_fname $ip_dom)
  265.         fi
  266.     done
  267.  
  268.     # optional: delete expired/stale static routes
  269.     if [ ! ${SW_RETAIN_EXPIRED_ROUTES+x} ]; then
  270.         for ip_dom in $STATIC_ROUTES; do
  271.             # skip comments and blank lines
  272.             echo $ip_dom | grep -Eq '^[[:space:]]*(#|$)' && continue
  273.  
  274.             # isolate ip|domain (treat the rest as comments)
  275.             ip_dom="$(echo $ip_dom | awk '{print $1}')"
  276.  
  277.             # skip explicit ip address
  278.             ip_address $ip_dom && continue
  279.  
  280.             # obtain prior name resolution from tracking file
  281.             ip_list="$(head -n1 $(dom_fname $ip_dom))"
  282.  
  283.             # skip failed prior name resolution
  284.             [ "$ip_list" == '255.255.255.255' ] && continue
  285.  
  286.             # delete any static routes no longer in use
  287.             for ip in ${ip_list// /$'\n'}; do
  288.                 echo $static_routes_added | grep -Eq "${ip//./\\.}" \
  289.                     || delete_route $ip
  290.             done
  291.         done
  292.  
  293.         # make current name resolution prior name resolution for next pass
  294.         for ip_dom in $STATIC_ROUTES; do
  295.             # skip comments and blank lines
  296.             echo $ip_dom | grep -Eq '^[[:space:]]*(#|$)' && continue
  297.  
  298.             # isolate ip|domain (treat the rest as comments)
  299.             ip_dom="$(echo $ip_dom | awk '{print $1}')"
  300.  
  301.             # skip explicit ip address
  302.             ip_address $ip_dom && continue
  303.  
  304.             # skip duplicate domain names (when tracking)
  305.             [ $(cat $(dom_fname $ip_dom) | wc -l) -le 1 ] && continue
  306.  
  307.             # remove prior name resolution from tracking file
  308.             sed -i '1d' $(dom_fname $ip_dom)
  309.         done
  310.     fi
  311.  
  312.     # force routing system to recognize changes
  313.     [ ${routing_change+x} ] && ip route flush cache
  314.  
  315.     # wait awhile and repeat
  316.     sleep $CHECK_INTERVAL
  317. done
  318.  
  319. ) 2>&1 | logger -t "$(basename $0 | grep -Eo '^.{0,23}')[$$]" &
  320. } # end startup
  321.  
  322. # ------------------------------- END STARTUP -------------------------------- #
  323.  
  324. # function ip_address( string )
  325. ip_address() {
  326.     echo $1 | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}(|/[0-9]{1,2})$'
  327. }
  328.  
  329. # function validate_static_routes()
  330. validate_static_routes() {
  331.     local ip_dom err_found
  332.  
  333.     # function _ip_address( string )
  334.     _ip_address() {
  335.         local i subnet netmask
  336.  
  337.         # note: validation is NOT absolute, only best effort
  338.  
  339.         # verify subnet/netmask for basic syntax/format
  340.         ip_address $1 || return 1
  341.  
  342.         # isolate subnet from netmask (if any)
  343.         subnet="$(echo $1 | sed -r 's:^([^/]*).*$:\1:')"
  344.  
  345.         # verify each octet of subnet for valid range (0-255)
  346.         for i in ${subnet//./$'\n'}; do [ $i -gt 255 ] && return 1; done
  347.  
  348.         # isolate netmask (if any) from subnet
  349.         netmask="$(echo $1 | sed -rn 's:^.*/(.*)$:\1:p')"
  350.  
  351.         # verify netmask (if any) for valid range (0-32)
  352.         [ $netmask ] && [ $netmask -gt 32 ] && return 1
  353.  
  354.         return 0
  355.     }
  356.  
  357.     for ip_dom in $STATIC_ROUTES; do
  358.         # skip comments and blank lines
  359.         echo $ip_dom | grep -Eq '^[[:space:]]*(#|$)' && continue
  360.  
  361.         # isolate ip|domain (treat the rest as comments)
  362.         ip_dom="$(echo $ip_dom | awk '{print $1}')"
  363.  
  364.         # validate ip address
  365.         _ip_address $ip_dom && continue
  366.  
  367.         # validate domain name
  368.         echo $ip_dom | \
  369.             grep -Eq '^([a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]\.)+[a-zA-Z]{2,}$' \
  370.                 && continue
  371.  
  372.         echo "fatal error: invalid static route: $ip_dom"
  373.         err_found=
  374.     done
  375.  
  376.     [ ${err_found+x} ] && return 1 || return 0
  377. }
  378.  
  379. # function usage()
  380. usage() {
  381.     echo "Usage: $(basename $0) [option...]"
  382.     echo
  383.     echo '  Options (stackable)'
  384.     echo '    db1       enable debug mode'
  385.     echo '    db0       disable debug mode'
  386.     echo '    validate  validate static routes (syntactically only)'
  387.     echo '    version   version information'
  388.     echo '    help      this usage information'
  389.     echo
  390. }
  391.  
  392. # script was called by system startup
  393. true <> /dev/tty || { startup; exit 0; }
  394.  
  395. # script was called from command-line (i.e., interactively)
  396.  
  397. # handle no options
  398. [ "$1" ] || { usage; exit 0; }
  399.  
  400. # handle -h|--help option
  401. for opt; do case $opt in help) usage; exit 0;; esac; done
  402.  
  403. # handle command-line options (stackable)
  404. while [ $# -gt 0 ]; do
  405.     case "$1" in
  406.          db1) sed -ri '2 s/^#(DEBUG)/\1/' $0; echo 'info: debug mode enabled';;
  407.          db0) sed -ri '2 s/^(DEBUG)/#\1/' $0; echo 'info: debug mode disabled';;
  408.     validate) validate_static_routes && echo 'info: all static routes are valid';;
  409.      version) echo "info: $(awk -F: '/^#.*version:/{print $2; exit}' $0)";;
  410.            *) echo "error: unknown option: $1";;
  411.     esac
  412.     shift
  413. done
  414.  
  415. exit 0
Add Comment
Please, Sign In to add comment