[v3,10/12] wpa_supplicant: support Multi-AP backhaul STA onboarding

Message ID 20181205102401.17810-11-arnout@mind.be
State Changes Requested
Headers show
Series
  • [v3,01/12] hostapd: Add Multi-AP protocol support
Related show

Commit Message

Arnout Vandecappelle Dec. 5, 2018, 10:24 a.m.
From: Davina Lu <ylu@quantenna.com>

The Wi-Fi Alliance Multi-AP Specification v1.0 allows onboarding of a
backhaul STA through WPS. To enable this, the backhaul STA needs to add
a Multi-AP IE to the WFA vendor extension element in the WSC M1 message
that indicates it supports the Multi-AP backhaul STA role. The registrar
(if it support Multi-AP onboarding) will respond to that with a WSC M8
message that also contains the Multi-AP IE, and that contains the
credentials for the backhaul SSID (which may be different from the SSID
on which WPS is performed).

Introduce a new, global configuration option multi_ap_backhaul_sta to
enable this feature. It has to be separate from the per-SSID
multi_ap_backhaul_sta option since no SSID exists yet when WPS is
performed. multi_ap_backhaul_sta is set to 1 in the automatically
created SSID. Thus, if the AP does not support Multi-AP, association
will fail and WPS will be terminated.

Only wps_pbc is supported.

Extend wps_build_wfa_ext() with an extra argument to add the additional
subelement to a WPS message, and use this when building M1. Note that we
don't reuse the existing MULTI_AP_SUB_ELEM_TYPE definition here, but
rather define a new WFA_ELEM_MULTI_AP, to make sure the enum of WFA
subelement types remains complete.

Signed-off-by: Davina Lu <ylu@quantenna.com>
Signed-off-by: Igor Mitsyanko <igor.mitsyanko.os@quantenna.com>
Signed-off-by: Arnout Vandecappelle (Essensium/Mind) <arnout@mind.be>
---
 src/p2p/p2p_build.c                |  2 +-
 src/wps/wps.c                      |  6 +++---
 src/wps/wps.h                      |  6 ++++++
 src/wps/wps_attr_build.c           | 10 +++++++++-
 src/wps/wps_common.c               | 16 ++++++++--------
 src/wps/wps_defs.h                 |  3 ++-
 src/wps/wps_enrollee.c             | 14 +++++++++-----
 src/wps/wps_er.c                   |  4 ++--
 src/wps/wps_i.h                    |  3 ++-
 src/wps/wps_registrar.c            | 14 +++++++-------
 src/wps/wps_upnp.c                 |  2 +-
 wpa_supplicant/config.c            |  1 +
 wpa_supplicant/config.h            |  2 ++
 wpa_supplicant/config_file.c       |  4 ++++
 wpa_supplicant/wpa_supplicant.conf |  5 +++++
 wpa_supplicant/wps_supplicant.c    | 13 ++++++++++++-
 16 files changed, 74 insertions(+), 31 deletions(-)

Comments

Jouni Malinen Dec. 20, 2018, 10:19 a.m. | #1
On Wed, Dec 05, 2018 at 11:24:00AM +0100, Arnout Vandecappelle (Essensium/Mind) wrote:
> Introduce a new, global configuration option multi_ap_backhaul_sta to
> enable this feature. It has to be separate from the per-SSID
> multi_ap_backhaul_sta option since no SSID exists yet when WPS is
> performed. multi_ap_backhaul_sta is set to 1 in the automatically
> created SSID. Thus, if the AP does not support Multi-AP, association
> will fail and WPS will be terminated.

Why would this configuration parameter be needed? I thought I already
suggested that it would be better to indicate this as a parameter
specific to each instance of WPS provisioning, i.e., as a new optional
argument to the WPS_PBC control interface command. In other words,
wpas_wps_start_pbc() should get this from the caller and
wpa_supplicant_wps_cred() should get this from a new field added into
struct wps_credential.
Arnout Vandecappelle Jan. 7, 2019, 2:56 p.m. | #2
On 20/12/2018 11:19, Jouni Malinen wrote:
> On Wed, Dec 05, 2018 at 11:24:00AM +0100, Arnout Vandecappelle (Essensium/Mind) wrote:
>> Introduce a new, global configuration option multi_ap_backhaul_sta to
>> enable this feature. It has to be separate from the per-SSID
>> multi_ap_backhaul_sta option since no SSID exists yet when WPS is
>> performed. multi_ap_backhaul_sta is set to 1 in the automatically
>> created SSID. Thus, if the AP does not support Multi-AP, association
>> will fail and WPS will be terminated.
> 
> Why would this configuration parameter be needed? I thought I already
> suggested

 I either missed that or misinterpreted it.

> that it would be better to indicate this as a parameter
> specific to each instance of WPS provisioning, i.e., as a new optional
> argument to the WPS_PBC control interface command.

 That makes sense.

> In other words,
> wpas_wps_start_pbc() should get this from the caller

 So an extra argument to wpas_wps_start_pbc, right?

 I guess then it should be added also to:

* wpa_cli;
* new D-Bus interface;
* (for OpenWrt only: ubus interface);

but not to:

* old D-Bus interfae;
* p2p enrollment.

 And I'm not sure what to do with EVENT_WPS_BUTTON_PUSHED. But the only caller
of that is apparently some atheros driver.

> and
> wpa_supplicant_wps_cred() should get this from a new field added into
> struct wps_credential. 

 Wouldn't struct wps_data be more appropriate? It's not very clear to me where
that gets initialized though... But actually, the same goes for wps_credential.
Is it possible that both of these only get initialized after
wpas_wps_start_pbc() has already returned?

 Maybe the best way to pass it on would be through the phase1 config string?
Although it feels like a bit of a hack to me, it looks like that is the way to
pass down context information into EAP, right?

 Regards,
 Arnout
Arnout Vandecappelle Jan. 8, 2019, 10:04 a.m. | #3
Hi Jouni,

 I forgot to ask in my earlier reply...

