#!/bin/bash

# Curby's Netfilter Script
VERSION=2.2.0
DATE=2011-05-13

# Copyright (C) 2010-2011  Michael Lee <curby@cur.by>
#
# This program 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 2
# of the License, or (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

# To install, copy into /etc/init.d and run the following (on Debian 6)
# update-rc.d curbywall defaults 02 02
# XXX Debian 5 (remove when we upgrade to 6)
# update-rc.d curbywall defaults 13 87

### BEGIN INIT INFO
# Provides:          curbywall netfilter iptables firewall
# Required-Start:    $syslog
# Required-Stop:     $syslog
# X-Start-Before:    $network
# X-Stop-After:      $network
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Load netfilter ruleset
# Description:       Load netfilter ruleset
### END INIT INFO

# TODO try out xtables-addons in debian 6 for knocking, TARPIT, etc.



#   ,------------------------------------------------------------------------------------,
#   |   General Configuration                                                            |
#   '------------------------------------------------------------------------------------'

TIMESTAMP=$(date)

# File system locations
DATAROOT="/var/cache/curbywall"
CONFROOT="/etc/curbywall"
n_table_file="$DATAROOT/nat.chains"
f_table_file="$DATAROOT/filter.chains"
n_rules_file="$DATAROOT/nat.rules"
f_rules_file="$DATAROOT/filter.rules"
ruleset_file="$DATAROOT/combined.ruleset"
config_file="$CONFROOT/config.$(hostname)"

# Default settings, override/supplement in host config if necessary
EXTDEV="eth0"
EXTDEV_ROUTABLE="YES"

# Throttling configuration
# 61/2 are lowest values that will allow successful pongs for normal, well-behaved pings
# burst raised to 3 to allow all three probes from a ICMP traceroute
# This mitigates single host spam, but we can still get DDOSed
PINGLIMIT="-m hashlimit --hashlimit-mode srcip --hashlimit-name ping --hashlimit 61/min --hashlimit-burst 3"
# Prevent untrusted hosts from creating parallel connections to DoS or sidestep fail2ban
# Do not use dstip here or else a single host could DoS us
SSHLIMIT="-m hashlimit --hashlimit-mode srcip --hashlimit-name ssh --hashlimit 1/min --hashlimit-burst 1 --hashlimit-htable-gcinterval 6000 --hashlimit-htable-expire 60000"
# For trusted IPs, allow some burstiness (e.g. for an scp followed by ssh access)
TRUSTEDSSHLIMIT="-m hashlimit --hashlimit-mode srcip --hashlimit-name sshtrusted --hashlimit 1/min --hashlimit-burst 3 --hashlimit-htable-gcinterval 6000 --hashlimit-htable-expire 60000"
# General SYN limiting for incoming connections.
# For most things, 1/sec burst 5 should be fine
# For allowing access to web servers, 1/sec burst 20 might be better
SYNLIMIT="-m hashlimit --hashlimit-mode srcip,dstport --hashlimit-name syn --hashlimit 1/sec --hashlimit-burst 20"
# Keep logs from filling up too quickly (this may make us miss useful things)
LOGLIMIT="-m limit --limit 10/min --limit-burst 20"

# Colors
RED="\033[1;31m"
GRN="\033[1;32m"
YEL="\033[1;33m"
CYN="\033[1;36m"
REG="\033[0m"



#   ,------------------------------------------------------------------------------------,
#   |   Helper Functions                                                                 |
#   '------------------------------------------------------------------------------------'

