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.
- Beginnings of a class for validating that peer addresses are routable.
- PeerAddress hashCode() and equals(): add hostname and refactor.
- PeerAddress: Use addr and hostname as alternatives.

Cherry pick 9f09a89
Cherry pick 0988148
Cherry pick 32d9142
Cherry pick ebeecab
  • Loading branch information
oscarguindzberg committed May 16, 2020
1 parent d8c30c9 commit ad73b86
Show file tree
Hide file tree
Showing 6 changed files with 427 additions and 20 deletions.
116 changes: 96 additions & 20 deletions core/src/main/java/org/bitcoinj/core/PeerAddress.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@

package org.bitcoinj.core;

import com.google.common.base.Objects;
import org.bitcoinj.net.OnionCatAddressChecker;
import org.bitcoinj.net.OnionCatConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.OutputStream;
Expand All @@ -35,11 +38,14 @@
* <p>Instances of this class are not safe for use by multiple threads.</p>
*/
public class PeerAddress extends ChildMessage {

private static final Logger log = LoggerFactory.getLogger(PeerAddress.class);

static final int MESSAGE_SIZE = 30;

//addr and hostname are alternatives and should not be used together.
private InetAddress addr;
private String hostname; // Used for .onion addresses
private String hostname;

private int port;
private BigInteger services;
private long time;
Expand Down Expand Up @@ -92,16 +98,55 @@ public PeerAddress(NetworkParameters params, InetAddress addr) {
this(params, addr, params.getPort());
}

/**
* Constructs a peer address from an {@link InetSocketAddress}. An InetSocketAddress can take in as parameters an
* InetAddress or a String hostname. If you want to connect to a .onion, set the hostname to the .onion address.
* Protocol version is the default for Bitcoin.
*/
public PeerAddress(InetSocketAddress addr) {
InetAddress inetAddress = addr.getAddress();
if(inetAddress != null) {
this.addr = inetAddress;
} else {
this.hostname = checkNotNull(addr.getHostString());
}
this.port = addr.getPort();
this.protocolVersion = NetworkParameters.ProtocolVersion.CURRENT.getBitcoinProtocolVersion();
this.services = BigInteger.ZERO;
length = protocolVersion > 31402 ? MESSAGE_SIZE : MESSAGE_SIZE - 4;
}

/**
* Constructs a peer address from an {@link InetSocketAddress}. An InetSocketAddress can take in as parameters an
* InetAddress or a String hostname. If you want to connect to a .onion, set the hostname to the .onion address.
*/
public PeerAddress(NetworkParameters params, InetSocketAddress addr) {
this(params, addr.getAddress(), addr.getPort());
super(params);
InetAddress inetAddress = addr.getAddress();
if(inetAddress != null) {
this.addr = inetAddress;
} else {
this.hostname = checkNotNull(addr.getHostString());
}
this.port = addr.getPort();
this.protocolVersion = params.getProtocolVersionNum(NetworkParameters.ProtocolVersion.CURRENT);
this.services = BigInteger.ZERO;
length = protocolVersion > 31402 ? MESSAGE_SIZE : MESSAGE_SIZE - 4;
}

/**
* Constructs a peer address from a stringified hostname+port.
* Protocol version is the default for Bitcoin.
*/
public PeerAddress(String hostname, int port) {
this.hostname = hostname;
this.port = port;
this.protocolVersion = NetworkParameters.ProtocolVersion.CURRENT.getBitcoinProtocolVersion();
this.services = BigInteger.ZERO;
}

/**
* Constructs a peer address from a stringified hostname+port. Use this if you want to connect to a Tor .onion address.
* Constructs a peer address from a stringified hostname+port.
*/
public PeerAddress(NetworkParameters params, String hostname, int port) {
super(params);
Expand All @@ -125,14 +170,26 @@ protected void bitcoinSerializeToStream(OutputStream stream) throws IOException
Utils.uint32ToByteStreamLE(secs, stream);
}
Utils.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;

byte[] ipBytes;
if (hostname!=null) {
if (hostname.endsWith(".onion")) {
ipBytes = OnionCatConverter.onionHostToIPV6Bytes(hostname);
} else {
ipBytes = new byte[16];
}
} 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 {
throw new IllegalStateException("Either hostname or addr should be not null");
}
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 @@ -156,14 +213,20 @@ protected void parse() throws ProtocolException {
time = -1;
services = readUint64();
byte[] addrBytes = readBytes(16);
InetAddress inetAddress;
try {
addr = InetAddress.getByAddress(addrBytes);
inetAddress = InetAddress.getByAddress(addrBytes);
} catch (UnknownHostException e) {
throw new RuntimeException(e); // Cannot happen.
}
if(OnionCatAddressChecker.isOnionCatTor(inetAddress)) {
hostname = OnionCatConverter.IPV6BytesToOnionHost(inetAddress.getAddress());
} else {
addr = inetAddress;
}
port = Utils.readUint16BE(payload, cursor);
cursor += 2;
// The 4 byte difference is the uint32 timestamp that was introduced in version 31402
// The 4 byte difference is the uint32 timestamp that was introduced in version 31402
length = isSerializeTime() ? MESSAGE_SIZE : MESSAGE_SIZE - 4;
}

Expand Down Expand Up @@ -196,22 +259,35 @@ 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.time == time && other.services.equals(services);
PeerAddress that = (PeerAddress) o;

if (port != that.port) return false;
if (time != that.time) return false;
if (addr != null ? !addr.equals(that.addr) : that.addr != null) return false;
if (hostname != null ? !hostname.equals(that.hostname) : that.hostname != null) return false;
return !(services != null ? !services.equals(that.services) : that.services != null);
}

