Nrsyncd: 802.11k Neighbor Report Sync Daemon

Hello OpenWrt community,

Over the weekend, @kissadamfkut and I discussed the future of the openwrt-rrm-nr-distributor project. For context, Adam started that work in August 2021, inspired by this forum, and I’ve been a regular user and occasional contributor ever since.

The project is continuing and evolving under a new name: nrsyncd

  • What it is: a small service that distributes per‑BSS 802.11k Neighbor Report data across your OpenWrt APs via mDNS so clients (and higher‑level steering logic) can discover other BSSes in the same ESS quickly.

The original goal remains unchanged:

Distribute per‑BSS 802.11k Neighbor Report data across a set of OpenWrt APs using mDNS so clients (and higher‑level steering logic) can discover other BSSes of the same ESS quickly.

Acknowledgements:

  • Huge thanks to Adam for creating and stewarding the original project, and for supporting its continued evolution.

Get started and migration:

Feedback:

  • Please try it on your APs and share results or issues. Bug reports and ideas are welcome.
  • If reporting issues

    Please include:
    - Device model + OpenWrt version
    - Output of /etc/init.d/nrsyncd metadata
    - umdns announcements TXT (jsonfilter array selector [*] tip)
    ubus call umdns announcements | jsonfilter -e '@["_nrsyncd_v1._udp.local"][*].txt[*]'
    - Relevant logread | grep nrsyncd

Cheers!


Coming Soon

Native package for inclusion in the OpenWrt build system!

8 Likes

< Hold for future >

I noticed some things happening on GitHub, will update/migrate somewhere next week!

–EDIT–

upgraded earlier today. I enabled the debug option. If anything unexpected happens, I’ll update this post.

1 Like

Thank you for putting this together! I have played a bit and it looks promising. It seems I need to stop and start it again to pick up SSIDs which where enabled/disabled while it was running. Is it supposed to do it by itself?

It definitely should be picking that up on its own without any need to restart the service. Let me look into that and see what’s going on. Thanks for bringing that forward!

Sure. Let me know if I can help.

Thanks for this, it’s great!

I’m wondering if I’ve run into a problem though. I’ve just tested on my setup which has two SSIDs across both 2.4 and 5ghz bands, but I’m only seeing one of the SSIDs (“Jaculu”) in the neighbors command output.

@_FailSafe does this seem to be working on both my SSDIs “Russell Family” and “Jaculu” to you?

$ cat /etc/config/wireless

config wifi-device 'radio0'
	option type 'mac80211'
	option path '1e140000.pcie/pci0000:00/0000:00:01.0/0000:02:00.0'
	option band '2g'
	option channel '1'
	option htmode 'HE20'
	option country 'CH'
	option cell_density '1'

config wifi-device 'radio1'
	option type 'mac80211'
	option path '1e140000.pcie/pci0000:00/0000:00:01.0/0000:02:00.0+1'
	option band '5g'
	option channel '36'
	option htmode 'HE80'
	option country 'CH'
	option cell_density '1'

config wifi-iface 'wifinet00'
	option device 'radio0'
	option mode 'ap'
	option network 'lan'
	option ssid 'Russell Family'
	option encryption 'sae'
	option key 'xxxx'
	option dtim_period '3'
	option ieee80211r '1'
	option reassociation_deadline '20000'
	option ft_over_ds '0'
	option ieee80211k '1'
	option bss_transition '1'
	option time_advertisement '2'

config wifi-iface 'wifinet01'
	option device 'radio1'
	option mode 'ap'
	option network 'lan'
	option ssid 'Russell Family'
	option encryption 'sae'
	option key 'xxxx'
	option dtim_period '3'
	option ieee80211r '1'
	option reassociation_deadline '20000'
	option ft_over_ds '0'
	option ieee80211k '1'
	option bss_transition '1'
	option time_advertisement '2'

config wifi-iface 'wifinet10'
	option device 'radio0'
	option mode 'ap'
	option network 'lan'
	option ssid 'Jaculu'
	option encryption 'psk2+ccmp'
	option key 'xxxx'
	option dtim_period '3'
	option ieee80211r '1'
	option reassociation_deadline '20000'
	option ft_over_ds '0'
	option ieee80211k '1'
	option bss_transition '1'
	option time_advertisement '2'

