Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

quic: fixup setSocket support #34669

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion doc/api/quic.md
Expand Up @@ -1246,12 +1246,16 @@ empty object when the key exchange is not ephemeral. The supported types are

For example: `{ type: 'ECDH', name: 'prime256v1', size: 256 }`.

#### `quicclientsession.setSocket(socket])`
#### `quicclientsession.setSocket(socket[, natRebinding])`
<!-- YAML
added: REPLACEME
-->

* `socket` {QuicSocket} A `QuicSocket` instance to move this session to.
* `natRebinding` {boolean} When `true`, indicates that the local address is to
be changed without triggering address validation. This will be rare and will
typically be used only to test resiliency in NAT rebind scenarios.
**Default**: `false`.
* Returns: {Promise}

Migrates the `QuicClientSession` to the given `QuicSocket` instance. If the new
Expand Down
10 changes: 7 additions & 3 deletions lib/internal/quic/core.js
Expand Up @@ -1803,8 +1803,10 @@ class QuicSession extends EventEmitter {
}, depth, options);
}

[kSetSocket](socket) {
[kSetSocket](socket, natRebinding = false) {
this[kInternalState].socket = socket;
if (socket !== undefined)
this[kHandle].setSocket(socket[kHandle], natRebinding);
}

// Called at the completion of the TLS handshake for the local peer
Expand Down Expand Up @@ -2432,7 +2434,7 @@ class QuicClientSession extends QuicSession {
{};
}

async setSocket(socket) {
async setSocket(socket, natRebinding = false) {
if (this.destroyed)
throw new ERR_INVALID_STATE('QuicClientSession is already destroyed');
if (this.closing)
Expand All @@ -2443,6 +2445,8 @@ class QuicClientSession extends QuicSession {
throw new ERR_INVALID_STATE('QuicSocket is already destroyed');
if (socket.closing)
throw new ERR_INVALID_STATE('QuicSocket is closing');
if (typeof natRebinding !== 'boolean')
throw new ERR_INVALID_ARG_TYPE('natRebinding', 'boolean', true);

await socket[kMaybeBind]();

Expand All @@ -2460,7 +2464,7 @@ class QuicClientSession extends QuicSession {
this[kSetSocket](undefined);
}
socket[kAddSession](this);
this[kSetSocket](socket);
this[kSetSocket](socket, natRebinding);
}
}

Expand Down
44 changes: 25 additions & 19 deletions src/quic/node_quic_session.cc
Expand Up @@ -2479,7 +2479,6 @@ int QuicSession::set_session(SSL_SESSION* session) {
}

// A client QuicSession can be migrated to a different QuicSocket instance.
// TODO(@jasnell): This will be revisited.
bool QuicSession::set_socket(QuicSocket* socket, bool nat_rebinding) {
CHECK(!is_server());
CHECK(!is_destroyed());
Expand All @@ -2492,8 +2491,6 @@ bool QuicSession::set_socket(QuicSocket* socket, bool nat_rebinding) {

Debug(this, "Migrating to %s", socket->diagnostic_name());

SendSessionScope send(this);

// Ensure that we maintain a reference to keep this from being
// destroyed while we are starting the migration.
BaseObjectPtr<QuicSession> ptr(this);
Expand All @@ -2511,22 +2508,31 @@ bool QuicSession::set_socket(QuicSocket* socket, bool nat_rebinding) {

// Step 4: Update ngtcp2
auto local_address = socket->local_address();
if (nat_rebinding) {
ngtcp2_addr addr;
ngtcp2_addr_init(
&addr,
local_address.data(),
local_address.length(),
nullptr);
ngtcp2_conn_set_local_addr(connection(), &addr);
} else {

// The nat_rebinding option here should rarely, if ever
// be used in a real application. It is intended to serve
// as a way of simulating a silent local address change,
// such as when the NAT binding changes. Currently, Node.js
// does not really have an effective way of detecting that.
// Manual user code intervention to handle the migration
// to the new QuicSocket is required, which should always
// trigger path validation using the ngtcp2_conn_initiate_migration.
if (LIKELY(!nat_rebinding)) {
SendSessionScope send(this);
QuicPath path(local_address, remote_address_);
if (ngtcp2_conn_initiate_migration(
connection(),
&path,
uv_hrtime()) != 0) {
return false;
}
return ngtcp2_conn_initiate_migration(
connection(),
&path,
uv_hrtime()) == 0;
} else {
ngtcp2_addr addr;
ngtcp2_conn_set_local_addr(
connection(),
ngtcp2_addr_init(
&addr,
local_address.data(),
local_address.length(),
nullptr));
}

return true;
Expand Down Expand Up @@ -3671,7 +3677,7 @@ void QuicSessionSetSocket(const FunctionCallbackInfo<Value>& args) {
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
CHECK(args[0]->IsObject());
ASSIGN_OR_RETURN_UNWRAP(&socket, args[0].As<Object>());
args.GetReturnValue().Set(session->set_socket(socket));
args.GetReturnValue().Set(session->set_socket(socket, args[1]->IsTrue()));
}

// GracefulClose flips a flag that prevents new local streams
Expand Down
25 changes: 22 additions & 3 deletions test/parallel/test-quic-simple-client-migrate.js
Expand Up @@ -5,10 +5,9 @@ const common = require('../common');
if (!common.hasQuic)
common.skip('missing quic');

common.skip('Not working correct yet... need to refactor');

const assert = require('assert');
const { key, cert, ca } = require('../common/quic');

const { once } = require('events');
const { createQuicSocket } = require('net');
const { pipeline } = require('stream');
Expand Down Expand Up @@ -58,14 +57,34 @@ const server = createQuicSocket({ server: options });
stream.on('end', common.mustCall(() => {
assert.strictEqual(data, 'Hello from the client');
}));
stream.on('close', common.mustCall());
stream.on('close', common.mustCall(() => {
req.close();
}));
// Send some data on one connection...
stream.write('Hello ');

// Wait just a bit, then migrate to a different
// QuicSocket and continue sending.
setTimeout(common.mustCall(async () => {
const s1 = req.socket;
const a1 = req.socket.endpoints[0].address;

await Promise.all([1, {}, 'test', false, null, undefined].map((i) => {
return assert.rejects(req.setSocket(i), {
code: 'ERR_INVALID_ARG_TYPE'
});
}));
await Promise.all([1, {}, 'test', null].map((i) => {
return assert.rejects(req.setSocket(req.socket, i), {
code: 'ERR_INVALID_ARG_TYPE'
});
}));

await req.setSocket(client2);

// Verify that it is using a different network endpoint
assert.notStrictEqual(s1, req.socket);
assert.notDeepStrictEqual(a1, req.socket.endpoints[0].address);
client.close();
stream.end('from the client');
}), common.platformTimeout(100));
Expand Down