#! /bin/sh

# vim:syntax=sh

# this file maintained at http://git.mdcc.cx/uruk.git

# Copyright (C) 2003 Stichting LogReport Foundation logreport@logreport.org
# Copyright (C) 2003, 2004, 2010 Tilburg University http://www.uvt.nl/
# Copyright (C) 2003, 2004, 2005, 2007, 2010 Joost van Baal
# Copyright (C) 2012, 2013 Joost van Baal-Ilić
# Copyright © 2014,2015 Wessel Dankers
#
# This file is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# This file is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU GPL for more details.
#
# You should have received a copy of the GNU GPL along with this file, see
# e.g. the file named COPYING.  If not, see <http://www.gnu.org/licenses/>.

#
# peeksheet: iptables predefined chains:
#
#               - INPUT - - localhost - - OUTPUT -
#             /                                    \
# PREROUTING - - - - - - - - FORWARD  - - - - - - - - POSTROUTING
#

iptables=${URUK_IPTABLES:-iptables}

ip6tables=${URUK_IP6TABLES:-ip6tables}
# Variables used: ip6_<...>, sources6_<...>, ip6tables.

interfaces_unprotect=${URUK_INTERFACES_UNPROTECT:-lo}
etcdir="/etc/uruk"
config=${URUK_CONFIG:-${etcdir}/rc}

# IPv4 ranges that should not send or receive packets unless specifically permitted
# See RFC 6890.
ip4_noroute_ranges='0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.0.0.0/24 192.0.2.0/24 192.88.99.0/24 192.168.0.0/16 198.18.0.0/15 198.51.100.0/24 203.0.113.0/24 224.0.0.0/3'

# IPv6 ranges that should not send or receive packets
# see http://www.iana.org/assignments/ipv6-address-space/
# and http://www.iana.org/assignments/ipv6-unicast-address-assignments/
# and RFC 6890.
# All IPv6 addresses in their canonical form.
ip6_noroute_ranges='64:ff9b::/96 ::ffff:0:0/96 100::/64 200::/7 2001:2::/48 2001:db8::/32 2001:10::/28 fc00::/7 fec0::/10 3ffe::/16 5f00::/8 ::1/128 ::/128'

uruk_version="20240930"

test -r $config || {
    echo >&2 "No readable rc file $config found.  Please create one." && exit 1
}

. $config

case $version in ?*)
    case $((version < 20040210)) in 1)
        cat >&2 <<EOT
 Uruk rc file $config claims to be pre-20040210 format.  That's likely not
 supported.  Please read the Uruk README file for upgrade instructions.
EOT
        exit 1
    esac
esac

uruk_log () {
    $iptables -A INPUT -j LOG --log-level debug --log-prefix 'iptables: ' $@
}

uruk6_log () {
    $ip6tables -A INPUT -j LOG --log-level debug --log-prefix 'ip6tables: ' $@
}

