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