Meshtastic: Difference between revisions

From RevSpace
Jump to navigation Jump to search
 
(24 intermediate revisions by the same user not shown)
Line 8: Line 8:


== Intro ==
== Intro ==
The plan is to use Meshtastic to transfer citizen science measurement data.
The page describes the research done to figure out if Meshtastic can be used to transfer citizen science measurement data.


Stuff to figure out:
The proposition of what I want to achieve:
* network coverage: [https://map.meshnet.nl/ meshnet.nl map]
* a citizen science data sensor sends measurement data into the meshtastic network
* do nodes forward packets that do not belong to their "own" network? -> appears to be so!
* at some point the data reaches a meshtastic node that is connected to the internet, which forwards it over MQTT
* a central listener process picks up the data from the internet and processes it further (e.g. forward it to sensor.community)


My node: https://db.meshnet.nl/da639b54.html
Stuff to research:
* how extensive is the network, see [https://map.meshnet.nl/ meshnet.nl coverage map]
* how reliable is the network:
** Nodes have a wide range of technical configuration, however the majority of nodes appear to use the following:
*** LongFast channel with encryption key '1' is used at 869.525 MHz
*** Internet-connected nodes in the Netherlands are usually connected over MQTT at mqtt.meshnet.nl
*** a fraction of about 1-in-5 nodes appears to be connected to internet
*** hop limit of 3, this is the default
*** most nodes are in CLIENT mode (this is good)


Done:
Interesting info:
* I know the basics of meshtastic
* My own node: https://db.meshnet.nl/da639b54.html
* Understand how the encryption works, how keys are constructed, how nonce/salt is constructed
* Can pick up messages sent through the network and shared via MQTT back into a local application and decrypt them


TODO
TODO
* create a particulate matter sensor that sends meshtastic
* create a particulate matter sensor that sends meshtastic
** figure out the LoRa settings for a LoRa transceiver
** actually build it and test it out
* write a backend/plugin for my sensor-data-bridge
* write a backend/plugin for my sensor-data-bridge


Line 33: Line 38:
* data is broadcasted inside the network, until it reaches a node with a MQTT backend connection, typically within a maximum of 3 hops.
* data is broadcasted inside the network, until it reaches a node with a MQTT backend connection, typically within a maximum of 3 hops.
* data arriving at the MQTT server is picked up by a backend application, which can then process it further
* data arriving at the MQTT server is picked up by a backend application, which can then process it further
* we use a more-or-less citizen-science-data specific channel, so we don't interfere with other meshtastic traffic


Protocol design:
Protocol design:
* Use either the default channel (longfast + 01-key) or a dedicated channel (longfast? + custom key)
* Use the common LongFast channel, with the default key, this makes sure that other nodes understand our message and are able to forward it over MQTT
* Use standard network concepts from meshtastic to comply with meshtastic expectations for smooth operation
* Follow the the MeshPacket structure, wrap our citizen science playload in a protobuf with specific portnr (PRIVATE_APP = 256)
* Use meshtastic google protobuf for the network part, with a custom port > 256, e.g. 300
* Citizen science payload has its own custom encoding, as usual, so it is basically opaque to meshtastic, just a byte array
* Use custom encoding within the payload, basically the same as now
* A checksum (16-bit/32-bit?) or MAC at the end of the payload allows us to verify that it really is a citizen science data packet after decryption
* A 16-bit checksum at the end of the payload allows us to verify that it really is a citizen science data packet after decryption
* Each packet already has a semi-unique packet id. Combined with the node id, we consider this unique, so we can identify duplicates at the backend
* Each packet already has a semi-unique packet id, so we can identify duplicates at the backend


Backend:
Backend:
Line 49: Line 52:
** Attempt to decode according to the the protobuf portnum+payload (meshtastic 'Data')
** Attempt to decode according to the the protobuf portnum+payload (meshtastic 'Data')
** Check the port number
** Check the port number
** Check and remove the 16-bit checksum in the payload
** Check and remove checksum/MAC in the payload
** Process the citizen science data payload
* If all of the steps above check out, consider it to be a valid packet
* If all of the steps above check out, consider it to be a valid packet
* Check in a local cache if this is a duplicate packet and if so, ignore it
* Check in a local cache if this is a duplicate packet and if so, ignore it
Line 56: Line 60:
=== Packet structure ===
=== Packet structure ===


On the radio level:
On the LoRa radio level:
<pre>
<pre>
[ lora preamble | ... | <payload> | <to be documented> ]
[ 16-symbol preamble | lora explicit header | lora payload ]
</pre>
</pre>


Within the radio payload:
Within the lora payload:
<pre>
<pre>
[ meshtastic 16-byte header | protobuf containing portnum + payload ]
[ meshtastic 16-byte header | meshtastic private-data protobuf ]
                            |<--          encrypted part          -->
</pre>
</pre>
The protobuf is encrypted using key "AQ==" and a nonce derived from data in the 16-byte header.


Within the protobuf payload:
Within the meshtastic protobuf:
<pre>
<pre>
[citizen science data | 16-bit CRC]
[ portnum | private-data | flag ]
</pre>
The private-data is not encrypted separately
 
Within the private-data:
<pre>
[ citizen-science payload | 4-byte message authentication code ]
</pre>
With respect to security coding:
* Algorithm is AES-GCM
* The payload is *not* encrypted, but it suffixed by a message authentication code (MAC)
* The MAC is only 32-bit, to keep the length manageable
* The nonce used to calculate the MAC is similar to the earlier used nonce, but with the 'extra nonce' data equal to some application-specific id
 
Could be something like (python):
<pre>
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce, mac_len=4)  # 4-byte auth tag
ciphertext, auth_tag = cipher.encrypt_and_digest(message)
</pre>
or (ESP32):
<pre>
    mbedtls_gcm_context gcm;
    mbedtls_gcm_init(&gcm);
    mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, key, 128); // 128-bit AES
    mbedtls_gcm_crypt_and_tag(&gcm, MBEDTLS_GCM_ENCRYPT,
                              sizeof(plaintext), nonce, sizeof(nonce),
                              NULL, 0, plaintext, ciphertext,
                              sizeof(auth_tag), auth_tag);
