diff --git a/LICENSE.txt b/LICENSE.txt
index d91b95fe795772203ae9e9b650aafe7774404cba..b769f516e82b49ac587c082e0821e7add6c8eee9 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -238,8 +238,8 @@ Applications:
       Bundles systray4j-2.4.1:
       See licenses/LICENSE-LGPLv2.1.txt
 
-   Tomcat 6.0.35:
-   Copyright 1999-2011 The Apache Software Foundation
+   Tomcat 6.0.36:
+   Copyright 1999-2012 The Apache Software Foundation
    See licenses/LICENSE-Apache2.0.txt
    See licenses/NOTICE-Tomcat.txt
 
diff --git a/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java b/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java
index a9b70041c8b74af4343a1768daaa261a3a17dee0..810a10db08e7c66031eeba934261d9fd8c5edf9c 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java
@@ -61,6 +61,7 @@ public class MetaInfo
   private final byte[] piece_hashes;
   private final long length;
   private final boolean privateTorrent;
+  private final List<List<String>> announce_list;
   private Map<String, BEValue> infoMap;
 
   /**
@@ -69,9 +70,11 @@ public class MetaInfo
    *  @param announce may be null
    *  @param files null for single-file torrent
    *  @param lengths null for single-file torrent
+   *  @param announce_list may be null
    */
   MetaInfo(String announce, String name, String name_utf8, List<List<String>> files, List<Long> lengths,
-           int piece_length, byte[] piece_hashes, long length, boolean privateTorrent)
+           int piece_length, byte[] piece_hashes, long length, boolean privateTorrent,
+           List<List<String>> announce_list)
   {
     this.announce = announce;
     this.name = name;
@@ -83,6 +86,7 @@ public class MetaInfo
     this.piece_hashes = piece_hashes;
     this.length = length;
     this.privateTorrent = privateTorrent;
+    this.announce_list = announce_list;
 
     // TODO if we add a parameter for other keys
     //if (other != null) {
@@ -141,6 +145,23 @@ public class MetaInfo
         this.announce = val.getString();
     }
 
+    // BEP 12
+    val = m.get("announce-list");
+    if (val == null) {
+        this.announce_list = null;
+    } else {
+        this.announce_list = new ArrayList();
+        List<BEValue> bl1 = val.getList();
+        for (BEValue bev : bl1) {
+            List<BEValue> bl2 = bev.getList();
+            List<String> sl2 = new ArrayList();           
+            for (BEValue bev2 : bl2) {
+                sl2.add(bev2.getString());
+            }
+            this.announce_list.add(sl2);
+        }
+    }
+
     val = m.get("info");
     if (val == null)
         throw new InvalidBEncodingException("Missing info map");
@@ -296,6 +317,15 @@ public class MetaInfo
     return announce;
   }
 
+  /**
+   * Returns a list of lists of urls.
+   *
+   * @since 0.9.5
+   */
+  public List<List<String>> getAnnounceList() {
+    return announce_list;
+  }
+
   /**
    * Returns the original 20 byte SHA1 hash over the bencoded info map.
    */
@@ -470,12 +500,13 @@ public class MetaInfo
   /**
    * Creates a copy of this MetaInfo that shares everything except the
    * announce URL.
+   * Drops any announce-list.
    */
   public MetaInfo reannounce(String announce)
   {
     return new MetaInfo(announce, name, name_utf8, files,
                         lengths, piece_length,
-                        piece_hashes, length, privateTorrent);
+                        piece_hashes, length, privateTorrent, null);
   }
 
   /**
@@ -486,6 +517,8 @@ public class MetaInfo
         Map m = new HashMap();
         if (announce != null)
             m.put("announce", announce);
+        if (announce_list != null)
+            m.put("announce-list", announce_list);
         Map info = createInfoMap();
         m.put("info", info);
         // don't save this locally, we should only do this once
diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
index eef6679ce2d74b2227ed1f03bb932c39ca4873ee..08e2ae67bd7bbc0dce8045aadf11855c30a44b5f 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
@@ -886,7 +886,9 @@ public class SnarkManager implements CompleteListener {
                         }
                     }
                 } catch (IOException ioe) {
-                    addMessage(_("Torrent in \"{0}\" is invalid", sfile.getName()) + ": " + ioe.getMessage());
+                    String err = _("Torrent in \"{0}\" is invalid", sfile.getName()) + ": " + ioe.getMessage();
+                    addMessage(err);
+                    _log.error(err, ioe);
                     if (sfile.exists())
                         sfile.delete();
                     return;
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Storage.java b/apps/i2psnark/java/src/org/klomp/snark/Storage.java
index a42001ba8c607613804fa34d2c1952b637d64ed0..036e0f39b3e45f88eeba0f227743ace581f63c55 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/Storage.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/Storage.java
@@ -122,6 +122,7 @@ public class Storage
    * @throws IOException when creating and/or checking files fails.
    */
   public Storage(I2PSnarkUtil util, File baseFile, String announce,
+                 List<List<String>> announce_list,
                  boolean privateTorrent, StorageListener listener)
     throws IOException
   {
@@ -182,7 +183,8 @@ public class Storage
     // TODO thread this so we can return and show something on the UI
     byte[] piece_hashes = fast_digestCreate();
     metainfo = new MetaInfo(announce, baseFile.getName(), null, files,
-                            lengthsList, piece_size, piece_hashes, total, privateTorrent);
+                            lengthsList, piece_size, piece_hashes, total, privateTorrent,
+                            announce_list);
 
   }
 
