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: tomato-ovpn-split-advanced.sh
- # version: 2.1.1, 18-sep-2020, by eibgrad
- # purpose: redirect specific traffic over the WAN|VPN
- # script type: openvpn (route-up, route-pre-down)
- # installation:
- # 1. enable jffs (administration->jffs)
- # 2. enable syslog (status->logs->logging configuration->syslog)
- # 3. use shell (telnet/ssh) to execute one of the following commands:
- # curl -kLs bit.ly/tomato-installer|tr -d '\r'|sh -s -- --dir /jffs GMUbEtGj
- # or
- # wget -qO - bit.ly/tomato-installer|tr -d '\r'|sh -s -- --dir /jffs GMUbEtGj
- # 4. use vi editor to modify script w/ your rules:
- # vi /jffs/tomato-ovpn-split-advanced.sh
- # 5. create symbolic links:
- # ln -sf /jffs/tomato-ovpn-split-advanced.sh /jffs/route-up
- # ln -sf /jffs/tomato-ovpn-split-advanced.sh /jffs/route-down
- # 6. add the following to openvpn client custom configuration:
- # script-security 2
- # route-up /jffs/route-up
- # route-pre-down /jffs/route-down
- # 7. optional: by default, the default gateway is changed to the VPN,
- # so the rules reroute over the WAN; to set/lockdown the default
- # gateway to the WAN and have the rules reroute to the VPN, add the
- # following directives to openvpn client custom configuration:
- # pull-filter ignore "redirect-gateway"
- # redirect-private def1
- # 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. disable policy based routing (vpn tunneling->openvpn client->
- # routing policy tab)
- # 10. disable qos
- # 11. reboot
- # limitations:-
- # - this script is only compatible w/ freshtomato v2020.2 or later
- # - this script is limited to *one* active OpenVPN client at a time
- # - this script can NOT be used while *any* OpenVPN client is actively
- # using tomato policy based routing
- # - this script is NOT compatible w/ tomato qos
- # - rules do NOT support domain names (e.g., google.com); domain names
- # are only supported w/ ipset feature (step #8)
- (
- 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.10
- #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 ----------------------- #
- CID="${dev:4:1}"
- ENV_VARS="/tmp/env_vars_${CID}"
- RPF_VARS="/tmp/rpf_vars_${CID}"
- # 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="20${CID}"
- 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))"
- 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
- }
- verify_prerequisites() {
- local err_found=false
- # tomato policy based routing cannot be active anywhere (firewall conflict)
- for i in 1 2 3; do
- if pidof vpnclient${i} >/dev/null; then
- local rgw="$(nvram get vpn_client${i}_rgw)"
- if [[ "$rgw" == '2' || "$rgw" == '3' ]]; then
- echo 'fatal error: tomato policy based routing is currently active'
- err_found=true
- fi
- fi
- done
- # qos must be disabled (packet marking conflict)
- if [ "$(nvram get qos_enable)" == '1' ]; then
- echo 'fatal error: qos must be disabled'
- err_found=true
- fi
- [[ $err_found == false ]] && return 0 || return 1
- }
- configure_ipset() {
- # verify DNSMasq supports ipset
- if ! dnsmasq -v | grep -Eq '^.*(^|[[:space:]]+)ipset([[:space:]]+|$)'; then
- echo 'warning: installed version of DNSMasq does not support ipset'
- return 1
- fi
- # load ipset module
- modprobe ip_set 2>/dev/null || return 1
- # ipset sub-modules vary depending on ipset version; adjust accordingly
- if modprobe ip_set_hash_ip 2>/dev/null; then
- # ipset protocol 6
- modprobe ip_set_hash_net
- else
- # ipset protocol 4
- modprobe ip_set_iphash
- modprobe ip_set_nethash
- fi
- # iptables "set" module varies depending on version; adjust accordingly
- modprobe ipt_set 2>/dev/null || modprobe xt_set
- # parse the iptables version # into subversions
- _subver() { awk -v v="$v" -v i="$1" 'BEGIN {split(v,a,"."); print a[i]}'; }
- local v="$(iptables --version | grep -o '[0-9\.]*')"
- local v1=$(_subver 1)
- local v2=$(_subver 2)
- local v3=$(_subver 3)
- # iptables v1.4.4 and above has deprecated --set in favor of --match-set
- if [[ $v1 -gt 1 || $v2 -gt 4 ]] || [[ $v2 -eq 4 && $v3 -ge 4 ]]; then
- MATCH_SET='--match-set'
- else
- MATCH_SET='--set'
- fi
- return 0
- }
- import_hosts_and_networks() {
- # import file naming format:
- # *.ipset
- # example import files:
- # /jffs/some_hosts.ipset
- # /jffs/some_networks.ipset
- # /jffs/some_hosts_and_networks.ipset
- # import file format (one per line):
- # ip | network(cidr)
- # 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
- # 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
- }
- up() {
- # 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
- # 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 show | 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
- }
- 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
- }
- main() {
- # reject cli invocation; script only applicable to routed (tun) tunnels
- [[ -t 0 || "$(env_get dev_type)" != 'tun' ]] && return 1
- # quit if we fail to meet any prerequisites
- verify_prerequisites || { echo 'exiting on fatal error(s)'; return 1; }
- # configure ipset modules and adjust iptables "set" syntax according to version
- configure_ipset && 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
- return 0
- }
- main
- ) 2>&1 | logger -p user.$([ ${DEBUG+x} ] && echo 'debug' || echo 'notice') \
- -t $(echo $(basename $0) | grep -Eo '^.{0,23}')[$$]
Add Comment
Please, Sign In to add comment