diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java
index a5c6506391aa82cd5572dfb2f87f3a96a5b48698..d7718890eae9fe5ef8c1603e7d1caaa755feca04 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java
@@ -25,6 +25,7 @@ import net.i2p.app.ClientAppManager;
 import net.i2p.app.Outproxy;
 import net.i2p.client.I2PSession;
 import net.i2p.client.LookupResult;
+import net.i2p.client.naming.NamingService;
 import net.i2p.client.streaming.I2PSocket;
 import net.i2p.client.streaming.I2PSocketManager;
 import net.i2p.client.streaming.I2PSocketOptions;
@@ -664,7 +665,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
                             host = null;
                             break;
                         }
-                    } else if(hostLowerCase.endsWith(".i2p")) {
+                    } else if(NamingService.isI2PHost(hostLowerCase)) {
                         // Destination gets the hostname
                         destination = host;
                         // Host becomes the destination's "{b32}.b32.i2p" string, or "i2p" on lookup failure
@@ -1279,7 +1280,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
                 }
             } else if("i2p".equals(host)) {
                 clientDest = null;
-            } else if (destination.toLowerCase(Locale.US).endsWith(".b32.i2p")) {
+            } else if (NamingService.isB32Host(destination)) {
                 int len = destination.length();
                 if (len < 60 || (len >= 61 && len <= 63)) {
                     // 8-59 or 61-63 chars, this won't work
@@ -1289,7 +1290,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
                     } catch (IOException ioe) {}
                     return;
                 }
