diff --git a/router/java/src/net/i2p/router/MultiRouter.java b/router/java/src/net/i2p/router/MultiRouter.java
new file mode 100644
index 0000000000000000000000000000000000000000..4bd110ac1f3abdc15b035d3f2e12f686e56df84d
--- /dev/null
+++ b/router/java/src/net/i2p/router/MultiRouter.java
@@ -0,0 +1,116 @@
+package net.i2p.router;
+
+import java.io.IOException;
+import java.io.FileInputStream;
+import java.util.ArrayList;
+import java.util.Properties;
+import net.i2p.I2PAppContext;
+import net.i2p.util.Log;
+
+/**
+ * Fire up multiple routers in the same VM, all with their own RouterContext
+ * (and all that entails).  In addition, this creates a root I2PAppContext for
+ * any objects not booted through one of the RouterContexts.  Each of these 
+ * contexts are configured through a simple properties file (where the name=value
+ * contained in them are used for the context's getProperty(name)). <p />
+ *
+ * <b>Usage:</b><pre>
+ *  MultiRouter globalContextFile routerContextFile[ routerContextFile]*
+ * </pre>
+ *
+ * Each routerContext specified is used to boot up a single router.  It is HIGHLY
+ * recommended that those context files contain a few base env properties: <ul>
+ *  <li>loggerFilenameOverride=rN/logs/log-router-#.txt</li>
+ *  <li>router.configLocation=rN/router.config</li>
+ * </ul>
+ * (where "rN" is an instance number, such as r0 or r9).
+ * Additionally, two other properties might be useful:<ul>
+ *  <li>i2p.vmCommSystem=true</li>
+ *  <li>i2p.encryption=off</li>
+ * </ul>
+ * The first line tells the router to use an in-VM comm system for sending 
+ * messages back and forth between routers (see net.i2p.transport.VMCommSystem),
+ * and the second tells the router to stub out ElGamal, AES, and DSA code, reducing
+ * the CPU load (but obviously making the router incapable of talking to things 
+ * that need the encryption enabled).  To run a client app through a router that
+ * has i2p.encryption=off, you should also add that line to the client's JVM
+ * (for example, <code>java -Di2p.encryption=off -jar lib/i2ptunnel.jar</code>).<p />
+ *
+ * The multirouter waits until all of the routers are shut down (which none will
+ * do on their own, so as before, you'll want to kill the proc or ^C it).
+ */
+public class MultiRouter {
+    private static Log _log;
+    private static ArrayList _routers = new ArrayList(8);
+    private static I2PAppContext _defaultContext;
+    
+    public static void main(String args[]) {
+        if ( (args == null) || (args.length <= 1) ) {
+            usage();
+            return;
+        }
+        _defaultContext = new I2PAppContext(getEnv(args[0]));
+        _log = _defaultContext.logManager().getLog(MultiRouter.class);
+        try { Thread.sleep(5*1000); } catch (InterruptedException ie) {}
+        
+        Runtime.getRuntime().addShutdownHook(new Thread() {
+            public void run() {
+                try { Thread.sleep(15*1000); } catch (InterruptedException ie) {}
+                Runtime.getRuntime().halt(-1);
+            }
+        });
+        
+        for (int i = 1; i < args.length; i++) {
+            Router router = new Router(getEnv(args[i]));
+            router.setKillVMOnEnd(false);
+            _routers.add(router);
+            _log.info("Router " + i + " created from " + args[i]);
+            try { Thread.sleep(1*1000); } catch (InterruptedException ie) {}
+        }
+        
+        for (int i = 0; i < _routers.size(); i++) {
+            ((Router)_routers.get(i)).runRouter();
+            //try { Thread.sleep(10*1000); } catch (InterruptedException ie) {}
+        }
+        _log.info("All " + _routers.size() + " routers started up");
+        waitForCompletion();
+    }
+    
+    private static Properties getEnv(String filename) {
+        Properties props = new Properties();
+        try {
+            props.load(new FileInputStream(filename));
+            return props;
+        } catch (IOException ioe) {
+            ioe.printStackTrace();
+            return null;
+        }
+    }
+    
+    private static void waitForCompletion() {
+        while (true) {
+            int alive = 0;
+            for (int i = 0; i < _routers.size(); i++) {
+                Router r = (Router)_routers.get(i);
+                if (!r.isAlive()) {
+                    _log.info("Router " + i + " is dead");
+                } else {
+                    alive++;
+                }
+            }
+            if (alive > 0) {
+                try { Thread.sleep(30*1000); } catch (InterruptedException ie) {}
+            } else {
+                break;
+            }
+        }
+        _log.info("All routers shut down");
+    }
+    
+    private static void usage() {
+        System.err.println("Usage: MultiRouter globalContextFile routerContextFile[ routerContextFile]*");
+        System.err.println("       The context files contain key=value entries specifying properties");
+        System.err.println("       to load into the given context.  In addition, each routerContextFile");
+        System.err.println("       in turn is used to boot a router");
+    }
+}
\ No newline at end of file
diff --git a/router/java/src/net/i2p/router/MultiRouterBuilder.java b/router/java/src/net/i2p/router/MultiRouterBuilder.java
new file mode 100644
index 0000000000000000000000000000000000000000..da50393b13b5b93dc5d18f43c69e4fbab90b81ab
--- /dev/null
+++ b/router/java/src/net/i2p/router/MultiRouterBuilder.java
@@ -0,0 +1,204 @@
+package net.i2p.router;
+
+import java.io.IOException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStreamReader;
+import java.io.BufferedReader;
+
+/**
+ * Build a set of config files suitable for use by the multirouter as a 
+ * simulation, as well as a pair of scripts for running the simulation.  
+ * Usage: <pre>
+ *   MultiRouterBuilder [routerDir routerPortStart]*
+ * </pre>
+ * 
+ * Each router is configured with their own $routerDir/router.config file so
+ * that all of its data is stored under the $routerDir (profiles, keys, netDb,
+ * etc).  In addition, each router has the i2cp port set to $routerPortStart+1,
+ * the admin port set to $routerPortStart+2, and some commented out clientApp
+ * lines (configured with the SAM bridge at $routerPortStart+3 and an EepProxy at
+ * $routerPortStart+4).<p />
+ *
+ * It then builds a $routerDir/heartbeat.config containing the following lines:<ul>
+ *  <li>i2cpHost=localhost</li>
+ *  <li>i2cpPort=$routerPortStart+1</li>
+ *  <li>numHops=2</li>
+ *  <li>privateDestinationFile=$routerDir/heartbeat.keys</li>
+ *  <li>publicDestinationFile=$routerDir/heartbeat.txt</li>
+ * </ul>
+ *
+ * Then it goes on to create the $routerDir/routerEnv.txt:<ul>
+ *  <li>loggerFilenameOverride=$routerDir/logs/log-router-#.txt</li>
+ *  <li>router.configLocation=$routerDir/router.config</li>
+ *  <li>i2p.vmCommSystem=true</li>
+ *  <li>i2p.encryption=off</li>
+ * </ul>
+ *
+ * In addition, it creates a baseEnv.txt: <ul>
+ *  <li>loggerFilenameOverride=logs/log-base-#.txt</li>
+ * </ul>
+ *
+ * Finally, it creates the MultiRouter startup script to launch all of these
+ * routers, stored at runNetSim.bat / runNetSim.sh
+ *
+ */
+public class MultiRouterBuilder {
+    public static void main(String args[]) {
+        if (args.length <= 2) {
+            usage();
+            return;
+        }
+        for (int i = 0; i < args.length; i += 2) {
+            String dir = args[i];
+            try {
+                int basePortNum = Integer.parseInt(args[i+1]);
+                buildConfig(dir, basePortNum);
+            } catch (NumberFormatException nfe) {
+                nfe.printStackTrace();
+            }
+        }
+        buildBaseEnv();
+        buildStartupScript(args);
+    }
+    
+    private static void buildBaseEnv() {
+        File envFile = new File("baseEnv.txt");
+        try {
+            FileOutputStream fos = new FileOutputStream(envFile);
+            fos.write(("loggerFilenameOverride=logs/log-base-#.txt\n").getBytes());
+            fos.close();
+        } catch (IOException ioe) {
+            ioe.printStackTrace();
+        }
+        File f = new File("logs");
+        f.mkdirs();
+    }
+    
+    private static void buildStartupScript(String args[]) {
+        buildStartupScriptWin(args);
+        buildStartupScriptNix(args);
+    }
+    private static void buildStartupScriptWin(String args[]) {
+        StringBuffer buf = new StringBuffer(4096);
+        buf.append("@echo off\ntitle I2P Router Sim\n");
+        buf.append("echo After all of the routers have started up, you should cross seed them\n");
+        buf.append("echo Simply copy */netDb/routerInfo-* to all of the various */netDb/ directories\n");
+        buf.append("java -cp lib\\i2p.jar;lib\\router.jar;lib\\mstreaming.jar;");
+        buf.append("lib\\heartbeat.jar;lib\\i2ptunnel.jar;lib\\netmonitor.jar;");
+        buf.append("lib\\sam.jar ");
+        buf.append("-Djava.library.path=. ");
+        buf.append("-DloggerFilenameOverride=logs\\log-sim-#.txt ");
+        buf.append("net.i2p.router.MultiRouter baseEnv.txt ");
+        for (int i = 0; i < args.length; i += 2) 
+            buf.append(args[i]).append("\\routerEnv.txt ");
+        buf.append("\npause\n");
+        try {
+            FileOutputStream fos = new FileOutputStream("runNetSim.bat");
+            fos.write(buf.toString().getBytes());
+            fos.close();
+        } catch (IOException ioe) { ioe.printStackTrace(); }
+    }
+    
+    private static void buildStartupScriptNix(String args[]) {
+        StringBuffer buf = new StringBuffer(4096);
+        buf.append("#!/bin/sh\n");
+        buf.append("nohup java -cp lib/i2p.jar:lib/router.jar:lib/mstreaming.jar:");
+        buf.append("lib/heartbeat.jar:lib/i2ptunnel.jar:lib/netmonitor.jar:");
+        buf.append("lib/sam.jar ");
+        buf.append("-Djava.library.path=. ");
+        buf.append("-DloggerFilenameOverride=logs/log-sim-#.txt ");
+        buf.append("net.i2p.router.MultiRouter baseEnv.txt ");
+        for (int i = 1; i < args.length; i += 2) 
+            buf.append(args[i]).append("/routerEnv.txt ");
+        buf.append(" > sim.txt &\n");
+        buf.append("echo $! > sim.pid\n");
+        buf.append("echo \"After all of the routers have started up, you should cross seed them\"\n");
+        buf.append("echo \"Simply copy */netDb/routerInfo-* to all of the various */netDb/ directories\"\n");
+        try {
+            FileOutputStream fos = new FileOutputStream("runNetSim.sh");
+            fos.write(buf.toString().getBytes());
+            fos.close();
+        } catch (IOException ioe) { ioe.printStackTrace(); }
+    }
+    
+    private static void buildConfig(String dir, int basePort) {
+        File baseDir = new File(dir);
+        baseDir.mkdirs();
+        File cfgFile = new File(baseDir, "router.config");
+        StringBuffer buf = new StringBuffer(8*1024);
+        buf.append("i2np.bandwidth.inboundBytesPerMinute=-60\n");
+        buf.append("i2np.bandwidth.outboundBytesPerMinute=-60\n");
+        buf.append("router.publishPeerRankings=true\n");
+        buf.append("router.keepHistory=true\n");
+        buf.append("router.submitHistory=false\n");
+        buf.append("router.maxJobRunners=1\n");
+        buf.append("router.jobLagWarning=10000\n");
+        buf.append("router.jobLagFatal=30000\n");
+        buf.append("router.jobRunWarning=10000\n");
+        buf.append("router.jobRunFatal=30000\n");
+        buf.append("router.jobWarmupTime=600000\n");
+        buf.append("router.targetClients=2\n");
+        buf.append("tunnels.numInbound=2\n");
+        buf.append("tunnels.numOutbound=2\n");
+        buf.append("tunnels.depthInbound=2\n");
+        buf.append("tunnels.depthOutbound=2\n");
+        buf.append("tunnels.tunnelDuration=600000\n");
+        buf.append("router.maxWaitingJobs=30\n");
+        buf.append("router.profileDir=").append(baseDir.getPath()).append("/peerProfiles\n");
+        buf.append("router.historyFilename=").append(baseDir.getPath()).append("/messageHistory.txt\n");
+        buf.append("router.sessionKeys.location=").append(baseDir.getPath()).append("/sessionKeys.dat\n");
+        buf.append("router.info.location=").append(baseDir.getPath()).append("/router.info\n");
+        buf.append("router.keys.location=").append(baseDir.getPath()).append("/router.keys\n");
+        buf.append("router.networkDatabase.dbDir=").append(baseDir.getPath()).append("/netDb\n");
+        buf.append("router.tunnelPoolFile=").append(baseDir.getPath()).append("/tunnelPool.dat\n");
+        buf.append("router.keyBackupDir=").append(baseDir.getPath()).append("/keyBackup\n");
+        buf.append("i2np.tcp.port=").append(basePort).append('\n');
+        buf.append("i2cp.port=").append(basePort+1).append('\n');
+        buf.append("router.adminPort=").append(basePort+2).append('\n');
+        buf.append("#clientApp.0.main=net.i2p.sam.SAMBridge\n");
+        buf.append("#clientApp.0.name=SAM\n");
+        buf.append("#clientApp.0.args=localhost ").append(basePort+3).append(" i2cp.tcp.host=localhost i2cp.tcp.port=").append(basePort+1).append("\n");
+        buf.append("#clientApp.1.main=net.i2p.i2ptunnel.I2PTunnel\n");
+        buf.append("#clientApp.1.name=EepProxy\n");
+        buf.append("#clientApp.1.args=-nogui -e \"config localhost ").append(basePort+1).append("\" -e \"httpclient ").append(basePort+4).append("\"\n");
+        buf.append("#clientApp.2.main=net.i2p.heartbeat.Heartbeat\n");
+        buf.append("#clientApp.2.name=Heartbeat\n");
+        buf.append("#clientApp.2.args=").append(baseDir.getPath()).append("/heartbeat.config\n");
+        
+        try {
+            FileOutputStream fos = new FileOutputStream(cfgFile);
+            fos.write(buf.toString().getBytes());
+            fos.close();
+            
+            fos = new FileOutputStream(new File(baseDir, "heartbeat.config"));
+            StringBuffer tbuf = new StringBuffer(1024);
+            tbuf.append("i2cpHost=localhost\n");
+            tbuf.append("i2cpPort=").append(basePort+1).append('\n');
+            tbuf.append("numHops=2\n");
+            tbuf.append("privateDestinationFile=").append(baseDir.getPath()).append("/heartbeat.keys\n");
+            tbuf.append("publicDestinationFile=").append(baseDir.getPath()).append("/heartbeat.txt\n");
+            fos.write(tbuf.toString().getBytes());
+            fos.close();
+            
+            
+            File envFile = new File(baseDir, "routerEnv.txt");
+            fos = new FileOutputStream(envFile);
+            fos.write(("loggerFilenameOverride="+baseDir+ "/logs/log-router-#.txt\n").getBytes());
+            fos.write(("router.configLocation="+baseDir+"/router.config\n").getBytes());
+            fos.write(("i2p.vmCommSystem=true\n").getBytes());
+            fos.write(("i2p.encryption=off\n").getBytes());
+            fos.close();
+            
+            File f = new File(baseDir, "logs");
+            f.mkdirs();
+        } catch (IOException ioe) {
+            ioe.printStackTrace();
+        }
+    }
+    
+    private static void usage() {
+        System.err.println("Usage: MultiRouterBuilder [routerDir routerPortStart]*");
+    }
+}
\ No newline at end of file
diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java
index 8161a5d85058b1db2384397874ae01abd0de52ba..5c2dcfeff6e6359d261e8af0b0568366ae01f44c 100644
--- a/router/java/src/net/i2p/router/Router.java
+++ b/router/java/src/net/i2p/router/Router.java
@@ -62,6 +62,10 @@ public class Router {
     private long _started;
     private boolean _higherVersionSeen;
     private SessionKeyPersistenceHelper _sessionKeyPersistenceHelper;
+    private boolean _killVMOnEnd;
+    private boolean _isAlive;
+    private I2PThread.OOMEventListener _oomListener;
+    private ShutdownHook _shutdownHook;
     
     public final static String PROP_CONFIG_FILE = "router.configLocation";
     
@@ -73,21 +77,47 @@ public class Router {
     public final static String PROP_KEYS_FILENAME = "router.keys.location";
     public final static String PROP_KEYS_FILENAME_DEFAULT = "router.keys";
         
-    public Router() {
+    static {
         // grumble about sun's java caching DNS entries *forever*
         System.setProperty("sun.net.inetaddr.ttl", "0");
         System.setProperty("networkaddress.cache.ttl", "0");
         // (no need for keepalive)
         System.setProperty("http.keepAlive", "false");
+    }
+    
+    public Router() { this(null, null); }
+    public Router(Properties envProps) { this(null, envProps); }
+    public Router(String configFilename) { this(configFilename, null); }
+    public Router(String configFilename, Properties envProps) {
         _config = new Properties();
-        _context = new RouterContext(this);
-        _configFilename = _context.getProperty(PROP_CONFIG_FILE, "router.config");
+        _context = new RouterContext(this, envProps);
+        if (configFilename == null)
+            _configFilename = _context.getProperty(PROP_CONFIG_FILE, "router.config");
+        else
+            _configFilename = configFilename;
         _routerInfo = null;
         _higherVersionSeen = false;
         _log = _context.logManager().getLog(Router.class);
+        _log.info("New router created with config file " + _configFilename);
         _sessionKeyPersistenceHelper = new SessionKeyPersistenceHelper(_context);
+        _killVMOnEnd = true;
+        _oomListener = new I2PThread.OOMEventListener() { 
+            public void outOfMemory(OutOfMemoryError oom) { 
+                _log.log(Log.CRIT, "Thread ran out of memory", oom);
+                shutdown(); 
+            }
+        };
+        _shutdownHook = new ShutdownHook();
     }
     
+    /**
+     * Configure the router to kill the JVM when the router shuts down, as well
+     * as whether to explicitly halt the JVM during the hard fail process.
+     *
+     */
+    public void setKillVMOnEnd(boolean shouldDie) { _killVMOnEnd = shouldDie; }
+    public boolean getKillVMOnEnd() { return _killVMOnEnd; }
+    
     public String getConfigFilename() { return _configFilename; }
     public void setConfigFilename(String filename) { _configFilename = filename; }
     
@@ -115,15 +145,11 @@ public class Router {
     /** wall clock uptime */
     public long getUptime() { return _context.clock().now() - _context.clock().getOffset() - _started; }
     
-    private void runRouter() {
+    void runRouter() {
+        _isAlive = true;
         _started = _context.clock().now();
-        Runtime.getRuntime().addShutdownHook(new ShutdownHook());
-        I2PThread.setOOMEventListener(new I2PThread.OOMEventListener() { 
-            public void outOfMemory(OutOfMemoryError oom) { 
-                _log.log(Log.CRIT, "Thread ran out of memory", oom);
-                shutdown(); 
-            }
-        });
+        Runtime.getRuntime().addShutdownHook(_shutdownHook);
+        I2PThread.addOOMEventListener(_oomListener);
         setupHandlers();
         startupQueue();
         _context.jobQueue().addJob(new CoallesceStatsJob());
@@ -133,6 +159,8 @@ public class Router {
         _context.jobQueue().addJob(new StartupJob(_context));
     }
     
+    public boolean isAlive() { return _isAlive; }
+    
     /**
      * coallesce the stats framework every minute
      *
@@ -355,6 +383,8 @@ public class Router {
     }
     
     public void shutdown() {
+        _isAlive = false;
+        I2PThread.removeOOMEventListener(_oomListener);
         try { _context.jobQueue().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the job queue", t); }
         try { _context.statPublisher().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the stats manager", t); }
         try { _context.clientManager().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the client manager", t); }
@@ -366,8 +396,10 @@ public class Router {
         dumpStats();
         _log.log(Log.CRIT, "Shutdown complete", new Exception("Shutdown"));
         try { _context.logManager().shutdown(); } catch (Throwable t) { }
-        try { Thread.sleep(1000); } catch (InterruptedException ie) {}
-        Runtime.getRuntime().halt(-1);
+        if (_killVMOnEnd) {
+            try { Thread.sleep(1000); } catch (InterruptedException ie) {}
+            Runtime.getRuntime().halt(-1);
+        }
     }
     
     private void dumpStats() {
diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java
index 99e546dfc5ba6e4cff9b6dd814ee3a755d7fac1b..5e82b333cad36fa6f06040ae93b9ca19e13c90ba 100644
--- a/router/java/src/net/i2p/router/RouterContext.java
+++ b/router/java/src/net/i2p/router/RouterContext.java
@@ -4,6 +4,7 @@ import net.i2p.data.Hash;
 import net.i2p.router.client.ClientManagerFacadeImpl;
 import net.i2p.router.transport.OutboundMessageRegistry;
 import net.i2p.router.networkdb.kademlia.KademliaNetworkDatabaseFacade;
+import net.i2p.router.transport.VMCommSystem;
 import net.i2p.router.transport.CommSystemFacadeImpl;
 import net.i2p.router.transport.BandwidthLimiter;
 import net.i2p.router.transport.TrivialBandwidthLimiter;
@@ -68,7 +69,10 @@ public class RouterContext extends I2PAppContext {
         _messageRegistry = new OutboundMessageRegistry(this);
         _netDb = new KademliaNetworkDatabaseFacade(this);
         _keyManager = new KeyManager(this);
-        _commSystem = new CommSystemFacadeImpl(this);
+        if ("false".equals(getProperty("i2p.vmCommSystem", "false")))
+            _commSystem = new CommSystemFacadeImpl(this);
+        else
+            _commSystem = new VMCommSystem(this);
         _profileOrganizer = new ProfileOrganizer(this);
         _peerManagerFacade = new PeerManagerFacadeImpl(this);
         _profileManager = new ProfileManagerImpl(this);
@@ -184,4 +188,33 @@ public class RouterContext extends I2PAppContext {
     public Calculator speedCalculator() { return _speedCalc; } 
     /** how do we rank the reliability of profiles? */
     public Calculator reliabilityCalculator() { return _reliabilityCalc; }
+    
+    public String toString() {
+        StringBuffer buf = new StringBuffer(512);
+        buf.append("RouterContext: ").append(super.toString()).append('\n');
+        buf.append(_router).append('\n');
+        buf.append(_clientManagerFacade).append('\n');
+        buf.append(_clientMessagePool).append('\n');
+        buf.append(_jobQueue).append('\n');
+        buf.append(_inNetMessagePool).append('\n');
+        buf.append(_outNetMessagePool).append('\n');
+        buf.append(_messageHistory).append('\n');
+        buf.append(_messageRegistry).append('\n');
+        buf.append(_netDb).append('\n');
+        buf.append(_keyManager).append('\n');
+        buf.append(_commSystem).append('\n');
+        buf.append(_profileOrganizer).append('\n');
+        buf.append(_peerManagerFacade).append('\n');
+        buf.append(_profileManager).append('\n');
+        buf.append(_bandwidthLimiter).append('\n');
+        buf.append(_tunnelManager).append('\n');
+        buf.append(_statPublisher).append('\n');
+        buf.append(_shitlist).append('\n');
+        buf.append(_messageValidator).append('\n');
+        buf.append(_isFailingCalc).append('\n');
+        buf.append(_integrationCalc).append('\n');
+        buf.append(_speedCalc).append('\n');
+        buf.append(_reliabilityCalc).append('\n');
+        return buf.toString();
+    }
 }
\ No newline at end of file
diff --git a/router/java/src/net/i2p/router/transport/VMCommSystem.java b/router/java/src/net/i2p/router/transport/VMCommSystem.java
new file mode 100644
index 0000000000000000000000000000000000000000..3592b41339e2d4d69f30b7688a2567fd967b9e2e
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/VMCommSystem.java
@@ -0,0 +1,126 @@
+package net.i2p.router.transport;
+
+import net.i2p.data.Hash;
+import net.i2p.data.i2np.I2NPMessage;
+import net.i2p.data.i2np.I2NPMessageHandler;
+import net.i2p.router.RouterContext;
+import net.i2p.router.CommSystemFacade;
+import net.i2p.router.OutNetMessage;
+import net.i2p.router.InNetMessage;
+import net.i2p.router.InNetMessagePool;
+import net.i2p.router.JobImpl;
+import net.i2p.util.Log;
+
+import java.io.ByteArrayInputStream;
+
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Collections;
+
+/**
+ * Hacked up in-VM comm system for talking between contexts.  It doesn't even
+ * generate any routerAddresses, but instead tracks the peers through a singleton.
+ * Currently, the comm system doesn't even inject any lag, though it could (later).
+ * It does honor the standard transport stats though, but not the TCP specific ones.
+ *
+ */
+public class VMCommSystem extends CommSystemFacade {
+    private Log _log;
+    private RouterContext _context;
+    /**
+     * Mapping from Hash to VMCommSystem for all routers hooked together
+     */
+    private static Map _commSystemFacades = Collections.synchronizedMap(new HashMap(16));
+    
+    public VMCommSystem(RouterContext context) {
+        _context = context;
+        _log = context.logManager().getLog(VMCommSystem.class);
+        _context.statManager().createFrequencyStat("transport.sendMessageFailureFrequency", "How often do we fail to send messages?", "Transport", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l });
+        _context.statManager().createRateStat("transport.sendMessageSize", "How large are the messages sent?", "Transport", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
+        _context.statManager().createRateStat("transport.receiveMessageSize", "How large are the messages received?", "Transport", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
+        _context.statManager().createRateStat("transport.sendProcessingTime", "How long does it take from noticing that we want to send the message to having it completely sent (successfully or failed)?", "Transport", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l });
+    }
+    
+    /**
+     * The router wants us to send the given message to the peer.  Do so, or fire 
+     * off the failing job.
+     */
+    public void processMessage(OutNetMessage msg) {
+        Hash peer = msg.getTarget().getIdentity().getHash();
+        VMCommSystem peerSys = (VMCommSystem)_commSystemFacades.get(peer);
+
+        long now = _context.clock().now();
+        long sendTime = now - msg.getSendBegin();
+
+        boolean sendSuccessful = false;
+        
+        if (peerSys == null) {
+            _context.jobQueue().addJob(msg.getOnFailedSendJob());
+            _context.statManager().updateFrequency("transport.sendMessageFailureFrequency");
+            _context.profileManager().messageFailed(msg.getTarget().getIdentity().getHash(), "vm");
+        } else {
+            _context.jobQueue().addJob(msg.getOnSendJob());
+            _context.profileManager().messageSent(msg.getTarget().getIdentity().getHash(), "vm", sendTime, msg.getMessageSize());
+            _context.statManager().addRateData("transport.sendMessageSize", msg.getMessageSize(), sendTime);
+            peerSys.receive(msg.getMessage().toByteArray(), _context.routerHash());
+            //_context.jobQueue().addJob(new SendJob(peerSys, msg.getMessage(), _context));
+            sendSuccessful = true;
+        }
+        
+        if (true) {
+            I2NPMessage dmsg = msg.getMessage();
+            String type = dmsg.getClass().getName();
+            _context.messageHistory().sendMessage(type, dmsg.getUniqueId(), dmsg.getMessageExpiration(), msg.getTarget().getIdentity().getHash(), sendSuccessful);
+        }
+
+        _context.statManager().addRateData("transport.sendProcessingTime", msg.getLifetime(), msg.getLifetime());
+    }    
+    
+    private class ReceiveJob extends JobImpl {
+        private Hash _from;
+        private byte _msg[];
+        private RouterContext _ctx;
+        public ReceiveJob(Hash from, byte msg[], RouterContext us) {
+            super(us);
+            _ctx = us;
+            _from = from;
+            _msg = msg;
+            // bah, uberspeed!  
+            //getTiming().setStartAfter(us.clock().now() + 50);
+        }
+        public void runJob() {
+            I2NPMessageHandler handler = new I2NPMessageHandler(_ctx);
+            try {
+                I2NPMessage msg = handler.readMessage(new ByteArrayInputStream(_msg));
+                int size = _msg.length;
+                InNetMessage inMsg = new InNetMessage();
+                inMsg.setFromRouterHash(_from);
+                inMsg.setMessage(msg);
+                _ctx.profileManager().messageReceived(_from, "vm", 1, size);
+                _ctx.statManager().addRateData("transport.receiveMessageSize", size, 1);
+                _ctx.inNetMessagePool().add(inMsg);
+            } catch (Exception e) {
+                _log.error("wtf, error reading/formatting a VM message?", e);
+            }
+        }
+        public String getName() { return "Receive Message"; }
+    }
+    
+    /**
+     * We send messages between comms as bytes so that we strip any router-local
+     * info.  For example, a router tags the # attempts to send through a 
+     * leaseSet, what type of tunnel a tunnelId is bound to, etc.
+     *
+     */
+    public void receive(byte message[], Hash fromPeer) {
+        _context.jobQueue().addJob(new ReceiveJob(fromPeer, message, _context));
+    }
+    
+    public void shutdown() {
+        _commSystemFacades.remove(_context.routerHash());
+    }
+    
+    public void startup() {
+        _commSystemFacades.put(_context.routerHash(), this);
+    }
+}
\ No newline at end of file