/* * 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 static the8472.bencode.Utils.prettyPrint; import static the8472.utils.Functional.awaitAll; import lbms.plugins.mldht.DHTConfiguration; import lbms.plugins.mldht.kad.GenericStorage.StorageItem; import lbms.plugins.mldht.kad.GenericStorage.UpdateResult; import lbms.plugins.mldht.kad.Node.RoutingTableEntry; import lbms.plugins.mldht.kad.messages.AbstractLookupRequest; import lbms.plugins.mldht.kad.messages.AbstractLookupResponse; import lbms.plugins.mldht.kad.messages.AnnounceRequest; import lbms.plugins.mldht.kad.messages.AnnounceResponse; import lbms.plugins.mldht.kad.messages.ErrorMessage; import lbms.plugins.mldht.kad.messages.ErrorMessage.ErrorCode; import lbms.plugins.mldht.kad.messages.FindNodeRequest; import lbms.plugins.mldht.kad.messages.FindNodeResponse; import lbms.plugins.mldht.kad.messages.GetPeersRequest; import lbms.plugins.mldht.kad.messages.GetPeersResponse; import lbms.plugins.mldht.kad.messages.GetRequest; import lbms.plugins.mldht.kad.messages.GetResponse; import lbms.plugins.mldht.kad.messages.MessageBase; import lbms.plugins.mldht.kad.messages.PingRequest; import lbms.plugins.mldht.kad.messages.PingResponse; import lbms.plugins.mldht.kad.messages.PutRequest; import lbms.plugins.mldht.kad.messages.PutResponse; import lbms.plugins.mldht.kad.messages.UnknownTypeResponse; import lbms.plugins.mldht.kad.tasks.AnnounceTask; import lbms.plugins.mldht.kad.tasks.NodeLookup; import lbms.plugins.mldht.kad.tasks.PeerLookupTask; import lbms.plugins.mldht.kad.tasks.PingRefreshTask; import lbms.plugins.mldht.kad.tasks.Task; import lbms.plugins.mldht.kad.tasks.TaskListener; import lbms.plugins.mldht.kad.tasks.TaskManager; import lbms.plugins.mldht.kad.utils.AddressUtils; import lbms.plugins.mldht.kad.utils.ByteWrapper; import lbms.plugins.mldht.kad.utils.PopulationEstimator; import lbms.plugins.mldht.utils.NIOConnectionManager; import java.io.IOException; import java.io.PrintWriter; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ProtocolFamily; import java.net.SocketException; import java.net.StandardProtocolFamily; import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.stream.Collectors; /** * @author Damokles * */ public class DHT implements DHTBase { public static enum DHTtype { IPV4_DHT("IPv4",20+4+2, 4+2, Inet4Address.class,20+8, 1450, StandardProtocolFamily.INET), IPV6_DHT("IPv6",20+16+2, 16+2, Inet6Address.class,40+8, 1200, StandardProtocolFamily.INET6); public final int HEADER_LENGTH; public final int NODES_ENTRY_LENGTH; public final int ADDRESS_ENTRY_LENGTH; public final Class<? extends InetAddress> PREFERRED_ADDRESS_TYPE; public final int MAX_PACKET_SIZE; public final String shortName; public final ProtocolFamily PROTO_FAMILY; private DHTtype(String shortName, int nodeslength, int addresslength, Class<? extends InetAddress> addresstype, int header, int maxSize, ProtocolFamily family) { this.shortName = shortName; this.NODES_ENTRY_LENGTH = nodeslength; this.PREFERRED_ADDRESS_TYPE = addresstype; this.ADDRESS_ENTRY_LENGTH = addresslength; this.HEADER_LENGTH = header; this.MAX_PACKET_SIZE = maxSize; this.PROTO_FAMILY = family; } public boolean canUseSocketAddress(InetSocketAddress addr) { return PREFERRED_ADDRESS_TYPE.isInstance(addr.getAddress()); } public boolean canUseAddress(InetAddress addr) { return PREFERRED_ADDRESS_TYPE.isInstance(addr); } } private static DHTLogger logger; private static LogLevel logLevel = LogLevel.Info; private volatile static ScheduledThreadPoolExecutor defaultScheduler; private static ThreadGroup executorGroup; static { logger = new DHTLogger() { public void log (String message, LogLevel l) { System.out.println(message); }; /* * (non-Javadoc) * * @see lbms.plugins.mldht.kad.DHTLogger#log(java.lang.Exception) */ public void log (Throwable e, LogLevel l) { e.printStackTrace(); } }; } private boolean running; enum BootstrapState { NONE, BOOTSTRAP, FILL } AtomicReference<BootstrapState> bootstrapping = new AtomicReference<>(BootstrapState.NONE); private long lastBootstrap; DHTConfiguration config; private Node node; private RPCServerManager serverManager; private GenericStorage storage; private Database db; private TaskManager tman; IDMismatchDetector mismatchDetector; NonReachableCache unreachableCache; private Path table_file; private boolean useRouterBootstrapping; private List<DHTStatsListener> statsListeners; private List<DHTStatusListener> statusListeners; private List<DHTIndexingListener> indexingListeners; private DHTStats stats; private DHTStatus status; private PopulationEstimator estimator; private AnnounceNodeCache cache; private NIOConnectionManager connectionManager; RPCStats serverStats; private final DHTtype type; private List<ScheduledFuture<?>> scheduledActions = new ArrayList<>(); private List<DHT> siblingGroup = new ArrayList<>(); private ScheduledExecutorService scheduler; public DHT(DHTtype type) { this.type = type; siblingGroup.add(this); stats = new DHTStats(); status = DHTStatus.Stopped; statsListeners = new ArrayList<>(2); statusListeners = new ArrayList<>(2); indexingListeners = new ArrayList<>(); estimator = new PopulationEstimator(); } public ScheduledExecutorService getScheduler() { return scheduler; } public void setScheduler(ScheduledExecutorService scheduler) { this.scheduler = scheduler; } public void addSiblings(List<DHT> toAdd) { toAdd.forEach(s -> { if(!siblingGroup.contains(s)) siblingGroup.add(s); }); } public Optional<DHT> getSiblingByType(DHTtype type) { return siblingGroup.stream().filter(sib -> sib.getType() == type).findAny(); } public List<DHT> getSiblings() { return Collections.unmodifiableList(siblingGroup); } public static interface IncomingMessageListener { void received(DHT dht, MessageBase msg); } private List<IncomingMessageListener> incomingMessageListeners = new ArrayList<>(); /** * Listeners must be threadsafe, non-blocking and exception-free and avoid modifying the passed messages or their contents. * They are invoked from the message-processing threads, so any incorrect behavior may prevent the messages from being processed properly. * * registration itself is not thread-safe, thus listeners should be registered between creation of the DHT instance and invocation of its start() method. */ public void addIncomingMessageListener(IncomingMessageListener l) { incomingMessageListeners.add(l); } void incomingMessage(MessageBase msg) { incomingMessageListeners.forEach(e -> e.received(this, msg)); } public void ping (PingRequest r) { if (!isRunning()) { return; } // ignore requests we get from ourself if (node.isLocalId(r.getID())) { return; } PingResponse rsp = new PingResponse(r.getMTID()); rsp.setDestination(r.getOrigin()); r.getServer().sendMessage(rsp); node.recieved(r); } public void findNode(AbstractLookupRequest r) { if (!isRunning()) { return; } // ignore requests we get from ourself if (node.isLocalId(r.getID())) { return; } AbstractLookupResponse response; if(r instanceof FindNodeRequest) response = new FindNodeResponse(r.getMTID()); else response = new UnknownTypeResponse(r.getMTID()); populateResponse(r.getTarget(), response, r.doesWant4() ? DHTConstants.MAX_ENTRIES_PER_BUCKET : 0, r.doesWant6() ? DHTConstants.MAX_ENTRIES_PER_BUCKET : 0); response.setDestination(r.getOrigin()); r.getServer().sendMessage(response); node.recieved(r); } void populateResponse(Key target, AbstractLookupResponse rsp, int v4, int v6) { if(v4 > 0) { getSiblingByType(DHTtype.IPV4_DHT).filter(DHT::isRunning).ifPresent(sib -> { KClosestNodesSearch kns = new KClosestNodesSearch(target, v4, sib); kns.fill(DHTtype.IPV4_DHT != type); rsp.setNodes(kns.asNodeList()); }); } if(v6 > 0) { getSiblingByType(DHTtype.IPV6_DHT).filter(DHT::isRunning).ifPresent(sib -> { KClosestNodesSearch kns = new KClosestNodesSearch(target, v6, sib); kns.fill(DHTtype.IPV6_DHT != type); rsp.setNodes(kns.asNodeList()); }); } } public void response (MessageBase r) { if (!isRunning()) { return; } node.recieved(r); } public void get(GetRequest req) { if (!isRunning()) { return; } GetResponse rsp = new GetResponse(req.getMTID()); populateResponse(req.getTarget(), rsp, req.doesWant4() ? DHTConstants.MAX_ENTRIES_PER_BUCKET : 0, req.doesWant6() ? DHTConstants.MAX_ENTRIES_PER_BUCKET : 0); Key k = req.getTarget(); Optional.ofNullable(db.genToken(req.getID(), req.getOrigin().getAddress(), req.getOrigin().getPort(), k)).ifPresent(token -> { rsp.setToken(token.arr); }); storage.get(k).ifPresent(item -> { if(req.getSeq() < 0 || item.sequenceNumber < 0 || req.getSeq() < item.sequenceNumber) { rsp.setRawValue(ByteBuffer.wrap(item.value)); rsp.setKey(item.pubkey); rsp.setSignature(item.signature); if(item.sequenceNumber >= 0) rsp.setSequenceNumber(item.sequenceNumber); } }); rsp.setDestination(req.getOrigin()); req.getServer().sendMessage(rsp); node.recieved(req); } public void put(PutRequest req) { Key k = req.deriveTargetKey(); if(!db.checkToken(new ByteWrapper(req.getToken()), req.getID(), req.getOrigin().getAddress(), req.getOrigin().getPort(), k)) { sendError(req, ErrorCode.ProtocolError.code, "received invalid or expired token for PUT request"); return; } UpdateResult result = storage.putOrUpdate(k, new StorageItem(req), req.getExpectedSequenceNumber()); switch(result) { case CAS_FAIL: sendError(req, ErrorCode.CasFail.code, "CAS failure"); return; case SIG_FAIL: sendError(req, ErrorCode.InvalidSignature.code, "signature validation failed"); return; case SEQ_FAIL: sendError(req, ErrorCode.CasNotMonotonic.code, "sequence number less than current"); return; case IMMUTABLE_SUBSTITUTION_FAIL: sendError(req, ErrorCode.ProtocolError.code, "PUT request replacing mutable data with immutable is not supported"); return; case SUCCESS: PutResponse rsp = new PutResponse(req.getMTID()); rsp.setDestination(req.getOrigin()); req.getServer().sendMessage(rsp); break; } node.recieved(req); } public void getPeers (GetPeersRequest r) { if (!isRunning()) { return; } // ignore requests we get from ourself if (node.isLocalId(r.getID())) { return; } BloomFilterBEP33 peerFilter = r.isScrape() ? db.createScrapeFilter(r.getInfoHash(), false) : null; BloomFilterBEP33 seedFilter = r.isScrape() ? db.createScrapeFilter(r.getInfoHash(), true) : null; boolean v6 = Inet6Address.class.isAssignableFrom(type.PREFERRED_ADDRESS_TYPE); boolean heavyWeight = peerFilter != null; int valuesTargetLength = v6 ? 35 : 50; // scrape filter gobble up a lot of space, restrict list sizes if(heavyWeight) valuesTargetLength = v6 ? 15 : 30; List<DBItem> dbl = db.sample(r.getInfoHash(), valuesTargetLength,type, r.isNoSeeds()); for(DHTIndexingListener listener : indexingListeners) { List<PeerAddressDBItem> toAdd = listener.incomingPeersRequest(r.getInfoHash(), r.getOrigin().getAddress(), r.getID()); if(dbl == null && !toAdd.isEmpty()) dbl = new ArrayList<>(); if(dbl != null && !toAdd.isEmpty()) dbl.addAll(toAdd); } // generate a token ByteWrapper token = null; if(db.insertForKeyAllowed(r.getInfoHash())) token = db.genToken(r.getID(), r.getOrigin().getAddress(), r.getOrigin().getPort(), r.getInfoHash()); int want4 = r.doesWant4() ? DHTConstants.MAX_ENTRIES_PER_BUCKET : 0; int want6 = r.doesWant6() ? DHTConstants.MAX_ENTRIES_PER_BUCKET : 0; if(v6 && peerFilter != null) want6 = Math.min(5, want6); // only fulfill both wants if we have neither filters nor values to send if(heavyWeight || dbl != null) { if(v6) want4 = 0; else want6 = 0; } GetPeersResponse resp = new GetPeersResponse(r.getMTID()); populateResponse(r.getTarget(), resp, want4, want6); resp.setToken(token != null ? token.arr : null); resp.setScrapePeers(peerFilter); resp.setScrapeSeeds(seedFilter); resp.setPeerItems(dbl); resp.setDestination(r.getOrigin()); r.getServer().sendMessage(resp); node.recieved(r); } public void announce (AnnounceRequest r) { if (!isRunning()) { return; } // ignore requests we get from ourself if (node.isLocalId(r.getID())) { return; } // first check if the token is OK ByteWrapper token = new ByteWrapper(r.getToken()); if (!db.checkToken(token, r.getID(), r.getOrigin().getAddress(), r.getOrigin().getPort(), r.getInfoHash())) { logDebug("DHT Received Announce Request with invalid token."); sendError(r, ErrorCode.ProtocolError.code, "Invalid Token; tokens expire after "+DHTConstants.TOKEN_TIMEOUT+"ms; only valid for the IP/port to which it was issued; only valid for the infohash for which it was issued"); return; } logDebug("DHT Received Announce Request, adding peer to db: " + r.getOrigin().getAddress()); // everything OK, so store the value PeerAddressDBItem item = PeerAddressDBItem.createFromAddress(r.getOrigin().getAddress(), r.getPort(), r.isSeed()); r.getVersion().ifPresent(item::setVersion); if(!AddressUtils.isBogon(item)) db.store(r.getInfoHash(), item); // send a proper response to indicate everything is OK AnnounceResponse rsp = new AnnounceResponse(r.getMTID()); rsp.setDestination(r.getOrigin()); r.getServer().sendMessage(rsp); node.recieved(r); } public void error (ErrorMessage r) { StringBuilder b = new StringBuilder(); b.append("Error [").append( r.getCode() ).append("] from: " ).append(r.getOrigin()); b.append(" Message: \"").append(r.getMessage()).append("\""); r.getVersion().ifPresent(v -> b.append(" version:").append(prettyPrint(v))); DHT.logError(b.toString()); } public void timeout (RPCCall r) { if (isRunning()) { node.onTimeout(r); } } /* * (non-Javadoc) * * @see lbms.plugins.mldht.kad.DHTBase#addDHTNode(java.lang.String, int) */ public void addDHTNode (String host, int hport) { if (!isRunning()) { return; } InetSocketAddress addr = new InetSocketAddress(host, hport); if (!addr.isUnresolved() && !AddressUtils.isBogon(addr)) { if(!type.PREFERRED_ADDRESS_TYPE.isInstance(addr.getAddress()) || node.getNumEntriesInRoutingTable() > DHTConstants.BOOTSTRAP_IF_LESS_THAN_X_PEERS) return; RPCServer srv = serverManager.getRandomActiveServer(true); if(srv != null) srv.ping(addr); } } /** * returns a non-enqueued task for further configuration. or zero if the request cannot be serviced. * use the task-manager to actually start the task. */ public PeerLookupTask createPeerLookup (byte[] info_hash) { if (!isRunning()) { return null; } Key id = new Key(info_hash); RPCServer srv = serverManager.getRandomActiveServer(false); if(srv == null) return null; PeerLookupTask lookupTask = new PeerLookupTask(srv, node, id); return lookupTask; } public AnnounceTask announce(PeerLookupTask lookup, boolean isSeed, int btPort) { if (!isRunning()) { return null; } // reuse the same server to make sure our tokens are still valid AnnounceTask announce = new AnnounceTask(lookup.getRPC(), node, lookup.getInfoHash(), btPort, lookup.getAnnounceCanidates()); announce.setSeed(isSeed); tman.addTask(announce); return announce; } public GenericStorage getStorage() { return storage; } public DHTConfiguration getConfig() { return config; } public AnnounceNodeCache getCache() { return cache; } public RPCServerManager getServerManager() { return serverManager; } public NIOConnectionManager getConnectionManager() { return connectionManager; } public PopulationEstimator getEstimator() { return estimator; } public DHTtype getType() { return type; } public NonReachableCache getUnreachableCache() { return unreachableCache; } /* * (non-Javadoc) * * @see lbms.plugins.mldht.kad.DHTBase#getStats() */ public DHTStats getStats () { return stats; } /** * @return the status */ public DHTStatus getStatus () { return status; } /* * (non-Javadoc) * * @see lbms.plugins.mldht.kad.DHTBase#isRunning() */ public boolean isRunning () { return running; } private int getPort() { int port = config.getListeningPort(); if(port < 1 || port > 65535) port = 49001; return port; } final RPCCallListener rpcListener = new RPCCallListener() { public void stateTransition(RPCCall c, RPCState previous, RPCState current) { if(current == RPCState.RESPONDED) mismatchDetector.add(c); if(current == RPCState.RESPONDED || current == RPCState.TIMEOUT) unreachableCache.onCallFinished(c); if(current == RPCState.RESPONDED || current == RPCState.TIMEOUT || current == RPCState.STALLED) tman.dequeue(c.getRequest().getServer()); } }; final Consumer<RPCServer> serverListener = (srv) -> { node.registerServer(srv); srv.onEnqueue((c) -> { c.addListener(rpcListener); }); }; void populate() { serverStats = new RPCStats(); cache = new AnnounceNodeCache(); stats.setRpcStats(serverStats); serverManager = new RPCServerManager(this); mismatchDetector = new IDMismatchDetector(this); node = new Node(this); unreachableCache = new NonReachableCache(); serverManager.notifyOnServerAdded(serverListener); db = new Database(); stats.setDbStats(db.getStats()); tman = new TaskManager(this); running = true; storage = new GenericStorage(); } /* * (non-Javadoc) * * @see lbms.plugins.mldht.kad.DHTBase#start(java.lang.String, int) */ public void start (DHTConfiguration config) throws SocketException { if (running) { return; } if(this.scheduler == null) this.scheduler = getDefaultScheduler(); this.config = config; useRouterBootstrapping = !config.noRouterBootstrap(); if(!Files.isDirectory(config.getStoragePath())) DHT.log("Warning: storage path " + config.getStoragePath() +" is not a directory. DHT will not be able to persist state" , LogLevel.Info); table_file = config.getStoragePath().resolve(type.shortName+"-table.cache"); setStatus(DHTStatus.Stopped, DHTStatus.Initializing); stats.resetStartedTimestamp(); logInfo("Starting DHT on port " + getPort()); resolveBootstrapAddresses(); connectionManager = new NIOConnectionManager("mlDHT "+type.shortName+" NIO Selector"); populate(); node.initKey(config); node.loadTable(table_file); // these checks are fairly expensive on large servers (network interface enumeration) // schedule them separately scheduledActions.add(scheduler.scheduleWithFixedDelay(serverManager::doBindChecks, 10, 10, TimeUnit.SECONDS)); scheduledActions.add(scheduler.scheduleWithFixedDelay(() -> { // maintenance that should run all the time, before the first queries tman.dequeue(); if (running) onStatsUpdate(); }, 5000, DHTConstants.DHT_UPDATE_INTERVAL, TimeUnit.MILLISECONDS)); // initialize as many RPC servers as we need serverManager.refresh(System.currentTimeMillis()); started(); } public void started () { for(RoutingTableEntry bucket : node.table().list()) { RPCServer srv = serverManager.getRandomServer(); if(srv == null) break; Task t = new PingRefreshTask(srv, node, bucket.getBucket(), true); t.setInfo("Startup ping for " + bucket.prefix); if(t.getTodoCount() > 0) tman.addTask(t); } bootstrap(); /* if(type == DHTtype.IPV6_DHT) { Task t = new KeyspaceCrawler(srv, node); tman.addTask(t); }*/ scheduledActions.add(scheduler.scheduleWithFixedDelay(() -> { try { update(); } catch (RuntimeException e) { log(e, LogLevel.Fatal); } }, 5000, DHTConstants.DHT_UPDATE_INTERVAL, TimeUnit.MILLISECONDS)); scheduledActions.add(scheduler.scheduleWithFixedDelay(() -> { try { long now = System.currentTimeMillis(); db.expire(now); cache.cleanup(now); storage.cleanup(); } catch (Exception e) { log(e, LogLevel.Fatal); } }, 1000, DHTConstants.CHECK_FOR_EXPIRED_ENTRIES, TimeUnit.MILLISECONDS)); scheduledActions.add(scheduler.scheduleWithFixedDelay(node::decayThrottle, 1, Node.throttleUpdateIntervalMinutes, TimeUnit.MINUTES)); // single ping to a random node per server to check socket liveness scheduledActions.add(scheduler.scheduleWithFixedDelay(() -> { for(RPCServer srv : serverManager.getAllServers()) { if(srv.getNumActiveRPCCalls() > 0) continue; node.getRandomEntry().ifPresent((entry) -> { PingRequest req = new PingRequest(); req.setDestination(entry.getAddress()); RPCCall call = new RPCCall(req); call.builtFromEntry(entry); call.setExpectedID(entry.getID()); srv.doCall(call); }); } ; }, 1, 10, TimeUnit.SECONDS)); // deep lookup to make ourselves known to random parts of the keyspace scheduledActions.add(scheduler.scheduleWithFixedDelay(() -> { try { for(RPCServer srv : serverManager.getAllServers()) findNode(Key.createRandomKey(), false, false, srv, t -> t.setInfo("Random Refresh Lookup")); } catch (RuntimeException e1) { log(e1, LogLevel.Fatal); } try { if(!node.isInSurvivalMode()) node.saveTable(table_file); } catch (IOException e2) { e2.printStackTrace(); } }, DHTConstants.RANDOM_LOOKUP_INTERVAL, DHTConstants.RANDOM_LOOKUP_INTERVAL, TimeUnit.MILLISECONDS)); scheduledActions.add(scheduler.scheduleWithFixedDelay(mismatchDetector::purge, 2, 3, TimeUnit.MINUTES)); scheduledActions.add(scheduler.scheduleWithFixedDelay(unreachableCache::cleanStaleEntries, 2, 3, TimeUnit.MINUTES)); } /* * (non-Javadoc) * * @see lbms.plugins.mldht.kad.DHTBase#stop() */ public void stop () { if (!running) { return; } //scheduler.shutdown(); logInfo("Initated DHT shutdown"); for (Task t : tman.getActiveTasks()) { t.kill(); } for(ScheduledFuture<?> future : scheduledActions) { future.cancel(false); // none of the scheduled tasks should experience exceptions, log them if they did try { future.get(); } catch (ExecutionException e) { DHT.log(e.getCause(), LogLevel.Fatal); } catch (InterruptedException e) { DHT.log(e, LogLevel.Fatal); } catch (CancellationException e) { // do nothing, we just cancelled it } } // scheduler.getQueue().removeAll(scheduledActions); scheduledActions.clear(); logInfo("stopping servers"); running = false; serverManager.destroy(); try { logInfo("persisting routing table on shutdown"); node.saveTable(table_file); logInfo("table persisted"); } catch (IOException e) { e.printStackTrace(); } stopped(); tman = null; db = null; node = null; cache = null; serverManager = null; setStatus(DHTStatus.Initializing, DHTStatus.Stopped); setStatus(DHTStatus.Running, DHTStatus.Stopped); } /* * (non-Javadoc) * * @see lbms.plugins.mldht.kad.DHTBase#getNode() */ public Node getNode () { return node; } public IDMismatchDetector getMismatchDetector() { return mismatchDetector; } public Database getDatabase() { return db; } /* * (non-Javadoc) * * @see lbms.plugins.mldht.kad.DHTBase#getTaskManager() */ public TaskManager getTaskManager () { return tman; } /* * (non-Javadoc) * * @see lbms.plugins.mldht.kad.DHTBase#stopped() */ public void stopped () { // TODO Auto-generated method stub } /* * (non-Javadoc) * * @see lbms.plugins.mldht.kad.DHTBase#update() */ public void update () { long now = System.currentTimeMillis(); serverManager.refresh(now); if (!isRunning()) { return; } node.doBucketChecks(now); if (node.getNumEntriesInRoutingTable() < DHTConstants.BOOTSTRAP_IF_LESS_THAN_X_PEERS || now - lastBootstrap > DHTConstants.SELF_LOOKUP_INTERVAL) { //regualary search for our id to update routing table bootstrap(); } else { setStatus(DHTStatus.Initializing, DHTStatus.Running); } } private Collection<InetSocketAddress> bootstrapAddresses = Collections.emptyList(); private void resolveBootstrapAddresses() { List<InetSocketAddress> nodeAddresses = new ArrayList<>(); try { for(InetSocketAddress unres : DHTConstants.UNRESOLVED_BOOTSTRAP_NODES) { for(InetAddress addr : InetAddress.getAllByName(unres.getHostString())) { if(type.canUseAddress(addr)) nodeAddresses.add(new InetSocketAddress(addr, unres.getPort())); } } } catch(Exception e) { DHT.log(e, LogLevel.Error); return; } bootstrapAddresses = nodeAddresses; } Collection<InetSocketAddress> getBootStrapNodes() { return bootstrapAddresses; } /** * Initiates a Bootstrap. * * This function bootstraps with router.bittorrent.com if there are less * than 10 Peers in the routing table. If there are more then a lookup on * our own ID is initiated. If the either Task is finished than it will try * to fill the Buckets. */ public synchronized void bootstrap () { if (!isRunning() || System.currentTimeMillis() - lastBootstrap < DHTConstants.BOOTSTRAP_MIN_INTERVAL) { return; } if(!bootstrapping.compareAndSet(BootstrapState.NONE, BootstrapState.FILL)) return; if (useRouterBootstrapping && node.getNumEntriesInRoutingTable() < DHTConstants.USE_BT_ROUTER_IF_LESS_THAN_X_PEERS) { routerBootstrap(); } else { fillHomeBuckets(Collections.emptyList()); } } void routerBootstrap() { List<CompletableFuture<RPCCall>> callFutures = new ArrayList<>(); resolveBootstrapAddresses(); Collection<InetSocketAddress> addrs = bootstrapAddresses; for(InetSocketAddress addr : addrs) { if (!type.canUseSocketAddress(addr)) continue; FindNodeRequest fnr = new FindNodeRequest(Key.createRandomKey()); fnr.setDestination(addr); RPCCall c = new RPCCall(fnr); CompletableFuture<RPCCall> f = new CompletableFuture<>(); RPCServer srv = serverManager.getRandomActiveServer(true); c.addListener(new RPCCallListener() { @Override public void stateTransition(RPCCall c, RPCState previous, RPCState current) { if(current == RPCState.RESPONDED || current == RPCState.ERROR || current == RPCState.TIMEOUT) f.complete(c); } }); callFutures.add(f); srv.doCall(c); } awaitAll(callFutures).thenAccept(calls -> { Class<FindNodeResponse> clazz = FindNodeResponse.class; Set<KBucketEntry> s = calls.stream().filter(clazz::isInstance).map(clazz::cast).map(fnr -> fnr.getNodes(getType())).flatMap(NodeList::entries).collect(Collectors.toSet()); fillHomeBuckets(s); }); } void fillHomeBuckets(Collection<KBucketEntry> entries) { if(node.getNumEntriesInRoutingTable() == 0 && entries.isEmpty()) { bootstrapping.set(BootstrapState.NONE); return; } bootstrapping.set(BootstrapState.BOOTSTRAP); final AtomicInteger taskCount = new AtomicInteger(); TaskListener bootstrapListener = t -> { int count = taskCount.decrementAndGet(); if(count == 0) { bootstrapping.set(BootstrapState.NONE); ; lastBootstrap = System.currentTimeMillis(); } // fill the remaining buckets once all bootstrap operations finished if (count == 0 && running && node.getNumEntriesInRoutingTable() > DHTConstants.USE_BT_ROUTER_IF_LESS_THAN_X_PEERS) { node.fillBuckets(); } }; for(RPCServer srv : serverManager.getAllServers()) { findNode(srv.getDerivedID(), true, true, srv, t -> { taskCount.incrementAndGet(); t.setInfo("Bootstrap: lookup for self"); t.injectCandidates(entries); t.addListener(bootstrapListener); }); } if(taskCount.get() == 0) bootstrapping.set(BootstrapState.NONE); } private void findNode (Key id, boolean isBootstrap, boolean isPriority, RPCServer server, Consumer<NodeLookup> configureTask) { if (!running || server == null) { return; } NodeLookup at = new NodeLookup(id, server, node, isBootstrap); if(configureTask != null) configureTask.accept(at); tman.addTask(at, isPriority); } /* * (non-Javadoc) * * @see lbms.plugins.mldht.kad.DHTBase#fillBucket(lbms.plugins.mldht.kad.KBucket) */ public void fillBucket (Key id, KBucket bucket, Consumer<NodeLookup> configure) { bucket.updateRefreshTimer(); findNode(id, false, true, serverManager.getRandomActiveServer(true), configure); } public void sendError (MessageBase origMsg, int code, String msg) { ErrorMessage errMsg = new ErrorMessage(origMsg.getMTID(), code, msg); errMsg.setMethod(origMsg.getMethod()); errMsg.setDestination(origMsg.getOrigin()); origMsg.getServer().sendMessage(errMsg); } public Key getOurID () { if (running) { return node.getRootID(); } return null; } private void onStatsUpdate () { stats.setNumTasks(tman.getNumTasks() + tman.getNumQueuedTasks()); stats.setNumPeers(node.getNumEntriesInRoutingTable()); long numSent = 0;long numReceived = 0;int activeCalls = 0; for(RPCServer s : serverManager.getAllServers()) { numSent += s.getNumSent(); numReceived += s.getNumReceived(); activeCalls += s.getNumActiveRPCCalls(); } stats.setNumSentPackets(numSent); stats.setNumReceivedPackets(numReceived); stats.setNumRpcCalls(activeCalls); for (int i = 0; i < statsListeners.size(); i++) { statsListeners.get(i).statsUpdated(stats); } } private void setStatus (DHTStatus expected, DHTStatus newStatus) { if (this.status.equals(expected)) { DHTStatus old = this.status; this.status = newStatus; if (!statusListeners.isEmpty()) { for (int i = 0; i < statusListeners.size(); i++) { statusListeners.get(i).statusChanged(newStatus, old); } } } } public void addStatsListener (DHTStatsListener listener) { statsListeners.add(listener); } public void removeStatsListener (DHTStatsListener listener) { statsListeners.remove(listener); } public void addIndexingListener(DHTIndexingListener listener) { indexingListeners.add(listener); } public void addStatusListener (DHTStatusListener listener) { statusListeners.add(listener); } public void removeStatusListener (DHTStatusListener listener) { statusListeners.remove(listener); } public void printDiagnostics(PrintWriter w) { if(!running) return; //StringBuilder b = new StringBuilder(); for(ScheduledFuture<?> f : scheduledActions) if(f.isDone()) { // check for exceptions try { f.get(); } catch (ExecutionException | InterruptedException e) { e.printStackTrace(w); } } w.println("=========================="); w.println("DHT Diagnostics. Type "+type); w.println("# of active servers / all servers: "+ serverManager.getActiveServerCount()+ '/'+ serverManager.getServerCount()); if(!isRunning()) return; w.append("-----------------------\n"); w.append("Stats\n"); w.append("Reachable node estimate: "+ estimator.getEstimate()+ " ("+estimator.getStability()+")\n"); w.append(stats.toString()); w.append("-----------------------\n"); w.append("Routing table\n"); w.append(node.toString() + "\n"); w.append("-----------------------\n"); w.append("RPC Servers\n"); for(RPCServer srv : serverManager.getAllServers()) w.append(srv.toString()); w.append("-----------------------\n"); w.append("Blacklist\n"); w.append(mismatchDetector.toString() + '\n'); w.append("-----------------------\n"); w.append("Lookup Cache\n"); cache.printDiagnostics(w); w.append("-----------------------\n"); w.append("Tasks\n"); w.append(tman.toString()); w.append("\n\n\n"); } /** * @return the logger */ // public static DHTLogger getLogger () { // return logger; // } /** * @param logger the logger to set */ public static void setLogger (DHTLogger logger) { DHT.logger = logger; } /** * @return the logLevel */ public static LogLevel getLogLevel () { return logLevel; } /** * @param logLevel the logLevel to set */ public static void setLogLevel (LogLevel logLevel) { DHT.logLevel = logLevel; logger.log("Change LogLevel to: " + logLevel, LogLevel.Info); } /** * @return the scheduler */ private static ScheduledExecutorService getDefaultScheduler () { ScheduledExecutorService service = defaultScheduler; if(service == null) { initDefaultScheduler(); service = defaultScheduler; } return service; } private static void initDefaultScheduler() { synchronized (DHT.class) { if(defaultScheduler == null) { executorGroup = new ThreadGroup("mlDHT"); int threads = Math.max(Runtime.getRuntime().availableProcessors(),2); defaultScheduler = new ScheduledThreadPoolExecutor(threads, (ThreadFactory) r -> { Thread t = new Thread(executorGroup, r, "mlDHT Scheduler"); t.setUncaughtExceptionHandler((t1, e) -> DHT.log(e, LogLevel.Error)); t.setDaemon(true); return t; }); defaultScheduler.setCorePoolSize(threads); defaultScheduler.setKeepAliveTime(20, TimeUnit.SECONDS); defaultScheduler.allowCoreThreadTimeOut(true); } } } public static void log (String message, LogLevel level) { if (level.compareTo(logLevel) < 1) { // <= logger.log(message, level); } } public static void log (Throwable e, LogLevel level) { if (level.compareTo(logLevel) < 1) { // <= logger.log(e, level); } } public static void logFatal (String message) { log(message, LogLevel.Fatal); } public static void logError (String message) { log(message, LogLevel.Error); } public static void logInfo (String message) { log(message, LogLevel.Info); } public static void logDebug (String message) { log(message, LogLevel.Debug); } public static void logVerbose (String message) { log(message, LogLevel.Verbose); } public static boolean isLogLevelEnabled (LogLevel level) { return level.compareTo(logLevel) < 1; } public static enum LogLevel { Fatal, Error, Info, Debug, Verbose } }