@ 261106.11
current
#include "dtls_session.h"
#include "dtls.h"
#include "dtls_endpoint.h"

#if HAVE_OPENSSL && HAVE_DTLS

#include <aliased_struct-inl.h>
#include <async_wrap-inl.h>
#include <base_object-inl.h>
#include <crypto/crypto_common.h>
#include <crypto/crypto_x509.h>
#include <env-inl.h>
#include <memory_tracker-inl.h>
#include <node_buffer.h>
#include <node_errors.h>
#include <node_sockaddr-inl.h>
#include <timer_wrap-inl.h>
#include <util-inl.h>

#include <openssl/err.h>
#include <openssl/srtp.h>
#include <openssl/ssl.h>
#include <openssl/x509_vfy.h>
#include <openssl/x509v3.h>

#include <cstring>

namespace node {

using ncrypto::MarkPopErrorOnReturn;
using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::MaybeLocal;
using v8::Object;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;

namespace dtls {

// The session state "indices" are byte offsets into DTLSSessionStateData,
// accessed from JS via a DataView. Pin them to the actual struct layout, as
// the endpoint state already does, so adding or reordering a field cannot
// silently point JS at the wrong byte.
static_assert(IDX_SESSION_STATE_HANDSHAKING ==
              offsetof(DTLSSessionStateData, handshaking));
static_assert(IDX_SESSION_STATE_OPEN == offsetof(DTLSSessionStateData, open));
static_assert(IDX_SESSION_STATE_CLOSING ==
              offsetof(DTLSSessionStateData, closing));
static_assert(IDX_SESSION_STATE_DESTROYED ==
              offsetof(DTLSSessionStateData, destroyed));
static_assert(IDX_SESSION_STATE_HAS_MESSAGE_LISTENER ==
              offsetof(DTLSSessionStateData, has_message_listener));
static_assert(IDX_SESSION_STATE_HAS_KEYLOG_LISTENER ==
              offsetof(DTLSSessionStateData, has_keylog_listener));
static_assert(IDX_SESSION_STATE_COUNT == sizeof(DTLSSessionStateData));

namespace {
// Format the OpenSSL error queue into a human readable message.
//
// The most recently queued entry is rendered and the queue is left alone for
// the enclosing MarkPopErrorOnReturn to unwind.
//
// Peeked at the top rather than taken from the bottom. ERR_get_error() pops
// the oldest entry in the whole queue, which is not necessarily one of ours:
// MarkPopErrorOnReturn records a position to unwind to, it does not empty the
// queue on the way in, so anything already there is below the mark and comes
// out first. An SSL_ERROR_SSL means this operation queued at least one entry,
// and the newest is certainly it.
//
// An SSL_ERROR_SSL can also be reported with nothing queued --
// ERR_error_string_n() renders 0 as "error:00000000:lib(0)::reason(0)", which
// tells nobody anything -- so fall back to a description of the SSL error
// code instead.
std::string FormatSSLError(int ssl_err) {
  unsigned long last = ERR_peek_last_error();  // NOLINT(runtime/int)
  if (last != 0) {
    char buf[256];
    ERR_error_string_n(last, buf, sizeof(buf));
    return buf;
  }
  switch (ssl_err) {
    case SSL_ERROR_SYSCALL:
      return "DTLS I/O error";
    case SSL_ERROR_ZERO_RETURN:
      return "DTLS connection closed by peer";
    default:
      return "DTLS protocol error";
  }
}
}  // namespace

DTLSSession::DTLSSession(Environment* env,
                         Local<Object> wrap,
                         DTLSEndpoint* endpoint,
                         ncrypto::SSLPointer ssl,
                         BIO* enc_in,
                         BIO* enc_out,
                         const SocketAddress& remote,
                         bool is_server)
    : AsyncWrap(env, wrap, PROVIDER_DTLS_SESSION),
      endpoint_(endpoint),
      ssl_(std::move(ssl)),
      enc_in_(enc_in),
      enc_out_(enc_out),
      retransmit_timer_(env,
                        [this] {
                          if (destroyed_) return;
                          // Keep ourselves alive across the callback: emitting
                          // an error or running Cycle() below can synchronously
                          // destroy this session, and this timer lives on it.
                          BaseObjectPtr<DTLSSession> strong_ref{this};
                          MarkPopErrorOnReturn mark_pop_error_on_return;

                          // The deadline is checked before retransmitting, so
                          // an expired handshake stops rather than sends once
                          // more.
                          if (HandshakeDeadlineExpired()) {
                            EmitHandshakeTimeout();
                            return;
                          }

                          DTLS_STAT_INCREMENT(DTLSSessionStats,
                                              retransmit_count);
                          int ret = DTLSv1_handle_timeout(ssl_.get());
                          if (ret < 0) {
                            // OpenSSL gave up first, having exhausted its own
                            // retransmit budget.
                            EmitHandshakeTimeout();
                            return;
                          }
                          Cycle();
                        }),
      remote_address_(remote),
      is_server_(is_server),
      state_(env->isolate()),
      stats_(env->isolate()) {
  // Both creation paths pass the endpoint that owns the session. The MTU is
  // read from it unconditionally below, so a null one was never survivable
  // and testing for it here only made it look as though it were.
  CHECK_NOT_NULL(endpoint);
  MakeWeak();
  DTLS_STAT_RECORD_TIMESTAMP(DTLSSessionStats, created_at);

  if (endpoint->handshake_timeout() > 0) {
    handshake_deadline_ = uv_hrtime() / 1000000 + endpoint->handshake_timeout();
  }
  retransmit_timer_.Unref();

  // Update shared state.
  state_->handshaking = 1;
  state_->open = 0;

  // Store this session in SSL app data for callbacks.
  SSL_set_app_data(ssl_.get(), this);

  // Set the MTU on the SSL object.
  SSL_set_mtu(ssl_.get(), endpoint->mtu());
}

DTLSSession::~DTLSSession() = default;

Local<FunctionTemplate> DTLSSession::GetConstructorTemplate(Environment* env) {
  auto tmpl = env->dtls_session_constructor_template();
  if (tmpl.IsEmpty()) {
    Isolate* isolate = env->isolate();
    tmpl = NewFunctionTemplate(isolate, New);
    tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "DTLSSession"));
    tmpl->InstanceTemplate()->SetInternalFieldCount(
        AsyncWrap::kInternalFieldCount);

