Matter over Thread in a VLAN-aware network on HA OS in Proxmox with VyOS
Ikea ~recently released really nice sensors (home-assistant.io), for example the ALPSTUGA CO2 (holliger.me), an improved successor to the VINDSTYRKA (ikea.com). Unfortunately, the ALPSTUGA runs on Matter over Thread, so I wanted to check how I could migrate my network.
TL;DR: ¶
- Read the HA Matter docs (home-assistant.io) & HA Thread docs (home-assistant.io)
- Read the Openthread docs (openthread.io)
- Read this guide (maxaller.name)
- Enable IPv6 on your local network, set firewall correctly
- Enable mDNS reflector between Thread/Matter server VLAN and client VLAN (
set service mdns repeater interface 'br100.20'), for firewall rules realize that mDNS broadcast/multicast goes to local zone. - Ensure
ha docker options --enable-ipv6=trueis set on HA OS docker - Ensure
accept_ra=2andaccept_ra_rt_info_max_plen=64are set on proxmox / VyOS hosts (maybe only VyOS is enough) - Ensure you’re close enough to the Thread radio when testing
- Ensure you like this kind of pain when setting up your VLAN-aware home network on proxmox/VyOS/HA OS.
Thread is a bitch ¶
Initially I thought this would take some reading up, an extra dongle, and then 10min of configuring. It turns out it was quite an IPv6 hell and took me hours to get working.
Zigbee install flow:
- Install Zigbee (USB) dongle
- Open Home Assistant app (via https)
- Push button & pair device in app (via Zigbee radio)
- Profit!
Matter over Thread install flow:
- Install Thread (USB) dongle
- Open Home Assistant app (via https)
- Transfer credentials to smartphone in specific app (e.g. Home Asistant or Apple Home) (via ipv6 mDNS) – for some reason this requires mDNS and if you have separate VLANs you need to reflect this using
avahior similar. - Start commissioning on Home Assistant app (via https)
- Smartphone connects to device to transfer credentials (via bluetooth)
- Device connects to Thread Border Router (via Thread radio)
- Thread Border Router announces new device (via IPv6 mDNS)
- Home Assistant app connects to device via Matter & Thread Border Router servers (via IPv6 Matter)
- Profit
Obviously, the latter has many more moving parts and has several more failure modes. Notably the requirement of IPv6 and multicast (mDNS/Matter) traffic.
While I guess it’s nice Matter over Thread devices are routable over IP, I don’t see the practical benefit:
- Range/reliability/latency: similar given the radio protocol is the same. Thread should have better latency, but I never had latency issues with my Zigbee bulbs.
- Interoperability: theoretically the same, Zigbee and Thread both have standards that can or cannot be implemented.
- Acceptance/adoption: Thread has a slight edge here in that you could use an Apple TV as Thread router. This saves a device plus an Apple TV is often positioned in a more RF-friendly environment.
- Compatibility: not great, I need another dongle to set up another RF network at home in the crowded 2.4GHz window (WiFi, Zigbee, and now Thread).
- Security: possibly worse for Thread, while they both use AES during commissioning, Thread devices are by default routable on IPv6, which is great for botnets.
Zigbee vs Thread ¶
Zigbee and Thread are peers ¶
One thing I quickly learned is that Zigbee and Thread are comparable (ti.com) and while Thread has some advantages, it’s not a clear successor to Zigbee. In fact, Thread and Zigbee use the same physical/MAC layer which is 802.15.4, and hence share some characteristics like bandwidth and battery life. The main difference seems to be IP-based routing for Thread with a dedicated networking layer in the Zigbee stack.
Apple TV supports Thread ¶
While a Zigbee network requires a dedicated hub or dongle, a Thread network apparently can also be set up from an Apple TV 4K (reddit.com), specifically the A2169 (Apple TV 4K 2nd generation) and A2843 (Apple TV 4K 3rd generation, 128GB) models. The advantages being that Apple TV is less ugly than a hub and can be placed in a more radio interference friendly environment.
Dual-radio double protocol hubs/dongles exist ¶
Some dongles/hubs support both protocols, for example the ZBT-2, and some support both protocols simultaenously (youtu.be) by using two radios. However, given the radio protocol is the same, placing two dongles/radios next to each other will likely cause degradation on both networks. Ideally one has two radios spaced apart, requiring two devices.
Thread has not implemented all features ¶
Zigbee can have direct switch to bulb pairing, as well as grouping to enable simultaenously power on/off. While Thread can theoretically do this, there is currently no hardware supporting this (source (tweakers.net)).
Scenarios migrating Zigbee to Thread ¶
If I want to use the ALPSTUGA, I have at least three options, I’m going for scenario 1.
- Keep existing Zigbee network(s), add new Thread network: Con: extra dongle, extra network. Pro: can migrate gradually, can keep existing devices
- Buy a dual-radio dongle: Con: need to migrate Zigbee network, new dongle, still extra network. Pro: only one dongle - potentially lower power consumption
- Migrate Zigbee devices to new Thread network. Con: requires new hardware while existing is working fine, need to migrate devices. Pro: only one (denser) network.
Installing ZBT-2 with ALPSTUGA ¶
Install in Home Assistant ¶
From the official guide (nabucasa.com):
- Connect USB dongle to Home Assistant computer
- Pass through USB in case Home Assistant runs in container/VM
- Home Assistant detects dongle, configure as Thread adapter
- Done
Ensure mDNS reflector on VyOS VLAN-aware network ¶
From the docs (vyos.io):
Make sure you set up firewall from zones to the LOCAL zone! mDNS does not travel from zone to zone but instead to the router (LOCAL) and is then reflected outward from the router again. This took 2 hours of my life I won’t get back.
set interfaces bridge br100 igmp snooping
set service mdns repeater interface 'br100.20'
set service mdns repeater interface 'br100.10'
set service mdns repeater interface 'br100.30'
set firewall ipv4 name FW_TRUST2LOCAL rule 300 action 'return'
set firewall ipv4 name FW_TRUST2LOCAL rule 300 description 'accept mDNS'
set firewall ipv4 name FW_TRUST2LOCAL rule 300 destination port '5353'
set firewall ipv4 name FW_TRUST2LOCAL rule 300 protocol 'udp'
set firewall ipv4 name FW_TRUST2LOCAL rule 300 destination address '224.0.0.251'
set firewall ipv4 name FW_2LOCAL rule 300 action 'return'
set firewall ipv4 name FW_2LOCAL rule 300 description 'accept mDNS'
set firewall ipv4 name FW_2LOCAL rule 300 destination port '5353'
set firewall ipv4 name FW_2LOCAL rule 300 protocol 'udp'
set firewall ipv4 name FW_2LOCAL rule 300 destination address '224.0.0.251'
set firewall ipv6 name FW_TRUST2LOCAL rule 300 action 'accept'
set firewall ipv6 name FW_TRUST2LOCAL rule 300 description 'Allow IPv6 mDNS to router'
set firewall ipv6 name FW_TRUST2LOCAL rule 300 protocol 'udp'
set firewall ipv6 name FW_TRUST2LOCAL rule 300 destination port '5353'
set firewall ipv6 name FW_TRUST2LOCAL rule 300 destination address 'ff02::fb'
set firewall ipv6 name FW_2LOCAL rule 300 action 'accept'
set firewall ipv6 name FW_2LOCAL rule 300 description 'Allow IPv6 mDNS to router'
set firewall ipv6 name FW_2LOCAL rule 300 protocol 'udp'
set firewall ipv6 name FW_2LOCAL rule 300 destination port '5353'
set firewall ipv6 name FW_2LOCAL rule 300 destination address 'ff02::fb'
set firewall zone LOCAL from TRUSTED firewall ipv6-name FW_TRUST2LOCAL
set firewall zone LOCAL from GUEST firewall ipv6-name FW_2LOCAL
- Ensure IGMP snooping is off
vyos@vyos:~$ cat /sys/class/net/br100/bridge/multicast_snooping - Ensure multicast is on
Not sure if this is necessary - because the app connects to Home Assistant over HTTP(S), so this is likely only needed if you have Matter over Wifi devices that connect to Home Assistant
set firewall ipv4 name FW_TRUST2INFRA rule 310 action 'return'
set firewall ipv4 name FW_TRUST2INFRA rule 310 description 'accept matter over wifi to home assistant'
set firewall ipv4 name FW_TRUST2INFRA rule 310 destination address '172.17.10.20'
set firewall ipv4 name FW_TRUST2INFRA rule 310 destination port '5584'
set firewall ipv4 name FW_TRUST2INFRA rule 310 protocol 'tcp_udp'
Transfer Thread credentials to Home Assistant app ¶
From the Thread docs (home-assistant.io):
- Go to Home Assistant App, login as admin user
- Go to settings, then Thread
- Transfer credentials to phone –> this requires mDNS working from Home Assistant to your phone
Add Matter devices to Home Assistant ¶
Check the Matter docs (home-assistant.io) for instructions.
Overview ¶
ChatGPT spit out these flows, can be helpful to diagnose what’s going on.
┌──────────────┐
│ Phone / │
│ Matter │
│ Controller │
└──────┬───────┘
│
│ 1. BLE
│ commissioning channel
▼
┌──────────────┐
│ Matter over │
│ Thread │
│ device │
└──────┬───────┘
│
│ 2. Device receives
│ Thread credentials
│
▼
┌──────────────┐
│ Thread │
│ Network │
└──────┬───────┘
│
│ 3. IPv6
▼
┌──────────────┐
│ OTBR │
│ │
│ Border │
│ Router │
└──────┬───────┘
│
│ 4. IPv6 routing
▼
┌──────────────┐
│ LAN / VyOS │
│ IPv6 router │
└──────┬───────┘
│
│
▼
┌──────────────┐
│ Matter │
│ Controller │
│ / HA │
└──────────────┘
BLE
│
│ commissioning
▼
Matter device
│
│ Thread
▼
Thread network
│
│ IPv6
▼
OTBR
│
│ IPv6 routing
▼
LAN
│
│ mDNS / Matter
▼
Matter Server
│
▼
Home Assistant
My critical IPv6 path:
Thread Device
│
│ Thread IPv6
▼
OTBR container
│
│ IPv6
▼
HAOS / enp0s18
│
│ IPv6 forwarding
│
│ accept_ra = 2
▼
Proxmox vmbr
│
▼
VyOS Router VM
│
│ LAN IPv6
▼
LAN
Enable IPv6 in docker on HA OS ¶
Doesn’t work. Home Assistant cannot talk to the OBRP add-on (which runs as separate server)?
Ensure IPv6 is enabled in Docker (source (home-assistant.io))
ha docker info
# enable_ipv6: null
# logging: journald
# mtu: null
# registries: {}
# storage: overlay2
# version: 29.6.2
ha docker options --enable-ipv6=true
# Note: System restart required to apply new IPv6 configuration.
# Command completed successfully.
ha host reboot
# Command completed successfully.
# Connection to homeassistant.lan.vanwerkhoven.org closed by remote host.
Set up IPv6 on VyOS ¶
This migration path is turning out to be a bit of a horror-story, but the Alpstuga is so tempting I’m going ahead with it anyway. I don’t get IPv6 from my ISP, so I set up a local IPv6 network. Not sure if these explicit IPs were needed or that auto-assigned link-local IPs would have sufficed.
# Setup static IP on interfaces
set interfaces bridge br100 vif 10 address fd00:beef:cafe:10::1/64
set interfaces bridge br100 vif 20 address fd00:beef:cafe:20::1/64
set interfaces bridge br100 vif 30 address fd00:beef:cafe:30::1/64
set interfaces bridge br100 vif 40 address fd00:beef:cafe:40::1/64
# Enable IPv6 routing
delete system ipv6 disable-forwarding
# Enable IPV6 icmp for now
set firewall global-options ipv6-receive-redirects 'enable'
set firewall global-options ipv6-src-route 'enable'
# Set up Router Advertisement
set service router-advert interface br100 prefix ::/64
delete service router-advert interface br100 managed-flag
delete service router-advert interface br100 other-config-flag
set service router-advert interface br100.10 prefix '::/64'
delete service router-advert interface br100.10 managed-flag
delete service router-advert interface br100.10 other-config-flag
set service router-advert interface br100.20 prefix '::/64'
delete service router-advert interface br100.20 managed-flag
delete service router-advert interface br100.20 other-config-flag
set service router-advert interface br100.30 prefix '::/64'
delete service router-advert interface br100.30 managed-flag
delete service router-advert interface br100.30 other-config-flag
set service router-advert interface br100.40 prefix '::/64'
delete service router-advert interface br100.40 managed-flag
delete service router-advert interface br100.40 other-config-flag
# Set up DNS listening address
set service dns forwarding allow-from 'fd00:beef:cafe::/48'
set service dns forwarding listen-address 'fd00:beef:cafe:10::1'
set service dns forwarding listen-address 'fd00:beef:cafe:20::1'
set service dns forwarding listen-address 'fd00:beef:cafe:30::1'
set service dns forwarding listen-address 'fd00:beef:cafe:40::1'
# I have these IPv6 firewall rules related to mDNS / matter traffic
set firewall ipv6 name FW_2LOCAL default-action 'drop'
set firewall ipv6 name FW_2LOCAL rule 200 action 'return'
set firewall ipv6 name FW_2LOCAL rule 200 description 'accept established/related'
set firewall ipv6 name FW_2LOCAL rule 200 state 'established'
set firewall ipv6 name FW_2LOCAL rule 200 state 'related'
set firewall ipv6 name FW_2LOCAL rule 230 action 'return'
set firewall ipv6 name FW_2LOCAL rule 230 description 'accept icmpv6'
set firewall ipv6 name FW_2LOCAL rule 230 protocol 'icmpv6'
set firewall ipv6 name FW_2LOCAL rule 300 action 'return'
set firewall ipv6 name FW_2LOCAL rule 300 description 'Allow IPv6 mDNS & matter to router'
set firewall ipv6 name FW_2LOCAL rule 300 destination address 'ff02::fb'
set firewall ipv6 name FW_2LOCAL rule 300 destination port '5353,5540'
set firewall ipv6 name FW_2LOCAL rule 300 protocol 'udp'
set firewall ipv6 name FW_TRUST2LOCAL default-action 'drop'
set firewall ipv6 name FW_TRUST2LOCAL rule 200 action 'return'
set firewall ipv6 name FW_TRUST2LOCAL rule 200 description 'accept established/related'
set firewall ipv6 name FW_TRUST2LOCAL rule 200 state 'established'
set firewall ipv6 name FW_TRUST2LOCAL rule 200 state 'related'
set firewall ipv6 name FW_TRUST2LOCAL rule 225 action 'return'
set firewall ipv6 name FW_TRUST2LOCAL rule 225 description 'accept icmpv6'
set firewall ipv6 name FW_TRUST2LOCAL rule 225 protocol 'icmpv6'
set firewall ipv6 name FW_TRUST2LOCAL rule 300 action 'return'
set firewall ipv6 name FW_TRUST2LOCAL rule 300 description 'Allow IPv6 mDNS & matter to router'
set firewall ipv6 name FW_TRUST2LOCAL rule 300 destination address 'ff02::fb'
set firewall ipv6 name FW_TRUST2LOCAL rule 300 destination port '5353'
set firewall ipv6 name FW_TRUST2LOCAL rule 300 protocol 'udp'
set firewall ipv6 name FW_TRUST2LOCAL rule 310 action 'return'
set firewall ipv6 name FW_TRUST2LOCAL rule 310 description 'accept 5540 matter to Home Assistant'
set firewall ipv6 name FW_TRUST2LOCAL rule 310 destination port '5540'
set firewall ipv6 name FW_TRUST2LOCAL rule 310 protocol 'tcp_udp'
set firewall zone LOCAL default-action 'drop'
set firewall zone LOCAL from TRUSTED firewall ipv6-name 'FW_TRUST2LOCAL'
set firewall zone LOCAL local-zone
Sources:
Enable RA on proxmox/VyOS ¶
Somehow, Router Advertisement was disabled on promox & VyOS, which I had to enable to get the Thread routes propagate through my network and to my clients.
Sources:
- https://github.com/home-assistant/core/issues/115361 (github.com)
- https://community.home-assistant.io/t/solved-ipv6-docker-proxmox/828006/21 (home-assistant.io)
- https://openthread.io/codelabs/openthread-border-router#0 (openthread.io)
The routing on my network is as follows (yes, needelessly complex, but fun!)
Thread devices
│
▼
OTBR
│
▼
HAOS VM (enp0s18)
│
▼
Proxmox bridge
│
▼
VyOS
│
▼
LAN / IPv6
On proxmox:
cat | sudo tee /etc/sysctl.d/99-thread-routing.conf <<'EOF'
net.ipv6.conf.vmbr0.accept_ra = 2
net.ipv6.conf.vmbr0.accept_ra_rt_info_max_plen = 64
net.ipv6.conf.vmbr0.forwarding = 1
net.ipv6.conf.vmbr0/10.accept_ra = 2
net.ipv6.conf.vmbr0/10.accept_ra_rt_info_max_plen = 64
net.ipv6.conf.vmbr0/10.forwarding = 1
EOF
sudo sysctl --system
sudo sysctl --all | grep net.ipv6.conf.br100.accept_ra
After this, proxmox can ping the thread thing.
On VyOS (see docs (vyos.io))
set system sysctl parameter net.ipv6.conf.br100.accept_ra value 2
set system sysctl parameter net.ipv6.conf.br100.accept_ra_rt_info_max_plen value 64
set system sysctl parameter net.ipv6.conf.br100/10.accept_ra value 2
set system sysctl parameter net.ipv6.conf.br100/10.accept_ra_rt_info_max_plen value 64
set system sysctl parameter net.ipv6.conf.br100/20.accept_ra value 2
set system sysctl parameter net.ipv6.conf.br100/20.accept_ra_rt_info_max_plen value 64
set system sysctl parameter net.ipv6.conf.br100/30.accept_ra value 2
set system sysctl parameter net.ipv6.conf.br100/30.accept_ra_rt_info_max_plen value 64
set system sysctl parameter net.ipv6.conf.br100/40.accept_ra value 2
set system sysctl parameter net.ipv6.conf.br100/40.accept_ra_rt_info_max_plen value 64
sudo sysctl --all | grep net.ipv6.conf.br100.accept_ra
Ensure to reboot where needed.
Appendix - ChatGPT ¶
ChatGPT: “Good news: HAOS now supports persistent sysctl overrides
Since Home Assistant OS 15.0, HAOS explicitly supports overriding sysctl parameters using files in /etc/sysctl.d.”
Me: “I only need commands to make sysctl permanent on HA OS.”
ChatGPT: “Important correction: current HAOS documentation does not list /etc/sysctl.d as a persistent configuration directory. The documented persistent host configuration locations include NetworkManager, modules, modprobe, udev, etc.”
Me: “it’s not working. How can I set accept_ra=2 permanently?”
ChatGPT: “You’re right — my previous cmdline.txt suggestion was wrong for this.
HAOS 15+ supports persistent sysctl overrides via /etc/sysctl.d, and the HAOS boot process mounts that directory from the persistent overlay before running systemd-sysctl.”
Me: “no that’s not working either”
ChatGPT: “You’re right. The issue is that the file can be persistent but NetworkManager can subsequently overwrite accept_ra during network initialization. HAOS does in fact have a persistent /etc/sysctl.d mount, but that doesn’t guarantee the value remains after NetworkManager configures enp0s18. "
#Home-Assistant #Networking #Smarthome #Zibgee #Thread #Matter