diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bf79e22 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +bin/ +obj/ +*.user +*.suo +*.log +.vs/ +.vscode/ +.git/ +.gitignore +Dockerfile +.dockerignore \ No newline at end of file diff --git a/.gitignore b/.gitignore index 35063fc..caed449 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,5 @@ CodeCoverage/ # NUnit *.VisualState.xml TestResult.xml -nunit-*.xml \ No newline at end of file +nunit-*.xml +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..101e03d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +# Copy csproj and restore dependencies +COPY Meshtastic.Mqtt.csproj ./ +RUN dotnet restore + +# Copy the rest of the code +COPY . ./ +RUN dotnet publish -c Release -o /app + +# Build runtime image +FROM mcr.microsoft.com/dotnet/runtime:9.0 +WORKDIR /app +COPY --from=build /app ./ + +# Expose ports +EXPOSE 1883 8883 + +# Set environment variable to control SSL mode +# ENV SSL=true # Uncomment to enable SSL by default + +ENTRYPOINT ["dotnet", "Meshtastic.Mqtt.dll"] \ No newline at end of file diff --git a/Meshtastic.Mqtt.csproj b/Meshtastic.Mqtt.csproj new file mode 100644 index 0000000..f46a74b --- /dev/null +++ b/Meshtastic.Mqtt.csproj @@ -0,0 +1,30 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/Meshtastic.Mqtt.sln b/Meshtastic.Mqtt.sln new file mode 100644 index 0000000..ce050c9 --- /dev/null +++ b/Meshtastic.Mqtt.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meshtastic.Mqtt", "Meshtastic.Mqtt.csproj", "{7467C293-5E7D-1ADF-451D-7E36BDAC9410}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7467C293-5E7D-1ADF-451D-7E36BDAC9410}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7467C293-5E7D-1ADF-451D-7E36BDAC9410}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7467C293-5E7D-1ADF-451D-7E36BDAC9410}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7467C293-5E7D-1ADF-451D-7E36BDAC9410}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {62FE50C6-004E-4C4D-BE74-89E28A6A1064} + EndGlobalSection +EndGlobal diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..c6af7ac --- /dev/null +++ b/Program.cs @@ -0,0 +1,178 @@ +using MQTTnet.Server; +using Meshtastic.Protobufs; +using Google.Protobuf; +using Serilog; +using MQTTnet.Protocol; +using System.Runtime.Loader; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog.Formatting.Compact; +using Meshtastic.Crypto; +using Meshtastic; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Reflection; + +var mqttFactory = new MqttServerFactory(); + +// #if SSL +var currentPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; +#pragma warning disable SYSLIB0057 // Type or member is obsolete +var certificate = new X509Certificate2(Path.Combine(currentPath, "certificate.pfx"), "large4cats", X509KeyStorageFlags.Exportable); +#pragma warning restore SYSLIB0057 // Type or member is obsolete + +var mqttServerOptions = new MqttServerOptionsBuilder() + .WithoutDefaultEndpoint() // This call disables the default unencrypted endpoint on port 1883 + .WithEncryptedEndpoint() + .WithEncryptedEndpointPort(8883) + .WithEncryptionCertificate(certificate.Export(X509ContentType.Pfx)) + .WithEncryptionSslProtocol(SslProtocols.Tls12) + .Build(); + Log.Logger.Information("Using SSL certificate for MQTT server"); + +// If you want to use a non-encrypted MQTT server, you can uncomment the following lines instead of the above +// var mqttServerOptions = new MqttServerOptionsBuilder() +// .WithDefaultEndpoint() +// .WithDefaultEndpointPort(1883) +// .Build(); +// Log.Logger.Information("Using unencrypted MQTT server"); +// #endif + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console(new RenderedCompactJsonFormatter()) + // .WriteTo.File(new RenderedCompactJsonFormatter(), "log.json", rollingInterval: RollingInterval.Hour) // File logging can be enabled if needed + .CreateLogger(); + +using var mqttServer = mqttFactory.CreateMqttServer(mqttServerOptions); + +static Data? DecryptMeshPacket(ServiceEnvelope serviceEnvelope) +{ + var nonce = new NonceGenerator(serviceEnvelope.Packet.From, serviceEnvelope.Packet.Id).Create(); + + var decrypted = PacketEncryption.TransformPacket(serviceEnvelope.Packet.Encrypted.ToByteArray(), nonce, Resources.DEFAULT_PSK); + var payload = Data.Parser.ParseFrom(decrypted); + + if (payload.Portnum > PortNum.UnknownApp && payload.Payload.Length > 0) + return payload; + + // Was not able to decrypt the payload + return null; +} + +mqttServer.InterceptingPublishAsync += async (args) => +{ + try + { + if (args.ApplicationMessage.Payload.Length == 0) + { + Log.Logger.Warning("Received empty payload on topic {@Topic} from {@ClientId}", args.ApplicationMessage.Topic, args.ClientId); + args.ProcessPublish = false; // This will block empty packets + return; + } + var serviceEnvelope = ServiceEnvelope.Parser.ParseFrom(args.ApplicationMessage.Payload); + + // Block malformed service envelopes / packets + if ( + String.IsNullOrWhiteSpace(serviceEnvelope.ChannelId) || + String.IsNullOrWhiteSpace(serviceEnvelope.GatewayId) || + serviceEnvelope.Packet == null || + serviceEnvelope.Packet.Id < 1 || + serviceEnvelope.Packet.From < 1 || + serviceEnvelope.Packet.Encrypted == null || + serviceEnvelope.Packet.Encrypted.Length < 1 || + serviceEnvelope.Packet.Decoded != null) + { + Log.Logger.Warning("Service envelope or packet is malformed. Blocking packet on topic {@Topic} from {@ClientId}", args.ApplicationMessage.Topic, args.ClientId); + args.ProcessPublish = false; + return; + } + + var data = DecryptMeshPacket(serviceEnvelope); + // If we were not able to decrypt the packet, it is likely encrypted with an unknown PSK + // Uncomment the following lines if you want to block these packets + // if (data == null) + // { + // Log.Logger.Warning("Service envelope does not contain a valid packet. Blocking packet"); + // args.ProcessPublish = false; // This will block packets that are not valid protobuf packets + // return; + // } + + if (data?.Portnum == PortNum.TextMessageApp) + { + Log.Logger.Information("Received text message on topic {@Topic} from {@ClientId}: {@Message}", + args.ApplicationMessage.Topic, args.ClientId, data.Payload.ToStringUtf8()); + } + else + { + Log.Logger.Information("Received packet on topic {@Topic} from {@ClientId} with port number: {@Portnum}", + args.ApplicationMessage.Topic, args.ClientId, data?.Portnum); + } + + // Any further validation logic to block a packet can be added here + args.ProcessPublish = true; + } + catch (InvalidProtocolBufferException) + { + Log.Logger.Warning("Failed to decode presumed protobuf packet. Blocking"); + args.ProcessPublish = false; // This will block packets encrypted on unknown PSKs + } + catch (Exception ex) + { + Log.Logger.Error("Exception occured while attempting to decode packet on {@Topic} from {@ClientId}: {@Exception}", args.ApplicationMessage.Topic, args.ClientId, ex.Message); + args.ProcessPublish = false; // This will block packets that caused us to encounter an exception + } +}; + +mqttServer.InterceptingSubscriptionAsync += (args) => +{ + args.ProcessSubscription = true; // Subscription filtering logic can be added here to only allow certain topics + + return Task.CompletedTask; +}; + +mqttServer.ValidatingConnectionAsync += (args) => +{ + args.ReasonCode = true ? // Authentication logic can be added here + MqttConnectReasonCode.Success : MqttConnectReasonCode.BadUserNameOrPassword; + + // You can block connections based on client ID, username, ip, etc. + return Task.CompletedTask; +}; + +static IHostBuilder CreateHostBuilder(string[] args) +{ + return Host.CreateDefaultBuilder(args) + .UseConsoleLifetime() + .ConfigureServices((hostContext, services) => + { + services + .AddSingleton(Console.Out); + }); +} + + +using var host = CreateHostBuilder(args).Build(); +await host.StartAsync(); +var lifetime = host.Services.GetRequiredService(); +await mqttServer.StartAsync(); + +var ended = new ManualResetEventSlim(); +var starting = new ManualResetEventSlim(); + +AssemblyLoadContext.Default.Unloading += ctx => +{ + starting.Set(); + Log.Logger.Debug("Waiting for completion"); + ended.Wait(); +}; + +starting.Wait(); + +Log.Logger.Debug("Received signal gracefully shutting down"); +await mqttServer.StopAsync(); +Thread.Sleep(1000); +ended.Set(); + +lifetime.StopApplication(); +await host.WaitForShutdownAsync(); diff --git a/README.md b/README.md index 4d495b9..2d4548e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,71 @@ -# mqtt -A meshtastic native packet aware MQTT broker +# Meshtastic MQTT Broker Boilerplate + +This project provides an MQTT broker boilerplate specifically designed for Meshtastic device networks. It handles encrypted mesh packets, validates messages, and can be configured to run with or without SSL. + +## Features + +- MQTT server implementation for Meshtastic devices +- Support for encrypted mesh packet handling and validation +- SSL support for secure MQTT connections +- Configurable logging with Serilog +- Packet filtering and validation logic + +## Docker Setup + +### Prerequisites + +- Docker installed on your system +- Certificate file (if using SSL mode) + +### Docker Installation + +1. Clone the repository: + ```bash + git clone https://github.com/meshtastic/mqtt + cd mqtt + ``` + +2. Build the Docker image: + ```bash + docker build -t meshtastic-mqtt-broker . + ``` + +#### SSL Mode (Port 8883) + +To run with SSL enabled: + +1. Place your certificate file (`certificate.pfx`) in the project directory. (see [MQTTnet Server Wiki](https://github.com/dotnet/MQTTnet/wiki/Server)) +2. Run the container with the SSL environment variable: + +```bash +docker run -p 8883:8883 -v $(pwd)/certificate.pfx:/app/certificate.pfx meshtastic-mqtt-broker +``` + +### Docker Compose Example + +```yaml +version: '3' +services: + mqtt-broker: + build: . + ports: + - "1883:1883" # Standard MQTT port + # - "8883:8883" # SSL port (uncomment if using SSL) + # environment: + # - SSL=true # Uncomment to enable SSL + # volumes: + # - ./certificate.pfx:/app/certificate.pfx # Mount certificate if using SSL + restart: unless-stopped +``` + +## Configuration Options + +- **SSL**: Set environment variable `SSL=true` to enable SSL mode +- **Certificate**: Mount your PFX certificate file to `/app/certificate.pfx` in the container +- **Ports**: The application uses port 1883 for standard MQTT and 8883 for SSL MQTT + +## Troubleshooting + +- Ensure proper network access to the Docker container +- Check that certificates are correctly formatted (for SSL mode) +- Review logs using `docker logs [container-id]` diff --git a/cert.pem b/cert.pem new file mode 100644 index 0000000..d0983bc --- /dev/null +++ b/cert.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIUAiOTlcvZA8d3i1YXISMcn+B4+PowDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNTA0MTYwMTQwNThaFw0yNjA0 +MTYwMTQwNThaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQC5qV6+hqXx9g3dEAWZ9ZfBL8K5s2M1JvPakRu4MW9l +My1Ugf6mAB/7FkY168STGU3/8r0a+E743eTaPGWrlhBY1tEfCYwXLi2+xiL3yOB1 ++6uzla7+THOM3G3CN/Hoq0zw6IB2PJQF2ZT8rDLbmYdvv8i4hWSHjxNU/cEK2/aP +rMhKv8uWieEmstDCoF8AKxpFvh9JYhyeDTF5w/2cPEP+LcR36F8rVr9EbiieJ27U +GDvfgDe+Vdjm4YDVPVswf8/goN+TehjAOMQx1BHQLEFEij9KBmYdl6+mQ2mMnlwK +YjZJ36133QolH00x/yYGEak/DS21gECMBbTUw3Y6hs6wdWmV/cHRV7tIrBKGkMxG +0RSdqgukDfXRzi7kPO7TuMu5ju2ifS7JKhhNrh0IbuHFKSNjFYW8tTaXNHBuumKo +GUH51XjlONN43kTDzKMHkPznDImrKKL3PhRqFXUdgiwSoRNHY6/XHPAW6ecJ7Jla +hctnLQFjf4zsRkgTQjlj+fz9JPQTljhFQUkQyotnnp2eC23ceX/dnKG4hz350GkP +zL31vQ62f2BPOHacaLVCJrLBffOJjyAqfvR2J0oGPUo+7iuvjurC2pq9wgKaLLbQ +Y3ncHaBD3bLZk/KzrirX8Rn8cPbCs1Q1Ehe+F2mbarRjSsGuOlEqatns3gYeIZSd +SwIDAQABo1MwUTAdBgNVHQ4EFgQUczeKEnJEd8rhlPzTSDhNUircBvUwHwYDVR0j +BBgwFoAUczeKEnJEd8rhlPzTSDhNUircBvUwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAgEAWTroM00DTyqsxj/SSAM9jx1XZXfEi5ebqCDawROmTdhF +ZnmoQ2wqtRU/B6en2GTah/RKOcAyby/Pgtd3X3/x+J1W1D2uH5wNlhCxlH8X+GbW +2Br4nRKlMxcg1TbHVowPybV5loAcGjiCuIsmk0Iq4M1WUn/Ex6SZENT6MxT3yYVS +neQ5NZhk6luUCpjxgY3XT2tclagM2i5YVVWT8kYQakmtRteBZmK4ZrFfc486/rNj +4hLX8kwhRriz6U2mCtNCLB1kMF5imDHnIBH2winOEmqnN2pEzC15PDFOvRMoJzIl +lXk3Odnv3BXIHosLJQheTsAMFrTDf82itVwoYFMx8QL/0jvYrVkdhzbZMJEcN6Bi +PeOcji3mqcOm1JKsYNPz9V/U0faoixOekG7y7PnW8cEsag9lrGPxr0rGJ9Y431O2 +GIV9D79WY25WWn23+R3VGZWrnOUY33UHl3ZngXnBSvyLMHP6ZSb/SuTM6hMPbqaB +uauhTqdXUc1P90hTToBz1wtbKwoYBYIM7sGZP8IFiFdCSaxX1Kckks6YELFxZWFN +1AWOG5GFT4xOGDgcBnbHcldejKEO6fRa+D0SSucyJ84EK1tvhKAMxPXnp73t4eE/ +gwnGZ/9oLm5RRjAFZeCFCjNgbA2bRTML8MqGYuaBSKMQX4cG2dSeJDH29vvj5tg= +-----END CERTIFICATE----- diff --git a/certificate.pfx b/certificate.pfx new file mode 100644 index 0000000..5b9c42c Binary files /dev/null and b/certificate.pfx differ diff --git a/key.pem b/key.pem new file mode 100644 index 0000000..6cb088a --- /dev/null +++ b/key.pem @@ -0,0 +1,54 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +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 +rH1xuKTR/w3KS06Rjacob21XXWL8FRkS +-----END ENCRYPTED PRIVATE KEY-----