Skip to content

Add OPC-UA protocol library and endpoint enumeration module - #21829

Open
ethan-thomason wants to merge 8 commits into
rapid7:masterfrom
ethan-thomason:opcua-getendpoints-v2
Open

Add OPC-UA protocol library and endpoint enumeration module#21829
ethan-thomason wants to merge 8 commits into
rapid7:masterfrom
ethan-thomason:opcua-getendpoints-v2

Conversation

@ethan-thomason

Copy link
Copy Markdown
Contributor

Summary

Adds auxiliary/scanner/scada/opcua_endpoint_enum, which enumerates the endpoints an OPC-UA server advertises and reports the security posture of each one.

This follows up #21612 (opcua_enum), which detects OPC-UA servers via the HEL/ACK handshake. This module goes a step further: it opens a secure channel with SecurityPolicy=None and calls GetEndpoints, then reports each endpoint's URL, MessageSecurityMode, SecurityPolicyUri, and accepted UserIdentityToken types, plus the server's ApplicationUri and ProductUri as a fingerprint.

Endpoints accepting the Anonymous identity token over a channel with MessageSecurityMode None are flagged and recorded via report_vuln - any host that can reach the port can connect with no credentials over an unencrypted channel, which is generally enough to read live process data and, depending on node permissions, write it.

Why a separate module rather than an action on opcua_enum

opcua_enum succeeds against servers where this one fails, so it isn't superseded. HEL/ACK is a transport handshake with no security negotiation, while GetEndpoints requires an OpenSecureChannel. The specification requires the discovery endpoint to accept SecurityPolicy=None precisely so clients can learn how to connect, but hardened deployments restrict it. opcua_enum remains the detector that works when the channel is closed.

Verification

Tested against two independent OPC-UA implementations:

node-opcua (7 endpoints, 3 security policy families, includes an unsecured endpoint):

msf6 auxiliary(scanner/scada/opcua_endpoint_enum) > run

[+] 127.0.0.1:4842        - OPC-UA server enumerated - 7 endpoint(s), 1 unauthenticated and unencrypted
[*] 127.0.0.1:4842        -   [0] opc.tcp://190d0c6ea827:4840/UA/BackdraftTest
[*] 127.0.0.1:4842        -       security: None/None  identity: UserName, Certificate, Anonymous
[!] 127.0.0.1:4842        -       endpoint accepts anonymous clients over an unencrypted channel
[*] 127.0.0.1:4842        -   [1] opc.tcp://190d0c6ea827:4840/UA/BackdraftTest
[*] 127.0.0.1:4842        -       security: Basic256Sha256/Sign  identity: UserName, Certificate, Anonymous
...
[*] 127.0.0.1:4842        -   ApplicationUri: urn:190d0c6ea827:NodeOPCUA-Server

The parsed endpoint list matches the server's own startup log line for line, including ordering and the Aes128/Aes256 policy URIs.

Inductive Automation Ignition 8.3.4 (3 endpoints, all Basic256Sha256/SignAndEncrypt, no unsecured endpoint):

[+] 10.10.0.3:62541       - OPC-UA server enumerated - 3 endpoint(s), 0 unauthenticated and unencrypted
[*] 10.10.0.3:62541       -   [0] opc.tcp://bd-83-primary:62541
[*] 10.10.0.3:62541       -       security: Basic256Sha256/SignAndEncrypt  identity: UserName

Worth noting that the endpoint list is returned even though no endpoint offers the None policy - the discovery channel is open by specification regardless of what the server's real endpoints require.

Output was also diffed field by field against an independent Python implementation using the asyncua stack, and matches.

Notes for reviewers

  • No new gem dependencies. Binary framing is hand-built with pack/unpack, following the iec104 style.
  • Reads accumulate until the full message length is received. GetEndpoints responses carry a certificate per endpoint and routinely span TCP segments; the largest observed was 10858 bytes.
  • Chunk reassembly is implemented but has not been exercised in testing - every server tested negotiated a buffer large enough to return a single F chunk. That path is written from Part 6 rather than validated on the wire.
  • Array lengths and all reads are bounds-checked, so a malformed or hostile response raises a parse error rather than allocating without bound.
  • CloseSecureChannel is sent so channels are released rather than left to expire.
  • Docker recipes for both test servers are in the module documentation.

