diff --git a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
index 92173a10d52b64661f21118ce94a3ba3ced6f8b7..7fba352139cf5737396ed1dc169ea6e51f38efb4 100644
--- a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
@@ -10,6 +10,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
+import net.i2p.data.Base64;
 import net.i2p.data.Hash;
 import net.i2p.data.router.RouterAddress;
 import net.i2p.data.router.RouterIdentity;
@@ -382,7 +383,17 @@ class EstablishmentManager {
                     }
                 } else {
                     // must have a valid session key
-                    byte[] keyBytes = addr.getIntroKey();
+                    byte[] keyBytes;
+                    int version = _transport.getSSUVersion(ra);
+                    if (version == 1) {
+                        keyBytes = addr.getIntroKey();
+                    } else {
+                        String siv = ra.getOption("i");
+                        if (siv != null)
+                            keyBytes = Base64.decode(siv);
+                        else
+                            keyBytes = null;
+                    }
                     if (keyBytes == null) {
                         _transport.markUnreachable(toHash);
                         _transport.failed(msg, "Peer has no key, cannot establish");
@@ -403,7 +414,6 @@ class EstablishmentManager {
                     // don't ask if they are indirect
                     boolean requestIntroduction = allowExtendedOptions && !isIndirect &&
                                                   _transport.introducersMaybeRequired(TransportUtil.isIPv6(ra));
-                    int version = _transport.getSSUVersion(ra);
                     if (version == 1) {
                         state = new OutboundEstablishState(_context, maybeTo, to,
                                                        toIdentity, allowExtendedOptions,
@@ -751,7 +761,7 @@ class EstablishmentManager {
      */
     void receiveRetry(OutboundEstablishState2 state, UDPPacket packet) {
         try {
-            state.receiveSessionCreated(packet);
+            state.receiveRetry(packet);
         } catch (GeneralSecurityException gse) {
             if (_log.shouldWarn())
                 _log.warn("Corrupt Retry from: " + state, gse);
@@ -760,7 +770,7 @@ class EstablishmentManager {
         }
         notifyActivity();
         if (_log.shouldLog(Log.DEBUG))
-            _log.debug("Receive retry from: " + state);
+            _log.debug("Receive retry with token " + state.getToken() + " from: " + state);
     }
 
     /**
@@ -1108,15 +1118,20 @@ class EstablishmentManager {
     public static final long MAX_TAG_VALUE = 0xFFFFFFFFl;
     
     /**
-     *  This may be called more than once
+     *  This handles both initial send and retransmission of Session Created,
+     *  and, for SSU2, send of Retry.
+     *  Retry is never retransmnitted.
+     *
+     *  This may be called more than once.
+     *
+     *  Caller must synch on state.
      */
     private void sendCreated(InboundEstablishState state) {
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug("Send created to: " + state);
-        
         int version = state.getVersion();
         UDPPacket pkt;
         if (version == 1) {
+            if (_log.shouldDebug())
+                _log.debug("Send created to: " + state);
             try {
                 state.generateSessionKey();
             } catch (DHSessionKeyBuilder.InvalidPublicParameterException ippe) {
@@ -1130,13 +1145,27 @@ class EstablishmentManager {
                                                      _transport.getExternalPort(state.getSentIP().length == 16),
                                                      _transport.getIntroKey());
         } else {
-            // if already sent, get from the state to retx
             InboundEstablishState2 state2 = (InboundEstablishState2) state;
             InboundEstablishState.InboundState istate = state2.getState();
-            if (istate == IB_STATE_CREATED_SENT)
+            if (istate == IB_STATE_CREATED_SENT) {
+                if (_log.shouldDebug())
+                    _log.debug("Send created to: " + state);
+                // if already sent, get from the state to retx
                 pkt = state2.getRetransmitSessionCreatedPacket();
-            else
-                pkt = _builder2.buildSessionCreatedPacket((InboundEstablishState2) state);
+            } else if (istate == IB_STATE_REQUEST_RECEIVED) {
+                if (_log.shouldDebug())
+                    _log.debug("Send created to: " + state);
+                pkt = _builder2.buildSessionCreatedPacket(state2);
+            } else if (istate == IB_STATE_TOKEN_REQUEST_RECEIVED ||
+                       istate == IB_STATE_REQUEST_BAD_TOKEN_RECEIVED) {
+                if (_log.shouldDebug())
+                    _log.debug("Send retry to: " + state);
+                pkt = _builder2.buildRetryPacket(state2);
+            } else {
+                if (_log.shouldWarn())
+                    _log.warn("Unhandled state " + istate + " on " + state);
+                return;
+            }
         }
         if (pkt == null) {
             if (_log.shouldLog(Log.WARN))
@@ -1152,23 +1181,44 @@ class EstablishmentManager {
     }
 
     /**
-     *  Caller should probably synch on outboundState
+     *  This handles both initial send and retransmission of Session Request,
+     *  and, for SSU2, initial send and retransmission of Token Request.
+     *
+     *  This may be called more than once.
+     *
+     *  Caller must synch on state.
      */
     private void sendRequest(OutboundEstablishState state) {
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug("Send SessionRequest to: " + state);
         int version = state.getVersion();
         UDPPacket packet;
         if (version == 1) {
+            if (_log.shouldDebug())
+                _log.debug("Send Session Request to: " + state);
             packet = _builder.buildSessionRequestPacket(state);
         } else {
-            // if already sent, get from the state to retx
             OutboundEstablishState2 state2 = (OutboundEstablishState2) state;
             OutboundEstablishState.OutboundState ostate = state2.getState();
-            if (ostate == OB_STATE_REQUEST_SENT)
+            if (ostate == OB_STATE_REQUEST_SENT ||
+                ostate == OB_STATE_REQUEST_SENT_NEW_TOKEN) {
+                if (_log.shouldDebug())
+                    _log.debug("Send Session Request to: " + state);
+                // if already sent, get from the state to retx
                 packet = state2.getRetransmitSessionRequestPacket();
-            else
+            } else if (ostate == OB_STATE_NEEDS_TOKEN ||
+                       ostate == OB_STATE_TOKEN_REQUEST_SENT) {
+                if (_log.shouldDebug())
+                    _log.debug("Send Token Request to: " + state);
+                packet = _builder2.buildTokenRequestPacket(state2);
+            } else if (ostate == OB_STATE_UNKNOWN ||
+                       ostate == OB_STATE_RETRY_RECEIVED) {
+                if (_log.shouldDebug())
+                    _log.debug("Send Session Request to: " + state);
                 packet = _builder2.buildSessionRequestPacket(state2);
+            } else {
+                if (_log.shouldWarn())
+                    _log.warn("Unhandled state " + ostate + " on " + state);
+                return;
+            }
         }
         if (packet != null) {
             _transport.send(packet);
@@ -1315,7 +1365,8 @@ class EstablishmentManager {
      *  Note that while a SessionConfirmed could in theory be fragmented,
      *  in practice a RouterIdentity is 387 bytes and a single fragment is 512 bytes max,
      *  so it will never be fragmented.
-     *  Caller should probably synch on state.
+     *
+     *  Caller must synch on state.
      */
     private void sendConfirmation(OutboundEstablishState state) {
         boolean valid = state.validateSessionCreated();
@@ -1379,9 +1430,13 @@ class EstablishmentManager {
      *  ack to the SessionConfirmed - otherwise we haven't generated the keys.
      *  Caller should probably synch on state.
      *
+     *  SSU1 only.
+     *
      *  @since 0.9.2
      */
     private void sendDestroy(OutboundEstablishState state) {
+        if (state.getVersion() > 1)
+            return;
         UDPPacket packet = _builder.buildSessionDestroyPacket(state);
         if (packet != null) {
             if (_log.shouldLog(Log.DEBUG))
@@ -1397,9 +1452,13 @@ class EstablishmentManager {
      *  Otherwise we haven't generated the keys.
      *  Caller should probably synch on state.
      *
+     *  SSU1 only.
+     *
      *  @since 0.9.2
      */
     private void sendDestroy(InboundEstablishState state) {
+        if (state.getVersion() > 1)
+            return;
         UDPPacket packet = _builder.buildSessionDestroyPacket(state);
         if (packet != null) {
             if (_log.shouldLog(Log.DEBUG))
@@ -1463,8 +1522,11 @@ class EstablishmentManager {
             //if (_log.shouldLog(Log.DEBUG))
             //    _log.debug("Processing for inbound: " + inboundState);
             synchronized (inboundState) {
-                switch (inboundState.getState()) {
+                InboundEstablishState.InboundState istate = inboundState.getState();
+                switch (istate) {
                   case IB_STATE_REQUEST_RECEIVED:
+                  case IB_STATE_TOKEN_REQUEST_RECEIVED:      // SSU2
+                  case IB_STATE_REQUEST_BAD_TOKEN_RECEIVED:  // SSU2
                     if (expired)
                         processExpired(inboundState);
                     else
@@ -1473,11 +1535,18 @@ class EstablishmentManager {
 
                   case IB_STATE_CREATED_SENT: // fallthrough
                   case IB_STATE_CONFIRMED_PARTIALLY:
+                  case IB_STATE_RETRY_SENT:                  // SSU2
                     if (expired) {
                         sendDestroy(inboundState);
                         processExpired(inboundState);
                     } else if (inboundState.getNextSendTime() <= now) {
-                        sendCreated(inboundState);
+                        if (istate == IB_STATE_RETRY_SENT) {
+                            // Retry is never retransmitted
+                            inboundState.fail();
+                            processExpired(inboundState);
+                        } else {
+                            sendCreated(inboundState);
+                        }
                     }
                     break;
 
@@ -1510,6 +1579,12 @@ class EstablishmentManager {
                     // Can't happen, always call receiveSessionRequest() before putting in map
                     if (_log.shouldLog(Log.ERROR))
                         _log.error("hrm, state is unknown for " + inboundState);
+                    break;
+
+                  default:
+                    if (_log.shouldWarn())
+                        _log.warn("Unhandled state on " + inboundState);
+                    break;
                 }
             }
 
@@ -1585,6 +1660,7 @@ class EstablishmentManager {
                 switch (outboundState.getState()) {
                     case OB_STATE_UNKNOWN:  // fall thru
                     case OB_STATE_INTRODUCED:
+                    case OB_STATE_NEEDS_TOKEN:             // SSU2 only
                         if (expired)
                             processExpired(outboundState);
                         else
@@ -1592,6 +1668,9 @@ class EstablishmentManager {
                         break;
 
                     case OB_STATE_REQUEST_SENT:
+                    case OB_STATE_TOKEN_REQUEST_SENT:      // SSU2 only
+                    case OB_STATE_RETRY_RECEIVED:          // SSU2 only
+                    case OB_STATE_REQUEST_SENT_NEW_TOKEN:  // SSU2 only
                         // no response yet (or it was invalid), lets retry
                         long rtime = outboundState.getRequestSentTime();
                         if (expired || (rtime > 0 && rtime + OB_MESSAGE_TIMEOUT <= now))
@@ -1635,6 +1714,11 @@ class EstablishmentManager {
                     case OB_STATE_VALIDATION_FAILED:
                         processExpired(outboundState);
                         break;
+
+                    default:
+                        if (_log.shouldWarn())
+                            _log.warn("Unhandled state on " + outboundState);
+                        break;
                 }
             }
             
@@ -1695,6 +1779,7 @@ class EstablishmentManager {
      *  @since 0.9.2
      */
     private void processExpired(InboundEstablishState inboundState) {
+        _inboundStates.remove(inboundState.getRemoteHostId());
         OutNetMessage msg;
         while ((msg = inboundState.getNextQueuedMessage()) != null) {
             _transport.failed(msg, "Expired during failed establish");
@@ -1794,6 +1879,8 @@ class EstablishmentManager {
                     doPass();
                 } catch (RuntimeException re) {
                     _log.log(Log.CRIT, "Error in the establisher", re);
+                    // don't loop too fast
+                    try { Thread.sleep(1000); } catch (InterruptedException ie) {}
                 }
             }
             _inboundStates.clear();
diff --git a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java
index a60b673e11a61067ba221de851133439ea6cbfbf..a45099c6dee0594c9c0a1ed2d2da2c9f905247bd 100644
--- a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java
+++ b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java
@@ -32,8 +32,8 @@ class InboundEstablishState {
     protected final Log _log;
     // SessionRequest message
     private byte _receivedX[];
-    private byte _bobIP[];
-    private final int _bobPort;
+    protected byte _bobIP[];
+    protected final int _bobPort;
     private final DHSessionKeyBuilder _keyBuilder;
     // SessionCreated message
     private byte _sentY[];
diff --git a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java
index cc49bb57723daa03798a605f9d37505ab2d75879..d0687eee6402806a31c2e286e33abca4ce7a1969 100644
--- a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java
+++ b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState2.java
@@ -77,9 +77,6 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
         //_sendHeaderEncryptKey2 set below
         //_rcvHeaderEncryptKey2 set below
         _introductionRequested = false; // todo
-        //_bobIP = TODO
-        //if (_log.shouldLog(Log.DEBUG))
-        //    _log.debug("Receive sessionRequest, BobIP = " + Addresses.toString(_bobIP));
         int off = pkt.getOffset();
         int len = pkt.getLength();
         byte data[] = pkt.getData();
@@ -107,12 +104,18 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
         } else if (type == SESSION_REQUEST_FLAG_BYTE &&
                    (token == 0 ||
                     (ENFORCE_TOKEN && !_transport.getEstablisher().isInboundTokenValid(_remoteHostId, token)))) {
+            if (_log.shouldInfo())
+                _log.info("Invalid token " + token + " in session request from: " + _aliceSocketAddress);
             _currentState = InboundState.IB_STATE_REQUEST_BAD_TOKEN_RECEIVED;
             _sendHeaderEncryptKey2 = introKey;
+            // Generate token for the retry.
+            // We do NOT register it with the EstablishmentManager, it must be used immediately.
             do {
                 token = ctx.random().nextLong();
             } while (token == 0);
             _token = token;
+            // do NOT bother to init the handshake state and decrypt the payload
+            _timeReceived = _establishBegin;
         } else {
             // fast MSB check for key < 2^255
             if ((data[off + LONG_HEADER_SIZE + KEY_LEN - 1] & 0x80) != 0)
@@ -251,7 +254,11 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
     }
 
     public void gotAddress(byte[] ip, int port) {
-        throw new IllegalStateException("Address in Handshake");
+        if (_log.shouldDebug())
+            _log.debug("Got Address: " + Addresses.toString(ip, port));
+        _bobIP = ip;
+        // final, see super
+        //_bobPort = port;
     }
 
     public void gotIntroKey(byte[] key) {
@@ -356,8 +363,7 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
         }
         _createdSentCount++;
         _nextSend = _lastSend + delay;
-        if ( (_currentState == InboundState.IB_STATE_UNKNOWN) || (_currentState == InboundState.IB_STATE_REQUEST_RECEIVED) )
-            _currentState = InboundState.IB_STATE_CREATED_SENT;
+        _currentState = InboundState.IB_STATE_CREATED_SENT;
     }
 
     
@@ -368,6 +374,9 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
             throw new IllegalStateException("Bad state for Retry Sent: " + _currentState);
         _currentState = InboundState.IB_STATE_RETRY_SENT;
         _lastSend = _context.clock().now();
+        // Won't really be transmitted, they have 3 sec to respond or
+        // EstablishmentManager.handleInbound() will fail the connection
+        _nextSend = _lastSend + RETRANSMIT_DELAY;
     }
 
     /**
@@ -539,17 +548,7 @@ class InboundEstablishState2 extends InboundEstablishState implements SSU2Payloa
         byte data[] = pkt.getData();
         int off = pkt.getOffset();
         System.arraycopy(_sessCrForReTX, 0, data, off, _sessCrForReTX.length);
-        InetAddress to;
-        try {
-            to = InetAddress.getByAddress(_aliceIP);
-        } catch (UnknownHostException uhe) {
-            if (_log.shouldLog(Log.ERROR))
-                _log.error("How did we think this was a valid IP?  " + _remoteHostId);
-            packet.release();
-            return null;
-        }
-        pkt.setAddress(to);
-        pkt.setPort(_alicePort);
+        pkt.setSocketAddress(_aliceSocketAddress);
         packet.setMessageType(PacketBuilder2.TYPE_CONF);
         packet.setPriority(PacketBuilder2.PRIORITY_HIGH);
         createdPacketSent();
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
index b77fbff12915bcb7c29389938e0ba5b839c5780a..5f4b1730b7beeabb66376e8937da81162db60f5d 100644
--- a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
@@ -35,8 +35,8 @@ class OutboundEstablishState {
     private DHSessionKeyBuilder _keyBuilder;
     // SessionCreated message
     private byte _receivedY[];
-    private byte _aliceIP[];
-    private int _alicePort;
+    protected byte _aliceIP[];
+    protected int _alicePort;
     private long _receivedRelayTag;
     private long _receivedSignedOnTime;
     private SessionKey _sessionKey;
@@ -94,6 +94,11 @@ class OutboundEstablishState {
         /** SessionConfirmed failed validation */
         OB_STATE_VALIDATION_FAILED,
 
+        /**
+         * SSU2: We don't have a token
+         * @since 0.9.54
+         */
+        OB_STATE_NEEDS_TOKEN,
         /**
          * SSU2: We have sent a token request
          * @since 0.9.54
diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java
index aa2d0fc492b937e25c809baad12857eb17a2e7dc..ea2ecaf48a0925fe4768d2927f1c2550dc0cc97d 100644
--- a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java
@@ -123,6 +123,8 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
         _routerAddress = ra;
         if (_token != 0)
             createNewState(ra);
+        else
+            _currentState = OutboundState.OB_STATE_NEEDS_TOKEN;
 
         byte[] ik = introKey.getData();
         _sendHeaderEncryptKey1 = ik;
@@ -196,7 +198,9 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
 
     public void gotAddress(byte[] ip, int port) {
         if (_log.shouldDebug())
-            _log.debug("Got ADDRESS block: " + Addresses.toString(ip, port));
+            _log.debug("Got Address: " + Addresses.toString(ip, port));
+        _aliceIP = ip;
+        _alicePort = port;
     }
 
     public void gotIntroKey(byte[] key) {
@@ -245,8 +249,15 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
     // end payload callbacks
     /////////////////////////////////////////////////////////
     
-    // SSU 1 unsupported things
+    // SSU 1 overrides
 
+    @Override
+    public synchronized boolean validateSessionCreated() {
+        // All validation is in receiveSessionCreated()
+        boolean rv = _currentState == OutboundState.OB_STATE_CREATED_RECEIVED ||
+                     _currentState == OutboundState.OB_STATE_CONFIRMED_COMPLETELY;
+        return rv;
+    }
 
     // SSU 2 things
 
@@ -268,6 +279,9 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
     public byte[] getSendHeaderEncryptKey1() { return _sendHeaderEncryptKey1; }
     public byte[] getRcvHeaderEncryptKey1() { return _rcvHeaderEncryptKey1; }
     public byte[] getSendHeaderEncryptKey2() { return _sendHeaderEncryptKey2; }
+    /**
+     *  @return null before Session Request is sent (i.e. we sent a Token Request first)
+     */
     public byte[] getRcvHeaderEncryptKey2() { return _rcvHeaderEncryptKey2; }
     public byte[] getRcvRetryHeaderEncryptKey2() { return _rcvRetryHeaderEncryptKey2; }
     public InetSocketAddress getSentAddress() { return _bobSocketAddress; }
@@ -278,9 +292,22 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
     public synchronized void receiveRetry(UDPPacket packet) throws GeneralSecurityException {
         ////// TODO state check
         DatagramPacket pkt = packet.getPacket();
+        SocketAddress from = pkt.getSocketAddress();
+        if (!from.equals(_bobSocketAddress))
+            throw new GeneralSecurityException("Address mismatch: req: " + _bobSocketAddress + " conf: " + from);
         int off = pkt.getOffset();
         int len = pkt.getLength();
         byte data[] = pkt.getData();
+        long rid = DataHelper.fromLong8(data, off);
+        if (rid != _rcvConnID)
+            throw new GeneralSecurityException("Conn ID mismatch: 1: " + _rcvConnID + " 2: " + rid);
+        long sid = DataHelper.fromLong8(data, off + SRC_CONN_ID_OFFSET);
+        if (sid != _sendConnID)
+            throw new GeneralSecurityException("Conn ID mismatch: 1: " + _sendConnID + " 2: " + sid);
+        long token = DataHelper.fromLong8(data, off + TOKEN_OFFSET);
+        if (token == 0)
+            throw new GeneralSecurityException("Bad token 0 in retry");
+        _token = token;
         _timeReceived = 0;
         try {
             // decrypt in-place
@@ -302,7 +329,8 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
         if (skew > MAX_SKEW || skew < 0 - MAX_SKEW)
             throw new GeneralSecurityException("Skew exceeded in Session/Token Request: " + skew);
         createNewState(_routerAddress);
-        ////// TODO state change
+        _currentState = OutboundState.OB_STATE_RETRY_RECEIVED;
+        packetReceived();
     }
 
     public synchronized void receiveSessionCreated(UDPPacket packet) throws GeneralSecurityException {
@@ -320,6 +348,13 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
         int off = pkt.getOffset();
         int len = pkt.getLength();
         byte data[] = pkt.getData();
+        long rid = DataHelper.fromLong8(data, off);
+        if (rid != _rcvConnID)
+            throw new GeneralSecurityException("Conn ID mismatch: 1: " + _rcvConnID + " 2: " + rid);
+        long sid = DataHelper.fromLong8(data, off + SRC_CONN_ID_OFFSET);
+        if (sid != _sendConnID)
+            throw new GeneralSecurityException("Conn ID mismatch: 1: " + _sendConnID + " 2: " + sid);
+
         _handshakeState.mixHash(data, off, LONG_HEADER_SIZE);
         if (_log.shouldDebug())
             _log.debug("State after mixHash 2: " + _handshakeState);
@@ -344,11 +379,7 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
         _sessReqForReTX = null;
         _sendHeaderEncryptKey2 = SSU2Util.hkdf(_context, _handshakeState.getChainingKey(), "SessionConfirmed");
 
-        if (_currentState == OutboundState.OB_STATE_UNKNOWN ||
-            _currentState == OutboundState.OB_STATE_REQUEST_SENT ||
-            _currentState == OutboundState.OB_STATE_INTRODUCED ||
-            _currentState == OutboundState.OB_STATE_PENDING_INTRO)
-            _currentState = OutboundState.OB_STATE_CREATED_RECEIVED;
+        _currentState = OutboundState.OB_STATE_CREATED_RECEIVED;
 
         if (_requestSentCount == 1) {
             _rtt = (int) (_context.clock().now() - _requestSentTime);
@@ -361,10 +392,10 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
      * and save them for retransmission
      */
     public synchronized void tokenRequestSent(DatagramPacket packet) {
-        if (_currentState == OutboundState.OB_STATE_UNKNOWN)
+        OutboundState old = _currentState;
+        requestSent();
+        if (old == OutboundState.OB_STATE_NEEDS_TOKEN)
             _currentState = OutboundState.OB_STATE_TOKEN_REQUEST_SENT;
-        else if (_currentState == OutboundState.OB_STATE_RETRY_RECEIVED)
-            _currentState = OutboundState.OB_STATE_REQUEST_SENT_NEW_TOKEN;
         // don't bother saving for retx, just make a new one every time
     }
 
@@ -383,7 +414,10 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
         }
         if (_rcvHeaderEncryptKey2 == null)
             _rcvHeaderEncryptKey2 = SSU2Util.hkdf(_context, _handshakeState.getChainingKey(), "SessCreateHeader");
+        OutboundState old = _currentState;
         requestSent();
+        if (old == OutboundState.OB_STATE_RETRY_RECEIVED)
+            _currentState = OutboundState.OB_STATE_REQUEST_SENT_NEW_TOKEN;
     }
 
     /**
@@ -461,17 +495,7 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
         byte data[] = pkt.getData();
         int off = pkt.getOffset();
         System.arraycopy(_sessReqForReTX, 0, data, off, _sessReqForReTX.length);
-        InetAddress to;
-        try {
-            to = InetAddress.getByAddress(_bobIP);
-        } catch (UnknownHostException uhe) {
-            if (_log.shouldLog(Log.ERROR))
-                _log.error("How did we think this was a valid IP?  " + _remoteHostId);
-            packet.release();
-            return null;
-        }
-        pkt.setAddress(to);
-        pkt.setPort(_bobPort);
+        pkt.setSocketAddress(_bobSocketAddress);
         packet.setMessageType(PacketBuilder2.TYPE_SREQ);
         packet.setPriority(PacketBuilder2.PRIORITY_HIGH);
         requestSent();
diff --git a/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java b/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java
index 6be94e5a49caf8ed9ac12daa45164a7f6bf2aa58..4977c9d7d9917692e9eaea55b0597d5623629ec5 100644
--- a/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java
+++ b/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java
@@ -207,7 +207,8 @@ class PacketBuilder2 {
             off += sz;
             sizeWritten += sz;
         }
-        Block block = getPadding(sizeWritten, peer.getMTU());
+        // FIXME
+        Block block = getPadding(sizeWritten, currentMTU);
         if (block != null) {
             blocks.add(block);
             int sz = block.getTotalLength();
@@ -216,13 +217,13 @@ class PacketBuilder2 {
         }
         SSU2Payload.writePayload(data, SHORT_HEADER_SIZE, blocks);
         pkt.setLength(off);
-        if (_log.shouldDebug())
-            _log.debug("Packet " + pktNum + " before encryption:\n" + HexDump.dump(data, 0, off));
+        //if (_log.shouldDebug())
+        //    _log.debug("Packet " + pktNum + " before encryption:\n" + HexDump.dump(data, 0, off));
 
         encryptDataPacket(packet, peer.getSendCipher(), pktNum, peer.getSendHeaderEncryptKey1(), peer.getSendHeaderEncryptKey2());
         setTo(packet, peer.getRemoteIPAddress(), peer.getRemotePort());
-        if (_log.shouldDebug())
-            _log.debug("Packet " + pktNum + " after encryption:\n" + HexDump.dump(data, 0, pkt.getLength()));
+        //if (_log.shouldDebug())
+        //    _log.debug("Packet " + pktNum + " after encryption:\n" + HexDump.dump(data, 0, pkt.getLength()));
         
         // FIXME ticket #2675
         // the packet could have been built before the current mtu got lowered, so
@@ -294,7 +295,6 @@ class PacketBuilder2 {
         pkt.setLength(LONG_HEADER_SIZE);
         byte[] introKey = state.getSendHeaderEncryptKey1();
         encryptTokenRequest(packet, introKey, n, introKey, introKey);
-        state.requestSent();
         pkt.setSocketAddress(state.getSentAddress());
         packet.setMessageType(TYPE_SREQ);
         packet.setPriority(PRIORITY_HIGH);
@@ -316,7 +316,6 @@ class PacketBuilder2 {
         pkt.setLength(LONG_HEADER_SIZE);
         byte[] introKey = state.getSendHeaderEncryptKey1();
         encryptSessionRequest(packet, state.getHandshakeState(), introKey, introKey, state.needIntroduction());
-        state.requestSent();
         pkt.setSocketAddress(state.getSentAddress());
         packet.setMessageType(TYPE_SREQ);
         packet.setPriority(PRIORITY_HIGH);
@@ -451,7 +450,8 @@ class PacketBuilder2 {
         pkt.setLength(SHORT_HEADER_SIZE);
         SSU2Payload.RIBlock block = new SSU2Payload.RIBlock(ourInfo,  0, len,
                                                             false, gzip, 0, numFragments);
-        encryptSessionConfirmed(packet, state.getHandshakeState(), state.getMTU(),
+        boolean isIPv6 = state.getSentIP().length == 16;
+        encryptSessionConfirmed(packet, state.getHandshakeState(), state.getMTU(), isIPv6,
                                 state.getSendHeaderEncryptKey1(), state.getSendHeaderEncryptKey2(), block, state.getNextToken());
         pkt.setSocketAddress(state.getSentAddress());
         packet.setMessageType(TYPE_CONF);
@@ -875,22 +875,24 @@ class PacketBuilder2 {
      *  @param packet containing only 16 byte header
      */
     private void encryptSessionConfirmed(UDPPacket packet, HandshakeState state, int mtu,
-                                         byte[] hdrKey1, byte[] hdrKey2,
+                                         boolean isIPv6, byte[] hdrKey1, byte[] hdrKey2,
                                          SSU2Payload.RIBlock riblock, long token) {
         DatagramPacket pkt = packet.getPacket();
         byte data[] = pkt.getData();
         int off = pkt.getOffset();
+        mtu -= UDP_HEADER_SIZE;
+        mtu -= isIPv6 ? IPV6_HEADER_SIZE : IP_HEADER_SIZE;
         try {
             List<Block> blocks = new ArrayList<Block>(3);
             int len = riblock.getTotalLength();
             blocks.add(riblock);
-            if (token > 0) {
-                // TODO only if room
+            // only if room
+            if (token > 0 && mtu - len >= 15) {
                 Block block = new SSU2Payload.NewTokenBlock(token, _context.clock().now() + EstablishmentManager.IB_TOKEN_EXPIRATION);
                 len += block.getTotalLength();
                 blocks.add(block);
             }
-            Block block = getPadding(len, mtu - 80);
+            Block block = getPadding(len, mtu - (SHORT_HEADER_SIZE + KEY_LEN + MAC_LEN + MAC_LEN)); // 80
             if (block != null) {
                 len += block.getTotalLength();
                 blocks.add(block);
@@ -904,6 +906,8 @@ class PacketBuilder2 {
                 _log.debug("State after mixHash 3: " + state);
             state.writeMessage(data, off + SHORT_HEADER_SIZE, data, off + SHORT_HEADER_SIZE + KEY_LEN + MAC_LEN, len);
             pkt.setLength(pkt.getLength() + KEY_LEN + MAC_LEN + len + MAC_LEN);
+            if (_log.shouldDebug())
+                _log.debug("Session confirmed packet length is: " + pkt.getLength());
         } catch (RuntimeException re) {
             if (!_log.shouldWarn())
                 _log.error("Bad msg 3 out", re);
diff --git a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
index 0d2f02e92437f26b813fbf147311312ee4dcc703..084796181d086191d346924dbd32296f9046e290 100644
--- a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
+++ b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
@@ -211,7 +211,7 @@ class PacketHandler {
                     handlePacket(_reader, packet);
                 } catch (RuntimeException e) {
                     if (_log.shouldLog(Log.ERROR))
-                        _log.error("Crazy error handling a packet: " + packet, e);
+                        _log.error("Internal error handling " + packet, e);
                 }
                 
                 // back to the cache with thee!
@@ -779,6 +779,8 @@ class PacketHandler {
      */
     private void receiveSSU2Packet(UDPPacket packet, PeerState2 state) {
         // header and body decryption is done by PeerState2
+        // This bypasses InboundMessageStates completely.
+        // All handling of fragments and acks is done in PeerState2.
         state.receivePacket(packet);
     }
 
@@ -830,8 +832,9 @@ class PacketHandler {
             // Session Request (after Retry) or Session Confirmed
             // or retransmitted Session Request or Token Rquest
             k2 = state.getRcvHeaderEncryptKey2();
-            if (state.getState() == InboundEstablishState.InboundState.IB_STATE_RETRY_SENT) {
-                // Session Request
+            if (k2 == null) {
+                // Session Request after Retry
+                k2 = k1;
                 header = SSU2Header.trialDecryptHandshakeHeader(packet, k1, k2);
                 if (header == null ||
                     header.getType() != SSU2Util.SESSION_REQUEST_FLAG_BYTE ||
@@ -841,6 +844,13 @@ class PacketHandler {
                         _log.warn("Failed decrypt Session Request after Retry: " + header);
                     return false;
                 }
+                if (header.getSrcConnID() != state.getSendConnID()) {
+                    if (_log.shouldWarn())
+                        _log.warn("Bad Source Conn id " + header);
+                    // TODO could be a retransmitted Session Request,
+                    // tell establisher?
+                    return false;
+                }
                 type = SSU2Util.SESSION_REQUEST_FLAG_BYTE;
             } else {
                 // Session Confirmed or retransmitted Session Request or Token Request
@@ -862,13 +872,6 @@ class PacketHandler {
                     _log.warn("Bad Dest Conn id " + header);
                 return false;
             }
-            if (header.getSrcConnID() != state.getSendConnID()) {
-                if (_log.shouldWarn())
-                    _log.warn("Bad Source Conn id " + header);
-                // TODO could be a retransmitted Session Request,
-                // tell establisher?
-                return false;
-            }
         }
 
         // all good
@@ -912,15 +915,21 @@ class PacketHandler {
         // decrypt header
         byte[] k1 = state.getRcvHeaderEncryptKey1();
         byte[] k2 = state.getRcvHeaderEncryptKey2();
-        SSU2Header.Header header = SSU2Header.trialDecryptHandshakeHeader(packet, k1, k2);
-        if (header != null) {
-            // dest conn ID decrypts the same for both Session Created
-            // and Retry, so we can bail out now if it doesn't match
-            if (header.getDestConnID() != state.getRcvConnID()) {
-                if (_log.shouldWarn())
-                    _log.warn("Bad Dest Conn id " + header);
-                return false;
+        SSU2Header.Header header;
+        if (k2 != null) {
+            header = SSU2Header.trialDecryptHandshakeHeader(packet, k1, k2);
+            if (header != null) {
+                // dest conn ID decrypts the same for both Session Created
+                // and Retry, so we can bail out now if it doesn't match
+                if (header.getDestConnID() != state.getRcvConnID()) {
+                    if (_log.shouldWarn())
+                        _log.warn("Bad Dest Conn id " + header);
+                    return false;
+                }
             }
+        } else {
+            // we have only sent a Token Request
+            header = null;
         }
         int type;
         if (header == null ||
diff --git a/router/java/src/net/i2p/router/transport/udp/PeerState2.java b/router/java/src/net/i2p/router/transport/udp/PeerState2.java
index 8c7f7b35970c98fb1d114390f09c98b073c05286..c863aa7b5511d630cda8004ea06de97253e34549 100644
--- a/router/java/src/net/i2p/router/transport/udp/PeerState2.java
+++ b/router/java/src/net/i2p/router/transport/udp/PeerState2.java
@@ -185,8 +185,8 @@ public class PeerState2 extends PeerState implements SSU2Payload.PayloadCallback
         byte[] data = dpacket.getData();
         int off = dpacket.getOffset();
         int len = dpacket.getLength();
-        if (_log.shouldDebug())
-            _log.debug("Packet before header decryption:\n" + HexDump.dump(data, off, len));
+        //if (_log.shouldDebug())
+        //    _log.debug("Packet before header decryption:\n" + HexDump.dump(data, off, len));
         try {
             if (len < MIN_DATA_LEN) {
                 if (_log.shouldWarn())
@@ -199,11 +199,6 @@ public class PeerState2 extends PeerState implements SSU2Payload.PayloadCallback
                     _log.warn("bad data header on " + this);
                 return;
             }
-            if (header.getDestConnID() != _rcvConnID) {
-                if (_log.shouldWarn())
-                    _log.warn("bad Dest Conn id " + header.getDestConnID() + " on " + this);
-                return;
-            }
             if (header.getType() != DATA_FLAG_BYTE) {
                 if (_log.shouldWarn())
                     _log.warn("bad data pkt type " + (header.getType() & 0xff) + " on " + this);
@@ -217,16 +212,21 @@ public class PeerState2 extends PeerState implements SSU2Payload.PayloadCallback
                 // we didn't know the session has disconnected yet.
                 return;
             }
+            if (header.getDestConnID() != _rcvConnID) {
+                if (_log.shouldWarn())
+                    _log.warn("bad Dest Conn id " + header.getDestConnID() + " on " + this);
+                return;
+            }
             long n = header.getPacketNumber();
             SSU2Header.acceptTrialDecrypt(packet, header);
-            if (_log.shouldDebug())
-                _log.debug("Packet " + n + " after header decryption:\n" + HexDump.dump(data, off, len));
+            //if (_log.shouldDebug())
+            //    _log.debug("Packet " + n + " after header decryption:\n" + HexDump.dump(data, off, len));
             synchronized (_rcvCha) {
                 _rcvCha.setNonce(n);
                 // decrypt in-place
                 _rcvCha.decryptWithAd(header.data, data, off + SHORT_HEADER_SIZE, data, off + SHORT_HEADER_SIZE, len - SHORT_HEADER_SIZE);
-                if (_log.shouldDebug())
-                    _log.debug("Packet " + n + " after full decryption:\n" + HexDump.dump(data, off, len - MAC_LEN));
+                //if (_log.shouldDebug())
+                //    _log.debug("Packet " + n + " after full decryption:\n" + HexDump.dump(data, off, len - MAC_LEN));
                 if (_receivedMessages.set(n)) {
                     if (_log.shouldWarn())
                         _log.warn("dup pkt rcvd " + n + " on " + this);
@@ -244,8 +244,6 @@ public class PeerState2 extends PeerState implements SSU2Payload.PayloadCallback
         } catch (IndexOutOfBoundsException ioobe) {
             if (_log.shouldWarn())
                 _log.warn("Bad encrypted packet:\n" + HexDump.dump(data, off, len), ioobe);
-        } finally {
-            packet.release();
         }
     }
 
diff --git a/router/java/src/net/i2p/router/transport/udp/SSU2Header.java b/router/java/src/net/i2p/router/transport/udp/SSU2Header.java
index d89836983a3a9fb4b219cefb2a63769d7b954044..67fee090fd2ceb476d71823a802f557afc902dbd 100644
--- a/router/java/src/net/i2p/router/transport/udp/SSU2Header.java
+++ b/router/java/src/net/i2p/router/transport/udp/SSU2Header.java
@@ -190,10 +190,12 @@ final class SSU2Header {
         public String toString() {
             if (data.length >= SESSION_HEADER_SIZE) {
                 return "Handshake header destID " + getDestConnID() + " pkt num " + getPacketNumber() + " type " + getType() +
+                       " version " + getVersion() + " netID " + getNetID() +
                        " srcID " + getSrcConnID() + " token " + getToken() + " key " + Base64.encode(getEphemeralKey());
             }
             if (data.length >= LONG_HEADER_SIZE) {
                 return "Long header destID " + getDestConnID() + " pkt num " + getPacketNumber() + " type " + getType() +
+                       " version " + getVersion() + " netID " + getNetID() +
                        " srcID " + getSrcConnID() + " token " + getToken();
             }
             return "Short header destID " + getDestConnID() + " pkt num " + getPacketNumber() + " type " + getType();