How to offer public WiFi without torpedoing domestic security

last updated: $Date: 2009/10/23 09:55:56 $

Several times recently, I've been surprised and grateful to find a public WiFi access point in places where I needed to get online and had no other easy way of doing it. I decided that the very least I could do was offer a similar service to the public where I live. However, I didn't want to compromise the security or usability of my home wireline network. This is how I decided to do it.

In broad outline, I separated my wireless network from my internal network by putting it on a third leg of my firewall. I used iptables to keep wireless traffic off my internal network, except where it was authorised. OpenVPN was used to authorise traffic to pass from the wireless into the internal network, and to protect that traffic in the air with strong encryption. Finally, iproute2 was used to restrict the internet bandwidth that unauthenticated wireless users could use. You can read about each of these bits in the firewall, OpenVPN and traffic shaping sections of this document.

Network geometry

Here is the physical network geometry. The mandatory bit is the separation (ie, the firewall) between the wireless and wireline networks; if you don't do that, you can't have public WiFi and security, because nothing's in a position to make sure that packets don't get between wireless and wireline unless they ought to.

The dia source for this diagram is here.

The firewall is a general-purpose Linux box, not a commodity device. That's pretty much mandatory, too, unless your commodity device is sufficiently Linux-based that you can use these tools. The wireless access point is a commodity device (in my case, a Linksys). It's not doing anything clever at all, not even DHCP, so it plays no role other than a frame translator between wireless and wireline networks. It needs no brain.

The rest of this document assumes you have a setup like the one above already running, with wireless and wireline networks working, the firewall dispensing addresses via DHCP on at least the wireless network, handling NAT between the internal networks and the internet, and handling routing between all three networks. If you don't have that working yet, don't go any futher; build the network first.

Firewall

The firewall script, in broad outline, allows anything from eth1 (inside) to anywhere else, anything from eth2 to the internet (eth0) on privileged ports (a step I've had to take after some users decided to run file-sharing software 7*24), but from the internet to anywhere and from eth2 to eth1 is tightly restricted (basically, to return-half packets from connections initiating inbound).

Here are some of the FORWARD rules. The firewall also has a tight set of INPUT rules, but those aren't relevant to this discussion (don't forget to allow inbound openvpn connections on TCP port 1194). These lines are taken from /etc/sysconfig/iptables.

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
#
# (input rules have been deleted here)
#
#############################################################
# FORWARD chain
#############################################################
#
# accept all those on the internal interfaces
-A FORWARD -i lo   -j ACCEPT
-A FORWARD -i eth1 -j ACCEPT
#
# allow return packets from connections we initiated
-A FORWARD -i eth0 -m state --state ESTABLISHED -j ACCEPT
-A FORWARD -i eth2 -m state --state ESTABLISHED -j ACCEPT
#
# and accept anything on a valid tun interface, since that's openvpn
# traffic
-A FORWARD -i tun+ -j ACCEPT
#
# allow certain classes of icmp
-A FORWARD -i eth0 -p icmp -m state --state ESTABLISHED --icmp-type echo-reply -j ACCEPT
-A FORWARD -i eth2 -p icmp -m state --state ESTABLISHED --icmp-type echo-reply -j ACCEPT
-A FORWARD -i eth0 -p icmp --icmp-type destination-unreachable -j ACCEPT
-A FORWARD -i eth0 -p icmp --icmp-type ttl-exceeded -j ACCEPT
#
# allow wireless traffic to internet only, and only to regular ports or from them
-A GKT-FORWARD -i eth2 -o eth0 -p tcp --dport 1:1023 -j ACCEPT
-A GKT-FORWARD -o eth2 -i eth0 -p tcp --sport 1:1023 -m state --state ESTABLISHED -j ACCEPT
-A GKT-FORWARD -i eth2 -o eth0 -p udp --dport 1:1023 -j ACCEPT
-A GKT-FORWARD -o eth2 -i eth0 -p udp --sport 1:1023 -m state --state ESTABLISHED -j ACCEPT
-A GKT-FORWARD -i eth2 -j LOG --log-prefix "WIFI REJECT: "
-A GKT-FORWARD -i eth2 -j REJECT
-A GKT-FORWARD -o eth2 -j LOG --log-prefix "WIFI REJECT: "
-A GKT-FORWARD -o eth2 -j REJECT

