---
title: "URLs and blocked networks — Safe Fetch for Laravel | Roundly"
description: "How URLs are canonicalised, the 26 blocked ranges, IPv4 inside IPv6, cloud-metadata endpoints, the inspection helpers and a resolver of your own."
url: https://roundly-consulting.com/open-source/docs/safe-fetch-for-laravel/urls-and-networks
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Safe Fetch for Laravel](https://roundly-consulting.com/open-source/docs/safe-fetch-for-laravel.md)

# URLs and blocked networks

## Canonicalisation

Every URL is parsed by the guard itself, never by PHP’s own URL parser: that parser and curl read some hosts differently (http://a.com\\@127.0.0.1/, http://127.0.0.1#@a.com/), so a guard would vet one host while curl dials another. Anything a parser could read two ways is refused, and the request is sent to the URL rebuilt from the parts that were vetted. In order, first failure wins:

- A C0 control character or DEL anywhere → invalid\_url.
- No scheme: → invalid\_url; a scheme other than http or https → scheme.
- No // or an empty authority → invalid\_url.
- A space, \\, %, Unicode whitespace or U+FEFF in the authority → non\_canonical\_host (a percent-encoded host, an IPv6 zone id).
- @ in the authority → credentials.
- A port that is not digits → invalid\_url; outside 1–65535 → port.
- The host: a bracketed IPv6 address must be valid and is compressed (\[0:0:0:0:0:0:0:1\] → \[::1\]); a non-ASCII or xn-- label goes through UTS #46 (needs ext-intl); lowercased; one trailing dot dropped; a host whose last label is numeric or hex must be exactly four decimal labels 0–255 without leading zeros, typed as such — else non\_canonical\_host.
- A \\ in the path, query or fragment → invalid\_url.

The fragment is dropped. The result is a CanonicalUrl — toString() gives a lowercase scheme and host, no user info, no fragment, no default port, IPv6 compressed and bracketed, path and query verbatim (empty → /); origin() gives scheme://host:port with the port always written. The scheme policy is applied afterwards, so a canonical http:// URL parses fine and is refused (scheme) at vetting.

## The blocked-network table

| Range | What |
| --- | --- |
| `0.0.0.0/8` | “this” network |
| `10.0.0.0/8 · 172.16.0.0/12 · 192.168.0.0/16` | private |
| `100.64.0.0/10` | carrier-grade NAT (Alibaba Cloud metadata at 100.100.100.200) |
| `127.0.0.0/8` | loopback |
| `168.63.129.16/32` | Azure WireServer (public address space, reachable from every Azure VM and AKS node) |
| `169.254.0.0/16` | link-local: AWS, GCP, Azure, OpenStack metadata at 169.254.169.254 |
| `192.0.0.0/24` | IETF protocol assignments (Oracle Cloud metadata at 192.0.0.192) |
| `192.0.2.0/24 · 198.51.100.0/24 · 203.0.113.0/24` | documentation |
| `192.88.99.0/24` | 6to4 relay anycast |
| `198.18.0.0/15` | benchmarking |
| `224.0.0.0/4` | multicast |
| `240.0.0.0/4` | reserved, broadcast |
| `64:ff9b:1::/48` | NAT64, local use |
| `100::/64` | discard |
| `2001::/23` | IETF protocol assignments (Teredo included) |
| `2001:db8::/32 · 3fff::/20` | documentation |
| `5f00::/16` | SRv6 SIDs |
| `fc00::/7` | unique local (AWS metadata over IPv6 at fd00:ec2::254) |
| `fe80::/10` | link-local |
| `fec0::/10` | site-local |
| `ff00::/8` | multicast |

- An IPv6 address that carries an IPv4 address is judged by the IPv4 address — the first match of ::ffff:0:0/96 (mapped), ::/96 (compatible, incl. :: and ::1), 64:ff9b::/96 (NAT64), ::ffff:0:0:0/96 (translated) and 2002::/16 (6to4). So \[::ffff:127.0.0.1\] and \[2002:a9fe:a9fe::\] are blocked, and 64:ff9b::808:808 (NAT64 for 8.8.8.8) is public.
- localhost and every \*.localhost are refused by name before any lookup.
- Anything unparseable counts as private. An IPv6 zone id on an address is ignored for the judgement — in a URL it is refused (%).

## PrivateNetworks

RoundlyConsulting\\SafeFetch\\Support\\PrivateNetworks holds the table — public, pure and static, no configuration, no lookup. The policy is applied on top of it by the guard; SafeFetch::networks() wraps isPrivate(), isBlockedHostname() and BLOCKED for code that prefers the facade:

| Member | What |
| --- | --- |
| `BLOCKED` | list<string> — the 26 ranges above. |
| `IPV4_EMBEDDING` | The 5 IPv6 prefixes that carry an IPv4 address, and the byte it starts at. |
| `METADATA` | The 6 cloud-metadata ranges and addresses. |
| `BLOCKED_HOSTNAMES · BLOCKED_HOSTNAME_SUFFIXES` | \['localhost'\] · \['.localhost'\] — refused by spelling. |
| `isPrivate(string $address): bool` | In the table, or carries an address that is; unparseable → true; a zone id is ignored. |
| `isMetadata(string $address): bool` | A cloud-metadata endpoint, or an IPv6 address carrying one. |
| `isBlockedHostname(string $host): bool` | localhost or \*.localhost, any case, one trailing dot dropped. |
| `carriedIpv4(string $address): ?string` | The IPv4 address an IPv6 one carries, dotted; null for IPv4, an address that carries none, or anything unparseable. |

```php
use RoundlyConsulting\SafeFetch\Support\PrivateNetworks;

PrivateNetworks::carriedIpv4('2002:a9fe:a9fe::');        // '169.254.169.254' — 6to4
PrivateNetworks::carriedIpv4('64:ff9b::808:808');        // '8.8.8.8' — NAT64
PrivateNetworks::isMetadata('::ffff:169.254.169.254');   // true
```

## Inspection helpers

The handles and the vetted destination expose what the guard computes, without sending anything:

```php
SafeFetch::networks()->isPrivate('169.254.169.254');        // true — cloud metadata
SafeFetch::networks()->isPrivate('::ffff:10.0.0.1');        // true — judged by the IPv4 address it carries
SafeFetch::networks()->isBlockedHostname('api.localhost');  // true
SafeFetch::networks()->blockedRanges();                     // the 26 ranges, IPv4 and IPv6

$url = SafeFetch::urls()->canonicalise('HTTPS://Example.COM.:443/a?b#frag');
$url->toString();                                           // 'https://example.com/a?b'
$url->origin();                                             // 'https://example.com:443'
SafeFetch::urls()->resolveRedirect('https://example.com/a/b', '../c'); // 'https://example.com/c'

$vetted = SafeFetch::vet('https://hooks.example/in');
$vetted->addresses;                                         // every address the name resolved to, each judged
$vetted->resolveEntry();                                    // 'hooks.example:443:93.184.216.34,[2606:2800:220:1:248:1893:25c8:1946]'
$vetted->connectsTo('::ffff:93.184.216.34');                // true — the mapped form is the same address
```

canonicalise() throws DestinationRejected with the parser’s reason (invalid\_url, scheme, credentials, non\_canonical\_host, port); resolveRedirect() throws it (invalid\_url) for a relative reference no parser agrees on.

## A resolver of your own

Every lookup goes through Contracts\\HostResolver — one method, resolve(string $host): array, returning every IPv4 and IPv6 address (IPv6 without brackets), \[\] when the name does not resolve. The host it receives is already canonical. The default, Support\\SystemHostResolver, uses gethostbynamel() (the system resolver, hosts file included) and dns\_get\_record(DNS\_AAAA). Whatever a resolver returns is still judged and pinned: it cannot make a private address pass, and an answer that is no address is refused.

```php
use RoundlyConsulting\SafeFetch\Contracts\HostResolver;

final class DnsRecordResolver implements HostResolver
{
    public function resolve(string $host): array
    {
        $addresses = [];

        foreach (@dns_get_record($host, DNS_A | DNS_AAAA) ?: [] as $record) {
            $addresses[] = $record['ip'] ?? $record['ipv6'] ?? null;
        }

        return array_values(array_unique(array_filter($addresses, is_string(...))));
    }
}
```

```php
// config/safe-fetch.php
'resolver' => App\Support\DnsRecordResolver::class,

// or in a service provider
app()->instance(HostResolver::class, new DnsRecordResolver);
```

A class that is not an instantiable HostResolver throws InvalidConfigurationException when resolved. In tests, use SafeFetch::fakeDns() instead.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/safe-fetch-for-laravel.md)
