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 af3707ea1cf92358be3178ec5a35453f8e7fe320..2a80676c7e2ba3c13cdc1c865ae41906610ab541 100644
--- a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
@@ -1296,6 +1296,8 @@ class EstablishmentManager {
 
     /**
      *  We are Alice, we sent a RelayRequest to Bob and got a response back.
+     *
+     *  SSU 1 only.
      */
     void receiveRelayResponse(RemoteHostId bob, UDPPacketReader reader) {
         long nonce = reader.getRelayResponseReader().readNonce();
@@ -1353,6 +1355,18 @@ class EstablishmentManager {
         notifyActivity();
     }
 
+    /**
+     *  We are Alice, we sent a RelayRequest to Bob and got a RelayResponse back.
+     *
+     *  SSU 2 only.
+     *
+     *  @param data 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
+    }
+
     /**
      *  Called from UDPReceiver.
      *  Accelerate response to RelayResponse if we haven't sent it yet.
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 e3d8147044065ef5992560409c7f546f9b0852f1..327e26e4b504e97b45d3626a173fe8409327bf1e 100644
--- a/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java
@@ -86,6 +86,8 @@ class IntroductionManager {
     private final Map<Long, PeerState> _outbound;
     /** map of relay tag to PeerState who have given us introduction tags */
     private final Map<Long, PeerState> _inbound;
+    /** map of relay nonce to alice PeerState who requested it */
+    private final Map<Long, PeerState2> _nonceToAlice;
     private final Set<InetAddress> _recentHolePunches;
     private long _lastHolePunchClean;
 
@@ -117,6 +119,7 @@ class IntroductionManager {
         _builder2 = transport.getBuilder2();
         _outbound = new ConcurrentHashMap<Long, PeerState>(MAX_OUTBOUND);
         _inbound = new ConcurrentHashMap<Long, PeerState>(MAX_INBOUND);
+        _nonceToAlice = (_builder2 != null) ? new ConcurrentHashMap<Long, PeerState2>(MAX_INBOUND) : null;
         _recentHolePunches = new HashSet<InetAddress>(16);
         ctx.statManager().createRateStat("udp.receiveRelayIntro", "How often we get a relayed request for us to talk to someone?", "udp", UDPTransport.RATES);
         ctx.statManager().createRateStat("udp.receiveRelayRequest", "How often we receive a good request to relay to someone else?", "udp", UDPTransport.RATES);
@@ -669,7 +672,6 @@ class IntroductionManager {
      *  @since 0.9.55
      */
     void receiveRelayRequest(PeerState2 alice, byte[] data) {
-        long tag = DataHelper.fromLong(data, 4, 4);
         long time = DataHelper.fromLong(data, 8, 4) * 1000;
         long now = _context.clock().now();
         long skew = time - now;
@@ -684,6 +686,8 @@ class IntroductionManager {
                 _log.warn("Bad relay req version " + ver + " from " + alice);
             return;
         }
+        long nonce = DataHelper.fromLong(data, 0, 4);
+        long tag = DataHelper.fromLong(data, 4, 4);
         PeerState charlie = _outbound.get(Long.valueOf(tag));
         RouterInfo aliceRI = null;
         int rcode;
@@ -700,7 +704,15 @@ class IntroductionManager {
                 SigningPublicKey spk = aliceRI.getIdentity().getSigningPublicKey();
                 if (SSU2Util.validateSig(_context, SSU2Util.RELAY_REQUEST_PROLOGUE,
                                          _context.routerHash(), charlie.getRemotePeer(), data, spk)) {
-                    rcode = SSU2Util.RELAY_ACCEPT;
+                    // save tag-to-alice mapping so we can forward the reply from charlie
+                    PeerState2 old = _nonceToAlice.putIfAbsent(Long.valueOf(nonce), alice);
+                    if (old != null && !old.equals(alice)) {
+                        // dup tag
+                        rcode = SSU2Util.RELAY_REJECT_BOB_UNSPEC;
+                    } else {
+                        rcode = SSU2Util.RELAY_ACCEPT;
+                    }
+                    // TODO add timer to remove from _nonceToAlice
                 } else {
                     if (_log.shouldWarn())
                         _log.warn("Signature failed relay intro\n" + aliceRI);
@@ -721,17 +733,21 @@ class IntroductionManager {
             dbsm.setEntry(aliceRI);
             dbsm.setMessageExpiration(now + 10*1000);
             _transport.send(dbsm, charlie);
-            packet = _builder2.buildRelayIntro(data, (PeerState2) charlie);
+            // put alice hash in intro data
+            byte[] idata = new byte[1 + Hash.HASH_LENGTH + data.length];
+            //idata[0] = 0; // flag
+            System.arraycopy(alice.getRemotePeer().getData(), 0, idata, 1, Hash.HASH_LENGTH);
+            System.arraycopy(data, 0, idata, 1 + Hash.HASH_LENGTH, data.length);
+            packet = _builder2.buildRelayIntro(idata, (PeerState2) charlie);
         } else {
             // send rejection to Alice
             SigningPrivateKey spk = _context.keyManager().getSigningPrivateKey();
-            long nonce = DataHelper.fromLong(data, 0, 4);
             int iplen = data[13] & 0xff;
             int testPort = (int) DataHelper.fromLong(data, 14, 2);
             byte[] testIP = new byte[iplen - 2];
             System.arraycopy(data, 16, testIP, 0, iplen - 2);
             data = SSU2Util.createRelayResponseData(_context, _context.routerHash(), rcode,
-                                                    nonce, testIP, testPort, spk);
+                                                    nonce, testIP, testPort, spk, 0);
             if (data == null) {
                 if (_log.shouldWarn())
                     _log.warn("sig fail");
@@ -828,9 +844,17 @@ class IntroductionManager {
 
         // generate our signed data
         // we sign it even if rejecting, not required though
+        long token;
+        if (rcode == SSU2Util.RELAY_ACCEPT) {
+            RemoteHostId aliceID = new RemoteHostId(testIP, testPort);
+            EstablishmentManager.Token tok = _transport.getEstablisher().getInboundToken(aliceID);
+            token = tok.token;
+        } else {
+            token = 0;
+        }
         SigningPrivateKey spk = _context.keyManager().getSigningPrivateKey();
         data = SSU2Util.createRelayResponseData(_context, bob.getRemotePeer(), rcode,
-                                                nonce, testIP, testPort, spk);
+                                                nonce, testIP, testPort, spk, token);
         if (data == null) {
             if (_log.shouldWarn())
                 _log.warn("sig fail");
@@ -864,6 +888,40 @@ class IntroductionManager {
      *  @since 0.9.55
      */
     void receiveRelayResponse(PeerState2 peer, int status, byte[] data) {
+        long nonce = DataHelper.fromLong(data, 0, 4);
+        long time = DataHelper.fromLong(data, 4, 4) * 1000;
+        long now = _context.clock().now();
+        long skew = time - now;
+        if (skew > MAX_SKEW || skew < 0 - MAX_SKEW) {
+            if (_log.shouldWarn())
+                _log.warn("Too skewed for relay resp from " + peer);
+            return;
+        }
+        int ver = data[8] & 0xff;
+        if (ver != 2) {
+            if (_log.shouldWarn())
+                _log.warn("Bad relay intro version " + ver + " from " + peer);
+            return;
+        }
+        // Look up nonce to determine if we are Alice or Bob
+        PeerState2 alice = _nonceToAlice.remove(Long.valueOf(nonce));
+        if (alice != null) {
+            // We are Bob, send to Alice
+            // We don't check the signature here
+            byte[] idata = new byte[2 + data.length];
+            //idata[0] = 0; // flag
+            idata[1] = (byte) status;
+            System.arraycopy(data, 0, idata, 2, data.length);
+            UDPPacket packet = _builder2.buildRelayResponse(idata, alice);
+            if (_log.shouldDebug())
+                _log.debug("Send relay response " + " nonce " + nonce + " to " + alice);
+            _transport.send(packet);
+        } else {
+            // We are Alice, give to EstablishmentManager to check sig and process
+            if (_log.shouldDebug())
+                _log.debug("Got relay response " + " nonce " + nonce + " from " + peer);
+            _transport.getEstablisher().receiveRelayResponse(peer, nonce, status, data);
+        }
     }
 
     /**
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 d70f8ff9a6cd4ff56e70f34668fe492df486cbba..158b6d79f2b2eec6a3de44b9c89bc74b25408375 100644
--- a/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java
+++ b/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java
@@ -723,6 +723,7 @@ class PacketBuilder2 {
      *  From Alice to Bob.
      *  In-session.
      *
+     *  @param signedData flag + signed data
      *  @return null on failure
      */
     UDPPacket buildRelayRequest(byte[] signedData, PeerState2 bob) {
@@ -737,6 +738,7 @@ class PacketBuilder2 {
      *  From Bob to Charlie.
      *  In-session.
      *
+     *  @param signedData flag + alice hash + signed data
      *  @return null on failure
      */
     UDPPacket buildRelayIntro(byte[] signedData, PeerState2 charlie) {
@@ -750,6 +752,7 @@ class PacketBuilder2 {
      *  From Charlie to Bob or Bob to Alice.
      *  In-session.
      *
+     *  @param signedData flag + response code + signed data + optional token
      *  @param state Alice or Bob
      *  @return null on failure
      */
diff --git a/router/java/src/net/i2p/router/transport/udp/SSU2Util.java b/router/java/src/net/i2p/router/transport/udp/SSU2Util.java
index 4f4878d55f3e3be080e70999931b7b7a6513bcf1..d98f6b8a30acfaf221478151cb1ebe596956b206 100644
--- a/router/java/src/net/i2p/router/transport/udp/SSU2Util.java
+++ b/router/java/src/net/i2p/router/transport/udp/SSU2Util.java
@@ -227,22 +227,25 @@ final class SSU2Util {
     public static byte[] createRelayRequestData(I2PAppContext ctx, Hash h, Hash h2,
                                                 long nonce, long tag, byte[] ip, int port,
                                                 SigningPrivateKey spk) {
-        int datalen = 17 + ip.length;
-        byte[] data = new byte[datalen + spk.getType().getSigLen()];
-        //data[0] = 0;  // flag
-        DataHelper.toLong(data, 1, 4, nonce);
-        DataHelper.toLong(data, 5, 4, tag);
-        DataHelper.toLong(data, 9, 4, ctx.clock().now() / 1000);
-        data[13] = 2;  // version
-        data[14] = (byte) (ip.length + 2);
-        DataHelper.toLong(data, 15, 2, port);
-        System.arraycopy(ip, 0, data, 17, ip.length);
+        int datalen = 16 + ip.length;
+        byte[] data = new byte[datalen];
+        DataHelper.toLong(data, 0, 4, nonce);
+        DataHelper.toLong(data, 4, 4, tag);
+        DataHelper.toLong(data, 8, 4, ctx.clock().now() / 1000);
+        data[12] = 2;  // version
+        data[13] = (byte) (ip.length + 2);
+        DataHelper.toLong(data, 14, 2, port);
+        System.arraycopy(ip, 0, data, 16, ip.length);
         Signature sig = sign(ctx, RELAY_REQUEST_PROLOGUE, h, h2, data, datalen, spk);
         if (sig == null)
             return null;
+        int len = 1 + datalen + spk.getType().getSigLen();
+        byte[] rv = new byte[len];
+        //rv[0] = 0;  // flag
+        System.arraycopy(data, 0, rv, 1, data.length);
         byte[] s = sig.getData();
-        System.arraycopy(s, 0, data, datalen, s.length);
-        return data;
+        System.arraycopy(s, 0, rv, 1 + datalen, s.length);
+        return rv;
     }
 
     /**
@@ -250,28 +253,36 @@ final class SSU2Util {
      *
      *  @param h Bob hash to be included in sig, not included in data
      *  @param ip non-null
+     *  @param token if nonzero, append it
      *  @return null on failure
      *  @since 0.9.55
      */
     public static byte[] createRelayResponseData(I2PAppContext ctx, Hash h, int code,
                                                  long nonce, byte[] ip, int port,
-                                                 SigningPrivateKey spk) {
-        int datalen = 14 + ip.length;
-        byte[] data = new byte[datalen + spk.getType().getSigLen()];
-        //data[0] = 0;  // flag
-        data[1] = (byte) code;
-        DataHelper.toLong(data, 2, 4, nonce);
-        DataHelper.toLong(data, 6, 4, ctx.clock().now() / 1000);
-        data[10] = 2;  // version
-        data[11] = (byte) (ip.length + 2);
-        DataHelper.toLong(data, 12, 2, port);
-        System.arraycopy(ip, 0, data, 14, ip.length);
+                                                 SigningPrivateKey spk, long token) {
+        int datalen = 12 + ip.length;
+        byte[] data = new byte[datalen];
+        DataHelper.toLong(data, 0, 4, nonce);
+        DataHelper.toLong(data, 4, 4, ctx.clock().now() / 1000);
+        data[8] = 2;  // version
+        data[9] = (byte) (ip.length + 2);
+        DataHelper.toLong(data, 10, 2, port);
+        System.arraycopy(ip, 0, data, 12, ip.length);
         Signature sig = sign(ctx, RELAY_RESPONSE_PROLOGUE, h, null, data, datalen, spk);
         if (sig == null)
             return null;
+        int len = 2 + datalen + spk.getType().getSigLen();
+        if (token != 0)
+            len += 8;
+        byte[] rv = new byte[len];
+        //rv[0] = 0;  // flag
+        rv[1] = (byte) code;
+        System.arraycopy(data, 0, rv, 2, data.length);
         byte[] s = sig.getData();
-        System.arraycopy(s, 0, data, datalen, s.length);
-        return data;
+        System.arraycopy(s, 0, rv, 2 + datalen, s.length);
+        if (token != 0)
+            DataHelper.toLong8(rv, 2 + datalen + s.length, token);
+        return rv;
     }
 
     /**