feat: implement full MQTT Go parity across 5 phases — binary protocol, auth/TLS, cross-protocol bridging, monitoring, and JetStream persistence

Phase 1: Binary MQTT 3.1.1 wire protocol with PipeReader-based parsing,
full packet type dispatch, and MQTT 3.1.1 compliance checks.

Phase 2: Auth pipeline routing MQTT CONNECT through AuthService,
TLS transport with SslStream wrapping, pinned cert validation.

Phase 3: IMessageRouter refactor (NatsClient → INatsClient),
MqttNatsClientAdapter for cross-protocol bridging, MqttTopicMapper
with full Go-parity topic/subject translation.

Phase 4: /connz mqtt_client field population, /varz actual MQTT port.

Phase 5: JetStream persistence — MqttStreamInitializer creates 5
internal streams, MqttConsumerManager for QoS 1/2 consumers,
subject-keyed session/retained lookups replacing linear scans.

All 503 MQTT tests and 1589 Core tests pass.
This commit is contained in:
Joseph Doherty
2026-03-13 10:09:40 -04:00
parent 0be321fa53
commit 845441b32c
34 changed files with 3194 additions and 126 deletions

View File

@@ -0,0 +1,136 @@
// Full Go-parity MQTT topic ↔ NATS subject translation.
// Go reference: golang/nats-server/server/mqtt.go mqttToNATSSubjectConversion ~line 2200
//
// Rules:
// MQTT → NATS:
// '/' → '.' (separator)
// '+' → '*' (single-level wildcard)
// '#' → '>' (multi-level wildcard)
// '.' in MQTT topics must be escaped (replaced with a placeholder)
// Empty levels (leading/trailing/consecutive slashes) produce empty tokens
// '$' prefix topics are protected from wildcard matching per MQTT spec [MQTT-4.7.2-1]
//
// NATS → MQTT (reverse):
// '.' → '/'
// '*' → '+'
// '>' → '#'
using System.Text;
namespace NATS.Server.Mqtt;
/// <summary>
/// Translates MQTT topics/filters to NATS subjects and vice versa with full Go parity.
/// Go reference: mqtt.go mqttToNATSSubjectConversion, mqttNATSToMQTTSubjectConversion.
/// </summary>
public static class MqttTopicMapper
{
// Escape sequence for dots that appear in MQTT topic names.
// Go uses _DOT_ internally to represent a literal dot in the NATS subject.
private const string DotEscape = "_DOT_";
private const string DotEscapeReverse = ".";
/// <summary>
/// Translates an MQTT topic or filter to a NATS subject.
/// Handles wildcards, dot escaping, empty levels, and '$' prefix protection.
/// </summary>
public static string MqttToNats(string mqttTopic)
{
if (mqttTopic.Length == 0)
return string.Empty;
var sb = new StringBuilder(mqttTopic.Length);
for (var i = 0; i < mqttTopic.Length; i++)
{
switch (mqttTopic[i])
{
case '/':
sb.Append('.');
break;
case '+':
sb.Append('*');
break;
case '#':
sb.Append('>');
break;
case '.':
// Dots in MQTT topic names must be escaped for NATS
sb.Append(DotEscape);
break;
default:
sb.Append(mqttTopic[i]);
break;
}
}
return sb.ToString();
}
/// <summary>
/// Translates a NATS subject back to an MQTT topic.
/// Reverses the mapping: '.' → '/', '*' → '+', '>' → '#', '_DOT_' → '.'.
/// </summary>
public static string NatsToMqtt(string natsSubject)
{
if (natsSubject.Length == 0)
return string.Empty;
// First, replace _DOT_ escape sequences back to dots
var working = natsSubject.Replace(DotEscape, "\x00");
var sb = new StringBuilder(working.Length);
for (var i = 0; i < working.Length; i++)
{
switch (working[i])
{
case '.':
sb.Append('/');
break;
case '*':
sb.Append('+');
break;
case '>':
sb.Append('#');
break;
case '\x00':
sb.Append('.');
break;
default:
sb.Append(working[i]);
break;
}
}
return sb.ToString();
}
/// <summary>
/// Returns true if an MQTT topic starts with '$', which means it should
/// NOT be matched by wildcard subscriptions (MQTT spec [MQTT-4.7.2-1]).
/// Topics starting with '$' are reserved for system/server use.
/// </summary>
public static bool IsDollarTopic(string mqttTopic)
=> mqttTopic.Length > 0 && mqttTopic[0] == '$';
/// <summary>
/// Returns true if an MQTT topic filter starts with '$', indicating
/// it explicitly targets system topics.
/// </summary>
public static bool IsDollarFilter(string mqttFilter)
=> mqttFilter.Length > 0 && mqttFilter[0] == '$';
/// <summary>
/// Checks if a wildcard filter would match a '$' topic.
/// Per MQTT spec, wildcard filters (starting with '#' or '+') must NOT
/// match topics beginning with '$'. Only explicit '$' filters match '$' topics.
/// </summary>
public static bool WildcardMatchesDollarTopic(string mqttFilter, string mqttTopic)
{
if (!IsDollarTopic(mqttTopic))
return true; // non-$ topics are always matchable
// $ topics only matched by filters that also start with $
return IsDollarFilter(mqttFilter);
}
}