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