diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Ipc/TcpFrameServer.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Ipc/TcpFrameServer.cs
index 313f44c7..cdaa4919 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Ipc/TcpFrameServer.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Ipc/TcpFrameServer.cs
@@ -69,6 +69,7 @@ public sealed class TcpFrameServer : IDisposable
try { client = await _listener!.AcceptTcpClientAsync().ConfigureAwait(false); }
catch (ObjectDisposedException) when (linked.Token.IsCancellationRequested) { throw new OperationCanceledException(linked.Token); }
catch (InvalidOperationException) when (linked.Token.IsCancellationRequested) { throw new OperationCanceledException(linked.Token); }
+ catch (SocketException) when (linked.Token.IsCancellationRequested) { throw new OperationCanceledException(linked.Token); }
using (client)
{
@@ -107,6 +108,7 @@ public sealed class TcpFrameServer : IDisposable
await writer.WriteAsync(MessageKind.HelloAck,
new HelloAck { Accepted = false, RejectReason = $"major-version-mismatch-peer={hello.ProtocolMajor}-server={Hello.CurrentMajor}" },
linked.Token).ConfigureAwait(false);
+ _logger.Warning("Sidecar TCP Hello rejected: major mismatch peer={Peer} server={Server}", hello.ProtocolMajor, Hello.CurrentMajor);
return;
}
await writer.WriteAsync(MessageKind.HelloAck,
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/Ipc/TcpRoundTripTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/Ipc/TcpRoundTripTests.cs
index 1b95aa41..c405c01b 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/Ipc/TcpRoundTripTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/Ipc/TcpRoundTripTests.cs
@@ -97,7 +97,7 @@ public sealed class TcpRoundTripTests
userCertificateValidationCallback: (_, cert, _, _) =>
cert is not null &&
string.Equals(
- new X509Certificate2(cert).Thumbprint,
+ cert.GetCertHashString(),
expectedThumbprint,
StringComparison.OrdinalIgnoreCase));
await ssl.AuthenticateAsClientAsync("otopcua-historian-sidecar-test", clientCertificates: null,
@@ -172,6 +172,38 @@ public sealed class TcpRoundTripTests
await serverTask;
}
+ ///
+ /// TLS: when the client pins a wrong thumbprint the validation callback returns false,
+ /// causing to throw
+ /// before any Hello is exchanged.
+ ///
+ [Fact]
+ public async Task Tls_BadThumbprint_AuthenticationFails()
+ {
+ using var cts = new CancellationTokenSource(Timeout);
+ using var cert = MakeSelfSignedCert();
+ using var server = new TcpFrameServer(IPAddress.Loopback, 0, "shh", tlsCert: cert, Quiet);
+ var serverTask = server.RunOneConnectionAsync(new EchoHandler(), cts.Token);
+
+ using var client = new TcpClient();
+ await client.ConnectAsync(IPAddress.Loopback, server.BoundPort);
+
+ // Deliberately pin the wrong thumbprint — all zeros.
+ const string wrongThumbprint = "0000000000000000000000000000000000000000";
+ var ssl = new SslStream(client.GetStream(), leaveInnerStreamOpen: false,
+ userCertificateValidationCallback: (_, serverCert, _, _) =>
+ serverCert is not null &&
+ string.Equals(serverCert.GetCertHashString(), wrongThumbprint, StringComparison.OrdinalIgnoreCase));
+
+ await Should.ThrowAsync(async () =>
+ await ssl.AuthenticateAsClientAsync("otopcua-historian-sidecar-test", clientCertificates: null,
+ enabledSslProtocols: SslProtocols.Tls12, checkCertificateRevocation: false));
+
+ ssl.Dispose();
+ // Server will see the broken TLS handshake and end the connection; let it finish.
+ try { await serverTask; } catch { /* server may throw on the aborted TLS */ }
+ }
+
/// Bad secret: Hello is rejected with Accepted=false and the shared-secret-mismatch reason.
[Fact]
public async Task BadSecret_HelloRejected()