Skip to content

Commit b03dd0a

Browse files
committed
Add malformed chat filtering and validation for public messages
- Introduced a new feature to validate and filter malformed chat messages in companion chat views. - Implemented logic to drop malformed public chat messages at repeaters by default, ensuring binary datagrams remain unaffected. - Enhanced text validation with UTF-8 checks and quality metrics to improve message display. - Updated documentation to reflect changes in chat handling and configuration options for malformed message handling. - Added new methods and structures to support the tracking and management of malformed message statistics.
1 parent 0e37c14 commit b03dd0a

22 files changed

Lines changed: 541 additions & 13 deletions

File tree

README-NL.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ MeshCoreNG is een Next Gen variant van MeshCore.
44

55
Kort gezegd: MeshCore laat LoRa-apparaten berichten naar elkaar doorgeven zonder internet. MeshCoreNG bouwt daarop verder en probeert vooral repeaters slimmer te maken, zodat grotere en drukkere netwerken beter blijven werken.
66

7+
Website en webflasher: https://michtronics.github.io/MeshCoreNG/
8+
79
Het doel is niet om MeshCore opnieuw te bouwen. Het doel is om stap voor stap verbeteringen toe te voegen, zonder bestaande clients of het bestaande protocol kapot te maken.
810

911
## Waarom dit in Nederland belangrijk is
@@ -463,13 +465,30 @@ region tree
463465
region save
464466
```
465467

468+
**Malformed chat afhandeling:**
469+
470+
Companion radio firmware valideert menselijke chat voordat die naar apps of displays gaat. Ongeldige UTF-8, binary-achtige tekst, te veel control characters, replacement characters, onmogelijke timestamps en tekst met een heel lage confidence score worden bij de chat/UI-laag gefilterd. Binary datagrams, raw/custom packets, requests, responses en toekomstige packet types blijven binary-safe.
471+
472+
Standaard wordt malformed companion chat als compacte placeholder getoond, zodat rommeltekst niet in de Android/app kant gerenderd wordt. Payloads die niet geïnspecteerd kunnen worden, binary channel datagrams en onbekende/toekomstige packet types worden niet blind gedropt.
473+
474+
Repeater firmware kan ook ingesteld worden om malformed default-public-channel group text te droppen voordat het opnieuw wordt uitgezonden:
475+
476+
```text
477+
get malformed.drop
478+
set malformed.drop on
479+
set malformed.drop off
480+
```
481+
482+
Dit staat standaard aan op repeaters. Repeaters droppen alleen tekstpackets die ze kunnen inspecteren en als malformed kunnen classificeren. Encrypted/private group text die de repeater niet kan decrypten, binary datagrams en onbekende/toekomstige packet types blijven volgens de normale forwardingregels lopen.
483+
466484
Meer CLI-uitleg staat in [docs/cli_commands.md](./docs/cli_commands.md).
467485

468486
## Compatibiliteit
469487

470488
MeshCoreNG blijft compatible met het bestaande MeshCore ecosysteem.
471489

472490
- Geen packet format wijziging voor deze dense-mesh stappen.
491+
- Chat-sanitatie geldt alleen voor menselijke chatweergave/forwarding policy; binary transport blijft ondersteund.
473492
- Bestaande MeshCore clients blijven werken.
474493
- Bestaande MeshCore firmware kan nog steeds met MeshCoreNG praten.
475494
- De standaardinstellingen blijven veilig voor normale en sparse netwerken.
@@ -498,9 +517,12 @@ Voor developers:
498517
MeshCoreNG heeft nu een GitHub Pages webflasher voor ESP32 repeater builds:
499518

500519
- MeshCoreNG webflasher: https://michtronics.github.io/MeshCoreNG/flasher/
520+
- MeshCoreNG website en docs: https://michtronics.github.io/MeshCoreNG/
501521

502522
De flasher gebruikt ESP Web Tools en werkt vanuit Chrome of Edge met Web Serial. Hij is bedoeld voor ESP32-family boards. nRF52, RP2040 en STM32 boards gebruiken nog steeds hun normale firmwarebestanden en flashing tools.
503523

524+
ESP32 repeater builds die je via deze site flasht hebben malformed public chat dropping standaard aan. Controleer of wijzig dit na het flashen met `get malformed.drop`, `set malformed.drop on` of `set malformed.drop off`.
525+
504526
De firmwarebestanden die de webflasher gebruikt komen uit GitHub Release assets. De release/CI workflow bouwt de ESP32 repeater-varianten, hangt de merged `.bin` bestanden aan de release, en de GitHub Pages workflow downloadt daarna die releasebestanden om de ESP Web Tools manifests onder `/flasher/` te maken.
505527

506528
Wil je later nog een ESP32-board toevoegen aan de webflasher, dan voeg je de PlatformIO environment name, display name, chip family en beschrijving toe aan `webflasher/boards.json`. De bijbehorende release asset moet een naam hebben zoals `<env>-*-merged.bin`.

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ MeshCoreNG is a Next Gen variant of MeshCore.
44

55
In simple terms: MeshCore lets LoRa devices pass messages to each other without the internet. MeshCoreNG builds on that and focuses on making repeaters smarter, so larger and busier networks can keep working better.
66

7+
Website and web flasher: https://michtronics.github.io/MeshCoreNG/
8+
79
The goal is not to rebuild MeshCore from scratch. The goal is to add improvements step by step, without breaking existing clients or the existing protocol.
810

911
## Why This Matters In The Netherlands
@@ -466,13 +468,30 @@ region tree
466468
region save
467469
```
468470

