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 3d746f152418e374b1e8d9469688df28eec3a539..94fdea2d48aeb1be1b0838bd5db2e38ce04350cc 100644
--- a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java
+++ b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java
@@ -1,6 +1,7 @@
 package net.i2p.router.transport.udp;
 
 import java.io.ByteArrayInputStream;
+import java.net.InetSocketAddress;
 import java.io.IOException;
 import java.util.Queue;
 import java.util.concurrent.LinkedBlockingQueue;
@@ -82,7 +83,23 @@ class InboundEstablishState {
         /** we are explicitly failing it */
         IB_STATE_FAILED,
         /** Successful completion, PeerState created and added to transport */
-        IB_STATE_COMPLETE
+        IB_STATE_COMPLETE,
+
+        /**
+         * SSU2: We have received a token request
+         * @since 0.9.54
+         */
+        IB_STATE_TOKEN_REQUEST_RECEIVED,
+        /**
+         * SSU2: We have received a request but the token is bad
+         * @since 0.9.54
+         */
+        IB_STATE_REQUEST_BAD_TOKEN_RECEIVED,
+        /**
+         * SSU2: We have sent a retry
+         * @since 0.9.54
+         */
+        IB_STATE_RETRY_SENT,
     }
     
     /** basic delay before backoff
@@ -112,6 +129,24 @@ class InboundEstablishState {
         _queuedMessages = new LinkedBlockingQueue<OutNetMessage>();
         receiveSessionRequest(req);
     }
+
+    /**
+     *  For SSU2
+     *
+     *  @since 0.9.54
+     */
+    protected InboundEstablishState(RouterContext ctx, InetSocketAddress addr) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(getClass());
+        _aliceIP = addr.getAddress().getAddress();
+        _alicePort = addr.getPort();
+        _remoteHostId = new RemoteHostId(_aliceIP, _alicePort);
+        _bobPort = 0;
+        _currentState = InboundState.IB_STATE_UNKNOWN;
+        _establishBegin = ctx.clock().now();
+        _keyBuilder = null;
+        _queuedMessages = new LinkedBlockingQueue<OutNetMessage>();
+    }
     
     /**
      * @since 0.9.54
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 2f706d842f4cc05639c0c24c1c18b3c6b15e2958..7324ffbeea70e00c6edf88b9963e1b6699995204 100644
--- a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
@@ -92,7 +92,23 @@ class OutboundEstablishState {
         /** RelayResponse received */
         OB_STATE_INTRODUCED,
         /** SessionConfirmed failed validation */
-        OB_STATE_VALIDATION_FAILED
+        OB_STATE_VALIDATION_FAILED,
+
+        /**
+         * SSU2: We have sent a token request
+         * @since 0.9.54
+         */
+        OB_STATE_TOKEN_REQUEST_SENT,
+        /**
+         * SSU2: We have received a retry
+         * @since 0.9.54
+         */
+        OB_STATE_RETRY_RECEIVED,
+        /**
+         * SSU2: We have sent a second token request with a new token
+         * @since 0.9.54
+         */
+        OB_STATE_REQUEST_SENT_NEW_TOKEN
     }
     
     /** basic delay before backoff
@@ -151,6 +167,45 @@ class OutboundEstablishState {
         }
     }
     
+    /**
+     *  For SSU2
+     *
+     *  @since 0.9.54
+     */
+    public OutboundEstablishState(RouterContext ctx, RemoteHostId claimedAddress,
+                                  RemoteHostId remoteHostId,
+                                  RouterIdentity remotePeer,
+                                  boolean needIntroduction,
+                                  SessionKey introKey, UDPAddress addr) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(getClass());
+        if (claimedAddress != null) {
+            _bobIP = claimedAddress.getIP();
+            _bobPort = claimedAddress.getPort();
+        } else {
+            //_bobIP = null;
+            _bobPort = -1;
+        }
+        _claimedAddress = claimedAddress;
+        _remoteHostId = remoteHostId;
+        _allowExtendedOptions = false;
+        _needIntroduction = needIntroduction;
+        _remotePeer = remotePeer;
+        _introKey = introKey;
+        _queuedMessages = new LinkedBlockingQueue<OutNetMessage>();
+        _establishBegin = ctx.clock().now();
+        _remoteAddress = addr;
+        _introductionNonce = -1;
+        _keyFactory = null;
+        if (addr.getIntroducerCount() > 0) {
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("new outbound establish to " + remotePeer.calculateHash() + ", with address: " + addr);
+            _currentState = OutboundState.OB_STATE_PENDING_INTRO;
+        } else {
+            _currentState = OutboundState.OB_STATE_UNKNOWN;
+        }
+    }
+    
     /**
      * @since 0.9.54
      */
