Files
natsdotnet/docs/plans/2026-02-23-mqtt-connection-type-plan.md

30 KiB

MQTT Connection Type Parity + Config Parsing Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task.

Goal: Port Go-compatible MQTT connection-type handling for JWT allowed_connection_types, add /connz mqtt_client filtering, parse all Go MQTTOpts config fields, and expand MqttOptsVarz monitoring output — with tests and docs updates.

Architecture: Thread a connection-type value into auth context and enforce Go-style allowed-connection-type semantics in JwtAuthenticator. Add connz query-option filtering for mqtt_client across open and closed connections. Parse the full mqtt {} config block into a new MqttOptions model following the existing ParseTls() pattern in ConfigProcessor. Expand MqttOptsVarz and wire into /varz. Keep behavior backward-compatible and transport-agnostic so MQTT runtime plumbing can be added later without changing auth/monitoring/config semantics.

Tech Stack: .NET 10, xUnit 3, Shouldly, ASP.NET minimal APIs, System.Text.Json.


Task 1: Add failing JWT connection-type behavior tests

Files:

  • Modify: tests/NATS.Server.Tests/JwtAuthenticatorTests.cs

Step 1: Write the failing tests

Add these 5 test methods to the existing JwtAuthenticatorTests class. Each test must build a valid operator/account/user JWT chain (reuse the existing helper pattern from other tests in the file). The user JWT's nats.allowed_connection_types array controls which connection types are permitted.

[Fact]
public async Task Allowed_connection_types_allows_standard_context()
{
    // Build valid operator/account/user JWT chain.
    // User JWT includes: "allowed_connection_types":["STANDARD"]
    // Context sets ConnectionType = "STANDARD".
    // Assert Authenticate() is not null.
}

[Fact]
public async Task Allowed_connection_types_rejects_mqtt_only_for_standard_context()
{
    // User JWT includes: "allowed_connection_types":["MQTT"]
    // Context sets ConnectionType = "STANDARD".
    // Assert Authenticate() is null.
}

[Fact]
public async Task Allowed_connection_types_allows_known_even_with_unknown_values()
{
    // User JWT includes: ["STANDARD", "SOME_NEW_TYPE"]
    // Context sets ConnectionType = "STANDARD".
    // Assert Authenticate() is not null.
}

[Fact]
public async Task Allowed_connection_types_rejects_when_only_unknown_values_present()
{
    // User JWT includes: ["SOME_NEW_TYPE"]
    // Context sets ConnectionType = "STANDARD".
    // Assert Authenticate() is null.
}

[Fact]
public async Task Allowed_connection_types_is_case_insensitive_for_input_values()
{
    // User JWT includes: ["standard"]
    // Context sets ConnectionType = "STANDARD".
    // Assert Authenticate() is not null.
}

Step 2: Run test to verify it fails

Run: dotnet test tests/NATS.Server.Tests/NATS.Server.Tests.csproj --filter "FullyQualifiedName~JwtAuthenticatorTests.Allowed_connection_types" -v minimal Expected: FAIL (current implementation ignores allowed_connection_types).

Step 3: Commit test-only checkpoint

git add tests/NATS.Server.Tests/JwtAuthenticatorTests.cs
git commit -m "test: add failing jwt allowed connection type coverage"

Task 2: Implement auth connection-type model and Go-style allowed-type conversion

Files:

  • Modify: src/NATS.Server/Auth/IAuthenticator.cs (line 11-16: add ConnectionType property to ClientAuthContext)
  • Create: src/NATS.Server/Auth/Jwt/JwtConnectionTypes.cs
  • Modify: src/NATS.Server/Auth/JwtAuthenticator.cs (insert check after step 7 revocation check, before step 8 permissions, around line 97)
  • Modify: src/NATS.Server/NatsClient.cs (line 382-387: add ConnectionType to auth context construction)

Step 1: Add connection type to auth context

In src/NATS.Server/Auth/IAuthenticator.cs, add the ConnectionType property to ClientAuthContext. Note: this requires adding a using NATS.Server.Auth.Jwt; at the top of the file.

