diff --git a/MQTT.md b/MQTT.md new file mode 100644 index 0000000..06dd289 --- /dev/null +++ b/MQTT.md @@ -0,0 +1,46 @@ +###### default.conf +``` +# Insecure +listener 1883 + +# TLS/SSL +listener 8883 +acl_file /etc/mosquitto/conf.d/aclfile +protocol mqtt +require_certificate false +certfile /etc/mosquitto/certs/cert.pem +cafile /etc/mosquitto/certs/fullchain.pem +keyfile /etc/mosquitto/certs/privkey.pem + +listener 8083 +protocol websockets +certfile /etc/mosquitto/certs/cert.pem +cafile /etc/mosquitto/certs/fullchain.pem +keyfile /etc/mosquitto/certs/privkey.pem +``` + +###### /etc/mosquitto/conf.d/aclfile +``` +user acidvegas +topic readwrite msh/# + +user mate +topic readwrite msh/# + +pattern write $SYS/broker/connection/%c/state +``` + +###### mosquito.conf +``` +pid_file /run/mosquitto/mosquitto.pid + +per_listener_settings true +allow_anonymous false +persistence true +persistence_location /var/lib/mosquitto +password_file /etc/mosquitto/passwd + +log_dest file /var/log/mosquitto/mosquitto.log + +include_dir /etc/mosquitto/conf.d +``` \ No newline at end of file diff --git a/README.md b/README.md index 997016e..690bcf7 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The hardware I am experimenting with: [Lilygo T-Deck](https://www.lilygo.cc/prod ## Bugs & Issues - Devices must have Wifi turned off when going mobile. Upon leaving my house with WiFi still enabled, the UI & connection was EXTREMELY laggy & poor. Couldn't even type well... -- Devices using a custom MQTT with TLS & auth will cause a reboot loop *(Need to fix this ASAP)* +- Devices using a MQTT with TLS will reboot loop. - A fix for the reboot loop is simply disabling MQTT over serial with `meshtastic --set mqtt.enabled false` - `event_node` event is called **AS** we are defining the interface, so using `self.interface` in that callback will error. diff --git a/SETUP.md b/SETUP.md index 351be3a..439c2c0 100644 --- a/SETUP.md +++ b/SETUP.md @@ -46,7 +46,7 @@ meshtastic --set network.wifi_enabled true --set network.wifi_ssid "CHANGEME" -- ###### MQTT ``` -meshtastic --set mqtt.enabled true --set mqtt.address "CHANGEME" --set mqtt.username "CHANGEME" --set mqtt.password "CHANGEME" --set mqtt.tls_enabled true --set mqtt.root "msh/CHANGEME" --set mqtt.encryption_enabled true +meshtastic --set mqtt.enabled true --set mqtt.address changeme --set mqtt.username changeme --set mqtt.password changeme --set mqtt.encryption_enabled true --set mqtt.tls_enabled false --set mqtt.root msh --set mqtt.map_reporting_enabled true --set mqtt.json_enabled true ``` ###### BLUETOOTH diff --git a/meshirc.py b/meshirc.py index d6033fb..abff8d5 100644 --- a/meshirc.py +++ b/meshirc.py @@ -7,7 +7,7 @@ import logging import ssl import time -# EF576MkXA3aEURbCfNn6p0FfZdua4I +# 0xEF576MkXA3aEURbCfNn6p0FfZdua4I # Formatting Control Characters / Color Codes bold = '\x02' diff --git a/meshmqtt.py b/meshmqtt.py index 22477d1..fbee4d0 100644 --- a/meshmqtt.py +++ b/meshmqtt.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python # Meshtastic MQTT Interface - Developed by acidvegas in Python (https://acid.vegas/meshtastic) import argparse @@ -7,7 +7,7 @@ import logging try: from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.backends import default_backend except ImportError: raise SystemExit('missing the cryptography module (pip install cryptography)') @@ -37,7 +37,9 @@ def decode_encrypted(message_packet): ''' Decrypt an encrypted message packet. - :param message_packet: The message packet to decrypt''' + :param message_packet: The message packet to decrypt + ''' + try: key_bytes = base64.b64decode(key.encode('ascii')) @@ -79,10 +81,33 @@ def decode_encrypted(message_packet): logging.error(f'Failed to decrypt message: {str(e)}') +def encrypt_message(channel, key, mesh_packet, encoded_message): + ''' + Encrypt a message packet. + + :param channel: The channel to encrypt the message for + :param key: The encryption key + :param mesh_packet: The mesh packet to encrypt + :param encoded_message: The encoded message to encrypt + ''' + + mesh_packet.channel = generate_hash(channel, key) + key_bytes = base64.b64decode(key.encode('ascii')) + nonce_packet_id = mesh_packet.id.to_bytes(8, "little") + nonce_from_node = node_number.to_bytes(8, "little") + nonce = nonce_packet_id + nonce_from_node + + cipher = Cipher(algorithms.AES(key_bytes), modes.CTR(nonce), backend=default_backend()) + encryptor = cipher.encryptor() + encrypted_bytes = encryptor.update(encoded_message.SerializeToString()) + encryptor.finalize() + + return encrypted_bytes + + def on_connect(client, userdata, flags, rc, properties): ''' Callback for when the client receives a CONNACK response from the server. - + :param client: The client instance for this callback :param userdata: The private user data as set in Client() or user_data_set() :param flags: Response flags sent by the broker @@ -99,53 +124,110 @@ def on_connect(client, userdata, flags, rc, properties): def on_message(client, userdata, msg): ''' Callback for when a message is received from the server. - + :param client: The client instance for this callback :param userdata: The private user data as set in Client() or user_data_set() :param msg: An instance of MQTTMessage. This is a ''' - + + print(f'{msg.topic}: {msg.payload}') + service_envelope = mqtt_pb2.ServiceEnvelope() try: service_envelope.ParseFromString(msg.payload) - # print(service_envelope) + print(service_envelope) message_packet = service_envelope.packet - # print(message_packet) + print(message_packet) except Exception as e: - logging.error(f'Failed to parse message: {str(e)}') + #logging.error(f'Failed to parse message: {str(e)}') return if message_packet.HasField('encrypted') and not message_packet.HasField('decoded'): decode_encrypted(message_packet) +def on_subscribe(client, userdata, mid, reason_code_list, properties): + ''' + Callback for when the client receives a SUBACK response from the server. + + :param client: The client instance for this callback + :param userdata: The private user data as set in Client() or user_data_set() + :param mid: The message ID of the subscribe request + :param reason_code_list: A list of SUBACK reason codes + :param properties: The properties returned by the broker + ''' + + # Since we subscribed only for a single channel, reason_code_list contains + # a single entry + if reason_code_list[0].is_failure: + print(f"Broker rejected you subscription: {reason_code_list[0]}") + else: + print(f"Broker granted the following QoS: {reason_code_list[0].value}") + + +def on_unsubscribe(client, userdata, mid, reason_code_list, properties): + ''' + Callback for when the client receives a UNSUBACK response from the server. + + :param client: The client instance for this callback + :param userdata: The private user data as set in Client() or user_data_set() + :param mid: The message ID of the unsubscribe request + :param reason_code_list: A list of UNSUBACK reason codes + :param properties: The properties returned by the broker + ''' + + # Be careful, the reason_code_list is only present in MQTTv5. + # In MQTTv3 it will always be empty + if len(reason_code_list) == 0 or not reason_code_list[0].is_failure: + print("unsubscribe succeeded (if SUBACK is received in MQTTv3 it success)") + else: + print(f"Broker replied with failure: {reason_code_list[0]}") + client.disconnect() + + if __name__ == '__main__': parser = argparse.ArgumentParser(description='Mesh MQTT') parser.add_argument('--broker', default='mqtt.meshtastic.org', help='MQTT broker address') parser.add_argument('--port', default=1883, type=int, help='MQTT broker port') - parser.add_argument('--root', default='msh/US/2/c', help='Root topic') + parser.add_argument('--root', default='#', help='Root topic') parser.add_argument('--tls', action='store_true', help='Enable TLS/SSL') parser.add_argument('--username', default='meshdev', help='MQTT username') parser.add_argument('--password', default='large4cats', help='MQTT password') parser.add_argument('--key', default='AQ==', help='Encryption key') args = parser.parse_args() - # Ensure the key is padded and formatted correctly + # Ensure the key is padded and formatted correctly padded_key = args.key.ljust(len(args.key) + ((4 - (len(args.key) % 4)) % 4), '=') replaced_key = padded_key.replace('-', '+').replace('_', '/') key = replaced_key - broadcast_id = 4294967295 + broadcast_id = 4294967295 # Do we need to change this for a custom channel? - # client = mqtt.Client(client_id='', clean_session=True, userdata=None) - client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) - client.on_connect = on_connect + # Create the MQTT client + client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id='', clean_session=True, userdata=None) # Defaults to mqtt.MQTTv311 (change with protocol=mqtt.MQTTv5) + + # Set the authentication details client.username_pw_set(username=args.username, password=args.password) + + # Enable TLS/SSL if the --tls flag is set + if args.tls: + import ssl + client.tls_set(cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLSv1_2) + client.tls_insecure_set(False) + + # Set the callbacks + client.on_connect = on_connect + client.on_message = on_message + client.on_subscribe = on_subscribe + client.on_unsubscribe = on_unsubscribe + + # Connect to the broker client.connect(args.broker, args.port, 60) - client.on_message = on_message + + # Subscribe to the root topic client.subscribe(args.root, 0) - while client.loop() == 0: - pass + # Start the keep-alive loop + client.loop_forever()