diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index da60c89b03276841c63379d793a4325d7424e6c9..dd52b10ad72d6a73dd1967f0e88cd08ca964d9f8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -52,6 +52,7 @@ test:ant:
         - echo junit.home=/usr/share/java >> override.properties
         - echo hamcrest.home=/usr/share/java >> override.properties
         - echo mockito.home=/usr/share/java >> override.properties
+        - echo build.built-by=GitHub Actions >> override.properties
     script:
         - ant test
     only:
diff --git a/Dockerfile b/Dockerfile
index f4fedaea5602ab8d383657bfbcaabeaba4ae3f2b..31da41399d12994d5eee5045d73c4270565e39b8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,6 +6,8 @@ WORKDIR /tmp/build
 COPY . .
 
 RUN apk add --virtual build-base gettext tar bzip2 apache-ant openjdk17 \
+    && echo "build.built-by=Docker" >> override.properties \
+
     && ant preppkg-linux-only \
     && rm -rf pkg-temp/osid pkg-temp/lib/wrapper pkg-temp/lib/wrapper.* \
     && apk del build-base gettext tar bzip2 apache-ant openjdk17
@@ -13,7 +15,8 @@ RUN apk add --virtual build-base gettext tar bzip2 apache-ant openjdk17 \
 FROM alpine:latest
 ENV APP_HOME="/i2p"
 
-RUN apk add openjdk17-jre
+RUN apk add openjdk17-jre ttf-dejavu
+
 WORKDIR ${APP_HOME}
 COPY --from=builder /tmp/build/pkg-temp .
 
diff --git a/apps/addressbook/build.xml b/apps/addressbook/build.xml
index b02dc2409ea07961dcec10de6652d41f5c030593..4f165ab3805e8c09065318e5b914650c3f3cbef6 100644
--- a/apps/addressbook/build.xml
+++ b/apps/addressbook/build.xml
@@ -84,28 +84,36 @@
             </jar>
         </target>
 	
+	<!-- actually the jar -->
 	<target name="warUpToDate">
-	        <uptodate property="war.uptodate" targetfile="${dist}/${war}">
-		            <srcfiles dir= "." includes="${build}/**/*.class, web.xml"/>
+	        <uptodate property="war.uptodate" targetfile="${dist}/${jar}">
+		            <srcfiles dir= "." includes="${build}/**/*.class"/>
 	        </uptodate>
                 <condition property="shouldListChanges" >
                     <and>
                         <not>
                             <isset property="war.uptodate" />
                         </not>
-                        <isset property="mtn.available" />
+                        <isset property="git.available" />
                     </and>
                 </condition>
 	</target>
 
 	<target name="changes" depends="warUpToDate" if="shouldListChanges" >
-	        <exec executable="mtn" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
-	            <arg value="list" />
-	            <arg value="changed" />
-	            <arg value="." />
-	        </exec>
+		<exec executable="git" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
+		    <arg value="status" />
+		    <arg value="-s" />
+		    <arg value="--porcelain" />
+		    <arg value="-uno" />
+		    <arg value="." />
+		</exec>
+		<!-- trim flags -->
+		<exec executable="sed" inputstring="${workspace.changes}" outputproperty="workspace.changes.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+		    <arg value="-e" />
+		    <arg value="s/^[MTADRCU ]*//" />
+		</exec>
 		<!-- \n in an attribute value generates an invalid manifest -->
-		<exec executable="tr" inputstring="${workspace.changes}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+		<exec executable="tr" inputstring="${workspace.changes.sed}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
 			<arg value="-s" />
 			<arg value="[:space:]" />
 			<arg value="," />
diff --git a/apps/desktopgui/build.xml b/apps/desktopgui/build.xml
index 6487bfceac9944d70b19171866d7e7394956c0e4..baf8d04c14e35d30aa9b2ecab0cb676af386d177 100644
--- a/apps/desktopgui/build.xml
+++ b/apps/desktopgui/build.xml
@@ -64,17 +64,24 @@
         </target>
 
         <target name="listChangedFiles" depends="jarUpToDate" if="shouldListChanges" >
-        <exec executable="mtn" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
-            <arg value="list" />
-            <arg value="changed" />
-            <arg value="." />
-        </exec>
-        <!-- \n in an attribute value generates an invalid manifest -->
-        <exec executable="tr" inputstring="${workspace.changes}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
-            <arg value="-s" />
-            <arg value="[:space:]" />
-            <arg value="," />
-        </exec>
+            <exec executable="git" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
+                <arg value="status" />
+                <arg value="-s" />
+                <arg value="--porcelain" />
+                <arg value="-uno" />
+                <arg value="." />
+            </exec>
+            <!-- trim flags -->
+            <exec executable="sed" inputstring="${workspace.changes}" outputproperty="workspace.changes.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+                <arg value="-e" />
+                <arg value="s/^[MTADRCU ]*//" />
+            </exec>
+            <!-- \n in an attribute value generates an invalid manifest -->
+            <exec executable="tr" inputstring="${workspace.changes.sed}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+                <arg value="-s" />
+                <arg value="[:space:]" />
+                <arg value="," />
+            </exec>
 	</target>
 
 	<target name="jar" depends="compile, bundle, listChangedFiles" unless="jar.uptodate" >
@@ -108,7 +115,7 @@
                 <not>
                     <isset property="jar.uptodate" />
                 </not>
-                <isset property="mtn.available" />
+                <isset property="git.available" />
             </and>
         </condition>
     </target>
diff --git a/apps/i2pcontrol/build.xml b/apps/i2pcontrol/build.xml
index cc24c7e74fc23b46b3358b36fbc83841dc5fad99..d3e4883da10ca3368db57325fbcc5b792da1f7e3 100644
--- a/apps/i2pcontrol/build.xml
+++ b/apps/i2pcontrol/build.xml
@@ -92,7 +92,29 @@
         </javac>
     </target>
 
-    <target name="jar" depends="compile">
+    <target name="listChangedFiles" if="git.available" >
+        <exec executable="git" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="status" />
+            <arg value="-s" />
+            <arg value="--porcelain" />
+            <arg value="-uno" />
+            <arg value="." />
+            <arg value="../resources" />
+        </exec>
+        <!-- trim flags -->
+        <exec executable="sed" inputstring="${workspace.changes}" outputproperty="workspace.changes.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-e" />
+            <arg value="s/^[MTADRCU ]*//" />
+        </exec>
+        <!-- \n in an attribute value generates an invalid manifest -->
+        <exec executable="tr" inputstring="${workspace.changes.sed}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-s" />
+            <arg value="[:space:]" />
+            <arg value="," />
+        </exec>
+    </target>
+
+    <target name="jar" depends="compile, listChangedFiles">
         <!-- set if unset -->
         <property name="workspace.changes.tr" value="" />
         <jar destfile="build/i2pcontrol.jar" basedir="./build/obj" includes="**/*.class" >
@@ -108,7 +130,7 @@
         </jar>
     </target>    
 
-    <target name="socketJar" depends="compileSocketJar">
+    <target name="socketJar" depends="compileSocketJar, listChangedFiles">
         <!-- set if unset -->
         <property name="workspace.changes.tr" value="" />
         <jar destfile="build/i2pcontrol.jar" basedir="./build/obj" includes="**/*.class" >
@@ -124,7 +146,7 @@
         </jar>
     </target>    
 
-    <target name="war" depends="compile" >
+    <target name="war" depends="compile, listChangedFiles" >
         <!-- set if unset -->
         <property name="workspace.changes.tr" value="" />
         <war destfile="build/jsonrpc.war" webxml="web.xml" >
diff --git a/apps/i2psnark/java/build.xml b/apps/i2psnark/java/build.xml
index b06593af08f0d96e488bd7bf7be20a4e14fc0815..edde6bf451ce1ca4337bd5bcc6cd91ac2bf78666 100644
--- a/apps/i2psnark/java/build.xml
+++ b/apps/i2psnark/java/build.xml
@@ -68,13 +68,20 @@
     </target>
 
     <target name="listChangedFiles" depends="jarUpToDate" if="shouldListChanges" >
-        <exec executable="mtn" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
-            <arg value="list" />
-            <arg value="changed" />
+        <exec executable="git" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="status" />
+            <arg value="-s" />
+            <arg value="--porcelain" />
+            <arg value="-uno" />
             <arg value=".." />
         </exec>
+        <!-- trim flags -->
+        <exec executable="sed" inputstring="${workspace.changes}" outputproperty="workspace.changes.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-e" />
+            <arg value="s/^[MTADRCU ]*//" />
+        </exec>
         <!-- \n in an attribute value generates an invalid manifest -->
-        <exec executable="tr" inputstring="${workspace.changes}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+        <exec executable="tr" inputstring="${workspace.changes.sed}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
             <arg value="-s" />
             <arg value="[:space:]" />
             <arg value="," />
@@ -111,7 +118,7 @@
                 <not>
                     <isset property="war.uptodate" />
                 </not>
-                <isset property="mtn.available" />
+                <isset property="git.available" />
             </and>
         </condition>
     </target>    
diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java
index 412663b6607fa5a56b0d04aeca5f1bcef9adcc81..f51ecf64b4002d91a4030144f724130d35242ed1 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java
@@ -77,6 +77,7 @@ public class I2PSnarkUtil implements DisconnectListener {
     private DHT _dht;
     private long _startedTime;
     private final DisconnectListener _discon;
+    private int _maxFilesPerTorrent = SnarkManager.DEFAULT_MAX_FILES_PER_TORRENT;
 
     private static final int EEPGET_CONNECT_TIMEOUT = 45*1000;
     private static final int EEPGET_CONNECT_TIMEOUT_SHORT = 5*1000;
@@ -242,6 +243,11 @@ public class I2PSnarkUtil implements DisconnectListener {
     /** @since 0.9.1 */
     public File getTempDir() { return _tmpDir; }
 
+    /** @since 0.9.58 */
+    public int getMaxFilesPerTorrent() { return _maxFilesPerTorrent; }
+    /** @since 0.9.58 */
+    public void setMaxFilesPerTorrent(int max) { _maxFilesPerTorrent = Math.max(max, 1); }
+
     /**
      * Connect to the router, if we aren't already
      */
diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
index e703cb9f8eb0c8fc128f679a195867a9c4eba373..d80366ca2e6a8e0307bfefbd1e239d5c47db702f 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
@@ -129,7 +129,7 @@ public class SnarkManager implements CompleteListener, ClientApp, DisconnectList
     private static final String PROP_META_ACTIVITY = "activity";
 
     private static final String CONFIG_FILE_SUFFIX = ".config";
-    private static final String CONFIG_FILE = "i2psnark" + CONFIG_FILE_SUFFIX;
+    public static final String CONFIG_FILE = "i2psnark" + CONFIG_FILE_SUFFIX;
     private static final String COMMENT_FILE_SUFFIX = ".comments.txt.gz";
     public static final String PROP_FILES_PUBLIC = "i2psnark.filesPublic";
     public static final String PROP_OLD_AUTO_START = "i2snark.autoStart";   // oops
@@ -164,6 +164,8 @@ public class SnarkManager implements CompleteListener, ClientApp, DisconnectList
     private static final String PROP_COMMENTS = "i2psnark.comments";
     /** @since 0.9.31 */
     private static final String PROP_COMMENTS_NAME = "i2psnark.commentsName";
+    /** @since 0.9.58 */
+    public static final String PROP_MAX_FILES_PER_TORRENT = "i2psnark.maxFilesPerTorrent";
 
     public static final int MIN_UP_BW = 10;
     public static final int DEFAULT_MAX_UP_BW = 25;
@@ -171,6 +173,7 @@ public class SnarkManager implements CompleteListener, ClientApp, DisconnectList
     public static final int DEFAULT_REFRESH_DELAY_SECS = 15;
     private static final int DEFAULT_PAGE_SIZE = 50;
     public static final int DEFAULT_TUNNEL_QUANTITY = 3;
+    public static final int DEFAULT_MAX_FILES_PER_TORRENT = 2000;
     public static final String CONFIG_DIR_SUFFIX = ".d";
     private static final String SUBDIR_PREFIX = "s";
     private static final String B64 = Base64.ALPHABET_I2P;
@@ -1000,6 +1003,7 @@ public class SnarkManager implements CompleteListener, ClientApp, DisconnectList
         //    _util.setProxy(eepHost, eepPort);
         _util.setMaxUploaders(getInt(PROP_UPLOADERS_TOTAL, Snark.MAX_TOTAL_UPLOADERS));
         _util.setMaxUpBW(getInt(PROP_UPBW_MAX, DEFAULT_MAX_UP_BW));
+        _util.setMaxFilesPerTorrent(getInt(PROP_MAX_FILES_PER_TORRENT, DEFAULT_MAX_FILES_PER_TORRENT));
         _util.setStartupDelay(getInt(PROP_STARTUP_DELAY, DEFAULT_STARTUP_DELAY));
         _util.setFilesPublic(areFilesPublic());
         _util.setOpenTrackers(getListConfig(PROP_OPENTRACKERS, DEFAULT_OPENTRACKERS));
@@ -1479,9 +1483,6 @@ public class SnarkManager implements CompleteListener, ClientApp, DisconnectList
         }
     }
     
