#!/bin/bash
#
# /usr/lib/snf/snf
#
# starts/stops/statuses snf, ITEG's Simple NetFilter toolset
#

# Defaults

START_SNF="no"
BLACKLISTED_HOSTS=""
ALLOWED_TCPPORTS=""
ALLOWED_UDPPORTS=""
SWALLOWED_TCPPORTS=""
SWALLOWED_UDPPORTS=""
PING_LIMIT="5/s"
LOG_LIMIT="1/s"
LOG_DROPS="no"
VERBOSITY=0
SNF_LOG="/var/log/snf.log"

SNFCONF="/etc/snf/snf.conf"

. /usr/lib/snf/parse_snf_conf.sh

if [ -z "$VERBOSITY" ] ; then
  VERBOSITY=0
fi

SNF_CMD="$1"
shift

while [ -n "$1" ] ; do
  if [ "$1" = "-v" ] ; then
    VERBOSITY=$((VERBOSITY + 1))
  elif [ "$1" = "-d" ] ; then
    VERBOSITY=$((VERBOSITY + 2))
  else
    echo "WARNING: Ignoring unknown option '${1}'"
  fi
  shift
done

export NFT=/usr/sbin/nft
if [ ! -x ${NFT} ] ; then
  echo "ERROR: No ${NFT}, please consider installing nftables"
  exit 1
fi

