diff --git a/node.gyp b/node.gyp index cad1061d878b59..45304452e74f33 100644 --- a/node.gyp +++ b/node.gyp @@ -335,6 +335,14 @@ 'src/node_crypto.cc', 'src/node_crypto.h', ], + 'node_quic_sources': [ + 'src/quic/cid.cc', + 'src/quic/data.cc', + 'src/quic/preferredaddress.cc', + 'src/quic/cid.h', + 'src/quic/data.h', + 'src/quic/preferredaddress.h', + ], 'node_mksnapshot_exec': '<(PRODUCT_DIR)/<(EXECUTABLE_PREFIX)node_mksnapshot<(EXECUTABLE_SUFFIX)', 'conditions': [ ['GENERATOR == "ninja"', { @@ -836,6 +844,7 @@ [ 'node_use_openssl=="true"', { 'sources': [ '<@(node_crypto_sources)', + '<@(node_quic_sources)', ], }], [ 'OS in "linux freebsd mac solaris" and ' @@ -1023,6 +1032,7 @@ 'sources': [ 'test/cctest/test_crypto_clienthello.cc', 'test/cctest/test_node_crypto.cc', + 'test/cctest/test_quic_cid.cc', ] }], ['v8_enable_inspector==1', { diff --git a/src/quic/cid.cc b/src/quic/cid.cc new file mode 100644 index 00000000000000..019896104fb63b --- /dev/null +++ b/src/quic/cid.cc @@ -0,0 +1,148 @@ +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC +#include "cid.h" +#include +#include +#include +#include + +namespace node { +namespace quic { + +// ============================================================================ +// CID + +CID::CID() : ptr_(&cid_) { + cid_.datalen = 0; +} + +CID::CID(const ngtcp2_cid& cid) : CID(cid.data, cid.datalen) {} + +CID::CID(const uint8_t* data, size_t len) : CID() { + DCHECK_GE(len, kMinLength); + DCHECK_LE(len, kMaxLength); + ngtcp2_cid_init(&cid_, data, len); +} + +CID::CID(const ngtcp2_cid* cid) : ptr_(cid) { + CHECK_NOT_NULL(cid); + DCHECK_GE(cid->datalen, kMinLength); + DCHECK_LE(cid->datalen, kMaxLength); +} + +CID::CID(const CID& other) : ptr_(&cid_) { + CHECK_NOT_NULL(other.ptr_); + ngtcp2_cid_init(&cid_, other.ptr_->data, other.ptr_->datalen); +} + +bool CID::operator==(const CID& other) const noexcept { + if (this == &other || (length() == 0 && other.length() == 0)) return true; + if (length() != other.length()) return false; + return memcmp(ptr_->data, other.ptr_->data, ptr_->datalen) == 0; +} + +bool CID::operator!=(const CID& other) const noexcept { + return !(*this == other); +} + +CID::operator const uint8_t*() const { + return ptr_->data; +} +CID::operator const ngtcp2_cid&() const { + return *ptr_; +} +CID::operator const ngtcp2_cid*() const { + return ptr_; +} +CID::operator bool() const { + return ptr_->datalen >= kMinLength; +} + +size_t CID::length() const { + return ptr_->datalen; +} + +std::string CID::ToString() const { + char dest[kMaxLength * 2]; + size_t written = + StringBytes::hex_encode(reinterpret_cast(ptr_->data), + ptr_->datalen, + dest, + arraysize(dest)); + return std::string(dest, written); +} + +CID CID::kInvalid{}; + +// ============================================================================ +// CID::Hash + +size_t CID::Hash::operator()(const CID& cid) const { + size_t hash = 0; + for (size_t n = 0; n < cid.length(); n++) { + hash ^= std::hash{}(cid.ptr_->data[n] + 0x9e3779b9 + (hash << 6) + + (hash >> 2)); + } + return hash; +} + +// ============================================================================ +// CID::Factory + +namespace { +class RandomCIDFactory : public CID::Factory { + public: + RandomCIDFactory() = default; + RandomCIDFactory(const RandomCIDFactory&) = delete; + RandomCIDFactory(RandomCIDFactory&&) = delete; + RandomCIDFactory& operator=(const RandomCIDFactory&) = delete; + RandomCIDFactory& operator=(RandomCIDFactory&&) = delete; + + CID Generate(size_t length_hint) const override { + DCHECK_GE(length_hint, CID::kMinLength); + DCHECK_LE(length_hint, CID::kMaxLength); + Mutex::ScopedLock lock(mutex_); + maybe_refresh_pool(length_hint); + auto start = pool_ + pos_; + pos_ += length_hint; + return CID(start, length_hint); + } + + void GenerateInto(ngtcp2_cid* cid, + size_t length_hint = CID::kMaxLength) const override { + DCHECK_GE(length_hint, CID::kMinLength); + DCHECK_LE(length_hint, CID::kMaxLength); + Mutex::ScopedLock lock(mutex_); + maybe_refresh_pool(length_hint); + auto start = pool_ + pos_; + pos_ += length_hint; + ngtcp2_cid_init(cid, start, length_hint); + } + + private: + void maybe_refresh_pool(size_t length_hint) const { + // We generate a pool of random data kPoolSize in length + // and pull our random CID from that. If we don't have + // enough random random remaining in the pool to generate + // a CID of the requested size, we regenerate the pool + // and reset it to zero. + if (pos_ + length_hint > kPoolSize) { + CHECK(crypto::CSPRNG(pool_, kPoolSize).is_ok()); + pos_ = 0; + } + } + + static constexpr int kPoolSize = 4096; + mutable int pos_ = kPoolSize; + mutable uint8_t pool_[kPoolSize]; + mutable Mutex mutex_; +}; +} // namespace + +const CID::Factory& CID::Factory::random() { + static RandomCIDFactory instance; + return instance; +} + +} // namespace quic +} // namespace node +#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC diff --git a/src/quic/cid.h b/src/quic/cid.h new file mode 100644 index 00000000000000..bfd6eb47c9ff9b --- /dev/null +++ b/src/quic/cid.h @@ -0,0 +1,126 @@ +#pragma once + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC +#include +#include +#include + +namespace node { +namespace quic { + +// CIDS are used to identify endpoints participating in a QUIC session. +// Once created, CID instances are immutable. +// +// CIDs contain between 1 to 20 bytes. Most typically they are selected +// randomly but there is a spec for creating "routable" CIDs that encode +// a specific structure that is meaningful only to the side that creates +// the CID. For most purposes, CIDs should be treated as opaque tokens. +// +// Each peer in a QUIC session generates one or more CIDs that the *other* +// peer will use to identify the session. When a QUIC client initiates a +// brand new session, it will initially generates a CID of its own (its +// source CID) and a random placeholder CID for the server (the original +// destination CID). When the server receives the initial packet, it will +// generate its own source CID and use the clients source CID as the +// server's destination CID. +// +// Client Server +// ------------------------------------------- +// Source CID <====> Destination CID +// Destination CID <====> Source CID +// +// While the connection is being established, it is possible for either +// peer to generate additional CIDs that are also associated with the +// connection. +class CID final : public MemoryRetainer { + public: + static constexpr size_t kMinLength = NGTCP2_MIN_CIDLEN; + static constexpr size_t kMaxLength = NGTCP2_MAX_CIDLEN; + + // Copy the given ngtcp2_cid. + explicit CID(const ngtcp2_cid& cid); + + // Copy the given buffer as a CID. The len must be within + // kMinLength and kMaxLength. + explicit CID(const uint8_t* data, size_t len); + + // Wrap the given ngtcp2_cid. The CID does not take ownership + // of the underlying ngtcp2_cid. + explicit CID(const ngtcp2_cid* cid); + + CID(const CID& other); + CID(CID&& other) = delete; + + struct Hash final { + size_t operator()(const CID& cid) const; + }; + + bool operator==(const CID& other) const noexcept; + bool operator!=(const CID& other) const noexcept; + + operator const uint8_t*() const; + operator const ngtcp2_cid&() const; + operator const ngtcp2_cid*() const; + + // True if the CID length is at least kMinLength; + operator bool() const; + size_t length() const; + + std::string ToString() const; + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(CID) + SET_SELF_SIZE(CID) + + template + using Map = std::unordered_map; + + // A CID::Factory, as the name suggests, is used to create new CIDs. + // Per https://datatracker.ietf.org/doc/draft-ietf-quic-load-balancers/, QUIC + // implementations MAY use the Connection ID associated with a QUIC session + // as a routing mechanism, with each CID instance securely encoding the + // routing information. By default, our implementation creates CIDs randomly + // but will allow user code to provide their own CID::Factory implementation. + class Factory; + + static CID kInvalid; + + private: + // The default constructor creates an empty, zero-length CID. + // Zero-length CIDs are not usable. We use them as a placeholder + // for a missing or empty CID value. + CID(); + + ngtcp2_cid cid_; + const ngtcp2_cid* ptr_; + + friend struct Hash; +}; + +class CID::Factory { + public: + virtual ~Factory() = default; + + // Generate a new CID. The length_hint must be between CID::kMinLength + // and CID::kMaxLength. The implementation can choose to ignore the length. + virtual CID Generate(size_t length_hint = CID::kMaxLength) const = 0; + + // Generate a new CID into the given ngtcp2_cid. This variation of + // Generate should be used far less commonly. It is provided largely + // for a couple of internal cases. + virtual void GenerateInto(ngtcp2_cid* cid, + size_t length_hint = CID::kMaxLength) const = 0; + + // The default random CID generator instance. + static const Factory& random(); + + // TODO(@jasnell): This will soon also include additional implementations + // of CID::Factory that implement the QUIC Load Balancers spec. +}; + +} // namespace quic +} // namespace node + +#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/data.cc b/src/quic/data.cc new file mode 100644 index 00000000000000..fcc2335db7adb9 --- /dev/null +++ b/src/quic/data.cc @@ -0,0 +1,259 @@ +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC + +#include "data.h" +#include +#include +#include +#include +#include +#include "util.h" + +namespace node { + +using v8::Array; +using v8::BigInt; +using v8::Integer; +using v8::Local; +using v8::MaybeLocal; +using v8::Undefined; +using v8::Value; + +namespace quic { + +Path::Path(const SocketAddress& local, const SocketAddress& remote) { + ngtcp2_addr_init(&this->local, local.data(), local.length()); + ngtcp2_addr_init(&this->remote, remote.data(), remote.length()); +} + +PathStorage::PathStorage() { + ngtcp2_path_storage_zero(this); +} +PathStorage::operator ngtcp2_path() { + return path; +} + +// ============================================================================ + +Store::Store(std::shared_ptr store, + size_t length, + size_t offset) + : store_(std::move(store)), length_(length), offset_(offset) { + CHECK_LE(offset_, store->ByteLength()); + CHECK_LE(length_, store->ByteLength() - offset_); +} + +Store::Store(std::unique_ptr store, + size_t length, + size_t offset) + : store_(std::move(store)), length_(length), offset_(offset) { + CHECK_LE(offset_, store->ByteLength()); + CHECK_LE(length_, store->ByteLength() - offset_); +} + +Store::Store(v8::Local buffer, Option option) + : Store(buffer->GetBackingStore(), buffer->ByteLength()) { + if (option == Option::DETACH) { + USE(buffer->Detach(Local())); + } +} + +Store::Store(v8::Local view, Option option) + : Store(view->Buffer()->GetBackingStore(), + view->ByteLength(), + view->ByteOffset()) { + if (option == Option::DETACH) { + USE(view->Buffer()->Detach(Local())); + } +} + +Store::operator bool() const { + return store_ != nullptr; +} +size_t Store::length() const { + return length_; +} + +template +T Store::convert() const { + T buf; + buf.base = + store_ != nullptr ? static_cast(store_->Data()) + offset_ : nullptr; + buf.len = length_; + return buf; +} + +Store::operator uv_buf_t() const { + return convert(); +} + +Store::operator ngtcp2_vec() const { + return convert(); +} + +Store::operator nghttp3_vec() const { + return convert(); +} + +void Store::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("store", store_); +} + +// ============================================================================ + +namespace { +std::string TypeName(QuicError::Type type) { + switch (type) { + case QuicError::Type::APPLICATION: + return "APPLICATION"; + case QuicError::Type::TRANSPORT: + return "TRANSPORT"; + case QuicError::Type::VERSION_NEGOTIATION: + return "VERSION_NEGOTIATION"; + case QuicError::Type::IDLE_CLOSE: + return "IDLE_CLOSE"; + } + UNREACHABLE(); +} +} // namespace + +QuicError::QuicError(const std::string_view reason) + : reason_(reason), ptr_(&error_) {} + +QuicError::QuicError(const ngtcp2_connection_close_error* ptr) + : reason_(reinterpret_cast(ptr->reason), ptr->reasonlen), + ptr_(ptr) {} + +QuicError::QuicError(const ngtcp2_connection_close_error& error) + : reason_(reinterpret_cast(error.reason), error.reasonlen), + error_(error), + ptr_(&error_) {} + +QuicError::operator bool() const { + if ((code() == QUIC_NO_ERROR && type() == Type::TRANSPORT) || + ((code() == QUIC_APP_NO_ERROR && type() == Type::APPLICATION))) { + return false; + } + return true; +} + +const uint8_t* QuicError::reason_c_str() const { + return reinterpret_cast(reason_.c_str()); +} + +bool QuicError::operator!=(const QuicError& other) const { + return !(*this == other); +} + +bool QuicError::operator==(const QuicError& other) const { + if (this == &other) return true; + return type() == other.type() && code() == other.code() && + frame_type() == other.frame_type(); +} + +QuicError::Type QuicError::type() const { + return static_cast(ptr_->type); +} + +QuicError::error_code QuicError::code() const { + return ptr_->error_code; +} + +uint64_t QuicError::frame_type() const { + return ptr_->frame_type; +} + +const std::string_view QuicError::reason() const { + return reason_; +} + +QuicError::operator const ngtcp2_connection_close_error&() const { + return *ptr_; +} + +QuicError::operator const ngtcp2_connection_close_error*() const { + return ptr_; +} + +MaybeLocal QuicError::ToV8Value(Environment* env) const { + Local argv[] = { + Integer::New(env->isolate(), static_cast(type())), + BigInt::NewFromUnsigned(env->isolate(), code()), + Undefined(env->isolate()), + }; + + if (reason_.length() > 0 && + !node::ToV8Value(env->context(), reason()).ToLocal(&argv[2])) { + return MaybeLocal(); + } + return Array::New(env->isolate(), argv, arraysize(argv)).As(); +} + +std::string QuicError::ToString() const { + std::string str = "QuicError("; + str += TypeName(type()) + ") "; + str += std::to_string(code()); + if (!reason_.empty()) str += ": " + reason_; + return str; +} + +void QuicError::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("reason", reason_.length()); +} + +QuicError QuicError::ForTransport(error_code code, + const std::string_view reason) { + QuicError error(reason); + ngtcp2_connection_close_error_set_transport_error( + &error.error_, code, error.reason_c_str(), reason.length()); + return error; +} + +QuicError QuicError::ForApplication(error_code code, + const std::string_view reason) { + QuicError error(reason); + ngtcp2_connection_close_error_set_application_error( + &error.error_, code, error.reason_c_str(), reason.length()); + return error; +} + +QuicError QuicError::ForVersionNegotiation(const std::string_view reason) { + return ForNgtcp2Error(NGTCP2_ERR_RECV_VERSION_NEGOTIATION, reason); +} + +QuicError QuicError::ForIdleClose(const std::string_view reason) { + return ForNgtcp2Error(NGTCP2_ERR_IDLE_CLOSE, reason); +} + +QuicError QuicError::ForNgtcp2Error(int code, const std::string_view reason) { + QuicError error(reason); + ngtcp2_connection_close_error_set_transport_error_liberr( + &error.error_, code, error.reason_c_str(), reason.length()); + return error; +} + +QuicError QuicError::ForTlsAlert(int code, const std::string_view reason) { + QuicError error(reason); + ngtcp2_connection_close_error_set_transport_error_tls_alert( + &error.error_, code, error.reason_c_str(), reason.length()); + return error; +} + +QuicError QuicError::FromConnectionClose(ngtcp2_conn* session) { + QuicError error; + ngtcp2_conn_get_connection_close_error(session, &error.error_); + return error; +} + +QuicError QuicError::TRANSPORT_NO_ERROR = + QuicError::ForTransport(QuicError::QUIC_NO_ERROR); +QuicError QuicError::APPLICATION_NO_ERROR = + QuicError::ForApplication(QuicError::QUIC_APP_NO_ERROR); +QuicError QuicError::VERSION_NEGOTIATION = QuicError::ForVersionNegotiation(); +QuicError QuicError::IDLE_CLOSE = QuicError::ForIdleClose(); +QuicError QuicError::INTERNAL_ERROR = + QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL); + +} // namespace quic +} // namespace node + +#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC diff --git a/src/quic/data.h b/src/quic/data.h new file mode 100644 index 00000000000000..14a613df69196b --- /dev/null +++ b/src/quic/data.h @@ -0,0 +1,137 @@ +#pragma once + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC + +#include +#include +#include +#include +#include +#include + +namespace node { +namespace quic { + +struct Path final : public ngtcp2_path { + Path(const SocketAddress& local, const SocketAddress& remote); +}; + +struct PathStorage final : public ngtcp2_path_storage { + PathStorage(); + operator ngtcp2_path(); +}; + +class Store final : public MemoryRetainer { + public: + Store() = default; + + Store(std::shared_ptr store, + size_t length, + size_t offset = 0); + Store(std::unique_ptr store, + size_t length, + size_t offset = 0); + + enum class Option { + NONE, + DETACH, + }; + + Store(v8::Local buffer, Option option = Option::NONE); + Store(v8::Local view, Option option = Option::NONE); + + operator uv_buf_t() const; + operator ngtcp2_vec() const; + operator nghttp3_vec() const; + operator bool() const; + size_t length() const; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Store) + SET_SELF_SIZE(Store) + + private: + template + T convert() const; + std::shared_ptr store_; + size_t length_ = 0; + size_t offset_ = 0; +}; + +class QuicError final : public MemoryRetainer { + public: + using error_code = uint64_t; + + static constexpr error_code QUIC_NO_ERROR = NGTCP2_NO_ERROR; + static constexpr error_code QUIC_APP_NO_ERROR = 65280; + + enum class Type { + TRANSPORT = NGTCP2_CONNECTION_CLOSE_ERROR_CODE_TYPE_TRANSPORT, + APPLICATION = NGTCP2_CONNECTION_CLOSE_ERROR_CODE_TYPE_APPLICATION, + VERSION_NEGOTIATION = + NGTCP2_CONNECTION_CLOSE_ERROR_CODE_TYPE_TRANSPORT_VERSION_NEGOTIATION, + IDLE_CLOSE = NGTCP2_CONNECTION_CLOSE_ERROR_CODE_TYPE_TRANSPORT_IDLE_CLOSE, + }; + + static constexpr error_code QUIC_ERROR_TYPE_TRANSPORT = + NGTCP2_CONNECTION_CLOSE_ERROR_CODE_TYPE_TRANSPORT; + static constexpr error_code QUIC_ERROR_TYPE_APPLICATION = + NGTCP2_CONNECTION_CLOSE_ERROR_CODE_TYPE_APPLICATION; + + explicit QuicError(const std::string_view reason = ""); + explicit QuicError(const ngtcp2_connection_close_error* ptr); + explicit QuicError(const ngtcp2_connection_close_error& error); + + Type type() const; + error_code code() const; + const std::string_view reason() const; + uint64_t frame_type() const; + + operator const ngtcp2_connection_close_error&() const; + operator const ngtcp2_connection_close_error*() const; + + // Returns false if the QuicError uses a no_error code with type + // transport or application. + operator bool() const; + + bool operator==(const QuicError& other) const; + bool operator!=(const QuicError& other) const; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(QuicError) + SET_SELF_SIZE(QuicError) + + std::string ToString() const; + v8::MaybeLocal ToV8Value(Environment* env) const; + + static QuicError ForTransport(error_code code, + const std::string_view reason = ""); + static QuicError ForApplication(error_code code, + const std::string_view reason = ""); + static QuicError ForVersionNegotiation(const std::string_view reason = ""); + static QuicError ForIdleClose(const std::string_view reason = ""); + static QuicError ForNgtcp2Error(int code, const std::string_view reason = ""); + static QuicError ForTlsAlert(int code, const std::string_view reason = ""); + + static QuicError FromConnectionClose(ngtcp2_conn* session); + + static QuicError TRANSPORT_NO_ERROR; + static QuicError APPLICATION_NO_ERROR; + static QuicError VERSION_NEGOTIATION; + static QuicError IDLE_CLOSE; + static QuicError INTERNAL_ERROR; + + private: + const uint8_t* reason_c_str() const; + + std::string reason_; + ngtcp2_connection_close_error error_; + const ngtcp2_connection_close_error* ptr_ = nullptr; +}; + +} // namespace quic +} // namespace node + +#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/preferredaddress.cc b/src/quic/preferredaddress.cc new file mode 100644 index 00000000000000..180241cf272aa8 --- /dev/null +++ b/src/quic/preferredaddress.cc @@ -0,0 +1,159 @@ +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC + +#include "preferredaddress.h" +#include +#include +#include +#include +#include +#include +#include + +namespace node { + +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::Nothing; +using v8::Value; + +namespace quic { + +namespace { +template +std::optional get_address_info( + const ngtcp2_preferred_addr& paddr) { + if constexpr (FAMILY == AF_INET) { + if (!paddr.ipv4_present) return std::nullopt; + PreferredAddress::AddressInfo address; + address.family = FAMILY; + address.port = paddr.ipv4_port; + if (uv_inet_ntop( + FAMILY, paddr.ipv4_addr, address.host, sizeof(address.host)) == 0) { + address.address = address.host; + } + return address; + } else { + if (!paddr.ipv6_present) return std::nullopt; + PreferredAddress::AddressInfo address; + address.family = FAMILY; + address.port = paddr.ipv6_port; + if (uv_inet_ntop( + FAMILY, paddr.ipv6_addr, address.host, sizeof(address.host)) == 0) { + address.address = address.host; + } + return address; + } +} + +template +void copy_to_transport_params(ngtcp2_transport_params* params, + const sockaddr* addr) { + params->preferred_address_present = true; + if constexpr (FAMILY == AF_INET) { + const sockaddr_in* src = reinterpret_cast(addr); + params->preferred_address.ipv4_port = SocketAddress::GetPort(addr); + memcpy(params->preferred_address.ipv4_addr, + &src->sin_addr, + sizeof(params->preferred_address.ipv4_addr)); + } else { + DCHECK_EQ(FAMILY, AF_INET6); + const sockaddr_in6* src = reinterpret_cast(addr); + params->preferred_address.ipv6_port = SocketAddress::GetPort(addr); + memcpy(params->preferred_address.ipv6_addr, + &src->sin6_addr, + sizeof(params->preferred_address.ipv4_addr)); + } + UNREACHABLE(); +} + +bool resolve(const PreferredAddress::AddressInfo& address, + uv_getaddrinfo_t* req) { + addrinfo hints{}; + hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV; + hints.ai_family = address.family; + hints.ai_socktype = SOCK_DGRAM; + + // ngtcp2 requires the selection of the preferred address + // to be synchronous, which means we have to do a sync resolve + // using uv_getaddrinfo here. + return uv_getaddrinfo(nullptr, + req, + nullptr, + address.host, + // TODO(@jasnell): The to_string here is not really + // the most performant way of converting the uint16_t + // port into a string. Depending on execution count, + // the potential cost here could be mitigated with a + // more efficient conversion. For now, however, this + // works. + std::to_string(address.port).c_str(), + &hints) == 0 && + req->addrinfo != nullptr; +} +} // namespace + +Maybe PreferredAddress::GetPolicy( + Environment* env, Local value) { + CHECK(value->IsUint32()); + uint32_t val = 0; + if (value->Uint32Value(env->context()).To(&val)) { + switch (val) { + case QUIC_PREFERRED_ADDRESS_USE: + return Just(Policy::USE_PREFERRED_ADDRESS); + case QUIC_PREFERRED_ADDRESS_IGNORE: + return Just(Policy::IGNORE_PREFERRED_ADDRESS); + } + } + THROW_ERR_INVALID_ARG_VALUE( + env, "%d is not a valid preferred address policy", val); + return Nothing(); +} + +PreferredAddress::PreferredAddress(ngtcp2_path* dest, + const ngtcp2_preferred_addr* paddr) + : dest_(dest), paddr_(paddr) { + DCHECK_NOT_NULL(paddr); + DCHECK_NOT_NULL(dest); +} + +std::optional PreferredAddress::ipv4() + const { + return get_address_info(*paddr_); +} + +std::optional PreferredAddress::ipv6() + const { + return get_address_info(*paddr_); +} + +void PreferredAddress::Use(const AddressInfo& address) { + uv_getaddrinfo_t req; + auto on_exit = OnScopeLeave([&] { + if (req.addrinfo != nullptr) uv_freeaddrinfo(req.addrinfo); + }); + + if (resolve(address, &req)) { + DCHECK_NOT_NULL(req.addrinfo); + dest_->remote.addrlen = req.addrinfo->ai_addrlen; + memcpy(dest_->remote.addr, req.addrinfo->ai_addr, req.addrinfo->ai_addrlen); + } +} + +void PreferredAddress::Set(ngtcp2_transport_params* params, + const sockaddr* addr) { + DCHECK_NOT_NULL(params); + DCHECK_NOT_NULL(addr); + switch (addr->sa_family) { + case AF_INET: + return copy_to_transport_params(params, addr); + case AF_INET6: + return copy_to_transport_params(params, addr); + } + // Any other value is just ignored. +} + +} // namespace quic +} // namespace node + +#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC diff --git a/src/quic/preferredaddress.h b/src/quic/preferredaddress.h new file mode 100644 index 00000000000000..6be468fac2cd08 --- /dev/null +++ b/src/quic/preferredaddress.h @@ -0,0 +1,72 @@ +#pragma once + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC + +#include +#include +#include +#include +#include + +namespace node { +namespace quic { + +// PreferredAddress is a helper class used only when a client Session receives +// an advertised preferred address from a server. The helper provides +// information about the server advertised preferred address and allows +// the preferred address to be selected. +class PreferredAddress final { + public: + enum class Policy { + // Ignore the server-advertised preferred address. + IGNORE_PREFERRED_ADDRESS, + // Use the server-advertised preferred address. + USE_PREFERRED_ADDRESS, + }; + + // The QUIC_* constants are expected to be exported out to be used on + // the JavaScript side of the API. + static constexpr uint32_t QUIC_PREFERRED_ADDRESS_USE = + static_cast(Policy::USE_PREFERRED_ADDRESS); + static constexpr uint32_t QUIC_PREFERRED_ADDRESS_IGNORE = + static_cast(Policy::IGNORE_PREFERRED_ADDRESS); + + static v8::Maybe GetPolicy(Environment* env, + v8::Local value); + + struct AddressInfo final { + char host[NI_MAXHOST]; + int family; + uint16_t port; + std::string_view address; + }; + + explicit PreferredAddress(ngtcp2_path* dest, + const ngtcp2_preferred_addr* paddr); + PreferredAddress(const PreferredAddress&) = delete; + PreferredAddress(PreferredAddress&&) = delete; + PreferredAddress& operator=(const PreferredAddress&) = delete; + PreferredAddress& operator=(PreferredAddress&&) = delete; + + void Use(const AddressInfo& address); + + std::optional ipv4() const; + std::optional ipv6() const; + + // Set the preferred address in the transport params. + // The address family (ipv4 or ipv6) will be automatically + // detected from the given addr. Any other address family + // will be ignored. + static void Set(ngtcp2_transport_params* params, const sockaddr* addr); + + private: + ngtcp2_path* dest_; + const ngtcp2_preferred_addr* paddr_; +}; + +} // namespace quic +} // namespace node + +#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/test/cctest/test_quic_cid.cc b/test/cctest/test_quic_cid.cc new file mode 100644 index 00000000000000..44e4e5d7b998e7 --- /dev/null +++ b/test/cctest/test_quic_cid.cc @@ -0,0 +1,95 @@ +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC +#include +#include +#include +#include +#include +#include + +using node::quic::CID; + +TEST(CID, Basic) { + auto& random = CID::Factory::random(); + { + auto cid = random.Generate(); + CHECK_EQ(cid.length(), CID::kMaxLength); + CHECK(cid); + CHECK_EQ(cid, cid); + } + { + auto cid = random.Generate(5); + CHECK_EQ(cid.length(), 5); + CHECK(cid); + } + { + auto cid1 = random.Generate(); + auto cid2 = random.Generate(); + CHECK_NE(cid1, cid2); + } + { + auto cid1 = random.Generate(5); + auto cid2 = random.Generate(); + CHECK_NE(cid1, cid2); + } + { + auto cid1 = random.Generate(); + auto cid2 = random.Generate(5); + CHECK_NE(cid1, cid2); + } + { + auto cid = CID::kInvalid; + // They are copy constructible... + auto cid2 = cid; + CHECK(!cid); + CHECK_EQ(cid.length(), 0); + CHECK_EQ(cid, cid2); + } + { + auto cid1 = random.Generate(); + auto cid2 = random.Generate(); + CID::Map map; + map[cid1] = "hello"; + map[cid2] = "there"; + CHECK_EQ(map[cid1], "hello"); + CHECK_EQ(map[cid2], "there"); + CHECK_NE(map[cid2], "hello"); + CHECK_NE(map[cid1], "there"); + } + { + ngtcp2_cid cid_; + uint8_t data[] = {1, 2, 3, 4, 5}; + ngtcp2_cid_init(&cid_, data, 5); + auto cid = CID(cid_); + // This variation of the constructor copies the cid_, so if we + // modify the original data it doesn't change in the CID. + cid_.data[0] = 9; + CHECK_EQ(cid.length(), 5); + CHECK_EQ(cid.ToString(), "0102030405"); + } + { + ngtcp2_cid cid_; + uint8_t data[] = {1, 2, 3, 4, 5}; + ngtcp2_cid_init(&cid_, data, 5); + auto cid = CID(&cid_); + // This variation of the constructor wraps the cid_, so if we + // modify the original data it does change in the CID. + cid_.data[0] = 9; + CHECK_EQ(cid.length(), 5); + CHECK_EQ(cid.ToString(), "0902030405"); + } + { + // Generate a bunch to ensure that the pool is regenerated. + for (int n = 0; n < 1000; n++) { + random.Generate(); + } + } + { + ngtcp2_cid cid_; + // Generate a bunch to ensure that the pool is regenerated. + for (int n = 0; n < 1000; n++) { + random.GenerateInto(&cid_, 10); + CHECK_EQ(cid_.datalen, 10); + } + } +} +#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC