diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java
index 560475e42f65999877b418ea3939220820aaf4fa..3b279a679aa008448a4170d8ce4f6b422840477c 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java
@@ -662,7 +662,7 @@ public class I2PTunnel implements Logging, EventDispatcher {
      * "openSOCKSTunnelResult" = "ok" or "error" after the client tunnel has
      * started.
      *
-     * @param args {portNumber}
+     * @param args {portNumber [, sharedClient]}
      * @param l logger to receive events and output
      */
     public void runSOCKSTunnel(String args[], Logging l) {
@@ -677,6 +677,11 @@ public class I2PTunnel implements Logging, EventDispatcher {
                 return;
             }
 
+            boolean isShared = false;
+            if (args.length > 1)
+                isShared = "true".equalsIgnoreCase(args[1].trim());
+
+            ownDest = !isShared;
             I2PTunnelTask task;
             task = new I2PSOCKSTunnel(port, l, ownDest, (EventDispatcher) this, this);
             addtask(task);
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java
index 955d3abd1e82afb47427608c7176579a7a608ea5..f8592fcd39586e2e61a9947f05c030e7cdd26c92 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java
@@ -135,8 +135,10 @@ public class TunnelController implements Logging {
         }
         if ("httpclient".equals(type)) {
             startHttpClient();
-        }else if("ircclient".equals(type)) {
+        } else if("ircclient".equals(type)) {
             startIrcClient();
+        } else if("sockstunnel".equals(type)) {
+            startSocksClient();
         } else if ("client".equals(type)) {
             startClient();
         } else if ("server".equals(type)) {
@@ -176,6 +178,17 @@ public class TunnelController implements Logging {
         _running = true;
     }
     
+    private void startSocksClient() {
+        setI2CPOptions();
+        setSessionOptions();
+        setListenOn();
+        String listenPort = getListenPort();
+        String sharedClient = getSharedClient();
+        _tunnel.runSOCKSTunnel(new String[] { listenPort, sharedClient }, this);
+        acquire();
+        _running = true;
+    }
+    
     /** 
      * Note the fact that we are using some sessions, so that they dont get
      * closed by some other tunnels
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 9b216e13ae6d5371f4451c46d540583613704e63..be398f770eb62d0af1962f4b745cb97615a5f689 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java
@@ -7,6 +7,12 @@
 package net.i2p.i2ptunnel.socks;
 
 import java.net.Socket;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.StringTokenizer;
 
 import net.i2p.client.streaming.I2PSocket;
 import net.i2p.data.Destination;
@@ -20,7 +26,7 @@ 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;
 
     //public I2PSOCKSTunnel(int localPort, Logging l, boolean ownDest) {
@@ -36,7 +42,7 @@ public class I2PSOCKSTunnel extends I2PTunnelClientBase {
         }
 
         setName(getLocalPort() + " -> SOCKSTunnel");
-
+        parseOptions();
         startRunning();
 
         notifyEvent("openSOCKSTunnelResult", "ok");
@@ -46,11 +52,49 @@ public class I2PSOCKSTunnel extends I2PTunnelClientBase {
         try {
             SOCKSServer serv = SOCKSServerFactory.createSOCKSServer(s);
             Socket clientSock = serv.getClientSocket();
-            I2PSocket destSock = serv.getDestinationI2PSocket();
+            I2PSocket destSock = serv.getDestinationI2PSocket(this);
             new I2PTunnelRunner(clientSock, destSock, sockLock, null, mySockets);
         } catch (SOCKSException e) {
             _log.error("Error from SOCKS connection: " + e.getMessage());
             closeSocket(s);
         }
     }
-}
\ No newline at end of file
+
+    private static final String PROP_PROXY = "i2ptunnel.socks.proxy.";
+    private void parseOptions() {
+        Properties opts = getTunnel().getClientOptions();
+        proxies = new HashMap(0);
+        for (Map.Entry e : opts.entrySet()) {
+           String prop = (String)e.getKey();
+           if ((!prop.startsWith(PROP_PROXY)) || prop.length() <= PROP_PROXY.length())
+              continue;
+           String port = prop.substring(PROP_PROXY.length());
+           List proxyList = new ArrayList(1);
+           StringTokenizer tok = new StringTokenizer((String)e.getValue(), ", \t");
+           while (tok.hasMoreTokens()) {
+               String proxy = tok.nextToken().trim();
+               if (proxy.endsWith(".i2p"))
+                   proxyList.add(proxy);
+               else
+                   _log.error("Non-i2p SOCKS outproxy: " + proxy);
+           }
+           proxies.put(port, proxyList);
+        }
+    }
+
+    public HashMap<String, List<String>> getProxyMap() {
+        return proxies;
+    }
+
+    public List<String> getProxies(int port) {
+        List<String> rv = proxies.get(port + "");
+        if (rv == null)
+            rv = getDefaultProxies();
+        return rv;
+    }
+
+    public List<String> getDefaultProxies() {
+        return proxies.get("default");
+    }
+
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java
index 5efc51d9fb54cab9adc8910b98f4756867a9b5de..252d4e1aa8e3b55ff6228e473829a9fdc15dfb7c 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java
@@ -12,7 +12,14 @@ import java.io.DataOutputStream;
 import java.io.IOException;
 import java.net.InetAddress;
 import java.net.Socket;
-
+import java.net.SocketException;
+import java.util.List;
+
+import net.i2p.I2PAppContext;
+import net.i2p.I2PException;
+import net.i2p.client.streaming.I2PSocket;
+import net.i2p.data.DataFormatException;
+import net.i2p.i2ptunnel.I2PTunnel;
 import net.i2p.util.HexDump;
 import net.i2p.util.Log;
 
@@ -28,7 +35,6 @@ public class SOCKS5Server extends SOCKSServer {
     private static final int SOCKS_VERSION_5 = 0x05;
 
     private Socket clientSock = null;
-
     private boolean setupCompleted = false;
 
     /**
@@ -126,6 +132,7 @@ public class SOCKS5Server extends SOCKSServer {
             throw new SOCKSException("UDP ASSOCIATE command not supported");
         default:
             _log.debug("unknown command in request (" + Integer.toHexString(command) + ")");
+            sendRequestReply(Reply.COMMAND_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out);
             throw new SOCKSException("Invalid command in request");
         }
 
@@ -166,12 +173,14 @@ public class SOCKS5Server extends SOCKSServer {
             throw new SOCKSException("IPV6 addresses not supported");
         default:
             _log.debug("unknown address type in request (" + Integer.toHexString(command) + ")");
+            sendRequestReply(Reply.ADDRESS_TYPE_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out);
             throw new SOCKSException("Invalid addresses type in request");
         }
 
         connPort = in.readUnsignedShort();
         if (connPort == 0) {
             _log.debug("trying to connect to TCP port 0?  Dropping!");
+            sendRequestReply(Reply.CONNECTION_NOT_ALLOWED_BY_RULESET, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out);
             throw new SOCKSException("Invalid port number in request");
         }
     }
@@ -248,6 +257,107 @@ public class SOCKS5Server extends SOCKSServer {
         out.write(reply);
     }
 
+    /**
+     * Get an I2PSocket that can be used to send/receive 8-bit clean data
+     * to/from the destination of the SOCKS connection.
+     *
+     * @return an I2PSocket connected with the destination
+     */
+    public I2PSocket getDestinationI2PSocket(I2PSOCKSTunnel t) throws SOCKSException {
+        setupServer();
+
+        if (connHostName == null) {
+            _log.error("BUG: destination host name has not been initialized!");
+            throw new SOCKSException("BUG! See the logs!");
+        }
+        if (connPort == 0) {
+            _log.error("BUG: destination port has not been initialized!");
+            throw new SOCKSException("BUG! See the logs!");
+        }
+
+        DataOutputStream out; // for errors
+        try {
+            out = new DataOutputStream(clientSock.getOutputStream());
+        } catch (IOException e) {
+            throw new SOCKSException("Connection error (" + e.getMessage() + ")");
+        }
+
+        // FIXME: here we should read our config file, select an
+        // outproxy, and instantiate the proper socket class that
+        // handles the outproxy itself (SOCKS4a, SOCKS5, HTTP CONNECT...).
+        I2PSocket destSock;
+
+        try {
+            if (connHostName.toLowerCase().endsWith(".i2p")) {
+                _log.debug("connecting to " + connHostName + "...");
+                // Let's not due a new Dest for every request, huh?
+                //I2PSocketManager sm = I2PSocketManagerFactory.createManager();
+                //destSock = sm.connect(I2PTunnel.destFromName(connHostName), null);
+                destSock = t.createI2PSocket(I2PTunnel.destFromName(connHostName));
+            } else if ("localhost".equals(connHostName) || "127.0.0.1".equals(connHostName)) {
+                String err = "No localhost accesses allowed through the Socks Proxy";
+                _log.error(err);
+                try {
+                    sendRequestReply(Reply.CONNECTION_NOT_ALLOWED_BY_RULESET, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out);
+                } catch (IOException ioe) {}
+                throw new SOCKSException(err);
+            } else if (connPort == 80) {
+                // rewrite GET line to include hostname??? or add Host: line???
+                // or forward to local eepProxy (but that's a Socket not an I2PSocket)
+                // use eepProxy configured outproxies?
+                String err = "No handler for HTTP outproxy implemented";
+                _log.error(err);
+                try {
+                    sendRequestReply(Reply.CONNECTION_NOT_ALLOWED_BY_RULESET, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out);
+                } catch (IOException ioe) {}
+                throw new SOCKSException(err);
+            } else {
+                List<String> proxies = t.getProxies(connPort);
+                if (proxies == null || proxies.size() <= 0) {
+                    String err = "No outproxy configured for port " + connPort + " and no default configured either";
+                    _log.error(err);
+                    try {
+                        sendRequestReply(Reply.CONNECTION_NOT_ALLOWED_BY_RULESET, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out);
+                    } catch (IOException ioe) {}
+                    throw new SOCKSException(err);
+                }
+                int p = I2PAppContext.getGlobalContext().random().nextInt(proxies.size());
+                String proxy = proxies.get(p);
+                _log.debug("connecting to port " + connPort + " proxy " + proxy + " for " + connHostName + "...");
+                // this isn't going to work, these need to be socks outproxies so we need
+                // to do a socks session to them?
+                destSock = t.createI2PSocket(I2PTunnel.destFromName(proxy));
+            }
+            confirmConnection();
+            _log.debug("connection confirmed - exchanging data...");
+        } catch (DataFormatException e) {
+            try {
+                sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out);
+            } catch (IOException ioe) {}
+            throw new SOCKSException("Error in destination format");
+        } catch (SocketException e) {
+            try {
+                sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out);
+            } catch (IOException ioe) {}
+            throw new SOCKSException("Error connecting ("
+                                     + e.getMessage() + ")");
+        } catch (IOException e) {
+            try {
+                sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out);
+            } catch (IOException ioe) {}
+            throw new SOCKSException("Error connecting ("
+                                     + e.getMessage() + ")");
+        } catch (I2PException e) {
+            try {
+                sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out);
+            } catch (IOException ioe) {}
+            throw new SOCKSException("Error connecting ("
+                                     + e.getMessage() + ")");
+        }
+
+        return destSock;
+    }
+
     /*
      * Some namespaces to enclose SOCKS protocol codes
      */
@@ -279,4 +389,4 @@ public class SOCKS5Server extends SOCKSServer {
         private static final int COMMAND_NOT_SUPPORTED = 0x07;
         private static final int ADDRESS_TYPE_NOT_SUPPORTED = 0x08;
     }
-}
\ No newline at end of file
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServer.java
index caf4d1ce3f2478751998fdfcfce21cf833d6f22d..06c3fab552af235f8bf34896d072b8d406a6d594 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServer.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServer.java
@@ -6,15 +6,9 @@
  */
 package net.i2p.i2ptunnel.socks;
 
-import java.io.IOException;
 import java.net.Socket;
-import java.net.SocketException;
 
-import net.i2p.I2PException;
 import net.i2p.client.streaming.I2PSocket;
-import net.i2p.client.streaming.I2PSocketManager;
-import net.i2p.client.streaming.I2PSocketManagerFactory;
-import net.i2p.data.DataFormatException;
 import net.i2p.i2ptunnel.I2PTunnel;
 import net.i2p.util.Log;
 
@@ -30,10 +24,6 @@ public abstract class SOCKSServer {
     protected String connHostName = null;
     protected int connPort = 0;
 
-    I2PSocket destSocket = null;
-
-    Object FIXME = new Object();
-
     /**
      * Perform server initialization (expecially regarding protected
      * variables).
@@ -59,47 +49,6 @@ public abstract class SOCKSServer {
      *
      * @return an I2PSocket connected with the destination
      */
-    public I2PSocket getDestinationI2PSocket() throws SOCKSException {
-        setupServer();
-
-        if (connHostName == null) {
-            _log.error("BUG: destination host name has not been initialized!");
-            throw new SOCKSException("BUG! See the logs!");
-        }
-        if (connPort == 0) {
-            _log.error("BUG: destination port has not been initialized!");
-            throw new SOCKSException("BUG! See the logs!");
-        }
-
-        // FIXME: here we should read our config file, select an
-        // outproxy, and instantiate the proper socket class that
-        // handles the outproxy itself (SOCKS4a, SOCKS5, HTTP CONNECT...).
-        I2PSocket destSock;
-
-        try {
-            if (connHostName.toLowerCase().endsWith(".i2p")) {
-                _log.debug("connecting to " + connHostName + "...");
-                I2PSocketManager sm = I2PSocketManagerFactory.createManager();
-                destSock = sm.connect(I2PTunnel.destFromName(connHostName), null);
-                confirmConnection();
-                _log.debug("connection confirmed - exchanging data...");
-            } else {
-                _log.error("We don't support outproxies (yet)");
-                throw new SOCKSException("Ouproxies not supported (yet)");
-            }
-        } catch (DataFormatException e) {
-            throw new SOCKSException("Error in destination format");
-        } catch (SocketException e) {
-            throw new SOCKSException("Error connecting ("
-                                     + e.getMessage() + ")");
-        } catch (IOException e) {
-            throw new SOCKSException("Error connecting ("
-                                     + e.getMessage() + ")");
-        } catch (I2PException e) {
-            throw new SOCKSException("Error connecting ("
-                                     + e.getMessage() + ")");
-        }
+    public abstract I2PSocket getDestinationI2PSocket(I2PSOCKSTunnel t) throws SOCKSException;
 
-        return destSock;
-    }
 }
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java
index 357149652e54fc3b0936727347d5ba0fec5133a4..67a52d6889e9dd6f3aa5a8cdcc99b6e5302cccd9 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java
@@ -7,6 +7,7 @@
 package net.i2p.i2ptunnel.socks;
 
 import java.io.DataInputStream;
+import java.io.DataOutputStream;
 import java.io.IOException;
 import java.net.Socket;
 
