From e394d3d4c5852910bd97748f478d6720018b1edc Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Sun, 26 May 2013 17:25:02 +0000
Subject: [PATCH] * DatabaseLookupmessage:   - Add support for requesting an
 encrypted reply * NetDB:   - Add support for encrypted
 DatabaseSearchReplyMessage and DatabaseStoreMessage     in response to a
 DatabaseLookupMessage * PRNG: Cleanups using Collections.singletonMap() *
 Router utils: New RemovableSingletonSet * TransientSessionKeyManager:   -
 Support variable expiration for inbound tag sets   - Several efficiency
 improvements * VersionComparator: Add static method, use most places

---
 .../router/update/ConsoleUpdateManager.java   |   3 +-
 .../i2p/router/update/PluginUpdateRunner.java |  14 +-
 .../net/i2p/router/update/UpdateRunner.java   |   2 +-
 .../i2p/router/web/ConfigServiceHandler.java  |   2 +-
 .../src/net/i2p/router/web/LogsHelper.java    |   2 +-
 .../src/net/i2p/router/web/PluginStarter.java |  10 +-
 .../crypto/prng/AsyncFortunaStandalone.java   |   5 +-
 .../gnu/crypto/prng/FortunaStandalone.java    |   5 +-
 .../src/net/i2p/crypto/SessionKeyManager.java |  14 +-
 .../crypto/TransientSessionKeyManager.java    | 132 ++++++++++--------
 .../src/net/i2p/crypto/TrustedUpdate.java     |   2 +-
 core/java/src/net/i2p/util/SystemVersion.java |   2 +-
 .../src/net/i2p/util/VersionComparator.java   |   8 +-
 .../i2p/data/i2np/DatabaseLookupMessage.java  |  87 +++++++++++-
 .../HandleDatabaseLookupMessageJob.java       |  26 +++-
 .../kademlia/FloodfillVerifyStoreJob.java     |   4 +
 .../kademlia/IterativeSearchJob.java          |   6 +
 .../networkdb/kademlia/MessageWrapper.java    |  63 ++++++++-
 .../router/networkdb/kademlia/StoreJob.java   |   2 +-
 .../router/tunnel/pool/BuildRequestor.java    |   3 +-
 .../net/i2p/router/tunnel/pool/TestJob.java   |   6 +-
 .../tunnel/pool/TunnelPeerSelector.java       |   3 +-
 .../i2p/router/tunnel/pool/TunnelPool.java    |   3 +-
 .../router/util/RemovableSingletonSet.java    |  78 +++++++++++
 24 files changed, 372 insertions(+), 110 deletions(-)
 create mode 100644 router/java/src/net/i2p/router/util/RemovableSingletonSet.java

