diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java
index 614bc1f7436662ec38db5c08a2008898bf896a67..955d3abd1e82afb47427608c7176579a7a608ea5 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java
@@ -14,6 +14,7 @@ import net.i2p.I2PException;
 import net.i2p.client.I2PClient;
 import net.i2p.client.I2PClientFactory;
 import net.i2p.client.I2PSession;
+import net.i2p.data.Base32;
 import net.i2p.data.Destination;
 import net.i2p.util.I2PThread;
 import net.i2p.util.Log;
@@ -361,6 +362,19 @@ public class TunnelController implements Logging {
         return null;
     }
     
+    public String getMyDestHashBase32() {
+        if (_tunnel != null) {
+            List sessions = _tunnel.getSessions();
+            for (int i = 0; i < sessions.size(); i++) {
+                I2PSession session = (I2PSession)sessions.get(i);
+                Destination dest = session.getMyDestination();
+                if (dest != null)
+                    return Base32.encode(dest.calculateHash().getData());
+            }
+        }
+        return null;
+    }
+    
     public boolean getIsRunning() { return _running; }
     public boolean getIsStarting() { return _starting; }
     
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
index c8d321ea24589c1046b1a5cfa87e90df44f0a834..8c361195add42d262599b83dd731901b95c13538 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
@@ -437,6 +437,19 @@ public class IndexBean {
         }
     }
     
+    public String getDestHashBase32(int tunnel) {
+        TunnelController tun = getController(tunnel);
+        if (tun != null) {
+            String rv = tun.getMyDestHashBase32();
+            if (rv != null)
+                return rv;
+            else
+                return "";
+        } else {
+            return "";
+        }
+    }
+    
     ///
     /// bean props for form submission
     ///
diff --git a/apps/i2ptunnel/jsp/index.jsp b/apps/i2ptunnel/jsp/index.jsp
index bcec39c17759788b4fe368322a1bfdf583035d0a..cc68096ad59cbef7d6d22e6e96df10f5854bd40e 100644
--- a/apps/i2ptunnel/jsp/index.jsp
+++ b/apps/i2ptunnel/jsp/index.jsp
@@ -180,13 +180,23 @@
         </div>
         <div class="targetField rowItem">
             <label>Points at:</label>