sysctl_on() { for file in $@; do if [ -e $file ]; then echo 1 > $file; fi; done }
sysctl_off() { for file in $@; do if [ -e $file ]; then echo 0 > $file; fi; done }
n_table() { echo "$*" >> $n_table_file; }
f_table() { echo "$*" >> $f_table_file; }
# XXX FORWARD chain is only configured when there are multiple networks
# XXX   this was put in place when we had generic rulesets
# XXX   now, with machine-specific rulesets, we probably don't need this
f_rule() { 
  if ! echo "$*" | grep -q -- '-A FORWARD';then
    echo "$*" >> $f_rules_file
  elif [[ $INTNET_EXISTS || $DMZNET_EXISTS ]]; then
    echo "$*" >> $f_rules_file
  fi
}
n_rule() {
  if ! echo "$*" | grep -q -- '-A FORWARD';then
    echo "$*" >> $n_rules_file
  elif [[ $INTNET_EXISTS || $DMZNET_EXISTS ]]; then
    echo "$*" >> $n_rules_file
  fi
}
echo_success() { echo -e " ${GRN}[OK]${REG}"; }
echo_warning() { echo -e " ${YEL}[WARNING]${REG}"; }
echo_failure() { echo -e " ${RED}[FAILED]${REG}"; }
log() { logger -p kern.notice -t "$(basename $0)[$$]" "$*"; }



#   ,------------------------------------------------------------------------------------,
#   |   Load Host-specific Configuration and Helper Functions                            |
#   '------------------------------------------------------------------------------------'

if [[ -r "$config_file" ]]; then
  . "$config_file"
  CONFIG_LOADED="true"
else
  CONFIG_LOADED="false"
fi



#   ,------------------------------------------------------------------------------------,
#   |   Find iptables                                                                    |
#   '------------------------------------------------------------------------------------'

if [ $IPTABLES ]; then
  if [ ! -x $IPTABLES ]; then
    echo -en "Curbywall: iptables not found at given location: $IPTABLES"
    echo_failure
    log "iptables not found at given location $IPTABLES"
    exit 2
  fi
elif [ -x /sbin/iptables ]; then
  IPTABLES=/sbin/iptables
elif [ -x /usr/sbin/iptables ]; then
  IPTABLES=/usr/sbin/iptables
else
  echo -en "Curbywall: iptables not found in the usual places"
  echo_failure
  log "iptables not found in the usual places"
  exit 2
fi



#   ,------------------------------------------------------------------------------------,
#   |   Netfilter Ruleset Head and Tail                                                  |
#   '------------------------------------------------------------------------------------'

initialize_ruleset() {
  # Only initialize default chains, zero their counters, and set their policies
  # Custom chains will be added later as needed
  n_table "# Generated by Curbywall v$VERSION with ruleset v$RULESVERSION on $TIMESTAMP"
  n_table "*nat"
  n_table ":PREROUTING ACCEPT [0:0]"
  n_table ":POSTROUTING ACCEPT [0:0]"
  n_table ":OUTPUT ACCEPT [0:0]"

  f_table "# Generated by Curbywall v$VERSION with ruleset v$RULESVERSION on $TIMESTAMP"
  f_table "*filter"
  f_table ":INPUT DROP [0:0]"
  f_table ":FORWARD DROP [0:0]"
  f_table ":OUTPUT DROP [0:0]"
}

finalize_ruleset() {
  # Cap off each table's ruleset
  n_rule "COMMIT"
  f_rule "COMMIT"

  # Backup any existing ruleset
  if [[ -e $ruleset_file ]]; then
    mv $ruleset_file ${ruleset_file}.old
  fi

  # Generate final combined rulset
  cat $f_table_file $f_rules_file > $ruleset_file
  if [[ "$INTNET_EXISTS" || "$DMZNET_EXISTS" ]]; then
    cat $n_table_file $n_rules_file >> $ruleset_file
  fi
  echo "# Completed on $(date)" >> $ruleset_file
  echo -n "Curbywall reloaded: new ruleset written to $ruleset_file"
  echo_success
  log "reloaded: new ruleset written to $ruleset_file"
}



#   ,------------------------------------------------------------------------------------,
#   |   Start the Firewall                                                               |
#   '------------------------------------------------------------------------------------'

