feat: harden gateway reply remap and leaf loop transparency
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
19
tests/NATS.Server.Tests/GatewayAdvancedRemapRuntimeTests.cs
Normal file
19
tests/NATS.Server.Tests/GatewayAdvancedRemapRuntimeTests.cs
Normal file
@@ -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_.");
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user