</pre>
</pre>


Line 76: Line 107:


Quick links:
Quick links:
* radio parameters, for LongFast
** Frequency: 869.525 MHz
** LongFast = SF11BW250, CR 4/5, 16 symbols preamble?, explicit header, CRC on (?)
** sync word = 0x2B ("to be"), used to be 0x12, source code mentions it to be some hash containing channel name
* data structure:
* data structure:
** unencrypted header https://meshtastic.org/docs/overview/mesh-algo/#layer-1-unreliable-zero-hop-messaging
** unencrypted header https://meshtastic.org/docs/overview/mesh-algo/#layer-1-unreliable-zero-hop-messaging
Line 84: Line 119:
* decryption code used on the liam cottle map: https://github.com/liamcottle/meshtastic-map/blob/master/src/mqtt.js#L632
* decryption code used on the liam cottle map: https://github.com/liamcottle/meshtastic-map/blob/master/src/mqtt.js#L632
* Meshtastic encryption https://meshtastic.org/docs/overview/encryption/ This is very incomplete! Details need to be reverse-engineered from the code!
* Meshtastic encryption https://meshtastic.org/docs/overview/encryption/ This is very incomplete! Details need to be reverse-engineered from the code!
== Software ==
I wrote python scripts to interact with the MQTT server and arduino code to run on a LoRa capable ESP32 microcontroller.
See https://github.com/bertrik/mesh-backend


== Hardware ==
== Hardware ==
Line 89: Line 129:


== MQTT ==
== MQTT ==
In the netherlands, data is typically sent to the 'boreft' MQTT server, for example
In the netherlands, data is typically sent to the 'meshnet.nl' MQTT server, for example
   mosquitto_sub -h mqtt.meshnet.nl -u boreft -P meshboreft -t "#" -v
   mosquitto_sub -h mqtt.meshnet.nl -u boreft -P meshboreft -t "#" -v


Line 95: Line 135:
   msh/7460-7463/2/stat/!da5857c0 online
   msh/7460-7463/2/stat/!da5857c0 online
   msh/EU_868/NL/2/e/LongFast/!eb66115c �%]�g(=���gx�� H5��Aw=]�gE��H`���������LongFast␦
   msh/EU_868/NL/2/e/LongFast/!eb66115c �%]�g(=���gx�� H5��Aw=]�gE��H`���������LongFast␦
