From 9dc977b4da0ec552da3e0d09a3aaabe41d646dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Fri, 17 Jul 2026 10:17:33 +0200 Subject: [PATCH 1/2] TrafficManagement: gate role/NodeInfo cache writes on signer authenticity The tier-3 role cache and the PSRAM NodeInfo response cache were updated from any received NodeInfo with no authenticity check, so a spoofed NodeInfo could set a node's cached role (granting dedup exceptions) or poison the cached user served in direct responses. Skip both cache writes when a known signer's NodeInfo arrives unsigned, matching the identity-update gate on the direct-response path. --- src/modules/TrafficManagementModule.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/modules/TrafficManagementModule.cpp b/src/modules/TrafficManagementModule.cpp index f10a9f0d47e..7eeb4005c17 100644 --- a/src/modules/TrafficManagementModule.cpp +++ b/src/modules/TrafficManagementModule.cpp @@ -729,8 +729,15 @@ ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPack // Learn NodeInfo payloads into the dedicated PSRAM cache, and refresh the tier-3 // role cache for any node we already track (keeps the dedup role exception current). if (mp.decoded.portnum == meshtastic_PortNum_NODEINFO_APP) { - cacheNodeInfoPacket(mp); - updateCachedRoleFromNodeInfo(mp); + // A known signer's NodeInfo arriving unsigned is unauthenticated (the sender is forgeable), + // so don't let it drive the role cache or the direct-response NodeInfo cache. Same gate the + // identity-update path below uses. + const meshtastic_NodeInfoLite *senderNode = nodeDB->getMeshNode(getFrom(&mp)); + const bool unauthenticatedSigner = senderNode && nodeInfoLiteHasXeddsaSigned(senderNode) && !mp.xeddsa_signed; + if (!unauthenticatedSigner) { + cacheNodeInfoPacket(mp); + updateCachedRoleFromNodeInfo(mp); + } } // ------------------------------------------------------------------------- From 30be1c8ab089ae702fed0a01b677a689f618d7ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Fri, 17 Jul 2026 10:45:11 +0200 Subject: [PATCH 2/2] TrafficManagement: hoist shared NodeInfo signer lookup Compute the sender node lookup and unauthenticated-signer check once per NodeInfo packet and reuse it for both the cache-refresh gate and the direct-response identity gate, avoiding a second O(N) getMeshNode scan. --- src/modules/TrafficManagementModule.cpp | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/modules/TrafficManagementModule.cpp b/src/modules/TrafficManagementModule.cpp index 7eeb4005c17..74e8de6c24a 100644 --- a/src/modules/TrafficManagementModule.cpp +++ b/src/modules/TrafficManagementModule.cpp @@ -726,18 +726,18 @@ ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPack return ProcessMessage::CONTINUE; } + // A known signer's NodeInfo arriving unsigned is unauthenticated (the sender is forgeable), so + // it must not drive any cache or identity write below. Computed once here (getMeshNode is O(N)) + // and reused by both the cache-refresh and the direct-response identity path. + const bool isNodeInfo = mp.decoded.portnum == meshtastic_PortNum_NODEINFO_APP; + const meshtastic_NodeInfoLite *senderNode = isNodeInfo ? nodeDB->getMeshNode(getFrom(&mp)) : nullptr; + const bool unauthenticatedSigner = senderNode && nodeInfoLiteHasXeddsaSigned(senderNode) && !mp.xeddsa_signed; + // Learn NodeInfo payloads into the dedicated PSRAM cache, and refresh the tier-3 // role cache for any node we already track (keeps the dedup role exception current). - if (mp.decoded.portnum == meshtastic_PortNum_NODEINFO_APP) { - // A known signer's NodeInfo arriving unsigned is unauthenticated (the sender is forgeable), - // so don't let it drive the role cache or the direct-response NodeInfo cache. Same gate the - // identity-update path below uses. - const meshtastic_NodeInfoLite *senderNode = nodeDB->getMeshNode(getFrom(&mp)); - const bool unauthenticatedSigner = senderNode && nodeInfoLiteHasXeddsaSigned(senderNode) && !mp.xeddsa_signed; - if (!unauthenticatedSigner) { - cacheNodeInfoPacket(mp); - updateCachedRoleFromNodeInfo(mp); - } + if (isNodeInfo && !unauthenticatedSigner) { + cacheNodeInfoPacket(mp); + updateCachedRoleFromNodeInfo(mp); } // ------------------------------------------------------------------------- @@ -753,8 +753,7 @@ ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPack if (shouldRespondToNodeInfo(&mp, true)) { // Unicast NodeInfo is never signed, so a known signer's identity claim here is // unauthenticated: don't overwrite its stored name. The cached response is unaffected. - const meshtastic_NodeInfoLite *senderNode = nodeDB->getMeshNode(getFrom(&mp)); - const bool unauthenticatedSigner = senderNode && nodeInfoLiteHasXeddsaSigned(senderNode) && !mp.xeddsa_signed; + // unauthenticatedSigner was computed above (this branch is NodeInfo-only). meshtastic_User requester = meshtastic_User_init_zero; if (!unauthenticatedSigner && pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_User_msg, &requester)) {