public sealed class ClientAuthContext
{
    public required ClientOptions Opts { get; init; }
    public required byte[] Nonce { get; init; }
    public string ConnectionType { get; init; } = JwtConnectionTypes.Standard;
    public X509Certificate2? ClientCertificate { get; init; }
}

Step 2: Create JWT connection-type constants + converter helper

Create new file src/NATS.Server/Auth/Jwt/JwtConnectionTypes.cs:

namespace NATS.Server.Auth.Jwt;

/// <summary>
/// Known connection type constants matching Go server/client.go.
/// Used for JWT allowed_connection_types claim validation.
/// Reference: golang/nats-server/server/client.go connectionType constants.
/// </summary>
internal static class JwtConnectionTypes
{
    public const string Standard = "STANDARD";
    public const string Websocket = "WEBSOCKET";
    public const string Leafnode = "LEAFNODE";
    public const string LeafnodeWs = "LEAFNODE_WS";
    public const string Mqtt = "MQTT";
    public const string MqttWs = "MQTT_WS";
    public const string InProcess = "INPROCESS";

    private static readonly HashSet<string> Known =
    [
        Standard, Websocket, Leafnode, LeafnodeWs, Mqtt, MqttWs, InProcess,
    ];

    /// <summary>
    /// Converts a list of connection type strings (from JWT claims) into a set of
    /// known valid types plus a flag indicating unknown values were present.
    /// Reference: Go server/client.go convertAllowedConnectionTypes.
    /// </summary>
    public static (HashSet<string> Valid, bool HasUnknown) Convert(IEnumerable<string>? values)
    {
        var valid = new HashSet<string>(StringComparer.Ordinal);
        var hasUnknown = false;
        if (values is null) return (valid, false);

        foreach (var raw in values)
        {
            var up = (raw ?? string.Empty).Trim().ToUpperInvariant();
            if (up.Length == 0) continue;
            if (Known.Contains(up)) valid.Add(up);
            else hasUnknown = true;
        }

        return (valid, hasUnknown);
    }
}

Step 3: Enforce allowed connection types in JWT auth

In src/NATS.Server/Auth/JwtAuthenticator.cs, insert the following block after the revocation check (step 7, around line 96) and before the permissions build (step 8):

// 7b. Check allowed connection types
var (allowedTypes, hasUnknown) = JwtConnectionTypes.Convert(userClaims.Nats?.AllowedConnectionTypes);

if (allowedTypes.Count == 0)
{
    if (hasUnknown)
        return null; // unknown-only list should reject
}
else
{
    var connType = string.IsNullOrWhiteSpace(context.ConnectionType)
        ? JwtConnectionTypes.Standard
        : context.ConnectionType.ToUpperInvariant();

    if (!allowedTypes.Contains(connType))
        return null;
}

Step 4: Set auth context connection type in client connect path

In src/NATS.Server/NatsClient.cs around line 382, add ConnectionType to the existing ClientAuthContext construction:

var context = new ClientAuthContext
{
    Opts = ClientOpts,
    Nonce = _nonce ?? [],
    ConnectionType = JwtConnectionTypes.Standard,
    ClientCertificate = TlsState?.PeerCert,
};

Add using NATS.Server.Auth.Jwt; at the top of the file.

Step 5: Run tests to verify pass

Run: dotnet test tests/NATS.Server.Tests/NATS.Server.Tests.csproj --filter "FullyQualifiedName~JwtAuthenticatorTests.Allowed_connection_types" -v minimal Expected: PASS.

Step 6: Commit implementation checkpoint

git add src/NATS.Server/Auth/IAuthenticator.cs src/NATS.Server/Auth/Jwt/JwtConnectionTypes.cs src/NATS.Server/Auth/JwtAuthenticator.cs src/NATS.Server/NatsClient.cs
git commit -m "feat: enforce jwt allowed connection types with go-compatible semantics"

Task 3: Add failing connz mqtt_client filter tests