config wifi-iface 'wifinet11'
	option device 'radio1'
	option mode 'ap'
	option network 'lan'
	option ssid 'Jaculu'
	option encryption 'psk2+ccmp'
	option key 'xxxx'
	option dtim_period '3'
	option ieee80211r '1'
	option reassociation_deadline '20000'
	option ft_over_ds '0'
	option ieee80211k '1'
	option bss_transition '1'
	option time_advertisement '2'

$ service nrsyncd neighbors

phy0-ap0:
  {
  	"list": [
        // Why no "Russell Family" SSID here?
  	]
  }
phy0-ap1:
  {
  	"list": [
  		[
  			"xx:xx:xx:xx:5d:63",
  			"Jaculu",
  			"xxxxxxxx5d63ef5900008024090603022a00"
  		]
  	]
  }
phy1-ap0:
  {
  	"list": [
        // Why no "Russell Family" SSID here?
  	]
  }
phy1-ap1:
  {
  	"list": [
  		[
  			"xx:xx:xx:xx:5d:62",
  			"Jaculu",
  			"xxxxxxxx5d62ef4900005101070603000100"
  		]
  	]
  }
$ ubus call umdns browse
{

}
$ service nrsyncd status

nrsyncd status:
  update_interval=60
  jitter_max=10
  umdns_refresh_interval=30
  umdns_settle_delay=0
  debug=1
  skip_ifaces=
  last_reload=1757856909
  cycle=13
  last_update_time=1757856910
  primary_service=nrsyncd_v1
  initial_positional_ssids=4
  metrics:
    cycle=13
    cache_hits=13
    cache_misses=0
    nr_sets_sent=2
    nr_sets_suppressed=24
    remote_entries_merged=0
    remote_unique_cycle=0
    remote_unique_total=0
    last_update_time=1757856910
    baseline_ssids=1
    suppression_ratio_pct=92
    nr_set_failures=0
    neighbor_count_phy0-ap1=1
    neighbor_count_phy1-ap1=1
Sun Sep 14 15:35:08 2025 daemon.info nrsyncd: Removed hash map directory: /tmp/tmp.OcLAKF
Sun Sep 14 15:35:08 2025 daemon.info nrsyncd: Removed hash map directory: /tmp/tmp.OcLAKF
Sun Sep 14 15:35:08 2025 daemon.info nrsyncd: Starting (init ver=1.0.2)
Sun Sep 14 15:35:08 2025 daemon.info nrsyncd: Waiting for all wireless interfaces to initialize.
Sun Sep 14 15:35:08 2025 daemon.info nrsyncd: All wireless interfaces are initialized.
Sun Sep 14 15:35:08 2025 daemon.info nrsyncd: Assembled 4 SSID entries (no skips)
Sun Sep 14 15:35:08 2025 daemon.debug nrsyncd: Prepared SSID args: SSID1=[ "xx:xx:xx:xx:5d:62", "Russell Family", "xxxxxxxx5d62ef4900005101070603000100" ] SSID2=[ "xx:xx:xx:xx:5d:62", "Jaculu", "xxxxxxxx5d62ef4900005101070603000100" ] SSID3=[ "xx:xx:xx:xx:5d:63", "Russell Family", "xxxxxxxx5d63ef5900008024090603022a00" ] SSID4=[ "xx:xx:xx:xx:5d:63", "Jaculu", "xxxxxxxx5d63ef5900008024090603022a00" ] (total 4)
Sun Sep 14 15:35:09 2025 daemon.debug nrsyncd: startup: interval=60 jitter=10 umdns=30 max_cycles=0
Sun Sep 14 15:35:10 2025 daemon.debug nrsyncd: group_build: ssid='Jaculu' canon_count=2
Sun Sep 14 15:35:10 2025 daemon.debug nrsyncd: cache_hit: ssid='Jaculu'
Sun Sep 14 15:35:10 2025 daemon.debug nrsyncd: phy0-ap1: updated list=[["xx:xx:xx:xx:5d:63","Jaculu","xxxxxxxx5d63ef5900008024090603022a00"]] rc=0
Sun Sep 14 15:35:10 2025 daemon.debug nrsyncd: phy1-ap1: updated list=[["xx:xx:xx:xx:5d:62","Jaculu","xxxxxxxx5d62ef4900005101070603000100"]] rc=0
Sun Sep 14 15:36:10 2025 daemon.debug nrsyncd: group_build: ssid='Jaculu' canon_count=2
Sun Sep 14 15:36:10 2025 daemon.debug nrsyncd: cache_hit: ssid='Jaculu'
$ service nrsyncd mapping
  iface        bssid             chan freq   width c1     SSID
  phy0-ap0     xx:xx:xx:xx:5d:62 1    2412   20    2412   Russell Family
  phy0-ap1     xx:xx:xx:xx:5d:62 1    2412   20    2412   Jaculu
  phy1-ap0     xx:xx:xx:xx:5d:63 36   5180   80    5210   Russell Family
  phy1-ap1     xx:xx:xx:xx:5d:63 36   5180   80    5210   Jaculu

