Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/bin/sh
- #DEBUG=; set -x # uncomment/comment to enable/disable debug mode
- # name: ddwrt-ovpn-split-advanced.sh
- # version: 3.1.0, 08-aug-2022, by eibgrad
- # purpose: advanced extended-criteria based openvpn split tunneling
- # script type: openvpn route-up/route-pre-down
- # installation:
- # 1. enable jffs2 (administration->jffs2)
- # 2. disable policy based routing (services->vpn->openvpn client)
- # 3. disable nat loopback (security->firewall, "filter wan nat redirection"
- # must be checked)
- # 4. disable qos (nat/qos->qos)
- # 5. use shell (telnet/ssh) to execute one of the following commands:
- # curl -kLs bit.ly/ddwrt-installer|tr -d '\r'|sh -s nC27ETsp
- # or
- # wget -qO - bit.ly/ddwrt-installer|tr -d '\r'|sh -s nC27ETsp
- # 6. use vi editor to add/modify rules:
- # vi /jffs/etc/config/ddwrt-ovpn-split-advanced.sh
- # note: rules can be managed outside the script by placing them in a
- # file(s) w/ the extension .rules in the installation directory; these
- # will be imported and used at runtime in leu of the script's embedded
- # rules
- # 7. optional: by default, the default gateway is set to the WAN, so
- # the rules reroute over the VPN; to set/lockdown the default gateway
- # to the VPN and have the rules reroute to the WAN, disable the
- # following directives as shown in openvpn client additional config:
- # #pull-filter ignore redirect-gateway
- # #redirect-private def1
- # note: default routing is configurable during installation
- # 8. optional: add ipset directive(s) w/ your domains to dnsmasq custom
- # configuration (last field of directive must be ovpn_split):
- # ipset=/ipchicken.com/netflix.com/ovpn_split
- # ipset=/google.com/cnet.com/gov/ovpn_split
- # 9. optional: ipset hash tables can be preload w/ hosts and/or networks
- # by placing them in a file(s) w/ the extension .ipset in the
- # installation directory; these will be imported and used at runtime
- # 10. reboot
- # limitations, known issues, and miscellany:
- # - this script is only supported by dd-wrt v43904 (7/23/20) or later
- # - this script is NOT compatible w/ dd-wrt policy based routing
- # - this script is NOT compatible w/ dd-wrt nat loopback
- # - this script is NOT compatible w/ dd-wrt qos
- # - rules do NOT support domain names (e.g., google.com); domain names
- # are only supported w/ ipset feature (step #8)
- # - lan->wan default routing excludes the router itself from the vpn,
- # whereas lan->vpn does NOT; something you may wish to consider when
- # deciding on your preference
- {
- add_rules() {
- # ----------------------------------- FYI ------------------------------------ #
- # * the order of rules doesn't matter (there is no order of precedence)
- # * if any rule matches, those packets bypass the current default gateway
- # * remote access is already enabled; no additional rules are necessary
- # ---------------------------------------------------------------------------- #
- # ------------------------------- BEGIN RULES -------------------------------- #
- #add_rule -s 192.168.1.14
- #add_rule -p tcp -s 192.168.1.112 --dport 80
- #add_rule -p tcp -s 192.168.1.122 --dport 3000:3100
- #add_rule -i br1 # guest network
- #add_rule -i br2 # iot network
- # -------------------------------- END RULES --------------------------------- #
- :;}
- # ---------------------- DO NOT CHANGE BELOW THIS LINE ----------------------- #
- ENV_VARS='/tmp/env_vars'
- RPF_VARS='/tmp/rpf_vars'
- # make environment variables persistent across openvpn events
- [ "$script_type" == 'route-up' ] && env > $ENV_VARS
- # utility function for retrieving environment variable values
- env_get() { echo $(grep -Em1 "^$1=" $ENV_VARS | cut -d = -f2); }
- IMPORT_RULES_FILESPEC="$(dirname $0)/*.rules"
- IMPORT_IPSET_FILESPEC="$(dirname $0)/*.ipset"
- TID='200'
- WAN_GW="$(env_get route_net_gateway)"
- WAN_IF="$(ip route | awk '/^default/{print $NF}')"
- VPN_GW="$(env_get route_vpn_gateway)"
- VPN_IF="$(env_get dev)"
- FW_CHAIN='ovpn_split'
- FW_MARK=1
- IPSET_HOST='ovpn_split' # must match ipset directive in dnsmasq
- IPSET_NET='ovpn_split_net'
- IPT_MAN='iptables -t mangle'
- IPT_MARK_MATCHED="-j MARK --set-mark $FW_MARK"
- IPT_MARK_NOMATCH="-j MARK --set-mark $((FW_MARK + 1))"
- # function add_rule( rule )
- add_rule() {
- # precede addition w/ deletion to avoid dupes
- $IPT_MAN -D $FW_CHAIN "$@" $IPT_MARK_MATCHED 2>/dev/null
- $IPT_MAN -A $FW_CHAIN "$@" $IPT_MARK_MATCHED
- }
- # function verify_prerequisites()
- verify_prerequisites() {
- local err_found
- # dd-wrt policy based routing cannot be active (ip rule conflict)
- if [ "$(nvram get openvpncl_spbr)" ]; then
- if [ "$(nvram get openvpncl_spbr)" != '0' ]; then
- echo 'fatal error: dd-wrt policy based routing is currently active'
- err_found=
- fi
- elif [ "$(nvram get openvpncl_route)" ]; then
- echo 'fatal error: dd-wrt policy based routing is currently active'
- err_found=
- fi
- # nat loopback must be disabled (packet marking conflict)
- if [ "$(nvram get block_loopback)" == '0' ]; then
- echo 'fatal error: nat loopback must be disabled'
- err_found=
- fi
- # qos must be disabled (packet marking conflict)
- if [ "$(nvram get wshaper_enable)" == '1' ]; then
- echo 'fatal error: qos must be disabled'
- err_found=
- fi
- [ ${err_found+x} ] && return 1 || return 0
- }
- # function import_hosts_and_networks()
- import_hosts_and_networks() {
- # import file naming format:
- # *.ipset
- # example import files:
- # /jffs/etc/config/some_hosts.ipset
- # /jffs/etc/config/some_networks.ipset
- # /jffs/etc/config/some_hosts_and_networks.ipset
- # import file format (one per line):
- # ip | network (cidr notation)
- # example import file contents:
- # 122.122.122.122
- # 212.212.212.0/24
- local MASK_COMMENT='^[[:space:]]*(#|$)'
- local MASK_HOST='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
- local MASK_HOST_32='^([0-9]{1,3}\.){3}[0-9]{1,3}/32$'
- local MASK_NET='^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$'
- local ERR_MSG="/tmp/tmp.$$.err_msg"
- local files file line
- # ipset( set host|network )
- _ipset_add() {
- if ipset -A $1 $2 2> $ERR_MSG; then
- return
- elif grep -Eq 'already (added|in set)' $ERR_MSG; then
- echo "info: duplicate host|network; ignored: $2"
- else
- cat $ERR_MSG
- echo "error: cannot add host|network: $2"
- fi
- }
- # _add_hosts_and_networks( file )
- _add_hosts_and_networks() {
- while read line; do
- # skip comments and blank lines
- echo $line | grep -Eq $MASK_COMMENT && continue
- # isolate host|network (the rest is treated as comments)
- line="$(echo $line | awk '{print $1}')"
- # line may contain host/network; add to appropriate ipset hash table
- if echo $line | grep -Eq $MASK_HOST; then
- _ipset_add $IPSET_HOST $line
- elif echo $line | grep -Eq $MASK_HOST_32; then
- _ipset_add $IPSET_HOST $(echo $line | sed 's:/32::')
- elif echo $line | grep -Eq $MASK_NET; then
- _ipset_add $IPSET_NET $line
- else
- echo "error: unknown host|network: $line"
- fi
- done < "$1"
- }
- files="$(echo $IMPORT_IPSET_FILESPEC)"
- if [ "$files" != "$IMPORT_IPSET_FILESPEC" ]; then
- # convert from dos to linux file format (as necessary)
- for file in $files; do
- grep -q '\r' "$file" && sed -i 's/\r//g' "$file"
- done
- # add hosts and networks from import file(s) (if any)
- for file in $files; do _add_hosts_and_networks "$file"; done
- fi
- # cleanup
- rm -f $ERR_MSG
- }
- # function up()
- up() {
- # call dd-wrt route-up script
- /tmp/openvpncl/route-up.sh 2>/dev/null
- # add chain for user-defined rules
- $IPT_MAN -N $FW_CHAIN
- $IPT_MAN -A PREROUTING -j $FW_CHAIN
- # initialize chain for user-defined rules
- $IPT_MAN -A $FW_CHAIN -j CONNMARK --restore-mark
- $IPT_MAN -A $FW_CHAIN -m mark ! --mark 0 -j RETURN
- # test for presence of vpn gateway override in main routing table
- ip route | grep -q "^0\.0\.0\.0/1 .*$(env_get dev)" && VPN_IS_GW=
- # ignore remote access rule for bridged configurations
- if ! echo $WAN_IF | grep -q '^br[0-9]$'; then
- # add rule for remote access
- if [ ${VPN_IS_GW+x} ]; then
- # enable remote access over WAN
- add_rule -i $WAN_IF
- else
- # enable remote access over VPN
- add_rule -i $VPN_IF
- fi
- fi
- local files="$(echo $IMPORT_RULES_FILESPEC)"
- if [ "$files" != "$IMPORT_RULES_FILESPEC" ]; then
- # convert from dos to linux file format (as necessary)
- for file in $files; do
- grep -q '\r' "$file" && sed -i 's/\r//g' "$file"
- done
- # add rules from import file(s) (if any)
- for file in $files; do . "$file"; done
- else
- # use embedded rules
- add_rules
- fi
- # create ipset hash tables
- if [ ${IPSET_SUPPORTED+x} ]; then
- ipset -N $IPSET_HOST iphash -q || ipset -F $IPSET_HOST
- ipset -N $IPSET_NET nethash -q || ipset -F $IPSET_NET
- fi
- # add hosts and networks from import file(s) (if any)
- import_hosts_and_networks
- # add rules for ipset hash tables
- if [ ${IPSET_SUPPORTED+x} ]; then
- add_rule -m set --match-set $IPSET_HOST dst
- add_rule -m set --match-set $IPSET_NET dst
- fi
- # finalize chain for user-defined rules
- $IPT_MAN -A $FW_CHAIN -m mark ! --mark $FW_MARK $IPT_MARK_NOMATCH
- $IPT_MAN -A $FW_CHAIN -j CONNMARK --save-mark
- # add rules (router only)
- $IPT_MAN -A OUTPUT -j CONNMARK --restore-mark
- if [ ${IPSET_SUPPORTED+x} ]; then
- $IPT_MAN -A OUTPUT -m mark --mark 0 \
- -m set --match-set $IPSET_HOST dst $IPT_MARK_MATCHED
- $IPT_MAN -A OUTPUT -m mark --mark 0 \
- -m set --match-set $IPSET_NET dst $IPT_MARK_MATCHED
- fi
- # clear marks (not available on all builds)
- [ -f /proc/net/clear_marks ] && echo 1 > /proc/net/clear_marks
- # copy main routing table to alternate (exclude all default gateways)
- ip route | grep -Ev '^default |^0\.0\.0\.0/1 |^128\.0\.0\.0/1 ' \
- | while read route; do
- ip route add $route table $TID
- done
- if [ ${VPN_IS_GW+x} ]; then
- # add WAN as default gateway to alternate routing table
- ip route add default via $WAN_GW table $TID
- else
- # add VPN as default gateway to alternate routing table
- ip route add default via $VPN_GW table $TID
- fi
- # disable reverse path filtering
- for rpf in /proc/sys/net/ipv4/conf/*/rp_filter; do
- echo "echo $(cat $rpf) > $rpf" >> $RPF_VARS
- echo 0 > $rpf
- done
- # start split tunnel
- ip rule add fwmark $FW_MARK table $TID
- # force routing system to recognize changes
- ip route flush cache
- }
- # function down()
- down() {
- # stop split tunnel
- while ip rule del fwmark $FW_MARK table $TID 2>/dev/null; do :; done
- # enable reverse path filtering
- while read rpf; do eval $rpf; done < $RPF_VARS
- # remove rules
- while $IPT_MAN -D PREROUTING -j $FW_CHAIN 2>/dev/null; do :; done
- $IPT_MAN -F $FW_CHAIN
- $IPT_MAN -X $FW_CHAIN
- $IPT_MAN -D OUTPUT -j CONNMARK --restore-mark
- if [ ${IPSET_SUPPORTED+x} ]; then
- $IPT_MAN -D OUTPUT -m mark --mark 0 \
- -m set --match-set $IPSET_HOST dst $IPT_MARK_MATCHED
- $IPT_MAN -D OUTPUT -m mark --mark 0 \
- -m set --match-set $IPSET_NET dst $IPT_MARK_MATCHED
- fi
- # clear marks (not available on all builds)
- [ -f /proc/net/clear_marks ] && echo 1 > /proc/net/clear_marks
- # remove ipset hash tables
- if [ ${IPSET_SUPPORTED+x} ]; then
- ipset -F $IPSET_HOST && ipset -X $IPSET_HOST
- ipset -F $IPSET_NET && ipset -X $IPSET_NET
- fi
- # delete alternate routing table
- ip route flush table $TID
- # force routing system to recognize changes
- ip route flush cache
- # cleanup
- rm -f $ENV_VARS $RPF_VARS
- # call dd-wrt route-pre-down script
- /tmp/openvpncl/route-down.sh 2>/dev/null
- }
- # reject cli invocation; script only applicable to routed (tun) tunnels
- [[ -t 0 || "$(env_get dev_type)" != 'tun' ]] && exit 1
- # quit if we fail to meet any prerequisites
- verify_prerequisites || { echo 'exiting on fatal error(s)'; exit 1; }
- # determine if ipset utility is available
- which ipset &>/dev/null && IPSET_SUPPORTED= || echo 'warning: ipset not supported'
- # trap event-driven callbacks by openvpn and take appropriate action(s)
- case "$script_type" in
- 'route-up') up;;
- 'route-pre-down') down;;
- *) echo "warning: unexpected invocation: $script_type";;
- esac
- exit 0
- } 2>&1 | logger -t "$(basename $0 .${0##*.} | grep -Eo '^.{0,23}')[$$]"
Add Comment
Please, Sign In to add comment