Add IPv6 link local scope ID to IPEndPoint

Plumbs sockaddr_in6.sin6_scope_id when the address is IPv6 link local.
Note that this works only on platforms that correctly provides the
scope id via getaddrinfo().

Bug: 374756136

Change-Id: Ibd06218e0bc01367a2572b8fd60cde931a30afe2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6032935
Reviewed-by: David Schinazi <dschinazi@chromium.org>
Commit-Queue: Kenichi Ishibashi <bashi@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1387944}
diff --git a/net/base/ip_endpoint.cc b/net/base/ip_endpoint.cc
index e837710..b840a18 100644
--- a/net/base/ip_endpoint.cc
+++ b/net/base/ip_endpoint.cc
@@ -25,10 +25,15 @@
 
 #if BUILDFLAG(IS_WIN)
 #include <winsock2.h>
+#include <winternl.h>
 
+#include <netioapi.h>
+#include <ntstatus.h>
 #include <ws2bth.h>
 
 #include "net/base/winsock_util.h"  // For kBluetoothAddressSize
+#elif BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA)
+#include <net/if.h>
 #endif
 
 namespace net {
@@ -38,9 +43,64 @@
 // Value dictionary keys
 constexpr std::string_view kValueAddressKey = "address";
 constexpr std::string_view kValuePortKey = "port";
+constexpr std::string_view kInterfaceName = "interface_name";
 
 }  // namespace
 
+IPEndPoint::IndexToNameFunc IPEndPoint::index_to_name_func_for_testing_ =
+    nullptr;
+IPEndPoint::NameToIndexFunc IPEndPoint::name_to_index_func_for_testing_ =
+    nullptr;
+
+// static
+void IPEndPoint::SetNameToIndexFuncForTesting(NameToIndexFunc func) {
+  name_to_index_func_for_testing_ = func;
+}
+
+void IPEndPoint::SetIndexToNameFuncForTesting(IndexToNameFunc func) {
+  index_to_name_func_for_testing_ = func;
+}
+
+// static
+std::optional<uint32_t> IPEndPoint::ScopeIdFromDict(
+    const base::Value::Dict& dict) {
+  const std::string* name = dict.FindString(kInterfaceName);
+  if (!name) {
+    return std::nullopt;
+  }
+
+  unsigned int index = 0;
+  if (name_to_index_func_for_testing_) {
+    index = name_to_index_func_for_testing_(name->c_str());
+  } else {
+    index = if_nametoindex(name->c_str());
+  }
+
+  return index;
+}
+
+// static
+base::Value IPEndPoint::ScopeIdToValue(std::optional<uint32_t> scope_id) {
+  if (!scope_id.has_value()) {
+    return base::Value();
+  }
+
+  char* name = nullptr;
+  char buf[IF_NAMESIZE + 1];
+  memset(buf, 0, sizeof(buf));
+  if (index_to_name_func_for_testing_) {
+    name = index_to_name_func_for_testing_(scope_id.value(), buf);
+  } else {
+    name = if_indextoname(scope_id.value(), buf);
+  }
+
+  if (!name) {
+    return base::Value();
+  }
+
+  return base::Value(name);
+}
+
 // static
 std::optional<IPEndPoint> IPEndPoint::FromValue(const base::Value& value) {
   const base::Value::Dict* dict = value.GetIfDict();
@@ -62,16 +122,29 @@
     return std::nullopt;
   }
 
-  return IPEndPoint(address.value(),
-                    base::checked_cast<uint16_t>(port.value()));
+  IPEndPoint endpoint(address.value(),
+                      base::checked_cast<uint16_t>(port.value()));
+
+  std::optional<uint32_t> scope_id = ScopeIdFromDict(*dict);
+  if (scope_id.has_value()) {
+    if (scope_id.value() == 0 || !endpoint.IsIPv6LinkLocal() ||
+        !base::IsValueInRangeForNumericType<uint32_t>(scope_id.value())) {
+      return std::nullopt;
+    }
+    endpoint.scope_id_ = scope_id.value();
+  }
+
+  return endpoint;
 }
 
 IPEndPoint::IPEndPoint() = default;
 
 IPEndPoint::~IPEndPoint() = default;
 
