How does rrm work?

My intention is to exercise 11k protocol by itself. We have two RM capabilities in the beacon to enable through ubus call hostapd.wlan0 bss_mgmt_enable '{"neighbor_report":True, "beacon_report":True}'. I just wanted to find out how an AP collects neighbor AP list. My query is if AP collects neighbor ap list by scanning or by sending action frame to client and client responds with it's scan results. I am not attempting to do client guided roaming. Thanks.

There are different use cases:

  1. AP exchange NB reports through backbone and clients request roaming possibilites through beacon
  2. AP requests NB report from client to get hearing map information
  3. ...

I'm not sure, what else. :wink:

1 Like

Thanks for the response. I am pursuing pt.#2 which you've done it with your patch, I guess.

What are your plans in terms of coding? I sent a mail via openwrt mailing list and hopefully it is getting merged. :slight_smile:

1 Like

I have no plan to do any coding at the moment as regards to rrm feature. I'm just curious to see whether there is any of 802.11k feature/protocol in place in the OpenWrt. Anyway I appreciate your contribution!

Could we maybe finally package that script?

1 Like

In /etc/init.d/neighbor_report:

#!/bin/sh /etc/rc.common

START=90

USE_PROCD=1

radios=$(ubus list | grep hostapd.wlan)

start_service() {
	local rrm_own
	
	OIFS=$IFS
	IFS=$'\n'

	for value in ${radios}
	do
		rrm_own=${rrm_own}",$(ubus call ${value} rrm_nr_get_own | jsonfilter -e '$.value')"
	done

	IFS=$OIFS

	procd_open_instance
	procd_set_param command /bin/sh "/usr/bin/rrm"
	procd_add_mdns "rrm" "udp" "" "${rrm_own:1}"
	procd_close_instance
}

service_triggers()
{
	procd_add_reload_trigger wireless
}

In /usr/bin/rrm

#!/bin/sh

radios=$(ubus list | grep hostapd.wlan)

_discover_neighbors() {
	local rrm_neighbors

	ubus call umdns update
	sleep 2

	rrm_neighbors=$(ubus call umdns browse | jsonfilter -e '@["_rrm._udp"][*].txt')

	for value in ${rrm_neighbors}
	do
		rrm_nr_lists=${rrm_nr_lists}",${value}"
	done
}

_append_own() {
	for value in ${radios}
	do
		rrm_nr_lists=${rrm_nr_lists}",$(ubus call ${value} rrm_nr_get_own | jsonfilter -e '$.value')"
	done
}

while true
do
	OIFS=$IFS
	IFS=$'\n'

	rrm_nr_lists=""

	_discover_neighbors
	_append_own

	for value in ${radios}
	do
		ubus call ${value} bss_mgmt_enable '{"neighbor_report": true}'
		eval "ubus call ${value} rrm_nr_set '{ \"list\": [ ${rrm_nr_lists:1} ] }'"
	done

	sleep 60

	IFS=$OIFS
done

exit 0

These scripts would probably fare better if they were (1) integrated as easily as 802.11r is now enabled via mac80211.sh and (2) lives as a hotplud.d script watching the wlan0/wlan1 interface(s). Right now, I don't really have the time to figure this out myself and this script from @sotux and @muddyfeet does what it needs to do albeit a few caveats if something is shut down/rebooted.

Additionally, umdns is a requirement.

2 Likes

I'm now able to set the nr reports for hostapd with dawn:

2 Likes

I'm playing with the ubus commands to see how my devices respond to manual instructions. I'm finding that one of the APs is missing the wnm_disassoc_imminent method from ubus -v list hostapd.wlan0-1.

/tmp/run/hostapd-phy0.conf look very similar on each device, and both have disassoc_low_ack=1

Can anyone give me a clue if that is a hardware, driver, configuration or perhaps package snag?

In case it helps, the one without the method is running ath79/tiny DTS style, the one with it is ar71xx/nand.

EDIT: Looks like this may depend on hostapd and / or other things being compiled with CONFIG_WNM_AP defined...

Well it's appeared now after some random phaffing that included a reboot. My best guess is that swapping wpad-mini to wpad is what was needed, but I missed the correct init.d restart until the reboot sorted it.

