diff --git a/LICENSE.txt b/LICENSE.txt
index 324f532c612b68a041dabe09515f2cfdb5dd71cc..e937985488475f3aa8cfd8418bbb345594b736ab 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -113,6 +113,10 @@ Applications:
    See licenses/LICENSE-I2PTunnel.txt
    See licenses/LICENSE-GPLv2.txt
 
+   I2PTunnel UDP and Streamr:
+   By welterde.
+   See licenses/LICENSE-GPLv2.txt
+
    Jetty 5.1.12:
    Copyright 2000-2004 Mort Bay Consulting Pty. Ltd.
    See licenses/LICENSE-Apache1.1.txt
diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
index 7b62ace8435a92fa3930300ef709b3df5b199247..54367af1ac90658eceaaa1dd3a155b2bfef9234f 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
@@ -81,8 +81,7 @@ public class SnarkManager implements Snark.CompleteListener {
         I2PAppThread monitor = new I2PAppThread(new DirMonitor(), "Snark DirMonitor");
         monitor.setDaemon(true);
         monitor.start();
-        if (_context instanceof RouterContext)
-            ((RouterContext)_context).router().addShutdownTask(new SnarkManagerShutdown());
+        _context.addShutdownTask(new SnarkManagerShutdown());
     }
     
     /** hook to I2PSnarkUtil for the servlet */
