QBONE Bonnet: pull netlink usage out of packet exchanger

We had organically evolved to a point where every queue silo needed its own netlink to hand to a packet exchanger to craft `ethhdr`s, even though the header was uniform and constant.

Instead, in TAP mode, `TunTapDevice` builds its own eth_hdr via `ioctl(SIOCGIFHWADDR)` once the device is opened, which is passed as a parameter to any override or wrapper of `QboneClientPacketExchanger::Start`

Additionally move the eBPF loader creation up one level so that `QboneTunnel` can be agnostic as to whether the underlying device is TAP vs. TUN

PiperOrigin-RevId: 1003988200
diff --git a/quiche/quic/qbone/bonnet/async_write_packet_exchanger.cc b/quiche/quic/qbone/bonnet/async_write_packet_exchanger.cc
index 5fdabd0..4cc546f 100644
--- a/quiche/quic/qbone/bonnet/async_write_packet_exchanger.cc
+++ b/quiche/quic/qbone/bonnet/async_write_packet_exchanger.cc
@@ -7,6 +7,7 @@
 #include <cstddef>
 #include <cstdint>
 #include <memory>
+#include <optional>
 #include <queue>
 #include <thread>  // NOLINT (for open-sourceable thread ID)
 #include <utility>
@@ -22,8 +23,8 @@
 #include "absl/types/span.h"
 #include "quiche/quic/platform/api/quic_thread.h"
 #include "quiche/quic/qbone/bonnet/qbone_client_packet_exchanger.h"
+#include "quiche/quic/qbone/bonnet/tun_device_packet_exchanger.h"
 #include "quiche/quic/qbone/platform/kernel_interface.h"
-#include "quiche/quic/qbone/platform/netlink_interface.h"
 #include "quiche/common/platform/api/quiche_bug_tracker.h"
 #include "quiche/common/platform/api/quiche_logging.h"
 
@@ -232,8 +233,7 @@
 
 AsyncWritePacketExchanger::AsyncWritePacketExchanger(
     size_t mtu, KernelInterface* absl_nonnull kernel,
-    NetlinkInterface* absl_nonnull netlink, Visitor* absl_nonnull visitor,
-    bool is_tap, absl::string_view ifname, int64_t max_buffer_size_bytes,
+    Visitor* absl_nonnull visitor, int64_t max_buffer_size_bytes,
     int64_t max_result_buffer_size_bytes,
     absl_nonnull std::unique_ptr<SiloExecutor> silo_executor)
     : max_buffer_size_bytes_(max_buffer_size_bytes),
@@ -241,8 +241,7 @@
       silo_executor_(std::move(silo_executor)),
       visitor_(*visitor),
       intermediate_visitor_(std::make_unique<IntermediateVisitor>(this)),
