diff --git a/apps/i2psnark/java/build.xml b/apps/i2psnark/java/build.xml
index 02eee0aabc36ef863fdd4fd561ac646824761dd4..9689fda06823a17019d39cd05c3488b19a433d70 100644
--- a/apps/i2psnark/java/build.xml
+++ b/apps/i2psnark/java/build.xml
@@ -18,7 +18,6 @@
             <!-- Depend on classes instead of jars where available -->
             <classpath>
                 <pathelement location="../../../core/java/build/obj" />
-                <pathelement location="../../../router/java/build/obj" />
                 <pathelement location="../../ministreaming/java/build/obj" />
                 <pathelement location="../../jetty/jettylib/org.mortbay.jetty.jar" />
                 <pathelement location="../../jetty/jettylib/javax.servlet.jar" />
@@ -32,7 +31,7 @@
             srcdir="./src" 
             debug="true" deprecation="on" source="1.5" target="1.5" 
             destdir="./build/obj" 
-            classpath="../../../core/java/build/i2p.jar:../../../router/java/build/router.jar:../../jetty/jettylib/org.mortbay.jetty.jar:../../jetty/jettylib/javax.servlet.jar:../../ministreaming/java/build/mstreaming.jar" />
+            classpath="../../../core/java/build/i2p.jar:../../jetty/jettylib/org.mortbay.jetty.jar:../../jetty/jettylib/javax.servlet.jar:../../ministreaming/java/build/mstreaming.jar" />
     </target>
     <target name="jar" depends="builddep, compile">
         <jar destfile="./build/i2psnark.jar" basedir="./build/obj" includes="**/*.class" excludes="**/*Servlet.class">
