diff --git a/CHANGELOG.md b/CHANGELOG.md
index dbd8e7ee..b00f10c5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,9 @@ CHANGELOG
4.3.0
------------------
+* Support for the GeoIP Anonymous Plus database has been added. To do a
+ lookup in this database, use the `anonymousPlus` method on
+ `DatabaseReader`.
* `getMetroCode` in the `Location` model has been deprecated. The code
values are no longer being maintained.
diff --git a/README.md b/README.md
index 9529be16..2ed94358 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,7 @@ To use the web service API, you must create a new `WebServiceClient` using the
`WebServiceClient.Builder`. You must provide the `Builder` constructor your
MaxMind `accountId` and `licenseKey`. To use the GeoLite2 web services instead
of GeoIP2, set the `host` method on the builder to `geolite.info`. To use
-the Sandbox GeoIP2 web services intead of the production GeoIP2 web
+the Sandbox GeoIP2 web services instead of the production GeoIP2 web
services, set the `host` method on the builder to `sandbox.maxmind.com`.
You may also set a `timeout` or set the `locales` fallback order using the
methods on the `Builder`. After you have created the `WebServiceClient`,
@@ -304,7 +304,31 @@ try (DatabaseReader reader = new DatabaseReader.Builder(database).build()) {
System.out.println(response.isResidentialProxy()); // false
System.out.println(response.isTorExitNode()); //true
}
+```
+
+### Anonymous Plus ###
+
+```java
+// A File object pointing to your GeoIP2 Anonymous Plus database
+File database = new File("/path/to/GeoIP-Anonymous-Plus.mmdb");
+
+// This creates the DatabaseReader object. To improve performance, reuse
+// the object across lookups. The object is thread-safe.
+try (DatabaseReader reader = new DatabaseReader.Builder(database).build()) {
+ InetAddress ipAddress = InetAddress.getByName("85.25.43.84");
+
+ AnonymousIpResponse response = reader.anonymousPlus(ipAddress);
+ System.out.println(response.getAnonymizerConfidence()); // 30
+ System.out.println(response.isAnonymous()); // true
+ System.out.println(response.isAnonymousVpn()); // false
+ System.out.println(response.isHostingProvider()); // false
+ System.out.println(response.isPublicProxy()); // false
+ System.out.println(response.isResidentialProxy()); // false
+ System.out.println(response.isTorExitNode()); // true
+ System.out.println(response.getNetworkLastSeen()); // "2025-04-14"
+ System.out.println(response.getProviderName()); // "FooBar VPN"
+}
```
### ASN ###
diff --git a/pom.xml b/pom.xml
index 16076e0e..02fd1701 100644
--- a/pom.xml
+++ b/pom.xml
@@ -47,6 +47,11 @@
jackson-databind
2.18.3
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ 2.18.3
+
com.fasterxml.jackson.core
jackson-core
diff --git a/src/main/java/com/maxmind/geoip2/DatabaseProvider.java b/src/main/java/com/maxmind/geoip2/DatabaseProvider.java
index 997229c7..c7c28d57 100644
--- a/src/main/java/com/maxmind/geoip2/DatabaseProvider.java
+++ b/src/main/java/com/maxmind/geoip2/DatabaseProvider.java
@@ -2,6 +2,7 @@
import com.maxmind.geoip2.exception.GeoIp2Exception;
import com.maxmind.geoip2.model.AnonymousIpResponse;
+import com.maxmind.geoip2.model.AnonymousPlusResponse;
import com.maxmind.geoip2.model.AsnResponse;
import com.maxmind.geoip2.model.CityResponse;
import com.maxmind.geoip2.model.ConnectionTypeResponse;
@@ -59,6 +60,27 @@ AnonymousIpResponse anonymousIp(InetAddress ipAddress) throws IOException,
Optional tryAnonymousIp(InetAddress ipAddress) throws IOException,
GeoIp2Exception;
+ /**
+ * Look up an IP address in a GeoIP2 Anonymous Plus.
+ *
+ * @param ipAddress IPv4 or IPv6 address to lookup.
+ * @return a AnonymousPlusResponse for the requested IP address.
+ * @throws com.maxmind.geoip2.exception.GeoIp2Exception if there is an error looking up the IP
+ * @throws java.io.IOException if there is an IO error
+ */
+ AnonymousPlusResponse anonymousPlus(InetAddress ipAddress) throws IOException,
+ GeoIp2Exception;
+
+ /**
+ * Look up an IP address in a GeoIP2 Anonymous Plus.
+ *
+ * @param ipAddress IPv4 or IPv6 address to lookup.
+ * @return a AnonymousPlusResponse for the requested IP address or empty if it is not in the DB.
+ * @throws com.maxmind.geoip2.exception.GeoIp2Exception if there is an error looking up the IP
+ * @throws java.io.IOException if there is an IO error
+ */
+ Optional tryAnonymousPlus(InetAddress ipAddress) throws IOException,
+ GeoIp2Exception;
/**
* Look up an IP address in a GeoIP2 IP Risk database.
diff --git a/src/main/java/com/maxmind/geoip2/DatabaseReader.java b/src/main/java/com/maxmind/geoip2/DatabaseReader.java
index e63549e4..0109e45a 100644
--- a/src/main/java/com/maxmind/geoip2/DatabaseReader.java
+++ b/src/main/java/com/maxmind/geoip2/DatabaseReader.java
@@ -10,6 +10,7 @@
import com.maxmind.geoip2.exception.AddressNotFoundException;
import com.maxmind.geoip2.exception.GeoIp2Exception;
import com.maxmind.geoip2.model.AnonymousIpResponse;
+import com.maxmind.geoip2.model.AnonymousPlusResponse;
import com.maxmind.geoip2.model.AsnResponse;
import com.maxmind.geoip2.model.CityResponse;
import com.maxmind.geoip2.model.ConnectionTypeResponse;
@@ -81,6 +82,7 @@ public class DatabaseReader implements DatabaseProvider, Closeable {
private enum DatabaseType {
ANONYMOUS_IP,
+ ANONYMOUS_PLUS,
ASN,
CITY,
CONNECTION_TYPE,
@@ -119,6 +121,9 @@ private int getDatabaseType() {
if (databaseType.contains("GeoIP2-Anonymous-IP")) {
type |= DatabaseType.ANONYMOUS_IP.type;
}
+ if (databaseType.contains("GeoIP-Anonymous-Plus")) {
+ type |= DatabaseType.ANONYMOUS_PLUS.type;
+ }
if (databaseType.contains("GeoIP2-IP-Risk")) {
type |= DatabaseType.IP_RISK.type;
}
@@ -427,6 +432,54 @@ private Optional getAnonymousIp(
);
}
+ /**
+ * Look up an IP address in a GeoIP2 Anonymous Plus.
+ *
+ * @param ipAddress IPv4 or IPv6 address to lookup.
+ * @return a AnonymousPlusResponse for the requested IP address.
+ * @throws GeoIp2Exception if there is an error looking up the IP
+ * @throws IOException if there is an IO error
+ */
+ @Override
+ public AnonymousPlusResponse anonymousPlus(InetAddress ipAddress) throws IOException,
+ GeoIp2Exception {
+ Optional r = getAnonymousPlus(ipAddress);
+ if (r.isEmpty()) {
+ throw new AddressNotFoundException("The address "
+ + ipAddress.getHostAddress() + " is not in the database.");
+ }
+ return r.get();
+ }
+
+ @Override
+ public Optional tryAnonymousPlus(InetAddress ipAddress)
+ throws IOException,
+ GeoIp2Exception {
+ return getAnonymousPlus(ipAddress);
+ }
+
+ private Optional getAnonymousPlus(
+ InetAddress ipAddress
+ ) throws IOException, GeoIp2Exception {
+ LookupResult result = this.get(
+ ipAddress,
+ AnonymousPlusResponse.class,
+ DatabaseType.ANONYMOUS_PLUS
+ );
+ AnonymousPlusResponse response = result.getModel();
+ if (response == null) {
+ return Optional.empty();
+ }
+ return Optional.of(
+ new AnonymousPlusResponse(
+ response,
+ result.getIpAddress(),
+ result.getNetwork()
+ )
+ );
+ }
+
+
/**
* Look up an IP address in a GeoIP2 IP Risk database.
*
diff --git a/src/main/java/com/maxmind/geoip2/model/AbstractResponse.java b/src/main/java/com/maxmind/geoip2/model/AbstractResponse.java
index 6c8f1178..6c742f1b 100644
--- a/src/main/java/com/maxmind/geoip2/model/AbstractResponse.java
+++ b/src/main/java/com/maxmind/geoip2/model/AbstractResponse.java
@@ -3,6 +3,7 @@
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.io.IOException;
/**
@@ -18,6 +19,7 @@ public abstract class AbstractResponse {
public String toJson() throws IOException {
JsonMapper mapper = JsonMapper.builder()
.disable(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS)
+ .addModule(new JavaTimeModule())
.serializationInclusion(Include.NON_NULL)
.serializationInclusion(Include.NON_EMPTY)
.build();
diff --git a/src/main/java/com/maxmind/geoip2/model/AnonymousPlusResponse.java b/src/main/java/com/maxmind/geoip2/model/AnonymousPlusResponse.java
new file mode 100644
index 00000000..7397c36a
--- /dev/null
+++ b/src/main/java/com/maxmind/geoip2/model/AnonymousPlusResponse.java
@@ -0,0 +1,145 @@
+package com.maxmind.geoip2.model;
+
+import com.fasterxml.jackson.annotation.JacksonInject;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.maxmind.db.MaxMindDbConstructor;
+import com.maxmind.db.MaxMindDbParameter;
+import com.maxmind.db.Network;
+import com.maxmind.geoip2.NetworkDeserializer;
+import java.time.LocalDate;
+
+/**
+ * This class provides the GeoIP Anonymous Plus model.
+ */
+public class AnonymousPlusResponse extends AnonymousIpResponse {
+ private final Integer anonymizerConfidence;
+ private final LocalDate networkLastSeen;
+ private final String providerName;
+
+ /**
+ * Constructs an instance of {@code AnonymousPlusResponse} with the specified values.
+ *
+ * @param anonymizerConfidence confidence that the network is a VPN.
+ * @param ipAddress the IP address being checked
+ * @param isAnonymous whether the IP address belongs to any sort of anonymous network
+ * @param isAnonymousVpn whether the IP address belongs to an anonymous VPN system
+ * @param isHostingProvider whether the IP address belongs to a hosting provider
+ * @param isPublicProxy whether the IP address belongs to a public proxy system
+ * @param isResidentialProxy whether the IP address belongs to a residential proxy system
+ * @param isTorExitNode whether the IP address is a Tor exit node
+ * @param network the network associated with the record
+ * @param networkLastSeen the last sighting of the network.
+ * @param providerName the name of the VPN provider.
+ */
+ public AnonymousPlusResponse(
+ @JsonProperty("anonymizer_confidence") Integer anonymizerConfidence,
+ @JacksonInject("ip_address") @JsonProperty("ip_address") String ipAddress,
+ @JsonProperty("is_anonymous") Boolean isAnonymous,
+ @JsonProperty("is_anonymous_vpn") Boolean isAnonymousVpn,
+ @JsonProperty("is_hosting_provider") Boolean isHostingProvider,
+ @JsonProperty("is_public_proxy") Boolean isPublicProxy,
+ @JsonProperty("is_residential_proxy") Boolean isResidentialProxy,
+ @JsonProperty("is_tor_exit_node") Boolean isTorExitNode,
+ @JacksonInject("network") @JsonDeserialize(using = NetworkDeserializer.class)
+ @JsonProperty("network") Network network,
+ @JsonProperty("network_last_seen") LocalDate networkLastSeen,
+ @JsonProperty("provider_name") String providerName
+ ) {
+ super(ipAddress, isAnonymous, isAnonymousVpn, isHostingProvider, isPublicProxy,
+ isResidentialProxy, isTorExitNode, network);
+
+ this.anonymizerConfidence = anonymizerConfidence;
+ this.networkLastSeen = networkLastSeen;
+ this.providerName = providerName;
+ }
+
+ /**
+ * Constructs an instance of {@code AnonymousPlusResponse} with the specified values.
+ *
+ * @param anonymizerConfidence confidence that the network is a VPN.
+ * @param ipAddress the IP address being checked
+ * @param isAnonymous whether the IP address belongs to any sort of anonymous network
+ * @param isAnonymousVpn whether the IP address belongs to an anonymous VPN system
+ * @param isHostingProvider whether the IP address belongs to a hosting provider
+ * @param isPublicProxy whether the IP address belongs to a public proxy system
+ * @param isResidentialProxy whether the IP address belongs to a residential proxy system
+ * @param isTorExitNode whether the IP address is a Tor exit node
+ * @param network the network associated with the record
+ * @param networkLastSeen the last sighting of the network.
+ * @param providerName the name of the VPN provider.
+ */
+ @MaxMindDbConstructor
+ public AnonymousPlusResponse(
+ @MaxMindDbParameter(name = "anonymizer_confidence") Integer anonymizerConfidence,
+ @MaxMindDbParameter(name = "ip_address") String ipAddress,
+ @MaxMindDbParameter(name = "is_anonymous") Boolean isAnonymous,
+ @MaxMindDbParameter(name = "is_anonymous_vpn") Boolean isAnonymousVpn,
+ @MaxMindDbParameter(name = "is_hosting_provider") Boolean isHostingProvider,
+ @MaxMindDbParameter(name = "is_public_proxy") Boolean isPublicProxy,
+ @MaxMindDbParameter(name = "is_residential_proxy") Boolean isResidentialProxy,
+ @MaxMindDbParameter(name = "is_tor_exit_node") Boolean isTorExitNode,
+ @MaxMindDbParameter(name = "network") Network network,
+ @MaxMindDbParameter(name = "network_last_seen") String networkLastSeen,
+ @MaxMindDbParameter(name = "provider_name") String providerName
+ ) {
+ this(anonymizerConfidence, ipAddress, isAnonymous, isAnonymousVpn,
+ isHostingProvider, isPublicProxy, isResidentialProxy, isTorExitNode, network,
+ networkLastSeen != null ? LocalDate.parse(networkLastSeen) : null,
+ providerName);
+ }
+
+ /**
+ * Constructs an instance of {@code AnonymousPlusResponse} from the values in the
+ * response and the specified IP address and network.
+ *
+ * @param response the response to copy
+ * @param ipAddress the IP address being checked
+ * @param network the network associated with the record
+ */
+ public AnonymousPlusResponse(
+ AnonymousPlusResponse response,
+ String ipAddress,
+ Network network
+ ) {
+ this(
+ response.getAnonymizerConfidence(),
+ ipAddress,
+ response.isAnonymous(),
+ response.isAnonymousVpn(),
+ response.isHostingProvider(),
+ response.isPublicProxy(),
+ response.isResidentialProxy(),
+ response.isTorExitNode(),
+ network,
+ response.getNetworkLastSeen(),
+ response.getProviderName()
+ );
+ }
+
+ /**
+ * @return A score ranging from 1 to 99 that is our percent confidence that the network is
+ * currently part of an actively used VPN service.
+ */
+ @JsonProperty
+ public Integer getAnonymizerConfidence() {
+ return anonymizerConfidence;
+ }
+
+ /**
+ * @return The last day that the network was sighted in our analysis of anonymized networks.
+ */
+ @JsonProperty
+ public LocalDate getNetworkLastSeen() {
+ return networkLastSeen;
+ }
+
+ /**
+ * @return The name of the VPN provider (e.g., NordVPN, SurfShark, etc.) associated with the
+ * network.
+ */
+ @JsonProperty
+ public String getProviderName() {
+ return providerName;
+ }
+}
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 7020345e..fea2e6f6 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -2,6 +2,7 @@
module com.maxmind.geoip2 {
requires com.fasterxml.jackson.annotation;
requires com.fasterxml.jackson.databind;
+ requires com.fasterxml.jackson.datatype.jsr310;
requires transitive com.maxmind.db;
requires java.net.http;
diff --git a/src/test/java/com/maxmind/geoip2/DatabaseReaderTest.java b/src/test/java/com/maxmind/geoip2/DatabaseReaderTest.java
index b38e1826..e70b3e05 100644
--- a/src/test/java/com/maxmind/geoip2/DatabaseReaderTest.java
+++ b/src/test/java/com/maxmind/geoip2/DatabaseReaderTest.java
@@ -11,6 +11,7 @@
import com.maxmind.geoip2.exception.AddressNotFoundException;
import com.maxmind.geoip2.exception.GeoIp2Exception;
import com.maxmind.geoip2.model.AnonymousIpResponse;
+import com.maxmind.geoip2.model.AnonymousPlusResponse;
import com.maxmind.geoip2.model.AsnResponse;
import com.maxmind.geoip2.model.CityResponse;
import com.maxmind.geoip2.model.ConnectionTypeResponse;
@@ -227,6 +228,30 @@ public void testAnonymousIp() throws Exception {
}
}
+ @Test
+ public void testAnonymousPlus() throws Exception {
+ try (DatabaseReader reader = new DatabaseReader.Builder(
+ this.getFile("GeoIP-Anonymous-Plus-Test.mmdb")).build()
+ ) {
+ InetAddress ipAddress = InetAddress.getByName("1.2.0.1");
+ AnonymousPlusResponse response = reader.anonymousPlus(ipAddress);
+ assertEquals(30, response.getAnonymizerConfidence());
+ assertTrue(response.isAnonymous());
+ assertTrue(response.isAnonymousVpn());
+ assertFalse(response.isHostingProvider());
+ assertFalse(response.isPublicProxy());
+ assertFalse(response.isResidentialProxy());
+ assertFalse(response.isTorExitNode());
+ assertEquals(ipAddress.getHostAddress(), response.getIpAddress());
+ assertEquals("1.2.0.1/32", response.getNetwork().toString());
+ assertEquals("2025-04-14", response.getNetworkLastSeen().toString());
+ assertEquals("foo", response.getProviderName());
+
+ AnonymousPlusResponse tryResponse = reader.tryAnonymousPlus(ipAddress).get();
+ assertEquals(response.toJson(), tryResponse.toJson());
+ }
+ }
+
@Test
public void testAnonymousIpIsResidentialProxy() throws Exception {
try (DatabaseReader reader = new DatabaseReader.Builder(
diff --git a/src/test/resources/maxmind-db b/src/test/resources/maxmind-db
index 1271107c..f387805e 160000
--- a/src/test/resources/maxmind-db
+++ b/src/test/resources/maxmind-db
@@ -1 +1 @@
-Subproject commit 1271107ccad72c320bc7dc8aefd767cba550101a
+Subproject commit f387805e4af30036ec4245c9069ac1f2fdae4953