case "$SNF_CMD" in

  start)
    echo "INFO: Starting SNF (Simple NFTables)."

    test -f ${SNF_LOG} && mv ${SNF_LOG} ${SNF_LOG}.former
    echo "INFO: Starting SNF (Simple NFTables)." >${SNF_LOG}

    DO_START_SNF=$(echo "$START_SNF" |egrep -i '^(yes|true)$')
    if [ -z "$DO_START_SNF" ] ; then
      echo "WARNING: START_SNF set to '$START_SNF' rather than yes or true, quitting." |tee -a ${SNF_LOG}
      exit 0
    fi

    #echo "1" > /proc/sys/net/ipv4/ip_forward

    if [ $VERBOSITY -gt 1 ] ; then
      echo "" >> ${SNF_LOG}
      echo "nft ruleset before ..." >> ${SNF_LOG}
      ${NFT} list ruleset 2>&1 >> ${SNF_LOG}
      echo "" >> ${SNF_LOG}
    fi

    #echo "Sleeping a bit" |tee -a ${SNF_LOG}
    #sleep 1 2>&1 |tee -a ${SNF_LOG}

    # - clear existing sfw_* tables
    SNFTABLES=$(${NFT} list tables 2>/dev/null |awk '{print $3}' |egrep "^snf_")
    if [ -n "$SNFTABLES" ] ; then
      test $VERBOSITY -gt 0 && echo ""
      ${NFT} list tables 2>/dev/null |sed -e 's/^table //g' |egrep " snf_[a-z0-9_]+$" |while read TABLE_FAM_NAME
      do
        test $VERBOSITY -gt 0 && echo "INFO: Flushing table ${TABLE_FAM_NAME} ..."
        ${NFT} flush table ${TABLE_FAM_NAME}
        test $VERBOSITY -gt 0 && echo "INFO: Deleting table ${TABLE_FAM_NAME} ..."
        ${NFT} delete table ${TABLE_FAM_NAME}
        test $VERBOSITY -gt 0 && echo ""
      done
    else
      test $VERBOSITY -gt 0 && echo "INFO: No old table named ^snf_* to delete" |tee -a ${SNF_LOG}
    fi

    # table
    test $VERBOSITY -gt 0 && echo "INFO: Adding table snf_in ..." |tee -a ${SNF_LOG}
    ${NFT} add table inet snf_in

    # - allowed_input

    # DROP configured BLACKLISTED_HOSTS
    if [ -n "$BLACKLISTED_HOSTS" ] ; then
      test $VERBOSITY -gt 0 && echo "INFO: Creating chain snf_in.BLACKLIST ..." |tee -a ${SNF_LOG}
      ${NFT} "create chain inet snf_in BLACKLIST { type filter hook input priority 0; policy accept; }"
      for HOST in $BLACKLISTED_HOSTS ; do
        if echo "$HOST" |grep ":" >/dev/null ; then
          test $VERBOSITY -gt 0 && echo "INFO: Adding DROP for blacklisted IPv6 Host/Network $HOST" |tee -a ${SNF_LOG}
          ${NFT} add rule inet snf_in BLACKLIST ip6 saddr $HOST counter drop 2>&1 |tee -a ${SNF_LOG}
        else
          test $VERBOSITY -gt 0 && echo "INFO: Adding DROP for blacklisted IPv4 Host/Network $HOST" |tee -a ${SNF_LOG}
          ${NFT} add rule inet snf_in BLACKLIST ip saddr $HOST counter drop 2>&1 |tee -a ${SNF_LOG}
        fi
        PRC=${PIPESTATUS[0]}
        if [ "${PRC}" != "0" ] ; then
          echo "ERROR: Failed to add DROP for blacklisted Host/Network $HOST" |tee -a ${SNF_LOG}
          exit 11
        fi
      done
    else
      test $VERBOSITY -gt 0 && echo "INFO: No BLACKLISTED_HOSTS to block" |tee -a ${SNF_LOG}
    fi

    test $VERBOSITY -gt 0 && echo "INFO: Creating chain snf_in.INPUT ..." |tee -a ${SNF_LOG}
    ${NFT} "create chain inet snf_in INPUT { type filter hook input priority 0; policy accept; }"
    # we add a plain 'drop' at the end of this chain, if and only if we don't run into any errrors before.
    # this is to prevent admins from locking themselves out

    # ACCEPT existing connections
    test $VERBOSITY -gt 0 && echo "INFO: Addding ACCEPT for established connections." |tee -a ${SNF_LOG}
    ${NFT} "add rule inet snf_in INPUT ct state { established, related } counter accept" 2>&1 |tee -a ${SNF_LOG}
    #, untracked
    PRC=${PIPESTATUS[0]}
    if [ "${PRC}" != "0" ] ; then
      echo "ERROR: Failed to add ACCEPT for established connections." |tee -a ${SNF_LOG}
      exit 21
    fi

    # ACCEPT myself
    test $VERBOSITY -gt 0 && echo "INFO: Addding ACCEPT for local connections." |tee -a ${SNF_LOG}
    ${NFT} "add rule inet snf_in INPUT iifname lo counter accept" 2>&1 |tee -a ${SNF_LOG}
    PRC=${PIPESTATUS[0]}
    if [ "${PRC}" != "0" ] ; then
      echo "ERROR: Failed to add ACCEPT for local connections." |tee -a ${SNF_LOG}
      exit 22
    fi
    ${NFT} "add rule inet snf_in INPUT ip saddr 127.0.0.1 counter accept" 2>&1 |tee -a ${SNF_LOG}
    PRC=${PIPESTATUS[0]}
    if [ "${PRC}" != "0" ] ; then
      echo "ERROR: Failed to add ACCEPT for 127.0.0.1 connections." |tee -a ${SNF_LOG}
      exit 23
    fi
    ${NFT} "add rule inet snf_in INPUT ip6 saddr ::1 counter accept" 2>&1 |tee -a ${SNF_LOG}
    PRC=${PIPESTATUS[0]}
    if [ "${PRC}" != "0" ] ; then
      echo "ERROR: Failed to add ACCEPT for ::1 connections." |tee -a ${SNF_LOG}
      exit 24
    fi

    # ACCEPT configured TRUSTED_HOSTS
    if [ -n "$TRUSTED_HOSTS" ] ; then
      for HOST in $TRUSTED_HOSTS ; do
        if echo "$HOST" |grep ":" >/dev/null ; then
          test $VERBOSITY -gt 0 && echo "INFO: Adding ACCEPT for IPv6 Host/Network $HOST" |tee -a ${SNF_LOG}
          ${NFT} add rule inet snf_in INPUT ip6 saddr $HOST counter accept 2>&1 |tee -a ${SNF_LOG}
        else
          test $VERBOSITY -gt 0 && echo "INFO: Adding ACCEPT for IPv4 Host/Network $HOST" |tee -a ${SNF_LOG}
          ${NFT} add rule inet snf_in INPUT ip saddr $HOST counter accept 2>&1 |tee -a ${SNF_LOG}
        fi
        PRC=${PIPESTATUS[0]}
        if [ "${PRC}" != "0" ] ; then
          echo "ERROR: Failed to add ACCEPT for Host/Network $HOST" |tee -a ${SNF_LOG}
          exit 31
        fi
      done
    fi

    # ACCEPT configured ALLOWED_TCPPORTS
    if [ -n "$ALLOWED_TCPPORTS" ] ; then
      for PORT in $ALLOWED_TCPPORTS ; do
        test $VERBOSITY -gt 0 && echo "INFO: Adding ACCEPT for new connections to TCP Port $PORT" |tee -a ${SNF_LOG}
        ${NFT} add rule inet snf_in INPUT tcp dport $PORT counter accept 2>&1 |tee -a ${SNF_LOG}
        PRC=${PIPESTATUS[0]}
        if [ "${PRC}" != "0" ] ; then
          echo "ERROR: Failed to add ACCEPT for new connections to TCP Port $PORT" |tee -a ${SNF_LOG}
          exit 32
        fi
      done
    fi

    # ACCEPT configured ALLOWED_UDPPORTS
    if [ -n "$ALLOWED_UDPPORTS" ] ; then
      for PORT in $ALLOWED_UDPPORTS ; do
        test $VERBOSITY -gt 0 && echo "INFO: Adding ACCEPT for new connections to UDP Port $PORT" |tee -a ${SNF_LOG}
        ${NFT} add rule inet snf_in INPUT udp dport $PORT counter accept 2>&1 |tee -a ${SNF_LOG}
        PRC=${PIPESTATUS[0]}
        if [ "${PRC}" != "0" ] ; then
          echo "ERROR: Failed to add ACCEPT for new connections to UDP Port $PORT" |tee -a ${SNF_LOG}
          exit 33
        fi
      done
    fi

    # SWALLOW configured SWALLOWED_TCPPORTS
    if [ -n "$SWALLOWED_TCPPORTS" ] ; then
      for PORT in $SWALLOWED_TCPPORTS ; do
        test $VERBOSITY -gt 0 && echo "INFO: Adding swallowing of new connections to TCP Port $PORT" |tee -a ${SNF_LOG}
        ${NFT} add rule inet snf_in INPUT tcp dport $PORT counter drop 2>&1 |tee -a ${SNF_LOG}
        PRC=${PIPESTATUS[0]}
        if [ "${PRC}" != "0" ] ; then
          echo "ERROR: Failed to add swallowing of new connections to TCP Port $PORT" |tee -a ${SNF_LOG}
          exit 34
        fi
      done
    fi

    # SWALLOW configured SWALLOWED_UDPPORTS
    if [ -n "$SWALLOWED_UDPPORTS" ] ; then
      for PORT in $SWALLOWED_UDPPORTS ; do
        test $VERBOSITY -gt 0 && echo "INFO: Adding swallowing of new connections to UDP Port $PORT" |tee -a ${SNF_LOG}
        ${NFT} add rule inet snf_in INPUT udp dport $PORT counter drop 2>&1 |tee -a ${SNF_LOG}
        PRC=${PIPESTATUS[0]}
        if [ "${PRC}" != "0" ] ; then
          echo "ERROR: Failed to add swallowing of new connections to UDP Port $PORT" |tee -a ${SNF_LOG}
          exit 35
        fi
      done
    fi

    # ACCEPT some pings
    test $VERBOSITY -gt 0 && echo "INFO: Adding ACCEPT for $PING_LIMIT pings." |tee -a ${SNF_LOG}
    #${NFT} add rule inet snf_in INPUT ip protocol icmp limit rate $PING_LIMIT/second burst $PING_LIMIT packets counter accept 2>&1 |tee -a ${SNF_LOG}
    #${NFT} add rule inet snf_in INPUT icmp type echo-request limit rate $PING_LIMIT/second burst $PING_LIMIT packets counter accept 2>&1 |tee -a ${SNF_LOG}
    #${NFT} add rule inet snf_in INPUT meta l4proto ipv6-icmp limit rate $PING_LIMIT/second burst $PING_LIMIT packets counter accept 2>&1 |tee -a ${SNF_LOG}
    ${NFT} add rule inet snf_in INPUT meta l4proto icmp icmp type echo-request limit rate $PING_LIMIT/second counter accept 2>&1 |tee -a ${SNF_LOG}
    PRC=${PIPESTATUS[0]}
    if [ "${PRC}" != "0" ] ; then
      echo "ERROR: Failed to add ACCEPT for $PING_LIMIT IPv4 pings." |tee -a ${SNF_LOG}
      exit 51
    fi
    #${NFT} "add rule inet snf_in INPUT icmpv6 type { echo-request, nd-neighbor-solicit } limit rate $PING_LIMIT/second burst $PING_LIMIT packets counter accept" 2>&1 |tee -a ${SNF_LOG}
    ${NFT} add rule inet snf_in INPUT meta l4proto ipv6-icmp counter accept 2>&1 |tee -a ${SNF_LOG}
    PRC=${PIPESTATUS[0]}
    if [ "${PRC}" != "0" ] ; then
      echo "ERROR: Failed to add ACCEPT for $PING_LIMIT IPv6 pings." |tee -a ${SNF_LOG}
      exit 52
    fi

    # SPECIAL_RULES: At what point? Does not matter much any more, better later
    if [ -x "/etc/snf/special_rules.sh" ] ; then
      test $VERBOSITY -gt 0 && echo "INFO: Calling /etc/snf/special_rules.sh" |tee -a ${SNF_LOG}
      /etc/snf/special_rules.sh $* 2>&1 |tee -a ${SNF_LOG}
      PRC=${PIPESTATUS[0]}
      if [ "${PRC}" != "0" ] ; then
        echo "ERROR: Failed to execute /etc/snf/special_rules.sh" |tee -a ${SNF_LOG}
        exit 20
      fi
      test $VERBOSITY -gt 0 && echo "INFO: Back from /etc/snf/special_rules.sh" |tee -a ${SNF_LOG}
    else
      test $VERBOSITY -gt 0 && echo "INFO: No executable /etc/snf/special_rules.sh to call." |tee -a ${SNF_LOG}
    fi

    # - block resp. log rest_of_input
    DO_LOG_DROPS=$(echo "$LOG_DROPS" |egrep -i '^(yes|true)$')
    if [ -z "$DO_LOG_DROPS" ] ; then
      test $VERBOSITY -gt 0 && echo "INFO: Not adding LOGing of rest, because LOG_DROPS is not set to 'yes' or 'true'"
    else
      test $VERBOSITY -gt 0 && echo "INFO: Adding LOGing of rest, with --limit $LOG_LIMIT" |tee -a ${SNF_LOG}
      ${NFT} add rule inet snf_in INPUT limit rate $LOG_LIMIT/second burst $LOG_LIMIT packets counter log prefix DROPping 2>&1 |tee -a ${SNF_LOG}
      PRC=${PIPESTATUS[0]}
      if [ "${PRC}" != "0" ] ; then
        echo "ERROR: Failed to add LOGing of rest, with --limit $LOG_LIMIT" |tee -a ${SNF_LOG}
        exit 61
      fi
    fi
    test $VERBOSITY -gt 0 && echo "INFO: Adding DROP of rest." |tee -a ${SNF_LOG}
    ${NFT} add rule inet snf_in INPUT counter drop 2>&1 |tee -a ${SNF_LOG}
    PRC=${PIPESTATUS[0]}
    if [ "${PRC}" != "0" ] ; then
      echo "ERROR: Failed to add DROPing of rest, without failing in case of error" |tee -a ${SNF_LOG}
      exit 63
    fi

    if [ $VERBOSITY -gt 1 ] ; then
      echo "" >> ${SNF_LOG}
      echo "nft ruleset after ..." >> ${SNF_LOG}
      ${NFT} list ruleset 2>&1 >> ${SNF_LOG}
      echo "" >> ${SNF_LOG}
    fi

    echo "GOOD: Done Starting SNF (Simple NFTables)." |tee -a ${SNF_LOG}

    ;;

  stop)
    echo "INFO: Stopping SNF (Simple NFTables)." |tee -a ${SNF_LOG}

    # - clear existing sfw_* tables
    SNFTABLES=$(${NFT} list tables 2>/dev/null |awk '{print $3}' |egrep "^snf_")
    if [ -n "$SNFTABLES" ] ; then
      test $VERBOSITY -gt 0 && echo ""
      ${NFT} list tables 2>/dev/null |sed -e 's/^table //g' |egrep " snf_[a-z0-9_]+$" |while read TABLE_FAM_NAME
      do
        test $VERBOSITY -gt 0 && echo "Flushing table ${TABLE_FAM_NAME} ..."
        ${NFT} flush table ${TABLE_FAM_NAME}
        test $VERBOSITY -gt 0 && echo "Deleting table ${TABLE_FAM_NAME} ..."
        ${NFT} delete table ${TABLE_FAM_NAME}
        test $VERBOSITY -gt 0 && echo ""
      done
    else
      test $VERBOSITY -gt 0 && echo "No table named ^snf_* to delete"
    fi
    
    echo "GOOD: Done stopping SNF (Simple NFTables)."
    
   ;;

  restart|reload)
    $0 stop $2 && $0 start $2
    ;;

  status)
    echo "INFO: Statussing SNF (Simple NFTables)."
    
    # - list existing sfw_* tables
    SNFTABLES=$(${NFT} list tables 2>/dev/null |awk '{print $3}' |egrep "^snf_")
    if [ -n "$SNFTABLES" ] ; then
      test $VERBOSITY -gt 0 && echo ""
      ${NFT} list tables 2>/dev/null |sed -e 's/^table //g' |egrep " snf_[a-z0-9_]+$" |while read TABLE_FAM_NAME
      do
        test $VERBOSITY -gt 0 && echo "Listing table ${TABLE_FAM_NAME} ..."
        ${NFT} list table ${TABLE_FAM_NAME}
        test $VERBOSITY -gt 0 && echo ""
      done
    else
      test $VERBOSITY -gt 0 && echo "No table named ^snf_* to list"
    fi
    
    echo "GOOD: Done statussing SNF (Simple NFTables). To show all nft rules call   nft list ruleset"

    ;;

  configure)
    # it's ok to do nothing here
    ;;

  *)
    echo "Usage: $0 {start|stop|restart|reload|status}"
    exit 1

esac

