// Copyright 2012-2026 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // Adapted from server/reload.go in the NATS server Go source. using System.Reflection; using System.Text.Json; using ZB.MOM.NatsNet.Server.Auth; using ZB.MOM.NatsNet.Server.Internal; namespace ZB.MOM.NatsNet.Server; public sealed partial class NatsServer { /// /// Applies supported changes from to the running server. /// Mirrors Go Server.ReloadOptions(). /// public Exception? ReloadOptions(ServerOptions newOptions) { ArgumentNullException.ThrowIfNull(newOptions); _reloadMu.EnterWriteLock(); try { var currentOptions = GetOpts().Clone(); // Match Go behavior: when operators are configured, trusted keys are cleared. if (currentOptions.TrustedOperators.Count > 0 && currentOptions.TrustedKeys.Count > 0) currentOptions.TrustedKeys = []; var clientOriginalPort = currentOptions.Port; var clusterOriginalPort = currentOptions.Cluster.Port; var gatewayOriginalPort = currentOptions.Gateway.Port; var leafOriginalPort = currentOptions.LeafNode.Port; var websocketOriginalPort = currentOptions.Websocket.Port; var mqttOriginalPort = currentOptions.Mqtt.Port; if (!string.IsNullOrEmpty(currentOptions.Cluster.ListenStr)) { newOptions.Cluster.ListenStr = currentOptions.Cluster.ListenStr; var overrideError = newOptions.OverrideCluster(); if (overrideError != null) return overrideError; } newOptions = ServerOptions.MergeOptions(newOptions, ServerOptions.FlagSnapshot); if (ServerOptions.FlagSnapshot != null) ConfigReloader.ApplyBoolFlags(newOptions, ServerOptions.FlagSnapshot); newOptions.SetBaselineOptions(); // Preserve already-bound listener ports on reload when RANDOM was used. if (newOptions.Port == 0) newOptions.Port = clientOriginalPort; if (newOptions.Cluster.Port == ServerConstants.RandomPort) newOptions.Cluster.Port = clusterOriginalPort; if (newOptions.Gateway.Port == ServerConstants.RandomPort) newOptions.Gateway.Port = gatewayOriginalPort; if (newOptions.LeafNode.Port == ServerConstants.RandomPort) newOptions.LeafNode.Port = leafOriginalPort; if (newOptions.Websocket.Port == ServerConstants.RandomPort) newOptions.Websocket.Port = websocketOriginalPort; if (newOptions.Mqtt.Port == ServerConstants.RandomPort) newOptions.Mqtt.Port = mqttOriginalPort; var reloadError = ReloadOptionsInternal(currentOptions, newOptions); if (reloadError != null) return reloadError; RecheckPinnedCerts(currentOptions, newOptions); _mu.EnterWriteLock(); try { _configTime = DateTime.UtcNow; } finally { _mu.ExitWriteLock(); } return null; } finally { _reloadMu.ExitWriteLock(); } } /// /// Applies immutable/runtime-only fields and executes diff + apply. /// Mirrors Go Server.reloadOptions(). /// internal Exception? ReloadOptionsInternal(ServerOptions currentOptions, ServerOptions newOptions) { ArgumentNullException.ThrowIfNull(currentOptions); ArgumentNullException.ThrowIfNull(newOptions); newOptions.CustomClientAuthentication = currentOptions.CustomClientAuthentication; newOptions.CustomRouterAuthentication = currentOptions.CustomRouterAuthentication; var (changed, diffError) = DiffOptions(newOptions); if (diffError != null) return diffError; if (changed.Count > 0) { var validateError = ValidateOptions(newOptions); if (validateError != null) return validateError; } SetOpts(newOptions); ApplyOptions(new ReloadContext(currentOptions.Cluster.Permissions), changed); return null; } /// /// Diffs current and new options and returns ordered reload actions. /// Mirrors Go Server.diffOptions(). /// internal (List Changed, Exception? Error) DiffOptions(ServerOptions newOptions) { ArgumentNullException.ThrowIfNull(newOptions); var oldOptions = GetOpts().Clone(); var diffOptions = new List(); var jsEnabled = oldOptions.JetStream; var disableJetStream = false; var jsMemLimitsChanged = false; var jsStoreLimitsChanged = false; var jsStoreDirChanged = false; foreach (var property in typeof(ServerOptions).GetProperties(BindingFlags.Instance | BindingFlags.Public)) { if (!property.CanRead) continue; var oldValue = property.GetValue(oldOptions); var newValue = property.GetValue(newOptions); var oldImposeError = ConfigReloader.ImposeOrder(oldValue); if (oldImposeError != null) return ([], oldImposeError); var newImposeError = ConfigReloader.ImposeOrder(newValue); if (newImposeError != null) return ([], newImposeError); var optionName = property.Name; var changed = optionName is not nameof(ServerOptions.Accounts) and not nameof(ServerOptions.Users) ? !OptionValueEquals(optionName, oldValue, newValue) : true; if (!changed) { if (optionName == nameof(ServerOptions.JetStream) && newValue is bool jsWanted && jsWanted && !jsEnabled) diffOptions.Add(new JetStreamReloadOption(true)); continue; } switch (optionName) { case nameof(ServerOptions.TraceVerbose): diffOptions.Add(new TraceVerboseReloadOption((bool)newValue!)); break; case nameof(ServerOptions.TraceHeaders): diffOptions.Add(new TraceHeadersReloadOption((bool)newValue!)); break; case nameof(ServerOptions.Trace): diffOptions.Add(new TraceReloadOption((bool)newValue!)); break; case nameof(ServerOptions.Debug): diffOptions.Add(new DebugReloadOption((bool)newValue!)); break; case nameof(ServerOptions.Logtime): diffOptions.Add(new LogtimeReloadOption((bool)newValue!)); break; case nameof(ServerOptions.LogtimeUtc): diffOptions.Add(new LogtimeUTCOption((bool)newValue!)); break; case nameof(ServerOptions.LogFile): diffOptions.Add(new LogfileOption((string)newValue!)); break; case nameof(ServerOptions.Syslog): diffOptions.Add(new SyslogReloadOption((bool)newValue!)); break; case nameof(ServerOptions.RemoteSyslog): diffOptions.Add(new RemoteSyslogReloadOption((string)newValue!)); break; case nameof(ServerOptions.TlsConfig): diffOptions.Add(new TlsReloadOption(newValue)); break; case nameof(ServerOptions.TlsTimeout): diffOptions.Add(new TlsTimeoutReloadOption((double)newValue!)); break; case nameof(ServerOptions.TlsPinnedCerts): diffOptions.Add(new TlsPinnedCertReloadOption(newValue)); break; case nameof(ServerOptions.TlsHandshakeFirst): diffOptions.Add(new TlsHandshakeFirstReloadOption((bool)newValue!)); break; case nameof(ServerOptions.TlsHandshakeFirstFallback): diffOptions.Add(new TlsHandshakeFirstFallbackReloadOption((TimeSpan)newValue!)); break; case nameof(ServerOptions.Username): diffOptions.Add(new UsernameReloadOption()); break; case nameof(ServerOptions.Password): diffOptions.Add(new PasswordReloadOption()); break; case nameof(ServerOptions.Tags): diffOptions.Add(new TagsReloadOption()); break; case nameof(ServerOptions.Metadata): diffOptions.Add(new MetadataReloadOption()); break; case nameof(ServerOptions.Authorization): diffOptions.Add(new AuthorizationReloadOption()); break; case nameof(ServerOptions.AuthTimeout): diffOptions.Add(new AuthTimeoutReloadOption((double)newValue!)); break; case nameof(ServerOptions.Users): diffOptions.Add(new UsersReloadOption()); break; case nameof(ServerOptions.Nkeys): diffOptions.Add(new NkeysReloadOption()); break; case nameof(ServerOptions.Cluster): { var oldCluster = (ClusterOpts)oldValue!; var newCluster = (ClusterOpts)newValue!; var clusterError = ConfigReloader.ValidateClusterOpts(oldCluster, newCluster); if (clusterError != null) return ([], clusterError); var clusterOption = new ClusterReloadOption( newCluster, permsChanged: !OptionValueEquals(nameof(ClusterOpts.Permissions), oldCluster.Permissions, newCluster.Permissions), poolSizeChanged: false, compressChanged: !CompressOptsEqual(oldCluster.Compression, newCluster.Compression), accsAdded: [], accsRemoved: []); clusterOption.DiffPoolAndAccounts(oldCluster); foreach (var accountName in clusterOption.AccountsAdded) { var (_, lookupError) = LookupAccount(accountName); if (lookupError != null) { return ([], new InvalidOperationException( $"unable to add account '{accountName}' to dedicated routes: {lookupError.Message}", lookupError)); } } diffOptions.Add(clusterOption); break; } case nameof(ServerOptions.Routes): { var oldRoutes = oldValue as IReadOnlyList ?? []; var newRoutes = newValue as IReadOnlyList ?? []; var (add, remove) = ConfigReloader.DiffRoutes(oldRoutes, newRoutes); diffOptions.Add(new RoutesReloadOption([.. add.Cast()], [.. remove.Cast()])); break; } case nameof(ServerOptions.MaxConn): diffOptions.Add(new MaxConnReloadOption((int)newValue!)); break; case nameof(ServerOptions.PidFile): diffOptions.Add(new PidFileReloadOption((string)newValue!)); break; case nameof(ServerOptions.PortsFileDir): diffOptions.Add(new PortsFileDirReloadOption((string)oldValue!, (string)newValue!)); break; case nameof(ServerOptions.MaxControlLine): diffOptions.Add(new MaxControlLineReloadOption((int)newValue!)); break; case nameof(ServerOptions.MaxPayload): diffOptions.Add(new MaxPayloadReloadOption((int)newValue!)); break; case nameof(ServerOptions.PingInterval): diffOptions.Add(new PingIntervalReloadOption((TimeSpan)newValue!)); break; case nameof(ServerOptions.MaxPingsOut): diffOptions.Add(new MaxPingsOutReloadOption((int)newValue!)); break; case nameof(ServerOptions.WriteDeadline): diffOptions.Add(new WriteDeadlineReloadOption((TimeSpan)newValue!)); break; case nameof(ServerOptions.ClientAdvertise): { var clientAdvertise = (string)newValue!; if (!string.IsNullOrEmpty(clientAdvertise)) { var (_, _, parseError) = ServerUtilities.ParseHostPort(clientAdvertise, 0); if (parseError != null) { return ([], new InvalidOperationException( $"invalid ClientAdvertise value '{clientAdvertise}': {parseError.Message}", parseError)); } } diffOptions.Add(new ClientAdvertiseReloadOption(clientAdvertise)); break; } case nameof(ServerOptions.Accounts): diffOptions.Add(new AccountsReloadOption()); break; case nameof(ServerOptions.AccountResolver): if ((oldValue is null) != (newValue is null)) return ([], new InvalidOperationException("config reload does not support moving to or from an account resolver")); diffOptions.Add(new AccountsReloadOption()); break; case nameof(ServerOptions.AccountResolverTlsConfig): diffOptions.Add(new AccountsReloadOption()); break; case nameof(ServerOptions.Gateway): { var oldGateway = (GatewayOpts)oldValue!; var newGateway = (GatewayOpts)newValue!; if (!GatewayEquivalentForReload(oldGateway, newGateway)) { return ([], new InvalidOperationException( $"config reload not supported for {optionName}: old={oldGateway}, new={newGateway}")); } break; } case nameof(ServerOptions.LeafNode): { var oldLeaf = (LeafNodeOpts)oldValue!; var newLeaf = (LeafNodeOpts)newValue!; var (equivalent, tlsFirstChanged, compressionChanged, disabledChanged) = LeafNodeReloadCompare(oldLeaf, newLeaf); if (!equivalent) { return ([], new InvalidOperationException( $"config reload not supported for {optionName}: old={oldLeaf}, new={newLeaf}")); } diffOptions.Add(new LeafNodeReloadOption(tlsFirstChanged, compressionChanged, disabledChanged)); break; } case nameof(ServerOptions.JetStream): { var newJsValue = (bool)newValue!; var oldJsValue = (bool)oldValue!; if (newJsValue != oldJsValue) diffOptions.Add(new JetStreamReloadOption(newJsValue)); disableJetStream = !newJsValue; break; } case nameof(ServerOptions.StoreDir): { var oldStoreDir = (string)oldValue!; var newStoreDir = (string)newValue!; if (jsEnabled && !string.Equals(oldStoreDir, newStoreDir, StringComparison.Ordinal)) { if (string.IsNullOrEmpty(newStoreDir)) jsStoreDirChanged = true; else return ([], new InvalidOperationException("config reload not supported for jetstream storage directory")); } break; } case nameof(ServerOptions.JetStreamMaxMemory): case nameof(ServerOptions.JetStreamMaxStore): { var oldLimit = (long)oldValue!; var newLimit = (long)newValue!; if (jsEnabled && oldLimit != newLimit) { var fromUnset = oldLimit == -1; var toUnset = newLimit == -1; if (!fromUnset && toUnset) { if (optionName == nameof(ServerOptions.JetStreamMaxMemory)) jsMemLimitsChanged = true; else jsStoreLimitsChanged = true; } else if (fromUnset && !toUnset) { return ([], new InvalidOperationException( "config reload not supported for jetstream dynamic max memory and store")); } else { return ([], new InvalidOperationException( "config reload not supported for jetstream max memory and store")); } } break; } case nameof(ServerOptions.Websocket): { var oldWebsocket = CloneByJson((WebsocketOpts)oldValue!); var newWebsocket = CloneByJson((WebsocketOpts)newValue!); oldWebsocket.TlsConfig = null; oldWebsocket.TlsConfigOpts = null; newWebsocket.TlsConfig = null; newWebsocket.TlsConfigOpts = null; if (!JsonEquivalent(oldWebsocket, newWebsocket)) { return ([], new InvalidOperationException( $"config reload not supported for {optionName}: old={oldValue}, new={newValue}")); } break; } case nameof(ServerOptions.Mqtt): { var oldMqtt = CloneByJson((MqttOpts)oldValue!); var newMqtt = CloneByJson((MqttOpts)newValue!); diffOptions.Add(new MqttAckWaitReloadOption(newMqtt.AckWait)); diffOptions.Add(new MqttMaxAckPendingReloadOption(newMqtt.MaxAckPending)); diffOptions.Add(new MqttStreamReplicasReloadOption(newMqtt.StreamReplicas)); diffOptions.Add(new MqttConsumerReplicasReloadOption(newMqtt.ConsumerReplicas)); diffOptions.Add(new MqttConsumerMemoryStorageReloadOption(newMqtt.ConsumerMemoryStorage)); diffOptions.Add(new MqttInactiveThresholdReloadOption(newMqtt.ConsumerInactiveThreshold)); oldMqtt.TlsConfig = null; oldMqtt.TlsConfigOpts = null; oldMqtt.AckWait = default; oldMqtt.MaxAckPending = default; oldMqtt.StreamReplicas = default; oldMqtt.ConsumerReplicas = default; oldMqtt.ConsumerMemoryStorage = default; oldMqtt.ConsumerInactiveThreshold = default; newMqtt.TlsConfig = null; newMqtt.TlsConfigOpts = null; newMqtt.AckWait = default; newMqtt.MaxAckPending = default; newMqtt.StreamReplicas = default; newMqtt.ConsumerReplicas = default; newMqtt.ConsumerMemoryStorage = default; newMqtt.ConsumerInactiveThreshold = default; if (!JsonEquivalent(oldMqtt, newMqtt)) { return ([], new InvalidOperationException( $"config reload not supported for {optionName}: old={oldValue}, new={newValue}")); } break; } case nameof(ServerOptions.ConnectErrorReports): diffOptions.Add(new ConnectErrorReportsReloadOption((int)newValue!)); break; case nameof(ServerOptions.ReconnectErrorReports): diffOptions.Add(new ReconnectErrorReportsReloadOption((int)newValue!)); break; case nameof(ServerOptions.NoLog): case nameof(ServerOptions.NoSigs): break; case nameof(ServerOptions.DisableShortFirstPing): newOptions.DisableShortFirstPing = (bool)oldValue!; break; case nameof(ServerOptions.MaxTracedMsgLen): diffOptions.Add(new MaxTracedMsgLenReloadOption((int)newValue!)); break; case nameof(ServerOptions.Port): if ((int)newValue! == 0) break; return ([], new InvalidOperationException( $"config reload not supported for {optionName}: old={oldValue}, new={newValue}")); case nameof(ServerOptions.NoAuthUser): { var oldNoAuth = (string)oldValue!; var newNoAuth = (string)newValue!; if (!string.IsNullOrEmpty(oldNoAuth) && string.IsNullOrEmpty(newNoAuth)) { var matchesUser = newOptions.Users?.Any(u => string.Equals(u.Username, oldNoAuth, StringComparison.Ordinal)) == true; if (matchesUser) { return ([], new InvalidOperationException( $"config reload not supported for {optionName}: old={oldValue}, new={newValue}")); } } else { return ([], new InvalidOperationException( $"config reload not supported for {optionName}: old={oldValue}, new={newValue}")); } break; } case nameof(ServerOptions.DefaultSentinel): diffOptions.Add(new DefaultSentinelReloadOption((string)newValue!)); break; case nameof(ServerOptions.SystemAccount): { var oldSystem = (string)oldValue!; var newSystem = (string)newValue!; if (!string.Equals(oldSystem, ServerConstants.DefaultSystemAccount, StringComparison.Ordinal) || !string.IsNullOrEmpty(newSystem)) { return ([], new InvalidOperationException( $"config reload not supported for {optionName}: old={oldValue}, new={newValue}")); } break; } case nameof(ServerOptions.OcspConfig): diffOptions.Add(new OcspReloadOption(newValue)); break; case nameof(ServerOptions.OcspCacheConfig): diffOptions.Add(new OcspResponseCacheReloadOption(newValue)); break; case nameof(ServerOptions.ProfBlockRate): diffOptions.Add(new ProfBlockRateReloadOption((int)newValue!)); break; case nameof(ServerOptions.ConfigDigestValue): break; case nameof(ServerOptions.NoFastProducerStall): diffOptions.Add(new NoFastProducerStallReloadOption((bool)newValue!)); break; case nameof(ServerOptions.Proxies): { var oldProxies = oldValue as ProxiesConfig; var newProxies = newValue as ProxiesConfig; var (add, del) = ConfigReloader.DiffProxiesTrustedKeys(oldProxies?.Trusted, newProxies?.Trusted); if (add.Count > 0 || del.Count > 0) diffOptions.Add(new ProxiesReloadOption([.. add], [.. del])); break; } default: return ([], new InvalidOperationException( $"config reload not supported for {optionName}: old={oldValue}, new={newValue}")); } } if (!disableJetStream) { if (jsMemLimitsChanged || jsStoreLimitsChanged) return ([], new InvalidOperationException( "config reload not supported for jetstream max memory and max store")); if (jsStoreDirChanged) return ([], new InvalidOperationException( "config reload not supported for jetstream storage dir")); } return (diffOptions, null); } /// /// Applies reload options and executes post-processing hooks. /// Mirrors Go Server.applyOptions(). /// internal void ApplyOptions(ReloadContext context, IReadOnlyList options) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(options); var reloadLogging = false; var reloadAuthorization = false; var reloadClusterPermissions = false; var reloadClientTraceLevel = false; var statsChanged = false; var jetStreamChanged = false; var jetStreamEnabled = false; ClusterReloadOption? clusterOption = null; foreach (var option in options) { option.Apply(this); if (option.IsLoggingChange()) reloadLogging = true; if (option.IsTraceLevelChange()) reloadClientTraceLevel = true; if (option.IsAuthChange()) reloadAuthorization = true; if (option.IsClusterPermsChange()) reloadClusterPermissions = true; if (option.IsClusterPoolSizeOrAccountsChange() && option is ClusterReloadOption co) clusterOption = co; if (option.IsStatszChange()) statsChanged = true; if (option.IsJetStreamChange() && option is JetStreamReloadOption js) { jetStreamChanged = true; jetStreamEnabled = js.Value; } } if (reloadLogging) ConfigureLogger(); if (reloadClientTraceLevel) ReloadClientTraceLevel(); if (reloadAuthorization) ReloadAuthorization(); if (reloadClusterPermissions) ReloadClusterPermissions(context.OldClusterPermissions); var updatedOptions = GetOpts(); if (clusterOption != null) ReloadClusterPoolAndAccounts(clusterOption, updatedOptions); if (jetStreamChanged && !jetStreamEnabled) Noticef("Reloaded: JetStream disabled"); if (statsChanged) Noticef("Reloaded: stats-related settings updated"); var digest = updatedOptions.ConfigDigest(); if (string.IsNullOrEmpty(digest)) Noticef("Reloaded server configuration"); else Noticef("Reloaded server configuration ({0})", digest); } /// /// Disconnects clients that no longer satisfy pinned-certificate policy. /// Mirrors Go Server.recheckPinnedCerts(). /// internal void RecheckPinnedCerts(ServerOptions currentOptions, ServerOptions newOptions) { ArgumentNullException.ThrowIfNull(currentOptions); ArgumentNullException.ThrowIfNull(newOptions); var disconnectClients = new List(); var protocolPinnedSets = new Dictionary(); if (!PinnedCertSetEquals(newOptions.TlsPinnedCerts, currentOptions.TlsPinnedCerts)) protocolPinnedSets[ClientConnectionType.Nats] = newOptions.TlsPinnedCerts; if (!PinnedCertSetEquals(newOptions.Mqtt.TlsPinnedCerts, currentOptions.Mqtt.TlsPinnedCerts)) protocolPinnedSets[ClientConnectionType.Mqtt] = newOptions.Mqtt.TlsPinnedCerts; if (!PinnedCertSetEquals(newOptions.Websocket.TlsPinnedCerts, currentOptions.Websocket.TlsPinnedCerts)) protocolPinnedSets[ClientConnectionType.WebSocket] = newOptions.Websocket.TlsPinnedCerts; _mu.EnterReadLock(); try { foreach (var client in _clients.Values) { if (client.Kind != ClientKind.Client) continue; if (protocolPinnedSets.TryGetValue(client.ClientType(), out var pinned) && !MatchesPinnedCert(client, pinned)) { disconnectClients.Add(client); } } if (!PinnedCertSetEquals(newOptions.LeafNode.TlsPinnedCerts, currentOptions.LeafNode.TlsPinnedCerts)) { foreach (var leaf in _leafs.Values) { if (leaf.Kind == ClientKind.Leaf && !MatchesPinnedCert(leaf, newOptions.LeafNode.TlsPinnedCerts)) disconnectClients.Add(leaf); } } if (!PinnedCertSetEquals(newOptions.Cluster.TlsPinnedCerts, currentOptions.Cluster.TlsPinnedCerts)) { ForEachRoute(route => { if (!MatchesPinnedCert(route, newOptions.Cluster.TlsPinnedCerts)) disconnectClients.Add(route); }); } } finally { _mu.ExitReadLock(); } if (_gateway.Enabled && !PinnedCertSetEquals(newOptions.Gateway.TlsPinnedCerts, currentOptions.Gateway.TlsPinnedCerts)) { _gateway.AcquireReadLock(); try { foreach (var outbound in _gateway.Out.Values) { if (!MatchesPinnedCert(outbound, newOptions.Gateway.TlsPinnedCerts)) disconnectClients.Add(outbound); } foreach (var inbound in _gateway.In.Values) { if (inbound.Kind == ClientKind.Gateway && !MatchesPinnedCert(inbound, newOptions.Gateway.TlsPinnedCerts)) { disconnectClients.Add(inbound); } } } finally { _gateway.ReleaseReadLock(); } } var unique = disconnectClients .Where(static c => c != null) .DistinctBy(static c => c.Cid) .ToList(); if (unique.Count == 0) return; Noticef("Disconnect {0} clients due to pinned certs reload", unique.Count); foreach (var client in unique) client.CloseConnection(ClosedState.TlsHandshakeError); } /// /// Signals the internal event-send loop to refresh state. /// Mirrors Go Server.resetInternalLoopInfo(). /// internal void ResetInternalLoopInfo() { System.Threading.Channels.Channel? resetChannel; _mu.EnterReadLock(); try { resetChannel = _sys?.ResetChannel; } finally { _mu.ExitReadLock(); } if (resetChannel != null) resetChannel.Writer.TryWrite(true); } /// /// Recomputes per-client trace flags after logging-level changes. /// Mirrors Go Server.reloadClientTraceLevel(). /// internal void ReloadClientTraceLevel() { var opts = GetOpts(); if (opts.NoLog) return; var clients = new List(); _mu.EnterReadLock(); try { clients.AddRange(_clients.Values); clients.AddRange(_leafs.Values); ForEachRoute(route => clients.Add(route)); } finally { _mu.ExitReadLock(); } lock (_grMu) { clients.AddRange(_grTmpClients.Values); } _gateway.AcquireReadLock(); try { clients.AddRange(_gateway.In.Values); clients.AddRange(_gateway.Outo); } finally { _gateway.ReleaseReadLock(); } foreach (var client in clients.DistinctBy(static c => c.Cid)) client.SetTraceLevel(); } /// /// Reloads authorization state and disconnects no-longer-authorized clients. /// Mirrors Go Server.reloadAuthorization(). /// internal void ReloadAuthorization() { var opts = GetOpts(); var deletedAccounts = new List(); var movedClients = new List(); var clientsToCheck = new List(); var routes = new List(); System.Threading.Channels.Channel? resetChannel = null; _mu.EnterWriteLock(); try { if (_trustedKeys == null) { var configuredAccounts = new HashSet( opts.Accounts.Select(static a => a.GetName()), StringComparer.Ordinal); foreach (var kvp in _accounts.ToArray()) { var accountName = kvp.Key; var account = kvp.Value; if (accountName == ServerConstants.DefaultGlobalAccount || accountName == ServerConstants.DefaultSystemAccount) { continue; } if (!configuredAccounts.Contains(accountName)) { deletedAccounts.Add(account); _accounts.TryRemove(accountName, out _); } } var (_, configureError) = ConfigureAccounts(true); if (configureError != null) Errorf("reloadAuthorization: configureAccounts failed: {0}", configureError.Message); ConfigureAuthorization(); } else if (opts.AccountResolver != null) { var resolverError = ConfigureResolver(); if (resolverError != null) Errorf("reloadAuthorization: configureResolver failed: {0}", resolverError.Message); if (_accResolver is MemoryAccountResolver) CheckResolvePreloads(); } foreach (var client in _clients.Values) { if (ClientHasMovedToDifferentAccount(client)) movedClients.Add(client); else clientsToCheck.Add(client); } ForEachRoute(route => routes.Add(route)); if (_sys?.Account != null && !opts.NoSystemAccount) _accounts[_sys.Account.Name] = _sys.Account; foreach (var account in _accounts.Values) { foreach (var client in account.GetClients()) { if (client.Kind != ClientKind.Client && client.Kind != ClientKind.Leaf) clientsToCheck.Add(client); } } resetChannel = _sys?.ResetChannel; } finally { _mu.ExitWriteLock(); } foreach (var account in deletedAccounts) account.ClearEventing(); if (resetChannel != null) resetChannel.Writer.TryWrite(true); foreach (var client in movedClients.DistinctBy(static c => c.Cid)) client.CloseConnection(ClosedState.ClientClosed); foreach (var client in clientsToCheck.DistinctBy(static c => c.Cid)) { if ((client.Kind == ClientKind.Client || client.Kind == ClientKind.Leaf) && !IsClientAuthorized(client)) { client.AuthViolation(); continue; } if (client.Kind == ClientKind.Router && !IsRouterAuthorized(client)) { client.SetNoReconnect(); client.AuthViolation(); } } foreach (var route in routes.DistinctBy(static c => c.Cid)) { if (!IsRouterAuthorized(route)) { route.SetNoReconnect(); route.AuthViolation(); } } AccountResolver()?.Reload(); } /// /// Returns true if a client's configured user/nkey now maps to a different account. /// Mirrors Go Server.clientHasMovedToDifferentAccount(). /// internal bool ClientHasMovedToDifferentAccount(ClientConnection connection) { ArgumentNullException.ThrowIfNull(connection); NkeyUser? newNkeyUser = null; User? newUser = null; var nkey = connection.GetNkey(); if (!string.IsNullOrEmpty(nkey)) { if (_nkeys != null) _nkeys.TryGetValue(nkey, out newNkeyUser); } else { var username = connection.GetUsername(); if (!string.IsNullOrEmpty(username) && _users != null) _users.TryGetValue(username, out newUser); else if (string.IsNullOrEmpty(username)) return false; } var currentAccountName = connection.GetAccount()?.Name ?? string.Empty; if (newNkeyUser?.Account != null) return !string.Equals(currentAccountName, newNkeyUser.Account.Name, StringComparison.Ordinal); if (newUser?.Account != null) return !string.Equals(currentAccountName, newUser.Account.Name, StringComparison.Ordinal); // User or nkey no longer exists in current config. return true; } /// /// Reloads cluster route permissions and propagates updated INFO to routes. /// Mirrors Go Server.reloadClusterPermissions(). /// internal void ReloadClusterPermissions(RoutePermissions? oldPermissions) { _ = oldPermissions; RoutePermissions? newPermissions; List routes = []; byte[] infoJson; _mu.EnterWriteLock(); try { newPermissions = GetOpts().Cluster.Permissions; _routeInfo.Import = newPermissions?.Import; _routeInfo.Export = newPermissions?.Export; infoJson = GenerateInfoJson(_routeInfo); ForEachRoute(route => routes.Add(route)); } finally { _mu.ExitWriteLock(); } foreach (var route in routes.DistinctBy(static c => c.Cid)) { var protocol = route.GetOpts().Protocol; if (protocol < ServerProtocol.RouteProtoInfo) { route.CloseConnection(ClosedState.RouteRemoved); continue; } route.EnqueueProto(infoJson); } } /// /// Reloads route-pool size and pinned-account route assignments. /// Mirrors Go Server.reloadClusterPoolAndAccounts(). /// internal void ReloadClusterPoolAndAccounts(ClusterReloadOption clusterOption, ServerOptions options) { ArgumentNullException.ThrowIfNull(clusterOption); ArgumentNullException.ThrowIfNull(options); var routesToClose = new HashSet(); _mu.EnterWriteLock(); try { _routesReject = true; if (clusterOption.AccountsAdded.Count > 0) { _accRoutes ??= new Dictionary>(StringComparer.Ordinal); foreach (var accountName in clusterOption.AccountsAdded) { if (!_accRoutes.ContainsKey(accountName)) _accRoutes[accountName] = new Dictionary(StringComparer.Ordinal); } } if (clusterOption.AccountsRemoved.Count > 0 && _accRoutes != null) { foreach (var accountName in clusterOption.AccountsRemoved) { if (_accRoutes.TryGetValue(accountName, out var remoteRoutes)) { foreach (var route in remoteRoutes.Values) { route.SetNoReconnect(); routesToClose.Add(route); } } } } if (clusterOption.PoolSizeChanged) { ForEachRoute(route => { route.SetNoReconnect(); routesToClose.Add(route); }); } } finally { _mu.ExitWriteLock(); } foreach (var route in routesToClose) route.CloseConnection(ClosedState.RouteRemoved); _mu.EnterWriteLock(); try { _accAddedReqId = string.Empty; _accAddedCh = null; if (_accRoutes != null) { foreach (var accountName in clusterOption.AccountsRemoved) _accRoutes.Remove(accountName); } if (options.Cluster.PinnedAccounts.Count == 0) _accRoutes = null; if (options.Cluster.PoolSize > 0) { _routesPoolSize = options.Cluster.PoolSize; _routeInfo.RoutePoolSize = options.Cluster.PoolSize; } else { _routesPoolSize = 1; _routeInfo.RoutePoolSize = 0; } if (clusterOption.PoolSizeChanged || clusterOption.AccountsRemoved.Count > 0) { foreach (var account in _accounts.Values) SetRouteInfo(account); } _routesReject = false; } finally { _mu.ExitWriteLock(); } } private static bool OptionValueEquals(string optionName, object? oldValue, object? newValue) { if (ReferenceEquals(oldValue, newValue)) return true; if (oldValue is null || newValue is null) return false; switch (oldValue) { case string oldString when newValue is string newString: return string.Equals(oldString, newString, StringComparison.Ordinal); case bool oldBool when newValue is bool newBool: return oldBool == newBool; case int oldInt when newValue is int newInt: return oldInt == newInt; case long oldLong when newValue is long newLong: return oldLong == newLong; case ulong oldUlong when newValue is ulong newUlong: return oldUlong == newUlong; case double oldDouble when newValue is double newDouble: return oldDouble.Equals(newDouble); case TimeSpan oldTimeSpan when newValue is TimeSpan newTimeSpan: return oldTimeSpan == newTimeSpan; case List oldStrings when newValue is List newStrings: return oldStrings.SequenceEqual(newStrings, StringComparer.Ordinal); case Dictionary oldMap when newValue is Dictionary newMap: return oldMap.Count == newMap.Count && oldMap.All(entry => newMap.TryGetValue(entry.Key, out var value) && string.Equals(value, entry.Value, StringComparison.Ordinal)); case List oldUris when newValue is List newUris: return oldUris.Count == newUris.Count && oldUris.Zip(newUris, static (oldUri, newUri) => ServerUtilities.UrlsAreEqual(oldUri, newUri)) .All(static same => same); case PinnedCertSet oldPinned when newValue is PinnedCertSet newPinned: return PinnedCertSetEquals(oldPinned, newPinned); case CompressionOpts oldCompression when newValue is CompressionOpts newCompression: return CompressOptsEqual(oldCompression, newCompression); case GatewayOpts oldGateway when newValue is GatewayOpts newGateway: return GatewayEquivalentForReload(oldGateway, newGateway); case LeafNodeOpts oldLeaf when newValue is LeafNodeOpts newLeaf: return LeafNodeReloadCompare(oldLeaf, newLeaf).Equivalent; case ClusterOpts oldCluster when newValue is ClusterOpts newCluster: return JsonEquivalent(oldCluster, newCluster); case WebsocketOpts oldWebsocket when newValue is WebsocketOpts newWebsocket: return JsonEquivalent(oldWebsocket, newWebsocket); case MqttOpts oldMqtt when newValue is MqttOpts newMqtt: return JsonEquivalent(oldMqtt, newMqtt); default: return JsonEquivalent(oldValue, newValue); } } private static bool JsonEquivalent(object left, object right) { try { var leftJson = JsonSerializer.Serialize(left); var rightJson = JsonSerializer.Serialize(right); return string.Equals(leftJson, rightJson, StringComparison.Ordinal); } catch { return Equals(left, right); } } private static T CloneByJson(T value) { var json = JsonSerializer.Serialize(value); var clone = JsonSerializer.Deserialize(json); return clone ?? throw new InvalidOperationException($"Failed to clone {typeof(T).Name} for reload comparison."); } private static bool GatewayEquivalentForReload(GatewayOpts oldValue, GatewayOpts newValue) { var oldGateway = CloneByJson(oldValue); var newGateway = CloneByJson(newValue); oldGateway.TlsConfig = null; oldGateway.TlsConfigOpts = null; newGateway.TlsConfig = null; newGateway.TlsConfigOpts = null; oldGateway.Gateways = ConfigReloader.CopyRemoteGWConfigsWithoutTLSConfig(oldGateway.Gateways) ?? []; newGateway.Gateways = ConfigReloader.CopyRemoteGWConfigsWithoutTLSConfig(newGateway.Gateways) ?? []; return JsonEquivalent(oldGateway, newGateway); } private static (bool Equivalent, bool TlsFirstChanged, bool CompressionChanged, bool DisabledChanged) LeafNodeReloadCompare(LeafNodeOpts oldValue, LeafNodeOpts newValue) { var oldLeaf = CloneByJson(oldValue); var newLeaf = CloneByJson(newValue); oldLeaf.TlsConfig = null; oldLeaf.TlsConfigOpts = null; newLeaf.TlsConfig = null; newLeaf.TlsConfigOpts = null; var tlsFirstChanged = oldLeaf.TlsHandshakeFirst != newLeaf.TlsHandshakeFirst || oldLeaf.TlsHandshakeFirstFallback != newLeaf.TlsHandshakeFirstFallback; if (tlsFirstChanged) { oldLeaf.TlsHandshakeFirst = false; newLeaf.TlsHandshakeFirst = false; oldLeaf.TlsHandshakeFirstFallback = default; newLeaf.TlsHandshakeFirstFallback = default; } else if (oldLeaf.Remotes.Count == newLeaf.Remotes.Count) { for (var i = 0; i < oldLeaf.Remotes.Count; i++) { if (oldLeaf.Remotes[i].TlsHandshakeFirst != newLeaf.Remotes[i].TlsHandshakeFirst) { tlsFirstChanged = true; break; } } } var compressionChanged = !CompressOptsEqual(oldLeaf.Compression, newLeaf.Compression); if (compressionChanged) { oldLeaf.Compression = new CompressionOpts(); newLeaf.Compression = new CompressionOpts(); } else if (oldLeaf.Remotes.Count == newLeaf.Remotes.Count) { for (var i = 0; i < oldLeaf.Remotes.Count; i++) { if (!CompressOptsEqual(oldLeaf.Remotes[i].Compression, newLeaf.Remotes[i].Compression)) { compressionChanged = true; break; } } } var disabledChanged = false; if (oldLeaf.Remotes.Count == newLeaf.Remotes.Count) { for (var i = 0; i < oldLeaf.Remotes.Count; i++) { if (oldLeaf.Remotes[i].Disabled != newLeaf.Remotes[i].Disabled) { disabledChanged = true; break; } } } oldLeaf.Remotes = ConfigReloader.CopyRemoteLNConfigForReloadCompare(oldLeaf.Remotes) ?? []; newLeaf.Remotes = ConfigReloader.CopyRemoteLNConfigForReloadCompare(newLeaf.Remotes) ?? []; if (LeafRemotesEquivalent(oldLeaf.Remotes, newLeaf.Remotes)) { oldLeaf.Remotes = []; newLeaf.Remotes = []; } if (LeafUsersEquivalent(oldLeaf.Users, newLeaf.Users)) { oldLeaf.Users = null; newLeaf.Users = null; } return (JsonEquivalent(oldLeaf, newLeaf), tlsFirstChanged, compressionChanged, disabledChanged); } private static bool LeafUsersEquivalent(List? oldUsers, List? newUsers) { if (oldUsers == null && newUsers == null) return true; if (oldUsers == null || newUsers == null) return false; if (oldUsers.Count != newUsers.Count) return false; var oldByUsername = oldUsers.ToDictionary(static user => user.Username, StringComparer.Ordinal); var newByUsername = newUsers.ToDictionary(static user => user.Username, StringComparer.Ordinal); foreach (var (username, oldUser) in oldByUsername) { if (!newByUsername.TryGetValue(username, out var newUser)) return false; var oldAccountName = oldUser.Account?.Name ?? string.Empty; var newAccountName = newUser.Account?.Name ?? string.Empty; if (!string.Equals(oldUser.Password, newUser.Password, StringComparison.Ordinal) || !string.Equals(oldAccountName, newAccountName, StringComparison.Ordinal)) { return false; } } return true; } private static bool LeafRemotesEquivalent(IReadOnlyList oldRemotes, IReadOnlyList newRemotes) { if (oldRemotes.Count != newRemotes.Count) return false; var remaining = new List(newRemotes.Count); foreach (var remote in newRemotes) remaining.Add(NormalizeLeafRemoteForCompare(remote)); foreach (var oldRemote in oldRemotes) { var normalizedOld = NormalizeLeafRemoteForCompare(oldRemote); var matchIndex = remaining.FindIndex(candidate => JsonEquivalent(normalizedOld, candidate)); if (matchIndex < 0) return false; remaining.RemoveAt(matchIndex); } return remaining.Count == 0; } private static RemoteLeafOpts NormalizeLeafRemoteForCompare(RemoteLeafOpts remote) { var normalized = CloneByJson(remote); if (string.IsNullOrEmpty(normalized.LocalAccount)) normalized.LocalAccount = ServerConstants.DefaultGlobalAccount; return normalized; } private static bool PinnedCertSetEquals(PinnedCertSet? left, PinnedCertSet? right) { if (ReferenceEquals(left, right)) return true; if (left is null || right is null) return false; return left.SetEquals(right); } private static bool MatchesPinnedCert(ClientConnection client, PinnedCertSet? pinnedCerts) { return client.MatchesPinnedCert(pinnedCerts); } } internal sealed class ReloadContext { public ReloadContext(RoutePermissions? oldClusterPermissions) { OldClusterPermissions = oldClusterPermissions; } public RoutePermissions? OldClusterPermissions { get; } }