    SetProtoMethod(isolate, tmpl, "send", DoSend);
    SetProtoMethod(isolate, tmpl, "close", DoClose);
    SetProtoMethod(isolate, tmpl, "destroy", DoDestroy);
    SetProtoMethod(isolate, tmpl, "getState", GetState);
    SetProtoMethod(isolate, tmpl, "getStats", GetStats);
    SetProtoMethod(isolate, tmpl, "getRemoteAddress", GetRemoteAddress);
    SetProtoMethod(isolate, tmpl, "getProtocol", GetProtocol);
    SetProtoMethod(isolate, tmpl, "getCipher", GetCipher);
    SetProtoMethod(isolate, tmpl, "getPeerCertificate", GetPeerCertificate);
    SetProtoMethod(
        isolate, tmpl, "getPeerX509Certificate", GetPeerX509Certificate);
    SetProtoMethod(isolate, tmpl, "getALPNProtocol", GetALPNProtocol);
    SetProtoMethod(isolate, tmpl, "exportKeyingMaterial", ExportKeyingMaterial);
    SetProtoMethod(isolate, tmpl, "getSRTPProfile", GetSRTPProfile);
    SetProtoMethod(isolate, tmpl, "getServername", GetServername);
    SetProtoMethod(isolate, tmpl, "getSession", GetSession);
    SetProtoMethod(isolate, tmpl, "wasReused", WasReused);
    SetProtoMethod(isolate, tmpl, "getVerifyError", GetVerifyError);
    SetProtoMethod(isolate, tmpl, "start", DoStart);

    env->set_dtls_session_constructor_template(tmpl);
  }
  return tmpl;
}

void DTLSSession::InitPerContext(Local<Object> target,
                                 Local<Context> context,
                                 Environment* env) {
  SetConstructorFunction(context,
                         target,
                         "DTLSSession",
                         GetConstructorTemplate(env),
                         SetConstructorFunctionFlag::NONE);
}

void DTLSSession::RegisterExternalReferences(
    ExternalReferenceRegistry* registry) {
  registry->Register(New);
  registry->Register(DoSend);
  registry->Register(DoClose);
  registry->Register(DoDestroy);
  registry->Register(GetState);
  registry->Register(GetStats);
  registry->Register(GetRemoteAddress);
  registry->Register(GetProtocol);
  registry->Register(GetCipher);
  registry->Register(GetPeerCertificate);
  registry->Register(GetPeerX509Certificate);
  registry->Register(GetALPNProtocol);
  registry->Register(ExportKeyingMaterial);
  registry->Register(GetSRTPProfile);
  registry->Register(GetServername);
  registry->Register(GetVerifyError);
  registry->Register(GetSession);
  registry->Register(WasReused);
  registry->Register(DoStart);
}

