bookends.m4: Provide a hook chain for fail2ban.
[firewall] / functions.m4
index 80caf1d..5cc70f8 100644 (file)
@@ -1,4 +1,4 @@
-### -*-m4-*-
+### -*-sh-*-
 ###
 ### Utility functions for firewall scripts
 ###
@@ -50,10 +50,31 @@ defport () {
   eval port_$name=$number
 }
 
+## defproto NAME NUMBER
+##
+## Define $proto_NAME to be NUMBER.
+defproto () {
+  name=$1 number=$2
+  eval proto_$name=$number
+}
+
+m4_divert(38)m4_dnl
+###--------------------------------------------------------------------------
+### Utility chains (used by function definitions).
+
 m4_divert(22)m4_dnl
 ###--------------------------------------------------------------------------
 ### Basic chain constructions.
 
+## ip46tables ARGS ...
+##
+## Do the same thing for `iptables' and `ip6tables'.
+ip46tables () {
+  set -e
+  iptables "$@"
+  ip6tables "$@"
+}
+
 ## clearchain CHAIN CHAIN ...
 ##
 ## Ensure that the named chains exist and are empty.
@@ -64,7 +85,7 @@ clearchain () {
       *:*) table=${chain%:*} chain=${chain#*:} ;;
       *) table=filter ;;
     esac
-    run iptables -t $table -N $chain
+    run ip46tables -t $table -N $chain
   done
 }
 
@@ -80,10 +101,10 @@ errorchain () {
     *) table=filter ;;
   esac
   clearchain $table:$chain
-  run iptables -t $table -A $chain -j LOG \
+  run ip46tables -t $table -A $chain -j LOG \
          -m limit --limit 3/minute --limit-burst 10 \
          --log-prefix "fw: $chain " --log-level notice
-  run iptables -t $table -A $chain -j "$@"
+  run ip46tables -t $table -A $chain -j "$@"
 }
 
 m4_divert(24)m4_dnl
@@ -95,19 +116,50 @@ 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/ipv4/conf/$i/$opt="$val"
+      fi
+    done
+    case $any in
+      nil) echo >&2 "$0: unknown device option $opt"; exit 1 ;;
+    esac
   done
 }
 
@@ -121,11 +173,38 @@ m4_divert(26)m4_dnl
 conntrack () {
   set -e
   chain=$1
-  run iptables -A $chain -p tcp -m state \
+  run ip46tables -A $chain -p tcp -m state \
          --state ESTABLISHED,RELATED -j ACCEPT
-  run iptables -A $chain -p tcp ! --syn -g bad-tcp
+  run ip46tables -A $chain -p tcp ! --syn -g bad-tcp
+}
+
+## commonrules CHAIN
+##
+## Add standard IP filtering rules to the CHAIN.
+commonrules () {
+  set -e
+  chain=$1
+
+  ## Pass fragments through, assuming that the eventual destination will sort
+  ## things out properly.  Except for TCP, that is, which should never be
+  ## fragmented.  This is an extra pain for ip6tables, which doesn't provide
+  ## a pleasant way to detect non-initial fragments.
+  run iptables -A $chain -p tcp -f -g tcp-fragment
+  run iptables -A $chain -f -j ACCEPT
+  run ip6tables -A $chain -p tcp -g tcp-fragment \
+         -m ipv6header --soft --header frag
+  run ip6tables -A $chain -j accept-non-init-frag
 }
 
+m4_divert(38)m4_dnl
+## Accept a non-initial fragment.  This is only needed by IPv6, to work
+## around a deficiency in the option parser.
+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
+
+m4_divert(26)m4_dnl
 ## allowservices CHAIN PROTO SERVICE ...
 ##
 ## Add rules to allow the SERVICES on the CHAIN.
@@ -137,26 +216,26 @@ allowservices () {
   for svc; do
     case $svc in
       *:*)
-        n=2
+       n=2
        left=${svc%:*} right=${svc#*:}
        case $left in *[!0-9]*) eval left=\$port_$left ;; esac
        case $right in *[!0-9]*) eval right=\$port_$right ;; esac
        svc=$left:$right
        ;;
       *)
-        n=1
+       n=1
        case $svc in *[!0-9]*) eval svc=\$port_$svc ;; esac
        ;;
     esac
     case $svc in
       *: | :* | "" | *[!0-9:]*)
