Regex for an IPv4 address

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

An IPv4 address is four numbers from 0 to 255 separated by dots. A regex can’t compare numbers, so it spells out which digit strings make 0 to 255, then repeats that piece four times with dots between.

How the octet works

25[0-5]    250 to 255
2[0-4]\d   200 to 249
1\d\d      100 to 199
[1-9]?\d   0 to 99, with no leading zero

(?:…\.){3} takes three octets each followed by a dot, and the last octet stands alone. The groups are non-capturing because nothing needs to be pulled out; if you want the four numbers, split the string on dots after the test passes.

What it accepts

The tester below checks one address per line with g and m; for a single value, drop both flags.

192.168.0.1        matches
10.0.0.255         matches
255.255.255.255    matches
0.0.0.0            matches
256.1.1.1          no: 256 is out of range
192.168.01.1       no: leading zero
1.2.3              no: three parts
1.2.3.4.5          no: five parts

Why not \d{1,3}

The short version, ^(?:\d{1,3}\.){3}\d{1,3}$, also accepts 999.999.999.999. It’s fine for picking addresses out of a log you trust, and wrong for checking input.

Leading zeros are rejected on purpose. Some parsers, including the C library’s inet_aton, read 010 as octal, which is 8, so 192.168.010.1 can mean a different machine from the one the person typed.

What it doesn’t know

It checks the format, not the meaning: it can’t tell a private range such as 10.0.0.0/8 from a public address, and it knows nothing of IPv6. In Node, net.isIPv4(text) does the same check without a regex, and net.isIP(text) returns 4, 6 or 0.

For an address with a prefix length, such as 10.0.0.0/8, add an optional suffix before the final $:

(?:\/(?:3[0-2]|[12]?\d))?

That takes /0 to /32. The CIDR & Subnet Calculator shows the range a prefix covers.

Try it in the Regex Tester

Flags

4 matches

Matches

#MatchAtGroups
1192.168.0.10–11none
210.0.0.25512–22none
3255.255.255.25523–38none
40.0.0.039–46none

What the pattern means

  • ^The start of a line
  • (?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}Non-capturing group, repeated exactly 3 times
    • (?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)Non-capturing group, one of 4 alternatives
      • 25[0-5]First alternative, in order
        • 25The text "25"
        • [0-5]One of: 0–5
      • 2[0-4]\dSecond alternative, in order
        • 2The character "2"
        • [0-4]One of: 0–4
        • \dA digit (0–9)
      • 1\d\dThird alternative, in order
        • 1The character "1"
        • \dA digit (0–9)
        • \dA digit (0–9)
      • [1-9]?\dFourth alternative, in order
        • [1-9]?One of: 1–9, optional (tried before skipping it)
        • \dA digit (0–9)
    • \.The character "." itself: escaped, so it isn't "any character"
  • (?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)Non-capturing group, one of 4 alternatives
    • 25[0-5]First alternative, in order
      • 25The text "25"
      • [0-5]One of: 0–5
    • 2[0-4]\dSecond alternative, in order
      • 2The character "2"
      • [0-4]One of: 0–4
      • \dA digit (0–9)
    • 1\d\dThird alternative, in order
      • 1The character "1"
      • \dA digit (0–9)
      • \dA digit (0–9)
    • [1-9]?\dFourth alternative, in order
      • [1-9]?One of: 1–9, optional (tried before skipping it)
      • \dA digit (0–9)
  • $The end of a line
  • g global: Find every match, not just the first. exec() and test() carry on from lastIndex.
  • m multiline: ^ and $ match at the start and end of each line, not just of the whole string.

Runs in your browser. Nothing you enter is sent anywhere.