#!/bin/sh -eu

LANGUAGE="${LANGUAGE:-C}"
IPTABLES_HELPER_CONF="${IPTABLES_HELPER_CONF:-/etc/alterator/net-iptables.conf}"

_(){
    gettext "alterator-net-iptables" "$1"
}

print_help(){
cat <<EOF
Script for simple iptables(8) rules management.

Usage:
 ${0##*/} help  -- show help message
 ${0##*/} show [<options>]  -- show current settings
 ${0##*/} write [<options>] -- change some settings and commit them to system
 ${0##*/} reset -- reset settings to the default values
 ${0##*/} list  -- show list of available services (using \$LANGUAGE)

Options for write action:
 -m <mode>   -- specify mode (gateway|router)
 -e <list>   -- specify list of external interfaces
 -s <list>   -- specify list of opened services on external ifaces
 -t <list>   -- specify list of opened extra tcp ports on external ifaces
 -u <list>   -- specify list of opened extra udp ports on external ifaces
 -U (on|off) -- add rules for ulogd
 -S <port>   -- add transparent squid redirection from int ifaces to <port>
                Use empty port to remove redirection
 -D <list>   -- specify list of dnat rules (rule="proto:ip:port:ip:port")
 -d          -- don't change system settings, only config file

* Values in lists must be separated by semicolon.
* You can add value to existing list using "+<value>" argument, remove it
  using "-<value>" or replace the whole list using "<value1>;<value2>;..."

Options for read action:
 -m          -- show mode (gateway|router)
 -e          -- show list of external interfaces (space separated)
 -i          -- show list of internal interfaces
 -s          -- show list of opened services on external ifaces
 -t          -- show list of opened extra tcp ports on external ifaces
 -u          -- show list of opened extra udp ports on external ifaces
 -U          -- show ulogd setting (on|off)
 -S          -- show transparent squid setting (empty or port number)
 -D          -- show list of dnat rules

Common options:
 -v          -- be verbose

For more information see alterator-net-iptables(1).
EOF
} #' -- for xgettext, not mc!

. shell-error
. shell-getopt
. shell-config
. alterator-net-functions

# etcnet configuration files:

INPUT="/etc/net/ifaces/default/fw/iptables/filter/INPUT"
OUTPUT="/etc/net/ifaces/default/fw/iptables/filter/OUTPUT"
FORWARD="/etc/net/ifaces/default/fw/iptables/filter/FORWARD"
NAT_POST="/etc/net/ifaces/default/fw/iptables/nat/POSTROUTING"
NAT_PRE="/etc/net/ifaces/default/fw/iptables/nat/PREROUTING"

EFW="/etc/net/scripts/contrib/efw --iptables default all restart"

IFOPTS="/etc/net/ifaces/default"
FWOPTS="/etc/net/ifaces/default/fw"

# alterator-net-iptables configuration files:

SERVICEDIR="/etc/alterator/net-iptables/"

##########################


# show current settings
show_settings(){
  echo -e "mode=\"$mode\""
  echo -e "external_ifaces=\"$external_ifaces\""
  echo -e "opened_services=\"$opened_services\""
  echo -e "opened_tcp_ports=\"$opened_tcp_ports\""
  echo -e "opened_udp_ports=\"$opened_udp_ports\""
  echo -e "ulogd=\"$ulogd\""
  echo -e "squid=\"$squid\""
  echo -e "dnat_rules=\"$dnat_rules\""
}

# read settings from file
read_settings(){
  [ -f "$IPTABLES_HELPER_CONF" ] || return 0
  local cmd="$(sed -n \
    -e '/^mode=/p'\
    -e '/^external_ifaces=/p'\
    -e '/^opened_services=/p'\
    -e '/^opened_tcp_ports=/p'\
    -e '/^opened_udp_ports=/p'\
    -e '/^ulogd=/p'\
    -e '/^squid=/p'\
    -e '/^dnat_rules=/p'\
    "$IPTABLES_HELPER_CONF")"
  eval $cmd
}

reset_vars(){
  mode="router"
  external_ifaces=
  opened_services=
  opened_tcp_ports=
  opened_udp_ports=
  ulogd="off"
  squid=
  dnat_rules=
}

reset_settings(){
  reset_vars
  write_settings
}

# test all values
test_vars(){

  [ "$mode" = "gateway" -o "$mode" = "router" ] ||
    fatal "`_ "Error: bad mode"` \"$mode\" `_ "(possible values: gateway or router)"`"

  for i in $(echo "$opened_tcp_ports" | tr -s ';,' ' '); do
    echo $i | grep -q '^[0-9]\+$' ||\
      fatal "`_ "Error: bad TCP port"` \"$i\" `_ "(must be a number)"`"
  done

  for i in $(echo "$opened_udp_ports" | tr -s ';,' ' '); do
    echo $i | grep -q '^[0-9]\+$' ||\
      fatal "`_ "Error: bad UTP port"` \"$i\" `_ "(must be a number)"`"
  done

#  local ifaces="$(list_iface | cut -f1 | tr -s '\n' ' ')"
#  ifaces=" $ifaces "
#  for i in $(echo "$external_ifaces" | tr -s ';,' ' '); do
#    [ -z "${ifaces##* $i *}" ] ||\
#      fatal "`_ "Error: unknown interface"` \"$i\" (`_ "possible values:"`$ifaces)"
#  done

  local services="$(list_services | cut -f1 | tr -s '\n' ' ')"
  services=" $services "
  for i in $(echo "$opened_services" | tr -s ';,' ' '); do
    [ -z "${services##* $i *}" ] ||\
      fatal "`_ "Error: unknown service"` \"$i\""
  done

  [ "$ulogd" = "on" -o "$ulogd" = "off" ] ||
    fatal "`_ "Error: bad ULOGD mode"` \"$ulogd\" `_ "(possible values: on or off)"`"

  echo $squid | grep -q '^[0-9]*$' ||\
    fatal "`_ "Error: bad SQUID port"` \"$squid\" `_ "(must be a number or empty string)"`"

  for i in $(echo "$dnat_rules" | tr -s ';,' ' '); do
    echo "$i" |\
    while IFS=":" read proto sip sp dip dp; do
      [ "$proto" = "tcp" -o "$proto" = "udp" ] ||\
        fatal "`_ "Error: unknown protocol"` $proto `_ "in DNAT rule"` \"$proto\" (`_ "possible values:"` tcp, udp)"
      echo $sp | grep -q '^[0-9]\+$' ||\
        fatal "`_ "Error: bad port"` $sp `_ "in DNAT rule"` \"$i\" `_ "(must be a number)"`"
      echo $dp | grep -q '^[0-9]\+$' ||\
        fatal "`_ "Error: bad port"` $dp `_ "in DNAT rule"` \"$i\" `_ "(must be a number)"`"
      echo $sip | grep -q '^[0-9]\{1,3\}\+\.[0-9]\{1,3\}\+\.[0-9]\{1,3\}\+\.[0-9]\{1,3\}\+$' ||\
        fatal "`_ "Error: bad ip"` $sip `_ "in DNAT rule"` \"$i\""
      echo $dip | grep -q '^[0-9]\{1,3\}\+\.[0-9]\{1,3\}\+\.[0-9]\{1,3\}\+\.[0-9]\{1,3\}\+$' ||\
        fatal "`_ "Error: bad ip"` $dip `_ "in DNAT rule"` \"$i\""
    done
  done

}

show_external_ifaces(){
  echo "$external_ifaces" | tr -s ';,' ' '
}

show_internal_ifaces(){
  local ext="$(show_external_ifaces)"
  local int=" $(list_iface | cut -f1 | tr -s '\n' ' ') "
  for i in $ext; do int="${int%% $i *} ${int##* $i }"; done
  echo "$int"
}

# write settings to config, commit them to system
write_settings(){
  verbose "checking all values"
  test_vars

  verbose "writing parameters to $IPTABLES_HELPER_CONF"
  show_settings > "$IPTABLES_HELPER_CONF"

  if [ -n "$dontcommit" ]; then
    verbose "don't commit settings to system"
    return
  fi

  verbose "turning on ip forwarding"
  local frdelim='[[:space:]]*=[[:space:]]*'
  local fwdelim=' = '
  shell_config_set /etc/net/sysctl.conf net.ipv4.ip_forward 1 "$frdelim" "$fwdelim"
  echo 1 >/proc/sys/net/ipv4/ip_forward

  verbose "turning on ip firewalling"
  write_iface_option "$IFOPTS" "CONFIG_FW"               "yes"
  write_iface_option "$FWOPTS" "FW_TYPE"                 "iptables"
  write_iface_option "$FWOPTS" "IPTABLES_HUMAN_SYNTAX"   "no"

  verbose "resetting chains"

  for i in "$INPUT" "$OUTPUT" "$FORWARD" "$NAT_PRE" "$NAT_POST"; do
    cat > "$i" << EOF
# This file was automatically created by alterator-net-iptables.
# If you are using alterator-net-iptables then all changes
# made in this file by hands will be lost!
# For more information see alterator-net-iptables(1).

EOF
  done

cat >> "$INPUT" << EOF
-P ACCEPT
-f -j DROP
-m state --state ESTABLISHED,RELATED -j ACCEPT
EOF
cat >> "$OUTPUT" << EOF
-P ACCEPT
-f -j DROP
EOF

  local i p s

  local ext="$(show_external_ifaces)"
  local int="$(show_internal_ifaces)"
  verbose "external ifaces: $ext"
  verbose "internal ifaces: $int"

  if [ "$ulogd" = "on" ]; then
    verbose "setting up ULOGD rules"
    local text='-j ULOG --ulog-nlgroup 1 --ulog-cprange 48 --ulog-qthreshold 50'
    echo "$text --ulog-prefix \"icount\"" >> "$INPUT"
    echo "$text --ulog-prefix \"ocount\"" >> "$OUTPUT"
    echo "$text --ulog-prefix \"fcount\"" >> "$FORWARD"
  fi

  for i in $(echo "$dnat_rules" | tr -s ';,' ' '); do
    echo "$i" |\
    while IFS=":" read proto sip sp dip dp; do
      verbose "setting up DNAT rule: $proto $sip:$sp -> $dip:$dp"
      echo "-p $proto --destination $sip --dport $sp -j ACCEPT" >> "$FORWARD"
      echo "-p $proto --destination $sip --dport $sp -j DNAT --to-destination $dip:$dp" >> "$NAT_PRE"
    done
  done

  if [ -n "$squid" ]; then
    verbose "setting up SQUID redirection:"
    for i in $int; do
      verbose "  iface: $i, port 80 -> $port"
      echo "-i $i -p tcp --dport 80 -j REDIRECT --to-port $squid" >> "$NAT_PRE"
    done
  fi


  for i in $ext; do

    verbose "configuring external interface $i:"

    for s in $(echo "$opened_services" | tr -s ';,' ' '); do
      verbose "  open service $s on interface $i:"
      local ports="$(alterator-dump-desktop -v out=X-Alterator-Port $SERVICEDIR/$s.desktop |\
                     tr ';' ' ')"
      for p in $ports; do
        if [ $p = "icmp:" ]; then
          verbose "  open icmp"
          echo "-i $i -p icmp -j ACCEPT" >> "$INPUT"
          continue
        fi
        echo $p | grep -q '^\(tcp\|udp\):[0-9]\+$' ||\
          fatal "`_ "Error: bad port"` $p `_ "in desktop-file for service"` $s."
        verbose "  open port $p"
        echo "-i $i -p ${p%%:*} --dport ${p##*:} -j ACCEPT" >> "$INPUT"
      done
    done

    for p in $(echo "$opened_tcp_ports" | tr -s ';,' ' '); do
      verbose "  open extra port tcp:$p on iface $i"
      echo "-i $i -p tcp --dport $p -j ACCEPT" >> "$INPUT"
    done

    for p in $(echo "$opened_udp_ports" | tr -s ';,' ' '); do
      verbose "  open extra port udp:$p on iface $i"
      echo "-i $i -p udp --dport $p -j ACCEPT" >> "$INPUT"
    done

    verbose "  close other ports on interface $i"
    echo "-i $i -j DROP" >> "$INPUT"

  done

  # settin up FORWARD rules
  for i in $ext; do
    verbose "closing forwarding from iface: $i"
    echo "-i $i -m state --state ESTABLISHED,RELATED -j ACCEPT" >> "$FORWARD"
    echo "-i $i -j DROP" >> "$FORWARD"
  done

  # setting up NAT
  if [ "$mode" = "gateway" ]; then
    verbose "setting up NAT"
    echo > "$NAT_POST"
    local i1 i2

    for i in $ext; do
      echo "-o $i -j MASQUERADE" >> "$NAT_POST"
        verbose "  to $i iface"
    done
  fi

  verbose "restarting efw"
  if [ -z "$verbose" ]; then
    $EFW > /dev/null || fatal "`_ "Error while reloading firewalling rules"`"
  else
    $EFW > /dev/stderr || fatal "`_ "Error while reloading firewalling rules"`"
  fi

  verbose "running /usr/lib/alterator/hooks/net-iptables.d/*"
  run-parts /usr/lib/alterator/hooks/net-iptables.d/ ||
    fatal "`_ "Error while running /usr/lib/alterator/hooks/net-iptables.d/*"`"
}

# show available services (note: $LANGUAGE is used)
list_services(){
  alterator-dump-desktop \
        -v lang="$LANGUAGE" \
        -v out="Filename;X-Alterator-Port;Name" \
        -v def="notfound;noport;" \
    $SERVICEDIR/*.desktop |
  while read filename port name; do
    filename="${filename##*/}"
    filename="${filename%.desktop}"
    printf '%s\t%s\t%s\n' "$filename" "$port" "$name"
  done
}

# modify list: <list> +<val> or <list> -<val>  or <list> <val1>;<val2>...
modify_list(){
  local list="$1"
  local arg="$2"
  if [ -z "$arg" ]; then
    eval $list=""
  elif [ -z "${arg%%+*}" ]; then
    [ -z "$(echo $arg | tr -c -d ';, ')" ] ||
      fatal "`_ "Can't add multiple values to list"`" #'
    local new="$(eval "echo \$$list" | tr -s ';, ' '\n' |
    while read l; do
      [ "$l" = "${arg#+}" ] || echo -n "${l:+$l;}"
    done)"
    eval "$list=\"\$new\${arg#+}\""
  elif [ -z "${arg%%-*}" ]; then
    [ -z "$(echo $arg | tr -c -d ';, ')" ] ||
      fatal "`_ "Can't remove multiple values from list"`" #'
    local new="$(eval "echo \$$list" | tr -s ';, ' '\n' |
    while read l; do
      [ "$l" = "${arg#-}" ] || echo -n "${l:+$l;}"
    done)"
    eval $list="\${new%;}"
  else
    arg="$(echo "$arg" | tr -s ';, ' ';')"
    eval $list="\$arg"
  fi
}

########################

# default settings

action="${1:-help}"
shift ||:

# reset vars to default values
reset_vars

# read current settings from config file
read_settings


dontcommit=
# parse options
case "$action" in
  write)
    while getopts "m:e:s:t:u:U:S:D:vd" "$@"; do
      case $OPTOPT in
        m) mode="$OPTARG"   ;;
        e) modify_list external_ifaces  "$OPTARG" ;;
        s) modify_list opened_services  "$OPTARG" ;;
        t) modify_list opened_tcp_ports "$OPTARG" ;;
        u) modify_list opened_udp_ports "$OPTARG" ;;
        U) ulogd="$OPTARG" ;;
        S) squid="$OPTARG" ;;
        D) modify_list dnat_rules "$OPTARG" ;;
        v) verbose=1    ;;
        d) dontcommit=1 ;;
      esac
    done
  ;;
  reset)
    while getopts "vd" "$@"; do
      case $OPTOPT in
        v) verbose=1    ;;
        d) dontcommit=1 ;;
      esac
    done
  ;;
  show)
    while getopts "meistuUSDv" "$@"; do
      case $OPTOPT in
        m) echo $mode; exit 0 ;;
        e) show_external_ifaces; exit 0 ;;
        i) show_internal_ifaces; exit 0 ;;
        s) echo "$opened_services"   | tr -s ';,' ' '; exit 0 ;;
        t) echo "$opened_tcp_ports"  | tr -s ';,' ' '; exit 0 ;;
        u) echo "$opened_udp_ports"  | tr -s ';,' ' '; exit 0 ;;
        U) echo "$ulogd"; exit 0 ;;
        S) echo "$squid"; exit 0 ;;
        D) echo "$dnat_rules" | tr -s ';,' ' '; exit 0 ;;
        v) verbose=1 ;;
      esac
    done
  ;;
  *)
    while getopts "v" "$@"; do
      case $OPTOPT in
        v) verbose=1    ;;
      esac
    done
  ;;
esac

verbose "executing $action action"
case $action in
  show) show_settings ;;
  list) list_services ;;
  write) write_settings ;;
  reset) reset_settings ;;
  *) print_help ;;
esac

