How does rrm work?

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...

1 Like

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!

@Pablomagno Hi, did you fix it= I'm facing the same problem and I don't know where to start!

Try using the rrm_nr_set command with double brackets. For example instead of:

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

Try:

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

2 Likes

I finally managed to obtain some results. I'm going to document them here so others can repeat the process:

  1. update /etc/config/dawn and set parameter set_hostapd_nr to '1' note, default value is '0' and another possible value is '2' I set it to '1' because the report is set by a different script but maybe '2' should be the right option

  2. Install /etc/init.d/neighbor_reporting by just doing a copy and paste of the script up on this thread; then chmod +x to it.

  3. Install /usr/bin/rrm by just doing a copy and paste of the script up on this thread; then chmod +x to it.

  4. Start and enable the neighbor_reporting so it can produce reports.

Now I can obtain results. I don't know if this is obsolete or if there is another preferred method, but this is what I've been table to set up today.

Hope this is useful to any others.

Regards
Ignacio

I am glad that this worked for you. But I want to notice that this is specific for the "DAWN" controller, which is a particular case.

802.11v should be available to be used directly, via ubus calls, to make an integration with any wifi controller.

Since my last post, I did not advance with this because I was working in the optimization of the "legacy" way (simply send a deauth to steer a STA) with my own controller. But I am about to continue with the implementation of 802.11v so probably I will be back soon with news.

I seemed to see that it was no longer necessary to register the neighbors and a BTM request could be sent to a device directly, just indicating the mac addresses in the same ubus call. But I have to check it.

If not, it would go back to the investigation to load the neighboring BSSs into the list.

2 Likes

I'm here, after a long time, but with good news. First of all, thank you to @huaracheguarache because his tip helped to me to write properlly the ubus calls.

To begin, I want to clarify something, because always there are a confusion about 802.11 k/v/r. They are often being mixed as part of the same thing, like a bounding condition, and this is not true. Each standard can work independently, of course there are sinergy points between them, but you need to understan what is the scope of each one:

802.11v: BTM requests. This feature allows an AP to steer a STA to a desired BSS, or just to suggest to him to check if they have a better BSS to connect (in facts, this is always a suggestion because the "last word" is always from the client).

802.11k: This is meant for metrics exchange. The most common usage is the Beacon report metrics, in which one device asks to another for information/metrics regarding the beacons a device can monitor.

802.11r: aka "Fast-Roaming"(really this should be called "Fast-Steering"). Is a mechanism to allow fast associations when a STA leaves a BSS and associates to another of the same network.

Done my disclaimer, I want to explain how I was able to make BSS changes with 802.11v without having to configure anything else, that had nothing to do with this issue.

I explain from an example:

ubus call hostapd.bss0 bss_transition_request '{"addr":"24:46:c8:bb:08:5f","disassociation_imminent":false, "abridged":true, "validity_
period":20, "neighbors":["3460f9226df2ef1900008095090603029b00"]}'

In this case, I am moving the STA of MAC address "24:46:c8:bb:08:5f" to the BSS of BSSID "34:60:f9:22:6d:f2". That is all, when I used this command, the BTM request/response occured:

My only doubt left is how if formed that ID which should be used to indicate the target BSS. This is not the BSSID. Is the BSSID+something, that probably depends on the SSID and password (an encoding, a hash or a signature by using both cretentials). I remember something about some IDs which are mainly relevant in 802.11r (like mobility domain) but at the moment I don't figure which one is this ID.

So my workaround, to obtain this ID, is by using:

ubus call hostapd.bss1 rrm_nr_get_own

Which returns something like:

{
        "value": [
                "34:60:f9:22:6d:f2",
                "Pablomagno",
                "3460f9226df2ef1900008095090603029b00"
        ]
}

And you can notice inmediatelly that the trailer after the BSSID is the same at least to the whole WiFi network which shares the SSID and (maybe) the password.

2 Likes

This may help explain how the ID is generated:

1 Like

This remains unclear for me, because it indicates BSSID starting from byte 3 while I find that is starting from byte 0.

Also, if all of that is true, I find really inconvenient the need of all of that information just to indicate a client steering. Considering that this information should be available in an AP, which makes really a problem if you are using a centralized wifi controller with edge APs as dumb as possible. Also this has several issues regarding WiFi Alliance EasyMesh where all that additional information to the BSSID is not indicated as required to make an AP change :melting_face:

I will try to check directly in 802.11 if all that info can be omited if it is not available...

Here we have a really complex structure:

image

The first two bytes are implicit in the configuration, so they should be ommited.

It is easy to see that if you need to infere all these fields, specially the BSSID information, you probably will have a bad time.

I have done some tests and filling with 7 00s ("seven zeroes") after the target BSSID, the BTM request seems to work as expected. Of course the STA probably does the task quickier and easier if you fill this fields with proper infomation (specially channel and operating class), but I think at least I can convey a bit of peace of mind that you can do without filling all that information, to be able to achieve a more "simple" implementation of STA steering between physically separated APs.

For example, in the previous example, filling with seven 00s the same result can be achieved:

BSSID -> 3460f9226df2 -> 3460f9226df200000000000000

ubus call hostapd.bss0 bss_transition_request '{"addr":"24:46:c8:bb:08:5f","disassociation_imminent":false, "abridged":true, "validity_
period":20, "neighbors":["3460f9226df200000000000000"]}'

I have been playing around with various things to improve the way my 2 AP's play along and keep STA's connected to the one which is nearest to them.

I read the above and I wonder... You know what AP's are in your network and what IP they have. If you setup a dropbear ssh key between the AP's you can simply request the other AP's BSSID Information hash:

ssh [ OTHERAPIP ] -i ~/.ssh/id_rsa "for WIFIIF in `iwinfo | grep "[ SSID ]" | awk -F' ' '{print $1}'`; do rrmown=`ubus call hostapd.$WIFIIF rrm_nr_get_own | jq -r '.value[2]'`; echo $rrmown; done"

Obviously this requires jq and iwinfo to be installed, but you may be able to find a more elegant way to achieve the same solution in lua. The above will request the BSS for all WiFi interfaces active on a certain SSID. This can then be stored in a cache file in /tmp for all known AP's and used to forward STA's to another (better suited) AP. This only needs to be done once every time the wifi config changes.