Thursday, June 21, 2007

Walled Garden: FreeBSD + natd + ipfw + squid

This is going to be an overview of the steps it takes to create a Walled Garden using FreeBSD, natd, ipfw and squid.

The basic scenario: You have a private IP network that you want to allow people to connect with, and you allow them basic web access (we'll just do port 80 for now). For your default access you only want to allow these users to access certain URL's - if they try to access anything else it will redirect them to your "portal" page. Presumbably your portal would have software that would do account signups and such, and once you authorize an ip you would allow it to connect to anything on the internet. Portal design won't be discussed here, but I will show you how to punch a whole through the firewall.


For this exercise we are going to have a private ip network, and a public ip. Splitting off a management IP is highly advisable, but that won't be covered here.

Our private IP network is going to be 10.7.0.0/16 our "public ip" is going to be 192.168.0.1 (which is really private, but ignore that - when deploying this substitute in a real public ip here)

First things first, you need to make sure your kernel has some options compiled into it, before doing anything else, go compile these in right now:

options IPFIREWALL
options IPDIVERT
options IPFIREWALL_FORWARD

Once you install that kernel and reboot your server we can proceed with configuration.

For the next step let's go ahead and install squid. This can be done using whatever method for installing software you prefer, but I'm going to list the package add method, because it's so simple:

# pkg_add -r squid


And that will get your squid installed.

For my installation the public interface is em0 and the private is em1.

Put the following in your rc.conf :

defaultrouter="192.168.0.254" #make sure to put YOUR defaultrouter in, not this one
hostname="wall.yourdomain.com"
ifconfig_em0="inet 192.168.0.1 netmask 255.255.255.0" #again, your IP, and your netmask
ifconfig_em1="inet 10.7.255.254 netmask 255.255.0.0" #we are setting 10.7.255.254 to be the gateway of our walled garden machines
gateway_enable="YES"
firewall_enable="YES"
firewall_script="/etc/rc.ipfw-walledgarden"
natd_enable="YES"
natd_interface="em0"
squid_enable="YES"


A note here - if you confuse your internal (private) and external (public) interfaces you are not going to be able to pass traffic from inside of your private network to the world. You can waste a lot of time fighting the sillyness of typing em1 instead of em0 (or vice-versa).

Now let's edit /usr/local/etc/squid/squid.conf
(Edit: Squid changed it's config at 2.6, modified entry to include both versions)
Squid <2.6:
acl garden_customers src 10.7.0.0/16 127.0.0.1
http_access allow garden_customers
http_reply_access allow all
httpd_accel_host virtual
httpd_accel_uses_host_header on
httpd_accel_with_proxy on
ie_refresh on
redirect_program /usr/local/bin/walled_garden
Squid >= 2.6:
acl garden_customers src 10.7.0.0/16 127.0.0.1
http_access allow garden_customers
http_reply_access allow all
http_port 127.0.0.1:3128 transparent
ie_refresh on
redirect_program /usr/local/bin/walled_garden

This set's squid up to act as a transparent proxy for the relevant networks, and hands off the job of figuring out what to do with redirecting url's to an external program named "walled_garden" (included later).

Here is a sample you can use to start yourself off for /etc/rc.ipfw-walledgarden:
#!/bin/sh


ipfw="ipfw -q"

$ipfw -f flush

private_if="em1"
public_if="em0"
public_ip="192.168.0.1"
private_ip="10.7.255.254"


$ipfw add 00050 divert natd ip4 from any to any via $public_if


#Setup loopback
$ipfw add 00060 allow ip from any to any via lo0
$ipfw add 00061 deny ip from any to 127.0.0.0/8
$ipfw add 00062 deny ip from 127.0.0.0/8 to any

#allow our firewall to talk DNS directly
$ipfw add 00070 allow udp from $public_ip to any 53
$ipfw add 00071 allow udp from any 53 to $public_ip

$ipfw add 00074 allow tcp from $public_ip to any 80
$ipfw add 00075 allow tcp from any 80 to $public_ip

#Allow icmp to the gateway IP, deny everything else from private network from talking to gateway
$ipfw add 00100 allow icmp from 10.7.0.0/16 to $private_ip
$ipfw add 00101 deny ip from 10.7.0.0/16 to $private_ip

#This needed?
$ipfw add 00120 allow tcp from 10.7.0.0/16 to $private_ip 3128

#Allow dns for private network
$ipfw add 00130 allow udp from 10.7.0.0/16 to any 53 via em1
$ipfw add 00131 allow udp from any 53 to 10.7.0.0/16

#An authorized client
$ipfw add 10000 skipto 65000 ip from 10.7.0.1 to any


#walled garden - forces through transparent squid proxy
$ipfw add 64000 fwd 127.0.0.1,3128 tcp from 10.7.0.0/16 to any dst-port 80

#Allow web for private network
$ipfw add 64140 allow tcp from 10.7.0.0/16 to any 80 via em1
$ipfw add 64141 allow tcp from any 80 to 10.7.0.0/16


#$ipfw add 65000 allow log logamount 1000 ip from any to any
$ipfw add 64500 deny log logamount 1000 ip from any to any

$ipfw add 65500 pass all from any to any


This is a really rudimentary firewall setup, it just allows DNS and port 80 web traffic through. If you look at rule 10000 that is a rule that is specifically exempting the ip 10.7.0.1 from being trapped in the walled garden. It does this by skipping past the section of firewall rules dealing with that, and goes straight up to the end of the firewall where it get's to do whatever it wants. For a production deploy you should get more hardcore about protecting the firewall box itself as well.

The last bit you need is your walled_garden script that let's you decide what is good and what isn't. I've written a pretty lame one, but it does the trick of showing the example:

Contents of /usr/local/bin/walled_garden:
#!/usr/bin/perl

use warnings;
use strict;
use Sys::Syslog qw(:DEFAULT setlogsock);
setlogsock('unix');
openlog("walled_garden", "pid", "auth");
syslog('info', "started");

my $portal = 'portal.yourdomain.com';

$| = 1;

while(<>) {
chomp;

my ($orig_url, $ip,$host) = (m#^(\S+)\s([^/]+)/(\S+)#);

my $new_url = $orig_url;
my $accept = 0;
my ($mech,$request) = $new_url =~ m#^(\w+)://(\S+)#;

if ($request =~ m#^(?:portal.yourdomain.com|www.yourdomain.com|webmail.yourdomain.com/webmail)#) {
$accept = 1;
}


unless ($accept) {
$new_url = $mech . '://' . $portal;
}

my $out = $new_url . " " . $ip . '/' . $host;

syslog('info', "client:$ip host:$host requested $orig_url, sending to $new_url");
print "$out\n";
}
syslog('info', "finished");

And that is it! The general gist isn't hard to do - but messing up any of the details can be quite tricky to troubleshoot, so move in baby steps if you have to. When setting up your firewall I'd recommend using this trick - first turn your firewall off, then:
sysctl net.inet.ip.fw.enable=1 && sleep 10 && sysctl net.inet.ip.fw.enable=0
It will turn your firwall on for 10 seconds and then automatically shut it off. If you are working remotely this is a very good thing, as botching firewall rules and locking yourself out of them is very frustrating.