uruk_hook () {
    if test -d "$1"
    then
        for f in "$1"/*.rc
        do
            test -r "$f" && . "$f"
        done
    else
        test -r "$1" && . "$1"
    fi
}

uruk_save () {
    table=filter
    case $1 in -t|--table)
        shift
        table=$1
        shift
    ;; -t*)
        table=${1#-t}
        shift
    ;; --table=*)
        table=${1#--table=}
        shift
    esac
    case " $* " in *[$IFS]-t*|*[$IFS]--table[=$IFS]*)
        echo "Warning: -t must be the first argument or it will be ignored" >&2
    esac
    if test -f $uruk_save_dir/$table
    then
        space=
        for arg
        do
            case $arg in -[a-zA-Z0-9])
                echo -n "$space-"
                echo -n "${arg#-}"
            ;; *[!a-zA-Z0-9_!+,./:=@-]*)
                echo -n "$space\""
                echo -n "$arg" | sed 's/[\\\"'\'']/\\&/g'
                echo -n \"
            ;; *)
                echo -n "$space$arg"
            esac
            space=' '
        done >>$uruk_save_dir/$table
        echo >>$uruk_save_dir/$table
    else
        echo "Unknown table '$table'; skipping rule '" -t $table $* "'" >&2
    fi
}

#
# bootstrap these rules
#

# 40 < 60 (         50) medium:  log denied non-broadcasts  (default)
test -z "$loglevel" && loglevel=50

#
# traffic on interfaces_unprotect (lo, per default) is trusted
for iface in ${interfaces_unprotect}
do
    $iptables -A INPUT -i $iface -j ACCEPT
    $iptables -A OUTPUT -o $iface -j ACCEPT

    $ip6tables -A INPUT -i $iface -j ACCEPT
    $ip6tables -A OUTPUT -o $iface -j ACCEPT
done

uruk_hook "$rc_a"

if test $loglevel -ge 80
then
    # 80 < 99 (         90) fascist: log all packets
    uruk_log
    uruk6_log
fi

$iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
$ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
$iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
$ip6tables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# workaround bug(?) in linux kernel, see also
# http://serverfault.com/questions/309691/why-is-our-firewall-ubuntu-8-04-rejecting-the-final-packet-fin-ack-psh-wit

# first argument is the flags which we should examine, the second argument is
# the flags which must be set
$iptables -A INPUT -p tcp --tcp-flags SYN,ACK,FIN,RST FIN,ACK -j ACCEPT
$ip6tables -A INPUT -p tcp --tcp-flags SYN,ACK,FIN,RST FIN,ACK -j ACCEPT

$iptables -A INPUT -p tcp --tcp-flags SYN,ACK,FIN,RST RST -j ACCEPT
$ip6tables -A INPUT -p tcp --tcp-flags SYN,ACK,FIN,RST RST -j ACCEPT

uruk_hook "$rc_b"

#
# protect interfaces_public against spoofing
#
for iface in ${interfaces}
do
    #
    # don't allow anyone to spoof non-routeable addresses
    #
    eval "is=\$ips_${iface}"
    case $is in '')
        interfaces_x=$iface
    ;; *)
        interfaces_x=
        for i in $is
        do
            interfaces_x="$interfaces_x ${iface}_$i"
        done
    esac

    # set of all addresses on this (physical) interface
    blockips=
    blockips6=

    # set of all permitted "special" nets for this (physical) interface
    eval "blocknet=\$net_${iface}"
    eval "blocknet6=\$net6_${iface}"

    for iface_x in $interfaces_x
    do
        eval "ips=\$ip_${iface_x}"
        eval "ips6_defined=\${ip6_${iface_x}+DEFINED}"
        case $ips6_defined in '')
            ips6=$ips
        ;; *)
            eval "ips6=\$ip6_${iface_x}"
        esac

        eval "net=\$net_${iface_x}"
        eval "net6=\$net6_${iface_x}"

        blocknet="$blocknet $net"
        blocknet6="$blocknet6 $net6"

        for ip in $ips
        do
            case $ip in *:*) ;; *) # if it doesn't look like an IPv6 address/range
                for no_route_ip in $ip4_noroute_ranges
                do
                    case " $net " in *[$IFS]$no_route_ip[$IFS]*) ;; *)
                        $iptables -A INPUT -i $iface -s $no_route_ip -d $ip -j DROP
                        $iptables -A OUTPUT -o $iface -s $ip -d $no_route_ip -j DROP
                    esac
                done
                blockips="$blockips $ip"
            esac
        done

        for ip6 in $ips6
        do
            case $ip6 in *[!0-9/.]*) # if it doesn't look like an IPv4 address/range
                for no_route_ip in $ip6_noroute_ranges
                do
                    case " $net $net6 " in *[$IFS]$no_route_ip[$IFS]*) ;; *)
                        $ip6tables -A INPUT -i $iface -s $no_route_ip -d $ip6 -j DROP
                        $ip6tables -A OUTPUT -o $iface -s $ip6 -d $no_route_ip -j DROP
                    esac
                done
                blockips6="$blockips6 $ip6"
            esac
        done
    done

    case $blockips in *[!$IFS][$IFS]*[!$IFS]*)
        # in multiple ip mode, we have to drop only if source is
        # not _one_ of the nic's IPs

        # supporting this for multiple ips would need multiple chains
        # or, perhaps, some iptables extension.

        # for now, we just block "known bad" addresses

        for no_route_ip in $ip4_noroute_ranges
        do
            case " $blocknet " in *[$IFS]$no_route_ip[$IFS]*) ;; *)
                $iptables -A INPUT -i $iface -d $no_route_ip -j DROP
                $iptables -A OUTPUT -o $iface -s $no_route_ip -j DROP
            esac
        done
    ;; *)
        # block outgoing packets that don't have our address as source,
        # they are either spoofed or something is misconfigured (NAT disabled,
        # for instance), we want to be nice and don't send out garbage.

        for ip in $blockips
        do
            # drop all outgoing packets which don't have us as a source
            $iptables -A OUTPUT -o $iface ! -s "$ip" -j DROP

            # drop all incoming packets which don't have us as destination
            $iptables -A INPUT -i $iface ! -d "$ip" -j DROP
        done
    esac

    # in IPv6 we always have a multiple IP mode, because an interface
    # always has a link-local address as well.
    # in multiple ip mode, we have to drop only if source is
    # not _one_ of the nic's IPs.
    # supporting this for multiple ips would need multiple chains
    # or, perhaps, some iptables extension.
    # for now, we just block "known bad" addresses.
    for no_route_ip in $ip6_noroute_ranges
    do
        case " $blocknet $blocknet6 " in *[$IFS]$no_route_ip[$IFS]*) ;; *)
            $ip6tables -A INPUT -i $iface -d $no_route_ip -j DROP
            $ip6tables -A OUTPUT -o $iface -s $no_route_ip -j DROP
        esac
    done

    # Always allow outgoing connections
    $iptables -A OUTPUT -m conntrack --ctstate NEW -o $iface -j ACCEPT
    $ip6tables -A OUTPUT -m conntrack --ctstate NEW -o $iface -j ACCEPT
done

uruk_hook "$rc_c"

#
# allow traffic to offered services, from trusted sources
#
for iface in $interfaces
do
    eval "is=\$ips_${iface}"
    case $is in '')
        interfaces_x=$iface
    ;; *)
        interfaces_x=
        for i in $is
        do
            interfaces_x="$interfaces_x ${iface}_$i"
        done
    esac

    for iface_x in $interfaces_x
    do
        # tcp is special
        eval "services_defined=\${services_${iface_x}_tcp+DEFINED}"
        case $services_defined in '')
            cat >&2 <<EOT
 WARNING: services_${iface_x}_tcp is undefined.  Processing uruk rc file
 nevertheless.  To suppress this warning, add 'services_${iface_x}_tcp='
 to your uruk rc.  NB: if udp, dccp or sctp is undefined for
 services_${iface_x}_, NO warning will be given.
EOT
        esac

        eval "ips_defined=\${ip_${iface_x}+DEFINED}"
        eval "ips6_defined=\${ip6_${iface_x}+DEFINED}"
        case $ips_defined$ips6_defined in '')
            cat >&2 <<EOT
 WARNING: neither ip_${iface_x} nor ip6_${iface_x} is defined.
 Processing uruk rc file nevertheless.
 To suppress this warning, add 'ip_${iface_x}=' to your uruk rc.
EOT
        esac

        eval "ips=\$ip_${iface_x}"
        case $ips6_defined in '')
            ips6=$ips
        ;; *)
            eval "ips6=\$ip6_${iface_x}"
        esac

        for proto in tcp udp dccp sctp
        do
            eval "services_defined=\${services_${iface_x}_${proto}+DEFINED}"
            eval "services=\$services_${iface_x}_${proto}"
            case $services_defined in '')
                # services_${iface_x}_${proto} is undefined.  Processing uruk rc file nevertheless.
                :
            ;; *)
                for service in $services
                do
                    # service is a servicegroupname, e.g. "local"
                    eval "sources_defined=\${sources_${iface_x}_${proto}_${service}+DEFINED}"
                    eval "sources=\$sources_${iface_x}_${proto}_${service}"
                    case $sources_defined in '')
                        echo >&2 "WARNING: sources_${iface_x}_${proto}_${service} is undefined.  (Processing uruk rc file nevertheless.)"
                    esac

                    eval "sources6_defined=\${sources6_${iface_x}_${proto}_${service}+DEFINED}"
                    eval "sources6=\$sources6_${iface_x}_${proto}_${service}"
                    case $sources6_defined in '')
                        eval "sources6=\$sources_${iface_x}_${proto}_${service}"
                    esac

                    eval "ports_defined=\${ports_${iface_x}_${proto}_${service}+DEFINED}"
                    eval "ports=\$ports_${iface_x}_${proto}_${service}"
                    case $ports_defined in '')
                        echo >&2 "WARNING: ports_${iface_x}_${proto}_${service} is undefined.  (Processing uruk rc file nevertheless.)"
                    ;; *)
                        for port in $ports
                        do
                            # port is e.g. www or 1023
                            for source in $sources
                            do
                                case $source in *:*) ;; *) # if it doesn't look like an IPv6 address/range
                                    # source is e.g. 10.56.0.10/32
                                    for ip in $ips
                                    do
                                        case $ip in *:*) ;; *) # if it doesn't look like an IPv6 address/range
                                            $iptables \
                                                --append INPUT \
                                                --match conntrack \
                                                --ctstate NEW \
                                                --in-interface $iface \
                                                --protocol $proto \
                                                --source "$source" \
                                                --destination "$ip" \
                                                --destination-port "$port" \
                                                --jump ACCEPT
                                        esac
                                    done
                                esac
                            done

                            for source6 in $sources6
                            do
                                case $source6 in *[!0-9/.]*) # if it doesn't look like an IPv4 address/range
                                    for ip6 in $ips6
                                    do
                                        case $ip6 in *[!0-9/.]*) # if it doesn't look like an IPv4 address/range
                                            $ip6tables \
                                                --append INPUT \
                                                --match conntrack \
                                                --ctstate NEW \
                                                --in-interface $iface \
                                                --protocol $proto \
                                                --source "$source6" \
                                                --destination "$ip6" \
                                                --destination-port "$port" \
                                                --jump ACCEPT
                                        esac
                                    done
                                esac
                            done
                        done
                    esac
                done
            esac
        done
    done
done

uruk_hook "$rc_d"

#
# rc_e: backwards compatibility.  should be removed one day.
#
uruk_hook "$rc_e"

#
# Don't answer broadcast and multicast packets
#
for iface in $interfaces_nocast
do
    eval "is=\$bcasts_${iface}"
    case $is in '')
        interfaces_x=$iface
    ;; *)
        interfaces_x=
        for i in $is
        do
            interfaces_x="$interfaces_x ${iface}_$i"
        done
    esac

    for iface_x in $interfaces_x
    do
        eval "bcast=\$bcast_${iface_x}"
        $iptables -A INPUT -i $iface -d "$bcast" -j DROP
    done

    $iptables -A INPUT -i $iface -d 255.255.255.255 -j DROP
done

uruk_hook "$rc_f"

#
# icmp stuff. See RFC 1122 and also RFC 792, RFC 950, RFC 1812, RFC 1349,
# RFC 2474 and Stevens' TCP/IP Illustrated Chapter 6, p 69.
# The icmp types are even in %num2icmp_type in Lire::Firewall.
# Running "iptables -p icmp -h" gives iptables's idea of icmp types
#

#
# By default, we disallow
#
#  source-quench
#  redirect (
#   network-redirect
#   host-redirect
#   TOS-network-redirect
#   TOS-host-redirect
#  )
#  router-advertisement
#  router-solicitation
#
# You might want to allow just
#
#  echo-request echo-reply ttl-zero-during-transit \
#   ttl-zero-during-reassembly ip-header-bad required-option-missing
#
# This makes pings succeed, as well as traceroute.  However
# debugging network problems might be _much_ more difficult when disallowing
# lots of other icmp types.  If you really want to do this, use rc_g.
#

for type in \
  address-mask-reply \
  address-mask-request \
  destination-unreachable \
  echo-reply \
  echo-request \
  parameter-problem \
  timestamp-reply \
  timestamp-request \
  ttl-zero-during-reassembly \
  ttl-zero-during-transit
do
    $iptables -A INPUT -p icmp --icmp-type $type -j ACCEPT
done

# Drop echo replies which have a multicast address as a
# destination.  See rfc4890-icmpv6-firewall.sh.
$ip6tables -A INPUT --protocol icmpv6 -d ff00::/8 \
        --icmpv6-type echo-reply -j DROP

# See http://www.iana.org/assignments/icmpv6-parameters for ICMPv6 types
# Or run # ip6tables -p ipv6-icmp -h
for type in \
  echo-request \
  echo-reply \
  destination-unreachable \
  packet-too-big \
  ttl-zero-during-transit \
  ttl-zero-during-reassembly \
  unknown-header-type \
  unknown-option \
  bad-header \
  redirect \
  144 \
  145 \
  146 \
  147 \
  router-solicitation \
  router-advertisement \
  neighbour-solicitation \
  neighbour-advertisement \
  141 \
  142 \
  130 \
  131 \
  132 \
  143 \
  148 \
  149 \
  151 \
  152 \
  153
do
    $ip6tables -A INPUT --protocol icmpv6 --icmpv6-type $type -j ACCEPT
done

# Type 144 - Home Agent Address Discovery [RFC3775]
# Type 145 - Home Agent Address Discovery [RFC3775]
# Type 146 - Mobile Prefix Solicitation [RFC3775]
# Type 147 - Mobile Prefix Advertisement [RFC3775]

# We DROP, a.o.:
# Router renumbering messages: 138
# Node information queries (139) and replies (140): 139 140
#
$ip6tables -A INPUT --protocol icmpv6 -j DROP

uruk_hook "$rc_g"

#
# log packets which make it till here: denied packets (not denied broadcasts
#     or spoofed stuff).  take loglevel into account.
#
if test $loglevel -lt 20
then
    # be silent
    :
elif test $loglevel -lt 40
then
    # log denied packets, targetted at our IPs

    # INVALID: The packet is associated with no known connection. See iptables-extensions(8)
    # may be due to the system running out of memory or ICMP error messages that do not
    # respond to any known connections.  It is helpfull to log these with explicitly
    # mentioning reason of logging (and dropping).
    $iptables -A INPUT -j LOG --log-level debug -m state --state INVALID --log-prefix 'iptables: REASON=invalid '
    $ip6tables -A INPUT -j LOG --log-level debug -m state --state INVALID --log-prefix 'ip6tables: REASON=invalid '

    for iface in $interfaces
    do
        eval "is=\$ips_${iface}"
        case $is in '')
            interfaces_x=$iface
        ;; *)
            interfaces_x=
            for i in $is
            do
                interfaces_x="$interfaces_x ${iface}_$i"
            done
        esac

        for iface_x in $interfaces_x
        do
            eval "ip=\$ip_${iface_x}"
            eval "ips6_defined=\${ip6_${iface_x}+DEFINED}"
            case $ips6_defined in '')
                ips6=$ips
            ;; *)
                eval "ips6=\$ip6_${iface_x}"
            esac
            for ip in $ips
            do
                case $ip in *:*) ;; *) # if it doesn't look like an IPv6 address/range
                    uruk_log -i $iface -d $ip
                esac
            done
            for ip6 in $ips6
            do
                case $ip6 in *[!0-9/.]*) # if it doesn't look like an IPv4 address/range
                    uruk6_log -i $iface -d $ip6
                esac
            done
        done
    done
elif test $loglevel -lt 60
then
    # 40 < 60 (         50) medium:  log denied non-broadcasts  (default)
    uruk_log
    uruk6_log
fi

# FIXME : yet to implement:
# 60 < 80 (         70) high:    log denied packets

uruk_hook "$rc_h"

#
# reject all others
#
$iptables -A INPUT -j REJECT --reject-with tcp-reset -p tcp
$iptables -A INPUT -j REJECT

# These ip6tables flags are supported since 2.4.5; we don't support older kernels
$ip6tables -A INPUT -j REJECT --reject-with tcp-reset -p tcp
$ip6tables -A INPUT -j REJECT --reject-with icmp6-adm-prohibited

uruk_hook "$rc_i"

# make sure we exit 0, even if last test failed
exit 0