-      thread_local_exchanger_(mtu, kernel, netlink, intermediate_visitor_.get(),
-                              is_tap, ifname) {}
+      thread_local_exchanger_(mtu, kernel, intermediate_visitor_.get()) {}
 
 AsyncWritePacketExchanger::~AsyncWritePacketExchanger() {
   QUICHE_DCHECK(silo_executor_->IsOnSiloThread());
@@ -255,14 +254,15 @@
 
 void AsyncWritePacketExchanger::Start(
     int read_fd, int write_fd,
-    QboneClientPacketExchanger* absl_nullable exchanger) {
+    QboneClientPacketExchanger* absl_nullable exchanger,
+    std::optional<ethhdr> eth_hdr) {
   QUICHE_DCHECK(silo_executor_->IsOnSiloThread());
 
   if (exchanger == nullptr) {
     exchanger = this;
   }
 
-  thread_local_exchanger_.Start(read_fd, write_fd, exchanger);
+  thread_local_exchanger_.Start(read_fd, write_fd, exchanger, eth_hdr);
 
   // Allow idempotent Start() calls (assuming `thread_local_exchanger_`
   // validates same inputs).
diff --git a/quiche/quic/qbone/bonnet/async_write_packet_exchanger.h b/quiche/quic/qbone/bonnet/async_write_packet_exchanger.h
index bb69645..49e50b4 100644
--- a/quiche/quic/qbone/bonnet/async_write_packet_exchanger.h
+++ b/quiche/quic/qbone/bonnet/async_write_packet_exchanger.h
@@ -5,9 +5,12 @@
 #ifndef QUICHE_QUIC_QBONE_BONNET_ASYNC_WRITE_PACKET_EXCHANGER_H_
 #define QUICHE_QUIC_QBONE_BONNET_ASYNC_WRITE_PACKET_EXCHANGER_H_
 
+#include <linux/if_ether.h>
+
 #include <cstddef>
 #include <cstdint>
 #include <memory>
+#include <optional>
 #include <queue>
 #include <vector>
 
@@ -15,14 +18,12 @@
 #include "absl/base/nullability.h"
 #include "absl/base/thread_annotations.h"
 #include "absl/status/statusor.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/qbone/bonnet/qbone_client_packet_exchanger.h"
 #include "quiche/quic/qbone/bonnet/tun_device_packet_exchanger.h"
 #include "quiche/quic/qbone/platform/kernel_interface.h"
-#include "quiche/quic/qbone/platform/netlink_interface.h"
 #include "quiche/common/quiche_callbacks.h"
 
 namespace quic {
@@ -55,16 +56,16 @@
   AsyncWritePacketExchanger(
       size_t mtu,
       KernelInterface* absl_nonnull kernel ABSL_ATTRIBUTE_LIFETIME_BOUND,
-      NetlinkInterface* absl_nonnull netlink ABSL_ATTRIBUTE_LIFETIME_BOUND,
-      Visitor* absl_nonnull visitor ABSL_ATTRIBUTE_LIFETIME_BOUND, bool is_tap,
-      absl::string_view ifname, int64_t max_buffer_size_bytes,
-      int64_t max_result_buffer_size_bytes,
+      Visitor* absl_nonnull visitor ABSL_ATTRIBUTE_LIFETIME_BOUND,
+      int64_t max_buffer_size_bytes, int64_t max_result_buffer_size_bytes,
       absl_nonnull std::unique_ptr<SiloExecutor> silo_executor);
+
   ~AsyncWritePacketExchanger() override;
 
   // QboneClientPacketExchanger:
   void Start(int read_fd, int write_fd,
-             QboneClientPacketExchanger* absl_nullable exchanger) override;
+             QboneClientPacketExchanger* absl_nullable exchanger,
+             std::optional<ethhdr> eth_hdr = std::nullopt) override;
   void Stop() override;
   int OnReadFromNetworkReady(int max_packets_to_read) override;
   void WritePacketToNetwork(absl::Span<const std::byte> packet) override;
diff --git a/quiche/quic/qbone/bonnet/async_write_packet_exchanger_test.cc b/quiche/quic/qbone/bonnet/async_write_packet_exchanger_test.cc
index d1cb113..3f42eac 100644
--- a/quiche/quic/qbone/bonnet/async_write_packet_exchanger_test.cc
+++ b/quiche/quic/qbone/bonnet/async_write_packet_exchanger_test.cc
@@ -26,8 +26,8 @@
 #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/platform/mock_netlink.h"
 #include "quiche/quic/qbone/qbone_constants.h"
 #include "quiche/common/platform/api/quiche_logging.h"
 #include "quiche/common/quiche_callbacks.h"
@@ -41,6 +41,7 @@
 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;
@@ -133,9 +134,8 @@
     auto executor = std::make_unique<TestExecutor>();
     executor_ = executor.get();
     exchanger_ = std::make_unique<AsyncWritePacketExchanger>(
-        kMtu, &mock_kernel_, &mock_netlink_, &mock_visitor_, /*is_tap=*/false,
-        "ifname", kMaxBufferSizeBytes, kMaxResultsBufferSizeBytes,
-        std::move(executor));
+        kMtu, &mock_kernel_, &mock_visitor_, kMaxBufferSizeBytes,
+        kMaxResultsBufferSizeBytes, std::move(executor));
   }
 
   ~AsyncWritePacketExchangerTest() override = default;
@@ -151,7 +151,6 @@
   const std::thread::id thread_id_ = std::this_thread::get_id();
 
   StrictMock<MockKernel> mock_kernel_;
-  NiceMock<MockNetlink> mock_netlink_;
   StrictMock<MockQboneClientPacketExchanger::MockVisitor> mock_visitor_;
   TestExecutor* executor_;
   std::unique_ptr<AsyncWritePacketExchanger> exchanger_;
@@ -324,8 +323,7 @@
 
   // Create an exchanger with a very small buffer.
   exchanger_ = std::make_unique<AsyncWritePacketExchanger>(
-      kMtu, &mock_kernel_, &mock_netlink_, &mock_visitor_, false,
-      absl::string_view(), /*max_buffer_size_bytes=*/5,
+      kMtu, &mock_kernel_, &mock_visitor_, /*max_buffer_size_bytes=*/5,
       kMaxResultsBufferSizeBytes, std::move(executor));
   exchanger_->Start(kReadFd, kWriteFd, /*exchanger=*/nullptr);
 
@@ -348,8 +346,7 @@
 
   // Create an exchanger with a very small response buffer.
   exchanger_ = std::make_unique<AsyncWritePacketExchanger>(
-      kMtu, &mock_kernel_, &mock_netlink_, &mock_visitor_, false,
-      absl::string_view(), kMaxBufferSizeBytes,
+      kMtu, &mock_kernel_, &mock_visitor_, kMaxBufferSizeBytes,
       /*max_result_buffer_size_bytes=*/5, std::move(executor));
   exchanger_->Start(kReadFd, kWriteFd, /*exchanger=*/nullptr);
 
@@ -427,11 +424,11 @@
 
   // Create an exchanger with TAP enabled.
   exchanger_ = std::make_unique<AsyncWritePacketExchanger>(
-      kMtu, &mock_kernel_, &mock_netlink_, &mock_visitor_, /*is_tap=*/true,
-      absl::string_view(), kMaxBufferSizeBytes, kMaxResultsBufferSizeBytes,
-      std::move(executor));
+      kMtu, &mock_kernel_, &mock_visitor_, kMaxBufferSizeBytes,
+      kMaxResultsBufferSizeBytes, std::move(executor));
 
