eibgrad

tomato-ovpn-split-advanced.sh

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