-                if (len >= 64) {
+                if (NamingService.isBlindedHost(destination)) {
                     // catch b33 errors before session lookup
                     try {
                         BlindData bd = Blinding.decode(_context, destination);
@@ -1363,7 +1364,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
                     header = getErrorPage("dnfp", ERR_DESTINATION_UNKNOWN);
                 } else if(ahelperPresent) {
                     header = getErrorPage("dnfb", ERR_DESTINATION_UNKNOWN);
-                } else if(destination.length() >= 60 && destination.toLowerCase(Locale.US).endsWith(".b32.i2p")) {
+                } else if (NamingService.isB32Host(destination)) {
                     header = getErrorPage("nols", ERR_DESTINATION_UNKNOWN);
                     extraMessage = _t("Destination lease set not found");
                 } else {
@@ -1728,7 +1729,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
         if(host == null) {
             return null;
         }
-        if (host.toLowerCase(Locale.US).endsWith(".b32.i2p")) {
+        if (NamingService.isB32Host(host)) {
             return host;
         }
         Destination dest = _context.namingService().lookup(host);
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java
index a9f3b2179f4654dca290e6a3e446b29da1c9a963..96e02bf16398be00ee520644c7cb505dfcd87bf7 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java
@@ -16,6 +16,7 @@ import java.util.Map;
 import java.util.Properties;
 import java.util.StringTokenizer;
 
+import net.i2p.client.naming.NamingService;
 import net.i2p.client.streaming.I2PSocket;
 import net.i2p.client.streaming.I2PSocketOptions;
 import net.i2p.data.Destination;
@@ -134,7 +135,7 @@ public class I2PSOCKSTunnel extends I2PTunnelClientBase {
                int colon = proxy.indexOf(':');
                if (colon > 0)
                    host = host.substring(0, colon);
-               if (host.endsWith(".i2p")) {
+               if (NamingService.isI2PHost(host)) {
                    proxyList.add(proxy);
                } else {
                    String m = "Non-i2p SOCKS outproxy: " + proxy;
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS4aServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS4aServer.java
index f3496d77832d1060ca110e3c75754248c0add2b2..ef7d0b6cc4655e20a1ce79dc7e4d5373bdf68227 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS4aServer.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS4aServer.java
@@ -21,6 +21,7 @@ import net.i2p.I2PException;
 import net.i2p.app.ClientApp;
 import net.i2p.app.ClientAppManager;
 import net.i2p.app.Outproxy;
+import net.i2p.client.naming.NamingService;
 import net.i2p.client.streaming.I2PSocket;
 import net.i2p.client.streaming.I2PSocketOptions;
 import net.i2p.data.DataFormatException;
@@ -211,7 +212,7 @@ class SOCKS4aServer extends SOCKSServer {
 
         try {
             String hostLowerCase = connHostName.toLowerCase(Locale.US);
-            if (hostLowerCase.endsWith(".i2p")) {
+            if (NamingService.isI2PHost(hostLowerCase)) {
                 Destination dest = _context.namingService().lookup(connHostName);
                 if (dest == null) {
                     try {
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java
index 7a35e77db5da4ce1d720f5a76fabf3db461fddac..71e6841a7ec3e2a651e69c35630e4afc28792111 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java
@@ -26,6 +26,7 @@ import net.i2p.I2PException;
 import net.i2p.app.ClientApp;
 import net.i2p.app.ClientAppManager;
 import net.i2p.app.Outproxy;
+import net.i2p.client.naming.NamingService;
 import net.i2p.client.streaming.I2PSocket;
 import net.i2p.client.streaming.I2PSocketOptions;
 import net.i2p.data.DataFormatException;
@@ -459,7 +460,7 @@ class SOCKS5Server extends SOCKSServer {
 
         try {
             String hostLowerCase = connHostName.toLowerCase(Locale.US);
-            if (hostLowerCase.endsWith(".i2p")) {
+            if (NamingService.isI2PHost(hostLowerCase)) {
                 // Let's not do a new Dest for every request, huh?
                 //I2PSocketManager sm = I2PSocketManagerFactory.createManager();
                 //destSock = sm.connect(I2PTunnel.destFromName(connHostName), null);
diff --git a/core/java/src/net/i2p/client/naming/NamingService.java b/core/java/src/net/i2p/client/naming/NamingService.java
index c584dcb28cb4bcfb41642a69dacd86c61ca2f89c..0479aba18270a0ac1eb27fbedbd854c91df60815 100644
--- a/core/java/src/net/i2p/client/naming/NamingService.java
+++ b/core/java/src/net/i2p/client/naming/NamingService.java
@@ -13,6 +13,7 @@ import java.lang.reflect.Constructor;
 import java.util.Collections;
 import java.util.Date;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
@@ -739,6 +740,61 @@ public abstract class NamingService {
 
     //// End new API for multiple Destinations
 
+    /**
+     * Is this an i2p hostname?
+     *
+     * ref: https://www.rfc-editor.org/rfc/rfc9476.html
+     *
+     * @param hostname non-null
+     * @return true if hostname (case insensitive) ends with .i2p or .i2p.alt
+     * @since 0.9.66
+     */
+    public static boolean isI2PHost(String hostname) {
+        String lc = hostname.toLowerCase(Locale.US);
+        return lc.endsWith(".i2p") || lc.endsWith(".i2p.alt");
+    }
+
+    /**
+     * Is this a b32 hostname?
+     * Does NOT validate the base32 part except for length.
+     *
+     * ref: https://www.rfc-editor.org/rfc/rfc9476.html
+     *
+     * @param hostname non-null
+     * @return true if hostname (case insensitive) ends with .b32.i2p or .b32.i2p.alt
+     *              and is long enough. May or may not be blinded.
+     * @since 0.9.66
+     */
+    public static boolean isB32Host(String hostname) {
+        int len = hostname.length();
+        if (len < 60)
+            return false;
+        String lc = hostname.toLowerCase(Locale.US);
+        return lc.endsWith(".b32.i2p") ||
+               (len >= 64 && lc.endsWith(".b32.i2p.alt"));
+    }
+
+    /**
+     * Is this a blinded ("b33") hostname?
+     * Does NOT validate the base32 part except for length.
+     * See Blinding.decode() for full validation.
+     *
+     * ref: https://www.rfc-editor.org/rfc/rfc9476.html
+     *
+     * @param hostname non-null
+     * @return true if hostname (case insensitive) ends with .b32.i2p or .b32.i2p.alt
+     *              and is long enough.
+     * @since 0.9.66
+     */
+    public static boolean isBlindedHost(String hostname) {
+        int len = hostname.length();
+        if (len < 64)
+            return false;
+        String lc = hostname.toLowerCase(Locale.US);
+        return lc.endsWith(".b32.i2p") ||
+               (len >= 68 && lc.endsWith(".b32.i2p.alt"));
+    }
+
     /**
      * WARNING - for use by I2PAppContext only - others must use
      * I2PAppContext.namingService()