diff --git a/Slackware/i2p-base/i2p-base.SlackBuild b/Slackware/i2p-base/i2p-base.SlackBuild
index d91d87263706086323d6bd5b76b22132e60a0f5d..7101fb950a38fdc0f9f9f59c68d16670985da5f2 100644
--- a/Slackware/i2p-base/i2p-base.SlackBuild
+++ b/Slackware/i2p-base/i2p-base.SlackBuild
@@ -24,7 +24,7 @@ mkdir -p $PKG
 # es: usr/local
 NAME=i2p-base
 VERSION=0.0.1
-BUILD=1sim
+BUILD=1sponge
 ARCH=noarch
 INSTALL_DIR=opt
 cd $PKG
@@ -38,5 +38,9 @@ sed "s|directory|/$INSTALL_DIR/i2p/|g" $CWD/doinst.sh > $PKG/install/doinst.sh
 cat $CWD/slack-desc > $PKG/install/slack-desc
 
 cd $PKG
-requiredbuilder -v -y -s $CWD $PKG
+#
+# Not really that important to exec this.
+#requiredbuilder -v -y -s $CWD $PKG
+#
+cat $CWD/slack-required > $PKG/install/slack-required
 makepkg -l y -c n $CWD/${NAME}-$VERSION-$ARCH-$BUILD.tgz
diff --git a/Slackware/i2p/i2p.SlackBuild b/Slackware/i2p/i2p.SlackBuild
index f7ced54bfbb4d727ae3e3f5d2dc1edb9b6972b94..a8ecf3be21e577517e5c609838b5512172fae596 100755
--- a/Slackware/i2p/i2p.SlackBuild
+++ b/Slackware/i2p/i2p.SlackBuild
@@ -15,7 +15,7 @@
 # It's suggested to subscribe to various dns host, like i2host.i2p
 # For any additional information, visit i2host.i2p and forum.i2p
 
-BUILD=1sim
+BUILD=1sponge
 
 # put here installation dir, without first and last /
 # eg: usr/local
@@ -113,5 +113,11 @@ sed "s|directory|/$INSTALL_DIR/i2p/|g" $CWD/doinst.sh > $PKG/install/doinst.sh
 cat $CWD/slack-desc > $PKG/install/slack-desc
 
 cd $PKG
-requiredbuilder -v -y -s $CWD $PKG
+#
+# requiredbuilder fucks up REALLY bad, and thinks java is perl?!
+# It also did not catch the shell requirements! BOOOOOOOOOOO! HISSSSSSSS!
+#
+#requiredbuilder -v -y -s $CWD $PKG
+#
+cat $CWD/slack-required > $PKG/install/slack-required
 makepkg -l y -c n $CWD/${NAME}-$VERSION-$ARCH-$BUILD.tgz