-IPEndPoint::IPEndPoint(const IPAddress& address, uint16_t port)
-    : address_(address), port_(port) {}
+IPEndPoint::IPEndPoint(const IPAddress& address,
+                       uint16_t port,
+                       std::optional<uint32_t> scope_id)
+    : address_(address), port_(port), scope_id_(scope_id) {}
 
 IPEndPoint::IPEndPoint(const IPEndPoint& endpoint) = default;
 
@@ -138,6 +211,9 @@
       addr6->sin6_port = base::HostToNet16(port_);
       memcpy(&addr6->sin6_addr, address_.bytes().data(),
              IPAddress::kIPv6AddressSize);
+      if (IsIPv6LinkLocal() && scope_id_) {
+        addr6->sin6_scope_id = *scope_id_;
+      }
       break;
     }
     default:
@@ -168,6 +244,9 @@
           reinterpret_cast<const struct sockaddr_in6*>(sock_addr);
       *this = IPEndPoint(IPAddress(addr->sin6_addr.s6_addr),
                          base::NetToHost16(addr->sin6_port));
+      if (IsIPv6LinkLocal() && addr->sin6_scope_id != 0) {
+        scope_id_ = addr->sin6_scope_id;
+      }
       return true;
     }
 #if BUILDFLAG(IS_WIN)
@@ -209,11 +288,13 @@
   if (address_.size() != other.address_.size()) {
     return address_.size() < other.address_.size();
   }
-  return std::tie(address_, port_) < std::tie(other.address_, other.port_);
+  return std::tie(address_, port_, scope_id_) <
+         std::tie(other.address_, other.port_, other.scope_id_);
 }
 
 bool IPEndPoint::operator==(const IPEndPoint& other) const {
-  return address_ == other.address_ && port_ == other.port_;
+  return address_ == other.address_ && port_ == other.port_ &&
+         scope_id_ == other.scope_id_;
 }
 
 bool IPEndPoint::operator!=(const IPEndPoint& that) const {
@@ -227,9 +308,20 @@
   dict.Set(kValueAddressKey, address_.ToValue());
   dict.Set(kValuePortKey, port_);
 
+  base::Value interface_name = ScopeIdToValue(scope_id_);
+  if (!interface_name.is_none()) {
+    DCHECK(IsIPv6LinkLocal());
+    dict.Set(kInterfaceName, std::move(interface_name));
+  }
+
   return base::Value(std::move(dict));
 }
 
+bool IPEndPoint::IsIPv6LinkLocal() const {
+  return address_.IsValid() && address_.IsIPv6() &&
+         !address_.IsIPv4MappedIPv6() && address_.IsLinkLocal();
+}
+
 std::ostream& operator<<(std::ostream& os, const IPEndPoint& ip_endpoint) {
   return os << ip_endpoint.ToString();
 }
diff --git a/net/base/ip_endpoint.h b/net/base/ip_endpoint.h
index de6bef1f..314ae06 100644
--- a/net/base/ip_endpoint.h
+++ b/net/base/ip_endpoint.h
@@ -36,14 +36,25 @@
 // An IPEndPoint represents the address of a transport endpoint:
 //  * IP address (either v4 or v6)
 //  * Port
