diff --git a/src/main.cpp b/src/main.cpp index cc7c239a4e0..acc372156ea 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -186,6 +186,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 @@ -1037,6 +1042,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(); diff --git a/src/main.h b/src/main.h index 36995f69491..6d2193d6f7e 100644 --- a/src/main.h +++ b/src/main.h @@ -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; diff --git a/src/mesh/Channels.h b/src/mesh/Channels.h index 6e17a7ab618..cc45ac0568d 100644 --- a/src/mesh/Channels.h +++ b/src/mesh/Channels.h @@ -112,13 +112,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); diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 7ae78cbb162..19017ad0a84 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -421,6 +421,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); } @@ -451,6 +458,33 @@ 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; +} #if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA) bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p) { @@ -588,30 +622,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); - - // 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; + decrypted = attemptAESDecrypt(p, rawSize); + if (decrypted) { + LOG_DEBUG("Packet decrypted using channel %d", chIndex); + break; + } + + // 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; } } diff --git a/src/mesh/udp/UdpUnicastConnector.h b/src/mesh/udp/UdpUnicastConnector.h new file mode 100644 index 00000000000..0b0c5f408dc --- /dev/null +++ b/src/mesh/udp/UdpUnicastConnector.h @@ -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 +#include +#include +#include + +#include +#include +#include +#include + +#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(&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 isRunning{false}; + std::thread rxThread; +}; +#endif // ARCH_PORTDUINO diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index 8da3d085b90..c53201b5708 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -1171,6 +1171,7 @@ bool loadConfig(const char *configPath) TCPPort = (portduino_config.api_port); } } + portduino_config.udp_unicast_peer = (yamlConfig["General"]["UDPUnicastPeer"]).as(""); portduino_config.mac_address = (yamlConfig["General"]["MACAddress"]).as(""); if (portduino_config.mac_address != "") { portduino_config.mac_address_explicit = true; diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index b5a47787e12..cf1e281817a 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -230,6 +230,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; @@ -641,6 +642,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 != "")