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,