Distributes 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. Client support of 802.11k is required for full functionality and support varies by client device manufacturer and operating system.
For clients supporting 802.11k, this project provides a way to efficiently and automatically share neighbor report data across multiple access points, improving the overall performance and responsiveness of roaming between bands and BSSes.
⚠️ As this project is heavily reliant on OpenWrt'subusandhostapdcomponents, it is designed to work seamlessly within the OpenWrt ecosystem. However, the specific requirement forubusinhibits the usage of this project in non-OpenWrt environments.
Inspiration for this nrsyncd project comes from pioneering efforts by Kiss Ádám in the openwrt-rrm-nr-distributor project. This project represents a significant evolution in the approach to 802.11k neighbor report synchronization as envisioned and established by Ádám in the former project beginning back in August 2021. Many thanks to Ádám for allowing this nrsyncd project to be spawned from openwrt-rrm-nr-distributor and continue the growth and evolution of 802.11k neighbor reporting.
nrsyncd is an extracted and renamed continuation of the refactored “Neighbor Report Distributor” (rrm_nr) project. It was released as nrsyncd v1.0.0 under GPLv2 with permission. Legacy names are intentionally not retained.
- Multi‑radio / multi‑band support (sequential
SSIDn=TXT records) - TXT metadata appended after SSID entries:
v=1,c=<count>,h=<8hex>(extensible; safe for consumers to ignore unknown keys) - Helps band / BSS steering by advertising sibling BSS information
- Works once
umdnsis functioning (no extra controller needed) - Minimal logging (single tag
nrsyncd) but enough to diagnose startup - Deterministic neighbor list ordering (by SSID then BSSID) & duplicate suppression (remote duplicates collapsed)
- Per‑interface neighbor counts (post self‑filter) exposed as metrics
- Concise one‑line
summarycommand (easy monitoring / telemetry) - Metrics reset (
reset_metrics) & on‑demand refresh (refresh) - Baseline per‑SSID push guarantees initial hostapd population even with no diff
- Remote uniqueness tracking (cycle + cumulative) for observability of discovery breadth
- Remote installer mode (stage & execute over SSH with a single command)
| Component | Purpose |
|---|---|
service/nrsyncd.init |
procd init script: gathers NR data via ubus, formats TXT args, registers mDNS service |
bin/nrsyncd |
Opaque daemon consuming positional SSIDn=<value> args (prebuilt; no source here) |
lib/nrsyncd_common.sh |
Shared POSIX helpers (adaptive retry, iface mapping, millisecond sleep abstraction, probes) |
examples/wireless.config |
Example /etc/config/wireless enabling 802.11k (ieee80211k '1', bss_transition '1') |
- Count enabled
wifi-ifacestanzas (skip those withoption disabled '1'). - Wait until that many
hostapd.*objects appear onubus(ensures hostapd ready). - For each
hostapd.*: callubus call <obj> rrm_nr_get_own(method name unchanged) → JSON{"value":"..."}. - Build concatenated string
+SSID<n>=<value>(delimiter+chosen because invalid in SSIDs) then split into positional args. - Start daemon & register versioned mDNS service
nrsyncd_v1(type_nrsyncd_v1._udp) on UDP/32025 with those TXT records (allSSIDn=first, then metadatav/c/h). Legacy_rrm_nr._udpis browsed only as a fallback for discovery.
sequenceDiagram
participant procd
participant init as nrsyncd.init
participant hostapd
participant ubus
participant daemon as nrsyncd (bin)
participant umdns
procd->>init: start()
init->>init: count enabled wifi-iface
loop until ready
init->>ubus: list hostapd.*
ubus-->>init: objects
end
init->>ubus: call hostapd.X rrm_nr_get_own (each iface)
ubus-->>init: {"value":"NR..."}
init->>init: assemble +SSIDn=NR tokens (delimiter '+')
init->>daemon: exec /usr/bin/nrsyncd SSID1=... SSID2=...
init->>umdns: register service nrsyncd_v1 (TXT=SSIDn=... v/c/h)
umdns-->>network: announce _nrsyncd_v1._udp (UDP/32025)
A helper script scripts/migrate_from_rrm_nr.sh exists for older deployments but is not required for fresh installs.
Installer behavior with legacy rrm_nr:
- On a live system (no
--prefix) if legacyrrm_nrconfig/service is detected and/etc/config/nrsyncdis not present,scripts/install.shaborts to avoid a mixed state. - To proceed, either run
sh scripts/migrate_from_rrm_nr.shfirst, or re-run the installer with--auto-migrate-legacyto migrate automatically.
Follow these minimal steps on an existing device running the legacy rrm_nr service:
# 0) (Optional) stop legacy service before changes
[ -x /etc/init.d/rrm_nr ] && /etc/init.d/rrm_nr stop || true
# 1) Migrate config (safe, idempotent). This also disables the old service when possible.
sh scripts/migrate_from_rrm_nr.sh
# Or do it one-shot during install:
# sh scripts/install.sh --auto-migrate-legacy
# 2) Install nrsyncd (enables/starts service by default)
sh scripts/install.sh
# 3) Validate runtime and mDNS
logread | grep nrsyncd
/etc/init.d/nrsyncd status
ubus call umdns browse | jsonfilter -e '@["_nrsyncd_v1._udp"][*].txt[*]'
# Tip: arrays in jsonfilter need [*]; omitting it often yields an empty []
# 4) (Recommended) Remove legacy artifacts after validation
sh scripts/migrate_from_rrm_nr.sh --remove-old --force
# 5) (Optional) Persist files across firmware upgrades
sh scripts/install.sh --add-sysupgrade --no-start
# Adds runtime paths to /etc/sysupgrade.conf:
# /etc/init.d/nrsyncd
# /usr/bin/nrsyncd
# /lib/nrsyncd_common.sh
# Note: /etc/config is persisted by OpenWrt by default, so /etc/config/nrsyncd is not added explicitly.Notes:
- Remote installs also support migration via
--auto-migrate-legacy(forward this flag with your remote run). - The migration helper won’t modify your wireless config; ensure 802.11k/BSS transition are enabled per the example.
You have three progressively more automated options (plus a remote orchestration helper):
On the target OpenWrt device (copy the repo over or clone with git if available):
# assuming repository now at /tmp/nrsyncd_src (e.g. scp -r ./ root@ap:/tmp/nrsyncd_src )
cd /tmp/nrsyncd_src
sh scripts/install.shOptions:
--no-startonly stage files (don’t enable/start service)--force-configoverwrite existing/etc/config/nrsyncd--add-sysupgradeappend required paths to/etc/sysupgrade.conffor persistence--prefix /overlaystage into another root (image build workflows)--deps-auto-yes/--deps-auto-nonon‑interactive dependency handling--install-optionalinstall high‑resolution sleep helper (coreutils-sleep or coreutils)--fix-wirelessauto-enable missing 802.11k / BSS transition options--auto-migrate-legacyif legacyrrm_nris detected on a live system, migrate it automatically instead of aborting--cleanup-legacyafter install, remove legacyrrm_nrinit/bin/lib from the live system--cleanup-legacy-forcealso remove legacy/etc/config/rrm_nr(implies--cleanup-legacy)
Run the installer locally and have it stage + execute itself on one or more remote OpenWrt hosts over SSH (no manual scp required). The script:
- Builds a minimal file set + a
sha256manifest. - Streams a tar archive over SSH to each host (parallel sequentially, one after another).
- Verifies integrity via
sha256sum -con the target (warns & skips verification ifsha256sumabsent). - Executes the same
install.shremotely with preserved non-remote flags. - Optionally cleans up the staging directory.
Examples:
# Basic (defaults to root user if user@ not provided)
sh scripts/install.sh --remote ap1
# Multiple hosts (comma or repeat) + auto dependency install + optional extras
sh scripts/install.sh --remote ap1,ap2,ap3 --deps-auto-yes --install-optional
sh scripts/install.sh --remote ap1 --remote ap2 --remote ap3 --force-config
# Remote with automatic legacy migration if detected on targets
sh scripts/install.sh --remote ap1,ap2 --auto-migrate-legacy
# Custom SSH port + force config + integrity check (automatic)
sh scripts/install.sh --remote ap1 --ssh-opts '-p 2222' --force-config
# Dry-run (prints plan, hashes, commands; makes no changes)
sh scripts/install.sh --remote ap1,ap2 --remote-dry-run
# Keep staging directory for inspection
sh scripts/install.sh --remote root@ap1 --remote-keep
# Alternate staging directory
sh scripts/install.sh --remote ap1 --remote-prefix /tmp/nrsyncd_stage_altRemote control flags (not forwarded to the target invocation):
| Flag | Purpose | Default |
|---|---|---|
--remote <host[,host2]> |
Enable remote mode for one or many hosts (comma or repeat) | (off) |
--remote-prefix <dir> |
Staging directory on target | /tmp/nrsyncd_remote |
--remote-keep |
Retain staging directory after success | (removed) |
--ssh-opts "..." |
Extra SSH options (port, identity, etc.) | (none) |
--remote-dry-run |
Show plan + hashes, exit without changes | (off) |
--remote-retries <n> |
Retry count per host (exponential backoff) | 1 |
--remote-backoff <s> |
Base seconds for backoff (2,4,8...) | 2 |
All other standard flags (--force-config, --no-start, --deps-auto-yes, etc.) are preserved and applied on the remote side.
Notes:
- Requires
ssh&tarlocally andtaron the target (present on typical OpenWrt images). - Integrity: Uses
sha256sum -con the target. If absent remotely, a warning is emitted and install proceeds (consideropkg install coreutils-sha256sum). - Multi-host executes sequentially; failures on one host do not halt others (overall exit code reflects any failure).
- Retry logic: Each host retried up to
--remote-retriestimes; delay sequence = base, base2, base4, ... using--remote-backoffas the starting seconds. - Dry-run still generates a manifest locally then removes it (no remote connections made).
- Missing optional files are noted but not fatal.
- Failures leave the staging directory intact for that host (unless missing connectivity precluded creation).
- Legacy note: remote installs will also abort on legacy
rrm_nrunless--auto-migrate-legacyis included; you can also add--cleanup-legacyor--cleanup-legacy-forceto remove legacy files/config after install. - Duplicate hosts listed in
--remoteare de‑duplicated automatically (order preserved).
- Ensure radios share a common ESSID where you want neighbor data propagated.
- Enable 802.11k + BSS transition in each interface:
option ieee80211k '1'option bss_transition '1'
- Ensure
umdnsis installed & running (opkg install umdns). - Copy files:
cp service/nrsyncd.init /etc/init.d/nrsyncd cp bin/nrsyncd /usr/bin/nrsyncd cp lib/nrsyncd_common.sh /lib/nrsyncd_common.sh 2>/dev/null || true chmod +x /etc/init.d/nrsyncd /usr/bin/nrsyncd /etc/init.d/nrsyncd enable /etc/init.d/nrsyncd start
- Verify basic operation:
logread | grep nrsyncd ubus call umdns browse | grep SSID1= /etc/init.d/nrsyncd status
logread | grep nrsyncdshows a line "All wireless interfaces are initialized." followed by periodic update lines (if debug enabled).ubus call umdns browsecontains TXT recordsSSID1=...etc./etc/init.d/nrsyncd statusshows current effective runtime values./etc/init.d/nrsyncd summaryprints concise counters & ratios./etc/init.d/nrsyncd reset_metricsclears metrics & remote uniqueness (SIGUSR2).- Install script warnings: if any
wifi-ifacelacksieee80211k '1'orbss_transition '1'(and is not disabled) you will see a WARNING prompting you to enable them for proper neighbor reports.
Enable debug logging at runtime (no restart):
uci set nrsyncd.global.debug=1
uci commit nrsyncd
/etc/init.d/nrsyncd reloadTemporarily shorten update + jitter for lab testing (DON'T use in production extremely low values):
uci set nrsyncd.global.update_interval=15
uci set nrsyncd.global.jitter_max=3
uci commit nrsyncd; /etc/init.d/nrsyncd reloadRestore defaults:
uci delete nrsyncd.global.update_interval
uci delete nrsyncd.global.jitter_max
uci delete nrsyncd.global.debug
uci commit nrsyncd; /etc/init.d/nrsyncd reload/etc/init.d/nrsyncd stop
/etc/init.d/nrsyncd disable
rm -f /etc/init.d/nrsyncd /usr/bin/nrsyncd /lib/nrsyncd_common.sh
# Optional: remove config/state
rm -f /etc/config/nrsyncd /tmp/nrsyncd_runtime /tmp/nrsyncd_metricsThe init script performs a short adaptive retry loop (200ms cadence, ≤1s total) when gathering initial rrm_nr_get_own values. On typical vanilla OpenWrt images usleep is NOT present (BusyBox sleep has 1s resolution). The script automatically detects this:
- If
usleepexists (e.g. viabusybox-extrasorcoreutils-sleep), it sleeps for true 200ms steps. - If absent, it degrades to a single 1s sleep (effectively a one-shot retry). Elapsed accounting ensures we still bound total wait time.
Installing a micro-sleep implementation (optional):
opkg update
opkg install coreutils-sleep # or a busybox providing usleep
This is purely an optimization; functionality still works without it, but initial readiness detection may be up to ~1s slower on hardware where readiness is actually ~0.2s.
Config file /etc/config/nrsyncd:
config nrsyncd 'global'
option enabled '1' # Set to 0 to disable service startup
option update_interval '60' # Seconds between update cycles (min 5 enforced)
option jitter_max '10' # Adds 0..jitter_max random seconds each cycle (capped at half interval)
option debug '0' # Set to 1 for verbose debug logging (daemon.debug)
option umdns_refresh_interval '30' # Minimum seconds between umdns update calls
option umdns_settle_delay '0' # Extra seconds to sleep immediately after an umdns update (default 0)
# Advanced (optional) readiness tuning (normally leave defaults):
# option quick_max_ms '2000' # Max ms spent in first-pass adaptive readiness (cap 5000)
# option second_pass_ms '800' # Delay ms before second-pass fetch (cap 1500)
# Skip specific interfaces (add one line per interface). Names are the part after 'hostapd.'
# Duplicates and ordering are normalized automatically. Optional 'hostapd.' prefixes are stripped.
# Examples:
# list skip_iface 'wlan0'
# list skip_iface 'wlan1-1'
# list skip_iface 'hostapd.phy0-ap1' # 'hostapd.' prefix accepted
# To remove a previously added skip, delete its line and run: /etc/init.d/nrsyncd reload
# Avoid blank lines like 'list skip_iface' with no value (they are ignored).
# list skip_iface 'phy0-ap1'
Live changes: Most runtime tunables (intervals, jitter, debug, umdns refresh, settle delay, skip_iface) can be reloaded without a full restart:
uci set nrsyncd.global.update_interval=45
uci commit nrsyncd
/etc/init.d/nrsyncd reload # sends SIGHUP; daemon re-reads UCI valuesFull restart still required only if you change structural aspects before daemon start (e.g. enabling/disabling service) or to clear internal state forcibly.
Environment mapping (init script → daemon) – new primary names with legacy (RRM_NR_*) still exported for backward compatibility in 1.0.x (deprecated 2025-10-01):
NRSYNCD_UPDATE_INTERVAL→UPDATE_INTERVALNRSYNCD_JITTER_MAX→JITTER_MAXNRSYNCD_DEBUG→ enables debug log pathNRSYNCD_UMDNS_REFRESH_INTERVAL→UMDNS_REFRESH_INTERVALNRSYNCD_QUICK_MAX_MS→ max milliseconds spent in first-pass adaptive iface readiness (default 2000, cap 5000)NRSYNCD_SECOND_PASS_MS→ delay in milliseconds before the targeted second-pass fetch (default 800, cap 1500)
All options are optional; defaults are compiled into the script.
Numeric validation & bounds:
- update_interval: minimum 5 enforced (values <5 auto-raised to 5)
- jitter_max: capped to half of effective update_interval
- umdns_refresh_interval: minimum 5 enforced
- umdns_settle_delay: forced >=0 (negative coerced to 0)
Out-of-range values are sanitized silently; check
/etc/init.d/nrsyncd statusafter reload for effective applied values.
Legacy artifacts retained for a single transition window:
| Item | Status in 1.0.0 | Deprecation Date | Action After Date |
|---|---|---|---|
RRM_NR_* environment variables |
Supported (alias to NRSYNCD_*) | 2025-10-01 | Will be removed in a minor release; update scripts to NRSYNCD_*. |
_rrm_nr._udp mDNS browse fallback |
Supported (fallback only) | 2025-10-01 | Removal planned; discovery must use _nrsyncd_v1._udp. |
Unversioned _nrsyncd._udp |
Never deployed | N/A | Do not rely on this name. |
rrm_nr_* ubus method names remain indefinitely (tied to hostapd expectations) until upstream hostapd gains a native rename.
Add one list skip_iface '<iface>' per interface (names are the portion after hostapd.). Example:
config nrsyncd 'global'
list skip_iface 'wlan0'
list skip_iface 'wlan1-1'
You may (optionally) include a hostapd. prefix; it will be stripped automatically (e.g. hostapd.wlan0).
Notes / behavior:
- Order and duplicates are normalized; the effective set is visible via
/etc/init.d/nrsyncd skiplist. - A blank
list skip_ifacewithout a value is ignored (no error); remove such lines to keep the config tidy. - Changes take effect on
/etc/init.d/nrsyncd reload(SIGHUP) without a full restart.
Skipped interfaces are omitted from TXT assembly and neighbor list updates.
On startup and reload, if any interfaces are skipped you'll see an additional log line:
Skip list: wlan0 wlan1-1
Note: The runtime status file still reports the environment variable as skip_ifaces= internally; this is only a naming artifact and maps to the configured list skip_iface entries.
| Command | Purpose | Notes |
|---|---|---|
status |
Show runtime state + embedded metrics | Reads /tmp/nrsyncd_runtime & metrics snapshot |
summary |
One‑line health overview (push/suppress/cache/remote/neighbor stats) | Fast parse for scripts |
metrics |
Raw metrics file contents | For detailed monitoring ingestion |
mapping |
Live iface → BSSID / channel / width / center1 / SSID table | Uses iwinfo + ubus |
mapping_json |
JSON array variant of mapping | Alias: mapping-json |
neighbors |
Dump current per‑iface neighbor lists (JSON) | After self‑filter |
cache |
Show per‑SSID cached list files | Includes mtime + sample head |
refresh |
Force immediate update cycle (SIGUSR1) | No timing wait |
reset_metrics |
Reset counters & remote uniqueness (SIGUSR2) | Keeps cache/data intact |
diag / timing_check |
Readiness probe timings for each iface | Alias: timing-check |
skiplist |
Effective configured skip_iface entries | Normalized view |
version |
Init script version (and git hash if available) | Sync with release tag |
metadata |
Show parsed live mDNS TXT advertisement (SSIDn tokens + metadata v/c/h) | Useful to verify ordering and hash |
/etc/init.d/nrsyncd reload triggers a SIGHUP which now:
- Re-reads UCI for: update_interval, jitter_max, umdns_refresh_interval, umdns_settle_delay, debug, skip_iface.
- Applies sanity caps (min 5s interval; jitter ≤ half interval; umdns refresh ≥5s).
- Rebuilds interface→SSID mapping and reapplies skip list.
- Logs a concise before→after summary (
Reload (SIGHUP): U=60 J=10 ... -> U=45 J=5 ...). If any timing shrinks, the current sleep is interrupted for an immediate cycle.
Status:
/etc/init.d/nrsyncd status
Shows current effective runtime values (from /tmp/nrsyncd_runtime).
Next cycle uses the new timing immediately; current sleep is unaffected until next iteration (keeps logic simple and race‑free).
See examples/wireless.config for a dual‑band setup. Key lines:
option ieee80211k '1'
option bss_transition '1'
option time_advertisement '2'
| Symptom | Check |
|---|---|
| Service never starts | logread for errors about missing /etc/config/wireless or zero interfaces. |
| Hangs at "Waiting for all wireless interfaces" | ubus list hostapd.* count vs enabled wifi-iface stanzas; ensure no disabled ones assumed active. |
| Missing TXT records | Confirm umdns running and firewall allows mDNS; inspect assembly by temporarily adding a logger -t nrsyncd "$rrm_own" before procd_open_instance. |
| Immediate restart with wpad restart | Likely ubus exit code 4 from rrm_nr_get_own; watch for upstream OpenWrt bug reference (see init script comment). |
Metrics are written to /tmp/nrsyncd_metrics each cycle (atomic rename). Fields:
| Field | Meaning | Notes |
|---|---|---|
cycle |
Number of update cycles completed | Increments once per full loop |
cache_hits |
Times a per-SSID group list matched existing cache | Diff done on whitespace-stripped JSON |
cache_misses |
Times a per-SSID list changed (or forced) and was rewritten | Includes forced writes from empty -> populated |
nr_sets_sent |
Per-interface neighbor lists actually pushed to hostapd | Baseline + subsequent changes |
nr_sets_suppressed |
Candidate per-interface updates skipped because list unchanged | Grows with stable environment |
remote_entries_merged |
Cumulative count of remote TXT lines ingested | Adds each cycle; not unique count |
remote_unique_cycle |
Distinct remote TXT entries observed in the most recent cycle | After per-cycle sort -u |
remote_unique_total |
Cumulative distinct remote TXT entries since daemon start | Backed by on-disk seen set |
last_update_time |
Epoch of most recent successful hostapd rrm_nr_set call | 0 until first push |
baseline_ssids |
Count of distinct SSIDs that received a baseline push this process | Ensures each SSID delivered once even if no diff |
suppression_ratio_pct |
Integer percent = suppressed / (sent+suppressed) * 100 | 0 when no updates yet |
nr_set_failures |
Count of failed rrm_nr_set ubus calls |
Inspect logs if non‑zero; indicates hostapd rejection or transient ubus issues |
neighbor_count_<iface> |
Post self‑filter neighbor list size for interface <iface> |
One dynamic key per active, non‑skipped interface |
Interpretation tips:
- A high
suppression_ratio_pct(e.g. >95%) indicates a stable environment (expected after convergence). baseline_ssidsshould equal the number of distinct SSIDs served locally (after a fresh start). If smaller, check logs for missing SSID mapping lines.remote_entries_mergedwill typically be (#remote TXT lines) * cycles; large growth alone is not an issue.- If
nr_sets_sentstalls at 0 whilecache_missesincrements, hostapdrrm_nr_setmay be failing (enable debug and inspect logs). - Non‑zero
nr_set_failureswarrants checkinglogread -e nrsyncdfor rejection causes. neighbor_count_<iface>lets you quickly spot asymmetric visibility (e.g. one radio consistently sees fewer peers).
Ordering & Dedupe:
- Neighbor lists are now deterministically ordered by (SSID, BSSID) and duplicate SSID+BSSID pairs arriving from multiple remote sources are suppressed to a single entry.
- Per‑interface lists exclude that interface’s own BSSID triplet; counts reflect only true neighbors.
Example excerpt (with newer fields):
cycle=37
cache_hits=96
cache_misses=2
nr_sets_sent=3
nr_sets_suppressed=102
remote_entries_merged=111
remote_unique_cycle=3
remote_unique_total=7
last_update_time=1756609138
baseline_ssids=2
suppression_ratio_pct=97
nr_set_failures=0
neighbor_count_phy0-ap0=4
neighbor_count_phy0-ap1=3
neighbor_count_phy1-ap0=4
In this example two SSIDs had baseline delivery, three interfaces received initial lists, and subsequent interface-level opportunities were mostly suppressed (97% stability). Remote uniqueness indicates 7 distinct remote entries seen cumulatively, 3 present in the latest cycle.
/etc/init.d/nrsyncd summary prints a single concise line aggregating key counters, uniqueness and neighbor distribution stats. Example:
summary: cycles=37 pushes=3 suppressed=102 suppression=97% cache(hit/miss)=96/2 baseline_ssids=2 remote(entries=111 uniq_cycle=3 uniq_total=7) failures=0 neigh(min=3@phy0-ap1 max=4@phy0-ap0,phy1-ap0 avg=3 ifaces=3) last_update=1756609138
Field groups:
cycles/pushes/suppressed/suppression: High suppression with low failures indicates convergence.cache(hit/miss): Misses correspond to per‑SSID list mutations (or baseline). Persistent misses imply churn.baseline_ssids: Distinct SSIDs baseline‑delivered this process (should match local distinct SSIDs after first cycle).remote(entries=E uniq_cycle=C uniq_total=T): Raw remote TXT lines merged, distinct in current cycle, and cumulative distinct since start (normalized).failures: Count of hostapdrrm_nr_setfailures. Non‑zero warrants log inspection.neigh(min=MIN@ifaces max=MAX@ifaces avg=AVG ifaces=N): Post self‑filter neighbor list sizes across active interfaces (min/max list the interface names; multiple if comma‑separated). Large disparity may indicate RF isolation or configuration differences.last_update: Epoch seconds for most recent successful neighbor list push.
Use the summary for quick health checks (automation, monitoring) without parsing the full metrics file.
- Read‑only interaction with hostapd via
ubus(rrm_nr_get_own,rrm_nr_setto push lists). Does not modify wireless configuration. - Writes ephemeral state only under
/tmp(/tmp/nrsyncd_state,/tmp/nrsyncd_metrics,/tmp/nrsyncd_runtime); no persistent data besides optional config. - mDNS exposure limited to TXT records enumerating encoded NR data; no management frames transmitted directly.
- No credentials or sensitive secrets handled; logging omits raw neighbor hex beyond what hostapd already provides.
See docs/CHANGELOG.md for detailed history and docs/DEVELOPER.md for the full release checklist. Upgrade from pre‑rebrand versions generally requires replacing service/nrsyncd.init (formerly service/rrm_nr.init), bin/nrsyncd (formerly bin/rrm_nr), and lib/nrsyncd_common.sh, then restarting (/etc/init.d/nrsyncd restart). Baseline pushes will repopulate hostapd lists and metrics reset automatically. Legacy env vars are still exported for backward compatibility this release.
High‑level ideas (details and additional guidance in docs/DEVELOPER.md):
- Per‑interface opt‑out (planned) via
option nrsyncd_disable '1'. - Additional feature TXT flags appended after SSID arguments.
- Retry logic for non‑4 ubus failures (bounded, minimally noisy).
- Delimiter
+is intentional (invalid in SSIDs) – do not change; consumers rely on orderedSSIDn=prefix positions. - Minimal dependencies: BusyBox POSIX shell only (avoid
[[and arrays).jsonfilteris assumed present on OpenWrt. - Assumes hostapd exposes
rrm_nr_get_own; if upstream API changes, a fallback probe sequence will be added. - Adaptive readiness / retry algorithm & detailed assembly log line formats are documented in
docs/DEVELOPER.md(see "Neighbor Assembly & Adaptive Retry").
To keep files across firmware flashes, add these lines to /etc/sysupgrade.conf (or use sh scripts/install.sh --add-sysupgrade):
/etc/init.d/nrsyncd
/usr/bin/nrsyncd
/lib/nrsyncd_common.sh
Note: UCI configs in /etc/config/* (including /etc/config/nrsyncd) are already preserved by sysupgrade; no need to list them.
Manual addition:
for f in /etc/init.d/nrsyncd /usr/bin/nrsyncd /lib/nrsyncd_common.sh; do
grep -qx "$f" /etc/sysupgrade.conf || echo "$f" >> /etc/sysupgrade.conf
doneVerify later:
grep nrsyncd /etc/sysupgrade.confRuntime tools normally present: ubus, jsonfilter, iwinfo, umdns. If missing:
Using opkg (traditional):
opkg update
opkg install umdns jsonfilter iwinfoUsing apk (emerging images):
apk update
apk add umdns jsonfilter iwinfoOptional high-resolution sleep:
opkg install coreutils-sleep # or
apk add coreutilsSummary (full discussion and future roadmap ideas in docs/DEVELOPER.md):
- Large deployments (>20 AP) may see slower convergence.
- Binary daemon is prebuilt (no current source in repo).
- Remote ingestion relies on mDNS reachability; packet loss can delay uniqueness visibility.
- Very large remote sets could inflate lists (future cap option contemplated).
GPLv2 – see LICENSE.
Collect each hostapd's 802.11k neighbor report and advertise them via mDNS TXT records so peer APs (same ESS) can rapidly share steering hints.