blob: 01a99db777a3c0170f5993aec913a86397be8018 [file]
// Copyright (c) 2019 The Chromium Authors. All rights reserved.
// 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/tun_device_packet_exchanger.h"
#include <netinet/icmp6.h>
#include <netinet/ip6.h>
#include <sys/uio.h>
#include <algorithm>
#include <cerrno>
#include <cstddef>
#include <cstring>
#include <memory>
#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"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "absl/types/span.h"
#include "quiche/quic/platform/api/quic_bug_tracker.h"
#include "quiche/quic/platform/api/quic_ip_address.h"
#include "quiche/quic/platform/api/quic_logging.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_client_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)
: 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) {}
TunDevicePacketExchanger::~TunDevicePacketExchanger() {
QUIC_BUG_IF(bonnet_tun_device_packet_exchanger_not_stopped,
read_fd_ >= 0 || write_fd_ >= 0);
}
void TunDevicePacketExchanger::Start(int read_fd, int write_fd) {
// 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,
(read_fd_ >= 0 || write_fd_ >= 0) &&
(read_fd_ != read_fd || write_fd_ != write_fd));
QUIC_BUG_IF(qbone_tun_device_packet_exchanger_invalid_read_fd, read_fd < 0);
QUIC_BUG_IF(qbone_tun_device_packet_exchanger_invalid_write_fd, write_fd < 0);
read_fd_ = read_fd;
write_fd_ = write_fd;
}
void TunDevicePacketExchanger::Stop() {
// This implementation does not employ any worker threads, so there cannot be
// any pending operations to wait for completion.
read_fd_ = -1;
write_fd_ = -1;
}
int TunDevicePacketExchanger::OnReadFromNetworkReady(
int max_packets_to_read, QboneClientInterface* qbone_client) {
if (read_fd_ < 0) {
QUIC_BUG(qbone_tun_device_packet_exchanger_read_with_invalid_fd)
<< "Invalid file descriptor of the TUN device: " << read_fd_;
return 0;
}
int packets_read = 0;
for (int i = 0; i < max_packets_to_read; ++i) {
// Should be at least one packet available to read if this is called, so
// fully process any blocked errors on the first read. After that, blocked
// errors are just the signal that there are no more packets to read.
bool exchange_blocked_error = packets_read == 0;
if (ReadAndExchangeSinglePacket(qbone_client, exchange_blocked_error)) {
packets_read++;
} else {
break;
}
}
return packets_read;
}
void TunDevicePacketExchanger::WritePacketToNetwork(const char* packet,
size_t size) {
if (write_fd_ < 0) {
QUIC_BUG(qbone_tun_device_packet_exchanger_write_with_invalid_fd)
<< "Invalid file descriptor of the TUN device: " << write_fd_;
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;
iov[1].iov_base = const_cast<char*>(packet);
iov[1].iov_len = size;
absl::Status status = absl::OkStatus();
absl::Time start = absl::Now();
int result = kernel_->writev(write_fd_, iov, ABSL_ARRAYSIZE(iov));
if (result < 0) {
status = absl::ErrnoToStatus(errno, "Write to the TUN device failed.");
}
absl::Duration latency = std::max(absl::Now() - start, absl::ZeroDuration());
if (!status.ok()) {
QUIC_LOG_EVERY_N_SEC(ERROR, 60) << "Packet write failed: " << status;
visitor_.OnWrite(std::move(status));
return;
}
visitor_.OnWrite(std::vector<WriteResult>{WriteResult{
.packet =
absl::MakeSpan(reinterpret_cast<const std::byte*>(packet), size),
.latency = latency}});
}
bool TunDevicePacketExchanger::ReadAndExchangeSinglePacket(
QboneClientInterface* qbone_client, bool exchange_blocked_error) {
QUICHE_DCHECK_GE(read_fd_, 0);
// TODO(ericorth): Consider allocating these buffers once and reusing rather
// than repeating for each packet.
ethhdr eth_header;
struct iovec iov[2];
iov[0].iov_base = is_tap_ ? &eth_header : nullptr;
iov[0].iov_len = is_tap_ ? ETH_HLEN : 0;
iov[1].iov_base = read_buffer_.data();
iov[1].iov_len = read_buffer_.size();
absl::Status status = absl::OkStatus();
absl::Time start = absl::Now();
int result = kernel_->readv(read_fd_, iov, ABSL_ARRAYSIZE(iov));
int saved_errno = errno;
absl::Duration latency = std::max(absl::Now() - start, absl::ZeroDuration());
if (result < 0) {
if ((saved_errno == EAGAIN || saved_errno == EWOULDBLOCK) &&
!exchange_blocked_error) {
// No more packets available to read.
return false;
} else {
status =
absl::ErrnoToStatus(saved_errno, "Read from the TUN device failed.");
}
} else if (result == 0) {
// Note that 0 means end of file, but we're talking about a TUN device -
// there is no end of file. Therefore 0 also indicates error.
status = absl::InternalError(
"Read from the TUN device returned unexpected 0 (EOF).");
}
if (!status.ok()) {
QUIC_LOG_EVERY_N_SEC(ERROR, 60) << "Packet read failed: " << status;
visitor_.OnRead(std::move(status));
return false; // Assume no more packets after any socket read error.
}
int l3_packet_size = is_tap_ ? result - ETH_HLEN : result;
if (l3_packet_size <= 0 || l3_packet_size > read_buffer_.size()) {
absl::Status error =
absl::InternalError(absl::StrCat("Invalid packet size."));
QUIC_LOG_EVERY_N_SEC(ERROR, 60) << "Packet read failed: " << error;
visitor_.OnRead(std::move(error));
return true; // Invalid packet does not mean there are no more packets.
}
absl::Span<const std::byte> l3_packet =
absl::MakeSpan(read_buffer_.data(), l3_packet_size);
if (is_tap_) {
switch (ValidateL2Headers(eth_header, l3_packet)) {
case L2ValidationResult::kInvalid: {
absl::Status error = absl::InvalidArgumentError("Invalid L2 headers.");
visitor_.OnRead(std::move(error));
return true; // Invalid packet does not mean there are no more packets.
}
case L2ValidationResult::kValidLinkLocal:
return true;
case L2ValidationResult::kValidNormal:
// Packet is valid and should be forwarded to the tunnel. Fall through
// to normal processing.
break;
}
}
visitor_.OnRead(std::vector<ReadResult>{
ReadResult{.packet = l3_packet, .latency = latency}});
qbone_client->ProcessPacketFromNetwork(absl::string_view(
reinterpret_cast<const char*>(l3_packet.data()), l3_packet.size()));
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)) {
return L2ValidationResult::kInvalid;
}
constexpr auto kIp6PrefixLen = sizeof(ip6_hdr);
constexpr auto kIcmp6PrefixLen = kIp6PrefixLen + sizeof(icmp6_hdr);
if (packet.length() < kIp6PrefixLen) {
// Packet is too short to be ipv6. Drop it.
return L2ValidationResult::kInvalid;
}
auto* ip_hdr = reinterpret_cast<const ip6_hdr*>(packet.data());
const bool is_icmp = ip_hdr->ip6_ctlun.ip6_un1.ip6_un1_nxt == IPPROTO_ICMPV6;
bool is_neighbor_solicit = false;
if (is_icmp) {
if (packet.length() < kIcmp6PrefixLen) {
// Packet is too short to be icmp6. Drop it.
return L2ValidationResult::kInvalid;
}
is_neighbor_solicit =
reinterpret_cast<const icmp6_hdr*>(packet.subspan(kIp6PrefixLen).data())
->icmp6_type == ND_NEIGHBOR_SOLICIT;
}
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);
if (icmp6_payload.size() < sizeof(in6_addr)) {
// Packet is too short to contain a valid ICMPv6 payload. Drop it.
return L2ValidationResult::kInvalid;
}
QuicIpAddress target_address(
*reinterpret_cast<const in6_addr*>(icmp6_payload.data()));
if (target_address != *QboneConstants::GatewayAddress()) {
// Only respond to solicitations for our gateway address
return L2ValidationResult::kValidLinkLocal;
}
// 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.data(), 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], eth_hdr_.h_source,
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) {
WritePacketToNetwork(packet.data(), packet.size());
});
return L2ValidationResult::kValidLinkLocal;
}
return L2ValidationResult::kValidNormal;
}
} // namespace quic