← Blog

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.

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 a rescue IPAddr::InvalidAddressError is 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_performed list. Geolocation and VPN detection aren't shipped; that honest scope is exactly why a valid means something.

Frequently asked questions

How do I check if a string is a valid IP address in Ruby?

Ruby's standard library includes the IPAddr class, so you can call IPAddr.new(str) and rescue IPAddr::InvalidAddressError to get a true or false result for both IPv4 and IPv6. That confirms the address is well-formed and tells you the version, but on its own it does not classify the address or tell you anything about who owns it. If you want format plus range classification (public, private, loopback, reserved) in a single call without maintaining your own range tables, Boundstone's IP endpoint returns valid, version, classification, and whether the address is publicly routable.

What does a 'valid' IP address result actually prove?

A valid result means the string is a correctly formatted IPv4 or IPv6 address, and alongside it you get the version and a range classification such as public, private, loopback, or reserved, plus whether the address is publicly routable. It deliberately does not prove geolocation, the owning network or ASN, whether the IP belongs to a hosting or datacenter provider, or whether it is a proxy, VPN, or Tor exit node. Boundstone lists all of those under checks.not_performed on every response instead of guessing, so you always know exactly what was and was not checked.

Can I validate a large list of IP addresses at once?

Yes. In pure Ruby you can loop a list through IPAddr, and for larger jobs Boundstone offers a bulk endpoint at /v1/bulk/ip that accepts a CSV of addresses. The free tier handles up to 250 rows per job and paid plans allow 10,000, with each row reserving one credit and any row that errors automatically refunded. Results come back as a CSV carrying the valid flag, version, and range classification for every address.

Thomas Tsui

Founder of Boundstone — building phone, email and IP validation you can actually verify.

One honest API for email, phone and IP — every response lists what it checked and what it didn't claim to. Free tier: 250 credits/month, no card, credits never expire.

More from Boundstone — API documentation · Benchmark methodology · The benchmark series · Buyer's checklist