-    /** hardcoded for sanity.  perhaps this should be customizable, for people who increase their ulimit, etc. */
-    public static final int MAX_FILES_PER_TORRENT = 2000;
-    
     /**
      *  Set of canonical .torrent filenames that we are dealing with.
      *  An unsynchronized copy.
@@ -2450,8 +2451,11 @@ public class SnarkManager implements CompleteListener, ClientApp, DisconnectList
      */
     private String validateTorrent(MetaInfo info) {
         List<List<String>> files = info.getFiles();
-        if ( (files != null) && (files.size() > MAX_FILES_PER_TORRENT) ) {
-            return _t("Too many files in \"{0}\" ({1})!", info.getName(), files.size());
+        if (files != null && files.size() > _util.getMaxFilesPerTorrent()) {
+            return _t("Too many files in \"{0}\" ({1})!", info.getName(), files.size()) +
+                   " - limit is " + _util.getMaxFilesPerTorrent() + ", zip them or set " +
+                   PROP_MAX_FILES_PER_TORRENT + '=' + files.size() + " in " +
+                   _configFile.getAbsolutePath() + " and restart";
         } else if ( (files == null) && (info.getName().endsWith(".torrent")) ) {
             return _t("Torrent file \"{0}\" cannot end in \".torrent\"!", info.getName());
         } else if (info.getPieces() <= 0) {
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Storage.java b/apps/i2psnark/java/src/org/klomp/snark/Storage.java
index e74cecb50e5f024d7a2cc6fbe98a66309a31b05c..5ae3e27fca2f1cad7796dcff434fc7111e96c2b7 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/Storage.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/Storage.java
@@ -288,9 +288,13 @@ public class Storage implements Closeable
    *  @throws IOException if too many total files
    */
   private void addFiles(List<File> l, File f) throws IOException {
+    int max = _util.getMaxFilesPerTorrent();
     if (!f.isDirectory()) {
-        if (l.size() >= SnarkManager.MAX_FILES_PER_TORRENT)
-            throw new IOException("Too many files, limit is " + SnarkManager.MAX_FILES_PER_TORRENT + ", zip them?");
+        if (l.size() >= max)
+            throw new IOException(_util.getString("Too many files in \"{0}\" ({1})!", metainfo.getName(), l.size()) +
+                                  " - limit is " + max + ", zip them or set " +
+                                  SnarkManager.PROP_MAX_FILES_PER_TORRENT + '=' + l.size() + " in " +
+                                  SnarkManager.CONFIG_FILE + " and restart");
         l.add(f);
     } else {
         File[] files = f.listFiles();
diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
index 80b268ac1085c993010401bf60372d73695b8477..98247aa7486b28aab66bce205200079f6411c222 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
@@ -326,7 +326,8 @@ public class I2PSnarkServlet extends BasicServlet {
             out.write("<script src=\".resources/js/configui.js?" + CoreVersion.VERSION + "\" type=\"text/javascript\"></script>\n");
         } else {
             delay = _manager.getRefreshDelaySeconds();
-            if (delay > 0) {
+            // init for search even if refresh disabled
+            //if (delay > 0) {
                 String jsPfx = _context.isRouterContext() ? "" : ".resources";
                 String downMsg = _context.isRouterContext() ? _t("Router is down") : _t("I2PSnark has stopped");
                 // fallback to metarefresh when javascript is disabled
@@ -337,7 +338,7 @@ public class I2PSnarkServlet extends BasicServlet {
                           "var ajaxDelay = " + (delay * 1000) + ";\n" +
                           "</script>\n" +
                           "<script src=\".resources/js/initajax.js?" + CoreVersion.VERSION + "\" type=\"text/javascript\"></script>\n");
-            }
+            //}
             out.write("<script nonce=\"" + cspNonce + "\" type=\"text/javascript\">\n"  +
                       "var deleteMessage1 = \"" + _t("Are you sure you want to delete the file \\''{0}\\'' (downloaded data will not be deleted) ?") + "\";\n" +
                       "var deleteMessage2 = \"" + _t("Are you sure you want to delete the torrent \\''{0}\\'' and all downloaded data?") + "\";\n" +
@@ -394,7 +395,7 @@ public class I2PSnarkServlet extends BasicServlet {
                 if (search != null)
                     out.write(" value=\"" + DataHelper.escapeHTML(search) + '"');
                 out.write(">" +
-                          "<input type=\"reset\" class=\"cancel\" id=\"searchcancel\" value=\"\">" +
+                          "<a class=\"cancel\" id=\"searchcancel\" href=\"" + _contextPath + "/\"></a>" +
                           "</form>\n");
             }
         }
diff --git a/apps/i2psnark/launch-i2psnark b/apps/i2psnark/launch-i2psnark
index 2cbd9390088007c7de524fa9829de578ad80ecb0..c72de0b27970348a6da17892b86b8e289241d105 100755
--- a/apps/i2psnark/launch-i2psnark
+++ b/apps/i2psnark/launch-i2psnark
@@ -35,5 +35,6 @@ raiseopenfilesulimit() {
 
 raiseopenfilesulimit
 
-I2P="."
-java $JAVA_OPTS -jar "$I2P/i2psnark.jar"
+I2P="`dirname $0`"
+cd "$I2P"
+java $JAVA_OPTS -jar i2psnark.jar
diff --git a/apps/i2psnark/resources/js/initajax.js b/apps/i2psnark/resources/js/initajax.js
index 7697b0086e02dc13f51c739f42a3450e2f105033..a5db3ac527211629012879c31052432ce54340fb 100644
--- a/apps/i2psnark/resources/js/initajax.js
+++ b/apps/i2psnark/resources/js/initajax.js
@@ -55,7 +55,9 @@ function requestAjax2(refreshtime) {
 }
 
 function initAjax() {
-    setTimeout(requestAjax1, ajaxDelay);
+    if (ajaxDelay > 0) {
+        setTimeout(requestAjax1, ajaxDelay);
+    }
 }
 
 document.addEventListener("DOMContentLoaded", function() {
diff --git a/apps/i2psnark/resources/js/search.js b/apps/i2psnark/resources/js/search.js
index 67339a5e778ac33879d11f00e2f06954576cba2d..b719ddf4e2b96e6a6bd49c301c13e38164458470 100644
--- a/apps/i2psnark/resources/js/search.js
+++ b/apps/i2psnark/resources/js/search.js
@@ -11,17 +11,32 @@ function initSearch()
 	var sch = document.getElementById("search");
 	if (sch != null) {
 		var box = document.getElementById("searchbox");
-		sch.addEventListener("reset", function(event) {
+		var cxl = document.getElementById("searchcancel");
+		cxl.addEventListener("click", function(event) {
 			if (box.value !== "") {
 				box.value = "";
 				requestAjax2(-1);
 			}
+			cxl.classList.add("disabled");
 			event.preventDefault();
 		});
 
 		box.addEventListener("input", function(event) {
+			if (box.value !== "") {
+				cxl.classList.remove("disabled");
+			} else {
+				cxl.classList.add("disabled");
+			}
 			requestAjax2(-1);
 		});
+
+		if (box.value !== "") {
+			cxl.classList.remove("disabled");
+		} else {
+			cxl.classList.add("disabled");
+		}
+                // so we don't get the link popup
+		cxl.removeAttribute("href");
 	}
 }
 
diff --git a/apps/i2psnark/resources/themes/dark/snark.css b/apps/i2psnark/resources/themes/dark/snark.css
index f392273d97dd32e718cca600a4ee1d821e6a6bf0..7394bbd705a7278e07176ee371a190c3bcf5e310 100644
--- a/apps/i2psnark/resources/themes/dark/snark.css
+++ b/apps/i2psnark/resources/themes/dark/snark.css
@@ -227,24 +227,33 @@ _:-ms-lang(x), .snarknavbar {
 #search {
      display: inline-block;
      position: absolute;
-     top: 6px;
-     right: 3px;
+     top: 16px;
+     right: 9px;
 }
 
 #searchbox {
-     background: #f60 url(/themes/console/images/buttons/search.png) 7px center no-repeat !important;
+     background: #000 url(/themes/console/images/buttons/search.png) 7px center no-repeat !important;
      margin: 2px 4px 2px 24px !important;
      padding: 4px 32px 4px 32px !important;
-     color: black;
+     color: #bb7;
+}
+
+#searchbox:focus, #searchbox:active {
+     color: #ee9;
 }
 
 #searchcancel {
-     background: url(images/cancel.png);
-     margin: 2px 4px 2px -28px;
+     background: url(images/delete.png) 0px center no-repeat;
+     margin: 2px 4px 2px -20px;
+     padding: 0px 6px;
      color: transparent;
      border: none;
 }
 
+#searchcancel.disabled {
+     display: none;
+}
+
 /* end topnav */
 
 /* screenlog */
diff --git a/apps/i2psnark/resources/themes/light/snark.css b/apps/i2psnark/resources/themes/light/snark.css
index 18134c4146da76fd04e5fc80fedb6a03c62530d3..fad322d4ad409aa816f505c003824f733a40970d 100644
--- a/apps/i2psnark/resources/themes/light/snark.css
+++ b/apps/i2psnark/resources/themes/light/snark.css
@@ -232,24 +232,33 @@ button::-moz-focus-inner, input::-moz-focus-inner {
 #search {
      display: inline-block;
      position: absolute;
-     top: 6px;
-     right: 3px;
+     top: 10px;
+     right: 6px;
 }
 
 #searchbox {
-     background: #f60 url(/themes/console/images/buttons/search.png) 7px center no-repeat !important;
+     background: #f8f8ff url(/themes/console/images/buttons/search.png) 7px center no-repeat !important;
      margin: 2px 4px 2px 24px !important;
      padding: 4px 32px 4px 32px !important;
-     color: black;
+     color: #47475f;
+}
+
+#searchbox:focus, #searchbox:active {
+     color: #19191f;
 }
 
 #searchcancel {
-     background: url(images/cancel.png);
-     margin: 2px 4px 2px -28px;
+     background: url(images/delete.png) 0px center no-repeat;
+     margin: 2px 4px 2px 4px;
+     padding: 0px 6px;
      color: transparent;
      border: none;
 }
 
+#searchcancel.disabled {
+     display: none;
+}
+
 /* end top nav */
 
 /* screenlog */
diff --git a/apps/i2psnark/resources/themes/ubergine/snark.css b/apps/i2psnark/resources/themes/ubergine/snark.css
index e3afeb55d5a32536b244b8c5efc3ab23519c019d..f1874006c05d554bdde287187ad7ee568bd408b8 100644
--- a/apps/i2psnark/resources/themes/ubergine/snark.css
+++ b/apps/i2psnark/resources/themes/ubergine/snark.css
@@ -237,19 +237,29 @@ _:-ms-lang(x), .snarkNav:last-child[href="/i2psnark/"] {
 }
 
 #searchbox {
-     background: #f60 url(/themes/console/images/buttons/search.png) 7px center no-repeat !important;
+     background: #212 url(/themes/console/images/buttons/search.png) 7px center no-repeat !important;
      margin: 2px 4px 2px 24px !important;
      padding: 4px 32px 4px 32px !important;
-     color: black;
+     color: #f60;
+}
+
+#searchbox:focus, #searchbox:active {
+     background: #f60 url(/themes/console/images/buttons/search.png) 7px center no-repeat !important;
+     color: #fff;
 }
 
 #searchcancel {
-     background: url(images/cancel.png);
+     background: url(images/cancel.png) no-repeat;
      margin: 2px 4px 2px -28px;
+     padding: 0px 12px;
      color: transparent;
      border: none;
 }
 
+#searchcancel.disabled {
+     display: none;
+}
+
 /* end topnav */
 
 /* screenlogger */
diff --git a/apps/i2psnark/resources/themes/vanilla/snark.css b/apps/i2psnark/resources/themes/vanilla/snark.css
index 1a70c5cac736040ec67d3fe65d40838d0884f34c..154a5ac8f740a07919157b9a6169d08b2b362908 100644
--- a/apps/i2psnark/resources/themes/vanilla/snark.css
+++ b/apps/i2psnark/resources/themes/vanilla/snark.css
@@ -275,24 +275,34 @@ _:-ms-lang(x), .snarkNav:link, .snarkNav:visited {
 #search {
      display: inline-block;
      position: absolute;
-     top: 6px;
+     top: 8px;
      right: 3px;
 }
 
 #searchbox {
-     background: #f60 url(/themes/console/images/buttons/search.png) 7px center no-repeat !important;
+     background: #efe6e0 url(/themes/console/images/buttons/search.png) 7px center no-repeat !important;
      margin: 2px 4px 2px 24px !important;
      padding: 4px 32px 4px 32px !important;
-     color: black;
+     color: #2f1500;
+}
+
+#searchbox:focus, #searchbox:active {
+     background: #fffcdf url(/themes/console/images/buttons/search.png) 7px center no-repeat !important;
+     color: #5f1227;
 }
 
 #searchcancel {
-     background: url(images/cancel.png);
-     margin: 2px 4px 2px -28px;
+     background: url(images/delete.png) 7px center no-repeat;
+     margin: 2px 4px 2px -30px;
+     padding: 0px 14px;
      color: transparent;
      border: none;
 }
 
+#searchcancel.disabled {
+     display: none;
+}
+
 /* end topnav */
 
 /* screenlog */
diff --git a/apps/i2ptunnel/java/build.xml b/apps/i2ptunnel/java/build.xml
index f6b6cae5cc450eefb05e710a613846fde40d4898..b8d8b24a993d14da4b98cd216f3e9b307ad5baab 100644
--- a/apps/i2ptunnel/java/build.xml
+++ b/apps/i2ptunnel/java/build.xml
@@ -68,13 +68,21 @@
     </target>
 
     <target name="listChangedFiles" depends="jarUpToDate" if="shouldListChanges" >
-        <exec executable="mtn" outputproperty="workspace.changes.j" errorproperty="mtn.error2" failifexecutionfails="false" >
-            <arg value="list" />
-            <arg value="changed" />
+        <exec executable="git" outputproperty="workspace.changes.j" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="status" />
+            <arg value="-s" />
+            <arg value="--porcelain" />
+            <arg value="-uno" />
             <arg value="." />
+            <arg value="../resources" />
+        </exec>
+        <!-- trim flags -->
+        <exec executable="sed" inputstring="${workspace.changes.j}" outputproperty="workspace.changes.j.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-e" />
+            <arg value="s/^[MTADRCU ]*//" />
         </exec>
         <!-- \n in an attribute value generates an invalid manifest -->
-        <exec executable="tr" inputstring="${workspace.changes.j}" outputproperty="workspace.changes.j.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+        <exec executable="tr" inputstring="${workspace.changes.j.sed}" outputproperty="workspace.changes.j.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
             <arg value="-s" />
             <arg value="[:space:]" />
             <arg value="," />
@@ -129,7 +137,7 @@
                 <not>
                     <isset property="jar.uptodate" />
                 </not>
-                <isset property="mtn.available" />
+                <isset property="git.available" />
             </and>
         </condition>
     </target>
@@ -162,7 +170,7 @@
                 <not>
                     <isset property="uiJar.uptodate" />
                 </not>
-                <isset property="mtn.available" />
+                <isset property="git.available" />
             </and>
         </condition>
     </target>
@@ -271,13 +279,20 @@
     </target>
 
     <target name="listChangedFiles2" depends="warUpToDate" if="shouldListChanges2" >
-        <exec executable="mtn" outputproperty="workspace.changes.w" errorproperty="mtn.error2" failifexecutionfails="false" >
-            <arg value="list" />
-            <arg value="changed" />
+        <exec executable="git" outputproperty="workspace.changes.w" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="status" />
+            <arg value="-s" />
+            <arg value="--porcelain" />
+            <arg value="-uno" />
             <arg value="../jsp" />
         </exec>
+        <!-- trim flags -->
+        <exec executable="sed" inputstring="${workspace.changes.w}" outputproperty="workspace.changes.w.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-e" />
+            <arg value="s/^[MTADRCU ]*//" />
+        </exec>
         <!-- \n in an attribute value generates an invalid manifest -->
-        <exec executable="tr" inputstring="${workspace.changes.w}" outputproperty="workspace.changes.w.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+        <exec executable="tr" inputstring="${workspace.changes.w.sed}" outputproperty="workspace.changes.w.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
             <arg value="-s" />
             <arg value="[:space:]" />
             <arg value="," />
@@ -318,7 +333,7 @@
                 <not>
                     <isset property="war.uptodate" />
                 </not>
-                <isset property="mtn.available" />
+                <isset property="git.available" />
             </and>
         </condition>
     </target>
diff --git a/apps/imagegen/identicon/build.xml b/apps/imagegen/identicon/build.xml
index fe735b4b548df909db546f7e85b6c8aa2de0ea04..7971176ca78a3a379ef105643046ffd97c8c0ee9 100644
--- a/apps/imagegen/identicon/build.xml
+++ b/apps/imagegen/identicon/build.xml
@@ -53,13 +53,20 @@
     </target>
 
     <target name="listChangedFiles" depends="jarUpToDate" if="shouldListChanges" >
-        <exec executable="mtn" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
-            <arg value="list" />
-            <arg value="changed" />
+        <exec executable="git" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="status" />
+            <arg value="-s" />
+            <arg value="--porcelain" />
+            <arg value="-uno" />
             <arg value="." />
         </exec>
+        <!-- trim flags -->
+        <exec executable="sed" inputstring="${workspace.changes}" outputproperty="workspace.changes.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-e" />
+            <arg value="s/^[MTADRCU ]*//" />
+        </exec>
         <!-- \n in an attribute value generates an invalid manifest -->
-        <exec executable="tr" inputstring="${workspace.changes}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+        <exec executable="tr" inputstring="${workspace.changes.sed}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
             <arg value="-s" />
             <arg value="[:space:]" />
             <arg value="," />
@@ -91,7 +98,7 @@
                 <not>
                     <isset property="jar.uptodate" />
                 </not>
-                <isset property="mtn.available" />
+                <isset property="git.available" />
             </and>
         </condition>
     </target>
diff --git a/apps/imagegen/imagegen/build.xml b/apps/imagegen/imagegen/build.xml
index 5b4eeeaa6f3f598fcc70b233bc6b5a3e118ee544..68cb47660bbc24c6fc12432ac5370482b3a5292f 100644
--- a/apps/imagegen/imagegen/build.xml
+++ b/apps/imagegen/imagegen/build.xml
@@ -46,20 +46,27 @@
     </target>
 
     <target name="listChangedFiles" if="shouldListChanges" >
