feat(wcf): C2 spike + ConnectViaAddress/connmode — WCF transport viable, rows server-gated #1

Merged
dohertj2 merged 16 commits from feat/c2-wcf-event-spike into main 2026-06-26 06:48:03 -04:00
3 changed files with 22 additions and 0 deletions
Showing only changes of commit 954b9cc9cc - Show all commits
@@ -53,6 +53,17 @@ public sealed class HistorianClientOptions
/// </summary> /// </summary>
public string? ServerDnsIdentity { get; init; } public string? ServerDnsIdentity { get; init; }
/// <summary>
/// Optional WCF "Via" address (e.g. <c>net.tcp://host:42568</c>). When set, the SDK's WCF
/// channel factories <b>connect</b> to this address while still addressing the SOAP message
/// <c>To</c> the logical endpoint built from <see cref="Host"/>/<see cref="Port"/>. 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 <see cref="Host"/>/<see cref="Port"/> 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).
/// </summary>
public string? ConnectViaAddress { get; init; }
/// <summary> /// <summary>
/// For <see cref="HistorianTransport.RemoteGrpc"/>: when true the channel uses TLS /// For <see cref="HistorianTransport.RemoteGrpc"/>: when true the channel uses TLS
/// (<c>https://</c>); when false it uses plaintext (<c>http://</c>). Matches the stock /// (<c>https://</c>); when false it uses plaintext (<c>http://</c>). Matches the stock
@@ -2,6 +2,7 @@ using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens; using System.IdentityModel.Tokens;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.ServiceModel; using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Security; using System.ServiceModel.Security;
namespace AVEVA.Historian.Client.Wcf; namespace AVEVA.Historian.Client.Wcf;
@@ -28,6 +29,14 @@ internal static class HistorianWcfClientCredentialsHelper
RevocationMode = X509RevocationMode.NoCheck, 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 private sealed class AcceptAnyCertificateValidator : X509CertificateValidator
@@ -49,6 +49,7 @@ public sealed class WcfEventReadSpikeTests
string? password = Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_PASSWORD"); string? password = Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_PASSWORD");
string? spn = Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_SPN"); string? spn = Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_SPN");
string? dnsId = Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_DNSID"); string? dnsId = Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_DNSID");
string? via = Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_VIA");
bool certificate = string.Equals( bool certificate = string.Equals(
Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_TRANSPORT"), "certificate", StringComparison.OrdinalIgnoreCase); Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_TRANSPORT"), "certificate", StringComparison.OrdinalIgnoreCase);
bool allowUntrusted = string.Equals( bool allowUntrusted = string.Equals(
@@ -69,6 +70,7 @@ public sealed class WcfEventReadSpikeTests
TargetSpn = string.IsNullOrWhiteSpace(spn) ? "NT SERVICE\\aahClientAccessPoint" : spn, TargetSpn = string.IsNullOrWhiteSpace(spn) ? "NT SERVICE\\aahClientAccessPoint" : spn,
ServerDnsIdentity = string.IsNullOrWhiteSpace(dnsId) ? null : dnsId, ServerDnsIdentity = string.IsNullOrWhiteSpace(dnsId) ? null : dnsId,
AllowUntrustedServerCertificate = allowUntrusted, AllowUntrustedServerCertificate = allowUntrusted,
ConnectViaAddress = string.IsNullOrWhiteSpace(via) ? null : via,
}; };
HistorianWcfEventOrchestrator orchestrator = new(options); HistorianWcfEventOrchestrator orchestrator = new(options);