eval proto_$name=$number
}
+## addword VAR WORD
+##
+## Adds WORD to the value of the shell variable VAR, if it's not there
+## already. Words are separated by a single space; no leading or trailing
+## spaces are introduced.
+addword () {
+ var=$1 word=$2
+ eval val=\$$var
+ case " $val " in
+ *" $word "*) ;;
+ *) eval "$var=\${$var:+\$val }\$word" ;;
+ esac
+}
+
m4_divert(38)m4_dnl
###--------------------------------------------------------------------------
### Utility chains (used by function definitions).
-m4_divert(22)m4_dnl
+m4_divert(20)m4_dnl
###--------------------------------------------------------------------------
### Basic chain constructions.
## Ensure that the named chains exist and are empty.
clearchain () {
set -e
- for chain; do
- case $chain in
- *:*) table=${chain%:*} chain=${chain#*:} ;;
+ for _chain; do
+ case $_chain in
+ *:*) table=${_chain%:*} _chain=${_chain#*:} ;;
*) table=filter ;;
esac
- run ip46tables -t $table -N $chain 2>/dev/null || :
+ run ip46tables -t $table -N $_chain 2>/dev/null || :
done
}
+## makeset SET TYPE [PARAMS]
+##
+## Ensure that the named ipset exists. Don't clear it.
+makeset () {
+ set -e
+ name=$1; shift
+ v=$(ipset --version)
+ createp=t
+ case "$v" in
+ "ipset v4"*)
+ if ipset -nL | grep -q "^Name: $name\$"; then createp=nil; fi
+ ;;
+ *)
+ if ipset -n -L | grep -q "^$name\$"; then createp=nil; fi
+ ;;
+ esac
+ case $createp in
+ t) ipset -N "$name" "$@" ;;
+ esac
+}
+
## errorchain CHAIN ACTION ARGS ...
##
## Make a chain which logs a message and then invokes some other action,
clearchain $table:$chain
run ip46tables -t $table -A $chain -j LOG \
-m limit --limit 3/minute --limit-burst 10 \
- --log-prefix "fw: $chain " --log-level notice
+ --log-prefix "fw: $chain " --log-level notice || :
run ip46tables -t $table -A $chain -j "$@" \
-m limit --limit 20/second --limit-burst 100
run ip46tables -t $table -A $chain -j DROP
}
-m4_divert(24)m4_dnl
+m4_divert(20)m4_dnl
###--------------------------------------------------------------------------
### Basic option setting.
for ver in ipv4 ipv6; do
if [ -f /proc/sys/net/$ver/conf/$i/$opt ]; then
any=t
- run sysctl -q net/ipv4/conf/$i/$opt="$val"
+ run sysctl -q net/$ver/conf/$i/$opt="$val"
fi
done
case $any in
done
}
-m4_divert(26)m4_dnl
+m4_divert(20)m4_dnl
###--------------------------------------------------------------------------
### Packet filter construction.
run ip6tables -N accept-non-init-frag
run ip6tables -A accept-non-init-frag -j RETURN \
-m frag --fragfirst
-run ip6tables -A accept-non-init-frag -j ACCEPT
+run ip6tables -A accept-non-init-frag -j ACCEPT \
+ -m ipv6header --header frag
-m4_divert(26)m4_dnl
+m4_divert(20)m4_dnl
## allowservices CHAIN PROTO SERVICE ...
##
## Add rules to allow the SERVICES on the CHAIN.
## Add rules to CHAIN to allow NTP with NTPSERVERs.
ntpclient () {
set -e
- chain=$1; shift
+ ntpchain=$1; shift
+
+ clearchain ntp-servers
for ntp; do
- run iptables -A $chain -s $ntp -j ACCEPT \
- -p udp --source-port 123 --destination-port 123
+ case $ntp in *:*) ipt=ip6tables ;; *) ipt=iptables ;; esac
+ run $ipt -A ntp-servers -j ACCEPT -s $ntp;
done
+ run ip46tables -A $ntpchain -j ntp-servers \
+ -p udp --source-port 123 --destination-port 123
}
## dnsresolver CHAIN
done
}
+## dnsserver CHAIN
+##
+## Add rules to allow CHAIN to be a DNS server.
+dnsserver () {
+ set -e
+ chain=$1
+
+ ## Allow TCP access. Hitting us with SYNs will make us deploy SYN cookies,
+ ## but that's tolerable.
+ run ip46tables -A $chain -j ACCEPT -p tcp --destination-port 53
+
+ ## Avoid being a DDoS amplifier by rate-limiting incoming DNS queries.
+ clearchain $chain-udp-dns
+ run ip46tables -A $chain-udp-dns -j ACCEPT \
+ -m limit --limit 20/second --limit-burst 300
+ run ip46tables -A $chain-udp-dns -g dns-rate-limit
+ run ip46tables -A $chain -j $chain-udp-dns \
+ -p udp --destination-port 53
+}
+
## openports CHAIN [MIN MAX]
##
## Add rules to CHAIN to allow the open ports.
run ip46tables -A $chain -p udp -g interesting --destination-port $1:$2
}
-m4_divert(28)m4_dnl
+bcp38_setup=:
+bcp38 () {
+ ipv=$1 ifname=$2; shift 2
+ ## Add rules for BCP38 egress filtering for IP version IPV (either 4 or 6).
+ ## IFNAME is the outgoing interface; the remaining arguments are network
+ ## prefixes.
+
+ ## Sort out which command we're using
+ case $ipv in
+ 4) ipt=iptables ;;
+ 6) ipt=ip6tables ;;
+ *) echo >&2 "Unknown IP version $ipv"; exit 1 ;;
+ esac
+
+ ## If we've not set up the error chain then do that.
+ case $bcp38_setup in
+ :)
+ errorchain bcp38 DROP
+ clearchain bcp38-check
+ ip46tables -A bcp38-check -g bcp38
+ ;;
+ esac
+
+ ## Stitch our egress filter into the outbound chains if we haven't done
+ ## that yet. Do this for both IP versions: if we're only ever given
+ ## IPv6 addresses for a particular interface then we assume that IPv4
+ ## packets aren't allowed on it at all.
+ case $bcp38_setup in
+ *:$ifname:*) ;;
+ *)
+ run ip46tables -A OUTPUT -j bcp38-check -o $ifname
+ case $forward in
+ 1) run ip46tables -A FORWARD -j bcp38-check -o $ifname ;;
+ esac
+ bcp38_setup=$bcp38_setup$ifname:
+ ;;
+ esac
+
+ ## Finally, add in our allowed networks.
+ for i in "$@"; do
+ run $ipt -I bcp38-check -j RETURN -s $i
+ done
+}
+
+m4_divert(20)m4_dnl
###--------------------------------------------------------------------------
### Packet classification.
+###
+### See `classify.m4' for an explanation of how the firewall machinery for
+### packet classification works.
+###
+### A list of all network names is kept in `allnets'. For each network NET,
+### shell variables are defined describing their properties.
+###
+### net_class_NET The class of the network, as defined by
+### `defnetclass'.
+### net_inet_NET List of IPv4 address ranges in the network.
+### net_inet6_NET List of IPv6 address ranges in the network.
+### net_via_NET List of other networks that this one forwards via.
+### net_hosts_NET List of hosts known to be in the network.
+### host_inet_HOST IPv4 address of the named HOST.
+### host_inet6_HOST IPv6 address of the named HOST.
+###
+### Similarly, a list of hosts is kept in `allhosts', and for each host HOST,
+### a shell variables are defined:
+###
+### host_ifaces_HOST List of interfaces for this host and the networks
+### they attach to, in the form IFACE=NET.
## defbitfield NAME WIDTH
##
## Pass 1. Establish the from_NAME and to_NAME constants, and the
## netclass's mask bit.
+ trace "netclass $name = $netclassindex"
eval from_$name=$(( $netclassindex << $BIT_FROM ))
eval to_$name=$(( $netclassindex << $BIT_TO ))
- eval _mask_$name=$(( 1 << ($netclassindex + $BIT_MASK) ))
+ eval fwd_$name=$(( 1 << ($netclassindex + $BIT_MASK) ))
nets="$nets $name"
;;
2)
- ## Pass 2. Compute the actual from and to values. We're a little bit
- ## clever during source classification, and set the TO field to
- ## all-bits-one, so that destination classification needs only a single
- ## AND operation.
- from=$(( ($netclassindex << $BIT_FROM) + (0xf << $BIT_TO) ))
+ ## Pass 2. Compute the actual from and to values. This is fiddly:
+ ## we want to preserve the other flags.
+ from=$(( ($netclassindex << $BIT_FROM) ))
+ frommask=$(( $MASK_FROM | $MASK_MASK ))
for net; do
- eval bit=\$_mask_$net
+ eval bit=\$fwd_$net
from=$(( $from + $bit ))
done
- to=$(( ($netclassindex << $BIT_TO) + \
- (0xf << $BIT_FROM) + \
- (1 << ($netclassindex + $BIT_MASK)) ))
- trace "from $name --> set $(printf %x $from)"
- trace " to $name --> and $(printf %x $from)"
+ to=$(( ($netclassindex << $BIT_TO) ))
+ tomask=$(( $MASK_TO | $MASK_MASK ^ (1 << ($netclassindex + $BIT_MASK)) ))
+ trace "from $name --> set $(printf %08x/%08x $from $frommask)"
+ trace " to $name --> set $(printf %08x/%08x $to $tomask)"
## Now establish the mark-from-NAME and mark-to-NAME chains.
clearchain mangle:mark-from-$name mangle:mark-to-$name
- run ip46tables -t mangle -A mark-from-$name -j MARK --set-mark $from
- run ip46tables -t mangle -A mark-to-$name -j MARK --and-mark $to
+ run ip46tables -t mangle -A mark-from-$name -j MARK \
+ --set-xmark $from/$frommask
+ run ip46tables -t mangle -A mark-to-$name -j MARK \
+ --set-xmark $to/$tomask
;;
esac
netclassindex=$(( $netclassindex + 1 ))
}
-## defiface NAME[,NAME,...] NETCLASS:NETWORK/MASK...
-##
-## Declares network interfaces with the given NAMEs and associates with them
-## a number of reachable networks. During source classification, a packet
-## arriving on interface NAME from an address in NETWORK/MASK is classified
-## as coming from to NETCLASS. During destination classification, all
-## packets going to NETWORK/MASK are classified as going to NETCLASS,
-## regardless of interface (which is good, because the outgoing interface
-## hasn't been determined yet).
-##
-## As a special case, the NETWORK/MASK can be the string `default', which
-## indicates that all addresses not matched elsewhere should be considered.
-ifaces=:
-defaultifaces=""
-allnets= allnets6=
-defiface () {
- set -e
- names=$1; shift
- seen=:
- for name in $(echo $names | sed 'y/,/ /'); do
- case $seen in *:"$name":*) continue ;; esac
- seen=$seen$name:
- case $ifaces in
- *:"$name":*) ;;
- *)
- clearchain mangle:in-$name
- run ip46tables -t mangle -A in-classify -i $name -g in-$name
- ;;
+## defnet NET CLASS
+##
+## Define a network. Follow by calls to `addr', `via', etc. to define
+## properties of the network. Networks are processed in order, so if their
+## addresses overlap then the more specific addresses should be defined
+## earlier.
+defnet () {
+ net=$1 class=$2
+ addword allnets $net
+ eval net_class_$1=\$class
+}
+
+## addr ADDRESS/LEN ...
+##
+## Define addresses for the network being defined. ADDRESSes are in
+## colon-separated IPv6 or dotted-quad IPv4 form.
+addr () {
+ for i in "$@"; do
+ case "$i" in
+ *:*) addword net_inet6_$net $i ;;
+ *) addword net_inet_$net $i ;;
esac
- ifaces=$ifaces$name:
- for item; do
- netclass=${item%:*} addr=${item#*:}
- case $addr in
- default)
- case "$defaultifaces,$defaultclass" in
- ,* | *,$netclass)
- defaultifaces="$defaultifaces $name"
- defaultclass=$netclass
- ;;
- *)
- echo >&2 "$0: inconsistent default netclasses"
- exit 1
- ;;
- esac
+ done
+}
+
+## via NET ...
+##
+## Declare that packets from this network are forwarded to the other NETs.
+via () {
+ eval "net_via_$net=\"$*\""
+}
+
+## noxit NET ...
+##
+## Declare that packets from this network must not be forwarded to the other
+## NETs.
+noxit () {
+ eval "net_noxit_$net=\"$*\""
+}
+
+## host HOST ADDR ...
+##
+## Define the address of an individual host on the current network. The
+## ADDRs may be full IPv4 or IPv6 addresses, or offsets from the containing
+## network address, which is a simple number for IPv4, or a suffix beginning
+## with `::' for IPv6. If an IPv6 base address is provided for the network
+## but not for the host then the host's IPv4 address is used as a suffix.
+host () {
+ name=$1; shift
+
+ ## Work out which addresses we've actually been given.
+ unset a6
+ for i in "$@"; do
+ case "$i" in ::*) a6=$i ;; *) a=$i ;; esac
+ done
+ case "${a+t}" in
+ t) ;;
+ *) echo >&2 "$0: no address for $name"; exit 1 ;;
+ esac
+ case "${a6+t}" in t) ;; *) a6=::$a ;; esac
+
+ ## Work out the IPv4 address.
+ eval nn=\$net_inet_$net
+ for n in $nn; do
+ addr=${n%/*}
+ base=${addr%.*}
+ offset=${addr##*.}
+ case $a in *.*) aa=$a ;; *) aa=$base.$(( $offset + $a )) ;; esac
+ eval host_inet_$name=$aa
+ done
+
+ ## Work out the IPv6 address.
+ eval nn=\$net_inet6_$net
+ for n in $nn; do
+ addr=${n%/*}
+ base=${addr%::*}
+ case $a6 in ::*) aa=$base$a6 ;; *) aa=$a6 ;; esac
+ eval host_inet6_$name=$aa
+ done
+
+ ## Remember the host in the list.
+ addword net_hosts_$net $name
+}
+
+## defhost NAME
+##
+## Define a new host. Follow by calls to `iface' to define the host's
+## interfaces.
+defhost () {
+ host=$1
+ addword allhosts $host
+ eval host_type_$host=server
+}
+
+## hosttype TYPE
+##
+## Declare the host to have the given type.
+hosttype () {
+ type=$1
+ case $type in
+ router | server | client) ;;
+ *) echo >&2 "$0: bad host type \`$type'"; exit 1 ;;
+ esac
+ eval host_type_$host=$type
+}
+
+## iface IFACE NET ...
+##
+## Define a host's interfaces. Specifically, declares that the host has an
+## interface IFACE attached to the listed NETs.
+iface () {
+ name=$1; shift
+ for net in "$@"; do
+ addword host_ifaces_$host $name=$net
+ done
+}
+
+## matchnets OPT WIN FLAGS PREPARE BASE SUFFIX NEXT NET [NET ...]
+##
+## Build rules which match a particular collection of networks.
+##
+## Specifically, use the address-comparison operator OPT (typically `-s' or
+## `-d') to match the addresses of each NET, writing the rules to the chain
+## BASESUFFIX. If we find a match, dispatch to WIN-CLASS, where CLASS is the
+## class of the matching network. In order to deal with networks containing
+## negative address ranges, more chains may need to be constructed; they will
+## be named BASE#Q for sequence numbers Q starting with NEXT. All of this
+## happens on the `mangle' table, and there isn't (currently) a way to tweak
+## this.
+##
+## The FLAGS gather additional interesting information about the job,
+## separated by colons. The only flag currently is :default: which means
+## that the default network was listed.
+##
+## Finally, there is a hook PREPARE which is called just in advance of
+## processing the final network, passing it the argument FLAGS. (The PREPARE
+## string will be subjected to shell word-splitting, so it can provide some
+## arguments of its own if it wants.) It should set `mode' to indicate how
+## the chain should be finished.
+##
+## goto If no networks matched, then issue a final `goto' to the
+## chain named by the variable `fail'.
+##
+## call Run `$finish CHAIN' to write final rules to the named CHAIN
+## (which may be suffixed from the original BASE argument if
+## this was necessary). This function will arrange to call
+## these rules if no networks match.
+##
+## ret If no network matches then return (maybe by falling off the
+## end of the chain).
+matchnets () {
+ local opt win flags prepare base suffix next net lose splitp
+ opt=$1 win=$2 flags=$3 prepare=$4 base=$5 suffix=$6 next=$7 net=$8
+ shift 8
+
+ ## If this is the default network, then set the flag.
+ case "$net" in default) flags=${flags}default: ;; esac
+
+ ## Do an initial pass over the addresses to see whether there are any
+ ## negative ranges. If so, we'll need to split. See also the standard
+ ## joke about soup.
+ splitp=nil
+ eval "addrs=\"\$net_inet_$net \$net_inet6_$net\""
+ for a in $addrs; do case $a in !*) splitp=t; break ;; esac; done
+
+ trace "MATCHNETS [splitp $splitp] $opt $win $flags [$prepare] $base $suffix $next : $net $*"
+
+ ## Work out how to handle matches against negative address ranges. If this
+ ## is the last network, invoke the PREPARE hook to find out. Otherwise, if
+ ## we have to split the chain, recursively build the target here.
+ case $splitp,$# in
+ t,0 | nil,0)
+ $prepare $flags
+ case $splitp,$mode in
+ *,goto)
+ lose="-g $fail"
;;
- *:*)
- run ip6tables -t mangle -A in-$name -g mark-from-$netclass \
- -s $addr
- run ip6tables -t mangle -A out-classify -g mark-to-$netclass \
- -d $addr
- allnets6="$allnets6 $name:$addr"
+ *,ret)
+ lose="-j RETURN"
;;
- *)
- run iptables -t mangle -A in-$name -g mark-from-$netclass \
- -s $addr
- run iptables -t mangle -A out-classify -g mark-to-$netclass \
- -d $addr
- allnets="$allnets $name:$addr"
+ t,call)
+ clearchain mangle:$base#$next
+ lose="-g $base#$next"
+ ;;
+ nil,call)
;;
esac
- done
+ ;;
+ t,*)
+ clearchain mangle:$base#$next
+ matchnets $opt $win $flags "$prepare" \
+ $base \#$next $(( $next + 1 )) "$@"
+ lose="-g $base#$next" mode=goto
+ ;;
+ *)
+ mode=continue
+ ;;
+ esac
+
+ ## Populate the chain with rules to match the necessary networks.
+ eval addr=\$net_inet_$net addr6=\$net_inet6_$net class=\$net_class_$net
+ for a in $addr; do
+ case $a in
+ !*) run iptables -t mangle -A $base$suffix $lose $opt ${a#!} ;;
+ *) run iptables -t mangle -A $base$suffix -g $win-$class $opt $a ;;
+ esac
done
+ for a in $addr6; do
+ case $a in
+ !*) run ip6tables -t mangle -A $base$suffix $lose $opt ${a#!} ;;
+ *) run ip6tables -t mangle -A $base$suffix -g $win-$class $opt $a ;;
+ esac
+ done
+
+ ## Wrap up the chain appropriately. If we didn't split and there are more
+ ## networks to handle then append the necessary rules now. (If we did
+ ## split, then we already wrote the rules for them above.) If there are no
+ ## more networks then consult the `mode' setting to find out what to do.
+ case $splitp,$#,$mode in
+ *,0,ret) ;;
+ *,*,goto) run ip46tables -t mangle -A $base$suffix $lose ;;
+ t,0,call) $finish $base#$next ;;
+ nil,0,call) $finish $base$suffix ;;
+ nil,*,*)
+ matchnets $opt $win $flags "$prepare" $base "$suffix" $next "$@"
+ ;;
+ esac
}
-## defvpn IFACE CLASS NET HOST:ADDR ...
+## net_interfaces HOST NET
##
-## Defines a VPN interface. If the interface has the form `ROOT+' (i.e., a
-## netfilter wildcard) then define a separate interface ROOTHOST routing to
-## ADDR; otherwise just write a blanket rule allowing the whole NET. All
-## addresses concerned are put in the named CLASS.
-defvpn () {
- set -e
- iface=$1 class=$2 net=$3; shift 3
- case $iface in
- *-+)
- root=${iface%+}
- for host; do
- name=${host%%:*} addr=${host#*:}
- defiface $root$name $class:$addr
+## Determine the interfaces on which packets may plausibly arrive from the
+## named NET. Returns `-' if no such interface exists.
+##
+## This algorithm is not very clever. It's just about barely good enough to
+## deduce transitivity through a simple routed network; with complicated
+## networks, it will undoubtedly give wrong answers. Check the results
+## carefully, and, if necessary, list the connectivity explicitly; use the
+## special interface `-' for networks you know shouldn't send packets to a
+## host.
+net_interfaces () {
+ host=$1 startnet=$2
+
+ ## Determine the locally attached networks.
+ targets=:
+ eval ii=\$host_ifaces_$host
+ for i in $ii; do targets=$targets$i:; done
+
+ ## Determine the transitivity.
+ seen=:
+ nets=$startnet
+ while :; do
+
+ ## First pass. Determine whether any of the networks we're considering
+ ## are in the target set. If they are, then return the corresponding
+ ## interfaces.
+ found=""
+ for net in $nets; do
+ tg=$targets
+ while :; do
+ any=nil
+ case $tg in
+ *"=$net:"*)
+ n=${tg%=$net:*}; tg=${n%:*}:; n=${n##*:}
+ addword found $n
+ any=t
+ ;;
+ esac
+ case $any in nil) break ;; esac
done
- ;;
- *)
- defiface $iface $class:$net
- ;;
- esac
+ done
+ case "$found" in ?*) echo $found; return ;; esac
+
+ ## No joy. Determine the set of networks which (a) these ones can
+ ## forward to, and (b) that we've not considered already. These are the
+ ## nets we'll consider next time around.
+ nextnets=""
+ any=nil
+ for net in $nets; do
+ eval via=\$net_via_$net
+ for n in $via; do
+ case $seen in *":$n:"*) continue ;; esac
+ seen=$seen$n:
+ eval noxit=\$net_noxit_$n
+ case " $noxit " in *" $startnet "*) continue ;; esac
+ case " $nextnets " in
+ *" $n "*) ;;
+ *) addword nextnets $n; any=t ;;
+ esac
+ done
+ done
+
+ ## If we've run out of networks then there's no reachability. Return a
+ ## failure.
+ case $any in nil) echo -; return ;; esac
+ nets=$nextnets
+ done
}
m4_divert(-1)