Add station-only WiFi UDP bridge with eight authenticated peers

This commit is contained in:
mikecarper committed 2026-10-22 12:20:43 -07:00
1 parent 7bc3aeb234
commit 5e25faa3d0
45 files changed
+2699 -35

No files matched your search

+6
View File
@@ -991,6 +991,9 @@ bool Dispatcher::tryParsePacket(Packet* pkt, const uint8_t* raw, int len) {
pkt->radio_local = false;
pkt->tx_radio = RADIO_TX_AUTO;
pkt->radio_reply = pkt->radio_reply_force = false;
#if defined(WITH_UDP_BRIDGE)
pkt->udp_bridge_ingress = false;
#endif
pkt->radio_generation = pkt->radio_origin_generation = _radio->receiveProfileGeneration();
pkt->header = raw[i++];
if (pkt->getPayloadVer() > PAYLOAD_VER_1) {
@@ -1368,6 +1371,9 @@ Packet* Dispatcher::obtainNewPacket() {
pkt->tx_radio = RADIO_TX_AUTO;
pkt->radio_reply = receive_context_active;
pkt->radio_reply_force = false;
#if defined(WITH_UDP_BRIDGE)
pkt->udp_bridge_ingress = false;
#endif
}
return pkt;
}
+1 -1
View File
@@ -35,7 +35,7 @@
((defined(MESH_DEBUG) && MESH_DEBUG) || \
(defined(BRIDGE_DEBUG) && BRIDGE_DEBUG) || \
(defined(POWERSAVING_DEBUG) && POWERSAVING_DEBUG) || \
defined(WITH_ESPNOW_BRIDGE) || defined(WITH_RS232_BRIDGE))
defined(WITH_ESPNOW_BRIDGE) || defined(WITH_RS232_BRIDGE) || defined(WITH_UDP_BRIDGE))
#include <Arduino.h>
#include <atomic>
#include <stdarg.h>
+6
View File
@@ -17,6 +17,9 @@ Packet::Packet() {
radio_bound = false; radio_local = true;
tx_radio = RADIO_TX_AUTO;
radio_reply = radio_reply_force = false;
#if defined(WITH_UDP_BRIDGE)
udp_bridge_ingress = false;
#endif
}
bool Packet::isValidPathLen(uint8_t path_len) {
@@ -85,6 +88,9 @@ bool Packet::readFrom(const uint8_t src[], uint8_t len) {
header = src[i++];
tx_radio = RADIO_TX_AUTO;
radio_reply = radio_reply_force = false;
#if defined(WITH_UDP_BRIDGE)
udp_bridge_ingress = false;
#endif
if (getPayloadVer() > PAYLOAD_VER_1) return false;
if (hasTransportCodes()) {
+4
View File
@@ -69,6 +69,10 @@ public:
uint8_t tx_radio = RADIO_TX_AUTO; // local policy, never sent over the air
bool radio_reply = false; // locally generated response, never a forwarded packet
bool radio_reply_force = false; // captured reply-only RX-mode override
#if defined(WITH_UDP_BRIDGE)
// Volatile origin marker, copied with radio retries but never serialized.
bool udp_bridge_ingress = false;
#endif
/**
* \brief calculate the hash of payload + type
+109 -2
View File
@@ -3175,9 +3175,92 @@ bool CommonCLI::handleEspNowDiagnosticCommand(uint32_t sender_timestamp,
}
#endif
#if defined(WITH_UDP_BRIDGE)
bool CommonCLI::handleUDPBridgeCommand(uint32_t sender_timestamp,
const char* command, char* reply) {
constexpr size_t capacity = 157;
using namespace mesh::bridge::udp;
const bool get = strncmp(command, "get udp.", 8) == 0;
const bool set = strncmp(command, "set udp.", 8) == 0;
if (get || set) {
const char* key = command + 8;
if (get && strcmp(key, "status") == 0) {
_callbacks->formatUDPBridgeStatus(reply, capacity);
return true;
}
if (get && strncmp(key, "stats", 5) == 0) {
const char* page = key + 5;
if (*page == 0) _callbacks->formatUDPBridgeStats("", reply, capacity);
else if (strcmp(page, " rx") == 0 || strcmp(page, " tx") == 0
|| strcmp(page, " drops") == 0 || strcmp(page, " test") == 0)
_callbacks->formatUDPBridgeStats(page + 1, reply, capacity);
else snprintf(reply, capacity, "Error: use get udp.stats [rx|tx|drops|test]");
return true;
}
const Prefs* current = _callbacks->getUDPBridgePrefs();
if (!current) { snprintf(reply, capacity, "Error: UDP bridge unavailable"); return true; }
Prefs candidate = *current;
if (strcmp(key, "enabled") == 0 && get) {
snprintf(reply, capacity, "%s", current->enabled ? "on" : "off"); return true;
}
if (strcmp(key, "port") == 0 && get) {
snprintf(reply, capacity, "%u", current->port); return true;
}
if (strncmp(key, "peer.", 5) == 0 && key[5] >= '1' && key[5] <= '8'
&& (key[6] == 0 || key[6] == ' ')) {
const uint8_t index = static_cast<uint8_t>(key[5] - '1');
if (get && key[6] == 0) { formatPeer(current->peers[index], reply, capacity); return true; }
if (!set || key[6] != ' ' || !parsePeer(key + 7, candidate.peers[index])) {
snprintf(reply, capacity, "Error: use set udp.peer.1..8 <IPv4:port|off>"); return true;
}
} else if (set && strncmp(key, "enabled ", 8) == 0) {
if (strcmp(key + 8, "on") == 0) candidate.enabled = 1;
else if (strcmp(key + 8, "off") == 0) candidate.enabled = 0;
else { snprintf(reply, capacity, "Error: use set udp.enabled on|off"); return true; }
} else if (set && strncmp(key, "port ", 5) == 0) {
if (!parsePort(key + 5, candidate.port)) {
snprintf(reply, capacity, "Error: UDP port must be 1..65535"); return true;
}
} else {
snprintf(reply, capacity, "Error: use udp.enabled, udp.port, udp.peer.1..8, udp.status or udp.stats");
return true;
}
if (!validPrefs(candidate)) {
snprintf(reply, capacity, "Error: invalid or duplicate UDP peer"); return true;
}
snprintf(reply, capacity, "%s", _callbacks->setUDPBridgePrefs(candidate)
? "OK - UDP settings saved; use get udp.status" : "Error: UDP settings not saved");
return true;
}
if (strncmp(command, "udp", 3) != 0 || (command[3] != 0 && command[3] != ' ')) return false;
if (sender_timestamp != 0) {
snprintf(reply, capacity, "Error: UDP test/reset requires a local connection"); return true;
}
if (strcmp(command, "udp resetstats") == 0) {
snprintf(reply, capacity, "%s", _callbacks->resetUDPBridgeStats()
? "OK - UDP statistics reset" : "Error: UDP bridge unavailable"); return true;
}
if (strncmp(command, "udp test ", 9) != 0 || command[9] < '1' || command[9] > '8'
|| (command[10] != 0 && command[10] != ' ')) {
snprintf(reply, capacity, "Error: use udp test <1..8> [8 hex digits] or udp resetstats"); return true;
}
uint32_t nonce = millis();
if (command[10] != 0 && !parseNonce(command + 11, nonce)) {
snprintf(reply, capacity, "Error: use udp test <1..8> [8 hex digits] or udp resetstats"); return true;
}
if (_callbacks->sendUDPBridgeTest(static_cast<uint8_t>(command[9] - '1'), nonce))
snprintf(reply, capacity, "OK - test queued id=%08lX; confirm on peer", static_cast<unsigned long>(nonce));
else snprintf(reply, capacity, "Error: UDP test not queued; check status/stats");
return true;
}
#endif
void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
PrefsSaveReplyGuard save_reply(_prefs_save_failures, reply);
mesh::cli::normalizeCommandVerb(command);
#if defined(WITH_UDP_BRIDGE)
if (handleUDPBridgeCommand(sender_timestamp, command, reply)) return;
#endif
#if defined(WITH_RS232_BRIDGE)
if (handleRs232DiagnosticCommand(sender_timestamp, command, reply)) return;
#endif
@@ -3328,7 +3411,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
return;
}
#endif
#if defined(WITH_ESPNOW_BRIDGE)
#if defined(WITH_ESPNOW_BRIDGE) || defined(WITH_UDP_BRIDGE)
const bool ota_was_running = _board->isOTAUpdateRunning();
// Keep an outstanding pause if an earlier resume failed. Only a
// successful resume or an explicitly disabled saved intent clears it.
@@ -3352,10 +3435,22 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
return;
}
_wifi_ota_resume_espnow = _wifi_ota_resume_espnow || espnow_paused;
#endif
#if defined(WITH_UDP_BRIDGE)
// Close the UDP socket before the browser uploader owns the driver.
const bool udp_paused = _callbacks->isUDPBridgeNetworkEnabled();
if (udp_paused) _callbacks->setUDPBridgeNetworkState(false);
_wifi_ota_resume_udp = _wifi_ota_resume_udp || udp_paused;
#endif
reply[0] = 0;
if (!_board->startOTAUpdate(_prefs->node_name, reply, force_ap)) {
if (!reply[0]) strcpy(reply, "Error");
#if defined(WITH_UDP_BRIDGE)
if (udp_paused && !ota_was_running) {
_callbacks->setUDPBridgeNetworkState(true);
_wifi_ota_resume_udp = false;
}
#endif
#if defined(WITH_ESPNOW_BRIDGE)
if (espnow_paused && !ota_was_running) {
if (_callbacks->setEspNowBridgeState(true)) {
@@ -3421,6 +3516,13 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
? "; ESP-NOW resumed" : "; ESP-NOW resume failed");
}
}
#endif
#if defined(WITH_UDP_BRIDGE)
if (ota_stopped && !_board->isOTAUpdateRunning() && _wifi_ota_resume_udp) {
const auto* udp_prefs = _callbacks->getUDPBridgePrefs();
if (udp_prefs && udp_prefs->enabled) _callbacks->setUDPBridgeNetworkState(true);
_wifi_ota_resume_udp = false;
}
#endif
} else if (memcmp(command, "clock", 5) == 0) {
uint32_t now = getRTCClock()->getCurrentTime();
@@ -6044,7 +6146,9 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
sprintf(reply, "> %s", _callbacks->getRole());
} else if (configKeyEquals(config, "bridge.type")) {
sprintf(reply, "> %s",
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_RS232_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
#if defined(WITH_UDP_BRIDGE) && !defined(WITH_MQTT_BRIDGE) && !defined(WITH_RS232_BRIDGE) && !defined(WITH_ESPNOW_BRIDGE)
"udp"
#elif defined(WITH_MQTT_BRIDGE) && defined(WITH_RS232_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
"mqtt+rs232+espnow"
#elif defined(WITH_MQTT_BRIDGE) && defined(WITH_RS232_BRIDGE)
"mqtt+rs232"
@@ -6060,6 +6164,9 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
"mqtt"
#else
"none"
#endif
#if defined(WITH_UDP_BRIDGE) && (defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE))
"+udp"
#endif
);
#ifdef WITH_BRIDGE
+29 -1
View File
@@ -17,6 +17,9 @@
#include <helpers/MorseReplyHandoff.h>
#include <helpers/WirelessControl.h>
#include <helpers/FleetCommand.h>
#if defined(WITH_UDP_BRIDGE)
#include <helpers/UDPBridgePrefs.h>
#endif
#if MESH_ENABLE_FLEET_CONTROL
#include <helpers/FleetChannel.h>
#endif
@@ -32,7 +35,7 @@
#include <helpers/UserGpio.h>
#endif
#if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) || defined(WITH_MQTT_BRIDGE)
#if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) || defined(WITH_MQTT_BRIDGE) || defined(WITH_UDP_BRIDGE)
#define WITH_BRIDGE
#endif
@@ -645,6 +648,25 @@ public:
virtual bool restartRs232Bridge() { return restartBridge(); }
virtual bool isEspNowBridgeRunning() { return isBridgeRunning(); }
virtual bool isRs232BridgeRunning() const { return isBridgeRunning(); }
#if defined(WITH_UDP_BRIDGE)
virtual const mesh::bridge::udp::Prefs* getUDPBridgePrefs() const { return nullptr; }
virtual bool setUDPBridgePrefs(const mesh::bridge::udp::Prefs& prefs) {
(void)prefs; return false;
}
virtual void formatUDPBridgeStatus(char* reply, size_t capacity) {
snprintf(reply, capacity, "Error: UDP bridge unavailable");
}
virtual void formatUDPBridgeStats(const char* page, char* reply, size_t capacity) {
(void)page;
snprintf(reply, capacity, "Error: UDP bridge unavailable");
}
virtual bool sendUDPBridgeTest(uint8_t peer_index, uint32_t nonce) {
(void)peer_index; (void)nonce; return false;
}
virtual bool resetUDPBridgeStats() { return false; }
virtual bool setUDPBridgeNetworkState(bool enabled) { (void)enabled; return false; }
virtual bool isUDPBridgeNetworkEnabled() const { return false; }
#endif
virtual void formatRs232Status(char* reply) { strcpy(reply, "RS232 unavailable"); }
#if defined(WITH_RS232_BRIDGE)
virtual void formatRs232Transport(char* reply, size_t capacity) {
@@ -856,6 +878,9 @@ class CommonCLI {
bool _wifi_ota_resume_mqtt = false;
bool _wifi_ota_resume_espnow = false;
#endif
#if defined(WITH_UDP_BRIDGE)
bool _wifi_ota_resume_udp = false;
#endif
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
UserGpio _user_gpio;
#endif
@@ -924,6 +949,9 @@ class CommonCLI {
#endif
#if defined(WITH_ESPNOW_BRIDGE)
bool handleEspNowDiagnosticCommand(uint32_t sender_timestamp, const char* command, char* reply);
#endif
#if defined(WITH_UDP_BRIDGE)
bool handleUDPBridgeCommand(uint32_t sender_timestamp, const char* command, char* reply);
#endif
void handleDelCmd(char* command, char* reply);
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
+5 -1
View File
@@ -168,7 +168,11 @@ StaticPoolPacketManager::StaticPoolPacketManager(int pool_size)
}
mesh::Packet* StaticPoolPacketManager::allocNew() {
return unused.removeByIdx(0); // just get first one (returns NULL if empty)
auto* packet = unused.removeByIdx(0); // NULL if empty.
#if defined(WITH_UDP_BRIDGE)
if (packet) packet->udp_bridge_ingress = false;
#endif
return packet;
}
void StaticPoolPacketManager::free(mesh::Packet* packet) {
+150
View File
@@ -0,0 +1,150 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
namespace mesh { namespace bridge { namespace udp {
static constexpr uint8_t MAX_PEERS = 8;
static constexpr uint16_t DEFAULT_PORT = 4404;
static constexpr size_t PREFS_IMAGE_SIZE = 64;
struct Peer {
uint8_t ip[4] = {};
uint16_t port = 0;
bool enabled() const { return port != 0; }
};
struct Prefs {
uint8_t enabled = 0;
uint16_t port = DEFAULT_PORT;
Peer peers[MAX_PEERS];
};
inline bool validAddress(const uint8_t ip[4]) {
// Subnet-directed broadcasts also depend on the connected interface mask;
// the runtime transport checks those before sending or accepting a peer.
return ip && ip[0] != 0 && ip[0] != 127 && ip[0] < 224;
}
inline bool equalPeer(const Peer& a, const Peer& b) {
return a.port == b.port && memcmp(a.ip, b.ip, 4) == 0;
}
inline bool validPrefs(const Prefs& prefs) {
if (prefs.enabled > 1 || prefs.port == 0) return false;
for (uint8_t i = 0; i < MAX_PEERS; ++i) {
const Peer& peer = prefs.peers[i];
if (!peer.enabled()) {
for (uint8_t octet : peer.ip) if (octet != 0) return false;
continue;
}
if (!validAddress(peer.ip)) return false;
for (uint8_t j = 0; j < i; ++j) {
if (prefs.peers[j].enabled() && equalPeer(peer, prefs.peers[j])) return false;
}
}
return true;
}
inline bool parsePort(const char* text, uint16_t& result) {
if (!text || !*text) return false;
uint32_t value = 0;
for (; *text; ++text) {
if (*text < '0' || *text > '9') return false;
value = value * 10 + static_cast<uint8_t>(*text - '0');
if (value > 65535) return false;
}
if (value == 0) return false;
result = static_cast<uint16_t>(value);
return true;
}
inline bool parsePeer(const char* text, Peer& result) {
if (!text) return false;
if (strcmp(text, "off") == 0) { result = Peer(); return true; }
Peer candidate;
const char* cursor = text;
for (uint8_t i = 0; i < 4; ++i) {
unsigned value = 0, digits = 0;
while (*cursor >= '0' && *cursor <= '9') {
value = value * 10 + static_cast<uint8_t>(*cursor++ - '0');
if (++digits > 3 || value > 255) return false;
}
if (digits == 0 || *cursor++ != (i == 3 ? ':' : '.')) return false;
candidate.ip[i] = static_cast<uint8_t>(value);
}
if (!validAddress(candidate.ip) || !parsePort(cursor, candidate.port)) return false;
result = candidate;
return true;
}
inline void formatPeer(const Peer& peer, char* reply, size_t capacity) {
if (!peer.enabled()) { snprintf(reply, capacity, "off"); return; }
snprintf(reply, capacity, "%u.%u.%u.%u:%u", peer.ip[0], peer.ip[1],
peer.ip[2], peer.ip[3], peer.port);
}
inline bool parseNonce(const char* text, uint32_t& result) {
if (!text || strlen(text) != 8) return false;
uint32_t value = 0;
for (uint8_t i = 0; i < 8; ++i) {
const char c = text[i];
uint8_t digit;
if (c >= '0' && c <= '9') digit = static_cast<uint8_t>(c - '0');
else if (c >= 'a' && c <= 'f') digit = static_cast<uint8_t>(c - 'a' + 10);
else if (c >= 'A' && c <= 'F') digit = static_cast<uint8_t>(c - 'A' + 10);
else return false;
value = (value << 4) | digit;
}
result = value;
return true;
}
inline uint32_t prefsChecksum(const uint8_t* data, size_t size) {
uint32_t crc = 0xffffffffU;
while (size--) {
crc ^= *data++;
for (uint8_t bit = 0; bit < 8; ++bit)
crc = (crc >> 1) ^ ((crc & 1) ? 0xedb88320U : 0);
}
return crc;
}
inline void encodePrefs(uint8_t image[PREFS_IMAGE_SIZE], const Prefs& prefs) {
memset(image, 0, PREFS_IMAGE_SIZE);
memcpy(image, "UDP1", 4);
image[4] = prefs.enabled;
image[6] = prefs.port >> 8; image[7] = prefs.port & 255;
for (uint8_t i = 0; i < MAX_PEERS; ++i) {
memcpy(image + 8 + i * 6, prefs.peers[i].ip, 4);
image[12 + i * 6] = prefs.peers[i].port >> 8;
image[13 + i * 6] = prefs.peers[i].port & 255;
}
const uint32_t crc = prefsChecksum(image, PREFS_IMAGE_SIZE - 4);
for (uint8_t i = 0; i < 4; ++i) image[60 + i] = (crc >> (24 - i * 8)) & 255;
}
inline bool decodePrefs(const uint8_t* image, size_t size, Prefs& prefs) {
if (!image || size != PREFS_IMAGE_SIZE || memcmp(image, "UDP1", 4) != 0
|| image[5] != 0) return false;
for (uint8_t i = 56; i < 60; ++i) if (image[i] != 0) return false;
uint32_t crc = 0;
for (uint8_t i = 0; i < 4; ++i) crc = (crc << 8) | image[60 + i];
if (crc != prefsChecksum(image, PREFS_IMAGE_SIZE - 4)) return false;
Prefs candidate;
candidate.enabled = image[4];
candidate.port = static_cast<uint16_t>((image[6] << 8) | image[7]);
for (uint8_t i = 0; i < MAX_PEERS; ++i) {
memcpy(candidate.peers[i].ip, image + 8 + i * 6, 4);
candidate.peers[i].port = static_cast<uint16_t>(
(image[12 + i * 6] << 8) | image[13 + i * 6]);
}
if (!validPrefs(candidate)) return false;
prefs = candidate;
return true;
}
}}} // namespace mesh::bridge::udp
+51
View File
@@ -0,0 +1,51 @@
#include "UDPBridgeSettings.h"
#if defined(WITH_UDP_BRIDGE)
#include "CheckedSettingsFile.h"
#include "ContactFileTransaction.h"
#include "FilePresence.h"
namespace mesh { namespace bridge { namespace udp {
namespace { constexpr char Path[] = "/udp_bridge"; }
bool Settings::begin(FILESYSTEM* fs) {
fs_ = fs;
prefs_ = Prefs();
healthy_ = false;
if (!fs_ || !ContactFileTransaction::recover(fs_, Path, filePresence<FILESYSTEM>)) return false;
bool present = false;
if (!filePresence(fs_, Path, present)) return false;
if (!present) { healthy_ = true; return true; }
uint8_t image[PREFS_IMAGE_SIZE];
const auto read = storage::readFixedSettingsFileResult(fs_, Path, image, sizeof(image));
if (read == storage::FixedSettingsRead::Unreadable) return false;
healthy_ = true;
return read == storage::FixedSettingsRead::Complete
&& decodePrefs(image, sizeof(image), prefs_);
}
bool Settings::setPrefs(const Prefs& prefs) {
if (!healthy_ || !fs_ || !validPrefs(prefs)) return false;
uint8_t image[PREFS_IMAGE_SIZE];
encodePrefs(image, prefs);
ContactFileTransaction writer(fs_, Path, filePresence<FILESYSTEM>);
if (!writer || writer.write(image, sizeof(image)) != sizeof(image)) {
if (writer.outcome() == ContactFileTransaction::Outcome::Indeterminate) healthy_ = false;
return false;
}
writer.commit();
if (writer.outcome() == ContactFileTransaction::Outcome::NotCommitted) return false;
if (writer.outcome() != ContactFileTransaction::Outcome::Committed) {
healthy_ = false;
return false;
}
uint8_t verified[PREFS_IMAGE_SIZE];
healthy_ = storage::readFixedSettingsFile(fs_, Path, verified, sizeof(verified))
&& memcmp(image, verified, sizeof(image)) == 0;
if (!healthy_) return false;
prefs_ = prefs;
return true;
}
}}} // namespace mesh::bridge::udp
#endif
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#if defined(WITH_UDP_BRIDGE)
#include <helpers/IdentityStore.h>
#include <helpers/UDPBridgePrefs.h>
namespace mesh { namespace bridge { namespace udp {
// This standalone record leaves the legacy /com_prefs binary layout intact.
class Settings {
FILESYSTEM* fs_ = nullptr;
Prefs prefs_;
bool healthy_ = false;
public:
// Missing/corrupt settings leave the transport off. A complete malformed
// record can be replaced explicitly; I/O uncertainty requires a reload.
bool begin(FILESYSTEM* fs);
const Prefs& prefs() const { return prefs_; }
bool healthy() const { return healthy_; }
// Publish and verify first; preserve runtime preferences on failed saves.
bool setPrefs(const Prefs& prefs);
};
}}} // namespace mesh::bridge::udp
#endif
+2 -1
View File
@@ -65,7 +65,8 @@ private:
enum class NetworkPowerHold : uint8_t {
CompanionClient = 1,
WebConfig = 2,
OtaSeeder = 4
OtaSeeder = 4,
UdpBridge = 8
};
// One program-wide handoff between the MQTT owner task and the station/UI
+5
View File
@@ -66,6 +66,11 @@ public:
void begin(Backend& backend) { backend_ = &backend; }
bool blocked(uint8_t service) const { return (blocked_.load() & service) != 0; }
bool pending() const { return pending_; }
// New background owners must honor the reply-drain grace period before
// the backend actually shuts down a requested service.
bool pendingDisable(uint8_t service) const {
return pending_ && (scope_ & service) && !(target_ & service);
}
bool masterOff() const { return master_off_; }
bool allowService(uint8_t service) {
if (master_off_ || pending_) return false;
+4 -4
View File
@@ -36,7 +36,7 @@ void BridgeBase::handleReceivedPacket(mesh::Packet *packet) {
if (_initialized == false) {
BRIDGE_DEBUG_PRINTLN("RX packet received before initialization\n");
_mgr->free(packet);
#if defined(WITH_ESPNOW_BRIDGE) || defined(WITH_RS232_BRIDGE)
#if defined(WITH_ESPNOW_BRIDGE) || defined(WITH_RS232_BRIDGE) || defined(WITH_UDP_BRIDGE)
onIngressResult(IngressResult::Stopped);
#endif
return;
@@ -44,7 +44,7 @@ void BridgeBase::handleReceivedPacket(mesh::Packet *packet) {
if (!preparePacketIngress(packet) || !allowsPacket(packet)) {
_mgr->free(packet);
#if defined(WITH_ESPNOW_BRIDGE) || defined(WITH_RS232_BRIDGE)
#if defined(WITH_ESPNOW_BRIDGE) || defined(WITH_RS232_BRIDGE) || defined(WITH_UDP_BRIDGE)
onIngressResult(IngressResult::Filtered);
#endif
return;
@@ -58,12 +58,12 @@ void BridgeBase::handleReceivedPacket(mesh::Packet *packet) {
if (!ota) _seen_packets.markSeen(packet);
// bridge_delay provides a buffer to prevent immediate processing conflicts in the mesh network.
_mgr->queueInbound(packet, millis() + _prefs->bridge_delay);
#if defined(WITH_ESPNOW_BRIDGE) || defined(WITH_RS232_BRIDGE)
#if defined(WITH_ESPNOW_BRIDGE) || defined(WITH_RS232_BRIDGE) || defined(WITH_UDP_BRIDGE)
onIngressResult(IngressResult::Queued);
#endif
} else {
_mgr->free(packet);
#if defined(WITH_ESPNOW_BRIDGE) || defined(WITH_RS232_BRIDGE)
#if defined(WITH_ESPNOW_BRIDGE) || defined(WITH_RS232_BRIDGE) || defined(WITH_UDP_BRIDGE)
onIngressResult(IngressResult::Duplicate);
#endif
}
+1 -1
View File
@@ -50,7 +50,7 @@ public:
static constexpr uint16_t BRIDGE_CHECKSUM_SIZE = sizeof(uint16_t);
protected:
#if defined(WITH_ESPNOW_BRIDGE) || defined(WITH_RS232_BRIDGE)
#if defined(WITH_ESPNOW_BRIDGE) || defined(WITH_RS232_BRIDGE) || defined(WITH_UDP_BRIDGE)
// Observe the existing admission decision without evaluating filters twice.
enum class IngressResult : uint8_t { Stopped, Filtered, Duplicate, Queued };
virtual void onIngressResult(IngressResult) {}
+340
View File
@@ -0,0 +1,340 @@
#include "UDPBridge.h"
#if defined(ESP32) && defined(WITH_UDP_BRIDGE)
#include <Arduino.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <lwip/sockets.h>
#include <SHA256.h>
#include <esp_system.h>
namespace udp = mesh::bridge::udp;
static_assert(sizeof(UDPBridge) <= 3072, "UDP bridge object exceeds its Full memory reserve");
UDPBridge::UDPBridge(NodePrefs* prefs, mesh::PacketManager* mgr, mesh::RTCClock* rtc,
const udp::Prefs* settings) : BridgeBase(prefs, mgr, rtc), _settings(settings) {}
UDPBridge::~UDPBridge() { end(); }
void UDPBridge::note(uint32_t& value) { udp::increment(value); _debug_dirty = true; }
void UDPBridge::error(int code) { _last_error = code; _debug_dirty = true; }
uint64_t UDPBridge::randomNonce() {
uint64_t value;
do { value = (uint64_t(esp_random()) << 32) | esp_random(); } while (!value);
return value;
}
bool UDPBridge::authenticate(void* context, const uint8_t* data, size_t size, uint8_t digest[32]) {
const auto* self = static_cast<const UDPBridge*>(context);
if (!self->_secret_valid) return false;
SHA256 sha;
static const uint8_t domain[] = "MeshCore UDP bridge v1";
sha.resetHMAC(self->_secret, self->_secret_length);
sha.update(domain, sizeof(domain));
sha.update(data, size);
sha.finalizeHMAC(self->_secret, self->_secret_length, digest, 32);
return true;
}
void UDPBridge::begin() {
if (_initialized || !_network_ready || !_settings || !_settings->enabled) return;
_active = *_settings;
_secret_length = 0;
while (_secret_length < sizeof(_prefs->bridge_secret) && _prefs->bridge_secret[_secret_length]) ++_secret_length;
_secret_valid = _secret_length > 0 && _secret_length < sizeof(_prefs->bridge_secret);
if (!_secret_valid || !udp::validPrefs(_active)) { error(EINVAL); return; }
for (const auto& peer : _active.peers) {
if (!peer.port) continue;
uint32_t address; memcpy(&address, peer.ip, sizeof(address));
if (_local_address && (address == _local_address || (_netmask
&& (address == (_local_address | ~_netmask)
|| address == (_local_address & _netmask))))) {
error(EADDRNOTAVAIL); return;
}
}
memcpy(_secret, _prefs->bridge_secret, _secret_length);
_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (_socket < 0) { error(errno); return; }
const int flags = fcntl(_socket, F_GETFL, 0);
if (flags < 0 || fcntl(_socket, F_SETFL, flags | O_NONBLOCK) < 0) {
const int code = errno; end(); error(code); return;
}
// Bound lwIP's receive-byte accounting where supported. Application ingress
// is additionally limited to two datagrams per loop and one owned RX slot.
#if defined(SO_RCVBUF)
const int receive_bytes = 768;
if (setsockopt(_socket, SOL_SOCKET, SO_RCVBUF, &receive_bytes, sizeof(receive_bytes)) != 0) {
const int code = errno; end(); error(code); return;
}
#else
end(); error(ENOTSUP); return;
#endif
sockaddr_in local{};
local.sin_family = AF_INET; local.sin_port = htons(_active.port); local.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(_socket, reinterpret_cast<sockaddr*>(&local), sizeof(local)) != 0) {
const int code = errno; end(); error(code); return;
}
_boot = randomNonce(); _sequence = 0;
for (auto& session : _sessions) { session = udp::Session{}; session.next_hello = millis() + esp_random() % 1000; }
_initialized = true; _last_error = 0; _debug_dirty = true;
}
void UDPBridge::end() {
if (_socket >= 0) { close(_socket); _socket = -1; }
_initialized = false; _tx_mask = 0; _tx_length = _rx_length = 0;
_boot = 0; _sequence = 0;
for (auto& session : _sessions) session = udp::Session{};
memset(_secret, 0, sizeof(_secret)); _secret_length = 0; _secret_valid = false;
_debug_dirty = true;
}
void UDPBridge::serviceNetwork(bool ready, uint32_t local_address, uint32_t netmask) {
// The shared bridge secret can change without a transport-specific restart
// callback. End the old authenticated session before any more socket work.
if (_initialized) {
const size_t length = strnlen(_prefs->bridge_secret, sizeof(_prefs->bridge_secret));
if (!length || length >= sizeof(_prefs->bridge_secret) || length != _secret_length
|| memcmp(_secret, _prefs->bridge_secret, length)) {
end(); _retry_at = millis();
}
}
if (_local_address != local_address || _netmask != netmask) {
_local_address = local_address; _netmask = netmask;
if (_initialized) end();
_retry_at = millis();
}
if (_network_ready != ready) { _network_ready = ready; _debug_dirty = true; _retry_at = millis(); }
if (!ready || !_settings || !_settings->enabled) {
if (_socket >= 0 || _initialized) end();
return;
}
const uint32_t now = millis();
if (!_initialized && udp::due(now, _retry_at)) { _retry_at = now + 5000; begin(); }
}
uint8_t UDPBridge::peerMask(bool established_only) const {
uint8_t mask = 0;
for (uint8_t i = 0; i < udp::MAX_PEERS; ++i)
if (_active.peers[i].port && (!established_only || _sessions[i].remote)) mask |= uint8_t(1U << i);
return mask;
}
uint32_t UDPBridge::nextSequence() {
// Never reuse a sequence within the same boot/session. Reopen on the next
// service iteration; queued data is explicitly best-effort and discarded.
if (_sequence == UINT32_MAX) { end(); error(EOVERFLOW); return 0; }
return ++_sequence;
}
bool UDPBridge::sendFrame(uint8_t peer, udp::Kind kind, uint64_t recipient,
const uint8_t* payload, uint16_t length, uint32_t sequence) {
if (!_initialized || peer >= udp::MAX_PEERS || !_active.peers[peer].port) return false;
if (!sequence) sequence = nextSequence();
if (!_initialized || !sequence) return false;
const udp::Frame frame{kind, _boot, recipient, sequence, payload, length};
uint8_t bytes[udp::MAX_FRAME];
const size_t size = udp::encode(bytes, sizeof(bytes), frame, authenticate, this);
if (!size) { error(EINVAL); return false; }
const auto& endpoint = _active.peers[peer];
sockaddr_in address{}; address.sin_family = AF_INET; address.sin_port = htons(endpoint.port);
memcpy(&address.sin_addr.s_addr, endpoint.ip, sizeof(endpoint.ip));
const int sent = sendto(_socket, bytes, size, MSG_DONTWAIT,
reinterpret_cast<const sockaddr*>(&address), sizeof(address));
if (sent != static_cast<int>(size)) { error(sent < 0 ? errno : EIO); return false; }
return true;
}
void UDPBridge::pumpHello() {
const uint32_t now = millis();
for (uint8_t attempts = 0; attempts < udp::MAX_PEERS; ++attempts) {
const uint8_t i = _hello_cursor++ % udp::MAX_PEERS;
if (!_active.peers[i].port) continue;
auto& session = _sessions[i];
if (session.candidate && !udp::due(now, session.challenge_deadline)
&& uint32_t(now - session.challenge_sent_at) >= 1000) {
uint8_t nonce[8]; udp::put64(nonce, session.challenge);
session.challenge_sent_at = now;
if (sendFrame(i, udp::Kind::Challenge, session.candidate, nonce, sizeof(nonce))) note(_stats.challenges);
return; // Retransmit handshake control only, never data or test probes.
}
if (!udp::due(now, session.next_hello)) continue;
_sessions[i].next_hello = now + (_sessions[i].remote ? 10000 : 1000) + esp_random() % 500;
if (sendFrame(i, udp::Kind::Hello, 0, nullptr, 0)) note(_stats.hellos);
return; // At most one periodic control datagram per main loop.
}
}
void UDPBridge::receiveFrame(uint8_t peer, const uint8_t* data, size_t length) {
udp::Frame frame;
const auto decoded = udp::decode(data, length, frame, authenticate, this);
if (decoded != udp::Decode::Ok) {
note(decoded == udp::Decode::Authentication ? _stats.authentication : _stats.malformed); return;
}
note(_stats.authenticated);
if (frame.sender == _boot) { note(_stats.foreign); return; }
auto& session = _sessions[peer];
const uint32_t now = millis();
if (frame.kind == udp::Kind::Hello) {
// Refreshing the same proven boot does not reset its replay high-water.
if (session.request(frame.sender, randomNonce(), now)) {
uint8_t nonce[8]; udp::put64(nonce, session.challenge);
if (sendFrame(peer, udp::Kind::Challenge, frame.sender, nonce, sizeof(nonce))) note(_stats.challenges);
}
return;
}
if (frame.recipient != _boot) { note(_stats.replay); return; }
if (frame.kind == udp::Kind::Challenge) {
// Proof is the only automatic control echo. Explicit probes never echo.
(void)sendFrame(peer, udp::Kind::Proof, frame.sender, frame.payload, frame.length);
return;
}
if (frame.kind == udp::Kind::Proof) {
const bool changed = session.remote != frame.sender;
if (session.prove(frame.sender, udp::get64(frame.payload), now)) {
note(_stats.established);
// Expedite the reciprocal challenge after first learning a boot. This
// also recovers a dropped startup Hello without a long ready interval.
if (changed) session.next_hello = now;
} else note(_stats.replay);
return;
}
if (!session.admit(frame.sender, frame.recipient, _boot, frame.sequence)) { note(_stats.replay); return; }
if (frame.kind == udp::Kind::Probe) {
_stats.last_rx_nonce = udp::get32(frame.payload); note(_stats.probe_received); return;
}
// Copy verified bytes into one bounded holding slot before allocating from
// the shared mesh pool. A full slot stops socket reads, preserving its owner.
memcpy(_rx_payload, frame.payload, frame.length); _rx_length = frame.length;
}
void UDPBridge::pumpReceive() {
for (uint8_t count = 0; count < 2 && !_rx_length && _initialized; ++count) {
uint8_t bytes[udp::MAX_FRAME + 1]; sockaddr_in source{}; socklen_t source_size = sizeof(source);
const int length = recvfrom(_socket, bytes, sizeof(bytes), MSG_DONTWAIT,
reinterpret_cast<sockaddr*>(&source), &source_size);
if (length < 0) {
if (errno != EAGAIN && errno != EWOULDBLOCK) error(errno);
return;
}
note(_stats.frames);
int peer = -1;
for (uint8_t i = 0; i < udp::MAX_PEERS; ++i)
if (_active.peers[i].port && source.sin_port == htons(_active.peers[i].port)
&& !memcmp(&source.sin_addr.s_addr, _active.peers[i].ip, 4)) { peer = i; break; }
if (source_size != sizeof(source) || source.sin_family != AF_INET || peer < 0) { note(_stats.foreign); continue; }
receiveFrame(static_cast<uint8_t>(peer), bytes, static_cast<size_t>(length));
drainReceive();
}
}
void UDPBridge::drainReceive() {
if (!_rx_length) return;
mesh::Packet* packet = _mgr->allocNew();
if (!packet) { note(_stats.pool); return; }
const bool parsed = packet->readFrom(_rx_payload, static_cast<uint8_t>(_rx_length));
_rx_length = 0;
if (!parsed) { _mgr->free(packet); note(_stats.parse); return; }
packet->udp_bridge_ingress = true;
onPacketReceived(packet);
}
void UDPBridge::onPacketReceived(mesh::Packet* packet) {
if (packet) { packet->udp_bridge_ingress = true; handleReceivedPacket(packet); }
}
void UDPBridge::onIngressResult(IngressResult result) {
switch (result) {
case IngressResult::Queued: note(_stats.delivered); break;
case IngressResult::Filtered: note(_stats.filtered); break;
case IngressResult::Duplicate: note(_stats.duplicate); break;
case IngressResult::Stopped: break;
}
}
void UDPBridge::sendPacket(mesh::Packet* packet) {
note(_stats.requested);
if (!_initialized) { note(_stats.stopped); return; }
if (!packet || packet->udp_bridge_ingress || !allowsPacket(packet)) { note(_stats.tx_filtered); return; }
const bool ota = packet->getPayloadType() == PAYLOAD_TYPE_OTA;
if (!ota && _seen_packets.wasSeen(packet)) { note(_stats.tx_duplicate); return; }
const uint8_t destinations = peerMask(true);
if (_tx_mask || !destinations) { note(_stats.busy); return; }
const int length = packet->getRawLength();
if (length < 2 || length > static_cast<int>(sizeof(_tx_payload))) { note(_stats.oversize); return; }
const uint32_t sequence = nextSequence();
if (!sequence) { note(_stats.stopped); return; }
if (packet->writeTo(_tx_payload) != length) { note(_stats.failed); return; }
_tx_length = length; _tx_mask = destinations; _tx_kind = udp::Kind::Mesh; _tx_sequence = sequence;
if (!ota) _seen_packets.markSeen(packet);
note(_stats.queued);
}
void UDPBridge::pumpTransmit() {
if (!_tx_mask) return;
uint8_t peer = 0;
while (!(_tx_mask & (1U << peer))) ++peer;
_tx_mask &= static_cast<uint8_t>(~(1U << peer));
const bool sent = sendFrame(peer, _tx_kind, _sessions[peer].remote, _tx_payload, _tx_length, _tx_sequence);
if (_tx_kind == udp::Kind::Probe) note(sent ? _stats.probe_sent : _stats.probe_failed);
else note(sent ? _stats.sent : _stats.failed);
if (!_tx_mask) _tx_length = 0;
}
bool UDPBridge::sendTest(uint8_t peer, uint32_t nonce) {
if (!_initialized || peer >= udp::MAX_PEERS || !(peerMask(true) & (1U << peer)) || _tx_mask) {
note(_stats.probe_failed); return false;
}
const uint32_t sequence = nextSequence();
if (!sequence) { note(_stats.probe_failed); return false; }
udp::put32(_tx_payload, nonce); _tx_length = 4;
_tx_mask = static_cast<uint8_t>(1U << peer); _tx_kind = udp::Kind::Probe; _tx_sequence = sequence;
_stats.last_tx_nonce = nonce; note(_stats.probe_queued); return true;
}
void UDPBridge::loop() {
if (_initialized) { drainReceive(); pumpReceive(); pumpTransmit(); pumpHello(); }
emitDebug();
}
void UDPBridge::resetStats() { _stats = Stats{}; _debug_dirty = true; }
void UDPBridge::formatStatus(char* out, size_t capacity) const {
if (!out || !capacity) return;
unsigned peers = 0, ready = 0;
for (uint8_t i = 0; i < udp::MAX_PEERS; ++i) {
const auto* prefs = _initialized ? &_active : _settings;
if (prefs && prefs->peers[i].port) ++peers;
if (_sessions[i].remote) ++ready;
}
const char* state = !_settings || !_settings->enabled ? "off" : !_network_ready ? "netwait"
: !_initialized ? "retrying" : "ready";
const uint8_t* ip = reinterpret_cast<const uint8_t*>(&_local_address);
snprintf(out, capacity, "> %s ip=%u.%u.%u.%u port=%u peers=%u ready=%u src=%s q=%u/%u last=%d", state,
unsigned(ip[0]), unsigned(ip[1]), unsigned(ip[2]), unsigned(ip[3]),
unsigned(_initialized ? _active.port : _settings ? _settings->port : 0), peers, ready,
_prefs->bridge_pkt_src ? "rx" : "tx", unsigned(_rx_length != 0), unsigned(_tx_mask != 0), _last_error);
out[capacity - 1] = 0;
}
void UDPBridge::formatStats(const char* page, char* out, size_t capacity) const {
if (!out || !capacity) return;
const auto& s = _stats;
if (!page || !page[0]) snprintf(out, capacity, "> rx=%lu mesh=%lu tx=%lu sent=%lu; pages: rx tx drops test",
(unsigned long)s.frames, (unsigned long)s.delivered, (unsigned long)s.queued, (unsigned long)s.sent);
else if (!strcmp(page, "rx")) snprintf(out, capacity, "> frames=%lu auth=%lu mesh=%lu parse=%lu pool=%lu hello=%lu proof=%lu",
(unsigned long)s.frames, (unsigned long)s.authenticated, (unsigned long)s.delivered,
(unsigned long)s.parse, (unsigned long)s.pool, (unsigned long)s.hellos, (unsigned long)s.established);
else if (!strcmp(page, "tx")) snprintf(out, capacity, "> req=%lu queued=%lu sent=%lu err=%lu busy=%lu stopped=%lu",
(unsigned long)s.requested, (unsigned long)s.queued, (unsigned long)s.sent,
(unsigned long)s.failed, (unsigned long)s.busy, (unsigned long)s.stopped);
else if (!strcmp(page, "drops")) snprintf(out, capacity, "> bad=%lu auth=%lu foreign=%lu replay=%lu filter=%lu dupe=%lu txfilter=%lu txdupe=%lu big=%lu",
(unsigned long)s.malformed, (unsigned long)s.authentication, (unsigned long)s.foreign, (unsigned long)s.replay,
(unsigned long)s.filtered, (unsigned long)s.duplicate, (unsigned long)s.tx_filtered,
(unsigned long)s.tx_duplicate, (unsigned long)s.oversize);
else if (!strcmp(page, "test")) snprintf(out, capacity, "> queued=%lu sent=%lu fail=%lu rx=%lu txid=%08lX rxid=%08lX",
(unsigned long)s.probe_queued, (unsigned long)s.probe_sent, (unsigned long)s.probe_failed,
(unsigned long)s.probe_received, (unsigned long)s.last_tx_nonce, (unsigned long)s.last_rx_nonce);
else snprintf(out, capacity, "Error: use get udp.stats [rx|tx|drops|test]");
out[capacity - 1] = 0;
}
void UDPBridge::emitDebug() {
if (!mesh::isUsbDebugLoggingEnabled()) { _debug_was_enabled = false; return; }
if (!_debug_was_enabled) _debug_dirty = true;
_debug_was_enabled = true;
const uint32_t now = millis();
if (!_debug_dirty || uint32_t(now - _debug_last_at) < 1000) return;
_debug_last_at = now; _debug_dirty = false;
char status[157]; formatStatus(status, sizeof(status));
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
mesh::nrf52DebugPrintf("UDP: %s rx=%lu mesh=%lu authbad=%lu replay=%lu tx=%lu sent=%lu\n", status + 2,
(unsigned long)_stats.frames, (unsigned long)_stats.delivered, (unsigned long)_stats.authentication,
(unsigned long)_stats.replay, (unsigned long)_stats.queued, (unsigned long)_stats.sent);
#else
char line[256];
const int length = snprintf(line, sizeof(line), "UDP: %s rx=%lu mesh=%lu authbad=%lu replay=%lu tx=%lu sent=%lu\n", status + 2,
(unsigned long)_stats.frames, (unsigned long)_stats.delivered, (unsigned long)_stats.authentication,
(unsigned long)_stats.replay, (unsigned long)_stats.queued, (unsigned long)_stats.sent);
if (length > 0 && size_t(length) < sizeof(line) && mesh::usbLoggingPort().availableForWrite() >= length)
mesh::usbLoggingPort().write(reinterpret_cast<const uint8_t*>(line), length);
#endif
}
#endif
+76
View File
@@ -0,0 +1,76 @@
#pragma once
#if defined(ESP32) && defined(WITH_UDP_BRIDGE)
#include "helpers/bridges/BridgeBase.h"
#include "helpers/bridges/UDPBridgeProtocol.h"
#include "helpers/UDPBridgePrefs.h"
// All methods run on the mesh loop. No WiFi callbacks own packet memory.
// Network ownership belongs to the role; this class never starts/stops WiFi.
class UDPBridge : public BridgeBase {
public:
UDPBridge(NodePrefs* prefs, mesh::PacketManager* mgr, mesh::RTCClock* rtc,
const mesh::bridge::udp::Prefs* settings);
~UDPBridge() override;
void begin() override;
void end() override;
void loop() override;
void serviceNetwork(bool ready, uint32_t local_address = 0, uint32_t netmask = 0);
void sendPacket(mesh::Packet* packet) override;
void onPacketReceived(mesh::Packet* packet) override;
void formatStatus(char* out, size_t capacity) const;
void formatStats(const char* page, char* out, size_t capacity) const;
bool sendTest(uint8_t peer_index, uint32_t nonce);
void resetStats();
private:
const mesh::bridge::udp::Prefs* _settings;
mesh::bridge::udp::Prefs _active{};
mesh::bridge::udp::Session _sessions[mesh::bridge::udp::MAX_PEERS]{};
uint64_t _boot = 0;
uint32_t _sequence = 0, _retry_at = 0;
uint32_t _local_address = 0, _netmask = 0;
int _socket = -1, _last_error = 0;
bool _network_ready = false, _secret_valid = false;
uint8_t _secret[16]{};
uint8_t _secret_length = 0, _hello_cursor = 0;
// One logical fan-out packet; each peer receives at most one send attempt.
uint8_t _tx_payload[mesh::bridge::udp::MAX_PAYLOAD]{};
uint16_t _tx_length = 0;
uint8_t _tx_mask = 0;
mesh::bridge::udp::Kind _tx_kind = mesh::bridge::udp::Kind::Mesh;
uint32_t _tx_sequence = 0;
// One verified receive slot survives packet-pool pressure; socket ingress
// stops until it drains. No packet allocation for probes or invalid frames.
uint8_t _rx_payload[mesh::bridge::udp::MAX_PAYLOAD]{};
uint16_t _rx_length = 0;
struct Stats {
uint32_t frames = 0, authenticated = 0, delivered = 0;
uint32_t malformed = 0, authentication = 0, foreign = 0, replay = 0;
uint32_t filtered = 0, duplicate = 0, pool = 0, parse = 0;
uint32_t requested = 0, queued = 0, sent = 0, failed = 0;
uint32_t busy = 0, stopped = 0, oversize = 0, tx_filtered = 0, tx_duplicate = 0;
uint32_t hellos = 0, challenges = 0, established = 0;
uint32_t probe_queued = 0, probe_sent = 0, probe_failed = 0, probe_received = 0;
uint32_t last_tx_nonce = 0, last_rx_nonce = 0;
} _stats;
bool _debug_dirty = true, _debug_was_enabled = false;
uint32_t _debug_last_at = 0;
static bool authenticate(void* context, const uint8_t* data, size_t size, uint8_t digest[32]);
static uint64_t randomNonce();
uint8_t peerMask(bool established_only) const;
uint32_t nextSequence();
bool sendFrame(uint8_t peer, mesh::bridge::udp::Kind kind, uint64_t recipient,
const uint8_t* payload, uint16_t length, uint32_t sequence = 0);
void receiveFrame(uint8_t peer, const uint8_t* data, size_t length);
void pumpReceive();
void drainReceive();
void pumpTransmit();
void pumpHello();
void emitDebug();
void note(uint32_t& value);
void error(int code);
void onIngressResult(IngressResult result) override;
};
#endif
+104
View File
@@ -0,0 +1,104 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <string.h>
namespace mesh { namespace bridge { namespace udp {
// A LAN transport, independent of the selected ESP-NOW raw/wrapped format.
// Every field and payload is authenticated. HMAC does not encrypt contents.
static constexpr size_t HEADER_SIZE = 28;
static constexpr size_t TAG_SIZE = 16;
static constexpr size_t MAX_PAYLOAD = 255;
static constexpr size_t MAX_FRAME = HEADER_SIZE + MAX_PAYLOAD + TAG_SIZE;
enum class Kind : uint8_t { Hello = 1, Challenge = 2, Proof = 3, Mesh = 4, Probe = 5 };
using Authenticate = bool (*)(void*, const uint8_t*, size_t, uint8_t digest[32]);
struct Frame {
Kind kind = Kind::Hello;
uint64_t sender = 0, recipient = 0;
uint32_t sequence = 0;
const uint8_t* payload = nullptr;
uint16_t length = 0;
};
inline void put32(uint8_t* out, uint32_t v) {
out[0] = v >> 24; out[1] = v >> 16; out[2] = v >> 8; out[3] = v;
}
inline uint32_t get32(const uint8_t* in) {
return (uint32_t(in[0]) << 24) | (uint32_t(in[1]) << 16)
| (uint32_t(in[2]) << 8) | in[3];
}
inline void put64(uint8_t* out, uint64_t v) { put32(out, v >> 32); put32(out + 4, v); }
inline uint64_t get64(const uint8_t* in) { return (uint64_t(get32(in)) << 32) | get32(in + 4); }
inline bool validShape(const Frame& f) {
if (!f.sender || !f.sequence || f.length > MAX_PAYLOAD || (f.length && !f.payload)) return false;
switch (f.kind) {
case Kind::Hello: return !f.recipient && f.length == 0;
case Kind::Challenge: case Kind::Proof: return f.recipient && f.length == 8 && get64(f.payload);
case Kind::Mesh: return f.recipient && f.length >= 2;
case Kind::Probe: return f.recipient && f.length == 4;
default: return false;
}
}
inline size_t encode(uint8_t* out, size_t capacity, const Frame& f,
Authenticate auth, void* context) {
const size_t size = HEADER_SIZE + f.length + TAG_SIZE;
if (!out || !auth || capacity < size || !validShape(f)) return 0;
memcpy(out, "MCUD", 4); out[4] = 1; out[5] = uint8_t(f.kind);
out[6] = f.length >> 8; out[7] = f.length;
put64(out + 8, f.sender); put64(out + 16, f.recipient); put32(out + 24, f.sequence);
if (f.length) memcpy(out + HEADER_SIZE, f.payload, f.length);
uint8_t digest[32];
if (!auth(context, out, HEADER_SIZE + f.length, digest)) return 0;
memcpy(out + HEADER_SIZE + f.length, digest, TAG_SIZE);
memset(digest, 0, sizeof(digest));
return size;
}
enum class Decode { Ok, Malformed, Authentication };
inline Decode decode(const uint8_t* data, size_t size, Frame& result,
Authenticate auth, void* context) {
if (!data || !auth || size < HEADER_SIZE + TAG_SIZE || size > MAX_FRAME
|| memcmp(data, "MCUD", 4) || data[4] != 1) return Decode::Malformed;
Frame f;
f.kind = Kind(data[5]); f.length = (uint16_t(data[6]) << 8) | data[7];
f.sender = get64(data + 8); f.recipient = get64(data + 16); f.sequence = get32(data + 24);
f.payload = data + HEADER_SIZE;
if (size != HEADER_SIZE + f.length + TAG_SIZE || !validShape(f)) return Decode::Malformed;
uint8_t digest[32];
if (!auth(context, data, HEADER_SIZE + f.length, digest)) return Decode::Authentication;
uint8_t different = 0;
for (size_t i = 0; i < TAG_SIZE; ++i) different |= digest[i] ^ data[HEADER_SIZE + f.length + i];
memset(digest, 0, sizeof(digest));
if (different) return Decode::Authentication;
result = f;
return Decode::Ok;
}
inline bool due(uint32_t now, uint32_t deadline) { return int32_t(now - deadline) >= 0; }
inline void increment(uint32_t& value) { if (value != UINT32_MAX) ++value; }
// Each configured endpoint has its own authenticated receive session. A Hello
// never replaces a live session. Only proof of a fresh local challenge can do
// so, making recorded old sessions unable to flip the receiver backwards.
struct Session {
uint64_t remote = 0, candidate = 0, challenge = 0;
uint32_t received = 0, challenge_deadline = 0, next_hello = 0, challenge_sent_at = 0;
bool request(uint64_t sender, uint64_t nonce, uint32_t now) {
if (!sender || !nonce || (candidate && !due(now, challenge_deadline))) return false;
candidate = sender; challenge = nonce; challenge_deadline = now + 5000; challenge_sent_at = now;
return true;
}
bool prove(uint64_t sender, uint64_t nonce, uint32_t now) {
if (!candidate || sender != candidate || nonce != challenge || due(now, challenge_deadline)) return false;
if (remote != sender) { remote = sender; received = 0; }
candidate = challenge = 0;
return true;
}
bool admit(uint64_t sender, uint64_t recipient, uint64_t local, uint32_t sequence) {
if (!remote || sender != remote || recipient != local || !sequence || sequence <= received) return false;
received = sequence;
return true;
}
};
} } } // namespace mesh::bridge::udp
+1 -1
View File
@@ -7,7 +7,7 @@
// WiFi SDK check when a compiled service can own that driver. Use build flags,
// rather than WITH_WEBCONFIG, which is defined by header inclusion order.
#if defined(WIFI_SSID) || defined(ENABLE_WIFI_INTERFACE) \
|| defined(WITH_MQTT_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) \
|| defined(WITH_MQTT_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) || defined(WITH_UDP_BRIDGE) \
|| (defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW) \
|| defined(WIFI_OTA_SEEDER) || defined(ADMIN_PASSWORD) \
|| defined(COMPANION_RADIO_FULL)