-        <exec executable="mtn" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
-            <arg value="list" />
-            <arg value="changed" />
+        <exec executable="git" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="status" />
+            <arg value="-s" />
+            <arg value="--porcelain" />
+            <arg value="-uno" />
             <arg value="." />
         </exec>
+        <!-- trim flags -->
+        <exec executable="sed" inputstring="${workspace.changes}" outputproperty="workspace.changes.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-e" />
+            <arg value="s/^[MTADRCU ]*//" />
+        </exec>
         <!-- \n in an attribute value generates an invalid manifest -->
-        <exec executable="tr" inputstring="${workspace.changes}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+        <exec executable="tr" inputstring="${workspace.changes.sed}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
             <arg value="-s" />
             <arg value="[:space:]" />
             <arg value="," />
         </exec>
     </target>
 
-    <target name="war" depends="compile, warUpToDate" unless="war.uptodate" > 
+    <target name="war" depends="compile, warUpToDate, listChangedFiles" unless="war.uptodate" > 
         <!-- set if unset -->
         <property name="workspace.changes.tr" value="" />
         <!-- put the identicon and zxing classes in the war -->
@@ -93,7 +100,7 @@
                 <not>
                     <isset property="war.uptodate" />
                 </not>
-                <isset property="mtn.available" />
+                <isset property="git.available" />
             </and>
         </condition>
     </target>
diff --git a/apps/imagegen/zxing/build.xml b/apps/imagegen/zxing/build.xml
index 23c3d6e0af17b48bd663377c1e8d6336b6b4172a..128798b5c7d9871baa1974d39a00aee26919ba57 100644
--- a/apps/imagegen/zxing/build.xml
+++ b/apps/imagegen/zxing/build.xml
@@ -60,13 +60,20 @@
     </target>
 
     <target name="listChangedFiles" depends="jarUpToDate" if="shouldListChanges" >
-        <exec executable="mtn" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
-            <arg value="list" />
-            <arg value="changed" />
+        <exec executable="git" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="status" />
+            <arg value="-s" />
+            <arg value="--porcelain" />
+            <arg value="-uno" />
             <arg value="." />
         </exec>
+        <!-- trim flags -->
+        <exec executable="sed" inputstring="${workspace.changes}" outputproperty="workspace.changes.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-e" />
+            <arg value="s/^[MTADRCU ]*//" />
+        </exec>
         <!-- \n in an attribute value generates an invalid manifest -->
-        <exec executable="tr" inputstring="${workspace.changes}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+        <exec executable="tr" inputstring="${workspace.changes.sed}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
             <arg value="-s" />
             <arg value="[:space:]" />
             <arg value="," />
@@ -98,7 +105,7 @@
                 <not>
                     <isset property="jar.uptodate" />
                 </not>
-                <isset property="mtn.available" />
+                <isset property="git.available" />
             </and>
         </condition>
     </target>
diff --git a/apps/jetty/build.xml b/apps/jetty/build.xml
index 4bb881f20c4bf7c538458df5cf8b939398b1b7ab..f8efc9641ebdc3449eadb8c6007cbb0f3c72af31 100644
--- a/apps/jetty/build.xml
+++ b/apps/jetty/build.xml
@@ -424,13 +424,20 @@
     </target>
 
     <target name="listChangedFiles" depends="jarUpToDate" if="shouldListChanges" >
-        <exec executable="mtn" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
-            <arg value="list" />
-            <arg value="changed" />
+        <exec executable="git" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="status" />
+            <arg value="-s" />
+            <arg value="--porcelain" />
+            <arg value="-uno" />
             <arg value="." />
         </exec>
+        <!-- trim flags -->
+        <exec executable="sed" inputstring="${workspace.changes}" outputproperty="workspace.changes.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-e" />
+            <arg value="s/^[MTADRCU ]*//" />
+        </exec>
         <!-- \n in an attribute value generates an invalid manifest -->
-        <exec executable="tr" inputstring="${workspace.changes}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+        <exec executable="tr" inputstring="${workspace.changes.sed}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
             <arg value="-s" />
             <arg value="[:space:]" />
             <arg value="," />
@@ -469,7 +476,7 @@
                 <not>
                     <isset property="jar.uptodate" />
                 </not>
-                <isset property="mtn.available" />
+                <isset property="git.available" />
             </and>
         </condition>
     </target>    
diff --git a/apps/jrobin/java/build.xml b/apps/jrobin/java/build.xml
index 528fc0dfd1b7920c86b85ad9534b784fa1cabbea..b949e769dd2be48539be95fa75675b7b9fab9b6e 100644
--- a/apps/jrobin/java/build.xml
+++ b/apps/jrobin/java/build.xml
@@ -41,13 +41,20 @@
     </target>
 
     <target name="listChangedFiles" depends="jarUpToDate" if="shouldListChanges" >
-        <exec executable="mtn" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
-            <arg value="list" />
-            <arg value="changed" />
+        <exec executable="git" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="status" />
+            <arg value="-s" />
+            <arg value="--porcelain" />
+            <arg value="-uno" />
             <arg value="." />
         </exec>
+        <!-- trim flags -->
+        <exec executable="sed" inputstring="${workspace.changes}" outputproperty="workspace.changes.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-e" />
+            <arg value="s/^[MTADRCU ]*//" />
+        </exec>
         <!-- \n in an attribute value generates an invalid manifest -->
-        <exec executable="tr" inputstring="${workspace.changes}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+        <exec executable="tr" inputstring="${workspace.changes.sed}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
             <arg value="-s" />
             <arg value="[:space:]" />
             <arg value="," />
@@ -79,7 +86,7 @@
                 <not>
                     <isset property="jar.uptodate" />
                 </not>
-                <isset property="mtn.available" />
+                <isset property="git.available" />
             </and>
         </condition>
     </target>
diff --git a/apps/ministreaming/java/build.xml b/apps/ministreaming/java/build.xml
index c7224b3ba7f513343b4fdc2a6cd2a6efbfc9dd6a..72944b25ce05180b3999090ea193894160b0eb8b 100644
--- a/apps/ministreaming/java/build.xml
+++ b/apps/ministreaming/java/build.xml
@@ -64,13 +64,20 @@
     </target>
 
     <target name="listChangedFiles" depends="jarUpToDate" if="shouldListChanges" >
-        <exec executable="mtn" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
-            <arg value="list" />
-            <arg value="changed" />
+        <exec executable="git" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="status" />
+            <arg value="-s" />
+            <arg value="--porcelain" />
+            <arg value="-uno" />
             <arg value="." />
         </exec>
+        <!-- trim flags -->
+        <exec executable="sed" inputstring="${workspace.changes}" outputproperty="workspace.changes.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-e" />
+            <arg value="s/^[MTADRCU ]*//" />
+        </exec>
         <!-- \n in an attribute value generates an invalid manifest -->
-        <exec executable="tr" inputstring="${workspace.changes}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+        <exec executable="tr" inputstring="${workspace.changes.sed}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
             <arg value="-s" />
             <arg value="[:space:]" />
             <arg value="," />
@@ -108,7 +115,7 @@
                 <not>
                     <isset property="jar.uptodate" />
                 </not>
-                <isset property="mtn.available" />
+                <isset property="git.available" />
             </and>
         </condition>
     </target>
diff --git a/apps/routerconsole/java/build.xml b/apps/routerconsole/java/build.xml
index 65cddc0728e3c73eb2cfd9d008d44bb27a9fc6be..2c3fe7b616c03df55ea815c039612cf519033cc4 100644
--- a/apps/routerconsole/java/build.xml
+++ b/apps/routerconsole/java/build.xml
@@ -117,14 +117,21 @@
 
     <!-- the jar without the latest message classes from the jsps -->
     <target name="listChangedFiles" depends="jarUpToDate" if="shouldListChanges" >
-        <exec executable="mtn" outputproperty="workspace.changes.j" errorproperty="mtn.error2" failifexecutionfails="false" >
-            <arg value="list" />
-            <arg value="changed" />
+        <exec executable="git" outputproperty="workspace.changes.j" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="status" />
+            <arg value="-s" />
+            <arg value="--porcelain" />
+            <arg value="-uno" />
             <arg value="." />
             <arg value="../locale" />
         </exec>
+        <!-- trim flags -->
+        <exec executable="sed" inputstring="${workspace.changes.j}" outputproperty="workspace.changes.j.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-e" />
+            <arg value="s/^[MTADRCU ]*//" />
+        </exec>
         <!-- \n in an attribute value generates an invalid manifest -->
-        <exec executable="tr" inputstring="${workspace.changes.j}" outputproperty="workspace.changes.j.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+        <exec executable="tr" inputstring="${workspace.changes.j.sed}" outputproperty="workspace.changes.j.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
             <arg value="-s" />
             <arg value="[:space:]" />
             <arg value="," />
@@ -211,7 +218,7 @@
                 <not>
                     <isset property="jar.uptodate" />
                 </not>
-                <isset property="mtn.available" />
+                <isset property="git.available" />
             </and>
         </condition>
     </target>
@@ -332,13 +339,23 @@
     </target>
 
     <target name="listChangedFiles2" depends="warUpToDate" if="shouldListChanges2" >
-        <exec executable="mtn" outputproperty="workspace.changes.w" errorproperty="mtn.error2" failifexecutionfails="false" >
-            <arg value="list" />
-            <arg value="changed" />
+        <exec executable="git" outputproperty="workspace.changes.w" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="status" />
+            <arg value="-s" />
+            <arg value="--porcelain" />
+            <arg value="-uno" />
             <arg value="../jsp" />
+            <arg value="../resources" />
+            <arg value="src/net/i2p/router/web/helpers" />
+            <arg value="src/net/i2p/router/web/servlets" />
+        </exec>
+        <!-- trim flags -->
+        <exec executable="sed" inputstring="${workspace.changes.w}" outputproperty="workspace.changes.w.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-e" />
+            <arg value="s/^[MTADRCU ]*//" />
         </exec>
         <!-- \n in an attribute value generates an invalid manifest -->
-        <exec executable="tr" inputstring="${workspace.changes.w}" outputproperty="workspace.changes.w.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+        <exec executable="tr" inputstring="${workspace.changes.w.sed}" outputproperty="workspace.changes.w.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
             <arg value="-s" />
             <arg value="[:space:]" />
             <arg value="," />
@@ -376,12 +393,12 @@
             <srcfiles dir= "build/obj" includes="net/i2p/router/web/helpers/*.class net/i2p/router/web/servlets/*.class" />
             <srcfiles dir= "../resources" />
         </uptodate>
-        <condition property="shouldListChanges" >
+        <condition property="shouldListChanges2" >
             <and>
                 <not>
-                    <isset property="jar.uptodate" />
+                    <isset property="war.uptodate" />
                 </not>
-                <isset property="mtn.available" />
+                <isset property="git.available" />
             </and>
         </condition>
     </target>
diff --git a/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java b/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java
index 39b8739975d85895ff705980fd8180c620e8f4fb..b910e4f019bc866d335994c5d7f79a0fd496e61a 100644
--- a/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java
+++ b/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java
@@ -1755,8 +1755,27 @@ public class ConsoleUpdateManager implements UpdateManager, RouterApp {
 
         @Override
         public String toString() {
-            return "VersionAvailable \"" + version + "\" " + sourceMap +
-                   (constraint != null ? (" \"" + constraint + '"') : "");
+            StringBuilder buf = new StringBuilder(128);
+            buf.append("Version ").append(version).append(' ');
+            for (Map.Entry<UpdateMethod, List<URI>> e : sourceMap.entrySet()) {
+                buf.append(e.getKey());
+                List<URI> u = e.getValue();
+                if (u.isEmpty()) {
+                    buf.append(' ');
+                } else {
+                    buf.append('=');
+                    if (u.size() > 1)
+                        buf.append('[');
+                    for (URI uri : u) {
+                        buf.append(uri).append(' ');
+                    }
+                    if (u.size() > 1)
+                        buf.append(']');
+                }
+            }
+            if (constraint != null)
+                buf.append(" \"").append(constraint).append('"');
+            return buf.toString();
         }
     }
 
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 533b18fabff86e29f936baa65e16a46c6908830e..31261e3deb076c5df90e017e9cb03022e496e09b 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
@@ -40,6 +40,7 @@ import net.i2p.util.Addresses;
 import net.i2p.util.FileSuffixFilter;
 import net.i2p.util.FileUtil;
 import net.i2p.util.I2PAppThread;
+import net.i2p.util.OrderedProperties;
 import net.i2p.util.PortMapper;
 import net.i2p.util.SecureDirectory;
 import net.i2p.util.I2PSSLSocketFactory;
