eibgrad

ddwrt-ovpn-split-advanced.sh

Mar 21st, 2017 (edited)
2,717
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. #!/bin/sh
  2. DEBUG=; set -x # uncomment/comment to enable/disable debug mode
  3.  
  4. #          name: ddwrt-ovpn-split-advanced.sh
  5. #       version: 2.1.1, 18-sep-2020, by eibgrad
  6. #       purpose: redirect specific traffic over the WAN|VPN
  7. #   script type: openvpn route-up/route-pre-down
  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 -- --dir /jffs nC27ETsp
  13. #       or
  14. #         wget -qO - bit.ly/ddwrt-installer|tr -d '\r'|sh -s -- --dir /jffs nC27ETsp
  15. #    4. use vi editor to modify script w/ your rules:
  16. #         vi /jffs/ddwrt-ovpn-split-advanced.sh
  17. #    5. create symbolic links:
  18. #         ln -sf /jffs/ddwrt-ovpn-split-advanced.sh /jffs/route-up
  19. #         ln -sf /jffs/ddwrt-ovpn-split-advanced.sh /jffs/route-down
  20. #    6. add the following to openvpn client additional config:
  21. #         route-up /jffs/route-up
  22. #         route-pre-down /jffs/route-down
  23. #    7. optional: by default, the default gateway is changed to the VPN,
  24. #       so the rules reroute over the WAN; to set/lockdown the default
  25. #       gateway to the WAN and have the rules reroute to the VPN, add the
  26. #       following directives to openvpn client additional config:
  27. #         pull-filter ignore "redirect-gateway"
  28. #         redirect-private def1
  29. #    8. optional: add ipset directive(s) w/ your domains to dnsmasq custom
  30. #       configuration (last field of directive must be ovpn_split):
  31. #         ipset=/ipchicken.com/netflix.com/ovpn_split
  32. #         ipset=/google.com/cnet.com/gov/ovpn_split
  33. #    9. disable/clear policy based routing (services->vpn->openvpn client)
  34. #   10. disable nat loopback (security->firewall, "filter wan nat redirection"
  35. #       must be checked)
  36. #   11. disable qos (nat/qos->qos)
  37. #   12. reboot
  38. #   limitations:
  39. #     - this script is only supported by dd-wrt v43904 (7/23/20) or later
  40. #     - this script is NOT compatible w/ dd-wrt policy based routing
  41. #     - this script is NOT compatible w/ dd-wrt nat loopback
  42. #     - this script is NOT compatible w/ dd-wrt qos
  43. #     - rules do NOT support domain names (e.g., google.com)
  44. (
  45. add_rules() {
  46. # ----------------------------------- FYI ------------------------------------ #
  47. # * the order of rules doesn't matter (there is no order of precedence)
  48. # * if any rule matches, those packets bypass the current default gateway
  49. # * remote access is already enabled; no additional rules are necessary
  50. # ---------------------------------------------------------------------------- #
  51.  
  52. # ------------------------------- BEGIN RULES -------------------------------- #
  53. #add_rule -s 192.168.1.10
  54. #add_rule -p tcp -s 192.168.1.112 --dport 80
  55. #add_rule -p tcp -s 192.168.1.122 --dport 3000:3100
  56. #add_rule -i br1 # guest network
  57. #add_rule -i br2 # iot network
  58. # -------------------------------- END RULES --------------------------------- #
  59. :;}
  60. # ---------------------- DO NOT CHANGE BELOW THIS LINE ----------------------- #
  61.  
  62. ENV_VARS='/tmp/env_vars'
  63. RPF_VARS='/tmp/rpf_vars'
  64.  
  65. # make environment variables persistent across openvpn events
  66. [ "$script_type" == 'route-up' ] && env > $ENV_VARS
  67.  
  68. # utility function for retrieving environment variable values
  69. env_get() { echo $(grep -Em1 "^$1=" $ENV_VARS | cut -d = -f2); }
  70.  
  71. IMPORT_RULES_FILESPEC="$(dirname $0)/*.rules"
  72. IMPORT_IPSET_FILESPEC="$(dirname $0)/*.ipset"
  73.  
  74. TID='200'
  75.  
  76. WAN_GW="$(env_get route_net_gateway)"
  77. WAN_IF="$(ip route | awk '/^default/{print $NF}')"
  78. VPN_GW="$(env_get route_vpn_gateway)"
  79. VPN_IF="$(env_get dev)"
  80.  
  81. FW_CHAIN='ovpn_split'
  82. FW_MARK=1
  83.  
  84. IPSET_HOST='ovpn_split' # must match ipset directive in dnsmasq
  85. IPSET_NET='ovpn_split_net'
  86.  
  87. IPT_MAN='iptables -t mangle'
  88. IPT_MARK_MATCHED="-j MARK --set-mark $FW_MARK"
  89. IPT_MARK_NOMATCH="-j MARK --set-mark $((FW_MARK + 1))"
  90.  
  91. add_rule() {
  92.     # precede addition w/ deletion to avoid dupes
  93.     $IPT_MAN -D $FW_CHAIN "$@" $IPT_MARK_MATCHED 2> /dev/null
  94.     $IPT_MAN -A $FW_CHAIN "$@" $IPT_MARK_MATCHED
  95. }
  96.  
  97. verify_prerequisites() {
  98.     local err_found=false
  99.  
  100.     # dd-wrt policy based routing cannot be active (ip rule conflict)
  101.     if [ "$(nvram get openvpncl_route)" ]; then
  102.         echo 'fatal error: dd-wrt policy based routing is currently active'
  103.         err_found=true
  104.     fi
  105.  
  106.     # nat loopback must be disabled (packet marking conflict)
  107.     if [ "$(nvram get block_loopback)" == '0' ]; then
  108.         echo 'fatal error: nat loopback must be disabled'
  109.         err_found=true
  110.     fi
  111.  
  112.     # qos must be disabled (packet marking conflict)
  113.     if [ "$(nvram get wshaper_enable)" == '1' ]; then
  114.         echo 'fatal error: qos must be disabled'
  115.         err_found=true
  116.     fi
  117.  
  118.     [[ $err_found == false ]] && return 0 || return 1
  119. }
  120.  
  121. import_hosts_and_networks() {
  122.     # import file naming format:
  123.     #   *.ipset
  124.     # example import files:
  125.     #   /jffs/some_hosts.ipset
  126.     #   /jffs/some_networks.ipset
  127.     #   /jffs/some_hosts_and_networks.ipset
  128.     # import file format (one per line):
  129.     #   ip | network(cidr)
  130.     # example import file contents:
  131.     #   122.122.122.122
  132.     #   212.212.212.0/24
  133.  
  134.     local MASK_COMMENT='^[[:space:]]*(#|$)'
  135.     local MASK_HOST='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
  136.     local MASK_HOST_32='^([0-9]{1,3}\.){3}[0-9]{1,3}/32$'
  137.     local MASK_NET='^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$'
  138.     local ERR_MSG="/tmp/tmp.$$.err_msg"
  139.  
  140.     local files file line
  141.  
  142.     # ipset( set host|network )
  143.     _ipset_add() {
  144.         if ipset -A $1 $2 2> $ERR_MSG; then
  145.             return
  146.         elif grep -Eq 'already (added|in set)' $ERR_MSG; then
  147.             echo "info: duplicate host|network; ignored: $2"
  148.         else
  149.             cat $ERR_MSG
  150.             echo "error: cannot add host|network: $2"
  151.         fi
  152.     }
  153.  
  154.     # _add_hosts_and_networks( file )
  155.     _add_hosts_and_networks() {
  156.         while read line; do
  157.             # skip comments and blank lines
  158.             echo $line | grep -Eq $MASK_COMMENT && continue
  159.  
  160.             # isolate host|network (the rest is treated as comments)
  161.             line="$(echo $line | awk '{print $1}')"
  162.  
  163.             # line may contain host/network; add to appropriate ipset hash table
  164.             if echo $line | grep -Eq $MASK_HOST; then
  165.                 _ipset_add $IPSET_HOST $line
  166.             elif echo $line | grep -Eq $MASK_HOST_32; then
  167.                 _ipset_add $IPSET_HOST $(echo $line | sed 's:/32::')
  168.             elif echo $line | grep -Eq $MASK_NET; then
  169.                 _ipset_add $IPSET_NET $line
  170.             else
  171.                 echo "error: unknown host|network: $line"
  172.             fi
  173.  
  174.         done < "$1"
  175.     }
  176.  
  177.     files="$(echo $IMPORT_IPSET_FILESPEC)"
  178.     if [ "$files" != "$IMPORT_IPSET_FILESPEC" ]; then
  179.         # add hosts and networks from import file(s) (if any)
  180.         for file in $files; do
  181.             _add_hosts_and_networks "$file"
  182.         done
  183.     fi
  184.  
  185.     # cleanup
  186.     rm -f $ERR_MSG
  187. }
  188.  
  189. up() {
  190.     # call dd-wrt route-up script
  191.     /tmp/openvpncl/route-up.sh 2> /dev/null
  192.  
  193.     # add chain for user-defined rules
  194.     $IPT_MAN -N $FW_CHAIN
  195.     $IPT_MAN -A PREROUTING -j $FW_CHAIN
  196.  
  197.     # initialize chain for user-defined rules
  198.     $IPT_MAN -A $FW_CHAIN -j CONNMARK --restore-mark
  199.     $IPT_MAN -A $FW_CHAIN -m mark ! --mark 0 -j RETURN
  200.  
  201.     # test for presence of vpn gateway override in main routing table
  202.     ip route | grep -q "^0\.0\.0\.0/1 .*$(env_get dev)" && VPN_IS_GW=
  203.  
  204.     # ignore remote access rule for bridged configurations
  205.     if ! echo $WAN_IF | grep -q '^br[0-9]$'; then
  206.         # add rule for remote access
  207.         if [ ${VPN_IS_GW+x} ]; then
  208.             # enable remote access over WAN
  209.             add_rule -i $WAN_IF
  210.         else
  211.             # enable remote access over VPN
  212.             add_rule -i $VPN_IF
  213.         fi
  214.     fi
  215.  
  216.     local files="$(echo $IMPORT_RULES_FILESPEC)"
  217.     if [ "$files" != "$IMPORT_RULES_FILESPEC" ]; then
  218.         # add rules from import file(s) (if any)
  219.         for file in $files; do . "$file"; done
  220.     else
  221.         # use embedded rules
  222.         add_rules
  223.     fi
  224.  
  225.     # create ipset hash tables
  226.     if [ ${IPSET_SUPPORTED+x} ]; then
  227.         ipset -N $IPSET_HOST iphash -q || ipset -F $IPSET_HOST
  228.         ipset -N $IPSET_NET nethash -q || ipset -F $IPSET_NET
  229.     fi
  230.  
  231.     # add hosts and networks from import file(s) (if any)
  232.     import_hosts_and_networks
  233.  
  234.     # add rules for ipset hash tables
  235.     if [ ${IPSET_SUPPORTED+x} ]; then
  236.         add_rule -m set --match-set $IPSET_HOST dst
  237.         add_rule -m set --match-set $IPSET_NET  dst
  238.     fi
  239.  
  240.     # finalize chain for user-defined rules
  241.     $IPT_MAN -A $FW_CHAIN -m mark ! --mark $FW_MARK $IPT_MARK_NOMATCH
  242.     $IPT_MAN -A $FW_CHAIN -j CONNMARK --save-mark
  243.  
  244.     # add rules (router only)
  245.     $IPT_MAN -A OUTPUT -j CONNMARK --restore-mark
  246.     if [ ${IPSET_SUPPORTED+x} ]; then
  247.         $IPT_MAN -A OUTPUT -m mark --mark 0 \
  248.             -m set --match-set $IPSET_HOST dst $IPT_MARK_MATCHED
  249.         $IPT_MAN -A OUTPUT -m mark --mark 0 \
  250.             -m set --match-set $IPSET_NET  dst $IPT_MARK_MATCHED
  251.     fi
  252.  
  253.     # clear marks (not available on all builds)
  254.     [ -f /proc/net/clear_marks ] && echo 1 > /proc/net/clear_marks
  255.  
  256.     # copy main routing table to alternate (exclude all default gateways)
  257.     ip route | grep -Ev '^default |^0.0.0.0/1 |^128.0.0.0/1 ' \
  258.       | while read route; do
  259.             ip route add $route table $TID
  260.         done
  261.  
  262.     if [ ${VPN_IS_GW+x} ]; then
  263.         # add WAN as default gateway to alternate routing table
  264.         ip route add default via $WAN_GW table $TID
  265.     else
  266.         # add VPN as default gateway to alternate routing table
  267.         ip route add default via $VPN_GW table $TID
  268.     fi
  269.  
  270.     # disable reverse path filtering
  271.     for rpf in /proc/sys/net/ipv4/conf/*/rp_filter; do
  272.         echo "echo $(cat $rpf) > $rpf" >> $RPF_VARS
  273.         echo 0 > $rpf
  274.     done
  275.  
  276.     # start split tunnel
  277.     ip rule add fwmark $FW_MARK table $TID
  278.  
  279.     # force routing system to recognize changes
  280.     ip route flush cache
  281. }
  282.  
  283. down() {
  284.     # stop split tunnel
  285.     while ip rule del fwmark $FW_MARK table $TID 2> /dev/null; do :; done
  286.  
  287.     # enable reverse path filtering
  288.     while read rpf; do eval $rpf; done < $RPF_VARS
  289.  
  290.     # remove rules
  291.     while $IPT_MAN -D PREROUTING -j $FW_CHAIN 2> /dev/null; do :; done
  292.     $IPT_MAN -F $FW_CHAIN
  293.     $IPT_MAN -X $FW_CHAIN
  294.     $IPT_MAN -D OUTPUT -j CONNMARK --restore-mark
  295.     if [ ${IPSET_SUPPORTED+x} ]; then
  296.         $IPT_MAN -D OUTPUT -m mark --mark 0 \
  297.             -m set --match-set $IPSET_HOST dst $IPT_MARK_MATCHED
  298.         $IPT_MAN -D OUTPUT -m mark --mark 0 \
  299.             -m set --match-set $IPSET_NET  dst $IPT_MARK_MATCHED
  300.     fi
  301.  
  302.     # clear marks (not available on all builds)
  303.     [ -f /proc/net/clear_marks ] && echo 1 > /proc/net/clear_marks
  304.  
  305.     # remove ipset hash tables
  306.     if [ ${IPSET_SUPPORTED+x} ]; then
  307.         ipset -F $IPSET_HOST && ipset -X $IPSET_HOST
  308.         ipset -F $IPSET_NET  && ipset -X $IPSET_NET
  309.     fi
  310.  
  311.     # delete alternate routing table
  312.     ip route flush table $TID
  313.  
  314.     # force routing system to recognize changes
  315.     ip route flush cache
  316.  
  317.     # cleanup
  318.     rm -f $ENV_VARS $RPF_VARS
  319.  
  320.     # call dd-wrt route-pre-down script
  321.     /tmp/openvpncl/route-down.sh 2> /dev/null
  322. }
  323.  
  324. main() {
  325.     # reject cli invocation; script only applicable to routed (tun) tunnels
  326.     [[ -t 0 || "$(env_get dev_type)" != 'tun' ]] && return 1
  327.  
  328.     # quit if we fail to meet any prerequisites
  329.     verify_prerequisites || { echo 'exiting on fatal error(s)'; return 1; }
  330.  
  331.     # determine if ipset utility is available
  332.     which ipset > /dev/null 2>&1 && IPSET_SUPPORTED= || echo 'warning: ipset not supported'
  333.  
  334.     # trap event-driven callbacks by openvpn and take appropriate action(s)
  335.     case "$script_type" in
  336.               route-up) up;;
  337.         route-pre-down) down;;
  338.                      *) echo "warning: unexpected invocation: $script_type";;
  339.     esac
  340.  
  341.     return 0
  342. }
  343.  
  344. main
  345.  
  346. ) 2>&1 | logger -p user.$([ ${DEBUG+x} ] && echo 'debug' || echo 'notice') \
  347.     -t $(echo $(basename $0) | grep -Eo '^.{0,23}')[$$]
RAW Paste Data