blob: 378bd1725e15e258ac200eb443112843c4616106 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/network/public/cpp/ip_address_space_util.h"
#include <utility>
#include "base/command_line.h"
#include "base/no_destructor.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "net/base/ip_address.h"
#include "net/base/ip_endpoint.h"
#include "net/base/transport_info.h"
#include "services/network/public/cpp/content_security_policy/content_security_policy.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/network_switches.h"
#include "services/network/public/mojom/ip_address_space.mojom.h"
#include "services/network/public/mojom/parsed_headers.mojom.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
#include "services/network/public/mojom/url_loader_network_service_observer.mojom-shared.h"
#include "url/gurl.h"
namespace network {
namespace {
using mojom::IPAddressSpace;
using net::IPAddress;
using net::IPEndPoint;
using CIDR = IPAddressSpaceOverrides::CIDR;
using AddressSpaceOverride = IPAddressSpaceOverrides::AddressSpaceOverride;
// Parses a string of the form "<URL-safe IP address>:<port>".
std::optional<IPEndPoint> ParseEndpoint(std::string_view str) {
// Find the last colon character in `str`. We do not use
// `base::SplitStringPiece()` because IPv6 address literals may contain colon
// characters too.
const auto pos = str.rfind(':');
if (pos == str.npos) {
return std::nullopt;
}
std::string_view address_str = str.substr(0, pos);
// Skip the colon. Note that this is safe because if `pos` is not `npos`, it
// is guaranteed to be < `str.size()`, and `substr()` accepts arguments that
// are <= `str.size()`. In other words, if the colon character is the last in
// `str`, then `port_str` is assigned "".
std::string_view port_str = str.substr(pos + 1);
IPAddress address;
if (!net::ParseURLHostnameToAddress(address_str, &address)) {
return std::nullopt;
}
// Parse to a `unsigned int`, which is guaranteed to be at least 16 bits wide.
// See https://en.cppreference.com/w/cpp/language/types.
unsigned port_unsigned = 0;
if (!base::StringToUint(port_str, &port_unsigned)) {
return std::nullopt;
}
if (!base::IsValueInRangeForNumericType<uint16_t>(port_unsigned)) {
return std::nullopt;
}
// Use `checked_cast()` for extra safety, though this should never `CHECK()`
// thanks to the previous call to `IsValueInRangeForNumericType()`.
uint16_t port = base::checked_cast<uint16_t>(port_unsigned);
return IPEndPoint(address, port);
}
// Parses an IP block specifier from CIDR notation, with the IP prefix in a
// URL-safe format (e.g. `[2001::]/16`). Unlike ParseEndpoint, no port is
// specified.
std::optional<CIDR> ParseCIDR(std::string_view str) {
size_t mask_bits;
std::optional<IPAddress> ip_address =
net::ParseCIDRBlockNonStandardURLFormat(str, &mask_bits);
if (!ip_address) {
return std::nullopt;
}
CIDR cidr(std::move(*ip_address), mask_bits);
return cidr;
// cidr.ip_address = std::move(ip_address);
// return std::make_optional({std::move(*ip_address), mask_bits});
}
std::optional<IPAddressSpace> ParseIPAddressSpace(std::string_view str) {
if (str == "public") {
return IPAddressSpace::kPublic;
}
// Keep 'private' as an alias for 'local' until usages of 'private' are
// removed from Web Platform Test code base.
//
// TODO(crbug.com/418737577): remove private alias after Web Platform Test
// code base moves to using "local"
if (str == "private") {
return IPAddressSpace::kLocal;
}
if (str == "local") {
return IPAddressSpace::kLocal;
}
if (str == "loopback") {
return IPAddressSpace::kLoopback;
}
return std::nullopt;
}
// Parses an override from `str`, of the form "<endpoint|range>=<space>".
std::optional<AddressSpaceOverride> ParseAddressSpaceOverride(
std::string_view str) {
std::vector<std::string_view> tokens = base::SplitStringPiece(
str, "=", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
// There should be 2 parts: the CIDR/endpoint and the address space.
if (tokens.size() != 2) {
return std::nullopt;
}
std::string_view endpoint_or_cidr = tokens[0];
std::string_view address_space = tokens[1];
std::optional<IPAddressSpace> parsed_address_space =
ParseIPAddressSpace(address_space);
if (!parsed_address_space.has_value()) {
return std::nullopt;
}
std::optional<IPEndPoint> parsed_endpoint = ParseEndpoint(endpoint_or_cidr);
if (parsed_endpoint.has_value()) {
AddressSpaceOverride result;
result.endpoint = std::move(*parsed_endpoint);
result.space = *parsed_address_space;
return result;
}
std::optional<CIDR> parsed_cidr = ParseCIDR(endpoint_or_cidr);
if (parsed_cidr.has_value()) {
AddressSpaceOverride result;
result.cidr = std::move(*parsed_cidr);
result.space = *parsed_address_space;
return result;
}
return std::nullopt;
}
std::optional<IPAddressSpace> ApplyOverrides(
const IPEndPoint& endpoint,
std::vector<AddressSpaceOverride>& address_space_overrides) {
for (const auto& address_space_override : address_space_overrides) {
if (address_space_override.endpoint) {
if (address_space_override.endpoint == endpoint) {
return address_space_override.space;
}
if ((address_space_override.endpoint->port() == 0) &&
(address_space_override.endpoint->address() == endpoint.address())) {
return address_space_override.space;
}
} else if (address_space_override.cidr) {
// net::IPAddressMatchesPrefix will automatically convert IPv4 address to
// IPv4-mapped IPv6 addresses if only one of the address is IPv4. To avoid
// overrides in this case, a check is added to ensure that overrides only
// occur when both addresses are IPv4 or both are IPv6.
if (endpoint.address().IsIPv4() ==
address_space_override.cidr->ip_address.IsIPv4() &&
net::IPAddressMatchesPrefix(endpoint.address(),
address_space_override.cidr->ip_address,
address_space_override.cidr->mask_bits)) {
return address_space_override.space;
}
}
}
return std::nullopt;
}
// Represents a single entry of the form: subnet -> address space.
class AddressSpaceMapEntry {
public:
AddressSpaceMapEntry(IPAddress prefix,
size_t prefix_length,
IPAddressSpace space)
: prefix_(std::move(prefix)),
prefix_length_(prefix_length),
space_(space) {}
// Returns the assigned address space if `address` belongs to this instance's
// subnet. Returns nullopt otherwise.
std::optional<IPAddressSpace> Apply(const IPAddress& address) const;
private:
IPAddress prefix_;
size_t prefix_length_ = 0;
IPAddressSpace space_ = IPAddressSpace::kUnknown;
};
std::optional<IPAddressSpace> AddressSpaceMapEntry::Apply(
const IPAddress& address) const {
if (net::IPAddressMatchesPrefix(address, prefix_, prefix_length_)) {
return space_;
}
return std::nullopt;
}
// Maps IP addresses to IP address spaces.
class AddressSpaceMap {
public:
explicit AddressSpaceMap(std::vector<AddressSpaceMapEntry> entries)
: entries_(std::move(entries)) {}
// Applies entries in this map to `address`, in sequential order.
// Returns the address space of the first matching entry.
// Returns nullopt if no match is found.
std::optional<IPAddressSpace> Apply(const IPAddress& address) const;
private:
std::vector<AddressSpaceMapEntry> entries_;
};
std::optional<IPAddressSpace> AddressSpaceMap::Apply(
const IPAddress& address) const {
std::optional<IPAddressSpace> space;
for (const AddressSpaceMapEntry& entry : entries_) {
space = entry.Apply(address);
if (space.has_value()) {
break;
}
}
// If we never found a match, `space` is still `nullopt`.
return space;
}
// Returns a map containing all default-non-public subnets.
const AddressSpaceMap& NonPublicAddressSpaceMap() {
// For brevity below, otherwise entries do not fit on single lines.
using Entry = AddressSpaceMapEntry;
// Have to repeat `AddressSpaceMap` because perfect forwarding does not deal
// well with initializer lists.
static const base::NoDestructor<AddressSpaceMap> kMap(AddressSpaceMap({
// IPv6 Loopback (RFC 4291): ::1/128
Entry(IPAddress::IPv6Localhost(), 128, IPAddressSpace::kLoopback),
// IPv6 Unique-local (RFC 4193, RFC 8190): fc00::/7
Entry(IPAddress(0xfc, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), 7,
IPAddressSpace::kLocal),
// IPv6 Link-local unicast (RFC 4291): fe80::/10
Entry(IPAddress(0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), 10,
IPAddressSpace::kLocal),
// IPv4 Loopback (RFC 1122): 127.0.0.0/8
Entry(IPAddress(127, 0, 0, 0), 8, IPAddressSpace::kLoopback),
// IPv4 Private use (RFC 1918): 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
Entry(IPAddress(10, 0, 0, 0), 8, IPAddressSpace::kLocal),
Entry(IPAddress(172, 16, 0, 0), 12, IPAddressSpace::kLocal),
Entry(IPAddress(192, 168, 0, 0), 16, IPAddressSpace::kLocal),
// IPv4 Link-local (RFC 3927): 169.254.0.0/16
Entry(IPAddress(169, 254, 0, 0), 16, IPAddressSpace::kLocal),
// IPv4 Null IP (RFC 5735): 0.0.0.0/32 is "this host on this network".
// Other addresses in 0.0.0.0/8 may refer to "specified hosts on this
// network". This is somewhat under-defined for the purposes of assigning
// local vs loopback address space but we assign 0.0.0.0/32 to "loopback"
// and the rest of the block to "local". Note that this mapping can be
// overridden by a killswitch feature flag in IPAddressToIPAddressSpace()
// since these addresses were previously treated as public. See
// https://crbug.com/40058874.
Entry(IPAddress(0, 0, 0, 0), 32, IPAddressSpace::kLoopback),
Entry(IPAddress(0, 0, 0, 0), 8, IPAddressSpace::kLocal),
// IPv6 Null IP (RFC 1884): ::/128 is the unspecified address, but many
// documentation sources consider it to be treated the same as 0.0.0.0/32,
// so we map it to "loopback" out of an abundance of caution. RFC 1884
// specifies that the unspecified address "must never be assigned to any
// node" and "must not be used as the destination address of IPv6
// datagrams".
Entry(IPAddress(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), 128,
IPAddressSpace::kLoopback),
// Carrier Grade NAT (RFC 6598): 100.64.0.0/10
Entry(IPAddress(100, 64, 0, 0), 10, IPAddressSpace::kLocal),
// IPv6 Documentation Address Prefixes (RFC 3849, RFC 9637): 2001:db8::/32
// and 3fff::/20 are reserved for documrentation purposes, and they *must
// not* be routed to the public (and in general should not be used for
// production traffic). We include them for completeness. See
// https://github.com/WICG/local-network-access/issues/15.
Entry(
IPAddress(0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
32, IPAddressSpace::kLocal),
Entry(IPAddress(0x3f, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), 20,
IPAddressSpace::kLocal),
// IPv6 Site Local Unicast (RFC 3513): fec0::/10
// These are deprecated by RFC3879 but still allowed to be used.
// See https://github.com/WICG/local-network-access/issues/15
Entry(IPAddress(0xfe, 0xc0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), 10,
IPAddressSpace::kLocal),
}));
return *kMap;
}
} // namespace
IPAddressSpace IPAddressToIPAddressSpace(const IPAddress& address) {
return NonPublicAddressSpaceMap().Apply(address).value_or(
IPAddressSpace::kPublic);
}
IPAddressSpace IPEndPointToIPAddressSpace(const IPEndPoint& endpoint) {
if (!endpoint.address().IsValid()) {
return IPAddressSpace::kUnknown;
}
std::optional<IPAddressSpace> space =
IPAddressSpaceOverrides::GetInstance().HasOverride(endpoint);
if (space.has_value()) {
return *space;
}
return IPAddressToIPAddressSpace(endpoint.address());
}
std::string_view LocalNetworkAccessResultToStringPiece(
mojom::LocalNetworkAccessResult result) {
switch (result) {
case mojom::LocalNetworkAccessResult::kGranted:
return "granted";
case mojom::LocalNetworkAccessResult::kDenied:
return "denied";
case mojom::LocalNetworkAccessResult::kRetryDueToCache:
return "retryDueToCache";
}
// In case enum value gets corrupted.
return "unknown";
}
std::string_view TransportTypeToStringPiece(
mojom::TransportType transport_type) {
switch (transport_type) {
case mojom::TransportType::kDirect:
return "direct";
case mojom::TransportType::kProxied:
return "proxied";
case mojom::TransportType::kCached:
return "cached";
case mojom::TransportType::kCachedFromProxy:
return "cachedFromProxy";
}
// In case enum value gets corrupted.
return "unknown";
}
std::string_view IPAddressSpaceToStringPiece(IPAddressSpace space) {
switch (space) {
case IPAddressSpace::kUnknown:
return "unknown";
case IPAddressSpace::kPublic:
return "public";
case IPAddressSpace::kLocal:
return "local";
case IPAddressSpace::kLoopback:
return "loopback";
}
// In case enum value gets corrupted.
return "unknown";
}
IPAddressSpace TransportInfoToIPAddressSpace(const net::TransportInfo& info) {
switch (info.type) {
case net::TransportType::kDirect:
case net::TransportType::kCached:
return IPEndPointToIPAddressSpace(info.endpoint);
case net::TransportType::kProxied:
case net::TransportType::kCachedFromProxy:
return mojom::IPAddressSpace::kUnknown;
}
}
namespace {
// For comparison purposes, we treat kUnknown the same as kPublic.
IPAddressSpace CollapseUnknown(IPAddressSpace space) {
if (space == IPAddressSpace::kUnknown) {
return IPAddressSpace::kPublic;
}
return space;
}
// For comparison purposes, we treat kLocal and kLoopback as equivalent
// (kLocal arbitrarily chosen over kLoopback).
IPAddressSpace CollapseLocalAndLoopback(IPAddressSpace space) {
if (space == IPAddressSpace::kLoopback) {
return IPAddressSpace::kLocal;
}
return space;
}
} // namespace
bool IsLessPublicAddressSpace(IPAddressSpace lhs, IPAddressSpace rhs) {
// Apart from the special case for kUnknown, the built-in comparison operator
// works just fine. The comment on IPAddressSpace's definition notes that the
// enum values' ordering matters.
return CollapseUnknown(lhs) < CollapseUnknown(rhs);
}
bool IsLessPublicAddressSpaceLNA(IPAddressSpace lhs, IPAddressSpace rhs) {
// Similar to IsLessPublicAddressSpace but with additional collapsing of
// kLocal and kLoopback.
return CollapseLocalAndLoopback(CollapseUnknown(lhs)) <
CollapseLocalAndLoopback(CollapseUnknown(rhs));
}
CalculateClientAddressSpaceParams::~CalculateClientAddressSpaceParams() =
default;
mojom::IPAddressSpace CalculateClientAddressSpace(
const GURL& url,
std::optional<CalculateClientAddressSpaceParams> params) {
if (params.has_value() &&
params->client_address_space_inherited_from_service_worker.has_value()) {
return *params->client_address_space_inherited_from_service_worker;
}
if (url.SchemeIsFile()) {
// See: https://wicg.github.io/cors-rfc1918/#file-url.
return mojom::IPAddressSpace::kLoopback;
}
if (!params.has_value()) {
return mojom::IPAddressSpace::kUnknown;
}
// First, check whether the response forces itself into a public address space
// as per https://wicg.github.io/cors-rfc1918/#csp.
DCHECK(*params->parsed_headers) << "CalculateIPAddressSpace() called for URL "
<< url << " with null parsed_headers.";
if (ShouldTreatAsPublicAddress(
(*params->parsed_headers)->content_security_policy)) {
return mojom::IPAddressSpace::kPublic;
}
// Otherwise, calculate the address space via the provided IP address.
return IPEndPointToIPAddressSpace(*params->remote_endpoint);
}
mojom::IPAddressSpace CalculateResourceAddressSpace(
const GURL& url,
const net::IPEndPoint& endpoint) {
if (url.SchemeIsFile()) {
// See: https://wicg.github.io/cors-rfc1918/#file-url.
return mojom::IPAddressSpace::kLoopback;
}
return IPEndPointToIPAddressSpace(endpoint);
}
std::optional<net::IPAddress> ParsePrivateIpFromUrl(const GURL& url) {
net::IPAddress address;
if (!address.AssignFromIPLiteral(url.HostNoBracketsPiece())) {
return std::nullopt;
}
if (IPAddressToIPAddressSpace(address) != mojom::IPAddressSpace::kLocal) {
return std::nullopt;
}
return address;
}
std::optional<mojom::IPAddressSpace> GetAddressSpaceFromUrl(const GURL& url) {
if (url.DomainIs("local")) {
return mojom::IPAddressSpace::kLocal;
}
if (url.DomainIs("localhost")) {
// Check IP address space mapping for 127.0.0.1, on the off chance that
// there is an override remapping this to something else.
net::IPEndPoint endpoint(net::IPAddress::IPv4Localhost(),
url.EffectiveIntPort());
return IPEndPointToIPAddressSpace(endpoint);
}
net::IPAddress address;
if (!address.AssignFromIPLiteral(url.HostNoBracketsPiece())) {
return std::nullopt;
}
net::IPEndPoint endpoint(address, url.EffectiveIntPort());
return IPEndPointToIPAddressSpace(endpoint);
}
IPAddressSpaceOverrides& IPAddressSpaceOverrides::GetInstance() {
static base::NoDestructor<IPAddressSpaceOverrides> s_instance;
return *s_instance;
}
IPAddressSpaceOverrides::IPAddressSpaceOverrides() = default;
IPAddressSpaceOverrides::AddressSpaceOverride::AddressSpaceOverride() = default;
IPAddressSpaceOverrides::AddressSpaceOverride::~AddressSpaceOverride() =
default;
IPAddressSpaceOverrides::AddressSpaceOverride::AddressSpaceOverride(
const AddressSpaceOverride&) = default;
IPAddressSpaceOverrides::AddressSpaceOverride&
IPAddressSpaceOverrides::AddressSpaceOverride::operator=(
const AddressSpaceOverride& other) = default;
IPAddressSpaceOverrides::AddressSpaceOverride::AddressSpaceOverride(
AddressSpaceOverride&&) = default;
IPAddressSpaceOverrides::AddressSpaceOverride&
IPAddressSpaceOverrides::AddressSpaceOverride::operator=(
AddressSpaceOverride&& other) = default;
void IPAddressSpaceOverrides::SetAuxiliaryOverrides(
const std::string& auxiliary_overrides,
std::vector<std::string>* rejected_patterns) {
// Ignore empty entries
std::vector<std::string_view> tokens =
base::SplitStringPiece(auxiliary_overrides, ",", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
std::vector<AddressSpaceOverride> address_space_overrides;
std::vector<std::string> address_space_overrides_str;
for (std::string_view token : tokens) {
std::optional<AddressSpaceOverride> parsed =
ParseAddressSpaceOverride(token);
if (parsed.has_value()) {
address_space_overrides.push_back(*std::move(parsed));
address_space_overrides_str.emplace_back(token);
} else {
rejected_patterns->emplace_back(token);
}
}
base::AutoLock lock(lock_);
auxiliary_overrides_ = std::move(address_space_overrides_str);
parsed_auxiliary_overrides_ = std::move(address_space_overrides);
}
void IPAddressSpaceOverrides::ParseCmdlineIfNeeded() {
lock_.AssertAcquired();
if (has_cmdline_been_parsed_) {
return;
}
const base::CommandLine& command_line =
*base::CommandLine::ForCurrentProcess();
std::string cmdline_overrides_str = "";
if (command_line.HasSwitch(switches::kIpAddressSpaceOverrides)) {
cmdline_overrides_str =
command_line.GetSwitchValueASCII(switches::kIpAddressSpaceOverrides);
}
std::vector<std::string_view> tokens =
base::SplitStringPiece(cmdline_overrides_str, ",", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
for (std::string_view token : tokens) {
std::optional<AddressSpaceOverride> parsed =
ParseAddressSpaceOverride(token);
if (parsed.has_value()) {
parsed_cmdline_overrides_.push_back(*std::move(parsed));
cmdline_overrides_.emplace_back(token);
}
}
has_cmdline_been_parsed_ = true;
}
void IPAddressSpaceOverrides::ResetForTesting() {
base::AutoLock lock(lock_);
cmdline_overrides_.clear();
parsed_cmdline_overrides_.clear();
has_cmdline_been_parsed_ = false;
auxiliary_overrides_.clear();
parsed_auxiliary_overrides_.clear();
}
std::optional<IPAddressSpace> IPAddressSpaceOverrides::HasOverride(
const IPEndPoint& endpoint) {
base::AutoLock lock(lock_);
ParseCmdlineIfNeeded();
auto cmdline_address_space_override =
ApplyOverrides(endpoint, parsed_cmdline_overrides_);
if (cmdline_address_space_override) {
return cmdline_address_space_override;
}
auto auxiliary_address_space_override =
ApplyOverrides(endpoint, parsed_auxiliary_overrides_);
if (auxiliary_address_space_override) {
return auxiliary_address_space_override;
}
return std::nullopt;
}
std::vector<std::string> IPAddressSpaceOverrides::GetCurrentOverrides() {
base::AutoLock lock(lock_);
ParseCmdlineIfNeeded();
std::vector<std::string> result;
result.reserve(cmdline_overrides_.size() + auxiliary_overrides_.size());
std::ranges::copy(cmdline_overrides_, std::back_inserter(result));
std::ranges::copy(auxiliary_overrides_, std::back_inserter(result));
return result;
}
} // namespace network