SSU2: Redesign peer test symmetric NAT detection

- Send msg 6 even if msg 5 is not received, after a short delay, to detect symmetric NAT
- Prevent test result of symmetric NAT if Charlie is the one that appears to be symmetric natted.
- Mark if Charlie unreachable if apparently symmetric natted.
- Do not treat a different IP but same port as symmetric NAT
- Increase msg 6 retransmission frequency
- Ignore msg 5 received after sending msg 6
- Call externalAddressReceived() on reception of msg 7
- Add javadoc for testComplete() state machine

Most changes only affect SSU2; some SSU1 changes to follow.

ref: http://zzz.i2p/topics/3489
This commit is contained in:
zzz
2022-12-26 06:14:55 -05:00
parent 2ce2b894b7
commit ba0fee151b

View File

@@ -181,6 +181,10 @@ class PeerTestManager {
}
private static final int PENDING_PORT = 99999;
// Preliminary
private static final boolean ENABLE_SSU2_SYMNAT_TEST = true;
/**
* Have seen peer tests (as Alice) get stuck (_currentTest != null)
* so I've thrown some synchronizization on the methods;
@@ -296,7 +300,8 @@ class PeerTestManager {
testComplete();
return;
}
long timeSinceSend = _context.clock().now() - state.getLastSendTime();
long now = _context.clock().now();
long timeSinceSend = now - state.getLastSendTime();
if (timeSinceSend >= RESEND_TIMEOUT) {
int sent = state.incrementPacketsRelayed();
if (sent > MAX_RELAYED_PER_TEST_ALICE) {
@@ -315,9 +320,20 @@ class PeerTestManager {
// received from Bob, but no reply from Charlie. send it to
// Bob again so he pokes Charlie
// We don't resend to Bob for SSU2; Charlie will retransmit.
if (state.getBob().getVersion() == 1)
if (state.getBob().getVersion() == 1) {
sendTestToBob();
// TODO if version 2 and long enough, send msg 6 anyway
} else if (ENABLE_SSU2_SYMNAT_TEST) {
// if version 2 and it's been long enough, and Charlie isn't firewalled, send msg 6 anyway
// This allows us to detect Symmetric NAT.
// We don't have his IP/port if he's firewalled, and we wouldn't trust his answer
// anyway as he could be symmetric natted.
// After this, we will ignore any msg 5 received
if (now - bobTime > 5000 && state.getCharliePort() != PENDING_PORT) {
if (_log.shouldWarn())
_log.warn("Continuing test w/o msg 5: " + state);
sendTestToCharlie();
}
}
} else if (bobTime <= 0) {
// received from Charlie, but no reply from Bob. Send it to
// Bob again so he retransmits his reply.
@@ -331,8 +347,19 @@ class PeerTestManager {
// second message from Charlie yet
sendTestToCharlie();
}
// retx at 4, 10, 17, 25 elapsed time
reschedule(RESEND_TIMEOUT + (sent*1000));
if (bobTime > 0 && charlieTime <= 0) {
if (state.getBeginTime() + MAX_CHARLIE_LIFETIME < now) {
if (!_currentTestComplete)
testComplete();
return;
}
// earlier because charlie will go away at 15 sec
// retx at 4, 6, 9, 13 elapsed time
reschedule(sent*1000);
} else {
// retx at 4, 10, 17, 25 elapsed time
reschedule(RESEND_TIMEOUT + (sent*1000));
}
} else {
reschedule(RESEND_TIMEOUT - timeSinceSend);
}
@@ -637,6 +664,57 @@ class PeerTestManager {
* we have successfully received the second PeerTest from a Charlie.
*
* call from a synchronized method
*
* Old SSU 1 test result state machine:
* ref: SSU 1 doc http://i2p-projekt.i2p/en/docs/transport/ssu
*
*<pre>
* If Alice does not get msg 5: FIREWALLED
*
* If Alice gets msg 5:
* .. If Alice does not get msg 4: UNKNOWN (says the SSU 1 spec,
* but Java I2P proceeds for SSU 1)
* .. If Alice does not get msg 7: UNKNOWN
* .. If Alice gets msgs 4/5/7 and IP/port match: OK
* .. If Alice gets msgs 4/5/7 and IP/port do not match: SYMNAT
*</pre>
*
* New SSU 1/2 test result state machine:
*
*<pre>
* If Alice does not get msg 5:
* .. If Alice does not get msg 4: UNKNOWN
* .. If Alice does not get msg 7: UNKNOWN
* .. If Alice gets msgs 4/7 and IP/port match: FIREWALLED
* .. If Alice gets msgs 4/7 and IP matches, port does not match: SYMNAT, but needs confirmation with 2nd test
* .. If Alice gets msgs 4/7 and IP does not match, port matches: FIREWALLED, address change?
* .. If Alice gets msgs 4/7 and both IP and port do not match: SYMNAT, address change?
*
* If Alice gets msg 5:
* .. If Alice does not get msg 4: OK unless currently SYMNAT, else UNKNOWN
* (in SSU2 have to stop here anyway)
* .. If Alice does not get msg 7: OK unless currently SYMNAT, else UNKNOWN
* .. If Alice gets msgs 4/5/7 and IP/port match: OK
* .. If Alice gets msgs 4/5/7 and IP matches, port does not match: OK, charlie is probably sym. natted
* .. If Alice gets msgs 4/5/7 and IP does not match, port matches: OK, address change?
* .. If Alice gets msgs 4/5/7 and both IP and port do not match: OK, address change?
*</pre>
*
* Simplified version:
*
*<pre>
* 4 5 7 Result Notes
* ----- ------ -----
* n n n UNKNOWN
* y n n FIREWALLED (unless currently SYMNAT)
* n y n OK (unless currently SYMNAT, which is unlikely)
* y y n OK (unless currently SYMNAT, which is unlikely)
* n n y n/a (can't send msg 6)
* y n y FIREWALLED or SYMNAT (requires sending msg 6 w/o rcv msg 5)
* n y y n/a (can't send msg 6)
* y y y OK
*</pre>
*
*/
private void testComplete() {
_currentTestComplete = true;
@@ -651,15 +729,50 @@ class PeerTestManager {
boolean isIPv6 = test.isIPv6();
Status status;
if (test.getAlicePortFromCharlie() > 0) {
int apfc = test.getAlicePortFromCharlie();
if (apfc > 0) {
// we received a second message (7) from charlie
if ( (test.getAlicePort() == test.getAlicePortFromCharlie()) &&
(test.getAliceIP() != null) && (test.getAliceIPFromCharlie() != null) &&
(test.getAliceIP().equals(test.getAliceIPFromCharlie())) ) {
status = isIPv6 ? Status.IPV4_UNKNOWN_IPV6_OK : Status.IPV4_OK_IPV6_UNKNOWN;
// With ENABLE_SSU2_SYMNAT_TEST, we may not have gotten the first message (5)
InetAddress aIP = test.getAliceIP();
InetAddress aIPfc = test.getAliceIPFromCharlie();
if (test.getAlicePort() == apfc &&
aIP != null &&
aIP.equals(aIPfc)) {
// everything matches
if (test.getReceiveCharlieTime() <= 0) {
// SSU2 ENABLE_SSU2_SYMNAT_TEST only, msg 5 not received, msg 7 received
status = isIPv6 ? Status.IPV4_UNKNOWN_IPV6_FIREWALLED : Status.IPV4_FIREWALLED_IPV6_UNKNOWN;
if (_log.shouldWarn())
_log.warn("Test complete w/o msg 5, " + status + " on " + test);
} else {
// Got msgs 5 and 7, everything good
status = isIPv6 ? Status.IPV4_UNKNOWN_IPV6_OK : Status.IPV4_OK_IPV6_UNKNOWN;
}
} else {
// we don't have a SNAT state for IPv6
status = isIPv6 ? Status.IPV4_UNKNOWN_IPV6_FIREWALLED : Status.IPV4_SNAT_IPV6_UNKNOWN;
// IP/port mismatch
if (test.getAlicePort() == apfc) {
// Port matched, only IP was different
// Dot it go
if (_transport.isSymNatted()) {
status = Status.UNKNOWN;
} else if (test.getReceiveCharlieTime() <= 0) {
// SSU2 ENABLE_SSU2_SYMNAT_TEST only, msg 5 not received, msg 7 received
status = isIPv6 ? Status.IPV4_UNKNOWN_IPV6_FIREWALLED : Status.IPV4_FIREWALLED_IPV6_UNKNOWN;
} else {
status = isIPv6 ? Status.IPV4_UNKNOWN_IPV6_OK : Status.IPV4_OK_IPV6_UNKNOWN;
}
} else {
// SYMMETRIC NAT!
// SSU2 ENABLE_SSU2_SYMNAT_TEST only, msg 5 may or may not have been received
// For SSU1, or SSU2 with SNAT_TEST disabled, it's very difficult to get here,
// as we don't send msg 6 w/o receiving msg 5, and if we're symmetric natted, we
// won't get msg 5 unless the port happened to be open with Charlie due to
// previous activity.
// We don't have a SNAT state for IPv6, so set FIREWALLED.
status = isIPv6 ? Status.IPV4_UNKNOWN_IPV6_FIREWALLED : Status.IPV4_SNAT_IPV6_UNKNOWN;
if (_log.shouldWarn())
_log.warn("Test complete, SYMMETRIC NAT! " + status);
}
}
} else if (test.getReceiveCharlieTime() > 0) {
// we received only one message (5) from charlie
@@ -1635,6 +1748,7 @@ class PeerTestManager {
if (!_transport.isSymNatted()) {
test.setAliceIPFromCharlie(test.getAliceIP());
test.setAlicePortFromCharlie(test.getAlicePort());
_transport.markUnreachable(test.getCharlieHash());
}
testComplete();
return;
@@ -1747,7 +1861,26 @@ class PeerTestManager {
return;
}
if (test.getReceiveCharlieTime() <= 0) {
// ??
// ENABLE_SSU2_SYMNAT_TEST only, msg 5 not received
if (_log.shouldWarn())
_log.warn("Got msg 7 w/o msg 5 from Charlie " + from + " on " + test);
// Do additional mismatch checks here, since we don't have msg 5.
InetAddress charlieIP = test.getCharlieIP();
// should always be non-null
if (charlieIP != null) {
// compare msg 4 IP/port to msg 7 IP/port
byte[] oldIP = charlieIP.getAddress();
int oldPort = test.getCharliePort();
if (fromPort != oldPort || !DataHelper.eq(fromIP, oldIP)) {
// If Charlie is confused or symmetric natted, we don't want to become symmetric natted ourselves.
if (_log.shouldWarn())
_log.warn("Charlie IP/port mismatch, msg 4: " + Addresses.toString(oldIP, oldPort) +
", msg 7: " + Addresses.toString(fromIP, fromPort) + " on " + test);
fail();
return;
}
}
// OK, here we go with the snat test
}
// this is our second charlie, yay!
// Do NOT set this here, this is only for msg 5
@@ -1785,18 +1918,59 @@ class PeerTestManager {
Addresses.toString(addrBlockIP, addrBlockPort) + " on " + test);
_context.statManager().addRateData("udp.testBadIP", 1);
// TODO ban charlie or put on a list?
// complete test without setting AliceIPFromCharlie or AlicePortFromCHarlie,
// the result will be OK
} else {
RouterAddress ra = _transport.getCurrentExternalAddress(isIPv6);
if (ra != null) {
if (addrBlockPort != ra.getPort() || !DataHelper.eq(addrBlockIP, ra.getIP())) {
if (_log.shouldWarn())
_log.warn("Charlie said we had a different IP/port: " +
Addresses.toString(addrBlockIP, addrBlockPort) + " on " + test);
// More sanity checks here.
// we compare to the test address,
// however our address may have changed during the test
boolean portok = addrBlockPort == test.getAlicePort();
boolean IPok = DataHelper.eq(addrBlockIP, test.getAliceIP().getAddress());
if (!portok || !IPok) {
if (_log.shouldWarn())
_log.warn("Charlie said we had a different IP/port: " +
Addresses.toString(addrBlockIP, addrBlockPort) + " on " + test);
if (test.getReceiveCharlieTime() > 0) {
// if we did get msg 5, it's almost impossible for us to be symmetric natted.
// It's much more likely that Charlie is symmetric natted.
// However, our temporary IPv6 IP could have changed.
// testComplete() will deal with it
if (IPok) {
// Port different. Charlie probably symmetric natted.
// The result will be OK
// Note that charlie is probably not reachable
_transport.markUnreachable(test.getCharlieHash());
// Reset port so testComplete() will return success.
test.setAlicePortFromCharlie(test.getAlicePort());
// set bad so we don't call externalAddressReceived()
bad = true;
} else if (portok) {
// Our IP changed?
// The result will be SNAT
// we will call externalAddressReceived()
} else {
// Both IP and port changed, don't trust it
// The result will be OK
// Note that charlie is probably not reachable
_transport.markUnreachable(test.getCharlieHash());
// Reset IP and port so testComplete() will return success.
test.setAliceIPFromCharlie(test.getAliceIP());
test.setAlicePortFromCharlie(test.getAlicePort());
// set bad so we don't call externalAddressReceived()
bad = true;
}
} else {
// ENABLE_SSU2_SYMNAT_TEST only, msg 5 not received
// If we did not get msg 5, hard to say which of us is to blame.
// the result will be SNAT
// we will call externalAddressReceived()
// Let UDPTransport figure it out.
}
}
// We already call externalAddressReceived() for every outbound connection from EstablishmentManager
// and we don't do SNAT detection there
// _transport.externalAddressReceived(state.getCharlieHash(), addrBlockIP, addrBlockPort)
// but we can use this also to update our address faster
if (!bad)
_transport.externalAddressReceived(state.getCharlieHash(), addrBlockIP, addrBlockPort);
}
testComplete();
break;