/* * Copyright 2013 Matt Corallo * * 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 com.matthewmitchell.peercoinj.net.discovery; import com.matthewmitchell.peercoinj.core.*; import com.matthewmitchell.peercoinj.utils.Threading; import com.google.common.annotations.VisibleForTesting; import net.jcip.annotations.GuardedBy; import org.slf4j.LoggerFactory; import java.io.*; import java.net.Inet4Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.*; import java.util.concurrent.TimeUnit; import static com.google.common.base.Preconditions.checkState; /** * <p>A Peer discovery mechanism that keeps a database of peers which are announced by other peers and which we've * connected to and returns a subset of those.</p> * * <p>It is important to use a peer db in addition to DNS seeds (via a {@link DnsDiscovery}) as it:<ul> * <li>Spreads load across the network better, instead of having all bitcoinj clients connect to a specific set of * (rotating) peers.</li> * <li>Prevents DNS seeds from (maliciously or accidentally) forcing you to connect to a specific set of nodes which * are conspiring to provide bad data.</li> * <li>Allows for future protocol changes which allow nodes to fully verify the chain without serving the entire * blockchain.</li> * </ul></p> * * <p>The design is based pretty heavily on sipa/Bitcoin Core's addrman/addr.dat: * <ul> * <li>Addresses are stored in limited-size sets and thrown away randomly with weight given to addresses not seen in * some time (ie no peers have announced them in some time and we have not successfully connected to them in some time). * Unlike Bitcoin Core's addrman, we do not have separate groups of sets for addresses which are new and addresses which * have been connected to in the past.</li> * <li>Sets are indexed by three values: first the IP subnet of the peer which announced the given address, second by * the address' IP subnet, and third by a random key which randomizes where collisions will happen. The random key and * announcing peer subnet is used to select a group of 16 sets. The random key and IP's subnet are then used to select * one of these 16 sets, to which the address is added.</li> * <li>The random key is important as it prevents determinism and makes it impossible to predict which sets a given * source subnet is able to get its addresses placed in. Thus, an attacker which wants to fill the address database with * nodes it controls can only probabilistically fill all sets instead of being able to pick specific source IPs which * allow it to place entries in all sets.</li> * </ul></p> */ public class PeerDBDiscovery implements PeerDiscovery { private static final org.slf4j.Logger log = LoggerFactory.getLogger(PeerDBDiscovery.class); // Threading notes: // * In general all calls will come in on the USER_THREAD so we're probably OK to just ignore threading, but we // make some effort to ensure we are thread-safe against calls coming in via broken PeerGroup extensions. // * This means addAddress is synchronized so PeerData objects are created and updated atomically, however // PeerData.connected() is called outside of addAddress, so we have to ensure the fields it accesses are always // accessed in a thread-safe manner. static final int SETS_PER_SOURCE = 8; static final int TOTAL_SETS = 128; static final int MAX_SET_SIZE = 16; private static final int ADDRESSES_RETURNED = 128; private static final int MAX_ADDRESSES_FACTOR = 8; // Only ever return at max 1/8th the total addresses we have class PeerData { PeerAddress address; /*@GuardedBy("super")*/ volatile long vTimeLastHeard = Utils.currentTimeMillis(); // Last time we heard of this node (ie a peer told us about it/we connected to it) @GuardedBy("this") long lastConnected = 0; // Last time we successfully connected to this node @GuardedBy("this") long triedSinceLastConnection = 0; // Number of times we've tried to connect to this node since the last success PeerData(PeerAddress address) { this.address = address; } PeerData(InputStream input) throws IOException { byte[] peerAddress = new byte[30 + 8*3]; checkState(input.read(peerAddress) == peerAddress.length); address = new PeerAddress(params, peerAddress, 0, NetworkParameters.PROTOCOL_VERSION); vTimeLastHeard = Utils.readInt64(peerAddress, 30); lastConnected = Utils.readInt64(peerAddress, 30 + 8); triedSinceLastConnection = Utils.readInt64(peerAddress, 30 + 16); } synchronized void write(OutputStream s) throws IOException { address.peercoinSerialize(s); Utils.int64ToByteStreamLE(vTimeLastHeard, s); Utils.int64ToByteStreamLE(lastConnected, s); Utils.int64ToByteStreamLE(triedSinceLastConnection, s); triedSinceLastConnection = Math.max(0, triedSinceLastConnection); } synchronized void connected() { triedSinceLastConnection = -1; lastConnected = Utils.currentTimeMillis(); } synchronized void disconnected() { triedSinceLastConnection++; } synchronized boolean isBad() { return (lastConnected == 0 && triedSinceLastConnection >= 3) || // Tried 3 times and never connected (lastConnected < Utils.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) && triedSinceLastConnection >= 3) || // Tried 3 times since last connection, which was > 5 days ago (vTimeLastHeard < Utils.currentTimeMillis() - TimeUnit.DAYS.toMillis(14)); // Haven't heard of node in 14 days } @Override public synchronized int hashCode() { return (int) (address.toSocketAddress().hashCode() ^ rotatingRandomKey); } @Override public synchronized boolean equals(Object o) { return (o instanceof PeerData) && ((PeerData) o).address.toSocketAddress().equals(address.toSocketAddress()); } } class AddressSet extends HashSet<PeerData> { @Override public boolean add(PeerData peer) { if (size() == MAX_SET_SIZE) { // Loop through our elements, throwing away ones which are considered useless Iterator<PeerData> it = iterator(); while (it.hasNext()) { PeerData peerToCheck = it.next(); if (peerToCheck.isBad()) { log.debug("Removing bad node " + peerToCheck.address); it.remove(); } } if (size() == MAX_SET_SIZE) { // If we're still too large, throw away an element selected based on rotatingRandomKey it = iterator(); it.next(); it.remove(); } } checkState(size() < MAX_SET_SIZE); return super.add(peer); } } private NetworkParameters params; @GuardedBy("this") List<AddressSet> addressBuckets = new ArrayList<AddressSet>(TOTAL_SETS); // We never keep multiple entries for a peer on different ports as one of our primary goals it to get as diverse a set of peers as possible @GuardedBy("this") Map<InetAddress, PeerData> addressToSetMap = new HashMap<InetAddress, PeerData>(); // Keep a static random key that is used to select set groups/sets and a rotating random key that changes on each restart private long randomKey; private long rotatingRandomKey = new Random(Utils.currentTimeMillis()).nextLong(); private final File db; // Write some data representing the subnet address is in to out. Trying to figure out which subnet size we should // use to ensure a single ISP/user cannot get in tons of address groups simply by switching IPs within their // allocation. private void writeAddressGroup(PeerAddress address, OutputStream out) throws IOException { // We use a system similar to GetGroup() in Bitcoin Core, however we do not handle nearly as many cases for // address types which are rarely used (RFC6052) and a few which are used more commonly (Teredo, 6to4). // While more should probably be added, not having them simply means we consider entire blocks a single group // instead of splitting them into more realistic groups. byte[] addressBytes = address.getAddr().getAddress(); if (address.getAddr() instanceof Inet4Address) { // If the address is in a /8 that was allocated to a single group, use the /8, otherwise use the /16 if (addressBytes[0] <= 57 && addressBytes[0] != 50 && addressBytes[0] != 49 && addressBytes[0] != 46 && addressBytes[0] != 42 && addressBytes[0] != 41 && addressBytes[0] != 39 && addressBytes[0] != 37 && addressBytes[0] != 36 && addressBytes[0] != 31 && addressBytes[0] != 27 && addressBytes[0] != 24 && addressBytes[0] != 23 && addressBytes[0] != 14 && addressBytes[0] != 5 && addressBytes[0] != 2 && addressBytes[0] != 1) out.write(addressBytes[0]); else out.write(Arrays.copyOf(addressBytes, 2)); } else { // If the address is a Tor-encapsulated IPv6, use the whole /48 (ie all tor addresses are the same group, for now) if (addressBytes[0] == 0xfd && addressBytes[1] == 0x87 && addressBytes[2] == 0xdb && addressBytes[3] == 0x7e && addressBytes[4] == 0xeb && addressBytes[5] == 0x43) out.write(Arrays.copyOf(addressBytes, 6)); // If the address is HE (tunnelbroker.net), use the /40 (they allocate up to /48s) else if (addressBytes[0] == 20 && addressBytes[1] == 1 && addressBytes[2] == 4 && addressBytes[3] == 70) out.write(Arrays.copyOf(addressBytes, 40/8)); else // otherwise just use the /32 out.write(Arrays.copyOf(addressBytes, 32/8)); } } // May return null if address.getAddr() != from.getAddr(), otherwise must return a PeerData private synchronized PeerData addAddress(PeerAddress address, PeerAddress from) { PeerData peer = addressToSetMap.get(address.getAddr()); if (peer == null) { peer = new PeerData(address); addressToSetMap.put(address.getAddr(), peer); try { // We write out information needed and use a cryptographic hash to ensure there are no sets of IP groups // which have a higher probability of filling all sets than any other sets of groups (and because we do // not use a secure random value for randomKey). // setSelector is used to select the set used within the possible sets for a given source group // it is used % SETS_PER_SOURCE as there should only be SETS_PER_SOURCE sets used per source group ByteArrayOutputStream setWithinGroupSelector = new UnsafeByteArrayOutputStream(); Utils.uint32ToByteStreamLE(randomKey, setWithinGroupSelector); writeAddressGroup(address, setWithinGroupSelector); ByteArrayOutputStream setSelector = new UnsafeByteArrayOutputStream(); Utils.uint32ToByteStreamLE(randomKey, setSelector); writeAddressGroup(from, setSelector); // Select which group of sets we will use // Now select one of SETS_PER_SOURCE sets to use within the selected group Utils.uint32ToByteStreamLE(Math.abs(Sha256Hash.create(setWithinGroupSelector.toByteArray()).hashCode()) % SETS_PER_SOURCE, setSelector); addressBuckets.get(Math.abs(Sha256Hash.create(setSelector.toByteArray()).hashCode()) % TOTAL_SETS).add(peer); } catch (IOException e) { throw new RuntimeException(e); } } else { // We only keep one entry per IP to ensure our set of peers is as diverse as possible, so if the port // differs we have to either throw peer away or replace it if (address.getPort() != peer.address.getPort()) { if (from.getAddr().equals(address.getAddr())) // If the node announced itself or we connected, replace peer.address = address; else // Otherwise just ignore the new address (the old one will get thrown out eventually if necessary) return null; } // Pick up new service bits peer.address.setServices(peer.address.getServices().or(address.getServices())); } peer.vTimeLastHeard = Utils.currentTimeMillis(); return peer; } /** * Creates a PeerDB for the given {@link PeerGroup}, adding this as a PeerDiscovery to the given group. */ public PeerDBDiscovery(NetworkParameters params, File db, PeerGroup group) { this.params = params; this.db = db; for (int i = 0; i < TOTAL_SETS; i++) addressBuckets.add(new AddressSet()); boolean doInit = !db.exists(); if (!doInit) doInit = !maybeLoadFromFile(db); if (doInit) randomKey = new Random(Utils.currentTimeMillis()).nextLong(); listenForPeers(group); group.addPeerDiscovery(this); } /** * Attaches a {@link PeerEventListener} to the given {@link PeerGroup} which listens for {@link AddressMessage} * announcements and peer connections to track known peers. */ public void listenForPeers(PeerGroup group) { group.addEventListener(new AbstractPeerEventListener() { @Override public Message onPreMessageReceived(Peer p, Message m) { if (m instanceof AddressMessage) { for (PeerAddress address : ((AddressMessage) m).getAddresses()) addAddress(address, p.getAddress()); } if (p.isAcked && !p.gaveAddrs) // Use the opportunity to send a GetAddr message if we have not // received an addr message with more than one address p.sendMessage(new GetAddrMessage(params)); return m; } @Override public void onPeerConnected(Peer p, int peerCount) { // When PeerGroups accept incoming connections, we should skip this and onPeerDisconnected addAddress(p.getAddress(), p.getAddress()).connected(); // We want addresses from them. p.sendMessage(new GetAddrMessage(params)); } @Override public void onPeerDisconnected(Peer p, int peerCount) { addAddress(p.getAddress(), p.getAddress()).disconnected(); } }, Threading.SAME_THREAD); } @Override public synchronized InetSocketAddress[] getPeers(long timeoutValue, TimeUnit timeoutUnit) throws PeerDiscoveryException { int addressesToReturn = Math.min(ADDRESSES_RETURNED, addressToSetMap.size()/MAX_ADDRESSES_FACTOR); InetSocketAddress[] addresses = new InetSocketAddress[addressesToReturn]; //TODO: There is a better way to get a random set here ArrayList<PeerData> peerList = new ArrayList<PeerData>(addressToSetMap.values()); Collections.shuffle(peerList); Iterator<PeerData> iterator = peerList.iterator(); for (int i = 0; i < addressesToReturn; i++) { try { PeerData peer = iterator.next(); while (peer.isBad() && iterator.hasNext()) peer = iterator.next(); if (iterator.hasNext()) addresses[i] = peer.address.toSocketAddress(); } catch (NoSuchElementException e) { // No more peers break; } } log.debug("Returning {} addresses from db discovery", addressesToReturn); return addresses; } @Override public void shutdown() { try { saveToFile(db); } catch (IOException e) { log.error("Failed to save Peer set to file", e); } } private synchronized boolean maybeLoadFromFile(File f) { try { InputStream s = new FileInputStream(f); byte[] versionAndRandomKeyBytes = new byte[12]; if (s.read(versionAndRandomKeyBytes) != versionAndRandomKeyBytes.length) { log.warn("Failed to read PeerDB Header"); return false; } if (Utils.readUint32(versionAndRandomKeyBytes, 0) != 1) { log.warn("PeerDB is a different version (expected version 1 got " + Utils.readUint32(versionAndRandomKeyBytes, 0) + ")"); return false; // Newer version } randomKey = Utils.readInt64(versionAndRandomKeyBytes, 4); for (int i = 0; i < TOTAL_SETS; i++) { byte[] addressCountBytes = new byte[4]; checkState(s.read(addressCountBytes) == addressCountBytes.length); int addresses = (int) Utils.readUint32(addressCountBytes, 0); checkState(addresses <= MAX_SET_SIZE); for (int j = 0; j < addresses; j++) { PeerData peer = new PeerData(s); addressBuckets.get(i).add(peer); addressToSetMap.put(peer.address.getAddr(), peer); } } return true; } catch (FileNotFoundException e) { return false; } catch (IllegalStateException e) { return false; } catch (IOException e) { log.error("Error reading PeerDB from file", e); return false; } } //TODO Call on a regular basis private synchronized void saveToFile(File f) throws IOException { OutputStream s = new FileOutputStream(f); Utils.uint32ToByteStreamLE(1, s); // Version tag Utils.int64ToByteStreamLE(randomKey, s); for (int i = 0; i < TOTAL_SETS; i++) { Utils.uint32ToByteStreamLE(addressBuckets.get(i).size(), s); for (PeerData peerData : addressBuckets.get(i)) peerData.write(s); } } }