Merge pull request #535 from dan-da/route_bitcoinj_over_jtoryproxy

Route bitcoinj over jtoryproxy
This commit is contained in:
Manfred Karrer 2016-07-22 13:17:19 +02:00 committed by GitHub
commit dc06523814
10 changed files with 451 additions and 14 deletions

View File

@ -0,0 +1,94 @@
/**
* Copyright (C) 2010-2014 Leon Blakey <lord.quackstar at gmail.com>
*
* This file is part of PircBotX.
*
* PircBotX is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* PircBotX is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* PircBotX. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bitsquare.btc;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
import java.net.UnknownHostException;
import javax.net.SocketFactory;
/**
* A basic SocketFactory for creating sockets that connect through the specified
* proxy.
*
* @author Leon Blakey
*/
public class ProxySocketFactory extends SocketFactory {
protected final Proxy proxy;
/**
* Create all sockets with the specified proxy.
*
* @param proxy An existing proxy
*/
public ProxySocketFactory(Proxy proxy) {
this.proxy = proxy;
}
/**
* A convenience constructor for creating a proxy with the specified host
* and port.
*
* @param proxyType The type of proxy were connecting to
* @param hostname The hostname of the proxy server
* @param port The port of the proxy server
*/
public ProxySocketFactory(Proxy.Type proxyType, String hostname, int port) {
this.proxy = new Proxy(proxyType, new InetSocketAddress(hostname, port));
}
@Override
public Socket createSocket() throws IOException {
Socket socket = new Socket(proxy);
return socket;
}
@Override
public Socket createSocket(String string, int i) throws IOException, UnknownHostException {
Socket socket = new Socket(proxy);
socket.connect(new InetSocketAddress(string, i));
return socket;
}
@Override
public Socket createSocket(String string, int i, InetAddress localAddress, int localPort) throws IOException, UnknownHostException {
Socket socket = new Socket(proxy);
socket.bind(new InetSocketAddress(localAddress, localPort));
socket.connect(new InetSocketAddress(string, i));
return socket;
}
@Override
public Socket createSocket(InetAddress ia, int i) throws IOException {
Socket socket = new Socket(proxy);
socket.connect(new InetSocketAddress(ia, i));
return socket;
}
@Override
public Socket createSocket(InetAddress ia, int i, InetAddress localAddress, int localPort) throws IOException {
Socket socket = new Socket(proxy);
socket.bind(new InetSocketAddress(localAddress, localPort));
socket.connect(new InetSocketAddress(ia, i));
return socket;
}
}

View File

