diff mbox series

Adding 802.11ax decode to 'iw scan dump' Checking for interest and looking for feedback.

Message ID MWHPR12MB1533B5C250E9B35367EAF628D9330@MWHPR12MB1533.namprd12.prod.outlook.com (mailing list archive)
State Not Applicable
Delegated to: Johannes Berg
Headers show
Series Adding 802.11ax decode to 'iw scan dump' Checking for interest and looking for feedback. | expand

Commit Message

David Poole Jan. 19, 2020, 6:16 p.m. UTC
Hello.

Attached is a first pass of adding 802.11ax decode to iw scan.c  It's a little messy still. Posting here to check if there is any interest in my continuing to add the decode to 'iw'. If so, please let me know how I can get the code up to standard.

Here is the output so far: https://gist.github.com/linuxlizard/45914f588d49fd077205c851c62fa013

I don't have the 802.11ax spec so I'm using Wireshark's AX decode to guide me. Also sanity checking with WiFi Explorer (MacOS). I've tried to make the correct attributes in ie_he.[ch] but could also use feedback in that area as well. 

I've been testing with an ASUS RT_AX88U, a Netgear Nighthawk RAX80 (both with Broadcom chips), and a couple Qualcomm 807x devices.  For another test, I've captured the nlattr to a file where I can rebuild the nlmsg, feed it back into scan.c.

Thanks,
David

--
David Poole | Firmware Engineer | Cradlepoint | dpoole@cradlepoint.com

Comments

Johannes Berg Feb. 7, 2020, 12:08 p.m. UTC | #1
On Sun, 2020-01-19 at 18:16 +0000, David Poole wrote:
> Hello.
> 
> Attached is a first pass of adding 802.11ax decode to iw scan.c

Adding that would be great, but not using wireshark (GPL2) code here.

johannes
diff mbox series

Patch

From 45a369ccda08211ddf123ed99b76abb698937539 Mon Sep 17 00:00:00 2001
From: David Poole <dpoole@cradlepoint.com>
Date: Sun, 19 Jan 2020 10:59:31 -0700
Subject: [PATCH] work in progress: first pass adding 802.11ax

Signed-off-by: David Poole <dpoole@cradlepoint.com>
---
 Makefile    |   8 +-
 ie_he.c     | 706 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 ie_he.h     | 236 ++++++++++++++++++
 scan.c      |  77 +++++-
 test_scan.c | 213 ++++++++++++++++
 5 files changed, 1236 insertions(+), 4 deletions(-)
 create mode 100644 ie_he.c
 create mode 100644 ie_he.h
 create mode 100644 test_scan.c

