blob: 4a6d772f2021a5940b769f521b529789538dc68b [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 "url/gurl.h"
namespace network {
namespace {
using mojom::IPAddressSpace;
using net::IPAddress;
using net::IPEndPoint;
// 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);
}
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;
}
// Represents a single command-line-specified endpoint override.
struct EndpointOverride {
// The IP endpoint to override the address space for.
IPEndPoint endpoint;
// The IP address space to which `endpoint` should be mapped.
IPAddressSpace space;
};
// Parses an override from `str`, of the form "<endpoint>=<space>".
std::optional<EndpointOverride> ParseEndpointOverride(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 endpoint and the address space.
if (tokens.size() != 2) {
return std::nullopt;
}
std::string_view endpoint = tokens[0];
std::string_view address_space = tokens[1];
std::optional<IPEndPoint> parsed_endpoint = ParseEndpoint(endpoint);
if (!parsed_endpoint.has_value()) {
return std::nullopt;
}
std::optional<IPAddressSpace> parsed_address_space =
ParseIPAddressSpace(address_space);
if (!parsed_address_space.has_value()) {
return std::nullopt;
}
EndpointOverride result;
result.endpoint = std::move(*parsed_endpoint);
result.space = *parsed_address_space;
return result;
}
// Parses a comma-separated list of overrides. Ignores invalid entries.
std::vector<EndpointOverride> ParseEndpointOverrideList(std::string_view list) {
// Since we skip invalid entries anyway, we can skip empty entries.
std::vector<std::string_view> tokens = base::SplitStringPiece(
list, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
std::vector<EndpointOverride> endpoint_overrides;
for (std::string_view token : tokens) {
std::optional<EndpointOverride> parsed = ParseEndpointOverride(token);
if (parsed.has_value()) {
endpoint_overrides.push_back(*std::move(parsed));
}
}
return endpoint_overrides;
}
// Applies overrides specified on the command-line to `endpoint`.
// Returns nullopt if no override matches `endpoint`.
std::optional<IPAddressSpace> ApplyCommandLineOverrides(
const IPEndPoint& endpoint) {
const base::CommandLine& command_line =
*base::CommandLine::ForCurrentProcess();
if (!command_line.HasSwitch(switches::kIpAddressSpaceOverrides)) {
return std::nullopt;
}
std::string switch_str =
command_line.GetSwitchValueASCII(switches::kIpAddressSpaceOverrides);
std::vector<EndpointOverride> endpoint_overrides =
ParseEndpointOverrideList(switch_str);
for (const auto& endpoint_override : endpoint_overrides) {
if (endpoint_override.endpoint == endpoint) {
return endpoint_override.space;
}
if ((endpoint_override.endpoint.port() == 0) &&
(endpoint_override.endpoint.address() == endpoint.address())) {
return endpoint_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 = ApplyCommandLineOverrides(endpoint);
if (space.has_value()) {
return *space;
}
return IPAddressToIPAddressSpace(endpoint.address());
}
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";
}
}
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;
}
bool IsRFC6762LocalDomain(const GURL& url) {
return url.DomainIs("local");
}
} // namespace network