[ble] improve BLE link handling, related to TLS session closing, and callbacks (#13240)

This fixes issues where the closing of the TLS session or sudden closing of the BLE link
did not always lead to the required behavior. Changes:

 - detail the API for the callback otHandleBleSecureConnect() including valid state
   combinations and expected moments of calling.
 - introduce a new internal state kClosing for the BLE link. Tracking the "is closing" state
   avoids making duplicate platform function calls or duplicate callbacks.
 - if a TLS handshake is ongoing and the BLE link goes down, mTls state is now also properly
   cleaned up.
 - if the TLS session closes (for whatever reason), the BLE link is now also torn down.
   The 'Disconnect()' call is used to achieve this. When needed, guard time is used
   to properly close TLS - e.g. sending the Alert and let the BLE link transmit that
   beforing closing the link.
 - the simulation BLE platform had an issue that the closing handshake caused the simulated
   BLE link to be immediately 'connected' again. This is fixed by introducing a state var
   sIsAdvertising, draining any pending UDP data of the simulated BLE link, and checking
   sIsAdvertising before accepting a new BLE connection.
 - avoid calling platform BLE advertisement-enable function while the BleSecure link is
   still active/closing up. Request BLE advertising again after final disconnect.
 - code readability improvements.

Unit tests are expanded to test more details of the flow.
This commit is contained in:
Esko Dijk
2026-07-16 10:39:15 -07:00
committed by Jonathan Hui
parent df8c385387
commit 6677bdd762
8 changed files with 212 additions and 54 deletions
+23 -9
View File
@@ -49,6 +49,7 @@ static uint8_t sBleBuffer[PLAT_BLE_MSG_DATA_MAX];
static int sFd = -1;
static bool sIsConnected = false;
static bool sIsDisconnecting = false;
static bool sIsAdvertising = false;
static bool sIsEnabled = false;
static const uint16_t kPortBase = 10000;
@@ -115,6 +116,7 @@ otError otPlatBleEnable(otInstance *aInstance)
sIsEnabled = true;
sIsConnected = false;
sIsDisconnecting = false;
sIsAdvertising = false;
}
return OT_ERROR_NONE;
}
@@ -138,6 +140,7 @@ otError otPlatBleGapAdvStart(otInstance *aInstance, uint16_t aInterval)
return OT_ERROR_INVALID_STATE;
}
otLogDebgPlat("BLE adv start (interval %u)", aInterval);
sIsAdvertising = true;
return OT_ERROR_NONE;
}
@@ -149,6 +152,7 @@ otError otPlatBleGapAdvStop(otInstance *aInstance)
return OT_ERROR_INVALID_STATE;
}
otLogDebgPlat("BLE adv stop");
sIsAdvertising = false;
return OT_ERROR_NONE;
}
@@ -159,6 +163,7 @@ otError otPlatBleGapDisconnect(otInstance *aInstance)
{
return OT_ERROR_INVALID_STATE;
}
otLogDebgPlat("BLE GAP disconnect");
// Only flag the disconnection here. The 'disconnected' event is delivered asynchronously
// by platformBleProcess() (via otPlatBleGapOnDisconnected), per API contract.
sIsDisconnecting = true;
@@ -222,15 +227,19 @@ void platformBleProcess(otInstance *aInstance, const fd_set *aReadFdSet, const f
otEXPECT(sFd != -1);
// Deliver a pending disconnection (requested earlier via otPlatBleGapDisconnect)
// Deliver a pending disconnection (requested earlier via otPlatBleGapDisconnect).
if (sIsDisconnecting)
{
sIsConnected = false;
otPlatBleGapOnDisconnected(aInstance, 0);
sIsDisconnecting = false;
}
// Drain any remaining data and drop it: prevent stale data from triggering a new connection later on.
while (recvfrom(sFd, sBleBuffer, sizeof(sBleBuffer), MSG_DONTWAIT, NULL, NULL) >= 0)
{
}
if (FD_ISSET(sFd, aReadFdSet))
sIsConnected = false;
sIsDisconnecting = false;
otPlatBleGapOnDisconnected(aInstance, 0);
}
else if (FD_ISSET(sFd, aReadFdSet))
{
socklen_t len = sizeof(sSockaddr);
ssize_t rval;
@@ -242,7 +251,12 @@ void platformBleProcess(otInstance *aInstance, const fd_set *aReadFdSet, const f
if (!sIsConnected)
{
sIsConnected = true;
// Only accept a new connection while advertising. This helps to ignore trailing data from a
// closing TCAT session until the device is ready again for a new session.
otEXPECT(sIsAdvertising);
sIsConnected = true;
sIsAdvertising = false; // per API contract, advertising stops once a client connects
otLogDebgPlat("BLE client connected");
otPlatBleGapOnConnected(aInstance, 0);
}
@@ -314,7 +328,7 @@ otError otPlatBleGapAdvSetData(otInstance *aInstance, uint8_t *aAdvertisementDat
OT_UNUSED_VARIABLE(aInstance);
OT_UNUSED_VARIABLE(aAdvertisementData);
OT_UNUSED_VARIABLE(aAdvertisementLen);
if (!sIsEnabled)
if (!sIsEnabled || sIsAdvertising)
{
return OT_ERROR_INVALID_STATE;
}
@@ -326,7 +340,7 @@ otError otPlatBleGapAdvUpdateData(otInstance *aInstance, uint8_t *aAdvertisement
OT_UNUSED_VARIABLE(aInstance);
OT_UNUSED_VARIABLE(aAdvertisementData);
OT_UNUSED_VARIABLE(aAdvertisementLen);
if (!sIsEnabled)
if (!sIsEnabled || !sIsAdvertising)
{
return OT_ERROR_INVALID_STATE;
}
+19 -4
View File
@@ -70,12 +70,27 @@ extern "C" {
*/
/**
* Pointer to call when ble secure connection state changes.
* Pointer to a callback function invoked whenever the BLE Secure connection state changes.
*
* The connection includes two layers that are reported independently: the underlying BLE link
* (@p aBleConnectionOpen) and the TLS session carried over it (@p aConnected). Both arguments
* report the current state of their respective layer (without disclosing which one(s) changed).
*
* A TLS session cannot exist without a BLE link to carry it, so @p aConnected being TRUE always
* implies @p aBleConnectionOpen being TRUE. The three resulting states are:
*
* - (aBleConnectionOpen=FALSE, aConnected=FALSE): Fully disconnected; no BLE link and no TLS session.
* - (aBleConnectionOpen=TRUE, aConnected=FALSE): BLE link is up; TLS session not (yet) established.
* - (aBleConnectionOpen=TRUE, aConnected=TRUE): BLE link is up and the TLS session is established.
*
* The callback is invoked exactly once per change of the (@p aBleConnectionOpen, @p aConnected) pair
* and is never invoked twice in a row with identical argument values. If the BLE link drops while a
* TLS session is active, the TLS session is treated as closed at that same instant: a single call with
* (FALSE, FALSE) is made.
*
* @param[in] aInstance A pointer to an OpenThread instance.
* @param[in] aConnected TRUE, if a secure connection was established, FALSE otherwise.
* @param[in] aBleConnectionOpen TRUE if a BLE connection was established to carry a TLS data stream, FALSE
* otherwise.
* @param[in] aConnected TRUE if a secure TLS session is established, FALSE otherwise.
* @param[in] aBleConnectionOpen TRUE if a BLE link is open to carry the TLS data stream, FALSE otherwise.
* @param[in] aContext A pointer to arbitrary context information.
*/
typedef void (*otHandleBleSecureConnect)(otInstance *aInstance,
+1 -1
View File
@@ -52,7 +52,7 @@ extern "C" {
*
* @note This number versions both OpenThread platform and user APIs.
*/
#define OPENTHREAD_API_VERSION (612)
#define OPENTHREAD_API_VERSION (613)
/**
* @addtogroup api-instance
+47 -25
View File
@@ -57,6 +57,7 @@ BleSecure::BleSecure(Instance &aInstance)
, mTransmitTask(aInstance)
, mBleState(kStopped)
, mIsBleAdvRequested(false)
, mTlsConnected(false)
, mMtuSize(kInitialMtuSize)
{
}
@@ -124,6 +125,7 @@ void BleSecure::Stop(void)
mTls.Close();
Get<MeshCoP::TcatAgent>().Stop();
mTlsConnected = false;
mTransmitQueue.DequeueAndFreeAll();
@@ -173,29 +175,34 @@ exit:
void BleSecure::Disconnect(void)
{
if (mTls.IsConnected())
mTls.Disconnect(); // always call: to include cases where a TLS handshake is ongoing (not completed yet).
if (mTlsConnected)
{
mTls.Disconnect();
mConnectCallback.InvokeIfSet(&GetInstance(), false, mBleState == kConnected);
mTlsConnected = false;
}
if (mBleState == kConnected)
// If TLS is fully done/closed, then disconnect the BLE link right here. Otherwise, let
// HandleTlsConnectEvent() later close the BLE link after all TLS data related to the teardown
// has been sent/received.
if (!mTls.IsConnectionActive() && mBleState == kConnected)
{
// Request the platform to close the BLE connection. When the platform signals completion
// (asynchronously) it calls otPlatBleGapOnDisconnected() -> #HandleBleDisconnected(), which
// in turn calls this method again - now with mBleState no longer kConnected - to invoke below
// mConnectCallback and update the advertisement data. We therefore return early here when
// the platform accepted the request, to avoid invoking the callback (and updating the
// advertisement data) twice. If the platform did not start the disconnection, no completion
// callback will follow and we handle mConnectCallback directly below.
VerifyOrExit(otPlatBleGapDisconnect(&GetInstance()) != kErrorNone);
mBleState = kClosing;
// Request the platform to close the BLE connection asap. If it succeeds, the callback will be invoked
// asynchronously via otPlatBleGapOnDisconnected().
if (otPlatBleGapDisconnect(&GetInstance()) != kErrorNone)
{
// If it fails, finalize the disconnection here, including callback.
mBleState = kNotAdvertising;
mConnectCallback.InvokeIfSet(&GetInstance(), false, false);
}
}
mConnectCallback.InvokeIfSet(&GetInstance(), false, false);
// Update advertisement data
IgnoreError(NotifyAdvertisementChanged());
exit:
return;
if (mBleState != kClosing && mBleState != kConnected)
{
IgnoreError(NotifyAdvertisementChanged()); // Update advertisement data now since we might restart advertising
IgnoreError(SetRequestedBleAdvertisementsState());
}
}
Error BleSecure::NotifyAdvertisementChanged(void)
@@ -238,10 +245,15 @@ void BleSecure::NotifySendAdvertisements(bool aSendAdvertisements)
// performs platform calls to start or stop BLE advertisements as requested, and if successful
// update mBleState to reflect actual state of kAdvertising / kNotAdvertising.
// Note: only errors from otPlatBleGap...() calls are returned. In case of unsuitable state to
// make such platform calls, kErrorNone is returned and no action is taken.
Error BleSecure::SetRequestedBleAdvertisementsState(void)
{
Error error = kErrorNone;
// Must not make GapAdv platform calls when TLS is still active.
VerifyOrExit(!mTls.IsConnectionActive());
// Must not make GapAdv platform calls when kStopped, or kConnected.
if (mIsBleAdvRequested && mBleState == kNotAdvertising)
{
@@ -423,12 +435,19 @@ void BleSecure::HandleBleDisconnected(uint16_t aConnectionId)
{
OT_UNUSED_VARIABLE(aConnectionId);
mBleState = kNotAdvertising; // per otPlatBleGapAdvStart() API, advertising stopped already when client connected.
mTls.Disconnect(); // idempotent; tears down TLS if still active, no-op otherwise
mTlsConnected = false;
if (mBleState == kClosing || mBleState == kConnected)
{
mConnectCallback.InvokeIfSet(&GetInstance(), false, false);
}
mBleState = kNotAdvertising; // per otPlatBleGapAdvStart() API, advertising did stop already when client connected.
mMtuSize = kInitialMtuSize;
Disconnect(); // Stop TLS connection and update advertisement data
// Resume advertising (or fulfill a different advertising state requested while a client was connected).
// Note: if TLS teardown still ongoing, the below won't restart advertising here. This follows after teardown.
IgnoreError(NotifyAdvertisementChanged()); // Update advertisement data now since we might restart advertising
IgnoreError(SetRequestedBleAdvertisementsState());
}
@@ -458,6 +477,7 @@ void BleSecure::HandleTlsConnectEvent(MeshCoP::Tls::ConnectEvent aEvent)
{
mReceivedMessage = Get<MessagePool>().Allocate(Message::kTypeBle);
}
if (mReceivedMessage == nullptr)
{
err = kErrorNoBufs;
@@ -473,17 +493,20 @@ void BleSecure::HandleTlsConnectEvent(MeshCoP::Tls::ConnectEvent aEvent)
LogWarn("Rejected TCAT Commissioner, error: %s", ErrorToString(err));
ExitNow();
}
mTlsConnected = true;
mConnectCallback.InvokeIfSet(&GetInstance(), true, true);
}
else
else /* any kDisconnected... event */
{
FreeMessage(mReceivedMessage);
mReceivedMessage = nullptr;
FreeMessage(mSendMessage);
mSendMessage = nullptr;
Get<MeshCoP::TcatAgent>().Disconnected();
}
mConnectCallback.InvokeIfSet(&GetInstance(), aEvent == MeshCoP::Tls::kConnected, mBleState == kConnected);
Disconnect(); // invoke callback and close BLE link (if not already closed).
}
exit:
return;
@@ -595,7 +618,6 @@ void BleSecure::HandleTlsReceive(uint8_t *aBuf, uint16_t aLength)
// BleSecure is disconnected but not stopped here, it
// must remain active in advertising state and must be
// ready to receive a next TCAT commissioner.
Disconnect();
ExitNow();
}
+2
View File
@@ -347,6 +347,7 @@ private:
kAdvertising = 1, // Ble secure is advertising.
kConnected = 2, // Ble secure is connected (so not advertising).
kNotAdvertising = 3, // Ble secure is started but not advertising.
kClosing = 4, // Ble secure connection is closing (so not advertising).
};
typedef otBleRadioPacket RadioPacket;
@@ -387,6 +388,7 @@ private:
uint8_t mPacketBuffer[kPacketBufferSize];
BleState mBleState;
bool mIsBleAdvRequested;
bool mTlsConnected;
uint16_t mMtuSize;
};
+4
View File
@@ -844,6 +844,7 @@ OT_TOOL_WEAK otError otPlatSetMcuPowerState(otInstance *aInstance, otPlatMcuPowe
uint8_t sPlatBleLastAdvSetData[OT_TCAT_ADVERTISEMENT_MAX_LEN];
uint16_t sPlatBleLastAdvSetDataLen = 0;
bool sPlatBleAdvertising = false;
otError otPlatBleEnable(otInstance *aInstance)
{
@@ -854,6 +855,7 @@ otError otPlatBleEnable(otInstance *aInstance)
otError otPlatBleDisable(otInstance *aInstance)
{
OT_UNUSED_VARIABLE(aInstance);
sPlatBleAdvertising = false;
return OT_ERROR_NONE;
}
@@ -871,12 +873,14 @@ otError otPlatBleGapAdvStart(otInstance *aInstance, uint16_t aInterval)
{
OT_UNUSED_VARIABLE(aInstance);
OT_UNUSED_VARIABLE(aInterval);
sPlatBleAdvertising = true;
return OT_ERROR_NONE;
}
otError otPlatBleGapAdvStop(otInstance *aInstance)
{
OT_UNUSED_VARIABLE(aInstance);
sPlatBleAdvertising = false;
return OT_ERROR_NONE;
}
+1
View File
@@ -65,6 +65,7 @@ extern "C" {
#endif
extern uint8_t sPlatBleLastAdvSetData[OT_TCAT_ADVERTISEMENT_MAX_LEN];
extern uint16_t sPlatBleLastAdvSetDataLen;
extern bool sPlatBleAdvertising;
#ifdef __cplusplus
}
#endif
+115 -15
View File
@@ -80,6 +80,26 @@
#define COMM_XPAN_ID {0xde, 0xad, 0x00, 0xbe, 0xef, 0x00, 0xca, 0xfe}
#define COMM_XPAN_ID_ALT {0xef, 0x13, 0x98, 0xc2, 0xfd, 0x50, 0x4b, 0x67}
// Fake time/alarm platform, overriding the weak definitions in test_platform.cpp. Time only
// progresses when a test calls AdvanceTime().
static uint32_t sNow = 0;
static uint32_t sAlarmTime;
static bool sAlarmOn = false;
extern "C" {
void otPlatAlarmMilliStop(otInstance *) { sAlarmOn = false; }
void otPlatAlarmMilliStartAt(otInstance *, uint32_t aT0, uint32_t aDt)
{
sAlarmOn = true;
sAlarmTime = aT0 + aDt;
}
uint32_t otPlatAlarmMilliGetNow(void) { return sNow; }
} // extern "C"
namespace ot {
namespace MeshCoP {
@@ -187,7 +207,7 @@ static NetworkName sCommNetworkName, sCommDomainNam
static ExtendedPanId sCommExtPanId;
static TcatAgent::CertificateAuthorizationField sCommAuth, sDeviceAuth;
// Helper class to test BLE connection state.
// Helper class to test the BLE Secure connection state and validate the documented connect-callback contract.
class TestBleSecure
{
public:
@@ -195,18 +215,51 @@ public:
: mIsConnected(false)
, mIsBleConnectionOpen(false)
, mConnectCallbackCount(0)
, mContractHonored(true)
{
}
void HandleBleSecureConnect(bool aConnected, bool aBleConnectionOpen)
{
// A TLS session cannot exist without an open BLE link to carry it.
if (aConnected && !aBleConnectionOpen)
{
printf("TestBleSecure: illegal pair reported (aConnected=1, aBleConnectionOpen=0)\n");
mContractHonored = false;
}
// The callback must fire only on a change of the pair, so never twice in a row with identical values.
if (aConnected == mIsConnected && aBleConnectionOpen == mIsBleConnectionOpen)
{
printf("TestBleSecure: pair repeated without change (aConnected=%d, aBleConnectionOpen=%d)\n", aConnected,
aBleConnectionOpen);
mContractHonored = false;
}
mIsConnected = aConnected;
mIsBleConnectionOpen = aBleConnectionOpen;
mConnectCallbackCount++;
}
bool IsConnected(void) const { return mIsConnected; }
bool IsBleConnectionOpen(void) const { return mIsBleConnectionOpen; }
// Returns TRUE if the contract has been honored by every callback so far AND the currently reported state and
// the callback count (since the last reset) match the expected values.
bool Verify(bool aConnected, bool aBleConnectionOpen, uint32_t aExpectedCallbackCount) const
{
bool ok = mContractHonored && (mIsConnected == aConnected) && (mIsBleConnectionOpen == aBleConnectionOpen) &&
(mConnectCallbackCount == aExpectedCallbackCount);
if (!ok)
{
printf("TestBleSecure::Verify mismatch: got (aConnected=%d, aBleConnectionOpen=%d, count=%lu, "
"contractHonored=%d), expected (aConnected=%d, aBleConnectionOpen=%d, count=%lu)\n",
mIsConnected, mIsBleConnectionOpen, ToUlong(mConnectCallbackCount), mContractHonored, aConnected,
aBleConnectionOpen, ToUlong(aExpectedCallbackCount));
}
return ok;
}
bool IsContractHonored(void) const { return mContractHonored; }
uint32_t GetConnectCallbackCount(void) const { return mConnectCallbackCount; }
void ResetConnectCallbackCount(void) { mConnectCallbackCount = 0; }
@@ -214,6 +267,7 @@ private:
bool mIsConnected;
bool mIsBleConnectionOpen;
uint32_t mConnectCallbackCount;
bool mContractHonored;
};
static void HandleBleSecureConnect(otInstance *aInstance, bool aConnected, bool aBleConnectionOpen, void *aContext)
@@ -253,10 +307,29 @@ static bool SetActiveDatasetAuthorized(const TcatAgent *aAgent, const Dataset::I
return aAgent->IsSetActiveDatasetAuthorized(&dataset);
}
// Advances fake time by aDuration, firing any expired alarms and processing tasklets along the way.
static void AdvanceTime(Instance *aInstance, uint32_t aDuration)
{
uint32_t time = sNow + aDuration;
while (sAlarmOn && TimeMilli(sAlarmTime) <= TimeMilli(time))
{
sNow = sAlarmTime;
sAlarmOn = false;
otPlatAlarmMilliFired(aInstance);
otTaskletsProcess(aInstance);
}
sNow = time;
otTaskletsProcess(aInstance);
}
static Instance *TestInitInstanceTcat(void)
{
Instance *instance = testInitInstance();
sAlarmOn = false; // discard any pending alarm of a previous test's instance
otBleSecureSetCertificate(instance, reinterpret_cast<const uint8_t *>(OT_TCAT_X509_CERT), sizeof(OT_TCAT_X509_CERT),
reinterpret_cast<const uint8_t *>(OT_TCAT_PRIV_KEY), sizeof(OT_TCAT_PRIV_KEY));
otBleSecureSetCaCertificateChain(instance, reinterpret_cast<const uint8_t *>(OT_TCAT_TRUSTED_ROOT_CERTIFICATE),
@@ -275,6 +348,7 @@ static Instance *TestInitInstanceTcat(void)
memcpy(&sDeviceAuth, &kDeviceCert1AuthField, sizeof(sDeviceAuth));
sPlatBleLastAdvSetDataLen = 0;
memset(sPlatBleLastAdvSetData, 0, OT_TCAT_ADVERTISEMENT_MAX_LEN);
sPlatBleAdvertising = false;
return instance;
}
@@ -292,12 +366,12 @@ void TestTcatConnectionAndCertAttributes(void)
VerifyOrQuit(otBleSecureStart(instance, HandleBleSecureConnect, nullptr, true, nullptr) == kErrorAlready);
SuccessOrQuit(otBleSecureTcatStart(instance, nullptr));
// Validate connection callbacks when platform informs that peer has connected/disconnected
// Validate connection callbacks when platform informs that peer has connected/disconnected.
VerifyOrQuit(!otBleSecureIsConnected(instance));
otPlatBleGapOnConnected(instance, kConnectionId);
VerifyOrQuit(!ble.IsConnected() && ble.IsBleConnectionOpen());
VerifyOrQuit(ble.Verify(/* aConnected */ false, /* aBleConnectionOpen */ true, /* aExpectedCallbackCount */ 1));
otPlatBleGapOnDisconnected(instance, kConnectionId);
VerifyOrQuit(!ble.IsConnected() && !ble.IsBleConnectionOpen());
VerifyOrQuit(ble.Verify(/* aConnected */ false, /* aBleConnectionOpen */ false, /* aExpectedCallbackCount */ 2));
// Verify that Thread-attribute parsing isn't available yet when not connected as client or server.
attributeLen = sizeof(attributeBuffer);
@@ -310,12 +384,12 @@ void TestTcatConnectionAndCertAttributes(void)
// Validate connection callbacks when calling `otBleSecureDisconnect()`
otPlatBleGapOnConnected(instance, kConnectionId);
VerifyOrQuit(!ble.IsConnected() && ble.IsBleConnectionOpen());
VerifyOrQuit(ble.Verify(/* aConnected */ false, /* aBleConnectionOpen */ true, /* aExpectedCallbackCount */ 3));
ble.ResetConnectCallbackCount();
otBleSecureDisconnect(instance);
VerifyOrQuit(!ble.IsConnected() && !ble.IsBleConnectionOpen());
// Regression test: a locally-initiated disconnect must invoke the connect callback exactly once.
VerifyOrQuit(ble.GetConnectCallbackCount() == 1);
// Regression test: a locally-initiated disconnect (with no TLS session) must invoke the connect callback
// exactly once, with the fully-disconnected pair.
VerifyOrQuit(ble.Verify(/* aConnected */ false, /* aBleConnectionOpen */ false, /* aExpectedCallbackCount */ 1));
// Validate TLS connection can be started (as client) only when peer is BLE-connected
otPlatBleGapOnConnected(instance, kConnectionId);
@@ -344,6 +418,8 @@ void TestTcatConnectionAndCertAttributes(void)
otBleSecureStop(instance);
VerifyOrQuit(!otBleSecureIsTcatAgentStarted(instance));
VerifyOrQuit(ble.IsContractHonored());
testFreeInstance(instance);
}
@@ -360,11 +436,13 @@ void TestTcatAdvertisementUpdates(void)
SuccessOrQuit(otBleSecureTcatStart(instance, nullptr));
VerifyOrQuit(sPlatBleLastAdvSetDataLen > 0, "Adv data should be set after BleSecure start");
VerifyOrQuit(sPlatBleAdvertising, "Advertising should be started after BleSecure start");
advDataSnapshotLen = sPlatBleLastAdvSetDataLen;
memcpy(advDataSnapshot, sPlatBleLastAdvSetData, advDataSnapshotLen);
otPlatBleGapOnConnected(instance, kConnectionId);
SuccessOrQuit(otBleSecureConnect(instance));
sPlatBleAdvertising = false; // model BLE platform behavior: advertising stops when a client connects.
SuccessOrQuit(otBleSecureConnect(instance)); // mock "TLS handshake active" by initiating a client connection.
// BLE connect and initiating TLS does not change the adv data.
VerifyOrQuit(sPlatBleLastAdvSetDataLen == advDataSnapshotLen &&
@@ -373,17 +451,39 @@ void TestTcatAdvertisementUpdates(void)
advDataSnapshotLen = sPlatBleLastAdvSetDataLen;
memcpy(advDataSnapshot, sPlatBleLastAdvSetData, advDataSnapshotLen);
// Commissioner sets dataset, then disconnects
// Commissioner sets dataset, then disconnects its BLE suddenly
instance->Get<ActiveDatasetManager>().SaveLocal(sPartialDataset);
otBleSecureDisconnect(instance);
otPlatBleGapOnDisconnected(instance, kConnectionId);
// Adv is changed due to the partial dataset now being advertised in the S flag.
// Since the TLS session teardown is ongoing, no change in advertisement state yet.
VerifyOrQuit(!sPlatBleAdvertising, "Advertising restarted while TLS teardown is still ongoing");
// Still within the TLS teardown guard time (kGuardTimeNewConnectionMilli). Meanwhile time advances and
// the advertisement data is updated to reflect the new sPartialDataset state. But not advertising yet.
AdvanceTime(instance, 1000);
VerifyOrQuit(sPlatBleLastAdvSetDataLen == advDataSnapshotLen, "Adv data length changed unexpectedly");
VerifyOrQuit(memcmp(sPlatBleLastAdvSetData, advDataSnapshot, advDataSnapshotLen) != 0,
"Adv data did not change after disconnect, which it should due to S flag");
"Adv data did not change after processing sPartialDataset, which it should due to S flag");
VerifyOrQuit(!sPlatBleAdvertising, "Advertising already restarted while TLS teardown is still ongoing");
// Take new snapshot of advertisement data.
advDataSnapshotLen = sPlatBleLastAdvSetDataLen;
memcpy(advDataSnapshot, sPlatBleLastAdvSetData, advDataSnapshotLen);
// Advance time beyond the guard time, so that the TLS session fully disconnects and the (deferred)
// advertising restart is performed now.
AdvanceTime(instance, 1000 + 10);
VerifyOrQuit(sPlatBleAdvertising, "Advertising was not restarted after TLS teardown completed");
// Adv content itself is not changed now - it was already done while waiting for the TLS guard time.
VerifyOrQuit(sPlatBleLastAdvSetDataLen == advDataSnapshotLen, "Adv data length changed unexpectedly");
VerifyOrQuit(memcmp(sPlatBleLastAdvSetData, advDataSnapshot, advDataSnapshotLen) == 0,
"Adv data changed unexpectedly after TLS guard timeout expired");
otBleSecureStop(instance);
VerifyOrQuit(ble.IsContractHonored());
testFreeInstance(instance);
}