@@ -1225,7 +1227,7 @@ public class Storage
       File file = null;
       FileOutputStream out = null;
       try {
-          Storage storage = new Storage(util, base, announce, false, null);
+          Storage storage = new Storage(util, base, announce, null, false, null);
           MetaInfo meta = storage.getMetaInfo();
           file = new File(storage.getBaseName() + ".torrent");
           out = new FileOutputStream(file);
diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java
index d6cf2fe7cadf931c1c98ecdc9124d1b2d2211ce0..65dc6d505f68074dc53adbaffb6a60e90fa1cebb 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java
@@ -31,6 +31,7 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
@@ -40,6 +41,7 @@ import java.util.Set;
 import net.i2p.I2PAppContext;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Hash;
+import net.i2p.util.ConvertToHash;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
 import net.i2p.util.SimpleTimer2;
@@ -109,8 +111,8 @@ public class TrackerClient implements Runnable {
   private boolean completed;
   private volatile boolean _fastUnannounce;
   private long lastDHTAnnounce;
-  private final List<Tracker> trackers;
-  private final List<Tracker> backupTrackers;
+  private final List<TCTracker> trackers;
+  private final List<TCTracker> backupTrackers;
 
   /**
    * Call start() to start it.
@@ -270,9 +272,12 @@ public class TrackerClient implements Runnable {
         primary = meta.getAnnounce();
     else if (additionalTrackerURL != null)
         primary = additionalTrackerURL;
+    Set<Hash> trackerHashes = new HashSet(8);
+
+    // primary tracker
     if (primary != null) {
-        if (isValidAnnounce(primary)) {
-            trackers.add(new Tracker(primary, true));
+        if (isNewValidTracker(trackerHashes, primary)) {
+            trackers.add(new TCTracker(primary, true));
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug("Announce: [" + primary + "] infoHash: " + infoHash);
         } else {
@@ -281,36 +286,35 @@ public class TrackerClient implements Runnable {
         }
     } else {
         _log.warn("No primary announce");
-        primary = "";
     }
+
+    // announce list
+    if (meta != null && !meta.isPrivate()) {
+        List<List<String>> list = meta.getAnnounceList();
+        if (list != null) {
+            for (List<String> llist : list) {
+                for (String url : llist) {
+                    if (!isNewValidTracker(trackerHashes, url))
+                        continue;
+                    trackers.add(new TCTracker(url, trackers.isEmpty()));
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug("Additional announce (list): [" + url + "] for infoHash: " + infoHash);
+                }
+            }
+        }
+    }
+
+    // configured open trackers
     if (meta == null || !meta.isPrivate()) {
         List<String> tlist = _util.getOpenTrackers();
         for (int i = 0; i < tlist.size(); i++) {
-             String url = tlist.get(i);
-             if (!isValidAnnounce(url)) {
-                _log.error("Bad announce URL: [" + url + "]");
-                continue;
-             }
-             int slash = url.indexOf('/', 7);
-             if (slash <= 7) {
-                _log.error("Bad announce URL: [" + url + "]");
+            String url = tlist.get(i);
+            if (!isNewValidTracker(trackerHashes, url))
                 continue;
-             }
-             if (primary.startsWith(url.substring(0, slash)))
-                continue;
-             String dest = _util.lookup(url.substring(7, slash));
-             if (dest == null) {
-                _log.error("Announce host unknown: [" + url.substring(7, slash) + "]");
-                continue;
-             }
-             if (primary.startsWith("http://" + dest))
-                continue;
-             if (primary.startsWith("http://i2p/" + dest))
-                continue;
-             // opentrackers are primary if we don't have primary
-             trackers.add(new Tracker(url, primary.equals("")));
-             if (_log.shouldLog(Log.DEBUG))
-                 _log.debug("Additional announce: [" + url + "] for infoHash: " + infoHash);
+            // opentrackers are primary if we don't have primary
+            trackers.add(new TCTracker(url, trackers.isEmpty()));
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Additional announce: [" + url + "] for infoHash: " + infoHash);
         }
     }
 
@@ -318,31 +322,40 @@ public class TrackerClient implements Runnable {
     if (trackers.isEmpty() && (meta == null || !meta.isPrivate())) {
         List<String> tlist = _util.getBackupTrackers();
         for (int i = 0; i < tlist.size(); i++) {
-             String url = tlist.get(i);
-             if (!isValidAnnounce(url)) {
-                _log.error("Bad announce URL: [" + url + "]");
-                continue;
-             }
-             int slash = url.indexOf('/', 7);
-             if (slash <= 7) {
-                _log.error("Bad announce URL: [" + url + "]");
+            String url = tlist.get(i);
+            if (!isNewValidTracker(trackerHashes, url))
                 continue;
-             }
-             String dest = _util.lookup(url.substring(7, slash));
-             if (dest == null) {
-                _log.error("Announce host unknown: [" + url.substring(7, slash) + "]");
-                continue;
-             }
-             backupTrackers.add(new Tracker(url, false));
-             if (_log.shouldLog(Log.DEBUG))
-                 _log.debug("Backup announce: [" + url + "] for infoHash: " + infoHash);
+            backupTrackers.add(new TCTracker(url, false));
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Backup announce: [" + url + "] for infoHash: " + infoHash);
+        }
+        if (backupTrackers.isEmpty()) {
+            backupTrackers.add(new TCTracker(DEFAULT_BACKUP_TRACKER, false));
         }
-        if (backupTrackers.isEmpty())
-            backupTrackers.add(new Tracker(DEFAULT_BACKUP_TRACKER, false));
     }
     this.completed = coordinator.getLeft() == 0;
   }
 
+  /**
+   *  @param existing the ones we already know about
+   *  @param ann an announce URL non-null
+   *  @return true if ann is valid and new; adds to existing if returns true
+   *  @since 0.9.5
+   */
+  private boolean isNewValidTracker(Set<Hash> existing, String ann) {
+      Hash h = getHostHash(ann);
+      if (h == null) {
+         _log.error("Bad announce URL: [" + ann + ']');
+         return false;
+      }
+      boolean rv = existing.add(h);
+      if (!rv) {
+          if (_log.shouldLog(Log.INFO))
+             _log.info("Dup announce URL: [" + ann + ']');
+      }
+      return rv;
+  }
+
   /**
    *  Announce to all the trackers, get peers from PEX and DHT, then queue up a SimpleTimer2 event.
    *  This will take several seconds to several minutes.
@@ -425,7 +438,7 @@ public class TrackerClient implements Runnable {
   /**
    *  @return max peers seen
    */
-  private int getPeersFromTrackers(List<Tracker> trckrs) {
+  private int getPeersFromTrackers(List<TCTracker> trckrs) {
             long uploaded = coordinator.getUploaded();
             long downloaded = coordinator.getDownloaded();
             long left = coordinator.getLeft();   // -1 in magnet mode
@@ -442,7 +455,7 @@ public class TrackerClient implements Runnable {
 
             // *** loop once for each tracker
             int maxSeenPeers = 0;
-            for (Tracker tr : trckrs) {
+            for (TCTracker tr : trckrs) {
               if ((!stop) && (!tr.stop) &&
                   (completed || coordinator.needOutboundPeers() || !tr.started) &&
                   (event.equals(COMPLETED_EVENT) || System.currentTimeMillis() > tr.lastRequestTime + tr.interval))
@@ -639,7 +652,7 @@ public class TrackerClient implements Runnable {
       if (dht != null)
           dht.unannounce(snark.getInfoHash());
       int i = 0;
-      for (Tracker tr : trackers) {
+      for (TCTracker tr : trackers) {
           if (_util.connected() &&
               tr.started && (!tr.stop) && tr.trackerProblems == null) {
               try {
@@ -659,9 +672,9 @@ public class TrackerClient implements Runnable {
    *  @since 0.9.1
    */
   private class Unannouncer implements Runnable {
-     private final Tracker tr;
+     private final TCTracker tr;
 
-     public Unannouncer(Tracker tr) {
+     public Unannouncer(TCTracker tr) {
          this.tr = tr;
      }
 
@@ -685,7 +698,7 @@ public class TrackerClient implements Runnable {
      }
   }
   
-  private TrackerInfo doRequest(Tracker tr, String infoHash,
+  private TrackerInfo doRequest(TCTracker tr, String infoHash,
                                 String peerID, long uploaded,
                                 long downloaded, long left, String event)
     throws IOException
@@ -775,6 +788,7 @@ public class TrackerClient implements Runnable {
   }
 
   /**
+   *  @param ann an announce URL
    *  @return true for i2p hosts only
    *  @since 0.7.12
    */
@@ -790,10 +804,38 @@ public class TrackerClient implements Runnable {
            url.getPort() < 0;
   }
 
-  private static class Tracker
+  /**
+   *  @param ann an announce URL non-null
+   *  @return a Hash for i2p hosts only, null otherwise
+   *  @since 0.9.5
+   */
+  private static Hash getHostHash(String ann) {
+    URL url;
+    try {
+        url = new URL(ann);
+    } catch (MalformedURLException mue) {
+        return null;
+    }
+    if (url.getPort() >= 0 || !url.getProtocol().equals("http"))
+        return null;
+    String host = url.getHost();
+    if (host.endsWith(".i2p"))
+        return ConvertToHash.getHash(host);
+    if (host.equals("i2p")) {
+        String path = url.getPath();
+        if (path == null || path.length() < 517 ||
+            !path.startsWith("/"))
+            return null;
+        String[] parts = path.substring(1).split("/?&;", 2);
+        return ConvertToHash.getHash(parts[0]);
+    }
+    return null;
+  }
+
+  private static class TCTracker
   {
-      String announce;
-      boolean isPrimary;
+      final String announce;
+      final boolean isPrimary;
       long interval;
       long lastRequestTime;
       String trackerProblems;
@@ -803,7 +845,7 @@ public class TrackerClient implements Runnable {
       int consecutiveFails;
       int seenPeers;
 
-      public Tracker(String a, boolean p)
+      public TCTracker(String a, boolean p)
       {
           announce = a;
           isPrimary = p;
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 ea5d751e554cbc2062acdf1343b72d11374c4c18..2765d587310cbc7fbf419b66e496875fb009a3c1 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
@@ -61,7 +61,7 @@ public class I2PSnarkServlet extends DefaultServlet {
     private Resource _resourceBase;
     private String _themePath;
     private String _imgPath;
-    private String _lastAnnounceURL = "";
+    private String _lastAnnounceURL;
     
     public static final String PROP_CONFIG_FILE = "i2psnark.configFile";
  
@@ -730,18 +730,54 @@ public class I2PSnarkServlet extends DefaultServlet {
                 //if ( (announceURLOther != null) && (announceURLOther.trim().length() > "http://.i2p/announce".length()) )
                 //    announceURL = announceURLOther;
 
-                if (announceURL == null || announceURL.length() <= 0)
-                    _manager.addMessage(_("Error creating torrent - you must select a tracker"));
-                else if (baseFile.exists()) {
-                    _lastAnnounceURL = announceURL;
+                if (baseFile.exists()) {
                     if (announceURL.equals("none"))
                         announceURL = null;
+                    _lastAnnounceURL = announceURL;
+                    List<String> backupURLs = new ArrayList();
+                    Enumeration e = req.getParameterNames();
+                    while (e.hasMoreElements()) {
+                         Object o = e.nextElement();
+                         if (!(o instanceof String))
+                             continue;
+                         String k = (String) o;
+                        if (k.startsWith("backup_")) {
+                            String url = k.substring(7);
+                            if (!url.equals(announceURL))
+                                backupURLs.add(url);
+                        }
+                    }
+                    List<List<String>> announceList = null;
+                    if (!backupURLs.isEmpty()) {
+                        // BEP 12 - Put primary first, then the others, each as the sole entry in their own list
+                        if (announceURL == null) {
+                            _manager.addMessage(_("Error - Cannot include alternate trackers without a primary tracker"));
+                            return;
+                        }
+                        backupURLs.add(0, announceURL);
+                        boolean hasPrivate = false;
+                        boolean hasPublic = false;
+                        for (String url : backupURLs) {
+                            if (_manager.getPrivateTrackers().contains(announceURL))
+                                hasPrivate = true;
+                            else
+                                hasPublic = true;
+                        }
+                        if (hasPrivate && hasPublic) {
+                            _manager.addMessage(_("Error - Cannot mix private and public trackers in a torrent"));
+                            return;
+                        }
+                        announceList = new ArrayList(backupURLs.size());
+                        for (String url : backupURLs) {
+                            announceList.add(Collections.singletonList(url));
+                        }
+                    }
                     try {
                         // This may take a long time to check the storage, but since it already exists,
                         // it shouldn't be THAT bad, so keep it in this thread.
                         // TODO thread it for big torrents, perhaps a la FetchAndAdd
                         boolean isPrivate = _manager.getPrivateTrackers().contains(announceURL);
-                        Storage s = new Storage(_manager.util(), baseFile, announceURL, isPrivate, null);
+                        Storage s = new Storage(_manager.util(), baseFile, announceURL, announceList, isPrivate, null);
                         s.close(); // close the files... maybe need a way to pass this Storage to addTorrent rather than starting over
                         MetaInfo info = s.getMetaInfo();
                         File torrentFile = new File(_manager.getDataDir(), s.getBaseName() + ".torrent");
@@ -1372,6 +1408,7 @@ public class I2PSnarkServlet extends DefaultServlet {
     }
 
     /**
+     *  Start of anchor only, caller must add anchor text or img and close anchor
      *  @return string or null
      *  @since 0.8.4
      */
@@ -1399,6 +1436,7 @@ public class I2PSnarkServlet extends DefaultServlet {
     }
 
     /**
+     *  Full anchor with img
      *  @return string or null
      *  @since 0.8.4
      */
@@ -1414,6 +1452,29 @@ public class I2PSnarkServlet extends DefaultServlet {
         return null;
     }
 
+    /**
+     *  Full anchor with shortened URL as anchor text
+     *  @return string, non-null
+     *  @since 0.9.5
+     */
+    private String getShortTrackerLink(String announce, byte[] infohash) {
+        StringBuilder buf = new StringBuilder(128);
+        String trackerLinkUrl = getTrackerLinkUrl(announce, infohash);
+        if (trackerLinkUrl != null)
+            buf.append(trackerLinkUrl);
+        if (announce.startsWith("http://"))
+            announce = announce.substring(7);
+        int slsh = announce.indexOf('/');
+        if (slsh > 0)
+            announce = announce.substring(0, slsh);
+        if (announce.length() > 67)
+            announce = announce.substring(0, 40) + "&hellip;" + announce.substring(announce.length() - 8);
+        buf.append(announce);
+        if (trackerLinkUrl != null)
+            buf.append("</a>");
+        return buf.toString();
+    }
+
     private void writeAddForm(PrintWriter out, HttpServletRequest req) throws IOException {
         // display incoming parameter if a GET so links will work
         String newURL = req.getParameter("newURL");
@@ -1482,23 +1543,32 @@ public class I2PSnarkServlet extends DefaultServlet {
                   + "\" title=\"");
         out.write(_("File or directory to seed (must be within the specified path)"));
         out.write("\" ><tr><td>\n");
-        out.write(_("Tracker"));
-        out.write(":<td><select name=\"announceURL\"><option value=\"\">");
-        out.write(_("Select a tracker"));
-        out.write("</option>\n");
-        // todo remember this one with _lastAnnounceURL also
-        out.write("<option value=\"none\">");
-        //out.write(_("Open trackers and DHT only"));
-        out.write(_("Open trackers only"));
-        out.write("</option>\n");
+        out.write(_("Trackers"));
+        out.write(":<td><table style=\"width: 20%;\"><tr><td></td><td align=\"center\">");
+        out.write(_("Primary"));
+        out.write("</td><td align=\"center\">");
+        out.write(_("Alternates"));
+        out.write("</td></tr>\n");
         for (Tracker t : sortedTrackers) {
             String name = t.name;
             String announceURL = t.announceURL.replace("&#61;", "=");
+            out.write("<tr><td>");
+            out.write(name);
+            out.write("</td><td align=\"center\"><input type=\"radio\" name=\"announceURL\" value=\"");
+            out.write(announceURL);
+            out.write("\"");
             if (announceURL.equals(_lastAnnounceURL))
-                announceURL += "\" selected=\"selected";
-            out.write("\t<option value=\"" + announceURL + "\">" + name + "</option>\n");
+                out.write(" checked");
+            out.write("></td><td align=\"center\"><input type=\"checkbox\" name=\"backup_");
+            out.write(announceURL);
+            out.write("\" value=\"foo\"></td></tr>\n");
         }
-        out.write("</select>\n");
+        out.write("<tr><td>");
+        out.write(_("none"));
+        out.write("</td><td align=\"center\"><input type=\"radio\" name=\"announceURL\" value=\"none\"");
+        if (_lastAnnounceURL == null)
+            out.write(" checked");
+        out.write("></td><td></td></tr></table>\n");
         // make the user add a tracker on the config form now
         //out.write(_("or"));
         //out.write("&nbsp;<input type=\"text\" name=\"announceURLOther\" size=\"57\" value=\"http://\" " +
@@ -1998,20 +2068,26 @@ public class I2PSnarkServlet extends DefaultServlet {
                     String trackerLink = getTrackerLink(announce, snark.getInfoHash());
                     if (trackerLink != null)
                         buf.append(trackerLink).append(' ');
-                    buf.append("<b>").append(_("Tracker")).append(":</b> ");
-                    String trackerLinkUrl = getTrackerLinkUrl(announce, snark.getInfoHash());
-                    if (trackerLinkUrl != null)
-                        buf.append(trackerLinkUrl);
-                    if (announce.startsWith("http://"))
-                        announce = announce.substring(7);
-                    int slsh = announce.indexOf('/');
-                    if (slsh > 0)
-                        announce = announce.substring(0, slsh);
-                    if (announce.length() > 67)
-                        announce = announce.substring(0, 40) + "&hellip;" + announce.substring(announce.length() - 8);
-                    buf.append(announce);
-                    if (trackerLinkUrl != null)
-                        buf.append("</a>");
+                    buf.append("<b>").append(_("Primary Tracker")).append(":</b> ");
+                    buf.append(getShortTrackerLink(announce, snark.getInfoHash()));
+                    buf.append("</td></tr>");
+                }
+                List<List<String>> alist = meta.getAnnounceList();
+                if (alist != null) {
+                    buf.append("<tr><td><b>");
+                    buf.append(_("Tracker List")).append(":</b> ");
+                    for (List<String> alist2 : alist) {
+                        buf.append('[');
+                        boolean more = false;
+                        for (String s : alist2) {
+                            if (more)
+                                buf.append(' ');
+                            else
+                                more = true;
+                            buf.append(getShortTrackerLink(s, snark.getInfoHash()));
+                        }
+                        buf.append("] ");
+                    }
                     buf.append("</td></tr>");
                 }
             }
diff --git a/apps/i2ptunnel/jsp/editClient.jsp b/apps/i2ptunnel/jsp/editClient.jsp
index 6ee0ae540ef9fcf1ffda0c87c264193b071630ab..0ee00159b9622994f587765c70fcb6cd73d7f134 100644
--- a/apps/i2ptunnel/jsp/editClient.jsp
+++ b/apps/i2ptunnel/jsp/editClient.jsp
@@ -248,7 +248,7 @@
                     <option value="2"<%=(tunnelQuantity == 2 ? " selected=\"selected\"" : "") %>><%=intl._("2 inbound, 2 outbound tunnels (standard bandwidth usage, standard reliability)")%></option>
                     <option value="3"<%=(tunnelQuantity == 3 ? " selected=\"selected\"" : "") %>><%=intl._("3 inbound, 3 outbound tunnels (higher bandwidth usage, higher reliability)")%></option>
                 <% if (tunnelQuantity > 3) {
-                %>    <option value="<%=tunnelQuantity%>" selected="selected"><%=tunnelQuantity%> <%=intl._("tunnels")%></option>
+                %>    <option value="<%=tunnelQuantity%>" selected="selected"><%=tunnelQuantity%>&nbsp;<%=intl._("tunnels")%></option>
                 <% }
               %></select>                
             </div>
diff --git a/apps/i2ptunnel/jsp/editServer.jsp b/apps/i2ptunnel/jsp/editServer.jsp
index 9447fc36873025f04ffe74a285927afd3a9e3266..7b0deaff8e0dd14dcf879edd3c4f21a6c1a8bab4 100644
--- a/apps/i2ptunnel/jsp/editServer.jsp
+++ b/apps/i2ptunnel/jsp/editServer.jsp
@@ -264,8 +264,11 @@
                   %><option value="1"<%=(tunnelQuantity == 1 ? " selected=\"selected\"" : "") %>><%=intl._("1 inbound, 1 outbound tunnel  (low bandwidth usage, less reliability)")%></option>
                     <option value="2"<%=(tunnelQuantity == 2 ? " selected=\"selected\"" : "") %>><%=intl._("2 inbound, 2 outbound tunnels (standard bandwidth usage, standard reliability)")%></option>
                     <option value="3"<%=(tunnelQuantity == 3 ? " selected=\"selected\"" : "") %>><%=intl._("3 inbound, 3 outbound tunnels (higher bandwidth usage, higher reliability)")%></option>
-                <% if (tunnelQuantity > 3) {
-                %>    <option value="<%=tunnelQuantity%>" selected="selected"><%=tunnelQuantity%> <%=intl._("tunnels")%></option>
+                    <option value="4"<%=(tunnelQuantity == 4 ? " selected=\"selected\"" : "") %>><%=intl._("4 in, 4 out (high traffic server)")%></option>
+                    <option value="5"<%=(tunnelQuantity == 5 ? " selected=\"selected\"" : "") %>><%=intl._("5 in, 5 out (high traffic server)")%></option>
+                    <option value="6"<%=(tunnelQuantity == 6 ? " selected=\"selected\"" : "") %>><%=intl._("6 in, 6 out (high traffic server)")%></option>
+                <% if (tunnelQuantity > 6) {
+                %>    <option value="<%=tunnelQuantity%>" selected="selected"><%=tunnelQuantity%>&nbsp;<%=intl._("tunnels")%></option>
                 <% }
               %></select>                
             </div>
diff --git a/apps/jetty/apache-tomcat-deployer/NOTICE b/apps/jetty/apache-tomcat-deployer/NOTICE
index c44c35de8848e6dbe18ff5477c67694b74b19042..aaa19b6a8c8382fc1542021af67fa7b40ba5756e 100644
--- a/apps/jetty/apache-tomcat-deployer/NOTICE
+++ b/apps/jetty/apache-tomcat-deployer/NOTICE
@@ -1,5 +1,5 @@
 Apache Tomcat
-Copyright 1999-2011 The Apache Software Foundation
+Copyright 1999-2012 The Apache Software Foundation
 
 This product includes software developed by
 The Apache Software Foundation (http://www.apache.org/).
diff --git a/apps/jetty/apache-tomcat-deployer/README-i2p.txt b/apps/jetty/apache-tomcat-deployer/README-i2p.txt
index dd398113861368996037f52c137ce55db6445a44..5a0568444a76b401ca449db17ce9f08318456c01 100644
--- a/apps/jetty/apache-tomcat-deployer/README-i2p.txt
+++ b/apps/jetty/apache-tomcat-deployer/README-i2p.txt
@@ -2,7 +2,7 @@ This is Apache Tomcat 6.x, supporting Servlet 2.5 and JSP 2.1.
 The Glassfish JSP 2.1 bundled in Jetty 6 is way too old.
 
 Retrieved from the file
-	apache-tomcat-6.0.35-deployer.tar.gz
+	apache-tomcat-6.0.36-deployer.tar.gz
 
 minus the following files and directores:
 
diff --git a/apps/jetty/apache-tomcat-deployer/lib/el-api.jar b/apps/jetty/apache-tomcat-deployer/lib/el-api.jar
index 3518f7d76b3b83fc0ae8f660079439d7c68a27e0..7503cdabddd5f10328f7f80fed35d5c091c12009 100644
Binary files a/apps/jetty/apache-tomcat-deployer/lib/el-api.jar and b/apps/jetty/apache-tomcat-deployer/lib/el-api.jar differ
diff --git a/apps/jetty/apache-tomcat-deployer/lib/jasper-el.jar b/apps/jetty/apache-tomcat-deployer/lib/jasper-el.jar
index 23876732eeb471c2d620ab485745086e5b9e1269..c51e275e2f1a1731d3ff71f662671a10397ad2c7 100644
Binary files a/apps/jetty/apache-tomcat-deployer/lib/jasper-el.jar and b/apps/jetty/apache-tomcat-deployer/lib/jasper-el.jar differ
diff --git a/apps/jetty/apache-tomcat-deployer/lib/jasper.jar b/apps/jetty/apache-tomcat-deployer/lib/jasper.jar
index e64284f4f625353e50e1f2956261e1fb515de529..47284070c277533f3cdab32ca0b2fd63800a3a0d 100644
Binary files a/apps/jetty/apache-tomcat-deployer/lib/jasper.jar and b/apps/jetty/apache-tomcat-deployer/lib/jasper.jar differ
diff --git a/apps/jetty/apache-tomcat-deployer/lib/jsp-api.jar b/apps/jetty/apache-tomcat-deployer/lib/jsp-api.jar
index 6ef6574cea37f17321800b1921f18e6ab63d47fd..3030459542bceaa1c7705a015c840b215eceef23 100644
Binary files a/apps/jetty/apache-tomcat-deployer/lib/jsp-api.jar and b/apps/jetty/apache-tomcat-deployer/lib/jsp-api.jar differ
diff --git a/apps/jetty/apache-tomcat-deployer/lib/servlet-api.jar b/apps/jetty/apache-tomcat-deployer/lib/servlet-api.jar
index 1bf50af6e504fff15c5a3ad31b773b12aedee16d..44f490c6af297ea457d6126864327ae1f58cfaa9 100644
Binary files a/apps/jetty/apache-tomcat-deployer/lib/servlet-api.jar and b/apps/jetty/apache-tomcat-deployer/lib/servlet-api.jar differ
diff --git a/apps/jetty/apache-tomcat-deployer/lib/tomcat-juli.jar b/apps/jetty/apache-tomcat-deployer/lib/tomcat-juli.jar
index 07571768cafb3948f98eb0b1d3ac5fa01393875d..2fcbafd9de994260a4ca36dbc356121e370f31ef 100644
Binary files a/apps/jetty/apache-tomcat-deployer/lib/tomcat-juli.jar and b/apps/jetty/apache-tomcat-deployer/lib/tomcat-juli.jar differ
diff --git a/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java b/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java
index 3e612d166c8f37de7a8f92d183348965da498b9c..15d32cf0b812d3b2d59583290067824b611a92a9 100644
--- a/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java
+++ b/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java
@@ -558,6 +558,11 @@ public class ConsoleUpdateManager implements UpdateManager {
      *  Call once for each type/method pair.
      */
     public void register(Updater updater, UpdateType type, UpdateMethod method, int priority) {
+        if ((type == ROUTER_SIGNED || type == ROUTER_UNSIGNED) && NewsHelper.dontInstall(_context)) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Ignoring registration for " + type + ", router updates disabled");
+            return;
+        }
         // DEBUG slow start for snark updates
         // For 0.9.4 update, only for dev builds
         // For 0.9.5 update, only for dev builds and 1% more
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NewsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/NewsHelper.java
index 162290e9472e3dfb63f73b32df3020a951ea09cd..de5824bd363eb919df1080d41815b7358c5b4d1e 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/NewsHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/NewsHelper.java
@@ -230,10 +230,12 @@ public class NewsHelper extends ContentHelper {
      *  @since 0.9.4 moved from NewsFetcher
      */
     public static boolean dontInstall(RouterContext ctx) {
+        boolean disabled = ctx.getBooleanProperty(ConfigUpdateHandler.PROP_UPDATE_DISABLED);
+        if (disabled)
+            return true;
         File test = new File(ctx.getBaseDir(), "history.txt");
         boolean readonly = ((test.exists() && !test.canWrite()) || (!ctx.getBaseDir().canWrite()));
-        boolean disabled = ctx.getBooleanProperty(ConfigUpdateHandler.PROP_UPDATE_DISABLED);
-        return readonly || disabled;
+        return readonly;
     }
 
     /**
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java b/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java
index 199cf5e4e3bec88e52217a581c6641f0d7b954e6..c025fd7e32a00cc4e6011e5f82766aa508823046 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java
@@ -55,7 +55,7 @@ public class StatSummarizer implements Runnable {
     private Thread _thread;
     
     public StatSummarizer() {
-        _context = RouterContext.listContexts().get(0); // fuck it, only summarize one per jvm
+        _context = RouterContext.listContexts().get(0); // only summarize one per jvm
         _log = _context.logManager().getLog(getClass());
         _listeners = new CopyOnWriteArrayList();
         _instance = this;
diff --git a/history.txt b/history.txt
index a27d0df5f94f6deb76edb6be9ef2267329a4b047..15bdeab388ba3a33037baceeccbf2e7fd5b3b989 100644
--- a/history.txt
+++ b/history.txt
@@ -1,3 +1,8 @@
+2012-12-22 zzz
+  - i2psnark: Add announce list support (BEP 12) (ticket #778)
+  - i2ptunnel: Add more tunnel quantity options for servers
+  - Jetty: Update to Apache Tomcat 0.6.36
+
 2012-12-22 kytv
 * French language translation update from Transifex
 
diff --git a/installer/resources/i2prouter b/installer/resources/i2prouter
index 6d5104e8794df10b36a7cda6dfa215f7e1b1fdae..67df8920938bc7f1ec3a105225bb086284183ccd 100644
--- a/installer/resources/i2prouter
+++ b/installer/resources/i2prouter
@@ -493,7 +493,7 @@ gettext() {
 outputFile() {
     if [ -f "$1" ]
     then
-        echo '  $1  Found but not executable.';
+        echo "  $1  Found but not executable.";
     else
         echo "  $1"
     fi
@@ -656,7 +656,7 @@ checkUser() {
         fi
         if [ "`$IDEXE -u -n "$RUN_AS_USER" 2>/dev/null`" != "$RUN_AS_USER" ]
         then
-            echo 'User $RUN_AS_USER does not exist.'
+            echo "User $RUN_AS_USER does not exist."
             exit 1
         fi
 
@@ -794,12 +794,12 @@ getpid() {
                 then
                     # This is a stale pid file.
                     rm -f "$PIDFILE"
-                    echo 'Removed stale pid file: $PIDFILE'
+                    echo "Removed stale pid file: $PIDFILE"
                     pid=""
                 fi
             fi
         else
-            echo 'Cannot read $PIDFILE.'
+            echo "Cannot read $PIDFILE."
             exit 1
         fi
     fi
@@ -982,7 +982,7 @@ startwait() {
 
 macosxstart() {
     # The daemon has been installed.
-    echo 'Starting $APP_LONG_NAME.  Detected Mac OSX and installed launchd daemon.'
+    echo "Starting $APP_LONG_NAME.  Detected Mac OSX and installed launchd daemon."
     if [ `id | sed 's/^uid=//;s/(.*$//'` != "0" ] ; then
         eval echo `gettext 'Must be root to perform this action.'`
         exit 1
@@ -1010,7 +1010,7 @@ macosxstart() {
 
 upstartstart() {
     # The daemon has been installed.
-    echo 'Starting $APP_LONG_NAME.  Detected Linux and installed upstart.'
+    echo "Starting $APP_LONG_NAME.  Detected Linux and installed upstart."
     if [ `id | sed 's/^uid=//;s/(.*$//'` != "0" ] ; then
         eval echo `gettext 'Must be root to perform this action.'`
         exit 1
@@ -1156,11 +1156,11 @@ graceful() {
 }
 
 pause() {
-    echo 'Pausing $APP_LONG_NAME.'
+    echo "Pausing $APP_LONG_NAME."
 }
 
 resume() {
-    echo 'Resuming $APP_LONG_NAME.'
+    echo "Resuming $APP_LONG_NAME."
 }
 
 status() {
@@ -1182,9 +1182,9 @@ status() {
 }
 
 installUpstart() {
-    echo ' Installing the $APP_LONG_NAME daemon using upstart..'
+    echo " Installing the $APP_LONG_NAME daemon using upstart.."
     if [ -f "${APP_NAME}.conf" ] ; then
-        echo ' a custom upstart conf file ${APP_NAME}.conf found'
+        echo " a custom upstart conf file ${APP_NAME}.conf found"
         cp "${REALDIR}/${APP_NAME}.install" "/etc/init/${APP_NAME}.conf"
     else
         echo ' creating default upstart conf file..'
@@ -1328,7 +1328,7 @@ installdaemon() {
                                 echo "esac"  >> /etc/rc.d/${APP_NAME}
                                 chmod 755 /etc/rc.d/${APP_NAME}
                                 chown root:root /etc/rc.d/${APP_NAME}
-                                echo ' The $APP_LONG_NAME daemon has been installed.'
+                                echo " The $APP_LONG_NAME daemon has been installed."
                                 echo ' Add \"i2p\" to the DAEMONS variable in /etc/rc.conf to enable.'
                             else
                                 # We'll end up here if systemd is enabled.
@@ -1369,7 +1369,7 @@ installdaemon() {
                     if [ -n "$USE_UPSTART" -a -d "/etc/init" ] ; then
                         installUpstart
                     else
-                        echo ' Installing the $APP_LONG_NAME daemon using init.d..'
+                        echo " Installing the $APP_LONG_NAME daemon using init.d.."
                         ln -s "$REALPATH" "/etc/init.d/$APP_NAME"
                         update-rc.d "$APP_NAME" defaults
                     fi
@@ -1402,10 +1402,10 @@ installdaemon() {
         elif [ "$DIST_OS" = "aix" ] ; then
             echo 'Detected AIX:'
             if [ -f "/etc/rc.d/init.d/$APP_NAME" ] ; then
-                echo ' The $APP_LONG_NAME daemon is already installed as rc.d script.'
+                echo " The $APP_LONG_NAME daemon is already installed as rc.d script."
                 exit 1
             elif [ -n "`/usr/sbin/lsitab $APP_NAME`" -a -n "`/usr/bin/lssrc -S -s $APP_NAME`" ] ; then
-                echo ' The $APP_LONG_NAME daemon is already installed as SRC service.'
+                echo " The $APP_LONG_NAME daemon is already installed as SRC service."
                 exit 1
             else
                 eval echo " `gettext 'Installing the $APP_LONG_NAME daemon'`.."
@@ -1536,7 +1536,7 @@ removedaemon() {
                     /sbin/chkconfig --del "$APP_NAME"
                     rm -f "/etc/init.d/$APP_NAME"
                 elif [ -f "/etc/init/${APP_NAME}.conf" ] ; then
-                    echo ' Removing $APP_LONG_NAME daemon from upstart...'
+                    echo " Removing $APP_LONG_NAME daemon from upstart..."
                     rm "/etc/init/${APP_NAME}.conf"
                 else
                     eval echo " `gettext 'The $APP_LONG_NAME daemon is not currently installed.'`"
@@ -1575,11 +1575,11 @@ removedaemon() {
             elif [ -f /etc/lsb-release -o -f /etc/debian_version ] ; then
                 echo 'Detected Debian-based distribution:'
                 if [ -f "/etc/init.d/$APP_NAME" ] ; then
-                    echo ' Removing $APP_LONG_NAME daemon from init.d...'
+                    echo " Removing $APP_LONG_NAME daemon from init.d..."
                     update-rc.d -f "$APP_NAME" remove
                     rm -f "/etc/init.d/$APP_NAME"
                 elif [ -f "/etc/init/${APP_NAME}.conf" ] ; then
-                    echo ' Removing $APP_LONG_NAME daemon from upstart...'
+                    echo " Removing $APP_LONG_NAME daemon from upstart..."
                     rm "/etc/init/${APP_NAME}.conf"
                 else
                     eval echo " `gettext 'The $APP_LONG_NAME daemon is not currently installed.'`"
@@ -1674,7 +1674,7 @@ removedaemon() {
 }
 
 dump() {
-    echo 'Dumping $APP_LONG_NAME...'
+    echo "Dumping $APP_LONG_NAME..."
     getpid
     if [ "X$pid" = "X" ]
     then
@@ -1684,10 +1684,10 @@ dump() {
 
         if [ $? -ne 0 ]
         then
-            echo 'Failed to dump $APP_LONG_NAME.'
+            echo "Failed to dump $APP_LONG_NAME."
             exit 1
         else
-            echo 'Dumped $APP_LONG_NAME.'
+            echo "Dumped $APP_LONG_NAME."
         fi
     fi
 }
@@ -1697,14 +1697,14 @@ startmsg() {
     getpid
     if [ "X$pid" = "X" ]
     then
-        echo 'Starting $APP_LONG_NAME...  Wrapper:Stopped'
+        echo "Starting $APP_LONG_NAME...  Wrapper:Stopped"
     else
         if [ "X$DETAIL_STATUS" = "X" ]
         then
-            echo 'Starting $APP_LONG_NAME...  Wrapper:Running'
+            echo "Starting $APP_LONG_NAME...  Wrapper:Running"
         else
             getstatus
-            echo 'Starting $APP_LONG_NAME...  Wrapper:$STATUS, Java:$JAVASTATUS'
+            echo "Starting $APP_LONG_NAME...  Wrapper:$STATUS, Java:$JAVASTATUS"
         fi
     fi
 }
@@ -1714,14 +1714,14 @@ stopmsg() {
     getpid
     if [ "X$pid" = "X" ]
     then
-        echo 'Stopping $APP_LONG_NAME...  Wrapper:Stopped'
+        echo "Stopping $APP_LONG_NAME...  Wrapper:Stopped"
     else
         if [ "X$DETAIL_STATUS" = "X" ]
         then
-            echo 'Stopping $APP_LONG_NAME...  Wrapper:Running'
+            echo "Stopping $APP_LONG_NAME...  Wrapper:Running"
         else
             getstatus
-            echo 'Stopping $APP_LONG_NAME...  Wrapper:$STATUS, Java:$JAVASTATUS'
+            echo "Stopping $APP_LONG_NAME...  Wrapper:$STATUS, Java:$JAVASTATUS"
         fi
     fi
 }
@@ -1731,7 +1731,7 @@ showUsage() {
 
     if [ -n "$1" ]
     then
-        echo 'Unexpected command: $1'
+        echo "Unexpected command: $1"
         echo "";
     fi
 
diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java
index a411b009986dc5f1f6384384ad98aeea42885422..87e5bffca0684477c0ba8daee48da05fbea99f44 100644
--- a/router/java/src/net/i2p/router/RouterVersion.java
+++ b/router/java/src/net/i2p/router/RouterVersion.java
@@ -18,7 +18,7 @@ public class RouterVersion {
     /** deprecated */
     public final static String ID = "Monotone";
     public final static String VERSION = CoreVersion.VERSION;
-    public final static long BUILD = 0;
+    public final static long BUILD = 1;
 
     /** for example "-test" */
     public final static String EXTRA = "";