@@ -539,7 +538,7 @@ public class SnarkManager implements Snark.CompleteListener {
         String announce = info.getAnnounce();
         // basic validation of url
         if ((!announce.startsWith("http://")) ||
-            (announce.indexOf(".i2p/") < 0))
+            (announce.indexOf(".i2p/") < 0)) // need to do better than this
             return "Non-i2p tracker in " + info.getName() + ", deleting it";
         List files = info.getFiles();
         if ( (files != null) && (files.size() > MAX_FILES_PER_TORRENT) ) {
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java
index b72ae18b31af7771c85cbd54d12c6ac92a9ae8b0..dc9dfd2fcccb6dec67173603a8b19e254d94e971 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java
@@ -62,6 +62,8 @@ import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Destination;
 import net.i2p.i2ptunnel.socks.I2PSOCKSTunnel;
+import net.i2p.i2ptunnel.streamr.StreamrConsumer;
+import net.i2p.i2ptunnel.streamr.StreamrProducer;
 import net.i2p.util.EventDispatcher;
 import net.i2p.util.EventDispatcherImpl;
 import net.i2p.util.Log;
@@ -234,6 +236,8 @@ public class I2PTunnel implements Logging, EventDispatcher {
             runServer(args, l);
         } else if ("httpserver".equals(cmdname)) {
             runHttpServer(args, l);
+        } else if ("ircserver".equals(cmdname)) {
+            runIrcServer(args, l);
         } else if ("textserver".equals(cmdname)) {
             runTextServer(args, l);
         } else if ("client".equals(cmdname)) {
@@ -246,6 +250,10 @@ public class I2PTunnel implements Logging, EventDispatcher {
             runSOCKSTunnel(args, l);
         } else if ("connectclient".equals(cmdname)) {
             runConnectClient(args, l);
+        } else if ("streamrclient".equals(cmdname)) {
+            runStreamrClient(args, l);
+        } else if ("streamrserver".equals(cmdname)) {
+            runStreamrServer(args, l);
         } else if ("config".equals(cmdname)) {
             runConfig(args, l);
         } else if ("listen_on".equals(cmdname)) {
@@ -383,6 +391,53 @@ public class I2PTunnel implements Logging, EventDispatcher {
         }
     }
 
+    /**
+     * Same args as runServer
+     * (we should stop duplicating all this code...)
+     */
+    public void runIrcServer(String args[], Logging l) {
+        if (args.length == 3) {
+            InetAddress serverHost = null;
+            int portNum = -1;
+            File privKeyFile = null;
+            try {
+                serverHost = InetAddress.getByName(args[0]);
+            } catch (UnknownHostException uhe) {
+                l.log("unknown host");
+                _log.error(getPrefix() + "Error resolving " + args[0], uhe);
+                notifyEvent("serverTaskId", Integer.valueOf(-1));
+                return;
+            }
+
+            try {
+                portNum = Integer.parseInt(args[1]);
+            } catch (NumberFormatException nfe) {
+                l.log("invalid port");
+                _log.error(getPrefix() + "Port specified is not valid: " + args[1], nfe);
+                notifyEvent("serverTaskId", Integer.valueOf(-1));
+                return;
+            }
+
+            privKeyFile = new File(args[2]);
+            if (!privKeyFile.canRead()) {
+                l.log("private key file does not exist");
+                _log.error(getPrefix() + "Private key file does not exist or is not readable: " + args[2]);
+                notifyEvent("serverTaskId", Integer.valueOf(-1));
+                return;
+            }
+            I2PTunnelServer serv = new I2PTunnelIRCServer(serverHost, portNum, privKeyFile, args[2], l, (EventDispatcher) this, this);
+            serv.setReadTimeout(readTimeout);
+            serv.startRunning();
+            addtask(serv);
+            notifyEvent("serverTaskId", Integer.valueOf(serv.getId()));
+            return;
+        } else {
+            l.log("server <host> <port> <privkeyfile>");
+            l.log("  creates a server that sends all incoming data\n" + "  of its destination to host:port.");
+            notifyEvent("serverTaskId", Integer.valueOf(-1));
+        }
+    }
+
     /**
      * Run the HTTP server pointing at the host and port specified using the private i2p
      * destination loaded from the specified file, replacing the HTTP headers
@@ -751,6 +806,82 @@ public class I2PTunnel implements Logging, EventDispatcher {
         }
     }
 
+    /**
+     * Streamr client
+     *
+     * @param args {targethost, targetport, destinationString}
+     * @param l logger to receive events and output
+     */
+    public void runStreamrClient(String args[], Logging l) {
+        if (args.length == 3) {
+            InetAddress host;
+            try {
+                host = InetAddress.getByName(args[0]);
+            } catch (UnknownHostException uhe) {
+                l.log("unknown host");
+                _log.error(getPrefix() + "Error resolving " + args[0], uhe);
+                notifyEvent("streamrtunnelTaskId", Integer.valueOf(-1));
+                return;
+            }
+
+            int port = -1;
+            try {
+                port = Integer.parseInt(args[1]);
+            } catch (NumberFormatException nfe) {
+                l.log("invalid port");
+                _log.error(getPrefix() + "Port specified is not valid: " + args[0], nfe);
+                notifyEvent("streamrtunnelTaskId", Integer.valueOf(-1));
+                return;
+            }
+
+            StreamrConsumer task = new StreamrConsumer(host, port, args[2], l, (EventDispatcher) this, this);
+            task.startRunning();
+            addtask(task);
+            notifyEvent("streamrtunnelTaskId", Integer.valueOf(task.getId()));
+        } else {
+            l.log("streamrclient <host> <port> <destination>");
+            l.log("  creates a tunnel that receives streaming data.");
+            notifyEvent("streamrtunnelTaskId", Integer.valueOf(-1));
+        }
+    }
+
+    /**
+     * Streamr server
+     *
+     * @param args {port, privkeyfile}
+     * @param l logger to receive events and output
+     */
+    public void runStreamrServer(String args[], Logging l) {
+        if (args.length == 2) {
+            int port = -1;
+            try {
+                port = Integer.parseInt(args[0]);
+            } catch (NumberFormatException nfe) {
+                l.log("invalid port");
+                _log.error(getPrefix() + "Port specified is not valid: " + args[0], nfe);
+                notifyEvent("streamrtunnelTaskId", Integer.valueOf(-1));
+                return;
+            }
+
+            File privKeyFile = new File(args[1]);
+            if (!privKeyFile.canRead()) {
+                l.log("private key file does not exist");
+                _log.error(getPrefix() + "Private key file does not exist or is not readable: " + args[3]);
+                notifyEvent("serverTaskId", Integer.valueOf(-1));
+                return;
+            }
+
+            StreamrProducer task = new StreamrProducer(port, privKeyFile, args[1], l, (EventDispatcher) this, this);
+            task.startRunning();
+            addtask(task);
+            notifyEvent("streamrtunnelTaskId", Integer.valueOf(task.getId()));
+        } else {
+            l.log("streamrserver <port> <privkeyfile>");
+            l.log("  creates a tunnel that sends streaming data.");
+            notifyEvent("streamrtunnelTaskId", Integer.valueOf(-1));
+        }
+    }
+
     /**
      * Specify the i2cp host and port 
      *
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java
index 658dd5e3270bba2c7a7d137b382bc4c7b6bd4f58..536844af9cbd022bb34fa44d7f8462be4bd030f1 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java
@@ -124,8 +124,17 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
                     _log.error("Error while closing the received i2p con", ex);
             }
         } catch (IOException ex) {
+            try {
+                socket.close();
+            } catch (IOException ioe) {}
             if (_log.shouldLog(Log.WARN))
                 _log.warn("Error while receiving the new HTTP request", ex);
+        } catch (OutOfMemoryError oom) {
+            try {
+                socket.close();
+            } catch (IOException ioe) {}
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("OOM in HTTP server", oom);
         }
 
         long afterHandle = getTunnel().getContext().clock().now();
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java
index e6708aa21d3a58eea255b4407434cfef2c64ba57..5b223b1a42655d604ca983787f28dbd9d367d071 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java
@@ -83,9 +83,9 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable
             i2ps = createI2PSocket(dest);
             i2ps.setReadTimeout(readTimeout);
             StringBuffer expectedPong = new StringBuffer();
-            Thread in = new I2PThread(new IrcInboundFilter(s,i2ps, expectedPong));
+            Thread in = new I2PThread(new IrcInboundFilter(s,i2ps, expectedPong), "IRC Client " + __clientId + " in");
             in.start();
-            Thread out = new I2PThread(new IrcOutboundFilter(s,i2ps, expectedPong));
+            Thread out = new I2PThread(new IrcOutboundFilter(s,i2ps, expectedPong), "IRC Client " + __clientId + " out");
             out.start();
         } catch (Exception ex) {
             if (_log.shouldLog(Log.ERROR))
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java
new file mode 100644
index 0000000000000000000000000000000000000000..aa95e526c809a166f44bd219064a271d413adc7d
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java
@@ -0,0 +1,184 @@
+package net.i2p.i2ptunnel;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.SocketException;
+import java.util.Properties;
+
+import net.i2p.I2PAppContext;
+import net.i2p.client.streaming.I2PSocket;
+import net.i2p.crypto.SHA256Generator;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Destination;
+import net.i2p.data.Hash;
+import net.i2p.util.EventDispatcher;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+/**
+ * Simple extension to the I2PTunnelServer that filters the registration
+ * sequence to pass the destination hash of the client through as the hostname,
+ * so an IRC Server may track users across nick changes.
+ *
+ * Of course, this requires the ircd actually use the hostname sent by
+ * the client rather than the IP. It is common for ircds to ignore the
+ * hostname in the USER message (unless it's coming from another server)
+ * since it is easily spoofed. So you have to fix or, if you are lucky,
+ * configure your ircd first. At least in unrealircd and ngircd this is
+ * not configurable.
+ *
+ * There are three options for mangling the desthash. Put the option in the
+ * "custom options" section of i2ptunnel.
+ *   - ircserver.cloakKey unset:          Cloak with a random value that is persistent for
+ *                                        the life of this tunnel. This is the default.
+ *   - ircserver.cloakKey=none:           Don't cloak. Users may be correlated with their
+ *                                        (probably) shared clients destination.
+ *                                        Of course if the ircd does cloaking than this is ok.
+ *   - ircserver.cloakKey=somepassphrase: Cloak with the hash of the passphrase. Use this to
+ *                                        have consistent mangling across restarts, or to
+ *                                        have multiple IRC servers cloak consistently to
+ *                                        be able to track users even when they switch servers.
+ *                                        Note: don't quote or put spaces in the passphrase,
+ *                                        the i2ptunnel gui can't handle it.
+ *
+ * There is no outbound filtering.
+ *
+ * @author zzz
+ */
+public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable {
+
+    private static final Log _log = new Log(I2PTunnelIRCServer.class);
+    private static final String PROP_CLOAK="ircserver.cloakKey";
+    private boolean _cloak;
+    private byte[] _cloakKey; // 32 bytes of stuff to scramble the dest with
+    
+    /**
+     * @throws IllegalArgumentException if the I2PTunnel does not contain
+     *                                  valid config to contact the router
+     */
+
+    public I2PTunnelIRCServer(InetAddress host, int port, File privkey, String privkeyname, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) {
+        super(host, port, privkey, privkeyname, l, notifyThis, tunnel);
+        initCloak(tunnel);
+    }
+
+    /** generate a random 32 bytes, or the hash of the passphrase */
+    private void initCloak(I2PTunnel tunnel) {
+        Properties opts = tunnel.getClientOptions();
+        String passphrase = opts.getProperty(PROP_CLOAK);
+        _cloak = passphrase == null || !"none".equals(passphrase);
+        if (_cloak) {
+            if (passphrase == null) {
+                _cloakKey = new byte[Hash.HASH_LENGTH];
+                tunnel.getContext().random().nextBytes(_cloakKey);
+            } else {
+                _cloakKey = SHA256Generator.getInstance().calculateHash(passphrase.trim().getBytes()).getData();
+            }
+        }
+    }
+    
+    protected void blockingHandle(I2PSocket socket) {
+        try {
+            // give them 15 seconds to send in the request
+            socket.setReadTimeout(15*1000);
+            InputStream in = socket.getInputStream();
+            String modifiedRegistration = filterRegistration(in, cloakDest(socket.getPeerDestination()));
+            socket.setReadTimeout(readTimeout);
+            Socket s = new Socket(remoteHost, remotePort);
+            new I2PTunnelRunner(s, socket, slock, null, modifiedRegistration.getBytes(), null);
+        } catch (SocketException ex) {
+            try {
+                socket.close();
+            } catch (IOException ioe) {
+                if (_log.shouldLog(Log.ERROR))
+                    _log.error("Error while closing the received i2p con", ex);
+            }
+        } catch (IOException ex) {
+            try {
+                socket.close();
+            } catch (IOException ioe) {}
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Error while receiving the new IRC Connection", ex);
+        } catch (OutOfMemoryError oom) {
+            try {
+                socket.close();
+            } catch (IOException ioe) {}
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("OOM in IRC server", oom);
+        }
+    }
+
+    /**
+     * (Optionally) append 32 bytes of crap to the destination then return
+     * the first few characters of the hash of the whole thing, + ".i2p".
+     * Or do we want the full hash if the ircd is going to use this for
+     * nickserv auto-login? Or even Base32 if it will be used in a
+     * case-insensitive manner?
+     *
+     */
+    String cloakDest(Destination d) {
+        Hash h;
+        if (_cloak) {
+            byte[] b = new byte[d.size() + _cloakKey.length];
+            System.arraycopy(b, 0, d.toByteArray(), 0, d.size());
+            System.arraycopy(b, d.size(), _cloakKey, 0, _cloakKey.length);
+            h = SHA256Generator.getInstance().calculateHash(b);
+        } else {
+            h = d.calculateHash();
+        }
+        return h.toBase64().substring(0, 8) + ".i2p";
+    }
+
+    /** keep reading until we see USER or SERVER */
+    private String filterRegistration(InputStream in, String newHostname) throws IOException {
+        StringBuffer buf = new StringBuffer(128);
+        int lineCount = 0;
+        
+        while (true) {
+            String s = DataHelper.readLine(in);
+            if (s == null)
+                throw new IOException("EOF reached before the end of the headers [" + buf.toString() + "]");
+            if (++lineCount > 10)
+                throw new IOException("Too many lines before USER or SERVER, giving up");
+            s = s.trim();
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Got line: " + s);
+
+            String field[]=s.split(" ",5);
+            String command;
+            int idx=0;
+        
+            if(field[0].charAt(0)==':')
+                idx++;
+
+            try { command = field[idx++]; }
+             catch (IndexOutOfBoundsException ioobe) // wtf, server sent borked command?
+            {
+               throw new IOException("Dropping defective message: index out of bounds while extracting command.");
+            }
+
+            if ("USER".equalsIgnoreCase(command)) {
+                if (field.length < idx + 4)
+                    throw new IOException("Too few parameters in USER message: " + s);
+                // USER zzz1 hostname localhost :zzz
+                //  =>
+                // USER zzz1 abcd1234.i2p localhost :zzz
+                // this whole class is for these two lines...
+                buf.append("USER ").append(field[idx]).append(' ').append(newHostname).append(".i2p ");
+                buf.append(field[idx+2]).append(' ').append(field[idx+3]).append("\r\n");
+                break;
+            }
+            buf.append(s).append("\r\n");
+            if ("SERVER".equalsIgnoreCase(command))
+                break;
+        }
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("All done, sending: " + buf.toString());
+        return buf.toString();
+    }
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java
index 3c9640ce5a5ed1a40607c9f140898aef826b8da1..9cb3762ac9319ac5d82214942408b183de6df9a8 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java
@@ -58,7 +58,7 @@ public class TunnelController implements Logging {
         setConfig(config, prefix);
         _messages = new ArrayList(4);
         _running = false;
-        if (createKey && ("server".equals(getType()) || "httpserver".equals(getType())) )
+        if (createKey && getType().endsWith("server"))
             createPrivateKey();
         _starting = getStartOnLoad();
     }
@@ -134,6 +134,8 @@ public class TunnelController implements Logging {
                 _log.warn("Cannot start the tunnel - no type specified");
             return;
         }
+        setI2CPOptions();
+        setSessionOptions();
         if ("httpclient".equals(type)) {
             startHttpClient();
         } else if("ircclient".equals(type)) {
@@ -144,19 +146,26 @@ public class TunnelController implements Logging {
             startConnectClient();
         } else if ("client".equals(type)) {
             startClient();
+        } else if ("streamrclient".equals(type)) {
+            startStreamrClient();
         } else if ("server".equals(type)) {
             startServer();
         } else if ("httpserver".equals(type)) {
             startHttpServer();
+        } else if ("ircserver".equals(type)) {
+            startIrcServer();
+        } else if ("streamrserver".equals(type)) {
+            startStreamrServer();
         } else {
             if (_log.shouldLog(Log.ERROR))
                 _log.error("Cannot start tunnel - unknown type [" + type + "]");
+            return;
         }
+        acquire();
+        _running = true;
     }
     
     private void startHttpClient() {
-        setI2CPOptions();
-        setSessionOptions();
         setListenOn();
         String listenPort = getListenPort();
         String proxyList = getProxyList();
@@ -165,13 +174,9 @@ public class TunnelController implements Logging {
             _tunnel.runHttpClient(new String[] { listenPort, sharedClient }, this);
         else
             _tunnel.runHttpClient(new String[] { listenPort, sharedClient, proxyList }, this);
-        acquire();
-        _running = true;
     }
     
     private void startConnectClient() {
-        setI2CPOptions();
-        setSessionOptions();
         setListenOn();
         String listenPort = getListenPort();
         String proxyList = getProxyList();
@@ -180,31 +185,46 @@ public class TunnelController implements Logging {
             _tunnel.runConnectClient(new String[] { listenPort, sharedClient }, this);
         else
             _tunnel.runConnectClient(new String[] { listenPort, sharedClient, proxyList }, this);
-        acquire();
-        _running = true;
     }
     
     private void startIrcClient() {
-        setI2CPOptions();
-        setSessionOptions();
         setListenOn();
         String listenPort = getListenPort();
         String dest = getTargetDestination();
         String sharedClient = getSharedClient();
         _tunnel.runIrcClient(new String[] { listenPort, dest, sharedClient }, this);
-        acquire();
-        _running = true;
     }
     
     private void startSocksClient() {
-        setI2CPOptions();
-        setSessionOptions();
         setListenOn();
         String listenPort = getListenPort();
         String sharedClient = getSharedClient();
         _tunnel.runSOCKSTunnel(new String[] { listenPort, sharedClient }, this);
-        acquire();
-        _running = true;
+    }
+    
+    /*
+     *  Streamr client is a UDP server, use the listenPort field for targetPort
+     *  and the listenOnInterface field for the targetHost
+     */
+    private void startStreamrClient() {
+        String targetHost = getListenOnInterface();
+        String targetPort = getListenPort();
+        String dest = getTargetDestination();
+        _tunnel.runStreamrClient(new String[] { targetHost, targetPort, dest }, this);
+    }
+    
+    /**
+     *  Streamr server is a UDP client, use the targetPort field for listenPort
+     *  and the targetHost field for the listenOnInterface
+     */
+    private void startStreamrServer() {
+        String listenOn = getTargetHost();
+        if ( (listenOn != null) && (listenOn.length() > 0) ) {
+            _tunnel.runListenOn(new String[] { listenOn }, this);
+        }
+        String listenPort = getTargetPort();
+        String privKeyFile = getPrivKeyFile(); 
+        _tunnel.runStreamrServer(new String[] { listenPort, privKeyFile }, this);
     }
     
     /** 
@@ -240,38 +260,33 @@ public class TunnelController implements Logging {
     }
     
     private void startClient() {
-        setI2CPOptions();
-        setSessionOptions();
         setListenOn();
         String listenPort = getListenPort(); 
         String dest = getTargetDestination();
         String sharedClient = getSharedClient();
         _tunnel.runClient(new String[] { listenPort, dest, sharedClient }, this);
-        acquire();
-        _running = true;
     }
 
     private void startServer() {
-        setI2CPOptions();
-        setSessionOptions();
         String targetHost = getTargetHost(); 
         String targetPort = getTargetPort(); 
         String privKeyFile = getPrivKeyFile(); 
         _tunnel.runServer(new String[] { targetHost, targetPort, privKeyFile }, this);
-        acquire();
-        _running = true;
     }
     
     private void startHttpServer() {
-        setI2CPOptions();
-        setSessionOptions();
         String targetHost = getTargetHost(); 
         String targetPort = getTargetPort(); 
         String spoofedHost = getSpoofedHost(); 
         String privKeyFile = getPrivKeyFile(); 
         _tunnel.runHttpServer(new String[] { targetHost, targetPort, spoofedHost, privKeyFile }, this);
-        acquire();
-        _running = true;
+    }
+    
+    private void startIrcServer() {
+        String targetHost = getTargetHost(); 
+        String targetPort = getTargetPort(); 
+        String privKeyFile = getPrivKeyFile(); 
+        _tunnel.runIrcServer(new String[] { targetHost, targetPort, privKeyFile }, this);
     }
     
     private void setListenOn() {
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/MultiSink.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/MultiSink.java
new file mode 100644
index 0000000000000000000000000000000000000000..3c63758c138da9b08280a6a7a0cdb69412aceaef
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/MultiSink.java
@@ -0,0 +1,35 @@
+package net.i2p.i2ptunnel.socks;
+
+import java.util.Map;
+
+import net.i2p.data.Destination;
+import net.i2p.i2ptunnel.udp.*;
+import net.i2p.util.Log;
+
+/**
+ * Sends to one of many Sinks
+ * @author zzz modded from streamr/MultiSource
+ */
+public class MultiSink implements Source, Sink {
+    private static final Log _log = new Log(MultiSink.class);
+
+    public MultiSink(Map cache) {
+        this.cache = cache;
+    }
+    
+    /** Don't use this - put sinks in the cache */
+    public void setSink(Sink sink) {}
+
+    public void start() {}
+
+    public void send(Destination from, byte[] data) {
+        Sink s = this.cache.get(from);
+        if (s == null) {
+            _log.error("No where to go for " + from.calculateHash().toBase64().substring(0, 6));
+            return;
+        }
+        s.send(from, data);
+    }
+    
+    private Map<Destination, Sink> cache;
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/ReplyTracker.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/ReplyTracker.java
new file mode 100644
index 0000000000000000000000000000000000000000..f6a124c9518e32aa18533e2d4fcd80df18d4bb52
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/ReplyTracker.java
@@ -0,0 +1,36 @@
+package net.i2p.i2ptunnel.socks;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.Map;
+
+import net.i2p.data.Destination;
+import net.i2p.i2ptunnel.udp.*;
+import net.i2p.util.Log;
+
+/**
+ * Track who the reply goes to
+ * @author zzz
+ */
+public class ReplyTracker implements Source, Sink {
+    private static final Log _log = new Log(MultiSink.class);
+
+    public ReplyTracker(Sink reply, Map cache) {
+        this.reply = reply;
+        this.cache = cache;
+    }
+    
+    public void setSink(Sink sink) {
+        this.sink = sink;
+    }
+
+    public void start() {}
+
+    public void send(Destination to, byte[] data) {
+        this.cache.put(to, this.reply);
+        this.sink.send(to, data);
+    }
+    
+    private Sink reply;
+    private Map<Destination, Sink> cache;
+    private Sink sink;
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS4aServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS4aServer.java
new file mode 100644
index 0000000000000000000000000000000000000000..2745cb0fa9f7dcf73bc9e60e350f2cd18be14e84
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS4aServer.java
@@ -0,0 +1,283 @@
+/* I2PSOCKSTunnel is released under the terms of the GNU GPL,
+ * with an additional exception.  For further details, see the
+ * licensing terms in I2PTunnel.java.
+ *
+ * Copyright (c) 2004 by human
+ */
+package net.i2p.i2ptunnel.socks;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.SocketException;
+import java.util.List;
+
+import net.i2p.I2PAppContext;
+import net.i2p.I2PException;
+import net.i2p.client.streaming.I2PSocket;
+import net.i2p.data.DataFormatException;
+import net.i2p.i2ptunnel.I2PTunnel;
+import net.i2p.util.HexDump;
+import net.i2p.util.Log;
+
+/*
+ * Class that manages SOCKS 4/4a connections, and forwards them to
+ * destination hosts or (eventually) some outproxy.
+ *
+ * @author zzz modded from SOCKS5Server
+ */
+public class SOCKS4aServer extends SOCKSServer {
+    private static final Log _log = new Log(SOCKS4aServer.class);
+
+    private Socket clientSock = null;
+    private boolean setupCompleted = false;
+
+    /**
+     * Create a SOCKS4a server that communicates with the client using
+     * the specified socket.  This method should not be invoked
+     * directly: new SOCKS4aServer objects should be created by using
+     * SOCKSServerFactory.createSOCSKServer().  It is assumed that the
+     * SOCKS VER field has been stripped from the input stream of the
+     * client socket.
+     *
+     * @param clientSock client socket
+     */
+    public SOCKS4aServer(Socket clientSock) {
+        this.clientSock = clientSock;
+    }
+
+    public Socket getClientSocket() throws SOCKSException {
+        setupServer();
+
+        return clientSock;
+    }
+
+    protected void setupServer() throws SOCKSException {
+        if (setupCompleted) { return; }
+
+        DataInputStream in;
+        DataOutputStream out;
+        try {
+            in = new DataInputStream(clientSock.getInputStream());
+            out = new DataOutputStream(clientSock.getOutputStream());
+
+            manageRequest(in, out);
+        } catch (IOException e) {
+            throw new SOCKSException("Connection error (" + e.getMessage() + ")");
+        }
+
+        setupCompleted = true;
+    }
+
+    /**
+     * SOCKS4a request management.  This method assumes that all the
+     * stuff preceding or enveloping the actual request
+     * has been stripped out of the input/output streams.
+     */
+    private void manageRequest(DataInputStream in, DataOutputStream out) throws IOException, SOCKSException {
+
+        int command = in.readByte() & 0xff;
+        switch (command) {
+        case Command.CONNECT:
+            break;
+        case Command.BIND:
+            _log.debug("BIND command is not supported!");
+            sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
+            throw new SOCKSException("BIND command not supported");
+        default:
+            _log.debug("unknown command in request (" + Integer.toHexString(command) + ")");
+            sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
+            throw new SOCKSException("Invalid command in request");
+        }
+
+        connPort = in.readUnsignedShort();
+        if (connPort == 0) {
+            _log.debug("trying to connect to TCP port 0?  Dropping!");
+            sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
+            throw new SOCKSException("Invalid port number in request");
+        }
+
+        connHostName = new String("");
+        boolean alreadyWarned = false;
+        for (int i = 0; i < 4; ++i) {
+            int octet = in.readByte() & 0xff;
+            connHostName += Integer.toString(octet);
+            if (i != 3) {
+                connHostName += ".";
+                if (octet != 0 && !alreadyWarned) {
+                    _log.warn("IPV4 address type in request: " + connHostName + ". Is your client secure?");
+                    alreadyWarned = true;
+                }
+            }
+        }
+
+        // discard user name
+        readString(in);
+
+        // SOCKS 4a
+        if (connHostName.startsWith("0.0.0.") && !connHostName.equals("0.0.0.0"))
+            connHostName = readString(in);
+    }
+
+    private String readString(DataInputStream in) throws IOException {
+        StringBuffer sb = new StringBuffer(16);
+        char c;
+        while ((c = (char) (in.readByte() & 0xff)) != 0)
+            sb.append(c);
+        return sb.toString();
+    }
+
+    protected void confirmConnection() throws SOCKSException {
+        DataInputStream in;
+        DataOutputStream out;
+        try {
+            out = new DataOutputStream(clientSock.getOutputStream());
+
+            sendRequestReply(Reply.SUCCEEDED, InetAddress.getByName("127.0.0.1"), 1, out);
+        } catch (IOException e) {
+            throw new SOCKSException("Connection error (" + e.getMessage() + ")");
+        }
+    }
+
+    /**
+     * Send the specified reply to a request of the client.  Either
+     * one of inetAddr or domainName can be null, depending on
+     * addressType.
+     */
+    private void sendRequestReply(int replyCode, InetAddress inetAddr,
+                                  int bindPort, DataOutputStream out) throws IOException {
+        ByteArrayOutputStream reps = new ByteArrayOutputStream();
+        DataOutputStream dreps = new DataOutputStream(reps);
+
+        // Reserved byte, should be 0x00
+        dreps.write(0x00);
+        dreps.write(replyCode);
+        dreps.writeShort(bindPort);
+        dreps.write(inetAddr.getAddress());
+
+        byte[] reply = reps.toByteArray();
+
+        if (_log.shouldLog(Log.DEBUG)) {
+            _log.debug("Sending request reply:\n" + HexDump.dump(reply));
+        }
+
+        out.write(reply);
+    }
+
+    /**
+     * Get an I2PSocket that can be used to send/receive 8-bit clean data
+     * to/from the destination of the SOCKS connection.
+     *
+     * @return an I2PSocket connected with the destination
+     */
+    public I2PSocket getDestinationI2PSocket(I2PSOCKSTunnel t) throws SOCKSException {
+        setupServer();
+
+        if (connHostName == null) {
+            _log.error("BUG: destination host name has not been initialized!");
+            throw new SOCKSException("BUG! See the logs!");
+        }
+        if (connPort == 0) {
+            _log.error("BUG: destination port has not been initialized!");
+            throw new SOCKSException("BUG! See the logs!");
+        }
+
+        DataOutputStream out; // for errors
+        try {
+            out = new DataOutputStream(clientSock.getOutputStream());
+        } catch (IOException e) {
+            throw new SOCKSException("Connection error (" + e.getMessage() + ")");
+        }
+
+        // FIXME: here we should read our config file, select an
+        // outproxy, and instantiate the proper socket class that
+        // handles the outproxy itself (SOCKS4a, SOCKS4a, HTTP CONNECT...).
+        I2PSocket destSock;
+
+        try {
+            if (connHostName.toLowerCase().endsWith(".i2p")) {
+                _log.debug("connecting to " + connHostName + "...");
+                // Let's not due a new Dest for every request, huh?
+                //I2PSocketManager sm = I2PSocketManagerFactory.createManager();
+                //destSock = sm.connect(I2PTunnel.destFromName(connHostName), null);
+                destSock = t.createI2PSocket(I2PTunnel.destFromName(connHostName));
+            } else if ("localhost".equals(connHostName) || "127.0.0.1".equals(connHostName)) {
+                String err = "No localhost accesses allowed through the Socks Proxy";
+                _log.error(err);
+                try {
+                    sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
+                } catch (IOException ioe) {}
+                throw new SOCKSException(err);
+            } else if (connPort == 80) {
+                // rewrite GET line to include hostname??? or add Host: line???
+                // or forward to local eepProxy (but that's a Socket not an I2PSocket)
+                // use eepProxy configured outproxies?
+                String err = "No handler for HTTP outproxy implemented - to: " + connHostName;
+                _log.error(err);
+                try {
+                    sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
+                } catch (IOException ioe) {}
+                throw new SOCKSException(err);
+            } else {
+                List<String> proxies = t.getProxies(connPort);
+                if (proxies == null || proxies.size() <= 0) {
+                    String err = "No outproxy configured for port " + connPort + " and no default configured either";
+                    _log.error(err);
+                    try {
+                        sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
+                    } catch (IOException ioe) {}
+                    throw new SOCKSException(err);
+                }
+                int p = I2PAppContext.getGlobalContext().random().nextInt(proxies.size());
+                String proxy = proxies.get(p);
+                _log.debug("connecting to port " + connPort + " proxy " + proxy + " for " + connHostName + "...");
+                // this isn't going to work, these need to be socks outproxies so we need
+                // to do a socks session to them?
+                destSock = t.createI2PSocket(I2PTunnel.destFromName(proxy));
+            }
+            confirmConnection();
+            _log.debug("connection confirmed - exchanging data...");
+        } catch (DataFormatException e) {
+            try {
+                sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
+            } catch (IOException ioe) {}
+            throw new SOCKSException("Error in destination format");
+        } catch (SocketException e) {
+            try {
+                sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
+            } catch (IOException ioe) {}
+            throw new SOCKSException("Error connecting ("
+                                     + e.getMessage() + ")");
+        } catch (IOException e) {
+            try {
+                sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
+            } catch (IOException ioe) {}
+            throw new SOCKSException("Error connecting ("
+                                     + e.getMessage() + ")");
+        } catch (I2PException e) {
+            try {
+                sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
+            } catch (IOException ioe) {}
+            throw new SOCKSException("Error connecting ("
+                                     + e.getMessage() + ")");
+        }
+
+        return destSock;
+    }
+
+    /*
+     * Some namespaces to enclose SOCKS protocol codes
+     */
+    private static class Command {
+        private static final int CONNECT = 0x01;
+        private static final int BIND = 0x02;
+    }
+
+    private static class Reply {
+        private static final int SUCCEEDED = 0x5a;
+        private static final int CONNECTION_REFUSED = 0x5b;
+    }
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java
index 38c50f2661fdfa57ee0b3eb519c88a1a7abcb445..5e52926074ff9e1b1f268d30ac62cb33d9169fa8 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java
@@ -13,12 +13,15 @@ import java.io.IOException;
 import java.net.InetAddress;
 import java.net.Socket;
 import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
 import java.util.List;
 
 import net.i2p.I2PAppContext;
 import net.i2p.I2PException;
 import net.i2p.client.streaming.I2PSocket;
 import net.i2p.data.DataFormatException;
+import net.i2p.data.Destination;
 import net.i2p.i2ptunnel.I2PTunnel;
 import net.i2p.util.HexDump;
 import net.i2p.util.Log;
@@ -67,7 +70,8 @@ public class SOCKS5Server extends SOCKSServer {
             out = new DataOutputStream(clientSock.getOutputStream());
 
             init(in, out);
-            manageRequest(in, out);
+            if (manageRequest(in, out) == Command.UDP_ASSOCIATE)
+                handleUDP(in, out);
         } catch (IOException e) {
             throw new SOCKSException("Connection error (" + e.getMessage() + ")");
         }
@@ -111,7 +115,7 @@ public class SOCKS5Server extends SOCKSServer {
      * initialization, integrity/confidentiality encapsulations, etc)
      * has been stripped out of the input/output streams.
      */
-    private void manageRequest(DataInputStream in, DataOutputStream out) throws IOException, SOCKSException {
+    private int manageRequest(DataInputStream in, DataOutputStream out) throws IOException, SOCKSException {
         int socksVer = in.readByte() & 0xff;
         if (socksVer != SOCKS_VERSION_5) {
             _log.debug("error in SOCKS5 request (protocol != 5? wtf?)");
@@ -127,9 +131,12 @@ public class SOCKS5Server extends SOCKSServer {
             sendRequestReply(Reply.COMMAND_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out);
             throw new SOCKSException("BIND command not supported");
         case Command.UDP_ASSOCIATE:
+          /*** if(!Boolean.valueOf(tunnel.getOptions().getProperty("i2ptunnel.socks.allowUDP")).booleanValue()) {
             _log.debug("UDP ASSOCIATE command is not supported!");
             sendRequestReply(Reply.COMMAND_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out);
             throw new SOCKSException("UDP ASSOCIATE command not supported");
+           ***/
+            break;
         default:
             _log.debug("unknown command in request (" + Integer.toHexString(command) + ")");
             sendRequestReply(Reply.COMMAND_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out);
@@ -152,7 +159,8 @@ public class SOCKS5Server extends SOCKSServer {
                     connHostName += ".";
                 }
             }
-            _log.warn("IPV4 address type in request: " + connHostName + ". Is your client secure?");
+            if (command != Command.UDP_ASSOCIATE)
+                _log.warn("IPV4 address type in request: " + connHostName + ". Is your client secure?");
             break;
         case AddressType.DOMAINNAME:
             {
@@ -168,9 +176,12 @@ public class SOCKS5Server extends SOCKSServer {
             _log.debug("DOMAINNAME address type in request: " + connHostName);
             break;
         case AddressType.IPV6:
-            _log.warn("IP V6 address type in request! Is your client secure?" + " (IPv6 is not supported, anyway :-)");
-            sendRequestReply(Reply.ADDRESS_TYPE_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out);
-            throw new SOCKSException("IPV6 addresses not supported");
+            if (command != Command.UDP_ASSOCIATE) {
+                _log.warn("IP V6 address type in request! Is your client secure?" + " (IPv6 is not supported, anyway :-)");
+                sendRequestReply(Reply.ADDRESS_TYPE_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out);
+                throw new SOCKSException("IPV6 addresses not supported");
+            }
+            break;
         default:
             _log.debug("unknown address type in request (" + Integer.toHexString(command) + ")");
             sendRequestReply(Reply.ADDRESS_TYPE_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out);
@@ -183,6 +194,7 @@ public class SOCKS5Server extends SOCKSServer {
             sendRequestReply(Reply.CONNECTION_NOT_ALLOWED_BY_RULESET, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out);
             throw new SOCKSException("Invalid port number in request");
         }
+        return command;
     }
 
     protected void confirmConnection() throws SOCKSException {
@@ -293,6 +305,13 @@ public class SOCKS5Server extends SOCKSServer {
                 // Let's not due a new Dest for every request, huh?
                 //I2PSocketManager sm = I2PSocketManagerFactory.createManager();
                 //destSock = sm.connect(I2PTunnel.destFromName(connHostName), null);
+                Destination dest = I2PTunnel.destFromName(connHostName);
+                if (dest == null) {
+                    try {
+                        sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out);
+                    } catch (IOException ioe) {}
+                    throw new SOCKSException("Host not found");
+                }
                 destSock = t.createI2PSocket(I2PTunnel.destFromName(connHostName));
             } else if ("localhost".equals(connHostName) || "127.0.0.1".equals(connHostName)) {
                 String err = "No localhost accesses allowed through the Socks Proxy";
@@ -358,6 +377,59 @@ public class SOCKS5Server extends SOCKSServer {
         return destSock;
     }
 
+    // This isn't really the right place for this, we can't stop the tunnel once it starts.
+    static SOCKSUDPTunnel _tunnel;
+    static Object _startLock = new Object();
+    static byte[] dummyIP = new byte[4];
+    /**
+     * We got a UDP associate command.
+     * Loop here looking for more, never return normally,
+     * or else I2PSocksTunnel will create a streaming lib connection.
+     *
+     * Do UDP Socks clients actually send more than one Associate request?
+     * RFC 1928 isn't clear... maybe not.
+     */
+    private void handleUDP(DataInputStream in, DataOutputStream out) throws SOCKSException {
+        List<Integer> ports = new ArrayList(1);
+        synchronized (_startLock) {
+            if (_tunnel == null) {
+                // tunnel options?
+                _tunnel = new SOCKSUDPTunnel(new I2PTunnel());
+                _tunnel.startRunning();
+            }
+        }
+        while (true) {
+            // Set it up. connHostName and connPort are the client's info.
+            InetAddress ia = null;
+            try {
+                ia = InetAddress.getByAddress(connHostName, dummyIP);
+            } catch (UnknownHostException uhe) {} // won't happen, no resolving done here
+            int myPort = _tunnel.add(ia, connPort);
+            ports.add(Integer.valueOf(myPort));
+            try {
+                sendRequestReply(Reply.SUCCEEDED, AddressType.IPV4, InetAddress.getByName("127.0.0.1"), null, myPort, out);
+            } catch (IOException ioe) { break; }
+
+            // wait for more ???
+            try {
+                int command = manageRequest(in, out);
+                // don't do this...
+                if (command != Command.UDP_ASSOCIATE)
+                    break;
+            } catch (IOException ioe) { break; }
+            catch (SOCKSException ioe) { break; }
+        }
+
+        for (Integer i : ports)
+            _tunnel.remove(i);
+
+        // Prevent I2PSocksTunnel from calling getDestinationI2PSocket() above
+        // to create a streaming lib connection...
+        // This isn't very elegant...
+        //
+        throw new SOCKSException("End of UDP Processing");
+    }
+
     /*
      * Some namespaces to enclose SOCKS protocol codes
      */
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSHeader.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSHeader.java
new file mode 100644
index 0000000000000000000000000000000000000000..763b9aa10a3f227d825d3d3fdb5a292d7d6d7538
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSHeader.java
@@ -0,0 +1,89 @@
+package net.i2p.i2ptunnel.socks;
+
+import net.i2p.data.Base32;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.Destination;
+import net.i2p.i2ptunnel.I2PTunnel;
+
+/**
+ * Save the SOCKS header from a datagram
+ * Ref: RFC 1928
+ *
+ * @author zzz
+ */
+public class SOCKSHeader {
+
+    /**
+     * @param data the whole packet
+     */
+    public SOCKSHeader(byte[] data) {
+        if (data.length <= 8)
+            throw new IllegalArgumentException("Header too short: " + data.length);
+        if (data[0] != 0 || data[1] != 0)
+            throw new IllegalArgumentException("Not a SOCKS datagram?");
+        if (data[2] != 0)
+            throw new IllegalArgumentException("We can't handle fragments!");
+        int headerlen = 0;
+        int addressType = data[3];
+        if (addressType == 1) {
+            // this will fail in getDestination()
+            headerlen = 6 + 4;
+        } else if (addressType == 3) {
+            headerlen = 6 + 1 + (data[4] & 0xff);
+        } else if (addressType == 4) {
+            // this will fail in getDestination()
+            // but future garlicat partial hash lookup possible?
+            headerlen = 6 + 16;
+        } else {
+            throw new IllegalArgumentException("Unknown address type: " + addressType);
+        }
+        if (data.length < headerlen)
+            throw new IllegalArgumentException("Header too short: " + data.length);
+
+        this.header = new byte[headerlen];
+        System.arraycopy(this.header, 0, data, 0, headerlen);
+    }
+    
+    private static final byte[] beg = {0,0,0,3,60};
+    private static final byte[] end = {'.','b','3','2','.','i','2','p',0,0};
+
+    /**
+     *  Make a dummy header from a dest,
+     *  for those cases where we want to receive unsolicited datagrams.
+     *  Unused for now.
+     */
+    public SOCKSHeader(Destination dest) {
+        this.header = new byte[beg.length + 52 + end.length];
+        System.arraycopy(this.header, 0, beg, 0, beg.length);
+        String b32 = Base32.encode(dest.calculateHash().getData());
+        System.arraycopy(this.header, beg.length, b32.getBytes(), 0, 52);
+        System.arraycopy(this.header, beg.length + 52, end, 0, end.length);
+    }
+    
+    public String getHost() {
+        int addressType = this.header[3];
+        if (addressType != 3)
+            return null;
+        int namelen = (this.header[4] & 0xff);
+        byte[] nameBytes = new byte[namelen];
+        System.arraycopy(nameBytes, 0, this.header, 5, namelen);
+        return new String(nameBytes);
+    }
+
+    public Destination getDestination() {
+        String name = getHost();
+        if (name == null)
+            return null;
+        try {
+            // the naming service does caching (thankfully)
+            return I2PTunnel.destFromName(name);
+        } catch (DataFormatException dfe) {}
+        return null;
+    }
+
+    public byte[] getBytes() {
+        return header;
+    }
+
+    private byte[] header;
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java
index 67a52d6889e9dd6f3aa5a8cdcc99b6e5302cccd9..80dfacb6a0b03b00713111c32ad6d861522816b3 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java
@@ -44,6 +44,10 @@ public class SOCKSServerFactory {
             int socksVer = in.readByte();
 
             switch (socksVer) {
+            case 0x04:
+                // SOCKS version 4/4a
+                serv = new SOCKS4aServer(s);
+                break;
             case 0x05:
                 // SOCKS version 5
                 serv = new SOCKS5Server(s);
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPPort.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPPort.java
new file mode 100644
index 0000000000000000000000000000000000000000..b56c9082ffb8d52986e79372c303a2eaee5c0a3c
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPPort.java
@@ -0,0 +1,77 @@
+package net.i2p.i2ptunnel.socks;
+
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.Map;
+
+import net.i2p.data.Destination;
+import net.i2p.i2ptunnel.udp.*;
+
+/**
+ * Implements a UDP port and Socks encapsulation / decapsulation.
+ * This is for a single port. If there is demuxing for multiple
+ * ports, it happens outside of here.
+ *
+ * TX:
+ *   UDPSource -> SOCKSUDPUnwrapper -> ReplyTracker ( -> I2PSink in SOCKSUDPTunnel)
+ *
+ * RX:
+ *   UDPSink <- SOCKSUDPWrapper ( <- MultiSink <- I2PSource in SOCKSUDPTunnel)
+ *
+ * The Unwrapper passes headers to the Wrapper through a cache.
+ * The ReplyTracker passes sinks to MultiSink through a cache.
+ *
+ * @author zzz
+ */
+public class SOCKSUDPPort implements Source, Sink {
+
+    public SOCKSUDPPort(InetAddress host, int port, Map replyMap) {
+
+        // this passes the host and port from UDPUnwrapper to UDPWrapper
+        Map cache = new ConcurrentHashMap(4);
+
+        // rcv from I2P and send to a port
+        this.wrapper = new SOCKSUDPWrapper(cache);
+        this.udpsink = new UDPSink(host, port);
+        this.wrapper.setSink(this.udpsink);
+        
+        // rcv from the same port and send to I2P
+        DatagramSocket sock = this.udpsink.getSocket();
+        this.udpsource = new UDPSource(sock);
+        this.unwrapper = new SOCKSUDPUnwrapper(cache);
+        this.udpsource.setSink(this.unwrapper);
+        this.udptracker = new ReplyTracker(this, replyMap);
+        this.unwrapper.setSink(this.udptracker);
+    }
+
+    /** Socks passes this back to the client on the TCP connection */
+    public int getPort() {
+        return this.udpsink.getPort();
+    }
+
+    public void setSink(Sink sink) {
+        this.udptracker.setSink(sink);
+    }
+
+    public void start() {
+        // the other Sources don't use start
+        this.udpsource.start();
+    }
+
+    public void stop() {
+        this.udpsink.stop();
+        this.udpsource.stop();
+    }
+
+    public void send(Destination from, byte[] data) {
+        this.wrapper.send(from, data);
+    }
+    
+
+    private UDPSink udpsink;
+    private UDPSource udpsource;
+    private SOCKSUDPWrapper wrapper;
+    private SOCKSUDPUnwrapper unwrapper;
+    private ReplyTracker udptracker;
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPTunnel.java
new file mode 100644
index 0000000000000000000000000000000000000000..0adaa1950630a9d5719dd3b464f2c2b0b40f35ff
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPTunnel.java
@@ -0,0 +1,94 @@
+package net.i2p.i2ptunnel.socks;
+
+import java.net.InetAddress;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import net.i2p.data.Destination;
+import net.i2p.i2ptunnel.I2PTunnel;
+import net.i2p.i2ptunnel.Logging;
+import net.i2p.i2ptunnel.udp.*;
+import net.i2p.i2ptunnel.udpTunnel.I2PTunnelUDPClientBase;
+import net.i2p.util.EventDispatcher;
+
+/**
+ * A Datagram Tunnel that can have multiple bidirectional ports on the UDP side.
+ *
+ * TX:
+ *   (ReplyTracker in multiple SOCKSUDPPorts -> ) I2PSink
+ *
+ * RX:
+ *   (SOCKSUDPWrapper in multiple SOCKSUDPPorts <- ) MultiSink <- I2PSource
+ *
+ * The reply from a dest goes to the last SOCKSUDPPort that sent to that dest.
+ * If multiple ports are talking to a dest at the same time, this isn't
+ * going to work very well.
+ *
+ * @author zzz modded from streamr/StreamrConsumer
+ */
+public class SOCKSUDPTunnel extends I2PTunnelUDPClientBase {
+
+    /**
+     *  Set up a tunnel with no UDP side yet.
+     *  Use add() for each port.
+     */
+    public SOCKSUDPTunnel(I2PTunnel tunnel) {
+        super(null, tunnel, tunnel, tunnel);
+
+        this.ports = new ConcurrentHashMap(1);
+        this.cache = new ConcurrentHashMap(1);
+        this.demuxer = new MultiSink(this.cache);
+        setSink(this.demuxer);
+    }
+
+
+    /** @return the UDP port number */
+    public int add(InetAddress host, int port) {
+        SOCKSUDPPort sup = new SOCKSUDPPort(host, port, this.cache);
+        this.ports.put(Integer.valueOf(sup.getPort()), sup);
+        sup.setSink(this);
+        sup.start();
+        return sup.getPort();
+    }
+
+    public void remove(Integer port) {
+        SOCKSUDPPort sup = this.ports.remove(port);
+        if (sup != null)
+            sup.stop();
+        for (Iterator iter = cache.entrySet().iterator(); iter.hasNext();) {
+            Map.Entry<Destination, SOCKSUDPPort> e = (Map.Entry) iter.next();
+            if (e.getValue() == sup)
+                iter.remove();
+        }
+    }
+    
+    public final void startRunning() {
+        super.startRunning();
+        // demuxer start() doesn't do anything
+        startall();
+    }
+    
+    public boolean close(boolean forced) {
+        stopall();
+        return super.close(forced);
+    }
+
+    /** you should really add() after startRunning() */
+    private void startall() {
+    }
+
+    private void stopall() {
+         for (SOCKSUDPPort sup : this.ports.values()) {
+              sup.stop();
+         }
+         this.ports.clear();
+         this.cache.clear();
+    }
+
+
+
+    private Map<Integer, SOCKSUDPPort> ports;
+    private Map<Destination, SOCKSUDPPort> cache;
+    private MultiSink demuxer;
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPUnwrapper.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPUnwrapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..2720b6fd47eec7d926863bd6a926699af44d3991
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPUnwrapper.java
@@ -0,0 +1,59 @@
+package net.i2p.i2ptunnel.socks;
+
+import java.util.Map;
+
+import net.i2p.data.Destination;
+import net.i2p.i2ptunnel.udp.*;
+import net.i2p.util.Log;
+
+/**
+ * Strip a SOCKS header off a datagram, convert it to a Destination
+ * Ref: RFC 1928
+ *
+ * @author zzz
+ */
+public class SOCKSUDPUnwrapper implements Source, Sink {
+    private static final Log _log = new Log(SOCKSUDPUnwrapper.class);
+
+    /**
+     * @param cache put headers here to pass to SOCKSUDPWrapper
+     */
+    public SOCKSUDPUnwrapper(Map<Destination, SOCKSHeader> cache) {
+        this.cache = cache;
+    }
+    
+    public void setSink(Sink sink) {
+        this.sink = sink;
+    }
+
+    public void start() {}
+
+    /**
+     *
+     */
+    public void send(Destination ignored_from, byte[] data) {
+        SOCKSHeader h;
+        try {
+            h = new SOCKSHeader(data);
+        } catch (IllegalArgumentException iae) {
+            _log.error(iae.toString());
+            return;
+        }
+        Destination dest = h.getDestination();
+        if (dest == null) {
+            // no, we aren't going to send non-i2p traffic to a UDP outproxy :)
+            _log.error("Destination not found: " + h.getHost());
+            return;
+        }
+
+        cache.put(dest, h);
+
+        int headerlen = h.getBytes().length;
+        byte unwrapped[] = new byte[data.length - headerlen];
+        System.arraycopy(unwrapped, 0, data, headerlen, unwrapped.length);
+        this.sink.send(dest, unwrapped);
+    }
+    
+    private Sink sink;
+    private Map<Destination, SOCKSHeader> cache;
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPWrapper.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPWrapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..4ec8361576e1791696f1cebbc12dee674a4a102c
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPWrapper.java
@@ -0,0 +1,49 @@
+package net.i2p.i2ptunnel.socks;
+
+import java.util.Map;
+
+import net.i2p.data.Destination;
+import net.i2p.i2ptunnel.udp.*;
+
+/**
+ * Put a SOCKS header on a datagram
+ * Ref: RFC 1928
+ *
+ * @author zzz
+ */
+public class SOCKSUDPWrapper implements Source, Sink {
+    public SOCKSUDPWrapper(Map<Destination, SOCKSHeader> cache) {
+        this.cache = cache;
+    }
+    
+    public void setSink(Sink sink) {
+        this.sink = sink;
+    }
+
+    public void start() {}
+
+    /**
+     * Use the cached header, which should have the host string and port
+     *
+     */
+    public void send(Destination from, byte[] data) {
+        if (this.sink == null)
+            return;
+
+        SOCKSHeader h = cache.get(from);
+        if (h == null) {
+            // RFC 1928 says drop
+            // h = new SOCKSHeader(from);
+            return;
+        }
+
+        byte[] header = h.getBytes();
+        byte wrapped[] = new byte[header.length + data.length];
+        System.arraycopy(wrapped, 0, header, 0, header.length);
+        System.arraycopy(wrapped, header.length, data, 0, data.length);
+        this.sink.send(from, wrapped);
+    }
+    
+    private Sink sink;
+    private Map<Destination, SOCKSHeader> cache;
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/MultiSource.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/MultiSource.java
new file mode 100644
index 0000000000000000000000000000000000000000..5c5a08027ecdc257c911ec3ac4ce46957827ba97
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/MultiSource.java
@@ -0,0 +1,64 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+
+package net.i2p.i2ptunnel.streamr;
+
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.List;
+
+import net.i2p.data.Destination;
+import net.i2p.i2ptunnel.udp.*;
+
+/**
+ * Sends to many Sinks
+ * @author welterde
+ * @author zzz modded for I2PTunnel
+ */
+public class MultiSource implements Source, Sink {
+    public MultiSource() {
+        this.sinks = new CopyOnWriteArrayList<Destination>();
+    }
+    
+    public void setSink(Sink sink) {
+        this.sink = sink;
+    }
+
+    public void start() {}
+
+    public void stop() {
+        this.sinks.clear();
+    }
+
+    public void send(Destination ignored_from, byte[] data) {
+        for(Destination dest : this.sinks) {
+            this.sink.send(dest, data);
+        }
+    }
+    
+    public void add(Destination sink) {
+        this.sinks.add(sink);
+    }
+    
+    public void remove(Destination sink) {
+        this.sinks.remove(sink);
+    }
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    private Sink sink;
+    private List<Destination> sinks;
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/Pinger.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/Pinger.java
new file mode 100644
index 0000000000000000000000000000000000000000..a3a797536196f3f5eeee37a0c841d4648db3cba2
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/Pinger.java
@@ -0,0 +1,59 @@
+package net.i2p.i2ptunnel.streamr;
+
+import net.i2p.i2ptunnel.udp.*;
+
+/**
+ *
+ * @author welterde/zzz
+ */
+public class Pinger implements Source, Runnable {
+    public Pinger() {
+        this.thread = new Thread(this);
+    }
+    public void setSink(Sink sink) {
+        this.sink = sink;
+    }
+    
+    public void start() {
+        this.running = true;
+        this.waitlock = new Object();
+        this.thread.start();
+    }
+    
+    public void stop() {
+        this.running = false;
+        synchronized(this.waitlock) {
+            this.waitlock.notifyAll();
+        }
+        // send unsubscribe-message
+        byte[] data = new byte[1];
+        data[0] = 1;
+        this.sink.send(null, data);
+    }
+    
+    public void run() {
+        // send subscribe-message
+        byte[] data = new byte[1];
+        data[0] = 0;
+        int i = 0;
+        while(this.running) {
+            //System.out.print("p");
+            this.sink.send(null, data);
+            synchronized(this.waitlock) {
+                int delay = 10000;
+                if (i < 5) {
+                    i++;
+                    delay = 2000;
+                }
+                try {
+                    this.waitlock.wait(delay);
+                } catch(InterruptedException ie) {}
+            }
+        }
+    }
+
+    protected Sink sink;
+    protected Thread thread;
+    protected Object waitlock;
+    protected boolean running;
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrConsumer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrConsumer.java
new file mode 100644
index 0000000000000000000000000000000000000000..87ea0eefe6ec16e4c999e057be4430d1077c10cc
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrConsumer.java
@@ -0,0 +1,66 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+
+package net.i2p.i2ptunnel.streamr;
+
+import java.net.InetAddress;
+
+import net.i2p.data.Destination;
+import net.i2p.i2ptunnel.I2PTunnel;
+import net.i2p.i2ptunnel.Logging;
+import net.i2p.i2ptunnel.udp.*;
+import net.i2p.i2ptunnel.udpTunnel.I2PTunnelUDPClientBase;
+import net.i2p.util.EventDispatcher;
+
+/**
+ * Compared to a standard I2PTunnel,
+ * this acts like a client on the I2P side (no privkey file)
+ * but a server on the UDP side (sends to a configured host/port)
+ *
+ * @author welterde
+ * @author zzz modded for I2PTunnel
+ */
+public class StreamrConsumer extends I2PTunnelUDPClientBase {
+
+    public StreamrConsumer(InetAddress host, int port, String destination,
+                           Logging l, EventDispatcher notifyThis,
+                           I2PTunnel tunnel) {
+        super(destination, l, notifyThis, tunnel);
+
+        // create udp-destination
+        this.sink = new UDPSink(host, port);
+        setSink(this.sink);
+        
+        // create pinger
+        this.pinger = new Pinger();
+        this.pinger.setSink(this);
+    }
+    
+    public final void startRunning() {
+        super.startRunning();
+        // send subscribe-message
+        this.pinger.start();
+        l.log("Streamr client ready");
+    }
+    
+    public boolean close(boolean forced) {
+        // send unsubscribe-message
+        this.pinger.stop();
+        this.sink.stop();
+        return super.close(forced);
+    }
+
+
+
+
+
+
+
+
+    
+    
+    private UDPSink sink;
+    private Pinger pinger;
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrProducer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrProducer.java
new file mode 100644
index 0000000000000000000000000000000000000000..b801cb94f4db9a48054b965a4c0f5938139479f8
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrProducer.java
@@ -0,0 +1,72 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+
+package net.i2p.i2ptunnel.streamr;
+
+// system
+import java.io.File;
+
+// i2p
+import net.i2p.client.I2PSession;
+import net.i2p.i2ptunnel.I2PTunnel;
+import net.i2p.i2ptunnel.Logging;
+import net.i2p.i2ptunnel.udp.*;
+import net.i2p.i2ptunnel.udpTunnel.I2PTunnelUDPServerBase;
+import net.i2p.util.EventDispatcher;
+
+/**
+ * Compared to a standard I2PTunnel,
+ * this acts like a server on the I2P side (persistent privkey file)
+ * but a client on the UDP side (receives on a configured port)
+ *
+ * @author welterde
+ * @author zzz modded for I2PTunnel
+ */
+public class StreamrProducer extends I2PTunnelUDPServerBase {
+
+    public StreamrProducer(int port,
+                           File privkey, String privkeyname, Logging l,
+                           EventDispatcher notifyThis, I2PTunnel tunnel) {
+        // verify subscription requests
+        super(true, privkey, privkeyname, l, notifyThis, tunnel);
+        
+        // The broadcaster
+        this.multi = new MultiSource();
+        this.multi.setSink(this);
+
+        // The listener
+        this.subscriber = new Subscriber(this.multi);
+        setSink(this.subscriber);
+
+        // now start udp-server
+        this.server = new UDPSource(port);
+        this.server.setSink(this.multi);
+    }
+    
+    public final void startRunning() {
+        super.startRunning();
+        this.server.start();
+        l.log("Streamr server ready");
+    }
+    
+    public boolean close(boolean forced) {
+        this.server.stop();
+        this.multi.stop();
+        return super.close(forced);
+    }
+
+
+    
+    
+    
+    
+    
+    
+    
+    
+    private MultiSource multi;
+    private UDPSource server;
+    private Sink subscriber;
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/Subscriber.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/Subscriber.java
new file mode 100644
index 0000000000000000000000000000000000000000..97abdb889052e1b9241945e08ec52fc65d714bdb
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/Subscriber.java
@@ -0,0 +1,75 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+
+package net.i2p.i2ptunnel.streamr;
+
+// system
+import java.io.File;
+import java.util.Set;
+
+// i2p
+import net.i2p.client.I2PSession;
+import net.i2p.data.Destination;
+import net.i2p.i2ptunnel.I2PTunnel;
+import net.i2p.i2ptunnel.Logging;
+import net.i2p.i2ptunnel.udp.*;
+import net.i2p.i2ptunnel.udpTunnel.I2PTunnelUDPServerBase;
+import net.i2p.util.EventDispatcher;
+import net.i2p.util.ConcurrentHashSet;
+
+/**
+ * server-mode
+ * @author welterde
+ * @author zzz modded from Producer for I2PTunnel
+ */
+public class Subscriber implements Sink {
+
+    public Subscriber(MultiSource multi) {
+        this.multi = multi;
+        // subscriptions
+        this.subscriptions = new ConcurrentHashSet<Destination>();
+    }
+
+    public void send(Destination dest, byte[] data) {
+        if(dest == null || data.length < 1) {
+            // invalid packet
+            // TODO: write to log
+        } else {
+            byte ctrl = data[0];
+            if(ctrl == 0) {
+                if (!this.subscriptions.contains(dest)) {
+                    // subscribe
+                    System.out.println("Add subscription: " + dest.toBase64().substring(0,4));
+                    this.subscriptions.add(dest);
+                    this.multi.add(dest);
+                } // else already subscribed
+            } else if(ctrl == 1) {
+                // unsubscribe
+                System.out.println("Remove subscription: " + dest.toBase64().substring(0,4));
+                boolean removed = this.subscriptions.remove(dest);
+                if(removed)
+                    multi.remove(dest);
+            } else {
+                // invalid packet
+                // TODO: write to log
+            }
+        }
+    }
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    private I2PSession sess;
+    private Source listener;
+    private Set<Destination> subscriptions;
+    private MultiSource multi;
+    private Source server;
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSink.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSink.java
new file mode 100644
index 0000000000000000000000000000000000000000..3cbccf139e452ab51d56200506820e1ff03d5bda
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSink.java
@@ -0,0 +1,70 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+
+package net.i2p.i2ptunnel.udp;
+
+// i2p
+import net.i2p.client.I2PSession;
+import net.i2p.client.I2PSessionException;
+import net.i2p.data.Destination;
+import net.i2p.client.datagram.I2PDatagramMaker;
+
+/**
+ * Producer
+ *
+ * This sends to a fixed destination specified in the constructor
+ *
+ * @author welterde
+ */
+public class I2PSink implements Sink {
+    public I2PSink(I2PSession sess, Destination dest) {
+        this(sess, dest, false);
+    }
+    public I2PSink(I2PSession sess, Destination dest, boolean raw) {
+        this.sess = sess;
+        this.dest = dest;
+        this.raw = raw;
+        
+        // create maker
+        if (!raw)
+            this.maker = new I2PDatagramMaker(this.sess);
+    }
+    
+    /** @param src ignored */
+    public synchronized void send(Destination src, byte[] data) {
+        //System.out.print("w");
+        // create payload
+        byte[] payload;
+        if(!this.raw)
+            payload = this.maker.makeI2PDatagram(data);
+        else
+            payload = data;
+        
+        // send message
+        try {
+            this.sess.sendMessage(this.dest, payload);
+        } catch(I2PSessionException exc) {
+            // TODO: handle better
+            exc.printStackTrace();
+        }
+    }
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    protected boolean raw;
+    protected I2PSession sess;
+    protected Destination dest;
+    protected I2PDatagramMaker maker;
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSinkAnywhere.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSinkAnywhere.java
new file mode 100644
index 0000000000000000000000000000000000000000..58c5bfda4924df7278bf8bc65293bb1c4018f197
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSinkAnywhere.java
@@ -0,0 +1,68 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+
+package net.i2p.i2ptunnel.udp;
+
+// i2p
+import net.i2p.client.I2PSession;
+import net.i2p.client.I2PSessionException;
+import net.i2p.data.Destination;
+import net.i2p.client.datagram.I2PDatagramMaker;
+
+/**
+ * Producer
+ *
+ * This sends to any destination specified in send()
+ *
+ * @author zzz modded from I2PSink by welterde
+ */
+public class I2PSinkAnywhere implements Sink {
+    public I2PSinkAnywhere(I2PSession sess) {
+        this(sess, false);
+    }
+    public I2PSinkAnywhere(I2PSession sess, boolean raw) {
+        this.sess = sess;
+        this.raw = raw;
+        
+        // create maker
+        if (!raw)
+            this.maker = new I2PDatagramMaker(this.sess);
+    }
+    
+    /** @param to - where it's going */
+    public synchronized void send(Destination to, byte[] data) {
+        // create payload
+        byte[] payload;
+        if(!this.raw)
+            payload = this.maker.makeI2PDatagram(data);
+        else
+            payload = data;
+        
+        // send message
+        try {
+            this.sess.sendMessage(to, payload);
+        } catch(I2PSessionException exc) {
+            // TODO: handle better
+            exc.printStackTrace();
+        }
+    }
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    protected boolean raw;
+    protected I2PSession sess;
+    protected Destination dest;
+    protected I2PDatagramMaker maker;
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSource.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSource.java
new file mode 100644
index 0000000000000000000000000000000000000000..0b54747772f425169ef950bf31e0a2058edde98e
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSource.java
@@ -0,0 +1,123 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+
+package net.i2p.i2ptunnel.udp;
+
+// system
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+
+// i2p
+import net.i2p.client.I2PSession;
+import net.i2p.client.I2PSessionListener;
+import net.i2p.client.datagram.I2PDatagramDissector;
+
+/**
+ *
+ * @author welterde
+ */
+public class I2PSource implements Source, Runnable {
+    public I2PSource(I2PSession sess) {
+        this(sess, true, false);
+    }
+    public I2PSource(I2PSession sess, boolean verify) {
+        this(sess, verify, false);
+    }
+    public I2PSource(I2PSession sess, boolean verify, boolean raw) {
+        this.sess = sess;
+        this.sink = null;
+        this.verify = verify;
+        this.raw = raw;
+        
+        // create queue
+        this.queue = new ArrayBlockingQueue(256);
+        
+        // create listener
+        this.sess.setSessionListener(new Listener());
+        
+        // create thread
+        this.thread = new Thread(this);
+    }
+    
+    public void setSink(Sink sink) {
+        this.sink = sink;
+    }
+    
+    public void start() {
+        this.thread.start();
+    }
+    
+    public void run() {
+        // create dissector
+        I2PDatagramDissector diss = new I2PDatagramDissector();
+        while(true) {
+            try {
+                // get id
+                int id = this.queue.take();
+                
+                // receive message
+                byte[] msg = this.sess.receiveMessage(id);
+                
+                if(!this.raw) {
+                    // load datagram into it
+                    diss.loadI2PDatagram(msg);
+                    
+                    // now call sink
+                    if(this.verify)
+                        this.sink.send(diss.getSender(), diss.getPayload());
+                    else
+                        this.sink.send(diss.extractSender(), diss.extractPayload());
+                } else {
+                    // verify is ignored
+                    this.sink.send(null, msg);
+                }
+                //System.out.print("r");
+            } catch(Exception e) {
+                e.printStackTrace();
+            }
+        }
+    }
+    
+    
+    
+    
+    
+    
+    protected class Listener implements I2PSessionListener {
+
+        public void messageAvailable(I2PSession sess, int id, long size) {
+            try {
+                queue.put(id);
+            } catch(Exception e) {
+                // ignore
+            }
+        }
+
+        public void reportAbuse(I2PSession arg0, int arg1) {
+            // ignore
+        }
+
+        public void disconnected(I2PSession arg0) {
+            // ignore
+        }
+
+        public void errorOccurred(I2PSession arg0, String arg1, Throwable arg2) {
+            // ignore
+        }
+        
+    }
+    
+    
+    
+    
+    
+    
+    protected I2PSession sess;
+    protected BlockingQueue<Integer> queue;
+    protected Sink sink;
+    protected Thread thread;
+    protected boolean verify;
+    protected boolean raw;
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/Sink.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/Sink.java
new file mode 100644
index 0000000000000000000000000000000000000000..49e3e47a3ef52ca8bbac974195fcdb581d0c8843
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/Sink.java
@@ -0,0 +1,17 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+
+package net.i2p.i2ptunnel.udp;
+
+// i2p
+import net.i2p.data.Destination;
+
+/**
+ *
+ * @author welterde
+ */
+public interface Sink {
+    public void send(Destination src, byte[] data);
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/Source.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/Source.java
new file mode 100644
index 0000000000000000000000000000000000000000..f65d03b19659bc9fa15b4dbc2afe20e43e00829b
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/Source.java
@@ -0,0 +1,15 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+
+package net.i2p.i2ptunnel.udp;
+
+/**
+ *
+ * @author welterde
+ */
+public interface Source {
+    public void setSink(Sink sink);
+    public void start();
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/Stream.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/Stream.java
new file mode 100644
index 0000000000000000000000000000000000000000..b8b57e696c58f1dd17000496902faf7df2dc7232
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/Stream.java
@@ -0,0 +1,15 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+
+package net.i2p.i2ptunnel.udp;
+
+/**
+ *
+ * @author welterde
+ */
+public interface Stream {
+    public void start();
+    public void stop();
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSink.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSink.java
new file mode 100644
index 0000000000000000000000000000000000000000..d2e8e8924790164c07bbcb6c7418f9e08eeefbe2
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSink.java
@@ -0,0 +1,77 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+
+package net.i2p.i2ptunnel.udp;
+
+// system
+import java.net.DatagramSocket;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+
+// i2p
+import net.i2p.data.Destination;
+
+/**
+ *
+ * @author welterde
+ */
+public class UDPSink implements Sink {
+    public UDPSink(InetAddress host, int port) {
+        // create socket
+        try {
+            this.sock = new DatagramSocket();
+        } catch(Exception e) {
+            // TODO: fail better
+            throw new RuntimeException("failed to open udp-socket", e);
+        }
+        
+        this.remoteHost = host;
+        
+        // remote port
+        this.remotePort = port;
+    }
+    
+    public void send(Destination src, byte[] data) {
+        // if data.length > this.sock.getSendBufferSize() ...
+
+        // create packet
+        DatagramPacket packet = new DatagramPacket(data, data.length, this.remoteHost, this.remotePort);
+        
+        // send packet
+        try {
+            this.sock.send(packet);
+        } catch(Exception e) {
+            // TODO: fail a bit better
+            e.printStackTrace();
+        }
+    }
+    
+    public int getPort() {    
+        return this.sock.getLocalPort();    
+    }    
+    
+    /** to pass to UDPSource constructor */
+    public DatagramSocket getSocket() {    
+        return this.sock;    
+    }    
+    
+    public void stop() {    
+        this.sock.close();    
+    }    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    protected DatagramSocket sock;
+    protected InetAddress remoteHost;
+    protected int remotePort;
+
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSource.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSource.java
new file mode 100644
index 0000000000000000000000000000000000000000..fc1dd5bf22de3fa88bb534f4a4f8e41c87ed38f3
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSource.java
@@ -0,0 +1,91 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+
+package net.i2p.i2ptunnel.udp;
+
+// system
+import java.net.DatagramSocket;
+import java.net.DatagramPacket;
+
+/**
+ *
+ * @author welterde
+ */
+public class UDPSource implements Source, Runnable {
+    public static final int MAX_SIZE = 15360;
+    public UDPSource(int port) {
+        this.sink = null;
+        
+        // create udp-socket
+        try {
+            this.sock = new DatagramSocket(port);
+        } catch(Exception e) {
+            throw new RuntimeException("failed to listen...", e);
+        }
+        
+        // create thread
+        this.thread = new Thread(this);
+    }
+
+    /** use socket from UDPSink */
+    public UDPSource(DatagramSocket sock) {
+        this.sink = null;
+        this.sock = sock;
+        this.thread = new Thread(this);
+    }
+    
+    public void setSink(Sink sink) {
+        this.sink = sink;
+    }
+    
+    public void start() {
+        this.thread.start();
+    }
+    
+    public void run() {
+        // create packet
+        byte[] buf = new byte[MAX_SIZE];
+        DatagramPacket pack = new DatagramPacket(buf, buf.length);
+        while(true) {
+            try {
+                // receive...
+                this.sock.receive(pack);
+                
+                // create new data array
+                byte[] nbuf = new byte[pack.getLength()];
+                
+                // copy over
+                System.arraycopy(pack.getData(), 0, nbuf, 0, nbuf.length);
+                
+                // transfer to sink
+                this.sink.send(null, nbuf);
+                //System.out.print("i");
+            } catch(Exception e) {
+                e.printStackTrace();
+                break;
+            }
+        }
+    }
+    
+    public void stop() {    
+        this.sock.close();    
+    }    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    protected DatagramSocket sock;
+    protected Sink sink;
+    protected Thread thread;
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPClientBase.java
new file mode 100644
index 0000000000000000000000000000000000000000..c92da6ae8a15863cdaae74ad22ace42ee6087ad3
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPClientBase.java
@@ -0,0 +1,210 @@
+/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java)
+ * (c) 2003 - 2004 mihi
+ */
+package net.i2p.i2ptunnel.udpTunnel;
+
+import java.io.ByteArrayOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.ConnectException;
+import java.net.InetAddress;
+import java.net.NoRouteToHostException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Properties;
+
+import net.i2p.I2PAppContext;
+import net.i2p.I2PException;
+import net.i2p.client.I2PClient;
+import net.i2p.client.I2PClientFactory;
+import net.i2p.client.I2PSession;
+import net.i2p.client.I2PSessionException;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.Destination;
+import net.i2p.i2ptunnel.I2PTunnel;
+import net.i2p.i2ptunnel.I2PTunnelTask;
+import net.i2p.i2ptunnel.Logging;
+import net.i2p.i2ptunnel.udp.*;
+import net.i2p.util.EventDispatcher;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+public abstract class I2PTunnelUDPClientBase extends I2PTunnelTask implements Source, Sink {
+
+    private static final Log _log = new Log(I2PTunnelUDPClientBase.class);
+    protected I2PAppContext _context;
+    protected Logging l;
+
+    static final long DEFAULT_CONNECT_TIMEOUT = 60 * 1000;
+
+    private static volatile long __clientId = 0;
+    protected long _clientId;
+
+    protected Destination dest = null;
+
+    private boolean listenerReady = false;
+
+    private ServerSocket ss;
+
+    private Object startLock = new Object();
+    private boolean startRunning = false;
+
+    private byte[] pubkey;
+
+    private String handlerName;
+
+    private Object conLock = new Object();
+    
+    /** How many connections will we allow to be in the process of being built at once? */
+    private int _numConnectionBuilders;
+    /** How long will we allow sockets to sit in the _waitingSockets map before killing them? */
+    private int _maxWaitTime;
+    
+    private I2PSession _session;
+    private Source _i2pSource;
+    private Sink _i2pSink;
+    private Destination _otherDest;
+
+    /**
+     * Base client class that sets up an I2P Datagram client destination.
+     * The UDP side is not implemented here, as there are at least
+     * two possibilities:
+     *
+     * 1) UDP side is a "server"
+     *    Example: Streamr Consumer
+     *    - Configure a destination host and port
+     *    - External application sends no data
+     *    - Extending class must have a constructor with host and port arguments
+     *
+     * 2) UDP side is a client/server
+     *    Example: SOCKS UDP (DNS requests?)
+     *    - configure an inbound port and a destination host and port
+     *    - External application sends and receives data
+     *    - Extending class must have a constructor with host and 2 port arguments
+     *
+     * So the implementing class must create a UDPSource and/or UDPSink,
+     * and must call setSink().
+     *
+     * @throws IllegalArgumentException if the I2CP configuration is b0rked so
+     *                                  badly that we cant create a socketManager
+     *
+     * @author zzz with portions from welterde's streamr
+     */
+    public I2PTunnelUDPClientBase(String destination, Logging l, EventDispatcher notifyThis,
+                                  I2PTunnel tunnel) throws IllegalArgumentException {
+        super("UDPServer", notifyThis, tunnel);
+        _clientId = ++__clientId;
+        this.l = l;
+
+        _context = tunnel.getContext();
+
+        tunnel.getClientOptions().setProperty("i2cp.dontPublishLeaseSet", "true");
+        
+        // create i2pclient and destination
+        I2PClient client = I2PClientFactory.createClient();
+        Destination dest;
+        byte[] key;
+        try {
+            ByteArrayOutputStream out = new ByteArrayOutputStream(512);
+            dest = client.createDestination(out);
+            key = out.toByteArray();
+        } catch(Exception exc) {
+            throw new RuntimeException("failed to create i2p-destination", exc);
+        }
+
+        // create a session
+        try {
+            ByteArrayInputStream in = new ByteArrayInputStream(key);
+            _session = client.createSession(in, tunnel.getClientOptions());
+        } catch(Exception exc) {
+            throw new RuntimeException("failed to create session", exc);
+        }
+
+        // Setup the source. Always expect raw unverified datagrams.
+        _i2pSource = new I2PSource(_session, false, true);
+
+        // Setup the sink. Always send repliable datagrams.
+        if (destination != null && destination.length() > 0) {
+            try {
+                _otherDest = I2PTunnel.destFromName(destination);
+            } catch (DataFormatException dfe) {}
+            if (_otherDest == null) {
+                l.log("Could not resolve " + destination);
+                throw new RuntimeException("failed to create session - could not resolve " + destination);
+             }
+            _i2pSink = new I2PSink(_session, _otherDest, false);
+        } else {
+            _i2pSink = new I2PSinkAnywhere(_session, false);
+        }   
+    }
+    
+    /**
+     * Actually start working on outgoing connections.
+     * Classes should override to start UDP side as well.
+     *
+     * Not specified in I2PTunnelTask but used in both
+     * I2PTunnelClientBase and I2PTunnelServer so let's
+     * implement it here too.
+     */
+    public void startRunning() {
+        synchronized (startLock) {
+            try {
+                _session.connect();
+            } catch(I2PSessionException exc) {
+                throw new RuntimeException("failed to connect session", exc);
+            }
+            start();
+            startRunning = true;
+            startLock.notify();
+        }
+        open = true;
+    }
+
+    /**
+     * I2PTunnelTask Methods
+     *
+     * Classes should override to close UDP side as well
+     */
+    public boolean close(boolean forced) {
+        if (!open) return true;
+        if (_session != null) {
+            try {
+                _session.destroySession();
+            } catch (I2PSessionException ise) {}
+        }
+        l.log("Closing client " + toString());
+        open = false;
+        return true;
+    }
+
+    /**
+     *  Source Methods
+     *
+     *  Sets the receiver of the UDP datagrams from I2P
+     *  Subclass must call this after constructor
+     *  and before start()
+     */
+    public void setSink(Sink s) {
+        _i2pSource.setSink(s);
+    }
+
+    /** start the source */
+    public void start() {
+        _i2pSource.start();
+    }
+
+    /**
+     *  Sink Methods
+     *
+     * @param to - ignored if configured for a single destination
+     * (we use the dest specified in the constructor)
+     */
+    public void send(Destination to, byte[] data) {
+        _i2pSink.send(to, data);
+    }
+}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPServerBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPServerBase.java
new file mode 100644
index 0000000000000000000000000000000000000000..8dcd66a365e2de0ece17adc004febea7a94be88f
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPServerBase.java
@@ -0,0 +1,211 @@
+/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java)
+ * (c) 2003 - 2004 mihi
+ */
+package net.i2p.i2ptunnel.udpTunnel;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.ConnectException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.util.Iterator;
+import java.util.Properties;
+
+import net.i2p.I2PAppContext;
+import net.i2p.I2PException;
+import net.i2p.client.I2PClient;
+import net.i2p.client.I2PClientFactory;
+import net.i2p.client.I2PSession;
+import net.i2p.client.I2PSessionException;
+import net.i2p.data.Base64;
+import net.i2p.data.Destination;
+import net.i2p.i2ptunnel.I2PTunnel;
+import net.i2p.i2ptunnel.I2PTunnelTask;
+import net.i2p.i2ptunnel.Logging;
+import net.i2p.i2ptunnel.udp.*;
+import net.i2p.util.EventDispatcher;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+public class I2PTunnelUDPServerBase extends I2PTunnelTask implements Source, Sink {
+
+    private final static Log _log = new Log(I2PTunnelUDPServerBase.class);
+
+    private Object lock = new Object();
+    protected Object slock = new Object();
+
+    private static volatile long __serverId = 0;
+
+    protected Logging l;
+
+    private static final long DEFAULT_READ_TIMEOUT = -1; // 3*60*1000;
+    /** default timeout to 3 minutes - override if desired */
+    protected long readTimeout = DEFAULT_READ_TIMEOUT;
+
+    private I2PSession _session;
+    private Source _i2pSource;
+    private Sink _i2pSink;
+
+    /**
+     * Base client class that sets up an I2P Datagram server destination.
+     * The UDP side is not implemented here, as there are at least
+     * two possibilities:
+     *
+     * 1) UDP side is a "client"
+     *    Example: Streamr Producer
+     *    - configure an inbound port
+     *    - External application receives no data
+     *    - Extending class must have a constructor with a port argument
+     *
+     * 2) UDP side is a client/server
+     *    Example: DNS
+     *    - configure an inbound port and a destination host and port
+     *    - External application sends and receives data
+     *    - Extending class must have a constructor with host and 2 port arguments
+     *
+     * So the implementing class must create a UDPSource and/or UDPSink,
+     * and must call setSink().
+     *
+     * @throws IllegalArgumentException if the I2CP configuration is b0rked so
+     *                                  badly that we cant create a socketManager
+     *
+     * @author zzz with portions from welterde's streamr
+     */
+
+    public I2PTunnelUDPServerBase(boolean verify, File privkey, String privkeyname, Logging l,
+                           EventDispatcher notifyThis, I2PTunnel tunnel) {
+        super("UDPServer <- " + privkeyname, notifyThis, tunnel);
+        FileInputStream fis = null;
+        try {
+            fis = new FileInputStream(privkey);
+            init(verify, fis, privkeyname, l);
+        } catch (IOException ioe) {
+            _log.error("Error starting server", ioe);
+            notifyEvent("openServerResult", "error");
+        } finally {
+            if (fis != null)
+                try { fis.close(); } catch (IOException ioe) {}
+        }
+    }
+
+    private void init(boolean verify, InputStream privData, String privkeyname, Logging l) {
+        this.l = l;
+        int portNum = 7654;
+        if (getTunnel().port != null) {
+            try {
+                portNum = Integer.parseInt(getTunnel().port);
+            } catch (NumberFormatException nfe) {
+                _log.log(Log.CRIT, "Invalid port specified [" + getTunnel().port + "], reverting to " + portNum);
+            }
+        }
+
+        // create i2pclient
+        I2PClient client = I2PClientFactory.createClient();
+
+        try {
+            _session = client.createSession(privData, getTunnel().getClientOptions());
+        } catch(I2PSessionException exc) {
+            throw new RuntimeException("failed to create session", exc);
+        }
+
+        // Setup the source. Always expect repliable datagrams, optionally verify
+        _i2pSource = new I2PSource(_session, verify, false);
+
+        // Setup the sink. Always send raw datagrams.
+        _i2pSink = new I2PSinkAnywhere(_session, true);
+    }
+    
+    /**
+     * Classes should override to start UDP side as well.
+     *
+     * Not specified in I2PTunnelTask but used in both
+     * I2PTunnelClientBase and I2PTunnelServer so let's
+     * implement it here too.
+     */
+    public void startRunning() {
+        //synchronized (startLock) {
+            try {
+                _session.connect();
+            } catch(I2PSessionException exc) {
+                throw new RuntimeException("failed to connect session", exc);
+            }
+            start();
+        //}
+
+        notifyEvent("openServerResult", "ok");
+        open = true;
+    }
+
+    /**
+     * Set the read idle timeout for newly-created connections (in
+     * milliseconds).  After this time expires without data being reached from
+     * the I2P network, the connection itself will be closed.
+     */
+    public void setReadTimeout(long ms) {
+        readTimeout = ms;
+    }
+    
+    /**
+     * Get the read idle timeout for newly-created connections (in
+     * milliseconds).
+     *
+     * @return The read timeout used for connections
+     */
+    public long getReadTimeout() {
+        return readTimeout;
+    }
+
+    /**
+     * I2PTunnelTask Methods
+     *
+     * Classes should override to close UDP side as well
+     */
+    public boolean close(boolean forced) {
+        if (!open) return true;
+        synchronized (lock) {
+            l.log("Shutting down server " + toString());
+            try {
+                if (_session != null) {
+                    _session.destroySession();
+                }
+            } catch (I2PException ex) {
+                _log.error("Error destroying the session", ex);
+            }
+            l.log("Server shut down.");
+            open = false;
+            return true;
+        }
+    }
+
+    /**
+     *  Source Methods
+     *
+     *  Sets the receiver of the UDP datagrams from I2P
+     *  Subclass must call this after constructor
+     *  and before start()
+     */
+    public void setSink(Sink s) {
+        _i2pSource.setSink(s);
+    }
+
+    /** start the source */
+    public void start() {
+        _i2pSource.start();
+    }
+
+    /**
+     *  Sink Methods
+     *
+     * @param to
+     *
+     */
+    public void send(Destination to, byte[] data) {
+        _i2pSink.send(to, data);
+    }
+}
+
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
index 46b5557729e2217fe10543cac4ff90ed765badb8..6fcd9f2fe3ca7581b589cc7503d713d8bffed15b 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
@@ -351,6 +351,7 @@ public class IndexBean {
         		("httpclient".equals(type)) ||
         		("sockstunnel".equals(type)) ||
         		("connectclient".equals(type)) ||
+        		("streamrclient".equals(type)) ||
         		("ircclient".equals(type)));
     }
     
@@ -384,8 +385,11 @@ public class IndexBean {
         else if ("ircclient".equals(internalType)) return "IRC client";
         else if ("server".equals(internalType)) return "Standard server";
         else if ("httpserver".equals(internalType)) return "HTTP server";
-        else if ("sockstunnel".equals(internalType)) return "SOCKS 5 proxy";
+        else if ("sockstunnel".equals(internalType)) return "SOCKS 4/4a/5 proxy";
         else if ("connectclient".equals(internalType)) return "CONNECT/SSL/HTTPS proxy";
+        else if ("ircserver".equals(internalType)) return "IRC server";
+        else if ("streamrclient".equals(internalType)) return "Streamr client";
+        else if ("streamrserver".equals(internalType)) return "Streamr server";
         else return internalType;
     }
     
@@ -433,7 +437,8 @@ public class IndexBean {
         TunnelController tun = getController(tunnel);
         if (tun == null) return "";
         String rv;
-        if ("client".equals(tun.getType())||"ircclient".equals(tun.getType()))
+        if ("client".equals(tun.getType()) || "ircclient".equals(tun.getType()) ||
+            "streamrclient".equals(tun.getType()))
             rv = tun.getTargetDestination();
         else
             rv = tun.getProxyList();
@@ -797,7 +802,7 @@ public class IndexBean {
         if ("httpclient".equals(_type) || "connectclient".equals(_type)) {
             if (_proxyList != null)
                 config.setProperty("proxyList", _proxyList);
-        } else if ("ircclient".equals(_type) || "client".equals(_type)) {
+        } else if ("ircclient".equals(_type) || "client".equals(_type) || "streamrclient".equals(_type)) {
             if (_targetDestination != null)
                 config.setProperty("targetDestination", _targetDestination);
         } else if ("httpserver".equals(_type)) {
diff --git a/apps/i2ptunnel/jsp/edit.jsp b/apps/i2ptunnel/jsp/edit.jsp
index 67fdf016c37daf170f4b92db3851cc5df898dd8d..b58798b20296eaedcffbae70791a2f2f162352c8 100644
--- a/apps/i2ptunnel/jsp/edit.jsp
+++ b/apps/i2ptunnel/jsp/edit.jsp
@@ -16,10 +16,8 @@ String tun = request.getParameter("tunnel");
   int curTunnel = -1;
   if (EditBean.isClient(type)) {
     %><jsp:include page="editClient.jsp" /><%
-  } else if ("server".equals(type) || "httpserver".equals(type)) {
-    %><jsp:include page="editServer.jsp" /><%
   } else {
-    %>Invalid tunnel type<%
+    %><jsp:include page="editServer.jsp" /><%
   }
 }
 %>
diff --git a/apps/i2ptunnel/jsp/editClient.jsp b/apps/i2ptunnel/jsp/editClient.jsp
index 3e4c3ecd80f88a991073b2c550541ec7675bca14..6a796eb7df86c01e8e45af4eeae1fdc9677712c2 100644
--- a/apps/i2ptunnel/jsp/editClient.jsp
+++ b/apps/i2ptunnel/jsp/editClient.jsp
@@ -75,7 +75,11 @@
             </div>
                  
             <div id="accessField" class="rowItem">
+         <% if ("streamrclient".equals(tunnelType)) { %>
+                <label>Target:</label>
+         <% } else { %>
                 <label>Access Point:</label>
+         <% } %>
             </div>
             <div id="portField" class="rowItem">
                 <label for="port" accesskey="P">
@@ -87,14 +91,17 @@
                 </label>
                 <input type="text" size="6" maxlength="5" id="port" name="port" title="Access Port Number" value="<%=editBean.getClientPort(curTunnel)%>" class="freetext" />               
             </div>
+         <% String otherInterface = "";
+            String clientInterface = editBean.getClientInterface(curTunnel);
+            if ("streamrclient".equals(tunnelType)) {   
+                otherInterface = clientInterface;
+            } else { %>
             <div id="reachField" class="rowItem">
                 <label for="reachableBy" accesskey="r">
                     <span class="accessKey">R</span>eachable by:
                 </label>
                 <select id="reachableBy" name="reachableBy" title="Valid IP for Client Access" class="selectbox">
-                    <% String clientInterface = editBean.getClientInterface(curTunnel);
-                       String otherInterface = "";
-                       if (!("127.0.0.1".equals(clientInterface)) &&
+                  <%   if (!("127.0.0.1".equals(clientInterface)) &&
                            !("0.0.0.0".equals(clientInterface)) &&
                             (clientInterface != null) &&
                             (clientInterface.trim().length() > 0)) {
@@ -105,9 +112,18 @@
                     <option value="other"<%=(!("".equals(otherInterface))    ? " selected=\"selected\"" : "")%>>LAN Hosts (Please specify your LAN address)</option>
                 </select>                
             </div> 
+         <% } // streamrclient %>
             <div id="otherField" class="rowItem">
                 <label for="reachableByOther" accesskey="O">
+         <% if ("streamrclient".equals(tunnelType)) { %>
+                    Host:
+                    <% String vvv = otherInterface;
+                       if (vvv == null || "".equals(vvv.trim()))
+                           out.write(" <font color=\"red\">(required)</font>");
+                     %>
+         <% } else { %>
                     <span class="accessKey">O</span>ther:
+         <% } %>
                 </label>
                 <input type="text" size="20" id="reachableByOther" name="reachableByOther" title="Alternative IP for Client Access" value="<%=otherInterface%>" class="freetext" />                
             </div>
@@ -123,7 +139,7 @@
                 </label>
                 <input type="text" size="30" id="proxyList" name="proxyList" title="List of Outproxy I2P destinations" value="<%=editBean.getClientDestination(curTunnel)%>" class="freetext" />                
             </div>
-            <% } else if ("client".equals(tunnelType) || "ircclient".equals(tunnelType)) {
+            <% } else if ("client".equals(tunnelType) || "ircclient".equals(tunnelType) || "streamrclient".equals(tunnelType)) {
           %><div id="destinationField" class="rowItem">
                 <label for="targetDestination" accesskey="T">
                     <span class="accessKey">T</span>unnel Destination:
@@ -135,8 +151,9 @@
                 <input type="text" size="30" id="targetDestination" name="targetDestination" title="Destination of the Tunnel" value="<%=editBean.getClientDestination(curTunnel)%>" class="freetext" />                
                 <span class="comment">(name or destination)</span>
             </div>
-            <% }
-          %><div id="profileField" class="rowItem">
+         <% } %>
+         <% if (!"streamrclient".equals(tunnelType)) { %>
+            <div id="profileField" class="rowItem">
                 <label for="profile" accesskey="f">
                     Pro<span class="accessKey">f</span>ile:
                 </label>
@@ -160,6 +177,7 @@
                 <input value="true" type="checkbox" id="shared" name="shared" title="Share tunnels with other clients"<%=(editBean.isSharedClient(curTunnel) ? " checked=\"checked\"" : "")%> class="tickbox" />                
                 <span class="comment">(Share tunnels with other clients and irc/httpclients? Change requires restart of client proxy)</span>
             </div>
+         <% } // !streamrclient %>
             <div id="startupField" class="rowItem">
                 <label for="startOnLoad" accesskey="a">
                     <span class="accessKey">A</span>uto Start:
diff --git a/apps/i2ptunnel/jsp/editServer.jsp b/apps/i2ptunnel/jsp/editServer.jsp
index 82ac69dc5c12e25f3f08dc51e2ae2b8e4a3f4c08..70a9df9f729c9f103aa2c3a274f1abf8c5d42fd7 100644
--- a/apps/i2ptunnel/jsp/editServer.jsp
+++ b/apps/i2ptunnel/jsp/editServer.jsp
@@ -82,11 +82,19 @@
             </div>
                  
             <div id="targetField" class="rowItem">
+         <% if ("streamrserver".equals(tunnelType)) { %>
+                <label>Access Point:</label>
+         <% } else { %>
                 <label>Target:</label>
+         <% } %>
             </div>
             <div id="hostField" class="rowItem">
                 <label for="targetHost" accesskey="H">
+         <% if ("streamrserver".equals(tunnelType)) { %>
+                    <span class="accessKey">R</span>eachable by:
+         <% } else { %>
                     <span class="accessKey">H</span>ost:
+         <% } %>
                 </label>
                 <input type="text" size="20" id="targetHost" name="targetHost" title="Target Hostname or IP" value="<%=editBean.getTargetHost(curTunnel)%>" class="freetext" />                
             </div>
@@ -124,6 +132,7 @@
                 </label>
                 <input type="text" size="30" id="privKeyFile" name="privKeyFile" title="Path to Private Key File" value="<%=editBean.getPrivateKeyFile(curTunnel)%>" class="freetext" />               
             </div>
+         <% if (!"streamrserver".equals(tunnelType)) { %>
             <div id="profileField" class="rowItem">
                 <label for="profile" accesskey="f">
                     Pro<span class="accessKey">f</span>ile:
@@ -134,6 +143,7 @@
                     <option <%=(interactiveProfile == false ? "selected=\"selected\" " : "")%>value="bulk">bulk connection (downloads/websites/BT) </option>
                 </select>                
             </div> 
+         <% } // !streamrserver %>
             <div id="destinationField" class="rowItem">
                 <label for="localDestination" accesskey="L">
                     <span class="accessKey">L</span>ocal destination:
diff --git a/apps/i2ptunnel/jsp/index.jsp b/apps/i2ptunnel/jsp/index.jsp
index b96236ae140090a040e8fe6161f5e1eac8afed52..4d9f6c57d9eac57838f96fe76ccd61503c87f672 100644
--- a/apps/i2ptunnel/jsp/index.jsp
+++ b/apps/i2ptunnel/jsp/index.jsp
@@ -148,8 +148,9 @@
                         <option value="client">Standard</option>
                         <option value="httpclient">HTTP</option>
                         <option value="ircclient">IRC</option>
-                        <option value="sockstunnel">SOCKS 5</option>
+                        <option value="sockstunnel">SOCKS 4/4a/5</option>
                         <option value="connectclient">CONNECT</option>
+                        <option value="streamrclient">Streamr</option>
                     </select>
                     <input class="control" type="submit" value="Create" />
                 </div>
@@ -260,6 +261,8 @@
                     <select name="type">
                         <option value="server">Standard</option>
                         <option value="httpserver">HTTP</option>
+                        <option value="ircserver">IRC</option>
+                        <option value="streamrserver">Streamr</option>
                     </select>
                     <input class="control" type="submit" value="Create" />
                 </div>
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHandler.java
index 09f0905bf3122812de9733e5bb42dfbb705a76ed..b43bc4d1f1ac61c98e80ef3dda1d1b839de0b857 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHandler.java
@@ -2,9 +2,9 @@ package net.i2p.router.web;
 
 import net.i2p.I2PAppContext;
 import net.i2p.data.DataFormatException;
-import net.i2p.data.Destination;
 import net.i2p.data.Hash;
 import net.i2p.data.SessionKey;
+import net.i2p.util.ConvertToHash;
 
 /**
  *  Support additions via B64 Destkey, B64 Desthash, or blahblah.i2p
@@ -19,27 +19,12 @@ public class ConfigKeyringHandler extends FormHandler {
                 addFormError("You must enter a destination and a key");
                 return;
             }
-            Hash h = new Hash();
-            try {
-                h.fromBase64(_peer);
-            } catch (DataFormatException dfe) {}
-            if (h.getData() == null) {
-                try {
-                    Destination d = new Destination();
-                    d.fromBase64(_peer);
-                    h = d.calculateHash();
-                } catch (DataFormatException dfe) {}
-            }
-            if (h.getData() == null) {
-                Destination d = _context.namingService().lookup(_peer);
-                if (d != null)
-                    h = d.calculateHash();
-            }
+            Hash h = ConvertToHash.getHash(_peer);
             SessionKey sk = new SessionKey();
             try {
                 sk.fromBase64(_key);
             } catch (DataFormatException dfe) {}
-            if (h.getData() != null && sk.getData() != null) {
+            if (h != null && h.getData() != null && sk.getData() != null) {
                 _context.keyRing().put(h, sk);
                 addFormNotice("Key for " + h.toBase64() + " added to keyring");
             } else {
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHandler.java
index 0ddcd58a9dfad23964665658e7cd3f9fc83d425b..a4fe7483ecbbb4423ceb81bb0824653f87ee26ce 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHandler.java
@@ -237,7 +237,7 @@ public class ConfigNetHandler extends FormHandler {
 
     private void hiddenSwitch() {
         // Full restart required to generate new keys
-        _context.router().addShutdownTask(new UpdateWrapperManagerAndRekeyTask(Router.EXIT_GRACEFUL_RESTART));
+        _context.addShutdownTask(new UpdateWrapperManagerAndRekeyTask(Router.EXIT_GRACEFUL_RESTART));
         _context.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART);
     }
     
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigRestartBean.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigRestartBean.java
index 3c9fe1bbf4d2fa0ee25e309d2446e84c62888a16..e8eb6b26d3008f8c1436ad809515b377faf3bcef 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigRestartBean.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigRestartBean.java
@@ -25,18 +25,20 @@ public class ConfigRestartBean {
         String systemNonce = getNonce();
         if ( (nonce != null) && (systemNonce.equals(nonce)) && (action != null) ) {
             if ("shutdownImmediate".equals(action)) {
-                ctx.router().addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_HARD));
-                ctx.router().shutdown(Router.EXIT_HARD); // never returns
+                ctx.addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_HARD));
+                //ctx.router().shutdown(Router.EXIT_HARD); // never returns
+                ctx.router().shutdownGracefully(Router.EXIT_HARD); // give the UI time to respond
             } else if ("cancelShutdown".equals(action)) {
                 ctx.router().cancelGracefulShutdown();
             } else if ("restartImmediate".equals(action)) {
-                ctx.router().addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_HARD_RESTART));
-                ctx.router().shutdown(Router.EXIT_HARD_RESTART); // never returns
+                ctx.addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_HARD_RESTART));
+                //ctx.router().shutdown(Router.EXIT_HARD_RESTART); // never returns
+                ctx.router().shutdownGracefully(Router.EXIT_HARD_RESTART); // give the UI time to respond
             } else if ("restart".equals(action)) {
-                ctx.router().addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_GRACEFUL_RESTART));
+                ctx.addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_GRACEFUL_RESTART));
                 ctx.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART);
             } else if ("shutdown".equals(action)) {
-                ctx.router().addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_GRACEFUL));
+                ctx.addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_GRACEFUL));
                 ctx.router().shutdownGracefully();
             }
         }
@@ -79,9 +81,18 @@ public class ConfigRestartBean {
     }
 
     private static boolean isShuttingDown(RouterContext ctx) {
-        return Router.EXIT_GRACEFUL == ctx.router().scheduledGracefulExitCode();
+        return Router.EXIT_GRACEFUL == ctx.router().scheduledGracefulExitCode() ||
+               Router.EXIT_HARD == ctx.router().scheduledGracefulExitCode();
     }
     private static boolean isRestarting(RouterContext ctx) {
-        return Router.EXIT_GRACEFUL_RESTART == ctx.router().scheduledGracefulExitCode();
+        return Router.EXIT_GRACEFUL_RESTART == ctx.router().scheduledGracefulExitCode() ||
+               Router.EXIT_HARD_RESTART == ctx.router().scheduledGracefulExitCode();
+    }
+    /** this is for summaryframe.jsp */
+    public static long getRestartTimeRemaining() {
+        RouterContext ctx = ContextHelper.getContext(null);
+        if (ctx.router().gracefulShutdownInProgress())
+            return ctx.router().getShutdownTimeRemaining();
+        return Long.MAX_VALUE/2;  // summaryframe.jsp adds a safety factor so we don't want to overflow...
     }
 }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java
index bd3bf7a5ea9639920c3870d3dca652a806d8c968..8d3e5725ce370e0a8b8db6367ecbd3a0bc58e124 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java
@@ -53,31 +53,31 @@ public class ConfigServiceHandler extends FormHandler {
         if (_action == null) return;
         
         if ("Shutdown gracefully".equals(_action)) {
-            _context.router().addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_GRACEFUL));
+            _context.addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_GRACEFUL));
             _context.router().shutdownGracefully();
             addFormNotice("Graceful shutdown initiated");
         } else if ("Shutdown immediately".equals(_action)) {
-            _context.router().addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_HARD));
+            _context.addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_HARD));
             _context.router().shutdown(Router.EXIT_HARD);
             addFormNotice("Shutdown immediately!  boom bye bye bad bwoy");
         } else if ("Cancel graceful shutdown".equals(_action)) {
             _context.router().cancelGracefulShutdown();
             addFormNotice("Graceful shutdown cancelled");
         } else if ("Graceful restart".equals(_action)) {
-            _context.router().addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_GRACEFUL_RESTART));
+            _context.addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_GRACEFUL_RESTART));
             _context.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART);
             addFormNotice("Graceful restart requested");
         } else if ("Hard restart".equals(_action)) {
-            _context.router().addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_HARD_RESTART));
+            _context.addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_HARD_RESTART));
             _context.router().shutdown(Router.EXIT_HARD_RESTART);
             addFormNotice("Hard restart requested");
         } else if ("Rekey and Restart".equals(_action)) {
             addFormNotice("Rekeying after graceful restart");
-            _context.router().addShutdownTask(new UpdateWrapperManagerAndRekeyTask(Router.EXIT_GRACEFUL_RESTART));
+            _context.addShutdownTask(new UpdateWrapperManagerAndRekeyTask(Router.EXIT_GRACEFUL_RESTART));
             _context.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART);
         } else if ("Rekey and Shutdown".equals(_action)) {
             addFormNotice("Rekeying after graceful shutdown");
-            _context.router().addShutdownTask(new UpdateWrapperManagerAndRekeyTask(Router.EXIT_GRACEFUL));
+            _context.addShutdownTask(new UpdateWrapperManagerAndRekeyTask(Router.EXIT_GRACEFUL));
             _context.router().shutdownGracefully(Router.EXIT_GRACEFUL);
         } else if ("Run I2P on startup".equals(_action)) {
             installService();
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
index 2e56e858bcbbc928f6324df3085b2274cf63c924..47a07c37466c0e233b372ba62398d68a9309e5e3 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
@@ -1,11 +1,15 @@
 package net.i2p.router.web;
 
+import java.text.Collator;
 import java.text.DateFormat;
 import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.Date;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Locale;
-import java.util.Set;
 
 import net.i2p.data.DataHelper;
 import net.i2p.data.Destination;
@@ -346,20 +350,16 @@ public class SummaryHelper extends HelperBase {
      * @return html section summary
      */
     public String getDestinations() {
-        Set clients = _context.clientManager().listClients();
+        // covert the set to a list so we can sort by name and not lose duplicates
+        List clients = new ArrayList(_context.clientManager().listClients());
+        Collections.sort(clients, new AlphaComparator());
         
         StringBuffer buf = new StringBuffer(512);
         buf.append("<u><b>Local destinations</b></u><br />");
         
         for (Iterator iter = clients.iterator(); iter.hasNext(); ) {
             Destination client = (Destination)iter.next();
-            TunnelPoolSettings in = _context.tunnelManager().getInboundSettings(client.calculateHash());
-            TunnelPoolSettings out = _context.tunnelManager().getOutboundSettings(client.calculateHash());
-            String name = (in != null ? in.getDestinationNickname() : null);
-            if (name == null)
-                name = (out != null ? out.getDestinationNickname() : null);
-            if (name == null)
-                name = client.calculateHash().toBase64().substring(0,6);
+            String name = getName(client);
             
             buf.append("<b>*</b> ").append(name).append("<br />\n");
             LeaseSet ls = _context.netDb().lookupLeaseSetLocally(client.calculateHash());
@@ -373,14 +373,38 @@ public class SummaryHelper extends HelperBase {
                 buf.append("<i>No leases</i><br />\n");
             }
             buf.append("<a href=\"tunnels.jsp#").append(client.calculateHash().toBase64().substring(0,4));
-            buf.append("\">Details</a> ");
+            buf.append("\" target=\"_top\">Details</a> ");
             buf.append("<a href=\"configtunnels.jsp#").append(client.calculateHash().toBase64().substring(0,4));
-            buf.append("\">Config</a><br />\n");
+            buf.append("\" target=\"_top\">Config</a><br />\n");
         }
         buf.append("<hr />\n");
         return buf.toString();
     }
     
+    private class AlphaComparator implements Comparator {
+        public int compare(Object lhs, Object rhs) {
+            String lname = getName((Destination)lhs);
+            String rname = getName((Destination)rhs);
+            if (lname.equals("shared clients"))
+                return -1;
+            if (rname.equals("shared clients"))
+                return 1;
+            return Collator.getInstance().compare(lname, rname);
+        }
+    }
+
+    private String getName(Destination d) {
+        TunnelPoolSettings in = _context.tunnelManager().getInboundSettings(d.calculateHash());
+        String name = (in != null ? in.getDestinationNickname() : null);
+        if (name == null) {
+            TunnelPoolSettings out = _context.tunnelManager().getOutboundSettings(d.calculateHash());
+            name = (out != null ? out.getDestinationNickname() : null);
+            if (name == null)
+                name = d.calculateHash().toBase64().substring(0,6);
+        }
+        return name;
+    }
+
     /**
      * How many free inbound tunnels we have.
      *
@@ -511,4 +535,5 @@ public class SummaryHelper extends HelperBase {
     public boolean updateAvailable() { 
         return NewsFetcher.getInstance(_context).updateAvailable();
     }
+
 }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java
index be39da2fd65023d7303962ac553c6fdef42fdbeb..83495f33e1061d2dce8a812f3d60edbfd1cf513c 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java
@@ -185,7 +185,7 @@ public class UpdateHandler {
     }
     
     private void restart() {
-        _context.router().addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_GRACEFUL_RESTART));
+        _context.addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_GRACEFUL_RESTART));
         _context.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART);
     }
 
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java b/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java
index a56e7753dd83060398bc4cff9faf192c084e857d..e91cbdb7d4c7e7f22faeb5467196e19d33640c6e 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java
@@ -89,9 +89,17 @@ class PacketQueue {
                 // so if we retransmit it will use a new tunnel/lease combo
                 expires = rpe.getNextSendTime() - 500;
             if (expires > 0)
-                sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent, expires);
+                // I2PSessionImpl2
+                //sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent, expires);
+                // I2PSessionMuxedImpl
+                sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent, expires,
+                                 I2PSession.PROTO_STREAMING, I2PSession.PORT_UNSPECIFIED, I2PSession.PORT_UNSPECIFIED);
             else
-                sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent);
+                // I2PSessionImpl2
+                //sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent, 0);
+                // I2PSessionMuxedImpl
+                sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent,
+                                 I2PSession.PROTO_STREAMING, I2PSession.PORT_UNSPECIFIED, I2PSession.PORT_UNSPECIFIED);
             end = _context.clock().now();
             
             if ( (end-begin > 1000) && (_log.shouldLog(Log.WARN)) ) 
diff --git a/core/java/src/net/i2p/I2PAppContext.java b/core/java/src/net/i2p/I2PAppContext.java
index 6b3b0fd5bf3fcb6eee1de0be897febe0ebf250c9..f26f74ab7bf16469cfe3dcb2568d558f2278380c 100644
--- a/core/java/src/net/i2p/I2PAppContext.java
+++ b/core/java/src/net/i2p/I2PAppContext.java
@@ -23,6 +23,7 @@ import net.i2p.crypto.SessionKeyManager;
 import net.i2p.data.RoutingKeyGenerator;
 import net.i2p.stat.StatManager;
 import net.i2p.util.Clock;
+import net.i2p.util.ConcurrentHashSet;
 import net.i2p.util.FortunaRandomSource;
 import net.i2p.util.KeyRing;
 import net.i2p.util.LogManager;
@@ -94,6 +95,7 @@ public class I2PAppContext {
     private volatile boolean _randomInitialized;
     private volatile boolean _keyGeneratorInitialized;
     protected volatile boolean _keyRingInitialized; // used in RouterContext
+    private Set<Runnable> _shutdownTasks;
     
     
     /**
@@ -152,6 +154,7 @@ public class I2PAppContext {
         _elGamalAESEngineInitialized = false;
         _logManagerInitialized = false;
         _keyRingInitialized = false;
+        _shutdownTasks = new ConcurrentHashSet(0);
     }
     
     /**
@@ -557,4 +560,13 @@ public class I2PAppContext {
             _randomInitialized = true;
         }
     }
+
+    public void addShutdownTask(Runnable task) {
+        _shutdownTasks.add(task);
+    }
+    
+    public Set<Runnable> getShutdownTasks() {
+        return new HashSet(_shutdownTasks);
+    }
+    
 }
diff --git a/core/java/src/net/i2p/client/I2PClientImpl.java b/core/java/src/net/i2p/client/I2PClientImpl.java
index 4783458a3a6379768c9ea9760653cb11627e293b..5b1b44867d4e48440416e8220f753d48211fd089 100644
--- a/core/java/src/net/i2p/client/I2PClientImpl.java
+++ b/core/java/src/net/i2p/client/I2PClientImpl.java
@@ -77,6 +77,6 @@ class I2PClientImpl implements I2PClient {
      *
      */
     public I2PSession createSession(I2PAppContext context, InputStream destKeyStream, Properties options) throws I2PSessionException {
-        return new I2PSessionImpl2(context, destKeyStream, options); // thread safe
+        return new I2PSessionMuxedImpl(context, destKeyStream, options); // thread safe and muxed
     }
 }
diff --git a/core/java/src/net/i2p/client/I2PSession.java b/core/java/src/net/i2p/client/I2PSession.java
index d8c64f2222406a39710e32fee3bb91077e9467d1..1776af5c0f91200ccfcfbcbb20ec0101fb8f821e 100644
--- a/core/java/src/net/i2p/client/I2PSession.java
+++ b/core/java/src/net/i2p/client/I2PSession.java
@@ -40,6 +40,8 @@ public interface I2PSession {
      */
     public boolean sendMessage(Destination dest, byte[] payload) throws I2PSessionException;
     public boolean sendMessage(Destination dest, byte[] payload, int offset, int size) throws I2PSessionException;
+    /** See I2PSessionMuxedImpl for details */
+    public boolean sendMessage(Destination dest, byte[] payload, int proto, int fromport, int toport) throws I2PSessionException;
 
     /**
      * Like sendMessage above, except the key used and the tags sent are exposed to the 
@@ -71,6 +73,12 @@ public interface I2PSession {
     public boolean sendMessage(Destination dest, byte[] payload, SessionKey keyUsed, Set tagsSent) throws I2PSessionException;
     public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent) throws I2PSessionException;
     public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent, long expire) throws I2PSessionException;
+    /** See I2PSessionMuxedImpl for details */
+    public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent,
+                               int proto, int fromport, int toport) throws I2PSessionException;
+    /** See I2PSessionMuxedImpl for details */
+    public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent, long expire,
+                               int proto, int fromport, int toport) throws I2PSessionException;
 
     /** Receive a message that the router has notified the client about, returning
      * the payload.
@@ -134,4 +142,18 @@ public interface I2PSession {
      *
      */
     public Destination lookupDest(Hash h) throws I2PSessionException;
+
+    /** See I2PSessionMuxedImpl for details */
+    public void addSessionListener(I2PSessionListener lsnr, int proto, int port);
+    /** See I2PSessionMuxedImpl for details */
+    public void addMuxedSessionListener(I2PSessionMuxedListener l, int proto, int port);
+    /** See I2PSessionMuxedImpl for details */
+    public void removeListener(int proto, int port);
+
+    public static final int PORT_ANY = 0;
+    public static final int PORT_UNSPECIFIED = 0;
+    public static final int PROTO_ANY = 0;
+    public static final int PROTO_UNSPECIFIED = 0;
+    public static final int PROTO_STREAMING = 6;
+    public static final int PROTO_DATAGRAM = 17;
 }
diff --git a/core/java/src/net/i2p/client/I2PSessionDemultiplexer.java b/core/java/src/net/i2p/client/I2PSessionDemultiplexer.java
new file mode 100644
index 0000000000000000000000000000000000000000..9a1ff42e31ea9cf854696be79f6243c5d6c70901
--- /dev/null
+++ b/core/java/src/net/i2p/client/I2PSessionDemultiplexer.java
@@ -0,0 +1,135 @@
+package net.i2p.client;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.Map;
+
+import net.i2p.I2PAppContext;
+import net.i2p.util.Log;
+
+/*
+ * public domain
+ */
+
+/**
+ * Implement multiplexing with a 1-byte 'protocol' and a two-byte 'port'.
+ * Listeners register with either addListener() or addMuxedListener(),
+ * depending on whether they want to hear about the
+ * protocol, from port, and to port for every received message.
+ *
+ * This only calls one listener, not all that apply.
+ *
+ * @author zzz
+ */
+public class I2PSessionDemultiplexer implements I2PSessionMuxedListener {
+    private Log _log;
+    private Map<Integer, I2PSessionMuxedListener> _listeners;
+
+    public I2PSessionDemultiplexer(I2PAppContext ctx) {
+        _log = ctx.logManager().getLog(I2PSessionDemultiplexer.class);
+        _listeners = new ConcurrentHashMap();
+    }
+
+    /** unused */
+    public void messageAvailable(I2PSession session, int msgId, long size) {}
+
+    public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport ) {
+        I2PSessionMuxedListener l = findListener(proto, toport);
+        if (l != null)
+            l.messageAvailable(session, msgId, size, proto, fromport, toport);
+        else {
+            // no listener, throw it out
+            _log.error("No listener found for proto: " + proto + " port: " + toport + "msg id: " + msgId +
+                       " from pool of " + _listeners.size() + " listeners");
+            try {
+                session.receiveMessage(msgId);
+            } catch (I2PSessionException ise) {}
+        }
+    }
+
+    public void reportAbuse(I2PSession session, int severity) {
+        for (I2PSessionMuxedListener l : _listeners.values())
+            l.reportAbuse(session, severity);
+    }
+
+    public void disconnected(I2PSession session) {
+        for (I2PSessionMuxedListener l : _listeners.values())
+            l.disconnected(session);
+    }
+
+    public void errorOccurred(I2PSession session, String message, Throwable error) {
+        for (I2PSessionMuxedListener l : _listeners.values())
+            l.errorOccurred(session, message, error);
+    }
+
+    /**
+     *  For those that don't need to hear about the protocol and ports
+     *  in messageAvailable()
+     *  (Streaming lib)
+     */
+    public void addListener(I2PSessionListener l, int proto, int port) {
+        _listeners.put(key(proto, port), new NoPortsListener(l));
+    }
+
+    /**
+     *  For those that do care
+     *  UDP perhaps
+     */
+    public void addMuxedListener(I2PSessionMuxedListener l, int proto, int port) {
+        _listeners.put(key(proto, port), l);
+    }
+
+    public void removeListener(int proto, int port) {
+        _listeners.remove(key(proto, port));
+    }
+
+    /** find the one listener that most specifically matches the request */
+    private I2PSessionMuxedListener findListener(int proto, int port) {
+        I2PSessionMuxedListener rv = getListener(proto, port);
+        if (rv != null) return rv;
+        if (port != I2PSession.PORT_ANY) { // try any port
+            rv = getListener(proto, I2PSession.PORT_ANY);
+            if (rv != null) return rv;
+        }
+        if (proto != I2PSession.PROTO_ANY) { // try any protocol
+            rv = getListener(I2PSession.PROTO_ANY, port);
+            if (rv != null) return rv;
+        }
+        if (proto != I2PSession.PROTO_ANY && port != I2PSession.PORT_ANY) { // try default
+            rv = getListener(I2PSession.PROTO_ANY, I2PSession.PORT_ANY);
+        }
+        return rv;
+    }
+
+    private I2PSessionMuxedListener getListener(int proto, int port) {
+        return _listeners.get(key(proto, port));
+    }
+
+    private Integer key(int proto, int port) {
+        return Integer.valueOf(((port << 8) & 0xffff00) | proto);
+    }
+
+    /** for those that don't care about proto and ports */
+    private static class NoPortsListener implements I2PSessionMuxedListener {
+        private I2PSessionListener _l;
+
+        public NoPortsListener(I2PSessionListener l) {
+            _l = l;
+        }
+
+        public void messageAvailable(I2PSession session, int msgId, long size) {
+            throw new IllegalArgumentException("no");
+        }
+        public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) {
+            _l.messageAvailable(session, msgId, size);
+        }
+        public void reportAbuse(I2PSession session, int severity) {
+            _l.reportAbuse(session, severity);
+        }
+        public void disconnected(I2PSession session) {
+            _l.disconnected(session);
+        }
+        public void errorOccurred(I2PSession session, String message, Throwable error) {
+            _l.errorOccurred(session, message, error);
+        }
+    }
+}
diff --git a/core/java/src/net/i2p/client/I2PSessionImpl.java b/core/java/src/net/i2p/client/I2PSessionImpl.java
index 00da88aa222b1b9b3ebd07c283843e42c3b79b25..0e13f2c56308c82a587081bec7cc0b7e23e178b7 100644
--- a/core/java/src/net/i2p/client/I2PSessionImpl.java
+++ b/core/java/src/net/i2p/client/I2PSessionImpl.java
@@ -77,12 +77,12 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
     protected OutputStream _out;
 
     /** who we send events to */
-    private I2PSessionListener _sessionListener;
+    protected I2PSessionListener _sessionListener;
 
     /** class that generates new messages */
     protected I2CPMessageProducer _producer;
     /** map of Long --> MessagePayloadMessage */
-    private Map<Long, MessagePayloadMessage> _availableMessages;
+    protected Map<Long, MessagePayloadMessage> _availableMessages;
     
     protected I2PClientMessageHandlerMap _handlerMap;
     
@@ -366,14 +366,14 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
         }
         SimpleScheduler.getInstance().addEvent(new VerifyUsage(mid), 30*1000);
     }
-    private class VerifyUsage implements SimpleTimer.TimedEvent {
+    protected class VerifyUsage implements SimpleTimer.TimedEvent {
         private Long _msgId;
         public VerifyUsage(Long id) { _msgId = id; }
         
         public void timeReached() {
             MessagePayloadMessage removed = _availableMessages.remove(_msgId);
             if (removed != null && !isClosed())
-                _log.log(Log.CRIT, "Message NOT removed!  id=" + _msgId + ": " + removed);
+                _log.error("Message NOT removed!  id=" + _msgId + ": " + removed);
         }
     }
 
diff --git a/core/java/src/net/i2p/client/I2PSessionImpl2.java b/core/java/src/net/i2p/client/I2PSessionImpl2.java
index 56ef88974b88cd3d7201d68cf38131cc8bbaccdd..9abce4b72b8f98111124922566571da65dea66ec 100644
--- a/core/java/src/net/i2p/client/I2PSessionImpl2.java
+++ b/core/java/src/net/i2p/client/I2PSessionImpl2.java
@@ -93,7 +93,7 @@ class I2PSessionImpl2 extends I2PSessionImpl {
      *  set to false.
      */
     private static final int DONT_COMPRESS_SIZE = 66;
-    private boolean shouldCompress(int size) {
+    protected boolean shouldCompress(int size) {
          if (size <= DONT_COMPRESS_SIZE)
              return false;
          String p = getOptions().getProperty("i2cp.gzip");
@@ -102,12 +102,35 @@ class I2PSessionImpl2 extends I2PSessionImpl {
          return SHOULD_COMPRESS;
     }
     
+    public void addSessionListener(I2PSessionListener lsnr, int proto, int port) {
+        throw new IllegalArgumentException("Use MuxedImpl");
+    }
+    public void addMuxedSessionListener(I2PSessionMuxedListener l, int proto, int port) {
+        throw new IllegalArgumentException("Use MuxedImpl");
+    }
+    public void removeListener(int proto, int port) {
+        throw new IllegalArgumentException("Use MuxedImpl");
+    }
+    public boolean sendMessage(Destination dest, byte[] payload, int proto, int fromport, int toport) throws I2PSessionException {
+        throw new IllegalArgumentException("Use MuxedImpl");
+    }
+    public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent,
+                               int proto, int fromport, int toport) throws I2PSessionException {
+        throw new IllegalArgumentException("Use MuxedImpl");
+    }
+    public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent, long expire,
+                               int proto, int fromport, int toport) throws I2PSessionException {
+        throw new IllegalArgumentException("Use MuxedImpl");
+    }
+
     @Override
     public boolean sendMessage(Destination dest, byte[] payload) throws I2PSessionException {
         return sendMessage(dest, payload, 0, payload.length);
     }
     public boolean sendMessage(Destination dest, byte[] payload, int offset, int size) throws I2PSessionException {
-        return sendMessage(dest, payload, offset, size, new SessionKey(), new HashSet(64), 0);
+        // we don't do end-to-end crypto any more
+        //return sendMessage(dest, payload, offset, size, new SessionKey(), new HashSet(64), 0);
+        return sendMessage(dest, payload, offset, size, null, null, 0);
     }
     
     @Override
@@ -173,7 +196,7 @@ class I2PSessionImpl2 extends I2PSessionImpl {
     
     private static final int NUM_TAGS = 50;
 
-    private boolean sendBestEffort(Destination dest, byte payload[], SessionKey keyUsed, Set tagsSent, long expires)
+    protected boolean sendBestEffort(Destination dest, byte payload[], SessionKey keyUsed, Set tagsSent, long expires)
                     throws I2PSessionException {
         SessionKey key = null;
         SessionKey newKey = null;
diff --git a/core/java/src/net/i2p/client/I2PSessionListener.java b/core/java/src/net/i2p/client/I2PSessionListener.java
index 4c78c65272141d49d3d9a5cc291142587034ba10..740ebeeab34e0116436059df27dc164211ed3af7 100644
--- a/core/java/src/net/i2p/client/I2PSessionListener.java
+++ b/core/java/src/net/i2p/client/I2PSessionListener.java
@@ -20,7 +20,7 @@ public interface I2PSessionListener {
      * size # of bytes.
      * @param session session to notify
      * @param msgId message number available
-     * @param size size of the message
+     * @param size size of the message - why it's a long and not an int is a mystery
      */
     void messageAvailable(I2PSession session, int msgId, long size);
 
@@ -42,4 +42,4 @@ public interface I2PSessionListener {
      *
      */
     void errorOccurred(I2PSession session, String message, Throwable error);
-}
\ No newline at end of file
+}
diff --git a/core/java/src/net/i2p/client/I2PSessionMuxedImpl.java b/core/java/src/net/i2p/client/I2PSessionMuxedImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..b08d01c2646ac3adb1bb7f0b5c5e4046f14c13ce
--- /dev/null
+++ b/core/java/src/net/i2p/client/I2PSessionMuxedImpl.java
@@ -0,0 +1,320 @@
+package net.i2p.client;
+
+/*
+ * public domain
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.Set;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Destination;
+import net.i2p.data.SessionKey;
+import net.i2p.data.SessionTag;
+import net.i2p.data.i2cp.MessagePayloadMessage;
+import net.i2p.util.Log;
+import net.i2p.util.SimpleScheduler;
+
+/**
+ * I2PSession with protocol and ports
+ *
+ * Streaming lib has been modified to send I2PSession.PROTO_STREAMING but
+ * still receives all. It sends with fromPort and toPort = 0, and receives on all ports.
+ *
+ * No datagram apps have been modified yet.
+
+ * Therefore the compatibility situation is as follows:
+ *
+ * Compatibility:
+ *    old streaming -> new streaming: sends proto anything, rcvs proto anything
+ *    new streaming -> old streaming: sends PROTO_STREAMING, ignores rcvd proto
+ *    old datagram -> new datagram: sends proto anything, rcvs proto anything
+ *    new datagram -> old datagram: sends PROTO_DATAGRAM, ignores rcvd proto
+ *    In all the above cases, streaming and datagram receive traffic for the other
+ *    protocol, same as before.
+ *
+ *    old datagram -> new muxed: doesn't work because the old sends proto 0 but the udp side
+ *                               of the mux registers with PROTO_DATAGRAM, so the datagrams
+ *                               go to the streaming side, same as before.
+ *    old streaming -> new muxed: works
+ *
+ * Typical Usage:
+ *    Streaming + datagrams:
+ *        I2PSocketManager sockMgr = getSocketManager();
+ *        I2PSession session = sockMgr.getSession();
+ *        session.addMuxedSessionListener(myI2PSessionMuxedListener, I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY);
+ *         * or *
+ *        session.addSessionListener(myI2PSessionListener, I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY);
+ *        session.sendMessage(dest, payload, I2PSession.PROTO_DATAGRAM, fromPort, toPort);
+ *
+ *    Datagrams only, with multiple ports:
+ *        I2PClient client = I2PClientFactory.createClient();
+ *        ...
+ *        I2PSession session = client.createSession(...);
+ *        session.addMuxedSessionListener(myI2PSessionMuxedListener, I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY);
+ *         * or *
+ *        session.addSessionListener(myI2PSessionListener, I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY);
+ *        session.sendMessage(dest, payload, I2PSession.PROTO_DATAGRAM, fromPort, toPort);
+ *
+ *    Multiple streaming ports:
+ *        Needs some streaming lib hacking
+ *
+ * @author zzz
+ */
+class I2PSessionMuxedImpl extends I2PSessionImpl2 implements I2PSession {
+    private I2PSessionDemultiplexer _demultiplexer;
+
+    public I2PSessionMuxedImpl(I2PAppContext ctx, InputStream destKeyStream, Properties options) throws I2PSessionException {
+        super(ctx, destKeyStream, options);
+        // also stored in _sessionListener but we keep it in _demultipexer
+        // as well so we don't have to keep casting
+        _demultiplexer =  new I2PSessionDemultiplexer(ctx);
+        super.setSessionListener(_demultiplexer);
+        // discards the one in super(), sorry about that... (no it wasn't started yet)
+        _availabilityNotifier = new MuxedAvailabilityNotifier();
+    }
+    
+    /** listen on all protocols and ports */
+    @Override
+    public void setSessionListener(I2PSessionListener lsnr) {
+        _demultiplexer.addListener(lsnr, PROTO_ANY, PORT_ANY);
+    }
+
+    /**
+     *  Listen on specified protocol and port.
+     *
+     *  An existing listener with the same proto and port is replaced.
+     *  Only the listener with the best match is called back for each message.
+     *
+     *  @param proto 1-254 or PROTO_ANY for all; recommended:
+     *         I2PSession.PROTO_STREAMING
+     *         I2PSession.PROTO_DATAGRAM
+     *         255 disallowed
+     *  @param port 1-65535 or PORT_ANY for all
+     */
+    public void addSessionListener(I2PSessionListener lsnr, int proto, int port) {
+        _demultiplexer.addListener(lsnr, proto, port);
+    }
+
+    /**
+     *  Listen on specified protocol and port, and receive notification
+     *  of proto, fromPort, and toPort for every message.
+     *  @param proto 1-254 or 0 for all; 255 disallowed
+     *  @param port 1-65535 or 0 for all
+     */
+    public void addMuxedSessionListener(I2PSessionMuxedListener l, int proto, int port) {
+        _demultiplexer.addMuxedListener(l, proto, port);
+    }
+
+    /** removes the specified listener (only) */
+    public void removeListener(int proto, int port) {
+        _demultiplexer.removeListener(proto, port);
+    }
+
+    @Override
+    public boolean sendMessage(Destination dest, byte[] payload) throws I2PSessionException {
+        return sendMessage(dest, payload, 0, payload.length, null, null,
+                           0, PROTO_UNSPECIFIED, PORT_UNSPECIFIED, PORT_UNSPECIFIED);
+    }
+
+    @Override
+    public boolean sendMessage(Destination dest, byte[] payload, int proto, int fromport, int toport) throws I2PSessionException {
+        return sendMessage(dest, payload, 0, payload.length, null, null, 0, proto, fromport, toport);
+    }
+
+    @Override
+    public boolean sendMessage(Destination dest, byte[] payload, int offset, int size,
+                               SessionKey keyUsed, Set tagsSent, long expires)
+                   throws I2PSessionException {
+        return sendMessage(dest, payload, offset, size, keyUsed, tagsSent, 0, PROTO_UNSPECIFIED, PORT_UNSPECIFIED, PORT_UNSPECIFIED);
+    }
+
+    @Override
+    public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent,
+                               int proto, int fromport, int toport) throws I2PSessionException {
+        return sendMessage(dest, payload, offset, size, keyUsed, tagsSent, 0, proto, fromport, toport);
+    }
+
+    /**
+     *  @param proto 1-254 or 0 for unset; recommended:
+     *         I2PSession.PROTO_UNSPECIFIED
+     *         I2PSession.PROTO_STREAMING
+     *         I2PSession.PROTO_DATAGRAM
+     *         255 disallowed
+     *  @param fromport 1-65535 or 0 for unset
+     *  @param toport 1-65535 or 0 for unset
+     */
+    public boolean sendMessage(Destination dest, byte[] payload, int offset, int size,
+                               SessionKey keyUsed, Set tagsSent, long expires,
+                               int proto, int fromPort, int toPort)
+                   throws I2PSessionException {
+        if (isClosed()) throw new I2PSessionException("Already closed");
+        updateActivity();
+
+        boolean sc = shouldCompress(size);
+        if (sc)
+            payload = DataHelper.compress(payload, offset, size);
+        else
+            payload = DataHelper.compress(payload, offset, size, DataHelper.NO_COMPRESSION);
+
+        setProto(payload, proto);
+        setFromPort(payload, fromPort);
+        setToPort(payload, toPort);
+
+        _context.statManager().addRateData("i2cp.tx.msgCompressed", payload.length, 0);
+        _context.statManager().addRateData("i2cp.tx.msgExpanded", size, 0);
+        return sendBestEffort(dest, payload, keyUsed, tagsSent, expires);
+    }
+
+    /**
+     * Receive a payload message and let the app know its available
+     */
+    @Override
+    public void addNewMessage(MessagePayloadMessage msg) {
+        Long mid = new Long(msg.getMessageId());
+        _availableMessages.put(mid, msg);
+        long id = msg.getMessageId();
+        byte data[] = msg.getPayload().getUnencryptedData();
+        if ((data == null) || (data.length <= 0)) {
+            if (_log.shouldLog(Log.CRIT))
+                _log.log(Log.CRIT, getPrefix() + "addNewMessage of a message with no unencrypted data",
+                           new Exception("Empty message"));
+            return;
+        }
+        int size = data.length;
+        if (size < 10) {
+            _log.error(getPrefix() + "length too short for gzip header: " + size);
+            return;
+        }
+        ((MuxedAvailabilityNotifier)_availabilityNotifier).available(id, size, getProto(msg),
+                                                                     getFromPort(msg), getToPort(msg));
+        SimpleScheduler.getInstance().addEvent(new VerifyUsage(mid), 30*1000);
+    }
+
+    protected class MuxedAvailabilityNotifier extends AvailabilityNotifier {
+        private LinkedBlockingQueue<MsgData> _msgs;
+        private boolean _alive;
+        private static final int POISON_SIZE = -99999;
+ 
+        public MuxedAvailabilityNotifier() {
+            _msgs = new LinkedBlockingQueue();
+        }
+        
+        public void stopNotifying() { 
+            _msgs.clear(); 
+            if (_alive) {
+                _alive = false; 
+                try {
+                    _msgs.put(new MsgData(0, POISON_SIZE, 0, 0, 0));
+                } catch (InterruptedException ie) {}
+            }
+        }
+        
+        /** unused */
+        public void available(long msgId, int size) { throw new IllegalArgumentException("no"); }
+
+        public void available(long msgId, int size, int proto, int fromPort, int toPort) {
+            try {
+                _msgs.put(new MsgData((int)(msgId & 0xffffffff), size, proto, fromPort, toPort));
+            } catch (InterruptedException ie) {}
+        }
+
+        public void run() {
+            _alive = true;
+            while (true) {
+                MsgData msg;
+                try {
+                    msg = _msgs.take();
+                } catch (InterruptedException ie) {
+                    continue;
+                }
+                if (msg.size == POISON_SIZE)
+                    break;
+                try {
+                    _demultiplexer.messageAvailable(I2PSessionMuxedImpl.this, msg.id,
+                                                    msg.size, msg.proto, msg.fromPort, msg.toPort);
+                } catch (Exception e) {
+                    _log.error("Error notifying app of message availability");
+                }
+            }
+        }
+    }
+
+    /** let's keep this simple */
+    private static class MsgData {
+        public int id, size, proto, fromPort, toPort;
+        public MsgData(int i, int s, int p, int f, int t) {
+            id = i;
+            size = s;
+            proto = p;
+            fromPort = f;
+            toPort = t;
+        }
+    }
+    
+    /**
+     *  No, we couldn't put any protocol byte in front of everything and
+     *  keep backward compatibility. But there are several bytes that
+     *  are unused AND unchecked in the gzip header in releases <= 0.7.
+     *  So let's use 5 of them for a protocol and two 2-byte ports.
+     *
+     *  Following are all the methods to hide the
+     *  protocol, fromPort, and toPort in the gzip header
+     *
+     *  The fields used are all ignored on receive in ResettableGzipInputStream
+     *
+     *  See also ResettableGzipOutputStream.
+     *  Ref: RFC 1952
+     *
+     */
+
+    /** OS byte in gzip header */
+    private static final int PROTO_BYTE = 9;
+
+    /** Upper two bytes of MTIME in gzip header */
+    private static final int FROMPORT_BYTES = 4;
+
+    /** Lower two bytes of MTIME in gzip header */
+    private static final int TOPORT_BYTES = 6;
+
+    /** Non-muxed sets the OS byte to 0xff */
+    private static int getProto(MessagePayloadMessage msg) {
+        int rv = getByte(msg, PROTO_BYTE) & 0xff;
+        return rv == 0xff ? PROTO_UNSPECIFIED : rv;
+    }	
+
+    /** Non-muxed sets the MTIME bytes to 0 */
+    private static int getFromPort(MessagePayloadMessage msg) {
+        return (((getByte(msg, FROMPORT_BYTES) & 0xff) << 8) |
+                 (getByte(msg, FROMPORT_BYTES + 1) & 0xff));
+    }	
+
+    /** Non-muxed sets the MTIME bytes to 0 */
+    private static int getToPort(MessagePayloadMessage msg) {
+        return (((getByte(msg, TOPORT_BYTES) & 0xff) << 8) |
+                 (getByte(msg, TOPORT_BYTES + 1) & 0xff));
+    }	
+
+    private static int getByte(MessagePayloadMessage msg, int i) {
+        return msg.getPayload().getUnencryptedData()[i] & 0xff;
+    }	
+
+    private static void setProto(byte[] payload, int p) {
+        payload[PROTO_BYTE] = (byte) (p & 0xff);
+    }	
+
+    private static void setFromPort(byte[] payload, int p) {
+        payload[FROMPORT_BYTES] = (byte) ((p >> 8) & 0xff);
+        payload[FROMPORT_BYTES + 1] = (byte) (p & 0xff);
+    }	
+
+    private static void setToPort(byte[] payload, int p) {
+        payload[TOPORT_BYTES] = (byte) ((p >> 8) & 0xff);
+        payload[TOPORT_BYTES + 1] = (byte) (p & 0xff);
+    }	
+}
diff --git a/core/java/src/net/i2p/client/I2PSessionMuxedListener.java b/core/java/src/net/i2p/client/I2PSessionMuxedListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..118dc75caea9011239273448d7cc5f65266a4387
--- /dev/null
+++ b/core/java/src/net/i2p/client/I2PSessionMuxedListener.java
@@ -0,0 +1,62 @@
+package net.i2p.client;
+
+/*
+ * public domain
+ */
+
+/**
+ * Define a means for the router to asynchronously notify the client that a
+ * new message is available or the router is under attack.
+ *
+ * @author zzz extends I2PSessionListener
+ */
+public interface I2PSessionMuxedListener extends I2PSessionListener {
+
+    /**
+     * Will be called only if you register via
+     * setSessionListener() or addSessionListener().
+     * And if you are doing that, just use I2PSessionListener.
+     *
+     * If you register via addSessionListener(),
+     * this will be called only for the proto(s) and toport(s) you register for.
+     *
+     * @param session session to notify
+     * @param msgId message number available
+     * @param size size of the message - why it's a long and not an int is a mystery
+     */
+    void messageAvailable(I2PSession session, int msgId, long size);
+
+    /**
+     * Instruct the client that the given session has received a message
+     *
+     * Will be called only if you register via addMuxedSessionListener().
+     * Will be called only for the proto(s) and toport(s) you register for.
+     *
+     * @param session session to notify
+     * @param msgId message number available
+     * @param size size of the message - why it's a long and not an int is a mystery
+     * @param proto 1-254 or 0 for unspecified
+     * @param fromport 1-65535 or 0 for unspecified
+     * @param toport 1-65535 or 0 for unspecified
+     */
+    void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport);
+
+    /** Instruct the client that the session specified seems to be under attack
+     * and that the client may wish to move its destination to another router.
+     * @param session session to report abuse to
+     * @param severity how bad the abuse is
+     */
+    void reportAbuse(I2PSession session, int severity);
+
+    /**
+     * Notify the client that the session has been terminated
+     *
+     */
+    void disconnected(I2PSession session);
+
+    /**
+     * Notify the client that some error occurred
+     *
+     */
+    void errorOccurred(I2PSession session, String message, Throwable error);
+}
diff --git a/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java b/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java
index 9fa227f817be8050c5a939f3289a067095e50007..054bd9d8f10c03eff4d0df7bc6f4ecba58a5dc0b 100644
--- a/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java
+++ b/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java
@@ -16,8 +16,10 @@ import java.util.Set;
 import java.util.StringTokenizer;
 
 import net.i2p.I2PAppContext;
+import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Destination;
+import net.i2p.data.Hash;
 import net.i2p.util.Log;
 
 /**
@@ -135,4 +137,34 @@ public class HostsTxtNamingService extends NamingService {
         }
         return null;
     }
+
+    @Override
+    public String reverseLookup(Hash h) {
+        List filenames = getFilenames();
+        for (int i = 0; i < filenames.size(); i++) { 
+            String hostsfile = (String)filenames.get(i);
+            Properties hosts = new Properties();
+            try {
+                File f = new File(hostsfile);
+                if ( (f.exists()) && (f.canRead()) ) {
+                    DataHelper.loadProps(hosts, f, true);
+                    Set keyset = hosts.keySet();
+                    Iterator iter = keyset.iterator();
+                    while (iter.hasNext()) {
+                        String host = (String)iter.next();
+                        String key = hosts.getProperty(host);
+                        try {
+                            Destination destkey = new Destination();
+                            destkey.fromBase64(key);
+                            if (h.equals(destkey.calculateHash()))
+                                return host;
+                        } catch (DataFormatException dfe) {}
+                    }
+                }
+            } catch (Exception ioe) {
+                _log.error("Error loading hosts file " + hostsfile, ioe);
+            }
+        }
+        return null;
+    }
 }
diff --git a/core/java/src/net/i2p/client/naming/NamingService.java b/core/java/src/net/i2p/client/naming/NamingService.java
index 5b61b1bcf811aa04f02d0a6c36c66a8f95cfb6ba..ee02ec911138ca2e5e3f3094be678f85140f5e09 100644
--- a/core/java/src/net/i2p/client/naming/NamingService.java
+++ b/core/java/src/net/i2p/client/naming/NamingService.java
@@ -16,6 +16,7 @@ import java.util.Map;
 import net.i2p.I2PAppContext;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.Destination;
+import net.i2p.data.Hash;
 import net.i2p.util.Log;
 
 /**
@@ -61,6 +62,7 @@ public abstract class NamingService {
      * <code>null</code> if no reverse lookup is possible.
      */
     public abstract String reverseLookup(Destination dest);
+    public String reverseLookup(Hash h) { return null; };
 
     /**
      * Check if host name is valid Base64 encoded dest and return this
diff --git a/core/java/src/net/i2p/stat/StatManager.java b/core/java/src/net/i2p/stat/StatManager.java
index 4c5c69c7911a144b5d8641b823dbaeb7b2ddfd8e..56af55f71d6e3c5fa4f956977bc84971f93feb4d 100644
--- a/core/java/src/net/i2p/stat/StatManager.java
+++ b/core/java/src/net/i2p/stat/StatManager.java
@@ -1,5 +1,6 @@
 package net.i2p.stat;
 
+import java.text.Collator;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -178,7 +179,7 @@ public class StatManager {
 
     /** Group name (String) to a Set of stat names, ordered alphabetically */
     public Map getStatsByGroup() {
-        Map groups = new TreeMap();
+        Map groups = new TreeMap(Collator.getInstance());
         for (Iterator iter = _frequencyStats.values().iterator(); iter.hasNext();) {
             FrequencyStat stat = (FrequencyStat) iter.next();
             if (!groups.containsKey(stat.getGroupName())) groups.put(stat.getGroupName(), new TreeSet());
diff --git a/core/java/src/net/i2p/util/ConvertToHash.java b/core/java/src/net/i2p/util/ConvertToHash.java
new file mode 100644
index 0000000000000000000000000000000000000000..0878556400b539963a42eb359290ad3ac6d11d9a
--- /dev/null
+++ b/core/java/src/net/i2p/util/ConvertToHash.java
@@ -0,0 +1,76 @@
+package net.i2p.util;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.Base32;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.Destination;
+import net.i2p.data.Hash;
+
+/**
+ *  Convert any kind of destination String to a hash
+ *  Supported:
+ *    Base64 dest
+ *    Base64 dest.i2p
+ *    Base64 Hash
+ *    Base32 Hash
+ *    Base32 desthash.b32.i2p
+ *    example.i2p
+ *
+ *  @return null on failure
+ *
+ *  @author zzz
+ */
+public class ConvertToHash {
+    
+    public static Hash getHash(String peer) {
+        if (peer == null)
+            return null;
+        Hash h = new Hash();
+        String peerLC = peer.toLowerCase();
+        // b64 hash
+        if (peer.length() == 44 && !peerLC.endsWith(".i2p")) {
+            try {
+                h.fromBase64(peer);
+            } catch (DataFormatException dfe) {}
+        }
+        // b64 dest.i2p
+        if (h.getData() == null && peer.length() >= 520 && peerLC.endsWith(".i2p")) {
+            try {
+                Destination d = new Destination();
+                d.fromBase64(peer.substring(0, peer.length() - 4));
+                h = d.calculateHash();
+            } catch (DataFormatException dfe) {}
+        }
+        // b64 dest
+        if (h.getData() == null && peer.length() >= 516 && !peerLC.endsWith(".i2p")) {
+            try {
+                Destination d = new Destination();
+                d.fromBase64(peer);
+                h = d.calculateHash();
+            } catch (DataFormatException dfe) {}
+        }
+        // b32 hash.b32.i2p
+        // do this here rather than in naming service so it will work
+        // even if the leaseset is not found
+        if (h.getData() == null && peer.length() == 60 && peerLC.endsWith(".b32.i2p")) {
+            byte[] b = Base32.decode(peer.substring(0, 52));
+            if (b != null && b.length == Hash.HASH_LENGTH)
+                h.setData(b);
+        }
+        // b32 hash
+        if (h.getData() == null && peer.length() == 52 && !peerLC.endsWith(".i2p")) {
+            byte[] b = Base32.decode(peer);
+            if (b != null && b.length == Hash.HASH_LENGTH)
+                h.setData(b);
+        }
+        // example.i2p
+        if (h.getData() == null) {
+            Destination d = I2PAppContext.getGlobalContext().namingService().lookup(peer);
+            if (d != null)
+                h = d.calculateHash();
+        }
+        if (h.getData() == null)
+            return null;
+        return h;
+    }
+}
diff --git a/core/java/src/net/i2p/util/NativeBigInteger.java b/core/java/src/net/i2p/util/NativeBigInteger.java
index 7a64e24e42e1b7a563e03109e5fd1061699d787e..970de52c85d61dfd3cd40fc8dcce04ed55705d4a 100644
--- a/core/java/src/net/i2p/util/NativeBigInteger.java
+++ b/core/java/src/net/i2p/util/NativeBigInteger.java
@@ -23,6 +23,9 @@ import freenet.support.CPUInformation.CPUInfo;
 import freenet.support.CPUInformation.IntelCPUInfo;
 import freenet.support.CPUInformation.UnknownCPUException;
 
+import net.i2p.I2PAppContext;
+import net.i2p.util.Log;
+
 /**
  * <p>BigInteger that takes advantage of the jbigi library for the modPow operation,
  * which accounts for a massive segment of the processing cost of asymmetric 
@@ -89,6 +92,9 @@ public class NativeBigInteger extends BigInteger {
      * do we want to dump some basic success/failure info to stderr during 
      * initialization?  this would otherwise use the Log component, but this makes
      * it easier for other systems to reuse this class
+     *
+     * Well, we really want to use Log so if you are one of those "other systems"
+     * then comment out the I2PAppContext usage below.
      */
     private static final boolean _doLog = System.getProperty("jbigi.dontLog") == null;
     
@@ -401,38 +407,32 @@ public class NativeBigInteger extends BigInteger {
             boolean loaded = loadGeneric("jbigi");
             if (loaded) {
                 _nativeOk = true;
-                if (_doLog)
-                    System.err.println("INFO: Locally optimized native BigInteger loaded from the library path");
+                info("Locally optimized native BigInteger library loaded from the library path");
             } else {
                 loaded = loadFromResource("jbigi");
                 if (loaded) {
                     _nativeOk = true;
-                    if (_doLog)
-                        System.err.println("INFO: Locally optimized native BigInteger loaded from resource");
+                    info("Locally optimized native BigInteger library loaded from resource");
                 } else {
                     loaded = loadFromResource(true);
                     if (loaded) {
                         _nativeOk = true;
-                        if (_doLog)
-                            System.err.println("INFO: Optimized native BigInteger library '"+getResourceName(true)+"' loaded from resource");
+                        info("Optimized native BigInteger library '"+getResourceName(true)+"' loaded from resource");
                     } else {
                         loaded = loadGeneric(true);
                         if (loaded) {
                             _nativeOk = true;
-                            if (_doLog)
-                                System.err.println("INFO: Optimized native BigInteger library '"+getMiddleName(true)+"' loaded from somewhere in the path");
+                            info("Optimized native BigInteger library '"+getMiddleName(true)+"' loaded from somewhere in the path");
                         } else {
                             loaded = loadFromResource(false);
                             if (loaded) {
                                 _nativeOk = true;
-                                if (_doLog)
-                                    System.err.println("INFO: Non-optimized native BigInteger library '"+getResourceName(false)+"' loaded from resource");
+                                info("Non-optimized native BigInteger library '"+getResourceName(false)+"' loaded from resource");
                             } else {
                                 loaded = loadGeneric(false);
                                 if (loaded) {
                                     _nativeOk = true;
-                                    if (_doLog)
-                                        System.err.println("INFO: Non-optimized native BigInteger library '"+getMiddleName(false)+"' loaded from somewhere in the path");
+                                    info("Non-optimized native BigInteger library '"+getMiddleName(false)+"' loaded from somewhere in the path");
                                 } else {
                                     _nativeOk = false;          
                                 }
@@ -442,16 +442,27 @@ public class NativeBigInteger extends BigInteger {
                 }
             }
         }
-        if (_doLog && !_nativeOk)
-            System.err.println("INFO: Native BigInteger library jbigi not loaded - using pure java");
+        if (!_nativeOk) {
+            warn("Native BigInteger library jbigi not loaded - using pure Java - " +
+                 "poor performance may result - see http://www.i2p2.i2p/jbigi.html for help");
+        }
         }catch(Exception e){
-            if (_doLog) {
-                System.err.println("INFO: Native BigInteger library jbigi not loaded, reason: '"+e.getMessage()+"' - using pure java");
-                e.printStackTrace();
-            }
+            warn("Native BigInteger library jbigi not loaded, reason: '"+e.getMessage()+"' - using pure java");
         }
     }
     
+    private static void info(String s) {
+        if(_doLog)
+            System.err.println("INFO: " + s);
+        I2PAppContext.getGlobalContext().logManager().getLog(NativeBigInteger.class).info(s);
+    }
+
+    private static void warn(String s) {
+        if(_doLog)
+            System.err.println("WARNING: " + s);
+        I2PAppContext.getGlobalContext().logManager().getLog(NativeBigInteger.class).warn(s);
+    }
+
     /** 
      * <p>Try loading it from an explictly build jbigi.dll / libjbigi.so first, before 
      * looking into a jbigi.jar for any other libraries.</p>
diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java
index 033678924c63475de3ae6ac0d3083fc6dbdffb71..13e801458cd2ac0453e85c53fa5750d0ee0ccbb0 100644
--- a/router/java/src/net/i2p/router/Router.java
+++ b/router/java/src/net/i2p/router/Router.java
@@ -65,7 +65,6 @@ public class Router {
     private I2PThread.OOMEventListener _oomListener;
     private ShutdownHook _shutdownHook;
     private I2PThread _gracefulShutdownDetector;
-    private Set _shutdownTasks;
     
     public final static String PROP_CONFIG_FILE = "router.configLocation";
     
@@ -171,7 +170,6 @@ public class Router {
         watchdog.setDaemon(true);
         watchdog.start();
         
-        _shutdownTasks = new HashSet(0);
     }
     
     /**
@@ -446,13 +444,14 @@ public class Router {
      */
     private static final String _rebuildFiles[] = new String[] { "router.info", 
                                                                  "router.keys",
-                                                                 "netDb/my.info",
-                                                                 "connectionTag.keys",
+                                                                 "netDb/my.info",      // no longer used
+                                                                 "connectionTag.keys", // never used?
                                                                  "keyBackup/privateEncryption.key",
                                                                  "keyBackup/privateSigning.key",
                                                                  "keyBackup/publicEncryption.key",
                                                                  "keyBackup/publicSigning.key",
-                                                                 "sessionKeys.dat" };
+                                                                 "sessionKeys.dat"     // no longer used
+                                                               };
 
     static final String IDENTLOG = "identlog.txt";
     public static void killKeys() {
@@ -490,13 +489,12 @@ public class Router {
      */
     public void rebuildNewIdentity() {
         killKeys();
-        try {
-            for (Iterator iter = _shutdownTasks.iterator(); iter.hasNext(); ) {
-                Runnable task = (Runnable)iter.next();
+        for (Runnable task : _context.getShutdownTasks()) {
+            try {
                 task.run();
+            } catch (Throwable t) {
+                _log.log(Log.CRIT, "Error running shutdown task", t);
             }
-        } catch (Throwable t) {
-            _log.log(Log.CRIT, "Error running shutdown task", t);
         }
         // hard and ugly
         finalShutdown(EXIT_HARD_RESTART);
@@ -781,12 +779,6 @@ public class Router {
         buf.setLength(0);
     }
     
-    public void addShutdownTask(Runnable task) {
-        synchronized (_shutdownTasks) {
-            _shutdownTasks.add(task);
-        }
-    }
-    
     public static final int EXIT_GRACEFUL = 2;
     public static final int EXIT_HARD = 3;
     public static final int EXIT_OOM = 10;
@@ -799,13 +791,12 @@ public class Router {
         I2PThread.removeOOMEventListener(_oomListener);
         // Run the shutdown hooks first in case they want to send some goodbye messages
         // Maybe we need a delay after this too?
-        try {
-            for (Iterator iter = _shutdownTasks.iterator(); iter.hasNext(); ) {
-                Runnable task = (Runnable)iter.next();
+        for (Runnable task : _context.getShutdownTasks()) {
+            try {
                 task.run();
+            } catch (Throwable t) {
+                _log.log(Log.CRIT, "Error running shutdown task", t);
             }
-        } catch (Throwable t) {
-            _log.log(Log.CRIT, "Error running shutdown task", t);
         }
         try { _context.clientManager().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the client manager", t); }
         try { _context.jobQueue().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the job queue", t); }
@@ -859,6 +850,10 @@ public class Router {
     public void shutdownGracefully() {
         shutdownGracefully(EXIT_GRACEFUL);
     }
+    /**
+     * Call this with EXIT_HARD or EXIT_HARD_RESTART for a non-blocking,
+     * hard, non-graceful shutdown with a brief delay to allow a UI response
+     */
     public void shutdownGracefully(int exitCode) {
         _gracefulExitCode = exitCode;
         _config.setProperty(PROP_SHUTDOWN_IN_PROGRESS, "true");
@@ -887,7 +882,9 @@ public class Router {
     }
     /** How long until the graceful shutdown will kill us?  */
     public long getShutdownTimeRemaining() {
-        if (_gracefulExitCode <= 0) return -1;
+        if (_gracefulExitCode <= 0) return -1; // maybe Long.MAX_VALUE would be better?
+        if (_gracefulExitCode == EXIT_HARD || _gracefulExitCode == EXIT_HARD_RESTART)
+            return 0;
         long exp = _context.tunnelManager().getLastParticipatingExpiration();
         if (exp < 0)
             return -1;
@@ -906,9 +903,20 @@ public class Router {
             while (true) {
                 boolean shutdown = (null != _config.getProperty(PROP_SHUTDOWN_IN_PROGRESS));
                 if (shutdown) {
-                    if (_context.tunnelManager().getParticipatingCount() <= 0) {
-                        if (_log.shouldLog(Log.CRIT))
+                    if (_gracefulExitCode == EXIT_HARD || _gracefulExitCode == EXIT_HARD_RESTART ||
+                        _context.tunnelManager().getParticipatingCount() <= 0) {
+                        if (_gracefulExitCode == EXIT_HARD)
+                            _log.log(Log.CRIT, "Shutting down after a brief delay");
+                        else if (_gracefulExitCode == EXIT_HARD_RESTART)
+                            _log.log(Log.CRIT, "Restarting after a brief delay");
+                        else
                             _log.log(Log.CRIT, "Graceful shutdown progress - no more tunnels, safe to die");
+                        // Allow time for a UI reponse
+                        try {
+                            synchronized (Thread.currentThread()) {
+                                Thread.currentThread().wait(2*1000);
+                            }
+                        } catch (InterruptedException ie) {}
                         shutdown(_gracefulExitCode);
                         return;
                     } else {
diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
index 0515d5c34459e84384b6fdd17885880e0201fd43..0e858ef779e630b9718523bb7ffc6944cbbbfd5e 100644
--- a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
+++ b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
@@ -34,6 +34,8 @@ import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
 import net.i2p.router.TunnelInfo;
 import net.i2p.util.Log;
+import net.i2p.util.SimpleScheduler;
+import net.i2p.util.SimpleTimer;
 
 /**
  * Send a client message out a random outbound tunnel and into a random inbound
@@ -98,6 +100,10 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
      */
     private static final int BUNDLE_PROBABILITY_DEFAULT = 100;
     
+    private static final Object _initializeLock = new Object();
+    private static boolean _initialized = false;
+    private static final int CLEAN_INTERVAL = 5*60*1000;
+
     /**
      * Send the sucker
      */
@@ -105,20 +111,26 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
         super(ctx);
         _log = ctx.logManager().getLog(OutboundClientMessageOneShotJob.class);
         
-        ctx.statManager().createFrequencyStat("client.sendMessageFailFrequency", "How often does a client fail to send a message?", "ClientMessages", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l });
-        ctx.statManager().createRateStat("client.sendMessageSize", "How large are messages sent by the client?", "ClientMessages", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l });
-        ctx.statManager().createRateStat("client.sendAckTime", "Message round trip time", "ClientMessages", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
-        ctx.statManager().createRateStat("client.timeoutCongestionTunnel", "How lagged our tunnels are when a send times out?", "ClientMessages", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
-        ctx.statManager().createRateStat("client.timeoutCongestionMessage", "How fast we process messages locally when a send times out?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
-        ctx.statManager().createRateStat("client.timeoutCongestionInbound", "How much faster we are receiving data than our average bps when a send times out?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
-        ctx.statManager().createRateStat("client.leaseSetFoundLocally", "How often we tried to look for a leaseSet and found it locally?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
-        ctx.statManager().createRateStat("client.leaseSetFoundRemoteTime", "How long we tried to look for a remote leaseSet (when we succeeded)?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
-        ctx.statManager().createRateStat("client.leaseSetFailedRemoteTime", "How long we tried to look for a remote leaseSet (when we failed)?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
-        ctx.statManager().createRateStat("client.dispatchPrepareTime", "How long until we've queued up the dispatch job (since we started)?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
-        ctx.statManager().createRateStat("client.dispatchTime", "How long until we've dispatched the message (since we started)?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
-        ctx.statManager().createRateStat("client.dispatchSendTime", "How long the actual dispatching takes?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
-        ctx.statManager().createRateStat("client.dispatchNoTunnels", "How long after start do we run out of tunnels to send/receive with?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
-        ctx.statManager().createRateStat("client.dispatchNoACK", "Repeated message sends to a peer (no ack required)", "ClientMessages", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l });
+        synchronized (_initializeLock) {
+            if (!_initialized) {
+                SimpleScheduler.getInstance().addPeriodicEvent(new OCMOSJCacheCleaner(ctx), CLEAN_INTERVAL, CLEAN_INTERVAL);
+                ctx.statManager().createFrequencyStat("client.sendMessageFailFrequency", "How often does a client fail to send a message?", "ClientMessages", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l });
+                ctx.statManager().createRateStat("client.sendMessageSize", "How large are messages sent by the client?", "ClientMessages", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l });
+                ctx.statManager().createRateStat("client.sendAckTime", "Message round trip time", "ClientMessages", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
+                ctx.statManager().createRateStat("client.timeoutCongestionTunnel", "How lagged our tunnels are when a send times out?", "ClientMessages", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
+                ctx.statManager().createRateStat("client.timeoutCongestionMessage", "How fast we process messages locally when a send times out?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
+                ctx.statManager().createRateStat("client.timeoutCongestionInbound", "How much faster we are receiving data than our average bps when a send times out?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
+                ctx.statManager().createRateStat("client.leaseSetFoundLocally", "How often we tried to look for a leaseSet and found it locally?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
+                ctx.statManager().createRateStat("client.leaseSetFoundRemoteTime", "How long we tried to look for a remote leaseSet (when we succeeded)?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
+                ctx.statManager().createRateStat("client.leaseSetFailedRemoteTime", "How long we tried to look for a remote leaseSet (when we failed)?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
+                ctx.statManager().createRateStat("client.dispatchPrepareTime", "How long until we've queued up the dispatch job (since we started)?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
+                ctx.statManager().createRateStat("client.dispatchTime", "How long until we've dispatched the message (since we started)?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
+                ctx.statManager().createRateStat("client.dispatchSendTime", "How long the actual dispatching takes?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
+                ctx.statManager().createRateStat("client.dispatchNoTunnels", "How long after start do we run out of tunnels to send/receive with?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l });
+                ctx.statManager().createRateStat("client.dispatchNoACK", "Repeated message sends to a peer (no ack required)", "ClientMessages", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l });
+                _initialized = true;
+            }
+        }
         long timeoutMs = OVERALL_TIMEOUT_MS_DEFAULT;
         _clientMessage = msg;
         _clientMessageId = msg.getMessageId();
@@ -201,7 +213,6 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
       * Key the cache on the source+dest pair.
       */
     private static HashMap _leaseSetCache = new HashMap();
-    private static long _lscleanTime = 0;
     private LeaseSet getReplyLeaseSet(boolean force) {
         LeaseSet newLS = getContext().netDb().lookupLeaseSetLocally(_from.calculateHash());
         if (newLS == null)
@@ -235,10 +246,6 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
         // If the last leaseSet we sent him is still good, don't bother sending again
         long now = getContext().clock().now();
         synchronized (_leaseSetCache) {
-            if (now - _lscleanTime > 5*60*1000) {  // clean out periodically
-                cleanLeaseSetCache(_leaseSetCache);
-                _lscleanTime = now;
-            }
             if (!force) {
                 LeaseSet ls = (LeaseSet) _leaseSetCache.get(hashPair());
                 if (ls != null) {
@@ -306,7 +313,6 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
      *
      */
     private static HashMap _leaseCache = new HashMap();
-    private static long _lcleanTime = 0;
     private boolean getNextLease() {
         _leaseSet = getContext().netDb().lookupLeaseSetLocally(_to.calculateHash());
         if (_leaseSet == null) {
@@ -319,10 +325,6 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
         // Use the same lease if it's still good
         // Even if _leaseSet changed, _leaseSet.getEncryptionKey() didn't...
         synchronized (_leaseCache) {
-            if (now - _lcleanTime > 5*60*1000) {  // clean out periodically
-                cleanLeaseCache(_leaseCache);
-                _lcleanTime = now;
-            }
             _lease = (Lease) _leaseCache.get(hashPair());
             if (_lease != null) {
                 // if outbound tunnel length == 0 && lease.firsthop.isBacklogged() don't use it ??
@@ -607,7 +609,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
      * (needed for cleanTunnelCache)
      * 44 = 32 * 4 / 3
      */
-    private Hash sourceFromHashPair(String s) {
+    private static Hash sourceFromHashPair(String s) {
         return new Hash(Base64.decode(s.substring(44, 88)));
     }
 
@@ -648,8 +650,8 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
      * Clean out old leaseSets from a set.
      * Caller must synchronize on tc.
      */
-    private void cleanLeaseSetCache(HashMap tc) {
-        long now = getContext().clock().now();
+    private static void cleanLeaseSetCache(RouterContext ctx, HashMap tc) {
+        long now = ctx.clock().now();
         List deleteList = new ArrayList();
         for (Iterator iter = tc.entrySet().iterator(); iter.hasNext(); ) {
             Map.Entry entry = (Map.Entry)iter.next();
@@ -668,7 +670,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
      * Clean out old leases from a set.
      * Caller must synchronize on tc.
      */
-    private void cleanLeaseCache(HashMap tc) {
+    private static void cleanLeaseCache(HashMap tc) {
         List deleteList = new ArrayList();
         for (Iterator iter = tc.entrySet().iterator(); iter.hasNext(); ) {
             Map.Entry entry = (Map.Entry)iter.next();
@@ -687,13 +689,13 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
      * Clean out old tunnels from a set.
      * Caller must synchronize on tc.
      */
-    private void cleanTunnelCache(HashMap tc) {
+    private static void cleanTunnelCache(RouterContext ctx, HashMap tc) {
         List deleteList = new ArrayList();
         for (Iterator iter = tc.entrySet().iterator(); iter.hasNext(); ) {
             Map.Entry entry = (Map.Entry)iter.next();
             String k = (String) entry.getKey();
             TunnelInfo tunnel = (TunnelInfo) entry.getValue();
-            if (!getContext().tunnelManager().isValidTunnel(sourceFromHashPair(k), tunnel))
+            if (!ctx.tunnelManager().isValidTunnel(sourceFromHashPair(k), tunnel))
                 deleteList.add(k);
         }
         for (Iterator iter = deleteList.iterator(); iter.hasNext(); ) {
@@ -702,6 +704,25 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
         }
     }
 
+    private static class OCMOSJCacheCleaner implements SimpleTimer.TimedEvent {
+        private RouterContext _ctx;
+        private OCMOSJCacheCleaner(RouterContext ctx) {
+            _ctx = ctx;
+        }
+        public void timeReached() {
+            synchronized(_leaseSetCache) {
+                cleanLeaseSetCache(_ctx, _leaseSetCache);
+            }
+            synchronized(_leaseCache) {
+                cleanLeaseCache(_leaseCache);
+            }
+            synchronized(_tunnelCache) {
+                cleanTunnelCache(_ctx, _tunnelCache);
+                cleanTunnelCache(_ctx, _backloggedTunnelCache);
+            }
+        }
+    }
+
     /**
      * Use the same outbound tunnel as we did for the same destination previously,
      * if possible, to keep the streaming lib happy
@@ -712,16 +733,10 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
      */
     private static HashMap _tunnelCache = new HashMap();
     private static HashMap _backloggedTunnelCache = new HashMap();
-    private static long _cleanTime = 0;
     private TunnelInfo selectOutboundTunnel(Destination to) {
         TunnelInfo tunnel;
         long now = getContext().clock().now();
         synchronized (_tunnelCache) {
-            if (now - _cleanTime > 5*60*1000) {  // clean out periodically
-                cleanTunnelCache(_tunnelCache);
-                cleanTunnelCache(_backloggedTunnelCache);
-                _cleanTime = now;
-            }
             /**
              * If old tunnel is valid and no longer backlogged, use it.
              * This prevents an active anonymity attack, where a peer could tell