diff --git a/src/NATS.Server/Gateways/ReplyMapper.cs b/src/NATS.Server/Gateways/ReplyMapper.cs index 120c605..72c9253 100644 --- a/src/NATS.Server/Gateways/ReplyMapper.cs +++ b/src/NATS.Server/Gateways/ReplyMapper.cs @@ -4,6 +4,10 @@ public static class ReplyMapper { private const string GatewayReplyPrefix = "_GR_."; + public static bool HasGatewayReplyPrefix(string? subject) + => !string.IsNullOrWhiteSpace(subject) + && subject.StartsWith(GatewayReplyPrefix, StringComparison.Ordinal); + public static string? ToGatewayReply(string? replyTo, string localClusterId) { if (string.IsNullOrWhiteSpace(replyTo)) @@ -16,14 +20,20 @@ public static class ReplyMapper { restoredReply = string.Empty; - if (string.IsNullOrWhiteSpace(gatewayReply) || !gatewayReply.StartsWith(GatewayReplyPrefix, StringComparison.Ordinal)) + if (!HasGatewayReplyPrefix(gatewayReply)) return false; - var clusterSeparator = gatewayReply.IndexOf('.', GatewayReplyPrefix.Length); - if (clusterSeparator < 0 || clusterSeparator == gatewayReply.Length - 1) - return false; + var current = gatewayReply!; + while (HasGatewayReplyPrefix(current)) + { + var clusterSeparator = current.IndexOf('.', GatewayReplyPrefix.Length); + if (clusterSeparator < 0 || clusterSeparator == current.Length - 1) + return false; - restoredReply = gatewayReply[(clusterSeparator + 1)..]; + current = current[(clusterSeparator + 1)..]; + } + + restoredReply = current; return true; } } diff --git a/src/NATS.Server/LeafNodes/LeafLoopDetector.cs b/src/NATS.Server/LeafNodes/LeafLoopDetector.cs index 90f5359..6793d59 100644 --- a/src/NATS.Server/LeafNodes/LeafLoopDetector.cs +++ b/src/NATS.Server/LeafNodes/LeafLoopDetector.cs @@ -4,6 +4,9 @@ public static class LeafLoopDetector { private const string LeafLoopPrefix = "$LDS."; + public static bool HasLoopMarker(string subject) + => subject.StartsWith(LeafLoopPrefix, StringComparison.Ordinal); + public static string Mark(string subject, string serverId) => $"{LeafLoopPrefix}{serverId}.{subject}"; @@ -13,14 +16,20 @@ public static class LeafLoopDetector public static bool TryUnmark(string subject, out string unmarked) { unmarked = subject; - if (!subject.StartsWith(LeafLoopPrefix, StringComparison.Ordinal)) + if (!HasLoopMarker(subject)) return false; - var serverSeparator = subject.IndexOf('.', LeafLoopPrefix.Length); - if (serverSeparator < 0 || serverSeparator == subject.Length - 1) - return false; + var current = subject; + while (HasLoopMarker(current)) + { + var serverSeparator = current.IndexOf('.', LeafLoopPrefix.Length); + if (serverSeparator < 0 || serverSeparator == current.Length - 1) + return false; - unmarked = subject[(serverSeparator + 1)..]; + current = current[(serverSeparator + 1)..]; + } + + unmarked = current; return true; } } diff --git a/src/NATS.Server/NatsServer.cs b/src/NATS.Server/NatsServer.cs index ff2f230..66bb1e6 100644 --- a/src/NATS.Server/NatsServer.cs +++ b/src/NATS.Server/NatsServer.cs @@ -879,6 +879,8 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable var replyTo = message.ReplyTo; if (ReplyMapper.TryRestoreGatewayReply(replyTo, out var restoredReply)) replyTo = restoredReply; + else if (ReplyMapper.HasGatewayReplyPrefix(replyTo)) + replyTo = null; DeliverRemoteMessage(message.Account, message.Subject, replyTo, message.Payload); } @@ -891,6 +893,8 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable var subject = message.Subject; if (LeafLoopDetector.TryUnmark(subject, out var unmarked)) subject = unmarked; + else if (LeafLoopDetector.HasLoopMarker(subject)) + return; DeliverRemoteMessage(message.Account, subject, message.ReplyTo, message.Payload); } diff --git a/tests/NATS.Server.Tests/GatewayAdvancedRemapRuntimeTests.cs b/tests/NATS.Server.Tests/GatewayAdvancedRemapRuntimeTests.cs new file mode 100644 index 0000000..36ce481 --- /dev/null +++ b/tests/NATS.Server.Tests/GatewayAdvancedRemapRuntimeTests.cs @@ -0,0 +1,19 @@ +using NATS.Server.Gateways; + +namespace NATS.Server.Tests; + +public class GatewayAdvancedRemapRuntimeTests +{ + [Fact] + public void Transport_internal_reply_and_loop_markers_never_leak_to_client_visible_subjects() + { + const string clientReply = "_INBOX.123"; + var nested = ReplyMapper.ToGatewayReply( + ReplyMapper.ToGatewayReply(clientReply, "CLUSTER-A"), + "CLUSTER-B"); + + ReplyMapper.TryRestoreGatewayReply(nested, out var restored).ShouldBeTrue(); + restored.ShouldBe(clientReply); + restored.ShouldNotStartWith("_GR_."); + } +} diff --git a/tests/NATS.Server.Tests/LeafNodes/LeafLoopTransparencyRuntimeTests.cs b/tests/NATS.Server.Tests/LeafNodes/LeafLoopTransparencyRuntimeTests.cs new file mode 100644 index 0000000..81a244f --- /dev/null +++ b/tests/NATS.Server.Tests/LeafNodes/LeafLoopTransparencyRuntimeTests.cs @@ -0,0 +1,18 @@ +using NATS.Server.LeafNodes; + +namespace NATS.Server.Tests.LeafNodes; + +public class LeafLoopTransparencyRuntimeTests +{ + [Fact] + public void Transport_internal_reply_and_loop_markers_never_leak_to_client_visible_subjects() + { + var nested = LeafLoopDetector.Mark( + LeafLoopDetector.Mark("orders.created", "S1"), + "S2"); + + LeafLoopDetector.TryUnmark(nested, out var unmarked).ShouldBeTrue(); + unmarked.ShouldBe("orders.created"); + unmarked.ShouldNotStartWith("$LDS."); + } +}