@ -0,0 +1,178 @@
/**
* Copyright 2011 Micheal Swiggs
*
* 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 io.bitsquare.btc;
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
import com.runjva.sourceforge.jsocks.protocol.SocksSocket;
import org.bitcoinj.net.*;
import org.bitcoinj.net.discovery.PeerDiscovery;
import org.bitcoinj.net.discovery.PeerDiscoveryException;
import org.bitcoinj.core.NetworkParameters;
import javax.annotation.Nullable;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* SeedPeersSocks5Dns resolves peers via Proxy (Socks5) remote DNS.
*/
public class SeedPeersSocks5Dns implements PeerDiscovery {
private Socks5Proxy proxy;
private NetworkParameters params;
private InetSocketAddress[] seedAddrs;
private InetSocketAddress[] seedAddrsIP;
private int pnseedIndex;
private InetSocketAddress[] seedAddrsResolved;
private static final Logger log = LoggerFactory.getLogger(SeedPeersSocks5Dns.class);
/**
* Supports finding peers by hostname over a socks5 proxy.
*
* @param Socks5Proxy proxy the socks5 proxy to connect over.
* @param NetworkParameters param to be used for seed and port information.
*/
public SeedPeersSocks5Dns(Socks5Proxy proxy, NetworkParameters params) {
this.proxy = proxy;
this.params = params;
this.seedAddrs = convertAddrsString( params.getDnsSeeds(), params.getPort() );
if( false ) {
// This is an example of how .onion servers could be used. Unfortunately there is presently no way
// to hand the onion address (or a connected socket) back to bitcoinj without it crashing in PeerAddress.
// note: the onion addresses should be added into bitcoinj NetworkParameters classes, eg for mainnet, testnet
// not here!
this.seedAddrs = new InetSocketAddress[] { InetSocketAddress.createUnresolved( "cajrifqkvalh2ooa.onion", 8333 ),
InetSocketAddress.createUnresolved( "bk7yp6epnmcllq72.onion", 8333 )
};
}
seedAddrsResolved = new InetSocketAddress[seedAddrs.length];
for(int idx = seedAddrs.length; idx < seedAddrsResolved.length; idx ++) {
seedAddrsResolved[idx] = seedAddrsIP[idx-seedAddrs.length];
}
}
/**
* Acts as an iterator, returning the address of each node in the list sequentially.
* Once all the list has been iterated, null will be returned for each subsequent query.
*
* @return InetSocketAddress - The address/port of the next node.
* @throws PeerDiscoveryException
*/
@Nullable
public InetSocketAddress getPeer() throws PeerDiscoveryException {
try {
return nextPeer();
} catch (UnknownHostException e) {
throw new PeerDiscoveryException(e);
}
}
/**
* worker for getPeer()
*/
@Nullable
private InetSocketAddress nextPeer() throws UnknownHostException, PeerDiscoveryException {
if (seedAddrs == null || seedAddrs.length == 0) {
throw new PeerDiscoveryException("No IP address seeds configured; unable to find any peers");
}
if (pnseedIndex >= seedAddrsResolved.length) {
return null;
}
if( seedAddrsResolved[pnseedIndex] == null ) {
seedAddrsResolved[pnseedIndex] = lookup( proxy, seedAddrs[pnseedIndex] );
}
log.error("SeedPeersSocks5Dns::nextPeer: " + seedAddrsResolved[pnseedIndex] );
return seedAddrsResolved[pnseedIndex++];
}
/**
* Returns an array containing all the Bitcoin nodes within the list.
*/
@Override
public InetSocketAddress[] getPeers(long timeoutValue, TimeUnit timeoutUnit) throws PeerDiscoveryException {
try {
return allPeers();
} catch (UnknownHostException e) {
throw new PeerDiscoveryException(e);
}
}
/**
* returns all seed peers, performs hostname lookups if necessary.
*/
private InetSocketAddress[] allPeers() throws UnknownHostException {
for (int i = 0; i < seedAddrsResolved.length; ++i) {
if( seedAddrsResolved[i] == null ) {
seedAddrsResolved[i] = lookup( proxy, seedAddrs[i] );
}
}
return seedAddrsResolved;
}
/**
* Resolves a hostname via remote DNS over socks5 proxy.
*/
public static InetSocketAddress lookup( Socks5Proxy proxy, InetSocketAddress addr ) {
if( !addr.isUnresolved() ) {
return addr;
}
try {
SocksSocket proxySocket = new SocksSocket( proxy, addr.getHostString(), addr.getPort() );
InetAddress addrResolved = proxySocket.getInetAddress();
proxySocket.close();
if( addrResolved != null ) {
log.info("Resolved " + addr.getHostString() + " to " + addrResolved.getHostAddress() );
return new InetSocketAddress(addrResolved, addr.getPort() );
}
else {
// note: .onion nodes fall in here when proxy is Tor. But they have no IP address.
// Unfortunately bitcoinj crashes in PeerAddress if it finds an unresolved address.
log.error("Connected to " + addr.getHostString() + ". But did not resolve to address." );
}
} catch (Exception e) {
log.error("Error resolving " + addr.getHostString() + ". Exception:\n" + e.toString() );
}
return null;
}
/**
* Converts an array of hostnames to array of unresolved InetSocketAddress
*/
private InetSocketAddress[] convertAddrsString(String[] addrs, int port) {
InetSocketAddress[] list = new InetSocketAddress[addrs.length];
for( int i = 0; i < addrs.length; i++) {
list[i] = InetSocketAddress.createUnresolved(addrs[i], port);
}
return list;
}
@Override
public void shutdown() {
}
}

View File

