/*
* This file is part of mlDHT.
*
* mlDHT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* mlDHT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with mlDHT. If not, see <http://www.gnu.org/licenses/>.
*/
package lbms.plugins.mldht.kad;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import lbms.plugins.mldht.kad.messages.MessageBase;
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 {
/**
* use {@link #insertOrRefresh}, {@link #sortedInsert} or {@link #removeEntry} to handle this<br>
* using copy-on-write semantics for this list, referencing it is safe if you make local copy
*/
private volatile List<KBucketEntry> entries;
private AtomicInteger currentReplacementPointer;
private AtomicReferenceArray<KBucketEntry> replacementBucket;
private long lastRefresh;
public KBucket () {
entries = new ArrayList<>(); // using arraylist here since reading/iterating is far more common than writing.
currentReplacementPointer = new AtomicInteger(0);
replacementBucket = new AtomicReferenceArray<>(DHTConstants.MAX_ENTRIES_PER_BUCKET);
// needed for bitmasking
assert(Integer.bitCount(replacementBucket.length()) == 1);
}
/**
* Notify bucket of new incoming packet from a node, perform update or insert existing nodes where appropriate
* @param newEntry The entry to insert
*/
public void insertOrRefresh (final KBucketEntry newEntry) {
if (newEntry == null)
return;
List<KBucketEntry> entriesRef = entries;
for(KBucketEntry existing : entriesRef) {
if(existing.equals(newEntry)) {
existing.mergeInTimestamps(newEntry);
return;
}
if(existing.matchIPorID(newEntry)) {
DHT.logInfo("new node "+newEntry+" claims same ID or IP as "+existing+", might be impersonation attack or IP change. ignoring until old entry times out");
return;
}
}
if(newEntry.verifiedReachable()) {
if (entriesRef.size() < DHTConstants.MAX_ENTRIES_PER_BUCKET)
{
// insert if not already in the list and we still have room
modifyMainBucket(null,newEntry);
return;
}
if (replaceBadEntry(newEntry))
return;
KBucketEntry youngest = entriesRef.get(entriesRef.size()-1);
// older entries displace younger ones (although that kind of stuff should probably go through #modifyMainBucket directly)
// entries with a 2.5times lower RTT than the current youngest one displace the youngest. safety factor to prevent fibrilliation due to changing RTT-estimates / to only replace when it's really worth it
if (youngest.getCreationTime() > newEntry.getCreationTime() || newEntry.getRTT() * 2.5 < youngest.getRTT())
{
modifyMainBucket(youngest,newEntry);
// it was a useful entry, see if we can use it to replace something questionable
insertInReplacementBucket(youngest);
return;
}
}
insertInReplacementBucket(newEntry);
}
public void refresh(KBucketEntry toRefresh) {
entries.stream().filter(toRefresh::equals).findAny().ifPresent(e -> {
e.mergeInTimestamps(toRefresh);
});
replacementsStream().filter(toRefresh::equals).findAny().ifPresent(e -> {
e.mergeInTimestamps(toRefresh);
});
}
/**
* mostly meant for internal use or transfering entries into a new bucket.
* to update a bucket properly use {@link #insertOrRefresh(KBucketEntry)}
*/
public void modifyMainBucket(KBucketEntry toRemove, KBucketEntry toInsert) {
// we're synchronizing all modifications, therefore we can freely reference the old entry list, it will not be modified concurrently
synchronized (this)
{
if(toInsert != null && entries.stream().anyMatch(toInsert::matchIPorID))
return;
List<KBucketEntry> newEntries = new ArrayList<>(entries);
boolean removed = false;
boolean added = false;
// removal never violates ordering constraint, no checks required
if(toRemove != null)
removed = newEntries.remove(toRemove);
if(toInsert != null)
{
int oldSize = newEntries.size();
boolean wasFull = oldSize >= DHTConstants.MAX_ENTRIES_PER_BUCKET;
KBucketEntry youngest = oldSize > 0 ? newEntries.get(oldSize-1) : null;
boolean unorderedInsert = youngest != null && toInsert.getCreationTime() < youngest.getCreationTime();
added = !wasFull || unorderedInsert;
if(added)
{
newEntries.add(toInsert);
removeFromReplacement(toInsert).ifPresent(toInsert::mergeInTimestamps);
} else
{
insertInReplacementBucket(toInsert);
}
if(unorderedInsert)
Collections.sort(newEntries,KBucketEntry.AGE_ORDER);
if(wasFull && added)
while(newEntries.size() > DHTConstants.MAX_ENTRIES_PER_BUCKET)
insertInReplacementBucket(newEntries.remove(newEntries.size()-1));
}
// make changes visible
if(added || removed)
entries = newEntries;
}
}
/**
* Get the number of entries.
*
* @return The number of entries in this Bucket
*/
public int getNumEntries () {
return entries.size();
}
public boolean isFull() {
return entries.size() >= DHTConstants.MAX_ENTRIES_PER_BUCKET;
}
public int getNumReplacements() {
int c = 0;
for(int i=0;i<replacementBucket.length();i++)
if(replacementBucket.get(i) != null)
c++;
return c;
}
/**
* @return the entries
*/
public List<KBucketEntry> getEntries () {
return new ArrayList<>(entries);
}
public Stream<KBucketEntry> entriesStream() {
return entries.stream();
}
Stream<KBucketEntry> replacementsStream() {
return IntStream.range(0, replacementBucket.length()).mapToObj(replacementBucket::get).filter(Objects::nonNull);
}
public List<KBucketEntry> getReplacementEntries() {
List<KBucketEntry> repEntries = new ArrayList<>(replacementBucket.length());
int current = currentReplacementPointer.get();
for(int i=1;i<=replacementBucket.length();i++)
{
KBucketEntry e = replacementBucket.get((current + i) % replacementBucket.length());
if(e != null)
repEntries.add(e);
}
return repEntries;
}
/**
* A peer failed to respond
* @param addr Address of the peer
*/
public void onTimeout(InetSocketAddress addr) {
List<KBucketEntry> entriesRef = entries;
for (int i = 0, n=entriesRef.size(); i < n; i++)
{
KBucketEntry e = entriesRef.get(i);
if (e.getAddress().equals(addr))
{
e.signalRequestTimeout();
//only removes the entry if it is bad
removeEntryIfBad(e, false);
return;
}
}
for(int i=0, n=replacementBucket.length();i<n;i++) {
KBucketEntry e = replacementBucket.get(i);
if(e != null && e.getAddress().equals(addr)) {
e.signalRequestTimeout();
return;
}
}
return;
}
/**
* Check if the bucket needs to be refreshed
*
* @return true if it needs to be refreshed
*/
public boolean needsToBeRefreshed () {
long now = System.currentTimeMillis();
// TODO: timer may be somewhat redundant with needsPing logic
return now - lastRefresh > DHTConstants.BUCKET_REFRESH_INTERVAL && entries.stream().anyMatch(KBucketEntry::needsPing);
}
public static final long REPLACEMENT_PING_MIN_INTERVAL = 30*1000;
boolean needsReplacementPing() {
long now = System.currentTimeMillis();
return now - lastRefresh > REPLACEMENT_PING_MIN_INTERVAL && (entriesStream().anyMatch(KBucketEntry::needsReplacement) || entries.size() == 0) && replacementsStream().anyMatch(KBucketEntry::neverContacted);
}
/**
* Resets the last modified for this Bucket
*/
public void updateRefreshTimer () {
lastRefresh = System.currentTimeMillis();
}
@Override
public String toString() {
return "entries: "+entries+" replacements: "+replacementBucket;
}
/**
* 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) {
List<KBucketEntry> entriesRef = entries;
for (int i = 0,n=entriesRef.size();i<n;i++) {
KBucketEntry e = entriesRef.get(i);
if (e.needsReplacement()) {
// bad one get rid of it
modifyMainBucket(e, entry);
return true;
}
}
return false;
}
private KBucketEntry pollVerifiedReplacementEntry()
{
while(true) {
int bestIndex = -1;
KBucketEntry bestFound = null;
for(int i=0;i<replacementBucket.length();i++) {
KBucketEntry entry = replacementBucket.get(i);
if(entry == null || !entry.verifiedReachable())
continue;
boolean isBetter = bestFound == null || entry.getRTT() < bestFound.getRTT() || (entry.getRTT() == bestFound.getRTT() && entry.getLastSeen() > bestFound.getLastSeen());
if(isBetter) {
bestFound = entry;
bestIndex = i;
}
}
if(bestFound == null)
return null;
int newPointer = bestIndex-1;
if(newPointer < 0)
newPointer = replacementBucket.length()-1;
if(replacementBucket.compareAndSet(bestIndex, bestFound, null)) {
currentReplacementPointer.set(newPointer);
return bestFound;
}
}
}
private Optional<KBucketEntry> removeFromReplacement(KBucketEntry toRemove) {
for(int i=0;i<replacementBucket.length();i++) {
KBucketEntry e = replacementBucket.get(i);
if(e == null || !e.matchIPorID(toRemove))
continue;
replacementBucket.compareAndSet(i, e, null);
if(e.equals(toRemove))
return Optional.of(e);
}
return Optional.empty();
}
public Optional<KBucketEntry> findPingableReplacement() {
for(int i=0; i<replacementBucket.length();i++) {
KBucketEntry e = replacementBucket.get(i);
if(e == null || !e.neverContacted())
continue;
return Optional.of(e);
}
return Optional.empty();
}
void insertInReplacementBucket(KBucketEntry toInsert)
{
if(toInsert == null)
return;
outer:
while(true)
{
int insertationPoint = currentReplacementPointer.incrementAndGet() & (replacementBucket.length() - 1);
KBucketEntry toOverwrite = replacementBucket.get(insertationPoint);
boolean canOverwrite;
if(toOverwrite == null) {
canOverwrite = true;
} else {
int lingerTime = toOverwrite.verifiedReachable() && !toInsert.verifiedReachable() ? 5*60*1000 : 1000;
canOverwrite = toInsert.getLastSeen() - toOverwrite.getLastSeen() > lingerTime || toInsert.getRTT() < toOverwrite.getRTT();
}
if(!canOverwrite)
break;
for(int i=0;i<replacementBucket.length();i++)
{
// don't insert if already present
KBucketEntry potentialDuplicate = replacementBucket.get(i);
if(toInsert.matchIPorID(potentialDuplicate)) {
if(toInsert.equals(potentialDuplicate))
potentialDuplicate.mergeInTimestamps(toInsert);
break outer;
}
}
if(replacementBucket.compareAndSet(insertationPoint, toOverwrite, toInsert))
break;
}
}
/**
* main list not full or contains entry that needs a replacement -> promote verified entry (if any) from replacement
*/
public void promoteVerifiedReplacement() {
List<KBucketEntry> entriesRef = entries;
KBucketEntry toRemove = entriesRef.stream().filter(KBucketEntry::needsReplacement).findAny().orElse(null);
if (toRemove == null && entriesRef.size() >= DHTConstants.MAX_ENTRIES_PER_BUCKET)
return;
KBucketEntry replacement = pollVerifiedReplacementEntry();
if (replacement != null)
modifyMainBucket(toRemove, replacement);
}
public Optional<KBucketEntry> findByIPorID(InetAddress ip, Key id) {
return entries.stream().filter(e -> e.getID().equals(id) || e.getAddress().getAddress().equals(ip)).findAny();
}
public Optional<KBucketEntry> randomEntry() {
return Optional.of(entries).filter(l -> !l.isEmpty()).map(l -> l.get(ThreadLocalRandom.current().nextInt(l.size())));
}
public void notifyOfResponse(MessageBase msg)
{
if(msg.getType() != Type.RSP_MSG || msg.getAssociatedCall() == null)
return;
List<KBucketEntry> entriesRef = entries;
for (int i=0, n = entriesRef.size();i<n;i++)
{
KBucketEntry entry = entriesRef.get(i);
// update last responded. insert will be invoked soon, thus we don't have to do the move-to-end stuff
if(entry.getID().equals(msg.getID()))
{
entry.signalResponse(msg.getAssociatedCall().getRTT());
return;
}
}
}
/**
* @param toRemove Entry to remove, if its bad
* @param force if true entry will be removed regardless of its state
*/
public void removeEntryIfBad(KBucketEntry toRemove, boolean force) {
List<KBucketEntry> entriesRef = entries;
if (entriesRef.contains(toRemove) && (force || toRemove.needsReplacement()))
{
KBucketEntry replacement = null;
replacement = pollVerifiedReplacementEntry();
// only remove if we have a replacement or really need to
if(replacement != null || force)
modifyMainBucket(toRemove,replacement);
}
}
}