+//  * IPv6 link local scope ID (only for IPv6 link local address)
 class NET_EXPORT IPEndPoint {
  public:
+  // Function signatures of if_nametoindex() and if_indextoname().
+  using NameToIndexFunc = uint32_t (*)(const char*);
+  using IndexToNameFunc = char* (*)(unsigned int, char*);
+
+  // Set fake if_nametoindex() and if_indextoname() functions for testing.
+  static void SetNameToIndexFuncForTesting(NameToIndexFunc func);
+  static void SetIndexToNameFuncForTesting(IndexToNameFunc func);
+
   // Nullopt if `value` is malformed to be serialized to IPEndPoint.
   static std::optional<IPEndPoint> FromValue(const base::Value& value);
 
   IPEndPoint();
   ~IPEndPoint();
-  IPEndPoint(const IPAddress& address, uint16_t port);
+  IPEndPoint(const IPAddress& address,
+             uint16_t port,
+             std::optional<uint32_t> scope_id = std::nullopt);
   IPEndPoint(const IPEndPoint& endpoint);
 
   const IPAddress& address() const { return address_; }
@@ -53,6 +64,10 @@
   // Bluetooth socket.
   uint16_t port() const;
 
+  // Returns the IPv6 scope identifier if it has been set by FromSockAddr() and
+  // the address is link-local.
+  std::optional<uint32_t> scope_id() const { return scope_id_; }
+
   // Returns AddressFamily of the address. Returns ADDRESS_FAMILY_UNSPECIFIED if
   // this is the IPEndPoint for a Bluetooth socket.
   AddressFamily GetFamily() const;
@@ -81,12 +96,15 @@
 
   // Returns value as a string (e.g. "127.0.0.1:80"). Returns the empty string
   // when |address_| is invalid (the port will be ignored). This function will
-  // crash if the IPEndPoint is for a Bluetooth socket.
+  // crash if the IPEndPoint is for a Bluetooth socket. This function doesn't
+  // include IPv6 scope id intentionally because exposing scope id is
+  // discouraged for web purpose. See
+  // https://datatracker.ietf.org/doc/html/draft-ietf-6man-zone-ui#section-5
   std::string ToString() const;
 
   // As above, but without port. Returns the empty string when address_ is
   // invalid. The function will crash if the IPEndPoint is for a Bluetooth
-  // socket.
+  // socket. This function doesn't include IPv6 scope id intentionally.
   std::string ToStringWithoutPort() const;
 
   bool operator<(const IPEndPoint& that) const;
@@ -96,8 +114,21 @@
   base::Value ToValue() const;
 
  private:
+  static NameToIndexFunc name_to_index_func_for_testing_;
+  static IndexToNameFunc index_to_name_func_for_testing_;
+
+  // Returns a scope ID from `dict` when `dict` has a valid interface name that
+  // can be converted to an interface index.
+  static std::optional<uint32_t> ScopeIdFromDict(const base::Value::Dict& dict);
+
+  // Converts `scope_id` to an interface name as a base::Value.
+  static base::Value ScopeIdToValue(std::optional<uint32_t> scope_id);
+
+  bool IsIPv6LinkLocal() const;
+
   IPAddress address_;
   uint16_t port_ = 0;
+  std::optional<uint32_t> scope_id_;
 };
 
 NET_EXPORT_PRIVATE std::ostream& operator<<(std::ostream& os,
diff --git a/net/base/ip_endpoint_unittest.cc b/net/base/ip_endpoint_unittest.cc
index d1ea946..9ebc0a4 100644
--- a/net/base/ip_endpoint_unittest.cc
+++ b/net/base/ip_endpoint_unittest.cc
@@ -67,26 +67,56 @@
   return base::NetToHost16(*port_field);
 }
 
