using System.Collections.Concurrent; using System.Net; using System.Net.Sockets; using Microsoft.Extensions.Logging; using NATS.Server.Configuration; using NATS.Server.Gateways; using NATS.Server.Subscriptions; namespace NATS.Server.LeafNodes; /// /// Manages leaf node connections — both inbound (accepted) and outbound (solicited). /// Outbound connections use exponential backoff retry: 1s, 2s, 4s, ..., capped at 60s. /// Subject filtering via DenyExports (hub→leaf) and DenyImports (leaf→hub) is applied /// to both message forwarding and subscription propagation. /// Go reference: leafnode.go. /// public sealed class LeafNodeManager : IAsyncDisposable { public static readonly TimeSpan LeafNodeReconnectDelayAfterLoopDetected = TimeSpan.FromSeconds(30); public static readonly TimeSpan LeafNodeReconnectAfterPermViolation = TimeSpan.FromSeconds(30); public static readonly TimeSpan LeafNodeReconnectDelayAfterClusterNameSame = TimeSpan.FromSeconds(30); public static readonly TimeSpan LeafNodeWaitBeforeClose = TimeSpan.FromSeconds(5); private readonly LeafNodeOptions _options; private readonly ServerStats _stats; private readonly string _serverId; private readonly Action _remoteSubSink; private readonly Action _messageSink; private readonly ILogger _logger; private readonly ConcurrentDictionary _connections = new(StringComparer.Ordinal); private readonly ConcurrentDictionary _disabledRemotes = new(StringComparer.Ordinal); private readonly ConcurrentDictionary _leafClusters = new(StringComparer.Ordinal); private readonly LeafHubSpokeMapper _subjectFilter; private CancellationTokenSource? _cts; private Socket? _listener; private Task? _acceptLoopTask; /// /// Invoked each time a connection is successfully registered. /// Exposed for test synchronization only. /// internal Action? OnConnectionRegistered; /// /// Initial retry delay for solicited connections (1 second). /// Go reference: leafnode.go — DEFAULT_LEAF_NODE_RECONNECT constant. /// internal static readonly TimeSpan InitialRetryDelay = TimeSpan.FromSeconds(1); /// /// Maximum retry delay for solicited connections (60 seconds). /// internal static readonly TimeSpan MaxRetryDelay = TimeSpan.FromSeconds(60); public string ListenEndpoint => $"{_options.Host}:{_options.Port}"; /// /// Current TLS certificate path, or null if TLS is not configured. /// Updated by . /// Go reference: leafnode.go — TLSConfig / tlsCertFile on server options. /// public string? CurrentCertPath { get; private set; } /// /// Current TLS private key path, or null if TLS is not configured. /// Updated by . /// public string? CurrentKeyPath { get; private set; } /// /// True when a TLS certificate is currently configured for leaf node connections. /// Go reference: leafnode.go — tls_required / tlsTimeout on LeafNodeOpts. /// public bool IsTlsEnabled => CurrentCertPath is not null; /// /// When true, all outbound leaf connections are disabled regardless of per-remote settings. /// Go reference: leafnode.go isLeafConnectDisabled — global disable flag. /// public bool IsGloballyDisabled { get; private set; } /// /// Returns the number of remotes that have been individually disabled via /// . /// public int DisabledRemoteCount => _disabledRemotes.Count; /// /// Returns true when connections to are currently disabled, /// either because it was individually disabled or because all leaf connections are globally /// disabled. /// Go reference: leafnode.go isLeafConnectDisabled. /// public bool IsLeafConnectDisabled(string remoteUrl) => IsGloballyDisabled || _disabledRemotes.ContainsKey(remoteUrl); /// /// Returns true when the remote URL is still configured and not disabled. /// Go reference: leafnode.go remoteLeafNodeStillValid. /// internal bool RemoteLeafNodeStillValid(string remoteUrl) { if (IsLeafConnectDisabled(remoteUrl)) return false; if (_options.Remotes.Any(r => string.Equals(r, remoteUrl, StringComparison.OrdinalIgnoreCase))) return true; foreach (var remote in _options.RemoteLeaves) { if (remote.Urls.Any(u => string.Equals(u, remoteUrl, StringComparison.OrdinalIgnoreCase))) return true; } return false; } /// /// Disables outbound leaf connections to the specified remote URL. /// Has no effect if the remote is already disabled. /// Go reference: leafnode.go isLeafConnectDisabled — per-remote disable tracking. /// public void DisableLeafConnect(string remoteUrl, string? reason = null) { _disabledRemotes.TryAdd(remoteUrl, true); _logger.LogInformation( "Leaf connect disabled for remote {RemoteUrl} (reason={Reason})", remoteUrl, reason ?? "unspecified"); } /// /// Re-enables outbound leaf connections to the specified remote URL. /// Has no effect if the remote was not disabled. /// public void EnableLeafConnect(string remoteUrl) { _disabledRemotes.TryRemove(remoteUrl, out _); _logger.LogInformation("Leaf connect re-enabled for remote {RemoteUrl}", remoteUrl); } /// /// Disables all outbound leaf connections by setting the global disable flag. /// Per-remote disable state is preserved. /// Go reference: leafnode.go isLeafConnectDisabled — global flag. /// public void DisableAllLeafConnections(string? reason = null) { IsGloballyDisabled = true; _logger.LogInformation("All leaf connections globally disabled (reason={Reason})", reason ?? "unspecified"); } /// /// Clears the global disable flag so outbound leaf connections may resume. /// Per-remote disable state is unchanged. /// public void EnableAllLeafConnections() { IsGloballyDisabled = false; _logger.LogInformation("All leaf connections globally re-enabled"); } /// /// Returns a snapshot of the remote URLs that have been individually disabled via /// . /// public IReadOnlyList GetDisabledRemotes() => [.. _disabledRemotes.Keys]; /// /// Incremented each time detects a change and applies it. /// Useful for testing and observability. /// public int TlsReloadCount { get; private set; } /// /// Compares and against the /// currently active values. When either differs the paths are updated, the reload counter /// is incremented, and a with Changed = true is /// returned. When both are identical a result with Changed = false is returned and /// no state is mutated. /// Go reference: leafnode.go — reloadTLSConfig hot-reload path. /// public LeafTlsReloadResult UpdateTlsConfig(string? newCertPath, string? newKeyPath) { var previousCert = CurrentCertPath; if (string.Equals(CurrentCertPath, newCertPath, StringComparison.Ordinal) && string.Equals(CurrentKeyPath, newKeyPath, StringComparison.Ordinal)) { return new LeafTlsReloadResult(Changed: false, PreviousCertPath: previousCert, NewCertPath: newCertPath, Error: null); } CurrentCertPath = newCertPath; CurrentKeyPath = newKeyPath; TlsReloadCount++; _logger.LogInformation( "Leaf node TLS config updated (cert={CertPath}, key={KeyPath}, reloads={Count})", newCertPath, newKeyPath, TlsReloadCount); return new LeafTlsReloadResult(Changed: true, PreviousCertPath: previousCert, NewCertPath: newCertPath, Error: null); } public LeafNodeManager( LeafNodeOptions options, ServerStats stats, string serverId, Action remoteSubSink, Action messageSink, ILogger logger) { _options = options; _stats = stats; _serverId = serverId; _remoteSubSink = remoteSubSink; _messageSink = messageSink; _logger = logger; _subjectFilter = new LeafHubSpokeMapper( new Dictionary(), options.DenyExports, options.DenyImports, options.ExportSubjects, options.ImportSubjects); } public Task StartAsync(CancellationToken ct) { _cts = CancellationTokenSource.CreateLinkedTokenSource(ct); _listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); _listener.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); _listener.Bind(new IPEndPoint(IPAddress.Parse(_options.Host), _options.Port)); _listener.Listen(128); if (_options.Port == 0) _options.Port = ((IPEndPoint)_listener.LocalEndPoint!).Port; _acceptLoopTask = Task.Run(() => AcceptLoopAsync(_cts.Token)); foreach (var remote in _options.Remotes.Distinct(StringComparer.OrdinalIgnoreCase)) _ = Task.Run(() => ConnectSolicitedWithRetryAsync(remote, _options.JetStreamDomain, _cts.Token)); // Also start solicited connections for remotes parsed from the config file (RemoteLeaves). // RemoteLeaves are populated by the config parser from leafnodes.remotes[] blocks; // _options.Remotes is the simple programmatic list only. // Go reference: leafnode.go — createLeafNode starts solicited connections via connectToRemoteLeaf. foreach (var remoteLeaf in _options.RemoteLeaves) { foreach (var url in remoteLeaf.Urls.Distinct(StringComparer.OrdinalIgnoreCase)) _ = Task.Run(() => ConnectSolicitedWithRetryAsync(url, _options.JetStreamDomain, _cts.Token)); } _logger.LogDebug("Leaf manager started (listen={Host}:{Port})", _options.Host, _options.Port); return Task.CompletedTask; } /// /// Establishes a single solicited (outbound) leaf connection to the specified URL. /// Performs socket connection and LEAF handshake. If a JetStream domain is specified, /// it is propagated during the handshake. /// Go reference: leafnode.go — connectSolicited. /// public async Task ConnectSolicitedAsync(string url, string? account, CancellationToken ct) { var endPoint = ParseEndpoint(url); var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { await socket.ConnectAsync(endPoint.Address, endPoint.Port, ct); var connection = new LeafConnection(socket) { JetStreamDomain = _options.JetStreamDomain, IsSolicited = true, IsSpoke = true, }; await connection.PerformOutboundHandshakeAsync(_serverId, ct); Register(connection); _logger.LogDebug("Solicited leaf connection established to {Url} (account={Account})", url, account ?? "$G"); return connection; } catch { socket.Dispose(); throw; } } public async Task ForwardMessageAsync(string account, string subject, string? replyTo, ReadOnlyMemory payload, CancellationToken ct) { // Apply subject filtering: outbound direction is hub→leaf (DenyExports). // The subject may be loop-marked ($LDS.{serverId}.{realSubject}), so we // strip the marker before checking the filter against the logical subject. // Go reference: leafnode.go:475-478 (DenyExports → Publish deny list). var filterSubject = LeafLoopDetector.TryUnmark(subject, out var unmarked) ? unmarked : subject; if (!_subjectFilter.IsSubjectAllowed(filterSubject, LeafMapDirection.Outbound)) { _logger.LogDebug("Leaf outbound message denied for subject {Subject} (DenyExports)", filterSubject); return; } foreach (var connection in _connections.Values) await connection.SendMessageAsync(account, subject, replyTo, payload, ct); } public void PropagateLocalSubscription(string account, string subject, string? queue) => PropagateLocalSubscription(account, subject, queue, queueWeight: 0); public void PropagateLocalSubscription(string account, string subject, string? queue, int queueWeight) { // Subscription propagation is also subject to export filtering: // we don't propagate subscriptions for subjects that are denied. if (!_subjectFilter.IsSubjectAllowed(subject, LeafMapDirection.Outbound)) { _logger.LogDebug("Leaf subscription propagation denied for subject {Subject} (DenyExports)", subject); return; } foreach (var connection in _connections.Values) { if (!CanSpokeSendSubscription(connection, subject)) { _logger.LogDebug( "Leaf subscription propagation denied for spoke connection {RemoteId} and subject {Subject} (subscribe permissions)", connection.RemoteId ?? "", subject); continue; } _ = connection.SendLsPlusAsync(account, subject, queue, queueWeight, _cts?.Token ?? CancellationToken.None); } } public void PropagateLocalUnsubscription(string account, string subject, string? queue) { foreach (var connection in _connections.Values) _ = connection.SendLsMinusAsync(account, subject, queue, _cts?.Token ?? CancellationToken.None); } /// /// Sends permission and account information to the specified leaf connection. /// Finds the connection by ID, sets the account name, and applies the publish/subscribe /// allow lists. Returns a result describing whether the connection was found and its /// post-sync state. /// Go reference: leafnode.go — sendPermsAndAccountInfo. /// public LeafPermSyncResult SendPermsAndAccountInfo( string connectionId, string? account, IEnumerable? pubAllow, IEnumerable? subAllow) { if (!_connections.TryGetValue(connectionId, out var connection)) return new LeafPermSyncResult(Found: false, PermsSynced: false, AccountName: null, PublishAllowCount: 0, SubscribeAllowCount: 0); connection.AccountName = account; connection.SetPermissions(pubAllow, subAllow); _logger.LogDebug( "Leaf perms synced for connection {ConnectionId} (account={Account}, pubAllow={PubCount}, subAllow={SubCount})", connectionId, account ?? "$G", connection.AllowedPublishSubjects.Count, connection.AllowedSubscribeSubjects.Count); return new LeafPermSyncResult( Found: true, PermsSynced: connection.PermsSynced, AccountName: connection.AccountName, PublishAllowCount: connection.AllowedPublishSubjects.Count, SubscribeAllowCount: connection.AllowedSubscribeSubjects.Count); } /// /// Initialises the subject map for a leaf connection from the supplied set of subjects /// and returns the count of subjects sent. This mirrors the Go server's subject-map /// (smap) seeding that happens when a leaf first connects so the remote side knows /// which subjects have local interest. /// Go reference: leafnode.go — initLeafNodeSmapAndSendSubs. /// public int InitLeafNodeSmapAndSendSubs(string connectionId, IEnumerable subjects) { if (!_connections.TryGetValue(connectionId, out var connection)) return 0; var ct = _cts?.Token ?? CancellationToken.None; var count = 0; foreach (var subject in subjects) { _ = connection.SendLsPlusAsync("$G", subject, null, ct); count++; } _logger.LogDebug("Leaf smap initialised for connection {ConnectionId}: {Count} subjects sent", connectionId, count); return count; } /// /// Returns the current permission-sync status for the specified connection. /// Go reference: leafnode.go — sendPermsAndAccountInfo (read path). /// public LeafPermSyncResult GetPermSyncStatus(string connectionId) { if (!_connections.TryGetValue(connectionId, out var connection)) return new LeafPermSyncResult(Found: false, PermsSynced: false, AccountName: null, PublishAllowCount: 0, SubscribeAllowCount: 0); return new LeafPermSyncResult( Found: true, PermsSynced: connection.PermsSynced, AccountName: connection.AccountName, PublishAllowCount: connection.AllowedPublishSubjects.Count, SubscribeAllowCount: connection.AllowedSubscribeSubjects.Count); } /// /// Validates whether a leaf connection can migrate its JetStream domain to a proposed value. /// Clearing the domain (null/empty proposedDomain) is always valid. /// If the proposed domain matches the current domain, no migration is needed. /// If another connection already uses the proposed domain, a conflict is reported. /// Go reference: leafnode.go checkJetStreamMigrate. /// public JetStreamMigrationResult CheckJetStreamMigrate(string connectionId, string? proposedDomain) { if (!_connections.TryGetValue(connectionId, out var connection)) return new JetStreamMigrationResult(false, JetStreamMigrationStatus.ConnectionNotFound, $"Connection '{connectionId}' not found"); // Clearing domain is always valid. if (string.IsNullOrEmpty(proposedDomain)) return new JetStreamMigrationResult(true, JetStreamMigrationStatus.Valid, null); // If current domain already matches, no migration needed. if (string.Equals(connection.JetStreamDomain, proposedDomain, StringComparison.Ordinal)) return new JetStreamMigrationResult(true, JetStreamMigrationStatus.NoChangeNeeded, null); // Check for domain conflict with other connections. foreach (var (key, conn) in _connections) { if (string.Equals(key, connectionId, StringComparison.Ordinal)) continue; if (string.Equals(conn.JetStreamDomain, proposedDomain, StringComparison.Ordinal)) return new JetStreamMigrationResult(false, JetStreamMigrationStatus.DomainConflict, $"Domain '{proposedDomain}' is already in use by another connection"); } return new JetStreamMigrationResult(true, JetStreamMigrationStatus.Valid, null); } /// /// Returns the distinct set of JetStream domains across all active connections. /// Connections without a domain (null/empty JetStreamDomain) are excluded. /// Go reference: leafnode.go — per-connection domain tracking. /// public IReadOnlyList GetActiveJetStreamDomains() { var domains = new HashSet(StringComparer.Ordinal); foreach (var conn in _connections.Values) { if (!string.IsNullOrEmpty(conn.JetStreamDomain)) domains.Add(conn.JetStreamDomain); } return [.. domains]; } /// /// Returns true if any currently active connection is associated with the specified JetStream domain. /// Go reference: leafnode.go — checkJetStreamMigrate domain-in-use check. /// public bool IsJetStreamDomainInUse(string domain) { foreach (var conn in _connections.Values) { if (string.Equals(conn.JetStreamDomain, domain, StringComparison.Ordinal)) return true; } return false; } /// /// Count of connections that have a non-null, non-empty JetStream domain assigned. /// Go reference: leafnode.go — per-connection jsClusterDomain field. /// public int JetStreamEnabledConnectionCount { get { var count = 0; foreach (var conn in _connections.Values) { if (!string.IsNullOrEmpty(conn.JetStreamDomain)) count++; } return count; } } /// /// Registers a leaf cluster topology entry. /// Returns false if a cluster with the same name is already registered. /// Go reference: leafnode.go registerLeafNodeCluster. /// public bool RegisterLeafNodeCluster(string clusterName, string gatewayUrl, int connectionCount) { var info = new LeafClusterInfo { ClusterName = clusterName, GatewayUrl = gatewayUrl, ConnectionCount = connectionCount, }; return _leafClusters.TryAdd(clusterName, info); } /// /// Removes a leaf cluster entry by name. /// Returns false if no entry with that name exists. /// Go reference: leafnode.go — leaf cluster topology removal. /// public bool UnregisterLeafNodeCluster(string clusterName) => _leafClusters.TryRemove(clusterName, out _); /// /// Returns true if a leaf cluster with the given name is currently registered. /// Go reference: leafnode.go — leaf cluster topology lookup. /// public bool HasLeafNodeCluster(string clusterName) => _leafClusters.ContainsKey(clusterName); /// /// Returns the for the named cluster, or null if not registered. /// Go reference: leafnode.go — leaf cluster topology lookup. /// public LeafClusterInfo? GetLeafNodeCluster(string clusterName) => _leafClusters.TryGetValue(clusterName, out var info) ? info : null; /// /// Returns all registered leaf cluster entries as a read-only list. /// Go reference: leafnode.go — leaf cluster topology enumeration. /// public IReadOnlyList GetAllLeafClusters() => [.. _leafClusters.Values]; /// /// Count of registered leaf cluster topology entries. /// Go reference: leafnode.go — leaf cluster topology count. /// public int LeafClusterCount => _leafClusters.Count; /// /// Updates the connection count for the named leaf cluster. /// No-op if the cluster is not registered. /// Go reference: leafnode.go — leaf cluster connection count update. /// public void UpdateLeafClusterConnectionCount(string clusterName, int newCount) { if (_leafClusters.TryGetValue(clusterName, out var info)) info.ConnectionCount = newCount; } /// /// Returns all current connection IDs. Useful for tests and monitoring. /// internal IReadOnlyCollection GetConnectionIds() => _connections.Keys.ToArray(); /// /// Injects a directly into the tracked connections. /// For testing only — bypasses the normal handshake and registration path. /// internal void InjectConnectionForTesting(LeafConnection connection) { var key = $"{connection.RemoteId}:{connection.RemoteEndpoint}:{Guid.NewGuid():N}"; _connections.TryAdd(key, connection); } public async ValueTask DisposeAsync() { if (_cts == null) return; await _cts.CancelAsync(); _listener?.Dispose(); if (_acceptLoopTask != null) await _acceptLoopTask.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); foreach (var connection in _connections.Values) await connection.DisposeAsync(); _connections.Clear(); Interlocked.Exchange(ref _stats.Leafs, 0); _cts.Dispose(); _cts = null; _logger.LogDebug("Leaf manager stopped"); } /// /// Computes the next backoff delay using exponential backoff with a cap. /// Delay sequence: 1s, 2s, 4s, 8s, 16s, 32s, 60s, 60s, ... /// internal static TimeSpan ComputeBackoff(int attempt) { if (attempt < 0) attempt = 0; var seconds = Math.Min(InitialRetryDelay.TotalSeconds * Math.Pow(2, attempt), MaxRetryDelay.TotalSeconds); return TimeSpan.FromSeconds(seconds); } private async Task AcceptLoopAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { Socket socket; try { socket = await _listener!.AcceptAsync(ct); } catch { break; } _ = Task.Run(() => HandleInboundAsync(socket, ct), ct); } } private async Task HandleInboundAsync(Socket socket, CancellationToken ct) { var connection = new LeafConnection(socket) { JetStreamDomain = _options.JetStreamDomain, }; try { await connection.PerformInboundHandshakeAsync(_serverId, ct); Register(connection); } catch { await connection.DisposeAsync(); } } private async Task ConnectSolicitedWithRetryAsync(string remote, string? jetStreamDomain, CancellationToken ct) { var attempt = 0; while (!ct.IsCancellationRequested) { if (!RemoteLeafNodeStillValid(remote)) return; try { var endPoint = ParseEndpoint(remote); var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { await socket.ConnectAsync(endPoint.Address, endPoint.Port, ct); var connection = new LeafConnection(socket) { JetStreamDomain = jetStreamDomain, IsSolicited = true, IsSpoke = true, }; await connection.PerformOutboundHandshakeAsync(_serverId, ct); Register(connection); _logger.LogDebug("Solicited leaf connection established to {Remote}", remote); return; } catch { socket.Dispose(); throw; } } catch (OperationCanceledException) { return; } catch (Exception ex) { _logger.LogDebug(ex, "Leaf connect retry for {Remote} (attempt {Attempt})", remote, attempt); } var delay = ComputeBackoff(attempt); attempt++; try { await Task.Delay(delay, ct); } catch (OperationCanceledException) { return; } } } private void Register(LeafConnection connection) { var key = $"{connection.RemoteId}:{connection.RemoteEndpoint}:{Guid.NewGuid():N}"; if (!_connections.TryAdd(key, connection)) { _ = connection.DisposeAsync(); return; } OnConnectionRegistered?.Invoke(key); connection.RemoteSubscriptionReceived = sub => { _remoteSubSink(sub); return Task.CompletedTask; }; connection.MessageReceived = msg => { // Apply inbound filtering: DenyImports restricts leaf→hub messages. // The subject may be loop-marked ($LDS.{serverId}.{realSubject}), so we // strip the marker before checking the filter against the logical subject. // Go reference: leafnode.go:480-481 (DenyImports → Subscribe deny list). var filterSubject = LeafLoopDetector.TryUnmark(msg.Subject, out var unmarked) ? unmarked : msg.Subject; if (!_subjectFilter.IsSubjectAllowed(filterSubject, LeafMapDirection.Inbound)) { _logger.LogDebug("Leaf inbound message denied for subject {Subject} (DenyImports)", filterSubject); return Task.CompletedTask; } _messageSink(msg); return Task.CompletedTask; }; connection.StartLoop(_cts!.Token); Interlocked.Increment(ref _stats.Leafs); _ = Task.Run(() => WatchConnectionAsync(key, connection, _cts!.Token)); } private async Task WatchConnectionAsync(string key, LeafConnection connection, CancellationToken ct) { try { await connection.WaitUntilClosedAsync(ct); } catch { } finally { if (_connections.TryRemove(key, out _)) Interlocked.Decrement(ref _stats.Leafs); await connection.DisposeAsync(); } } /// /// Validates a reconnecting leaf node against the current server state. /// Checks for self-connect, duplicate connections, and JetStream domain conflicts. /// Go reference: leafnode.go addLeafNodeConnection — duplicate and domain checks. /// public LeafValidationResult ValidateRemoteLeafNode(string remoteId, string? account, string? jsDomain) { if (IsSelfConnect(remoteId)) return new LeafValidationResult(false, $"Self-connect detected: remoteId '{remoteId}' matches local server ID", LeafValidationError.SelfConnect); var existing = GetConnectionByRemoteId(remoteId); if (existing != null) return new LeafValidationResult(false, $"Duplicate connection: a connection from '{remoteId}' already exists", LeafValidationError.DuplicateConnection); if (!string.IsNullOrEmpty(jsDomain)) { foreach (var conn in _connections.Values) { if (!string.IsNullOrEmpty(conn.JetStreamDomain) && !string.Equals(conn.JetStreamDomain, jsDomain, StringComparison.Ordinal)) return new LeafValidationResult(false, $"JetStream domain conflict: incoming domain '{jsDomain}' conflicts with existing domain '{conn.JetStreamDomain}'", LeafValidationError.JetStreamDomainConflict); } } return new LeafValidationResult(true, null, LeafValidationError.None); } /// /// Returns true if the given remoteId matches this server's own ID (self-connect detection). /// Go reference: leafnode.go loop detection via server ID comparison. /// public bool IsSelfConnect(string remoteId) => string.Equals(remoteId, _serverId, StringComparison.Ordinal); /// /// Returns true if any currently registered connection has the specified remote server ID. /// public bool HasConnection(string remoteId) => GetConnectionByRemoteId(remoteId) != null; /// /// Returns the first registered connection whose RemoteId matches the given value, or null if none. /// public LeafConnection? GetConnectionByRemoteId(string remoteId) { foreach (var conn in _connections.Values) { if (string.Equals(conn.RemoteId, remoteId, StringComparison.Ordinal)) return conn; } return null; } private static bool CanSpokeSendSubscription(LeafConnection connection, string subject) { if (!connection.IsSpokeLeafNode()) return true; if (ShouldBypassSpokeSubscribePermission(subject)) return true; if (!connection.PermsSynced || connection.AllowedSubscribeSubjects.Count == 0) return true; for (var i = 0; i < connection.AllowedSubscribeSubjects.Count; i++) { if (SubjectMatch.MatchLiteral(subject, connection.AllowedSubscribeSubjects[i])) return true; } return false; } private static bool ShouldBypassSpokeSubscribePermission(string subject) { if (string.IsNullOrEmpty(subject)) return false; if (subject[0] != '$' && subject[0] != '_') return false; return subject.StartsWith("$LDS.", StringComparison.Ordinal) || subject.StartsWith(ReplyMapper.GatewayReplyPrefix, StringComparison.Ordinal) || subject.StartsWith(ReplyMapper.OldGatewayReplyPrefix, StringComparison.Ordinal); } private static IPEndPoint ParseEndpoint(string endpoint) { // Handle full URLs with a scheme (e.g. "nats-leaf://127.0.0.1:5222"). // Uri.TryCreate handles both schemed URLs and bare "host:port" strings. if (Uri.TryCreate(endpoint, UriKind.Absolute, out var uri)) return new IPEndPoint(IPAddress.Parse(uri.Host), uri.Port); // Fall back to bare "host:port" splitting for plain strings without a scheme. var parts = endpoint.Split(':', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 2) throw new FormatException($"Invalid endpoint: {endpoint}"); return new IPEndPoint(IPAddress.Parse(parts[0]), int.Parse(parts[1])); } } /// /// Describes the outcome of a or /// call. /// /// True when the connection ID was found in the manager. /// True when has been called at least once. /// The account name associated with the connection, or null if not set. /// Number of allowed publish subjects currently configured. /// Number of allowed subscribe subjects currently configured. public sealed record LeafPermSyncResult( bool Found, bool PermsSynced, string? AccountName, int PublishAllowCount, int SubscribeAllowCount); /// /// Describes the outcome of a call. /// /// True when the cert or key path differed from the previously active values. /// The cert path that was active before this call. /// The cert path supplied to this call. /// Non-null when an error prevented the reload (reserved for future use). public sealed record LeafTlsReloadResult( bool Changed, string? PreviousCertPath, string? NewCertPath, string? Error); /// /// Result of validating a reconnecting leaf node. /// Go reference: leafnode.go addLeafNodeConnection validation logic. /// public sealed record LeafValidationResult( bool Valid, string? Error, LeafValidationError ErrorCode); /// /// Error codes for leaf node validation failures. /// public enum LeafValidationError { None, SelfConnect, DuplicateConnection, JetStreamDomainConflict } /// /// Describes the outcome of a call. /// /// True when migration to the proposed domain is allowed. /// Detailed status code for the migration check. /// Human-readable error message when is false, otherwise null. public sealed record JetStreamMigrationResult( bool Valid, JetStreamMigrationStatus Status, string? Error); /// /// Status codes for . /// Go reference: leafnode.go checkJetStreamMigrate return values. /// public enum JetStreamMigrationStatus { /// Migration to the proposed domain is allowed. Valid, /// The specified connection ID was not found. ConnectionNotFound, /// The proposed domain is identical to the current domain — no migration required. NoChangeNeeded, /// Another connection already uses the proposed domain. DomainConflict } /// /// Holds topology information for a registered leaf cluster entry. /// Go reference: leafnode.go — leaf cluster registration / registerLeafNodeCluster. /// public sealed class LeafClusterInfo { public required string ClusterName { get; init; } public required string GatewayUrl { get; init; } public int ConnectionCount { get; set; } public DateTime RegisteredAt { get; init; } = DateTime.UtcNow; }