Files:

  • Modify: tests/NATS.Server.Tests/MonitorTests.cs

Step 1: Write the failing tests

Add these 2 test methods to the existing MonitorTests class. These test the /connz?mqtt_client=<id> query parameter filtering.

[Fact]
public async Task Connz_filters_by_mqtt_client_for_open_connections()
{
    // Start server with monitoring port.
    // Connect a regular NATS client (no MQTT ID).
    // Query /connz?mqtt_client=some-id.
    // Assert num_connections == 0 (no client has that MQTT ID).
}

[Fact]
public async Task Connz_filters_by_mqtt_client_for_closed_connections()
{
    // Start server with monitoring port.
    // Query /connz?state=closed&mqtt_client=missing-id.
    // Assert num_connections == 0.
}

Step 2: Run tests to verify expected failure mode

Run: dotnet test tests/NATS.Server.Tests/NATS.Server.Tests.csproj --filter "FullyQualifiedName~MonitorTests.Connz_filters_by_mqtt_client" -v minimal Expected: FAIL (query option not implemented yet — mqtt_client param ignored, so all connections returned).

Step 3: Commit test-only checkpoint

git add tests/NATS.Server.Tests/MonitorTests.cs
git commit -m "test: add failing connz mqtt_client filter coverage"

Task 4: Implement connz mqtt_client filtering and closed snapshot support

Files:

  • Modify: src/NATS.Server/Monitoring/Connz.cs (line 191-210: add MqttClient to ConnzOptions)
  • Modify: src/NATS.Server/Monitoring/ConnzHandler.cs (line 148-201: parse query param; line 18-29: apply filter after collection but before sort)
  • Modify: src/NATS.Server/Monitoring/ClosedClient.cs (line 6-25: add MqttClient property)
  • Modify: src/NATS.Server/NatsServer.cs (line 695-714: add MqttClient to closed snapshot)

Step 1: Add MqttClient to ConnzOptions

In src/NATS.Server/Monitoring/Connz.cs, add after FilterSubject property (line 205):

public string MqttClient { get; set; } = "";

Step 2: Parse mqtt_client query param in handler

In src/NATS.Server/Monitoring/ConnzHandler.cs ParseQueryParams method, add after the existing limit parse block (around line 198):

if (q.TryGetValue("mqtt_client", out var mqttClient))
    opts.MqttClient = mqttClient.ToString();

Step 3: Apply mqtt_client filter in HandleConnz

In src/NATS.Server/Monitoring/ConnzHandler.cs HandleConnz method, add after the closed connections collection block (after line 29) and before the sort validation (line 32):

// Filter by MQTT client ID
if (!string.IsNullOrEmpty(opts.MqttClient))
    connInfos = connInfos.Where(c => c.MqttClient == opts.MqttClient).ToList();

Step 4: Add MqttClient to ClosedClient model

In src/NATS.Server/Monitoring/ClosedClient.cs, add after line 24 (TlsCipherSuite):

public string MqttClient { get; init; } = "";

Step 5: Add MqttClient to closed snapshot creation in NatsServer.RemoveClient

In src/NATS.Server/NatsServer.cs around line 713 (inside the new ClosedClient { ... } block), add:

MqttClient = "", // populated when MQTT transport is implemented

Step 6: Add MqttClient to BuildClosedConnInfo

In src/NATS.Server/Monitoring/ConnzHandler.cs BuildClosedConnInfo method (line 119-146), add to the new ConnInfo { ... } initializer:

MqttClient = closed.MqttClient,

Step 7: Run connz mqtt filter tests

Run: dotnet test tests/NATS.Server.Tests/NATS.Server.Tests.csproj --filter "FullyQualifiedName~MonitorTests.Connz_filters_by_mqtt_client" -v minimal Expected: PASS.

Step 8: Commit implementation checkpoint

git add src/NATS.Server/Monitoring/Connz.cs src/NATS.Server/Monitoring/ConnzHandler.cs src/NATS.Server/Monitoring/ClosedClient.cs src/NATS.Server/NatsServer.cs
git commit -m "feat: add connz mqtt_client filtering"

