diff -pruN 1.30-1/AUTHORS 2.3-1/AUTHORS
--- 1.30-1/AUTHORS	2022-06-04 20:14:23.000000000 +0000
+++ 2.3-1/AUTHORS	2022-11-18 12:31:49.000000000 +0000
@@ -37,3 +37,5 @@ Diederik de Haas <didi.debian@cknow.org>
 Vladimír Dudr <vladimir@tango-dj.cz>
 Emmanuel VAUTRIN <Emmanuel.VAUTRIN@cpexterne.org>
 Jesse Lentz <jesse@twosheds.org>
+Pinghao Wu <xdavidwuph@gmail.com>
+Neehar Vijay <env252525@gmail.com>
diff -pruN 1.30-1/ChangeLog 2.3-1/ChangeLog
--- 1.30-1/ChangeLog	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/ChangeLog	2023-02-02 12:57:32.000000000 +0000
@@ -1,3 +1,39 @@
+ver 2.3:
+	Fix issue with length calculation for WMM IE.
+	Fix issue with channel number allocation off-by-one.
+	Fix issue with cached session when TLS phase2 fails.
+	Add support for FastReauthentication setting for EAP-TLS.
+
+ver 2.2:
+	Fix issue with handling FT and multiple roaming scans.
+	Fix issue with handling multiple wiphy registrations.
+	Fix issue with with EAP-PEAP session resumption.
+	Add support for using PTK rekeying in AP mode.
+	Add support for setting country IE in AP mode.
+	Add support for setting WMM parameter IE in AP mode.
+
+ver 2.1:
+	Fix issue with handling FT-over-DS action.
+	Fix issue with handling scan and 6 GHz support check.
+	Fix issue with handling when periodic scans get aborted.
+	Add support for using 5 GHz frequencies in AP mode.
+
+ver 2.0:
+	Fix issue with handling P2P and limiting ciphers to CCMP.
+	Fix issue with scanning before forced roaming action.
+	Fix issue with provided scan frequencies from RRM.
+	Fix issue with handling Michael MIC failure message.
+	Fix issue with handling timestamp size in MPDU frames.
+	Fix issue with handling enablement of OCVC for FT AKMs.
+	Fix issue with handling FT work as highest priority.
+	Fix issue with handling roaming events and Multi-BSS.
+	Add support for utilizing roaming candidates list.
+	Add support for utilizing TLS session caching.
+	Add support for ciphers with 256 bits key size.
+	Add support for Access Point mode with legacy TKIP.
+	Add support for MAC address changes while powered.
+	Add support for IPv4 and IPv6 network configuration.
+
 ver 1.30:
 	Fix issue with handling OWE if buggy AP is detected.
 	Fix issue with handling quick scan and enabling 6GHz.
diff -pruN 1.30-1/client/ap.c 2.3-1/client/ap.c
--- 1.30-1/client/ap.c	2022-07-15 17:08:06.000000000 +0000
+++ 2.3-1/client/ap.c	2022-11-18 12:31:49.000000000 +0000
@@ -36,6 +36,9 @@ struct ap {
 	bool started;
 	char *name;
 	bool scanning;
+	uint32_t freq;
+	char *pairwise;
+	char *group;
 };
 
 static void *ap_create(void)
@@ -50,6 +53,12 @@ static void ap_destroy(void *data)
 	if (ap->name)
 		l_free(ap->name);
 
+	if (ap->pairwise)
+		l_free(ap->pairwise);
+
+	if (ap->group)
+		l_free(ap->group);
+
 	l_free(ap);
 }
 
@@ -126,10 +135,91 @@ static const char *get_scanning_tostr(co
 	return ap->scanning ? "yes" : "no";
 }
 
+static void update_freq(void *data, struct l_dbus_message_iter *variant)
+{
+	struct ap *ap = data;
+	uint32_t value;
+
+	if (!l_dbus_message_iter_get_variant(variant, "u", &value)) {
+		ap->freq = 0;
+
+		return;
+	}
+
+	ap->freq = value;
+}
+
+static const char *get_freq_tostr(const void *data)
+{
+	const struct ap *ap = data;
+	static char str[5];
+
+	sprintf(str, "%u", ap->freq);
+
+	return str;
+}
+
+static void update_pairwise(void *data, struct l_dbus_message_iter *variant)
+{
+	struct ap *ap = data;
+	char *value;
+
+	if (ap->pairwise)
+		l_free(ap->pairwise);
+
+	if (!l_dbus_message_iter_get_variant(variant, "s", &value)) {
+		ap->pairwise = NULL;
+
+		return;
+	}
+
+	ap->pairwise = l_strdup(value);
+}
+
+static const char *get_pairwise_tostr(const void *data)
+{
+	const struct ap *ap = data;
+
+	if (!ap->pairwise)
+		return "";
+
+	return ap->pairwise;
+}
+
+static void update_group(void *data, struct l_dbus_message_iter *variant)
+{
+	struct ap *ap = data;
+	char *value;
+
+	if (ap->group)
+		l_free(ap->group);
+
+	if (!l_dbus_message_iter_get_variant(variant, "s", &value)) {
+		ap->group = NULL;
+
+		return;
+	}
+
+	ap->group = l_strdup(value);
+}
+
+static const char *get_group_tostr(const void *data)
+{
+	const struct ap *ap = data;
+
+	if (!ap->group)
+		return "";
+
+	return ap->group;
+}
+
 static const struct proxy_interface_property ap_properties[] = {
 	{ "Started",  "b", update_started,  get_started_tostr },
 	{ "Name",     "s", update_name, get_name_tostr },
 	{ "Scanning", "b", update_scanning, get_scanning_tostr },
+	{ "Frequency", "u", update_freq, get_freq_tostr },
+	{ "PairwiseCiphers", "s", update_pairwise, get_pairwise_tostr },
+	{ "GroupCipher", "s", update_group, get_group_tostr },
 	{ }
 };
 
diff -pruN 1.30-1/client/command.c 2.3-1/client/command.c
--- 1.30-1/client/command.c	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/client/command.c	2022-12-18 19:59:36.000000000 +0000
@@ -753,7 +753,7 @@ options_parsed:
 	argv += optind;
 	argc -= optind;
 
-	if (argc < 2) {
+	if (argc < 1) {
 		interactive_mode = true;
 		return false;
 	}
diff -pruN 1.30-1/client/display.c 2.3-1/client/display.c
--- 1.30-1/client/display.c	2022-07-15 17:08:06.000000000 +0000
+++ 2.3-1/client/display.c	2022-11-18 12:31:49.000000000 +0000
@@ -30,6 +30,7 @@
 #include <sys/stat.h>
 #include <sys/ioctl.h>
 #include <unistd.h>
+#include <wchar.h>
 
 #include <readline/history.h>
 #include <readline/readline.h>
@@ -387,39 +388,72 @@ static unsigned int color_end(char *s)
 }
 
 /*
- * Finds last space in 's' before 'max' characters, terminates at that index,
+ * Finds last space in 's' before 'width' characters, terminates at that index,
  * and returns a new string to be printed on the next line.
  *
- * 'max' should be set to the column width, but is also an out parameter since
- * this width can be updated if colored escapes are detected.
+ * 'new_width' will be updated to include extra bytes for color escapes or
+ * wide characters if found.
  *
  * Any colored escapes found are set to 'color_out' so they can be re-enabled
  * on the next line.
  */
-static char* next_line(char *s, unsigned int *max, char **color_out)
+static char* next_line(char *s, unsigned int width, unsigned int *new_width,
+			char **color_out)
 {
-	unsigned int i;
+	unsigned int i = 0;
 	int last_space = -1;
 	int last_color = -1;
+	unsigned int s_len = strlen(s);
+	unsigned int color_adjust = 0;
+	char *ret;
+
+	*new_width = width;
+	*color_out = NULL;
 
 	/* Find the last space before 'max', as well as any color */
-	for (i = 0; i <= *max && s[i] != '\0'; i++) {
-		if (s[i] == ' ')
-			last_space = i;
-		else if (s[i] == 0x1b) {
+	while (i <= *new_width && i < s_len) {
+		int sequence_len;
+		int sequence_columns;
+		wchar_t w;
+
+		if (s[i] == 0x1b) {
+			sequence_len = color_end(s + i);
 			/* color escape won't count for column width */
-			*max += color_end(s + i);
+			sequence_columns = 0;
 			last_color = i;
+
+			/*
+			 * Color after a space. If the line gets broken this
+			 * will need to be removed off new_width since it will
+			 * appear on the next line.
+			 */
+			if (last_space != -1)
+				color_adjust += sequence_len;
+
+		} else {
+			if (s[i] == ' ') {
+				last_space = i;
+				/* Any past colors will appear on this line */
+				color_adjust = 0;
+			}
+
+			sequence_len = l_utf8_get_codepoint(&s[i], s_len - i,
+									&w);
+			sequence_columns = wcwidth(w);
 		}
+
+		/* Compensate max bytes */
+		*new_width += sequence_len - sequence_columns;
+		i += sequence_len;
 	}
 
 	/* Reached the end of the string within the column bounds */
-	if (i <= *max)
+	if (i <= *new_width)
 		return NULL;
 
 	/* Not anywhere nice to split the line */
 	if (last_space == -1)
-		last_space = *max - 1;
+		last_space = *new_width;
 
 	/*
 	 * Only set the color if it occurred prior to the last space. If after,
@@ -428,12 +462,14 @@ static char* next_line(char *s, unsigned
 	if (last_color != -1 && last_space >= last_color)
 		*color_out = l_strndup(s + last_color,
 					color_end(s + last_color));
-	else
-		*color_out = NULL;
+	else if (last_color != -1 && last_space < last_color)
+		*new_width -= color_adjust;
+
+	ret = l_strdup(s + last_space + 1);
 
-	s[last_space] = '\0';
+	s[last_space + 1] = '\0';
 
-	return l_strdup(s + last_space + 1);
+	return ret;
 }
 
 struct table_entry {
@@ -450,7 +486,7 @@ static int entry_append(struct table_ent
 {
 	char *value = e->next;
 	unsigned int ret = 0;
-	unsigned int width = e->width;
+	unsigned int new_width;
 
 	/* Empty line */
 	if (!value)
@@ -464,10 +500,10 @@ static int entry_append(struct table_ent
 	}
 
 	/* Advance entry to next line, and terminate current */
-	e->next = next_line(value, &width, &e->color);
+	e->next = next_line(value, e->width, &new_width, &e->color);
 
 	/* Append current line */
-	ret += sprintf(line_buf + ret, "%-*s  ", width, value);
+	ret += sprintf(line_buf + ret, "%-*s  ", new_width, value);
 
 	l_free(value);
 
@@ -513,9 +549,17 @@ void display_table_row(const char *margi
 
 	for (i = 0; i < ncolumns; i++) {
 		struct table_entry *e = &entries[i];
+		char *v;
 
 		e->width = va_arg(va, unsigned int);
-		e->next = l_strdup(va_arg(va, char*));
+		v = va_arg(va, char *);
+
+		if (!l_utf8_validate(v, strlen(v), NULL)) {
+			display_error("Invalid utf-8 string!");
+			goto done;
+		}
+
+		e->next = l_strdup(v);
 
 		str += entry_append(e, str);
 	}
@@ -546,9 +590,13 @@ void display_table_row(const char *margi
 		str = buf;
 	}
 
+done:
 	for (i = 0; i < ncolumns; i++) {
 		if (entries[i].color)
 			l_free(entries[i].color);
+
+		if (entries[i].next)
+			l_free(entries[i].next);
 	}
 }
 
diff -pruN 1.30-1/client/main.c 2.3-1/client/main.c
--- 1.30-1/client/main.c	2020-09-05 07:43:41.000000000 +0000
+++ 2.3-1/client/main.c	2022-11-18 12:31:49.000000000 +0000
@@ -25,6 +25,7 @@
 #endif
 
 #include <errno.h>
+#include <locale.h>
 #include <signal.h>
 #include <ell/ell.h>
 
@@ -50,6 +51,8 @@ int main(int argc, char *argv[])
 {
 	bool all_done;
 
+	setlocale(LC_CTYPE, "");
+
 	if (!l_main_init())
 		return EXIT_FAILURE;
 
diff -pruN 1.30-1/client/station.c 2.3-1/client/station.c
--- 1.30-1/client/station.c	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/client/station.c	2022-11-18 12:31:49.000000000 +0000
@@ -217,15 +217,9 @@ static void display_station(const char *
 			47, network_get_name(station->connected_network));
 
 		display_addresses(device_name);
-
-		/*
-		 * If connected the diagnostic interface is presumably up so
-		 * don't add the table footer just yet.
-		 */
-		return;
 	}
 
-	display_table_footer();
+	/* The table footer is handled by cmd_show. */
 }
 
 static void display_station_inline(const char *margin, const void *data)
diff -pruN 1.30-1/configure 2.3-1/configure
--- 1.30-1/configure	2022-09-07 18:55:34.000000000 +0000
+++ 2.3-1/configure	2023-02-02 12:59:45.000000000 +0000
@@ -1,6 +1,6 @@
 #! /bin/sh
 # Guess values for system-dependent variables and create Makefiles.
-# Generated by GNU Autoconf 2.71 for iwd 1.30.
+# Generated by GNU Autoconf 2.71 for iwd 2.3.
 #
 #
 # Copyright (C) 1992-1996, 1998-2017, 2020-2021 Free Software Foundation,
@@ -618,8 +618,8 @@ MAKEFLAGS=
 # Identity of this package.
 PACKAGE_NAME='iwd'
 PACKAGE_TARNAME='iwd'
-PACKAGE_VERSION='1.30'
-PACKAGE_STRING='iwd 1.30'
+PACKAGE_VERSION='2.3'
+PACKAGE_STRING='iwd 2.3'
 PACKAGE_BUGREPORT=''
 PACKAGE_URL=''
 
@@ -1421,7 +1421,7 @@ if test "$ac_init_help" = "long"; then
   # Omit some internal or obsolete options to make the list less imposing.
   # This message is too long to be a string in the A/UX 3.1 sh.
   cat <<_ACEOF
-\`configure' configures iwd 1.30 to adapt to many kinds of systems.
+\`configure' configures iwd 2.3 to adapt to many kinds of systems.
 
 Usage: $0 [OPTION]... [VAR=VALUE]...
 
@@ -1492,7 +1492,7 @@ fi
 
 if test -n "$ac_init_help"; then
   case $ac_init_help in
-     short | recursive ) echo "Configuration of iwd 1.30:";;
+     short | recursive ) echo "Configuration of iwd 2.3:";;
    esac
   cat <<\_ACEOF
 
@@ -1643,7 +1643,7 @@ fi
 test -n "$ac_init_help" && exit $ac_status
 if $ac_init_version; then
   cat <<\_ACEOF
-iwd configure 1.30
+iwd configure 2.3
 generated by GNU Autoconf 2.71
 
 Copyright (C) 2021 Free Software Foundation, Inc.
@@ -1861,7 +1861,7 @@ cat >config.log <<_ACEOF
 This file contains any messages produced by compilers while
 running configure, to aid debugging if configure makes a mistake.
 
-It was created by iwd $as_me 1.30, which was
+It was created by iwd $as_me 2.3, which was
 generated by GNU Autoconf 2.71.  Invocation command line was
 
   $ $0$ac_configure_args_raw
@@ -3136,7 +3136,7 @@ fi
 
 # Define the identity of the package.
  PACKAGE='iwd'
- VERSION='1.30'
+ VERSION='2.3'
 
 
 printf "%s\n" "#define PACKAGE \"$PACKAGE\"" >>confdefs.h
@@ -13986,7 +13986,7 @@ if (test "${enable_external_ell}" = "yes
 			test "${enable_monitor}" != "no" ||
 			test "${enable_wired}" = "yes" ||
 			test "${enable_hwsim}" = "yes"); then
-		ell_min_version="0.53"
+		ell_min_version="0.56"
 	else
 		ell_min_version="0.5"
 	fi
@@ -14712,7 +14712,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_wri
 # report actual input values of CONFIG_FILES etc. instead of their
 # values after options handling.
 ac_log="
-This file was extended by iwd $as_me 1.30, which was
+This file was extended by iwd $as_me 2.3, which was
 generated by GNU Autoconf 2.71.  Invocation command line was
 
   CONFIG_FILES    = $CONFIG_FILES
@@ -14780,7 +14780,7 @@ ac_cs_config_escaped=`printf "%s\n" "$ac
 cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
 ac_cs_config='$ac_cs_config_escaped'
 ac_cs_version="\\
-iwd config.status 1.30
+iwd config.status 2.3
 configured by $0, generated by GNU Autoconf 2.71,
   with options \\"\$ac_cs_config\\"
 
diff -pruN 1.30-1/configure.ac 2.3-1/configure.ac
--- 1.30-1/configure.ac	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/configure.ac	2023-02-02 12:57:32.000000000 +0000
@@ -1,5 +1,5 @@
 AC_PREREQ([2.69])
-AC_INIT([iwd],[1.30])
+AC_INIT([iwd],[2.3])
 
 AC_CONFIG_HEADERS(config.h)
 AC_CONFIG_AUX_DIR(build-aux)
@@ -263,7 +263,7 @@ if (test "${enable_external_ell}" = "yes
 			test "${enable_monitor}" != "no" ||
 			test "${enable_wired}" = "yes" ||
 			test "${enable_hwsim}" = "yes"); then
-		ell_min_version="0.53"
+		ell_min_version="0.56"
 	else
 		ell_min_version="0.5"
 	fi
diff -pruN 1.30-1/debian/changelog 2.3-1/debian/changelog
--- 1.30-1/debian/changelog	2022-09-09 18:35:26.000000000 +0000
+++ 2.3-1/debian/changelog	2023-02-04 12:57:18.000000000 +0000
@@ -1,3 +1,46 @@
+iwd (2.3-1) unstable; urgency=medium
+
+  [ upstream ]
+  * new release
+
+ -- Jonas Smedegaard <dr@jones.dk>  Sat, 04 Feb 2023 13:57:18 +0100
+
+iwd (2.2-1) unstable; urgency=medium
+
+  [ upstream ]
+  * new release
+
+  [ Jonas Smedegaard ]
+  * fix source helper tool copyright-check
+    + work with Path::Tiny 0.144
+    + avoid insecure shell expansion
+  * tighten build-dependency on libell-dev
+
+ -- Jonas Smedegaard <dr@jones.dk>  Thu, 26 Jan 2023 18:31:20 +0100
+
+iwd (2.1-1) unstable; urgency=medium
+
+  [ upstream ]
+  * new release
+
+  [ Jonas Smedegaard ]
+  * declare compliance with Debian Policy 4.6.2
+  * tighten build-dependency on libell-dev
+
+ -- Jonas Smedegaard <dr@jones.dk>  Wed, 21 Dec 2022 13:50:08 +0100
+
+iwd (2.0-1) unstable; urgency=medium
+
+  [ upstream ]
+  * new release
+
+  [ Jonas Smedegaard ]
+  * update copyright info: update coverage
+  * unfuzz patches
+  * tighten build-dependency on libell-dev
+
+ -- Jonas Smedegaard <dr@jones.dk>  Thu, 01 Dec 2022 10:25:36 +0100
+
 iwd (1.30-1) unstable; urgency=medium
 
   [ upstream ]
diff -pruN 1.30-1/debian/control 2.3-1/debian/control
--- 1.30-1/debian/control	2022-09-09 18:35:17.000000000 +0000
+++ 2.3-1/debian/control	2023-01-26 12:40:39.000000000 +0000
@@ -6,12 +6,12 @@ Homepage: https://iwd.wiki.kernel.org/
 Build-Depends:
  debhelper-compat (= 13),
  libdbus-1-dev,
- libell-dev (>= 0.53),
+ libell-dev (>= 0.56),
  libreadline-dev,
  openssl <!nocheck>,
  python3-docutils <!nodoc>,
  systemd,
-Standards-Version: 4.6.1
+Standards-Version: 4.6.2
 Vcs-Git: https://salsa.debian.org/debian/iwd.git
 Vcs-Browser: https://salsa.debian.org/debian/iwd
 Rules-Requires-Root: no
diff -pruN 1.30-1/debian/copyright 2.3-1/debian/copyright
--- 1.30-1/debian/copyright	2022-06-06 08:32:09.000000000 +0000
+++ 2.3-1/debian/copyright	2022-11-26 09:06:24.000000000 +0000
@@ -202,7 +202,7 @@ Files:
  linux/nl80211.h
 Copyright:
   2008       Colin McCabe <colin@cozybit.com>
-  2018-2019  Intel Corporation
+  2018-2022  Intel Corporation
   2015-2017  Intel Deutschland GmbH
   2006-2010  Johannes Berg <johannes@sipsolutions.net>
   2008       Jouni Malinen <jouni.malinen@atheros.com>
diff -pruN 1.30-1/debian/copyright-check 2.3-1/debian/copyright-check
--- 1.30-1/debian/copyright-check	2021-09-18 14:48:44.000000000 +0000
+++ 2.3-1/debian/copyright-check	2023-01-08 15:52:01.000000000 +0000
@@ -1,5 +1,5 @@
 #!/bin/sh
-# Copyright 2020-2021  Jonas Smedegaard <dr@jones.dk>
+# Copyright 2020-2023  Jonas Smedegaard <dr@jones.dk>
 # Copyright 2020-2021  Purism, SPC
 # Description: helper script to update copyright_hints
 #
@@ -20,6 +20,7 @@
 #  licensecheck,
 #  libimage-exiftool-perl,
 #  libipc-system-simple-perl,
+#  libpath-tiny-perl,
 #  libregexp-assemble-perl,
 #  perl,
 
@@ -39,9 +40,10 @@ _file_regex() {
 		-e '$nonverb_re = Regexp::Assemble->new->add( "\\W+", map { "\\W+(?:$_)\\W+" } @{ $opt{nonverb} } )->as_string;'\
 		-e '@content_re = map { s/\W+/[**]/g; s/\Q[**]\E\d\Q[**]\E/[**]\\S{0,2}[**]/g; s/\Q[**]\E/$nonverb_re/g; qr/$_/ } @license, @grant;'\
 		-e '$inspect = sub {'\
+			-e 'return if $_[0]->is_dir;'\
 			-e '$file = $_[0];'\
 			-e 'if (@firstline_re) {'\
-				-e '($head) = $_[0]->lines( { count => 1 } );'\
+				-e '($head) = $file->lines( { count => 1 } );'\
 				-e 'push @match, quotemeta and return '\
 					-e 'if any { $head =~ $_ } @firstline_re };'\
 			-e 'push @match, quotemeta'\
@@ -61,7 +63,7 @@ SKIPFILES='skip|meta|comment'
 #RE_default=$(grep --files-with-matches --recursive --null \
 # --regexp='Copyright (C) .* Intel Corporation\. All rights reserved' \
 # | tr '\0' '|' | perl -pe 's/\|$//')
-RE_default=$(_file_regex --grantglob '*' *)
+RE_default=$(_file_regex --grantglob '*' -- *)
 
 # generated files
 RE_FSFUL_configure='(.*/)?configure'
diff -pruN 1.30-1/debian/copyright_hints 2.3-1/debian/copyright_hints
--- 1.30-1/debian/copyright_hints	2022-09-09 14:27:57.000000000 +0000
+++ 2.3-1/debian/copyright_hints	2023-02-04 12:56:38.000000000 +0000
@@ -238,7 +238,7 @@ Copyright:
   2008, Michael Wu <flamingice@sourmilk.net>
   2008-2009, Luis R. Rodriguez <lrodriguez@atheros.com>
   2015-2017, Intel Deutschland GmbH
-  2018-2020, Intel Corporation
+  2018-2022, Intel Corporation
 License: ISC
  FIXME
 
@@ -266,8 +266,8 @@ License: UNKNOWN
 Files:
  debian/copyright-check
 Copyright:
-  2020-2021, Jonas Smedegaard <dr@jones.dk>
   2020-2021, Purism, SPC
+  2020-2023, Jonas Smedegaard <dr@jones.dk>
   K / /mg;'
 License: GPL-3+
  FIXME
diff -pruN 1.30-1/debian/patches/1001_support_traditional_dbus_activation.patch 2.3-1/debian/patches/1001_support_traditional_dbus_activation.patch
--- 1.30-1/debian/patches/1001_support_traditional_dbus_activation.patch	2022-09-09 14:28:05.000000000 +0000
+++ 2.3-1/debian/patches/1001_support_traditional_dbus_activation.patch	2022-11-26 09:06:51.000000000 +0000
@@ -17,7 +17,7 @@ Last-Update: 2021-11-30
 This patch header follows DEP-3: http://dep.debian.net/deps/dep3/
 --- a/Makefile.am
 +++ b/Makefile.am
-@@ -268,7 +268,7 @@
+@@ -271,7 +271,7 @@
  endif
  
  if SYSTEMD_SERVICE
@@ -26,7 +26,7 @@ This patch header follows DEP-3: http://
  
  systemd_unit_DATA += src/iwd.service
  dbus_bus_DATA += src/net.connman.iwd.service
-@@ -354,7 +354,7 @@
+@@ -357,7 +357,7 @@
  endif
  
  if SYSTEMD_SERVICE
@@ -35,7 +35,7 @@ This patch header follows DEP-3: http://
  
  systemd_unit_DATA += wired/ead.service
  dbus_bus_DATA += wired/net.connman.ead.service
-@@ -575,8 +575,8 @@
+@@ -578,8 +578,8 @@
  
  TESTS = $(unit_tests)
  
@@ -46,7 +46,7 @@ This patch header follows DEP-3: http://
  			src/80-iwd.link src/pkcs8.conf unit/gencerts.cnf \
  			doc/main.conf \
  			$(manual_pages) $(patsubst %.1,%.rst, \
-@@ -592,7 +592,8 @@
+@@ -594,7 +594,8 @@
  AM_CFLAGS += -DHAVE_PKCS8_SUPPORT
  endif
  
diff -pruN 1.30-1/ell/asn1-private.h 2.3-1/ell/asn1-private.h
--- 1.30-1/ell/asn1-private.h	2021-03-29 12:19:13.000000000 +0000
+++ 2.3-1/ell/asn1-private.h	2022-11-18 09:08:38.000000000 +0000
@@ -34,6 +34,8 @@
 #define ASN1_ID_UTF8STRING	ASN1_ID(ASN1_CLASS_UNIVERSAL, 0, 0x0c)
 #define ASN1_ID_PRINTABLESTRING	ASN1_ID(ASN1_CLASS_UNIVERSAL, 0, 0x13)
 #define ASN1_ID_IA5STRING	ASN1_ID(ASN1_CLASS_UNIVERSAL, 0, 0x16)
+#define ASN1_ID_UTCTIME		ASN1_ID(ASN1_CLASS_UNIVERSAL, 0, 0x17)
+#define ASN1_ID_GENERALIZEDTIME	ASN1_ID(ASN1_CLASS_UNIVERSAL, 0, 0x18)
 
 struct asn1_oid {
 	uint8_t asn1_len;
diff -pruN 1.30-1/ell/cert.c 2.3-1/ell/cert.c
--- 1.30-1/ell/cert.c	2022-09-07 18:21:45.000000000 +0000
+++ 2.3-1/ell/cert.c	2022-11-18 09:08:38.000000000 +0000
@@ -25,6 +25,7 @@
 #include <string.h>
 #include <stdio.h>
 #include <errno.h>
+#include <time.h>
 
 #include "private.h"
 #include "useful.h"
@@ -33,6 +34,9 @@
 #include "asn1-private.h"
 #include "cipher.h"
 #include "pem-private.h"
+#include "time.h"
+#include "time-private.h"
+#include "utf8.h"
 #include "cert.h"
 #include "cert-private.h"
 #include "tls.h"
@@ -186,6 +190,188 @@ LIB_EXPORT const uint8_t *l_cert_get_dn(
 						-1);
 }
 
+static uint64_t cert_parse_asn1_time(const uint8_t *data, size_t len,
+					uint8_t tag)
+{
+	struct tm tm = {};
+	int tz_hours;
+	int tz_mins;
+	int century;
+	int msecs = 0;
+	time_t tt;
+	unsigned int i;
+
+	for (i = 0; i < len && i < 15; i++)
+		if (unlikely(!l_ascii_isdigit(data[i])))
+			break;
+
+	if (tag == ASN1_ID_UTCTIME) {
+		if (unlikely(!L_IN_SET(i, 10, 12)))
+			return L_TIME_INVALID;
+
+		century = 19;
+	} else if (tag == ASN1_ID_GENERALIZEDTIME) {
+		if (unlikely(!L_IN_SET(i, 10, 12, 14)))
+			return L_TIME_INVALID;
+
+		century = (data[0] - '0') * 10 + (data[1] - '0');
+		if (century < 19)
+				return L_TIME_INVALID;
+
+		if (len >= i + 4 && data[i] == '.') {
+			if (unlikely(!l_ascii_isdigit(data[i + 1]) ||
+						!l_ascii_isdigit(data[i + 2]) ||
+						!l_ascii_isdigit(data[i + 3])))
+				return L_TIME_INVALID;
+
+			i++;
+			msecs += (data[i++] - '0') * 100;
+			msecs += (data[i++] - '0') * 10;
+			msecs += (data[i++] - '0');
+		}
+
+		data += 2;
+		len -= 2;
+		i -= 2;
+	} else
+		return L_TIME_INVALID;
+
+	if (unlikely((len != i + 1 || data[i] != 'Z') &&
+			(len != i + 5 || (data[i] != '+' && data[i] != '-'))))
+		return L_TIME_INVALID;
+
+	tm.tm_year = (data[0] - '0') * 10 + (data[1] - '0');
+	tm.tm_mon = (data[2] - '0') * 10 + (data[3] - '0');
+	tm.tm_mday = (data[4] - '0') * 10 + (data[5] - '0');
+	tm.tm_hour = (data[6] - '0') * 10 + (data[7] - '0');
+
+	if (unlikely(tm.tm_mon < 1 || tm.tm_mon > 12 ||
+				tm.tm_mday < 1 || tm.tm_mday > 31 ||
+				tm.tm_hour > 23))
+		return L_TIME_INVALID;
+
+	if (i >= 10) {
+		tm.tm_min = (data[8] - '0') * 10 + (data[9] - '0');
+		if (unlikely(tm.tm_min > 59))
+			return L_TIME_INVALID;
+	}
+
+	if (i >= 12) {
+		tm.tm_sec = (data[10] - '0') * 10 + (data[11] - '0');
+		if (unlikely(tm.tm_sec > 59))
+			return L_TIME_INVALID;
+	}
+
+	/* RFC5280 Section 4.1.2.5.1 */
+	if (tag == ASN1_ID_UTCTIME && tm.tm_year < 50)
+		century = 20;
+
+	tm.tm_year += (century - 19) * 100;
+
+	/* Month number is 1-based in UTCTime and 0-based in struct tm */
+	tm.tm_mon -= 1;
+
+	tt = timegm(&tm);
+	if (unlikely(tt == (time_t) -1))
+		return L_TIME_INVALID;
+
+	if (len == i + 5) {
+		data += i;
+
+		for (i = 1; i < 5; i++)
+			if (unlikely(!l_ascii_isdigit(data[i])))
+				return L_TIME_INVALID;
+
+		tz_hours = (data[1] - '0') * 10 + (data[2] - '0');
+		tz_mins = (data[3] - '0') * 10 + (data[4] - '0');
+
+		if (unlikely(tz_hours > 14 || tz_mins > 59))
+			return L_TIME_INVALID;
+
+		/* The sign converts UTC to local so invert it */
+		if (data[0] == '+')
+			tt -= tz_hours * 3600 + tz_mins * 60;
+		else
+			tt += tz_hours * 3600 + tz_mins * 60;
+	}
+
+	return (uint64_t) tt * L_USEC_PER_SEC + msecs * L_USEC_PER_MSEC;
+}
+
+LIB_EXPORT bool l_cert_get_valid_times(struct l_cert *cert,
+					uint64_t *out_not_before_time,
+					uint64_t *out_not_after_time)
+{
+	const uint8_t *validity;
+	const uint8_t *not_before;
+	const uint8_t *not_after;
+	size_t seq_size;
+	size_t not_before_size;
+	size_t not_after_size;
+	uint8_t not_before_tag;
+	uint8_t not_after_tag;
+	uint64_t not_before_time = 0;
+	uint64_t not_after_time = 0;
+
+	if (unlikely(!cert))
+		return false;
+
+	validity = asn1_der_find_elem_by_path(cert->asn1, cert->asn1_len,
+						ASN1_ID_SEQUENCE, &seq_size,
+						X509_CERTIFICATE_POS,
+						X509_TBSCERTIFICATE_POS,
+						X509_TBSCERT_VALIDITY_POS,
+						-1);
+	if (unlikely(!validity))
+		return false;
+
+	not_before = asn1_der_find_elem(validity, seq_size, 0, &not_before_tag,
+					&not_before_size);
+	if (!not_before)
+		return false;
+
+	seq_size -= not_before_size + (not_before - validity);
+	validity = not_before + not_before_size;
+	not_after = asn1_der_find_elem(validity, seq_size, 0, &not_after_tag,
+					&not_after_size);
+	if (!not_after)
+		return false;
+
+	if (out_not_before_time) {
+		not_before_time = cert_parse_asn1_time(not_before,
+							not_before_size,
+							not_before_tag);
+		if (not_before_time == L_TIME_INVALID)
+			return false;
+	}
+
+	if (out_not_after_time) {
+		/*
+		 * RFC5280 Section 4.1.2.5: "To indicate that a certificate
+		 * has no well-defined expiration date, the notAfter SHOULD
+		 * be assigned the GeneralizedTime value of 99991231235959Z."
+		 */
+		if (not_after_size == 15 &&
+				!memcmp(not_after, "99991231235959Z", 15))
+			not_after_time = 0;
+		else {
+			not_after_time = cert_parse_asn1_time(not_after,
+								not_after_size,
+								not_after_tag);
+			if (not_after_time == L_TIME_INVALID)
+				return false;
+		}
+	}
+
+	if (out_not_before_time)
+		*out_not_before_time = not_before_time;
+
+	if (out_not_after_time)
+		*out_not_after_time = not_after_time;
+
+	return true;
+}
+
 const uint8_t *cert_get_extension(struct l_cert *cert,
 					const struct asn1_oid *ext_id,
 					bool *out_critical, size_t *out_len)
@@ -377,24 +563,25 @@ LIB_EXPORT void l_certchain_walk_from_ca
 			break;
 }
 
-static struct l_keyring *cert_set_to_keyring(struct l_queue *certs, char *error)
+static struct l_keyring *cert_set_to_keyring(struct l_cert **certs, char *error)
 {
 	struct l_keyring *ring;
-	const struct l_queue_entry *entry;
 	int i = 1;
+	int count;
 
 	ring = l_keyring_new();
 	if (!ring)
 		return NULL;
 
-	for (entry = l_queue_get_entries(certs); entry; entry = entry->next) {
-		struct l_cert *cert = entry->data;
+	for (count = 0; certs[count]; count++);
+
+	for (; *certs; certs++) {
+		struct l_cert *cert = *certs;
 		struct l_key *key = l_cert_get_pubkey(cert);
 
 		if (!key) {
 			sprintf(error, "Can't get public key from certificate "
-				"%i / %i in certificate set", i,
-				l_queue_length(certs));
+				"%i / %i in certificate set", i, count);
 			goto cleanup;
 		}
 
@@ -402,7 +589,7 @@ static struct l_keyring *cert_set_to_key
 			l_key_free(key);
 			sprintf(error, "Can't link the public key from "
 				"certificate %i / %i to target keyring",
-				i, l_queue_length(certs));
+				i, count);
 			goto cleanup;
 		}
 
@@ -417,12 +604,10 @@ cleanup:
 	return NULL;
 }
 
-static bool cert_is_in_set(struct l_cert *cert, struct l_queue *set)
+static bool cert_is_in_set(struct l_cert *cert, struct l_cert **set)
 {
-	const struct l_queue_entry *entry;
-
-	for (entry = l_queue_get_entries(set); entry; entry = entry->next) {
-		struct l_cert *cert2 = entry->data;
+	for (; *set; set++) {
+		struct l_cert *cert2 = *set;
 
 		if (cert == cert2)
 			return true;
@@ -436,6 +621,35 @@ static bool cert_is_in_set(struct l_cert
 	return false;
 }
 
+static struct l_cert **cert_set_filter_by_validity(struct l_queue *set,
+							uint64_t now,
+							int *out_total,
+							int *out_valid)
+{
+	const struct l_queue_entry *entry;
+	_auto_(l_free) struct l_cert **valid;
+
+	*out_total = l_queue_length(set);
+	*out_valid = 0;
+	valid = l_new(struct l_cert *, *out_total + 1);
+
+	for (entry = l_queue_get_entries(set); entry; entry = entry->next) {
+		struct l_cert *cert = entry->data;
+		uint64_t not_before;
+		uint64_t not_after;
+
+		if (!l_cert_get_valid_times(cert, &not_before, &not_after))
+			return NULL;
+
+		if (now < not_before || (not_after && now > not_after))
+			continue;
+
+		valid[(*out_valid)++] = cert;
+	}
+
+	return l_steal_ptr(valid);
+}
+
 static struct l_key *cert_try_link(struct l_cert *cert, struct l_keyring *ring)
 {
 	struct l_key *key;
@@ -470,22 +684,81 @@ LIB_EXPORT bool l_certchain_verify(struc
 	struct l_key *prev_key = NULL;
 	int verified = 0;
 	int ca_match = 0;
-	int i = 0;
-	static char error_buf[200];
+	int i;
+	static char error_buf[1024];
+	int total = 0;
+	uint64_t now;
+	_auto_(l_free) struct l_cert **ca_certs_valid = NULL;
+	int ca_certs_total_count = 0;
+	int ca_certs_valid_count = 0;
 
 	if (unlikely(!chain || !chain->leaf))
 		RETURN_ERROR("Chain empty");
 
+	for (cert = chain->ca; cert; cert = cert->issued, total++);
+
+	now = time_realtime_now();
+
+	for (cert = chain->ca, i = 0; cert; cert = cert->issued, i++) {
+		uint64_t not_before;
+		uint64_t not_after;
+		char time_str[100];
+
+		if (unlikely(!l_cert_get_valid_times(cert, &not_before,
+							&not_after)))
+			RETURN_ERROR("Can't parse validity in certificate "
+					"%i / %i", i + 1, total);
+
+		if (unlikely(now < not_before)) {
+			time_t t = not_before / L_USEC_PER_SEC;
+			struct tm *tm = gmtime(&t);
+
+			if (!tm || !strftime(time_str, sizeof(time_str),
+						"%a %F %T UTC", tm))
+				strcpy(time_str, "<error>");
+
+			RETURN_ERROR("Certificate %i / %i not valid before %s",
+					i + 1, total, time_str);
+		}
+
+		if (unlikely(not_after && now > not_after)) {
+			time_t t = not_after / L_USEC_PER_SEC;
+			struct tm *tm = gmtime(&t);
+
+			if (!tm || !strftime(time_str, sizeof(time_str),
+						"%a %F %T UTC", tm))
+				strcpy(time_str, "<error>");
+
+			RETURN_ERROR("Certificate %i / %i expired on %s",
+					i + 1, total, time_str);
+		}
+	}
+
+	if (ca_certs) {
+		if (unlikely(l_queue_isempty(ca_certs)))
+			RETURN_ERROR("No trusted CA certificates");
+
+		ca_certs_valid = cert_set_filter_by_validity(ca_certs, now,
+							&ca_certs_total_count,
+							&ca_certs_valid_count);
+		if (unlikely(!ca_certs_valid))
+			RETURN_ERROR("Can't parse validity in CA cert(s)");
+
+		if (unlikely(!ca_certs_valid_count))
+			RETURN_ERROR("All trusted CA certs are expired or "
+					"not-yet-valid");
+
+		for (cert = chain->ca, i = 0; cert; cert = cert->issued, i++)
+			if (cert_is_in_set(cert, ca_certs_valid)) {
+				ca_match = i + 1;
+				break;
+			}
+	}
+
 	verify_ring = l_keyring_new();
 	if (!verify_ring)
 		RETURN_ERROR("Can't create verify keyring");
 
-	for (cert = chain->ca; cert; cert = cert->issued, i++)
-		if (cert_is_in_set(cert, ca_certs)) {
-			ca_match = i + 1;
-			break;
-		}
-
 	cert = chain->ca;
 
 	/*
@@ -505,7 +778,7 @@ LIB_EXPORT bool l_certchain_verify(struc
 	 * all of the trusted certificates into the kernel, link them
 	 * to @ca_ring or link @ca_ring to @verify_ring, instead we
 	 * load the first certificate into @verify_ring before we set
-	 * the restric mode on it, same as when no trusted CAs are
+	 * the restrict mode on it, same as when no trusted CAs are
 	 * provided.
 	 *
 	 * Note this happens to work around a kernel issue preventing
@@ -516,7 +789,7 @@ LIB_EXPORT bool l_certchain_verify(struc
 	 * the chain.
 	 */
 	if (ca_certs && !ca_match) {
-		ca_ring = cert_set_to_keyring(ca_certs, error_buf);
+		ca_ring = cert_set_to_keyring(ca_certs_valid, error_buf);
 		if (!ca_ring) {
 			if (error)
 				*error = error_buf;
@@ -569,23 +842,28 @@ LIB_EXPORT bool l_certchain_verify(struc
 	}
 
 	if (!prev_key) {
-		int total = 0;
-		char str[100];
-
-		for (cert = chain->ca; cert; cert = cert->issued, total++);
+		char str1[100];
+		char str2[100] = "";
 
 		if (ca_match)
-			snprintf(str, sizeof(str), "%i / %i matched a trusted "
-					"certificate, root not verified",
+			snprintf(str1, sizeof(str1), "%i / %i matched a trusted"
+					" certificate, root not verified",
 					ca_match, total);
 		else
-			snprintf(str, sizeof(str), "root %sverified against "
+			snprintf(str1, sizeof(str1), "root %sverified against "
 					"trusted CA(s)",
-					ca_certs && !ca_match && verified ? "" :
-					"not ");
+					ca_certs && verified ? "" : "not ");
+
+		if (ca_certs && !ca_match && !verified &&
+				ca_certs_valid_count < ca_certs_total_count)
+			snprintf(str2, sizeof(str2), ", %i out of %i trused "
+					"CA(s) were expired or not-yet-valid",
+					ca_certs_total_count -
+					ca_certs_valid_count,
+					ca_certs_total_count);
 
-		RETURN_ERROR("Linking certificate %i / %i failed, %s",
-				verified + 1, total, str);
+		RETURN_ERROR("Linking certificate %i / %i failed, %s%s",
+				verified + 1, total, str1, str2);
 	}
 
 	l_key_free(prev_key);
diff -pruN 1.30-1/ell/cert.h 2.3-1/ell/cert.h
--- 1.30-1/ell/cert.h	2022-09-07 18:21:45.000000000 +0000
+++ 2.3-1/ell/cert.h	2022-11-18 09:08:38.000000000 +0000
@@ -48,6 +48,8 @@ DEFINE_CLEANUP_FUNC(l_cert_free);
 
 const uint8_t *l_cert_get_der_data(struct l_cert *cert, size_t *out_len);
 const uint8_t *l_cert_get_dn(struct l_cert *cert, size_t *out_len);
+bool l_cert_get_valid_times(struct l_cert *cert, uint64_t *out_not_before_time,
+				uint64_t *out_not_after_time);
 enum l_cert_key_type l_cert_get_pubkey_type(struct l_cert *cert);
 struct l_key *l_cert_get_pubkey(struct l_cert *cert);
 
diff -pruN 1.30-1/ell/checksum.c 2.3-1/ell/checksum.c
--- 1.30-1/ell/checksum.c	2021-03-29 12:19:13.000000000 +0000
+++ 2.3-1/ell/checksum.c	2022-12-18 19:40:29.000000000 +0000
@@ -146,55 +146,22 @@ static int create_alg(const char *alg)
 	return sk;
 }
 
-/**
- * l_checksum_new:
- * @type: checksum type
- *
- * Creates new #l_checksum, using the checksum algorithm @type.
- *
- * Returns: a newly allocated #l_checksum object.
- **/
-LIB_EXPORT struct l_checksum *l_checksum_new(enum l_checksum_type type)
-{
-	struct l_checksum *checksum;
-	int fd;
-
-	if (!is_valid_index(checksum_algs, type) || !checksum_algs[type].name)
-		return NULL;
-
-	checksum = l_new(struct l_checksum, 1);
-	checksum->alg_info = &checksum_algs[type];
-
-	fd = create_alg(checksum->alg_info->name);
-	if (fd < 0)
-		goto error;
-
-	checksum->sk = accept4(fd, NULL, 0, SOCK_CLOEXEC);
-	close(fd);
-
-	if (checksum->sk < 0)
-		goto error;
-
-	return checksum;
-
-error:
-	l_free(checksum);
-	return NULL;
-}
-
-LIB_EXPORT struct l_checksum *l_checksum_new_cmac_aes(const void *key,
-							size_t key_len)
+static struct l_checksum *checksum_new_common(const char *alg, int sockopt,
+						const void *data, size_t len,
+						struct checksum_info *info)
 {
 	struct l_checksum *checksum;
 	int fd;
 
-	fd = create_alg("cmac(aes)");
+	fd = create_alg(alg);
 	if (fd < 0)
 		return NULL;
 
-	if (setsockopt(fd, SOL_ALG, ALG_SET_KEY, key, key_len) < 0) {
-		close(fd);
-		return NULL;
+	if (data) {
+		if (setsockopt(fd, SOL_ALG, sockopt, data, len) < 0) {
+			close(fd);
+			return NULL;
+		}
 	}
 
 	checksum = l_new(struct l_checksum, 1);
@@ -206,40 +173,44 @@ LIB_EXPORT struct l_checksum *l_checksum
 		return NULL;
 	}
 
-	checksum->alg_info = &checksum_cmac_aes_alg;
+	checksum->alg_info = info;
 	return checksum;
 }
 
-LIB_EXPORT struct l_checksum *l_checksum_new_hmac(enum l_checksum_type type,
-					  const void *key, size_t key_len)
+/**
+ * l_checksum_new:
+ * @type: checksum type
+ *
+ * Creates new #l_checksum, using the checksum algorithm @type.
+ *
+ * Returns: a newly allocated #l_checksum object.
+ **/
+LIB_EXPORT struct l_checksum *l_checksum_new(enum l_checksum_type type)
 {
-	struct l_checksum *checksum;
-	int fd;
-
-	if (!is_valid_index(checksum_hmac_algs, type) ||
-			!checksum_hmac_algs[type].name)
-		return NULL;
-
-	fd = create_alg(checksum_hmac_algs[type].name);
-	if (fd < 0)
+	if (!is_valid_index(checksum_algs, type) || !checksum_algs[type].name)
 		return NULL;
 
-	if (setsockopt(fd, SOL_ALG, ALG_SET_KEY, key, key_len) < 0) {
-		close(fd);
-		return NULL;
-	}
+	return checksum_new_common(checksum_algs[type].name, 0, NULL, 0,
+					&checksum_algs[type]);
+}
 
-	checksum = l_new(struct l_checksum, 1);
-	checksum->sk = accept4(fd, NULL, 0, SOCK_CLOEXEC);
-	close(fd);
+LIB_EXPORT struct l_checksum *l_checksum_new_cmac_aes(const void *key,
+							size_t key_len)
+{
+	return checksum_new_common("cmac(aes)", ALG_SET_KEY, key, key_len,
+					&checksum_cmac_aes_alg);
+}
 
-	if (checksum->sk < 0) {
-		l_free(checksum);
+LIB_EXPORT struct l_checksum *l_checksum_new_hmac(enum l_checksum_type type,
+					const void *key, size_t key_len)
+{
+	if (!is_valid_index(checksum_hmac_algs, type) ||
+			!checksum_hmac_algs[type].name)
 		return NULL;
-	}
 
-	checksum->alg_info = &checksum_hmac_algs[type];
-	return checksum;
+	return checksum_new_common(checksum_hmac_algs[type].name,
+					ALG_SET_KEY, key, key_len,
+					&checksum_hmac_algs[type]);
 }
 
 /**
diff -pruN 1.30-1/ell/dhcp6.c 2.3-1/ell/dhcp6.c
--- 1.30-1/ell/dhcp6.c	2022-04-21 14:47:16.000000000 +0000
+++ 2.3-1/ell/dhcp6.c	2022-11-18 09:08:38.000000000 +0000
@@ -971,8 +971,9 @@ static void dhcp6_client_setup_lease(str
 	client->lease->start_time = timestamp;
 
 	/* TODO: Emit IP_CHANGED if any addresses were removed / added */
-	if (client->state == DHCP6_STATE_REQUESTING ||
-			client->state == DHCP6_STATE_SOLICITING)
+	if (L_IN_SET(client->state, DHCP6_STATE_REQUESTING,
+				DHCP6_STATE_SOLICITING,
+				DHCP6_STATE_REQUESTING_INFORMATION))
 		event = L_DHCP6_CLIENT_EVENT_LEASE_OBTAINED;
 	else
 		event = L_DHCP6_CLIENT_EVENT_LEASE_RENEWED;
@@ -1097,6 +1098,7 @@ bool _dhcp6_option_iter_next(struct dhcp
 }
 
 static int dhcp6_client_validate_message(struct l_dhcp6_client *client,
+					bool expect_client_id,
 					const struct dhcp6_message *message,
 					size_t len)
 {
@@ -1204,7 +1206,7 @@ static int dhcp6_client_validate_message
 		}
 	}
 
-	if (!duid_verified) {
+	if (expect_client_id && !duid_verified) {
 		CLIENT_DEBUG("Message %s - no client id option found", mstr);
 		return -EBADMSG;
 	}
@@ -1228,7 +1230,7 @@ static int dhcp6_client_receive_advertis
 	if (advertise->msg_type != DHCP6_MESSAGE_TYPE_ADVERTISE)
 		return -EINVAL;
 
-	r = dhcp6_client_validate_message(client, advertise, len);
+	r = dhcp6_client_validate_message(client, true, advertise, len);
 	if (r < 0)
 		return r;
 
@@ -1310,11 +1312,17 @@ static int dhcp6_client_receive_reply(st
 	struct l_dhcp6_lease *lease;
 	struct dhcp6_option_iter iter;
 	int r;
+	/*
+	 * Per RFC 7844 Section 4.3.1 we never send Client ID options in
+	 * Information-requests so don't expect the replies to contain them.
+	 */
+	bool expect_client_id =
+		(client->state != DHCP6_STATE_REQUESTING_INFORMATION);
 
 	if (reply->msg_type != DHCP6_MESSAGE_TYPE_REPLY)
 		return -EINVAL;
 
-	r = dhcp6_client_validate_message(client, reply, len);
+	r = dhcp6_client_validate_message(client, expect_client_id, reply, len);
 	if (r < 0)
 		return r;
 
@@ -1385,7 +1393,8 @@ static void dhcp6_client_rx_message(cons
 	case DHCP6_STATE_BOUND:
 		return;
 	case DHCP6_STATE_REQUESTING_INFORMATION:
-		if (dhcp6_client_receive_reply(client, message, len) < 0)
+		r = dhcp6_client_receive_reply(client, message, len);
+		if (r < 0)
 			return;
 
 		break;
@@ -1473,6 +1482,9 @@ static void dhcp6_client_icmp6_event(str
 {
 	struct l_dhcp6_client *client = user_data;
 
+	if (client->nora)
+		return;
+
 	switch (event) {
 	case L_ICMP6_CLIENT_EVENT_ROUTER_FOUND:
 	{
@@ -1524,7 +1536,6 @@ LIB_EXPORT struct l_dhcp6_client *l_dhcp
 
 	client->state = DHCP6_STATE_INIT;
 	client->ifindex = ifindex;
-	client->request_na = true;
 
 	client->icmp6 = l_icmp6_client_new(ifindex);
 	l_icmp6_client_add_event_handler(client->icmp6,
@@ -1619,6 +1630,9 @@ LIB_EXPORT bool l_dhcp6_client_set_link_
 	if (inet_pton(AF_INET6, ll, &client->ll_address) != 1)
 		return false;
 
+	if (!client->nora)
+		l_icmp6_client_set_link_local_address(client->icmp6, ll, false);
+
 	return true;
 }
 
@@ -1797,6 +1811,8 @@ LIB_EXPORT bool l_dhcp6_client_start(str
 	else
 		client_duid_generate_addr_plus_time(client);
 
+	client->request_na = !client->stateless;
+
 	if (!client->transport) {
 		client->transport =
 			_dhcp6_default_transport_new(client->ifindex,
diff -pruN 1.30-1/ell/dhcp6-lease.c 2.3-1/ell/dhcp6-lease.c
--- 1.30-1/ell/dhcp6-lease.c	2022-04-21 14:47:16.000000000 +0000
+++ 2.3-1/ell/dhcp6-lease.c	2022-11-18 09:08:38.000000000 +0000
@@ -312,7 +312,7 @@ struct l_dhcp6_lease *_dhcp6_lease_parse
 			lease->rapid_commit = true;
 			break;
 		case L_DHCP6_OPTION_DOMAIN_LIST:
-			lease->domain_list = net_domain_list_parse(v, l);
+			lease->domain_list = net_domain_list_parse(v, l, false);
 			if (!lease->domain_list)
 				goto error;
 
diff -pruN 1.30-1/ell/dhcp.c 2.3-1/ell/dhcp.c
--- 1.30-1/ell/dhcp.c	2022-07-15 16:22:44.000000000 +0000
+++ 2.3-1/ell/dhcp.c	2022-12-18 19:40:29.000000000 +0000
@@ -365,12 +365,27 @@ static int dhcp_client_send_unicast(stru
 					unsigned int len)
 {
 	struct sockaddr_in si;
+	int r;
 
 	memset(&si, 0, sizeof(si));
 	si.sin_family = AF_INET;
 	si.sin_port = L_CPU_TO_BE16(DHCP_PORT_SERVER);
 	si.sin_addr.s_addr = client->lease->server_address;
-	return client->transport->send(client->transport, &si, request, len);
+
+	/*
+	 * sendto() might fail with an EPERM error, which most likely means
+	 * that the unicast was prevented by netfilter.  Ignore this case
+	 * and assume that once the REBINDING timeout is hit, a broadcast
+	 * will go through which will have a chance of renewing the lease
+	 */
+	r = client->transport->send(client->transport, &si, request, len);
+	if (r == -EPERM) {
+		CLIENT_DEBUG("transport->send() failed with EPERM -> ignore");
+		CLIENT_DEBUG("Is a firewall denying unicast DHCP packets?");
+		return 0;
+	}
+
+	return r;
 }
 
 static int dhcp_client_send_request(struct l_dhcp_client *client)
@@ -820,6 +835,7 @@ static void dhcp_client_rx_message(const
 	const void *v;
 	int r, e;
 	struct in_addr ia;
+	enum l_dhcp_client_event event = L_DHCP_CLIENT_EVENT_LEASE_EXPIRED;
 
 	CLIENT_DEBUG("");
 
@@ -874,6 +890,8 @@ static void dhcp_client_rx_message(const
 			dhcp_client_handle_offer(client, message, len);
 			return;
 		}
+
+		event = L_DHCP_CLIENT_EVENT_NO_LEASE;
 		/* Fall through */
 	case DHCP_STATE_RENEWING:
 	case DHCP_STATE_REBINDING:
@@ -882,8 +900,7 @@ static void dhcp_client_rx_message(const
 			CLIENT_INFO("Received NAK, Stopping...");
 			l_dhcp_client_stop(client);
 
-			dhcp_client_event_notify(client,
-					L_DHCP_CLIENT_EVENT_NO_LEASE);
+			dhcp_client_event_notify(client, event);
 			return;
 		}
 
diff -pruN 1.30-1/ell/dhcp-transport.c 2.3-1/ell/dhcp-transport.c
--- 1.30-1/ell/dhcp-transport.c	2022-07-15 16:22:44.000000000 +0000
+++ 2.3-1/ell/dhcp-transport.c	2022-11-18 09:08:38.000000000 +0000
@@ -389,18 +389,8 @@ static int kernel_raw_socket_open(uint32
 		BPF_STMT(BPF_RET + BPF_K, 0),
 		/* A <- IP version + Header length */
 		BPF_STMT(BPF_LD + BPF_B + BPF_ABS, 0),
-		/* A <- A & 0xf0 (Mask off version */
-		BPF_STMT(BPF_ALU + BPF_AND + BPF_K, 0xf0),
-		/* A == IPVERSION (shifted left 4) ? */
-		BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, IPVERSION << 4, 1, 0),
-		/* ignore */
-		BPF_STMT(BPF_RET + BPF_K, 0),
-		/* A <- IP version + Header length */
-		BPF_STMT(BPF_LD + BPF_B + BPF_ABS, 0),
-		/* A <- A & 0x0f (Mask off IP Header Length */
-		BPF_STMT(BPF_ALU + BPF_AND + BPF_K, 0x0f),
-		/* A == 5 ? */
-		BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 5, 1, 0),
+		/* IP version == IPVERSION && Header length == 5 ? */
+		BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, (IPVERSION << 4) | 5, 1, 0),
 		/* ignore */
 		BPF_STMT(BPF_RET + BPF_K, 0),
 		/* A <- IP protocol */
diff -pruN 1.30-1/ell/icmp6.c 2.3-1/ell/icmp6.c
--- 1.30-1/ell/icmp6.c	2022-07-15 16:22:44.000000000 +0000
+++ 2.3-1/ell/icmp6.c	2022-11-18 09:08:38.000000000 +0000
@@ -24,6 +24,7 @@
 #include <config.h>
 #endif
 
+#define _GNU_SOURCE
 #include <sys/socket.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
@@ -31,12 +32,16 @@
 #include <netinet/icmp6.h>
 #include <linux/ipv6.h>
 #include <linux/rtnetlink.h>
+#include <linux/if_packet.h>
+#include <linux/filter.h>
 #include <sys/types.h>
 #include <sys/ioctl.h>
 #include <net/if.h>
+#include <net/ethernet.h>
 #include <unistd.h>
 #include <errno.h>
 #include <sys/time.h>
+#include <stddef.h>
 
 #include "private.h"
 #include "useful.h"
@@ -50,6 +55,7 @@
 #include "netlink.h"
 #include "rtnl.h"
 #include "missing.h"
+#include "utf8.h"
 #include "icmp6.h"
 #include "icmp6-private.h"
 
@@ -58,6 +64,14 @@
 #define ND_OPT_ROUTE_INFORMATION	24
 #endif
 
+/* RFC8106 */
+#ifndef ND_OPT_RECURSIVE_DNS_SERVER
+#define ND_OPT_RECURSIVE_DNS_SERVER	25
+#endif
+#ifndef ND_OPT_DNS_SEARCH_LIST
+#define ND_OPT_DNS_SEARCH_LIST		31
+#endif
+
 #define CLIENT_DEBUG(fmt, args...)					\
 	l_util_debug(client->debug_handler, client->debug_data,		\
 			"%s:%i " fmt, __func__, __LINE__, ## args)
@@ -66,135 +80,193 @@
 			{ { { 0xff,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1 } } }
 #define IN6ADDR_LINKLOCAL_ALLROUTERS_INIT \
 			{ { { 0xff,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2 } } }
+#define LLADDR_LINKLOCAL_ALLNODES_INIT	\
+			{ 0x33,0x33,0,0,0,1 }
+#define LLADDR_LINKLOCAL_ALLROUTERS_INIT \
+			{ 0x33,0x33,0,0,0,2 }
 
-static const struct in6_addr in6addr_linklocal_allnodes_init =
-					IN6ADDR_LINKLOCAL_ALLNODES_INIT;
-
-static int add_mreq(int s, int ifindex, const struct in6_addr *mc_addr)
-{
-	struct ipv6_mreq mreq = {
-		.ipv6mr_interface = ifindex,
-		.ipv6mr_multiaddr = *mc_addr,
-	};
-
-	return setsockopt(s, IPPROTO_IPV6,
-				IPV6_JOIN_GROUP, &mreq, sizeof(mreq));
-}
-
-static int icmp6_open_router_common(const struct icmp6_filter *filter,
-					int ifindex)
+static int icmp6_open_router_solicitation(int ifindex)
 {
 	int s;
-	int r;
-	int yes = 1;
-	int no = 0;
-	int nhops = 255;
+	struct sockaddr_ll addr;
+	struct sock_filter filter[] = {
+		/* A <- packet length */
+		BPF_STMT(BPF_LD | BPF_W | BPF_LEN, 0),
+		/* A >= sizeof(nd_router_advert) ? */
+		BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, sizeof(struct ip6_hdr) +
+				sizeof(struct nd_router_advert), 1, 0),
+		/* ignore */
+		BPF_STMT(BPF_RET | BPF_K, 0),
+		/* A <- IP version + Traffic class */
+		BPF_STMT(BPF_LD | BPF_B | BPF_ABS, 0),
+		/* A <- A & 0xf0 (Mask off version) */
+		BPF_STMT(BPF_ALU | BPF_AND | BPF_K, 0xf0),
+		/* A == IPv6 ? */
+		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 6 << 4, 1, 0),
+		/* ignore */
+		BPF_STMT(BPF_RET | BPF_K, 0),
+		/* A <- Next Header */
+		BPF_STMT(BPF_LD | BPF_B | BPF_ABS,
+				offsetof(struct ip6_hdr, ip6_nxt)),
+		/* A == ICMPv6 ? */
+		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_ICMPV6, 1, 0),
+		/* ignore */
+		BPF_STMT(BPF_RET | BPF_K, 0),
+		/* A <- ICMPv6 Type */
+		BPF_STMT(BPF_LD | BPF_B | BPF_ABS, sizeof(struct ip6_hdr) +
+				offsetof(struct icmp6_hdr, icmp6_type)),
+		/* A == Router Advertisement ? */
+		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ND_ROUTER_ADVERT, 1, 0),
+		/* ignore */
+		BPF_STMT(BPF_RET | BPF_K, 0),
+		/* A <- Payload Length */
+		BPF_STMT(BPF_LD | BPF_H | BPF_ABS,
+				offsetof(struct ip6_hdr, ip6_plen)),
+		/* A >= sizeof(nd_router_advert) ? */
+		BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K,
+				sizeof(struct nd_router_advert), 1, 0),
+		/* ignore */
+		BPF_STMT(BPF_RET | BPF_K, 0),
+		/* return all */
+		BPF_STMT(BPF_RET | BPF_K, 65535),
+	};
+	const struct sock_fprog fprog = {
+		.len = L_ARRAY_SIZE(filter),
+		.filter = filter
+	};
+	int one = 1;
 
-	s = socket(AF_INET6, SOCK_RAW | SOCK_CLOEXEC, IPPROTO_ICMPV6);
+	s = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC, htons(ETH_P_IPV6));
 	if (s < 0)
 		return -errno;
 
-	r = setsockopt(s, IPPROTO_ICMPV6,
-			ICMP6_FILTER, filter, sizeof(struct icmp6_filter));
-	if (r < 0)
-		goto fail;
+	if (setsockopt(s, SOL_SOCKET, SO_ATTACH_FILTER,
+						&fprog, sizeof(fprog)) < 0)
+		goto error;
+
+	if (setsockopt(s, SOL_SOCKET, SO_TIMESTAMP, &one, sizeof(one)) < 0)
+		goto error;
+
+	memset(&addr, 0, sizeof(addr));
+	addr.sll_family = AF_PACKET;
+	addr.sll_protocol = htons(ETH_P_IPV6);
+	addr.sll_ifindex = ifindex;
 
-	r = setsockopt(s, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &no, sizeof(no));
-	if (r < 0)
-		goto fail;
-
-	r = setsockopt(s, IPPROTO_IPV6,
-				IPV6_MULTICAST_IF, &ifindex, sizeof(ifindex));
-	if (r < 0)
-		goto fail;
-
-	r = setsockopt(s, IPPROTO_IPV6,
-				IPV6_RECVHOPLIMIT, &yes, sizeof(yes));
-	if (r < 0)
-		goto fail;
-
-	r = setsockopt(s, IPPROTO_IPV6, IPV6_MULTICAST_HOPS,
-							&nhops, sizeof(nhops));
-	if (r < 0)
-		goto fail;
-
-	r = setsockopt(s, SOL_SOCKET, SO_BINDTOIFINDEX,
-						&ifindex, sizeof(ifindex));
-	if (r < 0 && errno == ENOPROTOOPT) {
-		struct ifreq ifr = {
-			.ifr_ifindex = ifindex,
-		};
-
-		r = ioctl(s, SIOCGIFNAME, &ifr);
-		if (r < 0)
-			goto fail;
-
-		r = setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE,
-				ifr.ifr_name, strlen(ifr.ifr_name) + 1);
-	}
-
-	if (r < 0)
-		goto fail;
-
-	r = setsockopt(s, SOL_SOCKET, SO_TIMESTAMP, &yes, sizeof(yes));
-	if (r < 0)
-		goto fail;
+	if (bind(s, (struct sockaddr *) &addr, sizeof(addr)) < 0)
+		goto error;
 
 	return s;
 
-fail:
-	close(s);
+error:
+	L_TFR(close(s));
 	return -errno;
 }
 
-static int icmp6_open_router_solicitation(int ifindex)
+static uint16_t icmp6_checksum(const struct iovec *iov, unsigned int iov_len)
 {
-	struct icmp6_filter filter;
-	int s;
-	int r;
-
-	ICMP6_FILTER_SETBLOCKALL(&filter);
-	ICMP6_FILTER_SETPASS(ND_ROUTER_ADVERT, &filter);
+	const struct ip6_hdr *ip_hdr = iov[0].iov_base;
+	uint32_t sum = 0;
+	const uint16_t *ptr;
+	const uint16_t *buf_end;
+	/* Skip the real IPv6 header */
+	unsigned int buf_offset = sizeof(struct ip6_hdr);
 
-	s = icmp6_open_router_common(&filter, ifindex);
-	if (s < 0)
-		return s;
+	/*
+	 * ICMPv6 checksum according to RFC 4443 Section 2.3, this includes
+	 * the IPv6 payload + the IPv6 pseudo-header according to RFC 2460
+	 * Section 8.1, i.e. the two IPv6 addresses + the payload length +
+	 * the header type.  The caller must ensure that the IPv6 header is
+	 * all in one buffer and that all buffer starts and lengths are
+	 * 16-bit-aligned.
+	 *
+	 * We can skip all zero words such as the upper 16 bits of the
+	 * payload length.  No need to byteswap as the carry bits from
+	 * either byte (high or low) accumulate in the other byte in
+	 * exactly the same way.
+	 */
+	buf_end = (void *) &ip_hdr->ip6_src + 32;
+	for (ptr = (void *) &ip_hdr->ip6_src; ptr < buf_end; )
+		sum += *ptr++;
+
+	sum += ip_hdr->ip6_plen + htons(ip_hdr->ip6_nxt);
+
+	for (; iov_len; iov++, iov_len--) {
+		buf_end = iov->iov_base + iov->iov_len;
+		for (ptr = iov->iov_base + buf_offset; ptr < buf_end; )
+			sum += *ptr++;
 
-	r = add_mreq(s, ifindex, &in6addr_linklocal_allnodes_init);
-	if (r < 0) {
-		close(s);
-		return -errno;
+		buf_offset = 0;
 	}
 
-	return s;
+	while (sum >> 16)
+		sum = (sum & 0xffff) + (sum >> 16);
+
+	return ~sum;
 }
 
-static int icmp6_send_router_solicitation(int s, const uint8_t mac[static 6])
+static int icmp6_send_router_solicitation(int s, int ifindex,
+					const uint8_t src_mac[static 6],
+					const struct in6_addr *src_ip,
+					bool src_ip_optimistic)
 {
-	struct sockaddr_in6 dst = {
-		.sin6_family = AF_INET6,
-		.sin6_addr = IN6ADDR_LINKLOCAL_ALLROUTERS_INIT,
-	};
 	struct nd_router_solicit rs = {
 		.nd_rs_type = ND_ROUTER_SOLICIT,
+		.nd_rs_code = 0,
 	};
-	struct nd_opt_hdr rs_opt = {
+	struct nd_opt_hdr rs_sllao = {
 		.nd_opt_type = ND_OPT_SOURCE_LINKADDR,
 		.nd_opt_len = 1,
 	};
-	struct iovec iov[3] = {
+	const size_t rs_sllao_size = sizeof(rs_sllao) + 6;
+	struct ip6_hdr ip_hdr = {
+		.ip6_flow = htonl(6 << 28),
+		.ip6_hops = 255,
+		.ip6_nxt = IPPROTO_ICMPV6,
+		.ip6_plen = htons(sizeof(rs) + rs_sllao_size),
+		.ip6_dst = IN6ADDR_LINKLOCAL_ALLROUTERS_INIT,
+	};
+	struct iovec iov[4] = {
+		{ .iov_base = &ip_hdr, .iov_len = sizeof(ip_hdr) },
 		{ .iov_base = &rs, .iov_len = sizeof(rs) },
-		{ .iov_base = &rs_opt, .iov_len = sizeof(rs_opt) },
-		{ .iov_base = (void *) mac, .iov_len = 6 } };
+		{ .iov_base = &rs_sllao, .iov_len = sizeof(rs_sllao) },
+		{ .iov_base = (void *) src_mac, .iov_len = 6 } };
 
+	struct sockaddr_ll dst = {
+		.sll_family = AF_PACKET,
+		.sll_protocol = htons(ETH_P_IPV6),
+		.sll_ifindex = ifindex,
+		.sll_addr = LLADDR_LINKLOCAL_ALLROUTERS_INIT,
+		.sll_halen = 6,
+	};
 	struct msghdr msg = {
 		.msg_name = &dst,
 		.msg_namelen = sizeof(dst),
 		.msg_iov = iov,
-		.msg_iovlen = 3,
+		.msg_iovlen = L_ARRAY_SIZE(iov),
 	};
 	int r;
 
+	memcpy(&ip_hdr.ip6_src, src_ip, 16);
+
+	if (l_memeqzero(src_ip, 16) || src_ip_optimistic) {
+		/*
+		 * RFC 4429 Section 3.2: "A node MUST NOT send a Router
+		 * Solicitation with a SLLAO from an Optimistic Address.
+		 * Router Solicitations SHOULD be sent from a non-Optimistic
+		 * or the Unspecified Address; however, they MAY be sent from
+		 * an Optimistic Address as long as the SLLAO is not included."
+		 *
+		 * Additionally radvd will also discard and warn about RSs
+		 * from the unspecified address with the SLLAO.  Omit that
+		 * option by dropping the last two iov buffers.
+		 */
+		msg.msg_iovlen -= 2;
+		ip_hdr.ip6_plen = htons(ntohs(ip_hdr.ip6_plen) - rs_sllao_size);
+	}
+
+	/* Don't byteswap the checksum */
+	rs.nd_rs_cksum = icmp6_checksum(msg.msg_iov, msg.msg_iovlen);
+
 	r = sendmsg(s, &msg, 0);
 	if (r < 0)
 		return -errno;
@@ -202,22 +274,23 @@ static int icmp6_send_router_solicitatio
 	return 0;
 }
 
-static int icmp6_receive(int s, void *buf, size_t buf_len, struct in6_addr *src,
-				uint64_t *out_timestamp)
+static int icmp6_receive(int s, void *buf, ssize_t *buf_len,
+				struct in6_addr *src, uint64_t *out_timestamp)
 {
 	char c_msg_buf[CMSG_SPACE(sizeof(int)) +
 			CMSG_SPACE(sizeof(struct timeval))];
-	struct iovec iov = {
-		.iov_base = buf,
-		.iov_len = buf_len,
+	struct ip6_hdr ip_hdr;
+	struct iovec iov[2] = {
+		{ .iov_base = &ip_hdr, .iov_len = sizeof(ip_hdr) },
+		{ .iov_base = buf, .iov_len = *buf_len - sizeof(ip_hdr) },
 	};
-	struct sockaddr_in6 saddr;
+	struct sockaddr_ll saddr;
 	struct msghdr msg = {
 		.msg_name = (void *)&saddr,
-		.msg_namelen = sizeof(struct sockaddr_in6),
+		.msg_namelen = sizeof(struct sockaddr_ll),
 		.msg_flags = 0,
-		.msg_iov = &iov,
-		.msg_iovlen = 1,
+		.msg_iov = iov,
+		.msg_iovlen = L_ARRAY_SIZE(iov),
 		.msg_control = c_msg_buf,
 		.msg_controllen = sizeof(c_msg_buf),
 	};
@@ -229,22 +302,30 @@ static int icmp6_receive(int s, void *bu
 	if (l < 0)
 		return -errno;
 
-	if ((size_t) l != buf_len)
+	if (l != *buf_len)
 		return -EINVAL;
 
-	if (msg.msg_namelen != sizeof(struct sockaddr_in6) ||
-			saddr.sin6_family != AF_INET6)
-		return -EPFNOSUPPORT;
+	if (ntohs(ip_hdr.ip6_plen) > iov[1].iov_len)
+		return -EMSGSIZE;
+
+	iov[1].iov_len = ntohs(ip_hdr.ip6_plen);
+
+	/*
+	 * Unlikely but align length for icmp6_checksum().  We know we have
+	 * at least sizeof(struct ip6_hdr) extra bytes in buf so we can
+	 * append this 0 byte no problem.
+	 */
+	if (iov[1].iov_len & 1)
+		((uint8_t *) buf)[iov[1].iov_len++] = 0x00;
+
+	if (icmp6_checksum(iov, L_ARRAY_SIZE(iov)))
+		return -EBADMSG;
+
+	if (ip_hdr.ip6_hops != 255)
+		return -EMULTIHOP;
 
 	for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
-		if (cmsg->cmsg_level == SOL_IPV6 &&
-				cmsg->cmsg_type == IPV6_HOPLIMIT &&
-				cmsg->cmsg_len == CMSG_LEN(sizeof(int))) {
-			int hops = l_get_u32(CMSG_DATA(cmsg));
-
-			if (hops != 255)
-				return -EMULTIHOP;
-		} else if (cmsg->cmsg_level == SOL_SOCKET &&
+		if (cmsg->cmsg_level == SOL_SOCKET &&
 				cmsg->cmsg_type == SCM_TIMESTAMP &&
 				cmsg->cmsg_len ==
 				CMSG_LEN(sizeof(struct timeval))) {
@@ -254,7 +335,8 @@ static int icmp6_receive(int s, void *bu
 		}
 	}
 
-	memcpy(src, &saddr.sin6_addr, sizeof(saddr.sin6_addr));
+	*buf_len = ntohs(ip_hdr.ip6_plen);
+	memcpy(src, &ip_hdr.ip6_src, 16);
 	*out_timestamp = timestamp ?: l_time_now();
 	return 0;
 }
@@ -271,6 +353,8 @@ struct l_icmp6_client {
 	struct l_timeout *timeout_send;
 	uint64_t retransmit_time;
 	struct l_io *io;
+	struct in6_addr src_ip;
+	bool src_ip_optimistic;
 
 	struct l_icmp6_router *ra;
 	struct l_netlink *rtnl;
@@ -423,12 +507,16 @@ static bool icmp6_client_read_handler(st
 		return false;
 	}
 
-	ra = l_malloc(l);
-	if (icmp6_receive(s, ra, l, &src, &timestamp) < 0)
-		goto done;
+	if ((size_t) l < sizeof(struct ip6_hdr) +
+			sizeof(struct nd_router_advert)) {
+		CLIENT_DEBUG("Message too small - ignore");
+		return true;
+	}
 
-	if ((size_t) l < sizeof(struct nd_router_advert)) {
-		CLIENT_DEBUG("Message to small - ignore");
+	ra = l_malloc(l);
+	r = icmp6_receive(s, ra, &l, &src, &timestamp);
+	if (r < 0) {
+		CLIENT_DEBUG("icmp6_receive(): %s (%i)", strerror(-r), -r);
 		goto done;
 	}
 
@@ -466,7 +554,9 @@ static void icmp6_client_timeout_send(st
 						SOLICITATION_INTERVAL);
 
 	r = icmp6_send_router_solicitation(l_io_get_fd(client->io),
-								client->mac);
+						client->ifindex, client->mac,
+						&client->src_ip,
+						client->src_ip_optimistic);
 	if (r < 0) {
 		CLIENT_DEBUG("Error sending Router Solicitation: %s",
 				strerror(-r));
@@ -683,6 +773,26 @@ LIB_EXPORT bool l_icmp6_client_set_route
 	return true;
 }
 
+LIB_EXPORT bool l_icmp6_client_set_link_local_address(
+						struct l_icmp6_client *client,
+						const char *ll, bool optimistic)
+{
+	if (unlikely(!client))
+		return false;
+
+	/*
+	 * client->src_ip is all 0s initially which results in our Router
+	 * Solicitations being sent from the IPv6 Unspecified Address, which
+	 * is fine.  Once we have a confirmed link-local address we use that
+	 * as the source address.
+	 */
+	if (inet_pton(AF_INET6, ll, &client->src_ip) != 1)
+		return false;
+
+	client->src_ip_optimistic = optimistic;
+	return true;
+}
+
 struct l_icmp6_router *_icmp6_router_new()
 {
 	struct l_icmp6_router *r = l_new(struct l_icmp6_router, 1);
@@ -693,9 +803,58 @@ struct l_icmp6_router *_icmp6_router_new
 void _icmp6_router_free(struct l_icmp6_router *r)
 {
 	l_free(r->routes);
+	l_free(r->ac_prefixes);
 	l_free(r);
 }
 
+/* Note: the following two write to @out even when they return false */
+static bool icmp6_prefix_parse_rt_info(const uint8_t *data,
+					struct route_info *out)
+{
+	out->prefix_len = data[2];
+	out->onlink = true;
+	out->preference = 0;
+	out->valid_lifetime = l_get_be32(data + 4);
+
+	/*
+	 * Only the initial Prefix Length bits of the prefix are valid.
+	 * The remaining bits "MUST" be ignored by the receiver.
+	 */
+	memcpy(out->address, net_prefix_from_ipv6(data + 16, out->prefix_len),
+		16);
+
+	if (out->prefix_len >= 10 && IN6_IS_ADDR_LINKLOCAL(out->address))
+		return false;
+
+	return true;
+}
+
+static bool icmp6_prefix_parse_ac_info(const uint8_t *data,
+					struct autoconf_prefix_info *out)
+{
+	/*
+	 * Per RFC4862 we need to silently ignore prefixes with a
+	 * preferred lifetime longer than valid lifetime, those with
+	 * 0 valid lifetime and those with link-local prefixes.
+	 * Prefix Length must be 8 bytes (IPv6 address - Interface ID).
+	 */
+	if (data[2] != 64)
+		return false;
+
+	if (IN6_IS_ADDR_LINKLOCAL(data + 16))
+		return false;
+
+	out->valid_lifetime = l_get_be32(data + 4);
+	out->preferred_lifetime = l_get_be32(data + 8);
+
+	if (out->valid_lifetime == 0 ||
+			out->preferred_lifetime > out->valid_lifetime)
+		return false;
+
+	memcpy(out->prefix, data + 16, 8);
+	return true;
+}
+
 struct l_icmp6_router *_icmp6_router_parse(const struct nd_router_advert *ra,
 						size_t len,
 						const uint8_t src[static 16],
@@ -705,6 +864,9 @@ struct l_icmp6_router *_icmp6_router_par
 	const uint8_t *opts;
 	uint32_t opts_len;
 	uint32_t n_routes = 0;
+	uint32_t n_ac_prefixes = 0;
+	uint32_t n_dns = 0;
+	uint32_t n_domains = 0;
 
 	if (ra->nd_ra_type != ND_ROUTER_ADVERT)
 		return NULL;
@@ -742,6 +904,10 @@ struct l_icmp6_router *_icmp6_router_par
 
 			if (opts[3] & ND_OPT_PI_FLAG_ONLINK)
 				n_routes += 1;
+
+			if (opts[3] & ND_OPT_PI_FLAG_AUTO)
+				n_ac_prefixes += 1;
+
 			break;
 		case ND_OPT_ROUTE_INFORMATION:
 			if (l < 8)
@@ -771,6 +937,46 @@ struct l_icmp6_router *_icmp6_router_par
 
 			n_routes += 1;
 			break;
+		case ND_OPT_RECURSIVE_DNS_SERVER:
+			if (l < 24 || (l & 15) != 8)
+				return NULL;
+
+			n_dns += (l - 8) / 16;
+			break;
+		case ND_OPT_DNS_SEARCH_LIST:
+		{
+			unsigned int n_labels;
+			unsigned int pos = 8;
+
+			if (l < 16)
+				return NULL;
+
+			/* Count domains according to RFC1035 Section 3.1 */
+			do {
+				unsigned int label_len;
+
+				n_labels = 0;
+
+				do {
+					label_len = opts[pos];
+					pos += 1 + label_len;
+					n_labels += label_len ? 1 : 0;
+				} while (label_len && pos < l);
+
+				/*
+				 * Check if the root label was missing, or
+				 * a label didn't fit in the option bytes, or
+				 * the first domain had 0 labels, i.e. there
+				 * were no domains.
+				 */
+				if (label_len || pos > l || pos == 9)
+					return NULL;
+
+				n_domains += n_labels ? 1 : 0;
+			} while (n_labels && pos < l);
+
+			break;
+		}
 		}
 
 		opts += l;
@@ -780,7 +986,9 @@ struct l_icmp6_router *_icmp6_router_par
 	r = _icmp6_router_new();
 	memcpy(r->address, src, sizeof(r->address));
 	r->routes = l_new(struct route_info, n_routes);
-	r->n_routes = n_routes;
+	r->ac_prefixes = l_new(struct autoconf_prefix_info, n_ac_prefixes);
+	r->dns_list = l_new(struct dns_info, n_dns);
+	r->domains = l_new(struct domain_info, n_domains);
 
 	if (ra->nd_ra_flags_reserved & ND_RA_FLAG_MANAGED)
 		r->managed = true;
@@ -798,6 +1006,9 @@ struct l_icmp6_router *_icmp6_router_par
 	opts = (uint8_t *) (ra + 1);
 	opts_len = len - sizeof(struct nd_router_advert);
 	n_routes = 0;
+	n_ac_prefixes = 0;
+	n_dns = 0;
+	n_domains = 0;
 
 	while (opts_len) {
 		uint8_t t = opts[0];
@@ -814,41 +1025,22 @@ struct l_icmp6_router *_icmp6_router_par
 
 			break;
 		case ND_OPT_PREFIX_INFORMATION:
-		{
-			struct route_info *i = &r->routes[n_routes];
-
-			if (!(opts[3] & ND_OPT_PI_FLAG_ONLINK))
-				break;
+			if (opts[3] & ND_OPT_PI_FLAG_ONLINK) {
+				struct route_info *i = &r->routes[n_routes];
 
-			i->prefix_len = opts[2];
-			i->onlink = true;
-			i->valid_lifetime = l_get_be32(opts + 4);
-			i->preferred_lifetime = l_get_be32(opts + 8);
-
-			/*
-			 * Only the initial Prefix Length bits of the prefix
-			 * are valid.  The remaining bits "MUST" be ignored
-			 * by the receiver.
-			 */
-			memcpy(i->address, net_prefix_from_ipv6(opts + 16,
-							i->prefix_len), 16);
+				if (icmp6_prefix_parse_rt_info(opts, i))
+					n_routes++;
+			}
 
-			/*
-			 * For SLAAC (RFC4862) we need to "silently ignore"
-			 * routes with a preferred lifetime longer than valid
-			 * lifetime, and those with the link-local prefix.
-			 * Since it makes sense, do it regardless of SLAAC.
-			 */
-			if (i->preferred_lifetime > i->valid_lifetime)
-				break;
+			if (opts[3] & ND_OPT_PI_FLAG_AUTO) {
+				struct autoconf_prefix_info *i =
+					&r->ac_prefixes[n_ac_prefixes];
 
-			if (i->prefix_len >= 10 &&
-					IN6_IS_ADDR_LINKLOCAL(i->address))
-				break;
+				if (icmp6_prefix_parse_ac_info(opts, i))
+					n_ac_prefixes++;
+			}
 
-			n_routes += 1;
 			break;
-		}
 		case ND_OPT_RTR_ADV_INTERVAL:
 			if (l < 8)
 				break;
@@ -908,12 +1100,49 @@ struct l_icmp6_router *_icmp6_router_par
 			n_routes += 1;
 			break;
 		}
+		case ND_OPT_RECURSIVE_DNS_SERVER:
+		{
+			unsigned int pos;
+
+			for (pos = 8; pos < l; pos += 16) {
+				struct dns_info *i = &r->dns_list[n_dns++];
+
+				i->lifetime = l_get_be32(opts + 4);
+				memcpy(i->address, opts + pos, 16);
+			}
+
+			break;
+		}
+		case ND_OPT_DNS_SEARCH_LIST:
+		{
+			struct domain_info *info = &r->domains[n_domains];
+			_auto_(l_free) char **domain_list =
+				net_domain_list_parse(opts + 8, l - 8, true);
+			char **i;
+
+			/* Ignore malformed option */
+			if (!domain_list || !domain_list[0])
+				break;
+
+			for (i = domain_list; *i; i++) {
+				info->lifetime = l_get_be32(opts + 4);
+				info->domain = *i;
+				info++;
+				n_domains++;
+			}
+
+			break;
+		}
 		}
 
 		opts += l;
 		opts_len -= l;
 	}
 
+	r->n_routes = n_routes;
+	r->n_ac_prefixes = n_ac_prefixes;
+	r->n_dns = n_dns;
+	r->n_domains = n_domains;
 	return r;
 }
 
diff -pruN 1.30-1/ell/icmp6.h 2.3-1/ell/icmp6.h
--- 1.30-1/ell/icmp6.h	2022-04-21 14:47:16.000000000 +0000
+++ 2.3-1/ell/icmp6.h	2022-11-18 09:08:38.000000000 +0000
@@ -65,6 +65,9 @@ bool l_icmp6_client_set_rtnl(struct l_ic
 						struct l_netlink *rtnl);
 bool l_icmp6_client_set_route_priority(struct l_icmp6_client *client,
 						uint32_t priority);
+bool l_icmp6_client_set_link_local_address(struct l_icmp6_client *client,
+						const char *ll,
+						bool optimistic);
 
 char *l_icmp6_router_get_address(const struct l_icmp6_router *r);
 bool l_icmp6_router_get_managed(const struct l_icmp6_router *r);
diff -pruN 1.30-1/ell/icmp6-private.h 2.3-1/ell/icmp6-private.h
--- 1.30-1/ell/icmp6-private.h	2022-06-04 19:58:23.000000000 +0000
+++ 2.3-1/ell/icmp6-private.h	2022-11-18 09:08:38.000000000 +0000
@@ -25,10 +25,25 @@ struct route_info {
 	bool onlink : 1;
 	uint8_t prefix_len;
 	uint8_t preference;
+	uint32_t valid_lifetime;
+};
+
+struct autoconf_prefix_info {
+	uint8_t prefix[8];
 	uint32_t preferred_lifetime;
 	uint32_t valid_lifetime;
 };
 
+struct dns_info {
+	uint8_t address[16];
+	uint32_t lifetime;
+};
+
+struct domain_info {
+	char *domain;
+	uint32_t lifetime;
+};
+
 struct l_icmp6_router {
 	uint8_t address[16];
 	bool managed : 1;
@@ -40,6 +55,12 @@ struct l_icmp6_router {
 	uint32_t max_rtr_adv_interval_ms;
 	uint32_t n_routes;
 	struct route_info *routes;
+	uint32_t n_ac_prefixes;
+	struct autoconf_prefix_info *ac_prefixes;
+	uint32_t n_dns;
+	struct dns_info *dns_list;
+	uint32_t n_domains;
+	struct domain_info *domains;
 };
 
 struct l_icmp6_router *_icmp6_router_new();
diff -pruN 1.30-1/ell/net.c 2.3-1/ell/net.c
--- 1.30-1/ell/net.c	2021-03-29 12:19:13.000000000 +0000
+++ 2.3-1/ell/net.c	2022-11-18 09:08:38.000000000 +0000
@@ -295,7 +295,7 @@ char *net_domain_name_parse(const uint8_
 /*
  * Parse list of domain names encoded according to RFC 1035 Section 3.1
  */
-char **net_domain_list_parse(const uint8_t *raw, size_t raw_len)
+char **net_domain_list_parse(const uint8_t *raw, size_t raw_len, bool padded)
 {
 	size_t remaining = raw_len;
 	const uint8_t *p = raw;
@@ -305,6 +305,9 @@ char **net_domain_list_parse(const uint8
 	struct l_string *growable = NULL;
 
 	while (remaining) {
+		if (padded && p[0] == 0)
+			break;
+
 		r = validate_next_domain_name(p, remaining);
 		if (r < 0)
 			return NULL;
@@ -323,6 +326,9 @@ char **net_domain_list_parse(const uint8
 		remaining -= *p + 1;
 
 		if (*p == 0) {
+			if (!growable)
+				break;
+
 			p += 1;
 			ret[nitems++] = l_string_unwrap(growable);
 			growable = NULL;
diff -pruN 1.30-1/ell/netconfig.c 2.3-1/ell/netconfig.c
--- 1.30-1/ell/netconfig.c	1970-01-01 00:00:00.000000000 +0000
+++ 2.3-1/ell/netconfig.c	2022-11-18 09:08:38.000000000 +0000
@@ -0,0 +1,2518 @@
+/*
+ *
+ *  Embedded Linux library
+ *
+ *  Copyright (C) 2022  Intel Corporation. All rights reserved.
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.1 of the License, or (at your option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <net/if.h>
+#include <linux/types.h>
+#include <linux/if_ether.h>
+#include <linux/if_arp.h>
+#include <netinet/ip.h>
+#include <arpa/inet.h>
+#include <netinet/icmp6.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include "private.h"
+#include "useful.h"
+#include "log.h"
+#include "dhcp.h"
+#include "dhcp-private.h"
+#include "icmp6.h"
+#include "icmp6-private.h"
+#include "dhcp6.h"
+#include "netlink.h"
+#include "rtnl.h"
+#include "rtnl-private.h"
+#include "queue.h"
+#include "time.h"
+#include "idle.h"
+#include "strv.h"
+#include "net.h"
+#include "net-private.h"
+#include "acd.h"
+#include "timeout.h"
+#include "netconfig.h"
+
+struct l_netconfig {
+	uint32_t ifindex;
+	uint32_t route_priority;
+
+	bool v4_enabled;
+	struct l_rtnl_address *v4_static_addr;
+	char *v4_gateway_override;
+	char **v4_dns_override;
+	char **v4_domain_names_override;
+	bool acd_enabled;
+
+	bool v6_enabled;
+	struct l_rtnl_address *v6_static_addr;
+	char *v6_gateway_override;
+	char **v6_dns_override;
+	char **v6_domain_names_override;
+	bool optimistic_dad_enabled;
+
+	bool started;
+	struct l_idle *do_static_work;
+	bool v4_configured;
+	struct l_dhcp_client *dhcp_client;
+	bool v6_configured;
+	struct l_icmp6_client *icmp6_client;
+	struct l_dhcp6_client *dhcp6_client;
+	struct l_idle *signal_expired_work;
+	unsigned int ifaddr6_dump_cmd_id;
+	struct l_queue *icmp_route_data;
+	struct l_acd *acd;
+	unsigned int orig_disable_ipv6;
+	long orig_optimistic_dad;
+	uint8_t mac[ETH_ALEN];
+	struct l_timeout *ra_timeout;
+	bool have_lla;
+	enum {
+		NETCONFIG_V6_METHOD_UNSET,
+		NETCONFIG_V6_METHOD_DHCP,       /* Managed bit set in RA */
+		NETCONFIG_V6_METHOD_SLAAC_DHCP, /* Other bit set in RA */
+		NETCONFIG_V6_METHOD_SLAAC,      /* Neither flag set in RA */
+	} v6_auto_method;
+	struct l_queue *slaac_dnses;
+	struct l_queue *slaac_domains;
+
+	/* These objects, if not NULL, are owned by @addresses and @routes */
+	struct l_rtnl_address *v4_address;
+	struct l_rtnl_route *v4_subnet_route;
+	struct l_rtnl_route *v4_default_route;
+	struct l_rtnl_address *v6_address;
+
+	struct {
+		struct l_queue *current;
+
+		/*
+		 * Temporary lists for use by the UPDATED handler to avoid
+		 * having to remove all entries on the interface and re-add
+		 * them from @current.  Entries in @updated are those that
+		 * RTM_NEWADDR/RTM_NEWROUTE will correctly identify as
+		 * existing objects and replace (with NLM_F_REPLACE) or
+		 * error out (without it) rather than create duplicates,
+		 * for example those that only have their lifetime updated.
+		 *
+		 * Any entries in @added and @updated are owned by @current.
+		 * Entries in @removed need to be removed with an
+		 * RTM_DELADD/RTM_DELROUTE while those in @expired are only
+		 * informative as the kernel will have removed them already.
+		 */
+		struct l_queue *added;
+		struct l_queue *updated;
+		struct l_queue *removed;
+		struct l_queue *expired;
+	} addresses, routes;
+
+	struct {
+		l_netconfig_event_cb_t callback;
+		void *user_data;
+		l_netconfig_destroy_cb_t destroy;
+	} handler;
+};
+
+struct netconfig_route_data {
+	struct l_rtnl_route *route;
+	uint64_t last_ra_time;
+	uint64_t kernel_expiry;
+	uint64_t max_ra_interval;
+};
+
+union netconfig_addr {
+	struct in_addr v4;
+	struct in6_addr v6;
+};
+
+static struct l_queue *addr_wait_list;
+static unsigned int rtnl_id;
+
+static const unsigned int max_icmp6_routes = 100;
+static const unsigned int max_icmp6_dnses = 10;
+static const unsigned int max_icmp6_domains = 10;
+
+static void netconfig_update_cleanup(struct l_netconfig *nc)
+{
+	l_queue_clear(nc->addresses.added, NULL);
+	l_queue_clear(nc->addresses.updated, NULL);
+	l_queue_clear(nc->addresses.removed,
+			(l_queue_destroy_func_t) l_rtnl_address_free);
+	l_queue_clear(nc->addresses.expired,
+			(l_queue_destroy_func_t) l_rtnl_address_free);
+	l_queue_clear(nc->routes.added, NULL);
+	l_queue_clear(nc->routes.updated, NULL);
+	l_queue_clear(nc->routes.removed,
+			(l_queue_destroy_func_t) l_rtnl_route_free);
+	l_queue_clear(nc->routes.expired,
+			(l_queue_destroy_func_t) l_rtnl_route_free);
+}
+
+static void netconfig_emit_event(struct l_netconfig *nc, uint8_t family,
+					enum l_netconfig_event event)
+{
+	if (!nc->handler.callback)
+		return;
+
+	nc->handler.callback(nc, family, event, nc->handler.user_data);
+
+	if (L_IN_SET(event, L_NETCONFIG_EVENT_UPDATE,
+			L_NETCONFIG_EVENT_CONFIGURE,
+			L_NETCONFIG_EVENT_UNCONFIGURE))
+		netconfig_update_cleanup(nc);
+}
+
+static void netconfig_addr_wait_unregister(struct l_netconfig *nc,
+						bool in_notify);
+
+static void netconfig_failed(struct l_netconfig *nc, uint8_t family)
+{
+	if (family == AF_INET) {
+		l_dhcp_client_stop(nc->dhcp_client);
+		l_acd_destroy(l_steal_ptr(nc->acd));
+	} else {
+		netconfig_addr_wait_unregister(nc, false);
+		l_dhcp6_client_stop(nc->dhcp6_client);
+		l_icmp6_client_stop(nc->icmp6_client);
+		l_timeout_remove(l_steal_ptr(nc->ra_timeout));
+	}
+
+	netconfig_emit_event(nc, family, L_NETCONFIG_EVENT_FAILED);
+}
+
+static struct l_rtnl_route *netconfig_route_new(struct l_netconfig *nc,
+						uint8_t family,
+						const void *dst,
+						uint8_t prefix_len,
+						const void *gw,
+						uint8_t protocol)
+{
+	struct l_rtnl_route *rt = l_new(struct l_rtnl_route, 1);
+
+	rt->family = family;
+	rt->scope = (family == AF_INET && dst) ?
+		RT_SCOPE_LINK : RT_SCOPE_UNIVERSE;
+	rt->protocol = protocol;
+	rt->lifetime = 0xffffffff;
+	rt->priority = nc->route_priority;
+
+	if (dst) {
+		memcpy(&rt->dst, dst, family == AF_INET ? 4 : 16);
+		rt->dst_prefix_len = prefix_len;
+	}
+
+	if (gw)
+		memcpy(&rt->gw, gw, family == AF_INET ? 4 : 16);
+
+	return rt;
+}
+
+static void netconfig_signal_expired(struct l_idle *idle, void *user_data)
+{
+	struct l_netconfig *nc = user_data;
+
+	l_idle_remove(l_steal_ptr(nc->signal_expired_work));
+
+	/*
+	 * If the idle work was scheduled from within l_netconfig_get_routes
+	 * or netconfig_icmp6_event_handler, the user is likely to have
+	 * already received an event and had a chance to process the expired
+	 * routes list.  In that case there's no need to emit a new event,
+	 * and the list will have been emptied in netconfig_update_cleanup()
+	 * anyway.
+	 */
+	if (!l_queue_isempty(nc->routes.expired))
+		netconfig_emit_event(nc, AF_INET6, L_NETCONFIG_EVENT_UPDATE);
+}
+
+static void netconfig_add_v4_routes(struct l_netconfig *nc, const char *ip,
+					uint8_t prefix_len, const char *gateway,
+					uint8_t rtm_protocol)
+{
+	struct in_addr in_addr;
+
+	/* Subnet route */
+
+	if (L_WARN_ON(inet_pton(AF_INET, ip, &in_addr) != 1))
+		return;
+
+	in_addr.s_addr &= htonl(0xfffffffflu << (32 - prefix_len));
+	nc->v4_subnet_route = netconfig_route_new(nc, AF_INET, &in_addr,
+							prefix_len, NULL,
+							rtm_protocol);
+	l_queue_push_tail(nc->routes.current, nc->v4_subnet_route);
+	l_queue_push_tail(nc->routes.added, nc->v4_subnet_route);
+
+	/* Gateway route */
+
+	if (nc->v4_gateway_override) {
+		gateway = nc->v4_gateway_override;
+		rtm_protocol = RTPROT_STATIC;
+	}
+
+	if (!gateway)
+		return;
+
+	nc->v4_default_route = l_rtnl_route_new_gateway(gateway);
+	l_rtnl_route_set_protocol(nc->v4_default_route, rtm_protocol);
+	L_WARN_ON(!l_rtnl_route_set_prefsrc(nc->v4_default_route, ip));
+	l_rtnl_route_set_priority(nc->v4_default_route, nc->route_priority);
+	l_queue_push_tail(nc->routes.current, nc->v4_default_route);
+	l_queue_push_tail(nc->routes.added, nc->v4_default_route);
+}
+
+static void netconfig_add_v6_static_routes(struct l_netconfig *nc,
+						const char *ip,
+						uint8_t prefix_len)
+{
+	struct in6_addr in6_addr;
+	const void *prefix;
+	struct l_rtnl_route *v6_subnet_route;
+	struct l_rtnl_route *v6_default_route;
+
+	/* Subnet route */
+
+	if (L_WARN_ON(inet_pton(AF_INET6, ip, &in6_addr) != 1))
+		return;
+
+	/*
+	 * Zero out host address bits, aka. interface ID, to produce
+	 * the network address or prefix.
+	 */
+	prefix = net_prefix_from_ipv6(in6_addr.s6_addr, prefix_len);
+
+	/*
+	 * One reason we add a subnet route instead of letting the kernel
+	 * do it, by not specifying IFA_F_NOPREFIXROUTE for the address,
+	 * is that that would force a 0 metric for the route.
+	 */
+	v6_subnet_route = netconfig_route_new(nc, AF_INET6, prefix, prefix_len,
+						NULL, RTPROT_STATIC);
+	l_queue_push_tail(nc->routes.current, v6_subnet_route);
+	l_queue_push_tail(nc->routes.added, v6_subnet_route);
+
+	/* Gateway route */
+
+	if (!nc->v6_gateway_override)
+		return;
+
+	v6_default_route = l_rtnl_route_new_gateway(nc->v6_gateway_override);
+	l_rtnl_route_set_protocol(v6_default_route, RTPROT_STATIC);
+	/*
+	 * TODO: Optimally we'd set the prefsrc on the route with:
+	 * L_WARN_ON(!l_rtnl_route_set_prefsrc(v6_default_route, ip));
+	 *
+	 * but that means that we can only commit the route to the kernel
+	 * with an RTM_NEWROUTE command after the corresponding RTM_NEWADDR
+	 * has returned and the kernel has finished DAD for the address and
+	 * cleared IFA_F_TENTATIVE.  That will complicate
+	 * l_netconfig_apply_rtnl() significantly but may be inevitable.
+	 */
+
+	l_queue_push_tail(nc->routes.current, v6_default_route);
+	l_queue_push_tail(nc->routes.added, v6_default_route);
+}
+
+static bool netconfig_address_exists(struct l_queue *list,
+					const struct l_rtnl_address *address)
+{
+	const struct l_queue_entry *entry;
+
+	for (entry = l_queue_get_entries(list); entry;
+			entry = entry->next)
+		if ((const struct l_rtnl_address *) entry->data == address)
+			return true;
+
+	return false;
+}
+
+static bool netconfig_route_exists(struct l_queue *list,
+					const struct l_rtnl_route *route)
+{
+	const struct l_queue_entry *entry;
+
+	for (entry = l_queue_get_entries(list); entry;
+			entry = entry->next)
+		if ((const struct l_rtnl_route *) entry->data == route)
+			return true;
+
+	return false;
+}
+
+static void netconfig_add_dhcp_address_routes(struct l_netconfig *nc)
+{
+	const struct l_dhcp_lease *lease =
+		l_dhcp_client_get_lease(nc->dhcp_client);
+	_auto_(l_free) char *ip = NULL;
+	_auto_(l_free) char *broadcast = NULL;
+	_auto_(l_free) char *gateway = NULL;
+	uint32_t prefix_len;
+
+	ip = l_dhcp_lease_get_address(lease);
+	broadcast = l_dhcp_lease_get_broadcast(lease);
+
+	prefix_len = l_dhcp_lease_get_prefix_length(lease);
+	if (!prefix_len)
+		prefix_len = 24;
+
+	nc->v4_address = l_rtnl_address_new(ip, prefix_len);
+	if (L_WARN_ON(!nc->v4_address))
+		return;
+
+	l_rtnl_address_set_noprefixroute(nc->v4_address, true);
+
+	if (broadcast)
+		l_rtnl_address_set_broadcast(nc->v4_address, broadcast);
+
+	l_queue_push_tail(nc->addresses.current, nc->v4_address);
+	l_queue_push_tail(nc->addresses.added, nc->v4_address);
+
+	gateway = l_dhcp_lease_get_gateway(lease);
+	netconfig_add_v4_routes(nc, ip, prefix_len, gateway, RTPROT_DHCP);
+}
+
+static void netconfig_set_dhcp_lifetimes(struct l_netconfig *nc, bool updated)
+{
+	const struct l_dhcp_lease *lease =
+		l_dhcp_client_get_lease(nc->dhcp_client);
+	uint32_t lifetime = l_dhcp_lease_get_lifetime(lease);
+	uint64_t expiry = l_dhcp_lease_get_start_time(lease) +
+		lifetime * L_USEC_PER_SEC;
+
+	l_rtnl_address_set_lifetimes(nc->v4_address, lifetime, lifetime);
+	l_rtnl_address_set_expiry(nc->v4_address, expiry, expiry);
+
+	if (updated && !netconfig_address_exists(nc->addresses.added,
+							nc->v4_address))
+		l_queue_push_tail(nc->addresses.updated, nc->v4_address);
+
+	l_rtnl_route_set_lifetime(nc->v4_subnet_route, lifetime);
+	l_rtnl_route_set_expiry(nc->v4_subnet_route, expiry);
+
+	if (updated && !netconfig_route_exists(nc->routes.added,
+						nc->v4_subnet_route))
+		l_queue_push_tail(nc->routes.updated, nc->v4_subnet_route);
+
+	if (!nc->v4_default_route)
+		return;
+
+	l_rtnl_route_set_lifetime(nc->v4_default_route, lifetime);
+	l_rtnl_route_set_expiry(nc->v4_default_route, expiry);
+
+	if (updated && !netconfig_route_exists(nc->routes.added,
+						nc->v4_default_route))
+		l_queue_push_tail(nc->routes.updated, nc->v4_default_route);
+}
+
+static void netconfig_remove_v4_address_routes(struct l_netconfig *nc,
+						bool expired)
+{
+	struct l_queue *routes =
+		expired ? nc->routes.expired : nc->routes.removed;
+
+	l_queue_remove(nc->addresses.current, nc->v4_address);
+	l_queue_remove(nc->addresses.updated, nc->v4_address);
+
+	if (!l_queue_remove(nc->addresses.added, nc->v4_address))
+		l_queue_push_tail(
+			expired ? nc->addresses.expired : nc->addresses.removed,
+			nc->v4_address);
+
+	nc->v4_address = NULL;
+
+	l_queue_remove(nc->routes.current, nc->v4_subnet_route);
+	l_queue_remove(nc->routes.updated, nc->v4_subnet_route);
+
+	if (!l_queue_remove(nc->routes.added, nc->v4_subnet_route))
+		l_queue_push_tail(routes, nc->v4_subnet_route);
+
+	nc->v4_subnet_route = NULL;
+
+	if (nc->v4_default_route) {
+		l_queue_remove(nc->routes.current, nc->v4_default_route);
+		l_queue_remove(nc->routes.updated, nc->v4_default_route);
+
+		if (!l_queue_remove(nc->routes.added, nc->v4_default_route))
+			l_queue_push_tail(routes, nc->v4_default_route);
+
+		nc->v4_default_route = NULL;
+	}
+}
+
+static void netconfig_set_neighbor_entry_cb(int error,
+						uint16_t type, const void *data,
+						uint32_t len, void *user_data)
+{
+	/* Not critical.  TODO: log warning */
+}
+
+static int netconfig_dhcp_gateway_to_arp(struct l_netconfig *nc)
+{
+	const struct l_dhcp_lease *lease =
+		l_dhcp_client_get_lease(nc->dhcp_client);
+	_auto_(l_free) char *server_id = l_dhcp_lease_get_server_id(lease);
+	_auto_(l_free) char *gw = l_dhcp_lease_get_gateway(lease);
+	const uint8_t *server_mac = l_dhcp_lease_get_server_mac(lease);
+	struct in_addr in_gw;
+
+	if (!gw || strcmp(server_id, gw) || !server_mac)
+		return -ENOENT;
+
+	/* Gateway MAC is known, write it into ARP cache to save ARP traffic */
+	in_gw.s_addr = l_dhcp_lease_get_gateway_u32(lease);
+
+	if (!l_rtnl_neighbor_set_hwaddr(l_rtnl_get(), nc->ifindex, AF_INET,
+					&in_gw, server_mac, ETH_ALEN,
+					netconfig_set_neighbor_entry_cb, nc,
+					NULL))
+		return -EIO;
+
+	return 0;
+}
+
+static void netconfig_dhcp_event_handler(struct l_dhcp_client *client,
+						enum l_dhcp_client_event event,
+						void *user_data)
+{
+	struct l_netconfig *nc = user_data;
+
+	switch (event) {
+	case L_DHCP_CLIENT_EVENT_IP_CHANGED:
+		if (L_WARN_ON(!nc->v4_configured))
+			break;
+
+		netconfig_remove_v4_address_routes(nc, false);
+		netconfig_add_dhcp_address_routes(nc);
+		netconfig_set_dhcp_lifetimes(nc, false);
+		netconfig_emit_event(nc, AF_INET, L_NETCONFIG_EVENT_UPDATE);
+		break;
+	case L_DHCP_CLIENT_EVENT_LEASE_OBTAINED:
+		if (L_WARN_ON(nc->v4_configured))
+			break;
+
+		netconfig_add_dhcp_address_routes(nc);
+		netconfig_set_dhcp_lifetimes(nc, false);
+		nc->v4_configured = true;
+		netconfig_emit_event(nc, AF_INET, L_NETCONFIG_EVENT_CONFIGURE);
+		netconfig_dhcp_gateway_to_arp(nc);
+		break;
+	case L_DHCP_CLIENT_EVENT_LEASE_RENEWED:
+		if (L_WARN_ON(!nc->v4_configured))
+			break;
+
+		netconfig_set_dhcp_lifetimes(nc, true);
+		netconfig_emit_event(nc, AF_INET, L_NETCONFIG_EVENT_UPDATE);
+		break;
+	case L_DHCP_CLIENT_EVENT_LEASE_EXPIRED:
+		if (L_WARN_ON(!nc->v4_configured))
+			break;
+
+		netconfig_remove_v4_address_routes(nc, true);
+		nc->v4_configured = false;
+
+		if (l_dhcp_client_start(nc->dhcp_client))
+			/* TODO: also start a new timeout */
+			netconfig_emit_event(nc, AF_INET,
+						L_NETCONFIG_EVENT_UNCONFIGURE);
+		else
+			netconfig_failed(nc, AF_INET);
+
+		break;
+	case L_DHCP_CLIENT_EVENT_NO_LEASE:
+		L_WARN_ON(nc->v4_configured);
+
+		/*
+		 * The requested address is no longer available, try to restart
+		 * the client.
+		 *
+		 * TODO: this may need to be delayed so we don't flood the
+		 * network with DISCOVERs and NAKs.  Also add a retry limit or
+		 * better yet a configurable timeout.
+		 */
+		if (!l_dhcp_client_start(nc->dhcp_client))
+			netconfig_failed(nc, AF_INET);
+
+		break;
+	}
+}
+
+static void netconfig_add_dhcp6_address(struct l_netconfig *nc)
+{
+	const struct l_dhcp6_lease *lease =
+		l_dhcp6_client_get_lease(nc->dhcp6_client);
+	_auto_(l_free) char *ip = NULL;
+	uint32_t prefix_len;
+
+	if (L_WARN_ON(!lease))
+		return;
+
+	ip = l_dhcp6_lease_get_address(lease);
+	prefix_len = l_dhcp6_lease_get_prefix_length(lease);
+	nc->v6_address = l_rtnl_address_new(ip, prefix_len);
+
+	if (L_WARN_ON(!nc->v6_address))
+		return;
+
+	/*
+	 * Assume we already have a route from a Router Advertisement
+	 * covering the address from DHCPv6 + prefix length from DHCPv6.
+	 * We might want to emit a warning of some sort or
+	 * L_NETCONFIG_EVENT_FAILED if we don't since this would
+	 * basically be fatal for IPv6 connectivity.
+	 */
+	l_rtnl_address_set_noprefixroute(nc->v6_address, true);
+
+	l_queue_push_tail(nc->addresses.current, nc->v6_address);
+	l_queue_push_tail(nc->addresses.added, nc->v6_address);
+}
+
+static void netconfig_set_dhcp6_address_lifetimes(struct l_netconfig *nc,
+							bool updated)
+{
+	const struct l_dhcp6_lease *lease =
+		l_dhcp6_client_get_lease(nc->dhcp6_client);
+	uint32_t p, v;
+	uint64_t start_time;
+
+	if (L_WARN_ON(!lease))
+		return;
+
+	p = l_dhcp6_lease_get_preferred_lifetime(lease);
+	v = l_dhcp6_lease_get_valid_lifetime(lease);
+	start_time = l_dhcp6_lease_get_start_time(lease);
+
+	l_rtnl_address_set_lifetimes(nc->v6_address, p, v);
+	l_rtnl_address_set_expiry(nc->v6_address,
+					start_time + p * L_USEC_PER_SEC,
+					start_time + v * L_USEC_PER_SEC);
+
+	if (updated && !netconfig_address_exists(nc->addresses.added,
+							nc->v6_address))
+		l_queue_push_tail(nc->addresses.updated, nc->v6_address);
+}
+
+static void netconfig_remove_dhcp6_address(struct l_netconfig *nc, bool expired)
+{
+	l_queue_remove(nc->addresses.current, nc->v6_address);
+	l_queue_remove(nc->addresses.updated, nc->v6_address);
+
+	if (!l_queue_remove(nc->addresses.added, nc->v6_address))
+		l_queue_push_tail(
+			expired ? nc->addresses.expired : nc->addresses.removed,
+			nc->v6_address);
+
+	nc->v6_address = NULL;
+}
+
+static void netconfig_dhcp6_event_handler(struct l_dhcp6_client *client,
+						enum l_dhcp6_client_event event,
+						void *user_data)
+{
+	struct l_netconfig *nc = user_data;
+
+	switch (event) {
+	case L_DHCP6_CLIENT_EVENT_LEASE_OBTAINED:
+		if (L_WARN_ON(nc->v6_configured))
+			break;
+
+		if (nc->v6_auto_method == NETCONFIG_V6_METHOD_DHCP) {
+			netconfig_add_dhcp6_address(nc);
+			netconfig_set_dhcp6_address_lifetimes(nc, false);
+		}
+
+		nc->v6_configured = true;
+		netconfig_emit_event(nc, AF_INET6, L_NETCONFIG_EVENT_CONFIGURE);
+		break;
+	case L_DHCP6_CLIENT_EVENT_IP_CHANGED:
+		if (L_WARN_ON(!nc->v6_configured ||
+				nc->v6_auto_method != NETCONFIG_V6_METHOD_DHCP))
+			break;
+
+		netconfig_remove_dhcp6_address(nc, false);
+		netconfig_add_dhcp6_address(nc);
+		netconfig_set_dhcp6_address_lifetimes(nc, false);
+		netconfig_emit_event(nc, AF_INET6, L_NETCONFIG_EVENT_UPDATE);
+		break;
+	case L_DHCP6_CLIENT_EVENT_LEASE_EXPIRED:
+		if (L_WARN_ON(!nc->v6_configured ||
+				nc->v6_auto_method != NETCONFIG_V6_METHOD_DHCP))
+			break;
+
+		netconfig_remove_dhcp6_address(nc, true);
+		nc->v6_configured = false;
+
+		if (l_dhcp6_client_start(nc->dhcp6_client))
+			/* TODO: also start a new timeout */
+			netconfig_emit_event(nc, AF_INET6,
+						L_NETCONFIG_EVENT_UNCONFIGURE);
+		else
+			netconfig_failed(nc, AF_INET6);
+
+		break;
+	case L_DHCP6_CLIENT_EVENT_LEASE_RENEWED:
+		if (L_WARN_ON(!nc->v6_configured))
+			break;
+
+		if (nc->v6_auto_method == NETCONFIG_V6_METHOD_DHCP)
+			netconfig_set_dhcp6_address_lifetimes(nc, true);
+
+		netconfig_emit_event(nc, AF_INET6, L_NETCONFIG_EVENT_UPDATE);
+		break;
+	case L_DHCP6_CLIENT_EVENT_NO_LEASE:
+		if (L_WARN_ON(nc->v6_configured))
+			break;
+
+		if (nc->v6_auto_method == NETCONFIG_V6_METHOD_SLAAC_DHCP &&
+				!l_queue_isempty(nc->slaac_dnses))
+			break;
+
+		/*
+		 * The requested address is no longer available, try to restart
+		 * the client.
+		 *
+		 * TODO: this may need to be delayed so we don't flood the
+		 * network with SOLICITs and DECLINEs.  Also add a retry limit
+		 * or better yet a configurable timeout.
+		 */
+		if (!l_dhcp6_client_start(nc->dhcp6_client))
+			netconfig_failed(nc, AF_INET6);
+
+		break;
+	}
+}
+
+static bool netconfig_match(const void *a, const void *b)
+{
+	return a == b;
+}
+
+static bool netconfig_match_addr(const void *a, const void *b)
+{
+	return memcmp(a, b, 16) == 0;
+}
+
+static bool netconfig_match_str(const void *a, const void *b)
+{
+	return strcmp(a, b) == 0;
+}
+
+static bool netconfig_check_start_dhcp6(struct l_netconfig *nc)
+{
+	/* Don't start DHCPv6 until we get an RA with the managed bit set */
+	if (nc->ra_timeout || !L_IN_SET(nc->v6_auto_method,
+				NETCONFIG_V6_METHOD_DHCP,
+				NETCONFIG_V6_METHOD_SLAAC_DHCP))
+		return true;
+
+	/* Don't start DHCPv6 while waiting for the link-local address */
+	if (!nc->have_lla)
+		return true;
+
+	return l_dhcp6_client_start(nc->dhcp6_client);
+}
+
+static void netconfig_ra_timeout_cb(struct l_timeout *timeout, void *user_data)
+{
+	struct l_netconfig *nc = user_data;
+
+	/* No Router Advertisements received, assume no DHCPv6 or SLAAC */
+	netconfig_failed(nc, AF_INET6);
+}
+
+static void netconfig_add_slaac_address(struct l_netconfig *nc,
+					const struct l_icmp6_router *r)
+{
+	unsigned int i;
+	const struct autoconf_prefix_info *longest = &r->ac_prefixes[0];
+	uint8_t addr[16];
+	char addr_str[INET6_ADDRSTRLEN];
+	uint32_t p, v;
+
+	/* Find the autoconfiguration prefix that offers the longest lifetime */
+	for (i = 1; i < r->n_ac_prefixes; i++)
+		if (r->ac_prefixes[i].preferred_lifetime >
+				longest->preferred_lifetime)
+			longest = &r->ac_prefixes[i];
+
+	memcpy(addr, longest->prefix, 8);
+	/* EUI-64-based Interface Identifier (RFC2464 Section 4) */
+	addr[ 8] = nc->mac[0] ^ 0x02;
+	addr[ 9] = nc->mac[1];
+	addr[10] = nc->mac[2];
+	addr[11] = 0xff;
+	addr[12] = 0xfe;
+	addr[13] = nc->mac[3];
+	addr[14] = nc->mac[4];
+	addr[15] = nc->mac[5];
+	inet_ntop(AF_INET6, addr, addr_str, sizeof(addr_str));
+	p = longest->preferred_lifetime;
+	v = longest->valid_lifetime;
+
+	nc->v6_address = l_rtnl_address_new(addr_str, 128);
+	l_rtnl_address_set_noprefixroute(nc->v6_address, true);
+
+	if (p != 0xffffffff || v != 0xffffffff) {
+		l_rtnl_address_set_lifetimes(nc->v6_address,
+					p != 0xffffffff ? p : 0,
+					v != 0xffffffff ? v : 0);
+		l_rtnl_address_set_expiry(nc->v6_address,
+					p != 0xffffffff ?
+					r->start_time + p * L_USEC_PER_SEC : 0,
+					v != 0xffffffff ?
+					r->start_time + v * L_USEC_PER_SEC : 0);
+	}
+
+	l_queue_push_tail(nc->addresses.current, nc->v6_address);
+	l_queue_push_tail(nc->addresses.added, nc->v6_address);
+
+	if (nc->v6_auto_method == NETCONFIG_V6_METHOD_SLAAC ||
+			nc->slaac_dnses) {
+		nc->v6_configured = true;
+		netconfig_emit_event(nc, AF_INET6, L_NETCONFIG_EVENT_CONFIGURE);
+	} else
+		netconfig_emit_event(nc, AF_INET6, L_NETCONFIG_EVENT_UPDATE);
+
+	/* TODO: set a renew timeout */
+}
+
+static void netconfig_set_slaac_address_lifetimes(struct l_netconfig *nc,
+						const struct l_icmp6_router *r)
+{
+	const uint8_t *addr = l_rtnl_address_get_in_addr(nc->v6_address);
+	bool updated = false;
+	uint64_t p_expiry;
+	uint64_t v_expiry;
+	uint32_t remaining = 0xffffffff;
+	unsigned int i;
+
+	if (L_WARN_ON(!addr))
+		return;
+
+	l_rtnl_address_get_expiry(nc->v6_address, &p_expiry, &v_expiry);
+
+	if (v_expiry)
+		remaining = (v_expiry - r->start_time) / L_USEC_PER_SEC;
+
+	for (i = 0; i < r->n_ac_prefixes; i++) {
+		const struct autoconf_prefix_info *prefix = &r->ac_prefixes[i];
+		uint32_t p = prefix->preferred_lifetime;
+		uint32_t v = prefix->valid_lifetime;
+
+		if (memcmp(prefix->prefix, addr, 8))
+			continue;
+
+		/* RFC4862 Section 5.5.3 e) */
+		if (v < 120 * 60 && v < remaining)
+			v = 120 * 60; /* 2 hours */
+
+		l_rtnl_address_set_lifetimes(nc->v6_address,
+						p != 0xffffffff ? p : 0,
+						v != 0xffffffff ? v : 0);
+		p_expiry = p != 0xffffffff ? r->start_time + p * L_USEC_PER_SEC : 0;
+		v_expiry = v != 0xffffffff ? r->start_time + v * L_USEC_PER_SEC : 0;
+		l_rtnl_address_set_expiry(nc->v6_address, p_expiry, v_expiry);
+		updated = true;
+
+		/*
+		 * TODO: modify the renew timeout.
+		 *
+		 * Also we probably want to apply a mechanism similar to that
+		 * in netconfig_check_route_need_update() to avoid generating
+		 * and UPDATED event for every RA that covers this prefix
+		 * with constant lifetime values.
+		 */
+	}
+
+	if (updated && !l_queue_find(nc->addresses.added, netconfig_match,
+					nc->v6_address))
+		l_queue_push_tail(nc->addresses.updated, nc->v6_address);
+}
+
+static bool netconfig_process_slaac_dns_info(struct l_netconfig *nc,
+						const struct l_icmp6_router *r)
+{
+	bool updated = false;
+	unsigned int i;
+	unsigned int n_dns = l_queue_length(nc->slaac_dnses);
+	unsigned int n_domains = l_queue_length(nc->slaac_domains);
+
+	for (i = 0; i < r->n_dns; i++) {
+		const struct dns_info *info = &r->dns_list[i];
+
+		/*
+		 * For simplicity don't track lifetimes (TODO), add entries
+		 * when the lifetime is non-zero, remove them when the
+		 * lifetime is zero.  We have no API to add time-limited
+		 * entries to the system either.
+		 *
+		 * RFC8106 Section 5.1: "A value of zero means that the RDNSS
+		 * addresses MUST no longer be used."
+		 */
+		if (info->lifetime) {
+			if (n_dns >= max_icmp6_dnses)
+				continue;
+
+			if (l_queue_find(nc->slaac_dnses, netconfig_match_addr,
+						info->address))
+				continue;
+
+			l_queue_push_tail(nc->slaac_dnses,
+						l_memdup(info->address, 16));
+			n_dns++;
+		} else {
+			void *addr = l_queue_remove_if(nc->slaac_dnses,
+							netconfig_match_addr,
+							info->address);
+
+			if (!addr)
+				continue;
+
+			l_free(addr);
+			n_dns--;
+		}
+
+		updated = true;
+	}
+
+	for (i = 0; i < r->n_domains; i++) {
+		const struct domain_info *info = &r->domains[i];
+
+		/*
+		 * RFC8106 Section 5.2: "A value of zero means that the DNSSL
+		 * domain names MUST no longer be used."
+		 */
+		if (info->lifetime) {
+			if (n_domains >= max_icmp6_domains)
+				continue;
+
+			if (l_queue_find(nc->slaac_domains, netconfig_match_str,
+						info->domain))
+				continue;
+
+			l_queue_push_tail(nc->slaac_domains,
+						l_strdup(info->domain));
+			n_domains++;
+		} else {
+			void *str = l_queue_remove_if(nc->slaac_domains,
+							netconfig_match_str,
+							info->domain);
+
+			if (!str)
+				continue;
+
+			l_free(str);
+			n_domains--;
+		}
+
+		updated = true;
+	}
+
+	return updated;
+}
+
+static uint64_t now;
+
+static bool netconfig_check_route_expired(void *data, void *user_data)
+{
+	struct l_netconfig *nc = user_data;
+	struct netconfig_route_data *rd = data;
+
+	if (!rd->kernel_expiry || now < rd->kernel_expiry)
+		return false;
+
+	/*
+	 * Since we set lifetimes on the routes we submit to the kernel with
+	 * RTM_NEWROUTE, we count on them being deleted automatically so no
+	 * need to send an RTM_DELROUTE.  We signal the fact that the route
+	 * expired to the user by having it on the expired list but there's
+	 * nothing that the user needs to do with the routes on that list
+	 * like they do with the added, updated and removed lists.
+	 *
+	 * If for some reason the route is still on the added list, drop it
+	 * from there and there's nothing to notify the user of.
+	 */
+	if (!l_queue_remove(nc->routes.added, rd->route))
+		l_queue_push_tail(nc->routes.expired, rd->route);
+
+	l_queue_remove(nc->routes.current, rd->route);
+	l_queue_remove(nc->routes.updated, rd->route);
+	l_queue_remove(nc->routes.removed, rd->route);
+	return true;
+}
+
+static void netconfig_expire_routes(struct l_netconfig *nc)
+{
+	now = l_time_now();
+
+	if (l_queue_foreach_remove(nc->icmp_route_data,
+					netconfig_check_route_expired, nc) &&
+			!l_queue_isempty(nc->routes.expired) &&
+			!nc->signal_expired_work)
+		nc->signal_expired_work = l_idle_create(
+						netconfig_signal_expired,
+						nc, NULL);
+}
+
+static struct netconfig_route_data *netconfig_find_icmp6_route(
+						struct l_netconfig *nc,
+						const uint8_t *gateway,
+						const struct route_info *dst)
+{
+	const struct l_queue_entry *entry;
+
+	for (entry = l_queue_get_entries(nc->icmp_route_data); entry;
+			entry = entry->next) {
+		struct netconfig_route_data *rd = entry->data;
+		const uint8_t *route_gateway;
+		const uint8_t *route_dst;
+		uint8_t route_prefix_len = 0;
+
+		route_gateway = l_rtnl_route_get_gateway_in_addr(rd->route);
+		if ((gateway || route_gateway) &&
+				(!gateway || !route_gateway ||
+				 memcmp(gateway, route_gateway, 16)))
+			continue;
+
+		route_dst = l_rtnl_route_get_dst_in_addr(rd->route,
+							&route_prefix_len);
+		if ((dst || route_prefix_len) &&
+				(!dst || !route_prefix_len ||
+				 dst->prefix_len != route_prefix_len ||
+				 memcmp(dst->address, route_dst, 16)))
+			continue;
+
+		return rd;
+	}
+
+	return NULL;
+}
+
+static struct netconfig_route_data *netconfig_add_icmp6_route(
+						struct l_netconfig *nc,
+						const uint8_t *gateway,
+						const struct route_info *dst,
+						uint8_t preference)
+{
+	struct netconfig_route_data *rd;
+	struct l_rtnl_route *rt;
+
+	if (l_queue_length(nc->icmp_route_data) >= max_icmp6_routes)
+		return NULL;	/* TODO: log a warning the first time */
+
+	rt = netconfig_route_new(nc, AF_INET6, dst ? dst->address : NULL,
+					dst ? dst->prefix_len : 0, gateway,
+					RTPROT_RA);
+	if (L_WARN_ON(!rt))
+		return NULL;
+
+	l_rtnl_route_set_preference(rt, preference);
+	l_queue_push_tail(nc->routes.current, rt);
+	l_queue_push_tail(nc->routes.added, rt);
+
+	rd = l_new(struct netconfig_route_data, 1);
+	rd->route = rt;
+	l_queue_push_tail(nc->icmp_route_data, rd);
+	return rd;
+}
+
+static bool netconfig_check_route_need_update(
+					const struct netconfig_route_data *rd,
+					const struct l_icmp6_router *ra,
+					uint64_t new_expiry,
+					uint64_t old_expiry)
+{
+	/*
+	 * Decide whether the route is close enough to its expiry time that,
+	 * based on the expected Router Advertisement frequency, we should
+	 * notify the user and have them update the route's lifetime in the
+	 * kernel.  This is an optimization to avoid triggering a syscall and
+	 * potentially multiple context-switches in case we expect to have
+	 * many more opportunities to update the lifetime before we even get
+	 * close to the last expiry time we passed to the kernel.  Without
+	 * this we might be wasting a lot of cycles over time if the RAs are
+	 * frequent.
+	 *
+	 * Always update if we have no RA interval information or if the
+	 * expiry is moved forward.
+	 */
+	if (!rd->max_ra_interval || new_expiry < rd->kernel_expiry)
+		return true;
+
+	return rd->kernel_expiry < ra->start_time + rd->max_ra_interval * 10;
+}
+
+static void netconfig_set_icmp6_route_data(struct l_netconfig *nc,
+						struct netconfig_route_data *rd,
+						const struct l_icmp6_router *ra,
+						uint32_t valid_lifetime,
+						uint32_t mtu, bool updated)
+{
+	uint64_t expiry = ra->start_time + valid_lifetime * L_USEC_PER_SEC;
+	uint64_t old_expiry = l_rtnl_route_get_expiry(rd->route);
+	bool differs = false;
+
+	if (mtu != l_rtnl_route_get_mtu(rd->route)) {
+		l_rtnl_route_set_mtu(rd->route, mtu);
+		differs = true;
+	}
+
+	/*
+	 * The route's lifetime is pretty useless on its own but keep it
+	 * updated with the value from the last RA.  Routers can send the same
+	 * lifetime in every RA, keep decreasing the lifetimes linearly or
+	 * implement any other policy, regardless of whether the resulting
+	 * expiry time varies or not.
+	 */
+	l_rtnl_route_set_lifetime(rd->route, valid_lifetime);
+
+	if (rd->last_ra_time) {
+		uint64_t interval = ra->start_time - rd->last_ra_time;
+
+		if (interval > rd->max_ra_interval)
+			rd->max_ra_interval = interval;
+	}
+
+	rd->last_ra_time = ra->start_time;
+
+	/*
+	 * valid_lifetime of 0 from a route_info means the route is being
+	 * removed so we wouldn't be here.  valid_lifetime of 0xffffffff
+	 * means no timeout.  Check if the lifetime is changing between
+	 * finite and infinite, or two finite values that result in expiry
+	 * time difference of more than a second -- to avoid emitting
+	 * updates for changes resulting only from the valid_lifetime one
+	 * second resolution and RA transmission jitter.  As RFC4861
+	 * Section 6.2.7 puts it: "Due to link propagation delays and
+	 * potentially poorly synchronized clocks between the routers such
+	 * comparison SHOULD allow some time skew."  The RFC talks about
+	 * routers processing one another's RAs but the same logic applies
+	 * here.
+	 */
+	if (valid_lifetime == 0xffffffff)
+		expiry = 0;
+
+	if ((expiry || old_expiry) &&
+			(!expiry || !old_expiry ||
+			 l_time_diff(expiry, old_expiry) > L_USEC_PER_SEC)) {
+		l_rtnl_route_set_expiry(rd->route, expiry);
+
+		differs = differs || !expiry || !old_expiry ||
+			netconfig_check_route_need_update(rd, ra,
+							expiry, old_expiry);
+	}
+
+	if (updated && differs && !netconfig_route_exists(nc->routes.added,
+								rd->route)) {
+		l_queue_push_tail(nc->routes.updated, rd->route);
+		rd->kernel_expiry = expiry;
+	}
+}
+
+static void netconfig_remove_icmp6_route(struct l_netconfig *nc,
+						struct netconfig_route_data *rd)
+{
+	l_queue_remove(nc->icmp_route_data, rd);
+	l_queue_remove(nc->routes.current, rd->route);
+	l_queue_remove(nc->routes.updated, rd->route);
+
+	if (!l_queue_remove(nc->routes.added, rd->route))
+		l_queue_push_tail(nc->routes.removed, rd->route);
+}
+
+static void netconfig_icmp6_event_handler(struct l_icmp6_client *client,
+						enum l_icmp6_client_event event,
+						void *event_data,
+						void *user_data)
+{
+	struct l_netconfig *nc = user_data;
+	const struct l_icmp6_router *r;
+	struct netconfig_route_data *default_rd;
+	unsigned int i;
+	bool dns_updated = false;
+
+	if (event != L_ICMP6_CLIENT_EVENT_ROUTER_FOUND)
+		return;
+
+	r = event_data;
+
+	if (nc->ra_timeout)
+		l_timeout_remove(l_steal_ptr(nc->ra_timeout));
+
+	netconfig_expire_routes(nc);
+
+	if (nc->v6_gateway_override)
+		goto process_nondefault_routes;
+
+	/* Process the default gateway information */
+	default_rd = netconfig_find_icmp6_route(nc, r->address, NULL);
+
+	if (!default_rd && r->lifetime) {
+		default_rd = netconfig_add_icmp6_route(nc, r->address, NULL,
+								r->pref);
+		if (unlikely(!default_rd))
+			return;
+
+		/*
+		 * r->lifetime is 16-bit only so there's no risk it gets
+		 * confused for the special 0xffffffff value in
+		 * netconfig_set_icmp6_route_data.
+		 */
+		netconfig_set_icmp6_route_data(nc, default_rd, r, r->lifetime,
+						r->mtu, false);
+	} else if (default_rd && r->lifetime)
+		netconfig_set_icmp6_route_data(nc, default_rd, r, r->lifetime,
+						r->mtu, true);
+	else if (default_rd && !r->lifetime)
+		netconfig_remove_icmp6_route(nc, default_rd);
+
+process_nondefault_routes:
+	/*
+	 * Process the onlink and offlink routes, from the Router
+	 * Advertisement's Prefix Information options and Route
+	 * Information options respectively.
+	 */
+	for (i = 0; i < r->n_routes; i++) {
+		const struct route_info *info = &r->routes[i];
+		const uint8_t *gateway = info->onlink ? NULL : r->address;
+		struct netconfig_route_data *rd =
+			netconfig_find_icmp6_route(nc, gateway, info);
+
+		if (!rd && info->valid_lifetime) {
+			rd = netconfig_add_icmp6_route(nc, gateway, info,
+							info->preference);
+			if (unlikely(!rd))
+				continue;
+
+			netconfig_set_icmp6_route_data(nc, rd, r,
+						info->valid_lifetime,
+						gateway ? r->mtu : 0, false);
+		} else if (rd && info->valid_lifetime)
+			netconfig_set_icmp6_route_data(nc, rd, r,
+						info->valid_lifetime,
+						gateway ? r->mtu : 0, true);
+		else if (rd && !info->valid_lifetime)
+			netconfig_remove_icmp6_route(nc, rd);
+	}
+
+	/*
+	 * Do this first so that any changes are included in the event
+	 * emitted next, be it UPDATE or CONFIGURE.
+	 */
+	if (r->n_dns || r->n_domains) {
+		if (!nc->slaac_dnses && r->n_dns)
+			nc->slaac_dnses = l_queue_new();
+
+		if (!nc->slaac_domains && r->n_domains)
+			nc->slaac_domains = l_queue_new();
+
+		dns_updated = netconfig_process_slaac_dns_info(nc, r);
+	}
+
+	/*
+	 * For lack of a better policy, select between DHCPv6 and SLAAC based
+	 * on the first RA received.  Prefer DHCPv6.
+	 *
+	 * Just like we currently only request one address in l_dhcp6_client,
+	 * we only set up one address using SLAAC regardless of how many
+	 * prefixes are available.  Generate the address in the prefix that
+	 * offers the longest preferred_lifetime.
+	 */
+	if (nc->v6_auto_method == NETCONFIG_V6_METHOD_UNSET &&
+			l_icmp6_router_get_managed(r)) {
+		nc->v6_auto_method = NETCONFIG_V6_METHOD_DHCP;
+		l_dhcp6_client_set_stateless(nc->dhcp6_client, false);
+
+		if (!netconfig_check_start_dhcp6(nc)) {
+			netconfig_failed(nc, AF_INET6);
+			return;
+		}
+
+		goto emit_event;
+	}
+
+	/*
+	 * Stateful DHCP not available according to this router, check if
+	 * any of the prefixes allow SLAAC.
+	 */
+	if (nc->v6_auto_method == NETCONFIG_V6_METHOD_UNSET &&
+			r->n_ac_prefixes) {
+		if (l_icmp6_router_get_other(r)) {
+			nc->v6_auto_method = NETCONFIG_V6_METHOD_SLAAC_DHCP;
+			l_dhcp6_client_set_stateless(nc->dhcp6_client, true);
+			netconfig_check_start_dhcp6(nc);
+		} else
+			nc->v6_auto_method = NETCONFIG_V6_METHOD_SLAAC;
+
+		/*
+		 * The DAD for the link-local address may be still running
+		 * but again we can generate the global address already and
+		 * commit it to start in-kernel DAD for it.
+		 *
+		 * The global address alone should work for most uses.  On
+		 * the other hand since both the link-local address and the
+		 * global address are based on the same MAC, there's some
+		 * correlation between one failing DAD and the other
+		 * failing DAD due to another host using the same address.
+		 * As RFC4862 Section 5.4 notes we can't rely on that to
+		 * skip DAD for one of the addresses.
+		 */
+
+		netconfig_add_slaac_address(nc, r);
+		return;
+	}
+
+	/* Neither method seems available, fail */
+	if (nc->v6_auto_method == NETCONFIG_V6_METHOD_UNSET) {
+		netconfig_failed(nc, AF_INET6);
+		return;
+	}
+
+	/* DHCP already started or waiting for the LL address, nothing to do */
+	if (nc->v6_auto_method == NETCONFIG_V6_METHOD_DHCP)
+		goto emit_event;
+
+	/*
+	 * Otherwise we already have a SLAAC address, just check if any of the
+	 * auto-configuration prefixes in this RA covers our existing address
+	 * and allows us to extend its lifetime.
+	 */
+	netconfig_set_slaac_address_lifetimes(nc, r);
+
+emit_event:
+	/*
+	 * Note: we may be emitting this before L_NETCONFIG_EVENT_CONFIGURE.
+	 * We should probably instead save the affected routes in separate
+	 * lists and add them to the _CONFIGURE event, suppressing any _UPDATE
+	 * events while nc->v6_configured is false.
+	 */
+	if (!l_queue_isempty(nc->routes.added) ||
+			!l_queue_isempty(nc->routes.updated) ||
+			!l_queue_isempty(nc->routes.removed) ||
+			!l_queue_isempty(nc->routes.expired) ||
+			!l_queue_isempty(nc->addresses.updated) ||
+			dns_updated)
+		netconfig_emit_event(nc, AF_INET6, L_NETCONFIG_EVENT_UPDATE);
+}
+
+static int netconfig_proc_write_ipv6_uint_setting(struct l_netconfig *nc,
+							const char *setting,
+							unsigned int value)
+{
+	char ifname[IF_NAMESIZE];
+	_auto_(l_free) char *filename = NULL;
+	_auto_(close) int fd = -1;
+	int r;
+	char valuestr[20];
+
+	if (unlikely(!if_indextoname(nc->ifindex, ifname)))
+		return -errno;
+
+	filename = l_strdup_printf("/proc/sys/net/ipv6/conf/%s/%s",
+					ifname, setting);
+
+	fd = L_TFR(open(filename, O_WRONLY));
+	if (unlikely(fd < 0))
+		return -errno;
+
+	snprintf(valuestr, sizeof(valuestr), "%u", value);
+	r = L_TFR(write(fd, valuestr, strlen(valuestr)));
+	return r > 0 ? 0 : -errno;
+}
+
+static long netconfig_proc_read_ipv6_uint_setting(struct l_netconfig *nc,
+							const char *setting)
+{
+	char ifname[IF_NAMESIZE];
+	_auto_(l_free) char *filename = NULL;
+	_auto_(close) int fd = -1;
+	int r;
+	char valuestr[20];
+	long value;
+	char *endp;
+
+	if (unlikely(!if_indextoname(nc->ifindex, ifname)))
+		return -errno;
+
+	filename = l_strdup_printf("/proc/sys/net/ipv6/conf/%s/%s",
+					ifname, setting);
+
+	fd = L_TFR(open(filename, O_RDONLY));
+	if (unlikely(fd < 0))
+		return -errno;
+
+	r = L_TFR(read(fd, valuestr, sizeof(valuestr) - 1));
+	if (unlikely(r < 1))
+		return r == 0 ? -EINVAL : -errno;
+
+	valuestr[r - 1] = '\0';
+	errno = 0;
+	value = strtoul(valuestr, &endp, 10);
+
+	if (unlikely(errno || !L_IN_SET(*endp, '\n', '\0')))
+		return -EINVAL;
+
+	return value;
+}
+
+LIB_EXPORT struct l_netconfig *l_netconfig_new(uint32_t ifindex)
+{
+	struct l_netconfig *nc;
+
+	nc = l_new(struct l_netconfig, 1);
+	nc->ifindex = ifindex;
+
+	nc->addresses.current = l_queue_new();
+	nc->addresses.added = l_queue_new();
+	nc->addresses.updated = l_queue_new();
+	nc->addresses.removed = l_queue_new();
+	nc->routes.current = l_queue_new();
+	nc->routes.added = l_queue_new();
+	nc->routes.updated = l_queue_new();
+	nc->routes.removed = l_queue_new();
+	nc->icmp_route_data = l_queue_new();
+
+	nc->dhcp_client = l_dhcp_client_new(ifindex);
+	l_dhcp_client_set_event_handler(nc->dhcp_client,
+					netconfig_dhcp_event_handler,
+					nc, NULL);
+
+	nc->dhcp6_client = l_dhcp6_client_new(ifindex);
+	l_dhcp6_client_set_nora(nc->dhcp6_client, true);
+	l_dhcp6_client_set_event_handler(nc->dhcp6_client,
+					netconfig_dhcp6_event_handler,
+					nc, NULL);
+
+	nc->icmp6_client = l_dhcp6_client_get_icmp6(nc->dhcp6_client);
+	l_icmp6_client_add_event_handler(nc->icmp6_client,
+					netconfig_icmp6_event_handler,
+					nc, NULL);
+
+	/* Disable in-kernel autoconfiguration for the interface */
+	netconfig_proc_write_ipv6_uint_setting(nc, "accept_ra", 0);
+
+	l_netconfig_reset_config(nc);
+	return nc;
+}
+
+LIB_EXPORT void l_netconfig_destroy(struct l_netconfig *netconfig)
+{
+	if (unlikely(!netconfig))
+		return;
+
+	l_netconfig_stop(netconfig);
+
+	l_netconfig_set_static_addr(netconfig, AF_INET, NULL);
+	l_netconfig_set_gateway_override(netconfig, AF_INET, NULL);
+	l_netconfig_set_dns_override(netconfig, AF_INET, NULL);
+	l_netconfig_set_domain_names_override(netconfig, AF_INET, NULL);
+	l_netconfig_set_static_addr(netconfig, AF_INET6, NULL);
+	l_netconfig_set_gateway_override(netconfig, AF_INET6, NULL);
+	l_netconfig_set_dns_override(netconfig, AF_INET6, NULL);
+	l_netconfig_set_domain_names_override(netconfig, AF_INET6, NULL);
+
+	l_dhcp_client_destroy(netconfig->dhcp_client);
+	l_dhcp6_client_destroy(netconfig->dhcp6_client);
+	l_netconfig_set_event_handler(netconfig, NULL, NULL, NULL);
+	l_queue_destroy(netconfig->addresses.current, NULL);
+	l_queue_destroy(netconfig->addresses.added, NULL);
+	l_queue_destroy(netconfig->addresses.updated, NULL);
+	l_queue_destroy(netconfig->addresses.removed, NULL);
+	l_queue_destroy(netconfig->routes.current, NULL);
+	l_queue_destroy(netconfig->routes.added, NULL);
+	l_queue_destroy(netconfig->routes.updated, NULL);
+	l_queue_destroy(netconfig->routes.removed, NULL);
+	l_queue_destroy(netconfig->icmp_route_data, NULL);
+	l_free(netconfig);
+}
+
+/*
+ * The following l_netconfig_set_* functions configure the l_netconfig's
+ * client settings.  The setters can be called independently, without
+ * following a specific order.  Most of the setters will not validate the
+ * values passed, l_netconfig_start() will fail if settings are incorrect
+ * or inconsistent between themselves, e.g. if the static local IP and
+ * gateway IP are not in the same subnet.  Alternatively
+ * l_netconfig_check_config() can be called at any point to validate the
+ * current configuration.  The configuration can only be changed while
+ * the l_netconfig state machine is stopped, i.e. before
+ * l_netconfig_start() and after l_netconfig_stop().
+ *
+ * l_netconfig_set_hostname, l_netconfig_set_static_addr,
+ * l_netconfig_set_gateway_override, l_netconfig_set_dns_override and
+ * l_netconfig_set_domain_names_override can be passed NULL to unset a
+ * value that had been set before (revert to auto).  This is why the
+ * family parameter is needed even when it could otherwise be derived
+ * from the new value that is passed.
+ */
+LIB_EXPORT bool l_netconfig_set_family_enabled(struct l_netconfig *netconfig,
+						uint8_t family, bool enabled)
+{
+	if (unlikely(!netconfig || netconfig->started))
+		return false;
+
+	switch (family) {
+	case AF_INET:
+		netconfig->v4_enabled = enabled;
+		return true;
+	case AF_INET6:
+		netconfig->v6_enabled = enabled;
+		return true;
+	}
+
+	return false;
+}
+
+LIB_EXPORT bool l_netconfig_set_hostname(struct l_netconfig *netconfig,
+						const char *hostname)
+{
+	if (unlikely(!netconfig || netconfig->started))
+		return false;
+
+	return l_dhcp_client_set_hostname(netconfig->dhcp_client, hostname);
+}
+
+LIB_EXPORT bool l_netconfig_set_route_priority(struct l_netconfig *netconfig,
+						uint32_t priority)
+{
+	if (unlikely(!netconfig || netconfig->started))
+		return false;
+
+	netconfig->route_priority = priority;
+	return true;
+}
+
+LIB_EXPORT bool l_netconfig_set_static_addr(struct l_netconfig *netconfig,
+					uint8_t family,
+					const struct l_rtnl_address *addr)
+{
+	struct l_rtnl_address **ptr;
+
+	if (unlikely(!netconfig || netconfig->started))
+		return false;
+
+	if (addr && l_rtnl_address_get_family(addr) != family)
+		return false;
+
+	switch (family) {
+	case AF_INET:
+		ptr = &netconfig->v4_static_addr;
+		break;
+	case AF_INET6:
+		ptr = &netconfig->v6_static_addr;
+		break;
+	default:
+		return false;
+	}
+
+	l_rtnl_address_free(*ptr);
+	*ptr = NULL;
+
+	if (!addr)
+		return true;
+
+	*ptr = l_rtnl_address_clone(addr);
+	l_rtnl_address_set_lifetimes(*ptr, 0, 0);
+	l_rtnl_address_set_noprefixroute(*ptr, true);
+	return true;
+}
+
+LIB_EXPORT bool l_netconfig_set_gateway_override(struct l_netconfig *netconfig,
+							uint8_t family,
+							const char *gateway_str)
+{
+	char **ptr;
+
+	if (unlikely(!netconfig || netconfig->started))
+		return false;
+
+	switch (family) {
+	case AF_INET:
+		ptr = &netconfig->v4_gateway_override;
+		break;
+	case AF_INET6:
+		ptr = &netconfig->v6_gateway_override;
+		break;
+	default:
+		return false;
+	}
+
+	l_free(*ptr);
+	*ptr = NULL;
+
+	if (!gateway_str)
+		return true;
+
+	*ptr = l_strdup(gateway_str);
+	return true;
+}
+
+LIB_EXPORT bool l_netconfig_set_dns_override(struct l_netconfig *netconfig,
+						uint8_t family, char **dns_list)
+{
+	char ***ptr;
+
+	if (unlikely(!netconfig || netconfig->started))
+		return false;
+
+	switch (family) {
+	case AF_INET:
+		ptr = &netconfig->v4_dns_override;
+		break;
+	case AF_INET6:
+		ptr = &netconfig->v6_dns_override;
+		break;
+	default:
+		return false;
+	}
+
+	l_strv_free(*ptr);
+	*ptr = NULL;
+
+	if (!dns_list)
+		return true;
+
+	*ptr = l_strv_copy(dns_list);
+	return true;
+}
+
+LIB_EXPORT bool l_netconfig_set_domain_names_override(
+						struct l_netconfig *netconfig,
+						uint8_t family, char **names)
+{
+	char ***ptr;
+
+	if (unlikely(!netconfig || netconfig->started))
+		return false;
+
+	switch (family) {
+	case AF_INET:
+		ptr = &netconfig->v4_domain_names_override;
+		break;
+	case AF_INET6:
+		ptr = &netconfig->v6_domain_names_override;
+		break;
+	default:
+		return false;
+	}
+
+	l_strv_free(*ptr);
+	*ptr = NULL;
+
+	if (!names)
+		return true;
+
+	*ptr = l_strv_copy(names);
+	return true;
+}
+
+LIB_EXPORT bool l_netconfig_set_acd_enabled(struct l_netconfig *netconfig,
+						bool enabled)
+{
+	if (unlikely(!netconfig || netconfig->started))
+		return false;
+
+	netconfig->acd_enabled = enabled;
+	return true;
+}
+
+LIB_EXPORT bool l_netconfig_set_optimistic_dad_enabled(
+						struct l_netconfig *netconfig,
+						bool enabled)
+{
+	if (unlikely(!netconfig || netconfig->started))
+		return false;
+
+	netconfig->optimistic_dad_enabled = enabled;
+	return true;
+}
+
+static bool netconfig_check_family_config(struct l_netconfig *nc,
+						uint8_t family)
+{
+	struct l_rtnl_address *static_addr = (family == AF_INET) ?
+		nc->v4_static_addr : nc->v6_static_addr;
+	char *gateway_override = (family == AF_INET) ?
+		nc->v4_gateway_override : nc->v6_gateway_override;
+	char **dns_override = (family == AF_INET) ?
+		nc->v4_dns_override : nc->v6_dns_override;
+	unsigned int dns_num = 0;
+
+	if (static_addr && family == AF_INET) {
+		uint8_t prefix_len =
+			l_rtnl_address_get_prefix_length(static_addr);
+
+		if (prefix_len > 30)
+			return false;
+	}
+
+	if (gateway_override) {
+		union netconfig_addr gateway;
+
+		if (inet_pton(family, gateway_override, &gateway) != 1)
+			return false;
+	}
+
+	if (dns_override && (dns_num = l_strv_length(dns_override))) {
+		unsigned int i;
+		_auto_(l_free) union netconfig_addr *dns_list =
+			l_new(union netconfig_addr, dns_num);
+
+		for (i = 0; i < dns_num; i++)
+			if (inet_pton(family, dns_override[i],
+					&dns_list[i]) != 1)
+				return false;
+	}
+
+	return true;
+}
+
+static bool netconfig_check_config(struct l_netconfig *nc)
+{
+	/* TODO: error reporting through a debug log handler or otherwise */
+
+	return netconfig_check_family_config(nc, AF_INET) &&
+		netconfig_check_family_config(nc, AF_INET6);
+}
+
+LIB_EXPORT bool l_netconfig_check_config(struct l_netconfig *netconfig)
+{
+	if (unlikely(!netconfig || netconfig->started))
+		return false;
+
+	return netconfig_check_config(netconfig);
+}
+
+LIB_EXPORT bool l_netconfig_reset_config(struct l_netconfig *netconfig)
+{
+	if (unlikely(!netconfig || netconfig->started))
+		return false;
+
+	l_netconfig_set_hostname(netconfig, NULL);
+	l_netconfig_set_route_priority(netconfig, 0);
+	l_netconfig_set_family_enabled(netconfig, AF_INET, true);
+	l_netconfig_set_static_addr(netconfig, AF_INET, NULL);
+	l_netconfig_set_gateway_override(netconfig, AF_INET, NULL);
+	l_netconfig_set_dns_override(netconfig, AF_INET, NULL);
+	l_netconfig_set_domain_names_override(netconfig, AF_INET, NULL);
+	l_netconfig_set_acd_enabled(netconfig, true);
+	l_netconfig_set_family_enabled(netconfig, AF_INET6, false);
+	l_netconfig_set_static_addr(netconfig, AF_INET6, NULL);
+	l_netconfig_set_gateway_override(netconfig, AF_INET6, NULL);
+	l_netconfig_set_dns_override(netconfig, AF_INET6, NULL);
+	l_netconfig_set_domain_names_override(netconfig, AF_INET6, NULL);
+	return true;
+}
+
+static void netconfig_add_v4_static_address_routes(struct l_netconfig *nc)
+{
+	char ip[INET_ADDRSTRLEN];
+	uint32_t prefix_len;
+
+	nc->v4_address = l_rtnl_address_clone(nc->v4_static_addr);
+	l_queue_push_tail(nc->addresses.current, nc->v4_address);
+	l_queue_push_tail(nc->addresses.added, nc->v4_address);
+
+	l_rtnl_address_get_address(nc->v4_static_addr, ip);
+	prefix_len = l_rtnl_address_get_prefix_length(nc->v4_static_addr);
+	netconfig_add_v4_routes(nc, ip, prefix_len, NULL, RTPROT_STATIC);
+}
+
+/*
+ * Just mirror the IPv4 behaviour with static IPv6 configuration.  It would
+ * be more logical to let the user choose between static IPv6 address and
+ * DHCPv6, and, completely independently, choose between static routes
+ * (if a static prefix length and/or gateway address is set) and ICMPv6.
+ * Yet a mechanism identical with IPv4 is easier to understand for a typical
+ * user so providing a static address just disables all automatic
+ * configuration.
+ */
+static void netconfig_add_v6_static_address_routes(struct l_netconfig *nc)
+{
+	char ip[INET6_ADDRSTRLEN];
+	uint32_t prefix_len;
+
+	nc->v6_address = l_rtnl_address_clone(nc->v6_static_addr);
+	l_queue_push_tail(nc->addresses.current, nc->v6_address);
+	l_queue_push_tail(nc->addresses.added, nc->v6_address);
+
+	l_rtnl_address_get_address(nc->v6_static_addr, ip);
+	prefix_len = l_rtnl_address_get_prefix_length(nc->v6_static_addr);
+	netconfig_add_v6_static_routes(nc, ip, prefix_len);
+}
+
+static void netconfig_ipv4_acd_event(enum l_acd_event event, void *user_data)
+{
+	struct l_netconfig *nc = user_data;
+
+	switch (event) {
+	case L_ACD_EVENT_AVAILABLE:
+		if (L_WARN_ON(nc->v4_configured))
+			break;
+
+		netconfig_add_v4_static_address_routes(nc);
+		nc->v4_configured = true;
+		netconfig_emit_event(nc, AF_INET, L_NETCONFIG_EVENT_CONFIGURE);
+		break;
+	case L_ACD_EVENT_CONFLICT:
+		if (L_WARN_ON(nc->v4_configured))
+			break;
+
+		/*
+		 * Conflict found, no IP was actually set or routes added so
+		 * just emit the event.
+		 */
+		netconfig_failed(nc, AF_INET);
+		break;
+	case L_ACD_EVENT_LOST:
+		if (L_WARN_ON(!nc->v4_configured))
+			break;
+
+		/*
+		 * Set IP but lost it some time later.  Reset IPv4 in this
+		 * case and emit the FAILED event since we have no way to
+		 * recover from here.
+		 */
+		netconfig_remove_v4_address_routes(nc, false);
+		nc->v4_configured = false;
+		netconfig_failed(nc, AF_INET);
+		break;
+	}
+}
+
+static void netconfig_do_static_config(struct l_idle *idle, void *user_data)
+{
+	struct l_netconfig *nc = user_data;
+
+	l_idle_remove(l_steal_ptr(nc->do_static_work));
+
+	if (nc->v4_static_addr && !nc->v4_configured) {
+		if (nc->acd_enabled) {
+			char ip[INET_ADDRSTRLEN];
+
+			l_rtnl_address_get_address(nc->v4_static_addr, ip);
+
+			nc->acd = l_acd_new(nc->ifindex);
+			l_acd_set_event_handler(nc->acd,
+						netconfig_ipv4_acd_event, nc,
+						NULL);
+
+			if (l_acd_start(nc->acd, ip))
+				goto configure_ipv6;
+
+			l_acd_destroy(l_steal_ptr(nc->acd));
+			/* Configure right now as a fallback */
+		}
+
+		netconfig_add_v4_static_address_routes(nc);
+		nc->v4_configured = true;
+		netconfig_emit_event(nc, AF_INET, L_NETCONFIG_EVENT_CONFIGURE);
+	}
+
+configure_ipv6:
+	if (nc->v6_static_addr && !nc->v6_configured) {
+		netconfig_add_v6_static_address_routes(nc);
+		nc->v6_configured = true;
+		netconfig_emit_event(nc, AF_INET6, L_NETCONFIG_EVENT_CONFIGURE);
+	}
+}
+
+static void netconfig_rtnl_unregister(void *user_data)
+{
+	struct l_netlink *rtnl = user_data;
+
+	if (!addr_wait_list || !l_queue_isempty(addr_wait_list))
+		return;
+
+	l_queue_destroy(l_steal_ptr(addr_wait_list), NULL);
+	l_netlink_unregister(rtnl, rtnl_id);
+	rtnl_id = 0;
+}
+
+static void netconfig_addr_wait_unregister(struct l_netconfig *nc,
+						bool in_notify)
+{
+	struct l_netlink *rtnl = l_rtnl_get();
+
+	if (nc->ifaddr6_dump_cmd_id) {
+		unsigned int cmd_id = nc->ifaddr6_dump_cmd_id;
+
+		nc->ifaddr6_dump_cmd_id = 0;
+		l_netlink_cancel(rtnl, cmd_id);
+	}
+
+	if (!l_queue_remove(addr_wait_list, nc))
+		return;
+
+	if (!l_queue_isempty(addr_wait_list))
+		return;
+
+	/* We can't do l_netlink_unregister() inside a notification */
+	if (in_notify)
+		l_idle_oneshot(netconfig_rtnl_unregister, rtnl, NULL);
+	else
+		netconfig_rtnl_unregister(rtnl);
+}
+
+static void netconfig_ifaddr_ipv6_added(struct l_netconfig *nc,
+					const struct ifaddrmsg *ifa,
+					uint32_t len)
+{
+	struct in6_addr in6;
+	_auto_(l_free) char *ip = NULL;
+	bool new_lla;
+
+	if ((ifa->ifa_flags & IFA_F_TENTATIVE) &&
+			!(ifa->ifa_flags & IFA_F_OPTIMISTIC))
+		return;
+
+	if (!nc->started)
+		return;
+
+	l_rtnl_ifaddr6_extract(ifa, len, &ip);
+	inet_pton(AF_INET6, ip, &in6);
+
+	if (!IN6_IS_ADDR_LINKLOCAL(&in6))
+		return;
+
+	new_lla = !nc->have_lla;
+	nc->have_lla = true;
+
+	if (!(ifa->ifa_flags & IFA_F_TENTATIVE))
+		netconfig_addr_wait_unregister(nc, true);
+	else if (nc->ifaddr6_dump_cmd_id) {
+		struct l_netlink *rtnl = l_rtnl_get();
+		unsigned int cmd_id = nc->ifaddr6_dump_cmd_id;
+
+		nc->ifaddr6_dump_cmd_id = 0;
+		l_netlink_cancel(rtnl, cmd_id);
+	}
+
+	l_dhcp6_client_set_link_local_address(nc->dhcp6_client, ip);
+	l_icmp6_client_set_link_local_address(nc->icmp6_client, ip,
+					!!(ifa->ifa_flags & IFA_F_OPTIMISTIC));
+
+	/*
+	 * Only now that we have a link-local address see if we can start
+	 * actual DHCPv6 setup.
+	 */
+	if (new_lla && !netconfig_check_start_dhcp6(nc))
+		netconfig_failed(nc, AF_INET6);
+}
+
+static void netconfig_ifaddr_ipv6_notify(uint16_t type, const void *data,
+						uint32_t len, void *user_data)
+{
+	const struct ifaddrmsg *ifa = data;
+	uint32_t bytes = len - NLMSG_ALIGN(sizeof(struct ifaddrmsg));
+	const struct l_queue_entry *entry, *next;
+
+	switch (type) {
+	case RTM_NEWADDR:
+		/* Iterate safely since elements may be removed */
+		for (entry = l_queue_get_entries(addr_wait_list); entry;
+				entry = next) {
+			struct l_netconfig *nc = entry->data;
+
+			next = entry->next;
+
+			if (ifa->ifa_index == nc->ifindex)
+				netconfig_ifaddr_ipv6_added(nc, ifa, bytes);
+		}
+
+		break;
+	}
+}
+
+static void netconfig_ifaddr_ipv6_dump_cb(int error, uint16_t type,
+						const void *data, uint32_t len,
+						void *user_data)
+{
+	struct l_netconfig *nc = user_data;
+
+	if (!nc->ifaddr6_dump_cmd_id || !nc->started)
+		return;
+
+	if (error) {
+		netconfig_failed(nc, AF_INET6);
+		return;
+	}
+
+	if (type != RTM_NEWADDR)
+		return;
+
+	netconfig_ifaddr_ipv6_notify(type, data, len, user_data);
+}
+
+static void netconfig_ifaddr_ipv6_dump_done_cb(void *user_data)
+{
+	struct l_netconfig *nc = user_data;
+
+	/*
+	 * Handle the case of no link-local address having been found during
+	 * the dump.  If nc->ifaddr6_dump_cmd_id is 0, we have found one or
+	 * the dump is being cancelled.  Otherwise try disabing the
+	 * "disable_ipv6" setting for the interface since it may have been
+	 * enabled.  Also write "addr_gen_mode" which triggers regerating
+	 * the link-local addresss on the interface in the kernel if it
+	 * was previously removed.
+	 */
+	if (!nc->ifaddr6_dump_cmd_id || !nc->started)
+		return;
+
+	nc->ifaddr6_dump_cmd_id = 0;
+
+	/* "do not generate a link-local address" */
+	netconfig_proc_write_ipv6_uint_setting(nc, "addr_gen_mode", 1);
+	/* "generate address based on EUI64 (default)" */
+	netconfig_proc_write_ipv6_uint_setting(nc, "addr_gen_mode", 0);
+
+	/* "enable IPv6 operation" */
+	nc->orig_disable_ipv6 =
+		netconfig_proc_read_ipv6_uint_setting(nc, "disable_ipv6");
+	if (nc->orig_disable_ipv6)
+		netconfig_proc_write_ipv6_uint_setting(nc, "disable_ipv6", 0);
+}
+
+LIB_EXPORT bool l_netconfig_start(struct l_netconfig *netconfig)
+{
+	bool optimistic_dad;
+
+	if (unlikely(!netconfig || netconfig->started))
+		return false;
+
+	if (!netconfig_check_config(netconfig))
+		return false;
+
+	if (!netconfig->v4_enabled)
+		goto configure_ipv6;
+
+	if (netconfig->v4_static_addr) {
+		/*
+		 * We're basically ready to configure the interface
+		 * but do this in an idle callback.
+		 */
+		netconfig->do_static_work = l_idle_create(
+						netconfig_do_static_config,
+						netconfig, NULL);
+		goto configure_ipv6;
+	}
+
+	if (!l_dhcp_client_start(netconfig->dhcp_client))
+		return false;
+
+configure_ipv6:
+	if (!netconfig->v6_enabled)
+		goto done;
+
+	/*
+	 * Enable optimistic DAD if the user has requested it *and* it is
+	 * recommended by RFC 4429 Section 3.1 for the address generation
+	 * method in use:
+	 *   * mac-based Interface ID such as EUI-64
+	 *   * random
+	 *   * well-distributed hash function
+	 *   * DHCPv6
+	 * i.e. all autoconfiguration methods.  In any other case disable
+	 * it.
+	 */
+	optimistic_dad = netconfig->optimistic_dad_enabled &&
+		!netconfig->v6_static_addr;
+	netconfig->orig_optimistic_dad =
+		netconfig_proc_read_ipv6_uint_setting(netconfig,
+							"optimistic_dad");
+
+	if (netconfig->orig_optimistic_dad >= 0 &&
+			!!netconfig->orig_optimistic_dad != optimistic_dad)
+		netconfig_proc_write_ipv6_uint_setting(netconfig,
+							"optimistic_dad",
+							optimistic_dad ? 1 : 0);
+
+	if (netconfig->v6_static_addr) {
+		/*
+		 * We're basically ready to configure the interface
+		 * but do this in an idle callback.
+		 */
+		if (!netconfig->do_static_work)
+			netconfig->do_static_work = l_idle_create(
+						netconfig_do_static_config,
+						netconfig, NULL);
+
+		goto done;
+	}
+
+	netconfig->v6_auto_method = NETCONFIG_V6_METHOD_UNSET;
+
+	/*
+	 * We only care about being on addr_wait_list if we're waiting for
+	 * the link-local address for DHCP6.  Add ourself to the list here
+	 * before we start the dump, instead of after it ends, to eliminate
+	 * the possibility of missing an RTM_NEWADDR between the end of
+	 * the dump command and registering for the events.
+	 *
+	 * We stay on that list until we receive a non-tentative LL address.
+	 * Note that we may set .have_lla earlier, specifically when we
+	 * receive a tentative LL address that is also optimistic.  We will
+	 * however stay on addr_wait_list because we want to notify
+	 * l_icmp6_client again when the LL address completes DAD and becomes
+	 * non-tentative.
+	 */
+	if (!addr_wait_list) {
+		addr_wait_list = l_queue_new();
+
+		rtnl_id = l_netlink_register(l_rtnl_get(), RTNLGRP_IPV6_IFADDR,
+						netconfig_ifaddr_ipv6_notify,
+						netconfig, NULL);
+		if (!rtnl_id)
+			goto unregister;
+	}
+
+	netconfig->ifaddr6_dump_cmd_id = l_rtnl_ifaddr6_dump(l_rtnl_get(),
+					netconfig_ifaddr_ipv6_dump_cb,
+					netconfig,
+					netconfig_ifaddr_ipv6_dump_done_cb);
+	if (!netconfig->ifaddr6_dump_cmd_id)
+		goto unregister;
+
+	l_queue_push_tail(addr_wait_list, netconfig);
+	netconfig->have_lla = false;
+
+	if (!l_net_get_mac_address(netconfig->ifindex, netconfig->mac))
+		goto unregister;
+
+	l_dhcp6_client_set_address(netconfig->dhcp6_client, ARPHRD_ETHER,
+					netconfig->mac, ETH_ALEN);
+	l_icmp6_client_set_address(netconfig->icmp6_client, netconfig->mac);
+
+	/*
+	 * RFC4862 Section 4: "To speed the autoconfiguration process, a host
+	 * may generate its link-local address (and verify its uniqueness) in
+	 * parallel with waiting for a Router Advertisement.  Because a router
+	 * may delay responding to a Router Solicitation for a few seconds,
+	 * the total time needed to complete autoconfiguration can be
+	 * significantly longer if the two steps are done serially."
+	 *
+	 * We don't know whether we have the LL address yet.  The interface
+	 * may have been just brought up and DAD may still running or the LL
+	 * address may have been deleted and won't be added until
+	 * netconfig_ifaddr_ipv6_dump_done_cb() writes the /proc settings.
+	 * In any case the Router Solicitation doesn't depend on having the
+	 * LL address so send it now.  We won't start DHCPv6 however until we
+	 * have both the LL address and the Router Advertisement.
+	 */
+	if (!l_icmp6_client_start(netconfig->icmp6_client))
+		goto unregister;
+
+	netconfig->ra_timeout = l_timeout_create(10, netconfig_ra_timeout_cb,
+							netconfig, NULL);
+
+done:
+	netconfig->started = true;
+	return true;
+
+unregister:
+	netconfig_addr_wait_unregister(netconfig, false);
+
+	if (netconfig->v4_enabled) {
+		if (netconfig->v4_static_addr)
+			l_idle_remove(l_steal_ptr(netconfig->do_static_work));
+		else
+			l_dhcp_client_stop(netconfig->dhcp_client);
+	}
+
+	return false;
+}
+
+LIB_EXPORT void l_netconfig_stop(struct l_netconfig *netconfig)
+{
+	bool optimistic_dad;
+
+	if (unlikely(!netconfig || !netconfig->started))
+		return;
+
+	netconfig->started = false;
+
+	if (netconfig->do_static_work)
+		l_idle_remove(l_steal_ptr(netconfig->do_static_work));
+
+	if (netconfig->signal_expired_work)
+		l_idle_remove(l_steal_ptr(netconfig->signal_expired_work));
+
+	if (netconfig->ra_timeout)
+		l_timeout_remove(l_steal_ptr(netconfig->ra_timeout));
+
+	netconfig_addr_wait_unregister(netconfig, false);
+
+	netconfig_update_cleanup(netconfig);
+	l_queue_clear(netconfig->addresses.current,
+			(l_queue_destroy_func_t) l_rtnl_address_free);
+	l_queue_clear(netconfig->routes.current,
+			(l_queue_destroy_func_t) l_rtnl_route_free);
+	l_queue_clear(netconfig->icmp_route_data, l_free);
+	l_queue_clear(netconfig->slaac_dnses, l_free);
+	l_queue_clear(netconfig->slaac_domains, l_free);
+	netconfig->v4_address = NULL;
+	netconfig->v4_subnet_route = NULL;
+	netconfig->v4_default_route = NULL;
+	netconfig->v6_address = NULL;
+	netconfig->v4_configured = false;
+	netconfig->v6_configured = false;
+
+	l_dhcp_client_stop(netconfig->dhcp_client);
+	l_dhcp6_client_stop(netconfig->dhcp6_client);
+	l_icmp6_client_stop(netconfig->icmp6_client);
+
+	l_acd_destroy(l_steal_ptr(netconfig->acd));
+
+	if (netconfig->orig_disable_ipv6) {
+		netconfig_proc_write_ipv6_uint_setting(netconfig,
+						"disable_ipv6",
+						netconfig->orig_disable_ipv6);
+		netconfig->orig_disable_ipv6 = 0;
+	}
+
+	optimistic_dad = netconfig->optimistic_dad_enabled &&
+		!netconfig->v6_static_addr;
+	if (netconfig->orig_optimistic_dad >= 0 &&
+			!!netconfig->orig_optimistic_dad != optimistic_dad)
+		netconfig_proc_write_ipv6_uint_setting(netconfig,
+						"optimistic_dad",
+						netconfig->orig_optimistic_dad);
+}
+
+/*
+ * Undo any configuration already applied to the interface by previous
+ * calls to the event handler, by synchronously emitting
+ * L_NETCONFIG_EVENT_UNCONFIGURE events.  This can be called before
+ * l_netconfig_stop() which won't emit any events.  It mainly makes
+ * sense if the interface isn't being removed or brought DOWN, which
+ * would otherwise implicitly remove routes and addresses.
+ */
+LIB_EXPORT void l_netconfig_unconfigure(struct l_netconfig *netconfig)
+{
+	const struct l_queue_entry *entry;
+
+	if (netconfig->v4_configured) {
+		netconfig_remove_v4_address_routes(netconfig, false);
+		netconfig->v4_configured = false;
+
+		netconfig_emit_event(netconfig, AF_INET,
+					L_NETCONFIG_EVENT_UNCONFIGURE);
+	}
+
+	if (netconfig->v6_address) {
+		netconfig_remove_dhcp6_address(netconfig, false);
+		netconfig->v6_configured = false;
+	}
+
+	/* Bulk remove any other routes or addresses */
+	for (entry = l_queue_get_entries(netconfig->addresses.current); entry;
+			entry = entry->next)
+		l_queue_push_tail(netconfig->addresses.removed, entry->data);
+
+	l_queue_clear(netconfig->addresses.added, NULL);
+	l_queue_clear(netconfig->addresses.updated, NULL);
+	l_queue_clear(netconfig->addresses.current, NULL);
+
+	for (entry = l_queue_get_entries(netconfig->routes.current); entry;
+			entry = entry->next)
+		l_queue_push_tail(netconfig->routes.removed, entry->data);
+
+	l_queue_clear(netconfig->routes.added, NULL);
+	l_queue_clear(netconfig->routes.updated, NULL);
+	l_queue_clear(netconfig->routes.current, NULL);
+	l_queue_clear(netconfig->icmp_route_data, l_free);
+
+	if (!l_queue_isempty(netconfig->addresses.removed) ||
+			!l_queue_isempty(netconfig->routes.removed))
+		netconfig_emit_event(netconfig, AF_INET6,
+					L_NETCONFIG_EVENT_UNCONFIGURE);
+}
+
+LIB_EXPORT struct l_dhcp_client *l_netconfig_get_dhcp_client(
+						struct l_netconfig *netconfig)
+{
+	if (unlikely(!netconfig))
+		return NULL;
+
+	return netconfig->dhcp_client;
+}
+
+LIB_EXPORT struct l_dhcp6_client *l_netconfig_get_dhcp6_client(
+						struct l_netconfig *netconfig)
+{
+	if (unlikely(!netconfig))
+		return NULL;
+
+	return netconfig->dhcp6_client;
+}
+
+LIB_EXPORT struct l_icmp6_client *l_netconfig_get_icmp6_client(
+						struct l_netconfig *netconfig)
+{
+	if (unlikely(!netconfig))
+		return NULL;
+
+	return netconfig->icmp6_client;
+}
+
+LIB_EXPORT void l_netconfig_set_event_handler(struct l_netconfig *netconfig,
+					l_netconfig_event_cb_t handler,
+					void *user_data,
+					l_netconfig_destroy_cb_t destroy)
+{
+	if (unlikely(!netconfig))
+		return;
+
+	if (netconfig->handler.destroy)
+		netconfig->handler.destroy(netconfig->handler.user_data);
+
+	netconfig->handler.callback = handler;
+	netconfig->handler.user_data = user_data;
+	netconfig->handler.destroy = destroy;
+}
+
+LIB_EXPORT void l_netconfig_apply_rtnl(struct l_netconfig *netconfig)
+{
+	const struct l_queue_entry *entry;
+
+	for (entry = l_queue_get_entries(netconfig->addresses.removed); entry;
+			entry = entry->next)
+		l_rtnl_ifaddr_delete(l_rtnl_get(), netconfig->ifindex,
+					entry->data, NULL, NULL, NULL);
+
+	for (entry = l_queue_get_entries(netconfig->addresses.added); entry;
+			entry = entry->next)
+		l_rtnl_ifaddr_add(l_rtnl_get(), netconfig->ifindex,
+					entry->data, NULL, NULL, NULL);
+
+	/* We can use l_rtnl_ifaddr_add here since that uses NLM_F_REPLACE */
+	for (entry = l_queue_get_entries(netconfig->addresses.updated); entry;
+			entry = entry->next)
+		l_rtnl_ifaddr_add(l_rtnl_get(), netconfig->ifindex,
+					entry->data, NULL, NULL, NULL);
+
+	for (entry = l_queue_get_entries(netconfig->routes.removed); entry;
+			entry = entry->next)
+		l_rtnl_route_delete(l_rtnl_get(), netconfig->ifindex,
+					entry->data, NULL, NULL, NULL);
+
+	for (entry = l_queue_get_entries(netconfig->routes.added); entry;
+			entry = entry->next)
+		l_rtnl_route_add(l_rtnl_get(), netconfig->ifindex,
+					entry->data, NULL, NULL, NULL);
+
+	/* We can use l_rtnl_route_add here since that uses NLM_F_REPLACE */
+	for (entry = l_queue_get_entries(netconfig->routes.updated); entry;
+			entry = entry->next)
+		l_rtnl_route_add(l_rtnl_get(), netconfig->ifindex,
+					entry->data, NULL, NULL, NULL);
+}
+
+LIB_EXPORT const struct l_queue_entry *l_netconfig_get_addresses(
+					struct l_netconfig *netconfig,
+					const struct l_queue_entry **out_added,
+					const struct l_queue_entry **out_updated,
+					const struct l_queue_entry **out_removed,
+					const struct l_queue_entry **out_expired)
+{
+	if (out_added)
+		*out_added = l_queue_get_entries(netconfig->addresses.added);
+
+	if (out_updated)
+		*out_updated = l_queue_get_entries(netconfig->addresses.updated);
+
+	if (out_removed)
+		*out_removed = l_queue_get_entries(netconfig->addresses.removed);
+
+	if (out_expired)
+		*out_expired = l_queue_get_entries(netconfig->addresses.expired);
+
+	return l_queue_get_entries(netconfig->addresses.current);
+}
+
+LIB_EXPORT const struct l_queue_entry *l_netconfig_get_routes(
+					struct l_netconfig *netconfig,
+					const struct l_queue_entry **out_added,
+					const struct l_queue_entry **out_updated,
+					const struct l_queue_entry **out_removed,
+					const struct l_queue_entry **out_expired)
+{
+	netconfig_expire_routes(netconfig);
+
+	if (out_added)
+		*out_added = l_queue_get_entries(netconfig->routes.added);
+
+	if (out_updated)
+		*out_updated = l_queue_get_entries(netconfig->routes.updated);
+
+	if (out_removed)
+		*out_removed = l_queue_get_entries(netconfig->routes.removed);
+
+	if (out_expired)
+		*out_expired = l_queue_get_entries(netconfig->routes.expired);
+
+	return l_queue_get_entries(netconfig->routes.current);
+}
+
+static void netconfig_strv_cat(char ***dest, char **src, bool free)
+{
+	unsigned int dest_len;
+	unsigned int src_len;
+
+	if (!src)
+		return;
+
+	if (!free)
+		src = l_strv_copy(src);
+
+	if (!*dest) {
+		*dest = src;
+		return;
+	}
+
+	dest_len = l_strv_length(*dest);
+	src_len = l_strv_length(src);
+	*dest = l_realloc(*dest, sizeof(char *) * (dest_len + src_len + 1));
+	memcpy(*dest + dest_len, src, sizeof(char *) * (src_len + 1));
+	l_free(src);
+}
+
+/* Returns a new strv array to be freed by the caller */
+LIB_EXPORT char **l_netconfig_get_dns_list(struct l_netconfig *netconfig)
+{
+	char **ret = NULL;
+	const struct l_dhcp_lease *v4_lease;
+	const struct l_dhcp6_lease *v6_lease;
+
+	if (!netconfig->v4_configured)
+		goto append_v6;
+
+	if (netconfig->v4_dns_override)
+		netconfig_strv_cat(&ret, netconfig->v4_dns_override, false);
+	else if ((v4_lease =
+			l_dhcp_client_get_lease(netconfig->dhcp_client)))
+		netconfig_strv_cat(&ret, l_dhcp_lease_get_dns(v4_lease), true);
+
+append_v6:
+	if (!netconfig->v6_configured)
+		goto done;
+
+	if (netconfig->v6_dns_override) {
+		netconfig_strv_cat(&ret, netconfig->v6_dns_override, false);
+		goto done;
+	}
+
+	if (L_IN_SET(netconfig->v6_auto_method, NETCONFIG_V6_METHOD_DHCP,
+				NETCONFIG_V6_METHOD_SLAAC_DHCP) &&
+			(v6_lease = l_dhcp6_client_get_lease(
+						netconfig->dhcp6_client)))
+		netconfig_strv_cat(&ret, l_dhcp6_lease_get_dns(v6_lease), true);
+
+	if (!l_queue_isempty(netconfig->slaac_dnses)) {
+		unsigned int dest_len = l_strv_length(ret);
+		unsigned int src_len = l_queue_length(netconfig->slaac_dnses);
+		char **i;
+		const struct l_queue_entry *entry;
+
+		ret = l_realloc(ret, sizeof(char *) * (dest_len + src_len + 1));
+		i = ret + dest_len;
+
+		for (entry = l_queue_get_entries(netconfig->slaac_dnses);
+				entry; entry = entry->next) {
+			char addr_str[INET6_ADDRSTRLEN];
+
+			if (inet_ntop(AF_INET6, entry->data, addr_str,
+					sizeof(addr_str)))
+				*i++ = l_strdup(addr_str);
+		}
+
+		*i = NULL;
+	}
+
+done:
+	return ret;
+}
+
+/* Returns a new strv array to be freed by the caller */
+LIB_EXPORT char **l_netconfig_get_domain_names(struct l_netconfig *netconfig)
+{
+	char **ret = NULL;
+	const struct l_dhcp_lease *v4_lease;
+	const struct l_dhcp6_lease *v6_lease;
+	char *dn;
+
+	if (!netconfig->v4_configured)
+		goto append_v6;
+
+	if (netconfig->v4_domain_names_override)
+		netconfig_strv_cat(&ret, netconfig->v4_domain_names_override,
+					false);
+	else if ((v4_lease =
+			l_dhcp_client_get_lease(netconfig->dhcp_client)) &&
+			(dn = l_dhcp_lease_get_domain_name(v4_lease))) {
+		ret = l_new(char *, 2);
+		ret[0] = dn;
+	}
+
+append_v6:
+	if (!netconfig->v6_configured)
+		goto done;
+
+	if (netconfig->v6_domain_names_override) {
+		netconfig_strv_cat(&ret, netconfig->v6_domain_names_override,
+					false);
+		goto done;
+	}
+
+	if (L_IN_SET(netconfig->v6_auto_method, NETCONFIG_V6_METHOD_DHCP,
+				NETCONFIG_V6_METHOD_SLAAC_DHCP) &&
+			(v6_lease = l_dhcp6_client_get_lease(
+						netconfig->dhcp6_client)))
+		netconfig_strv_cat(&ret, l_dhcp6_lease_get_domains(v6_lease),
+					true);
+
+	if (!l_queue_isempty(netconfig->slaac_domains)) {
+		unsigned int dest_len = l_strv_length(ret);
+		unsigned int src_len = l_queue_length(netconfig->slaac_domains);
+		char **i;
+		const struct l_queue_entry *entry;
+
+		ret = l_realloc(ret, sizeof(char *) * (dest_len + src_len + 1));
+		i = ret + dest_len;
+
+		for (entry = l_queue_get_entries(netconfig->slaac_domains);
+				entry; entry = entry->next)
+			*i++ = l_strdup(entry->data);
+
+		*i = NULL;
+	}
+
+done:
+	return ret;
+}
diff -pruN 1.30-1/ell/netconfig.h 2.3-1/ell/netconfig.h
--- 1.30-1/ell/netconfig.h	1970-01-01 00:00:00.000000000 +0000
+++ 2.3-1/ell/netconfig.h	2022-11-18 09:08:38.000000000 +0000
@@ -0,0 +1,113 @@
+/*
+ *
+ *  Embedded Linux library
+ *
+ *  Copyright (C) 2022  Intel Corporation. All rights reserved.
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.1 of the License, or (at your option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifndef __ELL_NETCONFIG_H
+#define __ELL_NETCONFIG_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdbool.h>
+
+struct l_dhcp_client;
+struct l_dhcp6_client;
+struct l_icmp6_client;
+struct l_netlink;
+struct l_queue_entry;
+struct l_netconfig;
+struct l_rtnl_address;
+struct l_rtnl_route;
+
+enum l_netconfig_event {
+	L_NETCONFIG_EVENT_CONFIGURE,
+	L_NETCONFIG_EVENT_UPDATE,
+	L_NETCONFIG_EVENT_UNCONFIGURE,
+	L_NETCONFIG_EVENT_FAILED,
+};
+
+typedef void (*l_netconfig_event_cb_t)(struct l_netconfig *netconfig,
+					uint8_t family,
+					enum l_netconfig_event event,
+					void *user_data);
+typedef void (*l_netconfig_destroy_cb_t)(void *user_data);
+
+struct l_netconfig *l_netconfig_new(uint32_t ifindex);
+void l_netconfig_destroy(struct l_netconfig *netconfig);
+bool l_netconfig_set_family_enabled(struct l_netconfig *netconfig,
+					uint8_t family, bool enabled);
+bool l_netconfig_set_hostname(struct l_netconfig *netconfig,
+				const char *hostname);
+bool l_netconfig_set_route_priority(struct l_netconfig *netconfig,
+					uint32_t priority);
+bool l_netconfig_set_static_addr(struct l_netconfig *netconfig, uint8_t family,
+					const struct l_rtnl_address *addr);
+bool l_netconfig_set_gateway_override(struct l_netconfig *netconfig,
+					uint8_t family,
+					const char *gateway_str);
+bool l_netconfig_set_dns_override(struct l_netconfig *netconfig, uint8_t family,
+					char **dns_list);
+bool l_netconfig_set_domain_names_override(struct l_netconfig *netconfig,
+						uint8_t family, char **names);
+bool l_netconfig_set_acd_enabled(struct l_netconfig *netconfig, bool enabled);
+bool l_netconfig_set_optimistic_dad_enabled(struct l_netconfig *netconfig,
+						bool enabled);
+bool l_netconfig_check_config(struct l_netconfig *netconfig);
+bool l_netconfig_reset_config(struct l_netconfig *netconfig);
+
+bool l_netconfig_start(struct l_netconfig *netconfig);
+void l_netconfig_stop(struct l_netconfig *netconfig);
+void l_netconfig_unconfigure(struct l_netconfig *netconfig);
+
+struct l_dhcp_client *l_netconfig_get_dhcp_client(
+						struct l_netconfig *netconfig);
+struct l_dhcp6_client *l_netconfig_get_dhcp6_client(
+						struct l_netconfig *netconfig);
+struct l_icmp6_client *l_netconfig_get_icmp6_client(
+						struct l_netconfig *netconfig);
+
+void l_netconfig_set_event_handler(struct l_netconfig *netconfig,
+					l_netconfig_event_cb_t handler,
+					void *user_data,
+					l_netconfig_destroy_cb_t destroy);
+
+void l_netconfig_apply_rtnl(struct l_netconfig *netconfig);
+const struct l_queue_entry *l_netconfig_get_addresses(
+				struct l_netconfig *netconfig,
+				const struct l_queue_entry **out_added,
+				const struct l_queue_entry **out_updated,
+				const struct l_queue_entry **out_removed,
+				const struct l_queue_entry **out_expired);
+const struct l_queue_entry *l_netconfig_get_routes(
+				struct l_netconfig *netconfig,
+				const struct l_queue_entry **out_added,
+				const struct l_queue_entry **out_updated,
+				const struct l_queue_entry **out_removed,
+				const struct l_queue_entry **out_expired);
+char **l_netconfig_get_dns_list(struct l_netconfig *netconfig);
+char **l_netconfig_get_domain_names(struct l_netconfig *netconfig);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __ELL_NETCONFIG_H */
diff -pruN 1.30-1/ell/net-private.h 2.3-1/ell/net-private.h
--- 1.30-1/ell/net-private.h	2022-06-04 19:58:23.000000000 +0000
+++ 2.3-1/ell/net-private.h	2022-11-18 09:08:38.000000000 +0000
@@ -21,7 +21,7 @@
  */
 
 char *net_domain_name_parse(const uint8_t *raw, size_t raw_len);
-char **net_domain_list_parse(const uint8_t *raw, size_t raw_len);
+char **net_domain_list_parse(const uint8_t *raw, size_t raw_len, bool padded);
 
 static inline const void *net_prefix_from_ipv6(const uint8_t *address,
 						uint8_t prefix_len)
diff -pruN 1.30-1/ell/rtnl.c 2.3-1/ell/rtnl.c
--- 1.30-1/ell/rtnl.c	2022-07-15 16:22:44.000000000 +0000
+++ 2.3-1/ell/rtnl.c	2022-11-18 09:08:38.000000000 +0000
@@ -180,6 +180,15 @@ LIB_EXPORT bool l_rtnl_address_get_addre
 						out_buf);
 }
 
+LIB_EXPORT const void *l_rtnl_address_get_in_addr(
+					const struct l_rtnl_address *addr)
+{
+	if (unlikely(!addr))
+		return NULL;
+
+	return addr->family == AF_INET ? (void *) &addr->in_addr : &addr->in6_addr;
+}
+
 LIB_EXPORT uint8_t l_rtnl_address_get_family(const struct l_rtnl_address *addr)
 {
 	if (unlikely(!addr))
diff -pruN 1.30-1/ell/rtnl.h 2.3-1/ell/rtnl.h
--- 1.30-1/ell/rtnl.h	2022-07-15 16:22:44.000000000 +0000
+++ 2.3-1/ell/rtnl.h	2022-11-18 09:08:38.000000000 +0000
@@ -44,6 +44,7 @@ void l_rtnl_address_free(struct l_rtnl_a
 DEFINE_CLEANUP_FUNC(l_rtnl_address_free);
 bool l_rtnl_address_get_address(const struct l_rtnl_address *addr,
 				char *out_buf);
+const void *l_rtnl_address_get_in_addr(const struct l_rtnl_address *addr);
 uint8_t l_rtnl_address_get_family(const struct l_rtnl_address *addr);
 uint8_t l_rtnl_address_get_prefix_length(const struct l_rtnl_address *addr);
 bool l_rtnl_address_get_broadcast(const struct l_rtnl_address *addr,
diff -pruN 1.30-1/ell/time.c 2.3-1/ell/time.c
--- 1.30-1/ell/time.c	2022-07-15 16:22:44.000000000 +0000
+++ 2.3-1/ell/time.c	2022-11-18 09:08:38.000000000 +0000
@@ -58,6 +58,14 @@ LIB_EXPORT uint64_t l_time_now(void)
 	return _time_from_timespec(&now);
 }
 
+uint64_t time_realtime_now(void)
+{
+	struct timespec now;
+
+	clock_gettime(CLOCK_REALTIME, &now);
+	return _time_from_timespec(&now);
+}
+
 /**
  * l_time_after
  *
diff -pruN 1.30-1/ell/time-private.h 2.3-1/ell/time-private.h
--- 1.30-1/ell/time-private.h	2022-07-15 16:22:44.000000000 +0000
+++ 2.3-1/ell/time-private.h	2022-11-18 09:08:38.000000000 +0000
@@ -24,3 +24,4 @@ uint64_t _time_pick_interval_secs(uint32
 uint64_t _time_fuzz_msecs(uint64_t ms);
 uint64_t _time_fuzz_secs(uint32_t secs, uint32_t max_offset);
 uint64_t _time_realtime_to_boottime(const struct timeval *ts);
+uint64_t time_realtime_now(void);
diff -pruN 1.30-1/ell/tls.c 2.3-1/ell/tls.c
--- 1.30-1/ell/tls.c	2022-09-07 18:21:45.000000000 +0000
+++ 2.3-1/ell/tls.c	2023-01-23 18:26:15.000000000 +0000
@@ -46,6 +46,9 @@
 #include "strv.h"
 #include "missing.h"
 #include "string.h"
+#include "settings.h"
+#include "time.h"
+#include "time-private.h"
 
 bool tls10_prf(const void *secret, size_t secret_len,
 		const char *label,
@@ -195,6 +198,7 @@ static void tls_reset_handshake(struct l
 	tls->peer_cert = NULL;
 	tls->peer_pubkey = NULL;
 	tls->peer_pubkey_size = 0;
+	tls->peer_authenticated = false;
 	tls->negotiated_curve = NULL;
 	tls->negotiated_ff_group = NULL;
 
@@ -204,6 +208,12 @@ static void tls_reset_handshake(struct l
 	TLS_SET_STATE(TLS_HANDSHAKE_WAIT_START);
 	tls->cert_requested = 0;
 	tls->cert_sent = 0;
+
+	tls->session_id_size = 0;
+	tls->session_id_size_replaced = 0;
+	tls->session_id_new = false;
+	l_free(l_steal_ptr(tls->session_peer_identity));
+	tls->session_resumed = false;
 }
 
 static void tls_cleanup_handshake(struct l_tls *tls)
@@ -385,7 +395,7 @@ static void tls_reset_cipher_spec(struct
 	tls_change_cipher_spec(tls, txrx, NULL);
 }
 
-bool tls_cipher_suite_is_compatible(struct l_tls *tls,
+static bool tls_cipher_suite_is_compatible_no_key_xchg(struct l_tls *tls,
 					const struct tls_cipher_suite *suite,
 					const char **error)
 {
@@ -470,19 +480,6 @@ bool tls_cipher_suite_is_compatible(stru
 		return false;
 	}
 
-	if (suite->key_xchg->need_ffdh &&
-			!l_key_is_supported(L_KEY_FEATURE_DH)) {
-		if (error) {
-			*error = error_buf;
-			snprintf(error_buf, sizeof(error_buf),
-					"Cipher suite %s's key exchange "
-					"mechanism needs kernel DH support",
-					suite->name);
-		}
-
-		return false;
-	}
-
 	/*
 	 * If the certificate is compatible with the signature algorithm it
 	 * also must be compatible with the key exchange mechanism because
@@ -503,6 +500,38 @@ bool tls_cipher_suite_is_compatible(stru
 		return false;
 	}
 
+	return true;
+}
+
+/*
+ * Assumes that Client Hello extensions have been processed and that we
+ * will want to start a new session using this cipher suite, including the
+ * key exchange.  This is unlike tls_cipher_suite_is_compatible_no_key_xchg()
+ * which runs fewer checks and must succeed even for a cipher suite loaded
+ * during session resumption.
+ */
+bool tls_cipher_suite_is_compatible(struct l_tls *tls,
+					const struct tls_cipher_suite *suite,
+					const char **error)
+{
+	static char error_buf[200];
+
+	if (!tls_cipher_suite_is_compatible_no_key_xchg(tls, suite, error))
+		return false;
+
+	if (suite->key_xchg->need_ffdh &&
+			!l_key_is_supported(L_KEY_FEATURE_DH)) {
+		if (error) {
+			*error = error_buf;
+			snprintf(error_buf, sizeof(error_buf),
+					"Cipher suite %s's key exchange "
+					"mechanism needs kernel DH support",
+					suite->name);
+		}
+
+		return false;
+	}
+
 	/*
 	 * On the server we know what elliptic curve we'll be using as soon
 	 * as we've processed the ClientHello so for EC-based key exchange
@@ -820,6 +849,312 @@ parse_error:
 	return false;
 }
 
+static const char *tls_get_cache_group_name(struct l_tls *tls,
+						const uint8_t *session_id,
+						size_t session_id_size)
+{
+	_auto_(l_free) char *session_id_str = NULL;
+	static char group_name[256];
+
+	if (!tls->server)
+		return tls->session_prefix;
+
+	session_id_str = l_util_hexstring(session_id, session_id_size);
+	snprintf(group_name, sizeof(group_name), "%s-%s",
+			tls->session_prefix, session_id_str);
+	return group_name;
+}
+
+static void tls_forget_cached_session(struct l_tls *tls, const char *group_name,
+					const uint8_t *session_id,
+					size_t session_id_size, bool call_back)
+{
+	if (!group_name)
+		group_name = tls_get_cache_group_name(tls, session_id,
+							session_id_size);
+
+	l_settings_remove_group(tls->session_settings, group_name);
+
+	if (call_back && tls->session_update_cb) {
+		tls->in_callback = true;
+		tls->session_update_cb(tls->session_update_user_data);
+		tls->in_callback = false;
+	}
+}
+
+static bool tls_load_cached_session(struct l_tls *tls, const char *group_name,
+					const uint8_t *session_id,
+					size_t session_id_size,
+					const char *session_id_str)
+{
+	_auto_(l_free) uint8_t *master_secret = NULL;
+	int version;
+	_auto_(l_free) uint8_t *cipher_suite_id = NULL;
+	struct tls_cipher_suite *cipher_suite;
+	unsigned int compression_method_id;
+	_auto_(l_free) char *peer_identity = NULL;
+	size_t size;
+	const char *error;
+
+	if (l_settings_has_key(tls->session_settings, group_name,
+				"SessionExpiryTime")) {
+		uint64_t expiry_time;
+
+		if (unlikely(!l_settings_get_uint64(tls->session_settings,
+							group_name,
+							"SessionExpiryTime",
+							&expiry_time)))
+			goto warn_corrupt;
+
+		if (time_realtime_now() > expiry_time) {
+			TLS_DEBUG("Cached session %s is expired, removing it, "
+					"will start a new session",
+					session_id_str);
+			goto forget;
+		}
+	}
+
+	if (unlikely(!l_settings_get_int(tls->session_settings,
+						group_name,
+						"SessionVersion",
+						&version) ||
+			version < TLS_MIN_VERSION || version > TLS_MAX_VERSION))
+		goto warn_corrupt;
+
+	master_secret = l_settings_get_bytes(tls->session_settings,
+						group_name,
+						"SessionMasterSecret",
+						&size);
+	if (unlikely(!master_secret || size != 48))
+		goto warn_corrupt;
+
+	cipher_suite_id = l_settings_get_bytes(tls->session_settings,
+						group_name,
+						"SessionCipherSuite",
+						&size);
+	if (unlikely(!cipher_suite_id || size != 2 ||
+			!(cipher_suite =
+			  tls_find_cipher_suite(cipher_suite_id))))
+		goto warn_corrupt;
+
+	/*
+	 * While we could attempt to resume a session even though we're now
+	 * configured with, say, a different certificate type than what we
+	 * had when we cached that session, that is too questionable of a
+	 * scenario to support it.  We don't specifically check that all of
+	 * the authentication data is the same, e.g. we don't save the
+	 * certificate serial number or path, but ensure the cached cipher
+	 * suite is compatible with current authentication data.
+	 *
+	 * We filter the cipher suites in our Client Hello to only offer the
+	 * ones compatible with current configuration so if we also include
+	 * a Session ID from a session who's cipher suite is not one of those
+	 * listed in that same Client Hello, the server is likely to notice
+	 * and either start a new session or send a fatal Alert.
+	 *
+	 * It is up to the user to keep multiple cache instances if it needs
+	 * to save multiple sessions.
+	 */
+	if (unlikely(!tls_cipher_suite_is_compatible_no_key_xchg(tls,
+								cipher_suite,
+								&error))) {
+		TLS_DEBUG("Cached session %s cipher suite not compatible: %s",
+				session_id_str, error);
+		goto forget;
+	}
+
+	if (unlikely(!l_settings_get_uint(tls->session_settings, group_name,
+						"SessionCompressionMethod",
+						&compression_method_id) ||
+			!tls_find_compression_method(compression_method_id)))
+		goto warn_corrupt;
+
+	if (l_settings_has_key(tls->session_settings, group_name,
+				"SessionPeerIdentity")) {
+		peer_identity = l_settings_get_string(tls->session_settings,
+						group_name,
+						"SessionPeerIdentity");
+		if (unlikely(!peer_identity || !cipher_suite->signature))
+			goto warn_corrupt;
+	}
+
+	tls->session_id_size = session_id_size;
+	memcpy(tls->session_id, session_id, session_id_size);
+	tls->session_id_new = false;
+	tls->client_version = version;
+	memcpy(tls->pending.master_secret, master_secret, 48);
+	memcpy(tls->session_cipher_suite_id, cipher_suite_id, 2);
+	tls->session_compression_method_id = compression_method_id;
+	l_free(tls->session_peer_identity);
+	tls->session_peer_identity = l_steal_ptr(peer_identity);
+	return true;
+
+warn_corrupt:
+	TLS_DEBUG("Cached session %s data is corrupt or has unsupported "
+			"parameters, removing it, will start a new session",
+			session_id_str);
+
+forget:
+	tls_forget_cached_session(tls, group_name, session_id, session_id_size,
+					true);
+	return false;
+}
+
+static bool tls_load_cached_client_session(struct l_tls *tls)
+{
+	/*
+	 * The following settings are required:
+	 *   SessionID,
+	 *   SessionMasterSecret,
+	 *   SessionVersion,
+	 *   SessionCipherSuite,
+	 *   SessionCompressionMethod,
+	 * and these two are optional:
+	 *   SessionExpiryTime,
+	 *   SessionPeerIdentity.
+	 */
+	_auto_(l_free) uint8_t *session_id = NULL;
+	size_t session_id_size;
+	_auto_(l_free) char *session_id_str = NULL;
+	const char *group_name = tls_get_cache_group_name(tls, NULL, 0);
+
+	tls->session_id_size = 0;
+	tls->session_id_new = false;
+
+	if (!tls->session_settings ||
+			!l_settings_has_key(tls->session_settings, group_name,
+						"SessionID"))
+		/* No session cached, no error */
+		return false;
+
+	session_id = l_settings_get_bytes(tls->session_settings, group_name,
+						"SessionID", &session_id_size);
+	if (unlikely(!session_id ||
+			session_id_size < 1 || session_id_size > 32)) {
+		TLS_DEBUG("Bad cached session ID format");
+		tls_forget_cached_session(tls, group_name, NULL, 0, true);
+		return false;
+	}
+
+	session_id_str = l_util_hexstring(session_id, session_id_size);
+
+	return tls_load_cached_session(tls, group_name, session_id,
+					session_id_size, session_id_str);
+}
+
+static bool tls_load_cached_server_session(struct l_tls *tls,
+						const uint8_t *session_id,
+						size_t session_id_size)
+{
+	_auto_(l_free) char *session_id_str =
+		l_util_hexstring(session_id, session_id_size);
+	const char *target_group_name =
+		tls_get_cache_group_name(tls, session_id, session_id_size);
+	_auto_(l_strv_free) char **groups =
+		l_settings_get_groups(tls->session_settings);
+	char **group;
+	unsigned int cnt = 0;
+	size_t prefix_len = strlen(tls->session_prefix);
+	uint64_t now = time_realtime_now();
+	char *oldest_session_group = NULL;
+	uint64_t oldest_session_expiry = UINT64_MAX;
+	bool found = false;
+	bool changed = false;
+	bool loaded = false;
+
+	tls->session_id_size = 0;
+	tls->session_id_new = false;
+
+	/* Clean up expired entries and enforce session count limit */
+	for (group = groups; *group; group++) {
+		uint64_t expiry_time;
+
+		if (memcmp(*group, tls->session_prefix, prefix_len) ||
+				(*group)[prefix_len] != '-')
+			continue;
+
+		/* Group seems to be a session cache entry */
+
+		if (unlikely(!l_settings_get_uint64(tls->session_settings,
+							*group,
+							"SessionExpiryTime",
+							&expiry_time)) ||
+				expiry_time <= now) {
+			TLS_DEBUG("Cached session %s is expired or invalid, "
+					"purging entry",
+					*group + prefix_len + 1);
+			l_settings_remove_group(tls->session_settings, *group);
+			changed = true;
+			continue;
+		}
+
+		cnt++;
+
+		if (!strcmp(*group + prefix_len + 1,
+				target_group_name + prefix_len + 1)) {
+			found = true;
+			continue; /* Don't purge this entry */
+		}
+
+		if (expiry_time < oldest_session_expiry) {
+			oldest_session_group = *group;
+			oldest_session_expiry = expiry_time;
+		}
+	}
+
+	/*
+	 * Enforce tls->session_count_max by dropping the entry for the
+	 * oldest session (more specifically the one closest to its expiry
+	 * time) in the cache, that is not the session we're trying to
+	 * load.  If the target session was not found, do this as soon as
+	 * tls->session_count_max is reached rather than when it's exceeded
+	 * so as to make room for a new entry.  If we end up not saving
+	 * a new session due to an error before the handshake finish, we'll
+	 * be left with tls->session_count_max - 1 entries and will have
+	 * purged that entry unnecessarily but the cache will have room for
+	 * a future session.  Same when we did find the target session but
+	 * later had to forget it because of a fatal alert during or after
+	 * the handshake.
+	 *
+	 * If we did find the target session but we later find that we
+	 * can't resume it, we will forget it so the new session can take
+	 * the old session's place and the limit is not exceeded, see
+	 * discussion in tls_server_resume_error.
+	 */
+	if (tls->session_count_max && oldest_session_group &&
+			cnt >= tls->session_count_max + (found ? 1 : 0)) {
+		l_settings_remove_group(tls->session_settings,
+					oldest_session_group);
+		changed = true;
+	}
+
+	if (!found) {
+		TLS_DEBUG("Requested session %s not found in cache, will "
+				"start a new session", session_id_str);
+		goto call_back;
+	}
+
+	loaded = tls_load_cached_session(tls, target_group_name,
+						session_id, session_id_size,
+						session_id_str);
+
+	/*
+	 * If tls_load_cached_session() returned false it will have called
+	 * session_update_cb for us.
+	 */
+	if (!loaded)
+		changed = false;
+
+call_back:
+	if (changed && tls->session_update_cb) {
+		tls->in_callback = true;
+		tls->session_update_cb(tls->session_update_user_data);
+		tls->in_callback = false;
+	}
+
+	return loaded;
+}
+
 #define SWITCH_ENUM_TO_STR(val) \
 	case (val):		\
 		return L_STRINGIFY(val);
@@ -868,6 +1203,29 @@ static void tls_send_alert(struct l_tls
 void tls_disconnect(struct l_tls *tls, enum l_tls_alert_desc desc,
 			enum l_tls_alert_desc local_desc)
 {
+	bool forget_session = false;
+	/* Save session_id_size before tls_reset_handshake() */
+	size_t session_id_size = tls->session_id_size;
+
+	if ((desc || local_desc) && tls->session_settings &&
+			session_id_size && !tls->session_id_new)
+		/*
+		 * RFC5246 Section 7.2: "Alert messages with a level of fatal
+		 * result in the immediate termination of the connection.  In
+		 * this case, other connections corresponding to the session
+		 * may continue, but the session identifier MUST be
+		 * invalidated, preventing the failed session from being used
+		 * to establish new connections."
+		 *
+		 * and 7.2.1: "Note that as of TLS 1.1, failure to properly
+		 * close a connection no longer requires that a session not
+		 * be resumed."
+		 *
+		 * I.e. we need to remove the session from the cache here but
+		 * not on l_tls_close().
+		 */
+		forget_session = true;
+
 	tls_send_alert(tls, true, desc);
 
 	tls_reset_handshake(tls);
@@ -878,6 +1236,15 @@ void tls_disconnect(struct l_tls *tls, e
 
 	tls->negotiated_version = 0;
 	tls->ready = false;
+	tls->renegotiation_info.secure_renegotiation = false;
+
+	if (forget_session) {
+		tls_forget_cached_session(tls, NULL, tls->session_id,
+						session_id_size, true);
+
+		if (tls->pending_destroy)
+			return;
+	}
 
 	tls->disconnected(local_desc ?: desc, local_desc && !desc,
 				tls->user_data);
@@ -1003,14 +1370,19 @@ static bool tls_send_client_hello(struct
 
 	/* Fill in the Client Hello body */
 
-	*ptr++ = (uint8_t) (tls->max_version >> 8);
-	*ptr++ = (uint8_t) (tls->max_version >> 0);
+	*ptr++ = (uint8_t) (tls->client_version >> 8);
+	*ptr++ = (uint8_t) (tls->client_version >> 0);
 
 	tls_write_random(tls->pending.client_random);
 	memcpy(ptr, tls->pending.client_random, 32);
 	ptr += 32;
 
-	*ptr++ = 0; /* No SessionID */
+	if (tls->session_id_size) {
+		*ptr++ = tls->session_id_size;
+		memcpy(ptr, tls->session_id, tls->session_id_size);
+		ptr += tls->session_id_size;
+	} else
+		*ptr++ = 0;
 
 	len_ptr = ptr;
 	ptr += 2;
@@ -1065,7 +1437,12 @@ static bool tls_send_server_hello(struct
 	memcpy(ptr, tls->pending.server_random, 32);
 	ptr += 32;
 
-	*ptr++ = 0; /* Sessions are not cached */
+	if (tls->session_id_size) {
+		*ptr++ = tls->session_id_size;
+		memcpy(ptr, tls->session_id, tls->session_id_size);
+		ptr += tls->session_id_size;
+	} else
+		*ptr++ = 0;
 
 	*ptr++ = tls->pending.cipher_suite->id[0];
 	*ptr++ = tls->pending.cipher_suite->id[1];
@@ -1257,22 +1634,10 @@ static void tls_send_server_hello_done(s
 				TLS_HANDSHAKE_HEADER_SIZE);
 }
 
-void tls_generate_master_secret(struct l_tls *tls,
-				const uint8_t *pre_master_secret,
-				int pre_master_secret_len)
+static void tls_update_key_block(struct l_tls *tls)
 {
 	uint8_t seed[64];
-	int key_block_size;
-
-	memcpy(seed +  0, tls->pending.client_random, 32);
-	memcpy(seed + 32, tls->pending.server_random, 32);
-
-	tls_prf_get_bytes(tls, pre_master_secret, pre_master_secret_len,
-				"master secret", seed, 64,
-				tls->pending.master_secret, 48);
-
-	/* Directly generate the key block while we're at it */
-	key_block_size = 0;
+	int key_block_size = 0;
 
 	if (tls->pending.cipher_suite->encryption)
 		key_block_size += 2 *
@@ -1300,8 +1665,25 @@ void tls_generate_master_secret(struct l
 	tls_prf_get_bytes(tls, tls->pending.master_secret, 48,
 				"key expansion", seed, 64,
 				tls->pending.key_block, key_block_size);
+	explicit_bzero(seed, 64);
+}
+
+void tls_generate_master_secret(struct l_tls *tls,
+				const uint8_t *pre_master_secret,
+				int pre_master_secret_len)
+{
+	uint8_t seed[64];
 
+	memcpy(seed +  0, tls->pending.client_random, 32);
+	memcpy(seed + 32, tls->pending.server_random, 32);
+
+	tls_prf_get_bytes(tls, pre_master_secret, pre_master_secret_len,
+				"master secret", seed, 64,
+				tls->pending.master_secret, 48);
 	explicit_bzero(seed, 64);
+
+	/* Directly generate the key block while we're at it */
+	tls_update_key_block(tls);
 }
 
 static void tls_get_handshake_hash(struct l_tls *tls,
@@ -1370,7 +1752,7 @@ static void tls_send_change_cipher_spec(
 	tls_tx_record(tls, TLS_CT_CHANGE_CIPHER_SPEC, &buf, 1);
 }
 
-static size_t tls_verify_data_length(struct l_tls *tls, unsigned int index)
+size_t tls_verify_data_length(struct l_tls *tls, unsigned int index)
 {
 	/*
 	 * RFC 5246, Section 7.4.9:
@@ -1383,7 +1765,34 @@ static size_t tls_verify_data_length(str
 	return maxsize(tls->cipher_suite[index]->verify_data_length, 12);
 }
 
-static void tls_send_finished(struct l_tls *tls)
+static bool tls_save_verify_data(struct l_tls *tls, bool txrx,
+					const uint8_t *vd, size_t vdl)
+{
+	uint8_t *buf;
+
+	if (tls->server == txrx) {
+		if (vdl > sizeof(tls->renegotiation_info.server_verify_data))
+			goto error;
+
+		buf = tls->renegotiation_info.server_verify_data;
+	} else {
+		if (vdl > sizeof(tls->renegotiation_info.client_verify_data))
+			goto error;
+
+		buf = tls->renegotiation_info.client_verify_data;
+	}
+
+	memcpy(buf, vd, vdl);
+	return true;
+
+error:
+	TLS_DISCONNECT(TLS_ALERT_INTERNAL_ERROR, 0,
+			"tls->renegotiation_info.*verify too small for %s, "
+			"report an ell bug", tls->cipher_suite[txrx]->name);
+	return false;
+}
+
+static bool tls_send_finished(struct l_tls *tls)
 {
 	uint8_t buf[512];
 	uint8_t *ptr = buf + TLS_HANDSHAKE_HEADER_SIZE;
@@ -1406,9 +1815,14 @@ static void tls_send_finished(struct l_t
 				"client finished",
 				seed, seed_len,
 				ptr, vdl);
+
+	if (!tls_save_verify_data(tls, 1, ptr, vdl))
+		return false;
+
 	ptr += vdl;
 
 	tls_tx_handshake(tls, TLS_FINISHED, buf, ptr - buf);
+	return true;
 }
 
 static bool tls_verify_finished(struct l_tls *tls, const uint8_t *received,
@@ -1451,6 +1865,9 @@ static bool tls_verify_finished(struct l
 		return false;
 	}
 
+	if (!tls_save_verify_data(tls, 0, received, vdl))
+		return false;
+
 	return true;
 }
 
@@ -1586,6 +2003,37 @@ decode_error:
 	return false;
 }
 
+static void tls_server_resume_error(struct l_tls *tls)
+{
+	/*
+	 * When Client Hello parameters don't match the parameters of the
+	 * cached session that was requested by the client, we'll probably
+	 * start and cache a new session.  Even though RFC 5246 doesn't
+	 * specifically mandate that the requested session be forgotten
+	 * (there's no fatal Alert in that case), we overwrite the old
+	 * session's entry in the cache with the new session's data to
+	 * avoid keeping many sessions related to one client in the cache.
+	 * In theory this allows an attacker to connect as a client and
+	 * invalidate a legitimate client's session entry in our cache,
+	 * DoSing the session resumption mechanism so that clients have
+	 * to go through the full handshake.  In practice there are many
+	 * ways for an attacker to do that even without this.
+	 *
+	 * Our client mode only caches one last session anyway, other
+	 * implementations may work that way too.
+	 */
+	memcpy(tls->session_id_replaced, tls->session_id, tls->session_id_size);
+	tls->session_id_size_replaced = tls->session_id_size;
+
+	tls->session_id_size = 0;
+	tls->session_id_new = false;
+	l_free(l_steal_ptr(tls->session_peer_identity));
+}
+
+/* RFC 5746 */
+static const uint8_t tls_empty_renegotiation_info_scsv[2] = { 0x00, 0xff };
+static const uint16_t tls_renegotiation_info_id = 0xff01;
+
 static void tls_handle_client_hello(struct l_tls *tls,
 					const uint8_t *buf, size_t len)
 {
@@ -1596,6 +2044,10 @@ static void tls_handle_client_hello(stru
 	int i;
 	struct l_queue *extensions_offered = NULL;
 	enum l_tls_alert_desc alert_desc = TLS_ALERT_HANDSHAKE_FAIL;
+	bool resuming = false;
+	_auto_(l_free) char *session_id_str = NULL;
+	struct tls_cipher_suite *backup_suite = NULL;
+	struct tls_compression_method *backup_cm = NULL;
 
 	/* Do we have enough for ProtocolVersion + Random + SessionID size? */
 	if (len < 2 + 32 + 1)
@@ -1605,6 +2057,9 @@ static void tls_handle_client_hello(stru
 	session_id_size = buf[34];
 	len -= 35;
 
+	if (unlikely(session_id_size > 32))
+		goto decode_error;
+
 	/*
 	 * Do we have enough to hold the actual session ID + 2 byte field for
 	 * cipher_suite len + minimum of a single cipher suite identifier
@@ -1637,6 +2092,21 @@ static void tls_handle_client_hello(stru
 
 	len -= compression_methods_size;
 
+	if (session_id_size && tls->session_settings &&
+			tls_load_cached_server_session(tls, buf + 35,
+							session_id_size)) {
+		/*
+		 * Attempt a session resumption but note later checks may
+		 * spoil this.
+		 */
+		resuming = true;
+		session_id_str = l_util_hexstring(tls->session_id,
+							tls->session_id_size);
+	}
+
+	if (tls->pending_destroy)
+		return;
+
 	extensions_offered = l_queue_new();
 
 	if (!tls_handle_hello_extensions(tls, compression_methods +
@@ -1644,13 +2114,6 @@ static void tls_handle_client_hello(stru
 					len, extensions_offered))
 		goto cleanup;
 
-	/*
-	 * Note: if the client is supplying a SessionID we know it is false
-	 * because our server implementation never generates any SessionIDs
-	 * yet so either the client is attempting something strange or was
-	 * trying to connect somewhere else.  We might want to throw an error.
-	 */
-
 	/* Save client_version for Premaster Secret verification */
 	tls->client_version = l_get_be16(buf);
 
@@ -1679,6 +2142,30 @@ static void tls_handle_client_hello(stru
 		goto cleanup;
 	}
 
+	if (!tls->renegotiation_info.secure_renegotiation || tls->ready) {
+		for (i = 0; i < cipher_suites_size; i += 2)
+			if (l_get_be16(cipher_suites + i) == l_get_be16(
+					tls_empty_renegotiation_info_scsv))
+				break;
+
+		if (i < cipher_suites_size) {
+			if (unlikely(tls->ready)) {
+				TLS_DISCONNECT(TLS_ALERT_ILLEGAL_PARAM, 0,
+						"Empty renegotiation_info in "
+						"renegotiation Client Hello");
+				goto cleanup;
+			}
+
+			/*
+			 * RFC 5746 Section 3.6, act as if we had received an
+			 * empty renegotiation_info extension.
+			 */
+			tls->renegotiation_info.secure_renegotiation = true;
+			l_queue_push_tail(extensions_offered, L_UINT_TO_PTR(
+						tls_renegotiation_info_id));
+		}
+	}
+
 	/* Select a cipher suite according to client's preference list */
 	while (cipher_suites_size) {
 		struct tls_cipher_suite *suite =
@@ -1705,6 +2192,16 @@ static void tls_handle_client_hello(stru
 			alert_desc = TLS_ALERT_INSUFFICIENT_SECURITY;
 			TLS_DEBUG("non-fatal: Cipher suite %s disallowed "
 					"by config", suite->name);
+		} else if (resuming && memcmp(tls->session_cipher_suite_id,
+						suite->id, 2)) {
+			/*
+			 * For now skip this cipher suite because we're trying
+			 * to find the one from the cached session state.  But
+			 * keep it as a backup in case we end up starting a new
+			 * session.
+			 */
+			if (!backup_suite)
+				backup_suite = suite;
 		} else {
 			tls->pending.cipher_suite = suite;
 			break;
@@ -1714,7 +2211,15 @@ static void tls_handle_client_hello(stru
 		cipher_suites_size -= 2;
 	}
 
-	if (!cipher_suites_size) {
+	if (unlikely(!cipher_suites_size && backup_suite)) {
+		TLS_DEBUG("Cached session %s's cipher suite %04x "
+				"unavailable, will start a new session",
+				session_id_str,
+				l_get_be16(tls->session_cipher_suite_id));
+		tls->pending.cipher_suite = backup_suite;
+		resuming = false;
+		tls_server_resume_error(tls);
+	} else if (unlikely(!cipher_suites_size)) {
 		TLS_DISCONNECT(alert_desc, 0,
 				"No common cipher suites matching negotiated "
 				"TLS version and our certificate's key type");
@@ -1727,35 +2232,90 @@ static void tls_handle_client_hello(stru
 		goto cleanup;
 	}
 
-	TLS_DEBUG("Negotiated %s", tls->pending.cipher_suite->name);
-
 	/* Select a compression method */
 
 	/* CompressionMethod.null must be present in the vector */
 	while (compression_methods_size) {
-		tls->pending.compression_method =
+		struct tls_compression_method *cm =
 			tls_find_compression_method(*compression_methods);
 
-		if (tls->pending.compression_method)
+		if (!cm)
+			TLS_DEBUG("non-fatal: Compression %02x unknown",
+					*compression_methods);
+		else if (resuming && *compression_methods !=
+				tls->session_compression_method_id) {
+			/*
+			 * For now skip this compression method because we're
+			 * trying to find the one from the cached session state.
+			 * But keep it as a backup in case we end up starting
+			 * a new * session.
+			 */
+			if (!backup_cm)
+				backup_cm = cm;
+		} else {
+			tls->pending.compression_method = cm;
 			break;
+		}
 
 		compression_methods++;
 		compression_methods_size--;
 	}
 
-	if (!compression_methods_size) {
+	if (unlikely(!compression_methods_size && backup_cm)) {
+		TLS_DEBUG("Cached session %s's compression method %02x "
+				"unavailable, will start a new session",
+				session_id_str,
+				tls->session_compression_method_id);
+		tls->pending.compression_method = backup_cm;
+
+		if (backup_suite)
+			tls->pending.cipher_suite = backup_suite;
+
+		resuming = false;
+		tls_server_resume_error(tls);
+	} else if (unlikely(!compression_methods_size)) {
 		TLS_DISCONNECT(TLS_ALERT_HANDSHAKE_FAIL, 0,
 				"No common compression methods");
 		goto cleanup;
 	}
 
+	if (resuming)
+		TLS_DEBUG("Negotiated resumption of cached session %s",
+				session_id_str);
+
+	TLS_DEBUG("Negotiated %s", tls->pending.cipher_suite->name);
 	TLS_DEBUG("Negotiated %s", tls->pending.compression_method->name);
 
+	if (!resuming && tls->session_settings) {
+		tls->session_id_new = true;
+		tls->session_id_size = 32;
+		l_getrandom(tls->session_id, 32);
+	}
+
 	if (!tls_send_server_hello(tls, extensions_offered))
 		goto cleanup;
 
 	l_queue_destroy(extensions_offered, NULL);
 
+	if (resuming) {
+		const char *error;
+
+		tls_update_key_block(tls);
+		tls_send_change_cipher_spec(tls);
+
+		if (!tls_change_cipher_spec(tls, 1, &error)) {
+			TLS_DISCONNECT(TLS_ALERT_INTERNAL_ERROR, 0,
+					"change_cipher_spec: %s", error);
+			return;
+		}
+
+		if (!tls_send_finished(tls))
+			return;
+
+		TLS_SET_STATE(TLS_HANDSHAKE_WAIT_CHANGE_CIPHER_SPEC);
+		return;
+	}
+
 	if (tls->pending.cipher_suite->signature && tls->cert)
 		if (!tls_send_certificate(tls))
 			return;
@@ -1796,11 +2356,14 @@ static void tls_handle_server_hello(stru
 	int i;
 	struct l_queue *extensions_seen;
 	bool result;
+	uint16_t version;
+	bool resuming = false;
 
 	/* Do we have enough for ProtocolVersion + Random + SessionID len ? */
 	if (len < 2 + 32 + 1)
 		goto decode_error;
 
+	version = l_get_be16(buf);
 	memcpy(tls->pending.server_random, buf + 2, 32);
 	session_id_size = buf[34];
 	len -= 35;
@@ -1814,6 +2377,34 @@ static void tls_handle_server_hello(stru
 	compression_method_id = buf[35 + session_id_size + 2];
 	len -= session_id_size + 2 + 1;
 
+	if (session_id_size > 32)
+		goto decode_error;
+
+	if (tls->session_id_size) {
+		_auto_(l_free) char *session_id_str =
+			l_util_hexstring(tls->session_id, tls->session_id_size);
+
+		if (session_id_size == tls->session_id_size &&
+				!memcmp(buf + 35, tls->session_id,
+					session_id_size)) {
+			TLS_DEBUG("Negotiated resumption of cached session %s",
+					session_id_str);
+			resuming = true;
+		} else {
+			TLS_DEBUG("Server decided not to resume cached session "
+					"%s, sent %s session ID",
+					session_id_str,
+					session_id_size ? "a new" : "no");
+			tls->session_id_size = 0;
+		}
+	}
+
+	if (session_id_size && !resuming && tls->session_settings) {
+		tls->session_id_new = true;
+		tls->session_id_size = session_id_size;
+		memcpy(tls->session_id, buf + 35, session_id_size);
+	}
+
 	extensions_seen = l_queue_new();
 	result = tls_handle_hello_extensions(tls, buf + 38 + session_id_size,
 						len, extensions_seen);
@@ -1822,18 +2413,16 @@ static void tls_handle_server_hello(stru
 	if (!result)
 		return;
 
-	tls->negotiated_version = l_get_be16(buf);
-
-	if (tls->negotiated_version < tls->min_version ||
-			tls->negotiated_version > tls->max_version) {
-		TLS_DISCONNECT(tls->negotiated_version < tls->min_version ?
+	if (version < tls->min_version || version > tls->max_version) {
+		TLS_DISCONNECT(version < tls->min_version ?
 				TLS_ALERT_PROTOCOL_VERSION :
 				TLS_ALERT_ILLEGAL_PARAM, 0,
-				"Unsupported version %02x",
-				tls->negotiated_version);
+				"Unsupported version %02x", version);
 		return;
 	}
 
+	tls->negotiated_version = version;
+
 	/* Stop maintaining handshake message hashes other than MD1 and SHA. */
 	if (tls->negotiated_version < L_TLS_V12)
 		for (i = 0; i < __HANDSHAKE_HASH_COUNT; i++)
@@ -1889,7 +2478,30 @@ static void tls_handle_server_hello(stru
 
 	TLS_DEBUG("Negotiated %s", tls->pending.compression_method->name);
 
-	if (tls->pending.cipher_suite->signature)
+	if (resuming) {
+		/*
+		 * Now that we've validated the Server Hello parameters and
+		 * know that they're supported by this version of ell and
+		 * consistent with the current configuration, ensure that
+		 * they're identical with the ones in the cached session
+		 * being resumed.  This serves as a sanity check for
+		 * rare situations like a corrupt session cache file or
+		 * a file written by a newer ell version.
+		 */
+		if (tls->negotiated_version != tls->client_version ||
+				memcmp(cipher_suite_id,
+					tls->session_cipher_suite_id, 2) ||
+				compression_method_id !=
+				tls->session_compression_method_id) {
+			TLS_DISCONNECT(TLS_ALERT_HANDSHAKE_FAIL, 0,
+					"Session parameters don't match");
+			return;
+		}
+
+		tls_update_key_block(tls);
+
+		TLS_SET_STATE(TLS_HANDSHAKE_WAIT_CHANGE_CIPHER_SPEC);
+	} else if (tls->pending.cipher_suite->signature)
 		TLS_SET_STATE(TLS_HANDSHAKE_WAIT_CERTIFICATE);
 	else
 		TLS_SET_STATE(TLS_HANDSHAKE_WAIT_KEY_EXCHANGE);
@@ -2177,7 +2789,8 @@ static void tls_handle_server_hello_done
 		return;
 	}
 
-	tls_send_finished(tls);
+	if (!tls_send_finished(tls))
+		return;
 
 	TLS_SET_STATE(TLS_HANDSHAKE_WAIT_CHANGE_CIPHER_SPEC);
 }
@@ -2350,15 +2963,95 @@ error:
 
 static void tls_finished(struct l_tls *tls)
 {
+	_auto_(l_free) char *peer_cert_identity = NULL;
 	char *peer_identity = NULL;
-
-	if (tls->peer_authenticated) {
-		peer_identity = tls_get_peer_identity_str(tls->peer_cert);
-		if (!peer_identity) {
+	uint64_t peer_cert_expiry;
+	bool resuming = tls->session_id_size && !tls->session_id_new;
+	bool session_update = false;
+	bool renegotiation = tls->ready;
+
+	if (tls->peer_authenticated && !resuming) {
+		peer_cert_identity = tls_get_peer_identity_str(tls->peer_cert);
+		if (!peer_cert_identity) {
 			TLS_DISCONNECT(TLS_ALERT_INTERNAL_ERROR, 0,
 					"tls_get_peer_identity_str failed");
 			return;
 		}
+
+		peer_identity = peer_cert_identity;
+
+		if (tls->session_id_new &&
+				!l_cert_get_valid_times(tls->peer_cert, NULL,
+							&peer_cert_expiry)) {
+			TLS_DISCONNECT(TLS_ALERT_INTERNAL_ERROR, 0,
+					"l_cert_get_valid_times failed");
+			return;
+		}
+	} else if (tls->peer_authenticated && resuming)
+		peer_identity = tls->session_peer_identity;
+
+	if (tls->session_settings && tls->session_id_new) {
+		_auto_(l_free) char *session_id_str =
+			l_util_hexstring(tls->session_id, tls->session_id_size);
+		uint64_t expiry = tls->session_lifetime ?
+			time_realtime_now() + tls->session_lifetime : 0;
+		const char *group_name =
+			tls_get_cache_group_name(tls, tls->session_id,
+							tls->session_id_size);
+
+		if (tls->peer_authenticated &&
+				(!expiry || peer_cert_expiry < expiry))
+			expiry = peer_cert_expiry;
+
+		if (!tls->server)
+			l_settings_set_bytes(tls->session_settings, group_name,
+						"SessionID", tls->session_id,
+						tls->session_id_size);
+
+		l_settings_set_bytes(tls->session_settings, group_name,
+					"SessionMasterSecret",
+					tls->pending.master_secret, 48);
+		l_settings_set_int(tls->session_settings, group_name,
+					"SessionVersion",
+					tls->negotiated_version);
+		l_settings_set_bytes(tls->session_settings, group_name,
+					"SessionCipherSuite",
+					tls->pending.cipher_suite->id, 2);
+		l_settings_set_uint(tls->session_settings, group_name,
+					"SessionCompressionMethod",
+					tls->pending.compression_method->id);
+
+		if (expiry)
+			l_settings_set_uint64(tls->session_settings,
+						group_name,
+						"SessionExpiryTime", expiry);
+		else
+			/* We may be overwriting an older session's data */
+			l_settings_remove_key(tls->session_settings,
+						group_name,
+						"SessionExpiryTime");
+
+		if (tls->peer_authenticated)
+			l_settings_set_string(tls->session_settings,
+						group_name,
+						"SessionPeerIdentity",
+						peer_identity);
+		else
+			/* We may be overwriting an older session's data */
+			l_settings_remove_key(tls->session_settings,
+						group_name,
+						"SessionPeerIdentity");
+
+		TLS_DEBUG("Saving new session %s to cache", session_id_str);
+		session_update = true;
+
+		if (tls->session_id_size_replaced) {
+			tls_forget_cached_session(tls, NULL,
+						tls->session_id_replaced,
+						tls->session_id_size_replaced,
+						false);
+			tls->session_id_size_replaced = 0;
+		}
 	}
 
 	/* Free up the resources used in the handshake */
@@ -2366,11 +3059,22 @@ static void tls_finished(struct l_tls *t
 
 	TLS_SET_STATE(TLS_HANDSHAKE_DONE);
 	tls->ready = true;
+	tls->session_resumed = resuming;
+
+	if (session_update && tls->session_update_cb) {
+		tls->in_callback = true;
+		tls->session_update_cb(tls->session_update_user_data);
+		tls->in_callback = false;
 
-	tls->in_callback = true;
-	tls->ready_handle(peer_identity, tls->user_data);
-	tls->in_callback = false;
-	l_free(peer_identity);
+		if (tls->pending_destroy)
+			return;
+	}
+
+	if (!renegotiation) {
+		tls->in_callback = true;
+		tls->ready_handle(peer_identity, tls->user_data);
+		tls->in_callback = false;
+	}
 
 	tls_cleanup_handshake(tls);
 }
@@ -2378,6 +3082,8 @@ static void tls_finished(struct l_tls *t
 static void tls_handle_handshake(struct l_tls *tls, int type,
 					const uint8_t *buf, size_t len)
 {
+	bool resuming;
+
 	TLS_DEBUG("Handling a %s of %zi bytes",
 			tls_handshake_type_to_str(type), len);
 
@@ -2401,7 +3107,16 @@ static void tls_handle_handshake(struct
 		 * and "MAY be ignored by the client if it does not wish to
 		 * renegotiate a session".
 		 */
+		if (tls->state != TLS_HANDSHAKE_DONE) {
+			TLS_DEBUG("Message invalid in state %s",
+					tls_handshake_state_to_str(tls->state));
+			break;
+		}
 
+		if (!tls_send_client_hello(tls))
+			break;
+
+		TLS_SET_STATE(TLS_HANDSHAKE_WAIT_HELLO);
 		break;
 
 	case TLS_CLIENT_HELLO:
@@ -2565,7 +3280,9 @@ static void tls_handle_handshake(struct
 		if (!tls_verify_finished(tls, buf, len))
 			break;
 
-		if (tls->server) {
+		resuming = tls->session_id_size && !tls->session_id_new;
+
+		if ((tls->server && !resuming) || (!tls->server && resuming)) {
 			const char *error;
 
 			tls_send_change_cipher_spec(tls);
@@ -2575,13 +3292,15 @@ static void tls_handle_handshake(struct
 						error);
 				break;
 			}
-			tls_send_finished(tls);
+
+			if (!tls_send_finished(tls))
+				break;
 		}
 
 		/*
-		 * On the client, the server's certificate is now verified
-		 * regardless of the key exchange method, based on the
-		 * following logic:
+		 * When starting a new session on the client, the server's
+		 * certificate is now verified regardless of the key exchange
+		 * method, based on the following logic:
 		 *
 		 *  - tls->ca_certs is non-NULL so tls_handle_certificate
 		 *    (always called on the client) must have veritifed the
@@ -2606,9 +3325,14 @@ static void tls_handle_handshake(struct
 		 *      able to sign the client random together with the
 		 *      ServerKeyExchange parameters using its certified key
 		 *      pair.
+		 *
+		 * If we're resuming a cached session, we have authenticated
+		 * this peer before and the successful decryption of this
+		 * message confirms their identity hasn't changed.
 		 */
-		if (!tls->server && tls->cipher_suite[0]->signature &&
-				tls->ca_certs)
+		if (tls->cipher_suite[0]->signature &&
+				((!tls->server && !resuming && tls->ca_certs) ||
+				 (resuming && tls->session_peer_identity)))
 			tls->peer_authenticated = true;
 
 		tls_finished(tls);
@@ -2643,6 +3367,7 @@ LIB_EXPORT struct l_tls *l_tls_new(bool
 	tls->cipher_suite_pref_list = tls_cipher_suite_pref;
 	tls->min_version = TLS_MIN_VERSION;
 	tls->max_version = TLS_MAX_VERSION;
+	tls->session_lifetime = 24 * 3600 * L_USEC_PER_SEC;
 
 	/* If we're the server wait for the Client Hello already */
 	if (tls->server)
@@ -2669,6 +3394,7 @@ LIB_EXPORT void l_tls_free(struct l_tls
 	l_tls_set_auth_data(tls, NULL, NULL);
 	l_tls_set_domain_mask(tls, NULL);
 	l_tls_set_cert_dump_path(tls, NULL);
+	l_tls_set_session_cache(tls, NULL, NULL, 0, 0, NULL, NULL);
 
 	tls_reset_handshake(tls);
 	tls_cleanup_handshake(tls);
@@ -2870,6 +3596,27 @@ LIB_EXPORT bool l_tls_start(struct l_tls
 	if (!tls_init_handshake_hash(tls))
 		return false;
 
+	/*
+	 * If we're going to try resuming a cached session, send the Client
+	 * Hello with the version we think is supported.
+	 *
+	 * RFC5246 Appendix E.1:
+	 * "Whenever a client already knows the highest protocol version known
+	 * to a server (for example, when resuming a session), it SHOULD
+	 * initiate the connection in that native protocol."
+	 *
+	 * Don't directly set tls->{min,max}_version as that would make the
+	 * handshake fail if the server decides to start a new session with
+	 * a new version instead of resuming, which it is allowed to do.
+	 */
+	tls->client_version = tls->max_version;
+	tls_load_cached_client_session(tls);
+
+	if (tls->pending_destroy) {
+		l_tls_free(tls);
+		return false;
+	}
+
 	if (!tls_send_client_hello(tls))
 		return false;
 
@@ -3042,6 +3789,68 @@ LIB_EXPORT void l_tls_set_domain_mask(st
 	tls->subject_mask = l_strv_copy(mask);
 }
 
+/**
+ * l_tls_set_session_cache:
+ * @tls: TLS object being configured
+ * @settings: l_settings object to read and write session data from/to or
+ *   NULL to disable caching session states.  The object must remain valid
+ *   until this method is called with a different value.
+ * @group_prefix: prefix to build group names inside @settings.  Note:
+ *   existing settings in groups starting with the prefix may be
+ *   overwritten or removed.
+ * @lifetime: a CLOCK_REALTIME-based microsecond resolution lifetime for
+ *   cached sessions.  The RFC recommends 24 hours.
+ * @max_sessions: limit on the number of sessions in the cache, or 0 for
+ *   unlimited.  Ignored in client mode.
+ * @update_cb: a callback to be invoked whenever the settings in @settings
+ *   have been updated and may need to be written to persistent storage if
+ *   desired, or NULL.
+ * @user_data: user data pointer to pass to @update_cb.
+ *
+ * Enables caching and resuming session states as described in RFC 5246 for
+ * faster setup.  l_tls will maintain the required session state data in
+ * @settings including removing expired or erroneous sessions.
+ *
+ * A client's cache contains at most one session state since the client
+ * must request one specific Session ID from the server when resuming a
+ * session.  The resumption will only work while the state is cached by
+ * both the server and the client so clients should keep separate @settings
+ * objects or use separate groups inside one object for every discrete
+ * server they may want to connect to.
+ *
+ * Multiple l_tls clients connecting to the same server can share one cache
+ * to allow reusing an established session that is still active (actual
+ * concurrency is not supported as there's no locking.)
+ */
+LIB_EXPORT void l_tls_set_session_cache(struct l_tls *tls,
+					struct l_settings *settings,
+					const char *group_prefix,
+					uint64_t lifetime,
+					unsigned int max_sessions,
+					l_tls_session_update_cb_t update_cb,
+					void *user_data)
+{
+	if (unlikely(!tls))
+		return;
+
+	tls->session_settings = settings;
+	tls->session_lifetime = lifetime;
+	tls->session_count_max = max_sessions;
+	tls->session_update_cb = update_cb;
+	tls->session_update_user_data = user_data;
+
+	l_free(tls->session_prefix);
+	tls->session_prefix = l_strdup(group_prefix);
+}
+
+LIB_EXPORT bool l_tls_get_session_resumed(struct l_tls *tls)
+{
+	if (unlikely(!tls || !tls->ready))
+		return false;
+
+	return tls->session_resumed;
+}
+
 LIB_EXPORT const char *l_tls_alert_to_str(enum l_tls_alert_desc desc)
 {
 	switch (desc) {
diff -pruN 1.30-1/ell/tls-extensions.c 2.3-1/ell/tls-extensions.c
--- 1.30-1/ell/tls-extensions.c	2020-09-05 07:26:19.000000000 +0000
+++ 2.3-1/ell/tls-extensions.c	2022-11-18 09:08:38.000000000 +0000
@@ -31,6 +31,13 @@
 #include "cert.h"
 #include "tls-private.h"
 
+/* Most extensions are not used when resuming a cached session */
+#define SKIP_ON_RESUMPTION()	\
+	do {	\
+		if (tls->session_id_size && !tls->session_id_new)	\
+			return -ENOMSG;	\
+	} while (0);
+
 /* RFC 7919, Section A.1 */
 static const uint8_t tls_ffdhe2048_prime[] = {
 	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xad, 0xf8, 0x54, 0x58,
@@ -560,6 +567,8 @@ static bool tls_ec_point_formats_client_
 static ssize_t tls_ec_point_formats_server_write(struct l_tls *tls,
 						uint8_t *buf, size_t len)
 {
+	SKIP_ON_RESUMPTION(); /* RFC 4492 Section 4 */
+
 	if (len < 2)
 		return -ENOMEM;
 
@@ -785,7 +794,7 @@ ssize_t tls_parse_signature_algorithms(s
 	return ptr + len - buf;
 }
 
-/* RFC 5462, Section 7.4.1.4.1 */
+/* RFC 5246, Section 7.4.1.4.1 */
 static ssize_t tls_signature_algorithms_client_write(struct l_tls *tls,
 						uint8_t *buf, size_t len)
 {
@@ -841,6 +850,130 @@ static bool tls_signature_algorithms_cli
 	return true;
 }
 
+/* RFC 5746, Section 3.2 */
+static ssize_t tls_renegotiation_info_client_write(struct l_tls *tls,
+						uint8_t *buf, size_t len)
+{
+	/*
+	 * Section 4.2 implies we should send the client_verify_data on
+	 * renegotiation even if .secure_renegotiation is false, if we want
+	 * to go through with the renegotiation in the first place.
+	 */
+	if (tls->ready) {
+		size_t vdl = tls_verify_data_length(tls, 1);
+
+		if (len < vdl + 1)
+			return -ENOMEM;
+
+		buf[0] = vdl;
+		memcpy(buf + 1, tls->renegotiation_info.client_verify_data,
+			vdl);
+		return 1 + vdl;
+	} else {
+		if (len < 1)
+			return -ENOMEM;
+
+		buf[0] = 0x00;	/* Empty "renegotiated_connection" */
+		return 1;
+	}
+}
+
+static ssize_t tls_renegotiation_info_server_write(struct l_tls *tls,
+						uint8_t *buf, size_t len)
+{
+	if (tls->ready) {
+		size_t rx_vdl = tls_verify_data_length(tls, 0);
+		size_t tx_vdl = tls_verify_data_length(tls, 1);
+
+		if (len < rx_vdl + tx_vdl + 1)
+			return -ENOMEM;
+
+		buf[0] = rx_vdl + tx_vdl;
+		memcpy(buf + 1,
+			tls->renegotiation_info.client_verify_data, rx_vdl);
+		memcpy(buf + 1 + rx_vdl,
+			tls->renegotiation_info.server_verify_data, tx_vdl);
+		return 1 + rx_vdl + tx_vdl;
+	} else {
+		if (len < 1)
+			return -ENOMEM;
+
+		buf[0] = 0x00;	/* Empty "renegotiated_connection" */
+		return 1;
+	}
+}
+
+static bool tls_renegotiation_info_client_handle(struct l_tls *tls,
+						const uint8_t *buf, size_t len)
+{
+	if (tls->ready) {
+		size_t vdl = tls_verify_data_length(tls, 0);
+
+		return len >= 1 + vdl &&
+			tls->renegotiation_info.secure_renegotiation &&
+			!memcmp(tls->renegotiation_info.client_verify_data,
+				buf + 1, vdl);
+	}
+
+	/*
+	 * RFC 5746 Section 3.6: "The server MUST then verify that the length
+	 * of the "renegotiated_connection" field is zero, ..."
+	 */
+	if (len < 1 || buf[0] != 0x00)
+		return false;
+
+	tls->renegotiation_info.secure_renegotiation = true;
+	return true;
+}
+
+static bool tls_renegotiation_info_server_handle(struct l_tls *tls,
+						const uint8_t *buf, size_t len)
+{
+	if (tls->ready) {
+		size_t rx_vdl = tls_verify_data_length(tls, 0);
+		size_t tx_vdl = tls_verify_data_length(tls, 1);
+
+		return len >= 1 + rx_vdl + tx_vdl &&
+			tls->renegotiation_info.secure_renegotiation &&
+			!memcmp(tls->renegotiation_info.client_verify_data,
+				buf + 1, tx_vdl) &&
+			!memcmp(tls->renegotiation_info.server_verify_data,
+				buf + 1 + tx_vdl, rx_vdl);
+	}
+
+	/*
+	 * RFC 5746 Section 3.4: "The client MUST then verify that the length
+	 * of the "renegotiated_connection" field is zero, ..."
+	 */
+	if (len < 1 || buf[0] != 0x00)
+		return false;
+
+	tls->renegotiation_info.secure_renegotiation = true;
+	return true;
+}
+
+static bool tls_renegotiation_info_absent(struct l_tls *tls)
+{
+	/*
+	 * RFC 5746 Section 4.2: "It is possible that un-upgraded servers
+	 * will request that the client renegotiate.  It is RECOMMENDED
+	 * that clients refuse this renegotiation request." and Section 4.4:
+	 * "It is RECOMMENDED that servers not permit legacy renegotiation."
+	 *
+	 * This may need to be made configurable, for now follow the
+	 * recommendation and don't renegotiate.
+	 */
+	if (tls->ready)
+		return false;
+
+	/*
+	 * The normal policy otherwise is that the extension must be
+	 * present in renegotation if the previous Client or Server Hello
+	 * did include this extension, or the SCSV in the Client Hello case.
+	 */
+	return !tls->ready || !tls->renegotiation_info.secure_renegotiation;
+}
+
 const struct tls_hello_extension tls_extensions[] = {
 	{
 		"Supported Groups", "elliptic_curves", 10,
@@ -864,6 +997,15 @@ const struct tls_hello_extension tls_ext
 		tls_signature_algorithms_client_absent,
 		NULL, NULL, NULL,
 	},
+	{
+		"Secure Renegotiation", "renegotiation_info", 0xff01,
+		tls_renegotiation_info_client_write,
+		tls_renegotiation_info_client_handle,
+		tls_renegotiation_info_absent,
+		tls_renegotiation_info_server_write,
+		tls_renegotiation_info_server_handle,
+		tls_renegotiation_info_absent,
+	},
 	{}
 };
 
diff -pruN 1.30-1/ell/tls.h 2.3-1/ell/tls.h
--- 1.30-1/ell/tls.h	2022-06-04 19:58:23.000000000 +0000
+++ 2.3-1/ell/tls.h	2023-01-23 18:26:15.000000000 +0000
@@ -36,6 +36,7 @@ struct l_tls;
 struct l_key;
 struct l_certchain;
 struct l_queue;
+struct l_settings;
 
 enum l_tls_alert_desc {
 	TLS_ALERT_CLOSE_NOTIFY		= 0,
@@ -72,6 +73,7 @@ typedef void (*l_tls_disconnect_cb_t)(en
 					bool remote, void *user_data);
 typedef void (*l_tls_debug_cb_t)(const char *str, void *user_data);
 typedef void (*l_tls_destroy_cb_t)(void *user_data);
+typedef void (*l_tls_session_update_cb_t)(void *user_data);
 
 /*
  * app_data_handler gets called with newly received decrypted data.
@@ -127,6 +129,13 @@ void l_tls_set_version_range(struct l_tl
 
 void l_tls_set_domain_mask(struct l_tls *tls, char **mask);
 
+void l_tls_set_session_cache(struct l_tls *tls, struct l_settings *settings,
+				const char *group_prefix, uint64_t lifetime,
+				unsigned int max_sessions,
+				l_tls_session_update_cb_t update_cb,
+				void *user_data);
+bool l_tls_get_session_resumed(struct l_tls *tls);
+
 const char *l_tls_alert_to_str(enum l_tls_alert_desc desc);
 
 enum l_checksum_type;
diff -pruN 1.30-1/ell/tls-private.h 2.3-1/ell/tls-private.h
--- 1.30-1/ell/tls-private.h	2022-09-07 18:21:45.000000000 +0000
+++ 2.3-1/ell/tls-private.h	2023-01-23 18:26:15.000000000 +0000
@@ -218,6 +218,13 @@ struct l_tls {
 
 	struct tls_cipher_suite **cipher_suite_pref_list;
 
+	struct l_settings *session_settings;
+	char *session_prefix;
+	uint64_t session_lifetime;
+	unsigned int session_count_max;
+	l_tls_session_update_cb_t session_update_cb;
+	void *session_update_user_data;
+
 	bool in_callback;
 	bool pending_destroy;
 
@@ -251,6 +258,23 @@ struct l_tls {
 	const struct tls_named_group *negotiated_curve;
 	const struct tls_named_group *negotiated_ff_group;
 
+	uint8_t session_id[32];
+	size_t session_id_size;
+	uint8_t session_id_replaced[32];
+	size_t session_id_size_replaced;
+	bool session_id_new;
+	uint8_t session_cipher_suite_id[2];
+	uint8_t session_compression_method_id;
+	char *session_peer_identity;
+	bool session_resumed;
+
+	struct {
+		bool secure_renegotiation;
+		/* Max .verify_data_length over supported cipher suites */
+		uint8_t client_verify_data[12];
+		uint8_t server_verify_data[12];
+	} renegotiation_info;
+
 	/* SecurityParameters current and pending */
 
 	struct {
@@ -325,6 +349,8 @@ void tls_generate_master_secret(struct l
 				const uint8_t *pre_master_secret,
 				int pre_master_secret_len);
 
+size_t tls_verify_data_length(struct l_tls *tls, unsigned int index);
+
 const struct tls_named_group *tls_find_group(uint16_t id);
 const struct tls_named_group *tls_find_ff_group(const uint8_t *prime,
 						size_t prime_len,
diff -pruN 1.30-1/ell/tls-suites.c 2.3-1/ell/tls-suites.c
--- 1.30-1/ell/tls-suites.c	2022-09-07 18:21:45.000000000 +0000
+++ 2.3-1/ell/tls-suites.c	2022-11-18 09:08:38.000000000 +0000
@@ -352,8 +352,8 @@ static bool tls_send_rsa_client_key_xchg
 	}
 
 	/* Must match the version in tls_send_client_hello */
-	pre_master_secret[0] = (uint8_t) (tls->max_version >> 8);
-	pre_master_secret[1] = (uint8_t) (tls->max_version >> 0);
+	pre_master_secret[0] = (uint8_t) (tls->client_version >> 8);
+	pre_master_secret[1] = (uint8_t) (tls->client_version >> 0);
 
 	l_getrandom(pre_master_secret + 2, 46);
 
diff -pruN 1.30-1/ell/util.c 2.3-1/ell/util.c
--- 1.30-1/ell/util.c	2021-08-01 19:49:37.000000000 +0000
+++ 2.3-1/ell/util.c	2023-01-23 18:26:15.000000000 +0000
@@ -376,6 +376,36 @@ static char *hexstring_common(const unsi
 	return str;
 }
 
+static char *hexstringv_common(const struct iovec *iov, size_t n_iov,
+				const char hexdigits[static 16])
+{
+	char *str;
+	size_t i, j, c;
+	size_t len;
+
+	if (unlikely(!iov || !n_iov))
+		return NULL;
+
+	for (i = 0, len = 0; i < n_iov; i++)
+		len += iov[i].iov_len;
+
+	str = l_malloc(len * 2 + 1);
+	c = 0;
+
+	for (i = 0; i < n_iov; i++) {
+		const uint8_t *buf = iov[i].iov_base;
+
+		for (j = 0; j < iov[i].iov_len; j++) {
+			str[c++] = hexdigits[buf[j] >> 4];
+			str[c++] = hexdigits[buf[j] & 0xf];
+		}
+	}
+
+	str[len * 2] = '\0';
+
+	return str;
+}
+
 /**
  * l_util_hexstring:
  * @buf: buffer pointer
@@ -407,6 +437,36 @@ LIB_EXPORT char *l_util_hexstring_upper(
 }
 
 /**
+ * l_util_hexstringv:
+ * @iov: iovec
+ * @n_iov: length of the iovec
+ *
+ * Returns: a newly allocated hex string.  Note that the string will contain
+ * lower case hex digits a-f.  If you require upper case hex digits, use
+ * @l_util_hexstringv_upper
+ **/
+LIB_EXPORT char *l_util_hexstringv(const struct iovec *iov, size_t n_iov)
+{
+	static const char hexdigits[] = "0123456789abcdef";
+	return hexstringv_common(iov, n_iov, hexdigits);
+}
+
+/**
+ * l_util_hexstringv_upper:
+ * @iov: iovec
+ * @n_iov: length of the iovec
+ *
+ * Returns: a newly allocated hex string.  Note that the string will contain
+ * upper case hex digits a-f.  If you require lower case hex digits, use
+ * @l_util_hexstringv
+ **/
+LIB_EXPORT char *l_util_hexstringv_upper(const struct iovec *iov, size_t n_iov)
+{
+	static const char hexdigits[] = "0123456789ABCDEF";
+	return hexstringv_common(iov, n_iov, hexdigits);
+}
+
+/**
  * l_util_from_hexstring:
  * @str: Null-terminated string containing the hex-encoded bytes
  * @out_len: Number of bytes decoded
diff -pruN 1.30-1/ell/util.h 2.3-1/ell/util.h
--- 1.30-1/ell/util.h	2022-07-15 16:22:44.000000000 +0000
+++ 2.3-1/ell/util.h	2023-01-23 18:26:15.000000000 +0000
@@ -282,6 +282,8 @@ bool l_streq0(const char *a, const char
 
 char *l_util_hexstring(const void *buf, size_t len);
 char *l_util_hexstring_upper(const void *buf, size_t len);
+char *l_util_hexstringv(const struct iovec *iov, size_t n_iov);
+char *l_util_hexstringv_upper(const struct iovec *iov, size_t n_iov);
 unsigned char *l_util_from_hexstring(const char *str, size_t *out_len);
 
 typedef void (*l_util_hexdump_func_t) (const char *str, void *user_data);
diff -pruN 1.30-1/linux/nl80211.h 2.3-1/linux/nl80211.h
--- 1.30-1/linux/nl80211.h	2021-05-04 13:01:25.000000000 +0000
+++ 2.3-1/linux/nl80211.h	2022-11-18 12:31:49.000000000 +0000
@@ -11,7 +11,7 @@
  * Copyright 2008 Jouni Malinen <jouni.malinen@atheros.com>
  * Copyright 2008 Colin McCabe <colin@cozybit.com>
  * Copyright 2015-2017	Intel Deutschland GmbH
- * Copyright (C) 2018-2020 Intel Corporation
+ * Copyright (C) 2018-2022 Intel Corporation
  *
  * Permission to use, copy, modify, and/or distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
@@ -301,6 +301,40 @@
  */
 
 /**
+ * DOC: FILS shared key crypto offload
+ *
+ * This feature is applicable to drivers running in AP mode.
+ *
+ * FILS shared key crypto offload can be advertised by drivers by setting
+ * @NL80211_EXT_FEATURE_FILS_CRYPTO_OFFLOAD flag. The drivers that support
+ * FILS shared key crypto offload should be able to encrypt and decrypt
+ * association frames for FILS shared key authentication as per IEEE 802.11ai.
+ * With this capability, for FILS key derivation, drivers depend on userspace.
+ *
+ * After FILS key derivation, userspace shares the FILS AAD details with the
+ * driver and the driver stores the same to use in decryption of association
+ * request and in encryption of association response. The below parameters
+ * should be given to the driver in %NL80211_CMD_SET_FILS_AAD.
+ *	%NL80211_ATTR_MAC - STA MAC address, used for storing FILS AAD per STA
+ *	%NL80211_ATTR_FILS_KEK - Used for encryption or decryption
+ *	%NL80211_ATTR_FILS_NONCES - Used for encryption or decryption
+ *			(STA Nonce 16 bytes followed by AP Nonce 16 bytes)
+ *
+ * Once the association is done, the driver cleans the FILS AAD data.
+ */
+
+/**
+ * DOC: Multi-Link Operation
+ *
+ * In Multi-Link Operation, a connection between to MLDs utilizes multiple
+ * links. To use this in nl80211, various commands and responses now need
+ * to or will include the new %NL80211_ATTR_MLO_LINKS attribute.
+ * Additionally, various commands that need to operate on a specific link
+ * now need to be given the %NL80211_ATTR_MLO_LINK_ID attribute, e.g. to
+ * use %NL80211_CMD_START_AP or similar functions.
+ */
+
+/**
  * enum nl80211_commands - supported nl80211 commands
  *
  * @NL80211_CMD_UNSPEC: unspecified command to catch errors
@@ -337,17 +371,28 @@
  * @NL80211_CMD_DEL_INTERFACE: Virtual interface was deleted, has attributes
  *	%NL80211_ATTR_IFINDEX and %NL80211_ATTR_WIPHY. Can also be sent from
  *	userspace to request deletion of a virtual interface, then requires
- *	attribute %NL80211_ATTR_IFINDEX.
+ *	attribute %NL80211_ATTR_IFINDEX. If multiple BSSID advertisements are
+ *	enabled using %NL80211_ATTR_MBSSID_CONFIG, %NL80211_ATTR_MBSSID_ELEMS,
+ *	and if this command is used for the transmitting interface, then all
+ *	the non-transmitting interfaces are deleted as well.
  *
  * @NL80211_CMD_GET_KEY: Get sequence counter information for a key specified
- *	by %NL80211_ATTR_KEY_IDX and/or %NL80211_ATTR_MAC.
+ *	by %NL80211_ATTR_KEY_IDX and/or %NL80211_ATTR_MAC. %NL80211_ATTR_MAC
+ *	represents peer's MLD address for MLO pairwise key. For MLO group key,
+ *	the link is identified by %NL80211_ATTR_MLO_LINK_ID.
  * @NL80211_CMD_SET_KEY: Set key attributes %NL80211_ATTR_KEY_DEFAULT,
  *	%NL80211_ATTR_KEY_DEFAULT_MGMT, or %NL80211_ATTR_KEY_THRESHOLD.
+ *	For MLO connection, the link to set default key is identified by
+ *	%NL80211_ATTR_MLO_LINK_ID.
  * @NL80211_CMD_NEW_KEY: add a key with given %NL80211_ATTR_KEY_DATA,
  *	%NL80211_ATTR_KEY_IDX, %NL80211_ATTR_MAC, %NL80211_ATTR_KEY_CIPHER,
- *	and %NL80211_ATTR_KEY_SEQ attributes.
+ *	and %NL80211_ATTR_KEY_SEQ attributes. %NL80211_ATTR_MAC represents
+ *	peer's MLD address for MLO pairwise key. The link to add MLO
+ *	group key is identified by %NL80211_ATTR_MLO_LINK_ID.
  * @NL80211_CMD_DEL_KEY: delete a key identified by %NL80211_ATTR_KEY_IDX
- *	or %NL80211_ATTR_MAC.
+ *	or %NL80211_ATTR_MAC. %NL80211_ATTR_MAC represents peer's MLD address
+ *	for MLO pairwise key. The link to delete group key is identified by
+ *	%NL80211_ATTR_MLO_LINK_ID.
  *
  * @NL80211_CMD_GET_BEACON: (not used)
  * @NL80211_CMD_SET_BEACON: change the beacon on an access point interface
@@ -727,6 +772,13 @@
  *	%NL80211_ATTR_CSA_C_OFFSETS_TX is an array of offsets to CSA
  *	counters which will be updated to the current value. This attribute
  *	is used during CSA period.
+ *	For TX on an MLD, the frequency can be omitted and the link ID be
+ *	specified, or if transmitting to a known peer MLD (with MLD addresses
+ *	in the frame) both can be omitted and the link will be selected by
+ *	lower layers.
+ *	For RX notification, %NL80211_ATTR_RX_HW_TIMESTAMP may be included to
+ *	indicate the frame RX timestamp and %NL80211_ATTR_TX_HW_TIMESTAMP may
+ *	be included to indicate the ack TX timestamp.
  * @NL80211_CMD_FRAME_WAIT_CANCEL: When an off-channel TX was requested, this
  *	command may be used with the corresponding cookie to cancel the wait
  *	time if it is known that it is no longer necessary.  This command is
@@ -737,7 +789,9 @@
  *	transmitted with %NL80211_CMD_FRAME. %NL80211_ATTR_COOKIE identifies
  *	the TX command and %NL80211_ATTR_FRAME includes the contents of the
  *	frame. %NL80211_ATTR_ACK flag is included if the recipient acknowledged
- *	the frame.
+ *	the frame. %NL80211_ATTR_TX_HW_TIMESTAMP may be included to indicate the
+ *	tx timestamp and %NL80211_ATTR_RX_HW_TIMESTAMP may be included to
+ *	indicate the ack RX timestamp.
  * @NL80211_CMD_ACTION_TX_STATUS: Alias for @NL80211_CMD_FRAME_TX_STATUS for
  *	backward compatibility.
  *
@@ -1082,6 +1136,12 @@
  *	has been received. %NL80211_ATTR_FRAME is used to specify the
  *	frame contents.  The frame is the raw EAPoL data, without ethernet or
  *	802.11 headers.
+ *	For an MLD transmitter, the %NL80211_ATTR_MLO_LINK_ID may be given and
+ *	its effect will depend on the destination: If the destination is known
+ *	to be an MLD, this will be used as a hint to select the link to transmit
+ *	the frame on. If the destination is not an MLD, this will select both
+ *	the link to transmit on and the source address will be set to the link
+ *	address of that link.
  *	When used as an event indication %NL80211_ATTR_CONTROL_PORT_ETHERTYPE,
  *	%NL80211_ATTR_CONTROL_PORT_NO_ENCRYPT and %NL80211_ATTR_MAC are added
  *	indicating the protocol type of the received frame; whether the frame
@@ -1185,6 +1245,42 @@
  *	passed using %NL80211_ATTR_SAR_SPEC. %NL80211_ATTR_WIPHY is used to
  *	specify the wiphy index to be applied to.
  *
+ * @NL80211_CMD_OBSS_COLOR_COLLISION: This notification is sent out whenever
+ *	mac80211/drv detects a bss color collision.
+ *
+ * @NL80211_CMD_COLOR_CHANGE_REQUEST: This command is used to indicate that
+ *	userspace wants to change the BSS color.
+ *
+ * @NL80211_CMD_COLOR_CHANGE_STARTED: Notify userland, that a color change has
+ *	started
+ *
+ * @NL80211_CMD_COLOR_CHANGE_ABORTED: Notify userland, that the color change has
+ *	been aborted
+ *
+ * @NL80211_CMD_COLOR_CHANGE_COMPLETED: Notify userland that the color change
+ *	has completed
+ *
+ * @NL80211_CMD_SET_FILS_AAD: Set FILS AAD data to the driver using -
+ *	&NL80211_ATTR_MAC - for STA MAC address
+ *	&NL80211_ATTR_FILS_KEK - for KEK
+ *	&NL80211_ATTR_FILS_NONCES - for FILS Nonces
+ *		(STA Nonce 16 bytes followed by AP Nonce 16 bytes)
+ *
+ * @NL80211_CMD_ASSOC_COMEBACK: notification about an association
+ *      temporal rejection with comeback. The event includes %NL80211_ATTR_MAC
+ *      to describe the BSSID address of the AP and %NL80211_ATTR_TIMEOUT to
+ *      specify the timeout value.
+ *
+ * @NL80211_CMD_ADD_LINK: Add a new link to an interface. The
+ *	%NL80211_ATTR_MLO_LINK_ID attribute is used for the new link.
+ * @NL80211_CMD_REMOVE_LINK: Remove a link from an interface. This may come
+ *	without %NL80211_ATTR_MLO_LINK_ID as an easy way to remove all links
+ *	in preparation for e.g. roaming to a regular (non-MLO) AP.
+ *
+ * @NL80211_CMD_ADD_LINK_STA: Add a link to an MLD station
+ * @NL80211_CMD_MODIFY_LINK_STA: Modify a link of an MLD station
+ * @NL80211_CMD_REMOVE_LINK_STA: Remove a link of an MLD station
+ *
  * @NL80211_CMD_MAX: highest used command number
  * @__NL80211_CMD_AFTER_LAST: internal use
  */
@@ -1417,6 +1513,25 @@ enum nl80211_commands {
 
 	NL80211_CMD_SET_SAR_SPECS,
 
+	NL80211_CMD_OBSS_COLOR_COLLISION,
+
+	NL80211_CMD_COLOR_CHANGE_REQUEST,
+
+	NL80211_CMD_COLOR_CHANGE_STARTED,
+	NL80211_CMD_COLOR_CHANGE_ABORTED,
+	NL80211_CMD_COLOR_CHANGE_COMPLETED,
+
+	NL80211_CMD_SET_FILS_AAD,
+
+	NL80211_CMD_ASSOC_COMEBACK,
+
+	NL80211_CMD_ADD_LINK,
+	NL80211_CMD_REMOVE_LINK,
+
+	NL80211_CMD_ADD_LINK_STA,
+	NL80211_CMD_MODIFY_LINK_STA,
+	NL80211_CMD_REMOVE_LINK_STA,
+
 	/* add new commands above here */
 
 	/* used to define NL80211_CMD_MAX below */
@@ -2276,8 +2391,10 @@ enum nl80211_commands {
  *
  * @NL80211_ATTR_IFTYPE_EXT_CAPA: Nested attribute of the following attributes:
  *	%NL80211_ATTR_IFTYPE, %NL80211_ATTR_EXT_CAPA,
- *	%NL80211_ATTR_EXT_CAPA_MASK, to specify the extended capabilities per
- *	interface type.
+ *	%NL80211_ATTR_EXT_CAPA_MASK, to specify the extended capabilities and
+ *	other interface-type specific capabilities per interface type. For MLO,
+ *	%NL80211_ATTR_EML_CAPABILITY and %NL80211_ATTR_MLD_CAPA_AND_OPS are
+ *	present.
  *
  * @NL80211_ATTR_MU_MIMO_GROUP_DATA: array of 24 bytes that defines a MU-MIMO
  *	groupID for monitor mode.
@@ -2413,7 +2530,9 @@ enum nl80211_commands {
  *	space supports external authentication. This attribute shall be used
  *	with %NL80211_CMD_CONNECT and %NL80211_CMD_START_AP request. The driver
  *	may offload authentication processing to user space if this capability
- *	is indicated in the respective requests from the user space.
+ *	is indicated in the respective requests from the user space. (This flag
+ *	attribute deprecated for %NL80211_CMD_START_AP, use
+ *	%NL80211_ATTR_AP_SETTINGS_FLAGS)
  *
  * @NL80211_ATTR_NSS: Station's New/updated  RX_NSS value notified using this
  *	u8 attribute. This is used with %NL80211_CMD_STA_OPMODE_CHANGED.
@@ -2560,6 +2679,76 @@ enum nl80211_commands {
  *	disassoc events to indicate that an immediate reconnect to the AP
  *	is desired.
  *
+ * @NL80211_ATTR_OBSS_COLOR_BITMAP: bitmap of the u64 BSS colors for the
+ *	%NL80211_CMD_OBSS_COLOR_COLLISION event.
+ *
+ * @NL80211_ATTR_COLOR_CHANGE_COUNT: u8 attribute specifying the number of TBTT's
+ *	until the color switch event.
+ * @NL80211_ATTR_COLOR_CHANGE_COLOR: u8 attribute specifying the color that we are
+ *	switching to
+ * @NL80211_ATTR_COLOR_CHANGE_ELEMS: Nested set of attributes containing the IE
+ *	information for the time while performing a color switch.
+ *
+ * @NL80211_ATTR_MBSSID_CONFIG: Nested attribute for multiple BSSID
+ *	advertisements (MBSSID) parameters in AP mode.
+ *	Kernel uses this attribute to indicate the driver's support for MBSSID
+ *	and enhanced multi-BSSID advertisements (EMA AP) to the userspace.
+ *	Userspace should use this attribute to configure per interface MBSSID
+ *	parameters.
+ *	See &enum nl80211_mbssid_config_attributes for details.
+ *
+ * @NL80211_ATTR_MBSSID_ELEMS: Nested parameter to pass multiple BSSID elements.
+ *	Mandatory parameter for the transmitting interface to enable MBSSID.
+ *	Optional for the non-transmitting interfaces.
+ *
+ * @NL80211_ATTR_RADAR_BACKGROUND: Configure dedicated offchannel chain
+ *	available for radar/CAC detection on some hw. This chain can't be used
+ *	to transmit or receive frames and it is bounded to a running wdev.
+ *	Background radar/CAC detection allows to avoid the CAC downtime
+ *	switching on a different channel during CAC detection on the selected
+ *	radar channel.
+ *
+ * @NL80211_ATTR_AP_SETTINGS_FLAGS: u32 attribute contains ap settings flags,
+ *	enumerated in &enum nl80211_ap_settings_flags. This attribute shall be
+ *	used with %NL80211_CMD_START_AP request.
+ *
+ * @NL80211_ATTR_EHT_CAPABILITY: EHT Capability information element (from
+ *	association request when used with NL80211_CMD_NEW_STATION). Can be set
+ *	only if %NL80211_STA_FLAG_WME is set.
+ *
+ * @NL80211_ATTR_MLO_LINK_ID: A (u8) link ID for use with MLO, to be used with
+ *	various commands that need a link ID to operate.
+ * @NL80211_ATTR_MLO_LINKS: A nested array of links, each containing some
+ *	per-link information and a link ID.
+ * @NL80211_ATTR_MLD_ADDR: An MLD address, used with various commands such as
+ *	authenticate/associate.
+ *
+ * @NL80211_ATTR_MLO_SUPPORT: Flag attribute to indicate user space supports MLO
+ *	connection. Used with %NL80211_CMD_CONNECT. If this attribute is not
+ *	included in NL80211_CMD_CONNECT drivers must not perform MLO connection.
+ *
+ * @NL80211_ATTR_MAX_NUM_AKM_SUITES: U16 attribute. Indicates maximum number of
+ *	AKM suites allowed for %NL80211_CMD_CONNECT, %NL80211_CMD_ASSOCIATE and
+ *	%NL80211_CMD_START_AP in %NL80211_CMD_GET_WIPHY response. If this
+ *	attribute is not present userspace shall consider maximum number of AKM
+ *	suites allowed as %NL80211_MAX_NR_AKM_SUITES which is the legacy maximum
+ *	number prior to the introduction of this attribute.
+ *
+ * @NL80211_ATTR_EML_CAPABILITY: EML Capability information (u16)
+ * @NL80211_ATTR_MLD_CAPA_AND_OPS: MLD Capabilities and Operations (u16)
+ *
+ * @NL80211_ATTR_TX_HW_TIMESTAMP: Hardware timestamp for TX operation in
+ *	nanoseconds (u64). This is the device clock timestamp so it will
+ *	probably reset when the device is stopped or the firmware is reset.
+ *	When used with %NL80211_CMD_FRAME_TX_STATUS, indicates the frame TX
+ *	timestamp. When used with %NL80211_CMD_FRAME RX notification, indicates
+ *	the ack TX timestamp.
+ * @NL80211_ATTR_RX_HW_TIMESTAMP: Hardware timestamp for RX operation in
+ *	nanoseconds (u64). This is the device clock timestamp so it will
+ *	probably reset when the device is stopped or the firmware is reset.
+ *	When used with %NL80211_CMD_FRAME_TX_STATUS, indicates the ack RX
+ *	timestamp. When used with %NL80211_CMD_FRAME RX notification, indicates
+ *	the incoming frame RX timestamp.
  * @NUM_NL80211_ATTR: total number of nl80211_attrs available
  * @NL80211_ATTR_MAX: highest attribute number currently defined
  * @__NL80211_ATTR_AFTER_LAST: internal use
@@ -3057,6 +3246,37 @@ enum nl80211_attrs {
 
 	NL80211_ATTR_DISABLE_HE,
 
+	NL80211_ATTR_OBSS_COLOR_BITMAP,
+
+	NL80211_ATTR_COLOR_CHANGE_COUNT,
+	NL80211_ATTR_COLOR_CHANGE_COLOR,
+	NL80211_ATTR_COLOR_CHANGE_ELEMS,
+
+	NL80211_ATTR_MBSSID_CONFIG,
+	NL80211_ATTR_MBSSID_ELEMS,
+
+	NL80211_ATTR_RADAR_BACKGROUND,
+
+	NL80211_ATTR_AP_SETTINGS_FLAGS,
+
+	NL80211_ATTR_EHT_CAPABILITY,
+
+	NL80211_ATTR_DISABLE_EHT,
+
+	NL80211_ATTR_MLO_LINKS,
+	NL80211_ATTR_MLO_LINK_ID,
+	NL80211_ATTR_MLD_ADDR,
+
+	NL80211_ATTR_MLO_SUPPORT,
+
+	NL80211_ATTR_MAX_NUM_AKM_SUITES,
+
+	NL80211_ATTR_EML_CAPABILITY,
+	NL80211_ATTR_MLD_CAPA_AND_OPS,
+
+	NL80211_ATTR_TX_HW_TIMESTAMP,
+	NL80211_ATTR_RX_HW_TIMESTAMP,
+
 	/* add attributes here, update the policy in nl80211.c */
 
 	__NL80211_ATTR_AFTER_LAST,
@@ -3111,7 +3331,14 @@ enum nl80211_attrs {
 #define NL80211_HE_MIN_CAPABILITY_LEN           16
 #define NL80211_HE_MAX_CAPABILITY_LEN           54
 #define NL80211_MAX_NR_CIPHER_SUITES		5
+
+/*
+ * NL80211_MAX_NR_AKM_SUITES is obsolete when %NL80211_ATTR_MAX_NUM_AKM_SUITES
+ * present in %NL80211_CMD_GET_WIPHY response.
+ */
 #define NL80211_MAX_NR_AKM_SUITES		2
+#define NL80211_EHT_MIN_CAPABILITY_LEN          13
+#define NL80211_EHT_MAX_CAPABILITY_LEN          51
 
 #define NL80211_MIN_REMAIN_ON_CHANNEL_TIME	10
 
@@ -3139,7 +3366,7 @@ enum nl80211_attrs {
  *	and therefore can't be created in the normal ways, use the
  *	%NL80211_CMD_START_P2P_DEVICE and %NL80211_CMD_STOP_P2P_DEVICE
  *	commands to create and destroy one
- * @NL80211_IF_TYPE_OCB: Outside Context of a BSS
+ * @NL80211_IFTYPE_OCB: Outside Context of a BSS
  *	This mode corresponds to the MIB variable dot11OCBActivated=true
  * @NL80211_IFTYPE_NAN: NAN device interface type (not a netdev)
  * @NL80211_IFTYPE_MAX: highest interface type number currently defined
@@ -3281,6 +3508,56 @@ enum nl80211_he_ru_alloc {
 };
 
 /**
+ * enum nl80211_eht_gi - EHT guard interval
+ * @NL80211_RATE_INFO_EHT_GI_0_8: 0.8 usec
+ * @NL80211_RATE_INFO_EHT_GI_1_6: 1.6 usec
+ * @NL80211_RATE_INFO_EHT_GI_3_2: 3.2 usec
+ */
+enum nl80211_eht_gi {
+	NL80211_RATE_INFO_EHT_GI_0_8,
+	NL80211_RATE_INFO_EHT_GI_1_6,
+	NL80211_RATE_INFO_EHT_GI_3_2,
+};
+
+/**
+ * enum nl80211_eht_ru_alloc - EHT RU allocation values
+ * @NL80211_RATE_INFO_EHT_RU_ALLOC_26: 26-tone RU allocation
+ * @NL80211_RATE_INFO_EHT_RU_ALLOC_52: 52-tone RU allocation
+ * @NL80211_RATE_INFO_EHT_RU_ALLOC_52P26: 52+26-tone RU allocation
+ * @NL80211_RATE_INFO_EHT_RU_ALLOC_106: 106-tone RU allocation
+ * @NL80211_RATE_INFO_EHT_RU_ALLOC_106P26: 106+26 tone RU allocation
+ * @NL80211_RATE_INFO_EHT_RU_ALLOC_242: 242-tone RU allocation
+ * @NL80211_RATE_INFO_EHT_RU_ALLOC_484: 484-tone RU allocation
+ * @NL80211_RATE_INFO_EHT_RU_ALLOC_484P242: 484+242 tone RU allocation
+ * @NL80211_RATE_INFO_EHT_RU_ALLOC_996: 996-tone RU allocation
+ * @NL80211_RATE_INFO_EHT_RU_ALLOC_996P484: 996+484 tone RU allocation
+ * @NL80211_RATE_INFO_EHT_RU_ALLOC_996P484P242: 996+484+242 tone RU allocation
+ * @NL80211_RATE_INFO_EHT_RU_ALLOC_2x996: 2x996-tone RU allocation
+ * @NL80211_RATE_INFO_EHT_RU_ALLOC_2x996P484: 2x996+484 tone RU allocation
+ * @NL80211_RATE_INFO_EHT_RU_ALLOC_3x996: 3x996-tone RU allocation
+ * @NL80211_RATE_INFO_EHT_RU_ALLOC_3x996P484: 3x996+484 tone RU allocation
+ * @NL80211_RATE_INFO_EHT_RU_ALLOC_4x996: 4x996-tone RU allocation
+ */
+enum nl80211_eht_ru_alloc {
+	NL80211_RATE_INFO_EHT_RU_ALLOC_26,
+	NL80211_RATE_INFO_EHT_RU_ALLOC_52,
+	NL80211_RATE_INFO_EHT_RU_ALLOC_52P26,
+	NL80211_RATE_INFO_EHT_RU_ALLOC_106,
+	NL80211_RATE_INFO_EHT_RU_ALLOC_106P26,
+	NL80211_RATE_INFO_EHT_RU_ALLOC_242,
+	NL80211_RATE_INFO_EHT_RU_ALLOC_484,
+	NL80211_RATE_INFO_EHT_RU_ALLOC_484P242,
+	NL80211_RATE_INFO_EHT_RU_ALLOC_996,
+	NL80211_RATE_INFO_EHT_RU_ALLOC_996P484,
+	NL80211_RATE_INFO_EHT_RU_ALLOC_996P484P242,
+	NL80211_RATE_INFO_EHT_RU_ALLOC_2x996,
+	NL80211_RATE_INFO_EHT_RU_ALLOC_2x996P484,
+	NL80211_RATE_INFO_EHT_RU_ALLOC_3x996,
+	NL80211_RATE_INFO_EHT_RU_ALLOC_3x996P484,
+	NL80211_RATE_INFO_EHT_RU_ALLOC_4x996,
+};
+
+/**
  * enum nl80211_rate_info - bitrate information
  *
  * These attribute types are used with %NL80211_STA_INFO_TXRATE
@@ -3319,6 +3596,13 @@ enum nl80211_he_ru_alloc {
  * @NL80211_RATE_INFO_HE_DCM: HE DCM value (u8, 0/1)
  * @NL80211_RATE_INFO_RU_ALLOC: HE RU allocation, if not present then
  *	non-OFDMA was used (u8, see &enum nl80211_he_ru_alloc)
+ * @NL80211_RATE_INFO_320_MHZ_WIDTH: 320 MHz bitrate
+ * @NL80211_RATE_INFO_EHT_MCS: EHT MCS index (u8, 0-15)
+ * @NL80211_RATE_INFO_EHT_NSS: EHT NSS value (u8, 1-8)
+ * @NL80211_RATE_INFO_EHT_GI: EHT guard interval identifier
+ *	(u8, see &enum nl80211_eht_gi)
+ * @NL80211_RATE_INFO_EHT_RU_ALLOC: EHT RU allocation, if not present then
+ *	non-OFDMA was used (u8, see &enum nl80211_eht_ru_alloc)
  * @__NL80211_RATE_INFO_AFTER_LAST: internal use
  */
 enum nl80211_rate_info {
@@ -3340,6 +3624,11 @@ enum nl80211_rate_info {
 	NL80211_RATE_INFO_HE_GI,
 	NL80211_RATE_INFO_HE_DCM,
 	NL80211_RATE_INFO_HE_RU_ALLOC,
+	NL80211_RATE_INFO_320_MHZ_WIDTH,
+	NL80211_RATE_INFO_EHT_MCS,
+	NL80211_RATE_INFO_EHT_NSS,
+	NL80211_RATE_INFO_EHT_GI,
+	NL80211_RATE_INFO_EHT_RU_ALLOC,
 
 	/* keep last */
 	__NL80211_RATE_INFO_AFTER_LAST,
@@ -3650,11 +3939,20 @@ enum nl80211_mpath_info {
  *     capabilities IE
  * @NL80211_BAND_IFTYPE_ATTR_HE_CAP_PPE: HE PPE thresholds information as
  *     defined in HE capabilities IE
- * @NL80211_BAND_IFTYPE_ATTR_MAX: highest band HE capability attribute currently
- *     defined
  * @NL80211_BAND_IFTYPE_ATTR_HE_6GHZ_CAPA: HE 6GHz band capabilities (__le16),
  *	given for all 6 GHz band channels
+ * @NL80211_BAND_IFTYPE_ATTR_VENDOR_ELEMS: vendor element capabilities that are
+ *	advertised on this band/for this iftype (binary)
+ * @NL80211_BAND_IFTYPE_ATTR_EHT_CAP_MAC: EHT MAC capabilities as in EHT
+ *	capabilities element
+ * @NL80211_BAND_IFTYPE_ATTR_EHT_CAP_PHY: EHT PHY capabilities as in EHT
+ *	capabilities element
+ * @NL80211_BAND_IFTYPE_ATTR_EHT_CAP_MCS_SET: EHT supported NSS/MCS as in EHT
+ *	capabilities element
+ * @NL80211_BAND_IFTYPE_ATTR_EHT_CAP_PPE: EHT PPE thresholds information as
+ *	defined in EHT capabilities element
  * @__NL80211_BAND_IFTYPE_ATTR_AFTER_LAST: internal use
+ * @NL80211_BAND_IFTYPE_ATTR_MAX: highest band attribute currently defined
  */
 enum nl80211_band_iftype_attr {
 	__NL80211_BAND_IFTYPE_ATTR_INVALID,
@@ -3665,6 +3963,11 @@ enum nl80211_band_iftype_attr {
 	NL80211_BAND_IFTYPE_ATTR_HE_CAP_MCS_SET,
 	NL80211_BAND_IFTYPE_ATTR_HE_CAP_PPE,
 	NL80211_BAND_IFTYPE_ATTR_HE_6GHZ_CAPA,
+	NL80211_BAND_IFTYPE_ATTR_VENDOR_ELEMS,
+	NL80211_BAND_IFTYPE_ATTR_EHT_CAP_MAC,
+	NL80211_BAND_IFTYPE_ATTR_EHT_CAP_PHY,
+	NL80211_BAND_IFTYPE_ATTR_EHT_CAP_MCS_SET,
+	NL80211_BAND_IFTYPE_ATTR_EHT_CAP_PPE,
 
 	/* keep last */
 	__NL80211_BAND_IFTYPE_ATTR_AFTER_LAST,
@@ -3809,6 +4112,10 @@ enum nl80211_wmm_rule {
  *	on this channel in current regulatory domain.
  * @NL80211_FREQUENCY_ATTR_16MHZ: 16 MHz operation is allowed
  *	on this channel in current regulatory domain.
+ * @NL80211_FREQUENCY_ATTR_NO_320MHZ: any 320 MHz channel using this channel
+ *	as the primary or any of the secondary channels isn't possible
+ * @NL80211_FREQUENCY_ATTR_NO_EHT: EHT operation is not allowed on this channel
+ *	in current regulatory domain.
  * @NL80211_FREQUENCY_ATTR_MAX: highest frequency attribute number
  *	currently defined
  * @__NL80211_FREQUENCY_ATTR_AFTER_LAST: internal use
@@ -3845,6 +4152,8 @@ enum nl80211_frequency_attr {
 	NL80211_FREQUENCY_ATTR_4MHZ,
 	NL80211_FREQUENCY_ATTR_8MHZ,
 	NL80211_FREQUENCY_ATTR_16MHZ,
+	NL80211_FREQUENCY_ATTR_NO_320MHZ,
+	NL80211_FREQUENCY_ATTR_NO_EHT,
 
 	/* keep last */
 	__NL80211_FREQUENCY_ATTR_AFTER_LAST,
@@ -4043,6 +4352,7 @@ enum nl80211_sched_scan_match_attr {
  * @NL80211_RRF_NO_80MHZ: 80MHz operation not allowed
  * @NL80211_RRF_NO_160MHZ: 160MHz operation not allowed
  * @NL80211_RRF_NO_HE: HE operation not allowed
+ * @NL80211_RRF_NO_320MHZ: 320MHz operation not allowed
  */
 enum nl80211_reg_rule_flags {
 	NL80211_RRF_NO_OFDM		= 1<<0,
@@ -4061,6 +4371,7 @@ enum nl80211_reg_rule_flags {
 	NL80211_RRF_NO_80MHZ		= 1<<15,
 	NL80211_RRF_NO_160MHZ		= 1<<16,
 	NL80211_RRF_NO_HE		= 1<<17,
+	NL80211_RRF_NO_320MHZ		= 1<<18,
 };
 
 #define NL80211_RRF_PASSIVE_SCAN	NL80211_RRF_NO_IR
@@ -4558,6 +4869,8 @@ enum nl80211_key_mode {
  * @NL80211_CHAN_WIDTH_4: 4 MHz OFDM channel
  * @NL80211_CHAN_WIDTH_8: 8 MHz OFDM channel
  * @NL80211_CHAN_WIDTH_16: 16 MHz OFDM channel
+ * @NL80211_CHAN_WIDTH_320: 320 MHz channel, the %NL80211_ATTR_CENTER_FREQ1
+ *	attribute must be provided as well
  */
 enum nl80211_chan_width {
 	NL80211_CHAN_WIDTH_20_NOHT,
@@ -4573,6 +4886,7 @@ enum nl80211_chan_width {
 	NL80211_CHAN_WIDTH_4,
 	NL80211_CHAN_WIDTH_8,
 	NL80211_CHAN_WIDTH_16,
+	NL80211_CHAN_WIDTH_320,
 };
 
 /**
@@ -4644,6 +4958,8 @@ enum nl80211_bss_scan_width {
  *	Contains a nested array of signal strength attributes (u8, dBm),
  *	using the nesting index as the antenna number.
  * @NL80211_BSS_FREQUENCY_OFFSET: frequency offset in KHz
+ * @NL80211_BSS_MLO_LINK_ID: MLO link ID of the BSS (u8).
+ * @NL80211_BSS_MLD_ADDR: MLD address of this BSS if connected to it.
  * @__NL80211_BSS_AFTER_LAST: internal
  * @NL80211_BSS_MAX: highest BSS attribute
  */
@@ -4669,6 +4985,8 @@ enum nl80211_bss {
 	NL80211_BSS_PARENT_BSSID,
 	NL80211_BSS_CHAIN_SIGNAL,
 	NL80211_BSS_FREQUENCY_OFFSET,
+	NL80211_BSS_MLO_LINK_ID,
+	NL80211_BSS_MLD_ADDR,
 
 	/* keep last */
 	__NL80211_BSS_AFTER_LAST,
@@ -4887,6 +5205,7 @@ enum nl80211_txrate_gi {
  * @NL80211_BAND_60GHZ: around 60 GHz band (58.32 - 69.12 GHz)
  * @NL80211_BAND_6GHZ: around 6 GHz band (5.9 - 7.2 GHz)
  * @NL80211_BAND_S1GHZ: around 900MHz, supported by S1G PHYs
+ * @NL80211_BAND_LC: light communication band (placeholder)
  * @NUM_NL80211_BANDS: number of bands, avoid using this in userspace
  *	since newer kernel versions may support more bands
  */
@@ -4896,6 +5215,7 @@ enum nl80211_band {
 	NL80211_BAND_60GHZ,
 	NL80211_BAND_6GHZ,
 	NL80211_BAND_S1GHZ,
+	NL80211_BAND_LC,
 
 	NUM_NL80211_BANDS,
 };
@@ -5462,7 +5782,7 @@ enum nl80211_iface_limit_attrs {
  *	=> allows 8 of AP/GO that can have BI gcd >= min gcd
  *
  *	numbers = [ #{STA} <= 2 ], channels = 2, max = 2
- *	=> allows two STAs on different channels
+ *	=> allows two STAs on the same or on different channels
  *
  *	numbers = [ #{STA} <= 1, #{P2P-client,P2P-GO} <= 3 ], max = 4
  *	=> allows a STA plus three P2P interfaces
@@ -5507,7 +5827,7 @@ enum nl80211_if_combination_attrs {
  * @NL80211_PLINK_ESTAB: mesh peer link is established
  * @NL80211_PLINK_HOLDING: mesh peer link is being closed or cancelled
  * @NL80211_PLINK_BLOCKED: all frames transmitted from this mesh
- *	plink are discarded
+ *	plink are discarded, except for authentication frames
  * @NUM_NL80211_PLINK_STATES: number of peer link states
  * @MAX_NL80211_PLINK_STATES: highest numerical value of plink states
  */
@@ -5644,13 +5964,15 @@ enum nl80211_tdls_operation {
 	NL80211_TDLS_DISABLE_LINK,
 };
 
-/*
+/**
  * enum nl80211_ap_sme_features - device-integrated AP features
- * Reserved for future use, no bits are defined in
- * NL80211_ATTR_DEVICE_AP_SME yet.
+ * @NL80211_AP_SME_SA_QUERY_OFFLOAD: SA Query procedures offloaded to driver
+ *	when user space indicates support for SA Query procedures offload during
+ *	"start ap" with %NL80211_AP_SETTINGS_SA_QUERY_OFFLOAD_SUPPORT.
+ */
 enum nl80211_ap_sme_features {
+	NL80211_AP_SME_SA_QUERY_OFFLOAD		= 1 << 0,
 };
- */
 
 /**
  * enum nl80211_feature_flags - device/driver features
@@ -5661,7 +5983,7 @@ enum nl80211_ap_sme_features {
  * @NL80211_FEATURE_INACTIVITY_TIMER: This driver takes care of freeing up
  *	the connected inactive stations in AP mode.
  * @NL80211_FEATURE_CELL_BASE_REG_HINTS: This driver has been tested
- *	to work properly to suppport receiving regulatory hints from
+ *	to work properly to support receiving regulatory hints from
  *	cellular base stations.
  * @NL80211_FEATURE_P2P_DEVICE_NEEDS_CHANNEL: (no longer available, only
  *	here to reserve the value for API/ABI compatibility)
@@ -5950,6 +6272,25 @@ enum nl80211_feature_flags {
  *      frame protection for all management frames exchanged during the
  *      negotiation and range measurement procedure.
  *
+ * @NL80211_EXT_FEATURE_BSS_COLOR: The driver supports BSS color collision
+ *	detection and change announcemnts.
+ *
+ * @NL80211_EXT_FEATURE_FILS_CRYPTO_OFFLOAD: Driver running in AP mode supports
+ *	FILS encryption and decryption for (Re)Association Request and Response
+ *	frames. Userspace has to share FILS AAD details to the driver by using
+ *	@NL80211_CMD_SET_FILS_AAD.
+ *
+ * @NL80211_EXT_FEATURE_RADAR_BACKGROUND: Device supports background radar/CAC
+ *	detection.
+ *
+ * @NL80211_EXT_FEATURE_POWERED_ADDR_CHANGE: Device can perform a MAC address
+ *	change without having to bring the underlying network device down
+ *	first. For example, in station mode this can be used to vary the
+ *	origin MAC address prior to a connection to a new AP for privacy
+ *	or other reasons. Note that certain driver specific restrictions
+ *	might apply, e.g. no scans in progress, no offchannel operations
+ *	in progress, and no active connections.
+ *
  * @NUM_NL80211_EXT_FEATURES: number of extended features.
  * @MAX_NL80211_EXT_FEATURES: highest extended feature index.
  */
@@ -6014,6 +6355,10 @@ enum nl80211_ext_feature_index {
 	NL80211_EXT_FEATURE_SECURE_LTF,
 	NL80211_EXT_FEATURE_SECURE_RTT,
 	NL80211_EXT_FEATURE_PROT_RANGE_NEGO_AND_MEASURE,
+	NL80211_EXT_FEATURE_BSS_COLOR,
+	NL80211_EXT_FEATURE_FILS_CRYPTO_OFFLOAD,
+	NL80211_EXT_FEATURE_RADAR_BACKGROUND,
+	NL80211_EXT_FEATURE_POWERED_ADDR_CHANGE,
 
 	/* add new features before the definition below */
 	NUM_NL80211_EXT_FEATURES,
@@ -6912,6 +7257,9 @@ enum nl80211_peer_measurement_ftm_capa {
  * @NL80211_PMSR_FTM_REQ_ATTR_LMR_FEEDBACK: negotiate for LMR feedback. Only
  *	valid if either %NL80211_PMSR_FTM_REQ_ATTR_TRIGGER_BASED or
  *	%NL80211_PMSR_FTM_REQ_ATTR_NON_TRIGGER_BASED is set.
+ * @NL80211_PMSR_FTM_REQ_ATTR_BSS_COLOR: optional. The BSS color of the
+ *	responder. Only valid if %NL80211_PMSR_FTM_REQ_ATTR_NON_TRIGGER_BASED
+ *	or %NL80211_PMSR_FTM_REQ_ATTR_TRIGGER_BASED is set.
  *
  * @NUM_NL80211_PMSR_FTM_REQ_ATTR: internal
  * @NL80211_PMSR_FTM_REQ_ATTR_MAX: highest attribute number
@@ -6931,6 +7279,7 @@ enum nl80211_peer_measurement_ftm_req {
 	NL80211_PMSR_FTM_REQ_ATTR_TRIGGER_BASED,
 	NL80211_PMSR_FTM_REQ_ATTR_NON_TRIGGER_BASED,
 	NL80211_PMSR_FTM_REQ_ATTR_LMR_FEEDBACK,
+	NL80211_PMSR_FTM_REQ_ATTR_BSS_COLOR,
 
 	/* keep last */
 	NUM_NL80211_PMSR_FTM_REQ_ATTR,
@@ -7299,4 +7648,76 @@ enum nl80211_sar_specs_attrs {
 	NL80211_SAR_ATTR_SPECS_MAX = __NL80211_SAR_ATTR_SPECS_LAST - 1,
 };
 
+/**
+ * enum nl80211_mbssid_config_attributes - multiple BSSID (MBSSID) and enhanced
+ * multi-BSSID advertisements (EMA) in AP mode.
+ * Kernel uses some of these attributes to advertise driver's support for
+ * MBSSID and EMA.
+ * Remaining attributes should be used by the userspace to configure the
+ * features.
+ *
+ * @__NL80211_MBSSID_CONFIG_ATTR_INVALID: Invalid
+ *
+ * @NL80211_MBSSID_CONFIG_ATTR_MAX_INTERFACES: Used by the kernel to advertise
+ *	the maximum number of MBSSID interfaces supported by the driver.
+ *	Driver should indicate MBSSID support by setting
+ *	wiphy->mbssid_max_interfaces to a value more than or equal to 2.
+ *
+ * @NL80211_MBSSID_CONFIG_ATTR_MAX_EMA_PROFILE_PERIODICITY: Used by the kernel
+ *	to advertise the maximum profile periodicity supported by the driver
+ *	if EMA is enabled. Driver should indicate EMA support to the userspace
+ *	by setting wiphy->ema_max_profile_periodicity to
+ *	a non-zero value.
+ *
+ * @NL80211_MBSSID_CONFIG_ATTR_INDEX: Mandatory parameter to pass the index of
+ *	this BSS (u8) in the multiple BSSID set.
+ *	Value must be set to 0 for the transmitting interface and non-zero for
+ *	all non-transmitting interfaces. The userspace will be responsible
+ *	for using unique indices for the interfaces.
+ *	Range: 0 to wiphy->mbssid_max_interfaces-1.
+ *
+ * @NL80211_MBSSID_CONFIG_ATTR_TX_IFINDEX: Mandatory parameter for
+ *	a non-transmitted profile which provides the interface index (u32) of
+ *	the transmitted profile. The value must match one of the interface
+ *	indices advertised by the kernel. Optional if the interface being set up
+ *	is the transmitting one, however, if provided then the value must match
+ *	the interface index of the same.
+ *
+ * @NL80211_MBSSID_CONFIG_ATTR_EMA: Flag used to enable EMA AP feature.
+ *	Setting this flag is permitted only if the driver advertises EMA support
+ *	by setting wiphy->ema_max_profile_periodicity to non-zero.
+ *
+ * @__NL80211_MBSSID_CONFIG_ATTR_LAST: Internal
+ * @NL80211_MBSSID_CONFIG_ATTR_MAX: highest attribute
+ */
+enum nl80211_mbssid_config_attributes {
+	__NL80211_MBSSID_CONFIG_ATTR_INVALID,
+
+	NL80211_MBSSID_CONFIG_ATTR_MAX_INTERFACES,
+	NL80211_MBSSID_CONFIG_ATTR_MAX_EMA_PROFILE_PERIODICITY,
+	NL80211_MBSSID_CONFIG_ATTR_INDEX,
+	NL80211_MBSSID_CONFIG_ATTR_TX_IFINDEX,
+	NL80211_MBSSID_CONFIG_ATTR_EMA,
+
+	/* keep last */
+	__NL80211_MBSSID_CONFIG_ATTR_LAST,
+	NL80211_MBSSID_CONFIG_ATTR_MAX = __NL80211_MBSSID_CONFIG_ATTR_LAST - 1,
+};
+
+/**
+ * enum nl80211_ap_settings_flags - AP settings flags
+ *
+ * @NL80211_AP_SETTINGS_EXTERNAL_AUTH_SUPPORT: AP supports external
+ *	authentication.
+ * @NL80211_AP_SETTINGS_SA_QUERY_OFFLOAD_SUPPORT: Userspace supports SA Query
+ *	procedures offload to driver. If driver advertises
+ *	%NL80211_AP_SME_SA_QUERY_OFFLOAD in AP SME features, userspace shall
+ *	ignore SA Query procedures and validations when this flag is set by
+ *	userspace.
+ */
+enum nl80211_ap_settings_flags {
+	NL80211_AP_SETTINGS_EXTERNAL_AUTH_SUPPORT	= 1 << 0,
+	NL80211_AP_SETTINGS_SA_QUERY_OFFLOAD_SUPPORT	= 1 << 1,
+};
+
 #endif /* __LINUX_NL80211_H */
diff -pruN 1.30-1/Makefile.am 2.3-1/Makefile.am
--- 1.30-1/Makefile.am	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/Makefile.am	2022-11-18 12:31:49.000000000 +0000
@@ -62,7 +62,8 @@ ell_headers = ell/util.h \
 			ell/icmp6.h \
 			ell/dhcp6.h \
 			ell/acd.h \
-			ell/cleanup.h
+			ell/cleanup.h \
+			ell/netconfig.h
 
 ell_sources = ell/private.h \
 			ell/missing.h \
@@ -141,7 +142,8 @@ ell_sources = ell/private.h \
 			ell/icmp6-private.h \
 			ell/dhcp6-lease.c \
 			ell/dhcp6-transport.c \
-			ell/acd.c
+			ell/acd.c \
+			ell/netconfig.c
 
 ell_shared = ell/useful.h ell/asn1-private.h
 
@@ -235,7 +237,8 @@ src_iwd_SOURCES = src/main.c linux/nl802
 					src/anqp.h src/anqp.c \
 					src/anqputil.h src/anqputil.c \
 					src/netconfig.h src/netconfig.c\
-					src/resolve.h src/resolve.c\
+					src/netconfig-commit.c \
+					src/resolve.h src/resolve.c \
 					src/hotspot.c \
 					src/p2p.h src/p2p.c \
 					src/p2putil.h src/p2putil.c \
@@ -585,8 +588,7 @@ EXTRA_DIST = src/genbuiltin src/iwd.serv
 
 AM_CFLAGS = $(ell_cflags) -fvisibility=hidden \
 				-DUNITDIR=\""$(top_srcdir)/unit/"\" \
-				-DCERTDIR=\""$(top_builddir)/unit/"\" \
-				-DJSMN_PARENT_LINKS -DJSMN_STRICT
+				-DCERTDIR=\""$(top_builddir)/unit/"\"
 
 if MAINTAINER_MODE
 AM_CFLAGS += -DHAVE_PKCS8_SUPPORT
diff -pruN 1.30-1/Makefile.in 2.3-1/Makefile.in
--- 1.30-1/Makefile.in	2022-09-07 18:55:39.000000000 +0000
+++ 2.3-1/Makefile.in	2023-02-02 12:59:50.000000000 +0000
@@ -178,16 +178,16 @@ am__ell_libell_internal_la_SOURCES_DIST
 	ell/random.h ell/uintset.h ell/base64.h ell/pem.h ell/tls.h \
 	ell/uuid.h ell/key.h ell/file.h ell/dir.h ell/net.h ell/dhcp.h \
 	ell/cert.h ell/ecc.h ell/ecdh.h ell/time.h ell/path.h \
-	ell/icmp6.h ell/dhcp6.h ell/acd.h ell/cleanup.h ell/private.h \
-	ell/missing.h ell/util.c ell/test.c ell/strv.c ell/utf8.c \
-	ell/queue.c ell/hashmap.c ell/string.c ell/settings.c \
-	ell/main-private.h ell/main.c ell/idle.c ell/signal.c \
-	ell/timeout.c ell/io.c ell/ringbuf.c ell/log.c ell/checksum.c \
-	ell/netlink-private.h ell/netlink.c ell/genl.c \
-	ell/rtnl-private.h ell/rtnl.c ell/dbus-private.h ell/dbus.c \
-	ell/dbus-message.c ell/dbus-util.c ell/dbus-service.c \
-	ell/dbus-client.c ell/dbus-name-cache.c ell/dbus-filter.c \
-	ell/gvariant-private.h ell/gvariant-util.c \
+	ell/icmp6.h ell/dhcp6.h ell/acd.h ell/cleanup.h \
+	ell/netconfig.h ell/private.h ell/missing.h ell/util.c \
+	ell/test.c ell/strv.c ell/utf8.c ell/queue.c ell/hashmap.c \
+	ell/string.c ell/settings.c ell/main-private.h ell/main.c \
+	ell/idle.c ell/signal.c ell/timeout.c ell/io.c ell/ringbuf.c \
+	ell/log.c ell/checksum.c ell/netlink-private.h ell/netlink.c \
+	ell/genl.c ell/rtnl-private.h ell/rtnl.c ell/dbus-private.h \
+	ell/dbus.c ell/dbus-message.c ell/dbus-util.c \
+	ell/dbus-service.c ell/dbus-client.c ell/dbus-name-cache.c \
+	ell/dbus-filter.c ell/gvariant-private.h ell/gvariant-util.c \
 	ell/siphash-private.h ell/siphash.c ell/hwdb.c ell/cipher.c \
 	ell/random.c ell/uintset.c ell/base64.c ell/asn1-private.h \
 	ell/pem.c ell/pem-private.h ell/tls-private.h ell/tls.c \
@@ -199,7 +199,8 @@ am__ell_libell_internal_la_SOURCES_DIST
 	ell/ecc-external.c ell/ecc-private.h ell/ecc.c ell/ecdh.c \
 	ell/time.c ell/time-private.h ell/path.c ell/dhcp6.c \
 	ell/dhcp6-private.h ell/icmp6.c ell/icmp6-private.h \
-	ell/dhcp6-lease.c ell/dhcp6-transport.c ell/acd.c ell/useful.h
+	ell/dhcp6-lease.c ell/dhcp6-transport.c ell/acd.c \
+	ell/netconfig.c ell/useful.h
 am__objects_1 =
 am__dirstamp = $(am__leading_dot)dirstamp
 @EXTERNAL_ELL_FALSE@am__objects_2 = ell/util.lo ell/test.lo \
@@ -225,7 +226,8 @@ am__dirstamp = $(am__leading_dot)dirstam
 @EXTERNAL_ELL_FALSE@	ell/ecc-external.lo ell/ecc.lo ell/ecdh.lo \
 @EXTERNAL_ELL_FALSE@	ell/time.lo ell/path.lo ell/dhcp6.lo \
 @EXTERNAL_ELL_FALSE@	ell/icmp6.lo ell/dhcp6-lease.lo \
-@EXTERNAL_ELL_FALSE@	ell/dhcp6-transport.lo ell/acd.lo
+@EXTERNAL_ELL_FALSE@	ell/dhcp6-transport.lo ell/acd.lo \
+@EXTERNAL_ELL_FALSE@	ell/netconfig.lo
 @EXTERNAL_ELL_FALSE@am_ell_libell_internal_la_OBJECTS =  \
 @EXTERNAL_ELL_FALSE@	$(am__objects_1) $(am__objects_2) \
 @EXTERNAL_ELL_FALSE@	$(am__objects_1)
@@ -300,21 +302,22 @@ am__src_iwd_SOURCES_DIST = src/main.c li
 	src/blacklist.h src/blacklist.c src/manager.c src/erp.h \
 	src/erp.c src/fils.h src/fils.c src/auth-proto.h src/anqp.h \
 	src/anqp.c src/anqputil.h src/anqputil.c src/netconfig.h \
-	src/netconfig.c src/resolve.h src/resolve.c src/hotspot.c \
-	src/p2p.h src/p2p.c src/p2putil.h src/p2putil.c src/module.h \
-	src/module.c src/rrm.c src/frame-xchg.h src/frame-xchg.c \
-	src/eap-wsc.c src/eap-wsc.h src/wscutil.h src/wscutil.c \
-	src/diagnostic.h src/diagnostic.c src/ip-pool.h src/ip-pool.c \
-	src/band.h src/band.c src/sysfs.h src/sysfs.c src/offchannel.h \
-	src/offchannel.c src/dpp-util.h src/dpp-util.c src/json.h \
-	src/json.c src/dpp.c src/eap.c src/eap.h src/eap-private.h \
-	src/eap-md5.c src/eap-tls.c src/eap-ttls.c src/eap-mschapv2.c \
-	src/eap-mschapv2.h src/eap-sim.c src/eap-aka.c src/eap-peap.c \
-	src/eap-gtc.c src/eap-pwd.c src/util.h src/util.c src/crypto.h \
-	src/crypto.c src/simutil.h src/simutil.c src/simauth.h \
-	src/simauth.c src/watchlist.h src/watchlist.c \
-	src/eap-tls-common.h src/eap-tls-common.c src/mschaputil.h \
-	src/mschaputil.c src/ofono.c
+	src/netconfig.c src/netconfig-commit.c src/resolve.h \
+	src/resolve.c src/hotspot.c src/p2p.h src/p2p.c src/p2putil.h \
+	src/p2putil.c src/module.h src/module.c src/rrm.c \
+	src/frame-xchg.h src/frame-xchg.c src/eap-wsc.c src/eap-wsc.h \
+	src/wscutil.h src/wscutil.c src/diagnostic.h src/diagnostic.c \
+	src/ip-pool.h src/ip-pool.c src/band.h src/band.c src/sysfs.h \
+	src/sysfs.c src/offchannel.h src/offchannel.c src/dpp-util.h \
+	src/dpp-util.c src/json.h src/json.c src/dpp.c src/eap.c \
+	src/eap.h src/eap-private.h src/eap-md5.c src/eap-tls.c \
+	src/eap-ttls.c src/eap-mschapv2.c src/eap-mschapv2.h \
+	src/eap-sim.c src/eap-aka.c src/eap-peap.c src/eap-gtc.c \
+	src/eap-pwd.c src/util.h src/util.c src/crypto.h src/crypto.c \
+	src/simutil.h src/simutil.c src/simauth.h src/simauth.c \
+	src/watchlist.h src/watchlist.c src/eap-tls-common.h \
+	src/eap-tls-common.c src/mschaputil.h src/mschaputil.c \
+	src/ofono.c
 am__objects_3 = src/eap.$(OBJEXT) src/eap-md5.$(OBJEXT) \
 	src/eap-tls.$(OBJEXT) src/eap-ttls.$(OBJEXT) \
 	src/eap-mschapv2.$(OBJEXT) src/eap-sim.$(OBJEXT) \
@@ -343,16 +346,18 @@ am__objects_5 = $(am__objects_4)
 @DAEMON_TRUE@	src/blacklist.$(OBJEXT) src/manager.$(OBJEXT) \
 @DAEMON_TRUE@	src/erp.$(OBJEXT) src/fils.$(OBJEXT) \
 @DAEMON_TRUE@	src/anqp.$(OBJEXT) src/anqputil.$(OBJEXT) \
-@DAEMON_TRUE@	src/netconfig.$(OBJEXT) src/resolve.$(OBJEXT) \
-@DAEMON_TRUE@	src/hotspot.$(OBJEXT) src/p2p.$(OBJEXT) \
-@DAEMON_TRUE@	src/p2putil.$(OBJEXT) src/module.$(OBJEXT) \
-@DAEMON_TRUE@	src/rrm.$(OBJEXT) src/frame-xchg.$(OBJEXT) \
-@DAEMON_TRUE@	src/eap-wsc.$(OBJEXT) src/wscutil.$(OBJEXT) \
-@DAEMON_TRUE@	src/diagnostic.$(OBJEXT) src/ip-pool.$(OBJEXT) \
-@DAEMON_TRUE@	src/band.$(OBJEXT) src/sysfs.$(OBJEXT) \
-@DAEMON_TRUE@	src/offchannel.$(OBJEXT) src/dpp-util.$(OBJEXT) \
-@DAEMON_TRUE@	src/json.$(OBJEXT) src/dpp.$(OBJEXT) \
-@DAEMON_TRUE@	$(am__objects_3) $(am__objects_5)
+@DAEMON_TRUE@	src/netconfig.$(OBJEXT) \
+@DAEMON_TRUE@	src/netconfig-commit.$(OBJEXT) \
+@DAEMON_TRUE@	src/resolve.$(OBJEXT) src/hotspot.$(OBJEXT) \
+@DAEMON_TRUE@	src/p2p.$(OBJEXT) src/p2putil.$(OBJEXT) \
+@DAEMON_TRUE@	src/module.$(OBJEXT) src/rrm.$(OBJEXT) \
+@DAEMON_TRUE@	src/frame-xchg.$(OBJEXT) src/eap-wsc.$(OBJEXT) \
+@DAEMON_TRUE@	src/wscutil.$(OBJEXT) src/diagnostic.$(OBJEXT) \
+@DAEMON_TRUE@	src/ip-pool.$(OBJEXT) src/band.$(OBJEXT) \
+@DAEMON_TRUE@	src/sysfs.$(OBJEXT) src/offchannel.$(OBJEXT) \
+@DAEMON_TRUE@	src/dpp-util.$(OBJEXT) src/json.$(OBJEXT) \
+@DAEMON_TRUE@	src/dpp.$(OBJEXT) $(am__objects_3) \
+@DAEMON_TRUE@	$(am__objects_5)
 src_iwd_OBJECTS = $(am_src_iwd_OBJECTS)
 am__tools_hwsim_SOURCES_DIST = tools/hwsim.c src/mpdu.h src/util.h \
 	src/util.c src/nl80211cmd.h src/nl80211cmd.c src/nl80211util.h \
@@ -559,42 +564,43 @@ am__depfiles_remade = client/$(DEPDIR)/a
 	ell/$(DEPDIR)/idle.Plo ell/$(DEPDIR)/io.Plo \
 	ell/$(DEPDIR)/key.Plo ell/$(DEPDIR)/log.Plo \
 	ell/$(DEPDIR)/main.Plo ell/$(DEPDIR)/net.Plo \
-	ell/$(DEPDIR)/netlink.Plo ell/$(DEPDIR)/path.Plo \
-	ell/$(DEPDIR)/pem.Plo ell/$(DEPDIR)/queue.Plo \
-	ell/$(DEPDIR)/random.Plo ell/$(DEPDIR)/ringbuf.Plo \
-	ell/$(DEPDIR)/rtnl.Plo ell/$(DEPDIR)/settings.Plo \
-	ell/$(DEPDIR)/signal.Plo ell/$(DEPDIR)/siphash.Plo \
-	ell/$(DEPDIR)/string.Plo ell/$(DEPDIR)/strv.Plo \
-	ell/$(DEPDIR)/test.Plo ell/$(DEPDIR)/time.Plo \
-	ell/$(DEPDIR)/timeout.Plo ell/$(DEPDIR)/tls-extensions.Plo \
-	ell/$(DEPDIR)/tls-record.Plo ell/$(DEPDIR)/tls-suites.Plo \
-	ell/$(DEPDIR)/tls.Plo ell/$(DEPDIR)/uintset.Plo \
-	ell/$(DEPDIR)/utf8.Plo ell/$(DEPDIR)/util.Plo \
-	ell/$(DEPDIR)/uuid.Plo monitor/$(DEPDIR)/display.Po \
-	monitor/$(DEPDIR)/main.Po monitor/$(DEPDIR)/nlmon.Po \
-	monitor/$(DEPDIR)/pcap.Po src/$(DEPDIR)/adhoc.Po \
-	src/$(DEPDIR)/agent.Po src/$(DEPDIR)/anqp.Po \
-	src/$(DEPDIR)/anqputil.Po src/$(DEPDIR)/ap.Po \
-	src/$(DEPDIR)/backtrace.Po src/$(DEPDIR)/band.Po \
-	src/$(DEPDIR)/blacklist.Po src/$(DEPDIR)/common.Po \
-	src/$(DEPDIR)/crypto.Po src/$(DEPDIR)/dbus.Po \
-	src/$(DEPDIR)/device.Po src/$(DEPDIR)/diagnostic.Po \
-	src/$(DEPDIR)/dpp-util.Po src/$(DEPDIR)/dpp.Po \
-	src/$(DEPDIR)/eap-aka.Po src/$(DEPDIR)/eap-gtc.Po \
-	src/$(DEPDIR)/eap-md5.Po src/$(DEPDIR)/eap-mschapv2.Po \
-	src/$(DEPDIR)/eap-peap.Po src/$(DEPDIR)/eap-pwd.Po \
-	src/$(DEPDIR)/eap-sim.Po src/$(DEPDIR)/eap-tls-common.Po \
-	src/$(DEPDIR)/eap-tls.Po src/$(DEPDIR)/eap-ttls.Po \
-	src/$(DEPDIR)/eap-wsc.Po src/$(DEPDIR)/eap.Po \
-	src/$(DEPDIR)/eapol.Po src/$(DEPDIR)/eapolutil.Po \
-	src/$(DEPDIR)/erp.Po src/$(DEPDIR)/fils.Po \
-	src/$(DEPDIR)/frame-xchg.Po src/$(DEPDIR)/ft.Po \
-	src/$(DEPDIR)/handshake.Po src/$(DEPDIR)/hotspot.Po \
-	src/$(DEPDIR)/ie.Po src/$(DEPDIR)/ip-pool.Po \
-	src/$(DEPDIR)/json.Po src/$(DEPDIR)/knownnetworks.Po \
-	src/$(DEPDIR)/main.Po src/$(DEPDIR)/manager.Po \
-	src/$(DEPDIR)/module.Po src/$(DEPDIR)/mpdu.Po \
-	src/$(DEPDIR)/mschaputil.Po src/$(DEPDIR)/netconfig.Po \
+	ell/$(DEPDIR)/netconfig.Plo ell/$(DEPDIR)/netlink.Plo \
+	ell/$(DEPDIR)/path.Plo ell/$(DEPDIR)/pem.Plo \
+	ell/$(DEPDIR)/queue.Plo ell/$(DEPDIR)/random.Plo \
+	ell/$(DEPDIR)/ringbuf.Plo ell/$(DEPDIR)/rtnl.Plo \
+	ell/$(DEPDIR)/settings.Plo ell/$(DEPDIR)/signal.Plo \
+	ell/$(DEPDIR)/siphash.Plo ell/$(DEPDIR)/string.Plo \
+	ell/$(DEPDIR)/strv.Plo ell/$(DEPDIR)/test.Plo \
+	ell/$(DEPDIR)/time.Plo ell/$(DEPDIR)/timeout.Plo \
+	ell/$(DEPDIR)/tls-extensions.Plo ell/$(DEPDIR)/tls-record.Plo \
+	ell/$(DEPDIR)/tls-suites.Plo ell/$(DEPDIR)/tls.Plo \
+	ell/$(DEPDIR)/uintset.Plo ell/$(DEPDIR)/utf8.Plo \
+	ell/$(DEPDIR)/util.Plo ell/$(DEPDIR)/uuid.Plo \
+	monitor/$(DEPDIR)/display.Po monitor/$(DEPDIR)/main.Po \
+	monitor/$(DEPDIR)/nlmon.Po monitor/$(DEPDIR)/pcap.Po \
+	src/$(DEPDIR)/adhoc.Po src/$(DEPDIR)/agent.Po \
+	src/$(DEPDIR)/anqp.Po src/$(DEPDIR)/anqputil.Po \
+	src/$(DEPDIR)/ap.Po src/$(DEPDIR)/backtrace.Po \
+	src/$(DEPDIR)/band.Po src/$(DEPDIR)/blacklist.Po \
+	src/$(DEPDIR)/common.Po src/$(DEPDIR)/crypto.Po \
+	src/$(DEPDIR)/dbus.Po src/$(DEPDIR)/device.Po \
+	src/$(DEPDIR)/diagnostic.Po src/$(DEPDIR)/dpp-util.Po \
+	src/$(DEPDIR)/dpp.Po src/$(DEPDIR)/eap-aka.Po \
+	src/$(DEPDIR)/eap-gtc.Po src/$(DEPDIR)/eap-md5.Po \
+	src/$(DEPDIR)/eap-mschapv2.Po src/$(DEPDIR)/eap-peap.Po \
+	src/$(DEPDIR)/eap-pwd.Po src/$(DEPDIR)/eap-sim.Po \
+	src/$(DEPDIR)/eap-tls-common.Po src/$(DEPDIR)/eap-tls.Po \
+	src/$(DEPDIR)/eap-ttls.Po src/$(DEPDIR)/eap-wsc.Po \
+	src/$(DEPDIR)/eap.Po src/$(DEPDIR)/eapol.Po \
+	src/$(DEPDIR)/eapolutil.Po src/$(DEPDIR)/erp.Po \
+	src/$(DEPDIR)/fils.Po src/$(DEPDIR)/frame-xchg.Po \
+	src/$(DEPDIR)/ft.Po src/$(DEPDIR)/handshake.Po \
+	src/$(DEPDIR)/hotspot.Po src/$(DEPDIR)/ie.Po \
+	src/$(DEPDIR)/ip-pool.Po src/$(DEPDIR)/json.Po \
+	src/$(DEPDIR)/knownnetworks.Po src/$(DEPDIR)/main.Po \
+	src/$(DEPDIR)/manager.Po src/$(DEPDIR)/module.Po \
+	src/$(DEPDIR)/mpdu.Po src/$(DEPDIR)/mschaputil.Po \
+	src/$(DEPDIR)/netconfig-commit.Po src/$(DEPDIR)/netconfig.Po \
 	src/$(DEPDIR)/netdev.Po src/$(DEPDIR)/network.Po \
 	src/$(DEPDIR)/nl80211cmd.Po src/$(DEPDIR)/nl80211util.Po \
 	src/$(DEPDIR)/offchannel.Po src/$(DEPDIR)/ofono.Po \
@@ -1135,7 +1141,8 @@ builtin_sources = $(am__append_3)
 @EXTERNAL_ELL_FALSE@			ell/icmp6.h \
 @EXTERNAL_ELL_FALSE@			ell/dhcp6.h \
 @EXTERNAL_ELL_FALSE@			ell/acd.h \
-@EXTERNAL_ELL_FALSE@			ell/cleanup.h
+@EXTERNAL_ELL_FALSE@			ell/cleanup.h \
+@EXTERNAL_ELL_FALSE@			ell/netconfig.h
 
 @EXTERNAL_ELL_FALSE@ell_sources = ell/private.h \
 @EXTERNAL_ELL_FALSE@			ell/missing.h \
@@ -1214,7 +1221,8 @@ builtin_sources = $(am__append_3)
 @EXTERNAL_ELL_FALSE@			ell/icmp6-private.h \
 @EXTERNAL_ELL_FALSE@			ell/dhcp6-lease.c \
 @EXTERNAL_ELL_FALSE@			ell/dhcp6-transport.c \
-@EXTERNAL_ELL_FALSE@			ell/acd.c
+@EXTERNAL_ELL_FALSE@			ell/acd.c \
+@EXTERNAL_ELL_FALSE@			ell/netconfig.c
 
 @EXTERNAL_ELL_FALSE@ell_shared = ell/useful.h ell/asn1-private.h
 @EXTERNAL_ELL_FALSE@ell_libell_internal_la_SOURCES = $(ell_headers) $(ell_sources) $(ell_shared)
@@ -1290,7 +1298,8 @@ eap_sources = src/eap.c src/eap.h src/ea
 @DAEMON_TRUE@					src/anqp.h src/anqp.c \
 @DAEMON_TRUE@					src/anqputil.h src/anqputil.c \
 @DAEMON_TRUE@					src/netconfig.h src/netconfig.c\
-@DAEMON_TRUE@					src/resolve.h src/resolve.c\
+@DAEMON_TRUE@					src/netconfig-commit.c \
+@DAEMON_TRUE@					src/resolve.h src/resolve.c \
 @DAEMON_TRUE@					src/hotspot.c \
 @DAEMON_TRUE@					src/p2p.h src/p2p.c \
 @DAEMON_TRUE@					src/p2putil.h src/p2putil.c \
@@ -1550,8 +1559,7 @@ EXTRA_DIST = src/genbuiltin src/iwd.serv
 
 AM_CFLAGS = $(ell_cflags) -fvisibility=hidden \
 	-DUNITDIR=\""$(top_srcdir)/unit/"\" \
-	-DCERTDIR=\""$(top_builddir)/unit/"\" -DJSMN_PARENT_LINKS \
-	-DJSMN_STRICT $(am__append_25)
+	-DCERTDIR=\""$(top_builddir)/unit/"\" $(am__append_25)
 CLEANFILES = src/iwd.service wired/ead.service
 DISTCHECK_CONFIGURE_FLAGS = --disable-dbus-policy --disable-systemd-service \
 				--enable-ofono \
@@ -1817,6 +1825,7 @@ ell/dhcp6-lease.lo: ell/$(am__dirstamp)
 ell/dhcp6-transport.lo: ell/$(am__dirstamp) \
 	ell/$(DEPDIR)/$(am__dirstamp)
 ell/acd.lo: ell/$(am__dirstamp) ell/$(DEPDIR)/$(am__dirstamp)
+ell/netconfig.lo: ell/$(am__dirstamp) ell/$(DEPDIR)/$(am__dirstamp)
 
 ell/libell-internal.la: $(ell_libell_internal_la_OBJECTS) $(ell_libell_internal_la_DEPENDENCIES) $(EXTRA_ell_libell_internal_la_DEPENDENCIES) ell/$(am__dirstamp)
 	$(AM_V_CCLD)$(LINK) $(am_ell_libell_internal_la_rpath) $(ell_libell_internal_la_OBJECTS) $(ell_libell_internal_la_LIBADD) $(LIBS)
@@ -1953,6 +1962,8 @@ src/fils.$(OBJEXT): src/$(am__dirstamp)
 src/anqp.$(OBJEXT): src/$(am__dirstamp) src/$(DEPDIR)/$(am__dirstamp)
 src/netconfig.$(OBJEXT): src/$(am__dirstamp) \
 	src/$(DEPDIR)/$(am__dirstamp)
+src/netconfig-commit.$(OBJEXT): src/$(am__dirstamp) \
+	src/$(DEPDIR)/$(am__dirstamp)
 src/resolve.$(OBJEXT): src/$(am__dirstamp) \
 	src/$(DEPDIR)/$(am__dirstamp)
 src/hotspot.$(OBJEXT): src/$(am__dirstamp) \
@@ -2259,6 +2270,7 @@ distclean-compile:
 @AMDEP_TRUE@@am__include@ @am__quote@ell/$(DEPDIR)/log.Plo@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@ell/$(DEPDIR)/main.Plo@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@ell/$(DEPDIR)/net.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@ell/$(DEPDIR)/netconfig.Plo@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@ell/$(DEPDIR)/netlink.Plo@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@ell/$(DEPDIR)/path.Plo@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@ell/$(DEPDIR)/pem.Plo@am__quote@ # am--include-marker
@@ -2330,6 +2342,7 @@ distclean-compile:
 @AMDEP_TRUE@@am__include@ @am__quote@src/$(DEPDIR)/module.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@src/$(DEPDIR)/mpdu.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@src/$(DEPDIR)/mschaputil.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/$(DEPDIR)/netconfig-commit.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@src/$(DEPDIR)/netconfig.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@src/$(DEPDIR)/netdev.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@src/$(DEPDIR)/network.Po@am__quote@ # am--include-marker
@@ -3373,6 +3386,7 @@ distclean: distclean-am
 	-rm -f ell/$(DEPDIR)/log.Plo
 	-rm -f ell/$(DEPDIR)/main.Plo
 	-rm -f ell/$(DEPDIR)/net.Plo
+	-rm -f ell/$(DEPDIR)/netconfig.Plo
 	-rm -f ell/$(DEPDIR)/netlink.Plo
 	-rm -f ell/$(DEPDIR)/path.Plo
 	-rm -f ell/$(DEPDIR)/pem.Plo
@@ -3444,6 +3458,7 @@ distclean: distclean-am
 	-rm -f src/$(DEPDIR)/module.Po
 	-rm -f src/$(DEPDIR)/mpdu.Po
 	-rm -f src/$(DEPDIR)/mschaputil.Po
+	-rm -f src/$(DEPDIR)/netconfig-commit.Po
 	-rm -f src/$(DEPDIR)/netconfig.Po
 	-rm -f src/$(DEPDIR)/netdev.Po
 	-rm -f src/$(DEPDIR)/network.Po
@@ -3604,6 +3619,7 @@ maintainer-clean: maintainer-clean-am
 	-rm -f ell/$(DEPDIR)/log.Plo
 	-rm -f ell/$(DEPDIR)/main.Plo
 	-rm -f ell/$(DEPDIR)/net.Plo
+	-rm -f ell/$(DEPDIR)/netconfig.Plo
 	-rm -f ell/$(DEPDIR)/netlink.Plo
 	-rm -f ell/$(DEPDIR)/path.Plo
 	-rm -f ell/$(DEPDIR)/pem.Plo
@@ -3675,6 +3691,7 @@ maintainer-clean: maintainer-clean-am
 	-rm -f src/$(DEPDIR)/module.Po
 	-rm -f src/$(DEPDIR)/mpdu.Po
 	-rm -f src/$(DEPDIR)/mschaputil.Po
+	-rm -f src/$(DEPDIR)/netconfig-commit.Po
 	-rm -f src/$(DEPDIR)/netconfig.Po
 	-rm -f src/$(DEPDIR)/netdev.Po
 	-rm -f src/$(DEPDIR)/network.Po
diff -pruN 1.30-1/monitor/main.c 2.3-1/monitor/main.c
--- 1.30-1/monitor/main.c	2021-09-14 20:01:22.000000000 +0000
+++ 2.3-1/monitor/main.c	2022-11-18 12:31:49.000000000 +0000
@@ -451,7 +451,7 @@ static int analyze_pcap(const char *path
 
 	while (pcap_read(pcap, &tv, buf, snaplen, &len, &real_len)) {
 		struct nlmsghdr *nlmsg;
-		uint32_t aligned_len;
+		int64_t aligned_len;
 		uint16_t arphrd_type;
 		uint16_t proto_type;
 
@@ -569,7 +569,7 @@ done:
 	return exit_status;
 }
 
-static int process_pcap(struct pcap *pcap, uint16_t id)
+static int process_pcap(struct pcap *pcap, const struct nlmon_config *config)
 {
 	struct nlmon *nlmon = NULL;
 	struct timeval tv;
@@ -586,7 +586,7 @@ static int process_pcap(struct pcap *pca
 		return EXIT_FAILURE;
 	}
 
-	nlmon = nlmon_create(id);
+	nlmon = nlmon_create(0, config);
 
 	while (pcap_read(pcap, &tv, buf, snaplen, &len, &real_len)) {
 		uint16_t arphrd_type;
@@ -695,13 +695,12 @@ int main(int argc, char *argv[])
 	const char *reader_path = NULL;
 	const char *analyze_path = NULL;
 	const char *ifname = NULL;
-	uint16_t nl80211_family = 0;
 	int exit_status;
 
 	for (;;) {
 		int opt;
 
-		opt = getopt_long(argc, argv, "r:w:a:F:i:nvhyse",
+		opt = getopt_long(argc, argv, "r:w:a:i:nvhyse",
 						main_options, NULL);
 		if (opt < 0)
 			break;
@@ -709,6 +708,7 @@ int main(int argc, char *argv[])
 		switch (opt) {
 		case 'r':
 			reader_path = optarg;
+			config.read_only = true;
 			break;
 		case 'w':
 			writer_path = optarg;
@@ -716,26 +716,6 @@ int main(int argc, char *argv[])
 		case 'a':
 			analyze_path = optarg;
 			break;
-		case 'F':
-			if (strlen(optarg) > 3) {
-				if (!strncasecmp(optarg, "0x", 2) &&
-							!isxdigit(optarg[2])) {
-					usage();
-					return EXIT_FAILURE;
-				}
-				nl80211_family = strtoul(optarg + 2, NULL, 16);
-			} else {
-				if (!isdigit(optarg[0])) {
-					usage();
-					return EXIT_FAILURE;
-				}
-				nl80211_family = strtoul(optarg, NULL, 10);
-			}
-			if (nl80211_family == 0) {
-				usage();
-				return EXIT_FAILURE;
-			}
-			break;
 		case 'i':
 			ifname = optarg;
 			break;
@@ -797,7 +777,7 @@ int main(int argc, char *argv[])
 			fprintf(stderr, "Invalid packet format\n");
 			exit_status = EXIT_FAILURE;
 		} else
-			exit_status = process_pcap(pcap, nl80211_family);
+			exit_status = process_pcap(pcap, &config);
 
 		pcap_close(pcap);
 
diff -pruN 1.30-1/monitor/nlmon.c 2.3-1/monitor/nlmon.c
--- 1.30-1/monitor/nlmon.c	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/monitor/nlmon.c	2023-02-02 12:57:32.000000000 +0000
@@ -104,6 +104,7 @@ struct nlmon {
 	bool nowiphy;
 	bool noscan;
 	bool noies;
+	bool read;
 };
 
 struct nlmon_req {
@@ -493,7 +494,7 @@ static void print_ie_country(unsigned in
 			if (code[i + 2] < 32)
 				print_attr(level + 1, "%27c (air propagation "
 					"time %2d µs)", ' ', 3 * code[i + 2]);
-		} else {
+		} else if (code[i] != 0) {
 			print_attr(level + 1, "First channel %3d number of "
 				"channels %2d max tx power %2d dBm",
 				code[i], code[i + 1], code[i + 2]);
@@ -4529,6 +4530,7 @@ static void print_authentication_mgmt_fr
 {
 	const char *str;
 	const struct mmpdu_authentication *body;
+	uint16_t alg;
 
 	if (!mmpdu)
 		return;
@@ -4539,8 +4541,9 @@ static void print_authentication_mgmt_fr
 
 	print_mpdu_frame_control(level + 1, &mmpdu->fc);
 	print_mmpdu_header(level + 1, mmpdu);
+	alg = L_LE16_TO_CPU(body->algorithm);
 
-	switch (L_LE16_TO_CPU(body->algorithm)) {
+	switch (alg) {
 	case MMPDU_AUTH_ALGO_OPEN_SYSTEM:
 		str = "Open";
 		break;
@@ -4562,7 +4565,8 @@ static void print_authentication_mgmt_fr
 				L_LE16_TO_CPU(body->transaction_sequence),
 				L_LE16_TO_CPU(body->status));
 
-	if (L_LE16_TO_CPU(body->algorithm) != MMPDU_AUTH_ALGO_SHARED_KEY)
+	if (!L_IN_SET(alg, MMPDU_AUTH_ALGO_SHARED_KEY,
+				MMPDU_AUTH_ALGO_FT, MMPDU_AUTH_ALGO_SAE))
 		return;
 
 	if (L_LE16_TO_CPU(body->transaction_sequence) < 2 ||
@@ -4724,6 +4728,36 @@ static void print_anqp_frame(unsigned in
 	}
 }
 
+static void print_dpp_public_action_frame(unsigned int level,
+					const uint8_t *data, size_t len)
+{
+	print_attr(level, "DPP Action Frame");
+
+	print_attr(level + 1, "Crypto Suite: %u", *data);
+	data++;
+
+	switch (*data) {
+	case 0:
+		print_attr(level + 1, "Type: Authentication Request");
+		break;
+	case 1:
+		print_attr(level + 1, "Type: Authentication Response");
+		break;
+	case 2:
+		print_attr(level + 1, "Type: Authentication Confirm");
+		break;
+	case 11:
+		print_attr(level + 1, "Type: Configuration Result");
+		break;
+	case 13:
+		print_attr(level + 1, "Type: Presence Announcement");
+		break;
+	default:
+		print_attr(level + 1, "Type: Unknown (%u)", *data);
+		break;
+	}
+}
+
 static void print_public_action_frame(unsigned int level, const uint8_t *body,
 					size_t body_len)
 {
@@ -4791,6 +4825,12 @@ static void print_public_action_frame(un
 
 		print_p2p_public_action_frame(level + 1, body + 5,
 						body_len - 5);
+	} else if (!memcmp(oui, wifi_alliance_oui, 3) && oui[3] == 0x1a) {
+		if (!print_oui(level, oui))
+			return;
+
+		print_dpp_public_action_frame(level + 1, body + 5,
+						body_len - 5);
 	} else if (body[0] == 0x0a) {
 		if (body_len < 9)
 			return;
@@ -5018,6 +5058,16 @@ static void print_action_mgmt_frame(unsi
 	print_mmpdu_header(level + 1, mmpdu);
 }
 
+static void print_probe_response(unsigned int level,
+				const struct mmpdu_header *mmpdu, size_t len)
+{
+	const struct mmpdu_probe_response *resp = mmpdu_body(mmpdu);
+
+	print_attr(level, "Subtype: Probe Response");
+	print_ie(level + 1, "Probe Response IEs", resp->ies,
+			(const uint8_t *) mmpdu + len - resp->ies);
+}
+
 static void print_frame_type(unsigned int level, const char *label,
 					const void *data, uint16_t size)
 {
@@ -5063,7 +5113,10 @@ static void print_frame_type(unsigned in
 		str = "Probe request";
 		break;
 	case 0x05:
-		str = "Probe response";
+		if (mpdu)
+			print_probe_response(level + 1, mpdu, size);
+		else
+			str = "Probe response";
 		break;
 	case 0x06:
 		str = "Timing Advertisement";
@@ -7153,7 +7206,7 @@ static void nlmon_message(struct nlmon *
 		return;
 	}
 
-	if (nlmsg->nlmsg_type != nlmon->id) {
+	if (!nlmon->read && nlmsg->nlmsg_type != nlmon->id) {
 		if (nlmsg->nlmsg_type == GENL_ID_CTRL)
 			store_message(nlmon, tv, nlmsg);
 		return;
@@ -7204,7 +7257,7 @@ static void nlmon_message(struct nlmon *
 	}
 }
 
-struct nlmon *nlmon_create(uint16_t id)
+struct nlmon *nlmon_create(uint16_t id, const struct nlmon_config *config)
 {
 	struct nlmon *nlmon;
 
@@ -7212,6 +7265,11 @@ struct nlmon *nlmon_create(uint16_t id)
 
 	nlmon->id = id;
 	nlmon->req_list = l_queue_new();
+	nlmon->nortnl = config->nortnl;
+	nlmon->nowiphy = config->nowiphy;
+	nlmon->noscan = config->noscan;
+	nlmon->noies = config->noies;
+	nlmon->read = config->read_only;
 
 	return nlmon;
 }
@@ -7939,7 +7997,7 @@ static void print_nlmsghdr(const struct
 							nlmsg->nlmsg_flags);
 	print_field("Sequence number: %u (0x%08x)",
 					nlmsg->nlmsg_seq, nlmsg->nlmsg_seq);
-	print_field("Port ID: %d", nlmsg->nlmsg_pid);
+	print_field("Port ID: %u", nlmsg->nlmsg_pid);
 }
 
 static void print_nlmsg(const struct timeval *tv, const struct nlmsghdr *nlmsg)
@@ -8031,9 +8089,12 @@ static void print_rtnl_msg(const struct
 void nlmon_print_rtnl(struct nlmon *nlmon, const struct timeval *tv,
 					const void *data, uint32_t size)
 {
-	uint32_t aligned_size = NLMSG_ALIGN(size);
+	int64_t aligned_size = NLMSG_ALIGN(size);
 	const struct nlmsghdr *nlmsg;
 
+	if (nlmon->nortnl)
+		return;
+
 	update_time_offset(tv);
 
 	for (nlmsg = data; NLMSG_OK(nlmsg, aligned_size);
@@ -8063,7 +8124,7 @@ void nlmon_print_rtnl(struct nlmon *nlmo
 }
 
 void nlmon_print_genl(struct nlmon *nlmon, const struct timeval *tv,
-					const void *data, uint32_t size)
+					const void *data, int64_t size)
 {
 	const struct nlmsghdr *nlmsg;
 
@@ -8095,7 +8156,7 @@ static bool nlmon_receive(struct l_io *i
 	unsigned char buf[8192];
 	unsigned char control[32];
 	ssize_t bytes_read;
-	uint32_t nlmsg_len;
+	int64_t nlmsg_len;
 	int fd;
 
 	fd = l_io_get_fd(io);
@@ -8152,9 +8213,7 @@ static bool nlmon_receive(struct l_io *i
 		case NETLINK_ROUTE:
 			store_netlink(nlmon, tv, proto_type, nlmsg);
 
-			if (!nlmon->nortnl)
-				nlmon_print_rtnl(nlmon, tv, nlmsg,
-							nlmsg->nlmsg_len);
+			nlmon_print_rtnl(nlmon, tv, nlmsg, nlmsg->nlmsg_len);
 			break;
 		case NETLINK_GENERIC:
 			nlmon_message(nlmon, tv, tp, nlmsg);
@@ -8400,17 +8459,12 @@ struct nlmon *nlmon_open(const char *ifn
 	} else
 		pcap = NULL;
 
-	nlmon = l_new(struct nlmon, 1);
 
-	nlmon->id = id;
+	nlmon = nlmon_create(id, config);
+
 	nlmon->io = io;
 	nlmon->pae_io = pae_io;
-	nlmon->req_list = l_queue_new();
 	nlmon->pcap = pcap;
-	nlmon->nortnl = config->nortnl;
-	nlmon->nowiphy = config->nowiphy;
-	nlmon->noscan = config->noscan;
-	nlmon->noies = config->noies;
 
 	l_io_set_read_handler(nlmon->io, nlmon_receive, nlmon, NULL);
 	l_io_set_read_handler(nlmon->pae_io, pae_receive, nlmon, NULL);
diff -pruN 1.30-1/monitor/nlmon.h 2.3-1/monitor/nlmon.h
--- 1.30-1/monitor/nlmon.h	2021-02-16 20:36:24.000000000 +0000
+++ 2.3-1/monitor/nlmon.h	2022-11-18 12:31:49.000000000 +0000
@@ -30,18 +30,19 @@ struct nlmon_config {
 	bool nowiphy;
 	bool noscan;
 	bool noies;
+	bool read_only;
 };
 
 struct nlmon *nlmon_open(const char *ifname, uint16_t id, const char *pathname,
 				const struct nlmon_config *config);
 void nlmon_close(struct nlmon *nlmon);
 
-struct nlmon *nlmon_create(uint16_t id);
+struct nlmon *nlmon_create(uint16_t id, const struct nlmon_config *config);
 void nlmon_destroy(struct nlmon *nlmon);
 void nlmon_print_rtnl(struct nlmon *nlmon, const struct timeval *tv,
 					const void *data, uint32_t size);
 void nlmon_print_genl(struct nlmon *nlmon, const struct timeval *tv,
-					const void *data, uint32_t size);
+					const void *data, int64_t size);
 void nlmon_print_pae(struct nlmon *nlmon, const struct timeval *tv,
 					uint8_t type, int index,
 					const void *data, uint32_t size);
diff -pruN 1.30-1/README 2.3-1/README
--- 1.30-1/README	2021-06-12 12:33:02.000000000 +0000
+++ 2.3-1/README	2022-11-18 12:31:49.000000000 +0000
@@ -268,7 +268,8 @@ Information
 ===========
 
 Mailing list:
-	https://lists.01.org/postorius/lists/iwd.lists.01.org/
+	https://lists.linux.dev/
+	https://lore.kernel.org/iwd/
 
 IRC:
 	irc://irc.oftc.net/#iwd
diff -pruN 1.30-1/shared/jsmn.h 2.3-1/shared/jsmn.h
--- 1.30-1/shared/jsmn.h	2022-01-05 21:23:11.000000000 +0000
+++ 2.3-1/shared/jsmn.h	2022-11-18 12:31:49.000000000 +0000
@@ -45,10 +45,10 @@ extern "C" {
  */
 typedef enum {
   JSMN_UNDEFINED = 0,
-  JSMN_OBJECT = 1,
-  JSMN_ARRAY = 2,
-  JSMN_STRING = 3,
-  JSMN_PRIMITIVE = 4
+  JSMN_OBJECT = 1 << 0,
+  JSMN_ARRAY = 1 << 1,
+  JSMN_STRING = 1 << 2,
+  JSMN_PRIMITIVE = 1 << 3
 } jsmntype_t;
 
 enum jsmnerr {
@@ -66,7 +66,7 @@ enum jsmnerr {
  * start	start position in JSON data string
  * end		end position in JSON data string
  */
-typedef struct {
+typedef struct jsmntok {
   jsmntype_t type;
   int start;
   int end;
@@ -80,7 +80,7 @@ typedef struct {
  * JSON parser. Contains an array of token blocks available. Also stores
  * the string being parsed now and current position in that string.
  */
-typedef struct {
+typedef struct jsmn_parser {
   unsigned int pos;     /* offset in the JSON string */
   unsigned int toknext; /* next token to allocate */
   int toksuper;         /* superior token node, e.g. parent object or array */
@@ -154,6 +154,9 @@ static int jsmn_parse_primitive(jsmn_par
     case ']':
     case '}':
       goto found;
+    default:
+                   /* to quiet a warning from gcc*/
+      break;
     }
     if (js[parser->pos] < 32 || js[parser->pos] >= 127) {
       parser->pos = start;
@@ -193,10 +196,10 @@ static int jsmn_parse_string(jsmn_parser
   jsmntok_t *token;
 
   int start = parser->pos;
-
-  parser->pos++;
-
+  
   /* Skip starting quote */
+  parser->pos++;
+  
   for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
     char c = js[parser->pos];
 
diff -pruN 1.30-1/src/agent.c 2.3-1/src/agent.c
--- 1.30-1/src/agent.c	2022-01-24 21:28:47.000000000 +0000
+++ 2.3-1/src/agent.c	2022-11-18 12:31:49.000000000 +0000
@@ -28,6 +28,7 @@
 
 #include <ell/ell.h>
 #include "src/dbus.h"
+#include "src/netconfig.h"
 #include "src/agent.h"
 #include "src/iwd.h"
 #include "src/module.h"
@@ -584,6 +585,60 @@ static struct l_dbus_message *agent_unre
 	return reply;
 }
 
+static struct l_dbus_message *netconfig_agent_register(struct l_dbus *dbus,
+						struct l_dbus_message *message,
+						void *user_data)
+{
+	struct l_dbus_message *reply;
+	const char *path;
+	int r;
+
+	l_debug("");
+
+	if (!l_dbus_message_get_arguments(message, "o", &path))
+		return dbus_error_invalid_args(message);
+
+	if (!netconfig_enabled())
+		return dbus_error_not_supported(message);
+
+	r = netconfig_register_agent(l_dbus_message_get_sender(message), path);
+	if (r)
+		return dbus_error_from_errno(r, message);
+
+	l_debug("agent %s path %s",
+		l_dbus_message_get_sender(message), path);
+
+	reply = l_dbus_message_new_method_return(message);
+	l_dbus_message_set_arguments(reply, "");
+	return reply;
+}
+
+static struct l_dbus_message *netconfig_agent_unregister(struct l_dbus *dbus,
+						struct l_dbus_message *message,
+					void *user_data)
+{
+	struct l_dbus_message *reply;
+	const char *path;
+	int r;
+
+	l_debug("");
+
+	if (!l_dbus_message_get_arguments(message, "o", &path))
+		return dbus_error_invalid_args(message);
+
+	if (!netconfig_enabled())
+		return dbus_error_not_supported(message);
+
+	r = netconfig_unregister_agent(l_dbus_message_get_sender(message),
+					path);
+	if (r)
+		return dbus_error_from_errno(r, message);
+
+	reply = l_dbus_message_new_method_return(message);
+	l_dbus_message_set_arguments(reply, "");
+	return reply;
+}
+
 static void setup_agent_interface(struct l_dbus_interface *interface)
 {
 	l_dbus_interface_method(interface, "RegisterAgent", 0,
@@ -592,6 +647,13 @@ static void setup_agent_interface(struct
 	l_dbus_interface_method(interface, "UnregisterAgent", 0,
 				agent_unregister,
 				"", "o", "path");
+
+	l_dbus_interface_method(interface,
+				"RegisterNetworkConfigurationAgent", 0,
+				netconfig_agent_register, "", "o", "path");
+	l_dbus_interface_method(interface,
+				"UnregisterNetworkConfigurationAgent", 0,
+				netconfig_agent_unregister, "", "o", "path");
 }
 
 static bool release_agent(void *data, void *user_data)
diff -pruN 1.30-1/src/anqp.c 2.3-1/src/anqp.c
--- 1.30-1/src/anqp.c	2021-11-18 20:21:21.000000000 +0000
+++ 2.3-1/src/anqp.c	2022-11-18 12:31:49.000000000 +0000
@@ -156,6 +156,7 @@ static bool anqp_response_frame_event(co
 }
 
 static const struct frame_xchg_prefix anqp_frame_prefix = {
+	.frame_type = 0x00d0,
 	.data = (uint8_t []) {
 		0x04, 0x0b,
 	},
diff -pruN 1.30-1/src/ap.c 2.3-1/src/ap.c
--- 1.30-1/src/ap.c	2022-07-15 17:08:06.000000000 +0000
+++ 2.3-1/src/ap.c	2023-02-02 12:57:32.000000000 +0000
@@ -70,7 +70,9 @@ struct ap_state {
 	char ssid[33];
 	char passphrase[64];
 	uint8_t psk[32];
+	enum band_freq band;
 	uint8_t channel;
+	struct band_chandef chandef;
 	uint8_t *authorized_macs;
 	unsigned int authorized_macs_num;
 	char wsc_name[33];
@@ -104,12 +106,16 @@ struct ap_state {
 	struct l_dbus_message *scan_pending;
 	struct l_queue *networks;
 
+	struct l_timeout *rekey_timeout;
+	unsigned int rekey_time;
+
 	bool started : 1;
 	bool gtk_set : 1;
 	bool netconfig_set_addr4 : 1;
 	bool in_event : 1;
 	bool free_pending : 1;
 	bool scanning : 1;
+	bool supports_ht : 1;
 };
 
 struct sta_state {
@@ -134,6 +140,10 @@ struct sta_state {
 	bool wsc_v2;
 	struct l_dhcp_lease *ip_alloc_lease;
 	bool ip_alloc_sent;
+	uint64_t rekey_time;
+
+	bool ht_support : 1;
+	bool ht_greenfield : 1;
 };
 
 struct ap_wsc_pbc_probe_record {
@@ -339,6 +349,11 @@ static void ap_reset(struct ap_state *ap
 		l_queue_destroy(ap->networks, l_free);
 		ap->networks = NULL;
 	}
+
+	if (ap->rekey_timeout) {
+		l_timeout_remove(ap->rekey_timeout);
+		ap->rekey_timeout = NULL;
+	}
 }
 
 static bool ap_event_done(struct ap_state *ap, bool prev_in_event)
@@ -371,6 +386,8 @@ static bool ap_event(struct ap_state *ap
 	return ap_event_done(ap, prev);
 }
 
+static void ap_check_rekeys(struct ap_state *ap);
+
 static void ap_del_station(struct sta_state *sta, uint16_t reason,
 				bool disassociate)
 {
@@ -433,6 +450,93 @@ static void ap_del_station(struct sta_st
 
 		ap_event_done(ap, prev);
 	}
+
+	/*
+	 * Set the rekey time to zero which will skip this station when
+	 * determining the next rekey.
+	 */
+	sta->rekey_time = 0;
+	ap_check_rekeys(ap);
+}
+
+static void ap_start_rekey(struct ap_state *ap, struct sta_state *sta)
+{
+	l_debug("Rekey STA "MAC, MAC_STR(sta->addr));
+
+	eapol_start(sta->sm);
+}
+
+static void ap_rekey_timeout(struct l_timeout *timeout, void *user_data)
+{
+	struct ap_state *ap = user_data;
+
+	ap_check_rekeys(ap);
+}
+
+/*
+ * Used to check/start any rekeys which are due and reset the rekey timer to the
+ * next soonest station needing a rekey.
+ *
+ * TODO: Could adapt this to also take into account the next GTK rekey and
+ * service that as well. But GTK rekeys are not yet supported in AP mode.
+ */
+static void ap_check_rekeys(struct ap_state *ap)
+{
+	const struct l_queue_entry *e;
+	uint64_t now = l_time_now();
+	uint64_t next = 0;
+
+	if (!ap->rekey_time)
+		return;
+
+	/* Find the station(s) that need a rekey and start it */
+	for (e = l_queue_get_entries(ap->sta_states); e; e = e->next) {
+		struct sta_state *sta = e->data;
+
+		if (!sta->associated || !sta->rsna || sta->rekey_time == 0)
+			continue;
+
+		if (l_time_before(now, sta->rekey_time)) {
+			uint64_t diff = l_time_diff(now, sta->rekey_time);
+
+			/* Finding the next rekey time */
+			if (next < diff)
+				next = diff;
+
+			continue;
+		}
+
+		ap_start_rekey(ap, sta);
+	}
+
+	/*
+	 * Set the next rekey to the station needing it the soonest, or remove
+	 * if a single station and wait until the rekey is complete to reset
+	 * the timer.
+	 */
+	if (next)
+		l_timeout_modify(ap->rekey_timeout, l_time_to_secs(next));
+	else {
+		l_timeout_remove(ap->rekey_timeout);
+		ap->rekey_timeout = NULL;
+	}
+}
+
+static void ap_set_sta_rekey_timer(struct ap_state *ap, struct sta_state *sta)
+{
+	if (!ap->rekey_time)
+		return;
+
+	sta->rekey_time = l_time_now() + ap->rekey_time - 1;
+
+	/*
+	 * First/only station authenticated, set rekey timer. Any more stations
+	 * will just set their rekey time and be serviced by the single callback
+	 */
+	if (!ap->rekey_timeout)
+		ap->rekey_timeout = l_timeout_create(
+						l_time_to_secs(ap->rekey_time),
+						ap_rekey_timeout, ap, NULL);
 }
 
 static bool ap_sta_match_addr(const void *a, const void *b)
@@ -473,6 +577,8 @@ static void ap_new_rsna(struct sta_state
 
 	sta->rsna = true;
 
+	ap_set_sta_rekey_timer(ap, sta);
+
 	event_data.mac = sta->addr;
 	event_data.assoc_ies = sta->assoc_ies;
 	event_data.assoc_ies_len = sta->assoc_ies_len;
@@ -699,7 +805,7 @@ static size_t ap_write_wsc_ie(struct ap_
 	size_t len = 0;
 
 	/* WSC IE */
-	if (type == MPDU_MANAGEMENT_SUBTYPE_PROBE_RESPONSE) {
+	if (type == MPDU_MANAGEMENT_SUBTYPE_PROBE_RESPONSE && client_frame) {
 		const uint8_t *from = client_frame->address_2;
 		struct wsc_probe_response wsc_pr = {};
 		const struct mmpdu_probe_request *req =
@@ -801,6 +907,29 @@ static size_t ap_write_wsc_ie(struct ap_
 	return len;
 }
 
+static size_t ap_build_supported_rates(struct ap_state *ap,
+					uint8_t *rates)
+{
+	uint32_t minr, maxr, count, r;
+
+	minr = l_uintset_find_min(ap->rates);
+	maxr = l_uintset_find_max(ap->rates);
+	count = 0;
+	for (r = minr; r <= maxr && count < 8; r++)
+		if (l_uintset_contains(ap->rates, r)) {
+			uint8_t flag = 0;
+
+			/* Mark only the lowest rate as Basic Rate */
+			if (count == 0)
+				flag = 0x80;
+
+			*rates++ = r | flag;
+			count++;
+		}
+
+	return count;
+}
+
 static size_t ap_get_extra_ies_len(struct ap_state *ap,
 					enum mpdu_management_subtype type,
 					const struct mmpdu_header *client_frame,
@@ -810,6 +939,10 @@ static size_t ap_get_extra_ies_len(struc
 
 	len += ap_get_wsc_ie_len(ap, type, client_frame, client_frame_len);
 
+	/* WMM IE length */
+	if (ap->supports_ht)
+		len += 50;
+
 	if (ap->ops->get_extra_ies_len)
 		len += ap->ops->get_extra_ies_len(type, client_frame,
 							client_frame_len,
@@ -818,6 +951,67 @@ static size_t ap_get_extra_ies_len(struc
 	return len;
 }
 
+/* WMM Specification 2.2.2 WMM Parameter Element */
+struct ap_wmm_ac_record {
+#if defined(__LITTLE_ENDIAN_BITFIELD)
+	uint8_t aifsn : 4;
+	uint8_t acm : 1;
+	uint8_t aci : 2;
+	uint8_t reserved : 1;
+	uint8_t ecw_min : 4;
+	uint8_t ecw_max : 4;
+#elif defined (__BIG_ENDIAN_BITFIELD)
+	uint8_t reserved : 1;
+	uint8_t aci : 2;
+	uint8_t acm : 1;
+	uint8_t aifsn : 4;
+	uint8_t ecw_max : 4;
+	uint8_t ecw_min : 4;
+#else
+#error "Please fix <asm/byteorder.h"
+#endif
+	__le16 txop_limit;
+} __attribute__((packed));
+
+static size_t ap_write_wmm_ies(struct ap_state *ap, uint8_t *out_buf)
+{
+	unsigned int i;
+	struct wiphy *wiphy = netdev_get_wiphy(ap->netdev);
+
+	/*
+	 * Linux kernel requires APs include WMM Information element if
+	 * supporting HT/VHT/etc.
+	 *
+	 * The only value we can actually get from the kernel is UAPSD. The
+	 * remaining values (AC parameter records) are made up or defaults
+	 * defined in the WMM spec are used.
+	 */
+	*out_buf++ = IE_TYPE_VENDOR_SPECIFIC;
+	*out_buf++ = 24;
+	memcpy(out_buf, microsoft_oui, sizeof(microsoft_oui));
+	out_buf += sizeof(microsoft_oui);
+	*out_buf++ = 2; /* WMM OUI Type */
+	*out_buf++ = 1; /* WMM Parameter subtype */
+	*out_buf++ = 1; /* WMM Version */
+	*out_buf++ = wiphy_supports_uapsd(wiphy) ? 1 << 7 : 0;
+	*out_buf++ = 0; /* reserved */
+
+	for (i = 0; i < 4; i++) {
+		struct ap_wmm_ac_record ac = { 0 };
+
+		ac.aifsn = 2;
+		ac.acm = 0;
+		ac.aci = i;
+		ac.ecw_min = 1;
+		ac.ecw_max = 15;
+		l_put_le16(0, &ac.txop_limit);
+
+		memcpy(out_buf + (i * 4), &ac, sizeof(struct ap_wmm_ac_record));
+	}
+
+	return 26;
+}
+
 static size_t ap_write_extra_ies(struct ap_state *ap,
 					enum mpdu_management_subtype type,
 					const struct mmpdu_header *client_frame,
@@ -829,6 +1023,9 @@ static size_t ap_write_extra_ies(struct
 	len += ap_write_wsc_ie(ap, type, client_frame, client_frame_len,
 				out_buf + len);
 
+	if (ap->supports_ht)
+		len += ap_write_wmm_ies(ap, out_buf + len);
+
 	if (ap->ops->write_extra_ies)
 		len += ap->ops->write_extra_ies(type,
 						client_frame, client_frame_len,
@@ -837,6 +1034,66 @@ static size_t ap_write_extra_ies(struct
 	return len;
 }
 
+static size_t ap_build_ht_capability(struct ap_state *ap, uint8_t *buf)
+{
+	struct wiphy *wiphy = netdev_get_wiphy(ap->netdev);
+	size_t ht_capa_len;
+	const uint8_t *ht_capa = wiphy_get_ht_capabilities(wiphy, ap->band,
+								&ht_capa_len);
+
+	memcpy(buf, ht_capa, ht_capa_len);
+
+	return ht_capa_len;
+}
+
+static size_t ap_build_ht_operation(struct ap_state *ap, uint8_t *buf)
+{
+	const struct l_queue_entry *e;
+	unsigned int non_ht = false;
+	unsigned int non_greenfield = false;
+
+	memset(buf, 0, 22);
+	*buf++ = ap->channel;
+
+	/*
+	 * If 40MHz set 'Secondary Channel Offset' (bits 0-1) to above/below
+	 * and set 'STA Channel Width' (bit 2) to indicate non-20Mhz.
+	 */
+	if (ap->chandef.channel_width == BAND_CHANDEF_WIDTH_20)
+		goto check_stas;
+	else if (ap->chandef.frequency < ap->chandef.center1_frequency)
+		*buf |= 1 & 0x3;
+	else
+		*buf |= 3 & 0x3;
+
+	*buf |= 1 << 2;
+
+check_stas:
+	for (e = l_queue_get_entries(ap->sta_states); e; e = e->next) {
+		struct sta_state *sta = e->data;
+
+		if (!sta->associated)
+			continue;
+
+		if (!sta->ht_support)
+			non_ht = true;
+		else if (!sta->ht_greenfield)
+			non_greenfield = true;
+	}
+
+	if (non_greenfield)
+		set_bit(buf, 10);
+
+	if (non_ht)
+		set_bit(buf, 12);
+
+	/*
+	 * TODO: Basic MCS set for all associated STAs
+	 */
+
+	return 22;
+}
+
 /*
  * Build a Beacon frame or a Probe Response frame's header and body until
  * the TIM IE.  Except for the optional TIM IE which is inserted by the
@@ -857,8 +1114,7 @@ static size_t ap_build_beacon_pr_head(st
 	struct mmpdu_header *mpdu = (void *) out_buf;
 	uint16_t capability = IE_BSS_CAP_ESS | IE_BSS_CAP_PRIVACY;
 	const uint8_t *bssid = netdev_get_address(ap->netdev);
-	uint32_t minr, maxr, count, r;
-	uint8_t *rates;
+	size_t len;
 	struct ie_tlv_builder builder;
 
 	memset(mpdu, 0, 36); /* Zero out header + non-IE fields */
@@ -883,49 +1139,146 @@ static size_t ap_build_beacon_pr_head(st
 
 	/* Supported Rates IE */
 	ie_tlv_builder_next(&builder, IE_TYPE_SUPPORTED_RATES);
-	rates = ie_tlv_builder_get_data(&builder);
-
-	minr = l_uintset_find_min(ap->rates);
-	maxr = l_uintset_find_max(ap->rates);
-	count = 0;
-	for (r = minr; r <= maxr && count < 8; r++)
-		if (l_uintset_contains(ap->rates, r)) {
-			uint8_t flag = 0;
-
-			/* Mark only the lowest rate as Basic Rate */
-			if (count == 0)
-				flag = 0x80;
-
-			*rates++ = r | flag;
-			count++;
-		}
-
-	ie_tlv_builder_set_length(&builder, count);
+	len = ap_build_supported_rates(ap, ie_tlv_builder_get_data(&builder));
+	ie_tlv_builder_set_length(&builder, len);
 
 	/* DSSS Parameter Set IE for DSSS, HR, ERP and HT PHY rates */
 	ie_tlv_builder_next(&builder, IE_TYPE_DSSS_PARAMETER_SET);
 	ie_tlv_builder_set_data(&builder, &ap->channel, 1);
 
+	if (ap->supports_ht) {
+		ie_tlv_builder_next(&builder, IE_TYPE_HT_CAPABILITIES);
+		len = ap_build_ht_capability(ap,
+					ie_tlv_builder_get_data(&builder));
+		ie_tlv_builder_set_length(&builder, len);
+
+		ie_tlv_builder_next(&builder, IE_TYPE_HT_OPERATION);
+		len = ap_build_ht_operation(ap,
+					ie_tlv_builder_get_data(&builder));
+		ie_tlv_builder_set_length(&builder, len);
+	}
+
 	ie_tlv_builder_finalize(&builder, &out_len);
 	return 36 + out_len;
 }
 
+static size_t ap_build_country_ie(struct ap_state *ap, uint8_t *out_buf,
+					size_t buf_len)
+{
+	size_t len;
+	size_t i;
+	int spacing;
+	uint8_t *pos = out_buf;
+	uint8_t nchans = 1;
+	struct wiphy *wiphy = netdev_get_wiphy(ap->netdev);
+	const struct band_freq_attrs *last = NULL;
+	const struct band_freq_attrs *list = wiphy_get_frequency_info_list(
+							wiphy, ap->band, &len);
+
+	if (!list || wiphy_country_is_unknown(wiphy))
+		return 0;
+
+	if (L_WARN_ON(buf_len < 5))
+		goto no_space;
+
+	*pos++ = IE_TYPE_COUNTRY;
+	/* length not yet known */
+	pos++;
+
+	wiphy_get_reg_domain_country(wiphy, (char *)pos);
+	pos += 2;
+	*pos++ = ' ';
+
+	buf_len -= 5;
+
+	if (ap->band == BAND_FREQ_2_4_GHZ)
+		spacing = 1;
+	else
+		spacing = 4;
+
+	/*
+	 * Construct a list of subband triplet entries. Each entry contains a
+	 * starting channel and a number of channels which are spaced evenly
+	 * and use the same TX power. Any deviation from this results in a new
+	 * channel group.
+	 *
+	 * TODO: 6Ghz requires operating triplets, not subband triplets.
+	 */
+	for (i = 0; i < len; i++) {
+		const struct band_freq_attrs *attr = &list[i];
+
+		if (!attr->supported || attr->disabled)
+			continue;
+
+		if (!last) {
+			/* Room for one complete triplet */
+			if (L_WARN_ON(buf_len < 3))
+				goto no_space;
+
+			*pos++ = i;
+			last = attr;
+			continue;
+		}
+
+		if (spacing != attr - last ||
+					attr->tx_power != last->tx_power) {
+			/* finish current group */
+			*pos++ = nchans;
+			*pos++ = last->tx_power;
+			buf_len -= 3;
+
+			/* start a new group */
+			if (L_WARN_ON(buf_len < 3))
+				goto no_space;
+
+			*pos++ = i;
+			nchans = 1;
+		} else
+			nchans++;
+
+		last = attr;
+	}
+
+	/* finish final group */
+	*pos++ = nchans;
+	*pos++ = last->tx_power;
+
+	len = pos - out_buf - 2;
+
+	/* Pad to even byte */
+	if (len & 1) {
+		if (L_WARN_ON(buf_len < 1))
+			goto no_space;
+
+		*pos++ = 0;
+		len++;
+	}
+
+	out_buf[1] = len;
+
+	return out_buf[1] + 2;
+
+no_space:
+	return 0;
+}
+
 /* Beacon / Probe Response frame portion after the TIM IE */
 static size_t ap_build_beacon_pr_tail(struct ap_state *ap,
 					enum mpdu_management_subtype stype,
 					const struct mmpdu_header *req,
-					size_t req_len, uint8_t *out_buf)
+					size_t req_len, uint8_t *out_buf,
+					size_t buf_len)
 {
 	size_t len;
 	struct ie_rsn_info rsn;
 
-	/* TODO: Country IE between TIM IE and RSNE */
+	len = ap_build_country_ie(ap, out_buf, buf_len);
 
 	/* RSNE */
 	ap_set_rsn_info(ap, &rsn);
-	if (!ie_build_rsne(&rsn, out_buf))
+	if (!ie_build_rsne(&rsn, out_buf + len))
 		return 0;
-	len = 2 + out_buf[1];
+	len += 2 + out_buf[len + 1];
 
 	len += ap_write_extra_ies(ap, stype, req, req_len, out_buf + len);
 	return len;
@@ -943,11 +1296,11 @@ void ap_update_beacon(struct ap_state *a
 {
 	struct l_genl_msg *cmd;
 	uint8_t head[256];
-	L_AUTO_FREE_VAR(uint8_t *, tail) =
-		l_malloc(256 + ap_get_extra_ies_len(ap,
+	size_t tail_len = 256 + ap_get_extra_ies_len(ap,
 						MPDU_MANAGEMENT_SUBTYPE_BEACON,
-						NULL, 0));
-	size_t head_len, tail_len;
+						NULL, 0);
+	L_AUTO_FREE_VAR(uint8_t *, tail) = malloc(tail_len);
+	size_t head_len;
 	uint64_t wdev_id = netdev_get_wdev_id(ap->netdev);
 	static const uint8_t bcast_addr[6] = {
 		0xff, 0xff, 0xff, 0xff, 0xff, 0xff
@@ -959,7 +1312,7 @@ void ap_update_beacon(struct ap_state *a
 	head_len = ap_build_beacon_pr_head(ap, MPDU_MANAGEMENT_SUBTYPE_BEACON,
 						bcast_addr, head, sizeof(head));
 	tail_len = ap_build_beacon_pr_tail(ap, MPDU_MANAGEMENT_SUBTYPE_BEACON,
-						NULL, 0, tail);
+						NULL, 0, tail, tail_len);
 	if (L_WARN_ON(!head_len || !tail_len))
 		return;
 
@@ -985,7 +1338,7 @@ static uint32_t ap_send_mgmt_frame(struc
 					frame_xchg_cb_t callback,
 					void *user_data)
 {
-	uint32_t ch_freq = band_channel_to_freq(ap->channel, BAND_FREQ_2_4_GHZ);
+	uint32_t ch_freq = band_channel_to_freq(ap->channel, ap->band);
 	uint64_t wdev_id = netdev_get_wdev_id(ap->netdev);
 	struct iovec iov[2];
 
@@ -1120,6 +1473,9 @@ static void ap_handshake_event(struct ha
 		sta->hs->go_ip_addr = IP4_FROM_STR(own_addr_str);
 		break;
 	}
+	case HANDSHAKE_EVENT_REKEY_COMPLETE:
+		ap_set_sta_rekey_timer(ap, sta);
+		return;
 	default:
 		break;
 	}
@@ -1539,8 +1895,8 @@ static uint32_t ap_assoc_resp(struct ap_
 	struct mmpdu_header *mpdu = (void *) mpdu_buf;
 	struct mmpdu_association_response *resp;
 	size_t ies_len = 0;
+	size_t len;
 	uint16_t capability = IE_BSS_CAP_ESS | IE_BSS_CAP_PRIVACY;
-	uint32_t r, minr, maxr, count;
 
 	memset(mpdu, 0, sizeof(*mpdu));
 
@@ -1560,23 +1916,21 @@ static uint32_t ap_assoc_resp(struct ap_
 
 	/* Supported Rates IE */
 	resp->ies[ies_len++] = IE_TYPE_SUPPORTED_RATES;
-
-	minr = l_uintset_find_min(ap->rates);
-	maxr = l_uintset_find_max(ap->rates);
-	count = 0;
-	for (r = minr; r <= maxr && count < 8; r++)
-		if (l_uintset_contains(ap->rates, r)) {
-			uint8_t flag = 0;
-
-			/* Mark only the lowest rate as Basic Rate */
-			if (count == 0)
-				flag = 0x80;
-
-			resp->ies[ies_len + 1 + count++] = r | flag;
-		}
-
-	resp->ies[ies_len++] = count;
-	ies_len += count;
+	len = ap_build_supported_rates(ap, resp->ies + ies_len + 1);
+	resp->ies[ies_len++] = len;
+	ies_len += len;
+
+	if (ap->supports_ht) {
+		resp->ies[ies_len++] = IE_TYPE_HT_CAPABILITIES;
+		len = ap_build_ht_capability(ap, resp->ies + ies_len + 1);
+		resp->ies[ies_len++] = len;
+		ies_len += len;
+
+		resp->ies[ies_len++] = IE_TYPE_HT_OPERATION;
+		len = ap_build_ht_operation(ap, resp->ies + ies_len + 1);
+		resp->ies[ies_len++] = len;
+		ies_len += len;
+	}
 
 	ies_len += ap_write_extra_ies(ap, stype, req, req_len,
 					resp->ies + ies_len);
@@ -1783,6 +2137,17 @@ static void ap_assoc_reassoc(struct sta_
 
 			fils_ip_req = true;
 			break;
+		case IE_TYPE_HT_CAPABILITIES:
+			if (ie_tlv_iter_get_length(&iter) != 26) {
+				err = MMPDU_REASON_CODE_INVALID_IE;
+				goto bad_frame;
+			}
+
+			if (test_bit(ie_tlv_iter_get_data(&iter), 4))
+				sta->ht_greenfield = true;
+
+			sta->ht_support = true;
+			break;
 		}
 
 	if (!rates || !ssid || (!wsc_data && !rsn) ||
@@ -2134,7 +2499,7 @@ static void ap_probe_req_cb(const struct
 	len += ap_build_beacon_pr_tail(ap,
 					MPDU_MANAGEMENT_SUBTYPE_PROBE_RESPONSE,
 					hdr, body + body_len - (void *) hdr,
-					resp + len);
+					resp + len, resp_len - len);
 
 	ap_send_mgmt_frame(ap, (struct mmpdu_header *) resp, len,
 				ap_probe_resp_cb, NULL);
@@ -2391,11 +2756,11 @@ static struct l_genl_msg *ap_build_cmd_s
 	struct l_genl_msg *cmd;
 
 	uint8_t head[256];
-	L_AUTO_FREE_VAR(uint8_t *, tail) =
-		l_malloc(256 + ap_get_extra_ies_len(ap,
+	size_t tail_len = 256 + ap_get_extra_ies_len(ap,
 						MPDU_MANAGEMENT_SUBTYPE_BEACON,
-						NULL, 0));
-	size_t head_len, tail_len;
+						NULL, 0);
+	L_AUTO_FREE_VAR(uint8_t *, tail) = l_malloc(tail_len);
+	size_t head_len;
 
 	uint32_t dtim_period = 3;
 	uint32_t ifindex = netdev_get_ifindex(ap->netdev);
@@ -2408,14 +2773,14 @@ static struct l_genl_msg *ap_build_cmd_s
 	uint32_t nl_akm = CRYPTO_AKM_PSK;
 	uint32_t wpa_version = NL80211_WPA_VERSION_2;
 	uint32_t auth_type = NL80211_AUTHTYPE_OPEN_SYSTEM;
-	uint32_t ch_freq = band_channel_to_freq(ap->channel, BAND_FREQ_2_4_GHZ);
-	uint32_t ch_width = NL80211_CHAN_WIDTH_20;
 	unsigned int i;
 
 	static const uint8_t bcast_addr[6] = {
 		0xff, 0xff, 0xff, 0xff, 0xff, 0xff
 	};
 
+	static const uint8_t zero_addr[6] = { 0 };
+
 	for (i = 0, nl_ciphers_cnt = 0; i < 8; i++)
 		if (ap->ciphers & (1 << i))
 			nl_ciphers[nl_ciphers_cnt++] =
@@ -2424,7 +2789,7 @@ static struct l_genl_msg *ap_build_cmd_s
 	head_len = ap_build_beacon_pr_head(ap, MPDU_MANAGEMENT_SUBTYPE_BEACON,
 						bcast_addr, head, sizeof(head));
 	tail_len = ap_build_beacon_pr_tail(ap, MPDU_MANAGEMENT_SUBTYPE_BEACON,
-						NULL, 0, tail);
+						NULL, 0, tail, tail_len);
 
 	if (!head_len || !tail_len)
 		return NULL;
@@ -2455,8 +2820,29 @@ static struct l_genl_msg *ap_build_cmd_s
 	l_genl_msg_append_attr(cmd, NL80211_ATTR_WPA_VERSIONS, 4, &wpa_version);
 	l_genl_msg_append_attr(cmd, NL80211_ATTR_AKM_SUITES, 4, &nl_akm);
 	l_genl_msg_append_attr(cmd, NL80211_ATTR_AUTH_TYPE, 4, &auth_type);
-	l_genl_msg_append_attr(cmd, NL80211_ATTR_WIPHY_FREQ, 4, &ch_freq);
-	l_genl_msg_append_attr(cmd, NL80211_ATTR_CHANNEL_WIDTH, 4, &ch_width);
+	l_genl_msg_append_attr(cmd, NL80211_ATTR_WIPHY_FREQ, 4,
+				&ap->chandef.frequency);
+	l_genl_msg_append_attr(cmd, NL80211_ATTR_CHANNEL_WIDTH, 4,
+				&ap->chandef.channel_width);
+	if (ap->chandef.center1_frequency)
+		l_genl_msg_append_attr(cmd, NL80211_ATTR_CENTER_FREQ1, 4,
+					&ap->chandef.center1_frequency);
+
+	if (wiphy_supports_probe_resp_offload(wiphy)) {
+		uint8_t probe_resp[head_len + tail_len];
+		uint8_t *ptr = probe_resp;
+
+		ptr += ap_build_beacon_pr_head(ap,
+					MPDU_MANAGEMENT_SUBTYPE_PROBE_RESPONSE,
+					zero_addr, ptr, sizeof(probe_resp));
+		ptr += ap_build_beacon_pr_tail(ap,
+					MPDU_MANAGEMENT_SUBTYPE_PROBE_RESPONSE,
+					NULL, 0, ptr, sizeof(probe_resp) -
+					(ptr - probe_resp));
+
+		l_genl_msg_append_attr(cmd, NL80211_ATTR_PROBE_RESP,
+					ptr - probe_resp, probe_resp);
+	}
 
 	if (wiphy_has_ext_feature(wiphy,
 			NL80211_EXT_FEATURE_CONTROL_PORT_OVER_NL80211)) {
@@ -2609,6 +2995,9 @@ static void ap_handle_new_station(struct
 
 	l_queue_push_tail(ap->sta_states, sta);
 
+	if (ap->supports_ht)
+		ap_update_beacon(ap);
+
 	msg = nl80211_build_set_station_unauthorized(
 					netdev_get_ifindex(ap->netdev), mac);
 
@@ -3115,12 +3504,107 @@ static bool ap_load_psk(struct ap_state
 	return true;
 }
 
+/*
+ * Note: only PTK/GTK ciphers are supported here since this is all these are
+ *       used for.
+ */
+static enum ie_rsn_cipher_suite ap_string_to_cipher(const char *str)
+{
+	if (!strcmp(str, "UseGroupCipher"))
+		return IE_RSN_CIPHER_SUITE_USE_GROUP_CIPHER;
+	else if (!strcmp(str, "TKIP"))
+		return IE_RSN_CIPHER_SUITE_TKIP;
+	else if (!strcmp(str, "CCMP-128") || !strcmp(str, "CCMP"))
+		return IE_RSN_CIPHER_SUITE_CCMP;
+	else if (!strcmp(str, "GCMP-128") || !strcmp(str, "GCMP"))
+		return IE_RSN_CIPHER_SUITE_GCMP;
+	else if (!strcmp(str, "GCMP-256"))
+		return IE_RSN_CIPHER_SUITE_GCMP_256;
+	else if (!strcmp(str, "CCMP-256"))
+		return IE_RSN_CIPHER_SUITE_CCMP_256;
+	else
+		return 0;
+}
+
+static char **ap_ciphers_to_strv(uint16_t ciphers)
+{
+	uint16_t i;
+	char **list = l_strv_new();
+
+	for (i = 0; i < 16; i++) {
+		if (!(ciphers & (1 << i)))
+			continue;
+
+		list = l_strv_append(list,
+					ie_rsn_cipher_suite_to_string(1 << i));
+	}
+
+	return list;
+}
+
+static bool ap_validate_band_channel(struct ap_state *ap)
+{
+	struct wiphy *wiphy = netdev_get_wiphy(ap->netdev);
+	uint32_t freq;
+	const struct band_freq_attrs *attr;
+
+	if (!(wiphy_get_supported_bands(wiphy) & ap->band)) {
+		l_error("AP hardware does not support band");
+		return -EINVAL;
+	}
+
+	freq = band_channel_to_freq(ap->channel, ap->band);
+	if (!freq) {
+		l_error("AP invalid band (%s) and channel (%u) combination",
+			(ap->band & BAND_FREQ_5_GHZ) ? "5Ghz" : "2.4GHz",
+			ap->channel);
+		return false;
+	}
+
+	attr = wiphy_get_frequency_info(wiphy, freq);
+	if (!attr || attr->disabled) {
+		l_error("AP frequency %u disabled or unsupported", freq);
+		return false;
+	}
+
+	if (ap->supports_ht) {
+		if (band_freq_to_ht_chandef(freq, attr, &ap->chandef) < 0) {
+			/*
+			 * This is unlikely ever to fail since there are no
+			 * 20Mhz restrictions, but just in case fall back to
+			 * non-HT.
+			 */
+			ap->supports_ht = false;
+
+			l_warn("AP could not find HT chandef for frequency %u"
+				" using 20Mhz no-HT", freq);
+
+			goto no_ht;
+		}
+	} else {
+no_ht:
+		ap->chandef.frequency = freq;
+		ap->chandef.channel_width = BAND_CHANDEF_WIDTH_20NOHT;
+	}
+
+	l_debug("AP using frequency %u and channel width %s",
+			ap->chandef.frequency,
+			band_chandef_width_to_string(
+				ap->chandef.channel_width));
+
+	return true;
+}
+
 static int ap_load_config(struct ap_state *ap, const struct l_settings *config,
 				bool *out_cck_rates)
 {
+	struct wiphy *wiphy = netdev_get_wiphy(ap->netdev);
 	size_t len;
 	L_AUTO_FREE_VAR(char *, strval) = NULL;
+	_auto_(l_strv_free) char **ciphers_str = NULL;
+	uint16_t cipher_mask;
 	int err;
+	int i;
 
 	strval = l_settings_get_string(config, "General", "SSID");
 	if (L_WARN_ON(!strval))
@@ -3155,17 +3639,34 @@ static int ap_load_config(struct ap_stat
 		unsigned int uintval;
 
 		if (!l_settings_get_uint(config, "General", "Channel",
-						&uintval) ||
-				!band_channel_to_freq(uintval,
-							BAND_FREQ_2_4_GHZ)) {
+						&uintval)) {
 			l_error("AP Channel value unsupported");
 			return -EINVAL;
 		}
 
 		ap->channel = uintval;
-	} else
+
+		/*
+		 * 6GHz is not supported so we can use only a channel number to
+		 * distinguish between 2.4 and 5GHz.
+		 */
+		if (ap->channel >= 36)
+			ap->band = BAND_FREQ_5_GHZ;
+		else
+			ap->band = BAND_FREQ_2_4_GHZ;
+	} else {
 		/* TODO: Start a Get Survey to decide the channel */
 		ap->channel = 6;
+		ap->band = BAND_FREQ_2_4_GHZ;
+	}
+
+	ap->supports_ht = wiphy_get_ht_capabilities(wiphy, ap->band,
+							NULL) != NULL;
+
+	if (!ap_validate_band_channel(ap)) {
+		l_error("AP Band and Channel combination invalid");
+		return -EINVAL;
+	}
 
 	strval = l_settings_get_string(config, "WSC", "DeviceName");
 	if (strval) {
@@ -3195,6 +3696,8 @@ static int ap_load_config(struct ap_stat
 			l_error("AP [WSC].PrimaryDeviceType format unknown");
 			return -EINVAL;
 		}
+
+		l_free(l_steal_ptr(strval));
 	} else {
 		/* Make ourselves a WFA standard PC by default */
 		ap->wsc_primary_device_type.category = 1;
@@ -3229,7 +3732,26 @@ static int ap_load_config(struct ap_stat
 		l_strfreev(strvval);
 	}
 
-	if (l_settings_get_value(config, "General", "NoCCKRates")) {
+	if (l_settings_has_key(config, "General", "RekeyTimeout")) {
+		unsigned int uintval;
+
+		if (!l_settings_get_uint(config, "General",
+						"RekeyTimeout", &uintval)) {
+			l_error("AP [General].RekeyTimeout is not valid");
+			return -EINVAL;
+		}
+
+		ap->rekey_time = uintval * L_USEC_PER_SEC;
+	} else
+		ap->rekey_time = 0;
+
+	/*
+	 * Since 5GHz won't ever support only CCK rates we can ignore this
+	 * setting on that band.
+	 */
+	if (ap->band & BAND_FREQ_5_GHZ)
+		*out_cck_rates = false;
+	else if (l_settings_get_value(config, "General", "NoCCKRates")) {
 		bool boolval;
 
 		if (!l_settings_get_bool(config, "General", "NoCCKRates",
@@ -3243,6 +3765,61 @@ static int ap_load_config(struct ap_stat
 	} else
 		*out_cck_rates = true;
 
+	cipher_mask = wiphy_get_supported_ciphers(wiphy, IE_GROUP_CIPHERS);
+
+	/* If the config sets a group cipher use that directly */
+	strval = l_settings_get_string(config, "Security", "GroupCipher");
+	if (strval) {
+		enum ie_rsn_cipher_suite cipher = ap_string_to_cipher(strval);
+
+		if (!cipher || !(cipher & cipher_mask)) {
+			l_error("Unsupported or unknown group cipher %s",
+					strval);
+			return -ENOTSUP;
+		}
+
+		ap->group_cipher = cipher;
+		l_free(l_steal_ptr(strval));
+	} else {
+		/* No config override, use CCMP (or TKIP if not supported) */
+		if (cipher_mask & IE_RSN_CIPHER_SUITE_CCMP)
+			ap->group_cipher = IE_RSN_CIPHER_SUITE_CCMP;
+		else
+			ap->group_cipher = IE_RSN_CIPHER_SUITE_TKIP;
+	}
+
+	cipher_mask = wiphy_get_supported_ciphers(wiphy, IE_PAIRWISE_CIPHERS);
+
+	ciphers_str = l_settings_get_string_list(config, "Security",
+						"PairwiseCiphers", ',');
+	for (i = 0; ciphers_str && ciphers_str[i]; i++) {
+		enum ie_rsn_cipher_suite cipher =
+					ap_string_to_cipher(ciphers_str[i]);
+
+		/*
+		 * Constrain list to only values in both supported ciphers and
+		 * the cipher list provided.
+		 */
+		if (!cipher || !(cipher & cipher_mask)) {
+			l_error("Unsupported or unknown pairwise cipher %s",
+					ciphers_str[i]);
+			return -ENOTSUP;
+		}
+
+		ap->ciphers |= cipher;
+	}
+
+	if (!ap->ciphers) {
+		/*
+		 * Default behavior if no ciphers are specified, disable TKIP
+		 * for security if CCMP is available
+		 */
+		if (cipher_mask & IE_RSN_CIPHER_SUITE_CCMP)
+			cipher_mask &= ~IE_RSN_CIPHER_SUITE_TKIP;
+
+		ap->ciphers = cipher_mask;
+	}
+
 	return 0;
 }
 
@@ -3265,6 +3842,9 @@ struct ap_state *ap_start(struct netdev
 	uint64_t wdev_id = netdev_get_wdev_id(netdev);
 	int err;
 	bool cck_rates = true;
+	const uint8_t *rates;
+	unsigned int num_rates;
+	unsigned int i;
 
 	if (L_WARN_ON(!config)) {
 		if (err_out)
@@ -3285,30 +3865,22 @@ struct ap_state *ap_start(struct netdev
 
 	err = -EINVAL;
 
-	/* TODO: Add all ciphers supported by wiphy */
-	ap->ciphers = wiphy_select_cipher(wiphy, 0xffff);
-	ap->group_cipher = wiphy_select_cipher(wiphy, 0xffff);
 	ap->beacon_interval = 100;
 	ap->networks = l_queue_new();
 
 	wsc_uuid_from_addr(netdev_get_address(netdev), ap->wsc_uuid_r);
 
+	rates = wiphy_get_supported_rates(wiphy, ap->band, &num_rates);
+	if (!rates)
+		goto error;
+
 	ap->rates = l_uintset_new(200);
 
-	/* TODO: Pick from actual supported rates */
-	if (!cck_rates) {
-		l_uintset_put(ap->rates, 12); /* 6 Mbps*/
-		l_uintset_put(ap->rates, 18); /* 9 Mbps*/
-		l_uintset_put(ap->rates, 24); /* 12 Mbps*/
-		l_uintset_put(ap->rates, 36); /* 18 Mbps*/
-		l_uintset_put(ap->rates, 48); /* 24 Mbps*/
-		l_uintset_put(ap->rates, 72); /* 36 Mbps*/
-		l_uintset_put(ap->rates, 96); /* 48 Mbps*/
-		l_uintset_put(ap->rates, 108); /* 54 Mbps*/
-	} else {
-		l_uintset_put(ap->rates, 2); /* 1 Mbps*/
-		l_uintset_put(ap->rates, 11); /* 5.5 Mbps*/
-		l_uintset_put(ap->rates, 22); /* 11 Mbps*/
+	for (i = 0; i < num_rates; i++) {
+		if (cck_rates && !L_IN_SET(rates[i], 2, 4, 11, 22))
+			continue;
+
+		l_uintset_put(ap->rates, rates[i]);
 	}
 
 	if (!frame_watch_add(wdev_id, 0, 0x0000 |
@@ -3321,10 +3893,12 @@ struct ap_state *ap_start(struct netdev
 			NULL, 0, ap_reassoc_req_cb, ap, NULL))
 		goto error;
 
-	if (!frame_watch_add(wdev_id, 0, 0x0000 |
+	if (!wiphy_supports_probe_resp_offload(wiphy)) {
+		if (!frame_watch_add(wdev_id, 0, 0x0000 |
 				(MPDU_MANAGEMENT_SUBTYPE_PROBE_REQUEST << 4),
 				NULL, 0, ap_probe_req_cb, ap, NULL))
-		goto error;
+			goto error;
+	}
 
 	if (!frame_watch_add(wdev_id, 0, 0x0000 |
 				(MPDU_MANAGEMENT_SUBTYPE_DISASSOCIATION << 4),
@@ -3499,6 +4073,9 @@ bool ap_station_disconnect(struct ap_sta
 	if (!sta)
 		return false;
 
+	if (ap->supports_ht)
+		ap_update_beacon(ap);
+
 	ap_del_station(sta, reason, false);
 	ap_sta_free(sta);
 	return true;
@@ -3554,6 +4131,28 @@ struct ap_if_data {
 	struct l_dbus_message *pending;
 };
 
+static void ap_properties_changed(struct ap_if_data *ap_if)
+{
+	l_dbus_property_changed(dbus_get_bus(),
+				netdev_get_path(ap_if->netdev),
+				IWD_AP_INTERFACE, "Started");
+	l_dbus_property_changed(dbus_get_bus(),
+				netdev_get_path(ap_if->netdev),
+				IWD_AP_INTERFACE, "Name");
+	l_dbus_property_changed(dbus_get_bus(),
+				netdev_get_path(ap_if->netdev),
+				IWD_AP_INTERFACE, "Frequency");
+	l_dbus_property_changed(dbus_get_bus(),
+				netdev_get_path(ap_if->netdev),
+				IWD_AP_INTERFACE, "PairwiseCiphers");
+	l_dbus_property_changed(dbus_get_bus(),
+				netdev_get_path(ap_if->netdev),
+				IWD_AP_INTERFACE, "GroupCipher");
+	l_dbus_property_changed(dbus_get_bus(),
+				netdev_get_path(ap_if->netdev),
+				IWD_AP_INTERFACE, "Scanning");
+}
+
 static void ap_if_event_func(enum ap_event_type type, const void *event_data,
 				void *user_data)
 {
@@ -3585,12 +4184,8 @@ static void ap_if_event_func(enum ap_eve
 
 		reply = l_dbus_message_new_method_return(ap_if->pending);
 		dbus_pending_reply(&ap_if->pending, reply);
-		l_dbus_property_changed(dbus_get_bus(),
-					netdev_get_path(ap_if->netdev),
-					IWD_AP_INTERFACE, "Started");
-		l_dbus_property_changed(dbus_get_bus(),
-					netdev_get_path(ap_if->netdev),
-					IWD_AP_INTERFACE, "Name");
+
+		ap_properties_changed(ap_if);
 
 		l_rtnl_set_linkmode_and_operstate(rtnl,
 					netdev_get_ifindex(ap_if->netdev),
@@ -3603,12 +4198,7 @@ static void ap_if_event_func(enum ap_eve
 						netdev_get_path(ap_if->netdev),
 						IWD_AP_DIAGNOSTIC_INTERFACE);
 
-		l_dbus_property_changed(dbus_get_bus(),
-					netdev_get_path(ap_if->netdev),
-					IWD_AP_INTERFACE, "Started");
-		l_dbus_property_changed(dbus_get_bus(),
-					netdev_get_path(ap_if->netdev),
-					IWD_AP_INTERFACE, "Name");
+		ap_properties_changed(ap_if);
 
 		l_rtnl_set_linkmode_and_operstate(rtnl,
 					netdev_get_ifindex(ap_if->netdev),
@@ -3941,6 +4531,70 @@ static bool ap_dbus_property_get_scannin
 	return true;
 }
 
+static bool ap_dbus_property_get_freq(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_builder *builder,
+					void *user_data)
+{
+	struct ap_if_data *ap_if = user_data;
+	uint32_t freq;
+
+	if (!ap_if->ap || !ap_if->ap->started)
+		return false;
+
+	freq = band_channel_to_freq(ap_if->ap->channel, ap_if->ap->band);
+
+	l_dbus_message_builder_append_basic(builder, 'u', &freq);
+
+	return true;
+}
+
+static bool ap_dbus_property_get_pairwise(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_builder *builder,
+					void *user_data)
+{
+	struct ap_if_data *ap_if = user_data;
+	char **ciphers;
+	size_t i;
+
+	if (!ap_if->ap || !ap_if->ap->started)
+		return false;
+
+	ciphers = ap_ciphers_to_strv(ap_if->ap->ciphers);
+
+	l_dbus_message_builder_enter_array(builder, "s");
+
+	for (i = 0; ciphers[i]; i++)
+		l_dbus_message_builder_append_basic(builder, 's', ciphers[i]);
+
+	l_dbus_message_builder_leave_array(builder);
+
+	l_strv_free(ciphers);
+
+	return true;
+}
+
+static bool ap_dbus_property_get_group(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_builder *builder,
+					void *user_data)
+{
+	struct ap_if_data *ap_if = user_data;
+	char **cipher;
+
+	if (!ap_if->ap || !ap_if->ap->started)
+		return false;
+
+	cipher = ap_ciphers_to_strv(ap_if->ap->group_cipher);
+
+	/* Group cipher will only ever be a single value */
+	l_dbus_message_builder_append_basic(builder, 's', cipher[0]);
+	l_strv_free(cipher);
+
+	return true;
+}
+
 static void ap_setup_interface(struct l_dbus_interface *interface)
 {
 	l_dbus_interface_method(interface, "Start", 0, ap_dbus_start, "",
@@ -3960,6 +4614,12 @@ static void ap_setup_interface(struct l_
 					ap_dbus_property_get_name, NULL);
 	l_dbus_interface_property(interface, "Scanning", 0, "b",
 					ap_dbus_property_get_scanning, NULL);
+	l_dbus_interface_property(interface, "Frequency", 0, "u",
+					ap_dbus_property_get_freq, NULL);
+	l_dbus_interface_property(interface, "PairwiseCiphers", 0, "as",
+					ap_dbus_property_get_pairwise, NULL);
+	l_dbus_interface_property(interface, "GroupCipher", 0, "s",
+					ap_dbus_property_get_group, NULL);
 }
 
 static void ap_destroy_interface(void *user_data)
@@ -4124,32 +4784,14 @@ static int ap_init(void)
 	 * [General].EnableNetworkConfiguration is true.
 	 */
 	if (netconfig_enabled()) {
-		if (l_settings_get_value(settings, "IPv4", "APAddressPool")) {
-			global_addr4_strs = l_settings_get_string_list(settings,
-								"IPv4",
-								"APAddressPool",
-								',');
-			if (!global_addr4_strs || !*global_addr4_strs) {
-				l_error("Can't parse the [IPv4].APAddressPool "
+		global_addr4_strs =
+			l_settings_get_string_list(settings, "IPv4",
+							"APAddressPool", ',');
+		if (global_addr4_strs && !global_addr4_strs[0]) {
+			l_error("Can't parse the [IPv4].APAddressPool "
 					"setting as a string list");
-				l_strv_free(global_addr4_strs);
-				global_addr4_strs = NULL;
-			}
-		} else if (l_settings_get_value(settings,
-						"General", "APRanges")) {
-			l_warn("The [General].APRanges setting is deprecated, "
-				"use [IPv4].APAddressPool instead");
-
-			global_addr4_strs = l_settings_get_string_list(settings,
-								"General",
-								"APRanges",
-								',');
-			if (!global_addr4_strs || !*global_addr4_strs) {
-				l_error("Can't parse the [General].APRanges "
-					"setting as a string list");
-				l_strv_free(global_addr4_strs);
-				global_addr4_strs = NULL;
-			}
+			l_strv_free(global_addr4_strs);
+			global_addr4_strs = NULL;
 		}
 
 		/* Fall back to 192.168.0.0/16 */
diff -pruN 1.30-1/src/band.c 2.3-1/src/band.c
--- 1.30-1/src/band.c	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/src/band.c	2023-01-23 18:46:38.000000000 +0000
@@ -36,6 +36,8 @@ void band_free(struct band *band)
 	if (band->he_capabilities)
 		l_queue_destroy(band->he_capabilities, l_free);
 
+	l_free(band->freq_attrs);
+
 	l_free(band);
 }
 
@@ -1192,8 +1194,97 @@ int oci_from_chandef(const struct band_c
 	return -ENOENT;
 }
 
+/* Find an HT chandef for the frequency */
+int band_freq_to_ht_chandef(uint32_t freq, const struct band_freq_attrs *attr,
+				struct band_chandef *chandef)
+{
+	enum band_freq band;
+	enum band_chandef_width width;
+	unsigned int i;
+	const struct operating_class_info *best = NULL;
+
+	if (attr->disabled || !attr->supported)
+		return -EINVAL;
+
+	if (!band_freq_to_channel(freq, &band))
+		return -EINVAL;
+
+	for (i = 0; i < L_ARRAY_SIZE(e4_operating_classes); i++) {
+		const struct operating_class_info *info =
+						&e4_operating_classes[i];
+		enum band_chandef_width w;
+
+		if (e4_has_frequency(info, freq) < 0)
+			continue;
+
+		/* Any restrictions for this channel width? */
+		switch (info->channel_spacing) {
+		case 20:
+			w = BAND_CHANDEF_WIDTH_20;
+			break;
+		case 40:
+			w = BAND_CHANDEF_WIDTH_40;
+
+			/* 6GHz remove the upper/lower 40mhz channel concept */
+			if (band == BAND_FREQ_6_GHZ)
+				break;
+
+			if (info->flags & PRIMARY_CHANNEL_UPPER &&
+						attr->no_ht40_plus)
+				continue;
+
+			if (info->flags & PRIMARY_CHANNEL_LOWER &&
+						attr->no_ht40_minus)
+				continue;
+
+			break;
+		default:
+			continue;
+		}
+
+		if (!best || best->channel_spacing < info->channel_spacing) {
+			best = info;
+			width = w;
+		}
+	}
+
+	if (!best)
+		return -ENOENT;
+
+	chandef->frequency = freq;
+	chandef->channel_width = width;
+
+	/*
+	 * Choose a secondary channel frequency:
+	 * - 20mhz no secondary
+	 * - 40mhz we can base the selection off the channel flags, either
+	 *   higher or lower.
+	 */
+	switch (width) {
+	case BAND_CHANDEF_WIDTH_20:
+		return 0;
+	case BAND_CHANDEF_WIDTH_40:
+		if (band == BAND_FREQ_6_GHZ)
+			return 0;
+
+		if (best->flags & PRIMARY_CHANNEL_UPPER)
+			chandef->center1_frequency = freq - 10;
+		else
+			chandef->center1_frequency = freq + 10;
+
+		return 0;
+	default:
+		/* Should never happen */
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
 uint8_t band_freq_to_channel(uint32_t freq, enum band_freq *out_band)
 {
+	unsigned int i;
+	enum band_freq band = 0;
 	uint32_t channel = 0;
 
 	if (freq >= 2412 && freq <= 2484) {
@@ -1208,10 +1299,8 @@ uint8_t band_freq_to_channel(uint32_t fr
 			channel /= 5;
 		}
 
-		if (out_band)
-			*out_band = BAND_FREQ_2_4_GHZ;
-
-		return channel;
+		band = BAND_FREQ_2_4_GHZ;
+		goto check_e4;
 	}
 
 	if (freq >= 5005 && freq < 5900) {
@@ -1220,10 +1309,9 @@ uint8_t band_freq_to_channel(uint32_t fr
 
 		channel = (freq - 5000) / 5;
 
-		if (out_band)
-			*out_band = BAND_FREQ_5_GHZ;
+		band = BAND_FREQ_5_GHZ;
 
-		return channel;
+		goto check_e4;
 	}
 
 	if (freq >= 4905 && freq < 5000) {
@@ -1232,10 +1320,9 @@ uint8_t band_freq_to_channel(uint32_t fr
 
 		channel = (freq - 4000) / 5;
 
-		if (out_band)
-			*out_band = BAND_FREQ_5_GHZ;
+		band = BAND_FREQ_5_GHZ;
 
-		return channel;
+		goto check_e4;
 	}
 
 	if (freq > 5950 && freq <= 7115) {
@@ -1244,17 +1331,31 @@ uint8_t band_freq_to_channel(uint32_t fr
 
 		channel = (freq - 5950) / 5;
 
-		if (out_band)
-			*out_band = BAND_FREQ_6_GHZ;
+		band = BAND_FREQ_6_GHZ;
 
-		return channel;
+		goto check_e4;
 	}
 
 	if (freq == 5935) {
-		if (out_band)
-			*out_band = BAND_FREQ_6_GHZ;
+		band = BAND_FREQ_6_GHZ;
+		channel = 2;
+	}
 
-		return 2;
+	if (!band || !channel)
+		return 0;
+
+check_e4:
+	for (i = 0; i < L_ARRAY_SIZE(e4_operating_classes); i++) {
+		const struct operating_class_info *info =
+						&e4_operating_classes[i];
+
+		if (e4_has_frequency(info, freq) == 0 ||
+					e4_has_ccfi(info, freq) == 0) {
+			if (out_band)
+				*out_band = band;
+
+			return channel;
+		}
 	}
 
 	return 0;
@@ -1262,26 +1363,33 @@ uint8_t band_freq_to_channel(uint32_t fr
 
 uint32_t band_channel_to_freq(uint8_t channel, enum band_freq band)
 {
+	unsigned int i;
+	uint32_t freq = 0;
+
 	if (band == BAND_FREQ_2_4_GHZ) {
 		if (channel >= 1 && channel <= 13)
-			return 2407 + 5 * channel;
+			freq = 2407 + 5 * channel;
+		else if (channel == 14)
+			freq = 2484;
 
-		if (channel == 14)
-			return 2484;
+		goto check_e4;
 	}
 
 	if (band == BAND_FREQ_5_GHZ) {
 		if (channel >= 1 && channel <= 179)
-			return 5000 + 5 * channel;
+			freq = 5000 + 5 * channel;
+		else if (channel >= 181 && channel <= 199)
+			freq = 4000 + 5 * channel;
 
-		if (channel >= 181 && channel <= 199)
-			return 4000 + 5 * channel;
+		goto check_e4;
 	}
 
 	if (band == BAND_FREQ_6_GHZ) {
 		/* operating class 136 */
-		if (channel == 2)
-			return 5935;
+		if (channel == 2) {
+			freq = 5935;
+			goto check_e4;
+		}
 
 		/* Channels increment by 4, starting with 1 */
 		if (channel % 4 != 1)
@@ -1291,7 +1399,17 @@ uint32_t band_channel_to_freq(uint8_t ch
 			return 0;
 
 		/* operating classes 131, 132, 133, 134, 135 */
-		return 5950 + 5 * channel;
+		freq = 5950 + 5 * channel;
+	}
+
+check_e4:
+	for (i = 0; i < L_ARRAY_SIZE(e4_operating_classes); i++) {
+		const struct operating_class_info *info =
+						&e4_operating_classes[i];
+
+		if (e4_has_frequency(info, freq) == 0 ||
+					e4_has_ccfi(info, freq) == 0)
+			return freq;
 	}
 
 	return 0;
@@ -1420,3 +1538,23 @@ enum band_freq band_oper_class_to_band(c
 	else
 		return 0;
 }
+
+const char *band_chandef_width_to_string(enum band_chandef_width width)
+{
+	switch (width) {
+	case BAND_CHANDEF_WIDTH_20NOHT:
+		return "20MHz (no-HT)";
+	case BAND_CHANDEF_WIDTH_20:
+		return "20MHz";
+	case BAND_CHANDEF_WIDTH_40:
+		return "40MHz";
+	case BAND_CHANDEF_WIDTH_80:
+		return "80MHz";
+	case BAND_CHANDEF_WIDTH_80P80:
+		return "80+80MHz";
+	case BAND_CHANDEF_WIDTH_160:
+		return "160MHz";
+	}
+
+	return NULL;
+}
diff -pruN 1.30-1/src/band.h 2.3-1/src/band.h
--- 1.30-1/src/band.h	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/src/band.h	2023-01-23 18:46:38.000000000 +0000
@@ -55,8 +55,22 @@ struct band_he_capabilities {
 	uint8_t he_mcs_set[12];
 };
 
+struct band_freq_attrs {
+	uint8_t tx_power;
+	bool supported : 1;
+	bool disabled : 1;
+	bool no_ir : 1;
+	bool no_ht40_plus : 1;
+	bool no_ht40_minus : 1;
+	bool no_80mhz : 1;
+	bool no_160mhz : 1;
+	bool no_he : 1;
+} __attribute__ ((packed));
+
 struct band {
 	enum band_freq freq;
+	struct band_freq_attrs *freq_attrs;
+	size_t freqs_len;
 	/* Each entry is type struct band_he_capabilities */
 	struct l_queue *he_capabilities;
 	uint8_t vht_mcs_set[8];
@@ -64,6 +78,7 @@ struct band {
 	bool vht_supported : 1;
 	uint8_t ht_mcs_set[16];
 	uint8_t ht_capabilities[2];
+	uint8_t ht_ampdu_params;
 	bool ht_supported : 1;
 	uint16_t supported_rates_len;
 	uint8_t supported_rates[];
@@ -87,6 +102,8 @@ int band_estimate_nonht_rate(const struc
 				const uint8_t *supported_rates,
 				const uint8_t *ext_supported_rates,
 				int32_t rssi, uint64_t *out_data_rate);
+int band_freq_to_ht_chandef(uint32_t freq, const struct band_freq_attrs *attr,
+				struct band_chandef *chandef);
 
 int oci_to_frequency(uint32_t operating_class, uint32_t channel);
 
@@ -97,3 +114,4 @@ uint8_t band_freq_to_channel(uint32_t fr
 uint32_t band_channel_to_freq(uint8_t channel, enum band_freq band);
 enum band_freq band_oper_class_to_band(const uint8_t *country,
 					uint8_t oper_class);
+const char *band_chandef_width_to_string(enum band_chandef_width width);
diff -pruN 1.30-1/src/crypto.c 2.3-1/src/crypto.c
--- 1.30-1/src/crypto.c	2022-01-24 21:28:47.000000000 +0000
+++ 2.3-1/src/crypto.c	2022-11-18 12:31:49.000000000 +0000
@@ -500,9 +500,17 @@ int crypto_cipher_key_len(enum crypto_ci
 	case CRYPTO_CIPHER_TKIP:
 		return 32;
 	case CRYPTO_CIPHER_CCMP:
+	case CRYPTO_CIPHER_GCMP:
 		return 16;
-	case CRYPTO_CIPHER_BIP:
+	case CRYPTO_CIPHER_CCMP_256:
+	case CRYPTO_CIPHER_GCMP_256:
+		return 32;
+	case CRYPTO_CIPHER_BIP_CMAC:
+	case CRYPTO_CIPHER_BIP_GMAC:
 		return 16;
+	case CRYPTO_CIPHER_BIP_CMAC_256:
+	case CRYPTO_CIPHER_BIP_GMAC_256:
+		return 32;
 	}
 
 	return 0;
diff -pruN 1.30-1/src/crypto.h 2.3-1/src/crypto.h
--- 1.30-1/src/crypto.h	2022-01-05 21:23:11.000000000 +0000
+++ 2.3-1/src/crypto.h	2022-11-18 12:31:49.000000000 +0000
@@ -30,7 +30,13 @@ enum crypto_cipher {
 	CRYPTO_CIPHER_WEP104 = 0x000fac05,
 	CRYPTO_CIPHER_TKIP = 0x000fac02,
 	CRYPTO_CIPHER_CCMP = 0x000fac04,
-	CRYPTO_CIPHER_BIP = 0x000fac06,
+	CRYPTO_CIPHER_BIP_CMAC = 0x000fac06,
+	CRYPTO_CIPHER_GCMP = 0x000fac08,
+	CRYPTO_CIPHER_GCMP_256 = 0x000fac09,
+	CRYPTO_CIPHER_CCMP_256 = 0x000fac0a,
+	CRYPTO_CIPHER_BIP_GMAC = 0x000fac0b,
+	CRYPTO_CIPHER_BIP_GMAC_256 = 0x000fac0c,
+	CRYPTO_CIPHER_BIP_CMAC_256 = 0x000fac0d,
 };
 
 enum crypto_akm {
diff -pruN 1.30-1/src/dbus.h 2.3-1/src/dbus.h
--- 1.30-1/src/dbus.h	2022-01-05 21:23:11.000000000 +0000
+++ 2.3-1/src/dbus.h	2022-11-18 12:31:49.000000000 +0000
@@ -44,6 +44,8 @@
 #define IWD_AP_DIAGNOSTIC_INTERFACE "net.connman.iwd.AccessPointDiagnostic"
 #define IWD_STATION_DEBUG_INTERFACE "net.connman.iwd.StationDebug"
 #define IWD_DPP_INTERFACE "net.connman.iwd.DeviceProvisioning"
+#define IWD_NETCONFIG_AGENT_INTERFACE \
+	"net.connman.iwd.NetworkConfigurationAgent"
 
 #define IWD_BASE_PATH "/net/connman/iwd"
 #define IWD_AGENT_MANAGER_PATH IWD_BASE_PATH
diff -pruN 1.30-1/src/dpp.c 2.3-1/src/dpp.c
--- 1.30-1/src/dpp.c	2022-07-15 17:08:06.000000000 +0000
+++ 2.3-1/src/dpp.c	2022-11-18 12:31:49.000000000 +0000
@@ -1622,6 +1622,7 @@ static void dpp_start_offchannel(struct
 	 * called).
 	 */
 	uint32_t id = offchannel_start(netdev_get_wdev_id(dpp->netdev),
+				WIPHY_WORK_PRIORITY_OFFCHANNEL,
 				freq, dpp->dwell, dpp_roc_started,
 				dpp, dpp_presence_timeout);
 
diff -pruN 1.30-1/src/eap.c 2.3-1/src/eap.c
--- 1.30-1/src/eap.c	2022-07-15 17:08:06.000000000 +0000
+++ 2.3-1/src/eap.c	2022-11-18 12:31:49.000000000 +0000
@@ -59,6 +59,7 @@ struct eap_state {
 	char *identity;
 	char *identity_setting;
 	bool authenticator;
+	char *peer_id;
 
 	int last_id;
 	void *method_state;
@@ -154,6 +155,7 @@ void eap_free(struct eap_state *eap)
 
 	eap_free_common(eap);
 	l_timeout_remove(eap->complete_timeout);
+	eap_set_peer_id(eap, NULL);
 
 	l_free(eap);
 }
@@ -837,6 +839,17 @@ err:
 	return false;
 }
 
+void eap_set_peer_id(struct eap_state *eap, const char *id)
+{
+	l_free(eap->peer_id);
+	eap->peer_id = l_strdup(id);
+}
+
+const char *eap_get_peer_id(struct eap_state *eap)
+{
+	return eap->peer_id;
+}
+
 void eap_set_data(struct eap_state *eap, void *data)
 {
 	eap->method_state = data;
diff -pruN 1.30-1/src/eap.h 2.3-1/src/eap.h
--- 1.30-1/src/eap.h	2020-09-05 07:43:41.000000000 +0000
+++ 2.3-1/src/eap.h	2022-11-18 12:31:49.000000000 +0000
@@ -94,6 +94,9 @@ const char *eap_get_identity(struct eap_
 
 void eap_rx_packet(struct eap_state *eap, const uint8_t *pkt, size_t len);
 
+void eap_set_peer_id(struct eap_state *eap, const char *id);
+const char *eap_get_peer_id(struct eap_state *eap);
+
 void __eap_set_config(struct l_settings *config);
 
 int eap_init(void);
diff -pruN 1.30-1/src/eapol.c 2.3-1/src/eapol.c
--- 1.30-1/src/eapol.c	2022-07-15 17:08:06.000000000 +0000
+++ 2.3-1/src/eapol.c	2023-01-23 18:46:38.000000000 +0000
@@ -390,11 +390,31 @@ static int eapol_encrypt_key_data(const
 				size_t key_data_len,
 				struct eapol_key *out_frame, size_t mic_len)
 {
+	uint8_t key[32];
+	bool ret;
+
 	switch (out_frame->key_descriptor_version) {
 	case EAPOL_KEY_DESCRIPTOR_VERSION_HMAC_MD5_ARC4:
-		/* Not supported */
-		return -ENOTSUP;
+		/*
+		 * Not following the spec to generate the IV. The spec outlines
+		 * a procedure where a 32 byte buffer is held and incremented
+		 * each time nonces are created, and the IV comes from this
+		 * buffer. In the end randomizing the IV every time should be
+		 * just as good. This is how we handle the GTK in AP mode.
+		 */
+		l_getrandom(out_frame->eapol_key_iv, 16);
+
+		memcpy(key, out_frame->eapol_key_iv, 16);
+		memcpy(key + 16, kek, 16);
 
+		ret = arc4_skip(key, 32, 256, key_data, key_data_len,
+				EAPOL_KEY_DATA(out_frame, mic_len));
+		explicit_bzero(key, sizeof(key));
+
+		if (!ret)
+			return -ENOTSUP;
+
+		break;
 	case EAPOL_KEY_DESCRIPTOR_VERSION_HMAC_SHA1_AES:
 	case EAPOL_KEY_DESCRIPTOR_VERSION_AES_128_CMAC_AES:
 		if (key_data_len < 16 || key_data_len % 8)
@@ -492,8 +512,7 @@ bool eapol_verify_ptk_2_of_4(const struc
 	if (!ek->key_mic)
 		return false;
 
-	if (ek->secure != ptk_complete)
-		return false;
+	L_WARN_ON(ek->secure != ptk_complete);
 
 	if (ek->encrypted_key_data)
 		return false;
@@ -746,11 +765,12 @@ struct eapol_key *eapol_create_ptk_2_of_
 				size_t extra_len,
 				const uint8_t *extra_data,
 				bool is_wpa,
-				size_t mic_len)
+				size_t mic_len,
+				bool secure)
 {
-	return eapol_create_common(protocol, version, false, key_replay_counter,
-					snonce, extra_len, extra_data, 1,
-					is_wpa, mic_len);
+	return eapol_create_common(protocol, version, secure,
+					key_replay_counter, snonce, extra_len,
+					extra_data, 1, is_wpa, mic_len);
 }
 
 struct eapol_key *eapol_create_ptk_4_of_4(
@@ -1062,19 +1082,21 @@ static void eapol_send_ptk_1_of_4(struct
 	enum crypto_cipher cipher = ie_rsn_cipher_suite_to_cipher(
 				sm->handshake->pairwise_cipher);
 	uint8_t pmkid[16];
+	uint8_t key_descriptor_version;
 
 	handshake_state_new_anonce(sm->handshake);
 
-	sm->handshake->ptk_complete = false;
-
 	sm->replay_counter++;
 
 	memset(ek, 0, EAPOL_FRAME_LEN(sm->mic_len));
 	ek->header.protocol_version = sm->protocol_version;
 	ek->header.packet_type = 0x3;
 	ek->descriptor_type = EAPOL_DESCRIPTOR_TYPE_80211;
-	/* Must be HMAC-SHA1-128 + AES when using CCMP with PSK or 8021X */
-	ek->key_descriptor_version = EAPOL_KEY_DESCRIPTOR_VERSION_HMAC_SHA1_AES;
+	L_WARN_ON(eapol_key_descriptor_version_from_akm(
+				sm->handshake->akm_suite,
+				sm->handshake->pairwise_cipher,
+				&key_descriptor_version) < 0);
+	ek->key_descriptor_version = key_descriptor_version;
 	ek->key_type = true;
 	ek->key_ack = true;
 	ek->key_length = L_CPU_TO_BE16(crypto_cipher_key_len(cipher));
@@ -1087,6 +1109,13 @@ static void eapol_send_ptk_1_of_4(struct
 
 	eapol_key_data_append(ek, sm->mic_len, HANDSHAKE_KDE_PMKID, pmkid, 16);
 
+	if (sm->handshake->ptk_complete) {
+		sm->rekey = true;
+		sm->handshake->ptk_complete = false;
+	}
+
+	ek->secure = sm->rekey;
+
 	ek->header.packet_len = L_CPU_TO_BE16(EAPOL_FRAME_LEN(sm->mic_len) +
 				EAPOL_KEY_DATA_LEN(ek, sm->mic_len) - 4);
 
@@ -1302,7 +1331,8 @@ static void eapol_handle_ptk_1_of_4(stru
 					ek->key_descriptor_version,
 					L_BE64_TO_CPU(ek->key_replay_counter),
 					sm->handshake->snonce, ies_len, ies,
-					sm->handshake->wpa_ie, sm->mic_len);
+					sm->handshake->wpa_ie, sm->mic_len,
+					sm->rekey);
 
 	kck = handshake_state_get_kck(sm->handshake);
 
@@ -1358,6 +1388,7 @@ static void eapol_send_ptk_3_of_4(struct
 				sm->handshake->group_cipher);
 	const uint8_t *kck;
 	const uint8_t *kek;
+	uint8_t key_descriptor_version;
 
 	sm->replay_counter++;
 
@@ -1365,8 +1396,11 @@ static void eapol_send_ptk_3_of_4(struct
 	ek->header.protocol_version = sm->protocol_version;
 	ek->header.packet_type = 0x3;
 	ek->descriptor_type = EAPOL_DESCRIPTOR_TYPE_80211;
-	/* Must be HMAC-SHA1-128 + AES when using CCMP with PSK or 8021X */
-	ek->key_descriptor_version = EAPOL_KEY_DESCRIPTOR_VERSION_HMAC_SHA1_AES;
+	L_WARN_ON(eapol_key_descriptor_version_from_akm(
+				sm->handshake->akm_suite,
+				sm->handshake->pairwise_cipher,
+				&key_descriptor_version) < 0);
+	ek->key_descriptor_version = key_descriptor_version;
 	ek->key_type = true;
 	ek->install = true;
 	ek->key_ack = true;
@@ -1525,7 +1559,7 @@ static void eapol_handle_ptk_2_of_4(stru
 
 	l_debug("ifindex=%u", sm->handshake->ifindex);
 
-	if (!eapol_verify_ptk_2_of_4(ek, sm->handshake->ptk_complete))
+	if (!eapol_verify_ptk_2_of_4(ek, sm->rekey))
 		return;
 
 	if (L_BE64_TO_CPU(ek->key_replay_counter) != sm->replay_counter)
@@ -1577,12 +1611,18 @@ static void eapol_handle_ptk_2_of_4(stru
 		sm->handshake->support_ip_allocation = ip_req_kde != NULL;
 	}
 
+	/*
+	 * If the snonce is already set don't reset the retry counter as this
+	 * is a rekey. To be safe take the most recent snonce (in this frame)
+	 * in case the station created a new one.
+	 */
+	if (!sm->handshake->have_snonce)
+		sm->frame_retry = 0;
+
 	memcpy(sm->handshake->snonce, ek->key_nonce,
 			sizeof(sm->handshake->snonce));
 	sm->handshake->have_snonce = true;
 
-	sm->frame_retry = 0;
-
 	eapol_ptk_3_of_4_retry(NULL, sm);
 }
 
@@ -2238,12 +2278,14 @@ static void eapol_key_handle(struct eapo
 				const struct eapol_frame *frame,
 				bool unencrypted)
 {
+	struct handshake_state *hs = sm->handshake;
 	const struct eapol_key *ek;
 	const uint8_t *kck;
 	const uint8_t *kek;
 	uint8_t *decrypted_key_data = NULL;
 	size_t key_data_len = 0;
 	uint64_t replay_counter;
+	uint8_t expected_key_descriptor_version;
 
 	ek = eapol_key_validate((const uint8_t *) frame,
 				sizeof(struct eapol_header) +
@@ -2256,11 +2298,19 @@ static void eapol_key_handle(struct eapo
 	if (!ek->key_ack)
 		return;
 
+	if (L_WARN_ON(eapol_key_descriptor_version_from_akm(hs->akm_suite,
+				hs->pairwise_cipher,
+				&expected_key_descriptor_version) < 0))
+		return;
+
+	if (L_WARN_ON(expected_key_descriptor_version !=
+				ek->key_descriptor_version))
+		return;
+
 	/* Further Descriptor Type check */
-	if (!sm->handshake->wpa_ie &&
-			ek->descriptor_type != EAPOL_DESCRIPTOR_TYPE_80211)
+	if (!hs->wpa_ie && ek->descriptor_type != EAPOL_DESCRIPTOR_TYPE_80211)
 		return;
-	else if (sm->handshake->wpa_ie &&
+	else if (hs->wpa_ie &&
 			ek->descriptor_type != EAPOL_DESCRIPTOR_TYPE_WPA)
 		return;
 
@@ -2293,31 +2343,30 @@ static void eapol_key_handle(struct eapo
 	if (sm->have_replay && sm->replay_counter >= replay_counter)
 		return;
 
-	kck = handshake_state_get_kck(sm->handshake);
+	kck = handshake_state_get_kck(hs);
 
 	if (ek->key_mic) {
 		/* Haven't received step 1 yet, so no ptk */
-		if (!sm->handshake->have_snonce)
+		if (!hs->have_snonce)
 			return;
 
-		if (!eapol_verify_mic(sm->handshake->akm_suite, kck, ek,
-					sm->mic_len))
+		if (!eapol_verify_mic(hs->akm_suite, kck, ek, sm->mic_len))
 			return;
 	}
 
-	if ((ek->encrypted_key_data && !sm->handshake->wpa_ie) ||
-			(ek->key_type == 0 && sm->handshake->wpa_ie)) {
+	if ((ek->encrypted_key_data && !hs->wpa_ie) ||
+			(ek->key_type == 0 && hs->wpa_ie)) {
 		/*
 		 * If using a MIC (non-FILS) but haven't received step 1 yet
 		 * we disregard since there will be no ptk
 		 */
-		if (sm->mic_len && !sm->handshake->have_snonce)
+		if (sm->mic_len && !hs->have_snonce)
 			return;
 
-		kek = handshake_state_get_kek(sm->handshake);
+		kek = handshake_state_get_kek(hs);
 
 		decrypted_key_data = eapol_decrypt_key_data(
-					sm->handshake->akm_suite, kek,
+					hs->akm_suite, kek,
 					ek, &key_data_len, sm->mic_len);
 		if (!decrypted_key_data)
 			return;
@@ -2326,11 +2375,10 @@ static void eapol_key_handle(struct eapo
 
 	if (ek->key_type == 0) {
 		/* GTK handshake allowed only after PTK handshake complete */
-		if (!sm->handshake->ptk_complete)
+		if (!hs->ptk_complete)
 			goto done;
 
-		if (sm->handshake->group_cipher ==
-				IE_RSN_CIPHER_SUITE_NO_GROUP_TRAFFIC)
+		if (hs->group_cipher == IE_RSN_CIPHER_SUITE_NO_GROUP_TRAFFIC)
 			goto done;
 
 		if (!decrypted_key_data)
@@ -2408,6 +2456,8 @@ static void eapol_eap_complete_cb(enum e
 
 		/* sm->mic_len will have been set in eapol_eap_results_cb */
 
+		sm->frame_retry = 0;
+
 		/* Kick off 4-Way Handshake */
 		eapol_ptk_1_of_4_retry(NULL, sm);
 	}
@@ -2734,6 +2784,8 @@ void eapol_register(struct eapol_sm *sm)
 bool eapol_start(struct eapol_sm *sm)
 {
 	if (sm->handshake->settings_8021x) {
+		_auto_(l_free) char *network_id = NULL;
+
 		sm->eap = eap_new(eapol_eap_msg_cb, eapol_eap_complete_cb, sm);
 
 		if (!sm->eap)
@@ -2749,6 +2801,10 @@ bool eapol_start(struct eapol_sm *sm)
 
 		eap_set_key_material_func(sm->eap, eapol_eap_results_cb);
 		eap_set_event_func(sm->eap, eapol_eap_event_cb);
+
+		network_id = l_util_hexstring(sm->handshake->ssid,
+						sm->handshake->ssid_len);
+		eap_set_peer_id(sm->eap, network_id);
 	}
 
 	sm->started = true;
@@ -2793,6 +2849,8 @@ bool eapol_start(struct eapol_sm *sm)
 			if (L_WARN_ON(!sm->handshake->have_pmk))
 				return false;
 
+			sm->frame_retry = 0;
+
 			/* Kick off handshake */
 			eapol_ptk_1_of_4_retry(NULL, sm);
 		}
diff -pruN 1.30-1/src/eapol.h 2.3-1/src/eapol.h
--- 1.30-1/src/eapol.h	2022-07-15 17:08:06.000000000 +0000
+++ 2.3-1/src/eapol.h	2023-01-23 18:46:38.000000000 +0000
@@ -83,7 +83,8 @@ struct eapol_key *eapol_create_ptk_2_of_
 				size_t extra_len,
 				const uint8_t *extra_data,
 				bool is_wpa,
-				size_t mic_len);
+				size_t mic_len,
+				bool secure);
 
 struct eapol_key *eapol_create_ptk_4_of_4(
 				enum eapol_protocol_version protocol,
diff -pruN 1.30-1/src/eapolutil.c 2.3-1/src/eapolutil.c
--- 1.30-1/src/eapolutil.c	2021-03-29 13:06:08.000000000 +0000
+++ 2.3-1/src/eapolutil.c	2022-11-18 12:31:49.000000000 +0000
@@ -25,9 +25,11 @@
 #endif
 
 #include <stdio.h>
+#include <errno.h>
 #include <ell/ell.h>
 
 #include "src/eapolutil.h"
+#include "src/ie.h"
 
 const struct eapol_key *eapol_key_validate(const uint8_t *frame, size_t len,
 						size_t mic_len)
@@ -80,3 +82,45 @@ const struct eapol_key *eapol_key_valida
 
 	return ek;
 }
+
+int eapol_key_descriptor_version_from_akm(enum ie_rsn_akm_suite akm,
+					enum ie_rsn_cipher_suite pairwise,
+					uint8_t *outv)
+{
+	/* 802.11-2020 Section 12.7.2 */
+	switch (akm) {
+	case IE_RSN_AKM_SUITE_8021X:
+	case IE_RSN_AKM_SUITE_PSK:
+		if (pairwise == IE_RSN_CIPHER_SUITE_USE_GROUP_CIPHER ||
+				pairwise == IE_RSN_CIPHER_SUITE_TKIP)
+			*outv = EAPOL_KEY_DESCRIPTOR_VERSION_HMAC_MD5_ARC4;
+		else
+			*outv = EAPOL_KEY_DESCRIPTOR_VERSION_HMAC_SHA1_AES;
+
+		return 0;
+	case IE_RSN_AKM_SUITE_FT_OVER_8021X:
+	case IE_RSN_AKM_SUITE_FT_USING_PSK:
+	case IE_RSN_AKM_SUITE_8021X_SHA256:
+	case IE_RSN_AKM_SUITE_PSK_SHA256:
+		*outv = EAPOL_KEY_DESCRIPTOR_VERSION_AES_128_CMAC_AES;
+		return 0;
+	case IE_RSN_AKM_SUITE_SAE_SHA256:
+	case IE_RSN_AKM_SUITE_FT_OVER_SAE_SHA256:
+	case IE_RSN_AKM_SUITE_8021X_SUITE_B_SHA256:
+	case IE_RSN_AKM_SUITE_8021X_SUITE_B_SHA384:
+	case IE_RSN_AKM_SUITE_FT_OVER_8021X_SHA384:
+	case IE_RSN_AKM_SUITE_FILS_SHA256:
+	case IE_RSN_AKM_SUITE_FILS_SHA384:
+	case IE_RSN_AKM_SUITE_FT_OVER_FILS_SHA256:
+	case IE_RSN_AKM_SUITE_FT_OVER_FILS_SHA384:
+	case IE_RSN_AKM_SUITE_OWE:
+	case IE_RSN_AKM_SUITE_OSEN:
+		*outv = EAPOL_KEY_DESCRIPTOR_VERSION_AKM_DEFINED;
+		return 0;
+	case IE_RSN_AKM_SUITE_TDLS:
+	case IE_RSN_AKM_SUITE_AP_PEER_KEY_SHA256:
+		break;
+	}
+
+	return -ENOTSUP;
+};
diff -pruN 1.30-1/src/eapolutil.h 2.3-1/src/eapolutil.h
--- 1.30-1/src/eapolutil.h	2022-01-24 21:28:47.000000000 +0000
+++ 2.3-1/src/eapolutil.h	2022-11-18 12:31:49.000000000 +0000
@@ -25,6 +25,9 @@
 #include <asm/byteorder.h>
 #include <linux/types.h>
 
+enum ie_rsn_akm_suite;
+enum ie_rsn_cipher_suite;
+
 enum eapol_protocol_version {
 	EAPOL_PROTOCOL_VERSION_2001	= 1,
 	EAPOL_PROTOCOL_VERSION_2004	= 2,
@@ -116,3 +119,6 @@ struct eapol_key {
 
 const struct eapol_key *eapol_key_validate(const uint8_t *frame, size_t len,
 						size_t mic_len);
+int eapol_key_descriptor_version_from_akm(enum ie_rsn_akm_suite akm,
+					enum ie_rsn_cipher_suite pairwise,
+					uint8_t *out_version);
diff -pruN 1.30-1/src/eap-peap.c 2.3-1/src/eap-peap.c
--- 1.30-1/src/eap-peap.c	2022-01-24 21:28:47.000000000 +0000
+++ 2.3-1/src/eap-peap.c	2023-01-23 18:46:38.000000000 +0000
@@ -468,7 +468,8 @@ static void eap_extensions_handle_reques
 }
 
 static bool eap_peap_tunnel_ready(struct eap_state *eap,
-						const char *peer_identity)
+						const char *peer_identity,
+						bool resumed)
 {
 	struct peap_state *peap_state = eap_tls_common_get_variant_data(eap);
 
@@ -480,6 +481,15 @@ static bool eap_peap_tunnel_ready(struct
 	*/
 	eap_discard_success_and_failure(eap, true);
 
+	/*
+	 * In case of a resumed session, the server will typically not run
+	 * phase2 methods at all, but will instead send a result TLV right
+	 * away.  Treat this like a successful phase2.  In case the server
+	 * does proceed with phase2, the success/failure state will be updated.
+	 */
+	if (resumed)
+		eap_method_success(peap_state->phase2);
+
 	/* MSK, EMSK and challenge derivation */
 	eap_tls_common_tunnel_prf_get_bytes(eap, true, "client EAP encryption",
 						peap_state->key, 128);
diff -pruN 1.30-1/src/eap-tls.c 2.3-1/src/eap-tls.c
--- 1.30-1/src/eap-tls.c	2021-02-16 20:36:24.000000000 +0000
+++ 2.3-1/src/eap-tls.c	2023-01-23 18:46:38.000000000 +0000
@@ -35,7 +35,8 @@
 #include "src/eap-tls-common.h"
 
 static bool eap_tls_tunnel_ready(struct eap_state *eap,
-						const char *peer_identity)
+						const char *peer_identity,
+						bool resumed)
 {
 	uint8_t msk_emsk[128];
 	uint8_t iv[64];
diff -pruN 1.30-1/src/eap-tls-common.c 2.3-1/src/eap-tls-common.c
--- 1.30-1/src/eap-tls-common.c	2022-06-04 20:14:23.000000000 +0000
+++ 2.3-1/src/eap-tls-common.c	2023-02-02 12:57:32.000000000 +0000
@@ -28,7 +28,9 @@
 #include <errno.h>
 #include <ell/ell.h>
 
+#include "ell/useful.h"
 #include "src/missing.h"
+#include "src/module.h"
 #include "src/eap.h"
 #include "src/eap-private.h"
 #include "src/eap-tls-common.h"
@@ -113,6 +115,8 @@ struct eap_tls_state {
 
 	bool expecting_frag_ack:1;
 	bool tunnel_ready:1;
+	bool tls_session_resumed:1;
+	bool tls_cache_disabled:1;
 
 	struct l_queue *ca_cert;
 	struct l_certchain *client_cert;
@@ -123,8 +127,15 @@ struct eap_tls_state {
 	void *variant_data;
 };
 
-static void __eap_tls_common_state_reset(struct eap_tls_state *eap_tls)
+static struct l_settings *eap_tls_session_cache;
+static eap_tls_session_cache_load_func_t eap_tls_session_cache_load;
+static eap_tls_session_cache_sync_func_t eap_tls_session_cache_sync;
+
+static void __eap_tls_common_state_reset(struct eap_state *eap)
 {
+	struct eap_tls_state *eap_tls = eap_get_data(eap);
+	const char *peer_id;
+
 	eap_tls->version_negotiated = EAP_TLS_VERSION_NOT_NEGOTIATED;
 	eap_tls->method_completed = false;
 	eap_tls->phase2_failed = false;
@@ -139,6 +150,43 @@ static void __eap_tls_common_state_reset
 	if (eap_tls->tunnel)
 		l_tls_reset(eap_tls->tunnel);
 
+	/*
+	 * Drop the TLS session cache for this peer if the overall EAP
+	 * method didn't succeed.
+	 *
+	 * Additionally if the session was cached previously, meaning
+	 * that we've had a successful authentication at least once before,
+	 * and we now used session resumption successfully and the method
+	 * failed, become suspicious of this server's TLS session
+	 * resumption support.  Some authenticators strangely allow
+	 * resumption but can't handle it all the way to EAP method
+	 * success.  This improves the chances that authentication
+	 * succeeds on the next attempt.
+	 *
+	 * Drop the cache even if we have no indication that the
+	 * method failed but it just didn't succeed, to handle cases like
+	 * the server getting stuck and a timout occuring at a higher
+	 * layer.  The risk is that we may occasionally flush the session
+	 * data when there was only a momentary radio issue, invalid
+	 * phase2 credentials or decision to abort.  Those are not hot
+	 * paths.
+	 *
+	 * Note: TLS errors before the ready callback are handled in l_tls.
+	 */
+	peer_id = eap_get_peer_id(eap);
+	if (peer_id && eap_tls_session_cache && !eap_method_is_success(eap) &&
+			l_settings_has_group(eap_tls_session_cache, peer_id)) {
+		eap_tls_forget_peer(peer_id);
+
+		if (eap_tls->tls_session_resumed)
+			l_warn("EAP: method did not finish after successful TLS"
+				" session resumption.  If this repeats consider"
+				" disabling [Security].EAP-%sFastReauthentication",
+				eap_get_method_name(eap));
+	}
+
+	eap_tls->tls_session_resumed = false;
+
 	eap_tls->tx_frag_offset = 0;
 	eap_tls->tx_frag_last_len = 0;
 
@@ -181,7 +229,7 @@ bool eap_tls_common_state_reset(struct e
 {
 	struct eap_tls_state *eap_tls = eap_get_data(eap);
 
-	__eap_tls_common_state_reset(eap_tls);
+	__eap_tls_common_state_reset(eap);
 
 	if (eap_tls->variant_ops->reset)
 		eap_tls->variant_ops->reset(eap_tls->variant_data);
@@ -193,7 +241,7 @@ void eap_tls_common_state_free(struct ea
 {
 	struct eap_tls_state *eap_tls = eap_get_data(eap);
 
-	__eap_tls_common_state_reset(eap_tls);
+	__eap_tls_common_state_reset(eap);
 
 	eap_set_data(eap, NULL);
 
@@ -239,6 +287,9 @@ static void eap_tls_tunnel_ready(const c
 	struct eap_state *eap = user_data;
 	struct eap_tls_state *eap_tls = eap_get_data(eap);
 
+	eap_tls->tls_session_resumed =
+		l_tls_get_session_resumed(eap_tls->tunnel);
+
 	if (eap_tls->ca_cert && !peer_identity) {
 		l_error("%s: TLS did not verify AP identity",
 			eap_get_method_name(eap));
@@ -258,7 +309,8 @@ static void eap_tls_tunnel_ready(const c
 	if (!eap_tls->variant_ops->tunnel_ready)
 		return;
 
-	if (!eap_tls->variant_ops->tunnel_ready(eap, peer_identity))
+	if (!eap_tls->variant_ops->tunnel_ready(eap, peer_identity,
+						eap_tls->tls_session_resumed))
 		l_tls_close(eap_tls->tunnel);
 }
 
@@ -571,6 +623,15 @@ static int eap_tls_handle_fragmented_req
 	return r;
 }
 
+static void eap_tls_session_cache_update(void *user_data)
+{
+	if (L_WARN_ON(!eap_tls_session_cache_sync) ||
+			L_WARN_ON(!eap_tls_session_cache))
+		return;
+
+	eap_tls_session_cache_sync(eap_tls_session_cache);
+}
+
 static bool eap_tls_tunnel_init(struct eap_state *eap)
 {
 	struct eap_tls_state *eap_tls = eap_get_data(eap);
@@ -633,6 +694,17 @@ static bool eap_tls_tunnel_init(struct e
 	if (eap_tls->domain_mask)
 		l_tls_set_domain_mask(eap_tls->tunnel, eap_tls->domain_mask);
 
+	if (!eap_tls_session_cache_load || eap_tls->tls_cache_disabled)
+		goto start;
+
+	if (!eap_tls_session_cache)
+		eap_tls_session_cache = eap_tls_session_cache_load();
+
+	l_tls_set_session_cache(eap_tls->tunnel, eap_tls_session_cache,
+				eap_get_peer_id(eap),
+				24 * 3600 * L_USEC_PER_SEC, 0,
+				eap_tls_session_cache_update, NULL);
+
 start:
 	if (!l_tls_start(eap_tls->tunnel)) {
 		l_error("%s: Failed to start the TLS client",
@@ -971,6 +1043,16 @@ int eap_tls_common_settings_check(struct
 		return -EINVAL;
 	}
 
+	snprintf(setting_key, sizeof(setting_key),
+					"%sFastReauthentication", prefix);
+
+	if (l_settings_has_key(settings, "Security", setting_key) &&
+			!l_settings_get_bool(settings, "Security",
+						setting_key, NULL)) {
+		l_error("Can't parse %s", setting_key);
+		return -EINVAL;
+	}
+
 	return 0;
 }
 
@@ -982,6 +1064,7 @@ bool eap_tls_common_settings_load(struct
 	struct eap_tls_state *eap_tls;
 	char setting_key[72];
 	char *domain_mask_str;
+	bool bool_val;
 
 	L_AUTO_FREE_VAR(char *, value) = NULL;
 
@@ -1011,6 +1094,14 @@ bool eap_tls_common_settings_load(struct
 		l_free(domain_mask_str);
 	}
 
+	snprintf(setting_key, sizeof(setting_key),
+					"%sFastReauthentication", prefix);
+
+	if (!l_settings_get_bool(settings, "Security", setting_key, &bool_val))
+		bool_val = true;
+
+	eap_tls->tls_cache_disabled = !bool_val;
+
 	eap_set_data(eap, eap_tls);
 
 	return true;
@@ -1085,3 +1176,35 @@ void eap_tls_common_tunnel_close(struct
 
 	l_tls_close(eap_tls->tunnel);
 }
+
+void eap_tls_set_session_cache_ops(eap_tls_session_cache_load_func_t load,
+					eap_tls_session_cache_sync_func_t sync)
+{
+	eap_tls_session_cache_load = load;
+	eap_tls_session_cache_sync = sync;
+}
+
+void eap_tls_forget_peer(const char *peer_id)
+{
+	if (L_WARN_ON(!eap_tls_session_cache_sync))
+		return;
+
+	if (!eap_tls_session_cache)
+		eap_tls_session_cache = eap_tls_session_cache_load();
+
+	if (l_settings_remove_group(eap_tls_session_cache, peer_id))
+		eap_tls_session_cache_sync(eap_tls_session_cache);
+}
+
+static int eap_tls_common_init(void)
+{
+	return 0;
+}
+
+static void eap_tls_common_exit(void)
+{
+	l_settings_free(eap_tls_session_cache);
+	eap_tls_session_cache = NULL;
+}
+
+IWD_MODULE(eap_tls_common, eap_tls_common_init, eap_tls_common_exit);
diff -pruN 1.30-1/src/eap-tls-common.h 2.3-1/src/eap-tls-common.h
--- 1.30-1/src/eap-tls-common.h	2021-02-16 20:36:24.000000000 +0000
+++ 2.3-1/src/eap-tls-common.h	2023-01-23 18:46:38.000000000 +0000
@@ -20,6 +20,9 @@
  *
  */
 
+typedef struct l_settings *(*eap_tls_session_cache_load_func_t)(void);
+typedef void (*eap_tls_session_cache_sync_func_t)(const struct l_settings *);
+
 enum eap_tls_version {
 	EAP_TLS_VERSION_0               = 0x00,
 	EAP_TLS_VERSION_1               = 0x01,
@@ -30,7 +33,8 @@ enum eap_tls_version {
 struct eap_tls_variant_ops {
 	enum eap_tls_version version_max_supported;
 
-	bool (*tunnel_ready)(struct eap_state *eap, const char *peer_identity);
+	bool (*tunnel_ready)(struct eap_state *eap, const char *peer_identity,
+				bool resumed);
 	bool (*tunnel_handle_request)(struct eap_state *eap,
 					const uint8_t *data, size_t data_len);
 	void (*reset)(void *variant_data);
@@ -81,3 +85,7 @@ bool eap_tls_common_tunnel_prf_get_bytes
 void eap_tls_common_tunnel_send(struct eap_state *eap, const uint8_t *data,
 							size_t data_len);
 void eap_tls_common_tunnel_close(struct eap_state *eap);
+
+void eap_tls_set_session_cache_ops(eap_tls_session_cache_load_func_t load,
+					eap_tls_session_cache_sync_func_t sync);
+void eap_tls_forget_peer(const char *peer_id);
diff -pruN 1.30-1/src/eap-ttls.c 2.3-1/src/eap-ttls.c
--- 1.30-1/src/eap-ttls.c	2022-01-27 14:52:23.000000000 +0000
+++ 2.3-1/src/eap-ttls.c	2023-01-23 18:46:38.000000000 +0000
@@ -902,7 +902,8 @@ static const struct phase2_method_ops ph
 };
 
 static bool eap_ttls_tunnel_ready(struct eap_state *eap,
-						const char *peer_identity)
+						const char *peer_identity,
+						bool resumed)
 {
 	struct phase2_method *phase2 = eap_tls_common_get_variant_data(eap);
 	uint8_t msk_emsk[128];
diff -pruN 1.30-1/src/frame-xchg.c 2.3-1/src/frame-xchg.c
--- 1.30-1/src/frame-xchg.c	2022-01-24 21:28:47.000000000 +0000
+++ 2.3-1/src/frame-xchg.c	2022-11-18 12:31:49.000000000 +0000
@@ -1193,7 +1193,7 @@ uint32_t frame_xchg_startv(uint64_t wdev
 		watch = l_new(struct frame_xchg_watch_data, 1);
 		watch->prefix = prefix;
 		watch->cb = va_arg(resp_args, void *);
-		frame_watch_add(wdev_id, group_id, 0x00d0,
+		frame_watch_add(wdev_id, group_id, prefix->frame_type,
 				prefix->data, prefix->len,
 				frame_xchg_resp_cb, fx, NULL);
 
diff -pruN 1.30-1/src/frame-xchg.h 2.3-1/src/frame-xchg.h
--- 1.30-1/src/frame-xchg.h	2020-09-05 07:43:41.000000000 +0000
+++ 2.3-1/src/frame-xchg.h	2022-11-18 12:31:49.000000000 +0000
@@ -32,10 +32,17 @@ typedef void (*frame_xchg_cb_t)(int err,
 typedef void (*frame_xchg_destroy_func_t)(void *user_data);
 
 struct frame_xchg_prefix {
+	uint16_t frame_type;
 	const uint8_t *data;
 	size_t len;
 };
 
+enum frame_xchg_group {
+	FRAME_GROUP_DEFAULT = 0,
+	FRAME_GROUP_P2P_LISTEN,
+	FRAME_GROUP_P2P_CONNECT,
+};
+
 bool frame_watch_add(uint64_t wdev_id, uint32_t group, uint16_t frame_type,
 			const uint8_t *prefix, size_t prefix_len,
 			frame_watch_cb_t handler, void *user_data,
diff -pruN 1.30-1/src/ft.c 2.3-1/src/ft.c
--- 1.30-1/src/ft.c	2022-06-04 20:14:23.000000000 +0000
+++ 2.3-1/src/ft.c	2022-12-18 19:59:36.000000000 +0000
@@ -33,18 +33,36 @@
 #include "src/mpdu.h"
 #include "src/auth-proto.h"
 #include "src/band.h"
+#include "src/scan.h"
+#include "src/util.h"
+#include "src/netdev.h"
+#include "src/module.h"
+#include "src/offchannel.h"
+#include "src/wiphy.h"
+
+static ft_tx_frame_func_t tx_frame = NULL;
+static ft_tx_associate_func_t tx_assoc = NULL;
+static struct l_queue *info_list = NULL;
+
+struct ft_info {
+	uint32_t ifindex;
+	uint8_t spa[6];
+	uint8_t aa[6];
+	uint8_t snonce[32];
+	uint8_t mde[3];
+	uint8_t *fte;
+	uint8_t *authenticator_ie;
+	uint8_t prev_bssid[6];
+	uint32_t frequency;
+	uint32_t ds_frequency;
+	uint32_t offchannel_id;
 
-struct ft_sm {
-	struct auth_proto ap;
-	struct handshake_state *hs;
+	struct l_timeout *timeout;
+	struct wiphy_radio_work_item work;
 
-	ft_tx_authenticate_func_t tx_auth;
-	ft_tx_associate_func_t tx_assoc;
-	ft_get_oci get_oci;
+	struct ie_ft_info ft_info;
 
-	void *user_data;
-
-	bool over_ds : 1;
+	bool parsed : 1;
 };
 
 /*
@@ -202,11 +220,13 @@ static bool ft_parse_associate_resp_fram
 	return true;
 }
 
-static int ft_tx_reassociate(struct ft_sm *ft)
+static int ft_tx_reassociate(uint32_t ifindex, uint32_t freq,
+				const uint8_t *prev_bssid)
 {
+	struct netdev *netdev = netdev_find(ifindex);
+	struct handshake_state *hs = netdev_get_handshake(netdev);
 	struct iovec iov[3];
 	int iov_elems = 0;
-	struct handshake_state *hs = ft->hs;
 	uint32_t kck_len = handshake_state_get_kck_len(hs);
 	bool is_rsn = hs->supplicant_ie != NULL;
 	uint8_t *rsne = NULL;
@@ -233,9 +253,8 @@ static int ft_tx_reassociate(struct ft_s
 		rsn_info.num_pmkids = 1;
 		rsn_info.pmkids = hs->pmk_r1_name;
 
-		/* Always set OCVC false for FT-over-DS */
-		if (ft->over_ds)
-			rsn_info.ocvc = false;
+		/* Always set OCVC false for FT for now */
+		rsn_info.ocvc = false;
 
 		rsne = alloca(256);
 		ie_build_rsne(&rsn_info, rsne);
@@ -306,7 +325,7 @@ static int ft_tx_reassociate(struct ft_s
 		iov_elems += 1;
 	}
 
-	return ft->tx_assoc(iov, iov_elems, ft->user_data);
+	return tx_assoc(ifindex, freq, prev_bssid, iov, iov_elems);
 
 error:
 	return -EINVAL;
@@ -348,7 +367,7 @@ static bool ft_verify_rsne(const uint8_t
 	return true;
 }
 
-static int ft_parse_ies(struct handshake_state *hs,
+static int parse_ies(struct handshake_state *hs,
 			const uint8_t *authenticator_ie,
 			const uint8_t *ies, size_t ies_len,
 			const uint8_t **mde_out,
@@ -471,91 +490,25 @@ static bool mde_equal(const uint8_t *mde
 	return memcmp(mde1, mde1, mde1[1] + 2) == 0;
 }
 
-bool ft_over_ds_parse_action_ies(struct ft_ds_info *info,
-					struct handshake_state *hs,
-					const uint8_t *ies,
-					size_t ies_len)
-{
-	const uint8_t *mde = NULL;
-	const uint8_t *fte = NULL;
-	bool is_rsn = hs->supplicant_ie != NULL;
-
-	if (ft_parse_ies(hs, info->authenticator_ie, ies, ies_len,
-				&mde, &fte) < 0)
-		return false;
-
-	if (!mde_equal(info->mde, mde))
-		goto ft_error;
-
-	if (is_rsn) {
-		if (!ft_parse_fte(hs, info->snonce, fte, &info->ft_info))
-			goto ft_error;
-
-		info->fte = l_memdup(fte, fte[1] + 2);
-	} else if (fte)
-		goto ft_error;
-
-	return true;
-
-ft_error:
-	return false;
-}
-
-static int ft_process_ies(struct handshake_state *hs, const uint8_t *ies,
-			size_t ies_len)
-{
-	const uint8_t *mde = NULL;
-	const uint8_t *fte = NULL;
-	bool is_rsn = hs->supplicant_ie != NULL;
-
-	/* Check 802.11r IEs */
-	if (!ies)
-		goto ft_error;
-
-	if (ft_parse_ies(hs, hs->authenticator_ie, ies, ies_len,
-				&mde, &fte) < 0)
-		goto ft_error;
-
-	if (!mde_equal(hs->mde, mde))
-		goto ft_error;
-
-	if (is_rsn) {
-		struct ie_ft_info ft_info;
-
-		if (!ft_parse_fte(hs, hs->snonce, fte, &ft_info))
-			goto ft_error;
-
-		handshake_state_set_fte(hs, fte);
-
-		handshake_state_set_anonce(hs, ft_info.anonce);
-
-		handshake_state_set_kh_ids(hs, ft_info.r0khid,
-						ft_info.r0khid_len,
-						ft_info.r1khid);
-
-		handshake_state_derive_ptk(hs);
-	} else if (fte)
-		goto ft_error;
-
-	return 0;
-
-ft_error:
-	return -EBADMSG;
-}
-
-int ft_over_ds_parse_action_response(const uint8_t *frame, size_t frame_len,
+static int ft_over_ds_parse_action_response(const uint8_t *frame,
+					size_t frame_len,
 					const uint8_t **spa_out,
 					const uint8_t **aa_out,
 					const uint8_t **ies_out,
 					size_t *ies_len)
 {
+	struct mmpdu_header *hdr = (struct mmpdu_header *)frame;
+	size_t hdr_len = mmpdu_header_len(hdr);
 	uint16_t status;
 	const uint8_t *aa;
 	const uint8_t *spa;
 
-	if (frame_len < 16)
+	if (frame_len < hdr_len + 16)
 		return -EINVAL;
 
+	frame += hdr_len;
+	frame_len -= hdr_len;
+
 	/* Category FT */
 	if (frame[0] != 6)
 		return -EINVAL;
@@ -567,6 +520,9 @@ int ft_over_ds_parse_action_response(con
 	spa = frame + 2;
 	aa = frame + 8;
 
+	if (memcmp(spa, hdr->address_1, 6))
+		return -EINVAL;
+
 	status = l_get_le16(frame + 14);
 	if (status != 0)
 		return (int)status;
@@ -585,80 +541,10 @@ int ft_over_ds_parse_action_response(con
 	return 0;
 }
 
-bool ft_over_ds_prepare_handshake(struct ft_ds_info *info,
-					struct handshake_state *hs)
+int __ft_rx_associate(uint32_t ifindex, const uint8_t *frame, size_t frame_len)
 {
-	if (!hs->supplicant_ie)
-		return true;
-
-	memcpy(hs->snonce, info->snonce, sizeof(hs->snonce));
-
-	handshake_state_set_fte(hs, info->fte);
-
-	handshake_state_set_anonce(hs, info->ft_info.anonce);
-
-	handshake_state_set_kh_ids(hs, info->ft_info.r0khid,
-						info->ft_info.r0khid_len,
-						info->ft_info.r1khid);
-
-	handshake_state_derive_ptk(hs);
-
-	return true;
-}
-
-void ft_ds_info_free(struct ft_ds_info *info)
-{
-	__typeof__(info->free) destroy = info->free;
-
-	if (info->fte)
-		l_free(info->fte);
-
-	if (info->authenticator_ie)
-		l_free(info->authenticator_ie);
-
-	if (destroy)
-		destroy(info);
-}
-
-static int ft_rx_authenticate(struct auth_proto *ap, const uint8_t *frame,
-				size_t frame_len)
-{
-	struct ft_sm *ft = l_container_of(ap, struct ft_sm, ap);
-	uint16_t status_code = MMPDU_STATUS_CODE_UNSPECIFIED;
-	const uint8_t *ies = NULL;
-	size_t ies_len;
-	int ret;
-
-	/*
-	 * Parse the Authentication Response and validate the contents
-	 * according to 12.5.2 / 12.5.4: RSN or non-RSN Over-the-air
-	 * FT Protocol.
-	 */
-	if (!ft_parse_authentication_resp_frame(frame, frame_len, ft->hs->spa,
-						ft->hs->aa, ft->hs->aa,
-						2, &status_code,
-						&ies, &ies_len))
-		goto auth_error;
-
-	/* AP Rejected the authenticate / associate */
-	if (status_code != 0)
-		goto auth_error;
-
-	ret = ft_process_ies(ft->hs, ies, ies_len);
-	if (ret < 0)
-		goto auth_error;
-
-	return ft->get_oci(ft->user_data);
-
-auth_error:
-	return (int)status_code;
-}
-
-static int ft_rx_associate(struct auth_proto *ap, const uint8_t *frame,
-				size_t frame_len)
-{
-	struct ft_sm *ft = l_container_of(ap, struct ft_sm, ap);
-	struct handshake_state *hs = ft->hs;
+	struct netdev *netdev = netdev_find(ifindex);
+	struct handshake_state *hs = netdev_get_handshake(netdev);
 	uint32_t kck_len = handshake_state_get_kck_len(hs);
 	const uint8_t *rsne = NULL;
 	const uint8_t *mde = NULL;
@@ -671,6 +557,9 @@ static int ft_rx_associate(struct auth_p
 					&mde, &fte))
 		return -EBADMSG;
 
+	if (out_status != 0)
+		return (int)out_status;
+
 	/*
 	 * During a transition in an RSN, check for an RSNE containing the
 	 * PMK-R1-Name and the remaining fields same as in the advertised
@@ -785,34 +674,13 @@ static int ft_rx_associate(struct auth_p
 						ft_info.igtk_ipn);
 		}
 
-		handshake_state_install_ptk(ft->hs);
+		handshake_state_install_ptk(hs);
 	}
 
 	return 0;
 }
 
-static int ft_rx_oci(struct auth_proto *ap)
-{
-	struct ft_sm *ft = l_container_of(ap, struct ft_sm, ap);
-
-	return ft_tx_reassociate(ft);
-}
-
-static void ft_sm_free(struct auth_proto *ap)
-{
-	struct ft_sm *ft = l_container_of(ap, struct ft_sm, ap);
-
-	l_free(ft);
-}
-
-static bool ft_over_ds_start(struct auth_proto *ap)
-{
-	struct ft_sm *ft = l_container_of(ap, struct ft_sm, ap);
-
-	return ft_tx_reassociate(ft) == 0;
-}
-
-bool ft_build_authenticate_ies(struct handshake_state *hs, bool ocvc,
+static bool ft_build_authenticate_ies(struct handshake_state *hs, bool ocvc,
 				const uint8_t *new_snonce, uint8_t *buf,
 				size_t *len)
 {
@@ -884,63 +752,407 @@ bool ft_build_authenticate_ies(struct ha
 	return true;
 }
 
-static bool ft_start(struct auth_proto *ap)
+void __ft_set_tx_frame_func(ft_tx_frame_func_t func)
 {
-	struct ft_sm *ft = l_container_of(ap, struct ft_sm, ap);
-	struct handshake_state *hs = ft->hs;
-	struct iovec iov;
-	uint8_t buf[512];
-	size_t len;
+	tx_frame = func;
+}
+
+void __ft_set_tx_associate_func(ft_tx_associate_func_t func)
+{
+	tx_assoc = func;
+}
 
-	if (!ft_build_authenticate_ies(hs, hs->supplicant_ocvc, hs->snonce,
-					buf, &len))
+static bool ft_parse_ies(struct ft_info *info, struct handshake_state *hs,
+			const uint8_t *ies, size_t ies_len)
+{
+	const uint8_t *mde = NULL;
+	const uint8_t *fte = NULL;
+	bool is_rsn = hs->supplicant_ie != NULL;
+
+	if (parse_ies(hs, info->authenticator_ie, ies, ies_len,
+				&mde, &fte) < 0)
 		return false;
 
-	iov.iov_base = buf;
-	iov.iov_len = len;
+	if (!mde_equal(info->mde, mde))
+		goto ft_error;
+
+	if (is_rsn) {
+		if (!ft_parse_fte(hs, info->snonce, fte, &info->ft_info))
+			goto ft_error;
+
+		info->fte = l_memdup(fte, fte[1] + 2);
+	} else if (fte)
+		goto ft_error;
+
+	return true;
+
+ft_error:
+	return false;
+}
+
+static struct ft_info *ft_info_find(uint32_t ifindex, const uint8_t *aa)
+{
+	const struct l_queue_entry *e;
+
+	for (e = l_queue_get_entries(info_list); e; e = e->next) {
+		struct ft_info *info = e->data;
+
+		if (info->ifindex != ifindex)
+			continue;
 
-	ft->tx_auth(&iov, 1, ft->user_data);
+		if (aa && memcmp(info->aa, aa, 6))
+			continue;
 
+		return info;
+	}
+
+	return NULL;
+}
+
+void __ft_rx_action(uint32_t ifindex, const uint8_t *frame, size_t frame_len)
+{
+	struct netdev *netdev = netdev_find(ifindex);
+	struct handshake_state *hs = netdev_get_handshake(netdev);
+	struct ft_info *info;
+	int ret;
+	const uint8_t *aa = NULL;
+	const uint8_t *spa = NULL;
+	const uint8_t *ies = NULL;
+	size_t ies_len = 0;
+
+	ret = ft_over_ds_parse_action_response(frame, frame_len, &spa, &aa,
+						&ies, &ies_len);
+	if (ret != 0)
+		return;
+
+	info = ft_info_find(ifindex, aa);
+	if (!info)
+		return;
+
+	if (!ft_parse_ies(info, hs, ies, ies_len))
+		goto ft_error;
+
+	info->parsed = true;
+
+	l_timeout_remove(info->timeout);
+	info->timeout = NULL;
+
+	wiphy_radio_work_done(netdev_get_wiphy(netdev), info->work.id);
+
+	return;
+
+ft_error:
+	l_debug("FT-over-DS authenticate to "MAC" failed", MAC_STR(info->aa));
+}
+
+static struct ft_info *ft_info_new(struct handshake_state *hs,
+					const struct scan_bss *target_bss)
+{
+	struct ft_info *info = l_new(struct ft_info, 1);
+
+	info->ifindex = hs->ifindex;
+	memcpy(info->spa, hs->spa, 6);
+	memcpy(info->aa, target_bss->addr, 6);
+	memcpy(info->mde, target_bss->mde, sizeof(info->mde));
+	memcpy(info->prev_bssid, hs->aa, 6);
+
+	info->frequency = target_bss->frequency;
+
+	if (target_bss->rsne)
+		info->authenticator_ie = l_memdup(target_bss->rsne,
+						target_bss->rsne[1] + 2);
+
+	l_getrandom(info->snonce, 32);
+
+	return info;
+}
+
+static void ft_info_destroy(void *data)
+{
+	struct ft_info *info = data;
+
+	if (info->fte)
+		l_free(info->fte);
+
+	if (info->authenticator_ie)
+		l_free(info->authenticator_ie);
+
+	if (info->timeout)
+		l_timeout_remove(info->timeout);
+
+	l_free(info);
+}
+
+static void ft_prepare_handshake(struct ft_info *info,
+					struct handshake_state *hs)
+{
+	handshake_state_set_authenticator_address(hs, info->aa);
+
+	memcpy(hs->mde + 2, info->mde, 3);
+
+	handshake_state_set_chandef(hs, NULL);
+
+	if (!hs->supplicant_ie)
+		return;
+
+	if (info->authenticator_ie)
+		handshake_state_set_authenticator_ie(hs,
+							info->authenticator_ie);
+
+	memcpy(hs->snonce, info->snonce, sizeof(hs->snonce));
+
+	handshake_state_set_fte(hs, info->fte);
+
+	handshake_state_set_anonce(hs, info->ft_info.anonce);
+
+	handshake_state_set_kh_ids(hs, info->ft_info.r0khid,
+						info->ft_info.r0khid_len,
+						info->ft_info.r1khid);
+
+	handshake_state_derive_ptk(hs);
+}
+
+static bool ft_send_action(struct wiphy_radio_work_item *work)
+{
+	struct ft_info *info = l_container_of(work, struct ft_info, work);
+	struct netdev *netdev = netdev_find(info->ifindex);
+	struct handshake_state *hs = netdev_get_handshake(netdev);
+	uint8_t ft_req[14];
+	struct iovec iov[5];
+	uint8_t ies[512];
+	size_t len;
+	int ret = -EINVAL;
+
+	ft_req[0] = 6; /* FT category */
+	ft_req[1] = 1; /* FT Request action */
+	memcpy(ft_req + 2, info->spa, 6);
+	memcpy(ft_req + 8, info->aa, 6);
+
+	if (!ft_build_authenticate_ies(hs, hs->supplicant_ocvc, info->snonce,
+					ies, &len))
+		goto failed;
+
+	iov[0].iov_base = ft_req;
+	iov[0].iov_len = sizeof(ft_req);
+
+	iov[1].iov_base = ies;
+	iov[1].iov_len = len;
+
+	ret = tx_frame(hs->ifindex, 0x00d0, info->ds_frequency, hs->aa, iov, 2);
+	if (ret < 0)
+		goto failed;
+
+	l_queue_push_tail(info_list, info);
+
+	return false;
+
+failed:
+	l_debug("FT-over-DS action failed to "MAC, MAC_STR(hs->aa));
+
+	ft_info_destroy(info);
 	return true;
 }
 
-struct auth_proto *ft_over_air_sm_new(struct handshake_state *hs,
-				ft_tx_authenticate_func_t tx_auth,
-				ft_tx_associate_func_t tx_assoc,
-				ft_get_oci get_oci,
-				void *user_data)
+struct wiphy_radio_work_item_ops ft_ops = {
+	.do_work = ft_send_action,
+};
+
+static void ft_ds_timeout(struct l_timeout *timeout, void *user_data)
+{
+	struct ft_info *info = user_data;
+	struct netdev *netdev = netdev_find(info->ifindex);
+
+	wiphy_radio_work_done(netdev_get_wiphy(netdev), info->work.id);
+}
+
+int ft_action(uint32_t ifindex, uint32_t freq, const struct scan_bss *target)
+{
+	struct netdev *netdev = netdev_find(ifindex);
+	struct handshake_state *hs = netdev_get_handshake(netdev);
+	struct ft_info *info;
+
+	info = ft_info_new(hs, target);
+	info->ds_frequency = freq;
+	info->timeout = l_timeout_create_ms(200, ft_ds_timeout, info, NULL);
+
+	wiphy_radio_work_insert(netdev_get_wiphy(netdev), &info->work,
+				WIPHY_WORK_PRIORITY_FT, &ft_ops);
+
+	return 0;
+}
+
+void __ft_rx_authenticate(uint32_t ifindex, const uint8_t *frame,
+				size_t frame_len)
+{
+	struct netdev *netdev = netdev_find(ifindex);
+	struct handshake_state *hs = netdev_get_handshake(netdev);
+	struct ft_info *info;
+	uint16_t status;
+	const uint8_t *ies;
+	size_t ies_len;
+
+	info = ft_info_find(ifindex, NULL);
+	if (!info)
+		return;
+
+	if (!ft_parse_authentication_resp_frame(frame, frame_len,
+					info->spa, info->aa, info->aa, 2,
+					&status, &ies, &ies_len))
+		return;
+
+	if (status != 0)
+		goto cancel;
+
+	if (!ft_parse_ies(info, hs, ies, ies_len))
+		goto cancel;
+
+	info->parsed = true;
+
+cancel:
+	/* Verified to be expected target, offchannel can be canceled */
+	offchannel_cancel(netdev_get_wdev_id(netdev), info->offchannel_id);
+}
+
+static void ft_send_authenticate(void *user_data)
+{
+	struct ft_info *info = user_data;
+	struct netdev *netdev = netdev_find(info->ifindex);
+	struct handshake_state *hs = netdev_get_handshake(netdev);
+	uint8_t ies[256];
+	size_t len;
+	struct iovec iov[2];
+	struct mmpdu_authentication auth;
+
+	/* Authentication body */
+	auth.algorithm = L_CPU_TO_LE16(MMPDU_AUTH_ALGO_FT);
+	auth.transaction_sequence = L_CPU_TO_LE16(1);
+	auth.status = L_CPU_TO_LE16(0);
+
+	iov[0].iov_base = &auth;
+	iov[0].iov_len = sizeof(struct mmpdu_authentication);
+
+	if (!ft_build_authenticate_ies(hs, hs->supplicant_ocvc, info->snonce,
+					ies, &len))
+		return;
+
+	iov[1].iov_base = ies;
+	iov[1].iov_len = len;
+
+	tx_frame(info->ifindex, 0x00b0, info->frequency, info->aa, iov, 2);
+}
+
+static void ft_authenticate_destroy(int error, void *user_data)
+{
+	struct ft_info *info = user_data;
+
+	info->offchannel_id = 0;
+
+	if (!info->parsed) {
+		l_queue_remove(info_list, info);
+		ft_info_destroy(info);
+	}
+}
+
+/*
+ * There is no callback here because its assumed that another work item will
+ * be inserted following this call which will check if authentication succeeded
+ * via ft_associate.
+ *
+ * If the netdev goes away while authentication is in-flight station will clear
+ * the authentications during cleanup, and in turn cancel the offchannel
+ * request.
+ */
+int ft_authenticate(uint32_t ifindex, const struct scan_bss *target)
+{
+	struct netdev *netdev = netdev_find(ifindex);
+	struct handshake_state *hs = netdev_get_handshake(netdev);
+	struct ft_info *info = ft_info_new(hs, target);
+
+	info->offchannel_id = offchannel_start(netdev_get_wdev_id(netdev),
+						WIPHY_WORK_PRIORITY_FT,
+						target->frequency,
+						200, ft_send_authenticate, info,
+						ft_authenticate_destroy);
+
+	l_queue_push_tail(info_list, info);
+
+	return 0;
+}
+
+int ft_associate(uint32_t ifindex, const uint8_t *addr)
+{
+	struct netdev *netdev = netdev_find(ifindex);
+	struct handshake_state *hs = netdev_get_handshake(netdev);
+	struct ft_info *info;
+	int ret;
+
+	/*
+	 * TODO: Since FT-over-DS is done early, before the time of roaming, it
+	 *       may end up that a completely new BSS is the best candidate and
+	 *       we haven't yet authenticated. We could actually authenticate
+	 *       at this point, but for now just assume the caller will choose
+	 *       a different BSS.
+	 */
+	info = ft_info_find(ifindex, addr);
+	if (!info)
+		return -ENOENT;
+
+	/*
+	 * Either failed or no response. This may have been an FT-over-DS
+	 * attempt so clear out the entry so FT-over-Air can try again.
+	 */
+	if (!info->parsed) {
+		l_queue_remove(info_list, info);
+		ft_info_destroy(info);
+
+		return -ENOENT;
+	}
+
+	ft_prepare_handshake(info, hs);
+
+	ret = ft_tx_reassociate(ifindex, info->frequency, info->prev_bssid);
+
+	/* After this no previous auths will be valid */
+	ft_clear_authentications(ifindex);
+
+	return ret;
+}
+
+static bool remove_ifindex(void *data, void *user_data)
 {
-	struct ft_sm *ft = l_new(struct ft_sm, 1);
+	struct ft_info *info = data;
+	uint32_t ifindex = L_PTR_TO_UINT(user_data);
 
-	ft->tx_auth = tx_auth;
-	ft->tx_assoc = tx_assoc;
-	ft->get_oci = get_oci;
-	ft->hs = hs;
-	ft->user_data = user_data;
+	if (info->ifindex != ifindex)
+		return false;
 
-	ft->ap.rx_authenticate = ft_rx_authenticate;
-	ft->ap.rx_associate = ft_rx_associate;
-	ft->ap.start = ft_start;
-	ft->ap.free = ft_sm_free;
-	ft->ap.rx_oci = ft_rx_oci;
+	if (info->offchannel_id)
+		offchannel_cancel(netdev_get_wdev_id(netdev_find(ifindex)),
+					info->offchannel_id);
 
-	return &ft->ap;
+	ft_info_destroy(info);
+	return true;
 }
 
-struct auth_proto *ft_over_ds_sm_new(struct handshake_state *hs,
-				ft_tx_associate_func_t tx_assoc,
-				void *user_data)
+void ft_clear_authentications(uint32_t ifindex)
 {
-	struct ft_sm *ft = l_new(struct ft_sm, 1);
+	l_queue_foreach_remove(info_list, remove_ifindex,
+				L_UINT_TO_PTR(ifindex));
+}
 
-	ft->tx_assoc = tx_assoc;
-	ft->hs = hs;
-	ft->user_data = user_data;
-	ft->over_ds = true;
+static int ft_init(void)
+{
+	info_list = l_queue_new();
 
-	ft->ap.rx_associate = ft_rx_associate;
-	ft->ap.start = ft_over_ds_start;
-	ft->ap.free = ft_sm_free;
+	return 0;
+}
+
+static void ft_exit(void)
+{
+	if (!l_queue_isempty(info_list))
+		l_warn("stale FT info objects found!");
 
-	return &ft->ap;
+	l_queue_destroy(info_list, ft_info_destroy);
 }
+
+IWD_MODULE(ft, ft_init, ft_exit);
diff -pruN 1.30-1/src/ft.h 2.3-1/src/ft.h
--- 1.30-1/src/ft.h	2021-11-02 18:50:50.000000000 +0000
+++ 2.3-1/src/ft.h	2022-11-18 12:31:49.000000000 +0000
@@ -20,54 +20,26 @@
  *
  */
 
-struct handshake_state;
+struct scan_bss;
 
-typedef void (*ft_tx_authenticate_func_t)(struct iovec *iov, size_t iov_len,
-					void *user_data);
-typedef int (*ft_tx_associate_func_t)(struct iovec *ie_iov, size_t iov_len,
-					void *user_data);
-typedef int (*ft_get_oci)(void *user_data);
-
-typedef void (*ft_ds_free_func_t)(void *user_data);
-
-struct ft_ds_info {
-	uint8_t spa[6];
-	uint8_t aa[6];
-	uint8_t snonce[32];
-	uint8_t mde[3];
-	uint8_t *fte;
-	uint8_t *authenticator_ie;
-
-	struct ie_ft_info ft_info;
-
-	void (*free)(struct ft_ds_info *s);
-};
-
-void ft_ds_info_free(struct ft_ds_info *info);
-
-bool ft_build_authenticate_ies(struct handshake_state *hs, bool ocvc,
-				const uint8_t *new_snonce, uint8_t *buf,
-				size_t *len);
-
-int ft_over_ds_parse_action_response(const uint8_t *frame, size_t frame_len,
-					const uint8_t **spa_out,
-					const uint8_t **aa_out,
-					const uint8_t **ies_out,
-					size_t *ies_len);
-bool ft_over_ds_parse_action_ies(struct ft_ds_info *info,
-					struct handshake_state *hs,
-					const uint8_t *ies,
-					size_t ies_len);
-
-struct auth_proto *ft_over_air_sm_new(struct handshake_state *hs,
-				ft_tx_authenticate_func_t tx_auth,
-				ft_tx_associate_func_t tx_assoc,
-				ft_get_oci get_oci,
-				void *user_data);
-
-struct auth_proto *ft_over_ds_sm_new(struct handshake_state *hs,
-				ft_tx_associate_func_t tx_assoc,
-				void *user_data);
-
-bool ft_over_ds_prepare_handshake(struct ft_ds_info *info,
-					struct handshake_state *hs);
+typedef int (*ft_tx_frame_func_t)(uint32_t ifindex, uint16_t frame_type,
+					uint32_t frequency,
+					const uint8_t *dest, struct iovec *iov,
+					size_t iov_len);
+
+typedef int (*ft_tx_associate_func_t)(uint32_t ifindex, uint32_t freq,
+					const uint8_t *prev_bssid,
+					struct iovec *ie_iov, size_t iov_len);
+
+void __ft_set_tx_frame_func(ft_tx_frame_func_t func);
+void __ft_set_tx_associate_func(ft_tx_associate_func_t func);
+int __ft_rx_associate(uint32_t ifindex, const uint8_t *frame,
+			size_t frame_len);
+void __ft_rx_action(uint32_t ifindex, const uint8_t *frame, size_t frame_len);
+void __ft_rx_authenticate(uint32_t ifindex, const uint8_t *frame,
+				size_t frame_len);
+
+void ft_clear_authentications(uint32_t ifindex);
+int ft_action(uint32_t ifindex, uint32_t freq, const struct scan_bss *target);
+int ft_associate(uint32_t ifindex, const uint8_t *addr);
+int ft_authenticate(uint32_t ifindex, const struct scan_bss *target);
diff -pruN 1.30-1/src/handshake.h 2.3-1/src/handshake.h
--- 1.30-1/src/handshake.h	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/src/handshake.h	2023-01-23 18:46:38.000000000 +0000
@@ -60,6 +60,7 @@ enum handshake_event {
 	HANDSHAKE_EVENT_EAP_NOTIFY,
 	HANDSHAKE_EVENT_TRANSITION_DISABLE,
 	HANDSHAKE_EVENT_P2P_IP_REQUEST,
+	HANDSHAKE_EVENT_REKEY_COMPLETE,
 };
 
 typedef void (*handshake_event_func_t)(struct handshake_state *hs,
diff -pruN 1.30-1/src/ie.c 2.3-1/src/ie.c
--- 1.30-1/src/ie.c	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/src/ie.c	2022-11-18 12:31:49.000000000 +0000
@@ -428,13 +428,59 @@ uint32_t ie_rsn_cipher_suite_to_cipher(e
 		return CRYPTO_CIPHER_WEP40;
 	case IE_RSN_CIPHER_SUITE_WEP104:
 		return CRYPTO_CIPHER_WEP104;
-	case IE_RSN_CIPHER_SUITE_BIP:
-		return CRYPTO_CIPHER_BIP;
+	case IE_RSN_CIPHER_SUITE_BIP_CMAC:
+		return CRYPTO_CIPHER_BIP_CMAC;
+	case IE_RSN_CIPHER_SUITE_GCMP:
+		return CRYPTO_CIPHER_GCMP;
+	case IE_RSN_CIPHER_SUITE_GCMP_256:
+		return CRYPTO_CIPHER_GCMP_256;
+	case IE_RSN_CIPHER_SUITE_CCMP_256:
+		return CRYPTO_CIPHER_CCMP_256;
+	case IE_RSN_CIPHER_SUITE_BIP_GMAC:
+		return CRYPTO_CIPHER_BIP_GMAC;
+	case IE_RSN_CIPHER_SUITE_BIP_GMAC_256:
+		return CRYPTO_CIPHER_BIP_GMAC_256;
+	case IE_RSN_CIPHER_SUITE_BIP_CMAC_256:
+		return CRYPTO_CIPHER_BIP_CMAC_256;
 	default:
 		return 0;
 	}
 }
 
+const char *ie_rsn_cipher_suite_to_string(enum ie_rsn_cipher_suite suite)
+{
+	switch (suite) {
+	case IE_RSN_CIPHER_SUITE_CCMP:
+		return "CCMP-128";
+	case IE_RSN_CIPHER_SUITE_TKIP:
+		return "TKIP";
+	case IE_RSN_CIPHER_SUITE_WEP40:
+		return "WEP-40";
+	case IE_RSN_CIPHER_SUITE_WEP104:
+		return "WEP-104";
+	case IE_RSN_CIPHER_SUITE_BIP_CMAC:
+		return "BIP-CMAC-128";
+	case IE_RSN_CIPHER_SUITE_GCMP:
+		return "GCMP-128";
+	case IE_RSN_CIPHER_SUITE_GCMP_256:
+		return "GCMP-256";
+	case IE_RSN_CIPHER_SUITE_CCMP_256:
+		return "CCMP-256";
+	case IE_RSN_CIPHER_SUITE_NO_GROUP_TRAFFIC:
+		return "NO-TRAFFIC";
+	case IE_RSN_CIPHER_SUITE_USE_GROUP_CIPHER:
+		break;
+	case IE_RSN_CIPHER_SUITE_BIP_GMAC:
+		return "BIP-GMAC-128";
+	case IE_RSN_CIPHER_SUITE_BIP_GMAC_256:
+		return "BIP-GMAC-256";
+	case IE_RSN_CIPHER_SUITE_BIP_CMAC_256:
+		return "BIP-CMAC-256";
+	}
+
+	return NULL;
+}
+
 /* 802.11, Section 8.4.2.27.2 */
 static bool ie_parse_cipher_suite(const uint8_t *data,
 					enum ie_rsn_cipher_suite *out)
@@ -462,11 +508,29 @@ static bool ie_parse_cipher_suite(const
 			*out = IE_RSN_CIPHER_SUITE_WEP104;
 			return true;
 		case 6:
-			*out = IE_RSN_CIPHER_SUITE_BIP;
+			*out = IE_RSN_CIPHER_SUITE_BIP_CMAC;
 			return true;
 		case 7:
 			*out = IE_RSN_CIPHER_SUITE_NO_GROUP_TRAFFIC;
 			return true;
+		case 8:
+			*out = IE_RSN_CIPHER_SUITE_GCMP;
+			return true;
+		case 9:
+			*out = IE_RSN_CIPHER_SUITE_GCMP_256;
+			return true;
+		case 10:
+			*out = IE_RSN_CIPHER_SUITE_CCMP_256;
+			return true;
+		case 11:
+			*out = IE_RSN_CIPHER_SUITE_BIP_GMAC;
+			return true;
+		case 12:
+			*out = IE_RSN_CIPHER_SUITE_BIP_GMAC_256;
+			return true;
+		case 13:
+			*out = IE_RSN_CIPHER_SUITE_BIP_CMAC_256;
+			return true;
 		default:
 			return false;
 		}
@@ -580,6 +644,9 @@ static bool ie_parse_group_cipher(const
 	case IE_RSN_CIPHER_SUITE_WEP104:
 	case IE_RSN_CIPHER_SUITE_WEP40:
 	case IE_RSN_CIPHER_SUITE_NO_GROUP_TRAFFIC:
+	case IE_RSN_CIPHER_SUITE_GCMP:
+	case IE_RSN_CIPHER_SUITE_GCMP_256:
+	case IE_RSN_CIPHER_SUITE_CCMP_256:
 		break;
 	default:
 		return false;
@@ -589,15 +656,14 @@ static bool ie_parse_group_cipher(const
 	return true;
 }
 
-static bool ie_parse_pairwise_cipher(const uint8_t *data,
+static int ie_parse_pairwise_cipher(const uint8_t *data,
 					enum ie_rsn_cipher_suite *out)
 {
 	enum ie_rsn_cipher_suite tmp;
-
 	bool r = ie_parse_cipher_suite(data, &tmp);
 
 	if (!r)
-		return r;
+		return -ENOENT;
 
 	switch (tmp) {
 	case IE_RSN_CIPHER_SUITE_CCMP:
@@ -605,13 +671,16 @@ static bool ie_parse_pairwise_cipher(con
 	case IE_RSN_CIPHER_SUITE_WEP104:
 	case IE_RSN_CIPHER_SUITE_WEP40:
 	case IE_RSN_CIPHER_SUITE_USE_GROUP_CIPHER:
+	case IE_RSN_CIPHER_SUITE_GCMP:
+	case IE_RSN_CIPHER_SUITE_GCMP_256:
+	case IE_RSN_CIPHER_SUITE_CCMP_256:
 		break;
 	default:
-		return false;
+		return -ERANGE;
 	}
 
 	*out = tmp;
-	return true;
+	return 0;
 }
 
 static bool ie_parse_group_management_cipher(const uint8_t *data,
@@ -625,8 +694,11 @@ static bool ie_parse_group_management_ci
 		return r;
 
 	switch (tmp) {
-	case IE_RSN_CIPHER_SUITE_BIP:
+	case IE_RSN_CIPHER_SUITE_BIP_CMAC:
 	case IE_RSN_CIPHER_SUITE_NO_GROUP_TRAFFIC:
+	case IE_RSN_CIPHER_SUITE_BIP_GMAC:
+	case IE_RSN_CIPHER_SUITE_BIP_GMAC_256:
+	case IE_RSN_CIPHER_SUITE_BIP_CMAC_256:
 		break;
 	default:
 		return false;
@@ -682,9 +754,12 @@ static int parse_ciphers(const uint8_t *
 	/* Parse Pairwise Cipher Suite List field */
 	for (i = 0, out_info->pairwise_ciphers = 0; i < count; i++) {
 		enum ie_rsn_cipher_suite suite;
+		int r = ie_parse_pairwise_cipher(data + i * 4, &suite);
 
-		if (!ie_parse_pairwise_cipher(data + i * 4, &suite))
-			return -ERANGE;
+		if (r == -ENOENT) /* Skip unknown */
+			continue;
+		else if (r < 0)
+			return r;
 
 		out_info->pairwise_ciphers |= suite;
 	}
@@ -746,7 +821,8 @@ static int parse_ciphers(const uint8_t *
 	 * management frame protection enabled
 	 */
 	if (out_info->mfpc)
-		out_info->group_management_cipher = IE_RSN_CIPHER_SUITE_BIP;
+		out_info->group_management_cipher =
+						IE_RSN_CIPHER_SUITE_BIP_CMAC;
 
 	RSNE_ADVANCE(data, len, 2);
 
@@ -884,38 +960,55 @@ int ie_parse_osen_from_data(const uint8_
 static bool ie_build_cipher_suite(uint8_t *data, const uint8_t *oui,
 					const enum ie_rsn_cipher_suite suite)
 {
+	uint8_t selector;
+
 	switch (suite) {
 	case IE_RSN_CIPHER_SUITE_USE_GROUP_CIPHER:
-		memcpy(data, oui, 3);
-		data[3] = 0;
-		return true;
+		selector = 0;
+		goto done;
 	case IE_RSN_CIPHER_SUITE_WEP40:
-		memcpy(data, oui, 3);
-		data[3] = 1;
-		return true;
+		selector = 1;
+		goto done;
 	case IE_RSN_CIPHER_SUITE_TKIP:
-		memcpy(data, oui, 3);
-		data[3] = 2;
-		return true;
+		selector = 2;
+		goto done;
 	case IE_RSN_CIPHER_SUITE_CCMP:
-		memcpy(data, oui, 3);
-		data[3] = 4;
-		return true;
+		selector = 4;
+		goto done;
 	case IE_RSN_CIPHER_SUITE_WEP104:
-		memcpy(data, oui, 3);
-		data[3] = 5;
-		return true;
-	case IE_RSN_CIPHER_SUITE_BIP:
-		memcpy(data, oui, 3);
-		data[3] = 6;
-		return true;
+		selector = 5;
+		goto done;
+	case IE_RSN_CIPHER_SUITE_BIP_CMAC:
+		selector = 6;
+		goto done;
 	case IE_RSN_CIPHER_SUITE_NO_GROUP_TRAFFIC:
-		memcpy(data, oui, 3);
-		data[3] = 7;
-		return true;
+		selector = 7;
+		goto done;
+	case IE_RSN_CIPHER_SUITE_GCMP:
+		selector = 8;
+		goto done;
+	case IE_RSN_CIPHER_SUITE_GCMP_256:
+		selector = 9;
+		goto done;
+	case IE_RSN_CIPHER_SUITE_CCMP_256:
+		selector = 10;
+		goto done;
+	case IE_RSN_CIPHER_SUITE_BIP_GMAC:
+		selector = 11;
+		goto done;
+	case IE_RSN_CIPHER_SUITE_BIP_GMAC_256:
+		selector = 12;
+		goto done;
+	case IE_RSN_CIPHER_SUITE_BIP_CMAC_256:
+		selector = 13;
+		goto done;
 	}
 
 	return false;
+done:
+	memcpy(data, oui, 3);
+	data[3] = selector;
+	return true;
 }
 
 #define RETURN_AKM(data, oui, id)		\
@@ -997,6 +1090,9 @@ static int build_ciphers_common(const st
 		IE_RSN_CIPHER_SUITE_WEP104,
 		IE_RSN_CIPHER_SUITE_WEP40,
 		IE_RSN_CIPHER_SUITE_USE_GROUP_CIPHER,
+		IE_RSN_CIPHER_SUITE_GCMP,
+		IE_RSN_CIPHER_SUITE_GCMP_256,
+		IE_RSN_CIPHER_SUITE_CCMP_256,
 	};
 	unsigned int pos = 0;
 	unsigned int i;
@@ -1115,7 +1211,7 @@ static int build_ciphers_common(const st
 		else if (!info->mfpc)
 			goto done;
 		else if (info->group_management_cipher ==
-				IE_RSN_CIPHER_SUITE_BIP)
+				IE_RSN_CIPHER_SUITE_BIP_CMAC)
 			goto done;
 	}
 
@@ -1136,7 +1232,7 @@ static int build_ciphers_common(const st
 		goto done;
 
 	if (!force_group_mgmt_cipher && info->group_management_cipher ==
-							IE_RSN_CIPHER_SUITE_BIP)
+					IE_RSN_CIPHER_SUITE_BIP_CMAC)
 		goto done;
 
 	/* Group Management Cipher Suite */
diff -pruN 1.30-1/src/ie.h 2.3-1/src/ie.h
--- 1.30-1/src/ie.h	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/src/ie.h	2022-11-18 12:31:49.000000000 +0000
@@ -329,8 +329,14 @@ enum ie_rsn_cipher_suite {
 	IE_RSN_CIPHER_SUITE_TKIP		= 0x0004,
 	IE_RSN_CIPHER_SUITE_CCMP		= 0x0008,
 	IE_RSN_CIPHER_SUITE_WEP104		= 0x0010,
-	IE_RSN_CIPHER_SUITE_BIP			= 0x0020,
+	IE_RSN_CIPHER_SUITE_BIP_CMAC		= 0x0020,
 	IE_RSN_CIPHER_SUITE_NO_GROUP_TRAFFIC	= 0x0040,
+	IE_RSN_CIPHER_SUITE_GCMP		= 0x0080,
+	IE_RSN_CIPHER_SUITE_GCMP_256		= 0x0100,
+	IE_RSN_CIPHER_SUITE_CCMP_256		= 0x0200,
+	IE_RSN_CIPHER_SUITE_BIP_GMAC		= 0x0400,
+	IE_RSN_CIPHER_SUITE_BIP_GMAC_256	= 0x0800,
+	IE_RSN_CIPHER_SUITE_BIP_CMAC_256	= 0x1000,
 };
 
 enum ie_rsn_akm_suite {
@@ -387,6 +393,33 @@ static inline bool IE_AKM_IS_8021X(uint3
 			IE_RSN_AKM_SUITE_FT_OVER_8021X_SHA384);
 }
 
+static inline bool IE_CIPHER_IS_GCMP_CCMP(uint32_t cipher_suite)
+{
+	return cipher_suite & (IE_RSN_CIPHER_SUITE_CCMP |
+				IE_RSN_CIPHER_SUITE_CCMP_256 |
+				IE_RSN_CIPHER_SUITE_GCMP |
+				IE_RSN_CIPHER_SUITE_GCMP_256);
+}
+
+#define IE_GROUP_CIPHERS		\
+(					\
+	IE_RSN_CIPHER_SUITE_TKIP |	\
+	IE_RSN_CIPHER_SUITE_CCMP |	\
+	IE_RSN_CIPHER_SUITE_GCMP |	\
+	IE_RSN_CIPHER_SUITE_GCMP_256 |	\
+	IE_RSN_CIPHER_SUITE_CCMP_256	\
+)
+
+/*
+ * Since WEP is unsupported we can just use the group cipher list with
+ * "Use group cipher" appended
+ */
+#define IE_PAIRWISE_CIPHERS			\
+(						\
+	IE_GROUP_CIPHERS |			\
+	IE_RSN_CIPHER_SUITE_USE_GROUP_CIPHER	\
+)
+
 #define IE_LEN(ie) \
 	((ie) ? (ie)[1] + 2 : 0)
 
@@ -586,6 +619,7 @@ unsigned char *ie_tlv_builder_finalize(s
 					size_t *out_len);
 
 uint32_t ie_rsn_cipher_suite_to_cipher(enum ie_rsn_cipher_suite suite);
+const char *ie_rsn_cipher_suite_to_string(enum ie_rsn_cipher_suite suite);
 
 int ie_parse_rsne(struct ie_tlv_iter *iter, struct ie_rsn_info *info);
 int ie_parse_rsne_from_data(const uint8_t *data, size_t len,
diff -pruN 1.30-1/src/iwd.ap.5 2.3-1/src/iwd.ap.5
--- 1.30-1/src/iwd.ap.5	2022-09-07 18:56:37.000000000 +0000
+++ 2.3-1/src/iwd.ap.5	2023-02-02 13:01:33.000000000 +0000
@@ -61,8 +61,26 @@ Channel
 T}	T{
 Channel number
 .sp
-Optional channel number for the access point to operate on.  Only the
-2.4GHz\-band channels are currently allowed.
+Optional channel number for the access point to operate on. If omitted
+the channel will be chosen automatically. Channels greater than or equal
+to 36 will select the 5GHz band for the AP to operate on.
+.sp
+Note: Due to regulatory requirements the linux kernel does not allow or
+strictly limits 5GHz use in AP mode while in world roaming. These
+frequencies become available once the country is set, either via IWD\(aqs
+main.conf option \fB[General].Country\fP (see \fBman iwd.config\fP) or
+externally (e.g. iw reg set <CC>). If you are having trouble using 5GHz
+ensure the country is set, and that the desired frequency/channel is
+unrestricted.
+T}
+_
+T{
+RekeyTimeout
+T}	T{
+Timeout for PTK rekeys (seconds)
+.sp
+The time interval at which the AP starts a rekey for a given station. If
+not provided a default value of 0 is used (rekeying is disabled).
 T}
 _
 .TE
@@ -92,6 +110,27 @@ Processed passphrase for this network in
 32\-byte pre\-shared key.  Either this or \fIPassphrase\fP must be present.
 T}
 _
+T{
+PairwiseCiphers
+T}	T{
+Comma separated list of pairwise ciphers for the AP supports.
+.sp
+Values can include: TKIP, CCMP, GCMP, GCMP\-256, CCMP\-256
+.sp
+The underlying hardware and IWD\(aqs AP implementation must also support the
+ciphers listed
+T}
+_
+T{
+GroupCipher
+T}	T{
+Group cipher the AP uses
+.sp
+A single cipher value the AP can use as the group cipher. Values are the
+same as pairwise ciphers and the same restrictions apply (hardware and
+IWD implementation must support the cipher)
+T}
+_
 .TE
 .SS IPv4 Network Configuration
 .sp
diff -pruN 1.30-1/src/iwd.ap.rst 2.3-1/src/iwd.ap.rst
--- 1.30-1/src/iwd.ap.rst	2021-06-12 12:33:02.000000000 +0000
+++ 2.3-1/src/iwd.ap.rst	2023-01-23 18:46:38.000000000 +0000
@@ -55,8 +55,23 @@ The group ``[General]`` contains general
    * - Channel
      - Channel number
 
-       Optional channel number for the access point to operate on.  Only the
-       2.4GHz-band channels are currently allowed.
+       Optional channel number for the access point to operate on. If omitted
+       the channel will be chosen automatically. Channels greater than or equal
+       to 36 will select the 5GHz band for the AP to operate on.
+
+       Note: Due to regulatory requirements the linux kernel does not allow or
+       strictly limits 5GHz use in AP mode while in world roaming. These
+       frequencies become available once the country is set, either via IWD's
+       main.conf option ``[General].Country`` (see ``man iwd.config``) or
+       externally (e.g. iw reg set <CC>). If you are having trouble using 5GHz
+       ensure the country is set, and that the desired frequency/channel is
+       unrestricted.
+
+   * - RekeyTimeout
+     - Timeout for PTK rekeys (seconds)
+
+       The time interval at which the AP starts a rekey for a given station. If
+       not provided a default value of 0 is used (rekeying is disabled).
 
 Network Authentication Settings
 -------------------------------
@@ -82,6 +97,21 @@ configuration.
        Processed passphrase for this network in the form of a hex-encoded
        32-byte pre-shared key.  Either this or *Passphrase* must be present.
 
+   * - PairwiseCiphers
+     - Comma separated list of pairwise ciphers for the AP supports.
+
+       Values can include: TKIP, CCMP, GCMP, GCMP-256, CCMP-256
+
+       The underlying hardware and IWD's AP implementation must also support the
+       ciphers listed
+
+   * - GroupCipher
+     - Group cipher the AP uses
+
+       A single cipher value the AP can use as the group cipher. Values are the
+       same as pairwise ciphers and the same restrictions apply (hardware and
+       IWD implementation must support the cipher)
+
 IPv4 Network Configuration
 --------------------------
 
diff -pruN 1.30-1/src/iwd.config.5 2.3-1/src/iwd.config.5
--- 1.30-1/src/iwd.config.5	2022-09-07 18:56:35.000000000 +0000
+++ 2.3-1/src/iwd.config.5	2023-02-02 13:01:31.000000000 +0000
@@ -246,6 +246,19 @@ required are LoadCredentialEncrypted or
 secret identifier should be named whatever SystemdEncrypt is set to.
 T}
 _
+T{
+Country
+T}	T{
+Value: Country Code (ISO Alpha\-2)
+.sp
+Requests the country be set for the system. Note that setting this is
+simply a \fBrequest\fP to set the country, and does not guarantee the
+country will be set. For a self\-managed wiphy it is never possible to set
+the country from userspace. For other devices any regulatory domain
+request is just a \(aqhint\(aq and ultimately left up to the kernel to set the
+country.
+T}
+_
 .TE
 .SS Network
 .sp
@@ -257,11 +270,11 @@ _
 T{
 EnableIPv6
 T}	T{
-Values: true, \fBfalse\fP
+Values: \fBtrue\fP, false
 .sp
 Sets the global default that tells \fBiwd\fP whether it should configure
 IPv6 addresses and routes (either provided via static settings,
-Router Advertisements or DHCPv6 protocol).  This setting is disabled
+Router Advertisements or DHCPv6 protocol).  This setting is enabled
 by default.  This setting can also be overridden on a per\-network basis.
 T}
 _
@@ -354,6 +367,16 @@ networks are highly RSSI sensitive, so i
 prefer 2.4Ghz APs in certain circumstances.
 T}
 _
+T{
+BandModifier6Ghz
+T}	T{
+Values: floating point value (default: \fB1.0\fP)
+.sp
+Increase or decrease the preference for 6GHz access points by increasing
+or decreasing the value of this modifier.  Since 6GHz networks are highly
+RSSI sensitive, this gives an option to prefer 6GHz APs over 5GHz APs.
+T}
+_
 .TE
 .SS Scan
 .sp
diff -pruN 1.30-1/src/iwd.config.rst 2.3-1/src/iwd.config.rst
--- 1.30-1/src/iwd.config.rst	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/src/iwd.config.rst	2022-12-18 19:59:36.000000000 +0000
@@ -206,6 +206,16 @@ The group ``[General]`` contains general
        required are LoadCredentialEncrypted or SetCredentialEncrypted, and the
        secret identifier should be named whatever SystemdEncrypt is set to.
 
+   * - Country
+     - Value: Country Code (ISO Alpha-2)
+
+       Requests the country be set for the system. Note that setting this is
+       simply a **request** to set the country, and does not guarantee the
+       country will be set. For a self-managed wiphy it is never possible to set
+       the country from userspace. For other devices any regulatory domain
+       request is just a 'hint' and ultimately left up to the kernel to set the
+       country.
+
 Network
 -------
 
@@ -218,11 +228,11 @@ The group ``[Network]`` contains network
    :align: left
 
    * - EnableIPv6
-     - Values: true, **false**
+     - Values: **true**, false
 
        Sets the global default that tells **iwd** whether it should configure
        IPv6 addresses and routes (either provided via static settings,
-       Router Advertisements or DHCPv6 protocol).  This setting is disabled
+       Router Advertisements or DHCPv6 protocol).  This setting is enabled
        by default.  This setting can also be overridden on a per-network basis.
 
    * - NameResolvingService
@@ -300,6 +310,13 @@ autoconnect purposes.
        networks are highly RSSI sensitive, so it is still possible for IWD to
        prefer 2.4Ghz APs in certain circumstances.
 
+   * - BandModifier6Ghz
+     - Values: floating point value (default: **1.0**)
+
+       Increase or decrease the preference for 6GHz access points by increasing
+       or decreasing the value of this modifier.  Since 6GHz networks are highly
+       RSSI sensitive, this gives an option to prefer 6GHz APs over 5GHz APs.
+
 Scan
 ----
 
diff -pruN 1.30-1/src/iwd.network.5 2.3-1/src/iwd.network.5
--- 1.30-1/src/iwd.network.5	2022-09-07 18:56:36.000000000 +0000
+++ 2.3-1/src/iwd.network.5	2023-02-02 13:01:33.000000000 +0000
@@ -360,6 +360,27 @@ T}
 _
 T{
 .nf
+EAP\-TLS\-FastReauthentication,
+EAP\-TTLS\-FastReauthentication,
+EAP\-PEAP\-FastReauthentication,
+.fi
+T}	T{
+Values: \fBtrue\fP, false
+.sp
+Controls whether TLS session caching for EAP\-TLS, EAP\-TTLS and EAP\-PEAP
+is used.  This allows for faster re\-connections to EAP\-Enterprise based
+networks.
+.sp
+Some network authenticators may be misconfigured in a way that TLS
+session resumption is allowed but actually attempting it will cause
+the EAP method to fail or time out.  In that case, assuming the
+credentials and other settings are correct, every other connection
+attempt will fail as sessions are cached and forgotten in alternating
+attempts.  Use this setting to disable caching for this network.
+T}
+_
+T{
+.nf
 EAP\-TTLS\-Phase2\-Method
 .fi
 T}	T{
diff -pruN 1.30-1/src/iwd.network.rst 2.3-1/src/iwd.network.rst
--- 1.30-1/src/iwd.network.rst	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/src/iwd.network.rst	2023-02-02 12:57:32.000000000 +0000
@@ -281,6 +281,21 @@ connect to that network.
        domain name. An asterisk segment in the mask matches any label.  An
        asterisk segment at the beginning of the mask matches one or more
        consecutive labels from the beginning of the domain string.
+   * - | EAP-TLS-FastReauthentication,
+       | EAP-TTLS-FastReauthentication,
+       | EAP-PEAP-FastReauthentication,
+     - Values: **true**, false
+
+       Controls whether TLS session caching for EAP-TLS, EAP-TTLS and EAP-PEAP
+       is used.  This allows for faster re-connections to EAP-Enterprise based
+       networks.
+
+       Some network authenticators may be misconfigured in a way that TLS
+       session resumption is allowed but actually attempting it will cause
+       the EAP method to fail or time out.  In that case, assuming the
+       credentials and other settings are correct, every other connection
+       attempt will fail as sessions are cached and forgotten in alternating
+       attempts.  Use this setting to disable caching for this network.
    * - | EAP-TTLS-Phase2-Method
      - | The following values are allowed:
        |    Tunneled-CHAP,
diff -pruN 1.30-1/src/iwd.service.in 2.3-1/src/iwd.service.in
--- 1.30-1/src/iwd.service.in	2021-08-01 20:19:12.000000000 +0000
+++ 2.3-1/src/iwd.service.in	2022-11-18 12:31:49.000000000 +0000
@@ -1,5 +1,6 @@
 [Unit]
 Description=Wireless service
+Documentation=man:iwd(8) man:iwd.config(5) man:iwd.network(5) man:iwd.ap(5)
 After=network-pre.target
 Before=network.target
 Wants=network.target
diff -pruN 1.30-1/src/json.c 2.3-1/src/json.c
--- 1.30-1/src/json.c	2022-03-22 18:12:12.000000000 +0000
+++ 2.3-1/src/json.c	2022-11-18 12:31:49.000000000 +0000
@@ -30,6 +30,9 @@
 
 #include "src/json.h"
 
+#define JSMN_STATIC
+#define JSMN_STRICT
+#define JSMN_PARENT_LINKS
 #include "shared/jsmn.h"
 
 /* Max number of tokens supported. Increase if larger objects are expected */
diff -pruN 1.30-1/src/json.h 2.3-1/src/json.h
--- 1.30-1/src/json.h	2022-01-24 21:28:47.000000000 +0000
+++ 2.3-1/src/json.h	2022-11-18 12:31:49.000000000 +0000
@@ -27,10 +27,10 @@ struct json_iter;
  */
 enum json_type {
 	JSON_UNDEFINED = 0,
-	JSON_OBJECT = 1,
-	JSON_ARRAY = 2,
-	JSON_STRING = 3,
-	JSON_PRIMITIVE = 4
+	JSON_OBJECT = 1 << 0,
+	JSON_ARRAY = 1 << 1,
+	JSON_STRING = 1 << 2,
+	JSON_PRIMITIVE = 1 << 3,
 };
 
 enum json_flag {
diff -pruN 1.30-1/src/manager.c 2.3-1/src/manager.c
--- 1.30-1/src/manager.c	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/src/manager.c	2023-01-23 18:46:38.000000000 +0000
@@ -320,13 +320,18 @@ static void manager_setup_cmd_done(void
 static void manager_del_interface_cb(struct l_genl_msg *msg, void *user_data)
 {
 	struct wiphy_setup_state *state = user_data;
+	int err;
 
 	l_debug("");
 
 	if (state->aborted)
 		return;
 
-	if (l_genl_msg_get_error(msg) < 0) {
+	err = l_genl_msg_get_error(msg);
+
+	if (err == -ENODEV)
+		return;
+	else if (err < 0) {
 		l_error("DEL_INTERFACE failed: %s",
 			strerror(-l_genl_msg_get_error(msg)));
 		state->use_default = true;
@@ -800,6 +805,14 @@ static void manager_config_notify(struct
 	}
 }
 
+static void manager_set_reg_cb(struct l_genl_msg *msg, void *user_data)
+{
+	int err = l_genl_msg_get_error(msg);
+
+	if (err < 0)
+		l_error("Failed to set country (%d)", err);
+}
+
 static int manager_init(void)
 {
 	struct l_genl *genl = iwd_get_genl();
@@ -810,6 +823,7 @@ static int manager_init(void)
 	const char *randomize_str;
 	const char *if_whitelist = iwd_get_iface_whitelist();
 	const char *if_blacklist = iwd_get_iface_blacklist();
+	const char *cc;
 
 	nl80211 = l_genl_family_new(genl, NL80211_GENL_NAME);
 
@@ -853,6 +867,23 @@ static int manager_init(void)
 		goto error;
 	}
 
+	cc = l_settings_get_value(config, "General", "Country");
+	if (cc) {
+		if (strlen(cc) != 2 || !l_ascii_isalpha(cc[0]) ||
+					!l_ascii_isalpha(cc[1])) {
+			l_warn("[General].Country=%s is invalid. Country will "
+				"not be set", cc);
+		} else {
+			msg = l_genl_msg_new(NL80211_CMD_REQ_SET_REG);
+			l_genl_msg_append_attr(msg, NL80211_ATTR_REG_ALPHA2,
+						2, cc);
+			if (!l_genl_family_send(nl80211, msg,
+						manager_set_reg_cb,
+						NULL, NULL))
+				l_warn("Failed to set country");
+		}
+	}
+
 	randomize_str = l_settings_get_value(config, "General",
 							"AddressRandomization");
 	if (randomize_str) {
diff -pruN 1.30-1/src/mpdu.h 2.3-1/src/mpdu.h
--- 1.30-1/src/mpdu.h	2021-09-14 20:01:22.000000000 +0000
+++ 2.3-1/src/mpdu.h	2022-11-18 12:31:49.000000000 +0000
@@ -374,7 +374,7 @@ struct mmpdu_probe_request {
 
 /* 802.11, Section 8.3.3.10 */
 struct mmpdu_probe_response {
-	uint8_t timestamp;
+	__le64 timestamp;
 	__le16 beacon_interval;
 	struct mmpdu_field_capability capability;
 	uint8_t ies[0];
@@ -382,14 +382,14 @@ struct mmpdu_probe_response {
 
 /* 802.11, Section 8.3.3.15 */
 struct mmpdu_timing_advertisement {
-	uint8_t timestamp;
+	__le64 timestamp;
 	struct mmpdu_field_capability capability;
 	uint8_t ies[0];
 } __attribute__ ((packed));
 
 /* 802.11, Section 8.3.3.2 */
 struct mmpdu_beacon {
-	uint8_t timestamp;
+	__le64 timestamp;
 	__le16 beacon_interval;
 	struct mmpdu_field_capability capability;
 	uint8_t ies[0];
diff -pruN 1.30-1/src/netconfig.c 2.3-1/src/netconfig.c
--- 1.30-1/src/netconfig.c	2022-07-15 17:08:06.000000000 +0000
+++ 2.3-1/src/netconfig.c	2023-01-23 18:46:38.000000000 +0000
@@ -27,9 +27,7 @@
 #include <errno.h>
 #include <arpa/inet.h>
 #include <net/if_arp.h>
-#include <netinet/if_ether.h>
 #include <netinet/in.h>
-#include <linux/rtnetlink.h>
 #include <limits.h>
 #include <string.h>
 #include <fcntl.h>
@@ -52,49 +50,13 @@
 #include "src/netconfig.h"
 #include "src/sysfs.h"
 
-struct netconfig {
-	uint32_t ifindex;
-	struct l_dhcp_client *dhcp_client;
-	struct l_dhcp6_client *dhcp6_client;
-	uint8_t rtm_protocol;
-	uint8_t rtm_v6_protocol;
-	struct l_rtnl_address *v4_address;
-	struct l_rtnl_address *v6_address;
-	char **dns4_overrides;
-	char **dns6_overrides;
-	char **dns4_list;
-	char **dns6_list;
-	char *mdns;
-	struct ie_fils_ip_addr_response_info *fils_override;
-	char *v4_gateway_str;
-	char *v6_gateway_str;
-	char *v4_domain;
-	char **v6_domains;
-
-	const struct l_settings *active_settings;
-
-	netconfig_notify_func_t notify;
-	void *user_data;
-
-	struct resolve *resolve;
-
-	struct l_acd *acd;
-
-	uint32_t addr4_add_cmd_id;
-	uint32_t addr6_add_cmd_id;
-	uint32_t route4_add_gateway_cmd_id;
-	uint32_t route6_add_cmd_id;
-};
-
-static struct l_netlink *rtnl;
-static struct l_queue *netconfig_list;
-
 /*
  * Routing priority offset, configurable in main.conf. The route with lower
  * priority offset is preferred.
  */
 static uint32_t ROUTE_PRIORITY_OFFSET;
 static bool ipv6_enabled;
+static char *mdns_global;
 
 static void do_debug(const char *str, void *user_data)
 {
@@ -105,81 +67,56 @@ static void do_debug(const char *str, vo
 
 static void netconfig_free_settings(struct netconfig *netconfig)
 {
-	l_rtnl_address_free(netconfig->v4_address);
-	netconfig->v4_address = NULL;
-	l_rtnl_address_free(netconfig->v6_address);
-	netconfig->v6_address = NULL;
-
-	l_strfreev(netconfig->dns4_overrides);
-	netconfig->dns4_overrides = NULL;
-	l_strfreev(netconfig->dns6_overrides);
-	netconfig->dns6_overrides = NULL;
+	netconfig->enabled[0] = true;
+	netconfig->enabled[1] = false;
+	netconfig->static_config[0] = false;
+	netconfig->static_config[1] = false;
+	netconfig->gateway_overridden[0] = false;
+	netconfig->gateway_overridden[1] = false;
+	netconfig->dns_overridden[0] = false;
+	netconfig->dns_overridden[1] = false;
+	l_netconfig_reset_config(netconfig->nc);
 
 	l_free(netconfig->mdns);
 	netconfig->mdns = NULL;
+
+	l_free(l_steal_ptr(netconfig->fils_override));
 }
 
 static void netconfig_free(void *data)
 {
 	struct netconfig *netconfig = data;
 
-	l_dhcp_client_destroy(netconfig->dhcp_client);
-	l_dhcp6_client_destroy(netconfig->dhcp6_client);
-
+	l_netconfig_destroy(netconfig->nc);
 	l_free(netconfig);
 }
 
-static struct netconfig *netconfig_find(uint32_t ifindex)
+static bool netconfig_addr_to_str(uint8_t af, const void *v4_addr,
+					const void *v6_addr, char *out_str,
+					bool *out_is_zero)
 {
-	const struct l_queue_entry *entry;
-
-	for (entry = l_queue_get_entries(netconfig_list); entry;
-							entry = entry->next) {
-		struct netconfig *netconfig = entry->data;
+	const void *addr = (af == AF_INET ? v4_addr : v6_addr);
+	uint8_t bytes = (af == AF_INET ? 4 : 16);
 
-		if (netconfig->ifindex != ifindex)
-			continue;
-
-		return netconfig;
+	if (l_memeqzero(addr, bytes)) {
+		*out_is_zero = true;
+		return true;
 	}
 
-	return NULL;
-}
-
-static inline char *netconfig_ipv4_to_string(uint32_t addr)
-{
-	struct in_addr in_addr = { .s_addr = addr };
-	char *addr_str = l_malloc(INET_ADDRSTRLEN);
+	*out_is_zero = false;
 
-	if (L_WARN_ON(unlikely(!inet_ntop(AF_INET, &in_addr, addr_str,
-						INET_ADDRSTRLEN)))) {
-		l_free(addr_str);
-		return NULL;
-	}
+	if (L_WARN_ON(!inet_ntop(af, addr, out_str, INET6_ADDRSTRLEN)))
+		return false;
 
-	return addr_str;
+	return true;
 }
 
-static inline char *netconfig_ipv6_to_string(const uint8_t *addr)
+bool netconfig_use_fils_addr(struct netconfig *netconfig, int af)
 {
-	struct in6_addr in6_addr;
-	char *addr_str = l_malloc(INET6_ADDRSTRLEN);
-
-	memcpy(in6_addr.s6_addr, addr, 16);
-
-	if (L_WARN_ON(unlikely(!inet_ntop(AF_INET6, &in6_addr, addr_str,
-						INET6_ADDRSTRLEN)))) {
-		l_free(addr_str);
-		return NULL;
-	}
-
-	return addr_str;
-}
+	if (!netconfig->enabled[INDEX_FOR_AF(af)])
+		return false;
 
-static bool netconfig_use_fils_addr(struct netconfig *netconfig, int af)
-{
-	if ((af == AF_INET ? netconfig->rtm_protocol :
-				netconfig->rtm_v6_protocol) != RTPROT_DHCP)
+	if (netconfig->static_config[INDEX_FOR_AF(af)])
 		return false;
 
 	if (!netconfig->fils_override)
@@ -191,241 +128,10 @@ static bool netconfig_use_fils_addr(stru
 	return !l_memeqzero(netconfig->fils_override->ipv6_addr, 16);
 }
 
-static bool netconfig_use_fils_gateway(struct netconfig *netconfig, int af)
-{
-	if ((af == AF_INET ? netconfig->rtm_protocol :
-				netconfig->rtm_v6_protocol) != RTPROT_DHCP)
-		return false;
-
-	if (!netconfig->fils_override)
-		return false;
-
-	if (af == AF_INET)
-		return !!netconfig->fils_override->ipv4_gateway;
-
-	return !l_memeqzero(netconfig->fils_override->ipv6_gateway, 16);
-}
-
-static char **netconfig_get_dns_list(struct netconfig *netconfig, int af,
-					const uint8_t **out_dns_mac)
-{
-	const struct ie_fils_ip_addr_response_info *fils =
-		netconfig->fils_override;
-
-	if (af == AF_INET) {
-		const struct l_dhcp_lease *lease;
-
-		if (netconfig->dns4_overrides)
-			return l_strv_copy(netconfig->dns4_overrides);
-
-		if (netconfig->rtm_protocol != RTPROT_DHCP)
-			return NULL;
-
-		if (fils && fils->ipv4_dns) {
-			char **dns_list = l_new(char *, 2);
-
-			if (!l_memeqzero(fils->ipv4_dns_mac, 6) &&
-					out_dns_mac &&
-					util_ip_subnet_match(
-							fils->ipv4_prefix_len,
-							&fils->ipv4_addr,
-							&fils->ipv4_dns))
-				*out_dns_mac = fils->ipv4_dns_mac;
-
-			dns_list[0] = netconfig_ipv4_to_string(fils->ipv4_dns);
-			return dns_list;
-		}
-
-		lease = l_dhcp_client_get_lease(netconfig->dhcp_client);
-		if (!lease)
-			return NULL;
-
-		return l_dhcp_lease_get_dns(lease);
-	} else {
-		const struct l_dhcp6_lease *lease;
-
-		if (netconfig->dns6_overrides)
-			return l_strv_copy(netconfig->dns6_overrides);
-
-		if (netconfig->rtm_v6_protocol != RTPROT_DHCP)
-			return NULL;
-
-		if (fils && !l_memeqzero(fils->ipv6_dns, 16)) {
-			char **dns_list = l_new(char *, 2);
-
-			if (!l_memeqzero(fils->ipv6_dns_mac, 6) &&
-					out_dns_mac &&
-					util_ip_subnet_match(
-							fils->ipv6_prefix_len,
-							fils->ipv6_addr,
-							fils->ipv6_dns))
-				*out_dns_mac = fils->ipv6_dns_mac;
-
-			dns_list[0] = netconfig_ipv6_to_string(fils->ipv6_dns);
-			return dns_list;
-		}
-
-		lease = l_dhcp6_client_get_lease(netconfig->dhcp6_client);
-		if (!lease)
-			return NULL;
-
-		return l_dhcp6_lease_get_dns(lease);
-	}
-}
-
-static void netconfig_set_neighbor_entry_cb(int error,
-						uint16_t type, const void *data,
-						uint32_t len, void *user_data)
-{
-	if (error)
-		l_error("l_rtnl_neighbor_set_hwaddr failed: %s (%i)",
-			strerror(-error), error);
-}
-
-static void netconfig_set_dns(struct netconfig *netconfig)
-{
-	if (!netconfig->dns4_list && !netconfig->dns6_list)
-		return;
-
-	if (netconfig->dns4_list && netconfig->dns6_list) {
-		unsigned int n_entries4 = l_strv_length(netconfig->dns4_list);
-		unsigned int n_entries6 = l_strv_length(netconfig->dns6_list);
-		char **dns_list = l_malloc(sizeof(char *) *
-					(n_entries4 + n_entries6 + 1));
-
-		memcpy(dns_list, netconfig->dns4_list,
-			sizeof(char *) * n_entries4);
-		memcpy(dns_list + n_entries4, netconfig->dns6_list,
-			sizeof(char *) * (n_entries6 + 1));
-		resolve_set_dns(netconfig->resolve, dns_list);
-		l_free(dns_list);
-		return;
-	}
-
-	resolve_set_dns(netconfig->resolve,
-			netconfig->dns4_list ?: netconfig->dns6_list);
-}
-
-static bool netconfig_dns_list_update(struct netconfig *netconfig, uint8_t af)
-{
-	const uint8_t *fils_dns_mac = NULL;
-	char ***dns_list_ptr = af == AF_INET ?
-		&netconfig->dns4_list : &netconfig->dns6_list;
-	char **new_dns_list = netconfig_get_dns_list(netconfig, af,
-							&fils_dns_mac);
-
-	if (l_strv_eq(*dns_list_ptr, new_dns_list)) {
-		l_strv_free(new_dns_list);
-		return false;
-	}
-
-	l_strv_free(*dns_list_ptr);
-	*dns_list_ptr = new_dns_list;
-
-	if (fils_dns_mac) {
-		const struct ie_fils_ip_addr_response_info *fils =
-			netconfig->fils_override;
-		const void *dns_ip = af == AF_INET ?
-			(const void *) &fils->ipv4_dns :
-			(const void *) &fils->ipv6_dns;
-
-		if (!l_rtnl_neighbor_set_hwaddr(rtnl, netconfig->ifindex, af,
-						dns_ip, fils_dns_mac, 6,
-						netconfig_set_neighbor_entry_cb,
-						NULL, NULL))
-			l_debug("l_rtnl_neighbor_set_hwaddr failed");
-	}
-
-	return true;
-}
-
-static void append_domain(char **domains, unsigned int *n_domains,
-				size_t max, char *domain)
-{
-	unsigned int i;
-
-	if (*n_domains == max)
-		return;
-
-	for (i = 0; i < *n_domains; i++)
-		if (!strcmp(domains[i], domain))
-			return;
-
-	domains[*n_domains] = domain;
-	*n_domains += 1;
-}
-
-static void netconfig_set_domains(struct netconfig *netconfig)
-{
-	char *domains[31];
-	unsigned int n_domains = 0;
-	char **p;
-
-	memset(domains, 0, sizeof(domains));
-
-	append_domain(domains, &n_domains,
-			L_ARRAY_SIZE(domains) - 1, netconfig->v4_domain);
-
-	for (p = netconfig->v6_domains; p && *p; p++)
-		append_domain(domains, &n_domains,
-				L_ARRAY_SIZE(domains) - 1, *p);
-
-	resolve_set_domains(netconfig->resolve, domains);
-}
-
-static bool netconfig_domains_update(struct netconfig *netconfig, uint8_t af)
-{
-	bool changed = false;
-
-	if (af == AF_INET) {
-		/* Allow to override the DHCP domain name with setting entry. */
-		char *v4_domain = l_settings_get_string(
-						netconfig->active_settings,
-						"IPv4", "DomainName");
-
-		if (!v4_domain && netconfig->rtm_protocol == RTPROT_DHCP) {
-			const struct l_dhcp_lease *lease =
-				l_dhcp_client_get_lease(netconfig->dhcp_client);
-
-			if (lease)
-				v4_domain = l_dhcp_lease_get_domain_name(lease);
-		}
-
-		if (l_streq0(v4_domain, netconfig->v4_domain))
-			l_free(v4_domain);
-		else {
-			l_free(netconfig->v4_domain);
-			netconfig->v4_domain = v4_domain;
-			changed = true;
-		}
-	} else {
-		char **v6_domains = NULL;
-
-		if (netconfig->rtm_v6_protocol == RTPROT_DHCP) {
-			const struct l_dhcp6_lease *lease =
-				l_dhcp6_client_get_lease(
-						netconfig->dhcp6_client);
-
-			if (lease)
-				v6_domains = l_dhcp6_lease_get_domains(lease);
-		}
-
-		if (l_strv_eq(netconfig->v6_domains, v6_domains))
-			l_strv_free(v6_domains);
-		else {
-			l_strv_free(netconfig->v6_domains);
-			netconfig->v6_domains = v6_domains;
-			changed = true;
-		}
-	}
-
-	return changed;
-}
-
 static struct l_rtnl_address *netconfig_get_static4_address(
 				const struct l_settings *active_settings)
 {
-	struct l_rtnl_address *ifaddr = NULL;
+	_auto_(l_rtnl_address_free) struct l_rtnl_address *ifaddr = NULL;
 	L_AUTO_FREE_VAR(char *, ip) = NULL;
 	L_AUTO_FREE_VAR(char *, netmask) = NULL;
 	struct in_addr in_addr;
@@ -433,13 +139,22 @@ static struct l_rtnl_address *netconfig_
 	uint32_t prefix_len;
 
 	ip = l_settings_get_string(active_settings, "IPv4", "Address");
-	if (!ip)
+	if (unlikely(!ip)) {
+		l_error("netconfig: Can't load IPv4.Address");
 		return NULL;
+	}
+
+	if (l_settings_has_key(active_settings, "IPv4", "Netmask") &&
+			!(netmask = l_settings_get_string(active_settings,
+								"IPv4",
+								"Netmask"))) {
+		l_error("netconfig: Can't load IPv4.Netmask");
+		return NULL;
+	}
 
-	netmask = l_settings_get_string(active_settings, "IPv4", "Netmask");
 	if (netmask) {
 		if (inet_pton(AF_INET, netmask, &in_addr) != 1) {
-			l_error("netconfig: Can't parse IPv4 Netmask");
+			l_error("netconfig: Can't parse IPv4.Netmask");
 			return NULL;
 		}
 
@@ -447,14 +162,14 @@ static struct l_rtnl_address *netconfig_
 
 		if (ntohl(in_addr.s_addr) !=
 				util_netmask_from_prefix(prefix_len)) {
-			l_error("netconfig: Invalid IPv4 Netmask");
+			l_error("netconfig: Invalid IPv4.Netmask");
 			return NULL;
 		}
 	} else
 		prefix_len = 24;
 
 	ifaddr = l_rtnl_address_new(ip, prefix_len);
-	if (!ifaddr) {
+	if (!ifaddr || l_rtnl_address_get_family(ifaddr) != AF_INET) {
 		l_error("netconfig: Unable to parse IPv4.Address");
 		return NULL;
 	}
@@ -462,52 +177,10 @@ static struct l_rtnl_address *netconfig_
 	broadcast = l_settings_get_string(active_settings, "IPv4", "Broadcast");
 	if (broadcast && !l_rtnl_address_set_broadcast(ifaddr, broadcast)) {
 		l_error("netconfig: Unable to parse IPv4.Broadcast");
-		l_rtnl_address_free(ifaddr);
 		return NULL;
 	}
 
-	l_rtnl_address_set_noprefixroute(ifaddr, true);
-	return ifaddr;
-}
-
-static char *netconfig_ipv4_get_gateway(struct netconfig *netconfig,
-					const uint8_t **out_mac)
-{
-	const struct l_dhcp_lease *lease;
-	char *gateway;
-	const struct ie_fils_ip_addr_response_info *fils =
-		netconfig->fils_override;
-
-	switch (netconfig->rtm_protocol) {
-	case RTPROT_STATIC:
-		gateway = l_settings_get_string(netconfig->active_settings,
-							"IPv4", "Gateway");
-		if (!gateway)
-			gateway = l_settings_get_string(
-						netconfig->active_settings,
-						"IPv4", "gateway");
-
-		return gateway;
-
-	case RTPROT_DHCP:
-		if (netconfig_use_fils_gateway(netconfig, AF_INET)) {
-			gateway = netconfig_ipv4_to_string(fils->ipv4_gateway);
-
-			if (gateway && out_mac &&
-					!l_memeqzero(fils->ipv4_gateway_mac, 6))
-				*out_mac = fils->ipv4_gateway_mac;
-
-			return gateway;
-		}
-
-		lease = l_dhcp_client_get_lease(netconfig->dhcp_client);
-		if (!lease)
-			return NULL;
-
-		return l_dhcp_lease_get_gateway(lease);
-	}
-
-	return NULL;
+	return l_steal_ptr(ifaddr);
 }
 
 static struct l_rtnl_address *netconfig_get_static6_address(
@@ -516,12 +189,14 @@ static struct l_rtnl_address *netconfig_
 	L_AUTO_FREE_VAR(char *, ip);
 	char *p;
 	char *endp;
-	struct l_rtnl_address *ret;
-	uint32_t prefix_len = 128;
+	_auto_(l_rtnl_address_free) struct l_rtnl_address *ret = NULL;
+	uint32_t prefix_len = 64;
 
 	ip = l_settings_get_string(active_settings, "IPv6", "Address");
-	if (!ip)
+	if (unlikely(!ip)) {
+		l_error("netconfig: Can't load IPv6.Address");
 		return NULL;
+	}
 
 	p = strrchr(ip, '/');
 	if (!p)
@@ -542,966 +217,343 @@ static struct l_rtnl_address *netconfig_
 
 no_prefix_len:
 	ret = l_rtnl_address_new(ip, prefix_len);
-	if (!ret)
-		l_error("netconfig: Invalid IPv6 address %s is "
-				"provided in network configuration file.", ip);
-
-	return ret;
-}
-
-static struct l_rtnl_route *netconfig_get_static6_gateway(
-						struct netconfig *netconfig,
-						char **out_str,
-						const uint8_t **out_mac)
-{
-	L_AUTO_FREE_VAR(char *, gateway);
-	struct l_rtnl_route *ret;
-	const uint8_t *mac = NULL;
-
-	gateway = l_settings_get_string(netconfig->active_settings,
-						"IPv6", "Gateway");
-	if (!gateway && netconfig_use_fils_gateway(netconfig, AF_INET6)) {
-		gateway = netconfig_ipv6_to_string(
-					netconfig->fils_override->ipv6_gateway);
-
-		if (!l_memeqzero(netconfig->fils_override->ipv6_gateway_mac, 6))
-			mac = netconfig->fils_override->ipv6_gateway_mac;
-	} else if (!gateway)
-		return NULL;
-
-	ret = l_rtnl_route_new_gateway(gateway);
-	if (!ret) {
-		l_error("netconfig: Invalid IPv6 gateway address %s is "
-			"provided in network configuration file.",
-			gateway);
-		return ret;
-	}
-
-	l_rtnl_route_set_priority(ret, ROUTE_PRIORITY_OFFSET);
-	l_rtnl_route_set_protocol(ret, RTPROT_STATIC);
-	*out_str = l_steal_ptr(gateway);
-	*out_mac = mac;
-
-	return ret;
-}
-
-static struct l_rtnl_address *netconfig_get_dhcp4_address(
-						struct netconfig *netconfig)
-{
-	const struct l_dhcp_lease *lease =
-			l_dhcp_client_get_lease(netconfig->dhcp_client);
-	L_AUTO_FREE_VAR(char *, ip) = NULL;
-	L_AUTO_FREE_VAR(char *, broadcast) = NULL;
-	uint32_t prefix_len;
-	struct l_rtnl_address *ret;
-
-	if (L_WARN_ON(!lease))
+	if (!ret || l_rtnl_address_get_family(ret) != AF_INET6) {
+		l_error("netconfig: Invalid IPv6 address %s provided in "
+			"network configuration file.", ip);
 		return NULL;
-
-	ip = l_dhcp_lease_get_address(lease);
-	broadcast = l_dhcp_lease_get_broadcast(lease);
-
-	prefix_len = l_dhcp_lease_get_prefix_length(lease);
-	if (!prefix_len)
-		prefix_len = 24;
-
-	ret = l_rtnl_address_new(ip, prefix_len);
-	if (!ret)
-		return ret;
-
-	if (broadcast)
-		l_rtnl_address_set_broadcast(ret, broadcast);
-
-	l_rtnl_address_set_noprefixroute(ret, true);
-	return ret;
-}
-
-static void netconfig_gateway_to_arp(struct netconfig *netconfig)
-{
-	const struct l_dhcp_lease *lease;
-	_auto_(l_free) char *server_id = NULL;
-	_auto_(l_free) char *gw = NULL;
-	const uint8_t *server_mac;
-	struct in_addr in_gw;
-
-	/* Can only do this for DHCP in certain network setups */
-	if (netconfig->rtm_protocol != RTPROT_DHCP)
-		return;
-
-	lease = l_dhcp_client_get_lease(netconfig->dhcp_client);
-	if (!lease)
-		return;
-
-	server_id = l_dhcp_lease_get_server_id(lease);
-	gw = l_dhcp_lease_get_gateway(lease);
-	server_mac = l_dhcp_lease_get_server_mac(lease);
-
-	if (!gw || strcmp(server_id, gw) || !server_mac)
-		return;
-
-	l_debug("Gateway MAC is known, setting into ARP cache");
-	in_gw.s_addr = l_dhcp_lease_get_gateway_u32(lease);
-
-	if (!l_rtnl_neighbor_set_hwaddr(rtnl, netconfig->ifindex, AF_INET,
-					&in_gw, server_mac, ETH_ALEN,
-					netconfig_set_neighbor_entry_cb, NULL,
-					NULL))
-		l_debug("l_rtnl_neighbor_set_hwaddr failed");
-}
-
-static void netconfig_ifaddr_added(struct netconfig *netconfig,
-					const struct ifaddrmsg *ifa,
-					uint32_t len)
-{
-	L_AUTO_FREE_VAR(char *, label) = NULL;
-	L_AUTO_FREE_VAR(char *, ip) = NULL;
-	L_AUTO_FREE_VAR(char *, broadcast) = NULL;
-
-	l_rtnl_ifaddr4_extract(ifa, len, &label, &ip, &broadcast);
-	l_debug("%s: ifaddr %s/%u broadcast %s", label,
-					ip, ifa->ifa_prefixlen, broadcast);
-}
-
-static void netconfig_ifaddr_deleted(struct netconfig *netconfig,
-					const struct ifaddrmsg *ifa,
-					uint32_t len)
-{
-	L_AUTO_FREE_VAR(char *, ip);
-
-	l_rtnl_ifaddr4_extract(ifa, len, NULL, &ip, NULL);
-	l_debug("ifaddr %s/%u", ip, ifa->ifa_prefixlen);
-}
-
-static void netconfig_ifaddr_notify(uint16_t type, const void *data,
-						uint32_t len, void *user_data)
-{
-	const struct ifaddrmsg *ifa = data;
-	struct netconfig *netconfig;
-	uint32_t bytes;
-
-	netconfig = netconfig_find(ifa->ifa_index);
-	if (!netconfig)
-		/* Ignore the interfaces which aren't managed by iwd. */
-		return;
-
-	bytes = len - NLMSG_ALIGN(sizeof(struct ifaddrmsg));
-
-	switch (type) {
-	case RTM_NEWADDR:
-		netconfig_ifaddr_added(netconfig, ifa, bytes);
-		break;
-	case RTM_DELADDR:
-		netconfig_ifaddr_deleted(netconfig, ifa, bytes);
-		break;
-	}
-}
-
-static void netconfig_ifaddr_cmd_cb(int error, uint16_t type,
-						const void *data, uint32_t len,
-						void *user_data)
-{
-	if (error) {
-		l_error("netconfig: ifaddr command failure. "
-				"Error %d: %s", error, strerror(-error));
-		return;
-	}
-
-	if (type != RTM_NEWADDR)
-		return;
-
-	netconfig_ifaddr_notify(type, data, len, user_data);
-}
-
-static void netconfig_ifaddr_ipv6_added(struct netconfig *netconfig,
-					const struct ifaddrmsg *ifa,
-					uint32_t len)
-{
-	struct in6_addr in6;
-	L_AUTO_FREE_VAR(char *, ip) = NULL;
-
-	if (ifa->ifa_flags & IFA_F_TENTATIVE)
-		return;
-
-	l_rtnl_ifaddr6_extract(ifa, len, &ip);
-
-	l_debug("ifindex %u: ifaddr %s/%u", netconfig->ifindex,
-			ip, ifa->ifa_prefixlen);
-
-	if (netconfig->rtm_v6_protocol != RTPROT_DHCP ||
-			netconfig_use_fils_addr(netconfig, AF_INET6))
-		return;
-
-	inet_pton(AF_INET6, ip, &in6);
-	if (!IN6_IS_ADDR_LINKLOCAL(&in6))
-		return;
-
-	l_dhcp6_client_set_link_local_address(netconfig->dhcp6_client, ip);
-
-	if (l_dhcp6_client_start(netconfig->dhcp6_client))
-		return;
-
-	l_error("netconfig: Failed to start DHCPv6 client for "
-			"interface %u", netconfig->ifindex);
-}
-
-static void netconfig_ifaddr_ipv6_deleted(struct netconfig *netconfig,
-						const struct ifaddrmsg *ifa,
-						uint32_t len)
-{
-	L_AUTO_FREE_VAR(char *, ip);
-
-	l_rtnl_ifaddr6_extract(ifa, len, &ip);
-	l_debug("ifindex %u: ifaddr %s/%u", netconfig->ifindex,
-			ip, ifa->ifa_prefixlen);
-}
-
-static void netconfig_ifaddr_ipv6_notify(uint16_t type, const void *data,
-						uint32_t len, void *user_data)
-{
-	const struct ifaddrmsg *ifa = data;
-	struct netconfig *netconfig;
-	uint32_t bytes;
-
-	netconfig = netconfig_find(ifa->ifa_index);
-	if (!netconfig)
-		/* Ignore the interfaces which aren't managed by iwd. */
-		return;
-
-	bytes = len - NLMSG_ALIGN(sizeof(struct ifaddrmsg));
-
-	switch (type) {
-	case RTM_NEWADDR:
-		netconfig_ifaddr_ipv6_added(netconfig, ifa, bytes);
-		break;
-	case RTM_DELADDR:
-		netconfig_ifaddr_ipv6_deleted(netconfig, ifa, bytes);
-		break;
-	}
-}
-
-static void netconfig_ifaddr_ipv6_cmd_cb(int error, uint16_t type,
-						const void *data, uint32_t len,
-						void *user_data)
-{
-	if (error) {
-		l_error("netconfig: ifaddr IPv6 command failure. "
-				"Error %d: %s", error, strerror(-error));
-		return;
-	}
-
-	if (type != RTM_NEWADDR)
-		return;
-
-	netconfig_ifaddr_ipv6_notify(type, data, len, user_data);
-}
-
-static void netconfig_route_generic_cb(int error, uint16_t type,
-					const void *data, uint32_t len,
-					void *user_data)
-{
-	if (error) {
-		l_error("netconfig: Failed to add route. Error %d: %s",
-						error, strerror(-error));
-		return;
 	}
-}
-
-static void netconfig_route_add_cmd_cb(int error, uint16_t type,
-						const void *data, uint32_t len,
-						void *user_data)
-{
-	struct netconfig *netconfig = user_data;
-
-	netconfig->route4_add_gateway_cmd_id = 0;
-
-	if (error) {
-		l_error("netconfig: Failed to add route. Error %d: %s",
-						error, strerror(-error));
-		return;
-	}
-
-	if (!netconfig->notify)
-		return;
-
-	netconfig->notify(NETCONFIG_EVENT_CONNECTED, netconfig->user_data);
-	netconfig->notify = NULL;
-}
-
-static void netconfig_route6_add_cb(int error, uint16_t type,
-					const void *data, uint32_t len,
-					void *user_data)
-{
-	struct netconfig *netconfig = user_data;
-
-	netconfig->route6_add_cmd_id = 0;
 
-	if (error) {
-		l_error("netconfig: Failed to add route. Error %d: %s",
-						error, strerror(-error));
-		return;
-	}
+	return l_steal_ptr(ret);
 }
 
-static bool netconfig_ipv4_subnet_route_install(struct netconfig *netconfig)
+static bool netconfig_load_dns(struct netconfig *netconfig,
+				const struct l_settings *active_settings,
+				const char *group_name, uint8_t family)
 {
-	struct in_addr in_addr;
-	char ip[INET_ADDRSTRLEN];
-	char network[INET_ADDRSTRLEN];
-	unsigned int prefix_len =
-		l_rtnl_address_get_prefix_length(netconfig->v4_address);
-
-	if (!l_rtnl_address_get_address(netconfig->v4_address, ip) ||
-			inet_pton(AF_INET, ip, &in_addr) < 1)
-		return false;
+	_auto_(l_strv_free) char **dns_str_list = NULL;
 
-	in_addr.s_addr = in_addr.s_addr &
-				htonl(0xFFFFFFFFLU << (32 - prefix_len));
+	if (!l_settings_has_key(active_settings, group_name, "DNS"))
+		return true;
 
-	if (!inet_ntop(AF_INET, &in_addr, network, INET_ADDRSTRLEN))
+	dns_str_list = l_settings_get_string_list(active_settings,
+							group_name, "DNS", ' ');
+	if (unlikely(!dns_str_list)) {
+		l_error("netconfig: Can't load [%s].DNS", group_name);
 		return false;
+	}
 
-	if (!l_rtnl_route4_add_connected(rtnl, netconfig->ifindex,
-						prefix_len, network, ip,
-						netconfig->rtm_protocol,
-						netconfig_route_generic_cb,
-						netconfig, NULL)) {
-		l_error("netconfig: Failed to add subnet route.");
+	if (unlikely(!l_netconfig_set_dns_override(netconfig->nc, family,
+							dns_str_list))) {
+		l_error("netconfig: l_netconfig_set_dns_override(%s) failed",
+			family == AF_INET ? "AF_INET" : "AF_INET6");
 		return false;
 	}
 
+	netconfig->dns_overridden[INDEX_FOR_AF(family)] = true;
 	return true;
 }
 
-static bool netconfig_ipv4_gateway_route_install(struct netconfig *netconfig)
+static bool netconfig_load_gateway(struct netconfig *netconfig,
+				const struct l_settings *active_settings,
+				const char *group_name, uint8_t family)
 {
-	L_AUTO_FREE_VAR(char *, gateway) = NULL;
-	const uint8_t *gateway_mac = NULL;
-	struct in_addr in_addr;
-	char ip[INET_ADDRSTRLEN];
-
-	gateway = netconfig_ipv4_get_gateway(netconfig, &gateway_mac);
-	if (!gateway) {
-		l_debug("No gateway obtained from %s.",
-				netconfig->rtm_protocol == RTPROT_STATIC ?
-				"setting file" : "DHCPv4 lease");
-
-		if (netconfig->notify) {
-			netconfig->notify(NETCONFIG_EVENT_CONNECTED,
-						netconfig->user_data);
-			netconfig->notify = NULL;
-		}
+	_auto_(l_free) char *gateway_str = NULL;
 
+	if (!l_settings_has_key(active_settings, group_name, "Gateway"))
 		return true;
-	}
 
-	if (!l_rtnl_address_get_address(netconfig->v4_address, ip) ||
-			inet_pton(AF_INET, ip, &in_addr) < 1)
+	gateway_str = l_settings_get_string(active_settings, group_name,
+						"Gateway");
+	if (unlikely(!gateway_str)) {
+		l_error("netconfig: Can't load [%s].Gateway", group_name);
 		return false;
+	}
 
-	netconfig->route4_add_gateway_cmd_id =
-		l_rtnl_route4_add_gateway(rtnl, netconfig->ifindex, gateway, ip,
-						ROUTE_PRIORITY_OFFSET,
-						netconfig->rtm_protocol,
-						netconfig_route_add_cmd_cb,
-						netconfig, NULL);
-	if (!netconfig->route4_add_gateway_cmd_id) {
-		l_error("netconfig: Failed to add route for: %s gateway.",
-								gateway);
-
+	if (unlikely(!l_netconfig_set_gateway_override(netconfig->nc, family,
+							gateway_str))) {
+		l_error("netconfig: l_netconfig_set_gateway_override(%s) "
+			"failed", family == AF_INET ? "AF_INET" : "AF_INET6");
 		return false;
 	}
 
-	/*
-	 * Attempt to use the gateway MAC address received from the AP by
-	 * writing the mapping directly into the netdev's ARP table so as
-	 * to save one data frame roundtrip before first IP connections
-	 * are established.  This is very low-priority but print error
-	 * messages just because they may indicate bigger problems.
-	 */
-	if (gateway_mac && !l_rtnl_neighbor_set_hwaddr(rtnl, netconfig->ifindex,
-					AF_INET,
-					&netconfig->fils_override->ipv4_gateway,
-					gateway_mac, 6,
-					netconfig_set_neighbor_entry_cb, NULL,
-					NULL))
-		l_debug("l_rtnl_neighbor_set_hwaddr failed");
-
+	netconfig->gateway_overridden[INDEX_FOR_AF(family)] = true;
 	return true;
 }
 
-static void netconfig_ipv4_ifaddr_add_cmd_cb(int error, uint16_t type,
-						const void *data, uint32_t len,
-						void *user_data)
-{
-	struct netconfig *netconfig = user_data;
-
-	netconfig->addr4_add_cmd_id = 0;
-
-	if (error && error != -EEXIST) {
-		l_error("netconfig: Failed to add IP address. "
-				"Error %d: %s", error, strerror(-error));
-		return;
-	}
-
-	netconfig_gateway_to_arp(netconfig);
-
-	if (!netconfig_ipv4_subnet_route_install(netconfig) ||
-			!netconfig_ipv4_gateway_route_install(netconfig))
-		return;
-
-	netconfig_set_dns(netconfig);
-	netconfig_set_domains(netconfig);
-}
-
-static void netconfig_ipv6_ifaddr_add_cmd_cb(int error, uint16_t type,
-						const void *data, uint32_t len,
-						void *user_data)
-{
-	struct netconfig *netconfig = user_data;
-	struct l_rtnl_route *gateway;
-	const uint8_t *gateway_mac;
-
-	netconfig->addr6_add_cmd_id = 0;
-
-	if (error && error != -EEXIST) {
-		l_error("netconfig: Failed to add IPv6 address. "
-				"Error %d: %s", error, strerror(-error));
-		return;
-	}
-
-	gateway = netconfig_get_static6_gateway(netconfig,
-						&netconfig->v6_gateway_str,
-						&gateway_mac);
-	if (gateway) {
-		netconfig->route6_add_cmd_id = l_rtnl_route_add(rtnl,
-							netconfig->ifindex,
-							gateway,
-							netconfig_route6_add_cb,
-							netconfig, NULL);
-		L_WARN_ON(unlikely(!netconfig->route6_add_cmd_id));
-		l_rtnl_route_free(gateway);
-
-		if (gateway_mac && !l_rtnl_neighbor_set_hwaddr(rtnl,
-					netconfig->ifindex, AF_INET6,
-					netconfig->fils_override->ipv6_gateway,
-					gateway_mac, 6,
-					netconfig_set_neighbor_entry_cb, NULL,
-					NULL))
-			l_debug("l_rtnl_neighbor_set_hwaddr failed");
-	}
-
-	netconfig_set_dns(netconfig);
-	netconfig_set_domains(netconfig);
-}
-
-static void netconfig_ifaddr_del_cmd_cb(int error, uint16_t type,
-						const void *data, uint32_t len,
-						void *user_data)
+bool netconfig_load_settings(struct netconfig *netconfig,
+				const struct l_settings *active_settings)
 {
-	if (error == -ENODEV)
-		/* The device is unplugged, we are done. */
-		return;
-
-	if (!error)
-		/*
-		 * The kernel removes all of the routes associated with the
-		 * deleted IP on its own. There is no need to explicitly remove
-		 * them.
-		 */
-		return;
+	bool send_hostname = false;
+	char hostname[HOST_NAME_MAX + 1];
+	_auto_(l_free) char *mdns = NULL;
+	bool success = true;
+	bool static_ipv4 = false;
+	bool static_ipv6 = false;
+	bool enable_ipv4 = true;
+	bool enable_ipv6 = ipv6_enabled;
 
-	l_error("netconfig: Failed to delete IP address. "
-				"Error %d: %s", error, strerror(-error));
-}
+	netconfig_free_settings(netconfig);
 
-static void netconfig_ipv4_dhcp_event_handler(struct l_dhcp_client *client,
-						enum l_dhcp_client_event event,
-						void *userdata)
-{
-	struct netconfig *netconfig = userdata;
+	/*
+	 * Note we try to print errors and continue validating the
+	 * configuration until we've gone through all the settings so
+	 * as to make fixing the settings more efficient for the user.
+	 */
 
-	l_debug("DHCPv4 event %d", event);
+	if (l_settings_has_key(active_settings, "IPv4", "Address")) {
+		_auto_(l_rtnl_address_free) struct l_rtnl_address *addr =
+			netconfig_get_static4_address(active_settings);
 
-	switch (event) {
-	case L_DHCP_CLIENT_EVENT_IP_CHANGED:
-		L_WARN_ON(!l_rtnl_ifaddr_delete(rtnl, netconfig->ifindex,
-					netconfig->v4_address,
-					netconfig_ifaddr_del_cmd_cb,
-					netconfig, NULL));
-		/* Fall through. */
-	case L_DHCP_CLIENT_EVENT_LEASE_OBTAINED:
-	{
-		char *gateway_str;
-		struct l_rtnl_address *address;
-
-		gateway_str = netconfig_ipv4_get_gateway(netconfig, NULL);
-		if (l_streq0(netconfig->v4_gateway_str, gateway_str))
-			l_free(gateway_str);
-		else {
-			l_free(netconfig->v4_gateway_str);
-			netconfig->v4_gateway_str = gateway_str;
+		if (unlikely(!addr)) {
+			success = false;
+			goto ipv6_addr;
 		}
 
-		address = netconfig_get_dhcp4_address(netconfig);
-		l_rtnl_address_free(netconfig->v4_address);
-		netconfig->v4_address = address;
-
-		if (!netconfig->v4_address) {
-			l_error("netconfig: Failed to obtain IP addresses from "
-							"DHCPv4 lease.");
-			return;
+		if (!l_netconfig_set_static_addr(netconfig->nc, AF_INET,
+							addr)) {
+			l_error("netconfig: l_netconfig_set_static_addr("
+				"AF_INET) failed");
+			success = false;
+			goto ipv6_addr;
 		}
 
-		netconfig_dns_list_update(netconfig, AF_INET);
-		netconfig_domains_update(netconfig, AF_INET);
-
-		L_WARN_ON(!(netconfig->addr4_add_cmd_id =
-				l_rtnl_ifaddr_add(rtnl, netconfig->ifindex,
-					netconfig->v4_address,
-					netconfig_ipv4_ifaddr_add_cmd_cb,
-					netconfig, NULL)));
-		break;
+		static_ipv4 = true;
 	}
-	case L_DHCP_CLIENT_EVENT_LEASE_RENEWED:
-		break;
-	case L_DHCP_CLIENT_EVENT_LEASE_EXPIRED:
-		L_WARN_ON(!l_rtnl_ifaddr_delete(rtnl, netconfig->ifindex,
-					netconfig->v4_address,
-					netconfig_ifaddr_del_cmd_cb,
-					netconfig, NULL));
-		l_rtnl_address_free(netconfig->v4_address);
-		netconfig->v4_address = NULL;
-		l_free(l_steal_ptr(netconfig->v4_gateway_str));
 
-		/* Fall through. */
-	case L_DHCP_CLIENT_EVENT_NO_LEASE:
-		/*
-		 * The requested address is no longer available, try to restart
-		 * the client.
-		 */
-		if (!l_dhcp_client_start(client))
-			l_error("netconfig: Failed to re-start DHCPv4 client "
-					"for interface %u", netconfig->ifindex);
-
-		break;
-	default:
-		l_error("netconfig: Received unsupported DHCPv4 event: %d",
-									event);
-	}
-}
+ipv6_addr:
+	if (l_settings_has_key(active_settings, "IPv6", "Address")) {
+		_auto_(l_rtnl_address_free) struct l_rtnl_address *addr =
+			netconfig_get_static6_address(active_settings);
 
-static void netconfig_dhcp6_event_handler(struct l_dhcp6_client *client,
-						enum l_dhcp6_client_event event,
-						void *userdata)
-{
-	struct netconfig *netconfig = userdata;
+		if (unlikely(!addr)) {
+			success = false;
+			goto gateway;
+		}
 
-	switch (event) {
-	case L_DHCP6_CLIENT_EVENT_IP_CHANGED:
-	case L_DHCP6_CLIENT_EVENT_LEASE_OBTAINED:
-	case L_DHCP6_CLIENT_EVENT_LEASE_RENEWED:
-	{
-		const struct l_dhcp6_lease *lease =
-			l_dhcp6_client_get_lease(netconfig->dhcp6_client);
-		_auto_(l_free) char *addr_str =
-			l_dhcp6_lease_get_address(lease);
-		struct l_rtnl_address *address;
-		struct l_icmp6_client *icmp6 =
-			l_dhcp6_client_get_icmp6(netconfig->dhcp6_client);
-		const struct l_icmp6_router *router =
-			l_icmp6_client_get_router(icmp6);
-		char *gateway_str = l_icmp6_router_get_address(router);
-
-		if (l_streq0(netconfig->v6_gateway_str, gateway_str))
-			l_free(gateway_str);
-		else {
-			l_free(netconfig->v6_gateway_str);
-			netconfig->v6_gateway_str = gateway_str;
+		if (!l_netconfig_set_static_addr(netconfig->nc, AF_INET6,
+							addr)) {
+			l_error("netconfig: l_netconfig_set_static_addr("
+				"AF_INET6) failed");
+			success = false;
+			goto gateway;
 		}
 
-		address = l_rtnl_address_new(addr_str,
-					l_dhcp6_lease_get_prefix_length(lease));
-		l_rtnl_address_free(netconfig->v6_address);
-		netconfig->v6_address = address;
-
-		netconfig_dns_list_update(netconfig, AF_INET6);
-		netconfig_domains_update(netconfig, AF_INET6);
-		netconfig_set_dns(netconfig);
-		netconfig_set_domains(netconfig);
-		break;
-	}
-	case L_DHCP6_CLIENT_EVENT_LEASE_EXPIRED:
-		l_debug("Lease for interface %u expired", netconfig->ifindex);
-		netconfig_dns_list_update(netconfig, AF_INET6);
-		netconfig_domains_update(netconfig, AF_INET6);
-		netconfig_set_dns(netconfig);
-		netconfig_set_domains(netconfig);
-		l_rtnl_address_free(netconfig->v6_address);
-		netconfig->v6_address = NULL;
-		l_free(l_steal_ptr(netconfig->v6_gateway_str));
-
-		/* Fall through */
-	case L_DHCP6_CLIENT_EVENT_NO_LEASE:
-		if (!l_dhcp6_client_start(netconfig->dhcp6_client))
-			l_error("netconfig: Failed to re-start DHCPv6 client "
-					"for interface %u", netconfig->ifindex);
-		break;
+		static_ipv6 = true;
 	}
-}
 
-static void netconfig_remove_v4_address(struct netconfig *netconfig)
-{
-	if (!netconfig->v4_address)
-		return;
-
-	L_WARN_ON(!l_rtnl_ifaddr_delete(rtnl, netconfig->ifindex,
-					netconfig->v4_address,
-					netconfig_ifaddr_del_cmd_cb,
-					netconfig, NULL));
-	l_rtnl_address_free(netconfig->v4_address);
-	netconfig->v4_address = NULL;
-}
+gateway:
+	if (!netconfig_load_gateway(netconfig, active_settings,
+					"IPv4", AF_INET))
+		success = false;
 
-static void netconfig_reset_v4(struct netconfig *netconfig)
-{
-	if (netconfig->rtm_protocol) {
-		netconfig_remove_v4_address(netconfig);
-
-		l_strv_free(l_steal_ptr(netconfig->dns4_overrides));
-		l_strv_free(l_steal_ptr(netconfig->dns4_list));
+	if (!netconfig_load_gateway(netconfig, active_settings,
+					"IPv6", AF_INET6))
+		success = false;
 
-		l_dhcp_client_stop(netconfig->dhcp_client);
-		netconfig->rtm_protocol = 0;
+	if (!netconfig_load_dns(netconfig, active_settings, "IPv4", AF_INET))
+		success = false;
 
-		l_acd_destroy(netconfig->acd);
-		netconfig->acd = NULL;
+	if (!netconfig_load_dns(netconfig, active_settings, "IPv6", AF_INET6))
+		success = false;
 
-		l_free(l_steal_ptr(netconfig->v4_gateway_str));
-
-		l_free(l_steal_ptr(netconfig->v4_domain));
+	if (l_settings_has_key(active_settings, "IPv6", "Enabled") &&
+			!l_settings_get_bool(active_settings, "IPv6", "Enabled",
+						&enable_ipv6)) {
+		l_error("netconfig: Can't load IPv6.Enabled");
+		success = false;
+		goto send_hostname;
 	}
-}
 
-static void netconfig_ipv4_acd_event(enum l_acd_event event, void *user_data)
-{
-	struct netconfig *netconfig = user_data;
-
-	switch (event) {
-	case L_ACD_EVENT_AVAILABLE:
-		L_WARN_ON(!(netconfig->addr4_add_cmd_id =
-				l_rtnl_ifaddr_add(rtnl, netconfig->ifindex,
-					netconfig->v4_address,
-					netconfig_ipv4_ifaddr_add_cmd_cb,
-					netconfig, NULL)));
-		return;
-	case L_ACD_EVENT_CONFLICT:
-		/*
-		 * Conflict found, no IP was actually set so just free/unset
-		 * anything we set prior to starting ACD.
-		 */
-		l_error("netconfig: statically configured address conflict!");
-		l_rtnl_address_free(netconfig->v4_address);
-		netconfig->v4_address = NULL;
-		netconfig->rtm_protocol = 0;
-		break;
-	case L_ACD_EVENT_LOST:
-		/*
-		 * Set IP but lost it some time after. Full (IPv4) reset in this
-		 * case.
-		 */
-		l_error("netconfig: statically configured address was lost");
-		netconfig_remove_v4_address(netconfig);
-		break;
+	if (!l_netconfig_set_family_enabled(netconfig->nc, AF_INET,
+						enable_ipv4) ||
+			!l_netconfig_set_family_enabled(netconfig->nc, AF_INET6,
+							enable_ipv6)) {
+		l_error("netconfig: l_netconfig_set_family_enabled() failed");
+		success = false;
 	}
-}
-
-static bool netconfig_ipv4_select_and_install(struct netconfig *netconfig)
-{
-	struct netdev *netdev = netdev_find(netconfig->ifindex);
-	bool set_address = (netconfig->rtm_protocol == RTPROT_STATIC);
-
-	if (netconfig_use_fils_addr(netconfig, AF_INET)) {
-		L_AUTO_FREE_VAR(char *, addr_str) = netconfig_ipv4_to_string(
-					netconfig->fils_override->ipv4_addr);
-		uint8_t prefix_len = netconfig->fils_override->ipv4_prefix_len;
 
-		if (unlikely(!addr_str))
-			return false;
-
-		netconfig->v4_address = l_rtnl_address_new(addr_str,
-								prefix_len);
-		if (L_WARN_ON(!netconfig->v4_address))
-			return false;
-
-		l_rtnl_address_set_noprefixroute(netconfig->v4_address, true);
-		set_address = true;
-
-		/*
-		 * TODO: If netconfig->fils_override->ipv4_lifetime is set,
-		 * start a timeout to renew the address using FILS IP Address
-		 * Assignment or perhaps just start the DHCP client at that
-		 * time.
-		 */
+send_hostname:
+	if (l_settings_has_key(active_settings, "IPv4", "SendHostname") &&
+			!l_settings_get_bool(active_settings, "IPv4",
+						"SendHostname",
+						&send_hostname)) {
+		l_error("netconfig: Can't load [IPv4].SendHostname");
+		success = false;
+		goto mdns;
 	}
 
-	if (set_address) {
-		char ip[INET6_ADDRSTRLEN];
+	if (send_hostname && gethostname(hostname, sizeof(hostname)) != 0) {
+		/* Warning only */
+		l_warn("netconfig: Unable to get hostname. "
+			"Error %d: %s", errno, strerror(errno));
+		goto mdns;
+	}
 
-		if (L_WARN_ON(!netconfig->v4_address ||
-					!l_rtnl_address_get_address(
-							netconfig->v4_address,
-							ip)))
-			return false;
+	if (send_hostname &&
+			!l_netconfig_set_hostname(netconfig->nc, hostname)) {
+		l_error("netconfig: l_netconfig_set_hostname() failed");
+		success = false;
+		goto mdns;
+	}
 
-		netconfig_dns_list_update(netconfig, AF_INET);
-		netconfig_domains_update(netconfig, AF_INET);
+mdns:
+	/* If the networks has this set take that over the global */
+	if (l_settings_has_key(active_settings, "Network", "MulticastDNS")) {
+		mdns = l_settings_get_string(active_settings, "Network",
+							"MulticastDNS");
+		if (!mdns) {
+			l_error("netconfig: Can't load Network.MulticastDNS");
+			success = false;
+		}
 
-		netconfig->acd = l_acd_new(netconfig->ifindex);
-		l_acd_set_event_handler(netconfig->acd,
-					netconfig_ipv4_acd_event, netconfig,
-					NULL);
-		if (getenv("IWD_ACD_DEBUG"))
-			l_acd_set_debug(netconfig->acd, do_debug,
-					"[ACD] ", NULL);
-
-		if (!l_acd_start(netconfig->acd, ip)) {
-			l_error("failed to start ACD, continuing anyways");
-			l_acd_destroy(netconfig->acd);
-			netconfig->acd = NULL;
-
-			L_WARN_ON(!(netconfig->addr4_add_cmd_id =
-				l_rtnl_ifaddr_add(rtnl, netconfig->ifindex,
-					netconfig->v4_address,
-					netconfig_ipv4_ifaddr_add_cmd_cb,
-					netconfig, NULL)));
+		if (mdns && !L_IN_STRSET(mdns, "true", "false", "resolve")) {
+			l_error("netconfig: Bad profile Network.MulticastDNS "
+				"value '%s'", mdns);
+			success = false;
 		}
 
-		return true;
+		if (!success)
+			goto route_priority;
 	}
 
-	l_dhcp_client_set_address(netconfig->dhcp_client, ARPHRD_ETHER,
-					netdev_get_address(netdev), ETH_ALEN);
+	if (!mdns && mdns_global) {
+		mdns = l_strdup(mdns_global);
 
-	if (l_dhcp_client_start(netconfig->dhcp_client))
-		return true;
-
-	l_error("netconfig: Failed to start DHCPv4 client for interface %u",
-							netconfig->ifindex);
-	return false;
-}
-
-static bool netconfig_ipv6_select_and_install(struct netconfig *netconfig)
-{
-	struct netdev *netdev = netdev_find(netconfig->ifindex);
-
-	if (netconfig->rtm_v6_protocol == RTPROT_UNSPEC) {
-		l_debug("IPV6 configuration disabled");
-		return true;
+		if (!L_IN_STRSET(mdns, "true", "false", "resolve")) {
+			l_error("netconfig: Bad global Network.MulticastDNS "
+				"value '%s'", mdns);
+			success = false;
+		}
 	}
 
-	sysfs_write_ipv6_setting(netdev_get_name(netdev), "disable_ipv6", "0");
-
-	if (netconfig_use_fils_addr(netconfig, AF_INET6)) {
-		uint8_t prefix_len = netconfig->fils_override->ipv6_prefix_len;
-		L_AUTO_FREE_VAR(char *, addr_str) = netconfig_ipv6_to_string(
-					netconfig->fils_override->ipv6_addr);
-
-		if (unlikely(!addr_str))
-			return false;
+route_priority:
+	l_netconfig_set_route_priority(netconfig->nc, ROUTE_PRIORITY_OFFSET);
+	l_netconfig_set_optimistic_dad_enabled(netconfig->nc, true);
 
-		netconfig->v6_address = l_rtnl_address_new(addr_str,
-								prefix_len);
-		if (L_WARN_ON(unlikely(!netconfig->v6_address)))
-			return false;
-
-		l_rtnl_address_set_noprefixroute(netconfig->v6_address, true);
-
-		/*
-		 * TODO: If netconfig->fils_override->ipv6_lifetime is set,
-		 * start a timeout to renew the address using FILS IP Address
-		 * Assignment or perhaps just start the DHCP client at that
-		 * time.
-		 */
+	if (!l_netconfig_check_config(netconfig->nc)) {
+		l_error("netconfig: Invalid configuration");
+		success = false;
 	}
 
-	if (netconfig->v6_address) {
-		netconfig_dns_list_update(netconfig, AF_INET6);
-
-		L_WARN_ON(!(netconfig->addr6_add_cmd_id =
-			l_rtnl_ifaddr_add(rtnl, netconfig->ifindex,
-					netconfig->v6_address,
-					netconfig_ipv6_ifaddr_add_cmd_cb,
-					netconfig, NULL)));
+	if (success) {
+		netconfig->active_settings = active_settings;
+		netconfig->static_config[INDEX_FOR_AF(AF_INET)] = static_ipv4;
+		netconfig->static_config[INDEX_FOR_AF(AF_INET6)] = static_ipv6;
+		netconfig->enabled[INDEX_FOR_AF(AF_INET)] = enable_ipv4;
+		netconfig->enabled[INDEX_FOR_AF(AF_INET6)] = enable_ipv6;
+		netconfig->mdns = l_steal_ptr(mdns);
 		return true;
 	}
 
-	/* DHCPv6 or RA, update MAC */
-	l_dhcp6_client_set_address(netconfig->dhcp6_client, ARPHRD_ETHER,
-					netdev_get_address(netdev), ETH_ALEN);
-
-	return true;
+	l_netconfig_reset_config(netconfig->nc);
+	return false;
 }
 
-static int validate_dns_list(int family, char **dns_list)
+static bool netconfig_load_fils_settings(struct netconfig *netconfig,
+						uint8_t af)
 {
-	unsigned int n_valid = 0;
-	struct in_addr in_addr;
-	struct in6_addr in6_addr;
-	char **p;
-
-	for (p = dns_list; *p; p++) {
-		int r;
-
-		if (family == AF_INET)
-			r = inet_pton(AF_INET, *p, &in_addr);
-		else if (family == AF_INET6)
-			r = inet_pton(AF_INET6, *p, &in6_addr);
-		else
-			r = -EAFNOSUPPORT;
-
-		if (r > 0) {
-			n_valid += 1;
-			continue;
-		}
-
-		l_error("netconfig: Invalid DNS address '%s'.", *p);
-		return -EINVAL;
-	}
+	struct ie_fils_ip_addr_response_info *fils = netconfig->fils_override;
+	char addr_str[INET6_ADDRSTRLEN];
+	char gw_addr_str[INET6_ADDRSTRLEN];
+	char dns_addr_str[INET6_ADDRSTRLEN];
+	_auto_(l_rtnl_address_free) struct l_rtnl_address *rtnl_addr = NULL;
+	bool is_zero = false;
+	uint8_t prefix_len;
 
-	return n_valid;
-}
-
-bool netconfig_load_settings(struct netconfig *netconfig,
-				const struct l_settings *active_settings)
-{
-	_auto_(l_free) char *mdns = NULL;
-	bool send_hostname;
-	bool v6_enabled;
-	char hostname[HOST_NAME_MAX + 1];
-	_auto_(l_strv_free) char **dns4_overrides = NULL;
-	_auto_(l_strv_free) char **dns6_overrides = NULL;
-	_auto_(l_rtnl_address_free) struct l_rtnl_address *v4_address = NULL;
-	_auto_(l_rtnl_address_free) struct l_rtnl_address *v6_address = NULL;
-
-	dns4_overrides = l_settings_get_string_list(active_settings,
-							"IPv4", "DNS", ' ');
-	if (dns4_overrides) {
-		int r = validate_dns_list(AF_INET, dns4_overrides);
-
-		if (unlikely(r <= 0)) {
-			l_strfreev(dns4_overrides);
-			dns4_overrides = NULL;
+	if (!netconfig_addr_to_str(af, &fils->ipv4_addr, &fils->ipv6_addr,
+					addr_str, &is_zero) || is_zero)
+		return is_zero;
 
-			if (r < 0)
-				return false;
-		}
+	prefix_len = (af == AF_INET ? fils->ipv4_prefix_len :
+			fils->ipv6_prefix_len);
 
-		if (r == 0)
-			l_error("netconfig: Empty IPv4.DNS entry, skipping...");
-	}
-
-	dns6_overrides = l_settings_get_string_list(active_settings,
-							"IPv6", "DNS", ' ');
-
-	if (dns6_overrides) {
-		int r = validate_dns_list(AF_INET6, dns6_overrides);
-
-		if (unlikely(r <= 0)) {
-			l_strfreev(dns6_overrides);
-			dns6_overrides = NULL;
-
-			if (r < 0)
-				return false;
-		}
+	if (L_WARN_ON(!(rtnl_addr = l_rtnl_address_new(addr_str, prefix_len))))
+		return false;
 
-		if (r == 0)
-			l_error("netconfig: Empty IPv6.DNS entry, skipping...");
-	}
+	if (L_WARN_ON(!l_netconfig_set_static_addr(netconfig->nc, af,
+							rtnl_addr)))
+		return false;
 
-	if (!l_settings_get_bool(active_settings,
-					"IPv4", "SendHostname", &send_hostname))
-		send_hostname = false;
-
-	if (send_hostname) {
-		if (gethostname(hostname, sizeof(hostname)) != 0) {
-			l_warn("netconfig: Unable to get hostname. "
-					"Error %d: %s", errno, strerror(errno));
-			send_hostname = false;
-		}
-	}
+	if (af == AF_INET &&
+			L_WARN_ON(!l_netconfig_set_acd_enabled(netconfig->nc,
+								false)))
+		return false;
 
-	mdns = l_settings_get_string(active_settings,
-					"Network", "MulticastDNS");
+	/*
+	 * Done with local address, move on to gateway and DNS.
+	 *
+	 * Since load_settings is called early, generally before the actual
+	 * connection setup starts, and load_fils_settings is called after
+	 * 802.11 Authentication & Association, we need to check if either
+	 * the gateway or DNS settings were overridden in load_settings so
+	 * as not to overwrite the user-provided values.  Values received
+	 * with FILS are expected to have the same weight as those from
+	 * DHCP/SLAAC.
+	 *
+	 * TODO: If netconfig->fils_override->ipv{4,6}_lifetime is set,
+	 * start a timeout to renew the address using FILS IP Address
+	 * Assignment or perhaps just start the DHCP client after that
+	 * time.
+	 *
+	 * TODO: validate gateway and/or DNS on local subnet, link-local,
+	 * etc.?
+	 */
 
-	if (l_settings_has_key(active_settings, "IPv4", "Address")) {
-		v4_address = netconfig_get_static4_address(active_settings);
+	if (!netconfig_addr_to_str(af, &fils->ipv4_gateway, &fils->ipv6_gateway,
+					gw_addr_str, &is_zero))
+		return false;
 
-		if (unlikely(!v4_address)) {
-			l_error("netconfig: Can't parse IPv4 address");
-			return false;
-		}
-	}
+	if (!netconfig->gateway_overridden[INDEX_FOR_AF(af)] && !is_zero &&
+			L_WARN_ON(!l_netconfig_set_gateway_override(
+								netconfig->nc,
+								af,
+								gw_addr_str)))
+		return false;
 
-	if (!l_settings_get_bool(active_settings, "IPv6",
-					"Enabled", &v6_enabled))
-		v6_enabled = ipv6_enabled;
+	if (!netconfig_addr_to_str(af, &fils->ipv4_dns, &fils->ipv6_dns,
+					dns_addr_str, &is_zero))
+		return is_zero;
 
-	if (l_settings_has_key(active_settings, "IPv6", "Address")) {
-		v6_address = netconfig_get_static6_address(active_settings);
+	if (!netconfig->dns_overridden[INDEX_FOR_AF(af)] && !is_zero) {
+		char *dns_list[2] = { dns_addr_str, NULL };
 
-		if (unlikely(!v6_address)) {
-			l_error("netconfig: Can't parse IPv6 address");
+		if (L_WARN_ON(!l_netconfig_set_dns_override(netconfig->nc,
+								af, dns_list)))
 			return false;
-		}
 	}
 
-	/* No more validation steps for now, commit new values */
-	netconfig->rtm_protocol = v4_address ? RTPROT_STATIC : RTPROT_DHCP;
-
-	if (!v6_enabled)
-		netconfig->rtm_v6_protocol = RTPROT_UNSPEC;
-	else if (v6_address)
-		netconfig->rtm_v6_protocol = RTPROT_STATIC;
-	else
-		netconfig->rtm_v6_protocol = RTPROT_DHCP;
-
-	if (send_hostname)
-		l_dhcp_client_set_hostname(netconfig->dhcp_client, hostname);
-
-	netconfig_free_settings(netconfig);
-
-	if (netconfig->rtm_protocol == RTPROT_STATIC)
-		netconfig->v4_address = l_steal_ptr(v4_address);
-
-	if (netconfig->rtm_v6_protocol == RTPROT_STATIC)
-		netconfig->v6_address = l_steal_ptr(v6_address);
-
-	netconfig->active_settings = active_settings;
-	netconfig->dns4_overrides = l_steal_ptr(dns4_overrides);
-	netconfig->dns6_overrides = l_steal_ptr(dns6_overrides);
-	netconfig->mdns = l_steal_ptr(mdns);
 	return true;
 }
 
 bool netconfig_configure(struct netconfig *netconfig,
 				netconfig_notify_func_t notify, void *user_data)
 {
+	if (netconfig->started)
+		return false;
+
 	netconfig->notify = notify;
 	netconfig->user_data = user_data;
 
-	if (unlikely(!netconfig_ipv4_select_and_install(netconfig)))
+	if (netconfig_use_fils_addr(netconfig, AF_INET) &&
+			!netconfig_load_fils_settings(netconfig, AF_INET))
 		return false;
 
-	if (unlikely(!netconfig_ipv6_select_and_install(netconfig)))
+	if (netconfig_use_fils_addr(netconfig, AF_INET6) &&
+			!netconfig_load_fils_settings(netconfig, AF_INET6))
 		return false;
 
-	resolve_set_mdns(netconfig->resolve, netconfig->mdns);
+	if (unlikely(!l_netconfig_start(netconfig->nc)))
+		return false;
 
+	netconfig->started = true;
 	return true;
 }
 
 bool netconfig_reconfigure(struct netconfig *netconfig, bool set_arp_gw)
 {
+	if (!netconfig->started)
+		return false;
+
 	/*
 	 * Starting with kernel 4.20, ARP cache is flushed when the netdev
 	 * detects NO CARRIER.  This can result in unnecessarily long delays
@@ -1509,14 +561,23 @@ bool netconfig_reconfigure(struct netcon
 	 * lost or delayed.  Try to force the gateway into the ARP cache
 	 * to alleviate this
 	 */
-	if (set_arp_gw)
-		netconfig_gateway_to_arp(netconfig);
+	if (set_arp_gw) {
+		netconfig_dhcp_gateway_to_arp(netconfig);
+
+		if (netconfig->connected[INDEX_FOR_AF(AF_INET)] &&
+				netconfig_use_fils_addr(netconfig, AF_INET))
+			netconfig_commit_fils_macs(netconfig, AF_INET);
 
-	if (netconfig->rtm_protocol == RTPROT_DHCP) {
+		if (netconfig->connected[INDEX_FOR_AF(AF_INET6)] &&
+				netconfig_use_fils_addr(netconfig, AF_INET6))
+			netconfig_commit_fils_macs(netconfig, AF_INET6);
+	}
+
+	if (!netconfig->static_config[INDEX_FOR_AF(AF_INET)]) {
 		/* TODO l_dhcp_client sending a DHCP inform request */
 	}
 
-	if (netconfig->rtm_v6_protocol == RTPROT_DHCP) {
+	if (!netconfig->static_config[INDEX_FOR_AF(AF_INET6)]) {
 		/* TODO l_dhcp_v6_client sending a DHCP inform request */
 	}
 
@@ -1525,64 +586,30 @@ bool netconfig_reconfigure(struct netcon
 
 bool netconfig_reset(struct netconfig *netconfig)
 {
-	struct netdev *netdev = netdev_find(netconfig->ifindex);
-
-	if (netconfig->route4_add_gateway_cmd_id) {
-		l_netlink_cancel(rtnl, netconfig->route4_add_gateway_cmd_id);
-		netconfig->route4_add_gateway_cmd_id = 0;
-	}
-
-	if (netconfig->route6_add_cmd_id) {
-		l_netlink_cancel(rtnl, netconfig->route6_add_cmd_id);
-		netconfig->route6_add_cmd_id = 0;
-	}
-
-	if (netconfig->addr4_add_cmd_id) {
-		l_netlink_cancel(rtnl, netconfig->addr4_add_cmd_id);
-		netconfig->addr4_add_cmd_id = 0;
-	}
-
-	if (netconfig->addr6_add_cmd_id) {
-		l_netlink_cancel(rtnl, netconfig->addr6_add_cmd_id);
-		netconfig->addr6_add_cmd_id = 0;
-	}
-
-	if (netconfig->rtm_protocol || netconfig->rtm_v6_protocol)
-		resolve_revert(netconfig->resolve);
-
-	netconfig_reset_v4(netconfig);
-
-	if (netconfig->rtm_v6_protocol) {
-		l_rtnl_address_free(netconfig->v6_address);
-		netconfig->v6_address = NULL;
-
-		l_strv_free(l_steal_ptr(netconfig->dns6_overrides));
-		l_strv_free(l_steal_ptr(netconfig->dns6_list));
-
-		l_dhcp6_client_stop(netconfig->dhcp6_client);
-		netconfig->rtm_v6_protocol = 0;
-
-		sysfs_write_ipv6_setting(netdev_get_name(netdev),
-						"disable_ipv6", "1");
-
-		l_free(l_steal_ptr(netconfig->v6_gateway_str));
+	if (!netconfig->started)
+		return false;
 
-		l_strv_free(l_steal_ptr(netconfig->v6_domains));
-	}
+	netconfig->started = false;
+	l_netconfig_unconfigure(netconfig->nc);
+	l_netconfig_stop(netconfig->nc);
 
-	l_free(l_steal_ptr(netconfig->fils_override));
+	netconfig->connected[0] = false;
+	netconfig->connected[1] = false;
 
+	netconfig_free_settings(netconfig);
 	return true;
 }
 
 char *netconfig_get_dhcp_server_ipv4(struct netconfig *netconfig)
 {
+	struct l_dhcp_client *client =
+		l_netconfig_get_dhcp_client(netconfig->nc);
 	const struct l_dhcp_lease *lease;
 
-	if (!netconfig->dhcp_client)
+	if (!client)
 		return NULL;
 
-	lease = l_dhcp_client_get_lease(netconfig->dhcp_client);
+	lease = l_dhcp_client_get_lease(client);
 	if (!lease)
 		return NULL;
 
@@ -1598,15 +625,16 @@ bool netconfig_get_fils_ip_req(struct ne
 	 * configuration (usually DHCP).  If we're configured with static
 	 * values return false to mean the IE should not be sent.
 	 */
-	if (netconfig->rtm_protocol != RTPROT_DHCP &&
-			netconfig->rtm_v6_protocol != RTPROT_DHCP)
+	if (netconfig->static_config[0] && netconfig->static_config[1])
 		return false;
 
 	memset(info, 0, sizeof(*info));
-	info->ipv4 = (netconfig->rtm_protocol == RTPROT_DHCP);
-	info->ipv6 = (netconfig->rtm_v6_protocol == RTPROT_DHCP);
-	info->dns = (info->ipv4 && !netconfig->dns4_overrides) ||
-		(info->ipv6 && !netconfig->dns6_overrides);
+	info->ipv4 = !netconfig->static_config[INDEX_FOR_AF(AF_INET)];
+	info->ipv6 = !netconfig->static_config[INDEX_FOR_AF(AF_INET6)];
+	info->dns = (info->ipv4 &&
+			!netconfig->dns_overridden[INDEX_FOR_AF(AF_INET)]) ||
+		(info->ipv6 &&
+			!netconfig->dns_overridden[INDEX_FOR_AF(AF_INET)]);
 
 	return true;
 }
@@ -1618,31 +646,65 @@ void netconfig_handle_fils_ip_resp(struc
 	netconfig->fils_override = l_memdup(info, sizeof(*info));
 }
 
+static void netconfig_event_handler(struct l_netconfig *nc, uint8_t family,
+					enum l_netconfig_event event,
+					void *user_data)
+{
+	struct netconfig *netconfig = user_data;
+
+	/* Once stopped, only commit a final L_NETCONFIG_EVENT_UNCONFIGURE */
+	if (!netconfig->started && event != L_NETCONFIG_EVENT_UNCONFIGURE)
+		return;
+
+	l_debug("l_netconfig event %d", event);
+
+	netconfig_commit(netconfig, family, event);
+
+	switch (event) {
+	case L_NETCONFIG_EVENT_CONFIGURE:
+	case L_NETCONFIG_EVENT_UPDATE:
+		break;
+
+	case L_NETCONFIG_EVENT_UNCONFIGURE:
+		break;
+
+	case L_NETCONFIG_EVENT_FAILED:
+		netconfig->connected[INDEX_FOR_AF(family)] = false;
+
+		/*
+		 * l_netconfig might have emitted an UNCONFIGURE before this
+		 * but now it tells us it's given up on (re)establishing the
+		 * IP setup.
+		 */
+		if (family == AF_INET && netconfig->notify)
+			netconfig->notify(NETCONFIG_EVENT_FAILED,
+						netconfig->user_data);
+
+		break;
+
+	default:
+		l_error("netconfig: Received unsupported l_netconfig event: %d",
+			event);
+	}
+}
+
 struct netconfig *netconfig_new(uint32_t ifindex)
 {
 	struct netdev *netdev = netdev_find(ifindex);
 	struct netconfig *netconfig;
-	struct l_icmp6_client *icmp6;
 	const char *debug_level = NULL;
 	int dhcp_priority = L_LOG_INFO;
+	struct l_dhcp6_client *dhcp6;
+	struct l_icmp6_client *icmp6;
 
-	if (!netconfig_list)
-		return NULL;
-
-	l_debug("Starting netconfig for interface: %d", ifindex);
-
-	netconfig = netconfig_find(ifindex);
-	if (netconfig)
-		return netconfig;
+	l_debug("Creating netconfig for interface: %d", ifindex);
 
 	netconfig = l_new(struct netconfig, 1);
-	netconfig->ifindex = ifindex;
+	netconfig->nc = l_netconfig_new(ifindex);
+	netconfig->netdev = netdev;
 	netconfig->resolve = resolve_new(ifindex);
 
-	netconfig->dhcp_client = l_dhcp_client_new(ifindex);
-	l_dhcp_client_set_event_handler(netconfig->dhcp_client,
-					netconfig_ipv4_dhcp_event_handler,
-					netconfig, NULL);
+	netconfig_commit_init(netconfig);
 
 	debug_level = getenv("IWD_DHCP_DEBUG");
 	if (debug_level != NULL) {
@@ -1658,43 +720,32 @@ struct netconfig *netconfig_new(uint32_t
 			dhcp_priority = L_LOG_DEBUG;
 	}
 
-	l_dhcp_client_set_debug(netconfig->dhcp_client, do_debug,
-					"[DHCPv4] ", NULL, dhcp_priority);
+	l_netconfig_set_event_handler(netconfig->nc, netconfig_event_handler,
+					netconfig, NULL);
+
+	l_dhcp_client_set_debug(l_netconfig_get_dhcp_client(netconfig->nc),
+				do_debug, "[DHCPv4] ", NULL, dhcp_priority);
 
-	netconfig->dhcp6_client = l_dhcp6_client_new(ifindex);
-	l_dhcp6_client_set_event_handler(netconfig->dhcp6_client,
-						netconfig_dhcp6_event_handler,
-						netconfig, NULL);
-	l_dhcp6_client_set_lla_randomized(netconfig->dhcp6_client, true);
-	l_dhcp6_client_set_nodelay(netconfig->dhcp6_client, true);
-	l_dhcp6_client_set_rtnl(netconfig->dhcp6_client, rtnl);
-
-	if (getenv("IWD_DHCP_DEBUG"))
-		l_dhcp6_client_set_debug(netconfig->dhcp6_client, do_debug,
-							"[DHCPv6] ", NULL);
-
-	icmp6 = l_dhcp6_client_get_icmp6(netconfig->dhcp6_client);
-	l_icmp6_client_set_rtnl(icmp6, rtnl);
-	l_icmp6_client_set_route_priority(icmp6, ROUTE_PRIORITY_OFFSET);
+	dhcp6 = l_netconfig_get_dhcp6_client(netconfig->nc);
+	l_dhcp6_client_set_lla_randomized(dhcp6, true);
 
-	l_queue_push_tail(netconfig_list, netconfig);
+	icmp6 = l_netconfig_get_icmp6_client(netconfig->nc);
+	l_icmp6_client_set_nodelay(icmp6, true);
 
-	sysfs_write_ipv6_setting(netdev_get_name(netdev), "accept_ra", "0");
-	sysfs_write_ipv6_setting(netdev_get_name(netdev), "disable_ipv6", "1");
+	if (debug_level) {
+		l_dhcp6_client_set_debug(dhcp6, do_debug, "[DHCPv6] ", NULL);
+		l_icmp6_client_set_debug(icmp6, do_debug, "[ICMPv6] ", NULL);
+	}
 
 	return netconfig;
 }
 
 void netconfig_destroy(struct netconfig *netconfig)
 {
-	if (!netconfig_list)
-		return;
-
 	l_debug("");
 
-	l_queue_remove(netconfig_list, netconfig);
-
 	netconfig_reset(netconfig);
+	netconfig_commit_free(netconfig, "aborted");
 	resolve_free(netconfig->resolve);
 	netconfig_free(netconfig);
 }
@@ -1710,43 +761,6 @@ bool netconfig_enabled(void)
 
 static int netconfig_init(void)
 {
-	uint32_t r;
-
-	if (netconfig_list)
-		return -EALREADY;
-
-	rtnl = iwd_get_rtnl();
-
-	r = l_netlink_register(rtnl, RTNLGRP_IPV4_IFADDR,
-					netconfig_ifaddr_notify, NULL, NULL);
-	if (!r) {
-		l_error("netconfig: Failed to register for RTNL link address"
-							" notifications.");
-		goto error;
-	}
-
-	r = l_rtnl_ifaddr4_dump(rtnl, netconfig_ifaddr_cmd_cb, NULL, NULL);
-	if (!r) {
-		l_error("netconfig: Failed to get addresses from RTNL link.");
-		goto error;
-	}
-
-	r = l_netlink_register(rtnl, RTNLGRP_IPV6_IFADDR,
-				netconfig_ifaddr_ipv6_notify, NULL, NULL);
-	if (!r) {
-		l_error("netconfig: Failed to register for RTNL link IPv6 "
-					"address notifications.");
-		goto error;
-	}
-
-	r = l_rtnl_ifaddr6_dump(rtnl, netconfig_ifaddr_ipv6_cmd_cb, NULL,
-									NULL);
-	if (!r) {
-		l_error("netconfig: Failed to get IPv6 addresses from RTNL"
-								" link.");
-		goto error;
-	}
-
 	if (!l_settings_get_uint(iwd_get_config(), "Network",
 							"RoutePriorityOffset",
 							&ROUTE_PRIORITY_OFFSET))
@@ -1755,26 +769,17 @@ static int netconfig_init(void)
 	if (!l_settings_get_bool(iwd_get_config(), "Network",
 					"EnableIPv6",
 					&ipv6_enabled))
-		ipv6_enabled = false;
+		ipv6_enabled = true;
 
-	netconfig_list = l_queue_new();
+	mdns_global = l_settings_get_string(iwd_get_config(), "Network",
+						"MulticastDNS");
 
 	return 0;
-
-error:
-	rtnl = NULL;
-
-	return r;
 }
 
 static void netconfig_exit(void)
 {
-	if (!netconfig_list)
-		return;
-
-	rtnl = NULL;
-
-	l_queue_destroy(netconfig_list, netconfig_free);
+	l_free(mdns_global);
 }
 
 IWD_MODULE(netconfig, netconfig_init, netconfig_exit)
diff -pruN 1.30-1/src/netconfig-commit.c 2.3-1/src/netconfig-commit.c
--- 1.30-1/src/netconfig-commit.c	1970-01-01 00:00:00.000000000 +0000
+++ 2.3-1/src/netconfig-commit.c	2022-11-18 12:31:49.000000000 +0000
@@ -0,0 +1,711 @@
+/*
+ *
+ *  Wireless daemon for Linux
+ *
+ *  Copyright (C) 2022  Intel Corporation. All rights reserved.
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.1 of the License, or (at your option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <arpa/inet.h>
+#include <netinet/if_ether.h>
+#include <linux/icmpv6.h>
+
+#include <ell/ell.h>
+
+#include "ell/useful.h"
+#include "src/iwd.h"
+#include "src/common.h"
+#include "src/util.h"
+#include "src/netdev.h"
+#include "src/ie.h"
+#include "src/resolve.h"
+#include "src/dbus.h"
+#include "src/netconfig.h"
+
+struct netconfig_commit_ops {
+	bool (*init_data)(struct netconfig *netconfig);
+	void (*free_data)(struct netconfig *netconfig, const char *reasonstr);
+	void (*commit)(struct netconfig *netconfig, uint8_t family,
+			enum l_netconfig_event event);
+};
+
+static struct l_netlink *rtnl;
+
+static void netconfig_rtnl_commit(struct netconfig *netconfig, uint8_t family,
+					enum l_netconfig_event event);
+static void netconfig_rtnl_free_data(struct netconfig *netconfig,
+					const char *reasonstr);
+
+/* Default backend */
+static struct netconfig_commit_ops netconfig_rtnl_ops = {
+	.commit    = netconfig_rtnl_commit,
+	.free_data = netconfig_rtnl_free_data,
+};
+
+/* Same backend for all netconfig objects */
+static const struct netconfig_commit_ops *commit_ops = &netconfig_rtnl_ops;
+
+static struct l_queue *netconfig_list;
+
+void netconfig_commit_init(struct netconfig *netconfig)
+{
+	if (!rtnl)
+		rtnl = l_rtnl_get();
+
+	if (!netconfig_list)
+		netconfig_list = l_queue_new();
+
+	l_queue_push_tail(netconfig_list, netconfig);
+
+	L_WARN_ON(netconfig->commit_data);
+
+	if (commit_ops->init_data)
+		commit_ops->init_data(netconfig);
+}
+
+void netconfig_commit_free(struct netconfig *netconfig, const char *reasonstr)
+{
+	if (commit_ops->free_data)
+		commit_ops->free_data(netconfig, reasonstr);
+
+	L_WARN_ON(!l_queue_remove(netconfig_list, netconfig));
+
+	if (l_queue_isempty(netconfig_list))
+		l_queue_destroy(l_steal_ptr(netconfig_list), NULL);
+}
+
+static void netconfig_commit_print_addrs(const char *verb,
+					const struct l_queue_entry *addrs)
+{
+	for (; addrs; addrs = addrs->next) {
+		const struct l_rtnl_address *addr = addrs->data;
+		char str[INET6_ADDRSTRLEN];
+
+		if (l_rtnl_address_get_address(addr, str))
+			l_debug("%s address: %s", verb, str);
+	}
+}
+
+void netconfig_commit(struct netconfig *netconfig, uint8_t family,
+			enum l_netconfig_event event)
+{
+	const struct l_queue_entry *added;
+	const struct l_queue_entry *removed;
+	const struct l_queue_entry *expired;
+
+	l_netconfig_get_addresses(netconfig->nc, &added, NULL,
+						&removed, &expired);
+
+	/* Only print IP additions and removals to avoid cluttering the log */
+	netconfig_commit_print_addrs("installing", added);
+	netconfig_commit_print_addrs("removing", removed);
+	netconfig_commit_print_addrs("expired", expired);
+
+	commit_ops->commit(netconfig, family, event);
+
+	if (event == L_NETCONFIG_EVENT_CONFIGURE) {
+		/*
+		 * Done here instead of in ops->commit because the MACs are
+		 * not considered part of the network configuration
+		 * (particularly Network Manager's "level 3 config" or l3cfg)
+		 * so we can handle this ourselves independent of the backend.
+		 */
+		if (family == AF_INET &&
+				!netconfig->static_config[INDEX_FOR_AF(family)])
+			netconfig_dhcp_gateway_to_arp(netconfig);
+
+		if (!netconfig->connected[INDEX_FOR_AF(family)] &&
+				netconfig_use_fils_addr(netconfig, family))
+			netconfig_commit_fils_macs(netconfig, family);
+	}
+}
+
+static void netconfig_switch_backend(const struct netconfig_commit_ops *new_ops)
+{
+	const struct l_queue_entry *entry;
+
+	for (entry = l_queue_get_entries(netconfig_list); entry;
+			entry = entry->next) {
+		struct netconfig *netconfig = entry->data;
+
+		if (commit_ops->free_data)
+			commit_ops->free_data(netconfig, "");
+
+		if (new_ops->init_data)
+			new_ops->init_data(netconfig);
+	}
+
+	commit_ops = new_ops;
+}
+
+/*
+ * Called by all backends when netconfig_commit finishes, synchronously or
+ * asynchronously.
+ */
+static void netconfig_commit_done(struct netconfig *netconfig, uint8_t family,
+					enum l_netconfig_event event,
+					bool success)
+{
+	bool connected = netconfig->connected[INDEX_FOR_AF(family)];
+
+	if (!success) {
+		netconfig->connected[INDEX_FOR_AF(family)] = false;
+
+		if (netconfig->notify && family == AF_INET)
+			netconfig->notify(NETCONFIG_EVENT_FAILED,
+						netconfig->user_data);
+		return;
+	}
+
+	switch (event) {
+	case L_NETCONFIG_EVENT_CONFIGURE:
+	case L_NETCONFIG_EVENT_UPDATE:
+		netconfig->connected[INDEX_FOR_AF(family)] = true;
+
+		if (family == AF_INET && !connected && netconfig->notify)
+			netconfig->notify(NETCONFIG_EVENT_CONNECTED,
+						netconfig->user_data);
+
+		break;
+
+	case L_NETCONFIG_EVENT_UNCONFIGURE:
+	case L_NETCONFIG_EVENT_FAILED:
+		break;
+	}
+}
+
+static void netconfig_set_neighbor_entry_cb(int error,
+						uint16_t type, const void *data,
+						uint32_t len, void *user_data)
+{
+	if (error)
+		l_error("l_rtnl_neighbor_set_hwaddr failed: %s (%i)",
+			strerror(-error), error);
+}
+
+void netconfig_dhcp_gateway_to_arp(struct netconfig *netconfig)
+{
+	struct l_dhcp_client *dhcp = l_netconfig_get_dhcp_client(netconfig->nc);
+	const struct l_dhcp_lease *lease;
+	_auto_(l_free) char *server_id = NULL;
+	_auto_(l_free) char *gw = NULL;
+	const uint8_t *server_mac;
+	struct in_addr in_gw;
+	uint32_t ifindex = netdev_get_ifindex(netconfig->netdev);
+
+	/* Can only do this for DHCP in certain network setups */
+	if (netconfig->static_config[INDEX_FOR_AF(AF_INET)] ||
+			netconfig->fils_override)
+		return;
+
+	lease = l_dhcp_client_get_lease(dhcp);
+	if (!lease)
+		return;
+
+	server_id = l_dhcp_lease_get_server_id(lease);
+	gw = l_dhcp_lease_get_gateway(lease);
+	server_mac = l_dhcp_lease_get_server_mac(lease);
+
+	if (!gw || strcmp(server_id, gw) || !server_mac)
+		return;
+
+	l_debug("Gateway MAC is known, setting into ARP cache");
+	in_gw.s_addr = l_dhcp_lease_get_gateway_u32(lease);
+
+	if (!l_rtnl_neighbor_set_hwaddr(rtnl, ifindex, AF_INET,
+					&in_gw, server_mac, ETH_ALEN,
+					netconfig_set_neighbor_entry_cb, NULL,
+					NULL))
+		l_debug("l_rtnl_neighbor_set_hwaddr failed");
+}
+
+void netconfig_commit_fils_macs(struct netconfig *netconfig, uint8_t family)
+{
+	const struct ie_fils_ip_addr_response_info *fils =
+		netconfig->fils_override;
+	const void *addr;
+	const void *hwaddr;
+	size_t addr_len = (family == AF_INET ? 4 : 16);
+	uint32_t ifindex = netdev_get_ifindex(netconfig->netdev);
+
+	if (!fils)
+		return;
+
+	/*
+	 * Attempt to use the gateway/DNS MAC addressed received from the AP
+	 * by writing the mapping directly into the netdev's ARP table so as
+	 * to save one data frame roundtrip before first IP connections are
+	 * established.  This is very low-priority but print error messages
+	 * just because they may indicate bigger problems.
+	 */
+
+	addr = (family == AF_INET ? (void *) &fils->ipv4_gateway :
+			(void *) &fils->ipv6_gateway);
+	hwaddr = (family == AF_INET ?
+			&fils->ipv4_gateway_mac : &fils->ipv6_gateway_mac);
+
+	if (!l_memeqzero(addr, addr_len) && !l_memeqzero(hwaddr, ETH_ALEN) &&
+			unlikely(!l_rtnl_neighbor_set_hwaddr(rtnl, ifindex,
+						family, addr, hwaddr, ETH_ALEN,
+						netconfig_set_neighbor_entry_cb,
+						NULL, NULL)))
+		l_debug("l_rtnl_neighbor_set_hwaddr(%s, gateway) failed",
+			family == AF_INET ? "AF_INET" : "AF_INET6");
+
+	addr = (family == AF_INET ? (void *) &fils->ipv4_dns :
+			(void *) &fils->ipv6_dns);
+	hwaddr = (family == AF_INET ?
+			&fils->ipv4_dns_mac : &fils->ipv6_dns_mac);
+
+	if (!l_memeqzero(addr, addr_len) && !l_memeqzero(hwaddr, ETH_ALEN) &&
+			unlikely(!l_rtnl_neighbor_set_hwaddr(rtnl, ifindex,
+						family, addr, hwaddr, ETH_ALEN,
+						netconfig_set_neighbor_entry_cb,
+						NULL, NULL)))
+		l_debug("l_rtnl_neighbor_set_hwaddr(%s, DNS) failed",
+			family == AF_INET ? "AF_INET" : "AF_INET6");
+}
+
+static void netconfig_dns_list_update(struct netconfig *netconfig)
+{
+	_auto_(l_strv_free) char **dns_list =
+		l_netconfig_get_dns_list(netconfig->nc);
+
+	if (l_strv_eq(netconfig->dns_list, dns_list))
+		return;
+
+	if (netconfig->resolve && dns_list)
+		resolve_set_dns(netconfig->resolve, dns_list);
+
+	l_strv_free(netconfig->dns_list);
+	netconfig->dns_list = l_steal_ptr(dns_list);
+}
+
+static void netconfig_domains_update(struct netconfig *netconfig)
+{
+	_auto_(l_strv_free) char **domains =
+		l_netconfig_get_domain_names(netconfig->nc);
+
+	if (l_strv_eq(netconfig->domains, domains))
+		return;
+
+	if (netconfig->resolve && domains)
+		resolve_set_domains(netconfig->resolve, domains);
+
+	l_strv_free(netconfig->domains);
+	netconfig->dns_list = l_steal_ptr(domains);
+}
+
+static void netconfig_rtnl_commit(struct netconfig *netconfig, uint8_t family,
+					enum l_netconfig_event event)
+{
+	l_netconfig_apply_rtnl(netconfig->nc);
+
+	netconfig_dns_list_update(netconfig);
+	netconfig_domains_update(netconfig);
+
+	if (event == L_NETCONFIG_EVENT_CONFIGURE && family == AF_INET)
+		/*
+		 * netconfig->mdns is currently only loaded in
+		 * netconfig_load_settings() so we can set it once on
+		 * the CONFIGURE event.
+		 */
+		resolve_set_mdns(netconfig->resolve, netconfig->mdns);
+
+	if (event == L_NETCONFIG_EVENT_UNCONFIGURE && family == AF_INET) {
+		l_strv_free(l_steal_ptr(netconfig->dns_list));
+		l_strv_free(l_steal_ptr(netconfig->domains));
+		resolve_revert(netconfig->resolve);
+	}
+
+	netconfig_commit_done(netconfig, family, event, true);
+}
+
+static void netconfig_rtnl_free_data(struct netconfig *netconfig,
+					const char *reasonstr)
+{
+	l_strv_free(l_steal_ptr(netconfig->dns_list));
+	l_strv_free(l_steal_ptr(netconfig->domains));
+}
+
+
+struct netconfig_agent_data {
+	uint32_t pending_id[2];
+};
+
+struct netconfig_agent_call_data {
+	struct netconfig *netconfig;
+	uint8_t family;
+	enum l_netconfig_event event;
+};
+
+static char *netconfig_agent_name;
+static char *netconfig_agent_path;
+static unsigned int netconfig_agent_watch;
+
+static void netconfig_agent_cancel(struct netconfig *netconfig, uint8_t family,
+					const char *reasonstr)
+{
+	struct netconfig_agent_data *data = netconfig->commit_data;
+	struct l_dbus *dbus = dbus_get_bus();
+	const char *dev_path = netdev_get_path(netconfig->netdev);
+	struct l_dbus_message *message;
+	const char *method;
+
+	if (!data || !data->pending_id[INDEX_FOR_AF(family)])
+		return;
+
+	l_dbus_cancel(dbus, data->pending_id[INDEX_FOR_AF(family)]);
+	data->pending_id[INDEX_FOR_AF(family)] = 0;
+
+	method = (family == AF_INET ? "CancelIPv4" : "CancelIPv6");
+	l_debug("sending a %s(%s, %s) to %s %s", method, dev_path, reasonstr,
+		netconfig_agent_name, netconfig_agent_path);
+
+	message = l_dbus_message_new_method_call(dbus, netconfig_agent_name,
+						netconfig_agent_path,
+						IWD_NETCONFIG_AGENT_INTERFACE,
+						method);
+	l_dbus_message_set_arguments(message, "os", dev_path, reasonstr);
+	l_dbus_message_set_no_reply(message, true);
+	l_dbus_send(dbus, message);
+}
+
+static void netconfig_agent_receive_reply(struct l_dbus_message *reply,
+						void *user_data)
+{
+	struct netconfig_agent_call_data *cd = user_data;
+	struct netconfig_agent_data *data = cd->netconfig->commit_data;
+	const char *error, *text;
+	bool success = true;
+
+	l_debug("agent reply from %s", l_dbus_message_get_sender(reply));
+
+	data->pending_id[INDEX_FOR_AF(cd->family)] = 0;
+
+	if (!cd->netconfig->started)
+		return;
+
+	if (l_dbus_message_get_error(reply, &error, &text)) {
+		success = false;
+		l_error("netconfig agent call returned %s(\"%s\")",
+			error, text);
+	} else if (!l_dbus_message_get_arguments(reply, "")) {
+		success = false;
+		l_error("netconfig agent call reply signature wrong: %s",
+			l_dbus_message_get_signature(reply));
+	}
+
+	netconfig_commit_done(cd->netconfig, cd->family, cd->event, success);
+}
+
+#define IS_IPV6_STR_FAST(str)	(strchr(str, ':') != NULL)
+
+typedef void (*netconfig_build_entry_fn)(struct l_dbus_message_builder *builder,
+					const void *data, uint8_t family);
+
+static void netconfig_agent_append_dict_dict_array(
+					struct l_dbus_message_builder *builder,
+					const char *key,
+					const struct l_queue_entry *value,
+					netconfig_build_entry_fn build_entry,
+					uint8_t family)
+{
+	l_dbus_message_builder_enter_dict(builder, "sv");
+	l_dbus_message_builder_append_basic(builder, 's', key);
+	l_dbus_message_builder_enter_variant(builder, "aa{sv}");
+	l_dbus_message_builder_enter_array(builder, "a{sv}");
+
+	for (; value; value = value->next)
+		build_entry(builder, value->data, family);
+
+	l_dbus_message_builder_leave_array(builder);
+	l_dbus_message_builder_leave_variant(builder);
+	l_dbus_message_builder_leave_dict(builder);
+}
+
+static void netconfig_agent_append_dict_strv(
+					struct l_dbus_message_builder *builder,
+					const char *key, char **value,
+					uint8_t family)
+{
+	if (!value)
+		return;
+
+	l_dbus_message_builder_enter_dict(builder, "sv");
+	l_dbus_message_builder_append_basic(builder, 's', key);
+	l_dbus_message_builder_enter_variant(builder, "as");
+	l_dbus_message_builder_enter_array(builder, "s");
+
+	for (; *value; value++) {
+		uint8_t value_family = IS_IPV6_STR_FAST((char *) *value) ?
+			AF_INET6 : AF_INET;
+
+		if (family == AF_UNSPEC || value_family == family)
+			l_dbus_message_builder_append_basic(builder, 's',
+								*value);
+	}
+
+	l_dbus_message_builder_leave_array(builder);
+	l_dbus_message_builder_leave_variant(builder);
+	l_dbus_message_builder_leave_dict(builder);
+}
+
+static void netconfig_agent_append_address(
+					struct l_dbus_message_builder *builder,
+					const void *data, uint8_t family)
+{
+	const struct l_rtnl_address *addr = data;
+	char addr_str[INET6_ADDRSTRLEN];
+	uint64_t valid_expiry_time;
+	uint64_t preferred_expiry_time;
+	uint64_t now = l_time_now();
+
+	if (l_rtnl_address_get_family(addr) != family)
+		return;
+
+	l_dbus_message_builder_enter_array(builder, "{sv}");
+
+	l_rtnl_address_get_address(addr, addr_str);
+	dbus_append_dict_basic(builder, "Address", 's', addr_str);
+
+	if (family == AF_INET) {
+		uint8_t plen = l_rtnl_address_get_prefix_length(addr);
+		dbus_append_dict_basic(builder, "PrefixLength", 'y', &plen);
+
+		if (l_rtnl_address_get_broadcast(addr, addr_str) &&
+				strcmp(addr_str, "0.0.0.0"))
+			dbus_append_dict_basic(builder, "Broadcast", 's',
+						addr_str);
+	}
+
+	l_rtnl_address_get_expiry(addr, &preferred_expiry_time,
+					&valid_expiry_time);
+
+	if (valid_expiry_time > now) {
+		uint32_t lt = l_time_to_secs(valid_expiry_time - now);
+
+		dbus_append_dict_basic(builder, "ValidLifetime", 'u', &lt);
+	}
+
+	if (preferred_expiry_time > now) {
+		uint32_t lt = l_time_to_secs(preferred_expiry_time - now);
+
+		dbus_append_dict_basic(builder, "PreferredLifetime", 'u', &lt);
+	}
+
+	l_dbus_message_builder_leave_array(builder);
+}
+
+static void netconfig_agent_append_route(struct l_dbus_message_builder *builder,
+						const void *data,
+						uint8_t family)
+{
+	const struct l_rtnl_route *rt = data;
+	char addr_str[INET6_ADDRSTRLEN];
+	uint8_t prefix_len;
+	uint64_t expiry_time;
+	uint64_t now = l_time_now();
+	uint32_t priority;
+	uint8_t preference;
+	uint32_t mtu;
+
+	if (l_rtnl_route_get_family(rt) != family)
+		return;
+
+	l_dbus_message_builder_enter_array(builder, "{sv}");
+
+	if (l_rtnl_route_get_dst(rt, addr_str, &prefix_len) && prefix_len) {
+		l_dbus_message_builder_enter_dict(builder, "sv");
+		l_dbus_message_builder_append_basic(builder, 's',
+							"Destination");
+		l_dbus_message_builder_enter_variant(builder, "(sy)");
+		l_dbus_message_builder_enter_struct(builder, "sy");
+		l_dbus_message_builder_append_basic(builder, 's', addr_str);
+		l_dbus_message_builder_append_basic(builder, 'y', &prefix_len);
+		l_dbus_message_builder_leave_struct(builder);
+		l_dbus_message_builder_leave_variant(builder);
+		l_dbus_message_builder_leave_dict(builder);
+	}
+
+	if (l_rtnl_route_get_gateway(rt, addr_str))
+		dbus_append_dict_basic(builder, "Router", 's', addr_str);
+
+	if (l_rtnl_route_get_prefsrc(rt, addr_str))
+		dbus_append_dict_basic(builder, "PreferredSource", 's',
+					addr_str);
+
+	expiry_time = l_rtnl_route_get_expiry(rt);
+	if (expiry_time > now) {
+		uint32_t lt = l_time_to_secs(expiry_time - now);
+
+		dbus_append_dict_basic(builder, "Lifetime", 'u', &lt);
+	}
+
+	priority = l_rtnl_route_get_priority(rt);
+	dbus_append_dict_basic(builder, "Priority", 'u', &priority);
+
+	/*
+	 * ICMPV6_ROUTER_PREF_MEDIUM is returned by default even for IPv4
+	 * routes where this property doesn't make sense so filter those out.
+	 */
+	preference = l_rtnl_route_get_preference(rt);
+	if (preference != ICMPV6_ROUTER_PREF_INVALID && family == AF_INET6)
+		dbus_append_dict_basic(builder, "Preference", 'y', &preference);
+
+	mtu = l_rtnl_route_get_mtu(rt);
+	if (mtu)
+		dbus_append_dict_basic(builder, "Priority", 'u', &mtu);
+
+	l_dbus_message_builder_leave_array(builder);
+}
+
+static void netconfig_agent_commit(struct netconfig *netconfig, uint8_t family,
+					enum l_netconfig_event event)
+{
+	struct netconfig_agent_data *data;
+	struct netconfig_agent_call_data *cd;
+	struct l_dbus *dbus = dbus_get_bus();
+	struct l_dbus_message *message;
+	struct l_dbus_message_builder *builder;
+	const char *dev_path = netdev_get_path(netconfig->netdev);
+	const char *dbus_method =
+		(family == AF_INET ? "ConfigureIPv4" : "ConfigureIPv6");
+	const char *cfg_method =
+		netconfig->static_config[INDEX_FOR_AF(family)] ?
+		"static" : "auto";
+	_auto_(l_strv_free) char **dns_list = NULL;
+	_auto_(l_strv_free) char **domains = NULL;
+
+	if (!netconfig->commit_data)
+		netconfig->commit_data = l_new(struct netconfig_agent_data, 1);
+
+	netconfig_agent_cancel(netconfig, family, "superseded");
+
+	l_debug("sending a %s(%s, ...) to %s %s", dbus_method, dev_path,
+		netconfig_agent_name, netconfig_agent_path);
+
+	message = l_dbus_message_new_method_call(dbus, netconfig_agent_name,
+						netconfig_agent_path,
+						IWD_NETCONFIG_AGENT_INTERFACE,
+						dbus_method);
+
+	/*
+	 * Build the call arguments: the Device object path and
+	 * the complicated config dict.
+	 */
+	builder = l_dbus_message_builder_new(message);
+	l_dbus_message_builder_append_basic(builder, 'o', dev_path);
+	l_dbus_message_builder_enter_array(builder, "{sv}");
+	dbus_append_dict_basic(builder, "Method", 's', cfg_method);
+
+	netconfig_agent_append_dict_dict_array(builder, "Addresses",
+					l_netconfig_get_addresses(netconfig->nc,
+							NULL, NULL, NULL, NULL),
+					netconfig_agent_append_address, family);
+
+	netconfig_agent_append_dict_dict_array(builder, "Routes",
+					l_netconfig_get_routes(netconfig->nc,
+							NULL, NULL, NULL, NULL),
+					netconfig_agent_append_route, family);
+
+	dns_list = l_netconfig_get_dns_list(netconfig->nc);
+	netconfig_agent_append_dict_strv(builder, "DomainNameServers",
+						dns_list, family);
+
+	domains = l_netconfig_get_domain_names(netconfig->nc);
+	netconfig_agent_append_dict_strv(builder, "DomainNames",
+						domains, AF_UNSPEC);
+
+	l_dbus_message_builder_leave_array(builder);
+	l_dbus_message_builder_finalize(builder);
+	l_dbus_message_builder_destroy(builder);
+
+	cd = l_new(struct netconfig_agent_call_data, 1);
+	cd->netconfig = netconfig;
+	cd->family = family;
+	cd->event = event;
+	data = netconfig->commit_data;
+	data->pending_id[INDEX_FOR_AF(family)] =
+		l_dbus_send_with_reply(dbus, message,
+					netconfig_agent_receive_reply,
+					cd, l_free);
+}
+
+static void netconfig_agent_free_data(struct netconfig *netconfig,
+					const char *reasonstr)
+{
+	if (!netconfig->commit_data)
+		return;
+
+	netconfig_agent_cancel(netconfig, AF_INET, reasonstr);
+	netconfig_agent_cancel(netconfig, AF_INET6, reasonstr);
+	l_free(l_steal_ptr(netconfig->commit_data));
+}
+
+static struct netconfig_commit_ops netconfig_agent_ops = {
+	.commit    = netconfig_agent_commit,
+	.free_data = netconfig_agent_free_data,
+};
+
+static void netconfig_agent_disconnect_handle(void *user_data)
+{
+	netconfig_unregister_agent(netconfig_agent_name, netconfig_agent_path);
+}
+
+static void netconfig_agent_disconnect_cb(struct l_dbus *dbus, void *user_data)
+{
+	l_debug("");
+	l_idle_oneshot(netconfig_agent_disconnect_handle, NULL, NULL);
+}
+
+int netconfig_register_agent(const char *name, const char *path)
+{
+	if (netconfig_agent_path)
+		return -EEXIST;
+
+	netconfig_agent_name = l_strdup(name);
+	netconfig_agent_path = l_strdup(path);
+	netconfig_agent_watch = l_dbus_add_disconnect_watch(dbus_get_bus(),
+						name,
+						netconfig_agent_disconnect_cb,
+						NULL, NULL);
+
+	netconfig_switch_backend(&netconfig_agent_ops);
+
+	return 0;
+}
+
+int netconfig_unregister_agent(const char *name, const char *path)
+{
+	if (!netconfig_agent_path || strcmp(netconfig_agent_path, path))
+		return -ENOENT;
+
+	if (strcmp(netconfig_agent_name, name))
+		return -EPERM;
+
+	l_free(l_steal_ptr(netconfig_agent_name));
+	l_free(l_steal_ptr(netconfig_agent_path));
+	l_dbus_remove_watch(dbus_get_bus(), netconfig_agent_watch);
+
+	netconfig_switch_backend(&netconfig_rtnl_ops);
+	return 0;
+}
diff -pruN 1.30-1/src/netconfig.h 2.3-1/src/netconfig.h
--- 1.30-1/src/netconfig.h	2021-11-18 20:21:21.000000000 +0000
+++ 2.3-1/src/netconfig.h	2022-11-18 12:31:49.000000000 +0000
@@ -20,17 +20,47 @@
  *
  */
 
+struct netdev;
 struct netconfig;
 struct ie_fils_ip_addr_request_info;
 struct ie_fils_ip_addr_response_info;
 
 enum netconfig_event {
 	NETCONFIG_EVENT_CONNECTED,
+	NETCONFIG_EVENT_FAILED,
 };
 
 typedef void (*netconfig_notify_func_t)(enum netconfig_event event,
 							void *user_data);
 
+struct netconfig {
+	struct l_netconfig *nc;
+	struct netdev *netdev;
+
+	char *mdns;
+	struct ie_fils_ip_addr_response_info *fils_override;
+	bool enabled[2];
+	bool static_config[2];
+	bool gateway_overridden[2];
+	bool dns_overridden[2];
+	bool started;
+	bool connected[2];
+	char **dns_list;
+	char **domains;
+
+	const struct l_settings *active_settings;
+
+	netconfig_notify_func_t notify;
+	void *user_data;
+
+	struct resolve *resolve;
+
+	void *commit_data;
+};
+
+/* 0 for AF_INET, 1 for AF_INET6 */
+#define INDEX_FOR_AF(af)	((af) != AF_INET)
+
 bool netconfig_load_settings(struct netconfig *netconfig,
 				const struct l_settings *active_settings);
 bool netconfig_configure(struct netconfig *netconfig,
@@ -43,8 +73,19 @@ bool netconfig_get_fils_ip_req(struct ne
 				struct ie_fils_ip_addr_request_info *info);
 void netconfig_handle_fils_ip_resp(struct netconfig *netconfig,
 			const struct ie_fils_ip_addr_response_info *info);
+bool netconfig_use_fils_addr(struct netconfig *netconfig, int af);
 
 struct netconfig *netconfig_new(uint32_t ifindex);
 void netconfig_destroy(struct netconfig *netconfig);
 
 bool netconfig_enabled(void);
+
+void netconfig_commit_init(struct netconfig *netconfig);
+void netconfig_commit_free(struct netconfig *netconfig, const char *reasonstr);
+void netconfig_commit(struct netconfig *netconfig, uint8_t family,
+			enum l_netconfig_event event);
+int netconfig_register_agent(const char *name, const char *path);
+int netconfig_unregister_agent(const char *name, const char *path);
+
+void netconfig_dhcp_gateway_to_arp(struct netconfig *netconfig);
+void netconfig_commit_fils_macs(struct netconfig *netconfig, uint8_t family);
diff -pruN 1.30-1/src/netdev.c 2.3-1/src/netdev.c
--- 1.30-1/src/netdev.c	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/src/netdev.c	2023-01-23 18:46:38.000000000 +0000
@@ -96,13 +96,6 @@ struct netdev_handshake_state {
 	enum connection_type type;
 };
 
-struct netdev_ft_over_ds_info {
-	struct ft_ds_info super;
-	struct netdev *netdev;
-
-	bool parsed : 1;
-};
-
 struct netdev_ext_key_info {
 	uint16_t proto;
 	bool noencrypt;
@@ -145,7 +138,6 @@ struct netdev {
 	struct l_timeout *sa_query_delay;
 	struct l_timeout *group_handshake_timeout;
 	uint16_t sa_query_id;
-	uint8_t prev_snonce[32];
 	int8_t rssi_levels[16];
 	uint8_t rssi_levels_num;
 	uint8_t cur_rssi_level_idx;
@@ -176,8 +168,6 @@ struct netdev {
 	struct l_genl_msg *auth_cmd;
 	struct wiphy_radio_work_item work;
 
-	struct l_queue *ft_ds_list;
-
 	struct netdev_ext_key_info *ext_key_info;
 
 	bool connected : 1;
@@ -752,13 +742,6 @@ static void netdev_preauth_destroy(void
 	l_free(state);
 }
 
-static void netdev_ft_ds_entry_free(void *data)
-{
-	struct netdev_ft_over_ds_info *info = data;
-
-	ft_ds_info_free(&info->super);
-}
-
 static void netdev_connect_free(struct netdev *netdev)
 {
 	if (netdev->work.id)
@@ -847,11 +830,6 @@ static void netdev_connect_free(struct n
 		l_genl_family_cancel(nl80211, netdev->get_oci_cmd_id);
 		netdev->get_oci_cmd_id = 0;
 	}
-
-	if (netdev->ft_ds_list) {
-		l_queue_destroy(netdev->ft_ds_list, netdev_ft_ds_entry_free);
-		netdev->ft_ds_list = NULL;
-	}
 }
 
 static void netdev_connect_failed(struct netdev *netdev,
@@ -962,11 +940,6 @@ static void netdev_free(void *data)
 	if (netdev->fw_roam_bss)
 		scan_bss_free(netdev->fw_roam_bss);
 
-	if (netdev->ft_ds_list) {
-		l_queue_destroy(netdev->ft_ds_list, netdev_ft_ds_entry_free);
-		netdev->ft_ds_list = NULL;
-	}
-
 	scan_wdev_remove(netdev->wdev_id);
 
 	watchlist_destroy(&netdev->station_watches);
@@ -1413,14 +1386,12 @@ static void netdev_connect_ok(struct net
 			scan_bss_free(netdev->fw_roam_bss);
 
 		netdev->fw_roam_bss = NULL;
-	}
-
-	if (netdev->ft_ds_list) {
-		l_queue_destroy(netdev->ft_ds_list, netdev_ft_ds_entry_free);
-		netdev->ft_ds_list = NULL;
-	}
-
-	if (netdev->connect_cb) {
+	} else if (netdev->in_ft) {
+		if (netdev->event_filter)
+			netdev->event_filter(netdev, NETDEV_EVENT_FT_ROAMED,
+						NULL, netdev->user_data);
+		netdev->in_ft = false;
+	} else if (netdev->connect_cb) {
 		netdev->connect_cb(netdev, NETDEV_RESULT_OK, NULL,
 					netdev->user_data);
 		netdev->connect_cb = NULL;
@@ -1498,8 +1469,13 @@ static void netdev_setting_keys_failed(s
 
 static void try_handshake_complete(struct netdev_handshake_state *nhs)
 {
-	if (nhs->ptk_installed && nhs->gtk_installed && nhs->igtk_installed &&
-			!nhs->complete) {
+	if (nhs->ptk_installed && nhs->gtk_installed && nhs->igtk_installed) {
+		if (nhs->complete) {
+			handshake_event(&nhs->super,
+					HANDSHAKE_EVENT_REKEY_COMPLETE);
+			return;
+		}
+
 		nhs->complete = true;
 
 		if (handshake_event(&nhs->super, HANDSHAKE_EVENT_COMPLETE))
@@ -1593,12 +1569,17 @@ static bool netdev_copy_tk(uint8_t *tk_b
 {
 	switch (cipher) {
 	case CRYPTO_CIPHER_CCMP:
+	case CRYPTO_CIPHER_GCMP:
+	case CRYPTO_CIPHER_GCMP_256:
+	case CRYPTO_CIPHER_CCMP_256:
 		/*
-		 * 802.11-2016 12.8.3 Mapping PTK to CCMP keys:
+		 * 802.11-2020 12.8.3 Mapping PTK to CCMP keys:
 		 * "A STA shall use the temporal key as the CCMP key
 		 * for MPDUs between the two communicating STAs."
+		 *
+		 * Similar verbiage in 12.8.8
 		 */
-		memcpy(tk_buf, tk, 16);
+		memcpy(tk_buf, tk, crypto_cipher_key_len(cipher));
 		break;
 	case CRYPTO_CIPHER_TKIP:
 		/*
@@ -1659,7 +1640,7 @@ static void netdev_set_gtk(struct handsh
 
 	nhs->gtk_installed = false;
 
-	l_debug("%d", netdev->index);
+	l_debug("ifindex=%d key_idx=%u", netdev->index, key_index);
 
 	if (crypto_cipher_key_len(cipher) != gtk_len) {
 		l_error("Unexpected key length: %d", gtk_len);
@@ -1667,7 +1648,7 @@ static void netdev_set_gtk(struct handsh
 		return;
 	}
 
-	if (!netdev_copy_tk(gtk_buf, gtk, cipher, false)) {
+	if (!netdev_copy_tk(gtk_buf, gtk, cipher, hs->authenticator)) {
 		netdev_setting_keys_failed(nhs, -ENOENT);
 		return;
 	}
@@ -1698,13 +1679,13 @@ static void netdev_set_igtk(struct hands
 {
 	struct netdev_handshake_state *nhs =
 		l_container_of(hs, struct netdev_handshake_state, super);
-	uint8_t igtk_buf[16];
+	uint8_t igtk_buf[32];
 	struct netdev *netdev = nhs->netdev;
 	struct l_genl_msg *msg;
 
 	nhs->igtk_installed = false;
 
-	l_debug("%d", netdev->index);
+	l_debug("ifindex=%d key_idx=%u", netdev->index, key_index);
 
 	if (crypto_cipher_key_len(cipher) != igtk_len) {
 		l_error("Unexpected key length: %d", igtk_len);
@@ -1713,8 +1694,11 @@ static void netdev_set_igtk(struct hands
 	}
 
 	switch (cipher) {
-	case CRYPTO_CIPHER_BIP:
-		memcpy(igtk_buf, igtk, 16);
+	case CRYPTO_CIPHER_BIP_CMAC:
+	case CRYPTO_CIPHER_BIP_GMAC:
+	case CRYPTO_CIPHER_BIP_GMAC_256:
+	case CRYPTO_CIPHER_BIP_CMAC_256:
+		memcpy(igtk_buf, igtk, igtk_len);
 		break;
 	default:
 		l_error("Unexpected cipher: %x", cipher);
@@ -2045,6 +2029,8 @@ static void netdev_set_tk(struct handsha
 	const uint8_t *addr = netdev_choose_key_address(nhs);
 	int err;
 
+	nhs->ptk_installed = false;
+
 	/*
 	 * WPA1 does the group handshake after the 4-way finishes so we can't
 	 * rely on the gtk/igtk being set immediately after the ptk. Since
@@ -2075,10 +2061,10 @@ static void netdev_set_tk(struct handsha
 		return;
 	}
 
-	l_debug("%d", netdev->index);
+	l_debug("ifindex=%d key_idx=%u", netdev->index, key_index);
 
 	err = -ENOENT;
-	if (!netdev_copy_tk(tk_buf, tk, cipher, false))
+	if (!netdev_copy_tk(tk_buf, tk, cipher, hs->authenticator))
 		goto invalid_key;
 
 	msg = netdev_build_cmd_new_key_pairwise(netdev, cipher, addr, tk_buf,
@@ -2112,7 +2098,7 @@ static void netdev_set_ext_tk(struct han
 				L_BE16_TO_CPU(step4->header.packet_len);
 
 	err = -ENOENT;
-	if (!netdev_copy_tk(tk_buf, tk, cipher, false))
+	if (!netdev_copy_tk(tk_buf, tk, cipher, hs->authenticator))
 		goto error;
 
 	msg = netdev_build_cmd_new_rx_key_pairwise(netdev, cipher, addr, tk_buf,
@@ -2548,6 +2534,44 @@ static unsigned int ie_rsn_akm_suite_to_
 	return 0;
 }
 
+static void netdev_append_nl80211_rsn_attributes(struct l_genl_msg *msg,
+						struct handshake_state *hs)
+{
+	uint32_t nl_cipher;
+	uint32_t nl_akm;
+	uint32_t wpa_version;
+
+	nl_cipher = ie_rsn_cipher_suite_to_cipher(hs->pairwise_cipher);
+	L_WARN_ON(!nl_cipher);
+	l_genl_msg_append_attr(msg, NL80211_ATTR_CIPHER_SUITES_PAIRWISE,
+					4, &nl_cipher);
+
+	nl_cipher = ie_rsn_cipher_suite_to_cipher(hs->group_cipher);
+	L_WARN_ON(!nl_cipher);
+	l_genl_msg_append_attr(msg, NL80211_ATTR_CIPHER_SUITE_GROUP,
+					4, &nl_cipher);
+
+	if (hs->mfp) {
+		uint32_t use_mfp = NL80211_MFP_REQUIRED;
+
+		l_genl_msg_append_attr(msg, NL80211_ATTR_USE_MFP, 4, &use_mfp);
+	}
+
+	nl_akm = ie_rsn_akm_suite_to_nl80211(hs->akm_suite);
+	L_WARN_ON(!nl_akm);
+	l_genl_msg_append_attr(msg, NL80211_ATTR_AKM_SUITES, 4, &nl_akm);
+
+	if (IE_AKM_IS_SAE(hs->akm_suite))
+		wpa_version = NL80211_WPA_VERSION_3;
+	else if (hs->wpa_ie)
+		wpa_version = NL80211_WPA_VERSION_1;
+	else
+		wpa_version = NL80211_WPA_VERSION_2;
+
+	l_genl_msg_append_attr(msg, NL80211_ATTR_WPA_VERSIONS,
+						4, &wpa_version);
+}
+
 static struct l_genl_msg *netdev_build_cmd_connect(struct netdev *netdev,
 						struct handshake_state *hs,
 						const uint8_t *prev_bssid,
@@ -2604,49 +2628,18 @@ static struct l_genl_msg *netdev_build_c
 	l_genl_msg_append_attr(msg, NL80211_ATTR_SOCKET_OWNER, 0, NULL);
 
 	if (is_rsn) {
-		uint32_t nl_cipher;
-		uint32_t nl_akm;
-		uint32_t wpa_version;
-
-		if (hs->pairwise_cipher == IE_RSN_CIPHER_SUITE_CCMP)
-			nl_cipher = CRYPTO_CIPHER_CCMP;
-		else
-			nl_cipher = CRYPTO_CIPHER_TKIP;
-
-		l_genl_msg_append_attr(msg, NL80211_ATTR_CIPHER_SUITES_PAIRWISE,
-					4, &nl_cipher);
-
-		if (hs->group_cipher == IE_RSN_CIPHER_SUITE_CCMP)
-			nl_cipher = CRYPTO_CIPHER_CCMP;
-		else
-			nl_cipher = CRYPTO_CIPHER_TKIP;
-
-		l_genl_msg_append_attr(msg, NL80211_ATTR_CIPHER_SUITE_GROUP,
-					4, &nl_cipher);
-
-		if (hs->mfp) {
-			uint32_t use_mfp = NL80211_MFP_REQUIRED;
-			l_genl_msg_append_attr(msg, NL80211_ATTR_USE_MFP,
-								4, &use_mfp);
-		}
-
-		nl_akm = ie_rsn_akm_suite_to_nl80211(hs->akm_suite);
-		if (nl_akm)
-			l_genl_msg_append_attr(msg, NL80211_ATTR_AKM_SUITES,
-							4, &nl_akm);
-
-		if (IE_AKM_IS_SAE(hs->akm_suite))
-			wpa_version = NL80211_WPA_VERSION_3;
-		else if (hs->wpa_ie)
-			wpa_version = NL80211_WPA_VERSION_1;
-		else
-			wpa_version = NL80211_WPA_VERSION_2;
+		netdev_append_nl80211_rsn_attributes(msg, hs);
+		c_iov = iov_ie_append(iov, n_iov, c_iov, hs->supplicant_ie);
+	}
 
-		l_genl_msg_append_attr(msg, NL80211_ATTR_WPA_VERSIONS,
-						4, &wpa_version);
+	if (is_rsn || hs->settings_8021x) {
+		l_genl_msg_append_attr(msg, NL80211_ATTR_CONTROL_PORT,
+						0, NULL);
 
-		l_genl_msg_append_attr(msg, NL80211_ATTR_CONTROL_PORT, 0, NULL);
-		c_iov = iov_ie_append(iov, n_iov, c_iov, hs->supplicant_ie);
+		if (netdev->pae_over_nl80211)
+			l_genl_msg_append_attr(msg,
+					NL80211_ATTR_CONTROL_PORT_OVER_NL80211,
+					0, NULL);
 	}
 
 	if (netdev->owe_sm) {
@@ -2654,11 +2647,6 @@ static struct l_genl_msg *netdev_build_c
 		c_iov = iov_ie_append(iov, n_iov, c_iov, owe_dh_ie);
 	}
 
-	if (netdev->pae_over_nl80211)
-		l_genl_msg_append_attr(msg,
-				NL80211_ATTR_CONTROL_PORT_OVER_NL80211,
-				0, NULL);
-
 	c_iov = iov_ie_append(iov, n_iov, c_iov, hs->mde);
 	c_iov = netdev_populate_common_ies(netdev, hs, msg, iov, n_iov, c_iov);
 
@@ -2734,8 +2722,10 @@ static void netdev_connect_event(struct
 	size_t ies_len = 0;
 	struct ie_tlv_iter iter;
 	const uint8_t *resp_ies = NULL;
-	size_t resp_ies_len;
+	size_t resp_ies_len = 0;
 	struct handshake_state *hs = netdev->handshake;
+	bool timeout = false;
+	uint32_t timeout_reason = 0;
 
 	l_debug("");
 
@@ -2763,8 +2753,14 @@ static void netdev_connect_event(struct
 	while (l_genl_attr_next(&attr, &type, &len, &data)) {
 		switch (type) {
 		case NL80211_ATTR_TIMED_OUT:
-			l_warn("authentication timed out");
-			goto error;
+			timeout = true;
+			break;
+		case NL80211_ATTR_TIMEOUT_REASON:
+			if (len != 4)
+				break;
+
+			timeout_reason = l_get_u32(data);
+			break;
 		case NL80211_ATTR_STATUS_CODE:
 			if (len == sizeof(uint16_t))
 				status_code = data;
@@ -2780,6 +2776,11 @@ static void netdev_connect_event(struct
 		}
 	}
 
+	if (timeout) {
+		l_warn("connect event timed out, reason=%u", timeout_reason);
+		goto error;
+	}
+
 	if (netdev->expect_connect_failure) {
 		/*
 		 * The kernel may think we are connected when we are actually
@@ -2969,52 +2970,17 @@ static struct l_genl_msg *netdev_build_c
 	l_genl_msg_append_attr(msg, NL80211_ATTR_SSID, hs->ssid_len, hs->ssid);
 	l_genl_msg_append_attr(msg, NL80211_ATTR_SOCKET_OWNER, 0, NULL);
 
-	if (is_rsn) {
-		uint32_t nl_cipher;
-		uint32_t nl_akm;
-		uint32_t wpa_version;
+	if (is_rsn)
+		netdev_append_nl80211_rsn_attributes(msg, hs);
 
-		l_genl_msg_append_attr(msg, NL80211_ATTR_CONTROL_PORT, 0, NULL);
+	if (is_rsn || hs->settings_8021x) {
+		l_genl_msg_append_attr(msg, NL80211_ATTR_CONTROL_PORT,
+						0, NULL);
 
 		if (netdev->pae_over_nl80211)
 			l_genl_msg_append_attr(msg,
 					NL80211_ATTR_CONTROL_PORT_OVER_NL80211,
 					0, NULL);
-
-		if (hs->pairwise_cipher == IE_RSN_CIPHER_SUITE_CCMP)
-			nl_cipher = CRYPTO_CIPHER_CCMP;
-		else
-			nl_cipher = CRYPTO_CIPHER_TKIP;
-
-		l_genl_msg_append_attr(msg, NL80211_ATTR_CIPHER_SUITES_PAIRWISE,
-					4, &nl_cipher);
-
-		if (hs->group_cipher == IE_RSN_CIPHER_SUITE_CCMP)
-			nl_cipher = CRYPTO_CIPHER_CCMP;
-		else
-			nl_cipher = CRYPTO_CIPHER_TKIP;
-
-		l_genl_msg_append_attr(msg, NL80211_ATTR_CIPHER_SUITE_GROUP,
-					4, &nl_cipher);
-
-		if (hs->mfp) {
-			uint32_t use_mfp = NL80211_MFP_REQUIRED;
-			l_genl_msg_append_attr(msg, NL80211_ATTR_USE_MFP,
-								4, &use_mfp);
-		}
-
-		nl_akm = ie_rsn_akm_suite_to_nl80211(hs->akm_suite);
-		if (nl_akm)
-			l_genl_msg_append_attr(msg, NL80211_ATTR_AKM_SUITES,
-							4, &nl_akm);
-
-		if (hs->wpa_ie)
-			wpa_version = NL80211_WPA_VERSION_1;
-		else
-			wpa_version = NL80211_WPA_VERSION_2;
-
-		l_genl_msg_append_attr(msg, NL80211_ATTR_WPA_VERSIONS,
-						4, &wpa_version);
 	}
 
 	return msg;
@@ -3106,7 +3072,7 @@ static void netdev_authenticate_event(st
 	while (l_genl_attr_next(&attr, &type, &len, &data)) {
 		switch (type) {
 		case NL80211_ATTR_TIMED_OUT:
-			l_warn("authentication timed out");
+			l_warn("authentication event timed out");
 
 			if (auth_proto_auth_timeout(netdev->ap))
 				return;
@@ -3192,13 +3158,15 @@ static void netdev_associate_event(struc
 	const uint8_t *frame = NULL;
 	uint16_t status_code = MMPDU_STATUS_CODE_UNSPECIFIED;
 	int ret;
+	const struct mmpdu_header *hdr;
+	const struct mmpdu_association_response *assoc;
 
 	l_debug("");
 
 	if (!netdev->connected || netdev->aborting)
 		return;
 
-	if (!netdev->ap) {
+	if (!netdev->ap && !netdev->in_ft) {
 		netdev->associated = true;
 		netdev->in_reassoc = false;
 		return;
@@ -3238,61 +3206,58 @@ static void netdev_associate_event(struc
 	if (L_WARN_ON(!frame))
 		goto assoc_failed;
 
-	if (netdev->ap) {
-		const struct mmpdu_header *hdr;
-		const struct mmpdu_association_response *assoc;
-
-		hdr = mpdu_validate(frame, frame_len);
-		if (L_WARN_ON(!hdr))
-			goto assoc_failed;
+	hdr = mpdu_validate(frame, frame_len);
+	if (L_WARN_ON(!hdr))
+		goto assoc_failed;
 
-		assoc = mmpdu_body(hdr);
-		status_code = L_CPU_TO_LE16(assoc->status_code);
+	assoc = mmpdu_body(hdr);
+	status_code = L_CPU_TO_LE16(assoc->status_code);
 
-		ret = auth_proto_rx_associate(netdev->ap, frame, frame_len);
-		if (ret == 0) {
-			bool fils = !!(netdev->handshake->akm_suite &
-					(IE_RSN_AKM_SUITE_FILS_SHA256 |
-					 IE_RSN_AKM_SUITE_FILS_SHA384 |
-					 IE_RSN_AKM_SUITE_FT_OVER_FILS_SHA384 |
-					 IE_RSN_AKM_SUITE_FT_OVER_FILS_SHA256));
+	if (netdev->ap)
+		ret = auth_proto_rx_associate(netdev->ap, frame,
+							frame_len);
+	else
+		ret = __ft_rx_associate(netdev->index, frame,
+							frame_len);
+	if (ret == 0) {
+		bool fils = !!(netdev->handshake->akm_suite &
+				(IE_RSN_AKM_SUITE_FILS_SHA256 |
+				 IE_RSN_AKM_SUITE_FILS_SHA384 |
+				 IE_RSN_AKM_SUITE_FT_OVER_FILS_SHA384 |
+				 IE_RSN_AKM_SUITE_FT_OVER_FILS_SHA256));
 
+		if (netdev->ap) {
 			auth_proto_free(netdev->ap);
 			netdev->ap = NULL;
+		}
 
-			netdev->sm = eapol_sm_new(netdev->handshake);
-			eapol_register(netdev->sm);
-
-			/* Just in case this was a retry */
-			netdev->ignore_connect_event = false;
+		netdev->sm = eapol_sm_new(netdev->handshake);
+		eapol_register(netdev->sm);
 
-			/*
-			 * If in FT and/or FILS we don't force an initial 4-way
-			 * handshake and instead just keep the EAPoL state
-			 * machine for the rekeys.
-			 */
-			if (netdev->in_ft || fils)
-				eapol_sm_set_require_handshake(netdev->sm,
-								false);
-
-			netdev->in_ft = false;
-			netdev->in_reassoc = false;
-			netdev->associated = true;
-			return;
-		} else if (ret == -EAGAIN) {
-			/*
-			 * Here to support OWE retries. OWE will retry
-			 * internally, but a connect event will still be emitted
-			 */
-			netdev->ignore_connect_event = true;
-			return;
-		} else if (ret > 0)
-			status_code = (uint16_t)ret;
+		/* Just in case this was a retry */
+		netdev->ignore_connect_event = false;
 
-		goto assoc_failed;
-	}
+		/*
+		 * If in FT and/or FILS we don't force an initial 4-way
+		 * handshake and instead just keep the EAPoL state
+		 * machine for the rekeys.
+		 */
+		if (netdev->in_ft || fils)
+			eapol_sm_set_require_handshake(netdev->sm,
+							false);
 
-	return;
+		netdev->in_reassoc = false;
+		netdev->associated = true;
+		return;
+	} else if (ret == -EAGAIN) {
+		/*
+		 * Here to support OWE retries. OWE will retry
+		 * internally, but a connect event will still be emitted
+		 */
+		netdev->ignore_connect_event = true;
+		return;
+	} else if (ret > 0)
+		status_code = (uint16_t)ret;
 
 assoc_failed:
 	netdev->result = NETDEV_RESULT_ASSOCIATION_FAILED;
@@ -3584,8 +3549,7 @@ failed:
 	return -EIO;
 }
 
-static void netdev_mac_change_failed(struct netdev *netdev,
-					struct rtnl_data *req, int error)
+static void netdev_mac_change_failed(struct netdev *netdev, int error)
 {
 	l_error("Error setting mac address on %d: %s", netdev->index,
 			strerror(-error));
@@ -3601,12 +3565,8 @@ static void netdev_mac_change_failed(str
 
 		goto failed;
 	} else {
-		/*
-		 * If the interface is up we can still try and connect. This
-		 * is a very rare case and most likely will never happen.
-		 */
-		l_info("Interface still up after failing to change the MAC, "
-			"continuing with connection");
+		/* If the interface is up we can still try and connect */
+		l_info("Failed to change the MAC, continuing with connection");
 		if (netdev_begin_connection(netdev) < 0)
 			goto failed;
 
@@ -3641,9 +3601,9 @@ static void netdev_mac_power_up_cb(int e
 	netdev->mac_change_cmd_id = 0;
 
 	if (error) {
-		l_error("Error taking interface %u up for per-network MAC "
-			"generation: %s", netdev->index, strerror(-error));
-		netdev_mac_change_failed(netdev, req, error);
+		l_error("Error changing per-network MAC on interface %u: %s",
+			netdev->index, strerror(-error));
+		netdev_mac_change_failed(netdev, error);
 		return;
 	}
 
@@ -3669,18 +3629,16 @@ static void netdev_mac_power_down_cb(int
 	if (error) {
 		l_error("Error taking interface %u down for per-network MAC "
 			"generation: %s", netdev->index, strerror(-error));
-		netdev_mac_change_failed(netdev, req, error);
+		netdev_mac_change_failed(netdev, error);
 		return;
 	}
 
-	l_debug("Setting generated address on ifindex: %d to: "MAC,
-					netdev->index, MAC_STR(req->addr));
 	netdev->mac_change_cmd_id = l_rtnl_set_mac(rtnl, netdev->index,
 					req->addr, true,
 					netdev_mac_power_up_cb, req,
 					netdev_mac_destroy);
 	if (!netdev->mac_change_cmd_id) {
-		netdev_mac_change_failed(netdev, req, -EIO);
+		netdev_mac_change_failed(netdev, -EIO);
 		return;
 	}
 
@@ -3713,6 +3671,8 @@ static int netdev_start_powered_mac_chan
 {
 	struct rtnl_data *req;
 	uint8_t new_addr[6];
+	bool powered = wiphy_has_ext_feature(netdev->wiphy,
+				NL80211_EXT_FEATURE_POWERED_ADDR_CHANGE);
 
 	/* No address set in handshake, use per-network MAC generation */
 	if (l_memeqzero(netdev->handshake->spa, ETH_ALEN))
@@ -3734,9 +3694,16 @@ static int netdev_start_powered_mac_chan
 	req->ref++;
 	memcpy(req->addr, new_addr, sizeof(req->addr));
 
-	netdev->mac_change_cmd_id = l_rtnl_set_powered(rtnl, netdev->index,
-					false, netdev_mac_power_down_cb,
-					req, netdev_mac_destroy);
+	if (powered)
+		netdev->mac_change_cmd_id = l_rtnl_set_mac(rtnl, netdev->index,
+						req->addr, false,
+						netdev_mac_power_up_cb, req,
+						netdev_mac_destroy);
+	else
+		netdev->mac_change_cmd_id = l_rtnl_set_powered(rtnl,
+						netdev->index, false,
+						netdev_mac_power_down_cb, req,
+						netdev_mac_destroy);
 
 	if (!netdev->mac_change_cmd_id) {
 		l_free(req);
@@ -3744,6 +3711,10 @@ static int netdev_start_powered_mac_chan
 		return -EIO;
 	}
 
+	l_debug("Setting generated address on ifindex: %d to: "MAC" (%s)",
+					netdev->index, MAC_STR(req->addr),
+					powered ? "powered" : "power-down");
+
 	return 0;
 }
 
@@ -4332,6 +4303,7 @@ static uint32_t netdev_send_action_frame
 {
 	uint32_t id;
 	struct l_genl_msg *msg = nl80211_build_cmd_frame(netdev->index,
+								0x00d0,
 								netdev->addr,
 								to, freq,
 								iov, iov_len);
@@ -4360,53 +4332,44 @@ static uint32_t netdev_send_action_frame
 						user_data);
 }
 
-static void netdev_cmd_authenticate_ft_cb(struct l_genl_msg *msg,
-						void *user_data)
+static void netdev_ft_frame_cb(struct l_genl_msg *msg, void *user_data)
 {
-	struct netdev *netdev = user_data;
-
-	netdev->connect_cmd_id = 0;
-
 	if (l_genl_msg_get_error(msg) < 0)
-		netdev_connect_failed(netdev,
-					NETDEV_RESULT_AUTHENTICATION_FAILED,
-					MMPDU_STATUS_CODE_UNSPECIFIED);
+		l_debug("Failed to send FT-Frame");
 }
 
-static void netdev_ft_tx_authenticate(struct iovec *iov,
-					size_t iov_len, void *user_data)
+static int netdev_tx_ft_frame(uint32_t ifindex, uint16_t frame_type,
+					uint32_t frequency, const uint8_t *dest,
+					struct iovec *iov, size_t iov_len)
 {
-	struct netdev *netdev = user_data;
-	struct l_genl_msg *cmd_authenticate;
+	struct netdev *netdev = netdev_find(ifindex);
+	struct l_genl_msg *msg = nl80211_build_cmd_frame(netdev->index,
+							frame_type,
+							netdev->addr, dest,
+							frequency,
+							iov, iov_len);
 
-	cmd_authenticate = netdev_build_cmd_authenticate(netdev,
-							NL80211_AUTHTYPE_FT);
-	l_genl_msg_append_attrv(cmd_authenticate, NL80211_ATTR_IE, iov,
-					iov_len);
+	/*
+	 * Even though the kernel is doing offchannel for Authentication this
+	 * flag is still required otherwise the kernel gives -EBUSY.
+	 */
+	l_genl_msg_append_attr(msg, NL80211_ATTR_OFFCHANNEL_TX_OK, 0, NULL);
 
-	netdev->connect_cmd_id = l_genl_family_send(nl80211,
-						cmd_authenticate,
-						netdev_cmd_authenticate_ft_cb,
-						netdev, NULL);
-	if (!netdev->connect_cmd_id) {
-		l_genl_msg_unref(cmd_authenticate);
-		goto restore_snonce;
+	if (!l_genl_family_send(nl80211, msg, netdev_ft_frame_cb,
+				netdev, NULL)) {
+		l_genl_msg_unref(msg);
+		return -EIO;
 	}
 
-	return;
-
-restore_snonce:
-	memcpy(netdev->handshake->snonce, netdev->prev_snonce, 32);
-
-	netdev_connect_failed(netdev, NETDEV_RESULT_AUTHENTICATION_FAILED,
-					MMPDU_STATUS_CODE_UNSPECIFIED);
+	return 0;
 }
 
-static int netdev_ft_tx_associate(struct iovec *ft_iov, size_t n_ft_iov,
-					void *user_data)
+static int netdev_ft_tx_associate(uint32_t ifindex, uint32_t freq,
+					const uint8_t *prev_bssid,
+					struct iovec *ft_iov, size_t n_ft_iov)
 {
-	struct netdev *netdev = user_data;
-	struct auth_proto *ap = netdev->ap;
+	struct netdev *netdev = netdev_find(ifindex);
+	struct netdev_handshake_state *nhs;
 	struct handshake_state *hs = netdev->handshake;
 	struct l_genl_msg *msg;
 	struct iovec iov[64];
@@ -4415,61 +4378,16 @@ static int netdev_ft_tx_associate(struct
 	enum mpdu_management_subtype subtype =
 				MPDU_MANAGEMENT_SUBTYPE_REASSOCIATION_REQUEST;
 
-	msg = netdev_build_cmd_associate_common(netdev);
-
-	c_iov = netdev_populate_common_ies(netdev, hs, msg, iov, n_iov, c_iov);
-
-	if (!L_WARN_ON(n_iov - c_iov < n_ft_iov)) {
-		memcpy(iov + c_iov, ft_iov, sizeof(*ft_iov) * n_ft_iov);
-		c_iov += n_ft_iov;
-	}
-
-	mpdu_sort_ies(subtype, iov, c_iov);
-
-	l_genl_msg_append_attr(msg, NL80211_ATTR_PREV_BSSID, ETH_ALEN,
-				ap->prev_bssid);
-	l_genl_msg_append_attrv(msg, NL80211_ATTR_IE, iov, c_iov);
-
-	netdev->connect_cmd_id = l_genl_family_send(nl80211, msg,
-						netdev_cmd_ft_reassociate_cb,
-						netdev, NULL);
-	if (!netdev->connect_cmd_id) {
-		l_genl_msg_unref(msg);
-
-		return -EIO;
-	}
-
-	return 0;
-}
-
-static void prepare_ft(struct netdev *netdev, const struct scan_bss *target_bss)
-{
-	struct netdev_handshake_state *nhs;
-
 	/*
-	 * We reuse the handshake_state object and reset what's needed.
-	 * Could also create a new object and copy most of the state but
-	 * we would end up doing more work.
+	 * At this point there is no going back with FT so reset all the flags
+	 * needed to associate with a new BSS.
 	 */
-	memcpy(netdev->prev_snonce, netdev->handshake->snonce, 32);
-
-	netdev->frequency = target_bss->frequency;
-
-	handshake_state_set_authenticator_address(netdev->handshake,
-							target_bss->addr);
-
-	if (target_bss->rsne)
-		handshake_state_set_authenticator_ie(netdev->handshake,
-							target_bss->rsne);
-	memcpy(netdev->handshake->mde + 2, target_bss->mde, 3);
-
+	netdev->frequency = freq;
 	netdev->handshake->active_tk_index = 0;
 	netdev->associated = false;
 	netdev->operational = false;
 	netdev->in_ft = true;
 
-	handshake_state_set_chandef(netdev->handshake, NULL);
-
 	/*
 	 * Cancel commands that could be running because of EAPoL activity
 	 * like re-keying, this way the callbacks for those commands don't
@@ -4507,31 +4425,32 @@ static void prepare_ft(struct netdev *ne
 		eapol_sm_free(netdev->sm);
 		netdev->sm = NULL;
 	}
-}
 
-static void netdev_ft_over_ds_auth_failed(struct netdev_ft_over_ds_info *info,
-						uint16_t status)
-{
-	l_queue_remove(info->netdev->ft_ds_list, info);
-	ft_ds_info_free(&info->super);
-}
+	msg = netdev_build_cmd_associate_common(netdev);
 
-struct ft_ds_finder {
-	const uint8_t *spa;
-	const uint8_t *aa;
-};
+	c_iov = netdev_populate_common_ies(netdev, hs, msg, iov, n_iov, c_iov);
 
-static bool match_ft_ds_info(const void *a, const void *b)
-{
-	const struct netdev_ft_over_ds_info *info = a;
-	const struct ft_ds_finder *finder = b;
+	if (!L_WARN_ON(n_iov - c_iov < n_ft_iov)) {
+		memcpy(iov + c_iov, ft_iov, sizeof(*ft_iov) * n_ft_iov);
+		c_iov += n_ft_iov;
+	}
 
-	if (memcmp(info->super.spa, finder->spa, 6))
-		return false;
-	if (memcmp(info->super.aa, finder->aa, 6))
-		return false;
+	mpdu_sort_ies(subtype, iov, c_iov);
 
-	return true;
+	l_genl_msg_append_attr(msg, NL80211_ATTR_PREV_BSSID, ETH_ALEN,
+				prev_bssid);
+	l_genl_msg_append_attrv(msg, NL80211_ATTR_IE, iov, c_iov);
+
+	netdev->connect_cmd_id = l_genl_family_send(nl80211, msg,
+						netdev_cmd_ft_reassociate_cb,
+						netdev, NULL);
+	if (!netdev->connect_cmd_id) {
+		l_genl_msg_unref(msg);
+
+		return -EIO;
+	}
+
+	return 0;
 }
 
 static void netdev_ft_response_frame_event(const struct mmpdu_header *hdr,
@@ -4539,49 +4458,25 @@ static void netdev_ft_response_frame_eve
 					int rssi, void *user_data)
 {
 	struct netdev *netdev = user_data;
-	struct netdev_ft_over_ds_info *info;
-	int ret;
-	uint16_t status_code = MMPDU_STATUS_CODE_UNSPECIFIED;
-	const uint8_t *aa;
-	const uint8_t *spa;
-	const uint8_t *ies;
-	size_t ies_len;
-	struct ft_ds_finder finder;
 
 	if (!netdev->connected)
 		return;
 
-	ret = ft_over_ds_parse_action_response(body, body_len, &spa, &aa,
-						&ies, &ies_len);
-	if (ret < 0)
-		return;
+	__ft_rx_action(netdev->index, (const uint8_t *)hdr,
+			mmpdu_header_len(hdr) + body_len);
+}
 
-	finder.spa = spa;
-	finder.aa = aa;
+static void netdev_ft_auth_response_frame_event(const struct mmpdu_header *hdr,
+					const void *body, size_t body_len,
+					int rssi, void *user_data)
+{
+	struct netdev *netdev = user_data;
 
-	info = l_queue_find(netdev->ft_ds_list, match_ft_ds_info, &finder);
-	if (!info)
+	if (!netdev->connected)
 		return;
 
-	/* Lookup successful, now check the status code */
-	if (ret > 0) {
-		status_code = (uint16_t)ret;
-		goto ft_error;
-	}
-
-	if (!ft_over_ds_parse_action_ies(&info->super, netdev->handshake,
-						ies, ies_len))
-		goto ft_error;
-
-	info->parsed = true;
-
-	return;
-
-ft_error:
-	l_debug("FT-over-DS to "MAC" failed (%d)", MAC_STR(info->super.aa),
-			status_code);
-
-	netdev_ft_over_ds_auth_failed(info, status_code);
+	__ft_rx_authenticate(netdev->index, (const uint8_t *)hdr,
+			mmpdu_header_len(hdr) + body_len);
 }
 
 static void netdev_qos_map_frame_event(const struct mmpdu_header *hdr,
@@ -4606,185 +4501,6 @@ static void netdev_qos_map_frame_event(c
 	netdev_send_qos_map_set(netdev, body + 4, body_len - 4);
 }
 
-static bool netdev_ft_work_ready(struct wiphy_radio_work_item *item)
-{
-	struct netdev *netdev = l_container_of(item, struct netdev, work);
-
-	if (!auth_proto_start(netdev->ap)) {
-		/* Restore original nonce */
-		memcpy(netdev->handshake->snonce, netdev->prev_snonce, 32);
-
-		netdev_connect_failed(netdev, NETDEV_RESULT_ABORTED,
-						MMPDU_STATUS_CODE_UNSPECIFIED);
-		return true;
-	}
-
-	return false;
-}
-
-static const struct wiphy_radio_work_item_ops ft_work_ops = {
-	.do_work = netdev_ft_work_ready,
-};
-
-int netdev_fast_transition(struct netdev *netdev,
-				const struct scan_bss *target_bss,
-				const struct scan_bss *orig_bss,
-				netdev_connect_cb_t cb)
-{
-	if (!netdev->operational)
-		return -ENOTCONN;
-
-	if (!netdev->handshake->mde || !target_bss->mde_present ||
-			l_get_le16(netdev->handshake->mde + 2) !=
-			l_get_le16(target_bss->mde))
-		return -EINVAL;
-
-	prepare_ft(netdev, target_bss);
-
-	handshake_state_new_snonce(netdev->handshake);
-
-	netdev->connect_cb = cb;
-
-	netdev->ap = ft_over_air_sm_new(netdev->handshake,
-					netdev_ft_tx_authenticate,
-					netdev_ft_tx_associate,
-					netdev_get_oci, netdev);
-	memcpy(netdev->ap->prev_bssid, orig_bss->addr, ETH_ALEN);
-
-	wiphy_radio_work_insert(netdev->wiphy, &netdev->work,
-				WIPHY_WORK_PRIORITY_CONNECT, &ft_work_ops);
-
-	return 0;
-}
-
-int netdev_fast_transition_over_ds(struct netdev *netdev,
-					const struct scan_bss *target_bss,
-					const struct scan_bss *orig_bss,
-					netdev_connect_cb_t cb)
-{
-	struct netdev_ft_over_ds_info *info;
-	struct ft_ds_finder finder;
-
-	if (!netdev->operational)
-		return -ENOTCONN;
-
-	if (!netdev->handshake->mde || !target_bss->mde_present ||
-			l_get_le16(netdev->handshake->mde + 2) !=
-			l_get_le16(target_bss->mde))
-		return -EINVAL;
-
-	finder.spa = netdev->addr;
-	finder.aa = target_bss->addr;
-
-	info = l_queue_find(netdev->ft_ds_list, match_ft_ds_info, &finder);
-
-	if (!info || !info->parsed)
-		return -ENOENT;
-
-	prepare_ft(netdev, target_bss);
-
-	ft_over_ds_prepare_handshake(&info->super, netdev->handshake);
-
-	netdev->connect_cb = cb;
-
-	netdev->ap = ft_over_ds_sm_new(netdev->handshake,
-					netdev_ft_tx_associate,
-					netdev);
-	memcpy(netdev->ap->prev_bssid, orig_bss->addr, ETH_ALEN);
-
-	wiphy_radio_work_insert(netdev->wiphy, &netdev->work,
-				WIPHY_WORK_PRIORITY_CONNECT, &ft_work_ops);
-
-	return 0;
-}
-
-static void netdev_ft_request_cb(struct l_genl_msg *msg, void *user_data)
-{
-	struct netdev_ft_over_ds_info *info = user_data;
-
-	if (l_genl_msg_get_error(msg) < 0) {
-		l_error("Could not send CMD_FRAME for FT-over-DS");
-		netdev_ft_over_ds_auth_failed(info,
-					MMPDU_STATUS_CODE_UNSPECIFIED);
-	}
-}
-
-static void netdev_ft_ds_info_free(struct ft_ds_info *ft)
-{
-	struct netdev_ft_over_ds_info *info = l_container_of(ft,
-					struct netdev_ft_over_ds_info, super);
-
-	l_free(info);
-}
-
-int netdev_fast_transition_over_ds_action(struct netdev *netdev,
-					const struct scan_bss *target_bss)
-{
-	struct netdev_ft_over_ds_info *info;
-	uint8_t ft_req[14];
-	struct handshake_state *hs = netdev->handshake;
-	struct iovec iovs[5];
-	uint8_t buf[512];
-	size_t len;
-
-	if (!netdev->operational)
-		return -ENOTCONN;
-
-	if (!netdev->handshake->mde || !target_bss->mde_present ||
-			l_get_le16(netdev->handshake->mde + 2) !=
-			l_get_le16(target_bss->mde))
-		return -EINVAL;
-
-	l_debug("");
-
-	info = l_new(struct netdev_ft_over_ds_info, 1);
-	info->netdev = netdev;
-
-	memcpy(info->super.spa, hs->spa, ETH_ALEN);
-	memcpy(info->super.aa, target_bss->addr, ETH_ALEN);
-	memcpy(info->super.mde, target_bss->mde, sizeof(info->super.mde));
-
-	if (target_bss->rsne)
-		info->super.authenticator_ie = l_memdup(target_bss->rsne,
-						target_bss->rsne[1] + 2);
-
-	l_getrandom(info->super.snonce, 32);
-	info->super.free = netdev_ft_ds_info_free;
-
-	ft_req[0] = 6; /* FT category */
-	ft_req[1] = 1; /* FT Request action */
-	memcpy(ft_req + 2, netdev->addr, 6);
-	memcpy(ft_req + 8, info->super.aa, 6);
-
-	iovs[0].iov_base = ft_req;
-	iovs[0].iov_len = sizeof(ft_req);
-
-	if (!ft_build_authenticate_ies(hs, false, info->super.snonce,
-						buf, &len))
-		goto failed;
-
-	iovs[1].iov_base = buf;
-	iovs[1].iov_len = len;
-
-	iovs[2].iov_base = NULL;
-
-	if (!netdev->ft_ds_list)
-		netdev->ft_ds_list = l_queue_new();
-
-	l_queue_push_head(netdev->ft_ds_list, info);
-
-	netdev_send_action_framev(netdev, netdev->handshake->aa, iovs, 2,
-					netdev->frequency,
-					netdev_ft_request_cb,
-					info);
-
-	return 0;
-
-failed:
-	l_free(info);
-	return -EIO;
-}
-
 static void netdev_preauth_cb(const uint8_t *pmk, void *user_data)
 {
 	struct netdev_preauth_state *preauth = user_data;
@@ -5465,6 +5181,20 @@ static void netdev_channel_switch_event(
 				&netdev->frequency, netdev->user_data);
 }
 
+static void netdev_michael_mic_failure(struct l_genl_msg *msg,
+					struct netdev *netdev)
+{
+	uint8_t idx;
+	uint32_t type;
+
+	if (nl80211_parse_attrs(msg, NL80211_ATTR_KEY_IDX, &idx,
+				NL80211_ATTR_KEY_TYPE, &type,
+				NL80211_ATTR_UNSPEC) < 0)
+		return;
+
+	l_debug("ifindex=%u key_idx=%u type=%u", netdev->index, idx, type);
+}
+
 static void netdev_mlme_notify(struct l_genl_msg *msg, void *user_data)
 {
 	struct netdev *netdev = NULL;
@@ -5515,6 +5245,9 @@ static void netdev_mlme_notify(struct l_
 	case NL80211_CMD_DEL_STATION:
 		netdev_station_event(msg, netdev, false);
 		break;
+	case NL80211_CMD_MICHAEL_MIC_FAILURE:
+		netdev_michael_mic_failure(msg, netdev);
+		break;
 	}
 }
 
@@ -5811,6 +5544,7 @@ static void netdev_add_station_frame_wat
 	static const uint8_t action_sa_query_resp_prefix[2] = { 0x08, 0x01 };
 	static const uint8_t action_sa_query_req_prefix[2] = { 0x08, 0x00 };
 	static const uint8_t action_ft_response_prefix[] =  { 0x06, 0x02 };
+	static const uint8_t auth_ft_response_prefix[] = { 0x02, 0x00 };
 	static const uint8_t action_qos_map_prefix[] = { 0x01, 0x04 };
 	uint64_t wdev = netdev->wdev_id;
 
@@ -5831,6 +5565,10 @@ static void netdev_add_station_frame_wat
 			sizeof(action_ft_response_prefix),
 			netdev_ft_response_frame_event, netdev, NULL);
 
+	frame_watch_add(wdev, 0, 0x00b0, auth_ft_response_prefix,
+			sizeof(auth_ft_response_prefix),
+			netdev_ft_auth_response_frame_event, netdev, NULL);
+
 	if (wiphy_supports_qos_set_map(netdev->wiphy))
 		frame_watch_add(wdev, 0, 0x00d0, action_qos_map_prefix,
 				sizeof(action_qos_map_prefix),
@@ -6636,6 +6374,9 @@ static int netdev_init(void)
 	__eapol_set_tx_packet_func(netdev_control_port_frame);
 	__eapol_set_install_pmk_func(netdev_set_pmk);
 
+	__ft_set_tx_frame_func(netdev_tx_ft_frame);
+	__ft_set_tx_associate_func(netdev_ft_tx_associate);
+
 	unicast_watch = l_genl_add_unicast_watch(genl, NL80211_GENL_NAME,
 						netdev_unicast_notify,
 						NULL, NULL);
@@ -6700,3 +6441,4 @@ IWD_MODULE(netdev, netdev_init, netdev_e
 IWD_MODULE_DEPENDS(netdev, eapol);
 IWD_MODULE_DEPENDS(netdev, frame_xchg);
 IWD_MODULE_DEPENDS(netdev, wiphy);
+IWD_MODULE_DEPENDS(netdev, ft);
diff -pruN 1.30-1/src/netdev.h 2.3-1/src/netdev.h
--- 1.30-1/src/netdev.h	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/src/netdev.h	2022-11-18 12:31:49.000000000 +0000
@@ -50,6 +50,7 @@ enum netdev_event {
 	NETDEV_EVENT_RSSI_THRESHOLD_HIGH,
 	NETDEV_EVENT_RSSI_LEVEL_NOTIFY,
 	NETDEV_EVENT_PACKET_LOSS_NOTIFY,
+	NETDEV_EVENT_FT_ROAMED,
 };
 
 enum netdev_watch_event {
@@ -165,16 +166,7 @@ int netdev_reassociate(struct netdev *ne
 			struct handshake_state *hs,
 			netdev_event_func_t event_filter,
 			netdev_connect_cb_t cb, void *user_data);
-int netdev_fast_transition(struct netdev *netdev,
-				const struct scan_bss *target_bss,
-				const struct scan_bss *orig_bss,
-				netdev_connect_cb_t cb);
-int netdev_fast_transition_over_ds_action(struct netdev *netdev,
-					const struct scan_bss *target_bss);
-int netdev_fast_transition_over_ds(struct netdev *netdev,
-					const struct scan_bss *target_bss,
-					const struct scan_bss *orig_bss,
-					netdev_connect_cb_t cb);
+
 int netdev_preauthenticate(struct netdev *netdev,
 				const struct scan_bss *target_bss,
 				netdev_preauthenticate_cb_t cb,
diff -pruN 1.30-1/src/nl80211util.c 2.3-1/src/nl80211util.c
--- 1.30-1/src/nl80211util.c	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/src/nl80211util.c	2023-01-23 18:46:38.000000000 +0000
@@ -139,6 +139,17 @@ static bool extract_nested(const void *d
 	return true;
 }
 
+static bool extract_u8(const void *data, uint16_t len, void *o)
+{
+	uint8_t *out = o;
+
+	if (len != 1)
+		return false;
+
+	*out = l_get_u8(data);
+	return true;
+}
+
 static attr_handler handler_for_type(enum nl80211_attrs type)
 {
 	switch (type) {
@@ -146,6 +157,7 @@ static attr_handler handler_for_type(enu
 		return extract_ifindex;
 	case NL80211_ATTR_WIPHY:
 	case NL80211_ATTR_IFTYPE:
+	case NL80211_ATTR_KEY_TYPE:
 		return extract_uint32;
 	case NL80211_ATTR_WDEV:
 	case NL80211_ATTR_COOKIE:
@@ -170,6 +182,8 @@ static attr_handler handler_for_type(enu
 		return extract_iovec;
 	case NL80211_ATTR_WIPHY_BANDS:
 		return extract_nested;
+	case NL80211_ATTR_KEY_IDX:
+		return extract_u8;
 	default:
 		break;
 	}
@@ -431,6 +445,7 @@ const void *nl80211_parse_get_key_seq(st
 }
 
 struct l_genl_msg *nl80211_build_cmd_frame(uint32_t ifindex,
+						uint16_t frame_type,
 						const uint8_t *addr,
 						const uint8_t *to,
 						uint32_t freq,
@@ -439,18 +454,17 @@ struct l_genl_msg *nl80211_build_cmd_fra
 {
 	struct l_genl_msg *msg;
 	struct iovec iovs[iov_len + 1];
-	const uint16_t frame_type = 0x00d0;
-	uint8_t action_frame[24];
+	uint8_t hdr[24];
 
-	memset(action_frame, 0, 24);
+	memset(hdr, 0, 24);
 
-	l_put_le16(frame_type, action_frame + 0);
-	memcpy(action_frame + 4, to, 6);
-	memcpy(action_frame + 10, addr, 6);
-	memcpy(action_frame + 16, to, 6);
+	l_put_le16(frame_type, hdr + 0);
+	memcpy(hdr + 4, to, 6);
+	memcpy(hdr + 10, addr, 6);
+	memcpy(hdr + 16, to, 6);
 
-	iovs[0].iov_base = action_frame;
-	iovs[0].iov_len = sizeof(action_frame);
+	iovs[0].iov_base = hdr;
+	iovs[0].iov_len = sizeof(hdr);
 	memcpy(iovs + 1, iov, sizeof(*iov) * iov_len);
 
 	msg = l_genl_msg_new_sized(NL80211_CMD_FRAME, 128 + 512);
@@ -488,19 +502,21 @@ int nl80211_parse_chandef(struct l_genl_
 
 int nl80211_parse_supported_frequencies(struct l_genl_attr *band_freqs,
 					struct scan_freq_set *supported_list,
-					struct scan_freq_set *disabled_list)
+					struct band_freq_attrs *list,
+					size_t num_channels)
 {
 	uint16_t type, len;
 	const void *data;
 	struct l_genl_attr attr;
 	struct l_genl_attr nested;
+	uint8_t channel;
 
 	if (!l_genl_attr_recurse(band_freqs, &nested))
 		return -EBADMSG;
 
 	while (l_genl_attr_next(&nested, NULL, NULL, NULL)) {
 		uint32_t freq = 0;
-		bool disabled = false;
+		struct band_freq_attrs freq_attr = { 0 };
 
 		if (!l_genl_attr_recurse(&nested, &attr))
 			continue;
@@ -509,9 +525,31 @@ int nl80211_parse_supported_frequencies(
 			switch (type) {
 			case NL80211_FREQUENCY_ATTR_FREQ:
 				freq = *((uint32_t *) data);
+				freq_attr.supported = true;
 				break;
 			case NL80211_FREQUENCY_ATTR_DISABLED:
-				disabled = true;
+				freq_attr.disabled = true;
+				break;
+			case NL80211_FREQUENCY_ATTR_NO_IR:
+				freq_attr.no_ir = true;
+				break;
+			case NL80211_FREQUENCY_ATTR_NO_HT40_MINUS:
+				freq_attr.no_ht40_minus = true;
+				break;
+			case NL80211_FREQUENCY_ATTR_NO_HT40_PLUS:
+				freq_attr.no_ht40_plus = true;
+				break;
+			case NL80211_FREQUENCY_ATTR_NO_80MHZ:
+				freq_attr.no_80mhz = true;
+				break;
+			case NL80211_FREQUENCY_ATTR_NO_160MHZ:
+				freq_attr.no_160mhz = true;
+				break;
+			case NL80211_FREQUENCY_ATTR_NO_HE:
+				freq_attr.no_he = true;
+				break;
+			case NL80211_FREQUENCY_ATTR_MAX_TX_POWER:
+				freq_attr.tx_power = *((uint32_t *) data) / 100;
 				break;
 			}
 		}
@@ -519,11 +557,17 @@ int nl80211_parse_supported_frequencies(
 		if (!freq)
 			continue;
 
+		channel = band_freq_to_channel(freq, NULL);
+		if (!channel)
+			continue;
+
+		if (L_WARN_ON(channel > num_channels))
+			continue;
+
 		if (supported_list)
 			scan_freq_set_add(supported_list, freq);
 
-		if (disabled && disabled_list)
-			scan_freq_set_add(disabled_list, freq);
+		list[channel] = freq_attr;
 	}
 
 	return 0;
diff -pruN 1.30-1/src/nl80211util.h 2.3-1/src/nl80211util.h
--- 1.30-1/src/nl80211util.h	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/src/nl80211util.h	2022-12-18 19:59:36.000000000 +0000
@@ -24,6 +24,7 @@
 
 struct band_chandef;
 struct scan_freq_set;
+struct band_freq_attrs;
 
 int nl80211_parse_attrs(struct l_genl_msg *msg, int tag, ...);
 
@@ -49,6 +50,7 @@ struct l_genl_msg *nl80211_build_get_key
 const void *nl80211_parse_get_key_seq(struct l_genl_msg *msg);
 
 struct l_genl_msg *nl80211_build_cmd_frame(uint32_t ifindex,
+						uint16_t frame_type,
 						const uint8_t *addr,
 						const uint8_t *to,
 						uint32_t freq,
@@ -57,5 +59,6 @@ struct l_genl_msg *nl80211_build_cmd_fra
 
 int nl80211_parse_chandef(struct l_genl_msg *msg, struct band_chandef *out);
 int nl80211_parse_supported_frequencies(struct l_genl_attr *band_freqs,
-					struct scan_freq_set *supported,
-					struct scan_freq_set *disabled);
+					struct scan_freq_set *supported_list,
+					struct band_freq_attrs *list,
+					size_t num_channels);
diff -pruN 1.30-1/src/offchannel.c 2.3-1/src/offchannel.c
--- 1.30-1/src/offchannel.c	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/src/offchannel.c	2022-11-18 12:31:49.000000000 +0000
@@ -150,9 +150,9 @@ static const struct wiphy_radio_work_ite
 	.destroy = offchannel_work_destroy,
 };
 
-uint32_t offchannel_start(uint64_t wdev_id, uint32_t freq, uint32_t duration,
-			offchannel_started_cb_t started, void *user_data,
-			offchannel_destroy_cb_t destroy)
+uint32_t offchannel_start(uint64_t wdev_id, int priority, uint32_t freq,
+			uint32_t duration, offchannel_started_cb_t started,
+			void *user_data, offchannel_destroy_cb_t destroy)
 {
 	struct offchannel_info *info = l_new(struct offchannel_info, 1);
 
@@ -169,8 +169,7 @@ uint32_t offchannel_start(uint64_t wdev_
 	info->error = -ECANCELED;
 
 	return wiphy_radio_work_insert(wiphy_find_by_wdev(wdev_id), &info->work,
-					WIPHY_WORK_PRIORITY_OFFCHANNEL,
-					&offchannel_work_ops);
+					priority, &offchannel_work_ops);
 }
 
 void offchannel_cancel(uint64_t wdev_id, uint32_t id)
diff -pruN 1.30-1/src/offchannel.h 2.3-1/src/offchannel.h
--- 1.30-1/src/offchannel.h	2022-01-05 21:23:11.000000000 +0000
+++ 2.3-1/src/offchannel.h	2022-11-18 12:31:49.000000000 +0000
@@ -23,7 +23,7 @@
 typedef void (*offchannel_started_cb_t)(void *user_data);
 typedef void (*offchannel_destroy_cb_t)(int error, void *user_data);
 
-uint32_t offchannel_start(uint64_t wdev_id, uint32_t freq, uint32_t duration,
-			offchannel_started_cb_t started, void *user_data,
-			offchannel_destroy_cb_t destroy);
+uint32_t offchannel_start(uint64_t wdev_id, int priority, uint32_t freq,
+			uint32_t duration, offchannel_started_cb_t started,
+			void *user_data, offchannel_destroy_cb_t destroy);
 void offchannel_cancel(uint64_t wdev_id, uint32_t id);
diff -pruN 1.30-1/src/p2p.c 2.3-1/src/p2p.c
--- 1.30-1/src/p2p.c	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/src/p2p.c	2022-11-18 12:31:49.000000000 +0000
@@ -190,12 +190,6 @@ static const int channels_scan_2_4_other
  */
 #define P2P_GO_INTENT 2
 
-enum {
-	FRAME_GROUP_DEFAULT = 0,
-	FRAME_GROUP_LISTEN,
-	FRAME_GROUP_CONNECT,
-};
-
 static bool p2p_device_match(const void *a, const void *b)
 {
 	const struct p2p_device *dev = a;
@@ -718,7 +712,7 @@ static void p2p_connection_reset(struct
 
 	netdev_watch_remove(dev->conn_netdev_watch_id);
 
-	frame_watch_group_remove(dev->wdev_id, FRAME_GROUP_CONNECT);
+	frame_watch_group_remove(dev->wdev_id, FRAME_GROUP_P2P_CONNECT);
 	frame_xchg_stop_wdev(dev->wdev_id);
 
 	if (!dev->enabled || (dev->enabled && dev->start_stop_cmd_id)) {
@@ -820,6 +814,7 @@ static void p2p_peer_frame_xchg(struct p
 
 static const struct frame_xchg_prefix p2p_frame_go_neg_req = {
 	/* Management -> Public Action -> P2P -> GO Negotiation Request */
+	.frame_type = 0x00d0,
 	.data = (uint8_t []) {
 		0x04, 0x09, 0x50, 0x6f, 0x9a, 0x09,
 		P2P_ACTION_GO_NEGOTIATION_REQ
@@ -829,6 +824,7 @@ static const struct frame_xchg_prefix p2
 
 static const struct frame_xchg_prefix p2p_frame_go_neg_resp = {
 	/* Management -> Public Action -> P2P -> GO Negotiation Response */
+	.frame_type = 0x00d0,
 	.data = (uint8_t []) {
 		0x04, 0x09, 0x50, 0x6f, 0x9a, 0x09,
 		P2P_ACTION_GO_NEGOTIATION_RESP
@@ -838,6 +834,7 @@ static const struct frame_xchg_prefix p2
 
 static const struct frame_xchg_prefix p2p_frame_go_neg_confirm = {
 	/* Management -> Public Action -> P2P -> GO Negotiation Confirm */
+	.frame_type = 0x00d0,
 	.data = (uint8_t []) {
 		0x04, 0x09, 0x50, 0x6f, 0x9a, 0x09,
 		P2P_ACTION_GO_NEGOTIATION_CONFIRM
@@ -847,6 +844,7 @@ static const struct frame_xchg_prefix p2
 
 static const struct frame_xchg_prefix p2p_frame_pd_resp = {
 	/* Management -> Public Action -> P2P -> Provision Discovery Response */
+	.frame_type = 0x00d0,
 	.data = (uint8_t []) {
 		0x04, 0x09, 0x50, 0x6f, 0x9a, 0x09,
 		P2P_ACTION_PROVISION_DISCOVERY_RESP
@@ -1275,6 +1273,9 @@ static void p2p_group_start(struct p2p_d
 	/* Enable netconfig, set maximum usable DHCP lease time */
 	l_settings_set_uint(config, "IPv4", "LeaseTime", 0x7fffffff);
 
+	l_settings_set_string(config, "Security", "PairwiseCiphers", "CCMP");
+	l_settings_set_string(config, "Security", "GroupCipher", "CCMP");
+
 	dev->capability.group_caps |= P2P_GROUP_CAP_GO;
 	dev->capability.group_caps |= P2P_GROUP_CAP_GROUP_FORMATION;
 	dev->capability.group_caps |= P2P_GROUP_CAP_IP_ALLOCATION;
@@ -2523,13 +2524,13 @@ respond:
 
 	if (status == P2P_STATUS_SUCCESS)
 		p2p_peer_frame_xchg(peer, iov, dev->addr, 0, 600, 0, true,
-					FRAME_GROUP_CONNECT,
+					FRAME_GROUP_P2P_CONNECT,
 					p2p_go_negotiation_resp_done,
 					&p2p_frame_go_neg_confirm,
 					p2p_go_negotiation_confirm_cb, NULL);
 	else
 		p2p_peer_frame_xchg(peer, iov, dev->addr, 0, 0, 0, true,
-					FRAME_GROUP_CONNECT,
+					FRAME_GROUP_P2P_CONNECT,
 					p2p_go_negotiation_resp_err_done, NULL);
 
 	l_debug("GO Negotiation Response sent with status %i", status);
@@ -2800,7 +2801,7 @@ static bool p2p_go_negotiation_resp_cb(c
 	iov[iov_len].iov_base = NULL;
 
 	p2p_peer_frame_xchg(dev->conn_peer, iov, dev->conn_peer->device_addr,
-				0, 0, 0, false, FRAME_GROUP_CONNECT,
+				0, 0, 0, false, FRAME_GROUP_P2P_CONNECT,
 				p2p_go_negotiation_confirm_done, NULL);
 	l_free(confirm_body);
 
@@ -2889,7 +2890,7 @@ static void p2p_start_go_negotiation(str
 
 	p2p_peer_frame_xchg(dev->conn_peer, iov, dev->conn_peer->device_addr,
 				100, resp_timeout, 256, false,
-				FRAME_GROUP_CONNECT,
+				FRAME_GROUP_P2P_CONNECT,
 				p2p_go_negotiation_req_done,
 				&p2p_frame_go_neg_resp,
 				p2p_go_negotiation_resp_cb, NULL);
@@ -3031,7 +3032,7 @@ static void p2p_start_provision_discover
 	 * sent to the P2P Device Address of the P2P Group Owner"
 	 */
 	p2p_peer_frame_xchg(dev->conn_peer, iov, dev->conn_peer->device_addr,
-				200, 600, 8, false, FRAME_GROUP_CONNECT,
+				200, 600, 8, false, FRAME_GROUP_P2P_CONNECT,
 				p2p_provision_disc_req_done,
 				&p2p_frame_pd_resp, p2p_provision_disc_resp_cb,
 				NULL);
@@ -4163,9 +4164,9 @@ static void p2p_device_discovery_start(s
 		dev->listen_channel = channels_social[l_getrandom_uint32() %
 						L_ARRAY_SIZE(channels_social)];
 
-	frame_watch_add(dev->wdev_id, FRAME_GROUP_LISTEN, 0x0040,
+	frame_watch_add(dev->wdev_id, FRAME_GROUP_P2P_LISTEN, 0x0040,
 			(uint8_t *) "", 0, p2p_device_probe_cb, dev, NULL);
-	frame_watch_add(dev->wdev_id, FRAME_GROUP_LISTEN, 0x00d0,
+	frame_watch_add(dev->wdev_id, FRAME_GROUP_P2P_LISTEN, 0x00d0,
 			p2p_frame_go_neg_req.data, p2p_frame_go_neg_req.len,
 			p2p_device_go_negotiation_req_cb, dev, NULL);
 
@@ -4183,7 +4184,7 @@ static void p2p_device_discovery_stop(st
 		l_timeout_remove(dev->scan_timeout);
 
 	p2p_device_roc_cancel(dev);
-	frame_watch_group_remove(dev->wdev_id, FRAME_GROUP_LISTEN);
+	frame_watch_group_remove(dev->wdev_id, FRAME_GROUP_P2P_LISTEN);
 }
 
 static void p2p_device_enable_cb(struct l_genl_msg *msg, void *user_data)
diff -pruN 1.30-1/src/rrm.c 2.3-1/src/rrm.c
--- 1.30-1/src/rrm.c	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/src/rrm.c	2022-11-18 12:31:49.000000000 +0000
@@ -194,7 +194,7 @@ static bool rrm_send_response(struct rrm
 	iov.iov_base = (void *)frame;
 	iov.iov_len = len;
 
-	msg = nl80211_build_cmd_frame(rrm->ifindex, own_addr, bss->addr,
+	msg = nl80211_build_cmd_frame(rrm->ifindex, 0x00d0, own_addr, bss->addr,
 					bss->frequency, &iov, 1);
 
 	if (!l_genl_family_send(nl80211, msg, rrm_send_response_cb,
@@ -443,6 +443,9 @@ static void rrm_handle_beacon_scan(struc
 	freq = band_channel_to_freq(beacon->channel, band);
 	scan_freq_set_add(freqs, freq);
 
+	if (!wiphy_constrain_freq_set(wiphy_find_by_wdev(rrm->wdev_id), freqs))
+		goto free_freqs;
+
 	if (passive)
 		beacon->scan_id = scan_passive_full(rrm->wdev_id, &params,
 						rrm_scan_triggered,
@@ -454,12 +457,13 @@ static void rrm_handle_beacon_scan(struc
 						rrm_scan_results, rrm,
 						NULL);
 
+free_freqs:
 	scan_freq_set_free(freqs);
 
-	if (beacon->scan_id == 0) {
-		rrm_info_destroy(&beacon->info);
-		rrm->pending = NULL;
-	}
+	if (beacon->scan_id)
+		return;
+
+	rrm_reject_measurement_request(rrm, REPORT_REJECT_INCAPABLE);
 }
 
 static bool rrm_verify_beacon_request(const uint8_t *request, size_t len)
diff -pruN 1.30-1/src/sae.c 2.3-1/src/sae.c
--- 1.30-1/src/sae.c	2022-02-24 20:54:25.000000000 +0000
+++ 2.3-1/src/sae.c	2022-11-18 12:31:49.000000000 +0000
@@ -37,6 +37,9 @@
 #include "src/mpdu.h"
 #include "src/auth-proto.h"
 #include "src/sae.h"
+#include "src/module.h"
+
+static bool debug;
 
 /* SHA-512 is the highest supported hashing function as of 802.11-2020 */
 #define SAE_MAX_HASH_LEN 64
@@ -45,6 +48,12 @@
 #define SAE_SYNC_MAX		3
 #define SAE_MAX_ASSOC_RETRY	3
 
+#define sae_debug(fmat, ...) \
+({	\
+	if (debug) \
+		l_info("[SAE]: "fmat, ##__VA_ARGS__); \
+})
+
 enum sae_state {
 	SAE_STATE_NOTHING = 0,
 	SAE_STATE_COMMITTED = 1,
@@ -154,7 +163,7 @@ static int sae_choose_next_group(struct
 			return -ENOENT;
 		}
 
-		l_debug("Forcing default SAE group 19");
+		sae_debug("Forcing default SAE group 19");
 
 		sm->group_retry++;
 		sm->group = 19;
@@ -176,6 +185,8 @@ static int sae_choose_next_group(struct
 	sm->group = ecc_groups[sm->group_retry];
 
 get_curve:
+	sae_debug("Using group %u", sm->group);
+
 	sm->curve = l_ecc_curve_from_ike_group(sm->group);
 
 	return 0;
@@ -327,6 +338,9 @@ static int sae_reject(struct sae_sm *sm,
 		ptr += 2;
 	}
 
+	sae_debug("Rejecting exchange transaction=%u status=%u",
+			transaction, status);
+
 	sm->tx_auth(reject, ptr - reject, sm->user_data);
 
 	return status;
@@ -631,6 +645,9 @@ static bool sae_send_confirm(struct sae_
 	memcpy(ptr, confirm, r);
 	ptr += r;
 
+	sae_debug("Sending Confirm to "MAC" sc=%u",
+			MAC_STR(sm->handshake->aa), sm->sc);
+
 	sm->tx_auth(body, ptr - body, sm->user_data);
 	return true;
 }
@@ -827,6 +844,8 @@ static int sae_process_confirm(struct sa
 
 	sm->state = SAE_STATE_ACCEPTED;
 
+	sae_debug("Sending Associate to "MAC, MAC_STR(sm->handshake->aa));
+
 	sm->tx_assoc(sm->user_data);
 
 	return 0;
@@ -844,6 +863,8 @@ static bool sae_send_commit(struct sae_s
 	if (r < 0)
 		return false;
 
+	sae_debug("Sending Commit to "MAC, MAC_STR(hs->aa));
+
 	sm->tx_auth(commit, r, sm->user_data);
 
 	return true;
@@ -858,6 +879,8 @@ static bool sae_assoc_timeout(struct aut
 
 	sm->assoc_retry++;
 
+	sae_debug("Retry Associate to "MAC, MAC_STR(sm->handshake->aa));
+
 	sm->tx_assoc(sm->user_data);
 
 	return true;
@@ -921,6 +944,8 @@ static int sae_process_anti_clogging(str
 		return -EBADMSG;
 	}
 
+	sae_debug("Processed anti-clogging token");
+
 	l_free(sm->token);
 	sm->token = l_memdup(ptr, len);
 	sm->token_len = len;
@@ -1036,6 +1061,9 @@ static int sae_verify_committed(struct s
 			goto reject_unsupp_group;
 		}
 
+		sae_debug("AP rejected group, trying again with group %u",
+				sm->group);
+
 		sm->sync = 0;
 		sae_send_commit(sm, false);
 
@@ -1297,8 +1325,6 @@ static int sae_verify_packet(struct sae_
 				uint16_t status, const uint8_t *frame,
 				size_t len)
 {
-	l_debug("rx trans=%u, state=%s", trans, sae_state_to_str(sm->state));
-
 	if (trans != SAE_STATE_COMMITTED && trans != SAE_STATE_CONFIRMED)
 		return -EBADMSG;
 
@@ -1324,16 +1350,23 @@ static int sae_rx_authenticate(struct au
 	const struct mmpdu_header *hdr = (const struct mmpdu_header *) frame;
 	const struct mmpdu_authentication *auth = mmpdu_body(hdr);
 	int ret;
+	uint16_t transaction = L_LE16_TO_CPU(auth->transaction_sequence);
+	uint16_t status = L_LE16_TO_CPU(auth->status);
+
+	sae_debug("Received frame transaction=%u status=%u state=%s",
+			transaction, status, sae_state_to_str(sm->state));
 
 	len -= mmpdu_header_len(hdr);
 
-	ret = sae_verify_packet(sm, L_LE16_TO_CPU(auth->transaction_sequence),
-					L_LE16_TO_CPU(auth->status),
-					auth->ies, len - 6);
-	if (ret != 0)
+	ret = sae_verify_packet(sm, transaction, status, auth->ies, len - 6);
+	if (ret != 0) {
+		if (ret < 0 && ret != -EAGAIN)
+			sae_debug("Frame did not verify (%s)", strerror(-ret));
+
 		return ret;
+	}
 
-	switch (L_LE16_TO_CPU(auth->transaction_sequence)) {
+	switch (transaction) {
 	case SAE_STATE_COMMITTED:
 		return sae_process_commit(sm, hdr->address_2, auth->ies,
 						len - 6);
@@ -1341,8 +1374,7 @@ static int sae_rx_authenticate(struct au
 		return sae_process_confirm(sm, hdr->address_2, auth->ies,
 						len - 6);
 	default:
-		l_error("invalid transaction sequence %u",
-				L_LE16_TO_CPU(auth->transaction_sequence));
+		l_error("invalid transaction sequence %u", transaction);
 	}
 
 	/* should never get here */
@@ -1442,12 +1474,26 @@ struct auth_proto *sae_sm_new(struct han
 						hs->authenticator_rsnxe;
 
 	if (ie_rsnxe_capable(rsnxe, IE_RSNX_SAE_H2E) && hs->ecc_sae_pts) {
-		l_debug("Using SAE H2E");
+		sae_debug("Using SAE H2E");
 		sm->sae_type = CRYPTO_SAE_HASH_TO_ELEMENT;
 	} else {
-		l_debug("Using SAE Hunting and Pecking");
+		sae_debug("Using SAE Hunting and Pecking");
 		sm->sae_type = CRYPTO_SAE_LOOPING;
 	}
 
 	return &sm->ap;
 }
+
+static int sae_init(void)
+{
+	if (getenv("IWD_SAE_DEBUG"))
+		debug = true;
+
+	return 0;
+}
+
+static void sae_exit(void)
+{
+}
+
+IWD_MODULE(sae, sae_init, sae_exit);
diff -pruN 1.30-1/src/scan.c 2.3-1/src/scan.c
--- 1.30-1/src/scan.c	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/src/scan.c	2022-12-18 19:59:36.000000000 +0000
@@ -54,6 +54,7 @@
 
 /* User configurable options */
 static double RANK_5G_FACTOR;
+static double RANK_6G_FACTOR;
 static uint32_t SCAN_MAX_INTERVAL;
 static uint32_t SCAN_INIT_INTERVAL;
 
@@ -82,7 +83,6 @@ struct scan_request {
 	bool canceled : 1; /* Is scan_cancel being called on this request? */
 	bool passive:1; /* Active or Passive scan? */
 	bool started : 1; /* Has TRIGGER_SCAN succeeded at least once? */
-	bool periodic : 1; /* Started as a periodic scan? */
 	/*
 	 * Set to true if the TRIGGER_SCAN command at the head of the 'cmds'
 	 * queue was acked by the kernel indicating that the scan request was
@@ -425,7 +425,7 @@ static struct l_genl_msg *scan_build_cmd
 		 * rates we don't want to advertise support for 802.11b rates.
 		 */
 		if (L_WARN_ON(!(supported = wiphy_get_supported_rates(sc->wiphy,
-							NL80211_BAND_2GHZ,
+							BAND_FREQ_2_4_GHZ,
 							&num_supported))))
 			goto done;
 
@@ -996,7 +996,6 @@ static void scan_periodic_destroy(void *
 static bool scan_periodic_queue(struct scan_context *sc)
 {
 	struct scan_parameters params = {};
-	struct scan_request *sr;
 
 	if (sc->sp.needs_active_scan && known_networks_has_hidden()) {
 		params.randomize_mac_addr_hint = true;
@@ -1014,13 +1013,7 @@ static bool scan_periodic_queue(struct s
 					scan_periodic_notify, sc,
 					scan_periodic_destroy);
 
-	if (!sc->sp.id)
-		return false;
-
-	sr = l_queue_peek_tail(sc->requests);
-	sr->periodic = true;
-
-	return true;
+	return sc->sp.id != 0;
 }
 
 static bool scan_periodic_is_disabled(void)
@@ -1645,10 +1638,14 @@ static void scan_bss_compute_rank(struct
 
 	rank = (double)bss->data_rate / max_rate * USHRT_MAX;
 
-	/* Prefer 5G/6G networks over 2.4G */
-	if (bss->frequency > 4000)
+	/* Prefer 5G networks over 2.4G and 6G */
+	if (bss->frequency >= 4900 && bss->frequency < 5900)
 		rank *= RANK_5G_FACTOR;
 
+	/* Prefer 6G networks over 2.4G and 5G */
+	if (bss->frequency >= 5900 && bss->frequency < 7200)
+		rank *= RANK_6G_FACTOR;
+
 	/* Rank loaded APs lower and lightly loaded APs higher */
 	if (bss->utilization >= 192)
 		rank *= RANK_HIGH_UTILIZATION_FACTOR;
@@ -1928,12 +1925,9 @@ static void scan_wiphy_watch(struct wiph
 	struct scan_freq_set *freqs_6ghz;
 	struct scan_freq_set *allowed;
 	bool allow_6g;
-	const struct scan_freq_set *supported =
-					wiphy_get_supported_freqs(wiphy);
 
-	/* Only care about regulatory events, and if 6GHz capable */
-	if (event != WIPHY_STATE_WATCH_EVENT_REGDOM_DONE ||
-			!(scan_freq_set_get_bands(supported) & BAND_FREQ_6_GHZ))
+	/* Only care about completed regulatory dumps */
+	if (event != WIPHY_STATE_WATCH_EVENT_REGDOM_DONE)
 		return;
 
 	if (!sc->sp.id)
@@ -2039,6 +2033,33 @@ static void scan_parse_result_frequencie
 	}
 }
 
+static void scan_retry_pending(uint32_t wiphy_id)
+{
+	const struct l_queue_entry *entry;
+
+	l_debug("");
+
+	for (entry = l_queue_get_entries(scan_contexts); entry;
+						entry = entry->next) {
+		struct scan_context *sc = entry->data;
+		struct scan_request *sr = l_queue_peek_head(sc->requests);
+
+		if (wiphy_get_id(sc->wiphy) != wiphy_id)
+			continue;
+
+		if (!sr)
+			continue;
+
+		if (!wiphy_radio_work_is_running(sc->wiphy, sr->work.id))
+			continue;
+
+		sc->state = SCAN_STATE_NOT_RUNNING;
+		start_next_scan_request(&sr->work);
+
+		return;
+	}
+}
+
 static void scan_notify(struct l_genl_msg *msg, void *user_data)
 {
 	struct l_genl_attr attr;
@@ -2060,8 +2081,17 @@ static void scan_notify(struct l_genl_ms
 		return;
 
 	sc = l_queue_find(scan_contexts, scan_context_match, &wdev_id);
-	if (!sc)
+	if (!sc) {
+		/*
+		 * If the event is for an unmanaged device, retry pending scan
+		 * requests on the same wiphy.
+		 */
+		if (cmd == NL80211_CMD_NEW_SCAN_RESULTS ||
+		    cmd == NL80211_CMD_SCAN_ABORTED)
+			scan_retry_pending(wiphy_id);
+
 		return;
+	}
 
 	l_debug("Scan notification %s(%u)", nl80211cmd_to_string(cmd), cmd);
 
@@ -2201,13 +2231,7 @@ static void scan_notify(struct l_genl_ms
 
 		if (sr->triggered) {
 			sr->triggered = false;
-
-			/* If periodic scan, don't report the abort */
-			if (sr->periodic) {
-				l_queue_remove(sc->requests, sr);
-				wiphy_radio_work_done(sc->wiphy, sr->work.id);
-			} else
-				scan_finished(sc, -ECANCELED, NULL, NULL, sr);
+			scan_finished(sc, -ECANCELED, NULL, NULL, sr);
 		} else if (wiphy_radio_work_is_running(sc->wiphy,
 							sr->work.id)) {
 			/*
@@ -2343,6 +2367,10 @@ static int scan_init(void)
 					&RANK_5G_FACTOR))
 		RANK_5G_FACTOR = 1.0;
 
+	if (!l_settings_get_double(config, "Rank", "BandModifier6Ghz",
+					&RANK_6G_FACTOR))
+		RANK_6G_FACTOR = 1.0;
+
 	if (!l_settings_get_uint(config, "Scan", "InitialPeriodicScanInterval",
 					&SCAN_INIT_INTERVAL))
 		SCAN_INIT_INTERVAL = 10;
diff -pruN 1.30-1/src/station.c 2.3-1/src/station.c
--- 1.30-1/src/station.c	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/src/station.c	2023-01-23 18:46:38.000000000 +0000
@@ -59,6 +59,10 @@
 #include "src/frame-xchg.h"
 #include "src/sysfs.h"
 #include "src/band.h"
+#include "src/ft.h"
+#include "src/eap.h"
+#include "src/eap-tls-common.h"
+#include "src/storage.h"
 
 static struct l_queue *station_list;
 static uint32_t netdev_watch;
@@ -68,6 +72,7 @@ static bool anqp_disabled;
 static bool supports_arp_evict_nocarrier;
 static bool supports_ndisc_evict_nocarrier;
 static struct watchlist event_watches;
+static uint32_t known_networks_watch;
 
 struct station {
 	enum station_state state;
@@ -107,6 +112,7 @@ struct station {
 
 	/* Set of frequencies to scan first when attempting a roam */
 	struct scan_freq_set *roam_freqs;
+	struct l_queue *roam_bss_list;
 
 	/* Frequencies split into subsets by priority */
 	struct scan_freq_set *scan_freqs_order[3];
@@ -114,6 +120,8 @@ struct station {
 
 	uint32_t wiphy_watch;
 
+	struct wiphy_radio_work_item ft_work;
+
 	bool preparing_roam : 1;
 	bool roam_scan_full : 1;
 	bool signal_low : 1;
@@ -129,6 +137,39 @@ struct anqp_entry {
 	uint32_t pending;
 };
 
+/*
+ * Used as entries for the roam list since holding scan_bss pointers directly
+ * from station->bss_list is not 100% safe due to the possibility of the
+ * hardware scanning and overwriting station->bss_list.
+ */
+struct roam_bss {
+	uint8_t addr[6];
+	uint16_t rank;
+	int32_t signal_strength;
+};
+
+static struct roam_bss *roam_bss_from_scan_bss(const struct scan_bss *bss)
+{
+	struct roam_bss *rbss = l_new(struct roam_bss, 1);
+
+	memcpy(rbss->addr, bss->addr, 6);
+	rbss->rank = bss->rank;
+	rbss->signal_strength = bss->signal_strength;
+
+	return rbss;
+}
+
+static int roam_bss_rank_compare(const void *a, const void *b, void *user_data)
+{
+	const struct roam_bss *new_bss = a, *bss = b;
+
+	if (bss->rank == new_bss->rank)
+		return (bss->signal_strength >
+					new_bss->signal_strength) ? 1 : -1;
+
+	return (bss->rank > new_bss->rank) ? 1 : -1;
+}
+
 struct wiphy *station_get_wiphy(struct station *station)
 {
 	return station->wiphy;
@@ -146,12 +187,9 @@ struct network *station_get_connected_ne
 
 bool station_is_busy(struct station *station)
 {
-	if (station->state != STATION_STATE_DISCONNECTED &&
+	return station->state != STATION_STATE_DISCONNECTED &&
 			station->state != STATION_STATE_AUTOCONNECT_FULL &&
-			station->state != STATION_STATE_AUTOCONNECT_QUICK)
-		return true;
-
-	return false;
+			station->state != STATION_STATE_AUTOCONNECT_QUICK;
 }
 
 static bool station_is_autoconnecting(struct station *station)
@@ -160,6 +198,13 @@ static bool station_is_autoconnecting(st
 			station->state == STATION_STATE_AUTOCONNECT_QUICK;
 }
 
+static bool station_is_roaming(struct station *station)
+{
+	return station->state == STATION_STATE_ROAMING ||
+			station->state == STATION_STATE_FT_ROAMING ||
+			station->state == STATION_STATE_FW_ROAMING;
+}
+
 static bool station_debug_event(struct station *station, const char *name)
 {
 	struct l_dbus_message *signal;
@@ -970,6 +1015,7 @@ static void station_handshake_event(stru
 	case HANDSHAKE_EVENT_SETTING_KEYS_FAILED:
 	case HANDSHAKE_EVENT_EAP_NOTIFY:
 	case HANDSHAKE_EVENT_P2P_IP_REQUEST:
+	case HANDSHAKE_EVENT_REKEY_COMPLETE:
 		/*
 		 * currently we don't care about any other events. The
 		 * netdev_connect_cb will notify us when the connection is
@@ -1110,9 +1156,15 @@ build_ie:
 	 * seen that they do not include the OCI in the 4-way handshake yet
 	 * still advertise the capability. Because of this OCV is disabled if
 	 * any offload features are detected (since IWD prefers to use offload).
+	 *
+	 * TODO: For now OCV is disabled if the network is FT capable. This is
+	 *       being done until support in the kernel is added to
+	 *       automatically include the OCI element for the association
+	 *       request.
 	 */
 	info.ocvc = !disable_ocv && bss_info.ocvc && info.mfpc &&
-			!wiphy_can_offload(wiphy);
+			!wiphy_can_offload(wiphy) &&
+			!IE_AKM_IS_FT(info.akm_suites);
 
 	/*
 	 * IEEE 802.11-2020 9.4.2.24.4 states extended key IDs can only be used
@@ -1120,7 +1172,7 @@ build_ie:
 	 * also indicates support.
 	 */
 	if (wiphy_supports_ext_key_id(wiphy) && bss_info.extended_key_id &&
-			info.pairwise_ciphers == IE_RSN_CIPHER_SUITE_CCMP)
+			IE_CIPHER_IS_GCMP_CCMP(info.pairwise_ciphers))
 		info.extended_key_id = true;
 
 	/* RSN takes priority */
@@ -1356,8 +1408,6 @@ static int station_quick_scan_trigger(st
 {
 	_auto_(scan_freq_set_free) struct scan_freq_set *known_freq_set = NULL;
 	bool known_6ghz;
-	const struct scan_freq_set *disabled = wiphy_get_disabled_freqs(
-								station->wiphy);
 
 	if (wiphy_regdom_is_updating(station->wiphy)) {
 		l_debug("regdom is updating, delaying quick scan");
@@ -1379,9 +1429,11 @@ static int station_quick_scan_trigger(st
 	 * this since its so limited, so return an error which will fall back to
 	 * full autoconnect.
 	 */
-	if ((scan_freq_set_get_bands(disabled) & BAND_FREQ_6_GHZ) &&
-				wiphy_country_is_unknown(station->wiphy) &&
-				known_6ghz)
+	if (wiphy_get_supported_bands(station->wiphy) & BAND_FREQ_6_GHZ &&
+			wiphy_band_is_disabled(station->wiphy,
+						BAND_FREQ_6_GHZ) &&
+			wiphy_country_is_unknown(station->wiphy) &&
+			known_6ghz)
 		return -ENOTSUP;
 
 	if (!wiphy_constrain_freq_set(station->wiphy, known_freq_set)) {
@@ -1418,6 +1470,10 @@ static const char *station_state_to_stri
 		return "disconnecting";
 	case STATION_STATE_ROAMING:
 		return "roaming";
+	case STATION_STATE_FT_ROAMING:
+		return "ft-roaming";
+	case STATION_STATE_FW_ROAMING:
+		return "fw-roaming";
 	}
 
 	return "invalid";
@@ -1556,6 +1612,8 @@ static void station_enter_state(struct s
 	case STATION_STATE_DISCONNECTING:
 		break;
 	case STATION_STATE_ROAMING:
+	case STATION_STATE_FT_ROAMING:
+	case STATION_STATE_FW_ROAMING:
 		station_set_evict_nocarrier(station, false);
 		break;
 	}
@@ -1634,6 +1692,10 @@ static void station_roam_state_clear(str
 		scan_freq_set_free(station->roam_freqs);
 		station->roam_freqs = NULL;
 	}
+
+	l_queue_clear(station->roam_bss_list, l_free);
+
+	ft_clear_authentications(netdev_get_ifindex(station->netdev));
 }
 
 static void station_reset_connection_state(struct station *station)
@@ -1675,7 +1737,7 @@ static void station_reset_connection_sta
 	if (station->state == STATION_STATE_CONNECTED ||
 			station->state == STATION_STATE_CONNECTING ||
 			station->state == STATION_STATE_CONNECTING_AUTO ||
-			station->state == STATION_STATE_ROAMING)
+			station_is_roaming(station))
 		network_disconnected(network);
 }
 
@@ -1751,8 +1813,6 @@ static void parse_neighbor_report(struct
 	struct scan_freq_set *freq_set_md, *freq_set_no_md;
 	uint32_t current_freq = 0;
 	struct handshake_state *hs = netdev_get_handshake(station->netdev);
-	const struct scan_freq_set *supported =
-				wiphy_get_supported_freqs(station->wiphy);
 
 	freq_set_md = scan_freq_set_new();
 	freq_set_no_md = scan_freq_set_new();
@@ -1765,6 +1825,7 @@ static void parse_neighbor_report(struct
 		uint32_t freq;
 		enum band_freq band;
 		const uint8_t *cc = NULL;
+		const struct band_freq_attrs *attr;
 
 		if (ie_tlv_iter_get_tag(&iter) != IE_TYPE_NEIGHBOR_REPORT)
 			continue;
@@ -1789,8 +1850,9 @@ static void parse_neighbor_report(struct
 		if (!(band & wiphy_get_supported_bands(station->wiphy)))
 			continue;
 
-		/* Skip if frequency is not supported */
-		if (!scan_freq_set_contains(supported, freq))
+		/* Skip if frequency is not supported or disabled */
+		attr = wiphy_get_frequency_info(station->wiphy, freq);
+		if (!attr || attr->disabled)
 			continue;
 
 		if (!memcmp(info.addr,
@@ -1909,46 +1971,6 @@ static bool station_can_fast_transition(
 	return true;
 }
 
-static void station_ft_ds_action_start(struct station *station)
-{
-	struct handshake_state *hs = netdev_get_handshake(station->netdev);
-	uint16_t mdid;
-	const struct l_queue_entry *entry;
-	struct scan_bss *bss;
-	struct ie_rsn_info rsn_info;
-
-	if (!station_can_fast_transition(hs, station->connected_bss) ||
-						!(hs->mde[4] & 1))
-		return;
-
-	if (ie_parse_mobility_domain_from_data(hs->mde, hs->mde[1] + 2,
-						&mdid, NULL, NULL) < 0)
-		return;
-
-	for (entry = network_bss_list_get_entries(station->connected_network);
-						entry; entry = entry->next) {
-		bss = entry->data;
-
-		if (bss == station->connected_bss)
-			continue;
-
-		if (mdid != l_get_le16(bss->mde))
-			continue;
-
-		if (scan_bss_get_rsn_info(bss, &rsn_info) < 0)
-			continue;
-
-		if (!IE_AKM_IS_FT(rsn_info.akm_suites))
-			continue;
-
-		/*
-		 * Fire and forget. Netdev will maintain a cache of responses
-		 * and when the time comes these can be referenced for a roam
-		 */
-		netdev_fast_transition_over_ds_action(station->netdev, bss);
-	}
-}
-
 static void station_roamed(struct station *station)
 {
 	station->roam_scan_full = false;
@@ -1975,7 +1997,7 @@ static void station_roamed(struct statio
 			l_warn("Could not request neighbor report");
 	}
 
-	station_ft_ds_action_start(station);
+	l_queue_clear(station->roam_bss_list, l_free);
 
 	station_enter_state(station, STATION_STATE_CONNECTED);
 }
@@ -1999,11 +2021,13 @@ static void station_roam_failed(struct s
 {
 	l_debug("%u", netdev_get_ifindex(station->netdev));
 
+	l_queue_clear(station->roam_bss_list, l_free);
+
 	/*
 	 * If we attempted a reassociation or a fast transition, and ended up
 	 * here then we are now disconnected.
 	 */
-	if (station->state == STATION_STATE_ROAMING) {
+	if (station_is_roaming(station)) {
 		station_disassociated(station);
 		return;
 	}
@@ -2036,6 +2060,30 @@ delayed_retry:
 	station_roam_retry(station);
 }
 
+static void station_disconnect_on_error_cb(struct netdev *netdev, bool success,
+					void *user_data)
+{
+	struct station *station = user_data;
+	bool continue_autoconnect;
+
+	station_enter_state(station, STATION_STATE_DISCONNECTED);
+
+	continue_autoconnect = station->state == STATION_STATE_CONNECTING_AUTO;
+
+	if (continue_autoconnect) {
+		if (station_autoconnect_next(station) < 0) {
+			l_debug("Nothing left on autoconnect list");
+			station_enter_state(station,
+					STATION_STATE_AUTOCONNECT_FULL);
+		}
+
+		return;
+	}
+
+	if (station->autoconnect)
+		station_enter_state(station, STATION_STATE_AUTOCONNECT_QUICK);
+}
+
 static void station_netconfig_event_handler(enum netconfig_event event,
 							void *user_data)
 {
@@ -2044,7 +2092,26 @@ static void station_netconfig_event_hand
 	switch (event) {
 	case NETCONFIG_EVENT_CONNECTED:
 		station_enter_state(station, STATION_STATE_CONNECTED);
+		break;
+	case NETCONFIG_EVENT_FAILED:
+		if (station->connect_pending) {
+			struct l_dbus_message *reply = dbus_error_failed(
+						station->connect_pending);
+
+			dbus_pending_reply(&station->connect_pending, reply);
+		}
+
+		if (L_IN_SET(station->state, STATION_STATE_CONNECTING,
+				STATION_STATE_CONNECTING_AUTO))
+			network_connect_failed(station->connected_network,
+						false);
 
+		netdev_disconnect(station->netdev,
+					station_disconnect_on_error_cb,
+					station);
+		station_reset_connection_state(station);
+
+		station_enter_state(station, STATION_STATE_DISCONNECTING);
 		break;
 	default:
 		l_error("station: Unsupported netconfig event: %d.", event);
@@ -2070,42 +2137,26 @@ static void station_reassociate_cb(struc
 		station_roam_failed(station);
 }
 
-static void station_fast_transition_cb(struct netdev *netdev,
-					enum netdev_result result,
-					void *event_data,
-					void *user_data)
-{
-	struct station *station = user_data;
-
-	l_debug("%u, result: %d", netdev_get_ifindex(station->netdev), result);
-
-	if (station->state != STATION_STATE_ROAMING)
-		return;
-
-	if (result == NETDEV_RESULT_OK)
-		station_roamed(station);
-	else
-		station_roam_failed(station);
-}
-
 static void station_netdev_event(struct netdev *netdev, enum netdev_event event,
 					void *event_data, void *user_data);
 
-static void station_transition_reassociate(struct station *station,
+static int station_transition_reassociate(struct station *station,
 						struct scan_bss *bss,
 						struct handshake_state *new_hs)
 {
-	if (netdev_reassociate(station->netdev, bss, station->connected_bss,
+	int ret;
+
+	ret = netdev_reassociate(station->netdev, bss, station->connected_bss,
 				new_hs, station_netdev_event,
-				station_reassociate_cb, station) < 0) {
-		handshake_state_free(new_hs);
-		station_roam_failed(station);
-		return;
-	}
+				station_reassociate_cb, station);
+	if (ret < 0)
+		return ret;
 
 	station->connected_bss = bss;
 	station->preparing_roam = false;
 	station_enter_state(station, STATION_STATE_ROAMING);
+
+	return 0;
 }
 
 static bool bss_match_bssid(const void *a, const void *b)
@@ -2130,8 +2181,8 @@ static void station_preauthenticate_cb(s
 	if (!station->preparing_roam || result == NETDEV_RESULT_ABORTED)
 		return;
 
-	bss = l_queue_find(station->bss_list, bss_match_bssid,
-				station->preauth_bssid);
+	bss = network_bss_find_by_addr(station->connected_network,
+						station->preauth_bssid);
 	if (!bss) {
 		l_error("Roam target BSS not found");
 		station_roam_failed(station);
@@ -2177,18 +2228,96 @@ static void station_preauthenticate_cb(s
 		handshake_state_set_supplicant_ie(new_hs, rsne_buf);
 	}
 
-	station_transition_reassociate(station, bss, new_hs);
+	if (station_transition_reassociate(station, bss, new_hs) < 0) {
+		handshake_state_free(new_hs);
+		station_roam_failed(station);
+	}
 }
 
-static void station_transition_start(struct station *station,
-							struct scan_bss *bss)
+static void station_transition_start(struct station *station);
+
+static bool station_ft_work_ready(struct wiphy_radio_work_item *item)
+{
+	struct station *station = l_container_of(item, struct station, ft_work);
+	struct roam_bss *rbss = l_queue_pop_head(station->roam_bss_list);
+	struct scan_bss *bss = network_bss_find_by_addr(
+					station->connected_network, rbss->addr);
+	int ret;
+
+	l_free(rbss);
+
+	/* Very unlikely, but the BSS could have gone away */
+	if (!bss)
+		goto try_next;
+
+	ret = ft_associate(netdev_get_ifindex(station->netdev), bss->addr);
+	if (ret == -ENOENT) {
+		station_debug_event(station, "ft-roam-failed");
+try_next:
+		station_transition_start(station);
+		return true;
+	} else if (ret < 0)
+		goto assoc_failed;
+
+	station->connected_bss = bss;
+	station->preparing_roam = false;
+	station_enter_state(station, STATION_STATE_FT_ROAMING);
+
+	return true;
+
+assoc_failed:
+	station_roam_failed(station);
+	return true;
+}
+
+static const struct wiphy_radio_work_item_ops ft_work_ops = {
+	.do_work = station_ft_work_ready,
+};
+
+static bool station_fast_transition(struct station *station,
+					struct scan_bss *bss)
+{
+	struct handshake_state *hs = netdev_get_handshake(station->netdev);
+	struct network *connected = station->connected_network;
+	const struct network_info *info = network_get_info(connected);
+	const struct iovec *vendor_ies;
+	size_t iov_elems = 0;
+
+	/* Rebuild handshake RSN for target AP */
+	if (station_build_handshake_rsn(hs, station->wiphy,
+				station->connected_network, bss) < 0)
+		return false;
+
+	/* Reset the vendor_ies in case they're different */
+	vendor_ies = network_info_get_extra_ies(info, bss, &iov_elems);
+	handshake_state_set_vendor_ies(hs, vendor_ies, iov_elems);
+
+	if (station->roam_trigger_timeout) {
+		l_timeout_remove(station->roam_trigger_timeout);
+		station->roam_trigger_timeout = NULL;
+	}
+
+	/* Both ft_action/ft_authenticate will gate the associate work item */
+	if ((hs->mde[4] & 1))
+		ft_action(netdev_get_ifindex(station->netdev),
+				station->connected_bss->frequency, bss);
+	else
+		ft_authenticate(netdev_get_ifindex(station->netdev), bss);
+
+	wiphy_radio_work_insert(station->wiphy, &station->ft_work,
+				WIPHY_WORK_PRIORITY_CONNECT, &ft_work_ops);
+
+	return true;
+}
+
+static bool station_try_next_transition(struct station *station,
+					struct scan_bss *bss)
 {
 	struct handshake_state *hs = netdev_get_handshake(station->netdev);
 	struct network *connected = station->connected_network;
 	enum security security = network_get_security(connected);
 	struct handshake_state *new_hs;
 	struct ie_rsn_info cur_rsne, target_rsne;
-	int ret;
 
 	l_debug("%u, target %s", netdev_get_ifindex(station->netdev),
 			util_address_to_string(bss->addr));
@@ -2197,54 +2326,8 @@ static void station_transition_start(str
 	station->ap_directed_roaming = false;
 
 	/* Can we use Fast Transition? */
-	if (station_can_fast_transition(hs, bss)) {
-		const struct network_info *info = network_get_info(connected);
-		const struct iovec *vendor_ies;
-		size_t iov_elems = 0;
-
-		/* Rebuild handshake RSN for target AP */
-		if (station_build_handshake_rsn(hs, station->wiphy,
-				station->connected_network, bss) < 0) {
-			l_error("rebuilding handshake rsne failed");
-			station_roam_failed(station);
-			return;
-		}
-
-		/* Reset the vendor_ies in case they're different */
-		vendor_ies = network_info_get_extra_ies(info, bss, &iov_elems);
-		handshake_state_set_vendor_ies(hs, vendor_ies, iov_elems);
-
-		if ((hs->mde[4] & 1)) {
-			ret = netdev_fast_transition_over_ds(station->netdev,
-					bss, station->connected_bss,
-					station_fast_transition_cb);
-			/* No action responses from this BSS, try over air */
-			if (ret == -ENOENT)
-				goto try_over_air;
-			else if (ret < 0) {
-				/*
-				 * If we are here FT-over-air will not work
-				 * either (identical checks) so try again later.
-				 */
-				station_roam_retry(station);
-				return;
-			}
-		} else {
-try_over_air:
-			if (netdev_fast_transition(station->netdev, bss,
-					station->connected_bss,
-					station_fast_transition_cb) < 0) {
-				station_roam_failed(station);
-				return;
-			}
-		}
-
-		station->connected_bss = bss;
-		station->preparing_roam = false;
-		station_enter_state(station, STATION_STATE_ROAMING);
-
-		return;
-	}
+	if (station_can_fast_transition(hs, bss))
+		return station_fast_transition(station, bss);
 
 	/* Non-FT transition */
 
@@ -2274,17 +2357,46 @@ try_over_air:
 		if (netdev_preauthenticate(station->netdev, bss,
 						station_preauthenticate_cb,
 						station) >= 0)
-			return;
+			return true;
 	}
 
 	new_hs = station_handshake_setup(station, connected, bss);
 	if (!new_hs) {
 		l_error("station_handshake_setup failed in reassociation");
-		station_roam_failed(station);
-		return;
+		return false;
+	}
+
+	if (station_transition_reassociate(station, bss, new_hs) < 0) {
+		handshake_state_free(new_hs);
+		return false;
 	}
 
-	station_transition_reassociate(station, bss, new_hs);
+	return true;
+}
+
+static void station_transition_start(struct station *station)
+{
+	struct roam_bss *rbss;
+	bool roaming = false;
+
+	/*
+	 * For each failed attempt pop the BSS leaving the head of the queue
+	 * with the current roam candidate.
+	 */
+	while ((rbss = l_queue_peek_head(station->roam_bss_list))) {
+		struct scan_bss *bss = network_bss_find_by_addr(
+					station->connected_network, rbss->addr);
+
+		roaming = station_try_next_transition(station, bss);
+		if (roaming)
+			break;
+
+		l_queue_pop_head(station->roam_bss_list);
+		l_free(rbss);
+	}
+
+	if (!roaming)
+		station_roam_failed(station);
 }
 
 static void station_roam_scan_triggered(int err, void *user_data)
@@ -2304,6 +2416,20 @@ static void station_roam_scan_triggered(
 	 */
 }
 
+static void station_update_roam_bss(struct station *station,
+					struct scan_bss *bss)
+{
+	struct network *network = station->connected_network;
+	struct scan_bss *old =
+		l_queue_remove_if(station->bss_list, bss_match, bss);
+
+	network_bss_update(network, bss);
+	l_queue_push_tail(station->bss_list, bss);
+
+	if (old)
+		scan_bss_free(old);
+}
+
 static bool station_roam_scan_notify(int err, struct l_queue *bss_list,
 					const struct scan_freq_set *freqs,
 					void *userdata)
@@ -2313,12 +2439,10 @@ static bool station_roam_scan_notify(int
 	struct handshake_state *hs = netdev_get_handshake(station->netdev);
 	struct scan_bss *current_bss = station->connected_bss;
 	struct scan_bss *bss;
-	struct scan_bss *best_bss = NULL;
-	double best_bss_rank = 0.0;
+	double cur_bss_rank = 0.0;
 	static const double RANK_FT_FACTOR = 1.3;
 	uint16_t mdid;
 	enum security orig_security, security;
-	bool seen = false;
 
 	if (err) {
 		station_roam_failed(station);
@@ -2338,6 +2462,19 @@ static bool station_roam_scan_notify(int
 							&mdid, NULL, NULL);
 
 	/*
+	 * Find the current BSS rank, use the updated result if it exists. If
+	 * this is an AP roam keep the current rank as zero to force the roam
+	 * to occur.
+	 */
+	bss = l_queue_find(bss_list, bss_match_bssid, current_bss->addr);
+	if (bss && !station->ap_directed_roaming) {
+		cur_bss_rank = bss->rank;
+
+		if (hs->mde && bss->mde_present && l_get_le16(bss->mde) == mdid)
+			cur_bss_rank *= RANK_FT_FACTOR;
+	}
+
+	/*
 	 * BSSes in the bss_list come already ranked with their initial
 	 * association preference rank value.  We only need to add preference
 	 * for BSSes that are within the FT Mobility Domain so as to favor
@@ -2350,6 +2487,7 @@ static bool station_roam_scan_notify(int
 	while ((bss = l_queue_pop_head(bss_list))) {
 		double rank;
 		uint32_t kbps100 = DIV_ROUND_CLOSEST(bss->data_rate, 100000);
+		struct roam_bss *rbss;
 
 		l_debug("Processing BSS '%s' with SSID: %s, freq: %u, rank: %u,"
 				" strength: %i, data_rate: %u.%u",
@@ -2358,9 +2496,8 @@ static bool station_roam_scan_notify(int
 				bss->frequency, bss->rank, bss->signal_strength,
 				kbps100 / 10, kbps100 % 10);
 
-		/* Skip the BSS we are connected to if doing an AP roam */
-		if (station->ap_directed_roaming && !memcmp(bss->addr,
-				station->connected_bss->addr, 6))
+		/* Skip the BSS we are connected to */
+		if (!memcmp(bss->addr, station->connected_bss->addr, 6))
 			goto next;
 
 		/* Skip result if it is not part of the ESS */
@@ -2374,8 +2511,6 @@ static bool station_roam_scan_notify(int
 		if (security != orig_security)
 			goto next;
 
-		seen = true;
-
 		if (network_can_connect_bss(network, bss) < 0)
 			goto next;
 
@@ -2387,15 +2522,21 @@ static bool station_roam_scan_notify(int
 		if (hs->mde && bss->mde_present && l_get_le16(bss->mde) == mdid)
 			rank *= RANK_FT_FACTOR;
 
-		if (rank > best_bss_rank) {
-			if (best_bss)
-				scan_bss_free(best_bss);
+		if (rank <= cur_bss_rank)
+			goto next;
 
-			best_bss = bss;
-			best_bss_rank = rank;
+		/*
+		 * We need to update/add any potential roam candidate so
+		 * station/network know it exists.
+		 */
+		station_update_roam_bss(station, bss);
 
-			continue;
-		}
+		rbss = roam_bss_from_scan_bss(bss);
+
+		l_queue_insert(station->roam_bss_list, rbss,
+				roam_bss_rank_compare, NULL);
+
+		continue;
 
 next:
 		scan_bss_free(bss);
@@ -2403,36 +2544,17 @@ next:
 
 	l_queue_destroy(bss_list, NULL);
 
-	if (!seen)
-		goto fail_free_bss;
-
 	/* See if we have anywhere to roam to */
-	if (!best_bss || scan_bss_addr_eq(best_bss, station->connected_bss)) {
+	if (l_queue_isempty(station->roam_bss_list)) {
 		station_debug_event(station, "no-roam-candidates");
-		goto fail_free_bss;
+		goto fail;
 	}
 
-	bss = network_bss_find_by_addr(network, best_bss->addr);
-	if (bss) {
-		network_bss_update(network, best_bss);
-		l_queue_remove(station->bss_list, bss);
-		scan_bss_free(bss);
-
-		l_queue_insert(station->bss_list, best_bss,
-				scan_bss_rank_compare, NULL);
-	} else {
-		network_bss_add(network, best_bss);
-		l_queue_push_tail(station->bss_list, best_bss);
-	}
-
-	station_transition_start(station, best_bss);
+	station_transition_start(station);
 
 	return true;
 
-fail_free_bss:
-	if (best_bss)
-		scan_bss_free(best_bss);
-
+fail:
 	station_roam_failed(station);
 
 	return true;
@@ -2480,12 +2602,17 @@ static int station_roam_scan_known_freqs
 						station->connected_network);
 	struct scan_freq_set *freqs = network_info_get_roam_frequencies(info,
 					station->connected_bss->frequency, 5);
-	int r;
+	int r = -ENODATA;
 
 	if (!freqs)
-		return -ENODATA;
+		return r;
+
+	if (!wiphy_constrain_freq_set(station->wiphy, freqs))
+		goto free_set;
 
 	r = station_roam_scan(station, freqs);
+
+free_set:
 	scan_freq_set_free(freqs);
 	return r;
 }
@@ -2572,6 +2699,29 @@ static void station_start_roam(struct st
 		station_roam_failed(station);
 }
 
+static bool station_cannot_roam(struct station *station)
+{
+	const struct l_settings *config = iwd_get_config();
+	bool disabled;
+
+	/*
+	 * Disable roaming with hardware that can roam automatically. Note this
+	 * is now required for recent kernels which have CQM event support on
+	 * this type of hardware (e.g. brcmfmac).
+	 */
+	if (wiphy_supports_firmware_roam(station->wiphy))
+		return true;
+
+	if (!l_settings_get_bool(config, "Scan", "DisableRoamingScan",
+								&disabled))
+		disabled = false;
+
+	return disabled || station->preparing_roam ||
+				station->state == STATION_STATE_ROAMING ||
+				station->state == STATION_STATE_FT_ROAMING ||
+				station->ft_work.id;
+}
+
 static void station_roam_trigger_cb(struct l_timeout *timeout, void *user_data)
 {
 	struct station *station = user_data;
@@ -2581,6 +2731,9 @@ static void station_roam_trigger_cb(stru
 	l_timeout_remove(station->roam_trigger_timeout);
 	station->roam_trigger_timeout = NULL;
 
+	if (station_cannot_roam(station))
+		return;
+
 	station_start_roam(station);
 }
 
@@ -2606,28 +2759,8 @@ static void station_roam_timeout_rearm(s
 								station, NULL);
 }
 
-static bool station_cannot_roam(struct station *station)
-{
-	const struct l_settings *config = iwd_get_config();
-	bool disabled;
-
-	/*
-	 * Disable roaming with hardware that can roam automatically. Note this
-	 * is now required for recent kernels which have CQM event support on
-	 * this type of hardware (e.g. brcmfmac).
-	 */
-	if (wiphy_supports_firmware_roam(station->wiphy))
-		return true;
-
-	if (!l_settings_get_bool(config, "Scan", "DisableRoamingScan",
-								&disabled))
-		disabled = false;
-
-	return disabled || station->preparing_roam ||
-					station->state == STATION_STATE_ROAMING;
-}
-
 #define WNM_REQUEST_MODE_PREFERRED_CANDIDATE_LIST	(1 << 0)
+#define WNM_REQUEST_MODE_DISASSOCIATION_IMMINENT	(1 << 2)
 #define WNM_REQUEST_MODE_TERMINATION_IMMINENT		(1 << 3)
 #define WNM_REQUEST_MODE_ESS_DISASSOCIATION_IMMINENT	(1 << 4)
 
@@ -2645,6 +2778,31 @@ static void station_ap_directed_roam(str
 	if (station_cannot_roam(station))
 		return;
 
+	if (station->state != STATION_STATE_CONNECTED) {
+		l_debug("roam: unexpected AP directed roam -- ignore");
+		return;
+	}
+
+	/*
+	 * Sanitize the frame to check that it is from our current AP.
+	 *
+	 * 802.11-2020 Section 9.3.3.1 about Address2:
+	 * "If the STA is an AP with dot11MultiBSSDImplemented set to false,
+	 * then this address is the BSSID."
+	 *
+	 * Address3:
+	 * "If the STA is an AP or PCP, the Address 3 field is the same as the
+	 * Address 2 field."
+	 *
+	 * For now check that Address2 & Address3 is the same as the connected
+	 * BSS address.
+	 */
+	if (memcmp(hdr->address_2, station->connected_bss, ETH_ALEN) ||
+			memcmp(hdr->address_2, hdr->address_3, ETH_ALEN)) {
+		l_debug("roam: AP directed roam not from our AP -- ignore");
+		return;
+	}
+
 	if (body_len < 7)
 		goto format_error;
 
@@ -2675,7 +2833,20 @@ static void station_ap_directed_roam(str
 			dtimer, valid_interval,
 			MAC_STR(hdr->address_3));
 
-	/* check req_mode for optional values */
+	/*
+	 * The ap_directed_roaming flag forces IWD to roam if there are any
+	 * candidates, even if they are worse than the current BSS. This isn't
+	 * always a good idea since we may be associated to the best BSS. Where
+	 * this does matter is if the AP indicates its going down or will be
+	 * disassociating us. If either of these bits are set, set the
+	 * ap_directed_roaming flag. Otherwise still try roaming but don't
+	 * treat it any different than a normal roam.
+	 */
+	if (req_mode & (WNM_REQUEST_MODE_DISASSOCIATION_IMMINENT |
+			WNM_REQUEST_MODE_TERMINATION_IMMINENT |
+			WNM_REQUEST_MODE_ESS_DISASSOCIATION_IMMINENT))
+		station->ap_directed_roaming = true;
+
 	if (req_mode & WNM_REQUEST_MODE_TERMINATION_IMMINENT) {
 		if (pos + 12 > body_len)
 			goto format_error;
@@ -2698,37 +2869,6 @@ static void station_ap_directed_roam(str
 		pos += url_len;
 	}
 
-	if (station->state != STATION_STATE_CONNECTED) {
-		l_debug("roam: unexpected AP directed roam -- ignore");
-		return;
-	}
-
-	/*
-	 * Sanitize the frame to check that it is from our current AP.
-	 *
-	 * 802.11-2020 Section 9.3.3.1 about Address2:
-	 * "If the STA is an AP with dot11MultiBSSDImplemented set to false,
-	 * then this address is the BSSID."
-	 *
-	 * Address3:
-	 * "If the STA is an AP or PCP, the Address 3 field is the same as the
-	 * Address 2 field."
-	 *
-	 * For now check that Address2 & Address3 is the same as the connected
-	 * BSS address.
-	 */
-	if (memcmp(hdr->address_2, station->connected_bss, ETH_ALEN) ||
-			memcmp(hdr->address_2, hdr->address_3, ETH_ALEN)) {
-		l_debug("roam: AP directed roam not from our AP -- ignore");
-		return;
-	}
-
-	if (station->preparing_roam) {
-		l_debug("roam: roam attempt already in progress -- ignore");
-		return;
-	}
-
-	station->ap_directed_roaming = true;
 	station->preparing_roam = true;
 
 	l_timeout_remove(station->roam_trigger_timeout);
@@ -2780,8 +2920,7 @@ static void station_event_roamed(struct
 	network_bss_update(station->connected_network, new);
 
 	/* Remove new BSS if it exists in past scan results */
-	stale = l_queue_remove_if(station->bss_list, bss_match_bssid,
-					new->addr);
+	stale = l_queue_remove_if(station->bss_list, bss_match, new);
 	if (stale)
 		scan_bss_free(stale);
 
@@ -2924,8 +3063,6 @@ static void station_connect_ok(struct st
 			l_warn("Could not request neighbor report");
 	}
 
-	station_ft_ds_action_start(station);
-
 	network_connected(station->connected_network);
 
 	if (station->netconfig) {
@@ -3055,6 +3192,8 @@ static void station_disconnect_event(str
 					event_data, station);
 		return;
 	case STATION_STATE_CONNECTED:
+	case STATION_STATE_FT_ROAMING:
+	case STATION_STATE_FW_ROAMING:
 		station_disassociated(station);
 		return;
 	default:
@@ -3108,7 +3247,7 @@ static void station_netdev_event(struct
 			station_signal_agent_notify(station);
 		break;
 	case NETDEV_EVENT_ROAMING:
-		station_enter_state(station, STATION_STATE_ROAMING);
+		station_enter_state(station, STATION_STATE_FW_ROAMING);
 		break;
 	case NETDEV_EVENT_ROAMED:
 		station_event_roamed(station, (struct scan_bss *) event_data);
@@ -3119,6 +3258,12 @@ static void station_netdev_event(struct
 	case NETDEV_EVENT_PACKET_LOSS_NOTIFY:
 		station_packets_lost(station, l_get_u32(event_data));
 		break;
+	case NETDEV_EVENT_FT_ROAMED:
+		if (L_WARN_ON(station->state != STATION_STATE_FT_ROAMING))
+			return;
+
+		station_roamed(station);
+		break;
 	}
 }
 
@@ -3937,6 +4082,8 @@ static bool station_property_get_state(s
 		statestr = "disconnecting";
 		break;
 	case STATION_STATE_ROAMING:
+	case STATION_STATE_FT_ROAMING:
+	case STATION_STATE_FW_ROAMING:
 		statestr = "roaming";
 		break;
 	}
@@ -4146,6 +4293,8 @@ static struct station *station_create(st
 
 	station_set_autoconnect(station, autoconnect);
 
+	station->roam_bss_list = l_queue_new();
+
 	return station;
 }
 
@@ -4235,6 +4384,8 @@ static void station_free(struct station
 
 	wiphy_state_watch_remove(station->wiphy, station->wiphy_watch);
 
+	l_queue_destroy(station->roam_bss_list, l_free);
+
 	l_free(station);
 }
 
@@ -4305,6 +4456,21 @@ static void station_get_diagnostic_cb(
 				diagnostic_akm_suite_to_security(hs->akm_suite,
 								hs->wpa_ie));
 
+	if (hs->pairwise_cipher) {
+		const char *str;
+
+		if (hs->pairwise_cipher ==
+				IE_RSN_CIPHER_SUITE_USE_GROUP_CIPHER)
+			str = ie_rsn_cipher_suite_to_string(hs->group_cipher);
+		else
+			str = ie_rsn_cipher_suite_to_string(
+							hs->pairwise_cipher);
+
+		if (str)
+			dbus_append_dict_basic(builder, "PairwiseCipher",
+						's', str);
+	}
+
 	diagnostic_info_to_dict(info, builder);
 
 	l_dbus_message_builder_leave_array(builder);
@@ -4347,6 +4513,72 @@ static struct l_dbus_message *station_ge
 	return NULL;
 }
 
+struct station_roam_data {
+	struct station *station;
+	struct l_dbus_message *pending;
+	uint8_t bssid[6];
+};
+
+static void station_force_roam_scan_triggered(int err, void *user_data)
+{
+	struct station_roam_data *data = user_data;
+	struct station *station = data->station;
+
+	if (err)
+		station_roam_failed(station);
+}
+
+static bool station_force_roam_scan_notify(int err, struct l_queue *bss_list,
+					const struct scan_freq_set *freqs,
+					void *user_data)
+{
+	struct station_roam_data *data = user_data;
+	struct station *station = data->station;
+	struct scan_bss *target;
+	struct l_dbus_message *reply;
+
+	if (err) {
+		reply = dbus_error_from_errno(err, data->pending);
+		goto reply;
+	}
+
+	target = l_queue_remove_if(bss_list, bss_match_bssid, data->bssid);
+	if (!target) {
+		reply = dbus_error_not_found(data->pending);
+		goto reply;
+	}
+
+	l_debug("Attempting forced roam to BSS "MAC, MAC_STR(target->addr));
+
+	/* The various roam routines expect this to be set from scanning */
+	station->preparing_roam = true;
+	l_queue_push_tail(station->roam_bss_list,
+				roam_bss_from_scan_bss(target));
+
+	station_update_roam_bss(station, target);
+
+	station_transition_start(station);
+
+	reply = l_dbus_message_new_method_return(data->pending);
+
+reply:
+	dbus_pending_reply(&data->pending, reply);
+
+	return false;
+}
+
+static void station_force_roam_scan_destroy(void *user_data)
+{
+	struct station_roam_data *data = user_data;
+
+	data->station->roam_scan_id = 0;
+
+	if (data->pending)
+		l_dbus_message_unref(data->pending);
+
+	l_free(data);
+}
+
 static struct l_dbus_message *station_force_roam(struct l_dbus *dbus,
 						struct l_dbus_message *message,
 						void *user_data)
@@ -4356,6 +4588,9 @@ static struct l_dbus_message *station_fo
 	struct l_dbus_message_iter iter;
 	uint8_t *mac;
 	uint32_t mac_len;
+	struct scan_parameters params = { 0 };
+	struct scan_freq_set *freqs = NULL;
+	struct station_roam_data *data;
 
 	if (!l_dbus_message_get_arguments(message, "ay", &iter))
 		goto invalid_args;
@@ -4370,8 +4605,11 @@ static struct l_dbus_message *station_fo
 		return dbus_error_not_connected(message);
 
 	target = network_bss_find_by_addr(station->connected_network, mac);
-	if (!target || target == station->connected_bss)
-		return dbus_error_invalid_args(message);
+	if (!target)
+		goto full_scan;
+
+	if (target && target == station->connected_bss)
+		return dbus_error_already_exists(message);
 
 	if (station->connected_bss->ssid_len != target->ssid_len)
 		goto invalid_args;
@@ -4380,14 +4618,45 @@ static struct l_dbus_message *station_fo
 				target->ssid_len))
 		goto invalid_args;
 
-	l_debug("Attempting forced roam to BSS "MAC, MAC_STR(mac));
+	/*
+	 * Always scan before a roam to ensure the kernel has the BSS in its
+	 * cache. If we already see the BSS only scan that frequency
+	 */
+	freqs = scan_freq_set_new();
+	scan_freq_set_add(freqs, target->frequency);
 
-	/* The various roam routines expect this to be set from scanning */
-	station->preparing_roam = true;
+	params.freqs = freqs;
 
-	station_transition_start(station, target);
+full_scan:
+	params.flush = true;
 
-	return l_dbus_message_new_method_return(message);
+	data = l_new(struct station_roam_data, 1);
+	data->station = station;
+	data->pending = l_dbus_message_ref(message);
+	memcpy(data->bssid, mac, 6);
+
+	station->roam_scan_id = scan_active_full(
+					netdev_get_wdev_id(station->netdev),
+					&params,
+					station_force_roam_scan_triggered,
+					station_force_roam_scan_notify, data,
+					station_force_roam_scan_destroy);
+
+	if (freqs)
+		scan_freq_set_free(freqs);
+
+	if (!station->roam_scan_id) {
+		l_free(data);
+		return dbus_error_failed(message);
+	}
+
+	if (freqs)
+		l_debug("Scanning on %u for BSS "MAC, target->frequency,
+			MAC_STR(mac));
+	else
+		l_debug("Full scan for BSS "MAC, MAC_STR(mac));
+
+	return NULL;
 
 invalid_args:
 	return dbus_error_invalid_args(message);
@@ -4766,6 +5035,22 @@ static void station_netdev_watch(struct
 	}
 }
 
+static void station_known_networks_changed(enum known_networks_event event,
+						const struct network_info *info,
+						void *user_data)
+{
+	_auto_(l_free) char *network_id = NULL;
+
+	if (event != KNOWN_NETWORKS_EVENT_REMOVED)
+		return;
+
+	if (info->type != SECURITY_8021X)
+		return;
+
+	network_id = l_util_hexstring(info->ssid, strlen(info->ssid));
+	eap_tls_forget_peer(network_id);
+}
+
 static int station_init(void)
 {
 	station_list = l_queue_new();
@@ -4818,6 +5103,12 @@ static int station_init(void)
 
 	watchlist_init(&event_watches, NULL);
 
+	eap_tls_set_session_cache_ops(storage_eap_tls_cache_load,
+					storage_eap_tls_cache_sync);
+	known_networks_watch = known_networks_watch_add(
+						station_known_networks_changed,
+						NULL, NULL);
+
 	return 0;
 }
 
@@ -4833,9 +5124,12 @@ static void station_exit(void)
 	l_queue_destroy(station_list, NULL);
 	station_list = NULL;
 	watchlist_destroy(&event_watches);
+	known_networks_watch_remove(known_networks_watch);
+	known_networks_watch = 0;
 }
 
 IWD_MODULE(station, station_init, station_exit)
+IWD_MODULE_DEPENDS(station, known_networks)
 IWD_MODULE_DEPENDS(station, netdev);
 IWD_MODULE_DEPENDS(station, netconfig);
 IWD_MODULE_DEPENDS(station, frame_xchg);
diff -pruN 1.30-1/src/station.h 2.3-1/src/station.h
--- 1.30-1/src/station.h	2021-11-02 18:50:50.000000000 +0000
+++ 2.3-1/src/station.h	2022-11-18 12:31:49.000000000 +0000
@@ -42,7 +42,9 @@ enum station_state {
 	STATION_STATE_CONNECTING_AUTO,
 	STATION_STATE_CONNECTED,
 	STATION_STATE_DISCONNECTING,
-	STATION_STATE_ROAMING
+	STATION_STATE_ROAMING,		/* Reassociation */
+	STATION_STATE_FT_ROAMING,	/* Fast transition */
+	STATION_STATE_FW_ROAMING,	/* Firmware roamed by itself */
 };
 
 enum station_event {
diff -pruN 1.30-1/src/storage.c 2.3-1/src/storage.c
--- 1.30-1/src/storage.c	2022-07-15 17:08:06.000000000 +0000
+++ 2.3-1/src/storage.c	2022-11-18 12:31:49.000000000 +0000
@@ -53,6 +53,7 @@
 #define STORAGE_FILE_MODE (S_IRUSR | S_IWUSR)
 
 #define KNOWN_FREQ_FILENAME ".known_network.freq"
+#define EAP_TLS_CACHE_FILENAME ".eap-tls-session-cache"
 
 static char *storage_path = NULL;
 static char *storage_hotspot_path = NULL;
@@ -701,6 +702,47 @@ void storage_known_frequencies_sync(stru
 	l_free(known_freq_file_path);
 }
 
+struct l_settings *storage_eap_tls_cache_load(void)
+{
+	_auto_(l_free) char *path =
+		storage_get_path("%s", EAP_TLS_CACHE_FILENAME);
+	struct l_settings *cache = l_settings_new();
+
+	if (!l_settings_load_from_file(cache, path))
+		l_debug("No session cache loaded from %s, starting with an "
+			"empty cache", path);
+
+	return cache;
+}
+
+void storage_eap_tls_cache_sync(const struct l_settings *cache)
+{
+	_auto_(l_free) char *path =
+		storage_get_path("%s", EAP_TLS_CACHE_FILENAME);
+	_auto_(l_free) char *settings_data = NULL;
+	_auto_(l_free) char *data = NULL;
+	size_t len;
+	static const char comment[] =
+		"# External changes to this file are not tracked by IWD "
+		"and will be overwritten.\n\n";
+	static const size_t comment_len = L_ARRAY_SIZE(comment) - 1;
+
+	settings_data = l_settings_to_data(cache, &len);
+	data = l_malloc(comment_len + len);
+	memcpy(data, comment, comment_len);
+	memcpy(data + comment_len, settings_data, len);
+
+	/*
+	 * Note this data contains cryptographic secrets.  write_file()
+	 * happens to set the right permissions on the file.
+	 *
+	 * TODO: consider encrypting with system_key.
+	 */
+	write_file(data, comment_len + len, false, "%s", path);
+	explicit_bzero(settings_data, len);
+	explicit_bzero(data, len);
+}
+
 bool storage_is_file(const char *filename)
 {
 	char *path;
diff -pruN 1.30-1/src/storage.h 2.3-1/src/storage.h
--- 1.30-1/src/storage.h	2022-02-24 20:54:25.000000000 +0000
+++ 2.3-1/src/storage.h	2022-11-18 12:31:49.000000000 +0000
@@ -51,6 +51,9 @@ int storage_network_remove(enum security
 struct l_settings *storage_known_frequencies_load(void);
 void storage_known_frequencies_sync(struct l_settings *known_freqs);
 
+struct l_settings *storage_eap_tls_cache_load(void);
+void storage_eap_tls_cache_sync(const struct l_settings *cache);
+
 int __storage_decrypt(struct l_settings *settings, const char *ssid,
 				bool *changed);
 char *__storage_encrypt(const struct l_settings *settings, const char *ssid,
diff -pruN 1.30-1/src/util.c 2.3-1/src/util.c
--- 1.30-1/src/util.c	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/src/util.c	2022-12-18 19:59:36.000000000 +0000
@@ -360,6 +360,28 @@ bool scan_freq_set_add(struct scan_freq_
 	return false;
 }
 
+bool scan_freq_set_remove(struct scan_freq_set *freqs, uint32_t freq)
+{
+	enum band_freq band;
+	uint8_t channel;
+
+	channel = band_freq_to_channel(freq, &band);
+	if (!channel)
+		return false;
+
+	switch (band) {
+	case BAND_FREQ_2_4_GHZ:
+		freqs->channels_2ghz &= ~(1 << (channel - 1));
+		return true;
+	case BAND_FREQ_5_GHZ:
+		return l_uintset_take(freqs->channels_5ghz, channel);
+	case BAND_FREQ_6_GHZ:
+		return l_uintset_take(freqs->channels_6ghz, channel);
+	}
+
+	return false;
+}
+
 bool scan_freq_set_contains(const struct scan_freq_set *freqs, uint32_t freq)
 {
 	enum band_freq band;
diff -pruN 1.30-1/src/util.h 2.3-1/src/util.h
--- 1.30-1/src/util.h	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/src/util.h	2022-12-18 19:59:36.000000000 +0000
@@ -111,6 +111,7 @@ typedef void (*scan_freq_set_func_t)(uin
 struct scan_freq_set *scan_freq_set_new(void);
 void scan_freq_set_free(struct scan_freq_set *freqs);
 bool scan_freq_set_add(struct scan_freq_set *freqs, uint32_t freq);
+bool scan_freq_set_remove(struct scan_freq_set *freqs, uint32_t freq);
 bool scan_freq_set_contains(const struct scan_freq_set *freqs, uint32_t freq);
 uint32_t scan_freq_set_get_bands(const struct scan_freq_set *freqs);
 void scan_freq_set_foreach(const struct scan_freq_set *freqs,
diff -pruN 1.30-1/src/wiphy.c 2.3-1/src/wiphy.c
--- 1.30-1/src/wiphy.c	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/src/wiphy.c	2023-02-02 12:57:32.000000000 +0000
@@ -105,8 +105,6 @@ struct wiphy {
 	uint16_t supported_iftypes;
 	uint16_t supported_ciphers;
 	struct scan_freq_set *supported_freqs;
-	struct scan_freq_set *disabled_freqs;
-	struct scan_freq_set *pending_freqs;
 	struct band *band_2g;
 	struct band *band_5g;
 	struct band *band_6g;
@@ -138,6 +136,8 @@ struct wiphy {
 	bool blacklisted : 1;
 	bool registered : 1;
 	bool self_managed : 1;
+	bool ap_probe_resp_offload : 1;
+	bool supports_uapsd : 1;
 };
 
 static struct l_queue *wiphy_list = NULL;
@@ -149,19 +149,41 @@ enum ie_rsn_cipher_suite wiphy_select_ci
 
 	mask &= wiphy->supported_ciphers;
 
-	/* CCMP is our first choice, TKIP second */
+	if (mask & IE_RSN_CIPHER_SUITE_GCMP_256)
+		return IE_RSN_CIPHER_SUITE_GCMP_256;
+
+	if (mask & IE_RSN_CIPHER_SUITE_CCMP_256)
+		return IE_RSN_CIPHER_SUITE_CCMP_256;
+
+	if (mask & IE_RSN_CIPHER_SUITE_GCMP)
+		return IE_RSN_CIPHER_SUITE_GCMP;
+
 	if (mask & IE_RSN_CIPHER_SUITE_CCMP)
 		return IE_RSN_CIPHER_SUITE_CCMP;
 
 	if (mask & IE_RSN_CIPHER_SUITE_TKIP)
 		return IE_RSN_CIPHER_SUITE_TKIP;
 
-	if (mask & IE_RSN_CIPHER_SUITE_BIP)
-		return IE_RSN_CIPHER_SUITE_BIP;
+	if (mask & IE_RSN_CIPHER_SUITE_BIP_GMAC_256)
+		return IE_RSN_CIPHER_SUITE_BIP_GMAC_256;
+
+	if (mask & IE_RSN_CIPHER_SUITE_BIP_CMAC_256)
+		return IE_RSN_CIPHER_SUITE_BIP_CMAC_256;
+
+	if (mask & IE_RSN_CIPHER_SUITE_BIP_GMAC)
+		return IE_RSN_CIPHER_SUITE_BIP_GMAC;
+
+	if (mask & IE_RSN_CIPHER_SUITE_BIP_CMAC)
+		return IE_RSN_CIPHER_SUITE_BIP_CMAC;
 
 	return 0;
 }
 
+uint16_t wiphy_get_supported_ciphers(struct wiphy *wiphy, uint16_t mask)
+{
+	return wiphy->supported_ciphers & mask;
+}
+
 static bool wiphy_can_connect_sae(struct wiphy *wiphy)
 {
 	/*
@@ -177,7 +199,7 @@ static bool wiphy_can_connect_sae(struct
 	 * WPA3 Specification version 3, Section 2.3:
 	 * A STA shall negotiate PMF when associating to an AP using SAE
 	 */
-	if (!(wiphy->supported_ciphers & IE_RSN_CIPHER_SUITE_BIP)) {
+	if (!(wiphy->supported_ciphers & IE_RSN_CIPHER_SUITE_BIP_CMAC)) {
 		l_debug("HW not MFP capable, can't use SAE");
 		return false;
 	}
@@ -322,7 +344,6 @@ static struct wiphy *wiphy_new(uint32_t
 
 	wiphy->id = id;
 	wiphy->supported_freqs = scan_freq_set_new();
-	wiphy->disabled_freqs = scan_freq_set_new();
 	watchlist_init(&wiphy->state_watches, NULL);
 	wiphy->extended_capabilities[0] = IE_TYPE_EXTENDED_CAPABILITIES;
 	wiphy->extended_capabilities[1] = EXT_CAP_LEN;
@@ -370,7 +391,6 @@ static void wiphy_free(void *data)
 	}
 
 	scan_freq_set_free(wiphy->supported_freqs);
-	scan_freq_set_free(wiphy->disabled_freqs);
 	watchlist_destroy(&wiphy->state_watches);
 	l_free(wiphy->model_str);
 	l_free(wiphy->vendor_str);
@@ -468,9 +488,87 @@ const struct scan_freq_set *wiphy_get_su
 	return wiphy->supported_freqs;
 }
 
-const struct scan_freq_set *wiphy_get_disabled_freqs(const struct wiphy *wiphy)
+static struct band *wiphy_get_band(const struct wiphy *wiphy, enum band_freq band)
+{
+	switch (band) {
+	case BAND_FREQ_2_4_GHZ:
+		return wiphy->band_2g;
+	case BAND_FREQ_5_GHZ:
+		return wiphy->band_5g;
+	case BAND_FREQ_6_GHZ:
+		return wiphy->band_6g;
+	default:
+		return NULL;
+	}
+}
+
+const struct band_freq_attrs *wiphy_get_frequency_info(
+						const struct wiphy *wiphy,
+						uint32_t freq)
+{
+	struct band_freq_attrs *attr;
+	enum band_freq band;
+	uint8_t channel;
+	struct band *bandp;
+
+	channel = band_freq_to_channel(freq, &band);
+	if (!channel)
+		return NULL;
+
+	bandp = wiphy_get_band(wiphy, band);
+	if (!bandp)
+		return NULL;
+
+	attr = &bandp->freq_attrs[channel];
+	if (!attr->supported)
+		return NULL;
+
+	return attr;
+}
+
+const struct band_freq_attrs *wiphy_get_frequency_info_list(
+						const struct wiphy *wiphy,
+						enum band_freq band,
+						size_t *size)
+{
+	struct band *bandp;
+
+	bandp = wiphy_get_band(wiphy, band);
+	if (!bandp)
+		return NULL;
+
+	if (size)
+		*size = bandp->freqs_len;
+
+	return bandp->freq_attrs;
+}
+
+bool wiphy_band_is_disabled(const struct wiphy *wiphy, enum band_freq band)
+{
+	struct band_freq_attrs attr;
+	unsigned int i;
+	struct band *bandp;
+
+	bandp = wiphy_get_band(wiphy, band);
+	if (!bandp)
+		return true;
+
+	for (i = 0; i < bandp->freqs_len; i++) {
+		attr = bandp->freq_attrs[i];
+
+		if (!attr.supported)
+			continue;
+
+		if (!attr.disabled)
+			return false;
+	}
+
+	return true;
+}
+
+bool wiphy_supports_probe_resp_offload(struct wiphy *wiphy)
 {
-	return wiphy->disabled_freqs;
+	return wiphy->ap_probe_resp_offload;
 }
 
 bool wiphy_can_transition_disable(struct wiphy *wiphy)
@@ -482,7 +580,7 @@ bool wiphy_can_transition_disable(struct
 	if (!(wiphy->supported_ciphers & IE_RSN_CIPHER_SUITE_CCMP))
 		return false;
 
-	if (!(wiphy->supported_ciphers & IE_RSN_CIPHER_SUITE_BIP))
+	if (!(wiphy->supported_ciphers & IE_RSN_CIPHER_SUITE_BIP_CMAC))
 		return false;
 
 	return true;
@@ -717,8 +815,35 @@ void wiphy_generate_address_from_ssid(st
 bool wiphy_constrain_freq_set(const struct wiphy *wiphy,
 						struct scan_freq_set *set)
 {
+	struct band *bands[3] = { wiphy->band_2g,
+					wiphy->band_5g, wiphy->band_6g };
+	unsigned int b;
+	unsigned int i;
+
 	scan_freq_set_constrain(set, wiphy->supported_freqs);
-	scan_freq_set_subtract(set, wiphy->disabled_freqs);
+
+	for (b = 0; b < L_ARRAY_SIZE(bands); b++) {
+		struct band *band = bands[b];
+
+		if (!band)
+			continue;
+
+		for (i = 0; i < band->freqs_len; i++) {
+			uint32_t freq;
+
+			if (!band->freq_attrs[i].supported)
+				continue;
+
+			if (!band->freq_attrs[i].disabled)
+				continue;
+
+			freq = band_channel_to_freq(i, band->freq);
+			if (!freq)
+				continue;
+
+			scan_freq_set_remove(set, freq);
+		}
+	}
 
 	if (!scan_freq_set_get_bands(set))
 		/* The set is empty. */
@@ -760,24 +885,11 @@ bool wiphy_supports_iftype(struct wiphy
 	return wiphy->supported_iftypes & (1 << (iftype - 1));
 }
 
-const uint8_t *wiphy_get_supported_rates(struct wiphy *wiphy, unsigned int band,
+const uint8_t *wiphy_get_supported_rates(struct wiphy *wiphy,
+						enum band_freq band,
 						unsigned int *out_num)
 {
-	struct band *bandp;
-
-	switch (band) {
-	case NL80211_BAND_2GHZ:
-		bandp = wiphy->band_2g;
-		break;
-	case NL80211_BAND_5GHZ:
-		bandp = wiphy->band_5g;
-		break;
-	case NL80211_BAND_6GHZ:
-		bandp = wiphy->band_6g;
-		break;
-	default:
-		return NULL;
-	}
+	struct band *bandp = wiphy_get_band(wiphy, band);
 
 	if (!bandp)
 		return NULL;
@@ -816,6 +928,47 @@ bool wiphy_country_is_unknown(struct wip
 			(cc[0] == 'X' && cc[1] == 'X'));
 }
 
+bool wiphy_supports_uapsd(const struct wiphy *wiphy)
+{
+	return wiphy->supports_uapsd;
+}
+
+const uint8_t *wiphy_get_ht_capabilities(const struct wiphy *wiphy,
+						enum band_freq band,
+						size_t *size)
+{
+	static uint8_t ht_capa[26];
+	const struct band *bandp = wiphy_get_band(wiphy, band);
+
+	if (!bandp)
+		return NULL;
+
+	if (!bandp->ht_supported)
+		return NULL;
+
+	memset(ht_capa, 0, sizeof(ht_capa));
+
+	/*
+	 * The kernel segments the HT capabilities element into multiple
+	 * attributes. For convenience on the caller just combine them and
+	 * return the full IE rather than adding 3 separate getters. This also
+	 * provides a way to check if HT is supported.
+	 */
+	memcpy(ht_capa, bandp->ht_capabilities, 2);
+	ht_capa[2] = bandp->ht_ampdu_params;
+	memcpy(ht_capa + 3, bandp->ht_mcs_set, 16);
+
+	/*
+	 * TODO: HT Extended capabilities, beamforming, and ASEL capabilities
+	 * are not available to get from the kernel, leave as zero.
+	 */
+
+	if (size)
+		*size = sizeof(ht_capa);
+
+	return ht_capa;
+}
+
 int wiphy_estimate_data_rate(struct wiphy *wiphy,
 				const void *ies, uint16_t ies_len,
 				const struct scan_bss *bss,
@@ -835,19 +988,9 @@ int wiphy_estimate_data_rate(struct wiph
 	if (band_freq_to_channel(bss->frequency, &band) == 0)
 		return -ENOTSUP;
 
-	switch (band) {
-	case BAND_FREQ_2_4_GHZ:
-		bandp = wiphy->band_2g;
-		break;
-	case BAND_FREQ_5_GHZ:
-		bandp = wiphy->band_5g;
-		break;
-	case BAND_FREQ_6_GHZ:
-		bandp = wiphy->band_6g;
-		break;
-	default:
+	bandp = wiphy_get_band(wiphy, band);
+	if (!bandp)
 		return -ENOTSUP;
-	}
 
 	ie_tlv_iter_init(&iter, ies, ies_len);
 
@@ -923,7 +1066,7 @@ int wiphy_estimate_data_rate(struct wiph
 
 bool wiphy_regdom_is_updating(struct wiphy *wiphy)
 {
-	return wiphy->pending_freqs != NULL;
+	return wiphy->dump_id || (!wiphy->self_managed && wiphy_dump_id);
 }
 
 uint32_t wiphy_state_watch_add(struct wiphy *wiphy,
@@ -1114,7 +1257,7 @@ static void wiphy_print_band_info(struct
 
 static void wiphy_print_basic_info(struct wiphy *wiphy)
 {
-	char buf[1024];
+	char buf[2048];
 
 	l_info("Wiphy: %d, Name: %s", wiphy->id, wiphy->name);
 	l_info("\tPermanent Address: "MAC, MAC_STR(wiphy->permanent_addr));
@@ -1129,18 +1272,33 @@ static void wiphy_print_basic_info(struc
 		wiphy_print_band_info(wiphy->band_6g, "6GHz Band");
 
 	if (wiphy->supported_ciphers) {
-		int len = 0;
+		int n = 0;
+		size_t len = 0;
+		int i = sizeof(wiphy->supported_ciphers) * 8 - 1;
+
+		len += snprintf(buf, sizeof(buf), "\tCiphers:");
+
+		for (; i >= 0 && len < sizeof(buf); i--) {
+			typeof(wiphy->supported_ciphers) cipher = 1 << i;
+			const char *str;
 
-		len += sprintf(buf + len, "\tCiphers:");
+			if (cipher == IE_RSN_CIPHER_SUITE_WEP40 ||
+					cipher == IE_RSN_CIPHER_SUITE_WEP104)
+				continue;
 
-		if (wiphy->supported_ciphers & IE_RSN_CIPHER_SUITE_CCMP)
-			len += sprintf(buf + len, " CCMP");
+			if (!(wiphy->supported_ciphers & cipher))
+				continue;
+
+			str = ie_rsn_cipher_suite_to_string(cipher);
+			if (!str)
+				continue;
 
-		if (wiphy->supported_ciphers & IE_RSN_CIPHER_SUITE_TKIP)
-			len += sprintf(buf + len, " TKIP");
+			len += snprintf(buf + len, sizeof(buf) - len, "%s%s",
+					!n || (n % 4) ? " " : "\n\t\t ",
+					str);
 
-		if (wiphy->supported_ciphers & IE_RSN_CIPHER_SUITE_BIP)
-			len += sprintf(buf + len, " BIP");
+			n += 1;
+		}
 
 		l_info("%s", buf);
 	}
@@ -1209,8 +1367,32 @@ static void parse_supported_ciphers(stru
 		case CRYPTO_CIPHER_WEP104:
 			wiphy->supported_ciphers |= IE_RSN_CIPHER_SUITE_WEP104;
 			break;
-		case CRYPTO_CIPHER_BIP:
-			wiphy->supported_ciphers |= IE_RSN_CIPHER_SUITE_BIP;
+		case CRYPTO_CIPHER_BIP_CMAC:
+			wiphy->supported_ciphers |=
+				IE_RSN_CIPHER_SUITE_BIP_CMAC;
+			break;
+		case CRYPTO_CIPHER_GCMP:
+			wiphy->supported_ciphers |= IE_RSN_CIPHER_SUITE_GCMP;
+			break;
+		case CRYPTO_CIPHER_GCMP_256:
+			wiphy->supported_ciphers |=
+				IE_RSN_CIPHER_SUITE_GCMP_256;
+			break;
+		case CRYPTO_CIPHER_CCMP_256:
+			wiphy->supported_ciphers |=
+				IE_RSN_CIPHER_SUITE_CCMP_256;
+			break;
+		case CRYPTO_CIPHER_BIP_GMAC:
+			wiphy->supported_ciphers |=
+				IE_RSN_CIPHER_SUITE_BIP_GMAC;
+			break;
+		case CRYPTO_CIPHER_BIP_GMAC_256:
+			wiphy->supported_ciphers |=
+				IE_RSN_CIPHER_SUITE_BIP_GMAC_256;
+			break;
+		case CRYPTO_CIPHER_BIP_CMAC_256:
+			wiphy->supported_ciphers |=
+				IE_RSN_CIPHER_SUITE_BIP_CMAC_256;
 			break;
 		default:	/* TODO: Support other ciphers */
 			break;
@@ -1403,19 +1585,23 @@ static void parse_supported_bands(struct
 		struct band **bandp;
 		struct band *band;
 		enum band_freq freq;
+		size_t num_channels;
 
 		switch (type) {
 		case NL80211_BAND_2GHZ:
 			bandp = &wiphy->band_2g;
 			freq = BAND_FREQ_2_4_GHZ;
+			num_channels = 14;
 			break;
 		case NL80211_BAND_5GHZ:
 			bandp = &wiphy->band_5g;
 			freq = BAND_FREQ_5_GHZ;
+			num_channels = 196;
 			break;
 		case NL80211_BAND_6GHZ:
 			bandp = &wiphy->band_6g;
 			freq = BAND_FREQ_6_GHZ;
+			num_channels = 233;
 			break;
 		default:
 			continue;
@@ -1430,6 +1616,13 @@ static void parse_supported_bands(struct
 				continue;
 
 			band->freq = freq;
+			/*
+			 * Since channels start at 1, allocate one extra in
+			 * order to use channel indexes without arithmetic
+			 */
+			band->freq_attrs = l_new(struct band_freq_attrs,
+							num_channels + 1);
+			band->freqs_len = num_channels;
 
 			/* Reset iter to beginning */
 			if (!l_genl_attr_recurse(bands, &attr)) {
@@ -1439,7 +1632,6 @@ static void parse_supported_bands(struct
 		} else
 			band = *bandp;
 
-
 		while (l_genl_attr_next(&attr, &type, &len, &data)) {
 			struct l_genl_attr nested;
 
@@ -1447,7 +1639,8 @@ static void parse_supported_bands(struct
 			case NL80211_BAND_ATTR_FREQS:
 				nl80211_parse_supported_frequencies(&attr,
 							wiphy->supported_freqs,
-							wiphy->disabled_freqs);
+							band->freq_attrs,
+							band->freqs_len);
 				break;
 
 			case NL80211_BAND_ATTR_RATES:
@@ -1487,6 +1680,23 @@ static void parse_supported_bands(struct
 				memcpy(band->ht_capabilities, data, len);
 				band->ht_supported = true;
 				break;
+			/*
+			 * AMPDU factor/density are part of A-MPDU Parameters,
+			 * 802.11-2020 Section 9.4.2.55.3.
+			 */
+			case NL80211_BAND_ATTR_HT_AMPDU_FACTOR:
+				if (L_WARN_ON(len != 1))
+					continue;
+
+				band->ht_ampdu_params |= l_get_u8(data) & 0x3;
+				break;
+			case NL80211_BAND_ATTR_HT_AMPDU_DENSITY:
+				if (L_WARN_ON(len != 1))
+					continue;
+
+				band->ht_ampdu_params |=
+						(l_get_u8(data) & 0x7) << 2;
+				break;
 			case NL80211_BAND_ATTR_IFTYPE_DATA:
 				if (!l_genl_attr_recurse(&attr, &nested))
 					continue;
@@ -1644,6 +1854,12 @@ static void wiphy_parse_attributes(struc
 		case NL80211_ATTR_WIPHY_SELF_MANAGED_REG:
 			wiphy->self_managed = true;
 			break;
+		case NL80211_ATTR_PROBE_RESP_OFFLOAD:
+			wiphy->ap_probe_resp_offload = true;
+			break;
+		case NL80211_ATTR_SUPPORT_AP_UAPSD:
+			wiphy->supports_uapsd = true;
+			break;
 		}
 	}
 }
@@ -1861,9 +2077,6 @@ static void wiphy_dump_done(void *user_d
 
 	if (wiphy) {
 		wiphy->dump_id = 0;
-		scan_freq_set_free(wiphy->disabled_freqs);
-		wiphy->disabled_freqs = wiphy->pending_freqs;
-		wiphy->pending_freqs = NULL;
 
 		WATCHLIST_NOTIFY(&wiphy->state_watches,
 				wiphy_state_watch_func_t, wiphy,
@@ -1877,13 +2090,9 @@ static void wiphy_dump_done(void *user_d
 	for (e = l_queue_get_entries(wiphy_list); e; e = e->next) {
 		wiphy = e->data;
 
-		if (!wiphy->pending_freqs || wiphy->self_managed)
+		if (wiphy->self_managed)
 			continue;
 
-		scan_freq_set_free(wiphy->disabled_freqs);
-		wiphy->disabled_freqs = wiphy->pending_freqs;
-		wiphy->pending_freqs = NULL;
-
 		WATCHLIST_NOTIFY(&wiphy->state_watches,
 				wiphy_state_watch_func_t, wiphy,
 				WIPHY_STATE_WATCH_EVENT_REGDOM_DONE);
@@ -1899,6 +2108,7 @@ static void wiphy_dump_callback(struct l
 	struct l_genl_attr bands;
 	struct l_genl_attr attr;
 	uint16_t type;
+	struct band *band;
 
 	if (nl80211_parse_attrs(msg, NL80211_ATTR_WIPHY, &id,
 					NL80211_ATTR_WIPHY_BANDS, &bands,
@@ -1909,7 +2119,28 @@ static void wiphy_dump_callback(struct l
 	if (L_WARN_ON(!wiphy))
 		return;
 
-	while (l_genl_attr_next(&bands, NULL, NULL, NULL)) {
+	/* Unregistered means the wiphy is blacklisted, don't bother parsing */
+	if (!wiphy->registered)
+		return;
+
+	while (l_genl_attr_next(&bands, &type, NULL, NULL)) {
+		switch (type) {
+		case NL80211_BAND_2GHZ:
+			band = wiphy->band_2g;
+			break;
+		case NL80211_BAND_5GHZ:
+			band = wiphy->band_5g;
+			break;
+		case NL80211_BAND_6GHZ:
+			band = wiphy->band_6g;
+			break;
+		default:
+			continue;
+		}
+
+		if (L_WARN_ON(!band))
+			continue;
+
 		if (!l_genl_attr_recurse(&bands, &attr))
 			return;
 
@@ -1917,41 +2148,32 @@ static void wiphy_dump_callback(struct l
 			if (type != NL80211_BAND_ATTR_FREQS)
 				continue;
 
+			/*
+			 * Just write over the old list for each frequency. In
+			 * theory no new frequencies should be added so there
+			 * should never be any stale values.
+			 */
 			nl80211_parse_supported_frequencies(&attr, NULL,
-							wiphy->pending_freqs);
+							band->freq_attrs,
+							band->freqs_len);
 		}
 	}
 }
 
 static bool wiphy_cancel_last_dump(struct wiphy *wiphy)
 {
-	const struct l_queue_entry *e;
 	unsigned int id = 0;
 
 	/*
 	 * Zero command ID to signal that wiphy_dump_done doesn't need to do
-	 * anything. For a self-managed wiphy just free/NULL pending_freqs. For
-	 * a global dump each wiphy needs to be checked and dealt with.
+	 * anything.
 	 */
 	if (wiphy && wiphy->dump_id) {
 		id = wiphy->dump_id;
 		wiphy->dump_id = 0;
-
-		scan_freq_set_free(wiphy->pending_freqs);
-		wiphy->pending_freqs = NULL;
 	} else if (!wiphy && wiphy_dump_id) {
 		id = wiphy_dump_id;
 		wiphy_dump_id = 0;
-
-		for (e = l_queue_get_entries(wiphy_list); e; e = e->next) {
-			struct wiphy *w = e->data;
-
-			if (!w->pending_freqs || w->self_managed)
-				continue;
-
-			scan_freq_set_free(w->pending_freqs);
-			w->pending_freqs = NULL;
-		}
 	}
 
 	if (id) {
@@ -1996,7 +2218,6 @@ static void wiphy_dump_after_regdom(stru
 	/* Limited dump so just emit the event for this wiphy */
 	if (wiphy) {
 		wiphy->dump_id = id;
-		wiphy->pending_freqs = scan_freq_set_new();
 
 		if (no_start_event)
 			return;
@@ -2016,8 +2237,6 @@ static void wiphy_dump_after_regdom(stru
 		if (w->self_managed)
 			continue;
 
-		w->pending_freqs = scan_freq_set_new();
-
 		if (no_start_event)
 			continue;
 
@@ -2109,6 +2328,15 @@ static void wiphy_get_reg_domain(struct
 
 void wiphy_create_complete(struct wiphy *wiphy)
 {
+	/*
+	 * With really bad timing two wiphy dumps can occur (initial and a
+	 * NEW_WIPHY event) and actually register twice. Ignoring/preventing the
+	 * second dump is problematic since it _could_ be a legitimate event so
+	 * instead just prevent it from registering twice.
+	 */
+	if (wiphy->registered)
+		return;
+
 	wiphy_register(wiphy);
 
 	if (l_memeqzero(wiphy->permanent_addr, 6)) {
diff -pruN 1.30-1/src/wiphy.h 2.3-1/src/wiphy.h
--- 1.30-1/src/wiphy.h	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/src/wiphy.h	2023-01-23 18:46:38.000000000 +0000
@@ -28,7 +28,9 @@ struct scan_bss;
 struct scan_freq_set;
 struct wiphy_radio_work_item;
 struct ie_rsn_info;
+struct band_freq_attrs;
 enum security;
+enum band_freq;
 
 typedef bool (*wiphy_radio_work_func_t)(struct wiphy_radio_work_item *item);
 typedef void (*wiphy_radio_work_destroy_func_t)(
@@ -46,11 +48,12 @@ struct wiphy_radio_work_item {
 };
 
 enum {
-	WIPHY_WORK_PRIORITY_FRAME = 0,
-	WIPHY_WORK_PRIORITY_OFFCHANNEL = 0,
-	WIPHY_WORK_PRIORITY_CONNECT = 1,
-	WIPHY_WORK_PRIORITY_SCAN = 2,
-	WIPHY_WORK_PRIORITY_PERIODIC_SCAN = 3,
+	WIPHY_WORK_PRIORITY_FT = 0,
+	WIPHY_WORK_PRIORITY_FRAME = 1,
+	WIPHY_WORK_PRIORITY_OFFCHANNEL = 1,
+	WIPHY_WORK_PRIORITY_CONNECT = 2,
+	WIPHY_WORK_PRIORITY_SCAN = 3,
+	WIPHY_WORK_PRIORITY_PERIODIC_SCAN = 4,
 };
 
 enum wiphy_state_watch_event {
@@ -67,6 +70,8 @@ typedef void (*wiphy_destroy_func_t)(voi
 
 enum ie_rsn_cipher_suite wiphy_select_cipher(struct wiphy *wiphy,
 							uint16_t mask);
+uint16_t wiphy_get_supported_ciphers(struct wiphy *wiphy, uint16_t mask);
+
 enum ie_rsn_akm_suite wiphy_select_akm(struct wiphy *wiphy,
 					const struct scan_bss *bss,
 					enum security security,
@@ -95,7 +100,18 @@ const char *wiphy_get_path(struct wiphy
 uint32_t wiphy_get_supported_bands(struct wiphy *wiphy);
 const struct scan_freq_set *wiphy_get_supported_freqs(
 						const struct wiphy *wiphy);
-const struct scan_freq_set *wiphy_get_disabled_freqs(const struct wiphy *wiphy);
+
+const struct band_freq_attrs *wiphy_get_frequency_info(
+						const struct wiphy *wiphy,
+						uint32_t freq);
+const struct band_freq_attrs *wiphy_get_frequency_info_list(
+						const struct wiphy *wiphy,
+						enum band_freq band,
+						size_t *size);
+
+bool wiphy_band_is_disabled(const struct wiphy *wiphy, enum band_freq band);
+
+bool wiphy_supports_probe_resp_offload(struct wiphy *wiphy);
 bool wiphy_can_transition_disable(struct wiphy *wiphy);
 bool wiphy_can_offload(struct wiphy *wiphy);
 bool wiphy_supports_cmds_auth_assoc(struct wiphy *wiphy);
@@ -108,7 +124,8 @@ uint8_t wiphy_get_max_num_ssids_per_scan
 uint16_t wiphy_get_max_scan_ie_len(struct wiphy *wiphy);
 uint32_t wiphy_get_max_roc_duration(struct wiphy *wiphy);
 bool wiphy_supports_iftype(struct wiphy *wiphy, uint32_t iftype);
-const uint8_t *wiphy_get_supported_rates(struct wiphy *wiphy, unsigned int band,
+const uint8_t *wiphy_get_supported_rates(struct wiphy *wiphy,
+						enum band_freq band,
 						unsigned int *out_num);
 bool wiphy_supports_adhoc_rsn(struct wiphy *wiphy);
 bool wiphy_can_offchannel_tx(struct wiphy *wiphy);
@@ -125,7 +142,11 @@ const uint8_t *wiphy_get_rm_enabled_capa
 bool wiphy_get_rsnxe(const struct wiphy *wiphy, uint8_t *buf, size_t len);
 void wiphy_get_reg_domain_country(struct wiphy *wiphy, char *out);
 bool wiphy_country_is_unknown(struct wiphy *wiphy);
+bool wiphy_supports_uapsd(const struct wiphy *wiphy);
 
+const uint8_t *wiphy_get_ht_capabilities(const struct wiphy *wiphy,
+						enum band_freq band,
+						size_t *size);
 void wiphy_generate_random_address(struct wiphy *wiphy, uint8_t addr[static 6]);
 void wiphy_generate_address_from_ssid(struct wiphy *wiphy, const char *ssid,
 					uint8_t addr[static 6]);
diff -pruN 1.30-1/src/wsc.c 2.3-1/src/wsc.c
--- 1.30-1/src/wsc.c	2022-03-22 18:12:13.000000000 +0000
+++ 2.3-1/src/wsc.c	2022-11-18 12:31:49.000000000 +0000
@@ -665,6 +665,8 @@ static void wsc_check_can_connect(struct
 	case STATION_STATE_AUTOCONNECT_QUICK:
 	case STATION_STATE_AUTOCONNECT_FULL:
 	case STATION_STATE_ROAMING:
+	case STATION_STATE_FT_ROAMING:
+	case STATION_STATE_FW_ROAMING:
 		l_warn("%s: invalid station state", __func__);
 		break;
 	}
diff -pruN 1.30-1/tools/hwsim.c 2.3-1/tools/hwsim.c
--- 1.30-1/tools/hwsim.c	2022-04-21 15:02:58.000000000 +0000
+++ 2.3-1/tools/hwsim.c	2022-11-18 12:31:49.000000000 +0000
@@ -175,7 +175,13 @@ static const uint32_t hwsim_supported_ci
 	CRYPTO_CIPHER_WEP104,
 	CRYPTO_CIPHER_TKIP,
 	CRYPTO_CIPHER_CCMP,
-	CRYPTO_CIPHER_BIP,
+	CRYPTO_CIPHER_BIP_CMAC,
+	CRYPTO_CIPHER_GCMP,
+	CRYPTO_CIPHER_GCMP_256,
+	CRYPTO_CIPHER_CCMP_256,
+	CRYPTO_CIPHER_BIP_GMAC,
+	CRYPTO_CIPHER_BIP_GMAC_256,
+	CRYPTO_CIPHER_BIP_CMAC_256,
 };
 static uint32_t hwsim_ciphers[L_ARRAY_SIZE(hwsim_supported_ciphers)];
 static int hwsim_num_ciphers = 0;
@@ -196,7 +202,13 @@ static const struct hwsim_support cipher
 	{ "wep104", CRYPTO_CIPHER_WEP104 },
 	{ "tkip", CRYPTO_CIPHER_TKIP },
 	{ "ccmp", CRYPTO_CIPHER_CCMP },
-	{ "bip", CRYPTO_CIPHER_BIP },
+	{ "bip_cmac", CRYPTO_CIPHER_BIP_CMAC },
+	{ "gcmp", CRYPTO_CIPHER_GCMP },
+	{ "gcmp_256", CRYPTO_CIPHER_GCMP_256 },
+	{ "ccmp_256", CRYPTO_CIPHER_CCMP_256 },
+	{ "bip_gmac", CRYPTO_CIPHER_BIP_GMAC },
+	{ "bip_gmac_256", CRYPTO_CIPHER_BIP_GMAC_256 },
+	{ "bip_cmac_256", CRYPTO_CIPHER_BIP_CMAC_256 },
 	{ }
 };
 
@@ -1204,7 +1216,7 @@ static void process_rules(const struct r
 		struct hwsim_rule *rule = rule_entry->data;
 
 		if (!rule->enabled)
-			return;
+			continue;
 
 		if (!rule->source_any &&
 				!radio_match_addr(src_radio, rule->source) &&
diff -pruN 1.30-1/unit/test-arc4.c 2.3-1/unit/test-arc4.c
--- 1.30-1/unit/test-arc4.c	2019-10-30 10:50:34.000000000 +0000
+++ 2.3-1/unit/test-arc4.c	2022-11-18 12:31:49.000000000 +0000
@@ -92,6 +92,8 @@ static void arc4_skip_test(const void *d
 
 	assert(arc4_skip(test->key, 16, test->offset, buf, sizeof(buf), buf));
 	assert(!memcmp(buf, test->result, sizeof(buf)));
+	assert(arc4_skip(test->key, 16, test->offset, buf, sizeof(buf), buf));
+	assert(l_memeqzero(buf, 16));
 }
 
 int main(int argc, char *argv[])
diff -pruN 1.30-1/unit/test-band.c 2.3-1/unit/test-band.c
--- 1.30-1/unit/test-band.c	2022-09-07 18:42:27.000000000 +0000
+++ 2.3-1/unit/test-band.c	2023-01-23 18:46:38.000000000 +0000
@@ -644,12 +644,32 @@ static void test_6ghz_freqs(const void *
 	uint32_t i;
 	enum band_freq band;
 
-	for (i = 5955; i < 7115; i += 5) {
+	for (i = 5955; i < 7115; i += 20) {
 		assert(band_freq_to_channel(i, &band) != 0);
 		assert(band == BAND_FREQ_6_GHZ);
 	}
 }
 
+static void test_conversions(const void *data)
+{
+	/*
+	 * Test a few invalid channels/frequencies that appear valid but are
+	 * not in the E-4 table. The checks in band.c seem to cover 2.4Ghz and
+	 * 6Ghz very well since there are no gaps, but the 5GHz band has some
+	 * segmentation.
+	 */
+
+	/* Gap in 5GHz channels between 68 and 96 */
+	assert(!band_channel_to_freq(72, BAND_FREQ_5_GHZ));
+	assert(!band_freq_to_channel(5360, NULL));
+
+	/* Invalid channel using 4000mhz starting frequency */
+	assert(!band_channel_to_freq(183, BAND_FREQ_5_GHZ));
+	assert(!band_freq_to_channel(4915, NULL));
+
+	assert(!band_channel_to_freq(192, BAND_FREQ_5_GHZ));
+}
+
 int main(int argc, char *argv[])
 {
 	l_test_init(&argc, &argv);
@@ -715,5 +735,7 @@ int main(int argc, char *argv[])
 	l_test_add("/band/6ghz/channels", test_6ghz_channels, NULL);
 	l_test_add("/band/6ghz/freq", test_6ghz_freqs, NULL);
 
+	l_test_add("/band/conversions", test_conversions, NULL);
+
 	return l_test_run();
 }
diff -pruN 1.30-1/unit/test-eapol.c 2.3-1/unit/test-eapol.c
--- 1.30-1/unit/test-eapol.c	2022-07-15 17:08:06.000000000 +0000
+++ 2.3-1/unit/test-eapol.c	2023-01-23 18:46:38.000000000 +0000
@@ -1793,7 +1793,7 @@ static void eapol_4way_test(const void *
 				eapol_key_test_4.key_replay_counter,
 				snonce, eapol_key_test_4.key_data_len,
 				eapol_key_data_4 + EAPOL_FRAME_LEN(16),
-				false, 16);
+				false, 16, false);
 	assert(frame);
 	assert(eapol_calculate_mic(IE_RSN_AKM_SUITE_PSK, ptk, frame,
 					mic, 16));
@@ -1878,7 +1878,7 @@ static void eapol_wpa2_handshake_test(co
 				eapol_key_test_8.key_replay_counter,
 				snonce, eapol_key_test_8.key_data_len,
 				eapol_key_data_8 + EAPOL_FRAME_LEN(16),
-				false, 16);
+				false, 16, false);
 	assert(frame);
 	assert(eapol_calculate_mic(IE_RSN_AKM_SUITE_PSK, ptk, frame,
 					mic, 16));
@@ -2008,7 +2008,7 @@ static void eapol_wpa_handshake_test(con
 				eapol_key_test_14.key_replay_counter,
 				snonce, eapol_key_test_14.key_data_len,
 				eapol_key_data_14 + EAPOL_FRAME_LEN(16),
-				true, 16);
+				true, 16, false);
 	assert(frame);
 	assert(eapol_calculate_mic(IE_RSN_AKM_SUITE_PSK, ptk, frame,
 					mic, 16));
diff -pruN 1.30-1/unit/test-ie.c 2.3-1/unit/test-ie.c
--- 1.30-1/unit/test-ie.c	2021-08-01 20:19:12.000000000 +0000
+++ 2.3-1/unit/test-ie.c	2022-11-18 12:31:49.000000000 +0000
@@ -423,7 +423,7 @@ static const struct ie_rsne_info_test ie
 	.pairwise_ciphers = IE_RSN_CIPHER_SUITE_CCMP,
 	.akm_suites = IE_RSN_AKM_SUITE_8021X,
 	.mfpc = true, /* Management frame protection is enabled, not required */
-	.group_management_cipher = IE_RSN_CIPHER_SUITE_BIP,
+	.group_management_cipher = IE_RSN_CIPHER_SUITE_BIP_CMAC,
 };
 
 static void ie_test_rsne_info(const void *data)
diff -pruN 1.30-1/wired/ead.service.in 2.3-1/wired/ead.service.in
--- 1.30-1/wired/ead.service.in	2021-02-16 20:36:24.000000000 +0000
+++ 2.3-1/wired/ead.service.in	2022-11-18 12:31:49.000000000 +0000
@@ -1,5 +1,6 @@
 [Unit]
 Description=Ethernet service
+Documentation=man:ead(8)
 After=network-pre.target
 Before=network.target
 Wants=network.target
