Bonnet Tun Exchanger Refactor: Divorce exchanger from QbonePacketWriter

The Tun exchanger code and the server-side packet-to-network logic shared an interface in an old attempt to share the same to-network logic in both client and server.  But there was so much different logic, that it didn't really do much other than very slightly simplify the one call to pass error packets backwards on being too big for the tunnel.

Needlessley restrains my ability to modify the exchanger and interface as needed for async Tun refactors, so cleaner to remove the interface. Server session keeps using its writer implementation. Client session now deals directly with the Tun exchanger.

This also fixes the subtle issue that overriding the "quic_QbonePacketWriter" test value previously could not differentiate which direction was being overridden and could insert the same mock on both ends in an integration test. Now, that test value only affects server-side network writes.

PiperOrigin-RevId: 952297179
diff --git a/build/source_list.bzl b/build/source_list.bzl
index 537de81..7823208 100644
--- a/build/source_list.bzl
+++ b/build/source_list.bzl
@@ -1745,6 +1745,7 @@
     "quic/qbone/bonnet/tun_device_interface.h",
     "quic/qbone/bonnet/tun_device_packet_exchanger.h",
     "quic/qbone/mock_qbone_client.h",
+    "quic/qbone/mock_qbone_packet_exchanger.h",
     "quic/qbone/mock_qbone_server_session.h",
     "quic/qbone/platform/icmp_packet.h",
     "quic/qbone/platform/ip_range.h",
diff --git a/build/source_list.gni b/build/source_list.gni
index cfa9727..cb57ef7 100644
--- a/build/source_list.gni
+++ b/build/source_list.gni
@@ -1750,6 +1750,7 @@
     "src/quiche/quic/qbone/bonnet/tun_device_interface.h",
     "src/quiche/quic/qbone/bonnet/tun_device_packet_exchanger.h",
     "src/quiche/quic/qbone/mock_qbone_client.h",
+    "src/quiche/quic/qbone/mock_qbone_packet_exchanger.h",
     "src/quiche/quic/qbone/mock_qbone_server_session.h",
     "src/quiche/quic/qbone/platform/icmp_packet.h",
     "src/quiche/quic/qbone/platform/ip_range.h",
diff --git a/build/source_list.json b/build/source_list.json
index 515e539..8aef483 100644
--- a/build/source_list.json
+++ b/build/source_list.json
@@ -1749,6 +1749,7 @@
     "quiche/quic/qbone/bonnet/tun_device_interface.h",
     "quiche/quic/qbone/bonnet/tun_device_packet_exchanger.h",
     "quiche/quic/qbone/mock_qbone_client.h",
+    "quiche/quic/qbone/mock_qbone_packet_exchanger.h",
     "quiche/quic/qbone/mock_qbone_server_session.h",
     "quiche/quic/qbone/platform/icmp_packet.h",
     "quiche/quic/qbone/platform/ip_range.h",
diff --git a/quiche/quic/qbone/mock_qbone_packet_exchanger.h b/quiche/quic/qbone/mock_qbone_packet_exchanger.h
new file mode 100644
index 0000000..42a6691
--- /dev/null
+++ b/quiche/quic/qbone/mock_qbone_packet_exchanger.h
@@ -0,0 +1,31 @@
+// 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_MOCK_QBONE_PACKET_EXCHANGER_H_
+#define QUICHE_QUIC_QBONE_MOCK_QBONE_PACKET_EXCHANGER_H_
+
+#include <cstddef>
+#include <memory>
+#include <string>
+
+#include "quiche/quic/core/quic_packets.h"
+#include "quiche/quic/platform/api/quic_test.h"
+#include "quiche/quic/qbone/qbone_packet_exchanger.h"
+
+namespace quic::test {
+
+class MockQbonePacketExchanger : public QbonePacketExchanger {
+ public:
+  MockQbonePacketExchanger() : QbonePacketExchanger(/*visitor=*/nullptr) {}
+
+  MOCK_METHOD(std::unique_ptr<QuicData>, ReadPacket, (std::string * error),
+              (override));
+  MOCK_METHOD(bool, WritePacket,
+              (const char* packet, size_t size, std::string* error),
+              (override));
+};
+
+}  // namespace quic::test
+
+#endif  // QUICHE_QUIC_QBONE_MOCK_QBONE_PACKET_EXCHANGER_H_
diff --git a/quiche/quic/qbone/qbone_client.cc b/quiche/quic/qbone/qbone_client.cc
index 278a6b3..b86e6de 100644
--- a/quiche/quic/qbone/qbone_client.cc
+++ b/quiche/quic/qbone/qbone_client.cc
@@ -9,11 +9,13 @@
 #include <utility>
 
 #include "absl/base/casts.h"
