-
Notifications
You must be signed in to change notification settings - Fork 22
Expand file tree
/
Copy pathvpn-ip-routes.sh
More file actions
346 lines (276 loc) · 12.7 KB
/
vpn-ip-routes.sh
File metadata and controls
346 lines (276 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
#!/bin/sh
# Made by Jack'lul <jacklul.github.io>
#
# Allow routing specific IPs through specified VPN Fusion profiles
#
# Inspired by Domain VPN Routing script:
# https://github.com/Ranger802004/asusmerlin/tree/main/domain_vpn_routing
#
# fwmark based implementation based on mentioned above script
#
#jas-update=vpn-ip-routes.sh
#shellcheck shell=ash
#shellcheck disable=SC2155
#shellcheck source=./common.sh
readonly common_script="$(dirname "$0")/common.sh"
if [ -f "$common_script" ]; then . "$common_script"; else { echo "$common_script not found" >&2; exit 1; } fi
ROUTE_IPS="" # route IPs to specific VPN profiles, in format '5=1.1.1.1' (VPNC_ID=IP), separated by spaces, to find VPNC_ID run 'jas vpn-ip-routes identify'
ROUTE_IPS6="" # same as ROUTE_IPS but for IPv6, separated by spaces
EXECUTE_COMMAND="" # execute a command after rules are applied or removed (receives arguments: $1 = action - add/remove)
RUN_EVERY_MINUTE= # verify that the rules are still set (true/false), empty means false when service-event script is available but otherwise true
RETRY_ON_ERROR=false # retry setting the rules on error (once per run)
# The following is development/testing and might not work
USE_FWMARKS=false # use fwmarks instead of IP rules, this could improve routing performance with many rules
FWMARK_POOL="0xa000 0xb000 0xc000 0xd000 0xe000" # available fwmarks to use in rules, separated by spaces, careful as some can be in use by the firmware
FWMARK_MASK="0xf000" # fwmark mask to use, it must be compatible with all entries in FWMARK_POOL
load_script_config
state_file="$TMP_DIR/$script_name"
readonly CHAIN="jas-${script_name}"
is_vpnc_active() {
get_vpnc_clientlist | awk -F '>' '{print $7, $6}' | grep -Fq "$1 1"
}
get_profile_desc() {
local _desc="$(get_vpnc_clientlist | awk -F '>' '{print $7, $1}' | grep "^$1" | awk '{sub(/^[^ ]* /, ""); print}')"
[ -n "$_desc" ] && echo "$_desc" || echo "ID = $1"
}
get_fwmark_for_idx() {
# Since we are using eval later, it's worth making sure $1 is just a number
case $1 in
''|*[!0-9]*) echo "Expected numeric argument" >&2; return 1 ;;
*) ;;
esac
local _varname="_fwmark_$1"
local _value="$(eval "echo \$$_varname")"
if [ -n "$_value" ]; then
echo "$_value"
else
[ -z "$fwmark_pool" ] && fwmark_pool="$FWMARK_POOL"
local _first_fwmark=$(echo "$fwmark_pool" | cut -d ' ' -f 1)
[ -z "$_first_fwmark" ] && return 1 # No more available fmarks
fwmark_pool=$(echo "$fwmark_pool" | sed -E "s/$_first_fwmark//" | awk '{$1=$1};1')
eval "$_varname=$_first_fwmark"
echo "$_first_fwmark"
fi
}
cleanup_ip_rules() {
local _ip="$1"
[ -z "$_ip" ] && _ip="ip -4"
local _idx _priority removed_idx
# Clean up any existing ip rules for inactive profiles
for _idx in 5 6 7 8 9 10 11 12 13 14 15 16; do # VPNC_UNIT_BASIC - MAX_VPNC_PROFILE (max 12 profiles)
if ! echo "$active_idx" | grep -Fq " $_idx "; then
_priority=$((1000+_idx))
if [ -n "$($_ip rule show priority "$_priority")" ]; then
while $_ip rule del priority "$_priority" 2> /dev/null; do :; done
rules_removed=1
! echo "$removed_idx" | grep -Fq " $_idx " && removed_idx="$removed_idx $_idx "
fi
fi
done
}
iptables_rules() {
local _for_iptables="iptables"
[ "$(nvram get ipv6_service)" != "disabled" ] && _for_iptables="$_for_iptables ip6tables"
# If xt_comment module is not available, disable comments to avoid errors and continue working without them
modprobe xt_comment 2> /dev/null && iptables_comment="jas-$script_name" || iptables_comment=""
local _iptables _route_ips _ip _chain _route_ip _idx _ip_addr _fwmark _priority
for _iptables in $_for_iptables; do
if [ "$_iptables" = "ip6tables" ]; then
_route_ips="$ROUTE_IPS6"
_ip="ip -6"
else
_route_ips="$ROUTE_IPS"
_ip="ip -4"
fi
[ -z "$_route_ips" ] && continue
case "$1" in
"add")
# firewall restart does not delete out chains in mangle table?
if ! $_iptables -t mangle -nL "$CHAIN" > /dev/null 2>&1; then
$_iptables -t mangle -N "$CHAIN"
fi
for _chain in PREROUTING OUTPUT POSTROUTING; do
if ! $_iptables -t mangle -C "$_chain" -j "$CHAIN" ${iptables_comment:+-m comment --comment "$iptables_comment"} > /dev/null 2>&1; then
$_iptables -t mangle -A "$_chain" -j "$CHAIN" ${iptables_comment:+-m comment --comment "$iptables_comment"}
fi
done
#if ! $_iptables -t mangle -C PREROUTING -j CONNMARK --restore-mark > /dev/null 2>&1; then
# $_iptables -t mangle -I PREROUTING -j CONNMARK --restore-mark
#fi
#if ! $_iptables -t mangle -C POSTROUTING -j CONNMARK --save-mark > /dev/null 2>&1; then
# $_iptables -t mangle -A POSTROUTING -j CONNMARK --save-mark
#fi
# If the configuration has changed, flush the chain before setting new rules
[ -n "$do_update" ] && $_iptables -t mangle -F "$CHAIN"
for _route_ip in $_route_ips; do
_idx="$(echo "$_route_ip" | cut -d '=' -f 1)"
if is_vpnc_active "$_idx"; then
_ip_addr="$(echo "$_route_ip" | cut -d '=' -f 2)"
_fwmark="$(get_fwmark_for_idx "$_idx")"
if [ -z "$_fwmark" ]; then
if [ -z "$fwmark_exhausted_notify" ]; then
logecho "Error: Exhausted fwmark pool" error
fwmark_exhausted_notify=true
fi
continue;
fi
_priority=$((1000+_idx))
if [ -z "$($_ip rule show from all fwmark "$_fwmark/$FWMARK_MASK" table "$_idx" priority "$_priority")" ]; then
$_ip rule add from all fwmark "$_fwmark/$FWMARK_MASK" table "$_idx" priority "$_priority" || rules_error=1
fi
if ! $_iptables -t mangle -C "$CHAIN" -d "$_ip_addr" -j MARK --set-xmark "$_fwmark/$FWMARK_MASK" > /dev/null 2>&1; then
if $_iptables -t mangle -A "$CHAIN" -d "$_ip_addr" -j MARK --set-xmark "$_fwmark/$FWMARK_MASK"; then
rules_added=1
! echo "$added_idx" | grep -Fq " $_idx " && added_idx="$added_idx $_idx "
else
rules_error=1
fi
fi
! echo "$active_idx" | grep -Fq " $_idx " && active_idx="$active_idx $_idx "
fi
done
;;
"remove")
remove_iptables_rules_by_comment "$_iptables" "mangle"
if $_iptables -t mangle -nL "$CHAIN" > /dev/null 2>&1; then
$_iptables -t mangle -F "$CHAIN"
$_iptables -t mangle -X "$CHAIN" || rules_error=1
fi
;;
esac
cleanup_ip_rules "$_ip"
$_ip route flush cache
done
[ "$rules_error" = 1 ] && logecho "Errors detected while modifying iptables or IP routing rules ($1)" error
[ -z "$rules_error" ] && return 0 || return 1
}
ip_route_rules() {
local _for_ip="ip"
[ "$(nvram get ipv6_service)" != "disabled" ] && _for_ip="$_for_ip ip6"
local _ip _route_ips _route_ip _idx _ip_addr _priority
for _ip in $_for_ip; do
if [ "$_ip" = "ip6" ]; then
_route_ips="$ROUTE_IPS6"
_ip="ip -6"
else
_route_ips="$ROUTE_IPS"
_ip="ip -4"
fi
case "$1" in
"add")
[ -z "$_route_ips" ] && continue
for _route_ip in $_route_ips; do
_idx="$(echo "$_route_ip" | cut -d '=' -f 1)"
if is_vpnc_active "$_idx"; then
_ip_addr="$(echo "$_route_ip" | cut -d '=' -f 2)"
_priority=$((1000+_idx))
# If the configuration has changed, remove all rules of this idx before setting new rules
if [ -n "$do_update" ] && ! echo "$active_idx" | grep -Fq " $_idx "; then
while $_ip rule del priority "$_priority" 2> /dev/null; do :; done
fi
if [ -z "$($_ip rule show from all to "$_ip_addr" table "$_idx" priority "$_priority")" ]; then
if $_ip rule add from all to "$_ip_addr" table "$_idx" priority "$_priority"; then
rules_added=1
! echo "$added_idx" | grep -Fq " $_idx " && added_idx="$added_idx $_idx "
else
rules_error=1
fi
fi
! echo "$active_idx" | grep -Fq " $_idx " && active_idx="$active_idx $_idx "
fi
done
;;
esac
cleanup_ip_rules "$_ip"
$_ip route flush cache
done
[ "$rules_error" = 1 ] && logecho "Errors detected while modifying IP routing rules ($1)" error
[ -z "$rules_error" ] && return 0 || return 1
}
rules() {
{ [ -z "$ROUTE_IPS" ] && [ -z "$ROUTE_IPS6" ] ; } && { logecho "Error: ROUTE_IPS/ROUTE_IPS6 is not set" error; exit 1; }
lockfile lockwait
# Compare with previous configuration
if [ -f "$state_file" ]; then
local LAST_ROUTE_IPS LAST_ROUTE_IPS6 LAST_USE_FWMARKS LAST_FWMARK_POOL LAST_FWMARK_MASK
. "$state_file"
[ "$ROUTE_IPS" != "$LAST_ROUTE_IPS" ] && do_update=1
[ "$ROUTE_IPS6" != "$LAST_ROUTE_IPS6" ] && do_update=1
if [ "$USE_FWMARKS" != "$LAST_USE_FWMARKS" ] || [ "$FWMARK_POOL" != "$LAST_FWMARK_POOL" ] || [ "$FWMARK_MASK" != "$LAST_FWMARK_MASK" ]; then
echo "Error: Configuration for fwmarks has changed, please restart the script" >&2
exit 1
fi
fi
rules_added=
rules_removed=
added_idx=
active_idx=
if [ "$USE_FWMARKS" = true ]; then
iptables_rules "$1"
else
ip_route_rules "$1"
fi
if [ "$rules_added" = 1 ]; then
local _added_profiles _idx
for _idx in $added_idx; do
_added_profiles="$_added_profiles '$(get_profile_desc "$_idx")'"
done
logecho "Added IP routing rules for VPN profiles: $(echo "$_added_profiles" | awk '{$1=$1};1')" alert
fi
if [ "$rules_removed" = 1 ]; then
local _removed_profiles _idx
for _idx in $removed_idx; do
_removed_profiles="$_removed_profiles '$(get_profile_desc "$_idx")'"
done
logecho "Removed IP routing rules for VPN profiles: $(echo "$_removed_profiles" | awk '{$1=$1};1')" alert
fi
cat <<EOT > "$state_file"
LAST_ROUTE_IPS="$ROUTE_IPS"
LAST_ROUTE_IPS6="$ROUTE_IPS6"
LAST_USE_FWMARKS="$USE_FWMARKS"
LAST_FWMARK_POOL="$FWMARK_POOL"
LAST_FWMARK_MASK="$FWMARK_MASK"
EOT
[ -n "$EXECUTE_COMMAND" ] && { [ -n "$rules_added" ] || [ -n "$rules_removed" ] ; } && eval "$EXECUTE_COMMAND $1"
lockfile unlock
[ -z "$rules_error" ] && return 0 || return 1
}
case "$1" in
"run")
rules add || { [ "$RETRY_ON_ERROR" = true ] && rules add; }
;;
"identify")
printf "%-3s %-7s %-20s\n" "ID" "Active" "Description"
printf "%-3s %-7s %-20s\n" "--" "------" "--------------------"
IFS="$(printf '\n\b')"
for entry in $(get_vpnc_clientlist); do
desc="$(echo "$entry" | awk -F '>' '{print $1}')"
active="$(echo "$entry" | awk -F '>' '{print $6}')"
idx="$(echo "$entry" | awk -F '>' '{print $7}')"
[ "$active" = "1" ] && active=yes || active=no
printf "%-3s %-7s %-50s\n" "$idx" "$active" "$desc"
done
;;
"start")
rules add
# Set value of empty RUN_EVERY_MINUTE depending on situation
execute_script_basename "service-event.sh" check && service_event_active=true
[ -z "$RUN_EVERY_MINUTE" ] && [ -z "$service_event_active" ] && RUN_EVERY_MINUTE=true
if [ "$RUN_EVERY_MINUTE" = true ]; then
crontab_entry add "*/1 * * * * $script_path run"
fi
;;
"stop")
crontab_entry delete
rm -f "$state_file"
rules remove
;;
"restart")
sh "$script_path" stop
sh "$script_path" start
;;
*)
echo "Usage: $0 run|start|stop|restart|identify"
exit 1
;;
esac