I can now effortlessly push at least one of my devices between the two APs at will with shell commands. Now need a script to push the right ones at the right time, so will reread above...

Hi,

The neighbor_report init script fails in a number of interesting ways.

A partial fix is to move getting the interface list in the rrm script to within the loop.

Getting umdns to update is something to be sorted.

-- marcel

/etc/init.d/neighbor_report

#!/bin/sh /etc/rc.common

START=90

USE_PROCD=1

start_service() {
	local rrm_own
	
	OIFS=$IFS
	IFS=

	for value in $(ubus list | grep hostapd.wlan)
	do
		rrm_own=${rrm_own}",$(ubus call ${value} rrm_nr_get_own | jsonfilter -e '$.value')"
	done

	IFS=$OIFS

	procd_open_instance
	procd_set_param command /bin/sh "/usr/bin/rrm"
	procd_add_mdns "rrm" "udp" "0" "${rrm_own:1}"
	procd_close_instance
}

service_triggers()
{
	procd_add_reload_trigger wireless
}

/usr/bin/rrm

#!/bin/sh

_discover_neighbors() {
	local rrm_neighbors

	ubus call umdns update
	sleep 2

	rrm_neighbors=$(ubus call umdns browse | jsonfilter -e '@["_rrm._udp"][*].txt')

	for value in ${rrm_neighbors}
	do
		rrm_nr_lists=${rrm_nr_lists}",${value}"
	done
}

_append_own() {
	for value in $(ubus list | grep hostapd.wlan)
	do
		rrm_nr_lists=${rrm_nr_lists}",$(ubus call ${value} rrm_nr_get_own | jsonfilter -e '$.value')"
	done
}

while true
do
	OIFS=$IFS
	IFS=$'\n'

	rrm_nr_lists=""

	_discover_neighbors
	_append_own

	for value in $(ubus list | grep hostapd.wlan)
	do
		ubus call ${value} bss_mgmt_enable '{"neighbor_report": true}'
		eval "ubus call ${value} rrm_nr_set '{ \"list\": [ ${rrm_nr_lists:1} ] }'"
	done

	sleep 60

	IFS=$OIFS
done

exit 0
1 Like

@PolynomialDivision has continued work on rrm with his OpenWRT dawn package. Try out his dawn package + luci app and see what you think.

I have, and it looks very promising.