@bwatters-r7 bwatters-r7 self-assigned this Aug 26, 2026
@bwatters-r7 bwatters-r7 added the rn-modules release notes for new or majorly enhanced modules label Aug 26, 2026
@bwatters-r7
bwatters-r7 requested a lite review from Copilot August 26, 2026 22:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Metasploit auxiliary scanner module to enumerate OPC-UA server endpoints via OpenSecureChannel + GetEndpoints, report each endpoint’s advertised security settings and identity token types, and record weak (anonymous + unencrypted) endpoints as vulns; includes accompanying module documentation.

Changes:

  • Introduces auxiliary/scanner/scada/opcua_endpoint_enum implementing OPC-UA TCP framing, chunk reassembly, and binary decoding for GetEndpoints.
  • Reports endpoint security posture (policy/mode, identity token types) to console and Metasploit DB (report_service, report_note, report_vuln).
  • Adds module documentation with usage, port notes, test setup guidance, and scenarios.

Impact Analysis:

  • Blast radius: low; new auxiliary module and new documentation only.
  • Data and contract effects: low; adds new service/note/vuln records in DB for hosts scanned, no schema changes visible in diff.
  • Rollback and test focus: rollback is deletion/revert of new files; focus validation on weak-endpoint classification logic and documentation accuracy.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
modules/auxiliary/scanner/scada/opcua_endpoint_enum.rb New OPC-UA endpoint enumeration scanner with binary transport parsing and weak-endpoint reporting.
documentation/modules/auxiliary/scanner/scada/opcua_endpoint_enum.md New user-facing documentation covering behavior, options, setup, and example runs.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +542 to +544
def unencrypted?(endpoint)
endpoint[:security_mode_name] == 'None' || endpoint[:security_policy_name] == 'None'
end
Comment on lines +64 to +68
### Setting Up a Test Server

Any OPC-UA server will exercise the module. Two convenient options:

**Inductive Automation Ignition (Docker)**
Comment on lines +1 to +6
## Vulnerable Application

This module enumerates the endpoints advertised by an OPC-UA server over the
OPC-UA TCP binary transport (`opc.tcp://`). OPC-UA (IEC 62541) is the dominant
interoperability standard in industrial automation and is exposed by PLCs, SCADA
platforms, historians, and gateway products.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, run msftidy_docs.rb

@bwatters-r7 bwatters-r7 added the group-review PRs flagged to get a group review during our weekly module hacking meeting. label Aug 26, 2026

@bwatters-r7 bwatters-r7 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd very much like to see the parsing stuff in it's own library, especially since I'd also like to see rspec testing to verify it.

Comment on lines +187 to +188
skip_string if (encoding & 0x40).positive?
skip(4) if (encoding & 0x80).positive?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any chance these are reversed?
I read 0x80 means NamespaceUri is next and 0x40 means ServerIndex is next?
Citation: https://reference.opcfoundation.org/specs/OPC-10000-6/5.2.2.9

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, and you read it exactly right. Part 6 §5.2.2.9 has NamespaceUri = 0x80 and ServerIndex = 0x40, so a set 0x80 means a String follows and 0x40 means a UInt32. I had the two actions mapped to the wrong bits - skipping a string on 0x40 and four bytes on 0x80, the reverse of what it should be. Now fixed:

skip_string if (encoding & 0x80).positive?    # NamespaceUri (String)
skip(4) if (encoding & 0x40).positive?        # ServerIndex (UInt32)

It slipped through because every server I tested returns endpoint NodeIds with neither flag set (namespace 0, no server index), so that branch never executed in the lab - exactly the kind of untested path a spec read catches and a live test doesn't. Appreciate you going to the reference.

I also checked the merged opcua_enum for the same pattern - it only does the HEL/ACK handshake and never parses NodeIds, so nothing there needs the correction. This was contained to this PR.

Done: re-ran msftidy_docs.rb (clean), rebased onto current master to pick up yesterday's CI fix, and force-pushed. Should be a clean single commit on a current base now.

@bwatters-r7

Copy link
Copy Markdown
Contributor