471+
**Malformed chat handling:**
472+
473+
Companion radio firmware validates human-readable chat before it is shown to apps or displays. Invalid UTF-8, binary-looking text, excessive control characters, replacement characters, impossible timestamps and very low-confidence text are filtered at the chat rendering boundary. Binary datagrams, raw/custom packets, requests, responses and future packet types remain binary-safe.
474+
475+
By default malformed companion chat is shown as a compact filtered placeholder, so garbage text is not rendered in the Android/app side. Payloads that cannot be inspected, binary channel datagrams and unknown/future packet types are not blindly dropped.
476+
477+
Repeater firmware can be configured to drop malformed default-public-channel group text before retransmission:
478+
479+
```text
480+
get malformed.drop
481+
set malformed.drop on
482+
set malformed.drop off
483+
```
484+
485+
This is enabled by default on repeaters. Repeaters only drop text packets they can inspect and classify as malformed. Encrypted/private group text that the repeater cannot decrypt, binary datagrams and unknown/future packet types are still relayed according to the normal forwarding rules.
486+
469487
More CLI details are in [docs/cli_commands.md](./docs/cli_commands.md).
470488

471489
## Compatibility
472490

473491
MeshCoreNG remains compatible with the existing MeshCore ecosystem.
474492

475493
- No packet format change for these dense-mesh steps.
494+
- Chat sanitation is applied only to human-readable chat display/forwarding policy; binary transport stays supported.
476495
- Existing MeshCore clients keep working.
477496
- Existing MeshCore firmware can still talk to MeshCoreNG.
478497
- The default settings remain safe for normal and sparse networks.
@@ -501,9 +520,12 @@ For developers:
501520
MeshCoreNG now includes a GitHub Pages web flasher for ESP32-based repeater builds:
502521

503522
- MeshCoreNG web flasher: https://michtronics.github.io/MeshCoreNG/flasher/
523+
- MeshCoreNG website and docs: https://michtronics.github.io/MeshCoreNG/
504524

505525
The flasher is built with ESP Web Tools and works from Chrome or Edge using Web Serial. It is meant for ESP32-family boards. nRF52, RP2040 and STM32 boards still use their normal flashing files and tools.
506526

527+
ESP32 repeater builds flashed from this site have malformed public chat dropping enabled by default. Check or change it after flashing with `get malformed.drop`, `set malformed.drop on`, or `set malformed.drop off`.
528+
507529
The firmware files used by the web flasher come from GitHub Release assets. The release/CI workflow builds the ESP32 repeater variants, attaches the merged `.bin` files to the release, and the GitHub Pages workflow downloads those release files to create the ESP Web Tools manifests under `/flasher/`.
508530

509531
To add another ESP32 board to the web flasher, add its PlatformIO environment name, display name, chip family, and description to `webflasher/boards.json`. The matching release asset must be named like `<env>-*-merged.bin`.

docs/cli_commands.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,20 @@ On ESP32 boards with supported LoRa DIO1 wake wiring, sleep can wake by LoRa RX
474474

475475
---
476476

477+
#### View or change malformed repeater forwarding
478+
**Usage:**
479+
- `get malformed.drop`
480+
- `set malformed.drop on`
481+
- `set malformed.drop off`
482+
483+
**Parameters:**
484+
- `on`: drop malformed decryptable default-public-channel group text before retransmission
485+
- `off`: leave forwarding behavior unchanged
486+
487+
**Default:** `on`
488+
489+
**Note:** This only applies to human-readable default public group text that the repeater can decrypt and inspect. Binary channel datagrams, raw/custom payloads, requests, responses, private/encrypted packets that cannot be inspected and unknown/future packet types are still handled by the normal forwarding rules. Existing saved preferences are preserved; use `set malformed.drop off` to disable this behavior on a repeater.
490+
477491
#### View or change this node's advert path hash size
478492
**Usage:**
479493
- `get path.hash.mode`

docs/companion_protocol.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,8 @@ Bytes 7+: Message Text (UTF-8, variable length)
272272

273273
**Timestamp**: Unix timestamp in seconds (32-bit unsigned integer, little-endian)
274274

