Individual per-passphrase/per-MAC Wifi VLANs using wpa_psk_file (no RADIUS required)

Interesting observations!

I had read somewhere on this forum that a value of 20000 was recommended for Apple devices, so that is what I had been running for a long time. I changed it to 50000 during my pursuit of trying to solve this VLAN reassignment issue and haven't set it back down lower yet. Maybe I'll do that in a few minutes...

Is this a setting on your phone/test device?

I'm not sure what to make of this yet. I'm pondering this.

For clarification sake, are you saying the FT request is triggering a restart of the AP itself?

It is a setting on test device, it will only be random if the wifi phy restarted.

it's:
wireless.default_radio0.macaddr='random'

It looks like to happen like that but only when the reassociation deadline was default value, I still need to observe with 20000.

thanks. I managed to make your patches applied but placing it on the directory you specified doesnt seem to work, at least on 24.10.1 sdk, it ignores it and the compiled version works similarly as the one from openwrt's repo and ft is broken as usual.

ive read deeper on how to apply those patches manually on a specific package (eg on wpad-mbedtls in my case) involves cding deeper inside the build_dir directory all the way to the package you want to apply the patch (which in my case hostapd-wpad-full-mbedtls), from there, I wget your patch, run quilt import, quilt push, then verify it and run quilt series, after that I compiled it using the make command, install the ipks to the APs (for some reason, I need to reboot those device just to get them installed as opkg stubbornly wants the internet version of the ipk), after installing it, reboot it another just in case and it looks like somethings working this time, tried roaming around and see if it still jumps to default network when roaming and it seems to work great now, it no longer vlan hop to default vlan now. Im going to test it further and I hope this fixes it.

hardware used: 4x Tplilnk ax-23 and 1x Zbtlink ZBT-WG3526 (32M) all of them are mt7621 based soc.

will give feedback if issue pops up.

Super! That sounds great! Out of curiosity, did you run the logread command I provided and do you see log entries that match?

yes there are some new messages in logs containing like this

Wed Jul 30 13:36:10 2025 daemon.notice hostapd: FT: Found matching PSK for xx:xx:xx:xx:xx:xx (vlan_id=10)
Wed Jul 30 13:36:10 2025 daemon.notice hostapd: ap_sta_wpa_get_keyid: Looking for PSK match for xx:xx:xx:xx:xx:xx
Wed Jul 30 13:36:10 2025 daemon.notice hostapd: Found matching PSK (keyid='', vlan_id=10)

A small update: i moved the patch from folder std one folder back, now it is compiled and I receive the log messages from the patch.

This means my past observations where still without patch, it doesn't seem to disconnect anymore. :+1:

Wed Jul 30 12:29:42 2025 daemon.notice hostapd: PMK - hexdump(len=32): [REMOVED]
Wed Jul 30 12:29:42 2025 daemon.notice hostapd: Found matching PSK (keyid='', vlan_id=0)

Wed Jul 30 12:29:42 2025 daemon.debug hostapd: phy1-ap0: STA xx:xx:xx:xx:xx WPA:
FT authentication already completed - do not start 4-way handshake

Are you not using VLANs?

I do, but the client (a android phone) currently not, but since most of my devices are devices which don't move regulary they don't trigger a FT event :slight_smile:

Is it working well for you still? Do you still believe this code might be crashing something and causing the AP/radio to cycle?

It works fine :+1:

I second this, been 5 days with like 8-10 devices (mixed of laptops, smartphones and ipads roaming around) zero issues so far, everything is FT-ing without vlan hopping to default vlan. Configuration on 802.11r are all default configuration, didt even bother adjusting that reassociation value and our iphone and ipads are roaming just fine. Aside from actually me using band steering on the SSID since I dont really want to bother switching between 2.4 and 5ghz band manually all the time.

@remlei && @xize Thanks for the feedback! I’m very pleased to hear this patch has been doing its intended job, and not crashing people’s stuff while doing it :wink:

I am going to see about submitting it upstream, but I’ve got to refresh my memory on all the requirements to do so. If, by some chance, I need some help with testing confirmations and/or sign-offs, would you be willing to vouch for it?

What I was observing was a drop out from lose of power :slight_smile:, I changed not so long ago to PoE with one AP, but everytime I used speedtest it did just enough to restart the wifi but not the router.