One last note- I think we found the bug from yesterday that was causing the test issues and landed a fix for it just after you put up this PR. Could I ask you to please rebase this PR when you make those changes so we can avoid the CI issues we had yesterday?

@ethan-thomason
ethan-thomason force-pushed the opcua-getendpoints-v2 branch 2 times, most recently from 7b83d7a to d23c70b Compare August 26, 2026 23:40
@ethan-thomason

Copy link
Copy Markdown
Contributor Author

One last note- I think we found the bug from yesterday that was causing the test issues and landed a fix for it just after you put up this PR. Could I ask you to please rebase this PR when you make those changes so we can avoid the CI issues we had yesterday?

Done

@ethan-thomason

Copy link
Copy Markdown
Contributor Author

I'd very much like to see the parsing stuff in it's own library, especially since I'd also like to see rspec testing to verify it.

Happy to do that. Would you rather it land in this PR, or as a follow-up that extracts the parser from both opcua_enum and opcua_endpoint_enum together? The library only makes sense covering both, and that means touching the already-merged one.

@bwatters-r7

Copy link
Copy Markdown
Contributor

Now that I look at it, have you considered using BinData for the parser class?
Check out https://github.com/rapid7/metasploit-framework/blob/master/modules/auxiliary/scanner/msmq/cve_2023_21554_queuejumper.rb for an example.

@ethan-thomason

Copy link
Copy Markdown
Contributor Author

Now that I look at it, have you considered using BinData for the parser class? Check out https://github.com/rapid7/metasploit-framework/blob/master/modules/auxiliary/scanner/msmq/cve_2023_21554_queuejumper.rb for an example.

I hadn't considered it but looking at queuejumper I think you're right. It would certainly clean things up and avoid bugs like you caught earlier where I had manual bit math backwards.

2 design considerations:

  1. OPC-UA encodes String and ByteString as an int32 length where -1 means null rather than empty. I don't think read_length expresses that directly, so it needs a small BinData::Primitive — written once, used for every string field. That's the first piece I'd write and the first I'd spec.

  2. Chunk reassembly has to stay outside the records. A response body can span chunks, so per-chunk headers get stripped and the payload concatenated before anything parses it. Framing layer and body layer stay separate, BinData handles the second.

On sequencing — rewriting the parser inside this PR is a fair amount of churn for something that's about to move into a library anyway. My inclination is to land #21829 as-is and do BinData as part of the extraction, alongside the rspec coverage you asked about in #21830. Two things I'd want your call on there: whether you'd rather see BinData in this PR instead, and whether the extraction should port opcua_enum too, since it's already merged and a library serving one module seems like the wrong shape.

I captured pcaps of the GetEndpoints exchange against 2 lab targets last night, including one over a WAN link where the response spans TCP segments. First spec will be that the BinData parser produces identical output to the current one against those captures, before I go near the hand-built edge cases.

@bwatters-r7

Copy link
Copy Markdown
Contributor

Two things I'd want your call on there: whether you'd rather see BinData in this PR instead, and whether the extraction should port opcua_enum too, since it's already merged and a library serving one module seems like the wrong shape.

OK... I think I am following everything, but please correct me if I am wrong, and also I'm cognizant that this is unpaid labor, so let me know if I am asking too much.
I think it best to go ahead and create the bindata-based library backed by rspecs in this PR before we land it. Once we get the library sorted and landed with the endpoint enumeration, we can open another PR to adjust opcua_enum to use the new library.
I am basing this off the assumption that opcua_enum is going to use a subset of the methods required by opcua_endpoint_enum.
Please let me know if that makes sense to you.

@ethan-thomason

Copy link
Copy Markdown
Contributor Author

Two things I'd want your call on there: whether you'd rather see BinData in this PR instead, and whether the extraction should port opcua_enum too, since it's already merged and a library serving one module seems like the wrong shape.

OK... I think I am following everything, but please correct me if I am wrong, and also I'm cognizant that this is unpaid labor, so let me know if I am asking too much. I think it best to go ahead and create the bindata-based library backed by rspecs in this PR before we land it. Once we get the library sorted and landed with the endpoint enumeration, we can open another PR to adjust opcua_enum to use the new library. I am basing this off the assumption that opcua_enum is going to use a subset of the methods required by opcua_endpoint_enum. Please let me know if that makes sense to you.

