mirror of
https://github.com/meshtastic/firmware.git
synced 2026-09-15 15:59:40 -04:00
564 lines
21 KiB
C++
564 lines
21 KiB
C++
#include "DebugConfiguration.h"
|
|
#include "FSCommon.h"
|
|
#include "SPILock.h"
|
|
#include "TestUtil.h"
|
|
#include "concurrency/LockGuard.h"
|
|
#include <unity.h>
|
|
|
|
#if defined(ARCH_PORTDUINO) && HAS_BLE_MESH
|
|
|
|
#if HAS_SCREEN
|
|
#include "graphics/draw/MenuHandler.h"
|
|
#endif
|
|
#include "mesh/BLEMeshHandler.h"
|
|
#include "mesh/CryptoEngine.h"
|
|
#include "mesh/NodeDB.h"
|
|
#include "mesh/PacketHistory.h"
|
|
#include "mesh/Router.h"
|
|
|
|
#include <Curve25519.h>
|
|
#include <ErriezCRC32.h>
|
|
#include <array>
|
|
#include <vector>
|
|
|
|
namespace
|
|
{
|
|
|
|
/**
|
|
* A BLEMeshHandler with the radio replaced by a record of what it was asked to send.
|
|
*
|
|
* Everything worth testing here is platform-independent - the advertisement the transport builds
|
|
* and the guards it applies to what it receives - so the platform hooks only need to be observable,
|
|
* not real. The secure envelope, approval checks, verification code, and tamper detection are also
|
|
* pinned here.
|
|
*/
|
|
class FakeBLEMesh : public BLEMeshHandler
|
|
{
|
|
public:
|
|
std::vector<std::vector<uint8_t>> sent;
|
|
std::vector<meshtastic_MeshPacket> received;
|
|
bool ready = true;
|
|
bool advertising = false;
|
|
|
|
void start() override { isRunning = true; }
|
|
void stop() override { isRunning = false; }
|
|
|
|
// Exposed so tests can drive ingress without a BLE stack.
|
|
void feed(const uint8_t *data, size_t len, int8_t rssi) { deliverToRouter(data, len, rssi); }
|
|
uint8_t build(const meshtastic_MeshPacket *mp, NodeNum peer, uint8_t *out, size_t cap)
|
|
{
|
|
return buildAdvPayload(mp, peer, out, cap);
|
|
}
|
|
bool approveForTest(NodeNum nodeNum, const uint8_t publicKey[32]) { return addPairForTest(nodeNum, publicKey); }
|
|
int32_t pump() { return runOnce(); }
|
|
|
|
void enqueueReceived(meshtastic_MeshPacket *p) override
|
|
{
|
|
received.push_back(*p);
|
|
packetPool.release(p);
|
|
}
|
|
|
|
protected:
|
|
bool platformBeginAdvertising(const uint8_t *adv, size_t len) override
|
|
{
|
|
sent.emplace_back(adv, adv + len);
|
|
advertising = true;
|
|
return true;
|
|
}
|
|
bool platformAdvertisingActive() override { return advertising; }
|
|
void platformEndAdvertising() override { advertising = false; }
|
|
bool platformReady() override { return ready; }
|
|
};
|
|
|
|
/// A packet in the state Router::send hands to a transport: encrypted, with a real sender.
|
|
meshtastic_MeshPacket encryptedPacket(uint32_t from = 0x3061b02e, uint32_t id = 0x04050b6e, size_t payload = 32)
|
|
{
|
|
meshtastic_MeshPacket p = meshtastic_MeshPacket_init_zero;
|
|
p.from = from;
|
|
p.to = NODENUM_BROADCAST;
|
|
p.id = id;
|
|
p.channel = 50;
|
|
p.hop_limit = 3;
|
|
p.hop_start = 3;
|
|
p.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
|
|
p.encrypted.size = payload;
|
|
for (size_t i = 0; i < payload; i++)
|
|
p.encrypted.bytes[i] = (uint8_t)i;
|
|
return p;
|
|
}
|
|
|
|
struct TestIdentity {
|
|
std::array<uint8_t, 32> privateKey{};
|
|
std::array<uint8_t, 32> publicKey{};
|
|
NodeNum nodeNum = 0;
|
|
};
|
|
|
|
TestIdentity makeIdentity(uint8_t seed)
|
|
{
|
|
TestIdentity identity;
|
|
for (size_t i = 0; i < identity.privateKey.size(); i++)
|
|
identity.privateKey[i] = seed + static_cast<uint8_t>(i);
|
|
Curve25519::eval(identity.publicKey.data(), identity.privateKey.data(), nullptr);
|
|
identity.nodeNum = crc32Buffer(identity.publicKey.data(), identity.publicKey.size());
|
|
return identity;
|
|
}
|
|
|
|
void useIdentity(const TestIdentity &identity)
|
|
{
|
|
config.security.private_key.size = 32;
|
|
config.security.public_key.size = 32;
|
|
memcpy(config.security.private_key.bytes, identity.privateKey.data(), 32);
|
|
memcpy(config.security.public_key.bytes, identity.publicKey.data(), 32);
|
|
owner.public_key.size = 32;
|
|
memcpy(owner.public_key.bytes, identity.publicKey.data(), 32);
|
|
myNodeInfo.my_node_num = identity.nodeNum;
|
|
crypto->setDHPrivateKey(config.security.private_key.bytes);
|
|
}
|
|
|
|
std::vector<uint8_t> frameBody(const std::vector<uint8_t> &advertisement)
|
|
{
|
|
return std::vector<uint8_t>(advertisement.begin() + BLE_MESH_ADV_OVERHEAD, advertisement.end());
|
|
}
|
|
|
|
/// Largest ciphertext a MeshPacket can carry, i.e. one guaranteed not to fit an advertisement.
|
|
constexpr size_t MAX_ENCRYPTED_FOR_TEST = sizeof(meshtastic_MeshPacket().encrypted.bytes);
|
|
|
|
uint8_t buildAuthenticatedAdvertisement(FakeBLEMesh &handler, const TestIdentity &sender, const TestIdentity &recipient,
|
|
const meshtastic_MeshPacket &packet, uint8_t out[BLE_MESH_ADV_TOTAL_MAX])
|
|
{
|
|
useIdentity(sender);
|
|
handler.start();
|
|
if (!handler.approveForTest(recipient.nodeNum, recipient.publicKey.data()))
|
|
return 0;
|
|
return handler.build(&packet, recipient.nodeNum, out, BLE_MESH_ADV_TOTAL_MAX);
|
|
}
|
|
|
|
uint32_t readTestU32(const uint8_t *in)
|
|
{
|
|
uint32_t value;
|
|
memcpy(&value, in, sizeof(value));
|
|
return value;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void test_advertisement_carries_the_packet(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
const auto sender = makeIdentity(1);
|
|
const auto recipient = makeIdentity(65);
|
|
auto p = encryptedPacket();
|
|
|
|
uint8_t adv[BLE_MESH_ADV_TOTAL_MAX];
|
|
uint8_t len = buildAuthenticatedAdvertisement(h, sender, recipient, p, adv);
|
|
|
|
TEST_ASSERT_TRUE_MESSAGE(len > BLE_MESH_ADV_OVERHEAD, "built an advertisement");
|
|
// Flags AD, then manufacturer-specific data with our company ID and protocol version.
|
|
TEST_ASSERT_EQUAL_UINT8(2, adv[0]);
|
|
TEST_ASSERT_EQUAL_UINT8(0x01, adv[1]);
|
|
TEST_ASSERT_EQUAL_UINT8(0xFF, adv[4]);
|
|
TEST_ASSERT_EQUAL_UINT8(BLE_MESH_COMPANY_ID & 0xFF, adv[5]);
|
|
TEST_ASSERT_EQUAL_UINT8((BLE_MESH_COMPANY_ID >> 8) & 0xFF, adv[6]);
|
|
TEST_ASSERT_EQUAL_UINT8(BLE_MESH_PROTOCOL_VERSION, adv[7]);
|
|
TEST_ASSERT_EQUAL_UINT8(BLE_MESH_FRAME_DATA, adv[BLE_MESH_ADV_OVERHEAD]);
|
|
TEST_ASSERT_EQUAL_UINT32(sender.nodeNum, readTestU32(&adv[BLE_MESH_ADV_OVERHEAD + 1]));
|
|
TEST_ASSERT_EQUAL_UINT32(recipient.nodeNum, readTestU32(&adv[BLE_MESH_ADV_OVERHEAD + 5]));
|
|
// The AD length byte counts everything after itself.
|
|
TEST_ASSERT_EQUAL_UINT8(len - 4, adv[3]);
|
|
}
|
|
|
|
void test_refuses_an_unencrypted_packet(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
const auto sender = makeIdentity(1);
|
|
const auto recipient = makeIdentity(65);
|
|
useIdentity(sender);
|
|
h.start();
|
|
auto p = encryptedPacket();
|
|
p.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
|
|
uint8_t adv[BLE_MESH_ADV_TOTAL_MAX];
|
|
// Router::send encrypts before any transport sees a packet, so plaintext here is a bug
|
|
// upstream - putting it on air would leak the message.
|
|
h.approveForTest(recipient.nodeNum, recipient.publicKey.data());
|
|
TEST_ASSERT_EQUAL_UINT8(0, h.build(&p, recipient.nodeNum, adv, sizeof(adv)));
|
|
}
|
|
|
|
void test_refuses_a_packet_with_no_sender(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
const auto sender = makeIdentity(1);
|
|
const auto recipient = makeIdentity(65);
|
|
useIdentity(sender);
|
|
h.start();
|
|
auto p = encryptedPacket(0 /* from */);
|
|
|
|
uint8_t adv[BLE_MESH_ADV_TOTAL_MAX];
|
|
// Nothing legitimate advertises from=0, and a packet with no sender can reach remote admin
|
|
// without authorisation - the LoRa path refuses it for the same reason.
|
|
h.approveForTest(recipient.nodeNum, recipient.publicKey.data());
|
|
TEST_ASSERT_EQUAL_UINT8(0, h.build(&p, recipient.nodeNum, adv, sizeof(adv)));
|
|
}
|
|
|
|
void test_drops_a_packet_too_large_for_one_advertisement(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
const auto sender = makeIdentity(1);
|
|
const auto recipient = makeIdentity(65);
|
|
useIdentity(sender);
|
|
h.start();
|
|
// A single unfragmented extended advertisement holds 251 bytes and we never chain, so the
|
|
// largest packets cannot ride BLE. They still go out over LoRa - Router::send has already
|
|
// handed them to the radio by the time we refuse.
|
|
auto p = encryptedPacket(0x3061b02e, 0x04050b6e, MAX_ENCRYPTED_FOR_TEST);
|
|
|
|
uint8_t adv[BLE_MESH_ADV_TOTAL_MAX];
|
|
h.approveForTest(recipient.nodeNum, recipient.publicKey.data());
|
|
TEST_ASSERT_EQUAL_UINT8(0, h.build(&p, recipient.nodeNum, adv, sizeof(adv)));
|
|
}
|
|
|
|
void test_send_queues_rather_than_transmitting(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
const auto sender = makeIdentity(1);
|
|
const auto recipient = makeIdentity(65);
|
|
useIdentity(sender);
|
|
h.start();
|
|
TEST_ASSERT_TRUE(h.approveForTest(recipient.nodeNum, recipient.publicKey.data()));
|
|
auto p = encryptedPacket();
|
|
|
|
TEST_ASSERT_TRUE(h.onSend(&p));
|
|
// onSend is reached from Router::send on the main task. An implementation that advertised
|
|
// inline would stall the router - and so LoRa timing and the whole main loop - for the length
|
|
// of every burst.
|
|
TEST_ASSERT_EQUAL_MESSAGE(0, h.sent.size(), "nothing on air yet");
|
|
|
|
h.pump();
|
|
TEST_ASSERT_EQUAL_MESSAGE(1, h.sent.size(), "the pump transmits it");
|
|
}
|
|
|
|
void test_tx_queue_is_bounded(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
const auto sender = makeIdentity(1);
|
|
const auto recipient = makeIdentity(65);
|
|
useIdentity(sender);
|
|
h.start();
|
|
TEST_ASSERT_TRUE(h.approveForTest(recipient.nodeNum, recipient.publicKey.data()));
|
|
|
|
size_t accepted = 0;
|
|
for (size_t i = 0; i < BLE_MESH_TX_QUEUE_SIZE * 3; i++) {
|
|
auto p = encryptedPacket(0x3061b02e, (uint32_t)(0x1000 + i));
|
|
if (h.onSend(&p))
|
|
accepted++;
|
|
}
|
|
// A full ring drops rather than growing without bound or overwriting an unsent frame.
|
|
TEST_ASSERT_EQUAL(BLE_MESH_TX_QUEUE_SIZE, accepted);
|
|
}
|
|
|
|
void test_a_relayed_packet_is_re_advertised(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
const auto sender = makeIdentity(1);
|
|
const auto recipient = makeIdentity(65);
|
|
useIdentity(sender);
|
|
h.start();
|
|
TEST_ASSERT_TRUE(h.approveForTest(recipient.nodeNum, recipient.publicKey.data()));
|
|
auto p = encryptedPacket();
|
|
p.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_ADV;
|
|
|
|
// A packet already marked as BLE-sourced is what a *rebroadcast* looks like:
|
|
// perhapsRebroadcast allocCopy()s the received packet and nothing on the TX path rewrites
|
|
// transport_mechanism. Refusing it caps the mesh at a single hop - two nodes can talk and a
|
|
// three-node chain cannot form.
|
|
TEST_ASSERT_TRUE_MESSAGE(h.onSend(&p), "relay must not be refused");
|
|
}
|
|
|
|
void test_pairing_hello_produces_the_same_verification_code(void)
|
|
{
|
|
const auto a = makeIdentity(1);
|
|
const auto b = makeIdentity(65);
|
|
FakeBLEMesh nodeA;
|
|
FakeBLEMesh nodeB;
|
|
uint32_t codeA = UINT32_MAX;
|
|
uint32_t codeB = UINT32_MAX;
|
|
|
|
useIdentity(a);
|
|
nodeA.start();
|
|
TEST_ASSERT_TRUE(nodeA.beginPairing([&codeA](NodeNum, uint32_t code) { codeA = code; }));
|
|
nodeA.pump();
|
|
TEST_ASSERT_EQUAL(1, nodeA.sent.size());
|
|
nodeA.advertising = false;
|
|
|
|
useIdentity(b);
|
|
nodeB.start();
|
|
TEST_ASSERT_TRUE(nodeB.beginPairing([&codeB](NodeNum, uint32_t code) { codeB = code; }));
|
|
nodeB.pump();
|
|
TEST_ASSERT_EQUAL(1, nodeB.sent.size());
|
|
nodeB.advertising = false;
|
|
|
|
auto helloA = frameBody(nodeA.sent[0]);
|
|
auto helloB = frameBody(nodeB.sent[0]);
|
|
nodeB.feed(helloA.data(), helloA.size(), -40);
|
|
nodeB.pump();
|
|
useIdentity(a);
|
|
nodeA.feed(helloB.data(), helloB.size(), -41);
|
|
nodeA.pump();
|
|
|
|
TEST_ASSERT_NOT_EQUAL(UINT32_MAX, codeA);
|
|
TEST_ASSERT_EQUAL(codeA, codeB);
|
|
nodeA.cancelPairing();
|
|
nodeB.cancelPairing();
|
|
}
|
|
|
|
void test_approved_peer_survives_a_handler_restart(void)
|
|
{
|
|
const auto sender = makeIdentity(1);
|
|
const auto recipient = makeIdentity(65);
|
|
useIdentity(sender);
|
|
|
|
FakeBLEMesh first;
|
|
TEST_ASSERT_TRUE(first.approveForTest(recipient.nodeNum, recipient.publicKey.data()));
|
|
|
|
FakeBLEMesh restarted;
|
|
TEST_ASSERT_TRUE(restarted.isPaired(recipient.nodeNum));
|
|
TEST_ASSERT_EQUAL_UINT8(1, restarted.pairedCount());
|
|
}
|
|
|
|
void test_ingress_accepts_an_authenticated_approved_peer(void)
|
|
{
|
|
const auto sender = makeIdentity(1);
|
|
const auto recipient = makeIdentity(65);
|
|
FakeBLEMesh outbound;
|
|
FakeBLEMesh inbound;
|
|
auto p = encryptedPacket(0x3061b02e);
|
|
|
|
uint8_t adv[BLE_MESH_ADV_TOTAL_MAX];
|
|
const uint8_t len = buildAuthenticatedAdvertisement(outbound, sender, recipient, p, adv);
|
|
TEST_ASSERT_TRUE(len > BLE_MESH_ADV_OVERHEAD);
|
|
|
|
useIdentity(recipient);
|
|
inbound.start();
|
|
TEST_ASSERT_TRUE(inbound.approveForTest(sender.nodeNum, sender.publicKey.data()));
|
|
inbound.feed(adv + BLE_MESH_ADV_OVERHEAD, len - BLE_MESH_ADV_OVERHEAD, -42);
|
|
|
|
TEST_ASSERT_EQUAL_MESSAGE(1, inbound.received.size(), "delivered to the router");
|
|
const auto &got = inbound.received[0];
|
|
TEST_ASSERT_EQUAL_UINT32(0x3061b02e, got.from);
|
|
// Stamped so the router - and anything downstream - can tell how it arrived.
|
|
TEST_ASSERT_EQUAL(meshtastic_MeshPacket_TransportMechanism_TRANSPORT_BLE_ADV, got.transport_mechanism);
|
|
// Unlike UDP there IS a real measurement of this hop, so it is reported rather than cleared.
|
|
TEST_ASSERT_TRUE(got.has_rx_rssi);
|
|
TEST_ASSERT_EQUAL_INT(-42, got.rx_rssi);
|
|
TEST_ASSERT_EQUAL_MESSAGE(0, got.rx_snr, "no SNR exists for a BLE arrival");
|
|
TEST_ASSERT_FALSE_MESSAGE(got.pki_encrypted, "claimed authentication stripped");
|
|
TEST_ASSERT_EQUAL(0, got.public_key.size);
|
|
}
|
|
|
|
void test_ingress_rejects_an_unapproved_peer(void)
|
|
{
|
|
const auto sender = makeIdentity(1);
|
|
const auto recipient = makeIdentity(65);
|
|
FakeBLEMesh outbound;
|
|
FakeBLEMesh inbound;
|
|
auto p = encryptedPacket();
|
|
uint8_t adv[BLE_MESH_ADV_TOTAL_MAX];
|
|
const uint8_t len = buildAuthenticatedAdvertisement(outbound, sender, recipient, p, adv);
|
|
|
|
useIdentity(recipient);
|
|
inbound.start();
|
|
inbound.feed(adv + BLE_MESH_ADV_OVERHEAD, len - BLE_MESH_ADV_OVERHEAD, -50);
|
|
TEST_ASSERT_EQUAL_MESSAGE(0, inbound.received.size(), "unapproved bridge rejected");
|
|
}
|
|
|
|
void test_ingress_rejects_a_tampered_frame(void)
|
|
{
|
|
const auto sender = makeIdentity(1);
|
|
const auto recipient = makeIdentity(65);
|
|
FakeBLEMesh outbound;
|
|
FakeBLEMesh inbound;
|
|
auto p = encryptedPacket();
|
|
uint8_t adv[BLE_MESH_ADV_TOTAL_MAX];
|
|
const uint8_t len = buildAuthenticatedAdvertisement(outbound, sender, recipient, p, adv);
|
|
adv[len - 1] ^= 0x80;
|
|
|
|
useIdentity(recipient);
|
|
inbound.start();
|
|
TEST_ASSERT_TRUE(inbound.approveForTest(sender.nodeNum, sender.publicKey.data()));
|
|
inbound.feed(adv + BLE_MESH_ADV_OVERHEAD, len - BLE_MESH_ADV_OVERHEAD, -50);
|
|
TEST_ASSERT_EQUAL_MESSAGE(0, inbound.received.size(), "tampered frame rejected");
|
|
}
|
|
|
|
void test_existing_packet_history_rejects_an_authenticated_replay(void)
|
|
{
|
|
const auto sender = makeIdentity(1);
|
|
const auto recipient = makeIdentity(65);
|
|
FakeBLEMesh outbound;
|
|
FakeBLEMesh inbound;
|
|
auto p = encryptedPacket();
|
|
uint8_t adv[BLE_MESH_ADV_TOTAL_MAX];
|
|
const uint8_t len = buildAuthenticatedAdvertisement(outbound, sender, recipient, p, adv);
|
|
|
|
useIdentity(recipient);
|
|
inbound.start();
|
|
TEST_ASSERT_TRUE(inbound.approveForTest(sender.nodeNum, sender.publicKey.data()));
|
|
inbound.feed(adv + BLE_MESH_ADV_OVERHEAD, len - BLE_MESH_ADV_OVERHEAD, -50);
|
|
inbound.feed(adv + BLE_MESH_ADV_OVERHEAD, len - BLE_MESH_ADV_OVERHEAD, -50);
|
|
TEST_ASSERT_EQUAL_MESSAGE(2, inbound.received.size(), "transport hands authenticated packets to the router");
|
|
PacketHistory history(4);
|
|
TEST_ASSERT_FALSE(history.wasSeenRecently(&inbound.received[0]));
|
|
TEST_ASSERT_TRUE_MESSAGE(history.wasSeenRecently(&inbound.received[1]), "existing packet history rejects replay");
|
|
}
|
|
|
|
void test_ingress_rejects_the_removed_legacy_format(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
h.start();
|
|
auto p = encryptedPacket();
|
|
uint8_t body[meshtastic_MeshPacket_size];
|
|
const size_t n = pb_encode_to_bytes(body, sizeof(body), &meshtastic_MeshPacket_msg, &p);
|
|
|
|
h.feed(body, n, -50);
|
|
TEST_ASSERT_EQUAL_MESSAGE(0, h.received.size(), "legacy unauthenticated frame rejected");
|
|
}
|
|
|
|
void test_ingress_drops_an_impossible_hop_count(void)
|
|
{
|
|
const auto sender = makeIdentity(1);
|
|
const auto recipient = makeIdentity(65);
|
|
FakeBLEMesh outbound;
|
|
FakeBLEMesh inbound;
|
|
auto p = encryptedPacket();
|
|
p.hop_limit = HOP_MAX + 1;
|
|
uint8_t adv[BLE_MESH_ADV_TOTAL_MAX];
|
|
const uint8_t len = buildAuthenticatedAdvertisement(outbound, sender, recipient, p, adv);
|
|
|
|
useIdentity(recipient);
|
|
inbound.start();
|
|
TEST_ASSERT_TRUE(inbound.approveForTest(sender.nodeNum, sender.publicKey.data()));
|
|
inbound.feed(adv + BLE_MESH_ADV_OVERHEAD, len - BLE_MESH_ADV_OVERHEAD, -50);
|
|
TEST_ASSERT_EQUAL_MESSAGE(0, inbound.received.size(), "invalid hop count rejected");
|
|
}
|
|
|
|
void test_pump_waits_for_the_platform(void)
|
|
{
|
|
FakeBLEMesh h;
|
|
const auto sender = makeIdentity(1);
|
|
const auto recipient = makeIdentity(65);
|
|
useIdentity(sender);
|
|
h.start();
|
|
TEST_ASSERT_TRUE(h.approveForTest(recipient.nodeNum, recipient.publicKey.data()));
|
|
h.ready = false;
|
|
auto p = encryptedPacket();
|
|
TEST_ASSERT_TRUE(h.onSend(&p));
|
|
|
|
h.pump();
|
|
// Readiness is polled rather than pushed: the BLE stack comes up before main() constructs the
|
|
// handler about half the time, so a one-shot "ready" callback is a race that loses silently.
|
|
TEST_ASSERT_EQUAL_MESSAGE(0, h.sent.size(), "nothing transmitted before the stack is up");
|
|
|
|
h.ready = true;
|
|
h.pump();
|
|
TEST_ASSERT_EQUAL_MESSAGE(1, h.sent.size(), "transmits once ready");
|
|
}
|
|
|
|
#if HAS_BLE_MESH
|
|
// menuHandler::setNodePairingEnabled() in MenuHandler.cpp is the System-screen control plane for
|
|
// this transport. This pins the flag preservation and lifecycle calls so the menu cannot become
|
|
// cosmetic, or silently disable another configured network transport while enabling BLE sharing.
|
|
void test_node_pairing_menu_controls_ble_mesh_transport()
|
|
{
|
|
struct StateRestore {
|
|
meshtastic_LocalConfig savedConfig;
|
|
BLEMeshHandler *savedHandler;
|
|
~StateRestore()
|
|
{
|
|
bleMeshHandler = savedHandler;
|
|
config = savedConfig;
|
|
}
|
|
} restore{config, bleMeshHandler};
|
|
|
|
FakeBLEMesh handler;
|
|
bleMeshHandler = &handler;
|
|
|
|
config.has_network = false;
|
|
config.bluetooth.enabled = false;
|
|
config.network.enabled_protocols =
|
|
meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST | meshtastic_Config_NetworkConfig_ProtocolFlags_BLE_GATT_PEER;
|
|
|
|
TEST_ASSERT_TRUE(graphics::menuHandler::setNodePairingEnabled(true));
|
|
TEST_ASSERT_TRUE(config.has_network);
|
|
TEST_ASSERT_TRUE(config.bluetooth.enabled);
|
|
TEST_ASSERT_BITS_HIGH(meshtastic_Config_NetworkConfig_ProtocolFlags_BLE_BROADCAST, config.network.enabled_protocols);
|
|
TEST_ASSERT_BITS_HIGH(meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST |
|
|
meshtastic_Config_NetworkConfig_ProtocolFlags_BLE_GATT_PEER,
|
|
config.network.enabled_protocols);
|
|
|
|
const auto sender = makeIdentity(1);
|
|
const auto recipient = makeIdentity(65);
|
|
useIdentity(sender);
|
|
TEST_ASSERT_TRUE(handler.approveForTest(recipient.nodeNum, recipient.publicKey.data()));
|
|
auto packet = encryptedPacket();
|
|
TEST_ASSERT_TRUE(handler.onSend(&packet));
|
|
|
|
TEST_ASSERT_FALSE(graphics::menuHandler::setNodePairingEnabled(false));
|
|
TEST_ASSERT_BITS_LOW(meshtastic_Config_NetworkConfig_ProtocolFlags_BLE_BROADCAST, config.network.enabled_protocols);
|
|
TEST_ASSERT_BITS_HIGH(meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST |
|
|
meshtastic_Config_NetworkConfig_ProtocolFlags_BLE_GATT_PEER,
|
|
config.network.enabled_protocols);
|
|
TEST_ASSERT_FALSE(handler.onSend(&packet));
|
|
}
|
|
#endif
|
|
|
|
void setUp(void) {}
|
|
void tearDown(void)
|
|
{
|
|
concurrency::LockGuard guard(spiLock);
|
|
if (FSCom.exists("/prefs/ble-pairs.dat"))
|
|
FSCom.remove("/prefs/ble-pairs.dat");
|
|
}
|
|
|
|
void setup()
|
|
{
|
|
initializeTestEnvironment();
|
|
// deliverToRouter consults nodeDB to recognise - and drop - our own advertisement echoing back
|
|
// into our own scanner, so the ingress tests need a real one.
|
|
if (!nodeDB)
|
|
nodeDB = new NodeDB();
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_advertisement_carries_the_packet);
|
|
RUN_TEST(test_refuses_an_unencrypted_packet);
|
|
RUN_TEST(test_refuses_a_packet_with_no_sender);
|
|
RUN_TEST(test_drops_a_packet_too_large_for_one_advertisement);
|
|
RUN_TEST(test_send_queues_rather_than_transmitting);
|
|
RUN_TEST(test_tx_queue_is_bounded);
|
|
RUN_TEST(test_a_relayed_packet_is_re_advertised);
|
|
RUN_TEST(test_pairing_hello_produces_the_same_verification_code);
|
|
RUN_TEST(test_approved_peer_survives_a_handler_restart);
|
|
RUN_TEST(test_ingress_accepts_an_authenticated_approved_peer);
|
|
RUN_TEST(test_ingress_rejects_an_unapproved_peer);
|
|
RUN_TEST(test_ingress_rejects_a_tampered_frame);
|
|
RUN_TEST(test_existing_packet_history_rejects_an_authenticated_replay);
|
|
RUN_TEST(test_ingress_rejects_the_removed_legacy_format);
|
|
RUN_TEST(test_ingress_drops_an_impossible_hop_count);
|
|
RUN_TEST(test_pump_waits_for_the_platform);
|
|
#if HAS_BLE_MESH
|
|
RUN_TEST(test_node_pairing_menu_controls_ble_mesh_transport);
|
|
#endif
|
|
exit(UNITY_END());
|
|
}
|
|
|
|
#else
|
|
|
|
void setup()
|
|
{
|
|
initializeTestEnvironment();
|
|
LOG_WARN("BLE mesh tests require ARCH_PORTDUINO with HAS_BLE_MESH");
|
|
UNITY_BEGIN();
|
|
exit(UNITY_END());
|
|
}
|
|
|
|
#endif // ARCH_PORTDUINO && HAS_BLE_MESH
|
|
|
|
void loop() {}
|