The patch has no issues :+1:, I gonna try to overload this unifi PoE switch so it will not happen again on the AP when testing.

to be honest im not really familiar on this kind of stuff but ill try my best to help.

Excellent work and great write-up!

I used your example to create a mixed AP setup [WPA2-EAP and WPA2-PSK] because some client devices here do not support 802.1x. Both the external RADIUS and hostapd_wpa_psk file happily coexist.

The only thing I had to look up was use of hostapd_cli. The suggested command

fails in my mixed authentication scenario. Using

hostapd_cli -i wl-ap[N] reload_wpa_psk

did the trick.

@xize && @remlei, et.al.

I have a second revision of the patch I originally posted here:

The second revision is active at that same gist URL for anyone willing to try it. My hope still is to get it upstreamed, but I want to make sure it’s good and solid first.

Thanks in advance to anyone willing to give it a go!

Hi, I have done a small test between my dumbap and back to my main router.

I know that my dumbap has a small issue getting the default gateway if interfaces restart it sometimes uses the wrong one (theres no wan, all in lan and only the lan interface having default gateway checked, but somehow doesn’t make it default route), usually it should work, but to confirm I also walked back downstairs and ensured in wifiman I changed.

Now I keep hopping to the default vlan:

Fri Aug 22 18:34:46 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx WPA: sending 1/4 msg of 4-Way Handshake
Fri Aug 22 18:34:46 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:6xx:xx:xx WPA: received EAPOL-Key frame (2/4 Pairwise)
Fri Aug 22 18:34:46 2025 daemon.notice hostapd: Assigned VLAN ID 90 from wpa_psk_file to f6:a2:10:xx:xx:xx
Fri Aug 22 18:34:46 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx IEEE 802.11: binding station to interface 'phy1-ap0-aya'
Fri Aug 22 18:34:46 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx WPA: sending 3/4 msg of 4-Way Handshake
Fri Aug 22 18:34:46 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx WPA: received EAPOL-Key frame (4/4 Pairwise)
Fri Aug 22 18:34:46 2025 daemon.notice hostapd: phy1-ap0: AP-STA-CONNECTED f6:a2:10:xx:xx:xx auth_alg=open
Fri Aug 22 18:34:46 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx IEEE 802.1X: authorizing port
Fri Aug 22 18:34:46 2025 daemon.info hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx RADIUS: starting accounting session 2BA883C95B561D54
Fri Aug 22 18:34:46 2025 daemon.info hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx WPA: pairwise key handshake completed (RSN)
Fri Aug 22 18:34:46 2025 daemon.notice hostapd: phy1-ap0: EAPOL-4WAY-HS-COMPLETED f6:a2:10:xx:xx:xx
Fri Aug 22 18:40:15 2025 daemon.notice hostapd: phy1-ap0: AP-STA-DISCONNECTED f6:a2:10:xx:xx:xx
Fri Aug 22 18:40:15 2025 daemon.err hostapd: phy1-ap0: nl80211: kernel reports: key addition failed
Fri Aug 22 18:40:15 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx IEEE 802.11: binding station to interface 'phy1-ap0'
Fri Aug 22 18:40:15 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx IEEE 802.11: authentication OK (FT)
Fri Aug 22 18:40:15 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx MLME: MLME-AUTHENTICATE.indication(f6:a2:10:xx:xx:xx, FT)
Fri Aug 22 18:40:15 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx IEEE 802.11: association OK (aid 2)
Fri Aug 22 18:40:15 2025 daemon.info hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx IEEE 802.11: associated (aid 2)
Fri Aug 22 18:40:15 2025 daemon.notice hostapd: phy1-ap0: AP-STA-CONNECTED f6:a2:10:xx:xx:xx auth_alg=ft
Fri Aug 22 18:40:15 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx MLME: MLME-REASSOCIATE.indication(f6:a2:10:xx:xx:xx)
Fri Aug 22 18:40:15 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx IEEE 802.11: binding station to interface 'phy1-ap0'
Fri Aug 22 18:40:15 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx WPA: event 6 notification
Fri Aug 22 18:40:15 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx WPA: FT authentication already completed - do not start 4-way handshake

Does the patch target recent main branch commit until 3b54f82?

