diff --git a/LICENSE.txt b/LICENSE.txt
index 766691a55e7fe8c49c3ade68171d02cd66f8b5dd..d177d7983ccac15d23a3d94a0d4a919a64c2667f 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -283,7 +283,7 @@ Applications:
       Bundles systray4j-2.4.1:
       See licenses/LICENSE-LGPLv2.1.txt
 
-   Tomcat 6.0.47:
+   Tomcat 6.0.48:
    Copyright 1999-2016 The Apache Software Foundation
    See licenses/LICENSE-Apache2.0.txt
    See licenses/NOTICE-Tomcat.txt
diff --git a/apps/addressbook/build.xml b/apps/addressbook/build.xml
index e4e913cb20e6e43b58d744d24ad619d9cfaa91bc..eed69a3fa427bcef4fe73bd1d78ec65253a6b315 100644
--- a/apps/addressbook/build.xml
+++ b/apps/addressbook/build.xml
@@ -86,6 +86,8 @@
 		                <attribute name="Build-Date" value="${build.timestamp}" />
 		                <attribute name="Base-Revision" value="${workspace.version}" />
 		                <attribute name="Workspace-Changes" value="${workspace.changes.tr}" />
+				<attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+				<attribute name="X-Compile-Target-JDK" value="${javac.version}" />
 			</manifest>
 		</jar>
 	</target>
@@ -106,6 +108,8 @@
 		                <attribute name="Build-Date" value="${build.timestamp}" />
 		                <attribute name="Base-Revision" value="${workspace.version}" />
 		                <attribute name="Workspace-Changes" value="${workspace.changes.tr}" />
+				<attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+				<attribute name="X-Compile-Target-JDK" value="${javac.version}" />
 			</manifest>
 		</war>
 		<delete dir="${dist}/tmp"/>
diff --git a/apps/desktopgui/build.xml b/apps/desktopgui/build.xml
index b31cc86dfdbd29405a65c2a6afb24bcf9005224c..a42633518952ee435744c88e7fd31b406abc696e 100644
--- a/apps/desktopgui/build.xml
+++ b/apps/desktopgui/build.xml
@@ -83,6 +83,8 @@
 				<attribute name="Build-Date" value="${build.timestamp}" />
 				<attribute name="Base-Revision" value="${workspace.version}" />
 				<attribute name="Workspace-Changes" value="${workspace.changes.tr}" />
+				<attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+				<attribute name="X-Compile-Target-JDK" value="${javac.version}" />
 			</manifest>
 		</jar>
 	</target>
diff --git a/apps/i2psnark/java/build.xml b/apps/i2psnark/java/build.xml
index ee3ad0f036712bdb12cd40910d246bf45cb01e22..3ecde039383a1d13a66fa19cbbd0c083ee5fbb9d 100644
--- a/apps/i2psnark/java/build.xml
+++ b/apps/i2psnark/java/build.xml
@@ -84,6 +84,8 @@
                 <attribute name="Build-Date" value="${build.timestamp}" />
                 <attribute name="Base-Revision" value="${workspace.version}" />
                 <attribute name="Workspace-Changes" value="${workspace.changes.tr}" />
+                <attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+                <attribute name="X-Compile-Target-JDK" value="${javac.version}" />
             </manifest>
         </jar>
     </target>    
@@ -130,6 +132,8 @@
                 <attribute name="Build-Date" value="${build.timestamp}" />
                 <attribute name="Base-Revision" value="${workspace.version}" />
                 <attribute name="Workspace-Changes" value="${workspace.changes.tr}" />
+                <attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+                <attribute name="X-Compile-Target-JDK" value="${javac.version}" />
             </manifest>
         </war>
     </target>
diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java
index 29a0f33fc4ac851ef0c00e9166499ff53a5ac2f2..f92c3da9975d2fd974d53bcb3f07bcb314e913aa 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java
@@ -123,6 +123,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT {
     private final AtomicLong _rxBytes = new AtomicLong();
     private final AtomicLong _txBytes = new AtomicLong();
     private long _started;
+    private long _nodesLastSaved;
 
     /** all-zero NID used for pings */
     public static final NID FAKE_NID = new NID(new byte[NID.HASH_LENGTH]);
@@ -156,6 +157,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT {
     private static final long CLEAN_TIME = 63*1000;
     private static final long EXPLORE_TIME = 877*1000;
     private static final long BLACKLIST_CLEAN_TIME = 17*60*1000;
+    private static final long NODES_SAVE_TIME = 3*60*60*1000;
     public static final String DHT_FILE_SUFFIX = ".dht.dat";
 
     private static final int SEND_CRYPTO_TAGS = 8;
@@ -635,6 +637,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT {
         _txBytes.set(0);
         _rxBytes.set(0);
         _started = _context.clock().now();
+        _nodesLastSaved = _started;
     }
 
     /**
@@ -1675,6 +1678,10 @@ public class KRPC implements I2PSessionMuxedListener, DHT {
                 if (nid.lastSeen() < expire)
                     iter.remove();
             }
+            if (now - _nodesLastSaved > NODES_SAVE_TIME) {
+                PersistDHT.saveDHT(_knownNodes, false, _dhtFile);
+                _nodesLastSaved = now;
+            }
             // TODO sent queries?
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug("KRPC cleaner done, now with " +
diff --git a/apps/i2ptunnel/java/build.xml b/apps/i2ptunnel/java/build.xml
index 6c4698a27a61f81d5b1396620eb89c816271954a..93f5cc3391f6130fa152bc8bc77e837935d91328 100644
--- a/apps/i2ptunnel/java/build.xml
+++ b/apps/i2ptunnel/java/build.xml
@@ -72,6 +72,8 @@
                 <attribute name="Build-Date" value="${build.timestamp}" />
                 <attribute name="Base-Revision" value="${workspace.version}" />
                 <attribute name="Workspace-Changes" value="${workspace.changes.j.tr}" />
+                <attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+                <attribute name="X-Compile-Target-JDK" value="${javac.version}" />
             </manifest>
         </jar>
         <jar destfile="./build/temp-beans.jar" basedir="./build/obj" includes="**/ui/*.class **/EditBean.class **/IndexBean.class" />
@@ -103,6 +105,8 @@
                 <attribute name="Build-Date" value="${build.timestamp}" />
                 <attribute name="Base-Revision" value="${workspace.version}" />
                 <attribute name="Workspace-Changes" value="${workspace.changes.j.tr}" />
+                <attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+                <attribute name="X-Compile-Target-JDK" value="${javac.version}" />
             </manifest>
         </jar>
     </target>
@@ -241,6 +245,8 @@
                 <attribute name="Build-Date" value="${build.timestamp}" />
                 <attribute name="Base-Revision" value="${workspace.version}" />
                 <attribute name="Workspace-Changes" value="${workspace.changes.w.tr}" />
+                <attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+                <attribute name="X-Compile-Target-JDK" value="${javac.version}" />
             </manifest>
         </war>
     </target>
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
index 2d6e053b03054c67712bd0ac1394399e66d09dc3..4908042dd660030e4ef6adf6597e0067eb0e4ecc 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
@@ -304,7 +304,7 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
                             return AuthResult.AUTH_GOOD;
                         }
                     }
-                    _log.logAlways(Log.WARN, "PROXY AUTH FAILURE: user " + user);
+                    _log.logAlways(Log.WARN, "HTTP proxy authentication failed, user: " + user);
                 } catch (UnsupportedEncodingException uee) {
                     _log.error(getPrefix(requestId) + "No UTF-8 support? B64: " + authorization, uee);
                 } catch (ArrayIndexOutOfBoundsException aioobe) {
@@ -363,7 +363,7 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
         String ha1 = getTunnel().getClientOptions().getProperty(PROP_PROXY_DIGEST_PREFIX + user +
                                                                 PROP_PROXY_DIGEST_SUFFIX);
         if (ha1 == null) {
-            _log.logAlways(Log.WARN, "PROXY AUTH FAILURE: user " + user);
+            _log.logAlways(Log.WARN, "HTTP proxy authentication failed, user: " + user);
             return AuthResult.AUTH_BAD;
         }
         // get H(A2)
@@ -373,7 +373,7 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
         String kd = ha1 + ':' + nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2;
         String hkd = PasswordManager.md5Hex(kd);
         if (!response.equals(hkd)) {
-            _log.logAlways(Log.WARN, "PROXY AUTH FAILURE: user " + user);
+            _log.logAlways(Log.WARN, "HTTP proxy authentication failed, user: " + user);
             if (_log.shouldLog(Log.INFO))
                 _log.info("Bad digest auth: " + DataHelper.toString(args));
             return AuthResult.AUTH_BAD;
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java
index 823497f281c1460ccc5c08bc2cfc6c59749a3c1c..f3d5246fdcd2a5280c9a9664942fb319ee39b89f 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java
@@ -131,26 +131,32 @@ class SOCKS5Server extends SOCKSServer {
      */
     private void verifyPassword(DataInputStream in, DataOutputStream out) throws IOException, SOCKSException {
         int c = in.readUnsignedByte();
-        if (c != AUTH_VERSION)
+        if (c != AUTH_VERSION) {
+            _log.logAlways(Log.WARN, "SOCKS proxy authentication failed");
             throw new SOCKSException("Unsupported authentication version");
+        }
         c = in.readUnsignedByte();
-        if (c <= 0)
+        if (c <= 0) {
+            _log.logAlways(Log.WARN, "SOCKS proxy authentication failed");
             throw new SOCKSException("Bad authentication");
+        }
         byte[] user = new byte[c];
+        String u = new String(user, "UTF-8");
         in.readFully(user);
         c = in.readUnsignedByte();
-        if (c <= 0)
+        if (c <= 0) {
+            _log.logAlways(Log.WARN, "SOCKS proxy authentication failed, user: " + u);
             throw new SOCKSException("Bad authentication");
+        }
         byte[] pw = new byte[c];
         in.readFully(pw);
         // Hopefully these are in UTF-8, since that's what our config file is in
         // these throw UnsupportedEncodingException which is an IOE
-        String u = new String(user, "UTF-8");
         String p = new String(pw, "UTF-8");
         String configUser =  props.getProperty(I2PTunnelHTTPClientBase.PROP_USER);
         String configPW = props.getProperty(I2PTunnelHTTPClientBase.PROP_PW);
         if ((!u.equals(configUser)) || (!p.equals(configPW))) {
-            _log.error("SOCKS authorization failure");
+            _log.logAlways(Log.WARN, "SOCKS proxy authentication failed, user: " + u);
             sendAuthReply(AUTH_FAILURE, out);
             throw new SOCKSException("SOCKS authorization failure");
         }
@@ -591,9 +597,13 @@ class SOCKS5Server extends SOCKSServer {
             // todo pass the response through?
         } catch (IOException e) {
             try { destSock.close(); } catch (IOException ioe) {}
+            if (in != null) try { in.close(); } catch (IOException ioe) {}
+            if (out != null) try { out.close(); } catch (IOException ioe) {}
             throw e;
         } catch (SOCKSException e) {
             try { destSock.close(); } catch (IOException ioe) {}
+            if (in != null) try { in.close(); } catch (IOException ioe) {}
+            if (out != null) try { out.close(); } catch (IOException ioe) {}
             throw e;
         }
         // that's it, caller will send confirmation to our client
@@ -601,9 +611,10 @@ class SOCKS5Server extends SOCKSServer {
     }
 
     // This isn't really the right place for this, we can't stop the tunnel once it starts.
-    static SOCKSUDPTunnel _tunnel;
-    static final Object _startLock = new Object();
-    static byte[] dummyIP = new byte[4];
+    private static SOCKSUDPTunnel _tunnel;
+    private static final Object _startLock = new Object();
+    private static final byte[] dummyIP = new byte[4];
+
     /**
      * We got a UDP associate command.
      * Loop here looking for more, never return normally,
diff --git a/apps/imagegen/identicon/build.xml b/apps/imagegen/identicon/build.xml
index e0e85bd53287ac6658dd4f8998d4622a9907f031..831b3ff7f33236f33a7c2e826d8efb53f35a819b 100644
--- a/apps/imagegen/identicon/build.xml
+++ b/apps/imagegen/identicon/build.xml
@@ -71,6 +71,8 @@
                 <attribute name="Build-Date" value="${build.timestamp}" />
                 <attribute name="Base-Revision" value="${workspace.version}" />
                 <attribute name="Workspace-Changes" value="${workspace.changes.tr}" />
+                <attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+                <attribute name="X-Compile-Target-JDK" value="${javac.version}" />
             </manifest>
         </jar>
     </target>
diff --git a/apps/imagegen/imagegen/build.xml b/apps/imagegen/imagegen/build.xml
index 7cc657c05177f0613d2363cd530ae247b18d1011..bc0d96f24fbb3247c74c39c3025904c646d034c3 100644
--- a/apps/imagegen/imagegen/build.xml
+++ b/apps/imagegen/imagegen/build.xml
@@ -75,6 +75,8 @@
                 <attribute name="Build-Date" value="${build.timestamp}" />
                 <attribute name="Base-Revision" value="${workspace.version}" />
                 <attribute name="Workspace-Changes" value="${workspace.changes.tr}" />
+                <attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+                <attribute name="X-Compile-Target-JDK" value="${javac.version}" />
             </manifest>
         </war>
     </target>
diff --git a/apps/imagegen/zxing/build.xml b/apps/imagegen/zxing/build.xml
index c878015b17eafa31bacfd5441dc7aac3f16b4281..c615d86839e04f231bb76cdef0cc2dc6c65c6a80 100644
--- a/apps/imagegen/zxing/build.xml
+++ b/apps/imagegen/zxing/build.xml
@@ -17,6 +17,7 @@
     </target>
 
     <!-- only used if not set by a higher build.xml -->
+    <property name="javac.version" value="1.7" />
     <property name="javac.compilerargs7" value="" />
 
     <target name="compile" depends="depend">
@@ -39,7 +40,7 @@
 	<mkdir dir="./buildTest/obj" />
         <javac
             srcdir="./test/junit"
-            debug="true" deprecation="on" source="1.7" target="1.7"
+            debug="true" deprecation="on" source="${javac.version}" target="${javac.version}"
             includeAntRuntime="false"
             destdir="./buildTest/obj"
             classpath="./build/zxing.jar" >
@@ -75,6 +76,8 @@
                 <attribute name="Build-Date" value="${build.timestamp}" />
                 <attribute name="Base-Revision" value="${workspace.version}" />
                 <attribute name="Workspace-Changes" value="${workspace.changes.tr}" />
+                <attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+                <attribute name="X-Compile-Target-JDK" value="${javac.version}" />
             </manifest>
         </jar>
     </target>
diff --git a/apps/jetty/apache-tomcat-deployer/README-i2p.txt b/apps/jetty/apache-tomcat-deployer/README-i2p.txt
index c354fb71af810a69c72b2b26a837384326d59123..6b8c0edf3159935d2009190e588c58b9db99d51c 100644
--- a/apps/jetty/apache-tomcat-deployer/README-i2p.txt
+++ b/apps/jetty/apache-tomcat-deployer/README-i2p.txt
@@ -2,7 +2,7 @@ This is Apache Tomcat 6.x, supporting Servlet 2.5 and JSP 2.1.
 The Glassfish JSP 2.1 bundled in Jetty 6 is way too old.
 
 Retrieved from the file
-	apache-tomcat-6.0.44-deployer.tar.gz
+	apache-tomcat-6.0.48-deployer.tar.gz
 
 minus the following files and directores:
 
diff --git a/apps/jetty/apache-tomcat-deployer/lib/el-api.jar b/apps/jetty/apache-tomcat-deployer/lib/el-api.jar
index 4b5aa36d302381d8b05d04d91ed45141030ab908..2598a4a8fa66bcaec88ccede7710159f6d9fead8 100644
Binary files a/apps/jetty/apache-tomcat-deployer/lib/el-api.jar and b/apps/jetty/apache-tomcat-deployer/lib/el-api.jar differ
diff --git a/apps/jetty/apache-tomcat-deployer/lib/jasper-el.jar b/apps/jetty/apache-tomcat-deployer/lib/jasper-el.jar
index 51ec6f3080e949072f84ae6fbeb3383d846ca02a..b7205e347de032147194f587b4a5adaaae554fbe 100644
Binary files a/apps/jetty/apache-tomcat-deployer/lib/jasper-el.jar and b/apps/jetty/apache-tomcat-deployer/lib/jasper-el.jar differ
diff --git a/apps/jetty/apache-tomcat-deployer/lib/jasper.jar b/apps/jetty/apache-tomcat-deployer/lib/jasper.jar
index 7242ce7d1bd874900ca2ed17ff3ce825acb69a46..ca2d7269d81e81c090fbb58fd48c239a017b7466 100644
Binary files a/apps/jetty/apache-tomcat-deployer/lib/jasper.jar and b/apps/jetty/apache-tomcat-deployer/lib/jasper.jar differ
diff --git a/apps/jetty/apache-tomcat-deployer/lib/tomcat-juli.jar b/apps/jetty/apache-tomcat-deployer/lib/tomcat-juli.jar
index 37d316ecacc7d4746890c6a17f8c933de73adf7c..c5e0f8f7791109d3a488042772351e1a47f16c21 100644
Binary files a/apps/jetty/apache-tomcat-deployer/lib/tomcat-juli.jar and b/apps/jetty/apache-tomcat-deployer/lib/tomcat-juli.jar differ
diff --git a/apps/jetty/apache-tomcat/README-i2p.txt b/apps/jetty/apache-tomcat/README-i2p.txt
index f62c55e862898c2b60e0b83f2ef10ba23b9d8d8a..015ff4736a7757a2868b3ef29c58a02e4a4815e9 100644
--- a/apps/jetty/apache-tomcat/README-i2p.txt
+++ b/apps/jetty/apache-tomcat/README-i2p.txt
@@ -1,7 +1,7 @@
 This is Apache Tomcat 6.x, supporting Servlet 2.5 and JSP 2.1.
 
 Retrieved from the file
-	apache-tomcat-6.0.44.tar.gz
+	apache-tomcat-6.0.48.tar.gz
 
 containing only a small subset of lib/tomcat-coyote.jar.
 
diff --git a/apps/jetty/apache-tomcat/lib/tomcat-coyote-util.jar b/apps/jetty/apache-tomcat/lib/tomcat-coyote-util.jar
index 9403f2ece5024641e010d97365c52eeb0b6ad1cb..a45499414ec8c2d28372de113225872cb40113f9 100644
Binary files a/apps/jetty/apache-tomcat/lib/tomcat-coyote-util.jar and b/apps/jetty/apache-tomcat/lib/tomcat-coyote-util.jar differ
diff --git a/apps/jetty/build.xml b/apps/jetty/build.xml
index 7e19fa1dbeafa1d501dadabc62bea973df48c83a..f0b2c7cce669d63b66a528e62d1bd680f7d5fcdc 100644
--- a/apps/jetty/build.xml
+++ b/apps/jetty/build.xml
@@ -22,7 +22,7 @@
     <property name="javac.compilerargs" value="" />
     <property name="javac.version" value="1.7" />
     <property name="tomcat.lib" value="apache-tomcat-deployer/lib" />
-    <property name="tomcat.ver" value="6.0.47" />
+    <property name="tomcat.ver" value="6.0.48" />
     <property name="tomcat2.lib" value="apache-tomcat-${tomcat.ver}/lib" />
     <property name="tomcat2.lib.small" value="apache-tomcat/lib" />
 
@@ -340,6 +340,8 @@
                 <!-- needed by JettyStart for pre-0.7.5 wrapper.config -->
                 <attribute name="Class-Path" value="jetty-deploy.jar jetty-xml.jar" />
                 <attribute name="Workspace-Changes" value="${workspace.changes.tr}" />
+                <attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+                <attribute name="X-Compile-Target-JDK" value="${javac.version}" />
             </manifest>
         </jar>
     </target>    
diff --git a/apps/jrobin/java/build.xml b/apps/jrobin/java/build.xml
index 3d481f99eb392261508212634e41e5202e175be6..c569ddfecb31bda7289777ce8e7bdaca2d3a4127 100644
--- a/apps/jrobin/java/build.xml
+++ b/apps/jrobin/java/build.xml
@@ -13,6 +13,9 @@
             cache="../../../build"
             srcdir="./src"
             destdir="./build/obj" >
+            <classpath>
+                <pathelement location="../../../core/java/build/i2p.jar" />
+            </classpath>
         </depend>
     </target>
 
@@ -32,6 +35,9 @@
             includeAntRuntime="false"
             includes="**/*.java" >
             <compilerarg line="${javac.compilerargs}" />
+            <classpath>
+                <pathelement location="../../../core/java/build/i2p.jar" />
+            </classpath>
         </javac>
     </target>
 
@@ -54,6 +60,7 @@
         <property name="workspace.changes.tr" value="" />
         <jar destfile="./build/jrobin.jar" basedir="./build/obj" includes="**/*.class">
             <manifest>
+                <attribute name="Class-Path" value="i2p.jar" />
                 <attribute name="Implementation-Version" value="1.6.0-1" />
                 <attribute name="Built-By" value="${build.built-by}" />
                 <attribute name="Build-Date" value="${build.timestamp}" />
diff --git a/apps/jrobin/java/src/engine/misc/DeallocationHelper.java b/apps/jrobin/java/src/engine/misc/DeallocationHelper.java
index 679b658e5266b22b2a7ce55ff8438cc27374e895..3bc2205e03d95a37556c42862e984f2483661113 100644
--- a/apps/jrobin/java/src/engine/misc/DeallocationHelper.java
+++ b/apps/jrobin/java/src/engine/misc/DeallocationHelper.java
@@ -31,8 +31,10 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
+
+import net.i2p.I2PAppContext;
+import net.i2p.util.Log;
+
 
 /**
  * Helper to deallocate memory on the native heap allocated during the creation
@@ -56,12 +58,16 @@ import java.util.logging.Logger;
  */
 public class DeallocationHelper {
 
+    private final Log logger = I2PAppContext.getGlobalContext().logManager().getLog(DeallocationHelper.class);
+
     /**
      * tool responsible for releasing the native memory of a deallocatable byte
      * buffer
      */
     public static abstract class Deallocator {
 
+        protected final Log logger = I2PAppContext.getGlobalContext().logManager().getLog(DeallocationHelper.class);
+
         public Deallocator() {
             super();
         }
@@ -101,7 +107,7 @@ public class DeallocationHelper {
                     cleanerCleanMethod = cleanerClass.getDeclaredMethod("clean");
                 }
             } catch (ClassNotFoundException | NoSuchMethodException e) {
-                logger.log(Level.WARNING,
+                logger.warn(
                         "The initialization of the deallocator for Oracle Java, Sun Java and OpenJDK has failed", e);
             }
         }
@@ -122,7 +128,7 @@ public class DeallocationHelper {
                         success = true;
                     }
                 } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
-                    logger.log(Level.WARNING, "The deallocation of a direct NIO buffer has failed", e);
+                    logger.warn("The deallocation of a direct NIO buffer has failed", e);
                 } finally {
                     directByteBufferCleanerMethod.setAccessible(directByteBufferCleanerMethodWasAccessible);
                     cleanerCleanMethod.setAccessible(cleanerCleanMethodWasAccessible);
@@ -142,7 +148,7 @@ public class DeallocationHelper {
                 final Class<?> directByteBufferClass = Class.forName("java.nio.DirectByteBuffer");
                 directByteBufferFreeMethod = directByteBufferClass.getDeclaredMethod("free");
             } catch (ClassNotFoundException | NoSuchMethodException e) {
-                logger.log(Level.WARNING, "The initialization of the deallocator for Android has failed", e);
+                logger.warn("The initialization of the deallocator for Android has failed", e);
             }
         }
 
@@ -156,7 +162,7 @@ public class DeallocationHelper {
                     directByteBufferFreeMethod.invoke(directByteBuffer);
                     success = true;
                 } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
-                    logger.log(Level.WARNING, "The deallocation of a direct NIO buffer has failed", e);
+                    logger.warn("The deallocation of a direct NIO buffer has failed", e);
                 } finally {
                     directByteBufferFreeMethod.setAccessible(directByteBufferFreeMethodWasAccessible);
                 }
@@ -180,7 +186,7 @@ public class DeallocationHelper {
                         gnuClasspathPointerClass);
                 bufferAddressField = Buffer.class.getDeclaredField("address");
             } catch (ClassNotFoundException | NoSuchMethodException | NoSuchFieldException e) {
-                logger.log(Level.WARNING, "The initialization of the deallocator for GNU Classpath has failed", e);
+                logger.warn("The initialization of the deallocator for GNU Classpath has failed", e);
             }
         }
 
