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 9626f5671de4b6ed481d240dbce0efe341ea0af2..24f6a95e9f787bfbe5a958f9ad7d40a64ed10268 100644
--- a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
@@ -20,6 +20,7 @@ import net.i2p.data.router.RouterAddress;
 import net.i2p.data.router.RouterIdentity;
 import net.i2p.data.router.RouterInfo;
 import net.i2p.data.SessionKey;
+import net.i2p.data.SigningPublicKey;
 import net.i2p.data.i2np.DatabaseLookupMessage;
 import net.i2p.data.i2np.DatabaseStoreMessage;
 import net.i2p.data.i2np.DeliveryStatusMessage;
@@ -1514,14 +1515,130 @@ class EstablishmentManager {
 
     /**
      *  We are Alice, we sent a RelayRequest to Bob and got a RelayResponse back.
+     *  Time and version already checked by caller.
      *
      *  SSU 2 only.
      *
-     *  @param data including token if code == 0
+     *  @param data including nonce, including token if code == 0
      *  @since 0.9.55
      */
     void receiveRelayResponse(PeerState2 bob, long nonce, int code, byte[] data) {
-        // lookup nonce, determine who signed, validate sig, send SessionRequest if code == 0
+        // don't remove unless accepted or rejected by charlie
+        OutboundEstablishState charlie;
+        Long lnonce = Long.valueOf(nonce);
+        if (code > 0 && code < 64)
+            charlie = _liveIntroductions.get(lnonce);
+        else
+            charlie = _liveIntroductions.remove(lnonce);
+        if (charlie == null) {
+            if (_log.shouldDebug())
+                _log.debug("Dup or unknown RelayResponse: " + nonce);
+            return; // already established
+        }
+        long token;
+        if (code == 0) {
+            token = DataHelper.fromLong8(data, data.length - 8);
+            data = Arrays.copyOfRange(data, 0, data.length - 8);
+        } else {
+            token = 0;
+        }
+        Hash bobHash = bob.getRemotePeer();
+        Hash charlieHash = charlie.getRemoteHostId().getPeerHash();
+        RouterInfo bobRI = _context.netDb().lookupRouterInfoLocally(bobHash);
+        RouterInfo charlieRI = _context.netDb().lookupRouterInfoLocally(charlieHash);
+        Hash signer;
+        if (code > 0 && code < 64)
+            signer = bobHash;
+        else
+            signer = charlieHash;
+        RouterInfo signerRI = _context.netDb().lookupRouterInfoLocally(signer);
+        if (signerRI != null) {
+            // validate signed data
+            SigningPublicKey spk = signerRI.getIdentity().getSigningPublicKey();
+            if (SSU2Util.validateSig(_context, SSU2Util.RELAY_REQUEST_PROLOGUE,
+                                     bobHash, null, data, spk)) {
+            } else {
+                if (_log.shouldWarn())
+                    _log.warn("Signature failed relay response\n" + signerRI);
+            }
+        } else {
+            if (_log.shouldWarn())
+                _log.warn("Signer RI not found " + signer);
+        }
+        if (code == 0) {
+            int iplen = data[9] & 0xff;
+            if (iplen != 6 && iplen != 18) {
+                if (_log.shouldWarn())
+                    _log.warn("Bad IP length " + iplen + " from " + charlie);
+                charlie.fail();
+                return;
+            }
+            boolean isIPv6 = iplen == 18;
+            int port = (int) DataHelper.fromLong(data, 10, 2);
+            byte[] ip = new byte[iplen - 2];
+            System.arraycopy(data, 12, ip, 0, iplen - 2);
+            // validate
+            if (!TransportUtil.isValidPort(port) ||
+                !_transport.isValid(ip) ||
+                _transport.isTooClose(ip) ||
+                _context.blocklist().isBlocklisted(ip)) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Bad relay resp from " + charlie + " for " + Addresses.toString(ip, port));
+                _context.statManager().addRateData("udp.relayBadIP", 1);
+                charlie.fail();
+                return;
+            }
+            InetAddress charlieIP;
+            try {
+                charlieIP = InetAddress.getByAddress(ip);
+            } catch (UnknownHostException uhe) {
+                charlie.fail();
+                return;
+            }
+            if (_log.shouldDebug())
+                _log.debug("Received RelayResponse from " + charlie + " - they are on " +
+                           Addresses.toString(ip, port));
+            if (charlieRI == null) {
+                if (_log.shouldWarn())
+                    _log.warn("Charlie RI not found " + charlie);
+                // maybe it will show up later
+                return;
+            }
+            synchronized (charlie) {
+                RemoteHostId oldId = charlie.getRemoteHostId();
+                ((OutboundEstablishState2) charlie).introduced(ip, port, token);
+                RemoteHostId newId = charlie.getRemoteHostId();
+                addOutboundToken(newId, token, _context.clock().now() + 10*1000);
+                // Swap out the RemoteHostId the state is indexed under.
+                // It was a Hash, change it to a IP/port.
+                // Remove the entry in the byClaimedAddress map as it's now in main map.
+                // Add an entry in the byHash map so additional OB pkts can find it.
+                _outboundByHash.put(charlieHash, charlie);
+                RemoteHostId claimed = charlie.getClaimedAddress();
+                if (!oldId.equals(newId)) {
+                    _outboundStates.remove(oldId);
+                    _outboundStates.put(newId, charlie);
+                    if (_log.shouldLog(Log.INFO))
+                        _log.info("RR replaced " + oldId + " with " + newId + ", claimed address was " + claimed);
+                }
+                //
+                if (claimed != null)
+                    _outboundByClaimedAddress.remove(oldId, charlie);  // only if == state
+            }
+            notifyActivity();
+        } else if (code >= 64) {
+            // that's it
+            if (_log.shouldDebug())
+                _log.debug("Received RelayResponse rejection " + code + " from charlie " + charlie);
+            charlie.fail();
+            _liveIntroductions.remove(lnonce);
+        } else {
+            // don't give up, maybe more bobs out there
+            // TODO keep track
+            if (_log.shouldDebug())
+                _log.debug("Received RelayResponse rejection " + code + " from bob " + bob);
+            notifyActivity();
+        }
     }
 
     /**
diff --git a/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java b/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java
index 7f47889134e677cc26f3ac9187d6793b8f755660..960fb5ba711ca9c1f8896584f50d336654bae020 100644
--- a/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java
@@ -1010,7 +1010,27 @@ class IntroductionManager {
         PeerState2 alice = _nonceToAlice.remove(Long.valueOf(nonce));
         if (alice != null) {
             // We are Bob, send to Alice
-            // We don't check the signature here
+            // Debug, check the signature, but send it along even if failed
+            if (true) {
+                RouterInfo charlie = _context.netDb().lookupRouterInfoLocally(peer.getRemotePeer());
+                if (charlie != null) {
+                    byte[] signedData;
+                    if (status == 0)
+                        signedData = Arrays.copyOfRange(data, 0, data.length - 8);  // token
+                    else
+                        signedData = data;
+                    SigningPublicKey spk = charlie.getIdentity().getSigningPublicKey();
+                    if (SSU2Util.validateSig(_context, SSU2Util.RELAY_REQUEST_PROLOGUE,
+                                             _context.routerHash(), null, data, spk)) {
+                    } else {
+                        if (_log.shouldWarn())
+                            _log.warn("Signature failed relay response\n" + charlie);
+                    }
+                } else {
+                    if (_log.shouldWarn())
+                        _log.warn("Signer RI not found " + peer);
+                }
+            }
             byte[] idata = new byte[2 + data.length];
             //idata[0] = 0; // flag
             idata[1] = (byte) status;
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 df2068d663c13d9272f57997fa961bda6276e32c..7be86d014e024f2c0a441871e755b6b0c84eeb4e 100644
--- a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
@@ -110,7 +110,7 @@ class OutboundEstablishState {
          */
         OB_STATE_RETRY_RECEIVED,
         /**
-         * SSU2: We have sent a second token request with a new token
+         * SSU2: We have sent a session request after receiving a retry
          * @since 0.9.54
          */
         OB_STATE_REQUEST_SENT_NEW_TOKEN
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 b659d6c7e1bb6b1e05ae11753becf427864645ec..b35eeda95f38e8371d96cfd4b34c94ac6af23820 100644
--- a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java
+++ b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState2.java
@@ -111,12 +111,18 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
             }
         }
         _mtu = mtu;
+        _routerAddress = ra;
         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;
+            // we will get a token in the relay response or hole punch
         } else {
-            _currentState = OutboundState.OB_STATE_UNKNOWN;
+            _token = _transport.getEstablisher().getOutboundToken(_remoteHostId);
+            if (_token != 0) {
+                _currentState = OutboundState.OB_STATE_UNKNOWN;
+                createNewState(ra);
+            } else {
+                _currentState = OutboundState.OB_STATE_NEEDS_TOKEN;
+            }
         }
 
         _sendConnID = ctx.random().nextLong();
@@ -127,13 +133,6 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
         } while (_sendConnID == rcid);
         _rcvConnID = rcid;
 
-        _token = _transport.getEstablisher().getOutboundToken(_remoteHostId);
-        _routerAddress = ra;
-        if (_token != 0)
-            createNewState(ra);
-        else
-            _currentState = OutboundState.OB_STATE_NEEDS_TOKEN;
-
         byte[] ik = introKey.getData();
         _sendHeaderEncryptKey1 = ik;
         _rcvHeaderEncryptKey1 = ik;
@@ -144,6 +143,19 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
             _log.debug("New " + this);
     }
 
+    /**
+     *  After introduction
+     *
+     *  @since 0.9.55
+     */
+    public synchronized void introduced(byte[] ip, int port, long token) {
+        if (_currentState != OutboundState.OB_STATE_PENDING_INTRO)
+            return;
+        introduced(ip, port);
+        _token = token;
+        createNewState(_routerAddress);
+    }
+
     private void createNewState(RouterAddress addr) {
         String ss = addr.getOption("s");
         if (ss == null)
@@ -163,18 +175,6 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
                                                   _transport.getSSU2StaticPubKey(), 0);
     }
     
-    public synchronized void restart(long token) {
-        _token = token;
-        HandshakeState old = _handshakeState;
-        if (old != null) {
-            // TODO pass the old keys over to createNewState()
-            old.destroy();
-        }
-        createNewState(_routerAddress);
-        //_rcvHeaderEncryptKey2 will be set after the Session Request message is created
-        _rcvHeaderEncryptKey2 = null;
-    }
-
     private void processPayload(byte[] payload, int offset, int length, boolean isHandshake) throws GeneralSecurityException {
         try {
             int blocks = SSU2Payload.processPayload(_context, this, payload, offset, length, isHandshake);