mirror of
https://github.com/meshtastic/firmware.git
synced 2026-06-16 07:16:12 +00:00
Compare commits
9 Commits
develop
...
udp-unicast
| Author | SHA1 | Date | |
|---|---|---|---|
| 689e290670 | |||
| 708a194675 | |||
| b8edc9ca96 | |||
| 8e29d44e19 | |||
| dad92dea9e | |||
| e05b112f68 | |||
| fe615d7bf1 | |||
| bcd9b5508f | |||
| ac1f7f0f80 |
@@ -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();
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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 != "")
|
||||
|
||||
Reference in New Issue
Block a user