fix(dcl): rewrite advertised OPC UA endpoint host to the reachable one
An OPC UA server advertises its base address from its own config, not the route the client took — commonly a 0.0.0.0 wildcard bind or an internal container/NAT hostname. The OPC Foundation session dials EndpointDescription.EndpointUrl verbatim, so a 0.0.0.0 advertisement resolved to the client's own loopback and the connect failed. RealOpcUaClient now swaps the advertised authority for the reachable host/port (ConnectAsync + VerifyEndpointAsync), preserving scheme+path; no-op when already reachable. Surfaced live-gating OtOpcUa v3 (#14).
This commit is contained in:
@@ -123,6 +123,11 @@ public class RealOpcUaClient : IOpcUaClient
|
||||
endpoint = new EndpointDescription(endpointUrl);
|
||||
}
|
||||
|
||||
// The server advertises its own base address, which is frequently unroutable from here
|
||||
// (a 0.0.0.0 wildcard bind, or an internal container/NAT hostname). Swap it back to the
|
||||
// host the operator actually reached, or the session channel dials an unreachable address.
|
||||
RewriteEndpointHostForReachability(endpoint, endpointUrl);
|
||||
|
||||
var endpointConfig = EndpointConfiguration.Create(appConfig);
|
||||
var configuredEndpoint = new ConfiguredEndpoint(null, endpoint, endpointConfig);
|
||||
|
||||
@@ -157,6 +162,42 @@ public class RealOpcUaClient : IOpcUaClient
|
||||
await _subscription.CreateAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rewrites a discovered endpoint's host/port to match the reachable URL the operator
|
||||
/// configured, when the server advertised a different (unroutable) one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An OPC UA server advertises its base address from its OWN configuration, not the route the
|
||||
/// client took to reach it — so <c>GetEndpoints</c> routinely returns a wildcard bind
|
||||
/// (<c>opc.tcp://0.0.0.0:4840/…</c>) or an internal container/NAT hostname the client cannot
|
||||
/// dial. The OPC Foundation session opens its channel to <see cref="EndpointDescription.EndpointUrl"/>
|
||||
/// verbatim, so a 0.0.0.0 advertisement resolves to the client's OWN loopback and the connect
|
||||
/// fails. This swaps only the authority (host + port) to the reachable one, preserving the
|
||||
/// advertised scheme and path (e.g. <c>/OtOpcUa</c>). Mirrors <c>CoreClientUtils.SelectEndpoint</c>.
|
||||
/// A no-op when the advertised host/port is already reachable (so an opc-plc server that
|
||||
/// advertises its real container name is left untouched).
|
||||
/// </remarks>
|
||||
internal static void RewriteEndpointHostForReachability(EndpointDescription? endpoint, string discoveryUrl)
|
||||
{
|
||||
if (endpoint is null || string.IsNullOrWhiteSpace(endpoint.EndpointUrl))
|
||||
return;
|
||||
if (!Uri.TryCreate(discoveryUrl, UriKind.Absolute, out var reachable))
|
||||
return;
|
||||
if (!Uri.TryCreate(endpoint.EndpointUrl, UriKind.Absolute, out var advertised))
|
||||
return;
|
||||
|
||||
// Already reachable — leave it exactly as advertised.
|
||||
if (string.Equals(advertised.Host, reachable.Host, StringComparison.OrdinalIgnoreCase)
|
||||
&& advertised.Port == reachable.Port)
|
||||
return;
|
||||
|
||||
var port = reachable.Port >= 0 ? reachable.Port : advertised.Port;
|
||||
// Authority-only advertisements parse to AbsolutePath "/" — drop it so we don't append a
|
||||
// spurious trailing slash the server never advertised.
|
||||
var path = advertised.AbsolutePath == "/" ? string.Empty : advertised.AbsolutePath;
|
||||
endpoint.EndpointUrl = $"{advertised.Scheme}://{reachable.Host}:{port}{path}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Probes an OPC UA endpoint configuration WITHOUT persisting it or creating a
|
||||
/// long-lived connection — connect, capture the server certificate if it is untrusted,
|
||||
@@ -258,6 +299,10 @@ public class RealOpcUaClient : IOpcUaClient
|
||||
endpoint = new EndpointDescription(endpointUrl);
|
||||
}
|
||||
|
||||
// Same reachability rewrite as ConnectAsync — a probe against a server advertising a
|
||||
// 0.0.0.0 / NAT base address must dial the reachable host, not the advertised one.
|
||||
RewriteEndpointHostForReachability(endpoint, endpointUrl);
|
||||
|
||||
var endpointConfig = EndpointConfiguration.Create(appConfig);
|
||||
var configuredEndpoint = new ConfiguredEndpoint(null, endpoint, endpointConfig);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user