package lbms.plugins.mldht.kad; import java.io.*; import java.net.InetSocketAddress; import java.util.*; import lbms.plugins.mldht.kad.DHT.LogLevel; import lbms.plugins.mldht.kad.messages.MessageBase; import lbms.plugins.mldht.kad.messages.PingRequest; import lbms.plugins.mldht.kad.messages.MessageBase.Type; /** * A KBucket is just a list of KBucketEntry objects. * * The list is sorted by time last seen : * The first element is the least recently seen, the last * the most recently seen. * * @author Damokles */ public class KBucket implements Externalizable { private static final long serialVersionUID = -5507455162198975209L; // any modifying actions to entries must happen through removeAndInsert (which is synchronized)! private LinkedList<KBucketEntry> entries; // this is synchronized on entries, not on replacementBucket! private LinkedList<KBucketEntry> replacementBucket; private transient RPCServerBase srv; private transient Node node; private transient Set<KBucketEntry> pendingPings; private long last_modified; private transient Task refresh_task; public KBucket () { entries = new LinkedList<KBucketEntry>(); replacementBucket = new LinkedList<KBucketEntry>(); pendingPings = Collections.synchronizedSet(new HashSet<KBucketEntry>()); } public KBucket (RPCServerBase srv, Node node) { this(); last_modified = System.currentTimeMillis(); this.node = node; this.srv = srv; } /** * Inserts an entry into the bucket. * @param entry The entry to insert */ public void insert (KBucketEntry entry) { removeAndInsert(null, entry); } private void removeAndInsert(KBucketEntry toRemove, KBucketEntry toInsert) { KBucketEntry toPing = null; insertTests: synchronized (entries) { if (toRemove != null) { entries.remove(toRemove); } if (toInsert == null) { return; } int idx = entries.indexOf(toInsert); if (idx != -1) { // If in the list, move it to the end final KBucketEntry oldEntry = entries.get(idx); final KBucketEntry newEntry = toInsert; if (!oldEntry.getAddress().equals(toInsert.getAddress())) { // node changed address, check if old node is really dead to prevent impersonation pingEntry(oldEntry, new RPCCallListener() { public void onTimeout(RPCCallBase c) { removeAndInsert(oldEntry, newEntry); DHT.log("Node "+oldEntry.getID()+" changed address from "+oldEntry.getAddress()+" to "+newEntry.getAddress(), LogLevel.Info); } public void onResponse(RPCCallBase c, MessageBase rsp) { DHT.log("New node "+newEntry.getAddress()+" claims same Node ID ("+oldEntry.getID()+") as "+oldEntry.getAddress()+" ; node dropped as this might be an impersonation attack", LogLevel.Error); } }); break insertTests; } // only refresh the last seen time if it already exists oldEntry.signalLastSeen(); adjustTimerOnInsert(oldEntry); break insertTests; } if (entries.size() < DHTConstants.MAX_ENTRIES_PER_BUCKET) { // insert if not already in the list and we still have room sortedInsert(toInsert); break insertTests; } if (replaceBadEntry(toInsert)) break insertTests; // older entries displace younger ones (this code path is mostly used on changing node IDs) KBucketEntry youngest = entries.getLast(); if (youngest.getCreationTime() > toInsert.getCreationTime()) { entries.remove(youngest); sortedInsert(toInsert); toPing = youngest; break insertTests; } toPing = toInsert; } // ping outside the synchronized block to avoid deadlocks due to dual lock usage with entries + pending entries if(toPing != null) { pingQuestionable(toPing); } } private void sortedInsert(KBucketEntry toInsert) { if(toInsert == null) return; synchronized (entries) { KBucketEntry youngest = entries.peekLast(); entries.addLast(toInsert); adjustTimerOnInsert(toInsert); if (youngest != null && toInsert.getCreationTime() < youngest.getCreationTime()) Collections.sort(entries, KBucketEntry.AGE_ORDER); } } /** * Get the number of entries. * * @return The number of entries in this Bucket */ public int getNumEntries () { return entries.size(); } /** * @return the entries */ public List<KBucketEntry> getEntries () { return new ArrayList<KBucketEntry>(entries); } /** * Checks if this bucket contains an entry. * * @param entry Entry to check for * @return true if found */ public boolean contains (KBucketEntry entry) { return entries.contains(entry); } /** * Find the K closest entries to a key and store them in the KClosestNodesSearch * object. * @param kns The object to storre the search results * @return true if at least one Node was inserted successully */ public boolean findKClosestNodes (KClosestNodesSearch kns) { //return true if bucket was empty //otherwise Node.findKClosestNodes will break boolean successfulInsert = entries.size() == 0; for (int i = 0; i < entries.size(); i++) { if (!entries.get(i).isBad() && kns.tryInsert(entries.get(i))) { successfulInsert = true; } } return successfulInsert; } /** * A peer failed to respond * @param addr Address of the peer */ public boolean onTimeout (InetSocketAddress addr) { synchronized (entries) { for (int i = 0; i < entries.size(); i++) { KBucketEntry e = entries.get(i); if (e.getAddress() == addr) { e.signalRequestTimeout(); //only removes the entry if it is bad removeEntry(e, false); return true; } } return false; } } /** * Check if the bucket needs to be refreshed * * @return true if it needs to be refreshed */ public boolean needsToBeRefreshed () { long now = System.currentTimeMillis(); return (now - last_modified > DHTConstants.BUCKET_REFRESH_INTERVAL && (refresh_task == null || refresh_task.isFinished()) && entries.size() > 0); } /** * Resets the last modified for this Bucket */ public void updateRefreshTimer () { last_modified = System.currentTimeMillis(); } private void adjustTimerOnInsert(KBucketEntry entry) { if(entry.getLastSeen() > last_modified) { last_modified = entry.getLastSeen(); } } /** * Pings an entry and notifies the listener of the result. * * @param entry entry to ping * @return true if the ping was sent, false if there already is an outstanding ping for that entry */ private boolean pingEntry(final KBucketEntry entry, RPCCallListener listener) { // don't ping if there already is an outstanding ping if(pendingPings.contains(entry)) { return false; } PingRequest p = new PingRequest(node.getOurID()); p.setDestination(entry.getAddress()); RPCCall c = srv.doCall(p); if (c != null) { pendingPings.add(entry); c.addListener(listener); c.addListener(new RPCCallListener() { public void onTimeout(RPCCallBase c) { pendingPings.remove(entry); } @Override public void onResponse(RPCCallBase c, MessageBase rsp) { pendingPings.remove(entry); } }); return true; } return false; } private void pingQuestionable (final KBucketEntry replacement_entry) { if (pendingPings.size() >= 2) { insertInReplacementBucket(replacement_entry); return; } // we haven't found any bad ones so try the questionable ones synchronized (entries) { for (final KBucketEntry toTest : entries) { if (toTest.isQuestionable() && pingEntry(toTest, new RPCCallListener() { public void onTimeout(RPCCallBase c) { removeAndInsert(toTest, replacement_entry); // we could replace this one, try another one. KBucketEntry nextReplacementEntry; synchronized (entries) { nextReplacementEntry = replacementBucket.pollLast(); } if (replacement_entry != null && !replaceBadEntry(nextReplacementEntry)) pingQuestionable(nextReplacementEntry); } public void onResponse(RPCCallBase c, MessageBase rsp) { // it's alive, check another one if (!replaceBadEntry(replacement_entry)) { pingQuestionable(replacement_entry); } } })) { return; } } } //save the entry if all are good insertInReplacementBucket(replacement_entry); } /** * Tries to instert entry by replacing a bad entry. * * @param entry Entry to insert * @return true if replace was successful */ private boolean replaceBadEntry (KBucketEntry entry) { for (KBucketEntry e : entries) { if (e.isBad()) { // bad one get rid of it removeAndInsert(e, entry); return true; } } return false; } private void insertInReplacementBucket(KBucketEntry entry) { synchronized (entries) { //if it is already inserted remove it and add it to the end int idx = replacementBucket.indexOf(entry); if (idx != -1) { entry = replacementBucket.get(idx); replacementBucket.remove(idx); } replacementBucket.addLast(entry); if (replacementBucket.size() > DHTConstants.MAX_ENTRIES_PER_BUCKET) { //remove the least recently seen one replacementBucket.removeFirst(); } } } /** * only removes one bad entry at a time to prevent sudden flushing of the routing table */ public void checkBadEntries() { synchronized (entries) { KBucketEntry toRemove = null; KBucketEntry replacement; for (KBucketEntry e : entries) { if (!e.isBad()) continue; toRemove = e; break; } if(toRemove == null) return; replacement = replacementBucket.pollLast(); if(replacement == null) return; entries.remove(toRemove); sortedInsert(replacement); } } public boolean checkForIDChangeAndNotifyOfResponse(MessageBase msg) { synchronized (entries) { // check if node changed its ID for (KBucketEntry entry : entries) { // node ID change detected, reassign node to the appropriate bucket if (entry.getAddress().equals(msg.getOrigin()) && !entry.getID().equals(msg.getID())) { removeEntry(entry, true); //remove and replace from replacement bucket DHT.log("Node " + entry.getAddress() + " changed ID from " + entry.getID() + " to " + msg.getID(), LogLevel.Info); entry = new KBucketEntry(entry.getAddress(), msg.getID(), entry.getCreationTime()); entry.signalLastSeen(); // insert into appropriate bucket for the new ID node.insertEntry(entry); return true; } // no node ID change detected, update last responded. insert will be invoked soon, thus we don't have to do the move-to-end stuff if(msg.getType() == Type.RSP_MSG && entry.getID().equals(msg.getID())) entry.signalResponse(); } } return false; } /** * @param toRemove Entry to remove, if its bad * @param force if true entry will be removed regardless of its state */ public void removeEntry(KBucketEntry toRemove, boolean force) { synchronized (entries) { if (entries.contains(toRemove) && (force || toRemove.isBad())) { KBucketEntry replacement = null; replacement = replacementBucket.pollLast(); // only remove if we have a replacement or really need to if(replacement != null || force) { entries.remove(toRemove); sortedInsert(replacement); } } } } /** * @param srv * the srv to set */ public void setServer (RPCServerBase srv) { this.srv = srv; } /** * @param node the node to set */ public void setNode (Node node) { this.node = node; } /** * @param refresh_task the refresh_task to set */ public void setRefreshTask (Task refresh_task) { this.refresh_task = refresh_task; } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { Map<String,Object> serialized = (Map<String, Object>) in.readObject(); Object obj = serialized.get("mainBucket"); if(obj instanceof Collection<?>) entries.addAll((Collection<KBucketEntry>)obj); obj = serialized.get("replacementBucket"); if(obj instanceof Collection<?>) replacementBucket.addAll((Collection<KBucketEntry>)obj); obj = serialized.get("lastModifiedTime"); if(obj instanceof Long) last_modified = (Long)obj; entries.removeAll(Collections.singleton(null)); replacementBucket.removeAll(Collections.singleton(null)); Collections.sort(entries, KBucketEntry.AGE_ORDER); Collections.sort(replacementBucket,KBucketEntry.LAST_SEEN_ORDER); } public void writeExternal(ObjectOutput out) throws IOException { Map<String,Object> serialized = new HashMap<String, Object>(); // put entries as any type of collection, will convert them on deserialisation serialized.put("mainBucket", entries); serialized.put("replacementBucket", replacementBucket); serialized.put("lastModifiedTime", last_modified); synchronized (entries) { out.writeObject(serialized); } } }