378962eeb1
The AdminUI OPC UA browse picker connected to the EndpointUrl the server advertised in GetEndpoints verbatim, so behind NAT / a container port publish it dialled the server's private hostname + internal port and failed DNS where the runtime driver (which rewrites per #676) connected. - Move the rewrite to the shared OpcUaEndpointUrlRewriter (.Contracts, pure Uri logic, no SDK dependency) and the policy+mode selection to OpcUaEndpointSelection (.Session leaf). Driver and browser both select through it, so the browse connects exactly where the driver does. - Policy/mode filter, server certificate and strict browse PKI (no auto-accept) are unchanged. - Unit tests: rewrite (host/port differ, same, IPv6 both sides, path kept, port-less, scheme mismatch untouched), browser selection seam. - Live NAT tests (Fixture=opc-plc-nat, OPCUA_NAT_ENDPOINT): browse via a published port with a premise check that the advertised host:port differs and does not resolve, nsu= round-trip, unreachable endpoint fails cleanly, SignAndEncrypt reaches the server and is refused BadCertificateUntrusted. Verified red without the rewrite. Refs #806
109 lines
5.2 KiB
C#
109 lines
5.2 KiB
C#
using System.Security.Cryptography;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using Opc.Ua;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests;
|
|
|
|
/// <summary>
|
|
/// Gitea #806 — the AdminUI browse picker used the endpoint URL the server ADVERTISED in GetEndpoints verbatim, so
|
|
/// behind NAT / a container port publish it dialled the server's private hostname + internal port and failed DNS
|
|
/// (<c>opc.tcp://otopcua-sdk1-browser-nat-parity:50000/</c>) where the runtime driver, which rewrites to the
|
|
/// configured host/port, connected. The browser now selects through the driver's shared
|
|
/// <see cref="OpcUaEndpointSelection.SelectMatching"/>; these tests pin the browse seam's behaviour, including that
|
|
/// the security choice is not altered by the rewrite.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class OpcUaClientBrowserEndpointSelectionTests
|
|
{
|
|
private static EndpointDescription Ep(string url, MessageSecurityMode mode, string policyUri, byte[]? cert = null) =>
|
|
new()
|
|
{
|
|
EndpointUrl = url, SecurityMode = mode, SecurityPolicyUri = policyUri,
|
|
ServerCertificate = cert is null ? default : new ByteString(cert),
|
|
};
|
|
|
|
[Fact]
|
|
public void Advertised_private_host_and_port_are_replaced_by_the_configured_ones()
|
|
{
|
|
var advertised = new[] { Ep("opc.tcp://otopcua-sdk1-browser-nat-parity:50000/", MessageSecurityMode.None, SecurityPolicies.None) };
|
|
|
|
var picked = OpcUaClientDriverBrowser.SelectBrowseEndpoint(
|
|
advertised, "opc.tcp://127.0.0.1:32772", OpcUaSecurityPolicy.None, OpcUaSecurityMode.None);
|
|
|
|
picked.EndpointUrl.ShouldBe("opc.tcp://127.0.0.1:32772/");
|
|
}
|
|
|
|
[Fact]
|
|
public void The_advertised_path_is_kept()
|
|
{
|
|
var advertised = new[] { Ep("opc.tcp://internal:4840/Quickstarts/ReferenceServer", MessageSecurityMode.None, SecurityPolicies.None) };
|
|
|
|
OpcUaClientDriverBrowser.SelectBrowseEndpoint(
|
|
advertised, "opc.tcp://gateway.example:14840/", OpcUaSecurityPolicy.None, OpcUaSecurityMode.None)
|
|
.EndpointUrl.ShouldBe("opc.tcp://gateway.example:14840/Quickstarts/ReferenceServer");
|
|
}
|
|
|
|
[Fact]
|
|
public void The_browser_and_the_driver_choose_the_same_url()
|
|
{
|
|
const string advertisedUrl = "opc.tcp://internal-host:4840/OtOpcUa";
|
|
const string configured = "opc.tcp://[fd00::1]:54840";
|
|
var picked = OpcUaClientDriverBrowser.SelectBrowseEndpoint(
|
|
[Ep(advertisedUrl, MessageSecurityMode.None, SecurityPolicies.None)], configured,
|
|
OpcUaSecurityPolicy.None, OpcUaSecurityMode.None);
|
|
|
|
picked.EndpointUrl.ShouldBe(OpcUaEndpointUrlRewriter.Rewrite(advertisedUrl, configured));
|
|
picked.EndpointUrl.ShouldBe("opc.tcp://[fd00::1]:54840/OtOpcUa");
|
|
}
|
|
|
|
[Fact]
|
|
public void Security_selection_and_server_certificate_are_unchanged_by_the_rewrite()
|
|
{
|
|
using var rsa = RSA.Create(2048);
|
|
using var cert = new CertificateRequest("CN=srv", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)
|
|
.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1));
|
|
var der = cert.RawData;
|
|
var advertised = new[]
|
|
{
|
|
Ep("opc.tcp://internal:50000/", MessageSecurityMode.None, SecurityPolicies.None),
|
|
Ep("opc.tcp://internal:50000/", MessageSecurityMode.Sign, SecurityPolicies.Basic256Sha256, der),
|
|
Ep("opc.tcp://internal:50000/", MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Basic128Rsa15, der),
|
|
Ep("opc.tcp://internal:50000/", MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Basic256Sha256, der),
|
|
};
|
|
|
|
var picked = OpcUaClientDriverBrowser.SelectBrowseEndpoint(
|
|
advertised, "opc.tcp://10.100.0.35:32772",
|
|
OpcUaSecurityPolicy.Basic256Sha256, OpcUaSecurityMode.SignAndEncrypt);
|
|
|
|
picked.ShouldBeSameAs(advertised[3]);
|
|
picked.SecurityPolicyUri.ShouldBe(SecurityPolicies.Basic256Sha256);
|
|
picked.SecurityMode.ShouldBe(MessageSecurityMode.SignAndEncrypt);
|
|
picked.ServerCertificate.ToArray().ShouldBe(der);
|
|
picked.EndpointUrl.ShouldBe("opc.tcp://10.100.0.35:32772/");
|
|
}
|
|
|
|
[Fact]
|
|
public void An_unadvertised_policy_mode_pair_still_fails_loudly()
|
|
{
|
|
var advertised = new[] { Ep("opc.tcp://internal:50000/", MessageSecurityMode.None, SecurityPolicies.None) };
|
|
|
|
var ex = Should.Throw<InvalidOperationException>(() => OpcUaClientDriverBrowser.SelectBrowseEndpoint(
|
|
advertised, "opc.tcp://127.0.0.1:32772", OpcUaSecurityPolicy.Basic256Sha256, OpcUaSecurityMode.SignAndEncrypt));
|
|
ex.Message.ShouldContain("Basic256Sha256");
|
|
ex.Message.ShouldContain("None/None");
|
|
}
|
|
|
|
[Fact]
|
|
public void A_different_transport_scheme_is_left_alone()
|
|
{
|
|
var advertised = new[] { Ep("https://internal:443/ua", MessageSecurityMode.None, SecurityPolicies.None) };
|
|
|
|
OpcUaClientDriverBrowser.SelectBrowseEndpoint(
|
|
advertised, "opc.tcp://127.0.0.1:32772", OpcUaSecurityPolicy.None, OpcUaSecurityMode.None)
|
|
.EndpointUrl.ShouldBe("https://internal:443/ua");
|
|
}
|
|
}
|