Skip to content

Commit

Permalink
Changes to support connecting to .onion addresses:
Browse files Browse the repository at this point in the history
+ PeerAddress detects .onion and serializes/deserializes
  them using the onioncat format, which is also used by
  bitcoin-core, btcd, and probably others.

+ added some new DNS seeds from bitcoin-core

+ PeerGroup now listens for "addr" protocol messages and
  adds new peers to inactive list.  This enables peer discovery
  beyond what is found by DNS and hard-coded seeds.
  Discovered peers are not presently persisted to disk.

+ Beginnings of a class for validating that peer addresses are
  routable.

+ Catch PeerDiscoveryException during getPeers call. avoids stack trace when all DNS lookups timeout
  • Loading branch information
dan-da committed Jan 21, 2017
1 parent 64e3361 commit d8a5d08
Show file tree
Hide file tree
Showing 8 changed files with 465 additions and 36 deletions.
80 changes: 57 additions & 23 deletions core/src/main/java/org/bitcoinj/core/PeerAddress.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
package org.bitcoinj.core;

import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.net.AddressChecker;
import org.bitcoinj.net.OnionCat;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Objects;
import com.google.common.net.InetAddresses;

import java.io.IOException;
Expand All @@ -30,11 +37,14 @@
import static org.bitcoinj.core.Utils.uint64ToByteStreamLE;
import static com.google.common.base.Preconditions.checkNotNull;


