TechEarl

How to Match an IPv4 and IPv6 Address with Regex

Match an IPv4 or IPv6 address with regex. Octet-bounded IPv4, compressed and full IPv6, CIDR, IPv4-mapped IPv6, parser fallback, engine notes, and common mistakes.

Ishan KarunaratneIshan Karunaratne⏱️ 9 min readUpdated
Match an IPv4 or IPv6 address with regex. Octet-bounded IPv4, compressed/full IPv6, CIDR, mapped IPv6, parser fallback. JavaScript / Python / PHP examples, engine notes, common mistakes.

The practical IPv4 regex with octet bounds: ^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$. It rejects 999.999.999.999 (which a naive \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} would match) by encoding the 0-255 range per octet. The full IPv6 pattern is much longer and supports the compressed :: form, IPv4-mapped addresses like ::ffff:192.0.2.1, and zone identifiers. Below I walk both with code in JavaScript, Python, and PHP, engine notes per language, common mistakes, and call out when to skip regex entirely and use the language's built-in IP parser.

The reason IP regex is famously tricky is that IPv6 has eight different shorthand forms for the same address. The full pattern has to handle every one.

Quick reference

Bounded IPv4, ready to paste:

code
^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$

Lazy IPv4 (use only for log scraping, then validate ranges in code):

code
^(\d{1,3}\.){3}\d{1,3}$

IPv4 with CIDR:

code
^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\/(?:3[0-2]|[12]?[0-9])$

IPv4: the bounded pattern (rejects 999.999.999.999)

code
^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$

The single-octet sub-pattern reads as: 25[0-5] (250-255) or 2[0-4][0-9] (200-249) or 1[0-9]{2} (100-199) or [1-9]?[0-9] (0-99). This guarantees every octet is 0-255. Wrap in {3} for the first three octets and then one more without the trailing dot.

What this rejects:

  • 999.0.0.1, because the last octet pattern requires 0-255.
  • 1.2.3, because only three groups appear; the regex requires four.
  • 1.2.3.4.5, five groups.
  • 01.02.03.04. Strictly speaking the [1-9]?[0-9] branch allows leading zeros only at the small numbers (0-9). A leading-zero 010 would fail because 010 does not match any of the four branches cleanly.

The lazy version most people write: ^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$. It is simpler and accepts garbage like 999.999.999.999. Use it only for log scanning where you'll do range validation in a second step.

IPv6: the full pattern (with compression)

IPv6 addresses are eight 16-bit groups in hex, separated by colons. The shorthand allows leading zeros to drop and consecutive zero-groups to collapse to :: (used once per address).

code
^(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}$|^(?:[0-9A-Fa-f]{1,4}:){1,7}:|^(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}$|^(?:[0-9A-Fa-f]{1,4}:){1,5}(?::[0-9A-Fa-f]{1,4}){1,2}$|^(?:[0-9A-Fa-f]{1,4}:){1,4}(?::[0-9A-Fa-f]{1,4}){1,3}$|^(?:[0-9A-Fa-f]{1,4}:){1,3}(?::[0-9A-Fa-f]{1,4}){1,4}$|^(?:[0-9A-Fa-f]{1,4}:){1,2}(?::[0-9A-Fa-f]{1,4}){1,5}$|^[0-9A-Fa-f]{1,4}:(?:(?::[0-9A-Fa-f]{1,4}){1,6})$|^:(?:(?::[0-9A-Fa-f]{1,4}){1,7}|:)$|^fe80:(?:[0-9A-Fa-f]{0,4}:){0,4}%[0-9a-zA-Z]+$|^::(?:[Ff]{4}(?::0{1,4})?:)?(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$|^(?:[0-9A-Fa-f]{1,4}:){1,4}:(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$

Yes, it is that long. The pattern is a union of every legal IPv6 form: full 8-group, various compressed forms, link-local with zone identifier (fe80::1%eth0), IPv4-mapped (::ffff:192.0.2.1), and IPv4-translated.

In practice, almost nobody writes this regex by hand. Use the parser (next section) unless you absolutely need it.

Combined IPv4 or IPv6

For "is this any IP address", alternate the two patterns:

code
^(IPv4-pattern)|(IPv6-pattern)$

In code, this is usually two separate test() calls (one per pattern) rather than one giant union, for readability.

CIDR (network prefix length)

CIDR notation is an IP followed by a slash and a prefix length: 192.0.2.0/24 or 2001:db8::/32. To match:

code
IPv4 CIDR: ^(IPv4-pattern)\/(?:3[0-2]|[12]?[0-9])$
IPv6 CIDR: ^(IPv6-pattern)\/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])$

The prefix length is 0-32 for IPv4 and 0-128 for IPv6.

Examples in JavaScript, Python, and PHP

JavaScript:

javascript
const ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$/;

function isValidIPv4(input) {
  return ipv4.test(input);
}

isValidIPv4("192.0.2.1");      // true
isValidIPv4("999.0.0.1");      // false
isValidIPv4("192.0.2");        // false

Python:

python
import re

IPV4 = re.compile(
    r"^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}"
    r"(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$"
)

def is_valid_ipv4(value: str) -> bool:
    return bool(IPV4.match(value))

PHP:

php
function isValidIPv4(string $value): bool {
    $pattern = '/^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$/';
    return (bool) preg_match($pattern, $value);
}

When to skip regex and use the IP parser

