eibgrad

ddwrt-ovpn-split-advanced.sh

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