diff --git a/checklist.txt b/checklist.txt
index 75187a0c525545e101bee68f06250bc290316cf9..591cba775dc4811dbe099526f2048ef494c6a869 100644
--- a/checklist.txt
+++ b/checklist.txt
@@ -67,6 +67,7 @@ Website files to change:
 	index.html
 	index_de.html
 	hosts.txt (copy from mtn)
+        release-x.y.z.html (new)
 	Sync with mtn.i2p2.i2p
 
 Copy news.xml to subscription location
diff --git a/history.txt b/history.txt
index 3e39509d033d09679862ba7c895bfaed391d80d0..7a75d0f1377bc64100f5cc441b502991db6c403a 100644
--- a/history.txt
+++ b/history.txt
@@ -1,3 +1,12 @@
+2008-07-30 zzz
+    * Blocklists:
+      - New, disabled by default, except for blocking of
+        forever-shitlisted peers. See source for instructions
+        and file format.
+    * Transport - Reject peers from inbound connections:
+      - Check IP against blocklist
+      - Check router hash against forever-shitlist, then block IP
+
 2008-07-16 zzz
     * configpeer.jsp: New
     * i2psnark: Open completed files read-only the first time
diff --git a/news.xml b/news.xml
index 17199a86889969365868cfdb21b72a6306bb8b4c..e9efe9c7f414dcad271ac4816ee3585031595b72 100644
--- a/news.xml
+++ b/news.xml
@@ -24,16 +24,6 @@ for an adversary to gather meaningful statistics for a predecessor attack.
 Improvements to applications like I2PSnark and the Router Console are also introduced.
 </p>
 
-<p>
-&#149;
-2008-04-20: <b><a href="http://trac.i2p2.i2p/">We are now using Trac as bugtracker</a></b>
-</p>
-
-<p>
-&#149;
-2008-02-05: <b><a href="http://www.i2p2.i2p/upgrade-0.6.1.30.html">Upgrading from 0.6.1.30 and Earlier Releases</a></b>
-</p>
-
 <!--
 &#149;
 2007-04-10:
diff --git a/router/java/src/net/i2p/router/Blocklist.java b/router/java/src/net/i2p/router/Blocklist.java
new file mode 100644
index 0000000000000000000000000000000000000000..73682ec6096b166b4ce0ff01bcef51fd5758d7cc
--- /dev/null
+++ b/router/java/src/net/i2p/router/Blocklist.java
@@ -0,0 +1,749 @@
+package net.i2p.router;
+/*
+ * free (adj.): unencumbered; not under the control of others
+ * Use at your own risk.
+ * zzz 2008-06
+ */
+
+import java.io.IOException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.Writer;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.*;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.Base64;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Hash;
+import net.i2p.data.RouterAddress;
+import net.i2p.data.RouterInfo;
+import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade;
+import net.i2p.util.HexDump;
+import net.i2p.util.Log;
+
+/**
+ * Manage blocking by IP address, in a manner similar to the Shitlist,
+ * which blocks by router hash.
+ *
+ * We also try to keep the two lists in sync: if a router at a given IP is
+ * blocked, we will also shitlist it "forever" (until the next reboot).
+ *
+ * While the reverse case (blocking the IP of a router shitlisted forever)
+ * is not automatic, the transports will call add() below to block the IP,
+ * which allows the transports to terminate an inbound connection before
+ * the router ident handshake.
+ *
+ * And the on-disk blocklist can also contain router hashes to be shitlisted.
+ *
+ * So, this class maintains three separate lists:
+ *   1) The list of IP ranges, read in from a file at startup
+ *   2) The list of hashes, read in from the same file
+ *   3) A list of single IPs, initially empty, added to as needed
+ *
+ * Read in the IP blocklist from a file, store it in-memory as efficiently
+ * as we can, and perform tests against it as requested.
+ *
+ * When queried for a peer that is blocklisted but isn't shitlisted,
+ * shitlist it forever, then go back to the file to get the original
+ * entry so we can add the reason to the shitlist text.
+ *
+ */
+public class Blocklist {
+    private Log _log;
+    private RouterContext _context;
+    private long _blocklist[];
+    private int _blocklistSize;
+    private Object _lock;
+    private Entry _wrapSave;
+    private Set _inProcess;
+    private Map _peerBlocklist;
+    private Set _singleIPBlocklist;
+    
+    public Blocklist(RouterContext context) {
+        _context = context;
+        _log = context.logManager().getLog(Blocklist.class);
+        _blocklist = null;
+        _blocklistSize = 0;
+        _lock = new Object();
+        _wrapSave = null;
+        _inProcess = new HashSet(0);
+        _peerBlocklist = new HashMap(0);
+        _singleIPBlocklist = new HashSet(0);
+    }
+    
+    public Blocklist() {
+        _log = new Log(Blocklist.class);
+        _blocklist = null;
+        _blocklistSize = 0;
+    }
+    
+    static final String PROP_BLOCKLIST_ENABLED = "router.blocklist.enable";
+    static final String PROP_BLOCKLIST_DETAIL = "router.blocklist.detail";
+    static final String PROP_BLOCKLIST_FILE = "router.blocklist.file";
+    static final String BLOCKLIST_FILE_DEFAULT = "blocklist.txt";
+
+    public void startup() {
+        String enabled = _context.getProperty(PROP_BLOCKLIST_ENABLED, "false");
+        if (! "true".equals(enabled))
+            return;
+        String file = _context.getProperty(PROP_BLOCKLIST_FILE, BLOCKLIST_FILE_DEFAULT);
+        // Maybe someday we'll read in multiple files and merge them
+        // StringTokenizer tok = new StringTokenizer(file, " ,\r\n");
+        // while (tok.hasMoreTokens())
+        //    readBlocklistFile(tok.nextToken());
+        Job job = new ReadinJob(file);
+        job.getTiming().setStartAfter(_context.clock().now() + 2*60*1000);
+        _context.jobQueue().addJob(job);
+    }
+
+    private class ReadinJob extends JobImpl {
+        private String _file;
+        public ReadinJob (String f) {
+            super(_context);
+            _file = f;
+        }
+        public String getName() { return "Read Blocklist"; }
+        public void runJob() {
+            synchronized (_lock) {
+                try {
+                    readBlocklistFile(_file);
+                } catch (OutOfMemoryError oom) {
+                    _log.log(Log.CRIT, "OOM processing the blocklist");
+                    disable();
+                    return;
+                }
+            }
+            if (_blocklistSize <= 0)
+                return;
+            FloodfillNetworkDatabaseFacade fndf = (FloodfillNetworkDatabaseFacade) _context.netDb();
+            int count = 0;
+            for (Iterator iter = fndf.getKnownRouterData().iterator(); iter.hasNext(); ) {
+                RouterInfo ri = (RouterInfo) iter.next();
+                Hash peer = ri.getIdentity().getHash();
+                if (isBlocklisted(peer))
+                    count++;
+            }
+            if (count > 0 && _log.shouldLog(Log.ERROR))
+                _log.error("Blocklisted " + count + " routers in the netDb.");
+            for (Iterator iter = _peerBlocklist.keySet().iterator(); iter.hasNext(); ) {
+                Hash peer = (Hash) iter.next();
+                String reason = "Blocklisted by router hash";
+                String comment = (String) _peerBlocklist.get(peer);
+                if (comment != null)
+                    reason = reason + ": " + comment;
+                _context.shitlist().shitlistRouterForever(peer, reason);
+            }
+            _peerBlocklist = null;
+        }
+    }
+
+    public void disable() {
+        // hmm better block out any checks in process
+        synchronized (_lock) {
+            _blocklistSize = 0;
+            _blocklist = null;
+        }
+    }
+
+   /**
+    * Read in and parse the blocklist.
+    * The blocklist need not be sorted, and may contain overlapping entries.
+    *
+    * Acceptable formats (IPV4 only):
+    *   #comment (# must be in column 1)
+    *   comment:IP-IP
+    *   comment:morecomments:IP-IP
+    *   IP-IP
+    *   (comments also allowed before any of the following)
+    *   IP/masklength
+    *   IP
+    *   hostname (DNS looked up at list readin time, not dynamically, so may not be much use)
+    *   44-byte Base64 router hash
+    *
+    * No whitespace allowed after the last ':'.
+    *
+    * For further information and downloads:
+    *   http://www.bluetack.co.uk/forums/index.php?autocom=faq&CODE=02&qid=17
+    *   http://blocklist.googlepages.com/
+    *   http://www.cymru.com/Documents/bogon-list.html
+    */
+    private void readBlocklistFile(String file) {
+        File BLFile = new File(file);
+        if (BLFile == null || (!BLFile.exists()) || BLFile.length() <= 0) {
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("Blocklist file not found: " + file);
+            return;
+        }
+        long start = _context.clock().now();
+        int maxSize = getSize(BLFile);
+        try {
+            _blocklist = new long[maxSize + 1];  // extra for wrapsave
+        } catch (OutOfMemoryError oom) {
+            _log.log(Log.CRIT, "OOM creating the blocklist");
+            return;
+        }
+        int count = 0;
+        int badcount = 0;
+        int peercount = 0;
+        long ipcount = 0;
+        FileInputStream in = null;
+        try {
+            in = new FileInputStream(BLFile);
+            StringBuffer buf = new StringBuffer(128);
+            while (DataHelper.readLine(in, buf) && count < maxSize) {
+                Entry e = parse(buf, true);
+                buf.setLength(0);
+                if (e == null) {
+                    badcount++;
+                    continue;
+                }
+                if (e.peer != null) {
+                    _peerBlocklist.put(e.peer, e.comment);
+                    peercount++;
+                    continue;
+                }
+                byte[] ip1 = e.ip1;
+                byte[] ip2 = e.ip2;
+
+                store(ip1, ip2, count++);
+                ipcount += 1 + toInt(ip2) - toInt(ip1); // includes dups, oh well
+            }
+        } catch (IOException ioe) {
+            _blocklist = null;
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("Error reading the BLFile", ioe);
+            return;
+        } catch (OutOfMemoryError oom) {
+            _blocklist = null;
+            _log.log(Log.CRIT, "OOM reading the blocklist");
+            return;
+        } finally {
+            if (in != null) try { in.close(); } catch (IOException ioe) {}
+        }
+
+        if (_wrapSave != null) {
+            store(_wrapSave.ip1, _wrapSave.ip2, count++);
+            ipcount += 1 + toInt(_wrapSave.ip2) - toInt(_wrapSave.ip1);
+        }
+
+        // This is a standard signed sort, so the entries will be ordered
+        // 128.0.0.0 ... 255.255.255.255 0.0.0.0 .... 127.255.255.255
+        // But that's ok.
+        int removed = 0;
+        try {
+            Arrays.sort(_blocklist, 0, count);
+            removed = removeOverlap(_blocklist, count);
+            if (removed > 0) {
+                // Sort again to remove the dups that were "zeroed" out as 127.255.255.255-255.255.255.255
+                Arrays.sort(_blocklist, 0, count);
+                // sorry, no realloc to save memory, don't want to blow up now
+            }
+        } catch (OutOfMemoryError oom) {
+            _blocklist = null;
+            _log.log(Log.CRIT, "OOM sorting the blocklist");
+            return;
+        }
+        _blocklistSize = count - removed;
+        if (_log.shouldLog(Log.ERROR)) {
+            _log.error("Removed " + badcount + " bad entries and comment lines");
+            _log.error("Read " + count + " valid entries from the blocklist " + BLFile);
+            _log.error("Merged " + removed + " overlapping entries");
+            _log.error("Result is " + _blocklistSize + " entries");
+            _log.error("Blocking " + ipcount + " IPs and " + peercount + " hashes");
+            _log.error("Blocklist processing finished, time: " + (_context.clock().now() - start));
+        }
+    }
+
+    private class Entry {
+        String comment;
+        byte ip1[];
+        byte ip2[];
+        Hash peer;
+
+        public Entry(String c, Hash h, byte[] i1, byte[] i2) {
+             comment = c;
+             peer = h;
+             ip1 = i1;
+             ip2 = i2;
+        }
+    }
+
+    private Entry parse(StringBuffer buf, boolean bitch) {
+        byte[] ip1;
+        byte[] ip2;
+        int start1 = 0;
+        int end1 = buf.length();
+        int start2 = -1;
+        int mask = -1;
+        String comment = null;
+        int index = buf.indexOf("#");
+        if (index == 0)
+            return null;  // comment
+        index = buf.lastIndexOf(":");
+        if (index >= 0) {
+            comment = buf.substring(0, index);
+            start1 = index + 1;
+        }
+        if (end1 - start1 == 44 && buf.substring(start1).indexOf(".") < 0) {
+            byte b[] = Base64.decode(buf.substring(start1));
+            if (b != null)
+                return new Entry(comment, new Hash(b), null, null);
+        }
+        index = buf.indexOf("-", start1);
+        if (index >= 0) {
+            end1 = index;
+            start2 = index + 1;
+        } else {
+            index = buf.indexOf("/", start1);
+            if (index >= 0) {
+                end1 = index;
+                mask = index + 1;
+            }
+        }
+        try {
+            InetAddress pi = InetAddress.getByName(buf.substring(start1, end1));
+            if (pi == null) return null;
+            ip1 = pi.getAddress();
+            if (ip1.length != 4)
+                throw new UnknownHostException();
+            if (start2 >= 0) {
+                pi = InetAddress.getByName(buf.substring(start2));
+                if (pi == null) return null;
+                ip2 = pi.getAddress();
+                if (ip2.length != 4)
+                    throw new UnknownHostException();
+                if ((ip1[0] & 0xff) < 0x80 && (ip2[0] & 0xff) >= 0x80) {
+                    if (_wrapSave == null) {
+                        // don't cross the boundary 127.255.255.255 - 128.0.0.0
+                        // because we are sorting using signed arithmetic
+                        _wrapSave = new Entry(comment, null, new byte[] {(byte)0x80,0,0,0}, new byte[] {ip2[0], ip2[1], ip2[2], ip2[3]});
+                        ip2 = new byte[] {127, (byte)0xff, (byte)0xff, (byte)0xff};
+                    } else
+                        // We only save one entry crossing the boundary, throw the rest out
+                        throw new NumberFormatException();
+
+                }
+                for (int i = 0; i < 4; i++) {
+                     if ((ip2[i] & 0xff) > (ip1[i] & 0xff))
+                        break;
+                     if ((ip2[i] & 0xff) < (ip1[i] & 0xff))
+                        throw new NumberFormatException(); // backwards
+                }
+            } else if (mask >= 0) {
+                int m = Integer.parseInt(buf.substring(mask));
+                if (m < 3 || m > 32)
+                    throw new NumberFormatException();
+                ip2 = new byte[4];
+                // ick
+                for (int i = 0; i < 4; i++)
+                    ip2[i] = ip1[i];
+                for (int i = 0; i < 32-m; i++)
+                    ip2[(31-i)/8] |= (0x01 << (i%8));
+            } else {
+                ip2 = ip1;
+            }
+        } catch (UnknownHostException uhe) {
+            if (bitch && _log.shouldLog(Log.ERROR))
+                _log.error("Format error in the blocklist file: " + buf);
+            return null;
+        } catch (NumberFormatException nfe) {
+            if (bitch && _log.shouldLog(Log.ERROR))
+                _log.error("Format error in the blocklist file: " + buf);
+            return null;
+        } catch (IndexOutOfBoundsException ioobe) {
+            if (bitch && _log.shouldLog(Log.ERROR))
+                _log.error("Format error in the blocklist file: " + buf);
+            return null;
+        }
+        return new Entry(comment, null, ip1, ip2);
+    }
+
+    /**
+     * Read the file once just to see how many entries are in it,
+     * so we can size our array.
+     * This is i/o inefficient, but memory-efficient, which is what we want.
+     */
+    private int getSize(File BLFile) {
+        if ( (!BLFile.exists()) || (BLFile.length() <= 0) ) return 0;
+        int lines = 0;
+        FileInputStream in = null;
+        try {
+            in = new FileInputStream(BLFile);
+            StringBuffer buf = new StringBuffer(128);
+            while (DataHelper.readLine(in, buf)) {
+                lines++;
+                buf.setLength(0);
+            }
+        } catch (IOException ioe) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Error reading the BLFile", ioe);
+            return 0;
+        } finally {
+            if (in != null) try { in.close(); } catch (IOException ioe) {}
+        }
+        return lines;
+    }
+    
+    /**
+     * Merge and remove overlapping entries from a sorted list.
+     * Returns number of removed entries.
+     * Caller must re-sort if return code is > 0.
+     */
+    private int removeOverlap(long blist[], int count) {
+        if (count <= 0) return 0;
+        int lines = 0;
+        for (int i = 0; i < count - 1; ) {
+            int removed = 0;
+            int to = getTo(blist[i]);
+            for (int next = i + 1; next < count; next++) {
+                if (to < getFrom(blist[next]))
+                    break;
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Combining entries " + toStr(blist[i]) + " and " + toStr(blist[next]));
+                int nextTo = getTo(blist[next]);
+                if (nextTo > to) // else entry next is totally inside entry i
+                    store(getFrom(blist[i]), nextTo, i);
+                blist[next] = Long.MAX_VALUE;  // to be removed with another sort
+                lines++;
+                removed++;
+            }
+            i += removed + 1;
+        }
+        return lines;
+    }
+
+    // Maintain a simple in-memory single-IP blocklist
+    // This is used for new additions, NOT for the main list
+    // of IP ranges read in from the file.
+    public void add(String ip) {
+        InetAddress pi;
+        try {
+            pi = InetAddress.getByName(ip);
+        } catch (UnknownHostException uhe) {
+            return;
+        }
+        if (pi == null) return;
+        byte[] pib = pi.getAddress();
+        add(pib);
+    }
+
+    public void add(byte ip[]) {
+        if (ip.length != 4)
+            return;
+        if (add(toInt(ip)))
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("Adding IP to blocklist: " + ip[0] + '.' + ip[1] + '.' + ip[2] + '.' + ip[3]);
+    }
+
+    private boolean add(int ip) {
+        synchronized(_singleIPBlocklist) {
+            return _singleIPBlocklist.add(new Integer(ip));
+        }
+    }
+
+    private boolean isOnSingleList(int ip) {
+        synchronized(_singleIPBlocklist) {
+            return _singleIPBlocklist.contains(new Integer(ip));
+        }
+    }
+
+    /**
+     * this tries to not return duplicates
+     * but I suppose it could.
+     */
+    public List getAddresses(Hash peer) {
+        List rv = new ArrayList(1);
+        RouterInfo pinfo = _context.netDb().lookupRouterInfoLocally(peer);
+        if (pinfo == null) return rv;
+        Set paddr = pinfo.getAddresses();
+        if (paddr == null || paddr.size() == 0)
+            return rv;
+        String oldphost = null;
+        List pladdr = new ArrayList(paddr);
+        // for each peer address
+        for (int j = 0; j < paddr.size(); j++) {
+            RouterAddress pa = (RouterAddress) pladdr.get(j);
+            if (pa == null) continue;
+            Properties pprops = pa.getOptions();
+            if (pprops == null) continue;
+            String phost = pprops.getProperty("host");
+            if (phost == null) continue;
+            if (oldphost != null && oldphost.equals(phost)) continue;
+            oldphost = phost;
+            InetAddress pi;
+            try {
+                pi = InetAddress.getByName(phost);
+            } catch (UnknownHostException uhe) {
+                continue;
+            }
+            if (pi == null) continue;
+            byte[] pib = pi.getAddress();
+            rv.add(pib);
+         }
+         return rv;
+    }
+
+    /**
+     * Does the peer's IP address appear in the blocklist?
+     * If so, and it isn't shitlisted, shitlist it forever...
+     */
+    public boolean isBlocklisted(Hash peer) {
+        List ips = getAddresses(peer);
+        for (Iterator iter = ips.iterator(); iter.hasNext(); ) {
+            byte ip[] = (byte[]) iter.next();
+            if (isBlocklisted(ip)) {
+                if (! _context.shitlist().isShitlisted(peer))
+                    // nice knowing you...
+                    shitlist(peer);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    // calling this externally won't shitlist the peer, this is just an IP check
+    public boolean isBlocklisted(String ip) {
+        InetAddress pi;
+        try {
+            pi = InetAddress.getByName(ip);
+        } catch (UnknownHostException uhe) {
+            return false;
+        }
+        if (pi == null) return false;
+        byte[] pib = pi.getAddress();
+        return isBlocklisted(pib);
+    }
+
+    // calling this externally won't shitlist the peer, this is just an IP check
+    public boolean isBlocklisted(byte ip[]) {
+        if (ip.length != 4)
+            return false;
+        return isBlocklisted(toInt(ip));
+    }
+
+    /**
+     * First check the single-IP list.
+     * Then do a
+     * binary search through the in-memory range list which
+     * is a sorted array of longs.
+     * The array is sorted in signed order, but we don't care.
+     * Each long is ((from << 32) | to)
+     **/ 
+    private boolean isBlocklisted(int ip) {
+        if (isOnSingleList(ip))
+            return true;
+        int hi = _blocklistSize - 1;
+        if (hi <= 0)
+            return false;
+        int lo = 0;
+        int cur = hi / 2;
+
+        while  (!match(ip, cur)) {
+            if (isHigher(ip, cur))
+                lo = cur;
+            else
+                hi = cur;
+            // make sure we get the last one
+            if (hi - lo <= 1) {
+                if (lo == cur)
+                    cur = hi;        
+                else        
+                    cur = lo;        
+                break;
+            } else {
+                cur = lo + ((hi - lo) / 2);
+            }
+        }
+        return match(ip, cur);
+    }
+
+    // Is the IP included in the entry _blocklist[cur] ?
+    private boolean match(int ip, int cur) {
+        return match(ip, _blocklist[cur]);
+    }
+
+    // Is the IP included in the compressed entry?
+    private boolean match(int ip, long entry) {
+        if (getFrom(entry) > ip)
+                return false;
+        return (ip <= getTo(entry));
+    }
+
+    // Is the IP higher than the entry _blocklist[cur] ?
+    private boolean isHigher(int ip, int cur) {
+        return ip > getFrom(_blocklist[cur]);
+    }
+
+    // methods to get and store the from/to values in the array
+
+    private int getFrom(long entry) {
+        return (int) ((entry >> 32) & 0xffffffff);
+    }
+
+    private int getTo(long entry) {
+        return (int) (entry & 0xffffffff);
+    }
+
+    /**
+     * The in-memory blocklist is an array of longs, with the format
+     * ((from IP) << 32) | (to IP)
+     * The XOR is so the signed sort is in normal (unsigned) order.
+     *
+     * So the size is (cough) almost 2MB for the 240,000 line splist.txt.
+     *
+     */
+    private long toEntry(byte ip1[], byte ip2[]) {
+        long entry = 0;
+        for (int i = 0; i < 4; i++)
+            entry |= ((long) (ip2[i] & 0xff)) << ((3-i)*8);
+        for (int i = 0; i < 4; i++)
+            entry |= ((long) (ip1[i] & 0xff)) << (32 + ((3-i)*8));
+        return entry;
+    }
+
+    private void store(byte ip1[], byte ip2[], int idx) {
+        _blocklist[idx] = toEntry(ip1, ip2);
+    }
+
+    private void store(int ip1, int ip2, int idx) {
+        long entry = ((long) ip1) << 32;
+        entry |= ip2;
+        _blocklist[idx] = entry;
+    }
+
+    private int toInt(byte ip[]) {
+        int rv = 0;
+        for (int i = 0; i < 4; i++)
+            rv |= (ip[i] & 0xff) << ((3-i)*8);
+        return rv;
+    }
+
+    private String toStr(long entry) {
+        StringBuffer buf = new StringBuffer(128);
+        for (int i = 7; i >= 0; i--) {
+            buf.append((entry >> (8*i)) & 0xff);
+            if (i == 4)
+                buf.append('-');
+            else if (i > 0)
+                buf.append('.');
+        }
+        return buf.toString();
+    }
+
+    /**
+     * We don't keep the comment field in-memory,
+     * so we have to go back out to the file to find it.
+     *
+     * Put this in a job because we're looking for the
+     * actual line in the blocklist file, this could take a while.
+     *
+     */
+    public void shitlist(Hash peer) {
+        // Temporary reason, until the job finishes
+        _context.shitlist().shitlistRouterForever(peer, "IP Blocklisted");
+        if (! "true".equals( _context.getProperty(PROP_BLOCKLIST_DETAIL, "true")))
+            return;
+        boolean shouldRunJob;
+        int number;
+        synchronized (_inProcess) {
+            number = _inProcess.size();
+            shouldRunJob = _inProcess.add(peer);
+        }
+        if (!shouldRunJob)
+            return;
+        Job job = new ShitlistJob(peer);
+        if (number > 0)
+            job.getTiming().setStartAfter(_context.clock().now() + (number * 30*1000));
+        _context.jobQueue().addJob(job);
+    }
+
+    private class ShitlistJob extends JobImpl {
+        private Hash _peer;
+        public ShitlistJob (Hash p) {
+            super(_context);
+            _peer = p;
+        }
+        public String getName() { return "Shitlist Peer Forever"; }
+        public void runJob() {
+            shitlistForever(_peer);
+            synchronized (_inProcess) {
+                _inProcess.remove(_peer);
+            }
+        }
+    }
+
+    /**
+     * Look up the original record so we can record the reason in the shitlist.
+     * That's the only reason to do this.
+     * Only synchronize to cut down on the I/O load.
+     * Additional jobs can wait.
+     * Although could this clog up the job queue runners? Yes.
+     * So we also stagger these jobs.
+     *
+     */
+    private synchronized void shitlistForever(Hash peer) {
+        String file = _context.getProperty(PROP_BLOCKLIST_FILE, BLOCKLIST_FILE_DEFAULT);
+        File BLFile = new File(file);
+        if (BLFile == null || (!BLFile.exists()) || BLFile.length() <= 0) {
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("Blocklist file not found: " + file);
+            return;
+        }
+
+        // look through the file for each address to find which one was the cause
+        List ips = getAddresses(peer);
+        for (Iterator iter = ips.iterator(); iter.hasNext(); ) {
+            byte ip[] = (byte[]) iter.next();
+            int ipint = toInt(ip);
+            FileInputStream in = null;
+            try {
+                in = new FileInputStream(BLFile);
+                StringBuffer buf = new StringBuffer(128);
+                // assume the file is unsorted, so go through the whole thing
+                while (DataHelper.readLine(in, buf)) {
+                    Entry e = parse(buf, false);
+                    if (e == null || e.peer != null) {
+                        buf.setLength(0);
+                        continue;
+                    }
+                    if (match(ipint, toEntry(e.ip1, e.ip2))) {
+                        try { in.close(); } catch (IOException ioe) {}
+                        String reason = "IP ";
+                        for (int i = 0; i < 4; i++) {
+                            reason = reason + (ip[i] & 0xff);
+                            if (i != 3)
+                                reason = reason + '.';
+                        }
+                        reason = reason + " blocklisted by entry \"" + buf + "\"";
+                        if (_log.shouldLog(Log.ERROR))
+                            _log.error("Shitlisting " + peer + " " + reason);
+                        _context.shitlist().shitlistRouterForever(peer, reason);
+                        return;
+                    }
+                    buf.setLength(0);
+                }
+            } catch (IOException ioe) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Error reading the BLFile", ioe);
+            } finally {
+                if (in != null) try { in.close(); } catch (IOException ioe) {}
+            }
+        }
+        // We already shitlisted in shitlist(peer), that's good enough
+    }
+
+    public static void main(String args[]) {
+        Blocklist b = new Blocklist();
+        if ( (args != null) && (args.length == 1) )
+            b.readBlocklistFile(args[0]);
+        System.out.println("Saved " + b._blocklistSize + " records");
+        String tests[] = {"0.0.0.0", "0.0.0.1", "0.0.0.2", "0.0.0.255", "1.0.0.0",
+                                        "3.3.3.3", "77.1.2.3", "127.0.0.0", "127.127.127.127", "128.0.0.0",
+                                        "129.1.2.3", "255.255.255.254", "255.255.255.255"};
+        for (int i = 0; i < tests.length; i++) {
+            System.out.println("Testing " + tests[i] + " returns " + b.isBlocklisted(tests[i]));
+        }
+    }
+}
diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java
index 3b687098fdb84795ebd604c10687a03540be85d5..827e0c457e6ef9b474e5a32148aafef42848902a 100644
--- a/router/java/src/net/i2p/router/Router.java
+++ b/router/java/src/net/i2p/router/Router.java
@@ -259,6 +259,7 @@ public class Router {
         warmupCrypto();
         _sessionKeyPersistenceHelper.startup();
         //_context.adminManager().startup();
+        _context.blocklist().startup();
         
         // let the timestamper get us sync'ed
         long before = System.currentTimeMillis();
@@ -380,8 +381,8 @@ public class Router {
         int bwLim = Math.min(_context.bandwidthLimiter().getInboundKBytesPerSecond(),
                              _context.bandwidthLimiter().getOutboundKBytesPerSecond());
         bwLim = (int)(((float)bwLim) * getSharePercentage());
-        if (_log.shouldLog(Log.WARN))
-            _log.warn("Adding capabilities w/ bw limit @ " + bwLim, new Exception("caps"));
+        if (_log.shouldLog(Log.INFO))
+            _log.info("Adding capabilities w/ bw limit @ " + bwLim, new Exception("caps"));
         
         if (bwLim < 12) {
             ri.addCapability(CAPABILITY_BW12);
diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java
index 195db00d0d468786170b914feea3b4c2f9d1c922..8b34f636417fecf9f5d43345fb30cca459729e65 100644
--- a/router/java/src/net/i2p/router/RouterContext.java
+++ b/router/java/src/net/i2p/router/RouterContext.java
@@ -56,6 +56,7 @@ public class RouterContext extends I2PAppContext {
     private TunnelDispatcher _tunnelDispatcher;
     private StatisticsManager _statPublisher;
     private Shitlist _shitlist;
+    private Blocklist _blocklist;
     private MessageValidator _messageValidator;
     private MessageStateMonitor _messageStateMonitor;
     private RouterThrottle _throttle;
@@ -126,6 +127,7 @@ public class RouterContext extends I2PAppContext {
         _tunnelDispatcher = new TunnelDispatcher(this);
         _statPublisher = new StatisticsManager(this);
         _shitlist = new Shitlist(this);
+        _blocklist = new Blocklist(this);
         _messageValidator = new MessageValidator(this);
         //_throttle = new RouterThrottleImpl(this);
         _throttle = new RouterDoSThrottle(this);
@@ -249,6 +251,7 @@ public class RouterContext extends I2PAppContext {
      * who does this peer hate?
      */
     public Shitlist shitlist() { return _shitlist; }
+    public Blocklist blocklist() { return _blocklist; }
     /**
      * The router keeps track of messages it receives to prevent duplicates, as
      * well as other criteria for "validity".
@@ -346,4 +349,4 @@ public class RouterContext extends I2PAppContext {
         }
     }
 
-}
\ No newline at end of file
+}
diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java
index 6900613bee42561555e8b662e64b2ace753bda7d..2e107107a7603a6b96b67d6a66832259022dbf79 100644
--- a/router/java/src/net/i2p/router/RouterVersion.java
+++ b/router/java/src/net/i2p/router/RouterVersion.java
@@ -17,7 +17,7 @@ import net.i2p.CoreVersion;
 public class RouterVersion {
     public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $";
     public final static String VERSION = "0.6.2";
-    public final static long BUILD = 9;
+    public final static long BUILD = 10;
     public static void main(String args[]) {
         System.out.println("I2P Router version: " + VERSION + "-" + BUILD);
         System.out.println("Router ID: " + RouterVersion.ID);
diff --git a/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java b/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java
index 1ecc078d11de7f5ada4d6753daadf1f0651f988f..d18cc1ad3aea1d64dbedd0ad58f84ab86f654ae6 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java
@@ -546,6 +546,15 @@ public class EstablishState {
             Signature sig = new Signature(s);
             _verified = _context.dsa().verifySignature(sig, toVerify, alice.getSigningPublicKey());
             if (_verified) {
+                if (_context.shitlist().isShitlistedForever(alice.calculateHash())) {
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn("Dropping inbound connection from permanently shitlisted peer: " + alice.calculateHash().toBase64());
+                    // So next time we will not accept the con from this IP,
+                    // rather than doing the whole handshake
+                    _context.blocklist().add(_con.getChannel().socket().getInetAddress().getAddress());
+                    fail("Peer is shitlisted forever: " + alice.calculateHash().toBase64());
+                    return;
+                }
                 if (_log.shouldLog(Log.DEBUG))
                     _log.debug(prefix() + "verification successful for " + _con);
                                 
diff --git a/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java b/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java
index dfb5220044f840e73ffda1aecc0aa2b9d3b77283..61ef6df4594aaa50e6ec3fcb19fd0f7862a53903 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java
@@ -383,6 +383,14 @@ public class EventPumper implements Runnable {
         try {
             SocketChannel chan = servChan.accept();
             chan.configureBlocking(false);
+            if (_context.blocklist().isBlocklisted(chan.socket().getInetAddress().getAddress())) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Receive session request from blocklisted IP: " + chan.socket().getInetAddress());
+                // need to add this stat first
+                // _context.statManager().addRateData("ntcp.connectBlocklisted", 1, 0);
+                try { chan.close(); } catch (IOException ioe) { }
+                return;
+            }
             SelectionKey ckey = chan.register(_selector, SelectionKey.OP_READ);
             NTCPConnection con = new NTCPConnection(_context, _transport, chan, ckey);
             if (_log.shouldLog(Log.DEBUG))
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 bb35381ae77b9e87f8f6f9e0ca609f4351c71062..884947b11e07df0908bee7e89f753da4d204eb05 100644
--- a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
@@ -266,6 +266,11 @@ public class EstablishmentManager {
             
             state = (InboundEstablishState)_inboundStates.get(from);
             if (state == null) {
+                if (_context.blocklist().isBlocklisted(from.getIP())) {
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn("Receive session request from blocklisted IP: " + from);
+                    return; // drop the packet
+                }
                 state = new InboundEstablishState(_context, from.getIP(), from.getPort(), _transport.getLocalPort());
                 state.receiveSessionRequest(reader.getSessionRequestReader());
                 isNew = true;
@@ -793,7 +798,16 @@ public class EstablishmentManager {
                         sendCreated(inboundState);
                     break;
                 case InboundEstablishState.STATE_CONFIRMED_COMPLETELY:
-                    if (inboundState.getConfirmedIdentity() != null) {
+                    RouterIdentity remote = inboundState.getConfirmedIdentity();
+                    if (remote != null) {
+                        if (_context.shitlist().isShitlistedForever(remote.calculateHash())) {
+                            if (_log.shouldLog(Log.WARN))
+                                _log.warn("Dropping inbound connection from permanently shitlisted peer: " + remote.calculateHash().toBase64());
+                            // So next time we will not accept the con, rather than doing the whole handshake
+                            _context.blocklist().add(inboundState.getSentIP());
+                            inboundState.fail();
+                            break;
+                        }
                         handleCompletelyEstablished(inboundState);
                         break;
                     } else {