+#include "absl/base/nullability.h"
 #include "absl/strings/string_view.h"
 #include "quiche/quic/core/io/quic_event_loop.h"
 #include "quiche/quic/core/quic_bandwidth.h"
 #include "quiche/quic/core/quic_default_connection_helper.h"
 #include "quiche/quic/platform/api/quic_testvalue.h"
+#include "quiche/quic/qbone/qbone_packet_exchanger.h"
 #include "quiche/quic/tools/quic_client_base.h"
 #include "quiche/quic/tools/quic_client_default_network_helper.h"
 #include "quiche/common/platform/api/quiche_command_line_flags.h"
@@ -40,20 +42,19 @@
 }
 }  // namespace
 
-QboneClient::QboneClient(QuicSocketAddress server_address,
-                         const QuicServerId& server_id,
-                         const ParsedQuicVersionVector& supported_versions,
-                         QuicSession::Visitor* session_owner,
-                         const QuicConfig& config, QuicEventLoop* event_loop,
-                         std::unique_ptr<ProofVerifier> proof_verifier,
-                         QbonePacketWriter* qbone_writer,
-                         QboneClientControlStream::Handler* qbone_handler)
+QboneClient::QboneClient(
+    QuicSocketAddress server_address, const QuicServerId& server_id,
+    const ParsedQuicVersionVector& supported_versions,
+    QuicSession::Visitor* session_owner, const QuicConfig& config,
+    QuicEventLoop* event_loop, std::unique_ptr<ProofVerifier> proof_verifier,
+    QbonePacketExchanger* absl_nonnull local_network_packet_exchanger,
+    QboneClientControlStream::Handler* qbone_handler)
     : QuicClientBase(server_id, supported_versions, config,
                      new QuicDefaultConnectionHelper(),
                      event_loop->CreateAlarmFactory().release(),
                      CreateNetworkHelper(event_loop, this),
                      std::move(proof_verifier), nullptr),
-      qbone_writer_(qbone_writer),
+      local_network_packet_exchanger_(*local_network_packet_exchanger),
       qbone_handler_(qbone_handler),
       session_owner_(session_owner),
       max_pacing_rate_(QuicBandwidth::Zero()) {
@@ -108,7 +109,8 @@
   }
   return std::make_unique<QboneClientSessionWithConnection>(
       connection, crypto_config(), session_owner(), *config(),
-      supported_versions, server_id(), qbone_writer_, qbone_handler_);
+      supported_versions, server_id(), &local_network_packet_exchanger_,
+      qbone_handler_);
 }
 
 bool QboneClient::use_quarantine_mode() const { return use_quarantine_mode_; }
diff --git a/quiche/quic/qbone/qbone_client.h b/quiche/quic/qbone/qbone_client.h
index 087e279..1838c87 100644
--- a/quiche/quic/qbone/qbone_client.h
+++ b/quiche/quic/qbone/qbone_client.h
@@ -5,12 +5,16 @@
 #ifndef QUICHE_QUIC_QBONE_QBONE_CLIENT_H_
 #define QUICHE_QUIC_QBONE_QBONE_CLIENT_H_
 
+#include <memory>
+
+#include "absl/base/attributes.h"
+#include "absl/base/nullability.h"
 #include "absl/strings/string_view.h"
 #include "quiche/quic/core/io/quic_event_loop.h"
 #include "quiche/quic/core/quic_bandwidth.h"
 #include "quiche/quic/qbone/qbone_client_interface.h"
 #include "quiche/quic/qbone/qbone_client_session.h"
-#include "quiche/quic/qbone/qbone_packet_writer.h"
+#include "quiche/quic/qbone/qbone_packet_exchanger.h"
 #include "quiche/quic/tools/quic_client_base.h"
 
 namespace quic {
@@ -26,7 +30,8 @@
               QuicSession::Visitor* session_owner, const QuicConfig& config,
               QuicEventLoop* event_loop,
               std::unique_ptr<ProofVerifier> proof_verifier,
-              QbonePacketWriter* qbone_writer,
+              QbonePacketExchanger* absl_nonnull local_network_packet_exchanger
+                  ABSL_ATTRIBUTE_LIFETIME_BOUND,
               QboneClientControlStream::Handler* qbone_handler);
   ~QboneClient() override;
   QboneClientSession* qbone_session();
@@ -56,7 +61,9 @@
       const ParsedQuicVersionVector& supported_versions,
       QuicConnection* connection) override;
 
