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
State New
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
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