diff --git a/.github/workflows/toolchain.yml b/.github/workflows/toolchain.yml index 8e53314..bdf9eec 100644 --- a/.github/workflows/toolchain.yml +++ b/.github/workflows/toolchain.yml @@ -45,9 +45,10 @@ jobs: -DLAGHU_BUILD_PROFILE=FULL cmake --build "$RUNNER_TEMP/laghu-full" --target \ laghu_crypto_provider_test laghu_idna_test laghu_password_auth_test \ - laghu_password_auth_failure_token_test laghu_http2_test laghu_http2_contract_test + laghu_password_auth_failure_token_test laghu_http2_test laghu_http2_contract_test \ + laghu_http3_test laghu_http3_contract_test ctest --test-dir "$RUNNER_TEMP/laghu-full" --output-on-failure \ - -R '^laghu\.(crypto\.provider|adapters\.(idna|password_auth|password_auth\.failure_token|http2|http2\.contract))$' + -R '^laghu\.(crypto\.provider|adapters\.(idna|password_auth|password_auth\.failure_token|http2|http2\.contract|http3|http3\.contract))$' - name: Verify system dynamic IDNA and password adapters if: matrix.name == 'gcc-14-libstdc++' run: | @@ -81,12 +82,12 @@ jobs: -DCMAKE_CXX_COMPILER="${{ matrix.compiler }}" \ -DCMAKE_CXX_FLAGS="${{ matrix.cxxflags }}" \ -DLAGHU_BUILD_PROFILE=CUSTOM \ - '-DLAGHU_FEATURES=idna;password_auth;http2' + '-DLAGHU_FEATURES=idna;password_auth;http2;http3' cmake --build "$RUNNER_TEMP/laghu-clang-adapters" --target \ laghu_idna_test laghu_password_auth_test laghu_password_auth_failure_token_test \ - laghu_http2_test laghu_http2_contract_test + laghu_http2_test laghu_http2_contract_test laghu_http3_test laghu_http3_contract_test ctest --test-dir "$RUNNER_TEMP/laghu-clang-adapters" --output-on-failure \ - -R '^laghu\.adapters\.(idna|password_auth|password_auth\.failure_token|http2|http2\.contract)$' + -R '^laghu\.adapters\.(idna|password_auth|password_auth\.failure_token|http2|http2\.contract|http3|http3\.contract)$' - name: Verify LibreSSL crypto adapter if: matrix.name == 'gcc-14-libstdc++' run: | @@ -141,12 +142,12 @@ jobs: cmake -S . -B "$RUNNER_TEMP/laghu-adapters" -G Ninja \ -DCMAKE_CXX_COMPILER=clang++ \ -DLAGHU_BUILD_PROFILE=CUSTOM \ - '-DLAGHU_FEATURES=idna;password_auth;http2' + '-DLAGHU_FEATURES=idna;password_auth;http2;http3' cmake --build "$RUNNER_TEMP/laghu-adapters" --target \ laghu_idna_test laghu_password_auth_test laghu_password_auth_failure_token_test \ - laghu_http2_test laghu_http2_contract_test + laghu_http2_test laghu_http2_contract_test laghu_http3_test laghu_http3_contract_test ctest --test-dir "$RUNNER_TEMP/laghu-adapters" --output-on-failure \ - -R '^laghu\.adapters\.(idna|password_auth|password_auth\.failure_token|http2|http2\.contract)$' + -R '^laghu\.adapters\.(idna|password_auth|password_auth\.failure_token|http2|http2\.contract|http3|http3\.contract)$' - uses: actions/upload-artifact@v7 with: name: toolchain-capabilities-macos-appleclang @@ -176,12 +177,12 @@ jobs: cmake -S . -B /tmp/laghu-adapters -G Ninja \ -DCMAKE_CXX_COMPILER=clang++ \ -DLAGHU_BUILD_PROFILE=CUSTOM \ - '-DLAGHU_FEATURES=idna;password_auth;http2' + '-DLAGHU_FEATURES=idna;password_auth;http2;http3' cmake --build /tmp/laghu-adapters --target \ laghu_idna_test laghu_password_auth_test laghu_password_auth_failure_token_test \ - laghu_http2_test laghu_http2_contract_test + laghu_http2_test laghu_http2_contract_test laghu_http3_test laghu_http3_contract_test ctest --test-dir /tmp/laghu-adapters --output-on-failure \ - -R '^laghu\.adapters\.(idna|password_auth|password_auth\.failure_token|http2|http2\.contract)$' + -R '^laghu\.adapters\.(idna|password_auth|password_auth\.failure_token|http2|http2\.contract|http3|http3\.contract)$' linux-aarch64-cross: runs-on: ubuntu-24.04 steps: @@ -197,7 +198,7 @@ jobs: run: | cmake --preset linux-aarch64-gcc \ -DLAGHU_BUILD_PROFILE=CUSTOM \ - '-DLAGHU_FEATURES=tls;http2' + '-DLAGHU_FEATURES=tls;http2;http3' cmake --build build/linux-aarch64-gcc --target laghu_verify_toolchain - name: Verify ARM64 archive run: | @@ -207,6 +208,10 @@ jobs: aarch64-linux-gnu-ar t build/linux-aarch64-gcc/liblaghu_crypto.a test -f build/linux-aarch64-gcc/liblaghu_http2.a aarch64-linux-gnu-ar t build/linux-aarch64-gcc/liblaghu_http2.a + test -f build/linux-aarch64-gcc/liblaghu_quic.a + aarch64-linux-gnu-ar t build/linux-aarch64-gcc/liblaghu_quic.a + test -f build/linux-aarch64-gcc/liblaghu_http3.a + aarch64-linux-gnu-ar t build/linux-aarch64-gcc/liblaghu_http3.a linux-clang-asan-ubsan: runs-on: ubuntu-24.04 steps: @@ -242,12 +247,12 @@ jobs: -DCMAKE_CXX_FLAGS=-stdlib=libc++ \ -DLAGHU_SANITIZER_PROFILE=ASAN_UBSAN \ -DLAGHU_BUILD_PROFILE=CUSTOM \ - '-DLAGHU_FEATURES=idna;password_auth;http2' + '-DLAGHU_FEATURES=idna;password_auth;http2;http3' cmake --build "$RUNNER_TEMP/laghu-asan-adapters" --target \ laghu_idna_test laghu_password_auth_test laghu_password_auth_failure_token_test \ - laghu_http2_test laghu_http2_contract_test + laghu_http2_test laghu_http2_contract_test laghu_http3_test laghu_http3_contract_test ctest --test-dir "$RUNNER_TEMP/laghu-asan-adapters" --output-on-failure \ - -R '^laghu\.adapters\.(idna|password_auth|password_auth\.failure_token|http2|http2\.contract)$' + -R '^laghu\.adapters\.(idna|password_auth|password_auth\.failure_token|http2|http2\.contract|http3|http3\.contract)$' - name: Replay binary-envelope fuzz corpus under ASan and UBSan run: | cmake -S . -B "$RUNNER_TEMP/laghu-fuzz" -G Ninja \ diff --git a/CMakeLists.txt b/CMakeLists.txt index b3b5f67..2047f32 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -244,6 +244,33 @@ if(NOT laghu_http2_feature_index EQUAL -1) endif() endif() +list(FIND LAGHU_EFFECTIVE_FEATURES http3 laghu_http3_feature_index) +if(NOT laghu_http3_feature_index EQUAL -1) + add_library(laghu_quic STATIC + src/adapters/quic.cpp + $) + laghu_apply_first_party_contract(laghu_quic) + laghu_configure_api_consumer(laghu_quic adapters) + target_include_directories(laghu_quic PRIVATE "${CMAKE_SOURCE_DIR}/src/adapters/private") + laghu_register_subsystem_target(laghu_quic adapters) + laghu_link_subsystems(laghu_quic laghu_core) + target_link_libraries(laghu_quic PRIVATE laghu_feature_http3) + + add_library(laghu_http3 STATIC + src/adapters/http3.cpp + $) + laghu_apply_first_party_contract(laghu_http3) + laghu_configure_api_consumer(laghu_http3 adapters) + target_include_directories(laghu_http3 PRIVATE "${CMAKE_SOURCE_DIR}/src/adapters/private") + laghu_register_subsystem_target(laghu_http3 adapters) + laghu_link_subsystems(laghu_http3 laghu_core) + target_link_libraries(laghu_http3 PRIVATE laghu_feature_http3) + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set_source_files_properties(src/adapters/quic.cpp src/adapters/http3.cpp PROPERTIES + COMPILE_OPTIONS "-Wno-old-style-cast;-Wno-unsafe-buffer-usage;-Wno-zero-as-null-pointer-constant") + endif() +endif() + add_executable(laghu_core_smoke src/core/smoke.cpp) laghu_apply_first_party_contract(laghu_core_smoke) laghu_configure_api_consumer(laghu_core_smoke core) @@ -383,6 +410,23 @@ if(TARGET laghu_http2) laghu_configure_api_consumer(laghu_http2_contract_test adapters) laghu_add_native_test(laghu.adapters.http2.contract laghu_http2_contract_test) endif() +if(TARGET laghu_quic) + add_executable(laghu_http3_test tests/adapters/http3.cpp) + laghu_apply_first_party_contract(laghu_http3_test) + laghu_configure_api_consumer(laghu_http3_test adapters) + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set_source_files_properties(tests/adapters/http3.cpp PROPERTIES + COMPILE_OPTIONS -Wno-unsafe-buffer-usage) + endif() + target_link_libraries(laghu_http3_test PRIVATE + laghu_quic laghu_http3 laghu_test_support) + laghu_add_native_test(laghu.adapters.http3 laghu_http3_test) + + add_executable(laghu_http3_contract_test tests/adapters/http3_contract.cpp) + laghu_apply_first_party_contract(laghu_http3_contract_test) + laghu_configure_api_consumer(laghu_http3_contract_test adapters) + laghu_add_native_test(laghu.adapters.http3.contract laghu_http3_contract_test) +endif() laghu_write_fuzz_target_registry() laghu_add_fuzz_validation_tests() @@ -568,6 +612,10 @@ endif() if(TARGET laghu_http2_test) list(APPEND laghu_verify_targets laghu_http2 laghu_http2_test laghu_http2_contract_test) endif() +if(TARGET laghu_http3_test) + list(APPEND laghu_verify_targets + laghu_quic laghu_http3 laghu_http3_test laghu_http3_contract_test) +endif() laghu_collect_fuzz_targets(laghu_fuzz_verify_targets) list(APPEND laghu_verify_targets ${laghu_fuzz_verify_targets}) foreach(target IN ITEMS laghu_sanitizer_heap_misuse_fixture diff --git a/cmake/LaghuBuildIdentity.cmake b/cmake/LaghuBuildIdentity.cmake index 9b95f37..ff2dd2b 100644 --- a/cmake/LaghuBuildIdentity.cmake +++ b/cmake/LaghuBuildIdentity.cmake @@ -117,6 +117,16 @@ function(laghu_build_identity_input_hashes output) src/adapters/contract/laghu/adapters/http2.hpp src/adapters/http2.cpp) endif() + list(FIND LAGHU_EFFECTIVE_FEATURES http3 http3_feature_index) + if(NOT http3_feature_index EQUAL -1) + list(APPEND inputs + src/adapters/contract/laghu/adapters/http3.hpp + src/adapters/contract/laghu/adapters/native_memory.hpp + src/adapters/contract/laghu/adapters/quic.hpp + src/adapters/http3.cpp + src/adapters/quic.cpp + src/adapters/private/laghu/adapters/internal/arena_memory.hpp) + endif() set(entries) foreach(input IN LISTS inputs) set(path "${CMAKE_SOURCE_DIR}/${input}") diff --git a/cmake/LaghuDependencies.cmake b/cmake/LaghuDependencies.cmake index fddac8b..d50896c 100644 --- a/cmake/LaghuDependencies.cmake +++ b/cmake/LaghuDependencies.cmake @@ -596,6 +596,13 @@ function(laghu_acquire_vendored_cmake_dependency id private_target) endif() add_library("${private_target}" INTERFACE) target_link_libraries("${private_target}" INTERFACE "${vendored_target}") + if(id STREQUAL ngtcp2 OR id STREQUAL nghttp3) + # These upstream targets publish only their installed include directory. + # Laghu consumes them directly from FetchContent's source/build trees. + target_include_directories("${private_target}" SYSTEM INTERFACE + "${${content_name}_SOURCE_DIR}/lib/includes" + "${${content_name}_BINARY_DIR}/lib/includes") + endif() if(LAGHU_DEPENDENCY_LINK_MODE STREQUAL STATIC) add_dependencies("${private_target}" "${static_proof_target}") endif() diff --git a/src/adapters/contract/laghu/adapters/http3.hpp b/src/adapters/contract/laghu/adapters/http3.hpp new file mode 100644 index 0000000..43da299 --- /dev/null +++ b/src/adapters/contract/laghu/adapters/http3.hpp @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace laghu::adapters { + +enum class Http3Role : std::uint8_t { client, server }; +enum class Http3EventKind : std::uint8_t { + headers_begin, header, headers_end, data, stream_end, reset, stop_sending, + shutdown, qpack_failure, +}; + +struct Http3Header final { core::ByteView name{}; core::ByteView value{}; }; +struct Http3Event final { + // All views are valid only for the duration of the sink callback. + Http3EventKind kind{Http3EventKind::data}; + std::int64_t stream_id{-1}; + core::ByteView name{}; + core::ByteView value{}; + core::ByteView data{}; + std::uint64_t application_error{}; +}; +using Http3EventWrite = bool (*)(void*, const Http3Event&) noexcept; +struct Http3EventSink final { void* context{}; Http3EventWrite write{}; }; + +struct Http3Limits final { + std::uint64_t maximum_field_section_bytes{}; + std::uint64_t qpack_table_capacity{}; + std::uint64_t qpack_blocked_streams{}; +}; + +struct Http3Output final { + // The byte view remains valid only until the next operation on this session. + std::int64_t stream_id{-1}; + core::ByteView bytes{}; + bool fin{}; +}; + +class Http3Session final { + public: + Http3Session(const Http3Session&) = delete; + Http3Session& operator=(const Http3Session&) = delete; + Http3Session(Http3Session&& other) noexcept; + Http3Session& operator=(Http3Session&& other) noexcept; + ~Http3Session(); + + // The memory pool and its arena must outlive the session. The arena cannot + // be reset while this session holds its pin. + [[nodiscard]] static core::Result create( + Http3Role role, NativeMemoryPool& memory, + Http3Limits limits, Http3EventSink events = {}, + DependencyLogSink log_sink = {}) noexcept; + [[nodiscard]] core::Result bind_streams(std::int64_t control, + std::int64_t qpack_encoder, + std::int64_t qpack_decoder) noexcept; + [[nodiscard]] core::Result receive(std::int64_t stream_id, + core::ByteView data, + bool fin) noexcept; + [[nodiscard]] core::Result next_output() noexcept; + [[nodiscard]] core::Result mark_output_written(std::int64_t stream_id, + std::size_t bytes) noexcept; + [[nodiscard]] core::Result acknowledge_stream_data(std::int64_t stream_id, + std::uint64_t bytes) noexcept; + [[nodiscard]] core::Result submit_request( + std::int64_t stream_id, std::span headers) noexcept; + [[nodiscard]] core::Result submit_response( + std::int64_t stream_id, std::span headers) noexcept; + [[nodiscard]] core::Result close_stream(std::int64_t stream_id, + std::uint64_t code) noexcept; + [[nodiscard]] core::Result shutdown() noexcept; + + private: + Http3Session(void* connection, void* state, const core::BoundedArena& arena, + std::uint64_t generation, core::ArenaPin pin) noexcept + : connection_(connection), state_(state), arena_(&arena), generation_(generation), + pin_(std::move(pin)) {} + [[nodiscard]] core::Result require_valid() const noexcept; + void release() noexcept; + void move_from(Http3Session&& other) noexcept; + void* connection_{}; + void* state_{}; + const core::BoundedArena* arena_{}; + std::uint64_t generation_{}; + core::ArenaPin pin_{}; +}; + +static_assert(std::is_trivially_copyable_v); +static_assert(!std::is_copy_constructible_v); + +} // namespace laghu::adapters diff --git a/src/adapters/contract/laghu/adapters/native_memory.hpp b/src/adapters/contract/laghu/adapters/native_memory.hpp new file mode 100644 index 0000000..5c0a0d9 --- /dev/null +++ b/src/adapters/contract/laghu/adapters/native_memory.hpp @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#pragma once + +#include + +#include + +namespace laghu::adapters { + +namespace internal { +struct ArenaMemoryAccess; +} + +// Caller-owned reusable storage for native dependency allocations. The pool +// must outlive every adapter session that uses it. +class NativeMemoryPool final { + public: + constexpr NativeMemoryPool(core::WorkerId worker, core::BoundedArena& arena) noexcept + : arena_(&arena), worker_(worker), generation_(arena.generation()) {} + + NativeMemoryPool(const NativeMemoryPool&) = delete; + NativeMemoryPool& operator=(const NativeMemoryPool&) = delete; + NativeMemoryPool(NativeMemoryPool&&) = delete; + NativeMemoryPool& operator=(NativeMemoryPool&&) = delete; + + private: + friend struct internal::ArenaMemoryAccess; + core::BoundedArena* arena_{}; + core::WorkerId worker_; + std::uint64_t generation_{}; + void* free_list_{}; +}; + +} // namespace laghu::adapters diff --git a/src/adapters/contract/laghu/adapters/quic.hpp b/src/adapters/contract/laghu/adapters/quic.hpp new file mode 100644 index 0000000..7f3c422 --- /dev/null +++ b/src/adapters/contract/laghu/adapters/quic.hpp @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace laghu::adapters { + +enum class QuicRole : std::uint8_t { client, server }; +enum class QuicEncryptionLevel : std::uint8_t { initial, early_data, handshake, application }; +enum class QuicKeyDirection : std::uint8_t { receive, transmit }; +enum class QuicAddressFamily : std::uint8_t { ipv4, ipv6 }; + +struct QuicEndpoint final { + QuicAddressFamily family{QuicAddressFamily::ipv4}; + std::array address{}; + std::uint16_t port{}; +}; + +struct QuicPath final { + QuicEndpoint local{}; + QuicEndpoint remote{}; +}; + +class QuicConnectionId final { + public: + static constexpr std::size_t capacity = 20; + + [[nodiscard]] static core::Result create( + core::ByteView value, core::WorkerId worker, + core::GenerationId generation) noexcept; + [[nodiscard]] core::ByteView value() const noexcept { + return *core::ByteView::from(std::span{bytes_}.first(size_)); + } + [[nodiscard]] constexpr core::WorkerId worker() const noexcept { return worker_; } + [[nodiscard]] constexpr core::GenerationId generation() const noexcept { return generation_; } + + private: + friend class QuicSession; + constexpr QuicConnectionId(std::array bytes, std::uint8_t size, + core::WorkerId worker, core::GenerationId generation) noexcept + : bytes_(bytes), size_(size), worker_(worker), generation_(generation) {} + std::array bytes_{}; + std::uint8_t size_{}; + core::WorkerId worker_; + core::GenerationId generation_; +}; + +enum class QuicEventKind : std::uint8_t { + handshake_data, + handshake_completed, + stream_opened, + stream_data, + stream_data_acked, + stream_reset, + stop_sending, + stream_closed, +}; + +struct QuicEvent final { + // Any data view is valid only for the duration of the sink callback. + QuicEventKind kind{QuicEventKind::stream_data}; + QuicEncryptionLevel encryption_level{QuicEncryptionLevel::initial}; + std::int64_t stream_id{-1}; + std::uint64_t offset{}; + std::uint64_t length{}; + std::uint64_t application_error{}; + core::ByteView data{}; + bool fin{}; +}; + +using QuicEventWrite = bool (*)(void*, const QuicEvent&) noexcept; +struct QuicEventSink final { + void* context{}; + QuicEventWrite write{}; +}; + +using QuicConnectionIdWrite = bool (*)(void*, const QuicConnectionId&) noexcept; +struct QuicConnectionIdSink final { + void* context{}; + QuicConnectionIdWrite write{}; +}; + +using QuicRandomFill = bool (*)(void*, core::MutableByteView) noexcept; + +using QuicPacketProtect = bool (*)(void* callback_context, void* key_context, + core::MutableByteView output, core::ByteView input, core::ByteView nonce, + core::ByteView associated_data) noexcept; +using QuicHeaderProtect = bool (*)(void* callback_context, void* key_context, + core::MutableByteView output, core::ByteView sample) noexcept; +using QuicKeyDestroy = void (*)(void* callback_context, void* key_context) noexcept; + +struct QuicAeadAlgorithm final { + void* callback_context{}; + std::size_t maximum_overhead{}; + QuicPacketProtect encrypt{}; + QuicPacketProtect decrypt{}; +}; + +struct QuicHeaderAlgorithm final { + void* callback_context{}; + QuicHeaderProtect protect{}; +}; + +struct QuicKeyContext final { + void* handle{}; + void* destroy_context{}; + QuicKeyDestroy destroy{}; +}; + +struct QuicPacketKey final { + const QuicAeadAlgorithm* aead{}; + QuicKeyContext* aead_context{}; + const QuicHeaderAlgorithm* header{}; + QuicKeyContext* header_context{}; + core::ByteView iv{}; + core::ByteView secret{}; +}; + +struct QuicCryptoActions final { + void* context{}; + core::Result (*install_initial)(void*, const QuicPacketKey&, + const QuicPacketKey&) noexcept{}; + core::Result (*install_handshake)(void*, QuicKeyDirection, + const QuicPacketKey&) noexcept{}; + core::Result (*install_application)(void*, QuicKeyDirection, + const QuicPacketKey&) noexcept{}; + core::Result (*submit)(void*, QuicEncryptionLevel, + core::ByteView) noexcept{}; +}; + +using QuicCryptoStart = bool (*)(void*, QuicRole, core::ByteView, + const QuicCryptoActions&) noexcept; +using QuicCryptoReceive = bool (*)(void*, QuicEncryptionLevel, std::uint64_t, + core::ByteView, const QuicCryptoActions&) noexcept; +using QuicCryptoRetry = bool (*)(void*, const QuicCryptoActions&) noexcept; +using QuicCryptoUpdate = bool (*)(void*, core::MutableByteView, core::MutableByteView, + core::MutableByteView, core::MutableByteView, core::ByteView, core::ByteView, + QuicKeyContext*&, QuicKeyContext*&) noexcept; + +struct QuicCryptoCallbacks final { + void* context{}; + QuicRandomFill random_fill{}; + QuicCryptoStart start{}; + QuicCryptoReceive receive{}; + QuicCryptoRetry retry{}; + QuicCryptoUpdate update{}; +}; + +struct QuicLimits final { + std::uint64_t initial_max_data{}; + std::uint64_t initial_max_stream_data{}; + std::uint64_t initial_max_streams_bidi{}; + std::uint64_t initial_max_streams_uni{}; + std::size_t maximum_packet_bytes{}; +}; + +struct QuicPacketWrite final { + std::size_t packet_bytes{}; + std::size_t stream_bytes{}; + QuicPath path{}; +}; + +class QuicSession final { + public: + QuicSession(const QuicSession&) = delete; + QuicSession& operator=(const QuicSession&) = delete; + QuicSession(QuicSession&& other) noexcept; + QuicSession& operator=(QuicSession&& other) noexcept; + ~QuicSession(); + + // The memory pool and its arena must outlive the session. The arena cannot + // be reset while this session holds its pin. + [[nodiscard]] static core::Result create( + QuicRole role, const QuicConnectionId& destination, + const QuicConnectionId& source, NativeMemoryPool& memory, + QuicPath initial_path, + QuicLimits limits, QuicCryptoCallbacks crypto, + QuicEventSink events, QuicConnectionIdSink connection_ids, + DependencyLogSink log_sink = {}) noexcept; + + [[nodiscard]] core::Result receive_packet(QuicPath path, core::ByteView packet, + std::uint64_t now_ns) noexcept; + // Submitted stream data remains caller-owned and must stay unchanged until + // stream_data_acked covers it or stream_closed releases outstanding data. + [[nodiscard]] core::Result write_packet( + core::MutableByteView output, std::int64_t stream_id, + core::ByteView stream_data, bool fin, std::uint64_t now_ns) noexcept; + [[nodiscard]] core::Result packet_transmitted(std::uint64_t now_ns) noexcept; + [[nodiscard]] core::Result open_bidirectional_stream() noexcept; + [[nodiscard]] core::Result open_unidirectional_stream() noexcept; + [[nodiscard]] core::Result reset_stream(std::int64_t stream_id, + std::uint64_t code) noexcept; + [[nodiscard]] core::Result stop_sending(std::int64_t stream_id, + std::uint64_t code) noexcept; + [[nodiscard]] std::uint64_t expiry_ns() const noexcept; + [[nodiscard]] core::Result handle_expiry(std::uint64_t now_ns) noexcept; + + private: + QuicSession(void* connection, void* state, const core::BoundedArena& arena, + std::uint64_t generation, core::ArenaPin pin) noexcept + : connection_(connection), state_(state), arena_(&arena), generation_(generation), + pin_(std::move(pin)) {} + [[nodiscard]] core::Result require_valid() const noexcept; + void release() noexcept; + void move_from(QuicSession&& other) noexcept; + + void* connection_{}; + void* state_{}; + const core::BoundedArena* arena_{}; + std::uint64_t generation_{}; + core::ArenaPin pin_{}; +}; + +static_assert(std::is_trivially_copyable_v); +static_assert(!std::is_copy_constructible_v); + +} // namespace laghu::adapters diff --git a/src/adapters/dependency.cpp b/src/adapters/dependency.cpp index 864a014..83cec04 100644 --- a/src/adapters/dependency.cpp +++ b/src/adapters/dependency.cpp @@ -51,6 +51,15 @@ namespace { case core::DependencyOperation::http2_receive: case core::DependencyOperation::http2_send: case core::DependencyOperation::http2_submit: + case core::DependencyOperation::quic_session: + case core::DependencyOperation::quic_receive: + case core::DependencyOperation::quic_send: + case core::DependencyOperation::quic_stream: + case core::DependencyOperation::quic_expiry: + case core::DependencyOperation::http3_session: + case core::DependencyOperation::http3_receive: + case core::DependencyOperation::http3_send: + case core::DependencyOperation::http3_submit: return value; } return core::DependencyOperation::none; @@ -143,6 +152,24 @@ namespace { return "http2_send"; case core::DependencyOperation::http2_submit: return "http2_submit"; + case core::DependencyOperation::quic_session: + return "quic_session"; + case core::DependencyOperation::quic_receive: + return "quic_receive"; + case core::DependencyOperation::quic_send: + return "quic_send"; + case core::DependencyOperation::quic_stream: + return "quic_stream"; + case core::DependencyOperation::quic_expiry: + return "quic_expiry"; + case core::DependencyOperation::http3_session: + return "http3_session"; + case core::DependencyOperation::http3_receive: + return "http3_receive"; + case core::DependencyOperation::http3_send: + return "http3_send"; + case core::DependencyOperation::http3_submit: + return "http3_submit"; } return "none"; } diff --git a/src/adapters/http3.cpp b/src/adapters/http3.cpp new file mode 100644 index 0000000..9d69027 --- /dev/null +++ b/src/adapters/http3.cpp @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace laghu::adapters { +namespace { + +struct State final { + internal::ArenaMemory memory; + nghttp3_mem native_memory{}; + Http3EventSink events{}; + DependencyLogSink log{}; + std::size_t maximum_field_section_bytes{}; + bool terminal{}; +}; + +[[nodiscard]] core::Error core_error(core::ErrorCode code, const char* text) noexcept { + return {core::ErrorDomain::core, code, 0, text}; +} +[[nodiscard]] core::DependencyStatus status_for(int code) noexcept { + if (code == NGHTTP3_ERR_NOMEM) return core::DependencyStatus::exhaustion; + if (code == NGHTTP3_ERR_INVALID_ARGUMENT) return core::DependencyStatus::invalid_input; + return core::DependencyStatus::corrupt_data; +} +[[nodiscard]] core::Error native_error(core::DependencyOperation operation, int code, + const DependencyLogSink& sink) noexcept { + const auto error = normalize_dependency_error(core::DependencyId::nghttp3, operation, + status_for(code), code); + log_dependency_error(sink, error); + return error; +} +[[nodiscard]] int emit(State& state, const Http3Event& event) noexcept { + return state.events.write == nullptr || state.events.write(state.events.context, event) + ? 0 : NGHTTP3_ERR_CALLBACK_FAILURE; +} +int begin_headers(nghttp3_conn*, std::int64_t stream, void* user, void*) { + return emit(*static_cast(user), {Http3EventKind::headers_begin, stream}); +} +int recv_header(nghttp3_conn*, std::int64_t stream, std::int32_t, + nghttp3_rcbuf* name, nghttp3_rcbuf* value, std::uint8_t, + void* user, void*) { + const nghttp3_vec native_name = nghttp3_rcbuf_get_buf(name); + const nghttp3_vec native_value = nghttp3_rcbuf_get_buf(value); + const auto name_view = *core::ByteView::from(std::span{ + reinterpret_cast(native_name.base), native_name.len}); + const auto value_view = *core::ByteView::from(std::span{ + reinterpret_cast(native_value.base), native_value.len}); + return emit(*static_cast(user), {Http3EventKind::header, stream, + name_view, value_view}); +} +int end_headers(nghttp3_conn*, std::int64_t stream, int, void* user, void*) { + return emit(*static_cast(user), {Http3EventKind::headers_end, stream}); +} +int recv_data(nghttp3_conn*, std::int64_t stream, const std::uint8_t* data, + std::size_t size, void* user, void*) { + const auto view = *core::ByteView::from(std::span{ + reinterpret_cast(data), size}); + return emit(*static_cast(user), {Http3EventKind::data, stream, {}, {}, view}); +} +int end_stream(nghttp3_conn*, std::int64_t stream, void* user, void*) { + return emit(*static_cast(user), {Http3EventKind::stream_end, stream}); +} +int stop_sending(nghttp3_conn*, std::int64_t stream, std::uint64_t code, void* user, void*) { + return emit(*static_cast(user), {Http3EventKind::stop_sending, stream, {}, {}, {}, code}); +} +int reset_stream(nghttp3_conn*, std::int64_t stream, std::uint64_t code, void* user, void*) { + return emit(*static_cast(user), {Http3EventKind::reset, stream, {}, {}, {}, code}); +} +int shutdown_cb(nghttp3_conn*, std::int64_t id, void* user) { + return emit(*static_cast(user), {Http3EventKind::shutdown, id}); +} + +struct NativeHeaders final { + static constexpr std::size_t capacity = 64; + std::array values{}; + std::size_t size{}; +}; + +[[nodiscard]] core::Result native_headers( + std::span headers, std::size_t maximum_bytes) noexcept { + if (headers.empty() || headers.size() > NativeHeaders::capacity) { + return std::unexpected{core_error(core::ErrorCode::invalid_range, + "HTTP/3 headers must contain 1 to 64 fields")}; + } + NativeHeaders result{}; + std::size_t total{}; + for (const auto& header : headers) { + if (header.name.empty() || header.name.size() > maximum_bytes - total || + header.value.size() > maximum_bytes - total - header.name.size()) { + return std::unexpected{core_error(core::ErrorCode::invalid_input, + "HTTP/3 headers exceed the configured field-section limit")}; + } + total += header.name.size() + header.value.size(); + result.values[result.size] = { + const_cast(reinterpret_cast(header.name.data())), + const_cast(reinterpret_cast(header.value.data())), + header.name.size(), header.value.size(), NGHTTP3_NV_FLAG_NONE}; + ++result.size; + } + return result; +} + +} // namespace + +Http3Session::Http3Session(Http3Session&& other) noexcept { move_from(std::move(other)); } +Http3Session& Http3Session::operator=(Http3Session&& other) noexcept { + if (this != &other) { release(); move_from(std::move(other)); } + return *this; +} +Http3Session::~Http3Session() { release(); } + +core::Result Http3Session::create( + Http3Role role, NativeMemoryPool& memory, Http3Limits limits, + Http3EventSink events, DependencyLogSink log_sink) noexcept { + if ((role != Http3Role::client && role != Http3Role::server) || + limits.maximum_field_section_bytes == 0 || + limits.maximum_field_section_bytes > std::numeric_limits::max()) { + return std::unexpected{core_error(core::ErrorCode::invalid_input, + "HTTP/3 role or field-section limit is invalid")}; + } + internal::ArenaMemoryAccess::synchronize(memory); + auto& arena = internal::ArenaMemoryAccess::arena(memory); + const auto worker = internal::ArenaMemoryAccess::worker(memory); + auto pin = arena.pin(worker); + if (!pin.has_value()) return std::unexpected{pin.error()}; + internal::ArenaMemory allocator{&memory}; + void* const storage = internal::arena_malloc(sizeof(State), &allocator); + if (storage == nullptr) return std::unexpected{core_error( + core::ErrorCode::exhaustion, "HTTP/3 session storage is exhausted")}; + auto* const state = ::new (storage) State{{&memory}, {}, events, log_sink, + static_cast(limits.maximum_field_section_bytes), false}; + nghttp3_callbacks callbacks{}; + callbacks.begin_headers = begin_headers; + callbacks.recv_header = recv_header; + callbacks.end_headers = end_headers; + callbacks.recv_data = recv_data; + callbacks.end_stream = end_stream; + callbacks.stop_sending = stop_sending; + callbacks.reset_stream = reset_stream; + callbacks.shutdown = shutdown_cb; + nghttp3_settings settings{}; + nghttp3_settings_default(&settings); + settings.max_field_section_size = limits.maximum_field_section_bytes; + settings.qpack_max_dtable_capacity = limits.qpack_table_capacity; + settings.qpack_encoder_max_dtable_capacity = limits.qpack_table_capacity; + settings.qpack_blocked_streams = limits.qpack_blocked_streams; + state->native_memory = {&state->memory, internal::arena_malloc, internal::arena_free, + internal::arena_calloc, internal::arena_realloc}; + nghttp3_conn* connection{}; + const int result = role == Http3Role::client + ? nghttp3_conn_client_new(&connection, &callbacks, &settings, &state->native_memory, state) + : nghttp3_conn_server_new(&connection, &callbacks, &settings, &state->native_memory, state); + if (result != 0) { + state->~State(); + internal::arena_free(storage, &allocator); + return std::unexpected{native_error(core::DependencyOperation::http3_session, + result, log_sink)}; + } + return Http3Session{connection, state, arena, arena.generation(), std::move(*pin)}; +} + +core::Result Http3Session::require_valid() const noexcept { + if (connection_ == nullptr || state_ == nullptr || arena_ == nullptr || + arena_->generation() != generation_ || static_cast(state_)->terminal) { + return std::unexpected{core_error(core::ErrorCode::invalid_state, + "HTTP/3 session is inactive or its arena was reset")}; + } + return {}; +} +core::Result Http3Session::bind_streams(std::int64_t control, + std::int64_t encoder, + std::int64_t decoder) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) return valid; + auto* const connection = static_cast(connection_); + int result = nghttp3_conn_bind_control_stream(connection, control); + if (result == 0) result = nghttp3_conn_bind_qpack_streams(connection, encoder, decoder); + if (result != 0) return std::unexpected{native_error(core::DependencyOperation::http3_session, + result, static_cast(state_)->log)}; + return {}; +} +core::Result Http3Session::receive(std::int64_t stream, core::ByteView data, + bool fin) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) return std::unexpected{valid.error()}; + const auto result = nghttp3_conn_read_stream2(static_cast(connection_), stream, + reinterpret_cast(data.data()), data.size(), fin ? 1 : 0, 0); + if (result < 0) { + auto& state = *static_cast(state_); + state.terminal = true; + return std::unexpected{native_error(core::DependencyOperation::http3_receive, + static_cast(result), state.log)}; + } + return static_cast(result); +} +core::Result Http3Session::next_output() noexcept { + if (const auto valid = require_valid(); !valid.has_value()) return std::unexpected{valid.error()}; + std::int64_t stream{-1}; int fin{}; nghttp3_vec vector{}; + const auto count = nghttp3_conn_writev_stream(static_cast(connection_), + &stream, &fin, &vector, 1); + if (count < 0) { + auto& state = *static_cast(state_); + state.terminal = true; + return std::unexpected{native_error(core::DependencyOperation::http3_send, + static_cast(count), state.log)}; + } + if (count == 0) return Http3Output{stream, {}, fin != 0}; + const auto view = *core::ByteView::from(std::span{ + reinterpret_cast(vector.base), vector.len}); + return Http3Output{stream, view, fin != 0}; +} +core::Result Http3Session::mark_output_written(std::int64_t stream, + std::size_t bytes) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) return valid; + const int result = nghttp3_conn_add_write_offset(static_cast(connection_), stream, bytes); + if (result != 0) return std::unexpected{native_error(core::DependencyOperation::http3_send, + result, static_cast(state_)->log)}; + return {}; +} +core::Result Http3Session::acknowledge_stream_data(std::int64_t stream, + std::uint64_t bytes) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) return valid; + const int result = nghttp3_conn_add_ack_offset( + static_cast(connection_), stream, bytes); + if (result != 0) return std::unexpected{native_error(core::DependencyOperation::http3_send, + result, static_cast(state_)->log)}; + return {}; +} +core::Result Http3Session::submit_request(std::int64_t stream, + std::span headers) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) return valid; + const auto native = native_headers( + headers, static_cast(state_)->maximum_field_section_bytes); + if (!native.has_value()) return std::unexpected{native.error()}; + const int result = nghttp3_conn_submit_request(static_cast(connection_), stream, + native->values.data(), native->size, nullptr, nullptr); + if (result != 0) return std::unexpected{native_error(core::DependencyOperation::http3_submit, + result, static_cast(state_)->log)}; + return {}; +} +core::Result Http3Session::submit_response(std::int64_t stream, + std::span headers) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) return valid; + const auto native = native_headers( + headers, static_cast(state_)->maximum_field_section_bytes); + if (!native.has_value()) return std::unexpected{native.error()}; + const int result = nghttp3_conn_submit_response(static_cast(connection_), stream, + native->values.data(), native->size, nullptr); + if (result != 0) return std::unexpected{native_error(core::DependencyOperation::http3_submit, + result, static_cast(state_)->log)}; + return {}; +} +core::Result Http3Session::close_stream(std::int64_t stream, std::uint64_t code) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) return valid; + const int result = nghttp3_conn_close_stream2(static_cast(connection_), + NGHTTP3_STREAM_CLOSE_FLAG_RX_APP_ERROR_CODE_SET | + NGHTTP3_STREAM_CLOSE_FLAG_TX_APP_ERROR_CODE_SET, + stream, code, code); + if (result != 0) return std::unexpected{native_error(core::DependencyOperation::http3_submit, + result, static_cast(state_)->log)}; + return {}; +} +core::Result Http3Session::shutdown() noexcept { + if (const auto valid = require_valid(); !valid.has_value()) return valid; + const int result = nghttp3_conn_shutdown(static_cast(connection_)); + if (result != 0) return std::unexpected{native_error(core::DependencyOperation::http3_submit, + result, static_cast(state_)->log)}; + return {}; +} +void Http3Session::release() noexcept { + if (connection_ != nullptr && arena_ != nullptr && arena_->generation() == generation_) { + nghttp3_conn_del(static_cast(connection_)); + auto* const state = static_cast(state_); + internal::ArenaMemory allocator{state->memory.pool}; + state->~State(); + internal::arena_free(state, &allocator); + } + connection_ = nullptr; state_ = nullptr; arena_ = nullptr; generation_ = 0; + pin_ = {}; +} +void Http3Session::move_from(Http3Session&& other) noexcept { + connection_ = std::exchange(other.connection_, nullptr); + state_ = std::exchange(other.state_, nullptr); + arena_ = std::exchange(other.arena_, nullptr); + generation_ = std::exchange(other.generation_, 0); + pin_ = std::move(other.pin_); +} + +} // namespace laghu::adapters diff --git a/src/adapters/private/laghu/adapters/internal/arena_memory.hpp b/src/adapters/private/laghu/adapters/internal/arena_memory.hpp new file mode 100644 index 0000000..7739671 --- /dev/null +++ b/src/adapters/private/laghu/adapters/internal/arena_memory.hpp @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#pragma once + +#include +#include +#include +#include + +#include +#include + +namespace laghu::adapters::internal { + +struct alignas(std::max_align_t) ArenaAllocation final { + std::size_t capacity{}; + std::size_t used{}; + ArenaAllocation* next{}; +}; + +struct ArenaMemory final { + NativeMemoryPool* pool{}; +}; + +struct ArenaMemoryAccess final { + static void synchronize(NativeMemoryPool& pool) noexcept { + if (pool.generation_ != pool.arena_->generation()) { + pool.generation_ = pool.arena_->generation(); + pool.free_list_ = nullptr; + } + } + static core::BoundedArena& arena(NativeMemoryPool& pool) noexcept { return *pool.arena_; } + static core::WorkerId worker(const NativeMemoryPool& pool) noexcept { return pool.worker_; } + static void*& free_list(NativeMemoryPool& pool) noexcept { return pool.free_list_; } +}; + +inline void* arena_malloc(std::size_t requested, void* context) noexcept { + auto& memory = *static_cast(context); + ArenaMemoryAccess::synchronize(*memory.pool); + const std::size_t size = requested == 0U ? 1U : requested; + ArenaAllocation* previous{}; + auto* allocation = static_cast( + ArenaMemoryAccess::free_list(*memory.pool)); + while (allocation != nullptr && allocation->capacity < size) { + previous = allocation; + allocation = allocation->next; + } + if (allocation != nullptr) { + if (previous == nullptr) { + ArenaMemoryAccess::free_list(*memory.pool) = allocation->next; + } else { + previous->next = allocation->next; + } + allocation->used = size; + allocation->next = nullptr; + return allocation + 1; + } + if (size > std::numeric_limits::max() - sizeof(ArenaAllocation)) { + return nullptr; + } + const auto arena_allocation = ArenaMemoryAccess::arena(*memory.pool).try_allocate( + ArenaMemoryAccess::worker(*memory.pool), sizeof(ArenaAllocation) + size, + alignof(std::max_align_t)); + if (!arena_allocation.has_value()) return nullptr; + const auto bytes = arena_allocation->bytes(); + if (!bytes.has_value()) return nullptr; + auto* const header = ::new (bytes->data()) ArenaAllocation{size, size, nullptr}; + return header + 1; +} + +inline void arena_free(void* pointer, void* context) noexcept { + if (pointer == nullptr) return; + auto& memory = *static_cast(context); + ArenaMemoryAccess::synchronize(*memory.pool); + auto* const allocation = static_cast(pointer) - 1; + allocation->next = static_cast( + ArenaMemoryAccess::free_list(*memory.pool)); + ArenaMemoryAccess::free_list(*memory.pool) = allocation; +} + +inline void* arena_calloc(std::size_t count, std::size_t size, void* context) noexcept { + if (count != 0U && size > std::numeric_limits::max() / count) return nullptr; + const std::size_t total = count * size; + void* const result = arena_malloc(total, context); + if (result != nullptr) std::memset(result, 0, total); + return result; +} + +inline void* arena_realloc(void* pointer, std::size_t size, void* context) noexcept { + if (pointer == nullptr) return arena_malloc(size, context); + if (size == 0U) { + arena_free(pointer, context); + return nullptr; + } + auto* const old = static_cast(pointer) - 1; + if (size <= old->capacity) { + old->used = size; + return pointer; + } + void* const result = arena_malloc(size, context); + if (result == nullptr) return nullptr; + std::memcpy(result, pointer, old->used < size ? old->used : size); + arena_free(pointer, context); + return result; +} + +} // namespace laghu::adapters::internal diff --git a/src/adapters/quic.cpp b/src/adapters/quic.cpp new file mode 100644 index 0000000..bb4ff08 --- /dev/null +++ b/src/adapters/quic.cpp @@ -0,0 +1,786 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace laghu::adapters { +namespace { + +struct State final { + internal::ArenaMemory memory; + ngtcp2_mem native_memory{}; + ngtcp2_path_storage path{}; + QuicCryptoCallbacks crypto{}; + QuicEventSink events{}; + QuicConnectionIdSink connection_ids{}; + DependencyLogSink log{}; + QuicRole role{QuicRole::client}; + core::WorkerId worker; + core::GenerationId generation; + std::array initial_dcid{}; + std::size_t initial_dcid_size{}; + std::size_t application_iv_size{}; + std::size_t maximum_packet_bytes{}; + bool random_failed{}; + bool terminal{}; + bool packet_pending_transmit{}; +}; + +struct ActionContext final { + ngtcp2_conn* connection{}; + State* state{}; + std::uint32_t negotiated_version{}; +}; + +[[nodiscard]] core::Error core_error(core::ErrorCode code, const char* text) noexcept { + return {core::ErrorDomain::core, code, 0, text}; +} + +[[nodiscard]] core::DependencyStatus status_for(int code) noexcept { + if (code == NGTCP2_ERR_NOMEM || code == NGTCP2_ERR_STREAM_ID_BLOCKED || + code == NGTCP2_ERR_STREAM_DATA_BLOCKED) return core::DependencyStatus::exhaustion; + if (code == NGTCP2_ERR_INVALID_ARGUMENT) return core::DependencyStatus::invalid_input; + if (code == NGTCP2_ERR_CRYPTO) return core::DependencyStatus::crypto; + return core::DependencyStatus::corrupt_data; +} + +[[nodiscard]] bool terminal_error(int code) noexcept { + return code == NGTCP2_ERR_RETRY || code == NGTCP2_ERR_DROP_CONN || + code == NGTCP2_ERR_DRAINING || code == NGTCP2_ERR_CLOSING || + ngtcp2_err_is_fatal(code) != 0; +} + +[[nodiscard]] bool valid_endpoint(const QuicEndpoint& endpoint) noexcept { + return (endpoint.family == QuicAddressFamily::ipv4 || + endpoint.family == QuicAddressFamily::ipv6) && endpoint.port != 0U; +} + +[[nodiscard]] bool store_endpoint(const QuicEndpoint& endpoint, + ngtcp2_sockaddr_union& storage, + ngtcp2_addr& output) noexcept { + if (!valid_endpoint(endpoint)) return false; + std::memset(&storage, 0, sizeof(storage)); + if (endpoint.family == QuicAddressFamily::ipv4) { + auto& address = storage.in; + address.sin_family = AF_INET; + address.sin_port = htons(endpoint.port); + std::memcpy(&address.sin_addr, endpoint.address.data(), 4U); + output = {reinterpret_cast(&address), sizeof(address)}; + return true; + } + auto& address = storage.in6; + address.sin6_family = AF_INET6; + address.sin6_port = htons(endpoint.port); + std::memcpy(&address.sin6_addr, endpoint.address.data(), 16U); + output = {reinterpret_cast(&address), sizeof(address)}; + return true; +} + +[[nodiscard]] bool store_path(const QuicPath& path, ngtcp2_path_storage& output) noexcept { + output.path.user_data = nullptr; + return store_endpoint(path.local, output.local_addrbuf, output.path.local) && + store_endpoint(path.remote, output.remote_addrbuf, output.path.remote); +} + +[[nodiscard]] QuicEndpoint endpoint_from(const ngtcp2_addr& input) noexcept { + QuicEndpoint output{}; + if (input.addr != nullptr && input.addr->sa_family == AF_INET6 && + input.addrlen >= sizeof(sockaddr_in6)) { + sockaddr_in6 address{}; + std::memcpy(&address, input.addr, sizeof(address)); + output.family = QuicAddressFamily::ipv6; + output.port = ntohs(address.sin6_port); + std::memcpy(output.address.data(), &address.sin6_addr, 16U); + } else if (input.addr != nullptr && input.addr->sa_family == AF_INET && + input.addrlen >= sizeof(sockaddr_in)) { + sockaddr_in address{}; + std::memcpy(&address, input.addr, sizeof(address)); + output.family = QuicAddressFamily::ipv4; + output.port = ntohs(address.sin_port); + std::memcpy(output.address.data(), &address.sin_addr, 4U); + } + return output; +} + +[[nodiscard]] QuicPath path_from(const ngtcp2_path& input) noexcept { + return {endpoint_from(input.local), endpoint_from(input.remote)}; +} + +[[nodiscard]] core::Error native_error(core::DependencyOperation operation, int code, + const DependencyLogSink& sink) noexcept { + const auto error = normalize_dependency_error( + core::DependencyId::ngtcp2, operation, status_for(code), code); + log_dependency_error(sink, error); + return error; +} + +[[nodiscard]] core::Result require_random(State& state) noexcept { + if (!state.random_failed) return {}; + state.random_failed = false; + const auto error = normalize_dependency_error(core::DependencyId::ngtcp2, + core::DependencyOperation::quic_session, core::DependencyStatus::crypto, + NGTCP2_ERR_CALLBACK_FAILURE); + log_dependency_error(state.log, error); + return std::unexpected{error}; +} + +void random_fill(std::uint8_t* destination, std::size_t size, + const ngtcp2_rand_ctx* context) { + auto& state = *static_cast(context->native_handle); + const auto output = *core::MutableByteView::from( + std::span{reinterpret_cast(destination), size}); + if (state.crypto.random_fill == nullptr || + !state.crypto.random_fill(state.crypto.context, output)) { + state.random_failed = true; + std::memset(destination, 0, size); + } +} + +[[nodiscard]] QuicEncryptionLevel encryption_level(ngtcp2_encryption_level level) noexcept { + switch (level) { + case NGTCP2_ENCRYPTION_LEVEL_INITIAL: return QuicEncryptionLevel::initial; + case NGTCP2_ENCRYPTION_LEVEL_0RTT: return QuicEncryptionLevel::early_data; + case NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE: return QuicEncryptionLevel::handshake; + case NGTCP2_ENCRYPTION_LEVEL_1RTT: return QuicEncryptionLevel::application; + } + return QuicEncryptionLevel::initial; +} + +[[nodiscard]] ngtcp2_encryption_level native_level(QuicEncryptionLevel level) noexcept { + switch (level) { + case QuicEncryptionLevel::initial: return NGTCP2_ENCRYPTION_LEVEL_INITIAL; + case QuicEncryptionLevel::early_data: return NGTCP2_ENCRYPTION_LEVEL_0RTT; + case QuicEncryptionLevel::handshake: return NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE; + case QuicEncryptionLevel::application: return NGTCP2_ENCRYPTION_LEVEL_1RTT; + } + return NGTCP2_ENCRYPTION_LEVEL_INITIAL; +} + +[[nodiscard]] bool valid_key(const QuicPacketKey& key, bool require_secret) noexcept { + return key.aead != nullptr && key.aead_context != nullptr && key.header != nullptr && + key.header_context != nullptr && key.aead->maximum_overhead != 0U && + key.aead->encrypt != nullptr && key.aead->decrypt != nullptr && + key.header->protect != nullptr && !key.iv.empty() && + (!require_secret || !key.secret.empty()); +} + +[[nodiscard]] ngtcp2_crypto_ctx crypto_context(const QuicPacketKey& key) noexcept { + ngtcp2_crypto_ctx result{}; + result.aead = {const_cast(key.aead), key.aead->maximum_overhead}; + result.hp = {const_cast(key.header)}; + result.max_encryption = std::numeric_limits::max(); + result.max_decryption_failure = std::numeric_limits::max(); + return result; +} + +[[nodiscard]] ngtcp2_crypto_aead_ctx aead_context(QuicKeyContext& value) noexcept { + return {&value}; +} +[[nodiscard]] ngtcp2_crypto_cipher_ctx header_context(QuicKeyContext& value) noexcept { + return {&value}; +} + +core::Result install_initial(void* context, const QuicPacketKey& receive, + const QuicPacketKey& transmit) noexcept { + auto& action = *static_cast(context); + if (!valid_key(receive, false) || !valid_key(transmit, false) || + receive.iv.size() != transmit.iv.size() || receive.aead != transmit.aead || + receive.header != transmit.header) { + return std::unexpected{core_error(core::ErrorCode::invalid_input, + "QUIC initial keys are incomplete or inconsistent")}; + } + const ngtcp2_crypto_ctx native_crypto = crypto_context(receive); + ngtcp2_conn_set_initial_crypto_ctx(action.connection, &native_crypto); + const auto rx_aead = aead_context(*receive.aead_context); + const auto rx_hp = header_context(*receive.header_context); + const auto tx_aead = aead_context(*transmit.aead_context); + const auto tx_hp = header_context(*transmit.header_context); + const int result = action.negotiated_version == 0U + ? ngtcp2_conn_install_initial_key(action.connection, &rx_aead, + reinterpret_cast(receive.iv.data()), &rx_hp, &tx_aead, + reinterpret_cast(transmit.iv.data()), &tx_hp, + receive.iv.size()) + : ngtcp2_conn_install_vneg_initial_key(action.connection, action.negotiated_version, + &rx_aead, reinterpret_cast(receive.iv.data()), &rx_hp, + &tx_aead, reinterpret_cast(transmit.iv.data()), &tx_hp, + receive.iv.size()); + if (result != 0) return std::unexpected{native_error( + core::DependencyOperation::quic_session, result, action.state->log)}; + return {}; +} + +core::Result install_handshake(void* context, QuicKeyDirection direction, + const QuicPacketKey& key) noexcept { + auto& action = *static_cast(context); + if (!valid_key(key, false)) return std::unexpected{core_error( + core::ErrorCode::invalid_input, "QUIC handshake key is incomplete")}; + const ngtcp2_crypto_ctx native_crypto = crypto_context(key); + ngtcp2_conn_set_crypto_ctx(action.connection, &native_crypto); + const auto aead = aead_context(*key.aead_context); + const auto hp = header_context(*key.header_context); + const int result = direction == QuicKeyDirection::receive + ? ngtcp2_conn_install_rx_handshake_key(action.connection, &aead, + reinterpret_cast(key.iv.data()), key.iv.size(), &hp) + : ngtcp2_conn_install_tx_handshake_key(action.connection, &aead, + reinterpret_cast(key.iv.data()), key.iv.size(), &hp); + if (result != 0) return std::unexpected{native_error( + core::DependencyOperation::quic_session, result, action.state->log)}; + return {}; +} + +core::Result install_application(void* context, QuicKeyDirection direction, + const QuicPacketKey& key) noexcept { + auto& action = *static_cast(context); + if (!valid_key(key, true)) return std::unexpected{core_error( + core::ErrorCode::invalid_input, "QUIC application key is incomplete")}; + action.state->application_iv_size = key.iv.size(); + const ngtcp2_crypto_ctx native_crypto = crypto_context(key); + ngtcp2_conn_set_crypto_ctx(action.connection, &native_crypto); + const auto aead = aead_context(*key.aead_context); + const auto hp = header_context(*key.header_context); + const int result = direction == QuicKeyDirection::receive + ? ngtcp2_conn_install_rx_key(action.connection, + reinterpret_cast(key.secret.data()), key.secret.size(), &aead, + reinterpret_cast(key.iv.data()), key.iv.size(), &hp) + : ngtcp2_conn_install_tx_key(action.connection, + reinterpret_cast(key.secret.data()), key.secret.size(), &aead, + reinterpret_cast(key.iv.data()), key.iv.size(), &hp); + if (result != 0) return std::unexpected{native_error( + core::DependencyOperation::quic_session, result, action.state->log)}; + return {}; +} + +core::Result submit_crypto(void* context, QuicEncryptionLevel level, + core::ByteView data) noexcept { + auto& action = *static_cast(context); + const int result = ngtcp2_conn_submit_crypto_data(action.connection, native_level(level), + reinterpret_cast(data.data()), data.size()); + if (result != 0) return std::unexpected{native_error( + core::DependencyOperation::quic_send, result, action.state->log)}; + return {}; +} + +[[nodiscard]] QuicCryptoActions actions(ActionContext& context) noexcept { + return {&context, install_initial, install_handshake, install_application, submit_crypto}; +} + +int client_initial(ngtcp2_conn* connection, void* user_data) { + auto& state = *static_cast(user_data); + ActionContext context{connection, &state, 0}; + const auto id = *core::ByteView::from( + std::span{state.initial_dcid}.first(state.initial_dcid_size)); + return state.crypto.start != nullptr && + state.crypto.start(state.crypto.context, QuicRole::client, id, actions(context)) + ? 0 : NGTCP2_ERR_CALLBACK_FAILURE; +} +int recv_client_initial(ngtcp2_conn* connection, const ngtcp2_cid* dcid, + void* user_data) { + auto& state = *static_cast(user_data); + ActionContext context{connection, &state, 0}; + const auto id = *core::ByteView::from(std::span{ + reinterpret_cast(dcid->data), dcid->datalen}); + return state.crypto.start != nullptr && + state.crypto.start(state.crypto.context, QuicRole::server, id, actions(context)) + ? 0 : NGTCP2_ERR_CALLBACK_FAILURE; +} +int recv_crypto(ngtcp2_conn* connection, ngtcp2_encryption_level level, std::uint64_t offset, + const std::uint8_t* data, std::size_t size, void* user_data) { + auto& state = *static_cast(user_data); + const auto mapped = encryption_level(level); + const auto view = *core::ByteView::from( + std::span{reinterpret_cast(data), size}); + ActionContext context{connection, &state, 0}; + if (state.crypto.receive == nullptr || + !state.crypto.receive(state.crypto.context, mapped, offset, view, actions(context))) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + if (state.events.write == nullptr) return 0; + const QuicEvent event{QuicEventKind::handshake_data, mapped, -1, offset, + static_cast(size), 0, view, false}; + return state.events.write(state.events.context, event) ? 0 : NGTCP2_ERR_CALLBACK_FAILURE; +} +int encrypt(std::uint8_t* destination, const ngtcp2_crypto_aead* aead, + const ngtcp2_crypto_aead_ctx* key, const std::uint8_t* input, + std::size_t input_size, const std::uint8_t* nonce, std::size_t nonce_size, + const std::uint8_t* aad, std::size_t aad_size) { + const auto& algorithm = *static_cast(aead->native_handle); + auto& context = *static_cast(key->native_handle); + const auto output = *core::MutableByteView::from(std::span{ + reinterpret_cast(destination), input_size + aead->max_overhead}); + const auto source = *core::ByteView::from(std::span{ + reinterpret_cast(input), input_size}); + const auto nonce_view = *core::ByteView::from(std::span{ + reinterpret_cast(nonce), nonce_size}); + const auto aad_view = *core::ByteView::from(std::span{ + reinterpret_cast(aad), aad_size}); + return algorithm.encrypt(algorithm.callback_context, context.handle, output, source, + nonce_view, aad_view) ? 0 : NGTCP2_ERR_CALLBACK_FAILURE; +} +int decrypt(std::uint8_t* destination, const ngtcp2_crypto_aead* aead, + const ngtcp2_crypto_aead_ctx* key, const std::uint8_t* input, + std::size_t input_size, const std::uint8_t* nonce, std::size_t nonce_size, + const std::uint8_t* aad, std::size_t aad_size) { + const auto& algorithm = *static_cast(aead->native_handle); + auto& context = *static_cast(key->native_handle); + const std::size_t output_size = input_size >= aead->max_overhead + ? input_size - aead->max_overhead : 0U; + const auto output = *core::MutableByteView::from(std::span{ + reinterpret_cast(destination), output_size}); + const auto source = *core::ByteView::from(std::span{ + reinterpret_cast(input), input_size}); + const auto nonce_view = *core::ByteView::from(std::span{ + reinterpret_cast(nonce), nonce_size}); + const auto aad_view = *core::ByteView::from(std::span{ + reinterpret_cast(aad), aad_size}); + return algorithm.decrypt(algorithm.callback_context, context.handle, output, source, + nonce_view, aad_view) ? 0 : NGTCP2_ERR_DECRYPT; +} +int header_mask(std::uint8_t* destination, const ngtcp2_crypto_cipher* cipher, + const ngtcp2_crypto_cipher_ctx* key, const std::uint8_t* sample) { + const auto& algorithm = *static_cast(cipher->native_handle); + auto& context = *static_cast(key->native_handle); + const auto output = *core::MutableByteView::from(std::span{ + reinterpret_cast(destination), NGTCP2_HP_MASKLEN}); + const auto sample_view = *core::ByteView::from(std::span{ + reinterpret_cast(sample), NGTCP2_HP_SAMPLELEN}); + return algorithm.protect(algorithm.callback_context, context.handle, output, sample_view) + ? 0 : NGTCP2_ERR_CALLBACK_FAILURE; +} +int recv_retry(ngtcp2_conn* connection, const ngtcp2_pkt_hd*, void* user_data) { + auto& state = *static_cast(user_data); + ActionContext context{connection, &state, 0}; + return state.crypto.retry != nullptr && + state.crypto.retry(state.crypto.context, actions(context)) + ? 0 : NGTCP2_ERR_CALLBACK_FAILURE; +} +int update_key(ngtcp2_conn*, std::uint8_t* rx_secret, std::uint8_t* tx_secret, + ngtcp2_crypto_aead_ctx* rx_aead, std::uint8_t* rx_iv, + ngtcp2_crypto_aead_ctx* tx_aead, std::uint8_t* tx_iv, + const std::uint8_t* current_rx, const std::uint8_t* current_tx, + std::size_t secret_size, void* user_data) { + auto& state = *static_cast(user_data); + if (state.crypto.update == nullptr || state.application_iv_size == 0U) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + const auto mutable_view = [](std::uint8_t* data, std::size_t size) noexcept { + return *core::MutableByteView::from(std::span{ + reinterpret_cast(data), size}); + }; + const auto view = [](const std::uint8_t* data, std::size_t size) noexcept { + return *core::ByteView::from(std::span{ + reinterpret_cast(data), size}); + }; + QuicKeyContext* receive{}; + QuicKeyContext* transmit{}; + if (!state.crypto.update(state.crypto.context, mutable_view(rx_secret, secret_size), + mutable_view(tx_secret, secret_size), mutable_view(rx_iv, state.application_iv_size), + mutable_view(tx_iv, state.application_iv_size), view(current_rx, secret_size), + view(current_tx, secret_size), receive, transmit) || receive == nullptr || + transmit == nullptr) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + rx_aead->native_handle = receive; + tx_aead->native_handle = transmit; + return 0; +} +int version_negotiation(ngtcp2_conn* connection, std::uint32_t version, + const ngtcp2_cid* dcid, void* user_data) { + auto& state = *static_cast(user_data); + ActionContext context{connection, &state, version}; + const auto id = *core::ByteView::from(std::span{ + reinterpret_cast(dcid->data), dcid->datalen}); + return state.crypto.start != nullptr && + state.crypto.start(state.crypto.context, state.role, id, actions(context)) + ? 0 : NGTCP2_ERR_CALLBACK_FAILURE; +} +int new_connection_id(ngtcp2_conn*, ngtcp2_cid* cid, ngtcp2_stateless_reset_token* token, + std::size_t size, void* user_data) { + auto& state = *static_cast(user_data); + std::array bytes{}; + const auto cid_output = *core::MutableByteView::from(std::span{ + reinterpret_cast(bytes.data()), size}); + const auto token_output = *core::MutableByteView::from(std::span{ + reinterpret_cast(token->data), sizeof(token->data)}); + if (!state.crypto.random_fill(state.crypto.context, cid_output) || + !state.crypto.random_fill(state.crypto.context, token_output)) { + state.random_failed = true; + return NGTCP2_ERR_CALLBACK_FAILURE; + } + const auto value = *core::ByteView::from(std::span{ + reinterpret_cast(bytes.data()), size}); + const auto owned = QuicConnectionId::create(value, state.worker, state.generation); + if (!owned.has_value() || state.connection_ids.write == nullptr || + !state.connection_ids.write(state.connection_ids.context, *owned)) { + return NGTCP2_ERR_CALLBACK_FAILURE; + } + ngtcp2_cid_init(cid, bytes.data(), size); + return 0; +} +int path_challenge(ngtcp2_conn*, ngtcp2_path_challenge_data* data, void* user_data) { + auto& state = *static_cast(user_data); + const auto output = *core::MutableByteView::from(std::span{ + reinterpret_cast(data->data), sizeof(data->data)}); + if (!state.crypto.random_fill(state.crypto.context, output)) { + state.random_failed = true; + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} + +[[nodiscard]] int emit(State& state, const QuicEvent& event) noexcept { + return state.events.write == nullptr || state.events.write(state.events.context, event) + ? 0 : NGTCP2_ERR_CALLBACK_FAILURE; +} +int handshake_completed(ngtcp2_conn*, void* user_data) { + return emit(*static_cast(user_data), + {QuicEventKind::handshake_completed}); +} +int stream_opened(ngtcp2_conn*, std::int64_t stream, void* user_data) { + return emit(*static_cast(user_data), + {QuicEventKind::stream_opened, QuicEncryptionLevel::application, stream}); +} +int stream_data(ngtcp2_conn*, std::uint32_t flags, std::int64_t stream, + std::uint64_t offset, const std::uint8_t* data, std::size_t size, + void* user_data, void*) { + const auto view = *core::ByteView::from(std::span{ + reinterpret_cast(data), size}); + return emit(*static_cast(user_data), + {QuicEventKind::stream_data, QuicEncryptionLevel::application, stream, offset, + static_cast(size), 0, view, + (flags & NGTCP2_STREAM_DATA_FLAG_FIN) != 0U}); +} +int stream_data_acked(ngtcp2_conn*, std::int64_t stream, std::uint64_t offset, + std::uint64_t size, void* user_data, void*) { + return emit(*static_cast(user_data), + {QuicEventKind::stream_data_acked, QuicEncryptionLevel::application, stream, + offset, size}); +} +int stream_reset(ngtcp2_conn*, std::int64_t stream, std::uint64_t, + std::uint64_t code, void* user_data, void*) { + return emit(*static_cast(user_data), + {QuicEventKind::stream_reset, QuicEncryptionLevel::application, stream, 0, 0, code}); +} +int stop_sending_received(ngtcp2_conn*, std::int64_t stream, std::uint64_t code, + void* user_data, void*) { + return emit(*static_cast(user_data), + {QuicEventKind::stop_sending, QuicEncryptionLevel::application, stream, 0, 0, code}); +} +int stream_closed(ngtcp2_conn*, std::uint32_t, std::int64_t stream, + std::uint64_t receive_code, std::uint64_t transmit_code, + void* user_data, void*) { + const std::uint64_t code = receive_code != 0U ? receive_code : transmit_code; + return emit(*static_cast(user_data), + {QuicEventKind::stream_closed, QuicEncryptionLevel::application, stream, 0, 0, code}); +} +void delete_aead(ngtcp2_conn*, ngtcp2_crypto_aead_ctx* native, void*) { + auto& context = *static_cast(native->native_handle); + if (context.destroy != nullptr) context.destroy(context.destroy_context, context.handle); +} +void delete_cipher(ngtcp2_conn*, ngtcp2_crypto_cipher_ctx* native, void*) { + auto& context = *static_cast(native->native_handle); + if (context.destroy != nullptr) context.destroy(context.destroy_context, context.handle); +} + +[[nodiscard]] ngtcp2_cid native_cid(const QuicConnectionId& value) noexcept { + ngtcp2_cid result{}; + ngtcp2_cid_init(&result, reinterpret_cast(value.value().data()), + value.value().size()); + return result; +} + +} // namespace + +core::Result QuicConnectionId::create( + core::ByteView value, core::WorkerId worker, core::GenerationId generation) noexcept { + if (value.empty() || value.size() > capacity) { + return std::unexpected{core_error(core::ErrorCode::invalid_range, + "QUIC connection ID must contain 1 to 20 bytes")}; + } + std::array bytes{}; + std::copy(value.span().begin(), value.span().end(), bytes.begin()); + return QuicConnectionId{bytes, static_cast(value.size()), worker, generation}; +} + +QuicSession::QuicSession(QuicSession&& other) noexcept { move_from(std::move(other)); } +QuicSession& QuicSession::operator=(QuicSession&& other) noexcept { + if (this != &other) { release(); move_from(std::move(other)); } + return *this; +} +QuicSession::~QuicSession() { release(); } + +core::Result QuicSession::create( + QuicRole role, const QuicConnectionId& destination, const QuicConnectionId& source, + NativeMemoryPool& memory, QuicPath initial_path, QuicLimits limits, + QuicCryptoCallbacks crypto, QuicEventSink events, + QuicConnectionIdSink connection_ids, DependencyLogSink log_sink) noexcept { + if ((role != QuicRole::client && role != QuicRole::server) || + destination.worker().value() != source.worker().value() || + destination.generation().value() != source.generation().value() || + limits.maximum_packet_bytes < NGTCP2_MAX_UDP_PAYLOAD_SIZE || + limits.maximum_packet_bytes > NGTCP2_MAX_TX_UDP_PAYLOAD_SIZE || + crypto.random_fill == nullptr || crypto.start == nullptr || + crypto.receive == nullptr || crypto.retry == nullptr || crypto.update == nullptr || + connection_ids.write == nullptr || !valid_endpoint(initial_path.local) || + !valid_endpoint(initial_path.remote)) { + return std::unexpected{core_error(core::ErrorCode::invalid_input, + "QUIC role, CID ownership, limits, or crypto callbacks are invalid")}; + } + internal::ArenaMemoryAccess::synchronize(memory); + auto& arena = internal::ArenaMemoryAccess::arena(memory); + if (internal::ArenaMemoryAccess::worker(memory).value() != source.worker().value()) { + return std::unexpected{core_error(core::ErrorCode::invalid_state, + "QUIC memory pool belongs to a different worker")}; + } + auto pin = arena.pin(source.worker()); + if (!pin.has_value()) return std::unexpected{pin.error()}; + internal::ArenaMemory allocator{&memory}; + void* const storage = internal::arena_malloc(sizeof(State), &allocator); + if (storage == nullptr) return std::unexpected{core_error( + core::ErrorCode::exhaustion, "QUIC session storage is exhausted")}; + auto* const state = ::new (storage) State{{&memory}, {}, {}, crypto, events, + connection_ids, log_sink, role, source.worker(), source.generation(), {}, + destination.value().size(), 0, + limits.maximum_packet_bytes, false, false, false}; + std::copy(destination.value().span().begin(), destination.value().span().end(), + state->initial_dcid.begin()); + if (!store_path(initial_path, state->path)) { + state->~State(); + internal::arena_free(storage, &allocator); + return std::unexpected{core_error(core::ErrorCode::invalid_input, + "QUIC initial path is invalid")}; + } + + ngtcp2_callbacks callbacks{}; + callbacks.client_initial = client_initial; + callbacks.recv_client_initial = recv_client_initial; + callbacks.recv_crypto_data = recv_crypto; + callbacks.handshake_completed = handshake_completed; + callbacks.encrypt = encrypt; + callbacks.decrypt = decrypt; + callbacks.hp_mask = header_mask; + callbacks.recv_stream_data = stream_data; + callbacks.acked_stream_data_offset = stream_data_acked; + callbacks.stream_open = stream_opened; + callbacks.stream_reset = stream_reset; + callbacks.recv_stop_sending = stop_sending_received; + callbacks.stream_close2 = stream_closed; + callbacks.rand = random_fill; + callbacks.recv_retry = recv_retry; + callbacks.update_key = update_key; + callbacks.version_negotiation = version_negotiation; + callbacks.get_new_connection_id2 = new_connection_id; + callbacks.get_path_challenge_data2 = path_challenge; + callbacks.delete_crypto_aead_ctx = delete_aead; + callbacks.delete_crypto_cipher_ctx = delete_cipher; + + ngtcp2_settings settings{}; + ngtcp2_settings_default(&settings); + settings.initial_ts = 0; + settings.rand_ctx.native_handle = state; + settings.max_tx_udp_payload_size = limits.maximum_packet_bytes; + ngtcp2_transport_params params{}; + ngtcp2_transport_params_default(¶ms); + params.initial_max_data = limits.initial_max_data; + params.initial_max_stream_data_bidi_local = limits.initial_max_stream_data; + params.initial_max_stream_data_bidi_remote = limits.initial_max_stream_data; + params.initial_max_stream_data_uni = limits.initial_max_stream_data; + params.initial_max_streams_bidi = limits.initial_max_streams_bidi; + params.initial_max_streams_uni = limits.initial_max_streams_uni; + + const ngtcp2_cid dcid = native_cid(destination); + const ngtcp2_cid scid = native_cid(source); + if (role == QuicRole::server) { + params.original_dcid = dcid; + params.original_dcid_present = 1; + } + state->native_memory = {&state->memory, internal::arena_malloc, internal::arena_free, + internal::arena_calloc, internal::arena_realloc}; + ngtcp2_conn* connection{}; + const int result = role == QuicRole::client + ? ngtcp2_conn_client_new(&connection, &dcid, &scid, &state->path.path, NGTCP2_PROTO_VER_V1, + &callbacks, &settings, ¶ms, &state->native_memory, state) + : ngtcp2_conn_server_new(&connection, &dcid, &scid, &state->path.path, NGTCP2_PROTO_VER_V1, + &callbacks, &settings, ¶ms, &state->native_memory, state); + if (result != 0) { + state->~State(); + internal::arena_free(storage, &allocator); + return std::unexpected{native_error(core::DependencyOperation::quic_session, + result, log_sink)}; + } + if (const auto random = require_random(*state); !random.has_value()) { + ngtcp2_conn_del(connection); + state->~State(); + internal::arena_free(storage, &allocator); + return std::unexpected{random.error()}; + } + return QuicSession{connection, state, arena, arena.generation(), std::move(*pin)}; +} + +core::Result QuicSession::require_valid() const noexcept { + if (connection_ == nullptr || state_ == nullptr || arena_ == nullptr || + arena_->generation() != generation_ || static_cast(state_)->terminal) { + return std::unexpected{core_error(core::ErrorCode::invalid_state, + "QUIC session is inactive or its arena was reset")}; + } + return {}; +} + +core::Result QuicSession::receive_packet(QuicPath path, core::ByteView packet, + std::uint64_t now_ns) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) return valid; + auto& state = *static_cast(state_); + ngtcp2_path_storage input{}; + if (!store_path(path, input)) return std::unexpected{core_error( + core::ErrorCode::invalid_input, "QUIC packet path is invalid")}; + const int result = ngtcp2_conn_read_pkt(static_cast(connection_), &input.path, nullptr, + reinterpret_cast(packet.data()), packet.size(), now_ns); + if (result < 0 && terminal_error(result)) state.terminal = true; + if (const auto random = require_random(state); !random.has_value()) return random; + if (result != 0) return std::unexpected{native_error(core::DependencyOperation::quic_receive, + result, static_cast(state_)->log)}; + return {}; +} + +core::Result QuicSession::write_packet( + core::MutableByteView output, std::int64_t stream_id, core::ByteView stream_data, + bool fin, std::uint64_t now_ns) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) return std::unexpected{valid.error()}; + auto& state = *static_cast(state_); + if (state.packet_pending_transmit) { + return std::unexpected{core_error(core::ErrorCode::invalid_state, + "QUIC packet transmission is still pending")}; + } + const auto required = state.maximum_packet_bytes; + if (output.size() < required) { + return std::unexpected{core_error(core::ErrorCode::invalid_range, + "QUIC output is smaller than the configured packet buffer")}; + } + ngtcp2_ssize consumed{-1}; + const ngtcp2_vec vector{reinterpret_cast(const_cast(stream_data.data())), + stream_data.size()}; + const std::uint32_t flags = fin ? NGTCP2_WRITE_STREAM_FLAG_FIN : NGTCP2_WRITE_STREAM_FLAG_NONE; + ngtcp2_path_storage output_path{}; + ngtcp2_path_storage_zero(&output_path); + const auto result = ngtcp2_conn_writev_stream(static_cast(connection_), + &output_path.path, + nullptr, reinterpret_cast(output.data()), output.size(), &consumed, + flags, stream_id, stream_data.empty() ? nullptr : &vector, + stream_data.empty() ? 0U : 1U, now_ns); + if (result < 0 && terminal_error(static_cast(result))) state.terminal = true; + if (const auto random = require_random(state); !random.has_value()) { + return std::unexpected{random.error()}; + } + if (result < 0) return std::unexpected{native_error(core::DependencyOperation::quic_send, + static_cast(result), static_cast(state_)->log)}; + if (result > 0) state.packet_pending_transmit = true; + return QuicPacketWrite{static_cast(result), + consumed < 0 ? 0U : static_cast(consumed), + result > 0 ? path_from(output_path.path) : QuicPath{}}; +} + +core::Result QuicSession::packet_transmitted(std::uint64_t now_ns) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) return valid; + auto& state = *static_cast(state_); + if (!state.packet_pending_transmit) { + return std::unexpected{core_error(core::ErrorCode::invalid_state, + "QUIC has no generated packet awaiting transmission")}; + } + ngtcp2_conn_update_pkt_tx_time(static_cast(connection_), now_ns); + state.packet_pending_transmit = false; + return {}; +} + +core::Result QuicSession::open_bidirectional_stream() noexcept { + if (const auto valid = require_valid(); !valid.has_value()) return std::unexpected{valid.error()}; + std::int64_t stream{}; + const int result = ngtcp2_conn_open_bidi_stream(static_cast(connection_), &stream, + nullptr); + if (const auto random = require_random(*static_cast(state_)); !random.has_value()) { + return std::unexpected{random.error()}; + } + if (result != 0) return std::unexpected{native_error(core::DependencyOperation::quic_stream, + result, static_cast(state_)->log)}; + return stream; +} + +core::Result QuicSession::open_unidirectional_stream() noexcept { + if (const auto valid = require_valid(); !valid.has_value()) return std::unexpected{valid.error()}; + std::int64_t stream{}; + const int result = ngtcp2_conn_open_uni_stream(static_cast(connection_), &stream, + nullptr); + if (const auto random = require_random(*static_cast(state_)); !random.has_value()) { + return std::unexpected{random.error()}; + } + if (result != 0) return std::unexpected{native_error(core::DependencyOperation::quic_stream, + result, static_cast(state_)->log)}; + return stream; +} + +core::Result QuicSession::reset_stream(std::int64_t stream, std::uint64_t code) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) return valid; + const int result = ngtcp2_conn_shutdown_stream_write(static_cast(connection_), + 0, stream, code); + if (result != 0) return std::unexpected{native_error(core::DependencyOperation::quic_stream, + result, static_cast(state_)->log)}; + return {}; +} + +core::Result QuicSession::stop_sending(std::int64_t stream, std::uint64_t code) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) return valid; + const int result = ngtcp2_conn_shutdown_stream_read(static_cast(connection_), + 0, stream, code); + if (result != 0) return std::unexpected{native_error(core::DependencyOperation::quic_stream, + result, static_cast(state_)->log)}; + return {}; +} + +std::uint64_t QuicSession::expiry_ns() const noexcept { + return connection_ == nullptr || arena_ == nullptr || arena_->generation() != generation_ + ? 0 : ngtcp2_conn_get_expiry2(static_cast(connection_)); +} +core::Result QuicSession::handle_expiry(std::uint64_t now_ns) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) return valid; + const int result = ngtcp2_conn_handle_expiry(static_cast(connection_), now_ns); + auto& state = *static_cast(state_); + if (result == NGTCP2_ERR_IDLE_CLOSE || (result < 0 && terminal_error(result))) { + state.terminal = true; + } + if (const auto random = require_random(state); !random.has_value()) { + return random; + } + if (result != 0) return std::unexpected{native_error(core::DependencyOperation::quic_expiry, + result, static_cast(state_)->log)}; + return {}; +} +void QuicSession::release() noexcept { + if (connection_ != nullptr && arena_ != nullptr && arena_->generation() == generation_) { + ngtcp2_conn_del(static_cast(connection_)); + auto* const state = static_cast(state_); + internal::ArenaMemory allocator{state->memory.pool}; + state->~State(); + internal::arena_free(state, &allocator); + } + connection_ = nullptr; state_ = nullptr; arena_ = nullptr; generation_ = 0; + pin_ = {}; +} +void QuicSession::move_from(QuicSession&& other) noexcept { + connection_ = std::exchange(other.connection_, nullptr); + state_ = std::exchange(other.state_, nullptr); + arena_ = std::exchange(other.arena_, nullptr); + generation_ = std::exchange(other.generation_, 0); + pin_ = std::move(other.pin_); +} + +} // namespace laghu::adapters diff --git a/src/core/contract/laghu/core/bounded_arena.hpp b/src/core/contract/laghu/core/bounded_arena.hpp index 15df952..e67c1b5 100644 --- a/src/core/contract/laghu/core/bounded_arena.hpp +++ b/src/core/contract/laghu/core/bounded_arena.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -15,6 +16,22 @@ namespace laghu::core { class BoundedArena; +class ArenaPin final { + public: + constexpr ArenaPin() noexcept = default; + ArenaPin(const ArenaPin&) = delete; + ArenaPin& operator=(const ArenaPin&) = delete; + ArenaPin(ArenaPin&& other) noexcept; + ArenaPin& operator=(ArenaPin&& other) noexcept; + ~ArenaPin(); + + private: + friend class BoundedArena; + constexpr explicit ArenaPin(BoundedArena& arena) noexcept : arena_(&arena) {} + void release() noexcept; + BoundedArena* arena_{}; +}; + class ArenaView final { public: constexpr ArenaView() noexcept = default; @@ -22,6 +39,7 @@ class ArenaView final { [[nodiscard]] constexpr std::size_t size() const noexcept { return bytes_.size(); } [[nodiscard]] constexpr bool empty() const noexcept { return bytes_.empty(); } [[nodiscard]] constexpr std::uint64_t generation() const noexcept { return generation_; } + [[nodiscard]] Result bytes() const noexcept; private: @@ -77,7 +95,7 @@ class BoundedArena final { BoundedArena& operator=(BoundedArena&&) = delete; ~BoundedArena() { - if (reservation_.is_active()) { + if (reservation_.is_active() || live_pins_ != 0U) { std::terminate(); } } @@ -88,6 +106,18 @@ class BoundedArena final { [[nodiscard]] constexpr std::size_t used() const noexcept { return offset_; } [[nodiscard]] constexpr std::uint64_t generation() const noexcept { return generation_; } + [[nodiscard]] Result pin(WorkerId worker) noexcept { + if (const auto owner = require_worker(worker); !owner.has_value()) { + return std::unexpected{owner.error()}; + } + if (live_pins_ == std::numeric_limits::max()) { + return std::unexpected{Error{ErrorDomain::core, ErrorCode::exhaustion, 0, + "bounded arena pin capacity is exhausted"}}; + } + ++live_pins_; + return ArenaPin{*this}; + } + [[nodiscard]] Result try_allocate(WorkerId worker, std::size_t bytes, std::size_t alignment) noexcept { if (const auto owner = require_worker(worker); !owner.has_value()) { @@ -142,6 +172,10 @@ class BoundedArena final { return std::unexpected{Error{ErrorDomain::core, ErrorCode::invalid_state, 0, "bounded arena reset requires its current quiescent boundary"}}; } + if (live_pins_ != 0U) { + return std::unexpected{Error{ErrorDomain::core, ErrorCode::invalid_state, 0, + "bounded arena reset requires all live pins to be released"}}; + } if (generation_ == std::numeric_limits::max()) { return std::unexpected{Error{ErrorDomain::core, ErrorCode::overflow, 0, "bounded arena generation is exhausted"}}; @@ -165,6 +199,12 @@ class BoundedArena final { private: friend class ArenaView; friend class ArenaPmrResource; + friend class ArenaPin; + + constexpr void release_pin() noexcept { + if (live_pins_ == 0U) std::terminate(); + --live_pins_; + } [[nodiscard]] static constexpr bool is_valid_alignment(std::size_t alignment) noexcept { return alignment != 0 && (alignment & (alignment - 1)) == 0; @@ -248,9 +288,25 @@ class BoundedArena final { std::size_t maximum_capacity_{}; std::size_t capacity_{}; std::size_t offset_{}; + std::size_t live_pins_{}; std::uint64_t generation_{1}; }; +inline ArenaPin::ArenaPin(ArenaPin&& other) noexcept + : arena_(std::exchange(other.arena_, nullptr)) {} +inline ArenaPin& ArenaPin::operator=(ArenaPin&& other) noexcept { + if (this != &other) { + release(); + arena_ = std::exchange(other.arena_, nullptr); + } + return *this; +} +inline ArenaPin::~ArenaPin() { release(); } +inline void ArenaPin::release() noexcept { + if (arena_ != nullptr) arena_->release_pin(); + arena_ = nullptr; +} + class ArenaPmrResource final : public std::pmr::memory_resource { public: constexpr ArenaPmrResource(BoundedArena& arena, WorkerId worker) noexcept diff --git a/src/core/contract/laghu/core/contract.hpp b/src/core/contract/laghu/core/contract.hpp index bf38a9d..070f141 100644 --- a/src/core/contract/laghu/core/contract.hpp +++ b/src/core/contract/laghu/core/contract.hpp @@ -118,6 +118,15 @@ enum class DependencyOperation : std::uint8_t { http2_receive, http2_send, http2_submit, + quic_session, + quic_receive, + quic_send, + quic_stream, + quic_expiry, + http3_session, + http3_receive, + http3_send, + http3_submit, }; class Error final { diff --git a/tests/adapters/http3.cpp b/tests/adapters/http3.cpp new file mode 100644 index 0000000..ffea9ed --- /dev/null +++ b/tests/adapters/http3.cpp @@ -0,0 +1,430 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#include +#include +#include +#include +#include +#include +#include + +#include "laghu_test_support.hpp" + +#include +#include +#include + +namespace { + +using namespace laghu; + +struct FixedSource final { + alignas(std::max_align_t) std::array storage{}; + bool fail{}; + static core::Result acquire(void* context, + std::size_t minimum) noexcept { + auto& self = *static_cast(context); + if (self.fail || minimum > self.storage.size()) { + return std::unexpected{core::Error{core::ErrorDomain::core, + core::ErrorCode::exhaustion, 0, "HTTP/3 test storage exhausted"}}; + } + return core::MutableByteView::from(self.storage); + } + static core::Result reset(void*) noexcept { return {}; } +}; + +[[nodiscard]] core::ByteView bytes(std::string_view value) noexcept { + return *core::ByteView::from({reinterpret_cast(value.data()), value.size()}); +} + +[[nodiscard]] bool fill_random(void*, core::MutableByteView output) noexcept { + std::uint8_t value{1}; + for (std::byte& byte : output.span()) byte = static_cast(value++); + return true; +} + +[[nodiscard]] bool fail_random(void*, core::MutableByteView) noexcept { return false; } + +[[nodiscard]] bool crypto_start(void*, adapters::QuicRole, core::ByteView, + const adapters::QuicCryptoActions&) noexcept { return true; } +[[nodiscard]] bool crypto_receive(void*, adapters::QuicEncryptionLevel, std::uint64_t, + core::ByteView, const adapters::QuicCryptoActions&) noexcept { return true; } +[[nodiscard]] bool crypto_retry(void*, const adapters::QuicCryptoActions&) noexcept { + return true; +} +[[nodiscard]] bool crypto_update(void*, core::MutableByteView, core::MutableByteView, + core::MutableByteView, core::MutableByteView, core::ByteView, core::ByteView, + adapters::QuicKeyContext*&, adapters::QuicKeyContext*&) noexcept { return false; } + +[[nodiscard]] adapters::QuicCryptoCallbacks crypto_callbacks() noexcept { + return {nullptr, fill_random, crypto_start, crypto_receive, crypto_retry, crypto_update}; +} + +[[nodiscard]] adapters::QuicPath loopback_path(std::uint16_t local, + std::uint16_t remote) noexcept { + adapters::QuicEndpoint local_endpoint{}; + local_endpoint.port = local; + local_endpoint.address[0] = std::byte{127}; + local_endpoint.address[3] = std::byte{1}; + adapters::QuicEndpoint remote_endpoint{}; + remote_endpoint.port = remote; + remote_endpoint.address[0] = std::byte{127}; + remote_endpoint.address[3] = std::byte{1}; + return {local_endpoint, remote_endpoint}; +} + +[[nodiscard]] bool accept_connection_id(void*, + const adapters::QuicConnectionId&) noexcept { return true; } + +[[nodiscard]] adapters::QuicConnectionIdSink connection_id_sink() noexcept { + return {nullptr, accept_connection_id}; +} + +struct DeterministicCrypto final { + static constexpr std::size_t tag_size = 16; + std::array iv{}; + adapters::QuicKeyContext aead_context{}; + adapters::QuicKeyContext header_context{}; + adapters::QuicAeadAlgorithm aead{nullptr, tag_size, encrypt, decrypt}; + adapters::QuicHeaderAlgorithm header{nullptr, protect_header}; + std::size_t received_crypto_bytes{}; + + [[nodiscard]] adapters::QuicPacketKey key() noexcept { + return {&aead, &aead_context, &header, &header_context, + *core::ByteView::from(iv), {}}; + } + + static bool encrypt(void*, void*, core::MutableByteView output, + core::ByteView input, core::ByteView, + core::ByteView) noexcept { + if (output.size() != input.size() + tag_size) return false; + std::copy(input.span().begin(), input.span().end(), output.span().begin()); + std::fill(output.span().begin() + static_cast(input.size()), + output.span().end(), std::byte{0xa5}); + return true; + } + + static bool decrypt(void*, void*, core::MutableByteView output, + core::ByteView input, core::ByteView, + core::ByteView) noexcept { + if (input.size() != output.size() + tag_size) return false; + const auto tag = input.span().last(tag_size); + if (!std::all_of(tag.begin(), tag.end(), + [](std::byte value) { return value == std::byte{0xa5}; })) return false; + std::copy_n(input.span().begin(), output.size(), output.span().begin()); + return true; + } + + static bool protect_header(void*, void*, core::MutableByteView output, + core::ByteView sample) noexcept { + if (output.size() != 5U || sample.size() != 16U) return false; + std::fill(output.span().begin(), output.span().end(), std::byte{}); + return true; + } + + static bool start(void* context, adapters::QuicRole role, core::ByteView, + const adapters::QuicCryptoActions& actions) noexcept { + auto& self = *static_cast(context); + const auto key = self.key(); + if (!actions.install_initial(actions.context, key, key).has_value()) return false; + return role != adapters::QuicRole::client || + actions.submit(actions.context, adapters::QuicEncryptionLevel::initial, + bytes("deterministic client hello")).has_value(); + } + + static bool receive(void* context, adapters::QuicEncryptionLevel, + std::uint64_t, core::ByteView input, + const adapters::QuicCryptoActions&) noexcept { + static_cast(context)->received_crypto_bytes += input.size(); + return true; + } + + [[nodiscard]] adapters::QuicCryptoCallbacks callbacks() noexcept { + return {this, fill_random, start, receive, crypto_retry, crypto_update}; + } +}; + +[[nodiscard]] bool reset_arena(core::BoundedArena& arena, + core::WorkerId worker) noexcept { + const auto boundary = arena.quiescent_boundary(worker); + return boundary.has_value() && arena.reset(worker, *boundary).has_value(); +} + +bool contracts_construct_and_remain_separate() noexcept { + auto worker = *core::WorkerId::from_uint64(1); + auto generation = *core::GenerationId::from_uint64(2); + core::MemoryBudget budget{worker, 1024U * 1024U}; + FixedSource source{}; + core::BoundedArena arena{worker, budget, + {&source, FixedSource::acquire, FixedSource::reset}, 512U * 1024U, 512U * 1024U}; + adapters::NativeMemoryPool memory{worker, arena}; + const bool passed = [&]() noexcept { + auto destination = adapters::QuicConnectionId::create(bytes("destination-id"), worker, generation); + auto source_id = adapters::QuicConnectionId::create(bytes("source-id"), worker, generation); + if (!destination.has_value() || !source_id.has_value()) return false; + auto quic = adapters::QuicSession::create(adapters::QuicRole::client, *destination, + *source_id, memory, loopback_path(4433, 4434), + {65536, 16384, 8, 8, 1500}, crypto_callbacks(), {}, connection_id_sink()); + if (!quic.has_value()) return false; + auto h3 = adapters::Http3Session::create(adapters::Http3Role::client, memory, + {16384, 0, 0}); + return h3.has_value() && destination->worker().value() == 1 && + destination->generation().value() == 2 && + destination->value().size() == bytes("destination-id").size(); + }(); + return reset_arena(arena, worker) && passed; +} + +bool invalid_metadata_and_allocation_fail() noexcept { + auto worker = *core::WorkerId::from_uint64(1); + auto other = *core::WorkerId::from_uint64(2); + auto generation = *core::GenerationId::from_uint64(3); + const auto oversized = adapters::QuicConnectionId::create( + bytes("123456789012345678901"), worker, generation); + core::MemoryBudget budget{worker, 1024}; + FixedSource source{}; + source.fail = true; + core::BoundedArena arena{worker, budget, + {&source, FixedSource::acquire, FixedSource::reset}, 1024, 1024}; + adapters::NativeMemoryPool memory{worker, arena}; + const auto h3 = adapters::Http3Session::create(adapters::Http3Role::client, memory, + {1024, 0, 0}); + const auto first = adapters::QuicConnectionId::create(bytes("12345678"), worker, generation); + const auto same_worker = adapters::QuicConnectionId::create(bytes("abcdefgh"), worker, generation); + const auto second = adapters::QuicConnectionId::create(bytes("abcdefgh"), other, generation); + if (!first.has_value() || !same_worker.has_value()) return false; + const auto excessive_packet = adapters::QuicSession::create(adapters::QuicRole::client, + *first, *same_worker, memory, loopback_path(4433, 4434), + {65536, 16384, 8, 8, std::numeric_limits::max()}, + crypto_callbacks(), {}, connection_id_sink()); + return !oversized.has_value() && !h3.has_value() && second.has_value() && + !excessive_packet.has_value() && + excessive_packet.error().code() == core::ErrorCode::invalid_input; +} + +bool constructor_random_failure_is_rejected() noexcept { + auto worker = *core::WorkerId::from_uint64(8); + auto generation = *core::GenerationId::from_uint64(9); + core::MemoryBudget budget{worker, 512U * 1024U}; + FixedSource source{}; + core::BoundedArena arena{worker, budget, + {&source, FixedSource::acquire, FixedSource::reset}, 512U * 1024U, 512U * 1024U}; + adapters::NativeMemoryPool memory{worker, arena}; + const auto destination = *adapters::QuicConnectionId::create( + bytes("destination-id"), worker, generation); + const auto source_id = *adapters::QuicConnectionId::create( + bytes("source-id"), worker, generation); + auto crypto = crypto_callbacks(); + crypto.random_fill = fail_random; + const auto session = adapters::QuicSession::create(adapters::QuicRole::client, + destination, source_id, memory, loopback_path(4433, 4434), + {65536, 16384, 8, 8, 1500}, crypto, {}, connection_id_sink()); + return !session.has_value() && session.error().code() == core::ErrorCode::crypto && + reset_arena(arena, worker); +} + +bool malformed_inputs_are_typed() noexcept { + auto worker = *core::WorkerId::from_uint64(1); + auto generation = *core::GenerationId::from_uint64(2); + core::MemoryBudget budget{worker, 1024U * 1024U}; + FixedSource source{}; + core::BoundedArena arena{worker, budget, + {&source, FixedSource::acquire, FixedSource::reset}, 512U * 1024U, 512U * 1024U}; + adapters::NativeMemoryPool memory{worker, arena}; + const bool passed = [&]() noexcept { + auto destination = *adapters::QuicConnectionId::create(bytes("destination-id"), worker, generation); + auto source_id = *adapters::QuicConnectionId::create(bytes("source-id"), worker, generation); + auto quic = adapters::QuicSession::create(adapters::QuicRole::server, destination, source_id, + memory, loopback_path(4434, 4433), {65536, 16384, 8, 8, 1500}, + crypto_callbacks(), {}, connection_id_sink()); + if (!quic.has_value()) return false; + const auto packet = quic->receive_packet(loopback_path(4434, 4433), + bytes("not-a-quic-packet"), 1); + auto h3 = adapters::Http3Session::create(adapters::Http3Role::server, memory, + {16384, 0, 0}); + if (!h3.has_value()) return false; + const auto frame = h3->receive(0, bytes("malformed"), true); + return !packet.has_value() && !frame.has_value() && !h3->next_output().has_value(); + }(); + return reset_arena(arena, worker) && passed; +} + +bool arena_reset_requires_session_cleanup() noexcept { + auto worker = *core::WorkerId::from_uint64(1); + auto generation = *core::GenerationId::from_uint64(2); + core::MemoryBudget budget{worker, 1024U * 1024U}; + FixedSource source{}; + core::BoundedArena arena{worker, budget, + {&source, FixedSource::acquire, FixedSource::reset}, 512U * 1024U, 512U * 1024U}; + adapters::NativeMemoryPool memory{worker, arena}; + auto destination = *adapters::QuicConnectionId::create( + bytes("destination-id"), worker, generation); + auto source_id = *adapters::QuicConnectionId::create(bytes("source-id"), worker, generation); + { + auto quic = adapters::QuicSession::create(adapters::QuicRole::client, destination, + source_id, memory, loopback_path(4433, 4434), + {65536, 16384, 8, 8, 1500}, crypto_callbacks(), {}, connection_id_sink()); + auto h3 = adapters::Http3Session::create(adapters::Http3Role::client, memory, + {16384, 0, 0}); + if (!quic.has_value() || !h3.has_value() || reset_arena(arena, worker)) return false; + std::array packet{}; + const auto output = *core::MutableByteView::from(packet); + if (quic->write_packet(output, -1, {}, false, 1).has_value() || + quic->packet_transmitted(1).has_value()) return false; + } + return reset_arena(arena, worker); +} + +bool native_memory_reuses_freed_blocks() noexcept { + auto worker = *core::WorkerId::from_uint64(7); + core::MemoryBudget budget{worker, 64U * 1024U}; + FixedSource source{}; + core::BoundedArena arena{worker, budget, + {&source, FixedSource::acquire, FixedSource::reset}, 64U * 1024U, 64U * 1024U}; + adapters::NativeMemoryPool memory{worker, arena}; + std::size_t settled_usage{}; + for (std::size_t iteration = 0; iteration < 32U; ++iteration) { + { + auto session = adapters::Http3Session::create( + adapters::Http3Role::client, memory, {16384, 0, 0}); + if (!session.has_value()) return false; + } + if (iteration == 0U) { + settled_usage = arena.used(); + } else if (arena.used() != settled_usage) { + return false; + } + } + return reset_arena(arena, worker); +} + +bool encrypted_quic_packet_loopback() noexcept { + auto worker = *core::WorkerId::from_uint64(5); + auto generation = *core::GenerationId::from_uint64(6); + FixedSource client_source{}; + FixedSource server_source{}; + core::MemoryBudget client_budget{worker, client_source.storage.size()}; + core::MemoryBudget server_budget{worker, server_source.storage.size()}; + core::BoundedArena client_arena{worker, client_budget, + {&client_source, FixedSource::acquire, FixedSource::reset}, + client_source.storage.size(), client_source.storage.size()}; + core::BoundedArena server_arena{worker, server_budget, + {&server_source, FixedSource::acquire, FixedSource::reset}, + server_source.storage.size(), server_source.storage.size()}; + adapters::NativeMemoryPool client_memory{worker, client_arena}; + adapters::NativeMemoryPool server_memory{worker, server_arena}; + DeterministicCrypto client_crypto{}; + DeterministicCrypto server_crypto{}; + const bool passed = [&]() noexcept { + const auto server_id = *adapters::QuicConnectionId::create( + bytes("server01"), worker, generation); + const auto client_id = *adapters::QuicConnectionId::create( + bytes("client01"), worker, generation); + auto client = adapters::QuicSession::create(adapters::QuicRole::client, + server_id, client_id, client_memory, loopback_path(4433, 4434), + {65536, 16384, 8, 8, 1500}, client_crypto.callbacks(), {}, + connection_id_sink()); + auto server = adapters::QuicSession::create(adapters::QuicRole::server, + server_id, server_id, server_memory, loopback_path(4434, 4433), + {65536, 16384, 8, 8, 1500}, server_crypto.callbacks(), {}, + connection_id_sink()); + if (!client.has_value() || !server.has_value()) return false; + std::array packet{}; + const auto output = *core::MutableByteView::from(packet); + const auto written = client->write_packet(output, -1, {}, false, 1); + if (!written.has_value() || written->packet_bytes == 0U || + written->path.local.port != 4433 || written->path.remote.port != 4434) return false; + const auto duplicate = client->write_packet(output, -1, {}, false, 1); + if (duplicate.has_value() || duplicate.error().code() != core::ErrorCode::invalid_state || + !client->packet_transmitted(1).has_value()) return false; + const auto wire = *core::ByteView::from( + std::span{packet}.first(written->packet_bytes)); + if (!server->receive_packet(loopback_path(4434, 4433), wire, 2).has_value() || + server_crypto.received_crypto_bytes == 0U) return false; + std::fill(packet.begin(), packet.end(), std::byte{}); + const auto acknowledgement = server->write_packet(output, -1, {}, false, 3); + if (!acknowledgement.has_value() || acknowledgement->packet_bytes == 0U || + !server->packet_transmitted(3).has_value()) return false; + const auto acknowledgement_wire = *core::ByteView::from( + std::span{packet}.first(acknowledgement->packet_bytes)); + return client->receive_packet(loopback_path(4433, 4434), + acknowledgement_wire, 4).has_value(); + }(); + return reset_arena(client_arena, worker) && reset_arena(server_arena, worker) && passed; +} + +struct HeaderEvents final { + std::size_t headers{}; + static bool write(void* context, const adapters::Http3Event& event) noexcept { + auto& self = *static_cast(context); + if (event.kind == adapters::Http3EventKind::header) ++self.headers; + return true; + } +}; + +bool h3_stream_smoke_flow() noexcept { + auto worker = *core::WorkerId::from_uint64(4); + FixedSource client_source{}; + FixedSource server_source{}; + core::MemoryBudget client_budget{worker, client_source.storage.size()}; + core::MemoryBudget server_budget{worker, server_source.storage.size()}; + core::BoundedArena client_arena{worker, client_budget, + {&client_source, FixedSource::acquire, FixedSource::reset}, 512U * 1024U, 512U * 1024U}; + core::BoundedArena server_arena{worker, server_budget, + {&server_source, FixedSource::acquire, FixedSource::reset}, 512U * 1024U, 512U * 1024U}; + adapters::NativeMemoryPool client_memory{worker, client_arena}; + adapters::NativeMemoryPool server_memory{worker, server_arena}; + const bool passed = [&]() noexcept { + HeaderEvents events{}; + auto client = adapters::Http3Session::create(adapters::Http3Role::client, client_memory, + {16384, 0, 0}); + auto server = adapters::Http3Session::create(adapters::Http3Role::server, server_memory, + {16384, 0, 0}, {&events, HeaderEvents::write}); + if (!client.has_value() || !server.has_value() || + !client->bind_streams(2, 6, 10).has_value() || + !server->bind_streams(3, 7, 11).has_value()) return false; + std::array oversized_value{}; + const std::array oversized_headers{adapters::Http3Header{ + bytes(":path"), *core::ByteView::from(oversized_value)}}; + if (client->submit_request(0, oversized_headers).has_value() || + server->submit_response(0, oversized_headers).has_value()) return false; + const std::array headers{ + adapters::Http3Header{bytes(":method"), bytes("GET")}, + adapters::Http3Header{bytes(":scheme"), bytes("https")}, + adapters::Http3Header{bytes(":authority"), bytes("example.test")}, + adapters::Http3Header{bytes(":path"), bytes("/")}, + }; + if (!client->submit_request(0, headers).has_value()) return false; + for (std::size_t attempt = 0; attempt < 16 && events.headers < headers.size(); ++attempt) { + const auto output = client->next_output(); + if (!output.has_value()) return false; + if (output->stream_id < 0) continue; + const auto consumed = server->receive(output->stream_id, output->bytes, output->fin); + if (!consumed.has_value() || *consumed > output->bytes.size() || + !client->mark_output_written(output->stream_id, output->bytes.size()).has_value() || + !client->acknowledge_stream_data( + output->stream_id, output->bytes.size()).has_value()) { + return false; + } + } + return events.headers == headers.size(); + }(); + return reset_arena(client_arena, worker) && reset_arena(server_arena, worker) && passed; +} + +} // namespace + +static_assert(!std::is_aggregate_v); + +int main() { + constexpr std::array tests{ + laghu::test::TestCase{"adapters.http3.separate_contracts", contracts_construct_and_remain_separate}, + laghu::test::TestCase{"adapters.http3.cid_and_allocation", invalid_metadata_and_allocation_fail}, + laghu::test::TestCase{"adapters.http3.constructor_rng_failure", constructor_random_failure_is_rejected}, + laghu::test::TestCase{"adapters.http3.malformed_input", malformed_inputs_are_typed}, + laghu::test::TestCase{"adapters.http3.arena_reset", arena_reset_requires_session_cleanup}, + laghu::test::TestCase{"adapters.http3.native_memory_reuse", native_memory_reuses_freed_blocks}, + laghu::test::TestCase{"adapters.http3.encrypted_quic_loopback", encrypted_quic_packet_loopback}, + laghu::test::TestCase{"adapters.http3.stream_smoke", h3_stream_smoke_flow}, + }; + return laghu::test::run_tests(tests); +} diff --git a/tests/adapters/http3_contract.cpp b/tests/adapters/http3_contract.cpp new file mode 100644 index 0000000..3e15f6f --- /dev/null +++ b/tests/adapters/http3_contract.cpp @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#include + +#include +#include + +static_assert(!std::is_copy_constructible_v); +static_assert(!std::is_copy_constructible_v); +static_assert(std::is_trivially_copyable_v); +static_assert(std::is_trivially_copyable_v); +static_assert(std::is_member_function_pointer_v< + decltype(&laghu::adapters::QuicSession::open_unidirectional_stream)>); +static_assert(std::is_member_function_pointer_v< + decltype(&laghu::adapters::QuicSession::packet_transmitted)>); +static_assert(std::is_member_function_pointer_v< + decltype(&laghu::adapters::Http3Session::mark_output_written)>); +static_assert(std::is_member_function_pointer_v< + decltype(&laghu::adapters::Http3Session::acknowledge_stream_data)>); + +int main() { return 0; } diff --git a/tests/core/bounded_arena.cpp b/tests/core/bounded_arena.cpp index 6b70d4a..bc9814c 100644 --- a/tests/core/bounded_arena.cpp +++ b/tests/core/bounded_arena.cpp @@ -195,6 +195,21 @@ std::size_t DestructionProbe::destructions{}; return check(arena.reset(worker, *boundary).has_value() && *budget.charged(worker) == 0); } +[[nodiscard]] bool check_pin_blocks_reset_without_storage(WorkerId worker) noexcept { + FixedBlockSource source{}; + MemoryBudget budget{worker, 8}; + BoundedArena arena{worker, budget, fixed_source(source), 8, 8}; + const auto boundary = arena.quiescent_boundary(worker); + { + auto pin = arena.pin(worker); + if (!check(boundary.has_value() && pin.has_value())) return false; + const auto reset = arena.reset(worker, *boundary); + if (!check(!reset.has_value() && reset.error().code() == ErrorCode::invalid_state && + source.reset_calls == 0)) return false; + } + return check(arena.reset(worker, *boundary).has_value() && source.reset_calls == 1); +} + [[nodiscard]] bool check_pmr_success(WorkerId worker) noexcept { FixedBlockSource source{}; MemoryBudget budget{worker, 32}; @@ -274,12 +289,15 @@ int main() { if (!check(check_reset_failures_preserve_state(*worker))) { return 5; } - if (!check(check_pmr_success(*worker))) { + if (!check(check_pin_blocks_reset_without_storage(*worker))) { return 6; } + if (!check(check_pmr_success(*worker))) { + return 7; + } if (!check(child_terminates_for_pmr_exhaustion(*worker) && child_terminates_for_pmr_misuse(*worker))) { - return 7; + return 8; } - return allocation_attempts == allocation_attempts_before ? 0 : 8; + return allocation_attempts == allocation_attempts_before ? 0 : 9; }