From 3f26fce1c134576934e4d91f927eeeea4efec7c9 Mon Sep 17 00:00:00 2001 From: Przemyslaw Bida Date: Mon, 18 Dec 2023 15:38:44 +0100 Subject: [PATCH] [simulation] Adding simulation tests framework for tcat. This commit adds simulation framework for tcat based on posix udp sockets. --- examples/platforms/simulation/ble.c | 157 +++++++++++++++++- .../simulation/platform-simulation.h | 29 +++- examples/platforms/simulation/system.c | 7 + script/cmake-build | 1 + tests/scripts/expect/cli-tcat.exp | 61 +++++++ third_party/mbedtls/mbedtls-config.h | 5 +- tools/tcat_ble_client/bbtc.py | 14 +- .../tcat_ble_client/ble/ble_stream_secure.py | 23 ++- tools/tcat_ble_client/ble/udp_stream.py | 55 ++++++ 9 files changed, 325 insertions(+), 27 deletions(-) create mode 100755 tests/scripts/expect/cli-tcat.exp mode change 100644 => 100755 tools/tcat_ble_client/bbtc.py create mode 100644 tools/tcat_ble_client/ble/udp_stream.py diff --git a/examples/platforms/simulation/ble.c b/examples/platforms/simulation/ble.c index 2fe3c64535d5..d48922ac9516 100644 --- a/examples/platforms/simulation/ble.c +++ b/examples/platforms/simulation/ble.c @@ -26,50 +26,191 @@ * POSSIBILITY OF SUCH DAMAGE. */ +#include "platform-simulation.h" + +#include + +#include +#include #include +#include "openthread/error.h" +#include "utils/code_utils.h" + +#define PLAT_BLE_MSG_DATA_MAX 2048 +static uint8_t sBleBuffer[PLAT_BLE_MSG_DATA_MAX]; + +static int sFd = -1; + +static const uint16_t kPortBase = 10000; +static uint16_t sPort = 0; +struct sockaddr_in sSockaddr; + +static void initFds(void) +{ + int fd; + int one = 1; + struct sockaddr_in sockaddr; + + memset(&sockaddr, 0, sizeof(sockaddr)); + + sPort = (uint16_t)(kPortBase + gNodeId); + sockaddr.sin_family = AF_INET; + sockaddr.sin_port = htons(sPort); + sockaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); + + otEXPECT_ACTION((fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) != -1, perror("socket(sFd)")); + + otEXPECT_ACTION(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) != -1, + perror("setsockopt(sFd, SO_REUSEADDR)")); + otEXPECT_ACTION(setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)) != -1, + perror("setsockopt(sFd, SO_REUSEPORT)")); + + otEXPECT_ACTION(bind(fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) != -1, perror("bind(sFd)")); + + // Fd is successfully initialized. + sFd = fd; + +exit: + if (sFd == -1) + { + exit(EXIT_FAILURE); + } +} + +static void deinitFds(void) +{ + if (sFd != -1) + { + close(sFd); + sFd = -1; + } +} + otError otPlatBleEnable(otInstance *aInstance) { OT_UNUSED_VARIABLE(aInstance); - return OT_ERROR_NOT_IMPLEMENTED; + initFds(); + return OT_ERROR_NONE; } otError otPlatBleDisable(otInstance *aInstance) { + deinitFds(); OT_UNUSED_VARIABLE(aInstance); - return OT_ERROR_NOT_IMPLEMENTED; + return OT_ERROR_NONE; } otError otPlatBleGapAdvStart(otInstance *aInstance, uint16_t aInterval) { OT_UNUSED_VARIABLE(aInstance); OT_UNUSED_VARIABLE(aInterval); - return OT_ERROR_NOT_IMPLEMENTED; + return OT_ERROR_NONE; } otError otPlatBleGapAdvStop(otInstance *aInstance) { OT_UNUSED_VARIABLE(aInstance); - return OT_ERROR_NOT_IMPLEMENTED; + return OT_ERROR_NONE; } otError otPlatBleGapDisconnect(otInstance *aInstance) { OT_UNUSED_VARIABLE(aInstance); - return OT_ERROR_NOT_IMPLEMENTED; + return OT_ERROR_NONE; } otError otPlatBleGattMtuGet(otInstance *aInstance, uint16_t *aMtu) { OT_UNUSED_VARIABLE(aInstance); - OT_UNUSED_VARIABLE(aMtu); - return OT_ERROR_NOT_IMPLEMENTED; + *aMtu = PLAT_BLE_MSG_DATA_MAX - 1; + return OT_ERROR_NONE; } otError otPlatBleGattServerIndicate(otInstance *aInstance, uint16_t aHandle, const otBleRadioPacket *aPacket) +{ + OT_UNUSED_VARIABLE(aInstance); + OT_UNUSED_VARIABLE(aHandle); + + ssize_t rval; + otError error = OT_ERROR_NONE; + + otEXPECT_ACTION(sFd != -1, error = OT_ERROR_INVALID_STATE); + rval = sendto(sFd, (const char *)aPacket->mValue, aPacket->mLength, 0, (struct sockaddr *)&sSockaddr, + sizeof(sSockaddr)); + if (rval == -1) + { + perror("BLE simulation sendto failed."); + } + +exit: + return error; +} + +void platformBleDeinit(void) { deinitFds(); } + +void platformBleUpdateFdSet(fd_set *aReadFdSet, fd_set *aWriteFdSet, struct timeval *aTimeout, int *aMaxFd) +{ + OT_UNUSED_VARIABLE(aTimeout); + OT_UNUSED_VARIABLE(aWriteFdSet); + + if (aReadFdSet != NULL && sFd != -1) + { + FD_SET(sFd, aReadFdSet); + + if (aMaxFd != NULL && *aMaxFd < sFd) + { + *aMaxFd = sFd; + } + } +} + +void platformBleProcess(otInstance *aInstance, const fd_set *aReadFdSet, const fd_set *aWriteFdSet) +{ + OT_UNUSED_VARIABLE(aWriteFdSet); + + otEXPECT(sFd != -1); + + if (FD_ISSET(sFd, aReadFdSet)) + { + socklen_t len = sizeof(sSockaddr); + ssize_t rval; + memset(&sSockaddr, 0, sizeof(sSockaddr)); + rval = recvfrom(sFd, sBleBuffer, sizeof(sBleBuffer), 0, (struct sockaddr *)&sSockaddr, &len); + if (rval > 0) + { + otBleRadioPacket myPacket; + myPacket.mValue = sBleBuffer; + myPacket.mLength = (uint16_t)rval; + myPacket.mPower = 0; + otPlatBleGattServerOnWriteRequest( + aInstance, 0, + &myPacket); // TODO consider passing otPlatBleGattServerOnWriteRequest as a callback function + } + else if (rval == 0) + { + // socket is closed, which should not happen + assert(false); + } + else if (errno != EINTR && errno != EAGAIN) + { + perror("recvfrom BLE simulation failed"); + exit(EXIT_FAILURE); + } + } +exit: + return; +} + +OT_TOOL_WEAK void otPlatBleGattServerOnWriteRequest(otInstance *aInstance, + uint16_t aHandle, + const otBleRadioPacket *aPacket) { OT_UNUSED_VARIABLE(aInstance); OT_UNUSED_VARIABLE(aHandle); OT_UNUSED_VARIABLE(aPacket); - return OT_ERROR_NOT_IMPLEMENTED; + assert(false); + /* In case of rcp there is a problem with linking to otPlatBleGattServerOnWriteRequest + * which is available in FTD/MTD library. + */ } diff --git a/examples/platforms/simulation/platform-simulation.h b/examples/platforms/simulation/platform-simulation.h index 343821cef193..d4407686a935 100644 --- a/examples/platforms/simulation/platform-simulation.h +++ b/examples/platforms/simulation/platform-simulation.h @@ -159,7 +159,7 @@ void platformRadioDeinit(void); void platformRadioReceive(otInstance *aInstance, uint8_t *aBuf, uint16_t aBufLength); /** - * Updates the file descriptor sets with file descriptors used by the radio driver. + * Updates the file descriptor sets with file descriptors used by the BLE radio driver. * * @param[in,out] aReadFdSet A pointer to the read file descriptors. * @param[in,out] aWriteFdSet A pointer to the write file descriptors. @@ -341,4 +341,31 @@ void platformInfraIfProcess(otInstance *aInstance, const fd_set *aReadFdSet, con #endif // OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE +/** + * Shuts down the BLE service used by OpenThread. + * + */ +void platformBleDeinit(void); + +/** + * Updates the file descriptor sets with file descriptors used by the radio driver. + * + * @param[in,out] aReadFdSet A pointer to the read file descriptors. + * @param[in,out] aWriteFdSet A pointer to the write file descriptors. + * @param[in,out] aTimeout A pointer to the timeout. + * @param[in,out] aMaxFd A pointer to the max file descriptor. + * + */ +void platformBleUpdateFdSet(fd_set *aReadFdSet, fd_set *aWriteFdSet, struct timeval *aTimeout, int *aMaxFd); + +/** + * Performs BLE driver processing. + * + * @param[in] aInstance The OpenThread instance structure. + * @param[in] aReadFdSet A pointer to the read file descriptors. + * @param[in] aWriteFdSet A pointer to the write file descriptors. + * + */ +void platformBleProcess(otInstance *aInstance, const fd_set *aReadFdSet, const fd_set *aWriteFdSet); + #endif // PLATFORM_SIMULATION_H_ diff --git a/examples/platforms/simulation/system.c b/examples/platforms/simulation/system.c index 07af5891b919..2abe892e30aa 100644 --- a/examples/platforms/simulation/system.c +++ b/examples/platforms/simulation/system.c @@ -296,6 +296,10 @@ void otSysProcessDrivers(otInstance *aInstance) platformInfraIfUpdateFdSet(&read_fds, &write_fds, &max_fd); #endif +#if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE + platformBleUpdateFdSet(&read_fds, &write_fds, &timeout, &max_fd); +#endif + if (otTaskletsArePending(aInstance)) { timeout.tv_sec = 0; @@ -308,6 +312,9 @@ void otSysProcessDrivers(otInstance *aInstance) { platformUartProcess(); platformRadioProcess(aInstance, &read_fds, &write_fds); +#if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE + platformBleProcess(aInstance, &read_fds, &write_fds); +#endif } else if (errno != EINTR) { diff --git a/script/cmake-build b/script/cmake-build index 5ed2e4e4bc38..f8e227c5de40 100755 --- a/script/cmake-build +++ b/script/cmake-build @@ -109,6 +109,7 @@ OT_POSIX_SIM_COMMON_OPTIONS=( "-DOT_SRP_CLIENT=ON" "-DOT_SRP_SERVER=ON" "-DOT_UPTIME=ON" + "-DOT_BLE_TCAT=ON" ) readonly OT_POSIX_SIM_COMMON_OPTIONS diff --git a/tests/scripts/expect/cli-tcat.exp b/tests/scripts/expect/cli-tcat.exp new file mode 100755 index 000000000000..4b0566100577 --- /dev/null +++ b/tests/scripts/expect/cli-tcat.exp @@ -0,0 +1,61 @@ +#!/usr/bin/expect -f +# +# Copyright (c) 2022, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +source "tests/scripts/expect/_common.exp" + +spawn_node 1 "cli" + +switch_node 1 +send "tcat start\n" +expect_line "Done" + +spawn python "/home/pbida/Repos/ncs/modules/lib/openthread/tools/tcat_ble_client/bbtc.py" --simulation 1 --cert_path "tools/tcat_ble_client/auth" +set py_client "$spawn_id" +expect_line "Done" +send "commission\n" +expect_line "\tTYPE:\tRESPONSE_W_STATUS" +expect_line "\tVALUE:\t0x00" + +send "thread start\n" +expect_line "\tTYPE:\tRESPONSE_W_STATUS" +expect_line "\tVALUE:\t0x00" + +send "exit\n" +expect eof + +switch_node 1 +send "tcat stop\n" +expect_line "Done" + +send "networkkey\n" +expect_line "fda7c771a27202e232ecd04cf934f476" +expect_line "Done" + +wait_for "state" "leader" +expect_line "Done" diff --git a/third_party/mbedtls/mbedtls-config.h b/third_party/mbedtls/mbedtls-config.h index a3e06ac63ece..29aa49e7a39b 100644 --- a/third_party/mbedtls/mbedtls-config.h +++ b/third_party/mbedtls/mbedtls-config.h @@ -94,6 +94,7 @@ #if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE #define MBEDTLS_SSL_KEEP_PEER_CERTIFICATE +#define MBEDTLS_GCM_C #endif #ifdef MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED @@ -132,7 +133,9 @@ #define MBEDTLS_MEMORY_BUFFER_ALLOC_C #endif -#if OPENTHREAD_CONFIG_COAP_SECURE_API_ENABLE +#if OPENTHREAD_CONFIG_BLE_TCAT_ENABLE +#define MBEDTLS_SSL_MAX_CONTENT_LEN 2000 /**< Maxium fragment length in bytes */ +#elif OPENTHREAD_CONFIG_COAP_SECURE_API_ENABLE #define MBEDTLS_SSL_MAX_CONTENT_LEN 900 /**< Maxium fragment length in bytes */ #else #define MBEDTLS_SSL_MAX_CONTENT_LEN 768 /**< Maxium fragment length in bytes */ diff --git a/tools/tcat_ble_client/bbtc.py b/tools/tcat_ble_client/bbtc.py old mode 100644 new mode 100755 index fa2ebbb24904..6a9eaaa4f57a --- a/tools/tcat_ble_client/bbtc.py +++ b/tools/tcat_ble_client/bbtc.py @@ -34,6 +34,7 @@ from ble.ble_connection_constants import BBTC_SERVICE_UUID, BBTC_TX_CHAR_UUID, \ BBTC_RX_CHAR_UUID, SERVER_COMMON_NAME from ble.ble_stream import BleStream +from ble.udp_stream import UdpStream from ble.ble_stream_secure import BleStreamSecure from ble import ble_scanner from cli.cli import CLI @@ -47,10 +48,12 @@ async def main(): parser = argparse.ArgumentParser(description='Device parameters') parser.add_argument('--debug', help='Enable debug logs', action='store_true') + parser.add_argument('--cert_path', help='Path to certificate chain and key', action='store', default='auth') group = parser.add_mutually_exclusive_group() group.add_argument('--mac', type=str, help='Device MAC address', action='store') group.add_argument('--name', type=str, help='Device name', action='store') group.add_argument('--scan', help='Scan all available devices', action='store_true') + group.add_argument('--simulation', help='Connect to simulation node id', action='store') args = parser.parse_args() if args.debug: @@ -63,12 +66,11 @@ async def main(): if not (device is None): print(f'Connecting to {device}') - ble_stream = await BleStream.create(device.address, BBTC_SERVICE_UUID, BBTC_TX_CHAR_UUID, BBTC_RX_CHAR_UUID) - ble_sstream = BleStreamSecure(ble_stream) + ble_sstream = BleStreamSecure(device) ble_sstream.load_cert( - certfile=path.join('auth', 'commissioner_cert.pem'), - keyfile=path.join('auth', 'commissioner_key.pem'), - cafile=path.join('auth', 'ca_cert.pem'), + certfile=path.join(args.cert_path, 'commissioner_cert.pem'), + keyfile=path.join(args.cert_path, 'commissioner_key.pem'), + cafile=path.join(args.cert_path, 'ca_cert.pem'), ) print('Setting up secure channel...') @@ -101,6 +103,8 @@ async def get_device_by_args(args): elif args.scan: tcat_devices = await ble_scanner.scan_tcat_devices() device = select_device_by_user_input(tcat_devices) + elif args.simulation: + device = UdpStream("127.0.0.1", int(args.simulation)) return device diff --git a/tools/tcat_ble_client/ble/ble_stream_secure.py b/tools/tcat_ble_client/ble/ble_stream_secure.py index 9d15b7945368..329659f269e8 100644 --- a/tools/tcat_ble_client/ble/ble_stream_secure.py +++ b/tools/tcat_ble_client/ble/ble_stream_secure.py @@ -30,15 +30,14 @@ import ssl import logging -from .ble_stream import BleStream logger = logging.getLogger(__name__) class BleStreamSecure: - def __init__(self, ble_stream: BleStream): - self.ble_stream = ble_stream + def __init__(self, stream): + self.stream = stream self.ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) self.incoming = ssl.MemoryBIO() self.outgoing = ssl.MemoryBIO() @@ -67,12 +66,12 @@ async def do_handshake(self, hostname): # SSLWantWrite means ssl wants to send data over the link, # but might need a receive first except ssl.SSLWantWriteError: - output = await self.ble_stream.recv(4096) + output = await self.stream.recv(4096) if output: self.incoming.write(output) data = self.outgoing.read() if data: - await self.ble_stream.send(data) + await self.stream.send(data) await asyncio.sleep(0.1) # SSLWantRead means ssl wants to receive data from the link, @@ -80,8 +79,8 @@ async def do_handshake(self, hostname): except ssl.SSLWantReadError: data = self.outgoing.read() if data: - await self.ble_stream.send(data) - output = await self.ble_stream.recv(4096) + await self.stream.send(data) + output = await self.stream.recv(4096) if output: self.incoming.write(output) await asyncio.sleep(0.1) @@ -89,14 +88,14 @@ async def do_handshake(self, hostname): async def send(self, bytes): self.ssl_object.write(bytes) encode = self.outgoing.read(4096) - await self.ble_stream.send(encode) + await self.stream.send(encode) async def recv(self, buffersize, timeout=1): end_time = asyncio.get_event_loop().time() + timeout - data = await self.ble_stream.recv(buffersize) + data = await self.stream.recv(buffersize) while not data and asyncio.get_event_loop().time() < end_time: await asyncio.sleep(0.1) - data = await self.ble_stream.recv(buffersize) + data = await self.stream.recv(buffersize) if not data: logger.warning('No response when response expected.') return b'' @@ -108,10 +107,10 @@ async def recv(self, buffersize, timeout=1): break # if recv called before entire message was received from the link except ssl.SSLWantReadError: - more = await self.ble_stream.recv(buffersize) + more = await self.stream.recv(buffersize) while not more: await asyncio.sleep(0.1) - more = await self.ble_stream.recv(buffersize) + more = await self.stream.recv(buffersize) self.incoming.write(more) return decode diff --git a/tools/tcat_ble_client/ble/udp_stream.py b/tools/tcat_ble_client/ble/udp_stream.py new file mode 100644 index 000000000000..3447b4d2d632 --- /dev/null +++ b/tools/tcat_ble_client/ble/udp_stream.py @@ -0,0 +1,55 @@ +""" + Copyright (c) 2024, The OpenThread Authors. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holder nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +""" + +from itertools import count, takewhile +from typing import Iterator +import logging +import time +from asyncio import sleep +import socket + +logger = logging.getLogger(__name__) + + +class UdpStream: + BASE_PORT = 10000 + def __init__(self, address, node_id): + self.__receive_buffer = b'' + self.__last_recv_time = None + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.address = (address, self.BASE_PORT + node_id) + + async def send(self, data): + logger.debug(f'sending {data}') + self.socket.sendto(data, self.address) + return len(data) + + async def recv(self, bufsize): + message = self.socket.recv(bufsize) + logger.debug(f'retrieved {message}') + return message