@ -0,0 +1,74 @@
/*
* This file is part of Bitsquare.
*
* Bitsquare is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bitsquare.btc;
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.kits.WalletAppKit;
import org.bitcoinj.net.BlockingClientManager;
import org.bitcoinj.core.PeerGroup;
import java.io.File;
import java.net.Proxy;
import java.net.InetSocketAddress;
import java.util.concurrent.TimeoutException;
public class WalletAppKitBitSquare extends WalletAppKit {
private Socks5Proxy socks5Proxy;
/**
* Creates a new WalletAppKit, with a newly created {@link Context}. Files will be stored in the given directory.
*/
public WalletAppKitBitSquare(NetworkParameters params, Socks5Proxy socks5Proxy, File directory, String filePrefix) {
super(params, directory, filePrefix);
this.socks5Proxy = socks5Proxy;
}
public Socks5Proxy getProxy() {
return socks5Proxy;
}
protected PeerGroup createPeerGroup() throws TimeoutException {
// no proxy case.
if(socks5Proxy == null) {
return super.createPeerGroup();
}
// proxy case.
Proxy proxy = new Proxy ( Proxy.Type.SOCKS,
new InetSocketAddress(socks5Proxy.getInetAddress().getHostName(),
socks5Proxy.getPort() ) );
int CONNECT_TIMEOUT_MSEC = 60 * 1000; // same value used in bitcoinj.
ProxySocketFactory proxySocketFactory = new ProxySocketFactory(proxy);
BlockingClientManager mgr = new BlockingClientManager(proxySocketFactory);
PeerGroup peerGroup = new PeerGroup(params, vChain, mgr);
mgr.setConnectTimeoutMillis(CONNECT_TIMEOUT_MSEC);
peerGroup.setConnectTimeoutMillis(CONNECT_TIMEOUT_MSEC);
// This enables remote DNS lookup of peers over socks5 proxy.
// It is slower, but more private.
// This could be turned into a user option.
this.setDiscovery( new SeedPeersSocks5Dns(socks5Proxy, params) );
return peerGroup;
}
}

View File