Topics with data on MQTT have the following structure:
  msh/REGION/2/e/CHANNELNAME/USERID
=== Sending messages ===
Requirements for sending mqtt downlinks:
* the meshtastic node needs to have a channel named "mqtt" (exactly), see https://github.com/meshtastic/firmware/blob/master/src/mqtt/MQTT.cpp#L354
* the meshtastic node has JSON be enabled in its MQTT settings
* -> the meshtastic node listens on topic: "ROOT/2/json/mqtt/+", where ROOT = "msh/gouda" in my case
* -> the mqtt publisher sends to topic: 'msh/gouda/2/json/mqtt/!da639b54' for example
* example payload: <pre>{"from": 3663960916, "type": "sendtext", "payload": "Test"}'</pre>


=== Example data ===
=== Example data ===
Line 133: Line 185:
channel_id: "LongFast"
channel_id: "LongFast"
gateway_id: "!da5c87d4"
gateway_id: "!da5c87d4"
</pre>
Packet with encrypted data:
<pre>
packet {
  from: 2893499041
  to: 4294967295
  channel: 8
  encrypted: "t\235\250XV\314\256\211\222\253~\245"\357z\252\314U<VZn\210\031\272\230.5\322\345\272\035\212?f\214"
  id: 3551320943
  rx_time: 1741514045
  hop_limit: 3
  priority: BACKGROUND
  hop_start: 3
  relay_node: 161
}
channel_id: "LongFast"
gateway_id: "!ac774aa1"
</pre>
</pre>

Latest revision as of 11:50, 3 April 2025

Project Meshtastic
Experiments with Meshtastic
Status In progress
Contact bertrik
Last Update 2025-04-03

1. Intro

The page describes the research done to figure out if Meshtastic can be used to transfer citizen science measurement data.

The proposition of what I want to achieve:

  • a citizen science data sensor sends measurement data into the meshtastic network
  • at some point the data reaches a meshtastic node that is connected to the internet, which forwards it over MQTT
  • a central listener process picks up the data from the internet and processes it further (e.g. forward it to sensor.community)

Stuff to research:

  • how extensive is the network, see meshnet.nl coverage map
  • how reliable is the network:
    • Nodes have a wide range of technical configuration, however the majority of nodes appear to use the following:
      • LongFast channel with encryption key '1' is used at 869.525 MHz
      • Internet-connected nodes in the Netherlands are usually connected over MQTT at mqtt.meshnet.nl
      • a fraction of about 1-in-5 nodes appears to be connected to internet
      • hop limit of 3, this is the default
      • most nodes are in CLIENT mode (this is good)

Interesting info:

TODO

  • create a particulate matter sensor that sends meshtastic
  • write a backend/plugin for my sensor-data-bridge

2. Design

Overall design:

  • citizen science nodes send data in meshtastic-compatible format, so packets can be routed accross the meshtastic network
  • data is sent from the node typically every 5 minutes
  • data is broadcasted inside the network, until it reaches a node with a MQTT backend connection, typically within a maximum of 3 hops.
  • data arriving at the MQTT server is picked up by a backend application, which can then process it further

Protocol design:

  • Use the common LongFast channel, with the default key, this makes sure that other nodes understand our message and are able to forward it over MQTT
  • Follow the the MeshPacket structure, wrap our citizen science playload in a protobuf with specific portnr (PRIVATE_APP = 256)
  • Citizen science payload has its own custom encoding, as usual, so it is basically opaque to meshtastic, just a byte array
  • A checksum (16-bit/32-bit?) or MAC at the end of the payload allows us to verify that it really is a citizen science data packet after decryption
  • Each packet already has a semi-unique packet id. Combined with the node id, we consider this unique, so we can identify duplicates at the backend

Backend:

  • The backend application listens on a topic on the de-facto central MQTT server for the netherlands, used by most meshtastic nodes, which is mqtt.meshnet.nl
  • Decoding works as follows:
    • Decrypt with the pre-shared-key (this always work but might result in garbage)
    • Attempt to decode according to the the protobuf portnum+payload (meshtastic 'Data')
    • Check the port number
    • Check and remove checksum/MAC in the payload
    • Process the citizen science data payload
  • If all of the steps above check out, consider it to be a valid packet
  • Check in a local cache if this is a duplicate packet and if so, ignore it
  • process citizen science data payload: there is no such thing as TTN attributes, so any data required for further forwarding need to be kept locally (e.g. login credentials for opensense / sensor.community)

