Overhaul address classification.
[firewall] / functions.m4
index 555072e..891b037 100644 (file)
@@ -58,6 +58,20 @@ defproto () {
   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).
@@ -309,6 +323,27 @@ openports () {
 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_fwd_NET                List of other networks that this one forwards to.
+### 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
 ##
@@ -385,91 +420,186 @@ defnetclass () {
   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', `forwards', 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
-         ;;
-       *:*)
-         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"
-         ;;
-       *)
-         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"
-         ;;
-      esac
-    done
   done
 }
 
-## defvpn IFACE CLASS NET HOST:ADDR ...
+## forwards 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
-      done
-      ;;
-    *)
-      defiface $iface $class:$net
-      ;;
+## Declare that packets from this network are forwarded to the other NETs.
+forwards () {
+  eval "net_fwd_$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 $a in ::*) aa=$addr$a ;; *) aa=$a ;; 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=endsys
+}
+
+## router
+##
+## Declare the host to be a router, so it should forward packets and so on.
+router () {
+  eval host_type_$host=router
+}
+
+## 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
+}
+
+## net_interfaces HOST NET
+##
+## 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
+    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 fwd=\$net_fwd_$net
+      for n in $fwd; 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)