feat(wcf): EventReadConnectionModeOverride + cross-platform/bounded C2 spike
Live investigation (direct from a VPN host to the 2023 R2 historian's real WCF port) showed the certificate transport + NegotiateAuthentication auth work cross-platform, and that the event-read chain needs the 0x501 event connection mode for CM_EVENT RegisterTags to succeed (0x402/0x401 fail). Even with registration succeeding over a window that has events, StartEventQuery returns a 0-row header and long-polls — the same server-side per-connection row gate proven for gRPC. Adds: EventReadConnectionModeOverride (diagnostic), and spike knobs — cross-platform cert gate, version-check bypass, per-call timeout, overall budget with phase-diagnostic dump, connection-mode override. Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
This commit is contained in:
@@ -36,9 +36,14 @@ public sealed class WcfEventReadSpikeTests
|
||||
public async Task WcfEventRead_DiagnosticDump_AgainstRemoteHistorian()
|
||||
{
|
||||
string? host = Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_HOST");
|
||||
if (string.IsNullOrWhiteSpace(host) || !OperatingSystem.IsWindows())
|
||||
bool certificate = string.Equals(
|
||||
Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_TRANSPORT"), "certificate", StringComparison.OrdinalIgnoreCase);
|
||||
// The certificate (TLS) transport and NegotiateAuthentication-based app auth are cross-platform;
|
||||
// the integrated (Windows SSPI transport-security) transport is Windows-only. Skip when the gate
|
||||
// is unconfigured, or when an integrated run is requested off Windows.
|
||||
if (string.IsNullOrWhiteSpace(host) || (!certificate && !OperatingSystem.IsWindows()))
|
||||
{
|
||||
return; // inert without the dedicated gate / off Windows
|
||||
return;
|
||||
}
|
||||
|
||||
int port = int.TryParse(Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_PORT"), out int p)
|
||||
@@ -50,8 +55,17 @@ public sealed class WcfEventReadSpikeTests
|
||||
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 bypassVersion = string.Equals(
|
||||
Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_NOVERSIONCHECK"), "1", StringComparison.Ordinal);
|
||||
int timeoutSec = int.TryParse(Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_TIMEOUT_SEC"), out int ts) && ts > 0
|
||||
? ts : 30;
|
||||
string? connModeRaw = Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_CONNMODE");
|
||||
uint? connModeOverride = null;
|
||||
if (!string.IsNullOrWhiteSpace(connModeRaw))
|
||||
{
|
||||
string hex = connModeRaw.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? connModeRaw[2..] : connModeRaw;
|
||||
if (uint.TryParse(hex, System.Globalization.NumberStyles.HexNumber, null, out uint cm)) { connModeOverride = cm; }
|
||||
}
|
||||
bool allowUntrusted = string.Equals(
|
||||
Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_ALLOW_UNTRUSTED"), "1", StringComparison.Ordinal);
|
||||
// Certificate transport carries no Windows transport credential (cert-validated TLS channel);
|
||||
@@ -71,22 +85,49 @@ public sealed class WcfEventReadSpikeTests
|
||||
ServerDnsIdentity = string.IsNullOrWhiteSpace(dnsId) ? null : dnsId,
|
||||
AllowUntrustedServerCertificate = allowUntrusted,
|
||||
ConnectViaAddress = string.IsNullOrWhiteSpace(via) ? null : via,
|
||||
VerifyServerInterfaceVersion = !bypassVersion,
|
||||
ConnectTimeout = TimeSpan.FromSeconds(timeoutSec),
|
||||
RequestTimeout = TimeSpan.FromSeconds(timeoutSec),
|
||||
EventReadConnectionModeOverride = connModeOverride,
|
||||
};
|
||||
|
||||
HistorianWcfEventOrchestrator orchestrator = new(options);
|
||||
DateTime endUtc = DateTime.UtcNow;
|
||||
DateTime startUtc = endUtc - TimeSpan.FromDays(days);
|
||||
|
||||
int budgetSec = int.TryParse(Environment.GetEnvironmentVariable("HISTORIAN_WCF_EVENT_BUDGET_SEC"), out int bgs) && bgs > 0
|
||||
? bgs : 60;
|
||||
|
||||
int observed = 0;
|
||||
bool hasFirstEvent = false;
|
||||
string outcome = "completed";
|
||||
try
|
||||
{
|
||||
await foreach (HistorianEvent evt in orchestrator.ReadEventsAsync(startUtc, endUtc, filter: null, CancellationToken.None))
|
||||
// Race the read against an overall budget. The chain is synchronous WCF, so a stuck call
|
||||
// can't be token-cancelled — but the orchestrator's static/instance diagnostics are set as it
|
||||
// progresses, so on a budget timeout we still dump them to see which phase (auth / CM_EVENT
|
||||
// registration / query) is hanging. The abandoned task keeps running in the background harness.
|
||||
using var budget = new CancellationTokenSource(TimeSpan.FromSeconds(budgetSec));
|
||||
Task<int> readTask = Task.Run(async () =>
|
||||
{
|
||||
observed++;
|
||||
hasFirstEvent = true;
|
||||
_ = evt; // event identity intentionally NOT logged (sanitized)
|
||||
int n = 0;
|
||||
await foreach (HistorianEvent evt in orchestrator.ReadEventsAsync(startUtc, endUtc, filter: null, budget.Token))
|
||||
{
|
||||
n++;
|
||||
_ = evt; // event identity intentionally NOT logged (sanitized)
|
||||
}
|
||||
return n;
|
||||
});
|
||||
|
||||
Task finished = await Task.WhenAny(readTask, Task.Delay(TimeSpan.FromSeconds(budgetSec + 5)));
|
||||
if (finished == readTask)
|
||||
{
|
||||
observed = await readTask;
|
||||
hasFirstEvent = observed > 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
outcome = $"TIMED OUT after {budgetSec}s (chain still running — see return codes for phase reached)";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -100,7 +141,7 @@ public sealed class WcfEventReadSpikeTests
|
||||
}
|
||||
|
||||
// Sanitized diagnostic dump — counts, native return codes, buffer lengths, sha256 ONLY.
|
||||
_output.WriteLine($"[C2 WCF spike] transport: {options.Transport} (integratedSec={integrated}, allowUntrusted={allowUntrusted})");
|
||||
_output.WriteLine($"[C2 WCF spike] transport: {options.Transport} (integratedSec={integrated}, allowUntrusted={allowUntrusted}, connMode={(connModeOverride.HasValue ? "0x" + connModeOverride.Value.ToString("X") : "default-0x402")})");
|
||||
_output.WriteLine($"[C2 WCF spike] outcome: {outcome}");
|
||||
_output.WriteLine($"[C2 WCF spike] events observed: {observed}");
|
||||
_output.WriteLine($"[C2 WCF spike] hasFirstEvent: {hasFirstEvent}");
|
||||
|
||||
Reference in New Issue
Block a user