For anything more than a quick format check, use the language's built-in IP parser. They handle every shorthand, IPv4-mapped, and zone-identifier case correctly without you having to maintain the long regex.

JavaScript (Node.js):

javascript
const net = require("node:net");
net.isIP("192.0.2.1");       // 4
net.isIP("2001:db8::1");     // 6
net.isIP("not.an.ip");       // 0

Python:

python
import ipaddress
try:
    ipaddress.ip_address("2001:db8::1")  # works for both v4 and v6
    valid = True
except ValueError:
    valid = False

PHP:

php
$isV4 = filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
$isV6 = filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
$isAny = filter_var($value, FILTER_VALIDATE_IP);

The parser route is dramatically simpler and more correct. The regex route is for when you can't easily call a parser (logs being scanned by grep, validation rules in a config file that only accepts patterns, etc.).

Engine compatibility

The bounded IPv4 pattern is universal. The IPv6 pattern uses alternation and non-capturing groups, which all major engines support. The parser is the better option when available.

EngineBounded IPv4Full IPv6Parser fallback
JavaScriptWorksWorksnet.isIP (Node) returns 4 / 6 / 0
PythonWorksWorksipaddress.ip_address, ipaddress.ip_network for CIDR
PHP (PCRE)WorksWorksfilter_var($v, FILTER_VALIDATE_IP) with FILTER_FLAG_IPV4 / FILTER_FLAG_IPV6
JavaWorksWorksInetAddress.getByName (which also does DNS, so guard with isInetAddress check first)
.NETWorksWorksIPAddress.TryParse(s, out _)
Go (RE2)WorksWorksnet.ParseIP(s) returns nil for invalid input
Rust (regex crate)WorksWorksstd::net::IpAddr::from_str
RubyWorksWorksIPAddr.new(s) raises on invalid
POSIX ERE (grep -E)Works (replace \d with [0-9])WorksNone; combine with shell logic

For Rust and Go, where the IPv6 regex is annoyingly long, I always use the parser and only fall back to regex when scanning streams.

Common mistakes

The bugs I've shipped or seen in code review.

Using \d{1,3} per octet and calling it done. This matches 999.999.999.999, 0.0.0.0, and every other "four numbers separated by dots" but says nothing about whether each number is in range. Use the bounded form for validation; the lazy form is acceptable for log scraping only.

Forgetting the anchors. Without ^ and $, the IPv4 pattern matches 99.999.0.0.1 because the engine finds a valid IP substring at position 1. Always anchor for validation.

Accepting leading zeros silently. 010.020.030.040 looks valid but most parsers treat octets with leading zeros as octal, which changes the actual address. The bounded pattern in this article rejects leading-zero forms because the [1-9]?[0-9] branch only allows 0 by itself.

Treating the IPv6 zone identifier as part of the address. fe80::1%eth0 is a valid link-local address, but the %eth0 part is local context, not a network-routable suffix. If you store the IP for later use, strip the zone identifier first.

Trying to handle CIDR in the same pattern as the address. It gets ugly fast. Match the address and the prefix separately, or split on / first and validate the two halves independently.

Forgetting that :: can appear at most once. 2001::db8::1 is invalid because the regex can't tell which group of zeros each :: expands to. Real parsers reject this; a sloppy regex might accept it.

Test cases

InputIPv4IPv6
192.0.2.1MatchNo match
255.255.255.255MatchNo match
0.0.0.0MatchNo match
192.0.2No matchNo match
999.0.0.1No matchNo match
2001:db8::1No matchMatch
::1No matchMatch
::ffff:192.0.2.1No matchMatch (IPv4-mapped)
fe80::1%eth0No matchMatch (zone)
not.an.ipNo matchNo match

FAQ

See also

External reference: the IETF RFC 4291 defines IPv6 addressing. For interactive testing of these patterns, paste into regex101.com.

TagsRegexIPv4IPv6NetworkRegular ExpressionsJavaScriptPythonPHP
Share
Ishan Karunaratne

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years across software, Linux systems, DevOps, and infrastructure — and a more recent focus on AI. Currently Chief Technology Officer at a tech startup in the healthcare space.

Keep reading

Related posts

Match an email address with regex. Practical pattern, strict RFC 5321 pattern, JavaScript / Python / PHP examples, edge cases, engine compatibility, common mistakes, and a test table.

How to Match an Email Address with Regex

Match an email address with regex. The practical pattern, the strict RFC 5321 pattern, examples in JavaScript, Python, and PHP, edge cases, engine compatibility, common mistakes, and a validation test table.

Match a domain name with regex. Basic labels, RFC 1035 length rules, subdomains, IDN punycode, trailing-dot form, JavaScript / Python / PHP examples, engine notes, and common mistakes.

How to Match a Domain Name with Regex

Match a domain name with regex. Basic labels, RFC 1035 length rules, subdomains, IDN punycode, trailing-dot form, JavaScript / Python / PHP examples, engine notes, and common mistakes.

Match a URL with regex. http/https schemes, protocol-relative URLs, ports, paths, query strings, fragments. JavaScript / Python / PHP examples, engine notes, parser alternative, common mistakes, test table.

How to Match a URL with Regex

Match a URL with regex. Covers http/https schemes, protocol-relative URLs, ports, paths, query strings, fragments, runnable JavaScript / Python / PHP, engine notes, and the URL parser alternative.