diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java
index 2b269e6665e9f59c2d128994228591683be3afed..f522193f68a2c3de455a82f1861a59be3d5d1f88 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java
@@ -46,6 +46,7 @@ import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
 import java.util.StringTokenizer;
@@ -57,6 +58,8 @@ import net.i2p.I2PException;
 import net.i2p.client.I2PClient;
 import net.i2p.client.I2PClientFactory;
 import net.i2p.client.I2PSession;
+import net.i2p.client.I2PSessionException;
+import net.i2p.client.I2PSimpleClient;
 import net.i2p.client.naming.NamingService;
 import net.i2p.data.Base64;
 import net.i2p.data.DataFormatException;
@@ -68,6 +71,7 @@ import net.i2p.i2ptunnel.streamr.StreamrConsumer;
 import net.i2p.i2ptunnel.streamr.StreamrProducer;
 import net.i2p.util.EventDispatcherImpl;
 import net.i2p.util.Log;
+import net.i2p.util.OrderedProperties;
 
 /**
  *  An I2PTunnel tracks one or more I2PTunnelTasks and one or more I2PSessions.
@@ -87,9 +91,9 @@ public class I2PTunnel extends EventDispatcherImpl implements Logging {
 
     public boolean ownDest = false;
 
-    /** the I2CP port */
+    /** the I2CP port, non-null */
     public String port = System.getProperty(I2PClient.PROP_TCP_PORT, "7654");
-    /** the I2CP host */
+    /** the I2CP host, non-null */
     public String host = System.getProperty(I2PClient.PROP_TCP_HOST, "127.0.0.1");
     /** the listen-on host. Sadly the listen-on port does not have a field. */
     public String listenHost = host;
@@ -168,7 +172,7 @@ public class I2PTunnel extends EventDispatcherImpl implements Logging {
                 System.out.println("Enter 'help' for help.");
                 BufferedReader r = new BufferedReader(new InputStreamReader(System.in));
                 while (true) {
-                    System.out.print("I2PTunnel>");
+                    System.out.print("I2PTunnel> ");
                     String cmd = r.readLine();
                     if (cmd == null) break;
                     if (cmd.length() <= 0) continue;
@@ -293,6 +297,8 @@ public class I2PTunnel extends EventDispatcherImpl implements Logging {
             runPing(allargs, l);
         } else if (cmdname.equals("owndest")) {
             runOwnDest(args, l);
+        } else if (cmdname.equals("auth")) {
+            runAuth(args, l);
         } else {
             l.log("Unknown command [" + cmdname + "]");
         }
@@ -308,27 +314,28 @@ public class I2PTunnel extends EventDispatcherImpl implements Logging {
     private static void runHelp(Logging l) {
         l.log("Command list:");
         // alphabetical please...
-        l.log("client <port> <pubkey>[,<pubkey,...]|file:<pubkeyfile> [<sharedClient>]");
-        l.log("clientoptions[ key=value]*");
-        l.log("close [forced] <jobnumber>|all");
-        l.log("config <i2phost> <i2pport>");
-        l.log("connectclient <port> [<sharedClient>] [<proxy>]");
-        l.log("genkeys <privkeyfile> [<pubkeyfile>]");
-        l.log("gentextkeys");
-        l.log("httpbidirserver <host> <port> <proxyport> <spoofedhost> <privkeyfile>");
-        l.log("httpclient <port> [<sharedClient>] [<proxy>]");
-        l.log("httpserver <host> <port> <spoofedhost> <privkeyfile>");
-        l.log("ircclient <port> <pubkey>[,<pubkey,...]|file:<pubkeyfile> [<sharedClient>]");
-        l.log("list");
-        l.log("listen_on <ip>");
-        l.log("lookup <name>");
-        l.log("owndest yes|no");
-        l.log("ping <args>");
-        l.log("quit");
-        l.log("read_timeout <msecs>");
-        l.log("run <commandfile>");
-        l.log("server <host> <port> <privkeyfile>");
-        l.log("textserver <host> <port> <privkey>");
+        l.log("  auth <username> <password>");
+        l.log("  client <port> <pubkey>[,<pubkey,...]|file:<pubkeyfile> [<sharedClient>]");
+        l.log("  clientoptions [-acx] [key=value ]*");
+        l.log("  close [forced] <jobnumber>|all");
+        l.log("  config [-s] <i2phost> <i2pport>");
+        l.log("  connectclient <port> [<sharedClient>] [<proxy>]");
+        l.log("  genkeys <privkeyfile> [<pubkeyfile>]");
+        l.log("  gentextkeys");
+        l.log("  httpbidirserver <host> <port> <proxyport> <spoofedhost> <privkeyfile>");
+        l.log("  httpclient <port> [<sharedClient>] [<proxy>]");
+        l.log("  httpserver <host> <port> <spoofedhost> <privkeyfile>");
+        l.log("  ircclient <port> <pubkey>[,<pubkey,...]|file:<pubkeyfile> [<sharedClient>]");
+        l.log("  list");
+        l.log("  listen_on <ip>");
+        l.log("  lookup <name>");
+        l.log("  owndest yes|no");
+        l.log("  ping <args>");
+        l.log("  quit");
+        l.log("  read_timeout <msecs>");
+        l.log("  run <commandfile>");
+        l.log("  server <host> <port> <privkeyfile>");
+        l.log("  textserver <host> <port> <privkey>");
     }
     
     /**
@@ -345,15 +352,43 @@ public class I2PTunnel extends EventDispatcherImpl implements Logging {
      * @param l logger to receive events and output
      */
     public void runClientOptions(String args[], Logging l) {
-        _clientOptions.clear();
-        if (args != null) {
-            for (int i = 0; i < args.length; i++) {
+        if (args != null && args.length > 0) {
+            int i = 0;
+            if (args[0].equals("-a")) {
+                i++;
+            } else if (args[0].equals("-c")) {
+                _clientOptions.clear();
+                l.log("Client options cleared");
+                return;
+            } else if (args[0].equals("-x")) {
+                i++;
+                for ( ; i < args.length; i++) {
+                     if (_clientOptions.remove(args[i]) != null)
+                        l.log("Removed " + args[i]);
+                }
+                return;
+            } else {
+                _clientOptions.clear();
+            }
+            for ( ; i < args.length; i++) {
                 int index = args[i].indexOf('=');
                 if (index <= 0) continue;
                 String key = args[i].substring(0, index);
                 String val = args[i].substring(index+1);
                 _clientOptions.setProperty(key, val);
             }
+        } else {
+            l.log("Usage:");
+            l.log("  clientoptions [key=value ]*     // sets current options");
+            l.log("  clientoptions -a [key=value ]*  // adds to current options");
+            l.log("  clientoptions -c                // clears current options");
+            l.log("  clientoptions -x [key ]*        // removes listed options");
+            l.log("Current options:");
+            Properties p = new OrderedProperties();
+            p.putAll(_clientOptions);
+            for (Map.Entry<Object, Object> e : p.entrySet()) {
+                l.log("  [" + e.getKey() + "] = [" + e.getValue() + ']');
+            }
         }
         notifyEvent("clientoptions_onResult", "ok");
     }
@@ -1147,18 +1182,47 @@ public class I2PTunnel extends EventDispatcherImpl implements Logging {
      * @param l logger to receive events and output
      */
     private void runConfig(String args[], Logging l) {
-        if (args.length == 2) {
-            host = args[0];
+        if (args.length >= 2) {
+            int i = 0;
+            if (args[0].equals("-s")) {
+                _clientOptions.setProperty("i2cp.SSL", "true");
+                i++;
+            } else {
+                _clientOptions.remove("i2cp.SSL");
+            }
+            host = args[i++];
             listenHost = host;
-            port = args[1];
+            port = args[i];
             notifyEvent("configResult", "ok");
         } else {
-            l.log("config <i2phost> <i2pport>");
+            l.log("Usage:");
+            l.log("  config [-s] <i2phost> <i2pport>");
             l.log("  sets the connection to the i2p router.");
+            l.log("Current setting:");
+            boolean ssl = Boolean.parseBoolean(_clientOptions.getProperty("i2cp.SSL"));
+            l.log("  " + host + ' ' + port + (ssl ? " SSL" : ""));
             notifyEvent("configResult", "error");
         }
     }
 
+    /**
+     * Specify the i2cp username and password
+     *
+     * @param args {username, password}
+     * @param l logger to receive events and output
+     * @since 0.9.10
+     */
+    private void runAuth(String args[], Logging l) {
+        if (args.length == 2) {
+            _clientOptions.setProperty("i2cp.username", args[0]);
+            _clientOptions.setProperty("i2cp.password", args[1]);
+        } else {
+            l.log("Usage:");
+            l.log("  auth <username> <password>");
+            l.log("  Sets the i2cp credentials");
+        }
+    }
+
     /**
      * Specify whether to use its own destination for each outgoing tunnel
      * Deprecated - only used by CLI
@@ -1415,16 +1479,19 @@ public class I2PTunnel extends EventDispatcherImpl implements Logging {
             notifyEvent("lookupResult", "invalidUsage");
         } else {
             try {
-                Destination dest = destFromName(args[0]);
+                boolean ssl = Boolean.parseBoolean(_clientOptions.getProperty("i2cp.SSL"));
+                String user = _clientOptions.getProperty("i2cp.username");
+                String pw = _clientOptions.getProperty("i2cp.password");
+                Destination dest = destFromName(args[0], host, port, ssl, user, pw);
                 if (dest == null) {
-                    l.log("Unknown host");
+                    l.log("Unknown host: " + args[0]);
                     notifyEvent("lookupResult", "unkown host");
                 } else {
                     l.log(dest.toBase64());
                     notifyEvent("lookupResult", dest.toBase64());
                 }
             } catch (DataFormatException dfe) {
-                l.log("Unknown or invalid host");
+                l.log("Unknown or invalid host: " + args[0]);
                 notifyEvent("lookupResult", "invalid host");
             }
         }
@@ -1441,20 +1508,19 @@ public class I2PTunnel extends EventDispatcherImpl implements Logging {
      */
     private void runPing(String allargs, Logging l) {
         if (allargs.length() != 0) {
-            I2PTunnelTask task;
-            // pings always use the main destination
-            task = new I2Ping(allargs, l, false, this, this);
+            _clientOptions.setProperty(I2Ping.PROP_COMMAND, allargs);
+            I2PTunnelTask task = new I2Ping(l, ownDest, this, this);
             addtask(task);
             notifyEvent("pingTaskId", Integer.valueOf(task.getId()));
         } else {
-            l.log("ping <opts> <dest>");
+            l.log("ping <opts> <b64dest|host>");
             l.log("ping <opts> -h (pings all hosts in hosts.txt)");
             l.log("ping <opts> -l <destlistfile> (pings a list of hosts in a file)");
             l.log("   Options:\n" +
                   "     -c (require 5 consecutive pings to report success)\n" +
                   "     -m maxSimultaneousPings (default 10)\n" +
                   "     -n numberOfPings (default 3)\n" +
-                  "     -t timeout (ms, default 5000)\n");
+                  "     -t timeout (ms, default 30000)\n");
             l.log("   Tests communication with peers.\n");
             notifyEvent("pingTaskId", Integer.valueOf(-1));
         }
@@ -1599,6 +1665,19 @@ public class I2PTunnel extends EventDispatcherImpl implements Logging {
      * @deprecated Don't use i2ptunnel for lookup! Use I2PAppContext.getGlobalContext().namingService().lookup(name) from i2p.jar
      */
     public static Destination destFromName(String name) throws DataFormatException {
+        return destFromName(name, null, null, false, null, null);
+    }
+
+    /**
+     *  @param i2cpHost may be null
+     *  @param i2cpPort may be null
+     *  @param user may be null
+     *  @param pw may be null
+     *  @since 0.9.10
+     */
+    private static Destination destFromName(String name, String i2cpHost,
+                                            String i2cpPort, boolean isSSL,
+                                            String user, String pw) throws DataFormatException {
 
         if ((name == null) || (name.trim().length() <= 0)) throw new DataFormatException("Empty destination provided");
 
@@ -1642,8 +1721,46 @@ public class I2PTunnel extends EventDispatcherImpl implements Logging {
             }
         } else {
             // ask naming service
+            name = name.trim();
             NamingService inst = ctx.namingService();
-            return inst.lookup(name);
+            boolean b32 = name.length() == 60 && name.toLowerCase(Locale.US).endsWith(".b32.i2p");
+            Destination d = null;
+            if (ctx.isRouterContext() || !b32) {
+                // Local lookup.
+                // Even though we could do b32 outside router ctx here,
+                // we do it below instead so we can set the host and port,
+                // which we can't do with lookup()
+                d = inst.lookup(name);
+                if (d != null || ctx.isRouterContext() || name.length() >= 516)
+                    return d;
+            }
+            // Outside router context only,
+            // try simple session to ask the router.
+            I2PClient client = new I2PSimpleClient();
+            Properties opts = new Properties();
+            if (i2cpHost != null)
+                opts.put(I2PClient.PROP_TCP_HOST, i2cpHost);
+            if (i2cpPort != null)
+                opts.put(I2PClient.PROP_TCP_PORT, i2cpPort);
+            opts.put("i2cp.SSL", Boolean.toString(isSSL));
+            if (user != null)
+                opts.put("i2cp.username", user);
+            if (pw != null)
+                opts.put("i2cp.password", pw);
+            I2PSession session = null;
+            try {
+                session = client.createSession(null, opts);
+                session.connect();
+                d = session.lookupDest(name);
+            } catch (I2PSessionException ise) {
+                if (log.shouldLog(Log.WARN)) 
+                    log.warn("Lookup via router failed", ise);
+            } finally {
+                if (session != null) {
+                    try { session.destroySession(); } catch (I2PSessionException ise) {}
+                }
+            }
+            return d;
         }
     }
 
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java
index 909518a9f7e444f58451ee413534ef75407629d0..17ea0019f0b62b71d17ad3abce99c7842d328f36 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java
@@ -51,15 +51,18 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
     protected final List<I2PSocket> mySockets = new ArrayList<I2PSocket>();
     protected boolean _ownDest;
 
-    protected Destination dest = null;
+    protected Destination dest;
     private int localPort;
 
-    private boolean listenerReady = false;
+    /**
+     *  Protected for I2Ping since 0.9.10. Not for use outside package.
+     */
+    protected boolean listenerReady;
 
     protected ServerSocket ss;
 
     private final Object startLock = new Object();
-    private boolean startRunning = false;
+    private boolean startRunning;
 
     // private Object closeLock = new Object();
 
@@ -68,7 +71,7 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
     private String privKeyFile;
 
     // true if we are chained from a server.
-    private boolean chained = false;
+    private boolean chained;
 
     /** how long to wait before dropping an idle thread */
     private static final long HANDLER_KEEPALIVE_MS = 2*60*1000;
@@ -582,7 +585,11 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
         return i2ps;
     }
 
-    public final void run() {
+    /**
+     *  Non-final since 0.9.10.
+     *  Any overrides must set listenerReady = true.
+     */
+    public void run() {
         try {
             InetAddress addr = getListenHost(l);
             if (addr == null) {
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2Ping.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2Ping.java
index 0390f84b7e5317a9b27cd536de791080494d18ab..a41ae15ed9f2307517f71b8b78d5249efa66f7f6 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2Ping.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2Ping.java
@@ -6,69 +6,70 @@ package net.i2p.i2ptunnel;
 import java.io.BufferedReader;
 import java.io.FileReader;
 import java.io.IOException;
+import java.net.Socket;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 
 import net.i2p.I2PAppContext;
 import net.i2p.I2PException;
+import net.i2p.client.I2PSession;
+import net.i2p.client.I2PSessionException;
 import net.i2p.client.streaming.I2PSocketManager;
 import net.i2p.data.Destination;
 import net.i2p.util.EventDispatcher;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
 
-public class I2Ping extends I2PTunnelTask implements Runnable {
-    private final static Log _log = new Log(I2Ping.class);
+/**
+ *  Warning - not necessarily a stable API.
+ *  Used by I2PTunnel CLI only. Consider this sample code.
+ *  Not for use outside this package.
+ */
+public class I2Ping extends I2PTunnelClientBase {
+
+    public static final String PROP_COMMAND = "command";
 
-    private int PING_COUNT = 3;
+    private static final int PING_COUNT = 3;
     private static final int CPING_COUNT = 5;
-    private static final int PING_TIMEOUT = 5000;
+    private static final int PING_TIMEOUT = 30*1000;
 
     private static final long PING_DISTANCE = 1000;
 
     private int MAX_SIMUL_PINGS = 10; // not really final...
 
-    private boolean countPing = false;
-    private boolean reportTimes = true;
 
-    private I2PSocketManager sockMgr;
-    private Logging l;
-    private boolean finished = false;
-    private String command;
-    private long timeout = PING_TIMEOUT;
+    private volatile boolean finished;
 
     private final Object simulLock = new Object();
-    private int simulPings = 0;
-    private long lastPingTime = 0;
-
-    private final Object lock = new Object(), slock = new Object();
-
-    //public I2Ping(String cmd, Logging l,
-    //		  boolean ownDest) {
-    //	I2Ping(cmd, l, (EventDispatcher)null);
-    //}
-
-    public I2Ping(String cmd, Logging l, boolean ownDest, EventDispatcher notifyThis, I2PTunnel tunnel) {
-        super("I2Ping [" + cmd + "]", notifyThis, tunnel);
-        this.l = l;
-        command = cmd;
-        synchronized (slock) {
-            if (ownDest) {
-                sockMgr = I2PTunnelClient.buildSocketManager(tunnel);
-            } else {
-                sockMgr = I2PTunnelClient.getSocketManager(tunnel);
-            }
+    private int simulPings;
+    private long lastPingTime;
+
+    /**
+     *  tunnel.getOptions must contain "command".
+     *  @throws IllegalArgumentException if it doesn't
+     */
+    public I2Ping(Logging l, boolean ownDest, EventDispatcher notifyThis, I2PTunnel tunnel) {
+        super(-1, ownDest, l, notifyThis, "I2Ping", tunnel);
+        if (!tunnel.getClientOptions().containsKey(PROP_COMMAND)) {
+            // todo clean up
+            throw new IllegalArgumentException("Options does not contain " + PROP_COMMAND);
         }
-        Thread t = new I2PAppThread(this);
-        t.setName("Client");
-        t.start();
-        open = true;
     }
 
+    /**
+     *  Overrides super. No client ServerSocket is created.
+     */
+    @Override
     public void run() {
+        // Notify constructor that port is ready
+        synchronized (this) {
+            listenerReady = true;
+            notify();
+        }
         l.log("*** I2Ping results:");
         try {
-            runCommand(command);
+            runCommand(getTunnel().getClientOptions().getProperty(PROP_COMMAND));
         } catch (InterruptedException ex) {
             l.log("*** Interrupted");
             _log.error("Pinger interrupted", ex);
@@ -76,13 +77,15 @@ public class I2Ping extends I2PTunnelTask implements Runnable {
             _log.error("Pinger exception", ex);
         }
         l.log("*** Finished.");
-        synchronized (lock) {
-            finished = true;
-        }
+        finished = true;
         close(false);
     }
 
     public void runCommand(String cmd) throws InterruptedException, IOException {
+      long timeout = PING_TIMEOUT;
+      int count = PING_COUNT;
+      boolean countPing = false;
+      boolean reportTimes = true;
       while (true) {
         if (cmd.startsWith("-t ")) { // timeout
             cmd = cmd.substring(3);
@@ -92,6 +95,9 @@ public class I2Ping extends I2PTunnelTask implements Runnable {
                 return;
             } else {
                 timeout = Long.parseLong(cmd.substring(0, pos));
+                // convenience, convert msec to sec
+                if (timeout < 100)
+                    timeout *= 1000;
                 cmd = cmd.substring(pos + 1);
             }
         } else if (cmd.startsWith("-m ")) { // max simultaneous pings
@@ -111,11 +117,12 @@ public class I2Ping extends I2PTunnelTask implements Runnable {
                 l.log("Syntax error");
                 return;
             } else {
-                PING_COUNT = Integer.parseInt(cmd.substring(0, pos));
+                count = Integer.parseInt(cmd.substring(0, pos));
                 cmd = cmd.substring(pos + 1);
             }
         } else if (cmd.startsWith("-c ")) { // "count" ping
             countPing = true;
+            count = CPING_COUNT;
             cmd = cmd.substring(3);
         } else if (cmd.equals("-h")) { // ping all hosts
             cmd = "-l hosts.txt";
@@ -131,7 +138,9 @@ public class I2Ping extends I2PTunnelTask implements Runnable {
                 if (line.indexOf("=") != -1) { // maybe file is hosts.txt?
                     line = line.substring(0, line.indexOf("="));
                 }
-                pingHandlers.add(new PingHandler(line));
+                PingHandler ph = new PingHandler(line, count, timeout, countPing, reportTimes);
+                ph.start();
+                pingHandlers.add(ph);
                 if (++i > 1)
                     reportTimes = false;
             }
@@ -140,28 +149,28 @@ public class I2Ping extends I2PTunnelTask implements Runnable {
                 t.join();
             return;
         } else {
-            Thread t = new PingHandler(cmd);
+            Thread t = new PingHandler(cmd, count, timeout, countPing, reportTimes);
+            t.start();
             t.join();
             return;
         }
       }
     }
 
+    @Override
     public boolean close(boolean forced) {
         if (!open) return true;
-        synchronized (lock) {
-            if (!forced && !finished) {
-                l.log("There are still pings running!");
-                return false;
-            }
-            l.log("Closing pinger " + toString());
-            l.log("Pinger closed.");
-            open = false;
-            return true;
+        super.close(forced);
+        if (!forced && !finished) {
+            l.log("There are still pings running!");
+            return false;
         }
+        l.log("Closing pinger " + toString());
+        l.log("Pinger closed.");
+        return true;
     }
 
-    public boolean ping(Destination dest) throws I2PException {
+    private boolean ping(Destination dest, long timeout) throws I2PException {
         try {
             synchronized (simulLock) {
                 while (simulPings >= MAX_SIMUL_PINGS) {
@@ -186,33 +195,48 @@ public class I2Ping extends I2PTunnelTask implements Runnable {
         }
     }
 
-    public class PingHandler extends I2PAppThread {
-        private String destination;
+    /**
+     *  Does nothing.
+     *  @since 0.9.10
+     */
+    protected void clientConnectionRun(Socket s) {}
 
-        public PingHandler(String dest) {
+    private class PingHandler extends I2PAppThread {
+        private final String destination;
+        private final int cnt;
+        private final long timeout;
+        private final boolean countPing;
+        private final boolean reportTimes;
+
+        /**
+         *  As of 0.9.10, does NOT start itself.
+         *  Caller must call start()
+         *  @param dest b64 or b32 or host name
+         */
+        public PingHandler(String dest, int count, long timeout, boolean countPings, boolean report) {
             this.destination = dest;
+            cnt = count;
+            this.timeout = timeout;
+            countPing = countPings;
+            reportTimes = report;
             setName("PingHandler for " + dest);
-            start();
         }
 
         @Override
         public void run() {
             try {
-                Destination dest = I2PAppContext.getGlobalContext().namingService().lookup(destination);
+                Destination dest = lookup(destination);
                 if (dest == null) {
-                    synchronized (lock) { // Logger is not thread safe
-                        l.log("Unresolvable: " + destination + "");
-                    }
+                    l.log("Unresolvable: " + destination);
                     return;
                 }
                 int pass = 0;
                 int fail = 0;
                 long totalTime = 0;
-                int cnt = countPing ? CPING_COUNT : PING_COUNT;
                 StringBuilder pingResults = new StringBuilder(2 * cnt + destination.length() + 3);
                 for (int i = 0; i < cnt; i++) {
                     boolean sent;
-                    sent = ping(dest);
+                    sent = ping(dest, timeout);
                     if (countPing) {
                         if (!sent) {
                             pingResults.append(i).append(" ");
@@ -244,12 +268,35 @@ public class I2Ping extends I2PTunnelTask implements Runnable {
                     pingResults.append("and ").append(fail).append(" lost for destination: ");
                 }
                 pingResults.append("  ").append(destination);
-                synchronized (lock) { // Logger is not thread safe
-                    l.log(pingResults.toString());
-                }
+                l.log(pingResults.toString());
             } catch (I2PException ex) {
                 _log.error("Error pinging " + destination, ex);
             }
         }
+
+        /**
+         *  @param name b64 or b32 or host name
+         *  @since 0.9.10
+         */
+        private Destination lookup(String name) {
+            I2PAppContext ctx = I2PAppContext.getGlobalContext();
+            boolean b32 = name.length() == 60 && name.toLowerCase(Locale.US).endsWith(".b32.i2p");
+            if (ctx.isRouterContext() && !b32) {
+                // Local lookup.
+                // Even though we could do b32 outside router ctx here,
+                // we do it below instead so we can use the session,
+                // which we can't do with lookup()
+                Destination dest = ctx.namingService().lookup(name);
+                if (dest != null || ctx.isRouterContext() || name.length() >= 516)
+                    return dest;
+            }
+            try {
+                I2PSession sess = sockMgr.getSession();
+                return sess.lookupDest(name);
+            } catch (I2PSessionException ise) {
+                _log.error("Error looking up " + name, ise);
+                return null;
+            }
+        }
     }
 }
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/impl/ConnectionManager.java b/apps/streaming/java/src/net/i2p/client/streaming/impl/ConnectionManager.java
index 0e3c976a7ee13e30f0bffd6130dbf3fca3acb321..435a09ce42cd4d17ef69fe6d43b1238d4bb39c2d 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/impl/ConnectionManager.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/impl/ConnectionManager.java
@@ -593,8 +593,9 @@ class ConnectionManager {
         Long id = Long.valueOf(_context.random().nextLong(Packet.MAX_STREAM_ID-1)+1);
         PacketLocal packet = new PacketLocal(_context, peer);
         packet.setSendStreamId(id.longValue());
-        packet.setFlag(Packet.FLAG_ECHO);
-        packet.setFlag(Packet.FLAG_SIGNATURE_INCLUDED);
+        packet.setFlag(Packet.FLAG_ECHO |
+                       Packet.FLAG_NO_ACK |
+                       Packet.FLAG_SIGNATURE_INCLUDED);
         packet.setOptionalFrom(_session.getMyDestination());
         //if ( (keyToUse != null) && (tagsToSend != null) ) {
         //    packet.setKeyUsed(keyToUse);
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/impl/Packet.java b/apps/streaming/java/src/net/i2p/client/streaming/impl/Packet.java
index 010674c9b899ab99231ce60477fa6a6c60a0db8e..16784d38fcdb1155a1dd1dd7fef93df7a2a17f08 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/impl/Packet.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/impl/Packet.java
@@ -295,8 +295,15 @@ class Packet {
      */
     public boolean isFlagSet(int flag) { return 0 != (_flags & flag); }
 
+    /**
+     *  @param flag bitmask of any flag(s)
+     */
     public void setFlag(int flag) { _flags |= flag; }
 
+    /**
+     *  @param flag bitmask of any flag(s)
+     *  @param set true to set, false to clear
+     */
     public void setFlag(int flag, boolean set) { 
         if (set)
             _flags |= flag; 
@@ -304,7 +311,7 @@ class Packet {
             _flags &= ~flag;
     }
 
-    public void setFlags(int flags) { _flags = flags; } 
+    private void setFlags(int flags) { _flags = flags; } 
 
     /** the signature on the packet (only included if the flag for it is set)
      * @return signature on the packet if the flag for signatures is set
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/impl/PacketHandler.java b/apps/streaming/java/src/net/i2p/client/streaming/impl/PacketHandler.java
index 4316f59ef86f72f9635be99a076d3012a522335e..c40afe453a4b41d38b31c07397f90e5c52fb4afa 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/impl/PacketHandler.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/impl/PacketHandler.java
@@ -349,8 +349,7 @@ class PacketHandler {
             }
         } else {
             PacketLocal pong = new PacketLocal(_context, packet.getOptionalFrom());
-            pong.setFlag(Packet.FLAG_ECHO, true);
-            pong.setFlag(Packet.FLAG_SIGNATURE_INCLUDED, false);
+            pong.setFlag(Packet.FLAG_ECHO | Packet.FLAG_NO_ACK);
             pong.setReceiveStreamId(packet.getSendStreamId());
             _manager.getPacketQueue().enqueue(pong);
         }
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/impl/PacketQueue.java b/apps/streaming/java/src/net/i2p/client/streaming/impl/PacketQueue.java
index 399d9f71812cc97459158c12bc4d9fec8755831c..dd44087cec572ac4f5dd46ac33d2c59f3b97bbea 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/impl/PacketQueue.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/impl/PacketQueue.java
@@ -109,7 +109,13 @@ class PacketQueue {
                 options.setTagsToSend(INITIAL_TAGS_TO_SEND);
                 options.setTagThreshold(MIN_TAG_THRESHOLD);
             } else if (packet.isFlagSet(FLAGS_FINAL_TAGS)) {
-                options.setSendLeaseSet(false);
+                if (packet.isFlagSet(Packet.FLAG_ECHO)) {
+                    // Send LS for PING, not for PONG
+                    if (packet.getSendStreamId() <= 0)  // pong
+                        options.setSendLeaseSet(false);
+                } else {
+                    options.setSendLeaseSet(false);
+                }
                 options.setTagsToSend(FINAL_TAGS_TO_SEND);
                 options.setTagThreshold(FINAL_TAG_THRESHOLD);
             } else {
diff --git a/core/java/src/net/i2p/client/ClientWriterRunner.java b/core/java/src/net/i2p/client/ClientWriterRunner.java
index d25f741c5cf10b2f1308f48015b3aa1e6c20cf71..35ecce19d4f5bc1daf4ec196f3f3c8e64798a230 100644
--- a/core/java/src/net/i2p/client/ClientWriterRunner.java
+++ b/core/java/src/net/i2p/client/ClientWriterRunner.java
@@ -8,10 +8,12 @@ import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
 
+import net.i2p.I2PAppContext;
 import net.i2p.data.i2cp.I2CPMessage;
 import net.i2p.data.i2cp.I2CPMessageException;
 import net.i2p.internal.PoisonI2CPMessage;
 import net.i2p.util.I2PAppThread;
+import net.i2p.util.Log;
 
 /**
  * Copied from net.i2p.router.client
@@ -25,15 +27,24 @@ class ClientWriterRunner implements Runnable {
     private final I2PSessionImpl _session;
     private final BlockingQueue<I2CPMessage> _messagesToWrite;
     private static final AtomicLong __Id = new AtomicLong();
+    //private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(ClientWriterRunner.class);
 
     private static final int MAX_QUEUE_SIZE = 32;
     private static final long MAX_SEND_WAIT = 10*1000;
     
-    /** starts the thread too */
+    /**
+     *  As of 0.9.11 does not start the thread, caller must call startWriting()
+     */
     public ClientWriterRunner(OutputStream out, I2PSessionImpl session) {
         _out = new BufferedOutputStream(out);
         _session = session;
         _messagesToWrite = new LinkedBlockingQueue<I2CPMessage>(MAX_QUEUE_SIZE);
+    }
+
+    /**
+     *  @since 0.9.11
+     */
+    public void startWriting() {
         Thread t = new I2PAppThread(this, "I2CP Client Writer " + __Id.incrementAndGet(), true);
         t.start();
     }
@@ -76,7 +87,8 @@ class ClientWriterRunner implements Runnable {
             // only thread, we don't need synchronized
             try {
                 msg.writeMessage(_out);
-                _out.flush();
+                if (_messagesToWrite.isEmpty())
+                    _out.flush();
             } catch (I2CPMessageException ime) {
                 _session.propogateError("Error writing out the message", ime);
                 _session.disconnect();
diff --git a/core/java/src/net/i2p/client/HostReplyMessageHandler.java b/core/java/src/net/i2p/client/HostReplyMessageHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..94cce4758cdb322b67311873079c920ddb1b7491
--- /dev/null
+++ b/core/java/src/net/i2p/client/HostReplyMessageHandler.java
@@ -0,0 +1,38 @@
+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.HostReplyMessage;
+import net.i2p.util.Log;
+
+import net.i2p.data.Destination;
+
+/**
+ * Handle I2CP dest replies from the router
+ *
+ * @since 0.9.11
+ */
+class HostReplyMessageHandler extends HandlerImpl {
+
+    public HostReplyMessageHandler(I2PAppContext ctx) {
+        super(ctx, HostReplyMessage.MESSAGE_TYPE);
+    }
+    
+    public void handleMessage(I2CPMessage message, I2PSessionImpl session) {
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Handle message " + message);
+        HostReplyMessage msg = (HostReplyMessage) message;
+        Destination d = msg.getDestination();
+        long id = msg.getReqID();
+        if (d != null) {
+            session.destReceived(id, d);
+        } else {
+            session.destLookupFailed(id);
+        }
+    }
+}
diff --git a/core/java/src/net/i2p/client/I2PClientMessageHandlerMap.java b/core/java/src/net/i2p/client/I2PClientMessageHandlerMap.java
index 7bc912b97fe44a693125600c56492634d97b5a82..bd2de83d6343d436fe9e04432ecb2ac14455776c 100644
--- a/core/java/src/net/i2p/client/I2PClientMessageHandlerMap.java
+++ b/core/java/src/net/i2p/client/I2PClientMessageHandlerMap.java
@@ -13,6 +13,7 @@ import net.i2p.I2PAppContext;
 import net.i2p.data.i2cp.BandwidthLimitsMessage;
 import net.i2p.data.i2cp.DestReplyMessage;
 import net.i2p.data.i2cp.DisconnectMessage;
+import net.i2p.data.i2cp.HostReplyMessage;
 import net.i2p.data.i2cp.MessagePayloadMessage;
 import net.i2p.data.i2cp.MessageStatusMessage;
 import net.i2p.data.i2cp.RequestLeaseSetMessage;
@@ -40,6 +41,7 @@ class I2PClientMessageHandlerMap {
         highest = Math.max(highest, MessageStatusMessage.MESSAGE_TYPE);
         highest = Math.max(highest, SetDateMessage.MESSAGE_TYPE);
         highest = Math.max(highest, DestReplyMessage.MESSAGE_TYPE);
+        highest = Math.max(highest, HostReplyMessage.MESSAGE_TYPE);
         highest = Math.max(highest, BandwidthLimitsMessage.MESSAGE_TYPE);
         highest = Math.max(highest, RequestVariableLeaseSetMessage.MESSAGE_TYPE);
         
@@ -53,6 +55,7 @@ class I2PClientMessageHandlerMap {
         _handlers[DestReplyMessage.MESSAGE_TYPE] = new DestReplyMessageHandler(context);
         _handlers[BandwidthLimitsMessage.MESSAGE_TYPE] = new BWLimitsMessageHandler(context);
         _handlers[RequestVariableLeaseSetMessage.MESSAGE_TYPE] = new RequestVariableLeaseSetMessageHandler(context);
+        _handlers[HostReplyMessage.MESSAGE_TYPE] = new HostReplyMessageHandler(context);
     }
 
     public I2CPMessageHandler getHandler(int messageTypeId) {
diff --git a/core/java/src/net/i2p/client/I2PSession.java b/core/java/src/net/i2p/client/I2PSession.java
index 8822ea3aec23e1a36c00ac37457edb29d07ba497..d70e4041f2da03f6e672991894fabce51eaf3aae 100644
--- a/core/java/src/net/i2p/client/I2PSession.java
+++ b/core/java/src/net/i2p/client/I2PSession.java
@@ -214,6 +214,7 @@ public interface I2PSession {
     public Destination lookupDest(Hash h) throws I2PSessionException;
 
     /**
+     *  Lookup a Destination by Hash.
      *  Blocking.
      *  @param maxWait ms
      *  @since 0.8.3
@@ -221,6 +222,68 @@ public interface I2PSession {
      */
     public Destination lookupDest(Hash h, long maxWait) throws I2PSessionException;
 
+    /**
+     *  Ask the router to lookup a Destination by host name.
+     *  Blocking. Waits a max of 10 seconds by default.
+     *
+     *  This only makes sense for a b32 hostname, OR outside router context.
+     *  Inside router context, just query the naming service.
+     *  Outside router context, this does NOT query the context naming service.
+     *  Do that first if you expect a local addressbook.
+     *
+     *  This will log a warning for non-b32 in router context.
+     *
+     *  Suggested implementation:
+     *
+     *<pre>
+     *  if (name.length() == 60 && name.toLowerCase(Locale.US).endsWith(".b32.i2p")) {
+     *      if (session != null)
+     *          return session.lookup(Hash.create(Base32.decode(name.toLowerCase(Locale.US).substring(0, 52))));
+     *      else
+     *          return ctx.namingService().lookup(name); // simple session for xxx.b32.i2p handled by naming service (optional if you need lookup w/o an existing session)
+     *  } else if (ctx.isRouterContext()) {
+     *      return ctx.namingService().lookup(name); // hostname from router's naming service
+     *  } else {
+     *      Destination d = ctx.namingService().lookup(name); // local naming svc, optional
+     *      if (d != null)
+     *          return d;
+     *      if (session != null)
+     *          return session.lookup(name);
+     *      // simple session (optional if you need lookup w/o an existing session)
+     *      Destination rv = null;
+     *      I2PClient client = new I2PSimpleClient();
+     *      Properties opts = new Properties();
+     *      opts.put(I2PClient.PROP_TCP_HOST, host);
+     *      opts.put(I2PClient.PROP_TCP_PORT, port);
+     *      I2PSession session = null;
+     *      try {
+     *          session = client.createSession(null, opts);
+     *          session.connect();
+     *          rv = session.lookupDest(name);
+     *      } finally {
+     *          if (session != null)
+     *              session.destroySession();
+     *      }
+     *      return rv;
+     *  }
+     *</pre>
+     *
+     *  Requires router side to be 0.9.11 or higher. If the router is older,
+     *  this will return null immediately.
+     *
+     *  @since 0.9.11
+     */
+    public Destination lookupDest(String name) throws I2PSessionException;
+
+    /**
+     *  Ask the router to lookup a Destination by host name.
+     *  Blocking. See above for details.
+     *  @param maxWait ms
+     *  @since 0.9.11
+     *  @return null on failure
+     */
+    public Destination lookupDest(String name, long maxWait) throws I2PSessionException;
+
     /**
      *  Pass updated options to the router.
      *  Does not remove properties previously present but missing from this options parameter.
diff --git a/core/java/src/net/i2p/client/I2PSessionImpl.java b/core/java/src/net/i2p/client/I2PSessionImpl.java
index 8cf7ad095e827365bdbd093f3a123d51443fcaf3..293bc91636d83d1b2d4271f607da0078e93e90bc 100644
--- a/core/java/src/net/i2p/client/I2PSessionImpl.java
+++ b/core/java/src/net/i2p/client/I2PSessionImpl.java
@@ -19,13 +19,16 @@ import java.net.UnknownHostException;
 import java.security.GeneralSecurityException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Properties;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import net.i2p.CoreVersion;
 import net.i2p.I2PAppContext;
+import net.i2p.data.Base32;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.Destination;
 import net.i2p.data.Hash;
@@ -35,6 +38,7 @@ import net.i2p.data.SigningPrivateKey;
 import net.i2p.data.i2cp.DestLookupMessage;
 import net.i2p.data.i2cp.GetBandwidthLimitsMessage;
 import net.i2p.data.i2cp.GetDateMessage;
+import net.i2p.data.i2cp.HostLookupMessage;
 import net.i2p.data.i2cp.I2CPMessage;
 import net.i2p.data.i2cp.I2CPMessageReader;
 import net.i2p.data.i2cp.MessagePayloadMessage;
@@ -46,6 +50,7 @@ import net.i2p.util.I2PAppThread;
 import net.i2p.util.I2PSSLSocketFactory;
 import net.i2p.util.LHMCache;
 import net.i2p.util.Log;
+import net.i2p.util.OrderedProperties;
 import net.i2p.util.SimpleTimer;
 import net.i2p.util.VersionComparator;
 
@@ -98,12 +103,13 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
 
     /** hashes of lookups we are waiting for */
     protected final LinkedBlockingQueue<LookupWaiter> _pendingLookups = new LinkedBlockingQueue<LookupWaiter>();
+    private final AtomicInteger _lookupID = new AtomicInteger();
     protected final Object _bwReceivedLock = new Object();
     protected volatile int[] _bwLimits;
     
     protected final I2PClientMessageHandlerMap _handlerMap;
     
-    /** used to seperate things out so we can get rid of singletons */
+    /** used to separate things out so we can get rid of singletons */
     protected final I2PAppContext _context;
 
     /** monitor for waiting until a lease set has been granted */
@@ -114,6 +120,8 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
      */
     protected enum State {
         OPENING,
+        /** @since 0.9.11 */
+        GOTDATE,
         OPEN,
         CLOSING,
         CLOSED
@@ -122,11 +130,6 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
     private State _state = State.CLOSED;
     protected final Object _stateLock = new Object();
 
-    /** have we received the current date from the router yet? */
-    private volatile boolean _dateReceived;
-    /** lock that we wait upon, that the SetDateMessageHandler notifies */
-    private final Object _dateReceivedLock = new Object();
-
     /** 
      * thread that we tell when new messages are available who then tells us 
      * to fetch them.  The point of this is so that the fetch doesn't block the
@@ -139,14 +142,20 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
     private boolean _isReduced;
     private final boolean _fastReceive;
     private volatile boolean _routerSupportsFastReceive;
+    private volatile boolean _routerSupportsHostLookup;
 
     /**
+     *  Since 0.9.11, key is either a Hash or a String
      *  @since 0.8.9
      */
-    private static final Map<Hash, Destination> _lookupCache = new LHMCache<Hash, Destination>(16);
+    private static final Map<Object, Destination> _lookupCache = new LHMCache<Object, Destination>(16);
+    private static final String MIN_HOST_LOOKUP_VERSION = "0.9.11";
+    private static final boolean TEST_LOOKUP = false;
 
     /** SSL interface (only) @since 0.8.3 */
     protected static final String PROP_ENABLE_SSL = "i2cp.SSL";
+    protected static final String PROP_USER = "i2cp.username";
+    protected static final String PROP_PW = "i2cp.password";
 
     private static final long VERIFY_USAGE_TIME = 60*1000;
 
@@ -159,9 +168,15 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
         _routerSupportsFastReceive = _context.isRouterContext() ||
                                      (routerVersion != null && routerVersion.length() > 0 &&
                                       VersionComparator.comp(routerVersion, MIN_FAST_VERSION) >= 0);
-        _dateReceived = true;
-        synchronized (_dateReceivedLock) {
-            _dateReceivedLock.notifyAll();
+        _routerSupportsHostLookup = _context.isRouterContext() ||
+                                    TEST_LOOKUP ||
+                                     (routerVersion != null && routerVersion.length() > 0 &&
+                                      VersionComparator.comp(routerVersion, MIN_HOST_LOOKUP_VERSION) >= 0);
+        synchronized (_stateLock) {
+            if (_state == State.OPENING) {
+                _state = State.GOTDATE;
+                _stateLock.notifyAll();
+            }
         }
     }
 
@@ -205,6 +220,8 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
             _privateKey = null;
             _signingPrivateKey = null;
         }
+        _routerSupportsFastReceive = _context.isRouterContext();
+        _routerSupportsHostLookup = _context.isRouterContext();
     }
 
     /**
@@ -239,12 +256,12 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
         // auto-add auth if required, not set in the options, and we are not in the same JVM
         if ((!_context.isRouterContext()) &&
             _context.getBooleanProperty("i2cp.auth") &&
-            ((!opts.containsKey("i2cp.username")) || (!opts.containsKey("i2cp.password")))) {
-            String configUser = _context.getProperty("i2cp.username");
-            String configPW = _context.getProperty("i2cp.password");
+            ((!opts.containsKey(PROP_USER)) || (!opts.containsKey(PROP_PW)))) {
+            String configUser = _context.getProperty(PROP_USER);
+            String configPW = _context.getProperty(PROP_PW);
             if (configUser != null && configPW != null) {
-                options.setProperty("i2cp.username", configUser);
-                options.setProperty("i2cp.password", configPW);
+                options.setProperty(PROP_USER, configUser);
+                options.setProperty(PROP_PW, configPW);
             }
         }
         if (options.getProperty(I2PClient.PROP_FAST_RECEIVE) == null)
@@ -399,6 +416,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
                         loop = false;
                         break;
                     case OPENING:
+                    case GOTDATE:
                         wasOpening = true;
                         try {
                             _stateLock.wait(10*1000);
@@ -455,6 +473,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
                     out.write(I2PClient.PROTOCOL_BYTE);
                     out.flush();
                     _writer = new ClientWriterRunner(out, this);
+                    _writer.startWriting();
                     InputStream in = new BufferedInputStream(_socket.getInputStream(), BUF_SIZE);
                     _reader = new I2CPMessageReader(in, this);
                 }
@@ -462,26 +481,23 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
             if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "before startReading");
             _reader.startReading();
             if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Before getDate");
-            sendMessage(new GetDateMessage(CoreVersion.VERSION));
-            if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "After getDate / begin waiting for a response");
-            int waitcount = 0;
-            while (!_dateReceived) {
-                if (waitcount++ > 30) {
-                    throw new IOException("No handshake received from the router");
-                }
-                synchronized (_dateReceivedLock) {
-                    // InterruptedException caught below
-                    _dateReceivedLock.wait(1000);
-                }
+            Properties auth = null;
+            if ((!_context.isRouterContext()) && _options.containsKey(PROP_USER) && _options.containsKey(PROP_PW)) {
+                // Only supported by routers 0.9.11 or higher, but we don't know the version yet.	
+                // Auth will also be sent in the SessionConfig.
+                auth = new OrderedProperties();
+                auth.setProperty(PROP_USER, _options.getProperty(PROP_USER));
+                auth.setProperty(PROP_PW, _options.getProperty(PROP_PW));
             }
-            if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "After received a SetDate response");
+            sendMessage(new GetDateMessage(CoreVersion.VERSION, auth));
+            waitForDate();
 
             if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Before producer.connect()");
             _producer.connect(this);
             if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "After producer.connect()");
 
             // wait until we have created a lease set
-            waitcount = 0;
+            int waitcount = 0;
             while (_leaseSet == null) {
                 if (waitcount++ > 5*60) {
                     throw new IOException("No tunnels built after waiting 5 minutes. Your network connection may be down, or there is severe network congestion.");
@@ -524,6 +540,28 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
         }
     }
 
+    /**
+     *  @since 0.9.11 moved from connect()
+     */
+    protected void waitForDate() throws InterruptedException, IOException {
+            if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "After getDate / begin waiting for a response");
+            int waitcount = 0;
+            while (true) {
+                if (waitcount++ > 30) {
+                    throw new IOException("No handshake received from the router");
+                }
+                synchronized(_stateLock) {
+                    if (_state == State.GOTDATE)
+                        break;
+                    if (_state != State.OPENING)
+                        throw new IOException("Socket closed");
+                    // InterruptedException caught by caller
+                    _stateLock.wait(1000);
+                }
+            }
+            if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "After received a SetDate response");
+    }
+
     /**
      * Pull the unencrypted data from the message that we've already prefetched and
      * notified the user that its available.
@@ -890,13 +928,16 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
      * Will interrupt a connect in progress.
      */
     protected void disconnect() {
+        State oldState;
         synchronized(_stateLock) {
             if (_state == State.CLOSING || _state == State.CLOSED)
                 return;
+            oldState = _state;
             changeState(State.CLOSING);
         }
         if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Disconnect() called", new Exception("Disconnect"));
-        if (shouldReconnect()) {
+        // don't try to reconnect if it failed before GETTDATE
+        if (oldState != State.OPENING && shouldReconnect()) {
             if (reconnect()) {
                 if (_log.shouldLog(Log.INFO)) _log.info(getPrefix() + "I2CP reconnection successful");
                 return;
@@ -963,14 +1004,17 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
         return buf.toString();
     }
 
-    /** called by the message handler */
+    /**
+     *  Called by the message handler
+     *  on reception of DestReplyMessage
+     */
     void destReceived(Destination d) {
         Hash h = d.calculateHash();
         synchronized (_lookupCache) {
             _lookupCache.put(h, d);
         }
         for (LookupWaiter w : _pendingLookups) {
-            if (w.hash.equals(h)) {
+            if (h.equals(w.hash)) {
                 w.destination = d;
                 synchronized (w) {
                     w.notifyAll();
@@ -979,10 +1023,52 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
         }
     }
 
-    /** called by the message handler */
+    /**
+     *  Called by the message handler
+     *  on reception of DestReplyMessage
+     */
     void destLookupFailed(Hash h) {
         for (LookupWaiter w : _pendingLookups) {
-            if (w.hash.equals(h)) {
+            if (h.equals(w.hash)) {
+                synchronized (w) {
+                    w.notifyAll();
+                }
+            }
+        }
+    }
+
+    /**
+     *  Called by the message handler
+     *  on reception of HostReplyMessage
+     *  @since 0.9.11
+     */
+    void destReceived(long nonce, Destination d) {
+        // notify by hash
+        destReceived(d);
+        // notify by nonce
+        for (LookupWaiter w : _pendingLookups) {
+            if (nonce == w.nonce) {
+                w.destination = d;
+                if (w.name != null) {
+                    synchronized (_lookupCache) {
+                        _lookupCache.put(w.name, d);
+                    }
+                }
+                synchronized (w) {
+                    w.notifyAll();
+                }
+            }
+        }
+    }
+
+    /**
+     *  Called by the message handler
+     *  on reception of HostReplyMessage
+     *  @since 0.9.11
+     */
+    void destLookupFailed(long nonce) {
+        for (LookupWaiter w : _pendingLookups) {
+            if (nonce == w.nonce) {
                 synchronized (w) {
                     w.notifyAll();
                 }
@@ -1003,13 +1089,31 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
      *  @since 0.8.3
      */
     private static class LookupWaiter {
-        /** the request */
+        /** the request (Hash mode) */
         public final Hash hash;
+        /** the request (String mode) */
+        public final String name;
+        /** the request (nonce mode) */
+        public final long nonce;
         /** the reply */
         public volatile Destination destination;
 
         public LookupWaiter(Hash h) {
+            this(h, -1);
+        }
+
+        /** @since 0.9.11 */
+        public LookupWaiter(Hash h, long nonce) {
             this.hash = h;
+            this.name = null;
+            this.nonce = nonce;
+        }
+
+        /** @since 0.9.11 */
+        public LookupWaiter(String name, long nonce) {
+            this.hash = null;
+            this.name = name;
+            this.nonce = nonce;
         }
     }
 
@@ -1037,12 +1141,119 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
             if (rv != null)
                 return rv;
         }
-        if (isClosed())
+        if (isClosed()) {
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Session closed, cannot lookup " + h);
             return null;
-        LookupWaiter waiter = new LookupWaiter(h);
+        }
+        LookupWaiter waiter;
+        long nonce;
+        if (_routerSupportsHostLookup) {
+            nonce = _lookupID.incrementAndGet() & 0x7fffffff;
+            waiter = new LookupWaiter(h, nonce);
+        } else {
+            nonce = 0; // won't be used
+            waiter = new LookupWaiter(h);
+        }
         _pendingLookups.offer(waiter);
         try {
-            sendMessage(new DestLookupMessage(h));
+            if (_routerSupportsHostLookup) {
+                if (_log.shouldLog(Log.INFO))
+                    _log.info("Sending HostLookup for " + h);
+                SessionId id = _sessionId;
+                if (id == null)
+                    id = new SessionId(65535);
+                sendMessage(new HostLookupMessage(id, h, nonce, maxWait));
+            } else {
+                if (_log.shouldLog(Log.INFO))
+                    _log.info("Sending DestLookup for " + h);
+                sendMessage(new DestLookupMessage(h));
+            }
+            try {
+                synchronized (waiter) {
+                    waiter.wait(maxWait);
+                }
+            } catch (InterruptedException ie) {
+                throw new I2PSessionException("Interrupted", ie);
+            }
+        } finally {
+            _pendingLookups.remove(waiter);
+        }
+        return waiter.destination;
+    }
+
+    /**
+     *  Ask the router to lookup a Destination by host name.
+     *  Blocking. Waits a max of 10 seconds by default.
+     *
+     *  This only makes sense for a b32 hostname, OR outside router context.
+     *  Inside router context, just query the naming service.
+     *  Outside router context, this does NOT query the context naming service.
+     *  Do that first if you expect a local addressbook.
+     *
+     *  This will log a warning for non-b32 in router context.
+     *
+     *  See interface for suggested implementation.
+     *
+     *  Requires router side to be 0.9.11 or higher. If the router is older,
+     *  this will return null immediately.
+     *
+     *  @since 0.9.11
+     */
+    public Destination lookupDest(String name) throws I2PSessionException {
+        return lookupDest(name, 10*1000);
+    }
+
+    /**
+     *  Ask the router to lookup a Destination by host name.
+     *  Blocking. See above for details.
+     *  @param maxWait ms
+     *  @since 0.9.11
+     *  @return null on failure
+     */
+    public Destination lookupDest(String name, long maxWait) throws I2PSessionException {
+        if (name.length() == 0)
+            return null;
+        // Shortcut for b64
+        if (name.length() >= 516) {
+            try {
+                return new Destination(name);
+            } catch (DataFormatException dfe) {
+                return null;
+            }
+        }
+        // won't fit in Mapping
+        if (name.length() >= 256 && !_context.isRouterContext())
+            return null;
+        synchronized (_lookupCache) {
+            Destination rv = _lookupCache.get(name);
+            if (rv != null)
+                return rv;
+        }
+        if (isClosed()) {
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Session closed, cannot lookup " + name);
+            return null;
+        }
+        if (!_routerSupportsHostLookup) {
+            // do them a favor and convert to Hash lookup
+            if (name.length() == 60 && name.toLowerCase(Locale.US).endsWith(".b32.i2p"))
+                return lookupDest(Hash.create(Base32.decode(name.toLowerCase(Locale.US).substring(0, 52))), maxWait);
+            // else unsupported
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Router does not support HostLookup for " + name);
+            return null;
+        }
+        int nonce = _lookupID.incrementAndGet() & 0x7fffffff;
+        LookupWaiter waiter = new LookupWaiter(name, nonce);
+        _pendingLookups.offer(waiter);
+        try {
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Sending HostLookup for " + name);
+            SessionId id = _sessionId;
+            if (id == null)
+                id = new SessionId(65535);
+            sendMessage(new HostLookupMessage(id, name, nonce, maxWait));
             try {
                 synchronized (waiter) {
                     waiter.wait(maxWait);
diff --git a/core/java/src/net/i2p/client/I2PSimpleSession.java b/core/java/src/net/i2p/client/I2PSimpleSession.java
index e3a911e19a7a343cc7388c161bdb9c68011c41ef..b6f8bb7eb40998cbc2a383063d304c71de63bd5d 100644
--- a/core/java/src/net/i2p/client/I2PSimpleSession.java
+++ b/core/java/src/net/i2p/client/I2PSimpleSession.java
@@ -14,13 +14,20 @@ import java.net.UnknownHostException;
 import java.security.GeneralSecurityException;
 import java.util.Properties;
 
+import net.i2p.CoreVersion;
 import net.i2p.I2PAppContext;
 import net.i2p.data.i2cp.BandwidthLimitsMessage;
 import net.i2p.data.i2cp.DestReplyMessage;
+import net.i2p.data.i2cp.DisconnectMessage;
+import net.i2p.data.i2cp.GetDateMessage;
+import net.i2p.data.i2cp.HostReplyMessage;
 import net.i2p.data.i2cp.I2CPMessageReader;
+import net.i2p.data.i2cp.SetDateMessage;
 import net.i2p.internal.InternalClientManager;
 import net.i2p.internal.QueuedI2CPMessageReader;
 import net.i2p.util.I2PSSLSocketFactory;
+import net.i2p.util.Log;
+import net.i2p.util.OrderedProperties;
 
 /**
  * Create a new session for doing naming and bandwidth queries only. Do not create a Destination.
@@ -68,6 +75,7 @@ class I2PSimpleSession extends I2PSessionImpl2 {
                     // the following may throw an I2PSessionException
                     _queue = mgr.connect();
                     _reader = new QueuedI2CPMessageReader(_queue, this);
+                    _reader.startReading();
                 } else {
                     if (Boolean.parseBoolean(getOptions().getProperty(PROP_ENABLE_SSL))) {
                         try {
@@ -85,14 +93,47 @@ class I2PSimpleSession extends I2PSessionImpl2 {
                     out.write(I2PClient.PROTOCOL_BYTE);
                     out.flush();
                     _writer = new ClientWriterRunner(out, this);
+                    _writer.startWriting();
                     InputStream in = new BufferedInputStream(_socket.getInputStream(), BUF_SIZE);
                     _reader = new I2CPMessageReader(in, this);
+                    _reader.startReading();
                 }
             }
+            // must be out of synch block for writer to get unblocked
+            if (!_context.isRouterContext()) {
+                Properties opts = getOptions();
+                // Send auth message if required
+                // Auth was not enforced on a simple session until 0.9.11
+                // We will get disconnected for router version < 0.9.11 since it doesn't
+                // support the AuthMessage
+                if ((!opts.containsKey(PROP_USER)) && (!opts.containsKey(PROP_PW))) {
+                    // auto-add auth if not set in the options
+                    String configUser = _context.getProperty(PROP_USER);
+                    String configPW = _context.getProperty(PROP_PW);
+                    if (configUser != null && configPW != null) {
+                        opts.setProperty(PROP_USER, configUser);
+                        opts.setProperty(PROP_PW, configPW);
+                    }
+                }
+                if (opts.containsKey(PROP_USER) && opts.containsKey(PROP_PW)) {
+                    Properties auth = new OrderedProperties();
+                    auth.setProperty(PROP_USER, opts.getProperty(PROP_USER));
+                    auth.setProperty(PROP_PW, opts.getProperty(PROP_PW));
+                    sendMessage(new GetDateMessage(CoreVersion.VERSION, auth));
+                } else {
+                    // we must now send a GetDate even in SimpleSession, or we won't know
+                    // what version we are talking with and cannot use HostLookup
+                    sendMessage(new GetDateMessage(CoreVersion.VERSION));
+                }
+                waitForDate();
+            }
             // we do not receive payload messages, so we do not need an AvailabilityNotifier
             // ... or an Idle timer, or a VerifyUsage
-            _reader.startReading();
             success = true;
+            if (_log.shouldLog(Log.INFO))
+                _log.info(getPrefix() + " simple session connected");
+        } catch (InterruptedException ie) {
+            throw new I2PSessionException("Interrupted", ie);
         } catch (UnknownHostException uhe) {
             throw new I2PSessionException(getPrefix() + "Cannot connect to the router on " + _hostname + ':' + _portNum, uhe);
         } catch (IOException ioe) {
@@ -115,9 +156,15 @@ class I2PSimpleSession extends I2PSessionImpl2 {
     private static class SimpleMessageHandlerMap extends I2PClientMessageHandlerMap {
         public SimpleMessageHandlerMap(I2PAppContext context) {
             int highest = Math.max(DestReplyMessage.MESSAGE_TYPE, BandwidthLimitsMessage.MESSAGE_TYPE);
+            highest = Math.max(highest, DisconnectMessage.MESSAGE_TYPE);
+            highest = Math.max(highest, HostReplyMessage.MESSAGE_TYPE);
+            highest = Math.max(highest, SetDateMessage.MESSAGE_TYPE);
             _handlers = new I2CPMessageHandler[highest+1];
             _handlers[DestReplyMessage.MESSAGE_TYPE] = new DestReplyMessageHandler(context);
             _handlers[BandwidthLimitsMessage.MESSAGE_TYPE] = new BWLimitsMessageHandler(context);
+            _handlers[DisconnectMessage.MESSAGE_TYPE] = new DisconnectMessageHandler(context);
+            _handlers[HostReplyMessage.MESSAGE_TYPE] = new HostReplyMessageHandler(context);
+            _handlers[SetDateMessage.MESSAGE_TYPE] = new SetDateMessageHandler(context);
         }
     }
 }
diff --git a/core/java/src/net/i2p/client/naming/LookupDest.java b/core/java/src/net/i2p/client/naming/LookupDest.java
index 79f269efc1db8d5d63da0c7d4733ae156561b128..c8e5381c5e35a774274ee876e04abd448cb46eca 100644
--- a/core/java/src/net/i2p/client/naming/LookupDest.java
+++ b/core/java/src/net/i2p/client/naming/LookupDest.java
@@ -34,6 +34,9 @@ import net.i2p.data.Hash;
 class LookupDest {
 
     private static final long DEFAULT_TIMEOUT = 15*1000;
+    private static final String PROP_ENABLE_SSL = "i2cp.SSL";
+    private static final String PROP_USER = "i2cp.username";
+    private static final String PROP_PW = "i2cp.password";
 
     protected LookupDest(I2PAppContext context) {}
 
@@ -61,12 +64,23 @@ class LookupDest {
         Destination rv = null;
         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);
+        if (!ctx.isRouterContext()) {
+            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);
+            s = ctx.getProperty(PROP_ENABLE_SSL);
+            if (s != null)
+                opts.put(PROP_ENABLE_SSL, s);
+            s = ctx.getProperty(PROP_USER);
+            if (s != null)
+                opts.put(PROP_USER, s);
+            s = ctx.getProperty(PROP_PW);
+            if (s != null)
+                opts.put(PROP_PW, s);
+        }
         I2PSession session = null;
         try {
             session = client.createSession(null, opts);
diff --git a/core/java/src/net/i2p/data/i2cp/GetDateMessage.java b/core/java/src/net/i2p/data/i2cp/GetDateMessage.java
index f76aaaf2565aa61fcb225b61d3b9a77a7f27c8f0..087062cbb2cecaea1d65b2fa8aebd31bc4f00655 100644
--- a/core/java/src/net/i2p/data/i2cp/GetDateMessage.java
+++ b/core/java/src/net/i2p/data/i2cp/GetDateMessage.java
@@ -12,19 +12,24 @@ package net.i2p.data.i2cp;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.Map;
+import java.util.Properties;
 
 import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
+import net.i2p.util.OrderedProperties;
 
 /**
- * Request the other side to send us what they think the current time is/
+ * Request the other side to send us what they think the current time is.
  * Only supported from client to router.
  *
  * Since 0.8.7, optionally include a version string.
+ * Since 0.9.10, optionally include options.
  */
 public class GetDateMessage extends I2CPMessageImpl {
     public final static int MESSAGE_TYPE = 32;
     private String _version;
+    private Properties _options;
 
     public GetDateMessage() {
         super();
@@ -39,6 +44,21 @@ public class GetDateMessage extends I2CPMessageImpl {
         _version = version;
     }
 
+    /**
+     *  @param version the client's version String to be sent to the router; may be null;
+     *                 must be non-null if options is non-null and non-empty.
+     *  @param options Client options to be sent to the router; primarily for authentication; may be null;
+     *                 keys and values 255 bytes (not chars) max each
+     *  @since 0.9.10
+     */
+    public GetDateMessage(String version, Properties options) {
+        super();
+        if (version == null && options != null && !options.isEmpty())
+            throw new IllegalArgumentException();
+        _version = version;
+        _options = options;
+    }
+
     /**
      *  @return may be null
      *  @since 0.8.7
@@ -47,11 +67,24 @@ public class GetDateMessage extends I2CPMessageImpl {
         return _version;
     }
 
+    /**
+     *  Retrieve any configuration options for the connection.
+     *  Primarily for authentication.
+     *
+     *  @return may be null
+     *  @since 0.9.10
+     */
+    public Properties getOptions() {
+        return _options;
+    }
+
     @Override
     protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException {
         if (size > 0) {
             try {
                 _version = DataHelper.readString(in);
+                if (size > 1 + _version.length())  // assume ascii
+                    _options = DataHelper.readProperties(in);
             } catch (DataFormatException dfe) {
                 throw new I2CPMessageException("Bad version string", dfe);
             }
@@ -62,9 +95,11 @@ public class GetDateMessage extends I2CPMessageImpl {
     protected byte[] doWriteMessage() throws I2CPMessageException, IOException {
         if (_version == null)
             return new byte[0];
-        ByteArrayOutputStream os = new ByteArrayOutputStream(16);
+        ByteArrayOutputStream os = new ByteArrayOutputStream(_options != null ? 128 : 16);
         try {
             DataHelper.writeString(os, _version);
+            if (_options != null && !_options.isEmpty())
+                DataHelper.writeProperties(os, _options, true);  // UTF-8
         } catch (DataFormatException dfe) {
             throw new I2CPMessageException("Error writing out the message data", dfe);
         }
@@ -80,6 +115,16 @@ public class GetDateMessage extends I2CPMessageImpl {
         StringBuilder buf = new StringBuilder();
         buf.append("[GetDateMessage]");
         buf.append("\n\tVersion: ").append(_version);
+        if (_options != null && !_options.isEmpty()) {
+            buf.append("\n\tOptions: #: ").append(_options.size());
+            Properties sorted = new OrderedProperties();
+            sorted.putAll(_options);
+            for (Map.Entry<Object, Object> e : sorted.entrySet()) {
+                String key = (String) e.getKey();
+                String val = (String) e.getValue();
+                buf.append("\n\t\t[").append(key).append("] = [").append(val).append("]");
+            }
+        }
         return buf.toString();
     }
 }
diff --git a/core/java/src/net/i2p/data/i2cp/HostLookupMessage.java b/core/java/src/net/i2p/data/i2cp/HostLookupMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..1a8fcfe86239698ff2d7768270d7fa2164d412a1
--- /dev/null
+++ b/core/java/src/net/i2p/data/i2cp/HostLookupMessage.java
@@ -0,0 +1,183 @@
+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
+ * or a host. Replaces DestLookupMessage.
+ *
+ * @since 0.9.11; do not send to routers older than 0.9.11.
+ */
+public class HostLookupMessage extends I2CPMessageImpl {
+    public final static int MESSAGE_TYPE = 38;
+
+    private long _reqID;
+    private long _timeout;
+    private int _lookupType;
+    private Hash _hash;
+    private String _host;
+    private SessionId _sessionId;
+
+    public static final int LOOKUP_HASH = 0;
+    public static final int LOOKUP_HOST = 1;
+
+    private static final long MAX_INT = (1L << 32) - 1;
+
+    public HostLookupMessage() {}
+
+    /**
+     *  @param reqID 0 to 2**32 - 1
+     *  @param timeout ms 1 to 2**32 - 1
+     */
+    public HostLookupMessage(SessionId id, Hash h, long reqID, long timeout) {
+        if (id == null || h == null)
+            throw new IllegalArgumentException();
+        if (reqID < 0 || reqID > MAX_INT)
+            throw new IllegalArgumentException();
+        if (timeout <= 0 || timeout > MAX_INT)
+            throw new IllegalArgumentException();
+        _sessionId = id;
+        _hash = h;
+        _reqID = reqID;
+        _timeout = timeout;
+        _lookupType = LOOKUP_HASH;
+    }
+
+    /**
+     *  @param reqID 0 to 2**32 - 1
+     *  @param timeout ms 1 to 2**32 - 1
+     */
+    public HostLookupMessage(SessionId id, String host, long reqID, long timeout) {
+        if (id == null || host == null)
+            throw new IllegalArgumentException();
+        if (reqID < 0 || reqID > MAX_INT)
+            throw new IllegalArgumentException();
+        if (timeout <= 0 || timeout > MAX_INT)
+            throw new IllegalArgumentException();
+        _sessionId = id;
+        _host = host;
+        _reqID = reqID;
+        _timeout = timeout;
+        _lookupType = LOOKUP_HOST;
+    }
+
+    public SessionId getSessionId() {
+        return _sessionId;
+    }
+
+    /**
+     *  @return 0 to 2**32 - 1
+     */
+    public long getReqID() {
+        return _reqID;
+    }
+
+    /**
+     *  @return ms 1 to 2**32 - 1
+     */
+    public long getTimeout() {
+        return _timeout;
+    }
+
+    /**
+     *  @return 0 (hash) or 1 (host)
+     */
+    public int getLookupType() {
+        return _lookupType;
+    }
+
+    /**
+     *  @return only valid if lookup type == 0
+     */
+    public Hash getHash() {
+        return _hash;
+    }
+
+    /**
+     *  @return only valid if lookup type == 1
+     */
+    public String getHostname() {
+        return _host;
+    }
+
+    protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException {
+        try {
+            _sessionId = new SessionId();
+            _sessionId.readBytes(in);
+            _reqID = DataHelper.readLong(in, 4);
+            _timeout = DataHelper.readLong(in, 4);
+            _lookupType = (int) DataHelper.readLong(in, 1);
+            if (_lookupType == LOOKUP_HASH) {
+                _hash = Hash.create(in);
+            } else if (_lookupType == LOOKUP_HOST) {
+                _host = DataHelper.readString(in);
+                if (_host.length() == 0)
+                    throw new I2CPMessageException("bad host");
+            } else {
+                throw new I2CPMessageException("bad type");
+            }
+        } catch (DataFormatException dfe) {
+            throw new I2CPMessageException("bad data", dfe);
+        }
+    }
+
+    protected byte[] doWriteMessage() throws I2CPMessageException, IOException {
+        int len;
+        if (_lookupType == LOOKUP_HASH) {
+            if (_hash == null)
+                throw new I2CPMessageException("Unable to write out the message as there is not enough data");
+            len = 11 + Hash.HASH_LENGTH;
+        } else if (_lookupType == LOOKUP_HOST) {
+            if (_host == null)
+                throw new I2CPMessageException("Unable to write out the message as there is not enough data");
+            len = 12 + _host.length();
+        } else {
+            throw new I2CPMessageException("bad type");
+        }
+        ByteArrayOutputStream os = new ByteArrayOutputStream(len);
+        try {
+            _sessionId.writeBytes(os);
+            DataHelper.writeLong(os, 4, _reqID);
+            DataHelper.writeLong(os, 4, _timeout);
+            DataHelper.writeLong(os, 1, _lookupType);
+            if (_lookupType == LOOKUP_HASH) {
+                _hash.writeBytes(os);
+            } else {
+                DataHelper.writeString(os, _host);
+            }
+        } catch (DataFormatException dfe) {
+            throw new I2CPMessageException("bad data", dfe);
+        }
+        return os.toByteArray();
+    }
+
+    public int getType() {
+        return MESSAGE_TYPE;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("[HostLookupMessage: ");
+        buf.append("\n\t").append(_sessionId);
+        buf.append("\n\tReqID: ").append(_reqID);
+        buf.append("\n\tTimeout: ").append(_timeout);
+        if (_lookupType == LOOKUP_HASH)
+            buf.append("\n\tHash: ").append(_hash);
+        else if (_lookupType == LOOKUP_HOST)
+            buf.append("\n\tHost: ").append(_host);
+        buf.append("]");
+        return buf.toString();
+    }
+}
diff --git a/core/java/src/net/i2p/data/i2cp/HostReplyMessage.java b/core/java/src/net/i2p/data/i2cp/HostReplyMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..37faaa276f1cf991250dc7d80b688ed340b471f2
--- /dev/null
+++ b/core/java/src/net/i2p/data/i2cp/HostReplyMessage.java
@@ -0,0 +1,146 @@
+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 HostLookupMessage. Replaces DestReplyMessage.
+ *
+ * @since 0.9.11
+ */
+public class HostReplyMessage extends I2CPMessageImpl {
+    public final static int MESSAGE_TYPE = 39;
+
+    private Destination _dest;
+    private long _reqID;
+    private int _code;
+    private SessionId _sessionId;
+
+    public static final int RESULT_SUCCESS = 0;
+    /** generic fail, other codes TBD */
+    public static final int RESULT_FAILURE = 1;
+
+    private static final long MAX_INT = (1L << 32) - 1;
+
+    public HostReplyMessage() {}
+
+    /**
+     *  A message with RESULT_SUCCESS and a non-null Destination.
+     *
+     *  @param d non-null
+     *  @param reqID 0 to 2**32 - 1
+     */
+    public HostReplyMessage(SessionId id, Destination d, long reqID) {
+        if (id == null || d == null)
+            throw new IllegalArgumentException();
+        if (reqID < 0 || reqID > MAX_INT)
+            throw new IllegalArgumentException();
+        _sessionId = id;
+        _dest = d;
+        _reqID = reqID;
+    }
+
+    /**
+     *  A message with a failure code and no Destination.
+     *
+     *  @param failureCode 1-255
+     *  @param reqID from the HostLookup 0 to 2**32 - 1
+     */
+    public HostReplyMessage(SessionId id, int failureCode, long reqID) {
+        if (id == null)
+            throw new IllegalArgumentException();
+        if (failureCode <= 0 || failureCode > 255)
+            throw new IllegalArgumentException();
+        if (reqID < 0 || reqID > MAX_INT)
+            throw new IllegalArgumentException();
+        _sessionId = id;
+        _code = failureCode;
+        _reqID = reqID;
+    }
+
+    public SessionId getSessionId() {
+        return _sessionId;
+    }
+
+    /**
+     *  @return 0 to 2**32 - 1
+     */
+    public long getReqID() {
+        return _reqID;
+    }
+
+    /**
+     *  @return 0 on success, 1-255 on failure
+     */
+    public int getResultCode() {
+        return _code;
+    }
+
+    /**
+     *  @return non-null only if result code is zero
+     */
+    public Destination getDestination() {
+        return _dest;
+    }
+
+    protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException {
+        try {
+            _sessionId = new SessionId();
+            _sessionId.readBytes(in);
+            _reqID = DataHelper.readLong(in, 4);
+            _code = (int) DataHelper.readLong(in, 1);
+            if (_code == RESULT_SUCCESS)
+                _dest = Destination.create(in);
+        } catch (DataFormatException dfe) {
+            throw new I2CPMessageException("bad data", dfe);
+        }
+    }
+
+    protected byte[] doWriteMessage() throws I2CPMessageException, IOException {
+        int len = 7;
+        if (_code == RESULT_SUCCESS) {
+            if (_dest == null)
+                throw new I2CPMessageException("Unable to write out the message as there is not enough data");
+            len += _dest.size();
+        }
+        ByteArrayOutputStream os = new ByteArrayOutputStream(len);
+        try {
+            _sessionId.writeBytes(os);
+            DataHelper.writeLong(os, 4, _reqID);
+            DataHelper.writeLong(os, 1, _code);
+            if (_code == RESULT_SUCCESS)
+                _dest.writeBytes(os);
+        } catch (DataFormatException dfe) {
+            throw new I2CPMessageException("bad data", dfe);
+        }
+        return os.toByteArray();
+    }
+
+    public int getType() {
+        return MESSAGE_TYPE;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("[HostReplyMessage: ");
+        buf.append("\n\t").append(_sessionId);
+        buf.append("\n\tReqID: ").append(_reqID);
+        buf.append("\n\tResult: ").append(_code);
+        if (_code == RESULT_SUCCESS)
+            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 0a62416617116f4aeaf6aaa16c3a81a7b25718b8..bf34befde1c00ebfc6ab28b579ed9d9aee5cde7b 100644
--- a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java
+++ b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java
@@ -21,6 +21,12 @@ import net.i2p.data.DataHelper;
  */
 public class I2CPMessageHandler {
 
+    /**
+     *  This is huge. Mainly to catch a completly bogus response, possibly not an I2CP socket.
+     *  @since 0.9.10
+     */
+    public static final int MAX_LENGTH = 128*1024;
+
     /**
      * Read an I2CPMessage from the stream and return the fully populated object.
      * 
@@ -31,16 +37,21 @@ public class I2CPMessageHandler {
      *          message - if it is an unknown type or has improper formatting, etc.
      */
     public static I2CPMessage readMessage(InputStream in) throws IOException, I2CPMessageException {
-        int length = -1;
+        int length;
         try {
             length = (int) DataHelper.readLong(in, 4);
         } catch (DataFormatException dfe) {
             throw new IOException("Connection closed");
         }
+        if (length > MAX_LENGTH)
+            throw new I2CPMessageException("Invalid message length specified");
         try {
-            if (length < 0) throw new I2CPMessageException("Invalid message length specified");
             int type = (int) DataHelper.readLong(in, 1);
             I2CPMessage msg = createMessage(type);
+            // Note that the readMessage() calls don't, in general, read and discard
+            // extra data, so we can't add new fields to the end of messages
+            // in a compatible way. And the readers could read beyond the length too.
+            // To fix this we'd have to read into a BAOS/BAIS or use a filter input stream
             msg.readMessage(in, length, type);
             return msg;
         } catch (DataFormatException dfe) {
@@ -52,7 +63,7 @@ public class I2CPMessageHandler {
      * Yes, this is fairly ugly, but its the only place it ever happens.  
      *
      */
-    private static I2CPMessage createMessage(int type) throws IOException,
+    private static I2CPMessage createMessage(int type) throws
                                                        I2CPMessageException {
         switch (type) {
         case CreateLeaseSetMessage.MESSAGE_TYPE:
@@ -97,6 +108,10 @@ public class I2CPMessageHandler {
             return new GetBandwidthLimitsMessage();
         case BandwidthLimitsMessage.MESSAGE_TYPE:
             return new BandwidthLimitsMessage();
+        case HostLookupMessage.MESSAGE_TYPE:
+            return new HostLookupMessage();
+        case HostReplyMessage.MESSAGE_TYPE:
+            return new HostReplyMessage();
         default:
             throw new I2CPMessageException("The type " + type + " is an unknown I2CP message");
         }
diff --git a/core/java/src/net/i2p/data/i2cp/I2CPMessageReader.java b/core/java/src/net/i2p/data/i2cp/I2CPMessageReader.java
index 7dd702cf28b81a2f8a60906d87e4c5cbd9059d8d..72148a55dab2a1f0f41f69c36793b32fac35d8ac 100644
--- a/core/java/src/net/i2p/data/i2cp/I2CPMessageReader.java
+++ b/core/java/src/net/i2p/data/i2cp/I2CPMessageReader.java
@@ -110,7 +110,7 @@ public class I2CPMessageReader {
          * reader
          *
 	 * @param reader I2CPMessageReader to notify
-	 * @param error Exception that was thrown
+	 * @param error Exception that was thrown, non-null
 	 */
         public void readError(I2CPMessageReader reader, Exception error);
 
diff --git a/core/java/src/net/i2p/data/i2cp/SessionConfig.java b/core/java/src/net/i2p/data/i2cp/SessionConfig.java
index cd6db38e4edc9cb06d19a17287cfe9c073db1624..7e128576894f7ec3dc167dc688955a090b632e2e 100644
--- a/core/java/src/net/i2p/data/i2cp/SessionConfig.java
+++ b/core/java/src/net/i2p/data/i2cp/SessionConfig.java
@@ -89,7 +89,8 @@ public class SessionConfig extends DataStructureImpl {
     }
 
     /**
-     * Configure the session with the given options
+     * Configure the session with the given options;
+     * keys and values 255 bytes (not chars) max each
      *
      * @param options Properties for this session
      */
diff --git a/core/java/src/net/i2p/data/i2cp/SessionId.java b/core/java/src/net/i2p/data/i2cp/SessionId.java
index 68f8c1a29872dfd66cfa6bd8ee510e92276e0623..2cacd63fe280d102814bdca975ed21d53f4fe577 100644
--- a/core/java/src/net/i2p/data/i2cp/SessionId.java
+++ b/core/java/src/net/i2p/data/i2cp/SessionId.java
@@ -29,12 +29,24 @@ public class SessionId extends DataStructureImpl {
         _sessionId = -1;
     }
 
+    /**
+     *  @param id 0-65535
+     *  @since 0.9.11
+     */
+    public SessionId(int id) {
+        if (id < 0 || id > 65535)
+            throw new IllegalArgumentException();
+        _sessionId = id;
+    }
+
     public int getSessionId() {
         return _sessionId;
     }
 
     /** @param id 0-65535 */
     public void setSessionId(int id) {
+        if (id < 0 || id > 65535)
+            throw new IllegalArgumentException();
         _sessionId = id;
     }
 
diff --git a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
index e73041deaadddc1f4c4a4bd575aaa939f58d2b50..9b4dbed3ba82bc9c2b0a220d9431ac343e5f5e9e 100644
--- a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
+++ b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
@@ -335,12 +335,14 @@ class ClientConnectionRunner {
      *  This is always bad.
      *  See ClientMessageEventListener.handleCreateSession()
      *  for why we don't send a SessionStatusMessage when we do this.
+     *  @param reason will be truncated to 255 bytes
      */
     void disconnectClient(String reason) {
         disconnectClient(reason, Log.ERROR);
     }
 
     /**
+     * @param reason will be truncated to 255 bytes
      * @param logLevel e.g. Log.WARN
      * @since 0.8.2
      */
@@ -351,6 +353,8 @@ class ClientConnectionRunner {
                      + " config: "
                      + _config);
         DisconnectMessage msg = new DisconnectMessage();
+        if (reason.length() > 255)
+            reason = reason.substring(0, 255);
         msg.setReason(reason);
         try {
             doSend(msg);
diff --git a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
index 6ea78a0630816b60a9424df6a37c622e08d091f8..9518a21934acd4cd40c2a06751f2665dfdbe6720 100644
--- a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
+++ b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
@@ -20,6 +20,7 @@ import net.i2p.data.i2cp.DestLookupMessage;
 import net.i2p.data.i2cp.DestroySessionMessage;
 import net.i2p.data.i2cp.GetBandwidthLimitsMessage;
 import net.i2p.data.i2cp.GetDateMessage;
+import net.i2p.data.i2cp.HostLookupMessage;
 import net.i2p.data.i2cp.I2CPMessage;
 import net.i2p.data.i2cp.I2CPMessageException;
 import net.i2p.data.i2cp.I2CPMessageReader;
@@ -50,8 +51,11 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
     protected final RouterContext _context;
     protected final ClientConnectionRunner _runner;
     private final boolean  _enforceAuth;
+    private volatile boolean _authorized;
     
     private static final String PROP_AUTH = "i2cp.auth";
+    /** if true, user/pw must be in GetDateMessage */
+    private static final String PROP_AUTH_STRICT = "i2cp.strictAuth";
 
     /**
      *  @param enforceAuth set false for in-JVM, true for socket access
@@ -61,6 +65,8 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
         _log = _context.logManager().getLog(ClientMessageEventListener.class);
         _runner = runner;
         _enforceAuth = enforceAuth;
+        if ((!_enforceAuth) || !_context.getBooleanProperty(PROP_AUTH))
+            _authorized = true;
         _context.statManager().createRateStat("client.distributeTime", "How long it took to inject the client message into the router", "ClientMessages", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
     }
     
@@ -72,6 +78,20 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
         if (_runner.isDead()) return;
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("Message received: \n" + message);
+        int type = message.getType();
+        if (!_authorized) {
+            // TODO change to default true
+            boolean strict = _context.getBooleanProperty(PROP_AUTH_STRICT);
+            if ((strict && type != GetDateMessage.MESSAGE_TYPE) ||
+                (type != CreateSessionMessage.MESSAGE_TYPE &&
+                 type != GetDateMessage.MESSAGE_TYPE &&
+                 type != DestLookupMessage.MESSAGE_TYPE &&
+                 type != GetBandwidthLimitsMessage.MESSAGE_TYPE)) {
+                _log.error("Received message type " + type + " without required authentication");
+                _runner.disconnectClient("Authorization required");
+                return;
+            }
+        }
         switch (message.getType()) {
             case GetDateMessage.MESSAGE_TYPE:
                 handleGetDate((GetDateMessage)message);
@@ -103,6 +123,9 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
             case DestLookupMessage.MESSAGE_TYPE:
                 handleDestLookup((DestLookupMessage)message);
                 break;
+            case HostLookupMessage.MESSAGE_TYPE:
+                handleHostLookup((HostLookupMessage)message);
+                break;
             case ReconfigureSessionMessage.MESSAGE_TYPE:
                 handleReconfigureSession((ReconfigureSessionMessage)message);
                 break;
@@ -124,6 +147,8 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
         if (_log.shouldLog(Log.ERROR))
             _log.error("Error occurred", error);
         // Is this is a little drastic for an unknown message type?
+        // Send the whole exception string over for diagnostics
+        _runner.disconnectClient(error.toString());
         _runner.stopRunning();
     }
   
@@ -137,6 +162,9 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
         String clientVersion = message.getVersion();
         if (clientVersion != null)
             _runner.setClientVersion(clientVersion);
+        Properties props = message.getOptions();
+        if (!checkAuth(props))
+            return;
         try {
             // only send version if the client can handle it (0.8.7 or greater)
             _runner.doSend(new SetDateMessage(clientVersion != null ? CoreVersion.VERSION : null));
@@ -174,24 +202,9 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
         }
 
         // Auth, since 0.8.2
-        if (_enforceAuth && _context.getBooleanProperty(PROP_AUTH)) {
-                Properties props = in.getOptions();
-                String user = props.getProperty("i2cp.username");
-                String pw = props.getProperty("i2cp.password");
-                if (user == null || user.length() == 0 || pw == null || pw.length() == 0) {
-                    _log.error("I2CP auth failed for client: " + props.getProperty("inbound.nickname"));
-                    _runner.disconnectClient("Authorization required to create session, specify i2cp.username and i2cp.password in session options");
-                    return;
-                }
-                PasswordManager mgr = new PasswordManager(_context);
-                if (!mgr.checkHash(PROP_AUTH, user, pw)) {
-                    _log.error("I2CP auth failed for client: " + props.getProperty("inbound.nickname") + " user: " + user);
-                    _runner.disconnectClient("Authorization failed for Create Session, user = " + user);
-                    return;
-                }
-                if (_log.shouldLog(Log.INFO))
-                    _log.info("I2CP auth success for client: " + props.getProperty("inbound.nickname") + " user: " + user);
-        }
+        Properties inProps = in.getOptions();
+        if (!checkAuth(inProps))
+            return;
 
         SessionId sessionId = new SessionId();
         sessionId.setSessionId(getNextSessionId()); 
@@ -213,6 +226,44 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
         startCreateSessionJob();
     }
     
+    /**
+     *  Side effect - sets _authorized.
+     *  Side effect - disconnects session if not authorized.
+     *
+     *  @param props contains i2cp.username and i2cp.password, may be null
+     *  @return success
+     *  @since 0.9.11
+     */
+    private boolean checkAuth(Properties props) {
+        if (_authorized)
+            return true;
+        if (_enforceAuth && _context.getBooleanProperty(PROP_AUTH)) {
+            String user = null;
+            String pw = null;
+            if (props != null) {
+                user = props.getProperty("i2cp.username");
+                pw = props.getProperty("i2cp.password");
+            }
+            if (user == null || user.length() == 0 || pw == null || pw.length() == 0) {
+                _log.error("I2CP auth failed");
+                _runner.disconnectClient("Authorization required, specify i2cp.username and i2cp.password in options");
+                _authorized = false;
+                return false;
+            }
+            PasswordManager mgr = new PasswordManager(_context);
+            if (!mgr.checkHash(PROP_AUTH, user, pw)) {
+                _log.error("I2CP auth failed user: " + user);
+                _runner.disconnectClient("Authorization failed, user = " + user);
+                _authorized = false;
+                return false;
+            }
+            if (_log.shouldLog(Log.INFO))
+                _log.info("I2CP auth success user: " + user);
+        }
+        _authorized = true;
+        return true;
+    }
+
     /**
      *  Override for testing
      *  @since 0.9.8
@@ -315,6 +366,16 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
         _context.jobQueue().addJob(new LookupDestJob(_context, _runner, message.getHash()));
     }
 
+    /**
+     * override for testing
+     * @since 0.9.11
+     */
+    protected void handleHostLookup(HostLookupMessage message) {
+        _context.jobQueue().addJob(new LookupDestJob(_context, _runner, message.getReqID(),
+                                                     message.getTimeout(), message.getSessionId(),
+                                                     message.getHash(), message.getHostname()));
+    }
+
     /**
      * Message's Session ID ignored. This doesn't support removing previously set options.
      * Nor do we bother with message.getSessionConfig().verifySignature() ... should we?
diff --git a/router/java/src/net/i2p/router/client/LookupDestJob.java b/router/java/src/net/i2p/router/client/LookupDestJob.java
index 017401569a0346c5cdeb762ec9b7d4289d44bc6a..d5ab0877509d121844fdbcf9598609b09f35785f 100644
--- a/router/java/src/net/i2p/router/client/LookupDestJob.java
+++ b/router/java/src/net/i2p/router/client/LookupDestJob.java
@@ -4,32 +4,88 @@
  */
 package net.i2p.router.client;
 
+import java.util.Locale;
+
+import net.i2p.data.Base32;
 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.HostReplyMessage;
+import net.i2p.data.i2cp.I2CPMessage;
 import net.i2p.data.i2cp.I2CPMessageException;
+import net.i2p.data.i2cp.SessionId;
 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
+ * Look up the lease of a hash, to convert it to a Destination for the client.
+ * Or, since 0.9.11, lookup a host name in the naming service.
  */
 class LookupDestJob extends JobImpl {
     private final ClientConnectionRunner _runner;
+    private final long _reqID;
+    private final long _timeout;
     private final Hash _hash;
+    private final String _name;
+    private final SessionId _sessID;
+
+    private static final long DEFAULT_TIMEOUT = 15*1000;
 
     public LookupDestJob(RouterContext context, ClientConnectionRunner runner, Hash h) {
+        this(context, runner, -1, DEFAULT_TIMEOUT, null, h, null);
+    }
+
+    /**
+     *  One of h or name non-null
+     *  @param reqID must be >= 0 if name != null
+     *  @param sessID must non-null if reqID >= 0
+     *  @since 0.9.11
+     */
+    public LookupDestJob(RouterContext context, ClientConnectionRunner runner,
+                         long reqID, long timeout, SessionId sessID, Hash h, String name) {
         super(context);
+        if ((h == null && name == null) ||
+            (h != null && name != null) ||
+            (reqID >= 0 && sessID == null) ||
+            (reqID < 0 && name != null))
+            throw new IllegalArgumentException();
         _runner = runner;
+        _reqID = reqID;
+        _timeout = timeout;
+        _sessID = sessID;
+        if (name != null && name.length() == 60) {
+            // convert a b32 lookup to a hash lookup
+            String nlc = name.toLowerCase(Locale.US);
+            if (nlc.endsWith(".b32.i2p")) {
+                byte[] b = Base32.decode(nlc.substring(0, 52));
+                if (b != null && b.length == Hash.HASH_LENGTH) {
+                    h = Hash.create(b);
+                    name = null;
+                }
+            }
+        }
         _hash = h;
+        _name = name;
     }
     
-    public String getName() { return "LeaseSet Lookup for Client"; }
+    public String getName() { return _name != null ?
+                                     "HostName Lookup for Client" :
+                                     "LeaseSet Lookup for Client";
+    }
+
     public void runJob() {
-        DoneJob done = new DoneJob(getContext());
-        // TODO add support for specifying the timeout in the lookup message
-        getContext().netDb().lookupLeaseSet(_hash, done, done, 15*1000);
+        if (_name != null) {
+            // inline, ignore timeout
+            Destination d = getContext().namingService().lookup(_name);
+            if (d != null)
+                returnDest(d);
+            else
+                returnFail();
+        } else {
+            DoneJob done = new DoneJob(getContext());
+            getContext().netDb().lookupLeaseSet(_hash, done, done, _timeout);
+        }
     }
 
     private class DoneJob extends JobImpl {
@@ -42,12 +98,16 @@ class LookupDestJob extends JobImpl {
             if (ls != null)
                 returnDest(ls.getDestination());
             else
-                returnHash(_hash);
+                returnFail();
         }
     }
 
     private void returnDest(Destination d) {
-        DestReplyMessage msg = new DestReplyMessage(d);
+        I2CPMessage msg;
+        if (_reqID >= 0)
+            msg = new HostReplyMessage(_sessID, d, _reqID);
+        else
+            msg = new DestReplyMessage(d);
         try {
             _runner.doSend(msg);
         } catch (I2CPMessageException ime) {}
@@ -57,8 +117,12 @@ class LookupDestJob extends JobImpl {
      *  Return the failed hash so the client can correlate replies with requests
      *  @since 0.8.3
      */
-    private void returnHash(Hash h) {
-        DestReplyMessage msg = new DestReplyMessage(h);
+    private void returnFail() {
+        I2CPMessage msg;
+        if (_reqID >= 0)
+            msg = new HostReplyMessage(_sessID, HostReplyMessage.RESULT_FAILURE, _reqID);
+        else
+            msg = new DestReplyMessage(_hash);
         try {
             _runner.doSend(msg);
         } catch (I2CPMessageException ime) {}