Provide an option to use a TAP interface for QBONE clients.

This is necessary to work around a bug in ios xr that is unable to deal with L3 virtual devices in the TPA.

Protected by FLAGS_use_tap_instead_of_tun.

PiperOrigin-RevId: 398042759
diff --git a/quic/qbone/bonnet/tun_device.cc b/quic/qbone/bonnet/tun_device.cc
index 65197cd..3768a43 100644
--- a/quic/qbone/bonnet/tun_device.cc
+++ b/quic/qbone/bonnet/tun_device.cc
@@ -24,26 +24,25 @@
 
 const int kInvalidFd = -1;
 
-TunDevice::TunDevice(const std::string& interface_name,
-                     int mtu,
-                     bool persist,
-                     bool setup_tun,
-                     KernelInterface* kernel)
+TunTapDevice::TunTapDevice(const std::string& interface_name, int mtu,
+                           bool persist, bool setup_tun, bool is_tap,
+                           KernelInterface* kernel)
     : interface_name_(interface_name),
       mtu_(mtu),
       persist_(persist),
       setup_tun_(setup_tun),
+      is_tap_(is_tap),
       file_descriptor_(kInvalidFd),
       kernel_(*kernel) {}
 
-TunDevice::~TunDevice() {
+TunTapDevice::~TunTapDevice() {
   if (!persist_) {
     Down();
   }
   CloseDevice();
 }
 
-bool TunDevice::Init() {
+bool TunTapDevice::Init() {
   if (interface_name_.empty() || interface_name_.size() >= IFNAMSIZ) {
     QUIC_BUG(quic_bug_10995_1)
         << "interface_name must be nonempty and shorter than " << IFNAMSIZ;
@@ -63,7 +62,7 @@
 
 // TODO(pengg): might be better to use netlink socket, once we have a library to
 // use
-bool TunDevice::Up() {
+bool TunTapDevice::Up() {
   if (!setup_tun_) {
     return true;
   }
@@ -79,7 +78,7 @@
 
 // TODO(pengg): might be better to use netlink socket, once we have a library to
 // use
-bool TunDevice::Down() {
+bool TunTapDevice::Down() {
   if (!setup_tun_) {
     return true;
   }
@@ -93,11 +92,9 @@
   return NetdeviceIoctl(SIOCSIFFLAGS, reinterpret_cast<void*>(&if_request));
 }
 
-int TunDevice::GetFileDescriptor() const {
-  return file_descriptor_;
-}
+int TunTapDevice::GetFileDescriptor() const { return file_descriptor_; }
 
-bool TunDevice::OpenDevice() {
+bool TunTapDevice::OpenDevice() {
   if (file_descriptor_ != kInvalidFd) {
     CloseDevice();
   }
@@ -113,7 +110,12 @@
   // destroy the device and create a new one, but that deletes any existing
   // routing associated with the interface, which makes the meaning of the
   // 'persist' bit ambiguous.
-  if_request.ifr_flags = IFF_TUN | IFF_MULTI_QUEUE | IFF_NO_PI;
+  if_request.ifr_flags = IFF_MULTI_QUEUE | IFF_NO_PI;
+  if (is_tap_) {
+    if_request.ifr_flags |= IFF_TAP;
+  } else {
+    if_request.ifr_flags |= IFF_TUN;
+  }
 
   // When the device is running with IFF_MULTI_QUEUE set, each call to open will
   // create a queue which can be used to read/write packets from/to the device.
@@ -154,7 +156,7 @@
 
 // TODO(pengg): might be better to use netlink socket, once we have a library to
 // use
-bool TunDevice::ConfigureInterface() {
+bool TunTapDevice::ConfigureInterface() {
   if (!setup_tun_) {
     return true;
   }
@@ -174,7 +176,7 @@
   return true;
 }
 
-bool TunDevice::CheckFeatures(int tun_device_fd) {
+bool TunTapDevice::CheckFeatures(int tun_device_fd) {
   unsigned int actual_features;
   if (kernel_.ioctl(tun_device_fd, TUNGETFEATURES, &actual_features) != 0) {
     QUIC_PLOG(WARNING) << "Failed to TUNGETFEATURES";
@@ -191,7 +193,7 @@
   return true;
 }
 
-bool TunDevice::NetdeviceIoctl(int request, void* argp) {
+bool TunTapDevice::NetdeviceIoctl(int request, void* argp) {
   int fd = kernel_.socket(AF_INET6, SOCK_DGRAM, 0);
   if (fd < 0) {
     QUIC_PLOG(WARNING) << "Failed to create AF_INET6 socket.";
@@ -207,7 +209,7 @@
   return true;
 }
 
-void TunDevice::CloseDevice() {
+void TunTapDevice::CloseDevice() {
   if (file_descriptor_ != kInvalidFd) {
     kernel_.close(file_descriptor_);
     file_descriptor_ = kInvalidFd;
diff --git a/quic/qbone/bonnet/tun_device.h b/quic/qbone/bonnet/tun_device.h
index 129e1d1..66c06d2 100644
--- a/quic/qbone/bonnet/tun_device.h
+++ b/quic/qbone/bonnet/tun_device.h
@@ -13,7 +13,7 @@
 
 namespace quic {
 
-class TunDevice : public TunDeviceInterface {
+class TunTapDevice : public TunDeviceInterface {
  public:
   // This represents a tun device created in the OS kernel, which is a virtual
   // network interface that any packets sent to it can be read by a user space
@@ -32,13 +32,10 @@
   // routing rules go away.
   //
   // The caller should own kernel and make sure it outlives this.
-  TunDevice(const std::string& interface_name,
-            int mtu,
-            bool persist,
-            bool setup_tun,
-            KernelInterface* kernel);
+  TunTapDevice(const std::string& interface_name, int mtu, bool persist,
+               bool setup_tun, bool is_tap, KernelInterface* kernel);
 
-  ~TunDevice() override;
+  ~TunTapDevice() override;
 
   // Actually creates/reopens and configures the device.
   bool Init() override;
@@ -50,7 +47,7 @@
   bool Down() override;
 
   // Closes the open file descriptor for the TUN device (if one exists).
-  // It is safe to reinitialize and reuse this TunDevice after calling
+  // It is safe to reinitialize and reuse this TunTapDevice after calling
   // CloseDevice.
   void CloseDevice() override;
 
@@ -75,6 +72,7 @@
   const int mtu_;
   const bool persist_;
   const bool setup_tun_;
+  const bool is_tap_;
   int file_descriptor_;
   KernelInterface& kernel_;
 };
diff --git a/quic/qbone/bonnet/tun_device_interface.h b/quic/qbone/bonnet/tun_device_interface.h
index 3c6f962..e88efa9 100644
--- a/quic/qbone/bonnet/tun_device_interface.h
+++ b/quic/qbone/bonnet/tun_device_interface.h
@@ -24,7 +24,7 @@
   virtual bool Down() = 0;
 
   // Closes the open file descriptor for the TUN device (if one exists).
-  // It is safe to reinitialize and reuse this TunDevice after calling
+  // It is safe to reinitialize and reuse this TunTapDevice after calling
   // CloseDevice.
   virtual void CloseDevice() = 0;
 
diff --git a/quic/qbone/bonnet/tun_device_packet_exchanger.cc b/quic/qbone/bonnet/tun_device_packet_exchanger.cc
index 4783e9c..ecb4f5d 100644
--- a/quic/qbone/bonnet/tun_device_packet_exchanger.cc
+++ b/quic/qbone/bonnet/tun_device_packet_exchanger.cc
@@ -4,27 +4,35 @@
 
 #include "quic/qbone/bonnet/tun_device_packet_exchanger.h"
 
+#include <netinet/icmp6.h>
+#include <netinet/ip6.h>
+
 #include <utility>
 
 #include "absl/strings/str_cat.h"
+#include "quic/qbone/platform/icmp_packet.h"
+#include "quic/qbone/platform/netlink_interface.h"
 
 namespace quic {
 
 TunDevicePacketExchanger::TunDevicePacketExchanger(
-    size_t mtu,
-    KernelInterface* kernel,
-    QbonePacketExchanger::Visitor* visitor,
-    size_t max_pending_packets,
-    StatsInterface* stats)
+    size_t mtu, KernelInterface* kernel, NetlinkInterface* netlink,
+    QbonePacketExchanger::Visitor* visitor, size_t max_pending_packets,
+    bool is_tap, StatsInterface* stats, absl::string_view ifname)
     : QbonePacketExchanger(visitor, max_pending_packets),
       mtu_(mtu),
       kernel_(kernel),
-      stats_(stats) {}
+      netlink_(netlink),
+      ifname_(ifname),
+      is_tap_(is_tap),
+      stats_(stats) {
+  if (is_tap_) {
+    mtu_ += ETH_HLEN;
+  }
+}
 
-bool TunDevicePacketExchanger::WritePacket(const char* packet,
-                                           size_t size,
-                                           bool* blocked,
-                                           std::string* error) {
+bool TunDevicePacketExchanger::WritePacket(const char* packet, size_t size,
+                                           bool* blocked, std::string* error) {
   *blocked = false;
   if (fd_ < 0) {
     *error = absl::StrCat("Invalid file descriptor of the TUN device: ", fd_);
@@ -32,7 +40,11 @@
     return false;
   }
 
-  int result = kernel_->write(fd_, packet, size);
+  auto buffer = std::make_unique<QuicData>(packet, size);
+  if (is_tap_) {
+    buffer = ApplyL2Headers(*buffer);
+  }
+  int result = kernel_->write(fd_, buffer->data(), buffer->length());
   if (result == -1) {
     if (errno == EWOULDBLOCK || errno == EAGAIN) {
       // The tunnel is blocked. Note that this does not mean the receive buffer
@@ -50,8 +62,7 @@
 }
 
 std::unique_ptr<QuicData> TunDevicePacketExchanger::ReadPacket(
-    bool* blocked,
-    std::string* error) {
+    bool* blocked, std::string* error) {
   *blocked = false;
   if (fd_ < 0) {
     *error = absl::StrCat("Invalid file descriptor of the TUN device: ", fd_);
@@ -72,17 +83,140 @@
     }
     return nullptr;
   }
-  stats_->OnPacketRead(result);
-  return std::make_unique<QuicData>(read_buffer.release(), result, true);
+
+  auto buffer = std::make_unique<QuicData>(read_buffer.release(), result, true);
+  if (is_tap_) {
+    buffer = ConsumeL2Headers(*buffer);
+  }
+  if (buffer) {
+    stats_->OnPacketRead(buffer->length());
+  }
+  return buffer;
 }
 
-void TunDevicePacketExchanger::set_file_descriptor(int fd) {
-  fd_ = fd;
-}
+void TunDevicePacketExchanger::set_file_descriptor(int fd) { fd_ = fd; }
 
 const TunDevicePacketExchanger::StatsInterface*
 TunDevicePacketExchanger::stats_interface() const {
   return stats_;
 }
 
+std::unique_ptr<QuicData> TunDevicePacketExchanger::ApplyL2Headers(
+    const QuicData& l3_packet) {
+  if (is_tap_ && !mac_initialized_) {
+    NetlinkInterface::LinkInfo link_info{};
+    if (netlink_->GetLinkInfo(ifname_, &link_info)) {
+      memcpy(tap_mac_, link_info.hardware_address, ETH_ALEN);
+      mac_initialized_ = true;
+    } else {
+      QUIC_LOG_EVERY_N_SEC(ERROR, 30)
+          << "Unable to get link info for: " << ifname_;
+    }
+  }
+
+  const auto l2_packet_size = l3_packet.length() + ETH_HLEN;
+  auto l2_buffer = std::make_unique<char[]>(l2_packet_size);
+
+  // Populate the Ethernet header
+  auto* hdr = reinterpret_cast<ethhdr*>(l2_buffer.get());
+  // Set src & dst to my own address
+  memcpy(hdr->h_dest, tap_mac_, ETH_ALEN);
+  memcpy(hdr->h_source, tap_mac_, ETH_ALEN);
+  // Assume ipv6 for now
+  // TODO(b/195113643): Support additional protocols.
+  hdr->h_proto = absl::ghtons(ETH_P_IPV6);
+
+  // Copy the l3 packet into buffer, just after the ethernet header.
+  memcpy(l2_buffer.get() + ETH_HLEN, l3_packet.data(), l3_packet.length());
+
+  return std::make_unique<QuicData>(l2_buffer.release(), l2_packet_size, true);
+}
+
+std::unique_ptr<QuicData> TunDevicePacketExchanger::ConsumeL2Headers(
+    const QuicData& l2_packet) {
+  if (l2_packet.length() < ETH_HLEN) {
+    // Packet is too short for ethernet headers. Drop it.
+    return nullptr;
+  }
+  auto* hdr = reinterpret_cast<const ethhdr*>(l2_packet.data());
+  if (hdr->h_proto != absl::ghtons(ETH_P_IPV6)) {
+    return nullptr;
+  }
+  constexpr auto kIp6PrefixLen = ETH_HLEN + sizeof(ip6_hdr);
+  constexpr auto kIcmp6PrefixLen = kIp6PrefixLen + sizeof(icmp6_hdr);
+  if (l2_packet.length() < kIp6PrefixLen) {
+    // Packet is too short to be ipv6. Drop it.
+    return nullptr;
+  }
+  auto* ip_hdr = reinterpret_cast<const ip6_hdr*>(l2_packet.data() + ETH_HLEN);
+  const bool is_icmp = ip_hdr->ip6_ctlun.ip6_un1.ip6_un1_nxt == IPPROTO_ICMPV6;
+
+  bool is_neighbor_solicit = false;
+  if (is_icmp) {
+    if (l2_packet.length() < kIcmp6PrefixLen) {
+      // Packet is too short to be icmp6. Drop it.
+      return nullptr;
+    }
+    is_neighbor_solicit =
+        reinterpret_cast<const icmp6_hdr*>(l2_packet.data() + kIp6PrefixLen)
+            ->icmp6_type == ND_NEIGHBOR_SOLICIT;
+  }
+
+  if (is_neighbor_solicit) {
+    // If we've received a neighbor solicitation, craft an advertisement to
+    // respond with and write it back to the local interface.
+    auto* icmp6_payload = l2_packet.data() + kIcmp6PrefixLen;
+
+    // Neighbor Advertisement crafted per:
+    // https://datatracker.ietf.org/doc/html/rfc4861#section-4.4
+    //
+    // Using the Target link-layer address option defined at:
+    // https://datatracker.ietf.org/doc/html/rfc4861#section-4.6.1
+    constexpr size_t kIcmpv6OptionSize = 8;
+    const int payload_size = sizeof(in6_addr) + kIcmpv6OptionSize;
+    auto payload = std::make_unique<char[]>(payload_size);
+    // Place the solicited IPv6 address at the beginning of the response payload
+    memcpy(payload.get(), icmp6_payload, sizeof(in6_addr));
+    // Setup the Target link-layer address option:
+    //      0                   1                   2                   3
+    //  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+    // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+    // |     Type      |    Length     |    Link-Layer Address ...
+    // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+    int pos = sizeof(in6_addr);
+    payload[pos++] = ND_OPT_TARGET_LINKADDR;    // Type
+    payload[pos++] = 1;                         // Length in units of 8 octets
+    memcpy(&payload[pos], tap_mac_, ETH_ALEN);  // This interfaces' MAC address
+
+    // Populate the ICMPv6 header
+    icmp6_hdr response_hdr{};
+    response_hdr.icmp6_type = ND_NEIGHBOR_ADVERT;
+    // Set the solicited bit to true
+    response_hdr.icmp6_dataun.icmp6_un_data8[0] = 64;
+    // Craft the full ICMPv6 packet and then ship it off to WritePacket
+    // to have it frame it with L2 headers and send it back to the requesting
+    // neighbor.
+    CreateIcmpPacket(ip_hdr->ip6_src, ip_hdr->ip6_src, response_hdr,
+                     absl::string_view(payload.get(), payload_size),
+                     [this](absl::string_view packet) {
+                       bool blocked;
+                       std::string error;
+                       WritePacket(packet.data(), packet.size(), &blocked,
+                                   &error);
+                     });
+    // Do not forward the neighbor solicitation through the tunnel since it's
+    // link-local.
+    return nullptr;
+  }
+
+  // If this isn't a Neighbor Solicitation, remove the L2 headers and forward
+  // it as though it were an L3 packet.
+  const auto l3_packet_size = l2_packet.length() - ETH_HLEN;
+  auto shift_buffer = std::make_unique<char[]>(l3_packet_size);
+  memcpy(shift_buffer.get(), l2_packet.data() + ETH_HLEN, l3_packet_size);
+
+  return std::make_unique<QuicData>(shift_buffer.release(), l3_packet_size,
+                                    true);
+}
+
 }  // namespace quic
diff --git a/quic/qbone/bonnet/tun_device_packet_exchanger.h b/quic/qbone/bonnet/tun_device_packet_exchanger.h
index 115f5b5..3136b42 100644
--- a/quic/qbone/bonnet/tun_device_packet_exchanger.h
+++ b/quic/qbone/bonnet/tun_device_packet_exchanger.h
@@ -5,8 +5,11 @@
 #ifndef QUICHE_QUIC_QBONE_BONNET_TUN_DEVICE_PACKET_EXCHANGER_H_
 #define QUICHE_QUIC_QBONE_BONNET_TUN_DEVICE_PACKET_EXCHANGER_H_
 
+#include <linux/if_ether.h>
+
 #include "quic/core/quic_packets.h"
 #include "quic/qbone/platform/kernel_interface.h"
+#include "quic/qbone/platform/netlink_interface.h"
 #include "quic/qbone/qbone_client_interface.h"
 #include "quic/qbone/qbone_packet_exchanger.h"
 
@@ -42,11 +45,11 @@
   // the TUN device become blocked.
   // |stats| is notified about packet read/write statistics. It is not owned,
   // but should outlive objects of this class.
-  TunDevicePacketExchanger(size_t mtu,
-                           KernelInterface* kernel,
+  TunDevicePacketExchanger(size_t mtu, KernelInterface* kernel,
+                           NetlinkInterface* netlink,
                            QbonePacketExchanger::Visitor* visitor,
-                           size_t max_pending_packets,
-                           StatsInterface* stats);
+                           size_t max_pending_packets, bool is_tap,
+                           StatsInterface* stats, absl::string_view ifname);
 
   void set_file_descriptor(int fd);
 
@@ -63,9 +66,19 @@
                    bool* blocked,
                    std::string* error) override;
 
+  std::unique_ptr<QuicData> ApplyL2Headers(const QuicData& l3_packet);
+
+  std::unique_ptr<QuicData> ConsumeL2Headers(const QuicData& l2_packet);
+
   int fd_ = -1;
   size_t mtu_;
   KernelInterface* kernel_;
+  NetlinkInterface* netlink_;
+  const std::string ifname_;
+
+  const bool is_tap_;
+  uint8_t tap_mac_[ETH_ALEN]{};
+  bool mac_initialized_ = false;
 
   StatsInterface* stats_;
 };
diff --git a/quic/qbone/bonnet/tun_device_packet_exchanger_test.cc b/quic/qbone/bonnet/tun_device_packet_exchanger_test.cc
index c8f3ff0..8abf9a3 100644
--- a/quic/qbone/bonnet/tun_device_packet_exchanger_test.cc
+++ b/quic/qbone/bonnet/tun_device_packet_exchanger_test.cc
@@ -30,11 +30,9 @@
 class TunDevicePacketExchangerTest : public QuicTest {
  protected:
   TunDevicePacketExchangerTest()
-      : exchanger_(kMtu,
-                   &mock_kernel_,
-                   &mock_visitor_,
-                   kMaxPendingPackets,
-                   &mock_stats_) {
+      : exchanger_(kMtu, &mock_kernel_, nullptr, &mock_visitor_,
+                   kMaxPendingPackets, false, &mock_stats_,
+                   absl::string_view()) {
     exchanger_.set_file_descriptor(kFd);
   }
 
diff --git a/quic/qbone/bonnet/tun_device_test.cc b/quic/qbone/bonnet/tun_device_test.cc
index 6a02f07..18b818c 100644
--- a/quic/qbone/bonnet/tun_device_test.cc
+++ b/quic/qbone/bonnet/tun_device_test.cc
@@ -120,10 +120,10 @@
   int next_fd_ = 100;
 };
 
-// A TunDevice can be initialized and up
+// A TunTapDevice can be initialized and up
 TEST_F(TunDeviceTest, BasicWorkFlow) {
   SetInitExpectations(/* mtu = */ 1500, /* persist = */ false);
-  TunDevice tun_device(kDeviceName, 1500, false, true, &mock_kernel_);
+  TunTapDevice tun_device(kDeviceName, 1500, false, true, false, &mock_kernel_);
   EXPECT_TRUE(tun_device.Init());
   EXPECT_GT(tun_device.GetFileDescriptor(), -1);
 
@@ -136,7 +136,7 @@
   SetInitExpectations(/* mtu = */ 1500, /* persist = */ false);
   EXPECT_CALL(mock_kernel_, open(StrEq("/dev/net/tun"), _))
       .WillOnce(Return(-1));
-  TunDevice tun_device(kDeviceName, 1500, false, true, &mock_kernel_);
+  TunTapDevice tun_device(kDeviceName, 1500, false, true, false, &mock_kernel_);
   EXPECT_FALSE(tun_device.Init());
   EXPECT_EQ(tun_device.GetFileDescriptor(), -1);
   ExpectDown(false);
@@ -145,7 +145,7 @@
 TEST_F(TunDeviceTest, FailToCheckFeature) {
   SetInitExpectations(/* mtu = */ 1500, /* persist = */ false);
   EXPECT_CALL(mock_kernel_, ioctl(_, TUNGETFEATURES, _)).WillOnce(Return(-1));
-  TunDevice tun_device(kDeviceName, 1500, false, true, &mock_kernel_);
+  TunTapDevice tun_device(kDeviceName, 1500, false, true, false, &mock_kernel_);
   EXPECT_FALSE(tun_device.Init());
   EXPECT_EQ(tun_device.GetFileDescriptor(), -1);
   ExpectDown(false);
@@ -159,7 +159,7 @@
         *actual_features = IFF_TUN | IFF_ONE_QUEUE;
         return 0;
       }));
-  TunDevice tun_device(kDeviceName, 1500, false, true, &mock_kernel_);
+  TunTapDevice tun_device(kDeviceName, 1500, false, true, false, &mock_kernel_);
   EXPECT_FALSE(tun_device.Init());
   EXPECT_EQ(tun_device.GetFileDescriptor(), -1);
   ExpectDown(false);
@@ -168,7 +168,7 @@
 TEST_F(TunDeviceTest, FailToSetFlag) {
   SetInitExpectations(/* mtu = */ 1500, /* persist = */ true);
   EXPECT_CALL(mock_kernel_, ioctl(_, TUNSETIFF, _)).WillOnce(Return(-1));
-  TunDevice tun_device(kDeviceName, 1500, true, true, &mock_kernel_);
+  TunTapDevice tun_device(kDeviceName, 1500, true, true, false, &mock_kernel_);
   EXPECT_FALSE(tun_device.Init());
   EXPECT_EQ(tun_device.GetFileDescriptor(), -1);
 }
@@ -176,7 +176,7 @@
 TEST_F(TunDeviceTest, FailToPersistDevice) {
   SetInitExpectations(/* mtu = */ 1500, /* persist = */ true);
   EXPECT_CALL(mock_kernel_, ioctl(_, TUNSETPERSIST, _)).WillOnce(Return(-1));
-  TunDevice tun_device(kDeviceName, 1500, true, true, &mock_kernel_);
+  TunTapDevice tun_device(kDeviceName, 1500, true, true, false, &mock_kernel_);
   EXPECT_FALSE(tun_device.Init());
   EXPECT_EQ(tun_device.GetFileDescriptor(), -1);
 }
@@ -184,7 +184,7 @@
 TEST_F(TunDeviceTest, FailToOpenSocket) {
   SetInitExpectations(/* mtu = */ 1500, /* persist = */ true);
   EXPECT_CALL(mock_kernel_, socket(AF_INET6, _, _)).WillOnce(Return(-1));
-  TunDevice tun_device(kDeviceName, 1500, true, true, &mock_kernel_);
+  TunTapDevice tun_device(kDeviceName, 1500, true, true, false, &mock_kernel_);
   EXPECT_FALSE(tun_device.Init());
   EXPECT_EQ(tun_device.GetFileDescriptor(), -1);
 }
@@ -192,14 +192,14 @@
 TEST_F(TunDeviceTest, FailToSetMtu) {
   SetInitExpectations(/* mtu = */ 1500, /* persist = */ true);
   EXPECT_CALL(mock_kernel_, ioctl(_, SIOCSIFMTU, _)).WillOnce(Return(-1));
-  TunDevice tun_device(kDeviceName, 1500, true, true, &mock_kernel_);
+  TunTapDevice tun_device(kDeviceName, 1500, true, true, false, &mock_kernel_);
   EXPECT_FALSE(tun_device.Init());
   EXPECT_EQ(tun_device.GetFileDescriptor(), -1);
 }
 
 TEST_F(TunDeviceTest, FailToUp) {
   SetInitExpectations(/* mtu = */ 1500, /* persist = */ true);
-  TunDevice tun_device(kDeviceName, 1500, true, true, &mock_kernel_);
+  TunTapDevice tun_device(kDeviceName, 1500, true, true, false, &mock_kernel_);
   EXPECT_TRUE(tun_device.Init());
   EXPECT_GT(tun_device.GetFileDescriptor(), -1);
 
diff --git a/quic/qbone/platform/icmp_packet.cc b/quic/qbone/platform/icmp_packet.cc
index 50e3f2b..0acd712 100644
--- a/quic/qbone/platform/icmp_packet.cc
+++ b/quic/qbone/platform/icmp_packet.cc
@@ -17,7 +17,10 @@
 constexpr size_t kIPv6HeaderSize = sizeof(ip6_hdr);
 constexpr size_t kICMPv6HeaderSize = sizeof(icmp6_hdr);
 constexpr size_t kIPv6MinPacketSize = 1280;
-constexpr size_t kIcmpTtl = 64;
+
+// Hop limit set to 255 to satisfy:
+// https://datatracker.ietf.org/doc/html/rfc4861#section-11.2
+constexpr size_t kIcmpTtl = 255;
 constexpr size_t kICMPv6BodyMaxSize =
     kIPv6MinPacketSize - kIPv6HeaderSize - kICMPv6HeaderSize;
 
diff --git a/quic/qbone/platform/icmp_packet_test.cc b/quic/qbone/platform/icmp_packet_test.cc
index 9721e36..1e6a2e9 100644
--- a/quic/qbone/platform/icmp_packet_test.cc
+++ b/quic/qbone/platform/icmp_packet_test.cc
@@ -37,8 +37,8 @@
     0x00, 0x40,
     // Next header is 58
     0x3a,
-    // Hop limit is 64
-    0x40,
+    // Hop limit is 255
+    0xFF,
     // Source address of fe80:1:2:3:4::1
     0xfe, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03,
     0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,