From 3cf3edddc9a279c604653a4044c489ebd6c04627 Mon Sep 17 00:00:00 2001 From: Oscar Guindzberg Date: Thu, 21 Mar 2019 12:50:58 -0300 Subject: [PATCH 1/2] Tor/Onion services support code clean-up --- .../java/org/bitcoinj/core/PeerAddress.java | 38 +++++----- .../java/org/bitcoinj/net/AddressChecker.java | 71 ------------------- .../bitcoinj/net/OnionCatAddressChecker.java | 40 +++++++++++ .../{OnionCat.java => OnionCatConverter.java} | 38 ++++++---- .../java/org/bitcoinj/utils/CIDRUtils.java | 56 ++------------- 5 files changed, 85 insertions(+), 158 deletions(-) delete mode 100644 core/src/main/java/org/bitcoinj/net/AddressChecker.java create mode 100644 core/src/main/java/org/bitcoinj/net/OnionCatAddressChecker.java rename core/src/main/java/org/bitcoinj/net/{OnionCat.java => OnionCatConverter.java} (50%) diff --git a/core/src/main/java/org/bitcoinj/core/PeerAddress.java b/core/src/main/java/org/bitcoinj/core/PeerAddress.java index 13df7c160d4..e8c2c7047ff 100644 --- a/core/src/main/java/org/bitcoinj/core/PeerAddress.java +++ b/core/src/main/java/org/bitcoinj/core/PeerAddress.java @@ -18,8 +18,8 @@ package org.bitcoinj.core; import com.google.common.net.InetAddresses; -import org.bitcoinj.net.AddressChecker; -import org.bitcoinj.net.OnionCat; +import org.bitcoinj.net.OnionCatAddressChecker; +import org.bitcoinj.net.OnionCatConverter; import org.bitcoinj.params.MainNetParams; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -117,8 +117,7 @@ public PeerAddress(NetworkParameters params, InetAddress addr) { /** * 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. Protocol version is the default - * for Bitcoin. + * Protocol version is the default for Bitcoin. */ public PeerAddress(InetSocketAddress addr) { /* socks addresses, eg Tor, use hostname only because no local lookup is performed. @@ -128,13 +127,13 @@ public PeerAddress(InetSocketAddress addr) { 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 ); + this.addr = OnionCatConverter.onionHostToInetAddress(this.hostname); + } catch (UnknownHostException e) { + // No dns lookup is performed because InetAddress method is supposed to be invoked with numeric address + // parameter, so this is unreachable code. + throw new RuntimeException(e); } - } - else { + } else { this.addr = checkNotNull(addr.getAddress()); } this.port = addr.getPort(); @@ -188,12 +187,10 @@ protected void bitcoinSerializeToStream(OutputStream stream) throws IOException } uint64ToByteStreamLE(services, stream); // nServices. - AddressChecker addrChecker = new AddressChecker(); byte[] ipBytes; - if( addrChecker.IsOnionCatTor( addr ) ) { - ipBytes = OnionCat.onionHostToIPV6Bytes(hostname); - } - else if( addr != null ) { + if (OnionCatAddressChecker.isOnionCatTor(addr)) { + ipBytes = OnionCatConverter.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) { @@ -202,12 +199,10 @@ else if( addr != null ) { v6addr[10] = (byte) 0xFF; v6addr[11] = (byte) 0xFF; ipBytes = v6addr; - } - else { + } else { ipBytes = new byte[16]; } - } - else { + } else { ipBytes = new byte[16]; // zero-filled. } stream.write(ipBytes); @@ -229,12 +224,11 @@ 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() ); + if( OnionCatAddressChecker.isOnionCatTor( addr )) { + hostname = OnionCatConverter.IPV6BytesToOnionHost( addr.getAddress() ); } } catch (UnknownHostException e) { throw new RuntimeException(e); // Cannot happen. diff --git a/core/src/main/java/org/bitcoinj/net/AddressChecker.java b/core/src/main/java/org/bitcoinj/net/AddressChecker.java deleted file mode 100644 index 3e13a4b3d33..00000000000 --- a/core/src/main/java/org/bitcoinj/net/AddressChecker.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.bitcoinj.net; - -import org.bitcoinj.utils.CIDRUtils; - -import java.net.InetAddress; - -/** - * 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); - } - -} diff --git a/core/src/main/java/org/bitcoinj/net/OnionCatAddressChecker.java b/core/src/main/java/org/bitcoinj/net/OnionCatAddressChecker.java new file mode 100644 index 00000000000..706650e4be4 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/net/OnionCatAddressChecker.java @@ -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); + } +} diff --git a/core/src/main/java/org/bitcoinj/net/OnionCat.java b/core/src/main/java/org/bitcoinj/net/OnionCatConverter.java similarity index 50% rename from core/src/main/java/org/bitcoinj/net/OnionCat.java rename to core/src/main/java/org/bitcoinj/net/OnionCatConverter.java index 7081ecf9392..3f0faaa81eb 100644 --- a/core/src/main/java/org/bitcoinj/net/OnionCat.java +++ b/core/src/main/java/org/bitcoinj/net/OnionCatConverter.java @@ -1,5 +1,6 @@ package org.bitcoinj.net; +import org.bitcoinj.core.Utils; import org.bitcoinj.utils.Base32; import java.net.InetAddress; @@ -9,19 +10,28 @@ /** - * Created by danda on 1/12/17. + * Converts .onion addresses to IPv6 format and viceversa. + * @author danda + * @author Oscar Guindzberg */ -public class OnionCat { +public class OnionCatConverter { /** Converts a .onion address to onioncat format * - * @param hostname - * @return + * @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) ) { - hostname = hostname.substring(0,hostname.length() - needle.length()); + 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); @@ -36,17 +46,15 @@ public static InetAddress onionHostToInetAddress(String hostname) throws Unknown 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 + /** 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) { + 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"; } diff --git a/core/src/main/java/org/bitcoinj/utils/CIDRUtils.java b/core/src/main/java/org/bitcoinj/utils/CIDRUtils.java index e940a9e62cb..fe4c553e112 100644 --- a/core/src/main/java/org/bitcoinj/utils/CIDRUtils.java +++ b/core/src/main/java/org/bitcoinj/utils/CIDRUtils.java @@ -1,4 +1,3 @@ -// adapted from https://github.com/edazdarevic/CIDRUtils/ /* * The MIT License * @@ -33,6 +32,7 @@ /** * A class that enables to get an IP range from CIDR specification. It supports * both IPv4 and IPv6. + * Adapted from https://github.com/edazdarevic/CIDRUtils/ */ public class CIDRUtils { @@ -41,61 +41,30 @@ public class CIDRUtils { private InetAddress endAddress; private final int prefixLength; - - public CIDRUtils(String cidr) { - - try { - /* split CIDR to address and prefix part */ - if (cidr.contains("/")) { - int index = cidr.indexOf("/"); - String addressPart = cidr.substring(0, index); - String networkPart = cidr.substring(index + 1); - - inetAddress = InetAddress.getByName(addressPart); - prefixLength = Integer.parseInt(networkPart); - - calculate(); - } else { - throw new IllegalArgumentException("not an valid CIDR format!"); - } - } catch (UnknownHostException e) { - // note: no dns lookup is performed in try because these are numeric addresses. - throw new RuntimeException(e); // Cannot happen. - } - } - public CIDRUtils(String addr, Integer prefixLength) { try { inetAddress = InetAddress.getByName(addr); this.prefixLength = prefixLength; - calculate(); } catch (UnknownHostException e) { - // note: no dns lookup is performed in try because these are numeric addresses. - throw new RuntimeException(e); // Cannot happen. + // No dns lookup is performed because InetAddress methods are supposed to be invoked with numeric address + // parameters, so this is unreachable code. + throw new RuntimeException(e); } } private void calculate() throws UnknownHostException { - ByteBuffer maskBuffer; int targetSize; if (inetAddress.getAddress().length == 4) { - maskBuffer = - ByteBuffer - .allocate(4) - .putInt(-1); + maskBuffer = ByteBuffer.allocate(4).putInt(-1); targetSize = 4; } else { - maskBuffer = ByteBuffer.allocate(16) - .putLong(-1L) - .putLong(-1L); + maskBuffer = ByteBuffer.allocate(16).putLong(-1L).putLong(-1L); targetSize = 16; } - BigInteger mask = (new BigInteger(1, maskBuffer.array())).not().shiftRight(prefixLength); - BigInteger ipVal = new BigInteger(1, inetAddress.getAddress()); BigInteger startIp = ipVal.and(mask); @@ -130,15 +99,6 @@ private byte[] toBytes(byte[] array, int targetSize) { return ret; } - public String getNetworkAddress() { - - return this.startAddress.getHostAddress(); - } - - public String getBroadcastAddress() { - return this.endAddress.getHostAddress(); - } - public boolean isInRange(InetAddress address) { BigInteger start = new BigInteger(1, this.startAddress.getAddress()); BigInteger end = new BigInteger(1, this.endAddress.getAddress()); @@ -149,8 +109,4 @@ public boolean isInRange(InetAddress address) { return (st == -1 || st == 0) && (te == -1 || te == 0); } - - public boolean isInRange(String ipAddress) throws UnknownHostException { - return isInRange(InetAddress.getByName(ipAddress)); - } } \ No newline at end of file From b43145864f3f0fee35a3d43adc033453974a0777 Mon Sep 17 00:00:00 2001 From: Oscar Guindzberg Date: Thu, 21 Mar 2019 19:40:15 -0300 Subject: [PATCH 2/2] PeerAddress: Use addr and hostname as alternatives. --- .../java/org/bitcoinj/core/PeerAddress.java | 62 ++++++++++--------- .../org/bitcoinj/core/PeerAddressTest.java | 29 +++++++++ 2 files changed, 63 insertions(+), 28 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/PeerAddress.java b/core/src/main/java/org/bitcoinj/core/PeerAddress.java index e8c2c7047ff..12ee8aa0be8 100644 --- a/core/src/main/java/org/bitcoinj/core/PeerAddress.java +++ b/core/src/main/java/org/bitcoinj/core/PeerAddress.java @@ -46,8 +46,10 @@ public class PeerAddress extends ChildMessage { 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; @@ -120,21 +122,11 @@ public PeerAddress(NetworkParameters params, InetAddress addr) { * Protocol version is the default for Bitcoin. */ public PeerAddress(InetSocketAddress addr) { - /* 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 = OnionCatConverter.onionHostToInetAddress(this.hostname); - } catch (UnknownHostException e) { - // No dns lookup is performed because InetAddress method is supposed to be invoked with numeric address - // parameter, so this is unreachable code. - throw new RuntimeException(e); - } + InetAddress inetAddress = addr.getAddress(); + if(inetAddress != null) { + this.addr = inetAddress; } else { - this.addr = checkNotNull(addr.getAddress()); + this.hostname = checkNotNull(addr.getHostString()); } this.port = addr.getPort(); this.protocolVersion = NetworkParameters.ProtocolVersion.CURRENT.getBitcoinProtocolVersion(); @@ -147,11 +139,21 @@ public PeerAddress(InetSocketAddress addr) { * 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. Use this if you want to connect to a Tor .onion address. + * Constructs a peer address from a stringified hostname+port. * Protocol version is the default for Bitcoin. */ public PeerAddress(String hostname, int port) { @@ -162,7 +164,7 @@ public PeerAddress(String hostname, int port) { } /** - * 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); @@ -188,8 +190,12 @@ protected void bitcoinSerializeToStream(OutputStream stream) throws IOException uint64ToByteStreamLE(services, stream); // nServices. byte[] ipBytes; - if (OnionCatAddressChecker.isOnionCatTor(addr)) { - ipBytes = OnionCatConverter.onionHostToIPV6Bytes(hostname); + 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(); @@ -199,11 +205,9 @@ protected void bitcoinSerializeToStream(OutputStream stream) throws IOException v6addr[10] = (byte) 0xFF; v6addr[11] = (byte) 0xFF; ipBytes = v6addr; - } else { - ipBytes = new byte[16]; } } else { - ipBytes = new byte[16]; // zero-filled. + 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. @@ -224,15 +228,17 @@ protected void parse() throws ProtocolException { time = -1; services = readUint64(); byte[] addrBytes = readBytes(16); + InetAddress inetAddress; try { - addr = InetAddress.getByAddress(addrBytes); - - if( OnionCatAddressChecker.isOnionCatTor( addr )) { - hostname = OnionCatConverter.IPV6BytesToOnionHost( addr.getAddress() ); - } + 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 = ((0xFF & payload[cursor++]) << 8) | (0xFF & payload[cursor++]); // The 4 byte difference is the uint32 timestamp that was introduced in version 31402 length = protocolVersion > 31402 ? MESSAGE_SIZE : MESSAGE_SIZE - 4; diff --git a/core/src/test/java/org/bitcoinj/core/PeerAddressTest.java b/core/src/test/java/org/bitcoinj/core/PeerAddressTest.java index 88376d46e4c..f3c3ae7be5a 100644 --- a/core/src/test/java/org/bitcoinj/core/PeerAddressTest.java +++ b/core/src/test/java/org/bitcoinj/core/PeerAddressTest.java @@ -21,6 +21,7 @@ import org.junit.Test; import java.net.InetAddress; +import java.net.InetSocketAddress; import static org.bitcoinj.core.Utils.HEX; import static org.junit.Assert.assertEquals; @@ -43,4 +44,32 @@ public void testBitcoinSerialize() throws Exception { assertEquals("000000000000000000000000000000000000ffff7f000001208d", Utils.HEX.encode(pa.bitcoinSerialize())); } + + @Test + public void testOnionHostname() throws Exception { + PeerAddress pa = new PeerAddress(InetSocketAddress.createUnresolved("explorernuoc63nb.onion", 8333)); + assertEquals("explorernuoc63nb.onion", pa.toSocketAddress().getHostString()); + assertEquals("explorernuoc63nb.onion", pa.getHostname()); + assertEquals(null, pa.getAddr()); + assertEquals(8333, pa.toSocketAddress().getPort()); + assertEquals(8333, pa.getPort()); + PeerAddress pa2 = new PeerAddress(MainNetParams.get(), InetSocketAddress.createUnresolved("explorernuoc63nb.onion", 8333)); + assertPeerAddressEqualsRegardlessOfTime(pa, pa2); + PeerAddress pa3 = new PeerAddress("explorernuoc63nb.onion", 8333); + assertPeerAddressEqualsRegardlessOfTime(pa, pa3); + PeerAddress pa4 = new PeerAddress(MainNetParams.get(), "explorernuoc63nb.onion", 8333); + assertPeerAddressEqualsRegardlessOfTime(pa, pa4); + byte[] serialized = pa.unsafeBitcoinSerialize(); + PeerAddress paFromSerialized = new PeerAddress(MainNetParams.get(), serialized, 0, NetworkParameters.ProtocolVersion.CURRENT.getBitcoinProtocolVersion()); + assertPeerAddressEqualsRegardlessOfTime(pa, paFromSerialized); + } + + private void assertPeerAddressEqualsRegardlessOfTime(PeerAddress pa, PeerAddress pa2) { + assertEquals(pa.getPort(), pa2.getPort()); + assertEquals(pa.getAddr(), pa2.getAddr()); + assertEquals(pa.getHostname(), pa2.getHostname()); + assertEquals(pa.getServices(), pa2.getServices()); + } + + }