/**
* A PeerAddress holds an IP address and port number representing the network location of
* a peer in the Bitcoin P2P network. It exists primarily for serialization purposes.
*/
public class PeerAddress extends ChildMessage {
private static final Logger log = LoggerFactory.getLogger(PeerAddress.class);

private static final long serialVersionUID = 7501293709324197411L;
static final int MESSAGE_SIZE = 30;

Expand Down Expand Up @@ -102,17 +112,28 @@ public PeerAddress(InetAddress addr) {
* InetAddress or a String hostname. If you want to connect to a .onion, set the hostname to the .onion address.
*/
public PeerAddress(InetSocketAddress addr) {
if (addr.getHostName() == null || !addr.getHostName().toLowerCase().endsWith(".onion")) {
/* socks addresses, eg Tor, use hostname only because no local lookup is performed.
* includes .onion hidden services.
*/
String host = addr.getHostString();
if( host != null && host.endsWith(".onion") ) {
this.hostname = host;
try {
this.addr = OnionCat.onionHostToInetAddress(this.hostname);
}
catch (UnknownHostException e) {
log.warn( "Invalid format for onion address: {}", this.hostname );
}
}
else {
this.addr = checkNotNull(addr.getAddress());
} else {
this.hostname = addr.getHostName();
}
this.port = addr.getPort();
this.protocolVersion = NetworkParameters.PROTOCOL_VERSION;
this.services = BigInteger.ZERO;
length = protocolVersion > 31402 ? MESSAGE_SIZE : MESSAGE_SIZE - 4;
}

public static PeerAddress localhost(NetworkParameters params) {
return new PeerAddress(InetAddresses.forString("127.0.0.1"), params.getPort());
}
Expand All @@ -127,14 +148,25 @@ protected void bitcoinSerializeToStream(OutputStream stream) throws IOException
uint32ToByteStreamLE(secs, stream);
}
uint64ToByteStreamLE(services, stream); // nServices.
// Java does not provide any utility to map an IPv4 address into IPv6 space, so we have to do it by hand.
byte[] ipBytes = addr.getAddress();
if (ipBytes.length == 4) {
byte[] v6addr = new byte[16];
System.arraycopy(ipBytes, 0, v6addr, 12, 4);
v6addr[10] = (byte) 0xFF;
v6addr[11] = (byte) 0xFF;
ipBytes = v6addr;

AddressChecker addrChecker = new AddressChecker();
byte[] ipBytes;
if( addrChecker.IsOnionCatTor( addr ) ) {
ipBytes = OnionCat.onionHostToIPV6Bytes( hostname );
}
else if( addr != null ) {
// Java does not provide any utility to map an IPv4 address into IPv6 space, so we have to do it by hand.
ipBytes = addr.getAddress();
if (ipBytes.length == 4) {
byte[] v6addr = new byte[16];
System.arraycopy(ipBytes, 0, v6addr, 12, 4);
v6addr[10] = (byte) 0xFF;
v6addr[11] = (byte) 0xFF;
ipBytes = v6addr;
}
}
else {
ipBytes = new byte[16]; // zero-filled.
}
stream.write(ipBytes);
// And write out the port. Unlike the rest of the protocol, address and port is in big endian byte order.
Expand All @@ -160,8 +192,13 @@ protected void parse() throws ProtocolException {
time = -1;
services = readUint64();
byte[] addrBytes = readBytes(16);
AddressChecker addrChecker = new AddressChecker();
try {
addr = InetAddress.getByAddress(addrBytes);

if( addrChecker.IsOnionCatTor( addr )) {
hostname = OnionCat.IPV6BytesToOnionHost( addr.getAddress() );
}
} catch (UnknownHostException e) {
throw new RuntimeException(e); // Cannot happen.
}
Expand Down Expand Up @@ -236,24 +273,20 @@ public String toString() {
if (hostname != null) {
return "[" + hostname + "]:" + port;
}
return "[" + addr.getHostAddress() + "]:" + port;
if(addr != null ) {
return "[" + addr.getHostAddress() + "]:" + port;
}
return "[]";
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PeerAddress other = (PeerAddress) o;
return other.addr.equals(addr) &&
other.port == port &&
other.services.equals(services) &&
other.time == time;
//TODO: including services and time could cause same peer to be added multiple times in collections
return o.toString().equals(this.toString());
}

@Override
public int hashCode() {
return addr.hashCode() ^ port ^ (int) time ^ services.hashCode();
return Objects.hashCode(this.toString());
}

public InetSocketAddress toSocketAddress() {
Expand All @@ -263,5 +296,6 @@ public InetSocketAddress toSocketAddress() {
} else {
return new InetSocketAddress(addr, port);
}
}
}

}
36 changes: 29 additions & 7 deletions core/src/main/java/org/bitcoinj/core/PeerGroup.java
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,19 @@ public class PeerGroup implements TransactionBroadcaster {
public List<Message> getData(Peer peer, GetDataMessage m) {
return handleGetData(m);
}

@Override
public Message onPreMessageReceived(Peer peer, Message m) {

if(m instanceof AddressMessage) {
for( PeerAddress peerAddress : ((AddressMessage)m).getAddresses() ) {
addInactive(peerAddress);
}
}

// Just pass the message right through for further processing.
return m;
}

@Override
public void onBlocksDownloaded(Peer peer, Block block, @Nullable FilteredBlock filteredBlock, int blocksLeft) {
Expand Down Expand Up @@ -744,22 +757,25 @@ public void addAddress(PeerAddress peerAddress) {
int newMax;
lock.lock();
try {
addInactive(peerAddress);
newMax = getMaxConnections() + 1;
if( addInactive(peerAddress) ) {
newMax = getMaxConnections() + 1;
setMaxConnections(newMax);
}
} finally {
lock.unlock();
}
setMaxConnections(newMax);
}

private void addInactive(PeerAddress peerAddress) {
private boolean addInactive(PeerAddress peerAddress) {
lock.lock();
try {
// Deduplicate
if (backoffMap.containsKey(peerAddress))
return;
if (backoffMap.containsKey(peerAddress)) {
return false;
}
backoffMap.put(peerAddress, new ExponentialBackoff(peerBackoffParams));
inactives.offer(peerAddress);
return true;
} finally {
lock.unlock();
}
Expand Down Expand Up @@ -794,7 +810,13 @@ protected int discoverPeers() throws PeerDiscoveryException {
final List<PeerAddress> addressList = Lists.newLinkedList();
for (PeerDiscovery peerDiscovery : peerDiscoverers /* COW */) {
InetSocketAddress[] addresses;
addresses = peerDiscovery.getPeers(5, TimeUnit.SECONDS);
try {
addresses = peerDiscovery.getPeers(5, TimeUnit.SECONDS);
}
catch(PeerDiscoveryException e) {
log.warn(e.getMessage());
continue;
}
for (InetSocketAddress address : addresses) addressList.add(new PeerAddress(address));
if (addressList.size() >= maxPeersToDiscoverCount) break;
}
Expand Down
72 changes: 72 additions & 0 deletions core/src/main/java/org/bitcoinj/net/AddressChecker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.bitcoinj.net;

import org.bitcoinj.utils.CIDRUtils;

import java.net.InetAddress;
import java.net.UnknownHostException;

/**
* Created by danda on 1/12/17.
*/
public class AddressChecker {

private CIDRUtils onionCatNet;
private CIDRUtils rfc4193Net;

public AddressChecker() {

// Note: this is borrowed/ported from btcd (written in go).

// btcd has many more rules that are probably important and should be
// implemented in this class, but for now we only care about onion
// addresses for onioncat (ipv6) encoding/decoding.

// onionCatNet defines the IPv6 address block used to support Tor.
// bitcoind encodes a .onion address as a 16 byte number by decoding the
// address prior to the .onion (i.e. the key hash) base32 into a ten
// byte number. It then stores the first 6 bytes of the address as
// 0xfd, 0x87, 0xd8, 0x7e, 0xeb, 0x43.
//
// This is the same range used by OnionCat, which is part part of the
// RFC4193 unique local IPv6 range.
//
// In summary the format is:
// { magic 6 bytes, 10 bytes base32 decode of key hash }
onionCatNet = new CIDRUtils("fd87:d87e:eb43::", 48);

// rfc4193Net specifies the IPv6 unique local address block as defined
// by RFC4193 (FC00::/7).
rfc4193Net = new CIDRUtils("FC00::", 7);
}

// IsValid returns whether or not the passed address is valid. The address is
// considered invalid under the following circumstances:
// IPv4: It is either a zero or all bits set address.
// IPv6: It is either a zero or RFC3849 documentation address.
public boolean IsValid(InetAddress addr) {
// todo: port/implement.

// IsUnspecified returns if address is 0, so only all bits set, and
// RFC3849 need to be explicitly checked.

// return na.IP != nil && !(na.IP.IsUnspecified() ||
// na.IP.Equal(net.IPv4bcast))

return true;
}

// IsOnionCatTor returns whether or not the passed address is in the IPv6 range
// used by bitcoin to support Tor (fd87:d87e:eb43::/48). Note that this range
// is the same range used by OnionCat, which is part of the RFC4193 unique local
// IPv6 range.
public boolean IsOnionCatTor(InetAddress addr) {
return onionCatNet.isInRange(addr);
}

// IsRFC4193 returns whether or not the passed address is part of the IPv6
// unique local range as defined by RFC4193 (FC00::/7).
public boolean IsRFC4193(InetAddress addr) {
return rfc4193Net.isInRange(addr);
}

}
2 changes: 1 addition & 1 deletion core/src/main/java/org/bitcoinj/net/BlockingClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public void run() {
}
} catch (Exception e) {
if (!vCloseRequested) {
log.error("Error trying to open/read from connection: {}: {}", serverAddress, e.getMessage());
log.debug("Error trying to open/read from connection: {}: {}", serverAddress, e.getMessage());
connectFuture.setException(e);
}
} finally {
Expand Down
53 changes: 53 additions & 0 deletions core/src/main/java/org/bitcoinj/net/OnionCat.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.bitcoinj.net;

import org.bitcoinj.utils.Base32;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.Arrays;


/**
* Created by danda on 1/12/17.
*/
public class OnionCat {

/** Converts a .onion address to onioncat format
*
* @param hostname
* @return
*/
public static byte[] onionHostToIPV6Bytes(String hostname) {
String needle = ".onion";
if( hostname.endsWith(needle) ) {
hostname = hostname.substring(0,hostname.length() - needle.length());
}
byte[] prefix = new byte[] {(byte)0xfd, (byte)0x87, (byte)0xd8, (byte)0x7e, (byte)0xeb, (byte)0x43};
byte[] onionaddr = Base32.base32Decode(hostname);
byte[] ipBytes = new byte[prefix.length + onionaddr.length];
System.arraycopy(prefix, 0, ipBytes, 0, prefix.length);
System.arraycopy(onionaddr, 0, ipBytes, prefix.length, onionaddr.length);

return ipBytes;
}

public static InetAddress onionHostToInetAddress(String hostname) throws UnknownHostException {
return InetAddress.getByAddress(onionHostToIPV6Bytes(hostname));
}

public static InetSocketAddress onionHostToInetSocketAddress(String hostname, int port) throws UnknownHostException {
return new InetSocketAddress( onionHostToInetAddress(hostname), port);
}


/** Converts an IPV6 onioncat encoded address to a hostname
*
* @param bytes
* @return
*/
public static String IPV6BytesToOnionHost( byte[] bytes) {
String base32 = Base32.base32Encode( Arrays.copyOfRange(bytes, 6, 16) );
return base32.toLowerCase() + ".onion";
}
}
12 changes: 7 additions & 5 deletions core/src/main/java/org/bitcoinj/params/MainNetParams.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@ public MainNetParams() {
checkpoints.put(200000, Sha256Hash.wrap("000000000000034a7dedef4a161fa058a2d67a173a90155f3a2fe6fc132e0ebf"));

dnsSeeds = new String[] {
"seed.bitcoin.sipa.be", // Pieter Wuille
"dnsseed.bluematt.me", // Matt Corallo
"dnsseed.bitcoin.dashjr.org", // Luke Dashjr
"seed.bitcoinstats.com", // Chris Decker
"seed.bitnodes.io", // Addy Yeow
"seed.bitcoin.sipa.be", // Pieter Wuille
"dnsseed.bluematt.me", // Matt Corallo
"dnsseed.bitcoin.dashjr.org", // Luke Dashjr
"seed.bitcoinstats.com", // Chris Decker
"bitseed.xf2.org", // Jeff Garzik
"seed.bitcoin.jonasschnelli.ch", // Jonas Schnelli
"seed.bitnodes.io", // Addy Yeow
};

addrSeeds = new int[] {
Expand Down
Loading

0 comments on commit d8a5d08

Please sign in to comment.