/* This code is part of Freenet. It is distributed under the GNU General * Public License, version 2 (or at your option any later version). See * http://www.gnu.org/ for further details of the GPL. */ package freenet.node; import static java.util.concurrent.TimeUnit.DAYS; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.InetAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.EnumMap; import java.util.Enumeration; import java.util.Map; import freenet.crypt.Util; import freenet.io.comm.ByteCounter; import freenet.io.comm.DMT; import freenet.io.comm.DisconnectedException; import freenet.io.comm.FreenetInetAddress; import freenet.io.comm.Message; import freenet.io.comm.MessageFilter; import freenet.io.comm.NotConnectedException; import freenet.io.comm.Peer; import freenet.io.comm.PeerContext; import freenet.io.comm.PeerParseException; import freenet.io.comm.ReferenceSignatureVerificationException; import freenet.io.comm.RetrievalException; import freenet.io.comm.SlowAsyncMessageFilterCallback; import freenet.io.xfer.BulkReceiver; import freenet.io.xfer.BulkTransmitter; import freenet.io.xfer.BulkTransmitter.AllSentCallback; import freenet.io.xfer.PartiallyReceivedBulk; import freenet.node.OpennetPeerNode.NOT_DROP_REASON; import freenet.support.Fields; import freenet.support.HTMLNode; import freenet.support.LRUQueue; import freenet.support.LogThresholdCallback; import freenet.support.Logger; import freenet.support.Logger.LogLevel; import freenet.support.SimpleFieldSet; import freenet.support.TimeSortedHashtable; import freenet.support.io.ByteArrayRandomAccessBuffer; import freenet.support.io.Closer; import freenet.support.io.FileUtil; import freenet.support.io.NativeThread; import freenet.support.transport.ip.HostnameSyntaxException; import freenet.support.transport.ip.IPUtil; /** * Central location for all things opennet. * In particular: * - Opennet crypto * - LRU connections * * Both here and in OpennetPeerNode there are lots of dubious heuristics to avoid excessive * connection churn. * @author toad */ public class OpennetManager { final Node node; final NodeCrypto crypto; final Announcer announcer; final SeedAnnounceTracker seedTracker = new SeedAnnounceTracker(); /* The routing table is split into "buckets" by distance, each of which has a separate LRU * list. For now there are only 2 buckets; the PETS paper suggested many buckets, but this * would have larger overhead, more dependence on the network size, and it is not clear that * it is necessary at the moment. * * The measured global link length distribution showed a good (1/d) length distribution below * 0.01 (but nowhere near enough nodes) and a flat distribution above 0.01. Hence the choice of * LONG_DISTANCE as 0.01. It appeared that there were very few short links (~ 15% less than * 0.01 distance) and a lot of random long links, which is the opposite of what we need for * good routing, so requests would mostly bounce around randomly on the long links. * * LONG_PROPORTION is chosen as 30% for two reasons: (a) It is close to the Kleinberg optimum * (around 20%), and (b) it ensures that nodes with 10 connections still have 3 long links, so * long links cannot form chains and the routing still scales if the short routing is broken. * * See USK@ZLwcSLwqpM1527Tw1YmnSiXgzITU0neHQ11Cyl0iLmk,f6FLo3TvsEijIcJq-X3BTjjtm0ErVZwAPO7AUd9V7lY,AQACAAE/fix-link-length/7/ * (FIXME move to wiki or other permanent storage) */ /** Peers with more than this distance are considered "long links". */ static final double LONG_DISTANCE = 0.01; /** This proportion of the routing table consists of "long links". */ static final double LONG_PROPORTION = 0.3; /** The proportion of the routing table which consists of "short links". */ public static final double SHORT_PROPORTION = 1.0 - LONG_PROPORTION; enum LinkLengthClass { /** Shorter than LONG_DISTANCE */ SHORT { @Override public int getTargetPeers(int target) { int longPeers = (int) (target * LONG_PROPORTION); return target - longPeers; } }, /** Longer than LONG_DISTANCE */ LONG { @Override public int getTargetPeers(int target) { int longPeers = (int) (target * LONG_PROPORTION); return longPeers; } }; /** Get the target number of peers for this class, given the overall target number of peers */ public abstract int getTargetPeers(int target); } /** Peers LRUs by LinkLengthClass. PeerNodes are promoted within their LRU when they * successfully fetch a key. Normally we take the bottom peer, but if that isn't eligible * to be dropped, we iterate up the list. */ private final EnumMap<LinkLengthClass, LRUQueue<OpennetPeerNode>> peersLRUByDistance; /** Old peers. Opennet peers which we dropped but would still like to talk to * if we have no other option. */ private final LRUQueue<OpennetPeerNode> oldPeers; /** Maximum number of old peers */ static final int MAX_OLD_PEERS = 25; /** Time at which last dropped a peer due to an incoming connection of each type. */ private final EnumMap<ConnectionType,Long> timeLastDropped; // These only count stuff where we actually have a node to add. private final EnumMap<ConnectionType,Long> connectionAttempts; private final EnumMap<ConnectionType,Long> connectionAttemptsAdded; private final EnumMap<ConnectionType,Long> connectionAttemptsAddedPlentySpace; private final EnumMap<ConnectionType,Long> connectionAttemptsRejectedByPerTypeEnforcement; private final EnumMap<ConnectionType,Long> connectionAttemptsRejectedNoPeersDroppable; /** Number of successful CHK requests since last added a node. All values are incremented on a * successful request, but when we add a node, we reset the value for that type of node. */ private final EnumMap<ConnectionType,Long> successCount; /** Only drop a connection after at least this many successful requests. * This is per connection type. */ // FIXME should be a function of # opennet peers? max # opennet peers? ... public static final int MIN_SUCCESS_BETWEEN_DROP_CONNS = 10; /** Chance of resetting path folding (for plausible deniability) is 1 in this number. */ public static final int RESET_PATH_FOLDING_PROB = 20; /** Don't re-add a node until it's been up and disconnected for at least this long */ public static final long DONT_READD_TIME = MINUTES.toMillis(1); /** Don't drop a node until it's at least this old, if it's connected. */ public static final long DROP_MIN_AGE = MINUTES.toMillis(5); /** Don't drop a node until it's at least this old, if it's not connected (if it has connected once then DROP_DISCONNECT_DELAY applies, but only once an hour as below). Must be less than DROP_MIN_AGE. * Relatively generous because noderef transfers e.g. for announcement can be slow (Note * that announcements actually wait for previous transfers!). */ public static final long DROP_MIN_AGE_DISCONNECTED = MINUTES.toMillis(5); /** Don't drop a node until this long after startup */ public static final long DROP_STARTUP_DELAY = MINUTES.toMillis(2); /** Don't drop a node until this long after losing connection to it. * This should be long enough to cover a typical reboot, but not so long as to result in a lot * of disconnected nodes in the Strangers list. Also it should probably not be longer than DROP_MIN_AGE! */ public static final long DROP_DISCONNECT_DELAY = MINUTES.toMillis(5); /** But if it has disconnected more than once in this period, allow it to be dropped anyway */ public static final long DROP_DISCONNECT_DELAY_COOLDOWN = MINUTES.toMillis(60); /** Every DROP_CONNECTED_TIME, we may drop a peer even though it is connected. * This is per connection type, we should consider whether to reduce it further. */ public static final long DROP_CONNECTED_TIME = MINUTES.toMillis(5); /** Minimum time between offers, if we have maximum peers. Less than the above limits, * since an offer may not be accepted. */ public static final long MIN_TIME_BETWEEN_OFFERS = SECONDS.toMillis(30); private static volatile boolean logMINOR; static { Logger.registerLogThresholdCallback(new LogThresholdCallback(){ @Override public void shouldUpdate(){ logMINOR = Logger.shouldLog(LogLevel.MINOR, this); } }); } /** How big to pad opennet noderefs to? If they are bigger than this then we won't send them. */ public static final int PADDED_NODEREF_SIZE = 3072; /** Allow for future expansion. However at any given time all noderefs should be PADDED_NODEREF_SIZE */ public static final int MAX_OPENNET_NODEREF_LENGTH = 32768; /** Enable scaling of peers with bandwidth? */ public static final boolean ENABLE_PEERS_PER_KB_OUTPUT = true; /** Constant for scaling peers: we multiply bandwidth in kB/sec by this * and then take the square root. 12 gives 11 at 10K, 15 at 20K, 19 at * 30K, 26 at 60K, 34 at 100K, 40 at 140K, 100 at 2500K. * 212 at 30mbit/s (the mean upload in Japan in 2014) and * 363 at 88mbit/s (the mean upload in Hong Kong in 2014).*/ public static final double SCALING_CONSTANT = 12.0; /** * Minimum number of peers. Do not reduce this: As a rough estimate, because the vast majority * of requests complete in 5 hops, this gives just one binary decision per hop on average. */ public static final int MIN_PEERS_FOR_SCALING = 10; /** The maximum possible distance between two nodes in the wrapping [0,1) location space. */ public static final double MAX_DISTANCE = 0.5; /** The fraction of nodes which are only a short distance away. */ public static final double SHORT_NODES_FRACTION = LONG_DISTANCE / MAX_DISTANCE; /** The estimated average number of nodes which are active at any given time. */ public static final int LAST_NETWORK_SIZE_ESTIMATE = 5000; /** The estimated number of nodes which are a short distance away. */ public static final int AVAILABLE_SHORT_DISTANCE_NODES = (int) (LAST_NETWORK_SIZE_ESTIMATE * SHORT_NODES_FRACTION); /** * Maximum number of peers. * * This is limited by the expected availability of nodes with short links to a given location. * Above that number of peers, fast nodes will not be able to find enough peers with short * links. * * @see freenet.node.OpennetManager.LinkLengthClass */ public static final int MAX_PEERS_FOR_SCALING = (int) (AVAILABLE_SHORT_DISTANCE_NODES / SHORT_PROPORTION); /** Maximum number of peers for purposes of FOAF attack/sanity check */ public static final int PANIC_MAX_PEERS = MAX_PEERS_FOR_SCALING + 10; /** Stop trying to reconnect to an old-opennet-peer after a month. */ public static final long MAX_TIME_ON_OLD_OPENNET_PEERS = DAYS.toMillis(31); // This is only relevant while the connection is in the grace period. // Null means none of the above e.g. not in grace period. public enum ConnectionType { PATH_FOLDING, ANNOUNCE, RECONNECT } private final long creationTime; private boolean stopping; public OpennetManager(Node node, NodeCryptoConfig opennetConfig, long startupTime, boolean enableAnnouncement) throws NodeInitException { this.creationTime = System.currentTimeMillis(); this.node = node; crypto = new NodeCrypto(node, true, opennetConfig, startupTime, node.enableARKs); timeLastDropped = new EnumMap<ConnectionType,Long>(ConnectionType.class); connectionAttempts = new EnumMap<ConnectionType,Long>(ConnectionType.class); connectionAttemptsAdded = new EnumMap<ConnectionType,Long>(ConnectionType.class); connectionAttemptsAddedPlentySpace = new EnumMap<ConnectionType,Long>(ConnectionType.class); connectionAttemptsRejectedByPerTypeEnforcement = new EnumMap<ConnectionType,Long>(ConnectionType.class); connectionAttemptsRejectedNoPeersDroppable = new EnumMap<ConnectionType,Long>(ConnectionType.class); successCount = new EnumMap<ConnectionType,Long>(ConnectionType.class); for(ConnectionType c : ConnectionType.values()) { timeLastDropped.put(c, 0L); connectionAttempts.put(c, 0L); connectionAttemptsAdded.put(c, 0L); connectionAttemptsAddedPlentySpace.put(c, 0L); connectionAttemptsRejectedByPerTypeEnforcement.put(c, 0L); connectionAttemptsRejectedNoPeersDroppable.put(c, 0L); successCount.put(c, 0L); } File nodeFile = node.nodeDir().file("opennet-"+crypto.portNumber); File backupNodeFile = node.nodeDir().file("opennet-"+crypto.portNumber+".bak"); // Keep opennet crypto details in a separate file try { readFile(nodeFile); } catch (IOException e) { try { readFile(backupNodeFile); } catch (IOException e1) { crypto.initCrypto(); } } peersLRUByDistance = new EnumMap<LinkLengthClass, LRUQueue<OpennetPeerNode>>(LinkLengthClass.class); for(LinkLengthClass l : LinkLengthClass.values()) peersLRUByDistance.put(l, new LRUQueue<OpennetPeerNode>()); oldPeers = new LRUQueue<OpennetPeerNode>(); announcer = (enableAnnouncement ? new Announcer(this) : null); } public void writeFile() { File nodeFile = node.nodeDir().file("opennet-"+crypto.portNumber); File backupNodeFile = node.nodeDir().file("opennet-"+crypto.portNumber+".bak"); writeFile(nodeFile, backupNodeFile); } private void writeFile(File orig, File backup) { SimpleFieldSet fs = crypto.exportPrivateFieldSet(); if(orig.exists()) backup.delete(); FileOutputStream fos = null; OutputStreamWriter osr = null; BufferedWriter bw = null; try { fos = new FileOutputStream(backup); osr = new OutputStreamWriter(fos, "UTF-8"); bw = new BufferedWriter(osr); fs.writeTo(bw); bw.close(); FileUtil.renameTo(backup, orig); } catch (IOException e) { Closer.close(bw); Closer.close(osr); Closer.close(fos); } } private void readFile(File filename) throws IOException { // REDFLAG: Any way to share this code with Node and NodePeer? FileInputStream fis = new FileInputStream(filename); InputStreamReader isr = new InputStreamReader(fis, "UTF-8"); BufferedReader br = new BufferedReader(isr); SimpleFieldSet fs = new SimpleFieldSet(br, false, true); br.close(); // Read contents String[] udp = fs.getAll("physical.udp"); if((udp != null) && (udp.length > 0)) { for(String u: udp) { // Just keep the first one with the correct port number. Peer p; try { p = new Peer(u, false, true); } catch (HostnameSyntaxException e) { Logger.error(this, "Invalid hostname or IP Address syntax error while loading opennet peer node reference: "+u); System.err.println("Invalid hostname or IP Address syntax error while loading opennet peer node reference: "+u); continue; } catch (PeerParseException e) { throw (IOException)new IOException().initCause(e); } if(p.getPort() == crypto.portNumber) { // DNSRequester doesn't deal with our own node node.ipDetector.setOldIPAddress(p.getFreenetAddress()); break; } } } crypto.readCrypto(fs); } public void start() { synchronized(this) { stopping = false; } // Do this outside the constructor, since the constructor is called by the Node constructor, and callbacks may make assumptions about data structures being ready. node.peers.tryReadPeers(node.nodeDir().file("openpeers-"+crypto.portNumber).toString(), crypto, this, true, false); OpennetPeerNode[] nodes = node.peers.getOpennetPeers(); Arrays.sort(nodes, new Comparator<OpennetPeerNode>() { @Override public int compare(OpennetPeerNode pn1, OpennetPeerNode pn2) { if(pn1 == pn2) return 0; long lastSuccess1 = pn1.timeLastSuccess(); long lastSuccess2 = pn2.timeLastSuccess(); if(lastSuccess1 > lastSuccess2) return 1; if(lastSuccess2 > lastSuccess1) return -1; boolean neverConnected1 = pn1.neverConnected(); boolean neverConnected2 = pn2.neverConnected(); if(neverConnected1 && (!neverConnected2)) return -1; if((!neverConnected1) && neverConnected2) return 1; // a-b not opposite sign to b-a possible in a corner case (a=0 b=Integer.MIN_VALUE). if(pn1.hashCode > pn2.hashCode) return 1; else if(pn1.hashCode < pn2.hashCode) return -1; Logger.error(this, "Two OpennetPeerNodes with the same hashcode: "+pn1+" vs "+pn2); return Fields.compareObjectID(pn1, pn2); } }); for(OpennetPeerNode opn: nodes) { // Drop any peers which don't have a location yet. That means we haven't connected to // them yet, and we need the location to decide which LRU to put them in ... // This should only be a problem with old nodes; we will include the location in new // path folding noderefs... if(Location.isValid(opn.getLocation())) lruQueue(opn).push(opn); else node.peers.disconnectAndRemove(opn, false, false, false); } if(logMINOR) { Logger.minor(this, "My full compressed ref: "+crypto.myCompressedFullRef().length); Logger.minor(this, "My full setup ref: "+crypto.myCompressedSetupRef().length); Logger.minor(this, "My heavy setup ref: "+crypto.myCompressedHeavySetupRef().length); } dropAllExcessPeers(); writeFile(); // Read old peers node.peers.tryReadPeers(node.nodeDir().file("openpeers-old-"+crypto.portNumber).toString(), crypto, this, true, true); crypto.start(); if(announcer!= null) announcer.start(); } /** * Called when opennet is disabled */ public void stop(boolean purge) { synchronized(this) { stopping = true; } if(announcer != null) announcer.stop(); crypto.stop(); if(purge) node.peers.removeOpennetPeers(); crypto.socket.getAddressTracker().setPresumedInnocent(); } synchronized boolean stopping() { return stopping; } private LRUQueue<OpennetPeerNode> lruQueue(LinkLengthClass distance) { return peersLRUByDistance.get(distance); } private LRUQueue<OpennetPeerNode> lruQueue(OpennetPeerNode pn) { return lruQueue(pn.linkLengthClass()); } public boolean alreadyHaveOpennetNode(SimpleFieldSet fs) { try { // FIXME OPT can we do this cheaper? // Maybe just parse the pubkey, and then compare it with the existing peers? OpennetPeerNode pn = new OpennetPeerNode(fs, node, crypto, this, false); if(lruQueue(pn).contains(pn)) { if(logMINOR) Logger.minor(this, "Not adding "+pn.userToString()+" to opennet list as already there"); return true; } // Don't check for self. That should be passed through too. return false; } catch (Throwable t) { // Don't break the code flow in the caller which is normally a request. Logger.error(this, "Caught "+t+" parsing opennet node from fieldset", t); return false; } } public OpennetPeerNode addNewOpennetNode(SimpleFieldSet fs, ConnectionType connectionType, boolean allowExisting) throws FSParseException, PeerParseException, ReferenceSignatureVerificationException { try { OpennetPeerNode pn = new OpennetPeerNode(fs, node, crypto, this, false); if(Arrays.equals(pn.peerECDSAPubKeyHash, crypto.ecdsaPubKeyHash)) { if(logMINOR) Logger.minor(this, "Not adding self as opennet peer"); return null; // Equal to myself } LinkLengthClass distance = pn.linkLengthClass(); LRUQueue<OpennetPeerNode> peersLRU = lruQueue(distance); if(peersLRU.contains(pn)) { if(logMINOR) Logger.minor(this, "Not adding "+pn.userToString()+" to opennet list as already there"); if(allowExisting) { // However, we can reconnect. return (OpennetPeerNode) peersLRU.get(pn); } else { return null; } } if(pn.isUnroutableOlderVersion() && node.nodeUpdater != null && node.nodeUpdater.dontAllowUOM()) { // We can't send the UOM to it, so we should not accept it. // Plus, some versions around 1320 had big problems with being connected both as a seednode and as an opennet peer. return null; } if(wantPeer(pn, true, false, false, connectionType, distance)) return pn; else return null; // Start at bottom. Node must prove itself. } catch (Throwable t) { // Don't break the code flow in the caller which is normally a request. Logger.error(this, "Caught "+t+" adding opennet node from fieldset", t); return null; } } /** When did we last offer our noderef to some other node? */ private long timeLastOffered; void forceAddPeer(OpennetPeerNode nodeToAddNow, boolean addAtLRU) { LinkLengthClass distance = nodeToAddNow.linkLengthClass(); LRUQueue<OpennetPeerNode> peersLRU = lruQueue(distance); synchronized(this) { if(addAtLRU) peersLRU.pushLeast(nodeToAddNow); else peersLRU.push(nodeToAddNow); oldPeers.remove(nodeToAddNow); } dropExcessPeers(distance); } public boolean wantPeer(OpennetPeerNode nodeToAddNow, boolean addAtLRU, boolean justChecking, boolean oldOpennetPeer, ConnectionType connectionType) { if(nodeToAddNow != null) { if(!Location.isValid(nodeToAddNow.getLocation())) { Logger.error(this, "Added opennet node reference must include a valid location", new Exception("error")); return false; } // We have received a node reference, so we know whether it is long or short. LinkLengthClass distance = nodeToAddNow.linkLengthClass(); return wantPeer(nodeToAddNow, addAtLRU, justChecking, oldOpennetPeer, connectionType, distance); } else { // Initiate path folding if we want a long link *or* a short link. // FIXME ideally we'd like to indicate whether we want long links or short links. return wantPeer(nodeToAddNow, addAtLRU, justChecking, oldOpennetPeer, connectionType, LinkLengthClass.SHORT) || wantPeer(nodeToAddNow, addAtLRU, justChecking, oldOpennetPeer, connectionType, LinkLengthClass.LONG); } } /** * Trim the peers list and possibly add a new node. Note that if we are not adding a new node, * we will only return true every MIN_TIME_BETWEEN_OFFERS, to prevent problems caused by many * pending offers being accepted simultaneously. * @param nodeToAddNow Node to add. Can be null, which means the caller needs to know whether * we have space for another node, without actually adding one. This happens e.g. when we are * the data source and are trying to decide whether to send our noderef downstream for path * folding. * @param addAtLRU If there is a node to add, add it at the bottom rather than the top. Normally * we set this on new path folded nodes so that they will be replaced if during the trial period, * plus the time it takes to get a new path folding offer, they don't have a successful request. * @param justChecking If true, we want to know whether there is space for a node to be added * RIGHT NOW. If false, the normal behaviour applies: if nodeToAddNow is passed in, we decide * whether to add that node, if it's null, we decide whether to send an offer subject to the * inter-offer time. * @param oldOpennetPeer If true, we are trying to add an old-opennet-peer which has reconnected. * There is a throttle, we accept no more than one old-opennet-peer every 30 seconds. On receiving * a packet, we call once to decide whether to try to parse it against the old-opennet-peers, and * then again to decide whether it is worth keeping; in the latter case if we decide not, the * old-opennet-peer will be told to disconnect and go away, but normally we don't reach that point * because of the first check. * @param isLong True if the peer to add is distant. False otherwise. * @return True if the node was added / should be added. */ public boolean wantPeer(OpennetPeerNode nodeToAddNow, boolean addAtLRU, boolean justChecking, boolean oldOpennetPeer, ConnectionType connectionType, LinkLengthClass distance) { LRUQueue<OpennetPeerNode> peersLRU = lruQueue(distance); boolean notMany = false; boolean noDisconnect; long now = System.currentTimeMillis(); if(logMINOR) Logger.minor(this, "wantPeer("+(nodeToAddNow != null) + "," +addAtLRU+","+justChecking+","+oldOpennetPeer+","+connectionType+","+distance+")"); boolean outdated = nodeToAddNow == null ? false : nodeToAddNow.isUnroutableOlderVersion(); if(outdated && logMINOR) Logger.minor(this, "Peer is outdated: "+nodeToAddNow.getVersionNumber()+" for "+connectionType); if(outdated) { if(tooManyOutdatedPeers()) { if(logMINOR) Logger.minor(this, "Rejecting TOO OLD peer from "+connectionType+" (too many already): "+nodeToAddNow); return false; } } if(nodeToAddNow != null && crypto.config.oneConnectionPerAddress()) { boolean okay = false; boolean any = false; Peer[] handshakeIPs = nodeToAddNow.getHandshakeIPs(); if(handshakeIPs != null) { for(Peer p : handshakeIPs) { if(p == null) continue; FreenetInetAddress addr = p.getFreenetAddress(); if(addr == null) continue; InetAddress a = addr.getAddress(false); if(a == null) continue; if(a.isAnyLocalAddress() || a.isLinkLocalAddress() || IPUtil.isSiteLocalAddress(a)) continue; any = true; if(crypto.allowConnection(nodeToAddNow, addr)) okay = true; else { // if NodeCrypto reject *any* address, reject peer okay = false; break; } } } else { Logger.error(this, "Peer does not have any IP addresses???"); } if(any && !okay) { Logger.normal(this, "Rejecting peer as we are already connected to a peer with the same IP address"); return false; } } int maxPeers = getNumberOfConnectedPeersToAim(distance); if(logMINOR) Logger.minor(this, "Peers target: "+maxPeers); synchronized(this) { if(nodeToAddNow != null && peersLRU.contains(nodeToAddNow)) { if(logMINOR) Logger.minor(this, "Opennet peer already present in LRU: "+nodeToAddNow); return true; } if(nodeToAddNow != null) connectionAttempts.put(connectionType, connectionAttempts.get(connectionType)+1); if(getSize(distance) < maxPeers || outdated) { if(nodeToAddNow != null) { if(logMINOR) Logger.minor(this, "Added opennet peer "+nodeToAddNow+" as opennet peers list not full"); if(addAtLRU) peersLRU.pushLeast(nodeToAddNow); else peersLRU.push(nodeToAddNow); oldPeers.remove(nodeToAddNow); connectionAttemptsAddedPlentySpace.put(connectionType, connectionAttemptsAddedPlentySpace.get(connectionType)+1); } else { if(logMINOR) Logger.minor(this, "Want peer because not enough opennet nodes"); } if(nodeToAddNow == null && !justChecking) timeLastOffered = System.currentTimeMillis(); notMany = true; // Don't check timeLastAddedOldOpennetPeer, since we want it anyway. But do update it. } // Old opennet peers should only replace free slots / disconnected droppable nodes. // We can make offers regardless of timeLastOffered provided they are disconnected droppable peers. // And we only allow a connection to be dropped every 10 successful fetches. noDisconnect = successCount.get(connectionType) < MIN_SUCCESS_BETWEEN_DROP_CONNS || oldOpennetPeer || (nodeToAddNow == null && now - timeLastOffered <= MIN_TIME_BETWEEN_OFFERS) || now - timeLastDropped.get(connectionType) < DROP_CONNECTED_TIME; } if(nodeToAddNow != null) nodeToAddNow.setAddedReason(connectionType); if(notMany) { if(nodeToAddNow != null) { node.peers.addPeer(nodeToAddNow, true, true); // Add to peers outside the OM lock } return true; } boolean canAdd = true; ArrayList<OpennetPeerNode> dropList = new ArrayList<OpennetPeerNode>(); maxPeers = getNumberOfConnectedPeersToAim(distance); synchronized(this) { int size = getSize(distance); if(size == maxPeers && nodeToAddNow == null) { // Allow an offer to be predicated on throwing out a connected node, // provided that we meet the other criteria e.g. time since last added, // node isn't too new. OpennetPeerNode toDrop = peerToDrop(noDisconnect, false, nodeToAddNow != null, connectionType, maxPeers, distance, peersLRU); if(toDrop == null) { if(logMINOR) Logger.minor(this, "No more peers to drop (in first bit), still "+peersLRU.size()+" peers, cannot accept peer"+(nodeToAddNow == null ? "" : nodeToAddNow.toString())); canAdd = false; if(nodeToAddNow != null) connectionAttemptsRejectedNoPeersDroppable.put(connectionType, connectionAttemptsRejectedNoPeersDroppable.get(connectionType)+1); } else { // Only check per-type limits if we are throwing out connected peers. // This is important for bootstrapping, given the low announcement limit. if(toDrop.isConnected() && enforcePerTypeGracePeriodLimits(maxPeers, connectionType, nodeToAddNow != null, peersLRU)) { if(nodeToAddNow != null) connectionAttemptsRejectedByPerTypeEnforcement.put(connectionType, connectionAttemptsRejectedByPerTypeEnforcement.get(connectionType)+1); return false; } } } else while(canAdd && (size = getSize(distance)) > maxPeers - ((nodeToAddNow == null || outdated) ? 0 : 1)) { OpennetPeerNode toDrop; // can drop peers which are over the limit toDrop = peerToDrop(noDisconnect, false, nodeToAddNow != null, connectionType, maxPeers, distance, peersLRU); if(toDrop == null) { if(logMINOR) Logger.minor(this, "No more peers to drop, still "+peersLRU.size()+" peers, cannot accept peer"+(nodeToAddNow == null ? "" : nodeToAddNow.toString())); canAdd = false; if(nodeToAddNow != null) connectionAttemptsRejectedNoPeersDroppable.put(connectionType, connectionAttemptsRejectedNoPeersDroppable.get(connectionType)+1); break; } // Only check per-type limits if we are throwing out connected peers. // This is important for bootstrapping, given the low announcement limit. if(toDrop.isConnected() && enforcePerTypeGracePeriodLimits(maxPeers, connectionType, nodeToAddNow != null, peersLRU)) { if(nodeToAddNow != null) connectionAttemptsRejectedByPerTypeEnforcement.put(connectionType, connectionAttemptsRejectedByPerTypeEnforcement.get(connectionType)+1); return false; } if(nodeToAddNow != null || size > maxPeers) { if(logMINOR) Logger.minor(this, "Drop opennet peer: "+toDrop+" (connected="+toDrop.isConnected()+") of "+peersLRU.size()+":"+getSize(distance)); peersLRU.remove(toDrop); dropList.add(toDrop); } } if(canAdd && !justChecking) { if(nodeToAddNow != null) { successCount.put(connectionType, 0L); if(addAtLRU) peersLRU.pushLeast(nodeToAddNow); else peersLRU.push(nodeToAddNow); if(logMINOR) Logger.minor(this, "Added opennet peer "+nodeToAddNow+" after clearing "+dropList.size()+" items - now have "+peersLRU.size()+" opennet peers"); oldPeers.remove(nodeToAddNow); if(!dropList.isEmpty()) { if(logMINOR) Logger.minor(this, "Dropped opennet peer: "+dropList.get(0)); timeLastDropped.put(connectionType, now); } connectionAttemptsAdded.put(connectionType, connectionAttemptsAdded.get(connectionType)+1); } else { // Do not update timeLastDropped, anything dropped was over the limit so doesn't count (because nodeToAddNow == null). if(!justChecking) { timeLastOffered = now; if(logMINOR) Logger.minor(this, "Sending offer"); } } } } if(nodeToAddNow != null && canAdd && !node.peers.addPeer(nodeToAddNow, true, true)) { if(logMINOR) Logger.minor(this, "Already in global peers list: "+nodeToAddNow+" when adding opennet node"); // Just because it's in the global peers list doesn't mean its in the LRU, it may be an old-opennet-peers reconnection. // In which case we add it to the global peers list *before* adding it here. } for(OpennetPeerNode pn : dropList) { if(logMINOR) Logger.minor(this, "Dropping LRU opennet peer: "+pn); pn.setAddedReason(null); node.peers.disconnectAndRemove(pn, true, true, true); } return canAdd; } private int maxOutdatedPeers() { return Math.max(5, getNumberOfConnectedPeersToAimIncludingDarknet() / 4); } private boolean tooManyOutdatedPeers() { // This does not check whether they are short or long as it is irrelevant for outdated peers. int maxTooOldPeers = maxOutdatedPeers(); int count = 0; OpennetPeerNode[] peers = node.peers.getOpennetPeers(); for(OpennetPeerNode pn : peers) { if(pn.isUnroutableOlderVersion()) { count++; if(count >= maxTooOldPeers) return true; } } return false; } private synchronized boolean enforcePerTypeGracePeriodLimits(int maxPeers, ConnectionType type, boolean addingPeer, LRUQueue<OpennetPeerNode> peersLRU) { if(type == null) { if(logMINOR) Logger.minor(this, "No type set, not enforcing per type limits"); } // We do NOT want to have all our peers in grace periods! // For opennet to work, we need LRU. For LRU to work it needs a choice. // If everything is in a grace period, then we have no choice - we replace the one node that comes out of its grace period as soon as it does. // So first calculate an overall limit on the number of peers in grace periods. // Heuristic: Half rounded down. int maxGracePeriodPeers = maxPeers / 2; int announceMax; int reconnectMax; int pathFoldingMax; // Same total global number of slots as 1242/1243. announceMax = reconnectMax = (maxGracePeriodPeers / 5) + 1; pathFoldingMax = maxGracePeriodPeers - announceMax - reconnectMax; if(pathFoldingMax < 2) return false; if(logMINOR) Logger.minor(this, "Per type grace period limits: total peers: "+maxPeers+" announce "+announceMax+" reconnect "+reconnectMax+" path folding "+pathFoldingMax); int myLimit; if(type == ConnectionType.PATH_FOLDING) myLimit = pathFoldingMax; else if(type == ConnectionType.ANNOUNCE) myLimit = announceMax; else myLimit = reconnectMax; int count = 0; OpennetPeerNode[] peers = peersLRU.toArray(new OpennetPeerNode[peersLRU.size()]); for(OpennetPeerNode pn : peers) { if(pn.getAddedReason() != type) continue; if(!pn.isConnected()) continue; if(pn.isDroppable(false)) continue; if(++count >= myLimit) { if(logMINOR) Logger.minor(this, "Per type grace period limit rejected peer of type "+type+" count is "+count+" limit is "+myLimit); return true; } } if(logMINOR) Logger.minor(this, "Per type grace period limit allowed connection of type "+type+" count is "+count+" limit is "+myLimit+" addingPeer="+addingPeer); return false; } void dropAllExcessPeers() { for(LinkLengthClass l : LinkLengthClass.values()) dropExcessPeers(l); } void dropExcessPeers(LinkLengthClass distance) { LRUQueue<OpennetPeerNode> peersLRU = lruQueue(distance); int maxPeers = getNumberOfConnectedPeersToAim(distance); while(peersLRU.size() > maxPeers) { if(logMINOR) Logger.minor(this, "Dropping opennet peers: currently "+peersLRU.size()+" of "+maxPeers+" for "+distance+" distance links"); OpennetPeerNode toDrop; toDrop = peerToDrop(false, false, false, null, maxPeers, distance, peersLRU); if(toDrop == null) toDrop = peerToDrop(false, true, false, null, maxPeers, distance, peersLRU); if(toDrop == null) return; synchronized(this) { peersLRU.remove(toDrop); } if(logMINOR) Logger.minor(this, "Dropping "+toDrop); node.peers.disconnectAndRemove(toDrop, true, true, true); } } // A TOO OLD peer does not count towards the limit, even if it is not connected. // It can however be dumped if it doesn't connect in a reasonable time, and if // it upgrades, it may not have the usual grace period. /** * How many opennet peers do we have? * Connected but out of date nodes don't count towards the connection limit. Let them connect for * long enough to auto-update. They will be disconnected eventually, and then removed: * @see OpennetPeerNode#shouldDisconnectAndRemoveNow() */ synchronized public int getSize(LinkLengthClass distance) { int x = 0; for (Enumeration<OpennetPeerNode> e = lruQueue(distance).elements(); e.hasMoreElements();) { OpennetPeerNode pn = e.nextElement(); if(!pn.isUnroutableOlderVersion()) x++; } return x; } private OpennetPeerNode peerToDrop(boolean noDisconnect, boolean force, boolean addingNode, ConnectionType connectionType, int maxPeers, LinkLengthClass distance, LRUQueue<OpennetPeerNode> peersLRU) { if(getSize(distance) < maxPeers) { // Don't drop any peers if(logMINOR) Logger.minor(this, "peerToDrop(): Not dropping any peer (force="+force+" addingNode="+addingNode+") because don't need to"); return null; } synchronized(this) { EnumMap<NOT_DROP_REASON, Integer> map = null; if(addingNode) map = new EnumMap<NOT_DROP_REASON, Integer>(NOT_DROP_REASON.class); // Do we want it? OpennetPeerNode[] peers = peersLRU.toArrayOrdered(new OpennetPeerNode[peersLRU.size()]); for(OpennetPeerNode pn: peers) { if(pn == null) continue; boolean tooOld = pn.isUnroutableOlderVersion(); if(pn.isConnected() && tooOld) { // Doesn't count towards the opennet peers limit, so no point dropping it. continue; } NOT_DROP_REASON reason = pn.isDroppableWithReason(false); if(map != null) { Integer x = map.get(reason); if(x == null) map.put(reason, 1); else map.put(reason, x+1); } // Over the limit does not force us to drop TOO OLD peers since they don't count towards the limit. if((reason != NOT_DROP_REASON.DROPPABLE) && ((!force) || tooOld)) { continue; } // LOCKING: Always take the OpennetManager lock first if(!pn.isConnected()) { if(logMINOR) Logger.minor(this, "Possibly dropping opennet peer "+pn+" as is disconnected (reason="+reason+" force="+force+" tooOld="+tooOld); pn.setWasDropped(); return pn; } } if(noDisconnect) { if(addingNode && logMINOR) { Logger.minor(this, "Not disconnecting"); if(map != null) for(Map.Entry<NOT_DROP_REASON, Integer> entry : map.entrySet()) { Logger.minor(this, ""+entry.getKey()+" : "+entry.getValue()); } } return null; } if(map != null) map.clear(); for(OpennetPeerNode pn: peers) { if(pn == null) continue; boolean tooOld = pn.isUnroutableOlderVersion(); if(pn.isConnected() && tooOld) { // Doesn't count anyway. continue; } NOT_DROP_REASON reason = pn.isDroppableWithReason(false); if(map != null) { Integer x = map.get(reason); if(x == null) map.put(reason, 1); else map.put(reason, x+1); } // Over the limit does not force us to drop TOO OLD peers since they don't count towards the limit. if((reason != NOT_DROP_REASON.DROPPABLE) && ((!force) || tooOld)) { continue; } if(logMINOR) Logger.minor(this, "Possibly dropping opennet peer "+pn+" "+ ((connectionType == null) ? "" : ((System.currentTimeMillis() - timeLastDropped.get(connectionType))+" ms since last dropped peer of type "+connectionType))); pn.setWasDropped(); return pn; } if(addingNode && logMINOR) { Logger.minor(this, "Nothing to drop"); if(map != null) for(Map.Entry<NOT_DROP_REASON, Integer> entry : map.entrySet()) { Logger.minor(this, ""+entry.getKey()+" : "+entry.getValue()); } } } return null; } public void onSuccess(OpennetPeerNode pn) { LinkLengthClass distance = pn.linkLengthClass(); LRUQueue<OpennetPeerNode> peersLRU = lruQueue(distance); synchronized(this) { for(ConnectionType type : ConnectionType.values()) successCount.put(type, successCount.get(type)+1); if(peersLRU.contains(pn)) { peersLRU.push(pn); if(logMINOR) Logger.minor(this, "Opennet peer "+pn+" promoted to top of LRU because of successful request"); return; } else { if(logMINOR) Logger.minor(this, "Success on opennet peer which isn't in the LRU!: "+pn, new Exception("debug")); // Re-add it: nasty race condition when we have few peers } } if(!wantPeer(pn, false, false, false, ConnectionType.RECONNECT, distance)) // Start at top as it just succeeded node.peers.disconnectAndRemove(pn, true, false, true); } public void onRemove(OpennetPeerNode pn) { long now = System.currentTimeMillis(); LRUQueue<OpennetPeerNode> peersLRU = lruQueue(pn); synchronized (this) { peersLRU.remove(pn); if(pn.isDroppable(true) && !pn.grabWasDropped()) { if(logMINOR) Logger.minor(this, "onRemove() for "+pn); if(pn.timeLastConnected(now) > 0) { // Don't even add it if it never connected. oldPeers.push(pn); while (oldPeers.size() > MAX_OLD_PEERS) oldPeers.pop(); } } } } synchronized OpennetPeerNode[] getOldPeers() { return oldPeers.toArrayOrdered(new OpennetPeerNode[oldPeers.size()]); } synchronized OpennetPeerNode[] getUnsortedOldPeers() { return oldPeers.toArray(new OpennetPeerNode[oldPeers.size()]); } /** * Add an old opennet node - a node which might try to reconnect, and which we should accept * if we are desperate. * @param pn The node to add to the old opennet nodes LRU. */ synchronized void addOldOpennetNode(OpennetPeerNode pn) { oldPeers.push(pn); } final String getOldPeersFilename() { return node.nodeDir().file("openpeers-old-"+crypto.portNumber).toString(); } synchronized int countOldOpennetPeers() { return oldPeers.size(); } OpennetPeerNode randomOldOpennetNode() { OpennetPeerNode[] nodes = getUnsortedOldPeers(); if(nodes.length == 0) return null; return nodes[node.random.nextInt(nodes.length)]; } public synchronized void purgeOldOpennetPeer(OpennetPeerNode source) { oldPeers.remove(source); } public int getNumberOfConnectedPeersToAimIncludingDarknet() { int max = node.getMaxOpennetPeers(); if(ENABLE_PEERS_PER_KB_OUTPUT) { int obwLimit = node.getOutputBandwidthLimit(); int targetPeers = (int)Math.round(Math.min(MAX_PEERS_FOR_SCALING, Math.sqrt(obwLimit * SCALING_CONSTANT / 1000.0))); if(targetPeers < MIN_PEERS_FOR_SCALING) targetPeers = MIN_PEERS_FOR_SCALING; if(max > targetPeers) max = targetPeers; // Allow user to reduce it. } return max; } /** Get the target number of opennet peers. Do not call while holding locks. */ public int getNumberOfConnectedPeersToAim(LinkLengthClass distance) { if(distance == null) throw new IllegalArgumentException(); int target = getNumberOfConnectedPeersToAim(); return distance.getTargetPeers(target); } public int getNumberOfConnectedPeersToAim() { int max = getNumberOfConnectedPeersToAimIncludingDarknet(); return max - node.peers.countConnectedDarknetPeers(); } public void sendOpennetRef(boolean isReply, long uid, PeerNode peer, byte[] noderef, ByteCounter ctr) throws NotConnectedException { sendOpennetRef(isReply, uid, peer, noderef, ctr, null); } /** * Send our opennet noderef to a node. * @param isReply If true, send an FNPOpennetConnectReply, else send an FNPOpennetConnectDestination. * @param uid The unique ID of the request chain involved. * @param peer The node to send the noderef to. Not necessarily an OpennetPeerNode, as path * folding and possibly announcement can pass through darknet. * @param cs The full compressed noderef to send. * @throws NotConnectedException If the peer becomes disconnected while we are trying to send the noderef. */ public boolean sendOpennetRef(boolean isReply, long uid, PeerNode peer, byte[] noderef, ByteCounter ctr, AllSentCallback cb) throws NotConnectedException { byte[] padded = new byte[paddedSize(noderef.length)]; if(noderef.length > padded.length) { Logger.error(this, "Noderef too big: "+noderef.length+" bytes"); return false; } System.arraycopy(noderef, 0, padded, 0, noderef.length); Util.randomBytes(node.fastWeakRandom, padded, noderef.length, padded.length-noderef.length); long xferUID = node.random.nextLong(); Message msg2 = isReply ? DMT.createFNPOpennetConnectReplyNew(uid, xferUID, noderef.length, padded.length) : DMT.createFNPOpennetConnectDestinationNew(uid, xferUID, noderef.length, padded.length); peer.sendAsync(msg2, null, ctr); return innerSendOpennetRef(xferUID, padded, peer, ctr, cb); } /** * Just the actual transfer. * @param xferUID The transfer UID * @param padded The length of the data to transfer. * @param peer The peer to send it to. * @param cb * @throws NotConnectedException If the peer is not connected, or we lose the connection to the peer, * or it restarts. */ private boolean innerSendOpennetRef(long xferUID, byte[] padded, PeerNode peer, ByteCounter ctr, AllSentCallback cb) throws NotConnectedException { ByteArrayRandomAccessBuffer raf = new ByteArrayRandomAccessBuffer(padded); raf.setReadOnly(); PartiallyReceivedBulk prb = new PartiallyReceivedBulk(node.usm, padded.length, Node.PACKET_SIZE, raf, true); try { BulkTransmitter bt = new BulkTransmitter(prb, peer, xferUID, true, ctr, true, cb); return bt.send(); } catch (DisconnectedException e) { throw new NotConnectedException(e); } } public long startSendAnnouncementRequest(long uid, PeerNode peer, byte[] noderef, ByteCounter ctr, double target, short htl) throws NotConnectedException { long xferUID = node.random.nextLong(); Message msg = DMT.createFNPOpennetAnnounceRequest(uid, xferUID, noderef.length, paddedSize(noderef.length), target, htl); peer.sendAsync(msg, null, ctr); return xferUID; } public void finishSentAnnouncementRequest(PeerNode peer, byte[] noderef, ByteCounter ctr, long xferUID) throws NotConnectedException { byte[] padded = new byte[paddedSize(noderef.length)]; System.arraycopy(noderef, 0, padded, 0, noderef.length); Util.randomBytes(node.fastWeakRandom, padded, noderef.length, padded.length-noderef.length); innerSendOpennetRef(xferUID, padded, peer, ctr, null); } private int paddedSize(int length) { if(length < PADDED_NODEREF_SIZE) return PADDED_NODEREF_SIZE; Logger.normal(this, "Large noderef: "+length); if(length > MAX_OPENNET_NODEREF_LENGTH) throw new IllegalArgumentException("Too big noderef: "+length+" limit is "+MAX_OPENNET_NODEREF_LENGTH); return ((length >>> 10) + ((length & 1023) == 0 ? 0 : 1)) << 10; } public void sendAnnouncementReply(long uid, PeerNode peer, byte[] noderef, ByteCounter ctr) throws NotConnectedException { byte[] padded = new byte[PADDED_NODEREF_SIZE]; if(noderef.length > padded.length) { Logger.error(this, "Noderef too big: "+noderef.length+" bytes"); return; } System.arraycopy(noderef, 0, padded, 0, noderef.length); long xferUID = node.random.nextLong(); Message msg = DMT.createFNPOpennetAnnounceReply(uid, xferUID, noderef.length, padded.length); peer.sendAsync(msg, null, ctr); innerSendOpennetRef(xferUID, padded, peer, ctr, null); } interface NoderefCallback { /** Got a noderef. */ void gotNoderef(byte[] noderef); /** Timed out waiting for a noderef. */ void timedOut(); /** Got an ack - didn't timeout but there won't be a noderef. * @param timedOutMessage */ void acked(boolean timedOutMessage); } private static class SyncNoderefCallback implements NoderefCallback { byte[] returned; boolean finished; boolean timedOut; @Override public synchronized void timedOut() { timedOut = true; finished = true; notifyAll(); } @Override public void acked(boolean timedOutMessage) { gotNoderef(null); } @Override public synchronized void gotNoderef(byte[] noderef) { returned = noderef; finished = true; notifyAll(); } public synchronized byte[] waitForResult() throws WaitedTooLongForOpennetNoderefException { while(!finished) try { wait(); } catch (InterruptedException e) { // Ignore } if(timedOut) throw new WaitedTooLongForOpennetNoderefException(); return returned; } } @SuppressWarnings("serial") static class WaitedTooLongForOpennetNoderefException extends Exception { } /** * Wait for an opennet noderef. * @param isReply If true, wait for an FNPOpennetConnectReply[New], if false wait for an FNPOpennetConnectDestination[New]. * @param uid The UID of the parent request. * @return An opennet noderef. */ public static byte[] waitForOpennetNoderef(boolean isReply, PeerNode source, long uid, ByteCounter ctr, Node node) throws WaitedTooLongForOpennetNoderefException { SyncNoderefCallback cb = new SyncNoderefCallback(); if(logMINOR) Logger.minor(OpennetManager.class, "Waiting for opennet noderef on "+uid+" from "+source+" reply="+isReply); waitForOpennetNoderef(isReply, source, uid, ctr, cb, node); return cb.waitForResult(); } public static void waitForOpennetNoderef(final boolean isReply, final PeerNode source, final long uid, final ByteCounter ctr, final NoderefCallback callback, final Node node) { // FIXME remove back compat code MessageFilter mf = MessageFilter.create().setSource(source).setField(DMT.UID, uid). setTimeout(RequestSender.OPENNET_TIMEOUT). setType(isReply ? DMT.FNPOpennetConnectReplyNew : DMT.FNPOpennetConnectDestinationNew); // Also waiting for an ack MessageFilter mfAck = MessageFilter.create().setSource(source).setField(DMT.UID, uid). setTimeout(RequestSender.OPENNET_TIMEOUT).setType(DMT.FNPOpennetCompletedAck); // Also waiting for an upstream timed out. MessageFilter mfAckTimeout = MessageFilter.create().setSource(source).setField(DMT.UID, uid). setTimeout(RequestSender.OPENNET_TIMEOUT).setType(DMT.FNPOpennetCompletedTimeout); mf = mfAck.or(mfAckTimeout.or(mf)); try { node.usm.addAsyncFilter(mf, new SlowAsyncMessageFilterCallback() { boolean completed; @Override public void onMatched(Message msg) { if (msg.getSpec() == DMT.FNPOpennetCompletedAck || msg.getSpec() == DMT.FNPOpennetCompletedTimeout) { synchronized(this) { if(completed) return; completed = true; } callback.acked(msg.getSpec() == DMT.FNPOpennetCompletedTimeout); } else { // Noderef bulk transfer long xferUID = msg.getLong(DMT.TRANSFER_UID); int paddedLength = msg.getInt(DMT.PADDED_LENGTH); int realLength = msg.getInt(DMT.NODEREF_LENGTH); complete(innerWaitForOpennetNoderef(xferUID, paddedLength, realLength, source, isReply, uid, false, ctr, node)); } } @Override public boolean shouldTimeout() { return false; } @Override public void onTimeout() { synchronized(this) { if(completed) return; completed = true; } callback.timedOut(); } @Override public void onDisconnect(PeerContext ctx) { complete(null); } @Override public void onRestarted(PeerContext ctx) { complete(null); } @Override public int getPriority() { return NativeThread.NORM_PRIORITY; } private void complete(byte[] buf) { synchronized(this) { if(completed) return; completed = true; } callback.gotNoderef(buf); } }, ctr); } catch (DisconnectedException e) { callback.gotNoderef(null); } } static byte[] innerWaitForOpennetNoderef(long xferUID, int paddedLength, int realLength, PeerNode source, boolean isReply, long uid, boolean sendReject, ByteCounter ctr, Node node) { byte[] buf = new byte[paddedLength]; ByteArrayRandomAccessBuffer raf = new ByteArrayRandomAccessBuffer(buf); PartiallyReceivedBulk prb = new PartiallyReceivedBulk(node.usm, buf.length, Node.PACKET_SIZE, raf, false); BulkReceiver br = new BulkReceiver(prb, source, xferUID, ctr); if (logMINOR) { Logger.minor(OpennetManager.class, "Receiving noderef (reply="+isReply+") as bulk transfer for request uid "+uid+" with transfer "+xferUID+" from "+source); } if (!br.receive()) { if (source.isConnected()) { String msg = "Failed to receive noderef bulk transfer : " +RetrievalException.getErrString(prb.getAbortReason())+" : " +prb.getAbortDescription()+" from "+source; if (prb.getAbortReason() != RetrievalException.SENDER_DISCONNECTED) { Logger.warning(OpennetManager.class, msg); } else { Logger.normal(OpennetManager.class, msg); } if (sendReject) rejectRef(uid, source, DMT.NODEREF_REJECTED_TRANSFER_FAILED, ctr); } return null; } byte[] noderef = Arrays.copyOf(buf, realLength); return noderef; } public static void rejectRef(long uid, PeerNode source, int reason, ByteCounter ctr) { Message msg = DMT.createFNPOpennetNoderefRejected(uid, reason); try { source.sendAsync(msg, null, ctr); } catch (NotConnectedException e) { // Ignore } } public static SimpleFieldSet validateNoderef(byte[] noderef, int offset, int length, PeerNode from, boolean forceOpennetEnabled) { SimpleFieldSet ref; try { ref = PeerNode.compressedNoderefToFieldSet(noderef, 0, noderef.length); } catch (FSParseException e) { Logger.error(OpennetManager.class, "Invalid noderef: "+e, e); return null; } if(forceOpennetEnabled) ref.put("opennet", true); if(!OpennetPeerNode.validateRef(ref)) { Logger.error(OpennetManager.class, "Could not parse opennet noderef from "+from); return null; } if (ref != null) { String identity = ref.get("identity"); if (identity != null) // N2N_MESSAGE_TYPE_DIFFNODEREF don't have identity registerKnownIdentity(identity); } return ref; } /** Do an announcement !! * @param target The location to announce to. In 0.7 we don't try to prevent nodes from choosing their * announcement location, because it is easy for them to get the location they want later on anyway, * and we can do a much more effective announcement this way. */ public void announce(double target, AnnouncementCallback cb) { AnnounceSender sender = new AnnounceSender(target, this, node, cb, null); node.executor.execute(sender, "Announcement to "+target); } public long getCreationTime() { return creationTime; } private static final long MAX_AGE = DAYS.toMillis(7); private static final TimeSortedHashtable<String> knownIds = new TimeSortedHashtable<String>(); private static void registerKnownIdentity(String d) { if (logMINOR) Logger.minor(OpennetManager.class, "Known Id: " + d); long now = System.currentTimeMillis(); synchronized (knownIds) { if(logMINOR) Logger.minor(OpennetManager.class, "Adding Id " + d + " knownIds size " + knownIds.size()); knownIds.push(d, now); if(logMINOR) Logger.minor(OpennetManager.class, "Added Id " + d + " knownIds size " + knownIds.size()); knownIds.removeBefore(now - MAX_AGE); if(logMINOR) Logger.minor(OpennetManager.class, "Added and pruned location " + d + " knownIds size " + knownIds.size()); } if(logMINOR) Logger.minor(OpennetManager.class, "Estimated opennet size(session): " + knownIds.size()); } //Return the estimated network size based on locations seen after timestamp or for the whole session if -1 public int getNetworkSizeEstimate(long timestamp) { return knownIds.countValuesAfter(timestamp); } public int getAnnouncementThreshold() { return announcer.getAnnouncementThreshold(); } /** Notification that a peer was disconnected. Query the Announcer, * it may need to rerun. */ public void onDisconnect(PeerNode node2) { if(announcer != null) announcer.maybeSendAnnouncementOffThread(); } public void drawOpennetStatsBox(HTMLNode box) { HTMLNode table = box.addChild("table", "border", "0"); HTMLNode row = table.addChild("tr"); row.addChild("th"); for(ConnectionType type : ConnectionType.values()) { row.addChild("th", type.name()); } row = table.addChild("tr"); row.addChild("td", "Connection attempts"); for(ConnectionType type : ConnectionType.values()) { row.addChild("td", Long.toString(connectionAttempts.get(type))); } row = table.addChild("tr"); row.addChild("td", "Connections accepted"); for(ConnectionType type : ConnectionType.values()) { row.addChild("td", Long.toString(connectionAttemptsAdded.get(type))); } row = table.addChild("tr"); row.addChild("td", "Accepted (free slots)"); for(ConnectionType type : ConnectionType.values()) { row.addChild("td", Long.toString(connectionAttemptsAddedPlentySpace.get(type))); } row = table.addChild("tr"); row.addChild("td", "Rejected (per-type grace periods)"); for(ConnectionType type : ConnectionType.values()) { row.addChild("td", Long.toString(connectionAttemptsRejectedByPerTypeEnforcement.get(type))); } row = table.addChild("tr"); row.addChild("td", "Rejected (no droppable peers)"); for(ConnectionType type : ConnectionType.values()) { row.addChild("td", Long.toString(connectionAttemptsRejectedNoPeersDroppable.get(type))); } } public boolean waitingForUpdater() { return announcer.isWaitingForUpdater(); } public void reannounce() { announcer.reannounce(); } public void drawSeedStatsBox(HTMLNode content) { seedTracker.drawSeedStats(content); } /** Called when a connection completes. If it's an opennet peer, we may need to drop a peer * to make space. If it's a darknet peer, the connection limit for opennet peers may have * decreased so again we may need to drop a peer. */ public void onConnectedPeer(PeerNode pn) { if(pn instanceof OpennetPeerNode) { dropExcessPeers(((OpennetPeerNode)pn).linkLengthClass()); } else { // The peer count target may have decreased, so we may need to drop an opennet peer. dropAllExcessPeers(); } } }