@@ -199,7 +205,7 @@ public class DeallocationHelper {
                         success = true;
                     }
                 } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
-                    logger.log(Level.WARNING, "The deallocation of a direct NIO buffer has failed", e);
+                    logger.warn("The deallocation of a direct NIO buffer has failed", e);
                 } finally {
                     bufferAddressField.setAccessible(bufferAddressFieldWasAccessible);
                     vmDirectByteBufferFreeMethod.setAccessible(vmDirectByteBufferFreeMethodWasAccessible);
@@ -219,7 +225,7 @@ public class DeallocationHelper {
                 final Class<?> directByteBufferClass = Class.forName("java.nio.DirectByteBuffer");
                 directByteBufferFreeMethod = directByteBufferClass.getDeclaredMethod("free");
             } catch (ClassNotFoundException | NoSuchMethodException e) {
-                logger.log(Level.WARNING, "The initialization of the deallocator for Apache Harmony has failed", e);
+                logger.warn("The initialization of the deallocator for Apache Harmony has failed", e);
             }
         }
 
@@ -233,7 +239,7 @@ public class DeallocationHelper {
                     directByteBufferFreeMethod.invoke(directByteBuffer);
                     success = true;
                 } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
-                    logger.log(Level.WARNING, "The deallocation of a direct NIO buffer has failed", e);
+                    logger.warn("The deallocation of a direct NIO buffer has failed", e);
                 } finally {
                     directByteBufferFreeMethod.setAccessible(directByteBufferFreeMethodWasAccessible);
                 }
@@ -242,8 +248,6 @@ public class DeallocationHelper {
         }
     }
 
-    private static final Logger logger = Logger.getLogger(DeallocationHelper.class.getName());
-
     private Map<Class<?>, Field> attachmentOrByteBufferFieldMap;
 
     private Set<Class<?>> deallocatableBufferClassSet;
@@ -431,7 +435,7 @@ public class DeallocationHelper {
                             }
                             superClassesMsg = builder.toString();
                         }
-                        logger.warning("The field " + fieldname + " hasn't been found in the class " + classname
+                        logger.warn("The field " + fieldname + " hasn't been found in the class " + classname
                                 + superClassesMsg);
                     } else {// the field has been found, stores it into the map
                         attachmentOrByteBufferFieldMap.put(bufferClass, bufferField);
@@ -449,7 +453,7 @@ public class DeallocationHelper {
                     final String msg = "The class " + classname
                             + " hasn't been found while initializing the deallocator. Java vendor: " + javaVendor
                             + " Java version: " + javaVersion;
-                    logger.log(Level.WARNING, msg, cnfe);
+                    logger.warn(msg, cnfe);
                 }
             }
         // if a known implementation has drastically changed or if the current