diff --git a/Makefile b/Makefile
index 90f2251..631b9e9 100644
--- a/Makefile
+++ b/Makefile
@@ -13,13 +13,13 @@  cc-option = $(shell set -e ; $(CC) $(1) -c -x c /dev/null -o /dev/null >/dev/nul
 
 CFLAGS_EVAL := $(call cc-option,-Wstringop-overflow=4)
 
-CFLAGS ?= -O2 -g
+CFLAGS = -g
 CFLAGS += -Wall -Wextra -Wundef -Wstrict-prototypes -Wno-trigraphs -fno-strict-aliasing -fno-common \
 	  -Werror-implicit-function-declaration -Wsign-compare -Wno-unused-parameter \
 	  $(CFLAGS_EVAL)
 
 _OBJS := $(sort $(patsubst %.c,%.o,$(wildcard *.c)))
-VERSION_OBJS := $(filter-out version.o, $(_OBJS))
+VERSION_OBJS := $(filter-out version.o test_scan.o, $(_OBJS))
 OBJS := $(VERSION_OBJS) version.o
 
 ALL = iw
@@ -108,6 +108,10 @@  iw:	$(OBJS)
 	$(Q)$(CC) $(LDFLAGS) $(OBJS) $(LIBS) -o iw
 endif
 
+test_scan:	test_scan.o scan.o util.o version.o ie_he.o
+	@$(NQ) ' CC  ' test_scan
+	$(Q)$(CC) $(LDFLAGS) $^ $(LIBS) -o $@
+
 check:
 	$(Q)$(MAKE) all CC="REAL_CC=$(CC) CHECK=\"sparse -Wall\" cgcc"
 
diff --git a/ie_he.c b/ie_he.c
new file mode 100644
index 0000000..589bcd1
--- /dev/null
+++ b/ie_he.c
@@ -0,0 +1,706 @@ 
+/* Uses chunks of Wireshark, specifically the strings from packet-ieee80211.c
+ *
+ * So this file needs to be carefully aligned with Wireshark's license.
+ *
+ *
+ */ 
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+
+#include "iw.h"
+#include "ie_he.h"
+
+int ie_he_capabilities_init(struct IE_HE_Capabilities* ie, const uint8_t* data, uint8_t len)
+{
+	// I don't know officially how big this IE is (PPE is variable length, one
+	// entry per SS (Spatial Stream). Everything forward of PPE seems fixed length
+	// 1 byte exten id
+	// 26 bytes of data, min
+	// TODO I'm seeing 26 bytes sometimes and it's driving me crazy
+	if (len < 26) {
+		fprintf(stderr, "%s len=%u expected 26\n", __func__, len);
+		return -EINVAL;
+	}
+
+	const uint8_t* ptr = data;
+	// skip the Extension Id 
+	ptr++;
+	// point into the ie buf
+	ie->mac_capa = ptr;
+	ptr += IE_HE_CAPA_MAC_SIZE; 
+	ie->phy_capa = ptr;
+	ptr += IE_HE_CAPA_PHY_SIZE;
+	ie->mcs_and_nss_set = ptr;
+	ptr += IE_HE_CAPA_MCS_SIZE;
+	ie->ppe_threshold = ptr;
+
+	ie->mac = (const struct IE_HE_MAC*)ie->mac_capa;
+	ie->phy = (const struct IE_HE_PHY*)ie->phy_capa;
+
+	return 0;
+}
+
+int ie_he_operation_init(struct IE_HE_Operation* ie, const uint8_t* data, uint8_t len)
+{
+	// 1 byte for the extension ID
+	// 6 bytes for the payload
+	if (len != 6) {
+		return -EINVAL;
+	}
+
+	ie->params = data;
+	ie->bss_color = data + 4;
+	ie->mcs_and_nss_set = data + 5;
+
+	ie->fields = (struct IE_HE_Operation_Fields*)data;
+
+	return 0;
+}
+
+// from packet-ieee80211.c function max_frag_msdus_base_custom()
+int he_max_frag_msdus_base_to_str(uint8_t max_frag_msdus_value, char* s, size_t len)
+{
+  if (max_frag_msdus_value == 7)
+    return snprintf(s, len, "No restriction");
+  else
+    return snprintf( s, len, "%u", 1 << max_frag_msdus_value);
+}
+
+static const char* he_mac_cap_str[] = {
+	"+HTC HE Support", // htc_he_support
+	"TWT Requester Support", // twt_req_support
+	"TWT Responder Support", // twt_rsp_support
+	"Fragmentation Support", // fragmentation_support
+	NULL,
+	"Maximum Number of Fragmented MSDUs", // max_frag_msdus
+	NULL,
+	NULL,
+
+	"Minimum Fragment Size", // min_frag_size
+	NULL,
+	"Trigger Frame MAC Padding Duration", // trig_frm_mac_padding_dur
+	NULL,
+	"Multi-TID Aggregation Support", // multi_tid_agg_support
+	NULL, 
+	NULL,
+
+	"HE Link Adaptation Support", // he_link_adaptation_support
+	NULL,
+	"All Ack Support", // all_ack_support
+	"TRS Support", // trs_support
+	"BSR Support", // bsr_support
+	"Broadcast TWT Support", // broadcast_twt_support
+	"32-bit BA Bitmap Support", // 32_bit_ba_bitmap_support
+	"MU Cascading Support", // mu_cascading_support
+	"Ack-Enabled Aggregation Support", // ack_enabled_agg_support
+
+	"Reserved", // reserved_b24
+	"OM Control Support", // om_control_support
+	"OFDMA RA Support", // ofdma_ra_support
+	"Maximum A-MPDU Length Exponent Extension", // max_a_mpdu_len_exp_ext
+	NULL,
+	"A-MSDU Fragmentation Support", // a_msdu_frag_support
+	"Flexible TWT Schedule Support", // flexible_twt_sched_support
+	"Rx Control Frame to MultiBSS", // rx_ctl_frm_multibss
+
+	"BSRP BQRP A-MPDU Aggregation", // bsrp_bqrp_a_mpdu_agg
+	"QTP Support", // qtp_support
+	"BQR Support", // bqr_support
+	"SRP Responder Role", // sr_responder
+	"NDP Feedback Report Support", // ndp_feedback_report_support
+	"OPS Support", // ops_support
+	"A-MSDU in A-MPDU Support", // a_msdu_in_a_mpdu_support
+	"Multi-TID Aggregation TX Support", // multi_tid_agg_support
+	NULL,
+	NULL,
+
+	"HE Subchannel Selective Transmission Support", // subchannel_selective_xmit_support
+	"UL 2x996-tone RU Support", // ul_2_996_tone_ru_support
+	"OM Control UL MU Data Disable RX Support", // om_cntl_ul_mu_data_disable_rx_support
+	"HE Dynamic SM Power Save", // he_dynamic_sm_power_save
+	"Punctured Sounding Support", // he_punctured_sounding_support
+	"HT And VHT Trigger Frame RX Support", // he_ht_and_vht_trigger_frame_rx_support
+};
+
+static const char* he_phy_cap_str[] = {
+	// bits 0-7
+	NULL,
+	"40MHz in 2.4GHz band", // 40mhz_in_2_4ghz
+	"40 & 80MHz in the 5GHz band", // 40_80_in_5ghz
+	"160MHz in the 5GHz band", // 160_in_5ghz
+	"160/80+80MHz in the 5GHz band", // 160_80_80_in_5ghz
+	"242 tone RUs in the 2.4GHz band", // 242_tone_in_2_4ghz
+	"242 tone RUs in the 5GHz band", // 242_tone_in_5ghz
+	NULL,
+
+	// bits 8-23
+	"Punctured Preamble RX", // punc_preamble_rx 4-bits
+	NULL, 
+	NULL, 
+	NULL, 
+	"Device Class", // device_class
+	"LDPC Coding In Payload", // ldpc_coding_in_payload
+	"HE SU PPDU With 1x HE-LTF and 0.8us GI", // he_su_ppdu_with_1x_he_ltf_08us
+	"Midamble Rx Max NSTS", // midamble_rx_max_nsts 2-bits
+	NULL,
+	"NDP With 4x HE-LTF and 3.2us GI", // 2us
+	"STBC Tx <= 80 MHz", // stbc_tx_lt_80mhz
+	"STBC Rx <= 80 MHz", // stbc_rx_lt_80mhz
+	"Doppler Tx", // doppler_tx
+	"Doppler Rx", // doppler_rx
+	"Full Bandwidth UL MU-MIMO", // full_bw_ul_mu_mimo
+	"Partial Bandwidth UL MU-MIMO", // partial_bw_ul_mu_mimo
+
+	"DCM Max Constellation Tx", // dcm_max_const_tx 2-bits
+	NULL,
+	"DCM Max NSS Tx", // dcm_max_nss_tx
+	"DCM Max Constellation Rx", // dcm_max_const_rx 2-bits
+	NULL,
+	"DCM Max NSS Rx", // dcm_max_nss_tx
+	"Rx HE MU PPDU from Non-AP STA", // rx_he_mu_ppdu
+	"SU Beamformer", // su_beamformer
+	"SU Beamformee", // su_beamformee
+	"MU Beamformer", // mu_beamformer
+	"Beamformee STS <= 80 MHz", // beamformee_sts_lte_80mhz 3-bits
+	NULL, 
+	NULL,
+	"Beamformee STS > 80 MHz", // beamformee_sts_gt_80mhz 3-bits
+	NULL, 
+	NULL,
+
+	"Number Of Sounding Dimensions <= 80 MHz", // no_sounding_dims_lte_80 3-bits
+	NULL, 
+	NULL,
+	"Number Of Sounding Dimensions > 80 MHz", // no_sounding_dims_gt_80 3-bits
+	NULL, 
+	NULL,
+	"Ng = 16 SU Feedback", // ng_eq_16_su_fb
+	"Ng = 16 MU Feedback", // ng_eq_16_mu_fb
+	"Codebook Size SU Feedback", // codebook_size_su_fb
+	"Codebook Size MU Feedback", // codebook_size_mu_fb
+	"Triggered SU Beamforming Feedback", // trig_su_bf_fb
+	"Triggered MU Beamforming Feedback", // trig_mu_bf_fb
+	"Triggered CQI Feedback", // trig_cqi_fb
+	"Partial Bandwidth Extended Range", // partial_bw_er
+	"Partial Bandwidth DL MU-MIMO", // partial_bw_dl_mu_mimo
+	"PPE Threshold Present", // ppe_thres_present
+
+	"SRP-based SR Support", // srp_based_sr_sup
+	"Power Boost Factor ar Support", // pwr_bst_factor_ar_sup
+	"HE SU PPDU & HE MU PPDU w 4x HE-LTF & 0.8us GI", // he_su_ppdu_etc_gi
+	"Max Nc", // max_nc 3-bits
+	NULL, 
+	NULL,
+	"STBC Tx > 80 MHz", // stbc_tx_gt_80_mhz
+	"STBC Rx > 80 MHz", // stbc_rx_gt_80_mhz
+	"HE ER SU PPDU W 4x HE-LTF & 0.8us GI", // he_er_su_ppdu_4xxx_gi
+	"20 MHz In 40 MHz HE PPDU In 2.4GHz Band", // 20_mhz_in_40_in_2_4ghz
+	"20 MHz In 160/80+80 MHz HE PPDU", // 20_mhz_in_160_80p80_ppdu
+	"80 MHz In 160/80+80 MHz HE PPDU", // 80_mhz_in_160_80p80_ppdu
+	"HE ER SU PPDU W 1x HE-LTF & 0.8us GI", // he_er_su_ppdu_1xxx_gi
+	"Midamble Rx 2x & 1x HE-LTF", // midamble_rx_2x_1x_he_ltf
+	"DCM Max BW", // dcm_max_bw 2-bits
+	NULL,
+
+	"Longer Than 16 HE SIG-B OFDM Symbols Support", // longer_than_16_he_sigb_ofdm_sym_support
+	"Non-Triggered CQI Feedback", // non_triggered_feedback
+	"Tx 1024-QAM Support < 242-tone RU", // tx_1024_qam_support_lt_242_tone_ru
+	"Rx 1024-QAM Support < 242-tone RU", // rx_1024_qam_support_lt_242_tone_ru
+	"Rx Full BW SU Using HE MU PPDU With Compressed SIGB", // rx_full_bw_su_using_he_mu_ppdu_with_compressed_sigb
+	"Rx Full BW SU Using HE MU PPDU With Non-Compressed SIGB", // rx_full_bw_su_using_he_mu_ppdu_with_non_compressed_sigb
+	"Nominal Packet Padding", // nominal_packet_padding 2-bits
+	NULL,
+};
+
+// packet-ieee80211.c array of same name
+static const char* he_fragmentation_support_vals[] = {
+  "No support for dynamic fragmentation",
+  "Support for dynamic fragments in MPDUs or S-MPDUs",
+  "Support for dynamic fragments in MPDUs and S-MPDUs and up to 1 dyn frag in MSDUs...",
+  "Support for all types of dynamic fragments",
+};
+
+// from packet-ieee80211.c array of same name
+static const char* he_link_adaptation_support_vals[] = {
+  "No feedback if the STA does not provide HE MFB",
+  "Reserved",
+  "Unsolicited if the STA can receive and provide only unsolicited HE MFB",
+  "Both",
+};
+
+// from packet-ieee80211.c array of same name
+static const char* he_minimum_fragmentation_size_vals[] = {
+  "No restriction on minimum payload size",
+  "Minimum payload size of 128 bytes",
+  "Minimum payload size of 256 bytes",
+  "Minimum payload size of 512 bytes",
+};
+
+int he_mac_capa_to_str(const struct IE_HE_MAC* mac, unsigned int idx, char* s, size_t len)
+{
+
+	if (idx >= ARRAY_SIZE(he_mac_cap_str)){
+		return -EINVAL;
+	}
+
+	if (!he_mac_cap_str[idx]) {
+		return -ENOENT;
+	}
+
+	char tmpstr[32];
+	int ret;
+
+	switch (idx) {
+		case 3: // 4
+			// fragmentation support
+			return snprintf(s, len, "%s: %s (%d)", 
+					he_mac_cap_str[idx],
+					he_fragmentation_support_vals[mac->fragmentation_support], 
+					mac->fragmentation_support);
+
+		case 5:  // 6 7
+			// max frag msdu
+			ret = he_max_frag_msdus_base_to_str(mac->max_number_fragmented_msdus, tmpstr, sizeof(tmpstr));
+			if (ret < 0 ) {
+				return ret;
+			}
+			return snprintf(s, len, "%s: %s",
+					he_mac_cap_str[idx], tmpstr);
+
+		case 8: // 9
+			return snprintf(s, len, "%s: %s (%d)", 
+					he_mac_cap_str[idx],
+					he_minimum_fragmentation_size_vals[mac->min_fragment_size],
+					mac->min_fragment_size);
+
+		case 10: // 11
+			// min trigger frame mac
+			return snprintf(s, len, "%s (%d)", 
+					he_mac_cap_str[idx],
+					mac->trigger_frame_mac_padding_dur);
+
+		case 12: // 13 14
+			// multi tid
+			return snprintf(s, len, "%s: %d", 
+					he_mac_cap_str[idx],
+					mac->multi_tid_aggregation_support);
+
+		case 15: // 16
+			// he link adaptation
+			return snprintf(s, len, "%s: %s (%d)",
+					he_mac_cap_str[idx], 
+					he_link_adaptation_support_vals[mac->he_link_adaptation_support],
+					mac->he_link_adaptation_support);
+
+		case 27: // 28
+			// max ampdu len exponent exten
+			return snprintf(s, len, "%s (%d)",
+					he_mac_cap_str[idx], mac->max_a_mpdu_length_exponent_ext);
+
+		case 39: // 40 41
+			// multi-tid agg support
+			return snprintf(s, len, "%s (%d)",
+					he_mac_cap_str[idx], mac->multi_tid_aggregation_support);
+
+		default:
+			break;
+	}
+	return snprintf(s, len, "%s", he_mac_cap_str[idx]);
+}
+
+static const char* he_phy_device_class_vals[] = {
+  "Class A Device",
+  "Class B Device",
+};
+
+static const char* he_phy_midamble_rx_max_nsts_vals[] = {
+  "1 Space-Time Stream",
+  "2 Space-Time Streams",
+  "3 Space-Time Streams",
+  "4 Space-Time Streams",
+};
+
+static const char* he_phy_dcm_max_constellation_vals[] = {
+  "DCM is not supported",
+  "BPSK",
+  "QPSK",
+  "16-QAM",
+};
+
+static const char* he_phy_dcm_max_nss_vals[] = {
+  "1 Space-Time Stream",
+  "2 Space-Time Streams",
+};
+
+static const char* he_phy_nominal_packet_padding_vals[] = {
+  "0 µs for all Constellations",
+  "8 µs for all Constellations",
+  "16 µs for all Constellations",
+  "Reserved",
+};
+
+int he_phy_capa_to_str(const struct IE_HE_PHY* phy, unsigned int idx, char* s, size_t len)
+{
+
+	if (idx >= ARRAY_SIZE(he_phy_cap_str)){
+		return -EINVAL;
+	}
+
+	if (!he_phy_cap_str[idx]) {
+		return -ENOENT;
+	}
+
+	switch(idx)
+	{
+		case 8:
+			return snprintf(s, len, "%s: %d",
+					he_phy_cap_str[idx],
+					phy->punctured_preamble_rx);
+
+		case 12:
+			return snprintf(s, len, "%s: %s (%d)",
+					he_phy_cap_str[idx],
+					he_phy_device_class_vals[phy->device_class],
+					phy->device_class);
+
+		case 15: // 16
+			return snprintf(s, len, "%s: %s (%d)", 
+					he_phy_cap_str[idx], 
+					he_phy_midamble_rx_max_nsts_vals[phy->midamble_rx_max_nsts], 
+					phy->midamble_rx_max_nsts);
+
+		case 24: // 25
+			return snprintf(s, len, "%s: %s (%d)", 
+					he_phy_cap_str[idx], 
+					he_phy_dcm_max_constellation_vals[phy->dcm_max_constellation_tx],
+					phy->dcm_max_constellation_tx);
+
+		case 26:
+			return snprintf(s, len, "%s: %s (%d)", 
+					he_phy_cap_str[idx], 
+					he_phy_dcm_max_nss_vals[phy->dcm_max_nss_tx],
+					phy->dcm_max_nss_tx);
+
+		case 27: // 28
+			return snprintf(s, len, "%s: %s (%d)", 
+					he_phy_cap_str[idx], 
+					he_phy_dcm_max_constellation_vals[phy->dcm_max_constellation_rx],
+					phy->dcm_max_constellation_rx);
+
+		case 29:
+			return snprintf(s, len, "%s: %s (%d)", 
+					he_phy_cap_str[idx], 
+					he_phy_dcm_max_nss_vals[phy->dcm_max_nss_rx],
+					phy->dcm_max_nss_rx);
+
+		case 34: // 35 36
+			return snprintf(s, len, "%s: %d",
+					he_phy_cap_str[idx],
+					phy->beamformer_sts_lte_80mhz);
+
+		case 37: // 38 39
+			return snprintf(s, len, "%s: %d",
+					he_phy_cap_str[idx],
+					phy->beamformer_sts_gt_80mhz);
+
+		case 40: // 41 42
+			return snprintf(s, len, "%s: %d",
+					he_phy_cap_str[idx],
+					phy->number_of_sounding_dims_lte_80);
+
+		case 43: // 44 45
+			return snprintf(s, len, "%s: %d",
+					he_phy_cap_str[idx],
+					phy->number_of_sounding_dims_gt_80);
+
+		case 59: // 60 61
+			return snprintf(s, len, "%s: %d",
+					he_phy_cap_str[idx],
+					phy->max_nc);
+
+		case 70: // 71
+			return snprintf(s, len, "%s: %d",
+					he_phy_cap_str[idx],
+					phy->dcm_max_bw);
+
+		case 78: // 79
+			return snprintf(s, len, "%s: %s (%d)", 
+					he_phy_cap_str[idx], 
+					he_phy_nominal_packet_padding_vals[phy->nominal_packet_padding],
+					phy->nominal_packet_padding);
+
+		default:
+			break;
+	}
+
+	return snprintf(s, len, "%s", he_phy_cap_str[idx]);
+}
+
+static const char* he_operation_str[] = {
+	"Default PE Duration",  // 3 bits
+	NULL, NULL,
+	"TWT Required",
+	"TXOP Duration RTS Threshold", // 10 bits
+	NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 
+	"VHT Operation Information Present",
+	"Co-located BSS",
+	"ER SU Disable",
+	// 7 bits reserved
+	NULL, NULL, NULL, NULL, NULL, NULL, NULL, 
+
+	// bits 24-31
+	"BSS Color", // 6 bits
+	NULL, NULL, NULL, NULL, NULL, NULL, 
+	"Partial BSS Color",
+	"BSS Color Disabled",
+};
+
+int he_operation_to_str(const struct IE_HE_Operation* ie, unsigned int idx, char* s, size_t len)
+{
+	if (idx >= ARRAY_SIZE(he_operation_str)){
+		return -EINVAL;
+	}
+
+	if (!he_operation_str[idx]) {
+		return -ENOENT;
+	}
+
+	switch (idx) {
+		// default PE duration
+		case 0:
+			return snprintf(s, len, "%s: %d", 
+					he_operation_str[idx], 
+					ie->fields->default_pe_duration);
+
+		case 3:
+			return snprintf(s, len, "%s: %s",
+					he_operation_str[idx], 
+					ie->fields->twt_required ? "Required" : "Not required" );
+
+		case 4:
+			return snprintf(s, len, "%s: %d", 
+					he_operation_str[idx], 
+					ie->fields->txop_duration_rts_thresh);
+
+		case 24:
+			// bss color
+			return snprintf(s, len, "%s: 0x%02x", 
+					he_operation_str[idx],
+					ie->fields->bss_color);
+
+		default:
+			break;
+	}
+
+	return snprintf(s, len, "%s", he_operation_str[idx]);
+}
+
+void ie_print_he_capabilities(const struct IE_HE_Capabilities* ie)
+{
+	printf("\tHE capabilities:\n");
+	ie_print_he_capabilities_mac(ie->mac);
+	ie_print_he_capabilities_phy(ie->phy);
+}
+
+#define INDENT "\t\t\t"
+
+#define PRN(field, _idx)\
+	do {\
+		typeof (_idx) s_idx = (_idx);\
+		ret = STRING_FN(_struct, s_idx, s, sizeof(s));\
+		printf(INDENT " * %s\n", s);\
+	} while(0);
+
+#define PRNBOOL(field, _idx) \
+	do {\
+		typeof (_idx) s_idx = (_idx);\
+		if(_struct->field) {\
+			ret = STRING_FN(_struct, s_idx, s, sizeof(s));\
+			printf(INDENT " * %s\n", s);\
+		}\
+	} while(0);
+
+void ie_print_he_capabilities_mac(const struct IE_HE_MAC* mac)
+{
+	char s[128];
+	unsigned int bit;
+	int ret;
+
+#define STRING_FN he_mac_capa_to_str
+#define _struct mac
+
+	printf("\t\tHE MAC capabilities:\n");
+	// 0-7
+	bit = 0;
+	PRNBOOL(htc_he_support, bit++)
+	PRNBOOL(twt_requester_support,bit++)
+	PRNBOOL(twt_responder_support,bit++)
+	PRN(fragmentation_support,bit); bit+=2; // 2 bits
+	PRN(max_number_fragmented_msdus, bit); bit+=3; // 3 bits
+
+	// 8-14
+	PRN(min_fragment_size, bit); bit+=2; // 2 bits
+	PRN(trigger_frame_mac_padding_dur, bit); bit+=2;
+	PRN(multi_tid_aggregation_support, bit); bit+=3;
+	// bits 15,16
+	PRN(he_link_adaptation_support, bit); bit+=2;
+
+	// bits 16-23
+	bit = 17; // first field is at bit1
+	PRNBOOL(all_ack_support,bit++)
+	PRNBOOL(trs_support,bit++)
+	PRNBOOL(bsr_support,bit++)
+	PRNBOOL(broadcast_twt_support,bit++)
+	PRNBOOL(_32_bit_ba_bitmap_support,bit++)
+	PRNBOOL(mu_cascading_support,bit++)
+	PRNBOOL(ack_enabled_aggregation_support,bit++)
+
+	// bits 24-31
+	bit = 25; // bit 24 reserved
+	PRNBOOL(om_control_support,bit++)
+	PRNBOOL(ofdma_ra_support,bit++)
+	PRN(max_a_mpdu_length_exponent_ext,bit ); bit += 2; // 2 bits
+	PRNBOOL(a_msdu_fragmentation_support,bit++)
+	PRNBOOL(flexible_twt_schedule_support,bit++)
+	PRNBOOL(rx_control_frame_to_multibss,bit++)
+
+	// bits 32-39
+	PRNBOOL(bsrp_bqrp_a_mpdu_aggregation,bit++)
+	PRNBOOL(qtp_support,bit++)
+	PRNBOOL(bqr_support,bit++)
+	PRNBOOL(srp_responder,bit++)
+	PRNBOOL(ndp_feedback_report_support,bit++)
+	PRNBOOL(ops_support,bit++)
+	PRNBOOL(a_msdu_in_a_mpdu_support,bit++)
+
+	// bits 39-41
+	PRN(multi_tid_aggregation_support, bit); bit+= 3;
+
+	PRNBOOL(subchannel_selective_trans_support,bit++)
+	PRNBOOL(ul_2_996_tone_ru_support,bit++)
+	PRNBOOL(om_control_ul_mu_data_disable_rx_support,bit++)
+}
+
+void ie_print_he_capabilities_phy(const struct IE_HE_PHY* phy)
+{
+	char s[128];
+	unsigned int bit;
+	int ret;
+
+#undef STRING_FN
+#undef _struct
+#define _struct phy
+#define STRING_FN he_phy_capa_to_str
+
+	printf("\t\tHE PHY capabilities:\n");
+	// bit 0 reserved
+	bit = 1;
+	PRNBOOL(ch40mhz_channel_2_4ghz, bit++)
+	PRNBOOL(ch40_and_80_mhz_5ghz , bit++)
+	PRNBOOL(ch160_mhz_5ghz       , bit++)
+	PRNBOOL(ch160_80_plus_80_mhz_5ghz , bit++)
+	PRNBOOL(ch242_tone_rus_in_2_4ghz , bit++)
+	PRNBOOL(ch242_tone_rus_in_5ghz, bit++)
+	bit++; // bit 7 reserved
+
+	// bits 8-23
+	PRN(punctured_preamble_rx,bit); bit+=4; 
+	PRN(device_class,bit++)
+	PRNBOOL(ldpc_coding_in_payload, bit++);
+	PRNBOOL(he_su_ppdu_1x_he_ltf_08us, bit++);
+	PRN(midamble_rx_max_nsts, bit); bit+=2;
+	PRNBOOL(ndp_with_4x_he_ltf_32us, bit++);
+	PRNBOOL(stbc_tx_lt_80mhz, bit++);
+	PRNBOOL(stbc_rx_lt_80mhz, bit++);
+	PRNBOOL(doppler_tx, bit++);
+	PRNBOOL(doppler_rx, bit++);
+	PRNBOOL(full_bw_ul_mu_mimo, bit++);
+	PRNBOOL(partial_bw_ul_mu_mimo, bit++);
+
+	// 24-39
+	PRN(dcm_max_constellation_tx, bit); bit+=2;
+	PRNBOOL(dcm_max_nss_tx, bit++); // 1
+	PRN(dcm_max_constellation_rx, bit); bit+=2; // 2
+	PRNBOOL(dcm_max_nss_rx, bit++); // 1
+	PRNBOOL(rx_he_muppdu_from_non_ap, bit++); // 1
+	PRNBOOL(su_beamformer, bit++); // 1
+	PRNBOOL(su_beamformee, bit++); // 1
+	PRNBOOL(mu_beamformer, bit++); // 1
+	PRN(beamformer_sts_lte_80mhz, bit); bit+=3; // 3
+	PRN(beamformer_sts_gt_80mhz, bit); bit+=3;
+
+	// 40-55
+	PRN(number_of_sounding_dims_lte_80, bit); bit+=3;
+	PRN(number_of_sounding_dims_gt_80, bit); bit+=3; // 3
+	PRNBOOL(ng_eq_16_su_fb, bit++); // 1
+	PRNBOOL(ng_eq_16_mu_fb, bit++); // 1
+	PRNBOOL(codebook_size_eq_4_2_fb, bit++); // 1
+	PRNBOOL(codebook_size_eq_7_5_fb, bit++); // 1
+	PRNBOOL(triggered_su_beamforming_fb, bit++); // 1
+	PRNBOOL(triggered_mu_beamforming_fb, bit++); // 1
+	PRNBOOL(triggered_cqi_fb, bit++); // 1
+	PRNBOOL(partial_bw_extended_range, bit++); // 1
+	PRNBOOL(partial_bw_dl_mu_mimo, bit++); // 1
+	PRNBOOL(ppe_threshold_present, bit++);
+
+	// 56-71
+	PRNBOOL(srp_based_sr_support, bit++);
+	PRNBOOL(power_boost_factor_ar_support, bit++); // 1
+	PRNBOOL(he_su_ppdu_etc_gi, bit++); // 1
+	PRN(max_nc, bit); bit+=3; // 3
+	PRNBOOL(stbc_tx_gt_80_mhz, bit++); // 1
+	PRNBOOL(stbc_rx_gt_80_mhz, bit++); // 1
+	PRNBOOL(he_er_su_ppdu_4xxx_gi, bit++); // 1
+	PRNBOOL(_20mhz_in_40mhz_24ghz_band, bit++); // 1
+	PRNBOOL(_20mhz_in_160_80p80_ppdu, bit++); // 1
+	PRNBOOL(_80mgz_in_160_80p80_ppdu, bit++); // 1
+	PRNBOOL(he_er_su_ppdu_1xxx_gi, bit++); // 1
+	PRNBOOL(midamble_rx_2x_xxx_ltf, bit++); // 1
+	PRN(dcm_max_bw, bit); bit += 2;
+
+	// 72-87
+	PRNBOOL(longer_than_16_he_sigb_ofdm_symbol_support, bit++);
+	PRNBOOL(non_triggered_cqi_feedback, bit++); // 1
+	PRNBOOL(tx_1024_qam_242_tone_ru_support, bit++); // 1
+	PRNBOOL(rx_1024_qam_242_tone_ru_support, bit++); // 1
+	PRNBOOL(rx_full_bw_su_using_he_muppdu_w_compressed_sigb, bit++); // 1
+	PRNBOOL(rx_full_bw_su_using_he_muppdu_w_non_compressed_sigb, bit++); // 1
+	PRN(nominal_packet_padding, bit ); bit++; // 2
+
+#undef STRING_FN
+#undef _struct
+}
+
+void ie_print_he_operation(const struct IE_HE_Operation* sie)
+{
+	char s[128];
+	unsigned int bit;
+	int ret;
+
+#define STRING_FN he_operation_to_str
+#define _struct sie
+
+	printf("\tHE operation:\n");
+	printf("\t\tHE Operation Parameters:\n");
+	bit = 0;
+	PRN(default_pe_duration, bit);  bit += 3; 
+	PRN(twt_required, bit++);
+	PRN(txop_duration_rts_thresh, bit); bit += 10;
+	PRNBOOL(fields->vht_op_info_present, bit++);
+	PRNBOOL(fields->co_located_bss, bit++);
+	PRNBOOL(fields->er_su_disable, bit++);
+	bit += 7; // skip reserved bits
+
+	printf("\t\tHE BSS Color Information\n");
+	// bits 24-31
+	PRN(bss_color, bit); bit+= 6;
+	PRNBOOL(fields->partial_bss_color, bit++);
+	PRNBOOL(fields->bss_color_disabled, bit++);
+
+#undef STRING_FN
+#undef _struct
+}
+
diff --git a/ie_he.h b/ie_he.h
new file mode 100644
index 0000000..52c6243
--- /dev/null
+++ b/ie_he.h
@@ -0,0 +1,236 @@ 
+#ifndef IE_HE_H
+#define IE_HE_H
+
+#include <stdint.h>
+
+// I don't have the US$3000 to get the IEEE 80211.ax standard so I'm using
+// Wireshark's decode code. I'm putting HE decode into its own file to
+// carefully show the HE code from Wireshark.
+
+// the structure member names taken from Wireshark epan/dissectors/packet-ieee80211.c
+
+struct __attribute__((__packed__)) IE_HE_MAC
+{
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+	// 0-7
+	uint64_t htc_he_support : 1,
+	twt_requester_support : 1,
+	twt_responder_support : 1,
+	fragmentation_support : 2,
+	max_number_fragmented_msdus : 3,
+
+	// 8-14
+	min_fragment_size : 2,
+	trigger_frame_mac_padding_dur : 2,
+	multi_tid_aggregation_support : 3,
+
+	// 15,16
+	he_link_adaptation_support : 2,
+
+	// 17-23
+	all_ack_support : 1,
+	trs_support : 1,
+	bsr_support : 1,
+	broadcast_twt_support : 1,
+	_32_bit_ba_bitmap_support : 1,
+	mu_cascading_support : 1,
+	ack_enabled_aggregation_support : 1,
+
+	// 24-31
+	reserved_b24 : 1,
+	om_control_support : 1,
+	ofdma_ra_support : 1,
+	max_a_mpdu_length_exponent_ext : 2,
+	a_msdu_fragmentation_support : 1,
+	flexible_twt_schedule_support : 1,
+	rx_control_frame_to_multibss : 1,
+
+	// 32-38
+	bsrp_bqrp_a_mpdu_aggregation : 1,
+	qtp_support : 1,
+	bqr_support : 1,
+	srp_responder : 1,
+	ndp_feedback_report_support : 1,
+	ops_support : 1,
+	a_msdu_in_a_mpdu_support : 1,
+
+	// 39,40,41
+	multi_tid_aggregation_tx_support : 3,
+
+	// 42
+	subchannel_selective_trans_support : 1,
+	ul_2_996_tone_ru_support : 1,
+	om_control_ul_mu_data_disable_rx_support : 1,
+	reserved_b45: 1,
+	reserved_b46: 1,
+	reserved_b47: 1;
+#else
+#error TODO big endian
+#endif
+} ;
+
+struct __attribute__((__packed__)) IE_HE_PHY 
+{
+	// 0-7
+	uint8_t reserved_b0 : 1,
+	 ch40mhz_channel_2_4ghz : 1,
+	 ch40_and_80_mhz_5ghz : 1,
+	 ch160_mhz_5ghz : 1,
+	 ch160_80_plus_80_mhz_5ghz : 1,
+	 ch242_tone_rus_in_2_4ghz : 1,
+	 ch242_tone_rus_in_5ghz : 1,
+	 reserved_b7 : 1;
+
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+	// wireshark using uint16 for reasons I don't fully comprehend so when in
+	// Rome...  
+	// 8-23
+	uint16_t punctured_preamble_rx : 4,
+	 device_class : 1,
+	 ldpc_coding_in_payload : 1,
+	 he_su_ppdu_1x_he_ltf_08us : 1,
+	 midamble_rx_max_nsts : 2,
+	 ndp_with_4x_he_ltf_32us : 1,
+	 stbc_tx_lt_80mhz : 1,
+	 stbc_rx_lt_80mhz : 1,
+	 doppler_tx : 1,
+	 doppler_rx : 1,
+	 full_bw_ul_mu_mimo : 1,
+	 partial_bw_ul_mu_mimo : 1;
+
+	// 24-39
+	uint16_t dcm_max_constellation_tx : 2,
+	 dcm_max_nss_tx : 1,
+	 dcm_max_constellation_rx : 2,
+	 dcm_max_nss_rx : 1,
+	 rx_he_muppdu_from_non_ap : 1,
+	 su_beamformer : 1,
+	 su_beamformee : 1,
+	 mu_beamformer : 1,
+	 beamformer_sts_lte_80mhz : 3,
+	 beamformer_sts_gt_80mhz : 3;
+
+	// 40-55
+	uint16_t number_of_sounding_dims_lte_80 : 3,
+	 number_of_sounding_dims_gt_80 : 3,
+	 ng_eq_16_su_fb : 1,
+	 ng_eq_16_mu_fb : 1,
+	 codebook_size_eq_4_2_fb : 1,
+	 codebook_size_eq_7_5_fb : 1,
+	 triggered_su_beamforming_fb : 1,
+	 triggered_mu_beamforming_fb : 1,
+	 triggered_cqi_fb : 1,
+	 partial_bw_extended_range : 1,
+	 partial_bw_dl_mu_mimo : 1,
+	 ppe_threshold_present : 1;
+
+	// 56-71
+	uint16_t srp_based_sr_support : 1,
+	 power_boost_factor_ar_support : 1,
+	 he_su_ppdu_etc_gi : 1,
+	 max_nc : 3,
+	 stbc_tx_gt_80_mhz : 1,
+	 stbc_rx_gt_80_mhz : 1,
+	 he_er_su_ppdu_4xxx_gi : 1,
+	 _20mhz_in_40mhz_24ghz_band : 1,
+	 _20mhz_in_160_80p80_ppdu : 1,
+	 _80mgz_in_160_80p80_ppdu : 1,
+	 he_er_su_ppdu_1xxx_gi : 1,
+	 midamble_rx_2x_xxx_ltf : 1,
+	 dcm_max_bw : 2;
+
+	// 72-87
+	uint8_t longer_than_16_he_sigb_ofdm_symbol_support : 1,
+	 non_triggered_cqi_feedback : 1,
+	 tx_1024_qam_242_tone_ru_support : 1,
+	 rx_1024_qam_242_tone_ru_support : 1,
+	 rx_full_bw_su_using_he_muppdu_w_compressed_sigb : 1,
+	 rx_full_bw_su_using_he_muppdu_w_non_compressed_sigb : 1,
+	 nominal_packet_padding : 2,
+	 reserved_b80_b87;
+#else
+# error TODO big endian
+#endif
+} ;
+
+struct __attribute__((__packed__)) IE_HE_Operation_Fields
+{
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+	uint16_t default_pe_duration : 3,
+	 twt_required : 1,
+	 txop_duration_rts_thresh : 10,
+	 vht_op_info_present : 1,
+	 co_located_bss : 1;
+	uint8_t er_su_disable : 1,
+	 reserved_b17_b23 : 7;
+
+	uint8_t bss_color : 6,
+	 partial_bss_color : 1,
+	 bss_color_disabled : 1;
+
+	// HE MCS and NSS Set
+	// TODO
+
+#else
+# error TODO big endian
+#endif
+};
+
+#define IE_HE_CAPA_MAC_SIZE 6
+#define IE_HE_CAPA_PHY_SIZE 11
+#define IE_HE_CAPA_MCS_SIZE 4
+
+struct IE_HE_Capabilities
+{
+	// pointers into ie buf
+	const uint8_t* mac_capa;  // 6 bytes
+	const uint8_t* phy_capa;  // 11 bytes
+	const uint8_t* mcs_and_nss_set; // 4  bytes
+	const uint8_t* ppe_threshold; // 1+3*SS bytes
+
+	// HE Mac Capabilities
+	const struct IE_HE_MAC* mac;
+	const struct IE_HE_PHY* phy;
+
+	// HE MCS and NSS Set
+	// TODO
+
+	// PPE Thresholds
+	// Note this is a variable length field. Has an entry for each SS (Spatial
+	// Stream).  
+	// TODO
+};
+
+struct IE_HE_Operation
+{
+	// pointers into ie buf
+	const uint8_t* params;  // 3 bytes
+	const uint8_t* bss_color;  // 1 byte
+	const uint8_t* mcs_and_nss_set; // 2 bytes
+
+	struct IE_HE_Operation_Fields* fields;
+};
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+int ie_he_operation_init(struct IE_HE_Operation* ie, const uint8_t* data, uint8_t len);
+int ie_he_capabilities_init(struct IE_HE_Capabilities* ie, const uint8_t* data, uint8_t len);
+
+int he_max_frag_msdus_base_to_str(uint8_t max_frag_msdus_value, char* s, size_t len);
+int he_mac_capa_to_str(const struct IE_HE_MAC* sie, unsigned int idx, char* s, size_t len);
+int he_phy_capa_to_str(const struct IE_HE_PHY* sie, unsigned int idx, char* s, size_t len);
+int he_operation_to_str(const struct IE_HE_Operation* sie, unsigned int idx, char* s, size_t len);
+
+void ie_print_he_capabilities(const struct IE_HE_Capabilities* sie);
+void ie_print_he_capabilities_mac(const struct IE_HE_MAC* mac);
+void ie_print_he_capabilities_phy(const struct IE_HE_PHY* phy);
+void ie_print_he_operation(const struct IE_HE_Operation* sie);
+
+#ifdef __cplusplus
+} // end extern "C"
+#endif
+
+#endif
+
diff --git a/scan.c b/scan.c
index bfd39e4..a984e6a 100644
--- a/scan.c
+++ b/scan.c
@@ -2,6 +2,7 @@ 
 #include <errno.h>
 #include <string.h>
 #include <stdbool.h>
