Mobile LAN-oriented filtering in iptables

One of the things that I really like about pf, the OpenBSD firewall, is how it lets you define dynamic packet filtering rules — rules that filter based on your network interfaces’ current addresses at the time of filtering. For instance, if I want to allow SSH connections to my laptop only from my local network:

pass in on xl0 inet proto tcp from (xl0:network) to any \
port ssh flags S/SFRA

(xl0:network) is not resolved to a specific address block at configuration load time; if you switch networks — say, if you go from home to work — the rule’s behavior will change accordingly.

Unless I have overlooked some recent change in Linux, this cannot be achieved in a direct fashion with iptables. You can insert a rule to reject non-LAN source addresses, but such a rule is static. When you change network addresses, the rule must be explicitly updated.

In lieu of rewriting all of netfilter to accommodate this use case (*cough*), I just wrote a shell script to help mitigate the pain of manually updating my laptop’s firewall rules — merely a shortcut to cut down on the amount of typing I do on any given day, but if you tend to move around as much as I do, all those keystrokes can add up :) So with this script you can, in one fell swoop, start and open up global access to an SSH server:

# ssh-serve any

Or only allow local access from the networks you’re connected to:

# ssh-serve lan

Or only local network access on a specific interface:

# ssh-serve lan eth0

Or only access from a given set of IP addresses and/or CIDR blocks:

# ssh-serve addr 192.168.0.104 10.18.0.0/16

Better yet, you can make the whole process automagical by hooking into your Linux distribution’s DHCP client. For instance, in Ubuntu Hardy Heron you can automate ssh-serve by creating a file /etc/dhcp3/dhclient-exit-hooks.d/ssh-serve:

# Allow SSH access from your local network only, and keep these filter
# rules up-to-date as you move from one network to another.
case $reason in
BOUND|REBIND|REBOOT)
ssh-serve lan
;;
esac

This script was written (and named) with Secure Shell in mind, but it could just as easily govern over any other service controlled by a standard SysV init script. See below the jump for the code…

