Raspberry pi router update -- quicker route switching

After using my pi vpn gateway hotspot for a while, I’ve found the Chromecast’s method for switching wifi networks pretty slow and frustrating, so I made a one-button solution.

Quick switch page

I originally set up the pi router to make access to American Netflix content easier; however, I still often want to access their Canadian content too. But it turns out that the Chromecast isn’t made to frequently and reliably switch wifi networks. Since it won’t save multiple wifi passwords, you need to type in your WPA key each time you switch. The Chromecast also often fails to connect to the new network – this happens reliably if you didn’t disconnect it from an app, and occasionally also if you did, forcing a hard reboot of the device. Overall the process of swapping networks is more work, and more frustrating, than it should be.

So I thought maybe setting up a simple way of changing the routing tables on the fly would be easier than constantly switching networks.

If you don’t remember my network setup from the previous post, it goes a little something like this:

network diagram

Traffic from the dietrich wifi network is routed over the VPN, through the dietrich server, which is in the states. So all we need to do to get back to Canadian Netflix is reroute that traffic over eth0 on the pi to make it originate from my local IP.

Setup

On the server, the switch only requires a couple of small changes. To the /etc/init.d/ip_forward init script, we add:

iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

This allows NATting through eth0 – we leave in the equivelant line for tun0 since we want to be able to NAT through both interfaces.

In /etc/openvpn/routing.sh, the file that sets up our routing tables, we add the following line to the local routing table section:

ip route add 192.168.1.0/24 dev wlan0 src 192.168.1.1 table ether

Note that there is an almost identical line for the vpn_hotspot table. This is ok, because these routes only apply to traffic that has already been directed to the routing table; when wlan0 traffic is sent to one table, the rule in the other table will simply be ignored.

Swapping the routes

That’s it for setup. The router now knows how to route and NAT wlan0 traffic that is sent to either table. Then to actually swap the routes, you run this in a root terminal:

ip rule del from 192.168.1.0/24 table vpn_hotspot
ip rule add from 192.168.1.0/24 table ether

This tells the system which routing table to direct the traffic on wlan0 (192.168.0.1/24) to the ether routing table, rather than the vpn_hotspot table, and what do you know, it works as expected!

Making it easier

This is pretty simple, but having to ssh into the pi every time you want to switch is more work than I want to do. I really should just be able to press a button.

I set up a web-controlled script to do the switching; it wound up looking like this:

Quick switch page

It displays the current routing table being used for wifi traffic, and gives a nice big button to switch it. I bookmarked it in Firefox, and added a shortcut to my phone’s home screen, so now it takes all of two clicks to swap routes, rather than navigating menus and typing in wifi passwords.

I’ve been wanting to pick up go, so this was an opportunity for me to try something simple. This is the first thing I’ve done in go, so please forgive me if my code isn’t idiomatic.

netswitch.go

For security reasons, this “app” is split into two programs. All the first one does is check the current ip rules to determine which routing table our wlan0 traffic is going to, and then switch it.

// The code in this post is too simple to bother licensing, so 
// consider it as being under the WTFPL.
package main

import (
    "fmt"
    "os/exec"
    "strings"
)

// edit these values for your own network:
var routing_tables = []string{"vpn_hotspot", "ether"}

const wifi_network = "192.168.1.0/24"

func main() {
    fmt.Println(getCurrentNetwork())
    switchRoute()
    fmt.Println(getCurrentNetwork())
}

func getCurrentNetwork() string {
    // equivelant of ip rule list | grep $WIFI_NETWORK | 
    //          awk '{print $NF}' 
    // (the last word on the matching ip rule is the current 
    // routnig table)
    c := exec.Command("ip", "rule", "list")
    result, _ := c.Output()
    lines := strings.Split(string(result), "\n")
    for _, line := range lines {
        if strings.Contains(line, wifi_network) {
            words := strings.Split(strings.TrimSpace(line), " ")
            return words[len(words)-1]
        }
    }
    return ""
}

func getOtherNetwork(current string) string {
    for _, net := range routing_tables {
        if net != current {
            return net
        }
    }
    return ""
}

func switchRoute() {
    current := getCurrentNetwork()
    cmd := exec.Command("ip", "rule", "del", "from", wifi_network, "table", current)
    cmd.Run()
    cmd2 := exec.Command("ip", "rule", "add", "from", wifi_network, "table", getOtherNetwork(current))
    cmd2.Run()
}

Particularly important here is that this code accepts no user input. The ip route del and ip route add commands must be run with root privileges, so we don’t want to allow any possibility of arbitrary code execution. It only switches between two pre-defined available networks.

If I were to add a second VPN in another country (which I may do at some point), the easiest option would be to add a third routing table, and change the getOtherNetwork() function to return the next one in the list, so the button cycles through the three. The swap only takes about 30ms, so this isn’t really going to cost us any time compared to choosing from a menu.

Since this needs to be run as root, we’ll set its ownership to root, and set its setuid bit

brad@pi ~/netswitch% go build netswitch.go 
brad@pi ~/netswitch% sudo chown root netswitch
brad@pi ~/netswitch% sudo chmod 4750 netswitch
brad@pi ~/netswitch% ls -l netswitch
-rwsr-x--- 1 root brad 1.8M Jun  4 14:05 netswitch

This command now runs as root, and can be run by members of the brad group:

brad@pi ~/netswitch% ./netswitch 
vpn_hotspot
ether

switchui.go

The netswitch command is in turn run by a simple instance of go’s net/http webserver:

package main

import (
    "fmt"
    "html/template"
    "net/http"
    "os/exec"
    "strings"
)

// change these for your network.
const wifi_network = "192.168.1.0/24"
const listen_address = "192.168.1.1:1234"

type Content struct {
    Network string
}

func handler(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path[1:] == "switch" {
        cmd := exec.Command("./netswitch")
        cmd.Run()
        http.Redirect(w, r, "/", http.StatusFound)
        return
    }
    var content = Content{getCurrentNetwork()}
    t := template.Must(template.ParseFiles("index.html"))
    t.Execute(w, content)
}

func main() {
    http.HandleFunc("/", handler)
    fmt.Println("Listening on " + listen_address)
    http.ListenAndServe(listen_address, nil)
}

func getCurrentNetwork() string {
    c := exec.Command("ip", "rule", "list")
    result, _ := c.Output()
    lines := strings.Split(string(result), "\n")
    for _, line := range lines {
        if strings.Contains(line, wifi_network) {
            words := strings.Split(strings.TrimSpace(line), " ")
            return words[len(words)-1]
        }
    }
    return ""
}

Note that the listen address is on the wlan0 network. I don’t want anyone from the outside net stumbling on this interface and playing with my routes. Not that it’s risky, it would just be annoying to have a random stranger reroute my network in the middle of a movie.

Lastly, we need the html template, index.html:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en-us">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" user-scalable="no"/>
    <link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:500,800' rel='stylesheet' type='text/css' />
    <style>
        body {
            font-family: "Alegreya Sans";
        }
        a {
            display: block;
            height: 100px;
            width: 200px;
            font-size: 2em;
            background-color: #33b5e5;
            margin-right: auto;
            margin-left: auto;
            border-radius: 5px;
            text-align: center;
            line-height: 100px;
            text-decoration: none;
            color: black;
        }
    </style>
</head>
<body>
    <h2>Routing through {{ .Network}}</h2>
    <p>
        <a href="/switch">Switch</a>
    </p>
</body>

You could pretty it up some more, but I didn’t see much need.

Finally, build and run it:

brad@raspberrypi ~/netswitch% go build switchui.go
brad@raspberrypi ~/netswitch% nohup ./switchui &

I’ve started it with nohup for the time being, but the next time my pi gets rebooted I’ll replace it with a proper init script.

At this point, we connect the Chromecast to the dietrich wifi network, and just leave it there for good. To swap the routing, simply open up pi:1234 (I have dnsmasq set up to resolve pi as its address on the network the DNS request was made from) and hit that nice blue button – though there is one caveat.

Quick switch page

Still not quite perfect

This is a much better solution than swapping networks with the Chromecast’s kludgy UI, but when we switch the routing table, the Chromecast’s (and cell phone/desktop client’s) DNS cache is not cleared, so they can’t immediately access, say, the US Netflix servers if we were recently using the Canadian ones. Power cycling the Chromecast, and closing and restarting the Netflix app on Android, fixes this.

Still not perfect, but quite a bit better.

comments powered by Disqus