diff --git a/distribution/Makefile b/distribution/Makefile index 59accda..124da28 100644 --- a/distribution/Makefile +++ b/distribution/Makefile @@ -1,6 +1,6 @@ .PHONY: all clean distclean deb scp -VERSION=1.0.64 +VERSION=1.0.65 DIST=U18.04 FBASE=micronets-gw SDIRS=../filesystem diff --git a/filesystem/opt/micronets-gw/bin/ifupdown.sh b/filesystem/opt/micronets-gw/bin/ifupdown.sh index d78248d..66ea4b1 100755 --- a/filesystem/opt/micronets-gw/bin/ifupdown.sh +++ b/filesystem/opt/micronets-gw/bin/ifupdown.sh @@ -66,6 +66,23 @@ config_iptables_bridge_uplink() { /sbin/sysctl -w net.ipv4.ip_forward=1 } +# This is a temporary work-around to enable the forwarding of DPP Presence Announcements +# from the wifi driver into hostapd. The driver-level commands to enable the forwarding of +# these wifi broadcast packets (NL80211_EXT_FEATURE_MULTICAST_REGISTRATIONS) in the +# ATH9K driver isn't currently included in a lot of kernel distros (for Rasp Pi, kernel +# version 5.10+ is required) +enable_wifi_monitor_mode() { + WIPHY_DEV=phy0 + WIPHY_MON_DEV=wifimon0 + if ifconfig $WIPHY_MON_DEV > /dev/null; then + debug_log "Monitor mode already enabled on $WIPHY_DEV (monitor device $WIPHY_MON_DEV)" + else + debug_log "Enabling monitor mode on $WIPHY_DEV (monitor device $WIPHY_MON_DEV)" + iw phy $WIPHY_DEV interface add $WIPHY_MON_DEV type monitor + ifconfig $WIPHY_MON_DEV up + fi +} + dump_iptables() { iptables -L -v iptables -L -v --table nat @@ -93,6 +110,8 @@ else debug_log "completed /etc/init.d/openvswitch-switch start." fi +enable_wifi_monitor_mode + if [ "${MODE}" = "start" ]; then eval OVS_EXTRA=\"${IF_OVS_EXTRA}\" diff --git a/micronets-gw-service/app/dpp_handler.py b/micronets-gw-service/app/dpp_handler.py index 6d0a11c..445baa4 100644 --- a/micronets-gw-service/app/dpp_handler.py +++ b/micronets-gw-service/app/dpp_handler.py @@ -111,36 +111,93 @@ async def send_dpp_onboard_event_delayed(event_name, delay, reason=None, termina else: psk = None passphrase = dev_psk - dpp_auth_init_cmd = HostapdAdapter.DPPAuthInitCommand(self.dpp_configurator_id, qrcode_id, self.ssid, - akms, psk=psk, passphrase=passphrase, freq=self.freq) - - self.pending_onboard = {"micronet":micronet, "device": device, "onboard_params": onboard_params} - asyncio.ensure_future(self.send_dpp_onboard_event(micronet, device, DPPHandler.EVENT_ONBOARDING_STARTED, - f"DPP Started (issuing \"{dpp_auth_init_cmd.get_command_string()}\")")) - await self.hostapd_adapter.send_command(dpp_auth_init_cmd) - result = await dpp_auth_init_cmd.get_response() - logger.info(f"{__name__}: Auth Init result: {result}") - - async def onboard_timeout_handler(): - await asyncio.sleep(DPPHandler.DPP_ONBOARD_TIMEOUT_S) - if self.pending_onboard: - logger.info(f"{__name__}: Onboarding TIMED OUT (after {DPPHandler.DPP_ONBOARD_TIMEOUT_S} seconds)") - pend_micronet = self.pending_onboard['micronet'] - pend_device = self.pending_onboard['device'] - await self.send_dpp_onboard_event(pend_micronet, pend_device, - DPPHandler.EVENT_ONBOARDING_FAILED, - f"Onboarding timed out (after {DPPHandler.DPP_ONBOARD_TIMEOUT_S} seconds)") - self.pending_onboard = None + + if ";V:2;" in qrcode_uri: + # Perform DPP V2 onboarding (set bootstrapping info for DPP Chirp/Presence Announcement + dpp_bootstrap_set_cmd = HostapdAdapter.DPPBootstrapSet(self.dpp_configurator_id, qrcode_id, self.ssid, + akms, psk=psk, passphrase=passphrase) + await self.hostapd_adapter.send_command(dpp_bootstrap_set_cmd) + result = await dpp_bootstrap_set_cmd.get_response() + logger.info(f"{__name__}: Bootstrap Set result: {result}") + if await dpp_bootstrap_set_cmd.was_successful(): + logger.info(f"{__name__}: Successfully set credentials for URI {qrcode_uri}") + return '', 200 + else: + logger.info(f"{__name__}: Could not set credentials for URI {qrcode_uri}") + return f"Could not set DPP V2 credentials for given URI ({result})", 400 + else: + # Perform DPP V1 onboarding (send Auth Init) + dpp_auth_init_cmd = HostapdAdapter.DPPAuthInitCommand(self.dpp_configurator_id, qrcode_id, self.ssid, + akms, psk=psk, passphrase=passphrase, freq=self.freq) + + self.pending_onboard = {"micronet":micronet, "device": device, "onboard_params": onboard_params} + asyncio.ensure_future(self.send_dpp_onboard_event(micronet, device, DPPHandler.EVENT_ONBOARDING_STARTED, + f"DPP Started (issuing \"{dpp_auth_init_cmd.get_command_string()}\")")) + await self.hostapd_adapter.send_command(dpp_auth_init_cmd) + result = await dpp_auth_init_cmd.get_response() + logger.info(f"{__name__}: Auth Init result: {result}") + + async def onboard_timeout_handler(): + await asyncio.sleep(DPPHandler.DPP_ONBOARD_TIMEOUT_S) + if self.pending_onboard: + logger.info(f"{__name__}: Onboarding TIMED OUT (after {DPPHandler.DPP_ONBOARD_TIMEOUT_S} seconds)") + pend_micronet = self.pending_onboard['micronet'] + pend_device = self.pending_onboard['device'] + await self.send_dpp_onboard_event(pend_micronet, pend_device, + DPPHandler.EVENT_ONBOARDING_FAILED, + f"Onboarding timed out (after {DPPHandler.DPP_ONBOARD_TIMEOUT_S} seconds)") + self.pending_onboard = None + else: + logger.info(f"{__name__}: Onboarding completed before timeout (< {DPPHandler.DPP_ONBOARD_TIMEOUT_S} seconds)") + + if await dpp_auth_init_cmd.was_successful(): + self.pending_timeout_task = asyncio.ensure_future(onboard_timeout_handler()) else: - logger.info(f"{__name__}: Onboarding completed before timeout (< {DPPHandler.DPP_ONBOARD_TIMEOUT_S} seconds)") + asyncio.ensure_future(self.send_dpp_onboard_event(micronet, device, DPPHandler.EVENT_ONBOARDING_FAILED, + f"DPP Authorization failed {dpp_auth_init_cmd.get_command_string()} returned: ({result})")) + self.pending_onboard = None + return '', 200 + + async def reprovision_device(self, micronet_id, device_id,): + logger.info(f"DPPHandler.reprovision_device(micronet '{micronet_id}', device '{device_id}'')") + conf_model = get_conf_model() + + # Make sure an pending updates have been processed + await conf_model.update_conf_now() + + micronet = conf_model.check_micronet_reference(micronet_id) + device = conf_model.check_device_reference(micronet_id, device_id) + + if not self.hostapd_adapter.is_cli_connected(): + return "Hostapd CLI is not connected", 500 + + if not self.hostapd_adapter.is_cli_ready(): + return "Hostapd CLI is not ready (hostapd is probably not running)", 500 + + logger.info(f"DPPHandler.onboard_device: Issuing DPP reprovisioning commands for device '{device_id}' in micronet '{micronet_id}...") + + dev_psk = device.get("psk") + # For now, the psk field is dual-purpose. For WPA2, a <64char "psk" will be converted into a PSK internally + # So send it through as a PSK and a passphrase + if len(dev_psk) == 64: + psk = dev_psk + passphrase = None + else: + psk = None + passphrase = dev_psk - if await dpp_auth_init_cmd.was_successful(): - self.pending_timeout_task = asyncio.ensure_future(onboard_timeout_handler()) + dpp_reprovision_params_cmd = HostapdAdapter.DPPSetDPPConfigParamsCommand(self.dpp_configurator_id, self.ssid, + ["psk"], psk=psk, passphrase=passphrase) + await self.hostapd_adapter.send_command(dpp_reprovision_params_cmd) + result = await dpp_reprovision_params_cmd.get_response() + logger.info(f"{__name__}: Result of setting reprovision params: {result}") + + if await dpp_reprovision_params_cmd.was_successful(): + logger.info(f"{__name__}: Successfully set reprovisioning credentials for micronet/device {micronet_id}/{device_id}") + return '', 200 else: - asyncio.ensure_future(self.send_dpp_onboard_event(micronet, device, DPPHandler.EVENT_ONBOARDING_FAILED, - f"DPP Authorization failed {dpp_auth_init_cmd.get_command_string()} returned: ({result})")) - self.pending_onboard = None - return '', 200 + logger.info(f"{__name__}: Failed to set reprovisioning credentials for micronet/device {micronet_id}/{device_id}") + return f"Could not set DPP V2 reprovisioning credentials for micronet/device {micronet_id}/{device_id}", 400 async def handle_hostapd_cli_event(self, event): logger.info(f"DPPHandler.handle_hostapd_cli_event({event})") diff --git a/micronets-gw-service/app/gateway_service_api.py b/micronets-gw-service/app/gateway_service_api.py index cb9b987..1a205f5 100644 --- a/micronets-gw-service/app/gateway_service_api.py +++ b/micronets-gw-service/app/gateway_service_api.py @@ -380,15 +380,22 @@ async def onboard_device (micronet_id, device_id): check_micronet_id (micronet_id, request.path) check_device_id (device_id, request.path) top_level = await request.get_json () - logger.info(f"onboad_device: top_level: {top_level}") + logger.info(f"onboard_device: top_level: {top_level}") check_for_unrecognized_entries (top_level, ['dpp']) dpp_obj = top_level['dpp'] - logger.info(f"onboad_device: dpp_obj: {dpp_obj}") check_for_unrecognized_entries(dpp_obj, ['uri','akms']) uri = check_field (dpp_obj, 'uri', str, True) akms = check_akms (dpp_obj, 'akms', True) return await get_dpp_handler().onboard_device (micronet_id, device_id, top_level) +@app.route (api_prefix + '/micronets//devices//reconfigure', methods=['PUT']) +async def reconfig_device (micronet_id, device_id): + micronet_id = micronet_id.lower () + device_id = device_id.lower () + check_micronet_id (micronet_id, request.path) + check_device_id (device_id, request.path) + return await get_dpp_handler().reprovision_device (micronet_id, device_id) + valid_akms = ("psk", "dpp", "sae") def check_akms (container, field_name, required): diff --git a/micronets-gw-service/app/hostapd_adapter.py b/micronets-gw-service/app/hostapd_adapter.py index 58899e8..8a5da1a 100644 --- a/micronets-gw-service/app/hostapd_adapter.py +++ b/micronets-gw-service/app/hostapd_adapter.py @@ -256,7 +256,6 @@ async def send_command(self, command): return command - class PingCLICommand(HostapdCLICommand): def __init__ (self, event_loop=asyncio.get_event_loop()): super().__init__(event_loop) @@ -483,6 +482,112 @@ async def was_successful(self): await self.get_response() return self.success + class DPPBootstrapSet(HostapdCLICommand): + def __init__ (self, configurator_id, qrcode_id, ssid, akms, psk=None, passphrase=None, + event_loop=asyncio.get_event_loop()): + super().__init__(event_loop) + self.configurator_id = configurator_id + self.qrcode_id = qrcode_id + self.ssid = ssid + self.psk = psk + self.passphrase = passphrase + self.akms = akms + self.success = False + self.passphrase_asciihex = None + + def get_command_string(self): + ssid_asciihex = self.ssid.encode("ascii").hex() + + # dpp_bootstrap_set 1 conf=sta-psk ssid= psk=<64 hex chars) configurator=1 conn_status=0 group_id=micronet-01 + cmd = f"dpp_bootstrap_set {self.qrcode_id} ssid={ssid_asciihex} configurator={self.configurator_id}" + + # Currently allowed configs: psk, sae, dpp, psk+sae, dpp+sae, dpp+psk+sae + # (see dpp_configuration_alloc in src/common/dpp.c of hostap sources) + akm_str = "" + if 'dpp' in self.akms: + akm_str += "+dpp" + if 'psk' in self.akms: + if not (self.psk or self.passphrase): + raise Exception(f"'psk' included in AKMS but no PSK or passphrase provided") + akm_str += "+psk" + if 'sae' in self.akms: + if not self.passphrase: + raise Exception(f"'sae' included in AKMS but no passphrase provided") + akm_str += "+sae" + if len(akm_str) == 0: + raise Exception(f"No valid akms elements found (akms: {self.akms})") + # Note: akm_str will have an extra "+" at the front + cmd += f" conf=sta-{akm_str[1:]}" + + if self.psk: + cmd += f" psk={self.psk}" + if self.passphrase: + passphrase_asciihex = self.passphrase.encode("ascii").hex() + cmd += f" pass={passphrase_asciihex}" + return cmd + + async def process_response_data(self, response): + try: + self.success = "OK" in response + finally: + await super().process_response_data(response) + + async def was_successful(self): + await self.get_response() + return self.success + + class DPPSetDPPConfigParamsCommand(HostapdCLICommand): + def __init__ (self, configurator_id, ssid, akms, psk=None, passphrase=None, + event_loop=asyncio.get_event_loop()): + super().__init__(event_loop) + self.configurator_id = configurator_id + self.ssid = ssid + self.psk = psk + self.passphrase = passphrase + self.akms = akms + self.success = False + + def get_command_string(self): + ssid_asciihex = self.ssid.encode("ascii").hex() + # set dpp_configurator_params "conf=sta-psk ssid= pass= conn_status=0 group_id=micronet-01" + cmd = f"set dpp_configurator_params \"conn_status=0 ssid={ssid_asciihex}" + + # Currently allowed configs: psk, sae, dpp, psk+sae, dpp+sae, dpp+psk+sae + # (see dpp_configuration_alloc in src/common/dpp.c of hostap sources) + akm_str = "" + if 'dpp' in self.akms: + akm_str += "+dpp" + if 'psk' in self.akms: + if not (self.psk or self.passphrase): + raise Exception(f"'psk' included in AKMS but no PSK or passphrase provided") + akm_str += "+psk" + if 'sae' in self.akms: + if not self.passphrase: + raise Exception(f"'sae' included in AKMS but no passphrase provided") + akm_str += "+sae" + if len(akm_str) == 0: + raise Exception(f"No valid akms elements found (akms: {self.akms})") + # Note: akm_str will have an extra "+" at the front + cmd += f" conf=sta-{akm_str[1:]}" + + if self.psk: + cmd += f" psk={self.psk}" + if self.passphrase: + passphrase_asciihex = self.passphrase.encode("ascii").hex() + cmd += f" pass={passphrase_asciihex}" + cmd += '"' + return cmd + + async def process_response_data(self, response): + try: + self.success = "OK" in response + finally: + await super().process_response_data(response) + + async def was_successful(self): + await self.get_response() + return self.success + class ReloadPSKCLICommand(HostapdCLICommand): def __init__ (self, event_loop=asyncio.get_event_loop()): super().__init__(event_loop) diff --git a/micronets-gw-service/config.py b/micronets-gw-service/config.py index a6e7886..0096ead 100644 --- a/micronets-gw-service/config.py +++ b/micronets-gw-service/config.py @@ -130,6 +130,9 @@ class WirelessGatewayDebugConfig (WirelessGatewayConfig): LOGGING_LEVEL = logging.DEBUG LOGFILE_MODE = 'w' # clears the log at startup +class WirelessGatewayDebugConfigNoLogfile (WirelessGatewayDebugConfig): + LOGFILE_PATH = None + class WirelessGatewayDebugConfigWithWebsocket (WirelessGatewayDebugConfig, WirelessGatewayConfigWithWebsocket): pass