From 954b9cc9cc97c786704b600f5a4aac639d2b6baf Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 25 Jun 2026 20:35:46 -0400 Subject: [PATCH] feat(wcf): add ConnectViaAddress (WCF Via) for tunneled historian access + wire into C2 spike When the historian is reached through a port-forward whose local port differs from the server's real service port, WCF's server-side AddressFilter rejects the message (To = tunnel port != server port). ConnectViaAddress lets the channel connect to the tunnel while addressing the SOAP To the real Host/Port endpoint. Applied in HistorianWcfClientCredentialsHelper.Configure (the critical event factories already call it). The C2 spike reads HISTORIAN_WCF_EVENT_VIA. Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii --- src/AVEVA.Historian.Client/HistorianClientOptions.cs | 11 +++++++++++ .../Wcf/HistorianWcfClientCredentialsHelper.cs | 9 +++++++++ .../WcfEventReadSpikeTests.cs | 2 ++ 3 files changed, 22 insertions(+) diff --git a/src/AVEVA.Historian.Client/HistorianClientOptions.cs b/src/AVEVA.Historian.Client/HistorianClientOptions.cs index 23fc53e..29908b4 100644 --- a/src/AVEVA.Historian.Client/HistorianClientOptions.cs +++ b/src/AVEVA.Historian.Client/HistorianClientOptions.cs @@ -53,6 +53,17 @@ public sealed class HistorianClientOptions /// public string? ServerDnsIdentity { get; init; } + /// + /// Optional WCF "Via" address (e.g. net.tcp://host:42568). When set, the SDK's WCF + /// channel factories connect to this address while still addressing the SOAP message + /// To the logical endpoint built from /. Use this when + /// the Historian is reached through a port-forwarding tunnel or proxy whose local port differs + /// from the server's real service port: point / at the + /// server's real endpoint (so the server's WCF AddressFilter matches) and set this to the tunnel + /// endpoint. Has no effect on the gRPC transport. Default null (connect == address). + /// + public string? ConnectViaAddress { get; init; } + /// /// For : when true the channel uses TLS /// (https://); when false it uses plaintext (http://). Matches the stock diff --git a/src/AVEVA.Historian.Client/Wcf/HistorianWcfClientCredentialsHelper.cs b/src/AVEVA.Historian.Client/Wcf/HistorianWcfClientCredentialsHelper.cs index 8759671..b8689f5 100644 --- a/src/AVEVA.Historian.Client/Wcf/HistorianWcfClientCredentialsHelper.cs +++ b/src/AVEVA.Historian.Client/Wcf/HistorianWcfClientCredentialsHelper.cs @@ -2,6 +2,7 @@ using System.IdentityModel.Selectors; using System.IdentityModel.Tokens; using System.Security.Cryptography.X509Certificates; using System.ServiceModel; +using System.ServiceModel.Description; using System.ServiceModel.Security; namespace AVEVA.Historian.Client.Wcf; @@ -28,6 +29,14 @@ internal static class HistorianWcfClientCredentialsHelper RevocationMode = X509RevocationMode.NoCheck, }; } + + // Tunnel/proxy support: connect to the Via address while still addressing the message To the + // logical endpoint (Host/Port). Lets a port-forward whose local port differs from the server's + // real service port satisfy the server-side WCF AddressFilter (which checks the To header). + if (!string.IsNullOrWhiteSpace(options.ConnectViaAddress)) + { + factory.Endpoint.EndpointBehaviors.Add(new ClientViaBehavior(new Uri(options.ConnectViaAddress))); + } } private sealed class AcceptAnyCertificateValidator : X509CertificateValidator diff --git a/tests/AVEVA.Historian.Client.Tests/WcfEventReadSpikeTests.cs b/tests/AVEVA.Historian.Client.Tests/WcfEventReadSpikeTests.cs index 6a06d31..9e22ce8 100644 --- a/tests/AVEVA.Historian.Client.Tests/WcfEventReadSpikeTests.cs +++ b/tests/AVEVA.Historian.Client.Tests/WcfEventReadSpikeTests.cs @@ -49,6 +49,7 @@ public sealed class WcfEventReadSpikeTests string? password = Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_PASSWORD"); string? spn = Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_SPN"); string? dnsId = Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_DNSID"); + string? via = Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_VIA"); bool certificate = string.Equals( Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_TRANSPORT"), "certificate", StringComparison.OrdinalIgnoreCase); bool allowUntrusted = string.Equals( @@ -69,6 +70,7 @@ public sealed class WcfEventReadSpikeTests TargetSpn = string.IsNullOrWhiteSpace(spn) ? "NT SERVICE\\aahClientAccessPoint" : spn, ServerDnsIdentity = string.IsNullOrWhiteSpace(dnsId) ? null : dnsId, AllowUntrustedServerCertificate = allowUntrusted, + ConnectViaAddress = string.IsNullOrWhiteSpace(via) ? null : via, }; HistorianWcfEventOrchestrator orchestrator = new(options);