firewall_start() {
  lastmod=$(tail -n 1 $ruleset_file | sed 's/.* on //')
  iptables-restore < $ruleset_file
  result=$?
  if [[ $result == 0 ]]; then
    echo -n "Curbywall started: loaded $ruleset_file from $lastmod"
    echo_success
    log "started: loaded $ruleset_file from $lastmod"
  else
    echo -n "Curbywall: ERROR $result while loading $ruleset_file from $lastmod"
    echo_failure
    log "ERROR $result while loading $ruleset_file from $lastmod"
  fi
}



#   ,------------------------------------------------------------------------------------,
#   |   Stop the Firewall and Open the Machine                                           |
#   '------------------------------------------------------------------------------------'

firewall_stop() {
  # Flush rules
  $IPTABLES -F
  $IPTABLES -F -t filter
  $IPTABLES -F -t nat
  # Zero counters
  $IPTABLES -X
  $IPTABLES -X -t filter
  $IPTABLES -X -t nat
  # Reset policies
  $IPTABLES -P INPUT ACCEPT
  $IPTABLES -P OUTPUT ACCEPT
  $IPTABLES -P FORWARD ACCEPT

  echo -n "Curbywall stopped: all policies set to ACCEPT"
  echo_warning
  log "stopped: all policies set to ACCEPT"
}



#   ,------------------------------------------------------------------------------------,
#   |   Sysctl Setup                                                                     |
#   '------------------------------------------------------------------------------------'

sysctl_init() {
  # Drop source-routed packets, generally not useful
  sysctl_off /proc/sys/net/ipv4/conf/*/accept_source_route || die 01 60

  # Drop ICMP Redirects, generally only useful for multiple EXTDEVs
  sysctl_off /proc/sys/net/ipv4/conf/*/accept_redirects || die 01 61

  # Attempt to block and log martians (2nd level of defense added with iptables rules)
  sysctl_on /proc/sys/net/ipv4/conf/*/rp_filter || die 01 62
  sysctl_on /proc/sys/net/ipv4/conf/*/log_martians || die 01 63

  # Allow pings but not broadcast/timestamp pings
  sysctl_on /proc/sys/net/ipv4/icmp_echo_ignore_broadcasts || die 01 70
  sysctl_off /proc/sys/net/ipv4/icmp_echo_ignore_all || die 01 72

  # If there is more than one interface, forward traffic between them
  if [[ "$INTNET_EXISTS" || "$DMZNET_EXISTS" ]]; then
    sysctl_on /proc/sys/net/ipv4/ip_forward || die 01 80
  else
    sysctl_off /proc/sys/net/ipv4/ip_forward || die 01 81
  fi

  # Suppress RFC 1122 violation logging
  sysctl_on /proc/sys/net/ipv4/icmp_ignore_bogus_error_responses || die 01 92

  # default 1000, 2500 recommended for gig-e
  echo 2500 > /proc/sys/net/core/netdev_max_backlog

  # decrease fin timeout, default 60
  #echo 15 > /proc/sys/net/ipv4/tcp_fin_timeout
  # increase TCP max buffer size setable using setsockopt()
  #net.ipv4.tcp_rmem = 4096 87380 8388608
  #net.ipv4.tcp_wmem = 4096 87380 8388608

  # increase Linux auto tuning TCP buffer limits
  #net.core.rmem_max = 8388608
  #net.core.wmem_max = 8388608
}



#   ,------------------------------------------------------------------------------------,
#   |   Prepare Data Directory and Perform Network Discovery                             |
#   '------------------------------------------------------------------------------------'

