How to validate an IP address in Ruby
Ruby's standard library validates and classifies IP addresses without a single gem — here is the IPAddr class, the version checks, and where an HTTP API actually earns its place.
Contents
You need to validate an IP address in Ruby — maybe it arrived in a form field, a webhook payload, or a log line, and you want to know whether it's a real address before you store it or route on it. The good news, and the whole point of this post: Ruby's standard library already does this. No gem, no service call. The IPAddr class parses, validates, tells IPv4 from IPv6, and (on Ruby 2.6+) classifies private, loopback, and link-local addresses. Reach for an HTTP API only when you need something the stdlib genuinely cannot give you — and we'll be plain about where that line is.
Validate with IPAddr — the parse-or-raise pattern
IPAddr treats an invalid address as an exception, not a return value. IPAddr.new raises IPAddr::InvalidAddressError when the string isn't a valid address, so the idiomatic validator is a parse wrapped in a rescue:
require "ipaddr"
def valid_ip?(str)
IPAddr.new(str)
true
rescue IPAddr::InvalidAddressError
false
end
valid_ip?("8.8.8.8") # => true
valid_ip?("2606:4700:4700::1111") # => true
valid_ip?("999.1.1.1") # => false
valid_ip?("not an ip") # => false
One gotcha worth knowing: IPAddr.new also accepts CIDR notation and masks the host bits, so IPAddr.new("192.168.1.1/24") succeeds and represents the network 192.168.1.0/24. If you only want to accept single host addresses, reject any string containing a slash before you parse:
def valid_host_ip?(str)
return false if str.include?("/")
IPAddr.new(str)
true
rescue IPAddr::InvalidAddressError
false
end
IPv4 or IPv6? Ask the object
Once you have an IPAddr, ipv4? and ipv6? tell you the family:
ip = IPAddr.new("8.8.8.8")
ip.ipv4? # => true
ip.ipv6? # => false
v6 = IPAddr.new("2606:4700:4700::1111")
v6.ipv6? # => true
IPAddr normalizes as it parses, so IPAddr.new("2606:4700:4700:0:0:0:0:1111").to_s gives you the canonical 2606:4700:4700::1111 — handy when you're de-duplicating addresses that were written different ways.
Classify it: private, loopback, link-local
Ruby 2.6 (ipaddr 1.2.0) added range classification straight to the object. No lookup table required:
IPAddr.new("10.0.0.1").private? # => true
IPAddr.new("127.0.0.1").loopback? # => true
IPAddr.new("169.254.0.1").link_local? # => true
IPAddr.new("8.8.8.8").private? # => false
These cover the ranges you usually care about when deciding whether an address is routable on the public internet. If you want the reasoning behind those blocks, see what is a private IP.
If you're on a Ruby older than 2.6 the methods aren't there, but include? on a network gets you the same answer. Define the private ranges once and test membership:
PRIVATE_V4 = [
IPAddr.new("10.0.0.0/8"),
IPAddr.new("172.16.0.0/12"),
IPAddr.new("192.168.0.0/16")
].freeze
def private_ipv4?(str)
ip = IPAddr.new(str)
PRIVATE_V4.any? { |net| net.include?(ip) }
rescue IPAddr::InvalidAddressError
false
end
That's the whole thing. For a large share of real jobs — reject garbage input, branch on IPv4 vs IPv6, skip private addresses — the standard library is the entire answer, and you should ship exactly that.
Where an API earns its place
So when would you call an endpoint instead of IPAddr? Not for validation — that's solved locally, in-process, for free. The case for an HTTP call is consistency across a fleet: when your Ruby workers, a Go service, and a no-code automation all need to agree on what "private" or "bogon" means, one endpoint gives every client the same classification without each one reimplementing the range table and drifting.
That's what Boundstone's IP endpoint is — the same public/private/bogon classification you just wrote, exposed over HTTP as one system alongside phone and email checks:
curl -s https://api.boundstone.io/v1/verify/ip \
-H "Authorization: Bearer bs_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"ip":"8.8.8.8"}'
Every response tells you what it actually checked — and what it didn't:
{
"valid": true,
"version": 4,
"normalized": "8.8.8.8",
"classification": "public",
"is_public": true,
"is_bogon": false,
"checks": {
"performed": ["format", "version", "range_classification"],
"not_performed": ["geolocation", "asn", "hosting_datacenter", "proxy_vpn_tor", "reputation"]
}
}
Read that not_performed list and take it literally. Boundstone does not do IP geolocation, ASN lookup, hosting/datacenter detection, proxy/VPN/Tor detection, or reputation scoring. That intelligence layer needs licensed data and isn't shipped — so the API won't pretend a range check is a fraud verdict. What you get is a valid you can trust precisely because the response refuses to overstate itself. Want to try it without a key or a signup? The free IP validator runs the same classification in your browser.
The short version
- Validating? Use the stdlib.
IPAddr.new(str)inside arescue IPAddr::InvalidAddressErroris the whole validator. No gem. - Version:
ipv4?/ipv6?on the parsed object. - Classification:
private?,loopback?,link_local?on Ruby 2.6+;IPAddr.new("10.0.0.0/8").include?(ip)on older Rubies. - Reach for the API only when you want one shared classification contract across languages — not for something Ruby already does locally. Doing this in JavaScript instead? See validate an IP in Node.js.
- Trust the
not_performedlist. Geolocation and VPN detection aren't shipped; that honest scope is exactly why avalidmeans something.