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 135041171704af56f607319198473db7d7b154af..be750dfdb797c7b2c241dcfe8dfb89fd48544bb2 100644
--- a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
+++ b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
@@ -817,8 +817,8 @@ class PacketHandler {
                 header.getType() != SSU2Util.SESSION_REQUEST_FLAG_BYTE ||
                 header.getVersion() != 2 ||
                 header.getNetID() != _networkID) {
-                if (_log.shouldInfo())
-                    _log.info("Does not decrypt as Session Request, attempt to decrypt as Token Request/Peer Test: " + header);
+                if (header != null && _log.shouldInfo())
+                    _log.info("Does not decrypt as Session Request, attempt to decrypt as Token Request/Peer Test: " + header + " from " + from);
                 // The first 32 bytes were fine, but it corrupted the next 32 bytes
                 // TODO make this more efficient, just take the first 32 bytes
                 header = SSU2Header.trialDecryptLongHeader(packet, k1, k2);
@@ -853,14 +853,14 @@ class PacketHandler {
                 }
                 if (header.getSrcConnID() != state.getSendConnID()) {
                     if (_log.shouldWarn())
-                        _log.warn("Bad Source Conn id " + header);
+                        _log.warn("Bad Source Conn id " + header + " on " + state);
                     // TODO could be a retransmitted Session Request,
                     // tell establisher?
                     return false;
                 }
                 if (header.getDestConnID() != state.getRcvConnID()) {
                     if (_log.shouldWarn())
-                        _log.warn("Bad Dest Conn id " + header);
+                        _log.warn("Bad Dest Conn id " + header + " on " + state);
                     return false;
                 }
                 type = SSU2Util.SESSION_REQUEST_FLAG_BYTE;
@@ -870,12 +870,12 @@ class PacketHandler {
                 if (header == null) {
                     // too short
                     if (_log.shouldWarn())
-                        _log.warn("Failed decrypt Session Confirmed");
+                        _log.warn("Failed decrypt Session Confirmed on " + state);
                     return false;
                 }
                 if (header.getDestConnID() != state.getRcvConnID()) {
                     if (_log.shouldWarn())
-                        _log.warn("Bad Dest Conn id " + header);
+                        _log.warn("Bad Dest Conn id " + header + " on " + state);
                     return false;
                 }
                 if (header.getPacketNumber() != 0 ||
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 7397c4d512e3746ae70e05cf7c7bcd24217aa92f..a91d8bcd69f13a065f7edb58b753b3c81da33e4c 100644
--- a/router/java/src/net/i2p/router/transport/udp/PeerState.java
+++ b/router/java/src/net/i2p/router/transport/udp/PeerState.java
@@ -2386,11 +2386,11 @@ public class PeerState {
         else
             buf.append(_isInbound? " IB " : " OB ");
         long now = _context.clock().now();
-        buf.append(" recvAge: ").append(now-_lastReceiveTime);
-        buf.append(" sendAge: ").append(now-_lastSendFullyTime);
-        buf.append(" sendAttemptAge: ").append(now-_lastSendTime);
-        buf.append(" sendACKAge: ").append(now-_lastACKSend);
-        buf.append(" lifetime: ").append(now-_keyEstablishedTime);
+        buf.append(" recvAge: ").append(DataHelper.formatDuration(now - _lastReceiveTime));
+        buf.append(" sendAge: ").append(DataHelper.formatDuration(now - _lastSendFullyTime));
+        buf.append(" sendAttemptAge: ").append(DataHelper.formatDuration(now - _lastSendTime));
+        buf.append(" sendACKAge: ").append(DataHelper.formatDuration(now - _lastACKSend));
+        buf.append(" lifetime: ").append(DataHelper.formatDuration(now - _keyEstablishedTime));
         buf.append(" RTT: ").append(_rtt);
         buf.append(" RTO: ").append(_rto);
         buf.append(" cwin: ").append(_sendWindowBytes);
diff --git a/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java b/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
index ef4b96cad8fe77a6b7ccc6e27341e9d665c05084..27d21c4955687143775a857c92341d6060469b71 100644
--- a/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
@@ -274,31 +274,66 @@ class PeerTestManager {
             return true;
     }
     
-    /** call from a synchronized method */
+    /**
+     * SSU 1 or 2. We are Alice.
+     * Call from a synchronized method.
+     */
     private void sendTestToBob() {
         PeerTestState test = _currentTest;
         if (!expired()) {
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug("Sending test to Bob: " + test);
+            UDPPacket packet;
+            if (test.getBob().getVersion() == 1) {
+                packet = _packetBuilder.buildPeerTestFromAlice(test.getBobIP(), test.getBobPort(),
+                                                               test.getBobCipherKey(), test.getBobMACKey(),
+                                                               test.getNonce(), _transport.getIntroKey());
+            } else {
+                SigningPrivateKey spk = _context.keyManager().getSigningPrivateKey();
+                PeerState2 bob = (PeerState2) test.getBob();
+                // TODO only create this once
+                byte[] data = SSU2Util.createPeerTestData(_context, bob.getRemotePeer(), _context.routerHash(),
+                                                          ALICE, test.getNonce(), null, 0, spk);
+                if (data == null) {
+                    if (_log.shouldWarn())
+                        _log.warn("sig fail");
+                     testComplete();
+                     return;
+                }
+                packet = _packetBuilder2.buildPeerTestFromAlice(data, bob);
+            }
+            _transport.send(packet);
             test.setLastSendTime(_context.clock().now());
-            _transport.send(_packetBuilder.buildPeerTestFromAlice(test.getBobIP(), test.getBobPort(),
-                                                                  test.getBobCipherKey(), test.getBobMACKey(),
-                                                                  test.getNonce(), _transport.getIntroKey()));
         } else {
             _currentTest = null;
         }
     }
 
-    /** call from a synchronized method */
+    /**
+     * SSU 1 or 2. We are Alice.
+     * Call from a synchronized method.
+     */
     private void sendTestToCharlie() {
         PeerTestState test = _currentTest;
         if (!expired()) {
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug("Sending test to Charlie: " + test);
             test.setLastSendTime(_context.clock().now());
-            _transport.send(_packetBuilder.buildPeerTestFromAlice(test.getCharlieIP(), test.getCharliePort(),
-                                                                  test.getCharlieIntroKey(), 
-                                                                  test.getNonce(), _transport.getIntroKey()));
+            UDPPacket packet;
+            if (test.getBob().getVersion() == 1) {
+                packet = _packetBuilder.buildPeerTestFromAlice(test.getCharlieIP(), test.getCharliePort(),
+                                                               test.getCharlieIntroKey(), 
+                                                               test.getNonce(), _transport.getIntroKey());
+            } else {
+                long nonce = test.getNonce();
+                long sendId = (nonce << 32) | nonce;
+                long rcvId = ~sendId;
+                byte[] data = null; // TODO
+                packet = _packetBuilder2.buildPeerTestFromAlice(test.getCharlieIP(), test.getCharliePort(),
+                                                                test.getCharlieIntroKey(),
+                                                                sendId, rcvId, data);
+            }
+            _transport.send(packet);
         } else {
             _currentTest = null;
         }
@@ -319,6 +354,8 @@ class PeerTestManager {
      * Receive a PeerTest message which contains the correct nonce for our current 
      * test. We are Alice.
      *
+     * SSU 1 only.
+     *
      * @param fromPeer non-null if an associated session was found, otherwise null
      * @param inSession true if authenticated in-session
      */
@@ -727,14 +764,19 @@ class PeerTestManager {
         long nonce = DataHelper.fromLong(data, 2, 4);
         long time = DataHelper.fromLong(data, 6, 4) * 1000;
         int iplen = data[10] & 0xff;
-        if (iplen != 4 && iplen != 16) {
+        if (iplen != 0 && iplen != 4 && iplen != 16) {
             if (_log.shouldLog(Log.WARN))
                 _log.warn("Bad IP length " + iplen);
             return;
         }
         boolean isIPv6 = iplen == 16;
-        byte[] testIP = new byte[iplen];
-        System.arraycopy(data, 11, testIP, 0, iplen);
+        byte[] testIP;
+        if (iplen != 0) {
+            testIP = new byte[iplen];
+            System.arraycopy(data, 11, testIP, 0, iplen);
+        } else {
+            testIP = null;
+        }
         int testPort = (int) DataHelper.fromLong(data, 11 + iplen, 2);
         Long lNonce = Long.valueOf(nonce);
         PeerTestState state;
@@ -856,17 +898,14 @@ class PeerTestManager {
                     _transport.send(packet);
                     return;
                 }
-                InetAddress aliceIP;
-                try {
-                    aliceIP = InetAddress.getByAddress(testIP);
-                } catch (UnknownHostException uhe) {
-                    return;
-                }
+                InetAddress aliceIP = fromPeer.getRemoteIPAddress();
+                int alicePort = fromPeer.getRemotePort();
                 state = new PeerTestState(BOB, null, isIPv6, nonce, now);
                 state.setAlice(fromPeer);
-                state.setAlice(aliceIP, testPort, alice);
+                state.setAlice(aliceIP, alicePort, alice);
                 state.setCharlie(charlie.getRemoteIPAddress(), charlie.getRemotePort(), charlie.getRemotePeer());
                 state.setReceiveAliceTime(now);
+                state.setLastSendTime(now);
                 _activeTests.put(lNonce, state);
                 // send alice RI to charlie
                 DatabaseStoreMessage dbsm = new DatabaseStoreMessage(_context);
@@ -874,6 +913,7 @@ class PeerTestManager {
                 dbsm.setMessageExpiration(now + 10*1000);
                 _transport.send(dbsm, charlie);
                 // forward to charlie, don't bother to validate signed data
+                // FIXME this will probably get there before the RI
                 UDPPacket packet = _packetBuilder2.buildPeerTestToCharlie(alice, data, (PeerState2) charlie);
                 _transport.send(packet);
                 break;
@@ -923,17 +963,38 @@ class PeerTestManager {
                     state.setAlice(aliceIP, testPort, h);
                     state.setAliceIntroKey(aliceIntroKey);
                     state.setReceiveBobTime(now);
+                    state.setLastSendTime(now);
                     _activeTests.put(lNonce, state);
                 }
-                // TODO generate our signed data
+                // generate our signed data
+                // we sign it even if rejecting, not required though
+                SigningPrivateKey spk = _context.keyManager().getSigningPrivateKey();
+                data = SSU2Util.createPeerTestData(_context, fromPeer.getRemotePeer(), h,
+                                                   CHARLIE, nonce, testIP, testPort, spk);
+                if (data == null) {
+                    if (_log.shouldWarn())
+                        _log.warn("sig fail");
+                     if (rcode == SSU2Util.TEST_ACCEPT)
+                         _activeTests.remove(lNonce);
+                     return;
+                }
                 UDPPacket packet = _packetBuilder2.buildPeerTestToBob(rcode, data, fromPeer);
                 _transport.send(packet);
-                // delay, then send msg 5
+                // send msg 5
+                long rcvId = (nonce << 32) | nonce;
+                long sendId = ~rcvId;
+                // send the same data we sent to Bob
+                packet = _packetBuilder2.buildPeerTestToAlice(aliceIP, testPort,
+                                                              aliceIntroKey, true,
+                                                              sendId, rcvId, data);
+                _transport.send(packet);
                 break;
             }
 
             // charlie to bob, in-session
             case 3: {
+                state.setReceiveCharlieTime(now);
+                state.setLastSendTime(now);
                 PeerState2 alice = state.getAlice();
                 Hash charlie = fromPeer.getRemotePeer();
                 RouterInfo charlieRI = _context.netDb().lookupRouterInfoLocally(charlie);
@@ -949,6 +1010,7 @@ class PeerTestManager {
                         _log.warn("No charlie RI");
                 }
                 // forward to alice, don't bother to validate signed data
+                // FIXME this will probably get there before the RI
                 UDPPacket packet = _packetBuilder2.buildPeerTestToAlice(status, charlie, data, alice);
                 _transport.send(packet);
                 // we are done
@@ -961,7 +1023,7 @@ class PeerTestManager {
                 PeerTestState test = _currentTest;
                 if (test == null || test.getNonce() != nonce) {
                     if (_log.shouldWarn())
-                        _log.warn("Test nonce mismatch?");
+                        _log.warn("Test nonce mismatch? " + nonce);
                     return;
                 }
                 InetAddress charlieIP;
@@ -970,6 +1032,8 @@ class PeerTestManager {
                 } catch (UnknownHostException uhe) {
                     return;
                 }
+                test.setReceiveBobTime(now);
+                test.setLastSendTime(now);
                 boolean fail = false;
                 RouterInfo charlieRI = null;
                 SessionKey charlieIntroKey = null;
@@ -1003,21 +1067,90 @@ class PeerTestManager {
                     return;
                 }
                 state.setCharlie(charlieIP, testPort, h);
+                state.setCharlieIntroKey(charlieIntroKey);
                 // delay, await msg 5
                 break;
             }
 
             // charlie to alice, out-of-session
-            case 5:
+            case 5: {
+                PeerTestState test = _currentTest;
+                if (test == null || test.getNonce() != nonce) {
+                    if (_log.shouldWarn())
+                        _log.warn("Test nonce mismatch? " + nonce);
+                    return;
+                }
+                test.setReceiveCharlieTime(now);
+                test.setAlicePortFromCharlie(testPort);
+                try {
+                    InetAddress addr = InetAddress.getByAddress(testIP);
+                    test.setAliceIPFromCharlie(addr);
+                    if (test.getReceiveBobTime() > 0)
+                        testComplete();
+                } catch (UnknownHostException uhe) {
+                    if (_log.shouldWarn())
+                        _log.warn("Charlie @ " + from + " said we were an invalid IP address: " + uhe.getMessage(), uhe);
+                    _context.statManager().addRateData("udp.testBadIP", 1);
+                }
+                synchronized(this) {
+                    sendTestToCharlie();
+                }
                 break;
+            }
 
             // alice to charlie, out-of-session
-            case 6:
+            case 6: {
+                state.setReceiveAliceTime(now);
+                state.setLastSendTime(now);
+                long rcvId = (nonce << 32) | nonce;
+                long sendId = ~rcvId;
+                InetAddress addr = state.getAliceIP();
+                int alicePort = state.getAlicePort();
+                byte[] aliceIP = addr.getAddress();
+                iplen = aliceIP.length;
+                data = new byte[13 + iplen];
+                data[0] = 3;  // charlie
+                data[1] = 2;  // version
+                DataHelper.toLong(data, 2, 4, nonce);
+                DataHelper.toLong(data, 6, 4, now / 1000);
+                data[10] = (byte) iplen;
+                System.arraycopy(aliceIP, 0, data, 11, iplen);
+                DataHelper.toLong(data, 11 + iplen, 2, alicePort);
+                UDPPacket packet = _packetBuilder2.buildPeerTestToAlice(addr, alicePort,
+                                                                        state.getAliceIntroKey(), false,
+                                                                        sendId, rcvId, data);
+                _transport.send(packet);
                 break;
+            }
 
             // charlie to alice, out-of-session
-            case 7:
+            case 7: {
+                PeerTestState test = _currentTest;
+                if (test == null || test.getNonce() != nonce) {
+                    if (_log.shouldWarn())
+                        _log.warn("Test nonce mismatch? " + nonce);
+                    return;
+                }
+                if (test.getReceiveCharlieTime() <= 0) {
+                   // ??
+                }
+                // this is our second charlie, yay!
+                test.setReceiveCharlieTime(now);
+                test.setAlicePortFromCharlie(testPort);
+                try {
+                    InetAddress addr = InetAddress.getByAddress(testIP);
+                    test.setAliceIPFromCharlie(addr);
+                    if (test.getReceiveBobTime() > 0)
+                        testComplete();
+                } catch (UnknownHostException uhe) {
+                    if (_log.shouldWarn())
+                        _log.warn("Charlie @ " + from + " said we were an invalid IP address: " + uhe.getMessage(), uhe);
+                    _context.statManager().addRateData("udp.testBadIP", 1);
+                }
+                if (test.getReceiveBobTime() > 0)
+                    testComplete();
                 break;
+            }
 
             default:
                 return;
@@ -1068,6 +1201,8 @@ class PeerTestManager {
     /**
      * The packet's IP/port does not match the IP/port included in the message, 
      * so we must be Charlie receiving a PeerTest from Bob.
+     *
+     * SSU 1 only.
      *  
      * @param bob non-null if received in-session, otherwise null
      * @param inSession true if authenticated in-session
@@ -1150,6 +1285,8 @@ class PeerTestManager {
      * The PeerTest message came from the peer referenced in the message (or there wasn't
      * any info in the message), plus we are not acting as Charlie (so we've got to be Bob).
      *
+     * SSU 1 only.
+     *
      * testInfo IP/port ignored
      *
      * @param alice non-null
@@ -1304,6 +1441,8 @@ class PeerTestManager {
     /** 
      * We are charlie, so send Alice her PeerTest message  
      *
+     * SSU 1 only.
+     *
      * testInfo IP/port ignored
      * @param state non-null
      */
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 f9ccbd03941df5b36268343a15ec587eed8b37b9..2a456a82bd59b4c78f1b3b6e3fa8f5c17faad9c2 100644
--- a/router/java/src/net/i2p/router/transport/udp/SSU2Util.java
+++ b/router/java/src/net/i2p/router/transport/udp/SSU2Util.java
@@ -149,6 +149,7 @@ final class SSU2Util {
      *
      *  @param h to be included in sig, not included in data
      *  @param h2 may be null, to be included in sig, not included in data
+     *  @param ip may be null
      *  @return null on failure
      */
     public static byte[] createPeerTestData(I2PAppContext ctx, Hash h, Hash h2,
@@ -162,9 +163,11 @@ final class SSU2Util {
         data[1] = 2;  // version
         DataHelper.toLong(data, 2, 4, nonce);
         DataHelper.toLong(data, 6, 4, ctx.clock().now() / 1000);
-        data[10] = (byte) ip.length;
-        System.arraycopy(ip, 0, data, 11, ip.length);
-        DataHelper.toLong(data, 11 + ip.length, 2, port);
+        int iplen = (ip != null) ? ip.length : 0;
+        data[10] = (byte) iplen;
+        if (ip != null)
+            System.arraycopy(ip, 0, data, 11, iplen);
+        DataHelper.toLong(data, 11 + iplen, 2, port);
         Signature sig = sign(ctx, PEER_TEST_PROLOGUE, h, h2, data, datalen, spk);
         if (sig == null)
             return null;