diff --git a/core/java/src/net/i2p/crypto/DHSessionKeyBuilder.java b/core/java/src/net/i2p/crypto/DHSessionKeyBuilder.java
index b3c5813f13bf05746f0092b65365166dd5d1b69f..a96a0846a08ab5eb65c73de16743df750f7e74dd 100644
--- a/core/java/src/net/i2p/crypto/DHSessionKeyBuilder.java
+++ b/core/java/src/net/i2p/crypto/DHSessionKeyBuilder.java
@@ -247,6 +247,23 @@ public class DHSessionKeyBuilder {
         if (_myPublicValue == null) _myPublicValue = generateMyValue();
         return _myPublicValue;
     }
+    /**
+     * Return a 256 byte representation of our public key, with leading 0s 
+     * if necessary.
+     *
+     */
+    public byte[] getMyPublicValueBytes() {
+        BigInteger bi = getMyPublicValue();
+        byte data[] = bi.toByteArray();
+        byte rv[] = new byte[256];
+        if (data.length == 257) // high byte has the sign bit
+            System.arraycopy(data, 1, rv, 0, rv.length);
+        else if (data.length == 256)
+            System.arraycopy(data, 0, rv, 0, rv.length);
+        else
+            System.arraycopy(data, 0, rv, rv.length-data.length, data.length);
+        return rv;
+    }
 
     /**
      * Specify the value given by the peer for use in the session key negotiation
@@ -255,6 +272,20 @@ public class DHSessionKeyBuilder {
     public void setPeerPublicValue(BigInteger peerVal) {
         _peerValue = peerVal;
     }
+    public void setPeerPublicValue(byte val[]) {
+        if (val.length != 256)
+            throw new IllegalArgumentException("Peer public value must be exactly 256 bytes");
+
+        if (1 == (val[0] & 0x80)) {
+            // high bit set, need to inject an additional byte to keep 2s complement
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("High bit set");
+            byte val2[] = new byte[257];
+            System.arraycopy(val, 0, val2, 1, 256);
+            val = val2;
+        }
+        _peerValue = new NativeBigInteger(val);
+    }
 
     public BigInteger getPeerPublicValue() {
         return _peerValue;
diff --git a/router/java/src/net/i2p/data/i2np/I2NPMessage.java b/router/java/src/net/i2p/data/i2np/I2NPMessage.java
index 59d7fe5974526b95c8298cfcd6af0ced2396c1dd..dd4325fb15ec49befaa56f7229a3b8ce14fafb45 100644
--- a/router/java/src/net/i2p/data/i2np/I2NPMessage.java
+++ b/router/java/src/net/i2p/data/i2np/I2NPMessage.java
@@ -61,6 +61,7 @@ public interface I2NPMessage extends DataStructure {
      * Replay resistent message Id
      */
     public long getUniqueId(); 
+    public void setUniqueId(long id);
     
     /**
      * Date after which the message should be dropped (and the associated uniqueId forgotten)
@@ -72,7 +73,20 @@ public interface I2NPMessage extends DataStructure {
     
     /** How large the message is, including any checksums */
     public int getMessageSize();
+    /** How large the raw message is */
+    public int getRawMessageSize();
+
     
-    /** write the message to the buffer, returning the number of bytes written */
+    /** 
+     * write the message to the buffer, returning the number of bytes written.
+     * the data is formatted so as to be self contained, with the type, size,
+     * expiration, unique id, as well as a checksum bundled along.  
+     */
     public int toByteArray(byte buffer[]);
+    /**
+     * write the message to the buffer, returning the number of bytes written.
+     * the data is is not self contained - it does not include the size,
+     * unique id, or any checksum, but does include the type and expiration.
+     */
+    public int toRawByteArray(byte buffer[]);
 }