diff --git a/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java b/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java
index 1b295c68ea..fbf3525fa8 100644
--- a/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java
+++ b/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java
@@ -70,7 +70,6 @@ public class ConsoleUpdateManager implements UpdateManager {
     /** downloaded AND installed */
     private final Map<UpdateItem, Version> _installed;
     private static final DecimalFormat _pct = new DecimalFormat("0.0%");
-    private static final VersionComparator _versionComparator = new VersionComparator();
 
     private volatile String _status;
 
@@ -1289,7 +1288,7 @@ public class ConsoleUpdateManager implements UpdateManager {
         }
 
         public int compareTo(Version r) {
-            return _versionComparator.compare(version, r.version);
+            return VersionComparator.comp(version, r.version);
         }
 
         @Override
diff --git a/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java
index 4c8c394818..1f3f7fde41 100644
--- a/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java
+++ b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java
@@ -252,7 +252,7 @@ class PluginUpdateRunner extends UpdateRunner {
 
             String minVersion = ConfigClientsHelper.stripHTML(props, "min-i2p-version");
             if (minVersion != null &&
-                (new VersionComparator()).compare(CoreVersion.VERSION, minVersion) < 0) {
+                VersionComparator.comp(CoreVersion.VERSION, minVersion) < 0) {
                 to.delete();
                 statusDone("<b>" + _("This plugin requires I2P version {0} or higher", minVersion) + "</b>");
                 return;
@@ -260,7 +260,7 @@ class PluginUpdateRunner extends UpdateRunner {
 
             minVersion = ConfigClientsHelper.stripHTML(props, "min-java-version");
             if (minVersion != null &&
-                (new VersionComparator()).compare(System.getProperty("java.version"), minVersion) < 0) {
+                VersionComparator.comp(System.getProperty("java.version"), minVersion) < 0) {
                 to.delete();
                 statusDone("<b>" + _("This plugin requires Java version {0} or higher", minVersion) + "</b>");
                 return;
@@ -295,21 +295,21 @@ class PluginUpdateRunner extends UpdateRunner {
                 }
                 String oldVersion = oldProps.getProperty("version");
                 if (oldVersion == null ||
-                    (new VersionComparator()).compare(oldVersion, version) >= 0) {
+                    VersionComparator.comp(oldVersion, version) >= 0) {
                     to.delete();
                     statusDone("<b>" + _("Downloaded plugin version {0} is not newer than installed plugin", version) + "</b>");
                     return;
                 }
                 minVersion = ConfigClientsHelper.stripHTML(props, "min-installed-version");
                 if (minVersion != null &&
-                    (new VersionComparator()).compare(minVersion, oldVersion) > 0) {
+                    VersionComparator.comp(minVersion, oldVersion) > 0) {
                     to.delete();
                     statusDone("<b>" + _("Plugin update requires installed plugin version {0} or higher", minVersion) + "</b>");
                     return;
                 }
                 String maxVersion = ConfigClientsHelper.stripHTML(props, "max-installed-version");
                 if (maxVersion != null &&
-                    (new VersionComparator()).compare(maxVersion, oldVersion) < 0) {
+                    VersionComparator.comp(maxVersion, oldVersion) < 0) {
                     to.delete();
                     statusDone("<b>" + _("Plugin update requires installed plugin version {0} or lower", maxVersion) + "</b>");
                     return;
@@ -317,14 +317,14 @@ class PluginUpdateRunner extends UpdateRunner {
                 oldVersion = LogsHelper.jettyVersion();
                 minVersion = ConfigClientsHelper.stripHTML(props, "min-jetty-version");
                 if (minVersion != null &&
-                    (new VersionComparator()).compare(minVersion, oldVersion) > 0) {
+                    VersionComparator.comp(minVersion, oldVersion) > 0) {
                     to.delete();
                     statusDone("<b>" + _("Plugin requires Jetty version {0} or higher", minVersion) + "</b>");
                     return;
                 }
                 maxVersion = ConfigClientsHelper.stripHTML(props, "max-jetty-version");
                 if (maxVersion != null &&
-                    (new VersionComparator()).compare(maxVersion, oldVersion) < 0) {
+                    VersionComparator.comp(maxVersion, oldVersion) < 0) {
                     to.delete();
                     statusDone("<b>" + _("Plugin requires Jetty version {0} or lower", maxVersion) + "</b>");
                     return;
diff --git a/apps/routerconsole/java/src/net/i2p/router/update/UpdateRunner.java b/apps/routerconsole/java/src/net/i2p/router/update/UpdateRunner.java
index bb952bc50b..937f235fed 100644
--- a/apps/routerconsole/java/src/net/i2p/router/update/UpdateRunner.java
+++ b/apps/routerconsole/java/src/net/i2p/router/update/UpdateRunner.java
@@ -184,7 +184,7 @@ class UpdateRunner extends I2PAppThread implements UpdateTask, EepGet.StatusList
         if (_isPartial) {
             // Compare version with what we have now
             String newVersion = TrustedUpdate.getVersionString(new ByteArrayInputStream(_baos.toByteArray()));
-            boolean newer = (new VersionComparator()).compare(newVersion, RouterVersion.VERSION) > 0;
+            boolean newer = VersionComparator.comp(newVersion, RouterVersion.VERSION) > 0;
             if (newer) {
                 _newVersion = newVersion;
             } else {
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java
index 5a1b6c807b..54bc9bc3c4 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java
@@ -144,7 +144,7 @@ public class ConfigServiceHandler extends FormHandler {
         if (ctx.hasWrapper() && _wrapperListener == null &&
             !SystemVersion.isWindows()) {
             String wv = System.getProperty("wrapper.version");
-            if (wv != null && (new VersionComparator()).compare(wv, LISTENER_AVAILABLE) >= 0) {
+            if (wv != null && VersionComparator.comp(wv, LISTENER_AVAILABLE) >= 0) {
                 try {
                    _wrapperListener = new WrapperListener(ctx);
                 } catch (Throwable t) {}
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java
index 6e92ff9d84..651ace5728 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java
@@ -48,7 +48,7 @@ public class LogsHelper extends HelperBase {
         File f = null;
         if (ctx.hasWrapper()) {
             String wv = System.getProperty("wrapper.version");
-            if (wv != null && (new VersionComparator()).compare(wv, LOCATION_AVAILABLE) >= 0) {
+            if (wv != null && VersionComparator.comp(wv, LOCATION_AVAILABLE) >= 0) {
                 try {
                    f = WrapperManager.getWrapperLogFile();
                 } catch (Throwable t) {}
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java
index 08b198dd21..b7018d37fa 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java
@@ -73,7 +73,7 @@ public class PluginStarter implements Runnable {
             !NewsHelper.isUpdateInProgress()) {
             String prev = _context.getProperty("router.previousVersion");
             if (prev != null &&
-                (new VersionComparator()).compare(RouterVersion.VERSION, prev) > 0) {
+                VersionComparator.comp(RouterVersion.VERSION, prev) > 0) {
                 updateAll(_context, true);
             }
         }
@@ -236,7 +236,7 @@ public class PluginStarter implements Runnable {
 
         String minVersion = ConfigClientsHelper.stripHTML(props, "min-i2p-version");
         if (minVersion != null &&
-            (new VersionComparator()).compare(CoreVersion.VERSION, minVersion) < 0) {
+            VersionComparator.comp(CoreVersion.VERSION, minVersion) < 0) {
             String foo = "Plugin " + appName + " requires I2P version " + minVersion + " or higher";
             log.error(foo);
             disablePlugin(appName);
@@ -245,7 +245,7 @@ public class PluginStarter implements Runnable {
 
         minVersion = ConfigClientsHelper.stripHTML(props, "min-java-version");
         if (minVersion != null &&
-            (new VersionComparator()).compare(System.getProperty("java.version"), minVersion) < 0) {
+            VersionComparator.comp(System.getProperty("java.version"), minVersion) < 0) {
             String foo = "Plugin " + appName + " requires Java version " + minVersion + " or higher";
             log.error(foo);
             disablePlugin(appName);
@@ -255,7 +255,7 @@ public class PluginStarter implements Runnable {
         String jVersion = LogsHelper.jettyVersion();
         minVersion = ConfigClientsHelper.stripHTML(props, "min-jetty-version");
         if (minVersion != null &&
-            (new VersionComparator()).compare(minVersion, jVersion) > 0) {
+            VersionComparator.comp(minVersion, jVersion) > 0) {
             String foo = "Plugin " + appName + " requires Jetty version " + minVersion + " or higher";
             log.error(foo);
             disablePlugin(appName);
@@ -264,7 +264,7 @@ public class PluginStarter implements Runnable {
 
         String maxVersion = ConfigClientsHelper.stripHTML(props, "max-jetty-version");
         if (maxVersion != null &&
-            (new VersionComparator()).compare(maxVersion, jVersion) < 0) {
+            VersionComparator.comp(maxVersion, jVersion) < 0) {
             String foo = "Plugin " + appName + " requires Jetty version " + maxVersion + " or lower";
             log.error(foo);
             disablePlugin(appName);
diff --git a/core/java/src/gnu/crypto/prng/AsyncFortunaStandalone.java b/core/java/src/gnu/crypto/prng/AsyncFortunaStandalone.java
index 1a2ec48921..ba68e3d148 100644
--- a/core/java/src/gnu/crypto/prng/AsyncFortunaStandalone.java
+++ b/core/java/src/gnu/crypto/prng/AsyncFortunaStandalone.java
@@ -1,6 +1,6 @@
 package gnu.crypto.prng;
 
-import java.util.HashMap;
+import java.util.Collections;
 import java.util.Map;
 import java.util.concurrent.LinkedBlockingQueue;
 
@@ -77,8 +77,7 @@ public class AsyncFortunaStandalone extends FortunaStandalone implements Runnabl
     /** the seed is only propogated once the prng is started with startup() */
     @Override
     public void seed(byte val[]) {
-        Map props = new HashMap(1);
-        props.put(SEED, val);
+        Map props = Collections.singletonMap(SEED, val);
         init(props);
         //fillBlock();
     }
diff --git a/core/java/src/gnu/crypto/prng/FortunaStandalone.java b/core/java/src/gnu/crypto/prng/FortunaStandalone.java
index 927c6ef8d3..8128794daf 100644
--- a/core/java/src/gnu/crypto/prng/FortunaStandalone.java
+++ b/core/java/src/gnu/crypto/prng/FortunaStandalone.java
@@ -48,7 +48,7 @@ import java.io.Serializable;
 import java.security.InvalidKeyException;
 import java.security.MessageDigest;
 import java.util.Arrays;
-import java.util.HashMap;
+import java.util.Collections;
 import java.util.Map;
 
 import net.i2p.crypto.CryptixAESKeyCache;
@@ -131,8 +131,7 @@ public class FortunaStandalone extends BasePRNGStandalone implements Serializabl
   }
 
   public void seed(byte val[]) {
-      Map props = new HashMap(1);
-      props.put(SEED, val);
+      Map props = Collections.singletonMap(SEED, val);
       init(props);
       fillBlock();
   }
diff --git a/core/java/src/net/i2p/crypto/SessionKeyManager.java b/core/java/src/net/i2p/crypto/SessionKeyManager.java
index b9a00b9988..53ad0c02c3 100644
--- a/core/java/src/net/i2p/crypto/SessionKeyManager.java
+++ b/core/java/src/net/i2p/crypto/SessionKeyManager.java
@@ -150,11 +150,19 @@ public class SessionKeyManager {
     }
 
     /**
-     * Accept the given tags and associate them with the given key for decryption
+     * Accept the given tags and associate them with the given key for decryption,
+     * with the default expiration.
+     */
+    public void tagsReceived(SessionKey key, Set<SessionTag> sessionTags) {}
+
+    /**
+     * Accept the given tags and associate them with the given key for decryption,
+     * with specified expiration.
      *
+     * @param expire time from now
+     * @since 0.9.7
      */
-    public void tagsReceived(SessionKey key, Set<SessionTag> sessionTags) { // nop
-    }
+    public void tagsReceived(SessionKey key, Set<SessionTag> sessionTags, long expire) {}
 
     /**
      * Determine if we have received a session key associated with the given session tag,
diff --git a/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java b/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java
index 190004b574..dc9bdd67d4 100644
--- a/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java
+++ b/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java
@@ -89,9 +89,8 @@ public class TransientSessionKeyManager extends SessionKeyManager {
     private final int _lowThreshold;
 
     /** 
-     * Let session tags sit around for this long before expiring them.  We can now have such a large
-     * value since there is the persistent session key manager.  This value is for outbound tags - 
-     * inbound tags are managed by SESSION_LIFETIME_MAX_MS
+     * Let outbound session tags sit around for this long before expiring them.
+     * Inbound tag expiration is set by SESSION_LIFETIME_MAX_MS
      */
     private final static long SESSION_TAG_DURATION_MS = 12 * 60 * 1000;
 
@@ -99,6 +98,8 @@ public class TransientSessionKeyManager extends SessionKeyManager {
      * Keep unused inbound session tags around for this long (a few minutes longer than
      * session tags are used on the outbound side so that no reasonable network lag 
      * can cause failed decrypts)
+     *
+     * This is also the max idle time for an outbound session.
      */
     private final static long SESSION_LIFETIME_MAX_MS = SESSION_TAG_DURATION_MS + 3 * 60 * 1000;
 
@@ -499,11 +500,22 @@ public class TransientSessionKeyManager extends SessionKeyManager {
     /**
      * Accept the given tags and associate them with the given key for decryption
      *
+     * @param sessionTags modifiable; NOT copied
      */
     @Override
     public void tagsReceived(SessionKey key, Set<SessionTag> sessionTags) {
-        int overage = 0;
-        TagSet tagSet = new TagSet(sessionTags, key, _context.clock().now(), _rcvTagSetID.incrementAndGet());
+        tagsReceived(key, sessionTags, SESSION_LIFETIME_MAX_MS);
+    }
+
+    /**
+     * Accept the given tags and associate them with the given key for decryption
+     *
+     * @param sessionTags modifiable; NOT copied
+     */
+    @Override
+    public void tagsReceived(SessionKey key, Set<SessionTag> sessionTags, long expire) {
+        TagSet tagSet = new TagSet(sessionTags, key, _context.clock().now() + expire,
+                                   _rcvTagSetID.incrementAndGet());
         if (_log.shouldLog(Log.INFO))
             _log.info("Received " + tagSet);
         TagSet old = null;
@@ -513,7 +525,6 @@ public class TransientSessionKeyManager extends SessionKeyManager {
                 //if (_log.shouldLog(Log.DEBUG))
                 //    _log.debug("Receiving tag " + tag + " in tagSet: " + tagSet);
                 old = _inboundTagSets.put(tag, tagSet);
-                overage = _inboundTagSets.size() - MAX_INBOUND_SESSION_TAGS;
                 if (old != null) {
                     if (!old.getAssociatedKey().equals(tagSet.getAssociatedKey())) {
                         _inboundTagSets.remove(tag);
@@ -546,6 +557,7 @@ public class TransientSessionKeyManager extends SessionKeyManager {
             }
         }
         
+        int overage = _inboundTagSets.size() - MAX_INBOUND_SESSION_TAGS;
         if (overage > 0)
             clearExcess(overage);
 
@@ -573,25 +585,23 @@ public class TransientSessionKeyManager extends SessionKeyManager {
         _log.log(Log.CRIT, "TOO MANY SESSION TAGS! Starting cleanup, overage = " + overage);
         List<TagSet> removed = new ArrayList(toRemove);
         synchronized (_inboundTagSets) {
-            for (Iterator<TagSet> iter = _inboundTagSets.values().iterator(); iter.hasNext(); ) {
-                TagSet set = iter.next();
+            for (TagSet set : _inboundTagSets.values()) {
                 int size = set.getTags().size();
                 if (size > 1000)
                     absurd++;
                 if (size > 100)
                     large++;
-                if (now - set.getDate() > SESSION_LIFETIME_MAX_MS)
+                if (now >= set.getDate())
                     old++;
-                else if (now - set.getDate() < 1*60*1000)
+                else if (set.getDate() - now > 10*60*1000)
                     recent++;
 
-                if ((removed.size() < (toRemove)) || (now - set.getDate() > SESSION_LIFETIME_MAX_MS))
+                if ((removed.size() < (toRemove)) || (now >= set.getDate()))
                     removed.add(set);
             }
             for (int i = 0; i < removed.size(); i++) {
                 TagSet cur = removed.get(i);
-                for (Iterator<SessionTag> iter = cur.getTags().iterator(); iter.hasNext(); ) {
-                    SessionTag tag = iter.next();
+                for (SessionTag tag : cur.getTags()) {
                     _inboundTagSets.remove(tag);
                     tags++;
                 }
@@ -616,21 +626,21 @@ public class TransientSessionKeyManager extends SessionKeyManager {
      */
     @Override
     public SessionKey consumeTag(SessionTag tag) {
-        //if (false) aggressiveExpire();
+        TagSet tagSet;
         synchronized (_inboundTagSets) {
-            TagSet tagSet = _inboundTagSets.remove(tag);
+            tagSet = _inboundTagSets.remove(tag);
             if (tagSet == null) {
                 if (_log.shouldLog(Log.DEBUG))
                     _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("IB Tag consumed: " + tag + " from: " + tagSet);
-            return key;
         }
+
+        SessionKey key = tagSet.getAssociatedKey();
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("IB Tag consumed: " + tag + " from: " + tagSet);
+        return key;
     }
 
     private OutboundSession getSession(PublicKey target) {
@@ -660,44 +670,28 @@ public class TransientSessionKeyManager extends SessionKeyManager {
     /**
      * Aggressively expire inbound tag sets and outbound sessions
      *
-     * @return number of tag sets expired
+     * @return number of tag sets expired (bogus as it overcounts inbound)
      */
     private int aggressiveExpire() {
         int removed = 0;
         int remaining = 0;
         long now = _context.clock().now();
-        StringBuilder buf = null;
-        //StringBuilder bufSummary = null;
-        if (_log.shouldLog(Log.DEBUG)) {
-            buf = new StringBuilder(128);
-            buf.append("Expiring inbound: ");
-            //bufSummary = new StringBuilder(1024);
-        }
+
         synchronized (_inboundTagSets) {
-            for (Iterator<SessionTag> iter = _inboundTagSets.keySet().iterator(); iter.hasNext();) {
-                SessionTag tag = iter.next();
-                TagSet ts = _inboundTagSets.get(tag);
-                long age = now - ts.getDate();
-                if (age > SESSION_LIFETIME_MAX_MS) {
-                //if (ts.getDate() < now - SESSION_LIFETIME_MAX_MS) {
+            for (Iterator<TagSet> iter = _inboundTagSets.values().iterator(); iter.hasNext();) {
+                TagSet ts = iter.next();
+                // for inbound tagsets, getDate() is the expire time
+                if (ts.getDate() <= now) {
                     iter.remove();
+                    // bug, this counts inbound tags, not tag sets
                     removed++;
-                    if (buf != null)
-                        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();
         }
         _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());
-
-        //_log.warn("Expiring tags: [" + tagsToDrop + "]");
+        if (removed > 0 && _log.shouldLog(Log.DEBUG))
+            _log.debug("Expired inbound: " + removed);
 
         synchronized (_outboundSessions) {
             for (Iterator<OutboundSession> iter = _outboundSessions.values().iterator(); iter.hasNext();) {
@@ -722,10 +716,12 @@ public class TransientSessionKeyManager extends SessionKeyManager {
         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());
+        for (TagSet ts : inbound) {
             Set<TagSet> sets = inboundSets.get(ts.getAssociatedKey());
+            if (sets == null) {
+                sets = new HashSet();
+                inboundSets.put(ts.getAssociatedKey(), sets);
+            }
             sets.add(ts);
         }
         int total = 0;
@@ -741,8 +737,12 @@ public class TransientSessionKeyManager extends SessionKeyManager {
                 TagSet ts = siter.next();
                 int size = ts.getTags().size();
                 total += size;
-                buf.append("<li><b>ID: ").append(ts.getID())
-                   .append(" Received:</b> ").append(DataHelper.formatDuration2(now - ts.getDate())).append(" ago with ");
+                buf.append("<li><b>ID: ").append(ts.getID());
+                long expires = ts.getDate() - now;
+                if (expires > 0)
+                    buf.append(" Expires in:</b> ").append(DataHelper.formatDuration2(expires)).append(" with ");
+                else
+                    buf.append(" Expired:</b> ").append(DataHelper.formatDuration2(0 - expires)).append(" ago with ");
                 buf.append(size).append('/').append(ts.getOriginalSize()).append(" tags remaining</li>");
             }
             buf.append("</ul></td></tr>\n");
@@ -802,7 +802,10 @@ public class TransientSessionKeyManager extends SessionKeyManager {
      */
     private static class TagSetComparator implements Comparator<TagSet> {
          public int compare(TagSet l, TagSet r) {
-             return (int) (l.getDate() - r.getDate());
+             int rv = (int) (l.getDate() - r.getDate());
+             if (rv != 0)
+                 return rv;
+             return l.hashCode() - r.hashCode();
         }
     }
 
@@ -1088,6 +1091,9 @@ public class TransientSessionKeyManager extends SessionKeyManager {
         /** did we get an ack for this tagset? Only for outbound tagsets */
         private boolean _acked;
 
+        /**
+         *  @param date For inbound: when the TagSet will expire; for outbound: creation time
+         */
         public TagSet(Set<SessionTag> tags, SessionKey key, long date, int id) {
             if (key == null) throw new IllegalArgumentException("Missing key");
             if (tags == null) throw new IllegalArgumentException("Missing tags");
@@ -1104,7 +1110,9 @@ public class TransientSessionKeyManager extends SessionKeyManager {
             //}
         }
 
-        /** when the tag set was created */
+        /**
+         *  For inbound: when the TagSet will expire; for outbound: creation time
+         */
         public long getDate() {
             return _date;
         }
@@ -1135,23 +1143,29 @@ public class TransientSessionKeyManager extends SessionKeyManager {
         }
 
         /**
-         *  Let's do this without counting the elements first.
+         *  For outbound only.
          *  Caller must synch.
+         *  @return a tag or null
          */
         public SessionTag consumeNext() {
-            SessionTag first;
-            try {
-                first = _sessionTags.iterator().next();
-            } catch (NoSuchElementException nsee) {
+            Iterator<SessionTag> iter = _sessionTags.iterator();
+            if (!iter.hasNext())
                 return null;
-            }
-            _sessionTags.remove(first);
+            SessionTag first = iter.next();
+            iter.remove();
             return first;
         }
         
         //public Exception getCreatedBy() { return _createdBy; }
 
+        /**
+         *  For outbound only.
+         */
         public void setAcked() { _acked = true; }
+
+        /**
+         *  For outbound only.
+         */
         public boolean getAcked() { return _acked; }
         
 /******    this will return a dup if two in the same ms, so just use java
diff --git a/core/java/src/net/i2p/crypto/TrustedUpdate.java b/core/java/src/net/i2p/crypto/TrustedUpdate.java
index 90b6b301ca..8ad2d14ed2 100644
--- a/core/java/src/net/i2p/crypto/TrustedUpdate.java
+++ b/core/java/src/net/i2p/crypto/TrustedUpdate.java
@@ -302,7 +302,7 @@ riCe6OlAEiNpcc6mMyIYYWFICbrDFTrDR3wXqwc/Jkcx6L5VVWoagpSzbo3yGhc=
      *         version, otherwise <code>false</code>.
      */
     public static final boolean needsUpdate(String currentVersion, String newVersion) {
-        return (new VersionComparator()).compare(currentVersion, newVersion) < 0;
+        return VersionComparator.comp(currentVersion, newVersion) < 0;
     }
 
     /** @return success */
diff --git a/core/java/src/net/i2p/util/SystemVersion.java b/core/java/src/net/i2p/util/SystemVersion.java
index 57ee79d75b..4761731a85 100644
--- a/core/java/src/net/i2p/util/SystemVersion.java
+++ b/core/java/src/net/i2p/util/SystemVersion.java
@@ -56,7 +56,7 @@ public abstract class SystemVersion {
         if (_isAndroid) {
             _oneDotSix = _androidSDK >= 9;
         } else {
-            _oneDotSix = (new VersionComparator()).compare(System.getProperty("java.version"), "1.6") >= 0;
+            _oneDotSix = VersionComparator.comp(System.getProperty("java.version"), "1.6") >= 0;
         }
     }
 
diff --git a/core/java/src/net/i2p/util/VersionComparator.java b/core/java/src/net/i2p/util/VersionComparator.java
index f65cf33d95..6ae87247ba 100644
--- a/core/java/src/net/i2p/util/VersionComparator.java
+++ b/core/java/src/net/i2p/util/VersionComparator.java
@@ -11,9 +11,15 @@ import java.util.Comparator;
  */
 public class VersionComparator implements Comparator<String> {
 
-    @Override
     public int compare(String l, String r) {
+        return comp(l, r);
+    }
         
+    /**
+     *  To avoid churning comparators
+     *  @since 0.9.7
+     */
+    public static int comp(String l, String r) {
         if (l.equals(r))
             return 0;
         
diff --git a/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java b/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java
index d889fce74d..d8ac613633 100644
--- a/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java
+++ b/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java
@@ -17,8 +17,12 @@ import java.util.Set;
 import net.i2p.I2PAppContext;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
+import net.i2p.data.RouterInfo;
+import net.i2p.data.SessionKey;
+import net.i2p.data.SessionTag;
 import net.i2p.data.TunnelId;
 //import net.i2p.util.Log;
+import net.i2p.util.VersionComparator;
 
 /**
  * Defines the message a router sends to another router to search for a
@@ -34,6 +38,8 @@ public class DatabaseLookupMessage extends FastI2NPMessageImpl {
     private TunnelId _replyTunnel;
     /** this must be kept as a list to preserve the order and not break the checksum */
     private List<Hash> _dontIncludePeers;
+    private SessionKey _replyKey;
+    private SessionTag _replyTag;
     
     //private static volatile long _currentLookupPeriod = 0;
     //private static volatile int _currentLookupCount = 0;
@@ -45,6 +51,11 @@ public class DatabaseLookupMessage extends FastI2NPMessageImpl {
         Have to prevent a huge alloc on rcv of a malicious msg though */
     private static final int MAX_NUM_PEERS = 512;
     
+    private static final byte FLAG_TUNNEL = 0x01;
+    private static final byte FLAG_ENCRYPT = 0x02;
+
+    private static final String MIN_ENCRYPTION_VERSION = "0.9.8";
+
     public DatabaseLookupMessage(I2PAppContext context) {
         this(context, false);
     }
@@ -144,6 +155,49 @@ public class DatabaseLookupMessage extends FastI2NPMessageImpl {
         _replyTunnel = replyTunnel;
     }
     
+    /**
+     *  Does this router support encrypted replies?
+     *
+     *  @param to null OK
+     *  @since 0.9.7
+     */
+    public static boolean supportsEncryptedReplies(RouterInfo to) {
+        if (to == null)
+            return false;
+        String v = to.getOption("router.version");
+        return v != null &&
+               VersionComparator.comp(v, MIN_ENCRYPTION_VERSION) >= 0;
+    }
+    
+    /**
+     *  The included session key or null if unset
+     *
+     *  @since 0.9.7
+     */
+    public SessionKey getReplyKey() { return _replyKey; }
+    
+    /**
+     *  The included session tag or null if unset
+     *
+     *  @since 0.9.7
+     */
+    public SessionTag getReplyTag() { return _replyTag; }
+
+    /**
+     *  Only worthwhile if sending reply via tunnel
+     *
+     *  @throws IllegalStateException if key or tag previously set, to protect saved checksum
+     *  @param encryptKey non-null
+     *  @param encryptTag non-null
+     *  @since 0.9.7
+     */
+    public void setReplySession(SessionKey encryptKey, SessionTag encryptTag) {
+        if (_replyKey != null || _replyTag != null)
+            throw new IllegalStateException();
+        _replyKey = encryptKey;
+        _replyTag = encryptTag;
+    }
+    
     /**
      * Set of peers that a lookup reply should NOT include.
      * WARNING - returns a copy.
@@ -224,7 +278,8 @@ public class DatabaseLookupMessage extends FastI2NPMessageImpl {
         
         // as of 0.9.6, ignore other 7 bits of the flag byte
         // TODO store the whole flag byte
-        boolean tunnelSpecified = (data[curIndex] & 0x01) != 0;
+        boolean tunnelSpecified = (data[curIndex] & FLAG_TUNNEL) != 0;
+        boolean replyKeySpecified = (data[curIndex] & FLAG_ENCRYPT) != 0;
         curIndex++;
         
         if (tunnelSpecified) {
@@ -246,6 +301,15 @@ public class DatabaseLookupMessage extends FastI2NPMessageImpl {
             peers.add(p);
         }
         _dontIncludePeers = peers;
+        if (replyKeySpecified) {
+            byte[] rk = new byte[SessionKey.KEYSIZE_BYTES];
+            System.arraycopy(data, curIndex, rk, 0, SessionKey.KEYSIZE_BYTES);
+            _replyKey = new SessionKey(rk);
+            curIndex += SessionKey.KEYSIZE_BYTES;
+            byte[] rt = new byte[SessionTag.BYTE_LENGTH];
+            System.arraycopy(data, curIndex, rt, 0, SessionTag.BYTE_LENGTH);
+            _replyTag = new SessionTag(rt);
+        }
     }
 
     
@@ -258,6 +322,8 @@ public class DatabaseLookupMessage extends FastI2NPMessageImpl {
         totalLength += 2; // numPeers
         if (_dontIncludePeers != null) 
             totalLength += Hash.HASH_LENGTH * _dontIncludePeers.size();
+        if (_replyKey != null)
+            totalLength += SessionKey.KEYSIZE_BYTES + SessionTag.BYTE_LENGTH;
         return totalLength;
     }
     
@@ -269,12 +335,17 @@ public class DatabaseLookupMessage extends FastI2NPMessageImpl {
         curIndex += Hash.HASH_LENGTH;
         System.arraycopy(_fromHash.getData(), 0, out, curIndex, Hash.HASH_LENGTH);
         curIndex += Hash.HASH_LENGTH;
-        // TODO allow specification of the other 7 bits of the flag byte
+        // Generate the flag byte
         if (_replyTunnel != null) {
-            out[curIndex++] = 0x01;
+            byte flag = FLAG_TUNNEL;
+            if (_replyKey != null)
+                flag |= FLAG_ENCRYPT;
+            out[curIndex++] = flag;
             byte id[] = DataHelper.toLong(4, _replyTunnel.getTunnelId());
             System.arraycopy(id, 0, out, curIndex, 4);
             curIndex += 4;
+        } else if (_replyKey != null) {
+            out[curIndex++] = FLAG_ENCRYPT;
         } else {
             out[curIndex++] = 0x00;
         }
@@ -293,6 +364,12 @@ public class DatabaseLookupMessage extends FastI2NPMessageImpl {
                 curIndex += Hash.HASH_LENGTH;
             }
         }
+        if (_replyKey != null) {
+            System.arraycopy(_replyKey.getData(), 0, out, curIndex, SessionKey.KEYSIZE_BYTES);
+            curIndex += SessionKey.KEYSIZE_BYTES;
+            System.arraycopy(_replyTag.getData(), 0, out, curIndex, SessionTag.BYTE_LENGTH);
+            curIndex += SessionTag.BYTE_LENGTH;
+        }
         return curIndex;
     }
     
@@ -326,6 +403,10 @@ public class DatabaseLookupMessage extends FastI2NPMessageImpl {
         buf.append("\n\tSearch Key: ").append(_key);
         buf.append("\n\tFrom: ").append(_fromHash);
         buf.append("\n\tReply Tunnel: ").append(_replyTunnel);
+        if (_replyKey != null)
+            buf.append("\n\tReply Key: ").append(_replyKey);
+        if (_replyTag != null)
+            buf.append("\n\tReply Tag: ").append(_replyTag);
         buf.append("\n\tDont Include Peers: ");
         if (_dontIncludePeers != null)
             buf.append(_dontIncludePeers.size());
diff --git a/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java b/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java
index 37d5f0fc36..22bdb88890 100644
--- a/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java
+++ b/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java
@@ -8,6 +8,7 @@ package net.i2p.router.networkdb;
  *
  */
 
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -16,6 +17,7 @@ import net.i2p.data.Hash;
 import net.i2p.data.LeaseSet;
 import net.i2p.data.RouterIdentity;
 import net.i2p.data.RouterInfo;
+import net.i2p.data.SessionKey;
 import net.i2p.data.TunnelId;
 import net.i2p.data.i2np.DatabaseLookupMessage;
 import net.i2p.data.i2np.DatabaseSearchReplyMessage;
@@ -27,6 +29,7 @@ import net.i2p.router.JobImpl;
 import net.i2p.router.OutNetMessage;
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
+import net.i2p.router.networkdb.kademlia.MessageWrapper;
 import net.i2p.router.message.SendMessageDirectJob;
 import net.i2p.util.Log;
 
@@ -36,8 +39,9 @@ import net.i2p.util.Log;
  * Unused directly - see kademlia/ for extension
  */
 public class HandleDatabaseLookupMessageJob extends JobImpl {
-    private Log _log;
-    private DatabaseLookupMessage _message;
+    private final Log _log;
+    private final DatabaseLookupMessage _message;
+
     private final static int MAX_ROUTERS_RETURNED = 3;
     private final static int CLOSENESS_THRESHOLD = 8; // FNDF.MAX_TO_FLOOD + 1
     private final static int REPLY_TIMEOUT = 60*1000;
@@ -149,8 +153,7 @@ public class HandleDatabaseLookupMessageJob extends JobImpl {
                 if ( (info.getIdentity().isHidden()) || (isUnreachable(info) && !publishUnreachable()) ) {
                     if (_log.shouldLog(Log.DEBUG))
                         _log.debug("Not answering a query for a netDb peer who isn't reachable");
-                    Set<Hash> us = new HashSet<Hash>(1);
-                    us.add(getContext().routerHash());
+                    Set<Hash> us = Collections.singleton(getContext().routerHash());
                     sendClosest(_message.getSearchKey(), us, fromKey, _message.getReplyTunnel());
                 //} else if (info.isHidden()) {
                 //    // Don't return hidden nodes
@@ -203,9 +206,11 @@ public class HandleDatabaseLookupMessageJob extends JobImpl {
      */
     private Set<Hash> getNearestRouters() {
         Set<Hash> dontInclude = _message.getDontIncludePeers();
+        Hash us = getContext().routerHash();
         if (dontInclude == null)
-            dontInclude = new HashSet(1);
-        dontInclude.add(getContext().routerHash());
+            dontInclude = Collections.singleton(us);
+        else
+            dontInclude.add(us);
         // Honor flag to exclude all floodfills
         //if (dontInclude.contains(Hash.FAKE_HASH)) {
         // This is handled in FloodfillPeerSelector
@@ -289,6 +294,15 @@ public class HandleDatabaseLookupMessageJob extends JobImpl {
             getContext().tunnelDispatcher().dispatch(m);
         } else {
             // if we aren't the gateway, forward it on
+            SessionKey replyKey = _message.getReplyKey();
+            if (replyKey != null) {
+                // encrypt the reply
+                message = MessageWrapper.wrap(getContext(), message, replyKey, _message.getReplyTag());
+                if (message == null) {
+                    _log.error("Encryption error");
+                    return;
+                }
+            }
             TunnelGatewayMessage m = new TunnelGatewayMessage(getContext());
             m.setMessage(message);
             m.setMessageExpiration(message.getMessageExpiration());
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java
index 14aeaac13d..79e1d13d01 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java
@@ -111,6 +111,10 @@ class FloodfillVerifyStoreJob extends JobImpl {
             _facade.verifyFinished(_key);
             return;
         }
+        if (DatabaseLookupMessage.supportsEncryptedReplies(peer)) {
+            MessageWrapper.OneTimeSession sess = MessageWrapper.generateSession(getContext());
+            lookup.setReplySession(sess.key, sess.tag);
+        }
         Hash fromKey;
         if (_isRouterInfo)
             fromKey = null;
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java
index 25d57f6f83..c2d7bfb5cb 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java
@@ -24,6 +24,7 @@ import net.i2p.router.RouterContext;
 import net.i2p.router.TunnelInfo;
 import net.i2p.router.util.RandomIterator;
 import net.i2p.util.Log;
+import net.i2p.util.VersionComparator;
 
 /**
  * A traditional Kademlia search that continues to search
@@ -233,6 +234,11 @@ class IterativeSearchJob extends FloodSearchJob {
                 // if we have the ff RI, garlic encrypt it
                 RouterInfo ri = getContext().netDb().lookupRouterInfoLocally(peer);
                 if (ri != null) {
+                    // request encrypted reply
+                    if (DatabaseLookupMessage.supportsEncryptedReplies(ri)) {
+                        MessageWrapper.OneTimeSession sess = MessageWrapper.generateSession(getContext());
+                        dlm.setReplySession(sess.key, sess.tag);
+                    }
                     outMsg = MessageWrapper.wrap(getContext(), dlm, ri);
                     if (_log.shouldLog(Log.DEBUG))
                         _log.debug(getJobId() + ": Encrypted DLM for " + _key + " to " + peer);
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java b/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java
index 4359b840de..44a1b61ad7 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java
@@ -17,14 +17,16 @@ import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.router.RouterContext;
 import net.i2p.router.message.GarlicMessageBuilder;
 import net.i2p.router.message.PayloadGarlicConfig;
+import net.i2p.router.util.RemovableSingletonSet;
 
 /**
  *  Method and class for garlic encrypting outbound netdb traffic,
- *  including management of the ElGamal/AES tags
+ *  and sending keys and tags for others to encrypt inbound netdb traffic,
+ *  including management of the ElGamal/AES tags.
  *
  *  @since 0.7.10
  */
-class MessageWrapper {
+public class MessageWrapper {
 
     //private static final Log _log = RouterContext.getGlobalContext().logManager().getLog(MessageWrapper.class);
 
@@ -142,4 +144,61 @@ class MessageWrapper {
                                                               key, sentKey, null);
         return msg;
     }
+
+    /**
+     *  A single key and tag, for receiving a single message.
+     *
+     *  @since 0.9.7
+     */
+    public static class OneTimeSession {
+        public final SessionKey key;
+        public final SessionTag tag;
+
+        public OneTimeSession(SessionKey key, SessionTag tag) {
+            this.key = key; this.tag = tag;
+        }
+    }
+
+    /**
+     *  Create a single key and tag, for receiving a single encrypted message,
+     *  and register it with the router's session key manager, to expire in two minutes.
+     *  The recipient can then send us an AES-encrypted message,
+     *  avoiding ElGamal.
+     *
+     *  @since 0.9.7
+     */
+    public static OneTimeSession generateSession(RouterContext ctx) {
+        SessionKey key = ctx.keyGenerator().generateSessionKey();
+        SessionTag tag = new SessionTag(true);
+        Set<SessionTag> tags = new RemovableSingletonSet(tag);
+        ctx.sessionKeyManager().tagsReceived(key, tags, 2*60*1000);
+        return new OneTimeSession(key, tag);
+    }
+
+    /**
+     *  Garlic wrap a message from nobody, destined for an unknown router,
+     *  to hide the contents from the IBGW.
+     *  Uses a supplied session key and session tag for AES encryption,
+     *  avoiding ElGamal.
+     *
+     *  @param encryptKey non-null
+     *  @param encryptTag non-null
+     *  @return null on encrypt failure
+     *  @since 0.9.7
+     */
+    public static GarlicMessage wrap(RouterContext ctx, I2NPMessage m, SessionKey encryptKey, SessionTag encryptTag) {
+        DeliveryInstructions instructions = new DeliveryInstructions();
+        instructions.setDeliveryMode(DeliveryInstructions.DELIVERY_MODE_LOCAL);
+
+        PayloadGarlicConfig payload = new PayloadGarlicConfig();
+        payload.setCertificate(Certificate.NULL_CERT);
+        payload.setId(ctx.random().nextLong(I2NPMessage.MAX_ID_VALUE));
+        payload.setPayload(m);
+        payload.setDeliveryInstructions(instructions);
+        payload.setExpiration(m.getMessageExpiration());
+
+        GarlicMessage msg = GarlicMessageBuilder.buildMessage(ctx, payload, null, null, 
+                                                              null, encryptKey, encryptTag);
+        return msg;
+    }
 }    
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java
index d6e0f1ae7f..3c47769f4e 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java
@@ -489,7 +489,7 @@ class StoreJob extends JobImpl {
         String v = ri.getOption("router.version");
         if (v == null)
             return false;
-        return (new VersionComparator()).compare(v, MIN_ENCRYPTION_VERSION) >= 0;
+        return VersionComparator.comp(v, MIN_ENCRYPTION_VERSION) >= 0;
     }
 
     /**
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
index 80bd1803af..1c71e7bb71 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
@@ -202,7 +202,6 @@ abstract class BuildRequestor {
     private static final boolean SEND_VARIABLE = true;
     /** 5 (~2600 bytes) fits nicely in 3 tunnel messages */
     private static final int SHORT_RECORDS = 5;
-    private static final VersionComparator _versionComparator = new VersionComparator();
     private static final List<Integer> SHORT_ORDER = new ArrayList(SHORT_RECORDS);
     static {
         for (int i = 0; i < SHORT_RECORDS; i++)
@@ -217,7 +216,7 @@ abstract class BuildRequestor {
         String v = ri.getOption("router.version");
         if (v == null)
             return false;
-        return _versionComparator.compare(v, MIN_VARIABLE_VERSION) >= 0;
+        return VersionComparator.comp(v, MIN_VARIABLE_VERSION) >= 0;
     }
 
     /**
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 93b43a1fab..f5f6b8b425 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/TestJob.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/TestJob.java
@@ -1,6 +1,5 @@
 package net.i2p.router.tunnel.pool;
 
-import java.util.HashSet;
 import java.util.Set;
 
 import net.i2p.crypto.SessionKeyManager;
@@ -19,6 +18,7 @@ import net.i2p.router.RouterContext;
 import net.i2p.router.TunnelInfo;
 import net.i2p.router.message.GarlicMessageBuilder;
 import net.i2p.router.message.PayloadGarlicConfig;
+import net.i2p.router.util.RemovableSingletonSet;
 import net.i2p.stat.Rate;
 import net.i2p.stat.RateStat;
 import net.i2p.util.Log;
@@ -140,9 +140,7 @@ class TestJob extends JobImpl {
             scheduleRetest();
             return;
         }
-        // can't be a singleton, the SKM modifies it
-        Set encryptTags = new HashSet(1);
-        encryptTags.add(encryptTag);
+        Set<SessionTag> encryptTags = new RemovableSingletonSet(encryptTag);
         // Register the single tag with the appropriate SKM
         if (_cfg.isInbound() && !_pool.getSettings().isExploratory()) {
             SessionKeyManager skm = getContext().clientManager().getClientSessionKeyManager(_pool.getSettings().getDestination());
diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java
index f125cf42e8..dd449949b7 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java
@@ -345,7 +345,6 @@ public abstract class TunnelPeerSelector {
     
     /** 0.7.8 and earlier had major message corruption bugs */
     private static final String MIN_VERSION = "0.7.9";
-    private static final VersionComparator _versionComparator = new VersionComparator();
 
     private static boolean shouldExclude(RouterContext ctx, Log log, RouterInfo peer, char excl[]) {
         String cap = peer.getCapabilities();
@@ -371,7 +370,7 @@ public abstract class TunnelPeerSelector {
 
         // minimum version check
         String v = peer.getOption("router.version");
-        if (v == null || _versionComparator.compare(v, MIN_VERSION) < 0)
+        if (v == null || VersionComparator.comp(v, MIN_VERSION) < 0)
             return true;
 
         // uptime is always spoofed to 90m, so just remove all this
diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPool.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPool.java
index 924486e6a2..9e73535e45 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPool.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPool.java
@@ -1114,8 +1114,7 @@ public class TunnelPool {
                 return null;
             }
         } else {
-            peers = new ArrayList(1);
-            peers.add(_context.routerHash());
+            peers = Collections.singletonList(_context.routerHash());
         }
 
         PooledTunnelCreatorConfig cfg = new PooledTunnelCreatorConfig(_context, peers.size(), settings.isInbound(), settings.getDestination());
diff --git a/router/java/src/net/i2p/router/util/RemovableSingletonSet.java b/router/java/src/net/i2p/router/util/RemovableSingletonSet.java
new file mode 100644
index 0000000000..57aa1e9795
--- /dev/null
+++ b/router/java/src/net/i2p/router/util/RemovableSingletonSet.java
@@ -0,0 +1,78 @@
+package net.i2p.router.util;
+
+import java.util.AbstractSet;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+/**
+ *  Like Collections.singleton() but item is removable,
+ *  clear() is supported, and the iterator supports remove().
+ *  Item may not be null. add() and addAll() unsupported.
+ *  Unsynchronized.
+ *
+ *  @since 0.9.7
+ */
+public class RemovableSingletonSet<E> extends AbstractSet<E> {
+    private E _elem;
+
+    public RemovableSingletonSet(E element) {
+        if (element == null)
+            throw new NullPointerException();
+        _elem = element;
+    }
+
+    @Override
+    public void clear() {
+        _elem = null;
+    }
+
+    @Override
+    public boolean contains(Object o) {
+        return o != null && o.equals(_elem);
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return _elem == null;
+    }
+
+    @Override
+    public boolean remove(Object o) {
+        boolean rv = o.equals(_elem);
+        if (rv)
+            _elem = null;
+        return rv;
+    }
+
+    public int size() {
+        return _elem != null ? 1 : 0;
+    }
+
+    public Iterator<E> iterator() {
+        return new RSSIterator();
+    }
+
+    private class RSSIterator implements Iterator<E> {
+        boolean done;
+
+        public boolean hasNext() {
+            return _elem != null && !done;
+        }
+
+        public E next() {
+            if (!hasNext())
+                throw new NoSuchElementException();
+            done = true;
+            return _elem;
+        }
+
+        public void remove() {
+            if (_elem == null || !done)
+                throw new IllegalStateException();
+            _elem = null;
+        }
+    }
+}
+    
-- 
GitLab