# How to validate an IP address in Ruby

_2026-09-23 · Boundstone (https://boundstone.io/blog/validate-ip-ruby)_


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:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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](/blog/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:

```ruby
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:

```bash
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:

```json
{
  "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](/tools/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](/blog/validate-ip-nodejs).
- **Trust the `not_performed` list.** Geolocation and VPN detection aren't shipped; that honest scope is exactly why a `valid` means something.