#
# allow DNS query responses
-A FORWARD -i eth0 -p udp --sport 53 -m state --state ESTABLISHED -j ACCEPT
#
# finally, deny all other packets to FORWARD and LOG them.  let's see what's
#  hitting us...
-A FORWARD -j LOG --log-prefix "FORWARD DENY: "
#
COMMIT

OpenVPN

OpenVPN is a lightweight, SSL-based VPN that can be used to authenticate an endpoint to a network, and to encrypt traffic passing between the endpoint and the network.

OpenVPN comes as standard on my laptop, as part of Fedora; my firewall is running CentOS 5, and OpenVPN packages are available from Dag Wieers' repository. I advise you to get a packaged version if you can; the version numbers on both ends don't need to match precisely (one client is v2.1, the server is v2.0.9) as it's a stable and interoperable beast.

As I found out from this HOWTO, the trick is a to create a master signing key and certificate on the server, and use this to create signed server- and client-side certificates. Everyone gets a copy of the master signing certificate, which the server uses to verify the bona fides of the clients, and indeed vice versa.

Your packaged install will probably have the easy-rsa subdirectory as part of the distribution. As the HOWTO advises, this should be copied into some third-party location, and I linked it through to make it easier to access:

cd /etc/openvpn
cp -rp /usr/share/doc/openvpn-2.0.9/easy-rsa .
ln -s easy-rsa/keys keys
cd easy-rsa
First, edit the vars file and prepare to create the rest of the certificates, replacing servername with the FQDN of your server, and clientname with the FQDN of your client:
. ./vars
./clean-all
./build-ca

./build-server-key servername

./build-key clientname
This last step should be repeated once for each client whom you wish to be able to connect.

The server will need to be configured on both client and server. My sample configs can be found here for both client and server. The keys will need to be copied to the clients (ca.crt and client.{csr,crt,key} copied into clientmachine:/etc/openvpn).

Start the service on the server with service openvpn start (and ensure that it runs at reboot with chkconfig openvpn on).

On the client, delete any default route that your DHCP server has handed out to you (route delete default gw a.b.c.d). This is because the openvpn server will try to establish a new default route over the tunnel, and that can't be done if you already have one in place. I configured my DHCP server not to hand out a default route for clients likely to use openvpn, but NetworkManager insisted on assigning one anyway (that was true under Fedora 9, but Fedora 10 seems to have got brighter).

Now, start the openvpn service on a client with service openvpn start. You should see a device tun0 appear on both client and server, eg:

tun0      Link encap:UNSPEC  HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00  
          inet addr:10.11.0.10  P-t-P:10.11.0.9  Mask:255.255.255.255
          UP POINTOPOINT RUNNING NOARP MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:100 
          RX bytes:0 (0.0 b)  TX bytes:0 (0.0 b)
and your client's routing table (netstat -rn) should now say:
Kernel IP routing table
Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
10.11.0.1       10.11.0.9       255.255.255.255 UGH       0 0          0 tun0
10.11.0.9       0.0.0.0         255.255.255.255 UH        0 0          0 tun0
192.168.2.0     0.0.0.0         255.255.255.0   U         0 0          0 eth1
0.0.0.0         10.11.0.9       0.0.0.0         UG        0 0          0 tun0
Note the default route via tun0 in the last line. That's what we want to see.

Now, check connectivity to your wireline network via the tunnel. In my case, I have a desktop on 192.168.3.11; now the tunnel is up, I can telnet to the ssh port on it, and receive an ssh banner:

[madhatta@tiananmen ~]$ telnet 192.168.3.11 22
Trying 192.168.3.11...
Connected to risby (192.168.3.11).
Escape character is '^]'.
SSH-2.0-OpenSSH_4.5

Protocol mismatch.
Connection closed by foreign host.
You should also check that you can't do this when the tunnel is down. If you decide to use ping to do your connectivity tests, make sure it's not being blocked by firewalls on either the client or the server; or both.

Traffic shaping

I also want to prevent wireless clients from using up most of my internet bandwidth. The tool do do this is part of the
iproute2 toolchain, which is already installed on my CentOS box (the package is called simply iproute; the version number is 2.x).

