diff --git a/apps/addressbook/web.xml b/apps/addressbook/web.xml
index 1aebb0d0964a4e6db360cd5e5b83881a71715404..d86d32289b8c031d5855bcf9c7a3d776dcbd6d15 100644
--- a/apps/addressbook/web.xml
+++ b/apps/addressbook/web.xml
@@ -28,4 +28,11 @@
       <url-pattern>/*</url-pattern>
     </servlet-mapping>
 
+    <!-- this webapp doesn't actually use sessions or cookies -->
+    <session-config>
+        <session-timeout>30</session-timeout>
+        <cookie-config>
+            <http-only>true</http-only>
+        </cookie-config>
+    </session-config>
 </web-app>
diff --git a/apps/i2psnark/web.xml b/apps/i2psnark/web.xml
index 68e6abd6412fd4bac016ddb8c83c1a3a75f090e4..d98f637338d7e3456a36dac7ed5e9177d00cb6a3 100644
--- a/apps/i2psnark/web.xml
+++ b/apps/i2psnark/web.xml
@@ -26,73 +26,14 @@
       <url-pattern>/</url-pattern>
     </servlet-mapping>
     
+    <!-- this webapp doesn't actually use sessions or cookies -->
     <session-config>
         <session-timeout>
             30
         </session-timeout>
+        <cookie-config>
+            <http-only>true</http-only>
+        </cookie-config>
     </session-config>
 
-
-    <!-- mime types not in mime.properties in the jetty 5.1.15 source -->
-
-    <mime-mapping>
-        <extension>mkv</extension>
-        <mime-type>video/x-matroska</mime-type>
-    </mime-mapping>
-
-    <mime-mapping>
-        <extension>wmv</extension>
-        <mime-type>video/x-ms-wmv</mime-type>
-    </mime-mapping>
-
-    <mime-mapping>
-        <extension>flv</extension>
-        <mime-type>video/x-flv</mime-type>
-    </mime-mapping>
-
-    <mime-mapping>
-        <extension>mp4</extension>
-        <mime-type>video/mp4</mime-type>
-    </mime-mapping>
-
-    <mime-mapping>
-        <extension>rar</extension>
-        <mime-type>application/rar</mime-type>
-    </mime-mapping>
-
-    <mime-mapping>
-        <extension>7z</extension>
-        <mime-type>application/x-7z-compressed</mime-type>
-    </mime-mapping>
-
-    <mime-mapping>
-        <extension>iso</extension>
-        <mime-type>application/x-iso9660-image</mime-type>
-    </mime-mapping>
-
-    <mime-mapping>
-        <extension>ico</extension>
-        <mime-type>image/x-icon</mime-type>
-    </mime-mapping>
-
-    <mime-mapping>
-        <extension>exe</extension>
-        <mime-type>application/x-msdos-program</mime-type>
-    </mime-mapping>
-
-    <mime-mapping>
-        <extension>flac</extension>
-        <mime-type>audio/flac</mime-type>
-    </mime-mapping>
-
-    <mime-mapping>
-        <extension>m4a</extension>
-        <mime-type>audio/mpeg</mime-type>
-    </mime-mapping>
-
-    <mime-mapping>
-        <extension>wma</extension>
-        <mime-type>audio/x-ms-wma</mime-type>
-    </mime-mapping>
-
 </web-app>
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java
index 8f963e6492d173ee801bb94e8224ae917eee09d0..4b06ae0789ca1ae1d15217203453fcbbc961cae9 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java
@@ -71,11 +71,16 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
     private static final long TOTAL_HEADER_TIMEOUT = 2 * HEADER_TIMEOUT;
     private static final long START_INTERVAL = (60 * 1000) * 3;
     private static final int MAX_LINE_LENGTH = 8*1024;
+    /** ridiculously long, just to prevent OOM DOS @since 0.7.13 */
+    private static final int MAX_HEADERS = 60;
+    /** Includes request, just to prevent OOM DOS @since 0.9.20 */
+    private static final int MAX_TOTAL_HEADER_SIZE = 32*1024;
+    
     private long _startedOn = 0L;
     private ConnThrottler _postThrottler;
 
-    private final static byte[] ERR_UNAVAILABLE =
-        ("HTTP/1.1 503 Service Unavailable\r\n"+
+    private final static String 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"+
@@ -84,11 +89,10 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
          "<html><head><title>503 Service Unavailable</title></head>\n"+
          "<body><h2>503 Service Unavailable</h2>\n" +
          "<p>This I2P website is unavailable. It may be down or undergoing maintenance.</p>\n" +
-         "</body></html>")
-         .getBytes();
+         "</body></html>";
 
-    private final static byte[] ERR_DENIED =
-        ("HTTP/1.1 403 Denied\r\n"+
+    private final static String ERR_DENIED =
+         "HTTP/1.1 403 Denied\r\n"+
          "Content-Type: text/html; charset=iso-8859-1\r\n"+
          "Cache-control: no-cache\r\n"+
          "Connection: close\r\n"+
@@ -97,11 +101,10 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
          "<html><head><title>403 Denied</title></head>\n"+
          "<body><h2>403 Denied</h2>\n" +
          "<p>Denied due to excessive requests. Please try again later.</p>\n" +
-         "</body></html>")
-         .getBytes();
+         "</body></html>";
 
