Make server endpoint selection deterministic - #1833
Merged
kevinherron merged 2 commits intoAug 1, 2026
Merged
Conversation
Servers previously re-derived the endpoint for each request with first-match scans over the endpoint descriptions, so ambiguous configurations were resolved arbitrarily and a Session could bind to a different endpoint than the one its channel negotiated. Introduce EndpointSelectionKey, the wire-observable identity of an endpoint at OpenSecureChannel time, and select exactly one endpoint (or none) per key. The selection is captured on the channel and accompanies every inbound request via ServiceRequestContext.getEndpoint(), so Sessions always bind to the negotiated endpoint. OpcUaServer.startup() now fails with Bad_ConfigurationError when two non-equivalent endpoints share a selection key; this is intentionally backwards-incompatible for configurations that previously relied on arbitrary first-match resolution. Endpoints differing only in host/port (hostname aliases) remain valid and are preferred by requested URL. Unsecured channels stay discovery-only unless an explicit None endpoint currently exists, re-checked per request so removing the endpoint and resetting the description cache locks down open channels. An OPN that omits receiverCertificateThumbprint on a secured policy is now rejected explicitly per Part 6. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CI formatting check downloads the latest google-java-format release, which now strips the empty leading javadoc lines, while the google-java-format bundled by spotless re-adds them; the empty-summary javadoc was the one construct the two versions disagree on, failing the formatting check for every PR. A real summary sentence satisfies both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kevinherron
force-pushed
the
kevinherron/dig-19-endpoint-selection
branch
from
August 1, 2026 19:13
5557c32 to
c4d5251
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When a client opens a secure channel, OPC UA doesn't tell the server which configured endpoint the client picked — the server has to figure that out from what's observable on the wire (the URL, security policy, security mode, and certificate). Previously, the server re-derived this guess on every request using first-match scans over the endpoint list. If several endpoints looked alike, whichever happened to come first won, so security-sensitive Session state (user token policies, the advertised certificate) could silently depend on collection ordering — and a Session could end up bound to a different endpoint than the one its channel actually negotiated.
This PR makes endpoint selection deterministic: the endpoint is selected once, at OpenSecureChannel time, from the channel's wire-observable identity, and that selection travels with every request on the channel. If a configuration contains endpoints the server could never tell apart on the wire, that's now a startup error instead of a silent arbitrary choice.
Changes
EndpointSelectionKey(new, transport layer): the wire-observable identity of an endpoint — transport profile, URL path, SecurityPolicy, MessageSecurityMode, and certificate thumbprint. Selection yields exactly one endpoint or none, never an ordering-dependent pick. Endpoints differing only in host/port (hostname aliases) legitimately share a key and are disambiguated by preferring the client's requested URL.ServiceRequestContext.getEndpoint()(returnsOptional), soSessionManagerbinds Sessions to the negotiated endpoint instead of re-deriving one.EndpointSelectionIndex(new, SDK layer): built once per resolved endpoint set;OpcUaServer.startup()fails withBad_ConfigurationErrorwhen two non-equivalent endpoints share a selection key.resetEndpointDescriptionCache()re-validates and logs an error (it can't fail a running server).SecurityPolicy.Noneendpoint currently exists for the transport/path — re-checked per request, so removing the None endpoint and resetting the description cache immediately locks down already-open unsecured channels.receiverCertificateThumbprinton a secured policy is rejected explicitly withBad_SecurityChecksFailed(Part 6 §6.7.2.3) instead of failing incidentally with a misleading error.Breaking changes (intentional)
Bad_ConfigurationError. Combine the token policies into a single endpoint, or make the endpoints wire-distinguishable.OpcUaServerConfigBuilder.setEndpointsnow copies the supplied set; mutations after the call no longer affect the built config.ServiceRequestContext.getEndpoint()is a new abstract method returningOptional<EndpointDescription>; the endpoint-lessServiceRequest/UascServiceRequestconstructors were removed.Testing
SessionEndpointBindingTestcovering session-to-endpoint binding, hostname-alias preference, discovery-only rejection, and ambiguity handling.mvn clean verifypasses foropc-ua-stack/transportandopc-ua-sdk/sdk-server(full compile across all modules).Follow-up tracked in DIG-20: precompute the discovery-only gate lookup in
EndpointSelectionIndex.buildto avoid the per-request endpoint scan on unsecured channels.🤖 Generated with Claude Code