That makes sense and it's not too much to ask, maybe I'll get a msf t-shirt out of this. You're right opcua_enum does the HEL/ACK handshake and reads the ACK fields, which is the first step of what opcua_endpoint_enum does before it opens a secure channel and calls GetEndpoints. So I'll add a BinData-based library with rspec and port opcua_endpoint_enum into it for this PR and open a new one to port opcua_enum

It'll take me a few days, hope to have it ready by the end of the weekend.

@bwatters-r7 bwatters-r7 added group-reviewed Reviewed by the council of elders and removed group-review PRs flagged to get a group review during our weekly module hacking meeting. labels Aug 28, 2026
@ethan-thomason

Copy link
Copy Markdown
Contributor Author

Pushed the library and the ported module. Full description to follow shortly. Short version: Rex::Proto::OpcUa with rspec coverage backed by captured fixtures, and opcua_endpoint_enum ported onto it. opcua_enum follows in a separate PR as we discussed.

@ethan-thomason ethan-thomason changed the title Add OPC-UA endpoint enumeration auxiliary scanner module Add OPC-UA protocol library and endpoint enumeration module Aug 30, 2026
@ethan-thomason

ethan-thomason commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

What this does

Adds Rex::Proto::OpcUa, a BinData-based implementation of the OPC-UA binary encoding and UA TCP transport, and ports auxiliary/scanner/scada/opcua_endpoint_enum onto it.

The library extraction is per @bwatters-r7's suggestion in #21830. opcua_enum moves onto the same library in a follow-up PR - it uses a strict subset of what endpoint enumeration needs (HEL/ACK plus the ACK fields), so that diff should be small. Its STATUS_CODES is byte-identical to the library's, so it gets deleted rather than re-cited.

The diff

lines
lib/rex/proto/opc_ua/ 1,451
specs 2,655
module 496 (was 682)
module docs 369

The module drops 40% of its code. Nearly all of what's gone was protocol parsing that had no business in a module file.

Library layout follows amqp/ - types.rb, tcp.rb, secure_channel.rb, services.rb, enums.rb, error.rb. Class names carry the OpcUa prefix because BinData's type registry keys on the unqualified class name.

Testing

365 examples. Fixtures in spec/file_fixtures/opc_ua/ are real captured messages from node-opcua 2.175.6 - the ACK, the OpenSecureChannel response, and the 10,648-byte GetEndpoints response, complete with headers, straight off the wire.

The strongest assertions are that the captured messages parse end to end with no bytes left over and re-encode byte-identically. Field-by-field checks can pass while a record is subtly wrong; those two can't.

There's also a regression spec for the module (spec/modules/auxiliary/scanner/scada/opcua_endpoint_enum_spec.rb). It was written against the pre-port module and was green before I changed anything, so it's a genuine before/after comparison rather than a check that the new code agrees with itself. It pins the console narration verbatim and every report_* call.

Two paths had never executed before this: chunk reassembly and ERR status decoding. No lab server chunks a GetEndpoints response, and none of them misbehave. Both are now covered with hand-built frames per Part 6, which is only possible because MessageStream takes anything responding to get_once(len, timeout) rather than a socket.

Behavior changes

  1. ChannelSecurityToken is fully modelled. The old code read ChannelId and TokenId and stopped, leaving 16 bytes unread. A BinData record parses to its end, so the record covers CreatedAt, RevisedLifetime and the trailing ServerNonce. Confirmed against captures from two servers.
  2. UTF-8 handling fixed. The old Cursor#string did encode('UTF-8', undef: :replace) on a BINARY string. That's a transcode, not a scrub - every byte >= 0x80 is undefined from BINARY, so a valid café came back as caf??. Now force_encoding + scrub. Anything with a non-ASCII ApplicationName was reporting as mojibake.
  3. Abort chunks are decoded. Part 6 6.7.3 Table 63 gives the same two fields as the ERR body, so AbortError carries the StatusCode and reason. Spec-derived, not observed - no capture contains an abort, and the code says so.
  4. NodeId form mask is 0x3F, not 0x0F. The identifier form is six bits (Part 6 5.2.2.9, Table 17). Both masks behave identically on valid input, but 0x0F silently mis-decodes a byte with bit 4 or 5 set instead of rejecting it.
  5. Three GetEndpoints failed: detail strings changed wording, since they now come from library exception messages. no response, the ERR wording, and the unexpected-message-type wording are byte-identical. None of the three appears in the module docs; I checked all thirteen candidate strings.