-    private final static byte[] ERR_INPROXY =
-        ("HTTP/1.1 403 Denied\r\n"+
+    private final static String ERR_INPROXY =
+         "HTTP/1.1 403 Denied\r\n"+
          "Content-Type: text/html; charset=iso-8859-1\r\n"+
          "Cache-control: no-cache\r\n"+
          "Connection: close\r\n"+
@@ -110,8 +113,64 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
          "<html><head><title>403 Denied</title></head>\n"+
          "<body><h2>403 Denied</h2>\n" +
          "<p>Inproxy access denied. You must run <a href=\"https://geti2p.net/\">I2P</a> to access this site.</p>\n" +
-         "</body></html>")
-         .getBytes();
+         "</body></html>";
+
+    private final static String ERR_SSL =
+         "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 website is not configured for SSL.</p>\n" +
+         "</body></html>";
+
+    private final static String ERR_REQUEST_URI_TOO_LONG =
+         "HTTP/1.1 414 Request URI too long\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>414 Request URI Too Long</title></head>\n"+
+         "<body><h2>414 Request URI too long</h2>\n" +
+         "</body></html>";
+
+    private final static String ERR_HEADERS_TOO_LARGE =
+         "HTTP/1.1 431 Request header fields too large\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>431 Request Header Fields Too Large</title></head>\n"+
+         "<body><h2>431 Request header fields too large</h2>\n" +
+         "</body></html>";
+
+    private final static String ERR_REQUEST_TIMEOUT =
+         "HTTP/1.1 408 Request timeout\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>408 Request Timeout</title></head>\n"+
+         "<body><h2>408 Request timeout</h2>\n" +
+         "</body></html>";
+
+    private final static String ERR_BAD_REQUEST =
+         "HTTP/1.1 400 Bad Request\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>400 Bad Request</title></head>\n"+
+         "<body><h2>400 Bad request</h2>\n" +
+         "</body></html>";
+
 
     public I2PTunnelHTTPServer(InetAddress host, int port, String privData, String spoofHost, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) {
         super(host, port, privData, l, notifyThis, tunnel);
@@ -131,7 +190,6 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
     private void setupI2PTunnelHTTPServer(String spoofHost) {
         _spoofHost = (spoofHost != null && spoofHost.trim().length() > 0) ? spoofHost.trim() : null;
         getTunnel().getContext().statManager().createRateStat("i2ptunnel.httpserver.blockingHandleTime", "how long the blocking handle takes to complete", "I2PTunnel.HTTPServer", new long[] { 60*1000, 10*60*1000, 3*60*60*1000 });
-        getTunnel().getContext().statManager().createRateStat("i2ptunnel.httpNullWorkaround", "How often an http server works around a streaming lib or i2ptunnel bug", "I2PTunnel.HTTPServer", new long[] { 60*1000, 10*60*1000 });
     }
 
     @Override
@@ -208,13 +266,88 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
         //local is fast, so synchronously. Does not need that many
         //threads.
         try {
+            if (socket.getLocalPort() == 443) {
+                if (getTunnel().getClientOptions().getProperty("targetForPort.443") == null) {
+                    try {
+                        socket.getOutputStream().write(ERR_SSL.getBytes("UTF-8"));
+                    } catch (IOException ioe) {
+                    } finally {
+                        try {
+                            socket.close();
+                        } catch (IOException ioe) {}
+                    }
+                    return;
+                }
+                Socket s = getSocket(socket.getPeerDestination().calculateHash(), 443);
+                Runnable t = new I2PTunnelRunner(s, socket, slock, null, null,
+                                                 null, (I2PTunnelRunner.FailCallback) null);
+                _clientExecutor.execute(t);
+                return;
+            }
+
             long afterAccept = getTunnel().getContext().clock().now();
+
             // The headers _should_ be in the first packet, but
             // may not be, depending on the client-side options
 
             StringBuilder command = new StringBuilder(128);
-            Map<String, List<String>> headers = readHeaders(socket, null, command,
-                CLIENT_SKIPHEADERS, getTunnel().getContext());
+            Map<String, List<String>> headers;
+            try {
+                // catch specific exceptions thrown, to return a good
+                // error to the client
+                headers = readHeaders(socket, null, command,
+                                      CLIENT_SKIPHEADERS, getTunnel().getContext());
+            } catch (SocketTimeoutException ste) {
+                try {
+                    socket.getOutputStream().write(ERR_REQUEST_TIMEOUT.getBytes("UTF-8"));
+                } catch (IOException ioe) {
+                } finally {
+                     try { socket.close(); } catch (IOException ioe) {}
+                }
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Error while receiving the new HTTP request", ste);
+                return;
+            } catch (EOFException eofe) {
+                try {
+                    socket.getOutputStream().write(ERR_BAD_REQUEST.getBytes("UTF-8"));
+                } catch (IOException ioe) {
+                } finally {
+                     try { socket.close(); } catch (IOException ioe) {}
+                }
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Error while receiving the new HTTP request", eofe);
+                return;
+            } catch (LineTooLongException ltle) {
+                try {
+                    socket.getOutputStream().write(ERR_HEADERS_TOO_LARGE.getBytes("UTF-8"));
+                } catch (IOException ioe) {
+                } finally {
+                     try { socket.close(); } catch (IOException ioe) {}
+                }
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Error while receiving the new HTTP request", ltle);
+                return;
+            } catch (RequestTooLongException rtle) {
+                try {
+                    socket.getOutputStream().write(ERR_REQUEST_URI_TOO_LONG.getBytes("UTF-8"));
+                } catch (IOException ioe) {
+                } finally {
+                     try { socket.close(); } catch (IOException ioe) {}
+                }
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Error while receiving the new HTTP request", rtle);
+                return;
+            } catch (BadRequestException bre) {
+                try {
+                    socket.getOutputStream().write(ERR_BAD_REQUEST.getBytes("UTF-8"));
+                } catch (IOException ioe) {
+                } finally {
+                     try { socket.close(); } catch (IOException ioe) {}
+                }
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Error while receiving the new HTTP request", bre);
+                return;
+            }
             long afterHeaders = getTunnel().getContext().clock().now();
 
             Properties opts = getTunnel().getClientOptions();
@@ -239,7 +372,7 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
                 try {
                     // Send a 403, so the user doesn't get an HTTP Proxy error message
                     // and blame his router or the network.
-                    socket.getOutputStream().write(ERR_INPROXY);
+                    socket.getOutputStream().write(ERR_INPROXY.getBytes("UTF-8"));
                 } catch (IOException ioe) {}
                 try {
                     socket.close();
@@ -256,7 +389,7 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
                     try {
                         // Send a 403, so the user doesn't get an HTTP Proxy error message
                         // and blame his router or the network.
-                        socket.getOutputStream().write(ERR_DENIED);
+                        socket.getOutputStream().write(ERR_DENIED.getBytes("UTF-8"));
                     } catch (IOException ioe) {}
                     try {
                         socket.close();
@@ -341,7 +474,7 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
             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);
+                socket.getOutputStream().write(ERR_UNAVAILABLE.getBytes("UTF-8"));
             } catch (IOException ioe) {}
             try {
                 socket.close();
@@ -362,7 +495,7 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
             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);
+                socket.getOutputStream().write(ERR_UNAVAILABLE.getBytes("UTF-8"));
             } catch (IOException ioe) {}
             try {
                 socket.close();
@@ -453,7 +586,7 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
                 try {
                     if (browserout == null)
                         browserout = _browser.getOutputStream();
-                    browserout.write(ERR_UNAVAILABLE);
+                    browserout.write(ERR_UNAVAILABLE.getBytes("UTF-8"));
                 } catch (IOException ioe) {}
             } catch (IOException ioe) {
                 if (_log.shouldLog(Log.WARN))
@@ -633,9 +766,6 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
         return buf.toString();
     }
     
-    /** ridiculously long, just to prevent OOM DOS @since 0.7.13 */
-    private static final int MAX_HEADERS = 60;
-    
     /**
      * Add an entry to the multimap.
      */
@@ -680,10 +810,11 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
      *  @param socket if null, use in as InputStream
      *  @param in if null, use socket.getInputStream() as InputStream
      *  @param command out parameter, first line
-     *  @param command out parameter, first line
      *  @throws SocketTimeoutException if timeout is reached before newline
      *  @throws EOFException if EOF is reached before newline
-     *  @throws LineTooLongException if too long
+     *  @throws LineTooLongException if one header too long, or too many headers, or total size too big
+     *  @throws RequestTooLongException if too long
+     *  @throws BadRequestException on bad headers
      *  @throws IOException on other errors in the underlying stream
      */
     private static Map<String, List<String>> readHeaders(I2PSocket socket, InputStream in, StringBuilder command,
@@ -694,51 +825,49 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
         // slowloris / darkloris
         long expire = ctx.clock().now() + TOTAL_HEADER_TIMEOUT;
         if (socket != null) {
-            readLine(socket, command, HEADER_TIMEOUT);
+            try {
+                readLine(socket, command, HEADER_TIMEOUT);
+            } catch (LineTooLongException ltle) {
+                // convert for first line
+                throw new RequestTooLongException("Request too long - max " + MAX_LINE_LENGTH);
+            }
         } else {
              boolean ok = DataHelper.readLine(in, command);
              if (!ok)
-                 throw new IOException("EOF reached before the end of the headers [" + buf.toString() + "]");
+                 throw new EOFException("EOF reached before the end of the headers");
         }
         
         //if (_log.shouldLog(Log.DEBUG))
         //    _log.debug("Read the http command [" + command.toString() + "]");
         
-        // FIXME we probably don't need or want this in the outgoing direction
-        int trimmed = 0;
-        if (command.length() > 0) {
-            for (int i = 0; i < command.length(); i++) {
-                if (command.charAt(i) == 0) {
-                    command = command.deleteCharAt(i);
-                    i--;
-                    trimmed++;
-                }
-            }
-        }
-        if (trimmed > 0)
-            ctx.statManager().addRateData("i2ptunnel.httpNullWorkaround", trimmed);
-        
+        int totalSize = command.length();
         int i = 0;
         while (true) {
-            if (++i > MAX_HEADERS)
-                throw new IOException("Too many header lines - max " + MAX_HEADERS);
+            if (++i > MAX_HEADERS) {
+                throw new LineTooLongException("Too many header lines - max " + MAX_HEADERS);
+            }
             buf.setLength(0);
             if (socket != null) {
                 readLine(socket, buf, expire - ctx.clock().now());
             } else {
                  boolean ok = DataHelper.readLine(in, buf);
                  if (!ok)
-                     throw new IOException("EOF reached before the end of the headers [" + buf.toString() + "]");
+                     throw new BadRequestException("EOF reached before the end of the headers");
             }
             if ( (buf.length() == 0) || 
                  ((buf.charAt(0) == '\n') || (buf.charAt(0) == '\r')) ) {
                 // end of headers reached
                 return headers;
             } else {
-                if (ctx.clock().now() >= expire)
-                    throw new IOException("Headers took too long [" + buf.toString() + "]");
+                if (ctx.clock().now() > expire) {
+                    throw new SocketTimeoutException("Headers took too long");
+                }
                 int split = buf.indexOf(":");
-                if (split <= 0) throw new IOException("Invalid HTTP header, missing colon [" + buf.toString() + "]");
+                if (split <= 0)
+                    throw new BadRequestException("Invalid HTTP header, missing colon");
+                totalSize += buf.length();
+                if (totalSize > MAX_TOTAL_HEADER_SIZE)
+                    throw new LineTooLongException("Req+headers too big");
                 String name = buf.substring(0, split).trim();
                 String value = null;
                 if (buf.length() > split + 1)
@@ -831,5 +960,23 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
             super(s);
         }
     }
+
+    /**
+     *  @since 0.9.20
+     */
+    private static class RequestTooLongException extends IOException {
+        public RequestTooLongException(String s) {
+            super(s);
+        }
+    }
+
+    /**
+     *  @since 0.9.20
+     */
+    private static class BadRequestException extends IOException {
+        public BadRequestException(String s) {
+            super(s);
+        }
+    }
 }
 
diff --git a/apps/i2ptunnel/jsp/web.xml b/apps/i2ptunnel/jsp/web.xml
index 1fd11c13d8adb73bab7a400ef46d2a6cedc60a3c..0a74b68357862e51250b96e08bcf82b2db9e966a 100644
--- a/apps/i2ptunnel/jsp/web.xml
+++ b/apps/i2ptunnel/jsp/web.xml
@@ -32,10 +32,14 @@
         <url-pattern>/wizard</url-pattern>
     </servlet-mapping>
 
+    <!-- this webapp doesn't actually use sessions or cookies -->
     <session-config>
         <session-timeout>
             30
         </session-timeout>
+        <cookie-config>
+            <http-only>true</http-only>
+        </cookie-config>
     </session-config>
     <welcome-file-list>
         <welcome-file>index.html</welcome-file>
diff --git a/apps/routerconsole/jsp/web.xml b/apps/routerconsole/jsp/web.xml
index 67f0b4ca6e599a2aa4d0e5e2adb5f8eb97256f05..ea183c83564b125d4de28deb2d93f9492cb848e3 100644
--- a/apps/routerconsole/jsp/web.xml
+++ b/apps/routerconsole/jsp/web.xml
@@ -35,6 +35,9 @@
         <session-timeout>
             30
         </session-timeout>
+        <cookie-config>
+            <http-only>true</http-only>
+        </cookie-config>
     </session-config>
     <welcome-file-list>
         <welcome-file>index.html</welcome-file>
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/impl/Connection.java b/apps/streaming/java/src/net/i2p/client/streaming/impl/Connection.java
index 8c6212503f55472c8da027cfb1f8a3597e608c0e..451bb5f37c96197cb526f4a306e363192828de9c 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/impl/Connection.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/impl/Connection.java
@@ -41,7 +41,8 @@ class Connection {
     private final MessageInputStream _inputStream;
     private final MessageOutputStream _outputStream;
     private final SchedulerChooser _chooser;
-    private volatile long _nextSendTime;
+    /** Locking: _nextSendLock */
+    private long _nextSendTime;
     private long _ackedPackets;
     private final long _createdOn;
     private final AtomicLong _closeSentOn = new AtomicLong();
@@ -70,6 +71,8 @@ class Connection {
     private final AtomicBoolean _ackSinceCongestion;
     /** Notify this on connection (or connection failure) */
     private final Object _connectLock;
+    /** Locking for _nextSendTime */
+    private final Object _nextSendLock;
     /** how many messages have been resent and not yet ACKed? */
     private final AtomicInteger _activeResends = new AtomicInteger();
     private final ConEvent _connectionEvent;
@@ -145,6 +148,7 @@ class Connection {
         _activityTimer = new ActivityTimer();
         _ackSinceCongestion = new AtomicBoolean(true);
         _connectLock = new Object();
+        _nextSendLock = new Object();
         _connectionEvent = new ConEvent();
         _randomWait = _context.random().nextInt(10*1000); // just do this once to reduce usage
         // all createRateStats in ConnectionManager
@@ -907,7 +911,11 @@ class Connection {
      * instance, or want to delay an ACK.
      * @return the next time the scheduler will want to send a packet, or -1 if never.
      */
-    public long getNextSendTime() { return _nextSendTime; }
+    public long getNextSendTime() {
+        synchronized(_nextSendLock) {
+            return _nextSendTime;
+        }
+    }
 
     /**
      *  If the next send time is currently >= 0 (i.e. not "never"),
@@ -917,25 +925,20 @@ class Connection {
      *  options.getSendAckDelay() from now (1000 ms)
      */
     public void setNextSendTime(long when) { 
-        if (_nextSendTime >= 0) {
-            if (when < _nextSendTime)
-                _nextSendTime = when;
-        } else {
-            _nextSendTime = when; 
-        }
+        synchronized(_nextSendLock) {
+            if (_nextSendTime >= 0) {
+                if (when < _nextSendTime)
+                    _nextSendTime = when;
+            } else {
+                _nextSendTime = when; 
+            }
 
-        if (_nextSendTime >= 0) {
-            long max = _context.clock().now() + _options.getSendAckDelay();
-            if (max < _nextSendTime)
-                _nextSendTime = max;
+            if (_nextSendTime >= 0) {
+                long max = _context.clock().now() + _options.getSendAckDelay();
+                if (max < _nextSendTime)
+                    _nextSendTime = max;
+            }
         }
-        
-        //if (_log.shouldLog(Log.DEBUG) && false) {
-        //    if (_nextSendTime <= 0) 
-        //        _log.debug("set next send time to an unknown time", new Exception(toString()));
-        //    else
-        //        _log.debug("set next send time to " + (_nextSendTime-_context.clock().now()) + "ms from now", new Exception(toString()));
-        //}
     }
     
     /** how many packets have we sent and the other side has ACKed?
@@ -1260,17 +1263,17 @@ class Connection {
      */
     class ResendPacketEvent extends SimpleTimer2.TimedEvent {
         private final PacketLocal _packet;
-        private long _nextSendTime;
+        private long _nextSend;
 
         public ResendPacketEvent(PacketLocal packet, long delay) {
             super(_timer);
             _packet = packet;
-            _nextSendTime = delay + _context.clock().now();
+            _nextSend = delay + _context.clock().now();
             packet.setResendPacketEvent(ResendPacketEvent.this);
             schedule(delay);
         }
         
-        public long getNextSendTime() { return _nextSendTime; }
+        public long getNextSendTime() { return _nextSend; }
         public void timeReached() { retransmit(); }
         /**
          * Retransmit the packet if we need to.  
@@ -1320,7 +1323,7 @@ class Connection {
                                   + _activeResends + " active resend, "
                                   + _outboundPackets.size() + " unacked, window size = " + _options.getWindowSize());
                     forceReschedule(1333);
-                    _nextSendTime = 1333 + _context.clock().now();
+                    _nextSend = 1333 + _context.clock().now();
                     return false;
                 }
                 
@@ -1407,7 +1410,7 @@ class Connection {
                     if ( (timeout > MAX_RESEND_DELAY) || (timeout <= 0) )
                         timeout = MAX_RESEND_DELAY;
                     // set this before enqueue() as it passes it on to the router
-                    _nextSendTime = timeout + _context.clock().now();
+                    _nextSend = timeout + _context.clock().now();
 
                     if (_outboundQueue.enqueue(_packet)) {
                         // first resend for this packet ?
diff --git a/apps/susimail/src/WEB-INF/web.xml b/apps/susimail/src/WEB-INF/web.xml
index 23adde2d5b4eb77df38bfc366c1a640b85b25e30..22f6e9b6f086b9fa7b89473de7706660de888d16 100644
--- a/apps/susimail/src/WEB-INF/web.xml
+++ b/apps/susimail/src/WEB-INF/web.xml
@@ -23,6 +23,9 @@
   </servlet-mapping>
   <session-config>
     <session-timeout>1440</session-timeout>
+    <cookie-config>
+      <http-only>true</http-only>
+    </cookie-config>
   </session-config>
   <!-- tomcat (untested) -->
   <context-param>
diff --git a/core/java/src/net/i2p/util/EepGet.java b/core/java/src/net/i2p/util/EepGet.java
index 157a86a7139196220dddd912142f326c28c3a9e2..6a1c4230aa0ae3f56cd551c54b41aa0278504ccf 100644
--- a/core/java/src/net/i2p/util/EepGet.java
+++ b/core/java/src/net/i2p/util/EepGet.java
@@ -904,10 +904,14 @@ public class EepGet {
                 _keepFetching = false;
                 _notModified = true;
                 return; 
+            case 400: // bad req
             case 401: // server auth
             case 403: // bad req
             case 404: // not found
+            case 408: // req timeout
             case 409: // bad addr helper
+            case 414: // URI too long
+            case 431: // headers too long
             case 503: // no outproxy
                 _transferFailed = true;
                 if (_alreadyTransferred > 0 || !_shouldWriteErrorToOutput) {
diff --git a/installer/resources/eepsite/jetty-ssl.xml b/installer/resources/eepsite/jetty-ssl.xml
index c6d91cc83db728947563a057f3307e6698cd2e81..d27effe37b82227bccca1cade223e0fc5bcbfa8b 100644
--- a/installer/resources/eepsite/jetty-ssl.xml
+++ b/installer/resources/eepsite/jetty-ssl.xml
@@ -1,35 +1,255 @@
 <?xml version="1.0"?>
 <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
 
+<!-- ========================================================================= -->
+<!-- If you have a 'split' directory installation, with configuration          -->
+<!-- files in ~/.i2p (Linux) or %APPDATA%\I2P (Windows), be sure to            -->
+<!-- edit the file in the configuration directory, NOT the install directory.  -->
+<!-- When running as a Linux daemon, the configuration directory is            -->
+<!-- /var/lib/i2p and the install directory is /usr/share/i2p .                -->
+<!--                                                                           -->
+<!-- ========================================================================= -->
+
 <!-- =============================================================== -->
 <!-- Configure SSL for the Jetty Server                              -->
 <!-- this configuration file should be used in combination with      -->
-<!-- other configuration files.  e.g.                                -->
-<!--    java -jar start.jar etc/jetty-ssl.xml                        -->
+<!-- other configuration files.                                      -->
+<!--                                                                 -->
+<!-- =============================================================== -->
+<!-- Add a HTTPS SSL listener on port 7668                           -->
+<!--                                                                 -->
+<!-- NOTE:                                                           -->
+<!--                                                                 -->
+<!-- While I2P already encrypts end-to-end, HTTPS support            -->
+<!-- is valuable for authentication.                                 -->
+<!--                                                                 -->
+<!-- These instructions are to add SSL support to an existing        -->
+<!-- HTTP Jetty website.                                             -->
+<!--                                                                 -->
+<!-- For HTTPS ONLY, create a standard server tunnel                 -->
+<!-- (NOT HTTP server), and skip step 8.                             -->
+<!--                                                                 -->
+<!-- For non-Jetty servers (e.g. Apache), follow your server         -->
+<!-- instructions to generate and configure the certificates,        -->
+<!-- and skip steps 1-7.                                             -->
 <!--                                                                 -->
-<!--  alternately, add to the start.ini for easier usage             -->
 <!-- =============================================================== -->
+<!--                                                                 -->
+<!-- To add SSL support for your existing website:                   -->
+<!--                                                                 -->
+<!-- Step 1:                                                         -->
+<!-- Get the b32 for your wehsite, it's the link at the              -->
+<!-- "preview" button in the Hidden Services Manager in              -->
+<!-- the console. If you aren't running i2p, you can                 -->
+<!-- get it from your private key file                               -->
+<!-- (probably ~/.i2p/eepsite/eepPriv.dat)                           -->
+<!-- with the command:                                               -->
+<!--     java -cp ~/i2p/lib/i2p.jar net.i2p.data.PrivateKeyFile ~/.i2p/eepsite/eepPriv.dat  -->
+<!-- Save the b32 to put in the certificate's CN in Step 2.          -->
+<!--                                                                 -->
+<!--                                                                 -->
+<!-- Step 2:                                                         -->
+<!-- Generate selfsigned certificates.                               -->
+<!-- We recommend two: one for the hostname, and one for the b32.    -->
+<!-- Note that server-side SNI to serve the correct certificate      -->
+<!-- requires Java 8. Otherwise it will pick one.                    -->
+<!-- (at random? first one?)                                         -->
+<!-- Change the CN and key password in the example, of course.       -->
+<!-- It's OK to keep the keystore password as "changeit" if you like.  -->
+<!-- Use the same passwords for both certificates.                   -->
+<!-- See https://wiki.eclipse.org/Jetty/Howto/Configure_SSL          -->
+<!-- for alternate methods.                                          -->
+<!--
+   keytool -genkey -keystore ~/.i2p/eepsite/etc/keystore.ks -storepass changeit -alias b32 -dname CN=biglongkey.b32.i2p,OU=Eepsite,O=XX,L=XX,ST=XX,C=XX -validity 3652 -keyalg RSA -keysize 2048 -keypass myKeyPassword 
+   keytool -genkey -keystore ~/.i2p/eepsite/etc/keystore.ks -storepass changeit -alias hostname -dname CN=example.i2p,OU=Eepsite,O=XX,L=XX,ST=XX,C=XX -validity 3652 -keyalg RSA -keysize 2048 -keypass myKeyPassword 
+   chmod 600 ~/.i2p/eepsite/etc/keystore.ks
+ -->
+<!--                                                                 -->
+<!-- But does SNI work? see:                                         -->
+<!-- http://blog.ivanristic.com/2014/03/ssl-tls-improvements-in-java-8.html -->
+<!-- http://stackoverflow.com/questions/20887504/tls-extension-server-name-indication-sni-value-not-available-on-server-side -->
+<!--                                                                 -->
+<!-- And no, you can't get a real certificate for an i2p             -->
+<!-- address from a Certificate Authority, but someday               -->
+<!-- it may be possible. Here's how Tor did it:                      -->
+<!-- https://cabforum.org/2015/02/18/ballot-144-validation-rules-dot-onion-names/ -->
+<!--                                                                 -->
+<!--                                                                 -->
+<!-- Step 3:                                                         -->
+<!-- Update this configuration file.                                 -->
+<!-- Edit the KeyStorePassword, TrustStorePassword, and              -->
+<!-- KeyManagerPassword below to match the passwords from Step 2.    -->
+<!--                                                                 -->
+<!--                                                                 -->
+<!-- Step 4:                                                         -->
+<!-- If running I2P, stop the website Jetty on /configclients        -->
+<!-- in the console.                                                 -->
+<!--                                                                 -->
+<!--                                                                 -->
+<!-- Step 5:                                                         -->
+<!-- Configure Jetty to read in this file at startup.                -->
+<!-- If running I2P, edit the website Jetty on /configclients        -->
+<!-- to add the argument "/path/to/.i2p/eepsite/jetty-ssl.xml".      -->
+<!--                                                                 -->
+<!-- If I2P is not running, edit the file ~/.i2p/clients.config      -->
+<!-- to add the argument "/path/to/.i2p/eepsite/jetty-ssl.xml"       -->
+<!-- at the end of the line:                                         -->
+<--    clientApp.3.args="eepsite/jetty.xml"                          -->
+<!-- so it now looks like:                                           -->
+<--    clientApp.3.args="/path to/.i2p/eepsite/jetty.xml" "/path/to/.i2p/eepsite/jetty-ssl.xml" -->
+<!--                                                                 -->
+<!--                                                                 -->
+<!-- Step 6:                                                         -->
+<!-- Start Jetty.                                                    -->
+<!-- If running I2P, start the website Jetty on /configclients       -->
+<!-- in the console.                                                 -->
+<!-- If I2P is not running, start it.                                -->
+<!--                                                                 -->
+<!-- Now go to the /logs page in the console and check for errors    -->
+<!-- in both the router and wrapper logs.                            -->
+<!--                                                                 -->
+<!--                                                                 -->
+<!-- Step 7:                                                         -->
+<!-- Test Jetty.                                                     -->
+<!-- If there were no errors, test your Jetty SSL by                 -->
+<!-- going to https://127.0.0.1:7668/ in your browser.               -->
+<!-- You will have to confirm the security exception for             -->
+<!-- the selfsigned certificate.                                     -->
+<!--                                                                 -->
+<!--                                                                 -->
+<!-- Step 8:                                                         -->
+<!-- Configure i2ptunnel.                                            -->
+<!-- Tell i2ptunnel to route SSL to port 7668 by adding the          -->
+<!-- following custom option on the i2ptunnel edit page              -->
+<!-- for your website:                                               -->
+<!--        targetForPort.443=127.0.0.1:7668                         -->
+<!-- Also, verify that "Use SSL" near the top is NOT set.            -->
+<!-- That would be SSL-over-SSL, which won't work.                   -->
+<!--                                                                 -->
+<!--                                                                 -->
+<!-- Step 9:                                                         -->
+<!-- Start the tunnel if it isn't started.                           -->
+<!--                                                                 -->
+<!--                                                                 -->
+<!-- Step 10:                                                        -->
+<!-- In the i2ptunnel HTTP Client configuration,                     -->
+<!-- enable "Allow SSL to I2P addresses" if it isn't already.        -->
+<!--                                                                 -->
+<!--                                                                 -->
+<!-- Step 11:                                                        -->
+<!-- Test SSL via i2ptunnel.                                         -->
+<!-- Test SSL to your website through I2P by entering                -->
+<!-- https://yoursite.i2p/ in your browser.                          -->
+<!-- If it doesn't work, check the /logs page in the console.        -->
+<!-- You may need to adjust your browser proxy settings to           -->
+<!-- ensure that https i2p URLs are fetched through the I2P proxy.   -->
+<!-- For example, in privoxy, add                                    -->
+<!-- https://*.i2p/* and https://*.i2p:*/*                           -->
+<!--                                                                 -->
+<!--                                                                 -->
+<!-- Step 12:                                                        -->
+<!-- Tell your users.                                                -->
+<!-- Put a link to the https version on your                         -->
+<!-- home page. Remind them that in                                  -->
+<!-- the i2ptunnel HTTP Client configuration,                        -->
+<!-- enable "Allow SSL to I2P addresses" if it isn't already.        -->
+<!-- Remind them to confirm the security exception for               -->
+<!-- the selfsigned certificate (but not one for a hostname          -->
+<!-- mismatch) (but see SNI issues above).                           -->
+<!-- Users may need to adjust their browser proxy settings to        -->
+<!-- ensure that https i2p URLs are fetched through the I2P proxy.   -->
+<!-- For example, in privoxy, add                                    -->
+<!-- https://*.i2p/* and https://*.i2p:*/*                           -->
+<!--                                                                 -->
+<!-- Decide what link to use. The hostname is not secure,            -->
+<!-- as users may have a different hostname in their browser.        -->
+<!-- Also, new address helpers won't work with SSL.                  -->
+<!-- The b32 is the recommended hostname.                            -->
+<!--                                                                 -->
+<!--                                                                 -->
+<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
+
 <Configure id="Server" class="org.eclipse.jetty.server.Server">
 
   <!-- if NIO is not available, use org.eclipse.jetty.server.ssl.SslSocketConnector -->
   
   <New id="sslContextFactory" class="org.eclipse.jetty.http.ssl.SslContextFactory">
-    <Set name="KeyStore">./eepsite/etc/keystore</Set>
-    <Set name="KeyStorePassword">OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4</Set>
-    <Set name="KeyManagerPassword">OBF:1u2u1wml1z7s1z7a1wnl1u2g</Set>
-    <Set name="TrustStore">./eepsite/etc/keystore</Set>
-    <Set name="TrustStorePassword">OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4</Set>
+    <Set name="KeyStore">./eepsite/etc/keystore.ks</Set>
+    <Set name="KeyStorePassword">changeit</Set>
+    <Set name="KeyManagerPassword">myKeyPassword</Set>
+    <Set name="TrustStore">./eepsite/etc/keystore.ks</Set>
+    <Set name="TrustStorePassword">changeit</Set>
   </New>
 
   <Call name="addConnector">
     <Arg>
       <New class="org.eclipse.jetty.server.ssl.SslSelectChannelConnector">
         <Arg><Ref id="sslContextFactory" /></Arg>
-        <Set name="Port">8443</Set>
+        <Set name="host">127.0.0.1</Set>
+        <Set name="port">7668</Set>
         <Set name="maxIdleTime">600000</Set>
         <Set name="useDirectBuffers">false</Set>
-        <Set name="Acceptors">2</Set>
-        <Set name="AcceptQueueSize">100</Set>
+        <Set name="acceptors">1</Set>
+        <Set name="statsOn">false</Set>
+        <Set name="lowResourcesConnections">5000</Set>
+        <Set name="lowResourcesMaxIdleTime">5000</Set>
+        <Set name="ExcludeCipherSuites">
+          <Array type="java.lang.String">
+            <Item>SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA</Item>
+            <Item>SSL_DH_anon_EXPORT_WITH_RC4_40_MD5</Item>
+            <Item>SSL_DH_anon_WITH_3DES_EDE_CBC_SHA</Item>
+            <Item>SSL_DH_anon_WITH_DES_CBC_SHA</Item>
+            <Item>SSL_DH_anon_WITH_RC4_128_MD5</Item>
+            <Item>SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA</Item>
+            <Item>SSL_DHE_DSS_WITH_DES_CBC_SHA</Item>
+            <Item>SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA</Item>
+            <Item>SSL_DHE_RSA_WITH_DES_CBC_SHA</Item>
+            <Item>SSL_RSA_EXPORT_WITH_DES40_CBC_SHA</Item>
+            <Item>SSL_RSA_EXPORT_WITH_RC4_40_MD5</Item>
+            <Item>SSL_RSA_WITH_DES_CBC_SHA</Item>
+            <Item>SSL_RSA_WITH_NULL_MD5</Item>
+            <Item>SSL_RSA_WITH_NULL_SHA</Item>
+            <Item>TLS_DH_anon_WITH_AES_128_CBC_SHA</Item>
+            <Item>TLS_DH_anon_WITH_AES_128_CBC_SHA256</Item>
+            <Item>TLS_DH_anon_WITH_AES_128_GCM_SHA256</Item>
+            <Item>TLS_DH_anon_WITH_AES_256_CBC_SHA</Item>
+            <Item>TLS_DH_anon_WITH_AES_256_CBC_SHA256</Item>
+            <Item>TLS_DH_anon_WITH_AES_256_GCM_SHA384</Item>
+            <Item>TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA</Item>
+            <Item>TLS_ECDH_anon_WITH_AES_128_CBC_SHA</Item>
+            <Item>TLS_ECDH_anon_WITH_AES_256_CBC_SHA</Item>
+            <Item>TLS_ECDH_anon_WITH_NULL_SHA</Item>
+            <Item>TLS_ECDH_anon_WITH_RC4_128_SHA</Item>
+            <Item>TLS_ECDH_ECDSA_WITH_NULL_SHA</Item>
+            <Item>TLS_ECDHE_ECDSA_WITH_NULL_SHA</Item>
+            <Item>TLS_ECDHE_RSA_WITH_NULL_SHA</Item>
+            <Item>TLS_ECDH_RSA_WITH_NULL_SHA</Item>
+            <Item>TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5</Item>
+            <Item>TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA</Item>
+            <Item>TLS_KRB5_EXPORT_WITH_RC4_40_MD5</Item>
+            <Item>TLS_KRB5_EXPORT_WITH_RC4_40_SHA</Item>
+            <Item>TLS_KRB5_WITH_3DES_EDE_CBC_MD5</Item>
+            <Item>TLS_KRB5_WITH_3DES_EDE_CBC_SHA</Item>
+            <Item>TLS_KRB5_WITH_DES_CBC_MD5</Item>
+            <Item>TLS_KRB5_WITH_DES_CBC_SHA</Item>
+            <Item>TLS_KRB5_WITH_RC4_128_MD5</Item>
+            <Item>TLS_KRB5_WITH_RC4_128_SHA</Item>
+            <Item>TLS_RSA_WITH_NULL_SHA256</Item>
+            <Item>SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA</Item>
+            <Item>SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA</Item>
+            <Item>SSL_RSA_WITH_3DES_EDE_CBC_SHA</Item>
+            <Item>SSL_RSA_WITH_RC4_128_MD5</Item>
+            <Item>SSL_RSA_WITH_RC4_128_SHA</Item>
+            <Item>TLS_ECDH_ECDSA_WITH_RC4_128_SHA</Item>
+            <Item>TLS_ECDH_RSA_WITH_RC4_128_SHA</Item>
+            <Item>TLS_ECDHE_ECDSA_WITH_RC4_128_SHA</Item>
+            <Item>TLS_ECDHE_RSA_WITH_RC4_128_SHA</Item>
+            <Item>TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA</Item>
+            <Item>TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA</Item>
+            <Item>TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA</Item>
+            <Item>TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA</Item>
+          </Array>
+        </Set>
       </New>
     </Arg>
   </Call>
diff --git a/installer/resources/eepsite/jetty.xml b/installer/resources/eepsite/jetty.xml
index b52445953136e2fac897dd22ec1b107547eb4311..432bb347e3b5bb7e7cc10602fda52381bc556987 100644
--- a/installer/resources/eepsite/jetty.xml
+++ b/installer/resources/eepsite/jetty.xml
@@ -135,7 +135,6 @@
             <Set name="maxIdleTime">600000</Set>
             <Set name="Acceptors">1</Set>
             <Set name="statsOn">false</Set>
-            <Set name="confidentialPort">8443</Set>
 	    <Set name="lowResourcesConnections">5000</Set>
 	    <Set name="lowResourcesMaxIdleTime">5000</Set>
 	    <Set name="useDirectBuffers">false</Set>
@@ -155,7 +154,6 @@
             <Set name="maxIdleTime">600000</Set>
             <Set name="Acceptors">1</Set>
             <Set name="statsOn">false</Set>
-            <Set name="confidentialPort">8443</Set>
           </New>
       </Arg>
     </Call>
@@ -168,19 +166,6 @@
     <!--                                                                 -->
     <!--   clientApp3.args=etc/jetty.xml etc/jetty-ssl.xml               -->
     <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
-  <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
-  <!-- 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/etc/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.                           -->
-  <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
 
     <!-- =========================================================== -->
     <!-- Set up global session ID manager                            -->
diff --git a/installer/resources/i2ptunnel.config b/installer/resources/i2ptunnel.config
index 0f5092cc0aa261ab3cdfec4147b197e483567bcb..944def78d99d13aceada38f4f0c3fad9810cf2ba 100644
--- a/installer/resources/i2ptunnel.config
+++ b/installer/resources/i2ptunnel.config
@@ -98,6 +98,8 @@ tunnel.3.option.inbound.length=3
 tunnel.3.option.inbound.lengthVariance=0
 tunnel.3.option.outbound.length=3
 tunnel.3.option.outbound.lengthVariance=0
+# uncomment for HTTPS to port 7668
+#tunnel.3.option.targetForPort.443=127.0.0.1:7668
 tunnel.3.startOnLoad=false
 
 # postman's SMTP server - see hq.postman.i2p
diff --git a/router/java/src/net/i2p/router/JobQueue.java b/router/java/src/net/i2p/router/JobQueue.java
index 42e88ca2cd1ef0203f2cfcef536ea25a5b63360c..0b0f84fd27f1633f016b7614a7cc874d56c8125f 100644
--- a/router/java/src/net/i2p/router/JobQueue.java
+++ b/router/java/src/net/i2p/router/JobQueue.java
@@ -75,7 +75,7 @@ public class JobQueue {
 
     /** default max # job queue runners operating */
     private final static int DEFAULT_MAX_RUNNERS = 1;
-    /** router.config parameter to override the max runners @deprecated unimplemented */
+    /** router.config parameter to override the max runners */
     private final static String PROP_MAX_RUNNERS = "router.maxJobRunners";
     
     /** how frequently should we check and update the max runners */
@@ -330,7 +330,7 @@ public class JobQueue {
     
     public void allowParallelOperation() { 
         _allowParallelOperation = true; 
-        runQueue(RUNNERS);
+        runQueue(_context.getProperty(PROP_MAX_RUNNERS, RUNNERS));
     }
     
     /** 
diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java
index 524836cf3f2b04472b9556350f7a5d31be2fe8a8..3a244d715acf8a1d16677a1b45567657dc26f19b 100644
--- a/router/java/src/net/i2p/router/Router.java
+++ b/router/java/src/net/i2p/router/Router.java
@@ -8,15 +8,10 @@ package net.i2p.router;
  *
  */
 
-import java.io.BufferedReader;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.InputStreamReader;
-import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
@@ -49,7 +44,6 @@ import net.i2p.router.util.EventLog;
 import net.i2p.stat.RateStat;
 import net.i2p.stat.StatManager;
 import net.i2p.util.ByteCache;
-import net.i2p.util.FileUtil;
 import net.i2p.util.FortunaRandomSource;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.I2PThread;
@@ -113,6 +107,7 @@ public class Router implements RouterClock.ClockShiftListener {
     private final static String DNS_CACHE_TIME = "" + (5*60);
     private static final String EVENTLOG = "eventlog.txt";
     private static final String PROP_JBIGI = "jbigi.loadedResource";
+    public static final String UPDATE_FILE = "i2pupdate.zip";
         
     private static final String originalTimeZoneID;
     static {
@@ -1497,199 +1492,11 @@ public class Router implements RouterClock.ClockShiftListener {
             // If it does an update, it never returns.
             // I guess it's better to have the other-router check above this, we don't want to
             // overwrite an existing running router's jar files. Other than ours.
-            r.installUpdates();
+            InstallUpdate.installUpdates(r);
             // *********  Start no threads before here ********* //
             r.runRouter();
         }
     }
-    
-    public static final String UPDATE_FILE = "i2pupdate.zip";
-    private static final String DELETE_FILE = "deletelist.txt";
-    
-    /**
-     * Context must be available.
-     * Unzip update file found in the router dir OR base dir, to the base dir
-     *
-     * If successful, will call exit() and never return.
-     *
-     * If we can't write to the base dir, complain.
-     * Note: _log not available here.
-     */
-    private void installUpdates() {
-        File updateFile = new File(_context.getRouterDir(), UPDATE_FILE);
-        boolean exists = updateFile.exists();
-        if (!exists) {
-            updateFile = new File(_context.getBaseDir(), UPDATE_FILE);
-            exists = updateFile.exists();
-        }
-        if (exists) {
-            // do a simple permissions test, if it fails leave the file in place and don't restart
-            File test = new File(_context.getBaseDir(), "history.txt");
-            if ((test.exists() && !test.canWrite()) || (!_context.getBaseDir().canWrite())) {
-                System.out.println("ERROR: No write permissions on " + _context.getBaseDir() +
-                                   " to extract software update file");
-                // carry on
-                return;
-            }
-            System.out.println("INFO: Update file exists [" + UPDATE_FILE + "] - installing");
-            // verify the whole thing first
-            // we could remember this fails, and not bother restarting, but who cares...
-            boolean ok = FileUtil.verifyZip(updateFile);
-            if (ok) {
-                // This may be useful someday. First added in 0.8.2
-                // Moved above the extract so we don't NCDFE
-                _config.put("router.updateLastInstalled", "" + System.currentTimeMillis());
-                // Set the last version to the current version, since 0.8.13
-                _config.put("router.previousVersion", RouterVersion.VERSION);
-                _config.put("router.previousFullVersion", RouterVersion.FULL_VERSION);
-                saveConfig();
-                ok = FileUtil.extractZip(updateFile, _context.getBaseDir());
-            }
-
-            // Very important - we have now trashed our jars.
-            // After this point, do not use any new I2P classes, or they will fail to load
-            // and we will die with NCDFE.
-            // Ideally, do not use I2P classes at all, new or not.
-            try {
-                if (ok) {
-                    // We do this here so we may delete old jars before we restart
-                    deleteListedFiles();
-                    System.out.println("INFO: Update installed");
-                } else {
-                    System.out.println("ERROR: Update failed!");
-                }
-                if (!ok) {
-                    // we can't leave the file in place or we'll continually restart, so rename it
-                    File bad = new File(_context.getRouterDir(), "BAD-" + UPDATE_FILE);
-                    boolean renamed = updateFile.renameTo(bad);
-                    if (renamed) {
-                        System.out.println("Moved update file to " + bad.getAbsolutePath());
-                    } else {
-                        System.out.println("Deleting file " + updateFile.getAbsolutePath());
-                        ok = true;  // so it will be deleted
-                    }
-                }
-                if (ok) {
-                    boolean deleted = updateFile.delete();
-                    if (!deleted) {
-                        System.out.println("ERROR: Unable to delete the update file!");
-                        updateFile.deleteOnExit();
-                    }
-                }
-                // exit whether ok or not
-                if (_context.hasWrapper())
-                    System.out.println("INFO: Restarting after update");
-                else
-                    System.out.println("WARNING: Exiting after update, restart I2P");
-            } catch (Throwable t) {
-                // hide the NCDFE
-                // hopefully the update file got deleted or we will loop
-            }
-            System.exit(EXIT_HARD_RESTART);
-        } else {
-            deleteJbigiFiles();
-            // It was here starting in 0.8.12 so it could be used the very first time
-            // Now moved up so it is usually run only after an update
-            // But the first time before jetty 6 it will run here...
-            // Here we can't remove jars
-            deleteListedFiles();
-        }
-    }
-
-    /**
-     *  Remove extracted libjbigi.so and libjcpuid.so files if we have a newer jbigi.jar,
-     *  so the new ones will be extracted.
-     *  We do this after the restart, not after the extract, because it's safer, and
-     *  because people may upgrade their jbigi.jar file manually.
-     *
-     *  Copied from NativeBigInteger, which we can't access here or the
-     *  libs will get loaded.
-     */
-    private void deleteJbigiFiles() {
-            boolean isX86 = SystemVersion.isX86();
-            String osName = System.getProperty("os.name").toLowerCase(Locale.US);
-            boolean isWin = SystemVersion.isWindows();
-            boolean isMac = SystemVersion.isMac();
-            // only do this on these OSes
-            boolean goodOS = isWin || isMac ||
-                             osName.contains("linux") || osName.contains("freebsd");
-
-            // only do this on these x86
-            File jbigiJar = new File(_context.getBaseDir(), "lib/jbigi.jar");
-            if (isX86 && goodOS && jbigiJar.exists()) {
-                String libPrefix = (isWin ? "" : "lib");
-                String libSuffix = (isWin ? ".dll" : isMac ? ".jnilib" : ".so");
-
-                File jcpuidLib = new File(_context.getBaseDir(), libPrefix + "jcpuid" + libSuffix);
-                if (jcpuidLib.canWrite() && jbigiJar.lastModified() > jcpuidLib.lastModified()) {
-                    String path = jcpuidLib.getAbsolutePath();
-                    boolean success = FileUtil.copy(path, path + ".bak", true, true);
-                    if (success) {
-                        boolean success2 = jcpuidLib.delete();
-                        if (success2) {
-                            System.out.println("New jbigi.jar detected, moved jcpuid library to " +
-                                               path + ".bak");
-                            System.out.println("Check logs for successful installation of new library");
-                        }
-                    }
-                }
-
-                File jbigiLib = new File(_context.getBaseDir(), libPrefix + "jbigi" + libSuffix);
-                if (jbigiLib.canWrite() && jbigiJar.lastModified() > jbigiLib.lastModified()) {
-                    String path = jbigiLib.getAbsolutePath();
-                    boolean success = FileUtil.copy(path, path + ".bak", true, true);
-                    if (success) {
-                        boolean success2 = jbigiLib.delete();
-                        if (success2) {
-                            System.out.println("New jbigi.jar detected, moved jbigi library to " +
-                                               path + ".bak");
-                            System.out.println("Check logs for successful installation of new library");
-                        }
-                    }
-                }
-            }
-    }
-    
-    /**
-     *  Delete all files listed in the delete file.
-     *  Format: One file name per line, comment lines start with '#'.
-     *  All file names must be relative to $I2P, absolute file names not allowed.
-     *  We probably can't remove old jars this way.
-     *  Fails silently.
-     *  Use no new I2P classes here so it may be called after zip extraction.
-     *  @since 0.8.12
-     */
-    private void deleteListedFiles() {
-        File deleteFile = new File(_context.getBaseDir(), DELETE_FILE);
-        if (!deleteFile.exists())
-            return;
-        // this is similar to FileUtil.readTextFile() but we can't use any I2P classes here
-        FileInputStream fis = null;
-        BufferedReader in = null;
-        try {
-            fis = new FileInputStream(deleteFile);
-            in = new BufferedReader(new InputStreamReader(fis, "UTF-8"));
-            String line;
-            while ( (line = in.readLine()) != null) {
-                String fl = line.trim();
-                if (fl.contains("..") || fl.startsWith("#") || fl.length() == 0)
-                    continue;
-                File df = new File(fl);
-                if (df.isAbsolute())
-                    continue;
-                df = new File(_context.getBaseDir(), fl);
-                if (df.exists() && !df.isDirectory()) {
-                    if (df.delete())
-                        System.out.println("INFO: File [" + fl + "] deleted");
-                }
-            }
-        } catch (IOException ioe) {}
-        finally {
-            if (in != null) try { in.close(); } catch(IOException ioe) {}
-            if (deleteFile.delete())
-                System.out.println("INFO: File [" + DELETE_FILE + "] deleted");
-        }
-    }
 
 /*******
     private static void verifyWrapperConfig() {
diff --git a/router/java/src/net/i2p/router/tasks/InstallUpdate.java b/router/java/src/net/i2p/router/tasks/InstallUpdate.java
new file mode 100644
index 0000000000000000000000000000000000000000..b4bea6770669175e48e2ef59a18a66c858b16cea
--- /dev/null
+++ b/router/java/src/net/i2p/router/tasks/InstallUpdate.java
@@ -0,0 +1,215 @@
+package net.i2p.router.tasks;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import net.i2p.router.Router;
+import net.i2p.router.RouterContext;
+import net.i2p.router.RouterVersion;
+import net.i2p.util.FileUtil;
+import net.i2p.util.SystemVersion;
+
+/**
+ *  If the i2pupdate.zip file is present,
+ *  unzip it and JVM exit.
+ *
+ *  @since 0.9.20 moved from Router.java
+ */
+public class InstallUpdate {
+
+    private static final String DELETE_FILE = "deletelist.txt";
+    
+    /**
+     * Context must be available.
+     * Unzip update file found in the router dir OR base dir, to the base dir
+     *
+     * If successful, will call exit() and never return.
+     *
+     * If we can't write to the base dir, write message to System.out and return.
+     * Note: _log not available here.
+     */
+    public static void installUpdates(Router r) {
+        RouterContext context = r.getContext();
+        File updateFile = new File(context.getRouterDir(), Router.UPDATE_FILE);
+        boolean exists = updateFile.exists();
+        if (!exists) {
+            updateFile = new File(context.getBaseDir(), Router.UPDATE_FILE);
+            exists = updateFile.exists();
+        }
+        if (exists) {
+            // do a simple permissions test, if it fails leave the file in place and don't restart
+            File test = new File(context.getBaseDir(), "history.txt");
+            if ((test.exists() && !test.canWrite()) || (!context.getBaseDir().canWrite())) {
+                System.out.println("ERROR: No write permissions on " + context.getBaseDir() +
+                                   " to extract software update file");
+                // carry on
+                return;
+            }
+            System.out.println("INFO: Update file exists [" + Router.UPDATE_FILE + "] - installing");
+            // verify the whole thing first
+            // we could remember this fails, and not bother restarting, but who cares...
+            boolean ok = FileUtil.verifyZip(updateFile);
+            if (ok) {
+                // This may be useful someday. First added in 0.8.2
+                // Moved above the extract so we don't NCDFE
+                Map<String, String> config = new HashMap<String, String>(4);
+                config.put("router.updateLastInstalled", "" + System.currentTimeMillis());
+                // Set the last version to the current version, since 0.8.13
+                config.put("router.previousVersion", RouterVersion.VERSION);
+                config.put("router.previousFullVersion", RouterVersion.FULL_VERSION);
+                r.saveConfig(config, null);
+                ok = FileUtil.extractZip(updateFile, context.getBaseDir());
+            }
+
+            // Very important - we have now trashed our jars.
+            // After this point, do not use any new I2P classes, or they will fail to load
+            // and we will die with NCDFE.
+            // Ideally, do not use I2P classes at all, new or not.
+            try {
+                if (ok) {
+                    // We do this here so we may delete old jars before we restart
+                    deleteListedFiles(context);
+                    System.out.println("INFO: Update installed");
+                } else {
+                    System.out.println("ERROR: Update failed!");
+                }
+                if (!ok) {
+                    // we can't leave the file in place or we'll continually restart, so rename it
+                    File bad = new File(context.getRouterDir(), "BAD-" + Router.UPDATE_FILE);
+                    boolean renamed = updateFile.renameTo(bad);
+                    if (renamed) {
+                        System.out.println("Moved update file to " + bad.getAbsolutePath());
+                    } else {
+                        System.out.println("Deleting file " + updateFile.getAbsolutePath());
+                        ok = true;  // so it will be deleted
+                    }
+                }
+                if (ok) {
+                    boolean deleted = updateFile.delete();
+                    if (!deleted) {
+                        System.out.println("ERROR: Unable to delete the update file!");
+                        updateFile.deleteOnExit();
+                    }
+                }
+                // exit whether ok or not
+                if (context.hasWrapper())
+                    System.out.println("INFO: Restarting after update");
+                else
+                    System.out.println("WARNING: Exiting after update, restart I2P");
+            } catch (Throwable t) {
+                // hide the NCDFE
+                // hopefully the update file got deleted or we will loop
+            }
+            System.exit(Router.EXIT_HARD_RESTART);
+        } else {
+            deleteJbigiFiles(context);
+            // It was here starting in 0.8.12 so it could be used the very first time
+            // Now moved up so it is usually run only after an update
+            // But the first time before jetty 6 it will run here...
+            // Here we can't remove jars
+            deleteListedFiles(context);
+        }
+    }
+
+    /**
+     *  Remove extracted libjbigi.so and libjcpuid.so files if we have a newer jbigi.jar,
+     *  so the new ones will be extracted.
+     *  We do this after the restart, not after the extract, because it's safer, and
+     *  because people may upgrade their jbigi.jar file manually.
+     *
+     *  Copied from NativeBigInteger, which we can't access here or the
+     *  libs will get loaded.
+     */
+    private static void deleteJbigiFiles(RouterContext context) {
+        boolean isX86 = SystemVersion.isX86();
+        String osName = System.getProperty("os.name").toLowerCase(Locale.US);
+        boolean isWin = SystemVersion.isWindows();
+        boolean isMac = SystemVersion.isMac();
+        // only do this on these OSes
+        boolean goodOS = isWin || isMac ||
+                         osName.contains("linux") || osName.contains("freebsd");
+
+        // only do this on these x86
+        File jbigiJar = new File(context.getBaseDir(), "lib/jbigi.jar");
+        if (isX86 && goodOS && jbigiJar.exists()) {
+            String libPrefix = (isWin ? "" : "lib");
+            String libSuffix = (isWin ? ".dll" : isMac ? ".jnilib" : ".so");
+
+            File jcpuidLib = new File(context.getBaseDir(), libPrefix + "jcpuid" + libSuffix);
+            if (jcpuidLib.canWrite() && jbigiJar.lastModified() > jcpuidLib.lastModified()) {
+                String path = jcpuidLib.getAbsolutePath();
+                boolean success = FileUtil.copy(path, path + ".bak", true, true);
+                if (success) {
+                    boolean success2 = jcpuidLib.delete();
+                    if (success2) {
+                        System.out.println("New jbigi.jar detected, moved jcpuid library to " +
+                                           path + ".bak");
+                        System.out.println("Check logs for successful installation of new library");
+                    }
+                }
+            }
+
+            File jbigiLib = new File(context.getBaseDir(), libPrefix + "jbigi" + libSuffix);
+            if (jbigiLib.canWrite() && jbigiJar.lastModified() > jbigiLib.lastModified()) {
+                String path = jbigiLib.getAbsolutePath();
+                boolean success = FileUtil.copy(path, path + ".bak", true, true);
+                if (success) {
+                    boolean success2 = jbigiLib.delete();
+                    if (success2) {
+                        System.out.println("New jbigi.jar detected, moved jbigi library to " +
+                                           path + ".bak");
+                        System.out.println("Check logs for successful installation of new library");
+                    }
+                }
+            }
+        }
+    }
+    
+    /**
+     *  Delete all files listed in the delete file.
+     *  Format: One file name per line, comment lines start with '#'.
+     *  All file names must be relative to $I2P, absolute file names not allowed.
+     *  We probably can't remove old jars this way.
+     *  Fails silently.
+     *  Use no new I2P classes here so it may be called after zip extraction.
+     *  @since 0.8.12
+     */
+    private static void deleteListedFiles(RouterContext context) {
+        File deleteFile = new File(context.getBaseDir(), DELETE_FILE);
+        if (!deleteFile.exists())
+            return;
+        // this is similar to FileUtil.readTextFile() but we can't use any I2P classes here
+        FileInputStream fis = null;
+        BufferedReader in = null;
+        try {
+            fis = new FileInputStream(deleteFile);
+            in = new BufferedReader(new InputStreamReader(fis, "UTF-8"));
+            String line;
+            while ( (line = in.readLine()) != null) {
+                String fl = line.trim();
+                if (fl.contains("..") || fl.startsWith("#") || fl.length() == 0)
+                    continue;
+                File df = new File(fl);
+                if (df.isAbsolute())
+                    continue;
+                df = new File(context.getBaseDir(), fl);
+                if (df.exists() && !df.isDirectory()) {
+                    if (df.delete())
+                        System.out.println("INFO: File [" + fl + "] deleted");
+                }
+            }
+        } catch (IOException ioe) {}
+        finally {
+            if (in != null) try { in.close(); } catch(IOException ioe) {}
+            if (deleteFile.delete())
+                System.out.println("INFO: File [" + DELETE_FILE + "] deleted");
+        }
+    }
+}
+
diff --git a/router/java/src/net/i2p/router/tasks/package.html b/router/java/src/net/i2p/router/tasks/package.html
index e032edd6cbcbf50df121d685badfea90f4aff897..bc1a92730840d4a233751722629160ec4f1d7f60 100644
--- a/router/java/src/net/i2p/router/tasks/package.html
+++ b/router/java/src/net/i2p/router/tasks/package.html
@@ -4,6 +4,7 @@
 Miscellaneous classes, mostly things that are executed periodically as
 Jobs, Threads, and SimpleTimer.TimedEvents.
 These are used only by Router.java.
+Nothing here is to be used externally, not a part of the public API.
 </p>
 </body>
 </html>
diff --git a/router/java/src/net/i2p/router/transport/FIFOBandwidthRefiller.java b/router/java/src/net/i2p/router/transport/FIFOBandwidthRefiller.java
index 3dc610b7813905e4b30623881cc764396f985822..190c3b6416078782733e2299f7caf659f95a406b 100644
--- a/router/java/src/net/i2p/router/transport/FIFOBandwidthRefiller.java
+++ b/router/java/src/net/i2p/router/transport/FIFOBandwidthRefiller.java
@@ -70,8 +70,12 @@ public class FIFOBandwidthRefiller implements Runnable {
     public static final int MIN_INBOUND_BANDWIDTH_PEAK = 3;
     /** For now, until there is some tuning and safe throttling, we set the floor at a 3KBps during burst */
     public static final int MIN_OUTBOUND_BANDWIDTH_PEAK = 3;
-    /** Max for reasonable bloom filter false positive rate. See util/DecayingBloomFilter and tunnel/BloomFilterIVValidator */
-    public static final int MAX_OUTBOUND_BANDWIDTH = 4096;
+    /**
+     *  Max for reasonable Bloom filter false positive rate.
+     *  Do not increase without adding a new Bloom filter size!
+     *  See util/DecayingBloomFilter and tunnel/BloomFilterIVValidator.
+     */
+    public static final int MAX_OUTBOUND_BANDWIDTH = 8192;
     
     /** 
      * how often we replenish the queues.  
diff --git a/router/java/src/net/i2p/router/tunnel/BloomFilterIVValidator.java b/router/java/src/net/i2p/router/tunnel/BloomFilterIVValidator.java
index 578390aa3311d1ce1a7bb6cae70c21bde15cc73f..bf0faad97b4fa7fa80d6fc958eda39b9f052f75f 100644
--- a/router/java/src/net/i2p/router/tunnel/BloomFilterIVValidator.java
+++ b/router/java/src/net/i2p/router/tunnel/BloomFilterIVValidator.java
@@ -1,9 +1,12 @@
 package net.i2p.router.tunnel;
 
+import java.io.File;
+
 import net.i2p.data.DataHelper;
 import net.i2p.router.RouterContext;
 import net.i2p.router.util.DecayingBloomFilter;
 import net.i2p.router.util.DecayingHashSet;
+import net.i2p.util.Log;
 import net.i2p.util.SimpleByteCache;
 import net.i2p.util.SystemVersion;
 
@@ -26,12 +29,19 @@ class BloomFilterIVValidator implements IVValidator {
     private static final int MIN_SHARE_KBPS_TO_USE_BLOOM = 64;
     private static final int MIN_SHARE_KBPS_FOR_BIG_BLOOM = 512;
     private static final int MIN_SHARE_KBPS_FOR_HUGE_BLOOM = 1536;
+    private static final int MIN_SHARE_KBPS_FOR_HUGE2_BLOOM = 4096;
     private static final long MIN_MEM_TO_USE_BLOOM = 64*1024*1024l;
     private static final long MIN_MEM_FOR_BIG_BLOOM = 128*1024*1024l;
     private static final long MIN_MEM_FOR_HUGE_BLOOM = 256*1024*1024l;
+    private static final long MIN_MEM_FOR_HUGE2_BLOOM = 384*1024*1024l;
     /** for testing */
     private static final String PROP_FORCE = "router.forceDecayingBloomFilter";
+    /** for testing */
+    private static final String PROP_DISABLE = "router.disableDecayingBloomFilter";
 
+    /**
+     *  @param Kbps share bandwidth
+     */
     public BloomFilterIVValidator(RouterContext ctx, int KBps) {
         _context = ctx;
         // Select the filter based on share bandwidth and memory.
@@ -39,21 +49,36 @@ class BloomFilterIVValidator implements IVValidator {
         // to keep acceptable false positive rates.
         // See DBF, BloomSHA1, and KeySelector for details.
         long maxMemory = SystemVersion.getMaxMemory();
-        if (_context.getBooleanProperty(PROP_FORCE))
+        if (_context.getBooleanProperty(PROP_FORCE)) {
             _filter = new DecayingBloomFilter(ctx, HALFLIFE_MS, 16, "TunnelIVV");  // 2MB fixed
-        else if (KBps < MIN_SHARE_KBPS_TO_USE_BLOOM || maxMemory < MIN_MEM_TO_USE_BLOOM)
+        } else if (_context.getBooleanProperty(PROP_DISABLE)) {
+            _filter = null;
+        } else if (KBps < MIN_SHARE_KBPS_TO_USE_BLOOM || maxMemory < MIN_MEM_TO_USE_BLOOM) {
+            if (KBps >= MIN_SHARE_KBPS_TO_USE_BLOOM)
+                warn(maxMemory, KBps, MIN_MEM_TO_USE_BLOOM, MIN_SHARE_KBPS_TO_USE_BLOOM);
             _filter = new DecayingHashSet(ctx, HALFLIFE_MS, 16, "TunnelIVV"); // appx. 4MB max
-        else if (KBps >= MIN_SHARE_KBPS_FOR_HUGE_BLOOM && maxMemory >= MIN_MEM_FOR_HUGE_BLOOM)
+        } else if (KBps >= MIN_SHARE_KBPS_FOR_HUGE2_BLOOM && maxMemory >= MIN_MEM_FOR_HUGE2_BLOOM) {
+            _filter = new DecayingBloomFilter(ctx, HALFLIFE_MS, 16, "TunnelIVV", 26);  // 16MB fixed
+        } else if (KBps >= MIN_SHARE_KBPS_FOR_HUGE_BLOOM && maxMemory >= MIN_MEM_FOR_HUGE_BLOOM) {
+            if (KBps >= MIN_SHARE_KBPS_FOR_HUGE2_BLOOM)
+                warn(maxMemory, KBps, MIN_MEM_FOR_HUGE2_BLOOM, MIN_SHARE_KBPS_FOR_HUGE2_BLOOM);
             _filter = new DecayingBloomFilter(ctx, HALFLIFE_MS, 16, "TunnelIVV", 25);  // 8MB fixed
-        else if (KBps >= MIN_SHARE_KBPS_FOR_BIG_BLOOM && maxMemory >= MIN_MEM_FOR_BIG_BLOOM)
+        } else if (KBps >= MIN_SHARE_KBPS_FOR_BIG_BLOOM && maxMemory >= MIN_MEM_FOR_BIG_BLOOM) {
+            if (KBps >= MIN_SHARE_KBPS_FOR_HUGE_BLOOM)
+                warn(maxMemory, KBps, MIN_MEM_FOR_HUGE_BLOOM, MIN_SHARE_KBPS_FOR_HUGE_BLOOM);
             _filter = new DecayingBloomFilter(ctx, HALFLIFE_MS, 16, "TunnelIVV", 24);  // 4MB fixed
-        else
+        } else {
+            if (KBps >= MIN_SHARE_KBPS_FOR_BIG_BLOOM)
+                warn(maxMemory, KBps, MIN_MEM_FOR_BIG_BLOOM, MIN_SHARE_KBPS_FOR_BIG_BLOOM);
             _filter = new DecayingBloomFilter(ctx, HALFLIFE_MS, 16, "TunnelIVV");  // 2MB fixed
+        }
         ctx.statManager().createRateStat("tunnel.duplicateIV", "Note that a duplicate IV was received", "Tunnels", 
                                          new long[] { 60*60*1000l });
     }
     
     public boolean receiveIV(byte ivData[], int ivOffset, byte payload[], int payloadOffset) {
+        if (_filter == null)  // testing only
+            return true;
         byte[] buf = SimpleByteCache.acquire(HopProcessor.IV_LENGTH);
         DataHelper.xor(ivData, ivOffset, payload, payloadOffset, buf, 0, HopProcessor.IV_LENGTH);
         boolean dup = _filter.add(buf); 
@@ -62,5 +87,26 @@ class BloomFilterIVValidator implements IVValidator {
         return !dup; // return true if it is OK, false if it isn't
     }
 
-    public void destroy() { _filter.stopDecaying(); }
+    public void destroy() {
+        if (_filter != null)
+            _filter.stopDecaying();
+    }
+
+    /** @since 0.9.20 */
+    private void warn(long maxMemory, int KBps, long recMaxMem, int threshKBps) {
+        if (SystemVersion.isAndroid())
+            return;
+        String msg =
+            "Configured for " + DataHelper.formatSize(KBps *1024) +
+            "Bps share bandwidth but only " +
+            DataHelper.formatSize(maxMemory) + "B available memory." +
+            " Recommend increasing wrapper.java.maxmemory in " +
+            _context.getBaseDir() + File.separatorChar + "wrapper.config" +
+            // getMaxMemory() returns significantly lower than wrapper config, so add 10%
+            " to at least " + (recMaxMem * 11 / 10 / (1024*1024)) + " (MB)" +
+            " if the actual share bandwidth exceeds " +
+            DataHelper.formatSize(threshKBps * 1024) + "Bps.";
+        System.out.println("WARN: " + msg);
+        _context.logManager().getLog(BloomFilterIVValidator.class).logAlways(Log.WARN, msg);
+    }
 }
diff --git a/router/java/src/net/i2p/router/util/DecayingBloomFilter.java b/router/java/src/net/i2p/router/util/DecayingBloomFilter.java
index 9aa9c6715f3813d0ba5190ecdbd76ad545fd38e5..a5910e36efbb023735d05c3ad90aa75488ed9fca 100644
--- a/router/java/src/net/i2p/router/util/DecayingBloomFilter.java
+++ b/router/java/src/net/i2p/router/util/DecayingBloomFilter.java
@@ -372,6 +372,9 @@ public class DecayingBloomFilter {
      *
      *  Following stats for m=25, k=10:
      *  1792 2.4E-6; 4096 0.14%; 5120 0.6%; 6144 1.7%; 8192 6.8%; 10240 15%
+     *
+     *  Following stats for m=26, k=10:
+     *  4096 7.3E-6; 5120 4.5E-5; 6144 1.8E-4; 8192 0.14%; 10240 0.6%, 12288 1.7%
      *</pre>
      */
 /*****
@@ -400,16 +403,23 @@ public class DecayingBloomFilter {
     }
 
     private static void testByLong(int kbps, int m, int numRuns) {
+        System.out.println("Starting 8 byte test");
         int messages = 60 * 10 * kbps;
-        Random r = new Random();
+        java.util.Random r = new java.util.Random();
         DecayingBloomFilter filter = new DecayingBloomFilter(I2PAppContext.getGlobalContext(), 600*1000, 8, "test", m);
         int falsePositives = 0;
         long totalTime = 0;
         double fpr = 0d;
         for (int j = 0; j < numRuns; j++) {
+            // screen out birthday paradoxes (waste of time and space?)
+            java.util.Set<Long> longs = new java.util.HashSet<Long>(messages);
             long start = System.currentTimeMillis();
             for (int i = 0; i < messages; i++) {
-                if (filter.add(r.nextLong())) {
+                long rand;
+                do {
+                    rand = r.nextLong();
+                } while (!longs.add(Long.valueOf(rand)));
+                if (filter.add(rand)) {
                     falsePositives++;
                     //System.out.println("False positive " + falsePositives + " (testByLong j=" + j + " i=" + i + ")");
                 }
@@ -422,13 +432,14 @@ public class DecayingBloomFilter {
         System.out.println("False postive rate should be " + fpr);
         System.out.println("After " + numRuns + " runs pushing " + messages + " entries in "
                            + DataHelper.formatDuration(totalTime/numRuns) + " per run, there were "
-                           + falsePositives + " false positives");
-
+                           + falsePositives + " false positives (" +
+                           (((double) falsePositives) / messages) + ')');
     }
 
     private static void testByBytes(int kbps, int m, int numRuns) {
+        System.out.println("Starting 16 byte test");
         byte iv[][] = new byte[60*10*kbps][16];
-        Random r = new Random();
+        java.util.Random r = new java.util.Random();
         for (int i = 0; i < iv.length; i++)
             r.nextBytes(iv[i]);
 
@@ -452,7 +463,8 @@ public class DecayingBloomFilter {
         System.out.println("False postive rate should be " + fpr);
         System.out.println("After " + numRuns + " runs pushing " + iv.length + " entries in "
                            + DataHelper.formatDuration(totalTime/numRuns) + " per run, there were "
-                           + falsePositives + " false positives");
+                           + falsePositives + " false positives (" +
+                           (((double) falsePositives) / iv.length) + ')');
         //System.out.println("inserted: " + bloom.size() + " with " + bloom.capacity() 
         //                   + " (" + bloom.falsePositives()*100.0d + "% false positive)");
     }