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);