-  exchanger_->Start(kReadFd, kWriteFd, /*exchanger=*/nullptr);
+  exchanger_->Start(kReadFd, kWriteFd, /*exchanger=*/nullptr,
+                    CreateTapEthHdr(kTestMac));
 
   ip6_hdr ip_hdr{};
   ip_hdr.ip6_vfc = 0x60;  // Version 6
@@ -494,12 +491,12 @@
 
   // Create an exchanger with TAP enabled.
   exchanger_ = std::make_unique<AsyncWritePacketExchanger>(
-      kMtu, &mock_kernel_, &mock_netlink_, &mock_visitor_, /*is_tap=*/true,
-      absl::string_view(), kMaxBufferSizeBytes, kMaxResultsBufferSizeBytes,
-      std::move(executor));
+      kMtu, &mock_kernel_, &mock_visitor_, kMaxBufferSizeBytes,
+      kMaxResultsBufferSizeBytes, std::move(executor));
 
   StrictMock<MockQboneClientPacketExchanger> mock_outer_exchanger;
-  exchanger_->Start(kReadFd, kWriteFd, &mock_outer_exchanger);
+  exchanger_->Start(kReadFd, kWriteFd, &mock_outer_exchanger,
+                    CreateTapEthHdr(kTestMac));
 
   ip6_hdr ip_hdr{};
   ip_hdr.ip6_vfc = 0x60;  // Version 6
diff --git a/quiche/quic/qbone/bonnet/mock_qbone_client_packet_exchanger.h b/quiche/quic/qbone/bonnet/mock_qbone_client_packet_exchanger.h
index 5581d7c..233a74c 100644
--- a/quiche/quic/qbone/bonnet/mock_qbone_client_packet_exchanger.h
+++ b/quiche/quic/qbone/bonnet/mock_qbone_client_packet_exchanger.h
@@ -6,6 +6,7 @@
 #define QUICHE_QUIC_QBONE_MOCK_QBONE_PACKET_EXCHANGER_H_
 
 #include <cstddef>
+#include <optional>
 #include <vector>
 
 #include "absl/base/nullability.h"
@@ -32,7 +33,8 @@
 
   MOCK_METHOD(void, Start,
               (int read_fd, int write_fd,
-               QboneClientPacketExchanger* absl_nullable exchanger),
+               QboneClientPacketExchanger* absl_nullable exchanger,
+               std::optional<ethhdr> eth_hdr),
               (override));
   MOCK_METHOD(void, Stop, (), (override));
   MOCK_METHOD(int, OnReadFromNetworkReady, (int max_packets_to_read),
diff --git a/quiche/quic/qbone/bonnet/mock_tun_device.h b/quiche/quic/qbone/bonnet/mock_tun_device.h
index 5e086ee..c245f9a 100644
--- a/quiche/quic/qbone/bonnet/mock_tun_device.h
+++ b/quiche/quic/qbone/bonnet/mock_tun_device.h
@@ -5,6 +5,8 @@
 #ifndef QUICHE_QUIC_QBONE_BONNET_MOCK_TUN_DEVICE_H_
 #define QUICHE_QUIC_QBONE_BONNET_MOCK_TUN_DEVICE_H_
 
+#include <optional>
+
 #include "quiche/quic/platform/api/quic_test.h"
 #include "quiche/quic/qbone/bonnet/tun_device_interface.h"
 
@@ -24,6 +26,8 @@
   MOCK_METHOD(int, GetWriteFileDescriptor, (), (const, override));
 
   MOCK_METHOD(int, OpenQueue, (), (override));
+
+  MOCK_METHOD(std::optional<ethhdr>, eth_hdr, (), (const, override));
 };
 
 }  // namespace quic
diff --git a/quiche/quic/qbone/bonnet/qbone_client_packet_exchanger.h b/quiche/quic/qbone/bonnet/qbone_client_packet_exchanger.h
index 7763850..fcef539 100644
--- a/quiche/quic/qbone/bonnet/qbone_client_packet_exchanger.h
+++ b/quiche/quic/qbone/bonnet/qbone_client_packet_exchanger.h
@@ -5,14 +5,16 @@
 #ifndef QUICHE_QUIC_QBONE_QBONE_PACKET_EXCHANGER_H_
 #define QUICHE_QUIC_QBONE_QBONE_PACKET_EXCHANGER_H_
 
+#include <linux/if_ether.h>
+
 #include <cstddef>
+#include <optional>
 #include <vector>
 
 #include "absl/base/nullability.h"
 #include "absl/status/statusor.h"
 #include "absl/time/time.h"
 #include "absl/types/span.h"