-  QbonePacketWriter* qbone_writer() { return qbone_writer_; }
+  QbonePacketExchanger& local_network_packet_exchanger() {
+    return local_network_packet_exchanger_;
+  }
 
   QboneClientControlStream::Handler* qbone_control_handler() {
     return qbone_handler_;
@@ -67,7 +74,7 @@
   bool HasActiveRequests() override;
 
  private:
-  QbonePacketWriter* qbone_writer_;
+  QbonePacketExchanger& local_network_packet_exchanger_;
   QboneClientControlStream::Handler* qbone_handler_;
 
   QuicSession::Visitor* session_owner_;
diff --git a/quiche/quic/qbone/qbone_client_session.cc b/quiche/quic/qbone/qbone_client_session.cc
index 9af3394..8d2a111 100644
--- a/quiche/quic/qbone/qbone_client_session.cc
+++ b/quiche/quic/qbone/qbone_client_session.cc
@@ -10,6 +10,7 @@
 #include "absl/strings/string_view.h"
 #include "quiche/quic/core/quic_types.h"
 #include "quiche/quic/qbone/qbone_constants.h"
+#include "quiche/quic/qbone/qbone_packet_exchanger.h"
 #include "quiche/common/platform/api/quiche_command_line_flags.h"
 
 namespace quic {
@@ -19,9 +20,11 @@
     QuicCryptoClientConfig* quic_crypto_client_config,
     QuicSession::Visitor* owner, const QuicConfig& config,
     const ParsedQuicVersionVector& supported_versions,
-    const QuicServerId& server_id, QbonePacketWriter* writer,
+    const QuicServerId& server_id,
+    QbonePacketExchanger* absl_nonnull local_network_packet_exchanger,
     QboneClientControlStream::Handler* handler)
-    : QboneSessionBase(connection, owner, config, supported_versions, writer),
+    : QboneSessionBase(connection, owner, config, supported_versions),
+      local_network_packet_exchanger_(*local_network_packet_exchanger),
       server_id_(server_id),
       quic_crypto_client_config_(quic_crypto_client_config),
       handler_(handler) {}
@@ -34,6 +37,11 @@
       /*has_application_state = */ true);
 }
 
