-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathgeoblocker-bash-apply
554 lines (436 loc) · 18.7 KB
/
geoblocker-bash-apply
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
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
#!/bin/bash -l
# geoblocker-bash-apply
# Creates or removes ipsets and firewall rules for specified lists.
# Requires the 'ipset' utility. To install it on Debian or derivatives, use the command:
## apt install ipset
#
# Requires root priviliges
#### Initial setup
export LC_ALL=C
printf '%s\n' "$PATH" | grep '/usr/local/bin' &>/dev/null || export PATH="$PATH:/usr/local/bin"
me=$(basename "$0")
# check for root
[[ "$EUID" -ne 0 ]] && {
err="Error: $me needs to be run as root."
echo "$err" >&2
[[ ! "$nolog" ]] && logger "$err"
exit 1
}
suite_name="geoblocker-bash"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC2015
[[ -n "$script_dir" ]] && cd "$script_dir" || { err="$me: Error: Couldn't cd into '$script_dir'."; echo "$err" >&2; \
[[ ! "$nolog" ]] && logger "$err"; exit 1; }
# shellcheck source=geoblocker-bash-common
source "$script_dir/${suite_name}-common" || { err="$me: Error: Can't source ${suite_name}-common."; echo "$err" >&2; \
[[ ! "$nolog" ]] && logger "$err"; exit 1; }
# **NOTE** that some functions and variables are sourced from the *common script
# sanitize arguments
sanitize_args "$@"
# replace arguments with sanitized ones
set -- "${arguments[@]}"
#### USAGE
usage() {
cat <<EOF
$me
Loads or removes ipsets and iptables rules for specified lists.
If 'NoDrop' option is set to "true" in the config file (set up during installation): will perform the action,
except setting default iptables policies to DROP, which for a whitelist effectively leaves geoblocking *disabled*.
(NoDrop does not affect blacklist functionality)
Usage: $me <action> -l <"list_ids"> [-d] [-t] [-h]
Actions:
add|remove : Add or remove ipsets and iptables rules for lists specified with the '-l' option
Options:
-l <"list_ids"> : list id's in the format <country_code>_<family> (if specifying multiple list id's, put them in double quotes)
-d : Debug
-t : Simulate fault and test recovery
-h : This help
EOF
}
#### PARSE ARGUMENTS
# 1st argument should be the requested action
action="$1"
shift 1
[[ -z "$action" ]] && { usage; die 1 "Specify action!"; }
# check for valid action
case "$action" in
add | remove ) ;;
* ) usage; err1="Error: Unrecognized action: '$action'."; err2="Specify action in the 1st argument!"; die "$err1" "$err2" ;;
esac
# process the rest of the arguments
while getopts ":l:dht" opt; do
case $opt in
l) list_ids=$OPTARG;;
d) debugmode_args="true";;
h) usage; exit 0;;
t) test=true;;
\?) usage; die "Error: Unknown option: '$OPTARG'." ;;
esac
done
shift $((OPTIND -1))
[[ -n "$*" ]] && {
usage
err1="Error in arguments. First unrecognized argument: '$1'."
err2="Note: If specifying multiple list id's, put them in double quotes."
die "$err1" "$err2"
}
echo
# get debugmode variable from either the args or environment variable, depending on what's set
debugmode="${debugmode_args:-$debugmode}"
# set env var to match the result
export debugmode="$debugmode"
# Print script enter message for debug
debugentermsg
#### FUNCTIONS
destroy_temp_ipsets() {
curr_temp_ipsets="$(ipset list -n | grep "$suite_name" | grep "temp" | tr '\n' ' ')"
for temp_ipset in $curr_temp_ipsets; do
ipset destroy "$temp_ipset" &> /dev/null
done
}
#### Constants
# declare -A subnet_regex
#
# # ipv4 regex taken from here and modified for ERE matching:
# # https://stackoverflow.com/questions/5284147/validating-ipv4-addresses-with-regexp
# # the longer ("alternative") ipv4 regex from the top suggestion performs about 40x faster on a slow CPU with ERE grep than the shorter one
# # ipv6 regex taken from the BanIP code and modified for ERE matching
# # https://github.com/openwrt/packages/blob/master/net/banip/files/banip-functions.sh
# ipv4_regex='((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\.){3}(25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])'
# ipv6_regex='([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}:?'
# maskbits_regex_ipv6='(12[0-8]|((1[0-1]|[1-9])[0-9])|[8-9])'
# maskbits_regex_ipv4='(3[0-2]|([1-2][0-9])|[8-9])'
# subnet_regex[ipv4]="${ipv4_regex}/${maskbits_regex_ipv4}"
# subnet_regex[ipv6]="${ipv6_regex}/${maskbits_regex_ipv6}"
# unset ipv4_regex ipv6_regex maskbits_regex_ipv4 maskbits_regex_ipv6
#### VARIABLES
datadir="$(getconfig "Datadir")" || die "Error: Couldn't read value for Datadir from the config file."
list_type="$(getconfig "ListType")" || die "Error: Couldn't read value for ListType from the config file."
export list_type="$list_type"
families="$(getconfig "Families")" || die "Error: Couldn't read value for Families from the config file."
nodrop="$(getconfig "NoDrop")" || die "Error: Couldn't read value for NoDrop from the config file."
iplist_dir="${datadir}/ip_lists"
iptables_comment_aux="${suite_name}_aux"
ipset_hashsizes=()
ipset_maxelements=()
# convert to lower case
action="${action,,}"
exitvalue="0"
#### CHECKS
missing_deps="$(check_deps iptables-save iptables-restore ipset)" || die "Error: missing dependencies: $missing_deps."
# check that the config file exists
[[ ! -f "$conf_file" ]] && die "Config file '$conf_file' doesn't exist! Run the installation script again."
# check for list_ids
[[ -z "$list_ids" ]] && { usage; die 254 "Specify list id's!"; }
# check for datadir path
[[ -z "$datadir" ]] && die 254 "Error: failed to read the 'datadir' variable from the config file."
[[ -z "$list_type" ]] && die "\$list_type variable should not be empty! Something is wrong!"
#### MAIN
if [[ "$action" = "add" ]]; then
### create temporary ipsets and load ip lists into them
for list_id in $list_ids; do
family="$(printf '%s' "$list_id" | cut -s -d_ -f2)"
[ -z "$family" ] && { destroy_temp_ipsets; die "Error determining the family of list '$list_id'."; }
iplist_file="${iplist_dir}/${list_id}"
temp_ipset="${suite_name}_${list_id}_temp"
# check that the iplist file exists
[[ ! -f "$iplist_file" ]] && die 254 "Error: Can not find the iplist file in path: '$iplist_file'."
# first destroy temporary ipset with that name in case it exists
ipset destroy "$temp_ipset" &>/dev/null
# count lines in the iplist file
ip_cnt=$(wc -l < "$iplist_file")
# debugprint "ip count in the iplist file '$iplist_file': $ip_cnt"
# calculate necessary ipset size = (next high power of 2 > $ip_cnt)
ipset_maxelem=$(round_up_to_power2 "$ip_cnt")
# debugprint "calculated maxelem for ipset: $ipset_maxelem"
case "$family" in
ipv4 ) hashsize_factor=1 ;;
ipv6 ) hashsize_factor=4
esac
# set hashsize to (512) or (ipset_maxelements / 4), whichever is larger
prelim_hashsize=$(echo "scale=0 ; (${ipset_maxelem}*${hashsize_factor})/4" | bc)
debugprint "calculated hashsize: $prelim_hashsize"
if [[ "$prelim_hashsize" -lt $((512 * hashsize_factor)) ]]; then
ipset_hashsize=$((512 * hashsize_factor))
else
ipset_hashsize=$prelim_hashsize
fi
debugprint "final hashsize for the new ipset: $ipset_hashsize"
# create new temporary ipset
debugprint "Creating new ipset '$temp_ipset'... "
ipset create "$temp_ipset" hash:net family "$family" hashsize "$ipset_hashsize" maxelem "$ipset_maxelem"; rv=$?
if [[ $rv -ne 0 ]]; then
destroy_temp_ipsets
die "Error creating ipset '$temp_ipset' with hashsize '$ipset_hashsize' and maxelements '$ipset_maxelem'."
fi
debugprint "Ok."
# import the iplist into temporary ipset from file
echo -n "Importing the iplist '$list_id' into temporary ipset... "
# reads $iplist_file, transforms each line into 'ipset add' command and redirects the result into "ipset restore"
# this is about 50x faster than issuing discrete "ipset add" commands in a loop
# using awk to process the text lines rather than native bash construct "while read -r line ... do < $iplist_file"
# is yet about 4x faster for a large iplist
# the '-exist' option prevents the restore command from getting stuck when encountering duplicates
set -o pipefail
awk -v P="add \"$temp_ipset\"" '{ print P " " $0 }' "$iplist_file" | ipset restore -exist; rv=$?
set +o pipefail
if [[ $rv -ne 0 ]]; then
destroy_temp_ipsets
die 254 "Error when importing the list from '$iplist_file' into ipset '$temp_ipset'."
fi
echo "Ok."
[[ "$debugmode" ]] && ipset_lines_cnt="$(ipset save "$temp_ipset" | grep -c "add $temp_ipset")"
debugprint "subnets in the temporary ipset: $ipset_lines_cnt"
ipset_hashsizes["${list_id}"]="$ipset_hashsize"
ipset_maxelements["${list_id}"]="$ipset_maxelem"
done
echo
fi
### Remove existing geoblocker rules for iptables
if [[ "$list_type" = "whitelist" ]]; then
## Temporarily set the policy for the INPUT chain to ACCEPT, in order to prevent user lock out in case of an error
for family in $families; do
case "$family" in
ipv4 ) iptables_command="iptables" ;;
ipv6 ) iptables_command="ip6tables"
esac
echo -n "Setting $family INPUT chain policy to ACCEPT... "
$iptables_command -P INPUT ACCEPT; rv=$?
[[ $rv -ne 0 ]] && die "Error trying to change iptables policy with command '$iptables_command -P INPUT ACCEPT'."
echo "Ok."
done
echo
fi
## delete existing iptables rules matching comment "$iptables_comment"
for list_id in $list_ids; do
family="${list_id:3}"
iptables_comment="${suite_name}_${list_id}"
echo -n "Removing existing $list_type $family iptables rules for list '$list_id'... "
case "$family" in
ipv4 ) iptables_command="iptables"; iptables_save_command="iptables-save" ;;
ipv6 ) iptables_command="ip6tables"; iptables_save_command="ip6tables-save"
esac
set -o pipefail
# awk looks for rules with the comment which we use to stamp our rules
# gsub command replaces -A with 'iptables -D' to delete matching rules. system ($0) executes the command.
[ -n "$iptables_comment" ] && $iptables_save_command | \
awk -v c="$iptables_command" '$0 ~ /'"$iptables_comment"'/ {gsub(/^-A /, c" -D "); system ($0)}'; rv=$?
set +o pipefail
if [[ $rv -ne 0 ]]; then
echo "Failed."
destroy_temp_ipsets
die "Error removing existing rules for list '$list_id'."
fi
echo "Ok."
done
echo
## delete existing iptables rules matching comment "$iptables_comment_aux"
for family in $families; do
echo -n "Removing existing auxiliary $family firewall rules for '${suite_name}'... "
case "$family" in
ipv4 ) iptables_command="iptables"; iptables_save_command="iptables-save" ;;
ipv6 ) iptables_command="ip6tables"; iptables_save_command="ip6tables-save"
esac
set -o pipefail
[ -n "$iptables_comment_aux" ] && $iptables_save_command | \
awk -v c="$iptables_command" '$0 ~ /'"$iptables_comment_aux"'/ {gsub(/^-A /, c" -D "); system ($0)}'; rv=$?
set +o pipefail
if [[ $rv -ne 0 ]]; then
echo "Failed."
destroy_temp_ipsets
die "Error removing existing auxiliary rules for '$suite_name'."
fi
echo "Ok."
done
### Apply the "remove" action
if [[ "$action" = "remove" ]]; then
for list_id in $list_ids; do
perm_ipset="${suite_name}_${list_id}"
# Check if given ipset exists
matching_ipset="$(ipset list -n | grep "$perm_ipset")"
if [[ -z "$matching_ipset" ]]; then
echo "Warning: Can't remove ipset '$perm_ipset' because it doesn't appear to exist."
exitvalue="254"
else
echo -n "Destroying ipset for list '$list_id'... "
ipset destroy "$perm_ipset"; rv=$?
if [[ $rv -ne 0 ]]; then
echo "Failed."
destroy_temp_ipsets
die "Error destroying ipset '$perm_ipset'."
else
echo "Ok."
fi
fi
done
fi
# these are only useful for whitelists
if [[ "$list_type" = "whitelist" ]]; then
### Set auxiliary iptables rules
for family in $families; do
# Add rules to allow connections from the local network
echo -e "\nDetecting local $family subnets..."
set -o pipefail
localsubnets="$(sh detect-local-subnets-AIO.sh -s -f "$family" | tr '\n' ' ')" || die "Error: failed to detect local $family subnets."
set +o pipefail
if [[ -z "$localsubnets" ]]; then
destroy_temp_ipsets
die "Error: failed to detect local $family subnets."
else
echo -e "Found local $family subnets:\n${localsubnets}\n"
for localsubnet in $localsubnets; do
echo -n "Appending rule to allow traffic from local subnet '$localsubnet'... "
case "$family" in
ipv4 ) iptables_command="iptables" ;;
ipv6 ) iptables_command="ip6tables"
esac
$iptables_command -A INPUT -s "$localsubnet" -j ACCEPT -m comment --comment "${iptables_comment_aux}-localsubnet"; rv=$?
if [[ $rv -ne 0 ]]; then
destroy_temp_ipsets
die "Failed to append rule with command '$iptables_command -A INPUT -s \"$localsubnet\" -j ACCEPT'."
fi
echo "Ok."
done
fi
done
# Add rule to allow connections from the loopback interface
for family in $families; do
case "$family" in
ipv4 ) iptables_command="iptables" ;;
ipv6 ) iptables_command="ip6tables"
esac
echo -n "Inserting rule to allow $family traffic from the loopback interface... "
$iptables_command -I INPUT -i lo -j ACCEPT -m comment --comment "${iptables_comment_aux}-loopback"; rv=$?
if [[ $rv -ne 0 ]]; then
destroy_temp_ipsets
die "Failed to insert rule with command '$iptables_command -I INPUT -i lo -j ACCEPT'."
fi
echo "Ok."
done
echo
fi
# Add rule to allow established/related connections, regardless of the firewall mode (whitelist or blacklist)
for family in $families; do
echo -n "Inserting rule to allow $family established/related connections... "
case "$family" in
ipv4 ) iptables_command="iptables" ;;
ipv6 ) iptables_command="ip6tables"
esac
$iptables_command -I INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT -m comment --comment "${iptables_comment_aux}-rel-est"; rv=$?
if [[ $rv -ne 0 ]]; then
destroy_temp_ipsets
die "Failed to insert rules with command '$iptables_command -I INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT'."
fi
echo "Ok."
done
### Create permanent ipsets if they don't exist, add iptables rules for them, then swap them with temp ipsets
if [[ "$action" = "add" ]]; then
for list_id in $list_ids; do
family="$(printf '%s' "$list_id" | cut -s -d_ -f2)"
[ -z "$family" ] && { destroy_temp_ipsets; die "Error determining the family of list '$list_id'."; }
case "$family" in
ipv4 ) iptables_command="iptables" ;;
ipv6 ) iptables_command="ip6tables"
esac
perm_ipset="${suite_name}_${list_id}"
temp_ipset="${suite_name}_${list_id}_temp"
iptables_comment="${suite_name}_${list_id}"
ipset_maxelem="${ipset_maxelements[${list_id}]}"
ipset_hashsize="${ipset_hashsizes[${list_id}]}"
## check if permanent ipset already exists and is non-empty
ipset_length=$(ipset -L "$perm_ipset" 2>/dev/null | wc -l)
if [[ "$ipset_length" -ge 7 ]]; then
perm_ipset_exists="true"
debugprint "found existing permanent ipset '$perm_ipset'."
else
perm_ipset_exists=""
fi
if [[ "$ipset_length" -eq 0 ]]; then
debugprint "Ipset '$perm_ipset' doesn't exist yet."
# to avoid being dependent on ipset output staying constant down the road,
# still trying to destroy the ipset, just in case
ipset destroy "$perm_ipset" &>/dev/null
fi
## if permanent ipset doesn't exist yet, create it
if [[ ! "$perm_ipset_exists" ]]; then
# create new permanent ipset
debugprint "Creating permanent ipset '$perm_ipset'... "
ipset create "$perm_ipset" hash:net family "$family" hashsize "$ipset_hashsize" maxelem "$ipset_maxelem"; rv=$?
if [[ $rv -ne 0 ]]; then
echo "Failed."
[ -n "$iptables_comment" ] && iptables-save | awk '$0 ~ /'"$iptables_comment"'/ {gsub(/^-A /, "iptables -D "); system ($0)}'
[ -n "$iptables_comment" ] && ip6tables-save | awk '$0 ~ /'"$iptables_comment"'/ {gsub(/^-A /, "ip6tables -D "); system ($0)}'
ipset destroy "$perm_ipset"
destroy_temp_ipsets
die "Error when creating ipset '$perm_ipset'."
fi
debugprint "Ok."
fi
## append the iplist rule to the INPUT chain
if [[ "$list_type" = "whitelist" ]]; then iptables_action="ACCEPT"; else iptables_action="DROP"; fi
echo -n "Appending $list_type rule for list '$list_id' to the INPUT chain... "
$iptables_command -A INPUT -m set --match-set "$perm_ipset" src -j "$iptables_action" -m comment --comment "$iptables_comment"; rv=$?
if [[ $rv -ne 0 ]]; then
[ -n "$iptables_comment" ] && iptables-save | awk '$0 ~ /'"$iptables_comment"'/ {gsub(/^-A /, "iptables -D "); system ($0)}'
[ -n "$iptables_comment" ] && ip6tables-save | awk '$0 ~ /'"$iptables_comment"'/ {gsub(/^-A /, "ip6tables -D "); system ($0)}'
ipset destroy "$perm_ipset" &>/dev/null
destroy_temp_ipsets
die "Failed to append ipset rules with command: $iptables_command -I INPUT -m set --match-set \"$perm_ipset\" src -j \"$iptables_action\"."
fi
echo "Ok."
## swap the new (temporary) ipset with the old (permanent) ipset
echo -n "Making the new $list_type ipset for list '$list_id' permanent... "
ipset swap "$temp_ipset" "$perm_ipset"; rv=$?
if [[ $rv -ne 0 ]]; then
[ -n "$iptables_comment" ] && iptables-save | awk '$0 ~ /'"$iptables_comment"'/ {gsub(/^-A /, "iptables -D "); system ($0)}'
[ -n "$iptables_comment" ] && ip6tables-save | awk '$0 ~ /'"$iptables_comment"'/ {gsub(/^-A /, "ip6tables -D "); system ($0)}'
ipset destroy "$perm_ipset" &>/dev/null
destroy_temp_ipsets
die "Failed to swap temporary and permanent ipsets."
fi
echo "Ok."
## destroy the old ipset (now it's called temporary)
debugprint "Destroying the temporary ipset for list '$list_id'... "
ipset destroy "$temp_ipset"; rv=$?
[[ $rv -ne 0 ]] && die "Failed to destroy ipset '$temp_ipset'. Strange........."
debugprint "Ok."
done
fi
echo
### Configure iptables policies
# DROP policies are only used for whitelists
if [[ "$list_type" = "whitelist" ]]; then
if [[ ! "$nodrop" ]]; then
# set policy on INPUT and FORWARD chains to DROP
for family in $families; do
case "$family" in
ipv4 ) iptables_command="iptables" ;;
ipv6 ) iptables_command="ip6tables"
esac
echo -n "Setting $family default iptables policies for INPUT and FORWARD chains to DROP... "
$iptables_command -P INPUT DROP; rv=$?
[[ $rv -ne 0 ]] && die "Failed to change iptables policy with command '$iptables_command -P INPUT DROP'."
$iptables_command -P FORWARD DROP; rv=$?
[[ $rv -ne 0 ]] && die "Failed to change iptables policy with command '$iptables_command -P FORWARD DROP'."
echo "Ok."
done
else
echo "WARNING: nodrop was requested. Leaving INPUT and FORWARD chains with pre-install policies."
fi
else
# set policy on INPUT chain to ACCEPT
for family in $families; do
case "$family" in
ipv4 ) iptables_command="iptables" ;;
ipv6 ) iptables_command="ip6tables"
esac
echo -n "Setting $family default iptables policy for INPUT chains to ACCEPT... "
$iptables_command -P INPUT ACCEPT; rv=$?
[[ $rv -ne 0 ]] && die "Failed to change iptables policy with command '$iptables_command -P INPUT ACCEPT'."
done
fi
# This line is to simulate a simple fault and test recovery from backup
# Activates when running the script with the -t switch
[[ "$test" ]] && die "Test test test"
echo
exit "$exitvalue"