| // Copyright 2019 The ChromiumOS Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "patchpanel/ndproxy.h" | |
| #include <errno.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <sysexits.h> | |
| #include <unistd.h> | |
| #include <arpa/inet.h> | |
| #include <linux/filter.h> | |
| #include <linux/if_packet.h> | |
| #include <linux/in6.h> | |
| #include <linux/netlink.h> | |
| #include <linux/rtnetlink.h> | |
| #include <net/ethernet.h> | |
| #include <net/if.h> | |
| #include <netinet/icmp6.h> | |
| #include <sys/ioctl.h> | |
| #include <sys/socket.h> | |
| #include <fstream> | |
| #include <memory> | |
| #include <optional> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include <base/functional/bind.h> | |
| #include <base/logging.h> | |
| #include <base/strings/string_number_conversions.h> | |
| #include <base/strings/string_split.h> | |
| #include <chromeos/net-base/byte_utils.h> | |
| #include <chromeos/net-base/mac_address.h> | |
| #include <chromeos/net-base/socket.h> | |
| #include "patchpanel/ipc.h" | |
| #include "patchpanel/minijailed_process_runner.h" | |
| #include "patchpanel/net_util.h" | |
| namespace patchpanel { | |
| namespace { | |
| // Currently when we are unable to resolve the destination MAC for a proxied | |
| // packet (note this can only happen for unicast NA and NS), we send the packet | |
| // using all-nodes multicast MAC. Change this flag to true to drop those packets | |
| // on uplinks instead. | |
| // TODO(b/244271776): Investigate if it is safe to drop such packets, or if | |
| // there is a legitimate case that these packets are actually required. | |
| constexpr bool kDropUnresolvableUnicastToUpstream = false; | |
| constexpr net_base::MacAddress kAllNodesMulticastMacAddress( | |
| 0x33, 0x33, 0, 0, 0, 0x01); | |
| constexpr net_base::MacAddress kAllRoutersMulticastMacAddress( | |
| 0x33, 0x33, 0, 0, 0, 0x02); | |
| constexpr net_base::MacAddress kSolicitedNodeMulticastMacAddressPrefix( | |
| 0x33, 0x33, 0xff, 0, 0, 0); | |
| constexpr net_base::IPv6Address kAllNodesMulticastAddress( | |
| 0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01); | |
| constexpr net_base::IPv6Address kAllRoutersMulticastAddress( | |
| 0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x02); | |
| constexpr int kSolicitedGroupSuffixLength = 3; | |
| const net_base::IPv6CIDR kSolicitedNodeMulticastCIDR = | |
| *net_base::IPv6CIDR::CreateFromCIDRString("ff02::1:ff00:0/104"); | |
| // These filter instructions assume that the input is an IPv6 packet and check | |
| // that the packet is an ICMPv6 packet of whose ICMPv6 type is one of: neighbor | |
| // solicitation, neighbor advertisement, router solicitation, or router | |
| // advertisement. | |
| sock_filter kNDPacketBpfInstructions[] = { | |
| // Load IPv6 next header. | |
| BPF_STMT(BPF_LD | BPF_B | BPF_IND, offsetof(ip6_hdr, ip6_nxt)), | |
| // Check if equals ICMPv6, if not, then goto return 0. | |
| BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_ICMPV6, 0, 6), | |
| // Move index to start of ICMPv6 header. | |
| BPF_STMT(BPF_LDX | BPF_IMM, sizeof(ip6_hdr)), | |
| // Load ICMPv6 type. | |
| BPF_STMT(BPF_LD | BPF_B | BPF_IND, offsetof(icmp6_hdr, icmp6_type)), | |
| // Check if is ND ICMPv6 message. | |
| BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ND_ROUTER_SOLICIT, 4, 0), | |
| BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ND_ROUTER_ADVERT, 3, 0), | |
| BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ND_NEIGHBOR_SOLICIT, 2, 0), | |
| BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ND_NEIGHBOR_ADVERT, 1, 0), | |
| // Return 0. | |
| BPF_STMT(BPF_RET | BPF_K, 0), | |
| // Return MAX. | |
| BPF_STMT(BPF_RET | BPF_K, IP_MAXPACKET), | |
| }; | |
| const sock_fprog kNDPacketBpfProgram = { | |
| .len = sizeof(kNDPacketBpfInstructions) / sizeof(sock_filter), | |
| .filter = kNDPacketBpfInstructions}; | |
| std::string Icmp6TypeName(uint32_t type) { | |
| switch (type) { | |
| case ND_ROUTER_SOLICIT: | |
| return "ND_ROUTER_SOLICIT"; | |
| case ND_ROUTER_ADVERT: | |
| return "ND_ROUTER_ADVERT"; | |
| case ND_NEIGHBOR_SOLICIT: | |
| return "ND_NEIGHBOR_SOLICIT"; | |
| case ND_NEIGHBOR_ADVERT: | |
| return "ND_NEIGHBOR_ADVERT"; | |
| default: | |
| return "UNKNOWN"; | |
| } | |
| } | |
| std::optional<net_base::IPv6CIDR> NDOptPrefixInfoToCIDR( | |
| const nd_opt_prefix_info* info) { | |
| if (info == nullptr) { | |
| return std::nullopt; | |
| } | |
| return net_base::IPv6CIDR::CreateFromAddressAndPrefix( | |
| net_base::IPv6Address(info->nd_opt_pi_prefix), | |
| info->nd_opt_pi_prefix_len); | |
| } | |
| [[maybe_unused]] std::string Icmp6ToString(const uint8_t* packet, size_t len) { | |
| const ip6_hdr* ip6 = reinterpret_cast<const ip6_hdr*>(packet); | |
| const icmp6_hdr* icmp6 = | |
| reinterpret_cast<const icmp6_hdr*>(packet + sizeof(ip6_hdr)); | |
| if (len < sizeof(ip6_hdr) + sizeof(icmp6_hdr)) | |
| return "<packet too small>"; | |
| if (ip6->ip6_nxt != IPPROTO_ICMPV6) | |
| return "<not ICMP6 packet>"; | |
| if (icmp6->icmp6_type < ND_ROUTER_SOLICIT || | |
| icmp6->icmp6_type > ND_NEIGHBOR_ADVERT) | |
| return "<not ND ICMP6 packet>"; | |
| std::stringstream ss; | |
| ss << Icmp6TypeName(icmp6->icmp6_type) << " " | |
| << net_base::IPv6Address(ip6->ip6_src) << " -> " | |
| << net_base::IPv6Address(ip6->ip6_dst); | |
| switch (icmp6->icmp6_type) { | |
| case ND_NEIGHBOR_SOLICIT: | |
| case ND_NEIGHBOR_ADVERT: { | |
| // NS and NA has same packet format for Target Address | |
| ss << ", target " | |
| << net_base::IPv6Address( | |
| reinterpret_cast<const nd_neighbor_solicit*>(icmp6) | |
| ->nd_ns_target); | |
| break; | |
| } | |
| case ND_ROUTER_SOLICIT: | |
| // Nothing extra to print here | |
| break; | |
| case ND_ROUTER_ADVERT: { | |
| const nd_opt_prefix_info* prefix_info = NDProxy::GetPrefixInfoOption( | |
| reinterpret_cast<const uint8_t*>(icmp6), len - sizeof(ip6_hdr)); | |
| const auto prefix_cidr = NDOptPrefixInfoToCIDR(prefix_info); | |
| if (prefix_cidr) { | |
| ss << ", prefix " << *prefix_cidr; | |
| } | |
| break; | |
| } | |
| default: { | |
| NOTREACHED(); | |
| } | |
| } | |
| return ss.str(); | |
| } | |
| } // namespace | |
| NDProxy::NDProxy() = default; | |
| // static | |
| std::unique_ptr<net_base::Socket> NDProxy::PreparePacketSocket() { | |
| std::unique_ptr<net_base::Socket> socket = net_base::Socket::Create( | |
| AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC, htons(ETH_P_IPV6)); | |
| if (!socket) { | |
| PLOG(ERROR) << "socket() failed"; | |
| return nullptr; | |
| } | |
| if (!socket->SetSockOpt(SOL_SOCKET, SO_ATTACH_FILTER, | |
| net_base::byte_utils::AsBytes(kNDPacketBpfProgram))) { | |
| PLOG(ERROR) << "setsockopt(SO_ATTACH_FILTER) failed"; | |
| return nullptr; | |
| } | |
| return socket; | |
| } | |
| bool NDProxy::Init() { | |
| rtnl_client_ = RTNLClient::Create(); | |
| if (!rtnl_client_) { | |
| PLOG(ERROR) << "Failed to create rtnetlink client"; | |
| return false; | |
| } | |
| dummy_fd_ = base::ScopedFD(socket(AF_INET6, SOCK_DGRAM, 0)); | |
| if (!dummy_fd_.is_valid()) { | |
| PLOG(ERROR) << "socket() failed for dummy socket"; | |
| return false; | |
| } | |
| return true; | |
| } | |
| // static | |
| void NDProxy::ReplaceMacInIcmpOption(uint8_t* icmp6, | |
| size_t icmp6_len, | |
| size_t nd_hdr_len, | |
| uint8_t opt_type, | |
| net_base::MacAddress target_mac) { | |
| size_t opt_offset = nd_hdr_len; | |
| while (opt_offset + sizeof(nd_opt_hdr) <= icmp6_len) { | |
| nd_opt_hdr* opt = reinterpret_cast<nd_opt_hdr*>(icmp6 + opt_offset); | |
| // nd_opt_len is in 8 bytes unit. | |
| size_t opt_len = 8 * (opt->nd_opt_len); | |
| if (opt_len == 0 || icmp6_len < opt_offset + opt_len) { | |
| // Invalid packet. | |
| return; | |
| } | |
| if (opt->nd_opt_type == opt_type) { | |
| if (opt_len < sizeof(nd_opt_hdr) + ETHER_ADDR_LEN) { | |
| // Option length was inconsistent with the size of a MAC address. | |
| return; | |
| } | |
| memcpy(icmp6 + opt_offset + sizeof(nd_opt_hdr), target_mac.data(), | |
| ETHER_ADDR_LEN); | |
| } | |
| opt_offset += opt_len; | |
| } | |
| } | |
| // static | |
| ssize_t NDProxy::TranslateNDPacket( | |
| const uint8_t* in_packet, | |
| size_t packet_len, | |
| net_base::MacAddress local_mac_addr, | |
| const std::optional<net_base::IPv6Address>& new_src_ip, | |
| const std::optional<net_base::IPv6Address>& new_dst_ip, | |
| uint8_t* out_packet) { | |
| if (packet_len < sizeof(ip6_hdr) + sizeof(icmp6_hdr)) { | |
| return kTranslateErrorInsufficientLength; | |
| } | |
| if (reinterpret_cast<const ip6_hdr*>(in_packet)->ip6_nxt != IPPROTO_ICMPV6) { | |
| return kTranslateErrorNotICMPv6Packet; | |
| } | |
| if (ntohs(reinterpret_cast<const ip6_hdr*>(in_packet)->ip6_plen) != | |
| (packet_len - sizeof(struct ip6_hdr))) { | |
| return kTranslateErrorMismatchedIp6Length; | |
| } | |
| memcpy(out_packet, in_packet, packet_len); | |
| ip6_hdr* ip6 = reinterpret_cast<ip6_hdr*>(out_packet); | |
| icmp6_hdr* icmp6 = reinterpret_cast<icmp6_hdr*>(out_packet + sizeof(ip6_hdr)); | |
| const size_t icmp6_len = packet_len - sizeof(ip6_hdr); | |
| switch (icmp6->icmp6_type) { | |
| case ND_ROUTER_SOLICIT: | |
| ReplaceMacInIcmpOption(reinterpret_cast<uint8_t*>(icmp6), icmp6_len, | |
| sizeof(nd_router_solicit), ND_OPT_SOURCE_LINKADDR, | |
| local_mac_addr); | |
| break; | |
| case ND_ROUTER_ADVERT: { | |
| // RFC 4389 Section 4.1.3.3 - Set Proxy bit | |
| nd_router_advert* ra = reinterpret_cast<nd_router_advert*>(icmp6); | |
| if (ra->nd_ra_flags_reserved & 0x04) { | |
| // According to RFC 4389, an RA packet with 'Proxy' bit set already | |
| // should not be proxied again, in order to avoid loop. However, we'll | |
| // need this form of proxy cascading in Crostini (Host->VM->Container) | |
| // so we are ignoring the check here. Note that we know we are doing RA | |
| // proxy in only one direction so there should be no loop. | |
| } | |
| ra->nd_ra_flags_reserved |= 0x04; | |
| ReplaceMacInIcmpOption(reinterpret_cast<uint8_t*>(icmp6), icmp6_len, | |
| sizeof(nd_router_advert), ND_OPT_SOURCE_LINKADDR, | |
| local_mac_addr); | |
| break; | |
| } | |
| case ND_NEIGHBOR_SOLICIT: | |
| ReplaceMacInIcmpOption(reinterpret_cast<uint8_t*>(icmp6), icmp6_len, | |
| sizeof(nd_neighbor_solicit), | |
| ND_OPT_SOURCE_LINKADDR, local_mac_addr); | |
| break; | |
| case ND_NEIGHBOR_ADVERT: | |
| ReplaceMacInIcmpOption(reinterpret_cast<uint8_t*>(icmp6), icmp6_len, | |
| sizeof(nd_neighbor_advert), ND_OPT_TARGET_LINKADDR, | |
| local_mac_addr); | |
| break; | |
| default: | |
| return kTranslateErrorNotNDPacket; | |
| } | |
| if (new_src_ip) { | |
| // b/309528384: Only change the source IP if RA contains a PIO. This is to | |
| // avoid downstream confusing router lifetime from different RA senders. | |
| nd_opt_prefix_info* prefix_info = | |
| GetPrefixInfoOption(reinterpret_cast<uint8_t*>(icmp6), icmp6_len); | |
| if (prefix_info) { | |
| ip6->ip6_src = new_src_ip->ToIn6Addr(); | |
| // Turn off onlink flag if we are pretending to be the router. | |
| prefix_info->nd_opt_pi_flags_reserved &= ~ND_OPT_PI_FLAG_ONLINK; | |
| } | |
| } | |
| if (new_dst_ip) { | |
| ip6->ip6_dst = new_dst_ip->ToIn6Addr(); | |
| } | |
| // Recalculate the checksum. We need to clear the old checksum first so | |
| // checksum calculation does not wrongly take old checksum into account. | |
| icmp6->icmp6_cksum = 0; | |
| icmp6->icmp6_cksum = | |
| Icmpv6Checksum(reinterpret_cast<const uint8_t*>(ip6), packet_len); | |
| return static_cast<ssize_t>(packet_len); | |
| } | |
| void NDProxy::ReadAndProcessOnePacket(int fd) { | |
| uint8_t* in_packet = reinterpret_cast<uint8_t*>(in_packet_buffer_); | |
| uint8_t* out_packet = reinterpret_cast<uint8_t*>(out_packet_buffer_); | |
| sockaddr_ll recv_ll_addr; | |
| struct iovec iov_in = { | |
| .iov_base = in_packet, | |
| .iov_len = IP_MAXPACKET, | |
| }; | |
| msghdr hdr = { | |
| .msg_name = &recv_ll_addr, | |
| .msg_namelen = sizeof(recv_ll_addr), | |
| .msg_iov = &iov_in, | |
| .msg_iovlen = 1, | |
| .msg_control = nullptr, | |
| .msg_controllen = 0, | |
| .msg_flags = 0, | |
| }; | |
| ssize_t slen; | |
| if ((slen = recvmsg(fd, &hdr, 0)) < 0) { | |
| // Ignore ENETDOWN: this can happen if the interface is not yet configured | |
| if (errno != ENETDOWN) { | |
| PLOG(WARNING) << "recvmsg() failed"; | |
| } | |
| return; | |
| } | |
| size_t len = static_cast<size_t>(slen); | |
| ip6_hdr* ip6 = reinterpret_cast<ip6_hdr*>(in_packet); | |
| icmp6_hdr* icmp6 = reinterpret_cast<icmp6_hdr*>(in_packet + sizeof(ip6_hdr)); | |
| if (ip6->ip6_nxt != IPPROTO_ICMPV6 || icmp6->icmp6_type < ND_ROUTER_SOLICIT || | |
| icmp6->icmp6_type > ND_NEIGHBOR_ADVERT) | |
| return; | |
| VLOG_IF(2, (icmp6->icmp6_type == ND_ROUTER_SOLICIT || | |
| icmp6->icmp6_type == ND_ROUTER_ADVERT)) | |
| << "Received on interface " << recv_ll_addr.sll_ifindex << ": " | |
| << Icmp6ToString(in_packet, len); | |
| VLOG_IF(6, (icmp6->icmp6_type == ND_NEIGHBOR_SOLICIT || | |
| icmp6->icmp6_type == ND_NEIGHBOR_ADVERT)) | |
| << "Received on interface " << recv_ll_addr.sll_ifindex << ": " | |
| << Icmp6ToString(in_packet, len); | |
| NotifyPacketCallbacks(recv_ll_addr.sll_ifindex, in_packet, len); | |
| if (downlink_link_local_.find(recv_ll_addr.sll_ifindex) != | |
| downlink_link_local_.end() && | |
| downlink_link_local_[recv_ll_addr.sll_ifindex] == | |
| net_base::IPv6Address(ip6->ip6_dst)) { | |
| // If destination IP is our link local unicast, no need to proxy the packet. | |
| return; | |
| } | |
| // Translate the NDP frame and send it through proxy interface | |
| auto map_entry = | |
| MapForType(icmp6->icmp6_type)->find(recv_ll_addr.sll_ifindex); | |
| if (map_entry == MapForType(icmp6->icmp6_type)->end()) | |
| return; | |
| const auto& target_ifs = map_entry->second; | |
| for (int target_if : target_ifs) { | |
| const std::optional<net_base::MacAddress> local_mac = | |
| GetLocalMac(target_if); | |
| if (!local_mac.has_value()) { | |
| continue; | |
| } | |
| if (sleep_mode_filter_links_.contains(target_if) && | |
| (icmp6->icmp6_type == ND_NEIGHBOR_SOLICIT || | |
| icmp6->icmp6_type == ND_NEIGHBOR_ADVERT)) { | |
| if (++sleep_mode_filtered_packet_count_ % 100 == 0) { | |
| LOG(INFO) << "ARC sleep mode: " << sleep_mode_filtered_packet_count_ | |
| << " packets filterted in current session."; | |
| } | |
| continue; | |
| } | |
| // b/246444885: Overwrite source IP address with host address and set | |
| // prefix offlink, to prevent internal traffic causing ICMP messaged being | |
| // sent to upstream caused by internal traffic. | |
| // b/187918638: On L850 only this is a must instead of an optimization. | |
| // With those modems we are observing irregular RAs coming from a src IP | |
| // that either cannot map to a hardware address in the neighbor table, or | |
| // is mapped to the local MAC address on the cellular interface. Directly | |
| // proxying these RAs will cause the guest OS to set up a default route to | |
| // a next hop that is not reachable for them. | |
| std::optional<net_base::IPv6Address> new_src_ip = std::nullopt; | |
| if (modify_ra_uplinks_.find(recv_ll_addr.sll_ifindex) != | |
| modify_ra_uplinks_.end() && | |
| icmp6->icmp6_type == ND_ROUTER_ADVERT) { | |
| if (downlink_link_local_.find(target_if) == downlink_link_local_.end()) { | |
| continue; | |
| } | |
| new_src_ip = downlink_link_local_[target_if]; | |
| } | |
| // Always proxy RA to multicast address, so that every guest will accept it | |
| // therefore saving the total amount of RSs we sent to the network. | |
| // b/228574659: On L850 only this is a must instead of an optimization. | |
| std::optional<net_base::IPv6Address> new_dst_ip = std::nullopt; | |
| if (icmp6->icmp6_type == ND_ROUTER_ADVERT) { | |
| new_dst_ip = kAllNodesMulticastAddress; | |
| } | |
| const ssize_t result = TranslateNDPacket( | |
| in_packet, len, *local_mac, new_src_ip, new_dst_ip, out_packet); | |
| if (result < 0) { | |
| switch (result) { | |
| case kTranslateErrorNotICMPv6Packet: | |
| LOG(DFATAL) << "Attempt to TranslateNDPacket on a non-ICMPv6 packet"; | |
| return; | |
| case kTranslateErrorNotNDPacket: | |
| LOG(DFATAL) << "Attempt to TranslateNDPacket on a non-NDP packet, " | |
| "icmpv6 type = " | |
| << static_cast<int>(icmp6->icmp6_type); | |
| return; | |
| case kTranslateErrorInsufficientLength: | |
| LOG(DFATAL) << "TranslateNDPacket failed: packet length = " << len | |
| << " is too small"; | |
| return; | |
| case kTranslateErrorMismatchedIp6Length: | |
| LOG(DFATAL) << "TranslateNDPacket failed: expected ip6_plen = " | |
| << ntohs(ip6->ip6_plen) << ", received length = " | |
| << (len - sizeof(struct ip6_hdr)); | |
| return; | |
| default: | |
| LOG(DFATAL) << "Unknown error in TranslateNDPacket"; | |
| return; | |
| } | |
| } | |
| sockaddr_ll send_ll_addr = { | |
| .sll_family = AF_PACKET, | |
| .sll_protocol = htons(ETH_P_IPV6), | |
| .sll_ifindex = target_if, | |
| .sll_halen = ETHER_ADDR_LEN, | |
| }; | |
| ip6_hdr* new_ip6 = reinterpret_cast<ip6_hdr*>(out_packet); | |
| const net_base::IPv6Address dst_addr(new_ip6->ip6_dst); | |
| const std::optional<net_base::MacAddress> dst_mac = | |
| ResolveDestinationMac(dst_addr); | |
| if (dst_mac.has_value()) { | |
| memcpy(send_ll_addr.sll_addr, dst_mac->data(), ETHER_ADDR_LEN); | |
| } else { | |
| VLOG(1) << "Cannot resolve " << Icmp6TypeName(icmp6->icmp6_type) | |
| << " packet dest IP " << dst_addr | |
| << " into MAC address. In: " << recv_ll_addr.sll_ifindex | |
| << ", out: " << target_if; | |
| if (IsGuestInterface(target_if) || !kDropUnresolvableUnicastToUpstream) { | |
| // If we can't resolve the destination IP into MAC from kernel neighbor | |
| // table, fill destination MAC with all-nodes multicast MAC instead. | |
| memcpy(send_ll_addr.sll_addr, kAllNodesMulticastMacAddress.data(), | |
| ETHER_ADDR_LEN); | |
| } else { | |
| // Drop the packet. | |
| return; | |
| } | |
| } | |
| VLOG_IF(3, (icmp6->icmp6_type == ND_ROUTER_SOLICIT || | |
| icmp6->icmp6_type == ND_ROUTER_ADVERT)) | |
| << "Sending to interface " << target_if << ": " | |
| << Icmp6ToString(out_packet, len); | |
| VLOG_IF(7, (icmp6->icmp6_type == ND_NEIGHBOR_SOLICIT || | |
| icmp6->icmp6_type == ND_NEIGHBOR_ADVERT)) | |
| << "Sending to interface " << target_if << ": " | |
| << Icmp6ToString(out_packet, len); | |
| struct iovec iov_out = { | |
| .iov_base = out_packet, | |
| .iov_len = static_cast<size_t>(len), | |
| }; | |
| msghdr hdr = { | |
| .msg_name = &send_ll_addr, | |
| .msg_namelen = sizeof(send_ll_addr), | |
| .msg_iov = &iov_out, | |
| .msg_iovlen = 1, | |
| .msg_control = nullptr, | |
| .msg_controllen = 0, | |
| }; | |
| if (sendmsg(fd, &hdr, 0) < 0) { | |
| // Ignore ENETDOWN: this can happen if the interface is not yet configured | |
| if (if_map_ra_.find(target_if) != if_map_ra_.end() && errno != ENETDOWN) { | |
| PLOG(WARNING) << "sendmsg() failed on interface " << target_if; | |
| } | |
| } | |
| } | |
| } | |
| // static | |
| nd_opt_prefix_info* NDProxy::GetPrefixInfoOption(uint8_t* icmp6, | |
| size_t icmp6_len) { | |
| uint8_t* start = reinterpret_cast<uint8_t*>(icmp6); | |
| uint8_t* end = start + icmp6_len; | |
| uint8_t* ptr = start + sizeof(nd_router_advert); | |
| while (ptr + offsetof(nd_opt_hdr, nd_opt_len) < end) { | |
| nd_opt_hdr* opt = reinterpret_cast<nd_opt_hdr*>(ptr); | |
| if (opt->nd_opt_len == 0) | |
| return nullptr; | |
| ptr += opt->nd_opt_len << 3; // nd_opt_len is in 8 bytes | |
| if (ptr > end) | |
| return nullptr; | |
| if (opt->nd_opt_type == ND_OPT_PREFIX_INFORMATION && | |
| opt->nd_opt_len << 3 == sizeof(nd_opt_prefix_info)) { | |
| return reinterpret_cast<nd_opt_prefix_info*>(opt); | |
| } | |
| } | |
| return nullptr; | |
| } | |
| // static | |
| const nd_opt_prefix_info* NDProxy::GetPrefixInfoOption(const uint8_t* icmp6, | |
| size_t icmp6_len) { | |
| return NDProxy::GetPrefixInfoOption(const_cast<uint8_t*>(icmp6), icmp6_len); | |
| } | |
| void NDProxy::NotifyPacketCallbacks(int recv_ifindex, | |
| const uint8_t* packet, | |
| size_t len) { | |
| const ip6_hdr* ip6 = reinterpret_cast<const ip6_hdr*>(packet); | |
| const icmp6_hdr* icmp6 = | |
| reinterpret_cast<const icmp6_hdr*>(packet + sizeof(ip6_hdr)); | |
| // GuestDiscovery event is triggered whenever an NA advertising global | |
| // address or an NS with a global source address is received on a downlink. | |
| const in6_addr* guest_address = nullptr; | |
| if ((IsGuestInterface(recv_ifindex) || | |
| neighbor_monitor_links_.count(recv_ifindex) > 0) && | |
| !guest_discovery_handler_.is_null()) { | |
| if (icmp6->icmp6_type == ND_NEIGHBOR_ADVERT) { | |
| const nd_neighbor_advert* na = | |
| reinterpret_cast<const nd_neighbor_advert*>(icmp6); | |
| guest_address = &(na->nd_na_target); | |
| } else if (icmp6->icmp6_type == ND_NEIGHBOR_SOLICIT) { | |
| guest_address = &(ip6->ip6_src); | |
| // b/187918638: some cellular modems never send proper NS and NA, | |
| // there is no chance that we get the guest IP as normally from NA. | |
| // Instead, we have to monitor DAD NS frames and use it as judgement. | |
| // Notice that since upstream never reply NA, this DAD never fails. | |
| // b/266514205: extent this workaround to all technologies as we are | |
| // observing similar behavior in some wifi APs. | |
| // Empty source IP indicates DAD | |
| if (net_base::IPv6Address(ip6->ip6_src).IsZero()) { | |
| const nd_neighbor_solicit* ns = | |
| reinterpret_cast<const nd_neighbor_solicit*>(icmp6); | |
| guest_address = &(ns->nd_ns_target); | |
| } | |
| } | |
| } | |
| if (guest_address && | |
| ((guest_address->s6_addr[0] & 0xe0) == 0x20 || // Global Unicast | |
| (guest_address->s6_addr[0] & 0xfe) == 0xfc)) { // Unique Local | |
| const net_base::IPv6Address guest_addr(*guest_address); | |
| guest_discovery_handler_.Run(recv_ifindex, guest_addr); | |
| VLOG(2) << "GuestDiscovery on interface " << recv_ifindex << ": " | |
| << guest_addr; | |
| } | |
| // RouterDiscovery event is triggered whenever an RA is received on a uplink. | |
| if (icmp6->icmp6_type == ND_ROUTER_ADVERT && | |
| IsRouterInterface(recv_ifindex) && !router_discovery_handler_.is_null()) { | |
| const nd_opt_prefix_info* prefix_info = GetPrefixInfoOption( | |
| reinterpret_cast<const uint8_t*>(icmp6), len - sizeof(ip6_hdr)); | |
| const auto ipv6_cidr = NDOptPrefixInfoToCIDR(prefix_info); | |
| if (ipv6_cidr) { | |
| router_discovery_handler_.Run(recv_ifindex, *ipv6_cidr); | |
| VLOG(2) << "RouterDiscovery on interface " << recv_ifindex << ": " | |
| << *ipv6_cidr; | |
| } | |
| } | |
| } | |
| std::optional<net_base::MacAddress> NDProxy::ResolveDestinationMac( | |
| const net_base::IPv6Address& dest_ipv6) { | |
| if (dest_ipv6 == kAllNodesMulticastAddress) { | |
| return kAllNodesMulticastMacAddress; | |
| } | |
| if (dest_ipv6 == kAllRoutersMulticastAddress) { | |
| return kAllRoutersMulticastMacAddress; | |
| } | |
| if (kSolicitedNodeMulticastCIDR.InSameSubnetWith(dest_ipv6)) { | |
| const in6_addr dest_in6_addr = dest_ipv6.ToIn6Addr(); | |
| net_base::MacAddress::DataType dest_mac_data; | |
| memcpy(dest_mac_data.data(), kSolicitedNodeMulticastMacAddressPrefix.data(), | |
| ETHER_ADDR_LEN); | |
| memcpy( | |
| dest_mac_data.data() + ETHER_ADDR_LEN - kSolicitedGroupSuffixLength, | |
| &dest_in6_addr.s6_addr[sizeof(in6_addr) - kSolicitedGroupSuffixLength], | |
| kSolicitedGroupSuffixLength); | |
| return net_base::MacAddress(dest_mac_data); | |
| } | |
| return GetNeighborMac(dest_ipv6); | |
| } | |
| std::optional<net_base::IPv6Address> NDProxy::GetLinkLocalAddress(int ifindex) { | |
| std::ifstream proc_file("/proc/net/if_inet6"); | |
| std::string line; | |
| while (std::getline(proc_file, line)) { | |
| // Line format in /proc/net/if_inet6: | |
| // address ifindex prefix_len scope flags ifname | |
| const auto tokens = base::SplitString(line, " \t", base::TRIM_WHITESPACE, | |
| base::SPLIT_WANT_NONEMPTY); | |
| if (tokens.size() < 4) { | |
| continue; | |
| } | |
| if (tokens[3] != "20") { | |
| // We are only looking for link local address (scope value == "20") | |
| continue; | |
| } | |
| int line_if_id; | |
| if (!base::HexStringToInt(tokens[1], &line_if_id) || | |
| line_if_id != ifindex) { | |
| continue; | |
| } | |
| std::vector<uint8_t> line_address; | |
| if (!base::HexStringToBytes(tokens[0], &line_address)) { | |
| continue; | |
| } | |
| const auto addr = net_base::IPv6Address::CreateFromBytes(line_address); | |
| if (addr) { | |
| return addr; | |
| } | |
| } | |
| return std::nullopt; | |
| } | |
| std::optional<net_base::MacAddress> NDProxy::GetLocalMac(int if_id) { | |
| ifreq ifr = { | |
| .ifr_ifindex = if_id, | |
| }; | |
| if (ioctl(dummy_fd_.get(), SIOCGIFNAME, &ifr) < 0) { | |
| PLOG(ERROR) << "ioctl() failed to get interface name on interface " | |
| << if_id; | |
| return std::nullopt; | |
| } | |
| if (ioctl(dummy_fd_.get(), SIOCGIFHWADDR, &ifr) < 0) { | |
| PLOG(ERROR) << "ioctl() failed to get MAC address on interface " << if_id; | |
| return std::nullopt; | |
| } | |
| return net_base::MacAddress::CreateFromBytes( | |
| {ifr.ifr_addr.sa_data, net_base::MacAddress::kAddressLength}); | |
| } | |
| std::optional<net_base::MacAddress> NDProxy::GetNeighborMac( | |
| const net_base::IPv6Address& ipv6_addr) { | |
| DCHECK(rtnl_client_); | |
| const auto neighbor_mac_table = rtnl_client_->GetIPv6NeighborMacTable(); | |
| const auto it = neighbor_mac_table.find(ipv6_addr); | |
| if (it == neighbor_mac_table.end()) { | |
| return std::nullopt; | |
| } | |
| return net_base::MacAddress(it->second); | |
| } | |
| void NDProxy::RegisterOnGuestIpDiscoveryHandler( | |
| GuestIpDiscoveryHandler handler) { | |
| guest_discovery_handler_ = std::move(handler); | |
| } | |
| void NDProxy::RegisterOnRouterDiscoveryHandler(RouterDiscoveryHandler handler) { | |
| router_discovery_handler_ = std::move(handler); | |
| } | |
| NDProxy::interface_mapping* NDProxy::MapForType(uint8_t type) { | |
| switch (type) { | |
| case ND_ROUTER_SOLICIT: | |
| return &if_map_rs_; | |
| case ND_ROUTER_ADVERT: | |
| return &if_map_ra_; | |
| case ND_NEIGHBOR_SOLICIT: | |
| return &if_map_ns_; | |
| case ND_NEIGHBOR_ADVERT: | |
| return &if_map_na_; | |
| default: | |
| LOG(DFATAL) << "Attempt to get interface map on illegal icmpv6 type " | |
| << static_cast<int>(type); | |
| return nullptr; | |
| } | |
| } | |
| void NDProxy::StartRSRAProxy(int if_id_upstream, | |
| int if_id_downstream, | |
| bool modify_router_address) { | |
| VLOG(1) << "StartRARSProxy(" << if_id_upstream << ", " << if_id_downstream | |
| << (modify_router_address ? ", modify_router_address)" : ")"); | |
| if_map_ra_[if_id_upstream].insert(if_id_downstream); | |
| if_map_rs_[if_id_downstream].insert(if_id_upstream); | |
| if (modify_router_address) { | |
| modify_ra_uplinks_.insert(if_id_upstream); | |
| } | |
| const auto addr = GetLinkLocalAddress(if_id_downstream); | |
| if (addr) { | |
| downlink_link_local_[if_id_downstream] = *addr; | |
| } else { | |
| LOG(WARNING) << "Cannot find a link local address on interface " | |
| << if_id_downstream; | |
| downlink_link_local_[if_id_downstream] = net_base::IPv6Address(); | |
| } | |
| } | |
| void NDProxy::StartNSNAProxy(int if_id_na_side, int if_id_ns_side) { | |
| VLOG(1) << "StartNSNAProxy(" << if_id_na_side << ", " << if_id_ns_side << ")"; | |
| if_map_na_[if_id_na_side].insert(if_id_ns_side); | |
| if_map_ns_[if_id_ns_side].insert(if_id_na_side); | |
| } | |
| void NDProxy::StopProxy(int if_id1, int if_id2) { | |
| VLOG(1) << "StopProxy(" << if_id1 << ", " << if_id2 << ")"; | |
| auto remove_pair = [if_id1, if_id2](interface_mapping& mapping) { | |
| mapping[if_id1].erase(if_id2); | |
| if (mapping[if_id1].empty()) { | |
| mapping.erase(if_id1); | |
| } | |
| mapping[if_id2].erase(if_id1); | |
| if (mapping[if_id2].empty()) { | |
| mapping.erase(if_id2); | |
| } | |
| }; | |
| remove_pair(if_map_ra_); | |
| remove_pair(if_map_rs_); | |
| remove_pair(if_map_na_); | |
| remove_pair(if_map_ns_); | |
| if (!IsRouterInterface(if_id1)) { | |
| modify_ra_uplinks_.erase(if_id1); | |
| } | |
| if (!IsRouterInterface(if_id2)) { | |
| modify_ra_uplinks_.erase(if_id2); | |
| } | |
| } | |
| void NDProxy::StartNeighborMonitor(int if_id) { | |
| neighbor_monitor_links_.insert(if_id); | |
| } | |
| void NDProxy::StopNeighborMonitor(int if_id) { | |
| neighbor_monitor_links_.erase(if_id); | |
| } | |
| void NDProxy::StartARCSleepModeFilter(int if_id) { | |
| if (sleep_mode_filter_links_.empty()) { | |
| sleep_mode_filtered_packet_count_ = 0; | |
| } | |
| sleep_mode_filter_links_.insert(if_id); | |
| } | |
| void NDProxy::StopARCSleepModeFilter(int if_id) { | |
| sleep_mode_filter_links_.erase(if_id); | |
| } | |
| bool NDProxy::IsGuestInterface(int ifindex) { | |
| return if_map_rs_.find(ifindex) != if_map_rs_.end(); | |
| } | |
| bool NDProxy::IsRouterInterface(int ifindex) { | |
| return if_map_ra_.find(ifindex) != if_map_ra_.end(); | |
| } | |
| NDProxyDaemon::NDProxyDaemon(base::ScopedFD control_fd) | |
| : msg_dispatcher_(std::make_unique<MessageDispatcher<SubprocessMessage>>( | |
| std::move(control_fd))) {} | |
| NDProxyDaemon::~NDProxyDaemon() = default; | |
| int NDProxyDaemon::OnInit() { | |
| // Prevent the main process from sending us any signals. | |
| if (setsid() < 0) { | |
| PLOG(ERROR) << "Failed to created a new session with setsid: exiting"; | |
| return EX_OSERR; | |
| } | |
| EnterChildProcessJail(); | |
| // Register control fd callbacks | |
| if (msg_dispatcher_) { | |
| msg_dispatcher_->RegisterFailureHandler(base::BindRepeating( | |
| &NDProxyDaemon::OnParentProcessExit, weak_factory_.GetWeakPtr())); | |
| msg_dispatcher_->RegisterMessageHandler(base::BindRepeating( | |
| &NDProxyDaemon::OnControlMessage, weak_factory_.GetWeakPtr())); | |
| } | |
| // Initialize NDProxy and register guest IP discovery callback | |
| if (!proxy_.Init()) { | |
| PLOG(ERROR) << "Failed to initialize NDProxy internal state"; | |
| return EX_OSERR; | |
| } | |
| proxy_.RegisterOnGuestIpDiscoveryHandler(base::BindRepeating( | |
| &NDProxyDaemon::OnGuestIpDiscovery, weak_factory_.GetWeakPtr())); | |
| proxy_.RegisterOnRouterDiscoveryHandler(base::BindRepeating( | |
| &NDProxyDaemon::OnRouterDiscovery, weak_factory_.GetWeakPtr())); | |
| // Initialize data fd | |
| socket_ = NDProxy::PreparePacketSocket(); | |
| if (!socket_) { | |
| return EX_OSERR; | |
| } | |
| // Start watching on data fd | |
| socket_->SetReadableCallback(base::BindRepeating( | |
| &NDProxyDaemon::OnDataSocketReadReady, weak_factory_.GetWeakPtr())); | |
| LOG(INFO) << "Started watching on packet fd..."; | |
| return Daemon::OnInit(); | |
| } | |
| void NDProxyDaemon::OnDataSocketReadReady() { | |
| proxy_.ReadAndProcessOnePacket(socket_->Get()); | |
| } | |
| void NDProxyDaemon::OnParentProcessExit() { | |
| LOG(ERROR) << "Quitting because the parent process died"; | |
| Quit(); | |
| } | |
| void NDProxyDaemon::OnControlMessage(const SubprocessMessage& root_msg) { | |
| if (!root_msg.has_control_message() || | |
| !root_msg.control_message().has_ndproxy_control()) { | |
| LOG(ERROR) << "Unexpected message type"; | |
| return; | |
| } | |
| const NDProxyControlMessage& msg = | |
| root_msg.control_message().ndproxy_control(); | |
| VLOG(4) << "Received NDProxyControlMessage: " << msg.type() << ": " | |
| << msg.if_id_primary() << "<->" << msg.if_id_secondary(); | |
| switch (msg.type()) { | |
| case NDProxyControlMessage::START_NS_NA: { | |
| proxy_.StartNSNAProxy(msg.if_id_primary(), msg.if_id_secondary()); | |
| proxy_.StartNSNAProxy(msg.if_id_secondary(), msg.if_id_primary()); | |
| break; | |
| } | |
| case NDProxyControlMessage::START_NS_NA_RS_RA: { | |
| proxy_.StartNSNAProxy(msg.if_id_primary(), msg.if_id_secondary()); | |
| proxy_.StartNSNAProxy(msg.if_id_secondary(), msg.if_id_primary()); | |
| proxy_.StartRSRAProxy(msg.if_id_primary(), msg.if_id_secondary()); | |
| break; | |
| } | |
| case NDProxyControlMessage::START_NS_NA_RS_RA_MODIFYING_ROUTER_ADDRESS: { | |
| // TODO(taoyl): therotically whe should be able to stop proxying NS from | |
| // downlink to uplink and NA from uplink to downlink as we set prefix to | |
| // be not ONLINK. However, Android ignores the ONLINK flag and always add | |
| // a local subnet route when receiving a prefix [1]. Consider addressing | |
| // this in Android so we can remove the first line below. | |
| // [1] LinkProperties::ensureDirectlyConnectedRoutes() | |
| proxy_.StartNSNAProxy(msg.if_id_primary(), msg.if_id_secondary()); | |
| proxy_.StartNSNAProxy(msg.if_id_secondary(), msg.if_id_primary()); | |
| proxy_.StartRSRAProxy(msg.if_id_primary(), msg.if_id_secondary(), true); | |
| break; | |
| } | |
| case NDProxyControlMessage::STOP_PROXY: { | |
| proxy_.StopProxy(msg.if_id_primary(), msg.if_id_secondary()); | |
| break; | |
| } | |
| case NDProxyControlMessage::START_NEIGHBOR_MONITOR: { | |
| proxy_.StartNeighborMonitor(msg.if_id_primary()); | |
| break; | |
| } | |
| case NDProxyControlMessage::STOP_NEIGHBOR_MONITOR: { | |
| proxy_.StopNeighborMonitor(msg.if_id_primary()); | |
| break; | |
| } | |
| case NDProxyControlMessage::START_NS_NA_FILTER: { | |
| proxy_.StartARCSleepModeFilter(msg.if_id_primary()); | |
| break; | |
| } | |
| case NDProxyControlMessage::STOP_NS_NA_FILTER: { | |
| proxy_.StopARCSleepModeFilter(msg.if_id_primary()); | |
| break; | |
| } | |
| case NDProxyControlMessage::UNKNOWN: | |
| default: | |
| NOTREACHED(); | |
| } | |
| } | |
| void NDProxyDaemon::OnGuestIpDiscovery(int if_id, | |
| const net_base::IPv6Address& ip6addr) { | |
| if (!msg_dispatcher_) { | |
| return; | |
| } | |
| NeighborDetectedSignal msg; | |
| msg.set_if_id(if_id); | |
| msg.set_ip(ip6addr.ToByteString()); | |
| NDProxySignalMessage nm; | |
| *nm.mutable_neighbor_detected_signal() = msg; | |
| FeedbackMessage fm; | |
| *fm.mutable_ndproxy_signal() = nm; | |
| SubprocessMessage root_m; | |
| *root_m.mutable_feedback_message() = fm; | |
| msg_dispatcher_->SendMessage(root_m); | |
| } | |
| void NDProxyDaemon::OnRouterDiscovery(int if_id, | |
| const net_base::IPv6CIDR& prefix_cidr) { | |
| if (!msg_dispatcher_) { | |
| return; | |
| } | |
| RouterDetectedSignal msg; | |
| msg.set_if_id(if_id); | |
| msg.set_ip(prefix_cidr.address().ToByteString()); | |
| msg.set_prefix_len(prefix_cidr.prefix_length()); | |
| NDProxySignalMessage nm; | |
| *nm.mutable_router_detected_signal() = msg; | |
| FeedbackMessage fm; | |
| *fm.mutable_ndproxy_signal() = nm; | |
| SubprocessMessage root_m; | |
| *root_m.mutable_feedback_message() = fm; | |
| msg_dispatcher_->SendMessage(root_m); | |
| } | |
| } // namespace patchpanel |