+void QboneClientSession::SendErrorPacketToNetwork(absl::string_view packet) {
+  local_network_packet_exchanger_.WritePacketToNetwork(packet.data(),
+                                                       packet.size());
+}
+
 void QboneClientSession::CreateControlStream() {
   if (control_stream_ != nullptr) {
     return;
@@ -98,7 +106,8 @@
 }
 
 void QboneClientSession::ProcessPacketFromPeer(absl::string_view packet) {
-  writer_->WritePacketToNetwork(packet.data(), packet.size());
+  local_network_packet_exchanger_.WritePacketToNetwork(packet.data(),
+                                                       packet.size());
 }
 
 void QboneClientSession::OnProofValid(
diff --git a/quiche/quic/qbone/qbone_client_session.h b/quiche/quic/qbone/qbone_client_session.h
index 8a36c7e..c1e1cea 100644
--- a/quiche/quic/qbone/qbone_client_session.h
+++ b/quiche/quic/qbone/qbone_client_session.h
@@ -5,11 +5,14 @@
 #ifndef QUICHE_QUIC_QBONE_QBONE_CLIENT_SESSION_H_
 #define QUICHE_QUIC_QBONE_QBONE_CLIENT_SESSION_H_
 
+#include "absl/base/attributes.h"
+#include "absl/base/nullability.h"
 #include "absl/strings/string_view.h"
 #include "quiche/quic/core/quic_crypto_client_stream.h"
 #include "quiche/quic/platform/api/quic_export.h"
 #include "quiche/quic/qbone/qbone_control.pb.h"
 #include "quiche/quic/qbone/qbone_control_stream.h"
+#include "quiche/quic/qbone/qbone_packet_exchanger.h"
 #include "quiche/quic/qbone/qbone_packet_writer.h"
 #include "quiche/quic/qbone/qbone_session_base.h"
 
@@ -19,12 +22,15 @@
     : public QboneSessionBase,
       public QuicCryptoClientStream::ProofHandler {
  public:
-  QboneClientSession(QuicConnection* connection,
-                     QuicCryptoClientConfig* quic_crypto_client_config,
-                     QuicSession::Visitor* owner, const QuicConfig& config,
-                     const ParsedQuicVersionVector& supported_versions,
-                     const QuicServerId& server_id, QbonePacketWriter* writer,
-                     QboneClientControlStream::Handler* handler);
+  QboneClientSession(
+      QuicConnection* connection,
+      QuicCryptoClientConfig* quic_crypto_client_config,
+      QuicSession::Visitor* owner, const QuicConfig& config,
+      const ParsedQuicVersionVector& supported_versions,
+      const QuicServerId& server_id,
+      QbonePacketExchanger* absl_nonnull local_network_packet_exchanger
+          ABSL_ATTRIBUTE_LIFETIME_BOUND,
+      QboneClientControlStream::Handler* handler);
   QboneClientSession(const QboneClientSession&) = delete;
   QboneClientSession& operator=(const QboneClientSession&) = delete;
   ~QboneClientSession() override;
@@ -64,6 +70,7 @@
  protected:
   // QboneSessionBase interface implementation.
   std::unique_ptr<QuicCryptoStream> CreateCryptoStream() override;
+  void SendErrorPacketToNetwork(absl::string_view packet) override;
 
   // Instantiate QboneClientControlStream.
   void CreateControlStream();
@@ -81,6 +88,7 @@
   }
 
  private:
+  QbonePacketExchanger& local_network_packet_exchanger_;
   QuicServerId server_id_;
   // Config for QUIC crypto client stream, used by the client.
   QuicCryptoClientConfig* quic_crypto_client_config_;
diff --git a/quiche/quic/qbone/qbone_client_test.cc b/quiche/quic/qbone/qbone_client_test.cc
index 222bf8e..f875a3f 100644
--- a/quiche/quic/qbone/qbone_client_test.cc
+++ b/quiche/quic/qbone/qbone_client_test.cc
@@ -6,6 +6,7 @@
 
 #include "quiche/quic/qbone/qbone_client.h"
 
+#include <cstddef>
 #include <memory>
 #include <string>
 #include <utility>
@@ -25,6 +26,7 @@
 #include "quiche/quic/platform/api/quic_socket_address.h"
 #include "quiche/quic/platform/api/quic_test.h"
 #include "quiche/quic/platform/api/quic_test_loopback.h"
+#include "quiche/quic/qbone/mock_qbone_packet_exchanger.h"
 #include "quiche/quic/qbone/qbone_packet_processor_test_tools.h"
 #include "quiche/quic/qbone/qbone_server_session.h"
 #include "quiche/quic/test_tools/crypto_test_utils.h"
@@ -39,6 +41,7 @@
 namespace test {
 namespace {
 
+using ::testing::_;
 using ::testing::ElementsAre;
 
 ParsedQuicVersionVector GetTestParams() {
@@ -161,7 +164,14 @@
                   std::unique_ptr<ProofVerifier> proof_verifier)
       : QboneClient(server_address, server_id, supported_versions,
                     /*session_owner=*/nullptr, QuicConfig(), event_loop,
-                    std::move(proof_verifier), &qbone_writer_, nullptr) {}
+                    std::move(proof_verifier), &packet_exchanger_, nullptr) {
+    ON_CALL(packet_exchanger_, WritePacket(_, _, _))
+        .WillByDefault(
+            [this](const char* packet, size_t size, std::string* error) {
+              data_.push_back(std::string(packet, size));
+              return true;
+            });
+  }
 
   ~QboneTestClient() override {}
 
@@ -190,10 +200,11 @@
     return true;
   }
 
-  std::vector<std::string> data() { return qbone_writer_.data(); }
+  const std::vector<std::string>& data() { return data_; }
 
  private:
-  DataSavingQbonePacketWriter qbone_writer_;
+  MockQbonePacketExchanger packet_exchanger_;
+  std::vector<std::string> data_;
 };
 
 class QboneClientTest : public QuicTestWithParam<ParsedQuicVersion> {};
diff --git a/quiche/quic/qbone/qbone_packet_exchanger.h b/quiche/quic/qbone/qbone_packet_exchanger.h
index d6175b6..5616426 100644
--- a/quiche/quic/qbone/qbone_packet_exchanger.h
+++ b/quiche/quic/qbone/qbone_packet_exchanger.h
@@ -5,17 +5,20 @@
 #ifndef QUICHE_QUIC_QBONE_QBONE_PACKET_EXCHANGER_H_
 #define QUICHE_QUIC_QBONE_QBONE_PACKET_EXCHANGER_H_
 
+#include <cstddef>
+#include <memory>
+#include <string>
+
 #include "absl/status/status.h"
 #include "absl/strings/string_view.h"
 #include "quiche/quic/core/quic_packets.h"
 #include "quiche/quic/qbone/qbone_client_interface.h"
-#include "quiche/quic/qbone/qbone_packet_writer.h"
 
 namespace quic {
 
 // Handles reading and writing on the local network and exchange packets between
 // the local network with a QBONE connection.
-class QbonePacketExchanger : public QbonePacketWriter {
+class QbonePacketExchanger {
  public:
   // The owner might want to receive notifications when read or write fails.
   class Visitor {
@@ -36,17 +39,16 @@
   QbonePacketExchanger(QbonePacketExchanger&&) = delete;
   QbonePacketExchanger& operator=(QbonePacketExchanger&&) = delete;
 
-  ~QbonePacketExchanger() = default;
+  virtual ~QbonePacketExchanger() = default;
 
   // Returns true if there may be more packets to read.
   // Implementations handles the actual raw read and delivers the packet to
   // qbone_client.
   bool ReadAndDeliverPacket(QboneClientInterface* qbone_client);
 
-  // From QbonePacketWriter.
   // Writes a packet to the local network. If the write would be blocked, the
   // packet is dropped.
-  void WritePacketToNetwork(const char* packet, size_t size) override;
+  void WritePacketToNetwork(const char* packet, size_t size);
 
  private:
   // The actual implementation that reads a packet from the local network.
diff --git a/quiche/quic/qbone/qbone_server_session.cc b/quiche/quic/qbone/qbone_server_session.cc
index a4354e7..b0c9570 100644
--- a/quiche/quic/qbone/qbone_server_session.cc
+++ b/quiche/quic/qbone/qbone_server_session.cc
@@ -8,12 +8,16 @@
 #include <string>
 #include <utility>
 
+#include "absl/base/nullability.h"
 #include "absl/strings/string_view.h"
 #include "quiche/quic/core/quic_connection_id.h"
 #include "quiche/quic/core/quic_types.h"
 #include "quiche/quic/core/quic_utils.h"
+#include "quiche/quic/platform/api/quic_testvalue.h"
 #include "quiche/quic/qbone/qbone_constants.h"
+#include "quiche/quic/qbone/qbone_packet_writer.h"
 #include "quiche/common/platform/api/quiche_command_line_flags.h"
+#include "quiche/common/platform/api/quiche_logging.h"
 
 namespace quic {
 
@@ -34,23 +38,35 @@
     const quic::ParsedQuicVersionVector& supported_versions,
     QuicConnection* connection, Visitor* owner, const QuicConfig& config,
     const QuicCryptoServerConfig* quic_crypto_server_config,
-    QuicCompressedCertsCache* compressed_certs_cache, QbonePacketWriter* writer,
-    QuicIpAddress self_ip, QuicIpAddress client_ip,
-    size_t client_ip_subnet_length, QboneServerControlStream::Handler* handler)
-    : QboneSessionBase(connection, owner, config, supported_versions, writer),
+    QuicCompressedCertsCache* compressed_certs_cache,
+    QbonePacketWriter* absl_nullable writer, QuicIpAddress self_ip,
+    QuicIpAddress client_ip, size_t client_ip_subnet_length,
+    QboneServerControlStream::Handler* handler)
+    : QboneSessionBase(connection, owner, config, supported_versions),
       processor_(self_ip, client_ip, client_ip_subnet_length, this, this),
       quic_crypto_server_config_(quic_crypto_server_config),
       compressed_certs_cache_(compressed_certs_cache),
-      handler_(handler) {}
+      handler_(handler) {
+  set_writer(writer);
+}
 
 QboneServerSession::~QboneServerSession() {}
 
+void QboneServerSession::set_writer(QbonePacketWriter* absl_nullable writer) {
+  writer_ = writer;
+  quic::AdjustTestValue("quic_QbonePacketWriter", &writer_);
+}
+
 std::unique_ptr<QuicCryptoStream> QboneServerSession::CreateCryptoStream() {
   return CreateCryptoServerStream(quic_crypto_server_config_,
                                   compressed_certs_cache_, this,
                                   &stream_helper_);
 }
 
+void QboneServerSession::SendErrorPacketToNetwork(absl::string_view packet) {
+  SendPacketToNetwork(packet);
+}
+
 void QboneServerSession::CreateControlStream() {
   if (control_stream_ != nullptr) {
     return;
diff --git a/quiche/quic/qbone/qbone_server_session.h b/quiche/quic/qbone/qbone_server_session.h
index 7e150fc..ce1e12d 100644
--- a/quiche/quic/qbone/qbone_server_session.h
+++ b/quiche/quic/qbone/qbone_server_session.h
@@ -6,7 +6,10 @@
 #define QUICHE_QUIC_QBONE_QBONE_SERVER_SESSION_H_
 
 #include <cstdint>
+#include <memory>
 
+#include "absl/base/attributes.h"
+#include "absl/base/nullability.h"
 #include "absl/strings/string_view.h"
 #include "quiche/quic/core/quic_crypto_server_stream_base.h"
 #include "quiche/quic/core/quic_crypto_stream.h"
@@ -36,14 +39,18 @@
       public QbonePacketProcessor::OutputInterface,
       public QbonePacketProcessor::StatsInterface {
  public:
-  QboneServerSession(const quic::ParsedQuicVersionVector& supported_versions,
-                     QuicConnection* connection, Visitor* owner,
-                     const QuicConfig& config,
-                     const QuicCryptoServerConfig* quic_crypto_server_config,
-                     QuicCompressedCertsCache* compressed_certs_cache,
-                     QbonePacketWriter* writer, QuicIpAddress self_ip,
-                     QuicIpAddress client_ip, size_t client_ip_subnet_length,
-                     QboneServerControlStream::Handler* handler);
+  // `writer` may be nullptr, but a non-null writer must be given (through
+  // set_writer() or a test value override) before sending any packets to the
+  // network.
+  QboneServerSession(
+      const quic::ParsedQuicVersionVector& supported_versions,
+      QuicConnection* connection, Visitor* owner, const QuicConfig& config,
+      const QuicCryptoServerConfig* quic_crypto_server_config,
+      QuicCompressedCertsCache* compressed_certs_cache,
+      QbonePacketWriter* absl_nullable writer ABSL_ATTRIBUTE_LIFETIME_BOUND,
+      QuicIpAddress self_ip, QuicIpAddress client_ip,
+      size_t client_ip_subnet_length,
+      QboneServerControlStream::Handler* handler);
   QboneServerSession(const QboneServerSession&) = delete;
   QboneServerSession& operator=(const QboneServerSession&) = delete;
   ~QboneServerSession() override;
@@ -72,9 +79,13 @@
   void RecordThroughput(size_t bytes, QbonePacketProcessor::Direction direction,
                         uint8_t traffic_class) override {}
 
+  // `writer` must outlive the session.
+  void set_writer(QbonePacketWriter* absl_nullable writer);
+
  protected:
   // QboneSessionBase interface implementation.
   std::unique_ptr<QuicCryptoStream> CreateCryptoStream() override;
+  void SendErrorPacketToNetwork(absl::string_view packet) override;
 
   // Instantiates QboneServerControlStream.
   virtual void CreateControlStream();
@@ -90,6 +101,8 @@
   const QuicCryptoServerConfig* quic_crypto_server_config_;
 
  private:
+  QbonePacketWriter* absl_nullable writer_;
+
   // Used by QUIC crypto server stream to track most recently compressed certs.
   QuicCompressedCertsCache* compressed_certs_cache_;
   // This helper is needed when create QuicCryptoServerStream.
diff --git a/quiche/quic/qbone/qbone_session_base.cc b/quiche/quic/qbone/qbone_session_base.cc
index ebb3ced..b26831f 100644
--- a/quiche/quic/qbone/qbone_session_base.cc
+++ b/quiche/quic/qbone/qbone_session_base.cc
@@ -35,11 +35,9 @@
 
 QboneSessionBase::QboneSessionBase(
     QuicConnection* connection, Visitor* owner, const QuicConfig& config,
-    const ParsedQuicVersionVector& supported_versions,
-    QbonePacketWriter* writer)
+    const ParsedQuicVersionVector& supported_versions)
     : QuicSession(connection, owner, config, supported_versions,
                   /*num_expected_unidirectional_static_streams = */ 0) {
-  set_writer(writer);
   const uint32_t max_streams =
       (std::numeric_limits<uint32_t>::max() / kMaxAvailableStreamsMultiplier) -
       1;
@@ -152,8 +150,7 @@
 
         CreateIcmpPacket(header->ip6_dst, header->ip6_src, icmp_header, packet,
                          [this](absl::string_view icmp_packet) {
-                           writer_->WritePacketToNetwork(icmp_packet.data(),
-                                                         icmp_packet.size());
+                           SendErrorPacketToNetwork(icmp_packet);
                          });
         break;
       }
@@ -205,9 +202,4 @@
   return num_fallback_to_stream_;
 }
 
-void QboneSessionBase::set_writer(QbonePacketWriter* writer) {
-  writer_ = writer;
-  quic::AdjustTestValue("quic_QbonePacketWriter", &writer_);
-}
-
 }  // namespace quic
diff --git a/quiche/quic/qbone/qbone_session_base.h b/quiche/quic/qbone/qbone_session_base.h
index 3160f13..bd17dc5 100644
--- a/quiche/quic/qbone/qbone_session_base.h
+++ b/quiche/quic/qbone/qbone_session_base.h
@@ -5,6 +5,8 @@
 #ifndef QUICHE_QUIC_QBONE_QBONE_SESSION_BASE_H_
 #define QUICHE_QUIC_QBONE_QBONE_SESSION_BASE_H_
 
+#include <memory>
+
 #include "absl/strings/string_view.h"
 #include "quiche/quic/core/quic_crypto_server_stream_base.h"
 #include "quiche/quic/core/quic_crypto_stream.h"
@@ -20,8 +22,7 @@
  public:
   QboneSessionBase(QuicConnection* connection, Visitor* owner,
                    const QuicConfig& config,
-                   const ParsedQuicVersionVector& supported_versions,
-                   QbonePacketWriter* writer);
+                   const ParsedQuicVersionVector& supported_versions);
   QboneSessionBase(const QboneSessionBase&) = delete;
   QboneSessionBase& operator=(const QboneSessionBase&) = delete;
   ~QboneSessionBase() override;
@@ -54,13 +55,13 @@
   // session used an ephemeral stream instead.
   uint64_t GetNumFallbackToStream() const;
 
-  void set_writer(QbonePacketWriter* writer);
   void set_send_packets_as_datagrams(bool send_packets_as_datagrams) {
     send_packets_as_datagrams_ = send_packets_as_datagrams;
   }
 
  protected:
   virtual std::unique_ptr<QuicCryptoStream> CreateCryptoStream() = 0;
+  virtual void SendErrorPacketToNetwork(absl::string_view packet) = 0;
 
   // QuicSession interface implementation.
   QuicCryptoStream* GetMutableCryptoStream() override;
@@ -78,15 +79,11 @@
   // returns an unowned pointer to the stream for convenience.
   QuicStream* ActivateDataStream(std::unique_ptr<QuicStream> stream);
 
-  // Accepts a given packet from the network and writes it out
-  // to the QUIC stream. This will create an ephemeral stream per
-  // packet. This function will return true if a stream was created
-  // and the packet sent. It will return false if the stream could not
-  // be created.
+  // Accepts a given packet from the network and writes it out to the QUIC
+  // stream. This will send as a QUIC Datagram or create an ephemeral stream per
+  // packet.
   void SendPacketToPeer(absl::string_view packet);
 
-  QbonePacketWriter* writer_;
-
   // If true, send QUIC DATAGRAM (aka DATAGRAM) frames instead of ephemeral
   // streams. Note that receiving DATAGRAM frames is always supported.
   bool send_packets_as_datagrams_ = true;
diff --git a/quiche/quic/qbone/qbone_session_test.cc b/quiche/quic/qbone/qbone_session_test.cc
index de97a2b..6211a4c 100644
--- a/quiche/quic/qbone/qbone_session_test.cc
+++ b/quiche/quic/qbone/qbone_session_test.cc
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include <cstddef>
 #include <functional>
 #include <memory>
 #include <queue>
@@ -10,6 +11,7 @@
 #include <vector>
 
 #include "absl/strings/string_view.h"
+#include "quiche/quic/core/crypto/quic_crypto_server_config.h"
 #include "quiche/quic/core/io/quic_default_event_loop.h"
 #include "quiche/quic/core/io/quic_event_loop.h"
 #include "quiche/quic/core/proto/crypto_server_config_proto.h"
@@ -18,6 +20,7 @@
 #include "quiche/quic/platform/api/quic_expect_bug.h"
 #include "quiche/quic/platform/api/quic_test.h"
 #include "quiche/quic/platform/api/quic_test_loopback.h"
+#include "quiche/quic/qbone/mock_qbone_packet_exchanger.h"
 #include "quiche/quic/qbone/platform/icmp_packet.h"
 #include "quiche/quic/qbone/qbone_client_session.h"
 #include "quiche/quic/qbone/qbone_constants.h"
@@ -28,7 +31,6 @@
 #include "quiche/quic/test_tools/mock_clock.h"
 #include "quiche/quic/test_tools/mock_connection_id_generator.h"
 #include "quiche/quic/test_tools/quic_connection_peer.h"
-#include "quiche/quic/test_tools/quic_session_peer.h"
 #include "quiche/quic/test_tools/quic_test_utils.h"
 #include "quiche/common/quiche_callbacks.h"
 
@@ -42,6 +44,7 @@
 using ::testing::Eq;
 using ::testing::NiceMock;
 using ::testing::Not;
+using ::testing::StrictMock;
 
 std::string TestPacketIn(const std::string& body) {
   return PrependIPv6HeaderForTest(body, 5);
@@ -296,7 +299,6 @@
     helper_.AdvanceTime(QuicTime::Delta::FromMilliseconds(1000));
     event_loop_ = GetDefaultEventLoop()->Create(QuicDefaultClock::Get());
     alarm_factory_ = event_loop_->CreateAlarmFactory();
-    client_writer_ = std::make_unique<DataSavingQbonePacketWriter>();
     server_writer_ = std::make_unique<DataSavingQbonePacketWriter>();
     client_handler_ =
         std::make_unique<DataSavingQboneControlHandler<QboneClientRequest>>();
@@ -311,6 +313,13 @@
     }
 
     {
+      EXPECT_CALL(client_packet_exchanger_, WritePacket(_, _, _))
+          .WillRepeatedly(
+              [this](const char* packet, size_t size, std::string* error) {
+                client_packets_to_network_.push_back(std::string(packet, size));
+                return true;
+              });
+
       client_connection_ = new QuicConnection(
           TestConnectionId(), client_address, server_address, &helper_,
           alarm_factory_.get(), new NiceMock<MockPacketWriter>(), true,
@@ -326,7 +335,7 @@
       client_peer_ = std::make_unique<QboneClientSession>(
           client_connection_, client_crypto_config_.get(),
           /*owner=*/nullptr, config, supported_versions_,
-          QuicServerId("test.example.com", 1234), client_writer_.get(),
+          QuicServerId("test.example.com", 1234), &client_packet_exchanger_,
           client_handler_.get());
     }
 
@@ -437,7 +446,7 @@
     // anything yet.
     EXPECT_THAT(server_writer_->data(),
                 ElementsAre(TestPacketOut("hello"), TestPacketOut("world")));
-    EXPECT_TRUE(client_writer_->data().empty());
+    EXPECT_TRUE(client_packets_to_network_.empty());
     EXPECT_EQ(0u, server_peer_->GetNumActiveStreams());
     EXPECT_EQ(0u, client_peer_->GetNumActiveStreams());
 
@@ -449,7 +458,7 @@
     EXPECT_THAT(server_writer_->data(),
                 ElementsAre(TestPacketOut("hello"), TestPacketOut("world")));
     EXPECT_THAT(
-        client_writer_->data(),
+        client_packets_to_network_,
         ElementsAre(TestPacketOut("Hello Again"), TestPacketOut("Again")));
     EXPECT_EQ(0u, server_peer_->GetNumActiveStreams());
     EXPECT_EQ(0u, client_peer_->GetNumActiveStreams());
@@ -468,7 +477,8 @@
           server_peer_->connection()->GetGuaranteedLargestDatagramPayload(),
           TestPacketOut(long_data));
     } else {
-      EXPECT_THAT(client_writer_->data(), Contains(TestPacketOut(long_data)));
+      EXPECT_THAT(client_packets_to_network_,
+                  Contains(TestPacketOut(long_data)));
     }
     EXPECT_THAT(server_writer_->data(),
                 Not(Contains(TestPacketOut(long_data))));
@@ -480,7 +490,7 @@
     runner_.Run();
     if (use_datagrams) {
       ExpectICMPTooBigResponse(
-          client_writer_->data(),
+          client_packets_to_network_,
           client_peer_->connection()->GetGuaranteedLargestDatagramPayload(),
           TestPacketIn(long_data));
     } else {
@@ -533,7 +543,8 @@
 
   std::unique_ptr<QuicCryptoClientConfig> client_crypto_config_;
   std::unique_ptr<QuicCryptoServerConfig> server_crypto_config_;
-  std::unique_ptr<DataSavingQbonePacketWriter> client_writer_;
+  StrictMock<MockQbonePacketExchanger> client_packet_exchanger_;
+  std::vector<std::string> client_packets_to_network_;
   std::unique_ptr<DataSavingQbonePacketWriter> server_writer_;
   std::unique_ptr<DataSavingQboneControlHandler<QboneClientRequest>>
       client_handler_;
diff --git a/quiche/quic/qbone/qbone_stream_test.cc b/quiche/quic/qbone/qbone_stream_test.cc
index a19707e..8a83bb6 100644
--- a/quiche/quic/qbone/qbone_stream_test.cc
+++ b/quiche/quic/qbone/qbone_stream_test.cc
@@ -36,7 +36,7 @@
  public:
   MockQuicSession(QuicConnection* connection, const QuicConfig& config)
       : QboneSessionBase(connection, nullptr /*visitor*/, config,
-                         CurrentSupportedVersions(), nullptr /*writer*/) {}
+                         CurrentSupportedVersions()) {}
 
   ~MockQuicSession() override {}
 
@@ -85,6 +85,8 @@
     return std::make_unique<test::MockQuicCryptoStream>(this);
   }
 
+  MOCK_METHOD(void, SendErrorPacketToNetwork, (absl::string_view), (override));
+
   MOCK_METHOD(void, ProcessPacketFromPeer, (absl::string_view), (override));
   MOCK_METHOD(void, ProcessPacketFromNetwork, (absl::string_view), (override));