diff --git a/router/java/src/net/i2p/data/i2np/I2NPMessageHandler.java b/router/java/src/net/i2p/data/i2np/I2NPMessageHandler.java
index 1f5d6bdfabbdab1bea8b51c9f6b52814701172af..3ef157ec7491b0bf86e12293dc09b6ba42367b19 100644
--- a/router/java/src/net/i2p/data/i2np/I2NPMessageHandler.java
+++ b/router/java/src/net/i2p/data/i2np/I2NPMessageHandler.java
@@ -49,7 +49,7 @@ public class I2NPMessageHandler {
         try {
             int type = (int)DataHelper.readLong(in, 1);
             _lastReadBegin = System.currentTimeMillis();
-            I2NPMessage msg = createMessage(type);
+            I2NPMessage msg = I2NPMessageImpl.createMessage(_context, type);
             if (msg == null)
                 throw new I2NPMessageException("The type "+ type + " is an unknown I2NP message");
             try {
@@ -94,7 +94,7 @@ public class I2NPMessageHandler {
         int type = (int)DataHelper.fromLong(data, cur, 1);
         cur++;
         _lastReadBegin = System.currentTimeMillis();
-        I2NPMessage msg = createMessage(type);
+        I2NPMessage msg = I2NPMessageImpl.createMessage(_context, type);
         if (msg == null)
             throw new I2NPMessageException("The type "+ type + " is an unknown I2NP message");
         try {
@@ -118,39 +118,6 @@ public class I2NPMessageHandler {
     public long getLastReadTime() { return _lastReadEnd - _lastReadBegin; }
     public int getLastSize() { return _lastSize; }
     
-    /**
-     * Yes, this is fairly ugly, but its the only place it ever happens.
-     *
-     */
-    private I2NPMessage createMessage(int type) throws I2NPMessageException {
-        switch (type) {
-            case DatabaseStoreMessage.MESSAGE_TYPE:
-                return new DatabaseStoreMessage(_context);
-            case DatabaseLookupMessage.MESSAGE_TYPE:
-                return new DatabaseLookupMessage(_context);
-            case DatabaseSearchReplyMessage.MESSAGE_TYPE:
-                return new DatabaseSearchReplyMessage(_context);
-            case DeliveryStatusMessage.MESSAGE_TYPE:
-                return new DeliveryStatusMessage(_context);
-            case DateMessage.MESSAGE_TYPE:
-                return new DateMessage(_context);
-            case GarlicMessage.MESSAGE_TYPE:
-                return new GarlicMessage(_context);
-            case TunnelDataMessage.MESSAGE_TYPE:
-                return new TunnelDataMessage(_context);
-            case TunnelGatewayMessage.MESSAGE_TYPE:
-                return new TunnelGatewayMessage(_context);
-            case DataMessage.MESSAGE_TYPE:
-                return new DataMessage(_context);
-            case TunnelCreateMessage.MESSAGE_TYPE:
-                return new TunnelCreateMessage(_context);
-            case TunnelCreateStatusMessage.MESSAGE_TYPE:
-                return new TunnelCreateStatusMessage(_context);
-            default:
-                return null;
-        }
-    }
-    
     public static void main(String args[]) {
         try {
             I2NPMessage msg = new I2NPMessageHandler(I2PAppContext.getGlobalContext()).readMessage(new FileInputStream(args[0]));
diff --git a/router/java/src/net/i2p/data/i2np/I2NPMessageImpl.java b/router/java/src/net/i2p/data/i2np/I2NPMessageImpl.java
index cea2087b9ad6c43f8beae488bd25be3da1bf0b19..2e6f09149c6705acb22b8839e8b9f0e61a6fb844 100644
--- a/router/java/src/net/i2p/data/i2np/I2NPMessageImpl.java
+++ b/router/java/src/net/i2p/data/i2np/I2NPMessageImpl.java
@@ -35,6 +35,8 @@ public abstract class I2NPMessageImpl extends DataStructureImpl implements I2NPM
     public final static long DEFAULT_EXPIRATION_MS = 1*60*1000; // 1 minute by default
     public final static int CHECKSUM_LENGTH = 1; //Hash.HASH_LENGTH;
     
+    private static final boolean RAW_FULL_SIZE = true;
+    
     public I2NPMessageImpl(I2PAppContext context) {
         _context = context;
         _log = context.logManager().getLog(I2NPMessageImpl.class);
@@ -165,7 +167,13 @@ public abstract class I2NPMessageImpl extends DataStructureImpl implements I2NPM
     public void setMessageExpiration(long exp) { _expiration = exp; }
     
     public synchronized int getMessageSize() { 
-        return calculateWrittenLength()+15 + CHECKSUM_LENGTH; // 47 bytes in the header
+        return calculateWrittenLength()+15 + CHECKSUM_LENGTH; // 16 bytes in the header
+    }
+    public synchronized int getRawMessageSize() { 
+        if (RAW_FULL_SIZE) 
+            return getMessageSize();
+        else
+            return calculateWrittenLength()+5;
     }
     
     public byte[] toByteArray() {
@@ -248,4 +256,83 @@ public abstract class I2NPMessageImpl extends DataStructureImpl implements I2NPM
         return curIndex;
     }
      */
+
+    
+    public int toRawByteArray(byte buffer[]) {
+        if (RAW_FULL_SIZE)
+            return toByteArray(buffer);
+        try {
+            int off = 0;
+            DataHelper.toLong(buffer, off, 1, getType());
+            off += 1;
+            DataHelper.toLong(buffer, off, 4, _expiration/1000); // seconds
+            off += 4;
+            return writeMessageBody(buffer, off);
+        } catch (I2NPMessageException ime) {
+            _context.logManager().getLog(getClass()).log(Log.CRIT, "Error writing", ime);
+            throw new IllegalStateException("Unable to serialize the message (" + getClass().getName() 
+                                            + "): " + ime.getMessage());
+        }
+    }
+
+    public static I2NPMessage fromRawByteArray(I2PAppContext ctx, byte buffer[], int offset, int len) throws I2NPMessageException {
+        int type = (int)DataHelper.fromLong(buffer, offset, 1);
+        offset++;
+        I2NPMessage msg = createMessage(ctx, type);
+        if (msg == null) 
+            throw new I2NPMessageException("Unknown message type: " + type);
+        if (RAW_FULL_SIZE) {
+            try {
+                msg.readBytes(buffer, type, offset);
+            } catch (IOException ioe) {
+                throw new I2NPMessageException("Error reading the " + msg, ioe);
+            }
+            return msg;
+        }
+
+        long expiration = DataHelper.fromLong(buffer, offset, 4) * 1000; // seconds
+        offset += 4;
+        int dataSize = len - 1 - 4;
+        try {
+            msg.readMessage(buffer, offset, dataSize, type);
+            msg.setMessageExpiration(expiration);
+            return msg;
+        } catch (IOException ioe) {
+            throw new I2NPMessageException("IO error reading raw message", ioe);
+        }
+    }
+
+    
+    /**
+     * Yes, this is fairly ugly, but its the only place it ever happens.
+     *
+     */
+    public static I2NPMessage createMessage(I2PAppContext context, int type) throws I2NPMessageException {
+        switch (type) {
+            case DatabaseStoreMessage.MESSAGE_TYPE:
+                return new DatabaseStoreMessage(context);
+            case DatabaseLookupMessage.MESSAGE_TYPE:
+                return new DatabaseLookupMessage(context);
+            case DatabaseSearchReplyMessage.MESSAGE_TYPE:
+                return new DatabaseSearchReplyMessage(context);
+            case DeliveryStatusMessage.MESSAGE_TYPE:
+                return new DeliveryStatusMessage(context);
+            case DateMessage.MESSAGE_TYPE:
+                return new DateMessage(context);
+            case GarlicMessage.MESSAGE_TYPE:
+                return new GarlicMessage(context);
+            case TunnelDataMessage.MESSAGE_TYPE:
+                return new TunnelDataMessage(context);
+            case TunnelGatewayMessage.MESSAGE_TYPE:
+                return new TunnelGatewayMessage(context);
+            case DataMessage.MESSAGE_TYPE:
+                return new DataMessage(context);
+            case TunnelCreateMessage.MESSAGE_TYPE:
+                return new TunnelCreateMessage(context);
+            case TunnelCreateStatusMessage.MESSAGE_TYPE:
+                return new TunnelCreateStatusMessage(context);
+            default:
+                return null;
+        }
+    }
 }
diff --git a/router/java/src/net/i2p/router/transport/Transport.java b/router/java/src/net/i2p/router/transport/Transport.java
index 344e8427ea23b7cab33e0a8c8405233b3f3e7af3..9f3ee4e5351342c52ababafd2efa147ec63d62cb 100644
--- a/router/java/src/net/i2p/router/transport/Transport.java
+++ b/router/java/src/net/i2p/router/transport/Transport.java
@@ -8,6 +8,8 @@ package net.i2p.router.transport;
  *
  */
 
+import java.io.IOException;
+import java.io.Writer;
 import java.util.List;
 import java.util.Set;
 
@@ -38,5 +40,5 @@ public interface Transport {
     public int countActivePeers();    
     public List getMostRecentErrorMessages();
     
-    public String renderStatusHTML();
+    public void renderStatusHTML(Writer out) throws IOException;
 }
diff --git a/router/java/src/net/i2p/router/transport/TransportImpl.java b/router/java/src/net/i2p/router/transport/TransportImpl.java
index 147ad38d92a22964ef5c6fd1062d5576c80ca246..0da951571a33a2fc6e405fc3378cb0ce72a06633 100644
--- a/router/java/src/net/i2p/router/transport/TransportImpl.java
+++ b/router/java/src/net/i2p/router/transport/TransportImpl.java
@@ -8,6 +8,8 @@ package net.i2p.router.transport;
  *
  */
 
+import java.io.IOException;
+import java.io.Writer;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
@@ -365,7 +367,7 @@ public abstract class TransportImpl implements Transport {
     /** Who to notify on message availability */
     public void setListener(TransportEventListener listener) { _listener = listener; }
     /** Make this stuff pretty (only used in the old console) */
-    public String renderStatusHTML() { return null; }
+    public void renderStatusHTML(Writer out) throws IOException {}
     
     public RouterContext getContext() { return _context; }
 }
diff --git a/router/java/src/net/i2p/router/transport/TransportManager.java b/router/java/src/net/i2p/router/transport/TransportManager.java
index a03ea9c9e92d1eb5ce5abf83d29f53522b44acd9..eea745ae23dcde55472a488e71fdb3f7010581ae 100644
--- a/router/java/src/net/i2p/router/transport/TransportManager.java
+++ b/router/java/src/net/i2p/router/transport/TransportManager.java
@@ -21,6 +21,7 @@ import net.i2p.data.i2np.I2NPMessage;
 import net.i2p.router.OutNetMessage;
 import net.i2p.router.RouterContext;
 import net.i2p.router.transport.tcp.TCPTransport;
+import net.i2p.router.transport.udp.UDPTransport;
 import net.i2p.util.Log;
 
 public class TransportManager implements TransportEventListener {
@@ -29,6 +30,7 @@ public class TransportManager implements TransportEventListener {
     private RouterContext _context;
 
     private final static String PROP_DISABLE_TCP = "i2np.tcp.disable";
+    private static final boolean ENABLE_UDP = false;
     
     public TransportManager(RouterContext context) {
         _context = context;
@@ -57,6 +59,11 @@ public class TransportManager implements TransportEventListener {
             t.setListener(this);
             _transports.add(t);
         }
+        if (ENABLE_UDP) {
+            UDPTransport udp = new UDPTransport(_context);
+            udp.setListener(this);
+            _transports.add(udp);
+        }
     }
     
     public void startListening() {
@@ -172,13 +179,15 @@ public class TransportManager implements TransportEventListener {
             }   
         }
         buf.append("</pre>\n");
+        out.write(buf.toString());
         for (Iterator iter = _transports.iterator(); iter.hasNext(); ) {
             Transport t = (Transport)iter.next();
-            String str = t.renderStatusHTML();
-            if (str != null)
-                buf.append(str);
+            //String str = t.renderStatusHTML();
+            //if (str != null)
+            //    buf.append(str);
+            t.renderStatusHTML(out);
         }
-        out.write(buf.toString());
+        //out.write(buf.toString());
         out.flush();
     }
 }
diff --git a/router/java/src/net/i2p/router/transport/tcp/TCPTransport.java b/router/java/src/net/i2p/router/transport/tcp/TCPTransport.java
index 70130f32c904faebe4aae7cd413e63dc487b3448..289312c92bdd9104fd23c40c3f1490664e58f53b 100644
--- a/router/java/src/net/i2p/router/transport/tcp/TCPTransport.java
+++ b/router/java/src/net/i2p/router/transport/tcp/TCPTransport.java
@@ -1,5 +1,7 @@
 package net.i2p.router.transport.tcp;
 
+import java.io.IOException;
+import java.io.Writer;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
@@ -766,7 +768,7 @@ public class TCPTransport extends TransportImpl {
     }
     
     /** Make this stuff pretty (only used in the old console) */
-    public String renderStatusHTML() { 
+    public void renderStatusHTML(Writer out) throws IOException {
         StringBuffer buf = new StringBuffer(1024);
         synchronized (_connectionLock) {
             long offsetTotal = 0;
@@ -813,7 +815,7 @@ public class TCPTransport extends TransportImpl {
         }
         buf.append("</ul>");
         
-        return buf.toString();
+        out.write(buf.toString());
     }
 
     /**
diff --git a/router/java/src/net/i2p/router/transport/udp/ACKSender.java b/router/java/src/net/i2p/router/transport/udp/ACKSender.java
new file mode 100644
index 0000000000000000000000000000000000000000..8ec022e7c28db53964f918545f201f35690c74a6
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/ACKSender.java
@@ -0,0 +1,44 @@
+package net.i2p.router.transport.udp;
+
+import java.util.List;
+
+import net.i2p.router.RouterContext;
+import net.i2p.util.Log;
+
+/**
+ * Blocking thread that pulls peers off the inboundFragment pool and
+ * sends them any outstanding ACKs.  The logic of what peers get ACKed when
+ * is determined by the {@link InboundMessageFragments#getNextPeerToACK }
+ *
+ */
+public class ACKSender implements Runnable {
+    private RouterContext _context;
+    private Log _log;
+    private InboundMessageFragments _fragments;
+    private UDPTransport _transport;
+    private PacketBuilder _builder;
+    
+    public ACKSender(RouterContext ctx, InboundMessageFragments fragments, UDPTransport transport) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(ACKSender.class);
+        _fragments = fragments;
+        _transport = transport;
+        _builder = new PacketBuilder(_context, _transport);
+    }
+    
+    public void run() {
+        while (_fragments.isAlive()) {
+            PeerState peer = _fragments.getNextPeerToACK();
+            if (peer != null) {
+                List acks = peer.retrieveACKs();
+                if ( (acks != null) && (acks.size() > 0) ) {
+                    UDPPacket ack = _builder.buildACK(peer, acks);
+                    if (_log.shouldLog(Log.INFO))
+                        _log.info("Sending ACK for " + acks);
+                    _transport.send(ack);
+                }
+            }
+        }
+    }
+    
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..04654f6b2fcdb0a65fda5b2fb5168804f4360430
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
@@ -0,0 +1,556 @@
+package net.i2p.router.transport.udp;
+
+import java.net.InetAddress;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import net.i2p.data.RouterAddress;
+import net.i2p.data.RouterIdentity;
+import net.i2p.data.SessionKey;
+import net.i2p.data.Signature;
+import net.i2p.data.i2np.DatabaseStoreMessage;
+import net.i2p.router.OutNetMessage;
+import net.i2p.router.RouterContext;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+/**
+ * Coordinate the establishment of new sessions - both inbound and outbound.
+ * This has its own thread to add packets to the packet queue when necessary,
+ * as well as to drop any failed establishment attempts.
+ *
+ */
+public class EstablishmentManager {
+    private RouterContext _context;
+    private Log _log;
+    private UDPTransport _transport;
+    private PacketBuilder _builder;
+    /** map of host+port (String) to InboundEstablishState */
+    private Map _inboundStates;
+    /** map of host+port (String) to OutboundEstablishState */
+    private Map _outboundStates;
+    private boolean _alive;
+    private Object _activityLock;
+    private int _activity;
+    
+    public EstablishmentManager(RouterContext ctx, UDPTransport transport) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(EstablishmentManager.class);
+        _transport = transport;
+        _builder = new PacketBuilder(ctx, _transport);
+        _inboundStates = new HashMap(32);
+        _outboundStates = new HashMap(32);
+        _activityLock = new Object();
+    }
+    
+    public void startup() {
+        _alive = true;
+        I2PThread t = new I2PThread(new Establisher(), "UDP Establisher");
+        t.setDaemon(true);
+        t.start();
+    }
+    public void shutdown() { 
+        _alive = false;
+        notifyActivity();
+    }
+    
+    /**
+     * Grab the active establishing state
+     */
+    InboundEstablishState getInboundState(InetAddress fromHost, int fromPort) {
+        String from = PeerState.calculateRemoteHostString(fromHost.getAddress(), fromPort);
+        synchronized (_inboundStates) {
+            InboundEstablishState state = (InboundEstablishState)_inboundStates.get(from);
+            if ( (state == null) && (_log.shouldLog(Log.DEBUG)) )
+                _log.debug("No inbound states for " + from + ", with remaining: " + _inboundStates);
+            return state;
+        }
+    }
+    
+    OutboundEstablishState getOutboundState(InetAddress fromHost, int fromPort) {
+        String from = PeerState.calculateRemoteHostString(fromHost.getAddress(), fromPort);
+        synchronized (_outboundStates) {
+            OutboundEstablishState state = (OutboundEstablishState)_outboundStates.get(from);
+            if ( (state == null) && (_log.shouldLog(Log.DEBUG)) )
+                _log.debug("No outbound states for " + from + ", with remaining: " + _outboundStates);
+            return state;
+        }
+    }
+  
+    /**
+     * Send the message to its specified recipient by establishing a connection
+     * with them and sending it off.  This call does not block, and on failure,
+     * the message is failed.
+     *
+     */
+    public void establish(OutNetMessage msg) {
+        RouterAddress ra = msg.getTarget().getTargetAddress(_transport.getStyle());
+        if (ra == null) {
+            _transport.failed(msg);
+            return;
+        }
+        UDPAddress addr = new UDPAddress(ra);
+        InetAddress remAddr = addr.getHostAddress();
+        int port = addr.getPort();
+        String to = PeerState.calculateRemoteHostString(remAddr.getAddress(), port);
+        
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Add outobund establish state to: " + to);
+        
+        synchronized (_outboundStates) {
+            OutboundEstablishState state = (OutboundEstablishState)_outboundStates.get(to);
+            if (state == null) {
+                state = new OutboundEstablishState(_context, remAddr, port, 
+                                                   msg.getTarget().getIdentity(), 
+                                                   new SessionKey(addr.getIntroKey()));
+                _outboundStates.put(to, state);
+            }
+            state.addMessage(msg);
+        }
+        
+        notifyActivity();
+    }
+    
+    /**
+     * Got a SessionRequest (initiates an inbound establishment)
+     *
+     */
+    void receiveSessionRequest(String from, InetAddress host, int port, UDPPacketReader reader) {
+        InboundEstablishState state = null;
+        synchronized (_inboundStates) {
+            state = (InboundEstablishState)_inboundStates.get(from);
+            if (state == null) {
+                state = new InboundEstablishState(_context, host, port, _transport.getLocalPort());
+                _inboundStates.put(from, state);
+            }
+        }
+        state.receiveSessionRequest(reader.getSessionRequestReader());
+        
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Receive session request from: " + state.getRemoteHostInfo());
+        
+        notifyActivity();
+    }
+    
+    /** 
+     * got a SessionConfirmed (should only happen as part of an inbound 
+     * establishment) 
+     */
+    void receiveSessionConfirmed(String from, UDPPacketReader reader) {
+        InboundEstablishState state = null;
+        synchronized (_inboundStates) {
+            state = (InboundEstablishState)_inboundStates.get(from);
+        }
+        if (state != null) {
+            state.receiveSessionConfirmed(reader.getSessionConfirmedReader());
+            notifyActivity();
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Receive session confirmed from: " + state.getRemoteHostInfo());
+        }
+    }
+    
+    /**
+     * Got a SessionCreated (in response to our outbound SessionRequest)
+     *
+     */
+    void receiveSessionCreated(String from, UDPPacketReader reader) {
+        OutboundEstablishState state = null;
+        synchronized (_outboundStates) {
+            state = (OutboundEstablishState)_outboundStates.get(from);
+        }
+        if (state != null) {
+            state.receiveSessionCreated(reader.getSessionCreatedReader());
+            notifyActivity();
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Receive session created from: " + state.getRemoteHostInfo());
+        }
+    }
+
+    /**
+     * A data packet arrived on an outbound connection being established, which
+     * means its complete (yay!).  This is a blocking call, more than I'd like...
+     *
+     */
+    PeerState receiveData(OutboundEstablishState state) {
+        state.dataReceived();
+        synchronized (_outboundStates) {
+            _outboundStates.remove(state.getRemoteHostInfo());
+        }
+        if (_log.shouldLog(Log.INFO))
+            _log.info("Outbound established completely!  yay");
+        PeerState peer = handleCompletelyEstablished(state);
+        notifyActivity();
+        return peer;
+    }
+
+    
+    private void notifyActivity() {
+        synchronized (_activityLock) { 
+            _activity++;
+            _activityLock.notifyAll(); 
+        }
+    }
+    
+    /** kill any inbound or outbound that takes more than 30s */
+    private static final int MAX_ESTABLISH_TIME = 30*1000;
+    
+    /** 
+     * ok, fully received, add it to the established cons and queue up a
+     * netDb store to them
+     *
+     */
+    private void handleCompletelyEstablished(InboundEstablishState state) {
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Handle completely established (inbound): " + state.getRemoteHostInfo());
+        long now = _context.clock().now();
+        RouterIdentity remote = state.getConfirmedIdentity();
+        PeerState peer = new PeerState(_context);
+        peer.setCurrentCipherKey(state.getCipherKey());
+        peer.setCurrentMACKey(state.getMACKey());
+        peer.setCurrentReceiveSecond(now - (now % 1000));
+        peer.setKeyEstablishedTime(now);
+        peer.setLastReceiveTime(now);
+        peer.setLastSendTime(now);
+        peer.setRemoteAddress(state.getSentIP(), state.getSentPort());
+        peer.setRemotePeer(remote.calculateHash());
+        if (true) // for now, only support direct
+            peer.setRemoteRequiresIntroduction(false);
+        peer.setTheyRelayToUsAs(0);
+        peer.setWeRelayToThemAs(state.getSentRelayTag());
+        
+        _transport.addRemotePeerState(peer);
+        
+        sendOurInfo(peer);
+    }
+    
+    /** 
+     * ok, fully received, add it to the established cons and send any
+     * queued messages
+     *
+     */
+    private PeerState handleCompletelyEstablished(OutboundEstablishState state) {
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Handle completely established (outbound): " + state.getRemoteHostInfo());
+        long now = _context.clock().now();
+        RouterIdentity remote = state.getRemoteIdentity();
+        PeerState peer = new PeerState(_context);
+        peer.setCurrentCipherKey(state.getCipherKey());
+        peer.setCurrentMACKey(state.getMACKey());
+        peer.setCurrentReceiveSecond(now - (now % 1000));
+        peer.setKeyEstablishedTime(now);
+        peer.setLastReceiveTime(now);
+        peer.setLastSendTime(now);
+        peer.setRemoteAddress(state.getSentIP(), state.getSentPort());
+        peer.setRemotePeer(remote.calculateHash());
+        if (true) // for now, only support direct
+            peer.setRemoteRequiresIntroduction(false);
+        peer.setTheyRelayToUsAs(state.getReceivedRelayTag());
+        peer.setWeRelayToThemAs(0);
+        
+        _transport.addRemotePeerState(peer);
+        
+        sendOurInfo(peer);
+        
+        while (true) {
+            OutNetMessage msg = state.getNextQueuedMessage();
+            if (msg == null)
+                break;
+            _transport.send(msg);
+        }
+        return peer;
+    }
+    
+    private void sendOurInfo(PeerState peer) {
+        if (_log.shouldLog(Log.INFO))
+            _log.info("Publishing to the peer after confirm: " + peer);
+        
+        DatabaseStoreMessage m = new DatabaseStoreMessage(_context);
+        m.setKey(_context.routerHash());
+        m.setRouterInfo(_context.router().getRouterInfo());
+        m.setMessageExpiration(_context.clock().now() + 10*1000);
+        _transport.send(m, peer);
+    }
+    
+    private void sendCreated(InboundEstablishState state) {
+        long now = _context.clock().now();
+        if (true) // for now, don't offer to relay
+            state.setSentRelayTag(0);
+        
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Send created to: " + state.getRemoteHostInfo());
+        
+        state.generateSessionKey();
+        _transport.send(_builder.buildSessionCreatedPacket(state));
+        // if they haven't advanced to sending us confirmed packets in 5s,
+        // repeat
+        state.setNextSendTime(now + 5*1000);
+    }
+
+    private void sendRequest(OutboundEstablishState state) {
+        long now = _context.clock().now();
+        state.prepareSessionRequest();
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Send request to: " + state.getRemoteHostInfo());
+        _transport.send(_builder.buildSessionRequestPacket(state));
+        state.requestSent();
+    }
+    
+    private void sendConfirmation(OutboundEstablishState state) {
+        long now = _context.clock().now();
+        boolean valid = state.validateSessionCreated();
+        if (!valid) // validate clears fields on failure
+            return;
+        
+        // gives us the opportunity to "detect" our external addr
+        _transport.externalAddressReceived(state.getReceivedIP(), state.getReceivedPort());
+        
+        // signs if we havent signed yet
+        state.prepareSessionConfirmed();
+        
+        UDPPacket packets[] = _builder.buildSessionConfirmedPackets(state);
+        
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Send confirm to: " + state.getRemoteHostInfo());
+        
+        for (int i = 0; i < packets.length; i++)
+            _transport.send(packets[i]);
+        
+        state.confirmedPacketsSent();
+    }
+    
+    
+    /**
+     * Drive through the inbound establishment states, adjusting one of them
+     * as necessary
+     */
+    private long handleInbound() {
+        long now = _context.clock().now();
+        long nextSendTime = -1;
+        InboundEstablishState inboundState = null;
+        synchronized (_inboundStates) {
+            //if (_log.shouldLog(Log.DEBUG))
+            //    _log.debug("# inbound states: " + _inboundStates.size());
+            for (Iterator iter = _inboundStates.values().iterator(); iter.hasNext(); ) {
+                InboundEstablishState cur = (InboundEstablishState)iter.next();
+                if (cur.getState() == InboundEstablishState.STATE_CONFIRMED_COMPLETELY) {
+                    // completely received (though the signature may be invalid)
+                    iter.remove();
+                    inboundState = cur;
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug("Removing completely confirmed inbound state");
+                    break;
+                } else if (cur.getLifetime() > MAX_ESTABLISH_TIME) {
+                    // took too long, fuck 'em
+                    iter.remove();
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug("Removing expired inbound state");
+                } else {
+                    if (cur.getNextSendTime() <= now) {
+                        // our turn...
+                        inboundState = cur;
+                        if (_log.shouldLog(Log.DEBUG))
+                            _log.debug("Processing inbound that wanted activity");
+                        break;
+                    } else {
+                        // nothin to do but wait for them to send us
+                        // stuff, so lets move on to the next one being
+                        // established
+                        long when = -1;
+                        if (cur.getNextSendTime() <= 0) {
+                            when = cur.getEstablishBeginTime() + MAX_ESTABLISH_TIME;
+                        } else {
+                            when = cur.getNextSendTime();
+                        }
+                        if (when < nextSendTime)
+                            nextSendTime = when;
+                    }
+                }
+            }
+        }
+
+        if (inboundState != null) {
+            //if (_log.shouldLog(Log.DEBUG))
+            //    _log.debug("Processing for inbound: " + inboundState);
+            switch (inboundState.getState()) {
+                case InboundEstablishState.STATE_REQUEST_RECEIVED:
+                    sendCreated(inboundState);
+                    break;
+                case InboundEstablishState.STATE_CREATED_SENT: // fallthrough
+                case InboundEstablishState.STATE_CONFIRMED_PARTIALLY:
+                    // if its been 5s since we sent the SessionCreated, resend
+                    if (inboundState.getNextSendTime() <= now)
+                        sendCreated(inboundState);
+                    break;
+                case InboundEstablishState.STATE_CONFIRMED_COMPLETELY:
+                    if (inboundState.getConfirmedIdentity() != null) {
+                        handleCompletelyEstablished(inboundState);
+                        break;
+                    } else {
+                        if (_log.shouldLog(Log.WARN))
+                            _log.warn("why are we confirmed with no identity? " + inboundState);
+                        break;
+                    }
+                case InboundEstablishState.STATE_UNKNOWN: // fallthrough
+                default:
+                    // wtf
+                    if (_log.shouldLog(Log.ERROR))
+                        _log.error("hrm, state is unknown for " + inboundState);
+            }
+
+            // ok, since there was something to do, we want to loop again
+            nextSendTime = now;
+        }
+        
+        return nextSendTime;
+    }
+    
+    
+    /**
+     * Drive through the outbound establishment states, adjusting one of them
+     * as necessary
+     */
+    private long handleOutbound() {
+        long now = _context.clock().now();
+        long nextSendTime = -1;
+        OutboundEstablishState outboundState = null;
+        synchronized (_outboundStates) {
+            //if (_log.shouldLog(Log.DEBUG))
+            //    _log.debug("# outbound states: " + _outboundStates.size());
+            for (Iterator iter = _outboundStates.values().iterator(); iter.hasNext(); ) {
+                OutboundEstablishState cur = (OutboundEstablishState)iter.next();
+                if (cur.getState() == OutboundEstablishState.STATE_CONFIRMED_COMPLETELY) {
+                    // completely received
+                    iter.remove();
+                    outboundState = cur;
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug("Removing confirmed outbound: " + cur);
+                    break;
+                } else if (cur.getLifetime() > MAX_ESTABLISH_TIME) {
+                    // took too long, fuck 'em
+                    iter.remove();
+                    outboundState = cur;
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug("Removing expired outbound: " + cur);
+                    break;
+                } else {
+                    if (cur.getNextSendTime() <= now) {
+                        // our turn...
+                        outboundState = cur;
+                        if (_log.shouldLog(Log.DEBUG))
+                            _log.debug("Outbound wants activity: " + cur);
+                        break;
+                    } else {
+                        // nothin to do but wait for them to send us
+                        // stuff, so lets move on to the next one being
+                        // established
+                        long when = -1;
+                        if (cur.getNextSendTime() <= 0) {
+                            when = cur.getEstablishBeginTime() + MAX_ESTABLISH_TIME;
+                        } else {
+                            when = cur.getNextSendTime();
+                        }
+                        if ( (nextSendTime <= 0) || (when < nextSendTime) )
+                            nextSendTime = when;
+                        if (_log.shouldLog(Log.DEBUG))
+                            _log.debug("Outbound doesn't want activity: " + cur + " (next=" + (when-now) + ")");
+                    }
+                }
+            }
+        }
+
+        if (outboundState != null) {
+            if (outboundState.getLifetime() > MAX_ESTABLISH_TIME) {
+                if (outboundState.getState() != OutboundEstablishState.STATE_CONFIRMED_COMPLETELY) {
+                    while (true) {
+                        OutNetMessage msg = outboundState.getNextQueuedMessage();
+                        if (msg == null)
+                            break;
+                        _transport.failed(msg);
+                    }
+                    _context.shitlist().shitlistRouter(outboundState.getRemoteIdentity().calculateHash(), "Unable to establish");
+                } else {
+                    while (true) {
+                        OutNetMessage msg = outboundState.getNextQueuedMessage();
+                        if (msg == null)
+                            break;
+                        _transport.send(msg);
+                    }
+                }
+            } else {
+                switch (outboundState.getState()) {
+                    case OutboundEstablishState.STATE_UNKNOWN:
+                        sendRequest(outboundState);
+                        break;
+                    case OutboundEstablishState.STATE_REQUEST_SENT:
+                        // no response yet (or it was invalid), lets retry
+                        if (outboundState.getNextSendTime() <= now)
+                            sendRequest(outboundState);
+                        break;
+                    case OutboundEstablishState.STATE_CREATED_RECEIVED: // fallthrough
+                    case OutboundEstablishState.STATE_CONFIRMED_PARTIALLY:
+                        if (outboundState.getNextSendTime() <= now)
+                            sendConfirmation(outboundState);
+                        break;
+                    case OutboundEstablishState.STATE_CONFIRMED_COMPLETELY:
+                        handleCompletelyEstablished(outboundState);
+                        break;
+                    default:
+                        // wtf
+                }
+            }
+            
+            //if (_log.shouldLog(Log.DEBUG))
+            //    _log.debug("Since something happened outbound, next=now");
+            // ok, since there was something to do, we want to loop again
+            nextSendTime = now;
+        } else {
+            //if (_log.shouldLog(Log.DEBUG))
+            //    _log.debug("Nothing happened outbound, next is in " + (nextSendTime-now));
+        }
+        
+        return nextSendTime;
+    }
+
+    /**    
+     * Driving thread, processing up to one step for an inbound peer and up to
+     * one step for an outbound peer.  This is prodded whenever any peer's state
+     * changes as well.
+     *
+     */    
+    private class Establisher implements Runnable {
+        public void run() {
+            while (_alive) {
+                _activity = 0;
+                long now = _context.clock().now();
+                long nextSendTime = -1;
+                long nextSendInbound = handleInbound();
+                long nextSendOutbound = handleOutbound();
+                if (nextSendInbound > 0)
+                    nextSendTime = nextSendInbound;
+                if ( (nextSendTime < 0) || (nextSendOutbound < nextSendTime) )
+                    nextSendTime = nextSendOutbound;
+
+                long delay = nextSendTime - now;
+                if ( (nextSendTime == -1) || (delay > 0) ) {
+                    boolean interrupted = false;
+                    try {
+                        synchronized (_activityLock) {
+                            if (_activity > 0)
+                                continue;
+                            if (nextSendTime == -1)
+                                _activityLock.wait();
+                            else
+                                _activityLock.wait(delay);
+                        }
+                    } catch (InterruptedException ie) {
+                        interrupted = true;
+                    }
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug("After waiting w/ nextSend=" + nextSendTime 
+                                   + " and delay=" + delay + " and interrupted=" + interrupted);
+                }
+            }
+        }
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java
new file mode 100644
index 0000000000000000000000000000000000000000..d24097031afd0cbdc3a2fabfe762d4e8b31a92a2
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java
@@ -0,0 +1,308 @@
+package net.i2p.router.transport.udp;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+
+import net.i2p.crypto.DHSessionKeyBuilder;
+import net.i2p.data.Base64;
+import net.i2p.data.ByteArray;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.data.RouterIdentity;
+import net.i2p.data.SessionKey;
+import net.i2p.data.Signature;
+import net.i2p.router.OutNetMessage;
+import net.i2p.router.RouterContext;
+import net.i2p.util.Log;
+
+/**
+ * Data for a new connection being established, where the remote peer has
+ * initiated the connection with us.  In other words, they are Alice and
+ * we are Bob.
+ *
+ */
+public class InboundEstablishState {
+    private RouterContext _context;
+    private Log _log;
+    // SessionRequest message
+    private byte _receivedX[];
+    private byte _bobIP[];
+    private int _bobPort;
+    private DHSessionKeyBuilder _keyBuilder;
+    // SessionCreated message
+    private byte _sentY[];
+    private byte _aliceIP[];
+    private int _alicePort;
+    private long _sentRelayTag;
+    private long _sentSignedOnTime;
+    private SessionKey _sessionKey;
+    private SessionKey _macKey;
+    private Signature _sentSignature;
+    // SessionConfirmed messages
+    private byte _receivedIdentity[][];
+    private long _receivedSignedOnTime;
+    private byte _receivedSignature[];
+    private boolean _verificationAttempted;
+    private RouterIdentity _receivedConfirmedIdentity;
+    // general status 
+    private long _establishBegin;
+    private long _lastReceive;
+    private long _lastSend;
+    private long _nextSend;
+    private String _remoteHostInfo;
+    private int _currentState;
+    
+    /** nothin known yet */
+    public static final int STATE_UNKNOWN = 0;
+    /** we have received an initial request */
+    public static final int STATE_REQUEST_RECEIVED = 1;
+    /** we have sent a signed creation packet */
+    public static final int STATE_CREATED_SENT = 2;
+    /** we have received one or more confirmation packets */
+    public static final int STATE_CONFIRMED_PARTIALLY = 3;
+    /** we have completely received all of the confirmation packets */
+    public static final int STATE_CONFIRMED_COMPLETELY = 4;
+    
+    public InboundEstablishState(RouterContext ctx, InetAddress remoteHost, int remotePort, int localPort) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(InboundEstablishState.class);
+        _aliceIP = remoteHost.getAddress();
+        _alicePort = remotePort;
+        _remoteHostInfo = PeerState.calculateRemoteHostString(_aliceIP, _alicePort);
+        _bobPort = localPort;
+        _keyBuilder = null;
+        _verificationAttempted = false;
+        _currentState = STATE_UNKNOWN;
+        _establishBegin = ctx.clock().now();
+    }
+    
+    public synchronized int getState() { return _currentState; }
+    
+    public synchronized void receiveSessionRequest(UDPPacketReader.SessionRequestReader req) {
+        if (_receivedX == null)
+            _receivedX = new byte[UDPPacketReader.SessionRequestReader.X_LENGTH];
+        req.readX(_receivedX, 0);
+        if (_bobIP == null)
+            _bobIP = new byte[req.readIPSize()];
+        req.readIP(_bobIP, 0);
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Receive sessionRequest, BobIP = " + Base64.encode(_bobIP));
+        if (_currentState == STATE_UNKNOWN)
+            _currentState = STATE_REQUEST_RECEIVED;
+        packetReceived();
+    }
+    
+    public synchronized boolean sessionRequestReceived() { return _receivedX != null; }
+    public synchronized byte[] getReceivedX() { return _receivedX; }
+    public synchronized byte[] getReceivedOurIP() { return _bobIP; }
+    
+    public synchronized void generateSessionKey() {
+        if (_sessionKey != null) return;
+        _keyBuilder = new DHSessionKeyBuilder();
+        _keyBuilder.setPeerPublicValue(_receivedX);
+        _sessionKey = _keyBuilder.getSessionKey();
+        ByteArray extra = _keyBuilder.getExtraBytes();
+        _macKey = new SessionKey(new byte[SessionKey.KEYSIZE_BYTES]);
+        System.arraycopy(extra.getData(), 0, _macKey.getData(), 0, SessionKey.KEYSIZE_BYTES);
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Established inbound keys.  cipher: " + Base64.encode(_sessionKey.getData())
+                       + " mac: " + Base64.encode(_macKey.getData()));
+    }
+    
+    public synchronized SessionKey getCipherKey() { return _sessionKey; }
+    public synchronized SessionKey getMACKey() { return _macKey; }
+
+    /** what IP do they appear to be on? */
+    public synchronized byte[] getSentIP() { return _aliceIP; }
+    /** what port number do they appear to be coming from? */
+    public synchronized int getSentPort() { return _alicePort; }
+    
+    public synchronized byte[] getSentY() {
+        if (_sentY == null)
+            _sentY = _keyBuilder.getMyPublicValueBytes();
+        return _sentY;
+    }
+    
+    public synchronized long getSentRelayTag() { return _sentRelayTag; }
+    public synchronized void setSentRelayTag(long tag) { _sentRelayTag = tag; }
+    public synchronized long getSentSignedOnTime() { return _sentSignedOnTime; }
+    
+    public synchronized void prepareSessionCreated() {
+        if (_sentSignature == null) signSessionCreated();
+    }
+    
+    public synchronized Signature getSentSignature() { return _sentSignature; }
+    
+    /**
+     * Sign: Alice's IP + Alice's port + Bob's IP + Bob's port + Alice's
+     *       new relay tag + Bob's signed on time
+     */
+    private void signSessionCreated() {
+        byte signed[] = new byte[_aliceIP.length + 2
+                                 + _bobIP.length + 2
+                                 + 4 // sent relay tag
+                                 + 4 // signed on time
+                                 ];
+        _sentSignedOnTime = _context.clock().now() / 1000;
+        
+        int off = 0;
+        System.arraycopy(_aliceIP, 0, signed, off, _aliceIP.length);
+        off += _aliceIP.length;
+        DataHelper.toLong(signed, off, 2, _alicePort);
+        off += 2;
+        System.arraycopy(_bobIP, 0, signed, off, _bobIP.length);
+        off += _bobIP.length;
+        DataHelper.toLong(signed, off, 2, _bobPort);
+        off += 2;
+        DataHelper.toLong(signed, off, 4, _sentRelayTag);
+        off += 4;
+        DataHelper.toLong(signed, off, 4, _sentSignedOnTime);
+        
+        _sentSignature = _context.dsa().sign(signed, _context.keyManager().getSigningPrivateKey());
+        
+        if (_log.shouldLog(Log.DEBUG)) {
+            StringBuffer buf = new StringBuffer(128);
+            buf.append("Signing sessionCreated:");
+            buf.append(" AliceIP: ").append(Base64.encode(_aliceIP));
+            buf.append(" AlicePort: ").append(_alicePort);
+            buf.append(" BobIP: ").append(Base64.encode(_bobIP));
+            buf.append(" BobPort: ").append(_bobPort);
+            buf.append(" RelayTag: ").append(_sentRelayTag);
+            buf.append(" SignedOn: ").append(_sentSignedOnTime);
+            buf.append(" signature: ").append(Base64.encode(_sentSignature.getData()));
+            _log.debug(buf.toString());
+        }
+    }
+    
+    /** note that we just sent a SessionCreated packet */
+    public synchronized void createdPacketSent() {
+        _lastSend = _context.clock().now();
+        if ( (_currentState == STATE_UNKNOWN) || (_currentState == STATE_REQUEST_RECEIVED) )
+            _currentState = STATE_CREATED_SENT;
+    }
+    
+    /** how long have we been trying to establish this session? */
+    public synchronized long getLifetime() { return _context.clock().now() - _establishBegin; }
+    public synchronized long getEstablishBeginTime() { return _establishBegin; }
+    public synchronized long getNextSendTime() { return _nextSend; }
+    public synchronized void setNextSendTime(long when) { _nextSend = when; }
+
+    /** host+port, uniquely identifies an attempt */
+    public String getRemoteHostInfo() { return _remoteHostInfo; }
+
+    public synchronized void receiveSessionConfirmed(UDPPacketReader.SessionConfirmedReader conf) {
+        if (_receivedIdentity == null)
+            _receivedIdentity = new byte[conf.readTotalFragmentNum()][];
+        int cur = conf.readCurrentFragmentNum();
+        if (_receivedIdentity[cur] == null) {
+            byte fragment[] = new byte[conf.readCurrentFragmentSize()];
+            conf.readFragmentData(fragment, 0);
+            _receivedIdentity[cur] = fragment;
+        }
+        
+        if (cur == _receivedIdentity.length-1) {
+            _receivedSignedOnTime = conf.readFinalFragmentSignedOnTime();
+            if (_receivedSignature == null)
+                _receivedSignature = new byte[Signature.SIGNATURE_BYTES];
+            conf.readFinalSignature(_receivedSignature, 0);
+        }
+        
+        if ( (_currentState == STATE_UNKNOWN) || 
+             (_currentState == STATE_REQUEST_RECEIVED) ||
+             (_currentState == STATE_CREATED_SENT) ) {
+            if (confirmedFullyReceived())
+                _currentState = STATE_CONFIRMED_COMPLETELY;
+            else
+                _currentState = STATE_CONFIRMED_PARTIALLY;
+        }
+        
+        packetReceived();
+    }
+    
+    /** have we fully received the SessionConfirmed messages from Alice? */
+    public synchronized boolean confirmedFullyReceived() {
+        if (_receivedIdentity != null) {
+            for (int i = 0; i < _receivedIdentity.length; i++)
+                if (_receivedIdentity[i] == null)
+                    return false;
+            return true;
+        } else {
+            return false;
+        }
+    }
+    
+    /**
+     * Who is Alice (null if forged/unknown)
+     */
+    public synchronized RouterIdentity getConfirmedIdentity() {
+        if (!_verificationAttempted) {
+            verifyIdentity();
+            _verificationAttempted = true;
+        }
+        return _receivedConfirmedIdentity;
+    }
+    
+    /**
+     * Determine if Alice sent us a valid confirmation packet.  The 
+     * identity signs: Alice's IP + Alice's port + Bob's IP + Bob's port
+     * + Alice's new relay key + Alice's signed on time
+     */
+    private synchronized void verifyIdentity() {
+        int identSize = 0;
+        for (int i = 0; i < _receivedIdentity.length; i++)
+            identSize += _receivedIdentity[i].length;
+        byte ident[] = new byte[identSize];
+        int off = 0;
+        for (int i = 0; i < _receivedIdentity.length; i++) {
+            int len = _receivedIdentity[i].length;
+            System.arraycopy(_receivedIdentity[i], 0, ident, off, len);
+            off += len;
+        }
+        ByteArrayInputStream in = new ByteArrayInputStream(ident); 
+        RouterIdentity peer = new RouterIdentity();
+        try {
+            peer.readBytes(in);
+            
+            byte signed[] = new byte[_aliceIP.length + 2
+                                     + _bobIP.length + 2
+                                     + 4 // Alice's relay key
+                                     + 4 // signed on time
+                                     ];
+
+            off = 0;
+            System.arraycopy(_aliceIP, 0, signed, off, _aliceIP.length);
+            off += _aliceIP.length;
+            DataHelper.toLong(signed, off, 2, _alicePort);
+            off += 2;
+            System.arraycopy(_bobIP, 0, signed, off, _bobIP.length);
+            off += _bobIP.length;
+            DataHelper.toLong(signed, off, 2, _bobPort);
+            off += 2;
+            DataHelper.toLong(signed, off, 4, _sentRelayTag);
+            off += 4;
+            DataHelper.toLong(signed, off, 4, _receivedSignedOnTime);
+            Signature sig = new Signature(_receivedSignature);
+            boolean ok = _context.dsa().verifySignature(sig, signed, peer.getSigningPublicKey());
+            if (ok) {
+                _receivedConfirmedIdentity = peer;
+            } else {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Signature failed from " + peer);
+            }
+        } catch (DataFormatException dfe) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Improperly formatted yet fully received ident", dfe);
+        } catch (IOException ioe) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Improperly formatted yet fully received ident", ioe);
+        }
+    }
+    
+    private void packetReceived() {
+        _lastReceive = _context.clock().now();
+        _nextSend = _lastReceive;
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java b/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java
new file mode 100644
index 0000000000000000000000000000000000000000..1738e3da902842ed3239b87175aa284a6956ff86
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java
@@ -0,0 +1,228 @@
+package net.i2p.router.transport.udp;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import net.i2p.router.RouterContext;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+/**
+ * Organize the received data message fragments, allowing its 
+ * {@link MessageReceiver} to pull off completed messages and its 
+ * {@link ACKSender} to pull off peers who need to receive an ACK for
+ * these messages.  In addition, it drops failed fragments and keeps a
+ * minimal list of the most recently completed messages (even though higher
+ * up in the router we have full blown replay detection, its nice to have a
+ * basic line of defense here)
+ *
+ */
+public class InboundMessageFragments {
+    private RouterContext _context;
+    private Log _log;
+    /** Map of peer (Hash) to a Map of messageId (Long) to InboundMessageState objects */
+    private Map _inboundMessages;
+    /** list of peers (PeerState) who we have received data from but not yet ACKed to */
+    private List _unsentACKs;
+    /** list of messages (InboundMessageState) fully received but not interpreted yet */
+    private List _completeMessages;
+    /** list of message IDs (Long) recently received, so we can ignore in flight dups */
+    private List _recentlyCompletedMessages;
+    private OutboundMessageFragments _outbound;
+    private UDPTransport _transport;
+    /** this can be broken down further, but to start, OneBigLock does the trick */
+    private Object _stateLock;
+    private boolean _alive;
+    
+    private static final int RECENTLY_COMPLETED_SIZE = 100;
+    /** how frequently do we want to send ACKs to a peer? */
+    private static final int ACK_FREQUENCY = 100;
+        
+    public InboundMessageFragments(RouterContext ctx, OutboundMessageFragments outbound, UDPTransport transport) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(InboundMessageFragments.class);
+        _inboundMessages = new HashMap(64);
+        _unsentACKs = new ArrayList(64);
+        _completeMessages = new ArrayList(64);
+        _recentlyCompletedMessages = new ArrayList(RECENTLY_COMPLETED_SIZE);
+        _outbound = outbound;
+        _transport = transport;
+        _context.statManager().createRateStat("udp.receivedCompleteTime", "How long it takes to receive a full message", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+        _context.statManager().createRateStat("udp.receivedCompleteFragments", "How many fragments go in a fully received message", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+        _context.statManager().createRateStat("udp.receivedACKs", "How many messages were ACKed at a time", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+        _context.statManager().createRateStat("udp.ignoreRecentDuplicate", "Take note that we received a packet for a recently completed message", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+        _context.statManager().createRateStat("udp.receiveMessagePeriod", "How long it takes to pull the message fragments out of a packet", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+        _context.statManager().createRateStat("udp.receiveACKPeriod", "How long it takes to pull the ACKs out of a packet", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+        _stateLock = this;
+    }
+    
+    public void startup() { 
+        _alive = true; 
+        I2PThread t = new I2PThread(new ACKSender(_context, this, _transport), "UDP ACK sender");
+        t.setDaemon(true);
+        t.start();
+        
+        t = new I2PThread(new MessageReceiver(_context, this, _transport), "UDP message receiver");
+        t.setDaemon(true);
+        t.start();
+    }
+    public void shutdown() {
+        _alive = false;
+        synchronized (_stateLock) {
+            _completeMessages.clear();
+            _unsentACKs.clear();
+            _inboundMessages.clear();
+            _stateLock.notifyAll();
+        }
+    }
+    public boolean isAlive() { return _alive; }
+
+    /**
+     * Pull the fragments and ACKs out of the authenticated data packet
+     */
+    public void receiveData(PeerState from, UDPPacketReader.DataReader data) {
+        long beforeMsgs = _context.clock().now();
+        receiveMessages(from, data);
+        long afterMsgs = _context.clock().now();
+        receiveACKs(from, data);
+        long afterACKs = _context.clock().now();
+        
+        _context.statManager().addRateData("udp.receiveMessagePeriod", afterMsgs-beforeMsgs, afterACKs-beforeMsgs);
+        _context.statManager().addRateData("udp.receiveACKPeriod", afterACKs-afterMsgs, afterACKs-beforeMsgs);
+    }
+    
+    /**
+     * Pull out all the data fragments and shove them into InboundMessageStates.
+     * Along the way, if any state expires, or a full message arrives, move it
+     * appropriately.
+     *
+     */
+    private void receiveMessages(PeerState from, UDPPacketReader.DataReader data) {
+        int fragments = data.readFragmentCount();
+        if (fragments <= 0) return;
+        synchronized (_stateLock) {
+            Map messages = (Map)_inboundMessages.get(from.getRemotePeer());
+            if (messages == null) {
+                messages = new HashMap(fragments);
+                _inboundMessages.put(from.getRemotePeer(), messages);
+            }
+        
+            for (int i = 0; i < fragments; i++) {
+                Long messageId = new Long(data.readMessageId(i));
+            
+                if (_recentlyCompletedMessages.contains(messageId)) {
+                    _context.statManager().addRateData("udp.ignoreRecentDuplicate", 1, 0);
+                    continue;
+                }
+            
+                int size = data.readMessageFragmentSize(i);
+                InboundMessageState state = null;
+                boolean messageComplete = false;
+                boolean messageExpired = false;
+                boolean fragmentOK = false;
+                state = (InboundMessageState)messages.get(messageId);
+                if (state == null) {
+                    state = new InboundMessageState(_context, messageId.longValue(), from.getRemotePeer());
+                    messages.put(messageId, state);
+                }
+                fragmentOK = state.receiveFragment(data, i);
+                if (state.isComplete()) {
+                    messageComplete = true;
+                    messages.remove(messageId);
+                    
+                   while (_recentlyCompletedMessages.size() >= RECENTLY_COMPLETED_SIZE)
+                        _recentlyCompletedMessages.remove(0);
+                    _recentlyCompletedMessages.add(messageId);
+
+                    _completeMessages.add(state);
+                    
+                    from.messageFullyReceived(messageId);
+                    if (!_unsentACKs.contains(from))
+                        _unsentACKs.add(from);
+                    
+                    if (_log.shouldLog(Log.INFO))
+                        _log.info("Message received completely!  " + state);
+
+                    _context.statManager().addRateData("udp.receivedCompleteTime", state.getLifetime(), state.getLifetime());
+                    _context.statManager().addRateData("udp.receivedCompleteFragments", state.getFragmentCount(), state.getLifetime());
+
+                    _stateLock.notifyAll();
+                } else if (state.isExpired()) {
+                    messageExpired = true;
+                    messages.remove(messageId);
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn("Message expired while only being partially read: " + state);
+                    state.releaseResources();
+                }
+                
+                if (!fragmentOK)
+                    break;
+            }
+        }
+    }
+    
+    private void receiveACKs(PeerState from, UDPPacketReader.DataReader data) {
+        if (data.readACKsIncluded()) {
+            int fragments = 0;
+            long acks[] = data.readACKs();
+            _context.statManager().addRateData("udp.receivedACKs", acks.length, 0);
+            for (int i = 0; i < acks.length; i++) {
+                if (_log.shouldLog(Log.INFO))
+                    _log.info("Full ACK of message " + acks[i] + " received!");
+                fragments += _outbound.acked(acks[i], from.getRemotePeer());
+            }
+            from.messageACKed(fragments * from.getMTU()); // estimated size
+        }
+        if (data.readECN())
+            from.ECNReceived();
+        else
+            from.dataReceived();
+    }
+    
+    /**
+     * Blocking call to pull off the next fully received message
+     *
+     */
+    public InboundMessageState receiveNextMessage() {
+        while (_alive) {
+            try {
+                synchronized (_stateLock) {
+                    if (_completeMessages.size() > 0)
+                        return (InboundMessageState)_completeMessages.remove(0);
+                    _stateLock.wait();
+                }
+            } catch (InterruptedException ie) {}
+        }
+        return null;
+    }
+    
+    /** 
+     * Pull off the peer who we next want to send ACKs/NACKs to.
+     * This call blocks, and only returns null on shutdown.
+     *
+     */
+    public PeerState getNextPeerToACK() {
+        while (_alive) {
+            try {
+                long now = _context.clock().now();
+                synchronized (_stateLock) {
+                    for (int i = 0; i < _unsentACKs.size(); i++) {
+                        PeerState peer = (PeerState)_unsentACKs.get(i);
+                        if (peer.getLastACKSend() + ACK_FREQUENCY <= now) {
+                            _unsentACKs.remove(i);
+                            peer.setLastACKSend(now);
+                            return peer;
+                        }
+                    }
+                    if (_unsentACKs.size() > 0)
+                        _stateLock.wait(_context.random().nextInt(100));
+                    else
+                        _stateLock.wait();
+                }
+            } catch (InterruptedException ie) {}
+        }
+        return null;
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/InboundMessageState.java b/router/java/src/net/i2p/router/transport/udp/InboundMessageState.java
new file mode 100644
index 0000000000000000000000000000000000000000..a07d45341f5b82d24a48b59e4164575a9bea0d6a
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/InboundMessageState.java
@@ -0,0 +1,112 @@
+package net.i2p.router.transport.udp;
+
+import net.i2p.data.ByteArray;
+import net.i2p.data.Hash;
+import net.i2p.router.RouterContext;
+import net.i2p.util.ByteCache;
+import net.i2p.util.Log;
+
+/**
+ * Hold the raw data fragments of an inbound message
+ *
+ */
+public class InboundMessageState {
+    private RouterContext _context;
+    private Log _log;
+    private long _messageId;
+    private Hash _from;
+    /** 
+     * indexed array of fragments for the message, where not yet
+     * received fragments are null.
+     */
+    private ByteArray _fragments[];
+    /**
+     * what is the last fragment in the message (or -1 if not yet known)
+     */
+    private int _lastFragment;
+    private long _receiveBegin;
+    
+    /** expire after 30s */
+    private static final long MAX_RECEIVE_TIME = 30*1000;
+    private static final int MAX_FRAGMENTS = 32;
+    
+    private static final ByteCache _fragmentCache = ByteCache.getInstance(64, 2048);
+    
+    public InboundMessageState(RouterContext ctx, long messageId, Hash from) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(InboundMessageState.class);
+        _messageId = messageId;
+        _from = from;
+        _fragments = new ByteArray[MAX_FRAGMENTS];
+        _lastFragment = -1;
+        _receiveBegin = ctx.clock().now();
+    }
+    
+    /**
+     * Read in the data from the fragment.
+     *
+     * @return true if the data was ok, false if it was corrupt
+     */
+    public synchronized boolean receiveFragment(UDPPacketReader.DataReader data, int dataFragment) {
+        int fragmentNum = data.readMessageFragmentNum(dataFragment);
+        if ( (fragmentNum < 0) || (fragmentNum > _fragments.length)) {
+            _log.log(Log.CRIT, "Invalid fragment " + fragmentNum + ": " + data, new Exception("source"));
+            return false;
+        }
+        if (_fragments[fragmentNum] == null) {
+            // new fragment, read it
+            ByteArray message = _fragmentCache.acquire();
+            data.readMessageFragment(dataFragment, message.getData(), 0);
+            int size = data.readMessageFragmentSize(dataFragment);
+            message.setValid(size);
+            _fragments[fragmentNum] = message;
+            if (data.readMessageIsLast(dataFragment))
+                _lastFragment = fragmentNum;
+        }
+        return true;
+    }
+    
+    public synchronized boolean isComplete() {
+        if (_lastFragment < 0) return false;
+        for (int i = 0; i <= _lastFragment; i++)
+            if (_fragments[i] == null)
+                return false;
+        return true;
+    }
+    public synchronized boolean isExpired() { 
+        return _context.clock().now() > _receiveBegin + MAX_RECEIVE_TIME;
+    }
+    public long getLifetime() {
+        return _context.clock().now() - _receiveBegin;
+    }
+    public Hash getFrom() { return _from; }
+    public long getMessageId() { return _messageId; }
+    public synchronized int getCompleteSize() {
+        int size = 0;
+        for (int i = 0; i <= _lastFragment; i++)
+            size += _fragments[i].getValid();
+        return size;
+    }
+    
+    public void releaseResources() {
+        if (_fragments != null)
+            for (int i = 0; i < _fragments.length; i++)
+                _fragmentCache.release(_fragments[i]);
+        _fragments = null;
+    }
+    
+    public ByteArray[] getFragments() {
+        return _fragments;
+    }
+    public int getFragmentCount() { return _lastFragment+1; }
+    
+    public String toString() {
+        StringBuffer buf = new StringBuffer(32);
+        buf.append("Message: ").append(_messageId);
+        if (isComplete()) {
+            buf.append(" completely received with ");
+            buf.append(getCompleteSize()).append(" bytes");
+        }
+        return buf.toString();
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/MessageQueue.java b/router/java/src/net/i2p/router/transport/udp/MessageQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..cc38ebbafec279bca630c800643bc32c2177e144
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/MessageQueue.java
@@ -0,0 +1,20 @@
+package net.i2p.router.transport.udp;
+
+import net.i2p.router.OutNetMessage;
+
+/**
+ * Base queue for messages not yet packetized
+ */
+public interface MessageQueue {
+    /**
+     * Get the next message, blocking until one is found or the expiration
+     * reached.
+     *
+     * @param blockUntil expiration, or -1 if indefinite
+     */
+    public OutNetMessage getNext(long blockUntil);
+    /**
+     * Add on a new message to the queue
+     */
+    public void add(OutNetMessage message);
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java b/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java
new file mode 100644
index 0000000000000000000000000000000000000000..c13071159a97ecc1a8f75011f8e9870d5f565c4c
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java
@@ -0,0 +1,74 @@
+package net.i2p.router.transport.udp;
+
+import net.i2p.data.Base64;
+import net.i2p.data.ByteArray;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.i2np.I2NPMessage;
+import net.i2p.data.i2np.I2NPMessageImpl;
+import net.i2p.data.i2np.I2NPMessageException;
+import net.i2p.router.RouterContext;
+import net.i2p.util.Log;
+
+/**
+ * Pull fully completed fragments off the {@link InboundMessageFragments} queue,
+ * parse 'em into I2NPMessages, and stick them on the 
+ * {@link net.i2p.router.InNetMessagePool} by way of the {@link UDPTransport}.
+ */
+public class MessageReceiver implements Runnable {
+    private RouterContext _context;
+    private Log _log;
+    private InboundMessageFragments _fragments;
+    private UDPTransport _transport;
+    
+    public MessageReceiver(RouterContext ctx, InboundMessageFragments frag, UDPTransport transport) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(MessageReceiver.class);
+        _fragments = frag;
+        _transport = transport;
+    }
+
+    public void run() {
+        while (_fragments.isAlive()) {
+            InboundMessageState message = _fragments.receiveNextMessage();
+            if (message == null) continue;
+            
+            int size = message.getCompleteSize();
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Full message received (" + message.getMessageId() + ") after " + message.getLifetime() 
+                          + "... todo: parse and plop it onto InNetMessagePool");
+            I2NPMessage msg = readMessage(message);
+            if (msg != null)
+                _transport.messageReceived(msg, null, message.getFrom(), message.getLifetime(), size);
+        }
+    }
+    
+    private I2NPMessage readMessage(InboundMessageState state) {
+        try {
+            byte buf[] = new byte[state.getCompleteSize()];
+            ByteArray fragments[] = state.getFragments();
+            int numFragments = state.getFragmentCount();
+            int off = 0;
+            for (int i = 0; i < numFragments; i++) {
+                System.arraycopy(fragments[i].getData(), 0, buf, off, fragments[i].getValid());
+                if (_log.shouldLog(Log.DEBUG))
+                    _log.debug("Raw fragment[" + i + "] for " + state.getMessageId() + ": " 
+                               + Base64.encode(fragments[i].getData(), 0, fragments[i].getValid()));
+                off += fragments[i].getValid();
+            }
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Raw byte array for " + state.getMessageId() + ": " + Base64.encode(buf));
+            I2NPMessage m = I2NPMessageImpl.fromRawByteArray(_context, buf, 0, buf.length);
+            m.setUniqueId(state.getMessageId());
+            return m;
+        } catch (I2NPMessageException ime) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Message invalid: " + state, ime);
+            return null;
+        } catch (Exception e) {
+            _log.log(Log.CRIT, "Error dealing with a message: " + state, e);
+            return null;
+        } finally {
+            state.releaseResources();
+        }
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
new file mode 100644
index 0000000000000000000000000000000000000000..e359bccdf09b135d483a0f0f49c0b87862e65a8c
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
@@ -0,0 +1,349 @@
+package net.i2p.router.transport.udp;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+
+import net.i2p.crypto.DHSessionKeyBuilder;
+import net.i2p.data.Base64;
+import net.i2p.data.ByteArray;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.DataHelper;
+import net.i2p.data.RouterIdentity;
+import net.i2p.data.SessionKey;
+import net.i2p.data.Signature;
+import net.i2p.router.OutNetMessage;
+import net.i2p.router.RouterContext;
+import net.i2p.util.Log;
+
+/**
+ * Data for a new connection being established, where we initiated the 
+ * connection with a remote peer.  In other words, we are Alice and
+ * they are Bob.
+ *
+ */
+public class OutboundEstablishState {
+    private RouterContext _context;
+    private Log _log;
+    // SessionRequest message
+    private byte _sentX[];
+    private byte _bobIP[];
+    private int _bobPort;
+    private DHSessionKeyBuilder _keyBuilder;
+    // SessionCreated message
+    private byte _receivedY[];
+    private byte _aliceIP[];
+    private int _alicePort;
+    private long _receivedRelayTag;
+    private long _receivedSignedOnTime;
+    private SessionKey _sessionKey;
+    private SessionKey _macKey;
+    private Signature _receivedSignature;
+    private byte[] _receivedEncryptedSignature;
+    private byte[] _receivedIV;
+    // SessionConfirmed messages
+    private long _sentSignedOnTime;
+    private Signature _sentSignature;
+    // general status 
+    private long _establishBegin;
+    private long _lastReceive;
+    private long _lastSend;
+    private long _nextSend;
+    private String _remoteHostInfo;
+    private RouterIdentity _remotePeer;
+    private SessionKey _introKey;
+    private List _queuedMessages;
+    private int _currentState;
+    
+    /** nothin sent yet */
+    public static final int STATE_UNKNOWN = 0;
+    /** we have sent an initial request */
+    public static final int STATE_REQUEST_SENT = 1;
+    /** we have received a signed creation packet */
+    public static final int STATE_CREATED_RECEIVED = 2;
+    /** we have sent one or more confirmation packets */
+    public static final int STATE_CONFIRMED_PARTIALLY = 3;
+    /** we have received a data packet */
+    public static final int STATE_CONFIRMED_COMPLETELY = 4;
+    
+    public OutboundEstablishState(RouterContext ctx, InetAddress remoteHost, int remotePort, 
+                                  RouterIdentity remotePeer, SessionKey introKey) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(OutboundEstablishState.class);
+        _bobIP = remoteHost.getAddress();
+        _bobPort = remotePort;
+        _remoteHostInfo = PeerState.calculateRemoteHostString(_bobIP, _bobPort);
+        _remotePeer = remotePeer;
+        _introKey = introKey;
+        _keyBuilder = null;
+        _queuedMessages = new ArrayList(4);
+        _currentState = STATE_UNKNOWN;
+        _establishBegin = ctx.clock().now();
+    }
+    
+    public synchronized int getState() { return _currentState; }
+
+    public void addMessage(OutNetMessage msg) {
+        synchronized (_queuedMessages) {
+            _queuedMessages.add(msg);
+        }
+    }
+    public OutNetMessage getNextQueuedMessage() { 
+        synchronized (_queuedMessages) {
+            if (_queuedMessages.size() > 0)
+                return (OutNetMessage)_queuedMessages.remove(0);
+        }
+        return null;
+    }
+    
+    public RouterIdentity getRemoteIdentity() { return _remotePeer; }
+    public SessionKey getIntroKey() { return _introKey; }
+    
+    public synchronized void prepareSessionRequest() {
+        _keyBuilder = new DHSessionKeyBuilder();
+        byte X[] = _keyBuilder.getMyPublicValue().toByteArray();
+        if (_sentX == null)
+            _sentX = new byte[UDPPacketReader.SessionRequestReader.X_LENGTH];
+        if (X.length == 257)
+            System.arraycopy(X, 1, _sentX, 0, _sentX.length);
+        else if (X.length == 256)
+            System.arraycopy(X, 0, _sentX, 0, _sentX.length);
+        else
+            System.arraycopy(X, 0, _sentX, _sentX.length - X.length, X.length);
+    }
+
+    public synchronized byte[] getSentX() { return _sentX; }
+    public synchronized byte[] getSentIP() { return _bobIP; }
+    public synchronized int getSentPort() { return _bobPort; }
+
+    public synchronized void receiveSessionCreated(UDPPacketReader.SessionCreatedReader reader) {
+        if (_receivedY != null) {
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Session created already received, ignoring");
+            return; // already received
+        }
+        _receivedY = new byte[UDPPacketReader.SessionCreatedReader.Y_LENGTH];
+        reader.readY(_receivedY, 0);
+        if (_aliceIP == null)
+            _aliceIP = new byte[reader.readIPSize()];
+        reader.readIP(_aliceIP, 0);
+        _alicePort = reader.readPort();
+        _receivedRelayTag = reader.readRelayTag();
+        _receivedSignedOnTime = reader.readSignedOnTime();
+        _receivedEncryptedSignature = new byte[Signature.SIGNATURE_BYTES + 8];
+        reader.readEncryptedSignature(_receivedEncryptedSignature, 0);
+        _receivedIV = new byte[UDPPacket.IV_SIZE];
+        reader.readIV(_receivedIV, 0);
+        
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Receive session created:\neSig: " + Base64.encode(_receivedEncryptedSignature)
+                       + "\nreceivedIV: " + Base64.encode(_receivedIV)
+                       + "\nAliceIP: " + Base64.encode(_aliceIP)
+                       + " RelayTag: " + _receivedRelayTag
+                       + " SignedOn: " + _receivedSignedOnTime
+                       + "\nthis: " + this.toString());
+        
+        if ( (_currentState == STATE_UNKNOWN) || (_currentState == STATE_REQUEST_SENT) )
+            _currentState = STATE_CREATED_RECEIVED;
+        packetReceived();
+    }
+    
+    /**
+     * Blocking call (run in the establisher thread) to determine if the 
+     * session was created properly.  If it wasn't, all the SessionCreated
+     * remnants are dropped (perhaps they were spoofed, etc) so that we can
+     * receive another one
+     */
+    public synchronized boolean validateSessionCreated() {
+        if (_receivedSignature != null) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Session created already validated");
+            return true;
+        }
+        
+        generateSessionKey();
+        decryptSignature();
+        
+        if (verifySessionCreated()) {
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Session created passed validation");
+            return true;
+        } else {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Session created failed validation, clearing state");
+            _receivedY = null;
+            _aliceIP = null;
+            _receivedRelayTag = 0;
+            _receivedSignedOnTime = -1;
+            _receivedEncryptedSignature = null;
+            _receivedIV = null;
+            _receivedSignature = null;
+            
+            if ( (_currentState == STATE_UNKNOWN) || 
+                 (_currentState == STATE_REQUEST_SENT) || 
+                 (_currentState == STATE_CREATED_RECEIVED) )
+                _currentState = STATE_REQUEST_SENT;
+            
+            _nextSend = _context.clock().now();
+            return false;
+        }
+    }
+    
+    private void generateSessionKey() {
+        if (_sessionKey != null) return;
+        _keyBuilder.setPeerPublicValue(_receivedY);
+        _sessionKey = _keyBuilder.getSessionKey();
+        ByteArray extra = _keyBuilder.getExtraBytes();
+        _macKey = new SessionKey(new byte[SessionKey.KEYSIZE_BYTES]);
+        System.arraycopy(extra.getData(), 0, _macKey.getData(), 0, SessionKey.KEYSIZE_BYTES);
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Established outbound keys.  cipher: " + Base64.encode(_sessionKey.getData())
+                       + " mac: " + Base64.encode(_macKey.getData()));
+    }
+    
+    /** 
+     * decrypt the signature (and subsequent pad bytes) with the 
+     * additional layer of encryption using the negotiated key along side
+     * the packet's IV
+     */
+    private void decryptSignature() {
+        if (_receivedEncryptedSignature == null) throw new NullPointerException("encrypted signature is null! this=" + this.toString());
+        else if (_sessionKey == null) throw new NullPointerException("SessionKey is null!");
+        else if (_receivedIV == null) throw new NullPointerException("IV is null!");
+        _context.aes().decrypt(_receivedEncryptedSignature, 0, _receivedEncryptedSignature, 0, 
+                               _sessionKey, _receivedIV, _receivedEncryptedSignature.length);
+        byte signatureBytes[] = new byte[Signature.SIGNATURE_BYTES];
+        System.arraycopy(_receivedEncryptedSignature, 0, signatureBytes, 0, Signature.SIGNATURE_BYTES);
+        _receivedSignature = new Signature(signatureBytes);
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Decrypted received signature: \n" + Base64.encode(signatureBytes));
+    }
+
+    /**
+     * Verify: Alice's IP + Alice's port + Bob's IP + Bob's port + Alice's
+     *         new relay tag + Bob's signed on time
+     */
+    private boolean verifySessionCreated() {
+        byte signed[] = new byte[_aliceIP.length + 2
+                                 + _bobIP.length + 2
+                                 + 4 // sent relay tag
+                                 + 4 // signed on time
+                                 ];
+        
+        int off = 0;
+        System.arraycopy(_aliceIP, 0, signed, off, _aliceIP.length);
+        off += _aliceIP.length;
+        DataHelper.toLong(signed, off, 2, _alicePort);
+        off += 2;
+        System.arraycopy(_bobIP, 0, signed, off, _bobIP.length);
+        off += _bobIP.length;
+        DataHelper.toLong(signed, off, 2, _bobPort);
+        off += 2;
+        DataHelper.toLong(signed, off, 4, _receivedRelayTag);
+        off += 4;
+        DataHelper.toLong(signed, off, 4, _receivedSignedOnTime);
+        if (_log.shouldLog(Log.DEBUG)) {
+            StringBuffer buf = new StringBuffer(128);
+            buf.append("Signed sessionCreated:");
+            buf.append(" AliceIP: ").append(Base64.encode(_aliceIP));
+            buf.append(" AlicePort: ").append(_alicePort);
+            buf.append(" BobIP: ").append(Base64.encode(_bobIP));
+            buf.append(" BobPort: ").append(_bobPort);
+            buf.append(" RelayTag: ").append(_receivedRelayTag);
+            buf.append(" SignedOn: ").append(_receivedSignedOnTime);
+            buf.append(" signature: ").append(Base64.encode(_receivedSignature.getData()));
+            _log.debug(buf.toString());
+        }
+        return _context.dsa().verifySignature(_receivedSignature, signed, _remotePeer.getSigningPublicKey());
+    }
+    
+    public synchronized SessionKey getCipherKey() { return _sessionKey; }
+    public synchronized SessionKey getMACKey() { return _macKey; }
+
+    public synchronized long getReceivedRelayTag() { return _receivedRelayTag; }
+    public synchronized long getSentSignedOnTime() { return _sentSignedOnTime; }
+    public synchronized long getReceivedSignedOnTime() { return _receivedSignedOnTime; }
+    public synchronized byte[] getReceivedIP() { return _aliceIP; }
+    public synchronized int getReceivedPort() { return _alicePort; }
+    
+    /**
+     * Lets sign everything so we can fragment properly
+     *
+     */
+    public synchronized void prepareSessionConfirmed() {
+        if (_sentSignedOnTime > 0)
+            return;
+        byte signed[] = new byte[_aliceIP.length + 2
+                             + _bobIP.length + 2
+                             + 4 // Alice's relay key
+                             + 4 // signed on time
+                             ];
+
+        _sentSignedOnTime = _context.clock().now() / 1000;
+        
+        int off = 0;
+        System.arraycopy(_aliceIP, 0, signed, off, _aliceIP.length);
+        off += _aliceIP.length;
+        DataHelper.toLong(signed, off, 2, _alicePort);
+        off += 2;
+        System.arraycopy(_bobIP, 0, signed, off, _bobIP.length);
+        off += _bobIP.length;
+        DataHelper.toLong(signed, off, 2, _bobPort);
+        off += 2;
+        DataHelper.toLong(signed, off, 4, _receivedRelayTag);
+        off += 4;
+        DataHelper.toLong(signed, off, 4, _sentSignedOnTime);
+        _sentSignature = _context.dsa().sign(signed, _context.keyManager().getSigningPrivateKey());
+    }
+    
+    public synchronized Signature getSentSignature() { return _sentSignature; }
+    
+    /** note that we just sent the SessionConfirmed packet */
+    public synchronized void confirmedPacketsSent() {
+        _lastSend = _context.clock().now();
+        _nextSend = _lastSend + 5*1000;
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Send confirm packets, nextSend = 5s");
+        if ( (_currentState == STATE_UNKNOWN) || 
+             (_currentState == STATE_REQUEST_SENT) ||
+             (_currentState == STATE_CREATED_RECEIVED) )
+            _currentState = STATE_CONFIRMED_PARTIALLY;
+    }
+    /** note that we just sent the SessionRequest packet */
+    public synchronized void requestSent() {
+        _lastSend = _context.clock().now();
+        _nextSend = _lastSend + 5*1000;
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Send a request packet, nextSend = 5s");
+        if (_currentState == STATE_UNKNOWN)
+            _currentState = STATE_REQUEST_SENT;
+    }
+    
+    /** how long have we been trying to establish this session? */
+    public synchronized long getLifetime() { return _context.clock().now() - _establishBegin; }
+    public synchronized long getEstablishBeginTime() { return _establishBegin; }
+    public synchronized long getNextSendTime() { return _nextSend; }
+    public synchronized void setNextSendTime(long when) { 
+        _nextSend = when; 
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Explicit nextSend=" + (_nextSend-_context.clock().now()), new Exception("Set by"));
+    }
+
+    /** host+port, uniquely identifies an attempt */
+    public String getRemoteHostInfo() { return _remoteHostInfo; }
+
+    /** we have received a real data packet, so we're done establishing */
+    public synchronized void dataReceived() {
+        packetReceived();
+        _currentState = STATE_CONFIRMED_COMPLETELY;
+    }
+    
+    private void packetReceived() {
+        _lastReceive = _context.clock().now();
+        _nextSend = _lastReceive;
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Got a packet, nextSend == now");
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java b/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java
new file mode 100644
index 0000000000000000000000000000000000000000..60c2503a2c767ff2934e8e4c1eee461628da7f93
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java
@@ -0,0 +1,358 @@
+package net.i2p.router.transport.udp;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.i2p.data.Hash;
+import net.i2p.router.OutNetMessage;
+import net.i2p.router.RouterContext;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+/**
+ * Coordinate the outbound fragments and select the next one to be built.
+ * This pool contains messages we are actively trying to send, essentially 
+ * doing a round robin across each message to send one fragment, as implemented
+ * in {@link #getNextPacket()}.  This also honors per-peer throttling, taking 
+ * note of each peer's allocations.  If a message has each of its fragments
+ * sent more than a certain number of times, it is failed out.  In addition, 
+ * this instance also receives notification of message ACKs from the 
+ * {@link InboundMessageFragments}, signaling that we can stop sending a 
+ * message.
+ * 
+ */
+public class OutboundMessageFragments {
+    private RouterContext _context;
+    private Log _log;
+    private UDPTransport _transport;
+    /** OutboundMessageState for messages being sent */
+    private List _activeMessages;
+    private boolean _alive;
+    /** which message should we build the next packet out of? */
+    private int _nextPacketMessage;
+    private PacketBuilder _builder;
+    
+    private static final int MAX_ACTIVE = 64;
+    // don't send a packet more than 10 times
+    private static final int MAX_VOLLEYS = 10;
+    
+    public OutboundMessageFragments(RouterContext ctx, UDPTransport transport) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(OutboundMessageFragments.class);
+        _transport = transport;
+        _activeMessages = new ArrayList(MAX_ACTIVE);
+        _nextPacketMessage = 0;
+        _builder = new PacketBuilder(ctx, _transport);
+        _alive = true;
+        _context.statManager().createRateStat("udp.sendVolleyTime", "Long it takes to send a full volley", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+        _context.statManager().createRateStat("udp.sendConfirmTime", "How long it takes to send a message and get the ACK", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+        _context.statManager().createRateStat("udp.sendConfirmFragments", "How many fragments are included in a fully ACKed message", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+        _context.statManager().createRateStat("udp.sendConfirmVolley", "How many times did fragments need to be sent before ACK", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+        _context.statManager().createRateStat("udp.sendFailed", "How many fragments were in a message that couldn't be delivered", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+        _context.statManager().createRateStat("udp.sendAggressiveFailed", "How many volleys was a packet sent before we gave up", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
+    }
+    
+    public void startup() { _alive = true; }
+    public void shutdown() {
+        _alive = false;
+        synchronized (_activeMessages) {
+            _activeMessages.notifyAll();
+        }
+    }
+    
+    /**
+     * Block until we allow more messages to be admitted to the active
+     * pool.  This is called by the {@link OutboundRefiller}
+     *
+     * @return true if more messages are allowed
+     */
+    public boolean waitForMoreAllowed() {
+        while (_alive) {
+            finishMessages();
+            try {
+                synchronized (_activeMessages) {
+                    if (!_alive)
+                        return false;
+                    else if (_activeMessages.size() < MAX_ACTIVE)
+                        return true;
+                    else 
+                        _activeMessages.wait();
+                }
+            } catch (InterruptedException ie) {}
+        }
+        return false;
+    }
+    
+    /**
+     * Add a new message to the active pool
+     *
+     */
+    public void add(OutNetMessage msg) {
+        OutboundMessageState state = new OutboundMessageState(_context);
+        state.initialize(msg);
+        finishMessages();
+        synchronized (_activeMessages) {
+            _activeMessages.add(state);
+            _activeMessages.notifyAll();
+        }
+    }
+    
+    /** 
+     * short circuit the OutNetMessage, letting us send the establish 
+     * complete message reliably
+     */
+    public void add(OutboundMessageState state) {
+        synchronized (_activeMessages) {
+            _activeMessages.add(state);
+            _activeMessages.notifyAll();
+        }
+    }
+
+    /**
+     * Remove any expired or complete messages
+     */
+    private void finishMessages() {
+        synchronized (_activeMessages) {
+            for (int i = 0; i < _activeMessages.size(); i++) {
+                OutboundMessageState state = (OutboundMessageState)_activeMessages.get(i);
+                if (state.isComplete()) {
+                    _activeMessages.remove(i);
+                    _transport.succeeded(state.getMessage());
+                    i--;
+                } else if (state.isExpired()) {
+                    _activeMessages.remove(i);
+                    _context.statManager().addRateData("udp.sendFailed", state.getFragmentCount(), state.getLifetime());
+
+                    if (state.getMessage() != null) {
+                        _transport.failed(state.getMessage());
+                    } else {
+                        // it can not have an OutNetMessage if the source is the
+                        // final after establishment message
+                        if (_log.shouldLog(Log.WARN))
+                            _log.warn("Unable to send a direct message: " + state);
+                    }
+                    i--;
+                } else if (state.getPushCount() > MAX_VOLLEYS) {
+                    _activeMessages.remove(i);
+                    _context.statManager().addRateData("udp.sendAggressiveFailed", state.getPushCount(), state.getLifetime());
+                    if (state.getPeer() != null)
+                        state.getPeer().congestionOccurred();
+
+                    if (state.getMessage() != null) {
+                        _transport.failed(state.getMessage());
+                    } else {
+                        // it can not have an OutNetMessage if the source is the
+                        // final after establishment message
+                        if (_log.shouldLog(Log.WARN))
+                            _log.warn("Unable to send a direct message: " + state);
+                    }
+                    i--;
+                    
+                }
+            }
+        }
+    }
+    
+    /**
+     * Grab the next packet that we want to send, blocking until one is ready.
+     * This is the main driver for the packet scheduler
+     *
+     */
+    public UDPPacket getNextPacket() {
+        PeerState peer = null;
+        OutboundMessageState state = null;
+        int currentFragment = -1;
+        while (_alive && (currentFragment < 0) ) {
+            long now = _context.clock().now();
+            long nextSend = -1;
+            finishMessages();
+            synchronized (_activeMessages) {
+                for (int i = 0; i < _activeMessages.size(); i++) {
+                    int cur = (i + _nextPacketMessage) % _activeMessages.size();
+                    state = (OutboundMessageState)_activeMessages.get(cur);
+                    if (state.getNextSendTime() <= now) {
+                        peer = state.getPeer(); // known if this is immediately after establish
+                        if (peer == null)
+                            peer = _transport.getPeerState(state.getMessage().getTarget().getIdentity().calculateHash());
+                        
+                        if (peer == null) {
+                            // peer disconnected (whatever that means)
+                            _activeMessages.remove(cur);
+                            _transport.failed(state.getMessage());
+                            if (_log.shouldLog(Log.WARN))
+                                _log.warn("Peer disconnected for " + state);
+                            i--;
+                        } else {
+                            if (!state.isFragmented()) {
+                                state.fragment(fragmentSize(peer.getMTU()));
+                                
+                                if (_log.shouldLog(Log.INFO))
+                                    _log.info("Fragmenting " + state);
+                            }
+                            
+                            int oldVolley = state.getPushCount();
+                            // pickNextFragment increments the pushCount every
+                            // time we cycle through all of the packets
+                            currentFragment = state.pickNextFragment();
+
+                            int fragmentSize = state.fragmentSize(currentFragment);
+                            if (peer.allocateSendingBytes(fragmentSize)) {
+                                if (_log.shouldLog(Log.INFO))
+                                    _log.info("Allocation of " + fragmentSize + " allowed");
+                                
+                                // for fairness, we move on in a round robin
+                                _nextPacketMessage = i + 1;
+                                
+                                if (state.getPushCount() != oldVolley) {
+                                    _context.statManager().addRateData("udp.sendVolleyTime", state.getLifetime(), state.getFragmentCount());
+                                    state.setNextSendTime(now + 500);
+                                } else {
+                                    if (peer.getSendWindowBytesRemaining() > 0)
+                                        state.setNextSendTime(now);
+                                    else
+                                        state.setNextSendTime(now + 50 );
+                                }
+                                break;
+                            } else {
+                                if (_log.shouldLog(Log.WARN))
+                                    _log.warn("Allocation of " + fragmentSize + " rejected");
+                                state.setNextSendTime(now + _context.random().nextInt(500));
+                                currentFragment = -1;
+                            }
+                        }
+                    } 
+                    long time = state.getNextSendTime();
+                    if ( (nextSend < 0) || (time < nextSend) )
+                        nextSend = time;
+                }
+            
+                if (currentFragment < 0) {
+                    if (nextSend <= 0) {
+                        try {
+                            _activeMessages.wait(100);
+                        } catch (InterruptedException ie) {}
+                    } else {
+                        // none of the packets were eligible for sending
+                        long delay = nextSend - now;
+                        if (delay <= 0)
+                            delay = 10;
+                        if (delay > 500) 
+                            delay = 500;
+                        try {
+                            _activeMessages.wait(delay);
+                        } catch (InterruptedException ie) {}
+                    }
+                }
+            }
+        }
+        
+        if (currentFragment >= 0) {
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Building packet for fragment " + currentFragment 
+                          + " of " + state + " to " + peer);
+            UDPPacket rv = _builder.buildPacket(state, currentFragment, peer);
+            return rv;
+        } else {
+            // !alive
+            return null;
+        }
+    }
+    
+    private static final int SSU_HEADER_SIZE = 46;
+    private static final int UDP_HEADER_SIZE = 8;
+    private static final int IP_HEADER_SIZE = 20;
+    /** how much payload data can we shove in there? */
+    private static final int fragmentSize(int mtu) {
+        return mtu - SSU_HEADER_SIZE - UDP_HEADER_SIZE - IP_HEADER_SIZE;
+    }
+    
+    /**
+     * We received an ACK of the given messageId from the given peer, so if it
+     * is still unacked, mark it as complete. 
+     *
+     * @return fragments acked
+     */
+    public int acked(long messageId, Hash ackedBy) {
+        OutboundMessageState state = null;
+        synchronized (_activeMessages) {
+            // linear search, since its tiny
+            for (int i = 0; i < _activeMessages.size(); i++) {
+                state = (OutboundMessageState)_activeMessages.get(i);
+                if (state.getMessageId() == messageId) {
+                    OutNetMessage msg = state.getMessage();
+                    if (msg != null) {
+                        Hash expectedBy = msg.getTarget().getIdentity().getHash();
+                        if (!expectedBy.equals(ackedBy)) {
+                            state = null;
+                            return 0;
+                        }
+                    }
+                    // either the message was a short circuit after establishment,
+                    // or it was received from who we sent it to.  yay!
+                    _activeMessages.remove(i);
+                    _activeMessages.notifyAll();
+                    break;
+                } else {
+                    state = null;
+                }
+            }
+        }
+        
+        if (state != null) {
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Received ack of " + messageId + " by " + ackedBy.toBase64() 
+                          + " after " + state.getLifetime());
+            _context.statManager().addRateData("udp.sendConfirmTime", state.getLifetime(), state.getLifetime());
+            _context.statManager().addRateData("udp.sendConfirmFragments", state.getFragmentCount(), state.getLifetime());
+            int numSends = state.getMaxSends();
+            _context.statManager().addRateData("udp.sendConfirmVolley", numSends, state.getFragmentCount());
+            if ( (numSends > 1) && (state.getPeer() != null) )
+                state.getPeer().congestionOccurred();
+            _transport.succeeded(state.getMessage());
+            return state.getFragmentCount();
+        } else {
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("Received an ACK for a message not pending: " + messageId);
+            return 0;
+        }
+    }
+    
+    /**
+     * Receive a set of fragment ACKs for a given messageId from the 
+     * specified peer
+     *
+     */
+    public void acked(long messageId, int ackedFragments[], Hash ackedBy) {
+        if (_log.shouldLog(Log.INFO))
+            _log.info("Received partial ack of " + messageId + " by " + ackedBy.toBase64());
+        OutboundMessageState state = null;
+        synchronized (_activeMessages) {
+            // linear search, since its tiny
+            for (int i = 0; i < _activeMessages.size(); i++) {
+                state = (OutboundMessageState)_activeMessages.get(i);
+                if (state.getMessage().getMessageId() == messageId) {
+                    Hash expectedBy = state.getMessage().getTarget().getIdentity().calculateHash();
+                    if (!expectedBy.equals(ackedBy)) {
+                        return;
+                    } else {
+                        state.acked(ackedFragments);
+                        if (state.isComplete()) {
+                            _activeMessages.remove(i);
+                            _activeMessages.notifyAll();
+                        }
+                        break;
+                    }
+                }
+            }
+        }
+        
+        if ( (state != null) && (state.isComplete()) ) {
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Received ack of " + messageId + " by " + ackedBy.toBase64() 
+                          + " after " + state.getLifetime());
+            _context.statManager().addRateData("udp.sendConfirmTime", state.getLifetime(), state.getLifetime());
+            _context.statManager().addRateData("udp.sendConfirmFragments", state.getFragmentCount(), state.getLifetime());
+            _transport.succeeded(state.getMessage());
+        }
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java b/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java
new file mode 100644
index 0000000000000000000000000000000000000000..cff837dace30214755b55b1516d63bf65f9e189c
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java
@@ -0,0 +1,232 @@
+package net.i2p.router.transport.udp;
+
+import java.util.Arrays;
+import net.i2p.data.Base64;
+import net.i2p.data.ByteArray;
+import net.i2p.data.i2np.I2NPMessage;
+import net.i2p.router.RouterContext;
+import net.i2p.router.OutNetMessage;
+import net.i2p.util.ByteCache;
+import net.i2p.util.Log;
+
+/**
+ * Maintain the outbound fragmentation for resending
+ *
+ */
+public class OutboundMessageState {
+    private RouterContext _context;
+    private Log _log;
+    /** may be null if we are part of the establishment */
+    private OutNetMessage _message;
+    private long _messageId;
+    /** will be null, unless we are part of the establishment */
+    private PeerState _peer;
+    private long _expiration;
+    private ByteArray _messageBuf;
+    /** fixed fragment size across the message */
+    private int _fragmentSize;
+    /** sends[i] is how many times the fragment has been sent, or -1 if ACKed */
+    private short _fragmentSends[];
+    private long _startedOn;
+    private long _nextSendTime;
+    private int _pushCount;
+    private short _maxSends;
+    
+    public static final int MAX_FRAGMENTS = 32;
+    private static final ByteCache _cache = ByteCache.getInstance(64, MAX_FRAGMENTS*1024);
+    
+    public OutboundMessageState(RouterContext context) {
+        _context = context;
+        _log = _context.logManager().getLog(OutboundMessageState.class);
+        _pushCount = 0;
+        _maxSends = 0;
+    }
+    
+    public synchronized void initialize(OutNetMessage msg) {
+        initialize(msg, msg.getMessage(), null);
+    }
+    
+    public void initialize(I2NPMessage msg, PeerState peer) {
+        initialize(null, msg, peer);
+    }
+    
+    private void initialize(OutNetMessage m, I2NPMessage msg, PeerState peer) {
+        _message = m;
+        _peer = peer;
+        if (_messageBuf != null) {
+            _cache.release(_messageBuf);
+            _messageBuf = null;
+        }
+
+        _messageBuf = _cache.acquire();
+        int size = msg.getRawMessageSize();
+        if (size > _messageBuf.getData().length)
+            throw new IllegalArgumentException("Size too large!  " + size + " in " + msg);
+        int len = msg.toRawByteArray(_messageBuf.getData());
+        _messageBuf.setValid(len);
+        _messageId = msg.getUniqueId();
+        
+        _startedOn = _context.clock().now();
+        _nextSendTime = _startedOn;
+        _expiration = _startedOn + 10*1000;
+        //_expiration = msg.getExpiration();
+        
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Raw byte array for " + _messageId + ": " + Base64.encode(_messageBuf.getData(), 0, len));
+    }
+    
+    public OutNetMessage getMessage() { return _message; }
+    public long getMessageId() { return _messageId; }
+    public PeerState getPeer() { return _peer; }
+    public boolean isExpired() {
+        return _expiration < _context.clock().now(); 
+    }
+    public boolean isComplete() {
+        if (_fragmentSends == null) return false;
+        for (int i = 0; i < _fragmentSends.length; i++)
+            if (_fragmentSends[i] >= 0)
+                return false;
+        // nothing else pending ack
+        return true;
+    }
+    public long getLifetime() { return _context.clock().now() - _startedOn; }
+    
+    /**
+     * Ack all the fragments in the ack list
+     */
+    public void acked(int ackedFragments[]) {
+        // stupid brute force, but the cardinality should be trivial
+        for (int i = 0; i < ackedFragments.length; i++) {
+            if ( (ackedFragments[i] < 0) || (ackedFragments[i] >= _fragmentSends.length) )
+                continue;
+            _fragmentSends[ackedFragments[i]] = -1;
+        }
+    }
+    
+    public long getNextSendTime() { return _nextSendTime; }
+    public void setNextSendTime(long when) { _nextSendTime = when; }
+    public int getMaxSends() { return _maxSends; }
+    public int getPushCount() { return _pushCount; }
+    /** note that we have pushed the message fragments */
+    public void push() { _pushCount++; }
+    public boolean isFragmented() { return _fragmentSends != null; }
+    /**
+     * Prepare the message for fragmented delivery, using no more than
+     * fragmentSize bytes per fragment.
+     *
+     */
+    public void fragment(int fragmentSize) {
+        int totalSize = _messageBuf.getValid();
+        int numFragments = totalSize / fragmentSize;
+        if (numFragments * fragmentSize != totalSize)
+            numFragments++;
+        
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Fragmenting a " + totalSize + " message into " + numFragments + " fragments");
+        
+        //_fragmentEnd = new int[numFragments];
+        _fragmentSends = new short[numFragments];
+        //Arrays.fill(_fragmentEnd, -1);
+        Arrays.fill(_fragmentSends, (short)0);
+        
+        _fragmentSize = fragmentSize;
+    }
+    /** how many fragments in the message */
+    public int getFragmentCount() { 
+        if (_fragmentSends == null) 
+            return -1;
+        else
+            return _fragmentSends.length; 
+    }
+    /** should we continue sending this fragment? */
+    public boolean shouldSend(int fragmentNum) { return _fragmentSends[fragmentNum] >= (short)0; }
+    public int fragmentSize(int fragmentNum) {
+        if (fragmentNum + 1 == _fragmentSends.length)
+            return _messageBuf.getValid() % _fragmentSize;
+        else
+            return _fragmentSize;
+    }
+
+    /**
+     * Pick a fragment that we still need to send.  Current implementation 
+     * picks the fragment which has been sent the least (randomly choosing 
+     * among equals), incrementing the # sends of the winner in the process.
+     *
+     * @return fragment index, or -1 if all of the fragments were acked
+     */
+    public int pickNextFragment() {
+        short minValue = -1;
+        int minIndex = -1;
+        int startOffset = _context.random().nextInt(_fragmentSends.length);
+        for (int i = 0; i < _fragmentSends.length; i++) {
+            int cur = (i + startOffset) % _fragmentSends.length;
+            if (_fragmentSends[cur] < (short)0)
+                continue;
+            else if ( (minValue < (short)0) || (_fragmentSends[cur] < minValue) ) {
+                minValue = _fragmentSends[cur];
+                minIndex = cur;
+            }
+        }
+        if (minIndex >= 0) {
+            _fragmentSends[minIndex]++;
+            if (_fragmentSends[minIndex] > _maxSends)
+                _maxSends = _fragmentSends[minIndex];
+        }
+        
+        // if all fragments have now been sent an equal number of times,
+        // lets give pause for an ACK
+        boolean endOfVolley = true;
+        for (int i = 0; i < _fragmentSends.length; i++) {
+            if (_fragmentSends[i] < (short)0)
+                continue;
+            if (_fragmentSends[i] != (short)_pushCount+1) {
+                endOfVolley = false;
+                break;
+            }
+        }
+        if (endOfVolley)
+            _pushCount++;
+        
+        
+        if (_log.shouldLog(Log.DEBUG)) {
+            StringBuffer buf = new StringBuffer(64);
+            buf.append("Next fragment is ").append(minIndex);
+            if (minIndex >= 0) {
+                buf.append(" (#sends: ").append(_fragmentSends[minIndex]-1);
+                buf.append(" #fragments: ").append(_fragmentSends.length);
+                buf.append(")");
+            }
+            _log.debug(buf.toString());
+        }
+        return minIndex;
+    }
+    
+    /**
+     * Write a part of the the message onto the specified buffer.
+     *
+     * @param out target to write
+     * @param outOffset into outOffset to begin writing
+     * @param fragmentNum fragment to write (0 indexed)
+     * @return bytesWritten
+     */
+    public synchronized int writeFragment(byte out[], int outOffset, int fragmentNum) {
+        int start = _fragmentSize * fragmentNum;
+        int end = start + _fragmentSize;
+        if (end > _messageBuf.getValid())
+            end = _messageBuf.getValid();
+        int toSend = end - start;
+        System.arraycopy(_messageBuf.getData(), start, out, outOffset, toSend);
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Raw fragment[" + fragmentNum + "] for " + _messageId + ": " 
+                       + Base64.encode(_messageBuf.getData(), start, toSend));
+        return toSend;
+    }
+    
+    public String toString() {
+        StringBuffer buf = new StringBuffer(64);
+        buf.append("Message ").append(_messageId);
+        if (_fragmentSends != null)
+            buf.append(" with ").append(_fragmentSends.length).append(" fragments");
+        return buf.toString();
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundRefiller.java b/router/java/src/net/i2p/router/transport/udp/OutboundRefiller.java
new file mode 100644
index 0000000000000000000000000000000000000000..ee41ad8c096a91bb188050062fa813680b65bf15
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundRefiller.java
@@ -0,0 +1,62 @@
+package net.i2p.router.transport.udp;
+
+import net.i2p.router.OutNetMessage;
+import net.i2p.router.RouterContext;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+   
+/**
+ * Blocking thread to grab new messages off the outbound queue and
+ * plopping them into our active pool.  
+ *
+ */
+public class OutboundRefiller implements Runnable {
+    private RouterContext _context;
+    private Log _log;
+    private OutboundMessageFragments _fragments;
+    private MessageQueue _messages;
+    private boolean _alive;
+    private Object _refillLock;
+    
+    public OutboundRefiller(RouterContext ctx, OutboundMessageFragments fragments, MessageQueue messages) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(OutboundRefiller.class);
+        _fragments = fragments;
+        _messages = messages;
+        _refillLock = this;
+        _context.statManager().createRateStat("udp.timeToActive", "Message lifetime until it reaches the outbound fragment queue", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
+    }
+    
+    public void startup() {
+        _alive = true;
+        I2PThread t = new I2PThread(this, "UDP outbound refiller");
+        t.setDaemon(true);
+        t.start();
+    }
+    public void shutdown() { _alive = false; }
+     
+    public void run() {
+        while (_alive) {
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Check the fragments to see if we can add more...");
+            boolean wantMore = _fragments.waitForMoreAllowed();
+            if (wantMore) {
+                if (_log.shouldLog(Log.DEBUG))
+                    _log.debug("Want more fragments...");
+                OutNetMessage msg = _messages.getNext(-1);
+                if (msg != null) {
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug("New message found to fragments: " + msg);
+                    _context.statManager().addRateData("udp.timeToActive", msg.getLifetime(), msg.getLifetime());
+                    _fragments.add(msg);
+                } else {
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug("No message found to fragment");
+                }
+            } else {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("No more fragments allowed, looping");
+            }
+        }
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java b/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java
new file mode 100644
index 0000000000000000000000000000000000000000..cdeb8916956cf0d6d7e8255476d08804a0c77d72
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java
@@ -0,0 +1,445 @@
+package net.i2p.router.transport.udp;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+import net.i2p.data.Base64;
+import net.i2p.data.ByteArray;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Hash;
+import net.i2p.data.SessionKey;
+import net.i2p.data.Signature;
+import net.i2p.router.RouterContext;
+import net.i2p.util.ByteCache;
+import net.i2p.util.Log;
+
+/**
+ * Big ol' class to do all our packet formatting.  The UDPPackets generated are
+ * fully authenticated, encrypted, and configured for delivery to the peer. 
+ *
+ */
+public class PacketBuilder {
+    private RouterContext _context;
+    private Log _log;
+    private UDPTransport _transport;
+    
+    private static final ByteCache _ivCache = ByteCache.getInstance(64, UDPPacket.IV_SIZE);
+    
+    public PacketBuilder(RouterContext ctx, UDPTransport transport) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(PacketBuilder.class);
+        _transport = transport;
+    }
+    
+    public UDPPacket buildPacket(OutboundMessageState state, int fragment, PeerState peer) {
+        UDPPacket packet = UDPPacket.acquire(_context);
+        
+        byte data[] = packet.getPacket().getData();
+        Arrays.fill(data, 0, data.length, (byte)0x0);
+        int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
+        
+        // header
+        data[off] |= (UDPPacket.PAYLOAD_TYPE_DATA << 4);
+        // todo: add support for rekeying and extended options
+        off++;
+        long now = _context.clock().now() / 1000;
+        DataHelper.toLong(data, off, 4, now);
+        off += 4;
+        
+        // ok, now for the body...
+        
+        // just always ask for an ACK for now...
+        data[off] |= UDPPacket.DATA_FLAG_WANT_REPLY;
+        off++;
+        
+        DataHelper.toLong(data, off, 1, 1); // only one fragment in this message
+        off++;
+        
+        DataHelper.toLong(data, off, 4, state.getMessageId());
+        off += 4;
+        
+        data[off] |= fragment << 3;
+        if (fragment == state.getFragmentCount() - 1)
+            data[off] |= 1 << 2; // isLast
+        off++;
+        
+        DataHelper.toLong(data, off, 2, state.fragmentSize(fragment));
+        off += 2;
+        
+        off += state.writeFragment(data, off, fragment);
+
+        // we can pad here if we want, maybe randomized?
+        
+        // pad up so we're on the encryption boundary
+        if ( (off % 16) != 0)
+            off += 16 - (off % 16);
+        packet.getPacket().setLength(off);
+        authenticate(packet, peer.getCurrentCipherKey(), peer.getCurrentMACKey());
+        setTo(packet, peer.getRemoteIP(), peer.getRemotePort());
+        return packet;
+    }
+    
+    public UDPPacket buildACK(PeerState peer, List ackedMessageIds) {
+        UDPPacket packet = UDPPacket.acquire(_context);
+        
+        byte data[] = packet.getPacket().getData();
+        Arrays.fill(data, 0, data.length, (byte)0x0);
+        int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
+        
+        // header
+        data[off] |= (UDPPacket.PAYLOAD_TYPE_DATA << 4);
+        // todo: add support for rekeying and extended options
+        off++;
+        long now = _context.clock().now() / 1000;
+        DataHelper.toLong(data, off, 4, now);
+        off += 4;
+        
+        // ok, now for the body...
+        data[off] |= UDPPacket.DATA_FLAG_EXPLICIT_ACK;
+        // add ECN if (peer.getSomethingOrOther())
+        off++;
+        
+        DataHelper.toLong(data, off, 1, ackedMessageIds.size());
+        off++;
+        for (int i = 0; i < ackedMessageIds.size(); i++) {
+            Long id = (Long)ackedMessageIds.get(i);
+            DataHelper.toLong(data, off, 4, id.longValue());
+            off += 4;
+        }
+        
+        DataHelper.toLong(data, off, 1, 0); // no fragments in this message
+        off++;
+        
+        // we can pad here if we want, maybe randomized?
+        
+        // pad up so we're on the encryption boundary
+        if ( (off % 16) != 0)
+            off += 16 - (off % 16);
+        packet.getPacket().setLength(off);
+        authenticate(packet, peer.getCurrentCipherKey(), peer.getCurrentMACKey());
+        setTo(packet, peer.getRemoteIP(), peer.getRemotePort());
+        return packet;
+    }
+    
+    /** 
+     * full flag info for a sessionCreated message.  this can be fixed, 
+     * since we never rekey on startup, and don't need any extended options
+     */
+    private static final byte SESSION_CREATED_FLAG_BYTE = (UDPPacket.PAYLOAD_TYPE_SESSION_CREATED << 4);
+    
+    /**
+     * Build a new SessionCreated packet for the given peer, encrypting it 
+     * as necessary.
+     * 
+     * @return ready to send packet, or null if there was a problem
+     */
+    public UDPPacket buildSessionCreatedPacket(InboundEstablishState state) {
+        UDPPacket packet = UDPPacket.acquire(_context);
+        try {
+            packet.getPacket().setAddress(InetAddress.getByAddress(state.getSentIP()));
+        } catch (UnknownHostException uhe) {
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("How did we think this was a valid IP?  " + state.getRemoteHostInfo());
+            return null;
+        }
+        
+        state.prepareSessionCreated();
+        
+        byte data[] = packet.getPacket().getData();
+        Arrays.fill(data, 0, data.length, (byte)0x0);
+        int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
+        
+        // header
+        data[off] = SESSION_CREATED_FLAG_BYTE;
+        off++;
+        long now = _context.clock().now() / 1000;
+        DataHelper.toLong(data, off, 4, now);
+        off += 4;
+        
+        // now for the body
+        System.arraycopy(state.getSentY(), 0, data, off, state.getSentY().length);
+        off += state.getSentY().length;
+        DataHelper.toLong(data, off, 1, state.getSentIP().length);
+        off += 1;
+        System.arraycopy(state.getSentIP(), 0, data, off, state.getSentIP().length);
+        off += state.getSentIP().length;
+        DataHelper.toLong(data, off, 2, state.getSentPort());
+        off += 2;
+        DataHelper.toLong(data, off, 4, state.getSentRelayTag());
+        off += 4;
+        DataHelper.toLong(data, off, 4, state.getSentSignedOnTime());
+        off += 4;
+        System.arraycopy(state.getSentSignature().getData(), 0, data, off, Signature.SIGNATURE_BYTES);
+        off += Signature.SIGNATURE_BYTES;
+        // ok, we need another 8 bytes of random padding
+        // (ok, this only gives us 63 bits, not 64)
+        long l = _context.random().nextLong();
+        if (l < 0) l = 0 - l;
+        DataHelper.toLong(data, off, 8, l);
+        off += 8;
+        
+        if (_log.shouldLog(Log.DEBUG)) {
+            StringBuffer buf = new StringBuffer(128);
+            buf.append("Sending sessionCreated:");
+            buf.append(" AliceIP: ").append(Base64.encode(state.getSentIP()));
+            buf.append(" AlicePort: ").append(state.getSentPort());
+            buf.append(" BobIP: ").append(Base64.encode(state.getReceivedOurIP()));
+            buf.append(" BobPort: ").append(_transport.getExternalPort());
+            buf.append(" RelayTag: ").append(state.getSentRelayTag());
+            buf.append(" SignedOn: ").append(state.getSentSignedOnTime());
+            buf.append(" signature: ").append(Base64.encode(state.getSentSignature().getData()));
+            buf.append("\nRawCreated: ").append(Base64.encode(data, 0, off)); 
+            buf.append("\nsignedTime: ").append(Base64.encode(data, off-8-Signature.SIGNATURE_BYTES-4, 4));
+            _log.debug(buf.toString());
+        }
+        
+        // ok, now the full data is in there, but we also need to encrypt
+        // the signature, which means we need the IV
+        ByteArray iv = _ivCache.acquire();
+        _context.random().nextBytes(iv.getData());
+        
+        int encrWrite = Signature.SIGNATURE_BYTES + 8;
+        int sigBegin = off - encrWrite;
+        _context.aes().encrypt(data, sigBegin, data, sigBegin, state.getCipherKey(), iv.getData(), encrWrite);
+        
+        // pad up so we're on the encryption boundary
+        if ( (off % 16) != 0)
+            off += 16 - (off % 16);
+        packet.getPacket().setLength(off);
+        authenticate(packet, _transport.getIntroKey(), _transport.getIntroKey(), iv);
+        setTo(packet, state.getSentIP(), state.getSentPort());
+        _ivCache.release(iv);
+        return packet;
+    }
+    
+    /** 
+     * full flag info for a sessionRequest message.  this can be fixed, 
+     * since we never rekey on startup, and don't need any extended options
+     */
+    private static final byte SESSION_REQUEST_FLAG_BYTE = (UDPPacket.PAYLOAD_TYPE_SESSION_REQUEST << 4);
+    
+    /**
+     * Build a new SessionRequest packet for the given peer, encrypting it 
+     * as necessary.
+     * 
+     * @return ready to send packet, or null if there was a problem
+     */
+    public UDPPacket buildSessionRequestPacket(OutboundEstablishState state) {
+        UDPPacket packet = UDPPacket.acquire(_context);
+        try {
+            packet.getPacket().setAddress(InetAddress.getByAddress(state.getSentIP()));
+        } catch (UnknownHostException uhe) {
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("How did we think this was a valid IP?  " + state.getRemoteHostInfo());
+            return null;
+        }
+        
+        byte data[] = packet.getPacket().getData();
+        Arrays.fill(data, 0, data.length, (byte)0x0);
+        int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
+        
+        // header
+        data[off] = SESSION_REQUEST_FLAG_BYTE;
+        off++;
+        long now = _context.clock().now() / 1000;
+        DataHelper.toLong(data, off, 4, now);
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Sending request with time = " + new Date(now*1000));
+        off += 4;
+        
+        // now for the body
+        System.arraycopy(state.getSentX(), 0, data, off, state.getSentX().length);
+        off += state.getSentX().length;
+        DataHelper.toLong(data, off, 1, state.getSentIP().length);
+        off += 1;
+        System.arraycopy(state.getSentIP(), 0, data, off, state.getSentIP().length);
+        off += state.getSentIP().length;
+        DataHelper.toLong(data, off, 2, state.getSentPort());
+        off += 2;
+        
+        // we can pad here if we want, maybe randomized?
+        
+        // pad up so we're on the encryption boundary
+        if ( (off % 16) != 0)
+            off += 16 - (off % 16);
+        packet.getPacket().setLength(off);
+        authenticate(packet, state.getIntroKey(), state.getIntroKey());
+        setTo(packet, state.getSentIP(), state.getSentPort());
+        return packet;
+    }
+
+    private static final int MAX_IDENTITY_FRAGMENT_SIZE = 512;
+    
+    /**
+     * Build a new series of SessionConfirmed packets for the given peer, 
+     * encrypting it as necessary.
+     * 
+     * @return ready to send packets, or null if there was a problem
+     */
+    public UDPPacket[] buildSessionConfirmedPackets(OutboundEstablishState state) {
+        byte identity[] = _context.router().getRouterInfo().getIdentity().toByteArray();
+        int numFragments = identity.length / MAX_IDENTITY_FRAGMENT_SIZE;
+        if (numFragments * MAX_IDENTITY_FRAGMENT_SIZE != identity.length)
+            numFragments++;
+        UDPPacket packets[] = new UDPPacket[numFragments];
+        for (int i = 0; i < numFragments; i++)
+            packets[i] = buildSessionConfirmedPacket(state, i, numFragments, identity);
+        return packets;
+    }
+
+    
+    /** 
+     * full flag info for a sessionConfirmed message.  this can be fixed, 
+     * since we never rekey on startup, and don't need any extended options
+     */
+    private static final byte SESSION_CONFIRMED_FLAG_BYTE = (UDPPacket.PAYLOAD_TYPE_SESSION_CONFIRMED << 4);
+    
+    /**
+     * Build a new SessionConfirmed packet for the given peer
+     * 
+     * @return ready to send packets, or null if there was a problem
+     */
+    public UDPPacket buildSessionConfirmedPacket(OutboundEstablishState state, int fragmentNum, int numFragments, byte identity[]) {
+        UDPPacket packet = UDPPacket.acquire(_context);
+        try {
+            packet.getPacket().setAddress(InetAddress.getByAddress(state.getSentIP()));
+        } catch (UnknownHostException uhe) {
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("How did we think this was a valid IP?  " + state.getRemoteHostInfo());
+            return null;
+        }
+        
+        byte data[] = packet.getPacket().getData();
+        Arrays.fill(data, 0, data.length, (byte)0x0);
+        int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
+        
+        // header
+        data[off] = SESSION_CONFIRMED_FLAG_BYTE;
+        off++;
+        long now = _context.clock().now() / 1000;
+        DataHelper.toLong(data, off, 4, now);
+        off += 4;
+        
+        // now for the body
+        data[off] |= fragmentNum << 4;
+        data[off] |= (numFragments & 0xF);
+        off++;
+        
+        int curFragSize = MAX_IDENTITY_FRAGMENT_SIZE;
+        if (fragmentNum == numFragments-1) {
+            if (identity.length % MAX_IDENTITY_FRAGMENT_SIZE != 0)
+                curFragSize = identity.length % MAX_IDENTITY_FRAGMENT_SIZE;
+        }
+        
+        DataHelper.toLong(data, off, 2, curFragSize);
+        off += 2;
+        
+        int curFragOffset = fragmentNum * MAX_IDENTITY_FRAGMENT_SIZE;
+        System.arraycopy(identity, curFragOffset, data, off, curFragSize);
+        off += curFragSize;
+        
+        if (fragmentNum == numFragments - 1) {
+            DataHelper.toLong(data, off, 4, state.getSentSignedOnTime());
+            off += 4;
+            
+            int paddingRequired = 0;
+            // we need to pad this so we're at the encryption boundary
+            if ( (off + Signature.SIGNATURE_BYTES) % 16 != 0)
+                paddingRequired += 16 - ((off + Signature.SIGNATURE_BYTES) % 16);
+            
+            // add an arbitrary number of 16byte pad blocks too...
+            
+            for (int i = 0; i < paddingRequired; i++) {
+                data[off] = (byte)_context.random().nextInt(255);
+                off++;
+            }
+            
+            System.arraycopy(state.getSentSignature().getData(), 0, data, off, Signature.SIGNATURE_BYTES);
+            packet.getPacket().setLength(off + Signature.SIGNATURE_BYTES);
+            authenticate(packet, state.getCipherKey(), state.getMACKey());
+        } else {
+            // nothing more to add beyond the identity fragment, though we can
+            // pad here if we want.  maybe randomized?
+
+            // pad up so we're on the encryption boundary
+            if ( (off % 16) != 0)
+                off += 16 - (off % 16);
+            packet.getPacket().setLength(off);
+            authenticate(packet, state.getIntroKey(), state.getIntroKey());
+        } 
+        
+        setTo(packet, state.getSentIP(), state.getSentPort());
+        return packet;
+    }
+
+    private void setTo(UDPPacket packet, byte ip[], int port) {
+        try {
+            InetAddress to = InetAddress.getByAddress(ip);
+            packet.getPacket().setAddress(to);
+            packet.getPacket().setPort(port);
+        } catch (UnknownHostException uhe) {
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("Invalid IP? ", uhe);
+        }
+    }
+    
+    /**
+     * Encrypt the packet with the cipher key and a new random IV, generate a 
+     * MAC for that encrypted data and IV, and store the result in the packet.
+     *
+     * @param packet prepared packet with the first 32 bytes empty and a length
+     *               whose size is mod 16
+     * @param cipherKey key to encrypt the payload 
+     * @param macKey key to generate the, er, MAC
+     */
+    private void authenticate(UDPPacket packet, SessionKey cipherKey, SessionKey macKey) {
+        ByteArray iv = _ivCache.acquire();
+        _context.random().nextBytes(iv.getData());
+        authenticate(packet, cipherKey, macKey, iv);
+        _ivCache.release(iv);
+    }
+    
+    /**
+     * Encrypt the packet with the cipher key and the given IV, generate a 
+     * MAC for that encrypted data and IV, and store the result in the packet.
+     * The MAC used is: 
+     *     HMAC-SHA256(payload || IV || payloadLength, macKey)[0:15]
+     *
+     * @param packet prepared packet with the first 32 bytes empty and a length
+     *               whose size is mod 16
+     * @param cipherKey key to encrypt the payload 
+     * @param macKey key to generate the, er, MAC
+     * @param iv IV to deliver
+     */
+    private void authenticate(UDPPacket packet, SessionKey cipherKey, SessionKey macKey, ByteArray iv) {
+        int encryptOffset = packet.getPacket().getOffset() + UDPPacket.IV_SIZE + UDPPacket.MAC_SIZE;
+        int encryptSize = packet.getPacket().getLength() - UDPPacket.IV_SIZE - UDPPacket.MAC_SIZE - packet.getPacket().getOffset();
+        byte data[] = packet.getPacket().getData();
+        _context.aes().encrypt(data, encryptOffset, data, encryptOffset, cipherKey, iv.getData(), encryptSize);
+        
+        // ok, now we need to prepare things for the MAC, which requires reordering
+        int off = packet.getPacket().getOffset();
+        System.arraycopy(data, encryptOffset, data, off, encryptSize);
+        off += encryptSize;
+        System.arraycopy(iv.getData(), 0, data, off, UDPPacket.IV_SIZE);
+        off += UDPPacket.IV_SIZE;
+        DataHelper.toLong(data, off, 2, encryptSize);
+        
+        int hmacOff = packet.getPacket().getOffset();
+        int hmacLen = encryptSize + UDPPacket.IV_SIZE + 2;
+        Hash hmac = _context.hmac().calculate(macKey, data, hmacOff, hmacLen);
+        
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Authenticating " + packet.getPacket().getLength() +
+                       "\nIV: " + Base64.encode(iv.getData()) +
+                       "\nraw mac: " + hmac.toBase64() +
+                       "\nMAC key: " + macKey.toBase64());
+        // ok, now lets put it back where it belongs...
+        System.arraycopy(data, hmacOff, data, encryptOffset, encryptSize);
+        System.arraycopy(hmac.getData(), 0, data, hmacOff, UDPPacket.MAC_SIZE);
+        System.arraycopy(iv.getData(), 0, data, hmacOff + UDPPacket.MAC_SIZE, UDPPacket.IV_SIZE);
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..dacd2d8d7716e97cf245e24573923990b8ada010
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
@@ -0,0 +1,293 @@
+package net.i2p.router.transport.udp;
+
+import java.net.InetAddress;
+import java.util.Date;
+
+import net.i2p.data.Base64;
+import net.i2p.router.Router;
+import net.i2p.router.RouterContext;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+/**
+ * Pull inbound packets from the inbound receiver's queue, figure out what
+ * peer session they belong to (if any), authenticate and decrypt them 
+ * with the appropriate keys, and push them to the appropriate handler.  
+ * Data and ACK packets go to the InboundMessageFragments, the various 
+ * establishment packets go to the EstablishmentManager, and, once implemented,
+ * relay packets will go to the relay manager.  At the moment, this is 
+ * an actual pool of packet handler threads, each pulling off the inbound
+ * receiver's queue and pushing them as necessary.
+ *
+ */
+public class PacketHandler implements Runnable {
+    private RouterContext _context;
+    private Log _log;
+    private UDPTransport _transport;
+    private UDPEndpoint _endpoint;
+    private UDPPacketReader _reader;
+    private EstablishmentManager _establisher;
+    private InboundMessageFragments _inbound;
+    private boolean _keepReading;
+    
+    private static final int NUM_HANDLERS = 3;
+    
+    public PacketHandler(RouterContext ctx, UDPTransport transport, UDPEndpoint endpoint, EstablishmentManager establisher, InboundMessageFragments inbound) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(PacketHandler.class);
+        _transport = transport;
+        _endpoint = endpoint;
+        _establisher = establisher;
+        _inbound = inbound;
+        _reader = new UDPPacketReader(ctx);
+        _context.statManager().createRateStat("udp.handleTime", "How long it takes to handle a received packet after its been pulled off the queue", "udp", new long[] { 10*60*1000, 60*60*1000 });
+        _context.statManager().createRateStat("udp.queueTime", "How long after a packet is received can we begin handling it", "udp", new long[] { 10*60*1000, 60*60*1000 });
+        _context.statManager().createRateStat("udp.receivePacketSkew", "How long ago after the packet was sent did we receive it", "udp", new long[] { 10*60*1000, 60*60*1000 });
+    }
+    
+    public void startup() { 
+        _keepReading = true;
+        for (int i = 0; i < NUM_HANDLERS; i++) {
+            I2PThread t = new I2PThread(this, "Packet handler " + i + ": " + _endpoint.getListenPort());
+            t.setDaemon(true);
+            t.start();
+        }
+    }
+    
+    public void shutdown() { 
+        _keepReading = false; 
+    }
+    
+    public void run() {
+        while (_keepReading) {
+            UDPPacket packet = _endpoint.receive();
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Received the packet " + packet);
+            long queueTime = packet.getLifetime();
+            long handleStart = _context.clock().now();
+            handlePacket(packet);
+            long handleTime = _context.clock().now() - handleStart;
+            _context.statManager().addRateData("udp.handleTime", handleTime, packet.getLifetime());
+            _context.statManager().addRateData("udp.queueTime", queueTime, packet.getLifetime());
+            
+            if (handleTime > 1000) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Took " + handleTime + " to process the packet " 
+                              + packet + ": " + _reader);
+            }
+            
+            // back to the cache with thee!
+            packet.release();
+        }
+    }
+    
+    private void handlePacket(UDPPacket packet) {
+        if (packet == null) return;
+        
+        InetAddress remAddr = packet.getPacket().getAddress();
+        int remPort = packet.getPacket().getPort();
+        PeerState state = _transport.getPeerState(remAddr, remPort);
+        if (state == null) {
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Packet received is not for a connected peer");
+            InboundEstablishState est = _establisher.getInboundState(remAddr, remPort);
+            if (est != null) {
+                if (_log.shouldLog(Log.DEBUG))
+                    _log.debug("Packet received IS for an inbound establishment");
+                receivePacket(packet, est);
+            } else {
+                if (_log.shouldLog(Log.DEBUG))
+                    _log.debug("Packet received is not for an inbound establishment");
+                OutboundEstablishState oest = _establisher.getOutboundState(remAddr, remPort);
+                if (oest != null) {
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug("Packet received IS for an outbound establishment");
+                    receivePacket(packet, oest);
+                } else {
+                    if (_log.shouldLog(Log.DEBUG))
+                        _log.debug("Packet received is not for an inbound or outbound establishment");
+                    // ok, not already known establishment, try as a new one
+                    receivePacket(packet);
+                }
+            }
+        } else {
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Packet received IS for an existing peer");
+            receivePacket(packet, state);
+        }
+    }
+    
+    private void receivePacket(UDPPacket packet, PeerState state) {
+        boolean isValid = packet.validate(state.getCurrentMACKey());
+        if (!isValid) {
+            if (state.getNextMACKey() != null)
+                isValid = packet.validate(state.getNextMACKey());
+            if (!isValid) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Failed validation with existing con, trying as new con: " + packet);
+                
+                isValid = packet.validate(_transport.getIntroKey());
+                if (isValid) {
+                    // this is a stray packet from an inbound establishment
+                    // process, so try our intro key
+                    // (after an outbound establishment process, there wouldn't
+                    //  be any stray packets)
+                    if (_log.shouldLog(Log.INFO))
+                        _log.info("Validation with existing con failed, but validation as reestablish/stray passed");
+                    packet.decrypt(_transport.getIntroKey());
+                } else {
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn("Validation with existing con failed, and validation as reestablish failed too.  DROP");
+                    return;
+                }
+            } else {
+                packet.decrypt(state.getNextCipherKey());
+            }
+        } else {
+            packet.decrypt(state.getCurrentCipherKey());
+        }
+        
+        handlePacket(packet, state, null, null);
+    }
+    
+    private void receivePacket(UDPPacket packet) {
+        boolean isValid = packet.validate(_transport.getIntroKey());
+        if (!isValid) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Invalid introduction packet received: " + packet, new Exception("path"));
+            return;
+        } else {
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Valid introduction packet received: " + packet);
+        }
+        
+        packet.decrypt(_transport.getIntroKey());
+        handlePacket(packet, null, null, null);
+    }
+
+    private void receivePacket(UDPPacket packet, InboundEstablishState state) {
+        if ( (state != null) && (_log.shouldLog(Log.DEBUG)) ) {
+            StringBuffer buf = new StringBuffer(128);
+            buf.append("Attempting to receive a packet on a known inbound state: ");
+            buf.append(state);
+            buf.append(" MAC key: ").append(state.getMACKey());
+            buf.append(" intro key: ").append(_transport.getIntroKey());
+            _log.debug(buf.toString());
+        }
+        boolean isValid = false;
+        if (state.getMACKey() != null) {
+            isValid = packet.validate(state.getMACKey());
+            if (isValid) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Valid introduction packet received for inbound con: " + packet);
+
+                packet.decrypt(state.getCipherKey());
+                handlePacket(packet, null, null, null);
+                return;
+            } else {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Invalid introduction packet received for inbound con, falling back: " + packet);
+
+            }
+        }
+        // ok, we couldn't handle it with the established stuff, so fall back
+        // on earlier state packets
+        receivePacket(packet);
+    }
+
+    private void receivePacket(UDPPacket packet, OutboundEstablishState state) {
+        if ( (state != null) && (_log.shouldLog(Log.DEBUG)) ) {
+            StringBuffer buf = new StringBuffer(128);
+            buf.append("Attempting to receive a packet on a known outbound state: ");
+            buf.append(state);
+            buf.append(" MAC key: ").append(state.getMACKey());
+            buf.append(" intro key: ").append(state.getIntroKey());
+            _log.debug(buf.toString());
+        }
+        
+        boolean isValid = false;
+        if (state.getMACKey() != null) {
+            isValid = packet.validate(state.getMACKey());
+            if (isValid) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Valid introduction packet received for outbound established con: " + packet);
+                
+                packet.decrypt(state.getCipherKey());
+                handlePacket(packet, null, state, null);
+                return;
+            }
+        }
+        
+        // keys not yet exchanged, lets try it with the peer's intro key
+        isValid = packet.validate(state.getIntroKey());
+        if (isValid) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Valid introduction packet received for outbound established con with old intro key: " + packet);
+            packet.decrypt(state.getIntroKey());
+            handlePacket(packet, null, state, null);
+            return;
+        } else {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Invalid introduction packet received for outbound established con with old intro key, falling back: " + packet);
+        }
+        
+        // ok, we couldn't handle it with the established stuff, so fall back
+        // on earlier state packets
+        receivePacket(packet);
+    }
+
+    /** let packets be up to 30s slow */
+    private static final long GRACE_PERIOD = Router.CLOCK_FUDGE_FACTOR + 30*1000;
+    
+    /**
+     * Parse out the interesting bits and honor what it says
+     */
+    private void handlePacket(UDPPacket packet, PeerState state, OutboundEstablishState outState, InboundEstablishState inState) {
+        _reader.initialize(packet);
+        long now = _context.clock().now();
+        long when = _reader.readTimestamp() * 1000;
+        long skew = now - when;
+        if (skew > GRACE_PERIOD) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Packet too far in the future: " + new Date(when) + ": " + packet);
+            return;
+        } else if (skew < 0 - GRACE_PERIOD) {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Packet too far in the past: " + new Date(when) + ": " + packet);
+            return;
+        }
+        
+        _context.statManager().addRateData("udp.receivePacketSkew", skew, packet.getLifetime());
+        
+        InetAddress fromHost = packet.getPacket().getAddress();
+        int fromPort = packet.getPacket().getPort();
+        String from = PeerState.calculateRemoteHostString(fromHost.getAddress(), fromPort);
+        
+        switch (_reader.readPayloadType()) {
+            case UDPPacket.PAYLOAD_TYPE_SESSION_REQUEST:
+                _establisher.receiveSessionRequest(from, fromHost, fromPort, _reader);
+                break;
+            case UDPPacket.PAYLOAD_TYPE_SESSION_CONFIRMED:
+                _establisher.receiveSessionConfirmed(from, _reader);
+                break;
+            case UDPPacket.PAYLOAD_TYPE_SESSION_CREATED:
+                _establisher.receiveSessionCreated(from, _reader);
+                break;
+            case UDPPacket.PAYLOAD_TYPE_DATA:
+                if (outState != null)
+                    state = _establisher.receiveData(outState);
+                handleData(packet, state);
+                break;
+            default:
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Unknown payload type: " + _reader.readPayloadType());
+                return;
+        }
+    }
+    
+    private void handleData(UDPPacket packet, PeerState peer) {
+        if (_log.shouldLog(Log.INFO))
+            _log.info("Received new DATA packet from " + peer + ": " + packet);
+        _inbound.receiveData(peer, _reader.getDataReader());
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/PacketPusher.java b/router/java/src/net/i2p/router/transport/udp/PacketPusher.java
new file mode 100644
index 0000000000000000000000000000000000000000..392f70299190c9dfa8aca20885f0db192a701ff2
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/PacketPusher.java
@@ -0,0 +1,43 @@
+package net.i2p.router.transport.udp;
+
+import net.i2p.router.OutNetMessage;
+import net.i2p.router.RouterContext;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+   
+/**
+ * Blocking thread to grab new packets off the outbound fragment
+ * pool and toss 'em onto the outbound packet queue
+ *
+ */
+public class PacketPusher implements Runnable {
+    private RouterContext _context;
+    private Log _log;
+    private OutboundMessageFragments _fragments;
+    private UDPSender _sender;
+    private boolean _alive;
+    
+    public PacketPusher(RouterContext ctx, OutboundMessageFragments fragments, UDPSender sender) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(PacketPusher.class);
+        _fragments = fragments;
+        _sender = sender;
+    }
+    
+    public void startup() {
+        _alive = true;
+        I2PThread t = new I2PThread(this, "UDP packet pusher");
+        t.setDaemon(true);
+        t.start();
+    }
+    
+    public void shutdown() { _alive = false; }
+     
+    public void run() {
+        while (_alive) {
+            UDPPacket packet = _fragments.getNextPacket();
+            if (packet != null)
+                _sender.add(packet, true); // blocks
+        }
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/PeerState.java b/router/java/src/net/i2p/router/transport/udp/PeerState.java
new file mode 100644
index 0000000000000000000000000000000000000000..33cfde591a6347976ee285c3dc85cd3f6fcd0b5a
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/PeerState.java
@@ -0,0 +1,438 @@
+package net.i2p.router.transport.udp;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import java.net.InetAddress;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.Hash;
+import net.i2p.data.SessionKey;
+import net.i2p.util.Log;
+
+/**
+ * Contain all of the state about a UDP connection to a peer
+ *
+ */
+public class PeerState {
+    private I2PAppContext _context;
+    private Log _log;
+    /**
+     * The peer are we talking to.  This should be set as soon as this
+     * state is created if we are initiating a connection, but if we are
+     * receiving the connection this will be set only after the connection
+     * is established.
+     */
+    private Hash _remotePeer;
+    /** 
+     * The AES key used to verify packets, set only after the connection is
+     * established.  
+     */
+    private SessionKey _currentMACKey;
+    /**
+     * The AES key used to encrypt/decrypt packets, set only after the 
+     * connection is established.
+     */
+    private SessionKey _currentCipherKey;
+    /** 
+     * The pending AES key for verifying packets if we are rekeying the 
+     * connection, or null if we are not in the process of rekeying.
+     */
+    private SessionKey _nextMACKey;
+    /** 
+     * The pending AES key for encrypting/decrypting packets if we are 
+     * rekeying the connection, or null if we are not in the process 
+     * of rekeying.
+     */
+    private SessionKey _nextCipherKey;
+    /**
+     * The keying material used for the rekeying, or null if we are not in
+     * the process of rekeying.
+     */
+    private byte[] _nextKeyingMaterial;
+    /** true if we began the current rekeying, false otherwise */
+    private boolean _rekeyBeganLocally;
+    /** when were the current cipher and MAC keys established/rekeyed? */
+    private long _keyEstablishedTime;
+    /** how far off is the remote peer from our clock, in seconds? */
+    private short _clockSkew;
+    /** what is the current receive second, for congestion control? */
+    private long _currentReceiveSecond;
+    /** when did we last send them a packet? */
+    private long _lastSendTime;
+    /** when did we last receive a packet from them? */
+    private long _lastReceiveTime;
+    /** how many seconds have we sent packets without any ACKs received? */
+    private int _consecutiveSendingSecondsWithoutACKs;
+    /** list of messageIds (Long) that we have received but not yet sent */
+    private List _currentACKs;
+    /** when did we last send ACKs to the peer? */
+    private long _lastACKSend;
+    /** have we received a packet with the ECN bit set in the current second? */
+    private boolean _currentSecondECNReceived;
+    /** 
+     * have all of the packets received in the current second requested that
+     * the previous second's ACKs be sent?
+     */
+    private boolean _remoteWantsPreviousACKs;
+    /** how many bytes should we send to the peer in a second */
+    private int _sendWindowBytes;
+    /** how many bytes can we send to the peer in the current second */
+    private int _sendWindowBytesRemaining;
+    /** what IP is the peer sending and receiving packets on? */
+    private byte[] _remoteIP;
+    /** what port is the peer sending and receiving packets on? */
+    private int _remotePort;
+    /** cached remoteIP + port, used to find the peerState by remote info */
+    private String _remoteHostString;
+    /** if we need to contact them, do we need to talk to an introducer? */
+    private boolean _remoteRequiresIntroduction;
+    /** 
+     * if we are serving as an introducer to them, this is the the tag that
+     * they can publish that, when presented to us, will cause us to send
+     * a relay introduction to the current peer 
+     */
+    private long _weRelayToThemAs;
+    /**
+     * If they have offered to serve as an introducer to us, this is the tag
+     * we can use to publish that fact.
+     */
+    private long _theyRelayToUsAs;
+    /** what is the largest packet we can send to the peer? */
+    private int _mtu;
+    /** when did we last check the MTU? */
+    private long _mtuLastChecked;
+    
+    private long _messagesReceived;
+    private long _messagesSent;
+    
+    private static final int DEFAULT_SEND_WINDOW_BYTES = 16*1024;
+    private static final int MINIMUM_WINDOW_BYTES = DEFAULT_SEND_WINDOW_BYTES;
+    private static final int DEFAULT_MTU = 512;
+    
+    public PeerState(I2PAppContext ctx) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(PeerState.class);
+        _remotePeer = null;
+        _currentMACKey = null;
+        _currentCipherKey = null;
+        _nextMACKey = null;
+        _nextCipherKey = null;
+        _nextKeyingMaterial = null;
+        _rekeyBeganLocally = false;
+        _keyEstablishedTime = -1;
+        _clockSkew = Short.MIN_VALUE;
+        _currentReceiveSecond = -1;
+        _lastSendTime = -1;
+        _lastReceiveTime = -1;
+        _currentACKs = new ArrayList(8);
+        _currentSecondECNReceived = false;
+        _remoteWantsPreviousACKs = false;
+        _sendWindowBytes = DEFAULT_SEND_WINDOW_BYTES;
+        _sendWindowBytesRemaining = DEFAULT_SEND_WINDOW_BYTES;
+        _remoteIP = null;
+        _remotePort = -1;
+        _remoteRequiresIntroduction = false;
+        _weRelayToThemAs = 0;
+        _theyRelayToUsAs = 0;
+        _mtu = DEFAULT_MTU;
+        _mtuLastChecked = -1;
+        _lastACKSend = -1;
+        _messagesReceived = 0;
+        _messagesSent = 0;
+    }
+    
+    /**
+     * The peer are we talking to.  This should be set as soon as this
+     * state is created if we are initiating a connection, but if we are
+     * receiving the connection this will be set only after the connection
+     * is established.
+     */
+    public Hash getRemotePeer() { return _remotePeer; }
+    /** 
+     * The AES key used to verify packets, set only after the connection is
+     * established.  
+     */
+    public SessionKey getCurrentMACKey() { return _currentMACKey; }
+    /**
+     * The AES key used to encrypt/decrypt packets, set only after the 
+     * connection is established.
+     */
+    public SessionKey getCurrentCipherKey() { return _currentCipherKey; }
+    /** 
+     * The pending AES key for verifying packets if we are rekeying the 
+     * connection, or null if we are not in the process of rekeying.
+     */
+    public SessionKey getNextMACKey() { return _nextMACKey; }
+    /** 
+     * The pending AES key for encrypting/decrypting packets if we are 
+     * rekeying the connection, or null if we are not in the process 
+     * of rekeying.
+     */
+    public SessionKey getNextCipherKey() { return _nextCipherKey; }
+    /**
+     * The keying material used for the rekeying, or null if we are not in
+     * the process of rekeying.
+     */
+    public byte[] getNextKeyingMaterial() { return _nextKeyingMaterial; }
+    /** true if we began the current rekeying, false otherwise */
+    public boolean getRekeyBeganLocally() { return _rekeyBeganLocally; }
+    /** when were the current cipher and MAC keys established/rekeyed? */
+    public long getKeyEstablishedTime() { return _keyEstablishedTime; }
+    /** how far off is the remote peer from our clock, in seconds? */
+    public short getClockSkew() { return _clockSkew; }
+    /** what is the current receive second, for congestion control? */
+    public long getCurrentReceiveSecond() { return _currentReceiveSecond; }
+    /** when did we last send them a packet? */
+    public long getLastSendTime() { return _lastSendTime; }
+    /** when did we last receive a packet from them? */
+    public long getLastReceiveTime() { return _lastReceiveTime; }
+    /** how many seconds have we sent packets without any ACKs received? */
+    public int getConsecutiveSendingSecondsWithoutACKS() { return _consecutiveSendingSecondsWithoutACKs; }
+    /** have we received a packet with the ECN bit set in the current second? */
+    public boolean getCurrentSecondECNReceived() { return _currentSecondECNReceived; }
+    /** 
+     * have all of the packets received in the current second requested that
+     * the previous second's ACKs be sent?
+     */
+    public boolean getRemoteWantsPreviousACKs() { return _remoteWantsPreviousACKs; }
+    /** how many bytes should we send to the peer in a second */
+    public int getSendWindowBytes() { return _sendWindowBytes; }
+    /** how many bytes can we send to the peer in the current second */
+    public int getSendWindowBytesRemaining() { return _sendWindowBytesRemaining; }
+    /** what IP is the peer sending and receiving packets on? */
+    public byte[] getRemoteIP() { return _remoteIP; }
+    /** what port is the peer sending and receiving packets on? */
+    public int getRemotePort() { return _remotePort; }
+    /** if we need to contact them, do we need to talk to an introducer? */
+    public boolean getRemoteRequiresIntroduction() { return _remoteRequiresIntroduction; }
+    /** 
+     * if we are serving as an introducer to them, this is the the tag that
+     * they can publish that, when presented to us, will cause us to send
+     * a relay introduction to the current peer 
+     */
+    public long getWeRelayToThemAs() { return _weRelayToThemAs; }
+    /**
+     * If they have offered to serve as an introducer to us, this is the tag
+     * we can use to publish that fact.
+     */
+    public long getTheyRelayToUsAs() { return _theyRelayToUsAs; }
+    /** what is the largest packet we can send to the peer? */
+    public int getMTU() { return _mtu; }
+    /** when did we last check the MTU? */
+    public long getMTULastChecked() { return _mtuLastChecked; }
+    
+    
+    /**
+     * The peer are we talking to.  This should be set as soon as this
+     * state is created if we are initiating a connection, but if we are
+     * receiving the connection this will be set only after the connection
+     * is established.
+     */
+    public void setRemotePeer(Hash peer) { _remotePeer = peer; }
+    /** 
+     * The AES key used to verify packets, set only after the connection is
+     * established.  
+     */
+    public void setCurrentMACKey(SessionKey key) { _currentMACKey = key; }
+    /**
+     * The AES key used to encrypt/decrypt packets, set only after the 
+     * connection is established.
+     */
+    public void setCurrentCipherKey(SessionKey key) { _currentCipherKey = key; }
+    /** 
+     * The pending AES key for verifying packets if we are rekeying the 
+     * connection, or null if we are not in the process of rekeying.
+     */
+    public void setNextMACKey(SessionKey key) { _nextMACKey = key; }
+    /** 
+     * The pending AES key for encrypting/decrypting packets if we are 
+     * rekeying the connection, or null if we are not in the process 
+     * of rekeying.
+     */
+    public void setNextCipherKey(SessionKey key) { _nextCipherKey = key; }
+    /**
+     * The keying material used for the rekeying, or null if we are not in
+     * the process of rekeying.
+     */
+    public void setNextKeyingMaterial(byte data[]) { _nextKeyingMaterial = data; }
+    /** true if we began the current rekeying, false otherwise */
+    public void setRekeyBeganLocally(boolean local) { _rekeyBeganLocally = local; }
+    /** when were the current cipher and MAC keys established/rekeyed? */
+    public void setKeyEstablishedTime(long when) { _keyEstablishedTime = when; }
+    /** how far off is the remote peer from our clock, in seconds? */
+    public void setClockSkew(short skew) { _clockSkew = skew; }
+    /** what is the current receive second, for congestion control? */
+    public void setCurrentReceiveSecond(long sec) { _currentReceiveSecond = sec; }
+    /** when did we last send them a packet? */
+    public void setLastSendTime(long when) { _lastSendTime = when; }
+    /** when did we last receive a packet from them? */
+    public void setLastReceiveTime(long when) { _lastReceiveTime = when; }
+    public void incrementConsecutiveSendingSecondsWithoutACKS() { _consecutiveSendingSecondsWithoutACKs++; }
+    public void resetConsecutiveSendingSecondsWithoutACKS() { _consecutiveSendingSecondsWithoutACKs = 0; }
+    
+    /*
+    public void migrateACKs(List NACKs, long newSecond) {
+        _previousSecondACKs = _currentSecondACKs;
+        if (_currentSecondECNReceived)
+            _sendWindowBytes /= 2;
+        if (_sendWindowBytes < MINIMUM_WINDOW_BYTES)
+            _sendWindowBytes = MINIMUM_WINDOW_BYTES;
+        _sendWindowBytesRemaining = _sendWindowBytes;
+        _currentSecondECNReceived = false;
+        _remoteWantsPreviousACKs = true;
+        _currentReceiveSecond = newSecond;
+    }
+     */
+    
+    /** 
+     * have all of the packets received in the current second requested that
+     * the previous second's ACKs be sent?
+     */
+    public void remoteDoesNotWantPreviousACKs() { _remoteWantsPreviousACKs = false; }
+    /** 
+     * Decrement the remaining bytes in the current period's window,
+     * returning true if the full size can be decremented, false if it
+     * cannot.  If it is not decremented, the window size remaining is 
+     * not adjusted at all.
+     */
+    public boolean allocateSendingBytes(int size) { 
+        long now = _context.clock().now();
+        if (_lastSendTime > 0) {
+            if (_lastSendTime + 1000 <= now)
+                _sendWindowBytesRemaining = _sendWindowBytes;
+        }
+        if (size <= _sendWindowBytesRemaining) {
+            _sendWindowBytesRemaining -= size; 
+            _lastSendTime = now;
+            return true;
+        } else {
+            return false;
+        }
+    }
+    /** what IP+port is the peer sending and receiving packets on? */
+    public void setRemoteAddress(byte ip[], int port) { 
+        _remoteIP = ip;
+        _remotePort = port; 
+        _remoteHostString = calculateRemoteHostString(ip, port);
+    }
+    /** if we need to contact them, do we need to talk to an introducer? */
+    public void setRemoteRequiresIntroduction(boolean required) { _remoteRequiresIntroduction = required; }
+    /** 
+     * if we are serving as an introducer to them, this is the the tag that
+     * they can publish that, when presented to us, will cause us to send
+     * a relay introduction to the current peer 
+     */
+    public void setWeRelayToThemAs(long tag) { _weRelayToThemAs = tag; }
+    /**
+     * If they have offered to serve as an introducer to us, this is the tag
+     * we can use to publish that fact.
+     */
+    public void setTheyRelayToUsAs(long tag) { _theyRelayToUsAs = tag; }
+    /** what is the largest packet we can send to the peer? */
+    public void setMTU(int mtu) { 
+        _mtu = mtu; 
+        _mtuLastChecked = _context.clock().now();
+    }
+    
+    /** we received the message specified completely */
+    public void messageFullyReceived(Long messageId) {
+        synchronized (_currentACKs) {
+            if (!_currentACKs.contains(messageId))
+                _currentACKs.add(messageId);
+        }
+        _messagesReceived++;
+    }
+    
+    /** 
+     * either they told us to back off, or we had to resend to get 
+     * the data through.  
+     *
+     */
+    public void congestionOccurred() {
+        _sendWindowBytes /= 2;
+        if (_sendWindowBytes < MINIMUM_WINDOW_BYTES)
+            _sendWindowBytes = MINIMUM_WINDOW_BYTES;
+    }
+    
+    /** pull off the ACKs (Long) to send to the peer */
+    public List retrieveACKs() {
+        List rv = null;
+        synchronized (_currentACKs) {
+            rv = new ArrayList(_currentACKs);
+            _currentACKs.clear();
+        }
+        return rv;
+    }
+    
+    /** we sent a message which was ACKed containing the given # of bytes */
+    public void messageACKed(int bytesACKed) {
+        _consecutiveSendingSecondsWithoutACKs = 0;
+        _sendWindowBytes += bytesACKed;
+        _lastReceiveTime = _context.clock().now();
+        _messagesSent++;
+    }
+    
+    public long getMessagesSent() { return _messagesSent; }
+    public long getMessagesReceived() { return _messagesReceived; }
+    
+    /** 
+     * we received a backoff request, so cut our send window
+     */
+    public void ECNReceived() {
+        congestionOccurred();
+        _currentSecondECNReceived = true;
+        _lastReceiveTime = _context.clock().now();
+    }
+    
+    public void dataReceived() {
+        _lastReceiveTime = _context.clock().now();
+    }
+    
+    /** when did we last send an ACK to the peer? */
+    public long getLastACKSend() { return _lastACKSend; }
+    public void setLastACKSend(long when) { _lastACKSend = when; }
+    
+    public String getRemoteHostString() { return _remoteHostString; }
+
+    public static String calculateRemoteHostString(byte ip[], int port) {
+        StringBuffer buf = new StringBuffer(ip.length * 4 + 5);
+        for (int i = 0; i < ip.length; i++)
+            buf.append((int)ip[i]).append('.');
+        buf.append(port);
+        return buf.toString();
+    }
+    
+    public static String calculateRemoteHostString(UDPPacket packet) {
+        InetAddress remAddr = packet.getPacket().getAddress();
+        int remPort = packet.getPacket().getPort();
+        return calculateRemoteHostString(remAddr.getAddress(), remPort);
+    }
+    
+    public int hashCode() {
+        if (_remotePeer != null) 
+            return _remotePeer.hashCode();
+        else 
+            return super.hashCode();
+    }
+    public boolean equals(Object o) {
+        if (o == null) return false;
+        if (o instanceof PeerState) {
+            PeerState s = (PeerState)o;
+            if (_remotePeer == null)
+                return o == this;
+            else
+                return _remotePeer.equals(s.getRemotePeer());
+        } else {
+            return false;
+        }
+    }
+    
+    public String toString() {
+        StringBuffer buf = new StringBuffer(64);
+        buf.append(_remoteHostString);
+        if (_remotePeer != null)
+            buf.append(" ").append(_remotePeer.toBase64().substring(0,6));
+        return buf.toString();
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/RelayPeer.java b/router/java/src/net/i2p/router/transport/udp/RelayPeer.java
new file mode 100644
index 0000000000000000000000000000000000000000..6b42d0e2eac2f3b07e9179b0d1c58a853b1ac2fb
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/RelayPeer.java
@@ -0,0 +1,24 @@
+package net.i2p.router.transport.udp;
+
+import net.i2p.data.SessionKey;
+
+/**
+ * Describe the offering to act as an introducer
+ *
+ */
+class RelayPeer {
+    private String _host;
+    private int _port;
+    private byte _tag[];
+    private SessionKey _relayIntroKey;
+    public RelayPeer(String host, int port, byte tag[], SessionKey introKey) {
+        _host = host;
+        _port = port;
+        _tag = tag;
+        _relayIntroKey = introKey;
+    }
+    public String getHost() { return _host; }
+    public int getPort() { return _port; }
+    public byte[] getTag() { return _tag; }
+    public SessionKey getIntroKey() { return _relayIntroKey; }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/TimedWeightedPriorityMessageQueue.java b/router/java/src/net/i2p/router/transport/udp/TimedWeightedPriorityMessageQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..335dd548c62d32419ca484834a88ffd71bf9bae6
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/TimedWeightedPriorityMessageQueue.java
@@ -0,0 +1,226 @@
+package net.i2p.router.transport.udp;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import net.i2p.router.OutNetMessage;
+import net.i2p.router.RouterContext;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+/**
+ * Weighted priority queue implementation for the outbound messages, coupled
+ * with code to fail messages that expire.  
+ *
+ */
+public class TimedWeightedPriorityMessageQueue implements MessageQueue {
+    private RouterContext _context;
+    private Log _log;
+    /** FIFO queue of messages in a particular priority */
+    private List _queue[];
+    /** all messages in the indexed queue are at or below the given priority. */
+    private int _priorityLimits[];
+    /** weighting for each queue */
+    private int _weighting[];
+    /** how many bytes are enqueued */
+    private long _bytesQueued[];
+    /** how many messages have been pushed out in this pass */
+    private int _messagesFlushed[];
+    /** how many bytes total have been pulled off the given queue */
+    private long _bytesTransferred[];
+    /** lock to notify message enqueue/removal (and block for getNext()) */
+    private Object _nextLock;
+    /** have we shut down or are we still alive? */
+    private boolean _alive;
+    /** which queue should we pull out of next */
+    private int _nextQueue;
+    /** true if a message is enqueued while the getNext() call is in progress */
+    private volatile boolean _addedSincePassBegan;
+    private Expirer _expirer;
+    private FailedListener _listener;
+    
+    /**
+     * Build up a new queue
+     *
+     * @param priorityLimits ordered breakpoint for the different message 
+     *                       priorities, with the lowest limit first.
+     * @param weighting how much to prefer a given priority grouping.  
+     *                  specifically, this means how many messages in this queue
+     *                  should be pulled off in a row before moving on to the next.
+     */
+    public TimedWeightedPriorityMessageQueue(RouterContext ctx, int[] priorityLimits, int[] weighting, FailedListener lsnr) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(TimedWeightedPriorityMessageQueue.class);
+        _queue = new List[weighting.length];
+        _priorityLimits = new int[weighting.length];
+        _weighting = new int[weighting.length];
+        _bytesQueued = new long[weighting.length];
+        _bytesTransferred = new long[weighting.length];
+        _messagesFlushed = new int[weighting.length];
+        for (int i = 0; i < weighting.length; i++) {
+            _queue[i] = new ArrayList(8);
+            _weighting[i] = weighting[i];
+            _priorityLimits[i] = priorityLimits[i];
+            _messagesFlushed[i] = 0;
+            _bytesQueued[i] = 0;
+            _bytesTransferred[i] = 0;
+        }
+        _alive = true;
+        _nextLock = this;
+        _nextQueue = 0;
+        _listener = lsnr;
+        _context.statManager().createRateStat("udp.timeToEntrance", "Message lifetime until it reaches the UDP system", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
+        _context.statManager().createRateStat("udp.messageQueueSize", "How many messages are on the current class queue at removal", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
+        _expirer = new Expirer();
+        I2PThread t = new I2PThread(_expirer, "UDP outbound expirer");
+        t.setDaemon(true);
+        t.start();
+    }
+    
+    public void add(OutNetMessage message) {
+        if (message == null) return;
+        
+        _context.statManager().addRateData("udp.timeToEntrance", message.getLifetime(), message.getLifetime());
+                    
+        int queue = pickQueue(message);
+        long size = message.getMessageSize();
+        synchronized (_queue[queue]) {
+            _queue[queue].add(message);
+            _bytesQueued[queue] += size;
+        }
+        
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Added a " + size + " byte message to queue " + queue);
+        
+        synchronized (_nextLock) {
+            _addedSincePassBegan = true;
+            _nextLock.notifyAll();
+        }
+    }
+    
+    /**
+     * Grab the next message out of the next queue.  This only advances
+     * the _nextQueue var after pushing _weighting[currentQueue] messages
+     * or the queue is empty.  This call blocks until either a message 
+     * becomes available or the queue is shut down.
+     *
+     * @param blockUntil expiration, or -1 if indefinite
+     * @return message dequeued, or null if the queue was shut down
+     */
+    public OutNetMessage getNext(long blockUntil) {
+        while (_alive) {
+            _addedSincePassBegan = false;
+            for (int i = 0; i < _queue.length; i++) {
+                int currentQueue = (_nextQueue + i) % _queue.length;
+                synchronized (_queue[currentQueue]) {
+                    if (_queue[currentQueue].size() > 0) {
+                        OutNetMessage msg = (OutNetMessage)_queue[currentQueue].remove(0);
+                        long size = msg.getMessageSize();
+                        _bytesQueued[currentQueue] -= size;
+                        _bytesTransferred[currentQueue] += size;
+                        _messagesFlushed[currentQueue]++;
+                        if (_messagesFlushed[currentQueue] >= _weighting[currentQueue]) {
+                            _messagesFlushed[currentQueue] = 0;
+                            _nextQueue = (currentQueue + 1) % _queue.length;
+                        }
+                        _context.statManager().addRateData("udp.messageQueueSize", _queue[currentQueue].size(), currentQueue);
+                        if (_log.shouldLog(Log.DEBUG))
+                            _log.debug("Pulling a message off queue " + currentQueue + " with " 
+                                       + _queue[currentQueue].size() + " remaining");
+                        return msg;
+                    } else {
+                        // nothing waiting
+                        _messagesFlushed[currentQueue] = 0;
+                        if (_log.shouldLog(Log.DEBUG))
+                            _log.debug("Nothing on queue " + currentQueue);
+                    }
+                }
+            }
+            
+            long remaining = blockUntil - _context.clock().now();
+            if ( (blockUntil > 0) && (remaining < 0) ) {
+                if (_log.shouldLog(Log.DEBUG))
+                    _log.debug("Nonblocking, or block time has expired");
+                return null;
+            }
+            
+            try {
+                synchronized (_nextLock) {
+                    if (!_addedSincePassBegan && _alive) {
+                        // nothing added since we begun iterating through, 
+                        // so we can safely wait for the full period.  otoh, 
+                        // even if this is true, we might be able to safely 
+                        // wait, but it doesn't hurt to loop again.
+                        if (_log.shouldLog(Log.DEBUG))
+                            _log.debug("Wait for activity (up to " + remaining + "ms)");
+                        if (blockUntil < 0)
+                            _nextLock.wait();
+                        else
+                            _nextLock.wait(remaining);
+                    }
+                }
+            } catch (InterruptedException ie) {}
+        }
+        
+        return null;
+    }
+    
+    public void shutdown() {
+        _alive = false;
+        synchronized (_nextLock) {
+            _nextLock.notifyAll();
+        }
+    }
+    
+    private int pickQueue(OutNetMessage message) {
+        int target = message.getPriority();
+        for (int i = 0; i < _priorityLimits.length; i++) {
+            if (_priorityLimits[i] <= target) {
+                if (i == 0)
+                    return 0;
+                else
+                    return i - 1;
+            }
+        }
+        return _priorityLimits.length-1;
+    }
+    
+    public interface FailedListener {
+        public void failed(OutNetMessage msg);
+    }
+    
+    /**
+     * Drop expired messages off the queues
+     */
+    private class Expirer implements Runnable {
+        public void run() {
+            List removed = new ArrayList(1);
+            while (_alive) {
+                long now = _context.clock().now();
+                for (int i = 0; i < _queue.length; i++) {
+                    synchronized (_queue[i]) {
+                        for (int j = 0; j < _queue[i].size(); j++) {
+                            OutNetMessage m = (OutNetMessage)_queue[i].get(j);
+                            if (m.getExpiration() < now) {
+                                _bytesQueued[i] -= m.getMessageSize();
+                                removed.add(m);
+                                _queue[i].remove(j);
+                                j--;
+                                continue;
+                            }
+                        }
+                    }
+                }
+                
+                for (int i = 0; i < removed.size(); i++) {
+                    OutNetMessage m = (OutNetMessage)removed.get(i);
+                    _listener.failed(m);
+                }
+                removed.clear();
+                
+                try { Thread.sleep(1000); } catch (InterruptedException ie) {}
+            }
+        }
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPAddress.java b/router/java/src/net/i2p/router/transport/udp/UDPAddress.java
new file mode 100644
index 0000000000000000000000000000000000000000..c8ebe9a4f2da480584df33de5633d17e0c9ab0a3
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/UDPAddress.java
@@ -0,0 +1,56 @@
+package net.i2p.router.transport.udp;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Properties;
+
+import net.i2p.data.Base64;
+import net.i2p.data.RouterAddress;
+
+/**
+ * basic helper to parse out peer info from a udp address
+ */
+public class UDPAddress {
+    private String _host;
+    private InetAddress _hostAddress;
+    private int _port;
+    private byte[] _introKey;
+    
+    public static final String PROP_PORT = "port";
+    public static final String PROP_HOST = "host";
+    public static final String PROP_INTRO_KEY = "key";
+
+    public UDPAddress(RouterAddress addr) {
+        parse(addr);
+    }
+    
+    private void parse(RouterAddress addr) {
+        Properties opts = addr.getOptions();
+        _host = opts.getProperty(PROP_HOST);
+        if (_host != null) _host = _host.trim();
+        try { 
+            String port = opts.getProperty(PROP_PORT);
+            if (port != null)
+                _port = Integer.parseInt(port);
+        } catch (NumberFormatException nfe) {
+            _port = -1;
+        }
+        String key = opts.getProperty(PROP_INTRO_KEY);
+        if (key != null)
+            _introKey = Base64.decode(key.trim());
+    }
+    
+    public String getHost() { return _host; }
+    public InetAddress getHostAddress() {
+        if (_hostAddress == null) {
+            try {
+                _hostAddress = InetAddress.getByName(_host);
+            } catch (UnknownHostException uhe) {
+                _hostAddress = null;
+            }
+        }
+        return _hostAddress;
+    }
+    public int getPort() { return _port; }
+    public byte[] getIntroKey() { return _introKey; }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPEndpoint.java b/router/java/src/net/i2p/router/transport/udp/UDPEndpoint.java
new file mode 100644
index 0000000000000000000000000000000000000000..b40cf0c5db2853e3843d4aab4d767338c53a2a5b
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/UDPEndpoint.java
@@ -0,0 +1,80 @@
+package net.i2p.router.transport.udp;
+
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.SocketException;
+
+import net.i2p.router.RouterContext;
+import net.i2p.util.Log;
+
+/**
+ * Coordinate the low level datagram socket, managing the UDPSender and
+ * UDPReceiver
+ */
+public class UDPEndpoint {
+    private RouterContext _context;
+    private Log _log;
+    private int _listenPort;
+    private UDPSender _sender;
+    private UDPReceiver _receiver;
+    
+    public UDPEndpoint(RouterContext ctx, int listenPort) throws SocketException {
+        _context = ctx;
+        _log = ctx.logManager().getLog(UDPEndpoint.class);
+        
+        _listenPort = listenPort;
+    }
+    
+    public void startup() {
+        shutdown();
+        try {
+            DatagramSocket socket = new DatagramSocket(_listenPort);
+            _sender = new UDPSender(_context, socket, "UDPSend on " + _listenPort);
+            _receiver = new UDPReceiver(_context, socket, "UDPReceive on " + _listenPort);
+            _sender.startup();
+            _receiver.startup();
+        } catch (SocketException se) {
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("Unable to bind on " + _listenPort);
+        }
+    }
+    
+    public void shutdown() {
+        if (_sender != null) {
+            _sender.shutdown();
+            _receiver.shutdown();
+            _sender = null;
+            _receiver = null;
+        }
+    }
+    
+    public void updateListenPort(int newPort) {
+        if (newPort == _listenPort) return;
+        try {
+            DatagramSocket socket = new DatagramSocket(newPort);
+            _sender.updateListeningPort(socket, newPort);
+            // note: this closes the old socket, so call this after the sender!
+            _receiver.updateListeningPort(socket, newPort);
+            _listenPort = newPort;
+        } catch (SocketException se) {
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("Unable to bind on " + _listenPort);
+        }
+    }
+    
+    public int getListenPort() { return _listenPort; }
+    public UDPSender getSender() { return _sender; }
+    
+    /**
+     * Add the packet to the outobund queue to be sent ASAP (as allowed by
+     * the bandwidth limiter)
+     *
+     * @return number of packets in the send queue
+     */
+    public int send(UDPPacket packet) { return _sender.add(packet); }
+    
+    /**
+     * Blocking call to receive the next inbound UDP packet from any peer.
+     */
+    public UDPPacket receive() { return _receiver.receiveNext(); }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPEndpointTest.java b/router/java/src/net/i2p/router/transport/udp/UDPEndpointTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..52d533adb7d0f8a6de192c2f936ef7a1f790970a
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/UDPEndpointTest.java
@@ -0,0 +1,113 @@
+package net.i2p.router.transport.udp;
+
+import java.net.InetAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+
+import net.i2p.router.RouterContext;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+/**
+ *
+ */
+public class UDPEndpointTest {
+    private RouterContext _context;
+    private Log _log;
+    private UDPEndpoint _endpoints[];
+    private boolean _beginTest;
+    
+    public UDPEndpointTest(RouterContext ctx) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(UDPEndpointTest.class);
+    }
+    
+    public void runTest(int numPeers) {
+        RouterContext ctx = new RouterContext(null);
+        try {
+            _endpoints = new UDPEndpoint[numPeers];
+            int base = 2000 + ctx.random().nextInt(10000);
+            for (int i = 0; i < numPeers; i++) {
+                _log.debug("Building " + i);
+                UDPEndpoint endpoint = new UDPEndpoint(ctx, base + i);
+                _endpoints[i] = endpoint;
+                endpoint.startup();
+                I2PThread read = new I2PThread(new TestRead(endpoint), "Test read " + i);
+                I2PThread write = new I2PThread(new TestWrite(endpoint), "Test write " + i);
+                //read.setDaemon(true);
+                read.start();
+                //write.setDaemon(true);
+                write.start();
+            }
+        } catch (SocketException se) {
+            if (_log.shouldLog(Log.ERROR))
+                _log.error("Error initializing", se);
+            return;
+        }
+        _beginTest = true;
+        _log.debug("Test begin");
+    }
+    
+    private class TestRead implements Runnable {
+        private UDPEndpoint _endpoint;
+        public TestRead(UDPEndpoint peer) {
+            _endpoint = peer;
+        }
+        public void run() {
+            while (!_beginTest) {
+                try { Thread.sleep(1000); } catch (InterruptedException ie) {}
+            }
+            _log.debug("Beginning to read");
+            long start = System.currentTimeMillis();
+            int received = 0;
+            while (true) {
+                UDPPacket packet = _endpoint.receive();
+                received++;
+                if (received == 10000) {
+                    long time = System.currentTimeMillis() - start;
+                    _log.debug("Received 10000 in " + time);
+                }
+            }
+        }
+    }
+    
+    private class TestWrite implements Runnable {
+        private UDPEndpoint _endpoint;
+        public TestWrite(UDPEndpoint peer) {
+            _endpoint = peer;
+        }
+        public void run() {
+            while (!_beginTest) {
+                try { Thread.sleep(1000); } catch (InterruptedException ie) {}
+            }
+            _log.debug("Beginning to write");
+            for (int curPacket = 0; curPacket < 10000; curPacket++) {
+                byte data[] = new byte[1024];
+                _context.random().nextBytes(data);
+                int curPeer = (curPacket % _endpoints.length);
+                if (_endpoints[curPeer] == _endpoint)
+                    curPeer++;
+                if (curPeer >= _endpoints.length)
+                    curPeer = 0;
+                short priority = 1;
+                long expiration = -1;
+                try {
+                    UDPPacket packet = UDPPacket.acquire(_context);
+                    packet.initialize(priority, expiration, InetAddress.getLocalHost(), _endpoints[curPeer].getListenPort());
+                    packet.writeData(data, 0, 1024);
+                    _endpoint.send(packet);
+                } catch (UnknownHostException uhe) {
+                    _log.error("foo!", uhe);
+                }
+                //if (_log.shouldLog(Log.DEBUG)) {
+                //    _log.debug("Sent to " + _endpoints[curPeer].getListenPort() + " from " + _endpoint.getListenPort());
+                //}
+            }
+        }
+    }
+    
+    public static void main(String args[]) {
+        UDPEndpointTest test = new UDPEndpointTest(new RouterContext(null));
+        test.runTest(2);
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPPacket.java b/router/java/src/net/i2p/router/transport/udp/UDPPacket.java
new file mode 100644
index 0000000000000000000000000000000000000000..4fd392b34e0db6c311b74ae95db5f571b76ecb6a
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/UDPPacket.java
@@ -0,0 +1,188 @@
+package net.i2p.router.transport.udp;
+
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.Base64;
+import net.i2p.data.ByteArray;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Hash;
+import net.i2p.data.SessionKey;
+import net.i2p.util.ByteCache;
+import net.i2p.util.Log;
+
+/**
+ * Basic delivery unit containing the datagram.  This also maintains a cache
+ * of object instances to allow rapid reuse.
+ *
+ */
+public class UDPPacket {
+    private I2PAppContext _context;
+    private Log _log;
+    private DatagramPacket _packet;
+    private short _priority;
+    private long _initializeTime;
+    private long _expiration;
+    private byte[] _data;
+  
+    private static final List _packetCache;
+    static {
+        _packetCache = new ArrayList(256);
+    }
+    
+    private static final boolean CACHE = false;
+      
+    private static final int MAX_PACKET_SIZE = 2048;
+    public static final int IV_SIZE = 16;
+    public static final int MAC_SIZE = 16;
+    
+    public static final int PAYLOAD_TYPE_SESSION_REQUEST = 0;
+    public static final int PAYLOAD_TYPE_SESSION_CREATED = 1;
+    public static final int PAYLOAD_TYPE_SESSION_CONFIRMED = 2;
+    public static final int PAYLOAD_TYPE_RELAY_REQUEST = 3;
+    public static final int PAYLOAD_TYPE_RELAY_RESPONSE = 4;
+    public static final int PAYLOAD_TYPE_RELAY_INTRO = 5;
+    public static final int PAYLOAD_TYPE_DATA = 6;
+    
+    // various flag fields for use in the data packets
+    public static final byte DATA_FLAG_EXPLICIT_ACK = (byte)(1 << 7);
+    public static final byte DATA_FLAG_EXPLICIT_NACK = (1 << 6);
+    public static final byte DATA_FLAG_NUMACKS = (1 << 5);
+    public static final byte DATA_FLAG_ECN = (1 << 4);
+    public static final byte DATA_FLAG_WANT_ACKS = (1 << 3);
+    public static final byte DATA_FLAG_WANT_REPLY = (1 << 2);
+    public static final byte DATA_FLAG_EXTENDED = (1 << 1);
+    
+    private static final int MAX_VALIDATE_SIZE = MAX_PACKET_SIZE;
+    private static final ByteCache _validateCache = ByteCache.getInstance(16, MAX_VALIDATE_SIZE);
+    private static final ByteCache _ivCache = ByteCache.getInstance(16, IV_SIZE);
+    
+    private UDPPacket(I2PAppContext ctx) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(UDPPacket.class);
+        _data = new byte[MAX_PACKET_SIZE];
+        _packet = new DatagramPacket(_data, MAX_PACKET_SIZE);
+        _initializeTime = _context.clock().now();
+    }
+    
+    public void initialize(short priority, long expiration, InetAddress host, int port) {
+        _priority = priority;
+        _expiration = expiration;
+        resetBegin();
+        Arrays.fill(_data, (byte)0x00);
+        _packet.setLength(0);
+        _packet.setAddress(host);
+        _packet.setPort(port);
+    }
+    
+    public void writeData(byte src[], int offset, int len) { 
+        System.arraycopy(src, offset, _data, 0, len);
+        _packet.setLength(len);
+        resetBegin();
+    }
+    public DatagramPacket getPacket() { return _packet; }
+    public short getPriority() { return _priority; }
+    public long getExpiration() { return _expiration; }
+    public long getLifetime() { return _context.clock().now() - _initializeTime; }
+    public void resetBegin() { _initializeTime = _context.clock().now(); }
+    
+    /**
+     * Validate the packet against the MAC specified, returning true if the
+     * MAC matches, false otherwise.
+     *
+     */
+    public boolean validate(SessionKey macKey) {
+        boolean eq = false;
+        ByteArray buf = _validateCache.acquire();
+        
+        // validate by comparing _data[0:15] and
+        // HMAC(payload + IV + payloadLength, macKey)
+        
+        int payloadLength = _packet.getLength() - MAC_SIZE - IV_SIZE;
+        if (payloadLength > 0) {
+            int off = 0;
+            System.arraycopy(_data, _packet.getOffset() + MAC_SIZE + IV_SIZE, buf.getData(), off, payloadLength);
+            off += payloadLength;
+            System.arraycopy(_data, _packet.getOffset() + MAC_SIZE, buf.getData(), off, IV_SIZE);
+            off += IV_SIZE;
+            DataHelper.toLong(buf.getData(), off, 2, payloadLength);
+            off += 2;
+
+            Hash calculated = _context.hmac().calculate(macKey, buf.getData(), 0, off);
+
+            if (_log.shouldLog(Log.DEBUG)) {
+                StringBuffer str = new StringBuffer(128);
+                str.append(_packet.getLength()).append(" byte packet received, payload length ");
+                str.append(payloadLength);
+                str.append("\nIV: ").append(Base64.encode(buf.getData(), payloadLength, IV_SIZE));
+                str.append("\nIV2: ").append(Base64.encode(_data, MAC_SIZE, IV_SIZE));
+                str.append("\nlen: ").append(DataHelper.fromLong(buf.getData(), payloadLength + IV_SIZE, 2));
+                str.append("\nMAC key: ").append(macKey.toBase64());
+                str.append("\ncalc HMAC: ").append(calculated.toBase64());
+                str.append("\nread HMAC: ").append(Base64.encode(_data, _packet.getOffset(), MAC_SIZE));
+                str.append("\nraw: ").append(Base64.encode(_data, _packet.getOffset(), _packet.getLength()));
+                _log.debug(str.toString());
+            }
+            eq = DataHelper.eq(calculated.getData(), 0, _data, _packet.getOffset(), MAC_SIZE);
+        } else {
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Payload length is " + payloadLength);
+        }
+        
+        _validateCache.release(buf);
+        return eq;
+    }
+    
+    /**
+     * Decrypt this valid packet, overwriting the _data buffer's payload
+     * with the decrypted data (leaving the MAC and IV unaltered)
+     * 
+     */
+    public void decrypt(SessionKey cipherKey) {
+        ByteArray iv = _ivCache.acquire();
+        System.arraycopy(_data, MAC_SIZE, iv.getData(), 0, IV_SIZE);
+        _context.aes().decrypt(_data, _packet.getOffset() + MAC_SIZE + IV_SIZE, _data, _packet.getOffset() + MAC_SIZE + IV_SIZE, cipherKey, iv.getData(), _packet.getLength() - MAC_SIZE - IV_SIZE);
+        _ivCache.release(iv);
+    }
+    
+    public String toString() {
+        StringBuffer buf = new StringBuffer(64);
+        buf.append(_packet.getLength());
+        buf.append(" byte packet with ");
+        buf.append(_packet.getAddress().getHostAddress()).append(":");
+        buf.append(_packet.getPort());
+        return buf.toString();
+    }
+    
+    
+    public static UDPPacket acquire(I2PAppContext ctx) {
+        if (CACHE) {
+            synchronized (_packetCache) {
+                if (_packetCache.size() > 0) {
+                    UDPPacket rv = (UDPPacket)_packetCache.remove(0);
+                    rv._context = ctx;
+                    rv._log = ctx.logManager().getLog(UDPPacket.class);
+                    rv.resetBegin();
+                    Arrays.fill(rv._data, (byte)0x00);
+                    return rv;
+                }
+            }
+        }
+        return new UDPPacket(ctx);
+    }
+    
+    public void release() {
+        if (!CACHE) return;
+        synchronized (_packetCache) {
+            _packet.setLength(0);
+            _packet.setPort(1);
+            if (_packetCache.size() <= 64)
+                _packetCache.add(this);
+        }
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java b/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java
new file mode 100644
index 0000000000000000000000000000000000000000..b9cf54299eea8bce02c285ac2a0ece50995aefb2
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java
@@ -0,0 +1,448 @@
+package net.i2p.router.transport.udp;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.Base64;
+import net.i2p.data.DataHelper;
+import net.i2p.data.SessionKey;
+import net.i2p.data.Signature;
+import net.i2p.util.Log;
+
+/**
+ * To read a packet, initialize this reader with the data and fetch out
+ * the appropriate fields.  If the interesting bits are in message specific
+ * elements, grab the appropriate subreader.
+ *
+ */
+public class UDPPacketReader {
+    private I2PAppContext _context;
+    private Log _log;
+    private byte _message[];
+    private int _payloadBeginOffset;
+    private int _payloadLength;
+    private SessionRequestReader _sessionRequestReader;
+    private SessionCreatedReader _sessionCreatedReader;
+    private SessionConfirmedReader _sessionConfirmedReader;
+    private DataReader _dataReader;
+    
+    private static final int KEYING_MATERIAL_LENGTH = 64;
+    
+    public UDPPacketReader(I2PAppContext ctx) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(UDPPacketReader.class);
+        _sessionRequestReader = new SessionRequestReader();
+        _sessionCreatedReader = new SessionCreatedReader();
+        _sessionConfirmedReader = new SessionConfirmedReader();
+        _dataReader = new DataReader();
+    }
+    
+    public void initialize(UDPPacket packet) {
+        int off = packet.getPacket().getOffset();
+        int len = packet.getPacket().getLength();
+        off += UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
+        len -= UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE;
+        initialize(packet.getPacket().getData(), off, len);
+    }
+    
+    public void initialize(byte message[], int payloadOffset, int payloadLength) {
+        _message = message;
+        _payloadBeginOffset = payloadOffset;
+        _payloadLength = payloadLength;
+    }
+    
+    /** what type of payload is in here? */
+    public int readPayloadType() {
+        // 3 highest order bits == payload type
+        return _message[_payloadBeginOffset] >>> 4;
+    }
+    
+    /** does this packet include rekeying data? */
+    public boolean readRekeying() {
+        return (_message[_payloadBeginOffset] & (1 << 3)) != 0;
+    }
+    
+    public boolean readExtendedOptionsIncluded() {
+        return (_message[_payloadBeginOffset] & (1 << 2)) != 0;
+    }
+    
+    public long readTimestamp() {
+        return DataHelper.fromLong(_message, _payloadBeginOffset + 1, 4);
+    }
+    
+    public void readKeyingMaterial(byte target[], int targetOffset) {
+        if (!readRekeying())
+            throw new IllegalStateException("This packet is not rekeying!");
+        System.arraycopy(_message, _payloadBeginOffset + 1 + 4, target, targetOffset, KEYING_MATERIAL_LENGTH);
+    }
+    
+    /** index into the message where the body begins */
+    private int readBodyOffset() {
+        int offset = _payloadBeginOffset + 1 + 4;
+        if (readRekeying())
+            offset += KEYING_MATERIAL_LENGTH;
+        if (readExtendedOptionsIncluded()) {
+            int optionsSize = (int)DataHelper.fromLong(_message, offset, 1);
+            offset += optionsSize + 1;
+        }
+        return offset;
+    }
+    
+    public SessionRequestReader getSessionRequestReader() { return _sessionRequestReader; }
+    public SessionCreatedReader getSessionCreatedReader() { return _sessionCreatedReader; }
+    public SessionConfirmedReader getSessionConfirmedReader() { return _sessionConfirmedReader; }
+    public DataReader getDataReader() { return _dataReader; }
+    
+    public String toString() {
+        switch (readPayloadType()) {
+            case UDPPacket.PAYLOAD_TYPE_DATA:
+                return _dataReader.toString();
+            case UDPPacket.PAYLOAD_TYPE_SESSION_CONFIRMED:
+                return "Session confirmed packet";
+            case UDPPacket.PAYLOAD_TYPE_SESSION_CREATED:
+                return "Session created packet";
+            case UDPPacket.PAYLOAD_TYPE_SESSION_REQUEST:
+                return "Session request packet";
+            default:
+                return "Other packet type...";
+        }
+    }
+    
+    /** Help read the SessionRequest payload */
+    public class SessionRequestReader {
+        public static final int X_LENGTH = 256;
+        public void readX(byte target[], int targetOffset) {
+            int readOffset = readBodyOffset();
+            System.arraycopy(_message, readOffset, target, targetOffset, X_LENGTH);
+        }
+        
+        public int readIPSize() {
+            int offset = readBodyOffset() + X_LENGTH;
+            return (int)DataHelper.fromLong(_message, offset, 1);
+        }
+        
+        /** what IP bob is reachable on */
+        public void readIP(byte target[], int targetOffset) {
+            int offset = readBodyOffset() + X_LENGTH;
+            int size = (int)DataHelper.fromLong(_message, offset, 1);
+            offset++;
+            System.arraycopy(_message, offset, target, targetOffset, size);
+        }
+    }
+    
+    /** Help read the SessionCreated payload */
+    public class SessionCreatedReader {
+        public static final int Y_LENGTH = 256;
+        public void readY(byte target[], int targetOffset) {
+            int readOffset = readBodyOffset();
+            System.arraycopy(_message, readOffset, target, targetOffset, Y_LENGTH);
+        }
+        
+        /** sizeof(IP) */
+        public int readIPSize() {
+            int offset = readBodyOffset() + Y_LENGTH;
+            return (int)DataHelper.fromLong(_message, offset, 1);
+        }
+        
+        /** what IP do they think we are coming on? */
+        public void readIP(byte target[], int targetOffset) {
+            int offset = readBodyOffset() + Y_LENGTH;
+            int size = (int)DataHelper.fromLong(_message, offset, 1);
+            offset++;
+            System.arraycopy(_message, offset, target, targetOffset, size);
+        }
+        
+        /** what port do they think we are coming from? */
+        public int readPort() {
+            int offset = readBodyOffset() + Y_LENGTH + 1 + readIPSize();
+            return (int)DataHelper.fromLong(_message, offset, 2);
+        }
+        
+        /** write out the 4 byte relayAs tag */
+        public long readRelayTag() {
+            int offset = readBodyOffset() + Y_LENGTH + 1 + readIPSize() + 2;
+            return DataHelper.fromLong(_message, offset, 4);
+        }
+        
+        public long readSignedOnTime() {
+            int offset = readBodyOffset() + Y_LENGTH + 1 + readIPSize() + 2 + 4;
+            long rv = DataHelper.fromLong(_message, offset, 4);
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Signed on time offset: " + offset + " val: " + rv
+                           + "\nRawCreated: " + Base64.encode(_message, _payloadBeginOffset, _payloadLength));
+            return rv;
+        }
+        
+        public void readEncryptedSignature(byte target[], int targetOffset) {
+            int offset = readBodyOffset() + Y_LENGTH + 1 + readIPSize() + 2 + 4 + 4;
+            System.arraycopy(_message, offset, target, targetOffset, Signature.SIGNATURE_BYTES + 8);
+        }
+        
+        public void readIV(byte target[], int targetOffset) {
+            int offset = _payloadBeginOffset - UDPPacket.IV_SIZE;
+            System.arraycopy(_message, offset, target, targetOffset, UDPPacket.IV_SIZE);
+        }
+    }
+    
+    /** parse out the confirmed message */
+    public class SessionConfirmedReader {
+        /** which fragment is this? */
+        public int readCurrentFragmentNum() {
+            int readOffset = readBodyOffset();
+            return _message[readOffset] >>> 4;
+        }
+        /** how many fragments will there be? */
+        public int readTotalFragmentNum() {
+            int readOffset = readBodyOffset();
+            return (_message[readOffset] & 0xF);
+        }
+        
+        public int readCurrentFragmentSize() {
+            int readOffset = readBodyOffset() + 1;
+            return (int)DataHelper.fromLong(_message, readOffset, 2);
+        }
+
+        /** read the fragment data from the nonterminal sessionConfirmed packet */
+        public void readFragmentData(byte target[], int targetOffset) {
+            int readOffset = readBodyOffset() + 1 + 2;
+            int len = readCurrentFragmentSize();
+            System.arraycopy(_message, readOffset, target, targetOffset, len);
+        }
+        
+        /** read the time at which the signature was generated */
+        public long readFinalFragmentSignedOnTime() {
+            if (readCurrentFragmentNum() != readTotalFragmentNum()-1)
+                throw new IllegalStateException("This is not the final fragment");
+            int readOffset = readBodyOffset() + 1 + 2 + readCurrentFragmentSize();
+            return DataHelper.fromLong(_message, readOffset, 4);
+        }
+        
+        /** read the signature from the final sessionConfirmed packet */
+        public void readFinalSignature(byte target[], int targetOffset) {
+            if (readCurrentFragmentNum() != readTotalFragmentNum()-1)
+                throw new IllegalStateException("This is not the final fragment");
+            int readOffset = _payloadBeginOffset + _payloadLength - Signature.SIGNATURE_BYTES;
+            System.arraycopy(_message, readOffset, target, targetOffset, Signature.SIGNATURE_BYTES);
+        }
+    }
+    
+    /** parse out the data message */
+    public class DataReader {
+        public boolean readACKsIncluded() {
+            return flagSet(UDPPacket.DATA_FLAG_EXPLICIT_ACK);
+        }
+        public boolean readNACKsIncluded() {
+            return flagSet(UDPPacket.DATA_FLAG_EXPLICIT_NACK);
+        }
+        public boolean readNumACKsIncluded() {
+            return flagSet(UDPPacket.DATA_FLAG_NUMACKS);
+        }
+        public boolean readECN() {
+            return flagSet(UDPPacket.DATA_FLAG_ECN);
+        }
+        public boolean readWantPreviousACKs() {
+            return flagSet(UDPPacket.DATA_FLAG_WANT_ACKS);
+        }
+        public boolean readReplyRequested() { 
+            return flagSet(UDPPacket.DATA_FLAG_WANT_REPLY);
+        }
+        public boolean readExtendedDataIncluded() {
+            return flagSet(UDPPacket.DATA_FLAG_EXTENDED);
+        }
+        public long[] readACKs() {
+            if (!readACKsIncluded()) return null;
+            int off = readBodyOffset() + 1;
+            int num = (int)DataHelper.fromLong(_message, off, 1);
+            off++;
+            long rv[] = new long[num];
+            for (int i = 0; i < num; i++) {
+                rv[i] = DataHelper.fromLong(_message, off, 4);
+                off += 4;
+            }
+            return rv;
+        }
+        public long[] readNACKs() {
+            if (!readNACKsIncluded()) return null;
+            int off = readBodyOffset() + 1;
+            if (readACKsIncluded()) {
+                int numACKs = (int)DataHelper.fromLong(_message, off, 1);
+                off++;
+                off += 4 * numACKs;
+            }
+            
+            int numNACKs = (int)DataHelper.fromLong(_message, off, 1);
+            off++;
+            long rv[] = new long[numNACKs];
+            for (int i = 0; i < numNACKs; i++) {
+                rv[i] = DataHelper.fromLong(_message, off, 4);
+                off += 4;
+            }
+            return rv;
+        }
+        public int readNumACKs() {
+            if (!readNumACKsIncluded()) return -1;
+            int off = readBodyOffset() + 1;
+            
+            if (readACKsIncluded()) {
+                int numACKs = (int)DataHelper.fromLong(_message, off, 1);
+                off++;
+                off += 4 * numACKs;
+            }
+            if (readNACKsIncluded()) {
+                int numNACKs = (int)DataHelper.fromLong(_message, off, 1);
+                off++;
+                off += 4 * numNACKs;
+            }
+            return (int)DataHelper.fromLong(_message, off, 2);
+        }
+        
+        public int readFragmentCount() {
+            int off = readBodyOffset() + 1;
+            if (readACKsIncluded()) {
+                int numACKs = (int)DataHelper.fromLong(_message, off, 1);
+                off++;
+                off += 4 * numACKs;
+            }
+            if (readNACKsIncluded()) {
+                int numNACKs = (int)DataHelper.fromLong(_message, off, 1);
+                off++;
+                off += 4 * numNACKs;
+            }
+            if (readNumACKsIncluded())
+                off += 2;
+            if (readExtendedDataIncluded()) {
+                int size = (int)DataHelper.fromLong(_message, off, 1);
+                off++;
+                off += size;
+            }
+            return (int)_message[off];
+        }
+        
+        public long readMessageId(int fragmentNum) {
+            int fragmentBegin = getFragmentBegin(fragmentNum);
+            return DataHelper.fromLong(_message, fragmentBegin, 4);
+        }
+        public int readMessageFragmentNum(int fragmentNum) {
+            int off = getFragmentBegin(fragmentNum);
+            off += 4; // messageId
+            return _message[off] >>> 3;
+        }
+        public boolean readMessageIsLast(int fragmentNum) {
+            int off = getFragmentBegin(fragmentNum);
+            off += 4; // messageId
+            return ((_message[off] & (1 << 2)) != 0);
+        }
+        public int readMessageFragmentSize(int fragmentNum) {
+            int off = getFragmentBegin(fragmentNum);
+            off += 4; // messageId
+            off++; // fragment info
+            return (int)DataHelper.fromLong(_message, off, 2);
+        }
+        public void readMessageFragment(int fragmentNum, byte target[], int targetOffset) {
+            int off = getFragmentBegin(fragmentNum);
+            off += 4; // messageId
+            off++; // fragment info
+            int size = (int)DataHelper.fromLong(_message, off, 2);
+            off += 2;
+            System.arraycopy(_message, off, target, targetOffset, size);
+        }
+        
+        private int getFragmentBegin(int fragmentNum) {
+            int off = readBodyOffset() + 1;
+            if (readACKsIncluded()) {
+                int numACKs = (int)DataHelper.fromLong(_message, off, 1);
+                off++;
+                off += 4 * numACKs;
+            }
+            if (readNACKsIncluded()) {
+                int numNACKs = (int)DataHelper.fromLong(_message, off, 1);
+                off++;
+                off += 5 * numNACKs;
+            }
+            if (readNumACKsIncluded())
+                off += 2;
+            if (readExtendedDataIncluded()) {
+                int size = (int)DataHelper.fromLong(_message, off, 1);
+                off++;
+                off += size;
+            }
+            off++; // # fragments
+            
+            if (fragmentNum == 0) {
+                return off;
+            } else {
+                for (int i = 0; i < fragmentNum; i++) {
+                    off += 5; // messageId+info
+                    off += (int)DataHelper.fromLong(_message, off, 2);
+                    off += 2;
+                }
+                return off;
+            }
+        }
+
+        private boolean flagSet(byte flag) {
+            int flagOffset = readBodyOffset();
+            return ((_message[flagOffset] & flag) != 0);
+        }
+        
+        public String toString() {
+            StringBuffer buf = new StringBuffer(256);
+            long msAgo = _context.clock().now() - readTimestamp()*1000;
+            buf.append("Data packet sent ").append(msAgo).append("ms ago ");
+            buf.append("IV ");
+            buf.append(Base64.encode(_message, _payloadBeginOffset-UDPPacket.IV_SIZE, UDPPacket.IV_SIZE));
+            buf.append(" ");
+            int off = readBodyOffset() + 1;
+            if (readACKsIncluded()) {
+                int numACKs = (int)DataHelper.fromLong(_message, off, 1);
+                off++;
+                buf.append("with ACKs for ");
+                for (int i = 0; i < numACKs; i++) {
+                    buf.append(DataHelper.fromLong(_message, off, 4)).append(' ');
+                    off += 4;
+                }
+            }
+            if (readNACKsIncluded()) {
+                int numNACKs = (int)DataHelper.fromLong(_message, off, 1);
+                off++;
+                buf.append("with NACKs for ");
+                for (int i = 0; i < numNACKs; i++) {
+                    buf.append(DataHelper.fromLong(_message, off, 4)).append(' ');
+                    off += 5;
+                }
+                off += 5 * numNACKs;
+            }
+            if (readNumACKsIncluded()) {
+                buf.append("with numACKs of ");
+                buf.append(DataHelper.fromLong(_message, off, 2));
+                buf.append(' ');
+                off += 2;
+            }
+            if (readExtendedDataIncluded()) {
+                int size = (int)DataHelper.fromLong(_message, off, 1);
+                off++;
+                buf.append("with extended size of ");
+                buf.append(size);
+                buf.append(' ');
+                off += size;
+            }
+            
+            int numFragments = (int)DataHelper.fromLong(_message, off, 1);
+            off++;
+            buf.append("with fragmentCount of ");
+            buf.append(numFragments);
+            buf.append(' ');
+            
+            for (int i = 0; i < numFragments; i++) {
+                buf.append("containing messageId ");
+                buf.append(DataHelper.fromLong(_message, off, 4));
+                off += 5; // messageId+info
+                int size = (int)DataHelper.fromLong(_message, off, 2);
+                buf.append(" with ").append(size).append(" bytes");
+                buf.append(' ');
+                off += size;
+                off += 2;
+            }
+            
+            return buf.toString();
+        }
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java b/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java
new file mode 100644
index 0000000000000000000000000000000000000000..144ae839d4742e50851199574c2b1719f5e06d98
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java
@@ -0,0 +1,166 @@
+package net.i2p.router.transport.udp;
+
+import java.io.IOException;
+import java.net.DatagramSocket;
+import java.net.DatagramPacket;
+
+import java.util.ArrayList;
+import java.util.List;
+import net.i2p.router.RouterContext;
+import net.i2p.router.transport.FIFOBandwidthLimiter;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+/**
+ * Lowest level component to pull raw UDP datagrams off the wire as fast
+ * as possible, controlled by both the bandwidth limiter and the router's
+ * throttle.  If the inbound queue gets too large or packets have been
+ * waiting around too long, they are dropped.  Packets should be pulled off
+ * from the queue ASAP by a {@link PacketHandler}
+ *
+ */
+public class UDPReceiver {
+    private RouterContext _context;
+    private Log _log;
+    private DatagramSocket _socket;
+    private String _name;
+    private List _inboundQueue;
+    private boolean _keepRunning;
+    private Runner _runner;
+    
+    public UDPReceiver(RouterContext ctx, DatagramSocket socket, String name) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(UDPReceiver.class);
+        _name = name;
+        _inboundQueue = new ArrayList(128);
+        _socket = socket;
+        _runner = new Runner();
+        _context.statManager().createRateStat("udp.receivePacketSize", "How large packets received are", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
+        _context.statManager().createRateStat("udp.droppedInbound", "How many packet are queued up but not yet received when we drop", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
+    }
+    
+    public void startup() {
+        _keepRunning = true;
+        I2PThread t = new I2PThread(_runner, _name);
+        t.setDaemon(true);
+        t.start();
+    }
+    
+    public void shutdown() {
+        _keepRunning = false;
+        synchronized (_inboundQueue) {
+            _inboundQueue.clear();
+            _inboundQueue.notifyAll();
+        }
+    }
+    
+    /**
+     * Replace the old listen port with the new one, returning the old. 
+     * NOTE: this closes the old socket so that blocking calls unblock!
+     *
+     */
+    public DatagramSocket updateListeningPort(DatagramSocket socket, int newPort) {
+        return _runner.updateListeningPort(socket, newPort);
+    }
+
+    /** if a packet been sitting in the queue for 2 seconds, drop subsequent packets */
+    private static final long MAX_QUEUE_PERIOD = 2*1000;
+    
+    private void receive(UDPPacket packet) {
+        synchronized (_inboundQueue) {
+            int queueSize = _inboundQueue.size();
+            if (queueSize > 0) {
+                long headPeriod = ((UDPPacket)_inboundQueue.get(0)).getLifetime();
+                if (headPeriod > MAX_QUEUE_PERIOD) {
+                    _context.statManager().addRateData("udp.droppedInbound", queueSize, headPeriod);
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn("Dropping inbound packet with " + queueSize + " queued for " + headPeriod);
+                    _inboundQueue.notifyAll();
+                    return;
+                }
+            }
+            _inboundQueue.add(packet);
+            _inboundQueue.notifyAll();
+        }
+    }
+    
+    /**
+     * Blocking call to retrieve the next inbound packet, or null if we have
+     * shut down.
+     *
+     */
+    public UDPPacket receiveNext() {
+        while (_keepRunning) {
+            synchronized (_inboundQueue) {
+                if (_inboundQueue.size() <= 0) {
+                    try {
+                        _inboundQueue.wait();
+                    } catch (InterruptedException ie) {}
+                }
+                if (_inboundQueue.size() > 0)
+                    return (UDPPacket)_inboundQueue.remove(0);
+            }
+        }
+        return null;
+    }
+    
+    private class Runner implements Runnable {
+        private boolean _socketChanged;
+        public void run() {
+            _socketChanged = false;
+            while (_keepRunning) {
+                if (_socketChanged) {
+                    Thread.currentThread().setName(_name);
+                    _socketChanged = false;
+                }
+                UDPPacket packet = UDPPacket.acquire(_context);
+                
+                // block before we read...
+                while (!_context.throttle().acceptNetworkMessage())
+                    try { Thread.sleep(10); } catch (InterruptedException ie) {}
+                
+                try {
+                    synchronized (Runner.this) {
+                        _socket.receive(packet.getPacket());
+                    }
+                    int size = packet.getPacket().getLength();
+                    packet.resetBegin();
+                    _context.statManager().addRateData("udp.receivePacketSize", size, 0);
+
+                    // and block after we know how much we read but before
+                    // we release the packet to the inbound queue
+                    if (size > 0) {
+                        FIFOBandwidthLimiter.Request req = _context.bandwidthLimiter().requestInbound(size, "UDP receiver");
+                        while (req.getPendingInboundRequested() > 0)
+                            req.waitForNextAllocation();
+                    }
+                    
+                    receive(packet);
+                } catch (IOException ioe) {
+                    if (_socketChanged) {
+                        if (_log.shouldLog(Log.INFO))
+                            _log.info("Changing ports...");
+                    } else {
+                        if (_log.shouldLog(Log.ERROR))
+                            _log.error("Error receiving", ioe);
+                    }
+                    packet.release();
+                }
+            }
+        }
+        
+        public DatagramSocket updateListeningPort(DatagramSocket socket, int newPort) {
+            _name = "UDPReceive on " + newPort;
+            DatagramSocket old = null;
+            synchronized (Runner.this) {
+                old = _socket;
+                _socket = socket;
+            }
+            _socketChanged = true;
+            // ok, its switched, now lets break any blocking calls
+            old.close();
+            return old;
+        }
+    }
+    
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPSender.java b/router/java/src/net/i2p/router/transport/udp/UDPSender.java
new file mode 100644
index 0000000000000000000000000000000000000000..e20cd2c30c7118ce0c8a8425b37320b062b39f51
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/UDPSender.java
@@ -0,0 +1,174 @@
+package net.i2p.router.transport.udp;
+
+import java.io.IOException;
+import java.net.DatagramSocket;
+import java.net.DatagramPacket;
+
+import java.util.ArrayList;
+import java.util.List;
+import net.i2p.data.Base64;
+import net.i2p.router.RouterContext;
+import net.i2p.router.transport.FIFOBandwidthLimiter;
+import net.i2p.util.I2PThread;
+import net.i2p.util.Log;
+
+/**
+ * Lowest level packet sender, pushes anything on its queue ASAP.
+ *
+ */
+public class UDPSender {
+    private RouterContext _context;
+    private Log _log;
+    private DatagramSocket _socket;
+    private String _name;
+    private List _outboundQueue;
+    private boolean _keepRunning;
+    private Runner _runner;
+    
+    private static final int MAX_QUEUED = 64;
+    
+    public UDPSender(RouterContext ctx, DatagramSocket socket, String name) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(UDPSender.class);
+        _outboundQueue = new ArrayList(128);
+        _socket = socket;
+        _runner = new Runner();
+        _name = name;
+        _context.statManager().createRateStat("udp.pushTime", "How long a UDP packet takes to get pushed out", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
+        _context.statManager().createRateStat("udp.sendPacketSize", "How large packets sent are", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
+    }
+    
+    public void startup() {
+        _keepRunning = true;
+        I2PThread t = new I2PThread(_runner, _name);
+        t.setDaemon(true);
+        t.start();
+    }
+    
+    public void shutdown() {
+        _keepRunning = false;
+        synchronized (_outboundQueue) {
+            _outboundQueue.clear();
+            _outboundQueue.notifyAll();
+        }
+    }
+    
+    public DatagramSocket updateListeningPort(DatagramSocket socket, int newPort) {
+        return _runner.updateListeningPort(socket, newPort);
+    }
+
+    
+    /**
+     * Add the packet to the queue.  This may block until there is space
+     * available, if requested, otherwise it returns immediately
+     *
+     * @return number of packets queued
+     */
+    public int add(UDPPacket packet, boolean blocking) {
+        int remaining = -1;
+        while ( (_keepRunning) && (remaining < 0) ) {
+            try {
+                synchronized (_outboundQueue) {
+                    if (_outboundQueue.size() < MAX_QUEUED) {
+                        _outboundQueue.add(packet);
+                        remaining = _outboundQueue.size();
+                        _outboundQueue.notifyAll();
+                    } else {
+                        if (blocking) {
+                            _outboundQueue.wait();
+                        } else {
+                            remaining = _outboundQueue.size();
+                        }
+                    }
+                }
+            } catch (InterruptedException ie) {}
+        }
+        return remaining;
+    }
+    
+    /**
+     *
+     * @return number of packets in the queue
+     */
+    public int add(UDPPacket packet) {
+        int size = 0;
+        synchronized (_outboundQueue) {
+            _outboundQueue.add(packet);
+            size = _outboundQueue.size();
+            _outboundQueue.notifyAll();
+        }
+        return size;
+    }
+    
+    private class Runner implements Runnable {
+        private boolean _socketChanged;
+        public void run() {
+            _socketChanged = false;
+            while (_keepRunning) {
+                if (_socketChanged) {
+                    Thread.currentThread().setName(_name);
+                    _socketChanged = false;
+                }
+                
+                UDPPacket packet = getNextPacket();
+                if (packet != null) {
+                    int size = packet.getPacket().getLength();
+                    if (size > 0) {
+                        FIFOBandwidthLimiter.Request req = _context.bandwidthLimiter().requestOutbound(size, "UDP sender");
+                        while (req.getPendingOutboundRequested() > 0)
+                            req.waitForNextAllocation();
+                    }
+                    
+                    if (_log.shouldLog(Log.DEBUG)) {
+                        int len = packet.getPacket().getLength();
+                        //if (len > 128)
+                        //    len = 128;
+                        _log.debug("Sending packet: \nraw: " + Base64.encode(packet.getPacket().getData(), 0, len));
+                    }
+                    
+                    try {
+                        synchronized (Runner.this) {
+                            // synchronization lets us update safely
+                            _socket.send(packet.getPacket());
+                        }
+                        _context.statManager().addRateData("udp.pushTime", packet.getLifetime(), packet.getLifetime());
+                        _context.statManager().addRateData("udp.sendPacketSize", packet.getPacket().getLength(), packet.getLifetime());
+                    } catch (IOException ioe) {
+                        if (_log.shouldLog(Log.ERROR))
+                            _log.error("Error sending", ioe);
+                    }
+                    
+                    // back to the cache
+                    //packet.release();
+                }
+            }
+        }
+        
+        private UDPPacket getNextPacket() {
+            UDPPacket packet = null;
+            while ( (_keepRunning) && (packet == null) ) {
+                try {
+                    synchronized (_outboundQueue) {
+                        if (_outboundQueue.size() <= 0) {
+                            _outboundQueue.wait();
+                        } else {
+                            packet = (UDPPacket)_outboundQueue.remove(0);
+                            _outboundQueue.notifyAll();
+                        }
+                    }
+                } catch (InterruptedException ie) {}
+            }
+            return packet;
+        }
+        public DatagramSocket updateListeningPort(DatagramSocket socket, int newPort) {
+            _name = "UDPSend on " + newPort;
+            DatagramSocket old = null;
+            synchronized (Runner.this) {
+                old = _socket;
+                _socket = socket;
+            }
+            _socketChanged = true;
+            return old;
+        }
+    }
+}
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
new file mode 100644
index 0000000000000000000000000000000000000000..e37c87036db92b9fdca3fbeade47324fb5c31e9d
--- /dev/null
+++ b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
@@ -0,0 +1,546 @@
+package net.i2p.router.transport.udp;
+
+import java.net.InetAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import net.i2p.data.Base64;
+import net.i2p.data.DataHelper;
+import net.i2p.data.Hash;
+import net.i2p.data.RouterAddress;
+import net.i2p.data.RouterInfo;
+import net.i2p.data.SessionKey;
+import net.i2p.data.i2np.I2NPMessage;
+import net.i2p.router.OutNetMessage;
+import net.i2p.router.RouterContext;
+import net.i2p.router.transport.Transport;
+import net.i2p.router.transport.TransportImpl;
+import net.i2p.router.transport.TransportBid;
+import net.i2p.util.Log;
+
+/**
+ *
+ */
+public class UDPTransport extends TransportImpl implements TimedWeightedPriorityMessageQueue.FailedListener {
+    private RouterContext _context;
+    private Log _log;
+    private UDPEndpoint _endpoint;
+    /** Peer (Hash) to PeerState */
+    private Map _peersByIdent;
+    /** Remote host (ip+port as a string) to PeerState */
+    private Map _peersByRemoteHost;
+    /** Relay tag (base64 String) to PeerState */
+    private Map _peersByRelayTag;
+    private PacketHandler _handler;
+    private EstablishmentManager _establisher;
+    private MessageQueue _outboundMessages;
+    private OutboundMessageFragments _fragments;
+    private OutboundRefiller _refiller;
+    private PacketPusher _pusher;
+    private InboundMessageFragments _inboundFragments;
+    
+    /** list of RelayPeer objects for people who will relay to us */
+    private List _relayPeers;
+
+    /** summary info to distribute */
+    private RouterAddress _externalAddress;
+    /** port number on which we can be reached, or -1 */
+    private int _externalListenPort;
+    /** IP address of externally reachable host, or null */
+    private InetAddress _externalListenHost;
+    /** introduction key */
+    private SessionKey _introKey;
+    
+    /** shared fast bid for connected peers */
+    private TransportBid _fastBid;
+    /** shared slow bid for unconnected peers */
+    private TransportBid _slowBid;
+
+    public static final String STYLE = "udp";
+    public static final String PROP_INTERNAL_PORT = "i2np.udp.internalPort";
+
+    /** define this to explicitly set an external IP address */
+    public static final String PROP_EXTERNAL_HOST = "i2np.udp.host";
+    /** define this to explicitly set an external port */
+    public static final String PROP_EXTERNAL_PORT = "i2np.udp.port";
+    
+    
+    /** how many relays offered to us will we use at a time? */
+    public static final int PUBLIC_RELAY_COUNT = 3;
+    
+    /** configure the priority queue with the given split points */
+    private static final int PRIORITY_LIMITS[] = new int[] { 100, 200, 300, 400, 500, 1000 };
+    /** configure the priority queue with the given weighting per priority group */
+    private static final int PRIORITY_WEIGHT[] = new int[] { 1, 1, 1, 1, 1, 2 };
+    
+    public UDPTransport(RouterContext ctx) {
+        super(ctx);
+        _context = ctx;
+        _log = ctx.logManager().getLog(UDPTransport.class);
+        _peersByIdent = new HashMap(128);
+        _peersByRemoteHost = new HashMap(128);
+        _peersByRelayTag = new HashMap(128);
+        _endpoint = null;
+        
+        _outboundMessages = new TimedWeightedPriorityMessageQueue(ctx, PRIORITY_LIMITS, PRIORITY_WEIGHT, this);
+        _relayPeers = new ArrayList(1);
+
+        _fastBid = new SharedBid(50);
+        _slowBid = new SharedBid(100);
+        
+        _fragments = new OutboundMessageFragments(_context, this);
+        _inboundFragments = new InboundMessageFragments(_context, _fragments, this);
+    }
+    
+    public void startup() {
+        if (_fragments != null)
+            _fragments.shutdown();
+        if (_pusher != null)
+            _pusher.shutdown();
+        if (_handler != null) 
+            _handler.shutdown();
+        if (_endpoint != null)
+            _endpoint.shutdown();
+        if (_establisher != null)
+            _establisher.shutdown();
+        if (_refiller != null)
+            _refiller.shutdown();
+        if (_inboundFragments != null)
+            _inboundFragments.shutdown();
+        
+        _introKey = new SessionKey(new byte[SessionKey.KEYSIZE_BYTES]);
+        System.arraycopy(_context.routerHash().getData(), 0, _introKey.getData(), 0, SessionKey.KEYSIZE_BYTES);
+        
+        rebuildExternalAddress();
+        
+        if (_endpoint == null) {
+            int port = -1;
+            if (_externalListenPort <= 0) {
+                // no explicit external port, so lets try an internal one
+                String portStr = _context.getProperty(PROP_INTERNAL_PORT);
+                if (portStr != null) {
+                    try {
+                        port = Integer.parseInt(portStr);
+                    } catch (NumberFormatException nfe) {
+                        if (_log.shouldLog(Log.ERROR))
+                            _log.error("Invalid port specified [" + portStr + "]");
+                    }
+                }
+                if (port <= 0) {
+                    port = 1024 + _context.random().nextInt(31*1024);
+                    if (_log.shouldLog(Log.INFO))
+                        _log.info("Selecting a random port to bind to: " + port);
+                }
+            } else {
+                port = _externalListenPort;
+                if (_log.shouldLog(Log.INFO))
+                    _log.info("Binding to the explicitly specified external port: " + port);
+            }
+            try {
+                _endpoint = new UDPEndpoint(_context, port);
+            } catch (SocketException se) {
+                if (_log.shouldLog(Log.CRIT))
+                    _log.log(Log.CRIT, "Unable to listen on the UDP port (" + port + ")", se);
+                return;
+            }
+        }
+        
+        if (_establisher == null)
+            _establisher = new EstablishmentManager(_context, this);
+        
+        if (_handler == null)
+            _handler = new PacketHandler(_context, this, _endpoint, _establisher, _inboundFragments);
+        
+        if (_refiller == null)
+            _refiller = new OutboundRefiller(_context, _fragments, _outboundMessages);
+        
+        _endpoint.startup();
+        _establisher.startup();
+        _handler.startup();
+        _fragments.startup();
+        _inboundFragments.startup();
+        _pusher = new PacketPusher(_context, _fragments, _endpoint.getSender());
+        _pusher.startup();
+        _refiller.startup();
+    }
+    
+    public void shutdown() {
+        if (_refiller != null)
+            _refiller.shutdown();
+        if (_handler != null)
+            _handler.shutdown();
+        if (_endpoint != null)
+            _endpoint.shutdown();
+        if (_fragments != null)
+            _fragments.shutdown();
+        if (_pusher != null)
+            _pusher.shutdown();
+        if (_establisher != null)
+            _establisher.shutdown();
+        if (_inboundFragments != null)
+            _inboundFragments.shutdown();
+    }
+    
+    /**
+     * Introduction key that people should use to contact us
+     *
+     */
+    public SessionKey getIntroKey() { return _introKey; }
+    public int getLocalPort() { return _externalListenPort; }
+    public InetAddress getLocalAddress() { return _externalListenHost; }
+    public int getExternalPort() { return _externalListenPort; }
+    
+    /**
+     * Someone we tried to contact gave us what they think our IP address is.
+     * Right now, we just blindly trust them, changing our IP and port on a
+     * whim.  this is not good ;)
+     *
+     */
+    void externalAddressReceived(byte ourIP[], int ourPort) {
+        if (_log.shouldLog(Log.WARN))
+            _log.debug("External address received: " + Base64.encode(ourIP) + ":" + ourPort);
+        
+        if (explicitAddressSpecified()) 
+            return;
+            
+        synchronized (this) {
+            if ( (_externalListenHost == null) ||
+                 (!eq(_externalListenHost.getAddress(), _externalListenPort, ourIP, ourPort)) ) {
+                try {
+                    _externalListenHost = InetAddress.getByAddress(ourIP);
+                    _externalListenPort = ourPort;
+                    rebuildExternalAddress();
+                    replaceAddress(_externalAddress);
+                } catch (UnknownHostException uhe) {
+                    _externalListenHost = null;
+                }
+            }
+        }
+    }
+    
+    private static final boolean eq(byte laddr[], int lport, byte raddr[], int rport) {
+        return (rport == lport) && DataHelper.eq(laddr, raddr);
+    }
+    
+    /** 
+     * get the state for the peer at the given remote host/port, or null 
+     * if no state exists
+     */
+    public PeerState getPeerState(InetAddress remoteHost, int remotePort) {
+        String hostInfo = PeerState.calculateRemoteHostString(remoteHost.getAddress(), remotePort);
+        synchronized (_peersByRemoteHost) {
+            return (PeerState)_peersByRemoteHost.get(hostInfo);
+        }
+    }
+    
+    /** 
+     * get the state for the peer with the given ident, or null 
+     * if no state exists
+     */
+    public PeerState getPeerState(Hash remotePeer) { 
+        synchronized (_peersByIdent) {
+            return (PeerState)_peersByIdent.get(remotePeer);
+        }
+    }
+    
+    /**
+     * get the state for the peer being introduced, or null if we aren't
+     * offering to introduce anyone with that tag.
+     */
+    public PeerState getPeerState(String relayTag) {
+        synchronized (_peersByRelayTag) {
+            return (PeerState)_peersByRelayTag.get(relayTag);
+        }
+    }
+    
+    /** 
+     * add the peer info, returning true if it went in properly, false if
+     * it was rejected (causes include peer ident already connected, or no
+     * remote host info known
+     *
+     */
+    boolean addRemotePeerState(PeerState peer) {
+        if (_log.shouldLog(Log.WARN))
+            _log.debug("Add remote peer state: " + peer);
+        if (peer.getRemotePeer() != null) {
+            synchronized (_peersByIdent) {
+                PeerState oldPeer = (PeerState)_peersByIdent.put(peer.getRemotePeer(), peer);
+                if ( (oldPeer != null) && (oldPeer != peer) ) {
+                    _peersByIdent.put(oldPeer.getRemotePeer(), oldPeer);
+                    return false;
+                }
+            }
+        }
+        
+        String remoteString = peer.getRemoteHostString();
+        if (remoteString == null) return false;
+        
+        synchronized (_peersByRemoteHost) {
+            PeerState oldPeer = (PeerState)_peersByRemoteHost.put(remoteString, peer);
+            if ( (oldPeer != null) && (oldPeer != peer) ) {
+                _peersByRemoteHost.put(remoteString, oldPeer);
+                return false;
+            }
+        }
+        
+        _context.shitlist().unshitlistRouter(peer.getRemotePeer());
+
+        return true;
+    }
+    
+    int send(UDPPacket packet) { 
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Sending packet " + packet);
+        return _endpoint.send(packet); 
+    }
+    
+    public TransportBid bid(RouterInfo toAddress, long dataSize) {
+        Hash to = toAddress.getIdentity().calculateHash();
+        PeerState peer = getPeerState(to);
+        if (peer != null) {
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("bidding on a message to an established peer: " + peer);
+            return _fastBid;
+        } else {
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("bidding on a message to an unestablished peer: " + to.toBase64());
+            return _slowBid;
+        }
+    }
+    
+    public String getStyle() { return STYLE; }
+    public void send(OutNetMessage msg) { 
+        Hash to = msg.getTarget().getIdentity().calculateHash();
+        if (getPeerState(to) != null) {
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Sending outbound message to an established peer: " + to.toBase64());
+            _outboundMessages.add(msg);
+        } else {
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("Sending outbound message to an unestablished peer: " + to.toBase64());
+            _establisher.establish(msg);
+        }
+    }
+    void send(I2NPMessage msg, PeerState peer) {
+        if (_log.shouldLog(Log.DEBUG))
+            _log.debug("Injecting a data message to a new peer: " + peer);
+        OutboundMessageState state = new OutboundMessageState(_context);
+        state.initialize(msg, peer);
+        _fragments.add(state);
+    }
+
+    public OutNetMessage getNextMessage() { return getNextMessage(-1); }
+    /**
+     * Get the next message, blocking until one is found or the expiration
+     * reached.
+     *
+     * @param blockUntil expiration, or -1 if indefinite
+     */
+    public OutNetMessage getNextMessage(long blockUntil) {
+        return _outboundMessages.getNext(blockUntil);
+    }
+
+    
+    // we don't need the following, since we have our own queueing
+    protected void outboundMessageReady() { throw new UnsupportedOperationException("Not used for UDP"); }
+    
+    public RouterAddress startListening() {
+        startup();
+        return _externalAddress;
+    }
+    
+    public void stopListening() {
+        shutdown();
+    }
+    
+    void setExternalListenPort(int port) { _externalListenPort = port; }
+    void setExternalListenHost(InetAddress addr) { _externalListenHost = addr; }
+    void setExternalListenHost(byte addr[]) throws UnknownHostException { 
+        _externalListenHost = InetAddress.getByAddress(addr); 
+    }
+    void addRelayPeer(String host, int port, byte tag[], SessionKey relayIntroKey) {
+        if ( (_externalListenPort > 0) && (_externalListenHost != null) ) 
+            return; // no need for relay peers, as we are reachable
+        
+        RelayPeer peer = new RelayPeer(host, port, tag, relayIntroKey);
+        synchronized (_relayPeers) {
+            _relayPeers.add(peer);
+        }
+    }
+
+    private boolean explicitAddressSpecified() {
+        return (_context.getProperty(PROP_EXTERNAL_HOST) != null);
+    }
+    
+    void rebuildExternalAddress() {
+        if (explicitAddressSpecified()) {
+            try {
+                String host = _context.getProperty(PROP_EXTERNAL_HOST);
+                String port = _context.getProperty(PROP_EXTERNAL_PORT);
+                _externalListenHost = InetAddress.getByName(host);
+                _externalListenPort = Integer.parseInt(port);
+            } catch (UnknownHostException uhe) {
+                _externalListenHost = null;
+            } catch (NumberFormatException nfe) {
+                _externalListenPort = -1;
+            }
+        }
+            
+        Properties options = new Properties();
+        if ( (_externalListenPort > 0) && (_externalListenHost != null) ) {
+            options.setProperty(UDPAddress.PROP_PORT, String.valueOf(_externalListenPort));
+            options.setProperty(UDPAddress.PROP_HOST, _externalListenHost.getHostAddress());
+        } else {
+            // grab 3 relays randomly
+            synchronized (_relayPeers) {
+                Collections.shuffle(_relayPeers);
+                int numPeers = PUBLIC_RELAY_COUNT;
+                if (numPeers > _relayPeers.size())
+                    numPeers = _relayPeers.size();
+                for (int i = 0; i < numPeers; i++) {
+                    RelayPeer peer = (RelayPeer)_relayPeers.get(i);
+                    options.setProperty("relay." + i + ".host", peer.getHost());
+                    options.setProperty("relay." + i + ".port", String.valueOf(peer.getPort()));
+                    options.setProperty("relay." + i + ".tag", Base64.encode(peer.getTag()));
+                    options.setProperty("relay." + i + ".key", peer.getIntroKey().toBase64());
+                }
+            }
+            if (options.size() <= 0)
+                return;
+        }
+        options.setProperty(UDPAddress.PROP_INTRO_KEY, _introKey.toBase64());
+        
+        RouterAddress addr = new RouterAddress();
+        addr.setCost(5);
+        addr.setExpiration(null);
+        addr.setTransportStyle(STYLE);
+        addr.setOptions(options);
+        
+        _externalAddress = addr;
+        replaceAddress(addr);
+    }
+    
+    public void failed(OutNetMessage msg) {
+        if (msg == null) return;
+        if (_log.shouldLog(Log.WARN))
+            _log.warn("Sending message failed: " + msg, new Exception("failed from"));
+        super.afterSend(msg, false);
+    }
+    public void succeeded(OutNetMessage msg) {
+        if (msg == null) return;
+        if (_log.shouldLog(Log.INFO))
+            _log.info("Sending message succeeded: " + msg);
+        super.afterSend(msg, true);
+    }
+
+    public int countActivePeers() {
+        long now = _context.clock().now();
+        int active = 0;
+        int inactive = 0;
+        synchronized (_peersByIdent) {
+            for (Iterator iter = _peersByIdent.values().iterator(); iter.hasNext(); ) {
+                PeerState peer = (PeerState)iter.next();
+                if (now-peer.getLastReceiveTime() > 5*60*1000)
+                    inactive++;
+                else
+                    active++;
+            }
+        }
+        return active;
+    }
+    
+    public void renderStatusHTML(Writer out) throws IOException {
+        List peers = null;
+        synchronized (_peersByIdent) {
+            peers = new ArrayList(_peersByIdent.values());
+        }
+        
+        StringBuffer buf = new StringBuffer(512);
+        buf.append("<b>UDP connections: ").append(peers.size()).append("</b><br />\n");
+        buf.append("<table border=\"1\">\n");
+        buf.append(" <tr><td><b>Peer</b></td><td><b>Location</b></td>\n");
+        buf.append("     <td><b>Last send</b></td><td><b>Last recv</b></td>\n");
+        buf.append("     <td><b>Lifetime</b></td><td><b>Window size</b></td>\n");
+        buf.append("     <td><b>Sent</b></td><td><b>Received</b></td>\n");
+        buf.append(" </tr>\n");
+        out.write(buf.toString());
+        buf.setLength(0);
+        long now = _context.clock().now();
+        for (int i = 0; i < peers.size(); i++) {
+            PeerState peer = (PeerState)peers.get(i);
+            if (now-peer.getLastReceiveTime() > 60*60*1000)
+                continue; // don't include old peers
+            
+            buf.append("<tr>");
+            
+            buf.append("<td>");
+            buf.append(peer.getRemotePeer().toBase64().substring(0,6));
+            buf.append("</td>");
+            
+            buf.append("<td>");
+            byte ip[] = peer.getRemoteIP();
+            for (int j = 0; j < ip.length; j++) {
+                if (ip[j] < 0)
+                    buf.append(ip[j] + 255);
+                else
+                    buf.append(ip[j]);
+                if (j + 1 < ip.length)
+                    buf.append('.');
+            }
+            buf.append(':').append(peer.getRemotePort());
+            buf.append("</td>");
+            
+            buf.append("<td>");
+            buf.append(DataHelper.formatDuration(now-peer.getLastSendTime()));
+            buf.append("</td>");
+
+            buf.append("<td>");
+            buf.append(DataHelper.formatDuration(now-peer.getLastReceiveTime()));
+            buf.append("</td>");
+
+            buf.append("<td>");
+            buf.append(DataHelper.formatDuration(now-peer.getKeyEstablishedTime()));
+            buf.append("</td>");
+
+            buf.append("<td>");
+            buf.append(peer.getSendWindowBytes());
+            buf.append("</td>");
+
+            buf.append("<td>");
+            buf.append(peer.getMessagesSent());
+            buf.append("</td>");
+            
+            buf.append("<td>");
+            buf.append(peer.getMessagesReceived());
+            buf.append("</td>");
+
+            buf.append("</tr>");
+            out.write(buf.toString());
+            buf.setLength(0);
+        }
+        
+        out.write("</table>\n");
+    }
+
+    
+    /**
+     * Cache the bid to reduce object churn
+     */
+    private class SharedBid extends TransportBid {
+        private int _ms;
+        public SharedBid(int ms) { _ms = ms; }
+        public int getLatency() { return _ms; }
+        public Transport getTransport() { return UDPTransport.this; }
+    }
+}
diff --git a/router/java/src/net/i2p/router/tunnel/TunnelParticipant.java b/router/java/src/net/i2p/router/tunnel/TunnelParticipant.java
index 1a691ecae32566c4fceb31f21cfcc0b2dba69506..484cab14ec2328fa26088b3a4253df3046d3e041 100644
--- a/router/java/src/net/i2p/router/tunnel/TunnelParticipant.java
+++ b/router/java/src/net/i2p/router/tunnel/TunnelParticipant.java
@@ -68,8 +68,8 @@ public class TunnelParticipant {
             ok = _inboundEndpointProcessor.retrievePreprocessedData(msg.getData(), 0, msg.getData().length, recvFrom);
         
         if (!ok) {
-            if (_log.shouldLog(Log.ERROR))
-                _log.error("Failed to dispatch " + msg + ": processor=" + _processor 
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("Failed to dispatch " + msg + ": processor=" + _processor 
                            + " inboundEndpoint=" + _inboundEndpointProcessor);
             return;
         }