last updated: $Date: 2018/08/01 06:52:21 $
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.
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.
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.
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:
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:
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:
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).
*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.
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.
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.
[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
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:
The restriction is applied to all traffic which came in interface eth2. This is done with the iptables mangle table, the following fragment coming from /etc/sysconfig/iptables:
*mangle :PREROUTING ACCEPT [0:0] :INPUT ACCEPT [0:0] :FORWARD ACCEPT [0:0] :OUTPUT ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] # add a mark for traffic coming FROM wireless, for later traffic-shaping -A PREROUTING -i eth2 -j MARK --set-mark 7 # COMMITThe line of tc which sends appropriately-tagged traffic into the restricted queue (1:20) is this:
tc filter add dev eth0 protocol ip parent 1:0 handle 7 fw flowid 1:20handle 7 corresponds to the earlier --set-mark 7.
OpenVPN traffic isn't affected by this, as OpenVPN traffic coming in eth2 terminates on the firewall; decrypted and decapsulated traffic intended for the internet is seen by the firewall as having "come in" on interface tun0 (or tun1, tun2 for other connections) and thus is not tagged. This also means that traffic between the wireline network and the internet goes into the unrestricted queue, by default.
U32="tc filter add dev eth2 protocol ip parent 1:0 u32" $U32 match ip sport 1194 0xffff flowid 1:10which put any traffic with source port 1194 (ie, OpenVPN traffic) into queue 1:10. I could probably have done this with the mangle table as well, but I was trying to get my head around u32 filtering rules (not very successfully).
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.