Task 5: Verification checkpoint for JWT + connz tasks

Step 1: Run all JWT connection-type tests

Run: dotnet test tests/NATS.Server.Tests/NATS.Server.Tests.csproj --filter "FullyQualifiedName~JwtAuthenticatorTests.Allowed_connection_types" -v minimal Expected: PASS.

Step 2: Run all connz tests

Run: dotnet test tests/NATS.Server.Tests/NATS.Server.Tests.csproj --filter "FullyQualifiedName~MonitorTests.Connz" -v minimal Expected: PASS.

Step 3: Run full test suite

Run: dotnet test tests/NATS.Server.Tests/NATS.Server.Tests.csproj -v minimal Expected: PASS (no regressions).


Task 6: Add MqttOptions model and config parsing

Files:

  • Create: src/NATS.Server/MqttOptions.cs
  • Modify: src/NATS.Server/NatsOptions.cs (line 116-117: add Mqtt property)
  • Modify: src/NATS.Server/Configuration/ConfigProcessor.cs (line 248: add mqtt case; add ParseMqtt + ParseMqttAuth + ParseMqttTls + ToDouble methods)

Step 1: Create MqttOptions model

Create new file src/NATS.Server/MqttOptions.cs. This matches Go MQTTOpts struct (golang/nats-server/server/opts.go:613-707):

namespace NATS.Server;

/// <summary>
/// MQTT protocol configuration options.
/// Corresponds to Go server/opts.go MQTTOpts struct.
/// Config is parsed and stored but no MQTT listener is started yet.
/// </summary>
public sealed class MqttOptions
{
    // Network
    public string Host { get; set; } = "";
    public int Port { get; set; }

    // Auth override (MQTT-specific, separate from global auth)
    public string? NoAuthUser { get; set; }
    public string? Username { get; set; }
    public string? Password { get; set; }
    public string? Token { get; set; }
    public double AuthTimeout { get; set; }

    // TLS
    public string? TlsCert { get; set; }
    public string? TlsKey { get; set; }
    public string? TlsCaCert { get; set; }
    public bool TlsVerify { get; set; }
    public double TlsTimeout { get; set; } = 2.0;
    public bool TlsMap { get; set; }
    public HashSet<string>? TlsPinnedCerts { get; set; }

    // JetStream integration
    public string? JsDomain { get; set; }
    public int StreamReplicas { get; set; }
    public int ConsumerReplicas { get; set; }
    public bool ConsumerMemoryStorage { get; set; }
    public TimeSpan ConsumerInactiveThreshold { get; set; }

    // QoS
    public TimeSpan AckWait { get; set; } = TimeSpan.FromSeconds(30);
    public ushort MaxAckPending { get; set; }
    public TimeSpan JsApiTimeout { get; set; } = TimeSpan.FromSeconds(5);

    public bool HasTls => TlsCert != null && TlsKey != null;
}

Step 2: Add Mqtt property to NatsOptions

In src/NATS.Server/NatsOptions.cs, add before the HasTls property (around line 117):

// MQTT configuration (parsed from config, no listener yet)
public MqttOptions? Mqtt { get; set; }

Step 3: Add ToDouble helper to ConfigProcessor

In src/NATS.Server/Configuration/ConfigProcessor.cs, add after the ToString helper (around line 654):

private static double ToDouble(object? value) => value switch
{
    double d => d,
    long l => l,
    int i => i,
    string s when double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var d) => d,
    _ => throw new FormatException($"Cannot convert {value?.GetType().Name ?? "null"} to double"),
};

Step 4: Add mqtt case to ProcessKey switch

In src/NATS.Server/Configuration/ConfigProcessor.cs, replace the default case comment at line 248:

// MQTT
case "mqtt":
    if (value is Dictionary<string, object?> mqttDict)
        ParseMqtt(mqttDict, opts, errors);
    break;

// Unknown keys silently ignored (cluster, jetstream, gateway, leafnode, etc.)
default:
    break;

