diff --git a/router/java/src/net/i2p/router/transport/udp/PeerTestEvent.java b/router/java/src/net/i2p/router/transport/udp/PeerTestEvent.java
index de866d4df70ec350100187ba379ac852f0710d7c..6d48cf6906d68fc9bb516a953287ce5610dbe9c3 100644
--- a/router/java/src/net/i2p/router/transport/udp/PeerTestEvent.java
+++ b/router/java/src/net/i2p/router/transport/udp/PeerTestEvent.java
@@ -83,8 +83,9 @@ class PeerTestEvent extends SimpleTimer2.TimedEvent {
         if (bob != null) {
             if (_log.shouldLog(Log.INFO))
                 _log.info("Running periodic test with bob = " + bob);
-            _testManager.runTest(bob);
-            setLastTested(isIPv6);
+            boolean started = _testManager.runTest(bob);
+            if (started)
+                setLastTested(isIPv6);
         } else {
             if (_log.shouldLog(Log.WARN))
                 _log.warn("Unable to run peer test, no peers available - v6? " + isIPv6);
@@ -107,15 +108,15 @@ class PeerTestEvent extends SimpleTimer2.TimedEvent {
      *  @since 0.9.39
      */
     public synchronized void forceRunSoon(boolean isIPv6, long delay) {
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug("PTE.forceRunSoon() - v6? " + isIPv6, new Exception());
+        //if (_log.shouldLog(Log.DEBUG))
+        //    _log.debug("PTE.forceRunSoon() - v6? " + isIPv6, new Exception());
         if (!isIPv6 && _transport.isIPv4Firewalled())
             return;
         if (isIPv6 && _transport.isIPv6Firewalled())
             return;
         _forceRun = isIPv6 ? FORCE_IPV6 : FORCE_IPV4;
         if (_log.shouldDebug())
-            _log.debug("reschedule for " + net.i2p.data.DataHelper.formatDuration(delay));
+            _log.debug("PTE.forceRunSoon(), v6? " + isIPv6 + ", reschedule for " + net.i2p.data.DataHelper.formatDuration(delay));
         reschedule(delay);
     }
 
@@ -153,8 +154,8 @@ class PeerTestEvent extends SimpleTimer2.TimedEvent {
             _lastTestedV6.set(now);
         else
             _lastTested.set(now);
-        if (_log.shouldLog(Log.DEBUG))
-            _log.debug("PTE.setLastTested() - v6? " + isIPv6, new Exception());
+        //if (_log.shouldLog(Log.DEBUG))
+        //    _log.debug("PTE.setLastTested() - v6? " + isIPv6, new Exception());
     }
 
     private boolean shouldTest() {
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 6fa8779ff2fc96a2bbceb477bcc3a87fb3ccd6a0..4d5acfb95534c90c44340b05a17bbca4b3444326 100644
--- a/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
+++ b/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
@@ -171,6 +171,15 @@ class PeerTestManager {
     private static final long MAX_SKEW = 2*60*1000;
     private static final long MAX_NONCE = (1l << 32) - 1l;
 
+    // special markers for SSU2 when Charlie is firewalled
+    private static final InetAddress PENDING_IP;
+    static {
+        InetAddress p = null;
+        try { p = InetAddress.getByName("0.0.0.1"); } catch (UnknownHostException uhe) {}
+        PENDING_IP = p;
+    }
+    private static final int PENDING_PORT = 99999;
+
     /**
      *  Have seen peer tests (as Alice) get stuck (_currentTest != null)
      *  so I've thrown some synchronizization on the methods;
@@ -195,18 +204,19 @@ class PeerTestManager {
      *  The next few methods are for when we are Alice
      *
      *  @param bob IPv4 only
+     *  @return true if we successfully started a test
      */
-    public synchronized void runTest(PeerState bob) {
+    public synchronized boolean runTest(PeerState bob) {
         if (_currentTest != null) {
             if (_log.shouldLog(Log.WARN))
                 _log.warn("We are already running a test: " + _currentTest + ", aborting test with bob = " + bob);
-            return;
+            return false;
         }
         InetAddress bobIP = bob.getRemoteIPAddress();
         if (_transport.isTooClose(bobIP.getAddress())) {
             if (_log.shouldLog(Log.WARN))
                 _log.warn("Not running test with Bob too close to us " + bobIP);
-            return;
+            return false;
         }
         PeerTestState test = new PeerTestState(ALICE, bob, bobIP instanceof Inet6Address,
                                                _context.random().nextLong(MAX_NONCE),
@@ -219,7 +229,7 @@ class PeerTestManager {
             } catch (UnknownHostException uhe) {
                 if (_log.shouldWarn())
                     _log.warn("Unable to get our IP", uhe);
-                return;
+                return false;
             }
         }
         _currentTest = test;
@@ -234,14 +244,21 @@ class PeerTestManager {
         test.incrementPacketsRelayed();
         sendTestToBob();
         
-        _context.simpleTimer2().addEvent(new ContinueTest(test.getNonce()), RESEND_TIMEOUT);
+        new ContinueTest(test.getNonce());
+        return true;
     }
     
-    private class ContinueTest implements SimpleTimer.TimedEvent {
+    /**
+     * SSU 1 or 2. We are Alice.
+     */
+    private class ContinueTest extends SimpleTimer2.TimedEvent {
         private final long _nonce;
 
+        /** schedules itself */
         public ContinueTest(long nonce) {
+            super(_context.simpleTimer2());
             _nonce = nonce;
+            schedule(RESEND_TIMEOUT);
         }
 
         public void timeReached() {
@@ -265,24 +282,35 @@ class PeerTestManager {
                             testComplete();
                         return;
                     }
-                    if (state.getReceiveBobTime() <= 0) {
-                        // no message from Bob yet, send it again
+                    long bobTime = state.getReceiveBobTime();
+                    long charlieTime = state.getReceiveCharlieTime();
+                    if (bobTime <= 0 && charlieTime <= 0) {
+                        // no message from Bob or Charlie yet, send it again
                         sendTestToBob();
-                    } else if (state.getReceiveCharlieTime() <= 0) {
+                    } else if (charlieTime <= 0) {
                         // received from Bob, but no reply from Charlie.  send it to 
                         // Bob again so he pokes Charlie
-                        // This is only useful for SSU 1; SSU 2 discards dups
+                        // We don't resend to Bob for SSU2; Charlie will retransmit.
                         if (state.getBob().getVersion() == 1)
                             sendTestToBob();
+                        // TODO if version 2 and long enough, send msg 6 anyway
+                    } else if (bobTime <= 0) {
+                        // received from Charlie, but no reply from Bob.  Send it to 
+                        // Bob again so he retransmits his reply.
+                        // Bob handles dups / retx as of 0.9.57
+                        //if (state.getBob().getVersion() == 1)
+                            sendTestToBob();
+                        // TODO if version 1 and long enough, send msg 6 anyway
+                        // For version 2, we can't send msg 6 without knowing charlie's intro key
                     } else {
                         // received from both Bob and Charlie, but we haven't received a
                         // second message from Charlie yet
                         sendTestToCharlie();
                     }
                     // retx at 4, 10, 17, 25 elapsed time
-                    _context.simpleTimer2().addEvent(ContinueTest.this, RESEND_TIMEOUT + (sent*1000));
+                    reschedule(RESEND_TIMEOUT + (sent*1000));
                 } else {
-                    _context.simpleTimer2().addEvent(ContinueTest.this, RESEND_TIMEOUT - timeSinceSend);
+                    reschedule(RESEND_TIMEOUT - timeSinceSend);
                 }
             }
         }
@@ -307,28 +335,33 @@ class PeerTestManager {
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug("Sending test to Bob: " + test);
             UDPPacket packet;
-            if (test.getBob().getVersion() == 1) {
+            PeerState bob = test.getBob();
+            if (bob.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(), null,
-                                                          ALICE, test.getNonce(), bob.getOurIP(), bob.getOurPort(), spk);
+                PeerState2 bob2 = (PeerState2) bob;
+                // only create this once
+                byte[] data = test.getTestData();
                 if (data == null) {
-                    if (_log.shouldWarn())
-                        _log.warn("sig fail");
-                     testComplete();
-                     return;
+                    SigningPrivateKey spk = _context.keyManager().getSigningPrivateKey();
+                    data = SSU2Util.createPeerTestData(_context, bob2.getRemotePeer(), null,
+                                                       ALICE, test.getNonce(), bob2.getOurIP(), bob2.getOurPort(), spk);
+                    if (data == null) {
+                        if (_log.shouldWarn())
+                            _log.warn("sig fail");
+                         testComplete();
+                         return;
+                    }
+                    test.setTestData(data);
                 }
-                packet = _packetBuilder2.buildPeerTestFromAlice(data, bob);
+                packet = _packetBuilder2.buildPeerTestFromAlice(data, bob2);
             }
             _transport.send(packet);
             long now = _context.clock().now();
             test.setLastSendTime(now);
-            test.getBob().setLastSendTime(now);
+            bob.setLastSendTime(now);
         } else {
             _currentTest = null;
         }
@@ -580,12 +613,18 @@ class PeerTestManager {
             }
         } else if (test.getReceiveCharlieTime() > 0) {
             // we received only one message (5) from charlie
-            status = Status.UNKNOWN;
+            // change in 0.9.57; previously returned UNKNOWN always
+            if (_transport.isSnatted()) {
+                status = Status.UNKNOWN;
+            } else {
+                // assume good
+                status = isIPv6 ? Status.IPV4_UNKNOWN_IPV6_OK : Status.IPV4_OK_IPV6_UNKNOWN;
+            }
         } else if (test.getReceiveBobTime() > 0) {
             // we received a message from bob (4) but no messages from charlie
             status = isIPv6 ? Status.IPV4_UNKNOWN_IPV6_FIREWALLED : Status.IPV4_FIREWALLED_IPV6_UNKNOWN;
         } else {
-            // we never received anything from bob - he is either down, 
+            // we never received anything from bob or charlie,
             // ignoring us, or unable to get a Charlie to respond
             status = Status.UNKNOWN;
             // TODO disconnect from Bob if version 2?
@@ -890,6 +929,7 @@ class PeerTestManager {
         private volatile int count;
         private static final long DELAY = 50;
 
+        /** schedules itself */
         public DelayTest(RemoteHostId f, PeerState2 fp, int m, Hash h, byte[] d) {
             super(_context.simpleTimer2());
             from = f;
@@ -966,7 +1006,7 @@ class PeerTestManager {
         if (_log.shouldDebug())
             _log.debug("Got peer test msg " + msg +
                        " status: " + status +
-                       " hash: " + h +
+                       " hash: " + (h != null ? h.toBase64() : "null") +
                        " nonce: " + nonce +
                        " time: " + DataHelper.formatTime(time) +
                        " ip/port: " + Addresses.toString(testIP, testPort) +
@@ -1008,8 +1048,34 @@ class PeerTestManager {
         }
         if (msg < 3) {
             if (state != null) {
+                byte[] retx = state.getTestData();
+                if (retx != null) {
+                    if (msg == 1 && state.getSendAliceTime() > 0) {
+                        if (_log.shouldDebug())
+                            _log.debug("Retx msg 4 to alice on " + state);
+                        // we already sent to alice, send it again
+                        PeerState2 alice = state.getAlice();
+                        UDPPacket packet = _packetBuilder2.buildPeerTestToAlice(state.getStatus(), state.getCharlieHash(), data, alice);
+                        _transport.send(packet);
+                        alice.setLastSendTime(now);
+                        state.setSendAliceTime(now);
+                        return;
+                    } else if (msg == 2) {
+                        if (_log.shouldDebug())
+                            _log.debug("Retx msg 3 to bob on " + state);
+                        PeerState2 bob = (PeerState2) state.getBob();
+                        UDPPacket packet = _packetBuilder2.buildPeerTestToBob(state.getStatus(), data, bob);
+                        _transport.send(packet);
+                        bob.setLastSendTime(now);
+                        // should we retx msg 5 also?
+                        return;
+                    } else {
+                        // msg 1 but haven't heard from a good charlie yet
+                        // TODO pick a new charlie
+                    }
+                }
                 if (_log.shouldWarn())
-                    _log.warn("Dup msg " + msg + " from " + fromPeer);
+                    _log.warn("Dup msg " + msg + " from " + fromPeer + " on " + state);
                 return;
             }
             if (_activeTests.size() >= MAX_ACTIVE_TESTS) {
@@ -1041,6 +1107,8 @@ class PeerTestManager {
 
         switch (msg) {
             // alice to bob, in-session
+            // If we immediately reject with a TEST_REJECT_BOB code, we do not
+            // save the test state; so if Alice retransmits, we'll do it all again.
             case 1: {
                 if (status != 0) {
                     if (_log.shouldWarn())
@@ -1120,23 +1188,27 @@ class PeerTestManager {
                 // save alice-signed test data in case we need to send to another charlie
                 state.setTestData(data);
                 _activeTests.put(lNonce, state);
-                _context.simpleTimer2().addEvent(new RemoveTest(lNonce), MAX_BOB_LIFETIME);
+                // TODO we need a retx or pick-new-charlie timer
+                new RemoveTest(lNonce, MAX_BOB_LIFETIME);
                 // send alice RI to charlie
                 if (_log.shouldDebug())
                     _log.debug("Send Alice RI and msg 2 to charlie on " + state);
+                // TODO see if Alide RI will compress enough to fit in the peer test packet
                 DatabaseStoreMessage dbsm = new DatabaseStoreMessage(_context);
                 dbsm.setEntry(aliceRI);
                 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);
+                // delay because dbsm is queued, we want it to get there first
+                new DelaySend(packet, 100);
                 charlie.setLastSendTime(now);
                 break;
             }
 
             // bob to charlie, in-session
+            // If we immediately reject with a TEST_REJECT_CHARLIE code, we do not
+            // save the test state; so if Alice or Bob retransmits, we'll do it all again.
             case 2: {
                 if (status != 0) {
                     if (_log.shouldWarn())
@@ -1202,7 +1274,7 @@ class PeerTestManager {
                     state.setReceiveBobTime(now);
                     state.setLastSendTime(now);
                     _activeTests.put(lNonce, state);
-                    _context.simpleTimer2().addEvent(new RemoveTest(lNonce), MAX_CHARLIE_LIFETIME);
+                    new CharlieTimer(lNonce);
                 }
                 // generate our signed data
                 // we sign it even if rejecting, not required though
@@ -1232,6 +1304,10 @@ class PeerTestManager {
                                                                   aliceIntroKey, true,
                                                                   sendId, rcvId, data);
                     _transport.send(packet);
+                    state.incrementPacketsRelayed();
+                    // save charlie-signed test data in case we need to retransmit to alice or bob
+                    state.setStatus(rcode);
+                    state.setTestData(data);
                 }
                 break;
             }
@@ -1252,12 +1328,14 @@ class PeerTestManager {
                                     _log.info("Charlie response " + status + " picked a new one " + charlie + " on " + state);
                                 state.setCharlie(charlie.getRemoteIPAddress(), charlie.getRemotePort(), charlie.getRemotePeer());
                                 state.setLastSendTime(now);
+                                // TODO see if Alice RI will compress enough to fit in the peer test packet
                                 DatabaseStoreMessage dbsm = new DatabaseStoreMessage(_context);
                                 dbsm.setEntry(aliceRI);
                                 dbsm.setMessageExpiration(now + 10*1000);
                                 _transport.send(dbsm, charlie);
                                 UDPPacket packet = _packetBuilder2.buildPeerTestToCharlie(alice, state.getTestData(), (PeerState2) charlie);
-                                _transport.send(packet);
+                                // delay because dbsm is queued, we want it to get there first
+                                new DelaySend(packet, 100);
                                 charlie.setLastSendTime(now);
                                 break;
                             }
@@ -1269,11 +1347,13 @@ class PeerTestManager {
                 state.setLastSendTime(now);
                 PeerState2 alice = state.getAlice();
                 Hash charlie = fromPeer.getRemotePeer();
-                RouterInfo charlieRI = _context.netDb().lookupRouterInfoLocally(charlie);
+                RouterInfo charlieRI = (status == SSU2Util.TEST_ACCEPT) ? _context.netDb().lookupRouterInfoLocally(charlie) : null;
                 if (charlieRI != null) {
-                    // send charlie RI to alice
+                    // send charlie RI to alice, only if ACCEPT.
+                    // Alice would need it to verify sig, but not worth the bandwidth
                     if (_log.shouldDebug())
                         _log.debug("Send Charlie RI to alice on " + state);
+                    // TODO see if Charlie RI will compress enough to fit in the peer test packet
                     DatabaseStoreMessage dbsm = new DatabaseStoreMessage(_context);
                     dbsm.setEntry(charlieRI);
                     dbsm.setMessageExpiration(now + 10*1000);
@@ -1290,7 +1370,7 @@ class PeerTestManager {
                     }
                 } else  {
                     // oh well, maybe alice has it
-                    if (_log.shouldLog(Log.WARN))
+                    if (status == SSU2Util.TEST_ACCEPT && _log.shouldWarn())
                         _log.warn("No charlie RI");
                 }
                 // forward to alice, don't bother to validate signed data
@@ -1298,10 +1378,18 @@ class PeerTestManager {
                 if (_log.shouldDebug())
                     _log.debug("Send msg 4 status " + status + " to alice on " + state);
                 UDPPacket packet = _packetBuilder2.buildPeerTestToAlice(status, charlie, data, alice);
-                _transport.send(packet);
+                // delay because dbsm is queued, we want it to get there first
+                if (charlieRI != null)
+                    new DelaySend(packet, 100);
+                else
+                    _transport.send(packet);
                 alice.setLastSendTime(now);
-                // we are done
-                _activeTests.remove(lNonce);
+                // overwrite alice-signed test data with charlie-signed data in case we need to retransmit
+                state.setStatus(status);
+                state.setSendAliceTime(now);
+                state.setTestData(data);
+                // we should be done, but stick around for possible retx to alice
+                //_activeTests.remove(lNonce);
                 break;
             }
 
@@ -1374,6 +1462,8 @@ class PeerTestManager {
                                     // i2pd Bob picks firewalled Charlie
                                     if (_log.shouldWarn())
                                         _log.warn("Charlie IP not found: " + test + '\n' + ra);
+                                    charlieIP = PENDING_IP;
+                                    charliePort = PENDING_PORT;
                                 }
                             } else {
                                 if (_log.shouldWarn())
@@ -1397,7 +1487,44 @@ class PeerTestManager {
                     testComplete();
                     return;
                 }
-                test.setCharlie(charlieIP, charliePort, h);
+                InetAddress oldIP = test.getCharlieIP();
+                if (oldIP == null) {
+                    // msg 4 before msg 5
+                    test.setCharlie(charlieIP, charliePort, h);
+                } else if (charlieIP == PENDING_IP) {
+                    // dup msg 4 ??
+                } else {
+                    // msg 4 after msg 5, charlie is not firewalled
+                    int oldPort = test.getCharliePort();
+                    if (!charlieIP.equals(oldIP)) {
+                        if (_log.shouldWarn())
+                            _log.warn("Charlie IP mismatch, msg 4: " + Addresses.toString(charlieIP.getAddress(), charliePort) +
+                                      ", msg 5: " + Addresses.toString(oldIP.getAddress(), oldPort) + " on " + test);
+                        // stop here, assume good unless snatted
+                        if (!_transport.isSnatted()) {
+                            test.setAliceIPFromCharlie(test.getAliceIP());
+                            test.setAlicePortFromCharlie(test.getAlicePort());
+                        }
+                        testComplete();
+                        return;
+                    } else if (charliePort != oldPort) {
+                        if (_log.shouldWarn())
+                            _log.warn("Charlie port mismatch, msg 4: " + Addresses.toString(charlieIP.getAddress(), charliePort) +
+                                      ", msg 5: " + Addresses.toString(oldIP.getAddress(), oldPort) + " on " + test);
+                        if (TransportUtil.isValidPort(charliePort)) {
+                            // Charlie is snatted or confused about his port, update port and keep going
+                            test.setCharlie(charlieIP, charliePort, h);
+                        } else {
+                            // Don't like charlie's port, stop here, assume good unless snatted
+                            if (!_transport.isSnatted()) {
+                                test.setAliceIPFromCharlie(test.getAliceIP());
+                                test.setAlicePortFromCharlie(test.getAlicePort());
+                            }
+                            testComplete();
+                            return;
+                        }
+                    }
+                }
                 test.setCharlieIntroKey(charlieIntroKey);
                 if (test.getReceiveCharlieTime() > 0) {
                     // send msg 6
@@ -1421,6 +1548,51 @@ class PeerTestManager {
                         _log.warn("Test nonce mismatch? " + nonce);
                     return;
                 }
+                InetAddress charlieIP = test.getCharlieIP();
+                if (charlieIP == null) {
+                    // msg 5 before msg 4
+                    try {
+                        test.setCharlie(InetAddress.getByAddress(fromIP), fromPort, null);
+                    } catch (UnknownHostException uhe) {}
+                } else if (charlieIP == PENDING_IP) {
+                    // msg 5 after msg 4, charlie is firewalled
+                    // set charlie's real IP/port
+                    try {
+                        test.setCharlie(InetAddress.getByAddress(fromIP), fromPort, test.getCharlieHash());
+                    } catch (UnknownHostException uhe) {}
+                } else {
+                    // msg 5 after msg 4, charlie is not firewalled
+                    byte[] oldIP = charlieIP.getAddress();
+                    int oldPort = test.getCharliePort();
+                    if (!DataHelper.eq(fromIP, oldIP)) {
+                        if (_log.shouldWarn())
+                            _log.warn("Charlie IP mismatch, msg 4: " + Addresses.toString(oldIP, oldPort) +
+                                      ", msg 5: " + Addresses.toString(fromIP, fromPort) + " on " + test);
+                        // stop here, assume good unless snatted
+                        if (!_transport.isSnatted()) {
+                            test.setAliceIPFromCharlie(test.getAliceIP());
+                            test.setAlicePortFromCharlie(test.getAlicePort());
+                        }
+                        testComplete();
+                        return;
+                    } else if (fromPort != oldPort) {
+                        if (_log.shouldWarn())
+                            _log.warn("Charlie port mismatch, msg 4: " + Addresses.toString(oldIP, oldPort) +
+                                      ", msg 5: " + Addresses.toString(fromIP, fromPort) + " on " + test);
+                        if (TransportUtil.isValidPort(fromPort)) {
+                            // Charlie is snatted or confused about his port, update port and keep going
+                            test.setCharlie(charlieIP, fromPort, h);
+                        } else {
+                            // Don't like charlie's port, stop here, assume good unless snatted
+                            if (!_transport.isSnatted()) {
+                                test.setAliceIPFromCharlie(test.getAliceIP());
+                                test.setAlicePortFromCharlie(test.getAlicePort());
+                            }
+                            testComplete();
+                            return;
+                        }
+                    }
+                }
                 test.setReceiveCharlieTime(now);
                 // Do NOT set this here, only for msg 7, this is how testComplete() knows we got msg 7
                 //test.setAlicePortFromCharlie(testPort);
@@ -1440,6 +1612,7 @@ class PeerTestManager {
                     }
                 } else {
                     // we haven't gotten message 4 yet
+                    // We don't know Charlie's hash or intro key, we can't send msg 6 until we do
                     if (_log.shouldDebug())
                         _log.debug("Got msg 5 before msg 4 on " + test);
                 }
@@ -1478,7 +1651,10 @@ class PeerTestManager {
                                                                         state.getAliceIntroKey(), false,
                                                                         sendId, rcvId, data);
                 _transport.send(packet);
+                state.incrementPacketsRelayed();
                 // for now, ignore address block, we could pass it to externalAddressReceived()
+                // we should be done, but stick around in case we get a retransmitted msg 6
+                //_activeTests.remove(lNonce);
                 break;
             }
 
@@ -1562,6 +1738,10 @@ class PeerTestManager {
                 if (!host.contains(".") && !caps.contains(TransportImpl.CAP_IPV4))
                     continue;
             }
+            // skip bogus addresses
+            byte[] ip = addr.getIP();
+            if (ip != null && !TransportUtil.isPubliclyRoutable(ip, true))
+                continue;
             ra = addr;
             break;
         }
@@ -1652,7 +1832,7 @@ class PeerTestManager {
             if (isNew) {
                 Long lnonce = Long.valueOf(nonce);
                 _activeTests.put(lnonce, state);
-                _context.simpleTimer2().addEvent(new RemoveTest(lnonce), MAX_CHARLIE_LIFETIME);
+                new RemoveTest(lnonce, MAX_CHARLIE_LIFETIME);
             }
 
             state.setLastSendTime(now);
@@ -1766,7 +1946,7 @@ class PeerTestManager {
             if (isNew) {
                 Long lnonce = Long.valueOf(nonce);
                 _activeTests.put(lnonce, state);
-                _context.simpleTimer2().addEvent(new RemoveTest(lnonce), MAX_BOB_LIFETIME);
+                new RemoveTest(lnonce, MAX_BOB_LIFETIME);
             }
             
             state.setLastSendTime(now);
@@ -1873,15 +2053,19 @@ class PeerTestManager {
             _context.statManager().addRateData("udp.testBadIP", 1);
         }
     }
-    
+
     /** 
+     * SSU 1 Bob/Charlie and SSU 2 Bob
      * forget about charlie's nonce after a short while.
      */
-    private class RemoveTest implements SimpleTimer.TimedEvent {
+    private class RemoveTest extends SimpleTimer2.TimedEvent {
         private final Long _nonce;
 
-        public RemoveTest(Long nonce) {
+        /** schedules itself */
+        public RemoveTest(Long nonce, long delay) {
+            super(_context.simpleTimer2());
             _nonce = nonce;
+            schedule(delay);
         }
 
         public void timeReached() {
@@ -1890,6 +2074,77 @@ class PeerTestManager {
         }
     }
 
+    /** 
+     * SSU 2 Charlie only.
+     * Retransmit msg 5 if necessary, and then
+     * forget about charlie's nonce after a short while.
+     *
+     * @since 0.9.57
+     */
+    private class CharlieTimer extends SimpleTimer2.TimedEvent {
+        private final Long _nonce;
+
+        /** schedules itself */
+        public CharlieTimer(Long nonce) {
+            super(_context.simpleTimer2());
+            _nonce = nonce;
+            schedule(RESEND_TIMEOUT);
+        }
+
+        public void timeReached() {
+            PeerTestState state = _activeTests.get(_nonce);
+            if (state == null)
+                return;
+            long now = _context.clock().now();
+            long remaining = state.getBeginTime() + MAX_CHARLIE_LIFETIME - now;
+            if (remaining <= 0) {
+                if (_log.shouldDebug())
+                    _log.debug("Expired as charlie on " + state);
+                _activeTests.remove(_nonce);
+                return;
+            }
+            if (state.getReceiveAliceTime() > 0) {
+                // got msg 6, no more need to retx msg 5
+                reschedule(remaining);
+                return;
+            }
+
+            // retransmit at 4/8/12 sec, no backoff
+            if (_log.shouldDebug())
+                _log.debug("Retx msg 5 to alice on " + state);
+            long nonce = _nonce.longValue();
+            long sendId = (nonce << 32) | nonce;
+            long rcvId = ~sendId;
+            // send the same data we sent to Bob
+            UDPPacket packet = _packetBuilder2.buildPeerTestToAlice(state.getAliceIP(), state.getAlicePort(),
+                                                                    state.getAliceIntroKey(), true,
+                                                                    sendId, rcvId, state.getTestData());
+            _transport.send(packet);
+            state.incrementPacketsRelayed();
+            state.setLastSendTime(now);
+            reschedule(Math.min(RESEND_TIMEOUT, remaining));
+        }
+    }
+
+    /** 
+     * Simple fix for RI getting there before PeerTest.
+     * SSU2 only. We are Bob, for delaying msg sent after RI to Alice or Charlie.
+     * @since 0.9.57
+     */
+    private class DelaySend extends SimpleTimer2.TimedEvent {
+        private final UDPPacket pkt;
+
+        public DelaySend(UDPPacket packet, long delay) {
+            super(_context.simpleTimer2());
+            pkt = packet;
+            schedule(delay);
+        }
+
+        public void timeReached() {
+           _transport.send(pkt);
+        }
+    }
+
     /**
      *  This is only for out-of-session messages 5-7,
      *  where most blocks are not allowed.
diff --git a/router/java/src/net/i2p/router/transport/udp/PeerTestState.java b/router/java/src/net/i2p/router/transport/udp/PeerTestState.java
index 24376003d2e5cd32831307a67f99a1a7f38c907b..a2b82cbac5d45f0510fbca8c216532d89cfc8ead 100644
--- a/router/java/src/net/i2p/router/transport/udp/PeerTestState.java
+++ b/router/java/src/net/i2p/router/transport/udp/PeerTestState.java
@@ -42,6 +42,8 @@ class PeerTestState {
     private long _receiveAliceTime;
     private long _receiveBobTime;
     private long _receiveCharlieTime;
+    private long _sendAliceTime;
+    private int _status;
     private final AtomicInteger _packetsRelayed = new AtomicInteger();
     
     public enum Role {ALICE, BOB, CHARLIE};
@@ -198,13 +200,39 @@ class PeerTestState {
     public void setReceiveCharlieTime(long when) { _receiveCharlieTime = when; }
 
     /**
-     *  SSU2 only, we are Bob
+     * when did we send to alice, SSU2 Bob only
+     * @since 0.9.57
+     */
+    public long getSendAliceTime() { return _sendAliceTime; }
+
+    /**
+     * when did we send to alice, SSU2 Bob only
+     * @since 0.9.57
+     */
+    public void setSendAliceTime(long when) { _sendAliceTime = when; }
+
+    /**
+     * what code did we send to alice, SSU2 Bob only
+     * @since 0.9.57
+     */
+    public int getStatus() { return _status; }
+
+    /**
+     * what code did we send to alice, SSU2 Bob only
+     * @since 0.9.57
+     */
+    public void setStatus(int status) { _status = status; }
+
+    /**
+     *  Get for retransmission.
+     *  SSU2 only, we are Alice, Bob or Charlie
      *  @since 0.9.57
      */
     public byte[] getTestData() { return _testData; }
 
     /**
-     *  SSU2 only, we are Bob
+     *  Save for retransmission.
+     *  SSU2 only, we are Alice, Bob or Charlie
      *  @since 0.9.57
      */
     public void setTestData(byte[] data) { _testData = data; }