Commit | Line | Data |
---|---|---|
775bd287 | 1 | ### -*-sh-*- |
bfdc045d MW |
2 | ### |
3 | ### Utility functions for firewall scripts | |
4 | ### | |
5 | ### (c) 2008 Mark Wooding | |
6 | ### | |
7 | ||
8 | ###----- Licensing notice --------------------------------------------------- | |
9 | ### | |
10 | ### This program is free software; you can redistribute it and/or modify | |
11 | ### it under the terms of the GNU General Public License as published by | |
12 | ### the Free Software Foundation; either version 2 of the License, or | |
13 | ### (at your option) any later version. | |
14 | ### | |
15 | ### This program is distributed in the hope that it will be useful, | |
16 | ### but WITHOUT ANY WARRANTY; without even the implied warranty of | |
17 | ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
18 | ### GNU General Public License for more details. | |
19 | ### | |
20 | ### You should have received a copy of the GNU General Public License | |
21 | ### along with this program; if not, write to the Free Software Foundation, | |
22 | ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. | |
23 | ||
24 | m4_divert(20)m4_dnl | |
25 | ###-------------------------------------------------------------------------- | |
26 | ### Utility functions. | |
27 | ||
28 | ## doit COMMAND ARGS... | |
29 | ## | |
30 | ## If debugging, print the COMMAND and ARGS. If serious, execute them. | |
31 | run () { | |
32 | set -e | |
33 | if [ "$FW_DEBUG" ]; then echo "* $*"; fi | |
34 | if ! [ "$FW_NOACT" ]; then "$@"; fi | |
35 | } | |
36 | ||
37 | ## trace MESSAGE... | |
38 | ## | |
39 | ## If debugging, print the MESSAGE. | |
40 | trace () { | |
41 | set -e | |
42 | if [ "$FW_DEBUG" ]; then echo "$*"; fi | |
43 | } | |
44 | ||
45 | ## defport NAME NUMBER | |
46 | ## | |
47 | ## Define $port_NAME to be NUMBER. | |
48 | defport () { | |
49 | name=$1 number=$2 | |
50 | eval port_$name=$number | |
51 | } | |
52 | ||
ce79e94a MW |
53 | ## defproto NAME NUMBER |
54 | ## | |
55 | ## Define $proto_NAME to be NUMBER. | |
56 | defproto () { | |
57 | name=$1 number=$2 | |
58 | eval proto_$name=$number | |
59 | } | |
60 | ||
c70bfbbb MW |
61 | m4_divert(38)m4_dnl |
62 | ###-------------------------------------------------------------------------- | |
63 | ### Utility chains (used by function definitions). | |
64 | ||
bfdc045d MW |
65 | m4_divert(22)m4_dnl |
66 | ###-------------------------------------------------------------------------- | |
67 | ### Basic chain constructions. | |
68 | ||
0291d6d5 MW |
69 | ## ip46tables ARGS ... |
70 | ## | |
71 | ## Do the same thing for `iptables' and `ip6tables'. | |
72 | ip46tables () { | |
73 | set -e | |
74 | iptables "$@" | |
75 | ip6tables "$@" | |
76 | } | |
77 | ||
bfdc045d MW |
78 | ## clearchain CHAIN CHAIN ... |
79 | ## | |
80 | ## Ensure that the named chains exist and are empty. | |
81 | clearchain () { | |
82 | set -e | |
83 | for chain; do | |
84 | case $chain in | |
85 | *:*) table=${chain%:*} chain=${chain#*:} ;; | |
86 | *) table=filter ;; | |
87 | esac | |
e91f4bbf | 88 | run ip46tables -t $table -N $chain 2>/dev/null || : |
bfdc045d MW |
89 | done |
90 | } | |
91 | ||
9f3cffaa MW |
92 | ## makeset SET TYPE [PARAMS] |
93 | ## | |
94 | ## Ensure that the named ipset exists. Don't clear it. | |
95 | makeset () { | |
96 | set -e | |
97 | name=$1; shift | |
98 | if ipset -nL | grep -q "^Name: $name$"; then | |
99 | : | |
100 | else | |
101 | ipset -N "$name" "$@" | |
102 | fi | |
103 | } | |
104 | ||
bfdc045d MW |
105 | ## errorchain CHAIN ACTION ARGS ... |
106 | ## | |
107 | ## Make a chain which logs a message and then invokes some other action, | |
108 | ## typically REJECT. Log messages are prefixed by `fw: CHAIN'. | |
109 | errorchain () { | |
110 | set -e | |
111 | chain=$1; shift | |
112 | case $chain in | |
113 | *:*) table=${chain%:*} chain=${chain#*:} ;; | |
114 | *) table=filter ;; | |
115 | esac | |
116 | clearchain $table:$chain | |
0291d6d5 | 117 | run ip46tables -t $table -A $chain -j LOG \ |
bfdc045d | 118 | -m limit --limit 3/minute --limit-burst 10 \ |
fc10e52b | 119 | --log-prefix "fw: $chain " --log-level notice |
a188f549 MW |
120 | run ip46tables -t $table -A $chain -j "$@" \ |
121 | -m limit --limit 20/second --limit-burst 100 | |
122 | run ip46tables -t $table -A $chain -j DROP | |
bfdc045d MW |
123 | } |
124 | ||
125 | m4_divert(24)m4_dnl | |
126 | ###-------------------------------------------------------------------------- | |
127 | ### Basic option setting. | |
128 | ||
129 | ## setopt OPTION VALUE | |
130 | ## | |
131 | ## Set an IP sysctl. | |
132 | setopt () { | |
133 | set -e | |
0f6364ac MW |
134 | opt=$1 val=$2 |
135 | any=nil | |
136 | for ver in ipv4 ipv6; do | |
137 | if [ -f /proc/sys/net/$ver/$opt ]; then | |
138 | run sysctl -q net/$ver/$opt="$val" | |
139 | any=t | |
140 | fi | |
141 | done | |
142 | case $any in | |
143 | nil) echo >&2 "$0: unknown IP option $opt"; exit 1 ;; | |
144 | esac | |
bfdc045d MW |
145 | } |
146 | ||
0f6364ac | 147 | ## setdevopt OPTION VALUE [INTERFACES ...] |
bfdc045d MW |
148 | ## |
149 | ## Set an IP interface-level sysctl. | |
150 | setdevopt () { | |
151 | set -e | |
0f6364ac MW |
152 | opt=$1 val=$2; shift 2 |
153 | case "$#,$1" in | |
154 | 0, | 1,all) | |
155 | set -- $( | |
156 | seen=: | |
157 | for ver in ipv4 ipv6; do | |
158 | cd /proc/sys/net/$ver/conf | |
159 | for i in *; do | |
160 | [ -f $i/$opt ] || continue | |
161 | case "$seen" in (*:$i:*) continue ;; esac | |
162 | echo $i | |
163 | done | |
164 | done) | |
165 | ;; | |
166 | esac | |
167 | for i in "$@"; do | |
168 | any=nil | |
169 | for ver in ipv4 ipv6; do | |
170 | if [ -f /proc/sys/net/$ver/conf/$i/$opt ]; then | |
171 | any=t | |
172 | run sysctl -q net/ipv4/conf/$i/$opt="$val" | |
173 | fi | |
174 | done | |
175 | case $any in | |
176 | nil) echo >&2 "$0: unknown device option $opt"; exit 1 ;; | |
177 | esac | |
bfdc045d MW |
178 | done |
179 | } | |
180 | ||
181 | m4_divert(26)m4_dnl | |
182 | ###-------------------------------------------------------------------------- | |
183 | ### Packet filter construction. | |
184 | ||
185 | ## conntrack CHAIN | |
186 | ## | |
187 | ## Add connection tracking to CHAIN, and allow obvious stuff. | |
188 | conntrack () { | |
189 | set -e | |
190 | chain=$1 | |
0291d6d5 | 191 | run ip46tables -A $chain -p tcp -m state \ |
bfdc045d | 192 | --state ESTABLISHED,RELATED -j ACCEPT |
0291d6d5 | 193 | run ip46tables -A $chain -p tcp ! --syn -g bad-tcp |
bfdc045d MW |
194 | } |
195 | ||
ecdca131 MW |
196 | ## commonrules CHAIN |
197 | ## | |
198 | ## Add standard IP filtering rules to the CHAIN. | |
199 | commonrules () { | |
200 | set -e | |
201 | chain=$1 | |
202 | ||
203 | ## Pass fragments through, assuming that the eventual destination will sort | |
204 | ## things out properly. Except for TCP, that is, which should never be | |
c70bfbbb MW |
205 | ## fragmented. This is an extra pain for ip6tables, which doesn't provide |
206 | ## a pleasant way to detect non-initial fragments. | |
ecdca131 MW |
207 | run iptables -A $chain -p tcp -f -g tcp-fragment |
208 | run iptables -A $chain -f -j ACCEPT | |
0291d6d5 MW |
209 | run ip6tables -A $chain -p tcp -g tcp-fragment \ |
210 | -m ipv6header --soft --header frag | |
c70bfbbb | 211 | run ip6tables -A $chain -j accept-non-init-frag |
ecdca131 MW |
212 | } |
213 | ||
c70bfbbb MW |
214 | m4_divert(38)m4_dnl |
215 | ## Accept a non-initial fragment. This is only needed by IPv6, to work | |
216 | ## around a deficiency in the option parser. | |
217 | run ip6tables -N accept-non-init-frag | |
218 | run ip6tables -A accept-non-init-frag -j RETURN \ | |
219 | -m frag --fragfirst | |
220 | run ip6tables -A accept-non-init-frag -j ACCEPT | |
221 | ||
222 | m4_divert(26)m4_dnl | |
bfdc045d MW |
223 | ## allowservices CHAIN PROTO SERVICE ... |
224 | ## | |
225 | ## Add rules to allow the SERVICES on the CHAIN. | |
226 | allowservices () { | |
227 | set -e | |
228 | chain=$1 proto=$2; shift 2 | |
229 | count=0 | |
230 | list= | |
231 | for svc; do | |
232 | case $svc in | |
233 | *:*) | |
12ac65a1 | 234 | n=2 |
bfdc045d MW |
235 | left=${svc%:*} right=${svc#*:} |
236 | case $left in *[!0-9]*) eval left=\$port_$left ;; esac | |
237 | case $right in *[!0-9]*) eval right=\$port_$right ;; esac | |
238 | svc=$left:$right | |
239 | ;; | |
240 | *) | |
12ac65a1 | 241 | n=1 |
bfdc045d MW |
242 | case $svc in *[!0-9]*) eval svc=\$port_$svc ;; esac |
243 | ;; | |
244 | esac | |
245 | case $svc in | |
246 | *: | :* | "" | *[!0-9:]*) | |
12ac65a1 | 247 | echo >&2 "Bad service name" |
bfdc045d MW |
248 | exit 1 |
249 | ;; | |
250 | esac | |
251 | count=$(( $count + $n )) | |
252 | if [ $count -gt 15 ]; then | |
0291d6d5 | 253 | run ip46tables -A $chain -p $proto -m multiport -j ACCEPT \ |
bfdc045d MW |
254 | --destination-ports ${list#,} |
255 | list= count=$n | |
256 | fi | |
257 | list=$list,$svc | |
258 | done | |
259 | case $list in | |
260 | "") | |
261 | ;; | |
262 | ,*,*) | |
0291d6d5 | 263 | run ip46tables -A $chain -p $proto -m multiport -j ACCEPT \ |
bfdc045d MW |
264 | --destination-ports ${list#,} |
265 | ;; | |
12ac65a1 | 266 | *) |
0291d6d5 | 267 | run ip46tables -A $chain -p $proto -j ACCEPT \ |
bfdc045d MW |
268 | --destination-port ${list#,} |
269 | ;; | |
270 | esac | |
271 | } | |
272 | ||
273 | ## ntpclient CHAIN NTPSERVER ... | |
274 | ## | |
275 | ## Add rules to CHAIN to allow NTP with NTPSERVERs. | |
276 | ntpclient () { | |
277 | set -e | |
278 | chain=$1; shift | |
279 | for ntp; do | |
280 | run iptables -A $chain -s $ntp -j ACCEPT \ | |
281 | -p udp --source-port 123 --destination-port 123 | |
282 | done | |
283 | } | |
284 | ||
285 | ## dnsresolver CHAIN | |
286 | ## | |
287 | ## Add rules to allow CHAIN to be a DNS resolver. | |
288 | dnsresolver () { | |
289 | set -e | |
290 | chain=$1 | |
291 | for p in tcp udp; do | |
0291d6d5 | 292 | run ip46tables -A $chain -j ACCEPT \ |
bfdc045d MW |
293 | -m state --state ESTABLISHED \ |
294 | -p $p --source-port 53 | |
295 | done | |
296 | } | |
297 | ||
298 | ## openports CHAIN [MIN MAX] | |
299 | ## | |
300 | ## Add rules to CHAIN to allow the open ports. | |
301 | openports () { | |
302 | set -e | |
303 | chain=$1; shift | |
304 | [ $# -eq 0 ] && set -- $open_port_min $open_port_max | |
0291d6d5 MW |
305 | run ip46tables -A $chain -p tcp -g interesting --destination-port $1:$2 |
306 | run ip46tables -A $chain -p udp -g interesting --destination-port $1:$2 | |
bfdc045d MW |
307 | } |
308 | ||
309 | m4_divert(28)m4_dnl | |
310 | ###-------------------------------------------------------------------------- | |
311 | ### Packet classification. | |
312 | ||
313 | ## defbitfield NAME WIDTH | |
314 | ## | |
315 | ## Defines MASK_NAME and BIT_NAME symbolic constants for dealing with | |
316 | ## bitfields: x << BIT_NAME yields the value x in the correct position, and | |
317 | ## ff & MASK_NAME extracts the corresponding value. | |
318 | defbitfield () { | |
319 | set -e | |
320 | name=$1 width=$2 | |
321 | eval MASK_$name=$(( (1 << $width) - 1 << $bitindex )) | |
322 | eval BIT_$name=$bitindex | |
323 | bitindex=$(( $bitindex + $width )) | |
324 | } | |
325 | ||
326 | ## Define the layout of the bitfield. | |
327 | bitindex=0 | |
328 | defbitfield MASK 16 | |
329 | defbitfield FROM 4 | |
330 | defbitfield TO 4 | |
331 | ||
332 | ## defnetclass NAME FORWARD-TO... | |
333 | ## | |
334 | ## Defines a netclass called NAME, which is allowed to forward to the | |
335 | ## FORWARD-TO netclasses. | |
336 | ## | |
337 | ## For each netclass, constants from_NAME and to_NAME are defined as the | |
338 | ## appropriate values in the FROM and TO fields (i.e., not including any mask | |
339 | ## bits). | |
340 | ## | |
341 | ## This function also establishes mangle chains mark-from-NAME and | |
342 | ## mark-to-NAME for applying the appropriate mark bits to the packet. | |
343 | ## | |
344 | ## Because it needs to resolve forward references, netclasses must be defined | |
345 | ## in a two-pass manner, using a loop of the form | |
346 | ## | |
347 | ## for pass in 1 2; do netclassindex=0; ...; done | |
348 | netclassess= | |
349 | defnetclass () { | |
350 | set -e | |
351 | name=$1; shift | |
352 | case $pass in | |
353 | 1) | |
354 | ||
355 | ## Pass 1. Establish the from_NAME and to_NAME constants, and the | |
356 | ## netclass's mask bit. | |
357 | eval from_$name=$(( $netclassindex << $BIT_FROM )) | |
358 | eval to_$name=$(( $netclassindex << $BIT_TO )) | |
359 | eval _mask_$name=$(( 1 << ($netclassindex + $BIT_MASK) )) | |
360 | nets="$nets $name" | |
361 | ;; | |
362 | 2) | |
363 | ||
364 | ## Pass 2. Compute the actual from and to values. We're a little bit | |
365 | ## clever during source classification, and set the TO field to | |
366 | ## all-bits-one, so that destination classification needs only a single | |
367 | ## AND operation. | |
368 | from=$(( ($netclassindex << $BIT_FROM) + (0xf << $BIT_TO) )) | |
369 | for net; do | |
370 | eval bit=\$_mask_$net | |
371 | from=$(( $from + $bit )) | |
372 | done | |
373 | to=$(( ($netclassindex << $BIT_TO) + \ | |
12ac65a1 | 374 | (0xf << $BIT_FROM) + \ |
bfdc045d MW |
375 | (1 << ($netclassindex + $BIT_MASK)) )) |
376 | trace "from $name --> set $(printf %x $from)" | |
377 | trace " to $name --> and $(printf %x $from)" | |
378 | ||
379 | ## Now establish the mark-from-NAME and mark-to-NAME chains. | |
380 | clearchain mangle:mark-from-$name mangle:mark-to-$name | |
0291d6d5 MW |
381 | run ip46tables -t mangle -A mark-from-$name -j MARK --set-mark $from |
382 | run ip46tables -t mangle -A mark-to-$name -j MARK --and-mark $to | |
bfdc045d MW |
383 | ;; |
384 | esac | |
385 | netclassindex=$(( $netclassindex + 1 )) | |
386 | } | |
387 | ||
46be9bde | 388 | ## defiface NAME[,NAME,...] NETCLASS:NETWORK/MASK... |
bfdc045d | 389 | ## |
46be9bde MW |
390 | ## Declares network interfaces with the given NAMEs and associates with them |
391 | ## a number of reachable networks. During source classification, a packet | |
392 | ## arriving on interface NAME from an address in NETWORK/MASK is classified | |
393 | ## as coming from to NETCLASS. During destination classification, all | |
394 | ## packets going to NETWORK/MASK are classified as going to NETCLASS, | |
395 | ## regardless of interface (which is good, because the outgoing interface | |
396 | ## hasn't been determined yet). | |
bfdc045d MW |
397 | ## |
398 | ## As a special case, the NETWORK/MASK can be the string `default', which | |
399 | ## indicates that all addresses not matched elsewhere should be considered. | |
400 | ifaces=: | |
3a68f688 | 401 | defaultifaces="" |
0291d6d5 | 402 | allnets= allnets6= |
bfdc045d MW |
403 | defiface () { |
404 | set -e | |
46be9bde MW |
405 | names=$1; shift |
406 | seen=: | |
407 | for name in $(echo $names | sed 'y/,/ /'); do | |
408 | case $seen in *:"$name":*) continue ;; esac | |
409 | seen=$seen$name: | |
410 | case $ifaces in | |
411 | *:"$name":*) ;; | |
bfdc045d | 412 | *) |
46be9bde MW |
413 | clearchain mangle:in-$name |
414 | run ip46tables -t mangle -A in-classify -i $name -g in-$name | |
bfdc045d MW |
415 | ;; |
416 | esac | |
46be9bde MW |
417 | ifaces=$ifaces$name: |
418 | for item; do | |
419 | netclass=${item%:*} addr=${item#*:} | |
420 | case $addr in | |
421 | default) | |
3a68f688 MW |
422 | case "$defaultifaces,$defaultclass" in |
423 | ,* | *,$netclass) | |
424 | defaultifaces="$defaultifaces $name" | |
425 | defaultclass=$netclass | |
426 | ;; | |
427 | *) | |
428 | echo >&2 "$0: inconsistent default netclasses" | |
429 | exit 1 | |
430 | ;; | |
431 | esac | |
46be9bde MW |
432 | ;; |
433 | *:*) | |
434 | run ip6tables -t mangle -A in-$name -g mark-from-$netclass \ | |
435 | -s $addr | |
436 | run ip6tables -t mangle -A out-classify -g mark-to-$netclass \ | |
437 | -d $addr | |
438 | allnets6="$allnets6 $name:$addr" | |
439 | ;; | |
440 | *) | |
441 | run iptables -t mangle -A in-$name -g mark-from-$netclass \ | |
442 | -s $addr | |
443 | run iptables -t mangle -A out-classify -g mark-to-$netclass \ | |
444 | -d $addr | |
445 | allnets="$allnets $name:$addr" | |
446 | ;; | |
447 | esac | |
448 | done | |
bfdc045d MW |
449 | done |
450 | } | |
451 | ||
452 | ## defvpn IFACE CLASS NET HOST:ADDR ... | |
453 | ## | |
454 | ## Defines a VPN interface. If the interface has the form `ROOT+' (i.e., a | |
455 | ## netfilter wildcard) then define a separate interface ROOTHOST routing to | |
456 | ## ADDR; otherwise just write a blanket rule allowing the whole NET. All | |
457 | ## addresses concerned are put in the named CLASS. | |
458 | defvpn () { | |
459 | set -e | |
460 | iface=$1 class=$2 net=$3; shift 3 | |
461 | case $iface in | |
462 | *-+) | |
463 | root=${iface%+} | |
464 | for host; do | |
0291d6d5 | 465 | name=${host%%:*} addr=${host#*:} |
bfdc045d MW |
466 | defiface $root$name $class:$addr |
467 | done | |
468 | ;; | |
469 | *) | |
470 | defiface $iface $class:$net | |
471 | ;; | |
472 | esac | |
473 | } | |
474 | ||
475 | m4_divert(-1) | |
476 | ###----- That's all, folks -------------------------------------------------- |