QBONE Server Simulator: Implement open-source BasicQuicServer Fills a similar role as quic::QuicServer and quic::test::QuicTestServer but with even lower-level control and less assumptions that it is being used with HTTP. Also, because this is only for test/experimentation purposes, directly integrates with threading functionality similar to quic::test::ServerThread. This class is not necessarily QBONE-specific if somebody wants to do the work of better aligning it with the existing test QUIC server classes. PiperOrigin-RevId: 929451174
diff --git a/build/source_list.bzl b/build/source_list.bzl index ebc17ad..9c579af 100644 --- a/build/source_list.bzl +++ b/build/source_list.bzl
@@ -1758,6 +1758,7 @@ "quic/qbone/qbone_server_session.h", "quic/qbone/qbone_session_base.h", "quic/qbone/qbone_stream.h", + "quic/qbone/test_tools/basic_quic_server.h", ] qbone_srcs = [ "quic/qbone/bonnet/icmp_reachable.cc", @@ -1795,6 +1796,7 @@ "quic/qbone/qbone_session_test.cc", "quic/qbone/qbone_stream.cc", "quic/qbone/qbone_stream_test.cc", + "quic/qbone/test_tools/basic_quic_server.cc", ] blind_sign_auth_hdrs = [ "blind_sign_auth/blind_sign_auth.h",
diff --git a/build/source_list.gni b/build/source_list.gni index 0bc2ac7..b6a23cf 100644 --- a/build/source_list.gni +++ b/build/source_list.gni
@@ -1763,6 +1763,7 @@ "src/quiche/quic/qbone/qbone_server_session.h", "src/quiche/quic/qbone/qbone_session_base.h", "src/quiche/quic/qbone/qbone_stream.h", + "src/quiche/quic/qbone/test_tools/basic_quic_server.h", ] qbone_srcs = [ "src/quiche/quic/qbone/bonnet/icmp_reachable.cc", @@ -1800,6 +1801,7 @@ "src/quiche/quic/qbone/qbone_session_test.cc", "src/quiche/quic/qbone/qbone_stream.cc", "src/quiche/quic/qbone/qbone_stream_test.cc", + "src/quiche/quic/qbone/test_tools/basic_quic_server.cc", ] blind_sign_auth_hdrs = [ "src/quiche/blind_sign_auth/blind_sign_auth.h",
diff --git a/build/source_list.json b/build/source_list.json index 1b3cbe7..d0589ee 100644 --- a/build/source_list.json +++ b/build/source_list.json
@@ -1761,7 +1761,8 @@ "quiche/quic/qbone/qbone_packet_writer.h", "quiche/quic/qbone/qbone_server_session.h", "quiche/quic/qbone/qbone_session_base.h", - "quiche/quic/qbone/qbone_stream.h" + "quiche/quic/qbone/qbone_stream.h", + "quiche/quic/qbone/test_tools/basic_quic_server.h" ], "qbone_srcs": [ "quiche/quic/qbone/bonnet/icmp_reachable.cc", @@ -1798,7 +1799,8 @@ "quiche/quic/qbone/qbone_session_base.cc", "quiche/quic/qbone/qbone_session_test.cc", "quiche/quic/qbone/qbone_stream.cc", - "quiche/quic/qbone/qbone_stream_test.cc" + "quiche/quic/qbone/qbone_stream_test.cc", + "quiche/quic/qbone/test_tools/basic_quic_server.cc" ], "blind_sign_auth_hdrs": [ "quiche/blind_sign_auth/blind_sign_auth.h",
diff --git a/quiche/quic/qbone/test_tools/basic_quic_server.cc b/quiche/quic/qbone/test_tools/basic_quic_server.cc new file mode 100644 index 0000000..00d58bf --- /dev/null +++ b/quiche/quic/qbone/test_tools/basic_quic_server.cc
@@ -0,0 +1,596 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "quiche/quic/qbone/test_tools/basic_quic_server.h" + +#include <array> +#include <cstddef> +#include <memory> +#include <queue> +#include <string> +#include <thread> // NOLINT (for open-sourceable thread ID) +#include <utility> +#include <vector> + +#include "absl/base/attributes.h" +#include "absl/base/nullability.h" +#include "absl/base/thread_annotations.h" +#include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/synchronization/mutex.h" +#include "absl/synchronization/notification.h" +#include "absl/types/span.h" +#include "quiche/quic/core/connection_id_generator.h" +#include "quiche/quic/core/crypto/proof_source.h" +#include "quiche/quic/core/crypto/quic_crypto_server_config.h" +#include "quiche/quic/core/crypto/quic_random.h" +#include "quiche/quic/core/deterministic_connection_id_generator.h" +#include "quiche/quic/core/io/quic_default_event_loop.h" +#include "quiche/quic/core/io/quic_event_loop.h" +#include "quiche/quic/core/io/quic_server_io_harness.h" +#include "quiche/quic/core/io/socket.h" +#include "quiche/quic/core/quic_alarm_factory.h" +#include "quiche/quic/core/quic_connection.h" +#include "quiche/quic/core/quic_connection_id.h" +#include "quiche/quic/core/quic_crypto_server_stream_base.h" +#include "quiche/quic/core/quic_default_clock.h" +#include "quiche/quic/core/quic_default_connection_helper.h" +#include "quiche/quic/core/quic_dispatcher.h" +#include "quiche/quic/core/quic_session.h" +#include "quiche/quic/core/quic_stream.h" +#include "quiche/quic/core/quic_stream_sequencer.h" +#include "quiche/quic/core/quic_time.h" +#include "quiche/quic/core/quic_types.h" +#include "quiche/quic/core/quic_utils.h" +#include "quiche/quic/core/quic_version_manager.h" +#include "quiche/quic/core/quic_versions.h" +#include "quiche/quic/platform/api/quic_socket_address.h" +#include "quiche/quic/tools/quic_simple_crypto_server_stream_helper.h" +#include "quiche/common/platform/api/quiche_logging.h" +#include "quiche/common/platform/api/quiche_thread.h" +#include "quiche/common/quiche_callbacks.h" +#include "quiche/common/quiche_mem_slice.h" +#include "quiche/common/quiche_socket_address.h" +#include "quiche/common/quiche_status_utils.h" + +namespace quic::test { +namespace { + +using ::quiche::QuicheMemSlice; +using ::quiche::QuicheSocketAddress; +using ::quiche::QuicheThread; +using ::quiche::SingleUseCallback; + +constexpr auto kSupportedQuicVersions = std::to_array<ParsedQuicVersion>({ + ParsedQuicVersion::RFCv1(), +}); + +class Stream : public QuicStream { + public: + Stream(QuicStreamId id, + QuicSession* absl_nonnull session ABSL_ATTRIBUTE_LIFETIME_BOUND, + BasicQuicServer::Handler* absl_nonnull handler + ABSL_ATTRIBUTE_LIFETIME_BOUND) + : QuicStream(id, session, /*is_static=*/false, + QuicUtils::GetStreamType( + id, session->connection()->perspective(), + session->IsIncomingStream(id), session->version())), + handler_(handler) { + // This stream allows leaving data unconsumed in the sequencer, so need to + // use level-triggered reading to notify the handler on any increase in + // readable bytes. + sequencer()->set_level_triggered(true); + } + + void OnDataAvailable() override { + int consumed = handler_->OnStreamDataAvailable(session()->connection_id(), + id(), *sequencer()); + if (consumed > 0) { + sequencer()->MarkConsumed(consumed); + } + } + + private: + BasicQuicServer::Handler* absl_nonnull const handler_; +}; + +class Session : public QuicSession { + public: + class DestructionObserver { + public: + virtual ~DestructionObserver() = default; + + virtual void OnSessionDestroyed(Session* session) = 0; + }; + + Session(std::unique_ptr<QuicConnection> absl_nonnull connection, + Visitor* visitor, const QuicConfig& config, + const QuicCryptoServerConfig* crypto_config, + BasicQuicServer::Handler* absl_nonnull handler + ABSL_ATTRIBUTE_LIFETIME_BOUND) + : QuicSession(connection.get(), visitor, config, + connection->supported_versions(), + /*num_expected_unidirectional_static_streams=*/0), + connection_(std::move(connection)), + handler_(handler) { + crypto_stream_ = handler_->OnNewSession(this, crypto_config); + Initialize(); + } + + ~Session() override { + handler_->OnSessionEnd(connection_id()); + + if (destruction_observer_) { + destruction_observer_->OnSessionDestroyed(this); + } + } + + void RegisterDestructionObserver(DestructionObserver* absl_nonnull observer) { + QUICHE_CHECK(!destruction_observer_); + destruction_observer_ = observer; + } + + void UnregisterDestructionObserver() { + QUICHE_CHECK(destruction_observer_); + destruction_observer_ = nullptr; + } + + // QuicSession: + + const QuicCryptoStream* GetCryptoStream() const override { + return crypto_stream_.get(); + } + + std::vector<absl::string_view>::const_iterator SelectAlpn( + const std::vector<absl::string_view>& alpns) const override { + return handler_->SelectAlpn(connection_id(), alpns); + } + + protected: + QuicStream* CreateIncomingStream(QuicStreamId id) override { + if (handler_->OnNewStream(connection_id(), id)) { + auto stream = std::make_unique<Stream>(id, this, handler_); + Stream* stream_ptr = stream.get(); + ActivateStream(std::move(stream)); + return stream_ptr; + } else { + return nullptr; + } + } + + QuicCryptoStream* GetMutableCryptoStream() override { + return crypto_stream_.get(); + } + + bool ShouldKeepConnectionAlive() const override { return true; } + + void OnDatagramReceived(absl::string_view datagram) override { + absl::Span<const std::byte> datagram_bytes( + reinterpret_cast<const std::byte*>(datagram.data()), datagram.size()); + handler_->OnDatagramReceived(connection_id(), datagram_bytes); + } + + private: + std::unique_ptr<QuicConnection> connection_; + std::unique_ptr<QuicCryptoStream> crypto_stream_; + DestructionObserver* absl_nullable destruction_observer_ = nullptr; + BasicQuicServer::Handler* absl_nonnull const handler_; +}; + +class Dispatcher : public QuicDispatcher, public Session::DestructionObserver { + public: + Dispatcher( + const QuicConfig* config ABSL_ATTRIBUTE_LIFETIME_BOUND, + const QuicCryptoServerConfig* crypto_config ABSL_ATTRIBUTE_LIFETIME_BOUND, + QuicVersionManager* version_manager ABSL_ATTRIBUTE_LIFETIME_BOUND, + std::unique_ptr<QuicAlarmFactory> alarm_factory, + ConnectionIdGeneratorInterface* absl_nonnull connection_id_generator + ABSL_ATTRIBUTE_LIFETIME_BOUND, + BasicQuicServer::Handler* absl_nonnull handler + ABSL_ATTRIBUTE_LIFETIME_BOUND) + : QuicDispatcher(config, crypto_config, version_manager, + std::make_unique<QuicDefaultConnectionHelper>(), + std::make_unique<QuicSimpleCryptoServerStreamHelper>(), + std::move(alarm_factory), kQuicDefaultConnectionIdLength, + *connection_id_generator), + handler_(handler) {} + + ~Dispatcher() override { + // Stop sessions from notifying back to this dispatcher on destruction. + for (auto& [connection_id, session] : sessions_) { + session->UnregisterDestructionObserver(); + } + } + + absl::StatusOr<Session*> GetSession(QuicConnectionId connection_id) { + auto it = sessions_.find(connection_id); + if (it == sessions_.end()) { + return absl::NotFoundError("Session not found."); + } else { + return it->second; + } + } + + // Session::DestructionObserver + void OnSessionDestroyed(Session* session) override { + QUICHE_CHECK_EQ(sessions_.erase(session->connection_id()), 1); + } + + protected: + std::unique_ptr<QuicSession> CreateQuicSession( + QuicConnectionId server_connection_id, + const QuicSocketAddress& self_address, + const QuicSocketAddress& peer_address, absl::string_view alpn, + const ParsedQuicVersion& version, const ParsedClientHello& parsed_chlo, + ConnectionIdGeneratorInterface& connection_id_generator) override { + QUICHE_LOG(INFO) << "Creating QuicSession for connection " + << server_connection_id << " with peer " + << peer_address.ToString() << " and ALPN " << alpn; + + auto connection = std::make_unique<QuicConnection>( + server_connection_id, self_address, peer_address, helper(), + alarm_factory(), writer(), + /*owns_writer=*/false, Perspective::IS_SERVER, + ParsedQuicVersionVector{version}, connection_id_generator); + + auto session = std::make_unique<Session>( + std::move(connection), this, config(), crypto_config(), handler_); + auto [it, inserted] = + sessions_.emplace(server_connection_id, session.get()); + QUICHE_CHECK(inserted); + session->RegisterDestructionObserver(this); + + return session; + } + + private: + // Non-refed Session pointers to allow destruction, but expect the Session to + // call OnSessionDestroyed to remove itself. + absl::flat_hash_map<QuicConnectionId, Session*, QuicConnectionIdHash> + sessions_; + + BasicQuicServer::Handler* absl_nonnull const handler_; +}; + +} // namespace + +class BasicQuicServer::ServerThread : public QuicheThread { + public: + ServerThread(QuicheSocketAddress socket_address, + const QuicConfig* absl_nonnull config + ABSL_ATTRIBUTE_LIFETIME_BOUND, + const QuicCryptoServerConfig* absl_nonnull crypto_config + ABSL_ATTRIBUTE_LIFETIME_BOUND, + std::unique_ptr<BasicQuicServer::Handler> absl_nonnull handler) + : QuicheThread("BasicQuicServerThread"), + socket_address_(std::move(socket_address)), + config_(config), + crypto_config_(crypto_config), + handler_(std::move(handler)) {} + + ~ServerThread() override { + // Expect/require server to be stopped before destruction. + QUICHE_CHECK(!running_notification_.HasBeenNotified() || + shutdown_notification_.HasBeenNotified()); + QUICHE_CHECK(!dispatcher_); + QUICHE_CHECK(!event_loop_); + } + + void Run() override { + QUICHE_CHECK(!running_notification_.HasBeenNotified()); + QUICHE_CHECK(!shutdown_notification_.HasBeenNotified()); + + thread_id_ = std::this_thread::get_id(); + + QUICHE_CHECK(absl::IsUnknown(initialization_status_)); + initialization_status_ = Initialize(); + if (!initialization_status_.ok()) { + QUICHE_LOG(ERROR) << "Failed to initialize BasicQuicServer: " + << initialization_status_; + running_notification_.Notify(); + return; + } + + bound_address_ = io_harness_->local_address(); + + QUICHE_LOG(INFO) << "Starting BasicQuicServer on " << bound_address_; + running_notification_.Notify(); + while (!shutdown_notification_.HasBeenNotified()) { + ExecuteScheduledActions(); + event_loop_->RunEventLoopOnce(QuicTimeDelta::FromMilliseconds(50)); + } + thread_id_ = std::thread::id(); + + bound_address_ = QuicheSocketAddress(); + + dispatcher_->Shutdown(); + io_harness_.reset(); + socket_fd_.reset(); + dispatcher_.reset(); + + // Don't reset `event_loop_`, as off-thread callers may attempt to use it + // to call WakeUp(). + } + + absl::Status WaitForRunning() { + running_notification_.WaitForNotification(); + QUICHE_CHECK(!absl::IsUnknown(initialization_status_)); + return initialization_status_; + } + + void Shutdown() { + QUICHE_CHECK(running_notification_.HasBeenNotified()); + QUICHE_CHECK(!shutdown_notification_.HasBeenNotified()); + QUICHE_CHECK(!IsRunningOnThread()); + + shutdown_notification_.Notify(); + if (event_loop_->SupportsWakeUp()) { + event_loop_->WakeUp(); + } + + Join(); + } + + absl::StatusOr<QuicheSocketAddress> bound_address() const { + QUICHE_CHECK(IsRunningOnThread()); + + return bound_address_; + } + + absl::StatusOr<int> SendStreamData(QuicConnectionId server_connection_id, + QuicStreamId stream_id, + absl::Span<QuicheMemSlice> data, + bool fin) { + QUICHE_CHECK(IsRunningOnThread()); + QUICHE_CHECK(dispatcher_); + + QUICHE_ASSIGN_OR_RETURN(QuicSession * session, + dispatcher_->GetSession(server_connection_id)); + + QuicStream* stream = session->GetActiveStream(stream_id); + if (!stream) { + return absl::NotFoundError("Stream not found."); + } + return stream->WriteMemSlices(data, fin).bytes_consumed; + } + + absl::Status SendDatagram(QuicConnectionId server_connection_id, + absl::Span<QuicheMemSlice> data) { + QUICHE_CHECK(IsRunningOnThread()); + QUICHE_CHECK(dispatcher_); + + QUICHE_ASSIGN_OR_RETURN(QuicSession * session, + dispatcher_->GetSession(server_connection_id)); + + if (!session->connection() || !session->connection()->connected()) { + return absl::FailedPreconditionError("Session is not connected."); + } + + DatagramResult result = session->SendDatagram(data); + if (result.status == DATAGRAM_STATUS_SUCCESS) { + return absl::OkStatus(); + } else { + return absl::InternalError(absl::StrCat( + "Failed to send datagram: ", DatagramStatusToString(result.status))); + } + } + + void Schedule(quiche::SingleUseCallback<void()> callback) { + QUICHE_CHECK(running_notification_.HasBeenNotified()); + QUICHE_CHECK(!shutdown_notification_.HasBeenNotified()); + + { + absl::MutexLock lock(scheduled_actions_mutex_); + scheduled_actions_.push(std::move(callback)); + } + + if (IsRunningOnThread()) { + // Run all scheduled actions immediately if already on the server thread. + // This is important to avoid deadlocks if the caller of Schedule() waits + // on signals contained in the callback. + ExecuteScheduledActions(); + } else if (event_loop_->SupportsWakeUp()) { + event_loop_->WakeUp(); + } + } + + void ScheduleAndWaitForCompletion( + quiche::SingleUseCallback<void()> callback) { + QUICHE_CHECK(running_notification_.HasBeenNotified()); + QUICHE_CHECK(!shutdown_notification_.HasBeenNotified()); + + if (IsRunningOnThread()) { + ExecuteScheduledActions(); + std::move(callback)(); + } else { + absl::Notification done; + Schedule([&callback, &done]() { + std::move(callback)(); + done.Notify(); + }); + done.WaitForNotification(); + } + } + + bool IsRunningOnThread() const { + return running_notification_.HasBeenNotified() && + thread_id_ == std::this_thread::get_id(); + } + + private: + absl::Status Initialize() { + event_loop_ = GetDefaultEventLoop()->Create(QuicDefaultClock::Get()); + dispatcher_ = + std::make_unique<Dispatcher>(config_, crypto_config_, &version_manager_, + event_loop_->CreateAlarmFactory(), + &connection_id_generator_, handler_.get()); + + QUICHE_ASSIGN_OR_RETURN(socket_fd_, + CreateAndBindServerSocket(socket_address_)); + QUICHE_ASSIGN_OR_RETURN( + io_harness_, + QuicServerIoHarness::Create(event_loop_.get(), dispatcher_.get(), + socket_fd_.get())); + io_harness_->InitializeWriter(); + + return absl::OkStatus(); + } + + void ExecuteScheduledActions() { + QUICHE_CHECK(IsRunningOnThread()); + + std::queue<quiche::SingleUseCallback<void()>> actions; + { + absl::MutexLock lock(scheduled_actions_mutex_); + actions.swap(scheduled_actions_); + } + + while (!actions.empty() && !shutdown_notification_.HasBeenNotified()) { + std::move(actions.front())(); + actions.pop(); + } + } + + const QuicheSocketAddress socket_address_; + const QuicConfig* absl_nonnull const config_; + const QuicCryptoServerConfig* absl_nonnull const crypto_config_; + const std::unique_ptr<BasicQuicServer::Handler> absl_nonnull handler_; + + // Must be set before `running_notification_` is notified and not changed + // after that. + absl::Status initialization_status_ = absl::UnknownError("Not initialized."); + std::thread::id thread_id_; + + absl::Notification running_notification_; + absl::Notification shutdown_notification_; + + absl::Mutex scheduled_actions_mutex_; + std::queue<quiche::SingleUseCallback<void()>> scheduled_actions_ + ABSL_GUARDED_BY(scheduled_actions_mutex_); + + QuicVersionManager version_manager_{ParsedQuicVersionVector( + kSupportedQuicVersions.begin(), kSupportedQuicVersions.end())}; + DeterministicConnectionIdGenerator connection_id_generator_{ + kQuicDefaultConnectionIdLength}; + + std::unique_ptr<QuicEventLoop> event_loop_; + std::unique_ptr<Dispatcher> dispatcher_; + OwnedSocketFd socket_fd_; + std::unique_ptr<QuicServerIoHarness> io_harness_; + + QuicheSocketAddress bound_address_; +}; + +BasicQuicServer::BasicQuicServer( + QuicheSocketAddress socket_address, + std::unique_ptr<ProofSource> absl_nonnull proof_source) + : socket_address_(std::move(socket_address)), + crypto_config_(std::make_unique<QuicCryptoServerConfig>( + QuicCryptoServerConfig::TESTING, QuicRandom::GetInstance(), + std::move(proof_source), KeyExchangeSource::Default())) {} + +BasicQuicServer::~BasicQuicServer() { + if (server_thread_) { + QUICHE_LOG(WARNING) << "BasicQuicServer not stopped before destruction."; + QUICHE_CHECK_OK(Stop()); + } +} + +absl::Status BasicQuicServer::Start( + std::unique_ptr<Handler> absl_nonnull handler) { + QUICHE_CHECK(!server_thread_) << "Server already running."; + + server_thread_ = std::make_unique<ServerThread>( + socket_address_, &config_, crypto_config_.get(), std::move(handler)); + server_thread_->Start(); + + absl::Status status = server_thread_->WaitForRunning(); + if (!status.ok()) { + server_thread_->Shutdown(); + server_thread_.reset(); + return status; + } + + return absl::OkStatus(); +} + +absl::Status BasicQuicServer::Stop() { + QUICHE_CHECK(server_thread_) << "Server not running."; + + server_thread_->Shutdown(); + server_thread_.reset(); + + return absl::OkStatus(); +} + +void BasicQuicServer::Schedule(quiche::SingleUseCallback<void()> callback) { + QUICHE_CHECK(server_thread_) << "Server not running."; + server_thread_->Schedule(std::move(callback)); +} + +void BasicQuicServer::ScheduleAndWaitForCompletion( + quiche::SingleUseCallback<void()> callback) { + QUICHE_CHECK(server_thread_) << "Server not running."; + server_thread_->ScheduleAndWaitForCompletion(std::move(callback)); +} + +absl::StatusOr<QuicheSocketAddress> BasicQuicServer::bound_address() { + QUICHE_CHECK(server_thread_) << "Server not running."; + + absl::StatusOr<QuicheSocketAddress> result; + const ServerThread* server_thread = server_thread_.get(); + ScheduleAndWaitForCompletion( + [&result, server_thread]() { result = server_thread->bound_address(); }); + return result; +} + +absl::StatusOr<int> BasicQuicServer::SendStreamData( + QuicConnectionId server_connection_id, QuicStreamId stream_id, + absl::Span<const std::byte> data, bool fin) { + auto mem_slice = QuicheMemSlice::Copy(absl::string_view( + reinterpret_cast<const char*>(data.data()), data.size())); + return SendStreamData(server_connection_id, stream_id, + absl::MakeSpan(&mem_slice, 1), fin); +} + +absl::StatusOr<int> BasicQuicServer::SendStreamData( + QuicConnectionId server_connection_id, QuicStreamId stream_id, + absl::Span<quiche::QuicheMemSlice> data, bool fin) { + QUICHE_CHECK(server_thread_) << "Server not running."; + + ServerThread* server_thread = server_thread_.get(); + absl::StatusOr<int> result; + ScheduleAndWaitForCompletion( + [&result, server_thread, server_connection_id, stream_id, data, fin]() { + result = server_thread->SendStreamData(server_connection_id, stream_id, + data, fin); + }); + return result; +} + +absl::Status BasicQuicServer::SendDatagram( + QuicConnectionId server_connection_id, absl::Span<const std::byte> data) { + auto mem_slice = QuicheMemSlice::Copy(absl::string_view( + reinterpret_cast<const char*>(data.data()), data.size())); + return SendDatagram(server_connection_id, absl::MakeSpan(&mem_slice, 1)); +} + +absl::Status BasicQuicServer::SendDatagram( + QuicConnectionId server_connection_id, + absl::Span<quiche::QuicheMemSlice> data) { + QUICHE_CHECK(server_thread_) << "Server not running."; + + ServerThread* server_thread = server_thread_.get(); + absl::Status result; + ScheduleAndWaitForCompletion( + [&result, server_thread, server_connection_id, data]() { + result = server_thread->SendDatagram(server_connection_id, data); + }); + return result; +} + +} // namespace quic::test
diff --git a/quiche/quic/qbone/test_tools/basic_quic_server.h b/quiche/quic/qbone/test_tools/basic_quic_server.h new file mode 100644 index 0000000..91b190d --- /dev/null +++ b/quiche/quic/qbone/test_tools/basic_quic_server.h
@@ -0,0 +1,124 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef QUICHE_QUIC_QBONE_TEST_TOOLS_BASIC_QUIC_SERVER_H_ +#define QUICHE_QUIC_QBONE_TEST_TOOLS_BASIC_QUIC_SERVER_H_ + +#include <cstddef> +#include <memory> +#include <vector> + +#include "absl/base/nullability.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "quiche/quic/core/crypto/proof_source.h" +#include "quiche/quic/core/quic_connection.h" +#include "quiche/quic/core/quic_connection_id.h" +#include "quiche/quic/core/quic_dispatcher.h" +#include "quiche/quic/core/quic_session.h" +#include "quiche/quic/core/quic_stream_sequencer.h" +#include "quiche/quic/core/quic_types.h" +#include "quiche/quic/core/tls_server_handshaker.h" +#include "quiche/common/quiche_callbacks.h" +#include "quiche/common/quiche_mem_slice.h" +#include "quiche/common/quiche_socket_address.h" + +namespace quic::test { + +// Lightweight QUIC (not necessarily HTTP/3) server for tests and experiments, +// especially for simulating QBONE Terminator functionality. Receives traffic +// over a socket on the given socket address, which may be a loopback interface +// for same-machine (or same-process) testing. Runs on its own dedicated server +// thread for ease of running same-process as a client. +class BasicQuicServer final { + public: + // Interface for handling server events. All calls will be made on the server + // thread. + class Handler { + public: + virtual ~Handler() = default; + + // Called on construction of a new QuicSession. Returns the QuicCryptoStream + // to use for the session. + virtual std::unique_ptr<QuicCryptoStream> OnNewSession( + QuicSession* session, const QuicCryptoServerConfig* crypto_config) { + return std::make_unique<TlsServerHandshaker>(session, crypto_config); + } + + virtual void OnSessionEnd(QuicConnectionId server_connection_id) {} + + // See QuicSession::SelectAlpn. + virtual std::vector<absl::string_view>::const_iterator SelectAlpn( + QuicConnectionId server_connection_id, + const std::vector<absl::string_view>& alpns) const { + return alpns.cbegin(); + } + + // Called on initiation of a new stream from the peer. Returns true iff the + // stream should be accepted and created. + virtual bool OnNewStream(QuicConnectionId server_connection_id, + QuicStreamId stream_id) { + return true; + } + + // Called when new data is available on a stream. Returns the number of + // bytes that were successfully processed and that should be consumed from + // stream buffers. + virtual int OnStreamDataAvailable( + QuicConnectionId server_connection_id, QuicStreamId stream_id, + const QuicStreamSequencer& data_sequencer) { + return data_sequencer.ReadableBytes(); + } + + virtual void OnDatagramReceived(QuicConnectionId server_connection_id, + absl::Span<const std::byte> data) {} + }; + + BasicQuicServer(quiche::QuicheSocketAddress socket_address, + std::unique_ptr<ProofSource> absl_nonnull proof_source); + ~BasicQuicServer(); + + absl::Status Start(std::unique_ptr<Handler> absl_nonnull handler); + absl::Status Stop(); + + // The following interactions should be safe from any thread, and will + // marshall to the server thread as necessary. + + // Not guaranteed to run if the server is stopped. + void Schedule(quiche::SingleUseCallback<void()> callback); + // Not guaranteed to ever return if the server is stopped. + void ScheduleAndWaitForCompletion(quiche::SingleUseCallback<void()> callback); + + absl::StatusOr<quiche::QuicheSocketAddress> bound_address(); + + // Returns the number of bytes consumed into the stream (written or buffered). + absl::StatusOr<int> SendStreamData(QuicConnectionId server_connection_id, + QuicStreamId stream_id, + absl::Span<const std::byte> data, + bool fin = false); + absl::StatusOr<int> SendStreamData(QuicConnectionId server_connection_id, + QuicStreamId stream_id, + absl::Span<quiche::QuicheMemSlice> data, + bool fin = false); + + absl::Status SendDatagram(QuicConnectionId server_connection_id, + absl::Span<const std::byte> data); + absl::Status SendDatagram(QuicConnectionId server_connection_id, + absl::Span<quiche::QuicheMemSlice> data); + + private: + class ServerThread; + + const quiche::QuicheSocketAddress socket_address_; + + const QuicConfig config_; + const std::unique_ptr<QuicCryptoServerConfig> crypto_config_; + std::unique_ptr<ServerThread> server_thread_; +}; + +} // namespace quic::test + +#endif // QUICHE_QUIC_QBONE_TEST_TOOLS_BASIC_QUIC_SERVER_H_