BaseObjectPtr<DTLSSession> DTLSSession::Create(
    Environment* env,
    DTLSEndpoint* endpoint,
    DTLSContext* context,
    const SocketAddress& remote,
    bool is_server,
    const char* servername,
    const char* verify_host,
    bool verify_is_ip,
    const ncrypto::Buffer<const unsigned char>& resume) {
  // Create the SSL object.
  SSL* ssl_raw = SSL_new(context->ssl_ctx());
  if (ssl_raw == nullptr) {
    THROW_ERR_CRYPTO_OPERATION_FAILED(env, "SSL_new failed");
    return {};
  }
  context->BindToSSL(ssl_raw);

  ncrypto::SSLPointer ssl(ssl_raw);

  // Create memory BIOs for encrypted data I/O.
  // Both must preserve datagram boundaries. OpenSSL's DTLS record layer
  // assumes a read returns exactly one datagram (see the isdtls branch of
  // tls_default_read_n()), and its write path emits one BIO_write per record,
  // each sized to fit SSL_set_mtu(). BIO_s_dgram_mem() honours both, already
  // reports "empty" as a retry -- so no BIO_set_mem_eof_return() is needed --
  // and grows on write.
  auto enc_in = ncrypto::BIOPointer::New(BIO_s_dgram_mem());
  auto enc_out = ncrypto::BIOPointer::New(BIO_s_dgram_mem());
  if (!enc_in || !enc_out) {
    THROW_ERR_CRYPTO_OPERATION_FAILED(env, "BIO_new failed");
    return {};
  }

  // Associate BIOs with the SSL object. SSL_set_bio takes ownership.
  BIO* enc_in_raw = enc_in.release();
  BIO* enc_out_raw = enc_out.release();
  SSL_set_bio(ssl.get(), enc_in_raw, enc_out_raw);

  // Set the MTU (since we use SSL_OP_NO_QUERY_MTU).
  SSL_set_mtu(ssl.get(), endpoint->mtu());

  // Set the handshake direction.
  if (is_server) {
    SSL_set_accept_state(ssl.get());
  } else {
    SSL_set_connect_state(ssl.get());

    // Offer a previous session for resumption. Like SNI this has to happen
    // before Start() emits the ClientHello, since the session id and ticket
    // ride in it.
    //
    // A session that OpenSSL rejects is not an error: it falls back to a full
    // handshake, which is what an expired or unknown ticket should do. Only a
    // blob that will not parse is worth reporting, and that is the caller
    // handing over something that is not a session at all.
    //
    // This also copies the recorded verification result onto the connection,
    // where a resumed handshake leaves it: PeerVerificationPassed() is what
    // stops it standing in for verification that never ran.
    if (resume.len > 0) {
      const unsigned char* p = resume.data;
      ncrypto::SSLSessionPointer sess(d2i_SSL_SESSION(nullptr, &p, resume.len));
      if (!sess) {
        THROW_ERR_CRYPTO_OPERATION_FAILED(env,
                                          "Failed to parse resumed session");
        return {};
      }
      // Failure here also just means no resumption, so it is not fatal.
      USE(SSL_set_session(ssl.get(), sess.get()));
    }

    // Configure SNI and peer identity verification BEFORE the handshake
    // starts. Start() emits the ClientHello and is called as soon as the
    // JavaScript wrapper is built, so anything that must appear in that
    // flight (SNI) has to be set here rather than via a post-construction
    // setter.
    if (servername != nullptr && servername[0] != '\0') {
      if (!SSL_set_tlsext_host_name(ssl.get(), servername)) {
        THROW_ERR_CRYPTO_OPERATION_FAILED(env,
                                          "Failed to set servername (SNI)");
        return {};
      }
    }

    // When identity verification is requested, bind the expected peer name
    // (or IP) into the verification parameters. Combined with the context's
    // SSL_VERIFY_PEER mode this makes a name mismatch fail the handshake,
    // rather than accepting any certificate that merely chains to a trusted
    // CA. A failure to configure it is fatal: proceeding would silently skip
    // the identity check.
    if (verify_host != nullptr && verify_host[0] != '\0') {
      if (verify_is_ip) {
        if (!X509_VERIFY_PARAM_set1_ip_asc(SSL_get0_param(ssl.get()),
                                           verify_host)) {
          THROW_ERR_CRYPTO_OPERATION_FAILED(
              env, "Failed to set peer IP address for verification");
          return {};
        }
      } else {
        SSL_set_hostflags(ssl.get(), X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
        if (!SSL_set1_host(ssl.get(), verify_host)) {
          THROW_ERR_CRYPTO_OPERATION_FAILED(
              env, "Failed to set peer hostname for verification");
          return {};
        }
      }
    }
  }

  // Create the JS wrapper object.
  Local<FunctionTemplate> tmpl = GetConstructorTemplate(env);
  Local<Object> obj;
  if (!tmpl->InstanceTemplate()->NewInstance(env->context()).ToLocal(&obj)) {
    return {};
  }

  auto session = MakeBaseObject<DTLSSession>(env,
                                             obj,
                                             endpoint,
                                             std::move(ssl),
                                             enc_in_raw,
                                             enc_out_raw,
                                             remote,
                                             is_server);
  if (session) {
    // Hold the context. Nothing else does, for a client.
    session->context_ = BaseObjectPtr<DTLSContext>(context);
  }

  return session;
}

BaseObjectPtr<DTLSSession> DTLSSession::CreateFromSSL(
    Environment* env,
    DTLSEndpoint* endpoint,
    DTLSContext* context,
    ncrypto::SSLPointer ssl,
    BIO* enc_in,
    BIO* enc_out,
    const SocketAddress& remote) {
  Local<FunctionTemplate> tmpl = GetConstructorTemplate(env);
  Local<Object> obj;
  if (!tmpl->InstanceTemplate()->NewInstance(env->context()).ToLocal(&obj)) {
    return {};
  }

  auto session = MakeBaseObject<DTLSSession>(env,
                                             obj,
                                             endpoint,
                                             std::move(ssl),
                                             enc_in,
                                             enc_out,
                                             remote,
                                             true /* is_server */);
  if (session) {
    // The SSL was created from this context by the endpoint, before the
    // session existed, so bind it here. Held for the same reason the client
    // path holds it: the SSL references the SSL_CTX, but nothing references
    // the wrapper whose ex_data slot the PSK callbacks resolve through.
    context->BindToSSL(session->ssl_.get());
    session->context_ = BaseObjectPtr<DTLSContext>(context);
  }
  return session;
}

void DTLSSession::New(const FunctionCallbackInfo<Value>& args) {
  // Sessions are created internally via DTLSSession::Create,
  // not directly from JS.
  CHECK(args.IsConstructCall());
}

void DTLSSession::Receive(const uint8_t* data, size_t len) {
  if (destroyed_ || closed_) return;

  MarkPopErrorOnReturn mark_pop_error_on_return;

  // Write the encrypted datagram into enc_in_ BIO.
  int written = BIO_write(enc_in_, data, len);
  if (written <= 0) return;

  // Run the state machine.
  Cycle();
}

void DTLSSession::Start() {
  // A datagram can arrive for a server session between the new-session emit
  // and this call, and Receive() runs the pump. Starting again after that is
  // harmless but pointless, and a start() reaching the binding twice should
  // not re-enter the handshake.
  if (started_ || destroyed_) return;
  started_ = true;
  Cycle();
}

void DTLSSession::Cycle() {
  if (destroyed_) return;

  // Everything OpenSSL queues while the pump runs is consumed here (for the
  // error callbacks below) or discarded on the way out. Leaving entries behind
  // would misattribute them to whatever crypto operation runs next on this
  // thread -- including unrelated node:crypto work.
  MarkPopErrorOnReturn mark_pop_error_on_return;

  // Pin a strong reference to ourselves for the duration of the pump. A JS
  // callback dispatched below (message/handshake/error) can synchronously
  // destroy this session, which removes the endpoint's only strong reference
  // and would otherwise free `this` while we are still using ssl_/state_.
  BaseObjectPtr<DTLSSession> strong_ref{this};

  // Prevent infinite recursion.
  if (++cycle_depth_ > 1) {
    cycle_depth_--;
    return;
  }

  CycleInner();
  cycle_depth_--;

  // A callback that ran inside OpenSSL cannot report its own exception, so it
  // parks it and this is the first point where running JavaScript is safe
  // again. CycleInner() reports it itself when the handshake failed, since
  // there it is the more useful error; this covers the case where it did not
  // fail, which is every keylog exception and any PSK or SNI exception the
  // handshake recovered from.
  EmitPendingError();
  EmitSendError();
}

void DTLSSession::CycleInner() {
  HandleScope handle_scope(env()->isolate());
  Context::Scope context_scope(env()->context());

  // If handshake is not yet complete, drive it forward.
  if (!handshake_complete_) {
    int ret = SSL_do_handshake(ssl_.get());
    if (ret <= 0) {
      int err = SSL_get_error(ssl_.get(), ret);
      if (err == SSL_ERROR_SSL) {
        std::string message = FormatSSLError(err);
        // Flush any fatal alert OpenSSL queued for the peer before emitting the
        // error, which tears the session down and detaches the endpoint.
        EncOut();
        // An exception from a callback that ran inside the handshake is the
        // more useful report: OpenSSL only knows the handshake failed, not
        // that user code threw. It is emitted here, at a point where running
        // JavaScript is safe.
        if (!pending_error_.IsEmpty()) {
          Local<Value> argv[] = {pending_error_.Get(env()->isolate())};
          pending_error_.Reset();
          EmitCallback(DTLS_CB_SESSION_ERROR, 1, argv);
          return;
        }
        Local<Value> str;
        if (ToV8Value(env()->context(), message).ToLocal(&str)) {
          Local<Value> argv[] = {str};
          EmitCallback(DTLS_CB_SESSION_ERROR, 1, argv);
        }
        return;
      }
      // SSL_ERROR_WANT_READ/WRITE is normal during handshake.
    }
    // Flush any handshake data produced.
    EncOut();

    // Check if handshake just completed.
    if (SSL_is_init_finished(ssl_.get()) && !handshake_complete_) {
      // A resumed handshake carries no Certificate message, so OpenSSL runs
      // no verification and the verify mode has nothing to abort on. What it
      // does instead is restore the result recorded when the session was
      // first established -- SSL_set_session() copies verify_result straight
      // onto the connection, and both it and the peer certificate round-trip
      // through the DER blob. A session authenticated under
      // rejectUnauthorized: false therefore arrives here carrying its
      // original failure alongside a handshake that succeeded, and nothing
      // downstream re-checks it: a caller that asked for a verified peer
      // would be handed one that never verified. node:tls checks
      // verifyError() after a resumption for this reason.
      if (!PeerVerificationPassed()) return;

      handshake_complete_ = true;
      state_->handshaking = 0;
      state_->open = 1;
      DTLS_STAT_RECORD_TIMESTAMP(DTLSSessionStats, handshake_completed_at);

      // Skip only the emit on failure: the read below and the cycle_depth_
      // decrement at the end of Cycle() still have to happen.
      Local<Value> str;
      if (ToV8Value(env()->context(), SSL_get_version(ssl_.get()))
              .ToLocal(&str)) {
        Local<Value> argv[] = {str};
        EmitCallback(DTLS_CB_SESSION_HANDSHAKE, 1, argv);
      }
    }
  }

  // Read any decrypted application data.
  ClearOut();
  // Flush any pending encrypted output.
  EncOut();

  UpdateTimer();
}

void DTLSSession::ClearOut() {
  if (destroyed_) return;

  // Try to read decrypted application data from OpenSSL. A DTLS record's
  // plaintext is at most 2^14 bytes, so one SSL_read yields at most that much.
  uint8_t buf[16384];
  int read;

  while ((read = SSL_read(ssl_.get(), buf, sizeof(buf))) > 0) {
    DTLS_STAT_INCREMENT_N(DTLSSessionStats, bytes_received, read);
    DTLS_STAT_INCREMENT(DTLSSessionStats, messages_received);

    // Only materialise the payload as a JS Buffer if something is listening.
    // JS sets this flag from the onmessage setter; without the check, every
    // datagram was copied into the heap and dispatched into JS only for the
    // handler to find no listener and drop it. Reading continues either way,
    // so the data is still drained out of OpenSSL rather than accumulating.
    if (!state_->has_message_listener) continue;

    // continue, not return: this loop is what drains OpenSSL, and bailing
    // out of it would leave the remaining records buffered.
    Local<Object> chunk;
    if (!Buffer::Copy(env(), reinterpret_cast<const char*>(buf), read)
             .ToLocal(&chunk)) {
      continue;
    }
    Local<Value> argv[] = {chunk};
    EmitCallback(DTLS_CB_SESSION_MESSAGE, 1, argv);
    // The message handler may have destroyed the session synchronously; stop
    // reading if so (Cycle()'s strong reference keeps `this` itself alive).
    if (destroyed_) return;
  }

  int err = SSL_get_error(ssl_.get(), read);
  switch (err) {
    case SSL_ERROR_WANT_READ:
    case SSL_ERROR_WANT_WRITE:
      // Normal - need more data or need to flush.
      break;

    case SSL_ERROR_ZERO_RETURN:
      // Peer sent close_notify.
      if (!closed_) {
        closed_ = true;
        state_->closing = 1;
        state_->open = 0;
        // Send our close_notify back.
        SSL_shutdown(ssl_.get());
        EncOut();
        // Detach from the endpoint's session table before notifying JS so an
        // observer of the close sees a consistent session count. Cycle() holds
        // a strong reference for the duration of the pump.
        if (auto ep = endpoint_.get()) ep->RemoveSession(remote_address_);
        Local<Value> argv[] = {};
        EmitCallback(DTLS_CB_SESSION_CLOSE, 0, argv);
        Destroy();
      }
      break;

    case SSL_ERROR_SSL: {
      // SSL error during handshake or data exchange.
      std::string message = FormatSSLError(err);
      // Flush any fatal alert OpenSSL queued for the peer before emitting the
      // error, which tears the session down and detaches the endpoint.
      EncOut();
      Local<Value> str;
      if (ToV8Value(env()->context(), message).ToLocal(&str)) {
        Local<Value> argv[] = {str};
        EmitCallback(DTLS_CB_SESSION_ERROR, 1, argv);
      }
      break;
    }

    default:
      break;
  }
}

void DTLSSession::EncOut() {
  if (destroyed_) return;
  auto ep = endpoint_.get();
  if (ep == nullptr) return;

  // enc_out_ is a datagram BIO, so each BIO_read yields exactly one datagram
  // -- one DTLS record as OpenSSL framed it against the MTU. Loop because a
  // single flight is several records, and send each one separately rather than
  // concatenating them into a datagram that would need IP fragmentation.
  //
  // BIO_pending() on a datagram BIO reports the size of the next datagram, so
  // the buffer can be sized to it exactly. Reading into a smaller one would
  // silently truncate the record, and a fixed 64 KiB frame is a lot of stack
  // for a ~1200 byte datagram in a function Cycle() can re-enter.
  MaybeStackBuffer<uint8_t, 1500> buf;
  for (;;) {
    size_t pending = BIO_pending(enc_out_);
    if (pending == 0) break;
    buf.AllocateSufficientStorage(pending);
    int read = BIO_read(enc_out_, buf.out(), pending);
    if (read <= 0) break;
    int err = ep->SendTo(remote_address_, buf.out(), read);
    if (err != 0) {
      // A record that cannot be sent will not be retransmitted into
      // existence: EMSGSIZE means the MTU is wrong for this path and
      // ENETUNREACH means there is no path. Both used to present as a
      // handshake that went quiet until the timeout. Keep the first, since
      // the rest of the flight will fail the same way.
      if (send_error_ == 0) send_error_ = err;
      break;
    }
  }
}

bool DTLSSession::HandshakeDeadlineExpired() const {
  if (handshake_deadline_ == 0 || handshake_complete_) return false;
  return uv_hrtime() / 1000000 >= handshake_deadline_;
}

long DTLSSession::PeerVerifyResult() const {  // NOLINT(runtime/int)
  // SSL_get_verify_result() reports X509_V_OK when the peer sent no
  // certificate at all, because there was nothing to find fault with. Route
  // through ncrypto, which reports std::nullopt for that case (allowing for
  // PSK, where the identity was authenticated by the key instead) so it can
  // be distinguished from a certificate that actually verified.
  return ssl_.verifyPeerCertificate().value_or(
      X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT);
}

bool DTLSSession::PeerVerificationPassed() {
  // Consulted on every completed handshake rather than only a resumed one. It
  // costs nothing where OpenSSL already enforced -- there the handshake
  // failed and never reached here -- and it also covers a resumption the
  // server refused, where the result restored by SSL_set_session() stays on
  // the connection unless the full handshake that replaced it verified
  // something of its own.
  //
  // Whether a failure is fatal is decided by the verify mode OpenSSL itself
  // was given, read back off the SSL rather than tracked beside it, so the
  // two cannot disagree -- including after an SNI callback swapped the
  // SSL_CTX out from under this connection.
  //
  // A client aborts on a bad chain whenever the mode is not SSL_VERIFY_NONE.
  // That is the test OpenSSL itself applies when processing the server's
  // certificate -- it validates "if any flag is set", not only for
  // SSL_VERIFY_PEER -- and it is the whole of what rejectUnauthorized selects
  // between.
  //
  // A server sets SSL_VERIFY_PEER for requestCert and adds
  // SSL_VERIFY_FAIL_IF_NO_PEER_CERT only when rejectUnauthorized is set too:
  // requestCert on its own installs a permissive verify callback precisely so
  // the application can judge the certificate itself, so it is the added flag
  // and not SSL_VERIFY_PEER that marks a failure fatal there.
  int mode = SSL_get_verify_mode(ssl_.get());
  bool fatal = is_server_ ? (mode & SSL_VERIFY_FAIL_IF_NO_PEER_CERT) != 0
                          : mode != SSL_VERIFY_NONE;
  if (!fatal) return true;

  // Exactly the result session.authorized reports, so what is refused here
  // and what reads as unauthorized there cannot drift apart. It is also the
  // predicate node:tls applies -- TLSWrap::VerifyError() is the same
  // verifyPeerCertificate().value_or() -- so a peer authenticated by a PSK
  // rather than a certificate passes on the strength of the cipher's auth
  // method, as it does there.
  //
  // One difference from node:tls, which additionally requires a peer
  // certificate because ncrypto reports X509_V_OK for a TLS 1.3 resumption
  // without one: a DTLS 1.2 resumption restores the peer certificate along
  // with the result, so there is a real result to read here. Enabling DTLS
  // 1.3, once the bundled OpenSSL offers it, means revisiting this.
  long verify_error = PeerVerifyResult();  // NOLINT(runtime/int)
  if (verify_error == X509_V_OK) return true;

  // enc_out_ is deliberately not flushed, unlike the alert path in
  // CycleInner(): there is no alert queued to get out, and what is queued is
  // this side's own Finished. The peer is left to time out, as it is for
  // every other handshake failure that OpenSSL did not itself alert on.
  std::string message = "Peer certificate verification failed: ";
  message += ncrypto::X509Pointer::ErrorCode(verify_error);
  Local<Value> str;
  if (ToV8Value(env()->context(), message).ToLocal(&str)) {
    Local<Value> argv[] = {str};
    EmitCallback(DTLS_CB_SESSION_ERROR, 1, argv);
  }
  return false;
}

void DTLSSession::EmitHandshakeTimeout() {
  HandleScope hs(env()->isolate());
  Context::Scope cs(env()->context());
  Local<Value> message;
  if (!ToV8Value(env()->context(), "DTLS handshake timeout")
           .ToLocal(&message)) {
    return;
  }
  Local<Value> argv[] = {message};
  EmitCallback(DTLS_CB_SESSION_ERROR, 1, argv);
}

void DTLSSession::UpdateTimer() {
  if (destroyed_) return;

  struct timeval tv;
  if (DTLSv1_get_timeout(ssl_.get(), &tv)) {
    uint64_t timeout_ms = tv.tv_sec * 1000 + tv.tv_usec / 1000;
    if (timeout_ms == 0) timeout_ms = 1;  // Minimum 1ms.

    // Wake at the deadline if it falls first. The retransmit schedule doubles
    // -- 1s, 3s, 7s, 15s, 31s -- so without this a 60s limit would not be
    // noticed until 63s, and a shorter one could be out by almost its own
    // length.
    if (handshake_deadline_ > 0 && !handshake_complete_) {
      uint64_t now = uv_hrtime() / 1000000;
      uint64_t remaining =
          handshake_deadline_ > now ? handshake_deadline_ - now : 1;
      if (remaining < timeout_ms) timeout_ms = remaining;
    }

    retransmit_timer_.Update(timeout_ms);
  } else {
    // No timeout needed (handshake complete or not started).
    retransmit_timer_.Stop();
  }
}

// Returns the number of bytes written, or -1 with a JS exception pending.
// Every failure path throws: a bare -1 return is trivially ignored, and
// send() already throws for a destroyed session and for a bad argument type,
// so returning a sentinel from the remaining paths was the inconsistency.
int DTLSSession::Send(const uint8_t* data, size_t len) {
  // Kept as a guard, not as a message anyone reads. JavaScript drops the
  // handle on close and on destroy, and its own send() refuses a null one
  // first, so this cannot be reached through the public API -- including
  // when the peer closes, where the close callback clears the handle before
  // control returns. Instrumented across the suite and a close/send race:
  // never taken. Do not align the wording with the JavaScript message on the
  // assumption that a caller can see both.
  if (destroyed_ || closed_) {
    THROW_ERR_INVALID_STATE(env(), "Session is closed");
    return -1;
  }

  if (!handshake_complete_) {
    THROW_ERR_INVALID_STATE(
        env(), "Cannot send application data before the handshake completes");
    return -1;
  }

  // DTLS carries application data in a single record per datagram and does
  // not fragment it, so anything above the maximum plaintext record length is
  // unsendable. Report the limit rather than letting SSL_write fail opaquely.
  if (len > SSL3_RT_MAX_PLAIN_LENGTH) {
    THROW_ERR_OUT_OF_RANGE(
        env(),
        "data is %zu bytes, which exceeds the %d byte maximum for a single "
        "DTLS record",
        len,
        SSL3_RT_MAX_PLAIN_LENGTH);
    return -1;
  }

  MarkPopErrorOnReturn mark_pop_error_on_return;

  int written = SSL_write(ssl_.get(), data, len);
  if (written <= 0) {
    int err = SSL_get_error(ssl_.get(), written);
    std::string message = FormatSSLError(err);
    THROW_ERR_CRYPTO_OPERATION_FAILED(env(), message.c_str());
    return -1;
  }

  DTLS_STAT_INCREMENT_N(DTLSSessionStats, bytes_sent, written);
  DTLS_STAT_INCREMENT(DTLSSessionStats, messages_sent);
  EncOut();
  return written;
}

void DTLSSession::Close() {
  if (destroyed_ || closed_) return;

  // Emitting the close below can synchronously free this session (a client
  // session that owns its endpoint tears the endpoint -- and thus itself --
  // down from the close callback), and we call Destroy() afterwards. Pin a
  // strong reference so `this` survives until we return.
  BaseObjectPtr<DTLSSession> strong_ref{this};

  MarkPopErrorOnReturn mark_pop_error_on_return;

  closed_ = true;
  state_->closing = 1;
  DTLS_STAT_RECORD_TIMESTAMP(DTLSSessionStats, closing_at);

  // Send close_notify.
  int ret = SSL_shutdown(ssl_.get());
  if (ret == 0) {
    // Need to call again for bidirectional shutdown.
    SSL_shutdown(ssl_.get());
  }
  EncOut();

  retransmit_timer_.Stop();

  state_->open = 0;

  // Detach from the endpoint's session table before notifying JS, so an
  // observer of the close (e.g. one awaiting `closed`) sees a consistent
  // session count. We stay alive via strong_ref, and endpoint_ remains valid
  // for the callback below; the Destroy() that follows clears it.
  if (auto ep = endpoint_.get()) ep->RemoveSession(remote_address_);

  // Notify JS.
  HandleScope handle_scope(env()->isolate());
  Context::Scope context_scope(env()->context());
  Local<Value> argv[] = {};
  EmitCallback(DTLS_CB_SESSION_CLOSE, 0, argv);

  // Release the remaining resources. RemoveSession above already detached us,
  // so the one inside Destroy() is a no-op.
  Destroy();
}

void DTLSSession::Destroy() {
  if (destroyed_) return;
  destroyed_ = true;
  closed_ = true;

  state_->destroyed = 1;
  DTLS_STAT_RECORD_TIMESTAMP(DTLSSessionStats, destroyed_at);
  state_->open = 0;
  state_->handshaking = 0;

  retransmit_timer_.Close();

  // A parked exception is a strong reference to a JS Error, whose stack
  // normally reaches back to this session. Nothing will emit it now.
  pending_error_.Reset();

  // Promote to strong ref to keep endpoint alive during removal,
  // then release our weak pointer.
  BaseObjectPtr<DTLSEndpoint> ep = endpoint_;
  endpoint_.reset();
  if (ep) ep->RemoveSession(remote_address_);
}

void DTLSSession::SSLKeylogCallback(const SSL* ssl, const char* line) {
  DTLSSession* session = static_cast<DTLSSession*>(SSL_get_app_data(ssl));
  if (session == nullptr || session->destroyed_) return;

  // `line` carries the connection's secrets. Do not copy it into the JS heap
  // unless something is actually listening -- once it is a JS string it is
  // reachable from heap snapshots, core dumps and the inspector for as long as
  // the string lives.
  if (!session->state_->has_keylog_listener) return;

  HandleScope handle_scope(session->env()->isolate());
  Context::Scope context_scope(session->env()->context());

  Local<Value> str;
  if (!ToV8Value(session->env()->context(), line).ToLocal(&str)) {
    return;
  }
  // OpenSSL calls this from ssl_log_secret while deriving the master secret,
  // so this runs inside SSL_do_handshake(). MakeCallback() would drain the
  // microtask and tick queues here, letting a tick scheduled by the handler
  // re-enter this SSL -- session.close() calls SSL_shutdown() -- part-way
  // through a handshake transition. Call() runs the handler and nothing else.
  v8::TryCatch try_catch(session->env()->isolate());
  Local<Value> argv[] = {str};
  if (session->CallCallback(DTLS_CB_SESSION_KEYLOG, 1, argv).IsEmpty() &&
      try_catch.HasCaught() && !try_catch.HasTerminated()) {
    Local<Value> exception = try_catch.Exception();
    // Must not return into OpenSSL with an exception pending.
    try_catch.Reset();
    session->SetPendingError(exception);
  }
}

void DTLSSession::EmitSendError() {
  if (send_error_ == 0 || destroyed_) return;
  int err = send_error_;
  send_error_ = 0;

  HandleScope handle_scope(env()->isolate());
  Local<Value> message;
  if (!ToV8Value(env()->context(), uv_strerror(err)).ToLocal(&message)) {
    return;
  }
  Local<Value> argv[] = {message};
  EmitCallback(DTLS_CB_SESSION_ERROR, 1, argv);
}

void DTLSSession::EmitPendingError() {
  if (pending_error_.IsEmpty() || destroyed_) return;
  HandleScope handle_scope(env()->isolate());
  Local<Value> argv[] = {pending_error_.Get(env()->isolate())};
  pending_error_.Reset();
  EmitCallback(DTLS_CB_SESSION_ERROR, 1, argv);
}

MaybeLocal<Value> DTLSSession::CallCallback(int cb_index,
                                            int argc,
                                            Local<Value>* argv) {
  auto ep = endpoint_.get();
  if (ep == nullptr) return MaybeLocal<Value>();
  Local<Function> cb = ep->GetCallback(cb_index);
  if (cb.IsEmpty()) return MaybeLocal<Value>();

  return cb->Call(env()->context(), object(), argc, argv);
}

MaybeLocal<Value> DTLSSession::EmitCallback(int cb_index,
                                            int argc,
                                            Local<Value>* argv) {
  auto ep = endpoint_.get();
  if (ep == nullptr) return MaybeLocal<Value>();
  Local<Function> cb = ep->GetCallback(cb_index);
  if (cb.IsEmpty()) return MaybeLocal<Value>();

  return MakeCallback(cb, argc, argv);
}

// --- JS binding methods ---

void DTLSSession::DoStart(const FunctionCallbackInfo<Value>& args) {
  DTLSSession* session;
  ASSIGN_OR_RETURN_UNWRAP(&session, args.This());
  session->Start();
}

void DTLSSession::DoSend(const FunctionCallbackInfo<Value>& args) {
  DTLSSession* session;
  ASSIGN_OR_RETURN_UNWRAP(&session, args.This());

  // Any view over bytes, not only a Buffer. Buffer::HasInstance() is true for
  // every TypedArray, but a DataView is not one, and the rest of this module
  // -- exportKeyingMaterial(), the PSK key -- already takes both.
  if (!args[0]->IsArrayBufferView()) {
    return THROW_ERR_INVALID_ARG_TYPE(session->env(),
                                      "data must be a TypedArray or DataView");
  }

  ArrayBufferViewContents<uint8_t> view(args[0].As<ArrayBufferView>());
  const uint8_t* data = view.data();
  size_t len = view.length();

  int written = session->Send(data, len);
  if (written < 0) return;  // Send() has thrown.
  args.GetReturnValue().Set(written);
}

void DTLSSession::DoClose(const FunctionCallbackInfo<Value>& args) {
  DTLSSession* session;
  ASSIGN_OR_RETURN_UNWRAP(&session, args.This());
  session->Close();
}

void DTLSSession::DoDestroy(const FunctionCallbackInfo<Value>& args) {
  DTLSSession* session;
  ASSIGN_OR_RETURN_UNWRAP(&session, args.This());
  session->Destroy();
}

void DTLSSession::GetState(const FunctionCallbackInfo<Value>& args) {
  DTLSSession* session;
  ASSIGN_OR_RETURN_UNWRAP(&session, args.This());
  args.GetReturnValue().Set(session->state_.GetArrayBuffer());
}

void DTLSSession::GetStats(const FunctionCallbackInfo<Value>& args) {
  DTLSSession* session;
  ASSIGN_OR_RETURN_UNWRAP(&session, args.This());
  args.GetReturnValue().Set(session->stats_.GetArrayBuffer());
}

void DTLSSession::GetRemoteAddress(const FunctionCallbackInfo<Value>& args) {
  DTLSSession* session;
  ASSIGN_OR_RETURN_UNWRAP(&session, args.This());
  Environment* env = session->env();

  Local<Object> obj;
  if (session->remote_address_.ToJS(env).ToLocal(&obj)) {
    args.GetReturnValue().Set(obj);
  }
}

void DTLSSession::GetProtocol(const FunctionCallbackInfo<Value>& args) {
  DTLSSession* session;
  ASSIGN_OR_RETURN_UNWRAP(&session, args.This());

  const char* version = SSL_get_version(session->ssl_.get());
  Local<Value> str;
  if (!ToV8Value(session->env()->context(), version).ToLocal(&str)) {
    return;
  }
  args.GetReturnValue().Set(str);
}

void DTLSSession::GetCipher(const FunctionCallbackInfo<Value>& args) {
  DTLSSession* session;
  ASSIGN_OR_RETURN_UNWRAP(&session, args.This());
  Environment* env = session->env();

  args.GetReturnValue().Set(
      crypto::GetCipherInfo(env, session->ssl_).FromMaybe(Local<Object>()));
}

void DTLSSession::GetPeerCertificate(const FunctionCallbackInfo<Value>& args) {
  DTLSSession* session;
  ASSIGN_OR_RETURN_UNWRAP(&session, args.This());
  Environment* env = session->env();

  MarkPopErrorOnReturn mark_pop_error_on_return;

  X509* peer_cert = SSL_get0_peer_certificate(session->ssl_.get());
  if (peer_cert == nullptr) return;

  // Return the PEM-encoded certificate. This is the leaf only; the chain and
  // the parsed fields node:tls exposes are not available here.
  auto bio = ncrypto::BIOPointer::NewMem();
  if (!bio) return;

  if (PEM_write_bio_X509(bio.get(), peer_cert)) {
    char* data;
    long len = BIO_get_mem_data(bio.get(), &data);  // NOLINT(runtime/int)
    Local<Value> str;
    if (len > 0 &&
        ToV8Value(env->context(), std::string_view(data, len)).ToLocal(&str)) {
      args.GetReturnValue().Set(str);
    }
  }
}

void DTLSSession::GetPeerX509Certificate(
    const FunctionCallbackInfo<Value>& args) {
  DTLSSession* session;
  ASSIGN_OR_RETURN_UNWRAP(&session, args.This());
  Environment* env = session->env();

  MarkPopErrorOnReturn mark_pop_error_on_return;

  // The two sides need different handling: SSL_get_peer_cert_chain() omits
  // the peer's leaf on the server but includes it on the client, and this
  // flag is what tells X509Certificate which it is dealing with. Getting it
  // wrong drops or duplicates the leaf rather than failing outright.
  auto flag = session->is_server_
                  ? crypto::X509Certificate::GetPeerCertificateFlag::SERVER
                  : crypto::X509Certificate::GetPeerCertificateFlag::NONE;

  Local<Object> cert;
  if (crypto::X509Certificate::GetPeerCert(env, session->ssl_, flag)
          .ToLocal(&cert)) {
    args.GetReturnValue().Set(cert);
  }
}

void DTLSSession::GetALPNProtocol(const FunctionCallbackInfo<Value>& args) {
  DTLSSession* session;
  ASSIGN_OR_RETURN_UNWRAP(&session, args.This());

  const unsigned char* alpn = nullptr;
  unsigned int alpn_len = 0;
  SSL_get0_alpn_selected(session->ssl_.get(), &alpn, &alpn_len);

  if (alpn == nullptr || alpn_len == 0) return;

  Local<Value> str;
  if (!ToV8Value(
           session->env()->context(),
           std::string_view(reinterpret_cast<const char*>(alpn), alpn_len))
           .ToLocal(&str)) {
    return;
  }
  args.GetReturnValue().Set(str);
}

void DTLSSession::ExportKeyingMaterial(
    const FunctionCallbackInfo<Value>& args) {
  DTLSSession* session;
  ASSIGN_OR_RETURN_UNWRAP(&session, args.This());
  Environment* env = session->env();

  // The JS wrapper validates these; a bad value here is a bug on our side, not
  // a user error. Int32Value(...).FromJust() used to accept a negative length
  // straight into std::vector's constructor, which aborted the process.
  CHECK(args[0]->IsUint32());
  CHECK(args[1]->IsString());

  uint32_t length = args[0].As<Uint32>()->Value();
  Utf8Value label(env->isolate(), args[1]);

  const uint8_t* context_value = nullptr;
  size_t context_len = 0;
  bool use_context = false;

  if (args.Length() > 2 && Buffer::HasInstance(args[2])) {
    context_value = reinterpret_cast<const uint8_t*>(Buffer::Data(args[2]));
    context_len = Buffer::Length(args[2]);
    use_context = true;
  }

  MarkPopErrorOnReturn mark_pop_error_on_return;

  std::vector<uint8_t> out(length);
  int ret = SSL_export_keying_material(session->ssl_.get(),
                                       out.data(),
                                       length,
                                       *label,
                                       label.length(),
                                       context_value,
                                       context_len,
                                       use_context ? 1 : 0);

  if (ret != 1) {
    return THROW_ERR_CRYPTO_OPERATION_FAILED(
        env, "SSL_export_keying_material failed");
  }

  Local<Object> buf;
  if (!Buffer::Copy(env, reinterpret_cast<const char*>(out.data()), length)
           .ToLocal(&buf)) {
    return;
  }
  args.GetReturnValue().Set(buf);
}

void DTLSSession::GetSRTPProfile(const FunctionCallbackInfo<Value>& args) {
  DTLSSession* session;
  ASSIGN_OR_RETURN_UNWRAP(&session, args.This());

  const SRTP_PROTECTION_PROFILE* profile =
      SSL_get_selected_srtp_profile(session->ssl_.get());

  if (profile == nullptr) return;

  Local<Value> str;
  if (!ToV8Value(session->env()->context(), profile->name).ToLocal(&str)) {
    return;
  }
  args.GetReturnValue().Set(str);
}

void DTLSSession::GetVerifyError(const FunctionCallbackInfo<Value>& args) {
  DTLSSession* session;
  ASSIGN_OR_RETURN_UNWRAP(&session, args.This());

  MarkPopErrorOnReturn mark_pop_error_on_return;

  // Nothing has been verified before the handshake completes, and asking is
  // not merely meaningless but fatal: with no negotiated cipher,
  // SSL_get_current_cipher() is NULL and SSL_CIPHER_get_auth_nid(), which
  // ncrypto calls to allow for PSK, dereferences it without checking. The
  // session reaches JavaScript before its handshake runs, so this is
  // reachable from the listen() callback.
  if (!session->handshake_complete_) {
    args.GetReturnValue().Set(FIXED_ONE_BYTE_STRING(session->env()->isolate(),
                                                    "HANDSHAKE_INCOMPLETE"));
    return;
  }

  long verify_error = session->PeerVerifyResult();  // NOLINT(runtime/int)

  // undefined means authorized; anything else is the short error code, e.g.
  // "UNABLE_TO_GET_ISSUER_CERT" or "CERT_HAS_EXPIRED".
  if (verify_error == X509_V_OK) return;

  const char* code = ncrypto::X509Pointer::ErrorCode(verify_error);
  Local<Value> str;
  if (!ToV8Value(session->env()->context(), code).ToLocal(&str)) {
    return;
  }
  args.GetReturnValue().Set(str);
}

void DTLSSession::GetServername(const FunctionCallbackInfo<Value>& args) {
  DTLSSession* session;
  ASSIGN_OR_RETURN_UNWRAP(&session, args.This());

  const char* servername =
      SSL_get_servername(session->ssl_.get(), TLSEXT_NAMETYPE_host_name);
  if (servername == nullptr) return;

  Local<Value> str;
  if (!ToV8Value(session->env()->context(), servername).ToLocal(&str)) {
    return;
  }
  args.GetReturnValue().Set(str);
}

void DTLSSession::GetSession(const FunctionCallbackInfo<Value>& args) {
  DTLSSession* session;
  ASSIGN_OR_RETURN_UNWRAP(&session, args.This());
  Environment* env = session->env();

  if (!session->ssl_) return;

  // SSL_get1_session() takes a reference, which the pointer releases.
  ncrypto::SSLSessionPointer sess(SSL_get1_session(session->ssl_.get()));
  if (!sess) return;

  int size = i2d_SSL_SESSION(sess.get(), nullptr);
  if (size <= 0) return;

  auto store = ArrayBuffer::NewBackingStore(env->isolate(), size);
  unsigned char* p = static_cast<unsigned char*>(store->Data());
  if (i2d_SSL_SESSION(sess.get(), &p) <= 0) return;

  Local<ArrayBuffer> buffer =
      ArrayBuffer::New(env->isolate(), std::move(store));
  args.GetReturnValue().Set(Uint8Array::New(buffer, 0, size));
}

void DTLSSession::WasReused(const FunctionCallbackInfo<Value>& args) {
  DTLSSession* session;
  ASSIGN_OR_RETURN_UNWRAP(&session, args.This());
  if (!session->ssl_) return;
  args.GetReturnValue().Set(SSL_session_reused(session->ssl_.get()) == 1);
}

void DTLSSession::SetSNIContext(DTLSContext* context) {
  sni_context_ = BaseObjectPtr<DTLSContext>(context);
}

void DTLSSession::SetPendingError(Local<Value> error) {
  pending_error_.Reset(env()->isolate(), error);
}

void DTLSSession::MemoryInfo(MemoryTracker* tracker) const {
  tracker->TrackField("pending_error", pending_error_);
  tracker->TrackField("remote_address", remote_address_);
  tracker->TrackField("context", context_);
  tracker->TrackField("sni_context", sni_context_);
}

}  // namespace dtls
}  // namespace node

#endif  // HAVE_OPENSSL && HAVE_DTLS