HomeKit (1)

Isolating trusted, guest, and IoT HomeKit devices with OpenWRT

April 6, 2021Blog

Lockdowns are a perfect excuse to spend a ridiculous amount of time pretending reality doesn’t exist improving my Home network security.

I switched to using the OpenWRT firmware on my router last year (yes, it was during a lockdown) and never went back to using something else. The OpenWRT team has managed to produce qualitative software that is both very powerful, yet usable. Unlike MicroTik that is hardly approachable by a beginner, OpenWRT found the right balance. Let me be clear, though: network engineering is something I dread as I have a shallow understanding of the networking stack.

While I was using OpenWRT for a year, I still had the classic all-in-one network architecture where all my devices (computers, friends’ phones, IoT devices, …) were using the same network. As IoT devices are prone to hacking, and as we’re never quite sure of our friends’ computer hygiene, it is better to isolate as many devices as possible. I want to sleep at night by knowing my lightbulb won’t hack my NAS because I gave my WiFi password to someone. Yes, that sounds ridiculous, but if the past decade has taught me something, it’s that anything can happen, especially if it is stupid.

How long could it take me to improve my network? A few hours top? Of course no, Dunning-Kruger were right again, and I’m too proud to reveal the number of hours I spent doing that. If that was a walk in the park, I wouldn’t write a cathartic article for that.

This article is for you, the beautiful stranger from the future who had the same idea as me and found this article by nervously scouring the web. I hope this will help you. I hope this will save you some time.

After a 1-minute design sprint while sipping my morning coffee, I ended up wanting to create 3 different networks. The first for trusted devices, the second for guest devices, and the third for IoT devices.

The rules are simple:

  1. Only trusted devices can access IoT devices.
  2. Guest and IoT devices can access the internet, but only through different VPN tunnels (because why not?!).
  3. Guests and IoT devices are not isolated in the same network but cannot cross networks to talk to each other.

I wanted to experiment with several OpenVPN tunnels for different reasons. The first one is protecting my privacy from potentially spying IoT devices. Now that I can afford the luxury, I prefer to hide my real country. It’s not much, but it’s always a plus. I could have used the same VPN for guests, but having an IP in a different country brings some side effects. I didn’t want guests to have the Google homepage in a foreign language.

This setup is not perfect security-wise nor privacy-wise, and I might tighten it in the future once I get better at maintaining it.

By following this rich article by Sven Kiljan (hi Sven! Thanks!), I could setup VLANs, OpenVPN, and had a working version of my network pretty quickly. Unfortunately, this setup quickly showed its limit as the way the OpenVPN routing is handled breaks the possibility for my trusted network to communicate with my IoT network. I spent a ridiculous amount of time trying to understand how to route VLANs together. I tried a lot of things and couldn’t find information on the web. There’s something magic when somehow you’re not googling the right stuff that will bring you the solution. When you don’t know what you don’t know, things can take a long time.

If you want multiple VPN tunnels and if you’re going to route traffic from some subnets to different VPN based on some arbitrary rules, don’t start looking at ip route add, nor ip route get because it is vertiginous.

Here is the grammar for the ip route command.

# ip route help
Usage: ip route { list | flush } SELECTOR
       ip route save SELECTOR
       ip route restore
       ip route showdump
       ip route get [ ROUTE_GET_FLAGS ] ADDRESS
                            [ from ADDRESS iif STRING ]
                            [ oif STRING ] [ tos TOS ]
                            [ mark NUMBER ] [ vrf NAME ]
                            [ uid NUMBER ] [ ipproto PROTOCOL ]
                            [ sport NUMBER ] [ dport NUMBER ]
       ip route { add | del | change | append | replace } ROUTE
SELECTOR := [ root PREFIX ] [ match PREFIX ] [ exact PREFIX ]
            [ table TABLE_ID ] [ vrf NAME ] [ proto RTPROTO ]
            [ type TYPE ] [ scope SCOPE ]
             [ table TABLE_ID ] [ proto RTPROTO ]
             [ scope SCOPE ] [ metric METRIC ]
             [ ttl-propagate { enabled | disabled } ]
