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);