diff --git a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
index 052a06e3b388d0484973448e1853c25dac89a4f7..94d26378f3a5a3f9e75026284796511113fe4282 100644
--- a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
+++ b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java
@@ -58,7 +58,7 @@ import net.i2p.util.SimpleTimer;
 class ClientConnectionRunner {
     protected final Log _log;
     protected final RouterContext _context;
-    private final ClientManager _manager;
+    protected final ClientManager _manager;
     /** socket for this particular peer connection */
     private final Socket _socket;
     /** output stream of the socket that I2CP messages bound to the client should be written to */
@@ -137,7 +137,7 @@ class ClientConnectionRunner {
             if (_dead || _reader != null)
                 throw new IllegalStateException();
             _reader = new I2CPMessageReader(new BufferedInputStream(_socket.getInputStream(), BUF_SIZE),
-                                            new ClientMessageEventListener(_context, this, true));
+                                            createListener());
             _writer = new ClientWriterRunner(_context, this);
             I2PThread t = new I2PThread(_writer);
             t.setName("I2CP Writer " + __id.incrementAndGet());
@@ -148,6 +148,14 @@ class ClientConnectionRunner {
             // TODO need a cleaner for unclaimed items in _messages, but we have no timestamps...
     }
     
+    /**
+     *  Allow override for testing
+     *  @since 0.9.8
+     */
+    protected I2CPMessageReader.I2CPMessageEventListener createListener() {
+        return new ClientMessageEventListener(_context, this, true);
+    }
+
     /**
      *  Die a horrible death. Cannot be restarted.
      */
@@ -460,8 +468,8 @@ class ClientConnectionRunner {
      * @param set LeaseSet with requested leases - this object must be updated to contain the 
      *            signed version (as well as any changed/added/removed Leases)
      * @param expirationTime ms to wait before failing
-     * @param onCreateJob Job to run after the LeaseSet is authorized
-     * @param onFailedJob Job to run after the timeout passes without receiving authorization
+     * @param onCreateJob Job to run after the LeaseSet is authorized, null OK
+     * @param onFailedJob Job to run after the timeout passes without receiving authorization, null OK
      */
     void requestLeaseSet(LeaseSet set, long expirationTime, Job onCreateJob, Job onFailedJob) {
         if (_dead) {
diff --git a/router/java/src/net/i2p/router/client/ClientManager.java b/router/java/src/net/i2p/router/client/ClientManager.java
index e82ef49d9ad9d38d5f241706e0071775931014ec..21cac726ad9b0c81a79100b576efbbb917196f22 100644
--- a/router/java/src/net/i2p/router/client/ClientManager.java
+++ b/router/java/src/net/i2p/router/client/ClientManager.java
@@ -10,6 +10,7 @@ package net.i2p.router.client;
 
 import java.io.IOException;
 import java.io.Writer;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -44,7 +45,7 @@ import net.i2p.util.Log;
  */
 class ClientManager {
     private final Log _log;
-    private ClientListenerRunner _listener;
+    protected ClientListenerRunner _listener;
     // Destination --> ClientConnectionRunner
     // Locked for adds/removes but not lookups
     private final Map<Destination, ClientConnectionRunner>  _runners;
@@ -53,8 +54,9 @@ class ClientManager {
     private final Map<Hash, ClientConnectionRunner>  _runnersByHash;
     // ClientConnectionRunner for clients w/out a Dest yet
     private final Set<ClientConnectionRunner> _pendingRunners;
-    private final RouterContext _ctx;
-    private volatile boolean _isStarted;
+    protected final RouterContext _ctx;
+    protected final int _port;
+    protected volatile boolean _isStarted;
 
     /** Disable external interface, allow internal clients only @since 0.8.3 */
     private static final String PROP_DISABLE_EXTERNAL = "i2cp.disableInterface";
@@ -65,6 +67,10 @@ class ClientManager {
 
     private static final long REQUEST_LEASESET_TIMEOUT = 60*1000;
 
+    /**
+     *  Does not start the listeners.
+     *  Caller must call start()
+     */
     public ClientManager(RouterContext context, int port) {
         _ctx = context;
         _log = context.logManager().getLog(ClientManager.class);
@@ -75,22 +81,27 @@ class ClientManager {
         _runners = new ConcurrentHashMap();
         _runnersByHash = new ConcurrentHashMap();
         _pendingRunners = new HashSet();
-        startListeners(port);
+        _port = port;
         // following are for RequestLeaseSetJob
         _ctx.statManager().createRateStat("client.requestLeaseSetSuccess", "How frequently the router requests successfully a new leaseSet?", "ClientMessages", new long[] { 60*60*1000 });
         _ctx.statManager().createRateStat("client.requestLeaseSetTimeout", "How frequently the router requests a new leaseSet but gets no reply?", "ClientMessages", new long[] { 60*60*1000 });
         _ctx.statManager().createRateStat("client.requestLeaseSetDropped", "How frequently the router requests a new leaseSet but the client drops?", "ClientMessages", new long[] { 60*60*1000 });
     }
 
+    /** @since 0.9.8 */
+    public synchronized void start() {
+        startListeners();
+    }
+
     /** Todo: Start a 3rd listener for IPV6? */
-    private void startListeners(int port) {
+    protected void startListeners() {
         if (!_ctx.getBooleanProperty(PROP_DISABLE_EXTERNAL)) {
             // there's no option to start both an SSL and non-SSL listener
             if (_ctx.getBooleanProperty(PROP_ENABLE_SSL))
-                _listener = new SSLClientListenerRunner(_ctx, this, port);
+                _listener = new SSLClientListenerRunner(_ctx, this, _port);
             else
-                _listener = new ClientListenerRunner(_ctx, this, port);
-            Thread t = new I2PThread(_listener, "ClientListener:" + port, true);
+                _listener = new ClientListenerRunner(_ctx, this, _port);
+            Thread t = new I2PThread(_listener, "ClientListener:" + _port, true);
             t.start();
         }
         _isStarted = true;
@@ -102,9 +113,7 @@ class ClientManager {
         // to let the old listener die
         try { Thread.sleep(2*1000); } catch (InterruptedException ie) {}
         
-        int port = _ctx.getProperty(ClientManagerFacadeImpl.PROP_CLIENT_PORT,
-                                    ClientManagerFacadeImpl.DEFAULT_PORT);
-        startListeners(port);
+        startListeners();
     }
     
     /**
@@ -404,12 +413,18 @@ class ClientManager {
         }
     }
     
+    /**
+     *  @return unmodifiable, not a copy
+     */
     Set<Destination> getRunnerDestinations() {
-        Set<Destination> dests = new HashSet();
-        dests.addAll(_runners.keySet());
-        return dests;
+        return Collections.unmodifiableSet(_runners.keySet());
     }
     
+    /**
+     *  Unused
+     *
+     *  @param dest null for all local destinations
+     */
     public void reportAbuse(Destination dest, String reason, int severity) {
         if (dest != null) {
             ClientConnectionRunner runner = getRunner(dest);
@@ -417,9 +432,7 @@ class ClientManager {
                 runner.reportAbuse(reason, severity);
             }
         } else {
-            Set dests = getRunnerDestinations();
-            for (Iterator iter = dests.iterator(); iter.hasNext(); ) {
-                Destination d = (Destination)iter.next();
+            for (Destination d : _runners.keySet()) {
                 reportAbuse(d, reason, severity);
             }
         }
diff --git a/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java b/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java
index a9133182ddcf6a685314246c0aab9430de6023af..edb92147a96324be0c0e93c71861791c564da817 100644
--- a/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java
+++ b/router/java/src/net/i2p/router/client/ClientManagerFacadeImpl.java
@@ -56,6 +56,7 @@ public class ClientManagerFacadeImpl extends ClientManagerFacade implements Inte
         _log.info("Starting up the client subsystem");
         int port = _context.getProperty(PROP_CLIENT_PORT, DEFAULT_PORT);
         _manager = new ClientManager(_context, port);
+        _manager.start();
     }    
     
     public synchronized void shutdown() {
@@ -82,12 +83,12 @@ public class ClientManagerFacadeImpl extends ClientManagerFacade implements Inte
     public boolean isAlive() { return _manager != null && _manager.isAlive(); }
 
     private static final long MAX_TIME_TO_REBUILD = 10*60*1000;
+
     @Override
     public boolean verifyClientLiveliness() {
         if (_manager == null) return true;
         boolean lively = true;
-        for (Iterator iter = _manager.getRunnerDestinations().iterator(); iter.hasNext(); ) {
-            Destination dest = (Destination)iter.next();
+        for (Destination dest : _manager.getRunnerDestinations()) {
             ClientConnectionRunner runner = _manager.getRunner(dest);
             if ( (runner == null) || (runner.getIsDead())) continue;
             LeaseSet ls = runner.getLeaseSet();
diff --git a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
index de71f26a7a1ad711e6692be83d564d885fc6d42f..4107de39bee0f385501bb0aef66c070b158ad02a 100644
--- a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
+++ b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
@@ -46,8 +46,8 @@ import net.i2p.util.RandomSource;
  */
 class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventListener {
     private final Log _log;
-    private final RouterContext _context;
-    private final ClientConnectionRunner _runner;
+    protected final RouterContext _context;
+    protected final ClientConnectionRunner _runner;
     private final boolean  _enforceAuth;
     
     private static final String PROP_AUTH = "i2cp.auth";
@@ -73,40 +73,40 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
             _log.debug("Message received: \n" + message);
         switch (message.getType()) {
             case GetDateMessage.MESSAGE_TYPE:
-                handleGetDate(reader, (GetDateMessage)message);
+                handleGetDate((GetDateMessage)message);
                 break;
             case SetDateMessage.MESSAGE_TYPE:
-                handleSetDate(reader, (SetDateMessage)message);
+                handleSetDate((SetDateMessage)message);
                 break;
             case CreateSessionMessage.MESSAGE_TYPE:
-                handleCreateSession(reader, (CreateSessionMessage)message);
+                handleCreateSession((CreateSessionMessage)message);
                 break;
             case SendMessageMessage.MESSAGE_TYPE:
-                handleSendMessage(reader, (SendMessageMessage)message);
+                handleSendMessage((SendMessageMessage)message);
                 break;
             case SendMessageExpiresMessage.MESSAGE_TYPE:
-                handleSendMessage(reader, (SendMessageExpiresMessage)message);
+                handleSendMessage((SendMessageExpiresMessage)message);
                 break;
             case ReceiveMessageBeginMessage.MESSAGE_TYPE:
-                handleReceiveBegin(reader, (ReceiveMessageBeginMessage)message);
+                handleReceiveBegin((ReceiveMessageBeginMessage)message);
                 break;
             case ReceiveMessageEndMessage.MESSAGE_TYPE:
-                handleReceiveEnd(reader, (ReceiveMessageEndMessage)message);
+                handleReceiveEnd((ReceiveMessageEndMessage)message);
                 break;
             case CreateLeaseSetMessage.MESSAGE_TYPE:
-                handleCreateLeaseSet(reader, (CreateLeaseSetMessage)message);
+                handleCreateLeaseSet((CreateLeaseSetMessage)message);
                 break;
             case DestroySessionMessage.MESSAGE_TYPE:
-                handleDestroySession(reader, (DestroySessionMessage)message);
+                handleDestroySession((DestroySessionMessage)message);
                 break;
             case DestLookupMessage.MESSAGE_TYPE:
-                handleDestLookup(reader, (DestLookupMessage)message);
+                handleDestLookup((DestLookupMessage)message);
                 break;
             case ReconfigureSessionMessage.MESSAGE_TYPE:
-                handleReconfigureSession(reader, (ReconfigureSessionMessage)message);
+                handleReconfigureSession((ReconfigureSessionMessage)message);
                 break;
             case GetBandwidthLimitsMessage.MESSAGE_TYPE:
-                handleGetBWLimits(reader, (GetBandwidthLimitsMessage)message);
+                handleGetBWLimits((GetBandwidthLimitsMessage)message);
                 break;
             default:
                 if (_log.shouldLog(Log.ERROR))
@@ -131,7 +131,7 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
         _runner.disconnected();
     }
     
-    private void handleGetDate(I2CPMessageReader reader, GetDateMessage message) {
+    private void handleGetDate(GetDateMessage message) {
         // sent by clients >= 0.8.7
         String clientVersion = message.getVersion();
         if (clientVersion != null)
@@ -148,7 +148,7 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
     /**
      *  As of 0.8.7, does nothing. Do not allow a client to set the router's clock.
      */
-    private void handleSetDate(I2CPMessageReader reader, SetDateMessage message) {
+    private void handleSetDate(SetDateMessage message) {
         //_context.clock().setNow(message.getDate().getTime());
     }
 	
@@ -160,7 +160,7 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
      * DisconnectMessage in return, and not wait around for our DisconnectMessage.
      * So keep it simple.
      */
-    private void handleCreateSession(I2CPMessageReader reader, CreateSessionMessage message) {
+    private void handleCreateSession(CreateSessionMessage message) {
         SessionConfig in = message.getSessionConfig();
         if (in.verifySignature()) {
             if (_log.shouldLog(Log.DEBUG))
@@ -209,17 +209,24 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
 
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("after sessionEstablished for " + message.getSessionConfig().getDestination().calculateHash().toBase64());
-
-        _context.jobQueue().addJob(new CreateSessionJob(_context, _runner));
+        startCreateSessionJob();
     }
     
+    /**
+     *  Override for testing
+     *  @since 0.9.8
+     *
+     */
+    protected void startCreateSessionJob() {
+        _context.jobQueue().addJob(new CreateSessionJob(_context, _runner));
+    }
     
     /**
      * Handle a SendMessageMessage: give it a message Id, have the ClientManager distribute
      * it, and send the client an ACCEPTED message
      *
      */
-    private void handleSendMessage(I2CPMessageReader reader, SendMessageMessage message) {
+    private void handleSendMessage(SendMessageMessage message) {
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("handleSendMessage called");
         long beforeDistribute = _context.clock().now();
@@ -236,7 +243,7 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
      * The client asked for a message, so we send it to them.  
      *
      */
-    private void handleReceiveBegin(I2CPMessageReader reader, ReceiveMessageBeginMessage message) {
+    private void handleReceiveBegin(ReceiveMessageBeginMessage message) {
         if (_runner.isDead()) return;
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("Handling recieve begin: id = " + message.getMessageId());
@@ -266,17 +273,18 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
      * pending queue, though it should.
      *
      */
-    private void handleReceiveEnd(I2CPMessageReader reader, ReceiveMessageEndMessage message) {
+    private void handleReceiveEnd(ReceiveMessageEndMessage message) {
         _runner.removePayload(new MessageId(message.getMessageId()));
     }
     
-    private void handleDestroySession(I2CPMessageReader reader, DestroySessionMessage message) {
+    private void handleDestroySession(DestroySessionMessage message) {
         if (_log.shouldLog(Log.INFO))
             _log.info("Destroying client session " + _runner.getSessionId());
         _runner.stopRunning();
     }
     
-    private void handleCreateLeaseSet(I2CPMessageReader reader, CreateLeaseSetMessage message) {	
+    /** override for testing */
+    protected void handleCreateLeaseSet(CreateLeaseSetMessage message) {	
         if ( (message.getLeaseSet() == null) || (message.getPrivateKey() == null) || (message.getSigningPrivateKey() == null) ) {
             if (_log.shouldLog(Log.ERROR))
                 _log.error("Null lease set granted: " + message);
@@ -293,7 +301,8 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
         _runner.leaseSetCreated(message.getLeaseSet());
     }
 
-    private void handleDestLookup(I2CPMessageReader reader, DestLookupMessage message) {
+    /** override for testing */
+    protected void handleDestLookup(DestLookupMessage message) {
         _context.jobQueue().addJob(new LookupDestJob(_context, _runner, message.getHash()));
     }
 
@@ -305,7 +314,7 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
      * Note that this does NOT update the few options handled in
      * ClientConnectionRunner.sessionEstablished(). Those can't be changed later.
      */
-    private void handleReconfigureSession(I2CPMessageReader reader, ReconfigureSessionMessage message) {
+    private void handleReconfigureSession(ReconfigureSessionMessage message) {
         if (_log.shouldLog(Log.INFO))
             _log.info("Updating options - old: " + _runner.getConfig() + " new: " + message.getSessionConfig());
         if (!message.getSessionConfig().getDestination().equals(_runner.getConfig().getDestination())) {
@@ -343,7 +352,7 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
      * This could someday give a different answer to each client.
      * But it's not enforced anywhere.
      */
-    private void handleGetBWLimits(I2CPMessageReader reader, GetBandwidthLimitsMessage message) {
+    protected void handleGetBWLimits(GetBandwidthLimitsMessage message) {
         if (_log.shouldLog(Log.INFO))
             _log.info("Got BW Limits request");
         int in = _context.bandwidthLimiter().getInboundKBytesPerSecond() * 4 / 7;
diff --git a/router/java/test/junit/net/i2p/router/client/LocalClientConnectionRunner.java b/router/java/test/junit/net/i2p/router/client/LocalClientConnectionRunner.java
new file mode 100644
index 0000000000000000000000000000000000000000..85dcbcab08e4d320858cd9a2693afa5664ecb54b
--- /dev/null
+++ b/router/java/test/junit/net/i2p/router/client/LocalClientConnectionRunner.java
@@ -0,0 +1,67 @@
+package net.i2p.router.client;
+
+import java.net.Socket;
+
+import net.i2p.data.Destination;
+import net.i2p.data.Hash;
+import net.i2p.data.Lease;
+import net.i2p.data.LeaseSet;
+import net.i2p.data.i2cp.I2CPMessageException;
+import net.i2p.data.i2cp.I2CPMessageReader;
+import net.i2p.data.i2cp.RequestVariableLeaseSetMessage;
+import net.i2p.router.Job;
+import net.i2p.router.RouterContext;
+
+/**
+ *  For testing
+ *
+ *  @since 0.9.8
+ */
+class LocalClientConnectionRunner extends ClientConnectionRunner {
+    
+    /**
+     * Create a new runner with the given queues
+     *
+     */
+    public LocalClientConnectionRunner(RouterContext context, ClientManager manager, Socket socket) {
+        super(context, manager, socket);
+    }
+    
+    /**
+     *  Custom listener
+     */
+    @Override
+    protected I2CPMessageReader.I2CPMessageEventListener createListener() {
+        return new LocalClientMessageEventListener(_context, this, true);
+    }
+    
+    /**
+     *  Just send the message directly,
+     *  don't instantiate a RequestLeaseSetJob
+     */
+    @Override
+    void requestLeaseSet(LeaseSet set, long expirationTime, Job onCreateJob, Job onFailedJob) {
+        RequestVariableLeaseSetMessage msg = new RequestVariableLeaseSetMessage();
+        msg.setSessionId(getSessionId());
+        for (int i = 0; i < set.getLeaseCount(); i++) {
+            Lease lease = set.getLease(i);
+            msg.addEndpoint(lease);
+        }
+        try {
+            doSend(msg);
+        } catch (I2CPMessageException ime) {
+            ime.printStackTrace();
+        }
+    }
+
+    /**
+     *  So LocalClientMessageEventListener can lookup other local dests
+     */
+    public Destination localLookup(Hash h) {
+        for (Destination d : _manager.getRunnerDestinations()) {
+            if (d.calculateHash().equals(h))
+                return d;
+        }
+        return null;
+    }
+}
diff --git a/router/java/test/junit/net/i2p/router/client/LocalClientListenerRunner.java b/router/java/test/junit/net/i2p/router/client/LocalClientListenerRunner.java
new file mode 100644
index 0000000000000000000000000000000000000000..81d54ba7be17ec722f54822fa825ffe41453688a
--- /dev/null
+++ b/router/java/test/junit/net/i2p/router/client/LocalClientListenerRunner.java
@@ -0,0 +1,21 @@
+package net.i2p.router.client;
+
+import net.i2p.router.RouterContext;
+
+/**
+ *  For testing
+ *
+ *  @since 0.9.8
+ */
+class LocalClientListenerRunner extends ClientListenerRunner {
+
+    public LocalClientListenerRunner(RouterContext context, ClientManager manager, int port) {
+        super(context, manager, port);
+    }
+
+    @Override
+    protected void runConnection(Socket socket) {
+        ClientConnectionRunner runner = new LocalClientConnectionRunner(_context, _manager, socket);
+        _manager.registerConnection(runner);
+    }
+}
diff --git a/router/java/test/junit/net/i2p/router/client/LocalClientManager.java b/router/java/test/junit/net/i2p/router/client/LocalClientManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..233e6bb2d582d414b3eb244f515770e9491bdeef
--- /dev/null
+++ b/router/java/test/junit/net/i2p/router/client/LocalClientManager.java
@@ -0,0 +1,77 @@
+package net.i2p.router.client;
+/*
+ * free (adj.): unencumbered; not under the control of others
+ * Written by jrandom in 2003 and released into the public domain 
+ * with no warranty of any kind, either expressed or implied.  
+ * It probably won't make your computer catch on fire, or eat 
+ * your children, but it might.  Use at your own risk.
+ *
+ */
+
+import net.i2p.data.Destination;
+import net.i2p.data.Payload;
+import net.i2p.data.i2cp.MessageId;
+import net.i2p.data.i2cp.MessageStatusMessage;
+import net.i2p.router.RouterContext;
+import net.i2p.util.I2PThread;
+
+/**
+ * For testing clients without a full router.
+ * A complete router-side I2CP implementation, without requiring a router
+ * or any RouterContext subsystems or threads.
+ * Clients may connect only to other local clients.
+ * Lookups and bw limit messages also supported.
+ *
+ * @since 0.9.8
+ */
+class LocalClientManager extends ClientManager {
+
+    /**
+     *  @param context stub, may be constructed with new RouterContext(null),
+     *                 no initAll() necessary
+     */
+    public LocalClientManager(RouterContext context, int port) {
+        super(context, port);
+    }
+
+    @Override
+    protected void startListeners() {
+        _listener = new LocalClientListenerRunner(_ctx, this, _port);
+        Thread t = new I2PThread(_listener, "ClientListener:" + _port, true);
+        t.start();
+        _isStarted = true;
+    }
+
+    /**
+     * Local only
+     * TODO: add simulated delay and random drops to test streaming.
+     *
+     * @param flags ignored for local
+     */
+    @Override
+    void distributeMessage(Destination fromDest, Destination toDest, Payload payload, MessageId msgId, long expiration, int flags) { 
+        // check if there is a runner for it
+        ClientConnectionRunner sender = getRunner(fromDest);
+        ClientConnectionRunner runner = getRunner(toDest);
+        if (runner != null) {
+            runner.receiveMessage(toDest, fromDest, payload);
+            if (sender != null)
+                sender.updateMessageDeliveryStatus(msgId, MessageStatusMessage.STATUS_SEND_SUCCESS_LOCAL);
+        } else {
+            // remote.  ignore.
+            System.out.println("Message " + msgId + " is targeting a REMOTE destination - DROPPED");
+            if (sender != null)
+                sender.updateMessageDeliveryStatus(msgId, MessageStatusMessage.STATUS_SEND_GUARANTEED_FAILURE);
+        }
+    }
+
+    public static void main(String args[]) {
+        RouterContext ctx = new RouterContext(null);
+        int port = ClientManagerFacadeImpl.DEFAULT_PORT;
+        ClientManager mgr = new LocalClientManager(ctx, port);
+        mgr.start();
+        System.out.println("Listening on port " + port);
+        try { Thread.sleep(5*60*1000); } catch (InterruptedException ie) {}
+        System.out.println("Done listening on port " + port);
+    }
+}
diff --git a/router/java/test/junit/net/i2p/router/client/LocalClientMessageEventListener.java b/router/java/test/junit/net/i2p/router/client/LocalClientMessageEventListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..611e0c24fb6d2717c56487e202fdd70db8ae40ef
--- /dev/null
+++ b/router/java/test/junit/net/i2p/router/client/LocalClientMessageEventListener.java
@@ -0,0 +1,94 @@
+package net.i2p.router.client;
+/*
+ * free (adj.): unencumbered; not under the control of others
+ * Written by jrandom in 2003 and released into the public domain 
+ * with no warranty of any kind, either expressed or implied.  
+ * It probably won't make your computer catch on fire, or eat 
+ * your children, but it might.  Use at your own risk.
+ *
+ */
+
+import java.util.Date;
+
+import net.i2p.data.Destination;
+import net.i2p.data.Hash;
+import net.i2p.data.Lease;
+import net.i2p.data.LeaseSet;
+import net.i2p.data.TunnelId;
+import net.i2p.data.i2cp.BandwidthLimitsMessage;
+import net.i2p.data.i2cp.CreateLeaseSetMessage;
+import net.i2p.data.i2cp.DestLookupMessage;
+import net.i2p.data.i2cp.DestReplyMessage;
+import net.i2p.data.i2cp.GetBandwidthLimitsMessage;
+import net.i2p.data.i2cp.I2CPMessageException;
+import net.i2p.router.RouterContext;
+
+/**
+ *  For testing
+ *
+ *  @since 0.9.8
+ */
+class LocalClientMessageEventListener extends ClientMessageEventListener {
+
+    public LocalClientMessageEventListener(RouterContext context, ClientConnectionRunner runner, boolean enforceAuth) {
+        super(context, runner, enforceAuth);
+    }
+
+    /**
+     *  Immediately send a fake leaseset
+     */
+    @Override
+    protected void startCreateSessionJob() {
+        long exp = _context.clock().now() + 10*60*1000;
+        LeaseSet ls = new LeaseSet();
+        Lease lease = new Lease();
+        lease.setGateway(Hash.FAKE_HASH);
+        TunnelId id = new TunnelId(1);
+        lease.setTunnelId(id);
+        Date date = new Date(exp);
+        lease.setEndDate(date);
+        ls.addLease(lease);
+        _runner.requestLeaseSet(ls, exp, null, null);
+    }
+
+    /**
+     *  Don't tell the netdb or key manager
+     */
+    @Override
+    protected void handleCreateLeaseSet(CreateLeaseSetMessage message) {	
+        _runner.leaseSetCreated(message.getLeaseSet());
+    }
+
+    /**
+     *  Look only in current local dests
+     */
+    @Override
+    protected void handleDestLookup(DestLookupMessage message) {
+        Hash h = message.getHash();
+        DestReplyMessage msg;
+        Destination d = ((LocalClientConnectionRunner)_runner).localLookup(h);
+        if (d != null)
+            msg = new DestReplyMessage(d);
+        else
+            msg = new DestReplyMessage(h);
+        try {
+            _runner.doSend(msg);
+        } catch (I2CPMessageException ime) {
+            ime.printStackTrace();
+        }
+    }
+
+    /**
+     *  Send dummy limits
+     */
+    @Override
+    protected void handleGetBWLimits(GetBandwidthLimitsMessage message) {
+        int limit = 1024*1024;
+        BandwidthLimitsMessage msg = new BandwidthLimitsMessage(limit, limit);
+        try {
+            _runner.doSend(msg);
+        } catch (I2CPMessageException ime) {
+            ime.printStackTrace();
+        }
+    }
+}