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