mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-17 17:11:30 +10:00
fix(mesh): coerce coordinate traffic to the position channel on event builds (#11545)
Under USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL every coordinate packet a client aimed at the event channel was rejected with the "Location sharing is disabled on this channel" notification - including the phone's own location feed. Both apps hand a GPS-less node its fix as a POSITION_APP packet addressed to the node itself on channel 0; that packet never leaves the device (Router::sendLocal delivers it locally) but resolved to the event channel and was dropped before PositionModule saw it. Result: the toast on every location tick, and nodes without a GPS never learned a position to share on their private channel. Position traffic now converges on the position channel - findPositionChannel(), the first channel with non-zero on-wire precision, which is never the event channel: - From-us-to-us coordinate packets are exempt from the event block. - Local coordinate sends aimed at the event channel (phone share-location, request-position, waypoints, any module/UI originator) are moved onto the position channel in Router::sendLocal and PhoneAPI instead of rejected. The client notification is only sent when no channel carries positions at all. - A position request DM'd to us on the event channel is answered on the position channel at that channel's precision (request_id preserved, same reply throttle); the requester's coordinates are still not stored, forwarded, relayed or published. want_response from the bitfield is merged before the event-channel decode short-circuit so such requests are seen. - PositionModule::sendOurPosition, positionUnchangedSinceLastSend and MeshService::trySendPosition use the shared helper instead of three copies of the same walk. Non-event builds are unaffected: the coercion compiles out and the helper matches the previous walk. Tests: coverage-event-policy (test_event_channel_phone_api, test_event_channel_router, test_position_precision, test_mqtt, test_nexthop_routing) and the same suites with the policy off. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
8fe246e250
commit
a5fc95f774
@@ -419,27 +419,17 @@ bool MeshService::trySendPosition(NodeNum dest, bool wantReplies)
|
||||
LOG_DEBUG("Skip position ping; no fresh position since boot");
|
||||
return false;
|
||||
}
|
||||
// Prefer the node's current channel, but fall back to the first channel with
|
||||
// position enabled (matching PositionModule::sendOurPosition() behavior).
|
||||
// Prefer the node's current channel, but fall back to the position channel
|
||||
// (matching PositionModule::sendOurPosition() behavior).
|
||||
uint8_t sendChan = node->channel;
|
||||
if (getPositionPrecisionForChannel(sendChan) == 0) {
|
||||
bool found = false;
|
||||
for (uint8_t ch = 0; ch < 8; ++ch) {
|
||||
if (getPositionPrecisionForChannel(ch) != 0) {
|
||||
sendChan = ch;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
// No channel with position enabled: fall back to sending nodeinfo, as before.
|
||||
if (nodeInfoModule) {
|
||||
LOG_INFO("No position-enabled channel; send nodeinfo instead to 0x%08x, wantReplies=%d, channel=%d", dest,
|
||||
wantReplies, node->channel);
|
||||
nodeInfoModule->sendOurNodeInfo(dest, wantReplies, node->channel);
|
||||
}
|
||||
return false;
|
||||
if (getPositionPrecisionForChannel(sendChan) == 0 && !findPositionChannel(sendChan)) {
|
||||
// No channel with position enabled: fall back to sending nodeinfo, as before.
|
||||
if (nodeInfoModule) {
|
||||
LOG_INFO("No position-enabled channel; send nodeinfo instead to 0x%08x, wantReplies=%d, channel=%d", dest,
|
||||
wantReplies, node->channel);
|
||||
nodeInfoModule->sendOurNodeInfo(dest, wantReplies, node->channel);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("Send position ping to 0x%08x, wantReplies=%d, channel=%d", dest, wantReplies, sendChan);
|
||||
positionModule->sendOurPosition(dest, wantReplies, sendChan);
|
||||
|
||||
@@ -1826,8 +1826,11 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p)
|
||||
}
|
||||
#endif
|
||||
|
||||
// Reject before recording duplicate or per-port cooldown state, so a blocked
|
||||
// attempt cannot throttle a valid private-channel position retry.
|
||||
// Coordinates aimed at the event channel go out on the position channel instead (the phone picks the
|
||||
// channel it last heard the node on, which is the event channel for everyone). Only when there is no
|
||||
// channel to move them to is the send rejected. Reject before recording duplicate or per-port cooldown
|
||||
// state, so a blocked attempt cannot throttle a valid private-channel position retry.
|
||||
coerceCoordinatePacketToPositionChannel(&p);
|
||||
if (isBlockedEventCoordinatePacket(&p)) {
|
||||
LOG_DEBUG("Suppress phone coordinate send on event (everyone) channel");
|
||||
meshtastic_QueueStatus qs = router->getQueueStatus();
|
||||
|
||||
@@ -32,6 +32,17 @@ uint32_t getPositionPrecisionForChannel(uint8_t channelIndex)
|
||||
return precision;
|
||||
}
|
||||
|
||||
bool findPositionChannel(uint8_t &channelIndex)
|
||||
{
|
||||
for (uint8_t i = 0; i < channels.getNumChannels(); i++) {
|
||||
if (getPositionPrecisionForChannel(i) != 0) {
|
||||
channelIndex = i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int32_t truncateCoordinate(int32_t coordinate, uint32_t precision)
|
||||
{
|
||||
if (precision == 0 || precision >= 32)
|
||||
|
||||
@@ -16,6 +16,10 @@ uint32_t getPositionPrecisionForChannel(const meshtastic_Channel &channel);
|
||||
// Configured precision, clamped to MAX_POSITION_PRECISION_PUBLIC_KEY when the channel's effective key is publicly decryptable.
|
||||
uint32_t getPositionPrecisionForChannel(uint8_t channelIndex);
|
||||
|
||||
// The channel our position goes out on: the lowest index with a non-zero on-wire precision (disabled and event
|
||||
// channels never qualify). Returns false when position sharing is off on every channel.
|
||||
bool findPositionChannel(uint8_t &channelIndex);
|
||||
|
||||
// Truncate a single latitude_i/longitude_i to `precision` significant bits, centered in the
|
||||
// resulting grid cell (stable under GPS jitter). precision 0 or >=32 returns the value unchanged.
|
||||
// The return is the coordinate (int32_t); the uint8_t overload only narrows the precision arg.
|
||||
|
||||
+51
-3
@@ -16,6 +16,9 @@
|
||||
#include <ErriezCRC32.h>
|
||||
#include <pb_decode.h>
|
||||
#include <pb_encode.h>
|
||||
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL
|
||||
#include "modules/PositionModule.h"
|
||||
#endif
|
||||
#if HAS_TRAFFIC_MANAGEMENT
|
||||
#endif
|
||||
#if HAS_VARIABLE_HOPS
|
||||
@@ -86,6 +89,11 @@ bool isBlockedEventCoordinatePacket(const meshtastic_MeshPacket *p)
|
||||
if (p->pki_encrypted || willUsePki(p)) {
|
||||
return false;
|
||||
}
|
||||
// From us, to us: never leaves the device (sendLocal delivers it locally). This is how the phone
|
||||
// hands a GPS-less node its fix and time, so it shares nothing and must not be blocked.
|
||||
if (isFromUs(p) && isToUs(p)) {
|
||||
return false;
|
||||
}
|
||||
if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||
return isCoordinatePortnum(p->decoded.portnum) && channels.isEventChannel(getEffectiveChannelIndex(p));
|
||||
}
|
||||
@@ -96,6 +104,33 @@ bool isBlockedEventCoordinatePacket(const meshtastic_MeshPacket *p)
|
||||
#endif
|
||||
}
|
||||
|
||||
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL
|
||||
// A remote node's unicast position request to us. Only the reply is generated for these; the packet
|
||||
// itself is still dropped by the caller.
|
||||
static bool isEventChannelPositionRequestForUs(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
return p->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||
p->decoded.portnum == meshtastic_PortNum_POSITION_APP && p->decoded.want_response && isToUs(p) && !isFromUs(p);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool coerceCoordinatePacketToPositionChannel(meshtastic_MeshPacket *p)
|
||||
{
|
||||
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL
|
||||
if (!isBlockedEventCoordinatePacket(p))
|
||||
return false;
|
||||
uint8_t positionChannel;
|
||||
if (!findPositionChannel(positionChannel))
|
||||
return false;
|
||||
LOG_DEBUG("Coerce coordinate packet 0x%08x from event channel to position channel %u", p->id, positionChannel);
|
||||
p->channel = positionChannel;
|
||||
return true;
|
||||
#else
|
||||
(void)p;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool willUsePki(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
@@ -396,6 +431,11 @@ ErrorCode Router::sendLocal(meshtastic_MeshPacket *p, RxSource src)
|
||||
|
||||
return ERRNO_NO_INTERFACES;
|
||||
} else {
|
||||
// Coordinates never go out on the event channel: any local originator (phone, module, UI) that aimed
|
||||
// one there is moved onto the position channel instead. Before the loopback below so the local copy
|
||||
// carries the channel it will actually be sent on.
|
||||
coerceCoordinatePacketToPositionChannel(p);
|
||||
|
||||
// If we are sending a broadcast, we also treat it as if we just received it ourself
|
||||
// this allows local apps (and PCs) to see broadcasts sourced locally. Only the loopback
|
||||
// handleReceived is deferred when nested; send(p) below still transmits immediately.
|
||||
@@ -1029,14 +1069,16 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
return DecodeState::DECODE_POLICY_REJECT;
|
||||
#endif
|
||||
|
||||
if (p->decoded.has_bitfield)
|
||||
p->decoded.want_response |= p->decoded.bitfield & BITFIELD_WANT_RESPONSE_MASK;
|
||||
|
||||
if (isBlockedEventCoordinatePacket(p)) {
|
||||
// want_response is already merged above: a position request on the event channel is still
|
||||
// answered (on the position channel) even though its coordinates are dropped.
|
||||
LOG_DEBUG("Decoded coordinate packet on event channel; suppress payload logging");
|
||||
return DecodeState::DECODE_SUCCESS;
|
||||
}
|
||||
|
||||
if (p->decoded.has_bitfield)
|
||||
p->decoded.want_response |= p->decoded.bitfield & BITFIELD_WANT_RESPONSE_MASK;
|
||||
|
||||
/* Not actually ever used.
|
||||
// Decompress if needed. jm
|
||||
if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP) {
|
||||
@@ -1515,6 +1557,12 @@ void Router::dispatchReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||
// Discard coordinate-bearing packets that arrive on the event ("everyone")
|
||||
// channel: don't process, store in NodeDB, or rebroadcast them.
|
||||
if (!skipHandle && isBlockedEventCoordinatePacket(p)) {
|
||||
// A position request addressed to us is still answered, on our position channel at that
|
||||
// channel's precision, so "request position" from a node that only shares the event channel
|
||||
// with us resolves where positions actually live. The requester's own coordinates are
|
||||
// still dropped: not stored, not forwarded to the phone, not relayed, not published.
|
||||
if (isEventChannelPositionRequestForUs(p) && positionModule)
|
||||
positionModule->replyOnPositionChannel(*p);
|
||||
LOG_DEBUG("Drop coordinate packet on event (everyone) channel");
|
||||
cancelSending(p->from, p->id);
|
||||
skipHandle = true;
|
||||
|
||||
@@ -18,6 +18,10 @@ inline bool isCoordinatePortnum(meshtastic_PortNum portnum)
|
||||
}
|
||||
|
||||
bool isBlockedEventCoordinatePacket(const meshtastic_MeshPacket *p);
|
||||
/// Retarget a locally-originated coordinate packet that would be blocked on the event channel onto the
|
||||
/// position channel (see findPositionChannel). Returns true if p->channel was changed; false when the
|
||||
/// packet is not a blocked event coordinate packet or no channel carries positions.
|
||||
bool coerceCoordinatePacketToPositionChannel(meshtastic_MeshPacket *p);
|
||||
bool willUsePki(const meshtastic_MeshPacket *p);
|
||||
|
||||
/// rx_time/has_rx_time for "now": a real epoch when the clock is trustworthy, else a
|
||||
|
||||
@@ -289,6 +289,27 @@ meshtastic_MeshPacket *PositionModule::allocReply()
|
||||
return reply;
|
||||
}
|
||||
|
||||
void PositionModule::replyOnPositionChannel(const meshtastic_MeshPacket &req)
|
||||
{
|
||||
uint8_t positionChannel;
|
||||
if (!findPositionChannel(positionChannel)) {
|
||||
LOG_DEBUG("Skip position reply to 0x%08x: position sharing disabled on all channels", getFrom(&req));
|
||||
return;
|
||||
}
|
||||
if (!service)
|
||||
return;
|
||||
|
||||
precision = getPositionPrecisionForChannel(positionChannel);
|
||||
meshtastic_MeshPacket *reply = allocReply(); // reply throttle + precision-0/no-fix guards live here
|
||||
if (!reply)
|
||||
return;
|
||||
|
||||
setReplyTo(reply, req);
|
||||
reply->channel = positionChannel; // not the channel the request came in on
|
||||
LOG_INFO("Reply to position request from 0x%08x on position channel %u", getFrom(&req), positionChannel);
|
||||
service->sendToMesh(reply);
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *PositionModule::allocAtakPli()
|
||||
{
|
||||
LOG_INFO("Send TAK V2 PLI packet");
|
||||
@@ -374,12 +395,11 @@ void PositionModule::sendOurPosition()
|
||||
currentGeneration = radioGeneration;
|
||||
|
||||
// If we changed channels, ask everyone else for their latest info
|
||||
for (uint8_t channelNum = 0; channelNum < 8; channelNum++) {
|
||||
if (getPositionPrecisionForChannel(channelNum) != 0) {
|
||||
LOG_INFO("Send pos@%x:6 to mesh (wantReplies=%d)", localPosition.timestamp, requestReplies);
|
||||
sendOurPosition(NODENUM_BROADCAST, requestReplies, channelNum);
|
||||
return;
|
||||
}
|
||||
uint8_t positionChannel;
|
||||
if (findPositionChannel(positionChannel)) {
|
||||
LOG_INFO("Send pos@%x:6 to mesh (wantReplies=%d)", localPosition.timestamp, requestReplies);
|
||||
sendOurPosition(NODENUM_BROADCAST, requestReplies, positionChannel);
|
||||
return;
|
||||
}
|
||||
LOG_INFO("Skip pos@%x:6 broadcast; position sharing disabled on all channels", localPosition.timestamp);
|
||||
}
|
||||
@@ -467,12 +487,10 @@ bool PositionModule::positionUnchangedSinceLastSend(const meshtastic_PositionLit
|
||||
// precision). Default nodes gauge movement at that on-wire (public-clamped) resolution;
|
||||
// trackers use their own configured (unclamped) precision so finer moves still count.
|
||||
uint32_t precisionBits = 0;
|
||||
for (uint8_t ch = 0; ch < 8; ch++) {
|
||||
if (getPositionPrecisionForChannel(ch) == 0)
|
||||
continue;
|
||||
uint8_t ch;
|
||||
if (findPositionChannel(ch)) {
|
||||
precisionBits =
|
||||
useConfiguredPrecision ? getPositionPrecisionForChannel(channels.getByIndex(ch)) : getPositionPrecisionForChannel(ch);
|
||||
break;
|
||||
}
|
||||
|
||||
return positionWithinPrecisionCell(selfPos.latitude_i, selfPos.longitude_i, lastGpsLatitude, lastGpsLongitude, precisionBits);
|
||||
|
||||
@@ -36,6 +36,13 @@ class PositionModule : public ProtobufModule<meshtastic_Position>, private concu
|
||||
void sendOurPosition(NodeNum dest, bool wantReplies = false, uint8_t channel = 0);
|
||||
void sendOurPosition();
|
||||
|
||||
/**
|
||||
* Answer a position request that arrived on a channel we never share position on (the event channel):
|
||||
* the reply goes out on the position channel at that channel's precision, tagged as a reply to req.
|
||||
* Subject to the same reply throttle as allocReply(). No-op when no channel carries positions.
|
||||
*/
|
||||
void replyOnPositionChannel(const meshtastic_MeshPacket &req);
|
||||
|
||||
void handleNewPosition();
|
||||
|
||||
// Pure broadcast-policy helpers, split out so they're unit-testable without the module.
|
||||
|
||||
Reference in New Issue
Block a user