Integrate Tor support into the PeerGroup and WalletAppKit API.

This commit is contained in:
Mike Hearn 2014-04-27 18:57:56 +02:00
parent 6c3b6ce1b3
commit 3f3f637779
4 changed files with 119 additions and 17 deletions

View File

@ -17,10 +17,12 @@
package com.google.bitcoin.core;
import com.google.bitcoin.net.BlockingClientManager;
import com.google.bitcoin.net.ClientConnectionManager;
import com.google.bitcoin.net.NioClientManager;
import com.google.bitcoin.net.discovery.PeerDiscovery;
import com.google.bitcoin.net.discovery.PeerDiscoveryException;
import com.google.bitcoin.net.discovery.TorDiscovery;
import com.google.bitcoin.script.Script;
import com.google.bitcoin.utils.ExponentialBackoff;
import com.google.bitcoin.utils.ListenerRegistration;
@ -31,11 +33,16 @@ import com.google.common.collect.Sets;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import com.google.common.util.concurrent.*;
import com.subgraph.orchid.Tor;
import com.subgraph.orchid.TorClient;
import net.jcip.annotations.GuardedBy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.InetSocketAddress;
@ -69,9 +76,10 @@ import static com.google.common.base.Preconditions.checkState;
* network IO, but starting and stopping the service should be fine.</p>
*/
public class PeerGroup extends AbstractExecutionThreadService implements TransactionBroadcaster {
private static final int DEFAULT_CONNECTIONS = 4;
private static final Logger log = LoggerFactory.getLogger(PeerGroup.class);
private static final int DEFAULT_CONNECTIONS = 4;
private static final int TOR_TIMEOUT_SECONDS = 20;
protected final ReentrantLock lock = Threading.lock("peergroup");
// Addresses to try to connect to, excluding active peers.
@ -83,6 +91,7 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac
// Currently connecting peers.
private final CopyOnWriteArrayList<Peer> pendingPeers;
private final ClientConnectionManager channels;
@Nullable private final TorClient torClient;
// The peer that has been selected for the purposes of downloading announced data.
@GuardedBy("lock") private Peer downloadPeer;
@ -273,16 +282,51 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac
this(params, chain, new NioClientManager());
}
/**
* <p>Creates a PeerGroup that accesses the network via the Tor network. The provided TorClient is used so you can
* preconfigure it beforehand. It should not have been already started. You can just use "new TorClient()" if
* you don't have any particular configuration requirements.</p>
*
* <p>Peer discovery is automatically configured to use DNS seeds resolved via a random selection of exit nodes.
* If running on the Oracle JDK the unlimited strength jurisdiction checks will also be overridden,
* as they no longer apply anyway and can cause startup failures due to the requirement for AES-256.</p>
*
* <p>The user does not need any additional software for this: it's all pure Java. As of April 2014 <b>this mode
* is experimental</b>.</p>
*
* @throws java.util.concurrent.TimeoutException if Tor fails to start within 20 seconds.
*/
public static PeerGroup newWithTor(NetworkParameters params, @Nullable AbstractBlockChain chain, TorClient torClient) throws TimeoutException {
checkNotNull(torClient);
maybeDisableExportControls();
BlockingClientManager manager = new BlockingClientManager(torClient.getSocketFactory());
final int CONNECT_TIMEOUT_MSEC = TOR_TIMEOUT_SECONDS * 1000;
manager.setConnectTimeoutMillis(CONNECT_TIMEOUT_MSEC);
PeerGroup result = new PeerGroup(params, chain, manager, torClient);
result.setConnectTimeoutMillis(CONNECT_TIMEOUT_MSEC);
result.addPeerDiscovery(new TorDiscovery(params, torClient));
return result;
}
/**
* Creates a new PeerGroup allowing you to specify the {@link ClientConnectionManager} which is used to create new
* connections and keep track of existing ones.
*/
public PeerGroup(NetworkParameters params, @Nullable AbstractBlockChain chain, ClientConnectionManager connectionManager) {
this(params, chain, connectionManager, null);
}
/**
* Creates a new PeerGroup allowing you to specify the {@link ClientConnectionManager} which is used to create new
* connections and keep track of existing ones.
*/
private PeerGroup(NetworkParameters params, @Nullable AbstractBlockChain chain, ClientConnectionManager connectionManager, @Nullable TorClient torClient) {
this.params = checkNotNull(params);
this.chain = chain;
this.fastCatchupTimeSecs = params.getGenesisBlock().getTimeSeconds();
this.wallets = new CopyOnWriteArrayList<Wallet>();
this.peerFilterProviders = new CopyOnWriteArrayList<PeerFilterProvider>();
this.torClient = torClient;
// This default sentinel value will be overridden by one of two actions:
// - adding a peer discovery source sets it to the default
@ -697,6 +741,12 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac
protected void startUp() throws Exception {
// This is run in a background thread by the Service implementation.
vPingTimer = new Timer("Peer pinging thread", true);
if (torClient != null) {
log.info("Starting Tor/Orchid ...");
torClient.start();
torClient.waitUntilReady(TOR_TIMEOUT_SECONDS * 1000);
log.info("Tor ready");
}
channels.startAsync();
channels.awaitRunning();
triggerConnections();
@ -712,6 +762,9 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac
for (PeerDiscovery peerDiscovery : peerDiscoverers) {
peerDiscovery.shutdown();
}
if (torClient != null) {
torClient.stop();
}
}
@Override
@ -1503,4 +1556,36 @@ public class PeerGroup extends AbstractExecutionThreadService implements Transac
lock.unlock();
}
}
public TorClient getTorClient() {
return torClient;
}
private static void maybeDisableExportControls() {
// This sorry story is documented in https://bugs.openjdk.java.net/browse/JDK-7024850
// Oracle received permission to ship AES-256 by default in 2011, but didn't get around to it for Java 8
// even though that shipped in 2014! That's dumb. So we disable the ridiculous US government mandated DRM
// for AES-256 here, as Tor requires it.
if (Tor.isAndroidRuntime())
return;
try {
Field gate = Class.forName("javax.crypto.JceSecurity").getDeclaredField("isRestricted");
gate.setAccessible(true);
gate.setBoolean(null, false);
final Field allPerm = Class.forName("javax.crypto.CryptoAllPermission").getDeclaredField("INSTANCE");
allPerm.setAccessible(true);
Object accessAllAreasCard = allPerm.get(null);
final Constructor<?> constructor = Class.forName("javax.crypto.CryptoPermissions").getDeclaredConstructor();
constructor.setAccessible(true);
Object coll = constructor.newInstance();
Method addPerm = Class.forName("javax.crypto.CryptoPermissions").getDeclaredMethod("add", java.security.Permission.class);
addPerm.setAccessible(true);
addPerm.invoke(coll, accessAllAreasCard);
Field defaultPolicy = Class.forName("javax.crypto.JceSecurity").getDeclaredField("defaultPolicy");
defaultPolicy.setAccessible(true);
defaultPolicy.set(null, coll);
} catch (Exception e) {
log.warn("Failed to deactivate AES-256 barrier logic, Tor mode may crash if this JVM requires it: " + e.getMessage());
}
}
}

