This is a little sub-project of what I’ve been working on recently – a hideously over-engineered Raspberry Pi-based system to measure the amount of beer left in the kegs in my keezer.

Normally I would simply set up a web server on the Pi and have it on the home network, so I could see the levels remotely. The problem is that the routers are all inside the house and the Pi is in the garage, invisible to them all thanks to the 2 external walls between them. I needed some way to read out the beer levels on my phone – after all, walking up to something and looking at the level gauge is so last millennium.

So – Bluetooth or some sort of ad-hoc Wifi thing? I like to re-use stuff I’ve got lying around in drawers, so the solution seemed to be an old WiFi dongle that was gathering dust. And Bluetooth is awful. Setting up a Pi as an access point is fairly well covered on the internets, but this is a bit different in that we don’t want to forward traffic onto our network like an access point – not that it could connect anyway, being out of range. I also didn’t want to install a web server on the Pi. It’s only a Pi 1 model B, so sticking Apache and PHP on it might be asking a bit much – especially when you can do it all with one command and a small BASH script.

So the cunning plan was to take advantage of a feature of public access points – the ones that show you a registration page for you to fill in with fake info.

When you connect to a public WiFi hotspot your device tries to load a page on the internet using non-SSL http. It might be any page (captive.apple.com/ seems to be popular), but it will be a web page that the device knows should exist and if it loads, your device knows the internet is working.

A public access point intercepts the page request and, rather than forwarding it, sends a 30x redirect HTTP response back to the device – basically hijacking the request and spoofing the reply. Your device then loads up the page it has been redirected to and displays it as a sign-in page.

It is this mechanism that I used to show the keg levels on any phone, just by connecting to the Wifi. This is how to do it if you want to do something similar. I’m assuming you SSH on to a Pi connected with an ethernet cable to your network, and you have a Wifi dongle hanging out of its USB port. In all likelihood they will be eth0 and wlan0 respectively, so I’ll use them.

wlan0 is going to use a different range of IP addresses from the ones used by eth0, so edit /etc/dhcpcd.conf to manually assign an IP address to the wlan0 interface. Add this at the bottom (comment out any existing definition for wlan0):

interface wlan0
    static ip_address=192.168.4.1/24
    nohook wpa_supplicant

Next we need to install hostapd to run the hotspot and dnsmasq to sort out assigning IP addresses to devices that connect.

sudo apt-get install hostapd
sudo apt-get install dnsmasq
sudo systemctl stop hostapd
sudo systemctl stop dnsmasq

The second two commands disable the services we just installed so we can edit config files before starting them again.

Create the file /etc/dnsmasq.conf and put this in it:

interface=wlan0      # Usually wlan0
dhcp-range=192.168.4.2,192.168.4.20,255.255.255.0,24h
address=/#/192.168.4.1

This tells dnsmasq to assign the range 192.168.4.2 – 192.168.4.20 with a netmask of 255.255.255.0 and a lease time of 24 hours. The third line tells it to return the server address for all domain lookups that aren’t in /etc/hosts, i.e. all of them. When dnsmasq restarts it will look at this file and load up the config information.

Now to set up hostapd. Create /etc/hostapd/hostapd.conf and put this in it:

interface=wlan0
driver=nl80211
ssid=Your SSID here
hw_mode=g
channel=7
wmm_enabled=0
macaddr_acl=0
ignore_broadcast_ssid=0

It’s pretty obvious what is happening there, other than some of the technical bits; wmm_enabled is something to do with packets (no idea what, though), macaddr_acl tells it to whitelist all connections and ignore_broadcast_ssid tells it to broadcast the SSID – set it to 1 to hide it. There is no WPA password or setup, obviously. Change the SSID to something hilarious.

Now you need to tell hostapd where to find the config file when it starts. Edit /etc/default/hostapd and add (or uncomment and edit) the line:

DAEMON_CONF="/etc/hostapd/hostapd.conf"

We have now set up our access point. Start dnsmasq and hostapd again:

sudo systemctl start hostapd
sudo systemctl start dnsmasq

If there are no errors, your AP should show up in the list of APs on your phone, laptop etc. Try connecting to it – it should connect but you won’t be able to see the internet because there is no forwarding. One thing you can still do however, is connect to SSH on the Pi. You really don’t want any ports other than 80 visible from an unsecured AP. We’ll use iptables to set up a firewall and do the test page hijacking.

sudo iptables -A INPUT -p tcp -i wlan0 --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp -i wlan0 --dport 53 -j ACCEPT
sudo iptables -A INPUT -p tcp -i wlan0 -j DROP
sudo iptables -t nat -A PREROUTING -p tcp -i wlan0 --dport 80 -j DNAT --to-destination 192.168.4.1:80

The first two tell iptables to allow through connections on port 80 (HTTP) and 53 (DNS), the second tells iptables to drop all other TCP connections from wlan0. The third redirects any connection with a destination port 80 (regardless of the IP address) to the Pi at IP address 192.168.4.1, port 80, for our server to handle. If you are a bit confused about how iptables work, this flowchart will either clear things up or make it more confusing. Basically there are 4 tables – filter (default if no -t switch), nat, mangle and raw which each contain “chains” such as INPUT which are the instructions on how to route traffic. It’s a vast subject and I learned just enough to work out the 3 lines above. There are other guides that go into more details.

One thing to do at this point is make it so that the iptables configuration is not lost when the system is rebooted. This command saves it to a file:

sudo iptables-save >/etc/iptables.ipv4.nat

To reload the configuration on boot put this in /etc/rc.local

iptables-restore < /etc/iptables.ipv4.nat

So, moving on to the web server. I’m using socat and a bash script. socat is one of those amazing Linux tools that is impossible to explain to a layperson. “What it does is, it takes data from one place and puts it in another but it’s more complicated than that…” and so on. Best just to tell them it’s the computer equivalent of magic, before their eyes glaze over and they start thinking about feigning an illness in order to escape. We are going to use it to pipe data from an internet port to a script and back again. Incoming text from port 80 is sent to the script on stdin and anything written to stdout gets sent back to the port. It’s easy enough to set up with this command:

sudo socat TCP4-LISTEN:80,reuseaddr,fork EXEC:"/home/your_path_here/server.sh >/dev/null" 2>/dev/null &

Obviously change “your_path_here” to where you are doing all this stuff and put this line in /etc/rc.local if you want it to start automatically on boot. The command tells socat to listen on port 80 and then fork off the script when there is a connection. The script referred to as /home/your_path_here/server.sh is this:

#!/bin/bash

PAGE_NAME="kegs"
FOUND_URL="http://1.1.1.1/$PAGE_NAME"

request=""
while read -r  -t 5 line; do
  if [[ ! -z "${line:-}" && $line == *[^[:cntrl:]]* ]]; then
    if [[ ${line:0:4} == "GET " ]]; then
      request=$(expr "$line" : 'GET /\(.*\) HTTP.*')
    fi
  else
    break
  fi
done

if [[ "$request" == "$PAGE_NAME" ]]; then
  printf "HTTP/1.1 200 OK\n"
  printf "Content-Type: text/html\n\n"
  cat index.html	# Show this as a registration page.
else
  printf "HTTP/1.1 302 Found\n"
  printf "Location: $FOUND_URL\n"
  printf "Content-Type: text/html\n\n"
  printf "Redirect to <a href=\"$FOUND_URL\">$FOUND_URL</a>\n"
fi

That’s pretty dinky for a web server, huh? Don’t forget to change permissions of server.sh with chmod 755 server.sh. Rename PAGE_NAME and FOUND_URL to whatever you want. Note that because we are grabbing all port 80 traffic coming in on wlan0, it doesn’t matter what you put for an IP address – it’ll all go to our server. The first block of code reads the HTTP request coming from the device, which will be saying something along the lines of:

GET / HTTP/1.1
Host: captive.apple.com
Accept: image/gif, image/jpeg, */*
... and so on

The script ignores everything except the GET /… part, from which it extracts the page name, if any. It won’t match (unless the test page is called “/kegs” – unlikely), so it will respond with the redirect code 302, to send the device to “/kegs”. The device sees the redirect, thinks it’s for a registration page and loads 1.1.1.1/kegs. This time the script sees that /kegs has been requested, sends a 200 OK code and the contents of index.html, which the device displays. My beer measurement system generates index.html as a page showing how much is left in each keg.

As a useful tool with which to quickly see the levels of my kegs without any fuss, this is rubbish, quite frankly. But then the whole raspberry-pi-based-keg-measurement thing could be replaced with cheap mechanical bathroom scales, so I might as well go all in on the pointless technology.

Updated 10/6/2020 : Improved the firewall rules.
Updated 18/2/2021 : Improved DNS rules.