So what's this about /bin/nrsyncd being "prebuilt; no source here"? As far as I can see it's a shell script, not a binary?

Documentation oversight. Will get this corrected in an upcoming release. :+1:t2:

2 Likes

Sorry about the delayed reply. I've been under the weather the past few days and slow to dig into this and a few other issues/updates.

I will dig into this ASAP. Thanks for your patience!

1 Like

Thank you, I appreciate it. Feel better soon!

@qunvureze and @jasrusable Apologies again for the delay in any visible progress toward your questions. I ended up having COVID, so recovery has been (and continues to be) slow-going.

That said, I am working on an updated testing version that I will put out on a separate project branch ASAP (hopefully within just a few more days). If you're willing to test, you're welcome to pull that branch when it's ready and provide any feedback as you try it out.


@qunvureze and @jasrusable If you feel willing, you could begin testing this branch for me and let me know if you have successes or hit any issues: https://github.com/Fail-Safe/nrsyncd/tree/feature/activity-overlay-and-parsing

2 Likes

Sorry to hear, get well soon. Happy to help test and share feedback.

I have yet to try your script but just to be sure the only commands that work for me on 24.10.4 is

ubus call hostapd.phy1-ap0 rrm_nr_get_own

radio1 or similar are not working

Here is my list

hostapd
hostapd-auth
hostapd.phy0-ap0
hostapd.phy1-ap0