275+
**Text validity**: Channel text is expected to be valid human-readable UTF-8. Firmware validates received text before queuing it to the companion app. Malformed incoming text may be replaced with `[Malformed packet filtered]`. This does not affect channel data datagrams.
276+
275277
**Example** (send "Hello" to channel 1 at timestamp 1234567890):
276278
```
277279
03 00 01 D2 02 96 49 48 65 6C 6C 6F
@@ -329,6 +331,10 @@ Byte 0: 0x0A
329331

330332
**Note**: Poll this command periodically to retrieve queued messages. The device may also send `PACKET_MESSAGES_WAITING` (0x83) as a notification when messages are available.
331333

334+
**Malformed chat behavior**: Received contact and channel text is sanitized before it is queued. The firmware rejects malformed UTF-8, excessive control/replacement characters, binary-looking text and impossible timestamps. A malformed chat item may be queued as `[Malformed packet filtered]` instead of raw garbage text.
335+
336+
Binary channel data received through `CMD_SEND_CHANNEL_DATA` / channel data callbacks is not sanitized as text and remains binary-safe.
337+
332338
---
333339

334340
### 7. Get Battery and Storage

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Below are a few quick start guides.
99
- [Companion Protocol](./companion_protocol.md)
1010
- [Dutch Region Database](./dutch_region_db.md)
1111
- [Packet Format](./packet_format.md)
12+
- [Payloads](./payloads.md)
1213
- [QR Codes](./qr_codes.md)
1314

1415
If you find a mistake in any of our documentation, or find something is missing, please feel free to open a pull request for us to review.

docs/payloads.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ txt_type
174174
| `0x01` | CLI command | the command text of the message |
175175
| `0x02` | signed plain text message | first four bytes is sender pubkey prefix, followed by plain text message |
176176

177+
Human-readable text payloads are validated before firmware UI/app callbacks render them. Implementations may reject or hide malformed UTF-8, excessive replacement/control characters, binary-looking high-entropy text and impossible timestamps. This validation is a display/forwarding policy for text payloads only; it does not change the packet format and does not apply to binary datagram, raw/custom, request, response or unknown/future payload types.
178+
177179
# Anonymous request
178180

179181
| Field | Size (bytes) | Description |
@@ -236,6 +238,8 @@ txt_type
236238

237239
The plaintext contained in the ciphertext matches the format described in [plain text message](#plain-text-message). Specifically, it consists of a four byte timestamp, a flags byte, and the message. The flags byte will generally be `0x00` because it is a "plain text message". The message will be of the form `<sender name>: <message body>` (eg., `user123: I'm on my way`).
238240

241+
Nodes that can decrypt a group text packet may apply the same human-readable text validation before displaying or forwarding it. Companion radio firmware sanitizes malformed decryptable group text before queuing it to the app and does not expose a separate malformed-drop command. Repeater firmware can be configured with `set malformed.drop on` to drop malformed decryptable default-public-channel group text before retransmission. Group datagrams remain binary payloads and are not subject to this text filter.
242+
239243
# Group datagram
240244

241245
| Field | Size (bytes) | Description |

examples/companion_radio/DataStore.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
233233
file.read((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89
234234
file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90
235235
file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121
236+
file.read(pad, 1); // 137 : reserved padding
236237

237238
file.close();
238239
}
@@ -273,6 +274,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
273274
file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89
274275
file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90
275276
file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121
277+
file.write(pad, 1); // 137 : unused
276278

277279
file.close();
278280
}

examples/companion_radio/MyMesh.cpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,14 @@ void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uin
530530
queueMessage(from, TXT_TYPE_SIGNED_PLAIN, pkt, sender_timestamp, sender_prefix, 4, text);
531531
}
532532

533+
void MyMesh::onMalformedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
534+
const char *reason, uint8_t score) {
535+
(void)reason;
536+
(void)score;
537+
markConnectionActive(from);
538+
queueMessage(from, TXT_TYPE_PLAIN, pkt, sender_timestamp, NULL, 0, TXT_MALFORMED_PLACEHOLDER);
539+
}
540+
533541
void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp,
534542
const char *text) {
535543
int i = 0;
@@ -577,6 +585,13 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
577585
#endif
578586
}
579587

588+
void MyMesh::onMalformedChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp,
589+
const char *reason, uint8_t score) {
590+
(void)reason;
591+
(void)score;
592+
onChannelMessageRecv(channel, pkt, timestamp, TXT_MALFORMED_PLACEHOLDER);
593+
}
594+
580595
void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type,
581596
const uint8_t *data, size_t data_len) {
582597
if (data_len > MAX_CHANNEL_DATA_LENGTH) {
@@ -925,7 +940,6 @@ void MyMesh::begin(bool has_display) {
925940
_prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, -9, MAX_LORA_TX_POWER);
926941
_prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1
927942
_prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours
928-
929943
#ifdef BLE_PIN_CODE // 123456 by default
930944
if (_prefs.ble_pin == 0) {
931945
#ifdef DISPLAY_CLASS

examples/companion_radio/MyMesh.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,12 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
136136
const char *text) override;
137137
void onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
138138
const uint8_t *sender_prefix, const char *text) override;
139+
void onMalformedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
140+
const char *reason, uint8_t score) override;
139141
void onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp,
140142
const char *text) override;
143+
void onMalformedChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp,
144+
const char *reason, uint8_t score) override;
141145
void onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type,
142146
const uint8_t *data, size_t data_len) override;
143147

examples/companion_radio/NodePrefs.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,4 @@ struct NodePrefs { // persisted to file
3434
uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64)
3535
char default_scope_name[31];
3636
uint8_t default_scope_key[16];
37-
};
37+
};

0 commit comments

Comments
 (0)