@@ -1125,7 +1126,7 @@ public class RouterConsoleRunner implements RouterApp {
     }
 
     public static Properties webAppProperties(String dir) {
-        Properties rv = new Properties();
+        Properties rv = new OrderedProperties();
         // String webappConfigFile = _context.getProperty(PROP_WEBAPP_CONFIG_FILENAME, DEFAULT_WEBAPP_CONFIG_FILENAME);
         String webappConfigFile = DEFAULT_WEBAPP_CONFIG_FILENAME;
         File cfgFile = new File(dir, webappConfigFile);
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryRenderer.java
index b26de88978dfb521e0341702e70b87c2ae437292..7e8b1f7a5aa10636e68974c6d8e65080674608ed 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryRenderer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryRenderer.java
@@ -349,9 +349,14 @@ class SummaryRenderer {
                 // NPE here if system is missing fonts - see ticket #915
                 graph = new RrdGraph(def);
             } catch (NullPointerException npe) {
-                _log.error("Error rendering", npe);
+                _log.error("Error rendering graph", npe);
                 StatSummarizer.setDisabled(_context);
-                throw new IOException("Error rendering - disabling graph generation. Missing font? See http://trac.i2p2.i2p/ticket/915");
+                throw new IOException("Error rendering - disabling graph generation. Missing font?");
+            } catch (Error e) {
+                // Docker InternalError see Gitlab #383
+                _log.error("Error rendering graph", e);
+                StatSummarizer.setDisabled(_context);
+                throw new IOException("Error rendering - disabling graph generation. Missing font?");
             }
             int totalWidth = graph.getRrdGraphInfo().getWidth();
             int totalHeight = graph.getRrdGraphInfo().getHeight();
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/helpers/JobQueueHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/helpers/JobQueueHelper.java
index b5a67fcac1c6cf426b2f78fb79581544d1340743..185fdba80be6c8e1ae41b4066051c42c3a4b2cf2 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/helpers/JobQueueHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/helpers/JobQueueHelper.java
@@ -14,7 +14,7 @@ import net.i2p.data.DataHelper;
 import net.i2p.router.Job;
 import net.i2p.router.JobStats;
 import net.i2p.router.web.HelperBase;
-import net.i2p.util.ObjectCounter;
+import net.i2p.util.ObjectCounterUnsafe;
 
 public class JobQueueHelper extends HelperBase {
     
@@ -79,7 +79,7 @@ public class JobQueueHelper extends HelperBase {
         buf.append("<h3 id=\"readyjobs\">")
            .append(_t("Ready/waiting jobs")).append(": ").append(readyJobs.size())
            .append("</h3><ol>");
-        ObjectCounter<String> counter = new ObjectCounter<String>();
+        ObjectCounterUnsafe<String> counter = new ObjectCounterUnsafe<String>();
         for (int i = 0; i < readyJobs.size(); i++) {
             Job j = readyJobs.get(i);
             counter.increment(j.getName());
@@ -129,7 +129,7 @@ public class JobQueueHelper extends HelperBase {
     }
     
     /** @since 0.9.5 */
-    private void getJobCounts(StringBuilder buf, ObjectCounter<String> counter) {
+    private void getJobCounts(StringBuilder buf, ObjectCounterUnsafe<String> counter) {
         List<String> names = new ArrayList<String>(counter.objects());
         if (names.size() < 4)
             return;
@@ -232,10 +232,10 @@ public class JobQueueHelper extends HelperBase {
 
     /** @since 0.9.5 */
     private static class JobCountComparator implements Comparator<String>, Serializable {
-         private final ObjectCounter<String> _counter;
+         private final ObjectCounterUnsafe<String> _counter;
          private final Collator coll = Collator.getInstance();
 
-         public JobCountComparator(ObjectCounter<String> counter) {
+         public JobCountComparator(ObjectCounterUnsafe<String> counter) {
              _counter = counter;
          }
 
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/helpers/LogsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/helpers/LogsHelper.java
index c1aa060533ef50f270f524e4d745bfea12da9300..25021f61bc0960a0c1f555628b472b853557e25a 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/helpers/LogsHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/helpers/LogsHelper.java
@@ -23,6 +23,9 @@ import net.i2p.util.UIMessages;
 
 public class LogsHelper extends HelperBase {
 
+    // cache so we only load once
+    Attributes att;
+
     private static final String _jstlVersion = jstlVersion();
 
     private static final int MAX_WRAPPER_LINES = 250;
@@ -206,23 +209,46 @@ public class LogsHelper extends HelperBase {
         rv[2] = DataHelper.escapeHTML(f.getName()).replace(" ", "%20");
         return rv;
     }
-   
+
     /**
      * @since 0.9.35
      */
     public String getBuiltBy() {
-        File libDir = _context.getLibDir();
-        File f = new File(libDir, "i2p.jar");
-        Attributes att = FileDumpHelper.attributes(f);
+        return getAtt("Built-By");
+    }
+
+    /**
+     * @since 0.9.58
+     */
+    public String getBuildDate() {
+        return getAtt("Build-Date");
+    }
+
+    /**
+     * @since 0.9.58
+     */
+    public String getRevision() {
+        return getAtt("Base-Revision");
+    }
+
+    /**
+     * @since 0.9.58 pulled out from above
+     */
+    private String getAtt(String a) {
+        if (att == null) {
+            File libDir = _context.getLibDir();
+            File f = new File(libDir, "i2p.jar");
+            att = FileDumpHelper.attributes(f);
+        }
         if (att != null) {
-            String s = FileDumpHelper.getAtt(att, "Built-By");
+            String s = FileDumpHelper.getAtt(att, a);
             if (s != null) {
                 return s;
             }
         }
         return "Undefined";
     }
-    
+
     private final static String NL = System.getProperty("line.separator");
 
     /** formats in forward order */
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/helpers/NetDbRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/helpers/NetDbRenderer.java
index 11b2faf03ac394c0315add737f38d1c041f9efed..751c6af59a9558f54dac8d477a754bf817f9136c 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/helpers/NetDbRenderer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/helpers/NetDbRenderer.java
@@ -52,7 +52,7 @@ import net.i2p.router.web.WebAppStarter;
 import net.i2p.util.Addresses;
 import net.i2p.util.ConvertToHash;
 import net.i2p.util.Log;
-import net.i2p.util.ObjectCounter;
+import net.i2p.util.ObjectCounterUnsafe;
 import net.i2p.util.Translate;
 import net.i2p.util.VersionComparator;
 
@@ -935,8 +935,8 @@ class NetDbRenderer {
             buf.setLength(0);
         }
 
-        ObjectCounter<String> versions = new ObjectCounter<String>();
-        ObjectCounter<String> countries = new ObjectCounter<String>();
+        ObjectCounterUnsafe<String> versions = new ObjectCounterUnsafe<String>();
+        ObjectCounterUnsafe<String> countries = new ObjectCounterUnsafe<String>();
         int[] transportCount = new int[TNAMES.length];
 
         int skipped = 0;
@@ -1137,10 +1137,10 @@ class NetDbRenderer {
      */
     private class CountryCountComparator implements Comparator<String> {
          private static final long serialVersionUID = 1L;
-         private final ObjectCounter<String> counts;
+         private final ObjectCounterUnsafe<String> counts;
          private final Collator coll;
 
-         public CountryCountComparator(ObjectCounter<String> counts) {
+         public CountryCountComparator(ObjectCounterUnsafe<String> counts) {
              super();
              this.counts = counts;
              coll = Collator.getInstance(new Locale(Messages.getLanguage(_context)));
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/helpers/SummaryHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/helpers/SummaryHelper.java
index 689ea3d5342ff46c3d6c8c516ae74577e219c292..98eacce491db1db91ef327746d4ea7297c0ac8f5 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/helpers/SummaryHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/helpers/SummaryHelper.java
@@ -237,7 +237,7 @@ public class SummaryHelper extends HelperBase {
         long skew = _context.commSystem().getFramedAveragePeerClockSkew(33);
         // Display the actual skew, not the offset
         if (Math.abs(skew) > 30*1000)
-            return new NetworkStateMessage(NetworkState.CLOCKSKEW, _t("ERR-Clock Skew of {0}", DataHelper.formatDuration2(Math.abs(skew))));
+            return new NetworkStateMessage(NetworkState.CLOCKSKEW, fixup(_t("ERR-Clock Skew of {0}", DataHelper.formatDuration2(Math.abs(skew)))));
         if (_context.router().isHidden())
             return new NetworkStateMessage(NetworkState.HIDDEN, _t("Hidden"));
         RouterInfo routerInfo = _context.router().getRouterInfo();
@@ -272,22 +272,22 @@ public class SummaryHelper extends HelperBase {
                 // TODO set IPv6 arg based on configuration?
                 if (TransportUtil.isPubliclyRoutable(ip, true))
                     return new NetworkStateMessage(NetworkState.RUNNING, txstatus);
-                return new NetworkStateMessage(NetworkState.ERROR, _t("ERR-Private TCP Address"));
+                return new NetworkStateMessage(NetworkState.ERROR, fixup(_t("ERR-Private TCP Address")));
 
             case IPV4_SNAT_IPV6_UNKNOWN:
             case DIFFERENT:
-                return new NetworkStateMessage(NetworkState.ERROR, _t("ERR-SymmetricNAT"));
+                return new NetworkStateMessage(NetworkState.ERROR, fixup(_t("ERR-SymmetricNAT")));
 
             case REJECT_UNSOLICITED:
                 state = NetworkState.FIREWALLED;
             case IPV4_DISABLED_IPV6_FIREWALLED:
                 if (routerInfo.getTargetAddress("NTCP") != null)
-                    return new NetworkStateMessage(NetworkState.WARN, _t("WARN-Firewalled with Inbound TCP Enabled"));
+                    return new NetworkStateMessage(NetworkState.WARN, fixup(_t("WARN-Firewalled with Inbound TCP Enabled")));
                 // fall through...
             case IPV4_FIREWALLED_IPV6_OK:
             case IPV4_FIREWALLED_IPV6_UNKNOWN:
                 if (((FloodfillNetworkDatabaseFacade)_context.netDb()).floodfillEnabled())
-                    return new NetworkStateMessage(NetworkState.WARN, _t("WARN-Firewalled and Floodfill"));
+                    return new NetworkStateMessage(NetworkState.WARN, fixup(_t("WARN-Firewalled and Floodfill")));
                 //if (_context.router().getRouterInfo().getCapabilities().indexOf('O') >= 0)
                 //    return new NetworkStateMessage(NetworkState.WARN, _t("WARN-Firewalled and Fast"));
                 return new NetworkStateMessage(state, txstatus);
@@ -296,7 +296,7 @@ public class SummaryHelper extends HelperBase {
                 return new NetworkStateMessage(NetworkState.TESTING, _t("Disconnected - check network connection"));
 
             case HOSED:
-                return new NetworkStateMessage(NetworkState.ERROR, _t("ERR-UDP Port In Use - Set i2np.udp.internalPort=xxxx in advanced config and restart"));
+                return new NetworkStateMessage(NetworkState.ERROR, fixup(_t("ERR-UDP Port In Use - Set i2np.udp.internalPort=xxxx in advanced config and restart")));
 
             case UNKNOWN:
                 state = NetworkState.TESTING;
@@ -306,17 +306,39 @@ public class SummaryHelper extends HelperBase {
                 List<RouterAddress> ra = routerInfo.getTargetAddresses("SSU", "SSU2");
                 if (ra.isEmpty() && _context.router().getUptime() > 5*60*1000) {
                     if (getActivePeers() <= 0)
-                        return new NetworkStateMessage(NetworkState.ERROR, _t("ERR-No Active Peers, Check Network Connection and Firewall"));
+                        return new NetworkStateMessage(NetworkState.ERROR, fixup(_t("ERR-No Active Peers, Check Network Connection and Firewall")));
                     else if (_context.getProperty(ConfigNetHelper.PROP_I2NP_NTCP_HOSTNAME) == null ||
                         _context.getProperty(ConfigNetHelper.PROP_I2NP_NTCP_PORT) == null)
-                        return new NetworkStateMessage(NetworkState.ERROR, _t("ERR-UDP Disabled and Inbound TCP host/port not set"));
+                        return new NetworkStateMessage(NetworkState.ERROR, fixup(_t("ERR-UDP Disabled and Inbound TCP host/port not set")));
                     else
-                        return new NetworkStateMessage(NetworkState.WARN, _t("WARN-Firewalled with UDP Disabled"));
+                        return new NetworkStateMessage(NetworkState.WARN, fixup(_t("WARN-Firewalled with UDP Disabled")));
                 }
                 return new NetworkStateMessage(state, txstatus);
         }
     }
 
+    /**
+     *  Make the already-translated ERR- and WARN- strings a little prettier
+     *  @since 0.9.58
+     */
+    private static String fixup(String s) {
+        if (s.startsWith("ERR-")) {
+            s = s.substring(4);
+        } else if (s.startsWith("ERR -")) {
+            s = s.substring(5);
+        } else if (s.startsWith("WARN-")) {
+            s = s.substring(5);
+        } else if (s.startsWith("WARN -")) {
+            s = s.substring(6);
+        } else {
+            // translated
+            int d = s.indexOf('-');
+            if (d > 0 && d < 10)
+                s = s.substring(d + 1);
+        }
+        return s;
+    }
+
     /**
      * Retrieve amount of used memory.
      * @since 0.9.32 uncommented
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/helpers/SybilRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/helpers/SybilRenderer.java
index d48a415f9d853148573ab815cbf3479cb02fe1ae..3461d95e82dedce30c988e2c4dad2aecc7f12087 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/helpers/SybilRenderer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/helpers/SybilRenderer.java
@@ -46,7 +46,6 @@ import net.i2p.stat.RateAverages;
 import net.i2p.stat.RateStat;
 import net.i2p.util.ConvertToHash;
 import net.i2p.util.Log;
-import net.i2p.util.ObjectCounter;
 import net.i2p.util.SystemVersion;
 import net.i2p.util.Translate;
 import net.i2p.util.VersionComparator;
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/helpers/TunnelRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/helpers/TunnelRenderer.java
index 8c59725dabe35488a3e997945550ab39ba675cba..e69d33c30f7dd30a268afd6a6276307e5bb7c96d 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/helpers/TunnelRenderer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/helpers/TunnelRenderer.java
@@ -26,6 +26,7 @@ import net.i2p.router.web.HelperBase;
 import net.i2p.router.web.Messages;
 import net.i2p.stat.Rate;
 import net.i2p.stat.RateStat;
+import net.i2p.util.ObjectCounterUnsafe;
 
 /**
  *  For /tunnels.jsp, used by TunnelHelper.
@@ -138,8 +139,9 @@ class TunnelRenderer {
                           recv + "</span></td>");
             else
                 out.write("<td class=\"cells\" align=\"center\">n/a</td>");
-            if (cfg.getReceiveFrom() != null)
-                out.write("<td class=\"cells\" align=\"center\"><span class=\"tunnel_peer\">" + netDbLink(cfg.getReceiveFrom()) +"</span></td>");
+            Hash from = cfg.getReceiveFrom();
+            if (from != null)
+                out.write("<td class=\"cells\" align=\"center\"><span class=\"tunnel_peer\">" + netDbLink(from) +"</span></td>");
             else
                 out.write("<td class=\"cells\">&nbsp;</td>");
             long send = cfg.getSendTunnelId();
@@ -147,8 +149,9 @@ class TunnelRenderer {
                 out.write("<td class=\"cells\" align=\"center\" title=\"" + _t("Tunnel identity") + "\"><span class=\"tunnel_id\">" + send +"</span></td>");
             else
                 out.write("<td class=\"cells\">&nbsp;</td>");
-            if (cfg.getSendTo() != null)
-                out.write("<td class=\"cells\" align=\"center\"><span class=\"tunnel_peer\">" + netDbLink(cfg.getSendTo()) +"</span></td>");
+            Hash to = cfg.getSendTo();
+            if (to != null)
+                out.write("<td class=\"cells\" align=\"center\"><span class=\"tunnel_peer\">" + netDbLink(to) +"</span></td>");
             else
                 out.write("<td class=\"cells\">&nbsp;</td>");
             long timeLeft = cfg.getExpiration() - now;
@@ -164,9 +167,9 @@ class TunnelRenderer {
                 lifetime = 10*60;
             long bps = 1024L * count / lifetime;
             out.write("<td class=\"cells\" align=\"center\">" + DataHelper.formatSize2Decimal(bps) + "Bps</td>");
-            if (cfg.getSendTo() == null)
+            if (to == null)
                 out.write("<td class=\"cells\" align=\"center\">" + _t("Outbound Endpoint") + "</td>");
-            else if (cfg.getReceiveFrom() == null)
+            else if (from == null)
                 out.write("<td class=\"cells\" align=\"center\">" + _t("Inbound Gateway") + "</td>");
             else
                 out.write("<td class=\"cells\" align=\"center\">" + _t("Participant") + "</td>");
@@ -181,6 +184,49 @@ class TunnelRenderer {
         else if (displayed <= 0)
             out.write("<div class=\"statusnotes\"><b>" + _t("none") + "</b></div>\n");
         out.write("<div class=\"statusnotes\"><b>" + _t("Lifetime bandwidth usage") + ":&nbsp;&nbsp;" + DataHelper.formatSize2(processed*1024) + "B</b></div>\n");
+
+            if (debug && participating.size() > 1) {
+                // peer table sorted by number of tunnels
+                ObjectCounterUnsafe<Hash> counts = new ObjectCounterUnsafe<Hash>();
+                ObjectCounterUnsafe<Hash> bws = new ObjectCounterUnsafe<Hash>();
+                for (int i = 0; i < participating.size(); i++) {
+                    HopConfig cfg = participating.get(i);
+                    Hash from = cfg.getReceiveFrom();
+                    Hash to = cfg.getSendTo();
+                    int msgs = cfg.getProcessedMessagesCount();
+                    if (from != null) {
+                        counts.increment(from);
+                        if (msgs > 0)
+                            bws.add(from, msgs);
+                    }
+                    if (to != null) {
+                        counts.increment(to);
+                        if (msgs > 0)
+                            bws.add(to, msgs);
+                    }
+                }
+                // sort and output
+                out.write("<h3 class=\"tabletitle\">Peers in multiple participating tunnels (including inactive)</h3>\n");
+                out.write("<table class=\"tunneldisplay tunnels_participating\"><tr><th>" + _t("Router") + "</th><th>" + _t("Tunnels") + "</th><th>"
+                          + _t("Usage") + "</th></tr>\n");
+                displayed = 0;
+                List<Hash> sort = counts.sortedObjects();
+                for (Hash h : sort) {
+                    int count = counts.count(h);
+                    if (count <= 1)
+                        break;
+                    if (++displayed > DISPLAY_LIMIT)
+                        break;
+                    out.write("<tr><td class=\"cells\" align=\"center\"><span class=\"tunnel_peer\">" + netDbLink(h) + "</span></td>\n");
+                    out.write("<td class=\"cells\" align=\"center\">" + count + "</td>\n");
+                    out.write("<td class=\"cells\" align=\"center\">" + DataHelper.formatSize2(bws.count(h) * 1024) + "B</td></tr>\n");
+                }
+                out.write("</table>\n");
+                if (displayed <= 0)
+                    out.write("<div class=\"statusnotes\"><b>" + _t("none") + "</b></div>\n");
+            }
+
+
         } else {   // bwShare > 12
             out.write("<div class=\"statusnotes noparticipate\"><b>" + _t("Not enough shared bandwidth to build participating tunnels.") +
                       "</b> <a href=\"config\">[" + _t("Configure") + "]</a></div>\n");
diff --git a/apps/routerconsole/jsp/debug.jsp b/apps/routerconsole/jsp/debug.jsp
index c71739a2bb447d02dfbcd81d370834b7e9a06a35..b621ae6972ffd554ed22ff299eeac0a9df009524 100644
--- a/apps/routerconsole/jsp/debug.jsp
+++ b/apps/routerconsole/jsp/debug.jsp
@@ -16,12 +16,12 @@
 <div class="main" id="debug">
 
 <div class="confignav">
-<span class="tab"><a href="#debug_portmapper">Port Mapper</a></span>
-<span class="tab"><a href="#appmanager">App Manager</a></span>
-<span class="tab"><a href="#updatemanager">Update Manager</a></span>
-<span class="tab"><a href="#skm">Router Session Key Manager</a></span>
-<span class="tab"><a href="#cskm0">Client Session Key Managers</a></span>
-<span class="tab"><a href="#dht">Router DHT</a></span>
+<span class="tab"><a href="/debug">Port Mapper</a></span>
+<span class="tab"><a href="/debug?d=1">App Manager</a></span>
+<span class="tab"><a href="/debug?d=2">Update Manager</a></span>
+<span class="tab"><a href="/debug?d=3">Router Session Key Manager</a></span>
+<span class="tab"><a href="/debug?d=4">Client Session Key Managers</a></span>
+<span class="tab"><a href="/debug?d=5">Router DHT</a></span>
 </div>
 
 <%
@@ -30,6 +30,9 @@
      */
     net.i2p.router.RouterContext ctx = (net.i2p.router.RouterContext) net.i2p.I2PAppContext.getGlobalContext();
 
+String dd = request.getParameter("d");
+if (dd == null || dd.equals("0")) {
+
     /*
      *  Print out the status for the PortMapper
      */
@@ -40,6 +43,8 @@
      */
     net.i2p.util.InternalServerSocket.renderStatusHTML(out);
 
+} else if (dd.equals("1")) {
+
     /*
      *  Print out the status for the AppManager
      */
@@ -48,6 +53,7 @@
     ctx.routerAppManager().renderStatusHTML(out);
             out.print("</div>");
 
+} else if (dd.equals("2")) {
 
     /*
      *  Print out the status for the UpdateManager
@@ -63,14 +69,20 @@
     out.print("</div>");
     }
 
+} else if (dd.equals("3")) {
+
     /*
      *  Print out the status for all the SessionKeyManagers
      */
     out.print("<div class=\"debug_section\" id=\"skm\">");
     out.print("<h2>Router Session Key Manager</h2>");
     ctx.sessionKeyManager().renderStatusHTML(out);
-    java.util.Set<net.i2p.data.Destination> clients = ctx.clientManager().listClients();
     out.print("</div>");
+
+} else if (dd.equals("4")) {
+
+    out.print("<h2>Client Session Key Managers</h2>");
+    java.util.Set<net.i2p.data.Destination> clients = ctx.clientManager().listClients();
     int i = 0;
     for (net.i2p.data.Destination dest : clients) {
         net.i2p.data.Hash h = dest.calculateHash();
@@ -92,6 +104,7 @@
             out.print("</div>");
         }
     }
+} else if (dd.equals("5")) {
 
     /*
      *  Print out the status for the NetDB
@@ -99,5 +112,7 @@
     out.print("<h2 id=\"dht\">Router DHT</h2>");
     ctx.netDb().renderStatusHTML(out);
 
+}
+
 %>
 </div></body></html>
diff --git a/apps/routerconsole/jsp/logs.jsp b/apps/routerconsole/jsp/logs.jsp
index 781e2f51af7a82a3cfd0efe04141736bff1aa590..36b82fce34e10dbbc28380fadf83cb4bc0920983 100644
--- a/apps/routerconsole/jsp/logs.jsp
+++ b/apps/routerconsole/jsp/logs.jsp
@@ -54,7 +54,14 @@
 %><tr><td><b>Encoding:</b></td><td><%=System.getProperty("file.encoding")%></td></tr>
 <tr><td><b>Charset:</b></td><td><%=java.nio.charset.Charset.defaultCharset().name()%></td></tr>
 <tr><td><b>Service:</b></td><td><%=net.i2p.util.SystemVersion.isService()%></td></tr>
-<tr><td><b>Built By:</b></td><td><jsp:getProperty name="logsHelper" property="builtBy" /></tbody></table>
+<%
+   String rev = logsHelper.getRevision();
+   if (rev.length() == 40) {
+%><tr><td><b>Revision:</b></td><td><%=rev%></td></tr>
+<%
+   }
+%><tr><td><b>Built:</b></td><td><jsp:getProperty name="logsHelper" property="buildDate" /></td></tr>
+<tr><td><b>Built By:</b></td><td><jsp:getProperty name="logsHelper" property="builtBy" /></td></tr></tbody></table>
 
 <h3 class="tabletitle"><%=intl._t("Critical Logs")%><%
     String consoleNonce = net.i2p.router.web.CSSHelper.getNonce();
diff --git a/apps/routerconsole/jsp/summary.jsi b/apps/routerconsole/jsp/summary.jsi
index 12fdebeb5e51af7ae8e3971edc648633e72bf28a..8cbfab8863eb7646111214579ed603345bf62cdf 100644
--- a/apps/routerconsole/jsp/summary.jsi
+++ b/apps/routerconsole/jsp/summary.jsi
@@ -28,8 +28,10 @@
             // update disable boolean
             intl.setDisableRefresh(d);
         }
+/*
         if (false && !intl.getDisableRefresh())
             out.print("<noscript><iframe src=\"/summaryframe.jsp" + newDelay + "\" height=\"1500\" width=\"200\" scrolling=\"auto\" frameborder=\"0\" title=\"sidepanel\"></noscript>\n");
+*/
     }
 %>
 <div class="routersummary">
@@ -46,6 +48,7 @@
         out.print("</a>");
     }
 
+/*
     // d and allowIFrame defined above
     if (false && !intl.getDisableRefresh()) {
         out.print("</div><noscript></iframe></noscript>\n");
@@ -63,7 +66,10 @@
         out.print("</button>\n" +
                   "</form></div></noscript></div>\n");
     } else {
+*/
         out.print("</div>\n");
+/*
     }
+*/
 %>
 </div>
diff --git a/apps/routerconsole/jsp/themes/console/light/console.css b/apps/routerconsole/jsp/themes/console/light/console.css
index 4ffd64d0b14b68f8d00295093de8e12150d52d9b..298ddf755e54594e2580483848964f8b75f1116e 100644
--- a/apps/routerconsole/jsp/themes/console/light/console.css
+++ b/apps/routerconsole/jsp/themes/console/light/console.css
@@ -7563,7 +7563,6 @@ b.netdb_transport {
     word-break: break-all;
     margin-bottom: 5px;
     padding-bottom: 0;
-    max-height: 600px;
     overflow: auto;
 }
 
diff --git a/apps/sam/java/build.xml b/apps/sam/java/build.xml
index 02359f492f2266f4c7be86e1ba02e5e48a90520a..89233a415f1e62f3998a99220bd318627d7889f1 100644
--- a/apps/sam/java/build.xml
+++ b/apps/sam/java/build.xml
@@ -58,13 +58,20 @@
     </target>
 
     <target name="listChangedFiles" depends="jarUpToDate" if="shouldListChanges" >
-        <exec executable="mtn" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
-            <arg value="list" />
-            <arg value="changed" />
+        <exec executable="git" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="status" />
+            <arg value="-s" />
+            <arg value="--porcelain" />
+            <arg value="-uno" />
             <arg value="." />
         </exec>
+        <!-- trim flags -->
+        <exec executable="sed" inputstring="${workspace.changes}" outputproperty="workspace.changes.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-e" />
+            <arg value="s/^[MTADRCU ]*//" />
+        </exec>
         <!-- \n in an attribute value generates an invalid manifest -->
-        <exec executable="tr" inputstring="${workspace.changes}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+        <exec executable="tr" inputstring="${workspace.changes.sed}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
             <arg value="-s" />
             <arg value="[:space:]" />
             <arg value="," />
@@ -115,7 +122,7 @@
                 <not>
                     <isset property="jar.uptodate" />
                 </not>
-                <isset property="mtn.available" />
+                <isset property="git.available" />
             </and>
         </condition>
     </target>
diff --git a/apps/streaming/java/build.xml b/apps/streaming/java/build.xml
index 914867ae93f82c59f6d6325198dac548c7b8bbaf..ecfa0ebfd06b1fa758f8cbd1b676271a4c3a882f 100644
--- a/apps/streaming/java/build.xml
+++ b/apps/streaming/java/build.xml
@@ -191,13 +191,20 @@
     <!-- end unit tests -->
 
     <target name="listChangedFiles" depends="jarUpToDate" if="shouldListChanges" >
-        <exec executable="mtn" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
-            <arg value="list" />
-            <arg value="changed" />
+        <exec executable="git" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="status" />
+            <arg value="-s" />
+            <arg value="--porcelain" />
+            <arg value="-uno" />
             <arg value="." />
         </exec>
+        <!-- trim flags -->
+        <exec executable="sed" inputstring="${workspace.changes}" outputproperty="workspace.changes.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-e" />
+            <arg value="s/^[MTADRCU ]*//" />
+        </exec>
         <!-- \n in an attribute value generates an invalid manifest -->
-        <exec executable="tr" inputstring="${workspace.changes}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+        <exec executable="tr" inputstring="${workspace.changes.sed}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
             <arg value="-s" />
             <arg value="[:space:]" />
             <arg value="," />
@@ -234,7 +241,7 @@
                 <not>
                     <isset property="jar.uptodate" />
                 </not>
-                <isset property="mtn.available" />
+                <isset property="git.available" />
             </and>
         </condition>
     </target>
diff --git a/apps/susidns/src/build.xml b/apps/susidns/src/build.xml
index 6ddce6822fb8556b66ecd7efb33b90b7ac9dda62..eb603048af3fa4c9d3bd10b892465d0ea4d14698 100644
--- a/apps/susidns/src/build.xml
+++ b/apps/susidns/src/build.xml
@@ -150,20 +150,27 @@
     <target name="all" depends="war"/> 
 
     <target name="listChangedFiles" depends="warUpToDate" if="shouldListChanges" >
-        <exec executable="mtn" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
-            <arg value="list" />
-            <arg value="changed" />
+        <exec executable="git" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="status" />
+            <arg value="-s" />
+            <arg value="--porcelain" />
+            <arg value="-uno" />
             <arg value="." />
         </exec>
+        <!-- trim flags -->
+        <exec executable="sed" inputstring="${workspace.changes}" outputproperty="workspace.changes.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-e" />
+            <arg value="s/^[MTADRCU ]*//" />
+        </exec>
         <!-- \n in an attribute value generates an invalid manifest -->
-        <exec executable="tr" inputstring="${workspace.changes}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+        <exec executable="tr" inputstring="${workspace.changes.sed}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
             <arg value="-s" />
             <arg value="[:space:]" />
             <arg value="," />
         </exec>
     </target>
 
-    <target name="war" depends="compile, precompilejsp, bundle, warUpToDate" unless="war.uptodate" > 
+    <target name="war" depends="compile, precompilejsp, bundle, warUpToDate, listChangedFiles" unless="war.uptodate" > 
         <!-- set if unset -->
         <property name="workspace.changes.tr" value="" />
         <war destfile="${project}.war" webxml="WEB-INF/web-out.xml">
@@ -187,14 +194,14 @@
 
     <target name="warUpToDate">
         <uptodate property="war.uptodate" targetfile="${project}.war">
-            <srcfiles dir= "." includes="WEB-INF/web-out.xml WEB-INF/**/*.class js/* svg/*" />
+            <srcfiles dir= "." includes="WEB-INF/web-out.xml WEB-INF/**/*.class js/* svg/* themes/**/*" />
         </uptodate>
         <condition property="shouldListChanges" >
             <and>
                 <not>
                     <isset property="war.uptodate" />
                 </not>
-                <isset property="mtn.available" />
+                <isset property="git.available" />
             </and>
         </condition>
     </target>
diff --git a/apps/susimail/build.xml b/apps/susimail/build.xml
index d248a426c7f1aaf8ee16d000b8bfe7232fd10f58..e88fb3f2757f08453c880ea70354243171e83a9e 100644
--- a/apps/susimail/build.xml
+++ b/apps/susimail/build.xml
@@ -78,13 +78,20 @@
     </target>
 
     <target name="listChangedFiles" depends="warUpToDate" if="shouldListChanges" >
-        <exec executable="mtn" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
-            <arg value="list" />
-            <arg value="changed" />
+        <exec executable="git" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="status" />
+            <arg value="-s" />
+            <arg value="--porcelain" />
+            <arg value="-uno" />
             <arg value="." />
         </exec>
+        <!-- trim flags -->
+        <exec executable="sed" inputstring="${workspace.changes}" outputproperty="workspace.changes.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-e" />
+            <arg value="s/^[MTADRCU ]*//" />
+        </exec>
         <!-- \n in an attribute value generates an invalid manifest -->
-        <exec executable="tr" inputstring="${workspace.changes}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+        <exec executable="tr" inputstring="${workspace.changes.sed}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
             <arg value="-s" />
             <arg value="[:space:]" />
             <arg value="," />
@@ -118,7 +125,7 @@
                 <not>
                     <isset property="war.uptodate" />
                 </not>
-                <isset property="mtn.available" />
+                <isset property="git.available" />
             </and>
         </condition>
     </target>
diff --git a/apps/systray/java/build.xml b/apps/systray/java/build.xml
index 5897ef41b65f9f0b1936d2761b591008b343cafc..3f55ce618d5c229b045c61f1f3a39c24af24f883 100644
--- a/apps/systray/java/build.xml
+++ b/apps/systray/java/build.xml
@@ -46,13 +46,20 @@
     </target>
 
     <target name="listChangedFiles" depends="jarUpToDate" if="shouldListChanges" >
-        <exec executable="mtn" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
-            <arg value="list" />
-            <arg value="changed" />
+        <exec executable="git" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="status" />
+            <arg value="-s" />
+            <arg value="--porcelain" />
+            <arg value="-uno" />
             <arg value="." />
         </exec>
+        <!-- trim flags -->
+        <exec executable="sed" inputstring="${workspace.changes}" outputproperty="workspace.changes.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-e" />
+            <arg value="s/^[MTADRCU ]*//" />
+        </exec>
         <!-- \n in an attribute value generates an invalid manifest -->
-        <exec executable="tr" inputstring="${workspace.changes}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+        <exec executable="tr" inputstring="${workspace.changes.sed}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
             <arg value="-s" />
             <arg value="[:space:]" />
             <arg value="," />
@@ -85,7 +92,7 @@
                 <not>
                     <isset property="jar.uptodate" />
                 </not>
-                <isset property="mtn.available" />
+                <isset property="git.available" />
             </and>
         </condition>
     </target>
diff --git a/core/java/build.xml b/core/java/build.xml
index 351eb7642c25cd4695216a4bb10bd09404c1d6d2..8b36ce5f0bb201f3c67e8c04aa106fa8dcafc65b 100644
--- a/core/java/build.xml
+++ b/core/java/build.xml
@@ -81,14 +81,22 @@
         </javac>
     </target>
 
-    <target name="listChangedFiles" if="mtn.available" >
-        <exec executable="mtn" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
-            <arg value="list" />
-            <arg value="changed" />
+    <target name="listChangedFiles" if="git.available" >
+        <exec executable="git" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="status" />
+            <arg value="-s" />
+            <arg value="--porcelain" />
+            <arg value="-uno" />
             <arg value="." />
+            <arg value="../resources" />
+        </exec>
+        <!-- trim flags -->
+        <exec executable="sed" inputstring="${workspace.changes}" outputproperty="workspace.changes.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-e" />
+            <arg value="s/^[MTADRCU ]*//" />
         </exec>
         <!-- \n in an attribute value generates an invalid manifest -->
-        <exec executable="tr" inputstring="${workspace.changes}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+        <exec executable="tr" inputstring="${workspace.changes.sed}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
             <arg value="-s" />
             <arg value="[:space:]" />
             <arg value="," />
diff --git a/core/java/src/net/i2p/util/ObjectCounterUnsafe.java b/core/java/src/net/i2p/util/ObjectCounterUnsafe.java
new file mode 100644
index 0000000000000000000000000000000000000000..3e2f532cdb00306b875ccd704456fbb52571550e
--- /dev/null
+++ b/core/java/src/net/i2p/util/ObjectCounterUnsafe.java
@@ -0,0 +1,107 @@
+package net.i2p.util;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ *  Count things.
+ *  NOT thread safe, mostly for UI and Sybil.
+ *  Dropin replacement for ObjectCounter.
+ *  Much less object churn than ObjectCounter.
+ *  Also provides add() and sortedObjects()
+ *
+ *  @since 0.9.58
+ */
+public class ObjectCounterUnsafe<K> {
+    private final HashMap<K, Int> map = new HashMap<K, Int>();
+
+    /**
+     *  Add one.
+     *  @return count after increment
+     */
+    public int increment(K h) {
+        Int i = map.get(h);
+        if (i != null) {
+            return ++(i.c);
+        }
+        map.put(h, new Int(1));
+        return 1;
+    }
+
+    /**
+     *  Add a value
+     *  @return count after adding
+     */
+    public int add(K h, int val) {
+        Int i = map.get(h);
+        if (i != null) {
+            i.c += val;
+            return i.c;
+        }
+        map.put(h, new Int(val));
+        return val;
+    }
+
+    /**
+     *  @return current count
+     */
+    public int count(K h) {
+        Int i = map.get(h);
+        if (i != null)
+            return i.c;
+        return 0;
+    }
+
+    /**
+     *  @return set of objects with counts &gt; 0
+     */
+    public Set<K> objects() {
+        return map.keySet();
+    }
+
+    /**
+     *  @return list of objects reverse sorted by count, highest to lowest
+     */
+    public List<K> sortedObjects() {
+        List<K> rv = new ArrayList<K>(map.keySet());
+        Collections.sort(rv, new ObjComparator());
+        return rv;
+    }
+
+    /**
+     *  Start over. Reset the count for all keys to zero.
+     */
+    public void clear() {
+        map.clear();
+    }
+
+    /**
+     *  Reset the count for this key to zero
+     */
+    public void clear(K h) {
+        map.remove(h);
+    }
+
+    /**
+     *  Modifiable integer
+     */
+    private static class Int {
+        int c;
+        public Int(int i) { c = i; }
+    }
+
+    /**
+     *  reverse sort
+     */
+    private class ObjComparator implements Comparator<K> {
+        public int compare(K l, K r) {
+            return (map.get(r).c - map.get(l).c);
+        }
+    }
+}
+
diff --git a/history.txt b/history.txt
index a8da92a3c9318bd2f0f9e0efa323d16a9da2cc79..38dbf09088438b5fa206ce482202dd11a0536c4f 100644
--- a/history.txt
+++ b/history.txt
@@ -1,3 +1,24 @@
+2023-01-22 zzz
+ * Build: Fix list of changed files in manifests
+ * i2psnark: Add max files per torrent config
+
+2023-01-21 zzz
+ * Console:
+   - Remove ERR- and WARN- prefixes from status strings
+   - Catch graph error in Docker (Gitlab #383)
+ * i2psnark: Search box CSS
+ * NTCP: Do not rebind internal port if only SSU external port changed
+ * SSU:
+   - Eliminate Symmetric NAT errors for "full cone" NATs
+   - Fix rare peer test NPE
+   - Fix initial SSU2 MTU when SSU1 disabled
+
+2023-01-19 zzz
+ * Build: Add i2psnark-release target
+
+2023-01-18 zzz
+ * i2psnark: Search CSS and JS
+
 2023-01-17 zzz
  * i2psnark:
    - Add basic search box
diff --git a/installer/java/build.xml b/installer/java/build.xml
index 8a34405f308be7fe75662971aa4f7427f3637cf6..d3926a0f8992ab5af4770e2093de552a14615cc7 100644
--- a/installer/java/build.xml
+++ b/installer/java/build.xml
@@ -34,14 +34,21 @@
         </javac>
     </target>
 
-    <target name="listChangedFiles" if="mtn.available" >
-        <exec executable="mtn" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
-            <arg value="list" />
-            <arg value="changed" />
+    <target name="listChangedFiles" if="git.available" >
+        <exec executable="git" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="status" />
+            <arg value="-s" />
+            <arg value="--porcelain" />
+            <arg value="-uno" />
             <arg value="." />
         </exec>
+        <!-- trim flags -->
+        <exec executable="sed" inputstring="${workspace.changes}" outputproperty="workspace.changes.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-e" />
+            <arg value="s/^[MTADRCU ]*//" />
+        </exec>
         <!-- \n in an attribute value generates an invalid manifest -->
-        <exec executable="tr" inputstring="${workspace.changes}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+        <exec executable="tr" inputstring="${workspace.changes.sed}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
             <arg value="-s" />
             <arg value="[:space:]" />
             <arg value="," />
diff --git a/router/java/build.xml b/router/java/build.xml
index 344960976615be7d9f194679155f3e645201921b..4b0cedf2bcf626ad41de03d38141952349cc2248 100644
--- a/router/java/build.xml
+++ b/router/java/build.xml
@@ -53,14 +53,22 @@
         </javac>
     </target>
 
-    <target name="listChangedFiles" if="mtn.available" >
-        <exec executable="mtn" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
-            <arg value="list" />
-            <arg value="changed" />
+    <target name="listChangedFiles" if="git.available" >
+        <exec executable="git" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="status" />
+            <arg value="-s" />
+            <arg value="--porcelain" />
+            <arg value="-uno" />
             <arg value="." />
+            <arg value="../resources" />
+        </exec>
+        <!-- trim flags -->
+        <exec executable="sed" inputstring="${workspace.changes}" outputproperty="workspace.changes.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
+            <arg value="-e" />
+            <arg value="s/^[MTADRCU ]*//" />
         </exec>
         <!-- \n in an attribute value generates an invalid manifest -->
-        <exec executable="tr" inputstring="${workspace.changes}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
+        <exec executable="tr" inputstring="${workspace.changes.sed}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
             <arg value="-s" />
             <arg value="[:space:]" />
             <arg value="," />
diff --git a/router/java/src/net/i2p/router/CommandLine.java b/router/java/src/net/i2p/router/CommandLine.java
index 65c789e4123f7b451f31ab167d23b060d756bf9c..0d955338edfa67ef2c8b29fbec100512a79733ff 100644
--- a/router/java/src/net/i2p/router/CommandLine.java
+++ b/router/java/src/net/i2p/router/CommandLine.java
@@ -23,6 +23,7 @@ public class CommandLine extends net.i2p.util.CommandLine {
         "net.i2p.router.RouterVersion",
         "net.i2p.router.crypto.FamilyKeyCrypto",
         "net.i2p.router.naming.BlockfileNamingService",
+        "net.i2p.router.networkdb.reseed.Reseeder",
         "net.i2p.router.peermanager.ProfileOrganizer",
         "net.i2p.router.tasks.CryptoChecker",
         "net.i2p.router.time.NtpClient",
diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java
index 03c0ae981e5cf057e5ae1a677a3cf6a46e197a51..1de70e3bf38981f520395c4854781fe9445f166c 100644
--- a/router/java/src/net/i2p/router/Router.java
+++ b/router/java/src/net/i2p/router/Router.java
@@ -142,6 +142,8 @@ public class Router implements RouterClock.ClockShiftListener {
     private static final String PROP_JBIGI = "jbigi.loadedResource";
     private static final String PROP_JBIGI_PROCESSOR = "jbigi.lastProcessor";
     public static final String UPDATE_FILE = "i2pupdate.zip";
+    //// remove after release ////
+    private static final boolean CONGESTION_CAPS = net.i2p.CoreVersion.PUBLISHED_VERSION.equals("0.9.58");
         
     private static final int SHUTDOWN_WAIT_SECS = 60;
 
@@ -1096,6 +1098,14 @@ public class Router implements RouterClock.ClockShiftListener {
     
     public static final char CAPABILITY_REACHABLE = 'R';
     public static final char CAPABILITY_UNREACHABLE = 'U';
+
+    /** @since 0.9.58, proposal 162 */
+    public static final char CAPABILITY_CONGESTION_MODERATE = 'D';
+    /** @since 0.9.58, proposal 162 */
+    public static final char CAPABILITY_CONGESTION_SEVERE = 'E';
+    /** @since 0.9.58, proposal 162 */
+    public static final char CAPABILITY_NO_TUNNELS = 'G';
+
     /** for testing */
     public static final String PROP_FORCE_UNREACHABLE = "router.forceUnreachable";
 
@@ -1184,6 +1194,8 @@ public class Router implements RouterClock.ClockShiftListener {
         
         if (hidden || _context.getBooleanProperty(PROP_FORCE_UNREACHABLE)) {
             rv.append(CAPABILITY_UNREACHABLE);
+            if (CONGESTION_CAPS)
+                rv.append(CAPABILITY_NO_TUNNELS);
             return rv.toString();
         }
         switch (_context.commSystem().getStatus()) {
@@ -1214,6 +1226,31 @@ public class Router implements RouterClock.ClockShiftListener {
                 // no explicit capability
                 break;
         }
+
+        char cong = 0;
+        int maxTunnels = _context.getProperty(RouterThrottleImpl.PROP_MAX_TUNNELS, RouterThrottleImpl.DEFAULT_MAX_TUNNELS);
+        if (maxTunnels <= 0) {
+            cong = CAPABILITY_NO_TUNNELS;
+        } else if (maxTunnels <= 50 || SystemVersion.isSlow()) {
+            cong = CAPABILITY_CONGESTION_MODERATE;
+        } else {
+            int numTunnels = _context.tunnelManager().getParticipatingCount();
+            if (numTunnels > 9 * maxTunnels / 10) {
+                cong = CAPABILITY_CONGESTION_SEVERE;
+            } else if (numTunnels > 8 * maxTunnels / 10) {
+                cong = CAPABILITY_CONGESTION_MODERATE;
+            } else {
+                // TODO
+            }
+        }
+        if (cong != 0) {
+            if (CONGESTION_CAPS) {
+                rv.append(cong);
+            } else {
+                if (_log.shouldWarn())
+                    _log.warn("Congestion cap: " + cong);
+            }
+        }
         return rv.toString();
     }
     
diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java
index aee15848badbca4521bf1efa78db743907e65e98..35ab2b80b1541b415c48542b42a802e96ab15702 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 = "Git";
     public final static String VERSION = CoreVersion.VERSION;
-    public final static long BUILD = 2;
+    public final static long BUILD = 4;
 
     /** for example "-test" */
     public final static String EXTRA = "";
diff --git a/router/java/src/net/i2p/router/networkdb/reseed/Reseeder.java b/router/java/src/net/i2p/router/networkdb/reseed/Reseeder.java
index 6cbdaa59a3b21433e38ad89a84956294e65ec415..f9440e711753cd3f556cc9d02794879591e7e909 100644
--- a/router/java/src/net/i2p/router/networkdb/reseed/Reseeder.java
+++ b/router/java/src/net/i2p/router/networkdb/reseed/Reseeder.java
@@ -1219,15 +1219,102 @@ public class Reseeder {
         return Translate.getString(n, s, p, _context, BUNDLE_NAME);
     }
 
-/******
-    public static void main(String args[]) {
-        if ( (args != null) && (args.length == 1) && (!Boolean.parseBoolean(args[0])) ) {
-            System.out.println("Not reseeding, as requested");
-            return; // not reseeding on request
+    /**
+     *  @since 0.9.58
+     */
+    public static void main(String args[]) throws Exception {
+        if (args.length == 1 && args[0].equals("help")) {
+            System.out.println("Usage: reseeder [https://hostname/ ...]");
+            System.exit(1);
+        }
+        File f = new File("certificates");
+        if (!f.exists()) {
+            System.out.println("Must be run from $I2P or have symlink to $I2P/certificates in this directory");
+            System.exit(1);
+        }
+        String[] urls = (args.length > 0) ? args : DataHelper.split(DEFAULT_SSL_SEED_URL, ",");
+        int pass = 0, fail = 0;
+        SSLEepGet.SSLState sslState = null;
+        I2PAppContext ctx = I2PAppContext.getGlobalContext();
+        for (String url : urls) {
+            url += SU3_FILENAME + NETID_PARAM + '2';
+            URI uri = new URI(url);
+            String host = uri.getHost();
+            System.out.println("Testing " + host);
+            File su3 = new File(host + ".su3");
+            su3.delete();
+            try {
+                SSLEepGet get;
+                if (sslState == null) {
+                    get = new SSLEepGet(ctx, su3.getPath(), url);
+                    sslState = get.getSSLState();
+                } else {
+                    get = new SSLEepGet(ctx, su3.getPath(), url, sslState);
+                }
+                if (get.fetch()) {
+                    int rc = get.getStatusCode();
+                    if (rc == 200) {
+                        SU3File su3f = new SU3File(su3);
+                        File zip = new File(host + ".zip");
+                        zip.delete();
+                        su3f.verifyAndMigrate(zip);
+                        SU3File.main(new String[] {"showversion", su3.getPath()});
+                        String version = su3f.getVersionString();
+                        long ver = Long.parseLong(version.trim()) * 1000;
+                        long cutoff = System.currentTimeMillis() - MAX_FILE_AGE / 4;
+                        if (ver < cutoff)
+                            throw new IOException("su3 file too old");
+                        java.util.zip.ZipFile zipf = new java.util.zip.ZipFile(zip);
+                        java.util.Enumeration<? extends java.util.zip.ZipEntry> entries = zipf.entries();
+                        int ri = 0, old = 0;
+                        while (entries.hasMoreElements()) {
+                            java.util.zip.ZipEntry entry = (java.util.zip.ZipEntry) entries.nextElement();
+                            net.i2p.data.router.RouterInfo r = new net.i2p.data.router.RouterInfo();
+                            InputStream in = zipf.getInputStream(entry);
+                            r.readBytes(in);
+                            in.close();
+                            if (r.getPublished() > cutoff)
+                                ri++;
+                            else
+                                old++;
+                        }
+                        zipf.close();
+                        if (old > 0) {
+                            System.out.println("Test failed for " + host + ", returned " + old + " old router infos");
+                            fail++;
+                        } else if (ri >= 50) {
+                            System.out.println("Test passed for " + host + ", returned " + ri + " router infos");
+                            pass++;
+                        } else {
+                            System.out.println("Test failed for " + host + ", returned only " + ri + " router infos");
+                            fail++;
+                        }
+                    } else {
+                        System.out.println("Test failed for " + host + " return code: " + rc);
+                        su3.delete();
+                        fail++;
+                    }
+                } else {
+                    int rc = get.getStatusCode();
+                    System.out.println("Test failed for " + host + " return code: " + rc);
+                    su3.delete();
+                    fail++;
+                }
+            } catch (Exception ioe) {
+                System.out.println("Test failed for " + host + ": " + ioe);
+                ioe.printStackTrace();
+                if (su3.exists()) {
+                    try {
+                        SU3File.main(new String[] {"showversion", su3.getPath()});
+                    } catch (Exception e) {}
+                    su3.delete();
+                }
+                fail++;
+            }
+            System.out.println();
         }
-        System.out.println("Reseeding");
-        Reseeder reseedHandler = new Reseeder();
-        reseedHandler.requestReseed();
+        System.out.println("Passed: " + pass + "; Failed: " + fail);
+        if (fail > 0)
+            System.exit(1);
     }
-******/
 }
diff --git a/router/java/src/net/i2p/router/startup/ClientAppConfig.java b/router/java/src/net/i2p/router/startup/ClientAppConfig.java
index 291c2d66593dccdacdea91f7052e7233aff629a3..cc12c88cc07b43aa811b2c1a84c9806add779b10 100644
--- a/router/java/src/net/i2p/router/startup/ClientAppConfig.java
+++ b/router/java/src/net/i2p/router/startup/ClientAppConfig.java
@@ -15,7 +15,7 @@ import net.i2p.router.RouterContext;
 import net.i2p.util.FileSuffixFilter;
 import net.i2p.util.FileUtil;
 import net.i2p.util.Log;
-import net.i2p.util.ObjectCounter;
+import net.i2p.util.ObjectCounterUnsafe;
 import net.i2p.util.OrderedProperties;
 import net.i2p.util.SecureDirectory;
 import net.i2p.util.SystemVersion;
@@ -349,7 +349,7 @@ public class ClientAppConfig {
      */
     public synchronized static void writeClientAppConfig(I2PAppContext ctx, List<ClientAppConfig> apps) throws IOException {
         // Gather the set of config files
-        ObjectCounter<File> counter = new ObjectCounter<File>();
+        ObjectCounterUnsafe<File> counter = new ObjectCounterUnsafe<File>();
         for (ClientAppConfig cac : apps) {
             File f = cac.configFile;
             if (f == null)
diff --git a/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java b/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java
index 313531fdeb034455f7b1992cca13aff723b74599..a14281008a4a637f16c1ef8c950d4d7c9bc17754 100644
--- a/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java
+++ b/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java
@@ -46,7 +46,7 @@ class LoadRouterInfoJob extends JobImpl {
     private RouterInfo _us;
     private static final AtomicBoolean _keyLengthChecked = new AtomicBoolean();
     // 1 chance in this many to rekey if the defaults changed
-    private static final int REKEY_PROBABILITY = 4;
+    private static final int REKEY_PROBABILITY = 1;
     
     public LoadRouterInfoJob(RouterContext ctx) {
         super(ctx);
diff --git a/router/java/src/net/i2p/router/sybil/Analysis.java b/router/java/src/net/i2p/router/sybil/Analysis.java
index 074eae207f57760b23455b33f1dd0150d4d23d6c..d66c4386b204609e088fda2f513aa74f489188a3 100644
--- a/router/java/src/net/i2p/router/sybil/Analysis.java
+++ b/router/java/src/net/i2p/router/sybil/Analysis.java
@@ -41,7 +41,7 @@ import net.i2p.stat.RateAverages;
 import net.i2p.stat.RateStat;
 import net.i2p.util.Addresses;
 import net.i2p.util.Log;
-import net.i2p.util.ObjectCounter;
+import net.i2p.util.ObjectCounterUnsafe;
 import net.i2p.util.SystemVersion;
 import net.i2p.util.Translate;
 
@@ -683,7 +683,7 @@ public class Analysis extends JobImpl implements RouterApp {
      *  @since 0.9.38 split out from renderIPGroups32()
      */
     public Map<Integer, List<RouterInfo>> calculateIPGroups32(List<RouterInfo> ris, Map<Hash, Points> points) {
-        ObjectCounter<Integer> oc = new ObjectCounter<Integer>();
+        ObjectCounterUnsafe<Integer> oc = new ObjectCounterUnsafe<Integer>();
         for (RouterInfo info : ris) {
             byte[] ip = getIP(info);
             if (ip == null)
@@ -732,7 +732,7 @@ public class Analysis extends JobImpl implements RouterApp {
      *  @since 0.9.38 split out from renderIPGroups24()
      */
     public Map<Integer, List<RouterInfo>> calculateIPGroups24(List<RouterInfo> ris, Map<Hash, Points> points) {
-        ObjectCounter<Integer> oc = new ObjectCounter<Integer>();
+        ObjectCounterUnsafe<Integer> oc = new ObjectCounterUnsafe<Integer>();
         for (RouterInfo info : ris) {
             byte[] ip = getIP(info);
             if (ip == null)
@@ -785,7 +785,7 @@ public class Analysis extends JobImpl implements RouterApp {
      *  @since 0.9.38 split out from renderIPGroups16()
      */
     public Map<Integer, List<RouterInfo>> calculateIPGroups16(List<RouterInfo> ris, Map<Hash, Points> points) {
-        ObjectCounter<Integer> oc = new ObjectCounter<Integer>();
+        ObjectCounterUnsafe<Integer> oc = new ObjectCounterUnsafe<Integer>();
         for (RouterInfo info : ris) {
             byte[] ip = getIP(info);
             if (ip == null)
@@ -828,7 +828,7 @@ public class Analysis extends JobImpl implements RouterApp {
      *  @since 0.9.57
      */
     public Map<Long, List<RouterInfo>> calculateIPGroups64(List<RouterInfo> ris, Map<Hash, Points> points) {
-        ObjectCounter<Long> oc = new ObjectCounter<Long>();
+        ObjectCounterUnsafe<Long> oc = new ObjectCounterUnsafe<Long>();
         for (RouterInfo info : ris) {
             byte[] ip = getIPv6(info);
             if (ip == null)
@@ -893,7 +893,7 @@ public class Analysis extends JobImpl implements RouterApp {
      *  @since 0.9.57
      */
     public Map<Long, List<RouterInfo>> calculateIPGroups48(List<RouterInfo> ris, Map<Hash, Points> points) {
-        ObjectCounter<Long> oc = new ObjectCounter<Long>();
+        ObjectCounterUnsafe<Long> oc = new ObjectCounterUnsafe<Long>();
         for (RouterInfo info : ris) {
             byte[] ip = getIPv6(info);
             if (ip == null)
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 0de5f6300ba9a7fc1cdf854329ae38275f578b85..141bd411447d4c3fc80a2e78b24677e5a9d7d914 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
@@ -1099,6 +1099,15 @@ public class NTCPTransport extends TransportImpl {
                             _log.warn("Already listening on " + addr);
                         return null;
                     }
+                    // if UDP only changed external port and not internal port,
+                    // do not rebind internally and restart, just change the address
+                    int eport = _context.getProperty(UDPTransport.PROP_EXTERNAL_PORT, 0);
+                    int iport = _context.getProperty(UDPTransport.PROP_INTERNAL_PORT, 0);
+                    if (port == eport && iport > 0 && eport != iport) {
+                        if (_log.shouldWarn())
+                            _log.warn("External port changed to " + eport + ", keep listening on internal port " + iport);
+                        return null;
+                    }
                     // FIXME support multiple binds
                     // FIXME just close and unregister
                     stopWaitAndRestart();
@@ -1617,6 +1626,8 @@ public class NTCPTransport extends TransportImpl {
         String cport = _context.getProperty(PROP_I2NP_NTCP_PORT);
         if (cport != null && cport.length() > 0) {
             nport = cport;
+            if (port > 0 && !nport.equals(Integer.toString(port)))
+                _log.logAlways(Log.WARN, "UDP detected external port is " + port + " but TCP configured port is " + nport);
         } else if (_context.getBooleanPropertyDefaultTrue(PROP_I2NP_NTCP_AUTO_PORT)) {
             // 0.9.6 change
             // This wasn't quite right, as udpAddr is the EXTERNAL port and we really
diff --git a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
index 4914b7628b47edf257dfd6fe7aafaf5eadd3b152..82491dc811cc807a5bb6baf4a22f098f3cc72195 100644
--- a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
@@ -826,10 +826,10 @@ class EstablishmentManager {
         if (_transport.isTooClose(to.getIP()))
             return;
         DatagramPacket pkt = fromPacket.getPacket();
-        int off = pkt.getOffset();
         int len = pkt.getLength();
         if (len < MIN_LONG_DATA_LEN)
             return;
+        int off = pkt.getOffset();
         byte data[] = pkt.getData();
         int type = data[off + TYPE_OFFSET] & 0xff;
         if (type == SSU2Util.SESSION_REQUEST_FLAG_BYTE && len < MIN_SESSION_REQUEST_LEN)
diff --git a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java
index 15555ca2c7784fc01f63c579519566b1e7355ac8..c4f8df7e6dfc3fb6c12a41ae4389bc45c6c52550 100644
--- a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java
+++ b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java
@@ -7,6 +7,7 @@ import java.net.InetSocketAddress;
 import java.net.SocketAddress;
 import java.net.UnknownHostException;
 import java.security.GeneralSecurityException;
+import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -829,6 +830,11 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
                        "\nGenerated header key 2 for A->B:  " + Base64.encode(h_ab) +
                        "\nGenerated header key 2 for B->A:  " + Base64.encode(h_ba));
        ****/
+        Arrays.fill(ckd, (byte) 0);
+        Arrays.fill(k_ab, (byte) 0);
+        Arrays.fill(k_ba, (byte) 0);
+        Arrays.fill(d_ab, (byte) 0);
+        Arrays.fill(d_ba, (byte) 0);
         _handshakeState.destroy();
         if (_createdSentCount == 1)
             _rtt = (int) ( _context.clock().now() - _lastSend );
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java
index 0e98bac85870703e632c1bd0796f8c53a10f1d9b..092e20337565fdd8c890bc9e213ca05606fbbe8e 100644
--- a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java
@@ -6,6 +6,7 @@ import java.net.InetSocketAddress;
 import java.net.SocketAddress;
 import java.net.UnknownHostException;
 import java.security.GeneralSecurityException;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -766,6 +767,11 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
                            "\nGenerated header key 2 for A->B:  " + Base64.encode(h_ab) +
                            "\nGenerated header key 2 for B->A:  " + Base64.encode(h_ba));
             ****/
+            Arrays.fill(ckd, (byte) 0);
+            Arrays.fill(k_ab, (byte) 0);
+            Arrays.fill(k_ba, (byte) 0);
+            Arrays.fill(d_ab, (byte) 0);
+            Arrays.fill(d_ba, (byte) 0);
             _handshakeState.destroy();
             if (_requestSentCount == 1)
                 _rtt = (int) ( _context.clock().now() - _lastSend );
diff --git a/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java b/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
index 4dfde740fa0a211ce567b19dee589f0aa4e12f34..9ddf763a7e9ce4172036c50249ccec7a39fefcde 100644
--- a/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
@@ -1964,6 +1964,7 @@ class PeerTestManager {
                     // More sanity checks here.
                     // we compare to the test address,
                     // however our address may have changed during the test
+                    Hash charlieHash = test.getCharlieHash();
                     boolean portok = addrBlockPort == test.getAlicePort();
                     boolean IPok = DataHelper.eq(addrBlockIP, test.getAliceIP().getAddress());
                     if (!portok || !IPok) {
@@ -1979,7 +1980,8 @@ class PeerTestManager {
                                 // Port different. Charlie probably symmetric natted.
                                 // The result will be OK
                                 // Note that charlie is probably not reachable
-                                _transport.markUnreachable(test.getCharlieHash());
+                                if (charlieHash != null)
+                                    _transport.markUnreachable(charlieHash);
                                 // Reset port so testComplete() will return success.
                                 test.setAlicePortFromCharlie(test.getAlicePort());
                                 // set bad so we don't call externalAddressReceived()
@@ -1992,7 +1994,8 @@ class PeerTestManager {
                                 // Both IP and port changed, don't trust it
                                 // The result will be OK
                                 // Note that charlie is probably not reachable
-                                _transport.markUnreachable(test.getCharlieHash());
+                                if (charlieHash != null)
+                                    _transport.markUnreachable(charlieHash);
                                 // Reset IP and port so testComplete() will return success.
                                 test.setAliceIPFromCharlie(test.getAliceIP());
                                 test.setAlicePortFromCharlie(test.getAlicePort());
@@ -2009,8 +2012,8 @@ class PeerTestManager {
                     }
                     // We already call externalAddressReceived() for every outbound connection from EstablishmentManager
                     // but we can use this also to update our address faster
-                    if (!bad)
-                        _transport.externalAddressReceived(state.getCharlieHash(), addrBlockIP, addrBlockPort);
+                    if (!bad && charlieHash != null)
+                        _transport.externalAddressReceived(charlieHash, addrBlockIP, addrBlockPort);
                 }
                 testComplete();
                 break;
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 ddfce655580045c8740c885f425dc87cd2323973..cba2007dbc660e124fa9fd2f51de8dff55f3ba23 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
@@ -104,8 +104,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
     private final SSUHMACGenerator _hmac;
     private int _mtu = PeerState.MIN_MTU;
     private int _mtu_ipv6 = PeerState.MIN_IPV6_MTU;
-    private int _mtu_ssu2 = PeerState2.MIN_SSU_IPV4_MTU;
-    private int _mtu_ssu2_ipv6 = PeerState2.MIN_SSU_IPV6_MTU;
+    private int _mtu_ssu2;
+    private int _mtu_ssu2_ipv6;
     private final int _defaultMTU;
     private boolean _mismatchLogged;
     private final int _networkID;
@@ -282,6 +282,20 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
 
     // various state bitmaps
 
+    private static final Set<Status> STATUS_IPV4_UNK =   EnumSet.of(Status.UNKNOWN,
+                                                                    Status.DISCONNECTED,
+                                                                    Status.HOSED,
+                                                                    Status.IPV4_UNKNOWN_IPV6_OK,
+                                                                    Status.IPV4_UNKNOWN_IPV6_FIREWALLED);
+
+    private static final Set<Status> STATUS_IPV6_UNK =   EnumSet.of(Status.UNKNOWN,
+                                                                    Status.DISCONNECTED,
+                                                                    Status.HOSED,
+                                                                    Status.IPV4_OK_IPV6_UNKNOWN,
+                                                                    Status.IPV4_FIREWALLED_IPV6_UNKNOWN,
+                                                                    Status.IPV4_SNAT_IPV6_UNKNOWN,
+                                                                    Status.IPV4_DISABLED_IPV6_UNKNOWN);
+
     private static final Set<Status> STATUS_IPV4_FW =    EnumSet.of(Status.DIFFERENT,
                                                                     Status.REJECT_UNSOLICITED,
                                                                     Status.IPV4_FIREWALLED_IPV6_OK,
@@ -323,10 +337,6 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
                                                                     Status.IPV4_DISABLED_IPV6_FIREWALLED,
                                                                     Status.DISCONNECTED);
 
-    private static final Set<Status> STATUS_NEED_INTRO = EnumSet.of(Status.REJECT_UNSOLICITED,
-                                                                    Status.IPV4_FIREWALLED_IPV6_OK,
-                                                                    Status.IPV4_FIREWALLED_IPV6_UNKNOWN);
-
     private static final Set<Status> STATUS_OK =         EnumSet.of(Status.OK,
                                                                     Status.IPV4_DISABLED_IPV6_OK);
 
@@ -444,6 +454,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
         // SSU2 key and IV generation if required
         _enableSSU1 = dh != null;
         _defaultMTU = _enableSSU1 ? PeerState.LARGE_MTU : PeerState2.DEFAULT_MTU;
+        _mtu_ssu2 = _enableSSU1 ? PeerState2.MIN_SSU_IPV4_MTU : PeerState2.MIN_MTU;
+        _mtu_ssu2_ipv6 = _enableSSU1 ? PeerState2.MIN_SSU_IPV6_MTU : PeerState2.MIN_MTU;
         boolean enableSSU2 = xdh != null;
         if (enableSSU2) {
             // if any ipv4 address is lower than 1280 MTU, disable
@@ -539,8 +551,9 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
         if (port <= 0) {
             port = TransportUtil.selectRandomPort(_context, STYLE);
             Map<String, String> changes = new HashMap<String, String>(2);
-            changes.put(PROP_INTERNAL_PORT, Integer.toString(port));
-            changes.put(PROP_EXTERNAL_PORT, Integer.toString(port));
+            String sport = Integer.toString(port);
+            changes.put(PROP_INTERNAL_PORT, sport);
+            changes.put(PROP_EXTERNAL_PORT, sport);
             _context.router().saveConfig(changes, null);
             _log.logAlways(Log.INFO, "UDP selected random port " + port);
         }
@@ -756,12 +769,14 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
             return;
         }
         if (newPort > 0 &&
-            (newPort != port || newPort != oldIPort || newPort != oldEPort)) {
+            (newPort != port || newPort != oldIPort)) {
             // attempt to use it as our external port - this will be overridden by
             // externalAddressReceived(...)
             Map<String, String> changes = new HashMap<String, String>();
-            changes.put(PROP_INTERNAL_PORT, Integer.toString(newPort));
-            changes.put(PROP_EXTERNAL_PORT, Integer.toString(newPort));
+            String sport = Integer.toString(newPort);
+            changes.put(PROP_INTERNAL_PORT, sport);
+            if (oldEPort <= 0)
+                changes.put(PROP_EXTERNAL_PORT, sport);
             _context.router().saveConfig(changes, null);
         }
 
@@ -1424,7 +1439,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
                 if (!isIPv6) {
                     if (from.equals(_lastFromv4) || !eq(_lastOurIPv4, _lastOurPortv4, ourIP, ourPort)) {
                         if (_log.shouldLog(Log.INFO))
-                            _log.info("The router " + from + " told us we have a new IP - " 
+                            _log.info("The router " + from + " told us we have a new IP/port - " 
                                       + Addresses.toString(ourIP, ourPort) + ".  Wait until somebody else tells us the same thing.");
                     } else {
                         changeIt = true;
@@ -1436,7 +1451,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
                 } else {
                     if (from.equals(_lastFromv6) || !eq(_lastOurIPv6, _lastOurPortv6, ourIP, ourPort)) {
                         if (_log.shouldLog(Log.INFO))
-                            _log.info("The router " + from + " told us we have a new IP - " 
+                            _log.info("The router " + from + " told us we have a new IP/port - " 
                                       + Addresses.toString(ourIP, ourPort) + ".  Wait until somebody else tells us the same thing.");
                     } else {
                         changeIt = true;
@@ -1468,12 +1483,12 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
      * @return true if updated
      */
     private boolean changeAddress(byte ourIP[], int ourPort) {
-        // this defaults to true when we are firewalled and false otherwise.
-        boolean fixedPort = getIsPortFixed();
         boolean updated = false;
         boolean fireTest = false;
 
         boolean isIPv6 = ourIP.length == 16;
+        // this defaults to true when we are firewalled or unknown and false otherwise.
+        boolean fixedPort = getIsPortFixed(isIPv6);
 
         synchronized (_rebuildLock) {
             RouterAddress current = getCurrentExternalAddress(isIPv6);
@@ -1537,8 +1552,18 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
                     //if ( (_reachabilityStatus != CommSystemFacade.STATUS_OK) ||
                     //     (_externalListenHost == null) || (_externalListenPort <= 0) ||
                     //     (_context.clock().now() - _reachabilityStatusLastUpdated > 2*TEST_FREQUENCY) ) {
-                        // they told us something different and our tests are either old or failing
+
+                    // they told us something different and our tests are either old or failing
                     if (rebuild) {
+                            if (externalListenPort > 0 && ourPort > 0 &&
+                                externalListenPort != ourPort &&
+                                _context.getProperty(PROP_EXTERNAL_PORT, 0) != ourPort) {
+                                // save the external port setting only
+                                _context.router().saveConfig(PROP_EXTERNAL_PORT, Integer.toString(ourPort));
+                                _context.router().eventLog().addEvent(EventLog.CHANGE_PORT, "IPv" +
+                                                                                            (isIPv6 ? '6' : '4') +
+                                                                                            " port " + ourPort);
+                            }
                             if (_enableSSU2) {
                                 // flush SSU2 tokens
                                 if (ourPort != externalListenPort) {
@@ -1666,13 +1691,23 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
      *  our firewall is changing our port), unless overridden by the property.
      *  We must have an accurate external port when firewalled, or else
      *  our signature of the SessionCreated packet will be invalid.
+     *
+     *  As of 0.9.58, returns false if status is UNKNOWN
      */
-    private boolean getIsPortFixed() {
+    private boolean getIsPortFixed(boolean isIPv6) {
         String prop = _context.getProperty(PROP_FIXED_PORT);
         if (prop != null)
             return Boolean.parseBoolean(prop);
         Status status = getReachabilityStatus();
-        return !STATUS_NEED_INTRO.contains(status);
+        if (isIPv6) {
+            if (STATUS_IPV6_UNK.contains(status))
+                return false;
+            return !STATUS_IPV6_FW.contains(status);
+        } else {
+            if (STATUS_IPV4_UNK.contains(status))
+                return false;
+            return !STATUS_IPV4_FW.contains(status);
+        }
     }
 
     /** 
@@ -4000,7 +4035,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
                 // to prevent thrashing
                 if ((STATUS_OK.contains(old) && STATUS_FW.contains(status)) ||
                     (STATUS_OK.contains(status) && STATUS_FW.contains(old)) ||
-                    (STATUS_FW.contains(status) && STATUS_FW.contains(old))) {
+                    (STATUS_FW.contains(status) && STATUS_FW.contains(old)) ||
+                    (!isIPv6 && STATUS_IPV4_UNK.contains(old) && !STATUS_IPV4_UNK.contains(status))) {
                     if (status != _reachabilityStatusPending) {
                         if (_log.shouldLog(Log.WARN))
                             _log.warn("Old status: " + old + " status pending confirmation: " + status +
diff --git a/router/java/src/net/i2p/router/tunnel/TunnelDispatcher.java b/router/java/src/net/i2p/router/tunnel/TunnelDispatcher.java
index ce287f2d6585185034d222f5ecfa8792a7396d23..e04816b9222d1e6455f35e31a8ffabfb9664d9d6 100644
--- a/router/java/src/net/i2p/router/tunnel/TunnelDispatcher.java
+++ b/router/java/src/net/i2p/router/tunnel/TunnelDispatcher.java
@@ -949,7 +949,7 @@ public class TunnelDispatcher implements Service {
             long now = getContext().clock().now() + LEAVE_BATCH_TIME; // leave all expiring in next 10 sec
             long nextTime = now + 10*60*1000;
             while ((cur = _configs.peek()) != null) {
-                long exp = cur.getExpiration() + (2 * Router.CLOCK_FUDGE_FACTOR) + LEAVE_BATCH_TIME;
+                long exp = cur.getExpiration() + (3 * Router.CLOCK_FUDGE_FACTOR / 2) + LEAVE_BATCH_TIME;
                 if (exp < now) {
                     _configs.poll();
                     if (_log.shouldLog(Log.INFO))
diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java b/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java
index 7d655ff96b51210f738dd7dced0b029022d64982..395d93a1304c8ba05ba799345d6b5522080081ab 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java
@@ -226,7 +226,7 @@ class BuildExecutor implements Runnable {
             }
         }
         
-        _context.statManager().addRateData("tunnel.concurrentBuilds", concurrent, 0);
+        _context.statManager().addRateData("tunnel.concurrentBuilds", concurrent);
         
         long lag = _context.jobQueue().getMaxLag();
         if ( (lag > 2000) && (_context.router().getUptime() > 5*60*1000) ) {
@@ -420,7 +420,7 @@ class BuildExecutor implements Runnable {
                                     continue;
                                 }
                                 long pTime = System.currentTimeMillis() - bef;
-                                _context.statManager().addRateData("tunnel.buildConfigTime", pTime, 0);
+                                _context.statManager().addRateData("tunnel.buildConfigTime", pTime);
                                 if (_log.shouldLog(Log.DEBUG))
                                     _log.debug("Configuring new tunnel " + i + " for " + pool + ": " + cfg);
                                 buildTunnel(cfg);
@@ -544,7 +544,7 @@ class BuildExecutor implements Runnable {
             return;
         if (cfg.getLength() > 1) {
             long buildTime = System.currentTimeMillis() - beforeBuild;
-            _context.statManager().addRateData("tunnel.buildRequestTime", buildTime, 0);
+            _context.statManager().addRateData("tunnel.buildRequestTime", buildTime);
         }
         long id = cfg.getReplyMessageId();
         if (id > 0) {
@@ -649,7 +649,7 @@ class BuildExecutor implements Runnable {
         if (rv != null) {
             long requestedOn = rv.getExpiration() - 10*60*1000;
             long rtt = _context.clock().now() - requestedOn;
-            _context.statManager().addRateData("tunnel.buildReplySlow", rtt, 0);
+            _context.statManager().addRateData("tunnel.buildReplySlow", rtt);
             if (_log.shouldInfo())
                 _log.info("Got reply late (rtt = " + rtt + ") for: " + rv);
         }
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 33920f2b70f6db83b55175135e953634c070b8d5..c2e01b69a995da372498acbbf42031e7c9fd5462 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
@@ -541,7 +541,7 @@ abstract class BuildRequestor {
         public void runJob() {
             _exec.buildComplete(_cfg, OTHER_FAILURE);
             getContext().profileManager().tunnelTimedOut(_cfg.getPeer(1));
-            getContext().statManager().addRateData("tunnel.buildFailFirstHop", 1, 0);
+            getContext().statManager().addRateData("tunnel.buildFailFirstHop", 1);
             // static, no _log
             //System.err.println("Cant contact first hop for " + _cfg);
         }
diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPool.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPool.java
index 34372fda414d807d9549c71d024344dd95e9bb25..8c48ae150369749835933e7fda899f9d989691d7 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPool.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPool.java
@@ -166,6 +166,7 @@ public class TunnelPool {
     private TunnelInfo selectTunnel(boolean allowRecurseOnFail) {
         boolean avoidZeroHop = !_settings.getAllowZeroHop();
         
+        long now = _context.clock().now();
         synchronized (_tunnels) {
             if (_tunnels.isEmpty()) {
                 if (_log.shouldLog(Log.WARN))
@@ -181,7 +182,7 @@ public class TunnelPool {
                         if (_lastSelectedIdx >= _tunnels.size())
                             _lastSelectedIdx = 0;
                         TunnelInfo info = _tunnels.get(_lastSelectedIdx);
-                        if ( (info.getLength() > 1) && (info.getExpiration() > _context.clock().now()) ) {
+                        if (info.getLength() > 1 && info.getExpiration() > now) {
                             // avoid outbound tunnels where the 1st hop is backlogged
                             if (_settings.isInbound() || !_context.commSystem().isBacklogged(info.getPeer(1))) {
                                 return info;
@@ -201,7 +202,7 @@ public class TunnelPool {
                 // randomly
                 for (int i = 0; i < _tunnels.size(); i++) {
                     TunnelInfo info = _tunnels.get(i);
-                    if (info.getExpiration() > _context.clock().now()) {
+                    if (info.getExpiration() > now) {
                         // avoid outbound tunnels where the 1st hop is backlogged
                         if (_settings.isInbound() || info.getLength() <= 1 ||
                             !_context.commSystem().isBacklogged(info.getPeer(1))) {
@@ -243,12 +244,13 @@ public class TunnelPool {
     TunnelInfo selectTunnel(Hash closestTo) {
         boolean avoidZeroHop = !_settings.getAllowZeroHop();
         TunnelInfo rv = null;
+        long now = _context.clock().now();
         synchronized (_tunnels) {
             if (!_tunnels.isEmpty()) {
                 if (_tunnels.size() > 1)
                     Collections.sort(_tunnels, new TunnelInfoComparator(closestTo, avoidZeroHop));
                 for (TunnelInfo info : _tunnels) {
-                    if (info.getExpiration() > _context.clock().now()) {
+                    if (info.getExpiration() > now) {
                         rv = info;
                         break;
                     }
@@ -616,7 +618,7 @@ public class TunnelPool {
         long et = now - _lastRateUpdate;
         if (et > 2*60*1000) {
             long bw = 1024 * (_lifetimeProcessed - _lastLifetimeProcessed) * 1000 / et;   // Bps
-            _context.statManager().addRateData(_rateName, bw, 0);
+            _context.statManager().addRateData(_rateName, bw);
             _lastRateUpdate = now;
             _lastLifetimeProcessed = _lifetimeProcessed;
         }
@@ -969,7 +971,7 @@ public class TunnelPool {
                        + " soon " + expireSoon + " later " + expireLater
                        + " std " + wanted + " inProgress " + inProgress + " fallback " + fallback 
                        + " for " + toString());
-            _context.statManager().addRateData(rateName, rv + inProgress, 0);
+            _context.statManager().addRateData(rateName, rv + inProgress);
             return rv;
         }
 
@@ -1022,7 +1024,7 @@ public class TunnelPool {
         
         int rv = countHowManyToBuild(allowZeroHop, expire30s, expire90s, expire150s, expire210s, expire270s, 
                                    expireLater, wanted, inProgress, fallback);
-        _context.statManager().addRateData(rateName, (rv > 0 || inProgress > 0) ? 1 : 0, 0);
+        _context.statManager().addRateData(rateName, (rv > 0 || inProgress > 0) ? 1 : 0);
         return rv;
 
     }
diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java
index 27460c6a824e3137205b214193312c8fd82866c5..e6a13f91a9ee7d0fedce81443e48be415b18be8e 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java
@@ -24,7 +24,7 @@ import net.i2p.router.TunnelPoolSettings;
 import net.i2p.router.tunnel.TunnelDispatcher;
 import net.i2p.util.I2PThread;
 import net.i2p.util.Log;
-import net.i2p.util.ObjectCounter;
+import net.i2p.util.ObjectCounterUnsafe;
 import net.i2p.util.SimpleTimer;
 
 /**
@@ -645,7 +645,7 @@ public class TunnelPoolManager implements TunnelManagerFacade {
     }
 
     /** @return total number of non-fallback expl. + client tunnels */
-    private int countTunnelsPerPeer(ObjectCounter<Hash> lc) {
+    private int countTunnelsPerPeer(ObjectCounterUnsafe<Hash> lc) {
         List<TunnelPool> pools = new ArrayList<TunnelPool>();
         listPools(pools);
         int tunnelCount = 0;
@@ -681,7 +681,7 @@ public class TunnelPoolManager implements TunnelManagerFacade {
      *  @return Set of peers that should not be allowed in another tunnel
      */
     public Set<Hash> selectPeersInTooManyTunnels() {
-        ObjectCounter<Hash> lc = new ObjectCounter<Hash>();
+        ObjectCounterUnsafe<Hash> lc = new ObjectCounterUnsafe<Hash>();
         int tunnelCount = countTunnelsPerPeer(lc);
         Set<Hash> rv = new HashSet<Hash>();
         if (tunnelCount >= 4 && _context.router().getUptime() > 10*60*1000) {