local.m4: Fix whitespace oddity.
[firewall] / functions.m4
index 484c30d..765a94d 100644 (file)
@@ -50,11 +50,33 @@ defport () {
   eval port_$name=$number
 }
 
+## defproto NAME NUMBER
+##
+## Define $proto_NAME to be NUMBER.
+defproto () {
+  name=$1 number=$2
+  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.
 
@@ -72,15 +94,36 @@ ip46tables () {
 ## 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
+    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,
@@ -95,11 +138,13 @@ errorchain () {
   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
-  run ip46tables -t $table -A $chain -j "$@"
+         --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.
 
@@ -108,23 +153,54 @@ m4_divert(24)m4_dnl
 ## Set an IP sysctl.
 setopt () {
   set -e
-  opt=$1; shift; val=$*
-  run sysctl -q net/ipv4/$opt="$val"
+  opt=$1 val=$2
+  any=nil
+  for ver in ipv4 ipv6; do
+    if [ -f /proc/sys/net/$ver/$opt ]; then
+      run sysctl -q net/$ver/$opt="$val"
+      any=t
+    fi
+  done
+  case $any in
+    nil) echo >&2 "$0: unknown IP option $opt"; exit 1 ;;
+  esac
 }
 
-## setdevopt OPTION VALUE
+## setdevopt OPTION VALUE [INTERFACES ...]
 ##
 ## Set an IP interface-level sysctl.
 setdevopt () {
   set -e
-  opt=$1; shift; val=$*
-  for i in /proc/sys/net/ipv4/conf/*; do
-    [ -f $i/$opt ] &&
-      run sysctl -q net/ipv4/conf/${i#/proc/sys/net/ipv4/conf/}/$opt="$val"
+  opt=$1 val=$2; shift 2
+  case "$#,$1" in
+    0, | 1,all)
+      set -- $(
+       seen=:
+       for ver in ipv4 ipv6; do
+         cd /proc/sys/net/$ver/conf
+         for i in *; do
+           [ -f $i/$opt ] || continue
+           case "$seen" in (*:$i:*) continue ;; esac
+           echo $i
+         done
+       done)
+      ;;
+  esac
+  for i in "$@"; do
+    any=nil
+    for ver in ipv4 ipv6; do
+      if [ -f /proc/sys/net/$ver/conf/$i/$opt ]; then
+       any=t
+       run sysctl -q net/$ver/conf/$i/$opt="$val"
+      fi
+    done
+    case $any in
+      nil) echo >&2 "$0: unknown device option $opt"; exit 1 ;;
+    esac
   done
 }
 
-m4_divert(26)m4_dnl
+m4_divert(20)m4_dnl
 ###--------------------------------------------------------------------------
 ### Packet filter construction.
 
@@ -163,9 +239,10 @@ m4_divert(38)m4_dnl
 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.
@@ -221,11 +298,15 @@ allowservices () {
 ## 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
@@ -241,6 +322,26 @@ dnsresolver () {
   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.
@@ -252,9 +353,74 @@ openports () {
   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
 ##
@@ -300,115 +466,335 @@ defnetclass () {
 
       ## 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=:
-defaultiface=none
-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)
-         defaultiface=$name
-         defaultclass=$netclass
-         run ip46tables -t mangle -A out-classify -g mark-to-$netclass
+  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)