eibgrad

ddwrt-ovpn-split-advanced.sh

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