From 242e7cb6f95d9f8255443a30b4571be581e9fd16 Mon Sep 17 00:00:00 2001 From: manNomi Date: Wed, 2 Sep 2026 14:36:28 +0900 Subject: [PATCH 1/2] quic: refresh session path addresses Signed-off-by: manNomi --- lib/internal/quic/quic.js | 20 ++++++++++++++----- .../test-quic-session-preferred-address.mjs | 15 +++++++++++++- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 11152f070add..c631f1b65bfe 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -3272,11 +3272,21 @@ class QuicSession { get path() { assertIsQuicSession(this); if (this.destroyed) return undefined; - return this.#inner.path ??= { - __proto__: null, - local: new InternalSocketAddress(this.#handle.getLocalAddress()), - remote: new InternalSocketAddress(this.#handle.getRemoteAddress()), - }; + const local = new InternalSocketAddress(this.#handle.getLocalAddress()); + const remote = new InternalSocketAddress(this.#handle.getRemoteAddress()); + let path = this.#inner.path; + if (path === undefined) { + path = { + __proto__: null, + local, + remote, + }; + this.#inner.path = path; + return path; + } + path.local = local; + path.remote = remote; + return path; } /** diff --git a/test/parallel/test-quic-session-preferred-address.mjs b/test/parallel/test-quic-session-preferred-address.mjs index 92194cf42a87..b7874aa2cce2 100644 --- a/test/parallel/test-quic-session-preferred-address.mjs +++ b/test/parallel/test-quic-session-preferred-address.mjs @@ -19,6 +19,7 @@ const { listen, connect } = await import('../common/quic.mjs'); const allStatusDone = Promise.withResolvers(); const serverGot = Promise.withResolvers(); const serverPathValidated = Promise.withResolvers(); +const clientPathValidated = Promise.withResolvers(); let statusCount = 0; const handleSession = mustCall(async (serverSession) => { @@ -75,18 +76,30 @@ const clientSession = await connect(serverEndpoint.address, { assert.strictEqual(oldLocal, null); assert.strictEqual(oldRemote, null); assert.strictEqual(preferred, true); + clientPathValidated.resolve(); }), maxDatagramSendAttempts: 100, // While the connection is restablished, // all the acknowledgement packets of ngtcp2 are counted as send attempts // so either this or a delay, or a change in ngtcp2 interfaces }); +const initialPath = clientSession.path; +assertEqualAddress(initialPath.local, clientSession.endpoint.address); +assertEqualAddress(initialPath.remote, serverEndpoint.address); await clientSession.opened; // Send two datagrams. await clientSession.sendDatagram(new Uint8Array([1])); await clientSession.sendDatagram(new Uint8Array([2])); -await serverPathValidated.promise; +await Promise.all([ + serverPathValidated.promise, + clientPathValidated.promise, +]); + +const migratedPath = clientSession.path; +assert.strictEqual(migratedPath, initialPath); +assertEqualAddress(migratedPath.local, clientSession.endpoint.address); +assertEqualAddress(migratedPath.remote, preferredEndpoint.address); // Send more datagrams after the preferred address migration completes // To show that data is still flowing after we close the original From 6e30e5fb36db1cabb738aea06ab86137e3e0df9f Mon Sep 17 00:00:00 2001 From: manNomi Date: Wed, 2 Sep 2026 17:24:39 +0900 Subject: [PATCH 2/2] quic: read current session path safely Use ngtcp2's current path as the address source and keep mutable address state private so cached path objects remain safe to freeze. Signed-off-by: manNomi --- doc/api/quic.md | 4 ++- lib/internal/quic/quic.js | 34 ++++++++++++++----- src/quic/session.cc | 6 ++-- .../test-quic-session-preferred-address.mjs | 2 ++ 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/doc/api/quic.md b/doc/api/quic.md index d90b1b938bd2..80390b0d7d0e 100644 --- a/doc/api/quic.md +++ b/doc/api/quic.md @@ -1414,7 +1414,9 @@ added: v23.8.0 * `local` {net.SocketAddress} * `remote` {net.SocketAddress} -The local and remote socket addresses associated with the session. Read only. +The local and remote socket addresses associated with the session. The object is +cached, and its read-only `local` and `remote` properties are refreshed from the +current path whenever `session.path` is accessed. ### `session.remoteTransportParams` diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index c631f1b65bfe..57e601eb35c4 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -2786,6 +2786,8 @@ class QuicSession { handshakeInfo: undefined, /** @type {QuicSessionPath|undefined} */ path: undefined, + /** @type {QuicSessionPath|undefined} */ + pathState: undefined, certificate: undefined, peerCertificate: undefined, ephemeralKeyInfo: undefined, @@ -3274,19 +3276,34 @@ class QuicSession { if (this.destroyed) return undefined; const local = new InternalSocketAddress(this.#handle.getLocalAddress()); const remote = new InternalSocketAddress(this.#handle.getRemoteAddress()); - let path = this.#inner.path; - if (path === undefined) { - path = { + const inner = this.#inner; + let state = inner.pathState; + if (state === undefined) { + state = { __proto__: null, local, remote, }; - this.#inner.path = path; - return path; + inner.pathState = state; + inner.path = ObjectDefineProperties({ __proto__: null }, { + local: { + __proto__: null, + configurable: false, + enumerable: true, + get: () => state.local, + }, + remote: { + __proto__: null, + configurable: false, + enumerable: true, + get: () => state.remote, + }, + }); + return inner.path; } - path.local = local; - path.remote = remote; - return path; + state.local = local; + state.remote = remote; + return inner.path; } /** @@ -3732,6 +3749,7 @@ class QuicSession { inner.onorigin = undefined; inner.ongoaway = undefined; inner.path = undefined; + inner.pathState = undefined; inner.certificate = undefined; inner.peerCertificate = undefined; inner.ephemeralKeyInfo = undefined; diff --git a/src/quic/session.cc b/src/quic/session.cc index 2266f74a1277..1d034b316669 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -1051,7 +1051,8 @@ struct Session::Impl final : public MemoryRetainer { return THROW_ERR_INVALID_STATE(env, "Session is destroyed"); } - auto address = session->remote_address(); + const auto* path = ngtcp2_conn_get_path2(*session); + auto address = SocketAddress(path->remote.addr); args.GetReturnValue().Set( SocketAddressBase::Create(env, std::make_shared(address)) ->object()); @@ -1066,7 +1067,8 @@ struct Session::Impl final : public MemoryRetainer { return THROW_ERR_INVALID_STATE(env, "Session is destroyed"); } - auto address = session->local_address(); + const auto* path = ngtcp2_conn_get_path2(*session); + auto address = SocketAddress(path->local.addr); args.GetReturnValue().Set( SocketAddressBase::Create(env, std::make_shared(address)) ->object()); diff --git a/test/parallel/test-quic-session-preferred-address.mjs b/test/parallel/test-quic-session-preferred-address.mjs index b7874aa2cce2..0483143b0f6d 100644 --- a/test/parallel/test-quic-session-preferred-address.mjs +++ b/test/parallel/test-quic-session-preferred-address.mjs @@ -85,6 +85,8 @@ const clientSession = await connect(serverEndpoint.address, { const initialPath = clientSession.path; assertEqualAddress(initialPath.local, clientSession.endpoint.address); assertEqualAddress(initialPath.remote, serverEndpoint.address); +// Freezing the cached object must not prevent internal address refreshes. +Object.freeze(initialPath); await clientSession.opened; // Send two datagrams.