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_.";
|
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)
|
public static string? ToGatewayReply(string? replyTo, string localClusterId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(replyTo))
|
if (string.IsNullOrWhiteSpace(replyTo))
|
||||||
@@ -16,14 +20,20 @@ public static class ReplyMapper
|
|||||||
{
|
{
|
||||||
restoredReply = string.Empty;
|
restoredReply = string.Empty;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(gatewayReply) || !gatewayReply.StartsWith(GatewayReplyPrefix, StringComparison.Ordinal))
|
if (!HasGatewayReplyPrefix(gatewayReply))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var clusterSeparator = gatewayReply.IndexOf('.', GatewayReplyPrefix.Length);
|
var current = gatewayReply!;
|
||||||
if (clusterSeparator < 0 || clusterSeparator == gatewayReply.Length - 1)
|
while (HasGatewayReplyPrefix(current))
|
||||||
|
{
|
||||||
|
var clusterSeparator = current.IndexOf('.', GatewayReplyPrefix.Length);
|
||||||
|
if (clusterSeparator < 0 || clusterSeparator == current.Length - 1)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
restoredReply = gatewayReply[(clusterSeparator + 1)..];
|
current = current[(clusterSeparator + 1)..];
|
||||||
|
}
|
||||||
|
|
||||||
|
restoredReply = current;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ public static class LeafLoopDetector
|
|||||||
{
|
{
|
||||||
private const string LeafLoopPrefix = "$LDS.";
|
private const string LeafLoopPrefix = "$LDS.";
|
||||||
|
|
||||||
|
public static bool HasLoopMarker(string subject)
|
||||||
|
=> subject.StartsWith(LeafLoopPrefix, StringComparison.Ordinal);
|
||||||
|
|
||||||
public static string Mark(string subject, string serverId)
|
public static string Mark(string subject, string serverId)
|
||||||
=> $"{LeafLoopPrefix}{serverId}.{subject}";
|
=> $"{LeafLoopPrefix}{serverId}.{subject}";
|
||||||
|
|
||||||
@@ -13,14 +16,20 @@ public static class LeafLoopDetector
|
|||||||
public static bool TryUnmark(string subject, out string unmarked)
|
public static bool TryUnmark(string subject, out string unmarked)
|
||||||
{
|
{
|
||||||
unmarked = subject;
|
unmarked = subject;
|
||||||
if (!subject.StartsWith(LeafLoopPrefix, StringComparison.Ordinal))
|
if (!HasLoopMarker(subject))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var serverSeparator = subject.IndexOf('.', LeafLoopPrefix.Length);
|
var current = subject;
|
||||||
if (serverSeparator < 0 || serverSeparator == subject.Length - 1)
|
while (HasLoopMarker(current))
|
||||||
|
{
|
||||||
|
var serverSeparator = current.IndexOf('.', LeafLoopPrefix.Length);
|
||||||
|
if (serverSeparator < 0 || serverSeparator == current.Length - 1)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
unmarked = subject[(serverSeparator + 1)..];
|
current = current[(serverSeparator + 1)..];
|
||||||
|
}
|
||||||
|
|
||||||
|
unmarked = current;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -879,6 +879,8 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
|||||||
var replyTo = message.ReplyTo;
|
var replyTo = message.ReplyTo;
|
||||||
if (ReplyMapper.TryRestoreGatewayReply(replyTo, out var restoredReply))
|
if (ReplyMapper.TryRestoreGatewayReply(replyTo, out var restoredReply))
|
||||||
replyTo = restoredReply;
|
replyTo = restoredReply;
|
||||||
|
else if (ReplyMapper.HasGatewayReplyPrefix(replyTo))
|
||||||
|
replyTo = null;
|
||||||
|
|
||||||
DeliverRemoteMessage(message.Account, message.Subject, replyTo, message.Payload);
|
DeliverRemoteMessage(message.Account, message.Subject, replyTo, message.Payload);
|
||||||
}
|
}
|
||||||
@@ -891,6 +893,8 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
|||||||
var subject = message.Subject;
|
var subject = message.Subject;
|
||||||
if (LeafLoopDetector.TryUnmark(subject, out var unmarked))
|
if (LeafLoopDetector.TryUnmark(subject, out var unmarked))
|
||||||
subject = unmarked;
|
subject = unmarked;
|
||||||
|
else if (LeafLoopDetector.HasLoopMarker(subject))
|
||||||
|
return;
|
||||||
|
|
||||||
DeliverRemoteMessage(message.Account, subject, message.ReplyTo, message.Payload);
|
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