User-visible output is otherwise unchanged. The docs' Scenarios blocks still match a live run exactly.

Two things worth flagging

BinData do_read overrides on BinData::Array fail silently on 2.4.15. initialize_shared_instance picks a read strategy and installs it with extend, and sanitize_parameters! force-defaults initial_length: 0 when neither it nor read_until was given (array.rb:288). An extended module beats a subclass method in the lookup order, so InitialLengthPlugin#do_read shadows the override and reads zero elements. No exception. Every GetEndpoints response would have parsed as zero endpoints against a live server.

The fix is to extend a module after super in initialize_shared_instance, which puts it ahead of the plugin. Verified against singleton_class.ancestors. Flagging it because it'll catch anyone writing a count-prefixed array on this BinData version.

run_host rescues Error::OpcUaError. Msf::Auxiliary::Scanner line 137 swallows connection errors and continues, line 138 rescues ::RuntimeError and re-raises. OpcUaError descends from Rex::RuntimeError, so without that rescue one malformed host aborts an entire scan instead of skipping a host. Specced, with the reasoning in a comment.

Related: Rex::TimeoutError descends from Interrupt rather than StandardError, so it wasn't usable as a parent - rescue => e would miss it.

Spec citations

Every record carries a Part 4 or Part 6 section reference, checked against the v1.05.07 documents.

Worth noting for anyone touching OPC-UA code: nine of the Part 4 numbers in the original module were wrong. The numbering shifted between revisions - 7.28, cited for RequestHeader, lands on ReadValueId in the current spec. All fixed here. Constants get verified; citations look like prose and don't, which is how they go stale unnoticed.

Verification

msf6 > use auxiliary/scanner/scada/opcua_endpoint_enum
msf6 auxiliary(scanner/scada/opcua_endpoint_enum) > set RHOSTS 127.0.0.1
msf6 auxiliary(scanner/scada/opcua_endpoint_enum) > run

[+] 127.0.0.1:4840        - OPC-UA server enumerated - 7 endpoint(s), 1 unauthenticated and unencrypted
[*] 127.0.0.1:4840        -   [0] opc.tcp://ua-node:4840/UA/BackdraftTest
[*] 127.0.0.1:4840        -       security: None/None  identity: UserName, Certificate, Anonymous
[!] 127.0.0.1:4840        -       endpoint accepts anonymous clients over an unencrypted channel
[*] 127.0.0.1:4840        -   [1] opc.tcp://ua-node:4840/UA/BackdraftTest
[*] 127.0.0.1:4840        -       security: Basic256Sha256/Sign  identity: UserName, Certificate, Anonymous
...
[*] 127.0.0.1:4840        -   ApplicationUri: urn:ua-node:NodeOPCUA-Server
[*] 127.0.0.1:4840        -   ProductUri: NodeOPCUA-Server
[*] 127.0.0.1:4840        - Scanned 1 of 1 hosts (100% complete)

With a database connected this records one service, one opcua.endpoints note, and one vuln - not one per endpoint.

Both test targets are documented in the module docs as quoted Dockerfiles and commands, per your guidance: the Ignition gateway (including a pure-curl recipe for the OPC-UA bind address on 8.3.x, since it binds loopback by default) and the node-opcua server, which is the one that exercises the weak-endpoint path.

# Defensive ceilings. A malformed or hostile response must fail quickly rather
# than allocate without bound. Both values are carried over unchanged from the
# module this library was factored out of.
MAX_MESSAGE_SIZE = 4 * 1024 * 1024

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The values above match https://www.ibm.com/docs/en/streamsets/7.x?topic=origins-opc-ua-client, but in recv_service_response, the implementation reads MAX_MESSAGE_SIZE up to MAX_CHUNKS instead of MAX_CHUNK_SIZE up to MAX_CHUNKS
That means the maximum message size in this implementation is more like 256 MB.