@@ -18,6 +19,15 @@ import net.i2p.util.Log;
 public class SOCKSServerFactory {
     private final static Log _log = new Log(SOCKSServerFactory.class);
 
+    private final static String ERR_REQUEST_DENIED =
+        "HTTP/1.1 403 Access Denied\r\n" +
+        "Content-Type: text/html; charset=iso-8859-1\r\n" +
+        "Cache-control: no-cache\r\n" +
+        "\r\n" +
+        "<html><body><H1>I2P SOCKS PROXY ERROR: REQUEST DENIED</H1>" +
+        "Your browser is misconfigured. This is a SOCKS proxy, not a HTTP proxy" +
+        "</body></html>";
+    
     /**
      * Create a new SOCKS server, using the provided socket (that must
      * be connected to a client) to select the proper SOCKS protocol
@@ -38,9 +48,15 @@ public class SOCKSServerFactory {
                 // SOCKS version 5
                 serv = new SOCKS5Server(s);
                 break;
+            case 'C':
+            case 'G':
+            case 'H':
+            case 'P':
+                DataOutputStream out = new DataOutputStream(s.getOutputStream());
+                out.write(ERR_REQUEST_DENIED.getBytes());
+                throw new SOCKSException("HTTP request to socks");
             default:
-                _log.debug("SOCKS protocol version not supported (" + Integer.toHexString(socksVer) + ")");
-                return null;
+                throw new SOCKSException("SOCKS protocol version not supported (" + Integer.toHexString(socksVer) + ")");
             }
         } catch (IOException e) {
             _log.debug("error reading SOCKS protocol version");
@@ -49,4 +65,4 @@ public class SOCKSServerFactory {
 
         return serv;
     }
-}
\ No newline at end of file
+}
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 90e5f7e201086c5f0c612f480561b8dfd612c559..627024b67167e98ce1151ac41c89b9258aa02b40 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java
@@ -8,9 +8,12 @@ package net.i2p.i2ptunnel.web;
  *
  */
 
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Properties;
+import java.util.Set;
 import java.util.StringTokenizer;
 
 import net.i2p.i2ptunnel.TunnelController;
@@ -28,9 +31,7 @@ public class EditBean extends IndexBean {
         if (controllers.size() > tunnel) {
             TunnelController cur = (TunnelController)controllers.get(tunnel);
             if (cur == null) return false;
-            return ( ("client".equals(cur.getType())) || 
-            		 ("httpclient".equals(cur.getType()))||
-            		 ("ircclient".equals(cur.getType())));
+            return isClient(cur.getType());
         } else {
             return false;
         }
@@ -38,7 +39,7 @@ public class EditBean extends IndexBean {
     
     public String getTargetHost(int tunnel) {
         TunnelController tun = getController(tunnel);
-        if (tun != null)
+        if (tun != null && tun.getTargetHost() != null)
             return tun.getTargetHost();
         else
             return "127.0.0.1";
@@ -52,7 +53,7 @@ public class EditBean extends IndexBean {
     }
     public String getSpoofedHost(int tunnel) {
         TunnelController tun = getController(tunnel);
-        if (tun != null)
+        if (tun != null && tun.getSpoofedHost() != null)
             return tun.getSpoofedHost();
         else
             return "";
@@ -82,119 +83,100 @@ public class EditBean extends IndexBean {
     }
     
     public boolean shouldDelay(int tunnel) {
-        TunnelController tun = getController(tunnel);
-        if (tun != null) {
-            Properties opts = getOptions(tun);
-            if (opts != null) {
-                String delay = opts.getProperty("i2p.streaming.connectDelay");
-                if ( (delay == null) || ("0".equals(delay)) )
-                    return false;
-                else
-                    return true;
-            } else {
-                return false;
-            }
-        } else {
-            return false;
-        }
+        return getProperty(tunnel, "i2p.streaming.connectDelay", 0) > 0;
     }
     
     public boolean isInteractive(int tunnel) {
-        TunnelController tun = getController(tunnel);
-        if (tun != null) {
-            Properties opts = getOptions(tun);
-            if (opts != null) {
-                String wsiz = opts.getProperty("i2p.streaming.maxWindowSize");
-                if ( (wsiz == null) || (!"1".equals(wsiz)) )
-                    return false;
-                else
-                    return true;
-            } else {
-                return false;
-            }
-        } else {
-            return false;
-        }
+        return getProperty(tunnel, "i2p.streaming.maxWindowSize", 128) == 12;
     }
     
     public int getTunnelDepth(int tunnel, int defaultLength) {
-        TunnelController tun = getController(tunnel);
-        if (tun != null) {
-            Properties opts = getOptions(tun);
-            if (opts != null) {
-                String len = opts.getProperty("inbound.length");
-                if (len == null) return defaultLength;
-                try {
-                    return Integer.parseInt(len);
-                } catch (NumberFormatException nfe) {
-                    return defaultLength;
-                }
-            } else {
-                return defaultLength;
-            }
-        } else {
-            return defaultLength;
-        }
+        return getProperty(tunnel, "inbound.length", defaultLength);
     }
     
     public int getTunnelQuantity(int tunnel, int defaultQuantity) {
-        TunnelController tun = getController(tunnel);
-        if (tun != null) {
-            Properties opts = getOptions(tun);
-            if (opts != null) {
-                String len = opts.getProperty("inbound.quantity");
-                if (len == null) return defaultQuantity;
-                try {
-                    return Integer.parseInt(len);
-                } catch (NumberFormatException nfe) {
-                    return defaultQuantity;
-                }
-            } else {
-                return defaultQuantity;
-            }
-        } else {
-            return defaultQuantity;
-        }
+        return getProperty(tunnel, "inbound.quantity", defaultQuantity);
     }
    
     public int getTunnelBackupQuantity(int tunnel, int defaultBackupQuantity) {
+        return getProperty(tunnel, "inbound.backupQuantity", defaultBackupQuantity);
+    }
+  
+    public int getTunnelVariance(int tunnel, int defaultVariance) {
+        return getProperty(tunnel, "inbound.lengthVariance", defaultVariance);
+    }
+    
+    public boolean getReduce(int tunnel) {
+        return false;
+    }
+    
+    public int getReduceCount(int tunnel) {
+        return getProperty(tunnel, "inbound.reduceQuantity", 1);
+    }
+    
+    public int getReduceTime(int tunnel) {
+        return getProperty(tunnel, "reduceIdleTime", 20);
+    }
+    
+    public int getCert(int tunnel) {
+        return 0;
+    }
+    
+    public int getEffort(int tunnel) {
+        return 23;
+    }
+    
+    public String getSigner(int tunnel) {
+        return "";
+    }
+    
+    public boolean getEncrypt(int tunnel) {
+        return false;
+    }
+    
+    public String getEncryptKey(int tunnel) {
+        return getProperty(tunnel, "encryptKey", "");
+    }
+    
+    public boolean getAccess(int tunnel) {
+        return false;
+    }
+    
+    public String getAccessList(int tunnel) {
+        return getProperty(tunnel, "accessList", "");
+    }
+    
+    public boolean getClose(int tunnel) {
+        return false;
+    }
+    
+    public boolean getNewDest(int tunnel) {
+        return false;
+    }
+    
+    private int getProperty(int tunnel, String prop, int def) {
         TunnelController tun = getController(tunnel);
         if (tun != null) {
             Properties opts = getOptions(tun);
             if (opts != null) {
-                String len = opts.getProperty("inbound.backupQuantity");
-                if (len == null) return defaultBackupQuantity;
+                String s = opts.getProperty(prop);
+                if (s == null) return def;
                 try {
-                    return Integer.parseInt(len);
-                } catch (NumberFormatException nfe) {
-                    return defaultBackupQuantity;
-                }
-            } else {
-                return defaultBackupQuantity;
+                    return Integer.parseInt(s);
+                } catch (NumberFormatException nfe) {}
             }
-        } else {
-            return defaultBackupQuantity;
         }
+        return def;
     }
-  
-    public int getTunnelVariance(int tunnel, int defaultVariance) {
+    
+    private String getProperty(int tunnel, String prop, String def) {
         TunnelController tun = getController(tunnel);
         if (tun != null) {
             Properties opts = getOptions(tun);
-            if (opts != null) {
-                String len = opts.getProperty("inbound.lengthVariance");
-                if (len == null) return defaultVariance;
-                try {
-                    return Integer.parseInt(len);
-                } catch (NumberFormatException nfe) {
-                    return defaultVariance;
-                }
-            } else {
-                return defaultVariance;
-            }
-        } else {
-            return defaultVariance;
+            if (opts != null)
+                return opts.getProperty(prop, def);
         }
+        return def;
     }
     
     public String getI2CPHost(int tunnel) {
@@ -213,6 +195,14 @@ public class EditBean extends IndexBean {
             return "7654";
     }
 
+    private static final String noShowProps[] = {
+        "inbound.length", "outbound.length", "inbound.lengthVariance", "outbound.lengthVariance",
+        "inbound.backupQuantity", "outbound.backupQuantity", "inbound.quantity", "outbound.quantity",
+        "inbound.nickname", "outbound.nickname", "i2p.streaming.connectDelay", "i2p.streaming.maxWindowSize"
+        };
+    private static final Set noShowSet = new HashSet(noShowProps.length);
+    static { noShowSet.addAll(Arrays.asList(noShowProps)); }
+
     public String getCustomOptions(int tunnel) {
         TunnelController tun = getController(tunnel);
         if (tun != null) {
@@ -222,19 +212,9 @@ public class EditBean extends IndexBean {
             int i = 0;
             for (Iterator iter = opts.keySet().iterator(); iter.hasNext(); ) {
                 String key = (String)iter.next();
+                if (noShowSet.contains(key))
+                    continue;
                 String val = opts.getProperty(key);
-                if ("inbound.length".equals(key)) continue;
-                if ("outbound.length".equals(key)) continue;
-                if ("inbound.lengthVariance".equals(key)) continue;
-                if ("outbound.lengthVariance".equals(key)) continue;
-                if ("inbound.backupQuantity".equals(key)) continue;
-                if ("outbound.backupQuantity".equals(key)) continue;
-                if ("inbound.quantity".equals(key)) continue;
-                if ("outbound.quantity".equals(key)) continue;
-                if ("inbound.nickname".equals(key)) continue;
-                if ("outbound.nickname".equals(key)) continue;
-                if ("i2p.streaming.connectDelay".equals(key)) continue;
-                if ("i2p.streaming.maxWindowSize".equals(key)) continue;
                 if (i != 0) buf.append(' ');
                 buf.append(key).append('=').append(val);
                 i++;
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 8c361195add42d262599b83dd731901b95c13538..1500150e3e6e4b6882f714f2b50b05072e71dc78 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
@@ -209,10 +209,7 @@ public class IndexBean {
         }
         // Only modify other shared tunnels
         // if the current tunnel is shared, and of supported type
-        if ("true".equalsIgnoreCase(cur.getSharedClient()) &&
-            ("ircclient".equals(cur.getType()) ||
-             "httpclient".equals(cur.getType()) ||
-             "client".equals(cur.getType()))) {
+        if ("true".equalsIgnoreCase(cur.getSharedClient()) && isClient(cur.getType())) {
             // all clients use the same I2CP session, and as such, use the same I2CP options
             List controllers = _group.getControllers();
 
@@ -224,11 +221,7 @@ public class IndexBean {
 
                 // Only modify this non-current tunnel
                 // if it belongs to a shared destination, and is of supported type
-                if ("true".equalsIgnoreCase(c.getSharedClient()) &&
-                    ("httpclient".equals(c.getType()) ||
-                     "ircclient".equals(c.getType()) ||
-                     "client".equals(c.getType()))) {
-
+                if ("true".equalsIgnoreCase(c.getSharedClient()) && isClient(c.getType())) {
                     Properties cOpt = c.getConfig("");
                     if (_tunnelQuantity != null) {
                         cOpt.setProperty("option.inbound.quantity", _tunnelQuantity);
@@ -326,9 +319,14 @@ public class IndexBean {
     public boolean isClient(int tunnelNum) {
         TunnelController cur = getController(tunnelNum);
         if (cur == null) return false;
-        return ( ("client".equals(cur.getType())) || 
-        		("httpclient".equals(cur.getType())) ||
-        		("ircclient".equals(cur.getType())));
+        return isClient(cur.getType());
+    }
+
+    public static boolean isClient(String type) {
+        return ( ("client".equals(type)) || 
+        		("httpclient".equals(type)) ||
+        		("sockstunnel".equals(type)) ||
+        		("ircclient".equals(type)));
     }
     
     public String getTunnelName(int tunnel) {
@@ -361,6 +359,7 @@ public class IndexBean {
         else if ("ircclient".equals(internalType)) return "IRC client";
         else if ("server".equals(internalType)) return "Standard server";
         else if ("httpserver".equals(internalType)) return "HTTP server";
+        else if ("sockstunnel".equals(internalType)) return "SOCKS proxy";
         else return internalType;
     }
     
@@ -579,77 +578,40 @@ public class IndexBean {
         Properties config = new Properties();
         updateConfigGeneric(config);
         
-        if ("httpclient".equals(_type)) {
+        if (isClient(_type)) {
+            // generic client stuff
             if (_port != null)
                 config.setProperty("listenPort", _port);
             if (_reachableByOther != null)
                 config.setProperty("interface", _reachableByOther);
             else
                 config.setProperty("interface", _reachableBy);
-            if (_proxyList != null)
-                config.setProperty("proxyList", _proxyList);
-
-        	config.setProperty("option.inbound.nickname", CLIENT_NICKNAME);
-        	config.setProperty("option.outbound.nickname", CLIENT_NICKNAME);
+            config.setProperty("option.inbound.nickname", CLIENT_NICKNAME);
+            config.setProperty("option.outbound.nickname", CLIENT_NICKNAME);
             if (_name != null && !_sharedClient) {
                  config.setProperty("option.inbound.nickname", _name);
                  config.setProperty("option.outbound.nickname", _name);
             }
-
             config.setProperty("sharedClient", _sharedClient + "");
-        }else if ("ircclient".equals(_type)) {
-                if (_port != null)
-                    config.setProperty("listenPort", _port);
-                if (_reachableByOther != null)
-                    config.setProperty("interface", _reachableByOther);
-                else
-                    config.setProperty("interface", _reachableBy);
-                if (_targetDestination != null)
-                    config.setProperty("targetDestination", _targetDestination);
-
-            	config.setProperty("option.inbound.nickname", CLIENT_NICKNAME);
-            	config.setProperty("option.outbound.nickname", CLIENT_NICKNAME);
-                if (_name != null && !_sharedClient) {
-                     config.setProperty("option.inbound.nickname", _name);
-                     config.setProperty("option.outbound.nickname", _name);
-                }
-
-                config.setProperty("sharedClient", _sharedClient + "");
-        } else if ("client".equals(_type)) {
-            if (_port != null)
-                config.setProperty("listenPort", _port);
-            if (_reachableByOther != null)
-                config.setProperty("interface", _reachableByOther);
-            else
-                config.setProperty("interface", _reachableBy);
-            if (_targetDestination != null)
-                config.setProperty("targetDestination", _targetDestination);
-            
-            config.setProperty("option.inbound.nickname", CLIENT_NICKNAME);
-            config.setProperty("option.outbound.nickname", CLIENT_NICKNAME);
-            if (_name != null && !_sharedClient) {
-                config.setProperty("option.inbound.nickname", _name);
-                config.setProperty("option.outbound.nickname", _name);
-           }
-            config.setProperty("sharedClient", _sharedClient + "");
-        } else if ("server".equals(_type)) {
+        } else {
+            // generic server stuff
             if (_targetHost != null)
                 config.setProperty("targetHost", _targetHost);
             if (_targetPort != null)
                 config.setProperty("targetPort", _targetPort);
             if (_privKeyFile != null)
                 config.setProperty("privKeyFile", _privKeyFile);
+        }
+
+        if ("httpclient".equals(_type)) {
+            if (_proxyList != null)
+                config.setProperty("proxyList", _proxyList);
+        } else if ("ircclient".equals(_type) || "client".equals(_type)) {
+            if (_targetDestination != null)
+                config.setProperty("targetDestination", _targetDestination);
         } else if ("httpserver".equals(_type)) {
-            if (_targetHost != null)
-                config.setProperty("targetHost", _targetHost);
-            if (_targetPort != null)
-                config.setProperty("targetPort", _targetPort);
-            if (_privKeyFile != null)
-                config.setProperty("privKeyFile", _privKeyFile);
             if (_spoofedHost != null)
                 config.setProperty("spoofedHost", _spoofedHost);
-        } else {
-            return null;
         }
 
         return config;
diff --git a/apps/i2ptunnel/jsp/edit.jsp b/apps/i2ptunnel/jsp/edit.jsp
index 931629fb1f325479ccc820c17987de59757cbe3c..67fdf016c37daf170f4b92db3851cc5df898dd8d 100644
--- a/apps/i2ptunnel/jsp/edit.jsp
+++ b/apps/i2ptunnel/jsp/edit.jsp
@@ -14,7 +14,7 @@ String tun = request.getParameter("tunnel");
 } else {
   String type = request.getParameter("type");
   int curTunnel = -1;
-  if ("client".equals(type) || "httpclient".equals(type) || "ircclient".equals(type)) {
+  if (EditBean.isClient(type)) {
     %><jsp:include page="editClient.jsp" /><%
   } else if ("server".equals(type) || "httpserver".equals(type)) {
     %><jsp:include page="editServer.jsp" /><%
@@ -22,4 +22,4 @@ String tun = request.getParameter("tunnel");
     %>Invalid tunnel type<%
   }
 }
-%>
\ No newline at end of file
+%>
diff --git a/apps/i2ptunnel/jsp/editClient.jsp b/apps/i2ptunnel/jsp/editClient.jsp
index 97f8adbe5ec9d3e2a24802e5cba4cee7fb135876..f7ee2294c73726ad36296251b225c7c1061fb89e 100644
--- a/apps/i2ptunnel/jsp/editClient.jsp
+++ b/apps/i2ptunnel/jsp/editClient.jsp
@@ -116,14 +116,14 @@
                 <hr />
             </div>
            
-            <% if ("httpclient".equals(editBean.getInternalType(curTunnel))) {
+            <% if ("httpclient".equals(tunnelType)) {
           %><div id="destinationField" class="rowItem">
                 <label for="proxyList" accesskey="x">
                     Outpro<span class="accessKey">x</span>ies:
                 </label>
                 <input type="text" size="30" id="proxyList" name="proxyList" title="List of Outproxy I2P destinations" value="<%=editBean.getClientDestination(curTunnel)%>" class="freetext" />                
             </div>
-            <% } else {
+            <% } else if ("client".equals(tunnelType) || "ircclient".equals(tunnelType)) {
           %><div id="destinationField" class="rowItem">
                 <label for="targetDestination" accesskey="T">
                     <span class="accessKey">T</span>unnel Destination:
@@ -205,12 +205,12 @@
                     <span class="accessKey">V</span>ariance:
                 </label>
                 <select id="tunnelVariance" name="tunnelVariance" title="Level of Randomization for Tunnel Depth" class="selectbox">
-                    <% int tunnelVariance = editBean.getTunnelVariance(curTunnel, -1);
+                    <% int tunnelVariance = editBean.getTunnelVariance(curTunnel, 0);
                   %><option value="0"<%=(tunnelVariance  ==  0 ? " selected=\"selected\"" : "") %>>0 hop variance (no randomisation, consistant performance)</option>
-                    <option value="-1"<%=(tunnelVariance == -1 ? " selected=\"selected\"" : "") %>>+/- 0-1 hop variance (standard randomisation, standard performance)</option>
-                    <option value="-2"<%=(tunnelVariance == -2 ? " selected=\"selected\"" : "") %>>+/- 0-2 hop variance (high randomisation, variable performance)</option>
                     <option value="1"<%=(tunnelVariance  ==  1 ? " selected=\"selected\"" : "") %>>+ 0-1 hop variance (medium additive randomisation, subtractive performance)</option>
                     <option value="2"<%=(tunnelVariance  ==  2 ? " selected=\"selected\"" : "") %>>+ 0-2 hop variance (high additive randomisation, subtractive performance)</option>
+                    <option value="-1"<%=(tunnelVariance == -1 ? " selected=\"selected\"" : "") %>>+/- 0-1 hop variance (standard randomisation, standard performance)</option>
+                    <option value="-2"<%=(tunnelVariance == -2 ? " selected=\"selected\"" : "") %>>+/- 0-2 hop variance (not recommended)</option>
                 <% if (tunnelVariance > 2 || tunnelVariance < -2) {
                 %>    <option value="<%=tunnelVariance%>" selected="selected"><%= (tunnelVariance > 2 ? "+ " : "+/- ") %>0-<%=tunnelVariance%> hop variance</option>
                 <% }
@@ -265,6 +265,62 @@
                 </label>
                 <input type="text" id="clientPort" name="clientport" size="20" title="I2CP Port Number" value="<%=editBean.getI2CPPort(curTunnel)%>" class="freetext" />                
             </div>
+                 
+            <div class="subdivider">
+                <hr />
+            </div>
+           
+            <div id="optionsField" class="rowItem">
+                <label for="reduce" accesskey="c">
+                    <span class="accessKey">C</span>lose tunnels when idle:
+                </label>
+            </div>
+            <div id="portField" class="rowItem">
+                <label for="access" accesskey="c">
+                    Enable:
+                </label>
+                <input value="1" type="checkbox" id="startOnLoad" name="close" title="Close Tunnels"<%=(editBean.getClose(curTunnel) ? " checked=\"checked\"" : "")%> class="tickbox" />                
+            </div>
+            <div id="portField" class="rowItem">
+                <label for="access" accesskey="c">
+                    Generate New Destination Keys On Reopen:
+                </label>
+                <input value="1" type="checkbox" id="startOnLoad" name="newDest" title="New Destination"<%=(editBean.getNewDest(curTunnel) ? " checked=\"checked\"" : "")%> class="tickbox" />                
+            </div>
+            <div id="portField" class="rowItem">
+                <label for="reduceTime" accesskey="c">
+                    Reduce when idle (minutes):
+                </label>
+                <input type="text" id="port" name="reduceTime" size="4" maxlength="4" title="Reduced Tunnel Idle Time" value="<%=editBean.getReduceTime(curTunnel)%>" class="freetext" />                
+            </div>
+                 
+            <div class="subdivider">
+                <hr />
+            </div>
+           
+            <div id="optionsField" class="rowItem">
+                <label for="reduce" accesskey="d">
+                    Re<span class="accessKey">d</span>uce tunnel quantity when idle:
+                </label>
+            </div>
+            <div id="portField" class="rowItem">
+                <label for="access" accesskey="d">
+                    Enable:
+                </label>
+                <input value="1" type="checkbox" id="startOnLoad" name="reduce" title="Reduce Tunnels"<%=(editBean.getReduce(curTunnel) ? " checked=\"checked\"" : "")%> class="tickbox" />                
+            </div>
+            <div id="portField" class="rowItem">
+                <label for="reduceCount" accesskey="d">
+                    Reduced tunnel count:
+                </label>
+                <input type="text" id="port" name="reduceCount" size="1" maxlength="1" title="Reduced Tunnel Count" value="<%=editBean.getReduceCount(curTunnel)%>" class="freetext" />                
+            </div>
+            <div id="portField" class="rowItem">
+                <label for="reduceTime" accesskey="d">
+                    Reduce when idle (minutes):
+                </label>
+                <input type="text" id="port" name="reduceTime" size="4" maxlength="4" title="Reduced Tunnel Idle Time" value="<%=editBean.getReduceTime(curTunnel)%>" class="freetext" />                
+            </div>
             
             <div class="subdivider">
                 <hr />
@@ -284,8 +340,10 @@
             <div class="header"></div>
             <div class="footer">
                 <div class="toolbox">
+                    <span class="comment">NOTE: If tunnel is currently running, most changes will not take effect until tunnel is stopped and restarted</span>
                     <input type="hidden" value="true" name="removeConfirm" />
-                    <button id="controlSave" accesskey="S" class="control" type="submit" name="action" value="Save changes" title="Save Changes"><span class="accessKey">S</span>ave</button><button id="controlDelete" <%=(editBean.allowJS() ? "onclick=\"if (!confirm('Are you sure you want to delete?')) { return false; }\" " : "")%>accesskey="D" class="control" type="submit" name="action" value="Delete this proxy" title="Delete this Proxy"><span class="accessKey">D</span>elete</button>
+                    <button id="controlSave" accesskey="S" class="control" type="submit" name="action" value="Save changes" title="Save Changes"><span class="accessKey">S</span>ave</button>
+                    <button id="controlDelete" <%=(editBean.allowJS() ? "onclick=\"if (!confirm('Are you sure you want to delete?')) { return false; }\" " : "")%>accesskey="D" class="control" type="submit" name="action" value="Delete this proxy" title="Delete this Proxy"><span class="accessKey">D</span>elete</button>
                 </div>
             </div> 
         </div>
diff --git a/apps/i2ptunnel/jsp/editServer.jsp b/apps/i2ptunnel/jsp/editServer.jsp
index 0cdf5e0e9ec57bd2d8756fe41305f0b7aecd5858..e6cc0497d7d3219331a443260e4c12d301cf29b1 100644
--- a/apps/i2ptunnel/jsp/editServer.jsp
+++ b/apps/i2ptunnel/jsp/editServer.jsp
@@ -110,7 +110,8 @@
                 <label for="spoofedHost" accesskey="W">
                     <span class="accessKey">W</span>ebsite name:
                 </label>
-                <input type="text" size="20" id="spoofedHost" name="spoofedHost" title="Website Host Name" value="<%=editBean.getSpoofedHost(curTunnel)%>" class="freetext" />                
+                <input type="text" size="20" id="targetHost" name="spoofedHost" title="Website Host Name" value="<%=editBean.getSpoofedHost(curTunnel)%>" class="freetext" />                
+                <span class="comment">(leave blank for outproxies)</span>
             </div>
             <% }
           %><div id="privKeyField" class="rowItem">
@@ -177,12 +178,12 @@
                     <span class="accessKey">V</span>ariance:
                 </label>
                 <select id="tunnelVariance" name="tunnelVariance" title="Level of Randomization for Tunnel Depth" class="selectbox">
-                    <% int tunnelVariance = editBean.getTunnelVariance(curTunnel, -1);
+                    <% int tunnelVariance = editBean.getTunnelVariance(curTunnel, 0);
                   %><option value="0"<%=(tunnelVariance  ==  0 ? " selected=\"selected\"" : "") %>>0 hop variance          (no randomisation, consistant performance)</option>
-                    <option value="-1"<%=(tunnelVariance == -1 ? " selected=\"selected\"" : "") %>>+/- 0-1 hop variance    (standard randomisation, standard performance)</option>
-                    <option value="-2"<%=(tunnelVariance == -2 ? " selected=\"selected\"" : "") %>>+/- 0-2 hop variance    (high randomisation, variable performance)</option>
                     <option value="1"<%=(tunnelVariance  ==  1 ? " selected=\"selected\"" : "") %>>+ 0-1 hop variance      (medium additive randomisation, subtractive performance)</option>
                     <option value="2"<%=(tunnelVariance  ==  2 ? " selected=\"selected\"" : "") %>>+ 0-2 hop variance      (high additive randomisation, subtractive performance)</option>
+                    <option value="-1"<%=(tunnelVariance == -1 ? " selected=\"selected\"" : "") %>>+/- 0-1 hop variance    (standard randomisation, standard performance)</option>
+                    <option value="-2"<%=(tunnelVariance == -2 ? " selected=\"selected\"" : "") %>>+/- 0-2 hop variance    (not recommended)</option>
                 <% if (tunnelVariance > 2 || tunnelVariance < -2) {
                 %>    <option value="<%=tunnelVariance%>" selected="selected"><%= (tunnelVariance > 2 ? "+ " : "+/- ") %>0-<%=tunnelVariance%> hop variance</option>
                 <% }
@@ -242,6 +243,130 @@
                 <hr />
             </div>
            
+            <div id="optionsField" class="rowItem">
+                <label for="encrypt" accesskey="e">
+                    <span class="accessKey">E</span>ncrypt Leaseset:
+                </label>
+            </div>
+            <div id="portField" class="rowItem">
+                <label for="encrypt" accesskey="e">
+                    Enable:
+                </label>
+                <input value="1" type="checkbox" id="startOnLoad" name="encrypt" title="Encrypt LeaseSet"<%=(editBean.getEncrypt(curTunnel) ? " checked=\"checked\"" : "")%> class="tickbox" />                
+            </div>
+            <div id="hostField" class="rowItem">
+                <label for="encrypt" accesskey="e">
+                    Leaseset Encryption Key:
+                </label>
+                <input type="text" id="hostField" name="encryptKey" size="60" title="Encrypt Key" value="<%=editBean.getEncryptKey(curTunnel)%>" class="freetext" />                
+                <span class="comment">(Users will require this key)</span>
+            </div>
+                 
+            <div class="subdivider">
+                <hr />
+            </div>
+           
+            <div id="optionsField" class="rowItem">
+                <label for="access" accesskey="s">
+                    Restricted Acce<span class="accessKey">s</span>s List:
+                </label>
+            </div>
+            <div id="portField" class="rowItem">
+                <label for="access" accesskey="s">
+                    Enable:
+                </label>
+                <input value="1" type="checkbox" id="startOnLoad" name="access" title="Enable Access List"<%=(editBean.getAccess(curTunnel) ? " checked=\"checked\"" : "")%> class="tickbox" />                
+            </div>
+            <div id="hostField" class="rowItem">
+                <label for="accessList" accesskey="s">
+                    Access List:
+                </label>
+                <textarea rows="2" cols="60" id="hostField" title="Access List" wrap="off"><%=editBean.getAccessList(curTunnel)%></textarea>               
+                <span class="comment">(Restrict to these clients only)</span>
+            </div>
+                 
+            <div class="subdivider">
+                <hr />
+            </div>
+           
+            <div id="optionsField" class="rowItem">
+                <label for="reduce" accesskey="d">
+                    Re<span class="accessKey">d</span>uce tunnel quantity when idle:
+                </label>
+            </div>
+            <div id="portField" class="rowItem">
+                <label for="access" accesskey="d">
+                    Enable:
+                </label>
+                <input value="1" type="checkbox" id="startOnLoad" name="reduce" title="Reduce Tunnels"<%=(editBean.getReduce(curTunnel) ? " checked=\"checked\"" : "")%> class="tickbox" />                
+            </div>
+            <div id="portField" class="rowItem">
+                <label for="reduceCount" accesskey="d">
+                    Reduced tunnel count:
+                </label>
+                <input type="text" id="port" name="reduceCount" size="1" maxlength="1" title="Reduced Tunnel Count" value="<%=editBean.getReduceCount(curTunnel)%>" class="freetext" />                
+            </div>
+            <div id="portField" class="rowItem">
+                <label for="reduceTime" accesskey="d">
+                    Reduce when idle (minutes):
+                </label>
+                <input type="text" id="port" name="reduceTime" size="4" maxlength="4" title="Reduced Tunnel Idle Time" value="<%=editBean.getReduceTime(curTunnel)%>" class="freetext" />                
+            </div>
+
+            <div class="subdivider">
+                <hr />
+            </div>
+           
+            <div id="tunnelOptionsField" class="rowItem">
+                <label for="cert" accesskey="c">
+                    <span class="accessKey">C</span>ertificate type:
+                </label>
+            </div>
+            <div id="hostField" class="rowItem">
+              <div id="portField" class="rowItem">
+                <label>None</label>
+                <input value="0" type="radio" id="startOnLoad" name="cert" title="No Certificate"<%=(editBean.getCert(curTunnel)==0 ? " checked=\"checked\"" : "")%> class="tickbox" />                
+                <span class="comment"></span>
+              </div>
+              <div id="portField" class="rowItem">
+                <label>Hashcash (effort)</label>
+                <input value="1" type="radio" id="startOnLoad" name="cert" title="Hashcash Certificate"<%=(editBean.getCert(curTunnel)==1 ? " checked=\"checked\"" : "")%> class="tickbox" />                
+                <input type="text" id="port" name="effort" size="2" title="Hashcash Effort" value="<%=editBean.getEffort(curTunnel)%>" class="freetext" />                
+              </div>
+            </div>
+            <div id="portField" class="rowItem">
+                <label for="force" accesskey="c">
+                    Estimate Hashcash Calc Time:
+                </label>
+                <button id="controlSave" accesskey="S" class="control" type="submit" name="action" value="Estimate Calculation Time" title="Estimate Calculation Time">Estimate</button>
+            </div>
+            <div id="hostField" class="rowItem">
+              <div id="portField" class="rowItem">
+                <label>Hidden</label>
+                <input value="2" type="radio" id="startOnLoad" name="cert" title="Hidden Certificate"<%=(editBean.getCert(curTunnel)==2 ? " checked=\"checked\"" : "")%> class="tickbox" />                
+                <span class="comment"></span>
+              </div>
+              <div id="portField" class="rowItem">
+                <label for="signer" accesskey="c">
+                    Signed (signed by):
+                </label>
+                <input value="3" type="radio" id="startOnLoad" name="cert" title="Signed Certificate"<%=(editBean.getCert(curTunnel)==3 ? " checked=\"checked\"" : "")%> class="tickbox" />                
+                <input type="text" id="port" name="signer" size="50" title="Cert Signer" value="<%=editBean.getSigner(curTunnel)%>" class="freetext" />                
+                <span class="comment"></span>
+              </div>
+            </div>
+            <div id="portField" class="rowItem">
+                <label for="force" accesskey="c">
+                    Modify Certificate:
+                </label>
+                <button id="controlSave" accesskey="S" class="control" type="submit" name="action" value="Modify Cert Now" title="Force New Cert Now">Modify</button>
+                <span class="comment">(Tunnel must be stopped first)</span>
+            </div>
+                 
+            <div class="subdivider">
+                <hr />
+            </div>
+                 
             <div id="customOptionsField" class="rowItem">
                 <label for="customOptions" accesskey="u">
                     C<span class="accessKey">u</span>stom options:
@@ -256,8 +381,10 @@
             <div class="header"></div>
             <div class="footer">
                 <div class="toolbox">
+                    <span class="comment">NOTE: If tunnel is currently running, most changes will not take effect until tunnel is stopped and restarted</span>
                     <input type="hidden" value="true" name="removeConfirm" />
-                    <button id="controlSave" accesskey="S" class="control" type="submit" name="action" value="Save changes" title="Save Changes"><span class="accessKey">S</span>ave</button><button id="controlDelete" <%=(editBean.allowJS() ? "onclick=\"if (!confirm('Are you sure you want to delete?')) { return false; }\" " : "")%>accesskey="D" class="control" type="submit" name="action" value="Delete this proxy" title="Delete this Proxy"><span class="accessKey">D</span>elete</button>
+                    <button id="controlSave" accesskey="S" class="control" type="submit" name="action" value="Save changes" title="Save Changes"><span class="accessKey">S</span>ave</button>
+                    <button id="controlDelete" <%=(editBean.allowJS() ? "onclick=\"if (!confirm('Are you sure you want to delete?')) { return false; }\" " : "")%>accesskey="D" class="control" type="submit" name="action" value="Delete this proxy" title="Delete this Proxy"><span class="accessKey">D</span>elete</button>
                 </div>
             </div> 
         </div>
diff --git a/apps/i2ptunnel/jsp/index.jsp b/apps/i2ptunnel/jsp/index.jsp
index 9fc2d2a9677a116be38433e2f4292751b3c4a398..0825493073c731d37d6b568fc6609e9258f21986 100644
--- a/apps/i2ptunnel/jsp/index.jsp
+++ b/apps/i2ptunnel/jsp/index.jsp
@@ -112,10 +112,12 @@
             }
       %></div>
 
+      <% if (!"sockstunnel".equals(indexBean.getInternalType(curClient))) { %>
         <div class="destinationField rowItem">
             <label>Destination:</label>
             <input class="freetext" size="40" readonly="readonly" value="<%=indexBean.getClientDestination(curClient)%>" />
         </div>
+      <% } %>
 
         <div class="descriptionField rowItem">
             <label>Description:</label>
@@ -140,6 +142,7 @@
                         <option value="client">Standard</option>
                         <option value="httpclient">HTTP</option>
                         <option value="ircclient">IRC</option>
+                        <option value="sockstunnel">SOCKS</option>
                     </select>
                     <input class="control" type="submit" value="Create" />
                 </div>
diff --git a/apps/jetty/build.xml b/apps/jetty/build.xml
index aaa97835493bbb8039a16bb8de9d6450ea4ee949..643dd79fdcb77b1c908ef84be030b3b654224e60 100644
--- a/apps/jetty/build.xml
+++ b/apps/jetty/build.xml
@@ -78,7 +78,6 @@
                 <include name="jasper-runtime.jar" />
                 <include name="javax.servlet.jar" />
                 <include name="org.mortbay.jetty.jar" />
-                <include name="xercesImpl.jar" />
             </fileset>
         </copy>
         <delete dir="jetty-5.1.12" />
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..09f0905bf3122812de9733e5bb42dfbb705a76ed
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHandler.java
@@ -0,0 +1,55 @@
+package net.i2p.router.web;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.Destination;
+import net.i2p.data.Hash;
+import net.i2p.data.SessionKey;
+
+/**
+ *  Support additions via B64 Destkey, B64 Desthash, or blahblah.i2p
+ */
+public class ConfigKeyringHandler extends FormHandler {
+    private String _peer;
+    private String _key;
+    
+    protected void processForm() {
+        if ("Add key".equals(_action)) {
+            if (_peer == null || _key == null) {
+                addFormError("You must enter a destination and a key");
+                return;
+            }
+            Hash h = new Hash();
+            try {
+                h.fromBase64(_peer);
+            } catch (DataFormatException dfe) {}
+            if (h.getData() == null) {
+                try {
+                    Destination d = new Destination();
+                    d.fromBase64(_peer);
+                    h = d.calculateHash();
+                } catch (DataFormatException dfe) {}
+            }
+            if (h.getData() == null) {
+                Destination d = _context.namingService().lookup(_peer);
+                if (d != null)
+                    h = d.calculateHash();
+            }
+            SessionKey sk = new SessionKey();
+            try {
+                sk.fromBase64(_key);
+            } catch (DataFormatException dfe) {}
+            if (h.getData() != null && sk.getData() != null) {
+                _context.keyRing().put(h, sk);
+                addFormNotice("Key for " + h.toBase64() + " added to keyring");
+            } else {
+                addFormError("Invalid destination or key");
+            }
+        } else {
+            addFormError("Unsupported");
+        }
+    }
+
+    public void setPeer(String peer) { _peer = peer; }
+    public void setKey(String peer) { _key = peer; }
+}
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..48bc15068e9767b0af4029d33d2a25d7b719bdec
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHelper.java
@@ -0,0 +1,36 @@
+package net.i2p.router.web;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+
+import net.i2p.router.RouterContext;
+
+public class ConfigKeyringHelper {
+    private RouterContext _context;
+    /**
+     * Configure this bean to query a particular router context
+     *
+     * @param contextId begging few characters of the routerHash, or null to pick
+     *                  the first one we come across.
+     */
+    public void setContextId(String contextId) {
+        try {
+            _context = ContextHelper.getContext(contextId);
+        } catch (Throwable t) {
+            t.printStackTrace();
+        }
+    }
+
+    public ConfigKeyringHelper() {}
+    
+    public String getSummary() {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream(4*1024);
+        try {
+            _context.keyRing().renderStatusHTML(new OutputStreamWriter(baos));
+        } catch (IOException ioe) {
+            ioe.printStackTrace();
+        }
+        return new String(baos.toByteArray());
+    }
+}
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java
index 41f0ad90c578a73f11976116e10dabc1f0d52493..114579a27c854690343ae8ba66bdb42e0552bc81 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java
@@ -11,6 +11,7 @@ public class NetDbHelper {
     private RouterContext _context;
     private Writer _out;
     private String _routerPrefix;
+    private boolean _full = false;
 
     /**
      * Configure this bean to query a particular router context
@@ -30,6 +31,7 @@ public class NetDbHelper {
     
     public void setWriter(Writer writer) { _out = writer; }
     public void setRouter(String r) { _routerPrefix = r; }
+    public void setFull(String f) { _full = "1".equals(f); };
     
     public String getNetDbSummary() {
         try {
@@ -37,14 +39,14 @@ public class NetDbHelper {
                 if (_routerPrefix != null)
                     _context.netDb().renderRouterInfoHTML(_out, _routerPrefix);
                 else
-                    _context.netDb().renderStatusHTML(_out);
+                    _context.netDb().renderStatusHTML(_out, _full);
                 return "";
             } else {
                 ByteArrayOutputStream baos = new ByteArrayOutputStream(32*1024);
                 if (_routerPrefix != null)
                     _context.netDb().renderRouterInfoHTML(new OutputStreamWriter(baos), _routerPrefix);
                 else
-                    _context.netDb().renderStatusHTML(new OutputStreamWriter(baos));
+                    _context.netDb().renderStatusHTML(new OutputStreamWriter(baos), _full);
                 return new String(baos.toByteArray());
             }
         } catch (IOException ioe) {
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
index 2b445a0042361205218e0ae6042b5cca1de272d6..ad8e7135d5496dcd93fa768ea6457cae71f96baa 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
@@ -244,7 +244,7 @@ public class SummaryHelper {
      */
     public String getInboundSecondKBps() { 
         if (_context == null) 
-            return "0.0";
+            return "0";
         double kbps = _context.bandwidthLimiter().getReceiveBps()/1024d;
         DecimalFormat fmt = new DecimalFormat("##0.00");
         return fmt.format(kbps);
@@ -256,7 +256,7 @@ public class SummaryHelper {
      */
     public String getOutboundSecondKBps() { 
         if (_context == null) 
-            return "0.0";
+            return "0";
         double kbps = _context.bandwidthLimiter().getSendBps()/1024d;
         DecimalFormat fmt = new DecimalFormat("##0.00");
         return fmt.format(kbps);
@@ -269,10 +269,10 @@ public class SummaryHelper {
      */
     public String getInboundFiveMinuteKBps() {
         if (_context == null) 
-            return "0.0";
+            return "0";
         
         RateStat receiveRate = _context.statManager().getRate("bw.recvRate");
-        if (receiveRate == null) return "0.0";
+        if (receiveRate == null) return "0";
         Rate rate = receiveRate.getRate(5*60*1000);
         double kbps = rate.getAverageValue()/1024;
         DecimalFormat fmt = new DecimalFormat("##0.00");
@@ -286,10 +286,10 @@ public class SummaryHelper {
      */
     public String getOutboundFiveMinuteKBps() { 
         if (_context == null) 
-            return "0.0";
+            return "0";
         
         RateStat receiveRate = _context.statManager().getRate("bw.sendRate");
-        if (receiveRate == null) return "0.0";
+        if (receiveRate == null) return "0";
         Rate rate = receiveRate.getRate(5*60*1000);
         double kbps = rate.getAverageValue()/1024;
         DecimalFormat fmt = new DecimalFormat("##0.00");
@@ -303,10 +303,10 @@ public class SummaryHelper {
      */
     public String getInboundLifetimeKBps() { 
         if (_context == null) 
-            return "0.0";
+            return "0";
         
         RateStat receiveRate = _context.statManager().getRate("bw.recvRate");
-        if (receiveRate == null) return "0.0";
+        if (receiveRate == null) return "0";
         double kbps = receiveRate.getLifetimeAverageValue()/1024;
         DecimalFormat fmt = new DecimalFormat("##0.00");
         return fmt.format(kbps);
@@ -319,10 +319,10 @@ public class SummaryHelper {
      */
     public String getOutboundLifetimeKBps() { 
         if (_context == null) 
-            return "0.0";
+            return "0";
         
         RateStat sendRate = _context.statManager().getRate("bw.sendRate");
-        if (sendRate == null) return "0.0";
+        if (sendRate == null) return "0";
         double kbps = sendRate.getLifetimeAverageValue()/1024;
         DecimalFormat fmt = new DecimalFormat("##0.00");
         return fmt.format(kbps);
@@ -335,11 +335,11 @@ public class SummaryHelper {
      */
     public String getInboundTransferred() { 
         if (_context == null) 
-            return "0.0";
+            return "0";
         
         long received = _context.bandwidthLimiter().getTotalAllocatedInboundBytes();
 
-        return getTransferred(received);
+        return DataHelper.formatSize(received) + 'B';
     }
     
     /**
@@ -349,40 +349,10 @@ public class SummaryHelper {
      */
     public String getOutboundTransferred() { 
         if (_context == null) 
-            return "0.0";
+            return "0";
         
         long sent = _context.bandwidthLimiter().getTotalAllocatedOutboundBytes();
-        return getTransferred(sent);
-    }
-    
-    private static String getTransferred(long bytes) {
-        double val = bytes;
-        int scale = 0;
-        if (bytes > 1024*1024*1024) {
-            // gigs transferred
-            scale = 3; 
-            val /= (double)(1024*1024*1024);
-        } else if (bytes > 1024*1024) {
-            // megs transferred
-            scale = 2;
-            val /= (double)(1024*1024);
-        } else if (bytes > 1024) {
-            // kbytes transferred
-            scale = 1;
-            val /= (double)1024;
-        } else {
-            scale = 0;
-        }
-        
-        DecimalFormat fmt = new DecimalFormat("##0.00");
-
-        String str = fmt.format(val);
-        switch (scale) {
-            case 1: return str + "KB";
-            case 2: return str + "MB";
-            case 3: return str + "GB";
-            default: return bytes + "bytes";
-        }
+        return DataHelper.formatSize(sent) + 'B';
     }
     
     /**
diff --git a/apps/routerconsole/jsp/configkeyring.jsp b/apps/routerconsole/jsp/configkeyring.jsp
new file mode 100644
index 0000000000000000000000000000000000000000..7dd8bf1784bef7252ad2cad1bfd5c46374cd2c36
--- /dev/null
+++ b/apps/routerconsole/jsp/configkeyring.jsp
@@ -0,0 +1,58 @@
+<%@page contentType="text/html"%>
+<%@page pageEncoding="UTF-8"%>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+
+<html><head>
+<title>I2P Router Console - config keyring</title>
+<link rel="stylesheet" href="default.css" type="text/css" />
+</head><body>
+
+<%@include file="nav.jsp" %>
+<%@include file="summary.jsp" %>
+
+<div class="main" id="main">
+ <%@include file="confignav.jsp" %>
+  
+ <jsp:useBean class="net.i2p.router.web.ConfigKeyringHandler" id="formhandler" scope="request" />
+ <jsp:setProperty name="formhandler" property="*" />
+ <jsp:setProperty name="formhandler" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
+ <font color="red"><jsp:getProperty name="formhandler" property="errors" /></font>
+ <i><jsp:getProperty name="formhandler" property="notices" /></i>
+ 
+
+
+ <jsp:useBean class="net.i2p.router.web.ConfigKeyringHelper" id="keyringhelper" scope="request" />
+ <jsp:setProperty name="keyringhelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
+
+ <p>
+ <h2>Keyring</h2>
+ The router keyring is used to decrypt encrypted leaseSets.
+ The keyring may contain keys for local or remote encrypted destinations.
+ <p><jsp:getProperty name="keyringhelper" property="summary" />
+ </p>
+
+ <hr />
+
+ <form action="configkeyring.jsp" method="POST">
+ <% String prev = System.getProperty("net.i2p.router.web.ConfigKeyringHandler.nonce");
+    if (prev != null) System.setProperty("net.i2p.router.web.ConfigKeyringHandler.noncePrev", prev);
+    System.setProperty("net.i2p.router.web.ConfigKeyringHandler.nonce", new java.util.Random().nextLong()+""); %>
+ <input type="hidden" name="nonce" value="<%=System.getProperty("net.i2p.router.web.ConfigKeyringHandler.nonce")%>" />
+ <h2>Manual Keyring Addition</h2>
+ Enter keys for encrypted remote destinations here.
+ Keys for local destinations must be entered on the <a href="i2ptunnel/index.jsp">I2PTunnel page</a>.
+ <p>
+ <table>
+ <tr><td>Dest. name, hash, or full key:
+ <td><textarea name="peer" cols="44" rows="1" wrap="off"></textarea>
+ <tr><td align="right">Session Key:
+ <td><input type="text" size="55" name="key" />
+ <tr><td><td><input type="submit" name="action" value="Add key" />
+ </table>
+ </form>
+
+
+</div>
+
+</body>
+</html>
diff --git a/apps/routerconsole/jsp/confignav.jsp b/apps/routerconsole/jsp/confignav.jsp
index b6a5ce6df3b5b317799bfa69dc7ef542a0f97bd9..851ab79b5038edf07ad0151458ced47c8b673ffd 100644
--- a/apps/routerconsole/jsp/confignav.jsp
+++ b/apps/routerconsole/jsp/confignav.jsp
@@ -10,6 +10,8 @@
  %>Clients | <% } else { %><a href="configclients.jsp">Clients</a> | <% }
  if (request.getRequestURI().indexOf("configpeer.jsp") != -1) {
  %>Peers | <% } else { %><a href="configpeer.jsp">Peers</a> | <% }
+ if (request.getRequestURI().indexOf("configkeyring.jsp") != -1) {
+ %>Keyring | <% } else { %><a href="configkeyring.jsp">Keyring</a> | <% }
  if (request.getRequestURI().indexOf("configlogging.jsp") != -1) {
  %>Logging | <% } else { %><a href="configlogging.jsp">Logging</a> | <% }
  if (request.getRequestURI().indexOf("configstats.jsp") != -1) {
diff --git a/apps/routerconsole/jsp/help.jsp b/apps/routerconsole/jsp/help.jsp
index 8580f6e653907177c5bd0ddd1bb269951994b395..d53f93a885bffc821dd7bf1dbc3a30011a9f0a49 100644
--- a/apps/routerconsole/jsp/help.jsp
+++ b/apps/routerconsole/jsp/help.jsp
@@ -34,9 +34,8 @@ licenses and dependencies.  This webpage is being served as part of the I2P rout
 client application, which is built off a trimmed down <a href="http://jetty.mortbay.com/jetty/index.html">Jetty</a>
 instance (trimmed down, as in, we do not include the demo apps or other add-ons, and we simplify configuration), 
 allowing you to deploy standard JSP/Servlet web applications into your router.  Jetty in turn makes use of 
-Apache's javax.servlet (javax.servlet.jar) implementation, as well as their xerces-j XML parser (xerces.jar).
-Their XML parser requires the Sun XML APIs (JAXP) which is included in binary form (xml-apis.jar) as required 
-by their binary code license.  This product includes software developed by the Apache Software Foundation 
+Apache's javax.servlet (javax.servlet.jar) implementation.
+This product includes software developed by the Apache Software Foundation 
 (http://www.apache.org/). </p>
 
 <p>Another application you can see on this webpage is <a href="http://www.i2p2.i2p/i2ptunnel">I2PTunnel</a>
diff --git a/apps/routerconsole/jsp/netdb.jsp b/apps/routerconsole/jsp/netdb.jsp
index 89c2bdec20d5a954e4511bdd07fb69af0f42e7dd..08a1377d37ff35a188da0076466dc0cc95e9b519 100644
--- a/apps/routerconsole/jsp/netdb.jsp
+++ b/apps/routerconsole/jsp/netdb.jsp
@@ -14,6 +14,7 @@
  <jsp:useBean class="net.i2p.router.web.NetDbHelper" id="netdbHelper" scope="request" />
  <jsp:setProperty name="netdbHelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
  <jsp:setProperty name="netdbHelper" property="writer" value="<%=out%>" />
+ <jsp:setProperty name="netdbHelper" property="full" value="<%=request.getParameter("f")%>" />
  <jsp:setProperty name="netdbHelper" property="router" value="<%=request.getParameter("r")%>" />
  <jsp:getProperty name="netdbHelper" property="netDbSummary" />
 </div>
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java
index 184e4a5457e9e7ac6c0185cba8d2c07727ea781b..431540d46c51086a2f988152dcc6aa871fd7bea6 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java
@@ -45,6 +45,7 @@ public class Connection {
     private long _congestionWindowEnd;
     private long _highestAckedThrough;
     private boolean _isInbound;
+    private boolean _updatedShareOpts;
     /** Packet ID (Long) to PacketLocal for sent but unacked packets */
     private Map _outboundPackets;
     private PacketQueue _outboundQueue;
@@ -120,6 +121,7 @@ public class Connection {
         _activeResends = 0;
         _resetSentOn = -1;
         _isInbound = false;
+        _updatedShareOpts = false;
         _connectionEvent = new ConEvent();
 	_hardDisconnected = false;
         _context.statManager().createRateStat("stream.con.windowSizeAtCongestion", "How large was our send window when we send a dup?", "Stream", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
@@ -586,6 +588,8 @@ public class Connection {
         if (_remotePeerSet) throw new RuntimeException("Remote peer already set [" + _remotePeer + ", " + peer + "]");
         _remotePeerSet = true;
         _remotePeer = peer; 
+        // now that we know who the other end is, get the rtt etc. from the cache
+        _connectionManager.updateOptsFromShare(this);
     }
     
     private boolean _sendStreamIdSet = false;
@@ -709,7 +713,13 @@ public class Connection {
     }
     public long getCloseReceivedOn() { return _closeReceivedOn; }
     public void setCloseReceivedOn(long when) { _closeReceivedOn = when; }
-    
+
+    public void updateShareOpts() {
+        if (_closeSentOn > 0 && !_updatedShareOpts) {
+            _connectionManager.updateShareOpts(this);
+            _updatedShareOpts = true;
+        }
+    }
     public void incrementUnackedPacketsReceived() { _unackedPacketsReceived++; }
     public int getUnackedPacketsReceived() { return _unackedPacketsReceived; }
     /** how many packets have we sent but not yet received an ACK for?
@@ -998,7 +1008,7 @@ public class Connection {
     /**
      * Coordinate the resends of a given packet
      */
-    private class ResendPacketEvent implements SimpleTimer.TimedEvent {
+    public class ResendPacketEvent implements SimpleTimer.TimedEvent {
         private PacketLocal _packet;
         private long _nextSendTime;
         public ResendPacketEvent(PacketLocal packet, long sendTime) {
@@ -1104,7 +1114,22 @@ public class Connection {
                     _context.sessionKeyManager().failTags(_remotePeer.getPublicKey());
                 }
                 
-                if (numSends - 1 <= _options.getMaxResends()) {
+                if (numSends - 1 > _options.getMaxResends()) {
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug("Too many resends");
+                    _packet.cancelled();
+                    disconnect(false);
+                } else {
+                    //long timeout = _options.getResendDelay() << numSends;
+                    long rto = _options.getRTO();
+                    if (rto < MIN_RESEND_DELAY)
+                        rto = MIN_RESEND_DELAY;
+                    long timeout = rto << (numSends-1);
+                    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();
+
                     if (_log.shouldLog(Log.INFO))
                         _log.info("Resend packet " + _packet + " time " + numSends + 
                                   " activeResends: " + _activeResends + 
@@ -1113,6 +1138,10 @@ public class Connection {
                                   + (_context.clock().now() - _packet.getCreatedOn()) + "ms)");
                     _outboundQueue.enqueue(_packet);
                     _lastSendTime = _context.clock().now();
+
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug("Scheduling resend in " + timeout + "ms for " + _packet);
+                    RetransmissionTimer.getInstance().addEvent(ResendPacketEvent.this, timeout);
                 }
                 
                 // acked during resending (... or somethin')
@@ -1123,25 +1152,7 @@ public class Connection {
                     }
                     return true;
                 }
-                
-                if (numSends - 1 > _options.getMaxResends()) {
-                    if (_log.shouldLog(Log.DEBUG))
-                        _log.debug("Too many resends");
-                    _packet.cancelled();
-                    disconnect(false);
-                } else {
-                    //long timeout = _options.getResendDelay() << numSends;
-                    long rto = _options.getRTO();
-                    if (rto < MIN_RESEND_DELAY)
-                        rto = MIN_RESEND_DELAY;
-                    long timeout = rto << (numSends-1);
-                    if ( (timeout > MAX_RESEND_DELAY) || (timeout <= 0) )
-                        timeout = MAX_RESEND_DELAY;
-                    if (_log.shouldLog(Log.DEBUG))
-                        _log.debug("Scheduling resend in " + timeout + "ms for " + _packet);
-                    RetransmissionTimer.getInstance().addEvent(ResendPacketEvent.this, timeout);
-                    _nextSendTime = timeout + _context.clock().now();
-                }
+
                 return true;
             } else {
                 //if (_log.shouldLog(Log.DEBUG))
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionManager.java b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionManager.java
index da2b1ab1279ef04016477ce75115edf3a8d17793..7826ba2a81c0d86d48ce235ddedaa96c07d51994 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionManager.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionManager.java
@@ -30,6 +30,7 @@ public class ConnectionManager {
     private PacketQueue _outboundQueue;
     private SchedulerChooser _schedulerChooser;
     private ConnectionPacketHandler _conPacketHandler;
+    private TCBShare _tcbShare;
     /** Inbound stream ID (Long) to Connection map */
     private Map _connectionByInboundId;
     /** Ping ID (Long) to PingRequest */
@@ -52,6 +53,7 @@ public class ConnectionManager {
         _connectionHandler = new ConnectionHandler(context, this);
         _schedulerChooser = new SchedulerChooser(context);
         _conPacketHandler = new ConnectionPacketHandler(context);
+        _tcbShare = new TCBShare(context);
         _session = session;
         session.setSessionListener(_messageHandler);
         _outboundQueue = new PacketQueue(context, session, this);
@@ -127,6 +129,7 @@ public class ConnectionManager {
      */
     public Connection receiveConnection(Packet synPacket) {
         Connection con = new Connection(_context, this, _schedulerChooser, _outboundQueue, _conPacketHandler, new ConnectionOptions(_defaultOptions));
+        _tcbShare.updateOptsFromShare(con);
         con.setInbound();
         long receiveId = _context.random().nextLong(Packet.MAX_STREAM_ID-1)+1;
         boolean reject = false;
@@ -277,6 +280,8 @@ public class ConnectionManager {
     public ConnectionHandler getConnectionHandler() { return _connectionHandler; }
     public I2PSession getSession() { return _session; }
     public PacketQueue getPacketQueue() { return _outboundQueue; }
+    public void updateOptsFromShare(Connection con) { _tcbShare.updateOptsFromShare(con); }
+    public void updateShareOpts(Connection con) { _tcbShare.updateShareOpts(con); }
     
     /**
      * Something b0rked hard, so kill all of our connections without mercy.
@@ -292,6 +297,7 @@ public class ConnectionManager {
             _connectionByInboundId.clear();
             _connectionLock.notifyAll();
         }
+        _tcbShare.stop();
     }
     
     /**
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java
index 6a062d4a6c73f9e0daa04c002c690942c32806fd..7c445f0380140cb12199061d579da758cf260aa3 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java
@@ -213,6 +213,10 @@ public class ConnectionPacketHandler {
             packet.releasePayload();
         }
         
+        // update the TCB Cache now that we've processed the acks and updated our rtt etc.
+        if (isNew && packet.isFlagSet(Packet.FLAG_CLOSE) && packet.isFlagSet(Packet.FLAG_SIGNATURE_INCLUDED))
+            con.updateShareOpts();
+
         //if (choke)
         //    con.fastRetransmit();
     }
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java b/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java
index 2d22226d377f96dafab664a2f7e7eddc81388c2b..a56e7753dd83060398bc4cff9faf192c084e857d 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java
@@ -82,7 +82,16 @@ class PacketQueue {
             
             // this should not block!
             begin = _context.clock().now();
-            sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent);
+            long expires = 0;
+            Connection.ResendPacketEvent rpe = (Connection.ResendPacketEvent) packet.getResendEvent();
+            if (rpe != null)
+                // we want the router to expire it a little before we do,
+                // so if we retransmit it will use a new tunnel/lease combo
+                expires = rpe.getNextSendTime() - 500;
+            if (expires > 0)
+                sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent, expires);
+            else
+                sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent);
             end = _context.clock().now();
             
             if ( (end-begin > 1000) && (_log.shouldLog(Log.WARN)) ) 
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/TCBShare.java b/apps/streaming/java/src/net/i2p/client/streaming/TCBShare.java
new file mode 100644
index 0000000000000000000000000000000000000000..1562f948e5d4f1fe2b68777ee12dbf20c9b9fba3
--- /dev/null
+++ b/apps/streaming/java/src/net/i2p/client/streaming/TCBShare.java
@@ -0,0 +1,137 @@
+package net.i2p.client.streaming;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.Destination;
+import net.i2p.util.Log;
+import net.i2p.util.SimpleTimer;
+
+/**
+ *  Share important TCP Control Block parameters across Connections
+ *  to the same remote peer.
+ *  This is intended for "temporal" sharing at connection open/close time,
+ *  not "ensemble" sharing during a connection. Ref. RFC 2140.
+ *  
+ *  There is a TCB share per ConnectionManager (i.e. per local Destination)
+ *  so that there is no information leakage to other Destinations on the
+ *  same router.
+ *
+ */
+public class TCBShare {
+    private I2PAppContext _context;
+    private Log _log;
+    private Map<Destination, Entry> _cache;
+    private CleanEvent _cleaner;
+
+    private static final long EXPIRE_TIME = 30*60*1000;
+    private static final long CLEAN_TIME = 10*60*1000;
+    private static final double RTT_DAMPENING = 0.75;
+    private static final double WDW_DAMPENING = 0.75;
+    private static final int MAX_RTT = ((int) Connection.MAX_RESEND_DELAY) / 2;
+    private static final int MAX_WINDOW_SIZE = Connection.MAX_WINDOW_SIZE / 4;
+    
+    public TCBShare(I2PAppContext ctx) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(TCBShare.class);
+        _cache = new ConcurrentHashMap(4);
+        _cleaner = new CleanEvent();
+        SimpleTimer.getInstance().addEvent(_cleaner, CLEAN_TIME);
+    }
+
+    public void stop() {
+        SimpleTimer.getInstance().removeEvent(_cleaner);
+    }
+
+    public void updateOptsFromShare(Connection con) {
+        Destination dest = con.getRemotePeer();
+        if (dest == null)
+            return;
+        ConnectionOptions opts = con.getOptions();
+        if (opts == null)
+            return;
+        Entry e = _cache.get(dest);
+        if (e == null || e.isExpired())
+            return;
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("From cache: " +
+                       con.getSession().getMyDestination().calculateHash().toBase64().substring(0, 4) +
+                       '-' +
+                       dest.calculateHash().toBase64().substring(0, 4) +
+                       " RTT: " + e.getRTT() + " wdw: " + e.getWindowSize());
+        opts.setRTT(e.getRTT());
+        opts.setWindowSize(e.getWindowSize());
+    }
+
+    public void updateShareOpts(Connection con) {
+        Destination dest = con.getRemotePeer();
+        if (dest == null)
+            return;
+        if (con.getAckedPackets() <= 0)
+            return;
+        ConnectionOptions opts = con.getOptions();
+        if (opts == null)
+            return;
+        int old = -1;
+        int oldw = -1;
+        Entry e = _cache.get(dest);
+        if (e == null || e.isExpired()) {
+            e = new Entry(opts.getRTT(), opts.getWindowSize());
+            _cache.put(dest, e);
+        } else {
+            old = e.getRTT();
+            oldw = e.getWindowSize();
+            e.setRTT(opts.getRTT());
+            e.setWindowSize(opts.getWindowSize());
+        }
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("To cache: " +
+                       con.getSession().getMyDestination().calculateHash().toBase64().substring(0, 4) +
+                       '-' +
+                       dest.calculateHash().toBase64().substring(0, 4) +
+                       " old: " + old + " con: " + opts.getRTT() + " new: " + e.getRTT() +
+                       " oldw: " + oldw + " conw: " + opts.getWindowSize() + " neww: " + e.getWindowSize());
+    }
+
+    private class Entry {
+        int _rtt;
+        int _wdw;
+        long _updated;
+
+        public Entry(int ms, int wdw) {
+            _rtt = ms;
+            _wdw = wdw;
+            _updated = _context.clock().now();
+        }
+        public int getRTT() { return _rtt; }
+        public void setRTT(int ms) {
+            _rtt = (int)(RTT_DAMPENING*_rtt + (1-RTT_DAMPENING)*ms);        
+            if (_rtt > MAX_RTT)
+                _rtt = MAX_RTT;
+            _updated = _context.clock().now();
+        }
+        public int getWindowSize() { return _wdw; }
+        public void setWindowSize(int wdw) {
+            _wdw = (int)(0.5 + WDW_DAMPENING*_wdw + (1-WDW_DAMPENING)*wdw);       
+            if (_wdw > MAX_WINDOW_SIZE)
+                _wdw = MAX_WINDOW_SIZE;
+            _updated = _context.clock().now();
+        }
+        public boolean isExpired() {
+            return _updated < _context.clock().now() - EXPIRE_TIME;
+        }
+    }
+
+    private class CleanEvent implements SimpleTimer.TimedEvent {
+        public CleanEvent() {}
+        public void timeReached() {
+            for (Iterator iter = _cache.keySet().iterator(); iter.hasNext(); ) {
+                if (_cache.get(iter.next()).isExpired())
+                    iter.remove();
+            }
+            SimpleTimer.getInstance().addEvent(CleanEvent.this, CLEAN_TIME);
+        }
+    }
+}
diff --git a/build.xml b/build.xml
index f9a015a6ef89bfdacdc008c6b15c191783197d34..9b69a13f35c6740a5df2fa9a22d928b39f01b075 100644
--- a/build.xml
+++ b/build.xml
@@ -60,7 +60,6 @@
         <copy file="apps/jetty/jettylib/jasper-runtime.jar" todir="build/" />
         <copy file="apps/jetty/jettylib/commons-logging.jar" todir="build/" />
         <copy file="apps/jetty/jettylib/commons-el.jar" todir="build/" />
-        <copy file="apps/jetty/jettylib/xercesImpl.jar" todir="build/" />
         <copy file="apps/jetty/jettylib/javax.servlet.jar" todir="build/" />
     </target>
     <target name="buildexe">
@@ -87,7 +86,7 @@
         <jar destfile="./build/launchi2p.jar">
             <manifest>
 	     <attribute name="Main-Class" value="net.i2p.router.RouterLaunch" />
-	     <attribute name="Class-Path" value="lib/i2p.jar lib/router.jar lib/jbigi.jar lib/BOB.jar lib/sam.jar lib/mstreaming.jar lib/streaming.jar lib/routerconsole.jar lib/i2ptunnel.jar lib/org.mortbay.jetty.jar lib/javax.servlet.jar lib/jasper-compiler.jar lib/jasper-runtime.jar lib/commons-logging.jar lib/commons-el.jar lib/ant.jar lib/xercesImpl.jar lib/wrapper.jar lib/systray.jar lib/systray4j.jar" />
+	     <attribute name="Class-Path" value="lib/i2p.jar lib/router.jar lib/jbigi.jar lib/BOB.jar lib/sam.jar lib/mstreaming.jar lib/streaming.jar lib/routerconsole.jar lib/i2ptunnel.jar lib/org.mortbay.jetty.jar lib/javax.servlet.jar lib/jasper-compiler.jar lib/jasper-runtime.jar lib/commons-logging.jar lib/commons-el.jar lib/ant.jar lib/wrapper.jar lib/systray.jar lib/systray4j.jar" />
 	    </manifest>
         </jar>
 	<!-- now the standalone launcher exe -->
@@ -219,7 +218,6 @@
         <copy file="apps/systray/java/lib/systray4j.dll" todir="pkg-temp/lib" />
         <copy file="apps/systray/java/resources/iggy.ico" todir="pkg-temp/icons" />
         <copy file="apps/systray/java/resources/iggy.xpm" todir="pkg-temp/icons" />
-        <copy file="build/xercesImpl.jar" todir="pkg-temp/lib/" />
         <copy file="build/i2ptunnel.war" todir="pkg-temp/webapps/" />
         <copy file="build/routerconsole.war" todir="pkg-temp/webapps/" />
         <copy file="build/addressbook.war" todir="pkg-temp/webapps/" />
@@ -371,17 +369,16 @@
         <copy file="build/commons-el.jar" todir="pkg-temp/lib/" />
         <copy file="build/javax.servlet.jar" todir="pkg-temp/lib/" />
         <copy file="build/org.mortbay.jetty.jar" todir="pkg-temp/lib/" />
-        <copy file="build/xercesImpl.jar" todir="pkg-temp/lib/" />
     </target>
     <target name="installer" depends="preppkg">
         <taskdef name="izpack" classpath="${basedir}/installer/lib/izpack/standalone-compiler.jar" classname="com.izforge.izpack.ant.IzPackTask" />
-        <jar destfile="./pkg-temp/lib/copy.jar" basedir="./core/java/build/obj" includes="net/i2p/util/*.class">
+        <jar destfile="./pkg-temp/lib/copy.jar" basedir="./core/java/build/obj" includes="net/i2p/util/Copy.class net/i2p/util/FileUtil.class">
             <manifest><attribute name="Main-Class" value="net.i2p.util.Copy" /></manifest>
         </jar>
-        <jar destfile="./pkg-temp/lib/delete.jar" basedir="./core/java/build/obj" includes="net/i2p/util/*.class">
+        <jar destfile="./pkg-temp/lib/delete.jar" basedir="./core/java/build/obj" includes="net/i2p/util/Delete.class net/i2p/util/FileUtil.class">
             <manifest><attribute name="Main-Class" value="net.i2p.util.Delete" /></manifest>
         </jar>
-        <jar destfile="./pkg-temp/lib/exec.jar" basedir="./core/java/build/obj" includes="net/i2p/util/*.class">
+        <jar destfile="./pkg-temp/lib/exec.jar" basedir="./core/java/build/obj" includes="net/i2p/util/Exec.class">
             <manifest><attribute name="Main-Class" value="net.i2p.util.Exec" /></manifest>
         </jar>
         <izpack input="${basedir}/installer/install.xml" output="${basedir}/install.jar" installerType="standard" basedir="${basedir}" />
@@ -452,7 +449,7 @@
             <arg value="-output"/>
             <arg value="findbugs.xml"/>
             <arg value="-auxclasspath"/>
-            <arg value="build/ant.jar:build/commons-el.jar:build/commons-logging.jar:build/jasper-compiler.jar:build/jasper-runtime.jar:build/javax.servlet.jar:build/org.mortbay.jetty.jar:apps/jrobin/jrobin-1.4.0.jar:apps/systray/java/lib/systray4j.jar:installer/lib/wrapper/linux/wrapper.jar:build/xercesImpl.jar"/>
+            <arg value="build/ant.jar:build/commons-el.jar:build/commons-logging.jar:build/jasper-compiler.jar:build/jasper-runtime.jar:build/javax.servlet.jar:build/org.mortbay.jetty.jar:apps/jrobin/jrobin-1.4.0.jar:apps/systray/java/lib/systray4j.jar:installer/lib/wrapper/linux/wrapper.jar"/>
             <arg value="-sourcepath"/>
             <arg value="apps/BOB/src/:apps/addressbook/java/src/:apps/i2psnark/java/src/:apps/i2ptunnel/java/src/:apps/ministreaming/java/src/:apps/routerconsole/java/src/:apps/sam/java/src/:apps/streaming/java/src/:apps/susidns/src/java/src/:apps/susimail/src/src/:apps/systray/java/src/:core/java/src/:router/java/src/"/>
             <!-- start of the files to be analyzed -->
diff --git a/core/java/src/net/i2p/I2PAppContext.java b/core/java/src/net/i2p/I2PAppContext.java
index 3e514ea18b3e9db7fdfa3fb06c944a73b9e9ccd0..6b3b0fd5bf3fcb6eee1de0be897febe0ebf250c9 100644
--- a/core/java/src/net/i2p/I2PAppContext.java
+++ b/core/java/src/net/i2p/I2PAppContext.java
@@ -24,6 +24,7 @@ import net.i2p.data.RoutingKeyGenerator;
 import net.i2p.stat.StatManager;
 import net.i2p.util.Clock;
 import net.i2p.util.FortunaRandomSource;
+import net.i2p.util.KeyRing;
 import net.i2p.util.LogManager;
 import net.i2p.util.PooledRandomSource;
 import net.i2p.util.RandomSource;
@@ -75,6 +76,7 @@ public class I2PAppContext {
     private RoutingKeyGenerator _routingKeyGenerator;
     private RandomSource _random;
     private KeyGenerator _keyGenerator;
+    protected KeyRing _keyRing; // overridden in RouterContext
     private volatile boolean _statManagerInitialized;
     private volatile boolean _sessionKeyManagerInitialized;
     private volatile boolean _namingServiceInitialized;
@@ -91,6 +93,7 @@ public class I2PAppContext {
     private volatile boolean _routingKeyGeneratorInitialized;
     private volatile boolean _randomInitialized;
     private volatile boolean _keyGeneratorInitialized;
+    protected volatile boolean _keyRingInitialized; // used in RouterContext
     
     
     /**
@@ -141,12 +144,14 @@ public class I2PAppContext {
         _elGamalEngine = null;
         _elGamalAESEngine = null;
         _logManager = null;
+        _keyRing = null;
         _statManagerInitialized = false;
         _sessionKeyManagerInitialized = false;
         _namingServiceInitialized = false;
         _elGamalEngineInitialized = false;
         _elGamalAESEngineInitialized = false;
         _logManagerInitialized = false;
+        _keyRingInitialized = false;
     }
     
     /**
@@ -512,6 +517,23 @@ public class I2PAppContext {
         }
     }
     
+    /**
+     * Basic hash map
+     */
+    public KeyRing keyRing() {
+        if (!_keyRingInitialized)
+            initializeKeyRing();
+        return _keyRing;
+    }
+
+    protected void initializeKeyRing() {
+        synchronized (this) {
+            if (_keyRing == null)
+                _keyRing = new KeyRing();
+            _keyRingInitialized = true;
+        }
+    }
+    
     /**
      * [insert snarky comment here]
      *
diff --git a/core/java/src/net/i2p/client/I2CPMessageProducer.java b/core/java/src/net/i2p/client/I2CPMessageProducer.java
index 9af1fbd19ccb3ea2d40bbdeb739752ac8f5d2691..5b45ee7a362cda72097d00fac8e514f13096b37d 100644
--- a/core/java/src/net/i2p/client/I2CPMessageProducer.java
+++ b/core/java/src/net/i2p/client/I2CPMessageProducer.java
@@ -9,6 +9,7 @@ package net.i2p.client;
  *
  */
 
+import java.util.Date;
 import java.util.Set;
 
 import net.i2p.I2PAppContext;
@@ -28,6 +29,7 @@ import net.i2p.data.i2cp.DestroySessionMessage;
 import net.i2p.data.i2cp.MessageId;
 import net.i2p.data.i2cp.ReportAbuseMessage;
 import net.i2p.data.i2cp.SendMessageMessage;
+import net.i2p.data.i2cp.SendMessageExpiresMessage;
 import net.i2p.data.i2cp.SessionConfig;
 import net.i2p.util.Log;
 
@@ -91,8 +93,13 @@ class I2CPMessageProducer {
      *
      */
     public void sendMessage(I2PSessionImpl session, Destination dest, long nonce, byte[] payload, SessionTag tag,
-                            SessionKey key, Set tags, SessionKey newKey) throws I2PSessionException {
-        SendMessageMessage msg = new SendMessageMessage();
+                            SessionKey key, Set tags, SessionKey newKey, long expires) throws I2PSessionException {
+        SendMessageMessage msg;
+        if (expires > 0) {
+            msg = new SendMessageExpiresMessage();
+            ((SendMessageExpiresMessage)msg).setExpiration(new Date(expires));
+        } else
+            msg = new SendMessageMessage();
         msg.setDestination(dest);
         msg.setSessionId(session.getSessionId());
         msg.setNonce(nonce);
diff --git a/core/java/src/net/i2p/client/I2PSession.java b/core/java/src/net/i2p/client/I2PSession.java
index 627d1775a5f6e7943428e43346c9be5a0ca640ee..d8c64f2222406a39710e32fee3bb91077e9467d1 100644
--- a/core/java/src/net/i2p/client/I2PSession.java
+++ b/core/java/src/net/i2p/client/I2PSession.java
@@ -70,6 +70,7 @@ public interface I2PSession {
      */
     public boolean sendMessage(Destination dest, byte[] payload, SessionKey keyUsed, Set tagsSent) throws I2PSessionException;
     public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent) throws I2PSessionException;
+    public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent, long expire) throws I2PSessionException;
 
     /** Receive a message that the router has notified the client about, returning
      * the payload.
diff --git a/core/java/src/net/i2p/client/I2PSessionImpl.java b/core/java/src/net/i2p/client/I2PSessionImpl.java
index 78f4ba763c4682b38678d9f4016cc60d516718eb..a57957107a8d29380cee2ce7be4c7428ab331dd0 100644
--- a/core/java/src/net/i2p/client/I2PSessionImpl.java
+++ b/core/java/src/net/i2p/client/I2PSessionImpl.java
@@ -550,10 +550,10 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
      * Pass off the error to the listener
      */
     void propogateError(String msg, Throwable error) {
-        if (_log.shouldLog(Log.WARN)) 
-            _log.warn(getPrefix() + "Error occurred: " + msg + " - " + error.getMessage());
-        if (_log.shouldLog(Log.WARN))
-            _log.warn(getPrefix() + " cause", error);
+        if (_log.shouldLog(Log.ERROR)) 
+            _log.error(getPrefix() + "Error occurred: " + msg + " - " + error.getMessage());
+        if (_log.shouldLog(Log.ERROR))
+            _log.error(getPrefix() + " cause", error);
         
         if (_sessionListener != null) _sessionListener.errorOccurred(this, msg, error);
     }
diff --git a/core/java/src/net/i2p/client/I2PSessionImpl2.java b/core/java/src/net/i2p/client/I2PSessionImpl2.java
index 81c6ef22f066d670fe7e00530e124d9131e071bd..6a90952a52f2bcf1ec5312347f36c768e08da083 100644
--- a/core/java/src/net/i2p/client/I2PSessionImpl2.java
+++ b/core/java/src/net/i2p/client/I2PSessionImpl2.java
@@ -107,15 +107,19 @@ class I2PSessionImpl2 extends I2PSessionImpl {
         return sendMessage(dest, payload, 0, payload.length);
     }
     public boolean sendMessage(Destination dest, byte[] payload, int offset, int size) throws I2PSessionException {
-        return sendMessage(dest, payload, offset, size, new SessionKey(), new HashSet(64));
+        return sendMessage(dest, payload, offset, size, new SessionKey(), new HashSet(64), 0);
     }
     
     @Override
     public boolean sendMessage(Destination dest, byte[] payload, SessionKey keyUsed, Set tagsSent) throws I2PSessionException {
-        return sendMessage(dest, payload, 0, payload.length, keyUsed, tagsSent);
+        return sendMessage(dest, payload, 0, payload.length, keyUsed, tagsSent, 0);
     }
     public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent)
                    throws I2PSessionException {
+        return sendMessage(dest, payload, offset, size, keyUsed, tagsSent, 0);
+    }
+    public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent, long expires)
+                   throws I2PSessionException {
         if (_log.shouldLog(Log.DEBUG)) _log.debug("sending message");
         if (isClosed()) throw new I2PSessionException("Already closed");
 
@@ -142,7 +146,7 @@ class I2PSessionImpl2 extends I2PSessionImpl {
         }
         _context.statManager().addRateData("i2cp.tx.msgCompressed", compressed, 0);
         _context.statManager().addRateData("i2cp.tx.msgExpanded", size, 0);
-        return sendBestEffort(dest, payload, keyUsed, tagsSent);
+        return sendBestEffort(dest, payload, keyUsed, tagsSent, expires);
     }
 
     /**
@@ -168,7 +172,7 @@ class I2PSessionImpl2 extends I2PSessionImpl {
     
     private static final int NUM_TAGS = 50;
 
-    private boolean sendBestEffort(Destination dest, byte payload[], SessionKey keyUsed, Set tagsSent)
+    private boolean sendBestEffort(Destination dest, byte payload[], SessionKey keyUsed, Set tagsSent, long expires)
                     throws I2PSessionException {
         SessionKey key = null;
         SessionKey newKey = null;
@@ -176,6 +180,7 @@ class I2PSessionImpl2 extends I2PSessionImpl {
         Set sentTags = null;
         int oldTags = 0;
         long begin = _context.clock().now();
+        /***********
         if (I2CPMessageProducer.END_TO_END_CRYPTO) {
             if (_log.shouldLog(Log.DEBUG)) _log.debug("begin sendBestEffort");
             key = _context.sessionKeyManager().getCurrentKey(dest.getPublicKey());
@@ -220,6 +225,7 @@ class I2PSessionImpl2 extends I2PSessionImpl {
         } else {
             // not using end to end crypto, so don't ever bundle any tags
         }
+        **********/
         
         if (_log.shouldLog(Log.DEBUG)) _log.debug("before creating nonce");
         
@@ -233,14 +239,14 @@ class I2PSessionImpl2 extends I2PSessionImpl {
         if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Setting key = " + key);
 
         if (keyUsed != null) {
-            if (I2CPMessageProducer.END_TO_END_CRYPTO) {
-                if (newKey != null)
-                    keyUsed.setData(newKey.getData());
-                else
-                    keyUsed.setData(key.getData());
-            } else {
+            //if (I2CPMessageProducer.END_TO_END_CRYPTO) {
+            //    if (newKey != null)
+            //        keyUsed.setData(newKey.getData());
+            //    else
+            //        keyUsed.setData(key.getData());
+            //} else {
                 keyUsed.setData(SessionKey.INVALID_KEY.getData());
-            }
+            //}
         }
         if (tagsSent != null) {
             if (sentTags != null) {
@@ -261,7 +267,7 @@ class I2PSessionImpl2 extends I2PSessionImpl {
                        + state.getNonce() + " for best effort "
                        + " sync took " + (inSendingSync-beforeSendingSync) 
                        + " add took " + (afterSendingSync-inSendingSync));
-        _producer.sendMessage(this, dest, nonce, payload, tag, key, sentTags, newKey);
+        _producer.sendMessage(this, dest, nonce, payload, tag, key, sentTags, newKey, expires);
         
         // since this is 'best effort', all we're waiting for is a status update 
         // saying that the router received it - in theory, that should come back
diff --git a/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java b/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java
index 7d6d816c1900d5c9227daf7fdcd3bd89c2be0f3b..6163771e366c90d6cd68b45fe838f15707485202 100644
--- a/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java
+++ b/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java
@@ -21,6 +21,7 @@ import net.i2p.data.Lease;
 import net.i2p.data.LeaseSet;
 import net.i2p.data.PrivateKey;
 import net.i2p.data.PublicKey;
+import net.i2p.data.SessionKey;
 import net.i2p.data.SigningPrivateKey;
 import net.i2p.data.SigningPublicKey;
 import net.i2p.data.i2cp.I2CPMessage;
@@ -78,6 +79,17 @@ class RequestLeaseSetMessageHandler extends HandlerImpl {
 
         leaseSet.setEncryptionKey(li.getPublicKey());
         leaseSet.setSigningKey(li.getSigningPublicKey());
+        String sk = session.getOptions().getProperty("i2cp.sessionKey");
+        if (sk != null) {
+            SessionKey key = new SessionKey();
+            try {
+                key.fromBase64(sk);
+                leaseSet.encrypt(key);
+                _context.keyRing().put(session.getMyDestination().calculateHash(), key);
+            } catch (DataFormatException dfe) {
+                _log.error("Bad session key: " + sk);
+            }
+        }
         try {
             leaseSet.sign(session.getPrivateKey());
             session.getProducer().createLeaseSet(session, leaseSet, li.getSigningPrivateKey(), li.getPrivateKey());
@@ -137,4 +149,4 @@ class RequestLeaseSetMessageHandler extends HandlerImpl {
                    && DataHelper.eq(_signingPrivKey, li.getSigningPrivateKey());
         }
     }
-}
\ No newline at end of file
+}
diff --git a/core/java/src/net/i2p/data/DataHelper.java b/core/java/src/net/i2p/data/DataHelper.java
index 835e6a0dd74bb84ae47b72cddbd1928dbbd4c464..4a074f17cf9bdaefe2b59a8b90f16a48651a20a5 100644
--- a/core/java/src/net/i2p/data/DataHelper.java
+++ b/core/java/src/net/i2p/data/DataHelper.java
@@ -25,6 +25,7 @@ import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.io.UnsupportedEncodingException;
 import java.math.BigInteger;
+import java.text.DecimalFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -235,7 +236,7 @@ public class DataHelper {
                 int split = line.indexOf('=');
                 if (split <= 0) continue;
                 String key = line.substring(0, split);
-                String val = line.substring(split+1);
+                String val = line.substring(split+1);   //.trim() ??????????????
                 // Unescape line breaks after loading.
                 // Remember: "\" needs escaping both for regex and string.
                 val = val.replaceAll("\\\\r","\r");
@@ -842,6 +843,29 @@ public class DataHelper {
         }
     }
     
+    /**
+     * Caller should append 'B' or 'b' as appropriate
+     */
+    public static String formatSize(long bytes) {
+        double val = bytes;
+        int scale = 0;
+        while (val >= 1024) {
+            scale++; 
+            val /= 1024;
+        }
+        
+        DecimalFormat fmt = new DecimalFormat("##0.00");
+
+        String str = fmt.format(val);
+        switch (scale) {
+            case 1: return str + "K";
+            case 2: return str + "M";
+            case 3: return str + "G";
+            case 4: return str + "T";
+            default: return bytes + "";
+        }
+    }
+    
     /**
      * Strip out any HTML (simply removing any less than / greater than symbols)
      */
diff --git a/core/java/src/net/i2p/data/LeaseSet.java b/core/java/src/net/i2p/data/LeaseSet.java
index 7dd74a9d7cbd0f1954d8f3b33112bc9d74d1cd37..8a05dd9569bb744599f7d0e8dc1e51d8e6a786f4 100644
--- a/core/java/src/net/i2p/data/LeaseSet.java
+++ b/core/java/src/net/i2p/data/LeaseSet.java
@@ -9,6 +9,7 @@ package net.i2p.data;
  *
  */
 
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -17,13 +18,34 @@ import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
 
+import net.i2p.I2PAppContext;
 import net.i2p.crypto.DSAEngine;
 import net.i2p.util.Clock;
 import net.i2p.util.Log;
+import net.i2p.util.RandomSource;
 
 /**
  * Defines the set of leases a destination currently has.
  *
+ * Support encryption and decryption with a supplied key.
+ * Only the gateways and tunnel IDs in the individual
+ * leases are encrypted.
+ *
+ * Encrypted leases are not indicated as such.
+ * The only way to tell a lease is encrypted is to
+ * determine that the listed gateways do not exist.
+ * Routers wishing to decrypt a leaseset must have the
+ * desthash and key in their keyring.
+ * This is required for the local router as well, since
+ * the encryption is done on the client side of I2CP, the
+ * router must decrypt it back again for local usage
+ * (but not for transmission to the floodfills)
+ *
+ * Decrypted leases are only available through the getLease()
+ * method, so that storage and network transmission via
+ * writeBytes() will output the original encrypted
+ * leases and the original leaseset signature.
+ *
  * @author jrandom
  */
 public class LeaseSet extends DataStructureImpl {
@@ -40,6 +62,9 @@ public class LeaseSet extends DataStructureImpl {
     // Store these since isCurrent() and getEarliestLeaseDate() are called frequently
     private long _firstExpiration;
     private long _lastExpiration;
+    private List _decryptedLeases;
+    private boolean _decrypted;
+    private boolean _checked;
 
     /** This seems like plenty  */
     private final static int MAX_LEASES = 6;
@@ -55,6 +80,8 @@ public class LeaseSet extends DataStructureImpl {
         _receivedAsPublished = false;
         _firstExpiration = Long.MAX_VALUE;
         _lastExpiration = 0;
+        _decrypted = false;
+        _checked = false;
     }
 
     public Destination getDestination() {
@@ -104,11 +131,17 @@ public class LeaseSet extends DataStructureImpl {
     }
 
     public int getLeaseCount() {
-        return _leases.size();
+        if (isEncrypted())
+            return _leases.size() - 1;
+        else
+            return _leases.size();
     }
 
     public Lease getLease(int index) {
-        return (Lease) _leases.get(index);
+        if (isEncrypted())
+            return (Lease) _decryptedLeases.get(index);
+        else
+            return (Lease) _leases.get(index);
     }
 
     public Signature getSignature() {
@@ -335,4 +368,139 @@ public class LeaseSet extends DataStructureImpl {
         buf.append("]");
         return buf.toString();
     }
+
+    private static final int DATA_LEN = Hash.HASH_LENGTH + 4;
+    private static final int IV_LEN = 16;
+
+    /**
+     *  Encrypt the gateway and tunnel ID of each lease, leaving the expire dates unchanged.
+     *  This adds an extra dummy lease, because AES data must be padded to 16 bytes.
+     *  The fact that it is encrypted is not stored anywhere.
+     *  Must be called after all the leases are in place, but before sign().
+     */
+    public void encrypt(SessionKey key) {
+        if (_log.shouldLog(Log.WARN))
+            _log.warn("encrypting lease: " + _destination.calculateHash());
+        try {
+            encryp(key);
+        } catch (DataFormatException dfe) {
+            _log.error("Error encrypting lease: " + _destination.calculateHash());
+        } catch (IOException ioe) {
+            _log.error("Error encrypting lease: " + _destination.calculateHash());
+        }
+    }
+
+    /**
+     *  - Put the {Gateway Hash, TunnelID} pairs for all the leases in a buffer
+     *  - Pad with random data to a multiple of 16 bytes
+     *  - Use the first part of the dest's public key as an IV
+     *  - Encrypt
+     *  - Pad with random data to a multiple of 36 bytes
+     *  - Add an extra lease
+     *  - Replace the Hash and TunnelID in each Lease
+     */
+    private void encryp(SessionKey key) throws DataFormatException, IOException {
+        int size = _leases.size();
+        if (size < 1 || size > MAX_LEASES-1)
+            throw new IllegalArgumentException("Bad number of leases for encryption");
+        int datalen = ((DATA_LEN * size / 16) + 1) * 16;
+        ByteArrayOutputStream baos = new ByteArrayOutputStream(datalen);
+        for (int i = 0; i < size; i++) {
+            ((Lease)_leases.get(i)).getGateway().writeBytes(baos);
+            ((Lease)_leases.get(i)).getTunnelId().writeBytes(baos);
+        }
+        // pad out to multiple of 16 with random data before encryption
+        int padlen = datalen - (DATA_LEN * size);
+        byte[] pad = new byte[padlen];
+        RandomSource.getInstance().nextBytes(pad);
+        baos.write(pad);
+        byte[] iv = new byte[IV_LEN];
+        System.arraycopy(_destination.getPublicKey().getData(), 0, iv, 0, IV_LEN);
+        byte[] enc = new byte[DATA_LEN * (size + 1)];
+        I2PAppContext.getGlobalContext().aes().encrypt(baos.toByteArray(), 0, enc, 0, key, iv, datalen);
+        // pad out to multiple of 36 with random data after encryption
+        // (even for 4 leases, where 36*4 is a multiple of 16, we add another, just to be consistent)
+        padlen = enc.length - datalen;
+        pad = new byte[padlen];
+        RandomSource.getInstance().nextBytes(pad);
+        System.arraycopy(pad, 0, enc, datalen, padlen);
+        // add the padded lease...
+        Lease padLease = new Lease();
+        padLease.setEndDate(((Lease)_leases.get(0)).getEndDate());
+        _leases.add(padLease);
+        // ...and replace all the gateways and tunnel ids
+        ByteArrayInputStream bais = new ByteArrayInputStream(enc);
+        for (int i = 0; i < size+1; i++) {
+            Hash h = new Hash();
+            h.readBytes(bais);
+            ((Lease)_leases.get(i)).setGateway(h);
+            TunnelId t = new TunnelId();
+            t.readBytes(bais);
+            ((Lease)_leases.get(i)).setTunnelId(t);
+        }
+    }
+
+    /**
+     *  Decrypt the leases, except for the last one which is partially padding.
+     *  Store the new decrypted leases in a backing store,
+     *  and keep the original leases so that verify() still works and the
+     *  encrypted leaseset can be sent on to others (via writeBytes())
+     */
+    private void decrypt(SessionKey key) throws DataFormatException, IOException {
+        if (_log.shouldLog(Log.WARN))
+            _log.warn("decrypting lease: " + _destination.calculateHash());
+        int size = _leases.size();
+        if (size < 2)
+            throw new DataFormatException("Bad number of leases for decryption");
+        int datalen = DATA_LEN * size;
+        ByteArrayOutputStream baos = new ByteArrayOutputStream(datalen);
+        for (int i = 0; i < size; i++) {
+            ((Lease)_leases.get(i)).getGateway().writeBytes(baos);
+            ((Lease)_leases.get(i)).getTunnelId().writeBytes(baos);
+        }
+        byte[] iv = new byte[IV_LEN];
+        System.arraycopy(_destination.getPublicKey().getData(), 0, iv, 0, IV_LEN);
+        int enclen = ((DATA_LEN * (size - 1) / 16) + 1) * 16;
+        byte[] enc = new byte[enclen];
+        System.arraycopy(baos.toByteArray(), 0, enc, 0, enclen);
+        byte[] dec = new byte[enclen];
+        I2PAppContext.getGlobalContext().aes().decrypt(enc, 0, dec, 0, key, iv, enclen);
+        ByteArrayInputStream bais = new ByteArrayInputStream(dec);
+        _decryptedLeases = new ArrayList(size - 1);
+        for (int i = 0; i < size-1; i++) {
+            Lease l = new Lease();
+            Hash h = new Hash();
+            h.readBytes(bais);
+            l.setGateway(h);
+            TunnelId t = new TunnelId();
+            t.readBytes(bais);
+            l.setTunnelId(t);
+            l.setEndDate(((Lease)_leases.get(i)).getEndDate());
+            _decryptedLeases.add(l);
+        }
+    }
+
+    /**
+     * @return true if it was encrypted, and we decrypted it successfully.
+     * Decrypts on first call.
+     */
+    private synchronized boolean isEncrypted() {
+        if (_decrypted)
+           return true;
+        if (_checked || _destination == null)
+           return false;
+        SessionKey key = I2PAppContext.getGlobalContext().keyRing().get(_destination.calculateHash());
+        if (key != null) {
+            try {
+                decrypt(key);
+                _decrypted = true;
+            } catch (DataFormatException dfe) {
+                _log.error("Error decrypting lease: " + _destination.calculateHash() + dfe);
+            } catch (IOException ioe) {
+                _log.error("Error decrypting lease: " + _destination.calculateHash() + ioe);
+            }
+        }
+        _checked = true;
+        return _decrypted;
+    }
 }
diff --git a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java
index 128c312dce4cc1150c4f7f8e54c9c4a50e066fb6..15045028a88eb9b8ba4f0424ab35fdacbbaa4c7d 100644
--- a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java
+++ b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java
@@ -18,7 +18,7 @@ import net.i2p.data.DataHelper;
 import net.i2p.util.Log;
 
 /**
- * Handle messages from the server for the client
+ * Handle messages from the server for the client or vice versa
  *
  */
 public class I2CPMessageHandler {
@@ -75,6 +75,8 @@ public class I2CPMessageHandler {
             return new RequestLeaseSetMessage();
         case SendMessageMessage.MESSAGE_TYPE:
             return new SendMessageMessage();
+        case SendMessageExpiresMessage.MESSAGE_TYPE:
+            return new SendMessageExpiresMessage();
         case SessionStatusMessage.MESSAGE_TYPE:
             return new SessionStatusMessage();
         case GetDateMessage.MESSAGE_TYPE:
diff --git a/core/java/src/net/i2p/data/i2cp/ReconfigureSessionMessage.java b/core/java/src/net/i2p/data/i2cp/ReconfigureSessionMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..7165f6d3271c9d88b085d694bd0249f4a0d4ef59
--- /dev/null
+++ b/core/java/src/net/i2p/data/i2cp/ReconfigureSessionMessage.java
@@ -0,0 +1,103 @@
+package net.i2p.data.i2cp;
+
+/*
+ * 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.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.util.Log;
+
+/**
+ * Defines the message a client sends to a router when
+ * updating the config on an existing session.
+ *
+ * @author zzz
+ */
+public class ReconfigureSessionMessage extends I2CPMessageImpl {
+    private final static Log _log = new Log(ReconfigureSessionMessage.class);
+    public final static int MESSAGE_TYPE = 2;
+    private SessionId _sessionId;
+    private SessionConfig _sessionConfig;
+
+    public ReconfigureSessionMessage() {
+        _sessionId = null;
+        _sessionConfig = null;
+    }
+
+    public SessionId getSessionId() {
+        return _sessionId;
+    }
+
+    public void setSessionId(SessionId id) {
+        _sessionId = id;
+    }
+
+    public SessionConfig getSessionConfig() {
+        return _sessionConfig;
+    }
+
+    public void setSessionConfig(SessionConfig config) {
+        _sessionConfig = config;
+    }
+
+    @Override
+    protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException {
+        try {
+            _sessionId = new SessionId();
+            _sessionId.readBytes(in);
+            _sessionConfig = new SessionConfig();
+            _sessionConfig.readBytes(in);
+        } catch (DataFormatException dfe) {
+            throw new I2CPMessageException("Unable to load the message data", dfe);
+        }
+    }
+
+    @Override
+    protected byte[] doWriteMessage() throws I2CPMessageException, IOException {
+        if (_sessionId == null || _sessionConfig == null)
+            throw new I2CPMessageException("Unable to write out the message as there is not enough data");
+        ByteArrayOutputStream os = new ByteArrayOutputStream(64);
+        try {
+            _sessionId.writeBytes(os);
+            _sessionConfig.writeBytes(os);
+        } catch (DataFormatException dfe) {
+            throw new I2CPMessageException("Error writing out the message data", dfe);
+        }
+        return os.toByteArray();
+    }
+
+    public int getType() {
+        return MESSAGE_TYPE;
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        if ((object != null) && (object instanceof ReconfigureSessionMessage)) {
+            ReconfigureSessionMessage msg = (ReconfigureSessionMessage) object;
+            return DataHelper.eq(getSessionId(), msg.getSessionId())
+                   && DataHelper.eq(getSessionConfig(), msg.getSessionConfig());
+        }
+            
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer buf = new StringBuffer();
+        buf.append("[ReconfigureSessionMessage: ");
+        buf.append("\n\tSessionId: ").append(getSessionId());
+        buf.append("\n\tSessionConfig: ").append(getSessionConfig());
+        buf.append("]");
+        return buf.toString();
+    }
+}
diff --git a/core/java/src/net/i2p/data/i2cp/SendMessageExpiresMessage.java b/core/java/src/net/i2p/data/i2cp/SendMessageExpiresMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..d15c1979c4427f1673856cfe2ecff3cbcf731ccf
--- /dev/null
+++ b/core/java/src/net/i2p/data/i2cp/SendMessageExpiresMessage.java
@@ -0,0 +1,117 @@
+package net.i2p.data.i2cp;
+
+/*
+ * 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.io.InputStream;
+import java.io.OutputStream;
+import java.util.Date;
+
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Destination;
+import net.i2p.data.Payload;
+import net.i2p.util.Log;
+
+/**
+ * Same as SendMessageMessage, but with an expiration to be passed to the router
+ *
+ * @author zzz
+ */
+public class SendMessageExpiresMessage extends SendMessageMessage {
+    private final static Log _log = new Log(SendMessageExpiresMessage.class);
+    public final static int MESSAGE_TYPE = 36;
+    private SessionId _sessionId;
+    private Destination _destination;
+    private Payload _payload;
+    private Date _expiration;
+
+    public SendMessageExpiresMessage() {
+        super();
+        setExpiration(null);
+    }
+
+    public Date getExpiration() {
+        return _expiration;
+    }
+
+    public void setExpiration(Date d) {
+        _expiration = d;
+    }
+
+    /**
+     * Read the body into the data structures
+     *
+     * @throws IOException 
+     */
+    @Override
+    public void readMessage(InputStream in, int length, int type) throws I2CPMessageException, IOException {
+        super.readMessage(in, length, type);
+
+        try {
+            _expiration = DataHelper.readDate(in);
+        } catch (DataFormatException dfe) {
+            throw new I2CPMessageException("Unable to load the message data", dfe);
+        }
+    }
+
+    /**
+     * Write out the full message to the stream, including the 4 byte size and 1 
+     * byte type header.  Override the parent so we can be more mem efficient
+     *
+     * @throws IOException 
+     */
+    @Override
+    public void writeMessage(OutputStream out) throws I2CPMessageException, IOException {
+        if ((getSessionId() == null) || (getDestination() == null) || (getPayload() == null) || (getNonce() <= 0) || (_expiration == null))
+            throw new I2CPMessageException("Unable to write out the message as there is not enough data");
+        int len = 2 + getDestination().size() + getPayload().getSize() + 4 + 4 + DataHelper.DATE_LENGTH;
+        
+        try {
+            DataHelper.writeLong(out, 4, len);
+            DataHelper.writeLong(out, 1, getType());
+            getSessionId().writeBytes(out);
+            getDestination().writeBytes(out);
+            getPayload().writeBytes(out);
+            DataHelper.writeLong(out, 4, getNonce());
+            DataHelper.writeDate(out, _expiration);
+        } catch (DataFormatException dfe) {
+            throw new I2CPMessageException("Error writing the msg", dfe);
+        }
+    }
+    
+    public int getType() {
+        return MESSAGE_TYPE;
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        if ((object != null) && (object instanceof SendMessageExpiresMessage)) {
+            SendMessageExpiresMessage msg = (SendMessageExpiresMessage) object;
+            return super.equals(object)
+                   && DataHelper.eq(getExpiration(), msg.getExpiration());
+        }
+         
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer buf = new StringBuffer();
+        buf.append("[SendMessageMessage: ");
+        buf.append("\n\tSessionId: ").append(getSessionId());
+        buf.append("\n\tNonce: ").append(getNonce());
+        buf.append("\n\tDestination: ").append(getDestination());
+        buf.append("\n\tExpiration: ").append(getExpiration());
+        buf.append("\n\tPayload: ").append(getPayload());
+        buf.append("]");
+        return buf.toString();
+    }
+}
diff --git a/core/java/src/net/i2p/util/KeyRing.java b/core/java/src/net/i2p/util/KeyRing.java
new file mode 100644
index 0000000000000000000000000000000000000000..6bbfb38dee411b7c0ca29f03961b8b906e7dc817
--- /dev/null
+++ b/core/java/src/net/i2p/util/KeyRing.java
@@ -0,0 +1,20 @@
+package net.i2p.util;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+import net.i2p.data.Hash;
+import net.i2p.data.SessionKey;
+
+/**
+ *  simple
+ */
+public class KeyRing extends ConcurrentHashMap<Hash, SessionKey> {
+    public KeyRing() {
+        super(0);
+    }
+
+    public void renderStatusHTML(Writer out) throws IOException {}
+}
diff --git a/installer/resources/wrapper.config b/installer/resources/wrapper.config
index 2550f4e3dc428a27b77f33fc198fdf6ade953e73..177bc1c0bf062427d7148d8c38668d3c039137c4 100644
--- a/installer/resources/wrapper.config
+++ b/installer/resources/wrapper.config
@@ -47,14 +47,13 @@ wrapper.java.classpath.12=lib/jasper-runtime.jar
 wrapper.java.classpath.13=lib/commons-logging.jar
 wrapper.java.classpath.14=lib/commons-el.jar
 wrapper.java.classpath.15=lib/ant.jar
-wrapper.java.classpath.16=lib/xercesImpl.jar
 # java service wrapper, BSD
-wrapper.java.classpath.17=lib/wrapper.jar
+wrapper.java.classpath.16=lib/wrapper.jar
 # systray, LGPL
-wrapper.java.classpath.18=lib/systray.jar
-wrapper.java.classpath.19=lib/systray4j.jar
+wrapper.java.classpath.17=lib/systray.jar
+wrapper.java.classpath.18=lib/systray4j.jar
 # BOB
-wrapper.java.classpath.20=lib/BOB.jar 
+wrapper.java.classpath.19=lib/BOB.jar 
 
 # Java Library Path (location of Wrapper.DLL or libwrapper.so)
 wrapper.java.library.path.1=.
diff --git a/router/java/src/net/i2p/router/ClientMessage.java b/router/java/src/net/i2p/router/ClientMessage.java
index 005f69a2d2580b4806012aa38a7ba7b4242f2e2a..ec7820d696a47e8ff065f216e025b85d208d5c91 100644
--- a/router/java/src/net/i2p/router/ClientMessage.java
+++ b/router/java/src/net/i2p/router/ClientMessage.java
@@ -27,6 +27,7 @@ public class ClientMessage {
     private SessionConfig _senderConfig;
     private Hash _destinationHash;
     private MessageId _messageId;
+    private long _expiration;
     
     public ClientMessage() {
 	setPayload(null);
@@ -36,6 +37,7 @@ public class ClientMessage {
 	setSenderConfig(null);
 	setDestinationHash(null);
 	setMessageId(null);
+        setExpiration(0);
     }
     
     /**
@@ -91,4 +93,12 @@ public class ClientMessage {
      */
     public SessionConfig getSenderConfig() { return _senderConfig; }
     public void setSenderConfig(SessionConfig config) { _senderConfig = config; }
+
+    /**
+     * Expiration requested by the client that sent the message.  This will only be available
+     * for locally originated messages.
+     *
+     */
+    public long getExpiration() { return _expiration; }
+    public void setExpiration(long e) { _expiration = e; }
 }
diff --git a/router/java/src/net/i2p/router/NetworkDatabaseFacade.java b/router/java/src/net/i2p/router/NetworkDatabaseFacade.java
index ed4fe155502df3ef50f7025ade8e5bc58e099fef..e4a5ce08b663a7da1c9fc9752104dc33875bae0a 100644
--- a/router/java/src/net/i2p/router/NetworkDatabaseFacade.java
+++ b/router/java/src/net/i2p/router/NetworkDatabaseFacade.java
@@ -62,4 +62,5 @@ public abstract class NetworkDatabaseFacade implements Service {
     public int getKnownRouters() { return 0; }
     public int getKnownLeaseSets() { return 0; }
     public void renderRouterInfoHTML(Writer out, String s) throws IOException {}
+    public void renderStatusHTML(Writer out, boolean b) throws IOException {}
 }
diff --git a/router/java/src/net/i2p/router/PersistentKeyRing.java b/router/java/src/net/i2p/router/PersistentKeyRing.java
new file mode 100644
index 0000000000000000000000000000000000000000..d02275ea20c72bbb045269d74e52ac1d494ff1df
--- /dev/null
+++ b/router/java/src/net/i2p/router/PersistentKeyRing.java
@@ -0,0 +1,103 @@
+package net.i2p.router;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+
+import net.i2p.data.Base64;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.Destination;
+import net.i2p.data.Hash;
+import net.i2p.data.LeaseSet;
+import net.i2p.data.SessionKey;
+import net.i2p.router.TunnelPoolSettings;
+import net.i2p.util.KeyRing;
+
+/**
+ *  ConcurrentHashMap with backing in the router.config file.
+ *  router.keyring.key.{base64 hash, with = replaced with $}={base64 session key}
+ *  Caution - not all HashMap methods are overridden.
+ */
+public class PersistentKeyRing extends KeyRing {
+    private RouterContext _ctx;
+    private static final String PROP_PFX = "router.keyring.key.";
+
+    public PersistentKeyRing(RouterContext ctx) {
+        super();
+        _ctx = ctx;
+        addFromProperties();
+    }
+
+    public SessionKey put(Hash h, SessionKey sk) {
+        SessionKey old = super.put(h, sk);
+        if (!sk.equals(old)) {
+            _ctx.router().setConfigSetting(PROP_PFX + h.toBase64().replace("=", "$"),
+                                           sk.toBase64());
+            _ctx.router().saveConfig();
+        }
+        return old;
+    }
+
+    public SessionKey remove(Hash h) {
+        _ctx.router().removeConfigSetting(PROP_PFX + h.toBase64().replace("=", "$"));
+        _ctx.router().saveConfig();
+        return super.remove(h);
+    }
+
+    private void addFromProperties() {
+        for (Iterator iter = _ctx.getPropertyNames().iterator(); iter.hasNext(); ) {
+            String prop = (String) iter.next();
+            if (!prop.startsWith(PROP_PFX))
+                continue;
+            String key = _ctx.getProperty(prop);
+            if (key == null || key.length() != 44)
+                continue;
+            String hb = prop.substring(PROP_PFX.length());
+            hb.replace("$", "=");
+            Hash dest = new Hash();
+            SessionKey sk = new SessionKey();
+            try {
+                dest.fromBase64(hb);
+                sk.fromBase64(key);
+                super.put(dest, sk);
+            } catch (DataFormatException dfe) { continue; }
+        }
+    }
+
+    public void renderStatusHTML(Writer out) throws IOException {
+        StringBuffer buf = new StringBuffer(1024);
+        buf.append("\n<table border=\"1\"><tr><th align=\"left\">Destination Hash<th align=\"left\">Name or Dest.<th align=\"left\">Session Key</tr>");
+        for (Entry<Hash, SessionKey> e : entrySet()) {
+            buf.append("\n<tr><td>");
+            Hash h = e.getKey();
+            buf.append(h.toBase64().substring(0, 6)).append("...");
+            buf.append("<td>");
+            LeaseSet ls = _ctx.netDb().lookupLeaseSetLocally(h);
+            if (ls != null) {
+                Destination dest = ls.getDestination();
+                if (_ctx.clientManager().isLocal(dest)) {
+                    TunnelPoolSettings in = _ctx.tunnelManager().getInboundSettings(h);
+                    if (in != null && in.getDestinationNickname() != null)
+                        buf.append(in.getDestinationNickname());
+                    else
+                        buf.append(dest.toBase64().substring(0, 6)).append("...");
+                } else {
+                    String host = _ctx.namingService().reverseLookup(dest);
+                    if (host != null)
+                        buf.append(host);
+                    else
+                        buf.append(dest.toBase64().substring(0, 6)).append("...");
+                }
+            }
+            buf.append("<td>");
+            SessionKey sk = e.getValue();
+            buf.append(sk.toBase64());
+        }
+        buf.append("\n</table>\n");
+        out.write(buf.toString());
+        out.flush();
+    }
+}
diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java
index 087cde91847cb313680452266920a3bc5b090f37..517a5ba35883f68de8d4d765956d90ec7884a4e7 100644
--- a/router/java/src/net/i2p/router/RouterContext.java
+++ b/router/java/src/net/i2p/router/RouterContext.java
@@ -26,6 +26,7 @@ import net.i2p.router.transport.VMCommSystem;
 import net.i2p.router.tunnel.TunnelDispatcher;
 import net.i2p.router.tunnel.pool.TunnelPoolManager;
 import net.i2p.util.Clock;
+import net.i2p.util.KeyRing;
 
 /**
  * Build off the core I2P context to provide a root for a router instance to
@@ -366,4 +367,21 @@ public class RouterContext extends I2PAppContext {
         }
     }
 
+    /** override to support storage in router.config */
+    @Override
+    public KeyRing keyRing() {
+        if (!_keyRingInitialized)
+            initializeKeyRing();
+        return _keyRing;
+    }
+
+    @Override
+    protected void initializeKeyRing() {
+        synchronized (this) {
+            if (_keyRing == null)
+                _keyRing = new PersistentKeyRing(this);
+            _keyRingInitialized = true;
+        }
+    }
+    
 }
diff --git a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
index 544badcad42169139012fdf5f61993f359aa41c6..133ad142c8b040c415676c5fe9caff8b0f3a1896 100644
--- a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
+++ b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
@@ -29,6 +29,7 @@ import net.i2p.data.i2cp.I2CPMessageReader;
 import net.i2p.data.i2cp.MessageId;
 import net.i2p.data.i2cp.MessageStatusMessage;
 import net.i2p.data.i2cp.SendMessageMessage;
+import net.i2p.data.i2cp.SendMessageExpiresMessage;
 import net.i2p.data.i2cp.SessionConfig;
 import net.i2p.data.i2cp.SessionId;
 import net.i2p.router.Job;
@@ -270,6 +271,9 @@ public class ClientConnectionRunner {
         Destination dest = message.getDestination();
         MessageId id = new MessageId();
         id.setMessageId(getNextMessageId()); 
+        long expiration = 0;
+        if (message instanceof SendMessageExpiresMessage)
+            expiration = ((SendMessageExpiresMessage) message).getExpiration().getTime();
         long beforeLock = _context.clock().now();
         long inLock = 0;
         synchronized (_acceptedPending) {
@@ -291,7 +295,7 @@ public class ClientConnectionRunner {
         // the following blocks as described above
         SessionConfig cfg = _config;
         if (cfg != null)
-            _manager.distributeMessage(cfg.getDestination(), dest, payload, id);
+            _manager.distributeMessage(cfg.getDestination(), dest, payload, id, expiration);
         long timeToDistribute = _context.clock().now() - beforeDistribute;
         if (_log.shouldLog(Log.DEBUG))
             _log.warn("Time to distribute in the manager to " 
diff --git a/router/java/src/net/i2p/router/client/ClientManager.java b/router/java/src/net/i2p/router/client/ClientManager.java
index d9838ef7b5a3de471ec6f6a5bdb03b184d852814..18c9c77423d790836665096b9cc8d09926ab684e 100644
--- a/router/java/src/net/i2p/router/client/ClientManager.java
+++ b/router/java/src/net/i2p/router/client/ClientManager.java
@@ -140,7 +140,7 @@ public class ClientManager {
         }
     }
     
-    void distributeMessage(Destination fromDest, Destination toDest, Payload payload, MessageId msgId) { 
+    void distributeMessage(Destination fromDest, Destination toDest, Payload payload, MessageId msgId, long expiration) { 
         // check if there is a runner for it
         ClientConnectionRunner runner = getRunner(toDest);
         if (runner != null) {
@@ -168,6 +168,7 @@ public class ClientManager {
             msg.setSenderConfig(runner.getConfig());
             msg.setFromDestination(runner.getConfig().getDestination());
             msg.setMessageId(msgId);
+            msg.setExpiration(expiration);
             _ctx.clientMessagePool().add(msg, true);
         }
     }
diff --git a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
index 033e28f2f00b6f8f59acaccddd2332ee5e4d4c20..d36d26401a496ca09fc3eb96098da7ad7c448253 100644
--- a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
+++ b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
@@ -21,7 +21,9 @@ import net.i2p.data.i2cp.MessageId;
 import net.i2p.data.i2cp.MessagePayloadMessage;
 import net.i2p.data.i2cp.ReceiveMessageBeginMessage;
 import net.i2p.data.i2cp.ReceiveMessageEndMessage;
+import net.i2p.data.i2cp.ReconfigureSessionMessage;
 import net.i2p.data.i2cp.SendMessageMessage;
+import net.i2p.data.i2cp.SendMessageExpiresMessage;
 import net.i2p.data.i2cp.SessionId;
 import net.i2p.data.i2cp.SessionStatusMessage;
 import net.i2p.data.i2cp.SetDateMessage;
@@ -67,6 +69,9 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
             case SendMessageMessage.MESSAGE_TYPE:
                 handleSendMessage(reader, (SendMessageMessage)message);
                 break;
+            case SendMessageExpiresMessage.MESSAGE_TYPE:
+                handleSendMessage(reader, (SendMessageExpiresMessage)message);
+                break;
             case ReceiveMessageBeginMessage.MESSAGE_TYPE:
                 handleReceiveBegin(reader, (ReceiveMessageBeginMessage)message);
                 break;
@@ -237,6 +242,17 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
         _context.jobQueue().addJob(new LookupDestJob(_context, _runner, message.getHash()));
     }
 
+    /**
+     * Message's Session ID ignored. This doesn't support removing previously set options.
+     * Nor do we bother with message.getSessionConfig().verifySignature() ... should we?
+     *
+     */
+    private void handleReconfigureSession(I2CPMessageReader reader, ReconfigureSessionMessage message) {
+        if (_log.shouldLog(Log.INFO))
+            _log.info("Updating options - session " + _runner.getSessionId());
+        _runner.getConfig().getOptions().putAll(message.getSessionConfig().getOptions());
+    }
+    
     // this *should* be mod 65536, but UnsignedInteger is still b0rked.  FIXME
     private final static int MAX_SESSION_ID = 32767;
 
diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
index ccef8192a58ac143b9159c5680cd9b6959af9eee..0515d5c34459e84384b6fdd17885880e0201fd43 100644
--- a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
+++ b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
@@ -61,6 +61,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
     private long _leaseSetLookupBegin;
     private TunnelInfo _outTunnel;
     private TunnelInfo _inTunnel;
+    private boolean _wantACK;
     
     /**
      * final timeout (in milliseconds) that the outbound message will fail in.
@@ -69,6 +70,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
      */
     public final static String OVERALL_TIMEOUT_MS_PARAM = "clientMessageTimeout";
     private final static long OVERALL_TIMEOUT_MS_DEFAULT = 60*1000;
+    private final static long OVERALL_TIMEOUT_MS_MIN = 5*1000;
     
     /** priority of messages, that might get honored some day... */
     private final static int SEND_PRIORITY = 500;
@@ -125,23 +127,34 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
         _to = msg.getDestination();
         _toString = _to.calculateHash().toBase64().substring(0,4);
         _leaseSetLookupBegin = -1;
+        _start = getContext().clock().now();
         
-        String param = msg.getSenderConfig().getOptions().getProperty(OVERALL_TIMEOUT_MS_PARAM);
-        if (param == null)
-            param = ctx.router().getConfigSetting(OVERALL_TIMEOUT_MS_PARAM);
-        if (param != null) {
-            try {
-                timeoutMs = Long.parseLong(param);
-            } catch (NumberFormatException nfe) {
-                if (_log.shouldLog(Log.WARN))
-                    _log.warn("Invalid client message timeout specified [" + param 
-                              + "], defaulting to " + OVERALL_TIMEOUT_MS_DEFAULT, nfe);
-                timeoutMs = OVERALL_TIMEOUT_MS_DEFAULT;
+        // use expiration requested by client if available, otherwise session config,
+        // otherwise router config, otherwise default
+        _overallExpiration = msg.getExpiration();
+        if (_overallExpiration > 0) {
+           _overallExpiration = Math.max(_overallExpiration, _start + OVERALL_TIMEOUT_MS_MIN);
+           _overallExpiration = Math.min(_overallExpiration, _start + OVERALL_TIMEOUT_MS_DEFAULT);
+           if (_log.shouldLog(Log.WARN))
+               _log.warn("Message Expiration (ms): " + (_overallExpiration - _start));
+        } else {
+            String param = msg.getSenderConfig().getOptions().getProperty(OVERALL_TIMEOUT_MS_PARAM);
+            if (param == null)
+                param = ctx.router().getConfigSetting(OVERALL_TIMEOUT_MS_PARAM);
+            if (param != null) {
+                try {
+                    timeoutMs = Long.parseLong(param);
+                } catch (NumberFormatException nfe) {
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn("Invalid client message timeout specified [" + param 
+                                  + "], defaulting to " + OVERALL_TIMEOUT_MS_DEFAULT, nfe);
+                    timeoutMs = OVERALL_TIMEOUT_MS_DEFAULT;
+                }
             }
+            _overallExpiration = timeoutMs + _start;
+           if (_log.shouldLog(Log.WARN))
+               _log.warn("Default Expiration (ms): " + timeoutMs);
         }
-        
-        _start = getContext().clock().now();
-        _overallExpiration = timeoutMs + _start;
         _finished = false;
     }
     
@@ -267,6 +280,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
                 long lookupTime = getContext().clock().now() - _leaseSetLookupBegin;
                 getContext().statManager().addRateData("client.leaseSetFoundRemoteTime", lookupTime, lookupTime);
             }
+            _wantACK = false;
             boolean ok = getNextLease();
             if (ok) {
                 send();
@@ -400,6 +414,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
         }
         if (_log.shouldLog(Log.INFO))
             _log.info("Added to cache - lease for " + _toString); 
+        _wantACK = true;
         return true;
     }
 
@@ -443,10 +458,14 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
             dieFatal();
             return;
         }
-        boolean wantACK = true;
+
         int existingTags = GarlicMessageBuilder.estimateAvailableTags(getContext(), _leaseSet.getEncryptionKey());
-        if ( (existingTags > 30) && (getContext().random().nextInt(100) >= 5) )
-            wantACK = false;
+        _outTunnel = selectOutboundTunnel(_to);
+        // what's the point of 5% random? possible improvements or replacements:
+        // - wantACK if we changed their inbound lease (getNextLease() sets _wantACK)
+        // - wantACK if we changed our outbound tunnel (selectOutboundTunnel() sets _wantACK)
+        // - wantACK if we haven't in last 1m (requires a new static cache probably)
+        boolean wantACK = _wantACK || existingTags <= 30 || getContext().random().nextInt(100) < 5;
         
         PublicKey key = _leaseSet.getEncryptionKey();
         SessionKey sessKey = new SessionKey();
@@ -503,7 +522,6 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
                        + _lease.getTunnelId() + " on " 
                        + _lease.getGateway().toBase64());
         
-        _outTunnel = selectOutboundTunnel(_to);
         if (_outTunnel != null) {
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug(getJobId() + ": Sending tunnel message out " + _outTunnel.getSendTunnelId(0) + " to " 
@@ -718,6 +736,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
                             _log.warn("Switching back to tunnel " + tunnel + " for " + _toString); 
                         _backloggedTunnelCache.remove(hashPair());
                         _tunnelCache.put(hashPair(), tunnel);
+                        _wantACK = true;
                         return tunnel;
                     }  // else still backlogged
                 } else // no longer valid
@@ -740,6 +759,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
             tunnel = selectOutboundTunnel();
             if (tunnel != null)
                 _tunnelCache.put(hashPair(), tunnel);
+            _wantACK = true;
         }
         return tunnel;
     }
diff --git a/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java b/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java
index 96dbadd6cf2472a6b95dd24c4a157ced36e88a58..8fa729d63763dae195aa1321c2895fa8595478ce 100644
--- a/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java
+++ b/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java
@@ -20,12 +20,12 @@ import net.i2p.router.RouterContext;
 import net.i2p.util.Log;
 
 /**
- * Publish the local router's RouterInfo every 5 to 10 minutes
+ * Publish the local router's RouterInfo periodically
  *
  */
 public class PublishLocalRouterInfoJob extends JobImpl {
     private Log _log;
-    final static long PUBLISH_DELAY = 5*60*1000; // every 5 to 10 minutes (since we randomize)
+    final static long PUBLISH_DELAY = 20*60*1000;
     
     public PublishLocalRouterInfoJob(RouterContext ctx) {
         super(ctx);
@@ -67,6 +67,6 @@ public class PublishLocalRouterInfoJob extends JobImpl {
         } catch (DataFormatException dfe) {
             _log.error("Error signing the updated local router info!", dfe);
         }
-        requeue(PUBLISH_DELAY + getContext().random().nextInt((int)PUBLISH_DELAY));
+        requeue((PUBLISH_DELAY/2) + getContext().random().nextInt((int)PUBLISH_DELAY));
     }
 }
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
index c74499f957e62a3031c992d8eac374047342484d..23ef78d86956961261ff24e9013978ee01a43bf4 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
@@ -125,6 +125,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
     private final static long ROUTER_INFO_EXPIRATION_SHORT = 90*60*1000l;
     
     private final static long EXPLORE_JOB_DELAY = 10*60*1000l;
+    private final static long PUBLISH_JOB_DELAY = 5*60*1000l;
 
     public KademliaNetworkDatabaseFacade(RouterContext context) {
         _context = context;
@@ -326,7 +327,9 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
         }
         // periodically update and resign the router's 'published date', which basically
         // serves as a version
-        _context.jobQueue().addJob(new PublishLocalRouterInfoJob(_context));
+        Job plrij = new PublishLocalRouterInfoJob(_context);
+        plrij.getTiming().setStartAfter(_context.clock().now() + PUBLISH_JOB_DELAY);
+        _context.jobQueue().addJob(plrij);
         try {
             publish(ri);
         } catch (IllegalArgumentException iae) {
@@ -970,7 +973,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
         StringBuffer buf = new StringBuffer(4*1024);
         buf.append("<h2>Network Database RouterInfo Lookup</h2>\n");
         if (".".equals(routerPrefix)) {
-            renderRouterInfo(buf, _context.router().getRouterInfo(), true);
+            renderRouterInfo(buf, _context.router().getRouterInfo(), true, true);
         } else {
             boolean notFound = true;
             Set routers = getRouters();
@@ -978,7 +981,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
                 RouterInfo ri = (RouterInfo)iter.next();
                 Hash key = ri.getIdentity().getHash();
                 if (key.toBase64().startsWith(routerPrefix)) {
-                    renderRouterInfo(buf, ri, false);
+                    renderRouterInfo(buf, ri, false, true);
                     notFound = false;
                 }
             }
@@ -990,7 +993,14 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
     }
 
     public void renderStatusHTML(Writer out) throws IOException {
-        StringBuffer buf = new StringBuffer(getKnownRouters() * 2048);
+        renderStatusHTML(out, true);
+    }
+
+    public void renderStatusHTML(Writer out, boolean full) throws IOException {
+        int size = getKnownRouters() * 512;
+        if (full)
+            size *= 4;
+        StringBuffer buf = new StringBuffer(size);
         buf.append("<h2>Network Database Contents</h2>\n");
         if (!_initialized) {
             buf.append("<i>Not initialized</i>\n");
@@ -1044,10 +1054,15 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
         }
         
         Hash us = _context.routerHash();
-        out.write("<h3>Routers</h3>\n");
+        out.write("<a name=\"routers\" /><h3>Routers (<a href=\"netdb.jsp");
+        if (full)
+            out.write("#routers\" >view without");
+        else
+            out.write("?f=1#routers\" >view with");
+        out.write(" stats</a>)</h3>\n");
         
         RouterInfo ourInfo = _context.router().getRouterInfo();
-        renderRouterInfo(buf, ourInfo, true);
+        renderRouterInfo(buf, ourInfo, true, true);
         out.write(buf.toString());
         buf.setLength(0);
         
@@ -1061,7 +1076,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
             Hash key = ri.getIdentity().getHash();
             boolean isUs = key.equals(us);
             if (!isUs) {
-                renderRouterInfo(buf, ri, false);
+                renderRouterInfo(buf, ri, false, full);
                 out.write(buf.toString());
                 buf.setLength(0);
                 String coreVersion = ri.getOption("coreVersion");
@@ -1102,7 +1117,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
         out.flush();
     }
     
-    private void renderRouterInfo(StringBuffer buf, RouterInfo info, boolean isUs) {
+    private void renderRouterInfo(StringBuffer buf, RouterInfo info, boolean isUs, boolean full) {
         String hash = info.getIdentity().getHash().toBase64();
         buf.append("<a name=\"").append(hash.substring(0, 6)).append("\" />");
         if (isUs) {
@@ -1129,13 +1144,18 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
             }
         }
         buf.append("</i><br />\n");
-        buf.append("Stats: <br /><i><code>\n");
-        for (Iterator iter = info.getOptions().keySet().iterator(); iter.hasNext(); ) {
-            String key = (String)iter.next();
-            String val = info.getOption(key);
-            buf.append(DataHelper.stripHTML(key)).append(" = ").append(DataHelper.stripHTML(val)).append("<br />\n");
+        if (full) {
+            buf.append("Stats: <br /><i><code>\n");
+            for (Iterator iter = info.getOptions().keySet().iterator(); iter.hasNext(); ) {
+                String key = (String)iter.next();
+                String val = info.getOption(key);
+                buf.append(DataHelper.stripHTML(key)).append(" = ").append(DataHelper.stripHTML(val)).append("<br />\n");
+            }
+            buf.append("</code></i>\n");
+        } else {
+            buf.append("<a href=\"netdb.jsp?r=").append(hash.substring(0, 6)).append("\" >Full entry</a>\n");
         }
-        buf.append("</code></i><hr />\n");
+        buf.append("<hr />\n");
     }
     
 }
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java b/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java
index 77d2427cd79e41bde1bd28cfb524994cc3da1ba3..5d0d219dbe6fe125bbb698d0bcb49724f783b114 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java
@@ -65,10 +65,6 @@ class PersistentDataStore extends TransientDataStore {
         return super.remove(key);
     }
     
-    public DataStructure removeLease(Hash key) {
-        return super.removeLease(key);
-    }
-    
     public void put(Hash key, DataStructure data) {
         if ( (data == null) || (key == null) ) return;
         super.put(key, data);
@@ -77,26 +73,6 @@ class PersistentDataStore extends TransientDataStore {
             _writer.queue(key, data);
     }
     
-/*
- *  We don't store leasesets here anymore, use the TransientDataStore count
- *
-    public int countLeaseSets() {
-        File dbDir = null;
-        try {
-            dbDir = getDbDir();
-        } catch (IOException ioe) { 
-            return 0;
-        }
-        if (dbDir == null)
-            return 0;
-        File leaseSetFiles[] = dbDir.listFiles(LeaseSetFilter.getInstance());
-        if (leaseSetFiles == null) 
-            return 0;
-        else
-            return leaseSetFiles.length;
-    }
-*/
-    
     private void accept(LeaseSet ls) {
         super.put(ls.getDestination().calculateHash(), ls);
     }
@@ -249,18 +225,6 @@ class PersistentDataStore extends TransientDataStore {
             int routerCount = 0;
             try {
                 File dbDir = getDbDir();
-/****
-                if (getContext().router().getUptime() < 10*60*1000) {
-                    File leaseSetFiles[] = dbDir.listFiles(LeaseSetFilter.getInstance());
-                    if (leaseSetFiles != null) {
-                        for (int i = 0; i < leaseSetFiles.length; i++) {
-                            Hash key = getLeaseSetHash(leaseSetFiles[i].getName());
-                            if ( (key != null) && (!isKnown(key)) )
-                                PersistentDataStore.this._context.jobQueue().addJob(new ReadLeaseJob(leaseSetFiles[i], key));
-                        }
-                    }
-                }
-****/
                 File routerInfoFiles[] = dbDir.listFiles(RouterInfoFilter.getInstance());
                 if (routerInfoFiles != null) {
                     routerCount += routerInfoFiles.length;
@@ -283,63 +247,6 @@ class PersistentDataStore extends TransientDataStore {
         }
     }
     
-/****
-    private class ReadLeaseJob extends JobImpl {
-        private File _leaseFile;
-        private Hash _key;
-        public ReadLeaseJob(File leaseFile, Hash key) {
-            super(PersistentDataStore.this._context);
-            _leaseFile = leaseFile;
-            _key = key;
-        }
-        public String getName() { return "Read LeaseSet"; }
-        private boolean shouldRead() {
-            DataStructure data = get(_key);
-            if (data == null) return true;
-            if (data instanceof LeaseSet) {
-                long knownDate = ((LeaseSet)data).getEarliestLeaseDate();
-                long fileDate = _leaseFile.lastModified();
-                if (fileDate > knownDate)
-                    return true;
-                else
-                    return false;
-            } else {
-                // wtf
-                return true;
-            }
-        }
-        public void runJob() {
-            if (!shouldRead()) return;
-            try {
-                FileInputStream fis = null;
-                boolean corrupt = false;
-                try {
-                    fis = new FileInputStream(_leaseFile);
-                    LeaseSet ls = new LeaseSet();
-                    ls.readBytes(fis);
-                    try {
-                        _facade.store(ls.getDestination().calculateHash(), ls);
-                    } catch (IllegalArgumentException iae) {
-                        _log.info("Refused locally loaded leaseSet - deleting");
-                        corrupt = true;
-                    }
-                } catch (DataFormatException dfe) {
-                    _log.warn("Error reading the leaseSet from " + _leaseFile.getAbsolutePath(), dfe);
-                    corrupt = true;
-                } catch (FileNotFoundException fnfe) {
-                    _log.debug("Deleted prior to read.. a race during expiration / load");
-                    corrupt = false;
-                } finally {
-                    if (fis != null) try { fis.close(); } catch (IOException ioe) {}
-                }
-                if (corrupt) _leaseFile.delete();
-            } catch (IOException ioe) {
-                _log.warn("Error reading the leaseSet from " + _leaseFile.getAbsolutePath(), ioe);
-            }
-        }
-    }
-****/
-    
     private class ReadRouterJob extends JobImpl {
         private File _routerFile;
         private Hash _key;
@@ -464,31 +371,8 @@ class PersistentDataStore extends TransientDataStore {
                 _log.info("Removed router info at " + f.getAbsolutePath());
             return;
         }
-/***
-        String lsName = getLeaseSetName(key);
-        File f = new File(dir, lsName);
-        if (f.exists()) {
-            boolean removed = f.delete();
-            if (!removed)
-                _log.warn("Unable to remove lease set at " + f.getAbsolutePath());
-            else
-                _log.info("Removed lease set at " + f.getAbsolutePath());
-            return;
-        }
-***/
     }
     
-/***
-    private final static class LeaseSetFilter implements FilenameFilter {
-        private static final FilenameFilter _instance = new LeaseSetFilter();
-        public static final FilenameFilter getInstance() { return _instance; }
-        public boolean accept(File dir, String name) {
-            if (name == null) return false;
-            name = name.toUpperCase();
-            return (name.startsWith(LEASESET_PREFIX.toUpperCase()) && name.endsWith(LEASESET_SUFFIX.toUpperCase()));
-        }
-    }
-***/
     private final static class RouterInfoFilter implements FilenameFilter {
         private static final FilenameFilter _instance = new RouterInfoFilter();
         public static final FilenameFilter getInstance() { return _instance; }
diff --git a/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java b/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java
index a43fc631195dce3c82986f26a6e469959238247b..967bc7a797bc7e175261b4f6539e5af88e722d87 100644
--- a/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java
+++ b/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java
@@ -28,6 +28,13 @@ import net.i2p.router.RouterContext;
 import net.i2p.util.Log;
 
 /**
+ * This used be called from StartAcceptingClientsJob but is now disabled.
+ * It is still called once from LoadRouterInfoJob (but not run as a Job).
+ *
+ * The following comments appear to be incorrect...
+ * it rebuilds if the router.info file does not exist.
+ * There is no check for a router.info.rebuild file.
+ *
  * If the file router.info.rebuild exists, rebuild the router info and republish.
  * This is useful for dhcp or other situations where the router addresses change -
  * simply create the router.info.rebuild file after modifying router.config and within
diff --git a/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java b/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java
index 7ad54299fb9f00847b6db258e601fdad02edb5f2..727d06ac6c1cd3a4c05815bcdc553ba7ca4f20e2 100644
--- a/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java
+++ b/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java
@@ -28,7 +28,8 @@ public class StartAcceptingClientsJob extends JobImpl {
         getContext().clientManager().startup();
 
         getContext().jobQueue().addJob(new ReadConfigJob(getContext()));
-        getContext().jobQueue().addJob(new RebuildRouterInfoJob(getContext()));
+        // pointless
+        //getContext().jobQueue().addJob(new RebuildRouterInfoJob(getContext()));
         getContext().jobQueue().allowParallelOperation();
     }
 }
diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java
index 1a3e0d1b630ba36680ea5206eafe8af9a57a8b0b..43120d0b08039616e0485c3a856e17c100ef166b 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java
@@ -507,7 +507,7 @@ public class TunnelPoolManager implements TunnelManagerFacade {
         }
         out.write("</table>\n");
         out.write("Inactive participating tunnels: " + inactive + "<br />\n");
-        out.write("Lifetime bandwidth usage: " + processed + "KB<br />\n");
+        out.write("Lifetime bandwidth usage: " + DataHelper.formatSize(processed*1024) + "B<br />\n");
     }
     
     class TunnelComparator implements Comparator {
@@ -577,7 +577,8 @@ public class TunnelPoolManager implements TunnelManagerFacade {
         }
         if (live <= 0)
             out.write("<b>No tunnels, waiting for the grace period to end</b><br />\n");
-        out.write("Lifetime bandwidth usage: " + processedIn + "KB in, " + processedOut + "KB out<br />");
+        out.write("Lifetime bandwidth usage: " + DataHelper.formatSize(processedIn*1024) + "B in, " +
+                  DataHelper.formatSize(processedOut*1024) + "B out<br />");
     }
     
     private String getCapacity(Hash peer) {