+constexpr uint32_t kMaxFakeInterfaceIndex = 10;
+
+uint32_t FakeNameToIndexFunc(const char* name) {
+  uint32_t index = 0;
+  const bool ok = base::StringToUint(name, &index);
+  if (!ok || index > kMaxFakeInterfaceIndex) {
+    return 0;
+  }
+  return index;
+}
+
+char* FakeIndexToNameFunc(unsigned int index, char* ifname) {
+  if (index > kMaxFakeInterfaceIndex) {
+    return nullptr;
+  }
+  std::string name = base::NumberToString(index);
+  ifname[0] = name[0];
+  return ifname;
+}
+
 struct TestData {
   std::string host;
   std::string host_normalized;
   bool ipv6;
   IPAddress ip_address;
+  std::optional<uint32_t> scope_id = std::nullopt;
 } tests[] = {
     {"127.0.00.1", "127.0.0.1", false},
     {"192.168.1.1", "192.168.1.1", false},
     {"::1", "[::1]", true},
     {"2001:db8:0::42", "[2001:db8::42]", true},
+    {"fe80::1", "[fe80::1]", true, IPAddress(), /*scope_id=*/1},
 };
 
 class IPEndPointTest : public PlatformTest {
  public:
   void SetUp() override {
+    IPEndPoint::SetNameToIndexFuncForTesting(FakeNameToIndexFunc);
+    IPEndPoint::SetIndexToNameFuncForTesting(FakeIndexToNameFunc);
+
     // This is where we populate the TestData.
     for (auto& test : tests) {
       EXPECT_TRUE(test.ip_address.AssignFromIPLiteral(test.host));
     }
   }
+
+  void TearDown() override {
+    IPEndPoint::SetNameToIndexFuncForTesting(nullptr);
+    IPEndPoint::SetIndexToNameFuncForTesting(nullptr);
+  }
 };
 
 TEST_F(IPEndPointTest, Constructor) {
@@ -96,38 +126,41 @@
   }
 
   for (const auto& test : tests) {
-    IPEndPoint endpoint(test.ip_address, 80);
+    IPEndPoint endpoint(test.ip_address, 80, test.scope_id);
     EXPECT_EQ(80, endpoint.port());
     EXPECT_EQ(test.ip_address, endpoint.address());
+    EXPECT_EQ(test.scope_id, endpoint.scope_id());
   }
 }
 
 TEST_F(IPEndPointTest, Assignment) {
   uint16_t port = 0;
   for (const auto& test : tests) {
-    IPEndPoint src(test.ip_address, ++port);
+    IPEndPoint src(test.ip_address, ++port, test.scope_id);
     IPEndPoint dest = src;
 
     EXPECT_EQ(src.port(), dest.port());
     EXPECT_EQ(src.address(), dest.address());
+    EXPECT_EQ(src.scope_id(), dest.scope_id());
   }
 }
 
 TEST_F(IPEndPointTest, Copy) {
   uint16_t port = 0;
   for (const auto& test : tests) {
-    IPEndPoint src(test.ip_address, ++port);
+    IPEndPoint src(test.ip_address, ++port, test.scope_id);
     IPEndPoint dest(src);
 
     EXPECT_EQ(src.port(), dest.port());
     EXPECT_EQ(src.address(), dest.address());
+    EXPECT_EQ(src.scope_id(), dest.scope_id());
   }
 }
 
 TEST_F(IPEndPointTest, ToFromSockAddr) {
   uint16_t port = 0;
   for (const auto& test : tests) {
-    IPEndPoint ip_endpoint(test.ip_address, ++port);
+    IPEndPoint ip_endpoint(test.ip_address, ++port, test.scope_id);
 
     // Convert to a sockaddr.
     SockaddrStorage storage;
@@ -139,11 +172,17 @@
     EXPECT_EQ(expected_size, storage.addr_len);
     EXPECT_EQ(ip_endpoint.port(),
               GetPortFromSockaddr(storage.addr, storage.addr_len));
+    if (test.ipv6) {
+      uint32_t scope_id =
+          reinterpret_cast<struct sockaddr_in6*>(storage.addr)->sin6_scope_id;
+      EXPECT_EQ(scope_id, test.scope_id.value_or(0));
+    }
     // And convert back to an IPEndPoint.
     IPEndPoint ip_endpoint2;
     EXPECT_TRUE(ip_endpoint2.FromSockAddr(storage.addr, storage.addr_len));
     EXPECT_EQ(ip_endpoint.port(), ip_endpoint2.port());
     EXPECT_EQ(ip_endpoint.address(), ip_endpoint2.address());
+    EXPECT_EQ(ip_endpoint.scope_id(), ip_endpoint2.scope_id());
   }
 }
 