I had a look at the library and im curious about few things:

  1. Does it work with WPA2/WPA3 mix mode?
  2. What should it be effectively the performance increase we are looking at? Faster switching to another AP/another band/another AP band? Controlled steering? Can the information be tuned for steering the decision? I have an AP which irrespective of everything (except when I'm outside its range) it is by far the best AP, can we tamper the actual values with a synthetic increment?
  3. Is mDNS TXT information part of the standard 802.11?

Since i've followed the related thread about DAWN/usteer and the static method, do you think that this is still the best approach? As far as i've understood DAWN can actually kick and steer the decision, right?

@_FailSafe

I've been working on my own daemon for roaming with 802.11k. It's unfinished and currently it only implements 802.11-2024 neighbor report synchronisation (i.e. the stuff that was originally in 802.11k before that was absorbed into the main 802.11 standard), and not ex-802.11v things like forcing or recommending client roams.

In the meantime, whilst debugging my code, I've noticed a few minor issues which might (or might not) reduce the effectiveness of other daemons such as nrsyncd.

The following issues have showed up for me in packet captures. All development and captures have been done on ZyXEL NWA50AX Pro access points running OpenWrt 24.10.x

  1. Neighbor report responses sent by OpenWrt include a neighbor report for the BSSID (access point + radio + ssid combination) which the client is already connected to. Whilst this isn't prohibited by the standard I think, it might confuse some clients. There is a patch to fix this here: https://patchwork.ozlabs.org/project/hostap/patch/3c483f83-ca86-40b3-842b-cbb6f0e5d1ca@freebox.fr/ but it hasn't yet been applied to the upstream hostapd. You can check for this problem using wireshark with the filter wlan.nreport.bssid == wlan.sa i.e. this evaluates to true if the packet source address (bssid) matches the bssid within any of the included neighbor reports. On an unpatched hostapd this filter should match all neighbor report responses (wlan.fixed.action_code == 5).

  2. Neighbor reports include a flag to indicate that a particular neighbor is "colocated with" the bssid which is sending the report. e.g. on a dual band access point the 2.4GHz bssid can set the "colocated" flag when it includes information for its own 5GHz bssid (the same would be true the other way around too), but NOT when it was sending a neighbor report for a peer BSSID located elsewhere on a physically separate access point. hostapd doesn't automatically set this, so the daemon which implements the neighbor reports sync (e.g. nrsyncd) must set it. You can check for this in wireshark using the filter wlan.nreport.bssid.info.colocated_ap == True (or wlan.nreport.bssid.info.colocated_ap == False depending on what you are looking for). I've implemented a workaround for this in my daemon (set this flag if the nr is for another local radio on the same physical access point).

  3. On wifi6 (802.11ax) access points, hostapd doesn't set the PHY byte in the neighbor report correctly, instead it the PHY byte is set to either wifi4 (802.11n) for neighbor reports for 2.4 GHz BSSIDs or wifi5 (802.11ac) for 5 GHz BSSIDs. This is (I think) a hostapd bug (and should really be fixed in the hostap source code). I've worked around this in my daemon by checking the separate HE bit in the neighbor report "BSSID Information" bytes, and correcting the PHY byte for the neighbor report if the PHY byte is inconsistent with the HE bit. I haven't tested this fix yet to see what impact it has. Wireshark filter for this is wlan.nreport.phytype != 0x0e and wlan.nreport.bssid.info.he == True (n.b. this might false positive if you have a mixture of 802.11ax and 802.11ac access points in your ESSID).

HTH.

Cheers,

Tim.

4 Likes

I also came across this in an Apple support document:

"The roam scan runs more quickly if 802.11k is enabled on the network. This helps because supported Apple devices and operating systems use the first six entries in the Neighbor Report to prioritize the channels to be scanned." - src: https://support.apple.com/en-md/guide/deployment/dep98f116c0f/web

I found this out when deploying 802.11k in an environment with a lot of Apple devices. After 802.11k was enabled usage of the 5 GHz band dropped, and more devices used 2.4 GHz instead, overall average client signal strength also dropped (i.e. most things became worse!). I used prometheus along with a custom Grafana dashboard to verify this. I've added an option to my daemon to only advertise 5GHz BSS in neighbor reports, but I haven't tested this yet (in the meantime I've left 802.11k turned off).

3 Likes

FYI I've posted this to another thread regarding a potential pitfall when deploying multi-AP networks:

1 Like

I'm curious if you have observed any change/improvement in client behaviour after correcting the neighbour report PHY type byte to the expected wifi6 value? I presume the current value is originating from hostap here whereby the calculated type does not consider the he flag. I would naively assume that the helper could be updated accordingly but it also seemed unexpected to me that there is currently no wifi 6 type in the definitions.

More out of interest than a need for clients to have help on my network, I went to install nrsyncd earlier and found that on startup, it hung for 5 minutes before complaining:

Jan 10 17:19:12 dougal nrsyncd: Waiting for all wireless interfaces to initialize.
Jan 10 17:24:14 dougal nrsyncd: Timeout waiting for hostapd objects (got 4 / expected 5). Check wireless config & hostapd logs.

That turned out to be because I’m using an 802.11s mesh; the init script was finding 5 wifi-iface entries in the wireless config file, but only 4 were reported by uci. Small patch for the init script follows:

diff --git a/service/nrsyncd.init b/service/nrsyncd.init
index 0f0c4c7..4dd704c 100755
--- a/service/nrsyncd.init
+++ b/service/nrsyncd.init
@@ -74,6 +74,8 @@ get_enabled_iface_count() {
        for i in $(seq 0 "$seq_end"); do
                disabled=$(uci -q get "wireless.@wifi-iface[${i}].disabled" 2>/dev/null)
                [ "$disabled" = "1" ] && continue
+               mesh=$(uci -q get "wireless.@wifi-iface[${i}].mode" 2>/dev/null)
+               [ "$mesh" == "mesh" ] && continue
                count=$((count + 1))
        done
        echo "$count"

Feel free to include it or not, @_FailSafe !

3 Likes

Hi,

Unfortunately I don't seem to have created some notes at the time. During testing, I have a vague memory that a Windows Intel wifi driver for an old (now unsupported) device was unhappy with the corrected PHY byte in some way but I can't remember any details. I'm not using that particular wifi in either my test or production environments, and I seem to have left that correction enabled in production, so I think I must have decided the correction was overall either neutral or positive - sorry I can't help more than that.

Yep, that sounds right, I did look at patching hostapd, but I didn't have time before I needed to put the roaming assistant code into production.