2.1. Packet structure

On the LoRa radio level:

[ 16-symbol preamble | lora explicit header | lora payload ]

Within the lora payload:

[ meshtastic 16-byte header | meshtastic private-data protobuf ]

The protobuf is encrypted using key "AQ==" and a nonce derived from data in the 16-byte header.

Within the meshtastic protobuf:

[ portnum | private-data | flag ]

The private-data is not encrypted separately

Within the private-data:

[ citizen-science payload | 4-byte message authentication code ]

With respect to security coding:

  • Algorithm is AES-GCM
  • The payload is *not* encrypted, but it suffixed by a message authentication code (MAC)
  • The MAC is only 32-bit, to keep the length manageable
  • The nonce used to calculate the MAC is similar to the earlier used nonce, but with the 'extra nonce' data equal to some application-specific id

Could be something like (python):

cipher = AES.new(key, AES.MODE_GCM, nonce=nonce, mac_len=4)  # 4-byte auth tag
ciphertext, auth_tag = cipher.encrypt_and_digest(message)

or (ESP32):

    mbedtls_gcm_context gcm;
    mbedtls_gcm_init(&gcm);
    mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, key, 128); // 128-bit AES
    mbedtls_gcm_crypt_and_tag(&gcm, MBEDTLS_GCM_ENCRYPT,
                              sizeof(plaintext), nonce, sizeof(nonce),
                              NULL, 0, plaintext, ciphertext,
                              sizeof(auth_tag), auth_tag);

3. Protocol

See https://meshtastic.org/docs/overview/mesh-algo/

Quick links:

4. Software

I wrote python scripts to interact with the MQTT server and arduino code to run on a LoRa capable ESP32 microcontroller.

See https://github.com/bertrik/mesh-backend

5. Hardware

Nice antenna? https://nl.aliexpress.com/item/1005007301116616.html

6. MQTT

In the netherlands, data is typically sent to the 'meshnet.nl' MQTT server, for example

 mosquitto_sub -h mqtt.meshnet.nl -u boreft -P meshboreft -t "#" -v

Examples of typical data:

 msh/7460-7463/2/stat/!da5857c0 online
 msh/EU_868/NL/2/e/LongFast/!eb66115c �%]�g(=���gx�� H5��Aw=]�gE��H`���������LongFast␦

Topics with data on MQTT have the following structure:

 msh/REGION/2/e/CHANNELNAME/USERID

6.1. Sending messages

Requirements for sending mqtt downlinks:

  • the meshtastic node needs to have a channel named "mqtt" (exactly), see https://github.com/meshtastic/firmware/blob/master/src/mqtt/MQTT.cpp#L354
  • the meshtastic node has JSON be enabled in its MQTT settings
  • -> the meshtastic node listens on topic: "ROOT/2/json/mqtt/+", where ROOT = "msh/gouda" in my case
  • -> the mqtt publisher sends to topic: 'msh/gouda/2/json/mqtt/!da639b54' for example
  • example payload:
    {"from": 3663960916, "type": "sendtext", "payload": "Test"}'

6.2. Example data

Examples of data as decoded from MQTT using the meshtastic python service wrapper:

packet {
  from: 2732702784
  to: 4294967295
  decoded {
    portnum: POSITION_APP
    payload: "\r\224\234\024\037\025\303\266\233\002\030\n\270\001 "
  }
  id: 663882246
  rx_time: 1741511999
  rx_snr: -18
  hop_limit: 2
  rx_rssi: -128
  hop_start: 3
}
channel_id: "LongFast"
gateway_id: "!da544e50"

Packet with encrypted data:

packet {
  from: 1128181476
  to: 4294967295
  channel: 8
  encrypted: "\007\355{o\340e\352\221\204\3112\365h\304[0\321&\351^{]\264\334\373\320\313>\213\2635\023\345'"
  id: 4272151039
  rx_time: 1741512321
  rx_snr: 5.75
  hop_limit: 4
  rx_rssi: -83
  hop_start: 5
}
channel_id: "LongFast"
gateway_id: "!da5c87d4"