/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 |