Step 5: Add ParseMqtt method

Add this method after ParseTags (around line 621). It follows the exact key/alias structure from Go parseMQTT (opts.go:5443-5541):

// ─── MQTT parsing ─────────────────────────────────────────────

private static void ParseMqtt(Dictionary<string, object?> dict, NatsOptions opts, List<string> errors)
{
    var mqtt = opts.Mqtt ?? new MqttOptions();

    foreach (var (key, value) in dict)
    {
        switch (key.ToLowerInvariant())
        {
            case "listen":
                var (host, port) = ParseHostPort(value);
                if (host is not null) mqtt.Host = host;
                if (port is not null) mqtt.Port = port.Value;
                break;
            case "port":
                mqtt.Port = ToInt(value);
                break;
            case "host" or "net":
                mqtt.Host = ToString(value);
                break;
            case "no_auth_user":
                mqtt.NoAuthUser = ToString(value);
                break;
            case "tls":
                if (value is Dictionary<string, object?> tlsDict)
                    ParseMqttTls(tlsDict, mqtt, errors);
                break;
            case "authorization" or "authentication":
                if (value is Dictionary<string, object?> authDict)
                    ParseMqttAuth(authDict, mqtt, errors);
                break;
            case "ack_wait" or "ackwait":
                mqtt.AckWait = ParseDuration(value);
                break;
            case "js_api_timeout" or "api_timeout":
                mqtt.JsApiTimeout = ParseDuration(value);
                break;
            case "max_ack_pending" or "max_pending" or "max_inflight":
                var pending = ToInt(value);
                if (pending < 0 || pending > 0xFFFF)
                    errors.Add($"mqtt max_ack_pending invalid value {pending}, should be in [0..{0xFFFF}] range");
                else
                    mqtt.MaxAckPending = (ushort)pending;
                break;
            case "js_domain":
                mqtt.JsDomain = ToString(value);
                break;
            case "stream_replicas":
                mqtt.StreamReplicas = ToInt(value);
                break;
            case "consumer_replicas":
                mqtt.ConsumerReplicas = ToInt(value);
                break;
            case "consumer_memory_storage":
                mqtt.ConsumerMemoryStorage = ToBool(value);
                break;
            case "consumer_inactive_threshold" or "consumer_auto_cleanup":
                mqtt.ConsumerInactiveThreshold = ParseDuration(value);
                break;
            default:
                // Unknown MQTT keys silently ignored
                break;
        }
    }

    opts.Mqtt = mqtt;
}

private static void ParseMqttAuth(Dictionary<string, object?> dict, MqttOptions mqtt, List<string> errors)
{
    foreach (var (key, value) in dict)
    {
        switch (key.ToLowerInvariant())
        {
            case "user" or "username":
                mqtt.Username = ToString(value);
                break;
            case "pass" or "password":
                mqtt.Password = ToString(value);
                break;
            case "token":
                mqtt.Token = ToString(value);
                break;
            case "timeout":
                mqtt.AuthTimeout = ToDouble(value);
                break;
            default:
                break;
        }
    }
}

private static void ParseMqttTls(Dictionary<string, object?> dict, MqttOptions mqtt, List<string> errors)
{
    foreach (var (key, value) in dict)
    {
        switch (key.ToLowerInvariant())
        {
            case "cert_file":
                mqtt.TlsCert = ToString(value);
                break;
            case "key_file":
                mqtt.TlsKey = ToString(value);
                break;
            case "ca_file":
                mqtt.TlsCaCert = ToString(value);
                break;
            case "verify":
                mqtt.TlsVerify = ToBool(value);
                break;
            case "verify_and_map":
                var map = ToBool(value);
                mqtt.TlsMap = map;
                if (map) mqtt.TlsVerify = true;
                break;
            case "timeout":
                mqtt.TlsTimeout = ToDouble(value);
                break;
            case "pinned_certs":
                if (value is List<object?> pinnedList)
                {
                    var certs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
                    foreach (var item in pinnedList)
                    {
                        if (item is string s)
                            certs.Add(s.ToLowerInvariant());
                    }
                    mqtt.TlsPinnedCerts = certs;
                }
                break;
            default:
                break;
        }
    }
}

