diff --git a/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java b/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java
index be74de990f2c65fd3d61ae5afbc042c0f1509598..6c95e2e19653f90dc31065232e50a297354ee1c8 100644
--- a/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java
+++ b/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java
@@ -25,9 +25,9 @@ import java.io.BufferedReader;
 import java.io.BufferedWriter;
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileWriter;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
 import java.io.StringReader;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -35,6 +35,8 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 
+import net.i2p.util.SecureFileOutputStream;
+
 /**
  * Utility class providing methods to parse and write files in config file
  * format, and subscription file format.
@@ -277,7 +279,7 @@ public class ConfigParser {
      */
     public static void write(Map map, File file) throws IOException {
         ConfigParser
-                .write(map, new BufferedWriter(new FileWriter(file, false)));
+                .write(map, new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(file), "UTF-8")));
     }
 
     /**
@@ -316,7 +318,7 @@ public class ConfigParser {
     public static void writeSubscriptions(List list, File file)
             throws IOException {
         ConfigParser.writeSubscriptions(list, new BufferedWriter(
-                new FileWriter(file, false)));
+                new OutputStreamWriter(new SecureFileOutputStream(file), "UTF-8")));
     }
 
 }
diff --git a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java
index 077acdb7f02bbf7f1a8c13cd912cac599d994078..2588010118eb7156ba390464c942a1d3c4325174 100644
--- a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java
+++ b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java
@@ -29,6 +29,7 @@ import java.util.List;
 import java.util.Map;
 
 import net.i2p.I2PAppContext;
+import net.i2p.util.SecureDirectory;
 
 /**
  * Main class of addressbook.  Performs updates, and runs the main loop.
@@ -131,11 +132,11 @@ public class Daemon {
         String settingsLocation = "config.txt";
         File homeFile;
         if (args.length > 0) {
-            homeFile = new File(args[0]);
+            homeFile = new SecureDirectory(args[0]);
             if (!homeFile.isAbsolute())
-                homeFile = new File(I2PAppContext.getGlobalContext().getRouterDir(), args[0]);
+                homeFile = new SecureDirectory(I2PAppContext.getGlobalContext().getRouterDir(), args[0]);
         } else {
-            homeFile = new File(System.getProperty("user.dir"));
+            homeFile = new SecureDirectory(System.getProperty("user.dir"));
         }
         
         Map defaultSettings = new HashMap();
diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java
index 127a2bb19abfed633d7d79e440f590bff9a94e4b..6f17a810aba46d7520ff2636a2bb6beaf53b5cc2 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java
@@ -26,6 +26,7 @@ import net.i2p.util.ConcurrentHashSet;
 import net.i2p.util.EepGet;
 import net.i2p.util.FileUtil;
 import net.i2p.util.Log;
+import net.i2p.util.SecureDirectory;
 import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
 import net.i2p.util.Translate;
@@ -77,7 +78,7 @@ public class I2PSnarkUtil {
         // This is used for both announce replies and .torrent file downloads,
         // so it must be available even if not connected to I2CP.
         // so much for multiple instances
-        _tmpDir = new File(ctx.getTempDir(), "i2psnark");
+        _tmpDir = new SecureDirectory(ctx.getTempDir(), "i2psnark");
         FileUtil.rmdir(_tmpDir, false);
         _tmpDir.mkdirs();
     }
@@ -196,11 +197,14 @@ public class I2PSnarkUtil {
     
     /** connect to the given destination */
     I2PSocket connect(PeerID peer) throws IOException {
-        Hash dest = peer.getAddress().calculateHash();
+        Destination addr = peer.getAddress();
+        if (addr == null)
+            throw new IOException("Null address");
+        Hash dest = addr.calculateHash();
         if (_shitlist.contains(dest))
             throw new IOException("Not trying to contact " + dest.toBase64() + ", as they are shitlisted");
         try {
-            I2PSocket rv = _manager.connect(peer.getAddress());
+            I2PSocket rv = _manager.connect(addr);
             if (rv != null)
                 _shitlist.remove(dest);
             return rv;
@@ -224,7 +228,8 @@ public class I2PSnarkUtil {
     public File get(String url, boolean rewrite) { return get(url, rewrite, 0); }
     public File get(String url, int retries) { return get(url, true, retries); }
     public File get(String url, boolean rewrite, int retries) {
-        _log.debug("Fetching [" + url + "] proxy=" + _proxyHost + ":" + _proxyPort + ": " + _shouldProxy);
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Fetching [" + url + "] proxy=" + _proxyHost + ":" + _proxyPort + ": " + _shouldProxy);
         File out = null;
         try {
             // we could use the system tmp dir but deleteOnExit() doesn't seem to work on all platforms...
@@ -248,10 +253,12 @@ public class I2PSnarkUtil {
         }
         EepGet get = new I2PSocketEepGet(_context, _manager, retries, out.getAbsolutePath(), fetchURL);
         if (get.fetch()) {
-            _log.debug("Fetch successful [" + url + "]: size=" + out.length());
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Fetch successful [" + url + "]: size=" + out.length());
             return out;
         } else {
-            _log.warn("Fetch failed [" + url + "]");
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Fetch failed [" + url + "]");
             out.delete();
             return null;
         }
diff --git a/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java b/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java
index f8c8feaede812269b8733c14d21619aa9d3e3b9b..140d3cb468fc024d0f961b928ba3b07b44a3529d 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java
@@ -109,7 +109,8 @@ public class MetaInfo
    */
   public MetaInfo(Map m) throws InvalidBEncodingException
   {
-    _log.debug("Creating a metaInfo: " + m, new Exception("source"));
+    if (_log.shouldLog(Log.DEBUG))
+        _log.debug("Creating a metaInfo: " + m, new Exception("source"));
     BEValue val = (BEValue)m.get("announce");
     if (val == null)
         throw new InvalidBEncodingException("Missing announce string");
@@ -446,14 +447,16 @@ public class MetaInfo
         else
             buf.append(val.toString());
     }
-    _log.debug(buf.toString());
+    if (_log.shouldLog(Log.DEBUG))
+        _log.debug(buf.toString());
     byte[] infoBytes = BEncoder.bencode(info);
     //_log.debug("info bencoded: [" + Base64.encode(infoBytes, true) + "]");
     try
       {
         MessageDigest digest = MessageDigest.getInstance("SHA");
         byte hash[] = digest.digest(infoBytes);
-        _log.debug("info hash: [" + net.i2p.data.Base64.encode(hash) + "]");
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("info hash: [" + net.i2p.data.Base64.encode(hash) + "]");
         return hash;
       }
     catch(NoSuchAlgorithmException nsa)
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Peer.java b/apps/i2psnark/java/src/org/klomp/snark/Peer.java
index e0ae6e1f25449a2b47adf884629ace0d10f5aa66..da1003aaba116089bf2ebd32d4e79af8b5b06e08 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/Peer.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/Peer.java
@@ -92,7 +92,8 @@ public class Peer implements Comparable
     byte[] id  = handshake(in, out);
     this.peerID = new PeerID(id, sock.getPeerDestination());
     _id = ++__id;
-    _log.debug("Creating a new peer with " + peerID.getAddress().calculateHash().toBase64(), new Exception("creating " + _id));
+    if (_log.shouldLog(Log.DEBUG))
+        _log.debug("Creating a new peer with " + peerID.getAddress().calculateHash().toBase64(), new Exception("creating " + _id));
   }
 
   /**
@@ -129,7 +130,7 @@ public class Peer implements Comparable
     @Override
   public int hashCode()
   {
-    return peerID.hashCode() ^ (2 << _id);
+    return peerID.hashCode() ^ (7777 * (int)_id);
   }
 
   /**
@@ -150,6 +151,7 @@ public class Peer implements Comparable
 
   /**
    * Compares the PeerIDs.
+   * @deprecated unused?
    */
   public int compareTo(Object o)
   {
@@ -181,14 +183,16 @@ public class Peer implements Comparable
     if (state != null)
       throw new IllegalStateException("Peer already started");
 
-    _log.debug("Running connection to " + peerID.getAddress().calculateHash().toBase64(), new Exception("connecting"));    
+    if (_log.shouldLog(Log.DEBUG))
+        _log.debug("Running connection to " + peerID.getAddress().calculateHash().toBase64(), new Exception("connecting"));    
     try
       {
         // Do we need to handshake?
         if (din == null)
           {
             sock = util.connect(peerID);
-            _log.debug("Connected to " + peerID + ": " + sock);
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Connected to " + peerID + ": " + sock);
             if ((sock == null) || (sock.isClosed())) {
                 throw new IOException("Unable to reach " + peerID);
             }
@@ -207,14 +211,20 @@ public class Peer implements Comparable
             //  = new BufferedOutputStream(sock.getOutputStream());
             byte [] id = handshake(in, out); //handshake(bis, bos);
             byte [] expected_id = peerID.getID();
-            if (!Arrays.equals(expected_id, id))
-              throw new IOException("Unexpected peerID '"
+            if (expected_id == null) {
+                peerID.setID(id);
+            } else if (Arrays.equals(expected_id, id)) {
+                if (_log.shouldLog(Log.DEBUG))
+                    _log.debug("Handshake got matching IDs with " + toString());
+            } else {
+                throw new IOException("Unexpected peerID '"
                                     + PeerID.idencode(id)
                                     + "' expected '"
                                     + PeerID.idencode(expected_id) + "'");
-            _log.debug("Handshake got matching IDs with " + toString());
+            }
           } else {
-            _log.debug("Already have din [" + sock + "] with " + toString());
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Already have din [" + sock + "] with " + toString());
           }
         
         PeerConnectionIn in = new PeerConnectionIn(this, din);
@@ -229,7 +239,8 @@ public class Peer implements Comparable
         state = s;
         listener.connected(this);
   
-        _log.debug("Start running the reader with " + toString());
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Start running the reader with " + toString());
         // Use this thread for running the incomming connection.
         // The outgoing connection creates its own Thread.
         out.startup();
@@ -279,7 +290,8 @@ public class Peer implements Comparable
     dout.write(my_id);
     dout.flush();
     
-    _log.debug("Wrote my shared hash and ID to " + toString());
+    if (_log.shouldLog(Log.DEBUG))
+        _log.debug("Wrote my shared hash and ID to " + toString());
     
     // Handshake read - header
     byte b = din.readByte();
@@ -306,7 +318,8 @@ public class Peer implements Comparable
 
     // Handshake read - peer id
     din.readFully(bs);
-    _log.debug("Read the remote side's hash and peerID fully from " + toString());
+    if (_log.shouldLog(Log.DEBUG))
+        _log.debug("Read the remote side's hash and peerID fully from " + toString());
     return bs;
   }
 
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java b/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java
index c8693843712f0cc89f143aa9f986f9a802aba310..58ef3ae2db01c25582f858fcc26a74a095a46e41 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java
@@ -76,10 +76,12 @@ public class PeerAcceptor
         // is this working right?
         try {
           peerInfoHash = readHash(in);
-          _log.info("infohash read from " + socket.getPeerDestination().calculateHash().toBase64() 
-                    + ": " + Base64.encode(peerInfoHash));
+          if (_log.shouldLog(Log.INFO))
+              _log.info("infohash read from " + socket.getPeerDestination().calculateHash().toBase64() 
+                        + ": " + Base64.encode(peerInfoHash));
         } catch (IOException ioe) {
-            _log.info("Unable to read the infohash from " + socket.getPeerDestination().calculateHash().toBase64());
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Unable to read the infohash from " + socket.getPeerDestination().calculateHash().toBase64());
             throw ioe;
         }
         in = new SequenceInputStream(new ByteArrayInputStream(peerInfoHash), in);
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java
index 6d38950360761b8ef3cdafa592aea62b66944b88..8f24c864ac9a6c63e0bd8728cc5c587bd47933e0 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java
@@ -375,7 +375,8 @@ public class PeerCoordinator implements PeerListener
 
     if (need_more)
       {
-        _log.debug("Adding a peer " + peer.getPeerID().getAddress().calculateHash().toBase64() + " for " + metainfo.getName(), new Exception("add/run"));
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Adding a peer " + peer.getPeerID().toString() + " for " + metainfo.getName(), new Exception("add/run"));
 
         // Run the peer with us as listener and the current bitfield.
         final PeerListener listener = this;
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerID.java b/apps/i2psnark/java/src/org/klomp/snark/PeerID.java
index c4a4e9194164dc91b70bdc22c0eada56e9e1f886..37cf1a9b65ccbadad69299d2a783373ce665fc48 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/PeerID.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerID.java
@@ -24,19 +24,33 @@ import java.io.IOException;
 import java.net.UnknownHostException;
 import java.util.Map;
 
+import net.i2p.I2PAppContext;
+import net.i2p.data.Base32;
 import net.i2p.data.Base64;
+import net.i2p.data.DataHelper;
 import net.i2p.data.Destination;
 
 import org.klomp.snark.bencode.BDecoder;
 import org.klomp.snark.bencode.BEValue;
 import org.klomp.snark.bencode.InvalidBEncodingException;
 
+/**
+ *  Store the address information about a peer.
+ *  Prior to 0.8.1, an instantiation required a peer ID, and full Destination address.
+ *  Starting with 0.8.1, to support compact tracker responses,
+ *  a PeerID can be instantiated with a Destination Hash alone.
+ *  The full destination lookup is deferred until getAddress() is called,
+ *  and the PeerID is not required.
+ *  Equality is now determined solely by the dest hash.
+ */
 public class PeerID implements Comparable
 {
-  private final byte[] id;
-  private final Destination address;
+  private byte[] id;
+  private Destination address;
   private final int port;
-
+  private byte[] destHash;
+  /** whether we have tried to get the dest from the hash - only do once */
+  private boolean triedDestLookup;
   private final int hash;
 
   public PeerID(byte[] id, Destination address)
@@ -44,7 +58,7 @@ public class PeerID implements Comparable
     this.id = id;
     this.address = address;
     this.port = 6881;
-
+    this.destHash = address.calculateHash().getData();
     hash = calculateHash();
   }
 
@@ -77,17 +91,49 @@ public class PeerID implements Comparable
         throw new InvalidBEncodingException("Invalid destination [" + bevalue.getString() + "]");
 
     port = 6881;
-
+    this.destHash = address.calculateHash().getData();
     hash = calculateHash();
   }
 
+  /**
+   * Creates a PeerID from a destHash
+   * @since 0.8.1
+   */
+  public PeerID(byte[] dest_hash) throws InvalidBEncodingException
+  {
+    // id and address remain null
+    port = 6881;
+    if (dest_hash.length != 32)
+        throw new InvalidBEncodingException("bad hash length");
+    destHash = dest_hash;
+    hash = DataHelper.hashCode(dest_hash);
+  }
+
   public byte[] getID()
   {
     return id;
   }
 
-  public Destination getAddress()
+  /** for connecting out to peer based on desthash @since 0.8.1 */
+  public void setID(byte[] xid)
   {
+    id = xid;
+  }
+
+  /**
+   *  Get the destination.
+   *  If this PeerId was instantiated with a destHash,
+   *  and we have not yet done so, lookup the full destination, which may take
+   *  up to 10 seconds.
+   *  @return Dest or null if unknown
+   */
+  public synchronized Destination getAddress()
+  {
+    if (address == null && destHash != null && !triedDestLookup) {
+        String b32 = Base32.encode(destHash) + ".b32.i2p";
+        address = I2PAppContext.getGlobalContext().namingService().lookup(b32);
+        triedDestLookup = true;
+    }
     return address;
   }
 
@@ -96,16 +142,19 @@ public class PeerID implements Comparable
     return port;
   }
 
+  /** @since 0.8.1 */
+  public byte[] getDestHash()
+  {
+    return destHash;
+  }
+
   private int calculateHash()
   {
-    int b = 0;
-    for (int i = 0; i < id.length; i++)
-      b ^= id[i];
-    return (b ^ address.hashCode()) ^ port;
+    return DataHelper.hashCode(destHash);
   }
 
   /**
-   * The hash code of a PeerID is the exclusive or of all id bytes.
+   * The hash code of a PeerID is the hashcode of the desthash
    */
     @Override
   public int hashCode()
@@ -115,18 +164,15 @@ public class PeerID implements Comparable
 
   /**
    * Returns true if and only if this peerID and the given peerID have
-   * the same 20 bytes as ID.
+   * the same destination hash
    */
   public boolean sameID(PeerID pid)
   {
-    boolean equal = true;
-    for (int i = 0; equal && i < id.length; i++)
-      equal = id[i] == pid.id[i];
-    return equal;
+    return DataHelper.eq(destHash, pid.getDestHash());
   }
 
   /**
-   * Two PeerIDs are equal when they have the same id, address and port.
+   * Two PeerIDs are equal when they have the same dest hash
    */
     @Override
   public boolean equals(Object o)
@@ -135,9 +181,7 @@ public class PeerID implements Comparable
       {
         PeerID pid = (PeerID)o;
 
-        return port == pid.port
-          && address.equals(pid.address)
-          && sameID(pid);
+        return sameID(pid);
       }
     else
       return false;
@@ -145,6 +189,7 @@ public class PeerID implements Comparable
 
   /**
    * Compares port, address and id.
+   * @deprecated unused? and will NPE now that address can be null?
    */
   public int compareTo(Object o)
   {
@@ -176,6 +221,8 @@ public class PeerID implements Comparable
     @Override
   public String toString()
   {
+    if (id == null || address == null)
+        return "unkn@" + Base64.encode(destHash).substring(0, 6);
     int nonZero = 0;
     for (int i = 0; i < id.length; i++) {
         if (id[i] != 0) {
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java
index c2afec346fd73c3540cef4d2087d0ecafaef6577..9d6cd946000459a9f47c403064f1764c0646308d 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java
@@ -437,7 +437,7 @@ public class Snark
             try { storage.close(); } catch (IOException ioee) {
                 ioee.printStackTrace();
             }
-            fatal("Could not create storage", ioe);
+            fatal("Could not check or create storage", ioe);
           }
       }
 
diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
index 59ab86a12920ec5d2e70ee86fa2fb9df887914e5..b0fe6d1ba7760bfa2148e68c905595b751065d64 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
@@ -1,857 +1,880 @@
-package org.klomp.snark;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FilenameFilter;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-import java.util.Set;
-import java.util.StringTokenizer;
-import java.util.TreeMap;
-
-import net.i2p.I2PAppContext;
-import net.i2p.data.Base64;
-import net.i2p.data.DataHelper;
-import net.i2p.util.I2PAppThread;
-import net.i2p.util.Log;
-import net.i2p.util.OrderedProperties;
-
-/**
- * Manage multiple snarks
- */
-public class SnarkManager implements Snark.CompleteListener {
-    private static SnarkManager _instance = new SnarkManager();
-    public static SnarkManager instance() { return _instance; }
-    
-    /** map of (canonical) filename of the .torrent file to Snark instance (unsynchronized) */
-    private final Map<String, Snark> _snarks;
-    private final Object _addSnarkLock;
-    private /* FIXME final FIXME */ File _configFile;
-    private Properties _config;
-    private I2PAppContext _context;
-    private Log _log;
-    private final List _messages;
-    private I2PSnarkUtil _util;
-    private PeerCoordinatorSet _peerCoordinatorSet;
-    private ConnectionAcceptor _connectionAcceptor;
-    private Thread _monitor;
-    private boolean _running;
-    
-    public static final String PROP_I2CP_HOST = "i2psnark.i2cpHost";
-    public static final String PROP_I2CP_PORT = "i2psnark.i2cpPort";
-    public static final String PROP_I2CP_OPTS = "i2psnark.i2cpOptions";
-    //public static final String PROP_EEP_HOST = "i2psnark.eepHost";
-    //public static final String PROP_EEP_PORT = "i2psnark.eepPort";
-    public static final String PROP_UPLOADERS_TOTAL = "i2psnark.uploaders.total";
-    public static final String PROP_UPBW_MAX = "i2psnark.upbw.max";
-    public static final String PROP_DIR = "i2psnark.dir";
-    public static final String PROP_META_PREFIX = "i2psnark.zmeta.";
-    public static final String PROP_META_BITFIELD_SUFFIX = ".bitfield";
-
-    private static final String CONFIG_FILE = "i2psnark.config";
-    public static final String PROP_AUTO_START = "i2snark.autoStart";   // oops
-    public static final String DEFAULT_AUTO_START = "false";
-    public static final String PROP_LINK_PREFIX = "i2psnark.linkPrefix";
-    public static final String DEFAULT_LINK_PREFIX = "file:///";
-    public static final String PROP_STARTUP_DELAY = "i2psnark.startupDelay";
-   
-    public static final int MIN_UP_BW = 2;
-    public static final int DEFAULT_MAX_UP_BW = 10;
-    public static final int DEFAULT_STARTUP_DELAY = 3; 
-    private SnarkManager() {
-        _snarks = new HashMap();
-        _addSnarkLock = new Object();
-        _context = I2PAppContext.getGlobalContext();
-        _log = _context.logManager().getLog(SnarkManager.class);
-        _messages = new ArrayList(16);
-        _util = new I2PSnarkUtil(_context);
-        _configFile = new File(CONFIG_FILE);
-        if (!_configFile.isAbsolute())
-            _configFile = new File(_context.getConfigDir(), CONFIG_FILE);
-        loadConfig(null);
-    }
-
-    /** Caller _must_ call loadConfig(file) before this if setting new values
-     *  for i2cp host/port or i2psnark.dir
-     */
-    public void start() {
-        _running = true;
-        _peerCoordinatorSet = new PeerCoordinatorSet();
-        _connectionAcceptor = new ConnectionAcceptor(_util);
-        int minutes = getStartupDelayMinutes();
-        _messages.add(_("Adding torrents in {0} minutes", minutes));
-        _monitor = new I2PAppThread(new DirMonitor(), "Snark DirMonitor", true);
-        _monitor.start();
-        _context.addShutdownTask(new SnarkManagerShutdown());
-    }
-
-    public void stop() {
-        _running = false;
-        _monitor.interrupt();
-        _connectionAcceptor.halt();
-        (new SnarkManagerShutdown()).run();
-    }
-    
-    /** hook to I2PSnarkUtil for the servlet */
-    public I2PSnarkUtil util() { return _util; }
-
-    private static final int MAX_MESSAGES = 5;
-    public void addMessage(String message) {
-        synchronized (_messages) {
-            _messages.add(message);
-            while (_messages.size() > MAX_MESSAGES)
-                _messages.remove(0);
-        }
-        if (_log.shouldLog(Log.INFO))
-            _log.info("MSG: " + message);
-    }
-    
-    /** newest last */
-    public List getMessages() {
-        synchronized (_messages) {
-            return new ArrayList(_messages);
-        }
-    }
-    
-    public boolean shouldAutoStart() {
-        return Boolean.valueOf(_config.getProperty(PROP_AUTO_START, DEFAULT_AUTO_START+"")).booleanValue();
-    }
-    public String linkPrefix() {
-        return _config.getProperty(PROP_LINK_PREFIX, DEFAULT_LINK_PREFIX + getDataDir().getAbsolutePath() + File.separatorChar);
-    }
-    private int getStartupDelayMinutes() { 
-	return Integer.valueOf(_config.getProperty(PROP_STARTUP_DELAY)).intValue(); 
-    }
-    public File getDataDir() { 
-        String dir = _config.getProperty(PROP_DIR, "i2psnark");
-        File f = new File(dir);
-        if (!f.isAbsolute())
-            f = new File(_context.getAppDir(), dir);
-        return f; 
-    }
-    
-    /** null to set initial defaults */
-    public void loadConfig(String filename) {
-        if (_config == null)
-            _config = new OrderedProperties();
-        if (filename != null) {
-            File cfg = new File(filename);
-            if (!cfg.isAbsolute())
-                cfg = new File(_context.getConfigDir(), filename);
-            _configFile = cfg;
-            if (cfg.exists()) {
-                try {
-                    DataHelper.loadProps(_config, cfg);
-                } catch (IOException ioe) {
-                   _log.error("Error loading I2PSnark config '" + filename + "'", ioe);
-                }
-            } 
-        } 
-        // now add sane defaults
-        if (!_config.containsKey(PROP_I2CP_HOST))
-            _config.setProperty(PROP_I2CP_HOST, "127.0.0.1");
-        if (!_config.containsKey(PROP_I2CP_PORT))
-            _config.setProperty(PROP_I2CP_PORT, "7654");
-        if (!_config.containsKey(PROP_I2CP_OPTS))
-            _config.setProperty(PROP_I2CP_OPTS, "inbound.length=2 inbound.lengthVariance=0 outbound.length=2 outbound.lengthVariance=0 inbound.quantity=3 outbound.quantity=3");
-        //if (!_config.containsKey(PROP_EEP_HOST))
-        //    _config.setProperty(PROP_EEP_HOST, "127.0.0.1");
-        //if (!_config.containsKey(PROP_EEP_PORT))
-        //    _config.setProperty(PROP_EEP_PORT, "4444");
-        if (!_config.containsKey(PROP_UPLOADERS_TOTAL))
-            _config.setProperty(PROP_UPLOADERS_TOTAL, "" + Snark.MAX_TOTAL_UPLOADERS);
-        if (!_config.containsKey(PROP_DIR))
-            _config.setProperty(PROP_DIR, "i2psnark");
-        if (!_config.containsKey(PROP_AUTO_START))
-            _config.setProperty(PROP_AUTO_START, DEFAULT_AUTO_START);
-	if (!_config.containsKey(PROP_STARTUP_DELAY))
-            _config.setProperty(PROP_STARTUP_DELAY, "" + DEFAULT_STARTUP_DELAY);
-
-        updateConfig();
-    }
-
-    /** call from DirMonitor since loadConfig() is called before router I2CP is up */
-    private void getBWLimit() {
-        if (!_config.containsKey(PROP_UPBW_MAX)) {
-            int[] limits = BWLimits.getBWLimits(_util.getI2CPHost(), _util.getI2CPPort());
-            if (limits != null && limits[1] > 0)
-                _util.setMaxUpBW(limits[1]);
-        }
-    }
-    
-    private void updateConfig() {
-        String i2cpHost = _config.getProperty(PROP_I2CP_HOST);
-        int i2cpPort = getInt(PROP_I2CP_PORT, 7654);
-        String opts = _config.getProperty(PROP_I2CP_OPTS);
-        Map i2cpOpts = new HashMap();
-        if (opts != null) {
-            StringTokenizer tok = new StringTokenizer(opts, " ");
-            while (tok.hasMoreTokens()) {
-                String pair = tok.nextToken();
-                int split = pair.indexOf('=');
-                if (split > 0)
-                    i2cpOpts.put(pair.substring(0, split), pair.substring(split+1));
-            }
-        }
-        if (i2cpHost != null) {
-            _util.setI2CPConfig(i2cpHost, i2cpPort, i2cpOpts);
-            _log.debug("Configuring with I2CP options " + i2cpOpts);
-        }
-        //I2PSnarkUtil.instance().setI2CPConfig("66.111.51.110", 7654, new Properties());
-        //String eepHost = _config.getProperty(PROP_EEP_HOST);
-        //int eepPort = getInt(PROP_EEP_PORT, 4444);
-        //if (eepHost != null)
-        //    _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.setStartupDelay(getInt(PROP_STARTUP_DELAY, DEFAULT_STARTUP_DELAY));
-        String ot = _config.getProperty(I2PSnarkUtil.PROP_OPENTRACKERS);
-        if (ot != null)
-            _util.setOpenTrackerString(ot);
-        // FIXME set util use open trackers property somehow
-        getDataDir().mkdirs();
-    }
-    
-    private int getInt(String prop, int defaultVal) {
-        String p = _config.getProperty(prop);
-        try {
-            if ( (p != null) && (p.trim().length() > 0) )
-                return  Integer.parseInt(p.trim());
-        } catch (NumberFormatException nfe) {
-            // ignore
-        }
-        return defaultVal;
-    }
-    
-    public void updateConfig(String dataDir, boolean autoStart, String startDelay, String seedPct, String eepHost, 
-                             String eepPort, String i2cpHost, String i2cpPort, String i2cpOpts,
-                             String upLimit, String upBW, boolean useOpenTrackers, String openTrackers) {
-        boolean changed = false;
-        //if (eepHost != null) {
-        //    // unused, we use socket eepget
-        //    int port = _util.getEepProxyPort();
-        //    try { port = Integer.parseInt(eepPort); } catch (NumberFormatException nfe) {}
-        //    String host = _util.getEepProxyHost();
-        //    if ( (eepHost.trim().length() > 0) && (port > 0) &&
-        //         ((!host.equals(eepHost) || (port != _util.getEepProxyPort()) )) ) {
-        //        _util.setProxy(eepHost, port);
-        //        changed = true;
-        //        _config.setProperty(PROP_EEP_HOST, eepHost);
-        //        _config.setProperty(PROP_EEP_PORT, eepPort+"");
-        //        addMessage("EepProxy location changed to " + eepHost + ":" + port);
-        //    }
-        //}
-        if (upLimit != null) {
-            int limit = _util.getMaxUploaders();
-            try { limit = Integer.parseInt(upLimit); } catch (NumberFormatException nfe) {}
-            if ( limit != _util.getMaxUploaders()) {
-                if ( limit >= Snark.MIN_TOTAL_UPLOADERS ) {
-                    _util.setMaxUploaders(limit);
-                    changed = true;
-                    _config.setProperty(PROP_UPLOADERS_TOTAL, "" + limit);
-                    addMessage(_("Total uploaders limit changed to {0}", limit));
-                } else {
-                    addMessage(_("Minimum total uploaders limit is {0}", Snark.MIN_TOTAL_UPLOADERS));
-                }
-            }
-        }
-        if (upBW != null) {
-            int limit = _util.getMaxUpBW();
-            try { limit = Integer.parseInt(upBW); } catch (NumberFormatException nfe) {}
-            if ( limit != _util.getMaxUpBW()) {
-                if ( limit >= MIN_UP_BW ) {
-                    _util.setMaxUpBW(limit);
-                    changed = true;
-                    _config.setProperty(PROP_UPBW_MAX, "" + limit);
-                    addMessage(_("Up BW limit changed to {0}KBps", limit));
-                } else {
-                    addMessage(_("Minimum up bandwidth limit is {0}KBps", MIN_UP_BW));
-                }
-            }
-        }
-        
-	if (startDelay != null){
-		int minutes = _util.getStartupDelay();
-                try { minutes = Integer.parseInt(startDelay); } catch (NumberFormatException nfe) {}
-	        if ( minutes != _util.getStartupDelay()) {
-                	    _util.setStartupDelay(minutes);
-	                    changed = true;
-        	            _config.setProperty(PROP_STARTUP_DELAY, "" + minutes);
-                	    addMessage(_("Startup delay limit changed to {0} minutes", minutes));
-                	}
-
-	}
-	if (i2cpHost != null) {
-            int oldI2CPPort = _util.getI2CPPort();
-            String oldI2CPHost = _util.getI2CPHost();
-            int port = oldI2CPPort;
-            try { port = Integer.parseInt(i2cpPort); } catch (NumberFormatException nfe) {}
-            String host = oldI2CPHost;
-            Map opts = new HashMap();
-            if (i2cpOpts == null) i2cpOpts = "";
-            StringTokenizer tok = new StringTokenizer(i2cpOpts, " \t\n");
-            while (tok.hasMoreTokens()) {
-                String pair = tok.nextToken();
-                int split = pair.indexOf('=');
-                if (split > 0)
-                    opts.put(pair.substring(0, split), pair.substring(split+1));
-            }
-            Map oldOpts = new HashMap();
-            String oldI2CPOpts = _config.getProperty(PROP_I2CP_OPTS);
-            if (oldI2CPOpts == null) oldI2CPOpts = "";
-            tok = new StringTokenizer(oldI2CPOpts, " \t\n");
-            while (tok.hasMoreTokens()) {
-                String pair = tok.nextToken();
-                int split = pair.indexOf('=');
-                if (split > 0)
-                    oldOpts.put(pair.substring(0, split), pair.substring(split+1));
-            }
-            
-            if ( (i2cpHost.trim().length() > 0) && (port > 0) &&
-                 ((!host.equals(i2cpHost) || 
-                  (port != _util.getI2CPPort()) ||
-                  (!oldOpts.equals(opts)))) ) {
-                boolean snarksActive = false;
-                Set names = listTorrentFiles();
-                for (Iterator iter = names.iterator(); iter.hasNext(); ) {
-                    Snark snark = getTorrent((String)iter.next());
-                    if ( (snark != null) && (!snark.stopped) ) {
-                        snarksActive = true;
-                        break;
-                    }
-                }
-                if (snarksActive) {
-                    Properties p = new Properties();
-                    p.putAll(opts);
-                    _util.setI2CPConfig(i2cpHost, port, p);
-                    addMessage(_("I2CP and tunnel changes will take effect after stopping all torrents"));
-                    _log.debug("i2cp host [" + i2cpHost + "] i2cp port " + port + " opts [" + opts 
-                               + "] oldOpts [" + oldOpts + "]");
-                } else {
-                    if (_util.connected()) {
-                        _util.disconnect();
-                        addMessage(_("Disconnecting old I2CP destination"));
-                    }
-                    Properties p = new Properties();
-                    p.putAll(opts);
-                    addMessage(_("I2CP settings changed to {0}", i2cpHost + ":" + port + " (" + i2cpOpts.trim() + ")"));
-                    _util.setI2CPConfig(i2cpHost, port, p);
-                    boolean ok = _util.connect();
-                    if (!ok) {
-                        addMessage(_("Unable to connect with the new settings, reverting to the old I2CP settings"));
-                        _util.setI2CPConfig(oldI2CPHost, oldI2CPPort, oldOpts);
-                        ok = _util.connect();
-                        if (!ok)
-                            addMessage(_("Unable to reconnect with the old settings!"));
-                    } else {
-                        addMessage(_("Reconnected on the new I2CP destination"));
-                        _config.setProperty(PROP_I2CP_HOST, i2cpHost.trim());
-                        _config.setProperty(PROP_I2CP_PORT, "" + port);
-                        _config.setProperty(PROP_I2CP_OPTS, i2cpOpts.trim());
-                        changed = true;
-                        // no PeerAcceptors/I2PServerSockets to deal with, since all snarks are inactive
-                        for (Iterator iter = names.iterator(); iter.hasNext(); ) {
-                            String name = (String)iter.next();
-                            Snark snark = getTorrent(name);
-                            if ( (snark != null) && (snark.acceptor != null) ) {
-                                snark.acceptor.restart();
-                                addMessage(_("I2CP listener restarted for \"{0}\"", snark.meta.getName()));
-                            }
-                        }
-                    }
-                }
-                changed = true;
-            }
-        }
-        if (shouldAutoStart() != autoStart) {
-            _config.setProperty(PROP_AUTO_START, autoStart + "");
-            if (autoStart)
-                addMessage(_("Enabled autostart"));
-            else
-                addMessage(_("Disabled autostart"));
-            changed = true;
-        }
-        if (_util.shouldUseOpenTrackers() != useOpenTrackers) {
-            _config.setProperty(I2PSnarkUtil.PROP_USE_OPENTRACKERS, useOpenTrackers + "");
-            if (useOpenTrackers)
-                addMessage(_("Enabled open trackers - torrent restart required to take effect."));
-            else
-                addMessage(_("Disabled open trackers - torrent restart required to take effect."));
-            changed = true;
-        }
-        if (openTrackers != null) {
-            if (openTrackers.trim().length() > 0 && !openTrackers.trim().equals(_util.getOpenTrackerString())) {
-                _config.setProperty(I2PSnarkUtil.PROP_OPENTRACKERS, openTrackers.trim());
-                _util.setOpenTrackerString(openTrackers);
-                addMessage(_("Open Tracker list changed - torrent restart required to take effect."));
-                changed = true;
-            }
-        }
-        if (changed) {
-            saveConfig();
-        } else {
-            addMessage(_("Configuration unchanged."));
-        }
-    }
-    
-    public void saveConfig() {
-        try {
-            synchronized (_configFile) {
-                DataHelper.storeProps(_config, _configFile);
-            }
-        } catch (IOException ioe) {
-            addMessage(_("Unable to save the config to {0}", _configFile.getAbsolutePath()));
-        }
-    }
-    
-    public Properties getConfig() { return _config; }
-    
-    /** hardcoded for sanity.  perhaps this should be customizable, for people who increase their ulimit, etc. */
-    private static final int MAX_FILES_PER_TORRENT = 512;
-    
-    /** set of canonical .torrent filenames that we are dealing with */
-    public Set<String> listTorrentFiles() { synchronized (_snarks) { return new HashSet(_snarks.keySet()); } }
-
-    /**
-     * Grab the torrent given the (canonical) filename of the .torrent file
-     * @return Snark or null
-     */
-    public Snark getTorrent(String filename) { synchronized (_snarks) { return (Snark)_snarks.get(filename); } }
-
-    /**
-     * Grab the torrent given the base name of the storage
-     * @return Snark or null
-     * @since 0.7.14
-     */
-    public Snark getTorrentByBaseName(String filename) {
-        synchronized (_snarks) {
-            for (Snark s : _snarks.values()) {
-                if (s.storage.getBaseName().equals(filename))
-                    return s;
-            }
-        }
-        return null;
-    }
-
-    public void addTorrent(String filename) { addTorrent(filename, false); }
-    public void addTorrent(String filename, boolean dontAutoStart) {
-        if ((!dontAutoStart) && !_util.connected()) {
-            addMessage(_("Connecting to I2P"));
-            boolean ok = _util.connect();
-            if (!ok) {
-                addMessage(_("Error connecting to I2P - check your I2CP settings!"));
-                return;
-            }
-        }
-        File sfile = new File(filename);
-        try {
-            filename = sfile.getCanonicalPath();
-        } catch (IOException ioe) {
-            _log.error("Unable to add the torrent " + filename, ioe);
-            addMessage(_("Error: Could not add the torrent {0}", filename) + ": " + ioe.getMessage());
-            return;
-        }
-        File dataDir = getDataDir();
-        Snark torrent = null;
-        synchronized (_snarks) {
-            torrent = (Snark)_snarks.get(filename);
-        }
-        // don't hold the _snarks lock while verifying the torrent
-        if (torrent == null) {
-            synchronized (_addSnarkLock) {
-                // double-check
-                synchronized (_snarks) {
-                    if(_snarks.get(filename) != null)
-                        return;
-                }
-
-                FileInputStream fis = null;
-                try {
-                    fis = new FileInputStream(sfile);
-                } catch (IOException ioe) {
-                    // catch this here so we don't try do delete it below
-                    addMessage(_("Cannot open \"{0}\"", sfile.getName()) + ": " + ioe.getMessage());
-                    return;
-                }
-
-                try {
-                    MetaInfo info = new MetaInfo(fis);
-                    try {
-                        fis.close();
-                        fis = null;
-                    } catch (IOException e) {}
-                    
-                    if (!TrackerClient.isValidAnnounce(info.getAnnounce())) {
-                        if (_util.shouldUseOpenTrackers() && _util.getOpenTrackers() != null) {
-                            addMessage(_("Warning - Ignoring non-i2p tracker in \"{0}\", will announce to i2p open trackers only", info.getName()));
-                        } else {
-                            addMessage(_("Warning - Ignoring non-i2p tracker in \"{0}\", and open trackers are disabled, you must enable open trackers before starting the torrent!", info.getName()));
-                            dontAutoStart = true;
-                        }
-                    }
-                    String rejectMessage = locked_validateTorrent(info);
-                    if (rejectMessage != null) {
-                        sfile.delete();
-                        addMessage(rejectMessage);
-                        return;
-                    } else {
-                        torrent = new Snark(_util, filename, null, -1, null, null, this,
-                                            _peerCoordinatorSet, _connectionAcceptor,
-                                            false, dataDir.getPath());
-                        torrent.completeListener = this;
-                        synchronized (_snarks) {
-                            _snarks.put(filename, torrent);
-                        }
-                    }
-                } catch (IOException ioe) {
-                    addMessage(_("Torrent in \"{0}\" is invalid", sfile.getName()) + ": " + ioe.getMessage());
-                    if (sfile.exists())
-                        sfile.delete();
-                    return;
-                } finally {
-                    if (fis != null) try { fis.close(); } catch (IOException ioe) {}
-                }
-            }
-        } else {
-            return;
-        }
-        // ok, snark created, now lets start it up or configure it further
-        File f = new File(filename);
-        if (!dontAutoStart && shouldAutoStart()) {
-            torrent.startTorrent();
-            addMessage(_("Torrent added and started: \"{0}\"", f.getName()));
-        } else {
-            addMessage(_("Torrent added: \"{0}\"", f.getName()));
-        }
-    }
-    
-    /**
-     * Get the timestamp for a torrent from the config file
-     */
-    public long getSavedTorrentTime(Snark snark) {
-        MetaInfo metainfo = snark.meta;
-        byte[] ih = metainfo.getInfoHash();
-        String infohash = Base64.encode(ih);
-        infohash = infohash.replace('=', '$');
-        String time = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX);
-        if (time == null)
-            return 0;
-        int comma = time.indexOf(',');
-        if (comma <= 0)
-            return 0;
-        time = time.substring(0, comma);
-        try { return Long.parseLong(time); } catch (NumberFormatException nfe) {}
-        return 0;
-    }
-    
-    /**
-     * Get the saved bitfield for a torrent from the config file.
-     * Convert "." to a full bitfield.
-     */
-    public BitField getSavedTorrentBitField(Snark snark) {
-        MetaInfo metainfo = snark.meta;
-        byte[] ih = metainfo.getInfoHash();
-        String infohash = Base64.encode(ih);
-        infohash = infohash.replace('=', '$');
-        String bf = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX);
-        if (bf == null)
-            return null;
-        int comma = bf.indexOf(',');
-        if (comma <= 0)
-            return null;
-        bf = bf.substring(comma + 1).trim();
-        int len = metainfo.getPieces();
-        if (bf.equals(".")) {
-            BitField bitfield = new BitField(len);
-            for (int i = 0; i < len; i++)
-                 bitfield.set(i);
-            return bitfield;
-        }
-        byte[] bitfield = Base64.decode(bf);
-        if (bitfield == null)
-            return null;
-        if (bitfield.length * 8 < len)
-            return null;
-        return new BitField(bitfield, len);
-    }
-    
-    /**
-     * Save the completion status of a torrent and the current time in the config file
-     * in the form "i2psnark.zmeta.$base64infohash=$time,$base64bitfield".
-     * The config file property key is appended with the Base64 of the infohash,
-     * with the '=' changed to '$' since a key can't contain '='.
-     * The time is a standard long converted to string.
-     * The status is either a bitfield converted to Base64 or "." for a completed
-     * torrent to save space in the config file and in memory.
-     */
-    public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield) {
-        byte[] ih = metainfo.getInfoHash();
-        String infohash = Base64.encode(ih);
-        infohash = infohash.replace('=', '$');
-        String now = "" + System.currentTimeMillis();
-        String bfs;
-        if (bitfield.complete()) {
-          bfs = ".";
-        } else {
-          byte[] bf = bitfield.getFieldBytes();
-          bfs = Base64.encode(bf);
-        }
-        _config.setProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX, now + "," + bfs);
-        saveConfig();
-    }
-    
-    /**
-     * Remove the status of a torrent from the config file.
-     * This may help the config file from growing too big.
-     */
-    public void removeTorrentStatus(MetaInfo metainfo) {
-        byte[] ih = metainfo.getInfoHash();
-        String infohash = Base64.encode(ih);
-        infohash = infohash.replace('=', '$');
-        _config.remove(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX);
-        saveConfig();
-    }
-    
-    /**
-     *  Warning - does not validate announce URL - use TrackerClient.isValidAnnounce()
-     */
-    private String locked_validateTorrent(MetaInfo info) throws IOException {
-        List files = info.getFiles();
-        if ( (files != null) && (files.size() > MAX_FILES_PER_TORRENT) ) {
-            return _("Too many files in \"{0}\" ({1}), deleting it!", info.getName(), files.size());
-        } else if ( (files == null) && (info.getName().endsWith(".torrent")) ) {
-            return _("Torrent file \"{0}\" cannot end in \".torrent\", deleting it!", info.getName());
-        } else if (info.getPieces() <= 0) {
-            return _("No pieces in \"{0}\",  deleting it!", info.getName());
-        } else if (info.getPieces() > Storage.MAX_PIECES) {
-            return _("Too many pieces in \"{0}\", limit is {1}, deleting it!", info.getName(), Storage.MAX_PIECES);
-        } else if (info.getPieceLength(0) > Storage.MAX_PIECE_SIZE) {
-            return _("Pieces are too large in \"{0}\" ({1}B), deleting it.", info.getName(), DataHelper.formatSize2(info.getPieceLength(0))) + ' ' +
-                   _("Limit is {0}B", DataHelper.formatSize2(Storage.MAX_PIECE_SIZE));
-        } else if (info.getTotalLength() > Storage.MAX_TOTAL_SIZE) {
-            System.out.println("torrent info: " + info.toString());
-            List lengths = info.getLengths();
-            if (lengths != null)
-                for (int i = 0; i < lengths.size(); i++)
-                    System.out.println("File " + i + " is " + lengths.get(i) + " long.");
-            
-            return _("Torrents larger than {0}B are not supported yet, deleting \"{1}\"", Storage.MAX_TOTAL_SIZE, info.getName());
-        } else {
-            // ok
-            return null;
-        }
-    }
-    
-    /**
-     * Stop the torrent, leaving it on the list of torrents unless told to remove it
-     */
-    public Snark stopTorrent(String filename, boolean shouldRemove) {
-        File sfile = new File(filename);
-        try {
-            filename = sfile.getCanonicalPath();
-        } catch (IOException ioe) {
-            _log.error("Unable to remove the torrent " + filename, ioe);
-            addMessage(_("Error: Could not remove the torrent {0}", filename) + ": " + ioe.getMessage());
-            return null;
-        }
-        int remaining = 0;
-        Snark torrent = null;
-        synchronized (_snarks) {
-            if (shouldRemove)
-                torrent = (Snark)_snarks.remove(filename);
-            else
-                torrent = (Snark)_snarks.get(filename);
-            remaining = _snarks.size();
-        }
-        if (torrent != null) {
-            boolean wasStopped = torrent.stopped;
-            torrent.stopTorrent();
-            if (remaining == 0) {
-                // should we disconnect/reconnect here (taking care to deal with the other thread's
-                // I2PServerSocket.accept() call properly?)
-                ////_util.
-            }
-            if (!wasStopped)
-                addMessage(_("Torrent stopped: \"{0}\"", sfile.getName()));
-        }
-        return torrent;
-    }
-    /**
-     * Stop the torrent and delete the torrent file itself, but leaving the data
-     * behind.
-     */
-    public void removeTorrent(String filename) {
-        Snark torrent = stopTorrent(filename, true);
-        if (torrent != null) {
-            File torrentFile = new File(filename);
-            torrentFile.delete();
-            if (torrent.storage != null)
-                removeTorrentStatus(torrent.storage.getMetaInfo());
-            addMessage(_("Torrent removed: \"{0}\"", torrentFile.getName()));
-        }
-    }
-    
-    private class DirMonitor implements Runnable {
-        public void run() {
-            try { Thread.sleep(60*1000*getStartupDelayMinutes()); } catch (InterruptedException ie) {}
-            // the first message was a "We are starting up in 1m" 
-            synchronized (_messages) { 
-                if (_messages.size() == 1)
-                    _messages.remove(0);
-            }
-
-            // here because we need to delay until I2CP is up
-            // although the user will see the default until then
-            getBWLimit();
-            while (true) {
-                File dir = getDataDir();
-                _log.debug("Directory Monitor loop over " + dir.getAbsolutePath());
-                try {
-                    monitorTorrents(dir);
-                } catch (Exception e) {
-                    _log.error("Error in the DirectoryMonitor", e);
-                }
-                try { Thread.sleep(60*1000); } catch (InterruptedException ie) {}
-            }
-        }
-    }
-    
-    /** two listeners */
-    public void torrentComplete(Snark snark) {
-        File f = new File(snark.torrent);
-        long len = snark.meta.getTotalLength();
-        addMessage(_("Download finished: \"{0}\"", f.getName()) + " (" + _("size: {0}B", DataHelper.formatSize2(len)) + ')');
-        updateStatus(snark);
-    }
-    
-    public void updateStatus(Snark snark) {
-        saveTorrentStatus(snark.meta, snark.storage.getBitField());
-    }
-    
-    private void monitorTorrents(File dir) {
-        String fileNames[] = dir.list(TorrentFilenameFilter.instance());
-        List foundNames = new ArrayList(0);
-        if (fileNames != null) {
-            for (int i = 0; i < fileNames.length; i++) {
-                try {
-                    foundNames.add(new File(dir, fileNames[i]).getCanonicalPath());
-                } catch (IOException ioe) {
-                    _log.error("Error resolving '" + fileNames[i] + "' in '" + dir, ioe);
-                }
-            }
-        }
-        
-        Set existingNames = listTorrentFiles();
-        // lets find new ones first...
-        for (int i = 0; i < foundNames.size(); i++) {
-            if (existingNames.contains(foundNames.get(i))) {
-                // already known.  noop
-            } else {
-                if (shouldAutoStart() && !_util.connect())
-                    addMessage(_("Unable to connect to I2P!"));
-                addTorrent((String)foundNames.get(i), !shouldAutoStart());
-            }
-        }
-        // now lets see which ones have been removed...
-        for (Iterator iter = existingNames.iterator(); iter.hasNext(); ) {
-            String name = (String)iter.next();
-            if (foundNames.contains(name)) {
-                // known and still there.  noop
-            } else {
-                // known, but removed.  drop it
-                stopTorrent(name, true);
-            }
-        }
-    }
-
-    /** translate */
-    private String _(String s) {
-        return _util.getString(s);
-    }
-
-    /** translate */
-    private String _(String s, Object o) {
-        return _util.getString(s, o);
-    }
-
-    /** translate */
-    private String _(String s, Object o, Object o2) {
-        return _util.getString(s, o, o2);
-    }
-
-    /**
-     *  "name", "announceURL=websiteURL" pairs
-     */
-    private static final String DEFAULT_TRACKERS[] = { 
-//       "Postman", "http://YRgrgTLGnbTq2aZOZDJQ~o6Uk5k6TK-OZtx0St9pb0G-5EGYURZioxqYG8AQt~LgyyI~NCj6aYWpPO-150RcEvsfgXLR~CxkkZcVpgt6pns8SRc3Bi-QSAkXpJtloapRGcQfzTtwllokbdC-aMGpeDOjYLd8b5V9Im8wdCHYy7LRFxhEtGb~RL55DA8aYOgEXcTpr6RPPywbV~Qf3q5UK55el6Kex-6VCxreUnPEe4hmTAbqZNR7Fm0hpCiHKGoToRcygafpFqDw5frLXToYiqs9d4liyVB-BcOb0ihORbo0nS3CLmAwZGvdAP8BZ7cIYE3Z9IU9D1G8JCMxWarfKX1pix~6pIA-sp1gKlL1HhYhPMxwyxvuSqx34o3BqU7vdTYwWiLpGM~zU1~j9rHL7x60pVuYaXcFQDR4-QVy26b6Pt6BlAZoFmHhPcAuWfu-SFhjyZYsqzmEmHeYdAwa~HojSbofg0TMUgESRXMw6YThK1KXWeeJVeztGTz25sL8AAAA.i2p/announce.php=http://tracker.postman.i2p/"
-//       , "eBook", "http://E71FRom6PZNEqTN2Lr8P-sr23b7HJVC32KoGnVQjaX6zJiXwhJy2HsXob36Qmj81TYFZdewFZa9mSJ533UZgGyQkXo2ahctg82JKYZfDe5uDxAn1E9YPjxZCWJaFJh0S~UwSs~9AZ7UcauSJIoNtpxrtbmRNVFLqnkEDdLZi26TeucfOmiFmIWnVblLniWv3tG1boE9Abd-6j3FmYVrRucYuepAILYt6katmVNOk6sXmno1Eynrp~~MBuFq0Ko6~jsc2E2CRVYXDhGHEMdt-j6JUz5D7S2RIVzDRqQyAZLKJ7OdQDmI31przzmne1vOqqqLC~1xUumZVIvF~yOeJUGNjJ1Vx0J8i2BQIusn1pQJ6UCB~ZtZZLQtEb8EPVCfpeRi2ri1M5CyOuxN0V5ekmPHrYIBNevuTCRC26NP7ZS5VDgx1~NaC3A-CzJAE6f1QXi0wMI9aywNG5KGzOPifcsih8eyGyytvgLtrZtV7ykzYpPCS-rDfITncpn5hliPUAAAA.i2p/pub/bt/announce.php=http://de-ebook-archiv.i2p/pub/bt/"
-//       , "Gaytorrents", "http://uxPWHbK1OIj9HxquaXuhMiIvi21iK0~ZiG9d8G0840ZXIg0r6CbiV71xlsqmdnU6wm0T2LySriM0doW2gUigo-5BNkUquHwOjLROiETnB3ZR0Ml4IGa6QBPn1aAq2d9~g1r1nVjLE~pcFnXB~cNNS7kIhX1d6nLgYVZf0C2cZopEow2iWVUggGGnAA9mHjE86zLEnTvAyhbAMTqDQJhEuLa0ZYSORqzJDMkQt90MV4YMjX1ICY6RfUSFmxEqu0yWTrkHsTtRw48l~dz9wpIgc0a0T9C~eeWvmBFTqlJPtQZwntpNeH~jF7nlYzB58olgV2HHFYpVYD87DYNzTnmNWxCJ5AfDorm6AIUCV2qaE7tZtI1h6fbmGpGlPyW~Kw5GXrRfJwNvr6ajwAVi~bPVnrBwDZezHkfW4slOO8FACPR28EQvaTu9nwhAbqESxV2hCTq6vQSGjuxHeOuzBOEvRWkLKOHWTC09t2DbJ94FSqETmZopTB1ukEmaxRWbKSIaAAAA.i2p/announce.php=http://gaytorrents.i2p/"
-//       , "NickyB", "http://9On6d3cZ27JjwYCtyJJbowe054d5tFnfMjv4PHsYs-EQn4Y4mk2zRixatvuAyXz2MmRfXG-NAUfhKr0KCxRNZbvHmlckYfT-WBzwwpiMAl0wDFY~Pl8cqXuhfikSG5WrqdPfDNNIBuuznS0dqaczf~OyVaoEOpvuP3qV6wKqbSSLpjOwwAaQPHjlRtNIW8-EtUZp-I0LT45HSoowp~6b7zYmpIyoATvIP~sT0g0MTrczWhbVTUZnEkZeLhOR0Duw1-IRXI2KHPbA24wLO9LdpKKUXed05RTz0QklW5ROgR6TYv7aXFufX8kC0-DaKvQ5JKG~h8lcoHvm1RCzNqVE-2aiZnO2xH08H-iCWoLNJE-Td2kT-Tsc~3QdQcnEUcL5BF-VT~QYRld2--9r0gfGl-yDrJZrlrihHGr5J7ImahelNn9PpkVp6eIyABRmJHf2iicrk3CtjeG1j9OgTSwaNmEpUpn4aN7Kx0zNLdH7z6uTgCGD9Kmh1MFYrsoNlTp4AAAA.i2p/bittorrent/announce.php=http://nickyb.i2p/bittorrent/"
-//       , "Orion", "http://gKik1lMlRmuroXVGTZ~7v4Vez3L3ZSpddrGZBrxVriosCQf7iHu6CIk8t15BKsj~P0JJpxrofeuxtm7SCUAJEr0AIYSYw8XOmp35UfcRPQWyb1LsxUkMT4WqxAT3s1ClIICWlBu5An~q-Mm0VFlrYLIPBWlUFnfPR7jZ9uP5ZMSzTKSMYUWao3ejiykr~mtEmyls6g-ZbgKZawa9II4zjOy-hdxHgP-eXMDseFsrym4Gpxvy~3Fv9TuiSqhpgm~UeTo5YBfxn6~TahKtE~~sdCiSydqmKBhxAQ7uT9lda7xt96SS09OYMsIWxLeQUWhns-C~FjJPp1D~IuTrUpAFcVEGVL-BRMmdWbfOJEcWPZ~CBCQSO~VkuN1ebvIOr9JBerFMZSxZtFl8JwcrjCIBxeKPBmfh~xYh16BJm1BBBmN1fp2DKmZ2jBNkAmnUbjQOqWvUcehrykWk5lZbE7bjJMDFH48v3SXwRuDBiHZmSbsTY6zhGY~GkMQHNGxPMMSIAAAA.i2p/bt/announce.php=http://orion.i2p/bt/"
-//       , "anonymity", "http://8EoJZIKrWgGuDrxA3nRJs1jsPfiGwmFWL91hBrf0HA7oKhEvAna4Ocx47VLUR9retVEYBAyWFK-eZTPcvhnz9XffBEiJQQ~kFSCqb1fV6IfPiV3HySqi9U5Caf6~hC46fRd~vYnxmaBLICT3N160cxBETqH3v2rdxdJpvYt8q4nMk9LUeVXq7zqCTFLLG5ig1uKgNzBGe58iNcsvTEYlnbYcE930ABmrzj8G1qQSgSwJ6wx3tUQNl1z~4wSOUMan~raZQD60lRK70GISjoX0-D0Po9WmPveN3ES3g72TIET3zc3WPdK2~lgmKGIs8GgNLES1cXTolvbPhdZK1gxddRMbJl6Y6IPFyQ9o4-6Rt3Lp-RMRWZ2TG7j2OMcNSiOmATUhKEFBDfv-~SODDyopGBmfeLw16F4NnYednvn4qP10dyMHcUASU6Zag4mfc2-WivrOqeWhD16fVAh8MoDpIIT~0r9XmwdaVFyLcjbXObabJczxCAW3fodQUnvuSkwzAAAA.i2p/anonymityTracker/announce.php=http://anonymityweb.i2p/anonymityTracker/"
-//       , "The freak's tracker", "http://mHKva9x24E5Ygfey2llR1KyQHv5f8hhMpDMwJDg1U-hABpJ2NrQJd6azirdfaR0OKt4jDlmP2o4Qx0H598~AteyD~RJU~xcWYdcOE0dmJ2e9Y8-HY51ie0B1yD9FtIV72ZI-V3TzFDcs6nkdX9b81DwrAwwFzx0EfNvK1GLVWl59Ow85muoRTBA1q8SsZImxdyZ-TApTVlMYIQbdI4iQRwU9OmmtefrCe~ZOf4UBS9-KvNIqUL0XeBSqm0OU1jq-D10Ykg6KfqvuPnBYT1BYHFDQJXW5DdPKwcaQE4MtAdSGmj1epDoaEBUa9btQlFsM2l9Cyn1hzxqNWXELmx8dRlomQLlV4b586dRzW~fLlOPIGC13ntPXogvYvHVyEyptXkv890jC7DZNHyxZd5cyrKC36r9huKvhQAmNABT2Y~pOGwVrb~RpPwT0tBuPZ3lHYhBFYmD8y~AOhhNHKMLzea1rfwTvovBMByDdFps54gMN1mX4MbCGT4w70vIopS9yAAAA.i2p/bytemonsoon/announce.php"
-//       , "mastertracker", "http://VzXD~stRKbL3MOmeTn1iaCQ0CFyTmuFHiKYyo0Rd~dFPZFCYH-22rT8JD7i-C2xzYFa4jT5U2aqHzHI-Jre4HL3Ri5hFtZrLk2ax3ji7Qfb6qPnuYkuiF2E2UDmKUOppI8d9Ye7tjdhQVCy0izn55tBaB-U7UWdcvSK2i85sauyw3G0Gfads1Rvy5-CAe2paqyYATcDmGjpUNLoxbfv9KH1KmwRTNH6k1v4PyWYYnhbT39WfKMbBjSxVQRdi19cyJrULSWhjxaQfJHeWx5Z8Ev4bSPByBeQBFl2~4vqy0S5RypINsRSa3MZdbiAAyn5tr5slWR6QdoqY3qBQgBJFZppy-3iWkFqqKgSxCPundF8gdDLC5ddizl~KYcYKl42y9SGFHIukH-TZs8~em0~iahzsqWVRks3zRG~tlBcX2U3M2~OJs~C33-NKhyfZT7-XFBREvb8Szmd~p66jDxrwOnKaku-G6DyoQipJqIz4VHmY9-y5T8RrUcJcM-5lVoMpAAAA.i2p/announce.php=http://tracker.mastertracker.i2p/"
-//       , "Galen", "http://5jpwQMI5FT303YwKa5Rd38PYSX04pbIKgTaKQsWbqoWjIfoancFdWCShXHLI5G5ofOb0Xu11vl2VEMyPsg1jUFYSVnu4-VfMe3y4TKTR6DTpetWrnmEK6m2UXh91J5DZJAKlgmO7UdsFlBkQfR2rY853-DfbJtQIFl91tbsmjcA5CGQi4VxMFyIkBzv-pCsuLQiZqOwWasTlnzey8GcDAPG1LDcvfflGV~6F5no9mnuisZPteZKlrv~~TDoXTj74QjByWc4EOYlwqK8sbU9aOvz~s31XzErbPTfwiawiaZ0RUI-IDrKgyvmj0neuFTWgjRGVTH8bz7cBZIc3viy6ioD-eMQOrXaQL0TCWZUelRwHRvgdPiQrxdYQs7ixkajeHzxi-Pq0EMm5Vbh3j3Q9kfUFW3JjFDA-MLB4g6XnjCbM5J1rC0oOBDCIEfhQkszru5cyLjHiZ5yeA0VThgu~c7xKHybv~OMXION7V8pBKOgET7ZgAkw1xgYe3Kkyq5syAAAA.i2p/tr/announce.php=http://galen.i2p/tr/"
-       "POSTMAN", "http://tracker2.postman.i2p/announce.php=http://tracker2.postman.i2p/"
-       ,"WELTERDE", "http://tracker.welterde.i2p/a=http://tracker.welterde.i2p/stats?mode=top5"
-       , "CRSTRACK", "http://b4G9sCdtfvccMAXh~SaZrPqVQNyGQbhbYMbw6supq2XGzbjU4NcOmjFI0vxQ8w1L05twmkOvg5QERcX6Mi8NQrWnR0stLExu2LucUXg1aYjnggxIR8TIOGygZVIMV3STKH4UQXD--wz0BUrqaLxPhrm2Eh9Hwc8TdB6Na4ShQUq5Xm8D4elzNUVdpM~RtChEyJWuQvoGAHY3ppX-EJJLkiSr1t77neS4Lc-KofMVmgI9a2tSSpNAagBiNI6Ak9L1T0F9uxeDfEG9bBSQPNMOSUbAoEcNxtt7xOW~cNOAyMyGydwPMnrQ5kIYPY8Pd3XudEko970vE0D6gO19yoBMJpKx6Dh50DGgybLQ9CpRaynh2zPULTHxm8rneOGRcQo8D3mE7FQ92m54~SvfjXjD2TwAVGI~ae~n9HDxt8uxOecAAvjjJ3TD4XM63Q9TmB38RmGNzNLDBQMEmJFpqQU8YeuhnS54IVdUoVQFqui5SfDeLXlSkh4vYoMU66pvBfWbAAAA.i2p/tracker/announce.php=http://crstrack.i2p/tracker/"
-    };
-    
-    /** comma delimited list of name=announceURL=baseURL for the trackers to be displayed */
-    public static final String PROP_TRACKERS = "i2psnark.trackers";
-    private static Map trackerMap = null;
-    /** sorted map of name to announceURL=baseURL */
-    public Map getTrackers() { 
-        if (trackerMap != null) // only do this once, can't be updated while running
-            return trackerMap;
-        Map rv = new TreeMap();
-        String trackers = _config.getProperty(PROP_TRACKERS);
-        if ( (trackers == null) || (trackers.trim().length() <= 0) )
-            trackers = _context.getProperty(PROP_TRACKERS);
-        if ( (trackers == null) || (trackers.trim().length() <= 0) ) {
-            for (int i = 0; i < DEFAULT_TRACKERS.length; i += 2)
-                rv.put(DEFAULT_TRACKERS[i], DEFAULT_TRACKERS[i+1]);
-        } else {
-            StringTokenizer tok = new StringTokenizer(trackers, ",");
-            while (tok.hasMoreTokens()) {
-                String pair = tok.nextToken();
-                int split = pair.indexOf('=');
-                if (split <= 0)
-                    continue;
-                String name = pair.substring(0, split).trim();
-                String url = pair.substring(split+1).trim();
-                if ( (name.length() > 0) && (url.length() > 0) )
-                    rv.put(name, url);
-            }
-        }
-        
-        trackerMap = rv;
-        return trackerMap;
-    }
-    
-    private static class TorrentFilenameFilter implements FilenameFilter {
-        private static final TorrentFilenameFilter _filter = new TorrentFilenameFilter();
-        public static TorrentFilenameFilter instance() { return _filter; }
-        public boolean accept(File dir, String name) {
-            return (name != null) && (name.endsWith(".torrent"));
-        }
-    }
-
-    public class SnarkManagerShutdown extends I2PAppThread {
-        @Override
-        public void run() {
-            Set names = listTorrentFiles();
-            for (Iterator iter = names.iterator(); iter.hasNext(); ) {
-                Snark snark = getTorrent((String)iter.next());
-                if ( (snark != null) && (!snark.stopped) )
-                    snark.stopTorrent();
-            }
-        }
-    }
-}
+package org.klomp.snark;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.TreeMap;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.Base64;
+import net.i2p.data.DataHelper;
+import net.i2p.util.I2PAppThread;
+import net.i2p.util.Log;
+import net.i2p.util.OrderedProperties;
+import net.i2p.util.SecureDirectory;
+
+/**
+ * Manage multiple snarks
+ */
+public class SnarkManager implements Snark.CompleteListener {
+    private static SnarkManager _instance = new SnarkManager();
+    public static SnarkManager instance() { return _instance; }
+    
+    /** map of (canonical) filename of the .torrent file to Snark instance (unsynchronized) */
+    private final Map<String, Snark> _snarks;
+    private final Object _addSnarkLock;
+    private /* FIXME final FIXME */ File _configFile;
+    private Properties _config;
+    private I2PAppContext _context;
+    private Log _log;
+    private final List _messages;
+    private I2PSnarkUtil _util;
+    private PeerCoordinatorSet _peerCoordinatorSet;
+    private ConnectionAcceptor _connectionAcceptor;
+    private Thread _monitor;
+    private boolean _running;
+    
+    public static final String PROP_I2CP_HOST = "i2psnark.i2cpHost";
+    public static final String PROP_I2CP_PORT = "i2psnark.i2cpPort";
+    public static final String PROP_I2CP_OPTS = "i2psnark.i2cpOptions";
+    //public static final String PROP_EEP_HOST = "i2psnark.eepHost";
+    //public static final String PROP_EEP_PORT = "i2psnark.eepPort";
+    public static final String PROP_UPLOADERS_TOTAL = "i2psnark.uploaders.total";
+    public static final String PROP_UPBW_MAX = "i2psnark.upbw.max";
+    public static final String PROP_DIR = "i2psnark.dir";
+    public static final String PROP_META_PREFIX = "i2psnark.zmeta.";
+    public static final String PROP_META_BITFIELD_SUFFIX = ".bitfield";
+
+    private static final String CONFIG_FILE = "i2psnark.config";
+    public static final String PROP_AUTO_START = "i2snark.autoStart";   // oops
+    public static final String DEFAULT_AUTO_START = "false";
+    public static final String PROP_LINK_PREFIX = "i2psnark.linkPrefix";
+    public static final String DEFAULT_LINK_PREFIX = "file:///";
+    public static final String PROP_STARTUP_DELAY = "i2psnark.startupDelay";
+   
+    public static final int MIN_UP_BW = 2;
+    public static final int DEFAULT_MAX_UP_BW = 10;
+    public static final int DEFAULT_STARTUP_DELAY = 3; 
+    private SnarkManager() {
+        _snarks = new HashMap();
+        _addSnarkLock = new Object();
+        _context = I2PAppContext.getGlobalContext();
+        _log = _context.logManager().getLog(SnarkManager.class);
+        _messages = new ArrayList(16);
+        _util = new I2PSnarkUtil(_context);
+        _configFile = new File(CONFIG_FILE);
+        if (!_configFile.isAbsolute())
+            _configFile = new File(_context.getConfigDir(), CONFIG_FILE);
+        loadConfig(null);
+    }
+
+    /** Caller _must_ call loadConfig(file) before this if setting new values
+     *  for i2cp host/port or i2psnark.dir
+     */
+    public void start() {
+        _running = true;
+        _peerCoordinatorSet = new PeerCoordinatorSet();
+        _connectionAcceptor = new ConnectionAcceptor(_util);
+        int minutes = getStartupDelayMinutes();
+        _messages.add(_("Adding torrents in {0} minutes", minutes));
+        _monitor = new I2PAppThread(new DirMonitor(), "Snark DirMonitor", true);
+        _monitor.start();
+        _context.addShutdownTask(new SnarkManagerShutdown());
+    }
+
+    public void stop() {
+        _running = false;
+        _monitor.interrupt();
+        _connectionAcceptor.halt();
+        (new SnarkManagerShutdown()).run();
+    }
+    
+    /** hook to I2PSnarkUtil for the servlet */
+    public I2PSnarkUtil util() { return _util; }
+
+    private static final int MAX_MESSAGES = 5;
+    public void addMessage(String message) {
+        synchronized (_messages) {
+            _messages.add(message);
+            while (_messages.size() > MAX_MESSAGES)
+                _messages.remove(0);
+        }
+        if (_log.shouldLog(Log.INFO))
+            _log.info("MSG: " + message);
+    }
+    
+    /** newest last */
+    public List getMessages() {
+        synchronized (_messages) {
+            return new ArrayList(_messages);
+        }
+    }
+    
+    public boolean shouldAutoStart() {
+        return Boolean.valueOf(_config.getProperty(PROP_AUTO_START, DEFAULT_AUTO_START+"")).booleanValue();
+    }
+    public String linkPrefix() {
+        return _config.getProperty(PROP_LINK_PREFIX, DEFAULT_LINK_PREFIX + getDataDir().getAbsolutePath() + File.separatorChar);
+    }
+    private int getStartupDelayMinutes() { 
+	return Integer.valueOf(_config.getProperty(PROP_STARTUP_DELAY)).intValue(); 
+    }
+    public File getDataDir() { 
+        String dir = _config.getProperty(PROP_DIR, "i2psnark");
+        File f = new SecureDirectory(dir);
+        if (!f.isAbsolute())
+            f = new SecureDirectory(_context.getAppDir(), dir);
+        return f; 
+    }
+    
+    /** null to set initial defaults */
+    public void loadConfig(String filename) {
+        if (_config == null)
+            _config = new OrderedProperties();
+        if (filename != null) {
+            File cfg = new File(filename);
+            if (!cfg.isAbsolute())
+                cfg = new File(_context.getConfigDir(), filename);
+            _configFile = cfg;
+            if (cfg.exists()) {
+                try {
+                    DataHelper.loadProps(_config, cfg);
+                } catch (IOException ioe) {
+                   _log.error("Error loading I2PSnark config '" + filename + "'", ioe);
+                }
+            } 
+        } 
+        // now add sane defaults
+        if (!_config.containsKey(PROP_I2CP_HOST))
+            _config.setProperty(PROP_I2CP_HOST, "127.0.0.1");
+        if (!_config.containsKey(PROP_I2CP_PORT))
+            _config.setProperty(PROP_I2CP_PORT, "7654");
+        if (!_config.containsKey(PROP_I2CP_OPTS))
+            _config.setProperty(PROP_I2CP_OPTS, "inbound.length=2 inbound.lengthVariance=0 outbound.length=2 outbound.lengthVariance=0 inbound.quantity=3 outbound.quantity=3");
+        //if (!_config.containsKey(PROP_EEP_HOST))
+        //    _config.setProperty(PROP_EEP_HOST, "127.0.0.1");
+        //if (!_config.containsKey(PROP_EEP_PORT))
+        //    _config.setProperty(PROP_EEP_PORT, "4444");
+        if (!_config.containsKey(PROP_UPLOADERS_TOTAL))
+            _config.setProperty(PROP_UPLOADERS_TOTAL, "" + Snark.MAX_TOTAL_UPLOADERS);
+        if (!_config.containsKey(PROP_DIR))
+            _config.setProperty(PROP_DIR, "i2psnark");
+        if (!_config.containsKey(PROP_AUTO_START))
+            _config.setProperty(PROP_AUTO_START, DEFAULT_AUTO_START);
+	if (!_config.containsKey(PROP_STARTUP_DELAY))
+            _config.setProperty(PROP_STARTUP_DELAY, "" + DEFAULT_STARTUP_DELAY);
+
+        updateConfig();
+    }
+
+    /** call from DirMonitor since loadConfig() is called before router I2CP is up */
+    private void getBWLimit() {
+        if (!_config.containsKey(PROP_UPBW_MAX)) {
+            int[] limits = BWLimits.getBWLimits(_util.getI2CPHost(), _util.getI2CPPort());
+            if (limits != null && limits[1] > 0)
+                _util.setMaxUpBW(limits[1]);
+        }
+    }
+    
+    private void updateConfig() {
+        String i2cpHost = _config.getProperty(PROP_I2CP_HOST);
+        int i2cpPort = getInt(PROP_I2CP_PORT, 7654);
+        String opts = _config.getProperty(PROP_I2CP_OPTS);
+        Map i2cpOpts = new HashMap();
+        if (opts != null) {
+            StringTokenizer tok = new StringTokenizer(opts, " ");
+            while (tok.hasMoreTokens()) {
+                String pair = tok.nextToken();
+                int split = pair.indexOf('=');
+                if (split > 0)
+                    i2cpOpts.put(pair.substring(0, split), pair.substring(split+1));
+            }
+        }
+        if (i2cpHost != null) {
+            _util.setI2CPConfig(i2cpHost, i2cpPort, i2cpOpts);
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Configuring with I2CP options " + i2cpOpts);
+        }
+        //I2PSnarkUtil.instance().setI2CPConfig("66.111.51.110", 7654, new Properties());
+        //String eepHost = _config.getProperty(PROP_EEP_HOST);
+        //int eepPort = getInt(PROP_EEP_PORT, 4444);
+        //if (eepHost != null)
+        //    _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.setStartupDelay(getInt(PROP_STARTUP_DELAY, DEFAULT_STARTUP_DELAY));
+        String ot = _config.getProperty(I2PSnarkUtil.PROP_OPENTRACKERS);
+        if (ot != null)
+            _util.setOpenTrackerString(ot);
+        // FIXME set util use open trackers property somehow
+        getDataDir().mkdirs();
+    }
+    
+    private int getInt(String prop, int defaultVal) {
+        String p = _config.getProperty(prop);
+        try {
+            if ( (p != null) && (p.trim().length() > 0) )
+                return  Integer.parseInt(p.trim());
+        } catch (NumberFormatException nfe) {
+            // ignore
+        }
+        return defaultVal;
+    }
+    
+    public void updateConfig(String dataDir, boolean autoStart, String startDelay, String seedPct, String eepHost, 
+                             String eepPort, String i2cpHost, String i2cpPort, String i2cpOpts,
+                             String upLimit, String upBW, boolean useOpenTrackers, String openTrackers) {
+        boolean changed = false;
+        //if (eepHost != null) {
+        //    // unused, we use socket eepget
+        //    int port = _util.getEepProxyPort();
+        //    try { port = Integer.parseInt(eepPort); } catch (NumberFormatException nfe) {}
+        //    String host = _util.getEepProxyHost();
+        //    if ( (eepHost.trim().length() > 0) && (port > 0) &&
+        //         ((!host.equals(eepHost) || (port != _util.getEepProxyPort()) )) ) {
+        //        _util.setProxy(eepHost, port);
+        //        changed = true;
+        //        _config.setProperty(PROP_EEP_HOST, eepHost);
+        //        _config.setProperty(PROP_EEP_PORT, eepPort+"");
+        //        addMessage("EepProxy location changed to " + eepHost + ":" + port);
+        //    }
+        //}
+        if (upLimit != null) {
+            int limit = _util.getMaxUploaders();
+            try { limit = Integer.parseInt(upLimit); } catch (NumberFormatException nfe) {}
+            if ( limit != _util.getMaxUploaders()) {
+                if ( limit >= Snark.MIN_TOTAL_UPLOADERS ) {
+                    _util.setMaxUploaders(limit);
+                    changed = true;
+                    _config.setProperty(PROP_UPLOADERS_TOTAL, "" + limit);
+                    addMessage(_("Total uploaders limit changed to {0}", limit));
+                } else {
+                    addMessage(_("Minimum total uploaders limit is {0}", Snark.MIN_TOTAL_UPLOADERS));
+                }
+            }
+        }
+        if (upBW != null) {
+            int limit = _util.getMaxUpBW();
+            try { limit = Integer.parseInt(upBW); } catch (NumberFormatException nfe) {}
+            if ( limit != _util.getMaxUpBW()) {
+                if ( limit >= MIN_UP_BW ) {
+                    _util.setMaxUpBW(limit);
+                    changed = true;
+                    _config.setProperty(PROP_UPBW_MAX, "" + limit);
+                    addMessage(_("Up BW limit changed to {0}KBps", limit));
+                } else {
+                    addMessage(_("Minimum up bandwidth limit is {0}KBps", MIN_UP_BW));
+                }
+            }
+        }
+        
+	if (startDelay != null){
+		int minutes = _util.getStartupDelay();
+                try { minutes = Integer.parseInt(startDelay); } catch (NumberFormatException nfe) {}
+	        if ( minutes != _util.getStartupDelay()) {
+                	    _util.setStartupDelay(minutes);
+	                    changed = true;
+        	            _config.setProperty(PROP_STARTUP_DELAY, "" + minutes);
+                	    addMessage(_("Startup delay limit changed to {0} minutes", minutes));
+                	}
+
+	}
+	if (i2cpHost != null) {
+            int oldI2CPPort = _util.getI2CPPort();
+            String oldI2CPHost = _util.getI2CPHost();
+            int port = oldI2CPPort;
+            try { port = Integer.parseInt(i2cpPort); } catch (NumberFormatException nfe) {}
+            String host = oldI2CPHost;
+            Map opts = new HashMap();
+            if (i2cpOpts == null) i2cpOpts = "";
+            StringTokenizer tok = new StringTokenizer(i2cpOpts, " \t\n");
+            while (tok.hasMoreTokens()) {
+                String pair = tok.nextToken();
+                int split = pair.indexOf('=');
+                if (split > 0)
+                    opts.put(pair.substring(0, split), pair.substring(split+1));
+            }
+            Map oldOpts = new HashMap();
+            String oldI2CPOpts = _config.getProperty(PROP_I2CP_OPTS);
+            if (oldI2CPOpts == null) oldI2CPOpts = "";
+            tok = new StringTokenizer(oldI2CPOpts, " \t\n");
+            while (tok.hasMoreTokens()) {
+                String pair = tok.nextToken();
+                int split = pair.indexOf('=');
+                if (split > 0)
+                    oldOpts.put(pair.substring(0, split), pair.substring(split+1));
+            }
+            
+            if ( (i2cpHost.trim().length() > 0) && (port > 0) &&
+                 ((!host.equals(i2cpHost) || 
+                  (port != _util.getI2CPPort()) ||
+                  (!oldOpts.equals(opts)))) ) {
+                boolean snarksActive = false;
+                Set names = listTorrentFiles();
+                for (Iterator iter = names.iterator(); iter.hasNext(); ) {
+                    Snark snark = getTorrent((String)iter.next());
+                    if ( (snark != null) && (!snark.stopped) ) {
+                        snarksActive = true;
+                        break;
+                    }
+                }
+                if (snarksActive) {
+                    Properties p = new Properties();
+                    p.putAll(opts);
+                    _util.setI2CPConfig(i2cpHost, port, p);
+                    addMessage(_("I2CP and tunnel changes will take effect after stopping all torrents"));
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug("i2cp host [" + i2cpHost + "] i2cp port " + port + " opts [" + opts 
+                                   + "] oldOpts [" + oldOpts + "]");
+                } else {
+                    if (_util.connected()) {
+                        _util.disconnect();
+                        addMessage(_("Disconnecting old I2CP destination"));
+                    }
+                    Properties p = new Properties();
+                    p.putAll(opts);
+                    addMessage(_("I2CP settings changed to {0}", i2cpHost + ":" + port + " (" + i2cpOpts.trim() + ")"));
+                    _util.setI2CPConfig(i2cpHost, port, p);
+                    boolean ok = _util.connect();
+                    if (!ok) {
+                        addMessage(_("Unable to connect with the new settings, reverting to the old I2CP settings"));
+                        _util.setI2CPConfig(oldI2CPHost, oldI2CPPort, oldOpts);
+                        ok = _util.connect();
+                        if (!ok)
+                            addMessage(_("Unable to reconnect with the old settings!"));
+                    } else {
+                        addMessage(_("Reconnected on the new I2CP destination"));
+                        _config.setProperty(PROP_I2CP_HOST, i2cpHost.trim());
+                        _config.setProperty(PROP_I2CP_PORT, "" + port);
+                        _config.setProperty(PROP_I2CP_OPTS, i2cpOpts.trim());
+                        changed = true;
+                        // no PeerAcceptors/I2PServerSockets to deal with, since all snarks are inactive
+                        for (Iterator iter = names.iterator(); iter.hasNext(); ) {
+                            String name = (String)iter.next();
+                            Snark snark = getTorrent(name);
+                            if ( (snark != null) && (snark.acceptor != null) ) {
+                                snark.acceptor.restart();
+                                addMessage(_("I2CP listener restarted for \"{0}\"", snark.meta.getName()));
+                            }
+                        }
+                    }
+                }
+                changed = true;
+            }
+        }
+        if (shouldAutoStart() != autoStart) {
+            _config.setProperty(PROP_AUTO_START, autoStart + "");
+            if (autoStart)
+                addMessage(_("Enabled autostart"));
+            else
+                addMessage(_("Disabled autostart"));
+            changed = true;
+        }
+        if (_util.shouldUseOpenTrackers() != useOpenTrackers) {
+            _config.setProperty(I2PSnarkUtil.PROP_USE_OPENTRACKERS, useOpenTrackers + "");
+            if (useOpenTrackers)
+                addMessage(_("Enabled open trackers - torrent restart required to take effect."));
+            else
+                addMessage(_("Disabled open trackers - torrent restart required to take effect."));
+            changed = true;
+        }
+        if (openTrackers != null) {
+            if (openTrackers.trim().length() > 0 && !openTrackers.trim().equals(_util.getOpenTrackerString())) {
+                _config.setProperty(I2PSnarkUtil.PROP_OPENTRACKERS, openTrackers.trim());
+                _util.setOpenTrackerString(openTrackers);
+                addMessage(_("Open Tracker list changed - torrent restart required to take effect."));
+                changed = true;
+            }
+        }
+        if (changed) {
+            saveConfig();
+        } else {
+            addMessage(_("Configuration unchanged."));
+        }
+    }
+    
+    public void saveConfig() {
+        try {
+            synchronized (_configFile) {
+                DataHelper.storeProps(_config, _configFile);
+            }
+        } catch (IOException ioe) {
+            addMessage(_("Unable to save the config to {0}", _configFile.getAbsolutePath()));
+        }
+    }
+    
+    public Properties getConfig() { return _config; }
+    
+    /** hardcoded for sanity.  perhaps this should be customizable, for people who increase their ulimit, etc. */
+    private static final int MAX_FILES_PER_TORRENT = 512;
+    
+    /** set of canonical .torrent filenames that we are dealing with */
+    public Set<String> listTorrentFiles() { synchronized (_snarks) { return new HashSet(_snarks.keySet()); } }
+
+    /**
+     * Grab the torrent given the (canonical) filename of the .torrent file
+     * @return Snark or null
+     */
+    public Snark getTorrent(String filename) { synchronized (_snarks) { return (Snark)_snarks.get(filename); } }
+
+    /**
+     * Grab the torrent given the base name of the storage
+     * @return Snark or null
+     * @since 0.7.14
+     */
+    public Snark getTorrentByBaseName(String filename) {
+        synchronized (_snarks) {
+            for (Snark s : _snarks.values()) {
+                if (s.storage.getBaseName().equals(filename))
+                    return s;
+            }
+        }
+        return null;
+    }
+
+    /** @throws RuntimeException via Snark.fatal() */
+    public void addTorrent(String filename) { addTorrent(filename, false); }
+
+    /** @throws RuntimeException via Snark.fatal() */
+    public void addTorrent(String filename, boolean dontAutoStart) {
+        if ((!dontAutoStart) && !_util.connected()) {
+            addMessage(_("Connecting to I2P"));
+            boolean ok = _util.connect();
+            if (!ok) {
+                addMessage(_("Error connecting to I2P - check your I2CP settings!"));
+                return;
+            }
+        }
+        File sfile = new File(filename);
+        try {
+            filename = sfile.getCanonicalPath();
+        } catch (IOException ioe) {
+            _log.error("Unable to add the torrent " + filename, ioe);
+            addMessage(_("Error: Could not add the torrent {0}", filename) + ": " + ioe.getMessage());
+            return;
+        }
+        File dataDir = getDataDir();
+        Snark torrent = null;
+        synchronized (_snarks) {
+            torrent = (Snark)_snarks.get(filename);
+        }
+        // don't hold the _snarks lock while verifying the torrent
+        if (torrent == null) {
+            synchronized (_addSnarkLock) {
+                // double-check
+                synchronized (_snarks) {
+                    if(_snarks.get(filename) != null)
+                        return;
+                }
+
+                FileInputStream fis = null;
+                try {
+                    fis = new FileInputStream(sfile);
+                } catch (IOException ioe) {
+                    // catch this here so we don't try do delete it below
+                    addMessage(_("Cannot open \"{0}\"", sfile.getName()) + ": " + ioe.getMessage());
+                    return;
+                }
+
+                try {
+                    MetaInfo info = new MetaInfo(fis);
+                    try {
+                        fis.close();
+                        fis = null;
+                    } catch (IOException e) {}
+                    
+                    if (!TrackerClient.isValidAnnounce(info.getAnnounce())) {
+                        if (_util.shouldUseOpenTrackers() && _util.getOpenTrackers() != null) {
+                            addMessage(_("Warning - Ignoring non-i2p tracker in \"{0}\", will announce to i2p open trackers only", info.getName()));
+                        } else {
+                            addMessage(_("Warning - Ignoring non-i2p tracker in \"{0}\", and open trackers are disabled, you must enable open trackers before starting the torrent!", info.getName()));
+                            dontAutoStart = true;
+                        }
+                    }
+                    String rejectMessage = locked_validateTorrent(info);
+                    if (rejectMessage != null) {
+                        sfile.delete();
+                        addMessage(rejectMessage);
+                        return;
+                    } else {
+                        torrent = new Snark(_util, filename, null, -1, null, null, this,
+                                            _peerCoordinatorSet, _connectionAcceptor,
+                                            false, dataDir.getPath());
+                        torrent.completeListener = this;
+                        synchronized (_snarks) {
+                            _snarks.put(filename, torrent);
+                        }
+                    }
+                } catch (IOException ioe) {
+                    addMessage(_("Torrent in \"{0}\" is invalid", sfile.getName()) + ": " + ioe.getMessage());
+                    if (sfile.exists())
+                        sfile.delete();
+                    return;
+                } finally {
+                    if (fis != null) try { fis.close(); } catch (IOException ioe) {}
+                }
+            }
+        } else {
+            return;
+        }
+        // ok, snark created, now lets start it up or configure it further
+        File f = new File(filename);
+        if (!dontAutoStart && shouldAutoStart()) {
+            torrent.startTorrent();
+            addMessage(_("Torrent added and started: \"{0}\"", f.getName()));
+        } else {
+            addMessage(_("Torrent added: \"{0}\"", f.getName()));
+        }
+    }
+    
+    /**
+     * Get the timestamp for a torrent from the config file
+     */
+    public long getSavedTorrentTime(Snark snark) {
+        MetaInfo metainfo = snark.meta;
+        byte[] ih = metainfo.getInfoHash();
+        String infohash = Base64.encode(ih);
+        infohash = infohash.replace('=', '$');
+        String time = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX);
+        if (time == null)
+            return 0;
+        int comma = time.indexOf(',');
+        if (comma <= 0)
+            return 0;
+        time = time.substring(0, comma);
+        try { return Long.parseLong(time); } catch (NumberFormatException nfe) {}
+        return 0;
+    }
+    
+    /**
+     * Get the saved bitfield for a torrent from the config file.
+     * Convert "." to a full bitfield.
+     */
+    public BitField getSavedTorrentBitField(Snark snark) {
+        MetaInfo metainfo = snark.meta;
+        byte[] ih = metainfo.getInfoHash();
+        String infohash = Base64.encode(ih);
+        infohash = infohash.replace('=', '$');
+        String bf = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX);
+        if (bf == null)
+            return null;
+        int comma = bf.indexOf(',');
+        if (comma <= 0)
+            return null;
+        bf = bf.substring(comma + 1).trim();
+        int len = metainfo.getPieces();
+        if (bf.equals(".")) {
+            BitField bitfield = new BitField(len);
+            for (int i = 0; i < len; i++)
+                 bitfield.set(i);
+            return bitfield;
+        }
+        byte[] bitfield = Base64.decode(bf);
+        if (bitfield == null)
+            return null;
+        if (bitfield.length * 8 < len)
+            return null;
+        return new BitField(bitfield, len);
+    }
+    
+    /**
+     * Save the completion status of a torrent and the current time in the config file
+     * in the form "i2psnark.zmeta.$base64infohash=$time,$base64bitfield".
+     * The config file property key is appended with the Base64 of the infohash,
+     * with the '=' changed to '$' since a key can't contain '='.
+     * The time is a standard long converted to string.
+     * The status is either a bitfield converted to Base64 or "." for a completed
+     * torrent to save space in the config file and in memory.
+     */
+    public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield) {
+        byte[] ih = metainfo.getInfoHash();
+        String infohash = Base64.encode(ih);
+        infohash = infohash.replace('=', '$');
+        String now = "" + System.currentTimeMillis();
+        String bfs;
+        if (bitfield.complete()) {
+          bfs = ".";
+        } else {
+          byte[] bf = bitfield.getFieldBytes();
+          bfs = Base64.encode(bf);
+        }
+        _config.setProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX, now + "," + bfs);
+        saveConfig();
+    }
+    
+    /**
+     * Remove the status of a torrent from the config file.
+     * This may help the config file from growing too big.
+     */
+    public void removeTorrentStatus(MetaInfo metainfo) {
+        byte[] ih = metainfo.getInfoHash();
+        String infohash = Base64.encode(ih);
+        infohash = infohash.replace('=', '$');
+        _config.remove(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX);
+        saveConfig();
+    }
+    
+    /**
+     *  Warning - does not validate announce URL - use TrackerClient.isValidAnnounce()
+     */
+    private String locked_validateTorrent(MetaInfo info) throws IOException {
+        List files = info.getFiles();
+        if ( (files != null) && (files.size() > MAX_FILES_PER_TORRENT) ) {
+            return _("Too many files in \"{0}\" ({1}), deleting it!", info.getName(), files.size());
+        } else if ( (files == null) && (info.getName().endsWith(".torrent")) ) {
+            return _("Torrent file \"{0}\" cannot end in \".torrent\", deleting it!", info.getName());
+        } else if (info.getPieces() <= 0) {
+            return _("No pieces in \"{0}\",  deleting it!", info.getName());
+        } else if (info.getPieces() > Storage.MAX_PIECES) {
+            return _("Too many pieces in \"{0}\", limit is {1}, deleting it!", info.getName(), Storage.MAX_PIECES);
+        } else if (info.getPieceLength(0) > Storage.MAX_PIECE_SIZE) {
+            return _("Pieces are too large in \"{0}\" ({1}B), deleting it.", info.getName(), DataHelper.formatSize2(info.getPieceLength(0))) + ' ' +
+                   _("Limit is {0}B", DataHelper.formatSize2(Storage.MAX_PIECE_SIZE));
+        } else if (info.getTotalLength() > Storage.MAX_TOTAL_SIZE) {
+            System.out.println("torrent info: " + info.toString());
+            List lengths = info.getLengths();
+            if (lengths != null)
+                for (int i = 0; i < lengths.size(); i++)
+                    System.out.println("File " + i + " is " + lengths.get(i) + " long.");
+            
+            return _("Torrents larger than {0}B are not supported yet, deleting \"{1}\"", Storage.MAX_TOTAL_SIZE, info.getName());
+        } else {
+            // ok
+            return null;
+        }
+    }
+    
+    /**
+     * Stop the torrent, leaving it on the list of torrents unless told to remove it
+     */
+    public Snark stopTorrent(String filename, boolean shouldRemove) {
+        File sfile = new File(filename);
+        try {
+            filename = sfile.getCanonicalPath();
+        } catch (IOException ioe) {
+            _log.error("Unable to remove the torrent " + filename, ioe);
+            addMessage(_("Error: Could not remove the torrent {0}", filename) + ": " + ioe.getMessage());
+            return null;
+        }
+        int remaining = 0;
+        Snark torrent = null;
+        synchronized (_snarks) {
+            if (shouldRemove)
+                torrent = (Snark)_snarks.remove(filename);
+            else
+                torrent = (Snark)_snarks.get(filename);
+            remaining = _snarks.size();
+        }
+        if (torrent != null) {
+            boolean wasStopped = torrent.stopped;
+            torrent.stopTorrent();
+            if (remaining == 0) {
+                // should we disconnect/reconnect here (taking care to deal with the other thread's
+                // I2PServerSocket.accept() call properly?)
+                ////_util.
+            }
+            if (!wasStopped)
+                addMessage(_("Torrent stopped: \"{0}\"", sfile.getName()));
+        }
+        return torrent;
+    }
+    /**
+     * Stop the torrent and delete the torrent file itself, but leaving the data
+     * behind.
+     */
+    public void removeTorrent(String filename) {
+        Snark torrent = stopTorrent(filename, true);
+        if (torrent != null) {
+            File torrentFile = new File(filename);
+            torrentFile.delete();
+            if (torrent.storage != null)
+                removeTorrentStatus(torrent.storage.getMetaInfo());
+            addMessage(_("Torrent removed: \"{0}\"", torrentFile.getName()));
+        }
+    }
+    
+    private class DirMonitor implements Runnable {
+        public void run() {
+            try { Thread.sleep(60*1000*getStartupDelayMinutes()); } catch (InterruptedException ie) {}
+            // the first message was a "We are starting up in 1m" 
+            synchronized (_messages) { 
+                if (_messages.size() == 1)
+                    _messages.remove(0);
+            }
+
+            // here because we need to delay until I2CP is up
+            // although the user will see the default until then
+            getBWLimit();
+            while (true) {
+                File dir = getDataDir();
+                if (_log.shouldLog(Log.DEBUG))
+                    _log.debug("Directory Monitor loop over " + dir.getAbsolutePath());
+                try {
+                    monitorTorrents(dir);
+                } catch (Exception e) {
+                    _log.error("Error in the DirectoryMonitor", e);
+                }
+                try { Thread.sleep(60*1000); } catch (InterruptedException ie) {}
+            }
+        }
+    }
+    
+    /** two listeners */
+    public void torrentComplete(Snark snark) {
+        StringBuilder buf = new StringBuilder(256);
+        buf.append("<a href=\"/i2psnark/").append(snark.storage.getBaseName());
+        if (snark.meta.getFiles() != null)
+            buf.append('/');
+        buf.append("\">").append(snark.storage.getBaseName()).append("</a>");
+        long len = snark.meta.getTotalLength();
+        addMessage(_("Download finished: {0}", buf.toString()) + " (" + _("size: {0}B", DataHelper.formatSize2(len)) + ')');
+        updateStatus(snark);
+    }
+    
+    public void updateStatus(Snark snark) {
+        saveTorrentStatus(snark.meta, snark.storage.getBitField());
+    }
+    
+    private void monitorTorrents(File dir) {
+        String fileNames[] = dir.list(TorrentFilenameFilter.instance());
+        List<String> foundNames = new ArrayList(0);
+        if (fileNames != null) {
+            for (int i = 0; i < fileNames.length; i++) {
+                try {
+                    foundNames.add(new File(dir, fileNames[i]).getCanonicalPath());
+                } catch (IOException ioe) {
+                    _log.error("Error resolving '" + fileNames[i] + "' in '" + dir, ioe);
+                }
+            }
+        }
+        
+        Set<String> existingNames = listTorrentFiles();
+        // lets find new ones first...
+        for (int i = 0; i < foundNames.size(); i++) {
+            if (existingNames.contains(foundNames.get(i))) {
+                // already known.  noop
+            } else {
+                if (shouldAutoStart() && !_util.connect())
+                    addMessage(_("Unable to connect to I2P!"));
+                try {
+                    // Snark.fatal() throws a RuntimeException
+                    // don't let one bad torrent kill the whole loop
+                    addTorrent(foundNames.get(i), !shouldAutoStart());
+                } catch (Exception e) {
+                    addMessage(_("Unable to add {0}", foundNames.get(i)) + ": " + e);
+                }
+            }
+        }
+        // now lets see which ones have been removed...
+        for (Iterator iter = existingNames.iterator(); iter.hasNext(); ) {
+            String name = (String)iter.next();
+            if (foundNames.contains(name)) {
+                // known and still there.  noop
+            } else {
+                // known, but removed.  drop it
+                try {
+                    // Snark.fatal() throws a RuntimeException
+                    // don't let one bad torrent kill the whole loop
+                    stopTorrent(name, true);
+                } catch (Exception e) {
+                    // don't bother with message
+                }
+            }
+        }
+    }
+
+    /** translate */
+    private String _(String s) {
+        return _util.getString(s);
+    }
+
+    /** translate */
+    private String _(String s, Object o) {
+        return _util.getString(s, o);
+    }
+
+    /** translate */
+    private String _(String s, Object o, Object o2) {
+        return _util.getString(s, o, o2);
+    }
+
+    /**
+     *  "name", "announceURL=websiteURL" pairs
+     */
+    private static final String DEFAULT_TRACKERS[] = { 
+//       "Postman", "http://YRgrgTLGnbTq2aZOZDJQ~o6Uk5k6TK-OZtx0St9pb0G-5EGYURZioxqYG8AQt~LgyyI~NCj6aYWpPO-150RcEvsfgXLR~CxkkZcVpgt6pns8SRc3Bi-QSAkXpJtloapRGcQfzTtwllokbdC-aMGpeDOjYLd8b5V9Im8wdCHYy7LRFxhEtGb~RL55DA8aYOgEXcTpr6RPPywbV~Qf3q5UK55el6Kex-6VCxreUnPEe4hmTAbqZNR7Fm0hpCiHKGoToRcygafpFqDw5frLXToYiqs9d4liyVB-BcOb0ihORbo0nS3CLmAwZGvdAP8BZ7cIYE3Z9IU9D1G8JCMxWarfKX1pix~6pIA-sp1gKlL1HhYhPMxwyxvuSqx34o3BqU7vdTYwWiLpGM~zU1~j9rHL7x60pVuYaXcFQDR4-QVy26b6Pt6BlAZoFmHhPcAuWfu-SFhjyZYsqzmEmHeYdAwa~HojSbofg0TMUgESRXMw6YThK1KXWeeJVeztGTz25sL8AAAA.i2p/announce.php=http://tracker.postman.i2p/"
+//       , "eBook", "http://E71FRom6PZNEqTN2Lr8P-sr23b7HJVC32KoGnVQjaX6zJiXwhJy2HsXob36Qmj81TYFZdewFZa9mSJ533UZgGyQkXo2ahctg82JKYZfDe5uDxAn1E9YPjxZCWJaFJh0S~UwSs~9AZ7UcauSJIoNtpxrtbmRNVFLqnkEDdLZi26TeucfOmiFmIWnVblLniWv3tG1boE9Abd-6j3FmYVrRucYuepAILYt6katmVNOk6sXmno1Eynrp~~MBuFq0Ko6~jsc2E2CRVYXDhGHEMdt-j6JUz5D7S2RIVzDRqQyAZLKJ7OdQDmI31przzmne1vOqqqLC~1xUumZVIvF~yOeJUGNjJ1Vx0J8i2BQIusn1pQJ6UCB~ZtZZLQtEb8EPVCfpeRi2ri1M5CyOuxN0V5ekmPHrYIBNevuTCRC26NP7ZS5VDgx1~NaC3A-CzJAE6f1QXi0wMI9aywNG5KGzOPifcsih8eyGyytvgLtrZtV7ykzYpPCS-rDfITncpn5hliPUAAAA.i2p/pub/bt/announce.php=http://de-ebook-archiv.i2p/pub/bt/"
+//       , "Gaytorrents", "http://uxPWHbK1OIj9HxquaXuhMiIvi21iK0~ZiG9d8G0840ZXIg0r6CbiV71xlsqmdnU6wm0T2LySriM0doW2gUigo-5BNkUquHwOjLROiETnB3ZR0Ml4IGa6QBPn1aAq2d9~g1r1nVjLE~pcFnXB~cNNS7kIhX1d6nLgYVZf0C2cZopEow2iWVUggGGnAA9mHjE86zLEnTvAyhbAMTqDQJhEuLa0ZYSORqzJDMkQt90MV4YMjX1ICY6RfUSFmxEqu0yWTrkHsTtRw48l~dz9wpIgc0a0T9C~eeWvmBFTqlJPtQZwntpNeH~jF7nlYzB58olgV2HHFYpVYD87DYNzTnmNWxCJ5AfDorm6AIUCV2qaE7tZtI1h6fbmGpGlPyW~Kw5GXrRfJwNvr6ajwAVi~bPVnrBwDZezHkfW4slOO8FACPR28EQvaTu9nwhAbqESxV2hCTq6vQSGjuxHeOuzBOEvRWkLKOHWTC09t2DbJ94FSqETmZopTB1ukEmaxRWbKSIaAAAA.i2p/announce.php=http://gaytorrents.i2p/"
+//       , "NickyB", "http://9On6d3cZ27JjwYCtyJJbowe054d5tFnfMjv4PHsYs-EQn4Y4mk2zRixatvuAyXz2MmRfXG-NAUfhKr0KCxRNZbvHmlckYfT-WBzwwpiMAl0wDFY~Pl8cqXuhfikSG5WrqdPfDNNIBuuznS0dqaczf~OyVaoEOpvuP3qV6wKqbSSLpjOwwAaQPHjlRtNIW8-EtUZp-I0LT45HSoowp~6b7zYmpIyoATvIP~sT0g0MTrczWhbVTUZnEkZeLhOR0Duw1-IRXI2KHPbA24wLO9LdpKKUXed05RTz0QklW5ROgR6TYv7aXFufX8kC0-DaKvQ5JKG~h8lcoHvm1RCzNqVE-2aiZnO2xH08H-iCWoLNJE-Td2kT-Tsc~3QdQcnEUcL5BF-VT~QYRld2--9r0gfGl-yDrJZrlrihHGr5J7ImahelNn9PpkVp6eIyABRmJHf2iicrk3CtjeG1j9OgTSwaNmEpUpn4aN7Kx0zNLdH7z6uTgCGD9Kmh1MFYrsoNlTp4AAAA.i2p/bittorrent/announce.php=http://nickyb.i2p/bittorrent/"
+//       , "Orion", "http://gKik1lMlRmuroXVGTZ~7v4Vez3L3ZSpddrGZBrxVriosCQf7iHu6CIk8t15BKsj~P0JJpxrofeuxtm7SCUAJEr0AIYSYw8XOmp35UfcRPQWyb1LsxUkMT4WqxAT3s1ClIICWlBu5An~q-Mm0VFlrYLIPBWlUFnfPR7jZ9uP5ZMSzTKSMYUWao3ejiykr~mtEmyls6g-ZbgKZawa9II4zjOy-hdxHgP-eXMDseFsrym4Gpxvy~3Fv9TuiSqhpgm~UeTo5YBfxn6~TahKtE~~sdCiSydqmKBhxAQ7uT9lda7xt96SS09OYMsIWxLeQUWhns-C~FjJPp1D~IuTrUpAFcVEGVL-BRMmdWbfOJEcWPZ~CBCQSO~VkuN1ebvIOr9JBerFMZSxZtFl8JwcrjCIBxeKPBmfh~xYh16BJm1BBBmN1fp2DKmZ2jBNkAmnUbjQOqWvUcehrykWk5lZbE7bjJMDFH48v3SXwRuDBiHZmSbsTY6zhGY~GkMQHNGxPMMSIAAAA.i2p/bt/announce.php=http://orion.i2p/bt/"
+//       , "anonymity", "http://8EoJZIKrWgGuDrxA3nRJs1jsPfiGwmFWL91hBrf0HA7oKhEvAna4Ocx47VLUR9retVEYBAyWFK-eZTPcvhnz9XffBEiJQQ~kFSCqb1fV6IfPiV3HySqi9U5Caf6~hC46fRd~vYnxmaBLICT3N160cxBETqH3v2rdxdJpvYt8q4nMk9LUeVXq7zqCTFLLG5ig1uKgNzBGe58iNcsvTEYlnbYcE930ABmrzj8G1qQSgSwJ6wx3tUQNl1z~4wSOUMan~raZQD60lRK70GISjoX0-D0Po9WmPveN3ES3g72TIET3zc3WPdK2~lgmKGIs8GgNLES1cXTolvbPhdZK1gxddRMbJl6Y6IPFyQ9o4-6Rt3Lp-RMRWZ2TG7j2OMcNSiOmATUhKEFBDfv-~SODDyopGBmfeLw16F4NnYednvn4qP10dyMHcUASU6Zag4mfc2-WivrOqeWhD16fVAh8MoDpIIT~0r9XmwdaVFyLcjbXObabJczxCAW3fodQUnvuSkwzAAAA.i2p/anonymityTracker/announce.php=http://anonymityweb.i2p/anonymityTracker/"
+//       , "The freak's tracker", "http://mHKva9x24E5Ygfey2llR1KyQHv5f8hhMpDMwJDg1U-hABpJ2NrQJd6azirdfaR0OKt4jDlmP2o4Qx0H598~AteyD~RJU~xcWYdcOE0dmJ2e9Y8-HY51ie0B1yD9FtIV72ZI-V3TzFDcs6nkdX9b81DwrAwwFzx0EfNvK1GLVWl59Ow85muoRTBA1q8SsZImxdyZ-TApTVlMYIQbdI4iQRwU9OmmtefrCe~ZOf4UBS9-KvNIqUL0XeBSqm0OU1jq-D10Ykg6KfqvuPnBYT1BYHFDQJXW5DdPKwcaQE4MtAdSGmj1epDoaEBUa9btQlFsM2l9Cyn1hzxqNWXELmx8dRlomQLlV4b586dRzW~fLlOPIGC13ntPXogvYvHVyEyptXkv890jC7DZNHyxZd5cyrKC36r9huKvhQAmNABT2Y~pOGwVrb~RpPwT0tBuPZ3lHYhBFYmD8y~AOhhNHKMLzea1rfwTvovBMByDdFps54gMN1mX4MbCGT4w70vIopS9yAAAA.i2p/bytemonsoon/announce.php"
+//       , "mastertracker", "http://VzXD~stRKbL3MOmeTn1iaCQ0CFyTmuFHiKYyo0Rd~dFPZFCYH-22rT8JD7i-C2xzYFa4jT5U2aqHzHI-Jre4HL3Ri5hFtZrLk2ax3ji7Qfb6qPnuYkuiF2E2UDmKUOppI8d9Ye7tjdhQVCy0izn55tBaB-U7UWdcvSK2i85sauyw3G0Gfads1Rvy5-CAe2paqyYATcDmGjpUNLoxbfv9KH1KmwRTNH6k1v4PyWYYnhbT39WfKMbBjSxVQRdi19cyJrULSWhjxaQfJHeWx5Z8Ev4bSPByBeQBFl2~4vqy0S5RypINsRSa3MZdbiAAyn5tr5slWR6QdoqY3qBQgBJFZppy-3iWkFqqKgSxCPundF8gdDLC5ddizl~KYcYKl42y9SGFHIukH-TZs8~em0~iahzsqWVRks3zRG~tlBcX2U3M2~OJs~C33-NKhyfZT7-XFBREvb8Szmd~p66jDxrwOnKaku-G6DyoQipJqIz4VHmY9-y5T8RrUcJcM-5lVoMpAAAA.i2p/announce.php=http://tracker.mastertracker.i2p/"
+//       , "Galen", "http://5jpwQMI5FT303YwKa5Rd38PYSX04pbIKgTaKQsWbqoWjIfoancFdWCShXHLI5G5ofOb0Xu11vl2VEMyPsg1jUFYSVnu4-VfMe3y4TKTR6DTpetWrnmEK6m2UXh91J5DZJAKlgmO7UdsFlBkQfR2rY853-DfbJtQIFl91tbsmjcA5CGQi4VxMFyIkBzv-pCsuLQiZqOwWasTlnzey8GcDAPG1LDcvfflGV~6F5no9mnuisZPteZKlrv~~TDoXTj74QjByWc4EOYlwqK8sbU9aOvz~s31XzErbPTfwiawiaZ0RUI-IDrKgyvmj0neuFTWgjRGVTH8bz7cBZIc3viy6ioD-eMQOrXaQL0TCWZUelRwHRvgdPiQrxdYQs7ixkajeHzxi-Pq0EMm5Vbh3j3Q9kfUFW3JjFDA-MLB4g6XnjCbM5J1rC0oOBDCIEfhQkszru5cyLjHiZ5yeA0VThgu~c7xKHybv~OMXION7V8pBKOgET7ZgAkw1xgYe3Kkyq5syAAAA.i2p/tr/announce.php=http://galen.i2p/tr/"
+       "POSTMAN", "http://tracker2.postman.i2p/announce.php=http://tracker2.postman.i2p/"
+       ,"WELTERDE", "http://tracker.welterde.i2p/a=http://tracker.welterde.i2p/stats?mode=top5"
+       , "CRSTRACK", "http://b4G9sCdtfvccMAXh~SaZrPqVQNyGQbhbYMbw6supq2XGzbjU4NcOmjFI0vxQ8w1L05twmkOvg5QERcX6Mi8NQrWnR0stLExu2LucUXg1aYjnggxIR8TIOGygZVIMV3STKH4UQXD--wz0BUrqaLxPhrm2Eh9Hwc8TdB6Na4ShQUq5Xm8D4elzNUVdpM~RtChEyJWuQvoGAHY3ppX-EJJLkiSr1t77neS4Lc-KofMVmgI9a2tSSpNAagBiNI6Ak9L1T0F9uxeDfEG9bBSQPNMOSUbAoEcNxtt7xOW~cNOAyMyGydwPMnrQ5kIYPY8Pd3XudEko970vE0D6gO19yoBMJpKx6Dh50DGgybLQ9CpRaynh2zPULTHxm8rneOGRcQo8D3mE7FQ92m54~SvfjXjD2TwAVGI~ae~n9HDxt8uxOecAAvjjJ3TD4XM63Q9TmB38RmGNzNLDBQMEmJFpqQU8YeuhnS54IVdUoVQFqui5SfDeLXlSkh4vYoMU66pvBfWbAAAA.i2p/tracker/announce.php=http://crstrack.i2p/tracker/"
+    };
+    
+    /** comma delimited list of name=announceURL=baseURL for the trackers to be displayed */
+    public static final String PROP_TRACKERS = "i2psnark.trackers";
+    private static Map trackerMap = null;
+    /** sorted map of name to announceURL=baseURL */
+    public Map getTrackers() { 
+        if (trackerMap != null) // only do this once, can't be updated while running
+            return trackerMap;
+        Map rv = new TreeMap();
+        String trackers = _config.getProperty(PROP_TRACKERS);
+        if ( (trackers == null) || (trackers.trim().length() <= 0) )
+            trackers = _context.getProperty(PROP_TRACKERS);
+        if ( (trackers == null) || (trackers.trim().length() <= 0) ) {
+            for (int i = 0; i < DEFAULT_TRACKERS.length; i += 2)
+                rv.put(DEFAULT_TRACKERS[i], DEFAULT_TRACKERS[i+1]);
+        } else {
+            StringTokenizer tok = new StringTokenizer(trackers, ",");
+            while (tok.hasMoreTokens()) {
+                String pair = tok.nextToken();
+                int split = pair.indexOf('=');
+                if (split <= 0)
+                    continue;
+                String name = pair.substring(0, split).trim();
+                String url = pair.substring(split+1).trim();
+                if ( (name.length() > 0) && (url.length() > 0) )
+                    rv.put(name, url);
+            }
+        }
+        
+        trackerMap = rv;
+        return trackerMap;
+    }
+    
+    private static class TorrentFilenameFilter implements FilenameFilter {
+        private static final TorrentFilenameFilter _filter = new TorrentFilenameFilter();
+        public static TorrentFilenameFilter instance() { return _filter; }
+        public boolean accept(File dir, String name) {
+            return (name != null) && (name.endsWith(".torrent"));
+        }
+    }
+
+    public class SnarkManagerShutdown extends I2PAppThread {
+        @Override
+        public void run() {
+            Set names = listTorrentFiles();
+            for (Iterator iter = names.iterator(); iter.hasNext(); ) {
+                Snark snark = getTorrent((String)iter.next());
+                if ( (snark != null) && (!snark.stopped) )
+                    snark.stopTorrent();
+            }
+        }
+    }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java
index 065e7088014a7a1b19c489e75ab31fe7ee230528..05df351c69b7c28e93deff8e783e1aa0a537cf21 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java
@@ -357,6 +357,7 @@ public class TrackerClient extends I2PAppThread
       + "&uploaded=" + uploaded
       + "&downloaded=" + downloaded
       + "&left=" + left
+      + "&compact"
       + ((! event.equals(NO_EVENT)) ? ("&event=" + event) : "");
     if (left <= 0 || event.equals(STOPPED_EVENT) || !coordinator.needPeers())
         s += "&numwant=0";
diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java
index 84198f12f0f0eacca955c53cf14a018c91202f1e..360a4f47e416c3ed728523cfcfbd4e5a09ca9b55 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java
@@ -24,7 +24,6 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.util.HashSet;
 import java.util.Collections;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -33,11 +32,17 @@ import org.klomp.snark.bencode.BDecoder;
 import org.klomp.snark.bencode.BEValue;
 import org.klomp.snark.bencode.InvalidBEncodingException;
 
+/**
+ *  The data structure for the tracker response.
+ *  Handles both traditional and compact formats.
+ *  Compact format 1 - a list of hashes - early format for testing
+ *  Compact format 2 - One big string of concatenated hashes - official format
+ */
 public class TrackerInfo
 {
   private final String failure_reason;
   private final int interval;
-  private final Set peers;
+  private final Set<Peer> peers;
   private int complete;
   private int incomplete;
 
@@ -73,10 +78,19 @@ public class TrackerInfo
           interval = beInterval.getInt();
 
         BEValue bePeers = (BEValue)m.get("peers");
-        if (bePeers == null)
+        if (bePeers == null) {
           peers = Collections.EMPTY_SET;
-        else
-          peers = getPeers(bePeers.getList(), my_id, metainfo);
+        } else {
+            Set<Peer> p;
+            try {
+              // One big string (the official compact format)
+              p = getPeers(bePeers.getBytes(), my_id, metainfo);
+            } catch (InvalidBEncodingException ibe) {
+              // List of Dictionaries or List of Strings
+              p = getPeers(bePeers.getList(), my_id, metainfo);
+            }
+            peers = p;
+        }
 
         BEValue bev = (BEValue)m.get("complete");
         if (bev != null) try {
@@ -94,32 +108,68 @@ public class TrackerInfo
       }
   }
 
-  public static Set getPeers(InputStream in, byte[] my_id, MetaInfo metainfo)
+/******
+  public static Set<Peer> getPeers(InputStream in, byte[] my_id, MetaInfo metainfo)
     throws IOException
   {
     return getPeers(new BDecoder(in), my_id, metainfo);
   }
 
-  public static Set getPeers(BDecoder be, byte[] my_id, MetaInfo metainfo)
+  public static Set<Peer> getPeers(BDecoder be, byte[] my_id, MetaInfo metainfo)
     throws IOException
   {
     return getPeers(be.bdecodeList().getList(), my_id, metainfo);
   }
+******/
 
-  public static Set getPeers(List l, byte[] my_id, MetaInfo metainfo)
+  /** List of Dictionaries or List of Strings */
+  private static Set<Peer> getPeers(List<BEValue> l, byte[] my_id, MetaInfo metainfo)
     throws IOException
   {
-    Set peers = new HashSet(l.size());
+    Set<Peer> peers = new HashSet(l.size());
 
-    Iterator it = l.iterator();
-    while (it.hasNext())
-      {
+    for (BEValue bev : l) {
+        PeerID peerID;
+        try {
+            // Case 1 - non-compact - A list of dictionaries (maps)
+            peerID = new PeerID(bev.getMap());
+        } catch (InvalidBEncodingException ibe) {
+            try {
+                // Case 2 - compact - A list of 32-byte binary strings (hashes)
+                // This was just for testing and is not the official format
+                peerID = new PeerID(bev.getBytes());
+            } catch (InvalidBEncodingException ibe2) {
+                // don't let one bad entry spoil the whole list
+                //Snark.debug("Discarding peer from list: " + ibe, Snark.ERROR);
+                continue;
+            }
+        }
+        peers.add(new Peer(peerID, my_id, metainfo));
+      }
+
+    return peers;
+  }
+
+  private static final int HASH_LENGTH = 32;
+
+  /**
+   *  One big string of concatenated 32-byte hashes
+   *  @since 0.8.1
+   */
+  private static Set<Peer> getPeers(byte[] l, byte[] my_id, MetaInfo metainfo)
+    throws IOException
+  {
+    int count = l.length / HASH_LENGTH;
+    Set<Peer> peers = new HashSet(count);
+
+    for (int i = 0; i < count; i++) {
         PeerID peerID;
+        byte[] hash = new byte[HASH_LENGTH];
+        System.arraycopy(l, i * HASH_LENGTH, hash, 0, HASH_LENGTH);
         try {
-            peerID = new PeerID(((BEValue)it.next()).getMap());
+            peerID = new PeerID(hash);
         } catch (InvalidBEncodingException ibe) {
-            // don't let one bad entry spoil the whole list
-            //Snark.debug("Discarding peer from list: " + ibe, Snark.ERROR);
+            // won't happen
             continue;
         }
         peers.add(new Peer(peerID, my_id, metainfo));
@@ -128,7 +178,7 @@ public class TrackerInfo
     return peers;
   }
 
-  public Set getPeers()
+  public Set<Peer> getPeers()
   {
     return peers;
   }
diff --git a/apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java b/apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java
index cba6b973fd2d70f846e32dd690228cdcb6996e03..c1733cb13b2000ddcc62e0fa29cb82a5fcd6bd60 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java
@@ -140,7 +140,7 @@ public class BEValue
    * succeeds when the BEValue is actually a List, otherwise it will
    * throw a InvalidBEncodingException.
    */
-  public List getList() throws InvalidBEncodingException
+  public List<BEValue> getList() throws InvalidBEncodingException
   {
     try
       {
@@ -157,7 +157,7 @@ public class BEValue
    * values. This operation only succeeds when the BEValue is actually
    * a Map, otherwise it will throw a InvalidBEncodingException.
    */
-  public Map getMap() throws InvalidBEncodingException
+  public Map<BEValue, BEValue> getMap() throws InvalidBEncodingException
   {
     try
       {
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 9a291c89f9cc074cfc8461c30924ff9625c22bd5..36266de1be716d90842858e2effab990d5eca4fb 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
@@ -8,6 +8,7 @@ import java.io.PrintWriter;
 import java.text.Collator;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.Enumeration;
 import java.util.Iterator;
@@ -821,10 +822,10 @@ public class I2PSnarkServlet extends Default {
         out.write("</td>\n</tr>\n");
 
         if(showPeers && isRunning && curPeers > 0) {
-            List peers = snark.coordinator.peerList();
-            Iterator it = peers.iterator();
-            while (it.hasNext()) {
-                Peer peer = (Peer)it.next();
+            List<Peer> peers = snark.coordinator.peerList();
+            if (!showDebug)
+                Collections.sort(peers, new PeerComparator());
+            for (Peer peer : peers) {
                 if (!peer.isConnected())
                     continue;
                 out.write("<tr class=\"" + rowClass + "\">");
@@ -908,6 +909,19 @@ public class I2PSnarkServlet extends Default {
         }
     }
     
+    /**
+     *  Sort by completeness (seeds first), then by ID
+     *  @since 0.8.1
+     */
+    private static class PeerComparator implements Comparator<Peer> {
+        public int compare(Peer l, Peer r) {
+            int diff = r.completed() - l.completed();      // reverse
+            if (diff != 0)
+                return diff;
+            return l.toString().substring(5, 9).compareTo(r.toString().substring(5, 9));
+        }
+    }
+
     private void writeAddForm(PrintWriter out, HttpServletRequest req) throws IOException {
         String uri = req.getRequestURI();
         String newURL = req.getParameter("newURL");
diff --git a/apps/i2psnark/web.xml b/apps/i2psnark/web.xml
index 71260245f1d5fd295306f09c5779a7bfe98b8026..2925ff00e3267ce2a2539db7ca0e61ba489a1b46 100644
--- a/apps/i2psnark/web.xml
+++ b/apps/i2psnark/web.xml
@@ -22,4 +22,68 @@
             30
         </session-timeout>
     </session-config>
+
+
+    <!-- mime types not in mime.properties in the jetty 5.1.15 source -->
+
+    <mime-mapping>
+        <extension>mkv</extension>
+        <mime-type>video/x-matroska</mime-type>
+    </mime-mapping>
+
+    <mime-mapping>
+        <extension>wmv</extension>
+        <mime-type>video/x-ms-wmv</mime-type>
+    </mime-mapping>
+
+    <mime-mapping>
+        <extension>flv</extension>
+        <mime-type>video/x-flv</mime-type>
+    </mime-mapping>
+
+    <mime-mapping>
+        <extension>mp4</extension>
+        <mime-type>video/mp4</mime-type>
+    </mime-mapping>
+
+    <mime-mapping>
+        <extension>rar</extension>
+        <mime-type>application/rar</mime-type>
+    </mime-mapping>
+
+    <mime-mapping>
+        <extension>7z</extension>
+        <mime-type>application/x-7z-compressed</mime-type>
+    </mime-mapping>
+
+    <mime-mapping>
+        <extension>iso</extension>
+        <mime-type>application/x-iso9660-image</mime-type>
+    </mime-mapping>
+
+    <mime-mapping>
+        <extension>ico</extension>
+        <mime-type>image/x-icon</mime-type>
+    </mime-mapping>
+
+    <mime-mapping>
+        <extension>exe</extension>
+        <mime-type>application/x-msdos-program</mime-type>
+    </mime-mapping>
+
+    <mime-mapping>
+        <extension>flac</extension>
+        <mime-type>audio/flac</mime-type>
+    </mime-mapping>
+
+    <mime-mapping>
+        <extension>m4a</extension>
+        <mime-type>audio/mpeg</mime-type>
+    </mime-mapping>
+
+    <mime-mapping>
+        <extension>wma</extension>
+        <mime-type>audio/x-ms-wma</mime-type>
+    </mime-mapping>
+
 </web-app>
diff --git a/apps/i2ptunnel/java/build.xml b/apps/i2ptunnel/java/build.xml
index 7b66b6882e624a4f44ca3b04aaf9763f20d378f1..b17e8d9f38901bc3c90ff87546b2eaa352e49bf7 100644
--- a/apps/i2ptunnel/java/build.xml
+++ b/apps/i2ptunnel/java/build.xml
@@ -81,7 +81,7 @@
 
     <target name="war" depends="precompilejsp, bundle"> 
         <war destfile="build/i2ptunnel.war" webxml="../jsp/web-out.xml"
-             basedir="../jsp/" excludes="web.xml, **/*.java, *.jsp">
+             basedir="../jsp/" excludes="web.xml, web-fragment.xml, web-out.xml, **/*.java, *.jsp">
         </war>
     </target>
 
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java
index af4049a21b09183a70f2e84f4d588873e937535a..326ebb644172633fc2a8bcee2bcbbfb1d40f8db9 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java
@@ -24,6 +24,9 @@ import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
 
 /**
+ * This does the transparent gzip decompression on the client side.
+ * Extended in I2PTunnelHTTPServer to do the compression on the server side.
+ *
  * Simple stream for delivering an HTTP response to
  * the client, trivially filtered to make sure "Connection: close"
  * is always in the response.  Perhaps add transparent handling of the
@@ -33,29 +36,27 @@ import net.i2p.util.Log;
  *
  */
 class HTTPResponseOutputStream extends FilterOutputStream {
-    private I2PAppContext _context;
-    private Log _log;
-    private ByteCache _cache;
+    private final I2PAppContext _context;
+    private final Log _log;
     protected ByteArray _headerBuffer;
     private boolean _headerWritten;
-    private byte _buf1[];
+    private final byte _buf1[];
     protected boolean _gzip;
     private long _dataWritten;
     private InternalGZIPInputStream _in;
     private static final int CACHE_SIZE = 8*1024;
+    private static final ByteCache _cache = ByteCache.getInstance(8, CACHE_SIZE);
+    // OOM DOS prevention
+    private static final int MAX_HEADER_SIZE = 64*1024;
     
     public HTTPResponseOutputStream(OutputStream raw) {
         super(raw);
         _context = I2PAppContext.getGlobalContext();
-        _context.statManager().createRateStat("i2ptunnel.httpCompressionRatio", "ratio of compressed size to decompressed size after transfer", "I2PTunnel", new long[] { 60*1000, 30*60*1000 });
-        _context.statManager().createRateStat("i2ptunnel.httpCompressed", "compressed size transferred", "I2PTunnel", new long[] { 60*1000, 30*60*1000 });
-        _context.statManager().createRateStat("i2ptunnel.httpExpanded", "size transferred after expansion", "I2PTunnel", new long[] { 60*1000, 30*60*1000 });
+        _context.statManager().createRateStat("i2ptunnel.httpCompressionRatio", "ratio of compressed size to decompressed size after transfer", "I2PTunnel", new long[] { 60*60*1000 });
+        _context.statManager().createRateStat("i2ptunnel.httpCompressed", "compressed size transferred", "I2PTunnel", new long[] { 60*60*1000 });
+        _context.statManager().createRateStat("i2ptunnel.httpExpanded", "size transferred after expansion", "I2PTunnel", new long[] { 60*60*1000 });
         _log = _context.logManager().getLog(getClass());
-        _cache = ByteCache.getInstance(8, CACHE_SIZE);
         _headerBuffer = _cache.acquire();
-        _headerWritten = false;
-        _gzip = false;
-        _dataWritten = 0;
         _buf1 = new byte[1];
     }
 
@@ -96,14 +97,20 @@ class HTTPResponseOutputStream extends FilterOutputStream {
         }
     }
     
-    /** grow (and free) the buffer as necessary */
-    private void ensureCapacity() {
+    /**
+     *  grow (and free) the buffer as necessary
+     *  @throws IOException if the headers are too big
+     */
+    private void ensureCapacity() throws IOException {
+        if (_headerBuffer.getValid() >= MAX_HEADER_SIZE)
+            throw new IOException("Max header size exceeded: " + MAX_HEADER_SIZE);
         if (_headerBuffer.getValid() + 1 >= _headerBuffer.getData().length) {
             int newSize = (int)(_headerBuffer.getData().length * 1.5);
             ByteArray newBuf = new ByteArray(new byte[newSize]);
             System.arraycopy(_headerBuffer.getData(), 0, newBuf.getData(), 0, _headerBuffer.getValid());
             newBuf.setValid(_headerBuffer.getValid());
             newBuf.setOffset(0);
+            // if we changed the ByteArray size, don't put it back in the cache
             if (_headerBuffer.getData().length == CACHE_SIZE)
                 _cache.release(_headerBuffer);
             _headerBuffer = newBuf;
@@ -219,7 +226,7 @@ class HTTPResponseOutputStream extends FilterOutputStream {
         //out.flush();
         PipedInputStream pi = new PipedInputStream();
         PipedOutputStream po = new PipedOutputStream(pi);
-        new I2PAppThread(new Pusher(pi, out), "HTTP decompresser").start();
+        new I2PAppThread(new Pusher(pi, out), "HTTP decompressor").start();
         out = po;
     }
     
@@ -231,13 +238,13 @@ class HTTPResponseOutputStream extends FilterOutputStream {
             _out = out;
         }
         public void run() {
-            OutputStream to = null;
             _in = null;
-            long start = System.currentTimeMillis();
             long written = 0;
+            ByteArray ba = null;
             try {
                 _in = new InternalGZIPInputStream(_inRaw);
-                byte buf[] = new byte[8192];
+                ba = _cache.acquire();
+                byte buf[] = ba.getData();
                 int read = -1;
                 while ( (read = _in.read(buf)) != -1) {
                     if (_log.shouldLog(Log.DEBUG))
@@ -251,6 +258,8 @@ class HTTPResponseOutputStream extends FilterOutputStream {
             } catch (IOException ioe) {
                 if (_log.shouldLog(Log.WARN))
                     _log.warn("Error decompressing: " + written + ", " + (_in != null ? _in.getTotalRead() + "/" + _in.getTotalExpanded() : ""), ioe);
+            } catch (OutOfMemoryError oom) {
+                _log.error("OOM in HTTP Decompressor", oom);
             } finally {
                 if (_log.shouldLog(Log.WARN) && (_in != null))
                     _log.warn("After decompression, written=" + written + 
@@ -259,23 +268,26 @@ class HTTPResponseOutputStream extends FilterOutputStream {
                                 + ", expanded=" + _in.getTotalExpanded() + ", remaining=" + _in.getRemaining() 
                                 + ", finished=" + _in.getFinished()
                                : ""));
+                if (ba != null)
+                    _cache.release(ba);
                 if (_out != null) try { 
                     _out.close(); 
                 } catch (IOException ioe) {}
             }
-            long end = System.currentTimeMillis();
+
             double compressed = (_in != null ? _in.getTotalRead() : 0);
             double expanded = (_in != null ? _in.getTotalExpanded() : 0);
-            double ratio = 0;
-            if (expanded > 0)
-                ratio = compressed/expanded;
-
-            _context.statManager().addRateData("i2ptunnel.httpCompressionRatio", (int)(100d*ratio), end-start);
-            _context.statManager().addRateData("i2ptunnel.httpCompressed", (long)compressed, end-start);
-            _context.statManager().addRateData("i2ptunnel.httpExpanded", (long)expanded, end-start);
+            if (compressed > 0 && expanded > 0) {
+                // only update the stats if we did something
+                double ratio = compressed/expanded;
+                _context.statManager().addRateData("i2ptunnel.httpCompressionRatio", (int)(100d*ratio), 0);
+                _context.statManager().addRateData("i2ptunnel.httpCompressed", (long)compressed, 0);
+                _context.statManager().addRateData("i2ptunnel.httpExpanded", (long)expanded, 0);
+            }
         }
     }
 
+    /** just a wrapper to provide stats for debugging */
     private static class InternalGZIPInputStream extends GZIPInputStream {
         public InternalGZIPInputStream(InputStream in) throws IOException {
             super(in);
@@ -294,6 +306,12 @@ class HTTPResponseOutputStream extends FilterOutputStream {
                 return 0;
             }
         }
+
+        /**
+         *  From Inflater javadoc:
+         *  Returns the total number of bytes remaining in the input buffer. This can be used to find out
+         *  what bytes still remain in the input buffer after decompression has finished.
+         */
         public long getRemaining() { 
             try {
                 return super.inf.getRemaining(); 
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java
index d5b88d4d3df1341ea6a0868b5111175ac30285ea..435b309479c7b5da628d1a7944bd3038376ab8cc 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java
@@ -8,7 +8,6 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.StringTokenizer;
 
-import net.i2p.I2PAppContext;
 import net.i2p.client.streaming.I2PSocket;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.Destination;
@@ -110,7 +109,7 @@ public class I2PTunnelClient extends I2PTunnelClientBase {
         }
         if (size == 1) // skip the rand in the most common case
             return dests.get(0);
-        int index = I2PAppContext.getGlobalContext().random().nextInt(size);
+        int index = _context.random().nextInt(size);
         return dests.get(index);
     }
 }
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java
index fe92e94b6f443ab39032770ea705152f733aa6a7..d9afdc5f21b732301ffc92d3a3ab07889b6b44f9 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java
@@ -146,7 +146,7 @@ public class I2PTunnelConnectClient extends I2PTunnelClientBase implements Runna
             int size = _proxyList.size();
             if (size <= 0)
                 return null;
-            int index = I2PAppContext.getGlobalContext().random().nextInt(size);
+            int index = _context.random().nextInt(size);
             return _proxyList.get(index);
         }
     }
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java
index b6c72821e5327fdca304fae27f6e4798a9370b57..f34ceb1a59f929130c728669a18e55531c851c81 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java
@@ -205,7 +205,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable
                 l.log("Proxy list is empty - no outproxy available");
                 return null;
             }
-            int index = I2PAppContext.getGlobalContext().random().nextInt(size);
+            int index = _context.random().nextInt(size);
             String proxy = (String)proxyList.get(index);
             return proxy;
         }
@@ -626,6 +626,14 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable
                         //line = "From: i2p";
                         line = null;
                         continue; // completely strip the line
+                    } else if (lowercaseLine.startsWith("authorization: ntlm ")) {
+                        // Block Windows NTLM after 401
+                        line = null;
+                        continue;
+                    } else if (lowercaseLine.startsWith("proxy-authorization: ntlm ")) {
+                        // Block Windows NTLM after 407
+                        line = null;
+                        continue;
                     }
                 }
 
@@ -808,7 +816,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable
      *  @return non-null
      */
     private byte[] getErrorPage(String base, byte[] backup) {
-        return getErrorPage(getTunnel().getContext(), base, backup);
+        return getErrorPage(_context, base, backup);
     }
 
     private static byte[] getErrorPage(I2PAppContext ctx, String base, byte[] backup) {
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java
index 728d3537c62ba284a99bccd0e4048a591c9224aa..ca568ab9260e2838fc1dc8326d1c606ecc68a5e8 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java
@@ -290,6 +290,7 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
         }
     }
 
+    /** just a wrapper to provide stats for debugging */
     private static class InternalGZIPOutputStream extends GZIPOutputStream {
         public InternalGZIPOutputStream(OutputStream target) throws IOException {
             super(target);
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java
index 4d8b85a8177e002b8356a71448f1eed7c7cf6274..d0dc227ec1da09fde27392cfd4caab52ac4ee0a0 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java
@@ -9,7 +9,6 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.StringTokenizer;
 
-import net.i2p.I2PAppContext;
 import net.i2p.client.streaming.I2PSocket;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.Destination;
@@ -124,7 +123,7 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable
         }
         if (size == 1) // skip the rand in the most common case
             return dests.get(0);
-        int index = I2PAppContext.getGlobalContext().random().nextInt(size);
+        int index = _context.random().nextInt(size);
         return dests.get(index);
     }
 
@@ -182,6 +181,8 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable
                             }
                             outmsg=outmsg+"\r\n";   // rfc1459 sec. 2.3
                             output.write(outmsg.getBytes("ISO-8859-1"));
+                            // probably doesn't do much but can't hurt
+                            output.flush();
                         } else {
                             if (_log.shouldLog(Log.WARN))
                                 _log.warn("inbound BLOCKED: "+inmsg);
@@ -257,6 +258,8 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable
                                 }
                                 outmsg=outmsg+"\r\n";   // rfc1459 sec. 2.3
                                 output.write(outmsg.getBytes("ISO-8859-1"));
+                                // save 250 ms in streaming
+                                output.flush();
                             } else {
                                 if (_log.shouldLog(Log.WARN))
                                     _log.warn("outbound BLOCKED: "+"\""+inmsg+"\"");
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java
index c2a014020d2df2c74d34c1422bf0d9249afb19fd..0e1c9049f84b8ea5a9c57f314a16bf7b5b150176 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java
@@ -129,7 +129,14 @@ public class I2PTunnelRunner extends I2PAppThread implements I2PSocket.SocketErr
                     // do NOT flush here, it will block and then onTimeout.run() won't happen on fail.
                     // But if we don't flush, then we have to wait for the connectDelay timer to fire
                     // in i2p socket? To be researched and/or fixed.
-                    //i2pout.flush();
+                    //
+                    // AS OF 0.8.1, MessageOutputStream.flush() is fixed to only wait for accept,
+                    // not for "completion" (i.e. an ACK from the far end).
+                    // So we now get a fast return from flush(), and can do it here to save 250 ms.
+                    // To make sure we are under the initial window size and don't hang waiting for accept,
+                    // only flush if it fits in one message.
+                    if (initialI2PData.length <= 1730)   // ConnectionOptions.DEFAULT_MAX_MESSAGE_SIZE
+                        i2pout.flush();
                 }
             }
             if (initialSocketData != null) {
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java
index f0470c5a30e8a9e643d758cd48e435ddd7350634..02777dafbddd625a9876d83cff1a3ce65f8c8b75 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java
@@ -18,6 +18,7 @@ import net.i2p.data.Base32;
 import net.i2p.data.Destination;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
+import net.i2p.util.SecureFileOutputStream;
 
 /**
  * Coordinate the runtime operation and configuration of a tunnel.  
@@ -89,7 +90,7 @@ public class TunnelController implements Logging {
         }
         FileOutputStream fos = null;
         try {
-            fos = new FileOutputStream(keyFile);
+            fos = new SecureFileOutputStream(keyFile);
             Destination dest = client.createDestination(fos);
             String destStr = dest.toBase64();
             log("Private key created and saved in " + keyFile.getAbsolutePath());
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java
index 169f56954eb25d9cecef84dd532cf5ae84e7a725..618b035f7b68cf8bb7838cbb67f897d116c902d8 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java
@@ -20,6 +20,7 @@ import net.i2p.client.I2PSessionException;
 import net.i2p.data.DataHelper;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
+import net.i2p.util.SecureFileOutputStream;
 
 /**
  * Coordinate a set of tunnels within the JVM, loading and storing their config
@@ -255,7 +256,7 @@ public class TunnelControllerGroup {
         
         FileOutputStream fos = null;
         try {
-            fos = new FileOutputStream(cfgFile);
+            fos = new SecureFileOutputStream(cfgFile);
             fos.write(buf.toString().getBytes("UTF-8"));
             if (_log.shouldLog(Log.INFO))
                 _log.info("Config written to " + cfgFile.getPath());
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java
index 38bc8b4db9483ab53ac74b0df41166e6d50fbb3c..f6c76d7f6066a88b08a181ffe21936032feb939b 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java
@@ -13,6 +13,11 @@ import java.util.List;
 import java.util.Properties;
 import java.util.StringTokenizer;
 
+import net.i2p.data.Base64;
+import net.i2p.data.Destination;
+import net.i2p.data.PrivateKeyFile;
+import net.i2p.data.Signature;
+import net.i2p.data.SigningPrivateKey;
 import net.i2p.i2ptunnel.TunnelController;
 import net.i2p.i2ptunnel.TunnelControllerGroup;
 
@@ -68,6 +73,31 @@ public class EditBean extends IndexBean {
         return "i2ptunnel" + tunnel + "-privKeys.dat";
     }
     
+    public String getNameSignature(int tunnel) {
+        String spoof = getSpoofedHost(tunnel);
+        if (spoof.length() <= 0)
+            return "";
+        TunnelController tun = getController(tunnel);
+        if (tun == null)
+            return "";
+        String keyFile = tun.getPrivKeyFile();
+        if (keyFile != null && keyFile.trim().length() > 0) {
+            PrivateKeyFile pkf = new PrivateKeyFile(keyFile);
+            try {
+                Destination d = pkf.getDestination();
+                if (d == null)
+                    return "";
+                SigningPrivateKey privKey = pkf.getSigningPrivKey();
+                if (privKey == null)
+                    return "";
+                //System.err.println("Signing " + spoof + " with " + Base64.encode(privKey.getData()));
+                Signature sig = _context.dsa().sign(spoof.getBytes("UTF-8"), privKey);
+                return Base64.encode(sig.getData());
+            } catch (Exception e) {}
+        }
+        return "";
+    }
+    
     public boolean startAutomatically(int tunnel) {
         TunnelController tun = getController(tunnel);
         if (tun != null)
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
index 2a77b9be8ad3825bd1a73ae6fd52ed7137eb1cf8..d91a7900d793d29245a5e66ab0d676eadf079203 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
@@ -9,6 +9,7 @@ package net.i2p.i2ptunnel.web;
  */
 
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
@@ -41,10 +42,10 @@ public class IndexBean {
     protected TunnelControllerGroup _group;
     private String _action;
     private int _tunnel;
-    private long _prevNonce;
-    private long _prevNonce2;
-    private long _curNonce;
-    private long _nextNonce;
+    //private long _prevNonce;
+    //private long _prevNonce2;
+    private String _curNonce;
+    //private long _nextNonce;
 
     private String _type;
     private String _name;
@@ -85,10 +86,14 @@ public class IndexBean {
     /** deprecated unimplemented, now using routerconsole realm */
     //public static final String PROP_TUNNEL_PASSPHRASE = "i2ptunnel.passphrase";
     public static final String PROP_TUNNEL_PASSPHRASE = "consolePassword";
-    static final String PROP_NONCE = IndexBean.class.getName() + ".nonce";
-    static final String PROP_NONCE_OLD = PROP_NONCE + '2';
+    //static final String PROP_NONCE = IndexBean.class.getName() + ".nonce";
+    //static final String PROP_NONCE_OLD = PROP_NONCE + '2';
+    /** 3 wasn't enough for some browsers. They are reloading the page for some reason - maybe HEAD? @since 0.8.1 */
+    private static final int MAX_NONCES = 5;
+    /** store nonces in a static FIFO instead of in System Properties @since 0.8.1 */
+    private static final List<String> _nonces = new ArrayList(MAX_NONCES + 1);
+
     static final String CLIENT_NICKNAME = "shared clients";
-    
     public static final String PROP_THEME_NAME = "routerconsole.theme";
     public static final String DEFAULT_THEME = "light";
     public static final String PROP_CSS_DISABLED = "routerconsole.css.disabled";
@@ -98,34 +103,39 @@ public class IndexBean {
         _context = I2PAppContext.getGlobalContext();
         _log = _context.logManager().getLog(IndexBean.class);
         _group = TunnelControllerGroup.getInstance();
-        _action = null;
         _tunnel = -1;
-        _curNonce = -1;
-        _prevNonce = -1;
-        _prevNonce2 = -1;
-        try { 
-            String nonce2 = System.getProperty(PROP_NONCE_OLD);
-            if (nonce2 != null)
-                _prevNonce2 = Long.parseLong(nonce2);
-            String nonce = System.getProperty(PROP_NONCE);
-            if (nonce != null) {
-                _prevNonce = Long.parseLong(nonce);
-                System.setProperty(PROP_NONCE_OLD, nonce);
-            }
-        } catch (NumberFormatException nfe) {}
-        _nextNonce = _context.random().nextLong();
-        System.setProperty(PROP_NONCE, Long.toString(_nextNonce));
+        _curNonce = "-1";
+        addNonce();
         _booleanOptions = new ConcurrentHashSet(4);
         _otherOptions = new ConcurrentHashMap(4);
     }
     
-    public long getNextNonce() { return _nextNonce; }
+    public static String getNextNonce() {
+        synchronized (_nonces) {
+            return _nonces.get(0);
+        }
+    }
+
     public void setNonce(String nonce) {
         if ( (nonce == null) || (nonce.trim().length() <= 0) ) return;
-        try {
-            _curNonce = Long.parseLong(nonce);
-        } catch (NumberFormatException nfe) {
-            _curNonce = -1;
+        _curNonce = nonce;
+    }
+
+    /** add a random nonce to the head of the queue @since 0.8.1 */
+    private void addNonce() {
+        String nextNonce = Long.toString(_context.random().nextLong());
+        synchronized (_nonces) {
+            _nonces.add(0, nextNonce);
+            int sz = _nonces.size();
+            if (sz > MAX_NONCES)
+                _nonces.remove(sz - 1);
+        }
+    }
+
+    /** do we know this nonce? @since 0.8.1 */
+    private static boolean haveNonce(String nonce) {
+        synchronized (_nonces) {
+            return _nonces.contains(nonce);
         }
     }
 
@@ -155,7 +165,7 @@ public class IndexBean {
     private String processAction() {
         if ( (_action == null) || (_action.trim().length() <= 0) || ("Cancel".equals(_action)))
             return "";
-        if ( (_prevNonce != _curNonce) && (_prevNonce2 != _curNonce) && (!validPassphrase()) )
+        if ( (!haveNonce(_curNonce)) && (!validPassphrase()) )
             return "Invalid form submission, probably because you used the 'back' or 'reload' button on your browser. Please resubmit.";
         if ("Stop all".equals(_action)) 
             return stopAll();
diff --git a/apps/i2ptunnel/jsp/editServer.jsp b/apps/i2ptunnel/jsp/editServer.jsp
index 939ccf52079d490516b75e2215a484df20459b64..97a0a93c9c7eead98b070b8ccbfb2d57a6a57ac4 100644
--- a/apps/i2ptunnel/jsp/editServer.jsp
+++ b/apps/i2ptunnel/jsp/editServer.jsp
@@ -196,7 +196,16 @@
             <a href="/susidns/addressbook.jsp?book=private&hostname=<%=editBean.getTunnelName(curTunnel)%>&destination=<%=editBean.getDestinationBase64(curTunnel)%>#add"><%=intl._("Add to local addressbook")%></a>    
          <% } %>
             </div>
-            
+
+            <% if (("httpserver".equals(tunnelType)) || ("httpbidirserver".equals(tunnelType))) {
+          %><div id="sigField" class="rowItem">
+                <label for="signature">
+                    <%=intl._("Hostname Signature")%>
+                </label>
+                <input type="text" size="30" readonly="readonly" title="Use to prove that the website name is for this destination" value="<%=editBean.getNameSignature(curTunnel)%>" wrap="off" class="freetext" />                
+            </div>
+            <% } %>
+
             <div class="footer">
             </div>
         </div>
diff --git a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManagerFactory.java b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManagerFactory.java
index 393cf1ae216690d37b9903b1772607fb2ec7a902..34dc8ac593d0d1c33c9202d71a1eb0f7a354b5e8 100644
--- a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManagerFactory.java
+++ b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManagerFactory.java
@@ -130,16 +130,19 @@ public class I2PSocketManagerFactory {
             if (!opts.containsKey(name))
                 opts.setProperty(name, System.getProperty(name));
         }
-        boolean oldLib = DEFAULT_MANAGER.equals(opts.getProperty(PROP_MANAGER, DEFAULT_MANAGER));
-        if (oldLib && false) {
+        //boolean oldLib = DEFAULT_MANAGER.equals(opts.getProperty(PROP_MANAGER, DEFAULT_MANAGER));
+        //if (oldLib && false) {
             // for the old streaming lib
-            opts.setProperty(I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_GUARANTEED);
+        //    opts.setProperty(I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_GUARANTEED);
             //opts.setProperty("tunnels.depthInbound", "0");
-        } else {
+        //} else {
             // for new streaming lib:
-            opts.setProperty(I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_BEST_EFFORT);
+            //opts.setProperty(I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_BEST_EFFORT);
+            // as of 0.8.1 (I2CP default is BestEffort)
+            if (!opts.containsKey(I2PClient.PROP_RELIABILITY))
+                opts.setProperty(I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_NONE);
             //p.setProperty("tunnels.depthInbound", "0");
-        }
+        //}
 
         if (i2cpHost != null)
             opts.setProperty(I2PClient.PROP_TCP_HOST, i2cpHost);
diff --git a/apps/routerconsole/java/build.xml b/apps/routerconsole/java/build.xml
index 78384113f9d8023b5ab9b7e72bfce5e6f799ecd4..ae73734cd564f674899d42c9f141ad82ccfe4d72 100644
--- a/apps/routerconsole/java/build.xml
+++ b/apps/routerconsole/java/build.xml
@@ -113,7 +113,7 @@
     <target name="war" depends="precompilejsp">
         <!-- Don't include the css in the war, the main build.xml will copy it to docs/themes/console/ -->
         <war destfile="build/routerconsole.war" webxml="../jsp/web-out.xml"
-             basedir="../jsp/" excludes="web.xml, *.css, **/*.java, *.jsp, *.jsi, web-fragment.xml">
+             basedir="../jsp/" excludes="web.xml, *.css, **/*.java, *.jsp, *.jsi, web-fragment.xml, web-out.xml">
         </war>
     </target>
     <target name="precompilejsp" unless="precompilejsp.uptodate">
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHandler.java
index 347cfdab7f654cfe198d150d2d6fa3b273b4b34d..69013f60a469e9e2d2ec367ce1c1f0f5502e478d 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHandler.java
@@ -17,6 +17,8 @@ public class ConfigLoggingHandler extends FormHandler {
     private String _recordFormat;
     private String _dateFormat;
     private String _fileSize;
+    private String _newLogClass;
+    private String _newLogLevel = "WARN";
     
     @Override
     protected void processForm() {
@@ -26,7 +28,7 @@ public class ConfigLoggingHandler extends FormHandler {
             // noop
         }
     }
-    
+
     public void setShouldsave(String moo) { _shouldSave = true; }
     
     public void setLevels(String levels) {
@@ -47,6 +49,18 @@ public class ConfigLoggingHandler extends FormHandler {
     public void setLogfilesize(String size) {
         _fileSize = (size != null ? size.trim() : null);
     }
+
+    /** @since 0.8.1 */
+    public void setNewlogclass(String s) {
+        if (s != null && s.length() > 0)
+            _newLogClass = s;
+    }
+    
+    /** @since 0.8.1 */
+    public void setNewloglevel(String s) {
+        if (s != null)
+            _newLogLevel = s;
+    }
     
     /**
      * The user made changes to the config and wants to save them, so
@@ -56,14 +70,18 @@ public class ConfigLoggingHandler extends FormHandler {
     private void saveChanges() {
         boolean shouldSave = false;
         
-        if (_levels != null) {
+        if (_levels != null || _newLogClass != null) {
             try {
                 Properties props = new Properties();
-                props.load(new ByteArrayInputStream(_levels.getBytes()));
+                if (_levels != null)
+                    props.load(new ByteArrayInputStream(_levels.getBytes()));
+                if (_newLogClass != null)
+                    props.setProperty(_newLogClass, _newLogLevel);
                 _context.logManager().setLimits(props);
                 shouldSave = true;
-                addFormNotice("Log limits updated");
+                addFormNotice(_("Log overrides updated"));
             } catch (IOException ioe) {
+                // shouldn't ever happen (BAIS shouldnt cause an IOE)
                 _context.logManager().getLog(ConfigLoggingHandler.class).error("Error reading from the props?", ioe);
                 addFormError("Error updating the log limits - levels not valid");
             }
@@ -83,7 +101,7 @@ public class ConfigLoggingHandler extends FormHandler {
             }
         }
         
-        if (_dateFormat != null) {
+        if (_dateFormat != null && !_dateFormat.equals(_context.logManager().getDateFormatPattern())) {
             boolean valid = _context.logManager().setDateFormat(_dateFormat);
             if (valid) {
                 shouldSave = true;
@@ -139,7 +157,7 @@ public class ConfigLoggingHandler extends FormHandler {
             boolean saved = _context.logManager().saveConfig();
 
             if (saved) 
-                addFormNotice("Log configuration saved and applied successfully");
+                addFormNotice(_("Log configuration saved"));
             else
                 addFormNotice("Error saving the configuration (applied but not saved) - please see the error logs");
         }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHelper.java
index bde9b08930d2a5b83014a2053b244acd7a30f693..ce41b1e12e189f987ac9f2ce6b8a7c97cd2892e7 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHelper.java
@@ -1,9 +1,13 @@
 package net.i2p.router.web;
 
+import java.util.List;
 import java.util.Iterator;
 import java.util.Properties;
+import java.util.Set;
 import java.util.TreeSet;
 
+import net.i2p.data.DataHelper;
+import net.i2p.util.Log;
 
 public class ConfigLoggingHelper extends HelperBase {
     public ConfigLoggingHelper() {}
@@ -19,18 +23,13 @@ public class ConfigLoggingHelper extends HelperBase {
     }
     public String getMaxFileSize() {
         int bytes = _context.logManager().getFileSize();
-        if (bytes == 0) return "1m";
-        if (bytes > 1024*1024*1024)
-            return (bytes/(1024*1024*1024)) + "g";
-        else if (bytes > 1024*1024)
-            return (bytes/(1024*1024)) + "m";
-        else
-            return (bytes/(1024)) + "k";
+        if (bytes <= 0) return "1.00 MB";
+        return DataHelper.formatSize2(bytes) + 'B';
     }
     public String getLogLevelTable() {
         StringBuilder buf = new StringBuilder(32*1024);
         Properties limits = _context.logManager().getLimits();
-        TreeSet sortedLogs = new TreeSet();
+        TreeSet<String> sortedLogs = new TreeSet();
         for (Iterator iter = limits.keySet().iterator(); iter.hasNext(); ) {
             String prefix = (String)iter.next();
             sortedLogs.add(prefix);
@@ -46,6 +45,20 @@ public class ConfigLoggingHelper extends HelperBase {
         buf.append("<i>" + _("Add additional logging statements above. Example: net.i2p.router.tunnel=WARN") + "</i><br>");
         buf.append("<i>" + _("Or put entries in the logger.config file. Example: logger.record.net.i2p.router.tunnel=WARN") + "</i><br>");
         buf.append("<i>" + _("Valid levels are DEBUG, INFO, WARN, ERROR, CRIT") + "</i>\n");
+
+      /****
+        // this is too big and ugly
+        if (limits.size() <= 0)
+            return "";
+        buf.append("<table>");
+        for (String prefix : sortedLogs) {
+            buf.append("<tr><td>").append(prefix).append("</td><td>");
+            String level = limits.getProperty(prefix);
+            buf.append(getLogLevelBox("level-" + prefix, level, true)).append("</td></tr>");
+        }
+        buf.append("</table>");
+       ****/
+
         return buf.toString();
     }
 
@@ -53,18 +66,69 @@ public class ConfigLoggingHelper extends HelperBase {
 
     public String getDefaultLogLevelBox() {
         String cur = _context.logManager().getDefaultLimit();
+        return getLogLevelBox("defaultloglevel", cur, false);
+    }
+
+    private String getLogLevelBox(String name, String cur, boolean showRemove) {
         StringBuilder buf = new StringBuilder(128);
-        buf.append("<select name=\"defaultloglevel\">\n");
+        buf.append("<select name=\"").append(name).append("\">\n");
         
         for (int i = 0; i < levels.length; i++) {
             String l = levels[i];
-            buf.append("<option value=\"").append(l).append("\" ");
+            buf.append("<option value=\"").append(l).append('\"');
             if (l.equals(cur))
-                buf.append(" selected=\"true\" ");
+                buf.append(" selected=\"true\"");
             buf.append('>').append(_(l)).append("</option>\n");
         }        
         
+        if (showRemove)
+            buf.append("<option value=\"remove\">").append(_("Remove")).append("</option>");
+        buf.append("</select>\n");
+        return buf.toString();
+    }
+
+    /**
+     *  All the classes the log manager knows about, except ones that
+     *  already have overrides
+     *  @since 0.8.1
+     */
+    public String getNewClassBox() {
+        List<Log> logs = _context.logManager().getLogs();
+        Set limits = _context.logManager().getLimits().keySet();
+        TreeSet<String> sortedLogs = new TreeSet();
+
+        for (Log log : logs) {
+            String name = log.getName();
+            if (!limits.contains(name))
+                sortedLogs.add(name);
+
+            // add higher classes of length 3 or more
+            int dots = 0;
+            int lastdot = -1;
+            int nextdot = 0;
+            while ((nextdot = name.indexOf('.', lastdot + 1)) > 0) {
+                if (++dots >= 3) {
+                    String subst = name.substring(0, nextdot);
+                    if (!limits.contains(subst))
+                        sortedLogs.add(subst);
+                }
+                lastdot = nextdot;
+            }
+        }
+
+        StringBuilder buf = new StringBuilder(65536);
+        buf.append("<select name=\"newlogclass\">\n" +
+                   "<option value=\"\" selected=\"true\">")
+           .append(_("Select a class to add"))
+           .append("</option>\n");
+
+        for (String l : sortedLogs) {
+            buf.append("<option value=\"").append(l).append("\">")
+               .append(l).append("</option>\n");
+        }        
+        
         buf.append("</select>\n");
+        buf.append(getLogLevelBox("newloglevel", "WARN", false));
         return buf.toString();
     }
 }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java
index d6eb2edd1c8745de06f11351ab949c5d93253663..f499b2ea43ac0b185ff949950143937922a201a3 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java
@@ -44,13 +44,16 @@ public class LogsHelper extends HelperBase {
     }
     ******/
 
-    private String formatMessages(List msgs) {
+    /** formats in reverse order */
+    private String formatMessages(List<String> msgs) {
+        if (msgs.isEmpty())
+            return "<p><i>" + _("No log messages") + "</i></p>";
         boolean colorize = Boolean.valueOf(_context.getProperty("routerconsole.logs.color")).booleanValue();
         StringBuilder buf = new StringBuilder(16*1024); 
         buf.append("<ul>");
         buf.append("<code>\n");
         for (int i = msgs.size(); i > 0; i--) { 
-            String msg = (String)msgs.get(i - 1);
+            String msg = msgs.get(i - 1);
             buf.append("<li>");
             if (colorize) {
                 String color;
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java
index 244cc13a0f80e94009eb5a1de2a7c5b35914e4b8..a2c5632fa8e0e4d57e750d3f60447b3469f6d605 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java
@@ -19,6 +19,8 @@ import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
 import java.util.Set;
 import java.util.TreeSet;
 
@@ -34,6 +36,7 @@ import net.i2p.router.TunnelPoolSettings;
 import net.i2p.router.networkdb.kademlia.HashDistance;   // debug
 import net.i2p.util.HexDump;                             // debug
 import net.i2p.util.ObjectCounter;
+import net.i2p.util.OrderedProperties;
 import net.i2p.util.VersionComparator;
 
 public class NetDbRenderer {
@@ -371,9 +374,11 @@ public class NetDbRenderer {
             int cost = addr.getCost();
             if (!((style.equals("SSU") && cost == 5) || (style.equals("NTCP") && cost == 10)))
                 buf.append('[').append(_("cost")).append('=').append("" + cost).append("] ");
-            for (Iterator optIter = addr.getOptions().keySet().iterator(); optIter.hasNext(); ) {
-                String name = (String)optIter.next();
-                String val = addr.getOptions().getProperty(name);
+            Properties p = new OrderedProperties();
+            p.putAll(addr.getOptions());
+            for (Map.Entry e : p.entrySet()) {
+                String name = (String) e.getKey();
+                String val = (String) e.getValue();
                 buf.append('[').append(_(DataHelper.stripHTML(name))).append('=').append(DataHelper.stripHTML(val)).append("] ");
             }
         }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java
index b17dfbdade4264f1b98a579c86586e9404b44288..6958b350e9b2f2ccaf2e4126758ab75137f659af 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java
@@ -563,7 +563,7 @@ public class PluginStarter implements Runnable {
     /**
      *  http://jimlife.wordpress.com/2007/12/19/java-adding-new-classpath-at-runtime/
      */
-    public static void addPath(URL u) throws Exception {
+    private static void addPath(URL u) throws Exception {
         URLClassLoader urlClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
         Class urlClass = URLClassLoader.class;
         Method method = urlClass.getDeclaredMethod("addURL", new Class[]{URL.class});
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java
index 26d026a1694eb8bb62ced52827a31fb4b82c989f..38e8f28b00ef014aa859c54b13d40e39b0be6e13 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java
@@ -16,6 +16,7 @@ import net.i2p.util.FileUtil;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
 import net.i2p.util.OrderedProperties;
+import net.i2p.util.SecureDirectory;
 import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
 import net.i2p.util.VersionComparator;
@@ -150,7 +151,7 @@ public class PluginUpdateHandler extends UpdateHandler {
         public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
             updateStatus("<b>" + _("Plugin downloaded") + "</b>");
             File f = new File(_updateFile);
-            File appDir = new File(_context.getConfigDir(), PLUGIN_DIR);
+            File appDir = new SecureDirectory(_context.getConfigDir(), PLUGIN_DIR);
             if ((!appDir.exists()) && (!appDir.mkdir())) {
                 f.delete();
                 statusDone("<b>" + _("Cannot create plugin directory {0}", appDir.getAbsolutePath()) + "</b>");
@@ -273,7 +274,7 @@ public class PluginUpdateHandler extends UpdateHandler {
                 return;
             }
 
-            File destDir = new File(appDir, appName);
+            File destDir = new SecureDirectory(appDir, appName);
             if (destDir.exists()) {
                 if (Boolean.valueOf(props.getProperty("install-only")).booleanValue()) {
                     to.delete();
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 05bedee374fed8b931d06f3811739f9445e03bfa..68533f7ef63c19cfce8b1b4a7c384b4b90f1b309 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
@@ -14,6 +14,7 @@ import net.i2p.data.DataHelper;
 import net.i2p.router.RouterContext;
 import net.i2p.util.FileUtil;
 import net.i2p.util.I2PAppThread;
+import net.i2p.util.SecureDirectory;
 
 import org.mortbay.http.DigestAuthenticator;
 import org.mortbay.http.HashUserRealm;
@@ -62,7 +63,7 @@ public class RouterConsoleRunner {
     }
     
     public void startConsole() {
-        File workDir = new File(I2PAppContext.getGlobalContext().getTempDir(), "jetty-work");
+        File workDir = new SecureDirectory(I2PAppContext.getGlobalContext().getTempDir(), "jetty-work");
         boolean workDirRemoved = FileUtil.rmdir(workDir, false);
         if (!workDirRemoved)
             System.err.println("ERROR: Unable to remove Jetty temporary work directory");
@@ -115,7 +116,7 @@ public class RouterConsoleRunner {
             }
             _server.setRootWebApp(ROUTERCONSOLE);
             WebApplicationContext wac = _server.addWebApplication("/", _webAppsDir + ROUTERCONSOLE + ".war");
-            File tmpdir = new File(workDir, ROUTERCONSOLE + "-" + _listenPort);
+            File tmpdir = new SecureDirectory(workDir, ROUTERCONSOLE + "-" + _listenPort);
             tmpdir.mkdir();
             wac.setTempDirectory(tmpdir);
             baseHandler = new LocaleWebAppHandler(I2PAppContext.getGlobalContext());
@@ -130,7 +131,7 @@ public class RouterConsoleRunner {
                         String enabled = props.getProperty(PREFIX + appName + ENABLED);
                         if (! "false".equals(enabled)) {
                             String path = new File(dir, fileNames[i]).getCanonicalPath();
-                            tmpdir = new File(workDir, appName + "-" + _listenPort);
+                            tmpdir = new SecureDirectory(workDir, appName + "-" + _listenPort);
                             WebAppStarter.addWebApp(I2PAppContext.getGlobalContext(), _server, appName, path, tmpdir);
 
                             if (enabled == null) {
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/WebAppStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/WebAppStarter.java
index 0264d6307aa782147051ee26a3d7682e4850114d..1aac8ecc9fe7e3f70be3391c108a7c34666e7809 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/WebAppStarter.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/WebAppStarter.java
@@ -10,6 +10,7 @@ import java.util.concurrent.ConcurrentHashMap;
 
 import net.i2p.I2PAppContext;
 import net.i2p.util.FileUtil;
+import net.i2p.util.SecureDirectory;
 
 import org.mortbay.http.HttpContext;
 import org.mortbay.http.HttpListener;
@@ -41,7 +42,7 @@ public class WebAppStarter {
      *  @throws just about anything, caller would be wise to catch Throwable
      */
     static void startWebApp(I2PAppContext ctx, Server server, String appName, String warPath) throws Exception {
-         File tmpdir = new File(ctx.getTempDir(), "jetty-work-" + appName + ctx.random().nextInt());
+         File tmpdir = new SecureDirectory(ctx.getTempDir(), "jetty-work-" + appName + ctx.random().nextInt());
          WebApplicationContext wac = addWebApp(ctx, server, appName, warPath, tmpdir);
          wac.start();
     }
@@ -73,7 +74,7 @@ public class WebAppStarter {
             warModTimes.put(warPath, new Long(newmod));
         } else if (oldmod.longValue() < newmod) {
             // copy war to temporary directory
-            File warTmpDir = new File(ctx.getTempDir(), "war-copy-" + appName + ctx.random().nextInt());
+            File warTmpDir = new SecureDirectory(ctx.getTempDir(), "war-copy-" + appName + ctx.random().nextInt());
             warTmpDir.mkdir();
             String tmpPath = (new File(warTmpDir, appName + ".war")).getAbsolutePath();
             if (!FileUtil.copy(warPath, tmpPath, true))
diff --git a/apps/routerconsole/jsp/configlogging.jsp b/apps/routerconsole/jsp/configlogging.jsp
index 3de8d95beea1ea5dae27fb6f555f55ca511bfc2d..ca30423dc211da59d53d88a34025bfe87b946ba6 100644
--- a/apps/routerconsole/jsp/configlogging.jsp
+++ b/apps/routerconsole/jsp/configlogging.jsp
@@ -46,6 +46,8 @@
           </i></td>
         </tr><tr><td class="mediumtags" align="right"><b><%=intl._("Log level overrides")%>:</b></td>
           <td><jsp:getProperty name="logginghelper" property="logLevelTable" /></td>
+        </tr><tr><td class="mediumtags" align="right"><b><%=intl._("New override")%>:</b></td>
+          <td><jsp:getProperty name="logginghelper" property="newClassBox" /></td>
         </tr><tr><td colspan="2"><hr></td>
         </tr><tr class="tablefooter"><td colspan="2"> <div class="formaction">
           <input type="reset" value="<%=intl._("Cancel")%>" >
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java
index bc8c923699cf61f225625981264630d36dd046e8..e662ecc60a95174749196a406bf4636b2cfe53aa 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java
@@ -148,16 +148,21 @@ public class Connection {
     }
     
     /**
+     * This doesn't "send a choke". Rather, it blocks if the outbound window is full,
+     * thus choking the sender that calls this.
+     *
      * Block until there is an open outbound packet slot or the write timeout 
      * expires.  
+     * PacketLocal is the only caller, generally with -1.
      *
-     * @param timeoutMs PacketLocal is the only caller, often with -1??????
-     * @return true if the packet should be sent
+     * @param timeoutMs 0 or negative means wait forever, 5 minutes max
+     * @return true if the packet should be sent, false for a fatal error
+     *         will return false after 5 minutes even if timeoutMs is <= 0.
      */
     boolean packetSendChoke(long timeoutMs) {
         // if (false) return true; // <--- what the fuck??
         long start = _context.clock().now();
-        long writeExpire = start + timeoutMs;
+        long writeExpire = start + timeoutMs;  // only used if timeoutMs > 0
         boolean started = false;
         while (true) {
             long timeLeft = writeExpire - _context.clock().now();
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionOptions.java b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionOptions.java
index 627113abecbe3d78383bb673009e5fbb61404016..c6596b7c93b738167447ec5c3c7a0957d22274e9 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionOptions.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionOptions.java
@@ -646,6 +646,7 @@ public class ConnectionOptions extends I2PSocketOptionsImpl {
         return Boolean.valueOf(val).booleanValue();
     }
 
+/****
     public static void main(String args[]) {
         Properties p = new Properties();
         
@@ -656,4 +657,5 @@ public class ConnectionOptions extends I2PSocketOptionsImpl {
         c = new ConnectionOptions(new I2PSocketOptionsImpl(p));
         System.out.println("opts: " + c);
     }
+****/
 }
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/MessageInputStream.java b/apps/streaming/java/src/net/i2p/client/streaming/MessageInputStream.java
index 14b304b2688dfcb0e00b7d75f703476294c199d0..5a87d6be1490f9df0923925150d4fc589860cbdb 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/MessageInputStream.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/MessageInputStream.java
@@ -11,7 +11,7 @@ import java.util.Map;
 
 import net.i2p.I2PAppContext;
 import net.i2p.data.ByteArray;
-import net.i2p.util.ByteCache;
+//import net.i2p.util.ByteCache;
 import net.i2p.util.Log;
 
 /**
@@ -20,8 +20,8 @@ import net.i2p.util.Log;
  *
  */
 public class MessageInputStream extends InputStream {
-    private I2PAppContext _context;
-    private Log _log;
+    private final I2PAppContext _context;
+    private final Log _log;
     /** 
      * List of ByteArray objects of data ready to be read,
      * with the first ByteArray at index 0, and the next
@@ -29,7 +29,7 @@ public class MessageInputStream extends InputStream {
      * that array.
      *
      */
-    private List _readyDataBlocks;
+    private final List<ByteArray> _readyDataBlocks;
     private int _readyDataBlockIndex;
     /** highest message ID used in the readyDataBlocks */
     private volatile long _highestReadyBlockId;
@@ -40,7 +40,7 @@ public class MessageInputStream extends InputStream {
      * out of order when there are lower IDs not yet 
      * received
      */
-    private Map _notYetReadyBlocks;
+    private final Map<Long, ByteArray> _notYetReadyBlocks;
     /** 
      * if we have received a flag saying there won't be later messages, EOF
      * after we have cleared what we have received.
@@ -51,9 +51,9 @@ public class MessageInputStream extends InputStream {
     private int _readTimeout;
     private IOException _streamError;
     private long _readTotal;
-    private ByteCache _cache;
+    //private ByteCache _cache;
     
-    private byte[] _oneByte = new byte[1];
+    private final byte[] _oneByte = new byte[1];
     
     private final Object _dataLock;
     
@@ -61,16 +61,12 @@ public class MessageInputStream extends InputStream {
         _context = ctx;
         _log = ctx.logManager().getLog(MessageInputStream.class);
         _readyDataBlocks = new ArrayList(4);
-        _readyDataBlockIndex = 0;
         _highestReadyBlockId = -1;
         _highestBlockId = -1;
         _readTimeout = -1;
-        _readTotal = 0;
         _notYetReadyBlocks = new HashMap(4);
         _dataLock = new Object();
-        _closeReceived = false;
-        _locallyClosed = false;
-        _cache = ByteCache.getInstance(128, Packet.MAX_PAYLOAD_SIZE);
+        //_cache = ByteCache.getInstance(128, Packet.MAX_PAYLOAD_SIZE);
     }
     
     /** What is the highest block ID we've completely received through?
@@ -140,10 +136,8 @@ public class MessageInputStream extends InputStream {
             if (num <= 0) return null;
             blocks = new long[num];
             int i = 0;
-            for (Iterator iter = _notYetReadyBlocks.keySet().iterator(); iter.hasNext(); ) {
-                Long id = (Long)iter.next();
-                blocks[i] = id.longValue();
-                i++;
+            for (Long id : _notYetReadyBlocks.keySet()) {
+                blocks[i++] = id.longValue();
             }
         }
         Arrays.sort(blocks);
@@ -178,16 +172,15 @@ public class MessageInputStream extends InputStream {
                 buf.append("Close received, ready bytes: ");
                 long available = 0;
                 for (int i = 0; i < _readyDataBlocks.size(); i++) 
-                    available += ((ByteArray)_readyDataBlocks.get(i)).getValid();
+                    available += _readyDataBlocks.get(i).getValid();
                 available -= _readyDataBlockIndex;
                 buf.append(available);
                 buf.append(" blocks: ").append(_readyDataBlocks.size());
                 
                 buf.append(" not ready blocks: ");
                 long notAvailable = 0;
-                for (Iterator iter = _notYetReadyBlocks.keySet().iterator(); iter.hasNext(); ) {
-                    Long id = (Long)iter.next();
-                    ByteArray ba = (ByteArray)_notYetReadyBlocks.get(id);
+                for (Long id : _notYetReadyBlocks.keySet()) {
+                    ByteArray ba = _notYetReadyBlocks.get(id);
                     buf.append(id).append(" ");
                     
                     if (ba != null)
@@ -237,7 +230,7 @@ public class MessageInputStream extends InputStream {
                 long cur = _highestReadyBlockId + 1;
                 // now pull in any previously pending blocks
                 while (_notYetReadyBlocks.containsKey(new Long(cur))) {
-                    ByteArray ba = (ByteArray)_notYetReadyBlocks.remove(new Long(cur));
+                    ByteArray ba = _notYetReadyBlocks.remove(new Long(cur));
                     if ( (ba != null) && (ba.getData() != null) && (ba.getValid() > 0) ) {
                         _readyDataBlocks.add(ba);
                     }
@@ -341,7 +334,7 @@ public class MessageInputStream extends InputStream {
                     return i;
                 } else {
                     // either was already ready, or we wait()ed and it arrived
-                    ByteArray cur = (ByteArray)_readyDataBlocks.get(0);
+                    ByteArray cur = _readyDataBlocks.get(0);
                     byte rv = cur.getData()[cur.getOffset()+_readyDataBlockIndex];
                     _readyDataBlockIndex++;
                     boolean removed = false;
@@ -378,7 +371,7 @@ public class MessageInputStream extends InputStream {
         int numBytes = 0;
         synchronized (_dataLock) {
             for (int i = 0; i < _readyDataBlocks.size(); i++) {
-                ByteArray cur = (ByteArray)_readyDataBlocks.get(i);
+                ByteArray cur = _readyDataBlocks.get(i);
                 if (i == 0)
                     numBytes += cur.getValid() - _readyDataBlockIndex;
                 else
@@ -402,14 +395,13 @@ public class MessageInputStream extends InputStream {
             if (_locallyClosed) return 0;
             int numBytes = 0;
             for (int i = 0; i < _readyDataBlocks.size(); i++) {
-                ByteArray cur = (ByteArray)_readyDataBlocks.get(i);
+                ByteArray cur = _readyDataBlocks.get(i);
                 if (i == 0)
                     numBytes += cur.getValid() - _readyDataBlockIndex;
                 else
                     numBytes += cur.getValid();
             }
-            for (Iterator iter = _notYetReadyBlocks.values().iterator(); iter.hasNext(); ) {
-                ByteArray cur = (ByteArray)iter.next();
+            for (ByteArray cur : _notYetReadyBlocks.values()) {
                 numBytes += cur.getValid();
             }
             return numBytes;
@@ -421,7 +413,7 @@ public class MessageInputStream extends InputStream {
             if (_locallyClosed) return 0;
             int numBytes = 0;
             for (int i = 0; i < _readyDataBlocks.size(); i++) {
-                ByteArray cur = (ByteArray)_readyDataBlocks.get(i);
+                ByteArray cur = _readyDataBlocks.get(i);
                 if (i == 0)
                     numBytes += cur.getValid() - _readyDataBlockIndex;
                 else
@@ -440,8 +432,7 @@ public class MessageInputStream extends InputStream {
              
             // we don't need the data, but we do need to keep track of the messageIds
             // received, so we can ACK accordingly
-            for (Iterator iter = _notYetReadyBlocks.values().iterator(); iter.hasNext(); ) {
-                ByteArray ba = (ByteArray)iter.next();
+            for (ByteArray ba : _notYetReadyBlocks.values()) {
                 ba.setData(null);
                 //_cache.release(ba);
             }
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/MessageOutputStream.java b/apps/streaming/java/src/net/i2p/client/streaming/MessageOutputStream.java
index 1a7f0afbe82e313c6793168c702a087039d3d6d5..fd10d679cec33b72c459ecfeb416747f59f8fefb 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/MessageOutputStream.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/MessageOutputStream.java
@@ -43,6 +43,10 @@ public class MessageOutputStream extends OutputStream {
     private long _sendPeriodBytes;
     private int _sendBps;
     
+    /**
+     *  Since this is less than i2ptunnel's i2p.streaming.connectDelay default of 1000,
+     *  we only wait 250 at the start. Guess that's ok, 1000 is too long anyway.
+     */
     private static final int DEFAULT_PASSIVE_FLUSH_DELAY = 250;
 
     public MessageOutputStream(I2PAppContext ctx, DataReceiver receiver) {
@@ -273,8 +277,18 @@ public class MessageOutputStream extends OutputStream {
     }
     
     /** 
-     * Flush the data already queued up, blocking until it has been
-     * delivered.
+     * Flush the data already queued up, blocking only if the outbound
+     * window is full.
+     *
+     * Prior to 0.8.1, this blocked until "delivered".
+     * "Delivered" meant "received an ACK from the far end",
+     * which is not the commom implementation of flush(), and really hurt the
+     * performance of i2psnark, which flush()ed frequently.
+     * Calling flush() would cause a complete window stall.
+     *
+     * As of 0.8.1, only wait for accept into the streaming output queue.
+     * This will speed up snark significantly, and allow us to flush()
+     * the initial data in I2PTunnelRunner, saving 250 ms.
      *
      * @throws IOException if the write fails
      */
@@ -283,6 +297,14 @@ public class MessageOutputStream extends OutputStream {
      /* @throws InterruptedIOException if the write times out
       * Documented here, but doesn't belong in the javadoc. 
       */
+        flush(true);
+    }
+
+    /**
+     *  @param wait_for_accept_only see discussion in close() code
+     *  @@since 0.8.1
+     */
+    private void flush(boolean wait_for_accept_only) throws IOException {
         long begin = _context.clock().now();
         WriteStatus ws = null;
         if (_log.shouldLog(Log.INFO) && _valid > 0)
@@ -297,14 +319,28 @@ public class MessageOutputStream extends OutputStream {
                 throwAnyError();
                 return;
             }
-            ws = _dataReceiver.writeData(_buf, 0, _valid);
-            _written += _valid;
-            _valid = 0;
-            locked_updateBufferSize();
-            _lastFlushed = _context.clock().now();
-            _dataLock.notifyAll();
+            // if valid == 0 return ??? - no, this could flush a CLOSE packet too.
+
+            // Yes, flush here, inside the data lock, and do all the waitForCompletion() stuff below
+            // (disabled)
+            if (!wait_for_accept_only) {
+                ws = _dataReceiver.writeData(_buf, 0, _valid);
+                _written += _valid;
+                _valid = 0;
+                locked_updateBufferSize();
+                _lastFlushed = _context.clock().now();
+                _dataLock.notifyAll();
+            }
         }
         
+        // Skip all the waitForCompletion() stuff below, which is insanity, as of 0.8.1
+        // must do this outside the data lock
+        if (wait_for_accept_only) {
+            flushAvailable(_dataReceiver, true);
+            return;
+        }
+
+        // Wait a loooooong time, until we have the ACK
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("before waiting " + _writeTimeout + "ms for completion of " + ws);
         if (_closed && 
@@ -328,14 +364,28 @@ public class MessageOutputStream extends OutputStream {
         throwAnyError();
     }
     
+    /**
+     *  This does a flush, and BLOCKS until
+     *  the CLOSE packet is acked.
+     */
     @Override
     public void close() throws IOException {
         if (_closed) {
             synchronized (_dataLock) { _dataLock.notifyAll(); }
             return;
         }
+        // setting _closed before flush() will force flush() to send a CLOSE packet
         _closed = true;
-        flush();
+
+        // In 0.8.1 we rewrote flush() to only wait for accept into the window,
+        // not "completion" (i.e. ack from the far end).
+        // Unfortunately, that broke close(), at least in i2ptunnel HTTPClient.
+        // Symptom was premature close, i.e. incomplete pages and images.
+        // Possible cause - I2PTunnelRunner code? or the code here that follows flush()?
+        // It seems like we shouldn't have to wait for the far-end ACK for a close packet,
+        // should we? To be researched further.
+        // false -> wait for completion, not just accept.
+        flush(false);
         _log.debug("Output stream closed after writing " + _written);
         ByteArray ba = null;
         synchronized (_dataLock) {
@@ -351,7 +401,11 @@ public class MessageOutputStream extends OutputStream {
             _dataCache.release(ba);
         }
     }
-    /** nonblocking close */
+
+    /**
+     *  nonblocking close -
+     *  Use outside of this package is deprecated, should be made package local
+     */
     public void closeInternal() {
         _closed = true;
         if (_streamError == null)
@@ -412,6 +466,8 @@ public class MessageOutputStream extends OutputStream {
         if (_log.shouldLog(Log.INFO) && _valid > 0)
             _log.info("flushAvailable() valid = " + _valid);
         synchronized (_dataLock) {
+            // if valid == 0 return ??? - no, this could flush a CLOSE packet too.
+
             // _buf may be null, but the data receiver can handle that just fine,
             // deciding whether or not to send a packet
             ws = target.writeData(_buf, 0, _valid);
@@ -457,14 +513,21 @@ public class MessageOutputStream extends OutputStream {
     
     /** Define a way to detect the status of a write */
     public interface WriteStatus {
-        /** wait until the data written either fails or succeeds */
+        /**
+         * Wait until the data written either fails or succeeds.
+         * Success means an ACK FROM THE FAR END.
+         * @param maxWaitMs -1 = forever
+         */
         public void waitForCompletion(int maxWaitMs);
+
         /** 
-         * wait until the data written is accepted into the outbound pool,
+         * Wait until the data written is accepted into the outbound pool,
+         * (i.e. the outbound window is not full)
          * which we throttle rather than accept arbitrary data and queue 
-         * @param maxWaitMs -1 = forever ?
+         * @param maxWaitMs -1 = forever
          */
         public void waitForAccept(int maxWaitMs);
+
         /** the write was accepted.  aka did the socket not close? */
         public boolean writeAccepted();
         /** did the write fail?  */
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/PacketLocal.java b/apps/streaming/java/src/net/i2p/client/streaming/PacketLocal.java
index 674ff6179c531fcede87686b12d46b18a5f90823..cbe913e0505c028ac271af8399cb8b79e73c7bb5 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/PacketLocal.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/PacketLocal.java
@@ -194,7 +194,8 @@ public class PacketLocal extends Packet implements MessageOutputStream.WriteStat
     }
     
     /**
-     * @param maxWaitMs MessageOutputStream is the only caller, often with -1 ??????
+     * Blocks until outbound window is not full. See Connection.packetSendChoke().
+     * @param maxWaitMs MessageOutputStream is the only caller, generally with -1
      */
     public void waitForAccept(int maxWaitMs) {
         if (_connection == null) 
@@ -220,6 +221,7 @@ public class PacketLocal extends Packet implements MessageOutputStream.WriteStat
                        + toString());
     }
     
+    /** block until the packet is acked from the far end */
     public void waitForCompletion(int maxWaitMs) {
         long expiration = _context.clock().now()+maxWaitMs;
         while (true) {
diff --git a/apps/susidns/src/build.xml b/apps/susidns/src/build.xml
index 55b662a9d380184965e0db1f8d3e9bc23e36351f..8244d37cfb7dc2d1ebad4a1e3390ec64cb7e6bf1 100644
--- a/apps/susidns/src/build.xml
+++ b/apps/susidns/src/build.xml
@@ -72,7 +72,6 @@
         		<include name="images/*.png"/>
         		<include name="css.css"/>
         		<include name="index.html"/>
-        		<include name="WEB-INF/web-out.xml"/>
         		<include name="WEB-INF/classes/${project}.properties"/>
         	</fileset>
         </war>
diff --git a/apps/susidns/src/java/src/i2p/susi/dns/AddressbookBean.java b/apps/susidns/src/java/src/i2p/susi/dns/AddressbookBean.java
index 63c4db85acb1f61c285ff7b7a2231ee927d552be..b6af8a12dada88fa0d30c0f359f158379d1fc681 100644
--- a/apps/susidns/src/java/src/i2p/susi/dns/AddressbookBean.java
+++ b/apps/susidns/src/java/src/i2p/susi/dns/AddressbookBean.java
@@ -38,6 +38,7 @@ import java.util.Properties;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Destination;
+import net.i2p.util.SecureFileOutputStream;
 
 public class AddressbookBean
 {
@@ -330,7 +331,7 @@ public class AddressbookBean
 	{
 		String filename = properties.getProperty( getBook() + "_addressbook" );
 		
-		FileOutputStream fos = new FileOutputStream( ConfigBean.addressbookPrefix + filename  );
+		FileOutputStream fos = new SecureFileOutputStream( ConfigBean.addressbookPrefix + filename  );
 		addressbook.store( fos, null );
 		try {
 			fos.close();
diff --git a/apps/susidns/src/java/src/i2p/susi/dns/ConfigBean.java b/apps/susidns/src/java/src/i2p/susi/dns/ConfigBean.java
index f6655757d8318977ee667baf4c8cbf013948fb05..ee65753564a0ff961f9432664fdfd5c8444b57bd 100644
--- a/apps/susidns/src/java/src/i2p/susi/dns/ConfigBean.java
+++ b/apps/susidns/src/java/src/i2p/susi/dns/ConfigBean.java
@@ -27,13 +27,13 @@ package i2p.susi.dns;
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.FileReader;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.Serializable;
 
 import net.i2p.I2PAppContext;
+import net.i2p.util.SecureFileOutputStream;
 
 public class ConfigBean implements Serializable {
 	
@@ -111,7 +111,7 @@ public class ConfigBean implements Serializable {
 	{
 		File file = new File( configFileName );
 		try {
-			PrintWriter out = new PrintWriter( new FileOutputStream( file ) );
+			PrintWriter out = new PrintWriter( new SecureFileOutputStream( file ) );
 			out.print( config );
 			out.flush();
 			out.close();
diff --git a/apps/susidns/src/java/src/i2p/susi/dns/SubscriptionsBean.java b/apps/susidns/src/java/src/i2p/susi/dns/SubscriptionsBean.java
index 6b1a80576999b83406f5de862d881755b6d3f92a..176561acaf16c6934ae7c3a879b46b3f91d2cc63 100644
--- a/apps/susidns/src/java/src/i2p/susi/dns/SubscriptionsBean.java
+++ b/apps/susidns/src/java/src/i2p/susi/dns/SubscriptionsBean.java
@@ -28,12 +28,13 @@ import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.FileReader;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.Properties;
 
+import net.i2p.util.SecureFileOutputStream;
+
 public class SubscriptionsBean
 {
 	private String action, fileName, content, serial, lastSerial;
@@ -113,7 +114,7 @@ public class SubscriptionsBean
 	{
 		File file = new File( getFileName() );
 		try {
-			PrintWriter out = new PrintWriter( new FileOutputStream( file ) );
+			PrintWriter out = new PrintWriter( new SecureFileOutputStream( file ) );
 			out.print( content );
 			out.flush();
 			out.close();
diff --git a/core/java/src/net/i2p/I2PAppContext.java b/core/java/src/net/i2p/I2PAppContext.java
index 637596c4f3858c7aaee8bd21b9206b35cc68c76b..98e7d6af9aeb81b2d9435ce6151c80ab8e936150 100644
--- a/core/java/src/net/i2p/I2PAppContext.java
+++ b/core/java/src/net/i2p/I2PAppContext.java
@@ -31,6 +31,7 @@ import net.i2p.util.KeyRing;
 import net.i2p.util.LogManager;
 //import net.i2p.util.PooledRandomSource;
 import net.i2p.util.RandomSource;
+import net.i2p.util.SecureDirectory;
 
 /**
  * <p>Provide a base scope for accessing singletons that I2P exposes.  Rather than
@@ -218,7 +219,7 @@ public class I2PAppContext {
         // config defaults to base
         s = getProperty("i2p.dir.config");
         if (s != null) {
-            _configDir = new File(s);
+            _configDir = new SecureDirectory(s);
             if (!_configDir.exists())
                 _configDir.mkdir();
         } else {
@@ -227,7 +228,7 @@ public class I2PAppContext {
         // router defaults to config
         s = getProperty("i2p.dir.router");
         if (s != null) {
-            _routerDir = new File(s);
+            _routerDir = new SecureDirectory(s);
             if (!_routerDir.exists())
                 _routerDir.mkdir();
         } else {
@@ -241,7 +242,7 @@ public class I2PAppContext {
         // these all default to router
         s = getProperty("i2p.dir.log");
         if (s != null) {
-            _logDir = new File(s);
+            _logDir = new SecureDirectory(s);
             if (!_logDir.exists())
                 _logDir.mkdir();
         } else {
@@ -249,7 +250,7 @@ public class I2PAppContext {
         }
         s = getProperty("i2p.dir.app");
         if (s != null) {
-            _appDir = new File(s);
+            _appDir = new SecureDirectory(s);
             if (!_appDir.exists())
                 _appDir.mkdir();
         } else {
@@ -345,14 +346,14 @@ public class I2PAppContext {
                 String d = getProperty("i2p.dir.temp", System.getProperty("java.io.tmpdir"));
                 // our random() probably isn't warmed up yet
                 String f = "i2p-" + Math.abs((new java.util.Random()).nextInt()) + ".tmp";
-                _tmpDir = new File(d, f);
+                _tmpDir = new SecureDirectory(d, f);
                 if (_tmpDir.exists()) {
                     // good or bad ?
                 } else if (_tmpDir.mkdir()) {
                     _tmpDir.deleteOnExit();
                 } else {
                     System.err.println("Could not create temp dir " + _tmpDir.getAbsolutePath());
-                    _tmpDir = new File(_routerDir, "tmp");
+                    _tmpDir = new SecureDirectory(_routerDir, "tmp");
                     _tmpDir.mkdir();
                 }
             }
diff --git a/core/java/src/net/i2p/client/HandlerImpl.java b/core/java/src/net/i2p/client/HandlerImpl.java
index 98d1e76ff25bb5502a14369d4d595fa2a1ac5929..d1b5100e2754af29f51fc6656068d19227e98cd0 100644
--- a/core/java/src/net/i2p/client/HandlerImpl.java
+++ b/core/java/src/net/i2p/client/HandlerImpl.java
@@ -18,9 +18,9 @@ import net.i2p.util.Log;
  * @author jrandom
  */
 abstract class HandlerImpl implements I2CPMessageHandler {
-    protected Log _log;
-    private int _type;
-    protected I2PAppContext _context;
+    protected final Log _log;
+    private final int _type;
+    protected final I2PAppContext _context;
 
     public HandlerImpl(I2PAppContext context, int type) {
         _context = context;
diff --git a/core/java/src/net/i2p/client/I2PClient.java b/core/java/src/net/i2p/client/I2PClient.java
index 9a732c3dba0a20fdf9d36560937c8886c254de1f..941549d5372bf453f949fd7af1c62b9a22079f3f 100644
--- a/core/java/src/net/i2p/client/I2PClient.java
+++ b/core/java/src/net/i2p/client/I2PClient.java
@@ -34,6 +34,8 @@ public interface I2PClient {
     public final static String PROP_RELIABILITY_BEST_EFFORT = "BestEffort";
     /** Reliability value: guaranteed */
     public final static String PROP_RELIABILITY_GUARANTEED = "Guaranteed";
+    /** @since 0.8.1 */
+    public final static String PROP_RELIABILITY_NONE = "none";
 
     /** protocol flag that must be sent when opening the i2cp connection to the router */
     public final static int PROTOCOL_BYTE = 0x2A;
@@ -64,4 +66,4 @@ public interface I2PClient {
      * @return newly created destination
      */
     public Destination createDestination(OutputStream destKeyStream, Certificate cert) throws I2PException, IOException;
-}
\ No newline at end of file
+}
diff --git a/core/java/src/net/i2p/client/I2PSessionImpl2.java b/core/java/src/net/i2p/client/I2PSessionImpl2.java
index d8d8c8dfce202f8dd09c4dc37b1b4a6f42f0521d..62b0d3d08b9fcd1d398ad5c9fe2628881b7b7513 100644
--- a/core/java/src/net/i2p/client/I2PSessionImpl2.java
+++ b/core/java/src/net/i2p/client/I2PSessionImpl2.java
@@ -33,12 +33,14 @@ import net.i2p.util.Log;
 class I2PSessionImpl2 extends I2PSessionImpl {
 
     /** set of MessageState objects, representing all of the messages in the process of being sent */
-    private /* FIXME final FIXME */ Set _sendingStates;
+    private /* FIXME final FIXME */ Set<MessageState> _sendingStates;
     /** max # seconds to wait for confirmation of the message send */
     private final static long SEND_TIMEOUT = 60 * 1000; // 60 seconds to send 
     /** should we gzip each payload prior to sending it? */
     private final static boolean SHOULD_COMPRESS = true;
     private final static boolean SHOULD_DECOMPRESS = true;
+    /** Don't expect any MSMs from the router for outbound traffic @since 0.8.1 */
+    private boolean _noEffort;
 
     /** for extension */
     public I2PSessionImpl2() {}
@@ -53,6 +55,8 @@ class I2PSessionImpl2 extends I2PSessionImpl {
         super(ctx, destKeyStream, options);
         _log = ctx.logManager().getLog(I2PSessionImpl2.class);
         _sendingStates = new HashSet(32);
+        // default is BestEffort
+        _noEffort = "none".equalsIgnoreCase(options.getProperty(I2PClient.PROP_RELIABILITY));
 
         ctx.statManager().createRateStat("i2cp.sendBestEffortTotalTime", "how long to do the full sendBestEffort call?", "i2cp", new long[] { 10*60*1000 } );
         //ctx.statManager().createRateStat("i2cp.sendBestEffortStage0", "first part of sendBestEffort?", "i2cp", new long[] { 10*60*1000 } );
@@ -60,15 +64,16 @@ class I2PSessionImpl2 extends I2PSessionImpl {
         //ctx.statManager().createRateStat("i2cp.sendBestEffortStage2", "third part of sendBestEffort?", "i2cp", new long[] { 10*60*1000 } );
         //ctx.statManager().createRateStat("i2cp.sendBestEffortStage3", "fourth part of sendBestEffort?", "i2cp", new long[] { 10*60*1000 } );
         //ctx.statManager().createRateStat("i2cp.sendBestEffortStage4", "fifth part of sendBestEffort?", "i2cp", new long[] { 10*60*1000 } );
-        _context.statManager().createRateStat("i2cp.receiveStatusTime.0", "How long it took to get status=0 back", "i2cp", new long[] { 60*1000, 10*60*1000 });
-        _context.statManager().createRateStat("i2cp.receiveStatusTime.1", "How long it took to get status=1 back", "i2cp", new long[] { 60*1000, 10*60*1000 });
-        _context.statManager().createRateStat("i2cp.receiveStatusTime.2", "How long it took to get status=2 back", "i2cp", new long[] { 60*1000, 10*60*1000 });
-        _context.statManager().createRateStat("i2cp.receiveStatusTime.3", "How long it took to get status=3 back", "i2cp", new long[] { 60*1000, 10*60*1000 });
-        _context.statManager().createRateStat("i2cp.receiveStatusTime.4", "How long it took to get status=4 back", "i2cp", new long[] { 60*1000, 10*60*1000 });
-        _context.statManager().createRateStat("i2cp.receiveStatusTime.5", "How long it took to get status=5 back", "i2cp", new long[] { 60*1000, 10*60*1000 });
-        _context.statManager().createRateStat("i2cp.receiveStatusTime", "How long it took to get any status", "i2cp", new long[] { 60*1000, 10*60*1000 });
-        _context.statManager().createRateStat("i2cp.tx.msgCompressed", "compressed size transferred", "i2cp", new long[] { 60*1000, 30*60*1000 });
-        _context.statManager().createRateStat("i2cp.tx.msgExpanded", "size before compression", "i2cp", new long[] { 60*1000, 30*60*1000 });
+        //_context.statManager().createRateStat("i2cp.receiveStatusTime.0", "How long it took to get status=0 back", "i2cp", new long[] { 60*1000, 10*60*1000 });
+        _context.statManager().createRateStat("i2cp.receiveStatusTime.1", "How long it took to get status=1 back", "i2cp", new long[] { 10*60*1000 });
+        // best effort codes unused
+        //_context.statManager().createRateStat("i2cp.receiveStatusTime.2", "How long it took to get status=2 back", "i2cp", new long[] { 60*1000, 10*60*1000 });
+        //_context.statManager().createRateStat("i2cp.receiveStatusTime.3", "How long it took to get status=3 back", "i2cp", new long[] { 60*1000, 10*60*1000 });
+        _context.statManager().createRateStat("i2cp.receiveStatusTime.4", "How long it took to get status=4 back", "i2cp", new long[] { 10*60*1000 });
+        _context.statManager().createRateStat("i2cp.receiveStatusTime.5", "How long it took to get status=5 back", "i2cp", new long[] { 10*60*1000 });
+        _context.statManager().createRateStat("i2cp.receiveStatusTime", "How long it took to get any status", "i2cp", new long[] { 10*60*1000 });
+        _context.statManager().createRateStat("i2cp.tx.msgCompressed", "compressed size transferred", "i2cp", new long[] { 30*60*1000 });
+        _context.statManager().createRateStat("i2cp.tx.msgExpanded", "size before compression", "i2cp", new long[] { 30*60*1000 });
     }
 
     protected long getTimeout() {
@@ -186,7 +191,10 @@ class I2PSessionImpl2 extends I2PSessionImpl {
         }
         _context.statManager().addRateData("i2cp.tx.msgCompressed", compressed, 0);
         _context.statManager().addRateData("i2cp.tx.msgExpanded", size, 0);
-        return sendBestEffort(dest, payload, keyUsed, tagsSent, expires);
+        if (_noEffort)
+            return sendNoEffort(dest, payload, expires);
+        else
+            return sendBestEffort(dest, payload, keyUsed, tagsSent, expires);
     }
 
     /**
@@ -213,6 +221,9 @@ class I2PSessionImpl2 extends I2PSessionImpl {
     private static final int NUM_TAGS = 50;
 
     /**
+     * TODO - Don't need to save MessageState since actuallyWait is false...
+     * But for now just use sendNoEffort() instead.
+     *
      * @param keyUsed unused - no end-to-end crypto
      * @param tagsSent unused - no end-to-end crypto
      */
@@ -257,7 +268,7 @@ class I2PSessionImpl2 extends I2PSessionImpl {
                                + "ms left, " + oldTags + " tags known and " 
                                + (tag == null ? "no tag" : " a valid tag"));
             }
-        
+
             if (false) // rekey
                 newKey = _context.keyGenerator().generateSessionKey();
         
@@ -371,6 +382,37 @@ class I2PSessionImpl2 extends I2PSessionImpl {
         return found;
     }
     
+    /**
+     * Same as sendBestEffort(), except we do not expect any MessageStatusMessage responses -
+     * not for accepted, or success, or failure.
+     * So we don't create a MessageState and save it on the _sendingStates HashSet
+     *
+     * @return true always
+     * @since 0.8.1
+     */
+    protected boolean sendNoEffort(Destination dest, byte payload[], long expires)
+                    throws I2PSessionException {
+        // nonce always 0
+        _producer.sendMessage(this, dest, 0, payload, null, null, null, null, expires);
+        return true;
+    }
+    
+    /**
+     *  Only call this with nonzero status, i.e. for outbound messages
+     *  whose MessageState may be queued on _sendingStates.
+     *
+     *  Even when using sendBestEffort(), this is a waste, because the
+     *  MessageState is removed from _sendingStates immediately and
+     *  so the lookup here fails.
+     *  And iterating through the HashSet instead of having a map
+     *  is bad too.
+     *
+     *  This is now pretty much avoided since streaming now sets
+     *  i2cp.messageReliability = none, which forces sendNoEffort() instead of sendBestEffort(),
+     *  so the router won't send us any MSM's for outbound traffic.
+     *
+     *  @param status != 0
+     */
     @Override
     public void receiveStatus(int msgId, long nonce, int status) {
         if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Received status " + status + " for msgId " + msgId + " / " + nonce);
@@ -413,12 +455,13 @@ class I2PSessionImpl2 extends I2PSessionImpl {
                 case 1:
                     _context.statManager().addRateData("i2cp.receiveStatusTime.1", lifetime, 0);
                     break;
-                case 2:
-                    _context.statManager().addRateData("i2cp.receiveStatusTime.2", lifetime, 0);
-                    break;
-                case 3:
-                    _context.statManager().addRateData("i2cp.receiveStatusTime.3", lifetime, 0);
-                    break;
+                // best effort codes unused
+                //case 2:
+                //    _context.statManager().addRateData("i2cp.receiveStatusTime.2", lifetime, 0);
+                //    break;
+                //case 3:
+                //    _context.statManager().addRateData("i2cp.receiveStatusTime.3", lifetime, 0);
+                //    break;
                 case 4:
                     _context.statManager().addRateData("i2cp.receiveStatusTime.4", lifetime, 0);
                     break;
diff --git a/core/java/src/net/i2p/client/MessageState.java b/core/java/src/net/i2p/client/MessageState.java
index 84ec9ad6aa22b205bf41f4c33f3fee5b48b5113e..a67b5277044106eb323ef0c250314490acb99a18 100644
--- a/core/java/src/net/i2p/client/MessageState.java
+++ b/core/java/src/net/i2p/client/MessageState.java
@@ -37,13 +37,7 @@ class MessageState {
         _context = ctx;
         _nonce = nonce;
         _prefix = prefix + "[" + _stateId + "]: ";
-        _id = null;
         _receivedStatus = new HashSet();
-        _cancelled = false;
-        _key = null;
-        _newKey = null;
-        _tags = null;
-        _to = null;
         _created = ctx.clock().now();
         //ctx.statManager().createRateStat("i2cp.checkStatusTime", "how long it takes to go through the states", "i2cp", new long[] { 60*1000 });
     }
diff --git a/core/java/src/net/i2p/client/SessionIdleTimer.java b/core/java/src/net/i2p/client/SessionIdleTimer.java
index dad736d97f87440613132348c47e6edaf747da71..91c1c4b0fbc263c4f6f757cd63113cfbc9f008f0 100644
--- a/core/java/src/net/i2p/client/SessionIdleTimer.java
+++ b/core/java/src/net/i2p/client/SessionIdleTimer.java
@@ -18,13 +18,13 @@ import net.i2p.util.SimpleTimer;
  *
  * @author zzz
  */
-public class SessionIdleTimer implements SimpleTimer.TimedEvent {
+class SessionIdleTimer implements SimpleTimer.TimedEvent {
     public static final long MINIMUM_TIME = 5*60*1000;
     private static final long DEFAULT_REDUCE_TIME = 20*60*1000;
     private static final long DEFAULT_CLOSE_TIME = 30*60*1000;
     private final static Log _log = new Log(SessionIdleTimer.class);
-    private I2PAppContext _context;
-    private I2PSessionImpl _session;
+    private final I2PAppContext _context;
+    private final I2PSessionImpl _session;
     private boolean _reduceEnabled;
     private int _reduceQuantity;
     private long _reduceTime;
@@ -36,7 +36,6 @@ public class SessionIdleTimer implements SimpleTimer.TimedEvent {
     /**
      *  reduce, shutdown, or both must be true
      */
-    /* FIXME Exporting non-public type through public API FIXME */
     public SessionIdleTimer(I2PAppContext context, I2PSessionImpl session, boolean reduce, boolean shutdown) {
         _context = context;
         _session = session;
diff --git a/core/java/src/net/i2p/client/naming/EepGetNamingService.java b/core/java/src/net/i2p/client/naming/EepGetNamingService.java
index d8331796712770adeacfe73592675b7f85277f83..737e33423869ae32bf09cbd623716dcf18da8085 100644
--- a/core/java/src/net/i2p/client/naming/EepGetNamingService.java
+++ b/core/java/src/net/i2p/client/naming/EepGetNamingService.java
@@ -66,6 +66,10 @@ public class EepGetNamingService extends NamingService {
 
         hostname = hostname.toLowerCase();
 
+        // If you want b32, chain with HostsTxtNamingService
+        if (hostname.length() == 60 && hostname.endsWith(".b32.i2p"))
+            return null;
+
         // check the cache
         Destination d = getCache(hostname);
         if (d != null)
diff --git a/core/java/src/net/i2p/client/naming/ExecNamingService.java b/core/java/src/net/i2p/client/naming/ExecNamingService.java
index 446e907c477af9f8fa6c64c8a6b4c5d9dad1061c..118f06eac55c3c8e35825ab48f1ef71794262164 100644
--- a/core/java/src/net/i2p/client/naming/ExecNamingService.java
+++ b/core/java/src/net/i2p/client/naming/ExecNamingService.java
@@ -66,6 +66,10 @@ public class ExecNamingService extends NamingService {
 
         hostname = hostname.toLowerCase();
 
+        // If you want b32, chain with HostsTxtNamingService
+        if (hostname.length() == 60 && hostname.endsWith(".b32.i2p"))
+            return null;
+
         // check the cache
         Destination d = getCache(hostname);
         if (d != null)
diff --git a/core/java/src/net/i2p/client/naming/LookupDest.java b/core/java/src/net/i2p/client/naming/LookupDest.java
index 1a8a1632b887742f6a8d78967108eb89704fd4ba..2bbb2eeb6a2a5550fd5260a54366d2d879954e5b 100644
--- a/core/java/src/net/i2p/client/naming/LookupDest.java
+++ b/core/java/src/net/i2p/client/naming/LookupDest.java
@@ -27,6 +27,7 @@ class LookupDest {
 
     protected LookupDest(I2PAppContext context) {}
 
+    /** @param key 52 chars (do not include the .b32.i2p suffix) */
     static Destination lookupBase32Hash(I2PAppContext ctx, String key) {
         byte[] h = Base32.decode(key);
         if (h == null)
@@ -44,6 +45,7 @@ class LookupDest {
     }
     ****/
 
+    /** @param h 32 byte hash */
     static Destination lookupHash(I2PAppContext ctx, byte[] h) {
         Hash key = new Hash(h);
         Destination rv = null;
diff --git a/core/java/src/net/i2p/client/naming/NamingService.java b/core/java/src/net/i2p/client/naming/NamingService.java
index a7098d799335348048aa0fc73953b1d5f318cb79..fc9a5341400a896af1335ab1472d2e9ec38e54b0 100644
--- a/core/java/src/net/i2p/client/naming/NamingService.java
+++ b/core/java/src/net/i2p/client/naming/NamingService.java
@@ -32,7 +32,7 @@ public abstract class NamingService {
     public static final String PROP_IMPL = "i2p.naming.impl";
     private static final String DEFAULT_IMPL = "net.i2p.client.naming.HostsTxtNamingService";
 
-    protected static final int CACHE_MAX_SIZE = 8;
+    protected static final int CACHE_MAX_SIZE = 16;
 
     
     /** 
@@ -107,7 +107,7 @@ public abstract class NamingService {
      *  The service may override the age and/or size limit
      */
     /** Don't know why a dest would ever change but keep this short anyway */
-    protected static final long CACHE_MAX_AGE = 60*1000;
+    protected static final long CACHE_MAX_AGE = 7*60*1000;
 
     private class CacheEntry {
         public Destination dest;
@@ -174,4 +174,11 @@ public abstract class NamingService {
             return ce.dest;
         }
     }
+
+    /** @since 0.8.1 */
+    public void clearCache() {
+        synchronized (_cache) {
+            _cache.clear();
+        }
+    }
 }
diff --git a/core/java/src/net/i2p/crypto/CryptixAESKeyCache.java b/core/java/src/net/i2p/crypto/CryptixAESKeyCache.java
index 5084ae2b0a836d7d2c3e9416da754451e0fceacb..5d39c1578b755388f3a54089e09ff14ba6c5e48a 100644
--- a/core/java/src/net/i2p/crypto/CryptixAESKeyCache.java
+++ b/core/java/src/net/i2p/crypto/CryptixAESKeyCache.java
@@ -1,7 +1,6 @@
 package net.i2p.crypto;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
 
 /**
  * Cache the objects used in CryptixRijndael_Algorithm.makeKey to reduce
@@ -11,7 +10,7 @@ import java.util.List;
  *
  */
 public final class CryptixAESKeyCache {
-    private final List _availableKeys;
+    private final LinkedBlockingQueue<KeyCacheEntry> _availableKeys;
     
     private static final int KEYSIZE = 32; // 256bit AES
     private static final int BLOCKSIZE = 16;
@@ -22,7 +21,7 @@ public final class CryptixAESKeyCache {
     private static final int MAX_KEYS = 64;
     
     public CryptixAESKeyCache() {
-        _availableKeys = new ArrayList(MAX_KEYS);
+        _availableKeys = new LinkedBlockingQueue(MAX_KEYS);
     }
     
     /**
@@ -30,10 +29,9 @@ public final class CryptixAESKeyCache {
      *
      */
     public final KeyCacheEntry acquireKey() {
-        synchronized (_availableKeys) {
-            if (!_availableKeys.isEmpty())
-                return (KeyCacheEntry)_availableKeys.remove(0);
-        }
+        KeyCacheEntry rv = _availableKeys.poll();
+        if (rv != null)
+            return rv;
         return createNew();
     }
     
@@ -42,10 +40,7 @@ public final class CryptixAESKeyCache {
      *
      */
     public final void releaseKey(KeyCacheEntry key) {
-        synchronized (_availableKeys) {
-            if (_availableKeys.size() < MAX_KEYS)
-                _availableKeys.add(key);
-        }
+        _availableKeys.offer(key);
     }
     
     public static final KeyCacheEntry createNew() {
diff --git a/core/java/src/net/i2p/crypto/DHSessionKeyBuilder.java b/core/java/src/net/i2p/crypto/DHSessionKeyBuilder.java
index 2c1e57d8e5795d52a3ddfe05de1085f89ff23b7a..b438f2b471df9025e9de49b2cbc229ba5d92ed12 100644
--- a/core/java/src/net/i2p/crypto/DHSessionKeyBuilder.java
+++ b/core/java/src/net/i2p/crypto/DHSessionKeyBuilder.java
@@ -65,35 +65,17 @@ public class DHSessionKeyBuilder {
     public final static String PROP_DH_PRECALC_MIN = "crypto.dh.precalc.min";
     public final static String PROP_DH_PRECALC_MAX = "crypto.dh.precalc.max";
     public final static String PROP_DH_PRECALC_DELAY = "crypto.dh.precalc.delay";
-    public final static String DEFAULT_DH_PRECALC_MIN = "5";
-    public final static String DEFAULT_DH_PRECALC_MAX = "50";
-    public final static String DEFAULT_DH_PRECALC_DELAY = "10000";
+    public final static int DEFAULT_DH_PRECALC_MIN = 5;
+    public final static int DEFAULT_DH_PRECALC_MAX = 50;
+    public final static int DEFAULT_DH_PRECALC_DELAY = 10000;
 
     static {
         I2PAppContext ctx = _context;
         ctx.statManager().createRateStat("crypto.dhGeneratePublicTime", "How long it takes to create x and X", "Encryption", new long[] { 60*1000, 5*60*1000, 60*60*1000 });
         ctx.statManager().createRateStat("crypto.dhCalculateSessionTime", "How long it takes to create the session key", "Encryption", new long[] { 60*1000, 5*60*1000, 60*60*1000 });        
-        try {
-            int val = Integer.parseInt(ctx.getProperty(PROP_DH_PRECALC_MIN, DEFAULT_DH_PRECALC_MIN));
-            MIN_NUM_BUILDERS = val;
-        } catch (Throwable t) {
-            int val = Integer.parseInt(DEFAULT_DH_PRECALC_MIN);
-            MIN_NUM_BUILDERS = val;
-        }
-        try {
-            int val = Integer.parseInt(ctx.getProperty(PROP_DH_PRECALC_MAX, DEFAULT_DH_PRECALC_MAX));
-            MAX_NUM_BUILDERS = val;
-        } catch (Throwable t) {
-            int val = Integer.parseInt(DEFAULT_DH_PRECALC_MAX);
-            MAX_NUM_BUILDERS = val;
-        }
-        try {
-            int val = Integer.parseInt(ctx.getProperty(PROP_DH_PRECALC_DELAY, DEFAULT_DH_PRECALC_DELAY));
-            CALC_DELAY = val;
-        } catch (Throwable t) {
-            int val = Integer.parseInt(DEFAULT_DH_PRECALC_DELAY);
-            CALC_DELAY = val;
-        }
+        MIN_NUM_BUILDERS = ctx.getProperty(PROP_DH_PRECALC_MIN, DEFAULT_DH_PRECALC_MIN);
+        MAX_NUM_BUILDERS = ctx.getProperty(PROP_DH_PRECALC_MAX, DEFAULT_DH_PRECALC_MAX);
+        CALC_DELAY = ctx.getProperty(PROP_DH_PRECALC_DELAY, DEFAULT_DH_PRECALC_DELAY);
 
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("DH Precalc (minimum: " + MIN_NUM_BUILDERS + " max: " + MAX_NUM_BUILDERS + ", delay: "
diff --git a/core/java/src/net/i2p/crypto/DSAEngine.java b/core/java/src/net/i2p/crypto/DSAEngine.java
index b083cc24800395b4d22f301b33e50c9211f1e5cc..941774253ca91a774199e3eef09194f412362142 100644
--- a/core/java/src/net/i2p/crypto/DSAEngine.java
+++ b/core/java/src/net/i2p/crypto/DSAEngine.java
@@ -41,6 +41,10 @@ import net.i2p.data.SigningPublicKey;
 import net.i2p.util.Log;
 import net.i2p.util.NativeBigInteger;
 
+/**
+ *  Params and rv's changed from Hash to SHA1Hash for version 0.8.1
+ *  There shouldn't be any external users of those variants.
+ */
 public class DSAEngine {
     private Log _log;
     private I2PAppContext _context;
@@ -61,7 +65,9 @@ public class DSAEngine {
     public boolean verifySignature(Signature signature, InputStream in, SigningPublicKey verifyingKey) {
         return verifySignature(signature, calculateHash(in), verifyingKey);
     }
-    public boolean verifySignature(Signature signature, Hash hash, SigningPublicKey verifyingKey) {
+
+    /** @param hash SHA-1 hash, NOT a SHA-256 hash */
+    public boolean verifySignature(Signature signature, SHA1Hash hash, SigningPublicKey verifyingKey) {
         long start = _context.clock().now();
 
         try {
@@ -111,17 +117,18 @@ public class DSAEngine {
     }
     public Signature sign(byte data[], int offset, int length, SigningPrivateKey signingKey) {
         if ((signingKey == null) || (data == null) || (data.length <= 0)) return null;
-        Hash h = calculateHash(data, offset, length);
+        SHA1Hash h = calculateHash(data, offset, length);
         return sign(h, signingKey);
     }
     
     public Signature sign(InputStream in, SigningPrivateKey signingKey) {
         if ((signingKey == null) || (in == null) ) return null;
-        Hash h = calculateHash(in);
+        SHA1Hash h = calculateHash(in);
         return sign(h, signingKey);
     }
 
-    public Signature sign(Hash hash, SigningPrivateKey signingKey) {
+    /** @param hash SHA-1 hash, NOT a SHA-256 hash */
+    public Signature sign(SHA1Hash hash, SigningPrivateKey signingKey) {
         if ((signingKey == null) || (hash == null)) return null;
         long start = _context.clock().now();
 
@@ -186,7 +193,8 @@ public class DSAEngine {
         return sig;
     }
     
-    public Hash calculateHash(InputStream in) {
+    /** @return hash SHA-1 hash, NOT a SHA-256 hash */
+    public SHA1Hash calculateHash(InputStream in) {
         SHA1 digest = new SHA1();
         byte buf[] = new byte[64];
         int read = 0;
@@ -199,14 +207,15 @@ public class DSAEngine {
                 _log.warn("Unable to hash the stream", ioe);
             return null;
         }
-        return new Hash(digest.engineDigest());
+        return new SHA1Hash(digest.engineDigest());
     }
 
-    public static Hash calculateHash(byte[] source, int offset, int len) {
+    /** @return hash SHA-1 hash, NOT a SHA-256 hash */
+    public static SHA1Hash calculateHash(byte[] source, int offset, int len) {
         SHA1 h = new SHA1();
         h.engineUpdate(source, offset, len);
         byte digested[] = h.digest();
-        return new Hash(digested);
+        return new SHA1Hash(digested);
     }
 
     public static void main(String args[]) {
diff --git a/core/java/src/net/i2p/crypto/HMAC256Generator.java b/core/java/src/net/i2p/crypto/HMAC256Generator.java
index bf053210942f9e29025fd28889d13d01fcff16aa..3fc554639f5c6c6323cf6d303cd95fe3b4002e44 100644
--- a/core/java/src/net/i2p/crypto/HMAC256Generator.java
+++ b/core/java/src/net/i2p/crypto/HMAC256Generator.java
@@ -15,16 +15,16 @@ import org.bouncycastle.crypto.macs.I2PHMac;
  * in {@link org.bouncycastle.crypto.macs.I2PHMac} and 
  * {@link org.bouncycastle.crypto.digests.MD5Digest}.
  *
+ * deprecated unused
  */
 public class HMAC256Generator extends HMACGenerator {
     public HMAC256Generator(I2PAppContext context) { super(context); }
     
     @Override
     protected I2PHMac acquire() {
-        synchronized (_available) {
-            if (!_available.isEmpty())
-                return (I2PHMac)_available.remove(0);
-        }
+        I2PHMac rv = _available.poll();
+        if (rv != null)
+            return rv;
         // the HMAC is hardcoded to use SHA256 digest size
         // for backwards compatability.  next time we have a backwards
         // incompatible change, we should update this by removing ", 32"
@@ -43,6 +43,7 @@ public class HMAC256Generator extends HMACGenerator {
         
     }
     
+/******
     public static void main(String args[]) {
         I2PAppContext ctx = I2PAppContext.getGlobalContext();
         byte data[] = new byte[64];
@@ -51,4 +52,5 @@ public class HMAC256Generator extends HMACGenerator {
         Hash mac = ctx.hmac256().calculate(key, data);
         System.out.println(Base64.encode(mac.getData()));
     }
+******/
 }
diff --git a/core/java/src/net/i2p/crypto/HMACGenerator.java b/core/java/src/net/i2p/crypto/HMACGenerator.java
index aed444ed06118259ac253745cdc4d715510e0ed5..237c650550306ef5be52b57c64746d592202d203 100644
--- a/core/java/src/net/i2p/crypto/HMACGenerator.java
+++ b/core/java/src/net/i2p/crypto/HMACGenerator.java
@@ -1,8 +1,7 @@
 package net.i2p.crypto;
 
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
 
 import net.i2p.I2PAppContext;
 import net.i2p.data.DataHelper;
@@ -22,14 +21,14 @@ import org.bouncycastle.crypto.macs.I2PHMac;
 public class HMACGenerator {
     private I2PAppContext _context;
     /** set of available HMAC instances for calculate */
-    protected final List _available;
+    protected final LinkedBlockingQueue<I2PHMac> _available;
     /** set of available byte[] buffers for verify */
-    private final  List _availableTmp;
+    private final LinkedBlockingQueue<byte[]> _availableTmp;
     
     public HMACGenerator(I2PAppContext context) {
         _context = context;
-        _available = new ArrayList(32);
-        _availableTmp = new ArrayList(32);
+        _available = new LinkedBlockingQueue(32);
+        _availableTmp = new LinkedBlockingQueue(32);
     }
     
     /**
@@ -88,39 +87,30 @@ public class HMACGenerator {
     }
     
     protected I2PHMac acquire() {
-        synchronized (_available) {
-            if (!_available.isEmpty())
-                return (I2PHMac)_available.remove(0);
-        }
+        I2PHMac rv = _available.poll();
+        if (rv != null)
+            return rv;
         // the HMAC is hardcoded to use SHA256 digest size
         // for backwards compatability.  next time we have a backwards
         // incompatible change, we should update this by removing ", 32"
         return new I2PHMac(new MD5Digest(), 32);
     }
-    private void release(Mac mac) {
-        synchronized (_available) {
-            if (_available.size() < 64)
-                _available.add(mac);
-        }
+
+    private void release(I2PHMac mac) {
+        _available.offer(mac);
     }
 
     // temp buffers for verify(..)
     private byte[] acquireTmp() {
-        byte rv[] = null;
-        synchronized (_availableTmp) {
-            if (!_availableTmp.isEmpty())
-                rv = (byte[])_availableTmp.remove(0);
-        }
+        byte rv[] = _availableTmp.poll();
         if (rv != null)
             Arrays.fill(rv, (byte)0x0);
         else
             rv = new byte[Hash.HASH_LENGTH];
         return rv;
     }
+
     private void releaseTmp(byte tmp[]) {
-        synchronized (_availableTmp) {
-            if (_availableTmp.size() < 64)
-                _availableTmp.add((Object)tmp);
-        }
+        _availableTmp.offer(tmp);
     }
 }
diff --git a/core/java/src/net/i2p/crypto/SHA1.java b/core/java/src/net/i2p/crypto/SHA1.java
index d8a0ac1c20c42667c41ae2ad45ab7c66dc78a8d4..6dbdee074bae5567607c70b86d315f7b28475ddc 100644
--- a/core/java/src/net/i2p/crypto/SHA1.java
+++ b/core/java/src/net/i2p/crypto/SHA1.java
@@ -63,7 +63,7 @@ public final class SHA1 extends MessageDigest implements Cloneable {
     /**
      * This implementation returns a fixed-size digest.
      */
-    private static final int HASH_LENGTH = 20; // bytes == 160 bits
+    static final int HASH_LENGTH = 20; // bytes == 160 bits
  
     /**
      * Private context for incomplete blocks and padding bytes.
diff --git a/core/java/src/net/i2p/crypto/SHA1Hash.java b/core/java/src/net/i2p/crypto/SHA1Hash.java
new file mode 100644
index 0000000000000000000000000000000000000000..acbb68d4b8789455f587683f63e16d37c0f9826f
--- /dev/null
+++ b/core/java/src/net/i2p/crypto/SHA1Hash.java
@@ -0,0 +1,81 @@
+package net.i2p.crypto;
+
+/*
+ * free (adj.): unencumbered; not under the control of others
+ * Written by jrandom in 2003 and released into the public domain 
+ * with no warranty of any kind, either expressed or implied.  
+ * It probably won't make your computer catch on fire, or eat 
+ * your children, but it might.  Use at your own risk.
+ *
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.data.DataStructureImpl;
+
+/**
+ * Because DSAEngine was abusing Hash for 20-byte hashes
+ *
+ * @since 0.8.1
+ * @author zzz
+ */
+public class SHA1Hash extends DataStructureImpl {
+    private byte[] _data;
+    private int _cachedHashCode;
+
+    public final static int HASH_LENGTH = SHA1.HASH_LENGTH;
+    
+    /** @throws IllegalArgumentException if data is not 20 bytes (null is ok) */
+    public SHA1Hash(byte data[]) {
+        setData(data);
+    }
+
+    public byte[] getData() {
+        return _data;
+    }
+
+    /** @throws IllegalArgumentException if data is not 20 bytes (null is ok) */
+    public void setData(byte[] data) {
+        // FIXME DSAEngine uses a SHA-1 "Hash" as parameters and return values!
+        if (data != null && data.length != HASH_LENGTH)
+            throw new IllegalArgumentException("Hash must be 20 bytes");
+        _data = data;
+        _cachedHashCode = calcHashCode();
+    }
+
+    /** @throws IOException always */
+    public void readBytes(InputStream in) throws DataFormatException, IOException {
+        throw new IOException("unimplemented");
+    }
+    
+    /** @throws IOException always */
+    public void writeBytes(OutputStream out) throws DataFormatException, IOException {
+        throw new IOException("unimplemented");
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if ((obj == null) || !(obj instanceof SHA1Hash)) return false;
+        return DataHelper.eq(_data, ((SHA1Hash) obj)._data);
+    }
+    
+    /** a Hash is a hash, so just use the first 4 bytes for speed */
+    @Override
+    public int hashCode() {
+        return _cachedHashCode;
+    }
+
+    /** a Hash is a hash, so just use the first 4 bytes for speed */
+    private int calcHashCode() {
+        int rv = 0;
+        if (_data != null) {
+            for (int i = 0; i < 4; i++)
+                rv ^= (_data[i] << (i*8));
+        }
+        return rv;
+    }
+}
diff --git a/core/java/src/net/i2p/crypto/YKGenerator.java b/core/java/src/net/i2p/crypto/YKGenerator.java
index ad68ebec1fd56a2c2e78c6c18b383f9deadb89c4..af83f2c9298234d9b43001ae3ca66f069d3339b0 100644
--- a/core/java/src/net/i2p/crypto/YKGenerator.java
+++ b/core/java/src/net/i2p/crypto/YKGenerator.java
@@ -10,24 +10,22 @@ package net.i2p.crypto;
  */
 
 import java.math.BigInteger;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
 
 import net.i2p.I2PAppContext;
 import net.i2p.util.Clock;
 import net.i2p.util.I2PThread;
 import net.i2p.util.Log;
 import net.i2p.util.NativeBigInteger;
-import net.i2p.util.RandomSource;
 
 /**
  * Precalculate the Y and K for ElGamal encryption operations.
  *
  * This class precalcs a set of values on its own thread, using those transparently
  * when a new instance is created.  By default, the minimum threshold for creating 
- * new values for the pool is 5, and the max pool size is 10.  Whenever the pool has
+ * new values for the pool is 20, and the max pool size is 50.  Whenever the pool has
  * less than the minimum, it fills it up again to the max.  There is a delay after 
- * each precalculation so that the CPU isn't hosed during startup (defaulting to 10 seconds).  
+ * each precalculation so that the CPU isn't hosed during startup.
  * These three parameters are controlled by java environmental variables and 
  * can be adjusted via:
  *  -Dcrypto.yk.precalc.min=40 -Dcrypto.yk.precalc.max=100 -Dcrypto.yk.precalc.delay=60000
@@ -39,51 +37,36 @@ import net.i2p.util.RandomSource;
  * @author jrandom
  */
 class YKGenerator {
-    private final static Log _log = new Log(YKGenerator.class);
-    private static int MIN_NUM_BUILDERS = -1;
-    private static int MAX_NUM_BUILDERS = -1;
-    private static int CALC_DELAY = -1;
-    /* FIXME final type if you are to syncronize FIXME */
-    private static volatile List _values = new ArrayList(50); // list of BigInteger[] values (y and k)
-    private static Thread _precalcThread = null;
+    //private final static Log _log = new Log(YKGenerator.class);
+    private static final int MIN_NUM_BUILDERS;
+    private static final int MAX_NUM_BUILDERS;
+    private static final int CALC_DELAY;
+    private static final LinkedBlockingQueue<BigInteger[]> _values = new LinkedBlockingQueue(50); // list of BigInteger[] values (y and k)
+    private static final Thread _precalcThread;
+    private static final I2PAppContext ctx;
 
     public final static String PROP_YK_PRECALC_MIN = "crypto.yk.precalc.min";
     public final static String PROP_YK_PRECALC_MAX = "crypto.yk.precalc.max";
     public final static String PROP_YK_PRECALC_DELAY = "crypto.yk.precalc.delay";
-    public final static String DEFAULT_YK_PRECALC_MIN = "10";
-    public final static String DEFAULT_YK_PRECALC_MAX = "30";
-    public final static String DEFAULT_YK_PRECALC_DELAY = "10000";
+    public final static int DEFAULT_YK_PRECALC_MIN = 20;
+    public final static int DEFAULT_YK_PRECALC_MAX = 50;
+    public final static int DEFAULT_YK_PRECALC_DELAY = 200;
 
     /** check every 30 seconds whether we have less than the minimum */
-    private final static long CHECK_DELAY = 30 * 1000;
+    private static long CHECK_DELAY = 30 * 1000;
 
     static {
-        I2PAppContext ctx = I2PAppContext.getGlobalContext();
-        try {
-            int val = Integer.parseInt(ctx.getProperty(PROP_YK_PRECALC_MIN, DEFAULT_YK_PRECALC_MIN));
-            MIN_NUM_BUILDERS = val;
-        } catch (Throwable t) {
-            int val = Integer.parseInt(DEFAULT_YK_PRECALC_MIN);
-            MIN_NUM_BUILDERS = val;
-        }
-        try {
-            int val = Integer.parseInt(ctx.getProperty(PROP_YK_PRECALC_MAX, DEFAULT_YK_PRECALC_MAX));
-            MAX_NUM_BUILDERS = val;
-        } catch (Throwable t) {
-            int val = Integer.parseInt(DEFAULT_YK_PRECALC_MAX);
-            MAX_NUM_BUILDERS = val;
-        }
-        try {
-            int val = Integer.parseInt(ctx.getProperty(PROP_YK_PRECALC_DELAY, DEFAULT_YK_PRECALC_DELAY));
-            CALC_DELAY = val;
-        } catch (Throwable t) {
-            int val = Integer.parseInt(DEFAULT_YK_PRECALC_DELAY);
-            CALC_DELAY = val;
-        }
+        ctx = I2PAppContext.getGlobalContext();
+        MIN_NUM_BUILDERS = ctx.getProperty(PROP_YK_PRECALC_MIN, DEFAULT_YK_PRECALC_MIN);
+        MAX_NUM_BUILDERS = ctx.getProperty(PROP_YK_PRECALC_MAX, DEFAULT_YK_PRECALC_MAX);
+        CALC_DELAY = ctx.getProperty(PROP_YK_PRECALC_DELAY, DEFAULT_YK_PRECALC_DELAY);
+
+        //if (_log.shouldLog(Log.DEBUG))
+        //    _log.debug("ElGamal YK Precalc (minimum: " + MIN_NUM_BUILDERS + " max: " + MAX_NUM_BUILDERS + ", delay: "
+        //               + CALC_DELAY + ")");
 
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug("ElGamal YK Precalc (minimum: " + MIN_NUM_BUILDERS + " max: " + MAX_NUM_BUILDERS + ", delay: "
-                       + CALC_DELAY + ")");
+        ctx.statManager().createRateStat("crypto.YKUsed", "Need a YK from the queue", "Encryption", new long[] { 60*60*1000 });
+        ctx.statManager().createRateStat("crypto.YKEmpty", "YK queue empty", "Encryption", new long[] { 60*60*1000 });
 
         _precalcThread = new I2PThread(new YKPrecalcRunner(MIN_NUM_BUILDERS, MAX_NUM_BUILDERS));
         _precalcThread.setName("YK Precalc");
@@ -93,45 +76,36 @@ class YKGenerator {
     }
 
     private static final int getSize() {
-        synchronized (_values) {
-            return _values.size();
-        }
+        return _values.size();
     }
 
-    private static final int addValues(BigInteger yk[]) {
-        int sz = 0;
-        synchronized (_values) {
-            _values.add(yk);
-            sz = _values.size();
-        }
-        return sz;
+    /** @return true if successful, false if full */
+    private static final boolean addValues(BigInteger yk[]) {
+        return _values.offer(yk);
     }
 
+    /** @return rv[0] = Y; rv[1] = K */
     public static BigInteger[] getNextYK() {
-        if (true) {
-            synchronized (_values) {
-                if (!_values.isEmpty()) {
-                    if (_log.shouldLog(Log.DEBUG))
-                        _log.debug("Sufficient precalculated YK values - fetch the existing");
-                    return (BigInteger[]) _values.remove(0);
-                }
-            }
-        }
-        if (_log.shouldLog(Log.INFO)) _log.info("Insufficient precalculated YK values - create a new one");
+        ctx.statManager().addRateData("crypto.YKUsed", 1, 0);
+        BigInteger[] rv = _values.poll();
+        if (rv != null)
+            return rv;
+        ctx.statManager().addRateData("crypto.YKEmpty", 1, 0);
         return generateYK();
     }
 
     private final static BigInteger _two = new NativeBigInteger(1, new byte[] { 0x02});
 
+    /** @return rv[0] = Y; rv[1] = K */
     private static final BigInteger[] generateYK() {
         NativeBigInteger k = null;
         BigInteger y = null;
-        long t0 = 0;
-        long t1 = 0;
+        //long t0 = 0;
+        //long t1 = 0;
         while (k == null) {
-            t0 = Clock.getInstance().now();
-            k = new NativeBigInteger(KeyGenerator.PUBKEY_EXPONENT_SIZE, RandomSource.getInstance());
-            t1 = Clock.getInstance().now();
+            //t0 = Clock.getInstance().now();
+            k = new NativeBigInteger(KeyGenerator.PUBKEY_EXPONENT_SIZE, ctx.random());
+            //t1 = Clock.getInstance().now();
             if (BigInteger.ZERO.compareTo(k) == 0) {
                 k = null;
                 continue;
@@ -139,39 +113,31 @@ class YKGenerator {
             BigInteger kPlus2 = k.add(_two);
             if (kPlus2.compareTo(CryptoConstants.elgp) > 0) k = null;
         }
-        long t2 = Clock.getInstance().now();
+        //long t2 = Clock.getInstance().now();
         y = CryptoConstants.elgg.modPow(k, CryptoConstants.elgp);
 
         BigInteger yk[] = new BigInteger[2];
         yk[0] = y;
         yk[1] = k;
 
-        long diff = t2 - t0;
-        if (diff > 1000) {
-            if (_log.shouldLog(Log.WARN)) _log.warn("Took too long to generate YK value for ElGamal (" + diff + "ms)");
-        }
+        //long diff = t2 - t0;
+        //if (diff > 1000) {
+        //    if (_log.shouldLog(Log.WARN)) _log.warn("Took too long to generate YK value for ElGamal (" + diff + "ms)");
+        //}
 
         return yk;
     }
 
     public static void main(String args[]) {
-        RandomSource.getInstance().nextBoolean(); // warm it up
-        try {
-            Thread.sleep(20 * 1000);
-        } catch (InterruptedException ie) { // nop
-        }
-        _log.debug("\n\n\n\nBegin test\n");
+        System.out.println("\n\n\n\nBegin test\n");
         long negTime = 0;
         for (int i = 0; i < 5; i++) {
             long startNeg = Clock.getInstance().now();
             getNextYK();
             long endNeg = Clock.getInstance().now();
+            negTime += endNeg - startNeg;
         }
-        _log.debug("YK fetch time for 5 runs: " + negTime + " @ " + negTime / 5l + "ms each");
-        try {
-            Thread.sleep(30 * 1000);
-        } catch (InterruptedException ie) { // nop
-        }
+        System.out.println("YK fetch time for 5 runs: " + negTime + " @ " + negTime / 5l + "ms each");
     }
 
     private static class YKPrecalcRunner implements Runnable {
@@ -186,15 +152,21 @@ class YKGenerator {
         public void run() {
             while (true) {
                 int curSize = 0;
-                long start = Clock.getInstance().now();
+                //long start = Clock.getInstance().now();
                 int startSize = getSize();
+                // Adjust delay
+                if (startSize <= (_minSize / 2) && CHECK_DELAY > 1000)
+                    CHECK_DELAY -= 1000;
+                else if (startSize > (_minSize * 2) && CHECK_DELAY < 60000)
+                         CHECK_DELAY += 1000;
                 curSize = startSize;
-                while (curSize < _minSize) {
-                    while (curSize < _maxSize) {
-                        long begin = Clock.getInstance().now();
-                        curSize = addValues(generateYK());
-                        long end = Clock.getInstance().now();
-                        if (_log.shouldLog(Log.DEBUG)) _log.debug("Precalculated YK value in " + (end - begin) + "ms");
+                if (curSize < _minSize) {
+                    for (int i = curSize; i < _maxSize; i++) {
+                        //long begin = Clock.getInstance().now();
+                        if (!addValues(generateYK()))
+                            break;
+                        //long end = Clock.getInstance().now();
+                        //if (_log.shouldLog(Log.DEBUG)) _log.debug("Precalculated YK value in " + (end - begin) + "ms");
                         // for some relief...
                         try {
                             Thread.sleep(CALC_DELAY);
@@ -202,14 +174,14 @@ class YKGenerator {
                         }
                     }
                 }
-                long end = Clock.getInstance().now();
-                int numCalc = curSize - startSize;
-                if (numCalc > 0) {
-                    if (_log.shouldLog(Log.DEBUG))
-                        _log.debug("Precalced " + numCalc + " to " + curSize + " in "
-                                   + (end - start - CALC_DELAY * numCalc) + "ms (not counting "
-                                   + (CALC_DELAY * numCalc) + "ms relief).  now sleeping");
-                }
+                //long end = Clock.getInstance().now();
+                //int numCalc = curSize - startSize;
+                //if (numCalc > 0) {
+                //    if (_log.shouldLog(Log.DEBUG))
+                //        _log.debug("Precalced " + numCalc + " to " + curSize + " in "
+                //                   + (end - start - CALC_DELAY * numCalc) + "ms (not counting "
+                //                   + (CALC_DELAY * numCalc) + "ms relief).  now sleeping");
+                //}
                 try {
                     Thread.sleep(CHECK_DELAY);
                 } catch (InterruptedException ie) { // nop
@@ -217,4 +189,4 @@ class YKGenerator {
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/core/java/src/net/i2p/data/DataHelper.java b/core/java/src/net/i2p/data/DataHelper.java
index 0743defde87cedd8b630347ce0a52d630c18f54d..bb1ffe55c0c8839b36f8ca833da10f2a664d5975 100644
--- a/core/java/src/net/i2p/data/DataHelper.java
+++ b/core/java/src/net/i2p/data/DataHelper.java
@@ -17,7 +17,6 @@ import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -42,6 +41,7 @@ import net.i2p.util.ByteCache;
 import net.i2p.util.OrderedProperties;
 import net.i2p.util.ReusableGZIPInputStream;
 import net.i2p.util.ReusableGZIPOutputStream;
+import net.i2p.util.SecureFileOutputStream;
 
 /**
  * Defines some simple IO routines for dealing with marshalling data structures
@@ -339,11 +339,12 @@ public class DataHelper {
     /**
      * Writes the props to the file, unsorted (unless props is an OrderedProperties)
      * Note that this does not escape the \r or \n that are unescaped in loadProps() above.
+     * As of 0.8.1, file will be mode 600.
      */
     public static void storeProps(Properties props, File file) throws IOException {
         PrintWriter out = null;
         try {
-            out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8")));
+            out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(file), "UTF-8")));
             out.println("# NOTE: This I2P config file must use UTF-8 encoding");
             for (Iterator iter = props.keySet().iterator(); iter.hasNext(); ) {
                 String name = (String)iter.next();
diff --git a/core/java/src/net/i2p/data/Hash.java b/core/java/src/net/i2p/data/Hash.java
index 83579d11663d2d8f16cb412a079cbab2e24ac115..afd173c2454751219d6e28dbec32246935579c8b 100644
--- a/core/java/src/net/i2p/data/Hash.java
+++ b/core/java/src/net/i2p/data/Hash.java
@@ -31,6 +31,7 @@ public class Hash extends DataStructureImpl {
     public Hash() {
     }
 
+    /** @throws IllegalArgumentException if data is not 32 bytes (null is ok) */
     public Hash(byte data[]) {
         setData(data);
     }
@@ -39,7 +40,10 @@ public class Hash extends DataStructureImpl {
         return _data;
     }
 
+    /** @throws IllegalArgumentException if data is not 32 bytes (null is ok) */
     public void setData(byte[] data) {
+        if (data != null && data.length != HASH_LENGTH)
+            throw new IllegalArgumentException("Hash must be 32 bytes");
         _data = data;
         _stringified = null;
         _base64ed = null;
diff --git a/core/java/src/net/i2p/data/RouterAddress.java b/core/java/src/net/i2p/data/RouterAddress.java
index 9d199e57401add70fb9919555d489ee3e78918fc..1b4d6438f5e518caf134c9f4a9a61183f7aaf96e 100644
--- a/core/java/src/net/i2p/data/RouterAddress.java
+++ b/core/java/src/net/i2p/data/RouterAddress.java
@@ -14,8 +14,11 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.Date;
 import java.util.Iterator;
+import java.util.Map;
 import java.util.Properties;
 
+import net.i2p.util.OrderedProperties;
+
 /**
  * Defines a method of communicating with a router
  *
@@ -28,7 +31,7 @@ public class RouterAddress extends DataStructureImpl {
     private Properties _options;
 
     public RouterAddress() {
-        setCost(-1);
+        _cost = -1;
     }
 
     /**
@@ -134,18 +137,27 @@ public class RouterAddress extends DataStructureImpl {
         return DataHelper.hashCode(_transportStyle);
     }
     
+    /**
+     *  This is used on peers.jsp so sort options so it looks better.
+     *  We don't just use OrderedProperties for _options because DataHelper.writeProperties()
+     *  sorts also.
+     */
     @Override
     public String toString() {
-        StringBuilder buf = new StringBuilder(64);
+        StringBuilder buf = new StringBuilder(128);
         buf.append("[RouterAddress: ");
-        buf.append("\n\tTransportStyle: ").append(getTransportStyle());
-        buf.append("\n\tCost: ").append(getCost());
-        buf.append("\n\tExpiration: ").append(getExpiration());
-        buf.append("\n\tOptions: #: ").append(getOptions().size());
-        for (Iterator iter = getOptions().keySet().iterator(); iter.hasNext();) {
-            String key = (String) iter.next();
-            String val = getOptions().getProperty(key);
-            buf.append("\n\t\t[").append(key).append("] = [").append(val).append("]");
+        buf.append("\n\tTransportStyle: ").append(_transportStyle);
+        buf.append("\n\tCost: ").append(_cost);
+        buf.append("\n\tExpiration: ").append(_expiration);
+        if (_options != null) {
+            buf.append("\n\tOptions: #: ").append(_options.size());
+            Properties p = new OrderedProperties();
+            p.putAll(_options);
+            for (Map.Entry e : p.entrySet()) {
+                String key = (String) e.getKey();
+                String val = (String) e.getValue();
+                buf.append("\n\t\t[").append(key).append("] = [").append(val).append("]");
+            }
         }
         buf.append("]");
         return buf.toString();
diff --git a/core/java/src/net/i2p/data/i2cp/AbuseReason.java b/core/java/src/net/i2p/data/i2cp/AbuseReason.java
index f631fa591198c67bbb827b9d47b3af7122bd4018..99388faf201866938cc9a244ef94a610372c7cb5 100644
--- a/core/java/src/net/i2p/data/i2cp/AbuseReason.java
+++ b/core/java/src/net/i2p/data/i2cp/AbuseReason.java
@@ -49,16 +49,16 @@ public class AbuseReason extends DataStructureImpl {
     @Override
     public boolean equals(Object object) {
         if ((object == null) || !(object instanceof AbuseReason)) return false;
-        return DataHelper.eq(getReason(), ((AbuseReason) object).getReason());
+        return DataHelper.eq(_reason, ((AbuseReason) object).getReason());
     }
 
     @Override
     public int hashCode() {
-        return DataHelper.hashCode(getReason());
+        return DataHelper.hashCode(_reason);
     }
 
     @Override
     public String toString() {
-        return "[AbuseReason: " + getReason() + "]";
+        return "[AbuseReason: " + _reason + "]";
     }
 }
diff --git a/core/java/src/net/i2p/data/i2cp/AbuseSeverity.java b/core/java/src/net/i2p/data/i2cp/AbuseSeverity.java
index 69cba7d2470bd8db4f685843dcaedbccd11b7913..bc3b9bbc9769705a7b1a7ae218725c4a92dd42d9 100644
--- a/core/java/src/net/i2p/data/i2cp/AbuseSeverity.java
+++ b/core/java/src/net/i2p/data/i2cp/AbuseSeverity.java
@@ -28,7 +28,7 @@ public class AbuseSeverity extends DataStructureImpl {
     private int _severityId;
 
     public AbuseSeverity() {
-        setSeverity(-1);
+        _severityId = -1;
     }
 
     public int getSeverity() {
@@ -56,11 +56,11 @@ public class AbuseSeverity extends DataStructureImpl {
 
     @Override
     public int hashCode() {
-        return getSeverity();
+        return _severityId;
     }
 
     @Override
     public String toString() {
-        return "[AbuseSeverity: " + getSeverity() + "]";
+        return "[AbuseSeverity: " + _severityId + "]";
     }
 }
diff --git a/core/java/src/net/i2p/data/i2cp/CreateSessionMessage.java b/core/java/src/net/i2p/data/i2cp/CreateSessionMessage.java
index 1b8004ce00a61930801b81662bd49011471f0bbe..8590a554c061fe22ce162d60082bb552b709e7ee 100644
--- a/core/java/src/net/i2p/data/i2cp/CreateSessionMessage.java
+++ b/core/java/src/net/i2p/data/i2cp/CreateSessionMessage.java
@@ -27,11 +27,11 @@ public class CreateSessionMessage extends I2CPMessageImpl {
     private SessionConfig _sessionConfig;
 
     public CreateSessionMessage(SessionConfig config) {
-        setSessionConfig(config);
+        _sessionConfig = config;
     }
 
     public CreateSessionMessage() {
-        setSessionConfig(new SessionConfig());
+        _sessionConfig = new SessionConfig();
     }
 
     public SessionConfig getSessionConfig() {
@@ -75,7 +75,7 @@ public class CreateSessionMessage extends I2CPMessageImpl {
     public boolean equals(Object object) {
         if ((object != null) && (object instanceof CreateSessionMessage)) {
             CreateSessionMessage msg = (CreateSessionMessage) object;
-            return DataHelper.eq(getSessionConfig(), msg.getSessionConfig());
+            return DataHelper.eq(_sessionConfig, msg.getSessionConfig());
         }
             
         return false;
@@ -85,7 +85,7 @@ public class CreateSessionMessage extends I2CPMessageImpl {
     public String toString() {
         StringBuilder buf = new StringBuilder();
         buf.append("[CreateSessionMessage: ");
-        buf.append("\n\tConfig: ").append(getSessionConfig());
+        buf.append("\n\tConfig: ").append(_sessionConfig);
         buf.append("]");
         return buf.toString();
     }
diff --git a/core/java/src/net/i2p/data/i2cp/DestroySessionMessage.java b/core/java/src/net/i2p/data/i2cp/DestroySessionMessage.java
index 8b4db852f77cde845f902025f4aa02a825c7c70c..edb07ae41674e0b8898ed7e8f73d81f415a00793 100644
--- a/core/java/src/net/i2p/data/i2cp/DestroySessionMessage.java
+++ b/core/java/src/net/i2p/data/i2cp/DestroySessionMessage.java
@@ -69,7 +69,7 @@ public class DestroySessionMessage extends I2CPMessageImpl {
     public boolean equals(Object object) {
         if ((object != null) && (object instanceof DestroySessionMessage)) {
             DestroySessionMessage msg = (DestroySessionMessage) object;
-            return DataHelper.eq(getSessionId(), msg.getSessionId());
+            return DataHelper.eq(_sessionId, msg.getSessionId());
         }
             
         return false;
@@ -86,7 +86,7 @@ public class DestroySessionMessage extends I2CPMessageImpl {
     public String toString() {
         StringBuilder buf = new StringBuilder();
         buf.append("[DestroySessionMessage: ");
-        buf.append("\n\tSessionId: ").append(getSessionId());
+        buf.append("\n\tSessionId: ").append(_sessionId);
         buf.append("]");
         return buf.toString();
     }
diff --git a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java
index 739f7823ebb110b861e3cbc732a862cbacacd7ad..aaa35427b38ec56e6e69b5c3d9aed162cf34285c 100644
--- a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java
+++ b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java
@@ -41,7 +41,7 @@ public class I2CPMessageHandler {
         try {
             if (length < 0) throw new I2CPMessageException("Invalid message length specified");
             int type = (int) DataHelper.readLong(in, 1);
-            I2CPMessage msg = createMessage(in, length, type);
+            I2CPMessage msg = createMessage(type);
             msg.readMessage(in, length, type);
             return msg;
         } catch (DataFormatException dfe) {
@@ -53,8 +53,8 @@ public class I2CPMessageHandler {
      * Yes, this is fairly ugly, but its the only place it ever happens.  
      *
      */
-    private static I2CPMessage createMessage(InputStream in, int length, int type) throws IOException,
-                                                                                  I2CPMessageException {
+    private static I2CPMessage createMessage(int type) throws IOException,
+                                                       I2CPMessageException {
         switch (type) {
         case CreateLeaseSetMessage.MESSAGE_TYPE:
             return new CreateLeaseSetMessage();
@@ -101,6 +101,7 @@ public class I2CPMessageHandler {
         }
     }
 
+/***
     public static void main(String args[]) {
         try {
             I2CPMessage msg = readMessage(new FileInputStream(args[0]));
@@ -109,4 +110,5 @@ public class I2CPMessageHandler {
             e.printStackTrace();
         }
     }
+***/
 }
diff --git a/core/java/src/net/i2p/data/i2cp/MessageId.java b/core/java/src/net/i2p/data/i2cp/MessageId.java
index d1db7376137484e9f1da94fa38bd3548cae5632f..365dc6dbb7928685a634082f8377033d948b5154 100644
--- a/core/java/src/net/i2p/data/i2cp/MessageId.java
+++ b/core/java/src/net/i2p/data/i2cp/MessageId.java
@@ -27,10 +27,10 @@ public class MessageId extends DataStructureImpl {
     private long _messageId;
 
     public MessageId() {
-        setMessageId(-1);
+        _messageId = -1;
     }
     public MessageId(long id) {
-        setMessageId(id);
+        _messageId = id;
     }
 
     public long getMessageId() {
@@ -58,11 +58,11 @@ public class MessageId extends DataStructureImpl {
 
     @Override
     public int hashCode() {
-        return (int)getMessageId();
+        return (int)_messageId;
     }
 
     @Override
     public String toString() {
-        return "[MessageId: " + getMessageId() + "]";
+        return "[MessageId: " + _messageId + "]";
     }
 }
diff --git a/core/java/src/net/i2p/data/i2cp/MessagePayloadMessage.java b/core/java/src/net/i2p/data/i2cp/MessagePayloadMessage.java
index f06fe86f326cf2b2c945f3d2a44c4846020f55a1..d9739ee585f2b88cf7ae2041995c43f8d45c7faa 100644
--- a/core/java/src/net/i2p/data/i2cp/MessagePayloadMessage.java
+++ b/core/java/src/net/i2p/data/i2cp/MessagePayloadMessage.java
@@ -29,8 +29,8 @@ public class MessagePayloadMessage extends I2CPMessageImpl {
     private Payload _payload;
 
     public MessagePayloadMessage() {
-        setSessionId(-1);
-        setMessageId(-1);
+        _sessionId = -1;
+        _messageId = -1;
     }
 
     public long getSessionId() {
@@ -113,7 +113,7 @@ public class MessagePayloadMessage extends I2CPMessageImpl {
             MessagePayloadMessage msg = (MessagePayloadMessage) object;
             return _sessionId == msg.getSessionId()
                    && _messageId == msg.getMessageId()
-                   && DataHelper.eq(getPayload(), msg.getPayload());
+                   && DataHelper.eq(_payload, msg.getPayload());
         }
             
         return false;
@@ -123,9 +123,9 @@ public class MessagePayloadMessage extends I2CPMessageImpl {
     public String toString() {
         StringBuilder buf = new StringBuilder();
         buf.append("[MessagePayloadMessage: ");
-        buf.append("\n\tSessionId: ").append(getSessionId());
-        buf.append("\n\tMessageId: ").append(getMessageId());
-        buf.append("\n\tPayload: ").append(getPayload());
+        buf.append("\n\tSessionId: ").append(_sessionId);
+        buf.append("\n\tMessageId: ").append(_messageId);
+        buf.append("\n\tPayload: ").append(_payload);
         buf.append("]");
         return buf.toString();
     }
diff --git a/core/java/src/net/i2p/data/i2cp/MessageStatusMessage.java b/core/java/src/net/i2p/data/i2cp/MessageStatusMessage.java
index 67fde8134b9bdf0b034a831a017e9328307ed39c..933a80785a79f6bfb664ff3348bfe95a554f9d73 100644
--- a/core/java/src/net/i2p/data/i2cp/MessageStatusMessage.java
+++ b/core/java/src/net/i2p/data/i2cp/MessageStatusMessage.java
@@ -32,17 +32,19 @@ public class MessageStatusMessage extends I2CPMessageImpl {
 
     public final static int STATUS_AVAILABLE = 0;
     public final static int STATUS_SEND_ACCEPTED = 1;
+    /** unused */
     public final static int STATUS_SEND_BEST_EFFORT_SUCCESS = 2;
+    /** unused */
     public final static int STATUS_SEND_BEST_EFFORT_FAILURE = 3;
     public final static int STATUS_SEND_GUARANTEED_SUCCESS = 4;
     public final static int STATUS_SEND_GUARANTEED_FAILURE = 5;
 
     public MessageStatusMessage() {
-        setSessionId(-1);
-        setStatus(-1);
-        setMessageId(-1);
-        setSize(-1);
-        setNonce(-1);
+        _sessionId = -1;
+        _status = -1;
+        _messageId = -1;
+        _size = -1;
+        _nonce = -1;
     }
 
     public long getSessionId() {
@@ -169,11 +171,11 @@ public class MessageStatusMessage extends I2CPMessageImpl {
     public String toString() {
         StringBuilder buf = new StringBuilder();
         buf.append("[MessageStatusMessage: ");
-        buf.append("\n\tSessionId: ").append(getSessionId());
-        buf.append("\n\tNonce: ").append(getNonce());
-        buf.append("\n\tMessageId: ").append(getMessageId());
-        buf.append("\n\tStatus: ").append(getStatusString(getStatus()));
-        buf.append("\n\tSize: ").append(getSize());
+        buf.append("\n\tSessionId: ").append(_sessionId);
+        buf.append("\n\tNonce: ").append(_nonce);
+        buf.append("\n\tMessageId: ").append(_messageId);
+        buf.append("\n\tStatus: ").append(getStatusString(_status));
+        buf.append("\n\tSize: ").append(_size);
         buf.append("]");
         return buf.toString();
     }
diff --git a/core/java/src/net/i2p/data/i2cp/ReceiveMessageBeginMessage.java b/core/java/src/net/i2p/data/i2cp/ReceiveMessageBeginMessage.java
index 6ad5e2745ddbaeb02b2d75be2e695e9fa2142c42..15bd6779dac81f17ab43ffe9bff2fd0341ea3ce6 100644
--- a/core/java/src/net/i2p/data/i2cp/ReceiveMessageBeginMessage.java
+++ b/core/java/src/net/i2p/data/i2cp/ReceiveMessageBeginMessage.java
@@ -28,8 +28,8 @@ public class ReceiveMessageBeginMessage extends I2CPMessageImpl {
     private long _messageId;
 
     public ReceiveMessageBeginMessage() {
-        setSessionId(-1);
-        setMessageId(-1);
+        _sessionId = -1;
+        _messageId = -1;
     }
 
     public long getSessionId() {
@@ -103,8 +103,8 @@ public class ReceiveMessageBeginMessage extends I2CPMessageImpl {
     public String toString() {
         StringBuilder buf = new StringBuilder();
         buf.append("[ReceiveMessageBeginMessage: ");
-        buf.append("\n\tSessionId: ").append(getSessionId());
-        buf.append("\n\tMessageId: ").append(getMessageId());
+        buf.append("\n\tSessionId: ").append(_sessionId);
+        buf.append("\n\tMessageId: ").append(_messageId);
         buf.append("]");
         return buf.toString();
     }
diff --git a/core/java/src/net/i2p/data/i2cp/ReceiveMessageEndMessage.java b/core/java/src/net/i2p/data/i2cp/ReceiveMessageEndMessage.java
index 79cf883822e76b9722946c6c33dd322d9e95662c..718fd8b73985083235b6170abecb988083d3a0ff 100644
--- a/core/java/src/net/i2p/data/i2cp/ReceiveMessageEndMessage.java
+++ b/core/java/src/net/i2p/data/i2cp/ReceiveMessageEndMessage.java
@@ -27,8 +27,8 @@ public class ReceiveMessageEndMessage extends I2CPMessageImpl {
     private long _messageId;
 
     public ReceiveMessageEndMessage() {
-        setSessionId(-1);
-        setMessageId(-1);
+        _sessionId = -1;
+        _messageId = -1;
     }
 
     public long getSessionId() {
@@ -87,8 +87,8 @@ public class ReceiveMessageEndMessage extends I2CPMessageImpl {
     public String toString() {
         StringBuilder buf = new StringBuilder();
         buf.append("[ReceiveMessageEndMessage: ");
-        buf.append("\n\tSessionId: ").append(getSessionId());
-        buf.append("\n\tMessageId: ").append(getMessageId());
+        buf.append("\n\tSessionId: ").append(_sessionId);
+        buf.append("\n\tMessageId: ").append(_messageId);
         buf.append("]");
         return buf.toString();
     }
diff --git a/core/java/src/net/i2p/data/i2cp/ReconfigureSessionMessage.java b/core/java/src/net/i2p/data/i2cp/ReconfigureSessionMessage.java
index 9400091bede49bd3c7fcef052182e009a0f3710a..336fb3ed5b1703a47314239deeff3f46d58dc3fc 100644
--- a/core/java/src/net/i2p/data/i2cp/ReconfigureSessionMessage.java
+++ b/core/java/src/net/i2p/data/i2cp/ReconfigureSessionMessage.java
@@ -81,8 +81,8 @@ public class ReconfigureSessionMessage extends I2CPMessageImpl {
     public boolean equals(Object object) {
         if ((object != null) && (object instanceof ReconfigureSessionMessage)) {
             ReconfigureSessionMessage msg = (ReconfigureSessionMessage) object;
-            return DataHelper.eq(getSessionId(), msg.getSessionId())
-                   && DataHelper.eq(getSessionConfig(), msg.getSessionConfig());
+            return DataHelper.eq(_sessionId, msg.getSessionId())
+                   && DataHelper.eq(_sessionConfig, msg.getSessionConfig());
         }
             
         return false;
@@ -92,8 +92,8 @@ public class ReconfigureSessionMessage extends I2CPMessageImpl {
     public String toString() {
         StringBuilder buf = new StringBuilder();
         buf.append("[ReconfigureSessionMessage: ");
-        buf.append("\n\tSessionId: ").append(getSessionId());
-        buf.append("\n\tSessionConfig: ").append(getSessionConfig());
+        buf.append("\n\tSessionId: ").append(_sessionId);
+        buf.append("\n\tSessionConfig: ").append(_sessionConfig);
         buf.append("]");
         return buf.toString();
     }
diff --git a/core/java/src/net/i2p/data/i2cp/RequestLeaseSetMessage.java b/core/java/src/net/i2p/data/i2cp/RequestLeaseSetMessage.java
index ef877afe5e2c188fbd2524745ee793071722b566..32241e833861de22d05ad4fa36b6ba09e98673c8 100644
--- a/core/java/src/net/i2p/data/i2cp/RequestLeaseSetMessage.java
+++ b/core/java/src/net/i2p/data/i2cp/RequestLeaseSetMessage.java
@@ -30,7 +30,7 @@ import net.i2p.data.TunnelId;
 public class RequestLeaseSetMessage extends I2CPMessageImpl {
     public final static int MESSAGE_TYPE = 21;
     private SessionId _sessionId;
-    private List _endpoints;
+    private List<TunnelEndpoint> _endpoints;
     private Date _end;
 
     public RequestLeaseSetMessage() {
@@ -51,12 +51,12 @@ public class RequestLeaseSetMessage extends I2CPMessageImpl {
 
     public Hash getRouter(int endpoint) {
         if ((endpoint < 0) || (_endpoints.size() < endpoint)) return null;
-        return ((TunnelEndpoint) _endpoints.get(endpoint)).getRouter();
+        return _endpoints.get(endpoint).getRouter();
     }
 
     public TunnelId getTunnelId(int endpoint) {
         if ((endpoint < 0) || (_endpoints.size() < endpoint)) return null;
-        return ((TunnelEndpoint) _endpoints.get(endpoint)).getTunnelId();
+        return _endpoints.get(endpoint).getTunnelId();
     }
 
     /** @deprecated unused - presumably he meant remove? */
@@ -159,8 +159,6 @@ public class RequestLeaseSetMessage extends I2CPMessageImpl {
         private TunnelId _tunnelId;
 
         public TunnelEndpoint() {
-            _router = null;
-            _tunnelId = null;
         }
 
         public TunnelEndpoint(Hash router, TunnelId id) {
diff --git a/core/java/src/net/i2p/data/i2cp/SessionConfig.java b/core/java/src/net/i2p/data/i2cp/SessionConfig.java
index b96918a459d832f544545f929ceb22a95c9bab9f..6534e0dbbdeb2bd61c7ca18103653892e047c6ea 100644
--- a/core/java/src/net/i2p/data/i2cp/SessionConfig.java
+++ b/core/java/src/net/i2p/data/i2cp/SessionConfig.java
@@ -168,8 +168,8 @@ public class SessionConfig extends DataStructureImpl {
 
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         try {
-            _log.debug("PubKey size for destination: " + _destination.getPublicKey().getData().length);
-            _log.debug("SigningKey size for destination: " + _destination.getSigningPublicKey().getData().length);
+            //_log.debug("PubKey size for destination: " + _destination.getPublicKey().getData().length);
+            //_log.debug("SigningKey size for destination: " + _destination.getSigningPublicKey().getData().length);
             _destination.writeBytes(out);
             DataHelper.writeProperties(out, _options, true);  // UTF-8
             DataHelper.writeDate(out, _creationDate);
diff --git a/core/java/src/net/i2p/data/i2cp/SessionId.java b/core/java/src/net/i2p/data/i2cp/SessionId.java
index f79589c42b25dcd6ba50abc72f98fbac03364bba..708ba9c46026a0b3ff8ed6ebc085047a7bfda6df 100644
--- a/core/java/src/net/i2p/data/i2cp/SessionId.java
+++ b/core/java/src/net/i2p/data/i2cp/SessionId.java
@@ -26,7 +26,7 @@ public class SessionId extends DataStructureImpl {
     private int _sessionId;
 
     public SessionId() {
-        setSessionId(-1);
+        _sessionId = -1;
     }
 
     public int getSessionId() {
@@ -49,16 +49,16 @@ public class SessionId extends DataStructureImpl {
     @Override
     public boolean equals(Object obj) {
         if ((obj == null) || !(obj instanceof SessionId)) return false;
-        return getSessionId() == ((SessionId) obj).getSessionId();
+        return _sessionId == ((SessionId) obj).getSessionId();
     }
 
     @Override
     public int hashCode() {
-        return getSessionId();
+        return _sessionId;
     }
 
     @Override
     public String toString() {
-        return "[SessionId: " + getSessionId() + "]";
+        return "[SessionId: " + _sessionId + "]";
     }
 }
diff --git a/core/java/src/net/i2p/data/i2cp/SetDateMessage.java b/core/java/src/net/i2p/data/i2cp/SetDateMessage.java
index f0abce1a51d57847e1caabf8540737ca214e9112..9f6633b6ddda1f1d87b6bea65be90d52a1cae6b1 100644
--- a/core/java/src/net/i2p/data/i2cp/SetDateMessage.java
+++ b/core/java/src/net/i2p/data/i2cp/SetDateMessage.java
@@ -28,7 +28,7 @@ public class SetDateMessage extends I2CPMessageImpl {
 
     public SetDateMessage() {
         super();
-        setDate(new Date(Clock.getInstance().now()));
+        _date = new Date(Clock.getInstance().now());
     }
 
     public Date getDate() {
@@ -70,7 +70,7 @@ public class SetDateMessage extends I2CPMessageImpl {
     public boolean equals(Object object) {
         if ((object != null) && (object instanceof SetDateMessage)) {
             SetDateMessage msg = (SetDateMessage) object;
-            return DataHelper.eq(getDate(), msg.getDate());
+            return DataHelper.eq(_date, msg.getDate());
         }
             
         return false;
@@ -80,7 +80,7 @@ public class SetDateMessage extends I2CPMessageImpl {
     public String toString() {
         StringBuilder buf = new StringBuilder();
         buf.append("[SetDateMessage");
-        buf.append("\n\tDate: ").append(getDate());
+        buf.append("\n\tDate: ").append(_date);
         buf.append("]");
         return buf.toString();
     }
diff --git a/core/java/src/net/i2p/util/EepGet.java b/core/java/src/net/i2p/util/EepGet.java
index 3fec768a5857cf153a9bc7d0606019ffcd56d54a..5b3f25c1925103246a43e741f133965feb55d8bc 100644
--- a/core/java/src/net/i2p/util/EepGet.java
+++ b/core/java/src/net/i2p/util/EepGet.java
@@ -55,11 +55,11 @@ public class EepGet {
     protected long _bytesTransferred;
     protected long _bytesRemaining;
     protected int _currentAttempt;
-    private String _etag;
-    private String _lastModified;
+    protected String _etag;
+    protected String _lastModified;
     protected boolean _encodingChunked;
     protected boolean _notModified;
-    private String _contentType;
+    protected String _contentType;
     protected boolean _transferFailed;
     protected boolean _headersRead;
     protected boolean _aborted;
@@ -537,6 +537,15 @@ public class EepGet {
             if (_redirects > 5)
                 throw new IOException("Too many redirects: to " + _redirectLocation);
             if (_log.shouldLog(Log.INFO)) _log.info("Redirecting to " + _redirectLocation);
+
+            // reset some important variables, we don't want to save the values from the redirect
+            _bytesRemaining = -1;
+            _redirectLocation = null;
+            _etag = null;
+            _lastModified = null;
+            _contentType = null;
+            _encodingChunked = false;
+
             sendRequest(timeout);
             doFetch(timeout);
             return;
diff --git a/core/java/src/net/i2p/util/EepHead.java b/core/java/src/net/i2p/util/EepHead.java
index 938241616a86ac608940679af6e80e6fd1bcf419..c3a17bbea594b43607380a6cb7cb40a56ec8cd22 100644
--- a/core/java/src/net/i2p/util/EepHead.java
+++ b/core/java/src/net/i2p/util/EepHead.java
@@ -116,6 +116,7 @@ public class EepHead extends EepGet {
         else
             timeout.setInactivityTimeout(60*1000);
         
+        // Should we even follow redirects for HEAD?
         if (_redirectLocation != null) {
             //try {
                 URL oldURL = new URL(_actualURL);
@@ -143,6 +144,15 @@ public class EepHead extends EepGet {
             if (_redirects > 5)
                 throw new IOException("Too many redirects: to " + _redirectLocation);
             if (_log.shouldLog(Log.INFO)) _log.info("Redirecting to " + _redirectLocation);
+
+            // reset some important variables, we don't want to save the values from the redirect
+            _bytesRemaining = -1;
+            _redirectLocation = null;
+            _etag = null;
+            _lastModified = null;
+            _contentType = null;
+            _encodingChunked = false;
+
             sendRequest(timeout);
             doFetch(timeout);
             return;
diff --git a/core/java/src/net/i2p/util/LogConsoleBuffer.java b/core/java/src/net/i2p/util/LogConsoleBuffer.java
index a9aab2625ba74f793336b5bb8fcff0ae1b674146..2fe2708490a96ec5353691e7b68a303f88d44f38 100644
--- a/core/java/src/net/i2p/util/LogConsoleBuffer.java
+++ b/core/java/src/net/i2p/util/LogConsoleBuffer.java
@@ -11,8 +11,8 @@ import net.i2p.I2PAppContext;
  */
 public class LogConsoleBuffer {
     private I2PAppContext _context;
-    private final List _buffer;
-    private final List _critBuffer;
+    private final List<String> _buffer;
+    private final List<String> _critBuffer;
 
     public LogConsoleBuffer(I2PAppContext context) {
         _context = context;
@@ -43,7 +43,7 @@ public class LogConsoleBuffer {
      * in the logs)
      *
      */
-    public List getMostRecentMessages() {
+    public List<String> getMostRecentMessages() {
         synchronized (_buffer) {
             return new ArrayList(_buffer);
         }
@@ -54,9 +54,9 @@ public class LogConsoleBuffer {
      * in the logs)
      *
      */
-    public List getMostRecentCriticalMessages() {
+    public List<String> getMostRecentCriticalMessages() {
         synchronized (_critBuffer) {
             return new ArrayList(_critBuffer);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/core/java/src/net/i2p/util/LogManager.java b/core/java/src/net/i2p/util/LogManager.java
index 8bd344d653de3f2052562f2972b62627d765c6e0..d3ece13c560870fb8b6463075e4ae6fb33379662 100644
--- a/core/java/src/net/i2p/util/LogManager.java
+++ b/core/java/src/net/i2p/util/LogManager.java
@@ -156,7 +156,7 @@ public class LogManager {
         return rv;
     }
 
-    /** @deprecated unused */
+    /** now used by ConfigLogingHelper */
     public List<Log> getLogs() {
         return new ArrayList(_logs.values());
     }
@@ -415,15 +415,21 @@ public class LogManager {
 
     /** 
      * Determine how many bytes are in the given formatted string (5m, 60g, 100k, etc)
-     *
+     * Size may be k, m, or g; a trailing b is ignored. Upper-case is allowed.
+     * Spaces between the number and letter is are allowed.
+     * The number may be in floating point.
+     * 4096 min, 2 GB max (returns int)
      */
-    public int getFileSize(String size) {
-        int sz = -1;
+    public static int getFileSize(String size) {
         try {
-            String v = size;
-            char mod = size.toUpperCase().charAt(size.length() - 1);
-            if (!Character.isDigit(mod)) v = size.substring(0, size.length() - 1);
-            int val = Integer.parseInt(v);
+            String v = size.trim().toUpperCase();
+            if (v.length() < 2)
+                return -1;
+            if (v.endsWith("B"))
+                v = v.substring(0, v.length() - 1);
+            char mod = v.charAt(v.length() - 1);
+            if (!Character.isDigit(mod)) v = v.substring(0, v.length() - 1);
+            double val = Double.parseDouble(v);
             switch (mod) {
                 case 'K':
                     val *= 1024;
@@ -438,10 +444,11 @@ public class LogManager {
                     // blah, noop
                     break;
             }
-            return val;
+            if (val < 4096 || val > Integer.MAX_VALUE)
+                return -1;
+            return (int) val;
         } catch (Throwable t) {
             System.err.println("Error parsing config for filesize: [" + size + "]");
-            t.printStackTrace();
             return -1;
         }
     }
diff --git a/core/java/src/net/i2p/util/LogWriter.java b/core/java/src/net/i2p/util/LogWriter.java
index b15aa6cfbb723720958237b4ba5640b16cfb94dc..7dca7ba96dc75cdfe8d045491ee241baeda61aa7 100644
--- a/core/java/src/net/i2p/util/LogWriter.java
+++ b/core/java/src/net/i2p/util/LogWriter.java
@@ -11,7 +11,6 @@ package net.i2p.util;
 
 import java.io.BufferedWriter;
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
@@ -158,7 +157,8 @@ class LogWriter implements Runnable {
         File parent = f.getParentFile();
         if (parent != null) {
             if (!parent.exists()) {
-                boolean ok = parent.mkdirs();
+                File sd = new SecureDirectory(parent.getAbsolutePath());
+                boolean ok = sd.mkdirs();
                 if (!ok) {
                     System.err.println("Unable to create the parent directory: " + parent.getAbsolutePath());
                     //System.exit(0);
@@ -171,7 +171,7 @@ class LogWriter implements Runnable {
         }
         closeFile();
         try {
-            _currentOut = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f), "UTF-8"));
+            _currentOut = new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(f), "UTF-8"));
         } catch (IOException ioe) {
             System.err.println("Error rotating into [" + f.getAbsolutePath() + "]" + ioe);
         }
diff --git a/core/java/src/net/i2p/util/RandomSource.java b/core/java/src/net/i2p/util/RandomSource.java
index c6ac80a65ca63f6410b178098d2933928e2c3a7a..100ec47d736c8406b5d23a968384216fa88c000e 100644
--- a/core/java/src/net/i2p/util/RandomSource.java
+++ b/core/java/src/net/i2p/util/RandomSource.java
@@ -145,7 +145,7 @@ public class RandomSource extends SecureRandom implements EntropyHarvester {
         File f = new File(I2PAppContext.getGlobalContext().getConfigDir(), SEEDFILE);
         FileOutputStream fos = null;
         try {
-            fos = new FileOutputStream(f);
+            fos = new SecureFileOutputStream(f);
             fos.write(buf);
         } catch (IOException ioe) {
             // ignore
diff --git a/core/java/src/net/i2p/util/SecureDirectory.java b/core/java/src/net/i2p/util/SecureDirectory.java
new file mode 100644
index 0000000000000000000000000000000000000000..30d609e96fb70d50c1c206e8005ff0024623d92a
--- /dev/null
+++ b/core/java/src/net/i2p/util/SecureDirectory.java
@@ -0,0 +1,74 @@
+package net.i2p.util;
+
+import java.io.File;
+
+/**
+ * Same as File but sets the file mode after mkdir() so it can
+ * be read and written by the owner only (i.e. 700 on linux)
+ *
+ * @since 0.8.1
+ * @author zzz
+ */
+public class SecureDirectory extends File {
+
+    private static final boolean canSetPerms =
+        (new VersionComparator()).compare(System.getProperty("java.version"), "1.6") >= 0;
+    private static final boolean isNotWindows = !System.getProperty("os.name").startsWith("Win");
+
+    public SecureDirectory(String pathname) {
+        super(pathname);
+    }
+
+    public SecureDirectory(String parent, String child) {
+        super(parent, child);
+    }
+
+    public SecureDirectory(File parent, String child) {
+        super(parent, child);
+    }
+
+    /**
+     *  Sets directory to mode 700 if the directory is created
+     */
+    @Override
+    public boolean mkdir() {
+        boolean rv = super.mkdir();
+        if (rv)
+            setPerms();
+        return rv;
+    }
+
+    /**
+     *  Sets directory to mode 700 if the directory is created
+     *  Does NOT change the mode of other created directories
+     */
+    @Override
+    public boolean mkdirs() {
+        boolean rv = super.mkdirs();
+        if (rv)
+            setPerms();
+        return rv;
+    }
+
+    /**
+     *  Tries to set the permissions to 700,
+     *  ignores errors
+     */
+    private void setPerms() {
+        if (!canSetPerms)
+            return;
+        try {
+            setReadable(false, false);
+            setReadable(true, true);
+            setWritable(false, false);
+            setWritable(true, true);
+            if (isNotWindows) {
+                setExecutable(false, false);
+                setExecutable(true, true);
+            }
+        } catch (Throwable t) {
+            // NoSuchMethodException or NoSuchMethodError if we somehow got the
+            // version detection wrong or the JVM doesn't support it
+        }
+    }
+}
diff --git a/core/java/src/net/i2p/util/SecureFileOutputStream.java b/core/java/src/net/i2p/util/SecureFileOutputStream.java
new file mode 100644
index 0000000000000000000000000000000000000000..8a891344b1bdf6d69c8b410a8a81f2ddd2bc46eb
--- /dev/null
+++ b/core/java/src/net/i2p/util/SecureFileOutputStream.java
@@ -0,0 +1,72 @@
+package net.i2p.util;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+
+/**
+ * Same as FileOutputStream but sets the file mode so it can only
+ * be read and written by the owner only (i.e. 600 on linux)
+ *
+ * @since 0.8.1
+ * @author zzz
+ */
+public class SecureFileOutputStream extends FileOutputStream {
+
+    private static final boolean canSetPerms =
+        (new VersionComparator()).compare(System.getProperty("java.version"), "1.6") >= 0;
+
+    /**
+     *  Sets output file to mode 600
+     */
+    public SecureFileOutputStream(String file) throws FileNotFoundException {
+        super(file);
+        setPerms(new File(file));
+    }
+
+    /**
+     *  Sets output file to mode 600 only if append = false
+     *  (otherwise it is presumed to be 600 already)
+     */
+    public SecureFileOutputStream(String file, boolean append) throws FileNotFoundException {
+        super(file, append);
+        //if (!append)
+            setPerms(new File(file));
+    }
+
+    /**
+     *  Sets output file to mode 600
+     */
+    public SecureFileOutputStream(File file) throws FileNotFoundException {
+        super(file);
+        setPerms(file);
+    }
+
+    /**
+     *  Sets output file to mode 600 only if append = false
+     *  (otherwise it is presumed to be 600 already)
+     */
+    public SecureFileOutputStream(File file, boolean append) throws FileNotFoundException {
+        super(file, append);
+        //if (!append)
+            setPerms(file);
+    }
+
+    /**
+     *  Tries to set the permissions to 600,
+     *  ignores errors
+     */
+    private static void setPerms(File f) {
+        if (!canSetPerms)
+            return;
+        try {
+            f.setReadable(false, false);
+            f.setReadable(true, true);
+            f.setWritable(false, false);
+            f.setWritable(true, true);
+        } catch (Throwable t) {
+            // NoSuchMethodException or NoSuchMethodError if we somehow got the
+            // version detection wrong or the JVM doesn't support it
+        }
+    }
+}
diff --git a/installer/resources/i2ptunnel.config b/installer/resources/i2ptunnel.config
index 6f97a11700625f653e89af841c5837b357cfdb53..ecb45b2067b3b51c66fa398cd46b945de915eeb3 100644
--- a/installer/resources/i2ptunnel.config
+++ b/installer/resources/i2ptunnel.config
@@ -25,7 +25,7 @@ tunnel.0.startOnLoad=true
 
 # irc
 tunnel.1.name=IRC Proxy
-tunnel.1.description=IRC proxy to access the anonymous IRC network
+tunnel.1.description=IRC proxy to access anonymous IRC servers
 tunnel.1.type=ircclient
 tunnel.1.sharedClient=false
 tunnel.1.interface=127.0.0.1
diff --git a/installer/resources/start-i2p.txt b/installer/resources/start-i2p.txt
index e20ed1e734c562cdb74a85f9cbde35593f8e825e..2e51ea48cce3c5ef0621215eb7cd8552ad6a4230 100644
--- a/installer/resources/start-i2p.txt
+++ b/installer/resources/start-i2p.txt
@@ -1,3 +1,8 @@
 To start I2P, run:
 
 $INSTALL_PATH/i2prouter start
+
+
+On non-x86, run:
+
+$INSTALL_PATH/runplain.sh
diff --git a/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java b/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java
index 7ff81a2add6b44c19d12af2a63aed49c8baf2353..667d7fb18477372c529b95b37054cfadcabef810 100644
--- a/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java
+++ b/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java
@@ -9,7 +9,9 @@ import net.i2p.data.PublicKey;
 import net.i2p.data.SessionKey;
 
 /**
- * Hold the tunnel request record, managing its encryption and decryption.
+ * Hold the tunnel request record, managing its ElGamal encryption and decryption.
+ * Iterative AES encryption/decryption is done elsewhere.
+ *
  * Cleartext:
  * <pre>
  *   bytes     0-3: tunnel ID to receive messages as
@@ -26,6 +28,12 @@ import net.i2p.data.SessionKey;
  *   bytes 193-221: uninterpreted / random padding
  * </pre>
  *
+ * Encrypted:
+ * <pre>
+ *   bytes    0-15: First 16 bytes of router hash
+ *   bytes  16-527: ElGamal encrypted block (discarding zero bytes at elg[0] and elg[257])
+ * </pre>
+ *
  */
 public class BuildRequestRecord {
     private ByteArray _data;
@@ -152,7 +160,7 @@ public class BuildRequestRecord {
     
     /**
      * Encrypt the record to the specified peer.  The result is formatted as: <pre>
-     *   bytes 0-15: SHA-256-128 of the current hop's identity (the toPeer parameter)
+     *   bytes 0-15: truncated SHA-256 of the current hop's identity (the toPeer parameter)
      * bytes 15-527: ElGamal-2048 encrypted block
      * </pre>
      */
diff --git a/router/java/src/net/i2p/router/KeyManager.java b/router/java/src/net/i2p/router/KeyManager.java
index 4d19228b7208c16a177770fcfb490cb7acf62cf2..fd0e85a9eb6f31cefeac96dbc5cff92e4bc22564 100644
--- a/router/java/src/net/i2p/router/KeyManager.java
+++ b/router/java/src/net/i2p/router/KeyManager.java
@@ -27,6 +27,8 @@ import net.i2p.data.SigningPrivateKey;
 import net.i2p.data.SigningPublicKey;
 import net.i2p.util.Clock;
 import net.i2p.util.Log;
+import net.i2p.util.SecureDirectory;
+import net.i2p.util.SecureFileOutputStream;
 
 /**
  * Maintain all of the key pairs for the router.
@@ -142,7 +144,7 @@ public class KeyManager {
         }
         public void runJob() {
             String keyDir = getContext().getProperty(PROP_KEYDIR, DEFAULT_KEYDIR);
-            File dir = new File(getContext().getRouterDir(), keyDir);
+            File dir = new SecureDirectory(getContext().getRouterDir(), keyDir);
             if (!dir.exists())
                 dir.mkdirs();
             if (dir.exists() && dir.isDirectory() && dir.canRead() && dir.canWrite()) {
@@ -219,7 +221,7 @@ public class KeyManager {
             FileInputStream in = null;
             try {
                 if (exists) {
-                    out = new FileOutputStream(keyFile);
+                    out = new SecureFileOutputStream(keyFile);
                     structure.writeBytes(out);
                     return structure;
                 } else {
diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java
index 0651e35d261465bb41c27462b9ba291c404f34da..a091cba13451c44d9d3c62b98a198a5f69e1a7ac 100644
--- a/router/java/src/net/i2p/router/Router.java
+++ b/router/java/src/net/i2p/router/Router.java
@@ -46,6 +46,7 @@ import net.i2p.util.FileUtil;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.I2PThread;
 import net.i2p.util.Log;
+import net.i2p.util.SecureFileOutputStream;
 import net.i2p.util.SimpleScheduler;
 import net.i2p.util.SimpleTimer;
 
@@ -305,6 +306,7 @@ public class Router {
     public void setHigherVersionSeen(boolean seen) { _higherVersionSeen = seen; }
     
     public long getWhenStarted() { return _started; }
+
     /** wall clock uptime */
     public long getUptime() { 
         if ( (_context == null) || (_context.clock() == null) ) return 1; // racing on startup
@@ -1053,11 +1055,12 @@ public class Router {
      * this does escape the \r or \n that are unescaped in DataHelper.loadProps().
      * Note that the escaping of \r or \n was probably a mistake and should be taken out.
      *
+     * FIXME Synchronize!!
      */
     public boolean saveConfig() {
         FileOutputStream fos = null;
         try {
-            fos = new FileOutputStream(_configFilename);
+            fos = new SecureFileOutputStream(_configFilename);
             StringBuilder buf = new StringBuilder(8*1024);
             buf.append("# NOTE: This I2P config file must use UTF-8 encoding\n");
             synchronized (_config) {
@@ -1541,7 +1544,7 @@ private static class PersistRouterInfoJob extends JobImpl {
 
         FileOutputStream fos = null;
         try {
-            fos = new FileOutputStream(infoFile);
+            fos = new SecureFileOutputStream(infoFile);
             info.writeBytes(fos);
         } catch (DataFormatException dfe) {
             _log.error("Error rebuilding the router information", dfe);
diff --git a/router/java/src/net/i2p/router/RouterLaunch.java b/router/java/src/net/i2p/router/RouterLaunch.java
index 13ee2f3cfbc5927a32e2ef829b178a87e72b746e..069ee3ec5f61260f46b04f4bb68432e65b3f1203 100644
--- a/router/java/src/net/i2p/router/RouterLaunch.java
+++ b/router/java/src/net/i2p/router/RouterLaunch.java
@@ -1,10 +1,11 @@
 package net.i2p.router;
 
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.PrintStream;
 
+import net.i2p.util.SecureFileOutputStream;
+
 /**
  *  This is the class called by the runplain.sh script on linux
  *  and the i2p.exe launcher on Windows.
@@ -33,7 +34,7 @@ public class RouterLaunch {
         }
         System.setProperty(PROP_WRAPPER_LOG, logfile.getAbsolutePath());
         try {
-            System.setOut(new PrintStream(new FileOutputStream(logfile, true)));
+            System.setOut(new PrintStream(new SecureFileOutputStream(logfile, true)));
         } catch (IOException ioe) {
             ioe.printStackTrace();
 	}
diff --git a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
index e5aa1b5abb4d2a9af84ff6f2d6a57c2178581ed5..f363bd27814dfd7600f4a85126ffa42e214c3cf6 100644
--- a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
+++ b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
@@ -17,6 +17,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import net.i2p.client.I2PClient;
 import net.i2p.crypto.SessionKeyManager;
 import net.i2p.crypto.TransientSessionKeyManager;
 import net.i2p.data.Destination;
@@ -76,11 +77,13 @@ public class ClientConnectionRunner {
      * This contains the last 10 MessageIds that have had their (non-ack) status 
      * delivered to the client (so that we can be sure only to update when necessary)
      */
-    private final List _alreadyProcessed;
+    private final List<MessageId> _alreadyProcessed;
     private ClientWriterRunner _writer;
     private Hash _destHashCache;
     /** are we, uh, dead */
     private boolean _dead;
+    /** For outbound traffic. true if i2cp.messageReliability = "none"; @since 0.8.1 */
+    private boolean _dontSendMSM;
     
     /**
      * Create a new runner against the given socket
@@ -91,11 +94,9 @@ public class ClientConnectionRunner {
         _log = _context.logManager().getLog(ClientConnectionRunner.class);
         _manager = manager;
         _socket = socket;
-        _config = null;
         _messages = new ConcurrentHashMap();
         _alreadyProcessed = new ArrayList();
         _acceptedPending = new ConcurrentHashSet();
-        _dead = false;
     }
     
     private static volatile int __id = 0;
@@ -189,6 +190,9 @@ public class ClientConnectionRunner {
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("SessionEstablished called for destination " + _destHashCache.toBase64());
         _config = config;
+        // This is the only option that is interpreted here, not at the tunnel manager
+        if (config.getOptions() != null)
+            _dontSendMSM = "none".equalsIgnoreCase(config.getOptions().getProperty(I2PClient.PROP_RELIABILITY));
         // per-destination session key manager to prevent rather easy correlation
         if (_sessionKeyManager == null)
             _sessionKeyManager = new TransientSessionKeyManager(_context);
@@ -197,10 +201,18 @@ public class ClientConnectionRunner {
         _manager.destinationEstablished(this);
     }
     
+    /** 
+     * Send a notification to the client that their message (id specified) was
+     * delivered (or failed delivery)
+     * Note that this sends the Guaranteed status codes, even though we only support best effort.
+     * Doesn't do anything if i2cp.messageReliability = "none"
+     */
     void updateMessageDeliveryStatus(MessageId id, boolean delivered) {
-        if (_dead) return;
+        if (_dead || _dontSendMSM)
+            return;
         _context.jobQueue().addJob(new MessageDeliveryStatusUpdate(id, delivered));
     }
+
     /** 
      * called after a new leaseSet is granted by the client, the NetworkDb has been
      * updated.  This takes care of all the LeaseRequestState stuff (including firing any jobs)
@@ -254,7 +266,8 @@ public class ClientConnectionRunner {
         long expiration = 0;
         if (message instanceof SendMessageExpiresMessage)
             expiration = ((SendMessageExpiresMessage) message).getExpiration().getTime();
-        _acceptedPending.add(id);
+        if (!_dontSendMSM)
+            _acceptedPending.add(id);
 
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("** Receiving message [" + id.getMessageId() + "] with payload of size [" 
@@ -276,9 +289,11 @@ public class ClientConnectionRunner {
     /** 
      * Send a notification to the client that their message (id specified) was accepted 
      * for delivery (but not necessarily delivered)
-     *
+     * Doesn't do anything if i2cp.messageReliability = "none"
      */
     void ackSendMessage(MessageId id, long nonce) {
+        if (_dontSendMSM)
+            return;
         SessionId sid = _sessionId;
         if (sid == null) return;
         if (_log.shouldLog(Log.DEBUG))
@@ -517,12 +532,17 @@ public class ClientConnectionRunner {
         }
 
         public String getName() { return "Update Delivery Status"; }
+
+        /**
+         * Note that this sends the Guaranteed status codes, even though we only support best effort.
+         */
         public void runJob() {
             if (_dead) return;
 
             MessageStatusMessage msg = new MessageStatusMessage();
             msg.setMessageId(_messageId.getMessageId());
             msg.setSessionId(_sessionId.getSessionId());
+            // has to be >= 0, it is initialized to -1
             msg.setNonce(2);
             msg.setSize(0);
             if (_success) 
diff --git a/router/java/src/net/i2p/router/client/ClientListenerRunner.java b/router/java/src/net/i2p/router/client/ClientListenerRunner.java
index 2c96fa4870645af110ee40367e52c1e779b9d2dd..7a7d448ea99d10bf1ec51ee2953142ca17c915f4 100644
--- a/router/java/src/net/i2p/router/client/ClientListenerRunner.java
+++ b/router/java/src/net/i2p/router/client/ClientListenerRunner.java
@@ -41,8 +41,6 @@ public class ClientListenerRunner implements Runnable {
         _log = _context.logManager().getLog(ClientListenerRunner.class);
         _manager = manager;
         _port = port;
-        _running = false;
-        _listening = false;
         
         String val = context.getProperty(BIND_ALL_INTERFACES);
         _bindAllInterfaces = Boolean.valueOf(val).booleanValue();
diff --git a/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java b/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java
index e90d12f53599c7facc7c17b967c3ddc7b0c5cc5d..066d6cc354223c3ee1e53b56e31c78fc4f82e4ab 100644
--- a/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java
+++ b/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java
@@ -43,7 +43,6 @@ public class ClientManagerFacadeImpl extends ClientManagerFacade {
     
     public ClientManagerFacadeImpl(RouterContext context) {
         _context = context;
-        _manager = null;
         _log.debug("Client manager facade created");
     }
     
diff --git a/router/java/src/net/i2p/router/client/LeaseRequestState.java b/router/java/src/net/i2p/router/client/LeaseRequestState.java
index b4cf4415a4d7012515acf1b7bfe1c8040864e9f4..7e2a248ac948481129ceedd6890755f6f22b33ec 100644
--- a/router/java/src/net/i2p/router/client/LeaseRequestState.java
+++ b/router/java/src/net/i2p/router/client/LeaseRequestState.java
@@ -33,7 +33,6 @@ class LeaseRequestState {
         _onFailed = onFailed;
         _expiration = expiration;
         _requestedLeaseSet = requested;
-        _successful = false;
     }
     
     /** created lease set from client */
diff --git a/router/java/src/net/i2p/router/client/MessageReceivedJob.java b/router/java/src/net/i2p/router/client/MessageReceivedJob.java
index d0c767828f296fd312d7668c66a16e762114390a..82744659abed01762717dd1e067e1a4fc87887c8 100644
--- a/router/java/src/net/i2p/router/client/MessageReceivedJob.java
+++ b/router/java/src/net/i2p/router/client/MessageReceivedJob.java
@@ -47,9 +47,6 @@ class MessageReceivedJob extends JobImpl {
     
     /**
      * Deliver notification to the client that the given message is available.
-     * This is synchronous and returns true if the notification was sent safely,
-     * otherwise it returns false
-     *
      */
     public void messageAvailable(MessageId id, long size) {
         if (_log.shouldLog(Log.DEBUG))
@@ -59,6 +56,7 @@ class MessageReceivedJob extends JobImpl {
         msg.setMessageId(id.getMessageId());
         msg.setSessionId(_runner.getSessionId().getSessionId());
         msg.setSize(size);
+        // has to be >= 0, it is initialized to -1
         msg.setNonce(1);
         msg.setStatus(MessageStatusMessage.STATUS_AVAILABLE);
         try {
diff --git a/router/java/src/net/i2p/router/client/RequestLeaseSetJob.java b/router/java/src/net/i2p/router/client/RequestLeaseSetJob.java
index 51d3a304c907f4c5f91e3ff21b604d650f72b495..516a652d22b8b6381b5d94ec42c28193556649c1 100644
--- a/router/java/src/net/i2p/router/client/RequestLeaseSetJob.java
+++ b/router/java/src/net/i2p/router/client/RequestLeaseSetJob.java
@@ -44,9 +44,9 @@ class RequestLeaseSetJob extends JobImpl {
         _onCreate = onCreate;
         _onFail = onFail;
         _requestState = state;
-        ctx.statManager().createRateStat("client.requestLeaseSetSuccess", "How frequently the router requests successfully a new leaseSet?", "ClientMessages", new long[] { 10*60*1000, 60*60*1000, 24*60*60*1000 });
-        ctx.statManager().createRateStat("client.requestLeaseSetTimeout", "How frequently the router requests a new leaseSet but gets no reply?", "ClientMessages", new long[] { 10*60*1000, 60*60*1000, 24*60*60*1000 });
-        ctx.statManager().createRateStat("client.requestLeaseSetDropped", "How frequently the router requests a new leaseSet but the client drops?", "ClientMessages", new long[] { 10*60*1000, 60*60*1000, 24*60*60*1000 });
+        ctx.statManager().createRateStat("client.requestLeaseSetSuccess", "How frequently the router requests successfully a new leaseSet?", "ClientMessages", new long[] { 60*60*1000 });
+        ctx.statManager().createRateStat("client.requestLeaseSetTimeout", "How frequently the router requests a new leaseSet but gets no reply?", "ClientMessages", new long[] { 60*60*1000 });
+        ctx.statManager().createRateStat("client.requestLeaseSetDropped", "How frequently the router requests a new leaseSet but the client drops?", "ClientMessages", new long[] { 60*60*1000 });
     }
     
     public String getName() { return "Request Lease Set"; }
diff --git a/router/java/src/net/i2p/router/message/GarlicConfig.java b/router/java/src/net/i2p/router/message/GarlicConfig.java
index 1c616f43cfb59be51e006109830ae3c559f006b8..b23d4ac9a34836a5599991d9ae8cfa75debfb1eb 100644
--- a/router/java/src/net/i2p/router/message/GarlicConfig.java
+++ b/router/java/src/net/i2p/router/message/GarlicConfig.java
@@ -37,17 +37,9 @@ public class GarlicConfig {
     private long _replyBlockExpiration;
     
     public GarlicConfig() {
-	_recipient = null;
-	_recipientPublicKey = null;
-	_cert = null;
 	_id = -1;
 	_expiration = -1;
 	_cloveConfigs = new ArrayList();
-	_instructions = null;
-	_requestAck = false;
-	_replyThroughRouter = null;
-	_replyInstructions = null;
-	_replyBlockCertificate = null;
 	_replyBlockMessageId = -1;
 	_replyBlockExpiration = -1;
     }
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
index 688b8546d286c37c7055e8fb6d1e5960f67a70c9..acede96edba33cce0456b6c7869464b2adf09368 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
@@ -194,10 +194,12 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
     
     public void shutdown() {
         _initialized = false;
-        _kb = null;
+        // don't null out _kb, it can cause NPEs in concurrent operations
+        //_kb = null;
         if (_ds != null)
             _ds.stop();
-        _ds = null;
+        // don't null out _ds, it can cause NPEs in concurrent operations
+        //_ds = null;
         _exploreKeys.clear(); // hope this doesn't cause an explosion, it shouldn't.
         // _exploreKeys = null;
     }
@@ -750,6 +752,10 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
         } else if (upLongEnough && !routerInfo.isCurrent(ROUTER_INFO_EXPIRATION_SHORT)) {
             if (routerInfo.getAddresses().isEmpty())
                 return "Peer " + key.toBase64() + " published > 90m ago with no addresses";
+            // This should cover the introducers case below too
+            // And even better, catches the case where the router is unreachable but knows no introducers
+            if (routerInfo.getCapabilities().indexOf(Router.CAPABILITY_UNREACHABLE) >= 0)
+                return "Peer " + key.toBase64() + " published > 90m ago and thinks it is unreachable";
             RouterAddress ra = routerInfo.getTargetAddress("SSU");
             if (ra != null) {
                 // Introducers change often, introducee will ping introducer for 2 hours
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java b/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java
index 77ea8c4573136b0c58b402927c110b209f846b97..0172dfcc6907a5b54923498cfcd4364877d587bc 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java
@@ -29,6 +29,8 @@ import net.i2p.router.RouterContext;
 import net.i2p.router.networkdb.reseed.ReseedChecker;
 import net.i2p.util.I2PThread;
 import net.i2p.util.Log;
+import net.i2p.util.SecureDirectory;
+import net.i2p.util.SecureFileOutputStream;
 
 /**
  * Write out keys to disk when we get them and periodically read ones we don't know
@@ -288,7 +290,7 @@ class PersistentDataStore extends TransientDataStore {
             long dataPublishDate = getPublishDate(data);
             if (dbFile.lastModified() < dataPublishDate) {
                 // our filesystem is out of date, lets replace it
-                fos = new FileOutputStream(dbFile);
+                fos = new SecureFileOutputStream(dbFile);
                 try {
                     data.writeBytes(fos);
                     fos.close();
@@ -440,7 +442,7 @@ class PersistentDataStore extends TransientDataStore {
     
     
     private File getDbDir() throws IOException {
-        File f = new File(_context.getRouterDir(), _dbDir);
+        File f = new SecureDirectory(_context.getRouterDir(), _dbDir);
         if (!f.exists()) {
             boolean created = f.mkdirs();
             if (!created)
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 a9c85cd74b5c3c4f47d97fb6e677833620af5c29..776d7eee46b4fa0445be32cb80f84b54cdc6748e 100644
--- a/router/java/src/net/i2p/router/networkdb/reseed/Reseeder.java
+++ b/router/java/src/net/i2p/router/networkdb/reseed/Reseeder.java
@@ -17,6 +17,8 @@ import net.i2p.router.RouterContext;
 import net.i2p.util.EepGet;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
+import net.i2p.util.SecureDirectory;
+import net.i2p.util.SecureFileOutputStream;
 import net.i2p.util.SSLEepGet;
 import net.i2p.util.Translate;
 
@@ -260,11 +262,11 @@ public class Reseeder {
     
         private void writeSeed(String name, byte data[]) throws Exception {
             String dirName = "netDb"; // _context.getProperty("router.networkDatabase.dbDir", "netDb");
-            File netDbDir = new File(_context.getRouterDir(), dirName);
+            File netDbDir = new SecureDirectory(_context.getRouterDir(), dirName);
             if (!netDbDir.exists()) {
                 boolean ok = netDbDir.mkdirs();
             }
-            FileOutputStream fos = new FileOutputStream(new File(netDbDir, "routerInfo-" + name + ".dat"));
+            FileOutputStream fos = new SecureFileOutputStream(new File(netDbDir, "routerInfo-" + name + ".dat"));
             fos.write(data);
             fos.close();
         }
diff --git a/router/java/src/net/i2p/router/peermanager/ProfilePersistenceHelper.java b/router/java/src/net/i2p/router/peermanager/ProfilePersistenceHelper.java
index 606b4883cf288f4e7a905aa67df81a3fd7bf2456..610dfec4f8a26eb01f07a781b483bf180105a43b 100644
--- a/router/java/src/net/i2p/router/peermanager/ProfilePersistenceHelper.java
+++ b/router/java/src/net/i2p/router/peermanager/ProfilePersistenceHelper.java
@@ -3,7 +3,6 @@ package net.i2p.router.peermanager;
 import java.io.BufferedOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.FilenameFilter;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -19,6 +18,8 @@ import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
 import net.i2p.router.RouterContext;
 import net.i2p.util.Log;
+import net.i2p.util.SecureDirectory;
+import net.i2p.util.SecureFileOutputStream;
 
 class ProfilePersistenceHelper {
     private Log _log;
@@ -61,7 +62,7 @@ class ProfilePersistenceHelper {
         long before = _context.clock().now();
         OutputStream fos = null;
         try {
-            fos = new BufferedOutputStream(new GZIPOutputStream(new FileOutputStream(f)));
+            fos = new BufferedOutputStream(new GZIPOutputStream(new SecureFileOutputStream(f)));
             writeProfile(profile, fos);
         } catch (IOException ioe) {
             _log.error("Error writing profile to " + f);
@@ -310,7 +311,7 @@ class ProfilePersistenceHelper {
     private File getProfileDir() {
         if (_profileDir == null) {
             String dir = _context.getProperty(PROP_PEER_PROFILE_DIR, DEFAULT_PEER_PROFILE_DIR);
-            _profileDir = new File(_context.getRouterDir(), dir);
+            _profileDir = new SecureDirectory(_context.getRouterDir(), dir);
         }
         return _profileDir;
     }
diff --git a/router/java/src/net/i2p/router/startup/ClientAppConfig.java b/router/java/src/net/i2p/router/startup/ClientAppConfig.java
index 982d4a298c4987bc5bdebe601f5f19c4fe4c0d36..33f4351fefcc06754d57bad86465cdb25b76bb89 100644
--- a/router/java/src/net/i2p/router/startup/ClientAppConfig.java
+++ b/router/java/src/net/i2p/router/startup/ClientAppConfig.java
@@ -11,6 +11,7 @@ import java.util.Properties;
 import net.i2p.I2PAppContext;
 import net.i2p.data.DataHelper;
 import net.i2p.router.RouterContext;
+import net.i2p.util.SecureFileOutputStream;
 
 
 /**
@@ -191,7 +192,7 @@ public class ClientAppConfig {
         File cfgFile = configFile(ctx);
         FileOutputStream fos = null;
         try {
-            fos = new FileOutputStream(cfgFile);
+            fos = new SecureFileOutputStream(cfgFile);
             StringBuilder buf = new StringBuilder(2048);
             for(int i = 0; i < apps.size(); i++) {
                 ClientAppConfig app = (ClientAppConfig) apps.get(i);
diff --git a/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java b/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java
index 459cd0fb3ae2a8d07959a34cd208efb562cd1124..36b989bc7761609ab28b9bf3d83a3b994432d27a 100644
--- a/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java
+++ b/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java
@@ -27,6 +27,7 @@ import net.i2p.router.JobImpl;
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
 import net.i2p.util.Log;
+import net.i2p.util.SecureFileOutputStream;
 
 public class CreateRouterInfoJob extends JobImpl {
     private static Log _log = new Log(CreateRouterInfoJob.class);
@@ -80,12 +81,12 @@ public class CreateRouterInfoJob extends JobImpl {
             
             String infoFilename = getContext().getProperty(Router.PROP_INFO_FILENAME, Router.PROP_INFO_FILENAME_DEFAULT);
             File ifile = new File(getContext().getRouterDir(), infoFilename);
-            fos1 = new FileOutputStream(ifile);
+            fos1 = new SecureFileOutputStream(ifile);
             info.writeBytes(fos1);
             
             String keyFilename = getContext().getProperty(Router.PROP_KEYS_FILENAME, Router.PROP_KEYS_FILENAME_DEFAULT);
             File kfile = new File(getContext().getRouterDir(), keyFilename);
-            fos2 = new FileOutputStream(kfile);
+            fos2 = new SecureFileOutputStream(kfile);
             privkey.writeBytes(fos2);
             signingPrivKey.writeBytes(fos2);
             pubkey.writeBytes(fos2);
diff --git a/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java b/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java
index e3daf60ea44d7fec76f55b3b11eef195c44d1936..b417c32bbb3343ed72f34dae41f0d7399a32343c 100644
--- a/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java
+++ b/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java
@@ -26,6 +26,7 @@ import net.i2p.router.JobImpl;
 import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
 import net.i2p.util.Log;
+import net.i2p.util.SecureFileOutputStream;
 
 /**
  * This used be called from StartAcceptingClientsJob but is now disabled.
@@ -135,7 +136,7 @@ public class RebuildRouterInfoJob extends JobImpl {
             
             FileOutputStream fos = null;
             try {
-                fos = new FileOutputStream(infoFile);
+                fos = new SecureFileOutputStream(infoFile);
                 info.writeBytes(fos);
             } catch (DataFormatException dfe) {
                 _log.log(Log.CRIT, "Error rebuilding the router information", dfe);
diff --git a/router/java/src/net/i2p/router/startup/WorkingDir.java b/router/java/src/net/i2p/router/startup/WorkingDir.java
index 545239777fedf09d15a4706dacd5683a62083308..0b32c661f0ddce616cb77f0e184006375088afdf 100644
--- a/router/java/src/net/i2p/router/startup/WorkingDir.java
+++ b/router/java/src/net/i2p/router/startup/WorkingDir.java
@@ -11,6 +11,8 @@ import java.io.PrintWriter;
 import java.util.Properties;
 
 import net.i2p.data.DataHelper;
+import net.i2p.util.SecureDirectory;
+import net.i2p.util.SecureFileOutputStream;
 
 /**
  * Get a working directory for i2p.
@@ -64,19 +66,19 @@ public class WorkingDir {
         boolean isWindows = System.getProperty("os.name").startsWith("Win");
         File dirf = null;
         if (dir != null) {
-            dirf = new File(dir);
+            dirf = new SecureDirectory(dir);
         } else {
             String home = System.getProperty("user.home");
             if (isWindows) {
                 String appdata = System.getenv("APPDATA");
                 if (appdata != null)
                     home = appdata;
-                dirf = new File(home, WORKING_DIR_DEFAULT_WINDOWS);
+                dirf = new SecureDirectory(home, WORKING_DIR_DEFAULT_WINDOWS);
             } else {
                 if (DAEMON_USER.equals(System.getProperty("user.name")))
-                    dirf = new File(home, WORKING_DIR_DEFAULT_DAEMON);
+                    dirf = new SecureDirectory(home, WORKING_DIR_DEFAULT_DAEMON);
                 else
-                    dirf = new File(home, WORKING_DIR_DEFAULT);
+                    dirf = new SecureDirectory(home, WORKING_DIR_DEFAULT);
             }
         }
 
@@ -143,7 +145,7 @@ public class WorkingDir {
         // this one must be after MIGRATE_BASE
         success &= migrateJettyXml(oldDirf, dirf);
         success &= migrateClientsConfig(oldDirf, dirf);
-        success &= copy(new File(oldDirf, "docs/news.xml"), new File(dirf, "docs"));
+        success &= copy(new File(oldDirf, "docs/news.xml"), new SecureDirectory(dirf, "docs"));
 
         // Report success or failure
         if (success) {
@@ -197,7 +199,7 @@ public class WorkingDir {
         PrintWriter out = null;
         try {
             in = new FileInputStream(oldFile);
-            out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(newFile), "UTF-8")));
+            out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(newFile), "UTF-8")));
             out.println("# Modified by I2P User dir migration script");
             String s = null;
             boolean isDaemon = DAEMON_USER.equals(System.getProperty("user.name"));
@@ -240,7 +242,7 @@ public class WorkingDir {
         PrintWriter out = null;
         try {
             in = new FileInputStream(oldFile);
-            out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(newFile), "UTF-8")));
+            out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(newFile), "UTF-8")));
             String s = null;
             while ((s = DataHelper.readLine(in)) != null) {
                 if (s.indexOf("./eepsite/") >= 0) {
@@ -270,7 +272,7 @@ public class WorkingDir {
      * @param targetDir the directory to copy to, will be created if it doesn't exist
      * @return true for success OR if src does not exist
      */
-    public static final boolean copy(File src, File targetDir) {
+    private static boolean copy(File src, File targetDir) {
         if (!src.exists())
             return true;
         if (!targetDir.exists()) {
@@ -280,7 +282,8 @@ public class WorkingDir {
             }
             System.err.println("Created " + targetDir.getPath());
         }
-        File targetFile = new File(targetDir, src.getName());
+        // SecureDirectory is a File so this works for non-directories too
+        File targetFile = new SecureDirectory(targetDir, src.getName());
         if (!src.isDirectory())
             return copyFile(src, targetFile);
         File children[] = src.listFiles();
@@ -305,10 +308,10 @@ public class WorkingDir {
     
     /**
      * @param src not a directory, must exist
-     * @param dst not a directory, will be overwritten if existing
+     * @param dst not a directory, will be overwritten if existing, will be mode 600
      * @return true if it was copied successfully
      */
-    public static boolean copyFile(File src, File dst) {
+    private static boolean copyFile(File src, File dst) {
         if (!src.exists()) return false;
         boolean rv = true;
 
@@ -317,7 +320,7 @@ public class WorkingDir {
         FileOutputStream out = null;
         try {
             in = new FileInputStream(src);
-            out = new FileOutputStream(dst);
+            out = new SecureFileOutputStream(dst);
             
             int read = 0;
             while ( (read = in.read(buf)) != -1)
diff --git a/router/java/src/net/i2p/router/transport/GeoIP.java b/router/java/src/net/i2p/router/transport/GeoIP.java
index cc0a63ba6cd9aa9339ad95f399a811b40b4e6ef0..85a89b3c6410b5eeaf471a37ace4812e5bc0fc58 100644
--- a/router/java/src/net/i2p/router/transport/GeoIP.java
+++ b/router/java/src/net/i2p/router/transport/GeoIP.java
@@ -98,23 +98,26 @@ public class GeoIP {
         public void run() {
             if (_lock.getAndSet(true))
                 return;
-            // clear the negative cache every few runs, to prevent it from getting too big
-            if (((++_lookupRunCount) % CLEAR) == 0)
-                _notFound.clear();
-            Long[] search = _pendingSearch.toArray(new Long[_pendingSearch.size()]);
-            if (search.length <= 0)
-                return;
-            _pendingSearch.clear();
-            Arrays.sort(search);
-            String[] countries = readGeoIPFile(search);
-
-            for (int i = 0; i < countries.length; i++) {
-                if (countries[i] != null)
-                    _IPToCountry.put(search[i], countries[i]);
-                else
-                    _notFound.add(search[i]);
+            try {
+                // clear the negative cache every few runs, to prevent it from getting too big
+                if (((++_lookupRunCount) % CLEAR) == 0)
+                    _notFound.clear();
+                Long[] search = _pendingSearch.toArray(new Long[_pendingSearch.size()]);
+                if (search.length <= 0)
+                    return;
+                _pendingSearch.clear();
+                Arrays.sort(search);
+                String[] countries = readGeoIPFile(search);
+    
+                for (int i = 0; i < countries.length; i++) {
+                    if (countries[i] != null)
+                        _IPToCountry.put(search[i], countries[i]);
+                    else
+                        _notFound.add(search[i]);
+                }
+            } finally {
+                _lock.set(false);
             }
-            _lock.set(false);
         }
     }
 
diff --git a/router/java/src/net/i2p/router/transport/TransportManager.java b/router/java/src/net/i2p/router/transport/TransportManager.java
index 87a8bc8b2b993803178f5907fe11bf6b5c30137f..243c76237bc4f283681405a5154125c2fd9bbb93 100644
--- a/router/java/src/net/i2p/router/transport/TransportManager.java
+++ b/router/java/src/net/i2p/router/transport/TransportManager.java
@@ -21,6 +21,7 @@ import java.util.Properties;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.Vector;
+import java.util.concurrent.ConcurrentHashMap;
 
 import net.i2p.data.Hash;
 import net.i2p.data.RouterAddress;
@@ -36,7 +37,11 @@ import net.i2p.util.Translate;
 
 public class TransportManager implements TransportEventListener {
     private Log _log;
-    private List<Transport> _transports;
+    /**
+     * Converted from List to prevent concurrent modification exceptions.
+     * If we want more than one transport with the same style we will have to change this.
+     */
+    private Map<String, Transport> _transports;
     private RouterContext _context;
     private UPnPManager _upnpManager;
 
@@ -56,20 +61,20 @@ public class TransportManager implements TransportEventListener {
         _context.statManager().createRateStat("transport.bidFailSelf", "Could not attempt to bid on message, as it targeted ourselves", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
         _context.statManager().createRateStat("transport.bidFailNoTransports", "Could not attempt to bid on message, as none of the transports could attempt it", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
         _context.statManager().createRateStat("transport.bidFailAllTransports", "Could not attempt to bid on message, as all of the transports had failed", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
-        _transports = new ArrayList();
+        _transports = new ConcurrentHashMap(2);
         if (Boolean.valueOf(_context.getProperty(PROP_ENABLE_UPNP, "true")).booleanValue())
             _upnpManager = new UPnPManager(context, this);
     }
     
     public void addTransport(Transport transport) {
         if (transport == null) return;
-        _transports.add(transport);
+        _transports.put(transport.getStyle(), transport);
         transport.setListener(this);
     }
     
     public void removeTransport(Transport transport) {
         if (transport == null) return;
-        _transports.remove(transport);
+        _transports.remove(transport.getStyle());
         transport.setListener(null);
     }
 
@@ -140,11 +145,10 @@ public class TransportManager implements TransportEventListener {
             _upnpManager.start();
         configTransports();
         _log.debug("Starting up the transport manager");
-        for (int i = 0; i < _transports.size(); i++) {
-            Transport t = _transports.get(i);
+        for (Transport t : _transports.values()) {
             RouterAddress addr = t.startListening();
             if (_log.shouldLog(Log.DEBUG))
-                _log.debug("Transport " + i + " (" + t.getStyle() + ") started");
+                _log.debug("Transport " + t.getStyle() + " started");
         }
         // kick UPnP - Do this to get the ports opened even before UDP registers an address
         transportAddressChanged();
@@ -161,19 +165,14 @@ public class TransportManager implements TransportEventListener {
     public void stopListening() {
         if (_upnpManager != null)
             _upnpManager.stop();
-        for (int i = 0; i < _transports.size(); i++) {
-            _transports.get(i).stopListening();
+        for (Transport t : _transports.values()) {
+            t.stopListening();
         }
         _transports.clear();
     }
     
     public Transport getTransport(String style) {
-        for (int i = 0; i < _transports.size(); i++) {
-            Transport t = _transports.get(i);
-            if(style.equals(t.getStyle()))
-                return t;
-        }
-        return null;
+        return _transports.get(style);
     }
     
     int getTransportCount() { return _transports.size(); }
@@ -189,16 +188,16 @@ public class TransportManager implements TransportEventListener {
     
     public int countActivePeers() { 
         int peers = 0;
-        for (int i = 0; i < _transports.size(); i++) {
-            peers += _transports.get(i).countActivePeers();
+        for (Transport t : _transports.values()) {
+            peers += t.countActivePeers();
         }
         return peers;
     }
     
     public int countActiveSendPeers() { 
         int peers = 0;
-        for (int i = 0; i < _transports.size(); i++) {
-            peers += _transports.get(i).countActiveSendPeers();
+        for (Transport t : _transports.values()) {
+            peers += t.countActiveSendPeers();
         }
         return peers;
     }
@@ -210,8 +209,8 @@ public class TransportManager implements TransportEventListener {
       * @param pct percent of limit 0-100
       */
     public boolean haveOutboundCapacity(int pct) { 
-        for (int i = 0; i < _transports.size(); i++) {
-            if (_transports.get(i).haveCapacity(pct))
+        for (Transport t : _transports.values()) {
+            if (t.haveCapacity(pct))
                 return true;
         }
         return false;
@@ -225,8 +224,8 @@ public class TransportManager implements TransportEventListener {
     public boolean haveHighOutboundCapacity() { 
         if (_transports.isEmpty())
             return false;
-        for (int i = 0; i < _transports.size(); i++) {
-            if (!_transports.get(i).haveCapacity(HIGH_CAPACITY_PCT))
+        for (Transport t : _transports.values()) {
+            if (!t.haveCapacity(HIGH_CAPACITY_PCT))
                 return false;
         }
         return true;
@@ -239,8 +238,8 @@ public class TransportManager implements TransportEventListener {
       * @param pct percent of limit 0-100
       */
     public boolean haveInboundCapacity(int pct) { 
-        for (int i = 0; i < _transports.size(); i++) {
-            if (_transports.get(i).getCurrentAddress() != null && _transports.get(i).haveCapacity(pct))
+        for (Transport t : _transports.values()) {
+            if (t.getCurrentAddress() != null && t.haveCapacity(pct))
                 return true;
         }
         return false;
@@ -253,8 +252,8 @@ public class TransportManager implements TransportEventListener {
      */
     public Vector getClockSkews() {
         Vector skews = new Vector();
-        for (int i = 0; i < _transports.size(); i++) {
-            Vector tempSkews = _transports.get(i).getClockSkews();
+        for (Transport t : _transports.values()) {
+            Vector tempSkews = t.getClockSkews();
             if ((tempSkews == null) || (tempSkews.isEmpty())) continue;
             skews.addAll(tempSkews);
         }
@@ -266,7 +265,7 @@ public class TransportManager implements TransportEventListener {
     /** @return the best status of any transport */
     public short getReachabilityStatus() { 
         short rv = CommSystemFacade.STATUS_UNKNOWN;
-        for (Transport t : _transports) {
+        for (Transport t : _transports.values()) {
             short s = t.getReachabilityStatus();
             if (s < rv)
                 rv = s;
@@ -275,13 +274,12 @@ public class TransportManager implements TransportEventListener {
     }
 
     public void recheckReachability() { 
-        for (int i = 0; i < _transports.size(); i++)
-            _transports.get(i).recheckReachability();
+        for (Transport t : _transports.values())
+            t.recheckReachability();
     }
 
     public boolean isBacklogged(Hash dest) {
-        for (int i = 0; i < _transports.size(); i++) {
-            Transport t = _transports.get(i);
+        for (Transport t : _transports.values()) {
             if (t.isBacklogged(dest))
                 return true;
         }
@@ -289,8 +287,7 @@ public class TransportManager implements TransportEventListener {
     }    
     
     public boolean isEstablished(Hash dest) {
-        for (int i = 0; i < _transports.size(); i++) {
-            Transport t = _transports.get(i);
+        for (Transport t : _transports.values()) {
             if (t.isEstablished(dest))
                 return true;
         }
@@ -303,8 +300,7 @@ public class TransportManager implements TransportEventListener {
      * This is NOT reset if the peer contacts us.
      */
     public boolean wasUnreachable(Hash dest) {
-        for (int i = 0; i < _transports.size(); i++) {
-            Transport t = _transports.get(i);
+        for (Transport t : _transports.values()) {
             if (!t.wasUnreachable(dest))
                 return false;
         }
@@ -330,9 +326,9 @@ public class TransportManager implements TransportEventListener {
     public Map<String, RouterAddress> getAddresses() {
         Map<String, RouterAddress> rv = new HashMap(_transports.size());
         // do this first since SSU may force a NTCP change
-        for (Transport t : _transports)
+        for (Transport t : _transports.values())
             t.updateAddress();
-        for (Transport t : _transports) {
+        for (Transport t : _transports.values()) {
             if (t.getCurrentAddress() != null)
                 rv.put(t.getStyle(), t.getCurrentAddress());
         }
@@ -345,7 +341,7 @@ public class TransportManager implements TransportEventListener {
      */
     private Map<String, Integer> getPorts() {
         Map<String, Integer> rv = new HashMap(_transports.size());
-        for (Transport t : _transports) {
+        for (Transport t : _transports.values()) {
             int port = t.getRequestedPort();
             if (t.getCurrentAddress() != null) {
                 Properties opts = t.getCurrentAddress().getOptions();
@@ -386,8 +382,7 @@ public class TransportManager implements TransportEventListener {
 
         List<TransportBid> rv = new ArrayList(_transports.size());
         Set failedTransports = msg.getFailedTransports();
-        for (int i = 0; i < _transports.size(); i++) {
-            Transport t = _transports.get(i);
+        for (Transport t : _transports.values()) {
             if (failedTransports.contains(t.getStyle())) {
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug("Skipping transport " + t.getStyle() + " as it already failed");
@@ -415,8 +410,7 @@ public class TransportManager implements TransportEventListener {
         Hash peer = msg.getTarget().getIdentity().calculateHash();
         Set failedTransports = msg.getFailedTransports();
         TransportBid rv = null;
-        for (int i = 0; i < _transports.size(); i++) {
-            Transport t = _transports.get(i);
+        for (Transport t : _transports.values()) {
             if (t.isUnreachable(peer)) {
                 unreachableTransports++;
                 // this keeps GetBids() from shitlisting for "no common transports"
@@ -482,8 +476,7 @@ public class TransportManager implements TransportEventListener {
 
     public List getMostRecentErrorMessages() { 
         List rv = new ArrayList(16);
-        for (int i = 0; i < _transports.size(); i++) {
-            Transport t = _transports.get(i);
+        for (Transport t : _transports.values()) {
             rv.addAll(t.getMostRecentErrorMessages());
         }
         return rv;
@@ -491,8 +484,7 @@ public class TransportManager implements TransportEventListener {
     
     public void renderStatusHTML(Writer out, String urlBase, int sortFlags) throws IOException {
         TreeMap transports = new TreeMap();
-        for (int i = 0; i < _transports.size(); i++) {
-            Transport t = _transports.get(i);
+        for (Transport t : _transports.values()) {
             transports.put(t.getStyle(), t);
         }
         for (Iterator iter = transports.values().iterator(); iter.hasNext(); ) {
@@ -506,8 +498,7 @@ public class TransportManager implements TransportEventListener {
 
         StringBuilder buf = new StringBuilder(4*1024);
         buf.append("<h3>").append(_("Router Transport Addresses")).append("</h3><pre>\n");
-        for (int i = 0; i < _transports.size(); i++) {
-            Transport t = _transports.get(i);
+        for (Transport t : _transports.values()) {
             if (t.getCurrentAddress() != null)
                 buf.append(t.getCurrentAddress());
             else
diff --git a/router/java/src/net/i2p/router/transport/UPnP.java b/router/java/src/net/i2p/router/transport/UPnP.java
index aa2261cfc779527ce8ce4e578dc219b78ae8485e..d13a51cecdc97acdd9d9b1ff096492f250a0586d 100644
--- a/router/java/src/net/i2p/router/transport/UPnP.java
+++ b/router/java/src/net/i2p/router/transport/UPnP.java
@@ -581,6 +581,8 @@ public class UPnP extends ControlPoint implements DeviceChangeListener, EventLis
 					// If not in portsForwarded, it wasn't successful, try again
 					if(portsForwarded.contains(port)) {
 						// We have forwarded it, and it should be forwarded, cool.
+						// Big problem here, if firewall resets, we don't know it.
+						// Do we need to re-forward anyway? or poll the router?
 					} else {
 						// Needs forwarding
 						if(portsToForwardNow == null) portsToForwardNow = new HashSet<ForwardPort>();
diff --git a/router/java/src/net/i2p/router/transport/udp/ACKBitfield.java b/router/java/src/net/i2p/router/transport/udp/ACKBitfield.java
index f0201c4e8a9398c076f2884e84ada1baadffe264..c8b7c96309e414a9b14954f1bf090fd3e3b5aa19 100644
--- a/router/java/src/net/i2p/router/transport/udp/ACKBitfield.java
+++ b/router/java/src/net/i2p/router/transport/udp/ACKBitfield.java
@@ -4,7 +4,7 @@ package net.i2p.router.transport.udp;
  * Generic means of SACK/NACK transmission for partially or fully 
  * received messages
  */
-public interface ACKBitfield {
+interface ACKBitfield {
     /** what message is this partially ACKing? */
     public long getMessageId(); 
     /** how many fragments are covered in this bitfield? */
diff --git a/router/java/src/net/i2p/router/transport/udp/ACKSender.java b/router/java/src/net/i2p/router/transport/udp/ACKSender.java
index 180232d6f24046c835b6fd0ab82f5c0c3099dfba..d6035766db1410fbf757b842ce3e52ce6bca2b02 100644
--- a/router/java/src/net/i2p/router/transport/udp/ACKSender.java
+++ b/router/java/src/net/i2p/router/transport/udp/ACKSender.java
@@ -15,7 +15,7 @@ import net.i2p.util.Log;
  * any outstanding ACKs.  
  *
  */
-public class ACKSender implements Runnable {
+class ACKSender implements Runnable {
     private RouterContext _context;
     private Log _log;
     private UDPTransport _transport;
diff --git a/router/java/src/net/i2p/router/transport/udp/DummyThrottle.java b/router/java/src/net/i2p/router/transport/udp/DummyThrottle.java
index 383f807c5965eba299b1a2fa970ea2c919f428ca..d2cb73934cca7533353949eda435bfdd217319ab 100644
--- a/router/java/src/net/i2p/router/transport/udp/DummyThrottle.java
+++ b/router/java/src/net/i2p/router/transport/udp/DummyThrottle.java
@@ -14,7 +14,7 @@ import net.i2p.router.OutNetMessage;
  *
  * @since 0.7.12
  */
-public class DummyThrottle implements OutboundMessageFragments.ActiveThrottle {
+class DummyThrottle implements OutboundMessageFragments.ActiveThrottle {
 
     public DummyThrottle() {
     }
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 2e1fea71f4816e6ee86b23b7da9d92b6e955b3ca..d19f929c275b22df983e31aef2655689e376140f 100644
--- a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
@@ -33,7 +33,7 @@ import net.i2p.util.SimpleTimer;
  * as well as to drop any failed establishment attempts.
  *
  */
-public class EstablishmentManager {
+class EstablishmentManager {
     private final RouterContext _context;
     private final Log _log;
     private final UDPTransport _transport;
@@ -316,6 +316,40 @@ public class EstablishmentManager {
         }
     }
 
+    /**
+     * Got a SessionDestroy on an established conn
+     */
+    void receiveSessionDestroy(RemoteHostId from, PeerState state) {
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Receive session destroy (EST) from: " + from);
+        _transport.dropPeer(state, false, "received destroy message");
+    }
+
+    /**
+     * Got a SessionDestroy during outbound establish
+     */
+    void receiveSessionDestroy(RemoteHostId from, OutboundEstablishState state) {
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Receive session destroy (OB) from: " + from);
+        _outboundStates.remove(from);
+        Hash peer = state.getRemoteIdentity().calculateHash();
+        _transport.dropPeer(peer, false, "received destroy message");
+    }
+
+    /**
+     * Got a SessionDestroy - maybe after an inbound establish
+     */
+    void receiveSessionDestroy(RemoteHostId from) {
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Receive session destroy (IB) from: " + from);
+        InboundEstablishState state = _inboundStates.remove(from);
+        if (state != null) {
+            Hash peer = state.getConfirmedIdentity().calculateHash();
+            if (peer != null)
+                _transport.dropPeer(peer, false, "received destroy message");
+        }
+    }
+
     /**
      * A data packet arrived on an outbound connection being established, which
      * means its complete (yay!).  This is a blocking call, more than I'd like...
diff --git a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java
index 829f62ee00321d6656ccfacfb78e4691c961d79b..ea40c616c87d20aac7023f095640f03c61027a25 100644
--- a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java
+++ b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java
@@ -20,7 +20,7 @@ import net.i2p.util.Log;
  * we are Bob.
  *
  */
-public class InboundEstablishState {
+class InboundEstablishState {
     private final RouterContext _context;
     private final Log _log;
     // SessionRequest message
@@ -218,6 +218,13 @@ public class InboundEstablishState {
         if (_receivedIdentity == null)
             _receivedIdentity = new byte[conf.readTotalFragmentNum()][];
         int cur = conf.readCurrentFragmentNum();
+        if (cur >= _receivedIdentity.length) {
+            // avoid AIOOBE
+            // should do more than this, but what? disconnect?
+            fail();
+            packetReceived();
+            return;
+        }
         if (_receivedIdentity[cur] == null) {
             byte fragment[] = new byte[conf.readCurrentFragmentSize()];
             conf.readFragmentData(fragment, 0);
diff --git a/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java b/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java
index ddd44b1436c4852a60b249adb7890442c7824e93..bc0ddc5f5e1fdc0fabec9c8a0d520028b05e2617 100644
--- a/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java
+++ b/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java
@@ -17,7 +17,7 @@ import net.i2p.util.Log;
  * basic line of defense here).
  *
  */
-public class InboundMessageFragments /*implements UDPTransport.PartialACKSource */{
+class InboundMessageFragments /*implements UDPTransport.PartialACKSource */{
     private RouterContext _context;
     private Log _log;
     /** list of message IDs recently received, so we can ignore in flight dups */
diff --git a/router/java/src/net/i2p/router/transport/udp/InboundMessageState.java b/router/java/src/net/i2p/router/transport/udp/InboundMessageState.java
index 3159cad8734bbd4a4d3c126ceae7678d7e4df99b..9261dc70a383a681fbb236fdd59da2808169fe85 100644
--- a/router/java/src/net/i2p/router/transport/udp/InboundMessageState.java
+++ b/router/java/src/net/i2p/router/transport/udp/InboundMessageState.java
@@ -11,7 +11,7 @@ import net.i2p.util.Log;
  * Hold the raw data fragments of an inbound message
  *
  */
-public class InboundMessageState {
+class InboundMessageState {
     private RouterContext _context;
     private Log _log;
     private long _messageId;
diff --git a/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java b/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java
index 6a2707f6080d1683f359d3d97de4fdc7bf19da95..326c43f15d1b4721d641b7a9db81df3e5d6619ad 100644
--- a/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java
@@ -20,7 +20,7 @@ import net.i2p.util.Log;
 /**
  *
  */
-public class IntroductionManager {
+class IntroductionManager {
     private RouterContext _context;
     private Log _log;
     private UDPTransport _transport;
@@ -96,7 +96,10 @@ public class IntroductionManager {
         int sz = peers.size();
         start = start % sz;
         int found = 0;
-        long inactivityCutoff = _context.clock().now() - (UDPTransport.EXPIRE_TIMEOUT / 2);
+        long inactivityCutoff = _context.clock().now() - (UDPTransport.EXPIRE_TIMEOUT / 2);    // 15 min
+        // if not too many to choose from, be less picky
+        if (sz <= howMany + 2)
+            inactivityCutoff -= UDPTransport.EXPIRE_TIMEOUT / 4;
         for (int i = 0; i < sz && found < howMany; i++) {
             PeerState cur = peers.get((start + i) % sz);
             RouterInfo ri = _context.netDb().lookupRouterInfoLocally(cur.getRemotePeer());
@@ -119,7 +122,11 @@ public class IntroductionManager {
                 continue;
             }
             // Try to pick active peers...
-            if (cur.getLastReceiveTime() < inactivityCutoff || cur.getLastSendTime() < inactivityCutoff) {
+            // FIXME this is really strict and causes us to run out of introducers
+            // We have much less introducers than we used to have because routers don't offer
+            // if they are approaching max connections (see EstablishmentManager)
+            // FIXED, was ||, is this OK now?
+            if (cur.getLastReceiveTime() < inactivityCutoff && cur.getLastSendTime() < inactivityCutoff) {
                 if (_log.shouldLog(Log.INFO))
                     _log.info("Peer is idle too long: " + cur);
                 continue;
@@ -135,6 +142,8 @@ public class IntroductionManager {
             found++;
         }
 
+        // FIXME failsafe if found == 0, relax inactivityCutoff and try again?
+
         // Try to keep the connection up for two hours after we made anybody an introducer
         long pingCutoff = _context.clock().now() - (2 * 60 * 60 * 1000);
         inactivityCutoff = _context.clock().now() - (UDPTransport.EXPIRE_TIMEOUT / 4);
@@ -156,7 +165,7 @@ public class IntroductionManager {
      * Not as elaborate as pickInbound() above.
      * Just a quick check to see how many volunteers we know,
      * which the Transport uses to see if we need more.
-     * @return number of peers that have volunteerd to introduce us
+     * @return number of peers that have volunteered to introduce us
      */
     int introducerCount() {
             return _inbound.size();
diff --git a/router/java/src/net/i2p/router/transport/udp/MessageQueue.java b/router/java/src/net/i2p/router/transport/udp/MessageQueue.java
index cc38ebbafec279bca630c800643bc32c2177e144..3d51055ca87a4da76f8d8e0679282285a1db8309 100644
--- a/router/java/src/net/i2p/router/transport/udp/MessageQueue.java
+++ b/router/java/src/net/i2p/router/transport/udp/MessageQueue.java
@@ -5,7 +5,7 @@ import net.i2p.router.OutNetMessage;
 /**
  * Base queue for messages not yet packetized
  */
-public interface MessageQueue {
+interface MessageQueue {
     /**
      * Get the next message, blocking until one is found or the expiration
      * reached.
diff --git a/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java b/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java
index 1a95c227fe6d42a29cfc3bf61a6f04e150692980..08b6088c4ffbd01295121adbed5559037ca30ca9 100644
--- a/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java
+++ b/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java
@@ -19,7 +19,7 @@ import net.i2p.util.Log;
  * parse 'em into I2NPMessages, and stick them on the 
  * {@link net.i2p.router.InNetMessagePool} by way of the {@link UDPTransport}.
  */
-public class MessageReceiver {
+class MessageReceiver {
     private RouterContext _context;
     private Log _log;
     private UDPTransport _transport;
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
index 6e3738f306d117fec298375bdbcbf93f415526c8..1e23a210c5bf33dad9ee794006eafa23512a534f 100644
--- a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
@@ -21,7 +21,7 @@ import net.i2p.util.Log;
  * they are Bob.
  *
  */
-public class OutboundEstablishState {
+class OutboundEstablishState {
     private final RouterContext _context;
     private final Log _log;
     // SessionRequest message
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java b/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java
index 2d4f272929e9e098433f131799b854cbe66f8b64..e25ab036915dcb28ccd19d7a26e59ef62d4dcb24 100644
--- a/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java
@@ -22,7 +22,7 @@ import net.i2p.util.Log;
  * message.
  *
  */
-public class OutboundMessageFragments {
+class OutboundMessageFragments {
     private RouterContext _context;
     private Log _log;
     private UDPTransport _transport;
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java b/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java
index 877ad10b174ab43c7591ec1856c8c906d383e5e2..a545cfb06258d7a3e2a4bb9e8d48015f8c708335 100644
--- a/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java
@@ -15,7 +15,7 @@ import net.i2p.util.Log;
  * Maintain the outbound fragmentation for resending
  *
  */
-public class OutboundMessageState {
+class OutboundMessageState {
     private I2PAppContext _context;
     private Log _log;
     /** may be null if we are part of the establishment */
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundRefiller.java b/router/java/src/net/i2p/router/transport/udp/OutboundRefiller.java
index 841588ee4548491c2eb1551d79d2c3a3523f82a4..078105a4a7b1c00a471c9c758167879cdd1c7f37 100644
--- a/router/java/src/net/i2p/router/transport/udp/OutboundRefiller.java
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundRefiller.java
@@ -12,7 +12,7 @@ import net.i2p.util.Log;
  * WARNING - UNUSED since 0.6.1.11
  *
  */
-public class OutboundRefiller implements Runnable {
+class OutboundRefiller implements Runnable {
     private RouterContext _context;
     private Log _log;
     private OutboundMessageFragments _fragments;
diff --git a/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java b/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java
index 3ebbdd34382d3f3d0eb1adbdef49a8753834ac3c..4227aaad0e2985cecff7122382d1fa2a5ee34f62 100644
--- a/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java
+++ b/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java
@@ -4,7 +4,6 @@ import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.Date;
 import java.util.List;
 
 import net.i2p.I2PAppContext;
@@ -22,8 +21,80 @@ import net.i2p.util.Log;
  * Big ol' class to do all our packet formatting.  The UDPPackets generated are
  * fully authenticated, encrypted, and configured for delivery to the peer. 
  *
+ * The following is from udp.html on the website:
+
+<p>
+All UDP datagrams begin with a 16 byte MAC (Message Authentication Code)
+and a 16 byte IV (Initialization Vector
+followed by a variable
+size payload encrypted with the appropriate key.  The MAC used is 
+HMAC-MD5, truncated to 16 bytes, while the key is a full 32 byte AES256 
+key.  The specific construct of the MAC is the first 16 bytes from:</p>
+<pre>
+  HMAC-MD5(payload || IV || (payloadLength ^ protocolVersion), macKey)
+</pre>
+
+<p>The protocol version is currently 0.</p>
+
+<p>The payload itself is AES256/CBC encrypted with the IV and the 
+sessionKey, with replay prevention addressed within its body, 
+explained below.  The payloadLength in the MAC is a 2 byte unsigned 
+integer in 2s complement.</p>
+  
+<p>The protocolVersion is a 2 byte unsigned integer in 2s complement,
+and currently set to 0.  Peers using a different protocol version will
+not be able to communicate with this peer, though earlier versions not
+using this flag are.</p>
+
+<h2><a name="payload">Payload</a></h2>
+
+<p>Within the AES encrypted payload, there is a minimal common structure
+to the various messages - a one byte flag and a four byte sending 
+timestamp (*seconds* since the unix epoch).  The flag byte contains 
+the following bitfields:</p>
+<pre>
+  bits 0-3: payload type
+     bit 4: rekey?
+     bit 5: extended options included
+  bits 6-7: reserved
+</pre>
+
+<p>If the rekey flag is set, 64 bytes of keying material follow the 
+timestamp.  If the extended options flag is set, a one byte option 
+size value is appended to, followed by that many extended option 
+bytes, which are currently uninterpreted.</p>
+
+<p>When rekeying, the first 32 bytes of the keying material is fed 
+into a SHA256 to produce the new MAC key, and the next 32 bytes are
+fed into a SHA256 to produce the new session key, though the keys are
+not immediately used.  The other side should also reply with the 
+rekey flag set and that same keying material.  Once both sides have 
+sent and received those values, the new keys should be used and the 
+previous keys discarded.  It may be useful to keep the old keys 
+around briefly, to address packet loss and reordering.</p>
+
+<p>NOTE: Rekeying is currently unimplemented.</p>
+
+<pre>
+ Header: 37+ bytes
+ +----+----+----+----+----+----+----+----+
+ |                  MAC                  |
+ |                                       |
+ +----+----+----+----+----+----+----+----+
+ |                   IV                  |
+ |                                       |
+ +----+----+----+----+----+----+----+----+
+ |flag|        time       | (optionally  |
+ +----+----+----+----+----+              |
+ | this may have 64 byte keying material |
+ | and/or a one+N byte extended options) |
+ +---------------------------------------|
+</pre>
+
+ *
+ *
  */
-public class PacketBuilder {
+class PacketBuilder {
     private I2PAppContext _context;
     private Log _log;
     private UDPTransport _transport;
@@ -62,10 +133,16 @@ public class PacketBuilder {
         _context.statManager().createRateStat("udp.packetAuthTimeSlow", "How long it takes to encrypt and MAC a packet for sending (when its slow)", "udp", UDPTransport.RATES);
     }
     
+/****
     public UDPPacket buildPacket(OutboundMessageState state, int fragment, PeerState peer) {
         return buildPacket(state, fragment, peer, null, null);
     }
+****/
+
     /**
+     * This builds a data packet (PAYLOAD_TYPE_DATA).
+     * See the methods below for the other message types.
+     *
      * @param ackIdsRemaining list of messageIds (Long) that should be acked by this packet.  
      *                        The list itself is passed by reference, and if a messageId is
      *                        transmitted and the sender does not want the ID to be included
@@ -78,7 +155,9 @@ public class PacketBuilder {
      *                        included, it should be removed from the list.
      */
     public UDPPacket buildPacket(OutboundMessageState state, int fragment, PeerState peer, List<Long> ackIdsRemaining, List<ACKBitfield> partialACKsRemaining) {
-        UDPPacket packet = UDPPacket.acquire(_context, false);
+        UDPPacket packet = buildPacketHeader((byte)(UDPPacket.PAYLOAD_TYPE_DATA << 4));
+        byte data[] = packet.getPacket().getData();
+        int off = HEADER_SIZE;
 
         StringBuilder msg = null;
         boolean acksIncluded = false;
@@ -90,19 +169,6 @@ public class PacketBuilder {
                 msg.append("*");
         }
         
-        byte data[] = packet.getPacket().getData();
-        Arrays.fill(data, 0, data.length, (byte)0x0);
-        int start = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
-        int off = start;
-        
-        // header
-        data[off] |= (UDPPacket.PAYLOAD_TYPE_DATA << 4);
-        // todo: add support for rekeying and extended options
-        off++;
-        long now = _context.clock().now() / 1000;
-        DataHelper.toLong(data, off, 4, now);
-        off += 4;
-        
         // ok, now for the body...
         
         // just always ask for an ACK for now...
@@ -231,8 +297,10 @@ public class PacketBuilder {
         return packet;
     }
     
-    // We use this for keepalive purposes.
-    // It doesn't generate a reply, but that's ok.
+    /**
+     * We use this for keepalive purposes.
+     * It doesn't generate a reply, but that's ok.
+     */
     public UDPPacket buildPing(PeerState peer) {
         return buildACK(peer, Collections.EMPTY_LIST);
     }
@@ -242,11 +310,14 @@ public class PacketBuilder {
     /**
      *  Build the ack packet. The list need not be sorted into full and partial;
      *  this method will put all fulls before the partials in the outgoing packet.
+     *  An ack packet is just a data packet with no data.
      *
      * @param ackBitfields list of ACKBitfield instances to either fully or partially ACK
      */
     public UDPPacket buildACK(PeerState peer, List<ACKBitfield> ackBitfields) {
-        UDPPacket packet = UDPPacket.acquire(_context, false);
+        UDPPacket packet = buildPacketHeader((byte)(UDPPacket.PAYLOAD_TYPE_DATA << 4));
+        byte data[] = packet.getPacket().getData();
+        int off = HEADER_SIZE;
         
         StringBuilder msg = null;
         if (_log.shouldLog(Log.DEBUG)) {
@@ -254,18 +325,6 @@ public class PacketBuilder {
             msg.append("building ACK packet to ").append(peer.getRemotePeer().toBase64().substring(0,6));
         }
 
-        byte data[] = packet.getPacket().getData();
-        Arrays.fill(data, 0, data.length, (byte)0x0);
-        int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
-        
-        // header
-        data[off] |= (UDPPacket.PAYLOAD_TYPE_DATA << 4);
-        // todo: add support for rekeying and extended options
-        off++;
-        long now = _context.clock().now() / 1000;
-        DataHelper.toLong(data, off, 4, now);
-        off += 4;
-        
         int fullACKCount = 0;
         int partialACKCount = 0;
         for (int i = 0; i < ackBitfields.size(); i++) {
@@ -352,7 +411,9 @@ public class PacketBuilder {
      * @return ready to send packet, or null if there was a problem
      */
     public UDPPacket buildSessionCreatedPacket(InboundEstablishState state, int externalPort, SessionKey ourIntroKey) {
-        UDPPacket packet = UDPPacket.acquire(_context, false);
+        UDPPacket packet = buildPacketHeader(SESSION_CREATED_FLAG_BYTE);
+        byte data[] = packet.getPacket().getData();
+        int off = HEADER_SIZE;
         
         InetAddress to = null;
         try {
@@ -366,17 +427,6 @@ public class PacketBuilder {
 
         state.prepareSessionCreated();
         
-        byte data[] = packet.getPacket().getData();
-        Arrays.fill(data, 0, data.length, (byte)0x0);
-        int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
-        
-        // header
-        data[off] = SESSION_CREATED_FLAG_BYTE;
-        off++;
-        long now = _context.clock().now() / 1000;
-        DataHelper.toLong(data, off, 4, now);
-        off += 4;
-        
         byte sentIP[] = state.getSentIP();
         if ( (sentIP == null) || (sentIP.length <= 0) || ( (_transport != null) && (!_transport.isValid(sentIP)) ) ) {
             if (_log.shouldLog(Log.ERROR))
@@ -455,7 +505,10 @@ public class PacketBuilder {
      * @return ready to send packet, or null if there was a problem
      */
     public UDPPacket buildSessionRequestPacket(OutboundEstablishState state) {
-        UDPPacket packet = UDPPacket.acquire(_context, false);
+        UDPPacket packet = buildPacketHeader(SESSION_REQUEST_FLAG_BYTE);
+        byte data[] = packet.getPacket().getData();
+        int off = HEADER_SIZE;
+
         byte toIP[] = state.getSentIP();
         if ( (_transport !=null) && (!_transport.isValid(toIP)) ) {
             packet.release();
@@ -470,19 +523,8 @@ public class PacketBuilder {
             packet.release();
             return null;
         }
-        
-        byte data[] = packet.getPacket().getData();
-        Arrays.fill(data, 0, data.length, (byte)0x0);
-        int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
-        
-        // header
-        data[off] = SESSION_REQUEST_FLAG_BYTE;
-        off++;
-        long now = _context.clock().now() / 1000;
-        DataHelper.toLong(data, off, 4, now);
         if (_log.shouldLog(Log.DEBUG))
-            _log.debug("Sending request with time = " + new Date(now*1000));
-        off += 4;
+            _log.debug("Sending request");
         
         // now for the body
         System.arraycopy(state.getSentX(), 0, data, off, state.getSentX().length);
@@ -542,7 +584,10 @@ public class PacketBuilder {
      * @return ready to send packets, or null if there was a problem
      */
     public UDPPacket buildSessionConfirmedPacket(OutboundEstablishState state, int fragmentNum, int numFragments, byte identity[]) {
-        UDPPacket packet = UDPPacket.acquire(_context, false);
+        UDPPacket packet = buildPacketHeader(SESSION_CONFIRMED_FLAG_BYTE);
+        byte data[] = packet.getPacket().getData();
+        int off = HEADER_SIZE;
+
         InetAddress to = null;
         try {
             to = InetAddress.getByAddress(state.getSentIP());
@@ -553,17 +598,6 @@ public class PacketBuilder {
             return null;
         }
         
-        byte data[] = packet.getPacket().getData();
-        Arrays.fill(data, 0, data.length, (byte)0x0);
-        int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
-        
-        // header
-        data[off] = SESSION_CONFIRMED_FLAG_BYTE;
-        off++;
-        long now = _context.clock().now() / 1000;
-        DataHelper.toLong(data, off, 4, now);
-        off += 4;
-        
         // now for the body
         data[off] |= fragmentNum << 4;
         data[off] |= (numFragments & 0xF);
@@ -620,6 +654,36 @@ public class PacketBuilder {
     }
 
     
+    /**
+     *  Build a destroy packet, which contains a header but no body.
+     *
+     *  @since 0.8.1
+     */
+    public UDPPacket buildSessionDestroyPacket(PeerState peer) {
+        UDPPacket packet = buildPacketHeader((byte)(UDPPacket.PAYLOAD_TYPE_SESSION_DESTROY << 4));
+        byte data[] = packet.getPacket().getData();
+        int off = HEADER_SIZE;
+        
+        StringBuilder msg = null;
+        if (_log.shouldLog(Log.DEBUG)) {
+            msg = new StringBuilder(128);
+            msg.append("building session destroy packet to ").append(peer.getRemotePeer().toBase64().substring(0,6));
+        }
+
+        // no body in this message
+
+        if (msg != null)
+            _log.debug(msg.toString());
+        
+        // pad up so we're on the encryption boundary
+        if ( (off % 16) != 0)
+            off += 16 - (off % 16);
+        packet.getPacket().setLength(off);
+        authenticate(packet, peer.getCurrentCipherKey(), peer.getCurrentMACKey());
+        setTo(packet, peer.getRemoteIPAddress(), peer.getRemotePort());
+        return packet;
+    }
+    
     /** 
      * full flag info for a peerTest message.  this can be fixed, 
      * since we never rekey on test, and don't need any extended options
@@ -636,19 +700,11 @@ public class PacketBuilder {
         return buildPeerTestFromAlice(toIP, toPort, toIntroKey, toIntroKey, nonce, aliceIntroKey);
     }
     public UDPPacket buildPeerTestFromAlice(InetAddress toIP, int toPort, SessionKey toCipherKey, SessionKey toMACKey, long nonce, SessionKey aliceIntroKey) {
-        UDPPacket packet = UDPPacket.acquire(_context, false);
+        UDPPacket packet = buildPacketHeader(PEER_TEST_FLAG_BYTE);
         byte data[] = packet.getPacket().getData();
-        Arrays.fill(data, 0, data.length, (byte)0x0);
-        int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
-        
-        // header
-        data[off] = PEER_TEST_FLAG_BYTE;
-        off++;
-        long now = _context.clock().now() / 1000;
-        DataHelper.toLong(data, off, 4, now);
+        int off = HEADER_SIZE;
         if (_log.shouldLog(Log.DEBUG))
-            _log.debug("Sending peer test " + nonce + " to Bob with time = " + new Date(now*1000));
-        off += 4;
+            _log.debug("Sending peer test " + nonce + " to Bob");
         
         // now for the body
         DataHelper.toLong(data, off, 4, nonce);
@@ -678,19 +734,11 @@ public class PacketBuilder {
      * @return ready to send packet, or null if there was a problem
      */
     public UDPPacket buildPeerTestToAlice(InetAddress aliceIP, int alicePort, SessionKey aliceIntroKey, SessionKey charlieIntroKey, long nonce) {
-        UDPPacket packet = UDPPacket.acquire(_context, false);
+        UDPPacket packet = buildPacketHeader(PEER_TEST_FLAG_BYTE);
         byte data[] = packet.getPacket().getData();
-        Arrays.fill(data, 0, data.length, (byte)0x0);
-        int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
-        
-        // header
-        data[off] = PEER_TEST_FLAG_BYTE;
-        off++;
-        long now = _context.clock().now() / 1000;
-        DataHelper.toLong(data, off, 4, now);
+        int off = HEADER_SIZE;
         if (_log.shouldLog(Log.DEBUG))
-            _log.debug("Sending peer test " + nonce + " to Alice with time = " + new Date(now*1000));
-        off += 4;
+            _log.debug("Sending peer test " + nonce + " to Alice");
         
         // now for the body
         DataHelper.toLong(data, off, 4, nonce);
@@ -725,19 +773,11 @@ public class PacketBuilder {
     public UDPPacket buildPeerTestToCharlie(InetAddress aliceIP, int alicePort, SessionKey aliceIntroKey, long nonce, 
                                             InetAddress charlieIP, int charliePort, 
                                             SessionKey charlieCipherKey, SessionKey charlieMACKey) {
-        UDPPacket packet = UDPPacket.acquire(_context, false);
+        UDPPacket packet = buildPacketHeader(PEER_TEST_FLAG_BYTE);
         byte data[] = packet.getPacket().getData();
-        Arrays.fill(data, 0, data.length, (byte)0x0);
-        int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
-        
-        // header
-        data[off] = PEER_TEST_FLAG_BYTE;
-        off++;
-        long now = _context.clock().now() / 1000;
-        DataHelper.toLong(data, off, 4, now);
+        int off = HEADER_SIZE;
         if (_log.shouldLog(Log.DEBUG))
-            _log.debug("Sending peer test " + nonce + " to Charlie with time = " + new Date(now*1000));
-        off += 4;
+            _log.debug("Sending peer test " + nonce + " to Charlie");
         
         // now for the body
         DataHelper.toLong(data, off, 4, nonce);
@@ -770,19 +810,11 @@ public class PacketBuilder {
      * @return ready to send packet, or null if there was a problem
      */
     public UDPPacket buildPeerTestToBob(InetAddress bobIP, int bobPort, InetAddress aliceIP, int alicePort, SessionKey aliceIntroKey, long nonce, SessionKey bobCipherKey, SessionKey bobMACKey) {
-        UDPPacket packet = UDPPacket.acquire(_context, false);
+        UDPPacket packet = buildPacketHeader(PEER_TEST_FLAG_BYTE);
         byte data[] = packet.getPacket().getData();
-        Arrays.fill(data, 0, data.length, (byte)0x0);
-        int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
-        
-        // header
-        data[off] = PEER_TEST_FLAG_BYTE;
-        off++;
-        long now = _context.clock().now() / 1000;
-        DataHelper.toLong(data, off, 4, now);
+        int off = HEADER_SIZE;
         if (_log.shouldLog(Log.DEBUG))
-            _log.debug("Sending peer test " + nonce + " to Bob with time = " + new Date(now*1000));
-        off += 4;
+            _log.debug("Sending peer test " + nonce + " to Bob");
         
         // now for the body
         DataHelper.toLong(data, off, 4, nonce);
@@ -846,22 +878,15 @@ public class PacketBuilder {
     }
     
     public UDPPacket buildRelayRequest(InetAddress introHost, int introPort, byte introKey[], long introTag, SessionKey ourIntroKey, long introNonce, boolean encrypt) {
-        UDPPacket packet = UDPPacket.acquire(_context, false);
+        UDPPacket packet = buildPacketHeader(PEER_RELAY_REQUEST_FLAG_BYTE);
         byte data[] = packet.getPacket().getData();
-        Arrays.fill(data, 0, data.length, (byte)0x0);
-        int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
+        int off = HEADER_SIZE;
         
         byte ourIP[] = getOurExplicitIP();
         int ourPort = getOurExplicitPort();
         
-        // header
-        data[off] = PEER_RELAY_REQUEST_FLAG_BYTE;
-        off++;
-        long now = _context.clock().now() / 1000;
-        DataHelper.toLong(data, off, 4, now);
         if (_log.shouldLog(Log.INFO))
             _log.info("Sending intro relay request to " + introHost + ":" + introPort); // + " regarding " + state.getRemoteIdentity().calculateHash().toBase64());
-        off += 4;
         
         // now for the body
         DataHelper.toLong(data, off, 4, introTag);
@@ -915,19 +940,11 @@ public class PacketBuilder {
     private static final byte PEER_RELAY_INTRO_FLAG_BYTE = (UDPPacket.PAYLOAD_TYPE_RELAY_INTRO << 4);
     
     UDPPacket buildRelayIntro(RemoteHostId alice, PeerState charlie, UDPPacketReader.RelayRequestReader request) {
-        UDPPacket packet = UDPPacket.acquire(_context, false);
+        UDPPacket packet = buildPacketHeader(PEER_RELAY_INTRO_FLAG_BYTE);
         byte data[] = packet.getPacket().getData();
-        Arrays.fill(data, 0, data.length, (byte)0x0);
-        int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
-        
-        // header
-        data[off] = PEER_RELAY_INTRO_FLAG_BYTE;
-        off++;
-        long now = _context.clock().now() / 1000;
-        DataHelper.toLong(data, off, 4, now);
+        int off = HEADER_SIZE;
         if (_log.shouldLog(Log.INFO))
             _log.info("Sending intro to " + charlie + " for " + alice);
-        off += 4;
         
         // now for the body
         byte ip[] = alice.getIP();
@@ -972,17 +989,9 @@ public class PacketBuilder {
             return null;
         }
         
-        UDPPacket packet = UDPPacket.acquire(_context, false);
+        UDPPacket packet = buildPacketHeader(PEER_RELAY_RESPONSE_FLAG_BYTE);
         byte data[] = packet.getPacket().getData();
-        Arrays.fill(data, 0, data.length, (byte)0x0);
-        int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
-        
-        // header
-        data[off] = PEER_RELAY_RESPONSE_FLAG_BYTE;
-        off++;
-        long now = _context.clock().now() / 1000;
-        DataHelper.toLong(data, off, 4, now);
-        off += 4;
+        int off = HEADER_SIZE;
         
         if (_log.shouldLog(Log.INFO))
             _log.info("Sending relay response to " + alice + " for " + charlie + " with alice's intro key " + aliceIntroKey.toBase64());
@@ -1019,11 +1028,13 @@ public class PacketBuilder {
         return packet;
     }
     
+    /**
+     *  Sends an empty unauthenticated packet for hole punching
+     */
     public UDPPacket buildHolePunch(UDPPacketReader reader) {
         UDPPacket packet = UDPPacket.acquire(_context, false);
         byte data[] = packet.getPacket().getData();
         Arrays.fill(data, 0, data.length, (byte)0x0);
-        int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
         
         int ipSize = reader.getRelayIntroReader().readIPSize();
         byte ip[] = new byte[ipSize];
@@ -1052,7 +1063,34 @@ public class PacketBuilder {
         return packet;
     }
     
-    private void setTo(UDPPacket packet, InetAddress ip, int port) {
+    /** if no extended options or rekey data, which we don't support */
+    private static final int HEADER_SIZE = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE + 1 + 4;
+
+    /**
+     *  Create a new packet and add the flag byte and the time stamp.
+     *  Caller should add data starting at HEADER_SIZE.
+     *  At this point, adding support for extended options and rekeying is unlikely,
+     *  but if we do, we'll have to change this.
+     *
+     *  @param flagByte contains type and flags
+     *  @since 0.8.1
+     */
+    private UDPPacket buildPacketHeader(byte flagByte) {
+        UDPPacket packet = UDPPacket.acquire(_context, false);
+        byte data[] = packet.getPacket().getData();
+        Arrays.fill(data, 0, data.length, (byte)0x0);
+        int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
+        
+        // header
+        data[off] = flagByte;
+        off++;
+        long now = _context.clock().now() / 1000;
+        DataHelper.toLong(data, off, 4, now);
+        // todo: add support for rekeying and extended options
+        return packet;
+    }
+
+    private static void setTo(UDPPacket packet, InetAddress ip, int port) {
         packet.getPacket().setAddress(ip);
         packet.getPacket().setPort(port);
     }
diff --git a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
index 1a35c5d195741dc298b0f73968848080d86d285e..d130a4c8382c886b9c427bc0137f79d59f194fb1 100644
--- a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
+++ b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
@@ -19,7 +19,7 @@ import net.i2p.util.Log;
  * receiver's queue and pushing them as necessary.
  *
  */
-public class PacketHandler {
+class PacketHandler {
     private RouterContext _context;
     private Log _log;
     private UDPTransport _transport;
@@ -186,6 +186,10 @@ public class PacketHandler {
         }
     //}
 
+        /**
+         * Initial handling, called for every packet
+         * Find the state and call the correct receivePacket() variant
+         */
         private void handlePacket(UDPPacketReader reader, UDPPacket packet) {
             if (packet == null) return;
 
@@ -229,6 +233,10 @@ public class PacketHandler {
             }
         }
 
+        /**
+         * Established conn
+         * Decrypt and validate the packet then call handlePacket()
+         */
         private void receivePacket(UDPPacketReader reader, UDPPacket packet, PeerState state) {
             _state = 17;
             boolean isValid = packet.validate(state.getCurrentMACKey());
@@ -280,6 +288,12 @@ public class PacketHandler {
             _state = 26;
         }
 
+        /**
+         * New conn or failed validation
+         * Decrypt and validate the packet then call handlePacket()
+         *
+         * @param peerType OUTBOUND_FALLBACK, INBOUND_FALLBACK, or NEW_PEER
+         */
         private void receivePacket(UDPPacketReader reader, UDPPacket packet, short peerType) {
             _state = 27;
             boolean isValid = packet.validate(_transport.getIntroKey());
@@ -314,7 +328,11 @@ public class PacketHandler {
         private void receivePacket(UDPPacketReader reader, UDPPacket packet, InboundEstablishState state) {
             receivePacket(reader, packet, state, true);
         }
+
         /**
+         * Inbound establishing conn
+         * Decrypt and validate the packet then call handlePacket()
+         *
          * @param allowFallback if it isn't valid for this establishment state, try as a non-establishment packet
          */
         private void receivePacket(UDPPacketReader reader, UDPPacket packet, InboundEstablishState state, boolean allowFallback) {
@@ -355,6 +373,10 @@ public class PacketHandler {
             }
         }
 
+        /**
+         * Outbound establishing conn
+         * Decrypt and validate the packet then call handlePacket()
+         */
         private void receivePacket(UDPPacketReader reader, UDPPacket packet, OutboundEstablishState state) {
             _state = 35;
             if ( (state != null) && (_log.shouldLog(Log.DEBUG)) ) {
@@ -406,6 +428,10 @@ public class PacketHandler {
 
         /**
          * Parse out the interesting bits and honor what it says
+         *
+         * @param state non-null if fully established
+         * @param outState non-null if outbound establishing in process
+         * @param inState unused always null
          */
         private void handlePacket(UDPPacketReader reader, UDPPacket packet, PeerState state, OutboundEstablishState outState, InboundEstablishState inState) {
             _state = 43;
@@ -525,6 +551,15 @@ public class PacketHandler {
                     _establisher.receiveRelayResponse(from, reader);
                     _context.statManager().addRateData("udp.receivePacketSize.relayResponse", packet.getPacket().getLength(), packet.getLifetime());
                     break;
+                case UDPPacket.PAYLOAD_TYPE_SESSION_DESTROY:
+                    _state = 53;
+                    if (outState != null)
+                        _establisher.receiveSessionDestroy(from, outState);
+                    else if (state != null)
+                        _establisher.receiveSessionDestroy(from, state);
+                    else
+                        _establisher.receiveSessionDestroy(from);
+                    break;
                 default:
                     _state = 52;
                     if (_log.shouldLog(Log.WARN))
diff --git a/router/java/src/net/i2p/router/transport/udp/PacketPusher.java b/router/java/src/net/i2p/router/transport/udp/PacketPusher.java
index 271e83597c3435af227841eb072ac02067c48ba1..50b8d3ba780b50ce6035aa0e08d047e69b791ca1 100644
--- a/router/java/src/net/i2p/router/transport/udp/PacketPusher.java
+++ b/router/java/src/net/i2p/router/transport/udp/PacketPusher.java
@@ -9,7 +9,7 @@ import net.i2p.util.Log;
  * pool and toss 'em onto the outbound packet queue
  *
  */
-public class PacketPusher implements Runnable {
+class PacketPusher implements Runnable {
     // private RouterContext _context;
     private Log _log;
     private OutboundMessageFragments _fragments;
diff --git a/router/java/src/net/i2p/router/transport/udp/PeerState.java b/router/java/src/net/i2p/router/transport/udp/PeerState.java
index 65fb1c0147caea54f2ea5300b29c108ce957967e..6c1dec3e4563de374ca5cb820c437fe98e0d6886 100644
--- a/router/java/src/net/i2p/router/transport/udp/PeerState.java
+++ b/router/java/src/net/i2p/router/transport/udp/PeerState.java
@@ -23,7 +23,7 @@ import net.i2p.util.ConcurrentHashSet;
  * Contain all of the state about a UDP connection to a peer.
  *
  */
-public class PeerState {
+class PeerState {
     private RouterContext _context;
     private Log _log;
     /**
diff --git a/router/java/src/net/i2p/router/transport/udp/TimedWeightedPriorityMessageQueue.java b/router/java/src/net/i2p/router/transport/udp/TimedWeightedPriorityMessageQueue.java
index acd0bd0b532dde527703d37d13e99e25ca5d0844..9721f22cce5ec39bf6f58ab83fcfeebe5d7e2030 100644
--- a/router/java/src/net/i2p/router/transport/udp/TimedWeightedPriorityMessageQueue.java
+++ b/router/java/src/net/i2p/router/transport/udp/TimedWeightedPriorityMessageQueue.java
@@ -20,7 +20,7 @@ import net.i2p.util.Log;
  * See comments in DQAT.java and mtn history ca. 2006-02-19
  *
  */
-public class TimedWeightedPriorityMessageQueue implements MessageQueue, OutboundMessageFragments.ActiveThrottle {
+class TimedWeightedPriorityMessageQueue implements MessageQueue, OutboundMessageFragments.ActiveThrottle {
     private RouterContext _context;
     private Log _log;
     /** FIFO queue of messages in a particular priority */
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPEndpoint.java b/router/java/src/net/i2p/router/transport/udp/UDPEndpoint.java
index ac9369d8eb7a059a7cf314d44c1bafece9d3e55c..fc2426700e5e52ba00832b75ee34615268544b82 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPEndpoint.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPEndpoint.java
@@ -11,7 +11,7 @@ import net.i2p.util.Log;
  * Coordinate the low level datagram socket, managing the UDPSender and
  * UDPReceiver
  */
-public class UDPEndpoint {
+class UDPEndpoint {
     private RouterContext _context;
     private Log _log;
     private int _listenPort;
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPPacket.java b/router/java/src/net/i2p/router/transport/udp/UDPPacket.java
index 5dc001e18f0efad6d5c77f138a19618960c27975..092431db3778d5d5118fbe3838e4cee61e4df55d 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPPacket.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPPacket.java
@@ -16,7 +16,7 @@ import net.i2p.util.Log;
  * of object instances to allow rapid reuse.
  *
  */
-public class UDPPacket {
+class UDPPacket {
     private I2PAppContext _context;
     private static Log _log;
     private volatile DatagramPacket _packet;
@@ -55,6 +55,7 @@ public class UDPPacket {
     public static final int IV_SIZE = 16;
     public static final int MAC_SIZE = 16;
     
+    /** Message types, 4 bits max */
     public static final int PAYLOAD_TYPE_SESSION_REQUEST = 0;
     public static final int PAYLOAD_TYPE_SESSION_CREATED = 1;
     public static final int PAYLOAD_TYPE_SESSION_CONFIRMED = 2;
@@ -63,13 +64,17 @@ public class UDPPacket {
     public static final int PAYLOAD_TYPE_RELAY_INTRO = 5;
     public static final int PAYLOAD_TYPE_DATA = 6;
     public static final int PAYLOAD_TYPE_TEST = 7;
+    /** @since 0.8.1 */
+    public static final int PAYLOAD_TYPE_SESSION_DESTROY = 8;
     
     // various flag fields for use in the data packets
     public static final byte DATA_FLAG_EXPLICIT_ACK = (byte)(1 << 7);
     public static final byte DATA_FLAG_ACK_BITFIELDS = (1 << 6);
+    // unused
     public static final byte DATA_FLAG_ECN = (1 << 4);
     public static final byte DATA_FLAG_WANT_ACKS = (1 << 3);
     public static final byte DATA_FLAG_WANT_REPLY = (1 << 2);
+    // unused
     public static final byte DATA_FLAG_EXTENDED = (1 << 1);
     
     public static final byte BITFIELD_CONTINUATION = (byte)(1 << 7);
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java b/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java
index aaf0f721a62ca4537362c99101b1314a5ac1e60b..493d62771871491644e84fc7429b84fccbd9762b 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java
@@ -15,7 +15,7 @@ import net.i2p.util.Log;
  * elements, grab the appropriate subreader.
  *
  */
-public class UDPPacketReader {
+class UDPPacketReader {
     private I2PAppContext _context;
     private Log _log;
     private byte _message[];
@@ -125,6 +125,8 @@ public class UDPPacketReader {
                 return "Relay request packet";
             case UDPPacket.PAYLOAD_TYPE_RELAY_RESPONSE:
                 return "Relay response packet";
+            case UDPPacket.PAYLOAD_TYPE_SESSION_DESTROY:
+                return "Session destroyed packet";
             default:
                 return "Other packet type...";
         }
@@ -135,6 +137,8 @@ public class UDPPacketReader {
             buf.append(Base64.encode(_message, _payloadBeginOffset, _payloadLength));
     }
     
+    /* ------- Begin Reader Classes ------- */
+
     /** Help read the SessionRequest payload */
     public class SessionRequestReader {
         public static final int X_LENGTH = 256;
@@ -755,7 +759,9 @@ public class UDPPacketReader {
         }
     }
     
+    /* ------- End Reader Classes ------- */
     
+/******
     public static void main(String args[]) {
         I2PAppContext ctx = I2PAppContext.getGlobalContext();
         try {
@@ -781,4 +787,5 @@ public class UDPPacketReader {
         }
         
     }
+*******/
 }
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java b/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java
index 88cb207791269b7c3b2784f797cb20ea7f6c7b80..0a1937db10d8c7c59e9c21da37b49536e6e7a563 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java
@@ -19,7 +19,7 @@ import net.i2p.util.SimpleTimer;
  * from the queue ASAP by a {@link PacketHandler}
  *
  */
-public class UDPReceiver {
+class UDPReceiver {
     private RouterContext _context;
     private Log _log;
     private DatagramSocket _socket;
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPSender.java b/router/java/src/net/i2p/router/transport/udp/UDPSender.java
index 745b50df4b57b35ae11686cdc2048e782b9cb91e..01d10ed1c5c17e4b6019fcc358a6c9c6f70219c5 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPSender.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPSender.java
@@ -15,7 +15,7 @@ import net.i2p.util.Log;
  * Lowest level packet sender, pushes anything on its queue ASAP.
  *
  */
-public class UDPSender {
+class UDPSender {
     private RouterContext _context;
     private Log _log;
     private DatagramSocket _socket;
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 5570b83893f2bc4b5449553e1929ee697bc5f769..70aaa1ecc9f10d6d2042b9523cbe02d7509c9c5e 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
@@ -838,7 +838,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
         if (state != null)
             dropPeer(state, shouldShitlist, why);
     }
-    private void dropPeer(PeerState peer, boolean shouldShitlist, String why) {
+
+    void dropPeer(PeerState peer, boolean shouldShitlist, String why) {
         if (_log.shouldLog(Log.WARN)) {
             long now = _context.clock().now();
             StringBuilder buf = new StringBuilder(4096);
@@ -945,7 +946,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
                 // try to shift 'em around every 10 minutes or so
                 if (_introducersSelectedOn < _context.clock().now() - 10*60*1000) {
                     if (_log.shouldLog(Log.WARN))
-                        _log.warn("Our introducers are valid, but thy havent changed in a while, so lets rechoose");
+                        _log.warn("Our introducers are valid, but havent changed in a while, so lets rechoose");
                     return true;
                 } else {
                     if (_log.shouldLog(Log.INFO))
@@ -954,7 +955,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
                 }
             } else {
                 if (_log.shouldLog(Log.INFO))
-                    _log.info("Our introducers are not valid (" +valid + ")");
+                    _log.info("Need more introducers (have " +valid + " need " + PUBLIC_RELAY_COUNT + ')');
                 return true;
             }
         } else {
@@ -1017,7 +1018,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
     /** minimum active peers to maintain IP detection, etc. */
     private static final int MIN_PEERS = 3;
     /** minimum peers volunteering to be introducers if we need that */
-    private static final int MIN_INTRODUCER_POOL = 4;
+    private static final int MIN_INTRODUCER_POOL = 5;
 
     public TransportBid bid(RouterInfo toAddress, long dataSize) {
         if (dataSize > OutboundMessageState.MAX_MSG_SIZE) {
@@ -1233,7 +1234,9 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
                 _introducersSelectedOn = _context.clock().now();
                 introducersIncluded = true;
             } else {
+                // FIXME
                 // maybe we should fail to publish an address at all in this case?
+                // YES that would be better
                 if (_log.shouldLog(Log.WARN))
                     _log.warn("Need introducers but we don't know any");
             }
@@ -1331,13 +1334,13 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
         switch (status) {
             case CommSystemFacade.STATUS_REJECT_UNSOLICITED:
             case CommSystemFacade.STATUS_DIFFERENT:
-                if (_log.shouldLog(Log.INFO))
-                    _log.info("Require introducers, because our status is " + status);
+                if (_log.shouldLog(Log.DEBUG))
+                    _log.debug("Require introducers, because our status is " + status);
                 return true;
             default:
                 if (!allowDirectUDP()) {
-                    if (_log.shouldLog(Log.INFO))
-                        _log.info("Require introducers, because we do not allow direct UDP connections");
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug("Require introducers, because we do not allow direct UDP connections");
                     return true;
                 }
                 return false;
diff --git a/router/java/src/net/i2p/router/tunnel/BuildMessageProcessor.java b/router/java/src/net/i2p/router/tunnel/BuildMessageProcessor.java
index fddccd52f308cfbcbddbd61a3e4c44cb428b4037..1194cc103c33b9c328ad8779b915afcea065228b 100644
--- a/router/java/src/net/i2p/router/tunnel/BuildMessageProcessor.java
+++ b/router/java/src/net/i2p/router/tunnel/BuildMessageProcessor.java
@@ -24,7 +24,7 @@ public class BuildMessageProcessor {
     
     public BuildMessageProcessor(I2PAppContext ctx) {
         _filter = new DecayingHashSet(ctx, 60*1000, 32, "TunnelBMP");
-        ctx.statManager().createRateStat("tunnel.buildRequestDup", "How frequently we get dup build request messages", "Tunnels", new long[] { 60*1000, 10*60*1000 });
+        ctx.statManager().createRateStat("tunnel.buildRequestDup", "How frequently we get dup build request messages", "Tunnels", new long[] { 60*60*1000 });
     }
     /**
      * Decrypt the record targetting us, encrypting all of the other records with the included 
diff --git a/router/java/src/net/i2p/router/tunnel/InboundEndpointProcessor.java b/router/java/src/net/i2p/router/tunnel/InboundEndpointProcessor.java
index 7376a5046692207b666f6edccbe3d70c34c9af9f..6d467016a664636e72c84f61a1335ce24da766f3 100644
--- a/router/java/src/net/i2p/router/tunnel/InboundEndpointProcessor.java
+++ b/router/java/src/net/i2p/router/tunnel/InboundEndpointProcessor.java
@@ -15,17 +15,19 @@ import net.i2p.util.Log;
  *
  */
 public class InboundEndpointProcessor {
-    private RouterContext _context;
-    private Log _log;
-    private TunnelCreatorConfig _config;
-    private IVValidator _validator;    
+    private final RouterContext _context;
+    private final Log _log;
+    private final TunnelCreatorConfig _config;
+    private final IVValidator _validator;    
     
     static final boolean USE_ENCRYPTION = HopProcessor.USE_ENCRYPTION;
     private static final ByteCache _cache = ByteCache.getInstance(128, HopProcessor.IV_LENGTH);
     
+    /** @deprecated unused */
     public InboundEndpointProcessor(RouterContext ctx, TunnelCreatorConfig cfg) {
         this(ctx, cfg, DummyValidator.getInstance());
     }
+
     public InboundEndpointProcessor(RouterContext ctx, TunnelCreatorConfig cfg, IVValidator validator) {
         _context = ctx;
         _log = ctx.logManager().getLog(InboundEndpointProcessor.class);
@@ -84,6 +86,9 @@ public class InboundEndpointProcessor {
         return true;
     }
     
+    /**
+     * Iteratively undo the crypto that the various layers in the tunnel added.
+     */
     private void decrypt(RouterContext ctx, TunnelCreatorConfig cfg, byte iv[], byte orig[], int offset, int length) {
         Log log = ctx.logManager().getLog(OutboundGatewayProcessor.class);
         ByteArray ba = _cache.acquire();
diff --git a/router/java/src/net/i2p/router/tunnel/OutboundGatewayProcessor.java b/router/java/src/net/i2p/router/tunnel/OutboundGatewayProcessor.java
index 36593db0969f2725a542f66f502ad16c0e56e79c..bd912008098b31132064f14f53896ec056627fc5 100644
--- a/router/java/src/net/i2p/router/tunnel/OutboundGatewayProcessor.java
+++ b/router/java/src/net/i2p/router/tunnel/OutboundGatewayProcessor.java
@@ -14,9 +14,9 @@ import net.i2p.util.Log;
  *
  */
 public class OutboundGatewayProcessor {
-    private I2PAppContext _context;
-    private Log _log;
-    private TunnelCreatorConfig _config;
+    private final I2PAppContext _context;
+    private final Log _log;
+    private final TunnelCreatorConfig _config;
         
     static final boolean USE_ENCRYPTION = HopProcessor.USE_ENCRYPTION;
     private static final ByteCache _cache = ByteCache.getInstance(128, HopProcessor.IV_LENGTH);
@@ -54,10 +54,8 @@ public class OutboundGatewayProcessor {
     }
     
     /**
-     * Undo the crypto that the various layers in the tunnel added.  This is used
-     * by both the outbound gateway (preemptively undoing the crypto peers will add)
-     * and by the inbound endpoint.
-     *
+     * Iteratively undo the crypto that the various layers in the tunnel added.  This is used
+     * by the outbound gateway (preemptively undoing the crypto peers will add).
      */
     private void decrypt(I2PAppContext ctx, TunnelCreatorConfig cfg, byte iv[], byte orig[], int offset, int length) {
         Log log = ctx.logManager().getLog(OutboundGatewayProcessor.class);
@@ -73,6 +71,11 @@ public class OutboundGatewayProcessor {
         _cache.release(ba);
     }
     
+    /**
+     * Undo the crypto for a single hop.  This is used
+     * by both the outbound gateway (preemptively undoing the crypto peers will add)
+     * and by the inbound endpoint.
+     */
     static void decrypt(I2PAppContext ctx, byte iv[], byte orig[], int offset, int length, byte cur[], HopConfig config) {
         // update the IV for the previous (next?) hop
         ctx.aes().decryptBlock(orig, offset, config.getIVKey(), orig, offset);
diff --git a/router/java/src/net/i2p/router/tunnel/TunnelGateway.java b/router/java/src/net/i2p/router/tunnel/TunnelGateway.java
index 7291c8706a3207f7b07fda16c4b213874e4ac25f..3ec567098a91de813dde6e564b73eab7e797564b 100644
--- a/router/java/src/net/i2p/router/tunnel/TunnelGateway.java
+++ b/router/java/src/net/i2p/router/tunnel/TunnelGateway.java
@@ -60,7 +60,6 @@ public class TunnelGateway {
         _preprocessor = preprocessor;
         _sender = sender;
         _receiver = receiver;
-        _messagesSent = 0;
         _flushFrequency = 500;
         _delayedFlush = new DelayedFlush();
         _lastFlush = _context.clock().now();
@@ -128,8 +127,8 @@ public class TunnelGateway {
             FlushTimer.getInstance().addEvent(_delayedFlush, delayAmount);
         }
         _context.statManager().addRateData("tunnel.lockedGatewayAdd", afterAdded-beforeLock, remaining);
-        long complete = System.currentTimeMillis();
-        if (_log.shouldLog(Log.DEBUG))
+        if (_log.shouldLog(Log.DEBUG)) {
+            long complete = System.currentTimeMillis();
             _log.debug("Time to add the message " + msg.getUniqueId() + ": " + (complete-startAdd)
                        + " delayed? " + delayedFlush + " remaining: " + remaining
                        + " prepare: " + (beforeLock-startAdd)
@@ -137,6 +136,7 @@ public class TunnelGateway {
                        + " preprocess: " + (afterPreprocess-afterAdded)
                        + " expire: " + (afterExpire-afterPreprocess)
                        + " queue flush: " + (complete-afterExpire));
+        }
     }
     
     public int getMessagesSent() { return _messagesSent; }
@@ -202,10 +202,7 @@ public class TunnelGateway {
             _messageId = message.getUniqueId();
             _expiration = message.getMessageExpiration();
             _remaining = message.toByteArray();
-            _offset = 0;
-            _fragmentNumber = 0;
             _created = now;
-            _messageIds = null;
         }
         /** may be null */
         public Hash getToRouter() { return _toRouter; }
diff --git a/router/java/src/net/i2p/router/tunnel/pool/ClientPeerSelector.java b/router/java/src/net/i2p/router/tunnel/pool/ClientPeerSelector.java
index a9199b690e76d7ce89b4b5f27cceb1ae3e3e1171..f4042feee9f30014416dc1f96aa1f1f47e432ba2 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/ClientPeerSelector.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/ClientPeerSelector.java
@@ -5,6 +5,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
+import net.i2p.data.Hash;
 import net.i2p.router.RouterContext;
 import net.i2p.router.TunnelPoolSettings;
 
@@ -14,7 +15,7 @@ import net.i2p.router.TunnelPoolSettings;
  *
  */
 class ClientPeerSelector extends TunnelPeerSelector {
-    public List selectPeers(RouterContext ctx, TunnelPoolSettings settings) {
+    public List<Hash> selectPeers(RouterContext ctx, TunnelPoolSettings settings) {
         int length = getLength(ctx, settings);
         if (length < 0)
             return null;
@@ -31,7 +32,7 @@ class ClientPeerSelector extends TunnelPeerSelector {
         ctx.profileOrganizer().selectFastPeers(length, exclude, matches, settings.getIPRestriction());
         
         matches.remove(ctx.routerHash());
-        ArrayList rv = new ArrayList(matches);
+        ArrayList<Hash> rv = new ArrayList(matches);
         if (rv.size() > 1)
             orderPeers(rv, settings.getRandomKey());
         if (settings.isInbound())
diff --git a/router/java/src/net/i2p/router/tunnel/pool/ExploratoryPeerSelector.java b/router/java/src/net/i2p/router/tunnel/pool/ExploratoryPeerSelector.java
index 073088fb304f566687adce31106d3e7f1c7ef76d..503504cac56cf5dbb4f79f6f8de625e04b2a67d4 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/ExploratoryPeerSelector.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/ExploratoryPeerSelector.java
@@ -5,6 +5,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
+import net.i2p.data.Hash;
 import net.i2p.router.RouterContext;
 import net.i2p.router.TunnelPoolSettings;
 import net.i2p.stat.Rate;
@@ -17,7 +18,7 @@ import net.i2p.util.Log;
  *
  */
 class ExploratoryPeerSelector extends TunnelPeerSelector {
-    public List selectPeers(RouterContext ctx, TunnelPoolSettings settings) {
+    public List<Hash> selectPeers(RouterContext ctx, TunnelPoolSettings settings) {
         Log l = ctx.logManager().getLog(getClass());
         int length = getLength(ctx, settings);
         if (length < 0) { 
@@ -33,7 +34,7 @@ class ExploratoryPeerSelector extends TunnelPeerSelector {
             return rv;
         }
         
-        Set exclude = getExclude(ctx, settings.isInbound(), settings.isExploratory());
+        Set<Hash> exclude = getExclude(ctx, settings.isInbound(), settings.isExploratory());
         exclude.add(ctx.routerHash());
         // Don't use ff peers for exploratory tunnels to lessen exposure to netDb searches and stores
         // Hmm if they don't get explored they don't get a speed/capacity rating
@@ -56,7 +57,7 @@ class ExploratoryPeerSelector extends TunnelPeerSelector {
             l.debug("profileOrganizer.selectNotFailing(" + length + ") found " + matches);
         
         matches.remove(ctx.routerHash());
-        ArrayList rv = new ArrayList(matches);
+        ArrayList<Hash> rv = new ArrayList(matches);
         if (rv.size() > 1)
             orderPeers(rv, settings.getRandomKey());
         if (settings.isInbound())
@@ -67,7 +68,7 @@ class ExploratoryPeerSelector extends TunnelPeerSelector {
     }
     
     private static final int MIN_NONFAILING_PCT = 25;
-    private boolean shouldPickHighCap(RouterContext ctx) {
+    private static boolean shouldPickHighCap(RouterContext ctx) {
         if (Boolean.valueOf(ctx.getProperty("router.exploreHighCapacity", "false")).booleanValue())
             return true;
         // no need to explore too wildly at first
@@ -86,9 +87,9 @@ class ExploratoryPeerSelector extends TunnelPeerSelector {
             failPct = 100 - MIN_NONFAILING_PCT;
         } else {
             failPct = getExploratoryFailPercentage(ctx);
-            Log l = ctx.logManager().getLog(getClass());
-            if (l.shouldLog(Log.DEBUG))
-                l.debug("Normalized Fail pct: " + failPct);
+            //Log l = ctx.logManager().getLog(getClass());
+            //if (l.shouldLog(Log.DEBUG))
+            //    l.debug("Normalized Fail pct: " + failPct);
             // always try a little, this helps keep the failPct stat accurate too
             if (failPct > 100 - MIN_NONFAILING_PCT)
                 failPct = 100 - MIN_NONFAILING_PCT;
@@ -96,21 +97,23 @@ class ExploratoryPeerSelector extends TunnelPeerSelector {
         return (failPct >= ctx.random().nextInt(100));
     }
     
-    // We should really use the difference between the exploratory fail rate
-    // and the high capacity fail rate - but we don't have a stat for high cap,
-    // so use the fast (== client) fail rate, it should be close
-    // if the expl. and client tunnel lengths aren't too different.
-    // So calculate the difference between the exploratory fail rate
-    // and the client fail rate, normalized to 100:
-    //    100 * ((Efail - Cfail) / (100 - Cfail))
-    // Even this isn't the "true" rate for the NonFailingPeers pool, since we
-    // are often building exploratory tunnels using the HighCapacity pool.
-    private int getExploratoryFailPercentage(RouterContext ctx) {
+    /**
+     * We should really use the difference between the exploratory fail rate
+     * and the high capacity fail rate - but we don't have a stat for high cap,
+     * so use the fast (== client) fail rate, it should be close
+     * if the expl. and client tunnel lengths aren't too different.
+     * So calculate the difference between the exploratory fail rate
+     * and the client fail rate, normalized to 100:
+     *    100 * ((Efail - Cfail) / (100 - Cfail))
+     * Even this isn't the "true" rate for the NonFailingPeers pool, since we
+     * are often building exploratory tunnels using the HighCapacity pool.
+     */
+    private static int getExploratoryFailPercentage(RouterContext ctx) {
         int c = getFailPercentage(ctx, "Client");
         int e = getFailPercentage(ctx, "Exploratory");
-        Log l = ctx.logManager().getLog(getClass());
-        if (l.shouldLog(Log.DEBUG))
-            l.debug("Client, Expl. Fail pct: " + c + ", " + e);
+        //Log l = ctx.logManager().getLog(getClass());
+        //if (l.shouldLog(Log.DEBUG))
+        //    l.debug("Client, Expl. Fail pct: " + c + ", " + e);
         if (e <= c || e <= 25) // doing very well (unlikely)
             return 0;
         if (c >= 90) // doing very badly
@@ -118,7 +121,7 @@ class ExploratoryPeerSelector extends TunnelPeerSelector {
         return (100 * (e-c)) / (100-c);
     }
 
-    private int getFailPercentage(RouterContext ctx, String t) {
+    private static int getFailPercentage(RouterContext ctx, String t) {
         String pfx = "tunnel.build" + t;
         int timeout = getEvents(ctx, pfx + "Expire", 10*60*1000);
         int reject = getEvents(ctx, pfx + "Reject", 10*60*1000);
@@ -129,8 +132,8 @@ class ExploratoryPeerSelector extends TunnelPeerSelector {
         return (int)(100 * pct);
     }
     
-    // Use current + last to get more recent and smoother data
-    private int getEvents(RouterContext ctx, String stat, long period) {
+    /** Use current + last to get more recent and smoother data */
+    private static int getEvents(RouterContext ctx, String stat, long period) {
         RateStat rs = ctx.statManager().getRate(stat);
         if (rs == null) 
             return 0;
diff --git a/router/java/src/net/i2p/router/tunnel/pool/PooledTunnelCreatorConfig.java b/router/java/src/net/i2p/router/tunnel/pool/PooledTunnelCreatorConfig.java
index afe149fc65254cf645892aca6cc13555cc7d0e5a..cbe7948150a8897b3b5b26c891ac95504f5d096d 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/PooledTunnelCreatorConfig.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/PooledTunnelCreatorConfig.java
@@ -12,7 +12,7 @@ import net.i2p.util.Log;
 /**
  *
  */
-public class PooledTunnelCreatorConfig extends TunnelCreatorConfig {
+class PooledTunnelCreatorConfig extends TunnelCreatorConfig {
     private TunnelPool _pool;
     private TestJob _testJob;
     // private Job _expireJob;
diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java
index bb22ab0e448fe00a2c69fcb0e529130646d53f82..083e2501ae05f75dd087e6da942d1f4298be1f91 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java
@@ -21,10 +21,13 @@ import net.i2p.router.TunnelPoolSettings;
 import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade;
 import net.i2p.router.networkdb.kademlia.HashDistance;
 import net.i2p.util.Log;
+import net.i2p.util.VersionComparator;
 
 /**
  * Coordinate the selection of peers to go into a tunnel for one particular 
  * pool.
+ *
+ * Todo: there's nothing non-static in here
  */
 public abstract class TunnelPeerSelector {
     /**
@@ -36,7 +39,7 @@ public abstract class TunnelPeerSelector {
      *         to build through, and the settings reject 0 hop tunnels, this will
      *         return null.
      */
-    public abstract List selectPeers(RouterContext ctx, TunnelPoolSettings settings);
+    public abstract List<Hash> selectPeers(RouterContext ctx, TunnelPoolSettings settings);
     
     protected int getLength(RouterContext ctx, TunnelPoolSettings settings) {
         int length = settings.getLength();
@@ -79,6 +82,11 @@ public abstract class TunnelPeerSelector {
         return length;
     }
     
+    /**
+     *  For debugging, also possibly for restricted routes?
+     *  Needs analysis and testing
+     *  @return should always be false
+     */
     protected boolean shouldSelectExplicit(TunnelPoolSettings settings) {
         if (settings.isExploratory()) return false;
         Properties opts = settings.getUnknownOptions();
@@ -92,7 +100,12 @@ public abstract class TunnelPeerSelector {
         return false;
     }
     
-    protected List selectExplicit(RouterContext ctx, TunnelPoolSettings settings, int length) {
+    /**
+     *  For debugging, also possibly for restricted routes?
+     *  Needs analysis and testing
+     *  @return should always be false
+     */
+    protected List<Hash> selectExplicit(RouterContext ctx, TunnelPoolSettings settings, int length) {
         String peers = null;
         Properties opts = settings.getUnknownOptions();
         if (opts != null)
@@ -102,7 +115,7 @@ public abstract class TunnelPeerSelector {
             peers = I2PAppContext.getGlobalContext().getProperty("explicitPeers");
         
         Log log = ctx.logManager().getLog(ClientPeerSelector.class);
-        List rv = new ArrayList();
+        List<Hash> rv = new ArrayList();
         StringTokenizer tok = new StringTokenizer(peers, ",");
         while (tok.hasMoreTokens()) {
             String peerStr = tok.nextToken();
@@ -156,7 +169,7 @@ public abstract class TunnelPeerSelector {
     /** 
      * Pick peers that we want to avoid
      */
-    public Set getExclude(RouterContext ctx, boolean isInbound, boolean isExploratory) {
+    public Set<Hash> getExclude(RouterContext ctx, boolean isInbound, boolean isExploratory) {
         // we may want to update this to skip 'hidden' or 'unreachable' peers, but that
         // isn't safe, since they may publish one set of routerInfo to us and another to
         // other peers.  the defaults for filterUnreachable has always been to return false,
@@ -175,11 +188,12 @@ public abstract class TunnelPeerSelector {
         //
         // Defaults changed to true for inbound only in filterUnreachable below.
 
-        Set peers = new HashSet(1);
+        Set<Hash> peers = new HashSet(1);
         peers.addAll(ctx.profileOrganizer().selectPeersRecentlyRejecting());
         peers.addAll(ctx.tunnelManager().selectPeersInTooManyTunnels());
         // if (false && filterUnreachable(ctx, isInbound, isExploratory)) {
         if (filterUnreachable(ctx, isInbound, isExploratory)) {
+            // NOTE: filterUnreachable returns true for inbound, false for outbound
             // This is the only use for getPeersByCapability? And the whole set of datastructures in PeerManager?
             List<Hash> caps = ctx.peerManager().getPeersByCapability(Router.CAPABILITY_UNREACHABLE);
             if (caps != null)
@@ -189,6 +203,7 @@ public abstract class TunnelPeerSelector {
                 peers.addAll(caps);
         }
         if (filterSlow(ctx, isInbound, isExploratory)) {
+            // NOTE: filterSlow always returns true
             Log log = ctx.logManager().getLog(TunnelPeerSelector.class);
             char excl[] = getExcludeCaps(ctx);
             if (excl != null) {
@@ -301,6 +316,7 @@ public abstract class TunnelPeerSelector {
         return peers;
     }
     
+    /** warning, this is also called by ProfileOrganizer.isSelectable() */
     public static boolean shouldExclude(RouterContext ctx, RouterInfo peer) {
         Log log = ctx.logManager().getLog(TunnelPeerSelector.class);
         return shouldExclude(ctx, log, peer, getExcludeCaps(ctx));
@@ -318,6 +334,10 @@ public abstract class TunnelPeerSelector {
     }
     
     private static final long DONT_EXCLUDE_PERIOD = 15*60*1000;
+    /** 0.7.8 and earlier had major message corruption bugs */
+    private static final String MIN_VERSION = "0.7.9";
+    private static final VersionComparator _versionComparator = new VersionComparator();
+
     private static boolean shouldExclude(RouterContext ctx, Log log, RouterInfo peer, char excl[]) {
         String cap = peer.getCapabilities();
         if (cap == null) {
@@ -340,6 +360,13 @@ public abstract class TunnelPeerSelector {
         // otherwise, it contains flags we aren't trying to focus on,
         // so don't exclude it based on published capacity
 
+        // minimum version check
+        String v = peer.getOption("router.version");
+        if (v == null || _versionComparator.compare(v, MIN_VERSION) < 0)
+            return true;
+
+        // uptime is always spoofed to 90m, so just remove all this
+      /******
         String val = peer.getOption("stat_uptime");
         if (val != null) {
             long uptimeMs = 0;
@@ -390,6 +417,8 @@ public abstract class TunnelPeerSelector {
             // not publishing an uptime, so exclude it
             return true;
         }
+      ******/
+        return false;
     }
     
     private static final String PROP_OUTBOUND_EXPLORATORY_EXCLUDE_UNREACHABLE = "router.outboundExploratoryExcludeUnreachable";
@@ -403,6 +432,10 @@ public abstract class TunnelPeerSelector {
     private static final String DEFAULT_INBOUND_EXPLORATORY_EXCLUDE_UNREACHABLE = "true";
     private static final String DEFAULT_INBOUND_CLIENT_EXCLUDE_UNREACHABLE = "true";
     
+    /**
+     * do we want to skip peers who haven't been up for long?
+     * @return true for inbound, false for outbound, unless configured otherwise
+     */
     protected boolean filterUnreachable(RouterContext ctx, boolean isInbound, boolean isExploratory) {
         boolean def = false;
         String val = null;
@@ -429,6 +462,10 @@ public abstract class TunnelPeerSelector {
     private static final String PROP_INBOUND_EXPLORATORY_EXCLUDE_SLOW = "router.inboundExploratoryExcludeSlow";
     private static final String PROP_INBOUND_CLIENT_EXCLUDE_SLOW = "router.inboundClientExcludeSlow";
     
+    /**
+     * do we want to skip peers that are slow?
+     * @return true unless configured otherwise
+     */
     protected boolean filterSlow(RouterContext ctx, boolean isInbound, boolean isExploratory) {
         boolean def = true;
         String val = null;
@@ -454,7 +491,10 @@ public abstract class TunnelPeerSelector {
     private static final String PROP_INBOUND_EXPLORATORY_EXCLUDE_UPTIME = "router.inboundExploratoryExcludeUptime";
     private static final String PROP_INBOUND_CLIENT_EXCLUDE_UPTIME = "router.inboundClientExcludeUptime";
     
-    /** do we want to skip peers who haven't been up for long? */
+    /**
+     * do we want to skip peers who haven't been up for long?
+     * @return true unless configured otherwise
+     */
     protected boolean filterUptime(RouterContext ctx, boolean isInbound, boolean isExploratory) {
         boolean def = true;
         String val = null;