QBONE ICMP reachable: convert to "ping sockets" to avoid the need for `SOCK_RAW` permissions. ["ping sockets"](https://lore.kernel.org/all/20110510180957.GA3262@albatros/) are a kernel feature from ~2011 that allow initiating ICMP echo requests via `SOCK_DGRAM` with protocol `IPPPROTO_ICMPV6`, instead of requiring `SOCK_RAW`. Once such a socket is created, the kernel expects `send`/`sendto` payloads to start directly with the ICMP header. The kernel commit message says, in part: >ICMP headers given to send() are checked and sanitized. > > (... ) The id is set to the number (local port) of the socket which could perhaps have been written more clearly as "The id MUST BE set to the local port", as in my experience not doing so results in `EINVAL`. Because QBONE may ping addresses that are not explicitly listed in the routing table, additionally force the destination address scope ID to be that of the TUN/TAP interface used by the bonnet. For typical usage (tun device `qbone0`) this is the equivalent of the `%qbone0` bit of the command line: ``` $ ping -6 <destaddr>%qbone0 ``` PiperOrigin-RevId: 847850750
diff --git a/quiche/quic/qbone/bonnet/icmp_reachable.cc b/quiche/quic/qbone/bonnet/icmp_reachable.cc index 3b967a7..83239dc 100644 --- a/quiche/quic/qbone/bonnet/icmp_reachable.cc +++ b/quiche/quic/qbone/bonnet/icmp_reachable.cc
@@ -30,7 +30,8 @@ const char kUnknownSource[] = "UNKNOWN"; const char kNoSource[] = "N/A"; -IcmpReachable::IcmpReachable(QuicIpAddress source, QuicIpAddress destination, +IcmpReachable::IcmpReachable(absl::string_view interface_name, + QuicIpAddress source, QuicIpAddress destination, QuicTime::Delta timeout, KernelInterface* kernel, QuicEventLoop* event_loop, StatsInterface* stats) : timeout_(timeout), @@ -45,6 +46,9 @@ recv_fd_(0) { src_.sin6_family = AF_INET6; dst_.sin6_family = AF_INET6; + // Ensure the destination has its scope set to the QBONE TUN/TAP device. + dst_.sin6_scope_id = + kernel_->if_nametoindex(std::string(interface_name).c_str()); memcpy(&src_.sin6_addr, source.ToPackedString().data(), kIPv6AddrSize); memcpy(&dst_.sin6_addr, destination.ToPackedString().data(), kIPv6AddrSize); @@ -63,28 +67,29 @@ } bool IcmpReachable::Init() { - send_fd_ = kernel_->socket(PF_INET6, SOCK_RAW | SOCK_NONBLOCK, IPPROTO_RAW); + send_fd_ = + kernel_->socket(PF_INET6, SOCK_DGRAM | SOCK_NONBLOCK, IPPROTO_ICMPV6); if (send_fd_ < 0) { - QUIC_PLOG(ERROR) << "Unable to open socket."; + QUIC_PLOG(ERROR) << "Unable to open send socket."; return false; } if (kernel_->bind(send_fd_, reinterpret_cast<struct sockaddr*>(&src_), sizeof(sockaddr_in6)) < 0) { - QUIC_PLOG(ERROR) << "Unable to bind socket."; + QUIC_PLOG(ERROR) << "Unable to bind send socket."; return false; } recv_fd_ = kernel_->socket(PF_INET6, SOCK_RAW | SOCK_NONBLOCK, IPPROTO_ICMPV6); if (recv_fd_ < 0) { - QUIC_PLOG(ERROR) << "Unable to open socket."; + QUIC_PLOG(ERROR) << "Unable to open recv socket."; return false; } if (kernel_->bind(recv_fd_, reinterpret_cast<struct sockaddr*>(&src_), sizeof(sockaddr_in6)) < 0) { - QUIC_PLOG(ERROR) << "Unable to bind socket."; + QUIC_PLOG(ERROR) << "Unable to bind recv socket."; return false; } @@ -103,12 +108,25 @@ } alarm_->Set(clock_->Now()); + // Obtain the local port assigned to send_fd_. + struct sockaddr_in6 sa = {}; + socklen_t addrlen = sizeof(sa); + if (kernel_->getsockname(send_fd_, reinterpret_cast<struct sockaddr*>(&sa), + &addrlen) == -1) { + QUIC_PLOG(ERROR) << "Unable to getsockname:"; + } + + // Per the "ping socket" kernel commit message: + // "ICMP headers given to send() are checked and sanitized." + // "The type must be ICMP_ECHO and the code must be zero" + // "The id is set to the number (local port) of the socket, the + // checksum is always recomputed." absl::WriterMutexLock mu(header_lock_); icmp_header_.icmp6_type = ICMP6_ECHO_REQUEST; icmp_header_.icmp6_code = 0; - - QuicRandom::GetInstance()->RandBytes(&icmp_header_.icmp6_id, - sizeof(uint16_t)); + icmp_header_.icmp6_id = sa.sin6_port; + icmp_header_.icmp6_cksum = 0; + icmp_header_.icmp6_seq = 0; return true; } @@ -165,23 +183,22 @@ QUIC_VLOG(1) << "Timed out on sequence: " << icmp_header_.icmp6_seq; stats_->OnEvent({Status::UNREACHABLE, QuicTime::Delta::Zero(), kNoSource}); } - icmp_header_.icmp6_seq++; - CreateIcmpPacket(src_.sin6_addr, dst_.sin6_addr, icmp_header_, "", - [this](absl::string_view packet) { - QUIC_VLOG(2) << quiche::QuicheTextUtils::HexDump(packet); - ssize_t size = kernel_->sendto( - send_fd_, packet.data(), packet.size(), 0, - reinterpret_cast<struct sockaddr*>(&dst_), - sizeof(sockaddr_in6)); + QUIC_VLOG(2) << "ICMP Header: " + << quiche::QuicheTextUtils::HexDump(absl::string_view( + reinterpret_cast<const char*>(&icmp_header_), + sizeof(icmp_header_))); - if (size < static_cast<ssize_t>(packet.size())) { - stats_->OnWriteError(errno); - QUIC_PLOG(ERROR) << "Unable to send ICMP echo request:"; - } - start_ = clock_->Now(); - }); + ssize_t size = kernel_->sendto(send_fd_, &icmp_header_, sizeof(icmp6_hdr), 0, + reinterpret_cast<struct sockaddr*>(&dst_), + sizeof(sockaddr_in6)); + + if (size < static_cast<ssize_t>(sizeof(icmp6_hdr))) { + stats_->OnWriteError(errno); + QUIC_PLOG(ERROR) << "Unable to send ICMP echo request:"; + } + start_ = clock_->Now(); alarm_->Set(clock_->ApproximateNow() + timeout_); }
diff --git a/quiche/quic/qbone/bonnet/icmp_reachable.h b/quiche/quic/qbone/bonnet/icmp_reachable.h index 89e15aa..8efaade 100644 --- a/quiche/quic/qbone/bonnet/icmp_reachable.h +++ b/quiche/quic/qbone/bonnet/icmp_reachable.h
@@ -60,6 +60,7 @@ virtual void OnWriteError(int error) = 0; }; + // |interface_name| is the name of the TUN/TAP device bound in the kernel. // |source| is the IPv6 address bound to the interface that IcmpReachable will // send Echo Requests on. // |destination| is the IPv6 address of the destination of the Echo Requests. @@ -72,9 +73,10 @@ // Server's thread. // |stats| is not owned, but should outlive this instance. It will be called // back on Echo Replies, timeouts, and I/O errors. - IcmpReachable(QuicIpAddress source, QuicIpAddress destination, - QuicTime::Delta timeout, KernelInterface* kernel, - QuicEventLoop* event_loop, StatsInterface* stats); + IcmpReachable(absl::string_view interface_name, QuicIpAddress source, + QuicIpAddress destination, QuicTime::Delta timeout, + KernelInterface* kernel, QuicEventLoop* event_loop, + StatsInterface* stats); ~IcmpReachable() override;
diff --git a/quiche/quic/qbone/bonnet/icmp_reachable_test.cc b/quiche/quic/qbone/bonnet/icmp_reachable_test.cc index 555aa9f..81dbbce 100644 --- a/quiche/quic/qbone/bonnet/icmp_reachable_test.cc +++ b/quiche/quic/qbone/bonnet/icmp_reachable_test.cc
@@ -21,20 +21,23 @@ namespace { using ::testing::_; +using ::testing::DoAll; using ::testing::InSequence; using ::testing::Return; +using ::testing::SetArgPointee; using ::testing::StrictMock; +constexpr absl::string_view kInterfaceName = "qbone0"; constexpr char kSourceAddress[] = "fe80:1:2:3:4::1"; constexpr char kDestinationAddress[] = "fe80:4:3:2:1::1"; constexpr int kFakeWriteFd = 0; +constexpr int kSendPort = 12345; -icmp6_hdr GetHeaderFromPacket(const void* buf, size_t len) { - QUICHE_CHECK_GE(len, sizeof(ip6_hdr) + sizeof(icmp6_hdr)); - - auto* buffer = reinterpret_cast<const char*>(buf); - return *reinterpret_cast<const icmp6_hdr*>(&buffer[sizeof(ip6_hdr)]); +icmp6_hdr ParseIcmpHeader(const void* buf, size_t len) { + QUICHE_CHECK_EQ(len, sizeof(icmp6_hdr)); + return *reinterpret_cast<const icmp6_hdr*>( + &(reinterpret_cast<const char*>(buf))[0]); } class StatsInterface : public IcmpReachable::StatsInterface { @@ -97,6 +100,7 @@ void SetFdExpectations() { InSequence seq; + EXPECT_CALL(kernel_, if_nametoindex(_)); EXPECT_CALL(kernel_, socket(_, _, _)).WillOnce(Return(kFakeWriteFd)); EXPECT_CALL(kernel_, bind(kFakeWriteFd, _, _)).WillOnce(Return(0)); @@ -104,6 +108,10 @@ EXPECT_CALL(kernel_, bind(read_fd_, _, _)).WillOnce(Return(0)); EXPECT_CALL(kernel_, setsockopt(read_fd_, SOL_ICMPV6, ICMP6_FILTER, _, _)); + EXPECT_CALL(kernel_, getsockname(_, _, _)) + .WillOnce(DoAll(SetArgPointee<1>( + *reinterpret_cast<struct sockaddr*>(&send_socket_)), + Return(0))); EXPECT_CALL(kernel_, close(read_fd_)).WillOnce([](int fd) { return close(fd); @@ -114,6 +122,8 @@ QuicIpAddress source_; QuicIpAddress destination_; + struct sockaddr_in6 send_socket_ = {.sin6_port = kSendPort}; + int read_fd_; int read_src_fd_; @@ -123,18 +133,20 @@ }; TEST_F(IcmpReachableTest, SendsPings) { - IcmpReachable reachable(source_, destination_, QuicTime::Delta::Zero(), - &kernel_, event_loop_.get(), &stats_); - SetFdExpectations(); + IcmpReachable reachable(kInterfaceName, source_, destination_, + QuicTime::Delta::Zero(), &kernel_, event_loop_.get(), + &stats_); + ASSERT_TRUE(reachable.Init()); EXPECT_CALL(kernel_, sendto(kFakeWriteFd, _, _, _, _, _)) .WillOnce([](int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen) { - auto icmp_header = GetHeaderFromPacket(buf, len); + auto icmp_header = ParseIcmpHeader(buf, len); EXPECT_EQ(icmp_header.icmp6_type, ICMP6_ECHO_REQUEST); EXPECT_EQ(icmp_header.icmp6_seq, 1); + EXPECT_EQ(icmp_header.icmp6_id, kSendPort); return len; }); @@ -143,10 +155,11 @@ } TEST_F(IcmpReachableTest, HandlesUnreachableEvents) { - IcmpReachable reachable(source_, destination_, QuicTime::Delta::Zero(), - &kernel_, event_loop_.get(), &stats_); - SetFdExpectations(); + IcmpReachable reachable(kInterfaceName, source_, destination_, + QuicTime::Delta::Zero(), &kernel_, event_loop_.get(), + &stats_); + ASSERT_TRUE(reachable.Init()); EXPECT_CALL(kernel_, sendto(kFakeWriteFd, _, _, _, _, _)) @@ -165,10 +178,11 @@ } TEST_F(IcmpReachableTest, HandlesReachableEvents) { - IcmpReachable reachable(source_, destination_, QuicTime::Delta::Zero(), - &kernel_, event_loop_.get(), &stats_); - SetFdExpectations(); + IcmpReachable reachable(kInterfaceName, source_, destination_, + QuicTime::Delta::Zero(), &kernel_, event_loop_.get(), + &stats_); + ASSERT_TRUE(reachable.Init()); icmp6_hdr last_request_hdr{}; @@ -177,7 +191,7 @@ .WillRepeatedly([&last_request_hdr]( int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen) { - last_request_hdr = GetHeaderFromPacket(buf, len); + last_request_hdr = ParseIcmpHeader(buf, len); return len; }); @@ -209,10 +223,11 @@ } TEST_F(IcmpReachableTest, HandlesWriteErrors) { - IcmpReachable reachable(source_, destination_, QuicTime::Delta::Zero(), - &kernel_, event_loop_.get(), &stats_); - SetFdExpectations(); + IcmpReachable reachable(kInterfaceName, source_, destination_, + QuicTime::Delta::Zero(), &kernel_, event_loop_.get(), + &stats_); + ASSERT_TRUE(reachable.Init()); EXPECT_CALL(kernel_, sendto(kFakeWriteFd, _, _, _, _, _)) @@ -227,10 +242,11 @@ } TEST_F(IcmpReachableTest, HandlesReadErrors) { - IcmpReachable reachable(source_, destination_, QuicTime::Delta::Zero(), - &kernel_, event_loop_.get(), &stats_); - SetFdExpectations(); + IcmpReachable reachable(kInterfaceName, source_, destination_, + QuicTime::Delta::Zero(), &kernel_, event_loop_.get(), + &stats_); + ASSERT_TRUE(reachable.Init()); EXPECT_CALL(kernel_, sendto(kFakeWriteFd, _, _, _, _, _))
diff --git a/quiche/quic/qbone/platform/kernel_interface.h b/quiche/quic/qbone/platform/kernel_interface.h index d126af2..f1f97a1 100644 --- a/quiche/quic/qbone/platform/kernel_interface.h +++ b/quiche/quic/qbone/platform/kernel_interface.h
@@ -7,6 +7,7 @@ #include <errno.h> #include <fcntl.h> +#include <net/if.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <sys/types.h> @@ -41,6 +42,9 @@ virtual int setsockopt(int fd, int level, int optname, const void* optval, socklen_t optlen) = 0; virtual ssize_t write(int fd, const void* buf, size_t count) = 0; + virtual int getsockname(int sockfd, struct sockaddr* addr, + socklen_t* addrlen) = 0; + virtual unsigned int if_nametoindex(const char* ifname) = 0; }; // It is unfortunate to have R here, but std::result_of cannot be used. @@ -126,6 +130,15 @@ static Runner syscall("write"); return syscall.Run(&::write, fd, buf, count); } + int getsockname(int sockfd, struct sockaddr* addr, + socklen_t* addrlen) override { + static Runner syscall("getsockname"); + return syscall.Run(&::getsockname, sockfd, addr, addrlen); + } + unsigned int if_nametoindex(const char* ifname) override { + static Runner syscall("if_nametoindex"); + return syscall.Run(&::if_nametoindex, ifname); + } }; class DefaultKernelRunner {
diff --git a/quiche/quic/qbone/platform/mock_kernel.h b/quiche/quic/qbone/platform/mock_kernel.h index aff8a83..d86238d 100644 --- a/quiche/quic/qbone/platform/mock_kernel.h +++ b/quiche/quic/qbone/platform/mock_kernel.h
@@ -41,6 +41,10 @@ MOCK_METHOD(int, setsockopt, (int, int, int, const void*, socklen_t), (override)); MOCK_METHOD(ssize_t, write, (int fd, const void*, size_t count), (override)); + MOCK_METHOD(int, getsockname, + (int sockfd, struct sockaddr* addr, socklen_t* addrlen), + (override)); + MOCK_METHOD(unsigned int, if_nametoindex, (const char* ifname), (override)); }; } // namespace quic