-#include "quiche/common/quiche_callbacks.h"
 
 namespace quic {
 
@@ -45,8 +47,12 @@
   //
   // If `exchanger` is not null, it will be used instead of `this` to handle any
   // underlying-triggered read/write operations.
+  //
+  // In TAP mode, `eth_hdr` provides the Ethernet header to prepend on writes
+  // and validate on reads. In TUN mode, `eth_hdr` is std::nullopt.
   virtual void Start(int read_fd, int write_fd,
-                     QboneClientPacketExchanger* absl_nullable exchanger) = 0;
+                     QboneClientPacketExchanger* absl_nullable exchanger,
+                     std::optional<ethhdr> eth_hdr = std::nullopt) = 0;
 
   // Uninitializes the exchanger and blocks until all pending read/write
   // operations (on- or off-thread) are complete. No Visitor callbacks will be
diff --git a/quiche/quic/qbone/bonnet/tun_device.cc b/quiche/quic/qbone/bonnet/tun_device.cc
index 64a2c3d..c1427c7 100644
--- a/quiche/quic/qbone/bonnet/tun_device.cc
+++ b/quiche/quic/qbone/bonnet/tun_device.cc
@@ -5,13 +5,16 @@
 #include "quiche/quic/qbone/bonnet/tun_device.h"
 
 #include <fcntl.h>
+#include <linux/if_ether.h>
 #include <linux/if_tun.h>
 #include <net/if.h>
 #include <sys/ioctl.h>
 #include <sys/socket.h>
 
+#include <cstdint>
 #include <cstring>
 #include <ios>
+#include <optional>
 #include <string>
 #include <vector>
 
@@ -19,12 +22,22 @@
 #include "quiche/quic/platform/api/quic_bug_tracker.h"
 #include "quiche/quic/platform/api/quic_logging.h"
 #include "quiche/quic/qbone/platform/kernel_interface.h"
+#include "quiche/common/quiche_endian.h"
 
 ABSL_FLAG(std::string, qbone_client_tun_device_path, "/dev/net/tun",
           "The path to the QBONE client's TUN device.");
 
 namespace quic {
 
+ethhdr CreateTapEthHdr(const uint8_t mac[ETH_ALEN]) {
+  ethhdr hdr{};
+  memcpy(&hdr.h_dest, mac, ETH_ALEN);
+  memcpy(&hdr.h_source, mac, ETH_ALEN);
+  // Assume ipv6 for now
+  hdr.h_proto = quiche::QuicheEndian::HostToNet16(ETH_P_IPV6);
+  return hdr;
+}
+
 const int kInvalidFd = -1;
 
 TunTapDevice::TunTapDevice(const std::string& interface_name, int mtu,
@@ -59,6 +72,20 @@
     return false;
   }
 
+  if (is_tap_) {
+    struct ifreq if_request;
+    memset(&if_request, 0, sizeof(if_request));
+    interface_name_.copy(if_request.ifr_name, IFNAMSIZ);
+    if (!NetdeviceIoctl(SIOCGIFHWADDR, &if_request)) {
+      QUIC_LOG(ERROR) << "Failed to get hardware address for TAP device: "
+                      << interface_name_;
+      CloseDevice();
+      return false;
+    }
+    eth_hdr_ = CreateTapEthHdr(
+        reinterpret_cast<const uint8_t*>(if_request.ifr_hwaddr.sa_data));
+  }
+
   return true;
 }
 
@@ -232,6 +259,7 @@
     kernel_.close(fd);
   }
   file_descriptors_.clear();
+  eth_hdr_ = std::nullopt;
 }
 
 }  // namespace quic
diff --git a/quiche/quic/qbone/bonnet/tun_device.h b/quiche/quic/qbone/bonnet/tun_device.h
index 20ee320..7107f46 100644
--- a/quiche/quic/qbone/bonnet/tun_device.h
+++ b/quiche/quic/qbone/bonnet/tun_device.h
@@ -5,9 +5,11 @@
 #ifndef QUICHE_QUIC_QBONE_BONNET_TUN_DEVICE_H_
 #define QUICHE_QUIC_QBONE_BONNET_TUN_DEVICE_H_
 
+#include <linux/if_ether.h>
 #include <net/if.h>
 
-#include <cstddef>
+#include <cstdint>
+#include <optional>
 #include <string>
 #include <vector>
 
