Validate an IP address in Java without a DNS lookup
Guava's InetAddresses validates and classifies an address in memory — the real trick is knowing why InetAddress.getByName() is the wrong tool for validation.
Contents
You have a string. Maybe it came from a form field, an X-Forwarded-For header, or a config file. Before you store it, route on it, or drop it into a firewall rule, you want to answer one question: is this actually an IP address? To validate an IP address in Java sounds like a solved problem — and it nearly is, as long as you pick the method that answers the question you're asking instead of the one that quietly makes a network call.
The one-line answer: InetAddresses.isInetAddress
Google Guava ships a utility that does exactly this and nothing more.
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.4.0-jre</version>
</dependency>
import com.google.common.net.InetAddresses;
boolean ok = InetAddresses.isInetAddress("8.8.8.8"); // true
boolean v6 = InetAddresses.isInetAddress("2001:db8::1"); // true
boolean nope = InetAddresses.isInetAddress("999.1.1.1"); // false
boolean host = InetAddresses.isInetAddress("example.com"); // false
isInetAddress returns true only for a valid IPv4 or IPv6 string literal. It never resolves anything. A hostname is not an IP literal, so it returns false — which is the correct answer to "is this an IP address." For most callers, this single method is the whole job. If all you need is a yes/no, stop here.
The getByName trap
Search results will often point you at java.net.InetAddress.getByName(String). For validation it is the wrong tool, and the reason is the important part.
import java.net.InetAddress;
// Looks like validation. Isn't.
InetAddress addr = InetAddress.getByName(userInput);
Pass getByName an IP literal and it parses it in memory. Pass it a hostname — or anything that isn't a literal — and it attempts a DNS lookup. Since userInput is exactly the thing you haven't trusted yet, you've turned a validation check into a network round trip driven by input you don't control: latency you didn't budget for, a blocking call on your request thread, and a resolver query for whatever string someone typed. getByName also throws UnknownHostException when resolution fails, so you end up catching an exception to answer a boolean question.
Guava's isInetAddress and forString never touch the network. That's the property you want: the answer depends only on the string in front of you.
Classify the address locally
Validation is usually step one; step two is "what kind of address is this?" You rarely want to trust a 10.x private address or a 127.0.0.1 loopback the way you'd trust a public one. Parse the literal with InetAddresses.forString — again, no DNS — and read the classification off the resulting InetAddress.
import com.google.common.net.InetAddresses;
import java.net.InetAddress;
public final class IpCheck {
public static String classify(String s) {
if (!InetAddresses.isInetAddress(s)) {
return "invalid";
}
InetAddress ip = InetAddresses.forString(s); // parses only, no lookup
if (ip.isAnyLocalAddress()) return "unspecified"; // 0.0.0.0, ::
if (ip.isLoopbackAddress()) return "loopback"; // 127.0.0.0/8, ::1
if (ip.isLinkLocalAddress()) return "link_local"; // 169.254/16, fe80::/10
if (ip.isSiteLocalAddress()) return "private"; // 10/8, 172.16/12, 192.168/16
if (ip.isMulticastAddress()) return "multicast"; // 224/4, ff00::/8
return "public";
}
}
forString throws IllegalArgumentException on a bad literal, which is why the guard call to isInetAddress comes first. Everything after it is arithmetic on the address bytes — no sockets open, no resolver runs.
Where the standard library stops
The InetAddress predicates cover the ranges most apps care about, but they are not a complete map of special-use address space. Two gaps worth knowing:
isSiteLocalAddress()recognizes the IPv4 private ranges, but it does not flag IPv6 unique local addresses (fc00::/7). Anfd00::address falls straight through topublicabove.- There is no built-in predicate for the documentation ranges (
192.0.2.0/24,2001:db8::/32), the shared CGNAT block (100.64.0.0/10), or a general "bogon" check. You'd hand-roll those against the byte array.
None of this is a knock on Java — it's just the line where "parse and bucket locally" ends. If you want one classification that already includes those ranges, and the same answer whether the call comes from Java, Python, or a shell script, that's the layer to reach for next. If the private-vs-public split is where you're headed, what counts as a private IP walks the ranges.
The same classification over HTTP
Boundstone's IP endpoint does the parse-and-classify step above as a service, so every client in your stack reads the same field names. It's the same category of work — pure inspection of the address — exposed over one contract.
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"}'
{
"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"]
}
}
Every response carries that checks block, and the honest half is not_performed. Boundstone does not do IP geolocation, ASN lookup, hosting/datacenter detection, proxy/VPN/Tor detection, or reputation scoring — that layer needs licensed data and isn't shipped. So classification: "public" means precisely what it says: the address is a routable, non-reserved literal. It does not mean "not a VPN." When you get a valid, the not_performed list tells you exactly what that word did and didn't cost you.
Call it from Java with the built-in HTTP client:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.boundstone.io/v1/verify/ip"))
.header("Authorization", "Bearer bs_live_YOUR_KEY")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"ip\":\"8.8.8.8\"}"))
.build();
HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
No key required to try it — the keyless IP validator runs the same classification, no signup. Doing this in Python instead? See validate an IP address in Python.
The short version
- To validate in Java, use
InetAddresses.isInetAddress(s)— a pure yes/no with no DNS lookup. - Avoid
InetAddress.getByName()for validation: untrusted input can trigger a real DNS query. - To classify, guard with
isInetAddress, then callInetAddresses.forString(s)and readisLoopbackAddress(),isSiteLocalAddress(), and friends — all in memory. - The standard predicates skip IPv6 ULA (
fc00::/7), documentation, and CGNAT ranges; fill those in yourself or use the API'srange_classification. - Boundstone gives you that classification over HTTP and tells you, on every call, that geolocation and proxy/VPN detection are
not_performed. Avalidyou can trust is one that shows its work.