diff --git a/apps/i2psnark/java/src/org/klomp/snark/BWLimits.java b/apps/i2psnark/java/src/org/klomp/snark/BWLimits.java
new file mode 100644
index 0000000000000000000000000000000000000000..eb157cb5e4426bb87f5538fbe92fee86175724da
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/BWLimits.java
@@ -0,0 +1,44 @@
+/*
+ * Released into the public domain 
+ * with no warranty of any kind, either expressed or implied.  
+ */
+package org.klomp.snark;
+
+import java.util.Arrays;
+import java.util.Properties;
+
+import net.i2p.I2PAppContext;
+import net.i2p.client.I2PSessionException;
+import net.i2p.client.I2PClient;
+import net.i2p.client.I2PSession;
+import net.i2p.client.I2PSimpleClient;
+
+/**
+ * Connect via I2CP and ask the router the bandwidth limits.
+ *
+ * The call is blocking and returns null on failure.
+ * Timeout is set to 5 seconds in I2PSimpleSession but it should be much faster.
+ *
+ * @author zzz
+ */
+class BWLimits {
+
+    public static int[] getBWLimits(String host, int port) {
+        int[] rv = null;
+        try {
+            I2PClient client = new I2PSimpleClient();
+            Properties opts = new Properties();
+            opts.put(I2PClient.PROP_TCP_HOST, host);
+            opts.put(I2PClient.PROP_TCP_PORT, "" + port);
+            I2PSession session = client.createSession(null, opts);
+            session.connect();
+            rv = session.bandwidthLimits();
+            session.destroySession();
+        } catch (I2PSessionException ise) {}
+        return rv;
+    }
+
+    public static void main(String args[]) {
+        System.out.println(Arrays.toString(getBWLimits("127.0.0.1", 7654)));
+    }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java
index e124955cfc3bc551207bc5291e79f2c25ecdb71e..53c42dfadf30b51dfbd50bfc66e6140480267b99 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java
@@ -36,7 +36,6 @@ import java.util.Timer;
 import java.util.TimerTask;
 
 import net.i2p.I2PAppContext;
-import net.i2p.router.client.ClientManagerFacadeImpl;
 import net.i2p.client.streaming.I2PServerSocket;
 import net.i2p.data.Destination;
 import net.i2p.util.I2PThread;
@@ -261,9 +260,9 @@ public class Snark
   public Snark(I2PAppContext ctx, Properties opts, String torrent,
                StorageListener slistener, boolean start, String rootDir) { 
     this(new I2PSnarkUtil(ctx), torrent, null, -1, slistener, null, null, null, null, false, rootDir);
-    String host = opts.getProperty(ClientManagerFacadeImpl.PROP_CLIENT_HOST);
+    String host = opts.getProperty("i2cp.hostname");
     int port = 0;
-    String s = opts.getProperty(ClientManagerFacadeImpl.PROP_CLIENT_PORT);
+    String s = opts.getProperty("i2cp.port");
     if (s != null) {
         try {
              port = Integer.parseInt(s);
diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
index 54367af1ac90658eceaaa1dd3a155b2bfef9234f..01a93a58c12fae63ecff4158415105eda292130f 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
@@ -18,7 +18,6 @@ import java.util.TreeMap;
 import net.i2p.I2PAppContext;
 import net.i2p.data.Base64;
 import net.i2p.data.DataHelper;
-import net.i2p.router.RouterContext;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
 
@@ -147,22 +146,21 @@ public class SnarkManager implements Snark.CompleteListener {
             _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_UPBW_MAX)) {
-            try {
-                if (_context instanceof RouterContext)
-                    _config.setProperty(PROP_UPBW_MAX, "" + (((RouterContext)_context).bandwidthLimiter().getOutboundKBytesPerSecond() / 2));
-                else
-                    _config.setProperty(PROP_UPBW_MAX, "" + DEFAULT_MAX_UP_BW);
-            } catch (NoClassDefFoundError ncdfe) {
-                _config.setProperty(PROP_UPBW_MAX, "" + DEFAULT_MAX_UP_BW);
-            }
-        }
         if (!_config.containsKey(PROP_DIR))
             _config.setProperty(PROP_DIR, "i2psnark");
         if (!_config.containsKey(PROP_AUTO_START))
             _config.setProperty(PROP_AUTO_START, DEFAULT_AUTO_START);
         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);
@@ -619,6 +617,9 @@ public class SnarkManager implements Snark.CompleteListener {
                     _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());
diff --git a/core/java/src/net/i2p/client/BWLimitsMessageHandler.java b/core/java/src/net/i2p/client/BWLimitsMessageHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..b47eaa6c8f3ceaa4359c5fb1c76ab4ff44989434
--- /dev/null
+++ b/core/java/src/net/i2p/client/BWLimitsMessageHandler.java
@@ -0,0 +1,25 @@
+package net.i2p.client;
+
+/*
+ * Released into the public domain 
+ * with no warranty of any kind, either expressed or implied.  
+ */
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.i2cp.I2CPMessage;
+import net.i2p.data.i2cp.BandwidthLimitsMessage;
+
+/**
+ * Handle I2CP BW replies from the router
+ */
+class BWLimitsMessageHandler extends HandlerImpl {
+    public BWLimitsMessageHandler(I2PAppContext ctx) {
+        super(ctx, BandwidthLimitsMessage.MESSAGE_TYPE);
+    }
+    
+    public void handleMessage(I2CPMessage message, I2PSessionImpl session) {
+        _log.debug("Handle message " + message);
+        BandwidthLimitsMessage msg = (BandwidthLimitsMessage) message;
+       ((I2PSimpleSession)session).bwReceived(msg.getLimits());
+    }
+}
diff --git a/core/java/src/net/i2p/client/I2PSession.java b/core/java/src/net/i2p/client/I2PSession.java
index 1776af5c0f91200ccfcfbcbb20ec0101fb8f821e..1998dad55a737a016d558e456ae2f632eff13e6c 100644
--- a/core/java/src/net/i2p/client/I2PSession.java
+++ b/core/java/src/net/i2p/client/I2PSession.java
@@ -143,6 +143,11 @@ public interface I2PSession {
      */
     public Destination lookupDest(Hash h) throws I2PSessionException;
 
+    /**
+     * Get the current bandwidth limits
+     */
+    public int[] bandwidthLimits() throws I2PSessionException;
+
     /** See I2PSessionMuxedImpl for details */
     public void addSessionListener(I2PSessionListener lsnr, int proto, int port);
     /** See I2PSessionMuxedImpl for details */
diff --git a/core/java/src/net/i2p/client/I2PSessionImpl.java b/core/java/src/net/i2p/client/I2PSessionImpl.java
index 0e13f2c56308c82a587081bec7cc0b7e23e178b7..5b7603fdd605be34d10c3f3628fb1270175f6774 100644
--- a/core/java/src/net/i2p/client/I2PSessionImpl.java
+++ b/core/java/src/net/i2p/client/I2PSessionImpl.java
@@ -656,6 +656,10 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
         return null;
     }
 
+    public int[] bandwidthLimits() throws I2PSessionException {
+        return null;
+    }
+
     protected void updateActivity() {
         _lastActivity = _context.clock().now();
         if (_isReduced) {
diff --git a/core/java/src/net/i2p/client/I2PSimpleSession.java b/core/java/src/net/i2p/client/I2PSimpleSession.java
index fcfafe7672885b09bc532febc0618506c4aa4bfd..b417bd7f7abcf9b19af31ab16b2abab70ac75296 100644
--- a/core/java/src/net/i2p/client/I2PSimpleSession.java
+++ b/core/java/src/net/i2p/client/I2PSimpleSession.java
@@ -17,25 +17,32 @@ import net.i2p.I2PAppContext;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Destination;
 import net.i2p.data.Hash;
+import net.i2p.data.i2cp.BandwidthLimitsMessage;
 import net.i2p.data.i2cp.DestLookupMessage;
 import net.i2p.data.i2cp.DestReplyMessage;
+import net.i2p.data.i2cp.GetBandwidthLimitsMessage;
 import net.i2p.data.i2cp.I2CPMessageReader;
 import net.i2p.util.I2PThread;
 import net.i2p.util.Log;
 
 /**
- * Create a new session for doing naming queries only. Do not create a Destination.
+ * Create a new session for doing naming and bandwidth queries only. Do not create a Destination.
  * Don't create a producer. Do not send/receive messages to other Destinations.
  * Cannot handle multiple simultaneous queries atm.
  * Could be expanded to ask the router other things.
+ *
+ * @author zzz
  */
 class I2PSimpleSession extends I2PSessionImpl2 {
     private boolean _destReceived;
     private Object _destReceivedLock;
     private Destination _destination;
+    private boolean _bwReceived;
+    private Object _bwReceivedLock;
+    private int[] _bwLimits;
 
     /**
-     * Create a new session for doing naming queries only. Do not create a destination.
+     * Create a new session for doing naming and bandwidth queries only. Do not create a destination.
      *
      * @throws I2PSessionException if there is a problem
      */
@@ -94,6 +101,14 @@ class I2PSimpleSession extends I2PSessionImpl2 {
         }
     }
 
+    void bwReceived(int[] i) {
+        _bwReceived = true;
+        _bwLimits = i;
+        synchronized (_bwReceivedLock) {
+            _bwReceivedLock.notifyAll();
+        }
+    }
+
     public Destination lookupDest(Hash h) throws I2PSessionException {
         if (_closed)
             return null;
@@ -110,14 +125,31 @@ class I2PSimpleSession extends I2PSessionImpl2 {
         return _destination;
     }
 
+    public int[] bandwidthLimits() throws I2PSessionException {
+        if (_closed)
+            return null;
+        _bwReceivedLock = new Object();
+        sendMessage(new GetBandwidthLimitsMessage());
+        for (int i = 0; i < 5 && !_bwReceived; i++) {
+            try {
+                synchronized (_bwReceivedLock) {
+                    _bwReceivedLock.wait(1000);
+                }
+            } catch (InterruptedException ie) {}
+        }
+        _bwReceived = false;
+        return _bwLimits;
+    }
+
     /**
      * Only map message handlers that we will use
      */
     class SimpleMessageHandlerMap extends I2PClientMessageHandlerMap {
         public SimpleMessageHandlerMap(I2PAppContext context) {
-            int highest = DestReplyMessage.MESSAGE_TYPE;
+            int highest = Math.max(DestReplyMessage.MESSAGE_TYPE, BandwidthLimitsMessage.MESSAGE_TYPE);
             _handlers = new I2CPMessageHandler[highest+1];
             _handlers[DestReplyMessage.MESSAGE_TYPE] = new DestReplyMessageHandler(context);
+            _handlers[BandwidthLimitsMessage.MESSAGE_TYPE] = new BWLimitsMessageHandler(context);
         }
     }
 }
diff --git a/core/java/src/net/i2p/data/i2cp/BandwidthLimitsMessage.java b/core/java/src/net/i2p/data/i2cp/BandwidthLimitsMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..2f8d567869d70abf06bbb9690967d78e33c9affc
--- /dev/null
+++ b/core/java/src/net/i2p/data/i2cp/BandwidthLimitsMessage.java
@@ -0,0 +1,101 @@
+package net.i2p.data.i2cp;
+
+/*
+ * public domain
+ *
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.util.Log;
+
+/**
+ * Tell the other side the limits
+ *
+ * @author zzz
+ */
+public class BandwidthLimitsMessage extends I2CPMessageImpl {
+    private final static Log _log = new Log(BandwidthLimitsMessage.class);
+    public final static int MESSAGE_TYPE = 23;
+    private static final int LIMITS = 16;
+    private int[] data;
+
+    public BandwidthLimitsMessage() {
+        super();
+        data = new int[LIMITS];
+    }
+
+    /**
+     * Let's define it this way.
+     * Leave some extra. This is only local and rarely sent so we don't care about waste.
+     *
+     * 0) Client inbound limit (KBps)
+     * 1) Client outbound limit (KBps)
+     * 2) Router inbound limit (KBps)
+     * 3) Router inbound burst limit (KBps)
+     * 4) Router outbound limit (KBps)
+     * 5) Router outbound burst limit (KBps)
+     * 6) Router burst time (seconds)
+     * 7-15) undefined
+     */
+    public BandwidthLimitsMessage(int in, int out) {
+        this();
+        data[0] = in;
+        data[1] = out;
+    }
+
+    public int[] getLimits() {
+        return data;
+    }
+
+    @Override
+    protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException {
+        try {
+            for (int i = 0; i < LIMITS; i++) {
+                data[i] = (int) DataHelper.readLong(in, 4);
+            }
+        } catch (DataFormatException dfe) {
+            throw new I2CPMessageException("Unable to load the message data", dfe);
+        }
+    }
+
+    @Override
+    protected byte[] doWriteMessage() throws I2CPMessageException, IOException {
+        ByteArrayOutputStream os = new ByteArrayOutputStream(64);
+        try {
+            for (int i = 0; i < LIMITS; i++) {
+                DataHelper.writeLong(os, 4, data[i]);
+            }
+        } catch (DataFormatException dfe) {
+            throw new I2CPMessageException("Error writing out the message data", dfe);
+        }
+        return os.toByteArray();
+    }
+    
+    public int getType() {
+        return MESSAGE_TYPE;
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        if ((object != null) && (object instanceof BandwidthLimitsMessage)) {
+            BandwidthLimitsMessage msg = (BandwidthLimitsMessage) object;
+            return DataHelper.eq(data, msg.getLimits());
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer buf = new StringBuffer();
+        buf.append("[BandwidthLimitsMessage");
+        buf.append("\n\tIn: ").append(data[0]);
+        buf.append("\n\tOut: ").append(data[1]);
+        buf.append("]");
+        return buf.toString();
+    }
+}
diff --git a/core/java/src/net/i2p/data/i2cp/GetBandwidthLimitsMessage.java b/core/java/src/net/i2p/data/i2cp/GetBandwidthLimitsMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..a47f883d78a200a776e0d313d05d5fd4bd178c99
--- /dev/null
+++ b/core/java/src/net/i2p/data/i2cp/GetBandwidthLimitsMessage.java
@@ -0,0 +1,56 @@
+package net.i2p.data.i2cp;
+
+/*
+ * public domain
+ *
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import net.i2p.util.Log;
+
+/**
+ * Request the router tells us the current bw limits
+ *
+ * @author zzz
+ */
+public class GetBandwidthLimitsMessage extends I2CPMessageImpl {
+    private final static Log _log = new Log(GetBandwidthLimitsMessage.class);
+    public final static int MESSAGE_TYPE = 8;
+
+    public GetBandwidthLimitsMessage() {
+        super();
+    }
+
+    @Override
+    protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException {
+        // noop
+    }
+
+    @Override
+    protected byte[] doWriteMessage() throws I2CPMessageException, IOException {
+        byte rv[] = new byte[0];
+        return rv;
+    }
+
+    public int getType() {
+        return MESSAGE_TYPE;
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        if ((object != null) && (object instanceof GetBandwidthLimitsMessage)) {
+            return true;
+        }
+        
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer buf = new StringBuffer();
+        buf.append("[GetBandwidthLimitsMessage]");
+        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 61d86505344d1c201d24597edb35e98d09c07c8c..04da546f782f2bece1287062f972b8930f27598f 100644
--- a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java
+++ b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java
@@ -94,6 +94,10 @@ public class I2CPMessageHandler {
             return new DestLookupMessage();
         case DestReplyMessage.MESSAGE_TYPE:
             return new DestReplyMessage();
+        case GetBandwidthLimitsMessage.MESSAGE_TYPE:
+            return new GetBandwidthLimitsMessage();
+        case BandwidthLimitsMessage.MESSAGE_TYPE:
+            return new BandwidthLimitsMessage();
         default:
             throw new I2CPMessageException("The type " + type + " is an unknown I2CP message");
         }
diff --git a/router/java/src/net/i2p/router/DummyTunnelManagerFacade.java b/router/java/src/net/i2p/router/DummyTunnelManagerFacade.java
index f7f204857a1765913ff87dae454d857910f09664..aec0390fd3d78b2401136aaa536c728a0c67fe84 100644
--- a/router/java/src/net/i2p/router/DummyTunnelManagerFacade.java
+++ b/router/java/src/net/i2p/router/DummyTunnelManagerFacade.java
@@ -10,6 +10,7 @@ package net.i2p.router;
 
 import java.io.IOException;
 import java.io.Writer;
+import java.util.Set;
 
 import net.i2p.data.Destination;
 import net.i2p.data.Hash;
@@ -44,6 +45,7 @@ class DummyTunnelManagerFacade implements TunnelManagerFacade {
     public void setInboundSettings(Hash client, TunnelPoolSettings settings) {}
     public void setOutboundSettings(Hash client, TunnelPoolSettings settings) {}
     public int getInboundBuildQueueSize() { return 0; }
+    public Set<Hash> selectPeersInTooManyTunnels() { return null; }
     
     public void renderStatusHTML(Writer out) throws IOException {}
     public void restart() {}
diff --git a/router/java/src/net/i2p/router/TunnelManagerFacade.java b/router/java/src/net/i2p/router/TunnelManagerFacade.java
index a6c1c961466ff8953a1202cb9a8038609927329c..da0482e6ffd4072eac1416e95e3397cf0222fd20 100644
--- a/router/java/src/net/i2p/router/TunnelManagerFacade.java
+++ b/router/java/src/net/i2p/router/TunnelManagerFacade.java
@@ -10,6 +10,7 @@ package net.i2p.router;
 
 import java.io.IOException;
 import java.io.Writer;
+import java.util.Set;
 
 import net.i2p.data.Destination;
 import net.i2p.data.Hash;
@@ -62,6 +63,9 @@ public interface TunnelManagerFacade extends Service {
     /** count how many inbound tunnel requests we have received but not yet processed */
     public int getInboundBuildQueueSize();
     
+    /** @return Set of peers that should not be allowed to be in another tunnel */
+    public Set<Hash> selectPeersInTooManyTunnels();
+
     /** 
      * the client connected (or updated their settings), so make sure we have
      * the tunnels for them, and whenever necessary, ask them to authorize 
diff --git a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
index 0dcc8187091d85010525d1070547637285cd9dbd..3b4b1a6bed38fa27ce4434f5691256e96f902ce5 100644
--- a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
+++ b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
@@ -11,10 +11,12 @@ package net.i2p.router.client;
 import java.util.Properties;
 
 import net.i2p.data.Payload;
+import net.i2p.data.i2cp.BandwidthLimitsMessage;
 import net.i2p.data.i2cp.CreateLeaseSetMessage;
 import net.i2p.data.i2cp.CreateSessionMessage;
 import net.i2p.data.i2cp.DestLookupMessage;
 import net.i2p.data.i2cp.DestroySessionMessage;
+import net.i2p.data.i2cp.GetBandwidthLimitsMessage;
 import net.i2p.data.i2cp.GetDateMessage;
 import net.i2p.data.i2cp.I2CPMessage;
 import net.i2p.data.i2cp.I2CPMessageException;
@@ -93,6 +95,9 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
             case ReconfigureSessionMessage.MESSAGE_TYPE:
                 handleReconfigureSession(reader, (ReconfigureSessionMessage)message);
                 break;
+            case GetBandwidthLimitsMessage.MESSAGE_TYPE:
+                handleGetBWLimits(reader, (GetBandwidthLimitsMessage)message);
+                break;
             default:
                 if (_log.shouldLog(Log.ERROR))
                     _log.error("Unhandled I2CP type received: " + message.getType());
@@ -274,6 +279,24 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
         }
     }
 
+    /**
+     * Divide router limit by 1.75 for overhead.
+     * This could someday give a different answer to each client.
+     * But it's not enforced anywhere.
+     */
+    private void handleGetBWLimits(I2CPMessageReader reader, GetBandwidthLimitsMessage message) {
+        if (_log.shouldLog(Log.INFO))
+            _log.info("Got BW Limits request");
+        int in = _context.bandwidthLimiter().getInboundKBytesPerSecond() * 4 / 7;
+        int out = _context.bandwidthLimiter().getOutboundKBytesPerSecond() * 4 / 7;
+        BandwidthLimitsMessage msg = new BandwidthLimitsMessage(in, out);
+        try {
+            _runner.doSend(msg);
+        } catch (I2CPMessageException ime) {
+            _log.error("Error writing out the session status message", ime);
+        }
+    }
+
     // this *should* be mod 65536, but UnsignedInteger is still b0rked.  FIXME
     private final static int MAX_SESSION_ID = 32767;
 
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 1e0247c02227d61269e076ed50f4665980bd530c..92e4545171cac325a29e91147e752ceffed3e72a 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java
@@ -176,6 +176,7 @@ public abstract class TunnelPeerSelector {
 
         Set 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)) {
             List caps = ctx.peerManager().getPeersByCapability(Router.CAPABILITY_UNREACHABLE);
diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPool.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPool.java
index 06a9b499913f2890f90f10dec628e047417d8f7d..699a8be9f1468e57b09e33c235d589ff35247333 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPool.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPool.java
@@ -28,7 +28,7 @@ public class TunnelPool {
     private RouterContext _context;
     private Log _log;
     private TunnelPoolSettings _settings;
-    private ArrayList _tunnels;
+    private ArrayList<TunnelInfo> _tunnels;
     private TunnelPeerSelector _peerSelector;
     private TunnelPoolManager _manager;
     private boolean _alive;
@@ -227,7 +227,7 @@ public class TunnelPool {
      *
      * @return list of TunnelInfo objects
      */
-    public List listTunnels() {
+    public List<TunnelInfo> listTunnels() {
         synchronized (_tunnels) {
             return new ArrayList(_tunnels);
         }
diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java
index 58637ce63a1c50d981f1b9862548e1572cf47c01..acbb9345db9795ff85d8751f30e7ef42eed18b70 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java
@@ -6,9 +6,12 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 import net.i2p.data.DataHelper;
 import net.i2p.data.Destination;
@@ -506,6 +509,7 @@ public class TunnelPoolManager implements TunnelManagerFacade {
         out.write("</table>\n");
         out.write("Inactive participating tunnels: " + inactive + "<br />\n");
         out.write("Lifetime bandwidth usage: " + DataHelper.formatSize(processed*1024) + "B<br />\n");
+        renderPeers(out);
     }
     
     class TunnelComparator implements Comparator {
@@ -579,6 +583,135 @@ public class TunnelPoolManager implements TunnelManagerFacade {
                   DataHelper.formatSize(processedOut*1024) + "B out<br />");
     }
     
+    private void renderPeers(Writer out) throws IOException {
+        // count up the peers in the local pools
+        HashCounter lc = new HashCounter();
+        int tunnelCount = countTunnelsPerPeer(lc);
+
+        // count up the peers in the participating tunnels
+        HashCounter pc = new HashCounter();
+        int partCount = countParticipatingPerPeer(pc);
+
+        Set<Hash> peers = new HashSet(lc.hashes());
+        peers.addAll(pc.hashes());
+        List<Hash> peerList = new ArrayList(peers);
+        Collections.sort(peerList, new HashComparator());
+
+        out.write("<h2><a name=\"peers\">Tunnel Counts By Peer</a>:</h2>\n");
+        out.write("<table border=\"1\"><tr><td><b>Peer</b></td><td><b>Expl. + Client</b></td><td><b>% of total</b></td><td><b>Part. from + to</b></td><td><b>% of total</b></td></tr>\n");
+        for (Hash h : peerList) {
+             out.write("<tr><td>");
+             out.write(netDbLink(h));
+             out.write("<td align=\"right\">" + lc.count(h));
+             out.write("<td align=\"right\">");
+             if (tunnelCount > 0)
+                 out.write("" + (lc.count(h) * 100 / tunnelCount));
+             else
+                 out.write('0');
+             out.write("<td align=\"right\">" + pc.count(h));
+             out.write("<td align=\"right\">");
+             if (partCount > 0)
+                 out.write("" + (pc.count(h) * 100 / partCount));
+             else
+                 out.write('0');
+             out.write('\n');
+        }
+        out.write("<tr><td>Tunnels<td align=\"right\">" + tunnelCount);
+        out.write("<td>&nbsp;<td align=\"right\">" + partCount);
+        out.write("<td>&nbsp;</table>\n");
+    }
+
+    /** @return total number of non-fallback expl. + client tunnels */
+    private int countTunnelsPerPeer(HashCounter lc) {
+        List<TunnelPool> pools = new ArrayList();
+        listPools(pools);
+        int tunnelCount = 0;
+        for (TunnelPool tp : pools) {
+            for (TunnelInfo info : tp.listTunnels()) {
+                if (info.getLength() > 1) {
+                    tunnelCount++;
+                    for (int j = 0; j < info.getLength(); j++) {
+                        Hash peer = info.getPeer(j);
+                        if (!_context.routerHash().equals(peer))
+                            lc.increment(peer);
+                    }
+                }
+            }
+        }
+        return tunnelCount;
+    }
+
+    private static final int DEFAULT_MAX_PCT_TUNNELS = 33;
+    /**
+     *  For reliability reasons, don't allow a peer in more than x% of
+     *  client and exploratory tunnels.
+     *
+     *  This also will prevent a single huge-capacity (or malicious) peer from
+     *  taking all the tunnels in the network (although it would be nice to limit
+     *  the % of total network tunnels to 10% or so, but that appears to be
+     *  too low to set as a default here... much lower than 33% will push client
+     *  tunnels out of the fast tier into high cap or beyond...)
+     *
+     *  Possible improvement - restrict based on count per IP, or IP block,
+     *  to slightly increase costs of collusion
+     *
+     *  @return Set of peers that should not be allowed in another tunnel
+     */
+    public Set<Hash> selectPeersInTooManyTunnels() {
+        HashCounter lc = new HashCounter();
+        int tunnelCount = countTunnelsPerPeer(lc);
+        Set<Hash> rv = new HashSet();
+        if (tunnelCount >= 4 && _context.router().getUptime() > 10*60*1000) {
+            int max = _context.getProperty("router.maxTunnelPercentage", DEFAULT_MAX_PCT_TUNNELS);
+            for (Hash h : lc.hashes()) {
+                 if (lc.count(h) > 0 && (lc.count(h) + 1) * 100 / (tunnelCount + 1) > max)
+                     rv.add(h);
+            }
+        }
+        return rv;
+    }
+
+    /** @return total number of part. tunnels */
+    private int countParticipatingPerPeer(HashCounter pc) {
+        List<HopConfig> participating = _context.tunnelDispatcher().listParticipatingTunnels();
+        for (HopConfig cfg : participating) {
+            Hash from = cfg.getReceiveFrom();
+            if (from != null)
+                pc.increment(from);
+            Hash to = cfg.getSendTo();
+            if (to != null)
+                pc.increment(to);
+        }
+        return participating.size();
+    }
+
+    class HashComparator implements Comparator {
+         public int compare(Object l, Object r) {
+             return ((Hash)l).toBase64().compareTo(((Hash)r).toBase64());
+        }
+    }
+
+    private static class HashCounter {
+        private ConcurrentHashMap<Hash, Integer> _map;
+        public HashCounter() {
+            _map = new ConcurrentHashMap();
+        }
+        public void increment(Hash h) {
+            Integer i = _map.putIfAbsent(h, Integer.valueOf(1));
+            if (i != null)
+                _map.put(h, Integer.valueOf(i.intValue() + 1));
+        }
+        public int count(Hash h) {
+            Integer i = _map.get(h);
+            if (i != null)
+                return i.intValue();
+            return 0;
+        }
+        public Set<Hash> hashes() {
+            return _map.keySet();
+        }
+    }
+
     private String getCapacity(Hash peer) {
         RouterInfo info = _context.netDb().lookupRouterInfoLocally(peer);
         if (info != null) {