On 05/12/2018 11:24, Arnout Vandecappelle (Essensium/Mind) wrote:
> diff --git a/src/p2p/p2p_build.c b/src/p2p/p2p_build.c
> index 2882c6ad0..63eb2e84c 100644
> --- a/src/p2p/p2p_build.c
> +++ b/src/p2p/p2p_build.c
> @@ -802,7 +802,7 @@ int p2p_build_wps_ie(struct p2p_data *p2p, struct wpabuf *buf, int pw_id,
>  		wpabuf_put_be16(buf, p2p->cfg->config_methods);
>  	}
>  
> -	if (wps_build_wfa_ext(buf, 0, NULL, 0) < 0)
> +	if (wps_build_wfa_ext(buf, 0, NULL, 0, 0) < 0)

 These small changes make the patch very big and perhaps hard to review. Would
you like me to split off the additional argument to wps_build_wfa_ext as a
separate patch?

 Regards,
 Arnout


>  		return -1;
>  
>  	if (all_attr && p2p->cfg->num_sec_dev_types) {
Jouni Malinen Jan. 8, 2019, 10:56 a.m. | #4
On Tue, Jan 08, 2019 at 11:04:42AM +0100, Arnout Vandecappelle wrote:
> On 05/12/2018 11:24, Arnout Vandecappelle (Essensium/Mind) wrote:
> > diff --git a/src/p2p/p2p_build.c b/src/p2p/p2p_build.c
> > -	if (wps_build_wfa_ext(buf, 0, NULL, 0) < 0)
> > +	if (wps_build_wfa_ext(buf, 0, NULL, 0, 0) < 0)
> 
>  These small changes make the patch very big and perhaps hard to review. Would
> you like me to split off the additional argument to wps_build_wfa_ext as a
> separate patch?

That would certainly make it easier to review the real functional
changes, so yes, that's the preferred approach for this type of
refactoring cases needed for new functionality.
Jouni Malinen Jan. 8, 2019, 11:04 a.m. | #5
On Mon, Jan 07, 2019 at 03:56:24PM +0100, Arnout Vandecappelle wrote:
> On 20/12/2018 11:19, Jouni Malinen wrote:
> > that it would be better to indicate this as a parameter
> > specific to each instance of WPS provisioning, i.e., as a new optional
> > argument to the WPS_PBC control interface command.
> 
>  That makes sense.
> 
> > In other words,
> > wpas_wps_start_pbc() should get this from the caller
> 
>  So an extra argument to wpas_wps_start_pbc, right?

Yes.

>  I guess then it should be added also to:
> 
> * wpa_cli;

wpa_cli is already accepting arbitrary number of arguments to WPS_PBC.

> * new D-Bus interface;

If we expect this functionality to be used over D-Bus, sure.

> * (for OpenWrt only: ubus interface);

Sounds reasonable.

> but not to:
> 
> * old D-Bus interfae;
> * p2p enrollment.

Agreed.

>  And I'm not sure what to do with EVENT_WPS_BUTTON_PUSHED. But the only caller
> of that is apparently some atheros driver.

I'd ignore that for this, i.e., that wpas_wps_start_pbc() call should
just note that there was no request for Multi-AP.

> > and
> > wpa_supplicant_wps_cred() should get this from a new field added into
> > struct wps_credential. 
> 
>  Wouldn't struct wps_data be more appropriate? It's not very clear to me where
> that gets initialized though... But actually, the same goes for wps_credential.

wpa_supplicant_wps_cred() gets called with information parsed from M8,
so this should be filled based on what the Registrar told us. I don't
see why struct wps_data would be involved in that part (i.e., processing
of a received Credential).

> Is it possible that both of these only get initialized after
> wpas_wps_start_pbc() has already returned?

Yes and not only possible, but that will happen in practice every time.

>  Maybe the best way to pass it on would be through the phase1 config string?
> Although it feels like a bit of a hack to me, it looks like that is the way to
> pass down context information into EAP, right?

So this is a bit different direction, i.e., configuration of the local
WPS Enrollee behavior for the request information while the comment
about wpa_supplicant_wps_cred() was on how to process the response
information. Anyway, yes, this is a bit complex due to the way the EAP
peer implementation is kept as a separate module between core
wpa_supplicant and WPS module. I did not consider details for that part,
but yes, a new parameter within the phase1 config string seems to make
most sense for getting information about the new optional WPS_PBC
argument to the actual WPS Enrollee implementation which needs to
generate the WSC messages with some additional information.
Arnout Vandecappelle Jan. 8, 2019, 3:05 p.m. | #6
On 08/01/2019 12:04, Jouni Malinen wrote:
> On Mon, Jan 07, 2019 at 03:56:24PM +0100, Arnout Vandecappelle wrote:
>> On 20/12/2018 11:19, Jouni Malinen wrote:
[snip]
>>> wpa_supplicant_wps_cred() should get this from a new field added into
>>> struct wps_credential. 
>>
>>  Wouldn't struct wps_data be more appropriate? It's not very clear to me where
>> that gets initialized though... But actually, the same goes for wps_credential.
> 
> wpa_supplicant_wps_cred() gets called with information parsed from M8,
> so this should be filled based on what the Registrar told us. 

 Unfortunately, the registrar doesn't tell us anything. The M8 does NOT contain
the Multi-AP IE.

 However, wpa_supplicant_wps_cred has access to the current_ssid which has the
multi_ap setting. In fact, this bit of the patch:

+	if (wpa_s->conf->multi_ap_backhaul_sta)
+		ssid->multi_ap_backhaul_sta = 1;
+	else
+		ssid->multi_ap_backhaul_sta = 0;

is probably (I should check) redundant because the multi_ap_backhaul_sta bit is
already set in wpas_wps_start_pbc().

 I notice now, however, that the multi_ap_backhaul_sta option is missing from
wpa_config_write_network().


> I don't
> see why struct wps_data would be involved in that part (i.e., processing
> of a received Credential).
> 
>> Is it possible that both of these only get initialized after
>> wpas_wps_start_pbc() has already returned?
> 
> Yes and not only possible, but that will happen in practice every time.
> 
>>  Maybe the best way to pass it on would be through the phase1 config string?
>> Although it feels like a bit of a hack to me, it looks like that is the way to
>> pass down context information into EAP, right?
> 
> So this is a bit different direction, i.e., configuration of the local
> WPS Enrollee behavior for the request information while the comment
> about wpa_supplicant_wps_cred() was on how to process the response
> information.

 Indeed I was mixing those two.


> Anyway, yes, this is a bit complex due to the way the EAP
> peer implementation is kept as a separate module between core
> wpa_supplicant and WPS module. I did not consider details for that part,
> but yes, a new parameter within the phase1 config string seems to make
> most sense for getting information about the new optional WPS_PBC
> argument to the actual WPS Enrollee implementation which needs to
> generate the WSC messages with some additional information.

 OK, thanks.

 Regards,
 Arnout
Jouni Malinen Jan. 8, 2019, 11:34 p.m. | #7
On Tue, Jan 08, 2019 at 04:05:35PM +0100, Arnout Vandecappelle wrote:
> On 08/01/2019 12:04, Jouni Malinen wrote:
> > wpa_supplicant_wps_cred() gets called with information parsed from M8,
> > so this should be filled based on what the Registrar told us. 
> 
>  Unfortunately, the registrar doesn't tell us anything. The M8 does NOT contain
> the Multi-AP IE.

That sounds like a bad protocol design to me..

>  However, wpa_supplicant_wps_cred has access to the current_ssid which has the
> multi_ap setting.

I'd be a bit careful with wpa_s->current_ssid there.. It does not
actually have to be set in some WPS use cases. Anyway, for the Multi-AP
PBC case, I think this will in practice always be pointing to the
network profile where the extra parameter from WPS_PBC would be present.
Anyway, I would also be fine in adding a new flag in struct
wpa_supplicant for tracking this type of pending state for WPS, if
required.
Daniel Golle Jan. 18, 2019, 12:28 a.m. | #8
Hi Jouni,

On Tue, Jan 08, 2019 at 01:04:37PM +0200, Jouni Malinen wrote:
> On Mon, Jan 07, 2019 at 03:56:24PM +0100, Arnout Vandecappelle wrote:
> > On 20/12/2018 11:19, Jouni Malinen wrote:
> > > that it would be better to indicate this as a parameter
> > > specific to each instance of WPS provisioning, i.e., as a new optional
> > > argument to the WPS_PBC control interface command.
> > 
> >  That makes sense.

I'm sure you have good reasons for requesting it do be done in that
way. It'd be great if you can elaborate on those reasons, see below
for the cause of my confusion with that.

> > 
> > > In other words,
> > > wpas_wps_start_pbc() should get this from the caller
> > 
> >  So an extra argument to wpas_wps_start_pbc, right?
> 
> Yes.

I've been going that road for a bit now and more and more come to
the conclusion that setting a global variable (just like other WPS-
related global configurations) would be much less complex.
Also note that a station is most likely really either a regular station
or multi-ap backhaul station, also considering the context: multi-ap
interface in 4-addr-mode sitting in a bridge while regular station
being a whole different use-case.

Coming from OpenWrt, whether the interface is a multiap backhaul sta or
a regular sta is known at the time of wpa_supplicant.conf being
generated. Sure, at this point we could store that somewhere next to
wpa_supplicant_$ifname.conf and when pushing the WPS butten checking
that file to decide whether multi_ap=1 is being passed along the call
of wps_start_pbc or not. A more dynamic decission doesn't make much
sense, because higher layers are also already set according to whether
the STA interface is part of a bridge or not.



> 
> >  I guess then it should be added also to:
> > 
> > * wpa_cli;
> 
> wpa_cli is already accepting arbitrary number of arguments to WPS_PBC.
> 
> > * new D-Bus interface;
> 
> If we expect this functionality to be used over D-Bus, sure.
> 
> > * (for OpenWrt only: ubus interface);
> 
> Sounds reasonable.
> 
> > but not to:
> > 
> > * old D-Bus interfae;
> > * p2p enrollment.
> 
> Agreed.
> 
> >  And I'm not sure what to do with EVENT_WPS_BUTTON_PUSHED. But the only caller
> > of that is apparently some atheros driver.
> 
> I'd ignore that for this, i.e., that wpas_wps_start_pbc() call should
> just note that there was no request for Multi-AP.
> 
> > > and
> > > wpa_supplicant_wps_cred() should get this from a new field added into
> > > struct wps_credential. 
> > 
> >  Wouldn't struct wps_data be more appropriate? It's not very clear to me where
> > that gets initialized though... But actually, the same goes for wps_credential.
> 
> wpa_supplicant_wps_cred() gets called with information parsed from M8,
> so this should be filled based on what the Registrar told us. I don't
> see why struct wps_data would be involved in that part (i.e., processing
> of a received Credential).
> 
> > Is it possible that both of these only get initialized after
> > wpas_wps_start_pbc() has already returned?
> 
> Yes and not only possible, but that will happen in practice every time.
> 
> >  Maybe the best way to pass it on would be through the phase1 config string?
> > Although it feels like a bit of a hack to me, it looks like that is the way to
> > pass down context information into EAP, right?
> 
> So this is a bit different direction, i.e., configuration of the local
> WPS Enrollee behavior for the request information while the comment
> about wpa_supplicant_wps_cred() was on how to process the response
> information. Anyway, yes, this is a bit complex due to the way the EAP
> peer implementation is kept as a separate module between core
> wpa_supplicant and WPS module. I did not consider details for that part,
> but yes, a new parameter within the phase1 config string seems to make
> most sense for getting information about the new optional WPS_PBC
> argument to the actual WPS Enrollee implementation which needs to
> generate the WSC messages with some additional information.

That kinda adds to it complexity wise...


Cheers


Daniel
> 
> -- 
> Jouni Malinen                                            PGP id EFC895FA
> 
> _______________________________________________
> Hostap mailing list
> Hostap@lists.infradead.org
> http://lists.infradead.org/mailman/listinfo/hostap
Jouni Malinen Feb. 5, 2019, 11:13 p.m. | #9
On Fri, Jan 18, 2019 at 01:28:43AM +0100, Daniel Golle wrote:
> Hi Jouni,
> 
> On Tue, Jan 08, 2019 at 01:04:37PM +0200, Jouni Malinen wrote:
> > On Mon, Jan 07, 2019 at 03:56:24PM +0100, Arnout Vandecappelle wrote:
> > > On 20/12/2018 11:19, Jouni Malinen wrote:
> > > > that it would be better to indicate this as a parameter
> > > > specific to each instance of WPS provisioning, i.e., as a new optional
> > > > argument to the WPS_PBC control interface command.
> > > 
> > >  That makes sense.
> 
> I'm sure you have good reasons for requesting it do be done in that
> way. It'd be great if you can elaborate on those reasons, see below
> for the cause of my confusion with that.

This information (whether to request credentials for Multi-AP use case
or for something else) is associated with a specific instance of WPS
protocol exchange; not to global state of a WPS Enrollee device.

> > > > In other words,
> > > > wpas_wps_start_pbc() should get this from the caller
> > > 
> > >  So an extra argument to wpas_wps_start_pbc, right?
> > 
> > Yes.
> 
> I've been going that road for a bit now and more and more come to
> the conclusion that setting a global variable (just like other WPS-
> related global configurations) would be much less complex.

Sure, it might be less complex, but it would be less correct as well. It
should also not be assumed that there is only a single WPS exchange in
process at any point in time. The most common example of this is the use
of WPS ER in wpa_supplicant, but that would obviously be Registrar
instead of two Enrollees, so that might not hit this specific case.
Anyway, using a global variable for setting a parameter for a single WPS
protocol instance does not sound correct.

> Also note that a station is most likely really either a regular station
> or multi-ap backhaul station, also considering the context: multi-ap
> interface in 4-addr-mode sitting in a bridge while regular station
> being a whole different use-case.

Probably true as well, but still, this is just trying to justify taking
a shortcut over a cleaner design.

> Coming from OpenWrt, whether the interface is a multiap backhaul sta or
> a regular sta is known at the time of wpa_supplicant.conf being
> generated. Sure, at this point we could store that somewhere next to
> wpa_supplicant_$ifname.conf and when pushing the WPS butten checking
> that file to decide whether multi_ap=1 is being passed along the call
> of wps_start_pbc or not. A more dynamic decission doesn't make much
> sense, because higher layers are also already set according to whether
> the STA interface is part of a bridge or not.

I don't think wpa_supplicant implementation design should be based on
assumptions that OpenWrt or some other user of wpa_supplicant has such
knowledge of the device using WPS Enrollee functionality only for a
specific purpose.

Patch

diff --git a/src/p2p/p2p_build.c b/src/p2p/p2p_build.c
index 2882c6ad0..63eb2e84c 100644
--- a/src/p2p/p2p_build.c
+++ b/src/p2p/p2p_build.c
@@ -802,7 +802,7 @@  int p2p_build_wps_ie(struct p2p_data *p2p, struct wpabuf *buf, int pw_id,
 		wpabuf_put_be16(buf, p2p->cfg->config_methods);
 	}
 
-	if (wps_build_wfa_ext(buf, 0, NULL, 0) < 0)
+	if (wps_build_wfa_ext(buf, 0, NULL, 0, 0) < 0)
 		return -1;
 
 	if (all_attr && p2p->cfg->num_sec_dev_types) {
diff --git a/src/wps/wps.c b/src/wps/wps.c
index 8d228270f..c162e793d 100644
--- a/src/wps/wps.c
+++ b/src/wps/wps.c
@@ -430,7 +430,7 @@  struct wpabuf * wps_build_assoc_req_ie(enum wps_request_type req_type)
 
 	if (wps_build_version(ie) ||
 	    wps_build_req_type(ie, req_type) ||
-	    wps_build_wfa_ext(ie, 0, NULL, 0)) {
+	    wps_build_wfa_ext(ie, 0, NULL, 0, 0)) {
 		wpabuf_free(ie);
 		return NULL;
 	}
@@ -464,7 +464,7 @@  struct wpabuf * wps_build_assoc_resp_ie(void)
 
 	if (wps_build_version(ie) ||
 	    wps_build_resp_type(ie, WPS_RESP_AP) ||
-	    wps_build_wfa_ext(ie, 0, NULL, 0)) {
+	    wps_build_wfa_ext(ie, 0, NULL, 0, 0)) {
 		wpabuf_free(ie);
 		return NULL;
 	}
@@ -516,7 +516,7 @@  struct wpabuf * wps_build_probe_req_ie(u16 pw_id, struct wps_device_data *dev,
 	    wps_build_model_name(dev, ie) ||
 	    wps_build_model_number(dev, ie) ||
 	    wps_build_dev_name(dev, ie) ||
-	    wps_build_wfa_ext(ie, req_type == WPS_REQ_ENROLLEE, NULL, 0) ||
+	    wps_build_wfa_ext(ie, req_type == WPS_REQ_ENROLLEE, NULL, 0, 0) ||
 	    wps_build_req_dev_type(dev, ie, num_req_dev_types, req_dev_types)
 	    ||
 	    wps_build_secondary_dev_type(dev, ie)
diff --git a/src/wps/wps.h b/src/wps/wps.h
index 2505d2d9f..129f27ab4 100644
--- a/src/wps/wps.h
+++ b/src/wps/wps.h
@@ -612,6 +612,12 @@  struct wps_context {
 	 */
 	int ap_setup_locked;
 
+	/**
+	 * multi_ap_backhaul_sta - Whether this is a Multi-AP backhaul STA
+	 * enrollee
+	 */
+	int multi_ap_backhaul_sta;
+
 	/**
 	 * uuid - Own UUID
 	 */
diff --git a/src/wps/wps_attr_build.c b/src/wps/wps_attr_build.c
index 770f5e90c..22b33e8c3 100644
--- a/src/wps/wps_attr_build.c
+++ b/src/wps/wps_attr_build.c
@@ -203,7 +203,8 @@  int wps_build_version(struct wpabuf *msg)
 
 
 int wps_build_wfa_ext(struct wpabuf *msg, int req_to_enroll,
-		      const u8 *auth_macs, size_t auth_macs_count)
+		      const u8 *auth_macs, size_t auth_macs_count,
+		      u8 multi_ap_subelem)
 {
 	u8 *len;
 
@@ -244,6 +245,13 @@  int wps_build_wfa_ext(struct wpabuf *msg, int req_to_enroll,
 				   MAC2STR(&auth_macs[i * ETH_ALEN]));
 	}
 
+	if (multi_ap_subelem) {
+		wpa_printf(MSG_DEBUG, "WPS:  * Multi-AP (0x%x)", multi_ap_subelem);
+		wpabuf_put_u8(msg, WFA_ELEM_MULTI_AP);
+		wpabuf_put_u8(msg, 1); /* length */
+		wpabuf_put_u8(msg, multi_ap_subelem);
+	}
+
 	WPA_PUT_BE16(len, (u8 *) wpabuf_put(msg, 0) - len - 2);
 
 #ifdef CONFIG_WPS_TESTING
diff --git a/src/wps/wps_common.c b/src/wps/wps_common.c
index bcae1ba58..747dc4710 100644
--- a/src/wps/wps_common.c
+++ b/src/wps/wps_common.c
@@ -374,7 +374,7 @@  struct wpabuf * wps_get_oob_cred(struct wps_context *wps, int rf_band,
 	    (rf_band && wps_build_rf_bands_attr(plain, rf_band)) ||
 	    (channel && wps_build_ap_channel(plain, channel)) ||
 	    wps_build_mac_addr(plain, wps->dev.mac_addr) ||
-	    wps_build_wfa_ext(plain, 0, NULL, 0)) {
+	    wps_build_wfa_ext(plain, 0, NULL, 0, 0)) {
 		os_free(data.new_psk);
 		wpabuf_clear_free(plain);
 		return NULL;
@@ -421,7 +421,7 @@  struct wpabuf * wps_build_nfc_pw_token(u16 dev_pw_id,
 
 	if (wps_build_oob_dev_pw(data, dev_pw_id, pubkey,
 				 wpabuf_head(dev_pw), wpabuf_len(dev_pw)) ||
-	    wps_build_wfa_ext(data, 0, NULL, 0)) {
+	    wps_build_wfa_ext(data, 0, NULL, 0, 0)) {
 		wpa_printf(MSG_ERROR, "WPS: Failed to build NFC password "
 			   "token");
 		wpabuf_clear_free(data);
@@ -586,7 +586,7 @@  struct wpabuf * wps_build_wsc_ack(struct wps_data *wps)
 	    wps_build_msg_type(msg, WPS_WSC_ACK) ||
 	    wps_build_enrollee_nonce(wps, msg) ||
 	    wps_build_registrar_nonce(wps, msg) ||
-	    wps_build_wfa_ext(msg, 0, NULL, 0)) {
+	    wps_build_wfa_ext(msg, 0, NULL, 0, 0)) {
 		wpabuf_free(msg);
 		return NULL;
 	}
@@ -610,7 +610,7 @@  struct wpabuf * wps_build_wsc_nack(struct wps_data *wps)
 	    wps_build_enrollee_nonce(wps, msg) ||
 	    wps_build_registrar_nonce(wps, msg) ||
 	    wps_build_config_error(msg, wps->config_error) ||
-	    wps_build_wfa_ext(msg, 0, NULL, 0)) {
+	    wps_build_wfa_ext(msg, 0, NULL, 0, 0)) {
 		wpabuf_free(msg);
 		return NULL;
 	}
@@ -726,7 +726,7 @@  struct wpabuf * wps_build_nfc_handover_req(struct wps_context *ctx,
 	if (wps_build_oob_dev_pw(msg, DEV_PW_NFC_CONNECTION_HANDOVER,
 				 nfc_dh_pubkey, NULL, 0) ||
 	    wps_build_uuid_e(msg, ctx->uuid) ||
-	    wps_build_wfa_ext(msg, 0, NULL, 0)) {
+	    wps_build_wfa_ext(msg, 0, NULL, 0, 0)) {
 		wpabuf_free(msg);
 		return NULL;
 	}
@@ -809,7 +809,7 @@  struct wpabuf * wps_build_nfc_handover_sel(struct wps_context *ctx,
 	    wps_build_ssid(msg, ctx) ||
 	    wps_build_ap_freq(msg, freq) ||
 	    (bssid && wps_build_mac_addr(msg, bssid)) ||
-	    wps_build_wfa_ext(msg, 0, NULL, 0)) {
+	    wps_build_wfa_ext(msg, 0, NULL, 0, 0)) {
 		wpabuf_free(msg);
 		return NULL;
 	}
@@ -848,7 +848,7 @@  struct wpabuf * wps_build_nfc_handover_req_p2p(struct wps_context *ctx,
 	    wps_build_rf_bands(&ctx->dev, msg, 0) ||
 	    wps_build_serial_number(&ctx->dev, msg) ||
 	    wps_build_uuid_e(msg, ctx->uuid) ||
-	    wps_build_wfa_ext(msg, 0, NULL, 0)) {
+	    wps_build_wfa_ext(msg, 0, NULL, 0, 0)) {
 		wpabuf_free(msg);
 		return NULL;
 	}
@@ -900,7 +900,7 @@  struct wpabuf * wps_build_nfc_handover_sel_p2p(struct wps_context *ctx,
 	    wps_build_rf_bands(&ctx->dev, msg, 0) ||
 	    wps_build_serial_number(&ctx->dev, msg) ||
 	    wps_build_uuid_e(msg, ctx->uuid) ||
-	    wps_build_wfa_ext(msg, 0, NULL, 0)) {
+	    wps_build_wfa_ext(msg, 0, NULL, 0, 0)) {
 		wpabuf_free(msg);
 		return NULL;
 	}
diff --git a/src/wps/wps_defs.h b/src/wps/wps_defs.h
index 301864da4..9fccb4eeb 100644
--- a/src/wps/wps_defs.h
+++ b/src/wps/wps_defs.h
@@ -152,7 +152,8 @@  enum {
 	WFA_ELEM_NETWORK_KEY_SHAREABLE = 0x02,
 	WFA_ELEM_REQUEST_TO_ENROLL = 0x03,
 	WFA_ELEM_SETTINGS_DELAY_TIME = 0x04,
-	WFA_ELEM_REGISTRAR_CONFIGURATION_METHODS = 0x05
+	WFA_ELEM_REGISTRAR_CONFIGURATION_METHODS = 0x05,
+	WFA_ELEM_MULTI_AP = 0x06
 };
 
 /* Device Password ID */
diff --git a/src/wps/wps_enrollee.c b/src/wps/wps_enrollee.c
index 417507740..e175770f1 100644
--- a/src/wps/wps_enrollee.c
+++ b/src/wps/wps_enrollee.c
@@ -105,6 +105,7 @@  static struct wpabuf * wps_build_m1(struct wps_data *wps)
 {
 	struct wpabuf *msg;
 	u16 config_methods;
+	u8 multi_ap_backhaul_sta = 0;
 
 	if (random_get_bytes(wps->nonce_e, WPS_NONCE_LEN) < 0)
 		return NULL;
@@ -134,6 +135,9 @@  static struct wpabuf * wps_build_m1(struct wps_data *wps)
 				    WPS_CONFIG_PHY_PUSHBUTTON);
 	}
 
+	if (wps->wps->multi_ap_backhaul_sta)
+		multi_ap_backhaul_sta = MULTI_AP_BACKHAUL_STA;
+
 	if (wps_build_version(msg) ||
 	    wps_build_msg_type(msg, WPS_M1) ||
 	    wps_build_uuid_e(msg, wps->uuid_e) ||
@@ -152,7 +156,7 @@  static struct wpabuf * wps_build_m1(struct wps_data *wps)
 	    wps_build_dev_password_id(msg, wps->dev_pw_id) ||
 	    wps_build_config_error(msg, WPS_CFG_NO_ERROR) ||
 	    wps_build_os_version(&wps->wps->dev, msg) ||
-	    wps_build_wfa_ext(msg, 0, NULL, 0) ||
+	    wps_build_wfa_ext(msg, 0, NULL, 0, multi_ap_backhaul_sta) ||
 	    wps_build_vendor_ext_m1(&wps->wps->dev, msg)) {
 		wpabuf_free(msg);
 		return NULL;
@@ -190,7 +194,7 @@  static struct wpabuf * wps_build_m3(struct wps_data *wps)
 	    wps_build_msg_type(msg, WPS_M3) ||
 	    wps_build_registrar_nonce(wps, msg) ||
 	    wps_build_e_hash(wps, msg) ||
-	    wps_build_wfa_ext(msg, 0, NULL, 0) ||
+	    wps_build_wfa_ext(msg, 0, NULL, 0, 0) ||
 	    wps_build_authenticator(wps, msg)) {
 		wpabuf_free(msg);
 		return NULL;
@@ -223,7 +227,7 @@  static struct wpabuf * wps_build_m5(struct wps_data *wps)
 	    wps_build_e_snonce1(wps, plain) ||
 	    wps_build_key_wrap_auth(wps, plain) ||
 	    wps_build_encr_settings(wps, msg, plain) ||
-	    wps_build_wfa_ext(msg, 0, NULL, 0) ||
+	    wps_build_wfa_ext(msg, 0, NULL, 0, 0) ||
 	    wps_build_authenticator(wps, msg)) {
 		wpabuf_clear_free(plain);
 		wpabuf_free(msg);
@@ -393,7 +397,7 @@  static struct wpabuf * wps_build_m7(struct wps_data *wps)
 	    (wps->wps->ap && wps_build_ap_settings(wps, plain)) ||
 	    wps_build_key_wrap_auth(wps, plain) ||
 	    wps_build_encr_settings(wps, msg, plain) ||
-	    wps_build_wfa_ext(msg, 0, NULL, 0) ||
+	    wps_build_wfa_ext(msg, 0, NULL, 0, 0) ||
 	    wps_build_authenticator(wps, msg)) {
 		wpabuf_clear_free(plain);
 		wpabuf_free(msg);
@@ -430,7 +434,7 @@  static struct wpabuf * wps_build_wsc_done(struct wps_data *wps)
 	    wps_build_msg_type(msg, WPS_WSC_DONE) ||
 	    wps_build_enrollee_nonce(wps, msg) ||
 	    wps_build_registrar_nonce(wps, msg) ||
-	    wps_build_wfa_ext(msg, 0, NULL, 0)) {
+	    wps_build_wfa_ext(msg, 0, NULL, 0, 0)) {
 		wpabuf_free(msg);
 		return NULL;
 	}
diff --git a/src/wps/wps_er.c b/src/wps/wps_er.c
index affd6a4af..06a8fdaf3 100644
--- a/src/wps/wps_er.c
+++ b/src/wps/wps_er.c
@@ -1530,7 +1530,7 @@  void wps_er_set_sel_reg(struct wps_er *er, int sel_reg, u16 dev_passwd_id,
 	    wps_er_build_selected_registrar(msg, sel_reg) ||
 	    wps_er_build_dev_password_id(msg, dev_passwd_id) ||
 	    wps_er_build_sel_reg_config_methods(msg, sel_reg_config_methods) ||
-	    wps_build_wfa_ext(msg, 0, auth_macs, count) ||
+	    wps_build_wfa_ext(msg, 0, auth_macs, count, 0) ||
 	    wps_er_build_uuid_r(msg, er->wps->uuid)) {
 		wpabuf_free(msg);
 		return;
@@ -2048,7 +2048,7 @@  struct wpabuf * wps_er_config_token_from_cred(struct wps_context *wps,
 	data.wps = wps;
 	data.use_cred = cred;
 	if (wps_build_cred(&data, ret) ||
-	    wps_build_wfa_ext(ret, 0, NULL, 0)) {
+	    wps_build_wfa_ext(ret, 0, NULL, 0, 0)) {
 		wpabuf_free(ret);
 		return NULL;
 	}
diff --git a/src/wps/wps_i.h b/src/wps/wps_i.h
index fe0c60bd1..7972fc225 100644
--- a/src/wps/wps_i.h
+++ b/src/wps/wps_i.h
@@ -163,7 +163,8 @@  int wps_build_encr_settings(struct wps_data *wps, struct wpabuf *msg,
 			    struct wpabuf *plain);
 int wps_build_version(struct wpabuf *msg);
 int wps_build_wfa_ext(struct wpabuf *msg, int req_to_enroll,
-		      const u8 *auth_macs, size_t auth_macs_count);
+		      const u8 *auth_macs, size_t auth_macs_count,
+		      u8 multi_ap_subelem);
 int wps_build_msg_type(struct wpabuf *msg, enum wps_msg_type msg_type);
 int wps_build_enrollee_nonce(struct wps_data *wps, struct wpabuf *msg);
 int wps_build_registrar_nonce(struct wps_data *wps, struct wpabuf *msg);
diff --git a/src/wps/wps_registrar.c b/src/wps/wps_registrar.c
index 379925e3f..b5ac29bea 100644
--- a/src/wps/wps_registrar.c
+++ b/src/wps/wps_registrar.c
@@ -1281,7 +1281,7 @@  static int wps_set_ie(struct wps_registrar *reg)
 	    wps_build_sel_reg_config_methods(reg, beacon) ||
 	    wps_build_sel_pbc_reg_uuid_e(reg, beacon) ||
 	    (reg->dualband && wps_build_rf_bands(&reg->wps->dev, beacon, 0)) ||
-	    wps_build_wfa_ext(beacon, 0, auth_macs, count) ||
+	    wps_build_wfa_ext(beacon, 0, auth_macs, count, 0) ||
 	    wps_build_vendor_ext(&reg->wps->dev, beacon)) {
 		wpabuf_free(beacon);
 		wpabuf_free(probe);
@@ -1311,7 +1311,7 @@  static int wps_set_ie(struct wps_registrar *reg)
 	    wps_build_device_attrs(&reg->wps->dev, probe) ||
 	    wps_build_probe_config_methods(reg, probe) ||
 	    (reg->dualband && wps_build_rf_bands(&reg->wps->dev, probe, 0)) ||
-	    wps_build_wfa_ext(probe, 0, auth_macs, count) ||
+	    wps_build_wfa_ext(probe, 0, auth_macs, count, 0) ||
 	    wps_build_vendor_ext(&reg->wps->dev, probe)) {
 		wpabuf_free(beacon);
 		wpabuf_free(probe);
@@ -1845,7 +1845,7 @@  static struct wpabuf * wps_build_m2(struct wps_data *wps)
 	    wps_build_config_error(msg, WPS_CFG_NO_ERROR) ||
 	    wps_build_dev_password_id(msg, wps->dev_pw_id) ||
 	    wps_build_os_version(&wps->wps->dev, msg) ||
-	    wps_build_wfa_ext(msg, 0, NULL, 0)) {
+	    wps_build_wfa_ext(msg, 0, NULL, 0, 0)) {
 		wpabuf_free(msg);
 		return NULL;
 	}
@@ -1913,7 +1913,7 @@  static struct wpabuf * wps_build_m2d(struct wps_data *wps)
 	    wps_build_assoc_state(wps, msg) ||
 	    wps_build_config_error(msg, err) ||
 	    wps_build_os_version(&wps->wps->dev, msg) ||
-	    wps_build_wfa_ext(msg, 0, NULL, 0)) {
+	    wps_build_wfa_ext(msg, 0, NULL, 0, 0)) {
 		wpabuf_free(msg);
 		return NULL;
 	}
@@ -1949,7 +1949,7 @@  static struct wpabuf * wps_build_m4(struct wps_data *wps)
 	    wps_build_r_snonce1(wps, plain) ||
 	    wps_build_key_wrap_auth(wps, plain) ||
 	    wps_build_encr_settings(wps, msg, plain) ||
-	    wps_build_wfa_ext(msg, 0, NULL, 0) ||
+	    wps_build_wfa_ext(msg, 0, NULL, 0, 0) ||
 	    wps_build_authenticator(wps, msg)) {
 		wpabuf_clear_free(plain);
 		wpabuf_free(msg);
@@ -1984,7 +1984,7 @@  static struct wpabuf * wps_build_m6(struct wps_data *wps)
 	    wps_build_r_snonce2(wps, plain) ||
 	    wps_build_key_wrap_auth(wps, plain) ||
 	    wps_build_encr_settings(wps, msg, plain) ||
-	    wps_build_wfa_ext(msg, 0, NULL, 0) ||
+	    wps_build_wfa_ext(msg, 0, NULL, 0, 0) ||
 	    wps_build_authenticator(wps, msg)) {
 		wpabuf_clear_free(plain);
 		wpabuf_free(msg);
@@ -2021,7 +2021,7 @@  static struct wpabuf * wps_build_m8(struct wps_data *wps)
 	    (!wps->wps->ap && !wps->er && wps_build_ap_settings(wps, plain)) ||
 	    wps_build_key_wrap_auth(wps, plain) ||
 	    wps_build_encr_settings(wps, msg, plain) ||
-	    wps_build_wfa_ext(msg, 0, NULL, 0) ||
+	    wps_build_wfa_ext(msg, 0, NULL, 0, 0) ||
 	    wps_build_authenticator(wps, msg)) {
 		wpabuf_clear_free(plain);
 		wpabuf_clear_free(msg);
diff --git a/src/wps/wps_upnp.c b/src/wps/wps_upnp.c
index 0c458c6ad..ca893a43c 100644
--- a/src/wps/wps_upnp.c
+++ b/src/wps/wps_upnp.c
@@ -599,7 +599,7 @@  static struct wpabuf * build_fake_wsc_ack(void)
 	wpabuf_put_be16(msg, ATTR_REGISTRAR_NONCE);
 	wpabuf_put_be16(msg, WPS_NONCE_LEN);
 	wpabuf_put(msg, WPS_NONCE_LEN);
-	if (wps_build_wfa_ext(msg, 0, NULL, 0)) {
+	if (wps_build_wfa_ext(msg, 0, NULL, 0, 0)) {
 		wpabuf_free(msg);
 		return NULL;
 	}
diff --git a/wpa_supplicant/config.c b/wpa_supplicant/config.c
index 4ff93f62f..6cbc21fd8 100644
--- a/wpa_supplicant/config.c
+++ b/wpa_supplicant/config.c
@@ -4652,6 +4652,7 @@  static const struct global_parse_data global_fields[] = {
 	{ STR(config_methods), CFG_CHANGED_CONFIG_METHODS },
 	{ INT_RANGE(wps_cred_processing, 0, 2), 0 },
 	{ FUNC(wps_vendor_ext_m1), CFG_CHANGED_VENDOR_EXTENSION },
+	{ INT_RANGE(multi_ap_backhaul_sta, 0, 1), CFG_CHANGED_MULTI_AP_BACKHAUL_STA },
 #endif /* CONFIG_WPS */
 #ifdef CONFIG_P2P
 	{ FUNC(sec_device_type), CFG_CHANGED_SEC_DEVICE_TYPE },
diff --git a/wpa_supplicant/config.h b/wpa_supplicant/config.h
index cd7571f59..c8857016f 100644
--- a/wpa_supplicant/config.h
+++ b/wpa_supplicant/config.h
@@ -374,6 +374,7 @@  struct wpa_cred {
 #define CFG_CHANGED_P2P_PASSPHRASE_LEN BIT(16)
 #define CFG_CHANGED_SCHED_SCAN_PLANS BIT(17)
 #define CFG_CHANGED_WOWLAN_TRIGGERS BIT(18)
+#define CFG_CHANGED_MULTI_AP_BACKHAUL_STA BIT(19)
 
 /**
  * struct wpa_config - wpa_supplicant configuration data
@@ -768,6 +769,7 @@  struct wpa_config {
 	int p2p_optimize_listen_chan;
 
 	struct wpabuf *wps_vendor_ext_m1;
+	int multi_ap_backhaul_sta;
 
 #define MAX_WPS_VENDOR_EXT 10
 	/**
diff --git a/wpa_supplicant/config_file.c b/wpa_supplicant/config_file.c
index 09115e19d..e78f273b1 100644
--- a/wpa_supplicant/config_file.c
+++ b/wpa_supplicant/config_file.c
@@ -1183,6 +1183,10 @@  static void wpa_config_write_global(FILE *f, struct wpa_config *config)
 			fprintf(f, "\n");
 		}
 	}
+
+	if (config->multi_ap_backhaul_sta)
+		fprintf(f, "multi_ap_backhaul_sta=%d\n", config->multi_ap_backhaul_sta);
+
 #endif /* CONFIG_WPS */
 #ifdef CONFIG_P2P
 	{
diff --git a/wpa_supplicant/wpa_supplicant.conf b/wpa_supplicant/wpa_supplicant.conf
index e5dee2236..d22f76664 100644
--- a/wpa_supplicant/wpa_supplicant.conf
+++ b/wpa_supplicant/wpa_supplicant.conf
@@ -286,6 +286,11 @@  fast_reauth=1
 # The vendor attribute contents to be added in M1 (hex string)
 #wps_vendor_ext_m1=000137100100020001
 
+# Multi-AP STA config
+# When set to 1, the Multi-AP vendor extension is added to WPS M1 and the
+# Multi-AP IE is sent to the driver for inclusion in association request frames.
+#multi_ap_backhaul_sta=1
+
 # NFC password token for WPS
 # These parameters can be used to configure a fixed NFC password token for the
 # station. This can be generated, e.g., with nfc_pw_token. When these
diff --git a/wpa_supplicant/wps_supplicant.c b/wpa_supplicant/wps_supplicant.c
index 1a2677b8e..de3943912 100644
--- a/wpa_supplicant/wps_supplicant.c
+++ b/wpa_supplicant/wps_supplicant.c
@@ -473,6 +473,11 @@  static int wpa_supplicant_wps_cred(void *ctx,
 	wpa_config_set_network_defaults(ssid);
 	ssid->wps_run = wpa_s->wps_run;
 
+	if (wpa_s->conf->multi_ap_backhaul_sta)
+		ssid->multi_ap_backhaul_sta = 1;
+	else
+		ssid->multi_ap_backhaul_sta = 0;
+
 	os_free(ssid->ssid);
 	ssid->ssid = os_malloc(cred->ssid_len);
 	if (ssid->ssid) {
@@ -1181,6 +1186,8 @@  int wpas_wps_start_pbc(struct wpa_supplicant *wpa_s, const u8 *bssid,
 		return -1;
 	if (wpa_s->wps_fragment_size)
 		ssid->eap.fragment_size = wpa_s->wps_fragment_size;
+	if (wpa_s->conf->multi_ap_backhaul_sta)
+		ssid->multi_ap_backhaul_sta = 1;
 	wpa_supplicant_wps_event(wpa_s, WPS_EV_PBC_ACTIVE, NULL);
 	eloop_register_timeout(WPS_PBC_WALK_TIME, 0, wpas_wps_timeout,
 			       wpa_s, NULL);
@@ -1527,7 +1534,6 @@  static void wpas_wps_set_vendor_ext_m1(struct wpa_supplicant *wpa_s,
 	}
 }
 
-
 int wpas_wps_init(struct wpa_supplicant *wpa_s)
 {
 	struct wps_context *wps;
@@ -1569,6 +1575,8 @@  int wpas_wps_init(struct wpa_supplicant *wpa_s)
 
 	wpas_wps_set_vendor_ext_m1(wpa_s, wps);
 
+	wps->multi_ap_backhaul_sta = wpa_s->conf->multi_ap_backhaul_sta;
+
 	wps->dev.os_version = WPA_GET_BE32(wpa_s->conf->os_version);
 	modes = wpa_s->hw.modes;
 	if (modes) {
@@ -2205,6 +2213,9 @@  void wpas_wps_update_config(struct wpa_supplicant *wpa_s)
 	if (wpa_s->conf->changed_parameters & CFG_CHANGED_VENDOR_EXTENSION)
 		wpas_wps_set_vendor_ext_m1(wpa_s, wps);
 
+	if (wpa_s->conf->changed_parameters & CFG_CHANGED_MULTI_AP_BACKHAUL_STA)
+		wps->multi_ap_backhaul_sta = wpa_s->conf->multi_ap_backhaul_sta;
+
 	if (wpa_s->conf->changed_parameters & CFG_CHANGED_OS_VERSION)
 		wps->dev.os_version = WPA_GET_BE32(wpa_s->conf->os_version);