@ethan-thomason ethan-thomason Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. recv_message bounds a single chunk, recv_service_response bounds the chunk count, and nothing bounds the reassembled total - so 64 x 4MB is exactly right.

Worth noting how it got past the specs: there are examples for both ceilings and both pass. Each was tested in isolation and neither exercises them together. Two independently-correct guards composing into a weak one is precisely what an interaction test is for, and I didn't write one.

Chasing it turned up two more things. We send max_message_size: 0 in the Hello, not a value - zero is defined as no limit in 7.1.2.3, and max_chunk_count is zero too. So the module declines the protection the spec offers and then quietly applies limits of its own anyway.

7.1.2.2 also settles the per-chunk bound: "The OPC UA Connection Protocol layer shall verify the MessageType and make sure the MessageSize is less than the negotiated ReceiveBufferSize before passing any Message onto the SecureChannel layer." ReceiveBufferSize is the largest MessageChunk we can receive, and we negotiate 65535 - so the 4MB constant is 64x looser than the value we agreed to.

Fixing it as:

  1. Per-chunk bound becomes the negotiated ReceiveBufferSize from the ACK, per 7.1.2.2. The existing constant stays as MAX_CHUNK_SIZE, capping what we'll accept as a negotiated value so a server can't answer the Hello with 4GB.
  2. Advertise MAX_MESSAGE_SIZE in the Hello instead of zero, and enforce it on the reassembled payload inside the loop - a hostile server won't honor what it was told.
  3. Advertise MAX_CHUNKS as MaxChunkCount instead of zero, and keep enforcing it locally.

All three then trace to either a negotiated value or a limit we declared, rather than three numbers carried over from the module.

Plus a spec that pushes a stream of maximum-size chunks and asserts it fails on the cumulative bound before the chunk count runs out.

These values came across unchanged from the module, which had the same structure - but that's a reason it went unnoticed, not a defense. It matters more in a library, since every module built on this inherits the ceilings.

Will push shortly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction to my own fix list above: the ACK field that bounds incoming chunks is the server's SendBufferSize, not its ReceiveBufferSize. Both tables in 7.1.2 define their fields from the sender's perspective, and the sender of the Acknowledge is the server - so ACK.ReceiveBufferSize bounds what we send and ACK.SendBufferSize bounds what we receive. Implemented as min(our Hello ReceiveBufferSize, ACK SendBufferSize), since Table 75 says the server's SendBufferSize shall not exceed what we requested, and the min stops a non-conforming server from enlarging it.

@ethan-thomason

Copy link
Copy Markdown
Contributor Author

Pushed. Two corrections to what I said above, one of them mine.

The ACK field that bounds incoming chunks is the server's SendBufferSize, not its ReceiveBufferSize. Both tables in 7.1.2 define their fields from the sender's perspective, and the sender of the Acknowledge is the server - so ACK.ReceiveBufferSize bounds what we send and ACK.SendBufferSize bounds what we receive. Implemented as min(our Hello ReceiveBufferSize, ACK SendBufferSize): Table 75 says the server's SendBufferSize shall not exceed what we requested, and the min stops a non-conforming server from enlarging it.

What landed:

  1. MAX_CHUNK_SIZE is the standing per-chunk bound and the cap on any negotiated value. MessageStream#negotiate replaces it after the ACK, raising on zero or anything above the cap.
  2. MAX_MESSAGE_SIZE now means what Part 6 means by MaxMessageSize - the reassembled total, checked before each append inside the loop.
  3. The Hello advertises both MAX_MESSAGE_SIZE and MAX_CHUNKS instead of zeros.

One deliberate deviation: a server advertising below the 8192 minimum is accepted as its own tighter limit rather than rejected. A smaller figure can only make us allocate less, so rejecting it costs reach against sloppy servers for no safety gain.

The constants now sit in a consistent relationship. At a negotiated 65535, 64 chunks x 65511 usable bytes = 4,192,704, just under the 4,194,304 cumulative ceiling - so a conforming server hits the chunk count first and the cumulative bound is the backstop for a connection that never negotiated one. There's an example pinning that inequality so it can't drift if either constant moves.

386 examples green. Verified against a live node-opcua server, output unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

group-reviewed Reviewed by the council of elders rn-modules release notes for new or majorly enhanced modules

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

3 participants