#!/bin/bash

### BEGIN INIT INFO
# Provides:          linuxmuster-base
# Required-Start:    $network $local_fs
# Required-Stop:     $network $local_fs
# Should-Start:      
# Should-Stop:       
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: internal firewall script
# Description:       starts and stops internal firewall
### END INIT INFO

#
# thomas@linuxmuster.net
# 18.12.2013
# GPL v3
#

# source default settings
[ -f /etc/default/linuxmuster-base ] && . /etc/default/linuxmuster-base

# check if we are allowed to start
[ "$START_LINUXMUSTER" = "yes" ] || exit 0

# source linuxmuster defaults
. /usr/share/linuxmuster/config/dist.conf || exit 1

# source helperfunctions
. $HELPERFUNCTIONS || exit 1

# create $BLOCKEDHOSTSINTRANET if necessary
[ -e "$BLOCKEDHOSTSINTRANET" ] || touch $BLOCKEDHOSTSINTRANET

# file which saves the fw rules
IPTRULES="$CACHEDIR/iptables"

# lockfile defaults
# looks for unlock every 15 secs
LOCKSLEEPTIME=15
# 19 retries
LOCKRETRIES=19
# forced remove of lockfile after 300 secs
LOCKTIMEOUT=300
LOCKFILE="${IPTRULES}.lock"

# check if we were called on boot
echo "$0" | grep -q ^/etc/rc && ONBOOT=yes


### functions begin ###

# start locking iptables file
start_lock(){
 lockfile -s $LOCKSLEEPTIME -r $LOCKRETRIES -l $LOCKTIMEOUT "$LOCKFILE"
}

# stop locking iptables file
stop_lock(){
 rm -f "$LOCKFILE"
}


# read ips, subnets and ports etc. for fw start|reload
read_fwdata(){

 # maximum ports for iptables multiport list
 local max_ports=15
 local m
 local n
 local nr_ports
 local nr_portranges
 local p
 local portfile
 local portlist
 local proto
 local sep
 local summary

 # get imported ip addresses
 IPS="$(grep -v ^# $WIMPORTDATA | awk -F\; '{ print $5 }')"

 # on first time setup (when there is no diff to static config), ...
 workstationdiff=$(diff $WIMPORTDATA $STATICTPLDIR$WIMPORTDATA)
 # ... forcibly add all dhcp adresses
 if [ -z "$workstationdiff" ]; then
  echo $workstationdiff
  myiprange=$(echo $serverip | cut -d "." -f 2)
  for i in $( seq 100 200 ); do 
   IPS="$IPS 10.$myiprange.1.$i"
  done
 fi

 # get subnets which are allowed to access intranet
 [ "$subnetting" = "true" ] && ALLOWEDNETS="$(get_allowed_subnets intern)"
 
 # remove orphaned ips from blocked hosts intranet list
 if [ -s "$BLOCKEDHOSTSINTRANET" ]; then
  for i in $(cat $BLOCKEDHOSTSINTRANET); do
   echo "$IPS" | grep -wq "$i" || sed "/^\($i\)$/d" -i $BLOCKEDHOSTSINTRANET
  done
 fi

 # get blocked ips
 BLOCKED_IPS="$(cat $BLOCKEDHOSTSINTRANET)"

 # get unblocked ips
 UNBLOCKED_IPS=""
 if [ -n "$BLOCKED_IPS" ]; then
  for i in $IPS; do
   echo "$BLOCKED_IPS" | grep -wq "$i" && continue
   if [ -z "$UNBLOCKED_IPS" ]; then
    UNBLOCKED_IPS="$i"
   else
    UNBLOCKED_IPS="$UNBLOCKED_IPS $i"
   fi
  done
 else
  UNBLOCKED_IPS="$IPS"
 fi

 # on first time setup, forcibly unblock all dhcp adresses
 if [ -z "$workstationdiff" ]; then
  for i in $( seq 100 200 ); do 
   UNBLOCKED_IPS="$UNBLOCKED_IPS 10.$myiprange.1.$i"
  done
 fi

 # read firewall port definitions
 for portfile in $ALLOWEDPORTS $BASEPORTS $BLOCKEDPORTS; do
  # assemble all chain data in summary variable
  case $(basename $portfile) in
   allowed*) summary="ALLOWED" ;;
   base*) summary="BASE" ;;
   blocked*) summary="BLOCKED" ;;
   *) ;;
  esac
  # iterate over protocols
  for proto in tcp udp; do
   # get list of ports per protocol
   portlist="$(grep -i ^$proto $portfile | awk '{ print $2 }')"
   # next round if no ports in list
   [ -z "$portlist" ] && continue
   # change spaces to kommas in portlist
   portlist="$(echo $portlist | sed 's| |,|g')"
   # get number of ports in list
   nr_ports="$(echo $portlist | sed 's|,| |g' | wc -w)"
   # get number of portranges in list (contains :)
   nr_portranges="$(echo $portlist | grep -o ":" | wc -l)"
   # add numbers of single ports and portranges to get the effective number of ports
   nr_ports="$(($nr_ports + $nr_portranges))"
   # split portlist if there are more than max_ports defined
   if [ $nr_ports -gt $max_ports ]; then
    summary="$summary $proto"
    n=0
    for p in $(echo $portlist | sed 's|,| |g'); do
     n=$(($n + 1))
     # summary string for first item is different
     sep=","
     [ $n -eq 1 ] && sep=" "
     # increase port counter if portrange
     echo "$p" | grep -qo ":" && n=$(($n + 1))
     # continue if max port number per rule is not yet reached
     if [ $n -le $max_ports ]; then
      summary="${summary}${sep}${p}"
      continue
     fi
     # reset counter
     n=1
     summary="$summary $proto $p"
    done
   else # less or equal number of ports
    summary="$summary $proto $portlist"
   fi
  done
  # save summary to according variable
  case $(basename $portfile) in
   allowed*) ALLOWEDPORTS_LIST="$summary" ;;
   base*) BASEPORTS_LIST="$summary" ;;
   blocked*) BLOCKEDPORTS_LIST="$summary" ;;
  esac
 done

 } # read_fwdata

 
save_rules(){
 local RC=0
 iptables-save > "$IPTRULES" ; RC="$?"
 if [ "$RC" != "0" ]; then
  echo " ... failed to save rules!"
  echo
  rm -f "$IPTRULES"
 fi
 return "$RC"
}


# save rules and flush iptables rules completely
flush_fw() {

 local RC=0
 local i

 # do only if there are rules
 if iptables -L IN-$IFACE &> /dev/null; then
  # save rules first
  save_rules ; RC="$?"
  # remove it all
  for i in ALLOWED BASE BLOCKED; do
   iptables -D IN-$IFACE -i $IFACE -j IN-$IFACE-$i
   iptables -F IN-$IFACE-$i
   iptables -X IN-$IFACE-$i
  done
  iptables -D INPUT -i $IFACE -j IN-$IFACE
  iptables -F IN-$IFACE
  iptables -X IN-$IFACE
 fi
 return "$RC"

} # flush_fw


stop_firewall() {

 local RC=0

 echo -n "Stopping internal firewall"
 
 start_lock
 flush_fw ; RC="$?"
 stop_lock
 
 echo "."
 
 return "$RC"

} # stop_firewall

# create all chains and rules from scratch and write them to file
write_rulesfile() {

 read_fwdata

 local m
 local p
 local portlist
 local proto
 local RULE
 local suballowed
 local TYPE

 # write header
 cat > "$IPTRULES" <<EOF
# Generated by linuxmuster-base on `date`
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:IN-$IFACE - [0:0]
:IN-$IFACE-ALLOWED - [0:0]
:IN-$IFACE-BASE - [0:0]
:IN-$IFACE-BLOCKED - [0:0]
-A INPUT -i $IFACE -j IN-$IFACE
-A IN-$IFACE -i $IFACE -j IN-$IFACE-BASE
-A IN-$IFACE -i $IFACE -j IN-$IFACE-ALLOWED
-A IN-$IFACE -i $IFACE -j IN-$IFACE-BLOCKED
-A IN-$IFACE -m state --state RELATED,ESTABLISHED -j ACCEPT
-A IN-$IFACE -s $ipcopip/32 -j ACCEPT
-A IN-$IFACE ! -s $internalnet/$INTERNBITMASK -j ACCEPT
EOF

 unset global_subnets
 declare -A global_subnets
 prepare_subnet_lookup_tab "$ALLOWEDNETS"

 # add custom rules before
 [ -r "$CUSTOMRULESBEFORE" ] && cat "$CUSTOMRULESBEFORE" >> "$IPTRULES"

 # write base, allowed and blocked chains
 for p in $BASEPORTS_LIST $ALLOWEDPORTS_LIST $BLOCKEDPORTS_LIST; do
  # get type of chain
  case "$p" in
   ALLOWED|BASE|BLOCKED) TYPE="$p" ; continue ;;
  esac
  # get protocol
  case "$p" in
   tcp|udp) proto="$p" ; continue ;;
  esac
  # get portlist
  portlist="$p"
  # write allowed and blocked subnet rules
  if [ "$TYPE" != "BASE" -a -n "$ALLOWEDNETS" ]; then
   suballowed="yes"
   for i in $ALLOWEDNETS; do
    RULE="-A IN-$IFACE-$TYPE -s $i -m $proto -p $proto -m multiport --dports $portlist -j ACCEPT"
    echo "$RULE" >> "$IPTRULES"
   done
  fi
  # write rule into base chain
  if [ "$TYPE" = "BASE" ]; then
   RULE="-A IN-$IFACE-$TYPE -m $proto -p $proto -m multiport --dports $portlist -j ACCEPT"
   echo "$RULE" >> "$IPTRULES"
  else # write rules into allowed and blocked chains
   for i in $IPS; do
    # if ip matches an already allowed subnet only a reject rule for bocked ips is needed
    if ([ -n "$suballowed" ] && is_ip_in_subnets "$i"); then
     if [ "$TYPE" = "BLOCKED" ]; then
      # create reject rule for ips which are blocked
      if ! echo "$UNBLOCKED_IPS" | grep -qw "$i"; then
       RULE="-A IN-$IFACE-$TYPE -s $i -m $proto -p $proto -m multiport --dports $portlist -j REJECT"
       echo "$RULE" >> "$IPTRULES"
      fi
     fi
    else # create accept rules for ips which do not match an allowed subnet
     # for blocked ips do not create an accept rule
     if [ "$TYPE" = "BLOCKED" ]; then
      echo "$UNBLOCKED_IPS" | grep -qw "$i" || continue
     fi
     # create accept rules for the rest
     RULE="-A IN-$IFACE-$TYPE -s $i -m $proto -p $proto -m multiport --dports $portlist -j ACCEPT"
     echo "$RULE" >> "$IPTRULES"
    fi
   done
  fi   
 done

 # add custom rules after
 [ -r "$CUSTOMRULESAFTER" ] && cat "$CUSTOMRULESAFTER" >> "$IPTRULES"

 # footer
 cat >> "$IPTRULES" <<EOF