INFO_SPEC := { NH | nhid ID } OPTIONS FLAGS [ nexthop NH ]...
	    [ dev STRING ] [ weight NUMBER ] NHFLAGS
FAMILY := [ inet | inet6 | mpls | bridge | link ]
OPTIONS := FLAGS [ mtu NUMBER ] [ advmss NUMBER ] [ as [ to ] ADDRESS ]
           [ rtt TIME ] [ rttvar TIME ] [ reordering NUMBER ]
           [ window NUMBER ] [ cwnd NUMBER ] [ initcwnd NUMBER ]
           [ ssthresh NUMBER ] [ realms REALM ] [ src ADDRESS ]
           [ rto_min TIME ] [ hoplimit NUMBER ] [ initrwnd NUMBER ]
           [ features FEATURES ] [ quickack BOOL ] [ congctl NAME ]
           [ pref PREF ] [ expires TIME ] [ fastopen_no_cookie BOOL ]
TYPE := { unicast | local | broadcast | multicast | throw |
          unreachable | prohibit | blackhole | nat }
TABLE_ID := [ local | main | default | all | NUMBER ]
SCOPE := [ host | link | global | NUMBER ]
NHFLAGS := [ onlink | pervasive ]
RTPROTO := [ kernel | boot | static | NUMBER ]
PREF := [ low | medium | high ]
TIME := NUMBER[s|ms]
BOOL := [1|0]
ENCAPTYPE := [ mpls | ip | ip6 | seg6 | seg6local | rpl ]
SEG6HDR := [ mode SEGMODE ] segs ADDR1,ADDRi,ADDRn [hmac HMACKEYID] [cleanup]
SEGMODE := [ encap | inline ]
ROUTE_GET_FLAGS := [ fibmatch ]

This help message is not helpful. It’s a giant fuck you to all beginners. I don’t say I took it personally, but this is a reminder of how arid and hostile Linux can be. It’s fucking Arrakis.

I was trying to understand routes, tables, and the poor life-choices that drove me in this situation when I did a u-turn and ultimately decided to ask for help online. A stranger (who apparently survived Arrakis’ desert) pointed me to VPN Policy Based Routing. It was the solution! The name sounds pompous and complicated like AWS IAM Policies. But not this one. This one is fine. I promise. I was finally able to have access the IoT subnet from the trusted subnet. I had to switch from Sven’s OpenVPN way of doing things to ProtonVPN’s way.

My second most significant hurdle was HomeKit. Oh boy.

First, HomeKit uses mDNS, also called dns-sd, also called Bonjour, also called avahi, depending on your age, technical level, platform, and use. The principle is “simple”. UDP packets are broadcasted to multicast IP on port 5353. Devices answer on the same port. Besides configuring the firewall for data to move freely from one zone to another, UDP multicast stops at the subnet (to prevent DDoS or something like that). Packets need to be relayed to other subnets. Otherwise, the only thing mDNS software will see are things located in the same subnet. This is precisely what I didn’t want to have.

I spent a few hours trying to find the right way of relaying these packets. I tried on my router to build software from source until I discovered that iptables can forward UDP packets. I was again fearful of trying, but I finally found that forwarding packets is a core feature of avahi, the Linux mDNS daemon! It’s called reflector. What a nice name for a feature! Did I lose my time? No. Each and every learning opportunity is a blessing.

# /etc/avahi/avahi-daemon.conf
# Enable relaying packets over all local interfaces

At this point, I only had to open ports 80, 443, 51827, and 52934 for HomeKit devices to be available from my trusted devices. Some of these ports might be superfluous, but I need to keep some tidying work for future lockdowns.

I now have 3 isolated SSIDs and I can use the IoT devices that I enjoy.