@@ -16,6 +18,9 @@
 
 namespace quic {
 
+// Builds an ethhdr for a TAP device from the given hardware MAC address.
+ethhdr CreateTapEthHdr(const uint8_t mac[ETH_ALEN]);
+
 class TunTapDevice : public TunDeviceInterface {
  public:
   // This represents a tun device created in the OS kernel, which is a virtual
@@ -64,6 +69,8 @@
   // Returns the open queue FD on success, or -1 on failure.
   int OpenQueue() override;
 
+  std::optional<ethhdr> eth_hdr() const override { return eth_hdr_; }
+
   static bool CheckFeatures(KernelInterface& kernel, int tun_device_fd);
   static bool OpenFileDescriptor(KernelInterface& kernel,
                                  const std::string& path, ifreq if_request,
@@ -88,6 +95,7 @@
   KernelInterface& kernel_;
 
   std::vector<int> file_descriptors_;
+  std::optional<ethhdr> eth_hdr_;
 };
 
 }  // namespace quic
diff --git a/quiche/quic/qbone/bonnet/tun_device_interface.h b/quiche/quic/qbone/bonnet/tun_device_interface.h
index 57444c6..12e4d69 100644
--- a/quiche/quic/qbone/bonnet/tun_device_interface.h
+++ b/quiche/quic/qbone/bonnet/tun_device_interface.h
@@ -5,6 +5,10 @@
 #ifndef QUICHE_QUIC_QBONE_BONNET_TUN_DEVICE_INTERFACE_H_
 #define QUICHE_QUIC_QBONE_BONNET_TUN_DEVICE_INTERFACE_H_
 
+#include <linux/if_ether.h>
+
+#include <optional>
+
 namespace quic {
 
 // An interface with methods for interacting with a TUN device.
@@ -34,6 +38,10 @@
   // Opens the next sequential queue file descriptor for this multiqueue device.
   // Returns the open queue FD on success, or -1 on failure.
   virtual int OpenQueue() = 0;
+
+  // In TAP mode, returns the Ethernet header configured for this device once
+  // opened. In TUN mode or prior to Init(), returns std::nullopt.
+  virtual std::optional<ethhdr> eth_hdr() const { return std::nullopt; }
 };
 
 }  // namespace quic
diff --git a/quiche/quic/qbone/bonnet/tun_device_packet_exchanger.cc b/quiche/quic/qbone/bonnet/tun_device_packet_exchanger.cc
index 010c5d1..35b0f40 100644
--- a/quiche/quic/qbone/bonnet/tun_device_packet_exchanger.cc
+++ b/quiche/quic/qbone/bonnet/tun_device_packet_exchanger.cc
@@ -13,11 +13,11 @@
 #include <cstddef>
 #include <cstring>
 #include <memory>
+#include <optional>
 #include <string>
 #include <utility>
 #include <vector>
 
-#include "absl/base/macros.h"
 #include "absl/base/nullability.h"
 #include "absl/status/status.h"
 #include "absl/status/statusor.h"
@@ -32,26 +32,20 @@
 #include "quiche/quic/qbone/bonnet/qbone_client_packet_exchanger.h"
 #include "quiche/quic/qbone/platform/icmp_packet.h"
 #include "quiche/quic/qbone/platform/kernel_interface.h"
-#include "quiche/quic/qbone/platform/netlink_interface.h"
 #include "quiche/quic/qbone/qbone_constants.h"
 #include "quiche/common/platform/api/quiche_logging.h"
 #include "quiche/common/quiche_endian.h"
 
 namespace quic {
 
-using ::quiche::QuicheEndian;
-
 TunDevicePacketExchanger::TunDevicePacketExchanger(
-    size_t mtu, KernelInterface* kernel, NetlinkInterface* netlink,
-    Visitor* absl_nonnull visitor, bool is_tap, absl::string_view ifname)
+    size_t mtu, KernelInterface* absl_nonnull kernel,
+    Visitor* absl_nonnull visitor)
     : kernel_(kernel),
-      netlink_(netlink),
       visitor_(*visitor),
-      ifname_(ifname),
       // Reading on a TUN device returns a packet at a time. If the packet is
       // longer than the buffer, it's truncated.