-            <span class="text"><%=indexBean.getServerTarget(curServer)%></span>
+            <span class="text">
+        <%
+            if ("httpserver".equals(indexBean.getInternalType(curServer))) {
+          %>
+            <a href="http://<%=indexBean.getServerTarget(curServer)%>/" title="Test HTTP server, bypassing I2P"><%=indexBean.getServerTarget(curServer)%></a>
+        <%
+            } else {
+          %><%=indexBean.getServerTarget(curServer)%>
+        <%
+            }
+          %></span>
         </div>
         <div class="previewField rowItem">
             <%
             if ("httpserver".equals(indexBean.getInternalType(curServer)) && indexBean.getTunnelStatus(curServer) == IndexBean.RUNNING) {
           %><label>Preview:</label>    
-            <a class="control" title="Preview this Tunnel" href="http://<%=(new java.util.Random()).nextLong()%>.i2p/?i2paddresshelper=<%=indexBean.getDestinationBase64(curServer)%>" target="_new">Preview</a>     
+            <a class="control" title="Test HTTP server through I2P" href="http://<%=indexBean.getDestHashBase32(curServer)%>.i2p">Preview</a>     
         <%
             } else {
           %><span class="comment">No Preview</span>
diff --git a/core/java/src/net/i2p/client/DestReplyMessageHandler.java b/core/java/src/net/i2p/client/DestReplyMessageHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..25699ad02412c2d06550d02cf78623c2e8ff8389
--- /dev/null
+++ b/core/java/src/net/i2p/client/DestReplyMessageHandler.java
@@ -0,0 +1,25 @@
+package net.i2p.client;
+
+/*
+ * Released into the public domain 
+ * with no warranty of any kind, either expressed or implied.  
+ */
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.i2cp.I2CPMessage;
+import net.i2p.data.i2cp.DestReplyMessage;
+
+/**
+ * Handle I2CP dest replies from the router
+ */
+class DestReplyMessageHandler extends HandlerImpl {
+    public DestReplyMessageHandler(I2PAppContext ctx) {
+        super(ctx, DestReplyMessage.MESSAGE_TYPE);
+    }
+    
+    public void handleMessage(I2CPMessage message, I2PSessionImpl session) {
+        _log.debug("Handle message " + message);
+        DestReplyMessage msg = (DestReplyMessage) message;
+       ((I2PSimpleSession)session).destReceived(msg.getDestination());
+    }
+}
diff --git a/core/java/src/net/i2p/client/I2PClientMessageHandlerMap.java b/core/java/src/net/i2p/client/I2PClientMessageHandlerMap.java
index 50b7955719f244923cbf3a21bf03c7d08423c39d..6f0d950514db09e3187743e67d87b2c8006b5a5e 100644
--- a/core/java/src/net/i2p/client/I2PClientMessageHandlerMap.java
+++ b/core/java/src/net/i2p/client/I2PClientMessageHandlerMap.java
@@ -16,7 +16,6 @@ import net.i2p.data.i2cp.MessageStatusMessage;
 import net.i2p.data.i2cp.RequestLeaseSetMessage;
 import net.i2p.data.i2cp.SessionStatusMessage;
 import net.i2p.data.i2cp.SetDateMessage;
-import net.i2p.util.Log;
 
 /**
  * Contains a map of message handlers that a session will want to use
@@ -24,9 +23,11 @@ import net.i2p.util.Log;
  * @author jrandom
  */
 class I2PClientMessageHandlerMap {
-    private final static Log _log = new Log(I2PClientMessageHandlerMap.class);
     /** map of message type id --> I2CPMessageHandler */
-    private I2CPMessageHandler _handlers[];
+    protected I2CPMessageHandler _handlers[];
+
+    /** for extension */
+    public I2PClientMessageHandlerMap() {}
 
     public I2PClientMessageHandlerMap(I2PAppContext context) {
         int highest = DisconnectMessage.MESSAGE_TYPE;
@@ -49,4 +50,4 @@ class I2PClientMessageHandlerMap {
         if ( (messageTypeId < 0) || (messageTypeId >= _handlers.length) ) return null;
         return _handlers[messageTypeId];
     }
-}
\ No newline at end of file
+}
diff --git a/core/java/src/net/i2p/client/I2PSession.java b/core/java/src/net/i2p/client/I2PSession.java
index 9d053ef5d8f88f2c6398525da054d7acffffd25c..627d1775a5f6e7943428e43346c9be5a0ca640ee 100644
--- a/core/java/src/net/i2p/client/I2PSession.java
+++ b/core/java/src/net/i2p/client/I2PSession.java
@@ -12,6 +12,7 @@ package net.i2p.client;
 import java.util.Set;
 
 import net.i2p.data.Destination;
+import net.i2p.data.Hash;
 import net.i2p.data.PrivateKey;
 import net.i2p.data.SessionKey;
 import net.i2p.data.SigningPrivateKey;
@@ -126,4 +127,10 @@ public interface I2PSession {
      * Retrieve the signing SigningPrivateKey associated with the Destination
      */
     public SigningPrivateKey getPrivateKey();
+
+    /**
+     * Lookup up a Hash
+     *
+     */
+    public Destination lookupDest(Hash h) throws I2PSessionException;
 }
diff --git a/core/java/src/net/i2p/client/I2PSessionImpl.java b/core/java/src/net/i2p/client/I2PSessionImpl.java
index 6b62513c5b1c651675ff9316c60ec9e32b599855..78f4ba763c4682b38678d9f4016cc60d516718eb 100644
--- a/core/java/src/net/i2p/client/I2PSessionImpl.java
+++ b/core/java/src/net/i2p/client/I2PSessionImpl.java
@@ -26,6 +26,7 @@ import java.util.Set;
 import net.i2p.I2PAppContext;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.Destination;
+import net.i2p.data.Hash;
 import net.i2p.data.LeaseSet;
 import net.i2p.data.PrivateKey;
 import net.i2p.data.SessionKey;
@@ -48,7 +49,7 @@ import net.i2p.util.SimpleTimer;
  * @author jrandom
  */
 abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessageEventListener {
-    private Log _log;
+    protected Log _log;
     /** who we are */
     private Destination _myDestination;
     /** private key for decryption */
@@ -63,15 +64,15 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
     private LeaseSet _leaseSet;
 
     /** hostname of router */
-    private String _hostname;
+    protected String _hostname;
     /** port num to router */
-    private int _portNum;
+    protected int _portNum;
     /** socket for comm */
-    private Socket _socket;
+    protected Socket _socket;
     /** reader that always searches for messages */
-    private I2CPMessageReader _reader;
+    protected I2CPMessageReader _reader;
     /** where we pipe our messages */
-    private OutputStream _out;
+    protected OutputStream _out;
 
     /** who we send events to */
     private I2PSessionListener _sessionListener;
@@ -90,10 +91,10 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
     private Object _leaseSetWait = new Object();
 
     /** whether the session connection has already been closed (or not yet opened) */
-    private boolean _closed;
+    protected boolean _closed;
 
     /** whether the session connection is in the process of being closed */
-    private boolean _closing;
+    protected boolean _closing;
 
     /** have we received the current date from the router yet? */
     private boolean _dateReceived;
@@ -106,7 +107,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
      * reading of other messages (in turn, potentially leading to deadlock)
      *
      */
-    private AvailabilityNotifier _availabilityNotifier;
+    protected AvailabilityNotifier _availabilityNotifier;
 
     void dateUpdated() {
         _dateReceived = true;
@@ -117,6 +118,9 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
 
     public static final int LISTEN_PORT = 7654;
     
+    /** for extension */
+    public I2PSessionImpl() {}
+
     /**
      * Create a new session, reading the Destination, PrivateKey, and SigningPrivateKey
      * from the destKeyStream, and using the specified options to connect to the router
@@ -151,7 +155,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
      * Parse the config for anything we know about
      *
      */
-    private void loadConfig(Properties options) {
+    protected void loadConfig(Properties options) {
         _options = new Properties();
         _options.putAll(filter(options));
         _hostname = _options.getProperty(I2PClient.PROP_TCP_HOST, "localhost");
@@ -385,7 +389,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
         }
     }
 
-    private class AvailabilityNotifier implements Runnable {
+    protected class AvailabilityNotifier implements Runnable {
         private List _pendingIds;
         private List _pendingSizes;
         private boolean _alive;
@@ -566,7 +570,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
         
         if (_log.shouldLog(Log.INFO)) _log.info(getPrefix() + "Destroy the session", new Exception("DestroySession()"));
         _closing = true;   // we use this to prevent a race
-        if (sendDisconnect) {
+        if (sendDisconnect && _producer != null) {    // only null if overridden by I2PSimpleSession
             try {
                 _producer.disconnect(this);
             } catch (I2PSessionException ipe) {
@@ -659,4 +663,8 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
     }
     
     protected String getPrefix() { return "[" + (_sessionId == null ? -1 : _sessionId.getSessionId()) + "]: "; }
+
+    public Destination lookupDest(Hash h) throws I2PSessionException {
+        return null;
+    }
 }
diff --git a/core/java/src/net/i2p/client/I2PSessionImpl2.java b/core/java/src/net/i2p/client/I2PSessionImpl2.java
index bbaf399f4afd20209af4c754dedc79b6aa395edb..81c6ef22f066d670fe7e00530e124d9131e071bd 100644
--- a/core/java/src/net/i2p/client/I2PSessionImpl2.java
+++ b/core/java/src/net/i2p/client/I2PSessionImpl2.java
@@ -31,7 +31,6 @@ import net.i2p.util.Log;
  * @author jrandom
  */
 class I2PSessionImpl2 extends I2PSessionImpl {
-    private Log _log;
 
     /** set of MessageState objects, representing all of the messages in the process of being sent */
     private Set _sendingStates;
@@ -41,6 +40,9 @@ class I2PSessionImpl2 extends I2PSessionImpl {
     private final static boolean SHOULD_COMPRESS = true;
     private final static boolean SHOULD_DECOMPRESS = true;
 
+    /** for extension */
+    public I2PSessionImpl2() {}
+
     /**
      * Create a new session, reading the Destination, PrivateKey, and SigningPrivateKey
      * from the destKeyStream, and using the specified options to connect to the router
@@ -396,6 +398,8 @@ class I2PSessionImpl2 extends I2PSessionImpl {
     }
 
     private void clearStates() {
+        if (_sendingStates == null)    // only null if overridden by I2PSimpleSession
+            return;
         synchronized (_sendingStates) {
             for (Iterator iter = _sendingStates.iterator(); iter.hasNext();) {
                 MessageState state = (MessageState) iter.next();
diff --git a/core/java/src/net/i2p/client/I2PSimpleClient.java b/core/java/src/net/i2p/client/I2PSimpleClient.java
new file mode 100644
index 0000000000000000000000000000000000000000..9ce4b8d6f5dfb26e3867e365211c7ee77eb90132
--- /dev/null
+++ b/core/java/src/net/i2p/client/I2PSimpleClient.java
@@ -0,0 +1,47 @@
+package net.i2p.client;
+
+/*
+ * Released into the public domain 
+ * with no warranty of any kind, either expressed or implied.  
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Properties;
+
+import net.i2p.I2PAppContext;
+import net.i2p.I2PException;
+import net.i2p.data.Certificate;
+import net.i2p.data.Destination;
+
+/**
+ * Simple client implementation with no Destination,
+ * just used to talk to the router.
+ */
+public class I2PSimpleClient implements I2PClient {
+    /** Don't do this */
+    public Destination createDestination(OutputStream destKeyStream) throws I2PException, IOException {
+        return null;
+    }
+
+    /** or this */
+    public Destination createDestination(OutputStream destKeyStream, Certificate cert) throws I2PException, IOException {
+        return null;
+    }
+
+    /**
+     * Create a new session (though do not connect it yet)
+     *
+     */
+    public I2PSession createSession(InputStream destKeyStream, Properties options) throws I2PSessionException {
+        return createSession(I2PAppContext.getGlobalContext(), options);
+    }
+    /**
+     * Create a new session (though do not connect it yet)
+     *
+     */
+    public I2PSession createSession(I2PAppContext context, Properties options) throws I2PSessionException {
+        return new I2PSimpleSession(context, options);
+    }
+}
diff --git a/core/java/src/net/i2p/client/I2PSimpleSession.java b/core/java/src/net/i2p/client/I2PSimpleSession.java
new file mode 100644
index 0000000000000000000000000000000000000000..fcfafe7672885b09bc532febc0618506c4aa4bfd
--- /dev/null
+++ b/core/java/src/net/i2p/client/I2PSimpleSession.java
@@ -0,0 +1,123 @@
+package net.i2p.client;
+
+/*
+ * Released into the public domain 
+ * with no warranty of any kind, either expressed or implied.  
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.util.Properties;
+import java.util.Set;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Destination;
+import net.i2p.data.Hash;
+import net.i2p.data.i2cp.DestLookupMessage;
+import net.i2p.data.i2cp.DestReplyMessage;
+import net.i2p.data.i2cp.I2CPMessageReader;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+/**
+ * Create a new session for doing naming queries only. Do not create a Destination.
+ * Don't create a producer. Do not send/receive messages to other Destinations.
+ * Cannot handle multiple simultaneous queries atm.
+ * Could be expanded to ask the router other things.
+ */
+class I2PSimpleSession extends I2PSessionImpl2 {
+    private boolean _destReceived;
+    private Object _destReceivedLock;
+    private Destination _destination;
+
+    /**
+     * Create a new session for doing naming queries only. Do not create a destination.
+     *
+     * @throws I2PSessionException if there is a problem
+     */
+    public I2PSimpleSession(I2PAppContext context, Properties options) throws I2PSessionException {
+        _context = context;
+        _log = context.logManager().getLog(I2PSimpleSession.class);
+        _handlerMap = new SimpleMessageHandlerMap(context);
+        _closed = true;
+        _closing = false;
+        _availabilityNotifier = new AvailabilityNotifier();
+        if (options == null)
+            options = System.getProperties();
+        loadConfig(options);
+    }
+
+    /**
+     * Connect to the router and establish a session.  This call blocks until 
+     * a session is granted.
+     *
+     * @throws I2PSessionException if there is a configuration error or the router is
+     *                             not reachable
+     */
+    public void connect() throws I2PSessionException {
+        _closed = false;
+        _availabilityNotifier.stopNotifying();
+        I2PThread notifier = new I2PThread(_availabilityNotifier);
+        notifier.setName("Simple Notifier");
+        notifier.setDaemon(true);
+        notifier.start();
+        
+        try {
+            _socket = new Socket(_hostname, _portNum);
+            _out = _socket.getOutputStream();
+            synchronized (_out) {
+                _out.write(I2PClient.PROTOCOL_BYTE);
+            }
+            InputStream in = _socket.getInputStream();
+            _reader = new I2CPMessageReader(in, this);
+            _reader.startReading();
+
+        } catch (UnknownHostException uhe) {
+            _closed = true;
+            throw new I2PSessionException(getPrefix() + "Bad host ", uhe);
+        } catch (IOException ioe) {
+            _closed = true;
+            throw new I2PSessionException(getPrefix() + "Problem connecting to " + _hostname + " on port " + _portNum, ioe);
+        }
+    }
+
+    /** called by the message handler */
+    void destReceived(Destination d) {
+        _destReceived = true;
+        _destination = d;
+        synchronized (_destReceivedLock) {
+            _destReceivedLock.notifyAll();
+        }
+    }
+
+    public Destination lookupDest(Hash h) throws I2PSessionException {
+        if (_closed)
+            return null;
+        _destReceivedLock = new Object();
+        sendMessage(new DestLookupMessage(h));
+        for (int i = 0; i < 10 && !_destReceived; i++) {
+            try {
+                synchronized (_destReceivedLock) {
+                    _destReceivedLock.wait(1000);
+                }
+            } catch (InterruptedException ie) {}
+        }
+        _destReceived = false;
+        return _destination;
+    }
+
+    /**
+     * Only map message handlers that we will use
+     */
+    class SimpleMessageHandlerMap extends I2PClientMessageHandlerMap {
+        public SimpleMessageHandlerMap(I2PAppContext context) {
+            int highest = DestReplyMessage.MESSAGE_TYPE;
+            _handlers = new I2CPMessageHandler[highest+1];
+            _handlers[DestReplyMessage.MESSAGE_TYPE] = new DestReplyMessageHandler(context);
+        }
+    }
+}
diff --git a/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java b/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java
index d4ee7e3458b0d6d3e57ef23044e57e0b60db085b..4cfb07d40653f8945665e3eb8891a4a27fcdf99e 100644
--- a/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java
+++ b/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java
@@ -55,6 +55,8 @@ public class HostsTxtNamingService extends NamingService {
         return rv;
     }
     
+    private static final int BASE32_HASH_LENGTH = 52;   // 1 + Hash.HASH_LENGTH * 8 / 5
+
     @Override
     public Destination lookup(String hostname) {
         Destination d = getCache(hostname);
@@ -69,6 +71,15 @@ public class HostsTxtNamingService extends NamingService {
             return d;
         }
 
+        // Try Base32 decoding
+        if (hostname.length() == BASE32_HASH_LENGTH + 4 && hostname.endsWith(".i2p")) {
+            d = LookupDest.lookupBase32Hash(_context, hostname.substring(0, BASE32_HASH_LENGTH));
+            if (d != null) {
+                putCache(hostname, d);
+                return d;
+            }
+        }
+
         List filenames = getFilenames();
         for (int i = 0; i < filenames.size(); i++) { 
             String hostsfile = (String)filenames.get(i);
diff --git a/core/java/src/net/i2p/client/naming/LookupDest.java b/core/java/src/net/i2p/client/naming/LookupDest.java
new file mode 100644
index 0000000000000000000000000000000000000000..775ae6bcc1b03be35664f610fa47e074d619aacd
--- /dev/null
+++ b/core/java/src/net/i2p/client/naming/LookupDest.java
@@ -0,0 +1,72 @@
+/*
+ * Released into the public domain 
+ * with no warranty of any kind, either expressed or implied.  
+ */
+package net.i2p.client.naming;
+
+import java.util.Properties;
+
+import net.i2p.I2PAppContext;
+import net.i2p.client.I2PSessionException;
+import net.i2p.client.I2PClient;
+import net.i2p.client.I2PSession;
+import net.i2p.client.I2PSimpleClient;
+import net.i2p.data.Base32;
+import net.i2p.data.Base64;
+import net.i2p.data.Destination;
+import net.i2p.data.Hash;
+import net.i2p.data.LeaseSet;
+
+/**
+ * Connect via I2CP and ask the router to look up
+ * the lease of a hash, convert it to a Destination and return it.
+ * Obviously this can take a while.
+ *
+ * All calls are blocking and return null on failure.
+ * Timeout is set to 10 seconds in I2PSimpleSession.
+ */
+class LookupDest {
+
+    protected LookupDest(I2PAppContext context) {}
+
+    static Destination lookupBase32Hash(I2PAppContext ctx, String key) {
+        byte[] h = Base32.decode(key);
+        if (h == null)
+            return null;
+        return lookupHash(ctx, h);
+    }
+
+    /* Might be useful but not in the context of urls due to upper/lower case */
+    /****
+    static Destination lookupBase64Hash(I2PAppContext ctx, String key) {
+        byte[] h = Base64.decode(key);
+        if (h == null)
+            return null;
+        return lookupHash(ctx, h);
+    }
+    ****/
+
+    static Destination lookupHash(I2PAppContext ctx, byte[] h) {
+        Hash key = new Hash(h);
+        Destination rv = null;
+        try {
+            I2PClient client = new I2PSimpleClient();
+            Properties opts = new Properties();
+            String s = ctx.getProperty(I2PClient.PROP_TCP_HOST);
+            if (s != null)
+                opts.put(I2PClient.PROP_TCP_HOST, s);
+            s = ctx.getProperty(I2PClient.PROP_TCP_PORT);
+            if (s != null)
+                opts.put(I2PClient.PROP_TCP_PORT, s);
+            I2PSession session = client.createSession(null, opts);
+            session.connect();
+            rv = session.lookupDest(key);
+            session.destroySession();
+        } catch (I2PSessionException ise) {}
+        return rv;
+    }
+
+    public static void main(String args[]) {
+        System.out.println(lookupBase32Hash(I2PAppContext.getGlobalContext(), args[0]));
+    }
+}
diff --git a/core/java/src/net/i2p/data/Base32.java b/core/java/src/net/i2p/data/Base32.java
new file mode 100644
index 0000000000000000000000000000000000000000..b2cc2d548306d42693e852fb8cb1ac518075067b
--- /dev/null
+++ b/core/java/src/net/i2p/data/Base32.java
@@ -0,0 +1,245 @@
+package net.i2p.data;
+
+/*
+ * Released into the public domain 
+ * with no warranty of any kind, either expressed or implied.  
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import net.i2p.util.Log;
+
+/**
+ * Encodes and decodes to and from Base32 notation.
+ * Ref: RFC 3548
+ *
+ * Don't bother with '=' padding characters on encode or
+ * accept them on decode (i.e. don't require 5-character groups).
+ * No whitespace allowed.
+ *
+ * Decode accepts upper or lower case.
+ */
+public class Base32 {
+
+    private final static Log _log = new Log(Base32.class);
+
+    /** The 64 valid Base32 values. */
+    private final static char[] ALPHABET = {'a', 'b', 'c', 'd',
+                                            'e', 'f', 'g', 'h', 'i', 'j',
+                                            'k', 'l', 'm', 'n', 'o', 'p',
+                                            'q', 'r', 's', 't', 'u', 'v',
+                                            'w', 'x', 'y', 'z',
+                                            '2', '3', '4', '5', '6', '7'};
+
+    /** 
+     * Translates a Base32 value to either its 5-bit reconstruction value
+     * or a negative number indicating some other meaning.
+     * Allow upper or lower case.
+     **/
+    private final static byte[] DECODABET = {
+                                             26, 27, 28, 29, 30, 31, -9, -9, // Numbers two through nine
+                                             -9, -9, -9, // Decimal 58 - 60
+                                             -1, // Equals sign at decimal 61
+                                             -9, -9, -9, // Decimal 62 - 64
+                                             0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, // Letters 'A' through 'M'
+                                             13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'N' through 'Z'
+                                             -9, -9, -9, -9, -9, -9, // Decimal 91 - 96
+                                             0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, // Letters 'a' through 'm'
+                                             13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'n' through 'z'
+                                             -9, -9, -9, -9, -9 // Decimal 123 - 127
+    };
+
+    private final static byte BAD_ENCODING = -9; // Indicates error in encoding
+    private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding
+
+    /** Defeats instantiation. */
+    private Base32() { // nop
+    }
+
+    public static void main(String[] args) {
+        if (args.length == 0) {
+            help();
+            return;
+        }
+        runApp(args);
+    }
+
+    private static void runApp(String args[]) {
+        try {
+            if ("encodestring".equalsIgnoreCase(args[0])) {
+                System.out.println(encode(args[1].getBytes()));
+                return;
+            }
+            InputStream in = System.in;
+            OutputStream out = System.out;
+            if (args.length >= 3) {
+                out = new FileOutputStream(args[2]);
+            }
+            if (args.length >= 2) {
+                in = new FileInputStream(args[1]);
+            }
+            if ("encode".equalsIgnoreCase(args[0])) {
+                encode(in, out);
+                return;
+            }
+            if ("decode".equalsIgnoreCase(args[0])) {
+                decode(in, out);
+                return;
+            }
+        } catch (IOException ioe) {
+            ioe.printStackTrace(System.err);
+        }
+    }
+
+    private static byte[] read(InputStream in) throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream(4096);
+        byte buf[] = new byte[4096];
+        while (true) {
+            int read = in.read(buf);
+            if (read < 0) break;
+            baos.write(buf, 0, read);
+        }
+        return baos.toByteArray();
+    }
+
+    private static void encode(InputStream in, OutputStream out) throws IOException {
+        String encoded = encode(read(in));
+        for (int i = 0; i < encoded.length(); i++)
+            out.write((byte)(encoded.charAt(i) & 0xFF));
+    }
+
+    private static void decode(InputStream in, OutputStream out) throws IOException {
+        byte decoded[] = decode(new String(read(in)));
+        if (decoded == null) {
+            System.out.println("FAIL");
+            return;
+        }
+        out.write(decoded);
+    }
+
+    private static void help() {
+        System.out.println("Syntax: Base32 encode <inFile> <outFile>");
+        System.out.println("or    : Base32 encode <inFile>");
+        System.out.println("or    : Base32 encodestring <string>");
+        System.out.println("or    : Base32 encode");
+        System.out.println("or    : Base32 decode <inFile> <outFile>");
+        System.out.println("or    : Base32 decode <inFile>");
+        System.out.println("or    : Base32 decode");
+    }
+
+    public static String encode(String source) {
+        return (source != null ? encode(source.getBytes()) : "");
+    }
+
+    public static String encode(byte[] source) {
+        StringBuffer buf = new StringBuffer((source.length + 7) * 8 / 5);
+        encodeBytes(source, buf);
+        return buf.toString();
+    }
+
+    private final static byte[] emask = { (byte) 0x1f,
+                                          (byte) 0x01, (byte) 0x03, (byte) 0x07, (byte) 0x0f };
+    /**
+     * Encodes a byte array into Base32 notation.
+     *
+     * @param source The data to convert
+     */
+    private static void encodeBytes(byte[] source, StringBuffer out) {
+        int usedbits = 0;
+        for (int i = 0; i < source.length; ) {
+             int fivebits;
+             if (usedbits < 3) {
+                 fivebits = (source[i] >> (3 - usedbits)) & 0x1f;
+                 usedbits += 5;
+             } else if (usedbits == 3) {
+                 fivebits = source[i++] & 0x1f;
+                 usedbits = 0;
+             } else {
+                 fivebits = (source[i++] << (usedbits - 3)) & 0x1f;
+                 if (i < source.length) {
+                     usedbits -= 3;
+                     fivebits |= (source[i] >> (8 - usedbits)) & emask[usedbits];
+                 }
+             }
+             out.append(ALPHABET[fivebits]);
+        }
+    }
+
+    /**
+     * Decodes data from Base32 notation and
+     * returns it as a string.
+     *
+     * @param s the string to decode
+     * @return The data as a string or null on failure
+     */
+    public static String decodeToString(String s) {
+        byte[] b = decode(s);
+        if (b == null)
+            return null;
+        return new String(b);
+    }
+
+    public static byte[] decode(String s) {
+        return decode(s.getBytes());
+    }
+
+    private final static byte[] dmask = { (byte) 0xf8, (byte) 0x7c, (byte) 0x3e, (byte) 0x1f,
+                                          (byte) 0x0f, (byte) 0x07, (byte) 0x03, (byte) 0x01 };
+    /**
+     * Decodes Base32 content in byte array format and returns
+     * the decoded byte array.
+     *
+     * @param source The Base32 encoded data
+     * @return decoded data
+     */
+    private static byte[] decode(byte[] source) {
+        int len58;
+        if (source.length <= 1)
+            len58 = source.length;
+        else
+            len58 = source.length * 5 / 8;
+        byte[] outBuff = new byte[len58];
+        int outBuffPosn = 0;
+
+        int usedbits = 0;
+        for (int i = 0; i < source.length; i++) {
+            int fivebits;
+            if ((source[i] & 0x80) != 0 || source[i] < '2' || source[i] > 'z')
+                fivebits = BAD_ENCODING;
+            else
+                fivebits = DECODABET[source[i] - '2'];
+
+            if (fivebits >= 0) {
+                 if (usedbits == 0) {
+                     outBuff[outBuffPosn] = (byte) ((fivebits << 3) & 0xf8);
+                     usedbits = 5;
+                 } else if (usedbits < 3) {
+                     outBuff[outBuffPosn] |= (fivebits << (3 - usedbits)) & dmask[usedbits];
+                     usedbits += 5;
+                 } else if (usedbits == 3) {
+                     outBuff[outBuffPosn++] |= fivebits;
+                     usedbits = 0;
+                 } else {
+                     outBuff[outBuffPosn++] |= (fivebits >> (usedbits - 3)) & dmask[usedbits];
+                     byte next = (byte) (fivebits << (11 - usedbits));
+                     if (outBuffPosn < len58) {
+                         outBuff[outBuffPosn] = next;
+                         usedbits -= 3;
+                     } else if (next != 0) {
+                       _log.warn("Extra data at the end: " + next + "(decimal)");
+                       return null;
+                     }
+                 }
+            } else {
+                _log.warn("Bad Base32 input character at " + i + ": " + source[i] + "(decimal)");
+                return null;
+            }
+        }
+        return outBuff;
+    }
+}
diff --git a/core/java/src/net/i2p/data/i2cp/DestLookupMessage.java b/core/java/src/net/i2p/data/i2cp/DestLookupMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..13135a2a41150016461ea5a726ff9bb15af658e7
--- /dev/null
+++ b/core/java/src/net/i2p/data/i2cp/DestLookupMessage.java
@@ -0,0 +1,76 @@
+package net.i2p.data.i2cp;
+
+/*
+ * Released into the public domain 
+ * with no warranty of any kind, either expressed or implied.  
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Hash;
+
+/**
+ * Request the router look up the dest for a hash
+ */
+public class DestLookupMessage extends I2CPMessageImpl {
+    public final static int MESSAGE_TYPE = 34;
+    private Hash _hash;
+
+    public DestLookupMessage() {
+        super();
+    }
+
+    public DestLookupMessage(Hash h) {
+        _hash = h;
+    }
+
+    public Hash getHash() {
+        return _hash;
+    }
+
+    protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException {
+        Hash h = new Hash();
+        try {
+            h.readBytes(in);
+        } catch (DataFormatException dfe) {
+            throw new I2CPMessageException("Unable to load the hash", dfe);
+        }
+        _hash = h;
+    }
+
+    protected byte[] doWriteMessage() throws I2CPMessageException, IOException {
+        if (_hash == null)
+            throw new I2CPMessageException("Unable to write out the message as there is not enough data");
+        ByteArrayOutputStream os = new ByteArrayOutputStream(Hash.HASH_LENGTH);
+        try {
+            _hash.writeBytes(os);
+        } catch (DataFormatException dfe) {
+            throw new I2CPMessageException("Error writing out the hash", dfe);
+        }
+        return os.toByteArray();
+    }
+
+    public int getType() {
+        return MESSAGE_TYPE;
+    }
+
+    public boolean equals(Object object) {
+        if ((object != null) && (object instanceof DestLookupMessage)) {
+            DestLookupMessage msg = (DestLookupMessage) object;
+            return DataHelper.eq(getHash(), msg.getHash());
+        }
+        return false;
+    }
+
+    public String toString() {
+        StringBuffer buf = new StringBuffer();
+        buf.append("[DestLookupMessage: ");
+        buf.append("\n\tHash: ").append(_hash);
+        buf.append("]");
+        return buf.toString();
+    }
+}
diff --git a/core/java/src/net/i2p/data/i2cp/DestReplyMessage.java b/core/java/src/net/i2p/data/i2cp/DestReplyMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..1ed601dc28075bc73bfe273882fde94551f4978b
--- /dev/null
+++ b/core/java/src/net/i2p/data/i2cp/DestReplyMessage.java
@@ -0,0 +1,78 @@
+package net.i2p.data.i2cp;
+
+/*
+ * Released into the public domain 
+ * with no warranty of any kind, either expressed or implied.  
+ *
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Destination;
+
+/**
+ * Response to DestLookupMessage
+ *
+ */
+public class DestReplyMessage extends I2CPMessageImpl {
+    public final static int MESSAGE_TYPE = 35;
+    private Destination _dest;
+
+    public DestReplyMessage() {
+        super();
+    }
+
+    public DestReplyMessage(Destination d) {
+        _dest = d;
+    }
+
+    public Destination getDestination() {
+        return _dest;
+    }
+
+    protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException {
+        try {
+            Destination d = new Destination();
+            d.readBytes(in);
+            _dest = d;
+        } catch (DataFormatException dfe) {
+            _dest = null; // null dest allowed
+        }
+    }
+
+    protected byte[] doWriteMessage() throws I2CPMessageException, IOException {
+        if (_dest == null)
+            return new byte[0];  // null response allowed
+        ByteArrayOutputStream os = new ByteArrayOutputStream(_dest.size());
+        try {
+            _dest.writeBytes(os);
+        } catch (DataFormatException dfe) {
+            throw new I2CPMessageException("Error writing out the dest", dfe);
+        }
+        return os.toByteArray();
+    }
+
+    public int getType() {
+        return MESSAGE_TYPE;
+    }
+
+    public boolean equals(Object object) {
+        if ((object != null) && (object instanceof DestReplyMessage)) {
+            DestReplyMessage msg = (DestReplyMessage) object;
+            return DataHelper.eq(getDestination(), msg.getDestination());
+        }
+        return false;
+    }
+
+    public String toString() {
+        StringBuffer buf = new StringBuffer();
+        buf.append("[DestReplyMessage: ");
+        buf.append("\n\tDestination: ").append(_dest);
+        buf.append("]");
+        return buf.toString();
+    }
+}
diff --git a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java
index 481d26f0e23a6c686e63713f819fa621d6f63cbf..128c312dce4cc1150c4f7f8e54c9c4a50e066fb6 100644
--- a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java
+++ b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java
@@ -81,6 +81,10 @@ public class I2CPMessageHandler {
             return new GetDateMessage();
         case SetDateMessage.MESSAGE_TYPE:
             return new SetDateMessage();
+        case DestLookupMessage.MESSAGE_TYPE:
+            return new DestLookupMessage();
+        case DestReplyMessage.MESSAGE_TYPE:
+            return new DestReplyMessage();
         default:
             throw new I2CPMessageException("The type " + type + " is an unknown I2CP message");
         }
@@ -94,4 +98,4 @@ public class I2CPMessageHandler {
             e.printStackTrace();
         }
     }
-}
\ No newline at end of file
+}
diff --git a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
index d75e27fb481dce8b64faec858b2ae0ce87c7f5b3..033e28f2f00b6f8f59acaccddd2332ee5e4d4c20 100644
--- a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
+++ b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
@@ -11,6 +11,7 @@ package net.i2p.router.client;
 import net.i2p.data.Payload;
 import net.i2p.data.i2cp.CreateLeaseSetMessage;
 import net.i2p.data.i2cp.CreateSessionMessage;
+import net.i2p.data.i2cp.DestLookupMessage;
 import net.i2p.data.i2cp.DestroySessionMessage;
 import net.i2p.data.i2cp.GetDateMessage;
 import net.i2p.data.i2cp.I2CPMessage;
@@ -78,6 +79,9 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
             case DestroySessionMessage.MESSAGE_TYPE:
                 handleDestroySession(reader, (DestroySessionMessage)message);
                 break;
+            case DestLookupMessage.MESSAGE_TYPE:
+                handleDestLookup(reader, (DestLookupMessage)message);
+                break;
             default:
                 if (_log.shouldLog(Log.ERROR))
                     _log.error("Unhandled I2CP type received: " + message.getType());
@@ -85,13 +89,14 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
     }
 
     /**
-     * Handle notifiation that there was an error
+     * Handle notification that there was an error
      *
      */
     public void readError(I2CPMessageReader reader, Exception error) {
         if (_runner.isDead()) return;
         if (_log.shouldLog(Log.ERROR))
             _log.error("Error occurred", error);
+        // Is this is a little drastic for an unknown message type?
         _runner.stopRunning();
     }
     
@@ -228,6 +233,10 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
         _runner.leaseSetCreated(message.getLeaseSet());
     }
 
+    private void handleDestLookup(I2CPMessageReader reader, DestLookupMessage message) {
+        _context.jobQueue().addJob(new LookupDestJob(_context, _runner, message.getHash()));
+    }
+
     // this *should* be mod 65536, but UnsignedInteger is still b0rked.  FIXME
     private final static int MAX_SESSION_ID = 32767;
 
diff --git a/router/java/src/net/i2p/router/client/LookupDestJob.java b/router/java/src/net/i2p/router/client/LookupDestJob.java
new file mode 100644
index 0000000000000000000000000000000000000000..68edbcaa06f5a7a85e269121d265f522b1bf62fe
--- /dev/null
+++ b/router/java/src/net/i2p/router/client/LookupDestJob.java
@@ -0,0 +1,54 @@
+/*
+ * Released into the public domain 
+ * with no warranty of any kind, either expressed or implied.  
+ */
+package net.i2p.router.client;
+
+import net.i2p.data.Destination;
+import net.i2p.data.Hash;
+import net.i2p.data.LeaseSet;
+import net.i2p.data.i2cp.DestReplyMessage;
+import net.i2p.data.i2cp.I2CPMessageException;
+import net.i2p.router.JobImpl;
+import net.i2p.router.RouterContext;
+
+/**
+ * Look up the lease of a hash, to convert it to a Destination for the client
+ */
+class LookupDestJob extends JobImpl {
+    private ClientConnectionRunner _runner;
+    private Hash _hash;
+
+    public LookupDestJob(RouterContext context, ClientConnectionRunner runner, Hash h) {
+        super(context);
+        _runner = runner;
+        _hash = h;
+    }
+    
+    public String getName() { return "LeaseSet Lookup for Client"; }
+    public void runJob() {
+        DoneJob done = new DoneJob(getContext());
+        getContext().netDb().lookupLeaseSet(_hash, done, done, 10*1000);
+    }
+
+    private class DoneJob extends JobImpl {
+        public DoneJob(RouterContext enclosingContext) { 
+            super(enclosingContext);
+        }
+        public String getName() { return "LeaseSet Lookup Reply to Client"; }
+        public void runJob() {
+            LeaseSet ls = getContext().netDb().lookupLeaseSetLocally(_hash);
+            if (ls != null)
+                returnDest(ls.getDestination());
+            else
+                returnDest(null);
+        }
+    }
+
+    private void returnDest(Destination d) {
+        DestReplyMessage msg = new DestReplyMessage(d);
+        try {
+            _runner.doSend(msg);
+        } catch (I2CPMessageException ime) {}
+    }
+}