Step 6: Build to verify compilation

Run: dotnet build Expected: Build succeeded.

Step 7: Commit

git add src/NATS.Server/MqttOptions.cs src/NATS.Server/NatsOptions.cs src/NATS.Server/Configuration/ConfigProcessor.cs
git commit -m "feat: add mqtt config model and parser for all Go MQTTOpts fields"

Task 7: Add MQTT config parsing tests

Files:

  • Create: tests/NATS.Server.Tests/TestData/mqtt.conf
  • Modify: tests/NATS.Server.Tests/ConfigProcessorTests.cs

Step 1: Create MQTT test config file

Create tests/NATS.Server.Tests/TestData/mqtt.conf:

mqtt {
    listen: "10.0.0.1:1883"
    no_auth_user: "mqtt_default"

    authorization {
        user: "mqtt_user"
        pass: "mqtt_pass"
        token: "mqtt_token"
        timeout: 3.0
    }

    tls {
        cert_file: "/path/to/mqtt-cert.pem"
        key_file: "/path/to/mqtt-key.pem"
        ca_file: "/path/to/mqtt-ca.pem"
        verify: true
        timeout: 5.0
    }

    ack_wait: "60s"
    max_ack_pending: 2048
    js_domain: "mqtt-domain"
    js_api_timeout: "10s"
    stream_replicas: 3
    consumer_replicas: 1
    consumer_memory_storage: true
    consumer_inactive_threshold: "5m"
}

Ensure this file is copied to output: check that .csproj has a wildcard for TestData, or add:

<ItemGroup>
  <None Update="TestData\**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

Step 2: Add MQTT config tests

Add to tests/NATS.Server.Tests/ConfigProcessorTests.cs:

// ─── MQTT config ────────────────────────────────────────────

[Fact]
public void MqttConf_ListenHostAndPort()
{
    var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("mqtt.conf"));
    opts.Mqtt.ShouldNotBeNull();
    opts.Mqtt!.Host.ShouldBe("10.0.0.1");
    opts.Mqtt.Port.ShouldBe(1883);
}

[Fact]
public void MqttConf_NoAuthUser()
{
    var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("mqtt.conf"));
    opts.Mqtt.ShouldNotBeNull();
    opts.Mqtt!.NoAuthUser.ShouldBe("mqtt_default");
}

[Fact]
public void MqttConf_Authorization()
{
    var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("mqtt.conf"));
    opts.Mqtt.ShouldNotBeNull();
    opts.Mqtt!.Username.ShouldBe("mqtt_user");
    opts.Mqtt.Password.ShouldBe("mqtt_pass");
    opts.Mqtt.Token.ShouldBe("mqtt_token");
    opts.Mqtt.AuthTimeout.ShouldBe(3.0);
}

[Fact]
public void MqttConf_Tls()
{
    var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("mqtt.conf"));
    opts.Mqtt.ShouldNotBeNull();
    opts.Mqtt!.TlsCert.ShouldBe("/path/to/mqtt-cert.pem");
    opts.Mqtt.TlsKey.ShouldBe("/path/to/mqtt-key.pem");
    opts.Mqtt.TlsCaCert.ShouldBe("/path/to/mqtt-ca.pem");
    opts.Mqtt.TlsVerify.ShouldBeTrue();
    opts.Mqtt.TlsTimeout.ShouldBe(5.0);
    opts.Mqtt.HasTls.ShouldBeTrue();
}

[Fact]
public void MqttConf_QosSettings()
{
    var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("mqtt.conf"));
    opts.Mqtt.ShouldNotBeNull();
    opts.Mqtt!.AckWait.ShouldBe(TimeSpan.FromSeconds(60));
    opts.Mqtt.MaxAckPending.ShouldBe((ushort)2048);
    opts.Mqtt.JsApiTimeout.ShouldBe(TimeSpan.FromSeconds(10));
}