+#include <assert.h>
 
 #include <netlink/genl/genl.h>
 #include <netlink/genl/family.h>
@@ -11,6 +12,7 @@ 
 
 #include "nl80211.h"
 #include "iw.h"
+#include "ie_he.h"
 
 #define WLAN_CAPABILITY_ESS		(1<<0)
 #define WLAN_CAPABILITY_IBSS		(1<<1)
@@ -2011,6 +2013,74 @@  static void print_vendor(unsigned char len, unsigned char *data,
 	printf("\n");
 }
 
+enum nl80211_ie_extension
+{
+	NL80211_IE_EXT_HE_CAPABILITIES = 35,
+	NL80211_IE_EXT_HE_OPERATION = 36,
+	NL80211_IE_EXT_MU_EDCA_PARAM_SET = 38,
+	NL80211_IE_EXT_SPATIAL_REUSE_PARAM_SET = 39,
+};
+
+static void print_he_capabilities(const uint8_t type, uint8_t len, const uint8_t *data,
+		      const struct print_ies_data *ie_buffer)
+{
+	struct IE_HE_Capabilities ie;
+	int ret = ie_he_capabilities_init(&ie, data, len);
+	assert(ret==0);
+	if (ret==0) {
+		ie_print_he_capabilities(&ie);
+	}
+}
+
+static void print_he_operation(const uint8_t type, uint8_t len, const uint8_t *data,
+		      const struct print_ies_data *ie_buffer)
+{
+	struct IE_HE_Operation ie;
+	int ret = ie_he_operation_init(&ie, data, len);
+	assert(ret==0);
+	if (ret==0) {
+		ie_print_he_operation(&ie);
+	}
+}
+
+static const struct ie_print extension_ieprinters[] = {
+	[NL80211_IE_EXT_HE_CAPABILITIES] = { "HE Capabilities", print_he_capabilities, 25, 254, BIT(PRINT_SCAN), },
+	[NL80211_IE_EXT_HE_OPERATION] = { "HE Operation", print_he_operation, 6, 254, BIT(PRINT_SCAN), },
+};
+
+
+static void print_extension_ie(unsigned char len, unsigned char *data,
+			 bool unknown, enum print_ie_type ptype)
+{
+	struct print_ies_data ie_buffer = {
+		.ie = data,
+		.ielen = len };
+
+	printf("%s len=%u ", __func__, len);
+	for(int i = 0; i < len; i++)
+		printf(" %.02x", data[i]);
+	printf("\n");
+
+	uint8_t ext_id = data[0];
+	if (ext_id < ARRAY_SIZE(extension_ieprinters) &&
+		    extension_ieprinters[ext_id].name &&
+		    extension_ieprinters[ext_id].flags & BIT(ptype)) 
+	{
+		// note the length of the extension IE is not re-encoded after the EXT
+		// ID so we pass the original length minus the extension id
+		print_ie(&extension_ieprinters[ext_id],
+			 ext_id, len-1, data+1, &ie_buffer);
+	}
+	else if (unknown) {
+		int i;
+		printf("\tUnknown Extension IE (%d):", ext_id);
+		for (i=0; i<data[1]; i++)
+			printf(" %.2x", data[2+i]);
+		printf("\n");
+	}
+
+}
+
 void print_ies(unsigned char *ie, int ielen, bool unknown,
 	       enum print_ie_type ptype)
 {
@@ -2026,6 +2096,8 @@  void print_ies(unsigned char *ie, int ielen, bool unknown,
 				 ie[0], ie[1], ie + 2, &ie_buffer);
 		} else if (ie[0] == 221 /* vendor */) {
 			print_vendor(ie[1], ie + 2, unknown, ptype);
+		} else if (ie[0] == 255 /* extension */) {
+			print_extension_ie(ie[1], ie + 2, unknown, ptype);
 		} else if (unknown) {
 			int i;
 
@@ -2103,7 +2175,7 @@  static void print_capa_non_dmg(__u16 capa)
 		printf(" ImmediateBACK");
 }
 