@@ -322,10 +361,20 @@
 TEST_F(IPEndPointTest, Equality) {
   uint16_t port = 0;
   for (const auto& test : tests) {
-    IPEndPoint src(test.ip_address, ++port);
+    IPEndPoint src(test.ip_address, ++port, test.scope_id);
     IPEndPoint dest(src);
     EXPECT_TRUE(src == dest);
   }
+
+  // Compare scope_id.
+  const auto v6_link_local_address = *IPAddress::FromIPLiteral("fe80::1");
+  IPEndPoint ip_endpoint1 =
+      IPEndPoint(v6_link_local_address, 80, /*scope_id=*/1);
+  IPEndPoint ip_endpoint2 =
+      IPEndPoint(v6_link_local_address, 80, /*scope_id=*/1);
+  EXPECT_EQ(ip_endpoint1, ip_endpoint2);
+  ip_endpoint2 = IPEndPoint(v6_link_local_address, 80, /*scope_id=*/2);
+  EXPECT_NE(ip_endpoint1, ip_endpoint2);
 }
 
 TEST_F(IPEndPointTest, LessThan) {
@@ -369,7 +418,7 @@
   uint16_t port = 100;
   for (const auto& test : tests) {
     ++port;
-    IPEndPoint endpoint(test.ip_address, port);
+    IPEndPoint endpoint(test.ip_address, port, test.scope_id);
     const std::string result = endpoint.ToString();
     EXPECT_EQ(test.host_normalized + ":" + base::NumberToString(port), result);
   }
@@ -383,7 +432,7 @@
 
 TEST_F(IPEndPointTest, RoundtripThroughValue) {
   for (const auto& test : tests) {
-    IPEndPoint endpoint(test.ip_address, 1645);
+    IPEndPoint endpoint(test.ip_address, 1645, test.scope_id);
     base::Value value = endpoint.ToValue();
 
     EXPECT_THAT(IPEndPoint::FromValue(value), Optional(endpoint));
@@ -397,7 +446,8 @@
 
 TEST_F(IPEndPointTest, FromMalformedValues) {
   for (const auto& test : tests) {
-    base::Value valid_value = IPEndPoint(test.ip_address, 1111).ToValue();
+    base::Value valid_value =
+        IPEndPoint(test.ip_address, 1111, test.scope_id).ToValue();
     ASSERT_TRUE(IPEndPoint::FromValue(valid_value).has_value());
 
     base::Value missing_address = valid_value.Clone();
@@ -420,6 +470,32 @@
     *large_port.GetDict().Find("port") = base::Value(66000);
     EXPECT_FALSE(IPEndPoint::FromValue(large_port).has_value());
   }
+
+  // Invalid values for scope id.
+  const auto v6_link_local_address = *IPAddress::FromIPLiteral("fe80::1");
+  base::Value valid_value =
+      IPEndPoint(v6_link_local_address, /*port=*/80, /*scope_id=*/1).ToValue();
+
+  base::Value invalid_scope_id = valid_value.Clone();
+  *invalid_scope_id.GetDict().Find("interface_name") = base::Value("-1");
+  EXPECT_FALSE(IPEndPoint::FromValue(invalid_scope_id).has_value());
+
+  base::Value invalid_scope_id2 = valid_value.Clone();
+  *invalid_scope_id2.GetDict().Find("interface_name") = base::Value("0");
+  EXPECT_FALSE(IPEndPoint::FromValue(invalid_scope_id2).has_value());
+
+  base::Value invalid_address_v4 = valid_value.Clone();
+  *invalid_address_v4.GetDict().Find("address") = base::Value("169.254.0.1");
+  EXPECT_FALSE(IPEndPoint::FromValue(invalid_scope_id).has_value());
+
+  base::Value invalid_address_v6 = valid_value.Clone();
+  *invalid_address_v4.GetDict().Find("address") = base::Value("2001:db8:0::42");
+  EXPECT_FALSE(IPEndPoint::FromValue(invalid_scope_id).has_value());
+
+  base::Value invalid_ipv4_mapped_v6_address = valid_value.Clone();
+  *invalid_address_v4.GetDict().Find("address") =
+      base::Value("::ffff:169.254.0.1");
+  EXPECT_FALSE(IPEndPoint::FromValue(invalid_scope_id).has_value());
 }
 
 }  // namespace
diff --git a/net/dns/dns_config_service_linux_unittest.cc b/net/dns/dns_config_service_linux_unittest.cc
index 4b238b6..166b74b 100644
--- a/net/dns/dns_config_service_linux_unittest.cc
+++ b/net/dns/dns_config_service_linux_unittest.cc
@@ -104,6 +104,7 @@
     // `TestResolvReader::CloseResState()`.
     struct sockaddr_in6* sa6;
     sa6 = static_cast<sockaddr_in6*>(malloc(sizeof(*sa6)));
+    memset(sa6, 0, sizeof(*sa6));
     sa6->sin6_family = AF_INET6;
     sa6->sin6_port = base::HostToNet16(NS_DEFAULTPORT - i);
     inet_pton(AF_INET6, kNameserversIPv6[i], &sa6->sin6_addr);