@ -22,6 +22,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.Service;
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
import io.bitsquare.btc.listeners.AddressConfidenceListener;
import io.bitsquare.btc.listeners.BalanceListener;
import io.bitsquare.btc.listeners.TxConfidenceListener;
@ -30,6 +31,7 @@ import io.bitsquare.common.UserThread;
import io.bitsquare.common.handlers.ErrorMessageHandler;
import io.bitsquare.common.handlers.ExceptionHandler;
import io.bitsquare.common.handlers.ResultHandler;
import io.bitsquare.p2p.NodeAddress;
import io.bitsquare.storage.FileUtil;
import io.bitsquare.storage.Storage;
import io.bitsquare.user.Preferences;
@ -54,6 +56,7 @@ import javax.inject.Named;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.file.Paths;
import java.util.*;
@ -91,7 +94,7 @@ public class WalletService {
private final UserAgent userAgent;
private final boolean useTor;
private WalletAppKit walletAppKit;
private WalletAppKitBitSquare walletAppKit;
private Wallet wallet;
private final IntegerProperty numPeers = new SimpleIntegerProperty(0);
private final ObjectProperty<List<Peer>> connectedPeers = new SimpleObjectProperty<>();
@ -147,7 +150,7 @@ public class WalletService {
// Public Methods
///////////////////////////////////////////////////////////////////////////////////////////
public void initialize(@Nullable DeterministicSeed seed, ResultHandler resultHandler, ExceptionHandler exceptionHandler) {
public void initialize(@Nullable DeterministicSeed seed, Socks5Proxy proxy, ResultHandler resultHandler, ExceptionHandler exceptionHandler) {
// Tell bitcoinj to execute event handlers on the JavaFX UI thread. This keeps things simple and means
// we cannot forget to switch threads when adding event handlers. Unfortunately, the DownloadListener
// we give to the app kit is currently an exception and runs on a library thread. It'll get fixed in
@ -163,7 +166,7 @@ public class WalletService {
backupWallet();
// If seed is non-null it means we are restoring from backup.
walletAppKit = new WalletAppKit(params, walletDir, "Bitsquare") {
walletAppKit = new WalletAppKitBitSquare(params, proxy, walletDir, "Bitsquare") {
@Override
protected void onSetupCompleted() {
// Don't make the user wait for confirmations for now, as the intention is they're sending it
@ -264,13 +267,52 @@ public class WalletService {
// 1333 / (2800 + 1333) = 0.32 -> 32 % probability that a pub key is in our wallet
walletAppKit.setBloomFilterFalsePositiveRate(0.00005);
log.debug( "seedNodes: " + seedNodes.toString() );
// Pass custom seed nodes if set in options
if (seedNodes != null && !seedNodes.isEmpty()) {
//TODO Check how to pass seed nodes to the wallet kit. Probably via walletAppKit.setPeerNodes
// todo: this parsing should be more robust,
// give validation error if needed.
// also: it seems the peer nodes can be overridden in the case
// of regtest mode below. is that wanted?
String[] nodes = seedNodes.split(",");
List<PeerAddress> peerAddressList = new ArrayList<PeerAddress>();
for(String node : nodes) {
String[] parts = node.split(":");
if( parts.length == 2 ) {
// note: this will cause a DNS request if hostname used.
// note: DNS requests are routed over socks5 proxy, if used.
// fixme: .onion hostnames will fail! see comments in SeedPeersSocks5Dns
InetSocketAddress addr;
if( proxy != null ) {
InetSocketAddress unresolved = InetSocketAddress.createUnresolved(parts[0], Integer.parseInt(parts[1]));
// proxy remote DNS request happens here.
addr = SeedPeersSocks5Dns.lookup( proxy, unresolved );
}
else {
// DNS request happens here. if it fails, addr.isUnresolved() == true.
addr = new InetSocketAddress( parts[0], Integer.parseInt(parts[1]) );
}
// note: isUnresolved check should be removed once we fix PeerAddress
if( addr != null && !addr.isUnresolved() ) {
peerAddressList.add( new PeerAddress( addr.getAddress(), addr.getPort() ));
}
}
}
if(peerAddressList.size() > 0) {
PeerAddress peerAddressListFixed[] = new PeerAddress[peerAddressList.size()];
log.debug( "seedNodes parsed: " + peerAddressListFixed.toString() );
walletAppKit.setPeerNodes(peerAddressList.toArray(peerAddressListFixed));
}
}
if (useTor && params.getId().equals(NetworkParameters.ID_MAINNET))
walletAppKit.useTor();
// We do not call walletAppKit.useTor() anymore because that would turn
// on orchid Tor, which we do not want. Instead, we create a Tor proxy
// later.
// if (useTor && params.getId().equals(NetworkParameters.ID_MAINNET))
// walletAppKit.useTor();
// Now configure and start the appkit. This will take a second or two - we could show a temporary splash screen
// or progress widget to keep the user engaged whilst we initialise, but we don't.
@ -338,7 +380,7 @@ public class WalletService {
Context.propagate(ctx);
walletAppKit.stopAsync();
walletAppKit.awaitTerminated();
initialize(seed, resultHandler, exceptionHandler);
initialize(seed, walletAppKit.getProxy(), resultHandler, exceptionHandler);
} catch (Throwable t) {
t.printStackTrace();
log.error("Executing task failed. " + t.getMessage());

View File

@ -18,6 +18,7 @@
package io.bitsquare.gui.main;
import com.google.inject.Inject;
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
import io.bitsquare.alert.Alert;
import io.bitsquare.alert.AlertManager;
import io.bitsquare.alert.PrivateNotification;
@ -55,6 +56,7 @@ import io.bitsquare.gui.main.overlays.windows.WalletPasswordWindow;
import io.bitsquare.gui.util.BSFormatter;
import io.bitsquare.locale.CurrencyUtil;
import io.bitsquare.locale.TradeCurrency;
import io.bitsquare.p2p.NodeAddress;
import io.bitsquare.p2p.P2PService;
import io.bitsquare.p2p.P2PServiceListener;
import io.bitsquare.p2p.network.CloseConnectionReason;
@ -234,8 +236,8 @@ public class MainViewModel implements ViewModel {
showStartupTimeoutPopup();
}, 4, TimeUnit.MINUTES);
walletInitialized = initBitcoinWallet();
p2pNetWorkReady = initP2PNetwork();
walletInitialized = initBitcoinWallet();
// need to store it to not get garbage collected
allServicesDone = EasyBind.combine(walletInitialized, p2pNetWorkReady, (a, b) -> a && b);
@ -348,6 +350,9 @@ public class MainViewModel implements ViewModel {
public void onTorNodeReady() {
bootstrapState.set("Tor node created");
p2PNetworkIconId.set("image-connection-tor");
if( preferences.getUseTorForBitcoinJ() ) {
initWalletService();
}
}
@Override
@ -422,6 +427,18 @@ public class MainViewModel implements ViewModel {
}
private BooleanProperty initBitcoinWallet() {
final BooleanProperty walletInitialized = new SimpleBooleanProperty();
// We only init wallet service here if not using Tor for bitcoinj.
// When using Tor, wallet init must be deferred until Tor is ready.
if( !preferences.getUseTorForBitcoinJ() ) {
initWalletService();
}
return walletInitialized;
}
private void initWalletService() {
ObjectProperty<Throwable> walletServiceException = new SimpleObjectProperty<>();
btcInfoBinding = EasyBind.combine(walletService.downloadPercentageProperty(), walletService.numPeersProperty(), walletServiceException,
(downloadPercentage, numPeers, exception) -> {
@ -461,9 +478,26 @@ public class MainViewModel implements ViewModel {
btcInfoBinding.subscribe((observable, oldValue, newValue) -> {
btcInfo.set(newValue);
});
Socks5Proxy proxy = null;
if( preferences.getUseTorForBitcoinJ() ) {
// Use p2p service
proxy = p2PService.getNetworkNode().getSocksProxy();
}
/**
* Uncomment this to wire up user specified proxy via program args or config file.
* Could be Tor, i2p, ssh, vpn, etc.
if( preferences.getBitcoinProxyHost() != null &&
preferences.getBitcoinProxyPort() != null ) {
proxy = new Socks5Proxy( preferences.getBitcoinProxyHost(),
preferences.getBitcoinProxyPort() );
}
*/
final BooleanProperty walletInitialized = new SimpleBooleanProperty();
walletService.initialize(null,
proxy,
() -> {
numBtcPeers = walletService.numPeersProperty().get();
@ -484,7 +518,6 @@ public class MainViewModel implements ViewModel {
}
},
walletServiceException::set);
return walletInitialized;
}
private void onAllServicesInitialized() {

View File

@ -33,6 +33,10 @@ public abstract class TorNode<M extends OnionProxyManager, C extends OnionProxyC
log.info("TorSocks running on port " + proxyPort);
this.proxy = setupSocksProxy(proxyPort);
}
public Socks5Proxy getSocksProxy() {
return proxy;
}
private Socks5Proxy setupSocksProxy(int proxyPort) throws UnknownHostException {
Socks5Proxy proxy = new Socks5Proxy(PROXY_LOCALHOST, proxyPort);

View File

@ -4,5 +4,5 @@ DisableNetwork 1
AvoidDiskWrites 1
PidFile pid
RunAsDaemon 1
SafeSocks 1
SafeSocks 0
SOCKSPort auto

View File

@ -80,8 +80,7 @@ public class LocalhostNetworkNode extends NetworkNode {
protected Socket createSocket(NodeAddress peerNodeAddress) throws IOException {
return new Socket(peerNodeAddress.hostName, peerNodeAddress.port);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Tor delay simulation
///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -1,6 +1,7 @@
package io.bitsquare.p2p.network;
import com.google.common.util.concurrent.*;
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
import io.bitsquare.app.Log;
import io.bitsquare.common.UserThread;
import io.bitsquare.common.util.Utilities;
@ -213,6 +214,12 @@ public abstract class NetworkNode implements MessageListener {
}
}
@Nullable
public Socks5Proxy getSocksProxy() {
return null;
}
public SettableFuture<Connection> sendMessage(Connection connection, Message message) {
Log.traceCall("\n\tmessage=" + StringUtils.abbreviate(message.toString(), 100) + "\n\tconnection=" + connection);
// connection.sendMessage might take a bit (compression, write to stream), so we use a thread to not block

View File

@ -6,6 +6,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.msopentech.thali.java.toronionproxy.JavaOnionProxyContext;
import com.msopentech.thali.java.toronionproxy.JavaOnionProxyManager;
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
import io.bitsquare.app.Log;
import io.bitsquare.common.Timer;
import io.bitsquare.common.UserThread;
@ -95,10 +96,15 @@ public class TorNetworkNode extends NetworkNode {
@Override
protected Socket createSocket(NodeAddress peerNodeAddress) throws IOException {
checkArgument(peerNodeAddress.hostName.endsWith(".onion"), "PeerAddress is not an onion address");
// FIXME: disabling temporarily.
// checkArgument(peerNodeAddress.hostName.endsWith(".onion"), "PeerAddress is not an onion address");
return torNetworkNode.connectToHiddenService(peerNodeAddress.hostName, peerNodeAddress.port);
}
public Socks5Proxy getSocksProxy() {
return torNetworkNode != null ? torNetworkNode.getSocksProxy() : null;
}
public void shutDown(@Nullable Runnable shutDownCompleteHandler) {
Log.traceCall();