Docker/nat/docker-compose.yml (stack /opt/otopcua-opcuaclient-nat on 10.100.0.35,
project: lmxopcua label): opc-plc 2.14.10 advertising --ph=otopcua-opc-plc-nat-private.invalid
on internal 50000, published on 50010, so every advertised URL is unreachable on host AND
port. Deployed; NatPublishedEndpointBrowseTests 3/3 against opc.tcp://10.100.0.35:50010
(and the secure case positive once, with the fixture cert temporarily trusted).
CI: integration-fixtures.sh starts a throwaway otopcua-ci-opcplc-nat (50110->50000) and
exports OPCUA_NAT_ENDPOINT; v2-e2e.yml points it at the shared stack. infra/README.md,
scripts/ci/README.md and the fixture README document it.
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
Against opc-plc (opc.tcp://10.100.0.35:50000): a shutdown with three live
subscriptions is bounded and the same instance re-initialises and reads (O-9 +
lifecycle generation through the real SDK); a server-refused write
(BadTypeMismatch) passes through and keeps the driver Healthy with its stamp
advanced (health contract); the browse side-panel reports a live variable's type,
shape and access (O-5).
Against the MTConnect fixture Agent: the headers-only ping reaches it and fails on
a dead port (T-7); a delivering /sample stream serves reads and reports its lag and
effective count on the diagnostics bag (T-4/T-5).
Refs #743
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT