diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java
index d53ddde5d435c75c62354f2384c977f7ca38289b..034f74ae71e86bccb7d1ae44cb3ed4cb52b60588 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java
@@ -428,7 +428,7 @@ public class PeerCoordinator implements PeerListener
               peer.runConnection(_util, listener, bitfield);
             }
           };
-        String threadName = peer.toString();
+        String threadName = "Snark peer " + peer.toString();
         new I2PAppThread(r, threadName).start();
         return true;
       }
diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java
index e3cc8ae9f00ebd1c63b759497160d72ab258496b..a1a3f4cc21a2e5c0f11952594e85e728493a96c0 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java
@@ -72,8 +72,10 @@ public class TrackerClient extends I2PAppThread
 
   public TrackerClient(I2PSnarkUtil util, MetaInfo meta, PeerCoordinator coordinator)
   {
+    super();
     // Set unique name.
-    super("TrackerClient-" + urlencode(coordinator.getID()));
+    String id = urlencode(coordinator.getID());
+    setName("TrackerClient " + id.substring(id.length() - 12));
     _util = util;
     this.meta = meta;
     this.coordinator = coordinator;
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java
index 890a4f1414b510402fb6f791492435f6b94b9517..7fc808643b078992e2b2fb65e70d6eb610ef9fca 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java
@@ -16,6 +16,7 @@ import java.io.OutputStream;
 import java.io.PipedInputStream;
 import java.io.PipedOutputStream;
 import java.util.zip.GZIPInputStream;
+import java.util.concurrent.RejectedExecutionException;
 
 import net.i2p.I2PAppContext;
 import net.i2p.data.ByteArray;
@@ -228,7 +229,15 @@ class HTTPResponseOutputStream extends FilterOutputStream {
         //out.flush();
         PipedInputStream pi = new PipedInputStream();
         PipedOutputStream po = new PipedOutputStream(pi);
-        new I2PAppThread(new Pusher(pi, out), "HTTP decompressor").start();
+        // Run in the client thread pool, as there should be an unused thread
+        // there after the accept().
+        // Overridden in I2PTunnelHTTPServer, where it does not use the client pool.
+        try {
+            I2PTunnelClientBase._executor.execute(new Pusher(pi, out));
+        } catch (RejectedExecutionException ree) {
+            // shouldn't happen
+            throw ree;
+        }
         out = po;
     }
     
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java
index 053fc61ceab5227bb5d7570f1a2a1e6d93f4a70e..bfde2fb32432864bcade361925f7ff33b647c3f4 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java
@@ -16,8 +16,6 @@ import net.i2p.util.Log;
 
 public class I2PTunnelClient extends I2PTunnelClientBase {
 
-    private static final Log _log = new Log(I2PTunnelClient.class);
-
     /** list of Destination objects that we point at */
     protected List<Destination> dests;
     private static final long DEFAULT_READ_TIMEOUT = 5*60*1000; // -1
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java
index e48bae1a9a9ab1758290307d48b71bec6194bed7..a98551b8c5ba2643536acd48e1436a2b569d894c 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java
@@ -17,6 +17,13 @@ import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Properties;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.ThreadFactory;
 
 import net.i2p.I2PAppContext;
 import net.i2p.I2PException;
@@ -34,9 +41,9 @@ import net.i2p.util.SimpleTimer;
 
 public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runnable {
 
-    private static final Log _log = new Log(I2PTunnelClientBase.class);
-    protected I2PAppContext _context;
-    protected Logging l;
+    protected final Log _log;
+    protected final I2PAppContext _context;
+    protected final Logging l;
 
     static final long DEFAULT_CONNECT_TIMEOUT = 60 * 1000;
 
@@ -64,35 +71,24 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
     private String handlerName;
     private String privKeyFile;
 
-    // private Object conLock = new Object();
-    
-    /** List of Socket for those accept()ed but not yet started up */
-    protected final List _waitingSockets = new ArrayList(4); // FIXME should be final and use a factory. FIXME
-    /** How many connections will we allow to be in the process of being built at once? */
-    private int _numConnectionBuilders;
-    /** How long will we allow sockets to sit in the _waitingSockets map before killing them? */
-    private int _maxWaitTime;
-    
-    /**
-     * How many concurrent connections this I2PTunnel instance will allow to be 
-     * in the process of connecting (or if less than 1, there is no limit)?
-     */
-    public static final String PROP_NUM_CONNECTION_BUILDERS = "i2ptunnel.numConnectionBuilders";
-    /**
-     * How long will we let a socket wait after being accept()ed without getting
-     * pumped through a connection builder (in milliseconds).  If this time is 
-     * reached, the socket is unceremoniously closed and discarded.  If the max 
-     * wait time is less than 1, there is no limit.
-     *
-     */
-    public static final String PROP_MAX_WAIT_TIME = "i2ptunnel.maxWaitTime";
-    
-    private static final int DEFAULT_NUM_CONNECTION_BUILDERS = 5;
-    private static final int DEFAULT_MAX_WAIT_TIME = 30*1000;
-
     // true if we are chained from a server.
     private boolean chained = false;
 
+    /** how long to wait before dropping an idle thread */
+    private static final long HANDLER_KEEPALIVE_MS = 2*60*1000;
+
+    /**
+     *  We keep a static pool of socket handlers for all clients,
+     *  as there is no need for isolation on the client side.
+     *  Extending classes may use it for other purposes.
+     *  Not for use by servers, as there is no limit on threads.
+     */
+    static final Executor _executor;
+    private static int _executorThreadCount;
+    static {
+        _executor = new CustomThreadPoolExecutor();
+    }
+
     public I2PTunnelClientBase(int localPort, Logging l, I2PSocketManager sktMgr,
             I2PTunnel tunnel, EventDispatcher notifyThis, long clientId )
             throws IllegalArgumentException {
@@ -109,9 +105,9 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
         _context.statManager().createRateStat("i2ptunnel.client.closeNoBacklog", "How many pending sockets remain when it was removed prior to backlog timeout?", "I2PTunnel", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
         _context.statManager().createRateStat("i2ptunnel.client.manageTime", "How long it takes to accept a socket and fire it into an i2ptunnel runner (or queue it for the pool)?", "I2PTunnel", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
         _context.statManager().createRateStat("i2ptunnel.client.buildRunTime", "How long it takes to run a queued socket into an i2ptunnel runner?", "I2PTunnel", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
+        _log = _context.logManager().getLog(getClass());
 
-        Thread t = new I2PAppThread(this);
-        t.setName("Client " + _clientId);
+        Thread t = new I2PAppThread(this, "Client " + tunnel.listenHost + ':' + localPort);
         listenerReady = false;
         t.start();
         open = true;
@@ -125,8 +121,6 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
             }
         }
 
-        configurePool(tunnel);
-
         if (open && listenerReady) {
             l.log("Client ready, listening on " + tunnel.listenHost + ':' + localPort);
             notifyEvent("openBaseClientResult", "ok");
@@ -135,6 +129,7 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
             notifyEvent("openBaseClientResult", "error");
         }
     }
+
     public I2PTunnelClientBase(int localPort, boolean ownDest, Logging l, 
                                EventDispatcher notifyThis, String handlerName, 
                                I2PTunnel tunnel) throws IllegalArgumentException {
@@ -163,6 +158,7 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
         _context.statManager().createRateStat("i2ptunnel.client.closeNoBacklog", "How many pending sockets remain when it was removed prior to backlog timeout?", "I2PTunnel", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
         _context.statManager().createRateStat("i2ptunnel.client.manageTime", "How long it takes to accept a socket and fire it into an i2ptunnel runner (or queue it for the pool)?", "I2PTunnel", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
         _context.statManager().createRateStat("i2ptunnel.client.buildRunTime", "How long it takes to run a queued socket into an i2ptunnel runner?", "I2PTunnel", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
+        _log = _context.logManager().getLog(getClass());
 
         // normalize path so we can find it
         if (pkf != null) {
@@ -210,8 +206,6 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
             }
         }
 
-        configurePool(tunnel);
-        
         if (open && listenerReady) {
             if (openNow)
                 l.log("Client ready, listening on " + tunnel.listenHost + ':' + localPort);
@@ -224,37 +218,6 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
         }
     }
     
-    /** 
-     * build and configure the pool handling accept()ed but not yet 
-     * established connections 
-     *
-     */
-    private void configurePool(I2PTunnel tunnel) {
-        //_waitingSockets = new ArrayList(4);
-        
-        Properties opts = tunnel.getClientOptions();
-        String maxWait = opts.getProperty(PROP_MAX_WAIT_TIME, DEFAULT_MAX_WAIT_TIME+"");
-        try { 
-            _maxWaitTime = Integer.parseInt(maxWait); 
-        } catch (NumberFormatException nfe) {
-            _maxWaitTime = DEFAULT_MAX_WAIT_TIME;
-        }
-        
-        String numBuild = opts.getProperty(PROP_NUM_CONNECTION_BUILDERS, DEFAULT_NUM_CONNECTION_BUILDERS+"");
-        try {
-            _numConnectionBuilders = Integer.parseInt(numBuild);
-        } catch (NumberFormatException nfe) {
-            _numConnectionBuilders = DEFAULT_NUM_CONNECTION_BUILDERS;
-        }
-
-        for (int i = 0; i < _numConnectionBuilders; i++) {
-            String name = "ClientBuilder" + _clientId + '.' + i;
-            I2PAppThread b = new I2PAppThread(new TunnelConnectionBuilder(), name);
-            b.setDaemon(true);
-            b.start();
-        }
-    }
-
     /**
      * Sets the this.sockMgr field if it is null, or if we want a new one
      *
@@ -321,6 +284,8 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
      *                                  badly that we cant create a socketManager
      */
     protected static synchronized I2PSocketManager getSocketManager(I2PTunnel tunnel, String pkf) {
+        // shadows instance _log
+        Log _log = tunnel.getContext().logManager().getLog(I2PTunnelClientBase.class);
         if (socketManager != null) {
             I2PSession s = socketManager.getSession();
             if ( (s == null) || (s.isClosed()) ) {
@@ -378,6 +343,8 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
      *                                  badly that we cant create a socketManager
      */
     protected static I2PSocketManager buildSocketManager(I2PTunnel tunnel, String pkf, Logging log) {
+        // shadows instance _log
+        Log _log = tunnel.getContext().logManager().getLog(I2PTunnelClientBase.class);
         Properties props = new Properties();
         props.putAll(tunnel.getClientOptions());
         int portNum = 7654;
@@ -537,7 +504,6 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
                 synchronized (this) {
                     notifyAll();
                 }
-                synchronized (_waitingSockets) { _waitingSockets.notifyAll(); }
                 return;
             }
             ss = new ServerSocket(localPort, 0, addr);
@@ -566,12 +532,9 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
                 }
             }
 
-            while (true) {
+            while (open) {
                 Socket s = ss.accept();
-                long before = System.currentTimeMillis();
                 manageConnection(s);
-                long total = System.currentTimeMillis() - before;
-                _context.statManager().addRateData("i2ptunnel.client.manageTime", total, total);
             }
         } catch (IOException ex) {
             if (open) {
@@ -586,9 +549,6 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
                 notifyAll();
             }
         }
-        synchronized (_waitingSockets) {
-            _waitingSockets.notifyAll();
-        }
     }
 
     /**
@@ -598,24 +558,38 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
      */
     protected void manageConnection(Socket s) {
         if (s == null) return;
-        if (_numConnectionBuilders <= 0) {
-            new I2PAppThread(new BlockingRunner(s), "Clinet run").start();
-            return;
+        try {
+            _executor.execute(new BlockingRunner(s));
+        } catch (RejectedExecutionException ree) {
+             // should never happen, we have an unbounded pool and never stop the executor
+             try {
+                 s.close();
+             } catch (IOException ioe) {}
         }
-        
-        if (_maxWaitTime > 0)
-            SimpleScheduler.getInstance().addEvent(new CloseEvent(s), _maxWaitTime);
+    }
+
+    /**
+     * Not really needed for now but in case we want to add some hooks like afterExecute().
+     */
+    private static class CustomThreadPoolExecutor extends ThreadPoolExecutor {
+        public CustomThreadPoolExecutor() {
+             super(0, Integer.MAX_VALUE, HANDLER_KEEPALIVE_MS, TimeUnit.MILLISECONDS,
+                   new SynchronousQueue(), new CustomThreadFactory());
+        }
+    }
 
-        synchronized (_waitingSockets) {
-            _waitingSockets.add(s);
-            _waitingSockets.notifyAll();
+    /** just to set the name and set Daemon */
+    private static class CustomThreadFactory implements ThreadFactory {
+        public Thread newThread(Runnable r) {
+            Thread rv = Executors.defaultThreadFactory().newThread(r);
+            rv.setName("I2PTunnel Client Runner " + (++_executorThreadCount));
+            rv.setDaemon(true);
+            return rv;
         }
     }
 
     /** 
-     * Blocking runner, used during the connection establishment whenever we
-     * are not using the queued builders.
-     *
+     * Blocking runner, used during the connection establishment
      */
     private class BlockingRunner implements Runnable {
         private Socket _s;
@@ -625,32 +599,6 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
         }
     }
     
-    /**
-     * Remove and close the socket from the waiting list, if it is still there.
-     *
-     */
-    private class CloseEvent implements SimpleTimer.TimedEvent {
-        private Socket _s;
-        public CloseEvent(Socket s) { _s = s; }
-        public void timeReached() {
-            int remaining = 0;
-            boolean stillWaiting = false;
-            synchronized (_waitingSockets) {
-                stillWaiting = _waitingSockets.remove(_s);
-                remaining = _waitingSockets.size();
-            }
-            if (stillWaiting) {
-                try { _s.close(); } catch (IOException ioe) {}
-                if (_log.shouldLog(Log.INFO)) {
-                    _context.statManager().addRateData("i2ptunnel.client.closeBacklog", remaining, 0);
-                    _log.info("Closed a waiting socket because of backlog");
-                }
-            } else {
-                _context.statManager().addRateData("i2ptunnel.client.closeNoBacklog", remaining, 0);
-            }
-        }
-    }
-
     public boolean close(boolean forced) {
         if (_log.shouldLog(Log.INFO))
             _log.info("close() called: forced = " + forced + " open = " + open + " sockMgr = " + sockMgr);
@@ -688,7 +636,6 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
             //l.log("Client closed.");
         }
         
-        synchronized (_waitingSockets) { _waitingSockets.notifyAll(); }
         return true;
     }
 
@@ -696,40 +643,10 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
         try {
             s.close();
         } catch (IOException ex) {
-            _log.error("Could not close socket", ex);
+            //_log.error("Could not close socket", ex);
         }
     }
     
-    /**
-     * Pool runner pulling sockets off the waiting list and pushing them
-     * through clientConnectionRun.  This dies when the I2PTunnel instance
-     * is closed.
-     *
-     */
-    private class TunnelConnectionBuilder implements Runnable {
-        public void run() { 
-            Socket s = null;
-            while (open) {
-                try {
-                    synchronized (_waitingSockets) {
-                        if (_waitingSockets.isEmpty())
-                            _waitingSockets.wait();
-                        else
-                            s = (Socket)_waitingSockets.remove(0);
-                    }
-                } catch (InterruptedException ie) {}
-                
-                if (s != null) {
-                    long before = System.currentTimeMillis();
-                    clientConnectionRun(s);
-                    long total = System.currentTimeMillis() - before;
-                    _context.statManager().addRateData("i2ptunnel.client.buildRunTime", total, 0);
-                }
-                s = null;
-            }
-        }
-    }
-
     /**
      * Manage a connection in a separate thread. This only works if
      * you do not override manageConnection()
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java
index e151d733d7d8b55d75bb9e9b8b81fd8b4845ea49..70265a154d2a14fd94169b929921b1a427f00c29 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java
@@ -58,7 +58,6 @@ import net.i2p.util.Log;
  * @author zzz a stripped-down I2PTunnelHTTPClient
  */
 public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements Runnable {
-    private static final Log _log = new Log(I2PTunnelConnectClient.class);
 
     private final static byte[] ERR_DESTINATION_UNKNOWN =
         ("HTTP/1.1 503 Service Unavailable\r\n"+
@@ -340,8 +339,8 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R
             _requestId = id;
         }
         public void run() {
-            if (_log.shouldLog(Log.DEBUG))
-                _log.debug("Timeout occured requesting " + _target);
+            //if (_log.shouldLog(Log.DEBUG))
+            //    _log.debug("Timeout occured requesting " + _target);
             handleConnectClientException(new RuntimeException("Timeout"), _out, 
                                       _target, _usingProxy, _wwwProxy, _requestId);
             closeSocket(_socket);
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPBidirServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPBidirServer.java
index 54b2046cfb14259208453908574b6c25e423249b..ff1f3766188acdfca53cc383cb56bacf0311070f 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPBidirServer.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPBidirServer.java
@@ -11,7 +11,6 @@ import net.i2p.util.EventDispatcher;
 import net.i2p.util.Log;
 
 public class I2PTunnelHTTPBidirServer extends I2PTunnelHTTPServer {
-    private final static Log log = new Log(I2PTunnelHTTPBidirServer.class);
 
     public I2PTunnelHTTPBidirServer(InetAddress host, int port, int proxyport, String privData, String spoofHost, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) {
         super(host, port, privData, spoofHost, l, notifyThis, tunnel);
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java
index 8288e702fda131e109d611b363cf20fe5bf3cc2a..a9b1a82878597bcbfe4121d210768ccef001e2c3 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java
@@ -61,7 +61,6 @@ import net.i2p.util.Translate;
  *
  */
 public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runnable {
-    private static final Log _log = new Log(I2PTunnelHTTPClient.class);
 
     private HashMap addressHelpers = new HashMap();
 
@@ -894,15 +893,15 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
             _requestId = id;
         }
         public void run() {
-            if (_log.shouldLog(Log.DEBUG))
-                _log.debug("Timeout occured requesting " + _target);
+            //if (_log.shouldLog(Log.DEBUG))
+            //    _log.debug("Timeout occured requesting " + _target);
             handleHTTPClientException(new RuntimeException("Timeout"), _out,
                                       _target, _usingProxy, _wwwProxy, _requestId);
             closeSocket(_socket);
         }
     }
 
-    private static String DEFAULT_JUMP_SERVERS =
+    public static final String DEFAULT_JUMP_SERVERS =
                                            "http://i2host.i2p/cgi-bin/i2hostjump?," +
                                            "http://stats.i2p/cgi-bin/jump.cgi?a=," +
                                            "http://i2jump.i2p/";
@@ -940,8 +939,12 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
                         // Skip jump servers we don't know
                         String jumphost = jurl.substring(7);  // "http://"
                         jumphost = jumphost.substring(0, jumphost.indexOf('/'));
-                        Destination dest = I2PAppContext.getGlobalContext().namingService().lookup(jumphost);
-                        if (dest == null) continue;
+                        if (!jumphost.endsWith(".i2p"))
+                            continue;
+                        if (!jumphost.endsWith(".b32.i2p")) {
+                            Destination dest = I2PAppContext.getGlobalContext().namingService().lookup(jumphost);
+                            if (dest == null) continue;
+                        }
 
                         out.write("<br><a href=\"".getBytes());
                         out.write(jurl.getBytes());
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
index 131a02dbcadea22ce8716f821dce8d8d417973e1..0f1b35c3295d27805cf4dae23035e45f942501f6 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
@@ -25,7 +25,7 @@ import net.i2p.util.Log;
  * @since 0.8.2
  */
 public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implements Runnable {
-    private static final Log _log = new Log(I2PTunnelHTTPClientBase.class);
+
     protected final List<String> _proxyList;
 
     protected final static byte[] ERR_NO_OUTPROXY =
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java
index 921d8992bf731d3cba65673e0b0d7f1fc3822e97..d5af6bfd05bd3efd3429d57e05d46e6c6bf357c7 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java
@@ -31,7 +31,7 @@ import net.i2p.data.Base32;
  *
  */
 public class I2PTunnelHTTPServer extends I2PTunnelServer {
-    private final static Log _log = new Log(I2PTunnelHTTPServer.class);
+
     /** what Host: should we seem to be to the webserver? */
     private String _spoofHost;
     private static final String HASH_HEADER = "X-I2P-DestHash";
@@ -40,6 +40,20 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
     private static final String[] CLIENT_SKIPHEADERS = {HASH_HEADER, DEST64_HEADER, DEST32_HEADER};
     private static final String SERVER_HEADER = "Server";
     private static final String[] SERVER_SKIPHEADERS = {SERVER_HEADER};
+    private static final long HEADER_TIMEOUT = 60*1000;
+
+    private final static byte[] ERR_UNAVAILABLE =
+        ("HTTP/1.1 503 Service Unavailable\r\n"+
+         "Content-Type: text/html; charset=iso-8859-1\r\n"+
+         "Cache-control: no-cache\r\n"+
+         "Connection: close\r\n"+
+         "Proxy-Connection: close\r\n"+
+         "\r\n"+
+         "<html><head><title>503 Service Unavailable<title></head>\n"+
+         "<body><h2>503 Service Unavailable</h2>\n" +
+         "<p>This I2P eepsite is unavailable. It may be down or undergoing maintenance.</p>\n" +
+         "</body></html>")
+         .getBytes();
 
     public I2PTunnelHTTPServer(InetAddress host, int port, String privData, String spoofHost, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) {
         super(host, port, privData, l, notifyThis, tunnel);
@@ -73,8 +87,9 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
         //local is fast, so synchronously. Does not need that many
         //threads.
         try {
-            // give them 5 seconds to send in the HTTP request
-            socket.setReadTimeout(5*1000);
+            // The headers _should_ be in the first packet, but
+            // may not be, depending on the client-side options
+            socket.setReadTimeout(HEADER_TIMEOUT);
 
             InputStream in = socket.getInputStream();
 
@@ -130,13 +145,24 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
             } else {
                 new I2PTunnelRunner(s, socket, slock, null, modifiedHeader.getBytes(), null);
             }
+
+            long afterHandle = getTunnel().getContext().clock().now();
+            long timeToHandle = afterHandle - afterAccept;
+            getTunnel().getContext().statManager().addRateData("i2ptunnel.httpserver.blockingHandleTime", timeToHandle, 0);
+            if ( (timeToHandle > 1000) && (_log.shouldLog(Log.WARN)) )
+                _log.warn("Took a while to handle the request for " + remoteHost + ':' + remotePort +
+                          " [" + timeToHandle + ", socket create: " + (afterSocket-afterAccept) + "]");
         } catch (SocketException ex) {
+            try {
+                // Send a 503, so the user doesn't get an HTTP Proxy error message
+                // and blame his router or the network.
+                socket.getOutputStream().write(ERR_UNAVAILABLE);
+            } catch (IOException ioe) {}
             try {
                 socket.close();
-            } catch (IOException ioe) {
-                if (_log.shouldLog(Log.ERROR))
-                    _log.error("Error while closing the received i2p con", ex);
-            }
+            } catch (IOException ioe) {}
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("Error connecting to HTTP server " + remoteHost + ':' + remotePort, ex);
         } catch (IOException ex) {
             try {
                 socket.close();
@@ -150,12 +176,6 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
             if (_log.shouldLog(Log.ERROR))
                 _log.error("OOM in HTTP server", oom);
         }
-
-        long afterHandle = getTunnel().getContext().clock().now();
-        long timeToHandle = afterHandle - afterAccept;
-        getTunnel().getContext().statManager().addRateData("i2ptunnel.httpserver.blockingHandleTime", timeToHandle, 0);
-        if ( (timeToHandle > 1000) && (_log.shouldLog(Log.WARN)) )
-            _log.warn("Took a while to handle the request [" + timeToHandle + ", socket create: " + (afterSocket-afterAccept) + "]");
     }
     
     private static class CompressedRequestor implements Runnable {
@@ -169,6 +189,7 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
             _headers = headers;
             _ctx = ctx;
         }
+
         public void run() {
             if (_log.shouldLog(Log.INFO))
                 _log.info("Compressed requestor running");
@@ -183,7 +204,7 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
                     _log.info("request headers: " + _headers);
                 serverout.write(_headers.getBytes());
                 browserin = _browser.getInputStream();
-                I2PAppThread sender = new I2PAppThread(new Sender(serverout, browserin, "server: browser to server"), Thread.currentThread().getName() + "hcs");
+                I2PAppThread sender = new I2PAppThread(new Sender(serverout, browserin, "server: browser to server", _log), Thread.currentThread().getName() + "hcs");
                 sender.start();
                 
                 browserout = _browser.getOutputStream();
@@ -233,14 +254,19 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
     }
 
     private static class Sender implements Runnable {
-        private OutputStream _out;
-        private InputStream _in;
-        private String _name;
-        public Sender(OutputStream out, InputStream in, String name) {
+        private final OutputStream _out;
+        private final InputStream _in;
+        private final String _name;
+        // shadows _log in super()
+        private final Log _log;
+
+        public Sender(OutputStream out, InputStream in, String name, Log log) {
             _out = out;
             _in = in;
             _name = name;
+            _log = log;
         }
+
         public void run() {
             if (_log.shouldLog(Log.INFO))
                 _log.info(_name + ": Begin sending");
@@ -277,16 +303,16 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
         protected boolean shouldCompress() { return true; }
         @Override
         protected void finishHeaders() throws IOException {
-            if (_log.shouldLog(Log.INFO))
-                _log.info("Including x-i2p-gzip as the content encoding in the response");
+            //if (_log.shouldLog(Log.INFO))
+            //    _log.info("Including x-i2p-gzip as the content encoding in the response");
             out.write("Content-encoding: x-i2p-gzip\r\n".getBytes());
             super.finishHeaders();
         }
 
         @Override
         protected void beginProcessing() throws IOException {
-            if (_log.shouldLog(Log.INFO))
-                _log.info("Beginning compression processing");
+            //if (_log.shouldLog(Log.INFO))
+            //    _log.info("Beginning compression processing");
             //out.flush();
             _gzipOut = new InternalGZIPOutputStream(out);
             out = _gzipOut;
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java
index 850a1fedc4650a2463e2b84f9ff0c6b4ddaa60c5..d5b3dda65cd4b1b1aec0521cf74b99d60f51550f 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java
@@ -20,8 +20,6 @@ import net.i2p.util.Log;
  */
 public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable {
 
-    private static final Log _log = new Log(I2PTunnelIRCClient.class);
-    
     /** used to assign unique IDs to the threads / clients.  no logic or functionality */
     private static volatile long __clientId = 0;
     
@@ -130,6 +128,8 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable
         private Socket local;
         private I2PSocket remote;
         private StringBuffer expectedPong;
+        // shadows _log in super()
+        private final Log _log = new Log(I2PTunnelIRCClient.class);
                 
         public IrcInboundFilter(Socket _local, I2PSocket _remote, StringBuffer pong) {
             local=_local;
@@ -207,6 +207,8 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable
             private Socket local;
             private I2PSocket remote;
             private StringBuffer expectedPong;
+            // shadows _log in super()
+            private final Log _log = new Log(I2PTunnelIRCClient.class);
                 
             public IrcOutboundFilter(Socket _local, I2PSocket _remote, StringBuffer pong) {
                 local=_local;
@@ -308,7 +310,7 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable
         try { command = field[idx++]; }
          catch (IndexOutOfBoundsException ioobe) // wtf, server sent borked command?
         {
-           _log.warn("Dropping defective message: index out of bounds while extracting command.");
+           //_log.warn("Dropping defective message: index out of bounds while extracting command.");
            return null;
         }
 
@@ -431,13 +433,13 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable
                 rv = "PING " + field[1];
                 expectedPong.append("PONG ").append(field[2]).append(" :").append(field[1]); // PONG serverLocation nonce
             } else {
-                if (_log.shouldLog(Log.ERROR))
-                    _log.error("IRC client sent a PING we don't understand, filtering it (\"" + s + "\")");
+                //if (_log.shouldLog(Log.ERROR))
+                //    _log.error("IRC client sent a PING we don't understand, filtering it (\"" + s + "\")");
                 rv = null;
             }
             
-            if (_log.shouldLog(Log.WARN))
-                _log.warn("sending ping [" + rv + "], waiting for [" + expectedPong + "] orig was [" + s  + "]");
+            //if (_log.shouldLog(Log.WARN))
+            //    _log.warn("sending ping [" + rv + "], waiting for [" + expectedPong + "] orig was [" + s  + "]");
             
             return rv;
         }
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java
index 1d05103e79ea3fa29e24923a4d86ca86d8444786..4537389bd2ae3443dc922966474b17c41dc0e68a 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java
@@ -61,9 +61,7 @@ public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable {
 	public static final String PROP_WEBIRC_SPOOF_IP_DEFAULT="127.0.0.1";
     public static final String PROP_HOSTNAME="ircserver.fakeHostname";
     public static final String PROP_HOSTNAME_DEFAULT="%f.b32.i2p";
-    
-    private static final Log _log = new Log(I2PTunnelIRCServer.class);
-    
+    private static final long HEADER_TIMEOUT = 60*1000;
     
     /**
      * @throws IllegalArgumentException if the I2PTunnel does not contain
@@ -108,8 +106,9 @@ public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable {
         try {
 			String modifiedRegistration;
 			if(!this.method.equals("webirc")) {
-				// give them 15 seconds to send in the request
-				socket.setReadTimeout(15*1000);
+				// The headers _should_ be in the first packet, but
+				// may not be, depending on the client-side options
+				socket.setReadTimeout(HEADER_TIMEOUT);
 				InputStream in = socket.getInputStream();
 				modifiedRegistration = filterRegistration(in, cloakDest(socket.getPeerDestination()));
 				socket.setReadTimeout(readTimeout);
@@ -126,12 +125,12 @@ public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable {
             Socket s = new Socket(remoteHost, remotePort);
             new I2PTunnelRunner(s, socket, slock, null, modifiedRegistration.getBytes(), null);
         } catch (SocketException ex) {
+            // TODO send the equivalent of a 503?
             try {
                 socket.close();
-            } catch (IOException ioe) {
-                if (_log.shouldLog(Log.ERROR))
-                    _log.error("Error while closing the received i2p con", ex);
-            }
+            } catch (IOException ioe) {}
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("Error connecting to IRC server " + remoteHost + ':' + remotePort, ex);
         } catch (IOException ex) {
             try {
                 socket.close();
@@ -181,8 +180,8 @@ public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable {
             if (++lineCount > 10)
                 throw new IOException("Too many lines before USER or SERVER, giving up");
             s = s.trim();
-            if (_log.shouldLog(Log.DEBUG))
-                _log.debug("Got line: " + s);
+            //if (_log.shouldLog(Log.DEBUG))
+            //    _log.debug("Got line: " + s);
 
             String field[]=s.split(" ",5);
             String command;
@@ -214,8 +213,8 @@ public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable {
             if ("SERVER".equalsIgnoreCase(command))
                 break;
         }
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug("All done, sending: " + buf.toString());
+        //if (_log.shouldLog(Log.DEBUG))
+        //    _log.debug("All done, sending: " + buf.toString());
         return buf.toString();
     }
     
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java
index fc39e19964cbbd7c2f93b69695c824fd0e3371f0..5427130bc781366df814519bc292a9a3338a0ac6 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java
@@ -16,6 +16,12 @@ import java.net.SocketException;
 import java.net.SocketTimeoutException;
 import java.util.Iterator;
 import java.util.Properties;
+import java.util.concurrent.Executors;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.ThreadFactory;
 
 import net.i2p.I2PAppContext;
 import net.i2p.I2PException;
@@ -30,8 +36,7 @@ import net.i2p.util.Log;
 
 public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
 
-    private final static Log _log = new Log(I2PTunnelServer.class);
-
+    protected final Log _log;
     protected I2PSocketManager sockMgr;
     protected I2PServerSocket i2pss;
 
@@ -48,12 +53,17 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
     /** default timeout to 3 minutes - override if desired */
     protected long readTimeout = DEFAULT_READ_TIMEOUT;
 
-    private static final boolean DEFAULT_USE_POOL = false;
+    /** do we use threads? default true (ignored for standard servers, always false) */
+    private static final String PROP_USE_POOL = "i2ptunnel.usePool";
+    private static final boolean DEFAULT_USE_POOL = true;
     protected static volatile long __serverId = 0;
+    /** max number of threads  - this many slowlorisses will DOS this server, but too high could OOM the JVM */
     private static final String PROP_HANDLER_COUNT = "i2ptunnel.blockingHandlerCount";
-    private static final int DEFAULT_HANDLER_COUNT = 10;
-
-
+    private static final int DEFAULT_HANDLER_COUNT = 65;
+    /** min number of threads */
+    private static final int MIN_HANDLERS = 0;
+    /** how long to wait before dropping an idle thread */
+    private static final long HANDLER_KEEPALIVE_MS = 30*1000;
 
     protected I2PTunnelTask task = null;
     protected boolean bidir = false;
@@ -67,8 +77,8 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
      */
     public I2PTunnelServer(InetAddress host, int port, String privData, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) {
         super("Server at " + host + ':' + port, notifyThis, tunnel);
+        _log = tunnel.getContext().logManager().getLog(getClass());
         ByteArrayInputStream bais = new ByteArrayInputStream(Base64.decode(privData));
-        SetUsePool(tunnel);
         init(host, port, bais, privData, l);
     }
 
@@ -79,7 +89,7 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
     public I2PTunnelServer(InetAddress host, int port, File privkey, String privkeyname, Logging l,
                            EventDispatcher notifyThis, I2PTunnel tunnel) {
         super("Server at " + host + ':' + port, notifyThis, tunnel);
-        SetUsePool(tunnel);
+        _log = tunnel.getContext().logManager().getLog(getClass());
         FileInputStream fis = null;
         try {
             fis = new FileInputStream(privkey);
@@ -99,19 +109,10 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
      */
     public I2PTunnelServer(InetAddress host, int port, InputStream privData, String privkeyname, Logging l,  EventDispatcher notifyThis, I2PTunnel tunnel) {
         super("Server at " + host + ':' + port, notifyThis, tunnel);
-        SetUsePool(tunnel);
+        _log = tunnel.getContext().logManager().getLog(getClass());
         init(host, port, privData, privkeyname, l);
     }
 
-
-    private void SetUsePool(I2PTunnel Tunnel) {
-        String usePool = Tunnel.getClientOptions().getProperty("i2ptunnel.usePool");
-        if (usePool != null)
-            _usePool = "true".equalsIgnoreCase(usePool);
-        else
-            _usePool = DEFAULT_USE_POOL;
-    }
-
     private static final int RETRY_DELAY = 20*1000;
     private static final int MAX_RETRIES = 4;
 
@@ -143,6 +144,16 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
             return;
         }
 
+        // extending classes default to threaded, but for a standard server, we can't get slowlorissed
+        _usePool = !getClass().equals(I2PTunnelServer.class);
+        if (_usePool) {
+            String usePool = getTunnel().getClientOptions().getProperty(PROP_USE_POOL);
+            if (usePool != null)
+                _usePool = "true".equalsIgnoreCase(usePool);
+            else
+                _usePool = DEFAULT_USE_POOL;
+        }
+
         // Todo: Can't stop a tunnel from the UI while it's in this loop (no session yet)
         int retries = 0;
         while (sockMgr == null) {
@@ -199,8 +210,7 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
      *
      */
     public void startRunning() {
-        Thread t = new I2PAppThread(this);
-        t.setName("Server " + (++__serverId));
+        Thread t = new I2PAppThread(this, "Server " + remoteHost + ':' + remotePort, true);
         t.start();
     }
 
@@ -236,7 +246,7 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
                 }
                 return false;
             }
-            l.log("Stopping tunnels for server at " + getTunnel().listenHost + ':' + this.remotePort);
+            l.log("Stopping tunnels for server at " + this.remoteHost + ':' + this.remotePort);
             try {
                 if (i2pss != null) i2pss.close();
                 getTunnel().removeSession(sockMgr.getSession());
@@ -259,67 +269,106 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
                 rv = Integer.parseInt(cnt);
                 if (rv <= 0)
                     rv = DEFAULT_HANDLER_COUNT;
-            } catch (NumberFormatException nfe) {
-                rv = DEFAULT_HANDLER_COUNT;
-            }
+            } catch (NumberFormatException nfe) {}
         }
         return rv;
     }
     
+    /**
+     *  If usePool is set, this starts the executor pool.
+     *  Then, do the accept() loop, and either
+     *  hands each I2P socket to the executor or runs it in-line.
+     */
     public void run() {
-        if (shouldUsePool()) {
-            I2PServerSocket i2pS_S = sockMgr.getServerSocket();
-            int handlers = getHandlerCount();
-            for (int i = 0; i < handlers; i++) {
-                I2PAppThread handler = new I2PAppThread(new Handler(i2pS_S), "Handle Server " + i);
-                handler.start();
-            }
-        } else {
-            I2PServerSocket i2pS_S = sockMgr.getServerSocket();
-            while (true) {
-                try {
-                    final I2PSocket i2ps = i2pS_S.accept();
-                    if (i2ps == null) throw new I2PException("I2PServerSocket closed");
-                    new I2PAppThread(new Runnable() { public void run() { blockingHandle(i2ps); } }).start();
-                } catch (I2PException ipe) {
-                    if (_log.shouldLog(Log.ERROR))
-                        _log.error("Error accepting - KILLING THE TUNNEL SERVER", ipe);
-                    return;
-                } catch (ConnectException ce) {
-                    if (_log.shouldLog(Log.ERROR))
-                        _log.error("Error accepting", ce);
-                    // not killing the server..
-                } catch(SocketTimeoutException ste) {
-                    // ignored, we never set the timeout
+        I2PServerSocket i2pS_S = sockMgr.getServerSocket();
+        ThreadPoolExecutor executor = null;
+        if (_log.shouldLog(Log.WARN)) {
+            if (_usePool)
+                _log.warn("Starting executor with " + getHandlerCount() + " threads max");
+            else
+                _log.warn("Threads disabled, running blockingHandles inline");
+        }
+        if (_usePool) {
+            executor = new CustomThreadPoolExecutor(getHandlerCount(), "ServerHandler pool " + remoteHost + ':' + remotePort);
+        }
+        while (open) {
+            try {
+                final I2PSocket i2ps = i2pS_S.accept();
+                if (i2ps == null) throw new I2PException("I2PServerSocket closed");
+                if (_usePool) {
+                    try {
+                        executor.execute(new Handler(i2ps));
+                    } catch (RejectedExecutionException ree) {
+                         try {
+                             i2ps.close();
+                         } catch (IOException ioe) {}
+                         if (open && _log.shouldLog(Log.ERROR))
+                             _log.error("ServerHandler queue full for " + remoteHost + ':' + remotePort +
+                                        "; increase " + PROP_HANDLER_COUNT + '?', ree);
+                    }
+                } else {
+                    // use only for standard servers that can't get slowlorissed! Not for http or irc
+                    blockingHandle(i2ps);
                 }
+            } catch (I2PException ipe) {
+                if (_log.shouldLog(Log.ERROR))
+                    _log.error("Error accepting - KILLING THE TUNNEL SERVER", ipe);
+                return;
+            } catch (ConnectException ce) {
+                if (_log.shouldLog(Log.ERROR))
+                    _log.error("Error accepting", ce);
+                // not killing the server..
+                try {
+                    Thread.currentThread().sleep(500);
+                } catch (InterruptedException ie) {}
+            } catch(SocketTimeoutException ste) {
+                // ignored, we never set the timeout
             }
         }
+        if (executor != null)
+            executor.shutdownNow();
     }
     
+    /**
+     * Not really needed for now but in case we want to add some hooks like afterExecute().
+     */
+    private static class CustomThreadPoolExecutor extends ThreadPoolExecutor {
+        public CustomThreadPoolExecutor(int max, String name) {
+             super(MIN_HANDLERS, max, HANDLER_KEEPALIVE_MS, TimeUnit.MILLISECONDS,
+                   new SynchronousQueue(), new CustomThreadFactory(name));
+        }
+    }
+
+    /** just to set the name and set Daemon */
+    private static class CustomThreadFactory implements ThreadFactory {
+        private String _name;
+
+        public CustomThreadFactory(String name) {
+            _name = name;
+        }
+
+        public Thread newThread(Runnable r) {
+            Thread rv = Executors.defaultThreadFactory().newThread(r);
+            rv.setName(_name);
+            rv.setDaemon(true);
+            return rv;
+        }
+    }
+
     public boolean shouldUsePool() { return _usePool; }
     
     /**
-     * minor thread pool to pull off the accept() concurrently.  there are still lots
-     * (and lots) of wasted threads within the I2PTunnelRunner, but its a start
-     *
+     * Run the blockingHandler.
      */
     private class Handler implements Runnable { 
-        private I2PServerSocket _serverSocket;
-        public Handler(I2PServerSocket serverSocket) {
-            _serverSocket = serverSocket;
+        private I2PSocket _i2ps;
+
+        public Handler(I2PSocket socket) {
+            _i2ps = socket;
         }
+
         public void run() {
-            while (open) {
-                try {
-                    blockingHandle(_serverSocket.accept());   
-                } catch (I2PException ex) {
-                    _log.error("Error while waiting for I2PConnections", ex);
-                    return;
-                } catch (IOException ex) {
-                    _log.error("Error while waiting for I2PConnections", ex);
-                    return;
-                }
-            }
+            blockingHandle(_i2ps);   
         }
     }
     
@@ -335,20 +384,21 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable {
             Socket s = new Socket(remoteHost, remotePort);
             afterSocket = I2PAppContext.getGlobalContext().clock().now();
             new I2PTunnelRunner(s, socket, slock, null, null);
+
+            long afterHandle = I2PAppContext.getGlobalContext().clock().now();
+            long timeToHandle = afterHandle - afterAccept;
+            if ( (timeToHandle > 1000) && (_log.shouldLog(Log.WARN)) )
+                _log.warn("Took a while to handle the request for " + remoteHost + ':' + remotePort +
+                          " [" + timeToHandle + ", socket create: " + (afterSocket-afterAccept) + "]");
         } catch (SocketException ex) {
             try {
                 socket.close();
-            } catch (IOException ioe) {
-                _log.error("Error while closing the received i2p con", ex);
-            }
+            } catch (IOException ioe) {}
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("Error connecting to server " + remoteHost + ':' + remotePort, ex);
         } catch (IOException ex) {
             _log.error("Error while waiting for I2PConnections", ex);
         }
-
-        long afterHandle = I2PAppContext.getGlobalContext().clock().now();
-        long timeToHandle = afterHandle - afterAccept;
-        if (timeToHandle > 1000)
-            _log.warn("Took a while to handle the request [" + timeToHandle + ", socket create: " + (afterSocket-afterAccept) + "]");
     }
 }
 
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSIRCTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSIRCTunnel.java
index 01888d8d10481895bb96e0d782c4c3686976acc9..84e66cf7289e9f89d29c88c0b444c97c8235e702 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSIRCTunnel.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSIRCTunnel.java
@@ -30,7 +30,6 @@ import net.i2p.util.Log;
  */
 public class I2PSOCKSIRCTunnel extends I2PSOCKSTunnel {
 
-    private static final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(I2PSOCKSIRCTunnel.class);
     private static int __clientId = 0;
 
     /** @param pkf private key file name or null for transient key */
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java
index 14cafbdfdc5513079a93386f849e6b81f4f4c62f..10d51fe2e1dbc432990e6d3d794b8f57abff7c66 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java
@@ -26,7 +26,6 @@ import net.i2p.util.Log;
 
 public class I2PSOCKSTunnel extends I2PTunnelClientBase {
 
-    private static final Log _log = new Log(I2PSOCKSTunnel.class);
     private HashMap<String, List<String>> proxies = null;  // port# + "" or "default" -> hostname list
     protected Destination outProxyDest = null;
 
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPClientBase.java
index 38c82a70c54f4a90d32302cffd683e75521cc049..d9c5fcd03d760992da1c6b4931165533d2be917a 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPClientBase.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPClientBase.java
@@ -45,7 +45,6 @@ import net.i2p.util.Log;
      */
  public abstract class I2PTunnelUDPClientBase extends I2PTunnelTask implements Source, Sink {
 
-    private static final Log _log = new Log(I2PTunnelUDPClientBase.class);
     protected I2PAppContext _context;
     protected Logging l;
 
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPServerBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPServerBase.java
index 6ba8379f9453e647fb2485cfd798f7c7542de67a..4d43d53083a8c940919b48953e24f648522442b3 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPServerBase.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPServerBase.java
@@ -46,7 +46,7 @@ import net.i2p.util.Log;
 
 public class I2PTunnelUDPServerBase extends I2PTunnelTask implements Source, Sink {
 
-    private final static Log _log = new Log(I2PTunnelUDPServerBase.class);
+    private final Log _log;
 
     private final Object lock = new Object();
     protected Object slock = new Object();
@@ -73,6 +73,7 @@ public class I2PTunnelUDPServerBase extends I2PTunnelTask implements Source, Sin
     public I2PTunnelUDPServerBase(boolean verify, File privkey, String privkeyname, Logging l,
                            EventDispatcher notifyThis, I2PTunnel tunnel) {
         super("UDPServer <- " + privkeyname, notifyThis, tunnel);
+        _log = tunnel.getContext().logManager().getLog(I2PTunnelUDPServerBase.class);
         FileInputStream fis = null;
         try {
             fis = new FileInputStream(privkey);
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java
index b60dc289eb76eabccbe2a53202ecfa751249afad..87beb689cb3022bf97268321896537c59db2cbef 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java
@@ -18,6 +18,7 @@ import net.i2p.data.Destination;
 import net.i2p.data.PrivateKeyFile;
 import net.i2p.data.Signature;
 import net.i2p.data.SigningPrivateKey;
+import net.i2p.i2ptunnel.I2PTunnelHTTPClient;
 import net.i2p.i2ptunnel.I2PTunnelHTTPClientBase;
 import net.i2p.i2ptunnel.TunnelController;
 import net.i2p.i2ptunnel.TunnelControllerGroup;
@@ -171,14 +172,23 @@ public class EditBean extends IndexBean {
         return getProperty(tunnel, "i2cp.leaseSetKey", "");
     }
     
-    public boolean getAccess(int tunnel) {
-        return getBooleanProperty(tunnel, "i2cp.enableAccessList");
+    public String getAccessMode(int tunnel) {
+        if (getBooleanProperty(tunnel, PROP_ENABLE_ACCESS_LIST))
+            return "1";
+        if (getBooleanProperty(tunnel, PROP_ENABLE_BLACKLIST))
+            return "2";
+        return "0";
     }
     
     public String getAccessList(int tunnel) {
         return getProperty(tunnel, "i2cp.accessList", "").replace(",", "\n");
     }
     
+    public String getJumpList(int tunnel) {
+        return getProperty(tunnel, I2PTunnelHTTPClient.PROP_JUMP_SERVERS,
+                           I2PTunnelHTTPClient.DEFAULT_JUMP_SERVERS).replace(",", "\n");
+    }
+    
     public boolean getClose(int tunnel) {
         return getBooleanProperty(tunnel, "i2cp.closeOnIdle");
     }
@@ -234,6 +244,35 @@ public class EditBean extends IndexBean {
         return getProperty(tunnel, I2PTunnelHTTPClientBase.PROP_OUTPROXY_PW, "");
     }
     
+    /** all of these are @since 0.8.3 */
+    public String getLimitMinute(int tunnel) {
+        return getProperty(tunnel, PROP_MAX_CONNS_MIN, "0");
+    }
+
+    public String getLimitHour(int tunnel) {
+        return getProperty(tunnel, PROP_MAX_CONNS_HOUR, "0");
+    }
+
+    public String getLimitDay(int tunnel) {
+        return getProperty(tunnel, PROP_MAX_CONNS_DAY, "0");
+    }
+
+    public String getTotalMinute(int tunnel) {
+        return getProperty(tunnel, PROP_MAX_TOTAL_CONNS_MIN, "0");
+    }
+
+    public String getTotalHour(int tunnel) {
+        return getProperty(tunnel, PROP_MAX_TOTAL_CONNS_HOUR, "0");
+    }
+
+    public String getTotalDay(int tunnel) {
+        return getProperty(tunnel, PROP_MAX_TOTAL_CONNS_DAY, "0");
+    }
+
+    public String getMaxStreams(int tunnel) {
+        return getProperty(tunnel, PROP_MAX_STREAMS, "0");
+    }
+
     private int getProperty(int tunnel, String prop, int def) {
         TunnelController tun = getController(tunnel);
         if (tun != null) {
@@ -270,7 +309,14 @@ public class EditBean extends IndexBean {
         return false;
     }
     
+    /** @since 0.8.3 */
+    public boolean isRouterContext() {
+        return _context.isRouterContext();
+    }
+
     public String getI2CPHost(int tunnel) {
+        if (_context.isRouterContext())
+            return _("internal");
         TunnelController tun = getController(tunnel);
         if (tun != null)
             return tun.getI2CPHost();
@@ -279,6 +325,8 @@ public class EditBean extends IndexBean {
     }
     
     public String getI2CPPort(int tunnel) {
+        if (_context.isRouterContext())
+            return _("internal");
         TunnelController tun = getController(tunnel);
         if (tun != null)
             return tun.getI2CPPort();
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
index 2035f4627963baa237cb09a13ed5a19804b93331..bb1a339c294140da226b3711aa5066c6265eda61 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
@@ -24,6 +24,7 @@ import net.i2p.data.Certificate;
 import net.i2p.data.Destination;
 import net.i2p.data.PrivateKeyFile;
 import net.i2p.data.SessionKey;
+import net.i2p.i2ptunnel.I2PTunnelHTTPClient;
 import net.i2p.i2ptunnel.I2PTunnelHTTPClientBase;
 import net.i2p.i2ptunnel.TunnelController;
 import net.i2p.i2ptunnel.TunnelControllerGroup;
@@ -537,11 +538,11 @@ public class IndexBean {
     public void setDescription(String description) { 
         _description = (description != null ? description.trim() : null);
     }
-    /** I2CP host the router is on */
+    /** I2CP host the router is on, ignored when in router context */
     public void setClientHost(String host) {
         _i2cpHost = (host != null ? host.trim() : null);
     }
-    /** I2CP port the router is on */
+    /** I2CP port the router is on, ignored when in router context */
     public void setClientport(String port) {
         _i2cpPort = (port != null ? port.trim() : null);
     }
@@ -643,9 +644,17 @@ public class IndexBean {
     public void setEncrypt(String moo) {
         _booleanOptions.add("i2cp.encryptLeaseSet");
     }
-    public void setAccess(String moo) {
-        _booleanOptions.add("i2cp.enableAccessList");
+
+    protected static final String PROP_ENABLE_ACCESS_LIST = "i2cp.enableAccessList";
+    protected static final String PROP_ENABLE_BLACKLIST = "i2cp.enableBlackList";
+
+    public void setAccessMode(String val) {
+        if ("1".equals(val))
+            _booleanOptions.add(PROP_ENABLE_ACCESS_LIST);
+        else if ("2".equals(val))
+            _booleanOptions.add(PROP_ENABLE_BLACKLIST);
     }
+
     public void setDelayOpen(String moo) {
         _booleanOptions.add("i2cp.delayOpen");
     }
@@ -671,10 +680,17 @@ public class IndexBean {
         if (val != null)
             _otherOptions.put("i2cp.leaseSetKey", val.trim());
     }
+
     public void setAccessList(String val) {
         if (val != null)
             _otherOptions.put("i2cp.accessList", val.trim().replace("\r\n", ",").replace("\n", ",").replace(" ", ","));
     }
+
+    public void setJumpList(String val) {
+        if (val != null)
+            _otherOptions.put(I2PTunnelHTTPClient.PROP_JUMP_SERVERS, val.trim().replace("\r\n", ",").replace("\n", ",").replace(" ", ","));
+    }
+
     public void setCloseTime(String val) {
         if (val != null) {
             try {
@@ -712,6 +728,50 @@ public class IndexBean {
             _otherOptions.put(I2PTunnelHTTPClientBase.PROP_OUTPROXY_PW, s.trim());
     }
     
+    /** all of these are @since 0.8.3 */
+    protected static final String PROP_MAX_CONNS_MIN = "i2p.streaming.maxConnsPerMinute";
+    protected static final String PROP_MAX_CONNS_HOUR = "i2p.streaming.maxConnsPerHour";
+    protected static final String PROP_MAX_CONNS_DAY = "i2p.streaming.maxConnsPerDay";
+    protected static final String PROP_MAX_TOTAL_CONNS_MIN = "i2p.streaming.maxTotalConnsPerMinute";
+    protected static final String PROP_MAX_TOTAL_CONNS_HOUR = "i2p.streaming.maxTotalConnsPerHour";
+    protected static final String PROP_MAX_TOTAL_CONNS_DAY = "i2p.streaming.maxTotalConnsPerDay";
+    protected static final String PROP_MAX_STREAMS = "i2p.streaming.maxConcurrentStreams";
+
+    public void setLimitMinute(String s) {
+        if (s != null)
+            _otherOptions.put(PROP_MAX_CONNS_MIN, s.trim());
+    }
+
+    public void setLimitHour(String s) {
+        if (s != null)
+            _otherOptions.put(PROP_MAX_CONNS_HOUR, s.trim());
+    }
+
+    public void setLimitDay(String s) {
+        if (s != null)
+            _otherOptions.put(PROP_MAX_CONNS_DAY, s.trim());
+    }
+
+    public void setTotalMinute(String s) {
+        if (s != null)
+            _otherOptions.put(PROP_MAX_TOTAL_CONNS_MIN, s.trim());
+    }
+
+    public void setTotalHour(String s) {
+        if (s != null)
+            _otherOptions.put(PROP_MAX_TOTAL_CONNS_HOUR, s.trim());
+    }
+
+    public void setTotalDay(String s) {
+        if (s != null)
+            _otherOptions.put(PROP_MAX_TOTAL_CONNS_DAY, s.trim());
+    }
+
+    public void setMaxStreams(String s) {
+        if (s != null)
+            _otherOptions.put(PROP_MAX_STREAMS, s.trim());
+    }
+
     /** params needed for hashcash and dest modification */
     public void setEffort(String val) {
         if (val != null) {
@@ -904,16 +964,20 @@ public class IndexBean {
         I2PTunnelHTTPClientBase.PROP_AUTH, I2PTunnelHTTPClientBase.PROP_OUTPROXY_AUTH
         };
     private static final String _booleanServerOpts[] = {
-        "i2cp.reduceOnIdle", "i2cp.encryptLeaseSet", "i2cp.enableAccessList"
+        "i2cp.reduceOnIdle", "i2cp.encryptLeaseSet", PROP_ENABLE_ACCESS_LIST, PROP_ENABLE_BLACKLIST
         };
     private static final String _otherClientOpts[] = {
         "i2cp.reduceIdleTime", "i2cp.reduceQuantity", "i2cp.closeIdleTime",
-        "proxyUsername", "proxyPassword", "outproxyUsername", "outproxyPassword"
+        "proxyUsername", "proxyPassword", "outproxyUsername", "outproxyPassword",
+        I2PTunnelHTTPClient.PROP_JUMP_SERVERS
         };
     private static final String _otherServerOpts[] = {
-        "i2cp.reduceIdleTime", "i2cp.reduceQuantity", "i2cp.leaseSetKey", "i2cp.accessList"
+        "i2cp.reduceIdleTime", "i2cp.reduceQuantity", "i2cp.leaseSetKey", "i2cp.accessList",
+         PROP_MAX_CONNS_MIN, PROP_MAX_CONNS_HOUR, PROP_MAX_CONNS_DAY,
+         PROP_MAX_TOTAL_CONNS_MIN, PROP_MAX_TOTAL_CONNS_HOUR, PROP_MAX_TOTAL_CONNS_DAY,
+         PROP_MAX_STREAMS
         };
-    protected static final Set _noShowSet = new HashSet();
+    protected static final Set _noShowSet = new HashSet(64);
     static {
         _noShowSet.addAll(Arrays.asList(_noShowOpts));
         _noShowSet.addAll(Arrays.asList(_booleanClientOpts));
@@ -929,12 +993,14 @@ public class IndexBean {
             config.setProperty("name", _name);
         if (_description != null)
             config.setProperty("description", _description);
-        if (_i2cpHost != null)
-            config.setProperty("i2cpHost", _i2cpHost);
-        if ( (_i2cpPort != null) && (_i2cpPort.trim().length() > 0) ) {
-            config.setProperty("i2cpPort", _i2cpPort);
-        } else {
-            config.setProperty("i2cpPort", "7654");
+        if (!_context.isRouterContext()) {
+            if (_i2cpHost != null)
+                config.setProperty("i2cpHost", _i2cpHost);
+            if ( (_i2cpPort != null) && (_i2cpPort.trim().length() > 0) ) {
+                config.setProperty("i2cpPort", _i2cpPort);
+            } else {
+                config.setProperty("i2cpPort", "7654");
+            }
         }
         if (_privKeyFile != null)
             config.setProperty("privKeyFile", _privKeyFile);
@@ -1020,7 +1086,7 @@ public class IndexBean {
         }
     }
 
-    private String _(String key) {
+    protected String _(String key) {
         return Messages._(key, _context);
     }
 }
diff --git a/apps/i2ptunnel/jsp/editClient.jsp b/apps/i2ptunnel/jsp/editClient.jsp
index 2b69440ac1f290e065e42f6e8d40b13e29542540..f2b427542ba1c87d60310e99174a8692fb3634b3 100644
--- a/apps/i2ptunnel/jsp/editClient.jsp
+++ b/apps/i2ptunnel/jsp/editClient.jsp
@@ -286,19 +286,19 @@
          <% } // !streamrclient %>
 
             <div id="optionsField" class="rowItem">
-                <label><%=intl._("I2CP Options")%>:</label>
+                <label><%=intl._("Router I2CP Address")%>:</label>
             </div>
             <div id="optionsHostField" class="rowItem">
                 <label for="clientHost" accesskey="o">
                     <%=intl._("Host")%>(<span class="accessKey">o</span>):
                 </label>
-                <input type="text" id="clientHost" name="clientHost" size="20" title="I2CP Hostname or IP" value="<%=editBean.getI2CPHost(curTunnel)%>" class="freetext" />                
+                <input type="text" id="clientHost" name="clientHost" size="20" title="I2CP Hostname or IP" value="<%=editBean.getI2CPHost(curTunnel)%>" class="freetext" <% if (editBean.isRouterContext()) { %> readonly="readonly" <% } %> />                
             </div>
             <div id="optionsPortField" class="rowItem">
                 <label for="clientPort" accesskey="r">
                     <%=intl._("Port")%>(<span class="accessKey">r</span>):
                 </label>
-                <input type="text" id="port" name="clientport" size="20" title="I2CP Port Number" value="<%=editBean.getI2CPPort(curTunnel)%>" class="freetext" />                
+                <input type="text" id="clientPort" name="clientport" size="20" title="I2CP Port Number" value="<%=editBean.getI2CPPort(curTunnel)%>" class="freetext" <% if (editBean.isRouterContext()) { %> readonly="readonly" <% } %> />                
             </div>
                  
          <% if (!"streamrclient".equals(tunnelType)) { // streamr client sends pings so it will never be idle %>
@@ -465,6 +465,18 @@
             </div>
          <% } // httpclient || connect || socks || socksirc %>
 
+         <% if ("httpclient".equals(tunnelType)) { %>
+            <div id="optionsField" class="rowItem">
+                <label><%=intl._("Jump URL List")%>:</label>
+            </div>
+            <div id="hostField" class="rowItem">
+                <textarea rows="2" style="height: 8em;" cols="60" id="hostField" name="jumpList" title="List of helper URLs to offer when a host is not found in your addressbook" wrap="off"><%=editBean.getJumpList(curTunnel)%></textarea>               
+            </div>
+            <div class="subdivider">
+                <hr />
+            </div>
+         <% } // httpclient %>
+
             <div id="customOptionsField" class="rowItem">
                 <label for="customOptions" accesskey="u">
                     <%=intl._("Custom options")%>(<span class="accessKey">u</span>):
diff --git a/apps/i2ptunnel/jsp/editServer.jsp b/apps/i2ptunnel/jsp/editServer.jsp
index 773d323a2bf9c88f5d683798b2e485e012f84e3b..b0f870fb7aa5797730ea6a1bc6f38ff480b17ac2 100644
--- a/apps/i2ptunnel/jsp/editServer.jsp
+++ b/apps/i2ptunnel/jsp/editServer.jsp
@@ -305,19 +305,19 @@
          <% } // !streamrserver %>
 
             <div id="optionsField" class="rowItem">
-                <label><%=intl._("I2CP Options")%>:</label>
+                <label><%=intl._("Router I2CP Address")%>:</label>
             </div>
             <div id="optionsHostField" class="rowItem">
                 <label for="clientHost" accesskey="o">
                     <%=intl._("Host")%>(<span class="accessKey">o</span>):
                 </label>
-                <input type="text" id="clientHost" name="clientHost" size="20" title="I2CP Hostname or IP" value="<%=editBean.getI2CPHost(curTunnel)%>" class="freetext" />                
+                <input type="text" id="clientHost" name="clientHost" size="20" title="I2CP Hostname or IP" value="<%=editBean.getI2CPHost(curTunnel)%>" class="freetext" <% if (editBean.isRouterContext()) { %> readonly="readonly" <% } %> />                
             </div>
             <div id="optionsPortField" class="rowItem">
                 <label for="clientPort" accesskey="r">
                     <%=intl._("Port")%>(<span class="accessKey">r</span>):
                 </label>
-                <input type="text" id="clientPort" name="clientport" size="20" title="I2CP Port Number" value="<%=editBean.getI2CPPort(curTunnel)%>" class="freetext" />                
+                <input type="text" id="clientPort" name="clientport" size="20" title="I2CP Port Number" value="<%=editBean.getI2CPPort(curTunnel)%>" class="freetext" <% if (editBean.isRouterContext()) { %> readonly="readonly" <% } %> />                
             </div>
             
             <div class="subdivider">
@@ -333,7 +333,7 @@
                 <label for="encrypt" accesskey="e">
                     <%=intl._("Enable")%>:
                 </label>
-                <input value="1" type="checkbox" id="startOnLoad" name="encrypt" title="Encrypt LeaseSet"<%=(editBean.getEncrypt(curTunnel) ? " checked=\"checked\"" : "")%> class="tickbox" />                
+                <input value="1" type="checkbox" id="startOnLoad" name="encrypt" title="ONLY clients with the encryption key will be able to connect"<%=(editBean.getEncrypt(curTunnel) ? " checked=\"checked\"" : "")%> class="tickbox" />                
             </div>
             <div id="portField" class="rowItem">
                 <label for="encrypt" accesskey="e">
@@ -359,19 +359,64 @@
                 </label>
             </div>
             <div id="portField" class="rowItem">
-                <label for="access" accesskey="s">
-                    <%=intl._("Enable")%>:
-                </label>
-                <input value="1" type="checkbox" id="startOnLoad" name="access" title="Enable Access List"<%=(editBean.getAccess(curTunnel) ? " checked=\"checked\"" : "")%> class="tickbox" />                
+                <label><%=intl._("Disable")%></label>
+                <input value="0" type="radio" id="startOnLoad" name="accessMode" title="Allow all clients"<%=(editBean.getAccessMode(curTunnel).equals("0") ? " checked=\"checked\"" : "")%> class="tickbox" />                
+                <label><%=intl._("Whitelist")%></label>
+                <input value="1" type="radio" id="startOnLoad" name="accessMode" title="Allow listed clients only"<%=(editBean.getAccessMode(curTunnel).equals("1") ? " checked=\"checked\"" : "")%> class="tickbox" />                
+                <label><%=intl._("Blacklist")%></label>
+                <input value="2" type="radio" id="startOnLoad" name="accessMode" title="Reject listed clients"<%=(editBean.getAccessMode(curTunnel).equals("2") ? " checked=\"checked\"" : "")%> class="tickbox" />                
             </div>
             <div id="hostField" class="rowItem">
                 <label for="accessList" accesskey="s">
                     <%=intl._("Access List")%>:
                 </label>
-                <textarea rows="2" style="height: 4em;" cols="60" id="hostField" name="accessList" title="Access List" wrap="off"><%=editBean.getAccessList(curTunnel)%></textarea>               
-                <span class="comment"><%=intl._("(Restrict to these clients only)")%></span>
+                <textarea rows="2" style="height: 8em;" cols="60" id="hostField" name="accessList" title="Access List" wrap="off"><%=editBean.getAccessList(curTunnel)%></textarea>               
             </div>
                  
+            <div class="subdivider">
+                <hr />
+            </div>
+
+            <div class="rowItem">
+              <div id="optionsField" class="rowItem">
+                  <label><%=intl._("Inbound connection limits (0 to disable)")%><br><%=intl._("Per client")%>:</label>
+              </div>
+              <div id="portField" class="rowItem">
+                  <label><%=intl._("Per minute")%>:</label>
+                  <input type="text" id="port" name="limitMinute" value="<%=editBean.getLimitMinute(curTunnel)%>" class="freetext" />                
+              </div>
+              <div id="portField" class="rowItem">
+                  <label><%=intl._("Per hour")%>:</label>
+                  <input type="text" id="port" name="limitHour" value="<%=editBean.getLimitHour(curTunnel)%>" class="freetext" />                
+              </div>
+              <div id="portField" class="rowItem">
+                  <label><%=intl._("Per day")%>:</label>
+                  <input type="text" id="port" name="limitDay" value="<%=editBean.getLimitDay(curTunnel)%>" class="freetext" />                
+              </div>
+            </div>
+            <div class="rowItem">
+              <div id="optionsField" class="rowItem">
+                  <label><%=intl._("Total")%>:</label>
+              </div>
+              <div id="portField" class="rowItem">
+                  <input type="text" id="port" name="totalMinute" value="<%=editBean.getTotalMinute(curTunnel)%>" class="freetext" />                
+              </div>
+              <div id="portField" class="rowItem">
+                  <input type="text" id="port" name="totalHour" value="<%=editBean.getTotalHour(curTunnel)%>" class="freetext" />                
+              </div>
+              <div id="portField" class="rowItem">
+                  <input type="text" id="port" name="totalDay" value="<%=editBean.getTotalDay(curTunnel)%>" class="freetext" />                
+              </div>
+            </div>
+            <div class="rowItem">
+              <div id="optionsField" class="rowItem">
+                  <label><%=intl._("Max concurrent connections (0 to disable)")%>:</label>
+              </div>
+              <div id="portField" class="rowItem">
+                  <input type="text" id="port" name="maxStreams" value="<%=editBean.getMaxStreams(curTunnel)%>" class="freetext" />                
+              </div>
+            </div>
+
             <div class="subdivider">
                 <hr />
             </div>
diff --git a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PServerSocket.java b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PServerSocket.java
index d0028fdb81dba2d83e47844d722c4efdad513cff..9f43aa24644b1fb1f8d77c8214b7ca9b4ce4d547 100644
--- a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PServerSocket.java
+++ b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PServerSocket.java
@@ -19,9 +19,10 @@ public interface I2PServerSocket {
     /**
      * Waits for the next socket connecting.  If a remote user tried to make a 
      * connection and the local application wasn't .accept()ing new connections,
-     * they should get refused (if .accept() doesnt occur in some small period)
+     * they should get refused (if .accept() doesnt occur in some small period).
+     * Warning - unlike regular ServerSocket, may return null.
      *
-     * @return a connected I2PSocket
+     * @return a connected I2PSocket OR NULL
      *
      * @throws I2PException if there is a problem with reading a new socket
      *         from the data available (aka the I2PSession closed, etc)
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java
index 72acbb5404aa56ba05dca95e94d8c22348c5ff0c..8a525180bc384930124b543760117d26346ba7f4 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java
@@ -3,6 +3,7 @@ package net.i2p.router.web;
 import net.i2p.I2PAppContext;
 import net.i2p.crypto.TrustedUpdate;
 import net.i2p.data.DataHelper;
+import net.i2p.util.FileUtil;
 
 /**
  *
@@ -61,14 +62,10 @@ public class ConfigUpdateHandler extends FormHandler {
 
     public static final String DEFAULT_UPDATE_URL;
     static {
-        String foo;
-        try {
-            Class.forName("java.util.jar.Pack200", false, ClassLoader.getSystemClassLoader());
-            foo = PACK200_URLS;
-        } catch (ClassNotFoundException cnfe) {
-            foo = NO_PACK200_URLS;
-        }
-        DEFAULT_UPDATE_URL = foo;
+        if (FileUtil.isPack200Supported())
+            DEFAULT_UPDATE_URL = PACK200_URLS;
+        else
+            DEFAULT_UPDATE_URL = NO_PACK200_URLS;
     }
 
     public static final String PROP_TRUSTED_KEYS = "router.trustedUpdateKeys";
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/GraphHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/GraphHelper.java
index b3ce2fa83237dce542606c94fc40746a6bbb15ac..1b0e043e393067561bcb012b475a84b556ab7de8 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/GraphHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/GraphHelper.java
@@ -59,7 +59,13 @@ public class GraphHelper extends FormHandler {
         try { _width = Math.min(Integer.parseInt(str), MAX_X); } catch (NumberFormatException nfe) {}
     }
     public void setRefreshDelay(String str) {
-        try { _refreshDelaySeconds = Math.max(Integer.parseInt(str), MIN_REFRESH); } catch (NumberFormatException nfe) {}
+        try {
+            int rds = Integer.parseInt(str);
+            if (rds > 0)
+                _refreshDelaySeconds = Math.max(rds, MIN_REFRESH);
+            else
+                _refreshDelaySeconds = -1;
+        } catch (NumberFormatException nfe) {}
     }
     
     public String getImages() { 
@@ -83,7 +89,7 @@ public class GraphHelper extends FormHandler {
                            + "&amp;periodCount=" + (3 * _periodCount )
                            + "&amp;width=" + (3 * _width)
                            + "&amp;height=" + (3 * _height)
-                           + "\" / target=\"_blank\">");
+                           + "\" target=\"_blank\">");
                 String title = _("Combined bandwidth graph");
                 _out.write("<img class=\"statimage\" width=\""
                            + (_width + 83) + "\" height=\"" + (_height + 92)
@@ -129,6 +135,8 @@ public class GraphHelper extends FormHandler {
         return ""; 
     }
 
+    private static final int[] times = { 60, 2*60, 5*60, 10*60, 30*60, 60*60, -1 };
+
     public String getForm() { 
         String prev = System.getProperty("net.i2p.router.web.GraphHelper.nonce");
         if (prev != null) System.setProperty("net.i2p.router.web.GraphHelper.noncePrev", prev);
@@ -145,8 +153,22 @@ public class GraphHelper extends FormHandler {
             _out.write(_("Image sizes") + ": " + _("width") + ": <input size=\"4\" type=\"text\" name=\"width\" value=\"" + _width 
                        + "\"> " + _("pixels") + ", " + _("height") + ": <input size=\"4\" type=\"text\" name=\"height\" value=\"" + _height  
                        + "\"> " + _("pixels") + "<br>\n");
-            _out.write(_("Refresh delay") + ": <select name=\"refreshDelay\"><option value=\"60\">1 " + _("minute") + "</option><option value=\"120\">2 " + _("minutes") + "</option><option value=\"300\">5 " + _("minutes") + "</option><option value=\"600\">10 " + _("minutes") + "</option><option value=\"1800\">30 " + _("minutes") + "</option><option value=\"3600\">1 " + _("hour") + "</option><option value=\"-1\">" + _("Never") + "</option></select><br>\n");
-            _out.write("<hr><div class=\"formaction\"><input type=\"submit\" value=\"" + _("Redraw") + "\"></div></form>");
+            _out.write(_("Refresh delay") + ": <select name=\"refreshDelay\">");
+            for (int i = 0; i < times.length; i++) {
+                _out.write("<option value=\"");
+                _out.write(Integer.toString(times[i]));
+                _out.write("\"");
+                if (times[i] == _refreshDelaySeconds)
+                    _out.write(" selected=\"true\"");
+                _out.write(">");
+                if (times[i] > 0)
+                    _out.write(DataHelper.formatDuration2(times[i] * 1000));
+                else
+                    _out.write(_("Never"));
+                _out.write("</option>\n");
+            }
+            _out.write("</select><br>\n" +
+                       "<hr><div class=\"formaction\"><input type=\"submit\" value=\"" + _("Redraw") + "\"></div></form>");
         } catch (IOException ioe) {
             ioe.printStackTrace();
         }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/LocaleWebAppHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/LocaleWebAppHandler.java
index 91ede9e5297d0e89808e89b79b5d30c55380be52..ebf5bcf9c436465d7e80b29deb95534d42726001 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/LocaleWebAppHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/LocaleWebAppHandler.java
@@ -53,7 +53,8 @@ public class LocaleWebAppHandler extends WebApplicationHandler
             // home page
             pathInContext = "/index.jsp";
         } else if (pathInContext.indexOf("/", 1) < 0 &&
-                   !pathInContext.endsWith(".jsp")) {
+                   (!pathInContext.endsWith(".jsp")) &&
+                   (!pathInContext.endsWith(".txt"))) {
             // add .jsp to pages at top level
             pathInContext += ".jsp";
         }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
index 68533f7ef63c19cfce8b1b4a7c384b4b90f1b309..870f162d4f9b88a719ebe76a6543c2484aaf7b93 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
@@ -4,37 +4,53 @@ import java.util.ArrayList;
 import java.io.File;
 import java.io.FilenameFilter;
 import java.io.IOException;
+import java.security.KeyStore;
 import java.util.List;
 import java.util.Properties;
 import java.util.StringTokenizer;
 
 import net.i2p.I2PAppContext;
 import net.i2p.apps.systray.SysTray;
+import net.i2p.data.Base32;
 import net.i2p.data.DataHelper;
 import net.i2p.router.RouterContext;
 import net.i2p.util.FileUtil;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.SecureDirectory;
+import net.i2p.util.SecureFileOutputStream;
+import net.i2p.util.ShellCommand;
 
 import org.mortbay.http.DigestAuthenticator;
 import org.mortbay.http.HashUserRealm;
 import org.mortbay.http.SecurityConstraint;
+import org.mortbay.http.SslListener;
 import org.mortbay.http.handler.SecurityHandler;
 import org.mortbay.jetty.Server;
 import org.mortbay.jetty.servlet.WebApplicationContext;
 import org.mortbay.jetty.servlet.WebApplicationHandler;
+import org.mortbay.util.InetAddrPort;
 
 public class RouterConsoleRunner {
     private Server _server;
-    private String _listenPort = "7657";
-    private String _listenHost = "127.0.0.1";
-    private String _webAppsDir = "./webapps/";
+    private String _listenPort;
+    private String _listenHost;
+    private String _sslListenPort;
+    private String _sslListenHost;
+    private String _webAppsDir;
     private static final String PROP_WEBAPP_CONFIG_FILENAME = "router.webappsConfigFile";
     private static final String DEFAULT_WEBAPP_CONFIG_FILENAME = "webapps.config";
     private static final DigestAuthenticator authenticator = new DigestAuthenticator();
     public static final String ROUTERCONSOLE = "routerconsole";
     public static final String PREFIX = "webapps.";
     public static final String ENABLED = ".startOnLoad";
+    private static final String PROP_KEYSTORE_PASSWORD = "routerconsole.keystorePassword";
+    private static final String DEFAULT_KEYSTORE_PASSWORD = "changeit";
+    private static final String PROP_KEY_PASSWORD = "routerconsole.keyPassword";
+    private static final String DEFAULT_LISTEN_PORT = "7657";
+    private static final String DEFAULT_LISTEN_HOST = "127.0.0.1";
+    private static final String DEFAULT_WEBAPPS_DIR = "./webapps/";
+    private static final String USAGE = "Bad RouterConsoleRunner arguments, check clientApp.0.args in your clients.config file! " +
+                                        "Usage: [[port host[,host]] [-s sslPort [host[,host]]] [webAppsDir]]";
     
     static {
         System.setProperty("org.mortbay.http.Version.paranoid", "true");
@@ -42,6 +58,27 @@ public class RouterConsoleRunner {
     }
     
     /**
+     *  <pre>
+     *  non-SSL:
+     *  RouterConsoleRunner
+     *  RouterConsoleRunner 7657
+     *  RouterConsoleRunner 7657 127.0.0.1
+     *  RouterConsoleRunner 7657 127.0.0.1,::1
+     *  RouterConsoleRunner 7657 127.0.0.1,::1 ./webapps/
+     *
+     *  SSL:
+     *  RouterConsoleRunner -s 7657
+     *  RouterConsoleRunner -s 7657 127.0.0.1
+     *  RouterConsoleRunner -s 7657 127.0.0.1,::1
+     *  RouterConsoleRunner -s 7657 127.0.0.1,::1 ./webapps/
+     *
+     *  If using both, non-SSL must be first:
+     *  RouterConsoleRunner 7657 127.0.0.1 -s 7667
+     *  RouterConsoleRunner 7657 127.0.0.1 -s 7667 127.0.0.1
+     *  RouterConsoleRunner 7657 127.0.0.1,::1 -s 7667 127.0.0.1,::1
+     *  RouterConsoleRunner 7657 127.0.0.1,::1 -s 7667 127.0.0.1,::1 ./webapps/
+     *  </pre>
+     *
      *  @param args second arg may be a comma-separated list of bind addresses,
      *              for example ::1,127.0.0.1
      *              On XP, the other order (127.0.0.1,::1) fails the IPV6 bind,
@@ -50,10 +87,40 @@ public class RouterConsoleRunner {
      *              So the wise choice is ::1,127.0.0.1
      */
     public RouterConsoleRunner(String args[]) {
-        if (args.length == 3) {
-            _listenPort = args[0].trim();
-            _listenHost = args[1].trim();
-            _webAppsDir = args[2].trim();
+        if (args.length == 0) {
+            // _listenHost and _webAppsDir are defaulted below
+            _listenPort = DEFAULT_LISTEN_PORT;
+        } else {
+            boolean ssl = false;
+            for (int i = 0; i < args.length; i++) {
+                if (args[i].equals("-s"))
+                    ssl = true;
+                else if ((!ssl) && _listenPort == null)
+                    _listenPort = args[i];
+                else if ((!ssl) && _listenHost == null)
+                    _listenHost = args[i];
+                else if (ssl && _sslListenPort == null)
+                    _sslListenPort = args[i];
+                else if (ssl && _sslListenHost == null)
+                    _sslListenHost = args[i];
+                else if (_webAppsDir == null)
+                    _webAppsDir = args[i];
+                else {
+                    System.err.println(USAGE);
+                    throw new IllegalArgumentException(USAGE);
+                }
+            }
+        }
+        if (_listenHost == null)
+           _listenHost = DEFAULT_LISTEN_HOST;
+        if (_sslListenHost == null)
+           _sslListenHost = _listenHost;
+        if (_webAppsDir == null)
+           _webAppsDir = DEFAULT_WEBAPPS_DIR;
+        // _listenPort and _sslListenPort are not defaulted, if one or the other is null, do not enable
+        if (_listenPort == null && _sslListenPort == null) {
+            System.err.println(USAGE);
+            throw new IllegalArgumentException(USAGE);
         }
     }
     
@@ -96,22 +163,63 @@ public class RouterConsoleRunner {
         List<String> notStarted = new ArrayList();
         WebApplicationHandler baseHandler = null;
         try {
-            StringTokenizer tok = new StringTokenizer(_listenHost, " ,");
             int boundAddresses = 0;
-            while (tok.hasMoreTokens()) {
-                String host = tok.nextToken().trim();
+
+            // add standard listeners
+            if (_listenPort != null) {
+                StringTokenizer tok = new StringTokenizer(_listenHost, " ,");
+                while (tok.hasMoreTokens()) {
+                    String host = tok.nextToken().trim();
+                    try {
+                        if (host.indexOf(":") >= 0) // IPV6 - requires patched Jetty 5
+                            _server.addListener('[' + host + "]:" + _listenPort);
+                        else
+                            _server.addListener(host + ':' + _listenPort);
+                        boundAddresses++;
+                    } catch (IOException ioe) { // this doesn't seem to work, exceptions don't happen until start() below
+                        System.err.println("Unable to bind routerconsole to " + host + " port " + _listenPort + ' ' + ioe);
+                    }
+                }
+            }
+
+            // add SSL listeners
+            int sslPort = 0;
+            if (_sslListenPort != null) {
                 try {
-                    if (host.indexOf(":") >= 0) // IPV6 - requires patched Jetty 5
-                        _server.addListener('[' + host + "]:" + _listenPort);
-                    else
-                        _server.addListener(host + ':' + _listenPort);
-                    boundAddresses++;
-                } catch (IOException ioe) { // this doesn't seem to work, exceptions don't happen until start() below
-                    System.err.println("Unable to bind routerconsole to " + host + " port " + _listenPort + ' ' + ioe);
+                    sslPort = Integer.parseInt(_sslListenPort);
+                } catch (NumberFormatException nfe) {}
+                if (sslPort <= 0)
+                    System.err.println("Bad routerconsole SSL port " + _sslListenPort);
+            }
+            if (sslPort > 0) {
+                I2PAppContext ctx = I2PAppContext.getGlobalContext();
+                File keyStore = new File(ctx.getConfigDir(), "keystore/console.ks");
+                if (verifyKeyStore(keyStore)) {
+                    StringTokenizer tok = new StringTokenizer(_sslListenHost, " ,");
+                    while (tok.hasMoreTokens()) {
+                        String host = tok.nextToken().trim();
+                        // doing it this way means we don't have to escape an IPv6 host with []
+                        InetAddrPort iap = new InetAddrPort(host, sslPort);
+                        try {
+                            SslListener ssll = new SslListener(iap);
+                            // the keystore path and password
+                            ssll.setKeystore(keyStore.getAbsolutePath());
+                            ssll.setPassword(ctx.getProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD));
+                            // the X.509 cert password (if not present, verifyKeyStore() returned false)
+                            ssll.setKeyPassword(ctx.getProperty(PROP_KEY_PASSWORD, "thisWontWork"));
+                            _server.addListener(ssll);
+                            boundAddresses++;
+                        } catch (Exception e) {   // probably no exceptions at this point
+                            System.err.println("Unable to bind routerconsole to " + host + " port " + _listenPort + " for SSL: " + e);
+                        }
+                    }
+                } else {
+                    System.err.println("Unable to create or access keystore for SSL: " + keyStore.getAbsolutePath());
                 }
             }
+
             if (boundAddresses <= 0) {
-                System.err.println("Unable to bind routerconsole to any address on port " + _listenPort);
+                System.err.println("Unable to bind routerconsole to any address on port " + _listenPort + (sslPort > 0 ? (" or SSL port " + sslPort) : ""));
                 return;
             }
             _server.setRootWebApp(ROUTERCONSOLE);
@@ -201,6 +309,90 @@ public class RouterConsoleRunner {
         }
     }
     
+    /**
+     * @return success if it exists and we have a password, or it was created successfully.
+     * @since 0.8.3
+     */
+    private static boolean verifyKeyStore(File ks) {
+        if (ks.exists()) {
+            I2PAppContext ctx = I2PAppContext.getGlobalContext();
+            boolean rv = ctx.getProperty(PROP_KEY_PASSWORD) != null;
+            if (!rv)
+                System.err.println("Console SSL error, must set " + PROP_KEY_PASSWORD + " in " + (new File(ctx.getConfigDir(), "router.config")).getAbsolutePath());
+            return rv;
+        }
+        File dir = ks.getParentFile();
+        if (!dir.exists()) {
+            File sdir = new SecureDirectory(dir.getAbsolutePath());
+            if (!sdir.mkdir())
+                return false;
+        }
+        return createKeyStore(ks);
+    }
+
+
+    /**
+     * Call out to keytool to create a new keystore with a keypair in it.
+     * Trying to do this programatically is a nightmare, requiring either BouncyCastle
+     * libs or using proprietary Sun libs, and it's a huge mess.
+     *
+     * @return success
+     * @since 0.8.3
+     */
+    private static boolean createKeyStore(File ks) {
+        I2PAppContext ctx = I2PAppContext.getGlobalContext();
+        // make a random 48 character password (30 * 8 / 5)
+        byte[] rand = new byte[30];
+        ctx.random().nextBytes(rand);
+        String keyPassword = Base32.encode(rand);
+        // and one for the cname
+        ctx.random().nextBytes(rand);
+        String cname = Base32.encode(rand) + ".console.i2p.net";
+
+        String keytool = (new File(System.getProperty("java.home"), "bin/keytool")).getAbsolutePath();
+        String[] args = new String[] {
+                   keytool,
+                   "-genkey",            // -genkeypair preferred in newer keytools, but this works with more
+                   "-storetype", KeyStore.getDefaultType(),
+                   "-keystore", ks.getAbsolutePath(),
+                   "-storepass", DEFAULT_KEYSTORE_PASSWORD,
+                   "-alias", "console",
+                   "-dname", "CN=" + cname + ",OU=Console,O=I2P Anonymous Network,L=XX,ST=XX,C=XX",
+                   "-validity", "3652",  // 10 years
+                   "-keyalg", "DSA",
+                   "-keysize", "1024",
+                   "-keypass", keyPassword};
+        boolean success = (new ShellCommand()).executeSilentAndWaitTimed(args, 30);  // 30 secs
+        if (success) {
+            success = ks.exists();
+            if (success) {
+                SecureFileOutputStream.setPerms(ks);
+                try {
+                    RouterContext rctx = (RouterContext) ctx;
+                    rctx.router().setConfigSetting(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD);
+                    rctx.router().setConfigSetting(PROP_KEY_PASSWORD, keyPassword);
+                    rctx.router().saveConfig();
+                } catch (Exception e) {}  // class cast exception
+            }
+        }
+        if (success) {
+            System.err.println("Created self-signed certificate for " + cname + " in keystore: " + ks.getAbsolutePath() + "\n" +
+                               "The certificate name was generated randomly, and is not associated with your " +
+                               "IP address, host name, router identity, or destination keys.");
+        } else {
+            System.err.println("Failed to create console SSL keystore using command line:");
+            StringBuilder buf = new StringBuilder(256);
+            for (int i = 0;  i < args.length; i++) {
+                buf.append('"').append(args[i]).append("\" ");
+            }
+            System.err.println(buf.toString());
+            System.err.println("This is for the Sun/Oracle keytool, others may be incompatible.\n" +
+                               "If you create the keystore manually, you must add " + PROP_KEYSTORE_PASSWORD + " and " + PROP_KEY_PASSWORD +
+                               " to " + (new File(ctx.getConfigDir(), "router.config")).getAbsolutePath());
+        }
+        return success;
+    }
+
     static void initialize(WebApplicationContext context) {
         String password = getPassword();
         if (password != null) {
diff --git a/apps/routerconsole/jsp/viewhistory.jsp b/apps/routerconsole/jsp/viewhistory.jsp
new file mode 100644
index 0000000000000000000000000000000000000000..6268abd5a2778ef04b7907d6b10059e416e6752d
--- /dev/null
+++ b/apps/routerconsole/jsp/viewhistory.jsp
@@ -0,0 +1,12 @@
+<%
+/*
+ * USE CAUTION WHEN EDITING
+ * Trailing whitespace OR NEWLINE on the last line will cause
+ * IllegalStateExceptions !!!
+ *
+ * Do not tag this file for translation.
+ */
+response.setContentType("text/plain");
+String base = net.i2p.I2PAppContext.getGlobalContext().getBaseDir().getAbsolutePath();
+net.i2p.util.FileUtil.readFile("history.txt", base, response.getOutputStream());
+%>
\ No newline at end of file
diff --git a/apps/routerconsole/jsp/web.xml b/apps/routerconsole/jsp/web.xml
index 8679abcb773f94d7aeb6c789304c728ffb47e34d..e5bdbeb32d27599f182980802ce6e27b9f9ebf4c 100644
--- a/apps/routerconsole/jsp/web.xml
+++ b/apps/routerconsole/jsp/web.xml
@@ -17,6 +17,11 @@
       <url-pattern>/javadoc/*</url-pattern>
     </servlet-mapping>
     
+    <servlet-mapping> 
+      <servlet-name>net.i2p.router.web.jsp.viewhistory_jsp</servlet-name>
+      <url-pattern>/history.txt</url-pattern>
+    </servlet-mapping>
+    
     <session-config>
         <session-timeout>
             30
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/I2PServerSocketFull.java b/apps/streaming/java/src/net/i2p/client/streaming/I2PServerSocketFull.java
index 262b496243c65df870062d1df05e1c26a2d2cf65..acb58fe15fc7025f25bdf6882de2981b16706244 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/I2PServerSocketFull.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/I2PServerSocketFull.java
@@ -15,8 +15,9 @@ class I2PServerSocketFull implements I2PServerSocket {
     }
     
     /**
+     * Warning, unlike regular ServerSocket, may return null
      * 
-     * @return I2PSocket
+     * @return I2PSocket OR NULL
      * @throws net.i2p.I2PException
      * @throws SocketTimeoutException 
      */
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/I2PSocketManagerFull.java b/apps/streaming/java/src/net/i2p/client/streaming/I2PSocketManagerFull.java
index 314ff4e44805f77521982177ab0dd26146ccb8f3..03abafdda1163d6b9462696f1179a8c4355426a6 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/I2PSocketManagerFull.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/I2PSocketManagerFull.java
@@ -114,7 +114,7 @@ public class I2PSocketManagerFull implements I2PSocketManager {
 
     /**
      * 
-     * @return connected I2PSocket
+     * @return connected I2PSocket OR NULL
      * @throws net.i2p.I2PException
      * @throws java.net.SocketTimeoutException
      */
diff --git a/build.xml b/build.xml
index 7c75e857aeaa5d2e1dbe523463552a89bcafff2a..4e0af31268e5be45278eb0fd7106e8f0d313846e 100644
--- a/build.xml
+++ b/build.xml
@@ -6,10 +6,8 @@
     <!--
         <property name="javac.compilerargs" value="-warn:-unchecked,raw,unused,serial" />
     -->
-    <!-- Add Apache Harmony's Pack200 library if you don't have java.util.jar.Pack200
-         See core/java/src/net/i2p/util/FileUtil.java for code changes required
-         to use this library instead of Sun's version.
-         Or to comment it all out if you don't have either.
+    <!-- Additional classpath. No longer required; we find pack200 classes at runtime.
+         See core/java/src/net/i2p/util/FileUtil.java for more info.
     -->
     <!--
         <property name="javac.classpath" value="/PATH/TO/pack200.jar" />
@@ -239,7 +237,7 @@
             splitindex="true" 
             doctitle="I2P Javadocs for Release ${release.number} Build ${build.number}"
             windowtitle="I2P Anonymous Network - Java Documentation - Version ${release.number}">
-            <group title="Core SDK (i2p.jar)" packages="net.i2p:net.i2p.*:net.i2p.client:net.i2p.client.*:freenet.support.CPUInformation:org.bouncycastle.crypto:org.bouncycastle.crypto.*:gnu.crypto.*:gnu.gettext:org.xlattice.crypto.filters:com.nettgryppa.security" />
+            <group title="Core SDK (i2p.jar)" packages="net.i2p:net.i2p.*:net.i2p.client:net.i2p.client.*:net.i2p.internal:net.i2p.internal.*:freenet.support.CPUInformation:org.bouncycastle.crypto:org.bouncycastle.crypto.*:gnu.crypto.*:gnu.gettext:org.xlattice.crypto.filters:com.nettgryppa.security" />
             <group title="Streaming Library" packages="net.i2p.client.streaming" />
             <group title="Router" packages="net.i2p.router:net.i2p.router.*:net.i2p.data.i2np:org.cybergarage.*:org.freenetproject" />
             <group title="Router Console" packages="net.i2p.router.web" />
diff --git a/core/java/src/net/i2p/I2PAppContext.java b/core/java/src/net/i2p/I2PAppContext.java
index b497796408cb2b6051d9386c3ed7dd230bf82c31..e6df37e6f467ffb683daa48138fa4c3b866ec44e 100644
--- a/core/java/src/net/i2p/I2PAppContext.java
+++ b/core/java/src/net/i2p/I2PAppContext.java
@@ -3,6 +3,7 @@ package net.i2p;
 import java.io.File;
 import java.util.HashSet;
 import java.util.Properties;
+import java.util.Random;
 import java.util.Set;
 
 import net.i2p.client.naming.NamingService;
@@ -21,7 +22,9 @@ import net.i2p.crypto.KeyGenerator;
 import net.i2p.crypto.SHA256Generator;
 import net.i2p.crypto.SessionKeyManager;
 import net.i2p.crypto.TransientSessionKeyManager;
+import net.i2p.data.Base64;
 import net.i2p.data.RoutingKeyGenerator;
+import net.i2p.internal.InternalClientManager;
 import net.i2p.stat.StatManager;
 import net.i2p.util.Clock;
 import net.i2p.util.ConcurrentHashSet;
@@ -363,10 +366,12 @@ public class I2PAppContext {
             if (_tmpDir == null) {
                 String d = getProperty("i2p.dir.temp", System.getProperty("java.io.tmpdir"));
                 // our random() probably isn't warmed up yet
-                String f = "i2p-" + Math.abs((new java.util.Random()).nextInt()) + ".tmp";
+                byte[] rand = new byte[6];
+                (new Random()).nextBytes(rand);
+                String f = "i2p-" + Base64.encode(rand) + ".tmp";
                 _tmpDir = new SecureDirectory(d, f);
                 if (_tmpDir.exists()) {
-                    // good or bad ?
+                    // good or bad ? loop and try again?
                 } else if (_tmpDir.mkdir()) {
                     _tmpDir.deleteOnExit();
                 } else {
@@ -843,4 +848,13 @@ public class I2PAppContext {
     public boolean isRouterContext() {
         return false;
     }
+
+    /**
+     *  Use this to connect to the router in the same JVM.
+     *  @return always null in I2PAppContext, the client manager if in RouterContext
+     *  @since 0.8.3
+     */
+    public InternalClientManager internalClientManager() {
+        return null;
+    }
 }
diff --git a/core/java/src/net/i2p/client/ClientWriterRunner.java b/core/java/src/net/i2p/client/ClientWriterRunner.java
index 056208fb5a021470a0766c3205eda71cf8bc8ad6..f69148da3e212210782d41a65e1ec3577b9774bd 100644
--- a/core/java/src/net/i2p/client/ClientWriterRunner.java
+++ b/core/java/src/net/i2p/client/ClientWriterRunner.java
@@ -9,6 +9,7 @@ import java.util.concurrent.LinkedBlockingQueue;
 import net.i2p.data.i2cp.I2CPMessage;
 import net.i2p.data.i2cp.I2CPMessageImpl;
 import net.i2p.data.i2cp.I2CPMessageException;
+import net.i2p.internal.PoisonI2CPMessage;
 import net.i2p.util.I2PAppThread;
 
 /**
@@ -50,7 +51,7 @@ class ClientWriterRunner implements Runnable {
     public void stopWriting() {
         _messagesToWrite.clear();
         try {
-            _messagesToWrite.put(new PoisonMessage());
+            _messagesToWrite.put(new PoisonI2CPMessage());
         } catch (InterruptedException ie) {}
     }
 
@@ -62,7 +63,7 @@ class ClientWriterRunner implements Runnable {
             } catch (InterruptedException ie) {
                 continue;
             }
-            if (msg.getType() == PoisonMessage.MESSAGE_TYPE)
+            if (msg.getType() == PoisonI2CPMessage.MESSAGE_TYPE)
                 break;
             // only thread, we don't need synchronized
             try {
@@ -80,18 +81,4 @@ class ClientWriterRunner implements Runnable {
         }
         _messagesToWrite.clear();
     }
-
-    /**
-     * End-of-stream msg used to stop the concurrent queue
-     * See http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/BlockingQueue.html
-     *
-     */
-    private static class PoisonMessage extends I2CPMessageImpl {
-        public static final int MESSAGE_TYPE = 999999;
-        public int getType() {
-            return MESSAGE_TYPE;
-        }
-        public void doReadMessage(InputStream buf, int size) throws I2CPMessageException, IOException {}
-        public byte[] doWriteMessage() throws I2CPMessageException, IOException { return null; }
-    }
 }
diff --git a/core/java/src/net/i2p/client/I2CPSSLSocketFactory.java b/core/java/src/net/i2p/client/I2CPSSLSocketFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..d562388f13fff706ad9ca1c042addc0f12eb684f
--- /dev/null
+++ b/core/java/src/net/i2p/client/I2CPSSLSocketFactory.java
@@ -0,0 +1,183 @@
+package net.i2p.client;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Socket;
+import java.security.KeyStore;
+import java.security.GeneralSecurityException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+import net.i2p.I2PAppContext;
+import net.i2p.util.Log;
+
+/**
+ * Loads trusted ASCII certs from ~/.i2p/certificates/ and $CWD/certificates/.
+ * Keeps a single static SSLContext for the whole JVM.
+ *
+ * @author zzz
+ * @since 0.8.3
+ */
+class I2CPSSLSocketFactory {
+
+    private static final Object _initLock = new Object();
+    private static SSLSocketFactory _factory;
+    private static Log _log;
+
+    private static final String CERT_DIR = "certificates";
+
+    /**
+     * Initializes the static SSL Context if required, then returns a socket
+     * to the host.
+     *
+     * @param ctx just for logging
+     * @throws IOException on init error or usual socket errors
+     */
+    public static Socket createSocket(I2PAppContext ctx, String host, int port) throws IOException {
+        synchronized(_initLock) {
+            if (_factory == null) {
+                _log = ctx.logManager().getLog(I2CPSSLSocketFactory.class);
+                initSSLContext(ctx);
+                if (_factory == null)
+                    throw new IOException("Unable to create SSL Context for I2CP Client");
+                _log.info("I2CP Client-side SSL Context initialized");
+            }
+        }
+        return _factory.createSocket(host, port);
+    }
+
+    /**
+     *  Loads certs from
+     *  the ~/.i2p/certificates/ and $CWD/certificates/ directories.
+     */
+    private static void initSSLContext(I2PAppContext context) {
+        KeyStore ks;
+        try {
+            ks = KeyStore.getInstance(KeyStore.getDefaultType());
+            ks.load(null, "".toCharArray());
+        } catch (GeneralSecurityException gse) {
+            _log.error("Key Store init error", gse);
+            return;
+        } catch (IOException ioe) {
+            _log.error("Key Store init error", ioe);
+            return;
+        }
+
+        File dir = new File(context.getConfigDir(), CERT_DIR);
+        int adds = addCerts(dir, ks);
+        int totalAdds = adds;
+        if (adds > 0 && _log.shouldLog(Log.INFO))
+            _log.info("Loaded " + adds + " trusted certificates from " + dir.getAbsolutePath());
+
+        File dir2 = new File(System.getProperty("user.dir"), CERT_DIR);
+        if (!dir.getAbsolutePath().equals(dir2.getAbsolutePath())) {
+            adds = addCerts(dir2, ks);
+            totalAdds += adds;
+            if (adds > 0 && _log.shouldLog(Log.INFO))
+                _log.info("Loaded " + adds + " trusted certificates from " + dir.getAbsolutePath());
+        }
+        if (totalAdds > 0) {
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Loaded total of " + totalAdds + " new trusted certificates");
+        } else {
+            _log.error("No trusted certificates loaded (looked in " +
+                       dir.getAbsolutePath() + (dir.getAbsolutePath().equals(dir2.getAbsolutePath()) ? "" : (" and " + dir2.getAbsolutePath())) +
+                       ", I2CP SSL client connections will fail. " +
+                       "Copy the file certificates/i2cp.local.crt from the router to the directory.");
+            // don't continue, since we didn't load the system keystore, we have nothing.
+            return;
+        }
+
+        try {
+            SSLContext sslc = SSLContext.getInstance("TLS");
+            TrustManagerFactory tmf =   TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+            tmf.init(ks);
+            sslc.init(null, tmf.getTrustManagers(), context.random());
+            _factory = sslc.getSocketFactory();
+        } catch (GeneralSecurityException gse) {
+            _log.error("SSL context init error", gse);
+        }
+    }
+
+    /**
+     *  Load all X509 Certs from a directory and add them to the
+     *  trusted set of certificates in the key store
+     *
+     *  @return number successfully added
+     */
+    private static int addCerts(File dir, KeyStore ks) {
+        if (_log.shouldLog(Log.INFO))
+            _log.info("Looking for X509 Certificates in " + dir.getAbsolutePath());
+        int added = 0;
+        if (dir.exists() && dir.isDirectory()) {
+            File[] files = dir.listFiles();
+            if (files != null) {
+                for (int i = 0; i < files.length; i++) {
+                    File f = files[i];
+                    if (!f.isFile())
+                        continue;
+                    // use file name as alias
+                    String alias = f.getName().toLowerCase();
+                    boolean success = addCert(f, alias, ks);
+                    if (success)
+                        added++;
+                }
+            }
+        }
+        return added;
+    }
+
+    /**
+     *  Load an X509 Cert from a file and add it to the
+     *  trusted set of certificates in the key store
+     *
+     *  @return success
+     */
+    private static boolean addCert(File file, String alias, KeyStore ks) {
+        InputStream fis = null;
+        try {
+            fis = new FileInputStream(file);
+            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+            X509Certificate cert = (X509Certificate)cf.generateCertificate(fis);
+            if (_log.shouldLog(Log.INFO)) {
+                _log.info("Read X509 Certificate from " + file.getAbsolutePath() +
+                          " Issuer: " + cert.getIssuerX500Principal() +
+                          "; Valid From: " + cert.getNotBefore() +
+                          " To: " + cert.getNotAfter());
+            }
+            try {
+                cert.checkValidity();
+            } catch (CertificateExpiredException cee) {
+                _log.error("Rejecting expired X509 Certificate: " + file.getAbsolutePath(), cee);
+                return false;
+            } catch (CertificateNotYetValidException cnyve) {
+                _log.error("Rejecting X509 Certificate not yet valid: " + file.getAbsolutePath(), cnyve);
+                return false;
+            }
+            ks.setCertificateEntry(alias, cert);
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Now trusting X509 Certificate, Issuer: " + cert.getIssuerX500Principal());
+        } catch (GeneralSecurityException gse) {
+            _log.error("Error reading X509 Certificate: " + file.getAbsolutePath(), gse);
+            return false;
+        } catch (IOException ioe) {
+            _log.error("Error reading X509 Certificate: " + file.getAbsolutePath(), ioe);
+            return false;
+        } finally {
+            try { if (fis != null) fis.close(); } catch (IOException foo) {}
+        }
+        return true;
+    }
+}
diff --git a/core/java/src/net/i2p/client/I2PSessionImpl.java b/core/java/src/net/i2p/client/I2PSessionImpl.java
index ea75f73ef8ebb91d3a7db1f32296fd79bf2e539f..8090e0eaed88e65144a23f521edc75e650cd672b 100644
--- a/core/java/src/net/i2p/client/I2PSessionImpl.java
+++ b/core/java/src/net/i2p/client/I2PSessionImpl.java
@@ -39,8 +39,10 @@ import net.i2p.data.i2cp.I2CPMessageException;
 import net.i2p.data.i2cp.I2CPMessageReader;
 import net.i2p.data.i2cp.MessagePayloadMessage;
 import net.i2p.data.i2cp.SessionId;
-import net.i2p.util.I2PThread;
-import net.i2p.util.InternalSocket;
+import net.i2p.internal.I2CPMessageQueue;
+import net.i2p.internal.InternalClientManager;
+import net.i2p.internal.QueuedI2CPMessageReader;
+import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
 import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
@@ -66,9 +68,9 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
     /** currently granted lease set, or null */
     private LeaseSet _leaseSet;
 
-    /** hostname of router */
+    /** hostname of router - will be null if in RouterContext */
     protected String _hostname;
-    /** port num to router */
+    /** port num to router - will be 0 if in RouterContext */
     protected int _portNum;
     /** socket for comm */
     protected Socket _socket;
@@ -79,6 +81,13 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
     /** where we pipe our messages */
     protected /* FIXME final FIXME */OutputStream _out;
 
+    /**
+     *  Used for internal connections to the router.
+     *  If this is set, _socket, _writer, and _out will be null.
+     *  @since 0.8.3
+     */
+    protected I2CPMessageQueue _queue;
+
     /** who we send events to */
     protected I2PSessionListener _sessionListener;
 
@@ -122,6 +131,9 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
     private long _lastActivity;
     private boolean _isReduced;
 
+    /** SSL interface (only) @since 0.8.3 */
+    protected static final String PROP_ENABLE_SSL = "i2cp.SSL";
+
     void dateUpdated() {
         _dateReceived = true;
         synchronized (_dateReceivedLock) {
@@ -172,19 +184,24 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
     protected void loadConfig(Properties options) {
         _options = new Properties();
         _options.putAll(filter(options));
-        _hostname = _options.getProperty(I2PClient.PROP_TCP_HOST, "127.0.0.1");
-        String portNum = _options.getProperty(I2PClient.PROP_TCP_PORT, LISTEN_PORT + "");
-        try {
-            _portNum = Integer.parseInt(portNum);
-        } catch (NumberFormatException nfe) {
-            if (_log.shouldLog(Log.WARN))
-                _log.warn(getPrefix() + "Invalid port number specified, defaulting to "
-                          + LISTEN_PORT, nfe);
-            _portNum = LISTEN_PORT;
+        if (_context.isRouterContext()) {
+            // just for logging
+            _hostname = "[internal connection]";
+        } else {
+            _hostname = _options.getProperty(I2PClient.PROP_TCP_HOST, "127.0.0.1");
+            String portNum = _options.getProperty(I2PClient.PROP_TCP_PORT, LISTEN_PORT + "");
+            try {
+                _portNum = Integer.parseInt(portNum);
+            } catch (NumberFormatException nfe) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn(getPrefix() + "Invalid port number specified, defaulting to "
+                              + LISTEN_PORT, nfe);
+                _portNum = LISTEN_PORT;
+            }
         }
 
-        // auto-add auth if required, not set in the options, and we are in the same JVM
-        if (_context.isRouterContext() &&
+        // auto-add auth if required, not set in the options, and we are not in the same JVM
+        if ((!_context.isRouterContext()) &&
             Boolean.valueOf(_context.getProperty("i2cp.auth")).booleanValue() &&
             ((!options.containsKey("i2cp.username")) || (!options.containsKey("i2cp.password")))) {
             String configUser = _context.getProperty("i2cp.username");
@@ -272,10 +289,6 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
         setOpening(true);
         _closed = false;
         _availabilityNotifier.stopNotifying();
-        I2PThread notifier = new I2PThread(_availabilityNotifier);
-        notifier.setName("Notifier " + _myDestination.calculateHash().toBase64().substring(0,4));
-        notifier.setDaemon(true);
-        notifier.start();
         
         if ( (_options != null) && 
              (I2PClient.PROP_RELIABILITY_GUARANTEED.equals(_options.getProperty(I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_BEST_EFFORT))) ) {
@@ -285,17 +298,32 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
             
         long startConnect = _context.clock().now();
         try {
-            // If we are in the router JVM, connect using the interal pseudo-socket
-            _socket = InternalSocket.getSocket(_hostname, _portNum);
-            // _socket.setSoTimeout(1000000); // Uhmmm we could really-really use a real timeout, and handle it.
-            _out = _socket.getOutputStream();
-            synchronized (_out) {
-                _out.write(I2PClient.PROTOCOL_BYTE);
-                _out.flush();
+            // If we are in the router JVM, connect using the interal queue
+            if (_context.isRouterContext()) {
+                // _socket, _out, and _writer remain null
+                InternalClientManager mgr = _context.internalClientManager();
+                if (mgr == null)
+                    throw new I2PSessionException("Router is not ready for connections");
+                // the following may throw an I2PSessionException
+                _queue = mgr.connect();
+                _reader = new QueuedI2CPMessageReader(_queue, this);
+            } else {
+                if (Boolean.valueOf(_options.getProperty(PROP_ENABLE_SSL)).booleanValue())
+                    _socket = I2CPSSLSocketFactory.createSocket(_context, _hostname, _portNum);
+                else
+                    _socket = new Socket(_hostname, _portNum);
+                // _socket.setSoTimeout(1000000); // Uhmmm we could really-really use a real timeout, and handle it.
+                _out = _socket.getOutputStream();
+                synchronized (_out) {
+                    _out.write(I2PClient.PROTOCOL_BYTE);
+                    _out.flush();
+                }
+                _writer = new ClientWriterRunner(_out, this);
+                InputStream in = _socket.getInputStream();
+                _reader = new I2CPMessageReader(in, this);
             }
-            _writer = new ClientWriterRunner(_out, this);
-            InputStream in = _socket.getInputStream();
-            _reader = new I2CPMessageReader(in, this);
+            Thread notifier = new I2PAppThread(_availabilityNotifier, "ClientNotifier " + getPrefix(), true);
+            notifier.start();
             if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "before startReading");
             _reader.startReading();
             if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Before getDate");
@@ -435,6 +463,10 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
         }
     }
 
+    /**
+     *  This notifies the client of payload messages.
+     *  Needs work.
+     */
     protected class AvailabilityNotifier implements Runnable {
         private List _pendingIds;
         private List _pendingSizes;
@@ -497,8 +529,9 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
     }
     
     /**
-     * Recieve notification of some I2CP message and handle it if possible
-     *
+     * The I2CPMessageEventListener callback.
+     * Recieve notification of some I2CP message and handle it if possible.
+     * @param reader unused
      */
     public void messageReceived(I2CPMessageReader reader, I2CPMessage message) {
         I2CPMessageHandler handler = _handlerMap.getHandler(message.getType());
@@ -515,7 +548,9 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
     }
 
     /** 
-     * Recieve notifiation of an error reading the I2CP stream
+     * The I2CPMessageEventListener callback.
+     * Recieve notifiation of an error reading the I2CP stream.
+     * @param reader unused
      * @param error non-null
      */
     public void readError(I2CPMessageReader reader, Exception error) {
@@ -567,9 +602,14 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
      * @throws I2PSessionException if the message is malformed or there is an error writing it out
      */
     void sendMessage(I2CPMessage message) throws I2PSessionException {
-        if (isClosed() || _writer == null)
+        if (isClosed())
             throw new I2PSessionException("Already closed");
-        _writer.addMessage(message);
+        else if (_queue != null)
+            _queue.offer(message);  // internal
+        else if (_writer == null)
+            throw new I2PSessionException("Already closed");
+        else
+            _writer.addMessage(message);
     }
 
     /**
@@ -581,8 +621,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
         // Only log as WARN if the router went away
         int level;
         String msgpfx;
-        if ((error instanceof EOFException) ||
-            (error.getMessage() != null && error.getMessage().startsWith("Pipe closed"))) {
+        if (error instanceof EOFException) {
             level = Log.WARN;
             msgpfx = "Router closed connection: ";
         } else {
@@ -631,7 +670,9 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
                     _log.warn("Error destroying the session", ipe);
             }
         }
-        _availabilityNotifier.stopNotifying();
+        // SimpleSession does not initialize
+        if (_availabilityNotifier != null)
+            _availabilityNotifier.stopNotifying();
         _closed = true;
         _closing = false;
         closeSocket();
@@ -649,6 +690,10 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
             _reader.stopReading();
             _reader = null;
         }
+        if (_queue != null) {
+            // internal
+            _queue.close();
+        }
         if (_writer != null) {
             _writer.stopWriting();
             _writer = null;
@@ -666,7 +711,9 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
     }
 
     /**
-     * Recieve notification that the I2CP connection was disconnected
+     * The I2CPMessageEventListener callback.
+     * Recieve notification that the I2CP connection was disconnected.
+     * @param reader unused
      */
     public void disconnected(I2CPMessageReader reader) {
         if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Disconnected", new Exception("Disconnected"));
@@ -733,11 +780,8 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
             buf.append(s);
         else
             buf.append(getClass().getSimpleName());
-        buf.append(" #");
         if (_sessionId != null)
-            buf.append(_sessionId.getSessionId());
-        else
-            buf.append("n/a");
+            buf.append(" #").append(_sessionId.getSessionId());
         buf.append("]: ");
         return buf.toString();
     }
diff --git a/core/java/src/net/i2p/client/I2PSimpleSession.java b/core/java/src/net/i2p/client/I2PSimpleSession.java
index f4bc3e812ca6d42f8f4c58be9985ec049ffdf56b..ed9ec5cc369e1a492bc4e0c80b0cb1d6c60f0b2b 100644
--- a/core/java/src/net/i2p/client/I2PSimpleSession.java
+++ b/core/java/src/net/i2p/client/I2PSimpleSession.java
@@ -19,8 +19,10 @@ import net.i2p.data.i2cp.DestLookupMessage;
 import net.i2p.data.i2cp.DestReplyMessage;
 import net.i2p.data.i2cp.GetBandwidthLimitsMessage;
 import net.i2p.data.i2cp.I2CPMessageReader;
-import net.i2p.util.I2PThread;
-import net.i2p.util.InternalSocket;
+import net.i2p.internal.I2CPMessageQueue;
+import net.i2p.internal.InternalClientManager;
+import net.i2p.internal.QueuedI2CPMessageReader;
+import net.i2p.util.I2PAppThread;
 
 /**
  * Create a new session for doing naming and bandwidth queries only. Do not create a Destination.
@@ -44,12 +46,12 @@ class I2PSimpleSession extends I2PSessionImpl2 {
      * @throws I2PSessionException if there is a problem
      */
     public I2PSimpleSession(I2PAppContext context, Properties options) throws I2PSessionException {
+        // Warning, does not call super()
         _context = context;
         _log = context.logManager().getLog(I2PSimpleSession.class);
         _handlerMap = new SimpleMessageHandlerMap(context);
         _closed = true;
         _closing = false;
-        _availabilityNotifier = new AvailabilityNotifier();
         if (options == null)
             options = System.getProperties();
         loadConfig(options);
@@ -65,23 +67,32 @@ class I2PSimpleSession extends I2PSessionImpl2 {
     @Override
     public void connect() throws I2PSessionException {
         _closed = false;
-        _availabilityNotifier.stopNotifying();
-        I2PThread notifier = new I2PThread(_availabilityNotifier);
-        notifier.setName("Simple Notifier");
-        notifier.setDaemon(true);
-        notifier.start();
         
         try {
-            // If we are in the router JVM, connect using the interal pseudo-socket
-            _socket = InternalSocket.getSocket(_hostname, _portNum);
-            _out = _socket.getOutputStream();
-            synchronized (_out) {
-                _out.write(I2PClient.PROTOCOL_BYTE);
-                _out.flush();
+            // If we are in the router JVM, connect using the interal queue
+            if (_context.isRouterContext()) {
+                // _socket, _out, and _writer remain null
+                InternalClientManager mgr = _context.internalClientManager();
+                if (mgr == null)
+                    throw new I2PSessionException("Router is not ready for connections");
+                // the following may throw an I2PSessionException
+                _queue = mgr.connect();
+                _reader = new QueuedI2CPMessageReader(_queue, this);
+            } else {
+                if (Boolean.valueOf(getOptions().getProperty(PROP_ENABLE_SSL)).booleanValue())
+                    _socket = I2CPSSLSocketFactory.createSocket(_context, _hostname, _portNum);
+                else
+                    _socket = new Socket(_hostname, _portNum);
+                _out = _socket.getOutputStream();
+                synchronized (_out) {
+                    _out.write(I2PClient.PROTOCOL_BYTE);
+                    _out.flush();
+                }
+                _writer = new ClientWriterRunner(_out, this);
+                InputStream in = _socket.getInputStream();
+                _reader = new I2CPMessageReader(in, this);
             }
-            _writer = new ClientWriterRunner(_out, this);
-            InputStream in = _socket.getInputStream();
-            _reader = new I2CPMessageReader(in, this);
+            // we do not receive payload messages, so we do not need an AvailabilityNotifier
             _reader.startReading();
 
         } catch (UnknownHostException uhe) {
diff --git a/core/java/src/net/i2p/data/i2cp/I2CPMessageReader.java b/core/java/src/net/i2p/data/i2cp/I2CPMessageReader.java
index 057892b65fde0b8bf9976c7ade0d3044a0b36f0c..39633881e861456d1a22e232e1380d28b4400a4c 100644
--- a/core/java/src/net/i2p/data/i2cp/I2CPMessageReader.java
+++ b/core/java/src/net/i2p/data/i2cp/I2CPMessageReader.java
@@ -27,11 +27,11 @@ import net.i2p.util.Log;
 public class I2CPMessageReader {
     private final static Log _log = new Log(I2CPMessageReader.class);
     private InputStream _stream;
-    private I2CPMessageEventListener _listener;
-    private I2CPMessageReaderRunner _reader;
-    private Thread _readerThread;
+    protected I2CPMessageEventListener _listener;
+    protected I2CPMessageReaderRunner _reader;
+    protected Thread _readerThread;
     
-    private static volatile long __readerId = 0;
+    protected static volatile long __readerId = 0;
 
     public I2CPMessageReader(InputStream stream, I2CPMessageEventListener lsnr) {
         _stream = stream;
@@ -42,6 +42,14 @@ public class I2CPMessageReader {
         _readerThread.setName("I2CP Reader " + (++__readerId));
     }
 
+    /**
+     * For internal extension only. No stream.
+     * @since 0.8.3
+     */
+    protected I2CPMessageReader(I2CPMessageEventListener lsnr) {
+        setListener(lsnr);
+    }
+
     public void setListener(I2CPMessageEventListener lsnr) {
         _listener = lsnr;
     }
@@ -114,9 +122,9 @@ public class I2CPMessageReader {
         public void disconnected(I2CPMessageReader reader);
     }
 
-    private class I2CPMessageReaderRunner implements Runnable {
-        private volatile boolean _doRun;
-        private volatile boolean _stayAlive;
+    protected class I2CPMessageReaderRunner implements Runnable {
+        protected volatile boolean _doRun;
+        protected volatile boolean _stayAlive;
 
         public I2CPMessageReaderRunner() {
             _doRun = true;
@@ -175,7 +183,8 @@ public class I2CPMessageReader {
                         cancelRunner();
                     }
                 }
-                if (!_doRun) {
+                // ??? unused
+                if (_stayAlive && !_doRun) {
                     // pause .5 secs when we're paused
                     try {
                         Thread.sleep(500);
diff --git a/core/java/src/net/i2p/internal/I2CPMessageQueue.java b/core/java/src/net/i2p/internal/I2CPMessageQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..93bea3a3f29b55ece440d048f2fdd31e21975de8
--- /dev/null
+++ b/core/java/src/net/i2p/internal/I2CPMessageQueue.java
@@ -0,0 +1,51 @@
+package net.i2p.internal;
+
+import net.i2p.data.i2cp.I2CPMessage;
+
+/**
+ * Contains the methods to talk to a router or client via I2CP,
+ * when both are in the same JVM.
+ * This interface contains methods to access two queues,
+ * one for transmission and one for receiving.
+ * The methods are identical to those in java.util.concurrent.BlockingQueue.
+ *
+ * Reading may be done in a thread using the QueuedI2CPMessageReader class.
+ * Non-blocking writing may be done directly with offer().
+ *
+ * @author zzz
+ * @since 0.8.3
+ */
+public abstract class I2CPMessageQueue {
+
+    /**
+     *  Send a message, nonblocking.
+     *  @return success (false if no space available)
+     */
+    public abstract boolean offer(I2CPMessage msg);
+
+    /**
+     *  Receive a message, nonblocking.
+     *  Unused for now.
+     *  @return message or null if none available
+     */
+    public abstract I2CPMessage poll();
+
+    /**
+     *  Send a message, blocking until space is available.
+     *  Unused for now.
+     */
+    public abstract void put(I2CPMessage msg) throws InterruptedException;
+
+    /**
+     *  Receive a message, blocking until one is available.
+     *  @return message
+     */
+    public abstract I2CPMessage take() throws InterruptedException;
+
+    /**
+     *  == offer(new PoisonI2CPMessage());
+     */
+    public void close() {
+        offer(new PoisonI2CPMessage());
+    }
+}
diff --git a/core/java/src/net/i2p/internal/InternalClientManager.java b/core/java/src/net/i2p/internal/InternalClientManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..a923fb9f706ac0069356f2df2516d65ed11f69c0
--- /dev/null
+++ b/core/java/src/net/i2p/internal/InternalClientManager.java
@@ -0,0 +1,19 @@
+package net.i2p.internal;
+
+import net.i2p.client.I2PSessionException;
+import net.i2p.data.i2cp.I2CPMessage;
+
+/**
+ * A manager for the in-JVM I2CP message interface
+ *
+ * @author zzz
+ * @since 0.8.3
+ */
+public interface InternalClientManager {
+
+    /**
+     *  Connect to the router, receiving a message queue to talk to the router with.
+     *  @throws I2PSessionException if the router isn't ready
+     */
+    public I2CPMessageQueue connect() throws I2PSessionException;
+}
diff --git a/core/java/src/net/i2p/internal/PoisonI2CPMessage.java b/core/java/src/net/i2p/internal/PoisonI2CPMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..dda1301ee620d281da3f63b2101c7d350a558140
--- /dev/null
+++ b/core/java/src/net/i2p/internal/PoisonI2CPMessage.java
@@ -0,0 +1,56 @@
+package net.i2p.internal;
+
+import java.io.InputStream;
+
+import net.i2p.data.i2cp.I2CPMessageException;
+import net.i2p.data.i2cp.I2CPMessageImpl;
+
+/**
+ * For marking end-of-queues in a standard manner.
+ * Don't actually send it.
+ *
+ * @author zzz
+ * @since 0.8.3
+ */
+public class PoisonI2CPMessage extends I2CPMessageImpl {
+    public final static int MESSAGE_TYPE = 999999;
+
+    public PoisonI2CPMessage() {
+        super();
+    }
+
+    /**
+     *  @deprecated don't do this
+     *  @throws I2CPMessageException always
+     */
+    protected void doReadMessage(InputStream in, int size) throws I2CPMessageException {
+        throw new I2CPMessageException("Don't do this");
+    }
+
+    /**
+     *  @deprecated don't do this
+     *  @throws I2CPMessageException always
+     */
+    protected byte[] doWriteMessage() throws I2CPMessageException {
+        throw new I2CPMessageException("Don't do this");
+    }
+
+    public int getType() {
+        return MESSAGE_TYPE;
+    }
+
+    /* FIXME missing hashCode() method FIXME */
+    @Override
+    public boolean equals(Object object) {
+        if ((object != null) && (object instanceof PoisonI2CPMessage)) {
+            return true;
+        }
+        
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return "[PoisonMessage]";
+    }
+}
diff --git a/core/java/src/net/i2p/internal/QueuedI2CPMessageReader.java b/core/java/src/net/i2p/internal/QueuedI2CPMessageReader.java
new file mode 100644
index 0000000000000000000000000000000000000000..d713b678d4463a2ff617f54638a9e9500c8c421f
--- /dev/null
+++ b/core/java/src/net/i2p/internal/QueuedI2CPMessageReader.java
@@ -0,0 +1,62 @@
+package net.i2p.internal;
+
+import net.i2p.data.i2cp.I2CPMessage;
+import net.i2p.data.i2cp.I2CPMessageReader;
+import net.i2p.util.I2PThread;
+
+/**
+ * Get messages off an In-JVM queue, zero-copy
+ *
+ * @author zzz
+ * @since 0.8.3
+ */
+public class QueuedI2CPMessageReader extends I2CPMessageReader {
+    private final I2CPMessageQueue in;
+
+    public QueuedI2CPMessageReader(I2CPMessageQueue in, I2CPMessageEventListener lsnr) {
+        super(lsnr);
+        this.in = in;
+        _reader = new QueuedI2CPMessageReaderRunner();
+        _readerThread = new I2PThread(_reader, "I2CP Internal Reader " + (++__readerId), true);
+    }
+
+    protected class QueuedI2CPMessageReaderRunner extends I2CPMessageReaderRunner implements Runnable {
+
+        public QueuedI2CPMessageReaderRunner() {
+            super();
+        }
+
+        @Override
+        public void cancelRunner() {
+            super.cancelRunner();
+            _readerThread.interrupt();
+        }
+
+        @Override
+        public void run() {
+            while (_stayAlive) {
+                while (_doRun) {
+                    // do read
+                    I2CPMessage msg = null;
+                    try {
+                        msg = in.take();
+                        if (msg.getType() == PoisonI2CPMessage.MESSAGE_TYPE)
+                            cancelRunner();
+                        else
+                            _listener.messageReceived(QueuedI2CPMessageReader.this, msg);
+                    } catch (InterruptedException ie) {}
+                }
+                // ??? unused
+                if (_stayAlive && !_doRun) {
+                    // pause .5 secs when we're paused
+                    try {
+                        Thread.sleep(500);
+                    } catch (InterruptedException ie) {
+                        _listener.disconnected(QueuedI2CPMessageReader.this);
+                        cancelRunner();
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/core/java/src/net/i2p/internal/package.html b/core/java/src/net/i2p/internal/package.html
new file mode 100644
index 0000000000000000000000000000000000000000..edac509f01e1887342b677e561225ba20a3de00f
--- /dev/null
+++ b/core/java/src/net/i2p/internal/package.html
@@ -0,0 +1,7 @@
+<html><body>
+<p>
+Interface and classes for a router and client
+within the same JVM to directly pass I2CP messages using Queues
+instead of serialized messages over socket streams.
+</p>
+</body></html>
diff --git a/core/java/src/net/i2p/util/FileUtil.java b/core/java/src/net/i2p/util/FileUtil.java
index b56f197ab96c47142296125dc7b43196507e7d65..b5bb48a4353fef584bc6088f0408df34d97f5568 100644
--- a/core/java/src/net/i2p/util/FileUtil.java
+++ b/core/java/src/net/i2p/util/FileUtil.java
@@ -9,6 +9,8 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Enumeration;
 import java.util.List;
@@ -16,13 +18,11 @@ import java.util.jar.JarOutputStream;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 
-// Pack200 import
-// you must also uncomment the correct line in unpack() below
-// For gcj, gij, etc., comment both out
+// Pack200 now loaded dynamically in unpack() below
 //
 // For Sun, OpenJDK, IcedTea, etc, use this
-import java.util.jar.Pack200;
-
+//import java.util.jar.Pack200;
+//
 // For Apache Harmony or if you put its pack200.jar in your library directory use this
 //import org.apache.harmony.unpack200.Archive;
 
@@ -231,37 +231,79 @@ public class FileUtil {
     }
     
     /**
-     * This won't work right if one of the two options in unpack() is commented out.
+     * Public since 0.8.3
      * @since 0.8.1
      */
-    private static boolean isPack200Supported() {
+    public static boolean isPack200Supported() {
         try {
             Class.forName("java.util.jar.Pack200", false, ClassLoader.getSystemClassLoader());
             return true;
         } catch (Exception e) {}
         try {
-            Class.forName("org.apache.harmony.pack200.Archive", false, ClassLoader.getSystemClassLoader());
+            Class.forName("org.apache.harmony.unpack200.Archive", false, ClassLoader.getSystemClassLoader());
             return true;
         } catch (Exception e) {}
         return false;
     }
 
+    private static boolean _failedOracle;
+    private static boolean _failedApache;
+
     /**
+     * Unpack using either Oracle or Apache's unpack200 library,
+     * with the classes discovered at runtime so neither is required at compile time.
+     *
      * Caller must close streams
+     * @throws IOException on unpack error or if neither library is available.
+     *         Will not throw ClassNotFoundException.
+     * @throws org.apache.harmony.pack200.Pack200Exception which is not an IOException
      * @since 0.8.1
      */
     private static void unpack(InputStream in, JarOutputStream out) throws Exception {
         // For Sun, OpenJDK, IcedTea, etc, use this
-        Pack200.newUnpacker().unpack(in, out);
+        //Pack200.newUnpacker().unpack(in, out);
+        if (!_failedOracle) {
+            try {
+                Class p200 = Class.forName("java.util.jar.Pack200", true, ClassLoader.getSystemClassLoader());
+                Method newUnpacker = p200.getMethod("newUnpacker", (Class[]) null);
+                Object unpacker = newUnpacker.invoke(null,(Object[])  null);
+                Method unpack = unpacker.getClass().getMethod("unpack", new Class[] {InputStream.class, JarOutputStream.class});
+                // throws IOException
+                unpack.invoke(unpacker, new Object[] {in, out});
+                return;
+            } catch (ClassNotFoundException e) {
+                _failedOracle = true;
+                //e.printStackTrace();
+            } catch (NoSuchMethodException e) {
+                _failedOracle = true;
+                //e.printStackTrace();
+            }
+        }
 
         // ------------------
         // For Apache Harmony or if you put its pack200.jar in your library directory use this
         //(new Archive(in, out)).unpack();
-
+        if (!_failedApache) {
+            try {
+                Class p200 = Class.forName("org.apache.harmony.unpack200.Archive", true, ClassLoader.getSystemClassLoader());
+                Constructor newUnpacker = p200.getConstructor(new Class[] {InputStream.class, JarOutputStream.class});
+                Object unpacker = newUnpacker.newInstance(new Object[] {in, out});
+                Method unpack = unpacker.getClass().getMethod("unpack", (Class[]) null);
+                // throws IOException or Pack200Exception
+                unpack.invoke(unpacker, (Object[]) null);
+                return;
+            } catch (ClassNotFoundException e) {
+                _failedApache = true;
+                //e.printStackTrace();
+            } catch (NoSuchMethodException e) {
+                _failedApache = true;
+                //e.printStackTrace();
+            }
+        }
 
         // ------------------
         // For gcj, gij, etc., use this
-        //throw new IOException("Pack200 not supported");
+        throw new IOException("Unpack200 not supported");
     }
 
     /**
@@ -378,12 +420,13 @@ public class FileUtil {
     }
     
     /**
-     * Usage: FileUtil (delete path | copy source dest)
+     * Usage: FileUtil (delete path | copy source dest | unzip path.zip)
      *
      */
     public static void main(String args[]) {
         if ( (args == null) || (args.length < 2) ) {
-            testRmdir();
+            System.err.println("Usage: delete path | copy source dest | unzip path.zip");
+            //testRmdir();
         } else if ("delete".equals(args[0])) {
             boolean deleted = FileUtil.rmdir(args[1], false);
             if (!deleted)
@@ -407,6 +450,7 @@ public class FileUtil {
         }
     }
     
+  /*****
     private static void testRmdir() {
         File t = new File("rmdirTest/test/subdir/blah");
         boolean created = t.mkdirs();
@@ -417,4 +461,5 @@ public class FileUtil {
         else
             System.out.println("PASS: rmdirTest deleted");
     }
+   *****/
 }
diff --git a/core/java/src/net/i2p/util/I2PThread.java b/core/java/src/net/i2p/util/I2PThread.java
index c21c66f6bebbacd73094b8cdc45118185d261852..9b76b8fc9c92c9261c1e2338e3c5a05931f4e9fa 100644
--- a/core/java/src/net/i2p/util/I2PThread.java
+++ b/core/java/src/net/i2p/util/I2PThread.java
@@ -61,8 +61,8 @@ public class I2PThread extends Thread {
             _createdBy = new Exception("Created by");
     }
 
-    private void log(int level, String msg) { log(level, msg, null); }
-    private void log(int level, String msg, Throwable t) {
+    private static void log(int level, String msg) { log(level, msg, null); }
+    private static void log(int level, String msg, Throwable t) {
         // we cant assume log is created
         if (_log == null) _log = new Log(I2PThread.class);
         if (_log.shouldLog(level))
@@ -72,12 +72,12 @@ public class I2PThread extends Thread {
     @Override
     public void run() {
         _name = Thread.currentThread().getName();
-        log(Log.DEBUG, "New thread started: " + _name, _createdBy);
+        log(Log.INFO, "New thread started" + (isDaemon() ? " (daemon): " : ": ") + _name, _createdBy);
         try {
             super.run();
         } catch (Throwable t) {
             try {
-                log(Log.CRIT, "Killing thread " + getName(), t);
+                log(Log.CRIT, "Thread terminated unexpectedly: " + getName(), t);
             } catch (Throwable woof) {
                 System.err.println("Died within the OOM itself");
                 t.printStackTrace();
@@ -85,12 +85,12 @@ public class I2PThread extends Thread {
             if (t instanceof OutOfMemoryError)
                 fireOOM((OutOfMemoryError)t);
         }
-        log(Log.DEBUG, "Thread finished gracefully: " + _name);
+        log(Log.INFO, "Thread finished normally: " + _name);
     }
     
     @Override
     protected void finalize() throws Throwable {
-        log(Log.DEBUG, "Thread finalized: " + _name);
+        //log(Log.DEBUG, "Thread finalized: " + _name);
         super.finalize();
     }
     
diff --git a/core/java/src/net/i2p/util/ShellCommand.java b/core/java/src/net/i2p/util/ShellCommand.java
index d6fce002192ce4cbac1e67973ebe693971202b27..12b668f67ee7b9345b4d3c301e3969b182c7f96c 100644
--- a/core/java/src/net/i2p/util/ShellCommand.java
+++ b/core/java/src/net/i2p/util/ShellCommand.java
@@ -51,17 +51,18 @@ public class ShellCommand {
      */
     private class CommandThread extends Thread {
 
-        final Object  caller;
-        boolean consumeOutput;
-        String  shellCommand;
-
-        CommandThread(Object caller, String shellCommand, boolean consumeOutput) {
+        private final Object  caller;
+        private final boolean consumeOutput;
+        private final Object shellCommand;
+
+        /**
+         *  @param shellCommand either a String or a String[] (since 0.8.3)
+         */
+        CommandThread(Object caller, Object shellCommand, boolean consumeOutput) {
             super("CommandThread");
             this.caller = caller;
             this.shellCommand = shellCommand;
             this.consumeOutput = consumeOutput;
-            _commandSuccessful = false;
-            _commandCompleted = false;
         }
 
         @Override
@@ -200,6 +201,9 @@ public class ShellCommand {
      * {@link #getErrorStream()}, respectively. Input can be passed to the
      * <code>STDIN</code> of the shell process via {@link #getInputStream()}.
      * 
+     * Warning, no good way to quote or escape spaces in arguments with this method.
+     * @deprecated unused
+     * 
      * @param shellCommand The command for the shell to execute.
      */
     public void execute(String shellCommand) {
@@ -215,6 +219,9 @@ public class ShellCommand {
      * Input can be passed to the <code>STDIN</code> of the shell process via
      * {@link #getInputStream()}.
      * 
+     * Warning, no good way to quote or escape spaces in arguments with this method.
+     * @deprecated unused
+     * 
      * @param  shellCommand The command for the shell to execute.
      * @return              <code>true</code> if the spawned shell process
      *                      returns an exit status of 0 (indicating success),
@@ -237,6 +244,9 @@ public class ShellCommand {
      * {@link #getErrorStream()}, respectively. Input can be passed to the
      * <code>STDIN</code> of the shell process via {@link #getInputStream()}.
      * 
+     * Warning, no good way to quote or escape spaces in arguments with this method.
+     * @deprecated unused
+     * 
      * @param  shellCommand The command for the shell to execute.
      * @param  seconds      The method will return <code>true</code> if this
      *                      number of seconds elapses without the process
@@ -276,6 +286,9 @@ public class ShellCommand {
      * without waiting for an exit status. Any output produced by the executed
      * command will not be displayed.
      * 
+     * Warning, no good way to quote or escape spaces in arguments with this method.
+     * @deprecated unused
+     * 
      * @param  shellCommand The command for the shell to execute.
      * @throws IOException
      */
@@ -288,6 +301,8 @@ public class ShellCommand {
      * all of the command's resulting shell processes have completed. Any output
      * produced by the executed command will not be displayed.
      * 
+     * Warning, no good way to quote or escape spaces in arguments with this method.
+     * 
      * @param  shellCommand The command for the shell to execute.
      * @return              <code>true</code> if the spawned shell process
      *                      returns an exit status of 0 (indicating success),
@@ -307,7 +322,12 @@ public class ShellCommand {
      * specified number of seconds has elapsed first. Any output produced by the
      * executed command will not be displayed.
      * 
-     * @param  shellCommand The command for the shell to execute.
+     * Warning, no good way to quote or escape spaces in arguments when shellCommand is a String.
+     * Use a String array for best results, especially on Windows.
+     * 
+     * @param  shellCommand The command for the shell to execute, as a String.
+     *                      You can't quote arguments successfully.
+     *                      See Runtime.exec(String) for more info.
      * @param  seconds      The method will return <code>true</code> if this
      *                      number of seconds elapses without the process
      *                      returning an exit status. A value of <code>0</code>
@@ -317,7 +337,33 @@ public class ShellCommand {
      *                      else <code>false</code>.
      */
     public synchronized boolean executeSilentAndWaitTimed(String shellCommand, int seconds) {
+        return executeSAWT(shellCommand, seconds);
+    }
 
+    /**
+     * Passes a command to the shell for execution. This method blocks until
+     * all of the command's resulting shell processes have completed unless a
+     * specified number of seconds has elapsed first. Any output produced by the
+     * executed command will not be displayed.
+     * 
+     * @param  commandArray The command for the shell to execute,
+     *                      as a String[].
+     *                      See Runtime.exec(String[]) for more info.
+     * @param  seconds      The method will return <code>true</code> if this
+     *                      number of seconds elapses without the process
+     *                      returning an exit status. A value of <code>0</code>
+     *                      here disables waiting.
+     * @return              <code>true</code> if the spawned shell process
+     *                      returns an exit status of 0 (indicating success),
+     *                      else <code>false</code>.
+     * @since 0.8.3
+     */
+    public synchronized boolean executeSilentAndWaitTimed(String[] commandArray, int seconds) {
+        return executeSAWT(commandArray, seconds);
+    }
+
+    /** @since 0.8.3 */
+    private boolean executeSAWT(Object shellCommand, int seconds) {
         _commandThread = new CommandThread(this, shellCommand, CONSUME_OUTPUT);
         _commandThread.start();
         try {
@@ -364,7 +410,10 @@ public class ShellCommand {
         return;
     }
     
-    private boolean execute(String shellCommand, boolean consumeOutput, boolean waitForExitStatus) {
+    /**
+     *  @param shellCommand either a String or a String[] (since 0.8.3) - quick hack
+     */
+    private boolean execute(Object shellCommand, boolean consumeOutput, boolean waitForExitStatus) {
 
         StreamConsumer processStderrConsumer;
         StreamConsumer processStdoutConsumer;
@@ -374,7 +423,13 @@ public class ShellCommand {
         StreamReader   processStdoutReader;
 
         try {
-            _process = Runtime.getRuntime().exec(shellCommand, null);
+            // easy way so we don't have to copy this whole method
+            if (shellCommand instanceof String)
+                _process = Runtime.getRuntime().exec((String)shellCommand);
+            else if (shellCommand instanceof String[])
+                _process = Runtime.getRuntime().exec((String[])shellCommand);
+            else
+               throw new ClassCastException("shell command must be a String or a String[]");
             if (consumeOutput) {
                 processStderrConsumer = new StreamConsumer(_process.getErrorStream());
                 processStderrConsumer.start();
diff --git a/core/java/src/net/i2p/util/SimpleScheduler.java b/core/java/src/net/i2p/util/SimpleScheduler.java
index ee7d36e99a609d33555ef874adb5f0b1a6f4b1af..f764debe99f6cad3d730c64bae342b9679011d13 100644
--- a/core/java/src/net/i2p/util/SimpleScheduler.java
+++ b/core/java/src/net/i2p/util/SimpleScheduler.java
@@ -28,12 +28,14 @@ import net.i2p.I2PAppContext;
 public class SimpleScheduler {
     private static final SimpleScheduler _instance = new SimpleScheduler();
     public static SimpleScheduler getInstance() { return _instance; }
-    private static final int THREADS = 4;
+    private static final int MIN_THREADS = 2;
+    private static final int MAX_THREADS = 4;
     private I2PAppContext _context;
     private Log _log;
     private ScheduledThreadPoolExecutor _executor;
     private String _name;
     private int _count;
+    private final int _threads;
 
     protected SimpleScheduler() { this("SimpleScheduler"); }
     protected SimpleScheduler(String name) {
@@ -41,7 +43,9 @@ public class SimpleScheduler {
         _log = _context.logManager().getLog(SimpleScheduler.class);
         _name = name;
         _count = 0;
-        _executor = new ScheduledThreadPoolExecutor(THREADS, new CustomThreadFactory());
+        long maxMemory = Runtime.getRuntime().maxMemory();
+        _threads = (int) Math.max(MIN_THREADS, Math.min(MAX_THREADS, 1 + (maxMemory / (32*1024*1024))));
+        _executor = new ScheduledThreadPoolExecutor(_threads, new CustomThreadFactory());
         _executor.prestartAllCoreThreads();
     }
     
@@ -65,6 +69,13 @@ public class SimpleScheduler {
         re.schedule();
     }
     
+    /**
+     * Queue up the given event to be fired after timeoutMs and every
+     * timeoutMs thereafter. The TimedEvent must not do its own rescheduling.
+     * As all Exceptions are caught in run(), these will not prevent
+     * subsequent executions (unlike SimpleTimer, where the TimedEvent does
+     * its own rescheduling).
+     */
     public void addPeriodicEvent(SimpleTimer.TimedEvent event, long timeoutMs) {
         addPeriodicEvent(event, timeoutMs, timeoutMs);
     }
@@ -90,7 +101,7 @@ public class SimpleScheduler {
     private class CustomThreadFactory implements ThreadFactory {
         public Thread newThread(Runnable r) {
             Thread rv = Executors.defaultThreadFactory().newThread(r);
-            rv.setName(_name +  ' ' + (++_count) + '/' + THREADS);
+            rv.setName(_name +  ' ' + (++_count) + '/' + _threads);
 // Uncomment this to test threadgrouping, but we should be all safe now that the constructor preallocates!
 //            String name = rv.getThreadGroup().getName();
 //            if(!name.equals("main")) {
diff --git a/core/java/src/net/i2p/util/SimpleTimer.java b/core/java/src/net/i2p/util/SimpleTimer.java
index 6a8b855e305f3cfc892c78c4306de871c1346e73..0b543071199669c8ea3e802ead0b4aae82c7b254 100644
--- a/core/java/src/net/i2p/util/SimpleTimer.java
+++ b/core/java/src/net/i2p/util/SimpleTimer.java
@@ -18,14 +18,16 @@ import net.i2p.I2PAppContext;
 public class SimpleTimer {
     private static final SimpleTimer _instance = new SimpleTimer();
     public static SimpleTimer getInstance() { return _instance; }
-    private I2PAppContext _context;
-    private Log _log;
+    private final I2PAppContext _context;
+    private final Log _log;
     /** event time (Long) to event (TimedEvent) mapping */
     private final TreeMap _events;
     /** event (TimedEvent) to event time (Long) mapping */
     private Map _eventTimes;
     private final List _readyEvents;
     private SimpleStore runn;
+    private static final int MIN_THREADS = 2;
+    private static final int MAX_THREADS = 4;
 
     protected SimpleTimer() { this("SimpleTimer"); }
     protected SimpleTimer(String name) {
@@ -39,9 +41,11 @@ public class SimpleTimer {
         runner.setName(name);
         runner.setDaemon(true);
         runner.start();
-        for (int i = 0; i < 3; i++) {
+        long maxMemory = Runtime.getRuntime().maxMemory();
+        int threads = (int) Math.max(MIN_THREADS, Math.min(MAX_THREADS, 1 + (maxMemory / (32*1024*1024))));
+        for (int i = 1; i <= threads ; i++) {
             I2PThread executor = new I2PThread(new Executor(_context, _log, _readyEvents, runn));
-            executor.setName(name + "Executor " + i);
+            executor.setName(name + "Executor " + i + '/' + threads);
             executor.setDaemon(true);
             executor.start();
         }
diff --git a/core/java/src/net/i2p/util/SimpleTimer2.java b/core/java/src/net/i2p/util/SimpleTimer2.java
index b2af33cf2be15bd04606b720e0adce550bf426bf..bda41e6211b5a58c8ebd483d4118094eaa9e4e60 100644
--- a/core/java/src/net/i2p/util/SimpleTimer2.java
+++ b/core/java/src/net/i2p/util/SimpleTimer2.java
@@ -27,12 +27,14 @@ import net.i2p.I2PAppContext;
 public class SimpleTimer2 {
     private static final SimpleTimer2 _instance = new SimpleTimer2();
     public static SimpleTimer2 getInstance() { return _instance; }
-    private static final int THREADS = 4;
+    private static final int MIN_THREADS = 2;
+    private static final int MAX_THREADS = 4;
     private I2PAppContext _context;
     private static Log _log; // static so TimedEvent can use it
     private ScheduledThreadPoolExecutor _executor;
     private String _name;
     private int _count;
+    private final int _threads;
 
     protected SimpleTimer2() { this("SimpleTimer2"); }
     protected SimpleTimer2(String name) {
@@ -40,7 +42,9 @@ public class SimpleTimer2 {
         _log = _context.logManager().getLog(SimpleTimer2.class);
         _name = name;
         _count = 0;
-        _executor = new CustomScheduledThreadPoolExecutor(THREADS, new CustomThreadFactory());
+        long maxMemory = Runtime.getRuntime().maxMemory();
+        _threads = (int) Math.max(MIN_THREADS, Math.min(MAX_THREADS, 1 + (maxMemory / (32*1024*1024))));
+        _executor = new CustomScheduledThreadPoolExecutor(_threads, new CustomThreadFactory());
         _executor.prestartAllCoreThreads();
     }
     
@@ -67,7 +71,7 @@ public class SimpleTimer2 {
     private class CustomThreadFactory implements ThreadFactory {
         public Thread newThread(Runnable r) {
             Thread rv = Executors.defaultThreadFactory().newThread(r);
-            rv.setName(_name + ' ' + (++_count) + '/' + THREADS);
+            rv.setName(_name + ' ' + (++_count) + '/' + _threads);
 // Uncomment this to test threadgrouping, but we should be all safe now that the constructor preallocates!
 //            String name = rv.getThreadGroup().getName();
 //            if(!name.equals("main")) {
diff --git a/installer/resources/clients.config b/installer/resources/clients.config
index f82aec52687e5d872b266deecefba6e48c7d335e..08c6c62ba88e0e01d1d90b64e04f48f29e59ed30 100644
--- a/installer/resources/clients.config
+++ b/installer/resources/clients.config
@@ -6,6 +6,21 @@
 #
 
 # fire up the web console
+## There are several choices, here are some examples:
+## non-SSL, bind to local IPv4 only
+#clientApp.0.args=7657 127.0.0.1 ./webapps/
+## non-SSL, bind to local IPv6 only
+#clientApp.0.args=7657 ::1 ./webapps/
+## non-SSL, bind to all IPv4 addresses
+#clientApp.0.args=7657 0.0.0.0 ./webapps/
+## non-SSL, bind to all IPv6 addresses
+#clientApp.0.args=7657 :: ./webapps/
+## For SSL only, change clientApp.4.args below to https://
+## SSL only
+#clientApp.0.args=-s 7657 ::1,127.0.0.1 ./webapps/
+## non-SSL and SSL
+#clientApp.0.args=7657 ::1,127.0.0.1 -s 7667 ::1,127.0.0.1 ./webapps/
+## non-SSL only, both IPv6 and IPv4 local interfaces
 clientApp.0.args=7657 ::1,127.0.0.1 ./webapps/
 clientApp.0.main=net.i2p.router.web.RouterConsoleRunner
 clientApp.0.name=I2P Router Console
diff --git a/installer/resources/jetty.xml b/installer/resources/jetty.xml
index d82cf5580f0a5c7cfee11978d46cb99eb57d6c23..29900cb6b07975af1d9488ce9a82721971bc9c84 100644
--- a/installer/resources/jetty.xml
+++ b/installer/resources/jetty.xml
@@ -71,17 +71,29 @@
 
 
   <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
-  <!-- Add a HTTPS SSL listener on port 8443                           -->
+  <!-- Add a HTTPS SSL listener on port 8443                               -->
+  <!--                                                                     -->
+  <!-- In the unlikely event you would want SSL support for your eepsite.  -->
+  <!-- You would need to generate a selfsigned certificate in a keystore   -->
+  <!-- in ~/.i2p/eepsite/keystore.ks, for example with the command line:   -->
+  <!--
+       keytool -genkey -storetype JKS -keystore ~/.i2p/eepsite/keystore.ks -storepass changeit -alias console -dname CN=xyz123.eepsite.i2p.net,OU=Eepsite,O=I2P Anonymous Network,L=XX,ST=XX,C=XX -validity 3650 -keyalg DSA -keysize 1024 -keypass myKeyPassword 
+   -->
+  <!-- Change the CN and key password in the example, of course.           -->
+  <!-- You wouldn't want to open this up to the regular internet,          -->
+  <!-- would you?? Untested and not recommended.                           -->
   <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
   <!-- UNCOMMENT TO ACTIVATE
   <Call name="addListener">
     <Arg>
-      <New class="org.mortbay.http.SunJsseListener">
+      <New class="org.mortbay.http.SslListener">
         <Set name="Port">8443</Set>
         <Set name="PoolName">main</Set>
-        <Set name="Keystore"><SystemProperty name="jetty.home" default="."/>/etc/demokeystore</Set>
-	<Set name="Password">OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4</Set>
-	<Set name="KeyPassword">OBF:1u2u1wml1z7s1z7a1wnl1u2g</Set>
+        <Set name="Keystore">./eepsite/keystore.ks</Set>
+        <!-- the keystore password -->
+	<Set name="Password">changeit</Set>
+        <!-- the X.509 certificate password -->
+	<Set name="KeyPassword">myKeyPassword</Set>
         <Set name="NonPersistentUserAgent">MSIE 5</Set>
       </New>
     </Arg>
diff --git a/router/java/src/net/i2p/data/i2np/I2NPMessageReader.java b/router/java/src/net/i2p/data/i2np/I2NPMessageReader.java
index fd3e7063bd290ded9ab4cfcaac8d752fb6138f09..74b93b34f4f4d31862a659b6f781f2d294d23a87 100644
--- a/router/java/src/net/i2p/data/i2np/I2NPMessageReader.java
+++ b/router/java/src/net/i2p/data/i2np/I2NPMessageReader.java
@@ -161,7 +161,8 @@ public class I2NPMessageReader {
                         cancelRunner();
                     }
                 }
-                if (!_doRun) {
+                // ??? unused
+                if (_stayAlive && !_doRun) {
                     // pause .5 secs when we're paused
                     try { Thread.sleep(500); } catch (InterruptedException ie) {}
                 }
diff --git a/router/java/src/net/i2p/router/JobQueue.java b/router/java/src/net/i2p/router/JobQueue.java
index a8b5395b00c6c5cb114ad2b4776aed8ac02fe0fb..42ddadf5ab7e8704d1d9ff041b613311508a4d01 100644
--- a/router/java/src/net/i2p/router/JobQueue.java
+++ b/router/java/src/net/i2p/router/JobQueue.java
@@ -395,10 +395,8 @@ public class JobQueue {
                 for (int i = _queueRunners.size(); i < numThreads; i++) {
                     JobQueueRunner runner = new JobQueueRunner(_context, i);
                     _queueRunners.put(Integer.valueOf(i), runner);
-                    Thread t = new I2PThread(runner);
-                    t.setName("JobQueue"+(_runnerId++));
+                    Thread t = new I2PThread(runner, "JobQueue " + (++_runnerId) + '/' + numThreads, false);
                     //t.setPriority(I2PThread.MAX_PRIORITY-1);
-                    t.setDaemon(false);
                     t.start();
                 }
             } else if (_queueRunners.size() == numThreads) {
diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java
index 7f8b2788ea70983f9fcee6ce57226034e13b4c6f..5904d04ea0b6bc0412086b16ff3e98d1a7844696 100644
--- a/router/java/src/net/i2p/router/Router.java
+++ b/router/java/src/net/i2p/router/Router.java
@@ -1281,11 +1281,7 @@ public class Router {
      */
     private void beginMarkingLiveliness() {
         File f = getPingFile();
-        // not an I2PThread for context creation issues
-        Thread t = new Thread(new MarkLiveliness(_context, this, f));
-        t.setName("Mark router liveliness");
-        t.setDaemon(true);
-        t.start();
+        SimpleScheduler.getInstance().addPeriodicEvent(new MarkLiveliness(this, f), 0, LIVELINESS_DELAY);
     }
     
     public static final String PROP_BANDWIDTH_SHARE_PERCENTAGE = "router.sharePercentage";
@@ -1523,22 +1519,24 @@ private static class UpdateRoutingKeyModifierJob extends JobImpl {
     }
 }
 
-private static class MarkLiveliness implements Runnable {
-    private RouterContext _context;
+/**
+ *  Write a timestamp to the ping file where the wrapper can see it
+ */
+private static class MarkLiveliness implements SimpleTimer.TimedEvent {
     private Router _router;
     private File _pingFile;
-    public MarkLiveliness(RouterContext ctx, Router router, File pingFile) {
-        _context = ctx;
+
+    public MarkLiveliness(Router router, File pingFile) {
         _router = router;
         _pingFile = pingFile;
-    }
-    public void run() {
         _pingFile.deleteOnExit();
-        do {
+    }
+
+    public void timeReached() {
+        if (_router.isAlive())
             ping();
-            try { Thread.sleep(Router.LIVELINESS_DELAY); } catch (InterruptedException ie) {}
-        } while (_router.isAlive());
-        _pingFile.delete();
+        else
+            _pingFile.delete();
     }
 
     private void ping() {
diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java
index 3d5ed609edb65cb900a9ebc68bad1cf9af8b93ce..cb3c6366251378c0cf50972d197c446bc6d960f3 100644
--- a/router/java/src/net/i2p/router/RouterContext.java
+++ b/router/java/src/net/i2p/router/RouterContext.java
@@ -6,6 +6,7 @@ import java.util.Properties;
 
 import net.i2p.I2PAppContext;
 import net.i2p.data.Hash;
+import net.i2p.internal.InternalClientManager;
 import net.i2p.router.client.ClientManagerFacadeImpl;
 import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade;
 import net.i2p.router.peermanager.Calculator;
@@ -34,7 +35,7 @@ import net.i2p.util.KeyRing;
  */
 public class RouterContext extends I2PAppContext {
     private Router _router;
-    private ClientManagerFacade _clientManagerFacade;
+    private ClientManagerFacadeImpl _clientManagerFacade;
     private ClientMessagePool _clientMessagePool;
     private JobQueue _jobQueue;
     private InNetMessagePool _inNetMessagePool;
@@ -106,10 +107,12 @@ public class RouterContext extends I2PAppContext {
     }
 
     public void initAll() {
-        if ("false".equals(getProperty("i2p.dummyClientFacade", "false")))
-            _clientManagerFacade = new ClientManagerFacadeImpl(this);
-        else
-            _clientManagerFacade = new DummyClientManagerFacade(this);
+        if (getBooleanProperty("i2p.dummyClientFacade"))
+            System.err.println("i2p.dummpClientFacade currently unsupported");
+        _clientManagerFacade = new ClientManagerFacadeImpl(this);
+        // removed since it doesn't implement InternalClientManager for now
+        //else
+        //    _clientManagerFacade = new DummyClientManagerFacade(this);
         _clientMessagePool = new ClientMessagePool(this);
         _jobQueue = new JobQueue(this);
         _inNetMessagePool = new InNetMessagePool(this);
@@ -395,4 +398,13 @@ public class RouterContext extends I2PAppContext {
     public boolean isRouterContext() {
         return true;
     }
+
+    /**
+     *  Use this to connect to the router in the same JVM.
+     *  @return the client manager
+     *  @since 0.8.3
+     */
+    public InternalClientManager internalClientManager() {
+        return _clientManagerFacade;
+    }
 }
diff --git a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
index b3468e4e0be51c575d0998569cdea4de1fa83165..8bef2776d5a5a3687da21a6fe72eb35049adcddb 100644
--- a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
+++ b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
@@ -50,9 +50,9 @@ import net.i2p.util.SimpleTimer;
  *
  * @author jrandom
  */
-public class ClientConnectionRunner {
+class ClientConnectionRunner {
     private Log _log;
-    private RouterContext _context;
+    protected final RouterContext _context;
     private ClientManager _manager;
     /** socket for this particular peer connection */
     private Socket _socket;
@@ -71,7 +71,7 @@ public class ClientConnectionRunner {
     /** set of messageIds created but not yet ACCEPTED */
     private Set<MessageId> _acceptedPending;
     /** thingy that does stuff */
-    private I2CPMessageReader _reader;
+    protected I2CPMessageReader _reader;
     /** just for this destination */
     private SessionKeyManager _sessionKeyManager;
     /** 
@@ -109,7 +109,7 @@ public class ClientConnectionRunner {
      */
     public void startRunning() {
         try {
-            _reader = new I2CPMessageReader(_socket.getInputStream(), new ClientMessageEventListener(_context, this));
+            _reader = new I2CPMessageReader(_socket.getInputStream(), new ClientMessageEventListener(_context, this, true));
             _writer = new ClientWriterRunner(_context, this);
             I2PThread t = new I2PThread(_writer);
             t.setName("I2CP Writer " + ++__id);
@@ -469,18 +469,8 @@ public class ClientConnectionRunner {
                 _log.warn("Error sending I2CP message - client went away", eofe);
             stopRunning();
         } catch (IOException ioe) {
-            // only warn if client went away
-            int level;
-            String emsg;
-            if (ioe.getMessage() != null && ioe.getMessage().startsWith("Pipe closed")) {
-                level = Log.WARN;
-                emsg = "Error sending I2CP message - client went away";
-            } else {
-                level = Log.ERROR;
-                emsg = "IO Error sending I2CP message to client";
-            }
-            if (_log.shouldLog(level)) 
-                _log.log(level, emsg, ioe);
+            if (_log.shouldLog(Log.ERROR)) 
+                _log.error("IO Error sending I2CP message to client", ioe);
             stopRunning();
         } catch (Throwable t) {
             _log.log(Log.CRIT, "Unhandled exception sending I2CP message to client", t);
diff --git a/router/java/src/net/i2p/router/client/ClientListenerRunner.java b/router/java/src/net/i2p/router/client/ClientListenerRunner.java
index 7a7d448ea99d10bf1ec51ee2953142ca17c915f4..5dc5c650686d47b7425e403614f3e2f4d0c7e848 100644
--- a/router/java/src/net/i2p/router/client/ClientListenerRunner.java
+++ b/router/java/src/net/i2p/router/client/ClientListenerRunner.java
@@ -24,13 +24,13 @@ import net.i2p.util.Log;
  *
  * @author jrandom
  */
-public class ClientListenerRunner implements Runnable {
-    protected Log _log;
-    protected RouterContext _context;
-    protected ClientManager _manager;
+class ClientListenerRunner implements Runnable {
+    protected final Log _log;
+    protected final RouterContext _context;
+    protected final ClientManager _manager;
     protected ServerSocket _socket;
-    protected int _port;
-    private boolean _bindAllInterfaces;
+    protected final int _port;
+    protected final boolean _bindAllInterfaces;
     protected boolean _running;
     protected boolean _listening;
     
@@ -38,18 +38,33 @@ public class ClientListenerRunner implements Runnable {
 
     public ClientListenerRunner(RouterContext context, ClientManager manager, int port) {
         _context = context;
-        _log = _context.logManager().getLog(ClientListenerRunner.class);
+        _log = _context.logManager().getLog(getClass());
         _manager = manager;
         _port = port;
-        
-        String val = context.getProperty(BIND_ALL_INTERFACES);
-        _bindAllInterfaces = Boolean.valueOf(val).booleanValue();
+        _bindAllInterfaces = context.getBooleanProperty(BIND_ALL_INTERFACES);
     }
     
-    public void setPort(int port) { _port = port; }
-    public int getPort() { return _port; }
     public boolean isListening() { return _running && _listening; }
     
+    /** 
+     * Get a ServerSocket.
+     * Split out so it can be overridden for SSL.
+     * @since 0.8.3
+     */
+    protected ServerSocket getServerSocket() throws IOException {
+        if (_bindAllInterfaces) {
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Listening on port " + _port + " on all interfaces");
+            return new ServerSocket(_port);
+        } else {
+            String listenInterface = _context.getProperty(ClientManagerFacadeImpl.PROP_CLIENT_HOST, 
+                                                          ClientManagerFacadeImpl.DEFAULT_HOST);
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Listening on port " + _port + " of the specific interface: " + listenInterface);
+            return new ServerSocket(_port, 0, InetAddress.getByName(listenInterface));
+        }
+    }
+                
     /** 
      * Start up the socket listener, listens for connections, and
      * fires those connections off via {@link #runConnection runConnection}.  
@@ -62,18 +77,7 @@ public class ClientListenerRunner implements Runnable {
         int curDelay = 1000;
         while (_running) {
             try {
-                if (_bindAllInterfaces) {
-                    if (_log.shouldLog(Log.INFO))
-                        _log.info("Listening on port " + _port + " on all interfaces");
-                    _socket = new ServerSocket(_port);
-                } else {
-                    String listenInterface = _context.getProperty(ClientManagerFacadeImpl.PROP_CLIENT_HOST, 
-                                                                  ClientManagerFacadeImpl.DEFAULT_HOST);
-                    if (_log.shouldLog(Log.INFO))
-                        _log.info("Listening on port " + _port + " of the specific interface: " + listenInterface);
-                    _socket = new ServerSocket(_port, 0, InetAddress.getByName(listenInterface));
-                }
-                
+                _socket = getServerSocket();
                 
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug("ServerSocket created, before accept: " + _socket);
@@ -131,7 +135,8 @@ public class ClientListenerRunner implements Runnable {
     }
     
     /** give the i2cp client 5 seconds to show that they're really i2cp clients */
-    private final static int CONNECT_TIMEOUT = 5*1000;
+    protected final static int CONNECT_TIMEOUT = 5*1000;
+    private final static int LOOP_DELAY = 250;
 
     /**
      *  Verify the first byte.
@@ -141,16 +146,17 @@ public class ClientListenerRunner implements Runnable {
     protected boolean validate(Socket socket) {
         try {
             InputStream is = socket.getInputStream();
-            for (int i = 0; i < 20; i++) {
+            for (int i = 0; i < CONNECT_TIMEOUT / LOOP_DELAY; i++) {
                 if (is.available() > 0)
                     return is.read() == I2PClient.PROTOCOL_BYTE;
-                try { Thread.sleep(250); } catch (InterruptedException ie) {}
+                try { Thread.sleep(LOOP_DELAY); } catch (InterruptedException ie) {}
             }
         } catch (IOException ioe) {}
         if (_log.shouldLog(Log.WARN))
              _log.warn("Peer did not authenticate themselves as I2CP quickly enough, dropping");
         return false;
     }
+
     /**
      * Handle the connection by passing it off to a {@link ClientConnectionRunner ClientConnectionRunner}
      *
diff --git a/router/java/src/net/i2p/router/client/ClientManager.java b/router/java/src/net/i2p/router/client/ClientManager.java
index 7d866ab0b650e961ecd763be04d41434721291b4..a534bdfb19627fb9b9acab93d67a061167159959 100644
--- a/router/java/src/net/i2p/router/client/ClientManager.java
+++ b/router/java/src/net/i2p/router/client/ClientManager.java
@@ -15,7 +15,9 @@ import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
 
+import net.i2p.client.I2PSessionException;
 import net.i2p.crypto.SessionKeyManager;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Destination;
@@ -23,8 +25,10 @@ import net.i2p.data.Hash;
 import net.i2p.data.LeaseSet;
 import net.i2p.data.Payload;
 import net.i2p.data.TunnelId;
+import net.i2p.data.i2cp.I2CPMessage;
 import net.i2p.data.i2cp.MessageId;
 import net.i2p.data.i2cp.SessionConfig;
+import net.i2p.internal.I2CPMessageQueue;
 import net.i2p.router.ClientManagerFacade;
 import net.i2p.router.ClientMessage;
 import net.i2p.router.Job;
@@ -39,13 +43,18 @@ import net.i2p.util.Log;
  *
  * @author jrandom
  */
-public class ClientManager {
-    private Log _log;
+class ClientManager {
+    private final Log _log;
     private ClientListenerRunner _listener;
-    private ClientListenerRunner _internalListener;
     private final HashMap<Destination, ClientConnectionRunner>  _runners;        // Destination --> ClientConnectionRunner
     private final Set<ClientConnectionRunner> _pendingRunners; // ClientConnectionRunner for clients w/out a Dest yet
-    private RouterContext _ctx;
+    private final RouterContext _ctx;
+    private boolean _isStarted;
+
+    /** Disable external interface, allow internal clients only @since 0.8.3 */
+    private static final String PROP_DISABLE_EXTERNAL = "i2cp.disableInterface";
+    /** SSL interface (only) @since 0.8.3 */
+    private static final String PROP_ENABLE_SSL = "i2cp.SSL";
 
     /** ms to wait before rechecking for inbound messages to deliver to clients */
     private final static int INBOUND_POLL_INTERVAL = 300;
@@ -53,10 +62,10 @@ public class ClientManager {
     public ClientManager(RouterContext context, int port) {
         _ctx = context;
         _log = context.logManager().getLog(ClientManager.class);
-        _ctx.statManager().createRateStat("client.receiveMessageSize", 
-                                              "How large are messages received by the client?", 
-                                              "ClientMessages", 
-                                              new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l });
+        //_ctx.statManager().createRateStat("client.receiveMessageSize", 
+        //                                      "How large are messages received by the client?", 
+        //                                      "ClientMessages", 
+        //                                      new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l });
         _runners = new HashMap();
         _pendingRunners = new HashSet();
         startListeners(port);
@@ -64,16 +73,16 @@ public class ClientManager {
 
     /** Todo: Start a 3rd listener for IPV6? */
     private void startListeners(int port) {
-        _listener = new ClientListenerRunner(_ctx, this, port);
-        Thread t = new I2PThread(_listener);
-        t.setName("ClientListener:" + port);
-        t.setDaemon(true);
-        t.start();
-        _internalListener = new InternalClientListenerRunner(_ctx, this, port);
-        t = new I2PThread(_internalListener);
-        t.setName("ClientListener:" + port + "-i");
-        t.setDaemon(true);
-        t.start();
+        if (!_ctx.getBooleanProperty(PROP_DISABLE_EXTERNAL)) {
+            // there's no option to start both an SSL and non-SSL listener
+            if (_ctx.getBooleanProperty(PROP_ENABLE_SSL))
+                _listener = new SSLClientListenerRunner(_ctx, this, port);
+            else
+                _listener = new ClientListenerRunner(_ctx, this, port);
+            Thread t = new I2PThread(_listener, "ClientListener:" + port, true);
+            t.start();
+        }
+        _isStarted = true;
     }
     
     public void restart() {
@@ -95,9 +104,10 @@ public class ClientManager {
     }
     
     public void shutdown() {
+        _isStarted = false;
         _log.info("Shutting down the ClientManager");
-        _listener.stopListening();
-        _internalListener.stopListening();
+        if (_listener != null)
+            _listener.stopListening();
         Set<ClientConnectionRunner> runners = new HashSet();
         synchronized (_runners) {
             for (Iterator<ClientConnectionRunner> iter = _runners.values().iterator(); iter.hasNext();) {
@@ -117,7 +127,28 @@ public class ClientManager {
         }
     }
     
-    public boolean isAlive() { return _listener.isListening(); }
+    /**
+     *  The InternalClientManager interface.
+     *  Connects to the router, receiving a message queue to talk to the router with.
+     *  @throws I2PSessionException if the router isn't ready
+     *  @since 0.8.3
+     */
+    public I2CPMessageQueue internalConnect() throws I2PSessionException {
+        if (!_isStarted)
+            throw new I2PSessionException("Router client manager is shut down");
+        // for now we make these unlimited size
+        LinkedBlockingQueue<I2CPMessage> in = new LinkedBlockingQueue();
+        LinkedBlockingQueue<I2CPMessage> out = new LinkedBlockingQueue();
+        I2CPMessageQueue myQueue = new I2CPMessageQueueImpl(in, out);
+        I2CPMessageQueue hisQueue = new I2CPMessageQueueImpl(out, in);
+        ClientConnectionRunner runner = new QueuedClientConnectionRunner(_ctx, this, myQueue);
+        registerConnection(runner);
+        return hisQueue;
+    }
+
+    public boolean isAlive() {
+        return _isStarted && (_listener == null || _listener.isListening());
+    }
 
     public void registerConnection(ClientConnectionRunner runner) {
         synchronized (_pendingRunners) {
@@ -469,8 +500,8 @@ public class ClientManager {
                 runner = getRunner(_msg.getDestinationHash());
 
             if (runner != null) {
-                _ctx.statManager().addRateData("client.receiveMessageSize", 
-                                                   _msg.getPayload().getSize(), 0);
+                //_ctx.statManager().addRateData("client.receiveMessageSize", 
+                //                                   _msg.getPayload().getSize(), 0);
                 runner.receiveMessage(_msg.getDestination(), null, _msg.getPayload());
             } else {
                 // no client connection...
diff --git a/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java b/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java
index 066d6cc354223c3ee1e53b56e31c78fc4f82e4ab..5fd0bbc28baf0c4a7326efb7539f3fa35448cb1d 100644
--- a/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java
+++ b/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java
@@ -14,6 +14,7 @@ import java.util.Collections;
 import java.util.Iterator;
 import java.util.Set;
 
+import net.i2p.client.I2PSessionException;
 import net.i2p.crypto.SessionKeyManager;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Destination;
@@ -21,6 +22,8 @@ import net.i2p.data.Hash;
 import net.i2p.data.LeaseSet;
 import net.i2p.data.i2cp.MessageId;
 import net.i2p.data.i2cp.SessionConfig;
+import net.i2p.internal.I2CPMessageQueue;
+import net.i2p.internal.InternalClientManager;
 import net.i2p.router.ClientManagerFacade;
 import net.i2p.router.ClientMessage;
 import net.i2p.router.Job;
@@ -32,7 +35,7 @@ import net.i2p.util.Log;
  *
  * @author jrandom
  */
-public class ClientManagerFacadeImpl extends ClientManagerFacade {
+public class ClientManagerFacadeImpl extends ClientManagerFacade implements InternalClientManager {
     private final static Log _log = new Log(ClientManagerFacadeImpl.class);
     private ClientManager _manager; 
     private RouterContext _context;
@@ -220,4 +223,16 @@ public class ClientManagerFacadeImpl extends ClientManagerFacade {
         else
             return Collections.EMPTY_SET;
     }
+
+    /**
+     *  The InternalClientManager interface.
+     *  Connect to the router, receiving a message queue to talk to the router with.
+     *  @throws I2PSessionException if the router isn't ready
+     *  @since 0.8.3
+     */
+    public I2CPMessageQueue connect() throws I2PSessionException {
+        if (_manager != null)
+            return _manager.internalConnect();
+        throw new I2PSessionException("No manager yet");
+    }
 }
diff --git a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
index edaefc599005388b314a057ef42ded213487f2ec..d45df2cdb0245e7e779b29ec770f6832bc19da7a 100644
--- a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
+++ b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
@@ -42,14 +42,19 @@ import net.i2p.util.RandomSource;
  *
  */
 class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventListener {
-    private Log _log;
-    private RouterContext _context;
-    private ClientConnectionRunner _runner;
+    private final Log _log;
+    private final RouterContext _context;
+    private final ClientConnectionRunner _runner;
+    private final boolean  _enforceAuth;
     
-    public ClientMessageEventListener(RouterContext context, ClientConnectionRunner runner) {
+    /**
+     *  @param enforceAuth set false for in-JVM, true for socket access
+     */
+    public ClientMessageEventListener(RouterContext context, ClientConnectionRunner runner, boolean enforceAuth) {
         _context = context;
         _log = _context.logManager().getLog(ClientMessageEventListener.class);
         _runner = runner;
+        _enforceAuth = enforceAuth;
         _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 });
     }
     
@@ -153,10 +158,7 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
         }
 
         // Auth, since 0.8.2
-        // In-JVM accesses have access to the same context properties, so
-        // they will be set on the client side... therefore we don't need to pass in
-        // some indication of (socket instanceof InternalSocket)
-        if (Boolean.valueOf(_context.getProperty("i2cp.auth")).booleanValue()) {
+        if (_enforceAuth && Boolean.valueOf(_context.getProperty("i2cp.auth")).booleanValue()) {
             String configUser = _context.getProperty("i2cp.username");
             String configPW = _context.getProperty("i2cp.password");
             if (configUser != null && configPW != null) {
diff --git a/router/java/src/net/i2p/router/client/ClientWriterRunner.java b/router/java/src/net/i2p/router/client/ClientWriterRunner.java
index 49fcddcc208962b78097acc645c6b1215a5b7f77..b93a4e5f447e2102a66202280ea667b4baa9c135 100644
--- a/router/java/src/net/i2p/router/client/ClientWriterRunner.java
+++ b/router/java/src/net/i2p/router/client/ClientWriterRunner.java
@@ -8,6 +8,7 @@ import java.util.concurrent.LinkedBlockingQueue;
 import net.i2p.data.i2cp.I2CPMessage;
 import net.i2p.data.i2cp.I2CPMessageImpl;
 import net.i2p.data.i2cp.I2CPMessageException;
+import net.i2p.internal.PoisonI2CPMessage;
 import net.i2p.router.RouterContext;
 import net.i2p.util.Log;
 
@@ -52,7 +53,7 @@ class ClientWriterRunner implements Runnable {
     public void stopWriting() {
         _messagesToWrite.clear();
         try {
-            _messagesToWrite.put(new PoisonMessage());
+            _messagesToWrite.put(new PoisonI2CPMessage());
         } catch (InterruptedException ie) {}
     }
 
@@ -64,23 +65,9 @@ class ClientWriterRunner implements Runnable {
             } catch (InterruptedException ie) {
                 continue;
             }
-            if (msg.getType() == PoisonMessage.MESSAGE_TYPE)
+            if (msg.getType() == PoisonI2CPMessage.MESSAGE_TYPE)
                 break;
             _runner.writeMessage(msg);
         }
     }
-
-    /**
-     * End-of-stream msg used to stop the concurrent queue
-     * See http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/BlockingQueue.html
-     *
-     */
-    private static class PoisonMessage extends I2CPMessageImpl {
-        public static final int MESSAGE_TYPE = 999999;
-        public int getType() {
-            return MESSAGE_TYPE;
-        }
-        public void doReadMessage(InputStream buf, int size) throws I2CPMessageException, IOException {}
-        public byte[] doWriteMessage() throws I2CPMessageException, IOException { return null; }
-    }
 }
diff --git a/router/java/src/net/i2p/router/client/I2CPMessageQueueImpl.java b/router/java/src/net/i2p/router/client/I2CPMessageQueueImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..f65b061766a8bf9982e09601ebf775de0b03cd7e
--- /dev/null
+++ b/router/java/src/net/i2p/router/client/I2CPMessageQueueImpl.java
@@ -0,0 +1,57 @@
+package net.i2p.router.client;
+
+import java.util.concurrent.BlockingQueue;
+
+import net.i2p.data.i2cp.I2CPMessage;
+import net.i2p.internal.I2CPMessageQueue;
+
+/**
+ * Contains the methods to talk to a router or client via I2CP,
+ * when both are in the same JVM.
+ * This interface contains methods to access two queues,
+ * one for transmission and one for receiving.
+ * The methods are identical to those in java.util.concurrent.BlockingQueue
+ *
+ * @author zzz
+ * @since 0.8.3
+ */
+class I2CPMessageQueueImpl extends I2CPMessageQueue {
+    private final BlockingQueue<I2CPMessage> _in;
+    private final BlockingQueue<I2CPMessage> _out;
+
+    public I2CPMessageQueueImpl(BlockingQueue<I2CPMessage> in, BlockingQueue<I2CPMessage> out) {
+        _in = in;
+        _out = out;
+    }
+
+    /**
+     *  Send a message, nonblocking
+     *  @return success (false if no space available)
+     */
+    public boolean offer(I2CPMessage msg) {
+        return _out.offer(msg);
+    }
+
+    /**
+     *  Receive a message, nonblocking
+     *  @return message or null if none available
+     */
+    public I2CPMessage poll() {
+        return _in.poll();
+    }
+
+    /**
+     *  Send a message, blocking until space is available
+     */
+    public void put(I2CPMessage msg) throws InterruptedException {
+        _out.put(msg);
+    }
+
+    /**
+     *  Receive a message, blocking until one is available
+     *  @return message
+     */
+    public I2CPMessage take() throws InterruptedException {
+        return _in.take();
+    }
+}
diff --git a/router/java/src/net/i2p/router/client/InternalClientListenerRunner.java b/router/java/src/net/i2p/router/client/InternalClientListenerRunner.java
deleted file mode 100644
index 995c69400f5d17988f7ccd0f9d47008e6968956b..0000000000000000000000000000000000000000
--- a/router/java/src/net/i2p/router/client/InternalClientListenerRunner.java
+++ /dev/null
@@ -1,89 +0,0 @@
-package net.i2p.router.client;
-/*
- * free (adj.): unencumbered; not under the control of others
- * Written by jrandom in 2003 and released into the public domain 
- * with no warranty of any kind, either expressed or implied.  
- * It probably won't make your computer catch on fire, or eat 
- * your children, but it might.  Use at your own risk.
- *
- */
-
-import java.io.IOException;
-import java.net.Socket;
-
-import net.i2p.router.RouterContext;
-import net.i2p.util.Log;
-import net.i2p.util.InternalServerSocket;
-
-/**
- * Listen for in-JVM connections on the internal "socket"
- *
- * @author zzz
- * @since 0.7.9
- */
-public class InternalClientListenerRunner extends ClientListenerRunner {
-
-    public InternalClientListenerRunner(RouterContext context, ClientManager manager, int port) {
-        super(context, manager, port);
-        _log = _context.logManager().getLog(InternalClientListenerRunner.class);
-    }
-    
-    /** 
-     * Start up the socket listener, listens for connections, and
-     * fires those connections off via {@link #runConnection runConnection}.  
-     * This only returns if the socket cannot be opened or there is a catastrophic
-     * failure.
-     *
-     */
-    public void runServer() {
-        try {
-            if (_log.shouldLog(Log.INFO))
-                _log.info("Listening on internal port " + _port);
-            _socket = new InternalServerSocket(_port);
-            
-            if (_log.shouldLog(Log.DEBUG))
-                _log.debug("InternalServerSocket created, before accept: " + _socket);
-            
-            _listening = true;
-            _running = true;
-            while (_running) {
-                try {
-                    Socket socket = _socket.accept();
-                    if (validate(socket)) {
-                        if (_log.shouldLog(Log.DEBUG))
-                            _log.debug("Internal connection received");
-                        runConnection(socket);
-                    } else {
-                        if (_log.shouldLog(Log.WARN))
-                            _log.warn("Refused connection from " + socket.getInetAddress());
-                        try {
-                            socket.close();
-                        } catch (IOException ioe) {}
-                    }
-                } catch (IOException ioe) {
-                    if (_context.router().isAlive()) 
-                        _log.error("Server error accepting", ioe);
-                } catch (Throwable t) {
-                    if (_context.router().isAlive()) 
-                        _log.error("Fatal error running client listener - killing the thread!", t);
-                    _listening = false;
-                    return;
-                }
-            }
-        } catch (IOException ioe) {
-            if (_context.router().isAlive()) 
-                _log.error("Error listening on internal port " + _port, ioe);
-        }
-        
-        _listening = false;
-        if (_socket != null) {
-            try { _socket.close(); } catch (IOException ioe) {}
-            _socket = null; 
-        }
-        
-
-        if (_context.router().isAlive())
-            _log.error("CANCELING I2CP LISTEN", new Exception("I2CP Listen cancelled!!!"));
-        _running = false;
-    }
-}
diff --git a/router/java/src/net/i2p/router/client/QueuedClientConnectionRunner.java b/router/java/src/net/i2p/router/client/QueuedClientConnectionRunner.java
new file mode 100644
index 0000000000000000000000000000000000000000..758e8221e76e1956f1ed9596f3be895513e27414
--- /dev/null
+++ b/router/java/src/net/i2p/router/client/QueuedClientConnectionRunner.java
@@ -0,0 +1,76 @@
+package net.i2p.router.client;
+
+import java.io.IOException;
+
+import net.i2p.data.i2cp.I2CPMessage;
+import net.i2p.data.i2cp.I2CPMessageException;
+import net.i2p.internal.I2CPMessageQueue;
+import net.i2p.internal.QueuedI2CPMessageReader;
+import net.i2p.router.RouterContext;
+import net.i2p.util.Log;
+
+/**
+ * Zero-copy in-JVM.
+ * While super() starts both a reader and a writer thread, we only need a reader thread here.
+ *
+ * @author zzz
+ * @since 0.8.3
+ */
+class QueuedClientConnectionRunner extends ClientConnectionRunner {
+    private final I2CPMessageQueue queue;
+    
+    /**
+     * Create a new runner with the given queues
+     *
+     */
+    public QueuedClientConnectionRunner(RouterContext context, ClientManager manager, I2CPMessageQueue queue) {
+        super(context, manager, null);
+        this.queue = queue;
+    }
+    
+
+
+    /**
+     * Starts the reader thread. Does not call super().
+     */
+    @Override
+    public void startRunning() {
+        _reader = new QueuedI2CPMessageReader(this.queue, new ClientMessageEventListener(_context, this, false));
+        _reader.startReading();
+    }
+    
+    /**
+     * Calls super() to stop the reader, and sends a poison message to the client.
+     */
+    @Override
+    void stopRunning() {
+        super.stopRunning();
+        queue.close();
+    }
+    
+    /**
+     *  In super(), doSend queues it to the writer thread and
+     *  the writer thread calls writeMessage() to write to the output stream.
+     *  Since we have no writer thread this shouldn't happen.
+     */
+    @Override
+    void writeMessage(I2CPMessage msg) {
+        throw new RuntimeException("huh?");
+    }
+    
+    /**
+     * Actually send the I2CPMessage to the client.
+     * Nonblocking.
+     */
+    @Override
+    void doSend(I2CPMessage msg) throws I2CPMessageException {
+        // This will never fail, for now, as the router uses unbounded queues
+        // Perhaps in the future we may want to use bounded queues,
+        // with non-blocking writes for the router
+        // and blocking writes for the client?
+        boolean success = queue.offer(msg);
+        if (!success)
+            throw new I2CPMessageException("I2CP write to queue failed");
+    }
+    
+}
diff --git a/router/java/src/net/i2p/router/client/SSLClientListenerRunner.java b/router/java/src/net/i2p/router/client/SSLClientListenerRunner.java
new file mode 100644
index 0000000000000000000000000000000000000000..0dc053a3361e3b57d2d6d7931712cbe10d307b8a
--- /dev/null
+++ b/router/java/src/net/i2p/router/client/SSLClientListenerRunner.java
@@ -0,0 +1,282 @@
+package net.i2p.router.client;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.ServerSocket;
+import java.security.KeyStore;
+import java.security.GeneralSecurityException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.util.Arrays;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLServerSocketFactory;
+import javax.net.ssl.SSLContext;
+
+import net.i2p.client.I2PClient;
+import net.i2p.data.Base32;
+import net.i2p.data.Base64;
+import net.i2p.router.RouterContext;
+import net.i2p.util.Log;
+import net.i2p.util.SecureDirectory;
+import net.i2p.util.SecureFileOutputStream;
+import net.i2p.util.ShellCommand;
+
+/**
+ * SSL version of ClientListenerRunner
+ *
+ * @since 0.8.3
+ * @author zzz
+ */
+class SSLClientListenerRunner extends ClientListenerRunner {
+
+    private SSLServerSocketFactory _factory;
+
+    private static final String PROP_KEYSTORE_PASSWORD = "i2cp.keystorePassword";
+    private static final String DEFAULT_KEYSTORE_PASSWORD = "changeit";
+    private static final String PROP_KEY_PASSWORD = "i2cp.keyPassword";
+    private static final String KEY_ALIAS = "i2cp";
+    private static final String ASCII_KEYFILE = "i2cp.local.crt";
+
+    public SSLClientListenerRunner(RouterContext context, ClientManager manager, int port) {
+        super(context, manager, port);
+    }
+    
+    /**
+     * @return success if it exists and we have a password, or it was created successfully.
+     */
+    private boolean verifyKeyStore(File ks) {
+        if (ks.exists()) {
+            boolean rv = _context.getProperty(PROP_KEY_PASSWORD) != null;
+            if (!rv)
+                _log.error("I2CP SSL error, must set " + PROP_KEY_PASSWORD + " in " +
+                           (new File(_context.getConfigDir(), "router.config")).getAbsolutePath());
+            return rv;
+        }
+        File dir = ks.getParentFile();
+        if (!dir.exists()) {
+            File sdir = new SecureDirectory(dir.getAbsolutePath());
+            if (!sdir.mkdir())
+                return false;
+        }
+        boolean rv = createKeyStore(ks);
+
+        // Now read it back out of the new keystore and save it in ascii form
+        // where the clients can get to it.
+        // Failure of this part is not fatal.
+        if (rv)
+            exportCert(ks);
+        return rv;
+    }
+
+
+    /**
+     * Call out to keytool to create a new keystore with a keypair in it.
+     * Trying to do this programatically is a nightmare, requiring either BouncyCastle
+     * libs or using proprietary Sun libs, and it's a huge mess.
+     * If successful, stores the keystore password and key password in router.config.
+     *
+     * @return success
+     */
+    private boolean createKeyStore(File ks) {
+        // make a random 48 character password (30 * 8 / 5)
+        byte[] rand = new byte[30];
+        _context.random().nextBytes(rand);
+        String keyPassword = Base32.encode(rand);
+        // and one for the cname
+        _context.random().nextBytes(rand);
+        String cname = Base32.encode(rand) + ".i2cp.i2p.net";
+
+        String keytool = (new File(System.getProperty("java.home"), "bin/keytool")).getAbsolutePath();
+        String[] args = new String[] {
+                   keytool,
+                   "-genkey",            // -genkeypair preferred in newer keytools, but this works with more
+                   "-storetype", KeyStore.getDefaultType(),
+                   "-keystore", ks.getAbsolutePath(),
+                   "-storepass", DEFAULT_KEYSTORE_PASSWORD,
+                   "-alias", KEY_ALIAS,
+                   "-dname", "CN=" + cname + ",OU=I2CP,O=I2P Anonymous Network,L=XX,ST=XX,C=XX",
+                   "-validity", "3652",  // 10 years
+                   "-keyalg", "DSA",
+                   "-keysize", "1024",
+                   "-keypass", keyPassword};
+        boolean success = (new ShellCommand()).executeSilentAndWaitTimed(args, 30);  // 30 secs
+        if (success) {
+            success = ks.exists();
+            if (success) {
+                SecureFileOutputStream.setPerms(ks);
+                _context.router().setConfigSetting(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD);
+                _context.router().setConfigSetting(PROP_KEY_PASSWORD, keyPassword);
+                _context.router().saveConfig();
+            }
+        }
+        if (success) {
+            _log.logAlways(Log.INFO, "Created self-signed certificate for " + cname + " in keystore: " + ks.getAbsolutePath() + "\n" +
+                           "The certificate name was generated randomly, and is not associated with your " +
+                           "IP address, host name, router identity, or destination keys.");
+        } else {
+            _log.error("Failed to create I2CP SSL keystore using command line:");
+            StringBuilder buf = new StringBuilder(256);
+            for (int i = 0;  i < args.length; i++) {
+                buf.append('"').append(args[i]).append("\" ");
+            }
+            _log.error(buf.toString());
+            _log.error("This is for the Sun/Oracle keytool, others may be incompatible.\n" +
+                       "If you create the keystore manually, you must add " + PROP_KEYSTORE_PASSWORD + " and " + PROP_KEY_PASSWORD +
+                       " to " + (new File(_context.getConfigDir(), "router.config")).getAbsolutePath());
+        }
+        return success;
+    }
+
+    /** 
+     * Pull the cert back OUT of the keystore and save it as ascii
+     * so the clients can get to it.
+     */
+    private void exportCert(File ks) {
+        File sdir = new SecureDirectory(_context.getConfigDir(), "certificates");
+        if (sdir.exists() || sdir.mkdir()) {
+            try {
+                KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+                InputStream fis = new FileInputStream(ks);
+                String ksPass = _context.getProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD);
+                keyStore.load(fis, ksPass.toCharArray());
+                fis.close();
+                Certificate cert = keyStore.getCertificate(KEY_ALIAS);
+                if (cert != null) {
+                    File certFile = new File(sdir, ASCII_KEYFILE);
+                    saveCert(cert, certFile);
+                } else {
+                    _log.error("Error getting SSL cert to save as ASCII");
+                }
+            } catch (GeneralSecurityException gse) {
+                _log.error("Error saving ASCII SSL keys", gse);
+            } catch (IOException ioe) {
+                _log.error("Error saving ASCII SSL keys", ioe);
+            }
+        } else {
+            _log.error("Error saving ASCII SSL keys");
+        }
+    }
+
+    private static final int LINE_LENGTH = 64;
+
+    /**
+     *  Modified from:
+     *  http://www.exampledepot.com/egs/java.security.cert/ExportCert.html
+     *
+     *  Write a certificate to a file in base64 format.
+     */
+    private void saveCert(Certificate cert, File file) {
+        OutputStream os = null;
+        try {
+           // Get the encoded form which is suitable for exporting
+           byte[] buf = cert.getEncoded();
+           os = new SecureFileOutputStream(file);
+           PrintWriter wr = new PrintWriter(os);
+           wr.println("-----BEGIN CERTIFICATE-----");
+           String b64 = Base64.encode(buf, true);     // true = use standard alphabet
+           for (int i = 0; i < b64.length(); i += LINE_LENGTH) {
+               wr.println(b64.substring(i, Math.min(i + LINE_LENGTH, b64.length())));
+           }
+           wr.println("-----END CERTIFICATE-----");
+           wr.flush();
+        } catch (CertificateEncodingException cee) {
+           _log.error("Error writing X509 Certificate " + file.getAbsolutePath(), cee);
+        } catch (IOException ioe) {
+            _log.error("Error writing X509 Certificate " + file.getAbsolutePath(), ioe);
+        } finally {
+            try { if (os != null) os.close(); } catch (IOException foo) {}
+        }
+    }
+
+    /** 
+     * Sets up the SSLContext and sets the socket factory.
+     * @return success
+     */
+    private boolean initializeFactory(File ks) {
+        String ksPass = _context.getProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD);
+        String keyPass = _context.getProperty(PROP_KEY_PASSWORD);
+        if (keyPass == null) {
+            _log.error("No key password, set " + PROP_KEY_PASSWORD +
+                       " in " + (new File(_context.getConfigDir(), "router.config")).getAbsolutePath());
+            return false;
+        }
+        try {
+            SSLContext sslc = SSLContext.getInstance("TLS");
+            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+            InputStream fis = new FileInputStream(ks);
+            keyStore.load(fis, ksPass.toCharArray());
+            fis.close();
+            KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+            kmf.init(keyStore, keyPass.toCharArray());
+            sslc.init(kmf.getKeyManagers(), null, _context.random());
+            _factory = sslc.getServerSocketFactory();
+            return true;
+        } catch (GeneralSecurityException gse) {
+            _log.error("Error loading SSL keys", gse);
+        } catch (IOException ioe) {
+            _log.error("Error loading SSL keys", ioe);
+        }
+        return false;
+    }
+
+    /** 
+     * Get a SSLServerSocket.
+     */
+    @Override
+    protected ServerSocket getServerSocket() throws IOException {
+        ServerSocket rv;
+        if (_bindAllInterfaces) {
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Listening on port " + _port + " on all interfaces");
+            rv = _factory.createServerSocket(_port);
+        } else {
+            String listenInterface = _context.getProperty(ClientManagerFacadeImpl.PROP_CLIENT_HOST, 
+                                                          ClientManagerFacadeImpl.DEFAULT_HOST);
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Listening on port " + _port + " of the specific interface: " + listenInterface);
+            rv = _factory.createServerSocket(_port, 0, InetAddress.getByName(listenInterface));
+        }
+        return rv;
+    }
+
+    /** 
+     * Create (if necessary) and load the key store, then run.
+     */
+    @Override
+    public void runServer() {
+        File keyStore = new File(_context.getConfigDir(), "keystore/i2cp.ks");
+        if (verifyKeyStore(keyStore) && initializeFactory(keyStore)) {
+            super.runServer();
+        } else {
+            _log.error("SSL I2CP server error - Failed to create or open key store");
+        }
+    }
+
+    /**
+     *  Overridden because SSL handshake may need more time,
+     *  and available() in super doesn't work.
+     *  The handshake doesn't start until a read().
+     */
+    @Override
+    protected boolean validate(Socket socket) {
+        try {
+            InputStream is = socket.getInputStream();
+            int oldTimeout = socket.getSoTimeout();
+            socket.setSoTimeout(4 * CONNECT_TIMEOUT);
+            boolean rv = is.read() == I2PClient.PROTOCOL_BYTE;
+            socket.setSoTimeout(oldTimeout);
+            return rv;
+        } catch (IOException ioe) {}
+        if (_log.shouldLog(Log.WARN))
+             _log.warn("Peer did not authenticate themselves as I2CP quickly enough, dropping");
+        return false;
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java
index 39856f509f95bc1d2daf41fcb088ad1db7b85e66..094f526c79913e433bfa53e178bee6d2f0b22725 100644
--- a/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java
+++ b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java
@@ -45,6 +45,7 @@ public class CommSystemFacadeImpl extends CommSystemFacade {
         _context = context;
         _log = _context.logManager().getLog(CommSystemFacadeImpl.class);
         _manager = null;
+        _context.statManager().createRateStat("transport.getBidsJobTime", "How long does it take?", "Transport", new long[] { 10*60*1000l });
         startGeoIP();
     }
     
@@ -131,7 +132,9 @@ public class CommSystemFacadeImpl extends CommSystemFacade {
     public void processMessage(OutNetMessage msg) {	
         //GetBidsJob j = new GetBidsJob(_context, this, msg);
         //j.runJob();
+        long before = _context.clock().now();
         GetBidsJob.getBids(_context, this, msg);
+        _context.statManager().addRateData("transport.getBidsJobTime", _context.clock().now() - before, 0);
     }
     
     @Override
diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPSendFinisher.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPSendFinisher.java
index fd5cf1ac91c7445f7cdec059c058571a5d0a5c30..374c7a5ba0dbf89fca890c991106a7209377476d 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/NTCPSendFinisher.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPSendFinisher.java
@@ -24,22 +24,27 @@ import net.i2p.util.Log;
  * @author zzz
  */
 public class NTCPSendFinisher {
-    private static final int THREADS = 4;
+    private static final int MIN_THREADS = 1;
+    private static final int MAX_THREADS = 4;
     private final I2PAppContext _context;
     private final NTCPTransport _transport;
     private final Log _log;
-    private int _count;
+    private static int _count;
     private ThreadPoolExecutor _executor;
+    private static int _threads;
 
     public NTCPSendFinisher(I2PAppContext context, NTCPTransport transport) {
         _context = context;
         _log = _context.logManager().getLog(NTCPSendFinisher.class);
         _transport = transport;
+        _context.statManager().createRateStat("ntcp.sendFinishTime", "How long to queue and excecute msg.afterSend()", "ntcp", new long[] {5*1000});
     }
     
     public void start() {
         _count = 0;
-        _executor = new CustomThreadPoolExecutor();
+        long maxMemory = Runtime.getRuntime().maxMemory();
+        _threads = (int) Math.max(MIN_THREADS, Math.min(MAX_THREADS, 1 + (maxMemory / (32*1024*1024))));
+        _executor = new CustomThreadPoolExecutor(_threads);
     }
 
     public void stop() {
@@ -57,18 +62,18 @@ public class NTCPSendFinisher {
     }
     
     // not really needed for now but in case we want to add some hooks like afterExecute()
-    private class CustomThreadPoolExecutor extends ThreadPoolExecutor {
-        public CustomThreadPoolExecutor() {
+    private static class CustomThreadPoolExecutor extends ThreadPoolExecutor {
+        public CustomThreadPoolExecutor(int num) {
              // use unbounded queue, so maximumPoolSize and keepAliveTime have no effect
-             super(THREADS, THREADS, 1000, TimeUnit.MILLISECONDS,
+             super(num, num, 1000, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue(), new CustomThreadFactory());
         }
     }
 
-    private class CustomThreadFactory implements ThreadFactory {
+    private static class CustomThreadFactory implements ThreadFactory {
         public Thread newThread(Runnable r) {
             Thread rv = Executors.defaultThreadFactory().newThread(r);
-            rv.setName("NTCPSendFinisher " + (++_count) + '/' + THREADS);
+            rv.setName("NTCPSendFinisher " + (++_count) + '/' + _threads);
             rv.setDaemon(true);
             return rv;
         }
@@ -78,15 +83,18 @@ public class NTCPSendFinisher {
      * Call afterSend() for the message
      */
     private class RunnableEvent implements Runnable {
-        private OutNetMessage _msg;
+        private final OutNetMessage _msg;
+        private final long _queued;
 
         public RunnableEvent(OutNetMessage msg) {
             _msg = msg;
+            _queued = _context.clock().now();
         }
 
         public void run() {
             try {
                 _transport.afterSend(_msg, true, false, _msg.getSendTime());
+                _context.statManager().addRateData("ntcp.sendFinishTime", _context.clock().now() - _queued, 0);
             } catch (Throwable t) {
                 _log.log(Log.CRIT, " wtf, afterSend borked", t);
             }
diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
index 3d6d91f5167f1bb3e2509418b44ecf4494289f7e..31060ba411f41edcbb969a5f12dba21a5ebca97b 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
@@ -433,8 +433,10 @@ public class NTCPTransport extends TransportImpl {
         return skews;
     }
 
-    private static final int NUM_CONCURRENT_READERS = 3;
-    private static final int NUM_CONCURRENT_WRITERS = 3;
+    private static final int MIN_CONCURRENT_READERS = 2;  // unless < 32MB
+    private static final int MIN_CONCURRENT_WRITERS = 2;  // unless < 32MB
+    private static final int MAX_CONCURRENT_READERS = 4;
+    private static final int MAX_CONCURRENT_WRITERS = 4;
 
     /**
      *  Called by TransportManager.
@@ -449,12 +451,8 @@ public class NTCPTransport extends TransportImpl {
         if (_pumper.isAlive())
             return _myAddress != null ? _myAddress.toRouterAddress() : null;
         if (_log.shouldLog(Log.WARN)) _log.warn("Starting ntcp transport listening");
-        _finisher.start();
-        _pumper.startPumping();
-
-        _reader.startReading(NUM_CONCURRENT_READERS);
-        _writer.startWriting(NUM_CONCURRENT_WRITERS);
 
+        startIt();
         configureLocalAddress();
         return bindAddress();
     }
@@ -471,12 +469,8 @@ public class NTCPTransport extends TransportImpl {
         if (_pumper.isAlive())
             return _myAddress != null ? _myAddress.toRouterAddress() : null;
         if (_log.shouldLog(Log.WARN)) _log.warn("Restarting ntcp transport listening");
-        _finisher.start();
-        _pumper.startPumping();
-
-        _reader.startReading(NUM_CONCURRENT_READERS);
-        _writer.startWriting(NUM_CONCURRENT_WRITERS);
 
+        startIt();
         if (addr == null)
             _myAddress = null;
         else
@@ -484,6 +478,28 @@ public class NTCPTransport extends TransportImpl {
         return bindAddress();
     }
 
+    /**
+     *  Start up. Caller must synchronize.
+     *  @since 0.8.3
+     */
+    private void startIt() {
+        _finisher.start();
+        _pumper.startPumping();
+
+        long maxMemory = Runtime.getRuntime().maxMemory();
+        int nr, nw;
+        if (maxMemory < 32*1024*1024) {
+            nr = nw = 1;
+        } else if (maxMemory < 64*1024*1024) {
+            nr = nw = 2;
+        } else {
+            nr = Math.max(MIN_CONCURRENT_READERS, Math.min(MAX_CONCURRENT_READERS, _context.bandwidthLimiter().getInboundKBytesPerSecond() / 20));
+            nw = Math.max(MIN_CONCURRENT_WRITERS, Math.min(MAX_CONCURRENT_WRITERS, _context.bandwidthLimiter().getOutboundKBytesPerSecond() / 20));
+        }
+        _reader.startReading(nr);
+        _writer.startWriting(nw);
+    }
+
     public boolean isAlive() {
         return _pumper.isAlive();
     }
diff --git a/router/java/src/net/i2p/router/transport/ntcp/Reader.java b/router/java/src/net/i2p/router/transport/ntcp/Reader.java
index c1029b26e5031ac2cd6b59f76c557b5e97a29545..96948154536224b8c42532431a7aafc2613d517c 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/Reader.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/Reader.java
@@ -15,13 +15,13 @@ import net.i2p.util.Log;
  *
  */
 class Reader {
-    private RouterContext _context;
-    private Log _log;
+    private final RouterContext _context;
+    private final Log _log;
     // TODO change to LBQ ??
     private final List<NTCPConnection> _pendingConnections;
-    private List<NTCPConnection> _liveReads;
-    private List<NTCPConnection> _readAfterLive;
-    private List<Runner> _runners;
+    private final List<NTCPConnection> _liveReads;
+    private final List<NTCPConnection> _readAfterLive;
+    private final List<Runner> _runners;
     
     public Reader(RouterContext ctx) {
         _context = ctx;
@@ -33,9 +33,9 @@ class Reader {
     }
     
     public void startReading(int numReaders) {
-        for (int i = 0; i < numReaders; i++) {
+        for (int i = 1; i <= numReaders; i++) {
             Runner r = new Runner();
-            I2PThread t = new I2PThread(r, "NTCP read " + i, true);
+            I2PThread t = new I2PThread(r, "NTCP reader " + i + '/' + numReaders, true);
             _runners.add(r);
             t.start();
         }
diff --git a/router/java/src/net/i2p/router/transport/ntcp/Writer.java b/router/java/src/net/i2p/router/transport/ntcp/Writer.java
index ca676c572ce5c4dc7a6572f964d8ec918338ee34..260569df7554799ba6deabdd8c479c824ad74162 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/Writer.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/Writer.java
@@ -14,12 +14,12 @@ import net.i2p.util.Log;
  *
  */
 class Writer {
-    private RouterContext _context;
-    private Log _log;
+    private final RouterContext _context;
+    private final Log _log;
     private final List<NTCPConnection> _pendingConnections;
-    private List<NTCPConnection> _liveWrites;
-    private List<NTCPConnection> _writeAfterLive;
-    private List<Runner> _runners;
+    private final List<NTCPConnection> _liveWrites;
+    private final List<NTCPConnection> _writeAfterLive;
+    private final List<Runner> _runners;
     
     public Writer(RouterContext ctx) {
         _context = ctx;
@@ -31,9 +31,9 @@ class Writer {
     }
     
     public void startWriting(int numWriters) {
-        for (int i = 0; i < numWriters; i++) {
+        for (int i = 1; i <=numWriters; i++) {
             Runner r = new Runner();
-            I2PThread t = new I2PThread(r, "NTCP write " + i, true);
+            I2PThread t = new I2PThread(r, "NTCP writer " + i + '/' + numWriters, true);
             _runners.add(r);
             t.start();
         }
diff --git a/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java b/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java
index 08b6088c4ffbd01295121adbed5559037ca30ca9..4988a06851a463b3b9abd9367c623741e185e96c 100644
--- a/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java
+++ b/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java
@@ -27,7 +27,9 @@ class MessageReceiver {
     private final BlockingQueue<InboundMessageState> _completeMessages;
     private boolean _alive;
     //private ByteCache _cache;
-    private static final int THREADS = 5;
+    private static final int MIN_THREADS = 2;  // unless < 32MB
+    private static final int MAX_THREADS = 5;
+    private final int _threadCount;
     private static final long POISON_IMS = -99999999999l;
     
     public MessageReceiver(RouterContext ctx, UDPTransport transport) {
@@ -35,10 +37,19 @@ class MessageReceiver {
         _log = ctx.logManager().getLog(MessageReceiver.class);
         _transport = transport;
         _completeMessages = new LinkedBlockingQueue();
+
+        long maxMemory = Runtime.getRuntime().maxMemory();
+        if (maxMemory < 32*1024*1024)
+            _threadCount = 1;
+        else if (maxMemory < 64*1024*1024)
+            _threadCount = 2;
+        else
+            _threadCount = Math.max(MIN_THREADS, Math.min(MAX_THREADS, ctx.bandwidthLimiter().getInboundKBytesPerSecond() / 20));
+
         // the runners run forever, no need to have a cache
         //_cache = ByteCache.getInstance(64, I2NPMessage.MAX_SIZE);
         _context.statManager().createRateStat("udp.inboundExpired", "How many messages were expired before reception?", "udp", UDPTransport.RATES);
-        _context.statManager().createRateStat("udp.inboundRemaining", "How many messages were remaining when a message is pulled off the complete queue?", "udp", UDPTransport.RATES);
+        //_context.statManager().createRateStat("udp.inboundRemaining", "How many messages were remaining when a message is pulled off the complete queue?", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.inboundReady", "How many messages were ready when a message is added to the complete queue?", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.inboundReadTime", "How long it takes to parse in the completed fragments into a message?", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.inboundReceiveProcessTime", "How long it takes to add the message to the transport?", "udp", UDPTransport.RATES);
@@ -49,8 +60,8 @@ class MessageReceiver {
     
     public void startup() {
         _alive = true;
-        for (int i = 0; i < THREADS; i++) {
-            I2PThread t = new I2PThread(new Runner(), "UDP message receiver " + i + '/' + THREADS, true);
+        for (int i = 0; i < _threadCount; i++) {
+            I2PThread t = new I2PThread(new Runner(), "UDP message receiver " + (i+1) + '/' + _threadCount, true);
             t.start();
         }
     }
@@ -64,7 +75,7 @@ class MessageReceiver {
     public void shutdown() {
         _alive = false;
         _completeMessages.clear();
-        for (int i = 0; i < THREADS; i++) {
+        for (int i = 0; i < _threadCount; i++) {
             InboundMessageState ims = new InboundMessageState(_context, POISON_IMS, null);
             _completeMessages.offer(ims);
         }
@@ -119,8 +130,8 @@ class MessageReceiver {
             
             if (message != null) {
                 long before = System.currentTimeMillis();
-                if (remaining > 0)
-                    _context.statManager().addRateData("udp.inboundRemaining", remaining, 0);
+                //if (remaining > 0)
+                //    _context.statManager().addRateData("udp.inboundRemaining", remaining, 0);
                 int size = message.getCompleteSize();
                 if (_log.shouldLog(Log.INFO))
                     _log.info("Full message received (" + message.getMessageId() + ") after " + message.getLifetime());
diff --git a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
index d130a4c8382c886b9c427bc0137f79d59f194fb1..2c0138228d728d69a854573718659232a03526bc 100644
--- a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
+++ b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
@@ -31,11 +31,13 @@ class PacketHandler {
     private boolean _keepReading;
     private final Handler[] _handlers;
     
-    private static final int NUM_HANDLERS = 5;
+    private static final int MIN_NUM_HANDLERS = 2;  // unless < 32MB
+    private static final int MAX_NUM_HANDLERS = 5;
     /** let packets be up to 30s slow */
     private static final long GRACE_PERIOD = Router.CLOCK_FUDGE_FACTOR + 30*1000;
     
-    PacketHandler(RouterContext ctx, UDPTransport transport, UDPEndpoint endpoint, EstablishmentManager establisher, InboundMessageFragments inbound, PeerTestManager testManager, IntroductionManager introManager) {// LINT -- Exporting non-public type through public API
+    PacketHandler(RouterContext ctx, UDPTransport transport, UDPEndpoint endpoint, EstablishmentManager establisher,
+                  InboundMessageFragments inbound, PeerTestManager testManager, IntroductionManager introManager) {
         _context = ctx;
         _log = ctx.logManager().getLog(PacketHandler.class);
         _transport = transport;
@@ -44,10 +46,20 @@ class PacketHandler {
         _inbound = inbound;
         _testManager = testManager;
         _introManager = introManager;
-        _handlers = new Handler[NUM_HANDLERS];
-        for (int i = 0; i < NUM_HANDLERS; i++) {
+
+        long maxMemory = Runtime.getRuntime().maxMemory();
+        int num_handlers;
+        if (maxMemory < 32*1024*1024)
+            num_handlers = 1;
+        else if (maxMemory < 64*1024*1024)
+            num_handlers = 2;
+        else
+            num_handlers = Math.max(MIN_NUM_HANDLERS, Math.min(MAX_NUM_HANDLERS, ctx.bandwidthLimiter().getInboundKBytesPerSecond() / 20));
+        _handlers = new Handler[num_handlers];
+        for (int i = 0; i < num_handlers; i++) {
             _handlers[i] = new Handler();
         }
+
         _context.statManager().createRateStat("udp.handleTime", "How long it takes to handle a received packet after its been pulled off the queue", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.queueTime", "How long after a packet is received can we begin handling it", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.receivePacketSkew", "How long ago after the packet was sent did we receive it", "udp", UDPTransport.RATES);
@@ -79,8 +91,8 @@ class PacketHandler {
     
     public void startup() { 
         _keepReading = true;
-        for (int i = 0; i < NUM_HANDLERS; i++) {
-            I2PThread t = new I2PThread(_handlers[i], "UDP Packet handler " + i + '/' + NUM_HANDLERS, true);
+        for (int i = 0; i < _handlers.length; i++) {
+            I2PThread t = new I2PThread(_handlers[i], "UDP Packet handler " + (i+1) + '/' + _handlers.length, true);
             t.start();
         }
     }
@@ -91,8 +103,8 @@ class PacketHandler {
 
     String getHandlerStatus() {
         StringBuilder rv = new StringBuilder();
-        rv.append("Handlers: ").append(NUM_HANDLERS);
-        for (int i = 0; i < NUM_HANDLERS; i++) {
+        rv.append("Handlers: ").append(_handlers.length);
+        for (int i = 0; i < _handlers.length; i++) {
             Handler handler = _handlers[i];
             rv.append(" handler ").append(i).append(" state: ").append(handler._state);
         }
diff --git a/router/java/src/net/i2p/router/tunnel/TunnelGatewayPumper.java b/router/java/src/net/i2p/router/tunnel/TunnelGatewayPumper.java
index 05db0b0ce4a9c96b39661f32d371a200d52a642c..7f29f5743d09b94aaf127dcba64c73bd030393f4 100644
--- a/router/java/src/net/i2p/router/tunnel/TunnelGatewayPumper.java
+++ b/router/java/src/net/i2p/router/tunnel/TunnelGatewayPumper.java
@@ -16,22 +16,26 @@ public class TunnelGatewayPumper implements Runnable {
     private RouterContext _context;
     private final BlockingQueue<PumpedTunnelGateway> _wantsPumping;
     private boolean _stop;
-    private static final int PUMPERS = 4;
+    private static final int MIN_PUMPERS = 1;
+    private static final int MAX_PUMPERS = 4;
+    private final int _pumpers;
     
     /** Creates a new instance of TunnelGatewayPumper */
     public TunnelGatewayPumper(RouterContext ctx) {
         _context = ctx;
         _wantsPumping = new LinkedBlockingQueue();
         _stop = false;
-        for (int i = 0; i < PUMPERS; i++)
-            new I2PThread(this, "Tunnel GW pumper " + i + '/' + PUMPERS, true).start();
+        long maxMemory = Runtime.getRuntime().maxMemory();
+        _pumpers = (int) Math.max(MIN_PUMPERS, Math.min(MAX_PUMPERS, 1 + (maxMemory / (32*1024*1024))));
+        for (int i = 0; i < _pumpers; i++)
+            new I2PThread(this, "Tunnel GW pumper " + (i+1) + '/' + _pumpers, true).start();
     }
 
     public void stopPumping() {
         _stop=true;
         _wantsPumping.clear();
         PumpedTunnelGateway poison = new PoisonPTG(_context);
-        for (int i = 0; i < PUMPERS; i++)
+        for (int i = 0; i < _pumpers; i++)
             _wantsPumping.offer(poison);
         for (int i = 1; i <= 5 && !_wantsPumping.isEmpty(); i++) {
             try {