diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptionTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptionTypes.cs
new file mode 100644
index 0000000..713b43c
--- /dev/null
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptionTypes.cs
@@ -0,0 +1,477 @@
+// Copyright 2012-2025 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/opts.go in the NATS server Go source.
+
+using System.Net.Security;
+using System.Security.Authentication;
+using System.Security.Cryptography.X509Certificates;
+
+namespace ZB.MOM.NatsNet.Server;
+
+///
+/// Write timeout behavior policy.
+/// Mirrors WriteTimeoutPolicy in opts.go.
+///
+public enum WriteTimeoutPolicy : byte
+{
+ Default = 0,
+ Close = 1,
+ Retry = 2,
+}
+
+///
+/// Store encryption cipher selection.
+/// Mirrors StoreCipher in opts.go.
+///
+public enum StoreCipher
+{
+ ChaCha = 0,
+ Aes = 1,
+ NoCipher = 2,
+}
+
+///
+/// OCSP stapling mode.
+/// Mirrors OCSPMode in opts.go.
+///
+public enum OcspMode : byte
+{
+ Auto = 0,
+ Always = 1,
+ Never = 2,
+ Must = 3,
+}
+
+///
+/// Set of pinned certificate SHA256 hashes (lowercase hex-encoded DER SubjectPublicKeyInfo).
+/// Mirrors PinnedCertSet in opts.go.
+///
+public class PinnedCertSet : HashSet
+{
+ public PinnedCertSet() : base(StringComparer.OrdinalIgnoreCase) { }
+ public PinnedCertSet(IEnumerable collection) : base(collection, StringComparer.OrdinalIgnoreCase) { }
+}
+
+///
+/// Compression options for route/leaf connections.
+/// Mirrors CompressionOpts in opts.go.
+///
+public class CompressionOpts
+{
+ public string Mode { get; set; } = string.Empty;
+ public List RttThresholds { get; set; } = [];
+}
+
+///
+/// Compression mode string constants.
+///
+public static class CompressionModes
+{
+ public const string Off = "off";
+ public const string Accept = "accept";
+ public const string S2Fast = "s2_fast";
+ public const string S2Better = "s2_better";
+ public const string S2Best = "s2_best";
+ public const string S2Auto = "s2_auto";
+}
+
+///
+/// TLS configuration parsed from config file.
+/// Mirrors TLSConfigOpts in opts.go.
+///
+public class TlsConfigOpts
+{
+ public string CertFile { get; set; } = string.Empty;
+ public string KeyFile { get; set; } = string.Empty;
+ public string CaFile { get; set; } = string.Empty;
+ public bool Verify { get; set; }
+ public bool Insecure { get; set; }
+ public bool Map { get; set; }
+ public bool TlsCheckKnownUrls { get; set; }
+ public bool HandshakeFirst { get; set; }
+ public TimeSpan FallbackDelay { get; set; }
+ public double Timeout { get; set; }
+ public long RateLimit { get; set; }
+ public bool AllowInsecureCiphers { get; set; }
+ public List CurvePreferences { get; set; } = [];
+ public PinnedCertSet? PinnedCerts { get; set; }
+ public string CertMatch { get; set; } = string.Empty;
+ public bool CertMatchSkipInvalid { get; set; }
+ public List CaCertsMatch { get; set; } = [];
+ public List Certificates { get; set; } = [];
+ public SslProtocols MinVersion { get; set; }
+}
+
+///
+/// Certificate and key file pair.
+/// Mirrors TLSCertPairOpt in opts.go.
+///
+public class TlsCertPairOpt
+{
+ public string CertFile { get; set; } = string.Empty;
+ public string KeyFile { get; set; } = string.Empty;
+}
+
+///
+/// OCSP stapling configuration.
+/// Mirrors OCSPConfig in opts.go.
+///
+public class OcspConfig
+{
+ public OcspMode Mode { get; set; }
+ public List OverrideUrls { get; set; } = [];
+}
+
+///
+/// OCSP response cache configuration.
+/// Mirrors OCSPResponseCacheConfig in opts.go.
+///
+public class OcspResponseCacheConfig
+{
+ public string Type { get; set; } = string.Empty;
+ public string LocalStore { get; set; } = string.Empty;
+ public bool PreserveRevoked { get; set; }
+ public double SaveInterval { get; set; }
+}
+
+///
+/// Cluster configuration options.
+/// Mirrors ClusterOpts in opts.go.
+///
+public class ClusterOpts
+{
+ public string Name { get; set; } = string.Empty;
+ public string Host { get; set; } = string.Empty;
+ public int Port { get; set; }
+ public string Username { get; set; } = string.Empty;
+ public string Password { get; set; } = string.Empty;
+ public double AuthTimeout { get; set; }
+ public double TlsTimeout { get; set; }
+ public SslServerAuthenticationOptions? TlsConfig { get; set; }
+ public bool TlsMap { get; set; }
+ public bool TlsCheckKnownUrls { get; set; }
+ public PinnedCertSet? TlsPinnedCerts { get; set; }
+ public bool TlsHandshakeFirst { get; set; }
+ public TimeSpan TlsHandshakeFirstFallback { get; set; }
+ public string ListenStr { get; set; } = string.Empty;
+ public string Advertise { get; set; } = string.Empty;
+ public bool NoAdvertise { get; set; }
+ public int ConnectRetries { get; set; }
+ public bool ConnectBackoff { get; set; }
+ public int PoolSize { get; set; }
+ public List PinnedAccounts { get; set; } = [];
+ public CompressionOpts Compression { get; set; } = new();
+ public TimeSpan PingInterval { get; set; }
+ public int MaxPingsOut { get; set; }
+ public TimeSpan WriteDeadline { get; set; }
+ public WriteTimeoutPolicy WriteTimeout { get; set; }
+ internal TlsConfigOpts? TlsConfigOpts { get; set; }
+}
+
+///
+/// Gateway configuration options.
+/// Mirrors GatewayOpts in opts.go.
+///
+public class GatewayOpts
+{
+ public string Name { get; set; } = string.Empty;
+ public string Host { get; set; } = string.Empty;
+ public int Port { get; set; }
+ public string Username { get; set; } = string.Empty;
+ public string Password { get; set; } = string.Empty;
+ public double AuthTimeout { get; set; }
+ public SslServerAuthenticationOptions? TlsConfig { get; set; }
+ public double TlsTimeout { get; set; }
+ public bool TlsMap { get; set; }
+ public bool TlsCheckKnownUrls { get; set; }
+ public PinnedCertSet? TlsPinnedCerts { get; set; }
+ public string Advertise { get; set; } = string.Empty;
+ public int ConnectRetries { get; set; }
+ public bool ConnectBackoff { get; set; }
+ public List Gateways { get; set; } = [];
+ public bool RejectUnknown { get; set; }
+ public TimeSpan WriteDeadline { get; set; }
+ public WriteTimeoutPolicy WriteTimeout { get; set; }
+ internal TlsConfigOpts? TlsConfigOpts { get; set; }
+}
+
+///
+/// Remote gateway connection options.
+/// Mirrors RemoteGatewayOpts in opts.go.
+///
+public class RemoteGatewayOpts
+{
+ public string Name { get; set; } = string.Empty;
+ public SslServerAuthenticationOptions? TlsConfig { get; set; }
+ public double TlsTimeout { get; set; }
+ public List Urls { get; set; } = [];
+ internal TlsConfigOpts? TlsConfigOpts { get; set; }
+}
+
+///
+/// Leaf node configuration options.
+/// Mirrors LeafNodeOpts in opts.go.
+///
+public class LeafNodeOpts
+{
+ public string Host { get; set; } = string.Empty;
+ public int Port { get; set; }
+ public string Username { get; set; } = string.Empty;
+ public string Password { get; set; } = string.Empty;
+ public bool ProxyRequired { get; set; }
+ public string Nkey { get; set; } = string.Empty;
+ public string Account { get; set; } = string.Empty;
+ public double AuthTimeout { get; set; }
+ public SslServerAuthenticationOptions? TlsConfig { get; set; }
+ public double TlsTimeout { get; set; }
+ public bool TlsMap { get; set; }
+ public PinnedCertSet? TlsPinnedCerts { get; set; }
+ public bool TlsHandshakeFirst { get; set; }
+ public TimeSpan TlsHandshakeFirstFallback { get; set; }
+ public string Advertise { get; set; } = string.Empty;
+ public bool NoAdvertise { get; set; }
+ public TimeSpan ReconnectInterval { get; set; }
+ public TimeSpan WriteDeadline { get; set; }
+ public WriteTimeoutPolicy WriteTimeout { get; set; }
+ public CompressionOpts Compression { get; set; } = new();
+ public List Remotes { get; set; } = [];
+ public string MinVersion { get; set; } = string.Empty;
+ public bool IsolateLeafnodeInterest { get; set; }
+ internal TlsConfigOpts? TlsConfigOpts { get; set; }
+}
+
+///
+/// Signature handler delegate for NKey authentication.
+/// Mirrors SignatureHandler in opts.go.
+///
+public delegate (string jwt, byte[] signature, Exception? err) SignatureHandler(byte[] nonce);
+
+///
+/// Options for connecting to a remote leaf node.
+/// Mirrors RemoteLeafOpts in opts.go.
+///
+public class RemoteLeafOpts
+{
+ public string LocalAccount { get; set; } = string.Empty;
+ public bool NoRandomize { get; set; }
+ public List Urls { get; set; } = [];
+ public string Credentials { get; set; } = string.Empty;
+ public string Nkey { get; set; } = string.Empty;
+ public SignatureHandler? SignatureCb { get; set; }
+ public bool Tls { get; set; }
+ public SslServerAuthenticationOptions? TlsConfig { get; set; }
+ public double TlsTimeout { get; set; }
+ public bool TlsHandshakeFirst { get; set; }
+ public bool Hub { get; set; }
+ public List DenyImports { get; set; } = [];
+ public List DenyExports { get; set; } = [];
+ public TimeSpan FirstInfoTimeout { get; set; }
+ public CompressionOpts Compression { get; set; } = new();
+ public RemoteLeafWebsocketOpts Websocket { get; set; } = new();
+ public RemoteLeafProxyOpts Proxy { get; set; } = new();
+ public bool JetStreamClusterMigrate { get; set; }
+ public TimeSpan JetStreamClusterMigrateDelay { get; set; }
+ public bool LocalIsolation { get; set; }
+ public bool RequestIsolation { get; set; }
+ public bool Disabled { get; set; }
+ internal TlsConfigOpts? TlsConfigOpts { get; set; }
+}
+
+/// WebSocket sub-options for a remote leaf connection.
+public class RemoteLeafWebsocketOpts
+{
+ public bool Compression { get; set; }
+ public bool NoMasking { get; set; }
+}
+
+/// HTTP proxy sub-options for a remote leaf connection.
+public class RemoteLeafProxyOpts
+{
+ public string Url { get; set; } = string.Empty;
+ public string Username { get; set; } = string.Empty;
+ public string Password { get; set; } = string.Empty;
+ public TimeSpan Timeout { get; set; }
+}
+
+///
+/// WebSocket configuration options.
+/// Mirrors WebsocketOpts in opts.go.
+///
+public class WebsocketOpts
+{
+ public string Host { get; set; } = string.Empty;
+ public int Port { get; set; }
+ public string Advertise { get; set; } = string.Empty;
+ public string NoAuthUser { get; set; } = string.Empty;
+ public string JwtCookie { get; set; } = string.Empty;
+ public string UsernameCookie { get; set; } = string.Empty;
+ public string PasswordCookie { get; set; } = string.Empty;
+ public string TokenCookie { get; set; } = string.Empty;
+ public string Username { get; set; } = string.Empty;
+ public string Password { get; set; } = string.Empty;
+ public string Token { get; set; } = string.Empty;
+ public double AuthTimeout { get; set; }
+ public bool NoTls { get; set; }
+ public SslServerAuthenticationOptions? TlsConfig { get; set; }
+ public bool TlsMap { get; set; }
+ public PinnedCertSet? TlsPinnedCerts { get; set; }
+ public bool SameOrigin { get; set; }
+ public List AllowedOrigins { get; set; } = [];
+ public bool Compression { get; set; }
+ public TimeSpan HandshakeTimeout { get; set; }
+ public TimeSpan PingInterval { get; set; }
+ public Dictionary Headers { get; set; } = new();
+ internal TlsConfigOpts? TlsConfigOpts { get; set; }
+}
+
+///
+/// MQTT configuration options.
+/// Mirrors MQTTOpts in opts.go.
+///
+public class MqttOpts
+{
+ public string Host { get; set; } = string.Empty;
+ public int Port { get; set; }
+ public string NoAuthUser { get; set; } = string.Empty;
+ public string Username { get; set; } = string.Empty;
+ public string Password { get; set; } = string.Empty;
+ public string Token { get; set; } = string.Empty;
+ public string JsDomain { get; set; } = string.Empty;
+ public int StreamReplicas { get; set; }
+ public int ConsumerReplicas { get; set; }
+ public bool ConsumerMemoryStorage { get; set; }
+ public TimeSpan ConsumerInactiveThreshold { get; set; }
+ public double AuthTimeout { get; set; }
+ public SslServerAuthenticationOptions? TlsConfig { get; set; }
+ public bool TlsMap { get; set; }
+ public double TlsTimeout { get; set; }
+ public PinnedCertSet? TlsPinnedCerts { get; set; }
+ public TimeSpan AckWait { get; set; }
+ public TimeSpan JsApiTimeout { get; set; }
+ public ushort MaxAckPending { get; set; }
+ internal TlsConfigOpts? TlsConfigOpts { get; set; }
+ internal bool RejectQoS2Pub { get; set; }
+ internal bool DowngradeQoS2Sub { get; set; }
+}
+
+///
+/// JetStream server-level limits.
+/// Mirrors JSLimitOpts in opts.go.
+///
+public class JsLimitOpts
+{
+ public int MaxRequestBatch { get; set; }
+ public int MaxAckPending { get; set; }
+ public int MaxHaAssets { get; set; }
+ public TimeSpan Duplicates { get; set; }
+ public int MaxBatchInflightPerStream { get; set; }
+ public int MaxBatchInflightTotal { get; set; }
+ public int MaxBatchSize { get; set; }
+ public TimeSpan MaxBatchTimeout { get; set; }
+}
+
+///
+/// TPM configuration for JetStream encryption.
+/// Mirrors JSTpmOpts in opts.go.
+///
+public class JsTpmOpts
+{
+ public string KeysFile { get; set; } = string.Empty;
+ public string KeyPassword { get; set; } = string.Empty;
+ public string SrkPassword { get; set; } = string.Empty;
+ public int Pcr { get; set; }
+}
+
+///
+/// Auth callout options for external authentication.
+/// Mirrors AuthCallout in opts.go.
+///
+public class AuthCalloutOpts
+{
+ public string Issuer { get; set; } = string.Empty;
+ public string Account { get; set; } = string.Empty;
+ public List AuthUsers { get; set; } = [];
+ public string XKey { get; set; } = string.Empty;
+ public List AllowedAccounts { get; set; } = [];
+}
+
+///
+/// Proxy configuration for trusted proxies.
+/// Mirrors ProxiesConfig in opts.go.
+///
+public class ProxiesConfig
+{
+ public List Trusted { get; set; } = [];
+}
+
+///
+/// A single trusted proxy identified by public key.
+/// Mirrors ProxyConfig in opts.go.
+///
+public class ProxyConfig
+{
+ public string Key { get; set; } = string.Empty;
+}
+
+///
+/// Parsed authorization section from config file.
+/// Mirrors the unexported authorization struct in opts.go.
+///
+internal class AuthorizationConfig
+{
+ public string User { get; set; } = string.Empty;
+ public string Pass { get; set; } = string.Empty;
+ public string Token { get; set; } = string.Empty;
+ public string Nkey { get; set; } = string.Empty;
+ public string Acc { get; set; } = string.Empty;
+ public bool ProxyRequired { get; set; }
+ public double Timeout { get; set; }
+ public AuthCalloutOpts? Callout { get; set; }
+}
+
+///
+/// Custom authentication interface.
+/// Mirrors the Authentication interface in auth.go.
+///
+public interface IAuthentication
+{
+ bool Check(IClientAuthentication client);
+}
+
+///
+/// Client-side of authentication check.
+/// Mirrors ClientAuthentication in auth.go.
+///
+public interface IClientAuthentication
+{
+ string? GetOpts();
+ bool IsTls();
+ string? GetTlsConnectionState();
+ string RemoteAddress();
+}
+
+///
+/// Account resolver interface for dynamic account loading.
+/// Mirrors AccountResolver in accounts.go.
+///
+public interface IAccountResolver
+{
+ (string jwt, Exception? err) Fetch(string name);
+ Exception? Store(string name, string jwt);
+ bool IsReadOnly();
+ Exception? Start(object server);
+ bool IsTrackingUpdate();
+ Exception? Reload();
+ void Close();
+}
diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.Methods.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.Methods.cs
new file mode 100644
index 0000000..a356eb0
--- /dev/null
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.Methods.cs
@@ -0,0 +1,569 @@
+// Copyright 2012-2025 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/opts.go in the NATS server Go source.
+
+using System.Runtime.InteropServices;
+using System.Threading;
+
+namespace ZB.MOM.NatsNet.Server;
+
+// Global flag for unknown field handling.
+internal static class ConfigFlags
+{
+ private static int _allowUnknownTopLevelField;
+
+ ///
+ /// Sets whether unknown top-level config fields should be allowed.
+ /// Mirrors NoErrOnUnknownFields in opts.go.
+ ///
+ public static void NoErrOnUnknownFields(bool noError) =>
+ Interlocked.Exchange(ref _allowUnknownTopLevelField, noError ? 1 : 0);
+
+ public static bool AllowUnknownTopLevelField =>
+ Interlocked.CompareExchange(ref _allowUnknownTopLevelField, 0, 0) != 0;
+}
+
+public sealed partial class ServerOptions
+{
+ ///
+ /// Snapshot of command-line flags, populated during .
+ /// Mirrors FlagSnapshot in opts.go.
+ ///
+ public static ServerOptions? FlagSnapshot { get; internal set; }
+
+ ///
+ /// Deep-copies this instance.
+ /// Mirrors Options.Clone() in opts.go.
+ ///
+ public ServerOptions Clone()
+ {
+ // Start with a shallow memberwise clone.
+ var clone = (ServerOptions)MemberwiseClone();
+
+ // Deep-copy reference types that need isolation.
+ if (Routes.Count > 0)
+ clone.Routes = Routes.Select(u => new Uri(u.ToString())).ToList();
+
+ clone.Cluster = CloneClusterOpts(Cluster);
+ clone.Gateway = CloneGatewayOpts(Gateway);
+ clone.LeafNode = CloneLeafNodeOpts(LeafNode);
+ clone.Websocket = CloneWebsocketOpts(Websocket);
+ clone.Mqtt = CloneMqttOpts(Mqtt);
+
+ clone.Tags = [.. Tags];
+ clone.Metadata = new Dictionary(Metadata);
+ clone.TrustedKeys = [.. TrustedKeys];
+ clone.JsAccDefaultDomain = new Dictionary(JsAccDefaultDomain);
+ clone.InConfig = new Dictionary(InConfig);
+ clone.InCmdLine = new Dictionary(InCmdLine);
+ clone.OperatorJwt = [.. OperatorJwt];
+ clone.ResolverPreloads = new Dictionary(ResolverPreloads);
+ clone.ResolverPinnedAccounts = [.. ResolverPinnedAccounts];
+
+ return clone;
+ }
+
+ ///
+ /// Returns the SHA-256 digest of the configuration.
+ /// Mirrors Options.ConfigDigest() in opts.go.
+ ///
+ public string ConfigDigest() => ConfigDigestValue;
+
+ // -------------------------------------------------------------------------
+ // Merge / Baseline
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Merges file-based options with command-line flag options.
+ /// Flag options override file options where set.
+ /// Mirrors MergeOptions in opts.go.
+ ///
+ public static ServerOptions MergeOptions(ServerOptions? fileOpts, ServerOptions? flagOpts)
+ {
+ if (fileOpts == null) return flagOpts ?? new ServerOptions();
+ if (flagOpts == null) return fileOpts;
+
+ var opts = fileOpts.Clone();
+
+ if (flagOpts.Port != 0) opts.Port = flagOpts.Port;
+ if (!string.IsNullOrEmpty(flagOpts.Host)) opts.Host = flagOpts.Host;
+ if (flagOpts.DontListen) opts.DontListen = true;
+ if (!string.IsNullOrEmpty(flagOpts.ClientAdvertise)) opts.ClientAdvertise = flagOpts.ClientAdvertise;
+ if (!string.IsNullOrEmpty(flagOpts.Username)) opts.Username = flagOpts.Username;
+ if (!string.IsNullOrEmpty(flagOpts.Password)) opts.Password = flagOpts.Password;
+ if (!string.IsNullOrEmpty(flagOpts.Authorization)) opts.Authorization = flagOpts.Authorization;
+ if (flagOpts.HttpPort != 0) opts.HttpPort = flagOpts.HttpPort;
+ if (!string.IsNullOrEmpty(flagOpts.HttpBasePath)) opts.HttpBasePath = flagOpts.HttpBasePath;
+ if (flagOpts.Debug) opts.Debug = true;
+ if (flagOpts.Trace) opts.Trace = true;
+ if (flagOpts.Logtime) opts.Logtime = true;
+ if (!string.IsNullOrEmpty(flagOpts.LogFile)) opts.LogFile = flagOpts.LogFile;
+ if (!string.IsNullOrEmpty(flagOpts.PidFile)) opts.PidFile = flagOpts.PidFile;
+ if (!string.IsNullOrEmpty(flagOpts.PortsFileDir)) opts.PortsFileDir = flagOpts.PortsFileDir;
+ if (flagOpts.ProfPort != 0) opts.ProfPort = flagOpts.ProfPort;
+ if (!string.IsNullOrEmpty(flagOpts.Cluster.ListenStr)) opts.Cluster.ListenStr = flagOpts.Cluster.ListenStr;
+ if (flagOpts.Cluster.NoAdvertise) opts.Cluster.NoAdvertise = true;
+ if (flagOpts.Cluster.ConnectRetries != 0) opts.Cluster.ConnectRetries = flagOpts.Cluster.ConnectRetries;
+ if (!string.IsNullOrEmpty(flagOpts.Cluster.Advertise)) opts.Cluster.Advertise = flagOpts.Cluster.Advertise;
+ if (!string.IsNullOrEmpty(flagOpts.RoutesStr)) MergeRoutes(opts, flagOpts);
+ if (flagOpts.JetStream) opts.JetStream = true;
+ if (!string.IsNullOrEmpty(flagOpts.StoreDir)) opts.StoreDir = flagOpts.StoreDir;
+
+ return opts;
+ }
+
+ ///
+ /// Parses route URLs from a comma-separated string.
+ /// Mirrors RoutesFromStr in opts.go.
+ ///
+ public static List RoutesFromStr(string routesStr)
+ {
+ var parts = routesStr.Split(',');
+ if (parts.Length == 0) return [];
+
+ var urls = new List();
+ foreach (var r in parts)
+ {
+ var trimmed = r.Trim();
+ if (Uri.TryCreate(trimmed, UriKind.Absolute, out var u))
+ urls.Add(u);
+ }
+ return urls;
+ }
+
+ ///
+ /// Applies system-wide defaults to any unset options.
+ /// Mirrors setBaselineOptions in opts.go.
+ ///
+ public void SetBaselineOptions()
+ {
+ if (string.IsNullOrEmpty(Host))
+ Host = ServerConstants.DefaultHost;
+ if (string.IsNullOrEmpty(HttpHost))
+ HttpHost = Host;
+ if (Port == 0)
+ Port = ServerConstants.DefaultPort;
+ else if (Port == ServerConstants.RandomPort)
+ Port = 0;
+ if (MaxConn == 0)
+ MaxConn = ServerConstants.DefaultMaxConnections;
+ if (PingInterval == TimeSpan.Zero)
+ PingInterval = ServerConstants.DefaultPingInterval;
+ if (MaxPingsOut == 0)
+ MaxPingsOut = ServerConstants.DefaultPingMaxOut;
+ if (TlsTimeout == 0)
+ TlsTimeout = ServerConstants.TlsTimeout.TotalSeconds;
+ if (AuthTimeout == 0)
+ AuthTimeout = GetDefaultAuthTimeout(TlsConfig, TlsTimeout);
+
+ // Cluster defaults
+ if (Cluster.Port != 0 || !string.IsNullOrEmpty(Cluster.ListenStr))
+ {
+ if (string.IsNullOrEmpty(Cluster.Host))
+ Cluster.Host = ServerConstants.DefaultHost;
+ if (Cluster.TlsTimeout == 0)
+ Cluster.TlsTimeout = ServerConstants.TlsTimeout.TotalSeconds;
+ if (Cluster.AuthTimeout == 0)
+ Cluster.AuthTimeout = GetDefaultAuthTimeout(Cluster.TlsConfig, Cluster.TlsTimeout);
+ if (Cluster.PoolSize == 0)
+ Cluster.PoolSize = ServerConstants.DefaultRoutePoolSize;
+
+ // Add system account to pinned accounts if pool is enabled.
+ if (Cluster.PoolSize > 0)
+ {
+ var sysAccName = SystemAccount;
+ if (string.IsNullOrEmpty(sysAccName) && !NoSystemAccount)
+ sysAccName = ServerConstants.DefaultSystemAccount;
+ if (!string.IsNullOrEmpty(sysAccName) && !Cluster.PinnedAccounts.Contains(sysAccName))
+ Cluster.PinnedAccounts.Add(sysAccName);
+ }
+
+ // Default compression to "accept".
+ if (string.IsNullOrEmpty(Cluster.Compression.Mode))
+ Cluster.Compression.Mode = CompressionModes.Accept;
+ }
+
+ // LeafNode defaults
+ if (LeafNode.Port != 0)
+ {
+ if (string.IsNullOrEmpty(LeafNode.Host))
+ LeafNode.Host = ServerConstants.DefaultHost;
+ if (LeafNode.TlsTimeout == 0)
+ LeafNode.TlsTimeout = ServerConstants.TlsTimeout.TotalSeconds;
+ if (LeafNode.AuthTimeout == 0)
+ LeafNode.AuthTimeout = GetDefaultAuthTimeout(LeafNode.TlsConfig, LeafNode.TlsTimeout);
+ if (string.IsNullOrEmpty(LeafNode.Compression.Mode))
+ LeafNode.Compression.Mode = CompressionModes.S2Auto;
+ }
+
+ // Remote leafnode defaults
+ foreach (var r in LeafNode.Remotes)
+ {
+ foreach (var u in r.Urls)
+ {
+ if (u.IsDefaultPort || string.IsNullOrEmpty(u.GetComponents(UriComponents.Port, UriFormat.Unescaped)))
+ {
+ var builder = new UriBuilder(u) { Port = ServerConstants.DefaultLeafNodePort };
+ r.Urls[r.Urls.IndexOf(u)] = builder.Uri;
+ }
+ }
+ if (string.IsNullOrEmpty(r.Compression.Mode))
+ r.Compression.Mode = CompressionModes.S2Auto;
+ if (r.FirstInfoTimeout <= TimeSpan.Zero)
+ r.FirstInfoTimeout = ServerConstants.DefaultLeafNodeInfoWait;
+ }
+
+ if (LeafNode.ReconnectInterval == TimeSpan.Zero)
+ LeafNode.ReconnectInterval = ServerConstants.DefaultLeafNodeReconnect;
+
+ // Protocol limits
+ if (MaxControlLine == 0)
+ MaxControlLine = ServerConstants.MaxControlLineSize;
+ if (MaxPayload == 0)
+ MaxPayload = ServerConstants.MaxPayloadSize;
+ if (MaxPending == 0)
+ MaxPending = ServerConstants.MaxPendingSize;
+ if (WriteDeadline == TimeSpan.Zero)
+ WriteDeadline = ServerConstants.DefaultFlushDeadline;
+ if (MaxClosedClients == 0)
+ MaxClosedClients = ServerConstants.DefaultMaxClosedClients;
+ if (LameDuckDuration == TimeSpan.Zero)
+ LameDuckDuration = ServerConstants.DefaultLameDuckDuration;
+ if (LameDuckGracePeriod == TimeSpan.Zero)
+ LameDuckGracePeriod = ServerConstants.DefaultLameDuckGracePeriod;
+
+ // Gateway defaults
+ if (Gateway.Port != 0)
+ {
+ if (string.IsNullOrEmpty(Gateway.Host))
+ Gateway.Host = ServerConstants.DefaultHost;
+ if (Gateway.TlsTimeout == 0)
+ Gateway.TlsTimeout = ServerConstants.TlsTimeout.TotalSeconds;
+ if (Gateway.AuthTimeout == 0)
+ Gateway.AuthTimeout = GetDefaultAuthTimeout(Gateway.TlsConfig, Gateway.TlsTimeout);
+ }
+
+ // Error reporting
+ if (ConnectErrorReports == 0)
+ ConnectErrorReports = ServerConstants.DefaultConnectErrorReports;
+ if (ReconnectErrorReports == 0)
+ ReconnectErrorReports = ServerConstants.DefaultReconnectErrorReports;
+
+ // WebSocket defaults
+ if (Websocket.Port != 0)
+ {
+ if (string.IsNullOrEmpty(Websocket.Host))
+ Websocket.Host = ServerConstants.DefaultHost;
+ }
+
+ // MQTT defaults
+ if (Mqtt.Port != 0)
+ {
+ if (string.IsNullOrEmpty(Mqtt.Host))
+ Mqtt.Host = ServerConstants.DefaultHost;
+ if (Mqtt.TlsTimeout == 0)
+ Mqtt.TlsTimeout = ServerConstants.TlsTimeout.TotalSeconds;
+ }
+
+ // JetStream defaults
+ if (JetStreamMaxMemory == 0 && !MaxMemSet)
+ JetStreamMaxMemory = -1;
+ if (JetStreamMaxStore == 0 && !MaxStoreSet)
+ JetStreamMaxStore = -1;
+ if (SyncInterval == TimeSpan.Zero && !SyncSet)
+ SyncInterval = TimeSpan.FromMinutes(2); // defaultSyncInterval
+ if (JetStreamRequestQueueLimit <= 0)
+ JetStreamRequestQueueLimit = 4096; // JSDefaultRequestQueueLimit
+ }
+
+ ///
+ /// Normalizes an HTTP base path (ensure leading slash, clean redundant separators).
+ /// Mirrors normalizeBasePath in opts.go.
+ ///
+ public static string NormalizeBasePath(string p)
+ {
+ if (string.IsNullOrEmpty(p)) return "/";
+ if (p[0] != '/') p = "/" + p;
+ // Simple path clean: collapse repeated slashes and remove trailing slash.
+ while (p.Contains("//")) p = p.Replace("//", "/");
+ return p.Length > 1 && p.EndsWith('/') ? p[..^1] : p;
+ }
+
+ ///
+ /// Computes the default auth timeout based on TLS config presence.
+ /// Mirrors getDefaultAuthTimeout in opts.go.
+ ///
+ public static double GetDefaultAuthTimeout(object? tlsConfig, double tlsTimeout)
+ {
+ if (tlsConfig != null)
+ return tlsTimeout + 1.0;
+ return ServerConstants.AuthTimeout.TotalSeconds;
+ }
+
+ ///
+ /// Returns the user's home directory.
+ /// Mirrors homeDir in opts.go.
+ ///
+ public static string HomeDir() => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+
+ ///
+ /// Expands environment variables and ~/ prefix in a path.
+ /// Mirrors expandPath in opts.go.
+ ///
+ public static string ExpandPath(string p)
+ {
+ p = Environment.ExpandEnvironmentVariables(p);
+ if (!p.StartsWith('~')) return p;
+ return Path.Combine(HomeDir(), p[1..].TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
+ }
+
+ ///
+ /// Reads a PID from a file path if possible, otherwise returns the string as-is.
+ /// Mirrors maybeReadPidFile in opts.go.
+ ///
+ public static string MaybeReadPidFile(string pidStr)
+ {
+ try { return File.ReadAllText(pidStr).Trim(); }
+ catch { return pidStr; }
+ }
+
+ ///
+ /// Applies TLS overrides from command-line options.
+ /// Mirrors overrideTLS in opts.go.
+ ///
+ public Exception? OverrideTls()
+ {
+ if (string.IsNullOrEmpty(TlsCert))
+ return new InvalidOperationException("TLS Server certificate must be present and valid");
+ if (string.IsNullOrEmpty(TlsKey))
+ return new InvalidOperationException("TLS Server private key must be present and valid");
+
+ // TLS config generation is deferred to GenTlsConfig (session 06+).
+ // For now, mark that TLS is enabled.
+ Tls = true;
+ return null;
+ }
+
+ ///
+ /// Overrides cluster options from the --cluster flag.
+ /// Mirrors overrideCluster in opts.go.
+ ///
+ public Exception? OverrideCluster()
+ {
+ if (string.IsNullOrEmpty(Cluster.ListenStr))
+ {
+ Cluster.Port = 0;
+ return null;
+ }
+
+ var listenStr = Cluster.ListenStr;
+ var wantsRandom = false;
+ if (listenStr.EndsWith(":-1"))
+ {
+ wantsRandom = true;
+ listenStr = listenStr[..^3] + ":0";
+ }
+
+ if (!Uri.TryCreate(listenStr, UriKind.Absolute, out var clusterUri))
+ return new InvalidOperationException($"could not parse cluster URL: {Cluster.ListenStr}");
+
+ Cluster.Host = clusterUri.Host;
+ Cluster.Port = wantsRandom ? -1 : clusterUri.Port;
+
+ var userInfo = clusterUri.UserInfo;
+ if (!string.IsNullOrEmpty(userInfo))
+ {
+ var parts = userInfo.Split(':', 2);
+ if (parts.Length != 2)
+ return new InvalidOperationException("expected cluster password to be set");
+ Cluster.Username = parts[0];
+ Cluster.Password = parts[1];
+ }
+ else
+ {
+ Cluster.Username = string.Empty;
+ Cluster.Password = string.Empty;
+ }
+
+ return null;
+ }
+
+ // -------------------------------------------------------------------------
+ // Private helpers
+ // -------------------------------------------------------------------------
+
+ private static void MergeRoutes(ServerOptions opts, ServerOptions flagOpts)
+ {
+ var routeUrls = RoutesFromStr(flagOpts.RoutesStr);
+ if (routeUrls.Count == 0) return;
+ opts.Routes = routeUrls;
+ opts.RoutesStr = flagOpts.RoutesStr;
+ }
+
+ internal static void TrackExplicitVal(Dictionary pm, string name, bool val) =>
+ pm[name] = val;
+
+ private static ClusterOpts CloneClusterOpts(ClusterOpts src) => new()
+ {
+ Name = src.Name,
+ Host = src.Host,
+ Port = src.Port,
+ Username = src.Username,
+ Password = src.Password,
+ AuthTimeout = src.AuthTimeout,
+ TlsTimeout = src.TlsTimeout,
+ TlsConfig = src.TlsConfig,
+ TlsMap = src.TlsMap,
+ TlsCheckKnownUrls = src.TlsCheckKnownUrls,
+ TlsPinnedCerts = src.TlsPinnedCerts != null ? new PinnedCertSet(src.TlsPinnedCerts) : null,
+ TlsHandshakeFirst = src.TlsHandshakeFirst,
+ TlsHandshakeFirstFallback = src.TlsHandshakeFirstFallback,
+ ListenStr = src.ListenStr,
+ Advertise = src.Advertise,
+ NoAdvertise = src.NoAdvertise,
+ ConnectRetries = src.ConnectRetries,
+ ConnectBackoff = src.ConnectBackoff,
+ PoolSize = src.PoolSize,
+ PinnedAccounts = [.. src.PinnedAccounts],
+ Compression = new CompressionOpts { Mode = src.Compression.Mode, RttThresholds = [.. src.Compression.RttThresholds] },
+ PingInterval = src.PingInterval,
+ MaxPingsOut = src.MaxPingsOut,
+ WriteDeadline = src.WriteDeadline,
+ WriteTimeout = src.WriteTimeout,
+ };
+
+ private static GatewayOpts CloneGatewayOpts(GatewayOpts src) => new()
+ {
+ Name = src.Name,
+ Host = src.Host,
+ Port = src.Port,
+ Username = src.Username,
+ Password = src.Password,
+ AuthTimeout = src.AuthTimeout,
+ TlsConfig = src.TlsConfig,
+ TlsTimeout = src.TlsTimeout,
+ TlsMap = src.TlsMap,
+ TlsCheckKnownUrls = src.TlsCheckKnownUrls,
+ TlsPinnedCerts = src.TlsPinnedCerts != null ? new PinnedCertSet(src.TlsPinnedCerts) : null,
+ Advertise = src.Advertise,
+ ConnectRetries = src.ConnectRetries,
+ ConnectBackoff = src.ConnectBackoff,
+ Gateways = src.Gateways.Select(g => new RemoteGatewayOpts
+ {
+ Name = g.Name,
+ TlsConfig = g.TlsConfig,
+ TlsTimeout = g.TlsTimeout,
+ Urls = g.Urls.Select(u => new Uri(u.ToString())).ToList(),
+ }).ToList(),
+ RejectUnknown = src.RejectUnknown,
+ WriteDeadline = src.WriteDeadline,
+ WriteTimeout = src.WriteTimeout,
+ };
+
+ private static LeafNodeOpts CloneLeafNodeOpts(LeafNodeOpts src) => new()
+ {
+ Host = src.Host,
+ Port = src.Port,
+ Username = src.Username,
+ Password = src.Password,
+ ProxyRequired = src.ProxyRequired,
+ Nkey = src.Nkey,
+ Account = src.Account,
+ AuthTimeout = src.AuthTimeout,
+ TlsConfig = src.TlsConfig,
+ TlsTimeout = src.TlsTimeout,
+ TlsMap = src.TlsMap,
+ TlsPinnedCerts = src.TlsPinnedCerts != null ? new PinnedCertSet(src.TlsPinnedCerts) : null,
+ TlsHandshakeFirst = src.TlsHandshakeFirst,
+ TlsHandshakeFirstFallback = src.TlsHandshakeFirstFallback,
+ Advertise = src.Advertise,
+ NoAdvertise = src.NoAdvertise,
+ ReconnectInterval = src.ReconnectInterval,
+ WriteDeadline = src.WriteDeadline,
+ WriteTimeout = src.WriteTimeout,
+ Compression = new CompressionOpts { Mode = src.Compression.Mode, RttThresholds = [.. src.Compression.RttThresholds] },
+ Remotes = src.Remotes.Select(r => new RemoteLeafOpts
+ {
+ LocalAccount = r.LocalAccount,
+ NoRandomize = r.NoRandomize,
+ Urls = r.Urls.Select(u => new Uri(u.ToString())).ToList(),
+ Credentials = r.Credentials,
+ Nkey = r.Nkey,
+ Tls = r.Tls,
+ TlsTimeout = r.TlsTimeout,
+ TlsHandshakeFirst = r.TlsHandshakeFirst,
+ Hub = r.Hub,
+ DenyImports = [.. r.DenyImports],
+ DenyExports = [.. r.DenyExports],
+ FirstInfoTimeout = r.FirstInfoTimeout,
+ Compression = new CompressionOpts { Mode = r.Compression.Mode, RttThresholds = [.. r.Compression.RttThresholds] },
+ JetStreamClusterMigrate = r.JetStreamClusterMigrate,
+ JetStreamClusterMigrateDelay = r.JetStreamClusterMigrateDelay,
+ LocalIsolation = r.LocalIsolation,
+ RequestIsolation = r.RequestIsolation,
+ Disabled = r.Disabled,
+ }).ToList(),
+ MinVersion = src.MinVersion,
+ IsolateLeafnodeInterest = src.IsolateLeafnodeInterest,
+ };
+
+ private static WebsocketOpts CloneWebsocketOpts(WebsocketOpts src) => new()
+ {
+ Host = src.Host,
+ Port = src.Port,
+ Advertise = src.Advertise,
+ NoAuthUser = src.NoAuthUser,
+ JwtCookie = src.JwtCookie,
+ UsernameCookie = src.UsernameCookie,
+ PasswordCookie = src.PasswordCookie,
+ TokenCookie = src.TokenCookie,
+ Username = src.Username,
+ Password = src.Password,
+ Token = src.Token,
+ AuthTimeout = src.AuthTimeout,
+ NoTls = src.NoTls,
+ TlsConfig = src.TlsConfig,
+ TlsMap = src.TlsMap,
+ TlsPinnedCerts = src.TlsPinnedCerts != null ? new PinnedCertSet(src.TlsPinnedCerts) : null,
+ SameOrigin = src.SameOrigin,
+ AllowedOrigins = [.. src.AllowedOrigins],
+ Compression = src.Compression,
+ HandshakeTimeout = src.HandshakeTimeout,
+ PingInterval = src.PingInterval,
+ Headers = new Dictionary(src.Headers),
+ };
+
+ private static MqttOpts CloneMqttOpts(MqttOpts src) => new()
+ {
+ Host = src.Host,
+ Port = src.Port,
+ NoAuthUser = src.NoAuthUser,
+ Username = src.Username,
+ Password = src.Password,
+ Token = src.Token,
+ JsDomain = src.JsDomain,
+ StreamReplicas = src.StreamReplicas,
+ ConsumerReplicas = src.ConsumerReplicas,
+ ConsumerMemoryStorage = src.ConsumerMemoryStorage,
+ ConsumerInactiveThreshold = src.ConsumerInactiveThreshold,
+ AuthTimeout = src.AuthTimeout,
+ TlsConfig = src.TlsConfig,
+ TlsMap = src.TlsMap,
+ TlsTimeout = src.TlsTimeout,
+ TlsPinnedCerts = src.TlsPinnedCerts != null ? new PinnedCertSet(src.TlsPinnedCerts) : null,
+ AckWait = src.AckWait,
+ JsApiTimeout = src.JsApiTimeout,
+ MaxAckPending = src.MaxAckPending,
+ };
+}
diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.cs
new file mode 100644
index 0000000..22921b8
--- /dev/null
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.cs
@@ -0,0 +1,234 @@
+// Copyright 2012-2025 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/opts.go in the NATS server Go source.
+
+using System.Net.Security;
+using System.Security.Authentication;
+using System.Threading;
+
+namespace ZB.MOM.NatsNet.Server;
+
+///
+/// Server configuration options block.
+/// Mirrors Options struct in opts.go.
+///
+public sealed partial class ServerOptions
+{
+ // -------------------------------------------------------------------------
+ // General / Startup
+ // -------------------------------------------------------------------------
+
+ public string ConfigFile { get; set; } = string.Empty;
+ public string ServerName { get; set; } = string.Empty;
+ public string Host { get; set; } = string.Empty;
+ public int Port { get; set; }
+ public bool DontListen { get; set; }
+ public string ClientAdvertise { get; set; } = string.Empty;
+ public bool CheckConfig { get; set; }
+ public string PidFile { get; set; } = string.Empty;
+ public string PortsFileDir { get; set; } = string.Empty;
+
+ // -------------------------------------------------------------------------
+ // Logging & Debugging
+ // -------------------------------------------------------------------------
+
+ public bool Trace { get; set; }
+ public bool Debug { get; set; }
+ public bool TraceVerbose { get; set; }
+ public bool TraceHeaders { get; set; }
+ public bool NoLog { get; set; }
+ public bool NoSigs { get; set; }
+ public bool Logtime { get; set; }
+ public bool LogtimeUtc { get; set; }
+ public string LogFile { get; set; } = string.Empty;
+ public long LogSizeLimit { get; set; }
+ public long LogMaxFiles { get; set; }
+ public bool Syslog { get; set; }
+ public string RemoteSyslog { get; set; } = string.Empty;
+ public int ProfPort { get; set; }
+ public int ProfBlockRate { get; set; }
+ public int MaxTracedMsgLen { get; set; }
+
+ // -------------------------------------------------------------------------
+ // Networking & Limits
+ // -------------------------------------------------------------------------
+
+ public int MaxConn { get; set; }
+ public int MaxSubs { get; set; }
+ public byte MaxSubTokens { get; set; }
+ public int MaxControlLine { get; set; }
+ public int MaxPayload { get; set; }
+ public long MaxPending { get; set; }
+ public bool NoFastProducerStall { get; set; }
+ public bool ProxyRequired { get; set; }
+ public bool ProxyProtocol { get; set; }
+ public int MaxClosedClients { get; set; }
+
+ // -------------------------------------------------------------------------
+ // Connectivity
+ // -------------------------------------------------------------------------
+
+ public TimeSpan PingInterval { get; set; }
+ public int MaxPingsOut { get; set; }
+ public TimeSpan WriteDeadline { get; set; }
+ public WriteTimeoutPolicy WriteTimeout { get; set; }
+ public TimeSpan LameDuckDuration { get; set; }
+ public TimeSpan LameDuckGracePeriod { get; set; }
+
+ // -------------------------------------------------------------------------
+ // HTTP / Monitoring
+ // -------------------------------------------------------------------------
+
+ public string HttpHost { get; set; } = string.Empty;
+ public int HttpPort { get; set; }
+ public string HttpBasePath { get; set; } = string.Empty;
+ public int HttpsPort { get; set; }
+
+ // -------------------------------------------------------------------------
+ // Authentication & Authorization
+ // -------------------------------------------------------------------------
+
+ public string Username { get; set; } = string.Empty;
+ public string Password { get; set; } = string.Empty;
+ public string Authorization { get; set; } = string.Empty;
+ public double AuthTimeout { get; set; }
+ public string NoAuthUser { get; set; } = string.Empty;
+ public string DefaultSentinel { get; set; } = string.Empty;
+ public string SystemAccount { get; set; } = string.Empty;
+ public bool NoSystemAccount { get; set; }
+ public AuthCalloutOpts? AuthCallout { get; set; }
+ public bool AlwaysEnableNonce { get; set; }
+ public IAuthentication? CustomClientAuthentication { get; set; }
+ public IAuthentication? CustomRouterAuthentication { get; set; }
+
+ // -------------------------------------------------------------------------
+ // Sublist
+ // -------------------------------------------------------------------------
+
+ public bool NoSublistCache { get; set; }
+ public bool NoHeaderSupport { get; set; }
+ public bool DisableShortFirstPing { get; set; }
+
+ // -------------------------------------------------------------------------
+ // TLS (Client)
+ // -------------------------------------------------------------------------
+
+ public double TlsTimeout { get; set; }
+ public bool Tls { get; set; }
+ public bool TlsVerify { get; set; }
+ public bool TlsMap { get; set; }
+ public string TlsCert { get; set; } = string.Empty;
+ public string TlsKey { get; set; } = string.Empty;
+ public string TlsCaCert { get; set; } = string.Empty;
+ public SslServerAuthenticationOptions? TlsConfig { get; set; }
+ public PinnedCertSet? TlsPinnedCerts { get; set; }
+ public long TlsRateLimit { get; set; }
+ public bool TlsHandshakeFirst { get; set; }
+ public TimeSpan TlsHandshakeFirstFallback { get; set; }
+ public bool AllowNonTls { get; set; }
+
+ // -------------------------------------------------------------------------
+ // Cluster / Gateway / Leaf / WebSocket / MQTT
+ // -------------------------------------------------------------------------
+
+ public ClusterOpts Cluster { get; set; } = new();
+ public GatewayOpts Gateway { get; set; } = new();
+ public LeafNodeOpts LeafNode { get; set; } = new();
+ public WebsocketOpts Websocket { get; set; } = new();
+ public MqttOpts Mqtt { get; set; } = new();
+
+ // -------------------------------------------------------------------------
+ // Routing
+ // -------------------------------------------------------------------------
+
+ public List Routes { get; set; } = [];
+ public string RoutesStr { get; set; } = string.Empty;
+
+ // -------------------------------------------------------------------------
+ // JetStream
+ // -------------------------------------------------------------------------
+
+ public bool JetStream { get; set; }
+ public bool NoJetStreamStrict { get; set; }
+ public long JetStreamMaxMemory { get; set; }
+ public long JetStreamMaxStore { get; set; }
+ public string JetStreamDomain { get; set; } = string.Empty;
+ public string JetStreamExtHint { get; set; } = string.Empty;
+ public string JetStreamKey { get; set; } = string.Empty;
+ public string JetStreamOldKey { get; set; } = string.Empty;
+ public StoreCipher JetStreamCipher { get; set; }
+ public string JetStreamUniqueTag { get; set; } = string.Empty;
+ public JsLimitOpts JetStreamLimits { get; set; } = new();
+ public JsTpmOpts JetStreamTpm { get; set; } = new();
+ public long JetStreamMaxCatchup { get; set; }
+ public long JetStreamRequestQueueLimit { get; set; }
+ public ulong JetStreamMetaCompact { get; set; }
+ public ulong JetStreamMetaCompactSize { get; set; }
+ public bool JetStreamMetaCompactSync { get; set; }
+ public int StreamMaxBufferedMsgs { get; set; }
+ public long StreamMaxBufferedSize { get; set; }
+ public string StoreDir { get; set; } = string.Empty;
+ public TimeSpan SyncInterval { get; set; }
+ public bool SyncAlways { get; set; }
+ public Dictionary JsAccDefaultDomain { get; set; } = new();
+ public bool DisableJetStreamBanner { get; set; }
+
+ // -------------------------------------------------------------------------
+ // Security & Trust
+ // -------------------------------------------------------------------------
+
+ public List TrustedKeys { get; set; } = [];
+ public SslServerAuthenticationOptions? AccountResolverTlsConfig { get; set; }
+ public IAccountResolver? AccountResolver { get; set; }
+ public OcspConfig? OcspConfig { get; set; }
+ public OcspResponseCacheConfig? OcspCacheConfig { get; set; }
+
+ // -------------------------------------------------------------------------
+ // Tagging & Metadata
+ // -------------------------------------------------------------------------
+
+ public List Tags { get; set; } = [];
+ public Dictionary Metadata { get; set; } = new();
+
+ // -------------------------------------------------------------------------
+ // Proxies
+ // -------------------------------------------------------------------------
+
+ public ProxiesConfig? Proxies { get; set; }
+
+ // -------------------------------------------------------------------------
+ // Connectivity error reporting
+ // -------------------------------------------------------------------------
+
+ public int ConnectErrorReports { get; set; }
+ public int ReconnectErrorReports { get; set; }
+
+ // -------------------------------------------------------------------------
+ // Internal / Private fields
+ // -------------------------------------------------------------------------
+
+ internal Dictionary InConfig { get; set; } = new();
+ internal Dictionary InCmdLine { get; set; } = new();
+ internal List OperatorJwt { get; set; } = [];
+ internal Dictionary ResolverPreloads { get; set; } = new();
+ internal HashSet ResolverPinnedAccounts { get; set; } = [];
+ internal TimeSpan GatewaysSolicitDelay { get; set; }
+ internal int OverrideProto { get; set; }
+ internal bool MaxMemSet { get; set; }
+ internal bool MaxStoreSet { get; set; }
+ internal bool SyncSet { get; set; }
+ internal bool AuthBlockDefined { get; set; }
+ internal string ConfigDigestValue { get; set; } = string.Empty;
+ internal TlsConfigOpts? TlsConfigOpts { get; set; }
+}
diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ServerOptionsTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ServerOptionsTests.cs
new file mode 100644
index 0000000..85dd93a
--- /dev/null
+++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ServerOptionsTests.cs
@@ -0,0 +1,333 @@
+// Copyright 2012-2025 The NATS Authors
+// Licensed under the Apache License, Version 2.0
+
+using Shouldly;
+using ZB.MOM.NatsNet.Server;
+
+namespace ZB.MOM.NatsNet.Server.Tests;
+
+///
+/// Tests for ServerOptions — mirrors tests from server/opts_test.go.
+///
+public class ServerOptionsTests
+{
+ ///
+ /// Mirrors TestDefaultOptions — verifies baseline defaults are applied.
+ ///
+ [Fact] // T:2512
+ public void DefaultOptions_ShouldSetBaselineDefaults()
+ {
+ var opts = new ServerOptions();
+ opts.SetBaselineOptions();
+
+ opts.Host.ShouldBe(ServerConstants.DefaultHost);
+ opts.Port.ShouldBe(ServerConstants.DefaultPort);
+ opts.MaxConn.ShouldBe(ServerConstants.DefaultMaxConnections);
+ opts.HttpHost.ShouldBe(ServerConstants.DefaultHost);
+ opts.PingInterval.ShouldBe(ServerConstants.DefaultPingInterval);
+ opts.MaxPingsOut.ShouldBe(ServerConstants.DefaultPingMaxOut);
+ opts.TlsTimeout.ShouldBe(ServerConstants.TlsTimeout.TotalSeconds);
+ opts.AuthTimeout.ShouldBe(ServerConstants.AuthTimeout.TotalSeconds);
+ opts.MaxControlLine.ShouldBe(ServerConstants.MaxControlLineSize);
+ opts.MaxPayload.ShouldBe(ServerConstants.MaxPayloadSize);
+ opts.MaxPending.ShouldBe(ServerConstants.MaxPendingSize);
+ opts.WriteDeadline.ShouldBe(ServerConstants.DefaultFlushDeadline);
+ opts.MaxClosedClients.ShouldBe(ServerConstants.DefaultMaxClosedClients);
+ opts.LameDuckDuration.ShouldBe(ServerConstants.DefaultLameDuckDuration);
+ opts.LameDuckGracePeriod.ShouldBe(ServerConstants.DefaultLameDuckGracePeriod);
+ opts.LeafNode.ReconnectInterval.ShouldBe(ServerConstants.DefaultLeafNodeReconnect);
+ opts.ConnectErrorReports.ShouldBe(ServerConstants.DefaultConnectErrorReports);
+ opts.ReconnectErrorReports.ShouldBe(ServerConstants.DefaultReconnectErrorReports);
+ opts.JetStreamMaxMemory.ShouldBe(-1);
+ opts.JetStreamMaxStore.ShouldBe(-1);
+ opts.SyncInterval.ShouldBe(TimeSpan.FromMinutes(2));
+ opts.JetStreamRequestQueueLimit.ShouldBe(4096);
+ }
+
+ ///
+ /// Mirrors TestOptions_RandomPort — RANDOM_PORT should become 0 after baseline.
+ ///
+ [Fact] // T:2513
+ public void RandomPort_ShouldResolveToZero()
+ {
+ var opts = new ServerOptions { Port = ServerConstants.RandomPort };
+ opts.SetBaselineOptions();
+ opts.Port.ShouldBe(0);
+ }
+
+ ///
+ /// Mirrors TestMergeOverrides — flag options override file options.
+ ///
+ [Fact] // T:2516
+ public void MergeOverrides_ShouldLetFlagsWin()
+ {
+ var fileOpts = new ServerOptions
+ {
+ Host = "0.0.0.0",
+ Port = 4222,
+ Username = "fileuser",
+ Password = "filepass",
+ };
+
+ var flagOpts = new ServerOptions
+ {
+ Port = 9999,
+ Username = "flaguser",
+ };
+
+ var merged = ServerOptions.MergeOptions(fileOpts, flagOpts);
+
+ merged.Port.ShouldBe(9999);
+ merged.Username.ShouldBe("flaguser");
+ // Host not overridden (empty in flags)
+ merged.Host.ShouldBe("0.0.0.0");
+ // Password not overridden (empty in flags)
+ merged.Password.ShouldBe("filepass");
+ }
+
+ ///
+ /// Mirrors TestOptionsClone — deep copy should produce independent objects.
+ ///
+ [Fact] // T:2537
+ public void Clone_ShouldDeepCopyAllFields()
+ {
+ var opts = new ServerOptions
+ {
+ ConfigFile = "./configs/test.conf",
+ Host = "127.0.0.1",
+ Port = 2222,
+ Username = "derek",
+ Password = "porkchop",
+ AuthTimeout = 1.0,
+ Debug = true,
+ Trace = true,
+ Logtime = false,
+ HttpPort = ServerConstants.DefaultHttpPort,
+ HttpBasePath = ServerConstants.DefaultHttpBasePath,
+ PidFile = "/tmp/nats-server/nats-server.pid",
+ ProfPort = 6789,
+ Syslog = true,
+ RemoteSyslog = "udp://foo.com:33",
+ MaxControlLine = 2048,
+ MaxPayload = 65536,
+ MaxConn = 100,
+ PingInterval = TimeSpan.FromSeconds(60),
+ MaxPingsOut = 3,
+ Cluster = new ClusterOpts
+ {
+ NoAdvertise = true,
+ ConnectRetries = 2,
+ WriteDeadline = TimeSpan.FromSeconds(3),
+ },
+ Gateway = new GatewayOpts
+ {
+ Name = "A",
+ Gateways =
+ [
+ new RemoteGatewayOpts { Name = "B", Urls = [new Uri("nats://host:5222")] },
+ new RemoteGatewayOpts { Name = "C" },
+ ],
+ },
+ WriteDeadline = TimeSpan.FromSeconds(3),
+ Routes = [new Uri("nats://localhost:4222")],
+ };
+
+ var clone = opts.Clone();
+
+ // Values should match.
+ clone.ConfigFile.ShouldBe(opts.ConfigFile);
+ clone.Host.ShouldBe(opts.Host);
+ clone.Port.ShouldBe(opts.Port);
+ clone.Username.ShouldBe(opts.Username);
+ clone.Gateway.Name.ShouldBe("A");
+ clone.Gateway.Gateways.Count.ShouldBe(2);
+ clone.Gateway.Gateways[0].Urls[0].Authority.ShouldBe("host:5222");
+
+ // Mutating clone should not affect original.
+ clone.Username = "changed";
+ opts.Username.ShouldBe("derek");
+
+ // Mutating original gateway URLs should not affect clone.
+ opts.Gateway.Gateways[0].Urls[0] = new Uri("nats://other:9999");
+ clone.Gateway.Gateways[0].Urls[0].Authority.ShouldBe("host:5222");
+ }
+
+ ///
+ /// Mirrors TestOptionsCloneNilLists — clone of empty lists should produce empty lists.
+ ///
+ [Fact] // T:2538
+ public void CloneNilLists_ShouldProduceEmptyLists()
+ {
+ var opts = new ServerOptions();
+ var clone = opts.Clone();
+ clone.Routes.ShouldNotBeNull();
+ }
+
+ ///
+ /// Mirrors TestExpandPath — tilde and env var expansion.
+ ///
+ [Fact] // T:2563
+ public void ExpandPath_ShouldExpandTildeAndEnvVars()
+ {
+ // Absolute path should not change.
+ var result = ServerOptions.ExpandPath("/foo/bar");
+ result.ShouldBe("/foo/bar");
+
+ // Tilde expansion.
+ var home = ServerOptions.HomeDir();
+ var expanded = ServerOptions.ExpandPath("~/test");
+ expanded.ShouldBe(Path.Combine(home, "test"));
+ }
+
+ ///
+ /// Mirrors TestDefaultAuthTimeout — verifies auth timeout logic.
+ ///
+ [Fact] // T:2572
+ public void DefaultAuthTimeout_ShouldBe2sWithoutTls()
+ {
+ var timeout = ServerOptions.GetDefaultAuthTimeout(null, 0);
+ timeout.ShouldBe(ServerConstants.AuthTimeout.TotalSeconds);
+ }
+
+ [Fact]
+ public void DefaultAuthTimeout_ShouldBeTlsTimeoutPlusOneWithTls()
+ {
+ // When TLS is configured, auth timeout = tls_timeout + 1
+ var timeout = ServerOptions.GetDefaultAuthTimeout(new object(), 4.0);
+ timeout.ShouldBe(5.0);
+ }
+
+ ///
+ /// Tests RoutesFromStr parsing.
+ ///
+ [Fact] // T:2576 (partial)
+ public void RoutesFromStr_ShouldParseCommaSeparatedUrls()
+ {
+ var routes = ServerOptions.RoutesFromStr("nats://localhost:4222,nats://localhost:4223");
+ routes.Count.ShouldBe(2);
+ routes[0].Authority.ShouldBe("localhost:4222");
+ routes[1].Authority.ShouldBe("localhost:4223");
+ }
+
+ ///
+ /// Tests NormalizeBasePath.
+ ///
+ [Fact] // T:2581
+ public void NormalizeBasePath_ShouldCleanPaths()
+ {
+ ServerOptions.NormalizeBasePath("").ShouldBe("/");
+ ServerOptions.NormalizeBasePath("/").ShouldBe("/");
+ ServerOptions.NormalizeBasePath("foo").ShouldBe("/foo");
+ ServerOptions.NormalizeBasePath("/foo/").ShouldBe("/foo");
+ ServerOptions.NormalizeBasePath("//foo//bar//").ShouldBe("/foo/bar");
+ }
+
+ ///
+ /// Tests ConfigFlags.NoErrOnUnknownFields.
+ ///
+ [Fact] // T:2502
+ public void NoErrOnUnknownFields_ShouldToggleFlag()
+ {
+ ConfigFlags.NoErrOnUnknownFields(true);
+ ConfigFlags.AllowUnknownTopLevelField.ShouldBeTrue();
+
+ ConfigFlags.NoErrOnUnknownFields(false);
+ ConfigFlags.AllowUnknownTopLevelField.ShouldBeFalse();
+ }
+
+ ///
+ /// Tests SetBaseline with cluster port set.
+ ///
+ [Fact]
+ public void SetBaseline_WithClusterPort_ShouldApplyClusterDefaults()
+ {
+ var opts = new ServerOptions
+ {
+ Cluster = new ClusterOpts { Port = 6222 },
+ };
+ opts.SetBaselineOptions();
+
+ opts.Cluster.Host.ShouldBe(ServerConstants.DefaultHost);
+ opts.Cluster.TlsTimeout.ShouldBe(ServerConstants.TlsTimeout.TotalSeconds);
+ opts.Cluster.PoolSize.ShouldBe(ServerConstants.DefaultRoutePoolSize);
+ opts.Cluster.Compression.Mode.ShouldBe(CompressionModes.Accept);
+ // System account should be auto-pinned.
+ opts.Cluster.PinnedAccounts.ShouldContain(ServerConstants.DefaultSystemAccount);
+ }
+
+ ///
+ /// Tests SetBaseline with leaf node port set.
+ ///
+ [Fact]
+ public void SetBaseline_WithLeafNodePort_ShouldApplyLeafDefaults()
+ {
+ var opts = new ServerOptions
+ {
+ LeafNode = new LeafNodeOpts { Port = 7422 },
+ };
+ opts.SetBaselineOptions();
+
+ opts.LeafNode.Host.ShouldBe(ServerConstants.DefaultHost);
+ opts.LeafNode.Compression.Mode.ShouldBe(CompressionModes.S2Auto);
+ }
+
+ ///
+ /// Tests OverrideCluster with valid URL.
+ ///
+ [Fact] // T:2518
+ public void OverrideCluster_ShouldParseUrlAndSetHostPort()
+ {
+ var opts = new ServerOptions
+ {
+ Cluster = new ClusterOpts { ListenStr = "nats://user:pass@127.0.0.1:6222" },
+ };
+ var err = opts.OverrideCluster();
+ err.ShouldBeNull();
+ opts.Cluster.Host.ShouldBe("127.0.0.1");
+ opts.Cluster.Port.ShouldBe(6222);
+ opts.Cluster.Username.ShouldBe("user");
+ opts.Cluster.Password.ShouldBe("pass");
+ }
+
+ ///
+ /// Tests OverrideCluster with empty string disables clustering.
+ ///
+ [Fact]
+ public void OverrideCluster_EmptyString_ShouldDisableClustering()
+ {
+ var opts = new ServerOptions
+ {
+ Cluster = new ClusterOpts { Port = 6222, ListenStr = "" },
+ };
+ var err = opts.OverrideCluster();
+ err.ShouldBeNull();
+ opts.Cluster.Port.ShouldBe(0);
+ }
+
+ ///
+ /// Tests MaybeReadPidFile returns original string when file doesn't exist.
+ ///
+ [Fact] // T:2585
+ public void MaybeReadPidFile_NonExistent_ShouldReturnOriginalString()
+ {
+ ServerOptions.MaybeReadPidFile("12345").ShouldBe("12345");
+ }
+
+ ///
+ /// Tests that MergeOptions handles null inputs.
+ ///
+ [Fact]
+ public void MergeOptions_NullInputs_ShouldReturnNonNull()
+ {
+ var result = ServerOptions.MergeOptions(null, null);
+ result.ShouldNotBeNull();
+
+ var fileOpts = new ServerOptions { Port = 1234 };
+ var r1 = ServerOptions.MergeOptions(fileOpts, null);
+ r1.Port.ShouldBe(1234);
+
+ var flagOpts = new ServerOptions { Port = 5678 };
+ var r2 = ServerOptions.MergeOptions(null, flagOpts);
+ r2.Port.ShouldBe(5678);
+ }
+}
diff --git a/porting.db b/porting.db
index 2e12d22..3c7bf3a 100644
Binary files a/porting.db and b/porting.db differ
diff --git a/reports/current.md b/reports/current.md
index b385c39..9062e7b 100644
--- a/reports/current.md
+++ b/reports/current.md
@@ -1,6 +1,6 @@
# NATS .NET Porting Status Report
-Generated: 2026-02-26 14:39:36 UTC
+Generated: 2026-02-26 16:51:01 UTC
## Modules (12 total)
@@ -13,17 +13,19 @@ Generated: 2026-02-26 14:39:36 UTC
| Status | Count |
|--------|-------|
-| complete | 328 |
-| n_a | 79 |
-| not_started | 3266 |
+| complete | 344 |
+| n_a | 82 |
+| not_started | 3180 |
+| stub | 67 |
## Unit Tests (3257 total)
| Status | Count |
|--------|-------|
-| complete | 139 |
+| complete | 148 |
| n_a | 49 |
-| not_started | 3069 |
+| not_started | 2980 |
+| stub | 80 |
## Library Mappings (36 total)
@@ -34,4 +36,4 @@ Generated: 2026-02-26 14:39:36 UTC
## Overall Progress
-**606/6942 items complete (8.7%)**
+**634/6942 items complete (9.1%)**
diff --git a/reports/report_11c0b92.md b/reports/report_11c0b92.md
new file mode 100644
index 0000000..9062e7b
--- /dev/null
+++ b/reports/report_11c0b92.md
@@ -0,0 +1,39 @@
+# NATS .NET Porting Status Report
+
+Generated: 2026-02-26 16:51:01 UTC
+
+## Modules (12 total)
+
+| Status | Count |
+|--------|-------|
+| complete | 11 |
+| not_started | 1 |
+
+## Features (3673 total)
+
+| Status | Count |
+|--------|-------|
+| complete | 344 |
+| n_a | 82 |
+| not_started | 3180 |
+| stub | 67 |
+
+## Unit Tests (3257 total)
+
+| Status | Count |
+|--------|-------|
+| complete | 148 |
+| n_a | 49 |
+| not_started | 2980 |
+| stub | 80 |
+
+## Library Mappings (36 total)
+
+| Status | Count |
+|--------|-------|
+| mapped | 36 |
+
+
+## Overall Progress
+
+**634/6942 items complete (9.1%)**