Conversation
|
Review requested:
|
Review guideMost of the following was AI agent generated, verified by me. 80 commits is a lot to read end to end, so here is a route through them. The commits are ordered by dependency, not by theme, so the groups below jump around the history; each commit appears in exactly one group. Every commit builds and passes the suite on its own, so anything here can be checked out and run in isolation. Numbers are positions in the branch, oldest first. Datagram framing and the BIO layerOpenSSL's DTLS record layer assumes one BIO read yields exactly one datagram. The module used byte-stream BIOs, so that assumption held only by accident. Start here: several later commits depend on both BIOs being datagram BIOs.
Denial of service and resource boundsWork an unauthenticated peer could make the server do, and limits on what an authenticated one can hold or retain.
Peer address identityThe session table is keyed on the peer address, so what counts as the same peer matters. Note that 910a8cf changes shared code and 6f27438 reverts that part -- read them together; the net effect on node_sockaddr is additive only.
Certificate verification and peer identityThere was no way to see why a handshake was rejected, and two paths where verification silently did not happen.
ALPNProtocol list encoding and what happens when nothing is shared.
New features: secure contexts, SNI, PSK, resumptionThe largest group and the bulk of the new API surface. Read in order -- the later commits fix interactions the earlier ones created.
Exception safety and OpenSSL error reportingCallbacks that run inside SSL_do_handshake() cannot report anything to JavaScript from where they stand, and OpenSSL's error queue is shared process-wide.
Session and endpoint lifecyclePromises that never settled, and ordering between a session reaching JavaScript and its handshake running.
Public surface and argument validationOptions that reached a CHECK in the binding (a caller typo aborting the process), and internals that were reachable as public API.
Sockets and addressingWhich local socket an endpoint binds, and the UDP options it exposes.
Allocation gatingTwo paths that built V8 values whether or not anything was listening.
DocumentationCorrections and additions. 7988ed8 is structural (heading levels only, anchors preserved); the rest are content.
HousekeepingTest fixes and mechanical cleanups.
Worth a closer lookBehaviour changes that could affect an existing user of the experimental module:
Security-relevant:
Notes for the reviewer
|
This comment was marked as outdated.
This comment was marked as outdated.
3da1292 to
272930e
Compare
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as resolved.
This comment was marked as resolved.
272930e to
34f252d
Compare
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
a021dc5 to
92c6847
Compare
|
@nodejs/net |
92c6847 to
584f021
Compare
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
bc5fe2a to
22bb59d
Compare
This comment was marked as outdated.
This comment was marked as outdated.
send() took a Buffer or a string and refused a Uint8Array, which is the obvious thing to send, while exportKeyingMaterial() on the same object accepted one. Bare ArrayBuffers stay refused, as they are there too. The gate was Buffer.isBuffer() in JavaScript. The binding's check was Buffer::HasInstance(), which is defined as IsArrayBufferView() and so had been accepting every view all along. It is spelled IsArrayBufferView() now, and reads the bytes through ArrayBufferViewContents, so what it takes is stated rather than inherited from what a Buffer happens to be. A view sends the bytes it covers and not the buffer behind it: a subarray, a DataView at an offset, and an Int16Array all arrive as the bytes they span. Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode
Five options only a server can act on were handled four different ways when a client named one: sni threw, sessionIdContext was ignored, ticketKeys was applied to a client that has no tickets to issue, and requestCert was validated and then ignored. All refused now, by one rule checked before any of them is read. A client naming one has misunderstood the option, and the difference between "ignored" and "applied" was not something a caller could see. sni's own check goes away in favour of the shared one. pskIdentityHint names which key a client should pick. Given without psk there was no key to name, so it was dropped and the handshake failed for want of a PSK without mentioning the option that had been set. Each option is still accepted by a server context, so the rule is about which side may use it. ticketKeys and sni keep their own validation. Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode
unwrapSession folded the Buffer check in with the prefix and length checks, so all four failures reported ERR_INVALID_ARG_VALUE. Passing a string got the code that means the type was right and the contents were wrong. Split out. A Buffer that is not one of ours still reports ERR_INVALID_ARG_VALUE, which is what it is: the right type, contents that cannot be resumed. Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode
The binding says "Session is closed" where JavaScript says "Session is destroyed" for what looks like the same situation. The first is unreachable: JavaScript drops the handle on close and on destroy, and send() refuses a null handle before the binding is reached. That holds for a peer-initiated close too, where the close callback clears the handle before control returns to user code. The guard stays, because being unreachable today is not a reason to write into a closed SSL if that changes. The comment records why its wording is not being brought into line with a message it will never appear beside. Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode
Bind() set UV_UDP_IPV6ONLY for every IPv6 address, unconditionally. An
endpoint on :: therefore served IPv6 only and an IPv4 peer could not
reach it, with nothing to say so and no way to ask for anything else:
listen(..., { host: '::' })
connect('127.0.0.1', port) // handshake timeout
node:dgram and node:quic both bind dual stack by default. DTLS does now
too, and ipv6Only: true selects the old behaviour.
A dual-stack socket reports IPv4 peers with mapped addresses,
::ffff:127.0.0.1 rather than 127.0.0.1, so maxSessionsPerHost and
anything else keyed on the peer address sees them in that form.
The plumbing is a setSocketOptions() binding method read by Bind().
Signed-off-by: James M Snell <jasnell@gmail.com>
Assisted-by: Opencode
An endpoint took whatever socket the system gave it. There was no way to spread a server over several processes, and no way to give it room for bursts the default buffers drop. reusePort sets SO_REUSEPORT, where the kernel spreads datagrams between everyone bound to the port. Not SO_REUSEADDR, which libuv also offers and node:dgram exposes: on Linux that lets the last binder take the port from a running server. Without reusePort the port stays exclusive. udpReceiveBufferSize, udpSendBufferSize and udpTTL are applied once the bind succeeds, since there is no socket to set them on before that. Not naming one leaves the system default rather than substituting a number of ours. Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode
Both blocks enumerate the options they take and neither mentioned the five added for the UDP socket. Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode
Mentioning C++ in the dtls.md doc exposes implementation detail Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode
bind() moved to a symbol key so an endpoint cannot be rebound from outside. test-permission-net-dtls.mjs still called endpoint.bind() and had been failing with: TypeError: endpoint.bind is not a function which assert.throws() reported as the wrong error rather than as a missing method, so it read like a permission-check failure. Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode
The entry read "live and updated data flows through the endpoint". The session equivalent reads "updated as data flows". Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode
Signed-off-by: James M Snell <jasnell@gmail.com>
Signed-off-by: James M Snell <jasnell@gmail.com>
ReportPSKError took a const char* and passed it to ToV8Value(), which already has a std::string_view overload. Every call site hands it a literal, so the length is known rather than recovered with strlen(). Signed-off-by: James M Snell <jasnell@gmail.com>
Use timers/promise setTimeout and fix a hang in a test Signed-off-by: James M Snell <jasnell@gmail.com>
Signed-off-by: James M Snell <jasnell@gmail.com>
Signed-off-by: James M Snell <jasnell@gmail.com>
Signed-off-by: James M Snell <jasnell@gmail.com>
Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opecode
22bb59d to
36be49d
Compare
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opecode PR-URL: #65511 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Filip Skokan <panva.ip@gmail.com>
|
Landed in bd2b98f |
node:dtlslanded with the transport working but with many gaps. This addresses those, and fills in the API surface.This is a large PR but the commits are structured logically and sequentially. I chose to keep multiple PRs rather than squashing due to the size. Each has it's own description. I recommend stepping through and reviewing commit-by-commit.
A separate review guide comment will be included.