fix(opcuaclient): never pass checkDomain for a None endpoint; document both SDK check sites (#816 review)

SDK 2.0.1 checks the server host name in TWO places: Session.OpenAsync (after
the chain is trusted) and DefaultSessionFactory.CreateChannelAsync, against the
certificate in the GetEndpoints reply and before any security-mode test. opc-plc
sends a certificate on its None endpoint, so `connect -u
opc.tcp://10.100.0.35:50000 --validate-host-name` was refused on an
unauthenticated channel, where a host check proves nothing.

All three call sites now pass checkDomain only when the selected endpoint's
SecurityMode != None: OpcUaEndpointSelection.CheckDomainFor for the driver
(OpenSessionOnEndpointAsync, so the failover sweep too) and the AdminUI browser;
the same expression in Client.Shared ConnectToEndpointAsync (shared by
failover). New live tests: driver None + flag on vs :50000 reads; browser None +
flag on vs the NAT fixture browses; the secure + flag refusals still hold.

Docs: name both check sites; a zone-qualified IPv6 host never matches (FindDomain
compares the zone too), so link-local needs validateServerHostName=false; the
match is loose (first label alone, subject DC= parts count, no wildcards), which
caveats the CA recommendation.
This commit is contained in:
Joseph Doherty
2026-11-11 01:53:01 -05:00
parent ae91e6b7f7
commit 9aff90f884
15 changed files with 145 additions and 33 deletions
@@ -56,21 +56,28 @@ public class OpcUaClientServiceTests : IDisposable
}
/// <summary>
/// Gitea #816: the host-name check reaches the session factory (the SDK's checkDomain) — off by default, the
/// pre-#816 behaviour — and survives the per-endpoint settings copy the connect path makes.
/// Gitea #816: the host-name check reaches the session factory (the SDK's checkDomain) on a SECURED endpoint — off
/// by default, the pre-#816 behaviour — and survives the per-endpoint settings copy the connect path makes. On a
/// None endpoint it is never passed: SDK 2.0.1 would check the GetEndpoints certificate at channel creation even
/// there (review of #816, reproduced against opc-plc).
/// </summary>
[Theory]
[InlineData(null)]
[InlineData(true)]
[InlineData(false)]
public async Task ConnectAsync_threads_ValidateServerHostName_to_the_session(bool? validate)
[InlineData(SecurityMode.Sign, null, false)]
[InlineData(SecurityMode.Sign, true, true)]
[InlineData(SecurityMode.SignAndEncrypt, true, true)]
[InlineData(SecurityMode.Sign, false, false)]
[InlineData(SecurityMode.None, true, false)]
[InlineData(SecurityMode.None, null, false)]
public async Task ConnectAsync_threads_ValidateServerHostName_to_secured_sessions_only(
SecurityMode mode, bool? validate, bool expectedCheckDomain)
{
var settings = ValidSettings();
settings.SecurityMode = mode;
if (validate is { } v) settings.ValidateServerHostName = v;
await _service.ConnectAsync(settings, TestContext.Current.CancellationToken);
_sessionFactory.LastValidateServerHostName.ShouldBe(validate ?? false);
_sessionFactory.LastValidateServerHostName.ShouldBe(expectedCheckDomain);
_configFactory.LastSettings!.ValidateServerHostName.ShouldBe(validate ?? false);
}