There are two Access Points (APs) in my house: one in the living room, and one in the kitchen. They are connected via LAN-over-powerline, and have WiFi Roaming set up for a seamless connection throughout.
contents…
Roaming
WiFi Roaming allows a client device (e.g. a phone) to transparently switch from one AP to another. Unlike “repeaters” or “boosters”, which create a second WiFi network that the client needs to disconnect from and reconnect to, Roaming is the same network across APs and allows for a seamless experience.
This is implemented with two extensions of the WiFi spec, 802.11i and 802.11r:
- 802.11i is pre-authentication, which allows clients to start associating to a new AP while still connected to the previous one.
- 802.11r is Fast Transition (FT), where APs can share keys with each other. On large networks (e.g. corporate ones) this is done properly where the APs communicate and use intermediate keys and the like, but for a home network with a Pre-Shared Key (PSK), the APs can derive all the keys themselves.
How does 802.11r actually work?
In a given roaming connection, there are 3 keys:
- Master Session Key (MSK).
- R0, a key derived from the MSK.
- R1, a key derived from R0.
There are 3 players:
- The client.
- R0KH, the R0 Key Handler, may or may not be an AP.
- R1KH, the R1 Key Handler, an AP.
In “pull mode”:
- The client connects to the first AP.
- The client is given a key and an R0KH-ID (the NAS Identifier, named for RADIUS’ Network Access Server).
- The client connects to the second AP.
- The client gives R0KH-ID to the second AP.
- The second AP, now R1KH, contacts R0KH using the ID and its own R1KH-ID.
- R0KH sends R1KH a key R1 derived from R0 and R1KH-ID.
- R1KH, gives the client R1.
In “push mode”:
- R0KH knows all its APs.
- R0KH sends each AP a unique R1 derived from R0 and the AP’s R1KH-ID.
- the client connects normally.
For PSK mode, the MSK is the PSK, so any AP can generate R0 and R1 for any NAS Identifier.Router configuration
My router is a PC Engines APU 1d4, which has:
- 3 ethernet ports.
- 1 wifi card.
One of the ethernet ports is for WAN, and the other two and wifi card are LAN. The LAN interfaces are joined with a bridge, br0.
WAN
This is a fairly normal systemd-networkd client config, with the addition of IPForward=yes:
$ cat /etc/systemd/network/wan.network
[Match]
# enp1s0 is the name of my router's WAN interface.
Name=enp1s0
[Network]
# take a IP from my ISP's modem over DHCP.
DHCP=yes
# use Google's public DNS servers.
DNS=8.8.8.8
DNS=8.8.4.4
# prefer DNS-over-TLS if available.
DNSOverTLS=opportunistic
# forward packets from this interface to other interfaces.
IPForward=yes
[DHCP]
# some ISPs' DNS invent A records, so don't use them.
UseDNS=no
LAN
The LAN is controlled with systemd-networkd where possible, and the wifi is managed by hostapd. There are three parts to the networkd configuration:
- Create a bridge network device.
- Connect the LAN ethernet ports to it.
- Configure the bridge to be a DHCP server & NAT gateway.
First, create a bridge br0:
$ cat /etc/systemd/network/bridge.netdev
[NetDev]
Name=br0
Kind=bridge
Connect the LAN ethernet ports to the bridge:
$ cat /etc/systemd/network/lan.network
[Match]
Name=enp2s0 enp3s0
[Network]
Bridge=br0
Finally, configure the local network on the bridge itself.
systemd-networkd includes a minimal DHCP server, so also enable that.- Note the addition of both
IPForward=yes and IPMasquerade=yes to set up NAT.
$ cat /etc/systemd/network/bridge.network
[Match]
Name=br0
[Network]
# the router's LAN IP.
Address=192.168.16.1/24
# run a DHCP server.
DHCPServer=yes
# forward packets from this interface to other interfaces.
IPForward=yes
# make forwarded packets appear to come from this device,
# a.k.a. enable NAT.
IPMasquerade=yes
[DHCPServer]
DefaultLeaseTimeSec=600
# LAN DNS & Stubby DNS-over-TLS bridge.
DNS=192.168.16.1
# fallback DNS.
DNS=8.8.8.8
DNS=8.8.4.4
WiFi
Because hostapd has its own bridge management, it isn’t included in the systemd-networkd configuration.
This configuration:
- Uses our bridge
br0. - Uses the standard Linux WiFi driver set (others exist for other chipsets).
- Automatically selects a channel at start.
- Enables 802.11n (
hostapd also supports 802.11ac). - Enables WPA2 with a Pre-Shared Key (PSK).
- Enables pre-authentication (802.11i) on our bridge
br0. - Enables roaming (802.11r):
nas_identifier must be 6 bytes, and different between APs. I use the interface MAC.mobility_domain must be 2 bytes, and is shared between APs.
$ cat /etc/hostapd/hostapd.conf
bridge=br0
interface=wlan0
driver=nl80211
ssid=something-witty
# select a channel automatically
# using the ACS survey-based algorithm,
# instead of setting a channel manually.
channel=0
country_code=GB
ieee80211d=1
# hw_mode=g for 802.11n, hw_mode=a for 802.11ac.
hw_mode=g
# 802.11n support.
ieee80211n=1
wmm_enabled=1
# WPA2 encryption settings.
# note that wpa_key_mgmt also has FT-PSK for 802.11r.
wpa=2
wpa_passphrase=something-secret
wpa_key_mgmt=WPA-PSK FT-PSK
wpa_pairwise=TKIP
rsn_pairwise=CCMP
# 802.11i support.
rsn_preauth=1
rsn_preauth_interfaces=br0
# 802.11r support.
# mobility_domain must be shared across APs.
# nas_identifier must be different between APs.
mobility_domain=19fc
nas_identifier=8e71540f0467
ft_psk_generate_local=1
Kitchen
The WiFi in the kitchen is provided by a Raspberry Pi 2B and a USB WiFi adapter that supports host mode. I chose this hardware because I already owned it.
LAN
This is a simpler version of the Router’s LAN configuration, but with only two steps:
- Create a bridge network device.
- Connect the LAN ethernet ports to it.
As before, create a bridge br0:
$ cat /etc/systemd/network/bridge.netdev
[NetDev]
Name=br0
Kind=bridge
Connect the ethernet port to the bridge:
$ cat /etc/systemd/network/wired.network
[Match]
Name=eth0
[Network]
Bridge=br0
Configure the local network on the bridge itself. Because this is not the primary router, it can be just another DHCP client:
$ cat /etc/systemd/network/bridge.network
[Match]
Name=br0
[Network]
DHCP=yes
DNSOverTLS=opportunistic
WiFi
This is basically the same as the Router’s WiFi configuration, although remember to change the nas_identifier!