-A IN-$IFACE -m icmp -p icmp --icmp-type 8 -j ACCEPT
-A IN-$IFACE -j REJECT --reject-with icmp-port-unreachable
COMMIT
# Completed on `date`
EOF

} # write_rulesfile


start_firewall() {

 echo -n "Starting internal firewall"
 
 start_lock
 
 if [ -n "$ONBOOT" -a -s "$IPTRULES" ]; then
  echo -n " on boot, loading saved file"
 else
  write_rulesfile
 fi

 if ! iptables-restore < "$IPTRULES"; then
  echo " ... failed to load $IPTRULES!"
  echo
  return 1
 fi
 
 stop_lock

 echo "."

} # start_firewall

# it's a fake, restarting is fast enough
reload_firewall(){
 echo -n "Reloading internal firewall"
 stop_firewall &> /dev/null; RC="$?"
 if [ "$RC" = "0" ]; then
  start_firewall &> /dev/null ; RC="$?"
 fi
 if [ "$RC" = "0" ]; then
  echo "."
 else
  echo " ... failed!"
 fi
 return "$RC"
}

### functions end ###


RC=0

case "$1" in

 start)
  start_firewall ; RC="$?"
  ;;

 stop)
  stop_firewall ; RC="$?"
  ;;

 restart)
  stop_firewall ; RC="$?"
  if [ "$RC" = "0" ]; then
   start_firewall ; RC="$?"
  fi
  ;;

  reload)
   reload_firewall ; RC="$?"
  ;;
  
 *)
  echo "Usage: $0 <start|stop|restart|reload>"
  ;;

esac

exit "$RC"