diff --git a/Slackware/i2p/slack-required b/Slackware/i2p/slack-required
index 3dcf36221390ab28f06a57c9bb9921d2860b4b9e..d83d818e6fca7e64282f925dbbb293a8d8158ab3 100644
--- a/Slackware/i2p/slack-required
+++ b/Slackware/i2p/slack-required
@@ -1,2 +1,4 @@
-glibc >= 2.7-i486-17 | glibc-solibs >= 2.7-i486-17
-perl >= 5.10.0-i486-1
+jre >= 5
+i2p-base >= 0.0.1
+bash >= 3.1.017
+
diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
index b6f688110411342b9e53b5f94bb829f90773f12b..5d42e924327dcb924ff0423ddd6f9eca1f7b2c60 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
@@ -85,7 +85,7 @@ public class I2PSnarkServlet extends HttpServlet {
         out.write("<meta http-equiv=\"refresh\" content=\"60;" + req.getRequestURI() + peerString + "\">\n");
         out.write(HEADER);
         out.write("</head><body>");
-        out.write("<center><div class=\"page\">");
+        out.write("<center>");
         out.write("<div class=\"snarknavbar\"><a href=\"" + req.getRequestURI() + peerString + "\" title=\"Refresh page\" class=\"snarkRefresh\">I2PSnark</a> <a href=\"http://forum.i2p/viewforum.php?f=21\" class=\"snarkRefresh\" target=\"_blank\">Forum</a>\n");
         Map trackers = _manager.getTrackers();
         for (Iterator iter = trackers.entrySet().iterator(); iter.hasNext(); ) {
@@ -96,10 +96,10 @@ public class I2PSnarkServlet extends HttpServlet {
             if (e < 0)
                 continue;
             baseURL = baseURL.substring(e + 1);
-            out.write("<a href=\"" + baseURL + "\" class=\"snarkRefresh\" target=\"_blank\">" + name + "</a>");
+            out.write(" <a href=\"" + baseURL + "\" class=\"snarkRefresh\" target=\"_blank\">" + name + "</a>");
         }
         out.write("</div>\n");
-        out.write("<div class=\"mainsection\"><div class=\"snarkMessages\"><table><tr><td align=\"left\"><pre>");
+        out.write("<div class=\"page\"><div class=\"mainsection\"><div class=\"snarkMessages\"><table><tr><td align=\"left\"><pre>");
         List msgs = _manager.getMessages();
         for (int i = msgs.size()-1; i >= 0; i--) {
             String msg = (String)msgs.get(i);
@@ -498,7 +498,7 @@ public class I2PSnarkServlet extends HttpServlet {
         
         if (remaining == 0)
             out.write("<a href=\"" + _manager.linkPrefix() + snark.meta.getName() 
-                      + "\" title=\"Click to access completed downloaded..\">");
+                      + "\" title=\"View file\">");
         out.write(filename);
         if (remaining == 0)
             out.write("</a>");
@@ -573,7 +573,7 @@ public class I2PSnarkServlet extends HttpServlet {
                 out.write("<tr class=\"" + rowClass + "\">");
                 out.write("<td align=\"center\" class=\"snarkTorrentStatus " + rowClass + "\">");
                 out.write("</td>\n\t");
-                out.write("<td align=\"center\" class=\"snarkTorrentStatus " + rowClass + "\">");
+                out.write("<td align=\"left\" class=\"snarkTorrentStatus " + rowClass + "\">");
                 String ch = peer.toString().substring(0, 4);
                 String client;
                 if ("AwMD".equals(ch))
@@ -592,7 +592,7 @@ public class I2PSnarkServlet extends HttpServlet {
                     client = "Robert";
                 else
                     client = "Unknown (" + ch + ')';
-                out.write("<font size=-1>" + client + "</font>&nbsp;&nbsp;" + peer.toString().substring(5, 9) + "");
+                out.write("" + client + "&nbsp;&nbsp;" + peer.toString().substring(5, 9) + "");
                 if (showDebug)
                     out.write(" inactive " + (peer.getInactiveTime() / 1000) + "s");
                 out.write("</td>\n\t");
@@ -601,12 +601,12 @@ public class I2PSnarkServlet extends HttpServlet {
                 out.write("<td align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">");
                 float pct = (float) (100.0 * (float) peer.completed() / snark.meta.getPieces());
                 if (pct == 100.0)
-                    out.write("<font size=-1>Seed</font>");
+                    out.write("Seed");
                 else {
                     String ps = String.valueOf(pct);
                     if (ps.length() > 5)
                         ps = ps.substring(0, 5);
-                    out.write("<font size=-1>" + ps + "%</font>");
+                    out.write("" + ps + "%");
                 }
                 out.write("</td>\n\t");
                 out.write("<td class=\"snarkTorrentStatus " + rowClass + "\">");
@@ -615,14 +615,14 @@ public class I2PSnarkServlet extends HttpServlet {
                 if (remaining > 0) {
                     if (peer.isInteresting() && !peer.isChoked()) {
                         out.write("<font color=#008000>");
-                        out.write("<font size=-1>" + formatSize(peer.getDownloadRate()) + "ps</font></font>");
+                        out.write("" + formatSize(peer.getDownloadRate()) + "ps</font>");
                     } else {
-                        out.write("<font color=#a00000><font size=-1><a title=\"");
+                        out.write("<font color=#a00000><a title=\"");
                         if (!peer.isInteresting())
                             out.write("Uninteresting\">");
                         else
                             out.write("Choked\">");
-                        out.write(formatSize(peer.getDownloadRate()) + "ps</a></font></font>");
+                        out.write(formatSize(peer.getDownloadRate()) + "ps</a></font>");
                     }
                 }
                 out.write("</td>\n\t");
@@ -630,14 +630,14 @@ public class I2PSnarkServlet extends HttpServlet {
                 if (pct != 100.0) {
                     if (peer.isInterested() && !peer.isChoking()) {
                         out.write("<font color=#008000>");
-                        out.write("<font size=-1>" + formatSize(peer.getUploadRate()) + "ps</font></font>");
+                        out.write("" + formatSize(peer.getUploadRate()) + "ps</font>");
                     } else {
-                        out.write("<font color=#a00000><font size=-1><a title=\"");
+                        out.write("<font color=#a00000><a title=\"");
                         if (!peer.isInterested())
                             out.write("Uninterested\">");
                         else
                             out.write("Choking\">");
-                        out.write(formatSize(peer.getUploadRate()) + "ps</a></font></font>");
+                        out.write(formatSize(peer.getUploadRate()) + "ps</a></font>");
                     }
                 }
                 out.write("</td>\n\t");
diff --git a/apps/i2ptunnel/jsp/editClient.jsp b/apps/i2ptunnel/jsp/editClient.jsp
index 4957e3a576cb850c45cf38670cb548171449ca1a..c0ad7cdb5b336bf8996ae8fc2bd5157696e4c019 100644
--- a/apps/i2ptunnel/jsp/editClient.jsp
+++ b/apps/i2ptunnel/jsp/editClient.jsp
@@ -13,7 +13,7 @@
 %>
 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
 <head>
-    <title>I2PTunnel Webmanager - Edit</title>
+    <title>I2P Tunnel Manager - Edit</title>
     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
     <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=UTF-8" />
 
diff --git a/apps/i2ptunnel/jsp/editServer.jsp b/apps/i2ptunnel/jsp/editServer.jsp
index 5d2cc3cd4bf19fac07649e2b32412abde47ac5d6..958c7e8b1fb1314bcd9f1f05bcb7df68e02c7c47 100644
--- a/apps/i2ptunnel/jsp/editServer.jsp
+++ b/apps/i2ptunnel/jsp/editServer.jsp
@@ -13,7 +13,7 @@
 %>
 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
 <head>
-    <title>I2PTunnel Webmanager - Edit</title>
+    <title>I2P Tunnel Manager - Edit</title>
     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
     <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=UTF-8" />
 
diff --git a/apps/ministreaming/java/src/net/i2p/client/streaming/package.html b/apps/ministreaming/java/src/net/i2p/client/streaming/package.html
index 735135074871957df6d46764e63f54d99537e4d1..8418604532afb4a7ef62c9d7d6e0d9621895179a 100644
--- a/apps/ministreaming/java/src/net/i2p/client/streaming/package.html
+++ b/apps/ministreaming/java/src/net/i2p/client/streaming/package.html
@@ -16,9 +16,9 @@ net.i2p.client.streaming.I2PServerSocket#accept} method, which will provide an
 application wants to create a new stream to a peer, it should do so with the
 appropriate {@link net.i2p.client.streaming.I2PSocketManager#connect} call.</p>
 
-<p>There is a simple pair of demo applications available as well - {@link
-net.i2p.client.streaming.StreamSinkServer} listens to a destination and dumps 
-the data from all sockets it accepts to individual files, while {@link
-net.i2p.client.streaming.StreamSinkClient} connects to a particular destination
+<p>There is a simple pair of demo applications available as well -
+net.i2p.client.streaming.StreamSinkServer listens to a destination and dumps 
+the data from all sockets it accepts to individual files, while
+net.i2p.client.streaming.StreamSinkClient connects to a particular destination
 and sends a specific amount of random data then disconnects.</p>
 </body></html>
diff --git a/apps/routerconsole/jsp/debug.jsp b/apps/routerconsole/jsp/debug.jsp
new file mode 100644
index 0000000000000000000000000000000000000000..b05f46a21fa3cf8c779fb479ba15b293e025670a
--- /dev/null
+++ b/apps/routerconsole/jsp/debug.jsp
@@ -0,0 +1,31 @@
+<%@page contentType="text/html"%>
+<%@page pageEncoding="UTF-8"%>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html><head><title>I2P Router Console - Debug</title>
+<%@include file="css.jsp" %>
+</head><body>
+<%@include file="summary.jsp" %>
+<h1>Router SKM</h1>
+<div class="main" id="main">
+<%
+    /*
+     *  Quick and easy place to put debugging stuff
+     */
+    net.i2p.router.RouterContext ctx = (net.i2p.router.RouterContext) net.i2p.I2PAppContext.getGlobalContext();
+
+    /*
+     *  Print out the status for all the SessionKeyManagers
+     */
+
+    ctx.sessionKeyManager().renderStatusHTML(out);
+    java.util.Set<net.i2p.data.Destination> clients = ctx.clientManager().listClients();
+    for (net.i2p.data.Destination dest : clients) {
+        net.i2p.data.Hash h = dest.calculateHash();
+        net.i2p.crypto.SessionKeyManager skm = ctx.clientManager().getClientSessionKeyManager(h);
+        if (skm != null) {
+            out.print("<h1>" + h.toBase64().substring(0,6) + " SKM</h1>");
+            skm.renderStatusHTML(out);
+        }
+    }
+%>
+</div></body></html>
diff --git a/apps/routerconsole/jsp/error.jsp b/apps/routerconsole/jsp/error.jsp
index 3628a0c15bd70129e9b71fcee67df04a10045f4f..f865e85d8be63a8e011dfc24a4055b23c5539adf 100644
--- a/apps/routerconsole/jsp/error.jsp
+++ b/apps/routerconsole/jsp/error.jsp
@@ -12,7 +12,7 @@
     }
     // If it can't find the iframe or viewtheme.jsp I wonder if the whole thing blows up...
 %>
-<html><head><title>I2P Router Console</title>
+<html><head><title>I2P Router Console - Page Not Found</title>
 <%@include file="css.jsp" %>
 </head><body>
 <%
@@ -22,6 +22,7 @@ if (System.getProperty("router.consoleNonce") == null) {
 %>
 <%@include file="summary.jsp" %>
 <h1><%=ERROR_CODE%> <%=ERROR_MESSAGE%></h1>
-<div class="warning" id="warning">
-The Router Console page <%=ERROR_URI%> was not found.
+<div class="sorry" id="warning">
+Sorry! You appear to be requesting a non-existent Router Console page or resource.<hr>
+Error 404: <%=ERROR_URI%> not found.
 </div></body></html>
diff --git a/apps/routerconsole/jsp/help.jsp b/apps/routerconsole/jsp/help.jsp
index 48752b19e3af97cedabab5ad4e0034b4f8cb35f6..0ae11a95ae9515086b7e5da9041a47d03d156861 100644
--- a/apps/routerconsole/jsp/help.jsp
+++ b/apps/routerconsole/jsp/help.jsp
@@ -176,4 +176,4 @@ client applications can be found on our <a href="http://www.i2p2.i2p/download">d
 
  <p>A more complete list of changes can be found
  in the history.txt file in your i2p directory.
- </p><br></div></body></html>
+ </p><hr></div></body></html>
diff --git a/apps/sam/java/src/net/i2p/sam/SAMHandler.java b/apps/sam/java/src/net/i2p/sam/SAMHandler.java
index d53a5a66217f6906e63ee20ac5d24a7f29953ab7..cc36bef8279f011c7147273961f2c87cfdb75252 100644
--- a/apps/sam/java/src/net/i2p/sam/SAMHandler.java
+++ b/apps/sam/java/src/net/i2p/sam/SAMHandler.java
@@ -102,8 +102,8 @@ public abstract class SAMHandler implements Runnable {
     }
     
     static public void writeBytes(ByteBuffer data, SocketChannel out) throws IOException {
-    	while (data.hasRemaining()) out.write(data);           
-    	out.socket().getOutputStream().flush();
+        while (data.hasRemaining()) out.write(data);           
+        out.socket().getOutputStream().flush();
     }
     
     /** 
@@ -124,9 +124,11 @@ public abstract class SAMHandler implements Runnable {
      * @return True if the string was successfully written, false otherwise
      */
     protected final boolean writeString(String str) {
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug("Sending the client: [" + str + "]");
-        return writeString(str, socket);
+        synchronized (socketWLock) {
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Sending the client: [" + str + "]");
+            return writeString(str, socket);
+        }
     }
 
     public static boolean writeString(String str, SocketChannel out)
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 c7495131f3d1609cc31840fb67e892298245801f..a47d361a668e0e0495eaaebfec4f1578614d8606 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java
@@ -354,6 +354,7 @@ public class Connection {
          */
     }
     
+/*********
     private class PingNotifier implements ConnectionManager.PingNotifier {
         private long _startedPingOn;
         public PingNotifier() {
@@ -367,6 +368,7 @@ public class Connection {
                 _options.updateRTT((int)time*2);
         }
     }
+*********/
     
     List ackPackets(long ackThrough, long nacks[]) {
         if (ackThrough < _highestAckedThrough) {
@@ -548,20 +550,21 @@ public class Connection {
         killOutstandingPackets();
     }
     
+    /** ignore tag issues */
     private void killOutstandingPackets() {
-        boolean tagsCancelled = false;
+        //boolean tagsCancelled = false;
         synchronized (_outboundPackets) {
             for (Iterator iter = _outboundPackets.values().iterator(); iter.hasNext(); ) {
                 PacketLocal pl = (PacketLocal)iter.next();
-                if ( (pl.getTagsSent() != null) && (pl.getTagsSent().size() > 0) )
-                    tagsCancelled = true;
+                //if ( (pl.getTagsSent() != null) && (pl.getTagsSent().size() > 0) )
+                //    tagsCancelled = true;
                 pl.cancelled();
             }
             _outboundPackets.clear();
             _outboundPackets.notifyAll();
         }            
-        if (tagsCancelled)
-            _context.sessionKeyManager().failTags(_remotePeer.getPublicKey());
+        //if (tagsCancelled)
+        //    _context.sessionKeyManager().failTags(_remotePeer.getPublicKey());
     }
     
     private class DisconnectEvent implements SimpleTimer.TimedEvent {
@@ -1140,12 +1143,12 @@ public class Connection {
                 
                 // in case things really suck, the other side may have lost thier
                 // session tags (e.g. they restarted), so jump back to ElGamal.
-                int failTagsAt = _options.getMaxResends() - 2;
-                if ( (newWindowSize == 1) && (numSends == failTagsAt) ) {
-                    if (_log.shouldLog(Log.WARN))
-                        _log.warn("Optimistically failing tags at resend " + numSends);
-                    _context.sessionKeyManager().failTags(_remotePeer.getPublicKey());
-                }
+                //int failTagsAt = _options.getMaxResends() - 2;
+                //if ( (newWindowSize == 1) && (numSends == failTagsAt) ) {
+                //    if (_log.shouldLog(Log.WARN))
+                //        _log.warn("Optimistically failing tags at resend " + numSends);
+                //    _context.sessionKeyManager().failTags(_remotePeer.getPublicKey());
+                //}
                 
                 if (numSends - 1 > _options.getMaxResends()) {
                     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 7efc6cc401e99f56e9fb6fb1ba4b88c814238224..af44c41f24b1f0d02552f9241b5700732bb3a4f4 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionManager.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionManager.java
@@ -349,24 +349,35 @@ public class ConnectionManager {
             return new HashSet(_connectionByInboundId.values());
         }
     }
+
+    /** blocking */
     public boolean ping(Destination peer, long timeoutMs) {
-        return ping(peer, timeoutMs, true);
+        return ping(peer, timeoutMs, true, null);
     }
     public boolean ping(Destination peer, long timeoutMs, boolean blocking) {
-        return ping(peer, timeoutMs, blocking, null, null, null);
+        return ping(peer, timeoutMs, blocking, null);
     }
 
+    /**
+     * @deprecated I2PSession ignores tags, use non-tag variant
+     * @param keyToUse ignored
+     * @param tagsToSend ignored
+     */
     public boolean ping(Destination peer, long timeoutMs, boolean blocking, SessionKey keyToUse, Set tagsToSend, PingNotifier notifier) {
+        return ping(peer, timeoutMs, blocking, notifier);
+    }
+
+    public boolean ping(Destination peer, long timeoutMs, boolean blocking, PingNotifier notifier) {
         Long id = new Long(_context.random().nextLong(Packet.MAX_STREAM_ID-1)+1);
         PacketLocal packet = new PacketLocal(_context, peer);
         packet.setSendStreamId(id.longValue());
         packet.setFlag(Packet.FLAG_ECHO);
         packet.setFlag(Packet.FLAG_SIGNATURE_INCLUDED);
         packet.setOptionalFrom(_session.getMyDestination());
-        if ( (keyToUse != null) && (tagsToSend != null) ) {
-            packet.setKeyUsed(keyToUse);
-            packet.setTagsSent(tagsToSend);
-        }
+        //if ( (keyToUse != null) && (tagsToSend != null) ) {
+        //    packet.setKeyUsed(keyToUse);
+        //    packet.setTagsSent(tagsToSend);
+        //}
         
         PingRequest req = new PingRequest(peer, packet, notifier);
         
@@ -435,7 +446,7 @@ public class ConnectionManager {
         }
         public void pong() { 
             _log.debug("Ping successful");
-            _context.sessionKeyManager().tagsDelivered(_peer.getPublicKey(), _packet.getKeyUsed(), _packet.getTagsSent());
+            //_context.sessionKeyManager().tagsDelivered(_peer.getPublicKey(), _packet.getKeyUsed(), _packet.getTagsSent());
             synchronized (ConnectionManager.PingRequest.this) {
                 _ponged = true; 
                 ConnectionManager.PingRequest.this.notifyAll();
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 f7b245cb83fb0748f3d37e805ed1c2bd345b70b9..91a06e088b983b041a09d068bd5c46c8cffa22a0 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java
@@ -263,12 +263,12 @@ public class ConnectionPacketHandler {
                     numResends++;
                 
                 // ACK the tags we delivered so we can use them
-                if ( (p.getKeyUsed() != null) && (p.getTagsSent() != null) 
-                      && (p.getTagsSent().size() > 0) ) {
-                    _context.sessionKeyManager().tagsDelivered(p.getTo().getPublicKey(), 
-                                                               p.getKeyUsed(), 
-                                                               p.getTagsSent());
-                }
+                //if ( (p.getKeyUsed() != null) && (p.getTagsSent() != null) 
+                //      && (p.getTagsSent().size() > 0) ) {
+                //    _context.sessionKeyManager().tagsDelivered(p.getTo().getPublicKey(), 
+                //                                               p.getKeyUsed(), 
+                //                                               p.getTagsSent());
+                //}
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug("Packet acked after " + p.getAckTime() + "ms: " + p);
             }
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/PacketLocal.java b/apps/streaming/java/src/net/i2p/client/streaming/PacketLocal.java
index 9ed29b50fb21aca3d865fd281838246ef0926364..b1438a033ad63ec6c95e5a8483ce3014ddf92dbf 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/PacketLocal.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/PacketLocal.java
@@ -47,11 +47,31 @@ public class PacketLocal extends Packet implements MessageOutputStream.WriteStat
     public Destination getTo() { return _to; }
     public void setTo(Destination to) { _to = to; }
     
+    /**
+     * @deprecated should always return null
+     */
     public SessionKey getKeyUsed() { return _keyUsed; }
-    public void setKeyUsed(SessionKey key) { _keyUsed = key; }
+
+    /**
+     * @deprecated I2PSession throws out the tags
+     */
+    public void setKeyUsed(SessionKey key) {
+        if (key != null)
+            _log.error("Who is sending tags thru the streaming lib?");
+        _keyUsed = key;
+    }
     
+    /**
+     * @deprecated should always return null or an empty set
+     */
     public Set getTagsSent() { return _tagsSent; }
+
+    /**
+     * @deprecated I2PSession throws out the tags
+     */
     public void setTagsSent(Set tags) { 
+        if (tags != null && tags.size() > 0)
+            _log.error("Who is sending tags thru the streaming lib? " + tags.size());
         if ( (_tagsSent != null) && (_tagsSent.size() > 0) && (tags.size() > 0) ) {
             //int old = _tagsSent.size();
             //_tagsSent.addAll(tags);
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 db4adb27cd00f643199dc6ecb0e56273ad50cc43..8a4692ada76444ae972c52861b776d2453f6b4f5 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java
@@ -36,16 +36,18 @@ public class PacketQueue {
     
     /**
      * Add a new packet to be sent out ASAP
+     *
+     * keys and tags disabled since dropped in I2PSession
      */
     public void enqueue(PacketLocal packet) {
         packet.prepare();
         
-        SessionKey keyUsed = packet.getKeyUsed();
-        if (keyUsed == null)
-            keyUsed = new SessionKey();
-        Set tagsSent = packet.getTagsSent();
-        if (tagsSent == null)
-            tagsSent = new HashSet(0);
+        //SessionKey keyUsed = packet.getKeyUsed();
+        //if (keyUsed == null)
+        //    keyUsed = new SessionKey();
+        //Set tagsSent = packet.getTagsSent();
+        //if (tagsSent == null)
+        //    tagsSent = new HashSet(0);
 
         // cache this from before sendMessage
         String conStr = null;
@@ -92,13 +94,19 @@ public class PacketQueue {
                 // I2PSessionImpl2
                 //sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent, expires);
                 // I2PSessionMuxedImpl
-                sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent, expires,
+                //sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent, expires,
+                //                 I2PSession.PROTO_STREAMING, I2PSession.PORT_UNSPECIFIED, I2PSession.PORT_UNSPECIFIED);
+                // I2PSessionMuxedImpl no tags
+                sent = _session.sendMessage(packet.getTo(), buf, 0, size, null, null, expires,
                                  I2PSession.PROTO_STREAMING, I2PSession.PORT_UNSPECIFIED, I2PSession.PORT_UNSPECIFIED);
             else
                 // I2PSessionImpl2
                 //sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent, 0);
                 // I2PSessionMuxedImpl
-                sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent,
+                //sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent,
+                //                 I2PSession.PROTO_STREAMING, I2PSession.PORT_UNSPECIFIED, I2PSession.PORT_UNSPECIFIED);
+                // I2PSessionMuxedImpl no tags
+                sent = _session.sendMessage(packet.getTo(), buf, 0, size, null, null,
                                  I2PSession.PROTO_STREAMING, I2PSession.PORT_UNSPECIFIED, I2PSession.PORT_UNSPECIFIED);
             end = _context.clock().now();
             
@@ -129,13 +137,11 @@ public class PacketQueue {
             if (c != null) // handle race on b0rk
                 c.disconnect(false);
         } else {
-            packet.setKeyUsed(keyUsed);
-            packet.setTagsSent(tagsSent);
+            //packet.setKeyUsed(keyUsed);
+            //packet.setTagsSent(tagsSent);
             packet.incrementSends();
             if (_log.shouldLog(Log.DEBUG)) {
-                String msg = "SEND " + packet + (tagsSent.size() > 0 
-                             ? " with " + tagsSent.size() + " tags"
-                             : "")
+                String msg = "SEND " + packet
                              + " send # " + packet.getNumSends()
                              + " sendTime: " + (end-begin)
                              + " con: " + conStr;
diff --git a/checklist.txt b/checklist.txt
index e65134b7cdae102776720a46bfce211d4afb1781..3feaf2a4fd511ebf40ea5432f9e44b0dcc4a8c1d 100644
--- a/checklist.txt
+++ b/checklist.txt
@@ -73,4 +73,4 @@ Website files to change:
         release-x.y.z.html (new)
 	Sync with mtn.i2p2.i2p
 
-Announce on #i2p, forum.i2p
+Announce on #i2p, forum.i2p, freshmeat.net, launchpad.net
diff --git a/core/c/jbigi/build.sh b/core/c/jbigi/build.sh
index 7ef9000704ee8bfb0b8eef5011ca17719a05979e..af6dbaa90224da4ebf775836b939efb262d8707d 100755
--- a/core/c/jbigi/build.sh
+++ b/core/c/jbigi/build.sh
@@ -15,21 +15,21 @@
 
 mkdir -p lib/
 mkdir -p bin/local
-VER=4.2.4
+VER=4.3.1
 
 if [ "$1" != "dynamic" -a ! -d gmp-$VER ]
 then
-	TAR=gmp-$VER.tar.bz2
+	TAR=gmp-$VER.tar.lzma
         if [ ! -f $TAR ]
         then
-		echo "GMP tarball $TAR not found. You must download it from http://gmplib.org/"
-		exit 1
+	    echo "Downloading ftp://ftp.gmplib.org/pub/gmp-4.3.1/gmp-4.3.1.tar.lzma"
+	    wget ftp://ftp.gmplib.org/pub/gmp-4.3.1/gmp-4.3.1.tar.lzma
         fi
 
 	echo "Building the jbigi library with GMP Version $VER"
 
 	echo "Extracting GMP..."
-	tar -xjf gmp-$VER.tar.bz2
+	tar -xf gmp-$VER.tar.lzma --lzma
 fi
 
 cd bin/local
@@ -42,7 +42,7 @@ then
 			# --with-pic is required for static linking
 			../../gmp-$VER/configure --with-pic;;
 		*)
-			../../gmp-$VER/configure;;
+			../../gmp-$VER/configure --with-pic;;
 	esac
 	make
 	sh ../../build_jbigi.sh static
@@ -54,7 +54,7 @@ cp *jbigi???* ../../lib/
 echo 'Library copied to lib/'
 cd ../..
 
-I2P=~/i2p
+I2P=~/i2p/i2p
 if [ ! -f $I2P/lib/i2p.jar ]
 then
 	echo "I2P installation not found in $I2P - correct \$I2P definition in script to run speed test"
diff --git a/core/c/jcpuid/build.sh b/core/c/jcpuid/build.sh
index f5c8ea1e1002a7ee4050818b6d1b76f454c30863..244eb07b269badee3fa00609d457a38db531f48f 100755
--- a/core/c/jcpuid/build.sh
+++ b/core/c/jcpuid/build.sh
@@ -37,13 +37,13 @@ FreeBSD*)
 Linux*)
 	COMPILEFLAGS="-fPIC -Wall"
 	INCLUDES="-I. -Iinclude -I$JAVA_HOME/include -I$JAVA_HOME/include/linux"
-	LINKFLAGS="-shared -static -static-libgcc -Wl,-soname,libjcpuid-x86-linux.so"
+	LINKFLAGS="-shared -Wl,-soname,libjcpuid-x86-linux.so"
 	LIBFILE="lib/freenet/support/CPUInformation/libjcpuid-x86-linux.so";;
 esac
 
 echo "Compiling C code..."
 rm -f $LIBFILE
-$CC $LINKFLAGS $INCLUDES src/*.c -o $LIBFILE
+$CC $COMPILEFLAGS $LINKFLAGS $INCLUDES src/*.c -o $LIBFILE
 strip $LIBFILE
 echo Built $LIBFILE
 
diff --git a/core/java/src/net/i2p/I2PAppContext.java b/core/java/src/net/i2p/I2PAppContext.java
index 025378d8c41985c4f883e51edfb45cd7952bf65f..86c1dc907e53c18f8cd732d990e80332dd0c7846 100644
--- a/core/java/src/net/i2p/I2PAppContext.java
+++ b/core/java/src/net/i2p/I2PAppContext.java
@@ -388,9 +388,13 @@ public class I2PAppContext {
      * The session key manager which coordinates the sessionKey / sessionTag
      * data.  This component allows transparent operation of the 
      * ElGamal/AES+SessionTag algorithm, and contains all of the session tags
-     * for one particular application.  If you want to seperate multiple apps
-     * to have their own sessionTags and sessionKeys, they should use different
-     * I2PAppContexts, and hence, different sessionKeyManagers.
+     * for one particular application.
+     *
+     * This is deprecated for client use, it should be used only by the router
+     * as its own key manager. Not that clients are doing end-to-end crypto anyway.
+     *
+     * For client crypto within the router,
+     * use RouterContext.clientManager.getClientSessionKeyManager(dest)
      *
      */
     public SessionKeyManager sessionKeyManager() { 
diff --git a/core/java/src/net/i2p/client/I2CPMessageProducer.java b/core/java/src/net/i2p/client/I2CPMessageProducer.java
index b897d22d0ee2230577e80c6972f52ed77e97efba..220c0a851e0a59c4cd4887dbb8f9b1782962937e 100644
--- a/core/java/src/net/i2p/client/I2CPMessageProducer.java
+++ b/core/java/src/net/i2p/client/I2CPMessageProducer.java
@@ -93,6 +93,10 @@ class I2CPMessageProducer {
     /**
      * Package up and send the payload to the router for delivery
      *
+     * @param tag unused - no end-to-end crypto
+     * @param tags unused - no end-to-end crypto
+     * @param key unused - no end-to-end crypto
+     * @param newKey unused - no end-to-end crypto
      */
     public void sendMessage(I2PSessionImpl session, Destination dest, long nonce, byte[] payload, SessionTag tag,
                             SessionKey key, Set tags, SessionKey newKey, long expires) throws I2PSessionException {
@@ -135,6 +139,10 @@ class I2CPMessageProducer {
     /**
      * Create a new signed payload and send it off to the destination
      *
+     * @param tag unused - no end-to-end crypto
+     * @param tags unused - no end-to-end crypto
+     * @param key unused - no end-to-end crypto
+     * @param newKey unused - no end-to-end crypto
      */
     private Payload createPayload(Destination dest, byte[] payload, SessionTag tag, SessionKey key, Set tags,
                                   SessionKey newKey) throws I2PSessionException {
diff --git a/core/java/src/net/i2p/client/I2PSessionImpl.java b/core/java/src/net/i2p/client/I2PSessionImpl.java
index 169562b7be31437a3205f337fac71c3ae425aa9c..0cbde00ced86e91cac50d38807e07144ff30828b 100644
--- a/core/java/src/net/i2p/client/I2PSessionImpl.java
+++ b/core/java/src/net/i2p/client/I2PSessionImpl.java
@@ -361,17 +361,23 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
      */
     public abstract boolean sendMessage(Destination dest, byte[] payload) throws I2PSessionException;
     
+    /**
+     * @param keyUsed unused - no end-to-end crypto
+     * @param tagsSent unused - no end-to-end crypto
+     */
     public abstract boolean sendMessage(Destination dest, byte[] payload, SessionKey keyUsed, 
                                         Set tagsSent) throws I2PSessionException;
 
     public abstract void receiveStatus(int msgId, long nonce, int status);
 
+/****** no end-to-end crypto
     protected static final Set createNewTags(int num) {
         Set tags = new HashSet();
         for (int i = 0; i < num; i++)
             tags.add(new SessionTag(true));
         return tags;
     }
+*******/
 
     /**
      * Recieve a payload message and let the app know its available
diff --git a/core/java/src/net/i2p/client/I2PSessionImpl2.java b/core/java/src/net/i2p/client/I2PSessionImpl2.java
index 981ccfae2316bf04a59fb5253f9b512dfde4f713..ac61ee703084e6504ba4982311c4c83181a34830 100644
--- a/core/java/src/net/i2p/client/I2PSessionImpl2.java
+++ b/core/java/src/net/i2p/client/I2PSessionImpl2.java
@@ -135,14 +135,28 @@ class I2PSessionImpl2 extends I2PSessionImpl {
         return sendMessage(dest, payload, offset, size, null, null, 0);
     }
     
+    /**
+     * @param keyUsed unused - no end-to-end crypto
+     * @param tagsSent unused - no end-to-end crypto
+     */
     @Override
     public boolean sendMessage(Destination dest, byte[] payload, SessionKey keyUsed, Set tagsSent) throws I2PSessionException {
         return sendMessage(dest, payload, 0, payload.length, keyUsed, tagsSent, 0);
     }
+
+    /**
+     * @param keyUsed unused - no end-to-end crypto
+     * @param tagsSent unused - no end-to-end crypto
+     */
     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);
     }
+
+    /**
+     * @param keyUsed unused - no end-to-end crypto
+     * @param tagsSent unused - no end-to-end crypto
+     */
     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");
@@ -198,13 +212,17 @@ class I2PSessionImpl2 extends I2PSessionImpl {
     
     private static final int NUM_TAGS = 50;
 
+    /**
+     * @param keyUsed unused - no end-to-end crypto
+     * @param tagsSent unused - no end-to-end crypto
+     */
     protected boolean sendBestEffort(Destination dest, byte payload[], SessionKey keyUsed, Set tagsSent, long expires)
                     throws I2PSessionException {
-        SessionKey key = null;
-        SessionKey newKey = null;
-        SessionTag tag = null;
-        Set sentTags = null;
-        int oldTags = 0;
+        //SessionKey key = null;
+        //SessionKey newKey = null;
+        //SessionTag tag = null;
+        //Set sentTags = null;
+        //int oldTags = 0;
         long begin = _context.clock().now();
         /***********
         if (I2CPMessageProducer.END_TO_END_CRYPTO) {
@@ -258,27 +276,27 @@ class I2PSessionImpl2 extends I2PSessionImpl {
         long nonce = _context.random().nextInt(Integer.MAX_VALUE);
         if (_log.shouldLog(Log.DEBUG)) _log.debug("before sync state");
         MessageState state = new MessageState(_context, nonce, getPrefix());
-        state.setKey(key);
-        state.setTags(sentTags);
-        state.setNewKey(newKey);
+        //state.setKey(key);
+        //state.setTags(sentTags);
+        //state.setNewKey(newKey);
         state.setTo(dest);
-        if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Setting key = " + key);
+        //if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Setting key = " + key);
 
-        if (keyUsed != null) {
+        //if (keyUsed != null) {
             //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());
+            //    keyUsed.setData(SessionKey.INVALID_KEY.getData());
             //}
-        }
-        if (tagsSent != null) {
-            if (sentTags != null) {
-                tagsSent.addAll(sentTags);
-            }
-        }
+        //}
+        //if (tagsSent != null) {
+        //    if (sentTags != null) {
+        //        tagsSent.addAll(sentTags);
+        //    }
+        //}
 
         if (_log.shouldLog(Log.DEBUG)) _log.debug("before sync state");
         long beforeSendingSync = _context.clock().now();
@@ -293,7 +311,8 @@ 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, expires);
+        //_producer.sendMessage(this, dest, nonce, payload, tag, key, sentTags, newKey, expires);
+        _producer.sendMessage(this, dest, nonce, payload, null, null, null, null, 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/I2PSessionMuxedImpl.java b/core/java/src/net/i2p/client/I2PSessionMuxedImpl.java
index 58b5cae9f29005e774df26d8bb407ecd0ca86a0a..c0533b1fff24a466102abad0cbec70342c3facc3 100644
--- a/core/java/src/net/i2p/client/I2PSessionMuxedImpl.java
+++ b/core/java/src/net/i2p/client/I2PSessionMuxedImpl.java
@@ -128,6 +128,10 @@ class I2PSessionMuxedImpl extends I2PSessionImpl2 implements I2PSession {
         return sendMessage(dest, payload, 0, payload.length, null, null, 0, proto, fromport, toport);
     }
 
+    /**
+     * @param keyUsed unused - no end-to-end crypto
+     * @param tagsSent unused - no end-to-end crypto
+     */
     @Override
     public boolean sendMessage(Destination dest, byte[] payload, int offset, int size,
                                SessionKey keyUsed, Set tagsSent, long expires)
@@ -135,6 +139,10 @@ class I2PSessionMuxedImpl extends I2PSessionImpl2 implements I2PSession {
         return sendMessage(dest, payload, offset, size, keyUsed, tagsSent, 0, PROTO_UNSPECIFIED, PORT_UNSPECIFIED, PORT_UNSPECIFIED);
     }
 
+    /**
+     * @param keyUsed unused - no end-to-end crypto
+     * @param tagsSent unused - no end-to-end crypto
+     */
     @Override
     public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent,
                                int proto, int fromport, int toport) throws I2PSessionException {
@@ -142,6 +150,8 @@ class I2PSessionMuxedImpl extends I2PSessionImpl2 implements I2PSession {
     }
 
     /**
+     *  @param keyUsed unused - no end-to-end crypto
+     *  @param tagsSent unused - no end-to-end crypto
      *  @param proto 1-254 or 0 for unset; recommended:
      *         I2PSession.PROTO_UNSPECIFIED
      *         I2PSession.PROTO_STREAMING
diff --git a/core/java/src/net/i2p/client/MessagePayloadMessageHandler.java b/core/java/src/net/i2p/client/MessagePayloadMessageHandler.java
index 4b17a67d3a4c2c4d35754deab90b65c28d9773e1..7b294e3b6e54333d61966cb743aeaa9469c2fcc1 100644
--- a/core/java/src/net/i2p/client/MessagePayloadMessageHandler.java
+++ b/core/java/src/net/i2p/client/MessagePayloadMessageHandler.java
@@ -22,6 +22,8 @@ import net.i2p.util.Log;
  * of a message by accepting it, decrypting the payload, adding it to the set of
  * recieved messages, and telling the router that it has been recieved correctly.
  *
+ * We don't really decrypt (no more end-to-end crypto)
+ *
  * @author jrandom
  */
 class MessagePayloadMessageHandler extends HandlerImpl {
@@ -51,21 +53,24 @@ class MessagePayloadMessageHandler extends HandlerImpl {
 
     /**
      * Decrypt the payload
+     *
+     * We don't really decrypt (no more end-to-end crypto)
+     * If we do, we need to use the correct key manager in the decrypt() call below
      */
     private Payload decryptPayload(MessagePayloadMessage msg, I2PSessionImpl session) throws DataFormatException {
         Payload payload = msg.getPayload();
-        if (!I2CPMessageProducer.END_TO_END_CRYPTO) {
+        //if (!I2CPMessageProducer.END_TO_END_CRYPTO) {
             payload.setUnencryptedData(payload.getEncryptedData());
             return payload;
-        }
+        //}
             
-        byte[] data = _context.elGamalAESEngine().decrypt(payload.getEncryptedData(), session.getDecryptionKey());
-        if (data == null) {
-            if (_log.shouldLog(Log.WARN))
-                _log.warn("Error decrypting the payload");
-            throw new DataFormatException("Unable to decrypt the payload");
-        }
-        payload.setUnencryptedData(data);
-        return payload;
+        //byte[] data = _context.elGamalAESEngine().decrypt(payload.getEncryptedData(), session.getDecryptionKey());
+        //if (data == null) {
+        //    if (_log.shouldLog(Log.WARN))
+        //        _log.warn("Error decrypting the payload");
+        //    throw new DataFormatException("Unable to decrypt the payload");
+        //}
+        //payload.setUnencryptedData(data);
+        //return payload;
     }
 }
diff --git a/core/java/src/net/i2p/crypto/ElGamalAESEngine.java b/core/java/src/net/i2p/crypto/ElGamalAESEngine.java
index 3e191faaa4d6f335021dfcad87830e3b000e1a30..98e9e62cc4134b1c6455830c9394dfae96c3d276 100644
--- a/core/java/src/net/i2p/crypto/ElGamalAESEngine.java
+++ b/core/java/src/net/i2p/crypto/ElGamalAESEngine.java
@@ -59,14 +59,18 @@ public class ElGamalAESEngine {
     }
 
     /**
-     * Decrypt the message using the given private key using tags from the given key manager.
+     * Decrypt the message using the given private key using tags from the default key manager.
+     *
+     * @deprecated specify the key manager!
      */
     public byte[] decrypt(byte data[], PrivateKey targetPrivateKey) throws DataFormatException {
         return decrypt(data, targetPrivateKey, _context.sessionKeyManager());
     }
 
     /**
-     * Decrypt the message using the given private key.  This works according to the
+     * Decrypt the message using the given private key
+     * and using tags from the specified key manager.
+     * This works according to the
      * ElGamal+AES algorithm in the data structure spec.
      *
      */
diff --git a/core/java/src/net/i2p/crypto/HMAC256Generator.java b/core/java/src/net/i2p/crypto/HMAC256Generator.java
index 0335d1e7eb0ef6f3edfe96373847548dd4df1622..e84489d97173337d30dcdf92a26c85ac23c47067 100644
--- a/core/java/src/net/i2p/crypto/HMAC256Generator.java
+++ b/core/java/src/net/i2p/crypto/HMAC256Generator.java
@@ -12,7 +12,7 @@ import org.bouncycastle.crypto.macs.I2PHMac;
 
 /**
  * Calculate the HMAC-SHA256 of a key+message.  All the good stuff occurs
- * in {@link org.bouncycastle.crypto.macs.HMac} and 
+ * in {@link org.bouncycastle.crypto.macs.I2PHMac} and 
  * {@link org.bouncycastle.crypto.digests.MD5Digest}.
  *
  */
diff --git a/core/java/src/net/i2p/crypto/HMACGenerator.java b/core/java/src/net/i2p/crypto/HMACGenerator.java
index e37ec92023efbc7fb4d9f3ea5305b383cb38f5c3..9bf06aa70e32f83391daa70aa755b57e55928bcf 100644
--- a/core/java/src/net/i2p/crypto/HMACGenerator.java
+++ b/core/java/src/net/i2p/crypto/HMACGenerator.java
@@ -15,7 +15,7 @@ import org.bouncycastle.crypto.macs.I2PHMac;
 
 /**
  * Calculate the HMAC-MD5 of a key+message.  All the good stuff occurs
- * in {@link org.bouncycastle.crypto.macs.HMac} and 
+ * in {@link org.bouncycastle.crypto.macs.I2PHMac} and 
  * {@link org.bouncycastle.crypto.digests.MD5Digest}.
  *
  */
diff --git a/core/java/src/net/i2p/crypto/SessionKeyManager.java b/core/java/src/net/i2p/crypto/SessionKeyManager.java
index b1547864cd4c2d4a279b07b158ae04ee4691284b..126ab0c0385086651a2a706f7cafc5315f42e7d9 100644
--- a/core/java/src/net/i2p/crypto/SessionKeyManager.java
+++ b/core/java/src/net/i2p/crypto/SessionKeyManager.java
@@ -9,6 +9,8 @@ package net.i2p.crypto;
  *
  */
 
+import java.io.IOException;
+import java.io.Writer;
 import java.util.Set;
 
 import net.i2p.I2PAppContext;
@@ -93,7 +95,8 @@ public class SessionKeyManager {
      * method after receiving an ack to a message delivering them)
      *
      */
-    public void tagsDelivered(PublicKey target, SessionKey key, Set<SessionTag> sessionTags) { // nop
+    public TagSetHandle tagsDelivered(PublicKey target, SessionKey key, Set<SessionTag> sessionTags) { // nop
+         return null;
     }
 
     /**
@@ -130,4 +133,8 @@ public class SessionKeyManager {
      */
     public void shutdown() { // nop
     }
+
+    public void renderStatusHTML(Writer out) throws IOException {}
+    public void failTags(PublicKey target, SessionKey key, TagSetHandle ts) {}
+    public void tagsAcked(PublicKey target, SessionKey key, TagSetHandle ts) {}
 }
diff --git a/core/java/src/net/i2p/crypto/TagSetHandle.java b/core/java/src/net/i2p/crypto/TagSetHandle.java
new file mode 100644
index 0000000000000000000000000000000000000000..7e06939b56ef6feef36d5f30bf632315d1b80668
--- /dev/null
+++ b/core/java/src/net/i2p/crypto/TagSetHandle.java
@@ -0,0 +1,8 @@
+package net.i2p.crypto;
+
+/**
+ * An opaque handle to a TagSet returned by the SessionKeyManager,
+ * so that OCMOSJ can report that the tags were later acked, or not.
+ *
+ */
+public interface TagSetHandle {}
diff --git a/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java b/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java
index 02be5acd578f19d634d703c2273b6d4b464f104a..931da3ffa0958ff61614d00bbaf0a0c78aa62765 100644
--- a/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java
+++ b/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java
@@ -9,14 +9,19 @@ package net.i2p.crypto;
  *
  */
 
+import java.io.IOException;
+import java.io.Writer;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.NoSuchElementException;
 import java.util.Set;
+import java.util.TreeSet;
 
 import net.i2p.I2PAppContext;
 import net.i2p.data.DataHelper;
@@ -32,6 +37,41 @@ import net.i2p.util.SimpleTimer;
  * to disk).  However, this being java, we cannot guarantee that the keys aren't swapped
  * out to disk so this should not be considered secure in that sense.
  *
+ * The outbound and inbound sides are completely independent, each with
+ * their own keys and tags.
+ *
+ * For a new session, outbound tags are not considered delivered until an ack is received.
+ * Otherwise, the loss of the first message would render all subsequent messages
+ * undecryptable. True?
+ *
+ * For an existing session, outbound tags are immediately considered delivered, and are
+ * later revoked if the ack times out. This prevents massive stream slowdown caused by
+ * repeated tag delivery after the minimum tag threshold is reached. Included tags
+ * pushes messages above the ideal 1956 size by ~2KB and causes excessive fragmentation
+ * and padding. As the tags are not seen by the streaming lib, they aren't accounted
+ * for in the window size, and one or more of a series of large messages is likely to be dropped,
+ * either due to high fragmentation or drop priorites at the tunnel OBEP.
+ *
+ * For this to work, the minimum tag threshold and tag delivery quanitity defined in
+ * GarlicMessageBuilder must be chosen with streaming lib windows sizes in mind.
+ * If a single TagSet is not delivered, there will be no stall as long as the
+ * current window size is smaller than the minimum tag threshold.
+ * Additional TagSets will be sent before the acked tags completely run out. See below.
+ * all subsequent messages will fail to decrypt.
+ * See ConnectionOptions in streaming for more information.
+ *
+ * There are large inefficiencies caused by the repeated delivery of tags in a new session.
+ * With an initial streaming window size of 6 and 40 tags per delivery, a web server
+ * would deliver up to 240 tags (7680 bytes, not including bundled leaseset, etc.)
+ * in the first volley of the response.
+ *
+ * Could the two directions be linked somehow, such that the initial request could
+ * contain a key or tags for the response?
+ *
+ * Should the tag threshold and quantity be adaptive?
+ *
+ * Todo: Switch to ConcurrentHashMaps and ReadWriteLocks, only get write lock during cleanup
+ *
  */
 public class TransientSessionKeyManager extends SessionKeyManager {
     private Log _log;
@@ -122,6 +162,7 @@ public class TransientSessionKeyManager extends SessionKeyManager {
     }
 
     /* FIXME Exporting non-public type through public API */
+/****** leftover from when we had the persistent SKM
     protected void setData(Set<TagSet> inboundTagSets, Set<OutboundSession> outboundSessions) {
         if (_log.shouldLog(Log.INFO))
             _log.info("Loading " + inboundTagSets.size() + " inbound tag sets, and " 
@@ -148,6 +189,7 @@ public class TransientSessionKeyManager extends SessionKeyManager {
             _outboundSessions.putAll(sessions);
         }
     }
+******/
 
     /**
      * Retrieve the session key currently associated with encryption to the target,
@@ -175,13 +217,10 @@ public class TransientSessionKeyManager extends SessionKeyManager {
      * Associate a new session key with the specified target.  Metrics to determine
      * when to expire that key begin with this call.
      *
-     * Unused except in tests?
      */
     @Override
     public void createSession(PublicKey target, SessionKey key) {
-        OutboundSession sess = new OutboundSession(target);
-        sess.setCurrentKey(key);
-        addSession(sess);
+        createAndReturnSession(target, key);
     }
 
     /**
@@ -214,7 +253,7 @@ public class TransientSessionKeyManager extends SessionKeyManager {
         if (sess.getCurrentKey().equals(key)) {
             SessionTag nxt = sess.consumeNext();
             if (_log.shouldLog(Log.DEBUG))
-                _log.debug("Tag consumed: " + nxt + " with key: " + key.toBase64());
+                _log.debug("OB Tag consumed: " + nxt + " with: " + key);
             return nxt;
         }
         if (_log.shouldLog(Log.DEBUG))
@@ -257,23 +296,31 @@ public class TransientSessionKeyManager extends SessionKeyManager {
 
     /**
      * Take note of the fact that the given sessionTags associated with the key for
-     * encryption to the target have definitely been received at the target (aka call this
-     * method after receiving an ack to a message delivering them)
+     * encryption to the target have been sent. Whether to use the tags immediately
+     * (i.e. assume they will be received) or to wait until an ack, is implementation dependent.
+     *
+     * Here, we wait for the ack if the session is new, otherwise we use right away.
+     * Will this work???
+     * If the tags are pipelined sufficiently, it will.
      *
+     * @return the TagSetHandle. Caller MUST subsequently call failTags() or tagsAcked()
+     * with this handle.
      */
     @Override
-    public void tagsDelivered(PublicKey target, SessionKey key, Set sessionTags) {
+    public TagSetHandle tagsDelivered(PublicKey target, SessionKey key, Set<SessionTag> sessionTags) {
         if (_log.shouldLog(Log.DEBUG)) {
             //_log.debug("Tags delivered to set " + set + " on session " + sess);
             if (sessionTags.size() > 0)
-                _log.debug("Tags delivered: " + sessionTags.size() + " for key: " + key.toBase64() + ": " + sessionTags);
+                _log.debug("Tags delivered: " + sessionTags.size() + " for key: " + key + ": " + sessionTags);
         }
         OutboundSession sess = getSession(target);
         if (sess == null)
             sess = createAndReturnSession(target, key);
-        sess.setCurrentKey(key);
+        else
+            sess.setCurrentKey(key);
         TagSet set = new TagSet(sessionTags, key, _context.clock().now());
         sess.addTags(set);
+        return set;
     }
 
     /**
@@ -281,12 +328,44 @@ public class TransientSessionKeyManager extends SessionKeyManager {
      * has failed to respond when they should have.  This call essentially lets the system recover
      * from corrupted tag sets and crashes
      *
+     * @deprecated unused and rather drastic
      */
     @Override
     public void failTags(PublicKey target) {
         removeSession(target);
     }
 
+    /**
+     * Mark these tags as invalid, since the peer
+     * has failed to ack them in time.
+     */
+    @Override
+    public void failTags(PublicKey target, SessionKey key, TagSetHandle ts) {
+        OutboundSession sess = getSession(target);
+        if (sess == null)
+            return;
+        if(!key.equals(sess.getCurrentKey()))
+            return;
+        sess.failTags((TagSet)ts);
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("TagSet failed: " + ts);
+    }
+
+    /**
+     * Mark these tags as acked, start to use them (if we haven't already)
+     */
+    @Override
+    public void tagsAcked(PublicKey target, SessionKey key, TagSetHandle ts) {
+        OutboundSession sess = getSession(target);
+        if (sess == null)
+            return;
+        if(!key.equals(sess.getCurrentKey()))
+            return;
+        sess.ackTags((TagSet)ts);
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("TagSet acked: " + ts);
+    }
+
     /**
      * Accept the given tags and associate them with the given key for decryption
      *
@@ -300,9 +379,9 @@ public class TransientSessionKeyManager extends SessionKeyManager {
         for (Iterator<SessionTag> iter = sessionTags.iterator(); iter.hasNext();) {
             SessionTag tag = iter.next();
             if (_log.shouldLog(Log.DEBUG))
-                _log.debug("Receiving tag " + tag + " for key " + key.toBase64() + " / " + key.toString() + ": tagSet: " + tagSet);
+                _log.debug("Receiving tag " + tag + " for key " + key + ": tagSet: " + tagSet);
             synchronized (_inboundTagSets) {
-                old = (TagSet)_inboundTagSets.put(tag, tagSet);
+                old = _inboundTagSets.put(tag, tagSet);
                 overage = _inboundTagSets.size() - MAX_INBOUND_SESSION_TAGS;
                 if (old != null) {
                     if (!old.getAssociatedKey().equals(tagSet.getAssociatedKey())) {
@@ -330,9 +409,9 @@ public class TransientSessionKeyManager extends SessionKeyManager {
             }
 
             if (_log.shouldLog(Log.WARN)) {
-                _log.warn("Multiple tags matching!  tagSet: " + tagSet + " and old tagSet: " + old + " tag: " + dupTag + "/" + dupTag.toBase64());
-                _log.warn("Earlier tag set creation: " + old + ": key=" + old.getAssociatedKey().toBase64(), old.getCreatedBy());
-                _log.warn("Current tag set creation: " + tagSet + ": key=" + tagSet.getAssociatedKey().toBase64(), tagSet.getCreatedBy());
+                _log.warn("Multiple tags matching!  tagSet: " + tagSet + " and old tagSet: " + old + " tag: " + dupTag + "/" + dupTag);
+                _log.warn("Earlier tag set creation: " + old + ": key=" + old.getAssociatedKey());
+                _log.warn("Current tag set creation: " + tagSet + ": key=" + tagSet.getAssociatedKey());
             }
         }
         
@@ -341,7 +420,7 @@ public class TransientSessionKeyManager extends SessionKeyManager {
 
         if ( (sessionTags.size() <= 0) && (_log.shouldLog(Log.DEBUG)) )
             _log.debug("Received 0 tags for key " + key);
-        if (false) aggressiveExpire();
+        //if (false) aggressiveExpire();
     }
     
     /**
@@ -406,26 +485,26 @@ public class TransientSessionKeyManager extends SessionKeyManager {
      */
     @Override
     public SessionKey consumeTag(SessionTag tag) {
-        if (false) aggressiveExpire();
+        //if (false) aggressiveExpire();
         synchronized (_inboundTagSets) {
             TagSet tagSet = (TagSet) _inboundTagSets.remove(tag);
             if (tagSet == null) {
                 if (_log.shouldLog(Log.DEBUG))
-                    _log.debug("Cannot consume tag " + tag + " as it is not known");
+                    _log.debug("Cannot consume IB " + tag + " as it is not known");
                 return null;
             }
             tagSet.consume(tag);
 
             SessionKey key = tagSet.getAssociatedKey();
             if (_log.shouldLog(Log.DEBUG))
-                _log.debug("Consuming tag " + tag.toString() + " for sessionKey " + key.toBase64() + " / " + key.toString() + " on tagSet: " + tagSet);
+                _log.debug("Consuming IB " + tag + " for " + key + " on: " + tagSet);
             return key;
         }
     }
 
     private OutboundSession getSession(PublicKey target) {
         synchronized (_outboundSessions) {
-            return (OutboundSession) _outboundSessions.get(target);
+            return _outboundSessions.get(target);
         }
     }
 
@@ -439,7 +518,7 @@ public class TransientSessionKeyManager extends SessionKeyManager {
         if (target == null) return;
         OutboundSession session = null;
         synchronized (_outboundSessions) {
-            session = (OutboundSession)_outboundSessions.remove(target);
+            session = _outboundSessions.remove(target);
         }
         if ( (session != null) && (_log.shouldLog(Log.WARN)) )
             _log.warn("Removing session tags with " + session.availableTags() + " available for "
@@ -457,11 +536,11 @@ public class TransientSessionKeyManager extends SessionKeyManager {
         int remaining = 0;
         long now = _context.clock().now();
         StringBuilder buf = null;
-        StringBuilder bufSummary = null;
+        //StringBuilder bufSummary = null;
         if (_log.shouldLog(Log.DEBUG)) {
             buf = new StringBuilder(128);
             buf.append("Expiring inbound: ");
-            bufSummary = new StringBuilder(1024);
+            //bufSummary = new StringBuilder(1024);
         }
         synchronized (_inboundTagSets) {
             for (Iterator<SessionTag> iter = _inboundTagSets.keySet().iterator(); iter.hasNext();) {
@@ -473,10 +552,10 @@ public class TransientSessionKeyManager extends SessionKeyManager {
                     iter.remove();
                     removed++;
                     if (buf != null)
-                        buf.append(tag.toString()).append(" @ age ").append(DataHelper.formatDuration(age));
-                } else if (false && (bufSummary != null) ) {
-                    bufSummary.append("\nTagSet: " + ts.toString() + ", key: " + ts.getAssociatedKey().toBase64()+"/" + ts.getAssociatedKey().toString() 
-                                      + ": tag: " + tag.toString());
+                        buf.append(tag).append(" @ age ").append(DataHelper.formatDuration(age));
+                //} else if (false && (bufSummary != null) ) {
+                //    bufSummary.append("\nTagSet: " + ts + ", key: " + ts.getAssociatedKey()
+                //                      + ": tag: " + tag);
                 }
             }
             remaining = _inboundTagSets.size();
@@ -484,8 +563,8 @@ public class TransientSessionKeyManager extends SessionKeyManager {
         _context.statManager().addRateData("crypto.sessionTagsRemaining", remaining, 0);
         if ( (buf != null) && (removed > 0) )
             _log.debug(buf.toString());
-        if (bufSummary != null)
-            _log.debug("Cleaning up with remaining: " + bufSummary.toString());
+        //if (bufSummary != null)
+        //    _log.debug("Cleaning up with remaining: " + bufSummary.toString());
 
         //_log.warn("Expiring tags: [" + tagsToDrop + "]");
 
@@ -494,74 +573,111 @@ public class TransientSessionKeyManager extends SessionKeyManager {
                 PublicKey key = iter.next();
                 OutboundSession sess = _outboundSessions.get(key);
                 removed += sess.expireTags();
-                if (sess.availableTags() <= 0) {
+                // don't kill a new session or one that's temporarily out of tags
+                if (sess.getLastUsedDate() < now - (SESSION_LIFETIME_MAX_MS / 2) &&
+                    sess.availableTags() <= 0) {
                     iter.remove();
-                    removed++;
+                    removed++;   // just to have a non-zero return value?
                 }
             }
         }
         return removed;
     }
 
-    public String renderStatusHTML() {
+    @Override
+    public void renderStatusHTML(Writer out) throws IOException {
         StringBuilder buf = new StringBuilder(1024);
-        buf.append("<h2>Inbound sessions</h2>");
-        buf.append("<table>");
+        buf.append("<h2>Inbound sessions</h2>" +
+                   "<table>");
         Set<TagSet> inbound = getInboundTagSets();
         Map<SessionKey, Set<TagSet>> inboundSets = new HashMap(inbound.size());
+        // Build a map of the inbound tag sets, grouped by SessionKey
         for (Iterator<TagSet> iter = inbound.iterator(); iter.hasNext();) {
             TagSet ts = iter.next();
             if (!inboundSets.containsKey(ts.getAssociatedKey())) inboundSets.put(ts.getAssociatedKey(), new HashSet());
             Set<TagSet> sets = inboundSets.get(ts.getAssociatedKey());
             sets.add(ts);
         }
+        int total = 0;
+        long now = _context.clock().now();
         for (Iterator<SessionKey> iter = inboundSets.keySet().iterator(); iter.hasNext();) {
             SessionKey skey = iter.next();
-            Set<TagSet> sets = inboundSets.get(skey);
-            buf.append("<tr><td><b>Session key</b>: ").append(skey.toBase64()).append("</td>");
-            buf.append("<td><b># Sets:</b> ").append(sets.size()).append("</td></tr>");
-            buf.append("<tr><td colspan=\"2\"><ul>");
+            Set<TagSet> sets = new TreeSet(new TagSetComparator());
+            sets.addAll(inboundSets.get(skey));
+            buf.append("<tr><td><b>Session key</b>: ").append(skey.toBase64()).append("</td>" +
+                       "<td><b># Sets:</b> ").append(sets.size()).append("</td></tr>" +
+                       "<tr><td colspan=\"2\"><ul>");
             for (Iterator<TagSet> siter = sets.iterator(); siter.hasNext();) {
                 TagSet ts = siter.next();
-                buf.append("<li><b>Received on:</b> ").append(new Date(ts.getDate())).append(" with ")
-                   .append(ts.getTags().size()).append(" tags remaining</li>");
+                int size = ts.getTags().size();
+                total += size;
+                buf.append("<li><b>Received:</b> ").append(DataHelper.formatDuration(now - ts.getDate())).append(" ago with ");
+                buf.append(size).append(" tags remaining</li>");
             }
-            buf.append("</ul></td></tr>");
-        }
-        buf.append("</table>");
-
-        buf.append("<h2><b>Outbound sessions</b></h2>");
-
-        buf.append("<table>");
+            buf.append("</ul></td></tr>\n");
+            out.write(buf.toString());
+            buf.setLength(0);
+        }
+        buf.append("<tr><th colspan=\"2\">Total tags: ").append(total).append(" (");
+        buf.append(DataHelper.formatSize(32*total)).append("B)</th></tr>\n" +
+                   "</table>" +
+                   "<h2><b>Outbound sessions</b></h2>" +
+                   "<table>");
+        total = 0;
         Set<OutboundSession> outbound = getOutboundSessions();
         for (Iterator<OutboundSession> iter = outbound.iterator(); iter.hasNext();) {
             OutboundSession sess = iter.next();
-            buf.append("<tr><td><b>Target key:</b> ").append(sess.getTarget().toString()).append("<br>");
-            buf.append("<b>Established:</b> ").append(new Date(sess.getEstablishedDate())).append("<br>");
-            buf.append("<b>Last Used:</b> ").append(new Date(sess.getLastUsedDate())).append("<br>");
-            buf.append("<b># Sets:</b> ").append(sess.getTagSets().size()).append("</td></tr>");
-            buf.append("<tr><td><b>Session key:</b> ").append(sess.getCurrentKey().toBase64()).append("</td></tr>");
-            buf.append("<tr><td><ul>");
-            for (Iterator<TagSet> siter = sess.getTagSets().iterator(); siter.hasNext();) {
+            Set<TagSet> sets = new TreeSet(new TagSetComparator());
+            sets.addAll(sess.getTagSets());
+            buf.append("<tr><td><b>Target key:</b> ").append(sess.getTarget().toBase64().substring(0, 64)).append("<br>" +
+                       "<b>Established:</b> ").append(DataHelper.formatDuration(now - sess.getEstablishedDate())).append(" ago<br>" +
+                       "<b>Last Used:</b> ").append(DataHelper.formatDuration(now - sess.getLastUsedDate())).append(" ago<br>" +
+                       "<b>Session key:</b> ").append(sess.getCurrentKey().toBase64()).append("</td>" +
+                       "<td><b># Sets:</b> ").append(sess.getTagSets().size()).append("</td></tr>" +
+                       "<tr><td colspan=\"2\"><ul>");
+            for (Iterator<TagSet> siter = sets.iterator(); siter.hasNext();) {
                 TagSet ts = siter.next();
-                buf.append("<li><b>Sent on:</b> ").append(new Date(ts.getDate())).append(" with ").append(
-                                                                                                          ts.getTags()
-                                                                                                            .size())
-                   .append(" tags remaining</li>");
+                int size = ts.getTags().size();
+                total += size;
+                buf.append("<li><b>Sent:</b> ").append(DataHelper.formatDuration(now - ts.getDate())).append(" ago with ");
+                buf.append(size).append(" tags remaining; acked? ").append(ts.getAcked()).append("</li>");
             }
-            buf.append("</ul></td></tr>");
+            buf.append("</ul></td></tr>\n");
+            out.write(buf.toString());
+            buf.setLength(0);
         }
-        buf.append("</table>");
+        buf.append("<tr><th colspan=\"2\">Total tags: ").append(total).append(" (");
+        buf.append(DataHelper.formatSize(32*total)).append("B)</th></tr>\n" +
+                   "</table>");
 
-        return buf.toString();
+        out.write(buf.toString());
     }
 
-    class OutboundSession {
+    /**
+     *  Just for the HTML method above so we can see what's going on easier
+     *  Earliest first
+     */
+    private static class TagSetComparator implements Comparator {
+         public int compare(Object l, Object r) {
+             return (int) (((TagSet)l).getDate() - ((TagSet)r).getDate());
+        }
+    }
+
+    private class OutboundSession {
         private PublicKey _target;
         private SessionKey _currentKey;
         private long _established;
         private long _lastUsed;
+        /** before the first ack, all tagsets go here. These are never expired, we rely
+            on the callers to call failTags() or ackTags() to remove them from this list. */
+        private /* FIXME final FIXME */ List<TagSet> _unackedTagSets;
+        /**
+         *  As tagsets are acked, they go here.
+         *  After the first ack, new tagsets go here (i.e. presumed acked)
+         */
         private /* FIXME final FIXME */ List<TagSet> _tagSets;
+        /** set to true after first tagset is acked */
+        private boolean _acked;
 
         public OutboundSession(PublicKey target) {
             this(target, null, _context.clock().now(), _context.clock().now(), new ArrayList());
@@ -572,13 +688,44 @@ public class TransientSessionKeyManager extends SessionKeyManager {
             _currentKey = curKey;
             _established = established;
             _lastUsed = lastUsed;
-            _tagSets = tagSets;
+            _unackedTagSets = tagSets;
+            _tagSets = new ArrayList();
         }
 
-        /** list of TagSet objects */
+        /**
+         *  @return list of TagSet objects
+         *  This is used only by renderStatusHTML().
+         *  It includes both acked and unacked TagSets.
+         */
         List<TagSet> getTagSets() {
+            List<TagSet> rv;
             synchronized (_tagSets) {
-                return new ArrayList(_tagSets);
+                rv = new ArrayList(_unackedTagSets);
+                rv.addAll(_tagSets);
+            }
+            return rv;
+        }
+
+        /**
+         *  got an ack for these tags
+         *  For tagsets delivered after the session was acked, this is a nop
+         *  because the tagset was originally placed directly on the acked list.
+         */
+        void ackTags(TagSet set) {
+            synchronized (_tagSets) {
+                if (_unackedTagSets.remove(set)) {
+                    _tagSets.add(set);
+                    _acked = true;
+                }
+            }
+            set.setAcked();
+        }
+
+        /** didn't get an ack for these tags */
+        void failTags(TagSet set) {
+            synchronized (_tagSets) {
+                _unackedTagSets.remove(set);
+                _tagSets.remove(set);
             }
         }
 
@@ -626,7 +773,7 @@ public class TransientSessionKeyManager extends SessionKeyManager {
             int removed = 0;
             synchronized (_tagSets) {
                 for (int i = 0; i < _tagSets.size(); i++) {
-                    TagSet set = (TagSet) _tagSets.get(i);
+                    TagSet set = _tagSets.get(i);
                     if (set.getDate() + SESSION_TAG_DURATION_MS <= now) {
                         _tagSets.remove(i);
                         i--;
@@ -642,7 +789,7 @@ public class TransientSessionKeyManager extends SessionKeyManager {
             _lastUsed = now;
             synchronized (_tagSets) {
                 while (_tagSets.size() > 0) {
-                    TagSet set = (TagSet) _tagSets.get(0);
+                    TagSet set = _tagSets.get(0);
                     if (set.getDate() + SESSION_TAG_DURATION_MS > now) {
                         SessionTag tag = set.consumeNext();
                         if (tag != null) return tag;
@@ -656,14 +803,21 @@ public class TransientSessionKeyManager extends SessionKeyManager {
             return null;
         }
 
+        /** @return the total number of tags in acked TagSets */
         public int availableTags() {
             int tags = 0;
             long now = _context.clock().now();
             synchronized (_tagSets) {
                 for (int i = 0; i < _tagSets.size(); i++) {
-                    TagSet set = (TagSet) _tagSets.get(i);
-                    if (set.getDate() + SESSION_TAG_DURATION_MS > now)
-                        tags += set.getTags().size();
+                    TagSet set = _tagSets.get(i);
+                    if (set.getDate() + SESSION_TAG_DURATION_MS > now) {
+                        int sz = set.getTags().size();
+                        // so tags are sent when the acked tags are below
+                        // 30, 17, and 4.
+                        if (!set.getAcked())
+                            sz /= 3;
+                        tags += sz;
+                    }
                 }
             }
             return tags;
@@ -689,19 +843,31 @@ public class TransientSessionKeyManager extends SessionKeyManager {
                 return -1;
         }
 
+        /**
+         *  If the session has never been acked, put the TagSet on the unacked list.
+         *  Otherwise, consider it good right away.
+         */
         public void addTags(TagSet set) {
             _lastUsed = _context.clock().now();
-            synchronized (_tagSets) {
-                _tagSets.add(set);
+            if (_acked) {
+                synchronized (_tagSets) {
+                    _tagSets.add(set);
+                }
+            } else {
+                synchronized (_unackedTagSets) {
+                    _unackedTagSets.add(set);
+                }
             }
         }
     }
 
-    static class TagSet {
+    private static class TagSet implements TagSetHandle {
         private Set<SessionTag> _sessionTags;
         private SessionKey _key;
         private long _date;
-        private Exception _createdBy;
+        //private Exception _createdBy;
+        /** did we get an ack for this tagset? */
+        private boolean _acked;
 
         public TagSet(Set<SessionTag> tags, SessionKey key, long date) {
             if (key == null) throw new IllegalArgumentException("Missing key");
@@ -709,12 +875,12 @@ public class TransientSessionKeyManager extends SessionKeyManager {
             _sessionTags = tags;
             _key = key;
             _date = date;
-            if (true) {
-                long now = I2PAppContext.getGlobalContext().clock().now();
-                _createdBy = new Exception("Created by: key=" + _key.toBase64() + " on " 
-                                           + new Date(now) + "/" + now 
-                                           + " via " + Thread.currentThread().getName());
-            }
+            //if (true) {
+            //    long now = I2PAppContext.getGlobalContext().clock().now();
+            //    _createdBy = new Exception("Created by: key=" + _key.toBase64() + " on " 
+            //                               + new Date(now) + "/" + now 
+            //                               + " via " + Thread.currentThread().getName());
+            //}
         }
 
         /** when the tag set was created */
@@ -740,27 +906,31 @@ public class TransientSessionKeyManager extends SessionKeyManager {
         }
 
         public void consume(SessionTag tag) {
-            if (contains(tag)) {
-                _sessionTags.remove(tag);
-            }
+            _sessionTags.remove(tag);
         }
 
+        /** let's do this without counting the elements first */
         public SessionTag consumeNext() {
-            if (_sessionTags.size() <= 0) {
+            SessionTag first;
+            try {
+                first = _sessionTags.iterator().next();
+            } catch (NoSuchElementException nsee) {
                 return null;
             }
-
-            SessionTag first = (SessionTag) _sessionTags.iterator().next();
             _sessionTags.remove(first);
             return first;
         }
         
-        public Exception getCreatedBy() { return _createdBy; }
+        //public Exception getCreatedBy() { return _createdBy; }
+
+        public void setAcked() { _acked = true; }
+        public boolean getAcked() { return _acked; }
         
+/******    this will return a dup if two in the same ms, so just use java
         @Override
         public int hashCode() {
             long rv = 0;
-            if (_key != null) rv = rv * 7 + _key.hashCode();
+            if (_key != null) rv = _key.hashCode();
             rv = rv * 7 + _date;
             // no need to hashCode the tags, key + date should be enough
             return (int) rv;
@@ -770,9 +940,20 @@ public class TransientSessionKeyManager extends SessionKeyManager {
         public boolean equals(Object o) {
             if ((o == null) || !(o instanceof TagSet)) return false;
             TagSet ts = (TagSet) o;
-            return DataHelper.eq(ts.getAssociatedKey(), getAssociatedKey()) 
+            return DataHelper.eq(ts.getAssociatedKey(), _key) 
                    //&& DataHelper.eq(ts.getTags(), getTags())
-                   && ts.getDate() == getDate();
+                   && ts.getDate() == _date;
+        }
+******/
+
+        @Override
+        public String toString() {
+            StringBuilder buf = new StringBuilder(256);
+            buf.append("TagSet established: ").append(new Date(_date));
+            buf.append(" Session key: ").append(_key.toBase64());
+            buf.append(" Size: ").append(_sessionTags.size());
+            buf.append(" Acked? ").append(_acked);
+            return buf.toString();
         }
     }
 }
diff --git a/core/java/src/net/i2p/data/SessionKey.java b/core/java/src/net/i2p/data/SessionKey.java
index 1b2ae8a1e018265caec46458d3d1ad1679204b70..17190ef3f0018898c56070a5ec4b5ed32feb9ad0 100644
--- a/core/java/src/net/i2p/data/SessionKey.java
+++ b/core/java/src/net/i2p/data/SessionKey.java
@@ -89,6 +89,8 @@ public class SessionKey extends DataStructureImpl {
 
     @Override
     public String toString() { 
+        return "SessionKey " + toBase64();
+      /****
         if (true) return super.toString(); 
         StringBuilder buf = new StringBuilder(64);
         buf.append("[SessionKey: ");
@@ -103,5 +105,6 @@ public class SessionKey extends DataStructureImpl {
         }
         buf.append("]");
         return buf.toString();
+      ****/
     }
 }
diff --git a/core/java/src/net/i2p/data/SessionTag.java b/core/java/src/net/i2p/data/SessionTag.java
index 54826e7e3f057fcfcd4ec31bf75a1475f701526d..d50b392b49c902d789788f74d1ebed7c42143539 100644
--- a/core/java/src/net/i2p/data/SessionTag.java
+++ b/core/java/src/net/i2p/data/SessionTag.java
@@ -58,4 +58,8 @@ public class SessionTag extends ByteArray {
         out.write(getData());
     }
 
-}
\ No newline at end of file
+    @Override
+    public String toString() {
+        return "SessionTag " + toBase64();
+    }
+}
diff --git a/history.txt b/history.txt
index dd75c702334c3ce8e09a418cfcb8a4fc03c80cfd..724b25709b802f0200aa74b8f206587f4e28f868 100644
--- a/history.txt
+++ b/history.txt
@@ -1,3 +1,103 @@
+2009-09-21 sponge
+    * fixups to SlackBuilds. requiredbuilder does the wrong thing, and
+      thinks that java is perl! This isn't really a big deal, 
+      the file format is simple enough and the requirements are known.
+
+2009-09-07 mkvore
+    * removes a SAM v1&2 bug
+
+2009-09-04 zzz
+    * SessionKeyManager, OCMOSJ, Garlic:
+      - Enable per-client SessionKeyManagers for better anonymity
+      - tagsDelivered() now means tags are sent, not acked.
+      - OCMOSJ uses the new TagSetHandle object returned from tagsDelivered()
+        to call tagsAcked() or failTags() as appropriate.
+      - Assume tags delivered on an established session to
+        reduce streaming lib stalls caused by massive tag deliveries;
+        should increase throughput and window sizes on long-lived streams
+      - Unacked tagsets on a new session are stored on a separate list
+      - Don't kill an OB Session just because it's temporarily out of tags
+      - Increase min tag threshold to 30 (was 20) due to new speculative
+        tags delivered scheme, and to increase effective max window
+      - More Java 5 and dead code cleanups, and more comments and javadoc,
+        debug logging cleanups
+      - Key toString()s for easier debugging
+      - HandleGarlicMessageJob: cleanup of unused things
+    * Tunnel TestJob:
+      - Consume the tag after a failed test so it doesn't
+        stay in the SKM
+      - Disable tests with router.disableTunnelTesting=true
+    * configkeyring.jsp: Add delete and cancel buttons
+    * Logging: Fix directory for rotated log
+    * TunnelDispatcher: Cleanup
+
+2009-09-02 sponge
+    * Small logic fix for dr|z3d
+
+2009-08-28 zzz
+    * Client: Fail if no date handshake after 30s or no leaseset
+      after 5m, rather than hanging forever.
+    * Console:
+      - Prevent OOMs in NewsFetcher or StatsSummarizer from
+        killing the router
+      - Fix favicon (-17)
+    * Data: Speed up many hashcodes
+    * DataHelper: Fix byte array hashcode for small arrays
+    * DecayingBloomFilter:
+      - Replace with new DecayingHashSet for 3 of 4 uses,
+        and also in the 4th if the router is low-bandwidth.
+        Saves 8 MB heap.
+    * EepGet, I2PSnark:
+      - New I2PSocketEepGet fetches through existing tunnels
+        rather than through the proxy
+      - Use new eepget for i2psnark
+      - Add a fake user agent for non-proxied fetches
+      - Cleanups
+    * NetDb:
+      - oops, store leaseset locally even when shutting down
+        (fix -16)
+      - Java 5 cleanups
+    * PRNG:
+      - Rename config option to prng.buffers (was router.prng.buffers)
+      - Change the default from 16 to 2 for I2PAppContext (saves 3.5MB)
+    * Tunnel:
+      - Adjust the random drop probability for the message size
+      - Concurrentify HashSetIVValidator
+    * TunnelPool:
+      - Don't test tunnels when shutting down
+      - Less rates
+      - Java 5 cleanups
+
+2009-08-24 zzz
+    * ClientManager:
+      - Prevent client destination theft by rejecting duplicates
+      - Java 5 cleanups
+    * Console:
+      - Put favicon on every page
+      - Make every page UTF-8, ☃ safe for snowmen
+      - Remove options boxes on configtunnels.jsp
+      - Fix UTF-8 form submission (i2ptunnel too)
+      - Throw 403 instead of 404 from flags.jsp and viewstat.jsp
+        so we don't render error.jsp
+    * I2CP: Fix the SessionConfig serializer in DataHelper,
+      so that UTF-8 tunnel names are not corrupted by
+      I2CP and can be displayed on the console
+    * Message: Move 2 unused classes out of the router lib (~15KB)
+      (more SKM prep)
+    * Message, I2PSession, SessionKeyManager, Console:
+      Prep for SessionKeyManager work in the router -
+      Fix up SKM renderStatusHTML(); add debug.jsp to see it;
+      Redefine getClientSessionKeyManager();
+      More cleanups
+    * Ministreaming: Kill deprecation warnings
+    * profiles.jsp: Bulletproofing, less memory usage
+    * Streaming, I2PSession:
+      Prep for SessionKeyManager work in the router -
+      Comment out, deprecate, and javadoc for unused keys and tags,
+      they are vestiges of end-to-end crypto
+    * Updates: Verify zip at startup before extracting
+    * Wrapper: Take a couple fields out of the log so it's narrower
+
 2009-08-20 zzz
     * Config files:
       - Add some path and encoding help
diff --git a/installer/resources/themes/console/classic/console.css b/installer/resources/themes/console/classic/console.css
index 8b04beace4e0a0012296df391d3403deb4b7d8c9..b9bcaed8a6d2c039d45d7fc4a65042130b6a505e 100644
--- a/installer/resources/themes/console/classic/console.css
+++ b/installer/resources/themes/console/classic/console.css
@@ -103,6 +103,28 @@ div.warning hr {
      margin: 5px 0;
 }
 
+/* console error messages */
+
+div.sorry {
+     padding: 20px;
+     background: #ddf;
+     margin: -2px 1px 0 195px;
+     border: 5px solid #bbf;
+     text-align: justify;
+     -moz-box-shadow: inset 0px 0px 0px 1px #d00;
+     word-wrap: break-word;
+     font-weight: bold;
+     color: #001;
+}
+
+div.sorry hr {
+     color: #001;
+     background: #001;
+     height: 1px;
+     border: 1px solid #001;
+     margin: 10px 0;
+}
+
 div.toolbar {
      margin: 0em 0em 2em 0em;
      font-weight: bold;
@@ -123,7 +145,7 @@ div.routersummary {
      width: 185px;
      color: inherit;
      margin: 0;
-     padding: 7px 1px;
+     padding: 10px 1px 7px 1px;
      text-align: center !important;
      border: 5px solid #bbf;
      font-size: 9pt;
diff --git a/installer/resources/themes/console/classic/images/i2plogo.png b/installer/resources/themes/console/classic/images/i2plogo.png
new file mode 100644
index 0000000000000000000000000000000000000000..52d1fd6c7d7e7f5ccd9c1cac26f5502a4549d007
Binary files /dev/null and b/installer/resources/themes/console/classic/images/i2plogo.png differ
diff --git a/installer/resources/themes/console/dark/console.css b/installer/resources/themes/console/dark/console.css
index d36db5c62b9bde8b2fbafe04711440bd9c408480..10eb945fc69587e8f59cb84abf35c933fd3b31b3 100644
--- a/installer/resources/themes/console/dark/console.css
+++ b/installer/resources/themes/console/dark/console.css
@@ -223,6 +223,34 @@ div.warning {
      word-wrap: break-word;
 }
 
+/* console error messages */
+
+div.sorry {
+     margin: 5px 15px 10px 220px;
+     padding: 20px 20px 20px 75px;
+     background: #005;
+     border: 1px solid #99f;
+     -moz-border-radius: 4px;
+     -khtml-border-radius: 4px;
+     border-radius: 4px;
+     text-align: justify;
+     background-image:url("images/errortriangle.png");
+     background-position:15px center;
+     background-repeat:no-repeat;
+     -moz-box-shadow: inset 0px 0px 0px 1px #d00;
+     word-wrap: break-word;
+     font-weight: bold;
+     color: #eef;
+}
+
+div.sorry hr {
+     color: #eef;
+     background: #eef;
+     height: 1px;
+     border: 1px solid #eef;
+     margin: 10px 0;
+}     
+
 div.main {
      margin: 0px 0px 20px 195px;
      padding: 0 15px 15px 25px;
diff --git a/installer/resources/themes/console/dark/i2plogo.png b/installer/resources/themes/console/dark/i2plogo.png
new file mode 100644
index 0000000000000000000000000000000000000000..0448b9065f4d491581a7ff6bfb23ae5ed545d6ea
Binary files /dev/null and b/installer/resources/themes/console/dark/i2plogo.png differ
diff --git a/installer/resources/themes/console/images/i2plogo.png b/installer/resources/themes/console/images/i2plogo.png
index 52d1fd6c7d7e7f5ccd9c1cac26f5502a4549d007..ac6a6c619c99e2b8574275ccedc0b92b0b00b206 100644
Binary files a/installer/resources/themes/console/images/i2plogo.png and b/installer/resources/themes/console/images/i2plogo.png differ
diff --git a/installer/resources/themes/console/light/console.css b/installer/resources/themes/console/light/console.css
index 6138ab4c8982631a4146717797215bf0e5cf3763..71c33841fde8dffb5688f0e4d54955e9df11310b 100644
--- a/installer/resources/themes/console/light/console.css
+++ b/installer/resources/themes/console/light/console.css
@@ -1,7 +1,7 @@
 /* Not yet complete. Subject to flux and change. dr|z3d - 07.25.09 */
 
 body {
-     margin: 25px 10px 0 5px;
+     margin: 15px 0 0 10px;
      padding: 0em;
      text-align: center;
      background: #eef;
@@ -23,6 +23,7 @@ pre {
      text-align: left;
      font: 8pt "Lucida Console", "DejaVu Sans Mono", Courier, mono;
      color: #333;
+     margin: 10px;
 }
 
 div.logo {
@@ -78,7 +79,7 @@ a:active{
 div.routersummaryouter {
      float: left;
      width: 215px;
-     margin: 0 0 10px 20px;
+     margin: 0 0 10px 0px;
      padding: 0;
      border: 0;
      clear: left;/* fixes a bug in Opera */
@@ -183,7 +184,6 @@ div.routersummary td {
      border: 0 !important;
 }
 
-
 div.routersummary tr:nth-child(even) {
      background-color: #f60;
      background-image: none !important;
@@ -194,6 +194,8 @@ div.routersummarytr:nth-child(odd) {
      background-image: none !important;
 }
 
+/* proxy error messages */
+
 div.warning {
      margin: 5px 20px 10px 240px;
      padding: 0px 25px 20px 75px;
@@ -212,8 +214,36 @@ div.warning {
      word-wrap: break-word;
 }
 
+/* console error messages */
+
+div.sorry {
+     margin: 5px 15px 10px 220px;
+     padding: 20px 20px 20px 75px;
+     background: #ffb;
+     border: 1px solid #002;
+     -moz-border-radius: 4px;
+     -khtml-border-radius: 4px;
+     border-radius: 4px;
+     text-align: justify;
+     background-image: url("images/errortriangle.png");
+     background-position: 15px center;
+     background-repeat: no-repeat;
+     -moz-box-shadow: inset 0px 0px 0px 1px #d00;
+     word-wrap: break-word;
+     font-weight: bold;
+     color: #331;
+}
+
+div.sorry hr {
+     color: #552;
+     background: #552;
+     height: 1px;
+     border: 1px solid #552;
+     margin: 10px 0;
+}     
+
 div.main {
-     margin: 0px 0px 20px 220px;
+     margin: 0px 0px 20px 195px;
      padding: 0 15px 15px 25px;
      background: #eef;
      text-align: left;
@@ -237,7 +267,7 @@ div.main textarea {
 }
 
 div.news {
-     margin: 0px 15px 20px 245px;
+     margin: 0px 15px 10px 220px;
      padding: 20px 30px 20px 30px;
      border: 1px solid #003;
      color: #410;
@@ -302,7 +332,7 @@ div.news h4 {
 
 div.confignav {
      padding: 15px 10px !important;
-     margin: 0 0 25px 0;
+     margin: 0 0px 15px 0;
      background: #ddf url('images/lightbluetile.png');
      -moz-border-radius: 4px;
      -khtml-border-radius: 4px;
@@ -316,8 +346,8 @@ div.confignav {
 }
 
 div.configure {
-     padding: 0 15px 15px 15px !important;
-     margin: 10px 0px 25px 0;
+     padding: 0 15px 0px 15px !important;
+     margin: 0px 0px 15px 0;
      background: #ddf url('images/lightbluetile.png');
      -moz-border-radius: 4px;
      -khtml-border-radius: 4px;
@@ -327,6 +357,17 @@ div.configure {
      min-width: 400px;
 }
 
+div.configure h3, div.graphspanel h3 {
+     border: 1px solid #002;
+     border-left: 5px solid #002;
+     padding: 3px 5px 3px 5px;
+     margin: 15px 0 15px 0;
+     border-radius: 0 4px 4px 0;
+     -moz-border-radius: 0 4px 4px 0;
+     -khtml-border-radius: 0 4px 4px 0;
+     background: #eef;
+}
+
 div.graphspanel {
      padding: 12px;
      margin: 10px 0px 25px 0;
@@ -365,7 +406,7 @@ div.graphspanel form {
 
 div.messages {
      padding: 10px;
-     margin: 10px 0 20px 0;
+     margin: 10px 0 15px 0;
      background: #ddf;
      -moz-border-radius: 4px;
      -khtml-border-radius: 4px;
@@ -397,7 +438,7 @@ table {
      border-collapse: collapse;
      width: 100%;
      border: 1px solid #000022;
-     margin: 5px 0px 5px 0px;
+     margin: 10px -15px 5px 0px;
      cell-padding: 1px;
      font-size: 7pt;
      background: #b4c8ff url('images/tabletitlelight.png') repeat-x;
@@ -462,7 +503,7 @@ div.main li {
      text-align: left;
      list-style: square;
      margin: 2px 5px 0px 20px;
-     padding: 1px 20px 1px 10px;
+     padding: 1px 10px 1px 10px;
      line-height: 150%;
      word-wrap: break-word;
 }
@@ -528,7 +569,7 @@ h1 {
      text-align: left;
      color: #002;
      padding: 10px 15px;
-     margin: 0 15px 25px 245px;
+     margin: 0 15px 15px 220px;
      font: normal bold 16pt/120% "Lucida Sans Unicode", "Bitstream Vera Sans", Verdana, Tahoma, Helvetica, sans-serif;
      letter-spacing: 0.15em;
      text-transform: uppercase;
@@ -554,7 +595,7 @@ h2 {
      border-radius: 4px;
      -moz-border-radius: 4px;
      -khtml-border-radius: 4px;
-     margin: 25px 0 20px 0 !important;
+     margin: 15px 0px 10px 0 !important;
      -moz-box-shadow: inset 0px 0px 1px 0px #002;
      word-wrap: break-word;
 }
@@ -571,7 +612,7 @@ h3 {
      border: 1px solid #002;
      border-left: 5px solid #002;
      padding: 3px 5px 3px 5px;
-     margin: 20px 0 15px 0;
+     margin: 10px 0 15px 0;
      border-radius: 0 4px 4px 0;
      -moz-border-radius: 0 4px 4px 0;
      -khtml-border-radius: 0 4px 4px 0;
@@ -764,8 +805,8 @@ form {}
 }
 
 .joblog {
-     margin: 25px 0 25px 0;
-     padding: 20px 40px 20px 40px !important;
+     margin: 15px 0;
+     padding: 10px 20px !important;
      border: 1px solid #003;
      background-color: #004;
      background: #ddf url('images/lightbluetile.png');
@@ -786,6 +827,10 @@ form {}
      word-wrap: break-word !important;
 }
 
+.joblog table {
+     margin-top: 10px;
+}
+
 .smallhead {
      font-size: 7pt
 }
diff --git a/installer/resources/themes/console/light/images/i2plogo.png b/installer/resources/themes/console/light/images/i2plogo.png
index 52d1fd6c7d7e7f5ccd9c1cac26f5502a4549d007..ac6a6c619c99e2b8574275ccedc0b92b0b00b206 100644
Binary files a/installer/resources/themes/console/light/images/i2plogo.png and b/installer/resources/themes/console/light/images/i2plogo.png differ
diff --git a/installer/resources/themes/console/snark.css b/installer/resources/themes/console/snark.css
index 13c233add72e0daeba1508dda90f2b9b3d8885cb..8b2a82673e6bfacd29cdc8711c06e8da92db3c21 100644
--- a/installer/resources/themes/console/snark.css
+++ b/installer/resources/themes/console/snark.css
@@ -1,335 +1,300 @@
 /* Not yet complete. Subject to flux and change. dr|z3d - 07.25.09 */
 
 body {
-	background-color: #eef;
-	color:#001;
-	font-family:"Lucida Sans Unicode", "Bitstream Vera Sans", Verdana, Tahoma, Helvetica, sans-serif;
-  font-size: 8pt;
+     background: #eef;
+     color: #001;
+     font: 8pt "Lucida Sans Unicode","Bitstream Vera Sans",Verdana,Tahoma,Helvetica,sans-serif;
 }
 
 .snarkTitle {
-	font-size: 12pt;
-	font-weight: bold;
-	text-align: center;
-}
-
-.snarkRefresh:link, .snarkRefresh:visited {
-  text-decoration: none !important;
-  text-transform: uppercase !important;
-  padding: 0 16px;
-  letter-spacing: 0.05em;
-  font-weight: bold;
-  font-size: 11pt;
-  color: #005;
-  text-shadow: 0px 0px 1px rgba(0, 0, 148, 0.9);
-  }
- 
- .snarkRefresh:hover{
-  text-decoration: none !important;
-  text-transform: uppercase !important;
-  padding: 0 16px;
-  letter-spacing: 0.05em;
-  font-weight: bold;
-  font-size: 11pt;
-  color: #f60;
-  border-bottom: 3px solid #f60;
-  border-top: 3px solid #f60;
-  text-shadow: 0px 0px 1px rgba(255, 128, 0, 0.9);
-  }
-  
-.snarkRefresh:active{
-  text-decoration: none !important;
-  text-transform: uppercase !important;
-  padding: 0 16px;
-  letter-spacing: 0.05em;
-  font-weight: bold;
-  font-size: 11pt;
-  color: #f30;
-  border-bottom: 3px solid #f30;
-  border-top: 3px solid #f30;
-  text-shadow: 0px 0px 1px rgba(255, 32, 0, 0.5);
-  }
+     font-size: 12pt;
+     font-weight: bold;
+     text-align: center;
+}
+
+.snarkRefresh:link,.snarkRefresh:visited {
+     text-decoration: none !important;
+     text-transform: uppercase !important;
+     padding: 0 16px;
+     letter-spacing: 0.05em;
+     font-weight: bold;
+     font-size: 11pt;
+     color: #005;
+     text-shadow: 0px 0px 1px rgba(0,0,148,0.9);
+}
+
+.snarkRefresh:hover {
+     text-decoration: none !important;
+     text-transform: uppercase !important;
+     padding: 0 16px;
+     letter-spacing: 0.05em;
+     font-weight: bold;
+     font-size: 11pt;
+     color: #f60;
+     border-bottom: 3px solid #f60;
+     border-top: 3px solid #f60;
+     text-shadow: 0px 0px 1px rgba(255,128,0,0.9);
+}
+
+.snarkRefresh:active {
+     text-decoration: none !important;
+     text-transform: uppercase !important;
+     padding: 0 16px;
+     letter-spacing: 0.05em;
+     font-weight: bold;
+     font-size: 11pt;
+     color: #f30;
+     border-bottom: 3px solid #f30;
+     border-top: 3px solid #f30;
+     text-shadow: 0px 0px 1px rgba(255,32,0,0.5);
+}
 
 .snarkMessages {
-  background-color: #f83;
-  font-family: "Lucida Console", "DejaVu Sans Mono", Courier, mono !important;
-  font-size: 9pt;
-  font-weight: bold;
-  text-align: left;
-  margin: 0 0px 10px 0px;
-  padding: 0;
-  border-spacing: 0px;
-  -moz-border-radius: 4px 0 0 0;
-  -khtml-border-radius: 4px;
-  border-radius: 4px;
-  border: 2px solid #930;
-  text-align: left; 
-  overflow: auto;            
-  background: #f40 url('../console/images/orangetile.png');
-  color: #531;
-  height: 64px;
-  width: auto;
+     font: bold 9pt "Lucida Console","DejaVu Sans Mono",Courier,mono !important;
+     text-align: left;
+     margin: 0 0px 10px 0px;
+     padding: 0;
+     border-spacing: 0px;
+     -moz-border-radius: 4px 0 0 0;
+     -khtml-border-radius: 4px;
+     border-radius: 4px;
+     border: 2px solid #930;
+     overflow: auto;
+     color: #531;
+     height: 64px;
+     width: auto;
+     background: #f83 url('../console/images/orangetile.png');
 }
 
 pre {
-  font-family: "Lucida Console", "DejaVu Sans Mono", Courier, mono !important;
-  width: 100%;
-  font-size: 8pt;
-  padding: 0;
-  text-align: left !important;
-  height: 8px;
+     width: 100%;
+     font: 8pt "Lucida Console","DejaVu Sans Mono",Courier,mono !important;
+     padding: 0;
+     text-align: left !important;
+     height: 8px;
 }
 
 table {
-	margin: 0px 0px 10px 0px;
-	border: 0px;
-	padding: 0px;
-	border-width: 0px;
-	border-spacing: 0px;
-  border-collapse: collapse;
+     margin: 0px 0px 10px 0px;
+     border: 0px;
+     padding: 0px;
+     border-spacing: 0px;
+     border-collapse: collapse;
 }
 
 th {
-	padding: 5px;
-  font-size: 8pt;
-  border-top: 1px outset #001;
-  border-bottom: 1px inset #001;  
-  background: #f60 url('/themes/console/images/tabletitleorange.png') repeat-x;
-/*  text-align: right; */
-  whitespace: nowrap;
+     padding: 4px;
+     font-size: 8pt;
+     border-top: 1px outset #001;
+     border-bottom: 1px inset #001;
+     background: #f60 url('/themes/console/images/tabletitleorange.png') repeat-x;
+     whitespace: nowrap;
 }
 
 .SnarkTorrents {
-	margin: 0;
-	border: 1px solid #001;
-  background-color: #f9f;
+     margin: 0;
+     border: 1px solid #001;
+     background: #f9f;
 }
 
 td {
-	padding: 5px;
-/*  text-align: right;*/
+     padding: 4px;
 }
+
 .snarkTorrentEven {
-	background-color: #fb1;
-  font-size: 7pt;
+     background: #fb1;
+     font-size: 7pt;
 }
+
 .snarkTorrentOdd {
-	background-color: #fa1;
-  font-size: 7pt;
+     background: #fa1;
+     font-size: 7pt;
 }
+
 .snarkNewTorrent {
-	font-size: 9pt;
+     font-size: 9pt;
 }
+
 .snarkAddInfo {
-	font-size: 9pt;
-  line-height: 130% !important;
+     font-size: 9pt;
+     line-height: 130% !important;
 }
+
 .snarkConfigTitle {
-	font-size: 11pt;
-  font-weight: bold;
-  text-decoration: underline;
+     font-size: 11pt;
+     font-weight: bold;
+     text-decoration: underline;
+     text-transform: uppercase;
+     text-shadow: 0px 0px 2px rgba(172,172,192,0.9);
 }
 
 .snarkConfig {
-  font-size: 10pt;
+     font-size: 10pt;
+     width: 100%;
 }
 
 .page {
-	background-color: #fff;
-	color:#310;
-  min-width: 800px !important;
-/*  max-width: 800px !important; */
-  margin: 5px 0px;
-  padding: 10px 10px 0px 10px;
-  -moz-border-radius: 4px;
-  -khtml-border-radius: 4px;
-  border-radius: 4px;
-  border: 1px solid #001;
-  font-size: 9pt !important;
-  line-height: 160% !important;
-  -moz-box-shadow: inset 0px 0px 1px 0px #002;
-  text-align: center;
-  background: #ddf url('../console/light/images/lightbluetile.png');
-  opacity: 1.0;
+     background: #fff;
+     color: #310;
+     min-width: 800px !important;
+     margin: 5px 0 0 0;
+     padding: 10px 10px 0px 10px;
+     -moz-border-radius: 4px;
+     -khtml-border-radius: 4px;
+     border-radius: 4px;
+     border: 1px solid #001;
+     font-size: 9pt !important;
+     line-height: 160% !important;
+     -moz-box-shadow: inset 0px 0px 1px 0px #002;
+     text-align: center;
+     opacity: 1.0;
 }
 
 form {
-  line-height: 250%
-}
-
-p { 
-  line-height: 150%
+     line-height: 250%;
 }
 
-a:link {
-  padding 5px;
+p {
+     line-height: 150%;
 }
 
 hr {
-  color: #003;
-  background: #003;
-  height: 1px;
-  border: 0px solid #003;
-  width: 100%;
-  margin: 10px 0 7px 0;
-  text-align: center;
+     color: #003;
+     background: #003;
+     height: 1px;
+     border: 0px solid #003;
+     width: 100%;
+     margin: 10px 0 7px 0;
+     text-align: center;
 }
 
-a:link{
-  color: #930;
-  text-decoration: none;
-  font-weight: bold;
-  word-wrap: break-word;
+a:link {
+     color: #930;
+     text-decoration: none;
+     font-weight: bold;
+     word-wrap: break-word;
 }
 
-a:visited{
-  color: #606;
-  text-decoration: none;
-  font-weight: bold;
+a:visited {
+     color: #606;
+     text-decoration: none;
+     font-weight: bold;
 }
 
-a:hover{
-  color: #900;
-  font-weight: bold;
+a:hover {
+     color: #900;
+     font-weight: bold;
 }
 
 input {
-/*  font-family: "Lucida Console", "DejaVu Sans Mono", Courier, mono !important;*/
-  font-size: 9pt;
-  font-weight: bold;
-  text-align: left;
-  padding: 2px;
-}     
+     font-size: 9pt;
+     font-weight: bold;
+     text-align: left;
+     padding: 2px;
+}
 
 select {
-	font-family:"Lucida Sans Unicode", "Bitstream Vera Sans", Verdana, Tahoma, Helvetica, sans-serif;
-  background-color: #ffe;
-  color: #310;
-  font-size: 9pt;
+     background: #ffe;
+     color: #310;
+     font: 9pt "Lucida Sans Unicode","Bitstream Vera Sans",Verdana,Tahoma,Helvetica,sans-serif;
 }
 
 img {
-  border: none;
-  margin: 5px 5px 0px 5px;
-  opacity: 1.0;
-  line-height: 100%
-}
-
-img:hover{
-  border: none;
-  margin: 5px 5px 0px 5px;
-  opacity: 0.5;
-  line-height: 100%
-}
-
-
-div.section {
-  margin: 0 0 10px 0;
-  padding: 10px;
-  background: #ffe;
-  border: 1px solid #001;
-  text-align: center;
-  color: #001;
-  -moz-border-radius: 4px;
-  -khtml-border-radius: 4px;
-  border-radius: 4px;
-  -moz-box-shadow: inset 0px 0px 1px 0px #002;
-  word-wrap: break-word;
-  text-align: center;
-  background: #ffe url('../console/light/images/tabletile.png');
-  opacity: 1.0;
-}
-
-div.mainsection {
-  margin: 0 0 10px 0;
-  padding: 10px;
-  background: #ffe;
-  border: 1px solid #001;
-  text-align: center;
-  color: #001;
-  -moz-border-radius: 4px;
-  -khtml-border-radius: 4px;
-  border-radius: 4px;
-  -moz-box-shadow: inset 0px 0px 1px 0px #002;
-  word-wrap: break-word;
-  text-align: center;
-  background: #ffe url('../console/light/images/tabletile.png');
-  opacity: 1.0;
+     border: none;
+     margin: 5px 5px 0px 5px;
+     opacity: 1.0;
+     line-height: 100%;
+}
+
+img: hover {
+     border: none;
+     margin: 5px 5px 0px 5px;
+     opacity: 0.5;
+     line-height: 100%;
+}
+
+div.section,div.mainsection {
+     margin: 0 0 10px 0;
+     padding: 10px;
+     border: 1px solid #001;
+     color: #001;
+     -moz-border-radius: 4px;
+     -khtml-border-radius: 4px;
+     border-radius: 4px;
+     -moz-box-shadow: inset 0px 0px 1px 0px #002;
+     word-wrap: break-word;
+     text-align: center;
+     background: #ffe url('../console/light/images/tabletile.png');
+     opacity: 1.0;
 }
 
 div.newtorrentsection {
-  margin: 0 0 10px 0;
-  padding: 10px;
-  background: #ffe;
-  border: 1px solid #001;
-  text-align: center;
-  color: #001;
-  -moz-border-radius: 4px;
-  -khtml-border-radius: 4px;
-  border-radius: 4px;
-  -moz-box-shadow: inset 0px 0px 1px 0px #002;
-  word-wrap: break-word;
-  text-align: center;
-  background: #ffe url('../console/images/yellowtile.png');
-  opacity: 1.0;
+     margin: 0 0 10px 0;
+     padding: 0 10px 10px 10px;
+     border: 1px solid #001;
+     text-align: center;
+     color: #001;
+     -moz-border-radius: 4px;
+     -khtml-border-radius: 4px;
+     border-radius: 4px;
+     -moz-box-shadow: inset 0px 0px 1px 0px #002;
+     word-wrap: break-word;
+     background: #bb4 url('../console/images/yellowtile.png');
+     opacity: 1.0;
 }
 
 div.addtorrentsection {
-  margin: 0 0 10px 0;
-  padding: 10px;
-  background: #ffe;
-  border: 1px solid #001;
-  text-align: center;
-  color: #001;
-  -moz-border-radius: 4px;
-  -khtml-border-radius: 4px;
-  border-radius: 4px;
-  -moz-box-shadow: inset 0px 0px 1px 0px #002;
-  word-wrap: break-word;
-  text-align: center;
-  background: #ffe url('../console/images/greentile.png');
-  opacity: 1.0;
+     margin: 0 0 10px 0;
+     padding: 0 10px 10px 10px;
+     border: 1px solid #001;
+     text-align: center;
+     color: #001;
+     -moz-border-radius: 4px;
+     -khtml-border-radius: 4px;
+     border-radius: 4px;
+     -moz-box-shadow: inset 0px 0px 1px 0px #002;
+     word-wrap: break-word;
+     background: #7f7 url('../console/images/greentile.png');
+     opacity: 1.0;
 }
 
 div.configsection {
-  margin: 0;
-  padding: 10px;
-  background: #ffe;
-  border: 1px solid #001;
-  color: #ffb;
-  -moz-border-radius: 4px;
-  -khtml-border-radius: 4px;
-  border-radius: 4px;
-  -moz-box-shadow: inset 0px 0px 0px 1px #900;
-  word-wrap: break-word;
-  text-align: center;
-  background: #ffe url('../console/light/images/darkbluetile.png');
-  font-weight: bold; /* red tile needs bold text! */
+     margin: 0;
+     padding: 0 10px 10px 10px;
+     border: 1px solid #001;
+     color: #ffb;
+     -moz-border-radius: 4px;
+     -khtml-border-radius: 4px;
+     border-radius: 4px;
+     -moz-box-shadow: inset 0px 0px 0px 1px #900;
+     word-wrap: break-word;
+     text-align: center;
+     background: #700 url('../console/light/images/darkbluetile.png');
+     font-weight: bold;/* red tile needs bold text! */
 }
 
-div.configsection a{
-  color: #f90;
+div.configsection a {
+     color: #f90;
 }
 
-div.configsection a:hover{
-  color: #f60;
-  text-decoration: underline;
+div.configsection a: hover {
+     color: #f60;
+     text-decoration: underline;
 }
 
 .snarknavbar {
-  margin: 0 0 10px 0 !important;
-  padding: 10px;
-  border: 1px solid #001;
-  -moz-border-radius: 4px;
-  -khtml-border-radius: 4px;
-  border-radius: 4px;
-  background: #eef;
-  -moz-box-shadow: inset 0px 0px 1px 0px #002;
-  background: #ddf url('../console/light/images/tabletile.png');
-  text-transform: uppercase !important;
-  letter-spacing: 0.05em;
-  font-weight: bold;
-  font-size: 11pt;
-  color: #001;
-  text-shadow: 0px 0px 1px rgba(0, 0, 148, 0.9);
-}
+     margin: 0 0 10px 0 !important;
+     padding: 10px;
+     border: 1px solid #001;
+     -moz-border-radius: 4px;
+     -khtml-border-radius: 4px;
+     border-radius: 4px;
+     -moz-box-shadow: inset 0px 0px 1px 0px #002;
+     background: #ddf url('../console/light/images/tabletile.png');
+     text-transform: uppercase !important;
+     letter-spacing: 0.05em;
+     font-weight: bold;
+     font-size: 11pt;
+     color: #001;
+     text-shadow: 0px 0px 1px rgba(0,0,148,0.9);
+ }
diff --git a/router/java/src/net/i2p/router/ClientManagerFacade.java b/router/java/src/net/i2p/router/ClientManagerFacade.java
index 0ce20df6adbf00fd566b3364faa696c4df26536a..1318232879f32d20b7d60c582630d47d7158e0d3 100644
--- a/router/java/src/net/i2p/router/ClientManagerFacade.java
+++ b/router/java/src/net/i2p/router/ClientManagerFacade.java
@@ -85,13 +85,13 @@ public abstract class ClientManagerFacade implements Service {
      *
      * @return set of Destination objects
      */
-    public Set listClients() { return Collections.EMPTY_SET; }
+    public Set<Destination> listClients() { return Collections.EMPTY_SET; }
     
     /**
      * Return the client's current config, or null if not connected
      *
      */
     public abstract SessionConfig getClientSessionConfig(Destination dest);
-    public abstract SessionKeyManager getClientSessionKeyManager(Destination dest);
+    public abstract SessionKeyManager getClientSessionKeyManager(Hash dest);
     public void renderStatusHTML(Writer out) throws IOException { }
 }
diff --git a/router/java/src/net/i2p/router/DummyClientManagerFacade.java b/router/java/src/net/i2p/router/DummyClientManagerFacade.java
index 5e362e3ddb105d54e9d45b79c6c60219192a6aa4..9c0c6838e0b3f4365ff123757d4fd8218ce46ba1 100644
--- a/router/java/src/net/i2p/router/DummyClientManagerFacade.java
+++ b/router/java/src/net/i2p/router/DummyClientManagerFacade.java
@@ -41,7 +41,7 @@ public class DummyClientManagerFacade extends ClientManagerFacade {
     public void messageDeliveryStatusUpdate(Destination fromDest, MessageId id, boolean delivered) {}
     
     public SessionConfig getClientSessionConfig(Destination _dest) { return null; }
-    public SessionKeyManager getClientSessionKeyManager(Destination _dest) { return null; }
+    public SessionKeyManager getClientSessionKeyManager(Hash _dest) { return null; }
     
     public void requestLeaseSet(Hash dest, LeaseSet set) {}
     
diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java
index ac10366983cf0be7649ed5cac52541e405a98dec..67b9de3fe26c9de41ee483a7977c2d5a75e78d20 100644
--- a/router/java/src/net/i2p/router/RouterVersion.java
+++ b/router/java/src/net/i2p/router/RouterVersion.java
@@ -18,7 +18,7 @@ public class RouterVersion {
     /** deprecated */
     public final static String ID = "Monotone";
     public final static String VERSION = CoreVersion.VERSION;
-    public final static long BUILD = 16;
+    public final static long BUILD = 20;
     /** for example "-test" */
     public final static String EXTRA = "";
     public final static String FULL_VERSION = VERSION + "-" + BUILD + EXTRA;
diff --git a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
index 90c7fe23bf6b1c69265adf3afcf43288916bdbe3..e5aa1b5abb4d2a9af84ff6f2d6a57c2178581ed5 100644
--- a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
+++ b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
@@ -18,6 +18,7 @@ import java.util.Map;
 import java.util.Set;
 
 import net.i2p.crypto.SessionKeyManager;
+import net.i2p.crypto.TransientSessionKeyManager;
 import net.i2p.data.Destination;
 import net.i2p.data.Hash;
 import net.i2p.data.LeaseSet;
@@ -188,11 +189,11 @@ public class ClientConnectionRunner {
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("SessionEstablished called for destination " + _destHashCache.toBase64());
         _config = config;
-        // per-dest unimplemented
-        //if (_sessionKeyManager == null)
-        //    _sessionKeyManager = new TransientSessionKeyManager(_context);
-        //else
-        //    _log.error("SessionEstablished called for twice for destination " + _destHashCache.toBase64().substring(0,4));
+        // per-destination session key manager to prevent rather easy correlation
+        if (_sessionKeyManager == null)
+            _sessionKeyManager = new TransientSessionKeyManager(_context);
+        else
+            _log.error("SessionEstablished called for twice for destination " + _destHashCache.toBase64().substring(0,4));
         _manager.destinationEstablished(this);
     }
     
diff --git a/router/java/src/net/i2p/router/client/ClientManager.java b/router/java/src/net/i2p/router/client/ClientManager.java
index 5b4a4fb535ac0b1586d0308562adefa562dd084e..cbc5d778b01b4749f6e31294a774df45638c3701 100644
--- a/router/java/src/net/i2p/router/client/ClientManager.java
+++ b/router/java/src/net/i2p/router/client/ClientManager.java
@@ -42,8 +42,8 @@ import net.i2p.util.Log;
 public class ClientManager {
     private Log _log;
     private ClientListenerRunner _listener;
-    private final HashMap _runners;        // Destination --> ClientConnectionRunner
-    private final Set _pendingRunners; // ClientConnectionRunner for clients w/out a Dest yet
+    private final HashMap<Destination, ClientConnectionRunner>  _runners;        // Destination --> ClientConnectionRunner
+    private final Set<ClientConnectionRunner> _pendingRunners; // ClientConnectionRunner for clients w/out a Dest yet
     private RouterContext _ctx;
 
     /** ms to wait before rechecking for inbound messages to deliver to clients */
@@ -90,21 +90,21 @@ public class ClientManager {
     public void shutdown() {
         _log.info("Shutting down the ClientManager");
         _listener.stopListening();
-        Set runners = new HashSet();
+        Set<ClientConnectionRunner> runners = new HashSet();
         synchronized (_runners) {
-            for (Iterator iter = _runners.values().iterator(); iter.hasNext();) {
-                ClientConnectionRunner runner = (ClientConnectionRunner)iter.next();
+            for (Iterator<ClientConnectionRunner> iter = _runners.values().iterator(); iter.hasNext();) {
+                ClientConnectionRunner runner = iter.next();
                 runners.add(runner);
             }
         }
         synchronized (_pendingRunners) {
-            for (Iterator iter = _pendingRunners.iterator(); iter.hasNext();) {
-                ClientConnectionRunner runner = (ClientConnectionRunner)iter.next();
+            for (Iterator<ClientConnectionRunner> iter = _pendingRunners.iterator(); iter.hasNext();) {
+                ClientConnectionRunner runner = iter.next();
                 runners.add(runner);
             }
         }
-        for (Iterator iter = runners.iterator(); iter.hasNext(); ) {
-            ClientConnectionRunner runner = (ClientConnectionRunner)iter.next();
+        for (Iterator<ClientConnectionRunner> iter = runners.iterator(); iter.hasNext(); ) {
+            ClientConnectionRunner runner = iter.next();
             runner.stopRunning();
         }
     }
@@ -131,15 +131,26 @@ public class ClientManager {
         }
     }
     
+    /**
+     * Add to the clients list. Check for a dup destination.
+     */
     public void destinationEstablished(ClientConnectionRunner runner) {
+        Destination dest = runner.getConfig().getDestination();
         if (_log.shouldLog(Log.DEBUG))
-            _log.debug("DestinationEstablished called for destination " + runner.getConfig().getDestination().calculateHash().toBase64());
+            _log.debug("DestinationEstablished called for destination " + dest.calculateHash().toBase64());
 
         synchronized (_pendingRunners) {
             _pendingRunners.remove(runner);
         }
+        boolean fail = false;
         synchronized (_runners) {
-            _runners.put(runner.getConfig().getDestination(), runner);
+            fail = _runners.containsKey(dest);
+            if (!fail)
+                _runners.put(dest, runner);
+        }
+        if (fail) {
+            _log.log(Log.CRIT, "Client attempted to register duplicate destination " + dest.calculateHash().toBase64());
+            runner.disconnectClient("Duplicate destination");
         }
     }
     
@@ -278,8 +289,8 @@ public class ClientManager {
         return true;
     }
 
-    public Set listClients() {
-        Set rv = new HashSet();
+    public Set<Destination> listClients() {
+        Set<Destination> rv = new HashSet();
         synchronized (_runners) {
             rv.addAll(_runners.keySet());
         }
@@ -293,7 +304,7 @@ public class ClientManager {
         long inLock = 0;
         synchronized (_runners) {
             inLock = _ctx.clock().now();
-            rv = (ClientConnectionRunner)_runners.get(dest);
+            rv = _runners.get(dest);
         }
         long afterLock = _ctx.clock().now();
         if (afterLock - beforeLock > 50) {
@@ -317,9 +328,10 @@ public class ClientManager {
     
     /**
      * Return the client's SessionKeyManager
-     *
+     * Use this instead of the RouterContext.sessionKeyManager()
+     * to prevent correlation attacks across destinations
      */
-    public SessionKeyManager getClientSessionKeyManager(Destination dest) {
+    public SessionKeyManager getClientSessionKeyManager(Hash dest) {
         ClientConnectionRunner runner = getRunner(dest);
         if (runner != null)
             return runner.getSessionKeyManager();
@@ -331,8 +343,8 @@ public class ClientManager {
         if (destHash == null) 
             return null;
         synchronized (_runners) {
-            for (Iterator iter = _runners.values().iterator(); iter.hasNext(); ) {
-                ClientConnectionRunner cur = (ClientConnectionRunner)iter.next();
+            for (Iterator<ClientConnectionRunner> iter = _runners.values().iterator(); iter.hasNext(); ) {
+                ClientConnectionRunner cur = iter.next();
                 if (cur.getDestHash().equals(destHash))
                     return cur;
 	    }
@@ -354,8 +366,8 @@ public class ClientManager {
         }
     }
     
-    Set getRunnerDestinations() {
-        Set dests = new HashSet();
+    Set<Destination> getRunnerDestinations() {
+        Set<Destination> dests = new HashSet();
         long beforeLock = _ctx.clock().now();
         long inLock = 0;
         synchronized (_runners) {
@@ -390,13 +402,13 @@ public class ClientManager {
         StringBuilder buf = new StringBuilder(8*1024);
         buf.append("<u><b>Local destinations</b></u><br>");
         
-        Map runners = null;
+        Map<Destination, ClientConnectionRunner> runners = null;
         synchronized (_runners) {
             runners = (Map)_runners.clone();
         }
-        for (Iterator iter = runners.keySet().iterator(); iter.hasNext(); ) {
-            Destination dest = (Destination)iter.next();
-            ClientConnectionRunner runner = (ClientConnectionRunner)runners.get(dest);
+        for (Iterator<Destination> iter = runners.keySet().iterator(); iter.hasNext(); ) {
+            Destination dest = iter.next();
+            ClientConnectionRunner runner = runners.get(dest);
             buf.append("<b>*</b> ").append(dest.calculateHash().toBase64().substring(0,6)).append("<br>\n");
             LeaseSet ls = runner.getLeaseSet();
             if (ls == null) {
diff --git a/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java b/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java
index 9e706beda02fcc7449cf19214a08eb722f7744fe..e90d12f53599c7facc7c17b967c3ddc7b0c5cc5d 100644
--- a/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java
+++ b/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java
@@ -194,7 +194,7 @@ public class ClientManagerFacadeImpl extends ClientManagerFacade {
      * Return the client's current manager or null if not connected
      *
      */
-    public SessionKeyManager getClientSessionKeyManager(Destination dest) {
+    public SessionKeyManager getClientSessionKeyManager(Hash dest) {
         if (_manager != null)
             return _manager.getClientSessionKeyManager(dest);
         else {
@@ -215,7 +215,7 @@ public class ClientManagerFacadeImpl extends ClientManagerFacade {
      * @return set of Destination objects
      */
     @Override
-    public Set listClients() {
+    public Set<Destination> listClients() {
         if (_manager != null)
             return _manager.listClients();
         else
diff --git a/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java b/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java
index a049b7b8cd0266a892c2768693b913d3d428712d..961c0f769f1a7e47cc4585df16acec6ea6845a77 100644
--- a/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java
+++ b/router/java/src/net/i2p/router/message/GarlicMessageBuilder.java
@@ -17,7 +17,7 @@ import java.util.Set;
 import net.i2p.crypto.SessionKeyManager;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
-import net.i2p.data.Destination;
+import net.i2p.data.Hash;
 import net.i2p.data.PublicKey;
 import net.i2p.data.SessionKey;
 import net.i2p.data.SessionTag;
@@ -59,14 +59,16 @@ public class GarlicMessageBuilder {
      *
      *  So a value somewhat higher than the low threshold
      *  seems appropriate.
+     *
+     *  Use care when adjusting these values. See ConnectionOptions in streaming,
+     *  and TransientSessionKeyManager in crypto, for more information.
      */
     private static final int DEFAULT_TAGS = 40;
-    private static final int LOW_THRESHOLD = 20;
+    private static final int LOW_THRESHOLD = 30;
 
-    public static int estimateAvailableTags(RouterContext ctx, PublicKey key, Destination local) {
-        // per-dest Unimplemented
-        //SessionKeyManager skm = ctx.clientManager().getClientSessionKeyManager(local);
-        SessionKeyManager skm = ctx.sessionKeyManager();
+    /** @param local non-null; do not use this method for the router's SessionKeyManager */
+    public static int estimateAvailableTags(RouterContext ctx, PublicKey key, Hash local) {
+        SessionKeyManager skm = ctx.clientManager().getClientSessionKeyManager(local);
         if (skm == null)
             return 0;
         SessionKey curKey = skm.getCurrentKey(key);
@@ -75,19 +77,54 @@ public class GarlicMessageBuilder {
         return skm.getAvailableTags(key, curKey);
     }
     
-    public static GarlicMessage buildMessage(RouterContext ctx, GarlicConfig config) {
-        return buildMessage(ctx, config, new SessionKey(), new HashSet());
+    /**
+     * Unused and probably a bad idea.
+     *
+     * Used below only on a recursive call if the garlic message contains a garlic message.
+     * We don't need the SessionKey or SesssionTags returned
+     * This uses the router's SKM, which is probably not what you want.
+     * This isn't fully implemented, because the key and tags aren't saved - maybe
+     * it should force elGamal?
+     *
+     * @param ctx scope
+     * @param config how/what to wrap
+     */
+    private static GarlicMessage buildMessage(RouterContext ctx, GarlicConfig config) {
+        Log log = ctx.logManager().getLog(GarlicMessageBuilder.class);
+        log.error("buildMessage 2 args, using router SKM", new Exception("who did it"));
+        return buildMessage(ctx, config, new SessionKey(), new HashSet(), ctx.sessionKeyManager());
     }
 
-    public static GarlicMessage buildMessage(RouterContext ctx, GarlicConfig config, SessionKey wrappedKey, Set wrappedTags) {
-        return buildMessage(ctx, config, wrappedKey, wrappedTags, DEFAULT_TAGS);
+    /**
+     * called by OCMJH
+     *
+     * @param ctx scope
+     * @param config how/what to wrap
+     * @param wrappedKey output parameter that will be filled with the sessionKey used
+     * @param wrappedTags output parameter that will be filled with the sessionTags used
+     */
+    public static GarlicMessage buildMessage(RouterContext ctx, GarlicConfig config, SessionKey wrappedKey, Set<SessionTag> wrappedTags,
+                                             SessionKeyManager skm) {
+        return buildMessage(ctx, config, wrappedKey, wrappedTags, DEFAULT_TAGS, false, skm);
     }
 
-    public static GarlicMessage buildMessage(RouterContext ctx, GarlicConfig config, SessionKey wrappedKey, Set wrappedTags, int numTagsToDeliver) {
+    /** unused */
+    /***
+    public static GarlicMessage buildMessage(RouterContext ctx, GarlicConfig config, SessionKey wrappedKey, Set wrappedTags,
+                                             int numTagsToDeliver) {
         return buildMessage(ctx, config, wrappedKey, wrappedTags, numTagsToDeliver, false);
     }
+    ***/
 
-    public static GarlicMessage buildMessage(RouterContext ctx, GarlicConfig config, SessionKey wrappedKey, Set wrappedTags, int numTagsToDeliver, boolean forceElGamal) {
+    /**
+     * @param ctx scope
+     * @param config how/what to wrap
+     * @param wrappedKey output parameter that will be filled with the sessionKey used
+     * @param wrappedTags output parameter that will be filled with the sessionTags used
+     * @param numTagsToDeliver only if the estimated available tags are below the threshold
+     */
+    private static GarlicMessage buildMessage(RouterContext ctx, GarlicConfig config, SessionKey wrappedKey, Set<SessionTag> wrappedTags,
+                                             int numTagsToDeliver, boolean forceElGamal, SessionKeyManager skm) {
         Log log = ctx.logManager().getLog(GarlicMessageBuilder.class);
         PublicKey key = config.getRecipientPublicKey();
         if (key == null) {
@@ -104,14 +141,14 @@ public class GarlicMessageBuilder {
         if (log.shouldLog(Log.INFO))
             log.info("Encrypted with public key " + key + " to expire on " + new Date(config.getExpiration()));
         
-        SessionKey curKey = ctx.sessionKeyManager().getCurrentKey(key);
+        SessionKey curKey = skm.getCurrentKey(key);
         SessionTag curTag = null;
         if (curKey == null)
-            curKey = ctx.sessionKeyManager().createSession(key);
+            curKey = skm.createSession(key);
         if (!forceElGamal) {
-            curTag = ctx.sessionKeyManager().consumeNextAvailableTag(key, curKey);
+            curTag = skm.consumeNextAvailableTag(key, curKey);
             
-            int availTags = ctx.sessionKeyManager().getAvailableTags(key, curKey);
+            int availTags = skm.getAvailableTags(key, curKey);
             if (log.shouldLog(Log.DEBUG))
                 log.debug("Available tags for encryption to " + key + ": " + availTags);
 
@@ -120,7 +157,7 @@ public class GarlicMessageBuilder {
                     wrappedTags.add(new SessionTag(true));
                 if (log.shouldLog(Log.INFO))
                     log.info("Too few are available (" + availTags + "), so we're including more");
-            } else if (ctx.sessionKeyManager().getAvailableTimeLeft(key, curKey) < 60*1000) {
+            } else if (skm.getAvailableTimeLeft(key, curKey) < 60*1000) {
                 // if we have enough tags, but they expire in under 30 seconds, we want more
                 for (int i = 0; i < numTagsToDeliver; i++)
                     wrappedTags.add(new SessionTag(true));
@@ -138,16 +175,19 @@ public class GarlicMessageBuilder {
     }
     
     /**
+     *  used by TestJob and directly above
+     *
      * @param ctx scope
      * @param config how/what to wrap
-     * @param wrappedKey output parameter that will be filled with the sessionKey used
+     * @param wrappedKey unused - why??
      * @param wrappedTags output parameter that will be filled with the sessionTags used
      * @param target public key of the location being garlic routed to (may be null if we 
      *               know the encryptKey and encryptTag)
      * @param encryptKey sessionKey used to encrypt the current message
      * @param encryptTag sessionTag used to encrypt the current message
      */
-    public static GarlicMessage buildMessage(RouterContext ctx, GarlicConfig config, SessionKey wrappedKey, Set wrappedTags, PublicKey target, SessionKey encryptKey, SessionTag encryptTag) {
+    public static GarlicMessage buildMessage(RouterContext ctx, GarlicConfig config, SessionKey wrappedKey, Set<SessionTag> wrappedTags,
+                                             PublicKey target, SessionKey encryptKey, SessionTag encryptTag) {
         Log log = ctx.logManager().getLog(GarlicMessageBuilder.class);
         if (config == null)
             throw new IllegalArgumentException("Null config specified");
@@ -209,6 +249,7 @@ public class GarlicMessageBuilder {
                         cloves[i] = buildClove(ctx, (PayloadGarlicConfig)c);
                     } else {
                         log.debug("Subclove IS NOT a payload garlic clove");
+                        // See notes below
                         cloves[i] = buildClove(ctx, c);
                     }
                     if (cloves[i] == null)
@@ -242,6 +283,22 @@ public class GarlicMessageBuilder {
         return buildCommonClove(ctx, clove, config);
     }
     
+    /**
+     *  UNUSED
+     *
+     *  The Garlic Message we are building contains another garlic message,
+     *  as specified by a GarlicConfig (NOT a PayloadGarlicConfig).
+     *
+     *  So this calls back to the top, to buildMessage(ctx, config),
+     *  which uses the router's SKM, i.e. the wrong one.
+     *  Unfortunately we've lost the reference to the SessionKeyManager way down here,
+     *  so we can't call buildMessage(ctx, config, key, tags, skm).
+     *
+     *  If we do ever end up constructing a garlic message that contains a garlic message,
+     *  we'll have to fix this by passing the skm through the last buildMessage,
+     *  through buildCloveSet, to here.
+     *
+     */
     private static byte[] buildClove(RouterContext ctx, GarlicConfig config) throws DataFormatException, IOException {
         GarlicClove clove = new GarlicClove(ctx);
         GarlicMessage msg = buildMessage(ctx, config);
diff --git a/router/java/src/net/i2p/router/message/GarlicMessageParser.java b/router/java/src/net/i2p/router/message/GarlicMessageParser.java
index 84ed50b2c56baf9b41cd6642dc759d2df31bf1f9..8d53fc2122a9cb789acc50362d09a91c01af3551 100644
--- a/router/java/src/net/i2p/router/message/GarlicMessageParser.java
+++ b/router/java/src/net/i2p/router/message/GarlicMessageParser.java
@@ -10,6 +10,7 @@ package net.i2p.router.message;
 
 import java.util.Date;
 
+import net.i2p.crypto.SessionKeyManager;
 import net.i2p.data.Certificate;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
@@ -32,13 +33,14 @@ public class GarlicMessageParser {
         _log = _context.logManager().getLog(GarlicMessageParser.class);
     }
     
-    public CloveSet getGarlicCloves(GarlicMessage message, PrivateKey encryptionKey) {
+    /** @param skm use tags from this session key manager */
+    public CloveSet getGarlicCloves(GarlicMessage message, PrivateKey encryptionKey, SessionKeyManager skm) {
         byte encData[] = message.getData();
         byte decrData[] = null;
         try {
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug("Decrypting with private key " + encryptionKey);
-            decrData = _context.elGamalAESEngine().decrypt(encData, encryptionKey);
+            decrData = _context.elGamalAESEngine().decrypt(encData, encryptionKey, skm);
         } catch (DataFormatException dfe) {
             if (_log.shouldLog(Log.WARN))
                 _log.warn("Error decrypting", dfe);
diff --git a/router/java/src/net/i2p/router/message/GarlicMessageReceiver.java b/router/java/src/net/i2p/router/message/GarlicMessageReceiver.java
index fcc5bbddf84dd37e73f540cd85e8a924884e4244..a12d55452e15ca898b59305908aa1ecb7b60d5c8 100644
--- a/router/java/src/net/i2p/router/message/GarlicMessageReceiver.java
+++ b/router/java/src/net/i2p/router/message/GarlicMessageReceiver.java
@@ -8,6 +8,7 @@ package net.i2p.router.message;
  *
  */
 
+import net.i2p.crypto.SessionKeyManager;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
 import net.i2p.data.PrivateKey;
@@ -47,13 +48,16 @@ public class GarlicMessageReceiver {
         _clientDestination = clientDestination;
         _parser = new GarlicMessageParser(context);
         _receiver = receiver;
+        //_log.error("New GMR dest = " + clientDestination);
     }
     
     public void receive(GarlicMessage message) {
         PrivateKey decryptionKey = null;
+        SessionKeyManager skm = null;
         if (_clientDestination != null) {
             LeaseSetKeys keys = _context.keyManager().getKeys(_clientDestination);
-            if (keys != null) {
+            skm = _context.clientManager().getClientSessionKeyManager(_clientDestination);
+            if (keys != null && skm != null) {
                 decryptionKey = keys.getDecryptionKey();
             } else {
                 if (_log.shouldLog(Log.WARN))
@@ -62,9 +66,10 @@ public class GarlicMessageReceiver {
             }
         } else {
             decryptionKey = _context.keyManager().getPrivateKey();
+            skm = _context.sessionKeyManager();
         }
         
-        CloveSet set = _parser.getGarlicCloves(message, decryptionKey);
+        CloveSet set = _parser.getGarlicCloves(message, decryptionKey, skm);
         if (set != null) {
             for (int i = 0; i < set.getCloveCount(); i++) {
                 GarlicClove clove = set.getClove(i);
diff --git a/router/java/src/net/i2p/router/message/HandleGarlicMessageJob.java b/router/java/src/net/i2p/router/message/HandleGarlicMessageJob.java
index 05aecfb89b4ab0980a81831d0023b6f6bdd56949..32959b3a4107a1c55b8ac3719d0167a375ccaa51 100644
--- a/router/java/src/net/i2p/router/message/HandleGarlicMessageJob.java
+++ b/router/java/src/net/i2p/router/message/HandleGarlicMessageJob.java
@@ -31,14 +31,18 @@ import net.i2p.util.Log;
 public class HandleGarlicMessageJob extends JobImpl implements GarlicMessageReceiver.CloveReceiver {
     private Log _log;
     private GarlicMessage _message;
-    private RouterIdentity _from;
-    private Hash _fromHash;
-    private Map _cloves; // map of clove Id --> Expiration of cloves we've already seen
+    //private RouterIdentity _from;
+    //private Hash _fromHash;
+    //private Map _cloves; // map of clove Id --> Expiration of cloves we've already seen
     //private MessageHandler _handler;
-    private GarlicMessageParser _parser;
+    //private GarlicMessageParser _parser;
    
     private final static int FORWARD_PRIORITY = 50;
     
+    /**
+     *  @param from ignored
+     *  @param fromHash ignored
+     */
     public HandleGarlicMessageJob(RouterContext context, GarlicMessage msg, RouterIdentity from, Hash fromHash) {
         super(context);
         _log = context.logManager().getLog(HandleGarlicMessageJob.class);
@@ -46,11 +50,11 @@ public class HandleGarlicMessageJob extends JobImpl implements GarlicMessageRece
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("New handle garlicMessageJob called w/ message from [" + from + "]", new Exception("Debug"));
         _message = msg;
-        _from = from;
-        _fromHash = fromHash;
-        _cloves = new HashMap();
+        //_from = from;
+        //_fromHash = fromHash;
+        //_cloves = new HashMap();
         //_handler = new MessageHandler(context);
-        _parser = new GarlicMessageParser(context);
+        //_parser = new GarlicMessageParser(context);
     }
     
     public String getName() { return "Handle Inbound Garlic Message"; }
diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java b/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java
index efbdd90f05fb2712f33db7855ecaf1db4699e470..75b5c8dc98253514f368655e33460ada05eaf5ae 100644
--- a/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java
+++ b/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java
@@ -17,6 +17,7 @@ import net.i2p.data.LeaseSet;
 import net.i2p.data.Payload;
 import net.i2p.data.PublicKey;
 import net.i2p.data.SessionKey;
+import net.i2p.data.SessionTag;
 import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.DataMessage;
 import net.i2p.data.i2np.DatabaseStoreMessage;
@@ -46,13 +47,15 @@ class OutboundClientMessageJobHelper {
      *
      * For now, its just a tunneled DeliveryStatusMessage
      *
+     * Unused?
+     *
      * @param bundledReplyLeaseSet if specified, the given LeaseSet will be packaged with the message (allowing
      *                             much faster replies, since their netDb search will return almost instantly)
      * @return garlic, or null if no tunnels were found (or other errors)
      */
     static GarlicMessage createGarlicMessage(RouterContext ctx, long replyToken, long expiration, PublicKey recipientPK, 
                                              Payload data, Hash from, Destination dest, TunnelInfo replyTunnel,
-                                             SessionKey wrappedKey, Set wrappedTags, 
+                                             SessionKey wrappedKey, Set<SessionTag> wrappedTags, 
                                              boolean requireAck, LeaseSet bundledReplyLeaseSet) {
         PayloadGarlicConfig dataClove = buildDataClove(ctx, data, dest, expiration);
         return createGarlicMessage(ctx, replyToken, expiration, recipientPK, dataClove, from, dest, replyTunnel, wrappedKey, 
@@ -62,15 +65,18 @@ class OutboundClientMessageJobHelper {
      * Allow the app to specify the data clove directly, which enables OutboundClientMessage to resend the
      * same payload (including expiration and unique id) in different garlics (down different tunnels)
      *
+     * This is called from OCMOSJ
+     *
      * @return garlic, or null if no tunnels were found (or other errors)
      */
     static GarlicMessage createGarlicMessage(RouterContext ctx, long replyToken, long expiration, PublicKey recipientPK, 
                                              PayloadGarlicConfig dataClove, Hash from, Destination dest, TunnelInfo replyTunnel, SessionKey wrappedKey, 
-                                             Set wrappedTags, boolean requireAck, LeaseSet bundledReplyLeaseSet) {
+                                             Set<SessionTag> wrappedTags, boolean requireAck, LeaseSet bundledReplyLeaseSet) {
         GarlicConfig config = createGarlicConfig(ctx, replyToken, expiration, recipientPK, dataClove, from, dest, replyTunnel, requireAck, bundledReplyLeaseSet);
         if (config == null)
             return null;
-        GarlicMessage msg = GarlicMessageBuilder.buildMessage(ctx, config, wrappedKey, wrappedTags);
+        GarlicMessage msg = GarlicMessageBuilder.buildMessage(ctx, config, wrappedKey, wrappedTags,
+                                                              ctx.clientManager().getClientSessionKeyManager(from));
         return msg;
     }
     
diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
index 5263a614ec6eb0f5e9d50499e775cb19da7c43c4..9e1ad88a46afc67095cde572ff8c54e1a6d8c0b2 100644
--- a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
+++ b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
@@ -10,6 +10,8 @@ import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
 
+import net.i2p.crypto.SessionKeyManager;
+import net.i2p.crypto.TagSetHandle;
 import net.i2p.data.Base64;
 import net.i2p.data.Certificate;
 import net.i2p.data.Destination;
@@ -20,6 +22,7 @@ import net.i2p.data.Payload;
 import net.i2p.data.PublicKey;
 import net.i2p.data.RouterInfo;
 import net.i2p.data.SessionKey;
+import net.i2p.data.SessionTag;
 import net.i2p.data.i2cp.MessageId;
 import net.i2p.data.i2np.DataMessage;
 import net.i2p.data.i2np.DeliveryInstructions;
@@ -471,7 +474,8 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
             return;
         }
 
-        int existingTags = GarlicMessageBuilder.estimateAvailableTags(getContext(), _leaseSet.getEncryptionKey(), _from);
+        int existingTags = GarlicMessageBuilder.estimateAvailableTags(getContext(), _leaseSet.getEncryptionKey(),
+                                                                      _from.calculateHash());
         _outTunnel = selectOutboundTunnel(_to);
         // boolean wantACK = _wantACK || existingTags <= 30 || getContext().random().nextInt(100) < 5;
         // what's the point of 5% random? possible improvements or replacements:
@@ -489,7 +493,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
         
         PublicKey key = _leaseSet.getEncryptionKey();
         SessionKey sessKey = new SessionKey();
-        Set tags = new HashSet();
+        Set<SessionTag> tags = new HashSet();
         // If we want an ack, bundle a leaseSet... (so he can get back to us)
         LeaseSet replyLeaseSet = getReplyLeaseSet(wantACK);
         // ... and vice versa  (so we know he got it)
@@ -531,8 +535,16 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
         SendTimeoutJob onFail = null;
         ReplySelector selector = null;
         if (wantACK) {
-            onReply = new SendSuccessJob(getContext(), sessKey, tags);
-            onFail = new SendTimeoutJob(getContext());
+            TagSetHandle tsh = null;
+            if ( (sessKey != null) && (tags != null) && (tags.size() > 0) ) {
+                if (_leaseSet != null) {
+                    SessionKeyManager skm = getContext().clientManager().getClientSessionKeyManager(_from.calculateHash());
+                    if (skm != null)
+                        tsh = skm.tagsDelivered(_leaseSet.getEncryptionKey(), sessKey, tags);
+                }
+            }
+            onReply = new SendSuccessJob(getContext(), sessKey, tsh);
+            onFail = new SendTimeoutJob(getContext(), sessKey, tsh);
             selector = new ReplySelector(token);
         }
         
@@ -550,9 +562,9 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
                            + _lease.getGateway().toBase64());
 
             DispatchJob dispatchJob = new DispatchJob(getContext(), msg, selector, onReply, onFail, (int)(_overallExpiration-getContext().clock().now()));
-            if (false) // dispatch may take 100+ms, so toss it in its own job
-                getContext().jobQueue().addJob(dispatchJob);
-            else
+            //if (false) // dispatch may take 100+ms, so toss it in its own job
+            //    getContext().jobQueue().addJob(dispatchJob);
+            //else
                 dispatchJob.runJob();
         } else {
             if (_log.shouldLog(Log.WARN))
@@ -848,6 +860,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
     
     /** build the payload clove that will be used for all of the messages, placing the clove in the status structure */
     private boolean buildClove() {
+// FIXME set SKM
         PayloadGarlicConfig clove = new PayloadGarlicConfig();
         
         DeliveryInstructions instructions = new DeliveryInstructions();
@@ -932,14 +945,14 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
      */
     private class SendSuccessJob extends JobImpl implements ReplyJob {
         private SessionKey _key;
-        private Set _tags;
+        private TagSetHandle _tags;
         
         /**
          * Create a new success job that will be fired when the message encrypted with
          * the given session key and bearing the specified tags are confirmed delivered.
          *
          */
-        public SendSuccessJob(RouterContext enclosingContext, SessionKey key, Set tags) {
+        public SendSuccessJob(RouterContext enclosingContext, SessionKey key, TagSetHandle tags) {
             super(enclosingContext);
             _key = key;
             _tags = tags;
@@ -955,10 +968,10 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
                            + ": SUCCESS!  msg " + _clientMessageId
                            + " sent after " + sendTime + "ms");
             
-            if ( (_key != null) && (_tags != null) && (_tags.size() > 0) ) {
-                if (_leaseSet != null)
-                    getContext().sessionKeyManager().tagsDelivered(_leaseSet.getEncryptionKey(),
-                                                                   _key, _tags);
+            if (_key != null && _tags != null && _leaseSet != null) {
+                SessionKeyManager skm = getContext().clientManager().getClientSessionKeyManager(_from.calculateHash());
+                if (skm != null)
+                    skm.tagsAcked(_leaseSet.getEncryptionKey(), _key, _tags);
             }
             
             long dataMsgId = _cloveId;
@@ -994,8 +1007,13 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
      *
      */
     private class SendTimeoutJob extends JobImpl {
-        public SendTimeoutJob(RouterContext enclosingContext) {
+        private SessionKey _key;
+        private TagSetHandle _tags;
+
+        public SendTimeoutJob(RouterContext enclosingContext, SessionKey key, TagSetHandle tags) {
             super(enclosingContext);
+            _key = key;
+            _tags = tags;
         }
         
         public String getName() { return "Send client message timed out"; }
@@ -1005,6 +1023,11 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
                            + ": Soft timeout through the lease " + _lease);
             
             _lease.setNumFailure(_lease.getNumFailure()+1);
+            if (_key != null && _tags != null && _leaseSet != null) {
+                SessionKeyManager skm = getContext().clientManager().getClientSessionKeyManager(_from.calculateHash());
+                if (skm != null)
+                    skm.failTags(_leaseSet.getEncryptionKey(), _key, _tags);
+            }
             dieFatal();
         }
     }
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodSearchJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodSearchJob.java
new file mode 100644
index 0000000000000000000000000000000000000000..810b29c4fa51308ce3700809f5ad3993f8725a84
--- /dev/null
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodSearchJob.java
@@ -0,0 +1,225 @@
+package net.i2p.router.networkdb.kademlia;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.i2p.data.Hash;
+import net.i2p.data.i2np.DatabaseLookupMessage;
+import net.i2p.data.i2np.DatabaseSearchReplyMessage;
+import net.i2p.data.i2np.DatabaseStoreMessage;
+import net.i2p.data.i2np.I2NPMessage;
+import net.i2p.router.Job;
+import net.i2p.router.JobImpl;
+import net.i2p.router.MessageSelector;
+import net.i2p.router.OutNetMessage;
+import net.i2p.router.ReplyJob;
+import net.i2p.router.RouterContext;
+import net.i2p.router.TunnelInfo;
+import net.i2p.util.Log;
+
+/**
+ * Try sending a search to some floodfill peers, but if we don't get a successful
+ * match within half the allowed lookup time, give up and start querying through
+ * the normal (kademlia) channels.  This should cut down on spurious lookups caused
+ * by simple delays in responses from floodfill peers
+ *
+ */
+public class FloodSearchJob extends JobImpl {
+    private Log _log;
+    private FloodfillNetworkDatabaseFacade _facade;
+    private Hash _key;
+    private final List _onFind;
+    private final List _onFailed;
+    private long _expiration;
+    private int _timeoutMs;
+    private long _origExpiration;
+    private boolean _isLease;
+    private volatile int _lookupsRemaining;
+    private volatile boolean _dead;
+    public FloodSearchJob(RouterContext ctx, FloodfillNetworkDatabaseFacade facade, Hash key, Job onFind, Job onFailed, int timeoutMs, boolean isLease) {
+        super(ctx);
+        _log = ctx.logManager().getLog(FloodSearchJob.class);
+        _facade = facade;
+        _key = key;
+        _onFind = new ArrayList();
+        _onFind.add(onFind);
+        _onFailed = new ArrayList();
+        _onFailed.add(onFailed);
+        int timeout = -1;
+        timeout = timeoutMs / FLOOD_SEARCH_TIME_FACTOR;
+        if (timeout < timeoutMs)
+            timeout = timeoutMs;
+        _timeoutMs = timeout;
+        _expiration = timeout + ctx.clock().now();
+        _origExpiration = timeoutMs + ctx.clock().now();
+        _isLease = isLease;
+        _lookupsRemaining = 0;
+        _dead = false;
+    }
+    void addDeferred(Job onFind, Job onFailed, long timeoutMs, boolean isLease) {
+        if (_dead) {
+            getContext().jobQueue().addJob(onFailed);
+        } else {
+            if (onFind != null) synchronized (_onFind) { _onFind.add(onFind); }
+            if (onFailed != null) synchronized (_onFailed) { _onFailed.add(onFailed); }
+        }
+    }
+    public long getExpiration() { return _expiration; }
+    private static final int CONCURRENT_SEARCHES = 2;
+    private static final int FLOOD_SEARCH_TIME_FACTOR = 2;
+    private static final int FLOOD_SEARCH_TIME_MIN = 30*1000;
+    public void runJob() {
+        // pick some floodfill peers and send out the searches
+        List floodfillPeers = _facade.getFloodfillPeers();
+        FloodLookupSelector replySelector = new FloodLookupSelector(getContext(), this);
+        ReplyJob onReply = new FloodLookupMatchJob(getContext(), this);
+        Job onTimeout = new FloodLookupTimeoutJob(getContext(), this);
+        OutNetMessage out = getContext().messageRegistry().registerPending(replySelector, onReply, onTimeout, _timeoutMs);
+
+        for (int i = 0; _lookupsRemaining < CONCURRENT_SEARCHES && i < floodfillPeers.size(); i++) {
+            Hash peer = (Hash)floodfillPeers.get(i);
+            if (peer.equals(getContext().routerHash()))
+                continue;
+            
+            DatabaseLookupMessage dlm = new DatabaseLookupMessage(getContext(), true);
+            TunnelInfo replyTunnel = getContext().tunnelManager().selectInboundTunnel();
+            TunnelInfo outTunnel = getContext().tunnelManager().selectOutboundTunnel();
+            if ( (replyTunnel == null) || (outTunnel == null) ) {
+                _dead = true;
+                List removed = null;
+                synchronized (_onFailed) {
+                    removed = new ArrayList(_onFailed);
+                    _onFailed.clear();
+                }
+                while (removed.size() > 0)
+                    getContext().jobQueue().addJob((Job)removed.remove(0));
+                getContext().messageRegistry().unregisterPending(out);
+                return;
+            }
+            dlm.setFrom(replyTunnel.getPeer(0));
+            dlm.setMessageExpiration(getContext().clock().now()+10*1000);
+            dlm.setReplyTunnel(replyTunnel.getReceiveTunnelId(0));
+            dlm.setSearchKey(_key);
+            
+            if (_log.shouldLog(Log.INFO))
+                _log.info(getJobId() + ": Floodfill search for " + _key.toBase64() + " to " + peer.toBase64());
+            getContext().tunnelDispatcher().dispatchOutbound(dlm, outTunnel.getSendTunnelId(0), peer);
+            _lookupsRemaining++;
+        }
+        
+        if (_lookupsRemaining <= 0) {
+            if (_log.shouldLog(Log.INFO))
+                _log.info(getJobId() + ": Floodfill search for " + _key.toBase64() + " had no peers to send to");
+            // no floodfill peers, go to the normal ones
+            getContext().messageRegistry().unregisterPending(out);
+            _facade.searchFull(_key, _onFind, _onFailed, _timeoutMs*FLOOD_SEARCH_TIME_FACTOR, _isLease);
+        }
+    }
+    public String getName() { return "NetDb search (phase 1)"; }
+    
+    Hash getKey() { return _key; }
+    void decrementRemaining() { _lookupsRemaining--; }
+    int getLookupsRemaining() { return _lookupsRemaining; }
+    
+    void failed() {
+        if (_dead) return;
+        _dead = true;
+        int timeRemaining = (int)(_origExpiration - getContext().clock().now());
+        if (_log.shouldLog(Log.INFO))
+            _log.info(getJobId() + ": Floodfill search for " + _key.toBase64() + " failed with " + timeRemaining);
+        if (timeRemaining > 0) {
+            _facade.searchFull(_key, _onFind, _onFailed, timeRemaining, _isLease);
+        } else {
+            List removed = null;
+            synchronized (_onFailed) {
+                removed = new ArrayList(_onFailed);
+                _onFailed.clear();
+            }
+            while (removed.size() > 0)
+                getContext().jobQueue().addJob((Job)removed.remove(0));
+        }
+    }
+    void success() {
+        if (_dead) return;
+        if (_log.shouldLog(Log.INFO))
+            _log.info(getJobId() + ": Floodfill search for " + _key.toBase64() + " successful");
+        _dead = true;
+        _facade.complete(_key);
+        List removed = null;
+        synchronized (_onFind) {
+            removed = new ArrayList(_onFind);
+            _onFind.clear();
+        }
+        while (removed.size() > 0)
+            getContext().jobQueue().addJob((Job)removed.remove(0));
+    }
+
+    private static class FloodLookupTimeoutJob extends JobImpl {
+        private FloodSearchJob _search;
+        public FloodLookupTimeoutJob(RouterContext ctx, FloodSearchJob job) {
+            super(ctx);
+            _search = job;
+        }
+        public void runJob() {
+            _search.decrementRemaining();
+            if (_search.getLookupsRemaining() <= 0)
+                _search.failed(); 
+        }
+        public String getName() { return "NetDb search (phase 1) timeout"; }
+    }
+
+    private static class FloodLookupMatchJob extends JobImpl implements ReplyJob {
+        private Log _log;
+        private FloodSearchJob _search;
+        public FloodLookupMatchJob(RouterContext ctx, FloodSearchJob job) {
+            super(ctx);
+            _log = ctx.logManager().getLog(FloodLookupMatchJob.class);
+            _search = job;
+        }
+        public void runJob() { 
+            if ( (getContext().netDb().lookupLeaseSetLocally(_search.getKey()) != null) ||
+                 (getContext().netDb().lookupRouterInfoLocally(_search.getKey()) != null) ) {
+                _search.success();
+            } else {
+                int remaining = _search.getLookupsRemaining();
+                if (_log.shouldLog(Log.INFO))
+                    _log.info(getJobId() + "/" + _search.getJobId() + ": got a reply looking for " 
+                              + _search.getKey().toBase64() + ", with " + remaining + " outstanding searches");
+                // netDb reply pointing us at other people
+                if (remaining <= 0)
+                    _search.failed();
+            }
+        }
+        public String getName() { return "NetDb search (phase 1) match"; }
+        public void setMessage(I2NPMessage message) {}
+    }
+
+    private static class FloodLookupSelector implements MessageSelector {
+        private RouterContext _context;
+        private FloodSearchJob _search;
+        public FloodLookupSelector(RouterContext ctx, FloodSearchJob search) {
+            _context = ctx;
+            _search = search;
+        }
+        public boolean continueMatching() { return _search.getLookupsRemaining() > 0; }
+        public long getExpiration() { return _search.getExpiration(); }
+        public boolean isMatch(I2NPMessage message) {
+            if (message == null) return false;
+            if (message instanceof DatabaseStoreMessage) {
+                DatabaseStoreMessage dsm = (DatabaseStoreMessage)message;
+                // is it worth making sure the reply came in on the right tunnel?
+                if (_search.getKey().equals(dsm.getKey())) {
+                    _search.decrementRemaining();
+                    return true;
+                }
+            } else if (message instanceof DatabaseSearchReplyMessage) {
+                DatabaseSearchReplyMessage dsrm = (DatabaseSearchReplyMessage)message;
+                if (_search.getKey().equals(dsrm.getSearchKey())) {
+                    _search.decrementRemaining();
+                    return true;
+                }
+            }
+            return false;
+        }   
+    }
+}
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java
index 3772b18d1ee2694f0e542ac20f2b462d73c3cc4f..2b8659c697a9cfd7a12e9fbb865bd75238ab503c 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java
@@ -351,210 +351,3 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad
         }
     }
 }
-
-/**
- * Try sending a search to some floodfill peers, but if we don't get a successful
- * match within half the allowed lookup time, give up and start querying through
- * the normal (kademlia) channels.  This should cut down on spurious lookups caused
- * by simple delays in responses from floodfill peers
- *
- */
-class FloodSearchJob extends JobImpl {
-    private Log _log;
-    private FloodfillNetworkDatabaseFacade _facade;
-    private Hash _key;
-    private final List _onFind;
-    private final List _onFailed;
-    private long _expiration;
-    private int _timeoutMs;
-    private long _origExpiration;
-    private boolean _isLease;
-    private volatile int _lookupsRemaining;
-    private volatile boolean _dead;
-    public FloodSearchJob(RouterContext ctx, FloodfillNetworkDatabaseFacade facade, Hash key, Job onFind, Job onFailed, int timeoutMs, boolean isLease) {
-        super(ctx);
-        _log = ctx.logManager().getLog(FloodSearchJob.class);
-        _facade = facade;
-        _key = key;
-        _onFind = new ArrayList();
-        _onFind.add(onFind);
-        _onFailed = new ArrayList();
-        _onFailed.add(onFailed);
-        int timeout = -1;
-        timeout = timeoutMs / FLOOD_SEARCH_TIME_FACTOR;
-        if (timeout < timeoutMs)
-            timeout = timeoutMs;
-        _timeoutMs = timeout;
-        _expiration = timeout + ctx.clock().now();
-        _origExpiration = timeoutMs + ctx.clock().now();
-        _isLease = isLease;
-        _lookupsRemaining = 0;
-        _dead = false;
-    }
-    void addDeferred(Job onFind, Job onFailed, long timeoutMs, boolean isLease) {
-        if (_dead) {
-            getContext().jobQueue().addJob(onFailed);
-        } else {
-            if (onFind != null) synchronized (_onFind) { _onFind.add(onFind); }
-            if (onFailed != null) synchronized (_onFailed) { _onFailed.add(onFailed); }
-        }
-    }
-    public long getExpiration() { return _expiration; }
-    private static final int CONCURRENT_SEARCHES = 2;
-    private static final int FLOOD_SEARCH_TIME_FACTOR = 2;
-    private static final int FLOOD_SEARCH_TIME_MIN = 30*1000;
-    public void runJob() {
-        // pick some floodfill peers and send out the searches
-        List floodfillPeers = _facade.getFloodfillPeers();
-        FloodLookupSelector replySelector = new FloodLookupSelector(getContext(), this);
-        ReplyJob onReply = new FloodLookupMatchJob(getContext(), this);
-        Job onTimeout = new FloodLookupTimeoutJob(getContext(), this);
-        OutNetMessage out = getContext().messageRegistry().registerPending(replySelector, onReply, onTimeout, _timeoutMs);
-
-        for (int i = 0; _lookupsRemaining < CONCURRENT_SEARCHES && i < floodfillPeers.size(); i++) {
-            Hash peer = (Hash)floodfillPeers.get(i);
-            if (peer.equals(getContext().routerHash()))
-                continue;
-            
-            DatabaseLookupMessage dlm = new DatabaseLookupMessage(getContext(), true);
-            TunnelInfo replyTunnel = getContext().tunnelManager().selectInboundTunnel();
-            TunnelInfo outTunnel = getContext().tunnelManager().selectOutboundTunnel();
-            if ( (replyTunnel == null) || (outTunnel == null) ) {
-                _dead = true;
-                List removed = null;
-                synchronized (_onFailed) {
-                    removed = new ArrayList(_onFailed);
-                    _onFailed.clear();
-                }
-                while (removed.size() > 0)
-                    getContext().jobQueue().addJob((Job)removed.remove(0));
-                getContext().messageRegistry().unregisterPending(out);
-                return;
-            }
-            dlm.setFrom(replyTunnel.getPeer(0));
-            dlm.setMessageExpiration(getContext().clock().now()+10*1000);
-            dlm.setReplyTunnel(replyTunnel.getReceiveTunnelId(0));
-            dlm.setSearchKey(_key);
-            
-            if (_log.shouldLog(Log.INFO))
-                _log.info(getJobId() + ": Floodfill search for " + _key.toBase64() + " to " + peer.toBase64());
-            getContext().tunnelDispatcher().dispatchOutbound(dlm, outTunnel.getSendTunnelId(0), peer);
-            _lookupsRemaining++;
-        }
-        
-        if (_lookupsRemaining <= 0) {
-            if (_log.shouldLog(Log.INFO))
-                _log.info(getJobId() + ": Floodfill search for " + _key.toBase64() + " had no peers to send to");
-            // no floodfill peers, go to the normal ones
-            getContext().messageRegistry().unregisterPending(out);
-            _facade.searchFull(_key, _onFind, _onFailed, _timeoutMs*FLOOD_SEARCH_TIME_FACTOR, _isLease);
-        }
-    }
-    public String getName() { return "NetDb search (phase 1)"; }
-    
-    Hash getKey() { return _key; }
-    void decrementRemaining() { _lookupsRemaining--; }
-    int getLookupsRemaining() { return _lookupsRemaining; }
-    
-    void failed() {
-        if (_dead) return;
-        _dead = true;
-        int timeRemaining = (int)(_origExpiration - getContext().clock().now());
-        if (_log.shouldLog(Log.INFO))
-            _log.info(getJobId() + ": Floodfill search for " + _key.toBase64() + " failed with " + timeRemaining);
-        if (timeRemaining > 0) {
-            _facade.searchFull(_key, _onFind, _onFailed, timeRemaining, _isLease);
-        } else {
-            List removed = null;
-            synchronized (_onFailed) {
-                removed = new ArrayList(_onFailed);
-                _onFailed.clear();
-            }
-            while (removed.size() > 0)
-                getContext().jobQueue().addJob((Job)removed.remove(0));
-        }
-    }
-    void success() {
-        if (_dead) return;
-        if (_log.shouldLog(Log.INFO))
-            _log.info(getJobId() + ": Floodfill search for " + _key.toBase64() + " successful");
-        _dead = true;
-        _facade.complete(_key);
-        List removed = null;
-        synchronized (_onFind) {
-            removed = new ArrayList(_onFind);
-            _onFind.clear();
-        }
-        while (removed.size() > 0)
-            getContext().jobQueue().addJob((Job)removed.remove(0));
-    }
-}
-
-class FloodLookupTimeoutJob extends JobImpl {
-    private FloodSearchJob _search;
-    public FloodLookupTimeoutJob(RouterContext ctx, FloodSearchJob job) {
-        super(ctx);
-        _search = job;
-    }
-    public void runJob() {
-        _search.decrementRemaining();
-        if (_search.getLookupsRemaining() <= 0)
-            _search.failed(); 
-    }
-    public String getName() { return "NetDb search (phase 1) timeout"; }
-}
-
-class FloodLookupMatchJob extends JobImpl implements ReplyJob {
-    private Log _log;
-    private FloodSearchJob _search;
-    public FloodLookupMatchJob(RouterContext ctx, FloodSearchJob job) {
-        super(ctx);
-        _log = ctx.logManager().getLog(FloodLookupMatchJob.class);
-        _search = job;
-    }
-    public void runJob() { 
-        if ( (getContext().netDb().lookupLeaseSetLocally(_search.getKey()) != null) ||
-             (getContext().netDb().lookupRouterInfoLocally(_search.getKey()) != null) ) {
-            _search.success();
-        } else {
-            int remaining = _search.getLookupsRemaining();
-            if (_log.shouldLog(Log.INFO))
-                _log.info(getJobId() + "/" + _search.getJobId() + ": got a reply looking for " 
-                          + _search.getKey().toBase64() + ", with " + remaining + " outstanding searches");
-            // netDb reply pointing us at other people
-            if (remaining <= 0)
-                _search.failed();
-        }
-    }
-    public String getName() { return "NetDb search (phase 1) match"; }
-    public void setMessage(I2NPMessage message) {}
-}
-
-class FloodLookupSelector implements MessageSelector {
-    private RouterContext _context;
-    private FloodSearchJob _search;
-    public FloodLookupSelector(RouterContext ctx, FloodSearchJob search) {
-        _context = ctx;
-        _search = search;
-    }
-    public boolean continueMatching() { return _search.getLookupsRemaining() > 0; }
-    public long getExpiration() { return _search.getExpiration(); }
-    public boolean isMatch(I2NPMessage message) {
-        if (message == null) return false;
-        if (message instanceof DatabaseStoreMessage) {
-            DatabaseStoreMessage dsm = (DatabaseStoreMessage)message;
-            // is it worth making sure the reply came in on the right tunnel?
-            if (_search.getKey().equals(dsm.getKey())) {
-                _search.decrementRemaining();
-                return true;
-            }
-        } else if (message instanceof DatabaseSearchReplyMessage) {
-            DatabaseSearchReplyMessage dsrm = (DatabaseSearchReplyMessage)message;
-            if (_search.getKey().equals(dsrm.getSearchKey())) {
-                _search.decrementRemaining();
-                return true;
-            }
-        }
-        return false;
-    }   
-}
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 6ff84f68ce09cdfcea58fb58c6ebc62021ff783e..f6a1a31ef14cb67b421ab201bcec6bca47b51116 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
@@ -1003,7 +1003,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
         StringBuilder buf = new StringBuilder(size);
         out.write("<h2>Network Database Contents (<a href=\"netdb.jsp?l=1\">View LeaseSets</a>)</h2>\n");
         if (!_initialized) {
-            buf.append("<i>Not initialized</i>\n");
+            buf.append("Not initialized\n");
             out.write(buf.toString());
             out.flush();
             return;
@@ -1052,8 +1052,8 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
             buf.append("<tr><th>Version</th><th>Count</th></tr>\n");
             for (String routerVersion : versionList) {
                 int num = versions.count(routerVersion);
-                buf.append("<tr><td>").append(DataHelper.stripHTML(routerVersion));
-                buf.append("</td><td align=\"right\">").append(num).append("</td></tr>\n");
+                buf.append("<tr><td align=\"center\">").append(DataHelper.stripHTML(routerVersion));
+                buf.append("</td><td align=\"center\">").append(num).append("</td></tr>\n");
             }
             buf.append("</table>\n");
         }
@@ -1071,7 +1071,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
                 buf.append("<tr><td><img height=\"11\" width=\"16\" alt=\"").append(country.toUpperCase()).append("\"");
                 buf.append(" src=\"/flags.jsp?c=").append(country).append("\"> ");
                 buf.append(_context.commSystem().getCountryName(country));
-                buf.append("</td><td align=\"right\">").append(num).append("</td></tr>\n");
+                buf.append("</td><td align=\"center\">").append(num).append("</td></tr>\n");
             }
             buf.append("</table>\n");
         }
@@ -1086,21 +1086,26 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
      */
     private void renderRouterInfo(StringBuilder buf, RouterInfo info, boolean isUs, boolean full) {
         String hash = info.getIdentity().getHash().toBase64();
-        buf.append("<a name=\"").append(hash.substring(0, 6)).append("\" ></a>");
+        buf.append("<table><tr><th><a name=\"").append(hash.substring(0, 6)).append("\" ></a>");
         if (isUs) {
-            buf.append("<a name=\"our-info\" ></a><b>Our info: ").append(hash).append("</b><br>\n");
+            buf.append("<a name=\"our-info\" ></a><b>Our info: ").append(hash).append("</b></th></tr><tr><td>\n");
         } else {
-            buf.append("<b>Peer info for:</b> ").append(hash).append("<br>\n");
+            buf.append("<b>Peer info for:</b> ").append(hash).append("\n");
+            if (full) {
+                buf.append("[<a href=\"netdb.jsp\" >Back</a>]</th></tr><td>\n");
+            } else {
+                buf.append("[<a href=\"netdb.jsp?r=").append(hash.substring(0, 6)).append("\" >Full entry</a>]</th></tr><td>\n");
+            }
         }
         
         long age = _context.clock().now() - info.getPublished();
         if (isUs && _context.router().isHidden())
-            buf.append("Hidden, Updated: <i>").append(DataHelper.formatDuration(age)).append(" ago</i><br>\n");
+            buf.append("<b>Hidden, Updated:</b> ").append(DataHelper.formatDuration(age)).append(" ago<br>\n");
         else if (age > 0)
-            buf.append("Published: <i>").append(DataHelper.formatDuration(age)).append(" ago</i><br>\n");
+            buf.append("<b>Published:</b> ").append(DataHelper.formatDuration(age)).append(" ago<br>\n");
         else
-            buf.append("Published: <i>in ").append(DataHelper.formatDuration(0-age)).append("???</i><br>\n");
-        buf.append("Address(es): <i>");
+            buf.append("<b>Published:</b> in ").append(DataHelper.formatDuration(0-age)).append("???<br>\n");
+        buf.append("<b>Address(es):</b> ");
         String country = _context.commSystem().getCountry(info.getIdentity().getHash());
         if(country != null) {
             buf.append("<img height=\"11\" width=\"16\" alt=\"").append(country.toUpperCase()).append("\"");
@@ -1115,19 +1120,18 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
                 buf.append('[').append(DataHelper.stripHTML(name)).append('=').append(DataHelper.stripHTML(val)).append("] ");
             }
         }
-        buf.append("</i><br>\n");
+        buf.append("</td></tr>\n");
         if (full) {
-            buf.append("Stats: <br><i><code>\n");
+            buf.append("<tr><td>Stats: <br><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");
+            buf.append("</code></td></tr>\n");
         } else {
-            buf.append("<a href=\"netdb.jsp?r=").append(hash.substring(0, 6)).append("\" >Full entry</a>\n");
         }
-        buf.append("<hr>\n");
+        buf.append("</td></tr>\n");
     }
-    
+
 }
diff --git a/router/java/src/net/i2p/router/tunnel/pool/TestJob.java b/router/java/src/net/i2p/router/tunnel/pool/TestJob.java
index fc79c87f2589c00f0b0c4aaa4310f680d9892357..ba3eda6d666a635707f491a0ee19feb5a781659b 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/TestJob.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/TestJob.java
@@ -3,6 +3,7 @@ package net.i2p.router.tunnel.pool;
 import java.util.HashSet;
 import java.util.Set;
 
+import net.i2p.crypto.SessionKeyManager;
 import net.i2p.data.Certificate;
 import net.i2p.data.SessionKey;
 import net.i2p.data.SessionTag;
@@ -28,6 +29,8 @@ class TestJob extends JobImpl {
     private TunnelInfo _outTunnel;
     private TunnelInfo _replyTunnel;
     private PooledTunnelCreatorConfig _otherTunnel;
+    /** save this so we can tell the SKM to kill it if the test fails */
+    private SessionTag _encryptTag;
     
     /** base to randomize the test delay on */
     private static final int TEST_DELAY = 30*1000;
@@ -129,6 +132,7 @@ class TestJob extends JobImpl {
 
         SessionKey encryptKey = getContext().keyGenerator().generateSessionKey();
         SessionTag encryptTag = new SessionTag(true);
+        _encryptTag = encryptTag;
         SessionKey sentKey = new SessionKey();
         Set sentTags = null;
         GarlicMessage msg = GarlicMessageBuilder.buildMessage(getContext(), payload, sentKey, sentTags, 
@@ -142,7 +146,14 @@ class TestJob extends JobImpl {
         }
         Set encryptTags = new HashSet(1);
         encryptTags.add(encryptTag);
-        getContext().sessionKeyManager().tagsReceived(encryptKey, encryptTags);
+        // Register the single tag with the appropriate SKM
+        if (_cfg.isInbound() && !_pool.getSettings().isExploratory()) {
+            SessionKeyManager skm = getContext().clientManager().getClientSessionKeyManager(_pool.getSettings().getDestination());
+            if (skm != null)
+                skm.tagsReceived(encryptKey, encryptTags);
+        } else {
+            getContext().sessionKeyManager().tagsReceived(encryptKey, encryptTags);
+        }
 
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("Sending garlic test of " + _outTunnel + " / " + _replyTunnel);
@@ -307,8 +318,17 @@ class TestJob extends JobImpl {
         public void runJob() {
             if (_log.shouldLog(Log.WARN))
                 _log.warn("Timeout: found? " + _found, getAddedBy());
-            if (!_found)
+            if (!_found) {
+                // don't clog up the SKM with old one-tag tagsets
+                if (_cfg.isInbound() && !_pool.getSettings().isExploratory()) {
+                    SessionKeyManager skm = getContext().clientManager().getClientSessionKeyManager(_pool.getSettings().getDestination());
+                    if (skm != null)
+                        skm.consumeTag(_encryptTag);
+                } else {
+                    getContext().sessionKeyManager().consumeTag(_encryptTag);
+                }
                 testFailed(getContext().clock().now() - _started);
+            }
         }
         
         @Override
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 a83b78aaaa6229daf017bc6f75addb997d9b6119..f7c752c8c981ed1d61f2271888d4d8a2120cc81c 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java
@@ -309,7 +309,8 @@ public class TunnelPoolManager implements TunnelManagerFacade {
     void buildComplete(PooledTunnelCreatorConfig cfg) {
         //buildComplete();
         if (cfg.getLength() > 1 &&
-            !_context.router().gracefulShutdownInProgress()) {
+            (!_context.router().gracefulShutdownInProgress()) &&
+            !Boolean.valueOf(_context.getProperty("router.disableTunnelTesting")).booleanValue()) {
             TunnelPool pool = cfg.getTunnelPool();
             if (pool == null) {
                 // never seen this before, do we reallly need to bother
diff --git a/router/java/src/net/i2p/router/message/BuildTestMessageJob.java b/router/java/test/net/i2p/router/message/BuildTestMessageJob.java
similarity index 100%
rename from router/java/src/net/i2p/router/message/BuildTestMessageJob.java
rename to router/java/test/net/i2p/router/message/BuildTestMessageJob.java
diff --git a/router/java/src/net/i2p/router/message/SendGarlicJob.java b/router/java/test/net/i2p/router/message/SendGarlicJob.java
similarity index 100%
rename from router/java/src/net/i2p/router/message/SendGarlicJob.java
rename to router/java/test/net/i2p/router/message/SendGarlicJob.java