Ip
Ip works with IP addresses (IPv4 and IPv6) and ranges of them (CIDR). It is made of pure functions: they depend only on their arguments, never on $_SERVER, so they give the same answer anywhere.
What it does not do is say which address is the one of the client of a request. That depends on the deployment (which proxies are trusted to say it), and it is not a property of an address: take it from where it is decided, and use Ip to check and compare it.
Two rules apply to the whole class:
- An address that is not valid is a question that has an answer. The functions that ask something about an address (
isPrivate(),inRange()…) sayfalse, and the ones that give an address (normalize(),network()…) saynull. - A range or a prefix that is not valid is an error of the configuration. It is an
InvalidArgumentException(translatable, see Errors): a range that silently matches nothing is a hole that nobody sees.
An IPv4 address written as IPv6 (::ffff:192.0.2.1, which is what a socket of IPv6 gives for a client of IPv4) is the IPv4 address: normalize() gives it as IPv4, and the questions are asked about the IPv4 address.
Validating and normalizing
| Method | What it does |
|---|---|
Ip::isValid($ip) |
Whether the text is an IP address. |
Ip::version($ip) |
4, 6 or null. |
Ip::isIpv4($ip), Ip::isIpv6($ip) |
Whether it is of that version (an IPv4 written as IPv6 is IPv6). |
Ip::normalize($ip) |
The canonical form of the address, or null. |
Ip::expand($ip) |
The long form of an IPv6, with its eight groups. |
Ip::parse($address) |
The address inside a text with a port, brackets, a zone or quotes. |
use Derafu\Support\Ip;
Ip::isValid('192.0.2.1'); // true
Ip::isValid('010.0.0.1'); // false: it could be read as octal.
Ip::isValid('192.0.2.1:80'); // false: it is not only the address.
Ip::version('2001:db8::1'); // 6
Ip::normalize('2001:DB8:0:0:0:0:0:1'); // "2001:db8::1"
Ip::normalize('::ffff:192.0.2.1'); // "192.0.2.1"
Ip::expand('2001:db8::1'); // "2001:0db8:0000:0000:0000:0000:0000:0001"
The canonical form makes the same address always the same text, so it is safe to use as a key, to compare or to store: IPv6 is in lowercase and compressed as RFC 5952 says (the longest run of zero groups becomes ::, and a single zero group is not compressed), and an IPv4 written as IPv6 is IPv4. The text does not depend on the system.
isValid() accepts only the address. parse() takes it out of the forms that headers and logs use, and normalizes it:
Ip::parse('192.0.2.1:8080'); // "192.0.2.1"
Ip::parse('[2001:DB8::1]:443'); // "2001:db8::1"
Ip::parse('[2001:db8::1]'); // "2001:db8::1"
Ip::parse('fe80::1%eth0'); // "fe80::1": the zone is removed.
Ip::parse('"[2001:db8::1]:4711"'); // "2001:db8::1": quoted, as the header Forwarded does.
Ip::parse('::1:80'); // "::1:80": without brackets an IPv6 has no port.
Ip::parse('unknown'); // null
Ip::parse('example.com:80'); // null: it does not resolve names.
Kinds of address
| Method | It is true for |
|---|---|
Ip::isPrivate($ip) |
The private networks (RFC 1918: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) and the unique local addresses of IPv6 (fc00::/7). |
Ip::isReserved($ip) |
The ranges that the IANA reserves, as PHP defines them: the loopback, 0.0.0.0/8 and ::, the link-local ones, 240.0.0.0/4 and the others that PHP knows. |
Ip::isLoopback($ip) |
127.0.0.0/8 and ::1. |
Ip::isLinkLocal($ip) |
169.254.0.0/16 and fe80::/10. |
Ip::isPublic($ip) |
A valid address that is neither private nor reserved. |
Ip::isPrivate('10.0.0.1'); // true
Ip::isPrivate('127.0.0.1'); // false: the loopback is reserved, not private.
Ip::isReserved('127.0.0.1'); // true
Ip::isPublic('8.8.8.8'); // true
Ip::isPublic('::ffff:10.0.0.1'); // false: it is the private 10.0.0.1.
Ip::isPublic('nope'); // false
The addresses that are only for documentation (192.0.2.0/24, 203.0.113.0/24, 2001:db8::/32) are not excluded by PHP, so they are public here.
Ranges
A range is written in CIDR notation (10.0.0.0/8, 2001:db8::/32), or it is an address alone (192.0.2.1, a range of one). The bits that the prefix does not use are ignored (10.1.2.3/8 is 10.0.0.0/8).
| Method | What it does |
|---|---|
Ip::inRange($ip, $range) |
Whether the address is in the range. |
Ip::inAnyRange($ip, $ranges) |
Whether it is in any of the ranges (a list, or any iterable). |
Ip::isRange($range) |
Whether the text is a valid range. |
Ip::range($range) |
The first and the last address of the range. |
Ip::cidr($ip, $ipv4Prefix = 32, $ipv6Prefix = 64) |
The network of an address, written as a range (see Networks). |
Ip::inRange('10.1.2.3', '10.0.0.0/8'); // true
Ip::inRange('172.31.255.255', '172.16.0.0/12'); // true
Ip::inRange('2001:db8:1::5', '2001:db8::/32'); // true
Ip::inRange('192.0.2.1', '::/0'); // false: an address is never in a range of the other version.
Ip::inRange('::ffff:10.1.2.3', '10.0.0.0/8'); // true: it is the IPv4 address.
Ip::inRange('nope', '0.0.0.0/0'); // false: it is not an address, so it is in nothing.
Ip::inAnyRange($ip, ['10.0.0.0/8', '192.168.0.0/16', '::1']);
Ip::range('192.168.1.0/24'); // ['192.168.1.0', '192.168.1.255']
Ip::range('2001:db8::/32'); // ['2001:db8::', '2001:db8:ffff:ffff:ffff:ffff:ffff:ffff']
A range that is not valid (10.0.0.0/33, 10.0.0.0/8/9, localhost, 10.0.0.0/255.0.0.0) throws, even if the address is not an IP address, and inAnyRange() checks all the ranges, not only the ones before the one that matches. So a wrong configuration shows the first time it is used, not when an address happens to fall after it.
Networks
Ip::network($ip, $ipv4Prefix = 32, $ipv6Prefix = 64) gives the network that an address belongs to: the address with the bits of the host set to zero (the network address, as RFC 4632 calls it). Ip::cidr() takes the same arguments and gives the network written as a range, with its prefix, so the result says what it is and can be used as one (inRange(), range()).
It is the key to count or to limit by network instead of by address. A client of IPv6 has a whole /64 (or more), and changing its address within it costs nothing, so counting by address does not limit anything:
Ip::network('2001:db8:1:2:3:4:5:6'); // "2001:db8:1:2::": the /64 by default.
Ip::network('2001:db8:1:2:3:4:5:6', 32, 48); // "2001:db8:1::"
Ip::network('192.0.2.77'); // "192.0.2.77": an IPv4 is the address by default.
Ip::network('192.0.2.77', 24); // "192.0.2.0"
Ip::network('nope'); // null
Ip::cidr('2001:db8:1:2:3:4:5:6'); // "2001:db8:1:2::/64"
Ip::cidr('192.0.2.77'); // "192.0.2.77/32": a network of one address.
Ip::cidr('192.0.2.77', 24); // "192.0.2.0/24"
Ip::cidr('100.100.100.100', 10); // "100.64.0.0/10"
Each prefix is only used for the addresses of its version. A prefix that is not valid for the version of the address (33 for IPv4, 129 for IPv6, a negative one) is an InvalidArgumentException.
Comparing and binary form
$ips = ['2001:db8::2', '10.0.0.10', '10.0.0.2', '2001:db8::1', '9.9.9.9'];
usort($ips, Ip::compare(...));
// ['9.9.9.9', '10.0.0.2', '10.0.0.10', '2001:db8::1', '2001:db8::2']
Ip::toBinary('192.0.2.1'); // "\xC0\x00\x02\x01" (4 bytes for IPv4, 16 for IPv6).
Ip::fromBinary("\xC0\x00\x02\x01"); // "192.0.2.1"
compare() orders by value, not by text (10.0.0.2 goes before 10.0.0.10): every IPv4 goes before every IPv6, and the same address written in two ways is equal. An argument that is not an address is an InvalidArgumentException, because it can not be ordered.
toBinary() gives the bytes in which an address is stored or compared (null if it is not an address). fromBinary() goes back: it needs 4 or 16 bytes (null otherwise), and it gives an IPv4 written as IPv6 as IPv6 (::ffff:c000:201): the bytes say what it is. Use normalize() to have it as IPv4.