%PDF- %PDF-
| Direktori : /home/vacivi36/vittasync.vacivitta.com.br/vittasync/node/src/quic/ |
| Current File : /home/vacivi36/vittasync.vacivitta.com.br/vittasync/node/src/quic/endpoint.cc |
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
#include "endpoint.h"
#include <aliased_struct-inl.h>
#include <async_wrap-inl.h>
#include <debug_utils-inl.h>
#include <env-inl.h>
#include <memory_tracker-inl.h>
#include <ngtcp2/ngtcp2.h>
#include <node_errors.h>
#include <node_external_reference.h>
#include <node_sockaddr-inl.h>
#include <req_wrap-inl.h>
#include <util-inl.h>
#include <uv.h>
#include <v8.h>
#include <limits>
#include "application.h"
#include "bindingdata.h"
#include "defs.h"
namespace node {
using v8::ArrayBufferView;
using v8::BackingStore;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::HandleScope;
using v8::Int32;
using v8::Integer;
using v8::Just;
using v8::Local;
using v8::Maybe;
using v8::Nothing;
using v8::Number;
using v8::Object;
using v8::ObjectTemplate;
using v8::PropertyAttribute;
using v8::String;
using v8::Uint32;
using v8::Value;
namespace quic {
#define ENDPOINT_STATE(V) \
/* Bound to the UDP port */ \
V(BOUND, bound, uint8_t) \
/* Receiving packets on the UDP port */ \
V(RECEIVING, receiving, uint8_t) \
/* Listening as a QUIC server */ \
V(LISTENING, listening, uint8_t) \
/* In the process of closing down, waiting for pending send callbacks */ \
V(CLOSING, closing, uint8_t) \
/* Temporarily paused serving new initial requests */ \
V(BUSY, busy, uint8_t) \
/* The number of pending send callbacks */ \
V(PENDING_CALLBACKS, pending_callbacks, uint64_t)
#define ENDPOINT_STATS(V) \
V(CREATED_AT, created_at) \
V(DESTROYED_AT, destroyed_at) \
V(BYTES_RECEIVED, bytes_received) \
V(BYTES_SENT, bytes_sent) \
V(PACKETS_RECEIVED, packets_received) \
V(PACKETS_SENT, packets_sent) \
V(SERVER_SESSIONS, server_sessions) \
V(CLIENT_SESSIONS, client_sessions) \
V(SERVER_BUSY_COUNT, server_busy_count) \
V(RETRY_COUNT, retry_count) \
V(VERSION_NEGOTIATION_COUNT, version_negotiation_count) \
V(STATELESS_RESET_COUNT, stateless_reset_count) \
V(IMMEDIATE_CLOSE_COUNT, immediate_close_count)
#define ENDPOINT_CC(V) \
V(RENO, reno) \
V(CUBIC, cubic) \
V(BBR, bbr)
struct Endpoint::State {
#define V(_, name, type) type name;
ENDPOINT_STATE(V)
#undef V
};
STAT_STRUCT(Endpoint, ENDPOINT)
// ============================================================================
// Endpoint::Options
namespace {
#ifdef DEBUG
bool is_diagnostic_packet_loss(double probability) {
if (LIKELY(probability == 0.0)) return false;
unsigned char c = 255;
CHECK(crypto::CSPRNG(&c, 1).is_ok());
return (static_cast<double>(c) / 255) < probability;
}
#endif // DEBUG
Maybe<ngtcp2_cc_algo> getAlgoFromString(Environment* env, Local<String> input) {
auto& state = BindingData::Get(env);
#define V(name, str) \
if (input->StringEquals(state.str##_string())) { \
return Just(NGTCP2_CC_ALGO_##name); \
}
ENDPOINT_CC(V)
#undef V
return Nothing<ngtcp2_cc_algo>();
}
template <typename Opt, ngtcp2_cc_algo Opt::*member>
bool SetOption(Environment* env,
Opt* options,
const Local<Object>& object,
const Local<String>& name) {
Local<Value> value;
if (!object->Get(env->context(), name).ToLocal(&value)) return false;
if (!value->IsUndefined()) {
ngtcp2_cc_algo algo;
if (value->IsString()) {
if (!getAlgoFromString(env, value.As<String>()).To(&algo)) {
THROW_ERR_INVALID_ARG_VALUE(env, "The cc_algorithm option is invalid");
return false;
}
} else {
if (!value->IsInt32()) {
THROW_ERR_INVALID_ARG_VALUE(
env, "The cc_algorithm option must be a string or an integer");
return false;
}
Local<Int32> num;
if (!value->ToInt32(env->context()).ToLocal(&num)) {
THROW_ERR_INVALID_ARG_VALUE(env, "The cc_algorithm option is invalid");
return false;
}
switch (num->Value()) {
#define V(name, _) \
case NGTCP2_CC_ALGO_##name: \
break;
ENDPOINT_CC(V)
#undef V
default:
THROW_ERR_INVALID_ARG_VALUE(env,
"The cc_algorithm option is invalid");
return false;
}
algo = static_cast<ngtcp2_cc_algo>(num->Value());
}
options->*member = algo;
}
return true;
}
#if DEBUG
template <typename Opt, double Opt::*member>
bool SetOption(Environment* env,
Opt* options,
const Local<Object>& object,
const Local<String>& name) {
Local<Value> value;
if (!object->Get(env->context(), name).ToLocal(&value)) return false;
if (!value->IsUndefined()) {
Local<Number> num;
if (!value->ToNumber(env->context()).ToLocal(&num)) {
Utf8Value nameStr(env->isolate(), name);
THROW_ERR_INVALID_ARG_VALUE(
env, "The %s option must be a number", *nameStr);
return false;
}
options->*member = num->Value();
}
return true;
}
#endif // DEBUG
template <typename Opt, uint8_t Opt::*member>
bool SetOption(Environment* env,
Opt* options,
const Local<Object>& object,
const Local<String>& name) {
Local<Value> value;
if (!object->Get(env->context(), name).ToLocal(&value)) return false;
if (!value->IsUndefined()) {
if (!value->IsUint32()) {
Utf8Value nameStr(env->isolate(), name);
THROW_ERR_INVALID_ARG_VALUE(
env, "The %s option must be an uint8", *nameStr);
return false;
}
Local<Uint32> num;
if (!value->ToUint32(env->context()).ToLocal(&num) ||
num->Value() > std::numeric_limits<uint8_t>::max()) {
Utf8Value nameStr(env->isolate(), name);
THROW_ERR_INVALID_ARG_VALUE(
env, "The %s option must be an uint8", *nameStr);
return false;
}
options->*member = num->Value();
}
return true;
}
template <typename Opt, TokenSecret Opt::*member>
bool SetOption(Environment* env,
Opt* options,
const Local<Object>& object,
const Local<String>& name) {
Local<Value> value;
if (!object->Get(env->context(), name).ToLocal(&value)) return false;
if (!value->IsUndefined()) {
if (!value->IsArrayBufferView()) {
Utf8Value nameStr(env->isolate(), name);
THROW_ERR_INVALID_ARG_VALUE(
env, "The %s option must be an ArrayBufferView", *nameStr);
return false;
}
Store store(value.As<ArrayBufferView>());
if (store.length() != TokenSecret::QUIC_TOKENSECRET_LEN) {
Utf8Value nameStr(env->isolate(), name);
THROW_ERR_INVALID_ARG_VALUE(
env,
"The %s option must be an ArrayBufferView of length %d",
*nameStr,
TokenSecret::QUIC_TOKENSECRET_LEN);
return false;
}
ngtcp2_vec buf = store;
TokenSecret secret(buf.base);
options->*member = secret;
}
return true;
}
} // namespace
Maybe<Endpoint::Options> Endpoint::Options::From(Environment* env,
Local<Value> value) {
if (value.IsEmpty() || !value->IsObject()) {
THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object");
return Nothing<Options>();
}
auto& state = BindingData::Get(env);
auto params = value.As<Object>();
Options options;
#define SET(name) \
SetOption<Endpoint::Options, &Endpoint::Options::name>( \
env, &options, params, state.name##_string())
if (!SET(retry_token_expiration) || !SET(token_expiration) ||
!SET(max_connections_per_host) || !SET(max_connections_total) ||
!SET(max_stateless_resets) || !SET(address_lru_size) ||
!SET(max_retries) || !SET(max_payload_size) ||
!SET(unacknowledged_packet_threshold) || !SET(validate_address) ||
!SET(disable_stateless_reset) || !SET(ipv6_only) ||
!SET(handshake_timeout) || !SET(max_stream_window) || !SET(max_window) ||
!SET(no_udp_payload_size_shaping) ||
#ifdef DEBUG
!SET(rx_loss) || !SET(tx_loss) ||
#endif
!SET(cc_algorithm) || !SET(udp_receive_buffer_size) ||
!SET(udp_send_buffer_size) || !SET(udp_ttl) || !SET(reset_token_secret) ||
!SET(token_secret)) {
return Nothing<Options>();
}
Local<Value> address;
if (!params->Get(env->context(), env->address_string()).ToLocal(&address)) {
return Nothing<Options>();
}
if (!address->IsUndefined()) {
if (!SocketAddressBase::HasInstance(env, address)) {
THROW_ERR_INVALID_ARG_TYPE(env,
"The address option must be a SocketAddress");
return Nothing<Options>();
}
auto addr = FromJSObject<SocketAddressBase>(address.As<v8::Object>());
options.local_address = addr->address();
} else {
options.local_address = std::make_shared<SocketAddress>();
if (!SocketAddress::New("127.0.0.1", 0, options.local_address.get())) {
THROW_ERR_INVALID_ADDRESS(env);
return Nothing<Options>();
}
}
return Just<Options>(options);
#undef SET
}
void Endpoint::Options::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("reset_token_secret", reset_token_secret);
tracker->TrackField("token_secret", token_secret);
}
std::string Endpoint::Options::ToString() const {
DebugIndentScope indent;
auto prefix = indent.Prefix();
auto boolToString = [](uint8_t val) {
return val ? std::string("yes") : std::string("no");
};
std::string res = "{ ";
res += prefix + "local address: " + local_address->ToString();
res += prefix +
"retry token expiration: " + std::to_string(retry_token_expiration) +
" seconds";
res += prefix + "token expiration: " + std::to_string(token_expiration) +
" seconds";
res += prefix + "max connections per host: " +
std::to_string(max_connections_per_host);
res += prefix +
"max connections total: " + std::to_string(max_connections_total);
res +=
prefix + "max stateless resets: " + std::to_string(max_stateless_resets);
res += prefix + "address lru size: " + std::to_string(address_lru_size);
res += prefix + "max retries: " + std::to_string(max_retries);
res += prefix + "max payload size: " + std::to_string(max_payload_size);
res += prefix + "unacknowledged packet threshold: " +
std::to_string(unacknowledged_packet_threshold);
if (handshake_timeout == UINT64_MAX) {
res += prefix + "handshake timeout: <none>";
} else {
res += prefix + "handshake timeout: " + std::to_string(handshake_timeout) +
" nanoseconds";
}
res += prefix + "max stream window: " + std::to_string(max_stream_window);
res += prefix + "max window: " + std::to_string(max_window);
res += prefix + "no udp payload size shaping: " +
boolToString(no_udp_payload_size_shaping);
res += prefix + "validate address: " + boolToString(validate_address);
res += prefix +
"disable stateless reset: " + boolToString(disable_stateless_reset);
#ifdef DEBUG
res += prefix + "rx loss: " + std::to_string(rx_loss);
res += prefix + "tx loss: " + std::to_string(tx_loss);
#endif
auto ccalg = ([&] {
switch (cc_algorithm) {
case NGTCP2_CC_ALGO_RENO:
return "reno";
case NGTCP2_CC_ALGO_CUBIC:
return "cubic";
case NGTCP2_CC_ALGO_BBR:
return "bbr";
}
return "<unknown>";
})();
res += prefix + "cc algorithm: " + std::string(ccalg);
res += prefix + "reset token secret: " + reset_token_secret.ToString();
res += prefix + "token secret: " + token_secret.ToString();
res += prefix + "ipv6 only: " + boolToString(ipv6_only);
res += prefix +
"udp receive buffer size: " + std::to_string(udp_receive_buffer_size);
res +=
prefix + "udp send buffer size: " + std::to_string(udp_send_buffer_size);
res += prefix + "udp ttl: " + std::to_string(udp_ttl);
res += indent.Close();
return res;
}
// ======================================================================================
// Endpoint::UDP and Endpoint::UDP::Impl
class Endpoint::UDP::Impl final : public HandleWrap {
public:
static Local<FunctionTemplate> GetConstructorTemplate(Environment* env) {
auto& state = BindingData::Get(env);
auto tmpl = state.udp_constructor_template();
if (tmpl.IsEmpty()) {
tmpl = NewFunctionTemplate(env->isolate(), IllegalConstructor);
tmpl->Inherit(HandleWrap::GetConstructorTemplate(env));
tmpl->InstanceTemplate()->SetInternalFieldCount(
HandleWrap::kInternalFieldCount);
tmpl->SetClassName(state.endpoint_udp_string());
state.set_udp_constructor_template(tmpl);
}
return tmpl;
}
static Impl* Create(Endpoint* endpoint) {
Local<Object> obj;
if (!GetConstructorTemplate(endpoint->env())
->InstanceTemplate()
->NewInstance(endpoint->env()->context())
.ToLocal(&obj)) {
return nullptr;
}
return new Impl(endpoint, obj);
}
static Impl* From(uv_udp_t* handle) {
return ContainerOf(&Impl::handle_, handle);
}
static Impl* From(uv_handle_t* handle) {
return From(reinterpret_cast<uv_udp_t*>(handle));
}
Impl(Endpoint* endpoint, Local<Object> object)
: HandleWrap(endpoint->env(),
object,
reinterpret_cast<uv_handle_t*>(&handle_),
AsyncWrap::PROVIDER_QUIC_UDP),
endpoint_(endpoint) {
CHECK_EQ(uv_udp_init(endpoint->env()->event_loop(), &handle_), 0);
handle_.data = this;
}
SET_NO_MEMORY_INFO()
SET_MEMORY_INFO_NAME(Endpoint::UDP::Impl)
SET_SELF_SIZE(Impl)
private:
static void OnAlloc(uv_handle_t* handle,
size_t suggested_size,
uv_buf_t* buf) {
*buf = From(handle)->env()->allocate_managed_buffer(suggested_size);
}
static void OnReceive(uv_udp_t* handle,
ssize_t nread,
const uv_buf_t* buf,
const sockaddr* addr,
unsigned int flags) {
// Nothing to do in these cases. Specifically, if the nread
// is zero or we've received a partial packet, we're just
// going to ignore it.
if (nread == 0 || flags & UV_UDP_PARTIAL) return;
auto impl = From(handle);
DCHECK_NOT_NULL(impl);
DCHECK_NOT_NULL(impl->endpoint_);
if (nread < 0) {
impl->endpoint_->Destroy(CloseContext::RECEIVE_FAILURE,
static_cast<int>(nread));
return;
}
impl->endpoint_->Receive(uv_buf_init(buf->base, static_cast<size_t>(nread)),
SocketAddress(addr));
}
uv_udp_t handle_;
Endpoint* endpoint_;
friend class UDP;
};
Endpoint::UDP::UDP(Endpoint* endpoint) : impl_(Impl::Create(endpoint)) {
DCHECK(impl_);
}
Endpoint::UDP::~UDP() {
Close();
}
int Endpoint::UDP::Bind(const Endpoint::Options& options) {
if (is_bound_) return UV_EALREADY;
if (is_closed_or_closing()) return UV_EBADF;
int flags = 0;
if (options.local_address->family() == AF_INET6 && options.ipv6_only)
flags |= UV_UDP_IPV6ONLY;
int err = uv_udp_bind(&impl_->handle_, options.local_address->data(), flags);
int size;
if (!err) {
is_bound_ = true;
size = static_cast<int>(options.udp_receive_buffer_size);
if (size > 0) {
err = uv_recv_buffer_size(reinterpret_cast<uv_handle_t*>(&impl_->handle_),
&size);
if (err) return err;
}
size = static_cast<int>(options.udp_send_buffer_size);
if (size > 0) {
err = uv_send_buffer_size(reinterpret_cast<uv_handle_t*>(&impl_->handle_),
&size);
if (err) return err;
}
size = static_cast<int>(options.udp_ttl);
if (size > 0) {
err = uv_udp_set_ttl(&impl_->handle_, size);
if (err) return err;
}
}
return err;
}
void Endpoint::UDP::Ref() {
if (!is_closed_or_closing()) {
uv_ref(reinterpret_cast<uv_handle_t*>(&impl_->handle_));
}
}
void Endpoint::UDP::Unref() {
if (!is_closed_or_closing()) {
uv_unref(reinterpret_cast<uv_handle_t*>(&impl_->handle_));
}
}
int Endpoint::UDP::Start() {
if (is_closed_or_closing()) return UV_EBADF;
if (is_started_) return 0;
int err = uv_udp_recv_start(&impl_->handle_, Impl::OnAlloc, Impl::OnReceive);
is_started_ = (err == 0);
return err;
}
void Endpoint::UDP::Stop() {
if (is_closed_or_closing() || !is_started_) return;
USE(uv_udp_recv_stop(&impl_->handle_));
is_started_ = false;
}
void Endpoint::UDP::Close() {
if (is_closed_or_closing()) return;
DCHECK(impl_);
Stop();
is_bound_ = false;
is_closed_ = true;
impl_->Close();
impl_.reset();
}
bool Endpoint::UDP::is_bound() const {
return is_bound_;
}
bool Endpoint::UDP::is_closed() const {
return is_closed_;
}
bool Endpoint::UDP::is_closed_or_closing() const {
if (is_closed() || !impl_) return true;
return impl_->IsHandleClosing();
}
Endpoint::UDP::operator bool() const {
return impl_;
}
SocketAddress Endpoint::UDP::local_address() const {
DCHECK(!is_closed_or_closing() && is_bound());
return SocketAddress::FromSockName(impl_->handle_);
}
int Endpoint::UDP::Send(Packet* packet) {
if (is_closed_or_closing()) return UV_EBADF;
DCHECK_NOT_NULL(packet);
uv_buf_t buf = *packet;
// We don't use the default implementation of Dispatch because the packet
// itself is going to be reset and added to a freelist to be reused. The
// default implementation of Dispatch will cause the packet to be deleted,
// which we don't want. We call ClearWeak here just to be doubly sure.
packet->ClearWeak();
packet->Dispatched();
int err = uv_udp_send(
packet->req(),
&impl_->handle_,
&buf,
1,
packet->destination().data(),
uv_udp_send_cb{[](uv_udp_send_t* req, int status) {
auto ptr = static_cast<Packet*>(ReqWrap<uv_udp_send_t>::from_req(req));
ptr->env()->DecreaseWaitingRequestCounter();
ptr->Done(status);
}});
if (err < 0) {
// The packet failed.
packet->Done(err);
} else {
packet->env()->IncreaseWaitingRequestCounter();
}
return err;
}
void Endpoint::UDP::MemoryInfo(MemoryTracker* tracker) const {
if (impl_) tracker->TrackField("impl", impl_);
}
// ============================================================================
bool Endpoint::HasInstance(Environment* env, Local<Value> value) {
return GetConstructorTemplate(env)->HasInstance(value);
}
Local<FunctionTemplate> Endpoint::GetConstructorTemplate(Environment* env) {
auto& state = BindingData::Get(env);
auto tmpl = state.endpoint_constructor_template();
if (tmpl.IsEmpty()) {
auto isolate = env->isolate();
tmpl = NewFunctionTemplate(isolate, New);
tmpl->Inherit(AsyncWrap::GetConstructorTemplate(env));
tmpl->SetClassName(state.endpoint_string());
tmpl->InstanceTemplate()->SetInternalFieldCount(
Endpoint::kInternalFieldCount);
SetProtoMethod(isolate, tmpl, "listen", DoListen);
SetProtoMethod(isolate, tmpl, "closeGracefully", DoCloseGracefully);
SetProtoMethod(isolate, tmpl, "connect", DoConnect);
SetProtoMethod(isolate, tmpl, "markBusy", MarkBusy);
SetProtoMethod(isolate, tmpl, "ref", Ref);
SetProtoMethodNoSideEffect(isolate, tmpl, "address", LocalAddress);
state.set_endpoint_constructor_template(tmpl);
}
return tmpl;
}
void Endpoint::InitPerIsolate(IsolateData* data, Local<ObjectTemplate> target) {
// TODO(@jasnell): Implement the per-isolate state
}
void Endpoint::InitPerContext(Realm* realm, Local<Object> target) {
#define V(name, str) \
NODE_DEFINE_CONSTANT(target, QUIC_CC_ALGO_##name); \
NODE_DEFINE_STRING_CONSTANT(target, "QUIC_CC_ALGO_" #name "_STR", #str);
ENDPOINT_CC(V)
#undef V
#define V(name, _) IDX_STATS_ENDPOINT_##name,
enum IDX_STATS_ENDPONT { ENDPOINT_STATS(V) IDX_STATS_ENDPOINT_COUNT };
NODE_DEFINE_CONSTANT(target, IDX_STATS_ENDPOINT_COUNT);
#undef V
#define V(name, key) NODE_DEFINE_CONSTANT(target, IDX_STATS_ENDPOINT_##name);
ENDPOINT_STATS(V);
#undef V
#define V(name, key, type) \
static constexpr auto IDX_STATE_ENDPOINT_##name = \
offsetof(Endpoint::State, key); \
static constexpr auto IDX_STATE_ENDPOINT_##name##_SIZE = sizeof(type); \
NODE_DEFINE_CONSTANT(target, IDX_STATE_ENDPOINT_##name); \
NODE_DEFINE_CONSTANT(target, IDX_STATE_ENDPOINT_##name##_SIZE);
ENDPOINT_STATE(V)
#undef V
NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_CONNECTIONS);
NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_CONNECTIONS_PER_HOST);
NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE);
NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_STATELESS_RESETS);
NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_RETRY_LIMIT);
static constexpr auto DEFAULT_RETRYTOKEN_EXPIRATION =
RetryToken::QUIC_DEFAULT_RETRYTOKEN_EXPIRATION / NGTCP2_SECONDS;
static constexpr auto DEFAULT_REGULARTOKEN_EXPIRATION =
RegularToken::QUIC_DEFAULT_REGULARTOKEN_EXPIRATION / NGTCP2_SECONDS;
static constexpr auto DEFAULT_MAX_PACKET_LENGTH = kDefaultMaxPacketLength;
NODE_DEFINE_CONSTANT(target, DEFAULT_RETRYTOKEN_EXPIRATION);
NODE_DEFINE_CONSTANT(target, DEFAULT_REGULARTOKEN_EXPIRATION);
NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_PACKET_LENGTH);
SetConstructorFunction(realm->context(),
target,
"Endpoint",
GetConstructorTemplate(realm->env()));
}
void Endpoint::RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(New);
registry->Register(DoConnect);
registry->Register(DoListen);
registry->Register(DoCloseGracefully);
registry->Register(LocalAddress);
registry->Register(Ref);
registry->Register(MarkBusy);
}
Endpoint::Endpoint(Environment* env,
Local<Object> object,
const Endpoint::Options& options)
: AsyncWrap(env, object, AsyncWrap::PROVIDER_QUIC_ENDPOINT),
stats_(env->isolate()),
state_(env->isolate()),
options_(options),
udp_(this),
addrLRU_(options_.address_lru_size) {
MakeWeak();
IF_QUIC_DEBUG(env) {
Debug(this, "Endpoint created. Options %s", options.ToString());
}
const auto defineProperty = [&](auto name, auto value) {
object
->DefineOwnProperty(
env->context(), name, value, PropertyAttribute::ReadOnly)
.Check();
};
defineProperty(env->state_string(), state_.GetArrayBuffer());
defineProperty(env->stats_string(), stats_.GetArrayBuffer());
}
SocketAddress Endpoint::local_address() const {
DCHECK(!is_closed() && !is_closing());
return udp_.local_address();
}
void Endpoint::MarkAsBusy(bool on) {
Debug(this, "Marking endpoint as %s", on ? "busy" : "not busy");
state_->busy = on ? 1 : 0;
}
RegularToken Endpoint::GenerateNewToken(uint32_t version,
const SocketAddress& remote_address) {
IF_QUIC_DEBUG(env()) {
Debug(this,
"Generating new regular token for version %u and remote address %s",
version,
remote_address);
}
DCHECK(!is_closed() && !is_closing());
return RegularToken(version, remote_address, options_.token_secret);
}
StatelessResetToken Endpoint::GenerateNewStatelessResetToken(
uint8_t* token, const CID& cid) const {
IF_QUIC_DEBUG(env()) {
Debug(const_cast<Endpoint*>(this),
"Generating new stateless reset token for CID %s",
cid);
}
DCHECK(!is_closed() && !is_closing());
return StatelessResetToken(token, options_.reset_token_secret, cid);
}
void Endpoint::AddSession(const CID& cid, BaseObjectPtr<Session> session) {
if (is_closed() || is_closing()) return;
Debug(this, "Adding session for CID %s", cid);
sessions_[cid] = session;
IncrementSocketAddressCounter(session->remote_address());
if (session->is_server()) {
STAT_INCREMENT(Stats, server_sessions);
EmitNewSession(session);
} else {
STAT_INCREMENT(Stats, client_sessions);
}
}
void Endpoint::RemoveSession(const CID& cid) {
if (is_closed()) return;
Debug(this, "Removing session for CID %s", cid);
auto session = FindSession(cid);
if (!session) return;
DecrementSocketAddressCounter(session->remote_address());
sessions_.erase(cid);
if (state_->closing == 1) MaybeDestroy();
}
BaseObjectPtr<Session> Endpoint::FindSession(const CID& cid) {
BaseObjectPtr<Session> session;
auto session_it = sessions_.find(cid);
if (session_it == std::end(sessions_)) {
auto scid_it = dcid_to_scid_.find(cid);
if (scid_it != std::end(dcid_to_scid_)) {
session_it = sessions_.find(scid_it->second);
CHECK_NE(session_it, std::end(sessions_));
session = session_it->second;
}
} else {
session = session_it->second;
}
return session;
}
void Endpoint::AssociateCID(const CID& cid, const CID& scid) {
if (!is_closed() && !is_closing() && cid && scid && cid != scid &&
dcid_to_scid_[cid] != scid) {
Debug(this, "Associating CID %s with SCID %s", cid, scid);
dcid_to_scid_.emplace(cid, scid);
}
}
void Endpoint::DisassociateCID(const CID& cid) {
if (!is_closed() && cid) {
Debug(this, "Disassociating CID %s", cid);
dcid_to_scid_.erase(cid);
}
}
void Endpoint::AssociateStatelessResetToken(const StatelessResetToken& token,
Session* session) {
if (is_closed() || is_closing()) return;
Debug(this, "Associating stateless reset token %s with session", token);
token_map_[token] = session;
}
void Endpoint::DisassociateStatelessResetToken(
const StatelessResetToken& token) {
if (!is_closed()) {
Debug(this, "Disassociating stateless reset token %s", token);
token_map_.erase(token);
}
}
void Endpoint::Send(Packet* packet) {
CHECK_NOT_NULL(packet);
#ifdef DEBUG
// When diagnostic packet loss is enabled, the packet will be randomly
// dropped. This can happen to any type of packet. We use this only in
// testing to test various reliability issues.
if (UNLIKELY(is_diagnostic_packet_loss(options_.tx_loss))) {
packet->Done(0);
// Simulating tx packet loss
return;
}
#endif // DEBUG
if (is_closed() || is_closing() || packet->length() == 0) return;
Debug(this, "Sending %s", packet->ToString());
state_->pending_callbacks++;
int err = udp_.Send(packet);
if (err != 0) {
Debug(this, "Sending packet failed with error %d", err);
packet->Done(err);
Destroy(CloseContext::SEND_FAILURE, err);
}
STAT_INCREMENT_N(Stats, bytes_sent, packet->length());
STAT_INCREMENT(Stats, packets_sent);
}
void Endpoint::SendRetry(const PathDescriptor& options) {
// Generating and sending retry packets does consume some system resources,
// and it is possible for a malicious peer to trigger sending a large number
// of retry packets, resulting in a potential DOS vector. To help ward that
// off, we track how many retry packets we send to a particular host and
// enforce limits. Note that since we are using an LRU cache these limits
// aren't strict. If a retry is sent, we increment the retry_count statistic
// to give application code a means of detecting and responding to abuse on
// its own. What this count does not give is the rate of retry, so it is still
// somewhat limited.
Debug(this, "Sending retry on path %s", options);
auto info = addrLRU_.Upsert(options.remote_address);
if (++(info->retry_count) <= options_.max_retries) {
auto packet =
Packet::CreateRetryPacket(env(), this, options, options_.token_secret);
if (packet) {
STAT_INCREMENT(Stats, retry_count);
Send(std::move(packet));
}
// If creating the retry is unsuccessful, we just drop things on the floor.
// It's not worth committing any further resources to this one packet. We
// might want to log the failure at some point tho.
}
}
void Endpoint::SendVersionNegotiation(const PathDescriptor& options) {
Debug(this, "Sending version negotiation on path %s", options);
// While creating and sending a version negotiation packet does consume a
// small amount of system resources, and while it is fairly trivial for a
// malicious peer to force a version negotiation to be sent, these are more
// trivial to create than the cryptographically generated retry and stateless
// reset packets. If the packet is sent, then we'll at least increment the
// version_negotiation_count statistic so that application code can keep an
// eye on it.
auto packet = Packet::CreateVersionNegotiationPacket(env(), this, options);
if (packet) {
STAT_INCREMENT(Stats, version_negotiation_count);
Send(std::move(packet));
}
// If creating the packet is unsuccessful, we just drop things on the floor.
// It's not worth committing any further resources to this one packet. We
// might want to log the failure at some point tho.
}
bool Endpoint::SendStatelessReset(const PathDescriptor& options,
size_t source_len) {
if (UNLIKELY(options_.disable_stateless_reset)) return false;
Debug(this,
"Sending stateless reset on path %s with len %" PRIu64,
options,
source_len);
const auto exceeds_limits = [&] {
SocketAddressInfoTraits::Type* counts =
addrLRU_.Peek(options.remote_address);
auto count = counts != nullptr ? counts->reset_count : 0;
return count >= options_.max_stateless_resets;
};
// Per the QUIC spec, we need to protect against sending too many stateless
// reset tokens to an endpoint to prevent endless looping.
if (exceeds_limits()) return false;
auto packet = Packet::CreateStatelessResetPacket(
env(), this, options, options_.reset_token_secret, source_len);
if (packet) {
addrLRU_.Upsert(options.remote_address)->reset_count++;
STAT_INCREMENT(Stats, stateless_reset_count);
Send(std::move(packet));
return true;
}
return false;
}
void Endpoint::SendImmediateConnectionClose(const PathDescriptor& options,
QuicError reason) {
Debug(this,
"Sending immediate connection close on path %s with reason %s",
options,
reason);
// While it is possible for a malicious peer to cause us to create a large
// number of these, generating them is fairly trivial.
auto packet = Packet::CreateImmediateConnectionClosePacket(
env(), this, options, reason);
if (packet) {
STAT_INCREMENT(Stats, immediate_close_count);
Send(std::move(packet));
}
}
bool Endpoint::Start() {
if (is_closed() || is_closing()) return false;
// state_->receiving indicates that we're accepting inbound packets. It
// could be for server or client side, or both.
if (state_->receiving == 1) return true;
Debug(this, "Starting");
int err = 0;
if (state_->bound == 0) {
err = udp_.Bind(options_);
if (err != 0) {
// If we failed to bind, destroy the endpoint. There's nothing we can do.
Destroy(CloseContext::BIND_FAILURE, err);
return false;
}
state_->bound = 1;
}
err = udp_.Start();
if (err != 0) {
// If we failed to start listening, destroy the endpoint. There's nothing we
// can do.
Destroy(CloseContext::START_FAILURE, err);
return false;
}
BindingData::Get(env()).listening_endpoints[this] =
BaseObjectPtr<Endpoint>(this);
state_->receiving = 1;
return true;
}
void Endpoint::Listen(const Session::Options& options) {
if (is_closed() || is_closing() || state_->listening == 1) return;
server_options_ = options;
if (Start()) {
Debug(this, "Listening with options %s", server_options_.value());
state_->listening = 1;
}
}
BaseObjectPtr<Session> Endpoint::Connect(
const SocketAddress& remote_address,
const Session::Options& options,
std::optional<SessionTicket> session_ticket) {
// If starting fails, the endpoint will be destroyed.
if (!Start()) return BaseObjectPtr<Session>();
Session::Config config(
*this, options, local_address(), remote_address, session_ticket);
IF_QUIC_DEBUG(env()) {
Debug(
this,
"Connecting to %s with options %s and config %s [has 0rtt ticket? %s]",
remote_address,
options,
config,
session_ticket.has_value() ? "yes" : "no");
}
auto session = Session::Create(this, config);
if (!session) return BaseObjectPtr<Session>();
session->set_wrapped();
// Calling SendPendingData here triggers the session to send the initial
// handshake packets starting the connection.
session->application().SendPendingData();
return session;
}
void Endpoint::MaybeDestroy() {
if (!is_closed() && sessions_.empty() && state_->pending_callbacks == 0 &&
state_->listening == 0) {
// Destroy potentially creates v8 handles so let's make sure
// we have a HandleScope on the stack.
HandleScope scope(env()->isolate());
Destroy();
}
}
void Endpoint::Destroy(CloseContext context, int status) {
if (is_closed()) return;
IF_QUIC_DEBUG(env()) {
auto ctx = ([&] {
switch (context) {
case CloseContext::BIND_FAILURE:
return "bind failure";
case CloseContext::CLOSE:
return "close";
case CloseContext::LISTEN_FAILURE:
return "listen failure";
case CloseContext::RECEIVE_FAILURE:
return "receive failure";
case CloseContext::SEND_FAILURE:
return "send failure";
case CloseContext::START_FAILURE:
return "start failure";
}
return "<unknown>";
})();
Debug(
this, "Destroying endpoint due to \"%s\" with status %d", ctx, status);
}
STAT_RECORD_TIMESTAMP(Stats, destroyed_at);
state_->listening = 0;
close_context_ = context;
close_status_ = status;
// If there are open sessions still, shut them down. As those clean themselves
// up, they will remove themselves. The cleanup here will be synchronous and
// no attempt will be made to communicate further with the peer.
// Intentionally copy the sessions map so that we can safely iterate over it
// while those clean themselves up.
auto sessions = sessions_;
for (auto& session : sessions)
session.second->Close(Session::CloseMethod::SILENT);
sessions.clear();
DCHECK(sessions_.empty());
token_map_.clear();
dcid_to_scid_.clear();
udp_.Close();
state_->closing = 0;
state_->bound = 0;
state_->receiving = 0;
BindingData::Get(env()).listening_endpoints.erase(this);
EmitClose(close_context_, close_status_);
}
void Endpoint::CloseGracefully() {
if (is_closed() || is_closing()) return;
Debug(this, "Closing gracefully");
state_->listening = 0;
state_->closing = 1;
// Maybe we can go ahead and destroy now?
MaybeDestroy();
}
void Endpoint::Receive(const uv_buf_t& buf,
const SocketAddress& remote_address) {
const auto receive = [&](Session* session,
Store&& store,
const SocketAddress& local_address,
const SocketAddress& remote_address,
const CID& dcid,
const CID& scid) {
DCHECK_NOT_NULL(session);
size_t len = store.length();
Debug(this, "Passing received packet to session for processing");
if (session->Receive(std::move(store), local_address, remote_address)) {
STAT_INCREMENT_N(Stats, bytes_received, len);
STAT_INCREMENT(Stats, packets_received);
}
};
const auto accept = [&](const Session::Config& config, Store&& store) {
// One final check. If the endpoint is closed, closing, or is not listening
// as a server, then we cannot accept the initial packet.
if (is_closed() || is_closing() || !is_listening()) return;
Debug(this, "Trying to create new session for %s", config.dcid);
auto session = Session::Create(this, config);
if (session) {
receive(session.get(),
std::move(store),
config.local_address,
config.remote_address,
config.dcid,
config.scid);
}
};
const auto acceptInitialPacket = [&](const uint32_t version,
const CID& dcid,
const CID& scid,
Store&& store,
const SocketAddress& local_address,
const SocketAddress& remote_address) {
// Conditionally accept an initial packet to create a new session.
Debug(this,
"Trying to accept initial packet for %s from %s",
dcid,
remote_address);
// If we're not listening as a server, do not accept an initial packet.
if (state_->listening == 0) return;
ngtcp2_pkt_hd hd;
// This is our first condition check... A minimal check to see if ngtcp2 can
// even recognize this packet as a quic packet with the correct version.
ngtcp2_vec vec = store;
if (ngtcp2_accept(&hd, vec.base, vec.len) != NGTCP2_SUCCESS) {
// Per the ngtcp2 docs, ngtcp2_accept returns 0 if the check was
// successful, or an error code if it was not. Currently there's only one
// documented error code (NGTCP2_ERR_INVALID_ARGUMENT) but we'll handle
// any error here the same -- by ignoring the packet entirely.
Debug(this, "Failed to accept initial packet from %s", remote_address);
return;
}
// If ngtcp2_is_supported_version returns a non-zero value, the version is
// recognized and supported. If it returns 0, we'll go ahead and send a
// version negotiation packet in response.
if (ngtcp2_is_supported_version(hd.version) == 0) {
Debug(this,
"Packet was not accepted because the version (%d) is not supported",
hd.version);
SendVersionNegotiation(
PathDescriptor{version, dcid, scid, local_address, remote_address});
STAT_INCREMENT(Stats, packets_received);
return;
}
// This is the next important condition check... If the server has been
// marked busy or the remote peer has exceeded their maximum number of
// concurrent connections, any new connections will be shut down
// immediately.
const auto limits_exceeded = ([&] {
if (sessions_.size() >= options_.max_connections_total) return true;
SocketAddressInfoTraits::Type* counts = addrLRU_.Peek(remote_address);
auto count = counts != nullptr ? counts->active_connections : 0;
return count >= options_.max_connections_per_host;
})();
if (state_->busy || limits_exceeded) {
Debug(this,
"Packet was not accepted because the endpoint is busy or the "
"remote address %s has exceeded their maximum number of concurrent "
"connections",
remote_address);
// Endpoint is busy or the connection count is exceeded. The connection is
// refused. For the purpose of stats collection, we'll count both of these
// the same.
if (state_->busy) STAT_INCREMENT(Stats, server_busy_count);
SendImmediateConnectionClose(
PathDescriptor{version, scid, dcid, local_address, remote_address},
QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED));
// The packet was successfully processed, even if we did refuse the
// connection.
STAT_INCREMENT(Stats, packets_received);
return;
}
// At this point, we start to set up the configuration for our local
// session. We pass the received scid here as the dcid argument value
// because that is the value *this* session will use as the outbound dcid.
Session::Config config(Side::SERVER,
*this,
server_options_.value(),
version,
local_address,
remote_address,
scid,
dcid);
Debug(this, "Using session config for initial packet %s", config);
// The this point, the config.scid and config.dcid represent *our* views of
// the CIDs. Specifically, config.dcid identifies the peer and config.scid
// identifies us. config.dcid should equal scid. config.scid should *not*
// equal dcid.
DCHECK(config.dcid == scid);
DCHECK(config.scid != dcid);
const auto is_remote_address_validated = ([&] {
auto info = addrLRU_.Peek(remote_address);
return info != nullptr ? info->validated : false;
})();
// QUIC has address validation built in to the handshake but allows for
// an additional explicit validation request using RETRY frames. If we
// are using explicit validation, we check for the existence of a valid
// token in the packet. If one does not exist, we send a retry with
// a new token. If it does exist, and if it is valid, we grab the original
// cid and continue.
if (!is_remote_address_validated) {
Debug(this, "Remote address %s is not validated", remote_address);
switch (hd.type) {
case NGTCP2_PKT_INITIAL:
// First, let's see if we need to do anything here.
if (options_.validate_address) {
// If there is no token, generate and send one.
if (hd.tokenlen == 0) {
Debug(this,
"Initial packet has no token. Sending retry to %s to start "
"validation",
remote_address);
SendRetry(PathDescriptor{
version,
dcid,
scid,
local_address,
remote_address,
});
// We still consider this a successfully handled packet even
// if we send a retry.
STAT_INCREMENT(Stats, packets_received);
return;
}
// We have two kinds of tokens, each prefixed with a different magic
// byte.
switch (hd.token[0]) {
case RetryToken::kTokenMagic: {
RetryToken token(hd.token, hd.tokenlen);
Debug(this,
"Initial packet from %s has retry token %s",
remote_address,
token);
auto ocid = token.Validate(
version,
remote_address,
dcid,
options_.token_secret,
options_.retry_token_expiration * NGTCP2_SECONDS);
if (!ocid.has_value()) {
Debug(
this, "Retry token from %s is invalid.", remote_address);
// Invalid retry token was detected. Close the connection.
SendImmediateConnectionClose(
PathDescriptor{
version, scid, dcid, local_address, remote_address},
QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED));
// We still consider this a successfully handled packet even
// if we send a connection close.
STAT_INCREMENT(Stats, packets_received);
return;
}
// The ocid is the original dcid that was encoded into the
// original retry packet sent to the client. We use it for
// validation.
Debug(this,
"Retry token from %s is valid. Original dcid %s",
remote_address,
ocid.value());
config.ocid = ocid.value();
config.retry_scid = dcid;
config.set_token(token);
break;
}
case RegularToken::kTokenMagic: {
RegularToken token(hd.token, hd.tokenlen);
Debug(this,
"Initial packet from %s has regular token %s",
remote_address,
token);
if (!token.Validate(
version,
remote_address,
options_.token_secret,
options_.token_expiration * NGTCP2_SECONDS)) {
Debug(this,
"Regular token from %s is invalid.",
remote_address);
// If the regular token is invalid, let's send a retry to be
// lenient. There's a small risk that a malicious peer is
// trying to make us do some work but the risk is fairly low
// here.
SendRetry(PathDescriptor{
version,
dcid,
scid,
local_address,
remote_address,
});
// We still consider this to be a successfully handled packet
// if a retry is sent.
STAT_INCREMENT(Stats, packets_received);
return;
}
Debug(this, "Regular token from %s is valid.", remote_address);
config.set_token(token);
break;
}
default: {
Debug(this,
"Initial packet from %s has unknown token type",
remote_address);
// If our prefix bit does not match anything we know about,
// let's send a retry to be lenient. There's a small risk that a
// malicious peer is trying to make us do some work but the risk
// is fairly low here.
SendRetry(PathDescriptor{
version,
dcid,
scid,
local_address,
remote_address,
});
STAT_INCREMENT(Stats, packets_received);
return;
}
}
// Ok! If we've got this far, our token is valid! Which means our
// path to the remote address is valid (for now). Let's record that
// so we don't have to do this dance again for this endpoint
// instance.
Debug(this, "Remote address %s is validated", remote_address);
addrLRU_.Upsert(remote_address)->validated = true;
} else if (hd.tokenlen > 0) {
Debug(this,
"Ignoring initial packet from %s with unexpected token",
remote_address);
// If validation is turned off and there is a token, that's weird.
// The peer should only have a token if we sent it to them and we
// wouldn't have sent it unless validation was turned on. Let's
// assume the peer is buggy or malicious and drop the packet on the
// floor.
return;
}
break;
case NGTCP2_PKT_0RTT:
Debug(this,
"Sending retry to %s due to initial 0RTT packet",
remote_address);
// If it's a 0RTT packet, we're always going to perform path
// validation no matter what. This is a bit unfortunate since
// ORTT is supposed to be, you know, 0RTT, but sending a retry
// forces a round trip... but if the remote address is not
// validated, there's a possibility that this 0RTT is forged
// or otherwise suspicious. Before we can do anything with it,
// we have to validate it. Keep in mind that this means the
// client needs to respond with a proper initial packet in
// order to proceed.
// TODO(@jasnell): Validate this further to ensure this is
// the correct behavior.
SendRetry(PathDescriptor{
version,
dcid,
scid,
local_address,
remote_address,
});
STAT_INCREMENT(Stats, packets_received);
return;
}
}
accept(config, std::move(store));
};
// When a received packet contains a QUIC short header but cannot be matched
// to a known Session, it is either (a) garbage, (b) a valid packet for a
// connection we no longer have state for, or (c) a stateless reset. Because
// we do not yet know if we are going to process the packet, we need to try to
// quickly determine -- with as little cost as possible -- whether the packet
// contains a reset token. We do so by checking the final
// NGTCP2_STATELESS_RESET_TOKENLEN bytes in the packet to see if they match
// one of the known reset tokens previously given by the remote peer. If
// there's a match, then it's a reset token, if not, we move on the to the
// next check. It is very important that this check be as inexpensive as
// possible to avoid a DOS vector.
const auto maybeStatelessReset = [&](const CID& dcid,
const CID& scid,
Store& store,
const SocketAddress& local_address,
const SocketAddress& remote_address) {
// Support for stateless resets can be disabled by the application. If that
// case, or if the packet is too short to contain a reset token, then we
// skip the remaining checks.
if (options_.disable_stateless_reset ||
store.length() < NGTCP2_STATELESS_RESET_TOKENLEN) {
return false;
}
// The stateless reset token itself is the *final*
// NGTCP2_STATELESS_RESET_TOKENLEN bytes in the received packet. If it is a
// stateless reset then then rest of the bytes in the packet are garbage
// that we'll ignore.
ngtcp2_vec vec = store;
vec.base += (vec.len - NGTCP2_STATELESS_RESET_TOKENLEN);
// If a Session has been associated with the token, then it is a valid
// stateless reset token. We need to dispatch it to the session to be
// processed.
auto it = token_map_.find(StatelessResetToken(vec.base));
if (it != token_map_.end()) {
receive(it->second,
std::move(store),
local_address,
remote_address,
dcid,
scid);
return true;
}
// Otherwise, it's not a valid stateless reset token.
return false;
};
#ifdef DEBUG
// When diagnostic packet loss is enabled, the packet will be randomly
// dropped.
if (UNLIKELY(is_diagnostic_packet_loss(options_.rx_loss))) {
// Simulating rx packet loss
return;
}
#endif // DEBUG
// TODO(@jasnell): Implement blocklist support
// if (UNLIKELY(block_list_->Apply(remote_address))) {
// Debug(this, "Ignoring blocked remote address: %s", remote_address);
// return;
// }
Debug(this,
"Received packet with length %" PRIu64 " from %s",
buf.len,
remote_address);
// The managed buffer here contains the received packet. We do not yet know
// at this point if it is a valid QUIC packet. We need to do some basic
// checks. It is critical at this point that we do as little work as possible
// to avoid a DOS vector.
std::shared_ptr<BackingStore> backing = env()->release_managed_buffer(buf);
if (UNLIKELY(!backing)) {
// At this point something bad happened and we need to treat this as a fatal
// case. There's likely no way to test this specific condition reliably.
return Destroy(CloseContext::RECEIVE_FAILURE, UV_ENOMEM);
}
Store store(backing, buf.len, 0);
ngtcp2_vec vec = store;
ngtcp2_version_cid pversion_cid;
// This is our first check to see if the received data can be processed as a
// QUIC packet. If this fails, then the QUIC packet header is invalid and
// cannot be processed; all we can do is ignore it. If it succeeds, we have a
// valid QUIC header but there is still no guarantee that the packet can be
// successfully processed.
if (ngtcp2_pkt_decode_version_cid(
&pversion_cid, vec.base, vec.len, NGTCP2_MAX_CIDLEN) < 0) {
Debug(this, "Failed to decode packet header, ignoring");
return; // Ignore the packet!
}
// QUIC currently requires CID lengths of max NGTCP2_MAX_CIDLEN. Ignore any
// packet with a non-standard CID length.
if (UNLIKELY(pversion_cid.dcidlen > NGTCP2_MAX_CIDLEN ||
pversion_cid.scidlen > NGTCP2_MAX_CIDLEN)) {
Debug(this, "Packet had incorrectly sized CIDs, igoring");
return; // Ignore the packet!
}
// Each QUIC peer has two CIDs: The Source Connection ID (or scid), and the
// Destination Connection ID (or dcid). For each peer, the dcid is the CID
// identifying the other peer, and the scid is the CID identifying itself.
// That is, the client's scid is the server dcid; likewise the server's scid
// is the client's dcid.
//
// The dcid and scid below are the values sent from the peer received in the
// current packet, so in this case, dcid represents who the peer sent the
// packet too (this endpoint) and the scid represents who sent the packet.
CID dcid(pversion_cid.dcid, pversion_cid.dcidlen);
CID scid(pversion_cid.scid, pversion_cid.scidlen);
Debug(this, "Packet dcid %s, scid %s", dcid, scid);
// We index the current sessions by the dcid of the client. For initial
// packets, the dcid is some random value and the scid is omitted from the
// header (it uses what quic calls a "short header"). It is unlikely (but not
// impossible) that this randomly selected dcid will be in our index. If we do
// happen to have a collision, as unlikely as it is, ngtcp2 will do the right
// thing when it tries to process the packet so we really don't have to worry
// about it here. If the dcid is not known, the session here will be nullptr.
//
// When the session is established, this peer will create its own scid and
// will send that back to the remote peer to use as the new dcid on
// subsequent packets. When that session is added, we will index it by the
// local scid, so as long as the client sends the subsequent packets with the
// right dcid, everything should just work.
auto session = FindSession(dcid);
auto addr = local_address();
HandleScope handle_scope(env()->isolate());
// If a session is not found, there are four possible reasons:
// 1. The session has not been created yet
// 2. The session existed once but we've lost the local state for it
// 3. The packet is a stateless reset sent by the peer
// 4. This is a malicious or malformed packet.
if (!session) {
// No existing session.
Debug(this, "No existing session for dcid %s", dcid);
// Handle possible reception of a stateless reset token... If it is a
// stateless reset, the packet will be handled with no additional action
// necessary here. We want to return immediately without committing any
// further resources.
if (!scid && maybeStatelessReset(dcid, scid, store, addr, remote_address)) {
Debug(this, "Packet was a stateless reset");
return; // Stateless reset! Don't do any further processing.
}
// Process the packet as an initial packet...
return acceptInitialPacket(pversion_cid.version,
dcid,
scid,
std::move(store),
addr,
remote_address);
}
// If we got here, the dcid matched the scid of a known local session. Yay!
// The session will take over any further processing of the packet.
Debug(this, "Dispatching packet to known session");
receive(session.get(), std::move(store), addr, remote_address, dcid, scid);
}
void Endpoint::PacketDone(int status) {
if (is_closed()) return;
// At this point we should be waiting on at least one packet.
Debug(this, "Packet was sent with status %d", status);
DCHECK_GE(state_->pending_callbacks, 1);
state_->pending_callbacks--;
// Can we go ahead and close now?
if (state_->closing == 1) MaybeDestroy();
}
void Endpoint::IncrementSocketAddressCounter(const SocketAddress& addr) {
addrLRU_.Upsert(addr)->active_connections++;
}
void Endpoint::DecrementSocketAddressCounter(const SocketAddress& addr) {
auto* counts = addrLRU_.Peek(addr);
if (counts != nullptr && counts->active_connections > 0)
counts->active_connections--;
}
bool Endpoint::is_closed() const {
return !udp_;
}
bool Endpoint::is_closing() const {
return state_->closing;
}
bool Endpoint::is_listening() const {
return state_->listening;
}
void Endpoint::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("options", options_);
tracker->TrackField("udp", udp_);
if (server_options_.has_value()) {
tracker->TrackField("server_options", server_options_.value());
}
tracker->TrackField("token_map", token_map_);
tracker->TrackField("sessions", sessions_);
tracker->TrackField("cid_map", dcid_to_scid_);
tracker->TrackField("address LRU", addrLRU_);
}
// ======================================================================================
// Endpoint::SocketAddressInfoTraits
bool Endpoint::SocketAddressInfoTraits::CheckExpired(
const SocketAddress& address, const Type& type) {
return (uv_hrtime() - type.timestamp) > kSocketAddressInfoTimeout;
}
void Endpoint::SocketAddressInfoTraits::Touch(const SocketAddress& address,
Type* type) {
type->timestamp = uv_hrtime();
}
// ======================================================================================
// JavaScript call outs
void Endpoint::EmitNewSession(const BaseObjectPtr<Session>& session) {
if (!env()->can_call_into_js()) return;
CallbackScope<Endpoint> scope(this);
session->set_wrapped();
Local<Value> arg = session->object();
Debug(this, "Notifying JavaScript about new session");
MakeCallback(BindingData::Get(env()).session_new_callback(), 1, &arg);
}
void Endpoint::EmitClose(CloseContext context, int status) {
if (!env()->can_call_into_js()) return;
CallbackScope<Endpoint> scope(this);
auto isolate = env()->isolate();
Local<Value> argv[] = {Integer::New(isolate, static_cast<int>(context)),
Integer::New(isolate, static_cast<int>(status))};
Debug(this, "Notifying JavaScript about endpoint closing");
MakeCallback(
BindingData::Get(env()).endpoint_close_callback(), arraysize(argv), argv);
}
// ======================================================================================
// Endpoint JavaScript API
void Endpoint::New(const FunctionCallbackInfo<Value>& args) {
DCHECK(args.IsConstructCall());
auto env = Environment::GetCurrent(args);
Options options;
// Options::From will validate that args[0] is the correct type.
if (!Options::From(env, args[0]).To(&options)) {
// There was an error. Just exit to propagate.
return;
}
new Endpoint(env, args.This(), options);
}
void Endpoint::DoConnect(const FunctionCallbackInfo<Value>& args) {
auto env = Environment::GetCurrent(args);
Endpoint* endpoint;
ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder());
// args[0] is a SocketAddress
// args[1] is a Session OptionsObject (see session.cc)
// args[2] is an optional SessionTicket
DCHECK(SocketAddressBase::HasInstance(env, args[0]));
SocketAddressBase* address;
ASSIGN_OR_RETURN_UNWRAP(&address, args[0]);
DCHECK(args[1]->IsObject());
Session::Options options;
if (!Session::Options::From(env, args[1]).To(&options)) {
// There was an error. Return to propagate
return;
}
BaseObjectPtr<Session> session;
if (!args[2]->IsUndefined()) {
SessionTicket ticket;
if (SessionTicket::FromV8Value(env, args[2]).To(&ticket)) {
session = endpoint->Connect(*address->address(), options, ticket);
}
} else {
session = endpoint->Connect(*address->address(), options);
}
if (session) args.GetReturnValue().Set(session->object());
}
void Endpoint::DoListen(const FunctionCallbackInfo<Value>& args) {
Endpoint* endpoint;
ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder());
auto env = Environment::GetCurrent(args);
Session::Options options;
if (Session::Options::From(env, args[0]).To(&options)) {
endpoint->Listen(options);
}
}
void Endpoint::MarkBusy(const FunctionCallbackInfo<Value>& args) {
Endpoint* endpoint;
ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder());
endpoint->MarkAsBusy(args[0]->IsTrue());
}
void Endpoint::DoCloseGracefully(const FunctionCallbackInfo<Value>& args) {
Endpoint* endpoint;
ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder());
endpoint->CloseGracefully();
}
void Endpoint::LocalAddress(const FunctionCallbackInfo<Value>& args) {
auto env = Environment::GetCurrent(args);
Endpoint* endpoint;
ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder());
if (endpoint->is_closed() || !endpoint->udp_.is_bound()) return;
auto addr = SocketAddressBase::Create(
env, std::make_shared<SocketAddress>(endpoint->local_address()));
if (addr) args.GetReturnValue().Set(addr->object());
}
void Endpoint::Ref(const FunctionCallbackInfo<Value>& args) {
Endpoint* endpoint;
ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder());
auto env = Environment::GetCurrent(args);
if (args[0]->BooleanValue(env->isolate())) {
endpoint->udp_.Ref();
} else {
endpoint->udp_.Unref();
}
}
} // namespace quic
} // namespace node
#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC