HomeBlogDynamic Firewall for PPPoE

Dynamic Firewall for PPPoE

Until a few days ago, I had a TV cable-based network connection.  I had a fixed IP so it was easy to configure firewall on various machines that I can access, to allow access only from my home IP.  And I was happy.

My ISP has recently moved me to a more robust, Ethernet connection, which is done via PPPoE.  All great, but the IP is changing every time I connect.  That wouldn't be a problem if I never had any downtimes, but in practice this is not possible.  Every once in a while, my router will reconnect and will get me a brand new IP and machines where I configured a firewall won't let me in.

DynDNS comes to rescue

I asked around for solutions and turns out there is one.  DynDNS gives me a “dynamic domain name server”—which can update automatically when my home IP changes.  This does require some efforts to setup, but it wasn't that hard after all.  So I signed up for a free account at www.dyndns.com, and got a domain name (it's in the form whateveryouwant.homeip.net, although there are a lot of other domains to chose from if for some reason you don't like “homeip.net”).

Then I needed to setup my router to announce IP changes to DynDNS, so that my new domain would always point to my home IP.  I have a Linksys WRT54G router which happily runs OpenWRT for many months (or years?) now (so the following steps are appropriate for OpenWRT/Kamikaze).  OpenWRT is a very configurable beast, but the last release (named “Kamikaze”) doesn't provide a Web frontend that helps you configure by point and click, so I needed to Google around and help myself.  Long story short, all I had to do was throwing this script in /etc/ppp/ip-up.d/S01dyndns:

