Add station-only WiFi UDP bridge with eight authenticated peers
This commit is contained in:
1 parent
7bc3aeb234
commit
5e25faa3d0
45 files changed
+2699
-35
No files matched your search
@@ -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
@@ -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>
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user