Validate an IP address in PHP (filter_var, and when you need more)
PHP validates IP addresses without a single dependency — filter_var does format and private/reserved rejection natively; an API only earns its place past that line.
Contents
If you searched how to validate an IP address in PHP, the honest answer is that you almost certainly do not need a library, a package, or an API. PHP ships a built-in filter that checks IP format, and with a flag or two will also pin the version to IPv4 or IPv6 and reject private and reserved ranges — all in the standard library, no dependency, no network call. Most of the time that filter is the whole job. This post shows exactly how to use it, then draws a clear line at the point where a filter stops answering your question and a service starts.
When "validate IP address in PHP" just means filter_var
The core tool is filter_var with the FILTER_VALIDATE_IP filter. It returns the IP string on success and false on failure, so a strict comparison against false is the correct check.
<?php
function isValidIp(string $ip): bool
{
return filter_var($ip, FILTER_VALIDATE_IP) !== false;
}
var_dump(isValidIp('8.8.8.8')); // bool(true)
var_dump(isValidIp('2606:4700:4700::1111')); // bool(true)
var_dump(isValidIp('999.1.1.1')); // bool(false)
var_dump(isValidIp('not-an-ip')); // bool(false)
That is the answer for "is this a syntactically valid IP address." No Composer install, no wrapper. If format is all you care about, you can stop reading here — the standard library has you covered.
Pin the version: IPv4 or IPv6
By default FILTER_VALIDATE_IP accepts both families. When your schema expects one — an IPv4 column, an IPv6-only feature flag — pass a flag to pin the version. FILTER_FLAG_IPV4 accepts only v4, FILTER_FLAG_IPV6 only v6.
<?php
// Accept IPv4 only
filter_var('10.0.0.5', FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); // "10.0.0.5"
filter_var('::1', FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); // false
// Accept IPv6 only
filter_var('::1', FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); // "::1"
filter_var('10.0.0.5', FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); // false
Pinning the version at the boundary means a v6 string never sneaks into a v4-shaped field, and you fail fast instead of storing something your downstream code can't parse.
Reject private and reserved ranges natively
Here is the part people reach for a library to do, and shouldn't. filter_var will reject private and reserved ranges for you with two more flags: FILTER_FLAG_NO_PRIV_RANGE fails on private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, and fc00::/7), and FILTER_FLAG_NO_RES_RANGE fails on reserved ranges (loopback, link-local, and similar). Combine them with a bitwise OR.
<?php
function isPublicIp(string $ip): bool
{
return filter_var(
$ip,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
) !== false;
}
var_dump(isPublicIp('8.8.8.8')); // bool(true) — public
var_dump(isPublicIp('192.168.1.1')); // bool(false) — private
var_dump(isPublicIp('127.0.0.1')); // bool(false) — reserved (loopback)
var_dump(isPublicIp('169.254.0.1')); // bool(false) — reserved (link-local)
If you're accepting a client-supplied IP — a header like X-Forwarded-For, a webhook source, a form field — this one line filters out the addresses that should never appear as a public source. If the difference between those buckets is fuzzy to you, what is a private IP address walks through the ranges and why they exist. For a value you just want to paste and check without writing any PHP, the keyless IP validator tool runs the same classification in the browser.
What filter_var does not tell you
filter_var answers a boolean: valid or not, public or not. It does not tell you where the address is, who runs it, or whether it's a VPN, proxy, or Tor exit. That's the honest limit of format-and-range validation in any language — and it's worth stating plainly, because a "valid public IP" is not the same as a trustworthy one.
At Boundstone we don't paper over that gap. IP intelligence — geolocation, ASN, hosting/datacenter detection, proxy/VPN/Tor detection, reputation — needs licensed data, and it is not shipped. Every response says so in a machine-readable list rather than leaving you to guess.
Classification over HTTP, as one system
So where does an API earn its place past the standard library? Not by re-doing filter_var's job over the network — that would be slower for no gain. It earns it when you want the classification itself as structured data, returned by the same contract that validates your phone numbers and email addresses, so one integration covers all three. POST /v1/verify/ip returns valid, version, normalized, classification, is_public, and is_bogon.
<?php
$ch = curl_init('https://api.boundstone.io/v1/verify/ip');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer bs_live_YOUR_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['ip' => '8.8.8.8']),
]);
$res = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $res['classification']; // "public"
var_dump($res['is_public']); // bool(true)
var_dump($res['is_bogon']); // bool(false)
// The honesty contract — read it, don't assume:
print_r($res['checks']['performed']);
// ["format", "version", "range_classification"]
print_r($res['checks']['not_performed']);
// ["geolocation", "asn", "hosting_datacenter", "proxy_vpn_tor", "reputation"]
The checks.not_performed array is the point. It tells your code, every call, that a classification of "public" was reached by range math and nothing else — no geo, no VPN detection was consulted. That's what makes the "public" you get back trustworthy: you know precisely what it does and does not mean.
The short version
- Just validating format?
filter_var($ip, FILTER_VALIDATE_IP)— done, no dependency. - Need a specific family? Add
FILTER_FLAG_IPV4orFILTER_FLAG_IPV6. - Rejecting private/reserved source IPs? Add
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE. PHP does this natively — reach for it before any package. - Want public/private/bogon classification as structured data across phone, email, and IP in one contract?
POST /v1/verify/ip, and readchecks.not_performed. - Geolocation, VPN, or proxy detection? Not something
filter_varcan do — and at Boundstone it's honestlynot_performeduntil licensed data ships.
Same story in another runtime? Here's how to validate an IP in Node.js. The docs cover the full response shape and the free tier: 250 credits a month, no card, credits never expire.