View File

@ -23,10 +23,9 @@ import com.google.bitcoin.store.BlockStoreException;
import com.google.bitcoin.store.SPVBlockStore;
import com.google.bitcoin.store.WalletProtobufSerializer;
import com.google.common.util.concurrent.AbstractIdleService;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.Service;
import com.subgraph.orchid.TorClient;
import java.io.File;
import java.io.FileInputStream;
@ -35,6 +34,7 @@ import java.io.InputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
@ -78,6 +78,7 @@ public class WalletAppKit extends AbstractIdleService {
protected boolean autoStop = true;
protected InputStream checkpoints;
protected boolean blockingStartup = true;
protected boolean useTor = false; // Perhaps in future we can change this to true.
protected String userAgent, version;
public WalletAppKit(NetworkParameters params, File directory, String filePrefix) {
@ -157,6 +158,15 @@ public class WalletAppKit extends AbstractIdleService {
return this;
}
/**
* If called, then an embedded Tor client library will be used to connect to the P2P network. The user does not need
* any additional software for this: it's all pure Java. As of April 2014 <b>this mode is experimental</b>.
*/
public WalletAppKit useTor() {
this.useTor = true;
return this;
}
/**
* <p>Override this to load all wallet extensions if any are necessary.</p>
*
@ -262,8 +272,12 @@ public class WalletAppKit extends AbstractIdleService {
}
}
protected PeerGroup createPeerGroup() {
return new PeerGroup(params, vChain);
protected PeerGroup createPeerGroup() throws TimeoutException {
if (useTor) {
return PeerGroup.newWithTor(params, vChain, new TorClient());
}
else
return new PeerGroup(params, vChain);
}
private void installShutdownHook() {

View File

@ -19,10 +19,8 @@ package com.google.bitcoin.tools;
import com.google.bitcoin.core.*;
import com.google.bitcoin.crypto.KeyCrypterException;
import com.google.bitcoin.net.BlockingClientManager;
import com.google.bitcoin.net.discovery.DnsDiscovery;
import com.google.bitcoin.net.discovery.PeerDiscovery;
import com.google.bitcoin.net.discovery.TorDiscovery;
import com.google.bitcoin.params.MainNetParams;
import com.google.bitcoin.params.RegTestParams;
import com.google.bitcoin.params.TestNet3Params;
@ -37,7 +35,6 @@ import com.google.common.collect.ImmutableList;
import com.google.common.io.Resources;
import com.google.common.util.concurrent.ListenableFuture;
import com.subgraph.orchid.TorClient;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
@ -60,6 +57,7 @@ import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.LogManager;
@ -695,10 +693,13 @@ public class WalletTool {
// This will ensure the wallet is saved when it changes.
wallet.autosaveToFile(walletFile, 200, TimeUnit.MILLISECONDS, null);
if (options.has("tor")) {
torClient = new TorClient();
torClient.start();
peers = new PeerGroup(params, chain, new BlockingClientManager(torClient.getSocketFactory()));
} else {
try {
peers = PeerGroup.newWithTor(params, chain, new TorClient());
} catch (TimeoutException e) {
System.err.println("Tor startup timed out, falling back to clear net ...");
}
}
if (peers == null) {
peers = new PeerGroup(params, chain);
}
peers.setUserAgent("WalletTool", "1.0");
@ -714,9 +715,8 @@ public class WalletTool {
System.exit(1);
}
}
} else if (options.has("tor")) {
peers.addPeerDiscovery(new TorDiscovery(params, torClient));
} else {
} else if (!options.has("tor")) {
// If Tor mode then PeerGroup already has discovery set up.
if (params == RegTestParams.get()) {
log.info("Assuming regtest node on localhost");
peers.addAddress(PeerAddress.localhost(params));

View File

@ -86,6 +86,8 @@ public class Main extends Application {
// Checkpoint files are made using the BuildCheckpoints tool and usually we have to download the
// last months worth or more (takes a few seconds).
bitcoin.setCheckpoints(getClass().getResourceAsStream("checkpoints"));
// As an example!
bitcoin.useTor();
}
// Now configure and start the appkit. This will take a second or two - we could show a temporary splash screen
@ -165,7 +167,8 @@ public class Main extends Application {
public void stop() throws Exception {
bitcoin.stopAsync();
bitcoin.awaitTerminated();
super.stop();
// Forcibly terminate the JVM because Orchid likes to spew non-daemon threads everywhere.
Runtime.getRuntime().exit(0);
}
public static void main(String[] args) {