For those that want to wait for 20.x to be released, and not fiddle with dawn (I don't want to risk messing up my main gateway yet), I have updated the scripts above to address some of the shortcomings.

  • Fixed some race conditions and minor bugs
  • More robust parsing of inputs
  • Init script neighbor_reporting waits for interfaces (keep START=99, otherwise with DFS you may get a super-long boot)
  • Information is only published for proper SSIDs - if you have >1 SSID, now information is only published for the same SSID (e.g: you do 802.11r only on one of them, or 2.4 and 5 bands have different SSIDs)
  • Checks whether an action needs applying - if nothing changes, no action is performed: this helps with low-mem devices that cannot reload the configuration every 60s
  • Reports basic errors to logread when conditions are not met
  • Periodic script rrm also checks whether own_rrm is ok, and if not applies it
  • Info is published delimited by pipe "|", for easier parsing at the receivers

Limitations:

  1. Assumes wireless lan interfaces are named wlanX
  2. Assumes each station has exactly 1 IP address

/etc/init.d/neighbor_reporting

#!/bin/sh /etc/rc.common

START=99

NAME=neighbor_reporting
USE_PROCD=1

start_service() {
	[ ! -f /etc/config/wireless ] && logger -t "${NAME}" -pdaemon.error "/etc/config/wireless does not exist" && exit 1
	[ $(grep -q 'wifi-iface' /etc/config/wireless) ] && logger -t "${NAME}" -pdaemon.error "/etc/config/wireless does not have any wifi-iface stanzas" && exit 1

	local rrm_own
	
	while [ "$(ubus list hostapd.wlan* | wc -l)" != "$(grep 'wifi-iface' /etc/config/wireless | wc -l)" ]; do
		logger -t "${NAME}" -pdaemon.info "Waiting for all interfaces to initialize"
		sleep 30
	done

	OIFS=$IFS
	IFS=$'\x0a'

	for value in $(ubus list hostapd.wlan*); do
		rrm_own="${rrm_own}|$(/bin/ubus call ${value} rrm_nr_get_own | /usr/bin/jsonfilter -e '$.value')"
	done

	IFS=$OIFS

	procd_open_instance
	procd_set_param command /bin/sh "/usr/bin/rrm"
	procd_add_mdns "rrm" "udp" "0" "${rrm_own:1}"
	procd_close_instance
}

service_triggers()
{
	procd_add_reload_trigger wireless
}

/usr/bin/rrm

#!/bin/sh /etc/rc.common

NAME=rrm
USE_PROCD=1

# Check if our own rrm is set properly
function _check_own_rrm() {
	HOST_IP=$(ubus -v call network.interface.lan status | jsonfilter -e '@["ipv4-address"][0].address')
	CUR_UMDNS=$(ubus call umdns browse | jsonfilter -e "@[\"_rrm._udp\"][@.ipv4=\"${HOST_IP}\"].ipv4")
	[ -z "${CUR_UMDNS}" ] && return 1
	return 0
}

# Sets own rrm
function _setup_own_rrm() {
	[ ! -f /etc/config/wireless ] && logger -t "${NAME}" -pdaemon.error "/etc/config/wireless does not exist" && exit 1
	[ $(grep -q 'wifi-iface' /etc/config/wireless) ] && logger -t "${NAME}" -pdaemon.error "/etc/config/wireless does not have any wifi-iface stanzas" && exit 1

	local rrm_own

	while [ "$(ubus list hostapd.wlan* | wc -l)" != "$(grep 'wifi-iface' /etc/config/wireless | wc -l)" ]; do
		logger -t "${NAME}" -pdaemon.info "Waiting for all interfaces to initialize"
		sleep 30
	done

	OIFS=$IFS
	IFS=$'\x0a'

	for value in $(ubus list hostapd.wlan*); do
		rrm_own="${rrm_own}|$(/bin/ubus call ${value} rrm_nr_get_own | /usr/bin/jsonfilter -e '$.value')"
	done

	IFS=$OIFS

	procd_open_instance
	procd_set_param command /bin/sh "/usr/bin/rrm"
	procd_add_mdns "rrm" "udp" "0" "${rrm_own:1}"
	procd_close_instance
}

# We wrap everything into this function to allow GC by ash, esp. for low-mem devices
function _do_updates() {
	local rrm_nr_lists

	OIFS=$IFS
	IFS=$'\n'
	# Discover neighbors and self
	ubus call umdns update
	sleep 2
	for wifi_iface in $(ubus list hostapd.wlan* | awk -F. '{ print $2; }'); do
		net_ssid=$(uci get wireless.${wifi_iface}.ssid)
		[ -z "${net_ssid}" ] && logger -t "rrm" -p daemon.error "${wifi_iface}: does not have ssid according to uci" && continue
		
		# Discover other nodes
		rrm_nr_lists=""
		# Sort at the end stabilizes the result, so we can compare it across runs
		for discovered_node in $(ubus call umdns browse | jsonfilter -e '@["_rrm._udp"][*].txt' | tr '|' '\n' | grep "\"${net_ssid}\"" | sort -u); do
			rrm_nr_lists="${rrm_nr_lists},${discovered_node}"
		done

		# Add own rrm (worst case this is the only node)
		# Own's rrm is actually ignored when setting the list, no need to add it
#		rrm_nr_lists="${rrm_nr_lists},$(/bin/ubus call hostapd.${wifi_iface} rrm_nr_get_own | /usr/bin/jsonfilter -e '$.value')"
		[[ "${rrm_nr_lists}" == "," ]] && logger -t "rrm" -p daemon.error "${wifi_iface}: no neighbors detected, nothing to do" && continue

		ubus call hostapd.${wifi_iface} bss_mgmt_enable '{"neighbor_report": true}'
		prev_rrm_list=$(ubus call hostapd.${wifi_iface} rrm_nr_list | jsonfilter -e '@.list')
		if [[ "$(echo -n ${prev_rrm_list} | sed 's: ::g')" == "$(echo -n [${rrm_nr_lists:1}] | sed 's: ::g')" ]]; then
			# Setting a new list will cause the wifi to quickly cycle, which we do not want every 60s
			continue
		fi
		ubus call hostapd.${wifi_iface} rrm_nr_set "{ \"list\": [ ${rrm_nr_lists:1} ] }"
	done

	IFS=$OIFS
}

while true; do
	_check_own_rrm || _setup_own_rrm 
	_do_updates

	sleep 60
done

exit 0

3 Likes

For anyone else who finds that this script does not work for them, I had to remove the comment from the following line:

# rrm_nr_lists="${rrm_nr_lists},$(/bin/ubus call hostapd.${wifi_iface} rrm_nr_get_own | /usr/bin/jsonfilter -e '$.value')"

I also had to change config wifi-iface in /etc/config/wireless from default_radioX to wlanX

1 Like

Maybe it would be a good idea to write something like this in lua? :slight_smile:

Interesting, I commented that line exactly because when setting the list, the host's own AP gets ignored:

  • host A, address X
  • host B, address Y

Setting [X,Y] on host A, ubus call hostapd.<iface> rrm_nr_list yields only [Y].

I am not sure whether this behavior depends on drivers/hostapd etc. If that's the case also on your router, keep it commented, otherwise the comparison at the end of the loop will always be false, and configuration will be reapplied every 60s even though nothing changed.

@PolynomialDivision : I don't know lua enough to write a script in it :slight_smile:

Another update to /usr/bin/rrm: I couldn't test _setup_own_rrm it properly before, procd support wasn't really working, so it's just easier to restart the service when own rrm is not setup in umdns.

#!/bin/sh

NAME=rrm

# Check if our own rrm is set, otherwise force restart of neighbor_reporting service
function _check_own_rrm() {
	HOST_IP=$(ubus -v call network.interface.lan status | jsonfilter -e '@["ipv4-address"][0].address')
	CUR_UMDNS=$(ubus call umdns browse | jsonfilter -e "@[\"_rrm._udp\"][@.ipv4=\"${HOST_IP}\"].ipv4")
	[ -z "${CUR_UMDNS}" ] && return 1
	return 0
}

# Sets own rrm
function _setup_own_rrm() {
	nohup /etc/init.d/neighbor_reporting restart
}

# We wrap everything into this function to allow GC by ash, esp. for low-mem devices
function _do_updates() {
	local rrm_nr_lists

	OIFS=$IFS
	IFS=$'\n'
	# Discover neighbors and self
	ubus call umdns update
	sleep 2
	for wifi_iface in $(ubus list hostapd.wlan* | awk -F. '{ print $2; }'); do
		net_ssid=$(uci get wireless.${wifi_iface}.ssid)
		[ -z "${net_ssid}" ] && logger -t "rrm" -p daemon.error "${wifi_iface}: does not have ssid according to uci" && continue
		
		# Discover other nodes
		rrm_nr_lists=""
		# Sort at the end stabilizes the result, so we can compare it across runs
		for discovered_node in $(ubus call umdns browse | jsonfilter -e '@["_rrm._udp"][*].txt' | tr '|' '\n' | grep "\"${net_ssid}\"" | sort -u); do
			rrm_nr_lists="${rrm_nr_lists},${discovered_node}"
		done

		# Add own rrm (worst case this is the only node)
		# Own's rrm is actually ignored when setting the list, no need to add it
#		rrm_nr_lists="${rrm_nr_lists},$(/bin/ubus call hostapd.${wifi_iface} rrm_nr_get_own | /usr/bin/jsonfilter -e '$.value')"
		[[ "${rrm_nr_lists}" == "," ]] && logger -t "rrm" -p daemon.error "${wifi_iface}: no neighbors detected, nothing to do" && continue

		ubus call hostapd.${wifi_iface} bss_mgmt_enable '{"neighbor_report": true}'
		prev_rrm_list=$(ubus call hostapd.${wifi_iface} rrm_nr_list | jsonfilter -e '@.list')
		if [[ "$(echo -n ${prev_rrm_list} | sed 's: ::g')" == "$(echo -n [${rrm_nr_lists:1}] | sed 's: ::g')" ]]; then
			# Setting a new list will cause the wifi to quickly cycle, which we do not want every 60s
			continue
		fi
		ubus call hostapd.${wifi_iface} rrm_nr_set "{ \"list\": [ ${rrm_nr_lists:1} ] }"
	done

	IFS=$OIFS
}

while true; do
	_check_own_rrm || _setup_own_rrm 
	_do_updates

	sleep 60
done

exit 0

1 Like

I have tried:

ubus call hostapd.wlan0 rrm_nr_set '{"list": ["30:23:03:dc:fc:8a","Pablomagno","302303dcfc8aef1900008095090603029b00"]}'

Also:

ubus call hostapd.wlan0 rrm_nr_set '{"list": ["30:23:03:dc:fc:8a"]}'

Just to add a neighbor, but always the list from ubus call hostapd.wlan0 "rrm_nr_list" is empty:

{

        "list": [

        ]
}

I understand that I can not send a BTM request to a client indicating another BSSID if I don't have it registered as a neighbor, right? So I need to find the way to add the desired BSSID (30:23:03:dc:fc:8a) to the neighbor list... but this seems not to be happening.

Anyone find any clue to solve this??

Thanks!!

@Pablomagno: does "logread -f" show anything in a different terminal while running the commands?

Below another updated version of /usr/bin/rrm.
Changes: updated _check_own_rrm() function to actually work. Not sure how I didn't catch it earlier, but the previous implementation kept restarting the service. Reading above, umdns does not show local entries, so this would have never worked. Changed it to use the "service" call.

#!/bin/sh

NAME=rrm

# Check if our own rrm is set, otherwise force restart of neighbor_reporting service
function _check_own_rrm() {
	CUR_UMDNS=$(ubus call service list | jsonfilter -e "@[\"neighbor_reporting\"][*][*].data.mdns.rrm_0.txt")
	[ -z "${CUR_UMDNS}" ] && return 1
	return 0
}

# Sets own rrm
function _setup_own_rrm() {
	nohup /etc/init.d/neighbor_reporting restart
}

# We wrap everything into this function to allow GC by ash, esp. for low-mem devices
function _do_updates() {
	local rrm_nr_lists

	OIFS=$IFS
	IFS=$'\n'
	# Discover neighbors and self
	ubus call umdns update
	sleep 2
	for wifi_iface in $(ubus list hostapd.wlan* | awk -F. '{ print $2; }'); do
		net_ssid=$(uci get wireless.${wifi_iface}.ssid)
		[ -z "${net_ssid}" ] && logger -t "rrm" -p daemon.error "${wifi_iface}: does not have ssid according to uci" && continue
		
		# Discover other nodes
		rrm_nr_lists=""
		# Sort at the end stabilizes the result, so we can compare it across runs
		for discovered_node in $(ubus call umdns browse | jsonfilter -e '@["_rrm._udp"][*].txt' | tr '|' '\n' | grep "\"${net_ssid}\"" | sort -u); do
			rrm_nr_lists="${rrm_nr_lists},${discovered_node}"
		done

		# Add own rrm (worst case this is the only node)
		# Own's rrm is actually ignored when setting the list, no need to add it
#		rrm_nr_lists="${rrm_nr_lists},$(/bin/ubus call hostapd.${wifi_iface} rrm_nr_get_own | /usr/bin/jsonfilter -e '$.value')"
		[[ "${rrm_nr_lists}" == "," ]] && logger -t "rrm" -p daemon.error "${wifi_iface}: no neighbors detected, nothing to do" && continue

		ubus call hostapd.${wifi_iface} bss_mgmt_enable '{"neighbor_report": true}'
		prev_rrm_list=$(ubus call hostapd.${wifi_iface} rrm_nr_list | jsonfilter -e '@.list')
		if [[ "$(echo -n ${prev_rrm_list} | sed 's: ::g')" == "$(echo -n [${rrm_nr_lists:1}] | sed 's: ::g')" ]]; then
			# Setting a new list will cause the wifi to quickly cycle, which we do not want every 60s
			continue
		fi
		ubus call hostapd.${wifi_iface} rrm_nr_set "{ \"list\": [ ${rrm_nr_lists:1} ] }"
	done

	IFS=$OIFS
}

while true; do
	_check_own_rrm || _setup_own_rrm 
	_do_updates

	sleep 60
done

exit 0

I'll be checking this. Thank you very much!