diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs
index 8918f0c..14291d3 100644
--- a/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs
@@ -1139,7 +1139,8 @@ public sealed partial class ClientConnection
internal void ProcessErr(string err) { /* TODO session 09 */ }
// features 442-443: removeSecretsFromTrace, redact
- internal static string RemoveSecretsFromTrace(string s) => s;
+ // Delegates to ServerLogging.RemoveSecretsFromTrace (the real implementation lives there).
+ internal static string RemoveSecretsFromTrace(string s) => ServerLogging.RemoveSecretsFromTrace(s);
internal static string Redact(string s) => s;
// feature 444: computeRTT
diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Internal/NatsLogger.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/NatsLogger.cs
index b547b2c..04ca274 100644
--- a/dotnet/src/ZB.MOM.NatsNet.Server/Internal/NatsLogger.cs
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/NatsLogger.cs
@@ -14,6 +14,7 @@
// Adapted from server/log.go in the NATS server Go source.
using System.Collections.Concurrent;
+using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
namespace ZB.MOM.NatsNet.Server.Internal;
@@ -156,6 +157,53 @@ public sealed class ServerLogging
var statement = string.Format(format, args);
Warnf("{0}", statement);
}
+
+ // ---- Trace sanitization ----
+ // Mirrors removeSecretsFromTrace / redact in server/client.go.
+ // passPat = `"?\s*pass\S*?"?\s*[:=]\s*"?(([^",\r\n}])*)` — captures the value of any pass/password field.
+ // tokenPat = `"?\s*auth_token\S*?"?\s*[:=]\s*"?(([^",\r\n}])*)` — captures auth_token value.
+ // Only the FIRST match is redacted (mirrors the Go break-after-first-match behaviour).
+
+ // Go: "?\s*pass\S*?"?\s*[:=]\s*"?(([^",\r\n}])*)
+ private static readonly Regex s_passPattern = new(
+ @"""?\s*pass\S*?""?\s*[:=]\s*""?(([^"",\r\n}])*)",
+ RegexOptions.Compiled);
+
+ // Go: "?\s*auth_token\S*?"?\s*[:=]\s*"?(([^",\r\n}])*)
+ private static readonly Regex s_authTokenPattern = new(
+ @"""?\s*auth_token\S*?""?\s*[:=]\s*""?(([^"",\r\n}])*)",
+ RegexOptions.Compiled);
+
+ ///
+ /// Removes passwords from a protocol trace string.
+ /// Mirrors removeSecretsFromTrace in client.go (pass step).
+ /// Only the first occurrence is redacted.
+ ///
+ public static string RemovePassFromTrace(string s)
+ => RedactFirst(s_passPattern, s);
+
+ ///
+ /// Removes auth_token from a protocol trace string.
+ /// Mirrors removeSecretsFromTrace in client.go (auth_token step).
+ /// Only the first occurrence is redacted.
+ ///
+ public static string RemoveAuthTokenFromTrace(string s)
+ => RedactFirst(s_authTokenPattern, s);
+
+ ///
+ /// Removes both passwords and auth tokens from a protocol trace string.
+ /// Mirrors removeSecretsFromTrace in client.go.
+ ///
+ public static string RemoveSecretsFromTrace(string s)
+ => RemoveAuthTokenFromTrace(RemovePassFromTrace(s));
+
+ private static string RedactFirst(Regex pattern, string s)
+ {
+ var m = pattern.Match(s);
+ if (!m.Success) return s;
+ var cap = m.Groups[1]; // captured value substring
+ return string.Concat(s.AsSpan(0, cap.Index), "[REDACTED]", s.AsSpan(cap.Index + cap.Length));
+ }
}
///
diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ServerLoggerTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ServerLoggerTests.cs
new file mode 100644
index 0000000..86d0681
--- /dev/null
+++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ServerLoggerTests.cs
@@ -0,0 +1,138 @@
+// 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.
+
+using Shouldly;
+using ZB.MOM.NatsNet.Server.Internal;
+
+namespace ZB.MOM.NatsNet.Server.Tests.Internal;
+
+///
+/// Tests for server logging trace sanitization (RemovePassFromTrace, RemoveAuthTokenFromTrace).
+/// Mirrors server/log_test.go — TestNoPasswordsFromConnectTrace, TestRemovePassFromTrace,
+/// TestRemoveAuthTokenFromTrace.
+///
+public class ServerLoggerTests
+{
+ // ---------------------------------------------------------------------------
+ // T:2020 — TestNoPasswordsFromConnectTrace
+ // ---------------------------------------------------------------------------
+
+ ///
+ /// Mirrors TestNoPasswordsFromConnectTrace.
+ /// Verifies that a CONNECT trace with a password or auth_token does not
+ /// expose the secret value after sanitization.
+ ///
+ [Fact] // T:2020
+ public void NoPasswordsFromConnectTrace_ShouldSucceed()
+ {
+ const string connectWithPass =
+ """CONNECT {"verbose":false,"pedantic":false,"user":"derek","pass":"s3cr3t","tls_required":false}""";
+ const string connectWithToken =
+ """CONNECT {"verbose":false,"auth_token":"secret-token","tls_required":false}""";
+
+ ServerLogging.RemovePassFromTrace(connectWithPass).ShouldNotContain("s3cr3t");
+ ServerLogging.RemoveAuthTokenFromTrace(connectWithToken).ShouldNotContain("secret-token");
+ }
+
+ // ---------------------------------------------------------------------------
+ // T:2021 — TestRemovePassFromTrace
+ // ---------------------------------------------------------------------------
+
+ ///
+ /// Mirrors TestRemovePassFromTrace — covers all test vectors from log_test.go.
+ /// Each case verifies that RemovePassFromTrace redacts the first pass/password value
+ /// with [REDACTED] while leaving other fields intact.
+ ///
+ [Theory] // T:2021
+ [InlineData(
+ "user and pass",
+ "CONNECT {\"user\":\"derek\",\"pass\":\"s3cr3t\"}\r\n",
+ "CONNECT {\"user\":\"derek\",\"pass\":\"[REDACTED]\"}\r\n")]
+ [InlineData(
+ "user and pass extra space",
+ "CONNECT {\"user\":\"derek\",\"pass\": \"s3cr3t\"}\r\n",
+ "CONNECT {\"user\":\"derek\",\"pass\": \"[REDACTED]\"}\r\n")]
+ [InlineData(
+ "user and pass is empty",
+ "CONNECT {\"user\":\"derek\",\"pass\":\"\"}\r\n",
+ "CONNECT {\"user\":\"derek\",\"pass\":\"[REDACTED]\"}\r\n")]
+ [InlineData(
+ "user and pass is empty whitespace",
+ "CONNECT {\"user\":\"derek\",\"pass\":\" \"}\r\n",
+ "CONNECT {\"user\":\"derek\",\"pass\":\"[REDACTED]\"}\r\n")]
+ [InlineData(
+ "only pass",
+ "CONNECT {\"pass\":\"s3cr3t\",}\r\n",
+ "CONNECT {\"pass\":\"[REDACTED]\",}\r\n")]
+ [InlineData(
+ "complete connect",
+ "CONNECT {\"echo\":true,\"verbose\":false,\"pedantic\":false,\"user\":\"foo\",\"pass\":\"s3cr3t\",\"tls_required\":false,\"name\":\"APM7JU94z77YzP6WTBEiuw\"}\r\n",
+ "CONNECT {\"echo\":true,\"verbose\":false,\"pedantic\":false,\"user\":\"foo\",\"pass\":\"[REDACTED]\",\"tls_required\":false,\"name\":\"APM7JU94z77YzP6WTBEiuw\"}\r\n")]
+ [InlineData(
+ "user and pass are filtered",
+ "CONNECT {\"user\":\"s3cr3t\",\"pass\":\"s3cr3t\"}\r\n",
+ "CONNECT {\"user\":\"s3cr3t\",\"pass\":\"[REDACTED]\"}\r\n")]
+ [InlineData(
+ "single long password",
+ "CONNECT {\"pass\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"}\r\n",
+ "CONNECT {\"pass\":\"[REDACTED]\"}\r\n")]
+ public void RemovePassFromTrace_ShouldSucceed(string name, string input, string expected)
+ {
+ _ = name; // used for test display only
+ ServerLogging.RemovePassFromTrace(input).ShouldBe(expected);
+ }
+
+ // ---------------------------------------------------------------------------
+ // T:2022 — TestRemoveAuthTokenFromTrace
+ // ---------------------------------------------------------------------------
+
+ ///
+ /// Mirrors TestRemoveAuthTokenFromTrace — covers representative test vectors
+ /// from log_test.go. Each case verifies that RemoveAuthTokenFromTrace redacts
+ /// the first auth_token value with [REDACTED].
+ ///
+ [Theory] // T:2022
+ [InlineData(
+ "user and auth_token",
+ "CONNECT {\"user\":\"derek\",\"auth_token\":\"s3cr3t\"}\r\n",
+ "CONNECT {\"user\":\"derek\",\"auth_token\":\"[REDACTED]\"}\r\n")]
+ [InlineData(
+ "user and auth_token extra space",
+ "CONNECT {\"user\":\"derek\",\"auth_token\": \"s3cr3t\"}\r\n",
+ "CONNECT {\"user\":\"derek\",\"auth_token\": \"[REDACTED]\"}\r\n")]
+ [InlineData(
+ "user and auth_token is empty",
+ "CONNECT {\"user\":\"derek\",\"auth_token\":\"\"}\r\n",
+ "CONNECT {\"user\":\"derek\",\"auth_token\":\"[REDACTED]\"}\r\n")]
+ [InlineData(
+ "only auth_token",
+ "CONNECT {\"auth_token\":\"s3cr3t\",}\r\n",
+ "CONNECT {\"auth_token\":\"[REDACTED]\",}\r\n")]
+ [InlineData(
+ "complete connect",
+ "CONNECT {\"echo\":true,\"verbose\":false,\"pedantic\":false,\"auth_token\":\"s3cr3t\",\"tls_required\":false,\"name\":\"APM7JU94z77YzP6WTBEiuw\"}\r\n",
+ "CONNECT {\"echo\":true,\"verbose\":false,\"pedantic\":false,\"auth_token\":\"[REDACTED]\",\"tls_required\":false,\"name\":\"APM7JU94z77YzP6WTBEiuw\"}\r\n")]
+ [InlineData(
+ "user and token are filtered",
+ "CONNECT {\"user\":\"s3cr3t\",\"auth_token\":\"s3cr3t\"}\r\n",
+ "CONNECT {\"user\":\"s3cr3t\",\"auth_token\":\"[REDACTED]\"}\r\n")]
+ [InlineData(
+ "single long token",
+ "CONNECT {\"auth_token\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"}\r\n",
+ "CONNECT {\"auth_token\":\"[REDACTED]\"}\r\n")]
+ public void RemoveAuthTokenFromTrace_ShouldSucceed(string name, string input, string expected)
+ {
+ _ = name; // used for test display only
+ ServerLogging.RemoveAuthTokenFromTrace(input).ShouldBe(expected);
+ }
+}
diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/SignalHandlerTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/SignalHandlerTests.cs
index cfc3400..a9ad351 100644
--- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/SignalHandlerTests.cs
+++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/SignalHandlerTests.cs
@@ -67,4 +67,100 @@ public class SignalHandlerTests
var err = SignalHandler.ProcessSignal(ServerCommand.Stop, "not-a-pid");
err.ShouldNotBeNull();
}
+
+ // ---------------------------------------------------------------------------
+ // Tests ported from server/signal_test.go
+ // ---------------------------------------------------------------------------
+
+ ///
+ /// Mirrors TestProcessSignalInvalidCommand.
+ /// An out-of-range ServerCommand enum value is treated as an unknown signal
+ /// and ProcessSignal returns a non-null error.
+ ///
+ [Fact] // T:2919
+ public void ProcessSignalInvalidCommand_ShouldSucceed()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return; // Skip on Windows
+
+ var err = SignalHandler.ProcessSignal((ServerCommand)99, "123");
+ err.ShouldNotBeNull();
+ }
+
+ ///
+ /// Mirrors TestProcessSignalInvalidPid.
+ /// A non-numeric PID string returns an error containing "invalid pid".
+ ///
+ [Fact] // T:2920
+ public void ProcessSignalInvalidPid_ShouldSucceed()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return; // Skip on Windows
+
+ var err = SignalHandler.ProcessSignal(ServerCommand.Stop, "abc");
+ err.ShouldNotBeNull();
+ err!.Message.ShouldContain("invalid pid");
+ }
+
+ // ---------------------------------------------------------------------------
+ // Deferred signal tests — require pgrep/kill injection or real OS process spawning.
+ // These cannot be unit-tested without refactoring SignalHandler to accept
+ // injectable pgrep/kill delegates (as the Go source does).
+ // ---------------------------------------------------------------------------
+
+ /// Mirrors TestProcessSignalMultipleProcesses — deferred: requires pgrep injection.
+ [Fact(Skip = "deferred: requires pgrep/kill injection")] // T:2913
+ public void ProcessSignalMultipleProcesses_ShouldSucceed() { }
+
+ /// Mirrors TestProcessSignalMultipleProcessesGlob — deferred: requires pgrep injection.
+ [Fact(Skip = "deferred: requires pgrep/kill injection")] // T:2914
+ public void ProcessSignalMultipleProcessesGlob_ShouldSucceed() { }
+
+ /// Mirrors TestProcessSignalMultipleProcessesGlobPartial — deferred: requires pgrep injection.
+ [Fact(Skip = "deferred: requires pgrep/kill injection")] // T:2915
+ public void ProcessSignalMultipleProcessesGlobPartial_ShouldSucceed() { }
+
+ /// Mirrors TestProcessSignalPgrepError — deferred: requires pgrep injection.
+ [Fact(Skip = "deferred: requires pgrep injection")] // T:2916
+ public void ProcessSignalPgrepError_ShouldSucceed() { }
+
+ /// Mirrors TestProcessSignalPgrepMangled — deferred: requires pgrep injection.
+ [Fact(Skip = "deferred: requires pgrep injection")] // T:2917
+ public void ProcessSignalPgrepMangled_ShouldSucceed() { }
+
+ /// Mirrors TestProcessSignalResolveSingleProcess — deferred: requires pgrep and kill injection.
+ [Fact(Skip = "deferred: requires pgrep/kill injection")] // T:2918
+ public void ProcessSignalResolveSingleProcess_ShouldSucceed() { }
+
+ /// Mirrors TestProcessSignalQuitProcess — deferred: requires kill injection.
+ [Fact(Skip = "deferred: requires kill injection")] // T:2921
+ public void ProcessSignalQuitProcess_ShouldSucceed() { }
+
+ /// Mirrors TestProcessSignalTermProcess — deferred: requires kill injection and commandTerm equivalent.
+ [Fact(Skip = "deferred: requires kill injection")] // T:2922
+ public void ProcessSignalTermProcess_ShouldSucceed() { }
+
+ /// Mirrors TestProcessSignalReopenProcess — deferred: requires kill injection.
+ [Fact(Skip = "deferred: requires kill injection")] // T:2923
+ public void ProcessSignalReopenProcess_ShouldSucceed() { }
+
+ /// Mirrors TestProcessSignalReloadProcess — deferred: requires kill injection.
+ [Fact(Skip = "deferred: requires kill injection")] // T:2924
+ public void ProcessSignalReloadProcess_ShouldSucceed() { }
+
+ /// Mirrors TestProcessSignalLameDuckMode — deferred: requires kill injection and commandLDMode equivalent.
+ [Fact(Skip = "deferred: requires kill injection")] // T:2925
+ public void ProcessSignalLameDuckMode_ShouldSucceed() { }
+
+ /// Mirrors TestProcessSignalTermDuringLameDuckMode — deferred: requires full server (RunServer) and real OS signal.
+ [Fact(Skip = "deferred: requires RunServer and real OS SIGTERM")] // T:2926
+ public void ProcessSignalTermDuringLameDuckMode_ShouldSucceed() { }
+
+ /// Mirrors TestSignalInterruptHasSuccessfulExit — deferred: requires spawning a subprocess to test exit code on SIGINT.
+ [Fact(Skip = "deferred: requires subprocess process spawning")] // T:2927
+ public void SignalInterruptHasSuccessfulExit_ShouldSucceed() { }
+
+ /// Mirrors TestSignalTermHasSuccessfulExit — deferred: requires spawning a subprocess to test exit code on SIGTERM.
+ [Fact(Skip = "deferred: requires subprocess process spawning")] // T:2928
+ public void SignalTermHasSuccessfulExit_ShouldSucceed() { }
}
diff --git a/porting.db b/porting.db
index 4e1ba21..70f16ac 100644
Binary files a/porting.db and b/porting.db differ
diff --git a/reports/current.md b/reports/current.md
index 28f7551..bf4d42e 100644
--- a/reports/current.md
+++ b/reports/current.md
@@ -1,6 +1,6 @@
# NATS .NET Porting Status Report
-Generated: 2026-02-27 00:07:45 UTC
+Generated: 2026-02-27 00:15:57 UTC
## Modules (12 total)
@@ -21,11 +21,10 @@ Generated: 2026-02-27 00:07:45 UTC
| Status | Count |
|--------|-------|
-| complete | 209 |
-| deferred | 201 |
+| complete | 214 |
+| deferred | 215 |
| n_a | 187 |
| not_started | 2527 |
-| stub | 19 |
| verified | 114 |
## Library Mappings (36 total)
@@ -37,4 +36,4 @@ Generated: 2026-02-27 00:07:45 UTC
## Overall Progress
-**4194/6942 items complete (60.4%)**
+**4199/6942 items complete (60.5%)**
diff --git a/reports/report_364329c.md b/reports/report_364329c.md
new file mode 100644
index 0000000..bf4d42e
--- /dev/null
+++ b/reports/report_364329c.md
@@ -0,0 +1,39 @@
+# NATS .NET Porting Status Report
+
+Generated: 2026-02-27 00:15:57 UTC
+
+## Modules (12 total)
+
+| Status | Count |
+|--------|-------|
+| not_started | 1 |
+| verified | 11 |
+
+## Features (3673 total)
+
+| Status | Count |
+|--------|-------|
+| complete | 3368 |
+| n_a | 26 |
+| verified | 279 |
+
+## Unit Tests (3257 total)
+
+| Status | Count |
+|--------|-------|
+| complete | 214 |
+| deferred | 215 |
+| n_a | 187 |
+| not_started | 2527 |
+| verified | 114 |
+
+## Library Mappings (36 total)
+
+| Status | Count |
+|--------|-------|
+| mapped | 36 |
+
+
+## Overall Progress
+
+**4199/6942 items complete (60.5%)**