current commit
260916.1 · lib,src: apply multiple updates to dtls implementation
#pragma once

#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#if HAVE_OPENSSL && HAVE_DTLS

#include <aliased_struct.h>
#include <async_wrap.h>
#include <base_object.h>
#include <env.h>
#include <ncrypto.h>
#include <node_sockaddr.h>
#include <timer_wrap.h>
#include <uv.h>
#include <v8.h>

#include <openssl/bio.h>
#include <openssl/ssl.h>

#include "dtls.h"

namespace node::dtls {

class DTLSContext;
class DTLSEndpoint;

// Shared C++ <-> JS state for a DTLS session.
struct DTLSSessionStateData {
  uint8_t handshaking = 0;
  uint8_t open = 0;
  uint8_t closing = 0;
  uint8_t destroyed = 0;
  uint8_t has_message_listener = 0;
  // Gates SSLKeylogCallback. Secrets are only turned into JS strings when the
  // application has actually asked for them.
  uint8_t has_keylog_listener = 0;
};

// Stats collected for a DTLS session, backed by a BigUint64Array.
struct DTLSSessionStats {
  DTLS_SESSION_STATS(DTLS_STAT_FIELD)
};

// DTLSSession represents a single DTLS association with a remote peer.
// It wraps an OpenSSL SSL* object configured for DTLS, using memory BIOs
// to interface with the endpoint's UDP socket.
class DTLSSession final : public AsyncWrap {
 public:
  static v8::Local<v8::FunctionTemplate> GetConstructorTemplate(
      Environment* env);
  static void InitPerContext(v8::Local<v8::Object> target,
                             v8::Local<v8::Context> context,
                             Environment* env);
  static void RegisterExternalReferences(ExternalReferenceRegistry* registry);

  // Create a new DTLS session.
  // |endpoint| - the owning endpoint (for sending packets)
  // |ssl_ctx| - the SSL_CTX to create the SSL* from
  // |remote| - the peer address
  // |is_server| - true if this is a server-side session
  // |servername|   - SNI to advertise (client only); nullptr to omit.
  // |verify_host|  - expected peer identity to verify (client only);
  //                  nullptr disables identity checking.
  // |verify_is_ip| - true if |verify_host| is an IP literal (verified
  //                  against iPAddress SANs) rather than a DNS name.
  // |resume|       - DER-encoded SSL_SESSION to resume (client only), or an
  //                  empty span for a full handshake. Like servername this
  //                  has to be applied before the ClientHello is emitted, so
  //                  it is a creation parameter rather than a setter.
  static BaseObjectPtr<DTLSSession> Create(
      Environment* env,
      DTLSEndpoint* endpoint,
      DTLSContext* context,
      const SocketAddress& remote,
      bool is_server,
      const char* servername = nullptr,
      const char* verify_host = nullptr,
      bool verify_is_ip = false,
      const ncrypto::Buffer<const unsigned char>& resume = {});

  // Create a session from an already-initialized SSL object.
  // Used by the server after DTLSv1_listen() returns 1 — the SSL
  // has already verified the cookie and is ready to continue.
  static BaseObjectPtr<DTLSSession> CreateFromSSL(Environment* env,
                                                  DTLSEndpoint* endpoint,
                                                  DTLSContext* context,
                                                  ncrypto::SSLPointer ssl,
                                                  BIO* enc_in,
                                                  BIO* enc_out,
                                                  const SocketAddress& remote);

  ~DTLSSession() override;

  // Called by the endpoint when a datagram arrives from this session's peer.
  void Receive(const uint8_t* data, size_t len);

  // Send application data to the peer.
  int Send(const uint8_t* data, size_t len);

  // Initiate a graceful shutdown (sends close_notify).
  void Close();

  // Immediately destroy the session without sending close_notify.
  void Destroy();

  const SocketAddress& remote_address() const { return remote_address_; }
  bool is_server() const { return is_server_; }
  bool is_handshake_complete() const { return handshake_complete_; }
  bool is_closed() const { return closed_; }

  void MemoryInfo(MemoryTracker* tracker) const override;
  SET_MEMORY_INFO_NAME(DTLSSession)
  SET_SELF_SIZE(DTLSSession)

  // Public constructor required by MakeBaseObject<>.
  DTLSSession(Environment* env,
              v8::Local<v8::Object> wrap,
              DTLSEndpoint* endpoint,
              ncrypto::SSLPointer ssl,
              BIO* enc_in,
              BIO* enc_out,
              const SocketAddress& remote,
              bool is_server);

 private:
  static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void DoSend(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void DoClose(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void DoDestroy(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void GetState(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void GetStats(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void GetRemoteAddress(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void GetProtocol(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void GetCipher(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void GetPeerCertificate(
      const v8::FunctionCallbackInfo<v8::Value>& args);
  static void GetPeerX509Certificate(
      const v8::FunctionCallbackInfo<v8::Value>& args);
  static void GetALPNProtocol(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void ExportKeyingMaterial(
      const v8::FunctionCallbackInfo<v8::Value>& args);
  static void GetSRTPProfile(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void GetServername(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void GetSession(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void WasReused(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void GetVerifyError(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void DoStart(const v8::FunctionCallbackInfo<v8::Value>& args);

 public:
  // The core state machine pump. Processes pending OpenSSL I/O:
  //   1. ClearOut() - SSL_read() -> emit decrypted data to JS
  //   2. ClearIn()  - SSL_write() pending cleartext
  //   3. EncOut()   - read enc_out_ BIO -> send via endpoint UDP
  //   4. UpdateTimer() - schedule retransmit timer if needed
  void Cycle();

  // Run the first flight. Separate from creation because nothing a session
  // reports has anywhere to go until its JavaScript wrapper is in place: the
  // callback dispatch reaches the wrapper through the handle, and a session
  // still being constructed has no wrapper attached. The server emits its new
  // session and then starts it; the client's wrapper calls this once it is
  // built. Repeat calls do nothing.
  void Start();

 private:
  // Read decrypted application data from OpenSSL and emit to JS.
  void ClearOut();

  // Flush encrypted data from enc_out_ BIO and send via the endpoint.
  void EncOut();

  // Update the DTLS retransmission timer based on OpenSSL's timeout.
  void UpdateTimer();

 public:
  // OpenSSL keylog callback. Registered once per SSL_CTX by DTLSContext; it
  // resolves the session from the SSL and does nothing unless that session has
  // a keylog listener.
  static void SSLKeylogCallback(const SSL* ssl, const char* line);

  // Hold an exception thrown by a callback that ran inside the handshake, to
  // be emitted once SSL_do_handshake() has returned. Emitting it there and
  // then would mean running JavaScript -- and draining the tick queue -- in
  // the middle of OpenSSL's state machine, which is what calling the
  // callback with Call() rather than MakeCallback() set out to avoid.
  void SetPendingError(v8::Local<v8::Value> error);

  // Retain a context chosen by the SNI callback. Entries in an sni map are
  // owned by the context holding the map, but one returned from a callback
  // has no other owner: SSL_set_SSL_CTX() references the SSL_CTX and not the
  // DTLSContext wrapping it, and callbacks reached later in the handshake
  // find their configuration through that wrapper.
  void SetSNIContext(DTLSContext* context);

 private:
  bool HandshakeDeadlineExpired() const;
  void EmitHandshakeTimeout();

  // The peer's chain verification result, with "no certificate, and none was
  // needed" (PSK) told apart from "a certificate that verified". Shared by
  // session.authorizationError and the gate below so the two agree.
  long PeerVerifyResult() const;  // NOLINT(runtime/int)

  // Refuse a completed handshake whose peer was never verified, emitting the
  // error. True to carry on. Exists for resumption, which skips verification
  // and restores the result from the session being resumed, so the verify
  // mode has nothing to act on.
  bool PeerVerificationPassed();

  // Emit a callback to JS via the endpoint's callback dispatch.
  v8::MaybeLocal<v8::Value> EmitCallback(int cb_index,
                                         int argc,
                                         v8::Local<v8::Value>* argv);

  // As EmitCallback, but a plain Call() rather than MakeCallback(): for
  // callbacks OpenSSL invokes from inside its own state machine, where
  // draining the microtask and tick queues would run user code in the middle
  // of a transition.
  v8::MaybeLocal<v8::Value> CallCallback(int cb_index,
                                         int argc,
                                         v8::Local<v8::Value>* argv);

  // Everything Cycle() does between taking and releasing the reentrancy
  // guard. Split out so the guard and the pending-error drain happen on every
  // path out, rather than at each return.
  void CycleInner();

  // Emit an exception captured from a callback that ran inside OpenSSL.
  void EmitPendingError();

  // Emit a failure to put a record on the wire.
  void EmitSendError();

  BaseObjectWeakPtr<DTLSEndpoint> endpoint_;
  ncrypto::SSLPointer ssl_;

  // Memory BIOs: encrypted data flows through these.
  // enc_in_: network datagrams written here -> SSL_read() extracts cleartext
  // enc_out_: SSL_write() puts ciphertext here -> we read and send via UDP
  BIO* enc_in_ = nullptr;
  BIO* enc_out_ = nullptr;

  TimerWrapHandle retransmit_timer_;

  SocketAddress remote_address_;
  bool is_server_;
  bool started_ = false;
  bool handshake_complete_ = false;
  bool closed_ = false;
  bool destroyed_ = false;
  int cycle_depth_ = 0;

  v8::Global<v8::Value> pending_error_;

  // First libuv error from sending a record, or 0. Reported once Cycle() is
  // done rather than from inside the send loop, which runs while the SSL is
  // mid-flight.
  int send_error_ = 0;

  // Absolute time by which the handshake must finish, or 0 for no limit.
  //
  // OpenSSL already gives up on its own, but only after DTLS1_TMO_ALERT_COUNT
  // retransmits on a doubling backoff capped at 60s -- measured at roughly
  // eight minutes, which is a long time to hold a slot against maxSessions.
  // This bounds it without touching the retransmission schedule, which has to
  // stay as it is: compressing it to force earlier failure would cause
  // spurious retransmits on exactly the lossy links DTLS is for. Whichever
  // limit trips first ends the handshake.
  uint64_t handshake_deadline_ = 0;

  // The context this session was created from, kept alive for as long as the
  // session is. SSL_new() takes a reference to the SSL_CTX but not to the
  // DTLSContext wrapping it, and callbacks reached from the handshake find
  // their configuration through that wrapper. An endpoint holds its server
  // context, but nothing held a client's: it became garbage the moment
  // connect() returned, and the first client-side callback to look for it
  // read freed memory.
  BaseObjectPtr<DTLSContext> context_;
  BaseObjectPtr<DTLSContext> sni_context_;

  AliasedStruct<DTLSSessionStateData> state_;
  AliasedStruct<DTLSSessionStats> stats_;
};

}  // namespace node::dtls

#endif  // HAVE_OPENSSL && HAVE_DTLS
#endif  // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS