feat(config): make position & telemetry broadcast opt-in (#10929)

Position sharing was opt-out (the default primary channel shipped
position_precision=13) while device telemetry was already opt-in, and a
normal firmware upgrade preserves saved config, so existing nodes stayed
position-on. This makes both broadcasts opt-in, both on a fresh flash and via
a one-time migration for upgrading nodes.

- Fresh default: initDefaultChannel now sets position_precision=0.
- One-time migration in loadFromDisk, gated on a dedicated
  POSITION_TELEMETRY_OPTIN_VER (26) watermark kept separate from
  DEVICESTATE_CUR_VER (bumping that would re-run the NodeDatabase v24
  legacy decoder on already-migrated v25 DBs): disables position broadcast
  on PUBLIC/default-PSK channels and forces every device-telemetry
  mesh-broadcast flag plus the MQTT map-report location off.
- Private-PSK channels are left untouched: a channel with a custom key is a
  deliberate trusted-group setup, so its configured precision is preserved.
- Idempotent: ordinary saves never re-stamp .version, so a user who
  re-enables sharing is not re-disabled on the next boot.
- Added pure channelFileUsesPublicKey() (operates on the raw ChannelFile,
  since the channels singleton isn't initialized during loadFromDisk) and
  refactored Channels::usesPublicKey() to delegate to it.
- New test/test_optin_migration native suite (14 cases).
This commit is contained in:
Ben Meadors
2026-07-07 14:58:58 -05:00
committed by GitHub
parent d16ae2b098
commit 8d20606203
6 changed files with 305 additions and 8 deletions
+22 -7
View File
@@ -156,7 +156,9 @@ void Channels::initDefaultChannel(ChannelIndex chIndex)
channelSettings.psk.bytes[0] = defaultpskIndex;
channelSettings.psk.size = 1;
strncpy(channelSettings.name, "", sizeof(channelSettings.name));
channelSettings.module_settings.position_precision = 13; // default to sending location on the primary channel
// Position sharing is OPT-IN: precision 0 means "do not broadcast location". A user (or the phone app)
// must explicitly raise precision to start sharing. See the one-time opt-in migration in NodeDB.cpp.
channelSettings.module_settings.position_precision = 0;
channelSettings.has_module_settings = true;
ch.has_settings = true;
@@ -436,9 +438,11 @@ bool cryptoKeyIsPublic(const CryptoKey &key)
return false;
}
bool Channels::usesPublicKey(ChannelIndex chIndex)
bool channelFileUsesPublicKey(const meshtastic_ChannelFile &cf, ChannelIndex chIndex)
{
const meshtastic_Channel &ch = getByIndex(chIndex);
if (chIndex >= cf.channels_count)
return false;
const meshtastic_Channel &ch = cf.channels[chIndex];
if (!ch.has_settings || ch.role == meshtastic_Channel_Role_DISABLED)
return false;
@@ -446,10 +450,14 @@ bool Channels::usesPublicKey(ChannelIndex chIndex)
if (psk.size == 0) {
// Secondary channels inherit the primary key when unset; primary size==0 means encryption disabled.
if (ch.role == meshtastic_Channel_Role_SECONDARY) {
// Guard against malformed configs with no PRIMARY channel (primaryIndex could point back to us).
if (primaryIndex == chIndex)
return true; // fail closed: treat as public
return usesPublicKey(primaryIndex);
// Resolve against the PRIMARY channel's key. The singleton's primaryIndex isn't available in
// the raw-struct path (this runs during boot migration before initDefaults), so scan by role.
// Fail closed to "public" if no distinct PRIMARY is found (malformed config).
for (pb_size_t p = 0; p < cf.channels_count; p++) {
if (cf.channels[p].role == meshtastic_Channel_Role_PRIMARY)
return (p == chIndex) ? true : channelFileUsesPublicKey(cf, (ChannelIndex)p);
}
return true;
}
return true;
}
@@ -462,6 +470,13 @@ bool Channels::usesPublicKey(ChannelIndex chIndex)
return (psk.size == sizeof(defaultpsk) && memcmp(psk.bytes, defaultpsk, sizeof(defaultpsk) - 1) == 0);
}
bool Channels::usesPublicKey(ChannelIndex chIndex)
{
// Delegates to the pure, on-disk-struct variant so the two can't drift. getByIndex() reads the same
// global channelFile, so this is behavior-preserving for the position-precision clamp callers.
return channelFileUsesPublicKey(channelFile, chIndex);
}
bool Channels::isWellKnownChannel(ChannelIndex chIndex)
{
const auto &ch = getByIndex(chIndex);