#!/bin/sh
USER="YOUR DYNDNS.COM USER ID"
PASS="YOUR DYNDNS.COM PASSWORD"
DOMAIN="YOUR DYNDNS.COM DOMAIN NAME"
registered=$(nslookup $DOMAIN|sed 's/[^0-9. ]//g'|tail -n1|sed -e's/ [0-9.]*//2' -e's/ *//')
current=$(wget -O - http://checkip.dyndns.org|sed 's/[^0-9.]//g')
[ "$current" != "$registered" ] && {
        wget -O /dev/null http://$USER:$PASS@members.dyndns.org/nic/update?hostname=$DOMAIN &&
        registered=$current
}
sleep 3
newip=$(wget -O - http://checkip.dyndns.org|sed s/[^0-9.]//g)
newdns=$(nslookup $DOMAIN|sed 's/[^0-9. ]//g'|tail -n1|sed -e's/ [0-9.]*//2' -e's/ *//')
echo "Set ${newip} (DNS: ${newdns}), had ${current} (DNS: ${registered})" \
        | /usr/bin/logger -t ddupd

I found this somewhere on the OpenWRT wiki, but I didn't bookmark—so sorry for not giving a link.  Anyway, for the above script to work fine, you need to have “wget” (the one provided with Busybox will segfault), so make sure to:

ipkg install --force-overwrite wget

So whenever the ppp0 interface comes up, the above script will run and will inform DynDNS about the new IP for my DynDNS domain.

Using your own domain name

It's nice to have “whateveryouwant.homeip.net” always point to your home IP, but it's even nicer if you have your own domain name automatically updated.  I thought that's impossible, but I have smart friends. ;-)  So, it turns out that I only had to add a CNAME record for my domain—this would make my domain an alias to “whatever.homeip.net” thus also point to my home location, always.  Lucky me, I run my own domain name server.

Configure your firewalls

So back to the original problem: what do I need to do in order to configure firewalls on various remote machines, so that they allow unrestricted access from my ever-changing home IP?  With a little help from my friends, I came up with this script which is quite generic, so that's why I thought I'd share it here.

The script is a little Perl-based daemon which will check the IP of the DynDNS domain every 5 minutes.  If it was changed since the last check, it will update the firewall rules (deny access from old IP, allow access from new IP).  A small problem is that there's still a maximum 5 minutes time when you can't access the machine.  This could be circumvented by installing a CGI that will force an IP update (see -u flag) so if I ever want to force an update manually, I'd be able to.  I haven't check this yet but it should work.

Here's the script (you can download it here), and following there's brief usage information.

#! /usr/bin/perl -w

######
#
# (c) Mihai Bazon 2007, http://mihai.bazon.net/blog
# This is free software under the same terms as Perl itself.
#
######

use strict;
use Socket;
use Getopt::Long;
use POSIX ();

####### CONFIG

my @HOSTS = qw();
my $CHECK_INTERVAL = 300; # 5 min
my $CHAIN = 'DYNDNS'; # iptables chain name
my $IPTABLES = '/sbin/iptables';
my $PID_FILE = '/var/run/dyndns-firewall.pid';
my $LOG_FILE = '/var/log/dyndns-firewall.log';

####### /CONFIG

my $opt_kill = 0;
my $opt_update = 0;
my @opt_hosts;

GetOptions(
    'kill|k'         => \$opt_kill,
    'update|u'       => \$opt_update,
    'host|hosts|h=s' => \@opt_hosts,
);

my $running_pid;

if (-f $PID_FILE) {
    open PID, '<', $PID_FILE;
    local $/ = undef;
    $running_pid = <PID>;
    close PID;
}

if ($opt_kill) {
    if ($running_pid) {
        if (!kill('TERM', $running_pid)) {
            warn "Couldn't kill process - removing stale PID file";
            unlink $PID_FILE;
        }
    } else {
        warn "dyndns-firewall is not running (no PID file)";
    }
    exit 0;
}

if ($running_pid) {
    if ($opt_update) {
        debug("Already running - forcing update");
        kill 'USR1', $running_pid;
        exit 0;
    } else {
        die("dyndns-firewall.pl is already running.  Use -k to kill " .
            "it, use -u to force update (don't pass new hosts!)");
    }
}

## daemonize
fork && exit;
POSIX::setsid();
$SIG{HUP} = 'IGNORE';
fork && exit;
chdir '/';
umask 0;
open my $LOG, '>>', $LOG_FILE;
open STDIN, '+>/dev/null';
open STDOUT, '+>&=', $LOG;
open STDERR, '+>&=', $LOG;

@HOSTS = split(/,/, join(',', @opt_hosts));

if (!@HOSTS) {
    die "No hosts defined.";
}

## cleanup on exit
$SIG{TERM} = $SIG{INT} = sub {
    my ($signal) = @_;
    debug("--- Terminated (SIG$signal), cleaning up");
    iptables("-F $CHAIN");
    iptables("-D INPUT -j $CHAIN");
    iptables("-X $CHAIN");
    unlink $PID_FILE;
    exit 0;
};

## update IPs on SIGUSR1
$SIG{USR1} = sub {
    debug("Got SIGUSR1, forcing update");
    update_ips();
};

## initialize: lookup IPs at startup and build %ip_hash
my @ips = map { get_ip($_) } @HOSTS;
my %ip_hash;

@ip_hash{@HOSTS} = @ips;

debug("*** Starting up, pid=$$");
open PID, '>', $PID_FILE;
print PID $$;
close PID;

## setup new chain
iptables("-N $CHAIN");
iptables("-I INPUT 1 -j $CHAIN");
iptables("-F $CHAIN");

## add initial ALLOW rules
update_fw($_)
  foreach (@ips);

while (1) {
    sleep $CHECK_INTERVAL;
    update_ips();
}

####### UTILITIES

sub get_ip {
    inet_ntoa(inet_aton($_[0]));
}

sub update_fw {
    my ($newip, $oldip) = @_;
    if ($oldip) {
        iptables("-D $CHAIN -s $oldip -j ACCEPT");
    }
    if ($newip) {
        iptables("-A $CHAIN -s $newip -j ACCEPT");
    }
}

sub iptables {
    my $cmd = join(' ', "$IPTABLES", @_);
    debug("$cmd");
    system $cmd;
}

sub update_ips {
    while (my ($host, $ip) = each %ip_hash) {
        my $current_ip = get_ip($host);
        if ($current_ip ne $ip) {
            update_fw($current_ip, $ip);
            $ip_hash{$host} = $current_ip;
        }
    }
}

sub debug {
    print STDERR scalar(localtime), " - ", @_, "\n";
}

__END__

Usage

First off, customize, if you wish, the CONFIG section at the start.  Note that @HOSTS isn't customizable there—it was in my original script, but then I decided to make it a command line argument.  Start the script like this:

./dyndns-firewall.pl -h WhateverYourDomainIs.dyndns.org

(you can pass multiple hosts if you wish; either separate them with commas, or pass multiple -h arguments).

Upon start, it will check the current IP of “WhateverYourDomainIs.dyndns.org” and will add a firewall rule to allow it.  All rules are added in the “DYNDNS” chain (you can change its name if you wish).  This chain is created when the program starts and is linked to the “INPUT” chain.

The script forks a background process and will check every 5 minutes for IP changes.  When the IP was changed since last check, it will remove the old firewall rule and add one for the new IP.  All firewall operations are logged in /var/log/dyndns-firewall.log (change this file in config section if you wish).

In order to kill the script, use:

./dyndns-firewall.pl -k

When it's terminated, it will remove all rules that were added by it and the DYNDNS chain as well.  So it's important to note that if you stop it, the firewall will not have any rules to allow access from your IP—you're left out.  Be careful to keep one ssh session open if you do that, and restart dyndns-firewall.pl before you go away.

If you want to force an IP update during those 5 minutes, do:

./dyndns-firewall.pl -u

That's it.  To auto-start this script when the server starts, we put it in our iptables startup script.  We kill it first (call with “-k”, it won't do any harm if it wasn't already running), then load some manually configured iptables rules, and at the end, start dyndns-firewall.pl with the “trusted” hostnames.  And now I'm a happy PPPoE user. :-)

Thanks go to

for great ideas and suggestions.

Comments

  • By: software developmentOct 29 (15:39) 2009RE: Dynamic Firewall for PPPoE §

    That was an inspiring post,

    The script is really helpful,

    Keep up the good work,

    Thanks for bringing this up

  • By: reg cleaner reviewsApr 07 (09:57) 2010RE: Dynamic Firewall for PPPoE §

    wow i didnt read this before. Well u guys r right the broth does rock.

    • By: IR RepeaterMay 27 (16:43) 2010RE[2]: Dynamic Firewall for PPPoE §

      I had a fixed IP so it was easy to configure firewall on various machines that I can access, to allow access only from my home IP.  Every once in a while, my router will reconnect and will get me a brand new IP and machines where I configured a firewall won't let me in.  As packets pass through the firewall, packet header information is examined and fed into a dynamic state table where it is stored.  For some broadband Internet connections such as cable and DSL and business-grade Internet connections using T1/E1 or faster, the ISPs inform their customers (subscribers) to set their firewall to specifically set IP address into their firewall.

Page info
Created:
2007/12/18 20:11
Modified:
2007/12/19 12:07
Author:
Mihai Bazon
Comments:
4
Tags:
linux, perl, programming
See also