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