SSU: Add support for IPv6 SSU Peer Testing

(ticket #1752; proposal #126)
In PeerTestManager, this is simply the removal of v6 restrictions,
and the tracking of whether we are testing v4 or v6.
In UDPTransport, track v4 and v6 peer tests separately.
This commit is contained in:
zzz
2016-06-25 21:25:12 +00:00
parent 2506f6b143
commit 2c3311b471
5 changed files with 170 additions and 76 deletions

View File

@ -1,3 +1,6 @@
2016-06-26 zzz
* SSU peer testing: Add implementation (ticket #1752; proposal #126)
2016-06-22 zzz
* SSU peer testing:
- Forget the test and don't keep retransmitting to Charlie

View File

@ -18,7 +18,7 @@ public class RouterVersion {
/** deprecated */
public final static String ID = "Monotone";
public final static String VERSION = CoreVersion.VERSION;
public final static long BUILD = 2;
public final static long BUILD = 3;
/** for example "-test" */
public final static String EXTRA = "";

View File

@ -1,6 +1,7 @@
package net.i2p.router.transport.udp;
import java.net.InetAddress;
import java.net.Inet6Address;
import java.net.UnknownHostException;
import java.util.Map;
import java.util.Queue;
@ -187,7 +188,7 @@ class PeerTestManager {
_log.warn("Not running test with Bob too close to us " + bobIP);
return;
}
PeerTestState test = new PeerTestState(ALICE,
PeerTestState test = new PeerTestState(ALICE, bobIP instanceof Inet6Address,
_context.random().nextLong(MAX_NONCE),
_context.clock().now());
test.setBobIP(bobIP);
@ -315,7 +316,9 @@ class PeerTestManager {
// The reply is from Bob
int ipSize = testInfo.readIPSize();
if (ipSize != 4) {
boolean expectV6 = test.isIPv6();
if ((!expectV6 && ipSize != 4) ||
(expectV6 && ipSize != 16)) {
// There appears to be a bug where Bob is sending us a zero-length IP.
// We could proceed without setting the IP, but then when Charlie
// sends us his message, we will think we are behind a symmetric NAT
@ -364,7 +367,7 @@ class PeerTestManager {
// why are we doing this instead of calling testComplete() ?
_currentTestComplete = true;
_context.statManager().addRateData("udp.statusKnownCharlie", 1);
honorStatus(Status.UNKNOWN);
honorStatus(Status.UNKNOWN, test.isIPv6());
_currentTest = null;
return;
}
@ -377,8 +380,11 @@ class PeerTestManager {
throw new UnknownHostException("port 0");
test.setAlicePortFromCharlie(testPort);
byte ip[] = new byte[testInfo.readIPSize()];
if (ip.length != 4)
throw new UnknownHostException("not IPv4");
int ipSize = ip.length;
boolean expectV6 = test.isIPv6();
if ((!expectV6 && ipSize != 4) ||
(expectV6 && ipSize != 16))
throw new UnknownHostException("bad sz - expect v6? " + expectV6 + " act sz: " + ipSize);
testInfo.readIP(ip, 0);
InetAddress addr = InetAddress.getByAddress(ip);
test.setAliceIPFromCharlie(addr);
@ -442,22 +448,24 @@ class PeerTestManager {
// return;
// }
boolean isIPv6 = test.isIPv6();
Status status;
if (test.getAlicePortFromCharlie() > 0) {
// we received a second message from charlie
if ( (test.getAlicePort() == test.getAlicePortFromCharlie()) &&
(test.getAliceIP() != null) && (test.getAliceIPFromCharlie() != null) &&
(test.getAliceIP().equals(test.getAliceIPFromCharlie())) ) {
status = Status.IPV4_OK_IPV6_UNKNOWN;
status = isIPv6 ? Status.IPV4_UNKNOWN_IPV6_OK : Status.IPV4_OK_IPV6_UNKNOWN;
} else {
status = Status.IPV4_SNAT_IPV6_UNKNOWN;
// we don't have a SNAT state for IPv6
status = isIPv6 ? Status.IPV4_UNKNOWN_IPV6_FIREWALLED : Status.IPV4_SNAT_IPV6_UNKNOWN;
}
} else if (test.getReceiveCharlieTime() > 0) {
// we received only one message from charlie
status = Status.UNKNOWN;
} else if (test.getReceiveBobTime() > 0) {
// we received a message from bob but no messages from charlie
status = Status.IPV4_FIREWALLED_IPV6_UNKNOWN;
status = isIPv6 ? Status.IPV4_UNKNOWN_IPV6_FIREWALLED : Status.IPV4_FIREWALLED_IPV6_UNKNOWN;
} else {
// we never received anything from bob - he is either down,
// ignoring us, or unable to get a Charlie to respond
@ -467,7 +475,7 @@ class PeerTestManager {
if (_log.shouldLog(Log.INFO))
_log.info("Test complete: " + test);
honorStatus(status);
honorStatus(status, isIPv6);
if (forgetTest)
_currentTest = null;
}
@ -476,11 +484,12 @@ class PeerTestManager {
* Depending upon the status, fire off different events (using received port/ip/etc as
* necessary).
*
* @param isIPv6 Is the change an IPv6 change?
*/
private void honorStatus(Status status) {
private void honorStatus(Status status, boolean isIPv6) {
if (_log.shouldLog(Log.INFO))
_log.info("Test results: status = " + status);
_transport.setReachabilityStatus(status);
_log.info("Test results (IPv6? " + isIPv6 + "): status = " + status);
_transport.setReachabilityStatus(status, isIPv6);
}
/**
@ -518,7 +527,7 @@ class PeerTestManager {
if ((testPort > 0 && (!TransportUtil.isValidPort(testPort))) ||
(testIP != null &&
((!_transport.isValid(testIP)) ||
testIP.length != 4 ||
(testIP.length != 4 && testIP.length != 16) ||
_context.blocklist().isBlocklisted(testIP)))) {
// spoof check, and don't respond to privileged ports
if (_log.shouldLog(Log.WARN))
@ -645,10 +654,11 @@ class PeerTestManager {
*/
private void receiveFromBobAsCharlie(RemoteHostId from, UDPPacketReader.PeerTestReader testInfo, long nonce, PeerTestState state) {
long now = _context.clock().now();
int sz = testInfo.readIPSize();
boolean isNew = false;
if (state == null) {
isNew = true;
state = new PeerTestState(CHARLIE, nonce, now);
state = new PeerTestState(CHARLIE, sz == 16, nonce, now);
} else {
if (state.getReceiveBobTime() > now - (RESEND_TIMEOUT / 2)) {
if (_log.shouldLog(Log.WARN))
@ -658,12 +668,13 @@ class PeerTestManager {
}
// TODO should only do most of this if isNew
int sz = testInfo.readIPSize();
byte aliceIPData[] = new byte[sz];
try {
testInfo.readIP(aliceIPData, 0);
if (sz != 4)
throw new UnknownHostException("not IPv4");
boolean expectV6 = state.isIPv6();
if ((!expectV6 && sz != 4) ||
(expectV6 && sz != 16))
throw new UnknownHostException("bad sz - expect v6? " + expectV6 + " act sz: " + sz);
int alicePort = testInfo.readPort();
if (alicePort == 0)
throw new UnknownHostException("port 0");
@ -731,22 +742,27 @@ class PeerTestManager {
// we are Bob, so pick a (potentially) Charlie and send Charlie Alice's info
PeerState charlie;
RouterInfo charlieInfo = null;
int sz = from.getIP().length;
boolean isIPv6 = sz == 16;
if (state == null) { // pick a new charlie
if (from.getIP().length != 4) {
if (_log.shouldLog(Log.WARN))
_log.warn("PeerTest over IPv6 from Alice as Bob? " + from);
return;
}
charlie = _transport.pickTestPeer(CHARLIE, from);
//if (from.getIP().length != 4) {
// if (_log.shouldLog(Log.WARN))
// _log.warn("PeerTest over IPv6 from Alice as Bob? " + from);
// return;
//}
charlie = _transport.pickTestPeer(CHARLIE, isIPv6, from);
} else {
charlie = _transport.getPeerState(new RemoteHostId(state.getCharlieIP().getAddress(), state.getCharliePort()));
}
if (charlie != null)
charlieInfo = _context.netDb().lookupRouterInfoLocally(charlie.getRemotePeer());
if ( (charlie == null) || (charlieInfo == null) ) {
if (charlie == null) {
if (_log.shouldLog(Log.WARN))
_log.warn("Unable to pick a charlie");
_log.warn("Unable to pick a charlie (no peer), IPv6? " + isIPv6);
return;
}
charlieInfo = _context.netDb().lookupRouterInfoLocally(charlie.getRemotePeer());
if (charlieInfo == null) {
if (_log.shouldLog(Log.WARN))
_log.warn("Unable to pick a charlie (no RI), IPv6? " + isIPv6);
return;
}
@ -761,14 +777,14 @@ class PeerTestManager {
RouterAddress raddr = _transport.getTargetAddress(charlieInfo);
if (raddr == null) {
if (_log.shouldLog(Log.WARN))
_log.warn("Unable to pick a charlie");
_log.warn("Unable to pick a charlie (no addr), IPv6? " + isIPv6);
return;
}
UDPAddress addr = new UDPAddress(raddr);
byte[] ikey = addr.getIntroKey();
if (ikey == null) {
if (_log.shouldLog(Log.WARN))
_log.warn("Unable to pick a charlie");
_log.warn("Unable to pick a charlie (no ikey), IPv6? " + isIPv6);
return;
}
SessionKey charlieIntroKey = new SessionKey(ikey);
@ -780,7 +796,7 @@ class PeerTestManager {
boolean isNew = false;
if (state == null) {
isNew = true;
state = new PeerTestState(BOB, nonce, now);
state = new PeerTestState(BOB, isIPv6, nonce, now);
} else {
if (state.getReceiveAliceTime() > now - (RESEND_TIMEOUT / 2)) {
if (_log.shouldLog(Log.WARN))

View File

@ -12,6 +12,7 @@ import net.i2p.data.SessionKey;
class PeerTestState {
private final long _testNonce;
private final Role _ourRole;
private final boolean _isIPv6;
private InetAddress _aliceIP;
private int _alicePort;
private InetAddress _bobIP;
@ -33,8 +34,9 @@ class PeerTestState {
public enum Role {ALICE, BOB, CHARLIE};
public PeerTestState(Role role, long nonce, long now) {
public PeerTestState(Role role, boolean isIPv6, long nonce, long now) {
_ourRole = role;
_isIPv6 = isIPv6;
_testNonce = nonce;
_beginTime = now;
}
@ -44,6 +46,12 @@ class PeerTestState {
/** Are we Alice, bob, or Charlie. */
public Role getOurRole() { return _ourRole; }
/**
* Is this an IPv6 test?
* @since 0.9.27
*/
public boolean isIPv6() { return _isIPv6; }
/**
* If we are Alice, this will contain the IP that Bob says we
* can be reached at - the IP Charlie says we can be reached

View File

@ -95,7 +95,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
* Do we have a public IPv6 address?
* TODO periodically update via CSFI.NetMonitor?
*/
private boolean _haveIPv6Address;
private volatile boolean _haveIPv6Address;
private long _lastInboundIPv6;
/** do we need to rebuild our external router address asap? */
@ -219,6 +219,12 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
*/
private static final String MIN_SIGTYPE_VERSION = "0.9.17";
/**
* IPv6 Peer Testing supported
*/
///////////////////////////////////// Testing only, set to 0.9.27 before release ////////////////////////////////////////
private static final String MIN_V6_PEER_TEST_VERSION = "0.9.26";
public UDPTransport(RouterContext ctx, DHSessionKeyBuilder.Factory dh) {
super(ctx);
@ -535,7 +541,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
// change to firewalled maybe? but we don't have any test to restore
// a v6 address after it's removed.
_lastInboundIPv6 = _context.clock().now();
setReachabilityStatus(Status.IPV4_UNKNOWN_IPV6_OK);
if (!isIPv6Firewalled())
setReachabilityStatus(Status.IPV4_UNKNOWN_IPV6_OK, true);
} else {
if (!isIPv4Firewalled())
setReachabilityStatus(Status.IPV4_OK_IPV6_UNKNOWN);
@ -546,7 +553,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
for (InetAddress ia : bindToAddrs) {
if (ia.getAddress().length == 16) {
_lastInboundIPv6 = _context.clock().now();
setReachabilityStatus(Status.IPV4_UNKNOWN_IPV6_OK);
if (!isIPv6Firewalled())
setReachabilityStatus(Status.IPV4_UNKNOWN_IPV6_OK, true);
} else {
if (!isIPv4Firewalled())
setReachabilityStatus(Status.IPV4_OK_IPV6_UNKNOWN);
@ -763,12 +771,10 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
void inboundConnectionReceived(boolean isIPv6) {
if (isIPv6) {
// FIXME we need to check and time out after an hour of no inbound ipv6,
// change to firewalled maybe? but we don't have any test to restore
// a v6 address after it's removed.
_lastInboundIPv6 = _context.clock().now();
if (_currentOurV6Address != null)
setReachabilityStatus(Status.IPV4_UNKNOWN_IPV6_OK);
// former workaround for lack of IPv6 peer testing
//if (_currentOurV6Address != null)
// setReachabilityStatus(Status.IPV4_UNKNOWN_IPV6_OK, true);
} else {
// Introduced connections are still inbound, this is not evidence
// that we are not firewalled.
@ -845,7 +851,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
} else if (ip.length == 16) {
// TODO should we set both to unknown and wait for an inbound v6 conn,
// since there's no v6 testing?
setReachabilityStatus(Status.IPV4_UNKNOWN_IPV6_OK);
if (!isIPv6Firewalled())
setReachabilityStatus(Status.IPV4_UNKNOWN_IPV6_OK, true);
}
}
}
@ -1007,8 +1014,9 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
}
if (fireTest) {
// always false, commented out above
_context.statManager().addRateData("udp.addressTestInsteadOfUpdate", 1);
_testEvent.forceRunImmediately();
_testEvent.forceRunImmediately(isIPv6);
} else if (updated) {
_context.statManager().addRateData("udp.addressUpdated", 1);
Map<String, String> changes = new HashMap<String, String>();
@ -1061,7 +1069,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
}
// deadlock thru here ticket #1699
_context.router().rebuildRouterInfo();
_testEvent.forceRunImmediately();
_testEvent.forceRunImmediately(isIPv6);
}
return updated;
}
@ -1329,7 +1337,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
status != Status.IPV4_DISABLED_IPV6_FIREWALLED &&
status != Status.DISCONNECTED &&
_reachabilityStatusUnchanged < 7) {
_testEvent.forceRunSoon();
// IPv4 only for now
_testEvent.forceRunSoon(false);
}
}
return true;
@ -3076,17 +3085,31 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
}
}
void setReachabilityStatus(Status status) {
/**
* IPv4 only
*/
private void setReachabilityStatus(Status status) {
setReachabilityStatus(status, false);
}
/**
* @since 0.9.27
* @param isIPv6 Is the change an IPv6 change?
*/
void setReachabilityStatus(Status status, boolean isIPv6) {
synchronized (_rebuildLock) {
locked_setReachabilityStatus(status);
locked_setReachabilityStatus(status, isIPv6);
}
}
private void locked_setReachabilityStatus(Status newStatus) {
/**
* @param isIPv6 Is the change an IPv6 change?
*/
private void locked_setReachabilityStatus(Status newStatus, boolean isIPv6) {
Status old = _reachabilityStatus;
// merge new status into old
Status status = Status.merge(old, newStatus);
_testEvent.setLastTested();
_testEvent.setLastTested(isIPv6);
// now modify if we are IPv6 only
TransportUtil.IPv6Config config = getIPv6Config();
if (config == IPV6_ONLY) {
@ -3099,7 +3122,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
}
if (status != Status.UNKNOWN) {
// now modify if we have no IPv6 address
if (_currentOurV6Address == null) {
if (_currentOurV6Address == null && !_haveIPv6Address) {
if (status == Status.IPV4_OK_IPV6_UNKNOWN)
status = Status.OK;
else if (status == Status.IPV4_FIREWALLED_IPV6_UNKNOWN)
@ -3175,19 +3198,21 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
/**
* Pick a Bob (if we are Alice) or a Charlie (if we are Bob).
*
* For Bob (as called from PeerTestEvent below), returns an established IPv4 peer.
* For Bob (as called from PeerTestEvent below), returns an established IPv4/v6 peer.
* While the protocol allows Alice to select an unestablished Bob, we don't support that.
*
* For Charlie (as called from PeerTestManager), returns an established IPv4 or IPv6 peer.
* (doesn't matter how Bob and Charlie communicate)
*
* Any returned peer must advertise an IPv4 address to prove it is IPv4-capable.
* Ditto for v6.
*
* @param peerRole BOB or CHARLIE only
* @param peerRole The role of the peer we are looking for, BOB or CHARLIE only (NOT our role)
* @param isIPv6 true to get a v6-capable peer back
* @param dontInclude may be null
* @return IPv4 peer or null
*/
PeerState pickTestPeer(PeerTestState.Role peerRole, RemoteHostId dontInclude) {
PeerState pickTestPeer(PeerTestState.Role peerRole, boolean isIPv6, RemoteHostId dontInclude) {
if (peerRole == ALICE)
throw new IllegalArgumentException();
List<PeerState> peers = new ArrayList<PeerState>(_peersByIdent.values());
@ -3195,20 +3220,31 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
PeerState peer = iter.next();
if ( (dontInclude != null) && (dontInclude.equals(peer.getRemoteHostId())) )
continue;
// enforce IPv4 connection if we are ALICE looking for a BOB
// enforce IPv4/v6 connection if we are ALICE looking for a BOB
byte[] ip = peer.getRemoteIP();
if (peerRole == BOB && ip.length != 4)
if (peerRole == BOB) {
if ((!isIPv6 && ip.length != 4) ||
(isIPv6 && ip.length != 16))
continue;
// enforce IPv4 advertised for all
}
// enforce IPv4/v6 advertised for all
RouterInfo peerInfo = _context.netDb().lookupRouterInfoLocally(peer.getRemotePeer());
if (peerInfo == null)
continue;
if (isIPv6) {
String v = peerInfo.getVersion();
if (VersionComparator.comp(v, MIN_SIGTYPE_VERSION) < 0)
continue;
}
ip = null;
List<RouterAddress> addrs = getTargetAddresses(peerInfo);
for (RouterAddress addr : addrs) {
ip = addr.getIP();
if (ip != null && ip.length == 4)
if (ip != null) {
if ((!isIPv6 && ip.length != 4) ||
(isIPv6 && ip.length != 16))
break;
}
}
if (ip == null)
continue;
@ -3221,7 +3257,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
private boolean shouldTest() {
return ! (_context.router().isHidden() ||
isIPv4Firewalled());
(isIPv4Firewalled() && isIPv6Firewalled()));
//String val = _context.getProperty(PROP_SHOULD_TEST);
//return ( (val != null) && ("true".equals(val)) );
}
@ -3233,47 +3269,67 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
private boolean _alive;
/** when did we last test our reachability */
private final AtomicLong _lastTested = new AtomicLong();
private boolean _forceRun;
private final AtomicLong _lastTestedV6 = new AtomicLong();
private static final int NO_FORCE = 0, FORCE_IPV4 = 1, FORCE_IPV6 = 2;
private int _forceRun;
PeerTestEvent() {
super(_context.simpleTimer2());
}
public synchronized void timeReached() {
// just for IPv6 for now
if (shouldTest()) {
long sinceRun = _context.clock().now() - _lastTested.get();
if ( (_forceRun && sinceRun >= MIN_TEST_FREQUENCY) || (sinceRun >= TEST_FREQUENCY) ) {
locked_runTest();
long now = _context.clock().now();
long sinceRunV4 = now - _lastTested.get();
long sinceRunV6 = now - _lastTestedV6.get();
if (_forceRun == FORCE_IPV4 && sinceRunV4 >= MIN_TEST_FREQUENCY) {
locked_runTest(false);
} else if (_haveIPv6Address &&_forceRun == FORCE_IPV6 && sinceRunV6 >= MIN_TEST_FREQUENCY) {
locked_runTest(true);
} else if (sinceRunV4 >= TEST_FREQUENCY) {
locked_runTest(false);
} else if (_haveIPv6Address && sinceRunV6 >= TEST_FREQUENCY) {
locked_runTest(true);
} else {
if (_log.shouldLog(Log.INFO))
_log.info("PTE timeReached(), no test run, last v4 test: " + new java.util.Date(_lastTested.get()) +
" last v6 test: " + new java.util.Date(_lastTestedV6.get()));
}
}
if (_alive) {
long delay = (TEST_FREQUENCY / 2) + _context.random().nextInt(TEST_FREQUENCY);
// if we have 2 addresses, give IPv6 a chance also
if (_haveIPv6Address)
delay /= 2;
schedule(delay);
}
}
private void locked_runTest() {
PeerState bob = pickTestPeer(BOB, null);
private void locked_runTest(boolean isIPv6) {
PeerState bob = pickTestPeer(BOB, isIPv6, null);
if (bob != null) {
if (_log.shouldLog(Log.INFO))
_log.info("Running periodic test with bob = " + bob);
_testManager.runTest(bob.getRemoteIPAddress(), bob.getRemotePort(), bob.getCurrentCipherKey(), bob.getCurrentMACKey());
setLastTested();
setLastTested(isIPv6);
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("Unable to run a periodic test, as there are no peers with the capacity required");
_log.warn("Unable to run peer test, no peers available - v6? " + isIPv6);
}
_forceRun = false;
_forceRun = NO_FORCE;
}
/**
* Run within the next 45 seconds at the latest
* @since 0.9.13
*/
public synchronized void forceRunSoon() {
if (isIPv4Firewalled())
public synchronized void forceRunSoon(boolean isIPv6) {
if (!isIPv6 && isIPv4Firewalled())
return;
_forceRun = true;
if (isIPv6 && isIPv6Firewalled())
return;
_forceRun = isIPv6 ? FORCE_IPV6 : FORCE_IPV4;
reschedule(MIN_TEST_FREQUENCY);
}
@ -3282,11 +3338,16 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
* Run within the next 5 seconds at the latest
* @since 0.9.13
*/
public synchronized void forceRunImmediately() {
if (isIPv4Firewalled())
public synchronized void forceRunImmediately(boolean isIPv6) {
if (!isIPv6 && isIPv4Firewalled())
return;
_lastTested.set(0);
_forceRun = true;
if (isIPv6 && isIPv6Firewalled())
return;
if (isIPv6)
_lastTestedV6.set(0);
else
_lastTested.set(0);
_forceRun = isIPv6 ? FORCE_IPV6 : FORCE_IPV4;
reschedule(5*1000);
}
@ -3304,9 +3365,15 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
* Set the last-tested timer to now
* @since 0.9.13
*/
public void setLastTested() {
public void setLastTested(boolean isIPv6) {
// do not synchronize - deadlock with PeerTestManager
_lastTested.set(_context.clock().now());
long now = _context.clock().now();
if (isIPv6)
_lastTestedV6.set(now);
else
_lastTested.set(now);
if (_log.shouldLog(Log.DEBUG))
_log.debug("PTE.setLastTested() - v6? " + isIPv6, new Exception());
}
}