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:
In a given roaming connection, there are 3 keys:
There are 3 players:
In “pull mode”:
In “push mode”:
My router is a PC Engines APU 1d4, which has:
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
.
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
The LAN is controlled with systemd-networkd
where possible, and the wifi is managed by hostapd
. There are three parts to the networkd
configuration:
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.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
Because hostapd
has its own bridge management, it isn’t included in the systemd-networkd
configuration.
This configuration:
br0
.hostapd
also supports 802.11ac).br0
.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
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.
This is a simpler version of the Router’s LAN configuration, but with only two steps:
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
This is basically the same as the Router’s WiFi configuration, although remember to change the nas_identifier
!