[Fact]
public void MqttConf_JetStreamSettings()
{
    var opts = ConfigProcessor.ProcessConfigFile(TestDataPath("mqtt.conf"));
    opts.Mqtt.ShouldNotBeNull();
    opts.Mqtt!.JsDomain.ShouldBe("mqtt-domain");
    opts.Mqtt.StreamReplicas.ShouldBe(3);
    opts.Mqtt.ConsumerReplicas.ShouldBe(1);
    opts.Mqtt.ConsumerMemoryStorage.ShouldBeTrue();
    opts.Mqtt.ConsumerInactiveThreshold.ShouldBe(TimeSpan.FromMinutes(5));
}

[Fact]
public void MqttConf_MaxAckPendingValidation_ReportsError()
{
    var ex = Should.Throw<ConfigProcessorException>(() =>
        ConfigProcessor.ProcessConfig("""
            mqtt {
                max_ack_pending: 70000
            }
            """));
    ex.Errors.ShouldContain(e => e.Contains("max_ack_pending"));
}

[Fact]
public void MqttConf_Aliases()
{
    // Test alias keys: "ackwait" (alias for "ack_wait"), "net" (alias for "host"),
    // "max_inflight" (alias for "max_ack_pending"), "consumer_auto_cleanup" (alias)
    var opts = ConfigProcessor.ProcessConfig("""
        mqtt {
            net: "127.0.0.1"
            port: 1884
            ackwait: "45s"
            max_inflight: 500
            api_timeout: "8s"
            consumer_auto_cleanup: "10m"
        }
        """);
    opts.Mqtt.ShouldNotBeNull();
    opts.Mqtt!.Host.ShouldBe("127.0.0.1");
    opts.Mqtt.Port.ShouldBe(1884);
    opts.Mqtt.AckWait.ShouldBe(TimeSpan.FromSeconds(45));
    opts.Mqtt.MaxAckPending.ShouldBe((ushort)500);
    opts.Mqtt.JsApiTimeout.ShouldBe(TimeSpan.FromSeconds(8));
    opts.Mqtt.ConsumerInactiveThreshold.ShouldBe(TimeSpan.FromMinutes(10));
}

[Fact]
public void MqttConf_Absent_ReturnsNull()
{
    var opts = ConfigProcessor.ProcessConfig("port: 4222");
    opts.Mqtt.ShouldBeNull();
}

Step 3: Run MQTT config tests

Run: dotnet test tests/NATS.Server.Tests/NATS.Server.Tests.csproj --filter "FullyQualifiedName~ConfigProcessorTests.MqttConf" -v minimal Expected: PASS.

Step 4: Commit

git add tests/NATS.Server.Tests/TestData/mqtt.conf tests/NATS.Server.Tests/ConfigProcessorTests.cs
git commit -m "test: add mqtt config parsing coverage"

Task 8: Expand MqttOptsVarz and wire into /varz

Files:

  • Modify: src/NATS.Server/Monitoring/Varz.cs (lines 350-360: expand MqttOptsVarz)
  • Modify: src/NATS.Server/Monitoring/VarzHandler.cs (line 67-124: populate MQTT block from options)

Step 1: Expand MqttOptsVarz class

In src/NATS.Server/Monitoring/Varz.cs, replace the existing minimal MqttOptsVarz (lines 350-360) with the full Go-compatible struct (matching Go server/monitor.go:1365-1378):

/// <summary>
/// MQTT configuration monitoring information.
/// Corresponds to Go server/monitor.go MQTTOptsVarz struct.
/// </summary>
public sealed class MqttOptsVarz
{
    [JsonPropertyName("host")]
    public string Host { get; set; } = "";

    [JsonPropertyName("port")]
    public int Port { get; set; }

    [JsonPropertyName("no_auth_user")]
    public string NoAuthUser { get; set; } = "";

    [JsonPropertyName("auth_timeout")]
    public double AuthTimeout { get; set; }

    [JsonPropertyName("tls_map")]
    public bool TlsMap { get; set; }

