| // 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/bonnet/async_write_packet_exchanger.h" | |
| #include <netinet/icmp6.h> | |
| #include <netinet/ip6.h> | |
| #include <cerrno> | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <cstring> | |
| #include <memory> | |
| #include <string> | |
| #include <thread> // NOLINT (for open-sourceable thread ID) | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/base/thread_annotations.h" | |
| #include "absl/status/status.h" | |
| #include "absl/strings/string_view.h" | |
| #include "absl/synchronization/mutex.h" | |
| #include "absl/time/time.h" | |
| #include "absl/types/span.h" | |
| #include "quiche/quic/platform/api/quic_test.h" | |
| #include "quiche/quic/qbone/bonnet/mock_qbone_client_packet_exchanger.h" | |
| #include "quiche/quic/qbone/bonnet/qbone_client_packet_exchanger.h" | |
| #include "quiche/quic/qbone/bonnet/tun_device.h" | |
| #include "quiche/quic/qbone/platform/mock_kernel.h" | |
| #include "quiche/quic/qbone/qbone_constants.h" | |
| #include "quiche/common/platform/api/quiche_logging.h" | |
| #include "quiche/common/quiche_callbacks.h" | |
| #include "quiche/common/quiche_endian.h" | |
| namespace quic::test { | |
| namespace { | |
| constexpr size_t kMtu = 1000; | |
| constexpr int kReadFd = 15; | |
| constexpr int kWriteFd = 16; | |
| constexpr int64_t kMaxBufferSizeBytes = 1024 * 1024; | |
| constexpr int64_t kMaxResultsBufferSizeBytes = kMaxBufferSizeBytes; | |
| constexpr uint8_t kTestMac[ETH_ALEN] = {0x12, 0x12, 0x12, 0x12, 0x12, 0x12}; | |
| using ::absl_testing::IsOkAndHolds; | |
| using ::absl_testing::StatusIs; | |
| using ::quiche::QuicheEndian; | |
| using ::testing::_; | |
| using ::testing::ElementsAre; | |
| using ::testing::ElementsAreArray; | |
| using ::testing::Field; | |
| using ::testing::Mock; | |
| using ::testing::Ne; | |
| using ::testing::NiceMock; | |
| using ::testing::SizeIs; | |
| using ::testing::StrictMock; | |
| // Executor that collects callbacks and only runs them when released via | |
| // RunCallbacks(). | |
| class TestExecutor : public AsyncWritePacketExchanger::SiloExecutor { | |
| public: | |
| TestExecutor() = default; | |
| ~TestExecutor() override = default; | |
| void Stop() override { | |
| QUICHE_CHECK(IsOnSiloThread()); | |
| absl::MutexLock lock(mutex_); | |
| stopped_ = true; | |
| // While a real executor is expected to be able to handle the case of | |
| // stopping with pending callbacks, it's an error here because it means the | |
| // executor received unexpected callbacks. | |
| QUICHE_CHECK(callbacks_.empty()); | |
| } | |
| void ExecuteInSilo(quiche::SingleUseCallback<void()> callback) override { | |
| absl::MutexLock lock(mutex_); | |
| QUICHE_CHECK(!stopped_); | |
| callbacks_.push_back(std::move(callback)); | |
| } | |
| void WaitForCallback() { WaitForNCallbacks(1); } | |
| void WaitForNCallbacks(int n) { | |
| absl::MutexLock lock(mutex_); | |
| QUICHE_CHECK_LE(num_expected_callbacks_, 0); | |
| num_expected_callbacks_ = n; | |
| QUICHE_CHECK(mutex_.AwaitWithTimeout( | |
| absl::Condition(this, &TestExecutor::HasExpectedCallbacks), | |
| absl::Seconds(5))); | |
| QUICHE_CHECK(!stopped_); | |
| num_expected_callbacks_ = 0; | |
| } | |
| void RunCallbacks() { | |
| QUICHE_CHECK(IsOnSiloThread()); | |
| std::vector<quiche::SingleUseCallback<void()>> to_run; | |
| { | |
| absl::MutexLock lock(mutex_); | |
| QUICHE_CHECK(!stopped_); | |
| to_run.swap(callbacks_); | |
| } | |
| for (auto& cb : to_run) { | |
| std::move(cb)(); | |
| } | |
| } | |
| private: | |
| bool HasExpectedCallbacks() const ABSL_SHARED_LOCKS_REQUIRED(mutex_) { | |
| return stopped_ || callbacks_.size() >= num_expected_callbacks_; | |
| } | |
| bool IsOnSiloThread() const override { | |
| return std::this_thread::get_id() == thread_id_; | |
| } | |
| const std::thread::id thread_id_ = std::this_thread::get_id(); | |
| mutable absl::Mutex mutex_; | |
| bool stopped_ ABSL_GUARDED_BY(mutex_) = false; | |
| std::vector<quiche::SingleUseCallback<void()>> callbacks_ | |
| ABSL_GUARDED_BY(mutex_); | |
| int num_expected_callbacks_ ABSL_GUARDED_BY(mutex_) = 0; | |
| }; | |
| class AsyncWritePacketExchangerTest : public QuicTest { | |
| protected: | |
| AsyncWritePacketExchangerTest() { | |
| auto executor = std::make_unique<TestExecutor>(); | |
| executor_ = executor.get(); | |
| exchanger_ = std::make_unique<AsyncWritePacketExchanger>( | |
| kMtu, &mock_kernel_, &mock_visitor_, kMaxBufferSizeBytes, | |
| kMaxResultsBufferSizeBytes, std::move(executor)); | |
| } | |
| ~AsyncWritePacketExchangerTest() override = default; | |
| void ValidateOnMainThread() { | |
| QUICHE_CHECK_EQ(std::this_thread::get_id(), thread_id_); | |
| } | |
| void ValidateOffThread() { | |
| QUICHE_CHECK_NE(std::this_thread::get_id(), thread_id_); | |
| } | |
| const std::thread::id thread_id_ = std::this_thread::get_id(); | |
| StrictMock<MockKernel> mock_kernel_; | |
| StrictMock<MockQboneClientPacketExchanger::MockVisitor> mock_visitor_; | |
| TestExecutor* executor_; | |
| std::unique_ptr<AsyncWritePacketExchanger> exchanger_; | |
| }; | |
| TEST_F(AsyncWritePacketExchangerTest, WritePacket) { | |
| exchanger_->Start(kReadFd, kWriteFd, /*exchanger=*/nullptr); | |
| std::string packet = "fake packet"; | |
| EXPECT_CALL(mock_kernel_, writev(kWriteFd, _, 2)) | |
| .WillOnce([this, &packet](int /*fd*/, const struct iovec* iov, | |
| int /*iovcnt*/) -> ssize_t { | |
| ValidateOffThread(); | |
| EXPECT_EQ(iov[0].iov_base, nullptr); | |
| EXPECT_EQ(iov[0].iov_len, 0); | |
| EXPECT_EQ(absl::string_view(static_cast<const char*>(iov[1].iov_base), | |
| iov[1].iov_len), | |
| packet); | |
| EXPECT_EQ(iov[1].iov_len, packet.size()); | |
| return packet.size(); | |
| }); | |
| exchanger_->WritePacketToNetwork(absl::MakeConstSpan( | |
| reinterpret_cast<const std::byte*>(packet.data()), packet.size())); | |
| executor_->WaitForCallback(); | |
| EXPECT_TRUE(Mock::VerifyAndClear(&mock_visitor_)); | |
| EXPECT_CALL( | |
| mock_visitor_, | |
| OnWrite(IsOkAndHolds(ElementsAre(Field( | |
| &QboneClientPacketExchanger::WriteResult::packet, | |
| ElementsAreArray(reinterpret_cast<const std::byte*>(packet.data()), | |
| packet.size())))))); | |
| executor_->RunCallbacks(); | |
| exchanger_->Stop(); | |
| } | |
| TEST_F(AsyncWritePacketExchangerTest, WritePacketError) { | |
| exchanger_->Start(kReadFd, kWriteFd, /*exchanger=*/nullptr); | |
| std::string packet = "fake packet"; | |
| EXPECT_CALL(mock_kernel_, writev(kWriteFd, _, 2)) | |
| .WillOnce([this, &packet](int /*fd*/, const struct iovec* iov, | |
| int /*iovcnt*/) -> ssize_t { | |
| ValidateOffThread(); | |
| EXPECT_EQ(iov[0].iov_base, nullptr); | |
| EXPECT_EQ(iov[0].iov_len, 0); | |
| EXPECT_EQ(absl::string_view(static_cast<const char*>(iov[1].iov_base), | |
| iov[1].iov_len), | |
| packet); | |
| EXPECT_EQ(iov[1].iov_len, packet.size()); | |
| errno = EAGAIN; | |
| return -1; | |
| }); | |
| exchanger_->WritePacketToNetwork(absl::MakeConstSpan( | |
| reinterpret_cast<const std::byte*>(packet.data()), packet.size())); | |
| executor_->WaitForCallback(); | |
| EXPECT_TRUE(Mock::VerifyAndClear(&mock_visitor_)); | |
| EXPECT_CALL(mock_visitor_, OnWrite(StatusIs(Ne(absl::StatusCode::kOk)))); | |
| executor_->RunCallbacks(); | |
| exchanger_->Stop(); | |
| } | |
| TEST_F(AsyncWritePacketExchangerTest, RestartExchanger) { | |
| exchanger_->Start(kReadFd, kWriteFd, /*exchanger=*/nullptr); | |
| exchanger_->Stop(); | |
| exchanger_->Start(kReadFd, kWriteFd, /*exchanger=*/nullptr); | |
| std::string packet = "fake packet"; | |
| EXPECT_CALL(mock_kernel_, writev(kWriteFd, _, 2)) | |
| .WillOnce([this, &packet](int /*fd*/, const struct iovec* iov, | |
| int /*iovcnt*/) -> ssize_t { | |
| ValidateOffThread(); | |
| EXPECT_EQ(iov[0].iov_base, nullptr); | |
| EXPECT_EQ(iov[0].iov_len, 0); | |
| EXPECT_EQ(absl::string_view(static_cast<const char*>(iov[1].iov_base), | |
| iov[1].iov_len), | |
| packet); | |
| EXPECT_EQ(iov[1].iov_len, packet.size()); | |
| return packet.size(); | |
| }); | |
| exchanger_->WritePacketToNetwork(absl::MakeConstSpan( | |
| reinterpret_cast<const std::byte*>(packet.data()), packet.size())); | |
| executor_->WaitForCallback(); | |
| EXPECT_TRUE(Mock::VerifyAndClear(&mock_visitor_)); | |
| EXPECT_CALL( | |
| mock_visitor_, | |
| OnWrite(IsOkAndHolds(ElementsAre(Field( | |
| &QboneClientPacketExchanger::WriteResult::packet, | |
| ElementsAreArray(reinterpret_cast<const std::byte*>(packet.data()), | |
| packet.size())))))); | |
| executor_->RunCallbacks(); | |
| exchanger_->Stop(); | |
| } | |
| TEST_F(AsyncWritePacketExchangerTest, ReadPacket) { | |
| exchanger_->Start(kReadFd, kWriteFd, /*exchanger=*/nullptr); | |
| std::string packet = "fake_packet"; | |
| EXPECT_CALL(mock_kernel_, readv(kReadFd, _, 2)) | |
| .WillOnce( | |
| [this, packet](int /*fd*/, const struct iovec* iov, int /*iovcnt*/) { | |
| ValidateOnMainThread(); | |
| EXPECT_EQ(iov[0].iov_len, 0); | |
| EXPECT_EQ(iov[1].iov_len, kMtu); | |
| ::memcpy(iov[1].iov_base, packet.data(), packet.size()); | |
| return packet.size(); | |
| }); | |
| EXPECT_CALL( | |
| mock_visitor_, | |
| OnRead(IsOkAndHolds(ElementsAre(Field( | |
| &QboneClientPacketExchanger::ReadResult::packet, | |
| ElementsAreArray(reinterpret_cast<const std::byte*>(packet.data()), | |
| packet.size())))))); | |
| EXPECT_EQ(exchanger_->OnReadFromNetworkReady(/*max_packets_to_read=*/1), 1); | |
| exchanger_->Stop(); | |
| } | |
| TEST_F(AsyncWritePacketExchangerTest, ReadPacketError) { | |
| exchanger_->Start(kReadFd, kWriteFd, /*exchanger=*/nullptr); | |
| EXPECT_CALL(mock_kernel_, readv(kReadFd, _, 2)) | |
| .WillOnce( | |
| [this](int /*fd*/, const struct iovec* /*iov*/, int /*iovcnt*/) { | |
| ValidateOnMainThread(); | |
| errno = ECOMM; | |
| return -1; | |
| }); | |
| EXPECT_CALL(mock_visitor_, OnRead(StatusIs(Ne(absl::StatusCode::kOk)))); | |
| EXPECT_EQ(exchanger_->OnReadFromNetworkReady(/*max_packets_to_read=*/1), 0); | |
| exchanger_->Stop(); | |
| } | |
| TEST_F(AsyncWritePacketExchangerTest, ReadPacketBlocked) { | |
| exchanger_->Start(kReadFd, kWriteFd, /*exchanger=*/nullptr); | |
| EXPECT_CALL(mock_kernel_, readv(kReadFd, _, 2)) | |
| .WillOnce( | |
| [this](int /*fd*/, const struct iovec* /*iov*/, int /*iovcnt*/) { | |
| ValidateOnMainThread(); | |
| errno = EAGAIN; | |
| return -1; | |
| }); | |
| EXPECT_CALL(mock_visitor_, OnRead(StatusIs(Ne(absl::StatusCode::kOk)))); | |
| EXPECT_EQ(exchanger_->OnReadFromNetworkReady(/*max_packets_to_read=*/1), 0); | |
| exchanger_->Stop(); | |
| } | |
| TEST_F(AsyncWritePacketExchangerTest, WriteBufferFull) { | |
| auto executor = std::make_unique<TestExecutor>(); | |
| executor_ = executor.get(); | |
| // Create an exchanger with a very small buffer. | |
| exchanger_ = std::make_unique<AsyncWritePacketExchanger>( | |
| kMtu, &mock_kernel_, &mock_visitor_, /*max_buffer_size_bytes=*/5, | |
| kMaxResultsBufferSizeBytes, std::move(executor)); | |
| exchanger_->Start(kReadFd, kWriteFd, /*exchanger=*/nullptr); | |
| std::string packet = "fake packet"; // size 11 | |
| // Write should immediately fail and call OnWrite because buffer is full (max | |
| // size 5, packet is 11) | |
| EXPECT_CALL(mock_visitor_, | |
| OnWrite(StatusIs(absl::StatusCode::kResourceExhausted))); | |
| exchanger_->WritePacketToNetwork(absl::MakeConstSpan( | |
| reinterpret_cast<const std::byte*>(packet.data()), packet.size())); | |
| exchanger_->Stop(); | |
| } | |
| TEST_F(AsyncWritePacketExchangerTest, WriteResultBufferFull) { | |
| auto executor = std::make_unique<TestExecutor>(); | |
| executor_ = executor.get(); | |
| // Create an exchanger with a very small response buffer. | |
| exchanger_ = std::make_unique<AsyncWritePacketExchanger>( | |
| kMtu, &mock_kernel_, &mock_visitor_, kMaxBufferSizeBytes, | |
| /*max_result_buffer_size_bytes=*/5, std::move(executor)); | |
| exchanger_->Start(kReadFd, kWriteFd, /*exchanger=*/nullptr); | |
| std::string packet = "fake packet"; // size 11 | |
| EXPECT_CALL(mock_kernel_, writev(kWriteFd, _, 2)) | |
| .WillOnce([this, &packet](int /*fd*/, const struct iovec* iov, | |
| int /*iovcnt*/) -> ssize_t { | |
| ValidateOffThread(); | |
| EXPECT_EQ(iov[0].iov_base, nullptr); | |
| EXPECT_EQ(iov[0].iov_len, 0); | |
| EXPECT_EQ(absl::string_view(static_cast<const char*>(iov[1].iov_base), | |
| iov[1].iov_len), | |
| packet); | |
| EXPECT_EQ(iov[1].iov_len, packet.size()); | |
| return packet.size(); | |
| }); | |
| exchanger_->WritePacketToNetwork(absl::MakeConstSpan( | |
| reinterpret_cast<const std::byte*>(packet.data()), packet.size())); | |
| executor_->WaitForCallback(); | |
| EXPECT_TRUE(Mock::VerifyAndClear(&mock_visitor_)); | |
| // Expect no interaction with the visitor as no results were added to the | |
| // result buffer. | |
| executor_->RunCallbacks(); | |
| exchanger_->Stop(); | |
| } | |
| TEST_F(AsyncWritePacketExchangerTest, StopWithPendingWrites) { | |
| exchanger_->Start(kReadFd, kWriteFd, /*exchanger=*/nullptr); | |
| std::string packet = "fake packet"; | |
| EXPECT_CALL(mock_kernel_, writev(kWriteFd, _, 2)) | |
| .WillOnce([this, &packet](int /*fd*/, const struct iovec* iov, | |
| int /*iovcnt*/) -> ssize_t { | |
| ValidateOffThread(); | |
| EXPECT_EQ(iov[0].iov_base, nullptr); | |
| EXPECT_EQ(iov[0].iov_len, 0); | |
| EXPECT_EQ(absl::string_view(static_cast<const char*>(iov[1].iov_base), | |
| iov[1].iov_len), | |
| packet); | |
| EXPECT_EQ(iov[1].iov_len, packet.size()); | |
| return packet.size(); | |
| }); | |
| EXPECT_CALL( | |
| mock_visitor_, | |
| OnWrite(IsOkAndHolds(ElementsAre(Field( | |
| &QboneClientPacketExchanger::WriteResult::packet, | |
| ElementsAreArray(reinterpret_cast<const std::byte*>(packet.data()), | |
| packet.size())))))); | |
| exchanger_->WritePacketToNetwork(absl::MakeConstSpan( | |
| reinterpret_cast<const std::byte*>(packet.data()), packet.size())); | |
| // Immediately call Stop() without waiting for completion/callbacks. Expect to | |
| // cleanly block on completion and run visitor callbacks. | |
| exchanger_->Stop(); | |
| // Executor may have stale callbacks queued up, but visitor callbacks should | |
| // already have been made. | |
| EXPECT_TRUE(Mock::VerifyAndClear(&mock_visitor_)); | |
| executor_->RunCallbacks(); | |
| } | |
| // Neighbor solicitation packets over a TAP interface are expected to be | |
| // immediately responded to. Ensure that response is correctly handled | |
| // off-thread. | |
| TEST_F(AsyncWritePacketExchangerTest, ReadNeighborSolicitationPacket) { | |
| auto executor = std::make_unique<TestExecutor>(); | |
| executor_ = executor.get(); | |
| // Create an exchanger with TAP enabled. | |
| exchanger_ = std::make_unique<AsyncWritePacketExchanger>( | |
| kMtu, &mock_kernel_, &mock_visitor_, kMaxBufferSizeBytes, | |
| kMaxResultsBufferSizeBytes, std::move(executor)); | |
| exchanger_->Start(kReadFd, kWriteFd, /*exchanger=*/nullptr, | |
| CreateTapEthHdr(kTestMac)); | |
| ip6_hdr ip_hdr{}; | |
| ip_hdr.ip6_vfc = 0x60; // Version 6 | |
| ip_hdr.ip6_nxt = IPPROTO_ICMPV6; | |
| inet_pton(AF_INET6, "fe80::2", &ip_hdr.ip6_src); | |
| inet_pton(AF_INET6, "fe80::1", &ip_hdr.ip6_dst); | |
| icmp6_hdr icmp_hdr{}; | |
| icmp_hdr.icmp6_type = ND_NEIGHBOR_SOLICIT; | |
| in6_addr target_address = QboneConstants::GatewayAddress()->GetIPv6(); | |
| std::vector<std::byte> l3_packet(sizeof(ip_hdr) + sizeof(icmp_hdr) + | |
| sizeof(target_address)); | |
| ::memcpy(l3_packet.data(), &ip_hdr, sizeof(ip_hdr)); | |
| ::memcpy(l3_packet.data() + sizeof(ip_hdr), &icmp_hdr, sizeof(icmp_hdr)); | |
| ::memcpy(l3_packet.data() + sizeof(ip_hdr) + sizeof(icmp_hdr), | |
| &target_address, sizeof(target_address)); | |
| ethhdr eth_hdr{}; | |
| eth_hdr.h_proto = QuicheEndian::HostToNet16(ETH_P_IPV6); | |
| EXPECT_CALL(mock_kernel_, readv(kReadFd, _, 2)) | |
| .WillOnce([this, l3_packet, eth_hdr](int /*fd*/, const struct iovec* iov, | |
| int /*iovcnt*/) { | |
| ValidateOnMainThread(); | |
| EXPECT_EQ(iov[0].iov_len, ETH_HLEN); | |
| ::memcpy(iov[0].iov_base, ð_hdr, ETH_HLEN); | |
| EXPECT_EQ(iov[1].iov_len, kMtu); | |
| ::memcpy(iov[1].iov_base, l3_packet.data(), l3_packet.size()); | |
| return ETH_HLEN + l3_packet.size(); | |
| }); | |
| // Expect neighbor solicitation response to be written out asynchronously. | |
| EXPECT_CALL(mock_kernel_, writev(kWriteFd, _, 2)) | |
| .WillOnce([this](int fd, const struct iovec* iov, int iovcnt) -> ssize_t { | |
| ValidateOffThread(); | |
| return iov[0].iov_len + iov[1].iov_len; | |
| }); | |
| EXPECT_EQ(exchanger_->OnReadFromNetworkReady(/*max_packets_to_read=*/1), 1); | |
| executor_->WaitForCallback(); | |
| EXPECT_TRUE(Mock::VerifyAndClear(&mock_visitor_)); | |
| // Because the read packet is link-local and immediately responded to, expect | |
| // the visitor to be called with a *write* result. | |
| EXPECT_CALL(mock_visitor_, OnWrite(IsOkAndHolds(SizeIs(1)))); | |
| executor_->RunCallbacks(); | |
| exchanger_->Stop(); | |
| } | |
| // Ensure that if an outer exchanger is provided, it is used for internal | |
| // writes, e.g. neighbor solicitation responses. | |
| TEST_F(AsyncWritePacketExchangerTest, | |
| ReadNeighborSolicitationPacketWithOuterExchanger) { | |
| auto executor = std::make_unique<TestExecutor>(); | |
| executor_ = executor.get(); | |
| // Create an exchanger with TAP enabled. | |
| exchanger_ = std::make_unique<AsyncWritePacketExchanger>( | |
| kMtu, &mock_kernel_, &mock_visitor_, kMaxBufferSizeBytes, | |
| kMaxResultsBufferSizeBytes, std::move(executor)); | |
| StrictMock<MockQboneClientPacketExchanger> mock_outer_exchanger; | |
| exchanger_->Start(kReadFd, kWriteFd, &mock_outer_exchanger, | |
| CreateTapEthHdr(kTestMac)); | |
| ip6_hdr ip_hdr{}; | |
| ip_hdr.ip6_vfc = 0x60; // Version 6 | |
| ip_hdr.ip6_nxt = IPPROTO_ICMPV6; | |
| inet_pton(AF_INET6, "fe80::2", &ip_hdr.ip6_src); | |
| inet_pton(AF_INET6, "fe80::1", &ip_hdr.ip6_dst); | |
| icmp6_hdr icmp_hdr{}; | |
| icmp_hdr.icmp6_type = ND_NEIGHBOR_SOLICIT; | |
| in6_addr target_address = QboneConstants::GatewayAddress()->GetIPv6(); | |
| std::vector<std::byte> l3_packet(sizeof(ip_hdr) + sizeof(icmp_hdr) + | |
| sizeof(target_address)); | |
| ::memcpy(l3_packet.data(), &ip_hdr, sizeof(ip_hdr)); | |
| ::memcpy(l3_packet.data() + sizeof(ip_hdr), &icmp_hdr, sizeof(icmp_hdr)); | |
| ::memcpy(l3_packet.data() + sizeof(ip_hdr) + sizeof(icmp_hdr), | |
| &target_address, sizeof(target_address)); | |
| ethhdr eth_hdr{}; | |
| eth_hdr.h_proto = QuicheEndian::HostToNet16(ETH_P_IPV6); | |
| EXPECT_CALL(mock_kernel_, readv(kReadFd, _, 2)) | |
| .WillOnce([this, l3_packet, eth_hdr](int /*fd*/, const struct iovec* iov, | |
| int /*iovcnt*/) { | |
| ValidateOnMainThread(); | |
| EXPECT_EQ(iov[0].iov_len, ETH_HLEN); | |
| ::memcpy(iov[0].iov_base, ð_hdr, ETH_HLEN); | |
| EXPECT_EQ(iov[1].iov_len, kMtu); | |
| ::memcpy(iov[1].iov_base, l3_packet.data(), l3_packet.size()); | |
| return ETH_HLEN + l3_packet.size(); | |
| }); | |
| // Expect neighbor solicitation response to be written via the outer | |
| // exchanger. | |
| EXPECT_CALL(mock_outer_exchanger, WritePacketToNetwork(_)); | |
| // No visitor callback expected because that is handled by the outer | |
| // exchanger, here a mock that doesn't actually do it. | |
| EXPECT_EQ(exchanger_->OnReadFromNetworkReady(/*max_packets_to_read=*/1), 1); | |
| exchanger_->Stop(); | |
| } | |
| } // namespace | |
| } // namespace quic::test |