Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
quic: use net.BlockList for limiting access to a QuicSocket
  • Loading branch information
jasnell committed Aug 17, 2020
commit 40e2ca37908bf6b7a511f93f3ba1a9d8c50f4479
18 changes: 18 additions & 0 deletions doc/api/quic.md
Original file line number Diff line number Diff line change
Expand Up @@ -1445,6 +1445,24 @@ error will be thrown if `quicsock.addEndpoint()` is called either after
the `QuicSocket` has already started binding to the local ports, or after
the `QuicSocket` has been destroyed.

#### `quicsocket.blockList`
<!-- YAML
added: REPLACEME
-->

* Type: {net.BlockList}

A {net.BlockList} instance used to define rules for remote IPv4 or IPv6
addresses that this `QuicSocket` is not permitted to interact with. The
rules can be specified as either specific individual addresses, ranges
of addresses, or CIDR subnet ranges.

When listening as a server, if a packet is received from a blocked address,
the packet will be ignored.

When connecting as a client, if the remote IP address is blocked, the
connection attempt will be rejected.

#### `quicsocket.bound`
<!-- YAML
added: REPLACEME
Expand Down
11 changes: 11 additions & 0 deletions lib/internal/quic/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const { Duplex } = require('stream');
const {
createSecureContext: _createSecureContext
} = require('tls');
const BlockList = require('internal/blocklist');
const {
translatePeerCertificate
} = require('_tls_common');
Expand Down Expand Up @@ -891,6 +892,7 @@ class QuicSocket extends EventEmitter {
[kInternalState] = {
alpn: undefined,
bindPromise: undefined,
blockList: undefined,
client: undefined,
closePromise: undefined,
closePromiseResolve: undefined,
Expand Down Expand Up @@ -1007,8 +1009,10 @@ class QuicSocket extends EventEmitter {
this[async_id_symbol] = handle.getAsyncId();
this[kInternalState].sharedState =
new QuicSocketSharedState(handle.state);
this[kInternalState].blockList = new BlockList(handle.blockList);
} else {
this[kInternalState].sharedState = undefined;
this[kInternalState].blockList = undefined;
}
}

Expand Down Expand Up @@ -1303,6 +1307,9 @@ class QuicSocket extends EventEmitter {
if (this.closing)
throw new ERR_INVALID_STATE('QuicSocket is closing');

if (this.blockList.check(ip, type === AF_INET6 ? 'ipv6' : 'ipv4'))
throw new ERR_OPERATION_FAILED(`${ip} failed BlockList check`);

return new QuicClientSession(this, options, type, ip);
}

Expand Down Expand Up @@ -1458,6 +1465,10 @@ class QuicSocket extends EventEmitter {
return this;
}

get blockList() {
return this[kInternalState]?.blockList;
}

get endpoints() {
return Array.from(this[kInternalState].endpoints);
}
Expand Down
2 changes: 1 addition & 1 deletion src/quic/node_quic_session.h
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ enum QuicSessionStateFields {
V(SMOOTHED_RTT, smoothed_rtt, "Smoothed RTT") \
V(CWND, cwnd, "Cwnd") \
V(RECEIVE_RATE, receive_rate, "Receive Rate / Sec") \
V(SEND_RATE, send_rate, "Send Rate Sec")
V(SEND_RATE, send_rate, "Send Rate Sec") \

#define V(name, _, __) IDX_QUIC_SESSION_STATS_##name,
enum QuicSessionStatsIdx : int {
Expand Down
13 changes: 13 additions & 0 deletions src/quic/node_quic_socket.cc
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ QuicSocket::QuicSocket(
: AsyncWrap(quic_state->env(), wrap, AsyncWrap::PROVIDER_QUICSOCKET),
StatsBase(quic_state->env(), wrap),
alloc_info_(MakeAllocator()),
block_list_(SocketAddressBlockListWrap::New(quic_state->env())),
options_(options),
state_(quic_state->env()->isolate()),
max_connections_(max_connections),
Expand All @@ -269,6 +270,12 @@ QuicSocket::QuicSocket(

EntropySource(token_secret_, kTokenSecretLen);

wrap->DefineOwnProperty(
env()->context(),
env()->block_list_string(),
block_list_->object(),
PropertyAttribute::ReadOnly).Check();

wrap->DefineOwnProperty(
env()->context(),
env()->state_string(),
Expand Down Expand Up @@ -432,6 +439,12 @@ void QuicSocket::OnReceive(
return;
}

if (UNLIKELY(block_list_->Apply(remote_addr))) {
Debug(this, "Ignoring blocked remote address: %s", remote_addr);
IncrementStat(&QuicSocketStats::packets_ignored);
return;
}

IncrementStat(&QuicSocketStats::bytes_received, nread);

const uint8_t* data = reinterpret_cast<const uint8_t*>(buf.data());
Expand Down
1 change: 1 addition & 0 deletions src/quic/node_quic_socket.h
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ class QuicSocket : public AsyncWrap,
std::vector<BaseObjectPtr<QuicEndpoint>> endpoints_;
SocketAddress::Map<BaseObjectWeakPtr<QuicEndpoint>> bound_endpoints_;
BaseObjectWeakPtr<QuicEndpoint> preferred_endpoint_;
BaseObjectPtr<SocketAddressBlockListWrap> block_list_;

uint32_t flags_ = 0;
uint32_t options_ = 0;
Expand Down
52 changes: 52 additions & 0 deletions test/parallel/test-quic-blocklist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Flags: --no-warnings
'use strict';

const common = require('../common');

if (!common.hasQuic)
common.skip('missing quic');

const { createQuicSocket, BlockList } = require('net');
const assert = require('assert');

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

const idleTimeout = common.platformTimeout(1);
const options = { key, cert, ca, alpn: 'zzz', idleTimeout };

const client = createQuicSocket({ client: options });
const server = createQuicSocket({ server: options });

assert(client.blockList instanceof BlockList);
assert(server.blockList instanceof BlockList);

client.blockList.addAddress('10.0.0.1');

assert(client.blockList.check('10.0.0.1'));

// Connection fails because the IP address is blocked
assert.rejects(client.connect({ address: '10.0.0.1' }), {
code: 'ERR_OPERATION_FAILED'
}).then(common.mustCall());

server.blockList.addAddress('127.0.0.1');

(async () => {
server.on('session', common.mustNotCall());

await server.listen();

const session = await client.connect({
address: common.localhostIPv4,
port: server.endpoints[0].address.port,
idleTimeout,
});

session.on('secure', common.mustNotCall());

await once(session, 'close');

await Promise.all([server.close(), client.close()]);

})().then(common.mustCall());