The naive pattern
An IPv4 address is four numbers separated by dots: \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}. With anchors for validation: ^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$.
This accepts 999.999.999.999 which isn't a real IP. Each octet is 0-255.
The strict pattern
Each octet 0-255, expressed in regex:
(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)
Reading: 25[0-5] (250-255), OR 2[0-4]\d (200-249), OR 1\d\d (100-199), OR [1-9]?\d (0-99 with optional leading digit).
Full IPv4 with anchors:
^((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)$
Ugly but correct.
Or — let the standard library do it
For actual validation, use the IP parsing library: Python ipaddress.ip_address(s). It handles IPv4, IPv6, CIDR notation, and edge cases the regex doesn't. The regex is for FINDING IP-shaped strings; the library is for VALIDATING them.
IPv6 — don't even try
IPv6 has 8 groups of 4 hex digits, with multiple compression forms (::, dotted-quad embedded). The regex is over 100 characters and still doesn't catch all valid forms. Use ipaddress in Python, net.ParseIP in Go.