prepare_environment() {
  if ! $CONFIG_LOADED; then
    echo -n "Curbywall: config file not found.  Try \`$0 download\` to download configuration file."
    echo_failure
    exit 2
  fi

  # Create working directory if it doesn't exist
  if [[ ! -d $DATAROOT ]]; then
    echo -n "Curbywall: data directory not found while generating ruleset.  Creating at $DATAROOT"
    echo_warning
    log "Data directory not found while generating ruleset.  Creating at $DATAROOT"
    mkdir $DATAROOT
  fi

  # Truncate (clear) old rulesets
  for filename in $n_table_file $f_table_file $n_rules_file $f_rules_file; do
    if [[ -e $filename ]]; then
      echo -n "" > $filename
    fi
  done

  # This gives network addresses that aren't strictly correct, e.g. 192.168.0.1/24 instead
  # of 192.168.0.0/24, but netfilter doesn't seem to mind.
  INF=$(ip addr show $EXTDEV | grep 'inet ')
  EXTNET=$(echo $INF | cut -d\  -f 2)
  EXTIP=$(echo $EXTNET | cut -d/ -f 1)
  EXTBRD=$(echo $INF | cut -d\  -f 4)
  echo -n "EXTIP on $EXTDEV: "
  if [ -z $EXTIP ]; then
    echo -en ${YEL}EXTDEV required!$REG
    echo_failure
    exit 1
  else
    echo -en $EXTIP
    echo_success
  fi

  if [[ $INTDEV ]]; then
    INF=$(ip addr show $INTDEV | grep 'inet ')
    INTNET=$(echo $INF | cut -d\  -f 2)
    INTIP=$(echo $INTNET | cut -d/ -f 1)
    INTBRD=$(echo $INF | cut -d\  -f 4)
    echo -n "INTIP on $INTDEV: "
    if [ -z $INTIP ]; then
      echo -en ${YEL}not configured!$REG
      echo_warning
    else
      echo -en $INTIP
      echo_success
      INTNET_EXISTS=1
    fi
  fi

  if [[ $DMZDEV ]]; then
    INF=$(ip addr show $DMZDEV | grep 'inet ')
    DMZNET=$(echo $INF | cut -d\  -f 2)
    DMZIP=$(echo $DMZNET | cut -d/ -f 1)
    DMZBRD=$(echo $INF | cut -d\  -f 4)
    echo -n "DMZIP on $DMZDEV: "
    if [ -z $DMZIP ]; then
      echo -en ${YEL}not configured!$REG
      echo_warning
    else
      echo -en $DMZIP
      echo_success
      DMZNET_EXISTS=1
    fi
  fi

  # Not currently used?
  #GATEWAY=$(ip route | awk '/default/ {print $3}')
}



#   ,------------------------------------------------------------------------------------,
#   |   Download Host-specific Configuration and Ruleset                                 |
#   '------------------------------------------------------------------------------------'

download_configuration() {
  # Create configuration directory if it doesn't exist
  if [[ ! -d $CONFROOT ]]; then
    echo -n "Configuration directory not found.  Creating at $CONFROOT"
    echo_warning
    log "Configuration directory not found.  Creating at $CONFROOT"
    mkdir $CONFROOT
  fi
  if [[ -e $config_file ]]; then
    echo -n "Configuration file exists at $config_file.  Remove it before downloading a new one."
    echo_failure
    exit 2
  fi
  wget --output-document=$CONFROOT/temp.config http://curby.net/filelib/curbywall/config/config.$(hostname)
  if [[ $? == 0 ]]; then
    mv $CONFROOT/temp.config $config_file
    echo -n "Successfully downloaded configuration to $config_file"
    echo_success
    echo "Check it before loading it onto the system!"
  else
    echo -n "Configuration file not found or couldn't be saved.  Check that your host is supported."
    echo_failure
    exit 2
  fi
}



#   ,------------------------------------------------------------------------------------,
#   |   Perform the Requested Action                                                     |
#   '------------------------------------------------------------------------------------'

case "$1" in
  "start"|"restart")
    prepare_environment
    sysctl_init
    # comment out below if starting shouldn't refresh rules
