| // 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 <string> |
| #include <utility> |
| |
| #include <base/functional/bind.h> |
| #include <base/logging.h> |
| #include <base/strings/string_number_conversions.h> |
| #include <base/strings/string_split.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; |
| |
| const unsigned char kZeroMacAddress[] = {0, 0, 0, 0, 0, 0}; |
| const unsigned char kAllNodesMulticastMacAddress[] = {0x33, 0x33, 0, |
| 0, 0, 0x01}; |
| const unsigned char kAllRoutersMulticastMacAddress[] = {0x33, 0x33, 0, |
| 0, 0, 0x02}; |
| const unsigned char 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() {} |
| |
| // static |
| base::ScopedFD NDProxy::PreparePacketSocket() { |
| base::ScopedFD fd( |
| socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC, htons(ETH_P_IPV6))); |
| if (!fd.is_valid()) { |
| PLOG(ERROR) << "socket() failed"; |
| return base::ScopedFD(); |
| } |
| if (setsockopt(fd.get(), SOL_SOCKET, SO_ATTACH_FILTER, &kNDPacketBpfProgram, |
| sizeof(kNDPacketBpfProgram))) { |
| PLOG(ERROR) << "setsockopt(SO_ATTACH_FILTER) failed"; |
| return base::ScopedFD(); |
| } |
| return fd; |
| } |
| |
| 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, |
| const 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, |
| const 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) { |
| MacAddress local_mac; |
| if (!GetLocalMac(target_if, &local_mac)) |
| 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); |
| ResolveDestinationMac(dst_addr, send_ll_addr.sll_addr); |
| if (memcmp(send_ll_addr.sll_addr, &kZeroMacAddress, ETHER_ADDR_LEN) == 0) { |
| 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, |
| 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; |
| } |
| } |
| } |
| |
| void NDProxy::ResolveDestinationMac(const net_base::IPv6Address& dest_ipv6, |
| uint8_t* dest_mac) { |
| if (dest_ipv6 == kAllNodesMulticastAddress) { |
| memcpy(dest_mac, &kAllNodesMulticastMacAddress, ETHER_ADDR_LEN); |
| return; |
| } |
| if (dest_ipv6 == kAllRoutersMulticastAddress) { |
| memcpy(dest_mac, &kAllRoutersMulticastMacAddress, ETHER_ADDR_LEN); |
| return; |
| } |
| if (kSolicitedNodeMulticastCIDR.InSameSubnetWith(dest_ipv6)) { |
| const in6_addr dest_in6_addr = dest_ipv6.ToIn6Addr(); |
| memcpy(dest_mac, &kSolicitedNodeMulticastMacAddressPrefix, ETHER_ADDR_LEN); |
| memcpy( |
| dest_mac + ETHER_ADDR_LEN - kSolicitedGroupSuffixLength, |
| &dest_in6_addr.s6_addr[sizeof(in6_addr) - kSolicitedGroupSuffixLength], |
| kSolicitedGroupSuffixLength); |
| return; |
| } |
| |
| MacAddress neighbor_mac; |
| if (GetNeighborMac(dest_ipv6, &neighbor_mac)) { |
| memcpy(dest_mac, neighbor_mac.data(), ETHER_ADDR_LEN); |
| return; |
| } |
| |
| memcpy(dest_mac, &kZeroMacAddress, ETHER_ADDR_LEN); |
| } |
| |
| 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; |
| } |
| |
| bool NDProxy::GetLocalMac(int if_id, MacAddress* mac_addr) { |
| 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 false; |
| } |
| if (ioctl(dummy_fd_.get(), SIOCGIFHWADDR, &ifr) < 0) { |
| PLOG(ERROR) << "ioctl() failed to get MAC address on interface " << if_id; |
| return false; |
| } |
| memcpy(mac_addr->data(), ifr.ifr_addr.sa_data, ETHER_ADDR_LEN); |
| return true; |
| } |
| |
| bool NDProxy::GetNeighborMac(const net_base::IPv6Address& ipv6_addr, |
| MacAddress* mac_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 false; |
| } |
| |
| *mac_addr = it->second; |
| return true; |
| } |
| |
| 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); |
| } |
| |
| 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() {} |
| |
| 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 |
| fd_ = NDProxy::PreparePacketSocket(); |
| if (!fd_.is_valid()) { |
| return EX_OSERR; |
| } |
| |
| // Start watching on data fd |
| watcher_ = base::FileDescriptorWatcher::WatchReadable( |
| fd_.get(), base::BindRepeating(&NDProxyDaemon::OnDataSocketReadReady, |
| weak_factory_.GetWeakPtr())); |
| LOG(INFO) << "Started watching on packet fd..."; |
| |
| return Daemon::OnInit(); |
| } |
| |
| void NDProxyDaemon::OnDataSocketReadReady() { |
| proxy_.ReadAndProcessOnePacket(fd_.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::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 |