Hmmm, I see from your logs what you’re saying. You definitely aren’t landing on the right VLAN after FT.

The latest patch was built against OpenWrt main @44a5d1c. Specifically, I built it against hostapd:

PKG_NAME:=hostapd
PKG_RELEASE:=1

PKG_SOURCE_URL:=https://w1.fi/hostap.git
PKG_SOURCE_PROTO:=git
PKG_SOURCE_DATE:=2025-06 27
PKG_SOURCE_VERSION:=ea08700a762f13c415e319993720bed83ad9aa2c

Oddly enough, this latest patch seems to be working perfectly for me, so I’m a bit confused as to what the difference could be between our configs/environments.

Would you mind trying this so we can get more verbosity from your log? (changed MSG_DEBUG to MSG_INFO)

--- a/src/ap/ieee802_11.c
+++ b/src/ap/ieee802_11.c
@@ -38,6 +38,7 @@
 #include "sta_info.h"
 #include "ieee802_1x.h"
 #include "wpa_auth.h"
+#include "wpa_auth_i.h"
 #include "pmksa_cache_auth.h"
 #include "wmm.h"
 #include "ap_list.h"
@@ -466,6 +467,29 @@ static void handle_auth_ft_finish(void *
 	if (status != WLAN_STATUS_SUCCESS)
 		return;

+	/* Reapply per-PSK VLAN mapping for FT roam (dynamic VLAN only) */
+	if (hapd->conf->ssid.dynamic_vlan && sta && sta->wpa_sm) {
+		const u8 *pmk;
+		int pmk_len;
+		struct hostapd_wpa_psk *psk;
+
+		pmk = wpa_auth_get_pmk(sta->wpa_sm, &pmk_len);
+		if (pmk && pmk_len == PMK_LEN) {
+			for (psk = hapd->conf->ssid.wpa_psk; psk; psk = psk->next) {
+				if (os_memcmp(pmk, psk->psk, PMK_LEN) == 0) {
+					if (psk->vlan_id && psk->vlan_id != sta->vlan_id) {
+						wpa_printf(MSG_INFO,
+							   "FT: PSK VLAN reassignment " MACSTR " %d->%d",
+							   MAC2STR(sta->addr),
+							   sta->vlan_id, psk->vlan_id);
+						sta->vlan_id = psk->vlan_id;
+						ap_sta_set_vlan(hapd, sta, sta->vlan_desc);
+					}
+					break;
+				}
+			}
+		}
+	}
 	hostapd_logger(hapd, dst, HOSTAPD_MODULE_IEEE80211,
 		       HOSTAPD_LEVEL_DEBUG, "authentication OK (FT)");
 	sta->flags |= WLAN_STA_AUTH;

FWIW, I am going to rebuild with the same modified patch I gave you so we can be apples-to-apples in comparing what is going on. :slight_smile:

I have tried it with the new patch but I don’t see anything special, just the same as my first post.

Sat Aug 23 00:14:37 2025 daemon.err hostapd: phy1-ap0: nl80211: kernel reports: key addition failed
Sat Aug 23 00:14:37 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx IEEE 802.11: binding station to interface 'phy1-ap0'
Sat Aug 23 00:14:37 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx IEEE 802.11: authentication OK (FT)
Sat Aug 23 00:14:37 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx MLME: MLME-AUTHENTICATE.indication(f6:a2:10:xx:xx:xx, FT)
Sat Aug 23 00:14:37 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx IEEE 802.11: association OK (aid 1)
Sat Aug 23 00:14:37 2025 daemon.info hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx IEEE 802.11: associated (aid 1)
Sat Aug 23 00:14:37 2025 daemon.notice hostapd: phy1-ap0: AP-STA-CONNECTED f6:a2:10:xx:xx:xx auth_alg=ft
Sat Aug 23 00:14:37 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx MLME: MLME-REASSOCIATE.indication(f6:a2:10:xx:xx:xx)
Sat Aug 23 00:14:37 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx IEEE 802.11: binding station to interface 'phy1-ap0'
Sat Aug 23 00:14:37 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx WPA: event 6 notification
Sat Aug 23 00:14:37 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx WPA: FT authentication already completed - do not start 4-way handshake
Sat Aug 23 00:18:41 2025 authpriv.info dispatcher.uc: luci: accepted login on / for root from 10.39.95.2

Edit: and when I try to disable wifi and connect to this same ap:

Sat Aug 23 00:27:57 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx MLME: MLME-ASSOCIATE.indication(f6:a2:10:xx:xx:xx)
Sat Aug 23 00:27:57 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx MLME: MLME-DELETEKEYS.request(f6:a2:10:xx:xx:xx)
Sat Aug 23 00:27:57 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx IEEE 802.11: binding station to interface 'phy1-ap0'
Sat Aug 23 00:27:57 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx WPA: event 1 notification
Sat Aug 23 00:27:57 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx WPA: start authentication
Sat Aug 23 00:27:57 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx IEEE 802.1X: unauthorizing port
Sat Aug 23 00:27:57 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx WPA: sending 1/4 msg of 4-Way Handshake
Sat Aug 23 00:27:57 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx WPA: received EAPOL-Key frame (2/4 Pairwise)
Sat Aug 23 00:27:57 2025 daemon.notice hostapd: Assigned VLAN ID 90 from wpa_psk_file to f6:a2:10:xx:xx:xx
Sat Aug 23 00:27:57 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx IEEE 802.11: binding station to interface 'phy1-ap0-aya'
Sat Aug 23 00:27:57 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx WPA: sending 3/4 msg of 4-Way Handshake
Sat Aug 23 00:27:57 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx WPA: received EAPOL-Key frame (4/4 Pairwise)
Sat Aug 23 00:27:57 2025 daemon.notice hostapd: phy1-ap0: AP-STA-CONNECTED f6:a2:10:xx:xx:xx auth_alg=open
Sat Aug 23 00:27:57 2025 daemon.debug hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx IEEE 802.1X: authorizing port
Sat Aug 23 00:27:57 2025 daemon.info hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx RADIUS: starting accounting session 139787A7FE090E33
Sat Aug 23 00:27:57 2025 daemon.info hostapd: phy1-ap0: STA f6:a2:10:xx:xx:xx WPA: pairwise key handshake completed (RSN)
Sat Aug 23 00:27:57 2025 daemon.notice hostapd: phy1-ap0: EAPOL-4WAY-HS-COMPLETED f6:a2:10:xx:xx:xx

Is there a way I can confirm the patch is applied?

Yeah, it looks to me like the patch isn’t getting built in properly. With the MSG_INFO patch I posted earlier, you should see at least a handful of messages like this:

root@AP-Office:~# logread -e hostapd | grep reassignment
Fri Aug 22 19:19:44 2025 daemon.notice hostapd: FT: PSK VLAN reassignment 98:0d:af:xx:yy:zz 0->36
Fri Aug 22 19:44:04 2025 daemon.notice hostapd: FT: PSK VLAN reassignment 98:0d:af:xx:yy:zz 0->36
Fri Aug 22 19:44:28 2025 daemon.notice hostapd: FT: PSK VLAN reassignment 98:0d:af:xx:yy:zz 0->36
Fri Aug 22 19:56:24 2025 daemon.notice hostapd: FT: PSK VLAN reassignment d6:b9:e8:xx:yy:zz 0->9
Fri Aug 22 20:36:30 2025 daemon.notice hostapd: FT: PSK VLAN reassignment 9e:8d:8d:xx:yy:zz 0->36
Fri Aug 22 20:44:47 2025 daemon.notice hostapd: FT: PSK VLAN reassignment 9c:64:8b:xx:yy:zz 0->18
Fri Aug 22 20:52:56 2025 daemon.notice hostapd: FT: PSK VLAN reassignment 70:31:7f:xx:yy:zz 0->18
Fri Aug 22 20:54:00 2025 daemon.notice hostapd: FT: PSK VLAN reassignment 9c:64:8b:xx:yy:zz 0->18

When you build your image, do you have it output a build log? e.g. make -j$((1+nproc)) V=s 2>&1 | tee build.log | grep -i -E "^make.*(error|[12345]...Entering dir)"

If so, you can check if the patch is getting applied like so:

user@f0f7b4de7881:~/openwrt$ grep -A3 -E '^Applying.+\/999-ft-.+' build.log
Applying /home/user/openwrt/package/network/services/hostapd/patches/std/999-ft-psk-vlan-reassignment.patch using plaintext:

patching file src/ap/ieee802_11.c