Compare commits

...

9 Commits

Author SHA1 Message Date
Jonathan Bennett 689e290670 Use the right transport 2026-06-08 19:17:02 -05:00
Jonathan Bennett 708a194675 Fix double bracket 2026-06-08 18:42:07 -05:00
Jonathan Bennett b8edc9ca96 trunk 2026-06-08 18:37:03 -05:00
Jonathan Bennett 8e29d44e19 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-08 18:35:46 -05:00
Jonathan Bennett dad92dea9e Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-08 18:35:35 -05:00
Jonathan Bennett e05b112f68 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-08 18:34:51 -05:00
Jonathan Bennett fe615d7bf1 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-08 18:34:25 -05:00
Jonathan Bennett bcd9b5508f trunk 2026-06-08 16:50:58 -05:00
Jonathan Bennett ac1f7f0f80 UDP Unicast for Meshtasticd 2026-06-08 16:48:18 -05:00
7 changed files with 232 additions and 24 deletions
+19
View File
@@ -160,6 +160,11 @@ extern void tftSetup(void);
UdpMulticastHandler *udpHandler = nullptr;
#endif
#ifdef ARCH_PORTDUINO
#include "mesh/udp/UdpUnicastConnector.h"
UdpUnicastConnector *udpUnicastConnector = nullptr;
#endif
#if defined(TCXO_OPTIONAL)
float tcxoVoltage = SX126X_DIO3_TCXO_VOLTAGE; // if TCXO is optional, put this here so it can be changed further down.
#endif
@@ -932,6 +937,20 @@ void setup()
}
#endif
#endif
#ifdef ARCH_PORTDUINO
// Native unicast-UDP connector to the meshswitch routing daemon, enabled by setting a peer
// address in config.yaml (General -> UDPUnicastPeer). Independent of UDP multicast.
if (!portduino_config.udp_unicast_peer.empty()) {
LOG_DEBUG("Start UDP unicast connector");
udpUnicastConnector = new UdpUnicastConnector();
if (!udpUnicastConnector->start(portduino_config.udp_unicast_peer)) {
delete udpUnicastConnector;
udpUnicastConnector = nullptr;
}
}
#endif
service = new MeshService();
service->init();
+5
View File
@@ -67,6 +67,11 @@ extern AudioThread *audioThread;
extern UdpMulticastHandler *udpHandler;
#endif
#ifdef ARCH_PORTDUINO
#include "mesh/udp/UdpUnicastConnector.h"
extern UdpUnicastConnector *udpUnicastConnector;
#endif
// Global Screen singleton.
extern graphics::Screen *screen;
+1 -1
View File
@@ -98,13 +98,13 @@ class Channels
int16_t getHash(ChannelIndex i) { return hashes[i]; }
private:
/** Given a channel index, change to use the crypto key specified by that index
*
* @eturn the (0 to 255) hash for that channel - if no suitable channel could be found, return -1
*/
int16_t setCrypto(ChannelIndex chIndex);
private:
/** Return the channel index for the specified channel hash, or -1 for not found */
int8_t getIndexByHash(ChannelHash channelHash);
+54 -23
View File
@@ -434,6 +434,13 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
}
#endif
#ifdef ARCH_PORTDUINO
// Mirror every outbound packet to the meshswitch routing daemon, if a unicast peer is configured.
if (udpUnicastConnector) {
udpUnicastConnector->onSend(p);
}
#endif
assert(iface); // This should have been detected already in sendLocal (or we just received a packet from outside)
return iface->send(p);
}
@@ -464,6 +471,34 @@ void Router::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Rout
// FIXME, update nodedb here for any packet that passes through us
}
bool attemptAESDecrypt(meshtastic_MeshPacket *p, size_t rawSize)
{
memcpy(bytes, p->encrypted.bytes, rawSize);
// Try to decrypt the packet if we can
crypto->decrypt(p->from, p->id, rawSize, bytes);
// printBytes("plaintext", bytes, p->encrypted.size);
// Take those raw bytes and convert them back into a well structured protobuf we can understand
meshtastic_Data decodedtmp;
memset(&decodedtmp, 0, sizeof(decodedtmp));
if (!pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp)) {
LOG_DEBUG("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)", p->id);
} else if (decodedtmp.portnum == meshtastic_PortNum_UNKNOWN_APP) {
LOG_DEBUG("Invalid portnum (bad psk?)");
#if !(MESHTASTIC_EXCLUDE_PKI)
} else if (!owner.is_licensed && isToUs(p) && decodedtmp.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) {
LOG_WARN("Rejecting legacy DM");
return false;
#endif
} else {
p->decoded = decodedtmp;
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
return true;
}
return false;
}
DecodeState perhapsDecode(meshtastic_MeshPacket *p)
{
concurrency::LockGuard g(cryptLock);
@@ -523,30 +558,26 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
for (chIndex = 0; chIndex < channels.getNumChannels(); chIndex++) {
// Try to use this hash/channel pair
if (channels.decryptForHash(chIndex, p->channel)) {
// we have to copy into a scratch buffer, because these bytes are a union with the decoded protobuf. Create a
// fresh copy for each decrypt attempt.
memcpy(bytes, p->encrypted.bytes, rawSize);
// Try to decrypt the packet if we can
crypto->decrypt(p->from, p->id, rawSize, bytes);
decrypted = attemptAESDecrypt(p, rawSize);
if (decrypted) {
LOG_DEBUG("Packet decrypted using channel %d", chIndex);
break;
}
// printBytes("plaintext", bytes, p->encrypted.size);
// Take those raw bytes and convert them back into a well structured protobuf we can understand
meshtastic_Data decodedtmp;
memset(&decodedtmp, 0, sizeof(decodedtmp));
if (!pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp)) {
LOG_DEBUG("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)", p->id);
} else if (decodedtmp.portnum == meshtastic_PortNum_UNKNOWN_APP) {
LOG_DEBUG("Invalid portnum (bad psk?)");
#if !(MESHTASTIC_EXCLUDE_PKI)
} else if (!owner.is_licensed && isToUs(p) && decodedtmp.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) {
LOG_WARN("Rejecting legacy DM");
return DecodeState::DECODE_FAILURE;
#endif
} else {
p->decoded = decodedtmp;
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
decrypted = true;
// We have a sticky problem here in a specific case.
// If we're routing packets between presets over UDP
// The channel hash may be bogus.
// So we probably need to try to decrypt with every channel
// even if the hash doesn't match, to see if any of them work.
// If we find one that works, we can just re-write the hash
} else if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_UNICAST_UDP) {
// On Portduino, we allow UDP packets to bypass the channel hash check, since the hash may not be
// correct for packets coming over UDP from the meshswitch daemon.
// This allows us to support routing between presets over UDP even if the channel hashes don't match.
channels.setCrypto(chIndex);
decrypted = attemptAESDecrypt(p, rawSize);
if (decrypted) {
LOG_DEBUG("Packet decrypted using channel %d with UDP bypass", chIndex);
break;
}
}
+149
View File
@@ -0,0 +1,149 @@
#pragma once
#ifdef ARCH_PORTDUINO
// Native unicast-UDP connector for meshtasticd. Forwards every outbound MeshPacket to a single
// configured peer (the meshswitch routing daemon) and injects packets received back from it,
// routing on the cleartext header exactly like the multicast handler. This is a meshtasticd-only
// (Linux) feature, so it uses plain POSIX sockets directly rather than the portduino AsyncUDP
// library.
//
// meshtasticd is the client here: it connect()s one UDP socket to the peer, send()s outbound
// packets, and recv()s the daemon's replies on that same socket (the daemon always answers the
// source endpoint it last heard from, so no well-known listen port is required).
#include "configuration.h"
#include "main.h"
#include "mesh/Router.h"
#include "mesh/mesh-pb-constants.h"
#include <atomic>
#include <cstdlib>
#include <string>
#include <thread>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#define UDP_UNICAST_DEFAULT_PORT 4403 // matches the daemon's default listen port
class UdpUnicastConnector final
{
public:
UdpUnicastConnector() = default;
~UdpUnicastConnector() { stop(); }
// peer is "host" or "host:port" (IPv4 literal). Returns true once connected.
bool start(const std::string &peer)
{
if (isRunning)
return true;
std::string host = peer;
uint16_t port = UDP_UNICAST_DEFAULT_PORT;
auto colon = peer.rfind(':');
if (colon != std::string::npos) {
host = peer.substr(0, colon);
port = (uint16_t)atoi(peer.substr(colon + 1).c_str());
}
sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
if (inet_pton(AF_INET, host.c_str(), &addr.sin_addr) != 1) {
LOG_ERROR("UDP unicast: bad peer address '%s'", peer.c_str());
return false;
}
fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) {
LOG_ERROR("UDP unicast: socket() failed");
return false;
}
// connect() pins the destination for send() and filters recv() to datagrams from the peer.
if (::connect(fd, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) < 0) {
LOG_ERROR("UDP unicast: connect() to %s:%u failed", host.c_str(), port);
::close(fd);
fd = -1;
return false;
}
isRunning = true;
rxThread = std::thread([this]() { receiveLoop(); });
LOG_INFO("UDP unicast connector to %s:%u started", host.c_str(), port);
return true;
}
void stop()
{
if (!isRunning)
return;
isRunning = false;
if (fd >= 0)
::shutdown(fd, SHUT_RDWR); // unblock the recv() in the rx thread
if (rxThread.joinable())
rxThread.join();
if (fd >= 0) {
::close(fd);
fd = -1;
}
}
// Called from Router::send for every outbound packet.
void onSend(const meshtastic_MeshPacket *mp)
{
if (!isRunning || fd < 0 || !mp)
return;
// Never reflect a packet that arrived over UDP straight back out over UDP. The daemon's
// (from,id) dedup is the backstop if this ever slips through.
if (mp->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_UNICAST_UDP)
return;
uint8_t buffer[meshtastic_MeshPacket_size];
size_t len = pb_encode_to_bytes(buffer, sizeof(buffer), &meshtastic_MeshPacket_msg, mp);
if (len == 0)
return;
#ifdef MSG_NOSIGNAL
constexpr int kSendFlags = MSG_NOSIGNAL;
#else
constexpr int kSendFlags = 0;
#endif
if (::send(fd, buffer, len, kSendFlags) < 0)
LOG_DEBUG("UDP unicast: send() failed for packet id=%u", mp->id);
}
private:
void receiveLoop()
{
uint8_t buf[meshtastic_MeshPacket_size];
while (isRunning) {
ssize_t n = ::recv(fd, buf, sizeof(buf), 0);
if (n <= 0) {
if (!isRunning)
break;
continue; // transient error or shutdown in progress
}
meshtastic_MeshPacket mp = meshtastic_MeshPacket_init_zero;
if (!pb_decode_from_bytes(buf, (size_t)n, &meshtastic_MeshPacket_msg, &mp))
continue;
if (mp.which_payload_variant != meshtastic_MeshPacket_encrypted_tag)
continue;
// Drop packets with spoofed local origin — same guard as the multicast handler.
if (isFromUs(&mp)) {
LOG_WARN("UDP unicast packet with spoofed local from=0x%x, dropping", mp.from);
continue;
}
mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_UNICAST_UDP;
UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp);
p->rx_snr = 0;
p->rx_rssi = 0;
if (router)
router->enqueueReceivedMessage(p.release());
}
}
int fd = -1;
std::atomic<bool> isRunning{false};
std::thread rxThread;
};
#endif // ARCH_PORTDUINO
+1
View File
@@ -1063,6 +1063,7 @@ bool loadConfig(const char *configPath)
TCPPort = (portduino_config.api_port);
}
}
portduino_config.udp_unicast_peer = (yamlConfig["General"]["UDPUnicastPeer"]).as<std::string>("");
portduino_config.mac_address = (yamlConfig["General"]["MACAddress"]).as<std::string>("");
if (portduino_config.mac_address != "") {
portduino_config.mac_address_explicit = true;
+3
View File
@@ -192,6 +192,7 @@ extern struct portduino_config_struct {
bool mac_address_explicit = false;
std::string mac_address_source = "";
int api_port = -1;
std::string udp_unicast_peer = ""; // meshswitch routing daemon peer, "host" or "host:port"; empty = disabled
std::string config_directory = "";
std::string available_directory = "/etc/meshtasticd/available.d/";
int maxtophone = 100;
@@ -558,6 +559,8 @@ extern struct portduino_config_struct {
out << YAML::Key << "ConfigDirectory" << YAML::Value << config_directory;
if (api_port != -1)
out << YAML::Key << "TCPPort" << YAML::Value << api_port;
if (udp_unicast_peer != "")
out << YAML::Key << "UDPUnicastPeer" << YAML::Value << udp_unicast_peer;
if (mac_address_explicit)
out << YAML::Key << "MACAddress" << YAML::Value << mac_address;
if (mac_address_source != "")