-static int print_bss_handler(struct nl_msg *msg, void *arg)
+int print_bss_handler(struct nl_msg *msg, void *arg)
 {
 	struct nlattr *tb[NL80211_ATTR_MAX + 1];
 	struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg));
@@ -2126,8 +2198,9 @@  static int print_bss_handler(struct nl_msg *msg, void *arg)
 	int show = params->show_both_ie_sets ? 2 : 1;
 	bool is_dmg = false;
 
-	nla_parse(tb, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
+	int ret = nla_parse(tb, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
 		  genlmsg_attrlen(gnlh, 0), NULL);
+	assert(ret >= 0);
 
 	if (!tb[NL80211_ATTR_BSS]) {
 		fprintf(stderr, "bss info missing!\n");
diff --git a/test_scan.c b/test_scan.c
new file mode 100644
index 0000000..1985109
--- /dev/null
+++ b/test_scan.c
@@ -0,0 +1,213 @@ 
+#include <stdio.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+
+#include <netlink/genl/genl.h>
+#include <netlink/genl/family.h>
+#include <netlink/genl/ctrl.h>
+#include <netlink/msg.h>
+#include <netlink/attr.h>
+
+#include "nl80211.h"
+#include "iw.h"
+
+#define PTR_FREE(p) do { free(p); (p)=NULL; } while(0)
+#define PTR_ASSIGN(dst,src) do { (dst)=(src); (src)=NULL; } while(0)
+
+// from scan.c
+struct scan_params {
+	bool unknown;
+	enum print_ie_type type;
+	bool show_both_ie_sets;
+};
+
+extern int print_bss_handler(struct nl_msg *msg, void *arg);
+
+static struct nl_msg* msg_encode(uint8_t* buf, size_t buf_len)
+{
+//	DBG("%s\n", __func__);
+
+	// can I rebuild an nl_msg containing buf as a payload?
+	struct nl_msg* msg = nlmsg_alloc();
+
+	int nl80211_id = 0;
+
+	void* p = genlmsg_put(msg, NL_AUTO_PORT, NL_AUTO_SEQ, 
+						nl80211_id, 
+						0, 
+						NLM_F_DUMP, NL80211_CMD_NEW_SCAN_RESULTS, 0);
+	(void)p;
+
+	// have to parse the blob containing the nlattr into individual nlattrs so
+	// we can put them back into the msg
+	struct nlattr* attr = (struct nlattr*)buf;
+	size_t attr_len = buf_len;
+	struct nlattr* tb_msg[NL80211_ATTR_MAX + 1];
+	int err = nla_parse(tb_msg, NL80211_ATTR_MAX, attr, attr_len, NULL);
+	if (err<0) {
+		fprintf(stderr, "%s nla_parse failed err=%d\n", __func__, err);
+		nlmsg_free(msg);
+		return NULL;
+	}
+
+	for (size_t i=0 ; i<NL80211_ATTR_MAX ; i++ ) {
+		if (tb_msg[i]) {
+			err = nla_put(msg, 
+							nla_type(tb_msg[i]), 
+							nla_len(tb_msg[i]), 
+							(void *)nla_data(tb_msg[i]));
+			if (err<0) {
+				fprintf(stderr, "%s nla_put failed err=%d\n", __func__, err);
+				nlmsg_free(msg);
+				return NULL;
+			}
+		}
+	}
+
+	// now let's try taking it apart again
+	struct nlmsghdr *hdr = nlmsg_hdr(msg);
+	struct genlmsghdr* gnlh = (struct genlmsghdr*)nlmsg_data(hdr);
+
+	attr = genlmsg_attrdata(gnlh,0);
+	attr_len = genlmsg_attrlen(gnlh,0);
+
+	printf("%s buf=%p buflen=%zu attr=%p len=%zu\n", __func__, 
+			(void*)buf,
+			buf_len, 
+			(void*)attr,
+			attr_len);
+//	hex_dump(__func__, (unsigned char*)attr, len);
+
+//	peek_nla_attr(tb_msg, NL80211_ATTR_MAX);
+
+	return msg;
+}
+
+// stubs
+
+__u32 listen_events(struct nl80211_state *state,
+		    const int n_waits, const __u32 *waits)
+{
+	(void)state;
+	(void)n_waits;
+	(void)waits;
+	return 0;
+}
+
+int handle_cmd(struct nl80211_state *state, enum id_input idby,
+	       int argc, char **argv)
+{
+	(void)state;
+	(void)idby;
+	(void)argc;
+	(void)argv;
+	return 0;
+}
+
+void register_handler(int (*handler)(struct nl_msg *, void *), void *data)
+{
+	(void)handler;
+	(void)data;
+}
+
+static int load_file(const char* filename, uint8_t** p_buf, size_t* p_size)
+{
+	struct stat stats;
+	int err =  stat(filename, &stats);
+	if (err<0) {
+		fprintf(stderr, "stat file \"%s\" failed err=%d %s\n", filename, errno, strerror(errno));
+		return err;
+	}
+
+	uint8_t* buf = malloc(stats.st_size);
+	if (!buf) {
+		return -ENOMEM;
+	}
+
+	int fd = open(filename, O_RDONLY);
+	if (fd<0) {
+		PTR_FREE(buf);
+		fprintf(stderr, "open file \"%s\" failed err=%d %s\n", filename, errno, strerror(errno));
+		return -errno;
+	}
+
+	ssize_t count = read(fd, buf, stats.st_size);
+	if (count < 0) {
+		PTR_FREE(buf);
+		fprintf(stderr, "read file \"%s\" failed err=%d %s\n", filename, errno, strerror(errno));
+		return -errno;
+	}
+	close(fd);
+
+	if (count != stats.st_size) {
+		PTR_FREE(buf);
+		return -EIO;
+	}
+
+	PTR_ASSIGN(*p_buf, buf);
+	*p_size = stats.st_size;
+	return 0;
+}
+
+static int test_bss_handler( struct nl_msg* msg)
+{
+	nl_msg_dump(msg, stdout);
+
+	struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg));
+	struct nlattr* tb_msg[NL80211_ATTR_MAX + 1];
+	int err = nla_parse(tb_msg, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
+		  genlmsg_attrlen(gnlh, 0), NULL);
+	if (err < 0) {
+		fprintf(stderr, "nla_parse failed err=%d\n", err);
+		return err;
+	}
+
+	if (!tb_msg[NL80211_ATTR_BSS]) {
+		fprintf(stderr, "%s bss info missing!\n", __func__);
+		return -EINVAL;
+	}
+
+	struct scan_params scan_params;
+	memset(&scan_params, 0, sizeof(scan_params));
+	scan_params.unknown = true;
+	scan_params.type = PRINT_SCAN;
+
+	err = print_bss_handler(msg, (void*)&scan_params);
+
+	return 0;
+}
+
+int main(int argc, char*argv[] )
+{
+	for (int i=1 ; i<argc ; i++) {
+		uint8_t* buf;
+		size_t size;
+
+		int err = load_file(argv[i], &buf, &size);
+		if (err < 0) {
+			fprintf(stderr, "failed to load file \"%s\"; err=%d\n", argv[i], err);
+			continue;
+		}
+
+		struct nl_msg* msg = msg_encode(buf, size);
+		if (!msg) {
+			// encode logs error
+			goto clean;
+		}
+
+		test_bss_handler(msg);
+clean:
+		nlmsg_free(msg);
+		PTR_FREE(buf);
+	}
+
+	return 0;
+}
+
+
-- 
2.24.1