#!/bin/sh
# ssh-serve - Manage SSH server status and IP-based access control.
#
# Use this script to manage the state of the system's OpenSSH server, and
# the iptables rules allowing or denying remote access to it, in one fell
# swoop.  Synopsis:
#
# ssh-serve any
#   Start the server and allow access from anywhere.
#
# ssh-serve lan ( <iface-name> )*
#   Start the server and restrict access to clients connecting from from
#   local network addresses on one of any number of the computer's network
#   interfaces.  If no interface names are specified, then all non-loopback
#   interfaces listed by ifconfig will be provisioned for.
#
# ssh-serve addr ( <cidr> )+
#   Start the server and restrict access to clients connecting from one of
#   any number of specified IP addresses or CIDR blocks.
#
# ssh-serve off
#   Shut down the SSH server and close off access in the firewall.
#
# In order to use this, you will need to set up a separate iptables INPUT
# chain to which this script has exclusive write access; it will overwrite
# any other rules that may exist in its chain.  For example, you could do
# the following:
#
# $ sudo iptables -N SSH_ACTION
# $ sudo iptables -A SSH_ACTION -j REJECT
# $ sudo iptables -A INPUT -t tcp --dport 22 -m state --state NEW \
#   -j SSH_ACTION
#
# Then set the variable SSH_CHAIN to 'SSH_ACTION' in the configuration
# section below.
#
# Mark Shroyer
# Tue Sep 23 17:11:25 EDT 2008
### BEGIN CONFIGURATION ###################################################
# SSH connection logic iptables chain.  Only new, TCP port 22 connections
# in the INPUT table should be jumped to this chain.  WARNING: Any
# pre-existing rules in this chain will be overwritten by this script.
SSH_CHAIN=SSH_ACTION
# Jump target for accepted connections (typically ACCEPT)
JUMP_ACCEPT=SSH_ACCEPT
# Jump target for rejected connections (typically DROP or REJECT)
JUMP_REJECT=REJECT
# Where is our ssh init script?
SSH_INIT_SCRIPT=/etc/init.d/ssh
# Where is our iptables?
IPTABLES=iptables
# Where is our ifconfig?
IFCONFIG=ifconfig
### END CONFIGURATION #####################################################
run() {
echo $@
$@
}
usage() {
echo "Usage: $0 ( any | lan (<iface-name>)* | addr (<cidr>)+ | off )"
exit 1
}
mask_to_cidr() {
mask=$( echo "$1" | tr . ' ' )
sum=0
error=0
for part in $mask
do
case $part in
255) sum=$(( $sum+8 )) ;;
254) sum=$(( $sum+7 )) ;;
252) sum=$(( $sum+6 )) ;;
248) sum=$(( $sum+5 )) ;;
240) sum=$(( $sum+4 )) ;;
224) sum=$(( $sum+3 )) ;;
192) sum=$(( $sum+2 )) ;;
128) sum=$(( $sum+1 )) ;;
0) sum=$(( $sum )) ;;
*) error=1 ;;
esac
done
if [ $error -eq 0 ]
then
echo -n $sum
else
return 1
fi
}
ssh_serve_off() {
run $SSH_INIT_SCRIPT stop
run $IPTABLES -F $SSH_CHAIN
run $IPTABLES -A $SSH_CHAIN -j $JUMP_REJECT
}
ssh_serve_lan() {
ifaces=$( $IFCONFIG | awk '
/^[a-zA-Z0-9]+/ {
if ( $1 !~ /^lo[0-9]*$/ ) {
printf "%s ", $1;
}
}
' )
ifaddrs=''
for iface in $ifaces
do
info=$( $IFCONFIG $iface | awk '
/inet addr:/    { info=$0; }
/UP/            { up=1; }
/RUNNING/       { running=1; }
END             {
if ( up && running ) {
printf info;
}
}
' )
if [ $# -gt 1 ]
then
if ! echo " $@ " | grep -q " $iface "
then
continue
fi
fi
if [ ! -z "$info" ]
then
vals=$( echo "${info}" \
| sed -e 's/Bcast:[^\ ]\+//g' -e 's/[a-zA-Z]\+:\?//g' )
addr=$( echo "${vals}" | awk '{ printf "%s", $1; }' )
mask=$( echo "${vals}" | awk '{ printf "%s", $2; }' )
fi
if [ \( ! -z "$addr" \) -a \( ! -z "$mask" \) -a \( ! -z "$info" \) ]
then
cidr=$( mask_to_cidr $mask )
if [ $? -ne 0 ]
then
echo "CIDR conversion error on netmask ${mask}.  Aborting."
exit -1
fi
ifaddrs="${ifaddrs}${iface}:${addr}/${cidr} "
fi
done
run $IPTABLES -F $SSH_CHAIN
for ifaddr in $ifaddrs
do
iface=$( echo ${ifaddr} | awk 'BEGIN { FS=":"; } { printf "%s", $1 }' )
addr=$( echo ${ifaddr} | awk 'BEGIN { FS=":"; } { printf "%s", $2 }' )
run $IPTABLES -A $SSH_CHAIN -i $iface -s $addr -j $JUMP_ACCEPT
done
run $IPTABLES -A $SSH_CHAIN -j $JUMP_REJECT
run $SSH_INIT_SCRIPT start
}
ssh_serve_addr() {
if [ $# -lt 1 ]
then
usage
fi
run $IPTABLES -F $SSH_CHAIN
for addr in "$@"
do
run $IPTABLES -A $SSH_CHAIN -s "$addr" -j $JUMP_ACCEPT
done
run $IPTABLES -A $SSH_CHAIN -j $JUMP_REJECT
run $SSH_INIT_SCRIPT start
}
ssh_serve_any() {
run $IPTABLES -F $SSH_CHAIN
run $IPTABLES -A $SSH_CHAIN -j $JUMP_ACCEPT
run $SSH_INIT_SCRIPT start
}
command="$1"
if [ $# -gt 0 ]
then
shift
fi
case "$command" in
off|lan|addr|any)
ssh_serve_${command} $@
;;
*)
usage
;;
esac