-      read_buffer_(mtu),
-      is_tap_(is_tap) {}
+      read_buffer_(mtu) {}
 
 TunDevicePacketExchanger::~TunDevicePacketExchanger() {
   QUIC_BUG_IF(bonnet_tun_device_packet_exchanger_not_stopped,
@@ -60,11 +54,15 @@
 
 void TunDevicePacketExchanger::Start(
     int read_fd, int write_fd,
-    QboneClientPacketExchanger* absl_nullable exchanger) {
+    QboneClientPacketExchanger* absl_nullable exchanger,
+    std::optional<ethhdr> eth_hdr) {
   if (exchanger == nullptr) {
     exchanger = this;
   }
 
+  is_tap_ = eth_hdr.has_value();
+  eth_hdr_ = eth_hdr.value_or(ethhdr{});
+
   // Allow idempotent Start() calls with the same file descriptors, but
   // otherwise it's a bug to try starting an already started exchanger.
   QUIC_BUG_IF(qbone_tun_device_packet_exchanger_already_started,
@@ -86,6 +84,8 @@
   read_fd_ = -1;
   write_fd_ = -1;
   exchanger_ = nullptr;
+  is_tap_ = false;
+  eth_hdr_ = {};
 }
 
 int TunDevicePacketExchanger::OnReadFromNetworkReady(int max_packets_to_read) {
@@ -123,9 +123,6 @@
     return;
   }
 
-  if (is_tap_ && !eth_hdr_initialized_) {
-    InitializeEthHdr();
-  }
   struct iovec iov[2];
   iov[0].iov_base = is_tap_ ? &eth_hdr_ : nullptr;
   iov[0].iov_len = is_tap_ ? ETH_HLEN : 0;
@@ -224,28 +221,10 @@
   return true;
 }
 
-void TunDevicePacketExchanger::InitializeEthHdr() {
-  if (!eth_hdr_initialized_) {
-    NetlinkInterface::LinkInfo link_info{};
-    if (netlink_->GetLinkInfo(ifname_, &link_info)) {
-      // Set src & dst to my own address
-      memcpy(&eth_hdr_.h_dest, link_info.hardware_address, ETH_ALEN);
-      memcpy(&eth_hdr_.h_source, link_info.hardware_address, ETH_ALEN);
-      // Assume ipv6 for now
-      // TODO(b/195113643): Support additional protocols.
-      eth_hdr_.h_proto = QuicheEndian::HostToNet16(ETH_P_IPV6);
-      eth_hdr_initialized_ = true;
-    } else {
-      QUIC_LOG_EVERY_N_SEC(ERROR, 30)
-          << "Unable to get link info for: " << ifname_;
-    }
-  }
-}
-
 TunDevicePacketExchanger::L2ValidationResult
 TunDevicePacketExchanger::ValidateL2Headers(
     const ethhdr& eth_header, absl::Span<const std::byte> packet) {
-  if (eth_header.h_proto != QuicheEndian::HostToNet16(ETH_P_IPV6)) {
+  if (eth_header.h_proto != quiche::QuicheEndian::HostToNet16(ETH_P_IPV6)) {
     return L2ValidationResult::kInvalid;
   }
   constexpr auto kIp6PrefixLen = sizeof(ip6_hdr);
@@ -269,10 +248,6 @@
   }
 
   if (is_neighbor_solicit) {
-    // We need the local interface MAC address to respond.
-    if (!eth_hdr_initialized_) {
-      InitializeEthHdr();
-    }
     // If we've received a neighbor solicitation, craft an advertisement to
     // respond with and write it back to the local interface.
     absl::Span<const std::byte> icmp6_payload = packet.subspan(kIcmp6PrefixLen);
diff --git a/quiche/quic/qbone/bonnet/tun_device_packet_exchanger.h b/quiche/quic/qbone/bonnet/tun_device_packet_exchanger.h
index 2c8130d..8a8f5d6 100644
--- a/quiche/quic/qbone/bonnet/tun_device_packet_exchanger.h
+++ b/quiche/quic/qbone/bonnet/tun_device_packet_exchanger.h
@@ -8,16 +8,15 @@
 #include <linux/if_ether.h>
 
 #include <cstddef>
-#include <string>
+#include <memory>
+#include <optional>
 #include <vector>
 
 #include "absl/base/attributes.h"
 #include "absl/base/nullability.h"
-#include "absl/strings/string_view.h"
 #include "absl/types/span.h"
 #include "quiche/quic/qbone/bonnet/qbone_client_packet_exchanger.h"
 #include "quiche/quic/qbone/platform/kernel_interface.h"
-#include "quiche/quic/qbone/platform/netlink_interface.h"
 
 namespace quic {
 
@@ -27,20 +26,16 @@
 // the same thread and all reads occur on the same thread.
 class TunDevicePacketExchanger : public QboneClientPacketExchanger {
  public:
-  // |mtu| is the mtu of the TUN device.
-  // |kernel| is not owned but should out live objects of this class.
-  // |visitor| is not owned but should out live objects of this class.
-  TunDevicePacketExchanger(size_t mtu, KernelInterface* kernel,
-                           NetlinkInterface* netlink,
+  TunDevicePacketExchanger(size_t mtu, KernelInterface* absl_nonnull kernel,
                            QboneClientPacketExchanger::Visitor* absl_nonnull
-                               visitor ABSL_ATTRIBUTE_LIFETIME_BOUND,
-                           bool is_tap, absl::string_view ifname);
+                               visitor ABSL_ATTRIBUTE_LIFETIME_BOUND);
 
   ~TunDevicePacketExchanger() override;
 
   // QboneClientPacketExchanger:
   void Start(int read_fd, int write_fd,
-             QboneClientPacketExchanger* absl_nullable exchanger) override;
+             QboneClientPacketExchanger* absl_nullable exchanger,
+             std::optional<ethhdr> eth_hdr = std::nullopt) override;
   void Stop() override;
   int OnReadFromNetworkReady(int max_packets_to_read) override;
   void WritePacketToNetwork(absl::Span<const std::byte> packet) override;
@@ -62,20 +57,16 @@
   // Returns true if more packets may be available to read.
   bool ReadAndExchangeSinglePacket(bool exchange_blocked_error);
 
-  void InitializeEthHdr();
   L2ValidationResult ValidateL2Headers(const ethhdr& eth_header,
                                        absl::Span<const std::byte> packet);
 
   KernelInterface* kernel_;
-  NetlinkInterface* netlink_;
   QboneClientPacketExchanger::Visitor& visitor_;
-  const std::string ifname_;
 
   std::vector<std::byte> read_buffer_;
 
-  const bool is_tap_;
+  bool is_tap_ = false;
   ethhdr eth_hdr_ = {};
-  bool eth_hdr_initialized_ = false;
 
   // -1/nullptr before Start() or after Stop().
   int read_fd_ = -1;
diff --git a/quiche/quic/qbone/bonnet/tun_device_packet_exchanger_test.cc b/quiche/quic/qbone/bonnet/tun_device_packet_exchanger_test.cc
index 34a3f3d..5bc9276 100644
--- a/quiche/quic/qbone/bonnet/tun_device_packet_exchanger_test.cc
+++ b/quiche/quic/qbone/bonnet/tun_device_packet_exchanger_test.cc
@@ -21,9 +21,8 @@
 #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/platform/mock_netlink.h"
-#include "quiche/quic/qbone/platform/netlink_interface.h"
 #include "quiche/quic/qbone/qbone_constants.h"
 #include "quiche/common/quiche_endian.h"
 
@@ -33,6 +32,7 @@
 const size_t kMtu = 1000;
 const int kReadFd = 15;
 const int kWriteFd = 16;
+constexpr uint8_t kTestMac[ETH_ALEN] = {0x12, 0x12, 0x12, 0x12, 0x12, 0x12};
 
 using ::absl_testing::IsOkAndHolds;
 using ::absl_testing::StatusIs;
@@ -49,8 +49,7 @@
 class TunDevicePacketExchangerTest : public QuicTest {
  protected:
   TunDevicePacketExchangerTest()
-      : exchanger_(kMtu, &mock_kernel_, nullptr, &mock_visitor_, false,
-                   absl::string_view()) {}
+      : exchanger_(kMtu, &mock_kernel_, &mock_visitor_) {}
 
   ~TunDevicePacketExchangerTest() override = default;
 
@@ -165,25 +164,14 @@
 
 TEST_F(TunDevicePacketExchangerTest, TapWritePacketSuccessful) {
   StrictMock<MockKernel> mock_kernel;
-  StrictMock<MockNetlink> mock_netlink;
   StrictMock<MockQboneClientPacketExchanger::MockVisitor> mock_visitor;
-  TunDevicePacketExchanger tap_exchanger(kMtu, &mock_kernel, &mock_netlink,
-                                         &mock_visitor, /*is_tap=*/true,
-                                         "tap0");
-  tap_exchanger.Start(kReadFd, kWriteFd, &delegate_exchanger_);
+  uint8_t mac[ETH_ALEN] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55};
+  TunDevicePacketExchanger tap_exchanger(kMtu, &mock_kernel, &mock_visitor);
+  tap_exchanger.Start(kReadFd, kWriteFd, &delegate_exchanger_,
+                      CreateTapEthHdr(mac));
 
   std::string packet = "fake packet";
 
-  // Expectations on Netlink to get the hardware address when writing the
-  // first packet
-  EXPECT_CALL(mock_netlink, GetLinkInfo("tap0", _))
-      .WillOnce([](const std::string& ifname,
-                   NetlinkInterface::LinkInfo* link_info) -> bool {
-        uint8_t mac[ETH_ALEN] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55};
-        memcpy(link_info->hardware_address, mac, ETH_ALEN);
-        return true;
-      });
-
   // iov[0] should contain the Ethernet header populated by InitializeEthHdr
   EXPECT_CALL(mock_kernel, writev(kWriteFd, _, 2))
       .WillOnce([&packet](int fd, const struct iovec* iov,
@@ -383,20 +371,19 @@
 class TunDevicePacketExchangerTapTest : public QuicTest {
  protected:
   TunDevicePacketExchangerTapTest()
-      : exchanger_(kMtu, &mock_kernel_, &mock_netlink_, &mock_visitor_, true,
-                   "tap0") {}
+      : exchanger_(kMtu, &mock_kernel_, &mock_visitor_) {}
 
-  ~TunDevicePacketExchangerTapTest() override = default;
+  ~TunDevicePacketExchangerTapTest() override { exchanger_.Stop(); }
 
   MockKernel mock_kernel_;
-  StrictMock<MockNetlink> mock_netlink_;
   StrictMock<MockQboneClientPacketExchanger::MockVisitor> mock_visitor_;
   TunDevicePacketExchanger exchanger_;
   StrictMock<MockQboneClientPacketExchanger> delegate_exchanger_;
 };
 
 TEST_F(TunDevicePacketExchangerTapTest, ReadPacketTapSuccess) {
-  exchanger_.Start(kReadFd, kWriteFd, &delegate_exchanger_);
+  exchanger_.Start(kReadFd, kWriteFd, &delegate_exchanger_,
+                   CreateTapEthHdr(kTestMac));
 
   ip6_hdr ip_hdr{};
   ip_hdr.ip6_vfc = 0x60;  // Version 6
@@ -432,7 +419,8 @@
 }
 
 TEST_F(TunDevicePacketExchangerTapTest, ReadPacketTapInvalidL2) {
-  exchanger_.Start(kReadFd, kWriteFd, &delegate_exchanger_);
+  exchanger_.Start(kReadFd, kWriteFd, &delegate_exchanger_,
+                   CreateTapEthHdr(kTestMac));
 
   ethhdr eth_hdr{};
   eth_hdr.h_proto = QuicheEndian::HostToNet16(ETH_P_ARP);  // Non-IPv6
@@ -450,7 +438,8 @@
 }
 
 TEST_F(TunDevicePacketExchangerTapTest, ReadPacketTapNeighborSolicitation) {
-  exchanger_.Start(kReadFd, kWriteFd, &delegate_exchanger_);
+  exchanger_.Start(kReadFd, kWriteFd, &delegate_exchanger_,
+                   CreateTapEthHdr(kTestMac));
 
   ip6_hdr ip_hdr{};
   ip_hdr.ip6_vfc = 0x60;  // Version 6
@@ -480,14 +469,6 @@
             return ETH_HLEN + l3_packet.size();
           });
 
-  // Expect GetLinkInfo to populate ethhdr on writing neighbor solicit response.
-  EXPECT_CALL(mock_netlink_, GetLinkInfo("tap0", _))
-      .WillOnce(
-          [](const std::string& ifname, NetlinkInterface::LinkInfo* link_info) {
-            memset(link_info->hardware_address, 0x12, ETH_ALEN);
-            return true;
-          });
-
   // Expect neighbor solicitation response to be sent via delegate exchanger.
   EXPECT_CALL(delegate_exchanger_, WritePacketToNetwork(_));
 
@@ -503,7 +484,8 @@
 // requests.
 TEST_F(TunDevicePacketExchangerTapTest,
        ReadPacketTapNeighborSolicitationSameExchanger) {
-  exchanger_.Start(kReadFd, kWriteFd, /*exchanger=*/nullptr);
+  exchanger_.Start(kReadFd, kWriteFd, /*exchanger=*/nullptr,
+                   CreateTapEthHdr(kTestMac));
 
   ip6_hdr ip_hdr{};
   ip_hdr.ip6_vfc = 0x60;  // Version 6
@@ -533,14 +515,6 @@
             return ETH_HLEN + l3_packet.size();
           });
 
-  // Expect GetLinkInfo to populate ethhdr on writing neighbor solicit response.
-  EXPECT_CALL(mock_netlink_, GetLinkInfo("tap0", _))
-      .WillOnce(
-          [](const std::string& ifname, NetlinkInterface::LinkInfo* link_info) {
-            memset(link_info->hardware_address, 0x12, ETH_ALEN);
-            return true;
-          });
-
   // Expect neighbor solicitation response to be written out.
   EXPECT_CALL(mock_kernel_, writev(kWriteFd, _, 2))
       .WillOnce([](int fd, const struct iovec* iov, int iovcnt) -> ssize_t {
@@ -556,7 +530,8 @@
 }
 
 TEST_F(TunDevicePacketExchangerTapTest, MultiReadInvalidSizeShort) {
-  exchanger_.Start(kReadFd, kWriteFd, &delegate_exchanger_);
+  exchanger_.Start(kReadFd, kWriteFd, &delegate_exchanger_,
+                   CreateTapEthHdr(kTestMac));
 
   ip6_hdr ip_hdr{};
   ip_hdr.ip6_vfc = 0x60;  // Version 6
@@ -598,7 +573,8 @@
 }
 
 TEST_F(TunDevicePacketExchangerTapTest, MultiReadInvalidL2) {
-  exchanger_.Start(kReadFd, kWriteFd, &delegate_exchanger_);
+  exchanger_.Start(kReadFd, kWriteFd, &delegate_exchanger_,
+                   CreateTapEthHdr(kTestMac));
 
   ethhdr invalid_eth_hdr{};
   invalid_eth_hdr.h_proto = QuicheEndian::HostToNet16(ETH_P_ARP);  // Non-IPv6
diff --git a/quiche/quic/qbone/bonnet/tun_device_test.cc b/quiche/quic/qbone/bonnet/tun_device_test.cc
index ccbab2d..4519fb9 100644
--- a/quiche/quic/qbone/bonnet/tun_device_test.cc
+++ b/quiche/quic/qbone/bonnet/tun_device_test.cc
@@ -8,6 +8,9 @@
 #include <linux/if_tun.h>
 #include <sys/ioctl.h>
 
+#include <cstdint>
+#include <cstring>
+
 #include "quiche/quic/platform/api/quic_test.h"
 #include "quiche/quic/qbone/platform/mock_kernel.h"
 
@@ -84,6 +87,17 @@
           EXPECT_THAT(ifr->ifr_name, StrEq(kDeviceName));
           return 0;
         });
+    if (is_tap) {
+      EXPECT_CALL(mock_kernel_, ioctl(_, SIOCGIFHWADDR, _))
+          .Times(AnyNumber())
+          .WillRepeatedly([](Unused, Unused, void* argp) {
+            auto* ifr = reinterpret_cast<struct ifreq*>(argp);
+            constexpr uint8_t kMac[ETH_ALEN] = {0x00, 0x11, 0x22,
+                                                0x33, 0x44, 0x55};
+            std::memcpy(ifr->ifr_hwaddr.sa_data, kMac, ETH_ALEN);
+            return 0;
+          });
+    }
   }
 
   // Expect that Up() will be called. Force the call to fail when fail == true.