Most of what follows came from this HOWTO, to whose authors I am grateful. I also make heavy use of the hierarchical token-bucket queueing discipline, which is well-documented by its author here. I freely admit that I haven't done much with traffic shaping before, and this is mostly monkey-see-monkey-do; it does seem to work, though.

Before I go on, I must emphasise that traffic-shaping only works on outbound traffic from an interface. You can't directly control what you receive; only what you send. All of what follows will appear to be arse-face unless you realise I'm setting limits on what goes out an interface, not what comes in it.

You can interrogate your current queueing setup as follows (I have inserted gratuitous linefeeds for the sake of readability):

[root@balkerne ~]# tc qdisc show
qdisc pfifo_fast 0: dev eth2 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
qdisc pfifo_fast 0: dev eth1 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
qdisc pfifo_fast 0: dev eth0 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
qdisc pfifo_fast 0: dev tun0 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1

[root@balkerne ~]# tc class show dev eth0

[root@balkerne ~]# tc class show dev eth2

[root@balkerne ~]# tc filter show dev eth0

[root@balkerne ~]# tc filter show dev eth2

[root@balkerne ~]# 
This is the default queueing discipline. It does no shaping nor QoS. My queueing setup looks like this:
[root@balkerne ~]# tc qdisc show
qdisc htb 1: dev eth2 r2q 10 default 20 direct_packets_stat 3
qdisc sfq 10: dev eth2 parent 1:10 limit 128p quantum 1514b perturb 10sec 
qdisc sfq 20: dev eth2 parent 1:20 limit 128p quantum 1514b perturb 10sec 
qdisc pfifo_fast 0: dev eth1 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
qdisc htb 1: dev eth0 r2q 10 default 10 direct_packets_stat 0
qdisc sfq 10: dev eth0 parent 1:10 limit 128p quantum 1514b perturb 10sec 
qdisc sfq 20: dev eth0 parent 1:20 limit 128p quantum 1514b perturb 10sec 
qdisc pfifo_fast 0: dev tun0 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1

[root@balkerne ~]# tc class show dev eth0
class htb 1:1 root rate 400000bit ceil 400000bit burst 1650b cburst 1650b 
class htb 1:10 parent 1:1 leaf 10: prio 0 rate 380000bit ceil 400000bit burst 1646b cburst 1650b 
class htb 1:20 parent 1:1 leaf 20: prio 1 rate 20000bit ceil 100000bit burst 1602b cburst 1612b 

[root@balkerne ~]# tc class show dev eth2
class htb 1:1 root rate 100000Kbit ceil 100000Kbit burst 14100b cburst 14100b 
class htb 1:10 parent 1:1 leaf 10: prio 0 rate 99800Kbit ceil 10000Kbit burst 14071b cburst 2850b 
class htb 1:20 parent 1:1 leaf 20: prio 1 rate 200000bit ceil 500000bit burst 1624b cburst 1662b 

[root@balkerne ~]# tc filter show dev eth0
filter parent 1: protocol ip pref 49152 fw 
filter parent 1: protocol ip pref 49152 fw handle 0x7 classid 1:20 

[root@balkerne ~]# tc filter show dev eth2
filter parent 1: protocol ip pref 49152 u32 
filter parent 1: protocol ip pref 49152 u32 fh 800: ht divisor 1 
filter parent 1: protocol ip pref 49152 u32 fh 800::800 order 2048 key ht 800 bkt 0 flowid 1:10 
  match 04aa0000/ffff0000 at 20

[root@balkerne ~]# 
This is done with this shell script; I could embed it into a system startup script (/etc/rc.d/rc.local, in the worst case, I suppose) and I probably will. At the moment, however, it's run by hand.

The vital stuff is that:

I know this works, because without the shaping, I can download from the internet onto the wireless at (cable modem) wireline speeds; with the shaping in place, I can download at wireline speeds when the OpenVPN client is up, but without it I'm limited to 50-60kB/s (that is, 500kbit/s) download (ie, traffic coming out eth2).

The only thing that doesn't currently seem to work is that when I'm using privileged and unprivileged clients at the same time, the unpriv clients still seem to get their ceiling limits; what I want is for them to only get their rate limits when there's contention. Clearly, I need to learn more about HTB and tc in general. When I get it right, I'll update this document.

Back to Technotes index
Back to main page