@@ -567,7 +571,7 @@ public class DeallocationHelper {
                                     break;
                                 }
                             } catch (IllegalAccessException iae) {
-                                logger.log(Level.WARNING, "Cannot access the field " + field.getName()
+                                logger.warn("Cannot access the field " + field.getName()
                                         + " of the class " + bufferIntermediaryClass.getName(), iae);
                             } finally {
                                 field.setAccessible(fieldWasAccessible);
@@ -594,7 +598,7 @@ public class DeallocationHelper {
                 final String msg = "The class " + directByteBufferClassName
                         + " hasn't been found while initializing the deallocator. Java vendor: " + javaVendor
                         + " Java version: " + javaVersion;
-                logger.log(Level.WARNING, msg, cnfe);
+                logger.warn(msg, cnfe);
             }
             if (directByteBufferClass != null)
                 deallocatableBufferClassSet.add(directByteBufferClass);
@@ -607,7 +611,7 @@ public class DeallocationHelper {
                 final String msg = "The class " + readOnlyDirectByteBufferClassName
                         + " hasn't been found while initializing the deallocator. Java vendor: " + javaVendor
                         + " Java version: " + javaVersion;
-                logger.log(Level.WARNING, msg, cnfe);
+                logger.warn(msg, cnfe);
             }
             if (readOnlyDirectByteBufferClass != null)
                 deallocatableBufferClassSet.add(readOnlyDirectByteBufferClass);
@@ -619,7 +623,7 @@ public class DeallocationHelper {
                 final String msg = "The class " + readWriteDirectByteBufferClassName
                         + " hasn't been found while initializing the deallocator. Java vendor: " + javaVendor
                         + " Java version: " + javaVersion;
-                logger.log(Level.WARNING, msg, cnfe);
+                logger.warn(msg, cnfe);
             }
             if (readWriteDirectByteBufferClass != null)
                 deallocatableBufferClassSet.add(readWriteDirectByteBufferClass);
@@ -632,7 +636,7 @@ public class DeallocationHelper {
                 final String msg = "The class " + readOnlyDirectByteBufferClassName
                         + " hasn't been found while initializing the deallocator. Java vendor: " + javaVendor
                         + " Java version: " + javaVersion;
-                logger.log(Level.WARNING, msg, cnfe);
+                logger.warn(msg, cnfe);
             }
             if (readOnlyDirectByteBufferClass != null)
                 deallocatableBufferClassSet.add(readOnlyDirectByteBufferClass);
@@ -644,7 +648,7 @@ public class DeallocationHelper {
                 final String msg = "The class " + readWriteDirectByteBufferClassName
                         + " hasn't been found while initializing the deallocator. Java vendor: " + javaVendor
                         + " Java version: " + javaVersion;
-                logger.log(Level.WARNING, msg, cnfe);
+                logger.warn(msg, cnfe);
             }
             if (readWriteDirectByteBufferClass != null)
                 deallocatableBufferClassSet.add(readWriteDirectByteBufferClass);
@@ -730,7 +734,7 @@ public class DeallocationHelper {
                       // deallocatable buffer
                     deallocatableDirectByteBuffer = null;
                     final String bufferClassName = bufferClass.getName();
-                    logger.warning("No deallocatable buffer has been found for an instance of the class "
+                    logger.warn("No deallocatable buffer has been found for an instance of the class "
                             + bufferClassName + " whereas it is a direct NIO buffer");
                 }
             } else {// the passed buffer contains another buffer, looks for a
diff --git a/apps/jrobin/java/src/org/jrobin/core/jrrd/RRDFile.java b/apps/jrobin/java/src/org/jrobin/core/jrrd/RRDFile.java
index 179cc473e1729640f8f8c81c7f095158b1b2ef1f..0dcd7808d52ef4341bf4514fe8d898dc97697073 100644
--- a/apps/jrobin/java/src/org/jrobin/core/jrrd/RRDFile.java
+++ b/apps/jrobin/java/src/org/jrobin/core/jrrd/RRDFile.java
@@ -188,7 +188,7 @@ public class RRDFile implements Constants {
 		if(this.debug) {
 			System.out.println(value);
 		}
-		return (int)value;
+		return value;
 	}
 
 	String readString(int maxLength) throws IOException, RrdException {
diff --git a/apps/ministreaming/java/build.xml b/apps/ministreaming/java/build.xml
index 8232492b1f7ceaa2af6e7fbb430965d9f0ed4073..cbef281b9375b9ef35420fe91fd1ec03b231779b 100644
--- a/apps/ministreaming/java/build.xml
+++ b/apps/ministreaming/java/build.xml
@@ -86,6 +86,8 @@
                 <attribute name="Build-Date" value="${build.timestamp}" />
                 <attribute name="Base-Revision" value="${workspace.version}" />
                 <attribute name="Workspace-Changes" value="${workspace.changes.tr}" />
+                <attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+                <attribute name="X-Compile-Target-JDK" value="${javac.version}" />
             </manifest>
         </jar>
     </target>
diff --git a/apps/routerconsole/java/build.xml b/apps/routerconsole/java/build.xml
index 76162ae04cb781b9aae68ae7570cb2c45009ba13..522ee06a76f67e8f09eaefda393af1f4c5489b75 100644
--- a/apps/routerconsole/java/build.xml
+++ b/apps/routerconsole/java/build.xml
@@ -130,6 +130,8 @@
                 <attribute name="Build-Date" value="${build.timestamp}" />
                 <attribute name="Base-Revision" value="${workspace.version}" />
                 <attribute name="Workspace-Changes" value="${workspace.changes.j.tr}" />
+                <attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+                <attribute name="X-Compile-Target-JDK" value="${javac.version}" />
             </manifest>
         </jar>
     </target>
@@ -302,6 +304,8 @@
                 <attribute name="Build-Date" value="${build.timestamp}" />
                 <attribute name="Base-Revision" value="${workspace.version}" />
                 <attribute name="Workspace-Changes" value="${workspace.changes.w.tr}" />
+                <attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+                <attribute name="X-Compile-Target-JDK" value="${javac.version}" />
             </manifest>
         </war>
     </target>
diff --git a/apps/routerconsole/java/src/net/i2p/router/news/NewsManager.java b/apps/routerconsole/java/src/net/i2p/router/news/NewsManager.java
index e85e232085682a3dbfa3008141f7e49f0f4d8eed..2b5af405d1bbedb107666fc43e9f5a1ed4bf6b63 100644
--- a/apps/routerconsole/java/src/net/i2p/router/news/NewsManager.java
+++ b/apps/routerconsole/java/src/net/i2p/router/news/NewsManager.java
@@ -172,6 +172,23 @@ public class NewsManager implements ClientApp {
         return parseNews(newsContent, false);
     }
 
+    /**
+     *  The initial (welcome to i2p) news
+     *
+     *  @return entry with first-installed date stamp, or null
+     *  @since 0.9.28
+     */
+    public NewsEntry getInitialNews() {
+        List<NewsEntry> list = parseInitialNews();
+        if (list.isEmpty())
+            return null;
+        NewsEntry rv = list.get(0);
+        long installed = _context.getProperty("router.firstInstalled", 0L);
+        if (installed > 0)
+            rv.updated = installed;
+        return rv;
+    }
+
     private List<NewsEntry> parseInitialNews() {
         NewsEntry entry = new NewsEntry();
         File file = new File(_context.getBaseDir(), "docs/initialNews/initialNews.xml");
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHandler.java
index 7e4c7c5f74be947ad2969dc75fe4e219719a2302..8a53fde1c2882c77ea858dc144c2515c51f93a90 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHandler.java
@@ -52,6 +52,7 @@ public class ConfigNetHandler extends FormHandler {
     private boolean _udpDisabled;
     private String _ipv6Mode;
     private boolean _ipv4Firewalled;
+    private boolean _ipv6Firewalled;
     private final Map<String, String> changes = new HashMap<String, String>();
     private static final String PROP_HIDDEN = Router.PROP_HIDDEN_HIDDEN; // see Router for other choice
     
@@ -87,6 +88,9 @@ public class ConfigNetHandler extends FormHandler {
 
     /** @since 0.9.20 */
     public void setIPv4Firewalled(String moo) { _ipv4Firewalled = true; }
+
+    /** @since 0.9.28 */
+    public void setIPv6Firewalled(String moo) { _ipv6Firewalled = true; }
     
     public void setHostname(String hostname) { 
         _hostname = (hostname != null ? hostname.trim() : null); 
@@ -366,6 +370,16 @@ public class ConfigNetHandler extends FormHandler {
             }
             changes.put(TransportUtil.PROP_IPV4_FIREWALLED, "" + _ipv4Firewalled);
 
+            if (Boolean.parseBoolean(_context.getProperty(TransportUtil.PROP_IPV6_FIREWALLED)) !=
+                _ipv6Firewalled) {
+                if (_ipv6Firewalled)
+                    addFormNotice(_t("Disabling inbound IPv6"));
+                else
+                    addFormNotice(_t("Enabling inbound IPv6"));
+                restartRequired = true;
+            }
+            changes.put(TransportUtil.PROP_IPV6_FIREWALLED, "" + _ipv6Firewalled);
+
             if (_context.getBooleanPropertyDefaultTrue(TransportManager.PROP_ENABLE_UDP) !=
                 !_udpDisabled) {
                 if (_udpDisabled)
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java
index 76b1369888d1801787eeb2805758af654199d91e..8222a057230d26b65f5e47368df07aeba4d5bca3 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java
@@ -90,6 +90,11 @@ public class ConfigNetHelper extends HelperBase {
         return getChecked(TransportUtil.PROP_IPV4_FIREWALLED);
     }
 
+    /** @since 0.9.28 */
+    public String getIPv6FirewalledChecked() {
+        return getChecked(TransportUtil.PROP_IPV6_FIREWALLED);
+    }
+
     public String getTcpAutoPortChecked(int mode) {
         String port = _context.getProperty(PROP_I2NP_NTCP_PORT); 
         boolean specified = port != null && port.length() > 0;
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigStatsHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigStatsHandler.java
index 86e63a65f6de613a58f6acdd3f9661e2467dbaeb..b218a18f26af8923f79bb44d6431e7131b3e10f2 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigStatsHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigStatsHandler.java
@@ -31,7 +31,9 @@ public class ConfigStatsHandler extends FormHandler {
     
     @Override
     protected void processForm() {
-        saveChanges();
+        if (_action != null && _action.equals("foo")) {
+            saveChanges();
+        }
     }
     
     public void setFilename(String filename) {
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/HomeHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/HomeHelper.java
index c625ac19ec8322078f32097d1c71058e28619f7d..1a61bac550ef72cbc569af183b6c10ea8f06ccbb 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/HomeHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/HomeHelper.java
@@ -50,9 +50,9 @@ public class HomeHelper extends HelperBase {
         _x("FAQ") + S + _x("Frequently Asked Questions") + S + "http://i2p-projekt.i2p/faq" + S + I + "question.png" + S +
         _x("Forum") + S + _x("Community forum") + S + "http://forum.i2p/" + S + I + "group.png" + S +
         _x("Anonymous Git Hosting") + S + _x("A public anonymous Git hosting site - supports pulling via Git and HTTP and pushing via SSH") + S + "http://git.repo.i2p/" + S + I + "git-logo.png" + S +
-        "hiddengate.i2p" + S + _x("HiddenGate") + S + "http://hiddengate.i2p/" + S + I + "hglogo32.png" + S +
+        //"hiddengate.i2p" + S + _x("HiddenGate") + S + "http://hiddengate.i2p/" + S + I + "hglogo32.png" + S +
         _x("I2P Wiki") + S + _x("Anonymous wiki - share the knowledge") + S + "http://i2pwiki.i2p/" + S + I + "i2pwiki_logo.png" + S +
-        "Ident " + _x("Microblog") + S + _x("Your premier microblogging service on I2P") + S + "http://id3nt.i2p/" + S + I + "ident_icon_blue.png" + S +
+        //"Ident " + _x("Microblog") + S + _x("Your premier microblogging service on I2P") + S + "http://id3nt.i2p/" + S + I + "ident_icon_blue.png" + S +
         //_x("Javadocs") + S + _x("Technical documentation") + S + "http://i2p-javadocs.i2p/" + S + I + "education.png" + S +
         //"jisko.i2p" + S + _x("Simple and fast microblogging website") + S + "http://jisko.i2p/" + S + I + "jisko_console_icon.png" + S +
         //_x("Key Server") + S + _x("OpenPGP Keyserver") + S + "http://keys.i2p/" + S + I + "education.png" + S +
@@ -63,14 +63,14 @@ public class HomeHelper extends HelperBase {
         _x("Plugins") + S + _x("Add-on directory") + S + "http://i2pwiki.i2p/index.php?title=Plugins" + S + I + "plugin.png" + S +
         _x("Postman's Tracker") + S + _x("Bittorrent tracker") + S + "http://tracker2.postman.i2p/" + S + I + "magnet.png" + S +
         _x("Project Website") + S + _x("I2P home page") + S + "http://i2p-projekt.i2p/" + S + I + "info_rhombus.png" + S +
-        _x("Russian News Feed") + S + "lenta.i2p" + S + "http://lenta.i2p/" + S + I + "lenta_main_logo.png" + S +
+        //_x("Russian News Feed") + S + "lenta.i2p" + S + "http://lenta.i2p/" + S + I + "lenta_main_logo.png" + S +
         //"Salt" + S + "salt.i2p" + S + "http://salt.i2p/" + S + I + "salt_console.png" + S +
         "stats.i2p" + S + _x("I2P Network Statistics") + S + "http://stats.i2p/cgi-bin/dashboard.cgi" + S + I + "chart_line.png" + S +
         _x("Technical Docs") + S + _x("Technical documentation") + S + "http://i2p-projekt.i2p/how" + S + I + "education.png" + S +
         _x("The Tin Hat") + S + _x("Privacy guides and tutorials") + S + "http://secure.thetinhat.i2p/" + S + I + "thetinhat.png" + S +
         _x("Trac Wiki") + S + S + "http://trac.i2p2.i2p/" + S + I + "billiard_marker.png" + S +
         //_x("Ugha's Wiki") + S + S + "http://ugha.i2p/" + S + I + "billiard_marker.png" + S +
-        _x("Sponge's main site") + S + _x("Seedless and the Robert BitTorrent applications") + S + "http://sponge.i2p/" + S + I + "user_astronaut.png" + S +
+        //_x("Sponge's main site") + S + _x("Seedless and the Robert BitTorrent applications") + S + "http://sponge.i2p/" + S + I + "user_astronaut.png" + S +
         "";
 
 
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java
index ba977a6e8005b78a1fd78bfd747bb47119be3117..313018db9561615c0f83385fb4fed4140e52cbd5 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java
@@ -7,6 +7,7 @@ public class NetDbHelper extends HelperBase {
     private String _routerPrefix;
     private String _version;
     private String _country;
+    private String _family;
     private int _full;
     private boolean _lease;
     private boolean _debug;
@@ -49,6 +50,12 @@ public class NetDbHelper extends HelperBase {
             _country = DataHelper.stripHTML(c);  // XSS
     }
 
+    /** @since 0.9.28 */
+    public void setFamily(String c) {
+        if (c != null)
+            _family = DataHelper.stripHTML(c);  // XSS
+    }
+
     public void setFull(String f) {
         try {
             _full = Integer.parseInt(f);
@@ -75,8 +82,8 @@ public class NetDbHelper extends HelperBase {
         NetDbRenderer renderer = new NetDbRenderer(_context);
         try {
             renderNavBar();
-            if (_routerPrefix != null || _version != null || _country != null)
-                renderer.renderRouterInfoHTML(_out, _routerPrefix, _version, _country);
+            if (_routerPrefix != null || _version != null || _country != null || _family != null)
+                renderer.renderRouterInfoHTML(_out, _routerPrefix, _version, _country, _family);
             else if (_lease)
                 renderer.renderLeaseSetHTML(_out, _debug);
             else if (_full == 3)
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java
index c9a34ece06d5e9f7764d32e6b9b95b0a3fc84fef..05ef0c1ac443ab5198b4ba5e6a55273ee6dd2df5 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java
@@ -85,8 +85,10 @@ class NetDbRenderer {
      *  @param routerPrefix may be null. "." for our router only
      *  @param version may be null
      *  @param country may be null
+     *  @param family may be null
      */
-    public void renderRouterInfoHTML(Writer out, String routerPrefix, String version, String country) throws IOException {
+    public void renderRouterInfoHTML(Writer out, String routerPrefix, String version,
+                                     String country, String family) throws IOException {
         StringBuilder buf = new StringBuilder(4*1024);
         if (".".equals(routerPrefix)) {
             renderRouterInfo(buf, _context.router().getRouterInfo(), true, true);
@@ -97,7 +99,8 @@ class NetDbRenderer {
                 Hash key = ri.getIdentity().getHash();
                 if ((routerPrefix != null && key.toBase64().startsWith(routerPrefix)) ||
                     (version != null && version.equals(ri.getVersion())) ||
-                    (country != null && country.equals(_context.commSystem().getCountry(key)))) {
+                    (country != null && country.equals(_context.commSystem().getCountry(key))) ||
+                    (family != null && family.equals(ri.getOption("family")))) {
                     renderRouterInfo(buf, ri, false, true);
                     notFound = false;
                 }
@@ -110,6 +113,8 @@ class NetDbRenderer {
                     buf.append(version);
                 else if (country != null)
                     buf.append(country);
+                else if (family != null)
+                    buf.append(_t("Family")).append(' ').append(family);
                 buf.append(' ').append(_t("not found in network database"));
             }
         }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NewsFeedHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/NewsFeedHelper.java
index 5eaa3e3625ce662be63c37f1fd2226d9f8a8bbbb..a40e3061a61674aa1af7166c0229ed5b4ce70ce7 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/NewsFeedHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/NewsFeedHelper.java
@@ -50,8 +50,18 @@ public class NewsFeedHelper extends HelperBase {
         ClientAppManager cmgr = ctx.clientAppManager();
         if (cmgr != null) {
             NewsManager nmgr = (NewsManager) cmgr.getRegisteredApp(NewsManager.APP_NAME);
-            if (nmgr != null)
+            if (nmgr != null) {
                 entries = nmgr.getEntries();
+                NewsEntry init = nmgr.getInitialNews();
+                if (init != null) {
+                    // crude check to see if it's already in there
+                    if (entries.size() != 1 || !DataHelper.eq(entries.get(0).title, init.title))
+                        if (entries.isEmpty())
+                            entries = Collections.singletonList(init);  // in case it was an emtpyList
+                        else
+                            entries.add(init);
+                }
+            }
         }
         if (!entries.isEmpty()) {
             DateFormat fmt = DateFormat.getDateInstance(DateFormat.SHORT);
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
index ae9cb0eb72a684d90eb11fe88dcb1e818bee1a52..7b0414d2e644ed03a2e7268d5d99f9ba555a186a 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
@@ -48,6 +48,7 @@ import org.eclipse.jetty.server.AbstractConnector;
 import org.eclipse.jetty.server.Connector;
 import org.eclipse.jetty.server.NCSARequestLog;
 import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.UserIdentity;
 import org.eclipse.jetty.server.bio.SocketConnector;
 import org.eclipse.jetty.server.handler.ContextHandlerCollection;
 import org.eclipse.jetty.server.handler.DefaultHandler;
@@ -848,7 +849,8 @@ public class RouterConsoleRunner implements RouterApp {
                 enable = false;
                 ctx.router().saveConfig(PROP_CONSOLE_PW, "false");
             } else {
-                HashLoginService realm = new HashLoginService(JETTY_REALM);
+                HashLoginService realm = new CustomHashLoginService(JETTY_REALM, context.getContextPath(),
+                                                                    ctx.logManager().getLog(RouterConsoleRunner.class));
                 sec.setLoginService(realm);
                 sec.setAuthenticator(authenticator);
                 String[] role = new String[] {JETTY_ROLE};
@@ -932,6 +934,30 @@ public class RouterConsoleRunner implements RouterApp {
         context.setSecurityHandler(sec);
     }
     
+    /**
+     * For logging authentication failures
+     * @since 0.9.28
+     */
+    private static class CustomHashLoginService extends HashLoginService {
+        private final String _webapp;
+        private final net.i2p.util.Log _log;
+
+        public CustomHashLoginService(String realm, String webapp, net.i2p.util.Log log) {
+            super(realm);
+            _webapp = webapp;
+            _log = log;
+        }
+
+        @Override
+        public UserIdentity login(String username, Object credentials) {
+            UserIdentity rv = super.login(username, credentials);
+            if (rv == null)
+                //_log.logAlways(net.i2p.util.Log.WARN, "Console authentication failed, webapp: " + _webapp + ", user: " + username);
+                _log.logAlways(net.i2p.util.Log.WARN, "Console authentication failed, user: " + username);
+            return rv;
+        }
+    }
+    
     /** @since 0.8.8 */
     private class ServerShutdown implements Runnable {
         public void run() {
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SybilRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/SybilRenderer.java
index 98671a146fe49055105c8f47db120377aa24d577..703e93958a3cf0c82a14564138b010a8626e24b1 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/SybilRenderer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/SybilRenderer.java
@@ -4,6 +4,7 @@ import java.io.IOException;
 import java.io.Serializable;
 import java.io.Writer;
 import java.math.BigInteger;
+import java.text.Collator;
 import java.text.DecimalFormat;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -24,6 +25,7 @@ import net.i2p.data.router.RouterInfo;
 import net.i2p.data.router.RouterKeyGenerator;
 import net.i2p.router.RouterContext;
 import net.i2p.router.TunnelPoolSettings;
+import net.i2p.router.crypto.FamilyKeyCrypto;
 import net.i2p.router.peermanager.DBHistory;
 import net.i2p.router.peermanager.PeerProfile;
 import net.i2p.router.tunnel.pool.TunnelPool;
@@ -61,6 +63,8 @@ class SybilRenderer {
     private static final double POINTS_US24 = 25.0;
     private static final double POINTS_US16 = 10.0;
     private static final double POINTS_FAMILY = -2.0;
+    private static final double POINTS_BAD_OUR_FAMILY = 100.0;
+    private static final double POINTS_OUR_FAMILY = -100.0;
     private static final double MIN_CLOSE = 242.0;
     private static final double PAIR_DISTANCE_FACTOR = 2.0;
     private static final double OUR_KEY_FACTOR = 4.0;
@@ -370,6 +374,7 @@ class SybilRenderer {
 
     private static class FoofComparator implements Comparator<String>, Serializable {
          private final ObjectCounter<String> _o;
+         private final Collator _comp = Collator.getInstance();
          public FoofComparator(ObjectCounter<String> o) { _o = o;}
          public int compare(String l, String r) {
              // reverse by count
@@ -377,7 +382,7 @@ class SybilRenderer {
              if (rv != 0)
                  return rv;
              // foward by name
-             return l.compareTo(r);
+             return _comp.compare(l, r);
         }
     }
 
@@ -578,11 +583,14 @@ class SybilRenderer {
         }
         List<String> foo = new ArrayList<String>(oc.objects());
         Collections.sort(foo, new FoofComparator(oc));
+        FamilyKeyCrypto fkc = _context.router().getFamilyKeyCrypto();
+        String ourFamily = fkc != null ? fkc.getOurFamilyName() : null;
         boolean found = false;
         for (String s : foo) {
             int count = oc.count(s);
-            buf.append("<p><b>").append(count).append(" floodfills in declared family \"").append(DataHelper.escapeHTML(s) + '"')
-               .append("</b></p>");
+            String ss = DataHelper.escapeHTML(s);
+            buf.append("<p><b>").append(count).append(" floodfills in declared family \"<a href=\"/netdb?fam=")
+               .append(ss).append("\">").append(ss).append("</a>\"</b></p>");
             for (RouterInfo info : ris) {
                 String fam = info.getOption("family");
                 if (fam == null)
@@ -593,10 +601,16 @@ class SybilRenderer {
                 // limit display
                 //renderRouterInfo(buf, info, null, false, false);
                 double point = POINTS_FAMILY;
-                if (count > 1)
+                if (fkc != null && s.equals(ourFamily)) {
+                    if (fkc.verifyOurFamily(info))
+                        addPoints(points, info.getHash(), POINTS_OUR_FAMILY, "Our family \"" + DataHelper.escapeHTML(s) + "\" with " + (count - 1) + " other" + (( count > 2) ? "s" : ""));
+                    else
+                        addPoints(points, info.getHash(), POINTS_BAD_OUR_FAMILY, "Spoofed our family \"" + DataHelper.escapeHTML(s) + "\" with " + (count - 1) + " other" + (( count > 2) ? "s" : ""));
+                } else if (count > 1) {
                     addPoints(points, info.getHash(), point, "Same declared family \"" + DataHelper.escapeHTML(s) + "\" with " + (count - 1) + " other" + (( count > 2) ? "s" : ""));
-                else
+                } else {
                     addPoints(points, info.getHash(), point, "Declared family \"" + DataHelper.escapeHTML(s) + '"');
+                }
             }
         }
         if (!found)
diff --git a/apps/routerconsole/jsp/confignet.jsp b/apps/routerconsole/jsp/confignet.jsp
index 7b81518e34fc264f88b425b5d648b844fcbf9f9a..4551668637608408ebacf477dcc74a45f874e4b8 100644
--- a/apps/routerconsole/jsp/confignet.jsp
+++ b/apps/routerconsole/jsp/confignet.jsp
@@ -56,6 +56,8 @@
     <%=intl._t("Disable inbound (Firewalled by Carrier-grade NAT or DS-Lite)")%>
  </p><p>
  <%=intl._t("IPv6 Configuration")%>:<br>
+    <input type="checkbox" class="optbox" name="IPv6Firewalled" value="true" <jsp:getProperty name="nethelper" property="IPv6FirewalledChecked" /> >
+    <%=intl._t("Disable inbound (Firewalled by Carrier-grade NAT or DS-Lite)")%><br>
     <input type="radio" class="optbox" name="ipv6" value="false" <%=nethelper.getIPv6Checked("false") %> >
     <%=intl._t("Disable IPv6")%><br>
     <input type="radio" class="optbox" name="ipv6" value="enable" <%=nethelper.getIPv6Checked("enable") %> >
diff --git a/apps/routerconsole/jsp/netdb.jsp b/apps/routerconsole/jsp/netdb.jsp
index 1b9be5d03fef789276c176b2f1a6476fa421036c..c323aa760b4e8fb9c3d5f90935f4f6d4ef42252e 100644
--- a/apps/routerconsole/jsp/netdb.jsp
+++ b/apps/routerconsole/jsp/netdb.jsp
@@ -25,5 +25,6 @@
  <jsp:setProperty name="netdbHelper" property="lease" value="<%=request.getParameter(\"l\")%>" />
  <jsp:setProperty name="netdbHelper" property="version" value="<%=request.getParameter(\"v\")%>" />
  <jsp:setProperty name="netdbHelper" property="country" value="<%=request.getParameter(\"c\")%>" />
+ <jsp:setProperty name="netdbHelper" property="family" value="<%=request.getParameter(\"fam\")%>" />
  <jsp:getProperty name="netdbHelper" property="netDbSummary" />
 </div></div></body></html>
diff --git a/apps/sam/java/build.xml b/apps/sam/java/build.xml
index 9aab0527c6d07fdb4a400b525ac0751dc28a0862..5891bd1a84c57ebaf816cacc7f694c4b25edd2bf 100644
--- a/apps/sam/java/build.xml
+++ b/apps/sam/java/build.xml
@@ -77,6 +77,8 @@
                 <attribute name="Build-Date" value="${build.timestamp}" />
                 <attribute name="Base-Revision" value="${workspace.version}" />
                 <attribute name="Workspace-Changes" value="${workspace.changes.tr}" />
+                <attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+                <attribute name="X-Compile-Target-JDK" value="${javac.version}" />
             </manifest>
         </jar>
     </target>
diff --git a/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java b/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java
index 54e9c56b56446e853ad1917bd2e6cc29746cac6b..c6ed6243b64992544ea24940e6b8d41eba6d5bcb 100644
--- a/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java
+++ b/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java
@@ -91,14 +91,23 @@ class SAMHandlerFactory {
         if (Boolean.parseBoolean(i2cpProps.getProperty(SAMBridge.PROP_AUTH))) {
             String user = props.getProperty("USER");
             String pw = props.getProperty("PASSWORD");
-            if (user == null || pw == null)
+            if (user == null || pw == null) {
+                if (user == null)
+                    log.logAlways(Log.WARN, "SAM authentication failed");
+                else
+                    log.logAlways(Log.WARN, "SAM authentication failed, user: " + user);
                 throw new SAMException("USER and PASSWORD required");
+            }
             String savedPW = i2cpProps.getProperty(SAMBridge.PROP_PW_PREFIX + user + SAMBridge.PROP_PW_SUFFIX);
-            if (savedPW == null)
+            if (savedPW == null) {
+                log.logAlways(Log.WARN, "SAM authentication failed, user: " + user);
                 throw new SAMException("Authorization failed");
+            }
             PasswordManager pm = new PasswordManager(I2PAppContext.getGlobalContext());
-            if (!pm.checkHash(savedPW, pw))
+            if (!pm.checkHash(savedPW, pw)) {
+                log.logAlways(Log.WARN, "SAM authentication failed, user: " + user);
                 throw new SAMException("Authorization failed");
+            }
         }
 
         // Let's answer positively
diff --git a/apps/streaming/java/build.xml b/apps/streaming/java/build.xml
index 0c2b80d71e5e85e6d187d9ec0fde360b82945ca4..1b421e2f52e94c618633c4aea385423f370fa0c9 100644
--- a/apps/streaming/java/build.xml
+++ b/apps/streaming/java/build.xml
@@ -213,6 +213,8 @@
                 <attribute name="Build-Date" value="${build.timestamp}" />
                 <attribute name="Base-Revision" value="${workspace.version}" />
                 <attribute name="Workspace-Changes" value="${workspace.changes.tr}" />
+                <attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+                <attribute name="X-Compile-Target-JDK" value="${javac.version}" />
             </manifest>
         </jar>
     </target>
diff --git a/apps/susidns/src/build.xml b/apps/susidns/src/build.xml
index eefdacd5950b6c89d145b038ed541c6f011f17d1..ac9a6f63aa3e0e43ecefdfa783d2406fcf82c3c1 100644
--- a/apps/susidns/src/build.xml
+++ b/apps/susidns/src/build.xml
@@ -117,6 +117,8 @@
                 <attribute name="Build-Date" value="${build.timestamp}" />
                 <attribute name="Base-Revision" value="${workspace.version}" />
                 <attribute name="Workspace-Changes" value="${workspace.changes.tr}" />
+                <attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+                <attribute name="X-Compile-Target-JDK" value="${javac.version}" />
             </manifest>
         </war>
     </target>
diff --git a/apps/susimail/build.xml b/apps/susimail/build.xml
index b3ede69b37f2c70aaaac3a2975a8666f1683576f..1df5e1129bde691ce68eee4f87629b166839cac4 100644
--- a/apps/susimail/build.xml
+++ b/apps/susimail/build.xml
@@ -81,6 +81,8 @@
                 <attribute name="Build-Date" value="${build.timestamp}" />
                 <attribute name="Base-Revision" value="${workspace.version}" />
                 <attribute name="Workspace-Changes" value="${workspace.changes.tr}" />
+                <attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+                <attribute name="X-Compile-Target-JDK" value="${javac.version}" />
             </manifest>
         </war>
     </target>
diff --git a/apps/susimail/src/src/i2p/susi/webmail/WebMail.java b/apps/susimail/src/src/i2p/susi/webmail/WebMail.java
index edf94936fa2775af8d09ce995743c893a1cd7d12..4da8c680ea83c89572681d6363715d0d6ecb093e 100644
--- a/apps/susimail/src/src/i2p/susi/webmail/WebMail.java
+++ b/apps/susimail/src/src/i2p/susi/webmail/WebMail.java
@@ -87,6 +87,7 @@ public class WebMail extends HttpServlet
 	private static final int version = 13;
 	
 	private static final long serialVersionUID = 1L;
+	private static final String LOGIN_NONCE = Long.toString(I2PAppContext.getGlobalContext().random().nextLong());
 	
 	private static final String DEFAULT_HOST = "127.0.0.1";
 	private static final int DEFAULT_POP3PORT = 7660;
@@ -466,6 +467,8 @@ public class WebMail extends HttpServlet
 
 		/** @since 0.9.27 */
 		public boolean isValidNonce(String nonce) {
+			if (state == STATE_AUTH && LOGIN_NONCE.equals(nonce))
+				return true;
 			synchronized(nonces) {
 				return nonces.contains(nonce);
 			}
@@ -856,7 +859,7 @@ public class WebMail extends HttpServlet
 			}
 			sessionObject.info += _t("User logged out.") + '\n';
 			sessionObject.state = STATE_AUTH;
-		} else if( sessionObject.mailbox == null ) {
+		} else if( sessionObject.mailbox == null  && !buttonPressed(request, CANCEL)) {
 			sessionObject.error += _t("Internal error, lost connection.") + '\n';
 			sessionObject.state = STATE_AUTH;
 		}
@@ -878,7 +881,7 @@ public class WebMail extends HttpServlet
 		if( sessionObject.state == STATE_AUTH && isPOST )
 			processLogin( sessionObject, request );
 
-		if( sessionObject.state != STATE_AUTH && sessionObject.state != STATE_CONFIG )
+		if( sessionObject.state != STATE_AUTH )
 			processLogout( sessionObject, request, isPOST );
 
 		/*
@@ -1797,7 +1800,8 @@ public class WebMail extends HttpServlet
 					out.println("<script src=\"/susimail/js/folder.js\" type=\"text/javascript\"></script>");
 				}
 				out.print("</head>\n<body" + (sessionObject.state == STATE_LIST ? " onload=\"deleteboxclicked()\">" : ">"));
-				String nonce = Long.toString(ctx.random().nextLong());
+				String nonce = sessionObject.state == STATE_AUTH ? LOGIN_NONCE :
+				                                                   Long.toString(ctx.random().nextLong());
 				sessionObject.addNonce(nonce);
 				out.println(
 					"<div class=\"page\"><div class=\"header\"><img class=\"header\" src=\"" + sessionObject.imgPath + "susimail.png\" alt=\"Susimail\"></div>\n" +
@@ -2409,6 +2413,7 @@ public class WebMail extends HttpServlet
 			out.println(button2(DELETE, _t("Delete")));
 		else
 			out.println(button(DELETE, _t("Delete")));
+		out.println(spacer + button(LOGOUT, _t("Logout") ));
 		out.println("<br>" +
 			( sessionObject.folder.isFirstElement( sessionObject.showUIDL ) ? button2( PREV, _t("Previous") ) : button( PREV, _t("Previous") ) ) + spacer +
 			button( LIST, _t("Back to Folder") ) + spacer +
@@ -2416,7 +2421,6 @@ public class WebMail extends HttpServlet
 		out.println("</div>");
 		//if (Config.hasConfigFile())
 		//	out.println(button( RELOAD, _t("Reload Config") ) + spacer);
-		//out.println(button( LOGOUT, _t("Logout") ) );
 		if( mail != null ) {
 			out.println( "<table cellspacing=\"0\" cellpadding=\"5\">\n" +
 					"<tr><td colspan=\"2\" align=\"center\"><hr></td></tr>\n" +
@@ -2473,6 +2477,8 @@ public class WebMail extends HttpServlet
 		out.println("<br>");
 		out.println(button(SAVE, _t("Save Configuration")));
 		out.println(button(CANCEL, _t("Cancel")));
+		if (sessionObject.folder != null)
+			out.println(spacer + button(LOGOUT, _t("Logout") ));
 		out.println("</div>");
 	}
 
diff --git a/apps/systray/java/build.xml b/apps/systray/java/build.xml
index 448c5ca8fcafa0bff60a7aa5972f6d2284f61f2b..8d00ba1f8b6edac68f8b9f7b124ae246086baaa7 100644
--- a/apps/systray/java/build.xml
+++ b/apps/systray/java/build.xml
@@ -65,6 +65,8 @@
                 <attribute name="Build-Date" value="${build.timestamp}" />
                 <attribute name="Base-Revision" value="${workspace.version}" />
                 <attribute name="Workspace-Changes" value="${workspace.changes.tr}" />
+                <attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+                <attribute name="X-Compile-Target-JDK" value="${javac.version}" />
             </manifest>
         </jar>
     </target>
diff --git a/build.xml b/build.xml
index 180976fd758e6e8dc9bf16cee26073dfe8ab97b8..750db1ad0aab257e9e9d9cf0c7ad2ca86ee189b1 100644
--- a/build.xml
+++ b/build.xml
@@ -400,7 +400,7 @@
         <copy file="core/java/build/i2p.jar" todir="build/" />
     </target>
 
-    <target name="buildJrobin" depends="buildProperties" >
+    <target name="buildJrobin" depends="buildCore" >
         <ant dir="apps/jrobin/java/" target="jar" />
         <copy file="apps/jrobin/java/build/jrobin.jar" todir="build/" />
     </target>
@@ -1184,7 +1184,7 @@
     </target>
 
     <!-- see targets below for conditional copying -->
-    <target name="preppkg-base" depends="build, preplicenses, prepConsoleDocs, prepthemeupdates, prepCertificates, prepRouterInfos, copyjetty, copytomcat-unlesspkg, copyjstl-unlesspkg, copystandard-unlesspkg">
+    <target name="preppkg-base" depends="build, preplicenses, prepConsoleDocs, prepthemeupdates, prepCertificates, prepRouterInfos, copyjetty, copytomcat-unlesspkg, copyjstl-unlesspkg, copystandard-unlesspkg, truncatehistory">
         <!-- if updater200 was run previously, it left *.pack files in pkg-temp -->
         <!-- Also remove deletelist.txt used for updater only -->
         <delete>
@@ -1218,8 +1218,6 @@
         <copy file="installer/resources/wrapper.config" todir="pkg-temp/" />
         <copy file="installer/resources/hosts.txt" todir="pkg-temp/" />
         <copy file="INSTALL-headless.txt" todir="pkg-temp/" />
-        <!-- overwrite the truncated history put in by the updater -->
-        <copy file="history.txt" todir="pkg-temp/" overwrite="true" />
         <mkdir dir="pkg-temp/scripts" />
         <copy file="apps/proxyscript/i2pProxy.pac" todir="pkg-temp/scripts/" />
         <copy file="apps/apparmor/home.i2p.i2prouter" todir="pkg-temp/scripts/" />
@@ -1506,7 +1504,7 @@
         </exec>
     </target>
 
-    <target name="prepupdate" depends="build2, prepupdateSmall, prepConsoleDocUpdates, prepCertificates, prep-script-translation">
+    <target name="prepupdate" depends="build2, prepupdateSmall, prepConsoleDocUpdates, prepCertificates, prep-script-translation, truncatehistory">
         <copy file="build/BOB.jar" todir="pkg-temp/lib/" />
         <copy file="build/sam.jar" todir="pkg-temp/lib/" />
         <copy file="build/i2psnark.jar" todir="pkg-temp/lib" />
@@ -1523,6 +1521,14 @@
         <copy file="apps/susidns/src/lib/standard.jar" todir="pkg-temp/lib/" />
        -->
         <copy file="build/i2psnark.war" todir="pkg-temp/webapps/" />
+        <copy file="installer/resources/deletelist.txt" todir="pkg-temp/" />
+        <copy file="installer/resources/blocklist.txt" todir="pkg-temp/" />
+        <copy todir="pkg-temp/man/">
+            <fileset dir="installer/resources/man/" />
+        </copy>
+    </target>
+
+    <target name="truncatehistory">
         <copy file="history.txt" todir="pkg-temp/" />
         <!-- the following overwrites history.txt on unix to shrink the update file -->
         <copy file="history.txt" tofile="pkg-temp/history.txt" overwrite="true">
@@ -1530,12 +1536,7 @@
                 <headfilter lines="1500" />
             </filterchain>
         </copy>
-        <concat append="true" destfile="pkg-temp/history.txt">&#10;&#10;----------------&#10;&#10;EARLIER HISTORY IS AVAILABLE IN THE SOURCE PACKAGE"</concat>
-        <copy file="installer/resources/deletelist.txt" todir="pkg-temp/" />
-        <copy file="installer/resources/blocklist.txt" todir="pkg-temp/" />
-        <copy todir="pkg-temp/man/">
-            <fileset dir="installer/resources/man/" />
-        </copy>
+        <concat append="true" destfile="pkg-temp/history.txt">&#10;&#10;----------------&#10;&#10;EARLIER HISTORY IS AVAILABLE IN THE SOURCE PACKAGE</concat>
     </target>
 
     <target name="prepupdateSmall" depends="buildSmall, prepupdateRouter, prepjupdatefixes, prepthemeupdates">
diff --git a/core/java/build.xml b/core/java/build.xml
index 3143f550c92979fd9e76c3cd6f0770f78bacc2d0..1f301ae591b7982b0dcf06ecc6bd1b7b33dce65e 100644
--- a/core/java/build.xml
+++ b/core/java/build.xml
@@ -108,6 +108,8 @@
                 <attribute name="Base-Revision" value="${workspace.version}" />
                 <attribute name="Main-Class" value="net.i2p.util.CommandLine" />
                 <attribute name="Workspace-Changes" value="${workspace.changes.tr}" />
+                <attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+                <attribute name="X-Compile-Target-JDK" value="${javac.version}" />
             </manifest>
         </jar>
     </target>
diff --git a/core/java/src/net/i2p/client/impl/RequestLeaseSetMessageHandler.java b/core/java/src/net/i2p/client/impl/RequestLeaseSetMessageHandler.java
index 09dab72ff429486a5456a615a47c64ce3ff58354..809492c4cca9d62d08265d04135909908e1aecc1 100644
--- a/core/java/src/net/i2p/client/impl/RequestLeaseSetMessageHandler.java
+++ b/core/java/src/net/i2p/client/impl/RequestLeaseSetMessageHandler.java
@@ -9,6 +9,7 @@ package net.i2p.client.impl;
  *
  */
 
+import java.io.EOFException;
 import java.security.GeneralSecurityException;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
@@ -185,7 +186,16 @@ class RequestLeaseSetMessageHandler extends HandlerImpl {
         } catch (DataFormatException dfe) {
             session.propogateError("Error signing the leaseSet", dfe);
         } catch (I2PSessionException ise) {
-            session.propogateError("Error sending the signed leaseSet", ise);
+            if (session.isClosed()) {
+                // race, closed while signing leaseset
+                // EOFExceptions are logged at WARN level (see I2PSessionImpl.propogateError())
+                // so the user won't see this
+                EOFException eof = new EOFException("Session closed while signing leaseset");
+                eof.initCause(ise);
+                session.propogateError("Session closed while signing leaseset", eof);
+            } else {
+                session.propogateError("Error sending the signed leaseSet", ise);
+            }
         }
     }
 
diff --git a/core/java/src/net/i2p/crypto/CertUtil.java b/core/java/src/net/i2p/crypto/CertUtil.java
index ed0c63ce16348c7ff5666014b69a3ec2e82937f0..be7d100c938b6c8b02cc5c34edc4f30eb9edbaf1 100644
--- a/core/java/src/net/i2p/crypto/CertUtil.java
+++ b/core/java/src/net/i2p/crypto/CertUtil.java
@@ -535,8 +535,6 @@ public final class CertUtil {
     }
 
 
-
-/****
     public static final void main(String[] args) {
         if (args.length < 2) {
             System.out.println("Usage: [loadcert | loadcrl | loadcrldir | loadcrldirs | isrevoked | loadprivatekey] file");
@@ -545,7 +543,8 @@ public final class CertUtil {
         try {
             File f = new File(args[1]);
             if (args[0].equals("loadcert")) {
-                loadCert(f);
+                X509Certificate cert = loadCert(f);
+                System.out.println(net.i2p.util.HexDump.dump(cert.getEncoded()));
             } else if (args[0].equals("loadcrl")) {
                 loadCRL(f);
             } else if (args[0].equals("loadcrldir")) {
@@ -569,5 +568,4 @@ public final class CertUtil {
             System.exit(1);
         }
     }
-****/
 }
diff --git a/core/java/src/net/i2p/crypto/SelfSignedGenerator.java b/core/java/src/net/i2p/crypto/SelfSignedGenerator.java
index 35a0a831aa60792848cd5454e532246d93dd8638..5f525b2d7a59bef19eec2ee18aa11fd4b867a3fd 100644
--- a/core/java/src/net/i2p/crypto/SelfSignedGenerator.java
+++ b/core/java/src/net/i2p/crypto/SelfSignedGenerator.java
@@ -264,11 +264,11 @@ public final class SelfSignedGenerator {
         // a0 ???, int = 2
         byte[] version = { (byte) 0xa0, 3, 2, 1, 2 };
 
-        // postive serial number (int)
-        byte[] serial = new byte[6];
+        // positive serial number (long)
+        byte[] serial = new byte[10];
         serial[0] = 2;
-        serial[1] = 4;
-        RandomSource.getInstance().nextBytes(serial, 2, 4);
+        serial[1] = 8;
+        RandomSource.getInstance().nextBytes(serial, 2, 8);
         serial[2] &= 0x7f;
 
         // going to use this for both issuer and subject
diff --git a/core/java/src/net/i2p/crypto/eddsa/math/Constants.java b/core/java/src/net/i2p/crypto/eddsa/math/Constants.java
index 885bee57ff6c0b49602783c792a36f766e9e51f4..3b67f240ca4afdd2783d665a1a47e027e32706f9 100644
--- a/core/java/src/net/i2p/crypto/eddsa/math/Constants.java
+++ b/core/java/src/net/i2p/crypto/eddsa/math/Constants.java
@@ -2,7 +2,7 @@ package net.i2p.crypto.eddsa.math;
 
 import net.i2p.crypto.eddsa.Utils;
 
-public class Constants {
+final class Constants {
     public static final byte[] ZERO = Utils.hexToBytes("0000000000000000000000000000000000000000000000000000000000000000");
     public static final byte[] ONE = Utils.hexToBytes("0100000000000000000000000000000000000000000000000000000000000000");
     public static final byte[] TWO = Utils.hexToBytes("0200000000000000000000000000000000000000000000000000000000000000");
diff --git a/core/java/src/net/i2p/util/Addresses.java b/core/java/src/net/i2p/util/Addresses.java
index 15352975bc8bf14fdac88f634f1dcdfe0bff478b..db28be6baa8169b88402ffb1dbf6f17fc502fa38 100644
--- a/core/java/src/net/i2p/util/Addresses.java
+++ b/core/java/src/net/i2p/util/Addresses.java
@@ -4,14 +4,21 @@ package net.i2p.util;
  * public domain
  */
 
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
 import java.net.InetAddress;
 import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.net.NetworkInterface;
 import java.net.SocketException;
 import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Enumeration;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -21,6 +28,8 @@ import java.util.TreeSet;
 import org.apache.http.conn.util.InetAddressUtils;
 
 import net.i2p.I2PAppContext;
+import net.i2p.data.DataHelper;
+
 
 /**
  * Methods to get the local addresses, and other IP utilities
@@ -30,6 +39,16 @@ import net.i2p.I2PAppContext;
  */
 public abstract class Addresses {
     
+    private static final File IF_INET6_FILE = new File("/proc/net/if_inet6");
+    private static final long INET6_CACHE_EXPIRE = 10*60*1000;
+    private static final boolean INET6_CACHE_ENABLED = !SystemVersion.isMac() && !SystemVersion.isWindows() &&
+                                                    !SystemVersion.isAndroid() && IF_INET6_FILE.exists();
+    private static final int FLAG_PERMANENT = 0x80;
+    private static final int FLAG_DEPRECATED = 0x20;
+    private static final int FLAG_TEMPORARY = 0x01;
+    private static long _ifCacheTime;
+    private static final Map<Inet6Address, Inet6Addr> _ifCache = INET6_CACHE_ENABLED ? new HashMap<Inet6Address, Inet6Addr>(8) : null;
+
     /**
      *  Do we have any non-loop, non-wildcard IPv4 address at all?
      *  @since 0.9.4
@@ -101,18 +120,25 @@ public abstract class Addresses {
         boolean haveIPv4 = false;
         boolean haveIPv6 = false;
         SortedSet<String> rv = new TreeSet<String>();
+        final boolean omitDeprecated = INET6_CACHE_ENABLED && !includeSiteLocal && includeIPv6;
         try {
             InetAddress localhost = InetAddress.getLocalHost();
             InetAddress[] allMyIps = InetAddress.getAllByName(localhost.getCanonicalHostName());
             if (allMyIps != null) {
                 for (int i = 0; i < allMyIps.length; i++) {
-                    if (allMyIps[i] instanceof Inet4Address)
+                    boolean isv4 = allMyIps[i] instanceof Inet4Address;
+                    if (isv4)
                         haveIPv4 = true;
                     else
                         haveIPv6 = true;
+                    if (omitDeprecated && !isv4) {
+                        if (isDeprecated((Inet6Address) allMyIps[i]))
+                            continue;
+                    }
                     if (shouldInclude(allMyIps[i], includeSiteLocal,
-                                      includeLoopbackAndWildcard, includeIPv6))
+                                      includeLoopbackAndWildcard, includeIPv6)) {
                         rv.add(stripScope(allMyIps[i].getHostAddress()));
+                    }
                 }
             }
         } catch (UnknownHostException e) {}
@@ -124,13 +150,19 @@ public abstract class Addresses {
                     NetworkInterface ifc = ifcs.nextElement();
                     for(Enumeration<InetAddress> addrs =  ifc.getInetAddresses(); addrs.hasMoreElements();) {
                         InetAddress addr = addrs.nextElement();
-                        if (addr instanceof Inet4Address)
+                        boolean isv4 = addr instanceof Inet4Address;
+                        if (isv4)
                             haveIPv4 = true;
                         else
                             haveIPv6 = true;
+                        if (omitDeprecated && !isv4) {
+                            if (isDeprecated((Inet6Address) addr))
+                                continue;
+                        }
                         if (shouldInclude(addr, includeSiteLocal,
-                                          includeLoopbackAndWildcard, includeIPv6))
+                                          includeLoopbackAndWildcard, includeIPv6)) {
                             rv.add(stripScope(addr.getHostAddress()));
+                        }
                     }
                 }
             }
@@ -333,8 +365,8 @@ public abstract class Addresses {
 
     /**
      *  For literal IP addresses, this is the same as getIP(String).
-     *  For host names, will return the preferred type (IPv4/v6) if available,
-     *  else the other type if available.
+     *  For host names, may return multiple addresses, both IPv4 and IPv6,
+     *  even if those addresses are not reachable due to configuration or available interfaces.
      *  Will resolve but not cache DNS host names.
      *
      *  Note that order of returned results, and whether
@@ -370,6 +402,136 @@ public abstract class Addresses {
         return null;
     }
 
+    //////// IPv6 Cache Utils ///////
+
+    /**
+     *  @since 0.9.28
+     */
+    private static class Inet6Addr {
+        private final Inet6Address addr;
+        private final boolean isDyn, isDep, isTemp;
+
+        public Inet6Addr(Inet6Address a, int flags) {
+            addr = a;
+            isDyn = (flags & FLAG_PERMANENT) == 0;
+            isDep = (flags & FLAG_DEPRECATED) != 0;
+            isTemp = (flags & FLAG_TEMPORARY) != 0;
+        }
+
+        public Inet6Address getAddress() { return addr; }
+        public boolean isDynamic() { return isDyn; }
+        public boolean isDeprecated() { return isDep; }
+        public boolean isTemporary() { return isTemp; }
+    }
+
+    /**
+     *  Only call if INET6_CACHE_ENABLED.
+     *  Caller must sync on _ifCache.
+     *  @since 0.9.28
+     */
+    private static void refreshCache() {
+        long now = System.currentTimeMillis();
+        if (now - _ifCacheTime < INET6_CACHE_EXPIRE)
+            return;
+        _ifCache.clear();
+        BufferedReader in = null;
+        try {
+            in = new BufferedReader(new InputStreamReader(new FileInputStream(IF_INET6_FILE), "ISO-8859-1"), 4096);
+            String line = null;
+            while ( (line = in.readLine()) != null) {
+                // http://tldp.org/HOWTO/html_single/Linux+IPv6-HOWTO/#PROC-NET
+                // 00000000000000000000000000000001 01 80 10 80       lo
+                String[] parts = DataHelper.split(line, " ", 6);
+                if (parts.length < 5)
+                    continue;
+                String as = parts[0];
+                if (as.length() != 32)
+                    continue;
+                StringBuilder buf = new StringBuilder(40);
+                int i = 0;
+                while(true) {
+                    buf.append(as.substring(i, i+4));
+                    i += 4;
+                    if (i >= 32)
+                        break;
+                    buf.append(':');
+                }
+                Inet6Address addr;
+                try {
+                    addr = (Inet6Address) InetAddress.getByName(buf.toString());
+                } catch (UnknownHostException uhe) {
+                    continue;
+                }
+                int flags = FLAG_PERMANENT;
+                try {
+                    flags = Integer.parseInt(parts[4], 16);
+                } catch (NumberFormatException nfe) {}
+                Inet6Addr a = new Inet6Addr(addr, flags);
+                _ifCache.put(addr, a);
+            }
+        } catch (IOException ioe) {
+        } finally {
+            if (in != null) try { in.close(); } catch (IOException ioe) {}
+        }
+        _ifCacheTime = now;
+    }
+
+    /**
+     *  Is this address dynamic?
+     *  Returns false if unknown.
+     *  @since 0.9.28
+     */
+    public static boolean isDynamic(Inet6Address addr) {
+        if (!INET6_CACHE_ENABLED)
+            return false;
+        Inet6Addr a;
+        synchronized(_ifCache) {
+            refreshCache();
+            a = _ifCache.get(addr);
+        }
+        if (a == null)
+            return false;
+        return a.isDynamic();
+    }
+
+    /**
+     *  Is this address deprecated?
+     *  Returns false if unknown.
+     *  @since 0.9.28
+     */
+    public static boolean isDeprecated(Inet6Address addr) {
+        if (!INET6_CACHE_ENABLED)
+            return false;
+        Inet6Addr a;
+        synchronized(_ifCache) {
+            refreshCache();
+            a = _ifCache.get(addr);
+        }
+        if (a == null)
+            return false;
+        return a.isDeprecated();
+    }
+
+    /**
+     *  Is this address temporary?
+     *  Returns false if unknown.
+     *  @since 0.9.28
+     */
+    public static boolean isTemporary(Inet6Address addr) {
+        if (!INET6_CACHE_ENABLED)
+            return false;
+        Inet6Addr a;
+        synchronized(_ifCache) {
+            refreshCache();
+            a = _ifCache.get(addr);
+        }
+        if (a == null)
+            return false;
+        return a.isTemporary();
+    }
+
+    //////// End IPv6 Cache Utils ///////
+
     /**
      *  @since 0.9.3
      */
@@ -377,32 +539,66 @@ public abstract class Addresses {
         synchronized(_IPAddress) {
             _IPAddress.clear();
         }
+        if (_ifCache != null) {
+            synchronized(_ifCache) {
+                _ifCache.clear();
+                _ifCacheTime = 0;
+            }
+        }
     }
 
     /**
      *  Print out the local addresses
      */
     public static void main(String[] args) {
-        System.err.println("External IPv4 Addresses:");
+        System.out.println("External IPv4 Addresses:");
         Set<String> a = getAddresses(false, false, false);
         for (String s : a)
-            System.err.println(s);
-        System.err.println("\nExternal and Local IPv4 Addresses:");
+            System.out.println(s);
+        System.out.println("\nExternal and Local IPv4 Addresses:");
         a = getAddresses(true, false, false);
         for (String s : a)
-            System.err.println(s);
-        System.err.println("\nAll External Addresses:");
+            System.out.println(s);
+        System.out.println("\nAll External Addresses:");
         a = getAddresses(false, false, true);
         for (String s : a)
-            System.err.println(s);
-        System.err.println("\nAll External and Local Addresses:");
+            System.out.println(s);
+        System.out.println("\nAll External and Local Addresses:");
         a = getAddresses(true, false, true);
         for (String s : a)
-            System.err.println(s);
-        System.err.println("\nAll addresses:");
+            System.out.println(s);
+        System.out.println("\nAll addresses:");
         a = getAddresses(true, true, true);
         for (String s : a)
-            System.err.println(s);
-        System.err.println("\nIs connected? " + isConnected());
+            System.out.println(s);
+        System.out.println("\nIPv6 address flags:");
+        for (String s : a) {
+            if (!s.contains(":"))
+                continue;
+            StringBuilder buf = new StringBuilder(64);
+            buf.append(s);
+            Inet6Address addr;
+            try {
+                addr = (Inet6Address) InetAddress.getByName(buf.toString());
+                if (addr.isSiteLocalAddress())
+                    buf.append(" host");
+                else if (addr.isLinkLocalAddress())
+                    buf.append(" link");
+                else if (addr.isAnyLocalAddress())
+                    buf.append(" wildcard");
+                else if (addr.isLoopbackAddress())
+                    buf.append(" loopback");
+                else
+                    buf.append(" global");
+                if (isTemporary(addr))
+                    buf.append(" temporary");
+                if (isDeprecated(addr))
+                    buf.append(" deprecated");
+                if (isDynamic(addr))
+                    buf.append(" dynamic");
+            } catch (UnknownHostException uhe) {}
+            System.out.println(buf.toString());
+        }
+        System.out.println("\nIs connected? " + isConnected());
     }
 }
diff --git a/core/java/src/net/i2p/util/CommandLine.java b/core/java/src/net/i2p/util/CommandLine.java
index f2d57f5346f74d33b0400a4d3748fa4dc9bbd3f2..bfd1ca231a246b66a45d66c7f90bc2ef574008e9 100644
--- a/core/java/src/net/i2p/util/CommandLine.java
+++ b/core/java/src/net/i2p/util/CommandLine.java
@@ -22,6 +22,7 @@ public class CommandLine {
         "freenet.support.CPUInformation.CPUID",
         "net.i2p.CoreVersion",
         "net.i2p.client.naming.BlockfileNamingService",
+        "net.i2p.crypto.CertUtil",
         "net.i2p.crypto.CryptoCheck",
         "net.i2p.crypto.SU3File",
         "net.i2p.crypto.TrustedUpdate",
diff --git a/core/java/src/net/i2p/util/LogWriter.java b/core/java/src/net/i2p/util/LogWriter.java
index 1b585fb3868ad8950e8add394e3767f5bc065db1..38830841b6c4b6618277e86b2b320a676d7b5835 100644
--- a/core/java/src/net/i2p/util/LogWriter.java
+++ b/core/java/src/net/i2p/util/LogWriter.java
@@ -29,6 +29,7 @@ abstract class LogWriter implements Runnable {
 
     protected volatile boolean _write;
     private LogRecord _last;
+    private long _firstTimestamp;
     // ms
     private volatile long _flushInterval = FLUSH_INTERVAL;
 
@@ -95,7 +96,7 @@ abstract class LogWriter implements Runnable {
             Queue<LogRecord> records = _manager.getQueue();
             if (records == null) return;
             if (!records.isEmpty()) {
-                if (_last != null && _last.getDate() < _manager.getContext().clock().now() - 30*60*1000)
+                if (_last != null && _firstTimestamp < _manager.getContext().clock().now() - 30*60*1000)
                     _last = null;
                 LogRecord rec;
                 int dupCount = 0;
@@ -108,6 +109,7 @@ abstract class LogWriter implements Runnable {
                             dupCount = 0;
                         }
                         writeRecord(rec);
+                        _firstTimestamp = rec.getDate();
                     }
                     _last = rec;
                 }
diff --git a/core/java/src/net/i2p/util/SystemVersion.java b/core/java/src/net/i2p/util/SystemVersion.java
index aaa00da9b5d5818876fe223be11913ab34364780..bd159fb1ab991f18b7cc9e35d0287b5ef3d7aecc 100644
--- a/core/java/src/net/i2p/util/SystemVersion.java
+++ b/core/java/src/net/i2p/util/SystemVersion.java
@@ -34,8 +34,7 @@ public abstract class SystemVersion {
     private static final boolean _isOpenJDK;
     private static final boolean _is64;
     private static final boolean _hasWrapper = System.getProperty("wrapper.version") != null;
-    private static final boolean _isLinuxService = !_isWin && !_isMac &&
-                                                   DAEMON_USER.equals(System.getProperty("user.name"));
+    private static final boolean _isLinuxService;
 
     private static final boolean _oneDotSix;
     private static final boolean _oneDotSeven;
@@ -63,6 +62,8 @@ public abstract class SystemVersion {
                  vendor.startsWith("Free Software Foundation");      // gij
         String runtime = System.getProperty("java.runtime.name");
         _isOpenJDK = runtime != null && runtime.contains("OpenJDK");
+        _isLinuxService = !_isWin && !_isMac && !_isAndroid &&
+                          DAEMON_USER.equals(System.getProperty("user.name"));
 
         int sdk = 0;
         if (_isAndroid) {
diff --git a/history.txt b/history.txt
index 0c3a302a71ecec89e847d5898de2df05b5089edf..77bf74c33c1258d60579e325553805880af7c75e 100644
--- a/history.txt
+++ b/history.txt
@@ -1,3 +1,69 @@
+2016-11-16 zzz
+ * Console: Remove dead home page links (ticket #1882)
+ * Profiles: Pull same-IP detection into a utility class
+ * Router: Add methods to verify and track members of our family
+
+2016-11-15 zzz
+ * Certs: Add Let's Encrypt ISRG Root X1 cert
+
+2016-11-14 zzz
+ * Logs: Fix output of dup message after 30 minutes
+
+2016-11-13 zzz
+ * Console: Add initial news to bottom of news page (ticket #1153)
+ * i2psnark: Periodically DHT nodes (ticket #1328)
+ * UPnP:
+   - Prevent exception on bad HTTP header (ticket #1480)
+   - Prevent NPE on socket creation fail (tickets #728, #1681)
+
+2016-11-12 zzz
+ * Console:
+   - Fix inadvertent config save when clicking sidebar
+     buttons on /configstats
+   - Add IPv6 firewalled setting on /confignet
+ * I2CP: Reduce error level on session closed while signing LS (ticket #1606)
+ * JRobin: Move DeallocationHelper logging from wrapper log to router log
+ * Profiles: Periodically save, delete old ones after saving (ticket #1328)
+ * Susimail:
+   - Add logout button to more pages (ticket #1374)
+   - Fix nonce error on login after logout
+   - Fix internal error after cancel button on settings form when not logged in
+
+2016-11-11 zzz
+ * Build: Truncate history.txt bundled in installers
+
+2016-11-10 zzz
+ * Transport: Use NTCP for some outbound connections even before
+   SSU minimums are met (ticket #1835)
+
+2016-11-09 zzz
+ * Transport: Add stats for inbound v4/v6 connections (ticket #1854)
+ * Tunnels: Reduce default VTBM records from 5 to 4
+
+2016-11-08 zzz
+ * Build: Fix minimum Java version for Windows
+ * Install: Add max memory option to runplain.sh
+ * Crypto: Change serial number in selfsigned certs from int to long
+ * Router: Fix low-memory log messages for non-wrapper (ticket #1795)
+ * Transport: Improve IPv6 selection logic
+
+2016-11-06 zzz
+ * Console: Add Java 9 log warning (ticket #1870)
+ * Security: Consistently log authentication failures for all interfaces
+ * Util: Consolidate linux service detection code
+
+2016-11-05 zzz
+ * Build: Add support for using libtomcat8-java package
+ * Console: Add message to ignore InstanceManager warning (ticket #1818)
+ * SusiDNS: Fix jsp EL syntax error with EL 3.0 (Tomcat 8) (ticket #1870)
+
+2016-11-04 zzz
+ * Console: Improve handling and logging of webapps that fail to start
+ * i2psnark: Add launch-i2psnark.bat (ticket #1871)
+ * Transports:
+   - New config i2np.allowLocal, fixes test networks (ticket #1875)
+   - New configs i2np.udp.minpeers and i2np.udp.minv6peers, for testing (ticket #1876)
+
 2016-10-29 zzz
  * Console: Java 9 fixes for classloader (ticket #1870)
 
@@ -47,7 +113,7 @@
  * Console: Fix HTML error on /configservice
  * Debian: Update package descriptions, allow Java 9
  * i2psnark: Add ids to rows, add to per-torrent show peers link
- * SSU: Fix minimum version check for IPv6 peer test (ticket #1861)
+ * SSU: Fix minimum version check for IPv6 peer test (tickets #1829, #1861)
 
 * 2016-10-17 0.9.27 released
 
diff --git a/installer/i2pinstaller.xml b/installer/i2pinstaller.xml
index 84edded227a89180f3480ccc8412e2617e97fda1..62912b8284081aa27c0953fb43ba61303018be41 100644
--- a/installer/i2pinstaller.xml
+++ b/installer/i2pinstaller.xml
@@ -7,7 +7,7 @@
   <customProcName>false</customProcName>
   <icon>resources/console.ico</icon>
   <jre>
-    <minVersion>1.6.0</minVersion>
+    <minVersion>1.7.0</minVersion>
   </jre>
   <!--
   <splash>
diff --git a/installer/i2pstandalone.xml b/installer/i2pstandalone.xml
index 8b92ed21eeb1d0895cdf83de34e35d694aae03fc..71e338dd922ca913a852fe8d7799ebc75b4f2e68 100644
--- a/installer/i2pstandalone.xml
+++ b/installer/i2pstandalone.xml
@@ -7,7 +7,7 @@
   <customProcName>false</customProcName>
   <icon>resources/start.ico</icon>
   <jre>
-    <minVersion>1.6.0</minVersion>
+    <minVersion>1.7.0</minVersion>
     <!--
     <minHeapSize>64</minHeapSize>
     -->
diff --git a/installer/java/build.xml b/installer/java/build.xml
index f0caf6b8610222a1f6c04477548422f06b530345..e73ff9036eaea4fd1d2e7716b6b4ce35e7a56a51 100644
--- a/installer/java/build.xml
+++ b/installer/java/build.xml
@@ -57,6 +57,8 @@
                 <attribute name="Build-Date" value="${build.timestamp}" />
                 <attribute name="Base-Revision" value="${workspace.version}" />
                 <attribute name="Workspace-Changes" value="${workspace.changes.util.tr}" />
+                <attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+                <attribute name="X-Compile-Target-JDK" value="${javac.version}" />
             </manifest>
         </jar>
     </target>
diff --git a/installer/resources/blocklist.txt b/installer/resources/blocklist.txt
index fa05f88bfe4a8e1ae4b07822ccd0d7176615c420..74e7e01ffff380fb178b9c01fa370ddd2ccc9ca9 100644
--- a/installer/resources/blocklist.txt
+++ b/installer/resources/blocklist.txt
@@ -45,6 +45,10 @@
 #   *   hostname (DNS looked up at list readin time, not dynamically, so may not be much use)
 #   *   44-byte Base64 router hash
 #   *
+#   * Acceptable formats (IPV6 only):
+#   *   comment:IPv6 (must replace : with ; e.g. abcd;1234;0;12;;ff)
+#   *   IPv6 (must replace : with ; e.g. abcd;1234;0;12;;ff)
+#   *
 #   * No whitespace allowed after the last ':'.
 #   *
 #   * For further information and downloads:
diff --git a/installer/resources/certificates/ssl/isrgrootx1.crt b/installer/resources/certificates/ssl/isrgrootx1.crt
new file mode 100644
index 0000000000000000000000000000000000000000..9548dc1bf6f4423bec47e315927e687e47a44117
--- /dev/null
+++ b/installer/resources/certificates/ssl/isrgrootx1.crt
@@ -0,0 +1,31 @@
+-----BEGIN CERTIFICATE-----
+MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
+TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
+cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
+WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
+ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
+MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
+h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
+0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
+A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
+T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
+B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
+B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
+KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
+OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
+jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
+qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
+rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
+HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
+hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
+ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
+3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
+NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
+ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
+TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
+jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
+oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
+4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
+mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
+emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
+-----END CERTIFICATE-----
diff --git a/installer/resources/runplain.sh b/installer/resources/runplain.sh
index f1bf81d81034306dfef0e6b0d4bb5887f7e7cce5..eb4995dfed45a1aaf4c86c31591b6fcf0a0de262 100644
--- a/installer/resources/runplain.sh
+++ b/installer/resources/runplain.sh
@@ -3,7 +3,7 @@
 # This runs the router by itself, WITHOUT the wrapper.
 # This means the router will not restart if it crashes.
 # Also, you will be using the default memory size, which is
-# probably not enough for i2p.
+# probably not enough for i2p, unless you set it below.
 # You should really use the i2prouter script instead.
 #
 
@@ -19,6 +19,10 @@ I2PTEMP="%SYSTEM_java_io_tmpdir"
 PREFERv4="false"
 CP=
 
+# Uncomment to set the maximum memory. The default and the option may vary in different JVMs.
+# Check your java documentation to be sure.
+#MAXMEMOPT="-Xmx256m"
+
 # Try using the Java binary that I2P was installed with.
 # If it's not found, try looking in the system PATH.
 JAVA=$(which %JAVA_HOME/bin/java || which java)
@@ -39,7 +43,7 @@ done
 if [ $(uname -s) = "Darwin" ]; then
     export JAVA_TOOL_OPTIONS="-Djava.awt.headless=true"
 fi
-JAVAOPTS="-Djava.net.preferIPv4Stack=${PREFERv4} -Djava.library.path=${I2P}:${I2P}/lib -Di2p.dir.base=${I2P} -DloggerFilenameOverride=logs/log-router-@.txt"
+JAVAOPTS="${MAXMEMOPT} -Djava.net.preferIPv4Stack=${PREFERv4} -Djava.library.path=${I2P}:${I2P}/lib -Di2p.dir.base=${I2P} -DloggerFilenameOverride=logs/log-router-@.txt"
 (
     nohup ${JAVA} -cp \"${CP}\" ${JAVAOPTS} net.i2p.router.RouterLaunch > /dev/null 2>&1
 ) &
diff --git a/installer/tools/java/build.xml b/installer/tools/java/build.xml
index c7dff1b4e9cf3b28956732c2ef2368ebdd37d94d..40d8514a77570e640227ede7ae38bc3463649c50 100644
--- a/installer/tools/java/build.xml
+++ b/installer/tools/java/build.xml
@@ -38,6 +38,8 @@
                 <attribute name="Built-By" value="${build.built-by}" />
                 <attribute name="Build-Date" value="${build.timestamp}" />
                 <attribute name="Base-Revision" value="${workspace.version}" />
+                <attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+                <attribute name="X-Compile-Target-JDK" value="${javac.version}" />
             </manifest>
         </jar>
     </target>
diff --git a/router/java/build.xml b/router/java/build.xml
index fb2493089386d68efb39b0385f69cb653bdda360..1f42e4cd102b2f685d34aa02c5cd9249421a731b 100644
--- a/router/java/build.xml
+++ b/router/java/build.xml
@@ -78,6 +78,8 @@
                 <attribute name="Base-Revision" value="${workspace.version}" />
                 <attribute name="Main-Class" value="net.i2p.router.CommandLine" />
                 <attribute name="Workspace-Changes" value="${workspace.changes.tr}" />
+                <attribute name="X-Compile-Source-JDK" value="${javac.version}" />
+                <attribute name="X-Compile-Target-JDK" value="${javac.version}" />
             </manifest>
         </jar>
     </target>
diff --git a/router/java/src/net/i2p/router/Blocklist.java b/router/java/src/net/i2p/router/Blocklist.java
index 3ad8e7387507513ea489f484f8d5d2d3595cc5b1..9799ae443d5a7b0129517707a75b924e74c86ecc 100644
--- a/router/java/src/net/i2p/router/Blocklist.java
+++ b/router/java/src/net/i2p/router/Blocklist.java
@@ -248,6 +248,10 @@ public class Blocklist {
     *   hostname (DNS looked up at list readin time, not dynamically, so may not be much use)
     *   44-byte Base64 router hash
     *
+    * Acceptable formats (IPV6 only):
+    *   comment:IPv6 (must replace : with ; e.g. abcd;1234;0;12;;ff)
+    *   IPv6 (must replace : with ; e.g. abcd;1234;0;12;;ff)
+    *
     * No whitespace allowed after the last ':'.
     *
     * For further information and downloads:
@@ -290,10 +294,14 @@ public class Blocklist {
                     continue;
                 }
                 byte[] ip1 = e.ip1;
-                byte[] ip2 = e.ip2;
-
-                store(ip1, ip2, count++);
-                ipcount += 1 + toInt(ip2) - toInt(ip1); // includes dups, oh well
+                if (ip1.length == 4) {
+                    byte[] ip2 = e.ip2;
+                    store(ip1, ip2, count++);
+                    ipcount += 1 + toInt(ip2) - toInt(ip1); // includes dups, oh well
+                } else {
+                    // IPv6
+                    add(ip1);
+                }
             }
         } catch (IOException ioe) {
             if (_log.shouldLog(Log.ERROR))
@@ -393,25 +401,25 @@ public class Blocklist {
         int start2 = -1;
         int mask = -1;
         String comment = null;
-        int index = buf.indexOf("#");
+        int index = buf.indexOf('#');
         if (index == 0)
             return null;  // comment
-        index = buf.lastIndexOf(":");
+        index = buf.lastIndexOf(':');
         if (index >= 0) {
             comment = buf.substring(0, index);
             start1 = index + 1;
         }
-        if (end1 - start1 == 44 && buf.substring(start1).indexOf(".") < 0) {
+        if (end1 - start1 == 44 && buf.substring(start1).indexOf('.') < 0) {
             byte b[] = Base64.decode(buf.substring(start1));
             if (b != null)
                 return new Entry(comment, Hash.create(b), null, null);
         }
-        index = buf.indexOf("-", start1);
+        index = buf.indexOf('-', start1);
         if (index >= 0) {
             end1 = index;
             start2 = index + 1;
         } else {
-            index = buf.indexOf("/", start1);
+            index = buf.indexOf('/', start1);
             if (index >= 0) {
                 end1 = index;
                 mask = index + 1;
@@ -420,11 +428,14 @@ public class Blocklist {
         if (end1 - start1 <= 0)
             return null;  // blank
         try {
-            InetAddress pi = InetAddress.getByName(buf.substring(start1, end1));
+            String sip = buf.substring(start1, end1);
+            // IPv6
+            sip = sip.replace(';', ':');
+            InetAddress pi = InetAddress.getByName(sip);
             if (pi == null) return null;
             ip1 = pi.getAddress();
-            if (ip1.length != 4)
-                throw new UnknownHostException();
+            //if (ip1.length != 4)
+            //    throw new UnknownHostException();
             if (start2 >= 0) {
                 pi = InetAddress.getByName(buf.substring(start2));
                 if (pi == null) return null;
@@ -462,16 +473,16 @@ public class Blocklist {
                 ip2 = ip1;
             }
         } catch (UnknownHostException uhe) {
-            if (shouldLog && _log.shouldLog(Log.ERROR))
-                _log.error("Format error in the blocklist file: " + buf);
+            if (shouldLog)
+                _log.logAlways(Log.WARN, "Format error in the blocklist file: " + buf);
             return null;
         } catch (NumberFormatException nfe) {
-            if (shouldLog && _log.shouldLog(Log.ERROR))
-                _log.error("Format error in the blocklist file: " + buf);
+            if (shouldLog)
+                _log.logAlways(Log.WARN, "Format error in the blocklist file: " + buf);
             return null;
         } catch (IndexOutOfBoundsException ioobe) {
-            if (shouldLog && _log.shouldLog(Log.ERROR))
-                _log.error("Format error in the blocklist file: " + buf);
+            if (shouldLog)
+                _log.logAlways(Log.WARN, "Format error in the blocklist file: " + buf);
             return null;
         }
         return new Entry(comment, null, ip1, ip2);
@@ -743,6 +754,9 @@ public class Blocklist {
         return entry;
     }
 
+    /**
+     *  IPv4 only
+     */
     private void store(byte ip1[], byte ip2[], int idx) {
         _blocklist[idx] = toEntry(ip1, ip2);
     }
@@ -1035,10 +1049,16 @@ public class Blocklist {
     }
 
 /****
-    public static void main(String args[]) {
-        Blocklist b = new Blocklist();
-        if ( (args != null) && (args.length == 1) )
-            b.readBlocklistFile(args[0]);
+    public static void main(String args[]) throws Exception {
+        Blocklist b = new Blocklist(new Router().getContext());
+        if (args != null && args.length == 1) {
+            File f = new File(args[0]);
+            b.allocate(Collections.singletonList(f));
+            int count = b.readBlocklistFile(f, 0);
+            b.merge(count);
+            Writer w = new java.io.OutputStreamWriter(System.out);
+            b.renderStatusHTML(w);
+        }
         System.out.println("Saved " + b._blocklistSize + " records");
         String tests[] = {"0.0.0.0", "0.0.0.1", "0.0.0.2", "0.0.0.255", "1.0.0.0",
                                         "3.3.3.3", "77.1.2.3", "127.0.0.0", "127.127.127.127", "128.0.0.0",
diff --git a/router/java/src/net/i2p/router/CommandLine.java b/router/java/src/net/i2p/router/CommandLine.java
index 8a3986d86dd779f44b2d82f891396b22eed9a8ac..c3af8d7381c085b96e9bf4c449308d5d57cb83b3 100644
--- a/router/java/src/net/i2p/router/CommandLine.java
+++ b/router/java/src/net/i2p/router/CommandLine.java
@@ -23,7 +23,7 @@ public class CommandLine extends net.i2p.util.CommandLine {
         "net.i2p.router.tasks.CryptoChecker",
         "net.i2p.router.transport.GeoIPv6",
         "net.i2p.router.transport.udp.MTU",
-        //"net.i2p.router.transport.UPnP"
+        "net.i2p.router.transport.UPnP"
     });
 
     protected CommandLine() {}
diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java
index c10128fe6223a59f4fa2320e38c3309487438a87..f2522cd4ed540000a936a603d4922f4a287ece1f 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 = 6;
+    public final static long BUILD = 9;
 
     /** for example "-test" */
     public final static String EXTRA = "";
diff --git a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
index a50a6c9bce0ca68df89775f16516febbb57f75ed..b6893b59c0922a9ca95b8dc32e5a701a99031615 100644
--- a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
+++ b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
@@ -329,14 +329,14 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
                 pw = props.getProperty("i2cp.password");
             }
             if (user == null || user.length() == 0 || pw == null || pw.length() == 0) {
-                _log.error("I2CP auth failed");
+                _log.logAlways(Log.WARN, "I2CP authentication failed");
                 _runner.disconnectClient("Authorization required, specify i2cp.username and i2cp.password in options");
                 _authorized = false;
                 return false;
             }
             PasswordManager mgr = new PasswordManager(_context);
             if (!mgr.checkHash(PROP_AUTH, user, pw)) {
-                _log.error("I2CP auth failed user: " + user);
+                _log.logAlways(Log.WARN, "I2CP authentication failed, user: " + user);
                 _runner.disconnectClient("Authorization failed, user = " + user);
                 _authorized = false;
                 return false;
diff --git a/router/java/src/net/i2p/router/crypto/FamilyKeyCrypto.java b/router/java/src/net/i2p/router/crypto/FamilyKeyCrypto.java
index cdb0811dc7b52a7d83cfaeaf1a84e1ea81e29ea4..f1bfe2063a316196cb20003015fee061ebde2ce9 100644
--- a/router/java/src/net/i2p/router/crypto/FamilyKeyCrypto.java
+++ b/router/java/src/net/i2p/router/crypto/FamilyKeyCrypto.java
@@ -10,6 +10,7 @@ import java.security.PrivateKey;
 import java.security.PublicKey;
 import java.security.cert.X509Certificate;
 import java.security.cert.X509CRL;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
@@ -44,6 +45,7 @@ public class FamilyKeyCrypto {
     private final Log _log;
     private final Map<Hash, String> _verified;
     private final Set<Hash> _negativeCache;
+    private final Set<Hash> _ourFamily;
     // following for verification only, otherwise null
     private final String _fname;
     private final SigningPrivateKey _privkey;
@@ -82,13 +84,15 @@ public class FamilyKeyCrypto {
         _fname = _context.getProperty(PROP_FAMILY_NAME);
         if (_fname != null) {
             if (_fname.contains("/") || _fname.contains("\\") ||
-                _fname.contains("..") || (new File(_fname)).isAbsolute())
-                throw new GeneralSecurityException("Illegal family name");
+                _fname.contains("..") || (new File(_fname)).isAbsolute() ||
+                _fname.length() <= 0)
+                throw new GeneralSecurityException("Illegal family name: " + _fname);
         }
         _privkey = (_fname != null) ? initialize() : null;
         _pubkey = (_privkey != null) ? _privkey.toPublic() : null;
         _verified = new ConcurrentHashMap<Hash, String>(4);
         _negativeCache = new ConcurrentHashSet<Hash>(4);
+        _ourFamily = (_privkey != null) ? new ConcurrentHashSet<Hash>(4) : Collections.<Hash>emptySet();
     }
     
     /** 
@@ -144,6 +148,35 @@ public class FamilyKeyCrypto {
         return rv;
     }
 
+    /** 
+     *  Do we have a valid family?
+     *  @since 0.9.28
+     */
+    public boolean hasFamily() {
+        return _pubkey != null;
+    }
+
+    /** 
+     *  Get verified members of our family.
+     *  Will not contain ourselves.
+     *
+     *  @return non-null, not a copy, do not modify
+     *  @since 0.9.28
+     */
+    public Set<Hash> getOurFamily() {
+        return _ourFamily;
+    }
+
+    /** 
+     *  Get our family name.
+     *
+     *  @return name or null
+     *  @since 0.9.28
+     */
+    public String getOurFamilyName() {
+        return _fname;
+    }
+
     /** 
      *  Verify the family signature in a RouterInfo.
      *  @return true if good sig or if no family specified at all
@@ -152,6 +185,44 @@ public class FamilyKeyCrypto {
         String name = ri.getOption(OPT_NAME);
         if (name == null)
             return true;
+        return verify(ri, name);
+    }
+
+    /** 
+     *  Verify the family in a RouterInfo matches ours and the signature is good.
+     *  Returns false if we don't have a family and sig, or they don't.
+     *  Returns false for ourselves.
+     *
+     *  @return true if family matches with good sig
+     *  @since 0.9.28
+     */
+    public boolean verifyOurFamily(RouterInfo ri) {
+        if (_pubkey == null)
+            return false;
+        String name = ri.getOption(OPT_NAME);
+        if (!_fname.equals(name))
+            return false;
+        Hash h = ri.getHash();
+        if (_ourFamily.contains(h))
+            return true;
+        if (h.equals(_context.routerHash()))
+            return false;
+        boolean rv = verify(ri, name);
+        if (rv) {
+            _ourFamily.add(h);
+            _log.logAlways(Log.INFO, "Found and verified member of our family (" + _fname + "): " + h);
+        } else {
+            if (_log.shouldWarn())
+                _log.warn("Found spoofed member of our family (" + _fname + "): " + h);
+        }
+        return rv;
+    }
+
+    /** 
+     *  Verify the family in a RouterInfo, name already retrieved
+     *  @since 0.9.28
+     */
+    private boolean verify(RouterInfo ri, String name) {
         Hash h = ri.getHash();
         String ssig = ri.getOption(OPT_SIG);
         if (ssig == null) {
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 9ffda0201e34912afc0e9a4a2e3459e09328436d..d0f94131ccb1d700cabc47282eeda97ec20c0829 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java
@@ -19,6 +19,7 @@ import net.i2p.router.MessageSelector;
 import net.i2p.router.ReplyJob;
 import net.i2p.router.RouterContext;
 import net.i2p.router.TunnelInfo;
+import net.i2p.router.util.MaskedIPSet;
 import net.i2p.util.Log;
 
 /**
@@ -39,11 +40,13 @@ class FloodfillVerifyStoreJob extends JobImpl {
     private final boolean _isRouterInfo;
     private MessageWrapper.WrappedMessage _wrappedMessage;
     private final Set<Hash> _ignore;
+    private final MaskedIPSet _ipSet;
     
     private static final int START_DELAY = 18*1000;
     private static final int START_DELAY_RAND = 9*1000;
     private static final int VERIFY_TIMEOUT = 20*1000;
     private static final int MAX_PEERS_TO_TRY = 4;
+    private static final int IP_CLOSE_BYTES = 3;
     
     /**
      *  Delay a few seconds, then start the verify
@@ -60,7 +63,10 @@ class FloodfillVerifyStoreJob extends JobImpl {
         _facade = facade;
         _ignore = new HashSet<Hash>(MAX_PEERS_TO_TRY);
         if (sentTo != null) {
+            _ipSet = new MaskedIPSet(ctx, sentTo, IP_CLOSE_BYTES);
             _ignore.add(_sentTo);
+        } else {
+            _ipSet = new MaskedIPSet(4);
         }
         // wait some time before trying to verify the store
         getTiming().setStartAfter(ctx.clock().now() + START_DELAY + ctx.random().nextInt(START_DELAY_RAND));
@@ -188,10 +194,19 @@ class FloodfillVerifyStoreJob extends JobImpl {
                     break;
                 Hash peer = peers.get(0);
                 RouterInfo ri = _facade.lookupRouterInfoLocally(peer);
-                if (ri != null && StoreJob.supportsCert(ri, keyCert))
-                    return peer;
-                if (_log.shouldLog(Log.INFO))
-                    _log.info(getJobId() + ": Skipping verify w/ router that doesn't support key certs " + peer);
+                if (ri != null && StoreJob.supportsCert(ri, keyCert)) {
+                    Set<String> peerIPs = new MaskedIPSet(getContext(), ri, IP_CLOSE_BYTES);
+                    if (!_ipSet.containsAny(peerIPs)) {
+                        _ipSet.addAll(peerIPs);
+                        return peer;
+                    } else {
+                        if (_log.shouldLog(Log.INFO))
+                            _log.info(getJobId() + ": Skipping verify w/ router too close to the store " + peer);
+                    }
+                } else {
+                    if (_log.shouldLog(Log.INFO))
+                        _log.info(getJobId() + ": Skipping verify w/ router that doesn't support key certs " + peer);
+                }
                 _ignore.add(peer);
             }
         } else {
diff --git a/router/java/src/net/i2p/router/peermanager/PeerManager.java b/router/java/src/net/i2p/router/peermanager/PeerManager.java
index b3cc3643b029881c435b6239de036407b15525c9..362c67b5a7a7f8fc5a02fadd0884053099173fca 100644
--- a/router/java/src/net/i2p/router/peermanager/PeerManager.java
+++ b/router/java/src/net/i2p/router/peermanager/PeerManager.java
@@ -17,6 +17,7 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import net.i2p.data.Hash;
 import net.i2p.data.router.RouterInfo;
@@ -43,6 +44,9 @@ class PeerManager {
     private final Map<Character, Set<Hash>> _peersByCapability;
     /** value strings are lower case */
     private final Map<Hash, String> _capabilitiesByPeer;
+    private final AtomicBoolean _storeLock = new AtomicBoolean();
+    private volatile long _lastStore;
+
     private static final long REORGANIZE_TIME = 45*1000;
     private static final long REORGANIZE_TIME_MEDIUM = 123*1000;
     /**
@@ -52,6 +56,8 @@ class PeerManager {
      *  Rate contained in the profile, as the Rates must be coalesced.
      */
     private static final long REORGANIZE_TIME_LONG = 351*1000;
+    private static final long STORE_TIME = 19*60*60*1000;
+    private static final long EXPIRE_AGE = 3*24*60*60*1000;
     
     public static final String TRACKED_CAPS = "" +
         FloodfillNetworkDatabaseFacade.CAPABILITY_FLOODFILL +
@@ -97,11 +103,14 @@ class PeerManager {
     }
 
     /**
+     *  Reorganize the profiles. Also periodically store them,
+     *  and delete very old ones.
+     *
      *  This takes too long to run on the SimpleTimer2 queue
      *  @since 0.9.10
      */
     private class ReorgThread extends I2PThread {
-        private SimpleTimer2.TimedEvent _event;
+        private final SimpleTimer2.TimedEvent _event;
 
         public ReorgThread(SimpleTimer2.TimedEvent event) {
             super("PeerManager Reorg");
@@ -117,6 +126,19 @@ class PeerManager {
                 _log.log(Log.CRIT, "Error evaluating profiles", t);
             }
             long orgtime = System.currentTimeMillis() - start;
+            if (_lastStore == 0) {
+                _lastStore = start;
+            } else if (start - _lastStore > STORE_TIME) {
+                _lastStore = start;
+                try {
+                    _log.debug("Periodic profile store start");
+                    storeProfiles();
+                    _persistenceHelper.deleteOldProfiles(EXPIRE_AGE);
+                    _log.debug("Periodic profile store end");
+                } catch (Throwable t) {
+                    _log.log(Log.CRIT, "Error storing profiles", t);
+                }
+            }
             long uptime = _context.router().getUptime();
             long delay;
             if (orgtime > 1000 || uptime > 2*60*60*1000)
@@ -130,9 +152,16 @@ class PeerManager {
     }
     
     void storeProfiles() {
-        Set<Hash> peers = selectPeers();
-        for (Hash peer : peers) {
-            storeProfile(peer);
+        // lock in case shutdown bumps into periodic store
+        if (!_storeLock.compareAndSet(false, true))
+            return;
+        try {
+            Set<Hash> peers = selectPeers();
+            for (Hash peer : peers) {
+                storeProfile(peer);
+            }
+        } finally {
+            _storeLock.set(false);
         }
     }
 
diff --git a/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java
index c71f8b6a65fdfdc05b5e9c6e14099d45cf8e0a00..481cc18b626f701675e1f8013156115eb7ed0d5f 100644
--- a/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java
+++ b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java
@@ -24,6 +24,7 @@ import net.i2p.data.router.RouterInfo;
 import net.i2p.router.NetworkDatabaseFacade;
 import net.i2p.router.RouterContext;
 import net.i2p.router.tunnel.pool.TunnelPeerSelector;
+import net.i2p.router.util.MaskedIPSet;
 import net.i2p.router.util.RandomIterator;
 import net.i2p.stat.Rate;
 import net.i2p.stat.RateStat;
@@ -1245,7 +1246,7 @@ public class ProfileOrganizer {
      */
     private void locked_selectPeers(Map<Hash, PeerProfile> peers, int howMany, Set<Hash> toExclude, Set<Hash> matches, int mask) {
         List<Hash> all = new ArrayList<Hash>(peers.keySet());
-        Set<String> IPSet = new HashSet<String>(8);
+        MaskedIPSet IPSet = new MaskedIPSet(8);
         // use RandomIterator to avoid shuffling the whole thing
         for (Iterator<Hash> iter = new RandomIterator<Hash>(all); (matches.size() < howMany) && iter.hasNext(); ) {
             Hash peer = iter.next();
@@ -1277,77 +1278,14 @@ public class ProfileOrganizer {
      * @param mask is 1-4 (number of bytes to match)
      * @param IPMatches all IPs so far, modified by this routine
      */
-    private boolean notRestricted(Hash peer, Set<String> IPSet, int mask) {
-        Set<String> peerIPs = maskedIPSet(peer, mask);
-        if (containsAny(IPSet, peerIPs))
+    private boolean notRestricted(Hash peer, MaskedIPSet IPSet, int mask) {
+        Set<String> peerIPs = new MaskedIPSet(_context, peer, mask);
+        if (IPSet.containsAny(peerIPs))
             return false;
         IPSet.addAll(peerIPs);
         return true;
     }
 
-    /**
-      * The Set of IPs for this peer, with a given mask.
-      * Includes the comm system's record of the IP, and all netDb addresses.
-      *
-      * As of 0.9.24, returned set will include netdb family as well.
-      *
-      * @return an opaque set of masked IPs for this peer
-      */
-    private Set<String> maskedIPSet(Hash peer, int mask) {
-        Set<String> rv = new HashSet<String>(4);
-        byte[] commIP = _context.commSystem().getIP(peer);
-        if (commIP != null)
-            rv.add(maskedIP(commIP, mask));
-        RouterInfo pinfo = _context.netDb().lookupRouterInfoLocally(peer);
-        if (pinfo == null)
-            return rv;
-        Collection<RouterAddress> paddr = pinfo.getAddresses();
-        for (RouterAddress pa : paddr) {
-            byte[] pib = pa.getIP();
-            if (pib == null) continue;
-            rv.add(maskedIP(pib, mask));
-        }
-        String family = pinfo.getOption("family");
-        if (family != null) {
-            // TODO should KNDF put a family-verified indicator in the RI,
-            // after checking the sig, or does it matter?
-            // What's the threat here of not avoid ding a router
-            // falsely claiming to be in the family?
-            // Prefix with something so an IP can't be spoofed
-            rv.add('x' + family);
-        }
-        return rv;
-    }
-
-    /**
-     * generate an arbitrary unique value for this ip/mask (mask = 1-4)
-     * If IPv6, force mask = 6.
-     */
-    private static String maskedIP(byte[] ip, int mask) {
-        final StringBuilder buf = new StringBuilder(1 + (mask*2));
-        final char delim;
-        if (ip.length == 16) {
-            mask = 6;
-            delim = ':';
-        } else {
-            delim = '.';
-        }
-        buf.append(delim);
-        buf.append(Long.toHexString(DataHelper.fromLong(ip, 0, mask)));
-        return buf.toString();
-    }
-
-    /** does a contain any of the elements in b? */
-    private static <T> boolean  containsAny(Set<T> a, Set<T> b) {
-        if (a.isEmpty() || b.isEmpty())
-            return false;
-        for (T o : b) {
-            if (a.contains(o))
-                return true;
-        }
-        return false;
-    }
-
     /**
      * @param randomKey used for deterministic random partitioning into subtiers
      * @param subTierMode 2-7:
diff --git a/router/java/src/net/i2p/router/peermanager/ProfilePersistenceHelper.java b/router/java/src/net/i2p/router/peermanager/ProfilePersistenceHelper.java
index f978194d3867a3fec8eb02b52fc3a9c8f3a8ff32..f22bf0e3f14c5ebd3e69df5c41f595d3d7e5e05f 100644
--- a/router/java/src/net/i2p/router/peermanager/ProfilePersistenceHelper.java
+++ b/router/java/src/net/i2p/router/peermanager/ProfilePersistenceHelper.java
@@ -229,6 +229,26 @@ class ProfilePersistenceHelper {
         }
     }
     
+    /**
+     *  Delete profile files with timestamps older than 'age' ago
+     *  @since 0.9.28
+     */
+    public void deleteOldProfiles(long age) {
+        long cutoff = System.currentTimeMillis() - age;
+        List<File> files = selectFiles();
+        int i = 0;
+        for (File f :  files) {
+            if (!f.isFile())
+                continue;
+            if (f.lastModified() < cutoff) {
+                i++;
+                f.delete();
+            }
+        }
+        if (_log.shouldWarn())
+            _log.warn("Deleted " + i + " old profiles");
+    }
+
     private boolean isExpired(long lastSentToSuccessfully) {
         long timeSince = _context.clock().now() - lastSentToSuccessfully;
         return (timeSince > EXPIRE_AGE);
diff --git a/router/java/src/net/i2p/router/tasks/OOMListener.java b/router/java/src/net/i2p/router/tasks/OOMListener.java
index 3aa2c08661367d714d5888b8b314f66dbc47e5e6..ed2050a390188919a9fbada02f54df9073b10ed8 100644
--- a/router/java/src/net/i2p/router/tasks/OOMListener.java
+++ b/router/java/src/net/i2p/router/tasks/OOMListener.java
@@ -49,17 +49,23 @@ public class OOMListener implements I2PThread.OOMEventListener {
             log.log(Log.CRIT, "Thread ran out of memory, shutting down I2P", oom);
             log.log(Log.CRIT, "free mem: " + Runtime.getRuntime().freeMemory() + 
                               " total mem: " + Runtime.getRuntime().totalMemory());
+            // Can't find any System property or wrapper property that gives
+            // you the actual config file path, have to guess
+            String path;
+            if (SystemVersion.isLinuxService()) {
+                path = "/etc/i2p";
+            } else {
+                path = _context.getBaseDir().toString();
+            }
             if (_context.hasWrapper()) {
-                // Can't find any System property or wrapper property that gives
-                // you the actual config file path, have to guess
-                String path;
-                if (SystemVersion.isLinuxService()) {
-                    path = "/etc/i2p";
-                } else {
-                    path = _context.getBaseDir().toString();
-                }
                 log.log(Log.CRIT, "To prevent future shutdowns, increase wrapper.java.maxmemory in " +
                                   path + File.separatorChar + "wrapper.config");
+            } else if (!SystemVersion.isWindows()) {
+                log.log(Log.CRIT, "To prevent future shutdowns, increase MAXMEMOPT in " +
+                                  path + File.separatorChar + "runplain.sh or /usr/bin/i2prouter-nowrapper");
+            } else {
+                log.log(Log.CRIT, "To prevent future shutdowns, run the restartable version of I2P, and increase wrapper.java.maxmemory in " +
+                                  path + File.separatorChar + "wrapper.config");
             }
         } catch (OutOfMemoryError oome) {}
         try { 
diff --git a/router/java/src/net/i2p/router/transport/TransportManager.java b/router/java/src/net/i2p/router/transport/TransportManager.java
index 80888ead7bf56787c6977a14b38ca1367a2dd9b2..9e6c9a953c29a27c1b7767793450abe99a057925 100644
--- a/router/java/src/net/i2p/router/transport/TransportManager.java
+++ b/router/java/src/net/i2p/router/transport/TransportManager.java
@@ -11,6 +11,7 @@ package net.i2p.router.transport;
 import java.io.IOException;
 import java.io.Writer;
 import java.net.InetAddress;
+import java.net.Inet6Address;
 import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -165,7 +166,7 @@ public class TransportManager implements TransportEventListener {
                 // so that NTCP may bind early
                 int port = udp.getRequestedPort();
                 if (port > 0)
-                    ntcp.externalAddressReceived(SOURCE_CONFIG, null, port);
+                    ntcp.externalAddressReceived(SOURCE_CONFIG, (byte[]) null, port);
             }
         }
         if (_transports.isEmpty())
@@ -182,15 +183,52 @@ public class TransportManager implements TransportEventListener {
      */
     private void initializeAddress(Transport t) {
         Set<String> ipset = Addresses.getAddresses(false, true);  // non-local, include IPv6
+        //
+        // Avoid IPv6 temporary addresses if we have a non-temporary one
+        //
+        boolean hasNonTempV6Address = false;
+        List<InetAddress> addresses = new ArrayList<InetAddress>(4);
+        List<Inet6Address> tempV6Addresses = new ArrayList<Inet6Address>(4);
         for (String ips : ipset) {
             try {
-                InetAddress ia = InetAddress.getByName(ips);
-                byte[] ip = ia.getAddress();
-                t.externalAddressReceived(SOURCE_INTERFACE, ip, 0);
+                InetAddress addr = InetAddress.getByName(ips);
+                if (ips.contains(":") && (addr instanceof Inet6Address)) {
+                    Inet6Address v6addr = (Inet6Address) addr;
+                    // getAddresses(false, true) will not return deprecated addresses
+                    //if (Addresses.isDeprecated(v6addr)) {
+                    //    if (_log.shouldWarn())
+                    //        _log.warn("Not binding to deprecated temporary address " + bt);
+                    //    continue;
+                    //}
+                    if (Addresses.isTemporary(v6addr)) {
+                        // Save temporary addresses
+                        // we only use these if we don't have a non-temporary adress
+                        tempV6Addresses.add(v6addr);
+                        continue;
+                    }
+                    hasNonTempV6Address = true;
+                }
+                addresses.add(addr);
             } catch (UnknownHostException e) {
                 _log.error("UDP failed to bind to local address", e);
             }
         }
+        // we only use these if we don't have a non-temporary adress
+        if (!tempV6Addresses.isEmpty()) {
+            if (hasNonTempV6Address) {
+                if (_log.shouldWarn()) {
+                    for (Inet6Address addr : tempV6Addresses) {
+                        _log.warn("Not binding to temporary address " + addr.getHostAddress());
+                    }
+                }
+            } else {
+                addresses.addAll(tempV6Addresses);
+            }
+        }
+        for (InetAddress ia : addresses) {
+            byte[] ip = ia.getAddress();
+            t.externalAddressReceived(SOURCE_INTERFACE, ip, 0);
+        }
     }
 
     /**
diff --git a/router/java/src/net/i2p/router/transport/TransportUtil.java b/router/java/src/net/i2p/router/transport/TransportUtil.java
index 16ede3b34447095476c3ae04417bf1dfd8749a4a..bb4e022d6c301d0236f7e358bd8aca7a85499a88 100644
--- a/router/java/src/net/i2p/router/transport/TransportUtil.java
+++ b/router/java/src/net/i2p/router/transport/TransportUtil.java
@@ -25,6 +25,8 @@ public abstract class TransportUtil {
     public static final String NTCP_IPV6_CONFIG = "i2np.ntcp.ipv6";
     public static final String SSU_IPV6_CONFIG = "i2np.udp.ipv6";
     public static final String PROP_IPV4_FIREWALLED = "i2np.ipv4.firewalled";
+    /** @since 0.9.28 */
+    public static final String PROP_IPV6_FIREWALLED = "i2np.ipv6.firewalled";
 
     public enum IPv6Config {
         /** IPv6 disabled */
@@ -99,12 +101,10 @@ public abstract class TransportUtil {
      *  This returns true if the force-firewalled setting is configured, false otherwise.
      *
      *  @param transportStyle ignored
-     *  @since 0.9.27
+     *  @since 0.9.27, implemented in 0.9.28
      */
     public static boolean isIPv6Firewalled(RouterContext ctx, String transportStyle) {
-        // TODO
-        //return ctx.getBooleanProperty(PROP_IPV6_FIREWALLED);
-        return false;
+        return ctx.getBooleanProperty(PROP_IPV6_FIREWALLED);
     }
 
     /**
diff --git a/router/java/src/net/i2p/router/transport/UPnP.java b/router/java/src/net/i2p/router/transport/UPnP.java
index 1f30c768c1e3b34718853707d054187743a05386..52637cec9603ee78f7fd480c97a99e51138925b5 100644
--- a/router/java/src/net/i2p/router/transport/UPnP.java
+++ b/router/java/src/net/i2p/router/transport/UPnP.java
@@ -58,6 +58,8 @@ import org.freenetproject.ForwardPortStatus;
  *
  * some code has been borrowed from Limewire : @see com.limegroup.gnutella.UPnPManager
  *
+ * Public only for command line usage. Not a public API, not for external use.
+ *
  * @see "http://www.upnp.org/"
  * @see "http://en.wikipedia.org/wiki/Universal_Plug_and_Play"
  * @since 0.7.4
@@ -68,7 +70,7 @@ import org.freenetproject.ForwardPortStatus;
  * TODO: Advertise the node like the MDNS plugin does
  * TODO: Implement EventListener and react on ip-change
  */ 
-class UPnP extends ControlPoint implements DeviceChangeListener, EventListener {
+public class UPnP extends ControlPoint implements DeviceChangeListener, EventListener {
 	private final Log _log;
 	private final I2PAppContext _context;
 	
diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
index 13cba50f59162372fe3392df3883992e6f8ae447..90138313b8dad6eca4302a298a24aa31c1bf0e9c 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
@@ -150,6 +150,8 @@ public class NTCPTransport extends TransportImpl {
         //_context.statManager().createRateStat("ntcp.inboundCheckConnection", "", "ntcp", RATES);
         _context.statManager().createRateStat("ntcp.inboundEstablished", "", "ntcp", RATES);
         _context.statManager().createRateStat("ntcp.inboundEstablishedDuplicate", "", "ntcp", RATES);
+        _context.statManager().createRateStat("ntcp.inboundIPv4Conn", "Inbound IPv4 NTCP Connection", "ntcp", RATES);
+        _context.statManager().createRateStat("ntcp.inboundIPv6Conn", "Inbound IPv6 NTCP Connection", "ntcp", RATES);
         //_context.statManager().createRateStat("ntcp.infoMessageEnqueued", "", "ntcp", RATES);
         //_context.statManager().createRateStat("ntcp.floodInfoMessageEnqueued", "", "ntcp", RATES);
         _context.statManager().createRateStat("ntcp.invalidDH", "", "ntcp", RATES);
@@ -213,10 +215,13 @@ public class NTCPTransport extends TransportImpl {
         synchronized (_conLock) {
             old = _conByIdent.put(peer, con);
         }
-        if (con.isIPv6())
+        if (con.isIPv6()) {
             _lastInboundIPv6 = con.getCreated();
-        else
+            _context.statManager().addRateData("ntcp.inboundIPv6Conn", 1);
+        } else {
             _lastInboundIPv4 = con.getCreated();
+            _context.statManager().addRateData("ntcp.inboundIPv4Conn", 1);
+        }
         return old;
     }
 
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
index 064e1497ad5afaa42b64ca699c518751ee847dcb..5686a4aa8e7898662666f5c04b7fec47c65d0e14 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
@@ -289,6 +289,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
         _context.statManager().createRateStat("udp.proactiveReestablish", "How long a session was idle for when we proactively reestablished it", "udp", RATES);
         _context.statManager().createRateStat("udp.dropPeerDroplist", "How many peers currently have their packets dropped outright when a new peer is added to the list?", "udp", RATES);
         _context.statManager().createRateStat("udp.dropPeerConsecutiveFailures", "How many consecutive failed sends to a peer did we attempt before giving up and reestablishing a new session (lifetime is inactivity perood)", "udp", RATES);
+        _context.statManager().createRateStat("udp.inboundIPv4Conn", "Inbound IPv4 UDP Connection", "udp", RATES);
+        _context.statManager().createRateStat("udp.inboundIPv6Conn", "Inbound IPv4 UDP Connection", "udp", RATES);
         // following are for PacketBuider
         //_context.statManager().createRateStat("udp.packetAuthTime", "How long it takes to encrypt and MAC a packet for sending", "udp", RATES);
         //_context.statManager().createRateStat("udp.packetAuthTimeSlow", "How long it takes to encrypt and MAC a packet for sending (when its slow)", "udp", RATES);
@@ -780,6 +782,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
     void inboundConnectionReceived(boolean isIPv6) {
         if (isIPv6) {
             _lastInboundIPv6 = _context.clock().now();
+            _context.statManager().addRateData("udp.inboundIPv6Conn", 1);
             // former workaround for lack of IPv6 peer testing
             //if (_currentOurV6Address != null)
             //    setReachabilityStatus(Status.IPV4_UNKNOWN_IPV6_OK, true);
@@ -788,6 +791,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
             // that we are not firewalled.
             // use OS clock since its an ordering thing, not a time thing
             _lastInboundReceivedOn = System.currentTimeMillis(); 
+            _context.statManager().addRateData("udp.inboundIPv4Conn", 1);
         }
     }
     
@@ -1765,13 +1769,23 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
             // (Otherwise we only talk UDP to those that are firewalled, and we will
             // never get any introducers)
             int count = _peersByIdent.size();
-            if (alwaysPreferUDP() || count < _min_peers ||
-                (_haveIPv6Address && count < _min_v6_peers) ||
-                (introducersRequired() && _introManager.introducerCount() < MIN_INTRODUCER_POOL))
+            if (alwaysPreferUDP()) {
                 return _cachedBid[SLOW_PREFERRED_BID];
-            else if (preferUDP())
+            } else if (count < _min_peers ||
+                       (_haveIPv6Address && count < _min_v6_peers) ||
+                       (introducersRequired() && _introManager.introducerCount() < MIN_INTRODUCER_POOL)) {
+                 // Even if we haven't hit our minimums, give NTCP a chance some of the time.
+                 // This may make things work a little faster at startup
+                 // (especially when we have an IPv6 address and the increased minimums),
+                 // and if UDP is completely blocked we'll still have some connectivity.
+                 // TODO After some time, decide that UDP is blocked/broken and return TRANSIENT_FAIL_BID?
+                if (_context.random().nextInt(4) == 0)
+                    return _cachedBid[SLOWEST_BID];
+                else
+                    return _cachedBid[SLOW_PREFERRED_BID];
+            } else if (preferUDP()) {
                 return _cachedBid[SLOW_BID];
-            else if (haveCapacity()) {
+            } else if (haveCapacity()) {
                 if (addr.getCost() > DEFAULT_COST)
                     return _cachedBid[SLOWEST_COST_BID];
                 else
diff --git a/router/java/src/net/i2p/router/tunnel/BloomFilterIVValidator.java b/router/java/src/net/i2p/router/tunnel/BloomFilterIVValidator.java
index f96bdfece26e5622b063151fc24323fb402cd8f4..61a52e2a356effdb07349975a9944d2a08918001 100644
--- a/router/java/src/net/i2p/router/tunnel/BloomFilterIVValidator.java
+++ b/router/java/src/net/i2p/router/tunnel/BloomFilterIVValidator.java
@@ -104,7 +104,6 @@ class BloomFilterIVValidator implements IVValidator {
             return;
         // Can't find any System property or wrapper property that gives
         // you the actual config file path, have to guess
-        // TODO if !SystemVersion.hasWrapper ...
         String path;
         if (SystemVersion.isLinuxService()) {
             path = "/etc/i2p";
@@ -114,13 +113,21 @@ class BloomFilterIVValidator implements IVValidator {
         String msg =
             "Configured for " + DataHelper.formatSize(KBps *1024L) +
             "Bps share bandwidth but only " +
-            DataHelper.formatSize(maxMemory) + "B available memory." +
-            " Recommend increasing wrapper.java.maxmemory in " +
-            path + File.separatorChar + "wrapper.config" +
-            // getMaxMemory() returns significantly lower than wrapper config, so add 10%
-            " to at least " + (recMaxMem * 11 / 10 / (1024*1024)) + " (MB)" +
-            " if the actual share bandwidth exceeds " +
-            DataHelper.formatSize(threshKBps * 1024L) + "Bps.";
+            DataHelper.formatSize(maxMemory) + "B available memory.";
+        if (_context.hasWrapper()) {
+            msg += " Recommend increasing wrapper.java.maxmemory in " +
+                   path + File.separatorChar + "wrapper.config";
+        } else if (!SystemVersion.isWindows()) {
+            msg += " Recommend increasing MAXMEMOPT in " +
+                   path + File.separatorChar + "runplain.sh or /usr/bin/i2prouter-nowrapper";
+        } else {
+            msg += " Recommend running the restartable version of I2P, and increasing wrapper.java.maxmemory in " +
+                   path + File.separatorChar + "wrapper.config";
+        }
+        // getMaxMemory() returns significantly lower than wrapper config, so add 10%
+        msg += " to at least " + (recMaxMem * 11 / 10 / (1024*1024)) + " (MB)" +
+               " if the actual share bandwidth exceeds " +
+               DataHelper.formatSize(threshKBps * 1024L) + "Bps.";
         System.out.println("WARN: " + msg);
         _context.logManager().getLog(BloomFilterIVValidator.class).logAlways(Log.WARN, msg);
     }
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 15c90b092ca21390e2ed24026097e25f5c173c69..ca3caa1b7185734e716c978f553c8afe8cdd0fde 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
@@ -25,9 +25,23 @@ import net.i2p.util.VersionComparator;
  */
 abstract class BuildRequestor {
     private static final List<Integer> ORDER = new ArrayList<Integer>(TunnelBuildMessage.MAX_RECORD_COUNT);
+    private static final String MIN_VARIABLE_VERSION = "0.7.12";
+    private static final boolean SEND_VARIABLE = true;
+    private static final int SHORT_RECORDS = 4;
+    private static final List<Integer> SHORT_ORDER = new ArrayList<Integer>(SHORT_RECORDS);
+    /** 5 (~2600 bytes) fits nicely in 3 tunnel messages */
+    private static final int MEDIUM_RECORDS = 5;
+    private static final List<Integer> MEDIUM_ORDER = new ArrayList<Integer>(MEDIUM_RECORDS);
     static {
-        for (int i = 0; i < TunnelBuildMessage.MAX_RECORD_COUNT; i++)
+        for (int i = 0; i < TunnelBuildMessage.MAX_RECORD_COUNT; i++) {
             ORDER.add(Integer.valueOf(i));
+        }
+        for (int i = 0; i < SHORT_RECORDS; i++) {
+            SHORT_ORDER.add(Integer.valueOf(i));
+        }
+        for (int i = 0; i < MEDIUM_RECORDS; i++) {
+            MEDIUM_ORDER.add(Integer.valueOf(i));
+        }
     }
 
     private static final int PRIORITY = OutNetMessage.PRIORITY_MY_BUILD_REQUEST;
@@ -223,17 +237,6 @@ abstract class BuildRequestor {
         //              + "ms and dispatched in " + (System.currentTimeMillis()-beforeDispatch));
         return true;
     }
-    
-    private static final String MIN_VARIABLE_VERSION = "0.7.12";
-    /** change this to true in 0.7.13 if testing goes well */
-    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 List<Integer> SHORT_ORDER = new ArrayList<Integer>(SHORT_RECORDS);
-    static {
-        for (int i = 0; i < SHORT_RECORDS; i++)
-            SHORT_ORDER.add(Integer.valueOf(i));
-    }
 
     /** @since 0.7.12 */
     private static boolean supportsVariable(RouterContext ctx, Hash h) {
@@ -256,7 +259,7 @@ abstract class BuildRequestor {
         Log log = ctx.logManager().getLog(BuildRequestor.class);
         long replyTunnel = 0;
         Hash replyRouter = null;
-        boolean useVariable = SEND_VARIABLE && cfg.getLength() <= SHORT_RECORDS;
+        boolean useVariable = SEND_VARIABLE && cfg.getLength() <= MEDIUM_RECORDS;
         if (cfg.isInbound()) {
             //replyTunnel = 0; // as above
             replyRouter = ctx.routerHash();
@@ -295,10 +298,13 @@ abstract class BuildRequestor {
         TunnelBuildMessage msg;
         List<Integer> order;
         if (useVariable) {
-            msg = new VariableTunnelBuildMessage(ctx, SHORT_RECORDS);
-            order = new ArrayList<Integer>(SHORT_ORDER);
-            //if (log.shouldLog(Log.INFO))
-            //    log.info("Using new VTBM");
+            if (cfg.getLength() <= SHORT_RECORDS) {
+                msg = new VariableTunnelBuildMessage(ctx, SHORT_RECORDS);
+                order = new ArrayList<Integer>(SHORT_ORDER);
+            } else {
+                msg = new VariableTunnelBuildMessage(ctx, MEDIUM_RECORDS);
+                order = new ArrayList<Integer>(MEDIUM_ORDER);
+            }
         } else {
             msg = new TunnelBuildMessage(ctx);
             order = new ArrayList<Integer>(ORDER);
diff --git a/router/java/src/net/i2p/router/util/MaskedIPSet.java b/router/java/src/net/i2p/router/util/MaskedIPSet.java
new file mode 100644
index 0000000000000000000000000000000000000000..eaf3b6e7e3ccf14af8ed52dcb16840ba440d6b1e
--- /dev/null
+++ b/router/java/src/net/i2p/router/util/MaskedIPSet.java
@@ -0,0 +1,106 @@
+package net.i2p.router.util;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import net.i2p.data.DataHelper;
+import net.i2p.data.Hash;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterInfo;
+import net.i2p.router.RouterContext;
+
+/**
+ *  Used for detection of routers with matching IPs or family.
+ *  Moved out of ProfileOrganizer for use in netdb also.
+ *
+ *  @since 0.9.28
+ */
+public class MaskedIPSet extends HashSet<String> {
+
+    public MaskedIPSet() {
+        super();
+    }
+
+    public MaskedIPSet(int initialCapacity) {
+        super(initialCapacity);
+    }
+
+    /**
+      * The Set of IPs for this peer, with a given mask.
+      * Includes the comm system's record of the IP, and all netDb addresses.
+      *
+      * As of 0.9.24, returned set will include netdb family as well.
+      *
+      * @param peer non-null
+      * @param mask is 1-4 (number of bytes to match)
+      * @return an opaque set of masked IPs for this peer
+      */
+    public MaskedIPSet(RouterContext ctx, Hash peer, int mask) {
+        this(ctx, ctx.netDb().lookupRouterInfoLocally(peer), mask);
+    }
+
+    /**
+      * The Set of IPs for this peer, with a given mask.
+      * Includes the comm system's record of the IP, and all netDb addresses.
+      *
+      * As of 0.9.24, returned set will include netdb family as well.
+      *
+      * @param pinfo may be null
+      * @param mask is 1-4 (number of bytes to match)
+      * @return an opaque set of masked IPs for this peer
+      */
+    public MaskedIPSet(RouterContext ctx, RouterInfo pinfo, int mask) {
+        super(4);
+        if (pinfo == null)
+            return;
+        byte[] commIP = ctx.commSystem().getIP(pinfo.getHash());
+        if (commIP != null)
+            add(maskedIP(commIP, mask));
+        Collection<RouterAddress> paddr = pinfo.getAddresses();
+        for (RouterAddress pa : paddr) {
+            byte[] pib = pa.getIP();
+            if (pib == null) continue;
+            add(maskedIP(pib, mask));
+        }
+        String family = pinfo.getOption("family");
+        if (family != null) {
+            // TODO should KNDF put a family-verified indicator in the RI,
+            // after checking the sig, or does it matter?
+            // What's the threat here of not avoid ding a router
+            // falsely claiming to be in the family?
+            // Prefix with something so an IP can't be spoofed
+            add('x' + family);
+        }
+    }
+
+    /**
+     * generate an arbitrary unique value for this ip/mask (mask = 1-4)
+     * If IPv6, force mask = 6.
+     * @param mask is 1-4 (number of bytes to match)
+     */
+    private static String maskedIP(byte[] ip, int mask) {
+        final StringBuilder buf = new StringBuilder(1 + (mask*2));
+        final char delim;
+        if (ip.length == 16) {
+            mask = 6;
+            delim = ':';
+        } else {
+            delim = '.';
+        }
+        buf.append(delim);
+        buf.append(Long.toHexString(DataHelper.fromLong(ip, 0, mask)));
+        return buf.toString();
+    }
+
+    /** does this contain any of the elements in b? */
+    public boolean containsAny(Set<String> b) {
+        if (isEmpty() || b.isEmpty())
+            return false;
+        for (String s : b) {
+            if (contains(s))
+                return true;
+        }
+        return false;
+    }
+}
diff --git a/router/java/src/org/cybergarage/http/HTTPHeader.java b/router/java/src/org/cybergarage/http/HTTPHeader.java
index 9f3849b0af1f1e18011fd1145b9226c71f48e593..cfb1082d21f920f6b054ac427e08dc053b2e17f0 100644
--- a/router/java/src/org/cybergarage/http/HTTPHeader.java
+++ b/router/java/src/org/cybergarage/http/HTTPHeader.java
@@ -116,6 +116,9 @@ public class HTTPHeader
 
 	public final static String getValue(String data, String name)
 	{
+		// I2P #1480 avoid IAE
+		if (data.length() <= 0)
+			return "";
 		/* Thanks for Stephan Mehlhase (2010-10-26) */
 		StringReader strReader = new StringReader(data);
 		LineNumberReader lineReader = new LineNumberReader(strReader, Math.min(data.length(), MAX_LENGTH));
diff --git a/router/java/src/org/cybergarage/upnp/ssdp/HTTPUSocket.java b/router/java/src/org/cybergarage/upnp/ssdp/HTTPUSocket.java
index c21f452e3d457024a933b7b8cf04c73ca7f5c8e0..65f8b7302a3cb5dea29be6e5dad764078bc8e796 100644
--- a/router/java/src/org/cybergarage/upnp/ssdp/HTTPUSocket.java
+++ b/router/java/src/org/cybergarage/upnp/ssdp/HTTPUSocket.java
@@ -92,6 +92,9 @@ public class HTTPUSocket
 	{
 		if (0 < localAddr.length())
 			return localAddr;
+		// I2P prevent NPE #1681
+		if (ssdpUniSock == null)
+			return "";
 		return ssdpUniSock.getLocalAddress().getHostAddress();
 	}