package net.i2p.router.tunnel.pool;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import net.i2p.I2PAppContext;
import net.i2p.crypto.SHA256Generator;
import net.i2p.crypto.SigType;
import net.i2p.data.DataFormatException;
import net.i2p.data.Hash;
import net.i2p.data.router.RouterInfo;
import net.i2p.router.Router;
import net.i2p.router.RouterContext;
import net.i2p.router.TunnelPoolSettings;
import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade;
import net.i2p.router.util.HashDistance;
import net.i2p.util.Log;
import net.i2p.util.VersionComparator;
/**
* Coordinate the selection of peers to go into a tunnel for one particular
* pool.
*
* Todo: there's nothing non-static in here
*/
public abstract class TunnelPeerSelector {
protected final RouterContext ctx;
protected TunnelPeerSelector(RouterContext context) {
ctx = context;
}
/**
* Which peers should go into the next tunnel for the given settings?
*
* @return ordered list of Hash objects (one per peer) specifying what order
* they should appear in a tunnel (ENDPOINT FIRST). This includes
* the local router in the list. If there are no tunnels or peers
* to build through, and the settings reject 0 hop tunnels, this will
* return null.
*/
public abstract List<Hash> selectPeers(TunnelPoolSettings settings);
/**
* @return randomized number of hops 0-7, not including ourselves
*/
protected int getLength(TunnelPoolSettings settings) {
int length = settings.getLength();
int override = settings.getLengthOverride();
if (override >= 0) {
length = override;
} else if (settings.getLengthVariance() != 0) {
int skew = settings.getLengthVariance();
if (skew > 0)
length += ctx.random().nextInt(skew+1);
else {
skew = 1 - skew;
int off = ctx.random().nextInt(skew);
if (ctx.random().nextBoolean())
length += off;
else
length -= off;
}
}
if (length < 0)
length = 0;
else if (length > 7) // as documented in tunnel.html
length = 7;
/*
if ( (ctx.tunnelManager().getOutboundTunnelCount() <= 0) ||
(ctx.tunnelManager().getFreeTunnelCount() <= 0) ) {
Log log = ctx.logManager().getLog(TunnelPeerSelector.class);
// no tunnels to build tunnels with
if (settings.getAllowZeroHop()) {
if (log.shouldLog(Log.INFO))
log.info("no outbound tunnels or free inbound tunnels, but we do allow zeroHop: " + settings);
return 0;
} else {
if (log.shouldLog(Log.WARN))
log.warn("no outbound tunnels or free inbound tunnels, and we dont allow zeroHop: " + settings);
return -1;
}
}
*/
return length;
}
/**
* For debugging, also possibly for restricted routes?
* Needs analysis and testing
* @return should always be false
*/
protected boolean shouldSelectExplicit(TunnelPoolSettings settings) {
if (settings.isExploratory()) return false;
Properties opts = settings.getUnknownOptions();
if (opts != null) {
String peers = opts.getProperty("explicitPeers");
if (peers == null)
peers = I2PAppContext.getGlobalContext().getProperty("explicitPeers");
if (peers != null)
return true;
}
return false;
}
/**
* For debugging, also possibly for restricted routes?
* Needs analysis and testing
* @return should always be false
*/
protected List<Hash> selectExplicit(TunnelPoolSettings settings, int length) {
String peers = null;
Properties opts = settings.getUnknownOptions();
if (opts != null)
peers = opts.getProperty("explicitPeers");
if (peers == null)
peers = I2PAppContext.getGlobalContext().getProperty("explicitPeers");
Log log = ctx.logManager().getLog(ClientPeerSelector.class);
List<Hash> rv = new ArrayList<Hash>();
StringTokenizer tok = new StringTokenizer(peers, ",");
while (tok.hasMoreTokens()) {
String peerStr = tok.nextToken();
Hash peer = new Hash();
try {
peer.fromBase64(peerStr);
if (ctx.profileOrganizer().isSelectable(peer)) {
rv.add(peer);
} else {
if (log.shouldLog(Log.DEBUG))
log.debug("Explicit peer is not selectable: " + peerStr);
}
} catch (DataFormatException dfe) {
if (log.shouldLog(Log.ERROR))
log.error("Explicit peer is improperly formatted (" + peerStr + ")", dfe);
}
}
int sz = rv.size();
Collections.shuffle(rv, ctx.random());
while (rv.size() > length)
rv.remove(0);
if (log.shouldLog(Log.INFO)) {
StringBuilder buf = new StringBuilder();
if (settings.getDestinationNickname() != null)
buf.append("peers for ").append(settings.getDestinationNickname());
else if (settings.getDestination() != null)
buf.append("peers for ").append(settings.getDestination().toBase64());
else
buf.append("peers for exploratory ");
if (settings.isInbound())
buf.append(" inbound");
else
buf.append(" outbound");
buf.append(" peers: ").append(rv);
buf.append(", out of ").append(sz).append(" (not including self)");
log.info(buf.toString());
}
if (settings.isInbound())
rv.add(0, ctx.routerHash());
else
rv.add(ctx.routerHash());
return rv;
}
/**
* Pick peers that we want to avoid
*/
public Set<Hash> getExclude(boolean isInbound, boolean isExploratory) {
// we may want to update this to skip 'hidden' or 'unreachable' peers, but that
// isn't safe, since they may publish one set of routerInfo to us and another to
// other peers. the defaults for filterUnreachable has always been to return false,
// but might as well make it explicit with a "false &&"
//
// Unreachable peers at the inbound gateway is a major cause of problems.
// Due to a bug in SSU peer testing in 0.6.1.32 and earlier, peers don't know
// if they are unreachable, so the netdb indication won't help much.
// As of 0.6.1.33 we should have lots of unreachables, so enable this for now.
// Also (and more effectively) exclude peers we detect are unreachable,
// this should be much more effective, especially on a router that has been
// up a few hours.
//
// We could just try and exclude them as the inbound gateway but that's harder
// (and even worse for anonymity?).
//
// Defaults changed to true for inbound only in filterUnreachable below.
Set<Hash> peers = new HashSet<Hash>(1);
peers.addAll(ctx.profileOrganizer().selectPeersRecentlyRejecting());
peers.addAll(ctx.tunnelManager().selectPeersInTooManyTunnels());
// if (false && filterUnreachable(ctx, isInbound, isExploratory)) {
if (filterUnreachable(isInbound, isExploratory)) {
// NOTE: filterUnreachable returns true for inbound, false for outbound
// This is the only use for getPeersByCapability? And the whole set of datastructures in PeerManager?
Collection<Hash> caps = ctx.peerManager().getPeersByCapability(Router.CAPABILITY_UNREACHABLE);
if (caps != null)
peers.addAll(caps);
caps = ctx.profileOrganizer().selectPeersLocallyUnreachable();
if (caps != null)
peers.addAll(caps);
}
if (filterSlow(isInbound, isExploratory)) {
// NOTE: filterSlow always returns true
Log log = ctx.logManager().getLog(TunnelPeerSelector.class);
char excl[] = getExcludeCaps(ctx);
if (excl != null) {
FloodfillNetworkDatabaseFacade fac = (FloodfillNetworkDatabaseFacade)ctx.netDb();
List<RouterInfo> known = fac.getKnownRouterData();
if (known != null) {
for (int i = 0; i < known.size(); i++) {
RouterInfo peer = known.get(i);
boolean shouldExclude = shouldExclude(ctx, log, peer, excl);
if (shouldExclude) {
peers.add(peer.getIdentity().calculateHash());
continue;
}
/*
String cap = peer.getCapabilities();
if (cap == null) {
peers.add(peer.getIdentity().calculateHash());
continue;
}
for (int j = 0; j < excl.length; j++) {
if (cap.indexOf(excl[j]) >= 0) {
peers.add(peer.getIdentity().calculateHash());
continue;
}
}
int maxLen = 0;
if (cap.indexOf(FloodfillNetworkDatabaseFacade.CAPACITY_FLOODFILL) >= 0)
maxLen++;
if (cap.indexOf(Router.CAPABILITY_REACHABLE) >= 0)
maxLen++;
if (cap.indexOf(Router.CAPABILITY_UNREACHABLE) >= 0)
maxLen++;
if (cap.length() <= maxLen)
peers.add(peer.getIdentity().calculateHash());
// otherwise, it contains flags we aren't trying to focus on,
// so don't exclude it based on published capacity
if (filterUptime(ctx, isInbound, isExploratory)) {
Properties opts = peer.getOptions();
if (opts != null) {
String val = opts.getProperty("stat_uptime");
long uptimeMs = 0;
if (val != null) {
long factor = 1;
if (val.endsWith("ms")) {
factor = 1;
val = val.substring(0, val.length()-2);
} else if (val.endsWith("s")) {
factor = 1000l;
val = val.substring(0, val.length()-1);
} else if (val.endsWith("m")) {
factor = 60*1000l;
val = val.substring(0, val.length()-1);
} else if (val.endsWith("h")) {
factor = 60*60*1000l;
val = val.substring(0, val.length()-1);
} else if (val.endsWith("d")) {
factor = 24*60*60*1000l;
val = val.substring(0, val.length()-1);
}
try { uptimeMs = Long.parseLong(val); } catch (NumberFormatException nfe) {}
uptimeMs *= factor;
} else {
// not publishing an uptime, so exclude it
peers.add(peer.getIdentity().calculateHash());
continue;
}
long infoAge = ctx.clock().now() - peer.getPublished();
if (infoAge < 0) {
infoAge = 0;
} else if (infoAge > 24*60*60*1000) {
// Only exclude long-unseen peers if we haven't just started up
long DONT_EXCLUDE_PERIOD = 15*60*1000;
if (ctx.router().getUptime() < DONT_EXCLUDE_PERIOD) {
if (log.shouldLog(Log.DEBUG))
log.debug("Not excluding a long-unseen peer, since we just started up.");
} else {
if (log.shouldLog(Log.DEBUG))
log.debug("Excluding a long-unseen peer.");
peers.add(peer.getIdentity().calculateHash());
}
//peers.add(peer.getIdentity().calculateHash());
continue;
} else {
if (infoAge + uptimeMs < 2*60*60*1000) {
// up for less than 2 hours, so exclude it
peers.add(peer.getIdentity().calculateHash());
}
}
} else {
// not publishing stats, so exclude it
peers.add(peer.getIdentity().calculateHash());
continue;
}
}
*/
}
}
/*
for (int i = 0; i < excludeCaps.length(); i++) {
List matches = ctx.peerManager().getPeersByCapability(excludeCaps.charAt(i));
if (log.shouldLog(Log.INFO))
log.info("Filtering out " + matches.size() + " peers with capability " + excludeCaps.charAt(i));
peers.addAll(matches);
}
*/
}
}
return peers;
}
/**
* Pick peers that we want to avoid for the first OB hop or last IB hop.
* This is only filled in if our router sig type is not DSA.
*
* @param isInbound unused
* @return null if none
* @since 0.9.17
*/
protected Set<Hash> getClosestHopExclude(boolean isInbound) {
RouterInfo ri = ctx.router().getRouterInfo();
if (ri == null)
return null;
SigType type = ri.getIdentity().getSigType();
if (type == SigType.DSA_SHA1)
return null;
Set<Hash> rv = new HashSet<Hash>(1024);
FloodfillNetworkDatabaseFacade fac = (FloodfillNetworkDatabaseFacade)ctx.netDb();
List<RouterInfo> known = fac.getKnownRouterData();
if (known != null) {
for (int i = 0; i < known.size(); i++) {
RouterInfo peer = known.get(i);
String v = peer.getVersion();
// RI sigtypes added in 0.9.16
// SSU inbound connection bug fixed in 0.9.17, but it won't bid, so NTCP only,
// no need to check
if (VersionComparator.comp(v, "0.9.16") < 0)
rv.add(peer.getIdentity().calculateHash());
}
}
return rv;
}
/** warning, this is also called by ProfileOrganizer.isSelectable() */
public static boolean shouldExclude(RouterContext ctx, RouterInfo peer) {
Log log = ctx.logManager().getLog(TunnelPeerSelector.class);
return shouldExclude(ctx, log, peer, getExcludeCaps(ctx));
}
private static char[] getExcludeCaps(RouterContext ctx) {
String excludeCaps = ctx.getProperty("router.excludePeerCaps",
String.valueOf(Router.CAPABILITY_BW12));
if (excludeCaps != null) {
char excl[] = excludeCaps.toCharArray();
return excl;
} else {
return null;
}
}
/** 0.7.8 and earlier had major message corruption bugs */
private static final String MIN_VERSION = "0.7.9";
private static boolean shouldExclude(RouterContext ctx, Log log, RouterInfo peer, char excl[]) {
String cap = peer.getCapabilities();
for (int j = 0; j < excl.length; j++) {
if (cap.indexOf(excl[j]) >= 0) {
return true;
}
}
int maxLen = 0;
if (cap.indexOf(FloodfillNetworkDatabaseFacade.CAPABILITY_FLOODFILL) >= 0)
maxLen++;
if (cap.indexOf(Router.CAPABILITY_REACHABLE) >= 0)
maxLen++;
if (cap.indexOf(Router.CAPABILITY_UNREACHABLE) >= 0)
maxLen++;
if (cap.length() <= maxLen)
return true;
// otherwise, it contains flags we aren't trying to focus on,
// so don't exclude it based on published capacity
// minimum version check
String v = peer.getVersion();
if (VersionComparator.comp(v, MIN_VERSION) < 0)
return true;
// uptime is always spoofed to 90m, so just remove all this
/******
String val = peer.getOption("stat_uptime");
if (val != null) {
long uptimeMs = 0;
long factor = 1;
if (val.endsWith("ms")) {
factor = 1;
val = val.substring(0, val.length()-2);
} else if (val.endsWith("s")) {
factor = 1000l;
val = val.substring(0, val.length()-1);
} else if (val.endsWith("m")) {
factor = 60*1000l;
val = val.substring(0, val.length()-1);
} else if (val.endsWith("h")) {
factor = 60*60*1000l;
val = val.substring(0, val.length()-1);
} else if (val.endsWith("d")) {
factor = 24*60*60*1000l;
val = val.substring(0, val.length()-1);
}
try { uptimeMs = Long.parseLong(val); } catch (NumberFormatException nfe) {}
uptimeMs *= factor;
long infoAge = ctx.clock().now() - peer.getPublished();
if (infoAge < 0) {
return false;
} else if (infoAge > 5*24*60*60*1000) {
// Only exclude long-unseen peers if we haven't just started up
if (ctx.router().getUptime() < DONT_EXCLUDE_PERIOD) {
if (log.shouldLog(Log.DEBUG))
log.debug("Not excluding a long-unseen peer, since we just started up.");
return false;
} else {
if (log.shouldLog(Log.DEBUG))
log.debug("Excluding a long-unseen peer.");
return true;
}
} else {
if ( (infoAge + uptimeMs < 90*60*1000) && (ctx.router().getUptime() > DONT_EXCLUDE_PERIOD) ) {
// up for less than 90 min (which is really 1h since an uptime of 1h-2h is published as 90m),
// so exclude it
return true;
} else {
return false;
}
}
} else {
// not publishing an uptime, so exclude it
return true;
}
******/
return false;
}
private static final String PROP_OUTBOUND_EXPLORATORY_EXCLUDE_UNREACHABLE = "router.outboundExploratoryExcludeUnreachable";
private static final String PROP_OUTBOUND_CLIENT_EXCLUDE_UNREACHABLE = "router.outboundClientExcludeUnreachable";
private static final String PROP_INBOUND_EXPLORATORY_EXCLUDE_UNREACHABLE = "router.inboundExploratoryExcludeUnreachable";
private static final String PROP_INBOUND_CLIENT_EXCLUDE_UNREACHABLE = "router.inboundClientExcludeUnreachable";
private static final boolean DEFAULT_OUTBOUND_EXPLORATORY_EXCLUDE_UNREACHABLE = false;
private static final boolean DEFAULT_OUTBOUND_CLIENT_EXCLUDE_UNREACHABLE = false;
// see comments at getExclude() above
private static final boolean DEFAULT_INBOUND_EXPLORATORY_EXCLUDE_UNREACHABLE = true;
private static final boolean DEFAULT_INBOUND_CLIENT_EXCLUDE_UNREACHABLE = true;
/**
* do we want to skip peers who haven't been up for long?
* @return true for inbound, false for outbound, unless configured otherwise
*/
protected boolean filterUnreachable(boolean isInbound, boolean isExploratory) {
if (isExploratory) {
if (isInbound)
return ctx.getProperty(PROP_INBOUND_EXPLORATORY_EXCLUDE_UNREACHABLE, DEFAULT_INBOUND_EXPLORATORY_EXCLUDE_UNREACHABLE);
else
return ctx.getProperty(PROP_OUTBOUND_EXPLORATORY_EXCLUDE_UNREACHABLE, DEFAULT_OUTBOUND_EXPLORATORY_EXCLUDE_UNREACHABLE);
} else {
if (isInbound)
return ctx.getProperty(PROP_INBOUND_CLIENT_EXCLUDE_UNREACHABLE, DEFAULT_INBOUND_CLIENT_EXCLUDE_UNREACHABLE);
else
return ctx.getProperty(PROP_OUTBOUND_CLIENT_EXCLUDE_UNREACHABLE, DEFAULT_OUTBOUND_CLIENT_EXCLUDE_UNREACHABLE);
}
}
private static final String PROP_OUTBOUND_EXPLORATORY_EXCLUDE_SLOW = "router.outboundExploratoryExcludeSlow";
private static final String PROP_OUTBOUND_CLIENT_EXCLUDE_SLOW = "router.outboundClientExcludeSlow";
private static final String PROP_INBOUND_EXPLORATORY_EXCLUDE_SLOW = "router.inboundExploratoryExcludeSlow";
private static final String PROP_INBOUND_CLIENT_EXCLUDE_SLOW = "router.inboundClientExcludeSlow";
/**
* do we want to skip peers that are slow?
* @return true unless configured otherwise
*/
protected boolean filterSlow(boolean isInbound, boolean isExploratory) {
if (isExploratory) {
if (isInbound)
return ctx.getProperty(PROP_INBOUND_EXPLORATORY_EXCLUDE_SLOW, true);
else
return ctx.getProperty(PROP_OUTBOUND_EXPLORATORY_EXCLUDE_SLOW, true);
} else {
if (isInbound)
return ctx.getProperty(PROP_INBOUND_CLIENT_EXCLUDE_SLOW, true);
else
return ctx.getProperty(PROP_OUTBOUND_CLIENT_EXCLUDE_SLOW, true);
}
}
/****
private static final String PROP_OUTBOUND_EXPLORATORY_EXCLUDE_UPTIME = "router.outboundExploratoryExcludeUptime";
private static final String PROP_OUTBOUND_CLIENT_EXCLUDE_UPTIME = "router.outboundClientExcludeUptime";
private static final String PROP_INBOUND_EXPLORATORY_EXCLUDE_UPTIME = "router.inboundExploratoryExcludeUptime";
private static final String PROP_INBOUND_CLIENT_EXCLUDE_UPTIME = "router.inboundClientExcludeUptime";
****/
/**
* do we want to skip peers who haven't been up for long?
* @return true unless configured otherwise
*/
/****
protected boolean filterUptime(boolean isInbound, boolean isExploratory) {
if (isExploratory) {
if (isInbound)
return ctx.getProperty(PROP_INBOUND_EXPLORATORY_EXCLUDE_UPTIME, true);
else
return ctx.getProperty(PROP_OUTBOUND_EXPLORATORY_EXCLUDE_UPTIME, true);
} else {
if (isInbound)
return ctx.getProperty(PROP_INBOUND_CLIENT_EXCLUDE_UPTIME, true);
else
return ctx.getProperty(PROP_OUTBOUND_CLIENT_EXCLUDE_UPTIME, true);
}
}
****/
/** see HashComparator */
protected void orderPeers(List<Hash> rv, Hash hash) {
if (rv.size() > 1)
Collections.sort(rv, new HashComparator(hash));
}
/**
* Implement a deterministic comparison that cannot be predicted by
* others. A naive implementation (using the distance from a random key)
* allows an attacker who runs two routers with hashes far apart
* to maximize his chances of those two routers being at opposite
* ends of a tunnel.
*
* Previous:
* d(l, h) - d(r, h)
*
* Now:
* d((H(l+h), h) - d(H(r+h), h)
*/
private static class HashComparator implements Comparator<Hash>, Serializable {
private final Hash _hash, tmp;
private final byte[] data;
/** not thread safe */
private HashComparator(Hash h) {
_hash = h;
tmp = new Hash(new byte[Hash.HASH_LENGTH]);
data = new byte[2*Hash.HASH_LENGTH];
System.arraycopy(_hash.getData(), 0, data, Hash.HASH_LENGTH, Hash.HASH_LENGTH);
}
public int compare(Hash l, Hash r) {
System.arraycopy(l.getData(), 0, data, 0, Hash.HASH_LENGTH);
byte[] tb = tmp.getData();
// don't use caching version of calculateHash()
SHA256Generator.getInstance().calculateHash(data, 0, 2*Hash.HASH_LENGTH, tb, 0);
BigInteger ll = HashDistance.getDistance(_hash, tmp);
System.arraycopy(r.getData(), 0, data, 0, Hash.HASH_LENGTH);
SHA256Generator.getInstance().calculateHash(data, 0, 2*Hash.HASH_LENGTH, tb, 0);
BigInteger rr = HashDistance.getDistance(_hash, tmp);
return ll.compareTo(rr);
}
}
}