package com.subgraph.orchid.circuits;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import com.subgraph.orchid.Circuit;
import com.subgraph.orchid.CircuitBuildHandler;
import com.subgraph.orchid.CircuitManager;
import com.subgraph.orchid.CircuitNode;
import com.subgraph.orchid.Connection;
import com.subgraph.orchid.ConnectionCache;
import com.subgraph.orchid.ConsensusDocument;
import com.subgraph.orchid.Directory;
import com.subgraph.orchid.DirectoryCircuit;
import com.subgraph.orchid.ExitCircuit;
import com.subgraph.orchid.InternalCircuit;
import com.subgraph.orchid.OpenFailedException;
import com.subgraph.orchid.Router;
import com.subgraph.orchid.Stream;
import com.subgraph.orchid.StreamConnectFailedException;
import com.subgraph.orchid.Tor;
import com.subgraph.orchid.TorConfig;
import com.subgraph.orchid.circuits.guards.EntryGuards;
import com.subgraph.orchid.circuits.hs.HiddenServiceManager;
import com.subgraph.orchid.circuits.path.CircuitPathChooser;
import com.subgraph.orchid.crypto.TorRandom;
import com.subgraph.orchid.dashboard.DashboardRenderable;
import com.subgraph.orchid.dashboard.DashboardRenderer;
import com.subgraph.orchid.data.IPv4Address;
import com.subgraph.orchid.directory.downloader.DirectoryDownloaderImpl;
public class CircuitManagerImpl implements CircuitManager, DashboardRenderable {
private final static int OPEN_DIRECTORY_STREAM_RETRY_COUNT = 5;
private final static int OPEN_DIRECTORY_STREAM_TIMEOUT = 10 * 1000;
interface CircuitFilter {
boolean filter(Circuit circuit);
}
private final TorConfig config;
private final Directory directory;
private final ConnectionCache connectionCache;
private final Set<CircuitImpl> activeCircuits;
private final Queue<InternalCircuit> cleanInternalCircuits;
private int requestedInternalCircuitCount = 0;
private int pendingInternalCircuitCount = 0;
private final TorRandom random;
private final PendingExitStreams pendingExitStreams;
private final ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
private final CircuitCreationTask circuitCreationTask;
private final TorInitializationTracker initializationTracker;
private final CircuitPathChooser pathChooser;
private final HiddenServiceManager hiddenServiceManager;
public CircuitManagerImpl(TorConfig config, DirectoryDownloaderImpl directoryDownloader, Directory directory, ConnectionCache connectionCache, TorInitializationTracker initializationTracker) {
this.config = config;
this.directory = directory;
this.connectionCache = connectionCache;
this.pathChooser = CircuitPathChooser.create(config, directory);
if(config.getUseEntryGuards() || config.getUseBridges()) {
this.pathChooser.enableEntryGuards(new EntryGuards(config, connectionCache, directoryDownloader, directory));
}
this.pendingExitStreams = new PendingExitStreams(config);
this.circuitCreationTask = new CircuitCreationTask(config, directory, connectionCache, pathChooser, this, initializationTracker);
this.activeCircuits = new HashSet<CircuitImpl>();
this.cleanInternalCircuits = new LinkedList<InternalCircuit>();
this.random = new TorRandom();
this.initializationTracker = initializationTracker;
this.hiddenServiceManager = new HiddenServiceManager(config, directory, this);
directoryDownloader.setCircuitManager(this);
}
public void startBuildingCircuits() {
scheduledExecutor.scheduleAtFixedRate(circuitCreationTask, 0, 1000, TimeUnit.MILLISECONDS);
}
public synchronized void stopBuildingCircuits(boolean killCircuits) {
scheduledExecutor.shutdownNow();
if(killCircuits) {
List<CircuitImpl> circuits = new ArrayList<CircuitImpl>(activeCircuits);
for(CircuitImpl c: circuits) {
c.destroyCircuit();
}
}
}
public ExitCircuit createNewExitCircuit(Router exitRouter) {
return CircuitImpl.createExitCircuit(this, exitRouter);
}
void addActiveCircuit(CircuitImpl circuit) {
synchronized (activeCircuits) {
activeCircuits.add(circuit);
activeCircuits.notifyAll();
}
}
void removeActiveCircuit(CircuitImpl circuit) {
synchronized (activeCircuits) {
activeCircuits.remove(circuit);
}
}
synchronized int getActiveCircuitCount() {
return activeCircuits.size();
}
Set<Circuit> getPendingCircuits() {
return getCircuitsByFilter(new CircuitFilter() {
public boolean filter(Circuit circuit) {
return circuit.isPending();
}
});
}
synchronized int getPendingCircuitCount() {
return getPendingCircuits().size();
}
Set<Circuit> getCircuitsByFilter(CircuitFilter filter) {
final Set<Circuit> result = new HashSet<Circuit>();
synchronized (activeCircuits) {
for(CircuitImpl c: activeCircuits) {
if(filter == null || filter.filter(c)) {
result.add(c);
}
}
}
return result;
}
List<ExitCircuit> getRandomlyOrderedListOfExitCircuits() {
final Set<Circuit> notDirectory = getCircuitsByFilter(new CircuitFilter() {
public boolean filter(Circuit circuit) {
final boolean exitType = circuit instanceof ExitCircuit;
return exitType && !circuit.isMarkedForClose() && circuit.isConnected();
}
});
final ArrayList<ExitCircuit> ac = new ArrayList<ExitCircuit>();
for(Circuit c: notDirectory) {
if(c instanceof ExitCircuit) {
ac.add((ExitCircuit) c);
}
}
final int sz = ac.size();
for(int i = 0; i < sz; i++) {
final ExitCircuit tmp = ac.get(i);
final int swapIdx = random.nextInt(sz);
ac.set(i, ac.get(swapIdx));
ac.set(swapIdx, tmp);
}
return ac;
}
public Stream openExitStreamTo(String hostname, int port)
throws InterruptedException, TimeoutException, OpenFailedException {
if(hostname.endsWith(".onion")) {
return hiddenServiceManager.getStreamTo(hostname, port);
}
validateHostname(hostname);
circuitCreationTask.predictPort(port);
return pendingExitStreams.openExitStream(hostname, port);
}
private void validateHostname(String hostname) throws OpenFailedException {
maybeRejectInternalAddress(hostname);
if(hostname.toLowerCase().endsWith(".onion")) {
throw new OpenFailedException("Hidden services not supported");
} else if(hostname.toLowerCase().endsWith(".exit")) {
throw new OpenFailedException(".exit addresses are not supported");
}
}
private void maybeRejectInternalAddress(String hostname) throws OpenFailedException {
if(IPv4Address.isValidIPv4AddressString(hostname)) {
maybeRejectInternalAddress(IPv4Address.createFromString(hostname));
}
}
private void maybeRejectInternalAddress(IPv4Address address) throws OpenFailedException {
final InetAddress inetAddress = address.toInetAddress();
if(inetAddress.isSiteLocalAddress() && config.getClientRejectInternalAddress()) {
throw new OpenFailedException("Rejecting stream target with internal address: "+ address);
}
}
public Stream openExitStreamTo(IPv4Address address, int port)
throws InterruptedException, TimeoutException, OpenFailedException {
maybeRejectInternalAddress(address);
circuitCreationTask.predictPort(port);
return pendingExitStreams.openExitStream(address, port);
}
public List<StreamExitRequest> getPendingExitStreams() {
return pendingExitStreams.getUnreservedPendingRequests();
}
public Stream openDirectoryStream() throws OpenFailedException, InterruptedException, TimeoutException {
return openDirectoryStream(0);
}
public Stream openDirectoryStream(int purpose) throws OpenFailedException, InterruptedException {
final int requestEventCode = purposeToEventCode(purpose, false);
final int loadingEventCode = purposeToEventCode(purpose, true);
int failCount = 0;
while(failCount < OPEN_DIRECTORY_STREAM_RETRY_COUNT) {
final DirectoryCircuit circuit = openDirectoryCircuit();
if(requestEventCode > 0) {
initializationTracker.notifyEvent(requestEventCode);
}
try {
final Stream stream = circuit.openDirectoryStream(OPEN_DIRECTORY_STREAM_TIMEOUT, true);
if(loadingEventCode > 0) {
initializationTracker.notifyEvent(loadingEventCode);
}
return stream;
} catch (StreamConnectFailedException e) {
circuit.markForClose();
failCount += 1;
} catch (TimeoutException e) {
circuit.markForClose();
}
}
throw new OpenFailedException("Retry count exceeded opening directory stream");
}
public DirectoryCircuit openDirectoryCircuit() throws OpenFailedException {
int failCount = 0;
while(failCount < OPEN_DIRECTORY_STREAM_RETRY_COUNT) {
final DirectoryCircuit circuit = CircuitImpl.createDirectoryCircuit(this);
if(tryOpenCircuit(circuit, true, true)) {
return circuit;
}
failCount += 1;
}
throw new OpenFailedException("Could not create circuit for directory stream");
}
private int purposeToEventCode(int purpose, boolean getLoadingEvent) {
switch(purpose) {
case DIRECTORY_PURPOSE_CONSENSUS:
return getLoadingEvent ? Tor.BOOTSTRAP_STATUS_LOADING_STATUS : Tor.BOOTSTRAP_STATUS_REQUESTING_STATUS;
case DIRECTORY_PURPOSE_CERTIFICATES:
return getLoadingEvent ? Tor.BOOTSTRAP_STATUS_LOADING_KEYS : Tor.BOOTSTRAP_STATUS_REQUESTING_KEYS;
case DIRECTORY_PURPOSE_DESCRIPTORS:
return getLoadingEvent ? Tor.BOOTSTRAP_STATUS_LOADING_DESCRIPTORS : Tor.BOOTSTRAP_STATUS_REQUESTING_DESCRIPTORS;
default:
return 0;
}
}
private static class DirectoryCircuitResult implements CircuitBuildHandler {
private boolean isFailed;
public void connectionCompleted(Connection connection) {}
public void nodeAdded(CircuitNode node) {}
public void circuitBuildCompleted(Circuit circuit) {}
public void connectionFailed(String reason) {
isFailed = true;
}
public void circuitBuildFailed(String reason) {
isFailed = true;
}
boolean isSuccessful() {
return !isFailed;
}
}
public void dashboardRender(DashboardRenderer renderer, PrintWriter writer, int flags) throws IOException {
if((flags & DASHBOARD_CIRCUITS) == 0) {
return;
}
renderer.renderComponent(writer, flags, connectionCache);
renderer.renderComponent(writer, flags, circuitCreationTask.getCircuitPredictor());
writer.println("[Circuit Manager]");
writer.println();
for(Circuit c: getCircuitsByFilter(null)) {
renderer.renderComponent(writer, flags, c);
}
}
public InternalCircuit getCleanInternalCircuit() throws InterruptedException {
synchronized(cleanInternalCircuits) {
try {
requestedInternalCircuitCount += 1;
while(cleanInternalCircuits.isEmpty()) {
cleanInternalCircuits.wait();
}
return cleanInternalCircuits.remove();
} finally {
requestedInternalCircuitCount -= 1;
}
}
}
int getNeededCleanCircuitCount(boolean isPredicted) {
synchronized (cleanInternalCircuits) {
final int predictedCount = (isPredicted) ? 2 : 0;
final int needed = Math.max(requestedInternalCircuitCount, predictedCount) - (pendingInternalCircuitCount + cleanInternalCircuits.size());
if(needed < 0) {
return 0;
} else {
return needed;
}
}
}
void incrementPendingInternalCircuitCount() {
synchronized (cleanInternalCircuits) {
pendingInternalCircuitCount += 1;
}
}
void decrementPendingInternalCircuitCount() {
synchronized (cleanInternalCircuits) {
pendingInternalCircuitCount -= 1;
}
}
void addCleanInternalCircuit(InternalCircuit circuit) {
synchronized(cleanInternalCircuits) {
pendingInternalCircuitCount -= 1;
cleanInternalCircuits.add(circuit);
cleanInternalCircuits.notifyAll();
}
}
boolean isNtorEnabled() {
switch(config.getUseNTorHandshake()) {
case AUTO:
return isNtorEnabledInConsensus();
case FALSE:
return false;
case TRUE:
return true;
default:
throw new IllegalArgumentException("getUseNTorHandshake() returned "+ config.getUseNTorHandshake());
}
}
boolean isNtorEnabledInConsensus() {
ConsensusDocument consensus = directory.getCurrentConsensusDocument();
return (consensus != null) && (consensus.getUseNTorHandshake());
}
public DirectoryCircuit openDirectoryCircuitTo(List<Router> path) throws OpenFailedException {
final DirectoryCircuit circuit = CircuitImpl.createDirectoryCircuitTo(this, path);
if(!tryOpenCircuit(circuit, true, false)) {
throw new OpenFailedException("Could not create directory circuit for path");
}
return circuit;
}
public ExitCircuit openExitCircuitTo(List<Router> path) throws OpenFailedException {
final ExitCircuit circuit = CircuitImpl.createExitCircuitTo(this, path);
if(!tryOpenCircuit(circuit, false, false)) {
throw new OpenFailedException("Could not create exit circuit for path");
}
return circuit;
}
public InternalCircuit openInternalCircuitTo(List<Router> path) throws OpenFailedException {
final InternalCircuit circuit = CircuitImpl.createInternalCircuitTo(this, path);
if(!tryOpenCircuit(circuit, false, false)) {
throw new OpenFailedException("Could not create internal circuit for path");
}
return circuit;
}
private boolean tryOpenCircuit(Circuit circuit, boolean isDirectory, boolean trackInitialization) {
final DirectoryCircuitResult result = new DirectoryCircuitResult();
final CircuitCreationRequest req = new CircuitCreationRequest(pathChooser, circuit, result, isDirectory);
final CircuitBuildTask task = new CircuitBuildTask(req, connectionCache, isNtorEnabled(), (trackInitialization) ? (initializationTracker) : (null));
task.run();
return result.isSuccessful();
}
}