diff --git a/router/java/src/net/i2p/router/transport/udp/PeerState.java b/router/java/src/net/i2p/router/transport/udp/PeerState.java
index c3cc25b732f7ca92196fa35d476a3cb0c4d3ef3f..08f1d4a73591792b381647dd3bfb94ea555ae662 100644
--- a/router/java/src/net/i2p/router/transport/udp/PeerState.java
+++ b/router/java/src/net/i2p/router/transport/udp/PeerState.java
@@ -1,6 +1,7 @@
 package net.i2p.router.transport.udp;
 
 import java.net.InetAddress;
+import java.net.InetSocketAddress;
 import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -378,6 +379,61 @@ public class PeerState {
         _remoteHostId = new RemoteHostId(remoteIP, remotePort);
         _bwEstimator = new SimpleBandwidthEstimator(ctx, this);
     }
+
+    /**
+     *  For SSU2
+     *
+     *  @since 0.9.54
+     */
+    protected PeerState(RouterContext ctx, UDPTransport transport,
+                        InetSocketAddress addr, Hash remotePeer, boolean isInbound, int rtt) {
+        _context = ctx;
+        _log = ctx.logManager().getLog(getClass());
+        _transport = transport;
+        long now = ctx.clock().now();
+        _keyEstablishedTime = now;
+        _lastSendTime = now;
+        _lastReceiveTime = now;
+        _currentACKs = new ConcurrentHashSet<Long>();
+        _currentACKsResend = new LinkedBlockingQueue<ResendACK>();
+        _slowStartThreshold = MAX_SEND_WINDOW_BYTES/2;
+        _receivePeriodBegin = now;
+        _remoteIP = addr.getAddress().getAddress();
+        _remotePort = addr.getPort();
+        if (_remoteIP.length == 4) {
+            _mtu = DEFAULT_MTU;
+            _mtuReceive = DEFAULT_MTU;
+            _largeMTU = transport.getMTU(false);
+        } else {
+            _mtu = MIN_IPV6_MTU;
+            _mtuReceive = MIN_IPV6_MTU;
+            _largeMTU = transport.getMTU(true);
+        }
+        // RFC 5681 sec. 3.1
+        if (_mtu > 1095)
+            _sendWindowBytes = 3 * _mtu;
+        else
+            _sendWindowBytes = 4 * _mtu;
+        _sendWindowBytesRemaining = _sendWindowBytes;
+
+        _lastACKSend = -1;
+
+        _rto = INIT_RTO;
+        _rtt = INIT_RTT;
+        if (rtt > 0)
+            recalculateTimeouts(rtt);
+        else
+            _rttDeviation = _rtt;
+
+        _inboundMessages = new HashMap<Long, InboundMessageState>(8);
+        _outboundMessages = new CachedIteratorCollection<OutboundMessageState>();
+        _outboundQueue = new PriBlockingQueue<OutboundMessageState>(ctx, "UDP-PeerState", 32);
+        _ackedMessages = new AckedMessages();
+        _remotePeer = remotePeer;
+        _isInbound = isInbound;
+        _remoteHostId = new RemoteHostId(_remoteIP, _remotePort);
+        _bwEstimator = new SimpleBandwidthEstimator(ctx, this);
+    }
     
     /**
      * @since 0.9.54