You're capturing the octets individually. You already have to do bounds checking on RFC 1918 ranges and such. Why wouldn't you do the octet checking in your post-match logic? This is just pedantic.
^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$