@Override
public int hashCode() {
return Objects.hashCode(addr, port, time, services);
int result = addr != null ? addr.hashCode() : 0;
result = 31 * result + (hostname != null ? hostname.hashCode() : 0);
result = 31 * result + port;
result = 31 * result + (services != null ? services.hashCode() : 0);
result = 31 * result + (int) (time ^ (time >>> 32));
return result;
}

public InetSocketAddress toSocketAddress() {
// Reconstruct the InetSocketAddress properly
if (hostname != null) {
Expand Down
40 changes: 40 additions & 0 deletions core/src/main/java/org/bitcoinj/net/OnionCatAddressChecker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.bitcoinj.net;

import org.bitcoinj.utils.CIDRUtils;

import java.net.InetAddress;

/**
* Checks an IPv6 InetAddress represents a valid OnionCat address
* @author danda
* @author Oscar Guindzberg
*/
public class OnionCatAddressChecker {

// 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 }
private static CIDRUtils onionCatNet = new CIDRUtils("fd87:d87e:eb43::", 48);

// 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 static boolean isOnionCatTor(InetAddress addr) {
return onionCatNet.isInRange(addr);
}
}
61 changes: 61 additions & 0 deletions core/src/main/java/org/bitcoinj/net/OnionCatConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.bitcoinj.net;

import org.bitcoinj.core.Utils;
import org.bitcoinj.utils.Base32;

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


/**
* Converts .onion addresses to IPv6 format and viceversa.
* @author danda
* @author Oscar Guindzberg
*/
public class OnionCatConverter {

/** Converts a .onion address to onioncat format
*
* @param hostname e.g. explorernuoc63nb.onion
* @return e.g. fd87:d87e:eb43:25de:b744:916d:1c2f:6da1
*/
public static byte[] onionHostToIPV6Bytes(String hostname) {
String needle = ".onion";
if(hostname.endsWith(needle)) {
if (hostname.length() != 22) {
throw new IllegalArgumentException("Invalid hostname: " + hostname);
}
hostname = hostname.substring(0, hostname.length() - needle.length());
} else {
if (hostname.length() != 16) {
throw new IllegalArgumentException("Invalid hostname: " + hostname);
}
}
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));
}


/** Converts an IPV6 onioncat encoded address to a .onion address
* @param bytes e.g. fd87:d87e:eb43:25de:b744:916d:1c2f:6da1
* @return e.g. explorernuoc63nb.onion
*/
public static String IPV6BytesToOnionHost(byte[] bytes) {
if (bytes.length != 16) {
throw new IllegalArgumentException("Invalid IPv6 address: " + Utils.HEX.encode(bytes));
}
String base32 = Base32.base32Encode( Arrays.copyOfRange(bytes, 6, 16) );
return base32.toLowerCase() + ".onion";
}
}
88 changes: 88 additions & 0 deletions core/src/main/java/org/bitcoinj/utils/Base32.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copied from orchid Tor lib.

package org.bitcoinj.utils;

public class Base32 {
private final static String BASE32_CHARS = "abcdefghijklmnopqrstuvwxyz234567";

public static String base32Encode(byte[] source) {
return base32Encode(source, 0, source.length);
}

public static String base32Encode(byte[] source, int offset, int length) {
final int nbits = length * 8;
if(nbits % 5 != 0)
throw new IllegalArgumentException("Base32 input length must be a multiple of 5 bits");

final int outlen = nbits / 5;
final StringBuffer outbuffer = new StringBuffer();
int bit = 0;
for(int i = 0; i < outlen; i++) {
int v = (source[bit / 8] & 0xFF) << 8;
if(bit + 5 < nbits) v += (source[bit / 8 + 1] & 0xFF);
int u = (v >> (11 - (bit % 8))) & 0x1F;
outbuffer.append(BASE32_CHARS.charAt(u));
bit += 5;
}
return outbuffer.toString();
}

public static byte[] base32Decode(String source) {
int[] v = stringToIntVector(source);

int nbits = source.length() * 5;
if(nbits % 8 != 0)
throw new IllegalArgumentException("Base32 decoded array must be a muliple of 8 bits");

int outlen = nbits / 8;
byte[] outbytes = new byte[outlen];

int bit = 0;
for(int i = 0; i < outlen; i++) {
int bb = bit / 5;
outbytes[i] = (byte) decodeByte(bit, v[bb], v[bb + 1], v[bb + 2]);
bit += 8;
}
return outbytes;
}

private static int decodeByte(int bitOffset, int b0, int b1, int b2) {
switch(bitOffset % 40) {
case 0:
return ls(b0, 3) + rs(b1, 2);
case 8:
return ls(b0, 6) + ls(b1, 1) + rs (b2, 4);
case 16:
return ls(b0, 4) + rs(b1, 1);
case 24:
return ls(b0, 7) + ls(b1, 2) + rs(b2, 3);
case 32:
return ls(b0, 5) + (b1 & 0xFF);
}
throw new IllegalArgumentException("Illegal bit offset");
}

private static int ls(int n, int shift) {
return ((n << shift) & 0xFF);
}

private static int rs(int n, int shift) {
return ((n >> shift) & 0xFF);
}

private static int[] stringToIntVector(String s) {
final int[] ints = new int[s.length() + 1];
for(int i = 0; i < s.length(); i++) {
int b = s.charAt(i) & 0xFF;
if(b > 0x60 && b < 0x7B)
ints[i] = b - 0x61;
else if(b > 0x31 && b < 0x38)
ints[i] = b - 0x18;
else if(b > 0x40 && b < 0x5B)
ints[i] = b - 0x41;
else
throw new IllegalArgumentException("Illegal character in base32 encoded string: "+ s.charAt(i));
}
return ints;
}
}
Loading

0 comments on commit ad73b86

Please sign in to comment.