/** * Copyright 2014 Miron Cuperman * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.bitcoinj.net.discovery; import static com.google.common.base.Preconditions.checkArgument; import org.bitcoinj.core.NetworkParameters; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.subgraph.orchid.Circuit; import com.subgraph.orchid.RelayCell; import com.subgraph.orchid.Router; import com.subgraph.orchid.TorClient; import com.subgraph.orchid.circuits.path.CircuitPathChooser; import com.subgraph.orchid.data.HexDigest; import com.subgraph.orchid.data.exitpolicy.ExitTarget; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import static java.util.Collections.singleton; /** * <p>Supports peer discovery through Tor.</p> * * <p>Failure to obtain at least four different peers through different exit nodes will cause * a PeerDiscoveryException will be thrown during getPeers(). * </p> * * <p>DNS seeds do not attempt to enumerate every peer on the network. If you want more peers * to connect to, you need to discover them via other means (like addr broadcasts).</p> */ public class TorDiscovery implements PeerDiscovery { private static final Logger log = LoggerFactory.getLogger(TorDiscovery.class); public static final int MINIMUM_ROUTER_COUNT = 4; public static final int ROUTER_LOOKUP_COUNT = 10; public static final int MINIMUM_ROUTER_LOOKUP_COUNT = 6; public static final int RECEIVE_RETRIES = 3; public static final int RESOLVE_STREAM_ID = 0x1000; // An arbitrary stream ID public static final int RESOLVE_CNAME = 0x00; public static final int RESOLVE_ERROR = 0xf0; public static final int RESOLVE_IPV4 = 0x04; public static final int RESOLVE_IPV6 = 0x06; private final String[] hostNames; private final NetworkParameters netParams; private final CircuitPathChooser pathChooser; private final TorClient torClient; private ListeningExecutorService threadPool; /** * Supports finding peers through Tor. Community run DNS entry points will be used. * * @param netParams Network parameters to be used for port information. */ public TorDiscovery(NetworkParameters netParams, TorClient torClient) { this(netParams.getDnsSeeds(), netParams, torClient); } /** * Supports finding peers through Tor. * * @param hostNames Host names to be examined for seed addresses. * @param netParams Network parameters to be used for port information. * @param torClient an already-started Tor client. */ public TorDiscovery(String[] hostNames, NetworkParameters netParams, TorClient torClient) { this.hostNames = hostNames; this.netParams = netParams; this.torClient = torClient; this.pathChooser = CircuitPathChooser.create(torClient.getConfig(), torClient.getDirectory()); } private static class Lookup { final Router router; final InetAddress address; Lookup(Router router, InetAddress address) { this.router = router; this.address = address; } } @Override public InetSocketAddress[] getPeers(long timeoutValue, TimeUnit timeoutUnit) throws PeerDiscoveryException { if (hostNames == null) throw new PeerDiscoveryException("Unable to find any peers via DNS"); Set<Router> routers = Sets.newHashSet(); ArrayList<ExitTarget> dummyTargets = Lists.newArrayList(); // Collect exit nodes until we have enough while (routers.size() < ROUTER_LOOKUP_COUNT) { Router router = pathChooser.chooseExitNodeForTargets(dummyTargets); routers.add(router); } try { List<Circuit> circuits = getCircuits(torClient.getConfig().getCircuitBuildTimeout(), TimeUnit.MILLISECONDS, routers); if (circuits.isEmpty()) throw new PeerDiscoveryException("Failed to open any circuit within " + String.valueOf(timeoutValue) + " " + timeoutUnit); Collection<InetSocketAddress> addresses = lookupAddresses(timeoutValue, timeoutUnit, circuits); if (addresses.size() < MINIMUM_ROUTER_COUNT) throw new PeerDiscoveryException("Unable to find enough peers via Tor - got " + addresses.size()); ArrayList<InetSocketAddress> addressList = Lists.newArrayList(); addressList.addAll(addresses); Collections.shuffle(addressList); return addressList.toArray(new InetSocketAddress[addressList.size()]); } catch (InterruptedException e) { throw new PeerDiscoveryException(e); } } private List<Circuit> getCircuits(long timeoutValue, TimeUnit timeoutUnit, Set<Router> routers) throws InterruptedException { checkArgument(routers.size() >= MINIMUM_ROUTER_LOOKUP_COUNT, "Set of {} routers is smaller than required minimum {}", routers.size(), MINIMUM_ROUTER_LOOKUP_COUNT); createThreadPool(routers.size()); try { List<ListenableFuture<Circuit>> circuitFutures = Lists.newArrayList(); final CountDownLatch doneSignal = new CountDownLatch(MINIMUM_ROUTER_LOOKUP_COUNT); for (final Router router : routers) { ListenableFuture<Circuit> openCircuit = threadPool.submit(new Callable<Circuit>() { @Override public Circuit call() throws Exception { return torClient.getCircuitManager().openInternalCircuitTo(Lists.newArrayList(router)); } }); Futures.addCallback(openCircuit, new FutureCallback<Circuit>() { public void onSuccess(Circuit circuit) { doneSignal.countDown(); } public void onFailure(Throwable thrown) { doneSignal.countDown(); } }); circuitFutures.add(openCircuit); } boolean countedDown = doneSignal.await(timeoutValue, timeoutUnit); try { List<Circuit> circuits = new ArrayList<Circuit>(Futures.successfulAsList(circuitFutures).get()); // Any failures will result in null entries. Remove them. circuits.removeAll(singleton(null)); int failures = routers.size() - circuits.size(); if (failures > 0) log.warn("{} failures " + (countedDown ? "" : "(including timeout) ") + "opening DNS lookup circuits", failures); return circuits; } catch (ExecutionException e) { // Cannot happen, successfulAsList accepts failures throw new RuntimeException(e); } } finally { shutdownThreadPool(); } } private Collection<InetSocketAddress> lookupAddresses(long timeoutValue, TimeUnit timeoutUnit, List<Circuit> circuits) throws InterruptedException { createThreadPool(circuits.size() * hostNames.length); try { List<ListenableFuture<Lookup>> lookupFutures = Lists.newArrayList(); for (final Circuit circuit : circuits) { for (final String seed : hostNames) { lookupFutures.add(threadPool.submit(new Callable<Lookup>() { @Override public Lookup call() throws Exception { return new Lookup(circuit.getFinalCircuitNode().getRouter(), lookup(circuit, seed)); } })); } } threadPool.awaitTermination(timeoutValue, timeoutUnit); int timeouts = 0; for (ListenableFuture<Lookup> future : lookupFutures) { if (!future.isDone()) { timeouts++; future.cancel(true); } } if (timeouts > 0) log.warn("{} DNS lookups timed out", timeouts); try { List<Lookup> lookups = new ArrayList<Lookup>(Futures.successfulAsList(lookupFutures).get()); // Any failures will result in null entries. Remove them. lookups.removeAll(singleton(null)); // Use a map to enforce one result per exit node // TODO: randomize result selection better Map<HexDigest, InetSocketAddress> lookupMap = Maps.newHashMap(); for (Lookup lookup : lookups) { InetSocketAddress address = new InetSocketAddress(lookup.address, netParams.getPort()); lookupMap.put(lookup.router.getIdentityHash(), address); } return lookupMap.values(); } catch (ExecutionException e) { // Cannot happen, successfulAsList accepts failures throw new RuntimeException(e); } } finally { shutdownThreadPool(); } } private synchronized void shutdownThreadPool() { threadPool.shutdownNow(); threadPool = null; } private synchronized void createThreadPool(int size) { threadPool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(size)); } private InetAddress lookup(Circuit circuit, String seed) throws UnknownHostException { // Send a resolve cell to the exit node RelayCell cell = circuit.createRelayCell(RelayCell.RELAY_RESOLVE, RESOLVE_STREAM_ID, circuit.getFinalCircuitNode()); cell.putString(seed); circuit.sendRelayCell(cell); // Wait a few cell timeout periods (3 * 20 sec) for replies, in case the path is slow for (int i = 0 ; i < RECEIVE_RETRIES; i++) { RelayCell res = circuit.receiveRelayCell(); if (res != null) { while (res.cellBytesRemaining() > 0) { int type = res.getByte(); int len = res.getByte(); byte[] value = new byte[len]; res.getByteArray(value); int ttl = res.getInt(); if (type == RESOLVE_CNAME || type >= RESOLVE_ERROR) { // TODO handle .onion CNAME replies throw new RuntimeException(new String(value)); } else if (type == RESOLVE_IPV4 || type == RESOLVE_IPV6) { return InetAddress.getByAddress(value); } } break; } } throw new RuntimeException("Could not look up " + seed); } @Override public synchronized void shutdown() { if (threadPool != null) { shutdownThreadPool(); } } }