    [JsonPropertyName("tls_timeout")]
    public double TlsTimeout { get; set; }

    [JsonPropertyName("tls_pinned_certs")]
    public string[] TlsPinnedCerts { get; set; } = [];

    [JsonPropertyName("js_domain")]
    public string JsDomain { get; set; } = "";

    [JsonPropertyName("ack_wait")]
    public long AckWait { get; set; }

    [JsonPropertyName("max_ack_pending")]
    public ushort MaxAckPending { get; set; }
}

Note: Go's AckWait is serialized as time.Duration (nanoseconds as int64). We follow the same pattern used for PingInterval and WriteDeadline in the existing Varz class.

Step 2: Populate MQTT block in VarzHandler

In src/NATS.Server/Monitoring/VarzHandler.cs, add MQTT population to the return new Varz { ... } block (around line 123, after HttpReqStats):

Mqtt = BuildMqttVarz(),

And add the helper method to VarzHandler:

private MqttOptsVarz BuildMqttVarz()
{
    var mqtt = _options.Mqtt;
    if (mqtt is null)
        return new MqttOptsVarz();

    return new MqttOptsVarz
    {
        Host = mqtt.Host,
        Port = mqtt.Port,
        NoAuthUser = mqtt.NoAuthUser ?? "",
        AuthTimeout = mqtt.AuthTimeout,
        TlsMap = mqtt.TlsMap,
        TlsTimeout = mqtt.TlsTimeout,
        TlsPinnedCerts = mqtt.TlsPinnedCerts?.ToArray() ?? [],
        JsDomain = mqtt.JsDomain ?? "",
        AckWait = (long)mqtt.AckWait.TotalNanoseconds,
        MaxAckPending = mqtt.MaxAckPending,
    };
}

Step 3: Build to verify compilation

Run: dotnet build Expected: Build succeeded.

Step 4: Add varz MQTT test

In tests/NATS.Server.Tests/MonitorTests.cs, add a test that verifies the MQTT section appears in /varz response. If there's an existing varz test pattern, follow it. Otherwise add:

[Fact]
public async Task Varz_includes_mqtt_config_when_set()
{
    // Start server with monitoring enabled and mqtt config set.
    // GET /varz.
    // Assert response contains "mqtt" block with expected host/port values.
}

The exact test implementation depends on how the existing varz tests create and query the server — follow the existing pattern in MonitorTests.cs.

Step 5: Run full test suite

Run: dotnet test tests/NATS.Server.Tests/NATS.Server.Tests.csproj -v minimal Expected: PASS.

Step 6: Commit

git add src/NATS.Server/Monitoring/Varz.cs src/NATS.Server/Monitoring/VarzHandler.cs tests/NATS.Server.Tests/MonitorTests.cs
git commit -m "feat: expand mqtt varz monitoring with all Go-compatible fields"

Task 9: Final verification and differences.md update

Files:

  • Modify: differences.md

Step 1: Run full test suite

Run: dotnet test tests/NATS.Server.Tests/NATS.Server.Tests.csproj -v minimal Expected: PASS (all tests green, no regressions).

Step 2: Update parity document

Edit differences.md:

  1. In the Connection Types table (section 2), update the MQTT row:
| MQTT clients | Y | Partial | JWT connection-type constants + config parsing; no MQTT transport yet |
  1. In the Connz Response table (section 7), update the MQTT client ID filtering row:
| MQTT client ID filtering | Y | Y | `mqtt_client` query param filters open and closed connections |
  1. In the Missing Options Categories (section 6), replace the "WebSocket/MQTT options" line:
- WebSocket options
- ~~MQTT options~~ — `mqtt {}` config block parsed with all Go `MQTTOpts` fields; no listener yet
  1. In the Auth Mechanisms table (section 5), add note to JWT row:
| JWT validation | Y | Y | ... + `allowed_connection_types` enforcement with Go-compatible semantics |

Step 3: Commit docs update

git add differences.md
git commit -m "docs: update differences.md for mqtt connection type parity"