#    generate_rules
    # comment out below if starting should refresh rules
    if [[ ! -e "$ruleset_file" ]]; then
      echo -n "Starting, but no ruleset found.  Generating new ruleset."
      echo_warning
      log "Starting, but no ruleset found.  Generating new ruleset."
      generate_rules
    fi
    firewall_start
  ;;
  "stop")
    firewall_stop
  ;;
  "reload"|"force-reload")
    prepare_environment
    generate_rules
  ;;
  "download")
    download_configuration
  ;;
  *)
    echo "Write a new ruleset to $ruleset_file:"
    echo "  $0 reload|force-reload"
    echo "Apply the ruleset in $ruleset_file:"
    echo "  $0 start|restart"
    echo "Clear all rules and set all policies to ACCEPT:"
    echo "  $0 stop"
    echo "Download host-specific configuration and ruleset:"
    echo "  $0 download"
    echo "View current ruleset:"
    echo "  iptables -nvL"
    exit 3
  ;;
esac



#   ,------------------------------------------------------------------------------------,
#   |   Documentation                                                                    |
#   '------------------------------------------------------------------------------------'

# ________________________________________________________________________________________
# '-- Curbywall's View of Netfilter Tables ----------------------------------------------'

# This shows the overall dataflows used by Curbywall.  Unused chains are not shown.
#
#           <NETWORK>             - Nodes in <angle brackets> represent sources and
#               |                   destinations of packets.  This node represents packets
#               v                   coming to the local host from any network interface.
#         nat.PREROUTING          - Here, Curbywall implements Destination NAT, or port
#               |                   forwarding, policies to expose internally-hosted
#               v                   services to the Internet.
#       (Routing Decision)        - The kernel now decides whether the packet is destined
#         /            \            for the local host or another machine, which would
#     (Local)       (Network)       typically be a "firewalled" machine on an internal
#        |              |           LAN.  For a stand alone host with a single interface,
#        v              |           only the local path would ever be chosen.
#   filter.INPUT        |         - Here, Curbywall applies LOCAL DEFENSES by filtering
#        |              v           traffic that will be seen by local processes.
#        |        filter.FORWARD  - Here, Curbywall applies NETWORK DEFENSES by filtering
#        |              |           traffic between networks.  Traffic modified with DNAT
#        v              |           rules above is also allowed through the firewall here.
# <Local Process>       |         - This node represents a consolidation of all local
#        |              |           processes communicating with the network, including
#        v              |           interprocess communication via the loopback interface.
#  filter.OUTPUT        |         - Here, Curbywall filters locally-generated traffic.
#        |              |           While such traffic is generally trusted, filters here
#        v              v           can prevent malware from contacting the Internet.
#       (Routing Decision)        - The kernel now decides whether the packet is destined
#               |                   for another machine or the local host (in the case of
#               v                   traffic using the loopback interface).
#       mangle.POSTROUTING        - Curbywall can be used to homogenize packets, e.g. by
#               |                   setting the TTL to a single value.  This can hide the
#               v                   fact that multiple OSes/platforms are "firewalled."
#        nat.POSTROUTING          - Here, Curbywall implements SNAT policies to isolate
#               |                   hosts based on trust levels and allow "firewalled"
#               v                   hosts to share external-facing IP addresses.
#           <NETWORK>             - Outbound traffic is released onto the network, where
#                                   it may be sent to another network device or be looped
#                                   back to the local host.

# ________________________________________________________________________________________
# '-- Traffic Throttling ----------------------------------------------------------------'

# Curbywall uses the limit and hashlimit matches to limit certain traffic and prevent
# resource consumption or denial of service.
#
# Hashlimit sample entry for 61/min, burst 2:
#   8 192.168.0.123:0->0.0.0.0:0 62950 62950 31475
#  /  |                         /      |          \
# TTL connection spec      toks left   total toks  tokens used per matching packet
# Each bucket is refilled with 32000 tokens per second
# Be careful when using the dstip mode by itself, as attackers could perform a DoS by
# emptying the bucket and preventing legitimate users from accessing a service.

# exit codes
# 0 no problem
# 1 network critically broken
# 2 necessary files missing or broken
# 3 problem with input


