#include "dtls_endpoint.h"
#include "dtls.h"
#include "dtls_context.h"
#include "dtls_session.h"
#if HAVE_OPENSSL && HAVE_DTLS
#include <aliased_struct-inl.h>
#include <base_object-inl.h>
#include <env-inl.h>
#include <handle_wrap.h>
#include <memory_tracker-inl.h>
#include <node_buffer.h>
#include <node_errors.h>
#include <node_sockaddr-inl.h>
#include <permission/permission.h>
#include <util-inl.h>
#include <openssl/err.h>
#include <openssl/ssl.h>
#include <cstddef>
#include <cstring>
namespace node {
using v8::ArrayBufferView;
using v8::Boolean;
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::HandleScope;
using v8::Int32;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::Uint32;
using v8::Value;
namespace dtls {
namespace {
struct SendReq {
uv_udp_send_t req;
uv_buf_t buf;
std::vector<uint8_t> data;
};
} // namespace
// The endpoint state "indices" are byte offsets into DTLSEndpointStateData,
// accessed from JS via a DataView. Pin them to the actual struct layout so a
// mismatch (as once existed for `busy`, which follows a uint32) can't recur.
static_assert(IDX_ENDPOINT_STATE_BOUND ==
offsetof(DTLSEndpointStateData, bound));
static_assert(IDX_ENDPOINT_STATE_LISTENING ==
offsetof(DTLSEndpointStateData, listening));
static_assert(IDX_ENDPOINT_STATE_CLOSING ==
offsetof(DTLSEndpointStateData, closing));
static_assert(IDX_ENDPOINT_STATE_DESTROYED ==
offsetof(DTLSEndpointStateData, destroyed));
static_assert(IDX_ENDPOINT_STATE_SESSION_COUNT ==
offsetof(DTLSEndpointStateData, session_count));
static_assert(IDX_ENDPOINT_STATE_BUSY == offsetof(DTLSEndpointStateData, busy));
DTLSEndpoint::DTLSEndpoint(Environment* env, Local<Object> wrap)
: HandleWrap(env,
wrap,
reinterpret_cast<uv_handle_t*>(&handle_),
PROVIDER_DTLS_ENDPOINT),
state_(env->isolate()),
stats_(env->isolate()) {
CHECK_EQ(uv_udp_init(env->event_loop(), &handle_), 0);
handle_.data = this;
MakeWeak();
DTLS_STAT_RECORD_TIMESTAMP(DTLSEndpointStats, created_at);
}
Local<FunctionTemplate> DTLSEndpoint::GetConstructorTemplate(Environment* env) {
auto tmpl = env->dtls_endpoint_constructor_template();
if (tmpl.IsEmpty()) {
Isolate* isolate = env->isolate();
tmpl = NewFunctionTemplate(isolate, New);
tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "DTLSEndpoint"));
tmpl->InstanceTemplate()->SetInternalFieldCount(
HandleWrap::kInternalFieldCount);
SetProtoMethod(isolate, tmpl, "bind", DoBind);
SetProtoMethod(isolate, tmpl, "listen", DoListen);
SetProtoMethod(isolate, tmpl, "connect", DoConnect);
SetProtoMethod(isolate, tmpl, "close", DoClose);
SetProtoMethod(isolate, tmpl, "destroy", DoDestroy);
SetProtoMethod(isolate, tmpl, "getState", GetState);
SetProtoMethod(isolate, tmpl, "getStats", GetStats);
SetProtoMethod(isolate, tmpl, "getAddress", GetAddress);
SetProtoMethod(isolate, tmpl, "setMTU", SetMTU);
SetProtoMethod(isolate, tmpl, "setSocketOptions", SetSocketOptions);
SetProtoMethod(isolate, tmpl, "setHandshakeTimeout", SetHandshakeTimeout);
SetProtoMethod(isolate, tmpl, "setSessionLimits", SetSessionLimits);
SetProtoMethod(isolate, tmpl, "setCallbacks", DoSetCallbacks);
env->set_dtls_endpoint_constructor_template(tmpl);
}
return tmpl;
}
void DTLSEndpoint::InitPerContext(Local<Object> target,
Local<Context> context,
Environment* env) {
SetConstructorFunction(context,
target,
"DTLSEndpoint",
GetConstructorTemplate(env),
SetConstructorFunctionFlag::NONE);
}
void DTLSEndpoint::RegisterExternalReferences(
ExternalReferenceRegistry* registry) {
registry->Register(New);
registry->Register(DoBind);
registry->Register(DoListen);
registry->Register(DoConnect);
registry->Register(DoClose);
registry->Register(DoDestroy);
registry->Register(GetState);
registry->Register(GetStats);
registry->Register(GetAddress);
registry->Register(SetMTU);
registry->Register(SetSocketOptions);
registry->Register(SetHandshakeTimeout);
registry->Register(SetSessionLimits);
registry->Register(DoSetCallbacks);
}
void DTLSEndpoint::New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(args.IsConstructCall());
new DTLSEndpoint(env, args.This());
}
int DTLSEndpoint::Bind(const SocketAddress& address) {
if (IsHandleClosing()) return UV_EINVAL;
if (state_->bound) return UV_EALREADY;
unsigned int flags = 0;
if (address.family() == AF_INET6 && ipv6_only_) {
flags |= UV_UDP_IPV6ONLY;
}
if (reuse_port_) {
flags |= UV_UDP_REUSEPORT;
}
int err = uv_udp_bind(&handle_, address.data(), flags);
if (err != 0) return err;
// Only once bound: there is no socket to set them on before that. A zero
// means the option was not given, so the system default stands.
auto* h = reinterpret_cast<uv_handle_t*>(&handle_);
if (udp_receive_buffer_size_ > 0) {
int size = static_cast<int>(udp_receive_buffer_size_);
err = uv_recv_buffer_size(h, &size);
if (err != 0) return err;
}
if (udp_send_buffer_size_ > 0) {
int size = static_cast<int>(udp_send_buffer_size_);
err = uv_send_buffer_size(h, &size);
if (err != 0) return err;
}
if (udp_ttl_ > 0) {
err = uv_udp_set_ttl(&handle_, static_cast<int>(udp_ttl_));
if (err != 0) return err;
}
state_->bound = 1;
// Don't keep the event loop alive unless we're listening or have sessions.
uv_unref(reinterpret_cast<uv_handle_t*>(&handle_));
return 0;
}
int DTLSEndpoint::Listen(DTLSContext* context) {
if (IsHandleClosing()) return UV_EINVAL;
if (listening_) return UV_EALREADY;
server_context_.reset(context);
listening_ = true;
state_->listening = 1;
// Start receiving UDP datagrams.
int err = uv_udp_recv_start(&handle_, OnAlloc, OnRecv);
if (err != 0) {
listening_ = false;
state_->listening = 0;
server_context_.reset();
return err;
}
// Ref the handle while listening.
uv_ref(reinterpret_cast<uv_handle_t*>(&handle_));
return 0;
}
BaseObjectPtr<DTLSSession> DTLSEndpoint::Connect(
DTLSContext* context,
const SocketAddress& remote,
const char* servername,
const char* verify_host,
bool verify_is_ip,
const ncrypto::Buffer<const unsigned char>& resume) {
if (IsHandleClosing()) {
THROW_ERR_INVALID_STATE(env(), "Endpoint is closing");
return {};
}
// Check if we already have a session for this address.
auto it = sessions_.find(remote);
if (it != sessions_.end()) {
THROW_ERR_INVALID_STATE(env(), "Session already exists for this address");
return {};
}
auto session = DTLSSession::Create(env(),
this,
context,
remote,
false /* is_server */,
servername,
verify_host,
verify_is_ip,
resume);
if (!session) return {};
sessions_[remote] = session;
sessions_per_host_[remote]++;
state_->session_count = sessions_.size();
DTLS_STAT_INCREMENT(DTLSEndpointStats, client_sessions);
// Ref the handle while we have sessions.
uv_ref(reinterpret_cast<uv_handle_t*>(&handle_));
// Start receiving if not already. Listen() checks this and unwinds; here it
// was ignored, so a failure left a session in the table that no datagram
// could ever reach, and the only report was its handshake timing out.
if (!listening_) {
int err = uv_udp_recv_start(&handle_, OnAlloc, OnRecv);
if (err != 0) {
RemoveSession(remote);
env()->ThrowUVException(err, "recv_start");
return {};
}
}
// The handshake is not started here. Cycle() emits the ClientHello and can
// report a failure doing it -- a send error, or an exception from a
// callback -- and there is nothing to report it to yet: the callback
// dispatch finds the JavaScript session through the handle, and the wrapper
// that attaches itself to the handle is built from the value this returns.
// An error from the first flight was raised against a wrapper that did not
// exist, dropped, and then left to look like a handshake that timed out.
// The wrapper calls start() once it is in place.
return session;
}
int DTLSEndpoint::SendTo(const SocketAddress& dest,
const uint8_t* data,
size_t len) {
if (IsHandleClosing()) return UV_EINVAL;
// Try synchronous send first.
uv_buf_t buf =
uv_buf_init(const_cast<char*>(reinterpret_cast<const char*>(data)), len);
int err = uv_udp_try_send(&handle_, &buf, 1, dest.data());
// A datagram is sent whole or not at all, and libuv documents a
// non-negative return as always matching the buffer size, so there is no
// partial send to resume. Testing for == len instead let any other
// non-negative value fall through to the queued send below and put the
// same datagram on the wire a second time.
if (err >= 0) {
DTLS_STAT_INCREMENT_N(DTLSEndpointStats, bytes_sent, len);
DTLS_STAT_INCREMENT(DTLSEndpointStats, packets_sent);
return 0;
}
if (err != UV_EAGAIN) {
return err; // Real error.
}
// EAGAIN: the socket buffer is full, so queue it.
// Async send: copy the data since it won't outlive this call.
auto* req = new SendReq();
req->data.assign(data, data + len);
req->buf = uv_buf_init(reinterpret_cast<char*>(req->data.data()), len);
err = uv_udp_send(&req->req, &handle_, &req->buf, 1, dest.data(), OnSend);
if (err != 0) {
delete req;
return err;
}
DTLS_STAT_INCREMENT_N(DTLSEndpointStats, bytes_sent, len);
DTLS_STAT_INCREMENT(DTLSEndpointStats, packets_sent);
return 0;
}
void DTLSEndpoint::RemoveSession(const SocketAddress& addr) {
if (sessions_.erase(addr) != 0) {
// Drop the host entry entirely at zero so this map tracks live peers
// rather than every peer ever seen.
auto it = sessions_per_host_.find(addr);
if (it != sessions_per_host_.end() && --it->second == 0) {
sessions_per_host_.erase(it);
}
}
state_->session_count = sessions_.size();
// Unref if no more sessions and not listening.
if (sessions_.empty() && !listening_ && !IsHandleClosing()) {
uv_unref(reinterpret_cast<uv_handle_t*>(&handle_));
}
}
void DTLSEndpoint::CloseGracefully() {
if (IsHandleClosing()) return;
state_->closing = 1;
// Close all sessions gracefully (this may send close_notify).
auto sessions_copy = sessions_;
sessions_.clear();
state_->session_count = 0;
for (auto& [addr, session] : sessions_copy) {
session->Close();
}
// Stop listening.
if (listening_) {
uv_udp_recv_stop(&handle_);
listening_ = false;
state_->listening = 0;
}
server_context_.reset();
// Keep ourselves alive until OnClose() runs, so a garbage collection while
// uv_close() is in flight cannot collect the wrapper before the close is
// reported. Released in OnClose().
self_ref_ = BaseObjectPtr<DTLSEndpoint>(this);
// HandleWrap::Close() calls uv_close and manages the lifecycle.
HandleWrap::Close();
}
void DTLSEndpoint::Destroy() {
if (IsHandleClosing()) return;
state_->destroyed = 1;
// Copy session list to avoid iterator invalidation.
auto sessions_copy = sessions_;
sessions_.clear();
state_->session_count = 0;
for (auto& [addr, session] : sessions_copy) {
session->Destroy();
}
server_context_.reset();
if (listening_) {
uv_udp_recv_stop(&handle_);
listening_ = false;
state_->listening = 0;
}
// Keep ourselves alive until OnClose() (see CloseGracefully()).
self_ref_ = BaseObjectPtr<DTLSEndpoint>(this);
HandleWrap::Close();
}
Local<Function> DTLSEndpoint::GetCallback(int index) const {
if (index < 0 || index >= DTLS_CB_COUNT) return Local<Function>();
Local<Function> cb = callbacks_[index].Get(env()->isolate());
return cb;
}
void DTLSEndpoint::SetCallbacks(Local<Object> callbacks) {
Isolate* isolate = env()->isolate();
Local<Context> context = env()->context();
const char* names[] = {
"onEndpointClose",
"onEndpointError",
"onSessionNew",
"onSessionClose",
"onSessionError",
"onSessionHandshake",
"onSessionMessage",
"onSessionKeylog",
};
for (int i = 0; i < DTLS_CB_COUNT; i++) {
Local<Value> name;
if (!ToV8Value(context, names[i]).ToLocal(&name)) {
THROW_ERR_OPERATION_FAILED(isolate,
"Failed to create callback name string");
return;
}
Local<Value> val;
if (!callbacks->Get(context, name).ToLocal(&val) || !val->IsFunction()) {
THROW_ERR_MISSING_ARGS(
isolate, ("Missing DTLS callback: " + std::string(names[i])).c_str());
return;
}
callbacks_[i].Reset(isolate, val.As<Function>());
}
}
// --- libuv callbacks ---
void DTLSEndpoint::OnAlloc(uv_handle_t* handle,
size_t suggested_size,
uv_buf_t* buf) {
DTLSEndpoint* endpoint = static_cast<DTLSEndpoint*>(handle->data);
// Reuse a single receive buffer. libuv delivers datagrams one at a time on
// this thread, and OnRecv fully consumes each datagram (copying it into the
// session's BIO) before the next OnAlloc, so a per-endpoint buffer suffices
// and avoids a heap allocation on every packet.
//
// One datagram at a time holds only because recvmmsg is off: libuv sets
// UV_HANDLE_UDP_RECVMMSG solely when uv_udp_init_ex() is passed
// UV_UDP_RECVMMSG, and DTLSEndpoint uses plain uv_udp_init(). Enabling it
// would have libuv pack several datagrams into this one buffer and deliver
// them as UV_UDP_MMSG_CHUNK slices, which this design does not handle.
//
// The size matters too: OnRecv drops anything flagged UV_UDP_PARTIAL, so a
// buffer below the 65507-byte maximum UDP payload would start discarding
// large datagrams rather than truncating them.
if (endpoint->recv_buf_.empty()) {
endpoint->recv_buf_.resize(65536);
}
buf->base = endpoint->recv_buf_.data();
buf->len = endpoint->recv_buf_.size();
}
void DTLSEndpoint::OnRecv(uv_udp_t* handle,
ssize_t nread,
const uv_buf_t* buf,
const struct sockaddr* addr,
unsigned int flags) {
DTLSEndpoint* endpoint = static_cast<DTLSEndpoint*>(handle->data);
// buf->base is the endpoint's reusable recv_buf_; it is not freed here.
if (nread == 0 && addr == nullptr) {
return;
}
if (nread < 0) {
HandleScope handle_scope(endpoint->env()->isolate());
Context::Scope context_scope(endpoint->env()->context());
Local<Value> message;
if (!ToV8Value(endpoint->env()->context(), uv_strerror(nread))
.ToLocal(&message)) {
return;
}
Local<Value> argv[] = {message};
Local<Function> cb = endpoint->GetCallback(DTLS_CB_ENDPOINT_ERROR);
if (!cb.IsEmpty()) {
endpoint->MakeCallback(cb, 1, argv);
}
return;
}
if (addr == nullptr) {
return;
}
// A truncated datagram is not a short DTLS record, it is a corrupt one, so
// drop it rather than hand a fragment to OpenSSL.
//
// This cannot currently fire. libuv raises UV_UDP_PARTIAL from MSG_TRUNC,
// which the kernel only sets when the datagram did not fit the buffer, and
// OnAlloc always supplies 65536 -- above the largest possible UDP payload,
// 65507 over IPv4 and 65527 over IPv6. The check is here so that shrinking
// that buffer degrades to dropped packets instead of corrupt records.
if ((flags & UV_UDP_PARTIAL) != 0) {
return;
}
IncrementStat<DTLSEndpointStats, &DTLSEndpointStats::bytes_received>(
endpoint->stats_.Data(), nread);
IncrementStat<DTLSEndpointStats, &DTLSEndpointStats::packets_received>(
endpoint->stats_.Data());
SocketAddress remote(addr);
endpoint->ProcessDatagram(
reinterpret_cast<const uint8_t*>(buf->base), nread, remote);
}
void DTLSEndpoint::OnSend(uv_udp_send_t* req, int status) {
SendReq* send_req = reinterpret_cast<SendReq*>(req);
delete send_req;
}
void DTLSEndpoint::OnClose() {
state_->closing = 0;
state_->destroyed = 1;
DTLS_STAT_RECORD_TIMESTAMP(DTLSEndpointStats, destroyed_at);
// Release the strong self-reference taken when the close was initiated.
// HandleWrap::OnClose still holds its own reference for the duration of this
// call, so this does not free us here.
self_ref_.reset();
// A close initiated outside CloseGracefully()/Destroy() (e.g. an endpoint
// abandoned mid-construction and closed at environment teardown) takes no
// self-reference, so its wrapper may already be collected. There is no JS
// side to notify in that case; skip it rather than touch a freed wrapper.
if (persistent().IsEmpty()) return;
Local<Function> cb = GetCallback(DTLS_CB_ENDPOINT_CLOSE);
if (!cb.IsEmpty()) {
Local<Value> argv[] = {};
MakeCallback(cb, 0, argv);
}
}
void DTLSEndpoint::ProcessDatagram(const uint8_t* data,
size_t len,
const SocketAddress& remote) {
if (IsHandleClosing()) return;
// An empty datagram is legal UDP but can never carry a DTLS record, and
// anyone can send one. Dropping it here keeps it out of the accept path,
// where it would otherwise cost a full SSL_new()/DTLSv1_listen()/SSL_free()
// cycle, and out of the session BIOs, where a zero length write to a
// datagram BIO queues an empty datagram whose read reports EOF rather than
// "try again".
if (len == 0) return;
// Look up existing session by remote address.
auto it = sessions_.find(remote);
if (it != sessions_.end()) {
it->second->Receive(data, len);
return;
}
// No existing session. If we're in server mode, try to accept.
if (listening_ && server_context_) {
AcceptConnection(data, len, remote);
}
}
bool DTLSEndpoint::HasCapacityFor(const SocketAddress& remote) const {
if (max_sessions_ != 0 && sessions_.size() >= max_sessions_) return false;
if (max_sessions_per_host_ != 0) {
auto it = sessions_per_host_.find(remote);
if (it != sessions_per_host_.end() && it->second >= max_sessions_per_host_)
return false;
}
return true;
}
// Cheap structural screen for a datagram that could plausibly begin a
// handshake, applied before anything is allocated for it.
//
// Everything reaching the accept path is unauthenticated and from a source
// address that has not been validated yet, and DTLSv1_listen() only ever
// proceeds on a ClientHello. Recognising the obvious rejects here avoids
// paying SSL_new() + two BIO_new()s + SSL_free() to reach the same verdict.
// This deliberately does not attempt to parse the ClientHello itself; that is
// OpenSSL's job, and getting it wrong would mean rejecting real clients.
bool DTLSEndpoint::CouldBeClientHello(const uint8_t* data, size_t len) {
// DTLS record header is 13 bytes, then the handshake msg_type.
if (len < 14) return false;
// ContentType must be handshake(22).
if (data[0] != 22) return false;
// ProtocolVersion is DTLS 1.0 (0xfeff) or 1.2 (0xfefd); both have major
// 0xfe. DTLS 1.3 keeps the record version at 0xfefd for compatibility.
if (data[1] != 0xfe) return false;
// The record must not claim more payload than the datagram actually holds.
const size_t record_len = (static_cast<size_t>(data[11]) << 8) | data[12];
if (record_len > len - 13) return false;
// HandshakeType must be client_hello(1).
if (data[13] != 1) return false;
return true;
}
void DTLSEndpoint::AcceptConnection(const uint8_t* data,
size_t len,
const SocketAddress& remote) {
if (state_->busy) {
DTLS_STAT_INCREMENT(DTLSEndpointStats, server_busy_count);
return;
}
if (!CouldBeClientHello(data, len)) {
DTLS_STAT_INCREMENT(DTLSEndpointStats, server_rejected_count);
return;
}
// Refuse before allocating anything. Staying silent rather than sending an
// alert is deliberate: the peer has not completed the cookie exchange yet,
// so replying would make this an amplification vector. A real client simply
// retransmits and gets in once there is room.
if (!HasCapacityFor(remote)) {
DTLS_STAT_INCREMENT(DTLSEndpointStats, server_refused_count);
return;
}
HandleScope handle_scope(env()->isolate());
Context::Scope context_scope(env()->context());
// Anything reaching this point is unauthenticated and may well be garbage,
// so DTLSv1_listen() failing is routine rather than exceptional. Discard
// whatever it queues instead of letting it accumulate across packets and
// resurface as a bogus error somewhere else on this thread.
ncrypto::MarkPopErrorOnReturn mark_pop_error_on_return;
// Stateless cookie exchange via DTLSv1_listen() for DoS protection.
//
// The standard OpenSSL DTLS server flow (see s_server.c) is:
// 1. Create SSL with BIO_s_datagram() wrapping the UDP socket
// 2. DTLSv1_listen(ssl, &peer) -- stateless cookie exchange
// 3. Connect the socket to the verified peer
// 4. SSL_accept(ssl) -- continue the handshake on the SAME SSL
//
// We diverge in one key way: we use memory BIOs instead of datagram
// BIOs because Node.js manages UDP I/O through libuv (uv_udp_t),
// not through raw socket FDs. This means DTLSv1_listen()'s internal
// BIO_dgram_get_peer()/set_peer() calls are no-ops -- we provide the
// peer address to the cookie callbacks via DTLSContext::current_cookie_peer_
// instead. After DTLSv1_listen() returns 1, we hand the SSL (with its
// memory BIOs) to a DTLSSession via CreateFromSSL(). The SSL's internal
// state machine has been prepared by DTLSv1_listen() to continue the
// handshake from TLS_ST_SR_CLNT_HELLO, so Cycle() -> SSL_do_handshake()
// immediately produces the ServerHello flight.
ncrypto::SSLPointer ssl(SSL_new(server_context_->ssl_ctx()));
if (!ssl) return;
// These become the session's enc_in_/enc_out_, so both have to preserve
// datagram boundaries -- see DTLSSession::Create().
auto in = ncrypto::BIOPointer::New(BIO_s_dgram_mem());
auto out = ncrypto::BIOPointer::New(BIO_s_dgram_mem());
if (!in || !out) return;
// SSL_set_bio takes ownership of both BIOs.
BIO* in_raw = in.release();
BIO* out_raw = out.release();
SSL_set_bio(ssl.get(), in_raw, out_raw);
SSL_set_accept_state(ssl.get());
// SSL_OP_COOKIE_EXCHANGE is deliberately not set here: DTLSv1_listen() sets
// it on the SSL it is given (d1_lib.c:804) before doing anything else, so
// setting it again just invites the question of why the context does not.
SSL_set_options(ssl.get(), SSL_OP_NO_QUERY_MTU);
SSL_set_mtu(ssl.get(), mtu_);
// Set peer address on context for the cookie callbacks.
server_context_->set_cookie_peer(remote);
// Both are allocation failures rather than anything the peer did, and both
// used to be ignored: an unwritten BIO would put DTLSv1_listen() to work on
// an empty buffer, and a null BIO_ADDR is not something it accepts. Dropping
// the datagram is the right answer -- the peer retransmits.
if (BIO_write(in_raw, data, len) <= 0) return;
DeleteFnPtr<BIO_ADDR, BIO_ADDR_free> peer(BIO_ADDR_new());
if (!peer) return;
int ret = DTLSv1_listen(ssl.get(), peer.get());
if (ret == 0) {
// Send HelloVerifyRequest. `out_raw` is a datagram BIO, so BIO_pending()
// gives the exact size of the next datagram; a HelloVerifyRequest is well
// under a hundred bytes, so this never leaves the stack in practice.
MaybeStackBuffer<uint8_t, 256> resp_buf;
for (;;) {
size_t pending = BIO_pending(out_raw);
if (pending == 0) break;
resp_buf.AllocateSufficientStorage(pending);
int resp_len = BIO_read(out_raw, resp_buf.out(), pending);
if (resp_len <= 0) break;
SendTo(remote, resp_buf.out(), resp_len);
}
return;
}
if (ret < 0) {
return; // Error — drop packet.
}
// Cookie verified. Hand the SSL (which has already completed cookie
// exchange and consumed the ClientHello) to a DTLSSession. Calling
// Cycle() will drive SSL_do_handshake to produce the ServerHello.
auto session = DTLSSession::CreateFromSSL(env(),
this,
server_context_.get(),
std::move(ssl),
in_raw,
out_raw,
remote);
if (!session) return;
sessions_[remote] = session;
sessions_per_host_[remote]++;
state_->session_count = sessions_.size();
DTLS_STAT_INCREMENT(DTLSEndpointStats, server_sessions);
uv_ref(reinterpret_cast<uv_handle_t*>(&handle_));
// Emit the new session to JS before driving the handshake, so a listener
// is in place for anything the handshake reports. Cycle() runs second.
Local<Value> argv[] = {session->object()};
Local<Function> cb = GetCallback(DTLS_CB_SESSION_NEW);
if (!cb.IsEmpty()) {
MakeCallback(cb, 1, argv);
}
// Drive the handshake forward — produces ServerHello etc.
session->Start();
}
// --- JS binding methods ---
void DTLSEndpoint::DoBind(const FunctionCallbackInfo<Value>& args) {
DTLSEndpoint* endpoint;
ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This());
Environment* env = endpoint->env();
CHECK(args[0]->IsString()); // host
CHECK(args[1]->IsInt32()); // port
Utf8Value host(env->isolate(), args[0]);
int port = args[1].As<Int32>()->Value();
SocketAddress addr;
if (!SocketAddress::New(*host, port, &addr)) {
return THROW_ERR_INVALID_ARG_VALUE(env, "Invalid address");
}
THROW_IF_INSUFFICIENT_PERMISSIONS(
env, permission::PermissionScope::kNet, addr.ToString());
int err = endpoint->Bind(addr);
if (err != 0) {
// A libuv failure reported as ERR_INVALID_STATE loses the errno and the
// syscall, so a caller cannot tell EADDRINUSE from anything else. Every
// other binding surfaces these as the error code libuv gave, and code
// testing err.code === 'EADDRINUSE' is the usual way this is handled.
return env->ThrowUVException(err, "bind");
}
}
void DTLSEndpoint::DoListen(const FunctionCallbackInfo<Value>& args) {
DTLSEndpoint* endpoint;
ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This());
Environment* env = endpoint->env();
THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kNet, "");
CHECK(DTLSContext::HasInstance(env, args[0]));
DTLSContext* context;
ASSIGN_OR_RETURN_UNWRAP(&context, args[0].As<Object>());
int err = endpoint->Listen(context);
if (err != 0) {
return env->ThrowUVException(err, "listen");
}
}
void DTLSEndpoint::DoConnect(const FunctionCallbackInfo<Value>& args) {
DTLSEndpoint* endpoint;
ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This());
Environment* env = endpoint->env();
CHECK(DTLSContext::HasInstance(env, args[0]));
DTLSContext* context;
ASSIGN_OR_RETURN_UNWRAP(&context, args[0].As<Object>());
CHECK(args[1]->IsString()); // host
CHECK(args[2]->IsInt32()); // port
Utf8Value host(env->isolate(), args[1]);
int port = args[2].As<Int32>()->Value();
SocketAddress remote;
if (!SocketAddress::New(*host, port, &remote)) {
return THROW_ERR_INVALID_ARG_VALUE(env, "Invalid remote address");
}
THROW_IF_INSUFFICIENT_PERMISSIONS(
env, permission::PermissionScope::kNet, remote.ToString());
// Optional: servername (SNI), verifyHost (expected peer identity), and
// whether verifyHost is an IP literal. These are resolved in JS and applied
// to the client SSL before the handshake starts.
Utf8Value servername(env->isolate(), args[3]);
Utf8Value verify_host(env->isolate(), args[4]);
const char* servername_ptr = args[3]->IsString() ? *servername : nullptr;
const char* verify_host_ptr = args[4]->IsString() ? *verify_host : nullptr;
bool verify_is_ip = args[5]->IsTrue();
// Optional DER-encoded session to resume. The identity it was authenticated
// for is checked in JS before it reaches here.
ncrypto::Buffer<const unsigned char> resume{};
ArrayBufferViewContents<unsigned char> resume_buf;
if (args[6]->IsArrayBufferView()) {
resume_buf.Read(args[6].As<ArrayBufferView>());
resume = {resume_buf.data(), resume_buf.length()};
}
auto session = endpoint->Connect(
context, remote, servername_ptr, verify_host_ptr, verify_is_ip, resume);
if (session) {
args.GetReturnValue().Set(session->object());
}
}
void DTLSEndpoint::DoClose(const FunctionCallbackInfo<Value>& args) {
DTLSEndpoint* endpoint;
ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This());
endpoint->CloseGracefully();
}
void DTLSEndpoint::DoDestroy(const FunctionCallbackInfo<Value>& args) {
DTLSEndpoint* endpoint;
ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This());
endpoint->Destroy();
}
void DTLSEndpoint::GetState(const FunctionCallbackInfo<Value>& args) {
DTLSEndpoint* endpoint;
ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This());
args.GetReturnValue().Set(endpoint->state_.GetArrayBuffer());
}
void DTLSEndpoint::GetStats(const FunctionCallbackInfo<Value>& args) {
DTLSEndpoint* endpoint;
ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This());
args.GetReturnValue().Set(endpoint->stats_.GetArrayBuffer());
}
void DTLSEndpoint::GetAddress(const FunctionCallbackInfo<Value>& args) {
DTLSEndpoint* endpoint;
ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This());
if (endpoint->IsHandleClosing()) return;
SocketAddress addr = SocketAddress::FromSockName(endpoint->handle_);
Local<Object> obj;
if (addr.ToJS(endpoint->env()).ToLocal(&obj)) {
args.GetReturnValue().Set(obj);
}
}
void DTLSEndpoint::SetSessionLimits(const FunctionCallbackInfo<Value>& args) {
DTLSEndpoint* endpoint;
ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This());
CHECK(args[0]->IsUint32());
CHECK(args[1]->IsUint32());
endpoint->max_sessions_ = args[0].As<Uint32>()->Value();
endpoint->max_sessions_per_host_ = args[1].As<Uint32>()->Value();
}
void DTLSEndpoint::SetMTU(const FunctionCallbackInfo<Value>& args) {
DTLSEndpoint* endpoint;
ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This());
CHECK(args[0]->IsInt32());
int mtu = args[0].As<Int32>()->Value();
if (mtu < 256 || mtu > 65535) {
return THROW_ERR_OUT_OF_RANGE(endpoint->env(),
"MTU must be between 256 and 65535");
}
// Only affects sessions created after this point: DTLSSession reads the
// value once, when it builds its SSL. Not currently reachable after
// construction -- JS calls this from the DTLSEndpoint constructor only.
endpoint->mtu_ = mtu;
}
void DTLSEndpoint::SetSocketOptions(const FunctionCallbackInfo<Value>& args) {
DTLSEndpoint* endpoint;
ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This());
// Validated in JavaScript, and read before Bind(), which is what applies
// them. The DTLSEndpoint constructor is the only caller.
CHECK(args[0]->IsBoolean()); // ipv6Only
CHECK(args[1]->IsBoolean()); // reusePort
CHECK(args[2]->IsUint32()); // udpReceiveBufferSize
CHECK(args[3]->IsUint32()); // udpSendBufferSize
CHECK(args[4]->IsUint32()); // udpTTL
endpoint->ipv6_only_ = args[0].As<Boolean>()->Value();
endpoint->reuse_port_ = args[1].As<Boolean>()->Value();
endpoint->udp_receive_buffer_size_ = args[2].As<Uint32>()->Value();
endpoint->udp_send_buffer_size_ = args[3].As<Uint32>()->Value();
endpoint->udp_ttl_ = args[4].As<Uint32>()->Value();
}
void DTLSEndpoint::SetHandshakeTimeout(
const FunctionCallbackInfo<Value>& args) {
DTLSEndpoint* endpoint;
ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This());
CHECK(args[0]->IsNumber());
double timeout = args[0].As<Number>()->Value();
// Read once per session, when it is created, like the MTU above.
endpoint->handshake_timeout_ = static_cast<uint64_t>(timeout);
}
void DTLSEndpoint::DoSetCallbacks(const FunctionCallbackInfo<Value>& args) {
DTLSEndpoint* endpoint;
ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This());
CHECK(args[0]->IsObject());
endpoint->SetCallbacks(args[0].As<Object>());
}
void DTLSEndpoint::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("sessions", sessions_.size());
tracker->TrackFieldWithSize("recv_buf", recv_buf_.size());
}
} // namespace dtls
} // namespace node
#endif // HAVE_OPENSSL && HAVE_DTLS