package com.subgraph.orchid.circuits.guards;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import com.subgraph.orchid.ConnectionCache;
import com.subgraph.orchid.Directory;
import com.subgraph.orchid.DirectoryDownloader;
import com.subgraph.orchid.GuardEntry;
import com.subgraph.orchid.Router;
import com.subgraph.orchid.TorConfig;
import com.subgraph.orchid.circuits.path.CircuitNodeChooser;
import com.subgraph.orchid.circuits.path.CircuitNodeChooser.WeightRule;
import com.subgraph.orchid.circuits.path.RouterFilter;
import com.subgraph.orchid.crypto.TorRandom;
public class EntryGuards {
private final static Logger logger = Logger.getLogger(EntryGuards.class.getName());
private final static int MIN_USABLE_GUARDS = 2;
private final static int NUM_ENTRY_GUARDS = 3;
private final TorConfig config;
private final TorRandom random;
private final CircuitNodeChooser nodeChooser;
private final ConnectionCache connectionCache;
private final Directory directory;
private final Set<GuardEntry> pendingProbes;
private final Bridges bridges;
private final Object lock;
private final Executor executor;
public EntryGuards(TorConfig config, ConnectionCache connectionCache, DirectoryDownloader directoryDownloader, Directory directory) {
this.config = config;
this.random = new TorRandom();
this.nodeChooser = new CircuitNodeChooser(config, directory);
this.connectionCache = connectionCache;
this.directory = directory;
this.pendingProbes = new HashSet<GuardEntry>();
this.bridges = new Bridges(config, directoryDownloader);
this.lock = new Object();
this.executor = Executors.newCachedThreadPool();
}
public boolean isUsingBridges() {
return config.getUseBridges();
}
public Router chooseRandomGuard(Set<Router> excluded) throws InterruptedException {
if(config.getUseBridges()) {
return bridges.chooseRandomBridge(excluded);
}
/*
* path-spec 5.
*
* When choosing the first hop of a circuit, Tor chooses at random from among the first
* NumEntryGuards (default 3) usable guards on the list. If there are not at least 2
* usable guards on the list, Tor adds routers until there are, or until there are no
* more usable routers to add.
*/
final List<Router> usableGuards = getMinimumUsableGuards(excluded, MIN_USABLE_GUARDS);
final int n = Math.min(usableGuards.size(), NUM_ENTRY_GUARDS);
return usableGuards.get(random.nextInt(n));
}
private List<Router> getMinimumUsableGuards(Set<Router> excluded, int minSize) throws InterruptedException {
synchronized(lock) {
testStatusOfAllGuards();
while(true) {
List<Router> usableGuards = getUsableGuardRouters(excluded);
if(usableGuards.size() >= minSize) {
return usableGuards;
} else {
maybeChooseNew(usableGuards.size(), minSize, getExcludedForChooseNew(excluded, usableGuards));
}
lock.wait(5000);
}
}
}
void probeConnectionSucceeded(GuardEntry entry) {
synchronized (lock) {
pendingProbes.remove(entry);
if(entry.isAdded()) {
retestProbeSucceeded(entry);
} else {
initialProbeSucceeded(entry);
}
}
}
void probeConnectionFailed(GuardEntry entry) {
synchronized (lock) {
pendingProbes.remove(entry);
if(entry.isAdded()) {
retestProbeFailed(entry);
}
lock.notifyAll();
}
}
/* all methods below called holding 'lock' */
private void retestProbeSucceeded(GuardEntry entry) {
entry.clearDownSince();
}
private void initialProbeSucceeded(GuardEntry entry) {
logger.fine("Probe connection to "+ entry.getRouterForEntry() + " succeeded. Adding it as a new entry guard.");
directory.addGuardEntry(entry);
retestAllUnreachable();
}
private void retestProbeFailed(GuardEntry entry) {
entry.markAsDown();
}
/*
* path-spec 5.
*
* Additionally, Tor retries unreachable guards the first time it adds a new
* guard to the list, since it is possible that the old guards were only marked
* as unreachable because the network was unreachable or down.
*/
private void retestAllUnreachable() {
for(GuardEntry e: directory.getGuardEntries()) {
if(e.getDownSince() != null) {
launchEntryProbe(e);
}
}
}
private void testStatusOfAllGuards() {
for(GuardEntry entry: directory.getGuardEntries()) {
if(isPermanentlyUnlisted(entry) || isExpired(entry)) {
directory.removeGuardEntry(entry);
} else if(needsUnreachableTest(entry)) {
launchEntryProbe(entry);
}
}
}
private List<Router> getUsableGuardRouters(Set<Router> excluded) {
List<Router> usableRouters = new ArrayList<Router>();
for(GuardEntry entry: directory.getGuardEntries()) {
addRouterIfUsableAndNotExcluded(entry, excluded, usableRouters);
}
return usableRouters;
}
private void addRouterIfUsableAndNotExcluded(GuardEntry entry, Set<Router> excluded, List<Router> routers) {
if(entry.testCurrentlyUsable() && entry.getDownSince() == null) {
final Router r = entry.getRouterForEntry();
if(r != null && !excluded.contains(r)) {
routers.add(r);
}
}
}
private Set<Router> getExcludedForChooseNew(Set<Router> excluded, List<Router> usable) {
final Set<Router> set = new HashSet<Router>();
set.addAll(excluded);
set.addAll(usable);
addPendingInitialConnections(set);
return set;
}
private void addPendingInitialConnections(Set<Router> routerSet) {
for(GuardEntry entry: pendingProbes) {
if(!entry.isAdded()) {
Router r = entry.getRouterForEntry();
if(r != null) {
routerSet.add(r);
}
}
}
}
private void maybeChooseNew(int usableSize, int minSize, Set<Router> excluded) {
int sz = usableSize + countPendingInitialProbes();
while(sz < minSize) {
Router newGuard = chooseNewGuard(excluded);
if(newGuard == null) {
logger.warning("Need to add entry guards but no suitable guard routers are available");
return;
}
logger.fine("Testing "+ newGuard + " as a new guard since we only have "+ usableSize + " usable guards");
final GuardEntry entry = directory.createGuardEntryFor(newGuard);
launchEntryProbe(entry);
sz += 1;
}
}
private int countPendingInitialProbes() {
int count = 0;
for(GuardEntry entry: pendingProbes) {
if(!entry.isAdded()) {
count += 1;
}
}
return count;
}
private Router chooseNewGuard(final Set<Router> excluded) {
return nodeChooser.chooseRandomNode(WeightRule.WEIGHT_FOR_GUARD, new RouterFilter() {
public boolean filter(Router router) {
return router.isValid() && router.isPossibleGuard() && router.isRunning() && !excluded.contains(router);
}
});
}
private void launchEntryProbe(GuardEntry entry) {
if(!entry.testCurrentlyUsable() || pendingProbes.contains(entry)) {
return;
}
pendingProbes.add(entry);
executor.execute(new GuardProbeTask(connectionCache, this, entry));
}
/*
* path-spec 5.
*
* If the guard is excluded because of its status in the networkstatuses for
* over 30 days, Tor removes it from the list entirely, preserving order.
*/
private boolean isPermanentlyUnlisted(GuardEntry entry) {
final Date unlistedSince = entry.getUnlistedSince();
if(unlistedSince == null || pendingProbes.contains(entry)) {
return false;
}
final Date now = new Date();
final long unlistedTime = now.getTime() - unlistedSince.getTime();
return unlistedTime > THIRTY_DAYS;
}
/*
* Expire guards after 60 days since creation time.
*/
private boolean isExpired(GuardEntry entry) {
final Date createdAt = entry.getCreatedTime();
final Date now = new Date();
final long createdAgo = now.getTime() - createdAt.getTime();
return createdAgo > SIXTY_DAYS;
}
private boolean needsUnreachableTest(GuardEntry entry) {
final Date downSince = entry.getDownSince();
if(downSince == null || !entry.testCurrentlyUsable()) {
return false;
}
final Date now = new Date();
final Date lastConnect = entry.getLastConnectAttempt();
final long timeDown = now.getTime() - downSince.getTime();
final long timeSinceLastRetest = (lastConnect == null) ? timeDown : (now.getTime() - lastConnect.getTime());
return timeSinceLastRetest > getRetestInterval(timeDown);
}
private final static long ONE_HOUR = hoursToMs(1);
private final static long FOUR_HOURS = hoursToMs(4);
private final static long SIX_HOURS = hoursToMs(6);
private final static long EIGHTEEN_HOURS = hoursToMs(18);
private final static long THIRTYSIX_HOURS = hoursToMs(36);
private final static long THREE_DAYS = daysToMs(3);
private final static long SEVEN_DAYS = daysToMs(7);
private final static long THIRTY_DAYS = daysToMs(30);
private final static long SIXTY_DAYS = daysToMs(60);
private static long hoursToMs(long n) {
return TimeUnit.MILLISECONDS.convert(n, TimeUnit.HOURS);
}
private static long daysToMs(long n) {
return TimeUnit.MILLISECONDS.convert(n, TimeUnit.DAYS);
}
/*
* path-spec 5.
*
* If Tor fails to connect to an otherwise usable guard, it retries
* periodically: every hour for six hours, every 4 hours for 3 days, every
* 18 hours for a week, and every 36 hours thereafter.
*/
private long getRetestInterval(long timeDown) {
if(timeDown < SIX_HOURS) {
return ONE_HOUR;
} else if(timeDown < THREE_DAYS) {
return FOUR_HOURS;
} else if(timeDown < SEVEN_DAYS) {
return EIGHTEEN_HOURS;
} else {
return THIRTYSIX_HOURS;
}
}
}