-        echo >&2 "Bad service name"
+       echo >&2 "Bad service name"
        exit 1
        ;;
     esac
     count=$(( $count + $n ))
     if [ $count -gt 15 ]; then
-      run iptables -A $chain -p $proto -m multiport -j ACCEPT \
+      run ip46tables -A $chain -p $proto -m multiport -j ACCEPT \
             --destination-ports ${list#,}
       list= count=$n
     fi
@@ -166,11 +245,11 @@ allowservices () {
     "")
       ;;
     ,*,*)
-      run iptables -A $chain -p $proto -m multiport -j ACCEPT \
+      run ip46tables -A $chain -p $proto -m multiport -j ACCEPT \
              --destination-ports ${list#,}
       ;;
-    *) 
-      run iptables -A $chain -p $proto -j ACCEPT \
+    *)
+      run ip46tables -A $chain -p $proto -j ACCEPT \
              --destination-port ${list#,}
       ;;
   esac
@@ -195,7 +274,7 @@ dnsresolver () {
   set -e
   chain=$1
   for p in tcp udp; do
-    run iptables -A $chain -j ACCEPT \
+    run ip46tables -A $chain -j ACCEPT \
             -m state --state ESTABLISHED \
             -p $p --source-port 53
   done
@@ -208,8 +287,8 @@ openports () {
   set -e
   chain=$1; shift
   [ $# -eq 0 ] && set -- $open_port_min $open_port_max
-  run iptables -A $chain -p tcp -g interesting --destination-port $1:$2
-  run iptables -A $chain -p udp -g interesting --destination-port $1:$2
+  run ip46tables -A $chain -p tcp -g interesting --destination-port $1:$2
+  run ip46tables -A $chain -p udp -g interesting --destination-port $1:$2
 }
 
 m4_divert(28)m4_dnl
@@ -277,60 +356,81 @@ defnetclass () {
        from=$(( $from + $bit ))
       done
       to=$(( ($netclassindex << $BIT_TO) + \
-            (0xf << $BIT_FROM) + \
+            (0xf << $BIT_FROM) + \
             (1 << ($netclassindex + $BIT_MASK)) ))
       trace "from $name --> set $(printf %x $from)"
       trace "  to $name --> and $(printf %x $from)"
 
       ## Now establish the mark-from-NAME and mark-to-NAME chains.
       clearchain mangle:mark-from-$name mangle:mark-to-$name
-      run iptables -t mangle -A mark-from-$name -j MARK --set-mark $from
-      run iptables -t mangle -A mark-to-$name -j MARK --and-mark $to
+      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
       ;;
   esac
   netclassindex=$(( $netclassindex + 1 ))
 }
 
-## defiface NAME NETCLASS:NETWORK/MASK...
+## defiface NAME[,NAME,...] NETCLASS:NETWORK/MASK...
 ##
-## Declares a network interface NAME and associates with it 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).
+## 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=
+defaultifaces=""
+allnets= allnets6=
 defiface () {
   set -e
-  name=$1; shift
-  case $ifaces in
-    *:"$name":*) ;;
-    *)
-      clearchain mangle:in-$name
-      run iptables -t mangle -A in-classify -i $name -g in-$name
-      ;;
-  esac
-  ifaces=$ifaces$name:
-  for item; do
-    netclass=${item%:*} addr=${item#*:}
-    case $addr in
-      default)
-       defaultiface=$name
-       defaultclass=$netclass
-       run iptables -t mangle -A out-classify -g mark-to-$netclass
-       ;;
+  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":*) ;;
       *)
-       run iptables -t mangle -A in-$name -s $addr -g mark-from-$netclass
-       run iptables -t mangle -A out-classify -d $addr -g mark-to-$netclass
-       allnets="$allnets $name:$addr"
+       clearchain mangle:in-$name
+       run ip46tables -t mangle -A in-classify -i $name -g in-$name
        ;;
     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
 }
 
@@ -347,7 +447,7 @@ defvpn () {
     *-+)
       root=${iface%+}
       for host; do
-       name=${host%:*} addr=${host#*:}
+       name=${host%%:*} addr=${host#*:}
        defiface $root$name $class:$addr
       done
       ;;