Skip to content

Commit c8073e2

Browse files
Fix multi-hop response routing by embedding path in request payload
DIRECT routing consumes the header path at each hop, so the target node couldn't reverse it for the response. This fix: - Embeds the forward path in the ADVERT_REQUEST payload - Target extracts path, excludes itself (last element), and reverses - For direct neighbors (path=[target]), response sent as zero-hop - Works with stock firmware intermediate nodes (they only use header) Example: path [0x23, 0x1F] → response path [0x23] Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9fd8910 commit c8073e2

File tree

3 files changed

+47
-28
lines changed

3 files changed

+47
-28
lines changed

docs/payloads.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -284,11 +284,15 @@ Pull-based advert request. Sent to a specific node (via known path) to request i
284284

285285
**Note on sub-type value:** Uses 0x20 (not 0xA0) to enable multi-hop forwarding. In `Mesh.cpp`, control packets with high-bit sub-types (0x80+) are restricted to zero-hop only, while sub-types 0x00-0x7F follow normal DIRECT routing through intermediate nodes.
286286

287-
| Field | Size (bytes) | Description |
288-
|---------------|-----------------|--------------------------------------------|
289-
| sub_type | 1 | 0x20 |
290-
| target_prefix | 1 | PATH_HASH_SIZE of target node's public key |
291-
| tag | 4 | randomly generated by sender for matching |
287+
| Field | Size (bytes) | Description |
288+
|---------------|-----------------|-----------------------------------------------------|
289+
| sub_type | 1 | 0x20 |
290+
| target_prefix | 1 | PATH_HASH_SIZE of target node's public key |
291+
| tag | 4 | randomly generated by sender for matching |
292+
| path_len | 1 | length of return path |
293+
| path | path_len | forward path to target (target is last element) |
294+
295+
**Note on path:** The path is included in the payload (not just the header) so the target can reverse it for the response. With DIRECT routing, the header path is consumed at each hop, so embedding it in the payload preserves it for multi-hop response routing. The path includes the target as the last element (e.g., `[0x23, 0x1F]` to reach node `0x1F` via `0x23`). When building the response, the target excludes itself and reverses the remaining path (e.g., response path becomes `[0x23]`). For direct neighbors where the path is just `[target]`, the response is sent as zero-hop. Intermediate nodes (including stock firmware) forward normally based on the header path.
292296

293297
## ADVERT_RESPONSE (sub_type 0x30)
294298

examples/companion_radio/MyMesh.cpp

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1787,27 +1787,23 @@ void MyMesh::handleCmdFrame(size_t len) {
17871787
uint32_t tag;
17881788
getRNG()->random((uint8_t*)&tag, 4);
17891789

1790-
// Build request packet
1791-
uint8_t payload[1 + PATH_HASH_SIZE + 4];
1790+
// Build request packet - include path in payload for multi-hop response routing
1791+
// Format: sub_type(1) + target_prefix(1) + tag(4) + path_len(1) + path(N)
1792+
uint8_t payload[1 + PATH_HASH_SIZE + 4 + 1 + MAX_PATH_SIZE];
17921793
int pos = 0;
17931794
payload[pos++] = CTL_TYPE_ADVERT_REQUEST;
17941795
memcpy(&payload[pos], target_prefix, PATH_HASH_SIZE); pos += PATH_HASH_SIZE;
17951796
memcpy(&payload[pos], &tag, 4); pos += 4;
1797+
payload[pos++] = path_len;
1798+
memcpy(&payload[pos], path, path_len); pos += path_len;
17961799

17971800
mesh::Packet* packet = createControlData(payload, pos);
17981801
if (!packet) {
17991802
writeErrFrame(ERR_CODE_BAD_STATE);
18001803
return;
18011804
}
18021805

1803-
// For CONTROL packets with high-bit set, Mesh.cpp only processes them
1804-
// if path_len == 0 (zero-hop). For direct neighbors (path_len == 1 and
1805-
// path matches target prefix), use sendZeroHop.
1806-
if (path_len == PATH_HASH_SIZE && memcmp(path, target_prefix, PATH_HASH_SIZE) == 0) {
1807-
sendZeroHop(packet);
1808-
} else {
1809-
sendDirect(packet, path, path_len, 0);
1810-
}
1806+
sendDirect(packet, path, path_len, 0);
18111807

18121808
pending_advert_request = tag;
18131809

examples/simple_repeater/MyMesh.cpp

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -727,24 +727,33 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t
727727
}
728728

729729
void MyMesh::handleAdvertRequest(mesh::Packet* packet) {
730-
// Validate packet length: sub_type(1) + prefix(PATH_HASH_SIZE) + tag(4)
731-
if (packet->payload_len < 1 + PATH_HASH_SIZE + 4) {
730+
// Validate packet length: sub_type(1) + prefix(PATH_HASH_SIZE) + tag(4) + path_len(1)
731+
if (packet->payload_len < 1 + PATH_HASH_SIZE + 4 + 1) {
732732
MESH_DEBUG_PRINTLN("handleAdvertRequest: packet too short (%d bytes)", packet->payload_len);
733733
return;
734734
}
735735

736-
// Extract target prefix and tag
737-
const uint8_t* target_prefix = &packet->payload[1];
736+
// Extract target prefix, tag, and return path from payload
737+
int offset = 1;
738+
const uint8_t* target_prefix = &packet->payload[offset]; offset += PATH_HASH_SIZE;
738739
uint32_t tag;
739-
memcpy(&tag, &packet->payload[1 + PATH_HASH_SIZE], 4);
740+
memcpy(&tag, &packet->payload[offset], 4); offset += 4;
741+
uint8_t return_path_len = packet->payload[offset++];
742+
const uint8_t* return_path = &packet->payload[offset];
743+
744+
// Validate path length
745+
if (packet->payload_len < offset + return_path_len) {
746+
MESH_DEBUG_PRINTLN("handleAdvertRequest: invalid path_len (%d)", return_path_len);
747+
return;
748+
}
740749

741750
// Check if request is for us
742751
if (memcmp(target_prefix, self_id.pub_key, PATH_HASH_SIZE) != 0) {
743752
MESH_DEBUG_PRINTLN("handleAdvertRequest: not for us (prefix mismatch)");
744753
return;
745754
}
746755

747-
MESH_DEBUG_PRINTLN("handleAdvertRequest: request for us, tag=%08X", tag);
756+
MESH_DEBUG_PRINTLN("handleAdvertRequest: request for us, tag=%08X, return_path_len=%d", tag, return_path_len);
748757

749758
// Build app_data (adv_type + node_name + flags + optional: lat, lon, desc)
750759
uint8_t app_data[1 + 32 + 1 + 4 + 4 + 32];
@@ -800,19 +809,29 @@ void MyMesh::handleAdvertRequest(mesh::Packet* packet) {
800809
memcpy(&response[pos], signature, SIGNATURE_SIZE); pos += SIGNATURE_SIZE; // signature
801810
memcpy(&response[pos], app_data, app_data_len); pos += app_data_len; // app_data
802811

803-
MESH_DEBUG_PRINTLN("handleAdvertRequest: sending response, %d bytes, flags=%02X", pos, flags);
804-
805-
// Create response packet and send
812+
// Create response packet and send using reversed path from payload
806813
mesh::Packet* resp_packet = createControlData(response, pos);
807814
if (resp_packet) {
808-
if (packet->path_len == 0) {
815+
// Exclude ourselves (last element) from the path
816+
uint8_t response_path_len = (return_path_len > PATH_HASH_SIZE) ? return_path_len - PATH_HASH_SIZE : 0;
817+
818+
MESH_DEBUG_PRINTLN("handleAdvertRequest: sending response, %d bytes, flags=%02X, response_path_len=%d", pos, flags, response_path_len);
819+
820+
if (response_path_len == 0) {
821+
// Direct neighbor (or empty path) - send zero-hop
809822
sendZeroHop(resp_packet);
810823
} else {
824+
// Reverse the path excluding ourselves
811825
uint8_t reversed_path[MAX_PATH_SIZE];
812-
for (int i = 0; i < packet->path_len; i++) {
813-
reversed_path[i] = packet->path[packet->path_len - 1 - i];
826+
for (int i = 0; i < response_path_len; i++) {
827+
reversed_path[i] = return_path[response_path_len - 1 - i];
814828
}
815-
sendDirect(resp_packet, reversed_path, packet->path_len, SERVER_RESPONSE_DELAY);
829+
MESH_DEBUG_PRINTLN("handleAdvertRequest: response path = [%02X%s%s%s]",
830+
reversed_path[0],
831+
response_path_len > 1 ? ", " : "",
832+
response_path_len > 1 ? "" : "",
833+
response_path_len > 1 ? "..." : "");
834+
sendDirect(resp_packet, reversed_path, response_path_len, SERVER_RESPONSE_DELAY);
816835
}
817836
}
818837
}

0 commit comments

Comments
 (0)