Coverage Report

Created: 2025-07-04 09:33

/src/node/src/quic/endpoint.h
Line
Count
Source (jump to first uncovered line)
1
#pragma once
2
3
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
4
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
5
6
#include <aliased_struct.h>
7
#include <async_wrap.h>
8
#include <env.h>
9
#include <node_sockaddr.h>
10
#include <uv.h>
11
#include <v8.h>
12
#include <algorithm>
13
#include <optional>
14
#include "bindingdata.h"
15
#include "packet.h"
16
#include "session.h"
17
#include "sessionticket.h"
18
#include "tokens.h"
19
20
namespace node {
21
namespace quic {
22
23
// An Endpoint encapsulates the UDP local port binding and is responsible for
24
// sending and receiving QUIC packets. A single endpoint can act as both a QUIC
25
// client and server simultaneously.
26
class Endpoint final : public AsyncWrap, public Packet::Listener {
27
 public:
28
  static constexpr uint64_t DEFAULT_MAX_CONNECTIONS =
29
      std::min<uint64_t>(kMaxSizeT, static_cast<uint64_t>(kMaxSafeJsInteger));
30
  static constexpr uint64_t DEFAULT_MAX_CONNECTIONS_PER_HOST = 100;
31
  static constexpr uint64_t DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE =
32
      (DEFAULT_MAX_CONNECTIONS_PER_HOST * 10);
33
  static constexpr uint64_t DEFAULT_MAX_STATELESS_RESETS = 10;
34
  static constexpr uint64_t DEFAULT_MAX_RETRY_LIMIT = 10;
35
36
  static constexpr auto QUIC_CC_ALGO_RENO = NGTCP2_CC_ALGO_RENO;
37
  static constexpr auto QUIC_CC_ALGO_CUBIC = NGTCP2_CC_ALGO_CUBIC;
38
  static constexpr auto QUIC_CC_ALGO_BBR = NGTCP2_CC_ALGO_BBR;
39
40
  // Endpoint configuration options
41
  struct Options final : public MemoryRetainer {
42
    // The local socket address to which the UDP port will be bound. The port
43
    // may be 0 to have Node.js select an available port. IPv6 or IPv4 addresses
44
    // may be used. When using IPv6, dual mode will be supported by default.
45
    std::shared_ptr<SocketAddress> local_address;
46
47
    // Retry tokens issued by the Endpoint are time-limited. By default, retry
48
    // tokens expire after DEFAULT_RETRYTOKEN_EXPIRATION *seconds*. This is an
49
    // arbitrary choice that is not mandated by the QUIC specification; so we
50
    // can choose any value that makes sense here. Retry tokens are sent to the
51
    // client, which echoes them back to the server in a subsequent set of
52
    // packets, which means the expiration must be set high enough to allow a
53
    // reasonable round-trip time for the session TLS handshake to complete.
54
    uint64_t retry_token_expiration =
55
        RetryToken::QUIC_DEFAULT_RETRYTOKEN_EXPIRATION / NGTCP2_SECONDS;
56
57
    // Tokens issued using NEW_TOKEN are time-limited. By default, tokens expire
58
    // after DEFAULT_TOKEN_EXPIRATION *seconds*.
59
    uint64_t token_expiration =
60
        RegularToken::QUIC_DEFAULT_REGULARTOKEN_EXPIRATION / NGTCP2_SECONDS;
61
62
    // Each Endpoint places limits on the number of concurrent connections from
63
    // a single host, and the total number of concurrent connections allowed as
64
    // a whole. These are set to fairly modest, and arbitrary defaults. We can
65
    // set these to whatever we'd like.
66
    uint64_t max_connections_per_host = DEFAULT_MAX_CONNECTIONS_PER_HOST;
67
    uint64_t max_connections_total = DEFAULT_MAX_CONNECTIONS;
68
69
    // A stateless reset in QUIC is a discrete mechanism that one endpoint can
70
    // use to communicate to a peer that it has lost whatever state it
71
    // previously held about a session. Because generating a stateless reset
72
    // consumes resources (even very modestly), they can be a DOS vector in
73
    // which a malicious peer intentionally sends a large number of stateless
74
    // reset eliciting packets. To protect against that risk, we limit the
75
    // number of stateless resets that may be generated for a given remote host
76
    // within a window of time. This is not mandated by QUIC, and the limit is
77
    // arbitrary. We can set it to whatever we'd like.
78
    uint64_t max_stateless_resets = DEFAULT_MAX_STATELESS_RESETS;
79
80
    // For tracking the number of connections per host, the number of stateless
81
    // resets that have been sent, and tracking the path verification status of
82
    // a remote host, we maintain an LRU cache of the most recently seen hosts.
83
    // The address_lru_size parameter determines the size of that cache. The
84
    // default is set modestly at 10 times the default max connections per host.
85
    uint64_t address_lru_size = DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE;
86
87
    // Similar to stateless resets, we enforce a limit on the number of retry
88
    // packets that can be generated and sent for a remote host. Generating
89
    // retry packets consumes a modest amount of resources and it's fairly
90
    // trivial for a malcious peer to trigger generation of a large number of
91
    // retries, so limiting them helps prevent a DOS vector.
92
    uint64_t max_retries = DEFAULT_MAX_RETRY_LIMIT;
93
94
    // The max_payload_size is the maximum size of a serialized QUIC packet. It
95
    // should always be set small enough to fit within a single MTU without
96
    // fragmentation. The default is set by the QUIC specification at 1200. This
97
    // value should not be changed unless you know for sure that the entire path
98
    // supports a given MTU without fragmenting at any point in the path.
99
    uint64_t max_payload_size = kDefaultMaxPacketLength;
100
101
    // The unacknowledged_packet_threshold is the maximum number of
102
    // unacknowledged packets that an ngtcp2 session will accumulate before
103
    // sending an acknowledgement. Setting this to 0 uses the ngtcp2 defaults,
104
    // which is what most will want. The value can be changed to fine tune some
105
    // of the performance characteristics of the session. This should only be
106
    // changed if you have a really good reason for doing so.
107
    uint64_t unacknowledged_packet_threshold = 0;
108
109
    // The amount of time (in milliseconds) that the endpoint will wait for the
110
    // completion of the tls handshake.
111
    uint64_t handshake_timeout = UINT64_MAX;
112
113
    uint64_t max_stream_window = 0;
114
    uint64_t max_window = 0;
115
116
    bool no_udp_payload_size_shaping = true;
117
118
    // The validate_address parameter instructs the Endpoint to perform explicit
119
    // address validation using retry tokens. This is strongly recommended and
120
    // should only be disabled in trusted, closed environments as a performance
121
    // optimization.
122
    bool validate_address = true;
123
124
    // The stateless reset mechanism can be disabled. This should rarely ever be
125
    // needed, and should only ever be done in trusted, closed environments as a
126
    // performance optimization.
127
    bool disable_stateless_reset = false;
128
129
#ifdef DEBUG
130
    // The rx_loss and tx_loss parameters are debugging tools that allow the
131
    // Endpoint to simulate random packet loss. The value for each parameter is
132
    // a value between 0.0 and 1.0 indicating a probability of packet loss. Each
133
    // time a packet is sent or received, the packet loss bit is calculated and
134
    // if true, the packet is silently dropped. This should only ever be used
135
    // for testing and debugging. There is never a reason why rx_loss and
136
    // tx_loss should ever be used in a production system.
137
    double rx_loss = 0.0;
138
    double tx_loss = 0.0;
139
#endif  // DEBUG
140
141
    // There are several common congestion control algorithms that ngtcp2 uses
142
    // to determine how it manages the flow control window: RENO, CUBIC, and
143
    // BBR. The details of how each works is not relevant here. The choice of
144
    // which to use by default is arbitrary and we can choose whichever we'd
145
    // like. Additional performance profiling will be needed to determine which
146
    // is the better of the two for our needs.
147
    ngtcp2_cc_algo cc_algorithm = NGTCP2_CC_ALGO_CUBIC;
148
149
    // By default, when the endpoint is created, it will generate a
150
    // reset_token_secret at random. This is a secret used in generating
151
    // stateless reset tokens. In order for stateless reset to be effective,
152
    // however, it is necessary to use a deterministic secret that persists
153
    // across ngtcp2 endpoints and sessions. This means that the endpoint
154
    // configuration really should have a reset token secret passed in.
155
    TokenSecret reset_token_secret;
156
157
    // The secret used for generating new regular tokens.
158
    TokenSecret token_secret;
159
160
    // When the local_address specifies an IPv6 local address to bind to, the
161
    // ipv6_only parameter determines whether dual stack mode (supporting both
162
    // IPv6 and IPv4) transparently is supported. This sets the UV_UDP_IPV6ONLY
163
    // flag on the underlying uv_udp_t.
164
    bool ipv6_only = false;
165
166
    uint32_t udp_receive_buffer_size = 0;
167
    uint32_t udp_send_buffer_size = 0;
168
169
    // The UDP TTL configuration is the number of network hops a packet will be
170
    // forwarded through. The default is 64. The value is in the range 1 to 255.
171
    // Setting to 0 uses the default.
172
    uint8_t udp_ttl = 0;
173
174
    void MemoryInfo(MemoryTracker* tracker) const override;
175
    SET_MEMORY_INFO_NAME(Endpoint::Config)
176
    SET_SELF_SIZE(Options)
177
178
    static v8::Maybe<Options> From(Environment* env,
179
                                   v8::Local<v8::Value> value);
180
181
    std::string ToString() const;
182
  };
183
184
  bool HasInstance(Environment* env, v8::Local<v8::Value> value);
185
  static v8::Local<v8::FunctionTemplate> GetConstructorTemplate(
186
      Environment* env);
187
  static void InitPerIsolate(IsolateData* data,
188
                             v8::Local<v8::ObjectTemplate> target);
189
  static void InitPerContext(Realm* realm, v8::Local<v8::Object> target);
190
  static void RegisterExternalReferences(ExternalReferenceRegistry* registry);
191
192
  Endpoint(Environment* env,
193
           v8::Local<v8::Object> object,
194
           const Endpoint::Options& options);
195
196
0
  inline const Options& options() const {
197
0
    return options_;
198
0
  }
199
200
  // While the busy flag is set, the Endpoint will reject all initial packets
201
  // with a SERVER_BUSY response. This allows us to build a circuit breaker
202
  // directly into the implementation, explicitly signaling that the server is
203
  // blocked when activity is too high.
204
  void MarkAsBusy(bool on = true);
205
206
  // Use the endpoint's token secret to generate a new token.
207
  RegularToken GenerateNewToken(uint32_t version,
208
                                const SocketAddress& remote_address);
209
210
  // Use the endpoint's reset token secret to generate a new stateless reset.
211
  StatelessResetToken GenerateNewStatelessResetToken(uint8_t* token,
212
                                                     const CID& cid) const;
213
214
  void AddSession(const CID& cid, BaseObjectPtr<Session> session);
215
  void RemoveSession(const CID& cid);
216
  BaseObjectPtr<Session> FindSession(const CID& cid);
217
218
  // A single session may be associated with multiple CIDs.
219
  // AssociateCID registers the mapping both in the Endpoint and the inner
220
  // Endpoint.
221
  void AssociateCID(const CID& cid, const CID& scid);
222
  void DisassociateCID(const CID& cid);
223
224
  // Associates a given stateless reset token with the session. This allows
225
  // stateless reset tokens to be recognized and dispatched to the proper
226
  // Endpoint and Session for processing.
227
  void AssociateStatelessResetToken(const StatelessResetToken& token,
228
                                    Session* session);
229
  void DisassociateStatelessResetToken(const StatelessResetToken& token);
230
231
  void Send(Packet* packet);
232
233
  // Generates and sends a retry packet. This is terminal for the connection.
234
  // Retry packets are used to force explicit path validation by issuing a token
235
  // to the peer that it must thereafter include in all subsequent initial
236
  // packets. Upon receiving a retry packet, the peer must termination it's
237
  // initial attempt to establish a connection and start a new attempt.
238
  //
239
  // Retry packets will only ever be generated by QUIC servers, and only if the
240
  // QuicSocket is configured for explicit path validation. There is no way for
241
  // a client to force a retry packet to be created. However, once a client
242
  // determines that explicit path validation is enabled, it could attempt to
243
  // DOS by sending a large number of malicious initial packets to intentionally
244
  // ellicit retry packets (It can do so by intentionally sending initial
245
  // packets that ignore the retry token). To help mitigate that risk, we limit
246
  // the number of retries we send to a given remote endpoint.
247
  void SendRetry(const PathDescriptor& options);
248
249
  // Sends a version negotiation packet. This is terminal for the connection and
250
  // is sent only when a QUIC packet is received for an unsupported QUIC
251
  // version. It is possible that a malicious packet triggered this so we need
252
  // to be careful not to commit too many resources.
253
  void SendVersionNegotiation(const PathDescriptor& options);
254
255
  // Possibly generates and sends a stateless reset packet. This is terminal for
256
  // the connection. It is possible that a malicious packet triggered this so we
257
  // need to be careful not to commit too many resources.
258
  bool SendStatelessReset(const PathDescriptor& options, size_t source_len);
259
260
  // Shutdown a connection prematurely, before a Session is created. This should
261
  // only be called at the start of a session before the crypto keys have been
262
  // established.
263
  void SendImmediateConnectionClose(const PathDescriptor& options,
264
                                    QuicError error);
265
266
  // Listen for connections (act as a server).
267
  void Listen(const Session::Options& options);
268
269
  // Create a new client-side Session.
270
  BaseObjectPtr<Session> Connect(
271
      const SocketAddress& remote_address,
272
      const Session::Options& options,
273
      std::optional<SessionTicket> sessionTicket = std::nullopt);
274
275
  // Returns the local address only if the endpoint has been bound. Before
276
  // the endpoint is bound, or after it is closed, this will abort due to
277
  // a failed check so it is important to check `is_closed()` before calling.
278
  SocketAddress local_address() const;
279
280
  void MemoryInfo(MemoryTracker* tracker) const override;
281
  SET_MEMORY_INFO_NAME(Endpoint)
282
  SET_SELF_SIZE(Endpoint)
283
284
  struct Stats;
285
  struct State;
286
287
 private:
288
  class UDP final : public MemoryRetainer {
289
   public:
290
    explicit UDP(Endpoint* endpoint);
291
    ~UDP() override;
292
293
    int Bind(const Endpoint::Options& config);
294
    int Start();
295
    void Stop();
296
    void Close();
297
    int Send(Packet* packet);
298
299
    // Returns the local UDP socket address to which we are bound,
300
    // or fail with an assert if we are not bound.
301
    SocketAddress local_address() const;
302
303
    bool is_bound() const;
304
    bool is_closed() const;
305
    bool is_closed_or_closing() const;
306
    operator bool() const;
307
308
    void Ref();
309
    void Unref();
310
311
    void MemoryInfo(node::MemoryTracker* tracker) const override;
312
    SET_MEMORY_INFO_NAME(Endpoint::UDP)
313
    SET_SELF_SIZE(UDP)
314
315
   private:
316
    class Impl;
317
318
    BaseObjectWeakPtr<Impl> impl_;
319
    bool is_bound_ = false;
320
    bool is_started_ = false;
321
    bool is_closed_ = false;
322
  };
323
324
  bool is_closed() const;
325
  bool is_closing() const;
326
  bool is_listening() const;
327
328
  bool Start();
329
330
  // Destroy the endpoint if...
331
  // * There are no sessions,
332
  // * There are no sent packets with pending done callbacks, and
333
  // * We're not listening for new initial packets.
334
  void MaybeDestroy();
335
336
  // Specifies the general reason the endpoint is being destroyed.
337
  enum class CloseContext {
338
    CLOSE,
339
    BIND_FAILURE,
340
    START_FAILURE,
341
    RECEIVE_FAILURE,
342
    SEND_FAILURE,
343
    LISTEN_FAILURE,
344
  };
345
346
  void Destroy(CloseContext context = CloseContext::CLOSE, int status = 0);
347
348
  // A graceful close will destroy the endpoint once all existing sessions
349
  // have ended normally. Creating new sessions (inbound or outbound) will
350
  // be prevented.
351
  void CloseGracefully();
352
353
  void Release();
354
355
  void PacketDone(int status) override;
356
357
  void EmitNewSession(const BaseObjectPtr<Session>& session);
358
  void EmitClose(CloseContext context, int status);
359
360
  void IncrementSocketAddressCounter(const SocketAddress& address);
361
  void DecrementSocketAddressCounter(const SocketAddress& address);
362
363
  // JavaScript API
364
365
  // Create a new Endpoint.
366
  // @param Endpoint::Options options - Options to configure the Endpoint.
367
  static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
368
369
  // Methods on the Endpoint instance:
370
371
  // Create a new client Session on this endpoint.
372
  // @param node::SocketAddress local_address - The local address to bind to.
373
  // @param Session::Options options - Options to configure the Session.
374
  // @param v8::ArrayBufferView session_ticket - The session ticket to use for
375
  // the Session.
376
  // @param v8::ArrayBufferView remote_transport_params - The remote transport
377
  // params.
378
  static void DoConnect(const v8::FunctionCallbackInfo<v8::Value>& args);
379
380
  // Start listening as a QUIC server
381
  // @param Session::Options options - Options to configure the Session.
382
  static void DoListen(const v8::FunctionCallbackInfo<v8::Value>& args);
383
384
  // Mark the Endpoint as busy, temporarily pausing handling of new initial
385
  // packets.
386
  // @param bool on - If true, mark the Endpoint as busy.
387
  static void MarkBusy(const v8::FunctionCallbackInfo<v8::Value>& args);
388
  static void FastMarkBusy(v8::Local<v8::Object> receiver, bool on);
389
390
  // DoCloseGracefully is the signal that endpoint should close. Any packets
391
  // that are already in the queue or in flight will be allowed to finish, but
392
  // the EndpoingWrap will be otherwise no longer able to receive or send
393
  // packets.
394
  static void DoCloseGracefully(
395
      const v8::FunctionCallbackInfo<v8::Value>& args);
396
397
  // Get the local address of the Endpoint.
398
  // @return node::SocketAddress - The local address of the Endpoint.
399
  static void LocalAddress(const v8::FunctionCallbackInfo<v8::Value>& args);
400
401
  // Ref() causes a listening Endpoint to keep the event loop active.
402
  static void Ref(const v8::FunctionCallbackInfo<v8::Value>& args);
403
  static void FastRef(v8::Local<v8::Object> receiver, bool on);
404
405
  void Receive(const uv_buf_t& buf, const SocketAddress& from);
406
407
  AliasedStruct<Stats> stats_;
408
  AliasedStruct<State> state_;
409
  const Options options_;
410
  UDP udp_;
411
412
  // Set if/when the endpoint is configured to listen.
413
  std::optional<Session::Options> server_options_{};
414
415
  // A Session is generally identified by one or more CIDs. We use two
416
  // maps for this rather than one to avoid creating a whole bunch of
417
  // BaseObjectPtr references. The primary map (sessions_) just maps
418
  // the original CID to the Session, the second map (dcid_to_scid_)
419
  // maps the additional CIDs to the primary.
420
  CID::Map<BaseObjectPtr<Session>> sessions_;
421
  CID::Map<CID> dcid_to_scid_;
422
  StatelessResetToken::Map<Session*> token_map_;
423
424
  struct SocketAddressInfoTraits final {
425
    struct Type final {
426
      size_t active_connections;
427
      size_t reset_count;
428
      size_t retry_count;
429
      uint64_t timestamp;
430
      bool validated;
431
    };
432
433
    static bool CheckExpired(const SocketAddress& address, const Type& type);
434
    static void Touch(const SocketAddress& address, Type* type);
435
  };
436
437
  SocketAddressLRU<SocketAddressInfoTraits> addrLRU_;
438
439
  CloseContext close_context_ = CloseContext::CLOSE;
440
  int close_status_ = 0;
441
442
  friend class UDP;
443
  friend class Packet;
444
  friend class Session;
445
};
446
447
}  // namespace quic
448
}  // namespace node
449
450
#endif  // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
451
#endif  // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS