From d8080278b349fa9222767986454d25062d7dc3c5 Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 6 Feb 2011 00:12:54 +0000 Subject: [PATCH 01/23] initial DHT code, needs work --- .../src/org/klomp/snark/dht/DHTNodes.java | 110 ++ .../src/org/klomp/snark/dht/DHTTracker.java | 132 ++ .../src/org/klomp/snark/dht/InfoHash.java | 19 + .../java/src/org/klomp/snark/dht/KRPC.java | 1278 +++++++++++++++++ .../java/src/org/klomp/snark/dht/MsgID.java | 32 + .../java/src/org/klomp/snark/dht/NID.java | 19 + .../src/org/klomp/snark/dht/NodeInfo.java | 181 +++ .../klomp/snark/dht/NodeInfoComparator.java | 31 + .../java/src/org/klomp/snark/dht/Peer.java | 30 + .../java/src/org/klomp/snark/dht/Peers.java | 21 + .../org/klomp/snark/dht/SHA1Comparator.java | 31 + .../java/src/org/klomp/snark/dht/Token.java | 39 + .../src/org/klomp/snark/dht/TokenKey.java | 20 + .../src/org/klomp/snark/dht/Torrents.java | 19 + 14 files changed, 1962 insertions(+) create mode 100644 apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java create mode 100644 apps/i2psnark/java/src/org/klomp/snark/dht/DHTTracker.java create mode 100644 apps/i2psnark/java/src/org/klomp/snark/dht/InfoHash.java create mode 100644 apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java create mode 100644 apps/i2psnark/java/src/org/klomp/snark/dht/MsgID.java create mode 100644 apps/i2psnark/java/src/org/klomp/snark/dht/NID.java create mode 100644 apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java create mode 100644 apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfoComparator.java create mode 100644 apps/i2psnark/java/src/org/klomp/snark/dht/Peer.java create mode 100644 apps/i2psnark/java/src/org/klomp/snark/dht/Peers.java create mode 100644 apps/i2psnark/java/src/org/klomp/snark/dht/SHA1Comparator.java create mode 100644 apps/i2psnark/java/src/org/klomp/snark/dht/Token.java create mode 100644 apps/i2psnark/java/src/org/klomp/snark/dht/TokenKey.java create mode 100644 apps/i2psnark/java/src/org/klomp/snark/dht/Torrents.java diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java b/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java new file mode 100644 index 000000000..cd6d0f37c --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java @@ -0,0 +1,110 @@ +package org.klomp.snark.dht; +/* + * From zzzot, modded and relicensed to GPLv2 + */ + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; + +import net.i2p.I2PAppContext; +import net.i2p.crypto.SHA1Hash; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; +import net.i2p.util.SimpleScheduler; +import net.i2p.util.SimpleTimer; + +/** + * All the nodes we know about, stored as a mapping from + * node ID to a Destination and Port. + * Also uses the keySet as a subsitute for kbuckets. + * + * Swap this out for a real DHT later. + * + * @since 0.8.4 + * @author zzz + */ +public class DHTNodes extends ConcurrentHashMap { + + private final I2PAppContext _context; + private long _expireTime; + private final Log _log; + + /** stagger with other cleaners */ + private static final long CLEAN_TIME = 237*1000; + private static final long MAX_EXPIRE_TIME = 60*60*1000; + private static final long MIN_EXPIRE_TIME = 5*60*1000; + private static final long DELTA_EXPIRE_TIME = 7*60*1000; + private static final int MAX_PEERS = 9999; + + public DHTNodes(I2PAppContext ctx) { + super(); + _context = ctx; + _expireTime = MAX_EXPIRE_TIME; + _log = _context.logManager().getLog(DHTNodes.class); + SimpleScheduler.getInstance().addPeriodicEvent(new Cleaner(), CLEAN_TIME); + } + + /** + * Fake DHT + * @param sha1 either a InfoHash or a NID + */ + List findClosest(SHA1Hash h, int numWant) { + // sort the whole thing + Set all = new TreeSet(new SHA1Comparator(h)); + all.addAll(keySet()); + int sz = all.size(); + int max = Math.min(numWant, sz); + + // return the first ones + List rv = new ArrayList(max); + int count = 0; + for (NID nid : all) { + if (count++ >= max) + break; + NodeInfo nInfo = get(nid); + if (nInfo == null) + continue; + rv.add(nInfo); + } + return rv; + } + + /**** used CHM methods to be replaced: + public Collection values() {} + public NodeInfo get(NID nID) {} + public NodeInfo putIfAbssent(NID nID, NodeInfo nInfo) {} + public int size() {} + ****/ + + /** */ + private class Cleaner implements SimpleTimer.TimedEvent { + + public void timeReached() { + long now = _context.clock().now(); + int peerCount = 0; + for (Iterator iter = DHTNodes.this.values().iterator(); iter.hasNext(); ) { + NodeInfo peer = iter.next(); + if (peer.lastSeen() < now - _expireTime) + iter.remove(); + else + peerCount++; + } + + if (peerCount > MAX_PEERS) + _expireTime = Math.max(_expireTime - DELTA_EXPIRE_TIME, MIN_EXPIRE_TIME); + else + _expireTime = Math.min(_expireTime + DELTA_EXPIRE_TIME, MAX_EXPIRE_TIME); + + if (_log.shouldLog(Log.INFO)) + _log.info("DHT storage cleaner done, now with " + + peerCount + " peers, " + + DataHelper.formatDuration(_expireTime) + " expiration"); + + } + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/DHTTracker.java b/apps/i2psnark/java/src/org/klomp/snark/dht/DHTTracker.java new file mode 100644 index 000000000..712b89e4f --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/DHTTracker.java @@ -0,0 +1,132 @@ +package org.klomp.snark.dht; +/* + * From zzzot, relicensed to GPLv2 + */ + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import net.i2p.I2PAppContext; +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.util.Log; +import net.i2p.util.SimpleScheduler; +import net.i2p.util.SimpleTimer; + +/** + * The tracker stores peers, i.e. Dest hashes (not nodes). + * + * @since 0.8.4 + * @author zzz + */ +class DHTTracker { + + private final I2PAppContext _context; + private final Torrents _torrents; + private long _expireTime; + private final Log _log; + + /** stagger with other cleaners */ + private static final long CLEAN_TIME = 199*1000; + /** make this longer than postman's tracker */ + private static final long MAX_EXPIRE_TIME = 95*60*1000; + private static final long MIN_EXPIRE_TIME = 5*60*1000; + private static final long DELTA_EXPIRE_TIME = 7*60*1000; + private static final int MAX_PEERS = 9999; + + DHTTracker(I2PAppContext ctx) { + _context = ctx; + _torrents = new Torrents(); + _expireTime = MAX_EXPIRE_TIME; + _log = _context.logManager().getLog(DHTTracker.class); + SimpleScheduler.getInstance().addPeriodicEvent(new Cleaner(), CLEAN_TIME); + } + + void stop() { + _torrents.clear(); + // no way to stop the cleaner + } + + void announce(InfoHash ih, Hash hash) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Announce " + hash + " for " + ih); + Peers peers = _torrents.get(ih); + if (peers == null) { + peers = new Peers(); + Peers peers2 = _torrents.putIfAbsent(ih, peers); + if (peers2 != null) + peers = peers2; + } + + Peer peer = new Peer(hash.getData()); + Peer peer2 = peers.putIfAbsent(peer, peer); + if (peer2 != null) + peer = peer2; + peer.setLastSeen(_context.clock().now()); + } + + void unannounce(InfoHash ih, Hash hash) { + Peers peers = _torrents.get(ih); + if (peers == null) + return; + Peer peer = new Peer(hash.getData()); + peers.remove(peer); + } + + /** + * Caller's responsibility to remove himself from the list + * @return list or empty list (never null) + */ + List getPeers(InfoHash ih, int max) { + Peers peers = _torrents.get(ih); + if (peers == null) + return Collections.EMPTY_LIST; + + int size = peers.size(); + List rv = new ArrayList(peers.values()); + if (max < size) { + Collections.shuffle(rv, _context.random()); + rv = rv.subList(0, max); + } + return rv; + } + + private class Cleaner implements SimpleTimer.TimedEvent { + + public void timeReached() { + long now = _context.clock().now(); + int torrentCount = 0; + int peerCount = 0; + for (Iterator iter = _torrents.values().iterator(); iter.hasNext(); ) { + Peers p = iter.next(); + int recent = 0; + for (Iterator iterp = p.values().iterator(); iterp.hasNext(); ) { + Peer peer = iterp.next(); + if (peer.lastSeen() < now - _expireTime) + iterp.remove(); + else { + recent++; + peerCount++; + } + } + if (recent <= 0) + iter.remove(); + else + torrentCount++; + } + + if (peerCount > MAX_PEERS) + _expireTime = Math.max(_expireTime - DELTA_EXPIRE_TIME, MIN_EXPIRE_TIME); + else + _expireTime = Math.min(_expireTime + DELTA_EXPIRE_TIME, MAX_EXPIRE_TIME); + + if (_log.shouldLog(Log.INFO)) + _log.info("DHT tracker cleaner done, now with " + + torrentCount + " torrents, " + + peerCount + " peers, " + + DataHelper.formatDuration(_expireTime) + " expiration"); + } + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/InfoHash.java b/apps/i2psnark/java/src/org/klomp/snark/dht/InfoHash.java new file mode 100644 index 000000000..2b439c6c4 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/InfoHash.java @@ -0,0 +1,19 @@ +package org.klomp.snark.dht; +/* + * From zzzot, modded and relicensed to GPLv2 + */ + +import net.i2p.crypto.SHA1Hash; + +/** + * A 20-byte SHA1 info hash + * + * @since 0.8.4 + * @author zzz + */ +public class InfoHash extends SHA1Hash { + + public InfoHash(byte[] data) { + super(data); + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java new file mode 100644 index 000000000..ef6757a83 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java @@ -0,0 +1,1278 @@ +package org.klomp.snark.dht; + +/* + * GPLv2 + */ + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; + +import net.i2p.I2PAppContext; +import net.i2p.client.I2PClient; +import net.i2p.client.I2PSession; +import net.i2p.client.I2PSessionException; +import net.i2p.client.I2PSessionMuxedListener; +import net.i2p.client.datagram.I2PDatagramDissector; +import net.i2p.client.datagram.I2PDatagramMaker; +import net.i2p.client.datagram.I2PInvalidDatagramException; +import net.i2p.crypto.SHA1Hash; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.SimpleDataStructure; +import net.i2p.util.Log; +import net.i2p.util.SimpleScheduler; +import net.i2p.util.SimpleTimer; +import net.i2p.util.SimpleTimer2; + +import org.klomp.snark.bencode.BDecoder; +import org.klomp.snark.bencode.BEncoder; +import org.klomp.snark.bencode.BEValue; +import org.klomp.snark.bencode.InvalidBEncodingException; + + +/** + * Standard BEP 5 + * Mods for I2P: + *
+ * - The UDP port need not be pinged after receiving a PORT message.
+ *
+ * - The UDP (datagram) port listed in the compact node info is used
+ *   to receive repliable (signed) datagrams.
+ *   This is used for queries, except for announces.
+ *   We call this the "query port".
+ *   In addition to that UDP port, we use a second datagram
+ *   port equal to the signed port + 1. This is used to receive
+ *   unsigned (raw) datagrams for replies, errors, and announce queries..
+ *   We call this the "response port".
+ *
+ * - Compact peer info is 32 bytes (32 byte SHA256 Hash)
+ *   instead of 4 byte IP + 2 byte port. There is no peer port.
+ *
+ * - Compact node info is 54 bytes (20 byte SHA1 Hash + 32 byte SHA256 Hash + 2 byte port)
+ *   instead of 20 byte SHA1 Hash + 4 byte IP + 2 byte port.
+ *   Port is the query port, the response port is always the query port + 1.
+ *
+ * - The trackerless torrent dictionary "nodes" key is a list of
+ *   32 byte binary strings (SHA256 Hashes) instead of a list of lists
+ *   containing a host string and a port integer.
+ * 
+ * + * Questions: + * - nodes (in the find_node and get_peers response) is one concatenated string, not a list of strings, right? + * - Node ID enforcement, keyspace rotation? + * + * @since 0.8.4 + * @author zzz + */ +public class KRPC implements I2PSessionMuxedListener, DHT { + + private final I2PAppContext _context; + private final Log _log; + + /** our tracker */ + private final DHTTracker _tracker; + /** who we know */ + private final DHTNodes _knownNodes; + /** index to sent queries awaiting reply */ + private final ConcurrentHashMap _sentQueries; + /** index to outgoing tokens, sent in reply to a get_peers query */ + private final ConcurrentHashMap _outgoingTokens; + /** index to incoming tokens, received in a peers or nodes reply */ + private final ConcurrentHashMap _incomingTokens; + + /** hook to inject and receive datagrams */ + private final I2PSession _session; + /** 20 byte random id + 32 byte Hash + 2 byte port */ + private final NodeInfo _myNodeInfo; + /** unsigned dgrams */ + private final int _rPort; + /** signed dgrams */ + private final int _qPort; + + /** all-zero NID used for pings */ + private static final NID _fakeNID = new NID(new byte[NID.HASH_LENGTH]); + + /** Max number of nodes to return. BEP 5 says 8 */ + private static final int K = 8; + /** Max number of peers to return. BEP 5 doesn't say. We'll use the same as I2PSnarkUtil.MAX_CONNECTIONS */ + private static final int MAX_WANT = 16; + + /** overloads error codes which start with 201 */ + private static final int REPLY_NONE = 0; + private static final int REPLY_PONG = 1; + private static final int REPLY_PEERS = 2; + private static final int REPLY_NODES = 3; + + /** how long since last heard from do we delete - BEP 5 says 15 minutes */ + private static final long MAX_NODEINFO_AGE = 60*60*1000; + /** how long since generated do we delete - BEP 5 says 10 minutes */ + private static final long MAX_TOKEN_AGE = 60*60*1000; + /** how long since sent do we wait for a reply */ + private static final long MAX_MSGID_AGE = 2*60*1000; + /** how long since sent do we wait for a reply */ + private static final long DEFAULT_QUERY_TIMEOUT = 75*1000; + /** stagger with other cleaners */ + private static final long CLEAN_TIME = 63*1000; + + public KRPC (I2PAppContext ctx, I2PSession session) { + _context = ctx; + _session = session; + _log = ctx.logManager().getLog(KRPC.class); + _tracker = new DHTTracker(ctx); + + // in place of a DHT, store everybody we hear from for now + _knownNodes = new DHTNodes(ctx); + _sentQueries = new ConcurrentHashMap(); + _outgoingTokens = new ConcurrentHashMap(); + _incomingTokens = new ConcurrentHashMap(); + + // Construct my NodeInfo + // ports can really be fixed, just do this for testing + _qPort = 30000 + ctx.random().nextInt(99); + _rPort = _qPort + 1; + byte[] myID = new byte[NID.HASH_LENGTH]; + ctx.random().nextBytes(myID); + NID myNID = new NID(myID); + _myNodeInfo = new NodeInfo(myNID, session.getMyDestination(), _qPort); + + session.addMuxedSessionListener(this, I2PSession.PROTO_DATAGRAM, _rPort); + session.addMuxedSessionListener(this, I2PSession.PROTO_DATAGRAM, _qPort); + // can't be stopped + SimpleScheduler.getInstance().addPeriodicEvent(new Cleaner(), CLEAN_TIME); + } + + ///////////////// Public methods + + /** + * For bootstrapping if loaded from config file. + * @param when when did we hear from them + */ + public void addNode(NodeInfo nInfo, long when) { + heardFrom(nInfo, when); + } + + /** + * NodeInfo heard from + */ + public void addNode(NodeInfo nInfo) { + heardFrom(nInfo); + } + + /** + * For saving in a config file. + * @return the values, not a copy, could change, use an iterator + */ + public Collection getNodes() { + return _knownNodes.values(); + } + + /** + * @return The UDP port that should be included in a PORT message. + */ + public int getPort() { + return _qPort; + } + + /** + * Ping. We don't have a NID yet so the node is presumed + * to be absent from our DHT. + * Non-blocking, does not wait for pong. + * If and when the pong is received the node will be inserted in our DHT. + */ + public void ping(Destination dest, int port) { + NodeInfo nInfo = new NodeInfo(_fakeNID, dest, port); + sendPing(nInfo); + } + + /** + * Bootstrapping or background thread. + * Blocking! + * This is almost the same as getPeers() + * + * @param maxNodes how many to contact + * @param maxWait how long to wait for each to reply (not total) must be > 0 + * @param parallel how many outstanding at once (unimplemented, always 1) + */ + public void explore(int maxNodes, long maxWait, int parallel) { + // Initial set to try, will get added to as we go + NID myNID = _myNodeInfo.getNID(); + List nodes = _knownNodes.findClosest(myNID, maxNodes); + if (nodes.isEmpty()) { + if (_log.shouldLog(Log.WARN)) + _log.info("DHT is empty, cannot explore"); + return; + } + SortedSet toTry = new TreeSet(new NodeInfoComparator(myNID)); + toTry.addAll(nodes); + Set tried = new HashSet(); + + if (_log.shouldLog(Log.INFO)) + _log.info("Starting explore"); + for (int i = 0; i < maxNodes; i++) { + NodeInfo nInfo; + try { + nInfo = toTry.first(); + } catch (NoSuchElementException nsee) { + break; + } + toTry.remove(nInfo); + tried.add(nInfo); + + // this isn't going to work, he will just return our own? + ReplyWaiter waiter = sendFindNode(nInfo, _myNodeInfo); + if (waiter == null) + continue; + synchronized(waiter) { + try { + waiter.wait(maxWait); + } catch (InterruptedException ie) {} + } + + int replyType = waiter.getReplyCode(); + if (replyType == REPLY_NONE) { + if (_log.shouldLog(Log.INFO)) + _log.info("Got no reply"); + } else if (replyType == REPLY_NODES) { + List reply = (List) waiter.getReplyObject(); + // It seems like we are just going to get back ourselves all the time + if (_log.shouldLog(Log.INFO)) + _log.info("Got " + reply.size() + " nodes"); + for (NodeInfo ni : reply) { + if (! (ni.equals(_myNodeInfo) || (toTry.contains(ni) && tried.contains(ni)))) + toTry.add(ni); + } + } else { + if (_log.shouldLog(Log.INFO)) + _log.info("Got unexpected reply " + replyType + ": " + waiter.getReplyObject()); + } + } + if (_log.shouldLog(Log.INFO)) + _log.info("Finished explore"); + } + + /** + * Local lookup only + * @param ih a 20-byte info hash + * @param max max to return + * @return list or empty list (never null) + */ + public List findClosest(byte[] ih, int max) { + List nodes = _knownNodes.findClosest(new InfoHash(ih), max); + return nodes; + } + + /** + * Get peers for a torrent. + * Blocking! + * Caller should run in a thread. + * + * @param ih the Info Hash (torrent) + * @param max maximum number of peers to return + * @param maxWait the maximum time to wait (ms) must be > 0 + * @return list or empty list (never null) + */ + public List getPeers(byte[] ih, int max, long maxWait) { + // check local tracker first + InfoHash iHash = new InfoHash(ih); + List rv = _tracker.getPeers(iHash, max); + rv.remove(_myNodeInfo.getHash()); + if (!rv.isEmpty()) + return rv; // TODO get DHT too? + + // Initial set to try, will get added to as we go + List nodes = _knownNodes.findClosest(iHash, max); + SortedSet toTry = new TreeSet(new NodeInfoComparator(iHash)); + toTry.addAll(nodes); + Set tried = new HashSet(); + + if (_log.shouldLog(Log.INFO)) + _log.info("Starting getPeers"); + for (int i = 0; i < max; i++) { + NodeInfo nInfo; + try { + nInfo = toTry.first(); + } catch (NoSuchElementException nsee) { + break; + } + toTry.remove(nInfo); + tried.add(nInfo); + + ReplyWaiter waiter = sendGetPeers(nInfo, iHash); + if (waiter == null) + continue; + synchronized(waiter) { + try { + waiter.wait(maxWait); + } catch (InterruptedException ie) {} + } + + int replyType = waiter.getReplyCode(); + if (replyType == REPLY_NONE) { + if (_log.shouldLog(Log.INFO)) + _log.info("Got no reply"); + } else if (replyType == REPLY_PONG) { + if (_log.shouldLog(Log.INFO)) + _log.info("Got pong"); + } else if (replyType == REPLY_PEERS) { + if (_log.shouldLog(Log.INFO)) + _log.info("Got peers"); + List reply = (List) waiter.getReplyObject(); + if (!reply.isEmpty()) { + if (_log.shouldLog(Log.INFO)) + _log.info("Finished get Peers, returning " + reply.size()); + return reply; + } + } else if (replyType == REPLY_NODES) { + List reply = (List) waiter.getReplyObject(); + if (_log.shouldLog(Log.INFO)) + _log.info("Got " + reply.size() + " nodes"); + for (NodeInfo ni : reply) { + if (! (ni.equals(_myNodeInfo) || tried.contains(ni) || toTry.contains(ni))) + toTry.add(ni); + } + } else { + if (_log.shouldLog(Log.INFO)) + _log.info("Got unexpected reply " + replyType + ": " + waiter.getReplyObject()); + } + } + if (_log.shouldLog(Log.INFO)) + _log.info("Finished get Peers, fail"); + return Collections.EMPTY_LIST; + } + + /** + * Announce to ourselves. + * Non-blocking. + * + * @param ih the Info Hash (torrent) + */ + public void announce(byte[] ih) { + InfoHash iHash = new InfoHash(ih); + _tracker.announce(iHash, _myNodeInfo.getHash()); + } + + /** + * Announce somebody else we know about. + * Non-blocking. + * + * @param ih the Info Hash (torrent) + * @param peer the peer's Hash + */ + public void announce(byte[] ih, byte[] peerHash) { + InfoHash iHash = new InfoHash(ih); + _tracker.announce(iHash, new Hash(peerHash)); + } + + /** + * Remove reference to ourselves in the local tracker. + * Use when shutting down the torrent locally. + * Non-blocking. + * + * @param ih the Info Hash (torrent) + */ + public void unannounce(byte[] ih) { + InfoHash iHash = new InfoHash(ih); + _tracker.unannounce(iHash, _myNodeInfo.getHash()); + } + + /** + * Announce to the closest DHT peers. + * Blocking unless maxWait <= 0 + * Caller should run in a thread. + * This also automatically announces ourself to our local tracker. + * For best results do a getPeers() first so we have tokens. + * + * @param ih the Info Hash (torrent) + * @param maxWait the maximum total time to wait (ms) or 0 to do all in parallel and return immediately. + * @return the number of successful announces, not counting ourselves. + */ + public int announce(byte[] ih, int max, long maxWait) { + announce(ih); + int rv = 0; + long start = _context.clock().now(); + List nodes = _knownNodes.findClosest(new InfoHash(ih), max); + for (NodeInfo nInfo : nodes) { + if (announce(ih, nInfo, Math.min(maxWait, 60*1000))) + rv++; + maxWait -= _context.clock().now() - start; + if (maxWait < 1000) + break; + } + return rv; + } + + /** + * Announce to a single DHT peer. + * Blocking unless maxWait <= 0 + * Caller should run in a thread. + * For best results do a getPeers() first so we have a token. + * + * @param ih the Info Hash (torrent) + * @param nInfo the peer to announce to + * @param maxWait the maximum time to wait (ms) or 0 to return immediately. + * @return success + */ + public boolean announce(byte[] ih, NodeInfo nInfo, long maxWait) { + InfoHash iHash = new InfoHash(ih); + TokenKey tokenKey = new TokenKey(nInfo.getNID(), iHash); + Token token = _incomingTokens.get(tokenKey); + if (token == null) { + // we have no token, have to do a getPeers first to get a token + if (maxWait <= 0) + return false; + ReplyWaiter waiter = sendGetPeers(nInfo, iHash); + if (waiter == null) + return false; + long start = _context.clock().now(); + synchronized(waiter) { + try { + waiter.wait(maxWait); + } catch (InterruptedException ie) {} + } + int replyType = waiter.getReplyCode(); + if (!(replyType == REPLY_PEERS || replyType == REPLY_NODES)) + return false; + // we should have a token now + token = _incomingTokens.get(tokenKey); + if (token == null) + return false; + maxWait -= _context.clock().now() - start; + if (maxWait < 1000) + return false; + } + + // send and wait on rcv msg lock unless maxWait <= 0 + ReplyWaiter waiter = sendAnnouncePeer(nInfo, iHash, token); + if (waiter == null) + return false; + if (maxWait <= 0) + return true; + synchronized(waiter) { + try { + waiter.wait(maxWait); + } catch (InterruptedException ie) {} + } + int replyType = waiter.getReplyCode(); + return replyType == REPLY_PONG; + } + + /** + * Does nothing yet, everything is prestarted. + * Can't be restarted after stopping? + */ + public void start() { + // start the explore thread + } + + /** + * Does nothing yet. + */ + public void stop() { + // stop the explore thread + // unregister port listeners + // does not clear the DHT or tracker yet. + } + + /** + * Clears the tracker and DHT data. + * Call after saving DHT data to disk. + */ + public void clear() { + _tracker.stop(); + _knownNodes.clear(); + } + + ////////// All private below here ///////////////////////////////////// + + ///// Sending..... + + // Queries..... + // The first 3 queries use the query port. + // Announces use the response port. + + /** + * @param nInfo who to send it to + * @return null on error + */ + private ReplyWaiter sendPing(NodeInfo nInfo) { + Map map = new HashMap(); + map.put("q", "ping"); + Map args = new HashMap(); + map.put("a", args); + return sendQuery(nInfo, map, true); + } + + /** + * @param nInfo who to send it to + * @return null on error + */ + private ReplyWaiter sendFindNode(NodeInfo nInfo, NodeInfo tID) { + Map map = new HashMap(); + map.put("q", "find_node"); + Map args = new HashMap(); + args.put("target", tID.getData()); + map.put("a", args); + return sendQuery(nInfo, map, true); + } + + /** + * @param nInfo who to send it to + * @return null on error + */ + private ReplyWaiter sendGetPeers(NodeInfo nInfo, InfoHash ih) { + Map map = new HashMap(); + map.put("q", "get_peers"); + Map args = new HashMap(); + args.put("info_hash", ih.getData()); + map.put("a", args); + return sendQuery(nInfo, map, true); + } + + /** + * @param nInfo who to send it to + * @return null on error + */ + private ReplyWaiter sendAnnouncePeer(NodeInfo nInfo, InfoHash ih, Token token) { + Map map = new HashMap(); + map.put("q", "announce_peer"); + Map args = new HashMap(); + args.put("info_hash", ih.getData()); + // port ignored + args.put("port", Integer.valueOf(6881)); + args.put("token", token.getData()); + map.put("a", args); + // an announce need not be signed, we have a token + ReplyWaiter rv = sendQuery(nInfo, map, false); + // save the InfoHash so we can get it later + if (rv != null) + rv.setSentObject(ih); + return rv; + } + + // Responses..... + // All responses use the response port. + + /** + * @param nInfo who to send it to + * @return success + */ + private boolean sendPong(NodeInfo nInfo, MsgID msgID) { + Map map = new HashMap(); + Map resps = new HashMap(); + map.put("r", resps); + return sendResponse(nInfo, msgID, map); + } + + /** response to find_node (no token) */ + private boolean sendNodes(NodeInfo nInfo, MsgID msgID, byte[] ids) { + return sendNodes(nInfo, msgID, null, ids); + } + + /** + * response to find_node (token is null) or get_peers (has a token) + * @param nInfo who to send it to + * @return success + */ + private boolean sendNodes(NodeInfo nInfo, MsgID msgID, Token token, byte[] ids) { + Map map = new HashMap(); + Map resps = new HashMap(); + map.put("r", resps); + if (token != null) + resps.put("token", token.getData()); + resps.put("nodes", ids); + return sendResponse(nInfo, msgID, map); + } + + /** @param token non-null */ + private boolean sendPeers(NodeInfo nInfo, MsgID msgID, Token token, List peers) { + Map map = new HashMap(); + Map resps = new HashMap(); + map.put("r", resps); + resps.put("token", token.getData()); + resps.put("values", peers); + return sendResponse(nInfo, msgID, map); + } + + // All errors use the response port. + + /** + * @param nInfo who to send it to + * @return success + */ + private boolean sendError(NodeInfo nInfo, MsgID msgID, int err, String msg) { + Map map = new HashMap(); + Map resps = new HashMap(); + map.put("r", resps); + return sendResponse(nInfo, msgID, map); + } + + // Low-level send methods + + // TODO sendQuery with onReply / onTimeout args + + /** + * @param repliable true for all but announce + * @return null on error + */ + private ReplyWaiter sendQuery(NodeInfo nInfo, Map map, boolean repliable) { + if (nInfo.equals(_myNodeInfo)) + throw new IllegalArgumentException("wtf don't send to ourselves"); + if (_log.shouldLog(Log.INFO)) + _log.info("Sending query to: " + nInfo); + if (nInfo.getDestination() == null) { + NodeInfo newInfo = _knownNodes.get(nInfo.getNID()); + if (newInfo != null && newInfo.getDestination() != null) { + nInfo = newInfo; + } else { + // lookup b32? + if (_log.shouldLog(Log.WARN)) + _log.warn("No destination for: " + nInfo); + return null; + } + } + map.put("y", "q"); + MsgID mID = new MsgID(_context); + map.put("t", mID.getData()); + Map args = (Map) map.get("a"); + if (args == null) + throw new IllegalArgumentException("no args"); + args.put("id", _myNodeInfo.getData()); + int port = nInfo.getPort(); + if (!repliable) + port++; + boolean success = sendMessage(nInfo.getDestination(), port, map, true); + if (success) { + // save for the caller to get + ReplyWaiter rv = new ReplyWaiter(mID, nInfo, null, null); + _sentQueries.put(mID, rv); + return rv; + } + return null; + } + + /** + * @param toPort the query port, we will increment here + * @return success + */ + private boolean sendResponse(NodeInfo nInfo, MsgID msgID, Map map) { + if (nInfo.equals(_myNodeInfo)) + throw new IllegalArgumentException("wtf don't send to ourselves"); + if (_log.shouldLog(Log.INFO)) + _log.info("Sending response to: " + nInfo); + if (nInfo.getDestination() == null) { + NodeInfo newInfo = _knownNodes.get(nInfo.getNID()); + if (newInfo != null && newInfo.getDestination() != null) { + nInfo = newInfo; + } else { + // lookup b32? + if (_log.shouldLog(Log.WARN)) + _log.warn("No destination for: " + nInfo); + return false; + } + } + map.put("y", "r"); + map.put("t", msgID.getData()); + Map resps = (Map) map.get("r"); + if (resps == null) + throw new IllegalArgumentException("no resps"); + resps.put("id", _myNodeInfo.getData()); + return sendMessage(nInfo.getDestination(), nInfo.getPort() + 1, map, false); + } + + /** + * @param toPort the query port, we will increment here + * @return success + */ + private boolean sendError(NodeInfo nInfo, MsgID msgID, Map map) { + if (nInfo.equals(_myNodeInfo)) + throw new IllegalArgumentException("wtf don't send to ourselves"); + if (_log.shouldLog(Log.INFO)) + _log.info("Sending error to: " + nInfo); + if (nInfo.getDestination() == null) { + NodeInfo newInfo = _knownNodes.get(nInfo.getNID()); + if (newInfo != null && newInfo.getDestination() != null) { + nInfo = newInfo; + } else { + // lookup b32? + if (_log.shouldLog(Log.WARN)) + _log.warn("No destination for: " + nInfo); + return false; + } + } + map.put("y", "e"); + map.put("t", msgID.getData()); + return sendMessage(nInfo.getDestination(), nInfo.getPort() + 1, map, false); + } + + /** + * Lowest-level send message call. + * @param repliable true for all but announce + * @return success + */ + private boolean sendMessage(Destination dest, int toPort, Map map, boolean repliable) { + if (_session.isClosed()) { + // Don't allow DHT to open a closed session + if (_log.shouldLog(Log.WARN)) + _log.warn("Not sending message, session is closed"); + return false; + } + if (dest.calculateHash().equals(_myNodeInfo.getHash())) + throw new IllegalArgumentException("wtf don't send to ourselves"); + byte[] payload = BEncoder.bencode(map); + if (_log.shouldLog(Log.DEBUG)) { + ByteArrayInputStream bais = new ByteArrayInputStream(payload); + try { + _log.debug("Sending to: " + dest.calculateHash() + ' ' + BDecoder.bdecode(bais).toString()); + } catch (IOException ioe) {} + } + + // Always send query port, peer will increment for unsigned replies + int fromPort = _qPort; + if (repliable) { + I2PDatagramMaker dgMaker = new I2PDatagramMaker(_session); + payload = dgMaker.makeI2PDatagram(payload); + if (payload == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("WTF DGM fail"); + } + } + + try { + boolean success = _session.sendMessage(dest, payload, 0, payload.length, null, null, 60*1000, + I2PSession.PROTO_DATAGRAM, fromPort, toPort); + if (!success) { + if (_log.shouldLog(Log.WARN)) + _log.warn("WTF sendMessage fail"); + } + return success; + } catch (I2PSessionException ise) { + if (_log.shouldLog(Log.WARN)) + _log.warn("sendMessage fail", ise); + return false; + } + } + + ///// Reception..... + + /** + * @param from dest or null if it didn't come in on signed port + */ + private void receiveMessage(Destination from, int fromPort, byte[] payload) { + + try { + InputStream is = new ByteArrayInputStream(payload); + BDecoder dec = new BDecoder(is); + BEValue bev = dec.bdecodeMap(); + Map map = bev.getMap(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Got KRPC message " + bev.toString()); + + // Lazy here, just let missing Map entries throw NPEs, caught below + + byte[] msgIDBytes = map.get("t").getBytes(); + MsgID mID = new MsgID(msgIDBytes); + String type = map.get("y").getString(); + if (type.equals("q") && from != null) { + // queries must be repliable + String method = map.get("q").getString(); + Map args = map.get("a").getMap(); + receiveQuery(mID, from, fromPort, method, args); + } else if (type.equals("r") || type.equals("e")) { + // get dest from id->dest map + ReplyWaiter waiter = _sentQueries.remove(mID); + if (waiter != null) { + // TODO verify waiter NID and port? + if (type.equals("r")) { + Map response = map.get("r").getMap(); + receiveResponse(waiter, response); + } else { + List error = map.get("e").getList(); + receiveError(waiter, error); + } + } else { + if (_log.shouldLog(Log.WARN)) + _log.warn("Rcvd msg with no one waiting: " + bev.toString()); + } + } else { + if (_log.shouldLog(Log.WARN)) + _log.warn("Unknown msg type rcvd: " + bev.toString()); + throw new InvalidBEncodingException("Unknown type: " + type); + } + // success + /*** + } catch (InvalidBEncodingException e) { + } catch (IOException e) { + } catch (ArrayIndexOutOfBoundsException e) { + } catch (IllegalArgumentException e) { + } catch (ClassCastException e) { + } catch (NullPointerException e) { + ***/ + } catch (Exception e) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Receive error for message", e); + } + } + + + // Queries..... + + /** + * Adds sender to our DHT. + * @param dest non-null + * @throws NPE too + */ + private void receiveQuery(MsgID msgID, Destination dest, int fromPort, String method, Map args) throws InvalidBEncodingException { + byte[] nid = args.get("id").getBytes(); + NodeInfo nInfo = new NodeInfo(nid); + nInfo = heardFrom(nInfo); + nInfo.setDestination(dest); +// ninfo.checkport ? + + if (method.equals("ping")) { + receivePing(msgID, nInfo); + } else if (method.equals("find_node")) { + byte[] tid = args.get("target").getBytes(); + NodeInfo tID = new NodeInfo(tid); + receiveFindNode(msgID, nInfo, tID); + } else if (method.equals("get_peers")) { + byte[] hash = args.get("info_hash").getBytes(); + InfoHash ih = new InfoHash(hash); + receiveGetPeers(msgID, nInfo, ih); + } else if (method.equals("announce_peer")) { + byte[] hash = args.get("info_hash").getBytes(); + InfoHash ih = new InfoHash(hash); + // this is the "TCP" port, we don't care + //int port = args.get("port").getInt(); + byte[] token = args.get("token").getBytes(); + receiveAnnouncePeer(msgID, nInfo, ih, token); + } else { + if (_log.shouldLog(Log.WARN)) + _log.warn("Unknown query method rcvd: " + method); + } + } + + /** + * Called for a request or response + * @return old NodeInfo or nInfo if none, use this to reduce object churn + */ + private NodeInfo heardFrom(NodeInfo nInfo) { + return heardFrom(nInfo, _context.clock().now()); + } + + /** + * Used for initialization + * @return old NodeInfo or nInfo if none, use this to reduce object churn + */ + private NodeInfo heardFrom(NodeInfo nInfo, long when) { + // try to keep ourselves out of the DHT + if (nInfo.equals(_myNodeInfo)) + return _myNodeInfo; + NID nID = nInfo.getNID(); + NodeInfo oldInfo = _knownNodes.get(nID); + if (oldInfo == null) { + if (_log.shouldLog(Log.INFO)) + _log.info("Adding node: " + nInfo); + oldInfo = nInfo; + NodeInfo nInfo2 = _knownNodes.putIfAbsent(nID, nInfo); + if (nInfo2 != null) + oldInfo = nInfo2; + } + if (when > oldInfo.lastSeen()) + oldInfo.setLastSeen(when); + return oldInfo; + } + + /** + * Handle and respond to the query + */ + private void receivePing(MsgID msgID, NodeInfo nInfo) throws InvalidBEncodingException { + if (_log.shouldLog(Log.INFO)) + _log.info("Rcvd ping from: " + nInfo); + sendPong(nInfo, msgID); + } + + /** + * Handle and respond to the query + */ + private void receiveFindNode(MsgID msgID, NodeInfo nInfo, NodeInfo tID) throws InvalidBEncodingException { + if (_log.shouldLog(Log.INFO)) + _log.info("Rcvd find_node from: " + nInfo + " for: " + tID); + NodeInfo peer = _knownNodes.get(tID); + if (peer != null) { + // success, one answer + sendNodes(nInfo, msgID, peer.getData()); + } else { + // get closest from DHT + List nodes = _knownNodes.findClosest(tID.getNID(), K); + nodes.remove(nInfo); // him + nodes.remove(_myNodeInfo); // me + byte[] nodeArray = new byte[nodes.size() * NodeInfo.LENGTH]; + for (int i = 0; i < nodes.size(); i ++) { + System.arraycopy(nodes.get(i).getData(), 0, nodeArray, i * NodeInfo.LENGTH, NodeInfo.LENGTH); + } + sendNodes(nInfo, msgID, nodeArray); + } + } + + /** + * Handle and respond to the query + */ + private void receiveGetPeers(MsgID msgID, NodeInfo nInfo, InfoHash ih) throws InvalidBEncodingException { + if (_log.shouldLog(Log.INFO)) + _log.info("Rcvd get_peers from: " + nInfo + " for: " + ih); + // generate and save random token + Token token = new Token(_context); + _outgoingTokens.put(ih, token); + + List peers = _tracker.getPeers(ih, MAX_WANT); + if (peers.isEmpty()) { + // similar to find node, but with token + // get closest from DHT + List nodes = _knownNodes.findClosest(ih, K); + nodes.remove(nInfo); // him + nodes.remove(_myNodeInfo); // me + byte[] nodeArray = new byte[nodes.size() * NodeInfo.LENGTH]; + for (int i = 0; i < nodes.size(); i ++) { + System.arraycopy(nodes.get(i).getData(), 0, nodeArray, i * NodeInfo.LENGTH, NodeInfo.LENGTH); + } + sendNodes(nInfo, msgID, token, nodeArray); + } else { + List hashes = new ArrayList(peers.size()); + Hash him = nInfo.getHash(); + for (Hash peer : peers) { + if (!peer.equals(him)) + hashes.add(peer.getData()); + } + sendPeers(nInfo, msgID, token, hashes); + } + } + + /** + * Handle and respond to the query + */ + private void receiveAnnouncePeer(MsgID msgID, NodeInfo nInfo, InfoHash ih, byte[] token) throws InvalidBEncodingException { + if (_log.shouldLog(Log.INFO)) + _log.info("Rcvd announce from: " + nInfo + " for: " + ih); + // check token + // get desthash from token->dest map + Token oldToken = _outgoingTokens.get(ih); + if (oldToken == null || !DataHelper.eq(oldToken.getData(), token)) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Bad token"); + return; + } + + //msg ID -> NodeInfo -> Dest -> Hash + //verify with token -> nid or dest or hash ???? + + _tracker.announce(ih, nInfo.getHash()); + // the reply for an announce is the same as the reply for a ping + sendPong(nInfo, msgID); + } + + // Responses..... + + /** + * Handle the response and alert whoever sent the query it is responding to. + * Adds sender nodeinfo to our DHT. + * @throws NPE, IllegalArgumentException, and others too + */ + private void receiveResponse(ReplyWaiter waiter, Map response) throws InvalidBEncodingException { + NodeInfo nInfo = waiter; + + BEValue nodes = response.get("nodes"); + BEValue values = response.get("values"); + + // token handling - save it for later announces + if (nodes != null || values != null) { + BEValue btok = response.get("token"); + InfoHash ih = (InfoHash) waiter.getSentObject(); + if (btok != null && ih != null) { + byte[] tok = btok.getBytes(); + _incomingTokens.put(new TokenKey(nInfo.getNID(), ih), new Token(_context, tok)); + if (_log.shouldLog(Log.INFO)) + _log.info("Got token, must be a response to get_peers"); + } else { + if (_log.shouldLog(Log.INFO)) + _log.info("No token and saved infohash, must be a response to find_node"); + } + } + + // now do the right thing + if (nodes != null) { + // find node or get peers response - concatenated NodeInfos + byte[] ids = nodes.getBytes(); + List rlist = receiveNodes(nInfo, ids); + waiter.gotReply(REPLY_NODES, rlist); + } else if (values != null) { + // get peers response - list of Hashes + List peers = values.getList(); + List rlist = receivePeers(nInfo, peers); + waiter.gotReply(REPLY_PEERS, rlist); + } else { + // a ping response or an announce peer response + receivePong(nInfo); + waiter.gotReply(REPLY_PONG, null); + } + } + + /** + * rcv concatenated 54 byte NodeInfos, return as a List + * Adds all received nodeinfos to our DHT. + * @throws NPE, IllegalArgumentException, and others too + */ + private List receiveNodes(NodeInfo nInfo, byte[] ids) throws InvalidBEncodingException { + List rv = new ArrayList(ids.length / NodeInfo.LENGTH); + long fakeTime = _context.clock().now() - (MAX_NODEINFO_AGE * 3 / 4); + for (int off = 0; off < ids.length; off += NodeInfo.LENGTH) { + NodeInfo nInf = new NodeInfo(ids, off); + // anti-churn + // TODO do we need heardAbout too? + nInf = heardFrom(nInf, fakeTime); + rv.add(nInf); + } + if (_log.shouldLog(Log.INFO)) + _log.info("Rcvd nodes from: " + nInfo + ": " + DataHelper.toString(rv)); + return rv; + } + + /** + * rcv 32 byte Hashes, return as a List + * @throws NPE, IllegalArgumentException, and others too + */ + private List receivePeers(NodeInfo nInfo, List peers) throws InvalidBEncodingException { + if (_log.shouldLog(Log.INFO)) + _log.info("Rcvd peers from: " + nInfo); + List rv = new ArrayList(peers.size()); + for (BEValue bev : peers) { + byte[] b = bev.getBytes(); + Hash h = new Hash(b); + rv.add(h); + } + if (_log.shouldLog(Log.INFO)) + _log.info("Rcvd peers from: " + nInfo + ": " + DataHelper.toString(rv)); + return rv; + } + + /** does nothing, but node was already added to our DHT */ + private void receivePong(NodeInfo nInfo) { + if (_log.shouldLog(Log.INFO)) + _log.info("Rcvd pong from: " + nInfo); + } + + // Errors..... + + /** + * @throws NPE, and others too + */ + private void receiveError(ReplyWaiter waiter, List error) throws InvalidBEncodingException { + int errorCode = error.get(0).getInt(); + String errorString = error.get(1).getString(); + if (_log.shouldLog(Log.WARN)) + _log.warn("Rcvd error from: " + waiter + + " num: " + errorCode + + " msg: " + errorString); + // this calls heardFrom() + waiter.gotReply(errorCode, errorString); + } + + /** + * Callback for replies + */ + private class ReplyWaiter extends NodeInfo { + private final MsgID mid; + private final Runnable onReply; + private final Runnable onTimeout; + private final SimpleTimer2.TimedEvent event; + private int replyCode; + private Object sentObject; + private Object replyObject; + + /** + * Either wait on this object with a timeout, or use non-null Runnables. + * Any sent data to be rememberd may be stored by setSentObject(). + * Reply object may be in getReplyObject(). + * @param onReply must be fast, otherwise set to null and wait on this + * @param onTimeout must be fast, otherwise set to null and wait on this + */ + public ReplyWaiter(MsgID mID, NodeInfo nInfo, Runnable onReply, Runnable onTimeout) { + super(nInfo.getData()); + this.mid = mID; + this.onReply = onReply; + this.onTimeout = onTimeout; + if (onTimeout != null) + this.event = new Event(); + else + this.event = null; + } + + /** only used for announce, to save the Info Hash */ + public void setSentObject(Object o) { + sentObject = o; + } + + /** @return that stored with setSentObject() */ + public Object getSentObject() { + return sentObject; + } + + + /** + * Should contain null if getReplyCode is REPLY_PONG. + * Should contain List if getReplyCode is REPLY_PEERS. + * Should contain List if getReplyCode is REPLY_NODES. + * Should contain String if getReplyCode is > 200. + * @return may be null depending on what happened. Cast to expected type. + */ + public Object getReplyObject() { + return replyObject; + } + + /** + * If nonzero, we got a reply, and getReplyObject() may contain something. + * @return code or 0 if no error + */ + public int getReplyCode() { + return replyCode; + } + + /** + * Will notify this and run onReply. + * Also removes from _sentQueries and calls heardFrom(). + */ + public void gotReply(int code, Object o) { + replyCode = code; + replyObject = o; + if (event != null) + event.cancel(); + _sentQueries.remove(mid); + heardFrom(this); + if (onReply != null) + onReply.run(); + synchronized(this) { + this.notifyAll(); + } + } + + private class Event extends SimpleTimer2.TimedEvent { + public Event() { + super(SimpleTimer2.getInstance(), DEFAULT_QUERY_TIMEOUT); + } + + public void timeReached() { + _sentQueries.remove(mid); + if (onTimeout != null) + onTimeout.run(); + if (_log.shouldLog(Log.INFO)) + _log.warn("timeout waiting for reply from " + this.toString()); + } + } + } + + // I2PSessionMuxedListener interface ---------------- + + /** + * Instruct the client that the given session has received a message + * + * Will be called only if you register via addMuxedSessionListener(). + * Will be called only for the proto(s) and toPort(s) you register for. + * + * @param session session to notify + * @param msgId message number available + * @param size size of the message - why it's a long and not an int is a mystery + * @param proto 1-254 or 0 for unspecified + * @param fromPort 1-65535 or 0 for unspecified + * @param toPort 1-65535 or 0 for unspecified + */ + public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromPort, int toPort) { + try { + byte[] payload = session.receiveMessage(msgId); + if (toPort == _qPort) { + // repliable + I2PDatagramDissector dgDiss = new I2PDatagramDissector(); + dgDiss.loadI2PDatagram(payload); + payload = dgDiss.getPayload(); + Destination from = dgDiss.getSender(); + receiveMessage(from, fromPort, payload); + } else if (toPort == _rPort) { + // raw + receiveMessage(null, fromPort, payload); + } else { + if (_log.shouldLog(Log.WARN)) + _log.warn("msg on bad port"); + } + } catch (DataFormatException e) { + if (_log.shouldLog(Log.WARN)) + _log.warn("bad msg"); + } catch (I2PInvalidDatagramException e) { + if (_log.shouldLog(Log.WARN)) + _log.warn("bad msg"); + } catch (I2PSessionException e) { + if (_log.shouldLog(Log.WARN)) + _log.warn("bad msg"); + } + } + + /** for non-muxed */ + public void messageAvailable(I2PSession session, int msgId, long size) {} + + public void reportAbuse(I2PSession session, int severity) {} + + public void disconnected(I2PSession session) { + if (_log.shouldLog(Log.WARN)) + _log.warn("KRPC disconnected"); + } + + public void errorOccurred(I2PSession session, String message, Throwable error) { + if (_log.shouldLog(Log.WARN)) + _log.warn("KRPC got error msg: ", error); + } + + /** + * Cleaner-upper + */ + private class Cleaner implements SimpleTimer.TimedEvent { + + public void timeReached() { + long now = _context.clock().now(); + for (Iterator iter = _outgoingTokens.values().iterator(); iter.hasNext(); ) { + Token tok = iter.next(); + if (tok.lastSeen() < now - MAX_TOKEN_AGE) + iter.remove(); + } + for (Iterator iter = _incomingTokens.values().iterator(); iter.hasNext(); ) { + Token tok = iter.next(); + if (tok.lastSeen() < now - MAX_TOKEN_AGE) + iter.remove(); + } + // TODO sent queries? + for (Iterator iter = _knownNodes.values().iterator(); iter.hasNext(); ) { + NodeInfo ni = iter.next(); + if (ni.lastSeen() < now - MAX_NODEINFO_AGE) + iter.remove(); + } + if (_log.shouldLog(Log.INFO)) + _log.info("KRPC cleaner done, now with " + + _outgoingTokens.size() + " sent Tokens, " + + _incomingTokens.size() + " rcvd Tokens, " + + _knownNodes.size() + " known peers, " + + _sentQueries.size() + " queries awaiting response"); + } + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/MsgID.java b/apps/i2psnark/java/src/org/klomp/snark/dht/MsgID.java new file mode 100644 index 000000000..94b37a690 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/MsgID.java @@ -0,0 +1,32 @@ +package org.klomp.snark.dht; +/* + * GPLv2 + */ + +import net.i2p.I2PAppContext; +import net.i2p.data.ByteArray; + +/** + * Used for both incoming and outgoing message IDs + * + * @since 0.8.4 + * @author zzz + */ +public class MsgID extends ByteArray { + + private static final int MY_TOK_LEN = 8; + + /** outgoing - generate a random ID */ + public MsgID(I2PAppContext ctx) { + super(null); + byte[] data = new byte[MY_TOK_LEN]; + ctx.random().nextBytes(data); + setData(data); + setValid(MY_TOK_LEN); + } + + /** incoming - save the ID (arbitrary length) */ + public MsgID(byte[] data) { + super(data); + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/NID.java b/apps/i2psnark/java/src/org/klomp/snark/dht/NID.java new file mode 100644 index 000000000..3d0d0a496 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/NID.java @@ -0,0 +1,19 @@ +package org.klomp.snark.dht; +/* + * From zzzot, modded and relicensed to GPLv2 + */ + +import net.i2p.crypto.SHA1Hash; + +/** + * A 20-byte peer ID, used as a Map key in lots of places. + * + * @since 0.8.4 + * @author zzz + */ +public class NID extends SHA1Hash { + + public NID(byte[] data) { + super(data); + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java new file mode 100644 index 000000000..9e73ca03b --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java @@ -0,0 +1,181 @@ +package org.klomp.snark.dht; +/* + * From zzzot, modded and relicensed to GPLv2 + */ + +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.SimpleDataStructure; + +/* + * A Node ID, Hash, and port, and an optional Destination. + * This is what DHTNodes remembers. The DHT tracker just stores Hashes. + * getData() returns the 54 byte compact info (NID, Hash, port). + * + * Things are a little tricky in KRPC since we exchange Hashes and don't + * always have the Destination. + * + * @since 0.8.4 + * @author zzz + */ + +public class NodeInfo extends SimpleDataStructure { + + private long lastSeen; + private NID nID; + private Hash hash; + private Destination dest; + private int port; + + public static final int LENGTH = NID.HASH_LENGTH + Hash.HASH_LENGTH + 2; + + /** + * Use this if we have the full destination + * @throws IllegalArgumentException + */ + public NodeInfo(NID nID, Destination dest, int port) { + super(); + this.nID = nID; + this.dest = dest; + this.hash = dest.calculateHash(); + this.port = port; + initialize(nID, this.hash, port); + } + + /** + * No Destination yet available + * @deprecated unused + * @throws IllegalArgumentException + */ + public NodeInfo(NID nID, Hash hash, int port) { + super(); + this.nID = nID; + this.hash = hash; + this.port = port; + initialize(nID, hash, port); + } + + /** + * No Destination yet available + * @param compactInfo 20 byte node ID, 32 byte destHash, 2 byte port + * @throws IllegalArgumentException + */ + public NodeInfo(byte[] compactInfo) { + super(compactInfo); + initialize(compactInfo); + } + + /** + * No Destination yet available + * @param compactInfo 20 byte node ID, 32 byte destHash, 2 byte port + * @param offset starting at this offset in compactInfo + * @throws IllegalArgumentException + * @throws AIOOBE + */ + public NodeInfo(byte[] compactInfo, int offset) { + super(); + byte[] d = new byte[LENGTH]; + System.arraycopy(compactInfo, offset, d, 0, LENGTH); + setData(d); + initialize(d); + } + + /** + * Creates data structures from the compact info + * @throws IllegalArgumentException + */ + private void initialize(byte[] compactInfo) { + if (compactInfo.length != LENGTH) + throw new IllegalArgumentException("Bad compact info length"); + byte[] ndata = new byte[NID.HASH_LENGTH]; + System.arraycopy(compactInfo, 0, ndata, 0, NID.HASH_LENGTH); + this.nID = new NID(ndata); + byte[] hdata = new byte[Hash.HASH_LENGTH]; + System.arraycopy(compactInfo, NID.HASH_LENGTH, hdata, 0, Hash.HASH_LENGTH); + this.hash = new Hash(hdata); + this.port = (int) DataHelper.fromLong(compactInfo, NID.HASH_LENGTH + Hash.HASH_LENGTH, 2); + } + + /** + * Creates 54-byte compact info + * @throws IllegalArgumentException + */ + private void initialize(NID nID, Hash hash, int port) { + if (port < 0 || port > 65535) + throw new IllegalArgumentException("Bad port"); + byte[] compactInfo = new byte[LENGTH]; + System.arraycopy(nID.getData(), 0, compactInfo, 0, NID.HASH_LENGTH); + System.arraycopy(hash.getData(), 0, compactInfo, NID.HASH_LENGTH, Hash.HASH_LENGTH); + DataHelper.toLong(compactInfo, NID.HASH_LENGTH + Hash.HASH_LENGTH, 2, port); + setData(compactInfo); + } + + public int length() { + return LENGTH; + } + + public NID getNID() { + return this.nID; + } + + /** @return may be null if we don't have it */ + public Destination getDestination() { + return this.dest; + } + + public Hash getHash() { + return this.hash; + } + + @Override + public Hash calculateHash() { + return this.hash; + } + + /** + * This can come in later but the hash must match. + * @throws IllegalArgumentException if hash of dest doesn't match previous hash + */ + public void setDestination(Destination dest) throws IllegalArgumentException { + if (!dest.calculateHash().equals(this.hash)) + throw new IllegalArgumentException("Hash mismatch, was: " + this.hash + " new: " + dest.calculateHash()); + if (this.dest == null) + this.dest = dest; + else if (!this.dest.equals(dest)) + throw new IllegalArgumentException("Dest mismatch, was: " + this.dest+ " new: " + dest); + // else keep the old to reduce object churn + } + + public int getPort() { + return this.port; + } + + public long lastSeen() { + return lastSeen; + } + + public void setLastSeen(long now) { + lastSeen = now; + } + + @Override + public int hashCode() { + return super.hashCode() ^ nID.hashCode() ^ port; + } + + @Override + public boolean equals(Object o) { + try { + NodeInfo ni = (NodeInfo) o; + // assume dest matches, ignore it + return this.hash.equals(ni.hash) && nID.equals(ni.nID) && port == ni.port; + } catch (Exception e) { + return false; + } + } + + public String toString() { + return "NodeInfo: " + nID + ' ' + hash + " port: " + port; + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfoComparator.java b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfoComparator.java new file mode 100644 index 000000000..66eb57bba --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfoComparator.java @@ -0,0 +1,31 @@ +package org.klomp.snark.dht; +/* + * From zzzot, modded and relicensed to GPLv2 + */ + +import java.util.Comparator; + +import net.i2p.crypto.SHA1Hash; +import net.i2p.data.DataHelper; + +/** + * Closest to a InfoHash or NID key. + * Use for NodeInfos. + * + * @since 0.8.4 + * @author zzz + */ +class NodeInfoComparator implements Comparator { + private final SHA1Hash _base; + + public NodeInfoComparator(SHA1Hash h) { + _base = h; + } + + public int compare(NodeInfo lhs, NodeInfo rhs) { + byte lhsDelta[] = DataHelper.xor(lhs.getNID().getData(), _base.getData()); + byte rhsDelta[] = DataHelper.xor(rhs.getNID().getData(), _base.getData()); + return DataHelper.compareTo(lhsDelta, rhsDelta); + } + +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/Peer.java b/apps/i2psnark/java/src/org/klomp/snark/dht/Peer.java new file mode 100644 index 000000000..8943c06b5 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/Peer.java @@ -0,0 +1,30 @@ +package org.klomp.snark.dht; +/* + * From zzzot, modded and relicensed to GPLv2 + */ + +import net.i2p.data.Hash; + +/** + * A single peer for a single torrent. + * This is what the DHT tracker remembers. + * + * @since 0.8.4 + * @author zzz + */ +public class Peer extends Hash { + + private long lastSeen; + + public Peer(byte[] data) { + super(data); + } + + public long lastSeen() { + return lastSeen; + } + + public void setLastSeen(long now) { + lastSeen = now; + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/Peers.java b/apps/i2psnark/java/src/org/klomp/snark/dht/Peers.java new file mode 100644 index 000000000..2a2452e95 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/Peers.java @@ -0,0 +1,21 @@ +package org.klomp.snark.dht; +/* + * From zzzot, modded and relicensed to GPLv2 + */ + +import java.util.concurrent.ConcurrentHashMap; + +import net.i2p.data.Hash; + +/** + * All the peers for a single torrent + * + * @since 0.8.4 + * @author zzz + */ +public class Peers extends ConcurrentHashMap { + + public Peers() { + super(); + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/SHA1Comparator.java b/apps/i2psnark/java/src/org/klomp/snark/dht/SHA1Comparator.java new file mode 100644 index 000000000..36c355e49 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/SHA1Comparator.java @@ -0,0 +1,31 @@ +package org.klomp.snark.dht; +/* + * From zzzot, modded and relicensed to GPLv2 + */ + +import java.util.Comparator; + +import net.i2p.crypto.SHA1Hash; +import net.i2p.data.DataHelper; + +/** + * Closest to a InfoHash or NID key. + * Use for InfoHashes and NIDs. + * + * @since 0.8.4 + * @author zzz + */ +class SHA1Comparator implements Comparator { + private final byte[] _base; + + public SHA1Comparator(SHA1Hash h) { + _base = h.getData(); + } + + public int compare(SHA1Hash lhs, SHA1Hash rhs) { + byte lhsDelta[] = DataHelper.xor(lhs.getData(), _base); + byte rhsDelta[] = DataHelper.xor(rhs.getData(), _base); + return DataHelper.compareTo(lhsDelta, rhsDelta); + } + +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/Token.java b/apps/i2psnark/java/src/org/klomp/snark/dht/Token.java new file mode 100644 index 000000000..c8859155d --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/Token.java @@ -0,0 +1,39 @@ +package org.klomp.snark.dht; +/* + * GPLv2 + */ + +import net.i2p.I2PAppContext; +import net.i2p.data.ByteArray; +import net.i2p.data.DataHelper; + +/** + * Used for Both outgoing and incoming tokens + * + * @since 0.8.4 + * @author zzz + */ +public class Token extends ByteArray { + + private static final int MY_TOK_LEN = 8; + private final long lastSeen; + + /** outgoing - generate a random token */ + public Token(I2PAppContext ctx) { + super(null); + byte[] data = new byte[MY_TOK_LEN]; + ctx.random().nextBytes(data); + setData(data); + lastSeen = ctx.clock().now(); + } + + /** incoming - save the token (arbitrary length) */ + public Token(I2PAppContext ctx, byte[] data) { + super(data); + lastSeen = ctx.clock().now(); + } + + public long lastSeen() { + return lastSeen; + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/TokenKey.java b/apps/i2psnark/java/src/org/klomp/snark/dht/TokenKey.java new file mode 100644 index 000000000..d2217a015 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/TokenKey.java @@ -0,0 +1,20 @@ +package org.klomp.snark.dht; +/* + * GPLv2 + */ + +import net.i2p.crypto.SHA1Hash; +import net.i2p.data.DataHelper; + +/** + * Used to index incoming Tokens + * + * @since 0.8.4 + * @author zzz + */ +public class TokenKey extends SHA1Hash { + + public TokenKey(NID nID, InfoHash ih) { + super(DataHelper.xor(nID.getData(), ih.getData())); + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/Torrents.java b/apps/i2psnark/java/src/org/klomp/snark/dht/Torrents.java new file mode 100644 index 000000000..c791e7077 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/Torrents.java @@ -0,0 +1,19 @@ +package org.klomp.snark.dht; +/* + * From zzzot, relicensed to GPLv2 + */ + +import java.util.concurrent.ConcurrentHashMap; + +/** + * All the torrents + * + * @since 0.8.4 + * @author zzz + */ +public class Torrents extends ConcurrentHashMap { + + public Torrents() { + super(); + } +} From 7b07eb89a36d9ae8aaa9bc84865b14662607d617 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 2 Jun 2012 18:52:46 +0000 Subject: [PATCH 02/23] - Uncomment DHT - Change DHT from option bit to extension message - Add DHT start/stop code - Add UI for DHT enabling - Add raw datagram protocol type and use for response port --- .../src/org/klomp/snark/ExtensionHandler.java | 57 ++++++++++++++++++- .../src/org/klomp/snark/I2PSnarkUtil.java | 32 +++++++++-- .../java/src/org/klomp/snark/Peer.java | 18 +++--- .../src/org/klomp/snark/PeerCoordinator.java | 27 ++++++++- .../src/org/klomp/snark/PeerListener.java | 7 ++- .../java/src/org/klomp/snark/PeerState.java | 8 ++- .../src/org/klomp/snark/SnarkManager.java | 15 ++++- .../java/src/org/klomp/snark/dht/DHT.java | 12 +++- .../java/src/org/klomp/snark/dht/KRPC.java | 24 ++++++-- .../org/klomp/snark/web/I2PSnarkServlet.java | 12 +++- core/java/src/net/i2p/client/I2PSession.java | 11 ++++ 11 files changed, 195 insertions(+), 28 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/ExtensionHandler.java b/apps/i2psnark/java/src/org/klomp/snark/ExtensionHandler.java index e67466f4a..abde95e80 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/ExtensionHandler.java +++ b/apps/i2psnark/java/src/org/klomp/snark/ExtensionHandler.java @@ -28,6 +28,9 @@ abstract class ExtensionHandler { public static final int ID_PEX = 2; /** not ut_pex since the compact format is different */ public static final String TYPE_PEX = "i2p_pex"; + public static final int ID_DHT = 3; + /** not using the option bit since the compact format is different */ + public static final String TYPE_DHT = "i2p_dht"; /** Pieces * SHA1 Hash length, + 25% extra for file names, benconding overhead, etc */ private static final int MAX_METADATA_SIZE = Storage.MAX_PIECES * 20 * 5 / 4; private static final int PARALLEL_REQUESTS = 3; @@ -36,9 +39,10 @@ abstract class ExtensionHandler { /** * @param metasize -1 if unknown * @param pexAndMetadata advertise these capabilities + * @param dht advertise DHT capability * @return bencoded outgoing handshake message */ - public static byte[] getHandshake(int metasize, boolean pexAndMetadata) { + public static byte[] getHandshake(int metasize, boolean pexAndMetadata, boolean dht) { Map handshake = new HashMap(); Map m = new HashMap(); if (pexAndMetadata) { @@ -47,6 +51,9 @@ abstract class ExtensionHandler { if (metasize >= 0) handshake.put("metadata_size", Integer.valueOf(metasize)); } + if (dht) { + m.put(TYPE_DHT, Integer.valueOf(ID_DHT)); + } // include the map even if empty so the far-end doesn't NPE handshake.put("m", m); handshake.put("p", Integer.valueOf(6881)); @@ -65,6 +72,8 @@ abstract class ExtensionHandler { handleMetadata(peer, listener, bs, log); else if (id == ID_PEX) handlePEX(peer, listener, bs, log); + else if (id == ID_DHT) + handleDHT(peer, listener, bs, log); else if (log.shouldLog(Log.INFO)) log.info("Unknown extension msg " + id + " from " + peer); } @@ -87,6 +96,12 @@ abstract class ExtensionHandler { // peer state calls peer listener calls sendPEX() } + if (msgmap.get(TYPE_DHT) != null) { + if (log.shouldLog(Log.DEBUG)) + log.debug("Peer supports DHT extension: " + peer); + // peer state calls peer listener calls sendDHT() + } + MagnetState state = peer.getMagnetState(); if (msgmap.get(TYPE_METADATA) == null) { @@ -332,6 +347,28 @@ abstract class ExtensionHandler { } } + /** + * Receive the DHT port numbers + * @since DHT + */ + private static void handleDHT(Peer peer, PeerListener listener, byte[] bs, Log log) { + if (log.shouldLog(Log.DEBUG)) + log.debug("Got DHT msg from " + peer); + try { + InputStream is = new ByteArrayInputStream(bs); + BDecoder dec = new BDecoder(is); + BEValue bev = dec.bdecodeMap(); + Map map = bev.getMap(); + int qport = map.get("port").getInt(); + int rport = map.get("rport").getInt(); + listener.gotPort(peer, qport, rport); + } catch (Exception e) { + if (log.shouldLog(Log.INFO)) + log.info("DHT msg exception from " + peer, e); + //peer.disconnect(false); + } + } + /** * added.f and dropped unsupported * @param pList non-null @@ -359,4 +396,22 @@ abstract class ExtensionHandler { } } + /** + * Send the DHT port numbers + * @since DHT + */ + public static void sendDHT(Peer peer, int qport, int rport) { + Map map = new HashMap(); + map.put("port", Integer.valueOf(qport)); + map.put("rport", Integer.valueOf(rport)); + byte[] payload = BEncoder.bencode(map); + try { + int hisMsgCode = peer.getHandshakeMap().get("m").getMap().get(TYPE_DHT).getInt(); + peer.sendExtension(hisMsgCode, payload); + } catch (Exception e) { + // NPE, no DHT caps + //if (log.shouldLog(Log.INFO)) + // log.info("DHT msg exception to " + peer, e); + } + } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java index 320b5005a..dca9fbaf2 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java +++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java @@ -36,7 +36,7 @@ import net.i2p.util.SimpleTimer; import net.i2p.util.Translate; import org.klomp.snark.dht.DHT; -//import org.klomp.snark.dht.KRPC; +import org.klomp.snark.dht.KRPC; /** * I2P specific helpers for I2PSnark @@ -63,6 +63,7 @@ public class I2PSnarkUtil { private final File _tmpDir; private int _startupDelay; private boolean _shouldUseOT; + private boolean _shouldUseDHT; private boolean _areFilesPublic; private String _openTrackerString; private DHT _dht; @@ -73,7 +74,7 @@ public class I2PSnarkUtil { public static final int DEFAULT_MAX_UP_BW = 8; //KBps public static final int MAX_CONNECTIONS = 16; // per torrent public static final String PROP_MAX_BW = "i2cp.outboundBytesPerSecond"; - //private static final boolean ENABLE_DHT = true; + public static final boolean DEFAULT_USE_DHT = true; public I2PSnarkUtil(I2PAppContext ctx) { _context = ctx; @@ -88,6 +89,7 @@ public class I2PSnarkUtil { _maxConnections = MAX_CONNECTIONS; _startupDelay = DEFAULT_STARTUP_DELAY; _shouldUseOT = DEFAULT_USE_OPENTRACKERS; + _shouldUseDHT = DEFAULT_USE_DHT; // This is used for both announce replies and .torrent file downloads, // so it must be available even if not connected to I2CP. // so much for multiple instances @@ -234,8 +236,8 @@ public class I2PSnarkUtil { _manager = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, opts); } // FIXME this only instantiates krpc once, left stuck with old manager - //if (ENABLE_DHT && _manager != null && _dht == null) - // _dht = new KRPC(_context, _manager.getSession()); + if (_shouldUseDHT && _manager != null && _dht == null) + _dht = new KRPC(_context, _manager.getSession()); return (_manager != null); } @@ -250,7 +252,11 @@ public class I2PSnarkUtil { /** * Destroy the destination itself */ - public void disconnect() { + public synchronized void disconnect() { + if (_dht != null) { + _dht.stop(); + _dht = null; + } I2PSocketManager mgr = _manager; // FIXME this can cause race NPEs elsewhere _manager = null; @@ -490,6 +496,22 @@ public class I2PSnarkUtil { public boolean shouldUseOpenTrackers() { return _shouldUseOT; } + + /** @since DHT */ + public synchronized void setUseDHT(boolean yes) { + _shouldUseDHT = yes; + if (yes && _manager != null && _dht == null) { + _dht = new KRPC(_context, _manager.getSession()); + } else if (!yes && _dht != null) { + _dht.stop(); + _dht = null; + } + } + + /** @since DHT */ + public boolean shouldUseDHT() { + return _shouldUseDHT; + } /** * Like DataHelper.toHexString but ensures no loss of leading zero bytes diff --git a/apps/i2psnark/java/src/org/klomp/snark/Peer.java b/apps/i2psnark/java/src/org/klomp/snark/Peer.java index 02fb63560..8f33c01a7 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Peer.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Peer.java @@ -80,7 +80,9 @@ public class Peer implements Comparable static final long OPTION_FAST = 0x0000000000000004l; static final long OPTION_DHT = 0x0000000000000001l; /** we use a different bit since the compact format is different */ +/* no, let's use an extension message static final long OPTION_I2P_DHT = 0x0000000040000000l; +*/ static final long OPTION_AZMP = 0x1000000000000000l; private long options; @@ -269,15 +271,17 @@ public class Peer implements Comparable _log.debug("Peer supports extensions, sending reply message"); int metasize = metainfo != null ? metainfo.getInfoBytes().length : -1; boolean pexAndMetadata = metainfo == null || !metainfo.isPrivate(); - out.sendExtension(0, ExtensionHandler.getHandshake(metasize, pexAndMetadata)); + boolean dht = util.getDHT() != null; + out.sendExtension(0, ExtensionHandler.getHandshake(metasize, pexAndMetadata, dht)); } - if ((options & OPTION_I2P_DHT) != 0 && util.getDHT() != null) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Peer supports DHT, sending PORT message"); - int port = util.getDHT().getPort(); - out.sendPort(port); - } + // Old DHT PORT message + //if ((options & OPTION_I2P_DHT) != 0 && util.getDHT() != null) { + // if (_log.shouldLog(Log.DEBUG)) + // _log.debug("Peer supports DHT, sending PORT message"); + // int port = util.getDHT().getPort(); + // out.sendPort(port); + //} // Send our bitmap if (bitfield != null) diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java index 45b6ef82a..845b98904 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java @@ -1273,6 +1273,7 @@ class PeerCoordinator implements PeerListener } } else if (id == ExtensionHandler.ID_HANDSHAKE) { sendPeers(peer); + sendDHT(peer); } } @@ -1301,6 +1302,26 @@ class PeerCoordinator implements PeerListener } catch (InvalidBEncodingException ibee) {} } + /** + * Send a DHT message to the peer, if we both support DHT. + * @since DHT + */ + void sendDHT(Peer peer) { + DHT dht = _util.getDHT(); + if (dht == null) + return; + Map handshake = peer.getHandshakeMap(); + if (handshake == null) + return; + BEValue bev = handshake.get("m"); + if (bev == null) + return; + try { + if (bev.getMap().get(ExtensionHandler.TYPE_DHT) != null) + ExtensionHandler.sendDHT(peer, dht.getPort(), dht.getRPort()); + } catch (InvalidBEncodingException ibee) {} + } + /** * Sets the storage after transition out of magnet mode * Snark calls this after we call gotMetaInfo() @@ -1318,11 +1339,13 @@ class PeerCoordinator implements PeerListener /** * PeerListener callback * Tell the DHT to ping it, this will get back the node info + * @param rport must be port + 1 * @since 0.8.4 */ - public void gotPort(Peer peer, int port) { + public void gotPort(Peer peer, int port, int rport) { DHT dht = _util.getDHT(); - if (dht != null) + if (dht != null && + port > 0 && port < 65535 && rport == port + 1) dht.ping(peer.getDestination(), port); } diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java b/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java index f573d4455..ee2de562d 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java @@ -190,13 +190,14 @@ interface PeerListener void gotExtension(Peer peer, int id, byte[] bs); /** - * Called when a port message is received. + * Called when a DHT port message is received. * * @param peer the Peer that got the message. - * @param port the port + * @param port the query port + * @param port the response port * @since 0.8.4 */ - void gotPort(Peer peer, int port); + void gotPort(Peer peer, int port, int qport); /** * Called when peers are received via PEX diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java index f41bf1b57..2ed9c373d 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java @@ -526,10 +526,14 @@ class PeerState implements DataLoader setInteresting(true); } - /** @since 0.8.4 */ + /** + * Unused + * @since 0.8.4 + */ void portMessage(int port) { - listener.gotPort(peer, port); + // for compatibility with old DHT PORT message + listener.gotPort(peer, port, port + 1); } void unknownMessage(int type, byte[] bs) diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index 7c1c421a8..56c6ce431 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -85,6 +85,7 @@ public class SnarkManager implements Snark.CompleteListener { public static final String DEFAULT_THEME = "ubergine"; private static final String PROP_USE_OPENTRACKERS = "i2psnark.useOpentrackers"; public static final String PROP_OPENTRACKERS = "i2psnark.opentrackers"; + private static final String PROP_USE_DHT = "i2psnark.enableDHT"; public static final int MIN_UP_BW = 2; public static final int DEFAULT_MAX_UP_BW = 10; @@ -273,6 +274,8 @@ public class SnarkManager implements Snark.CompleteListener { _config.setProperty(PROP_STARTUP_DELAY, Integer.toString(DEFAULT_STARTUP_DELAY)); if (!_config.containsKey(PROP_THEME)) _config.setProperty(PROP_THEME, DEFAULT_THEME); + if (!_config.containsKey(PROP_USE_DHT)) + _config.setProperty(PROP_USE_DHT, Boolean.toString(I2PSnarkUtil.DEFAULT_USE_DHT)); updateConfig(); } /** @@ -347,6 +350,7 @@ public class SnarkManager implements Snark.CompleteListener { String useOT = _config.getProperty(PROP_USE_OPENTRACKERS); boolean bOT = useOT == null || Boolean.valueOf(useOT).booleanValue(); _util.setUseOpenTrackers(bOT); + _util.setUseDHT(Boolean.valueOf(PROP_USE_DHT).booleanValue()); getDataDir().mkdirs(); initTrackerMap(); } @@ -365,7 +369,7 @@ public class SnarkManager implements Snark.CompleteListener { public void updateConfig(String dataDir, boolean filesPublic, boolean autoStart, String refreshDelay, String startDelay, String seedPct, String eepHost, String eepPort, String i2cpHost, String i2cpPort, String i2cpOpts, - String upLimit, String upBW, boolean useOpenTrackers, String theme) { + String upLimit, String upBW, boolean useOpenTrackers, boolean useDHT, String theme) { boolean changed = false; //if (eepHost != null) { // // unused, we use socket eepget @@ -549,6 +553,15 @@ public class SnarkManager implements Snark.CompleteListener { _util.setUseOpenTrackers(useOpenTrackers); changed = true; } + if (_util.shouldUseDHT() != useDHT) { + _config.setProperty(PROP_USE_DHT, Boolean.toString(useDHT)); + if (useDHT) + addMessage(_("Enabled DHT.")); + else + addMessage(_("Disabled DHT.")); + _util.setUseDHT(useDHT); + changed = true; + } if (theme != null) { if(!theme.equals(_config.getProperty(PROP_THEME))) { _config.setProperty(PROP_THEME, theme); diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/DHT.java b/apps/i2psnark/java/src/org/klomp/snark/dht/DHT.java index 6a16e4e60..2e401f060 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/DHT.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/DHT.java @@ -17,10 +17,15 @@ public interface DHT { /** - * @return The UDP port that should be included in a PORT message. + * @return The UDP query port */ public int getPort(); + /** + * @return The UDP response port + */ + public int getRPort(); + /** * Ping. We don't have a NID yet so the node is presumed * to be absent from our DHT. @@ -79,4 +84,9 @@ public interface DHT { * @return the number of successful announces, not counting ourselves. */ public int announce(byte[] ih, int max, long maxWait); + + /** + * Stop everything. + */ + public void stop(); } diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java index ef6757a83..fc81f5479 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java @@ -151,7 +151,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { NID myNID = new NID(myID); _myNodeInfo = new NodeInfo(myNID, session.getMyDestination(), _qPort); - session.addMuxedSessionListener(this, I2PSession.PROTO_DATAGRAM, _rPort); + session.addMuxedSessionListener(this, I2PSession.PROTO_DATAGRAM_RAW, _rPort); session.addMuxedSessionListener(this, I2PSession.PROTO_DATAGRAM, _qPort); // can't be stopped SimpleScheduler.getInstance().addPeriodicEvent(new Cleaner(), CLEAN_TIME); @@ -183,12 +183,19 @@ public class KRPC implements I2PSessionMuxedListener, DHT { } /** - * @return The UDP port that should be included in a PORT message. + * @return The UDP query port */ public int getPort() { return _qPort; } + /** + * @return The UDP response port + */ + public int getRPort() { + return _rPort; + } + /** * Ping. We don't have a NID yet so the node is presumed * to be absent from our DHT. @@ -481,12 +488,19 @@ public class KRPC implements I2PSessionMuxedListener, DHT { } /** - * Does nothing yet. + * Stop everything. */ public void stop() { - // stop the explore thread + // FIXME stop the explore thread // unregister port listeners - // does not clear the DHT or tracker yet. + _session.removeListener(I2PSession.PROTO_DATAGRAM, _qPort); + _session.removeListener(I2PSession.PROTO_DATAGRAM_RAW, _rPort); + // clear the DHT and tracker + _tracker.stop(); + _knownNodes.clear(); + _sentQueries.clear(); + _outgoingTokens.clear(); + _incomingTokens.clear(); } /** diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java index dd9b14ba3..515ee18bf 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -693,11 +693,12 @@ public class I2PSnarkServlet extends DefaultServlet { String refreshDel = req.getParameter("refreshDelay"); String startupDel = req.getParameter("startupDelay"); boolean useOpenTrackers = req.getParameter("useOpenTrackers") != null; + boolean useDHT = req.getParameter("useDHT") != null; //String openTrackers = req.getParameter("openTrackers"); String theme = req.getParameter("theme"); _manager.updateConfig(dataDir, filesPublic, autoStart, refreshDel, startupDel, seedPct, eepHost, eepPort, i2cpHost, i2cpPort, i2cpOpts, - upLimit, upBW, useOpenTrackers, theme); + upLimit, upBW, useOpenTrackers, useDHT, theme); } else if ("Save2".equals(action)) { String taction = req.getParameter("taction"); if (taction != null) @@ -1438,6 +1439,7 @@ public class I2PSnarkServlet extends DefaultServlet { boolean autoStart = _manager.shouldAutoStart(); boolean useOpenTrackers = _manager.util().shouldUseOpenTrackers(); //String openTrackers = _manager.util().getOpenTrackerString(); + boolean useDHT = _manager.util().shouldUseDHT(); //int seedPct = 0; out.write("
\n" + @@ -1551,6 +1553,14 @@ public class I2PSnarkServlet extends DefaultServlet { + (useOpenTrackers ? "checked " : "") + "title=\""); out.write(_("If checked, announce torrents to open trackers as well as the tracker listed in the torrent file")); + out.write("\" >\n" + + + ""); + out.write(_("Enable DHT")); + out.write(": \n"); // ""); diff --git a/core/java/src/net/i2p/client/I2PSession.java b/core/java/src/net/i2p/client/I2PSession.java index 06b094488..d91c3f20d 100644 --- a/core/java/src/net/i2p/client/I2PSession.java +++ b/core/java/src/net/i2p/client/I2PSession.java @@ -252,5 +252,16 @@ public interface I2PSession { public static final int PROTO_ANY = 0; public static final int PROTO_UNSPECIFIED = 0; public static final int PROTO_STREAMING = 6; + + /** + * Generally a signed datagram, but could + * also be a raw datagram, depending on the application + */ public static final int PROTO_DATAGRAM = 17; + + /** + * A raw (unsigned) datagram + * @since 0.9.1 + */ + public static final int PROTO_DATAGRAM_RAW = 18; } From 558bb2f4f36ec9a9ec5d95f603dc61ead3a8664f Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 2 Jun 2012 18:56:10 +0000 Subject: [PATCH 03/23] select proto on UDP send --- apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java index fc81f5479..49874f459 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java @@ -769,7 +769,8 @@ public class KRPC implements I2PSessionMuxedListener, DHT { try { boolean success = _session.sendMessage(dest, payload, 0, payload.length, null, null, 60*1000, - I2PSession.PROTO_DATAGRAM, fromPort, toPort); + repliable ? I2PSession.PROTO_DATAGRAM : I2PSession.PROTO_DATAGRAM_RAW, + fromPort, toPort); if (!success) { if (_log.shouldLog(Log.WARN)) _log.warn("WTF sendMessage fail"); From f8c185d09fe52e14d35a65b3b990d4874c001a71 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 2 Jun 2012 21:44:23 +0000 Subject: [PATCH 04/23] prep for merging --- apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java | 2 +- apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java index dca9fbaf2..425d51aeb 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java +++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java @@ -74,7 +74,7 @@ public class I2PSnarkUtil { public static final int DEFAULT_MAX_UP_BW = 8; //KBps public static final int MAX_CONNECTIONS = 16; // per torrent public static final String PROP_MAX_BW = "i2cp.outboundBytesPerSecond"; - public static final boolean DEFAULT_USE_DHT = true; + public static final boolean DEFAULT_USE_DHT = false; public I2PSnarkUtil(I2PAppContext ctx) { _context = ctx; diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java index 515ee18bf..7f47fe3e2 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -1556,7 +1556,7 @@ public class I2PSnarkServlet extends DefaultServlet { out.write("\" >\n" + ""); - out.write(_("Enable DHT")); + out.write(_("Enable DHT") + " (**BETA**)"); out.write(": Date: Sun, 3 Jun 2012 15:25:51 +0000 Subject: [PATCH 05/23] - Fix node ID / node info confusion - Fix updating node ID when receiving pong - Fix getting DHT enable setting from config file - Fix handling of get_peers replies - Fix sending and receiving announces without signing - Fix incoming/outgoing token handling - Set cleanup timer for all queries - More debug logging --- .../src/org/klomp/snark/SnarkManager.java | 2 +- .../java/src/org/klomp/snark/dht/KRPC.java | 210 ++++++++++++------ .../src/org/klomp/snark/dht/NodeInfo.java | 10 +- .../java/src/org/klomp/snark/dht/Token.java | 32 +++ 4 files changed, 178 insertions(+), 76 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index 56c6ce431..9bfb885cf 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -350,7 +350,7 @@ public class SnarkManager implements Snark.CompleteListener { String useOT = _config.getProperty(PROP_USE_OPENTRACKERS); boolean bOT = useOT == null || Boolean.valueOf(useOT).booleanValue(); _util.setUseOpenTrackers(bOT); - _util.setUseDHT(Boolean.valueOf(PROP_USE_DHT).booleanValue()); + _util.setUseDHT(Boolean.valueOf(_config.getProperty(PROP_USE_DHT)).booleanValue()); getDataDir().mkdirs(); initTrackerMap(); } diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java index 49874f459..e0608df4e 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java @@ -91,13 +91,17 @@ public class KRPC implements I2PSessionMuxedListener, DHT { private final DHTNodes _knownNodes; /** index to sent queries awaiting reply */ private final ConcurrentHashMap _sentQueries; - /** index to outgoing tokens, sent in reply to a get_peers query */ - private final ConcurrentHashMap _outgoingTokens; - /** index to incoming tokens, received in a peers or nodes reply */ - private final ConcurrentHashMap _incomingTokens; + /** index to outgoing tokens we generated, sent in reply to a get_peers query */ + private final ConcurrentHashMap _outgoingTokens; + /** index to incoming opaque tokens, received in a peers or nodes reply */ + private final ConcurrentHashMap _incomingTokens; /** hook to inject and receive datagrams */ private final I2PSession _session; + /** 20 byte random id */ + private final byte[] _myID; + /** 20 byte random id */ + private final NID _myNID; /** 20 byte random id + 32 byte Hash + 2 byte port */ private final NodeInfo _myNodeInfo; /** unsigned dgrams */ @@ -146,10 +150,10 @@ public class KRPC implements I2PSessionMuxedListener, DHT { // ports can really be fixed, just do this for testing _qPort = 30000 + ctx.random().nextInt(99); _rPort = _qPort + 1; - byte[] myID = new byte[NID.HASH_LENGTH]; - ctx.random().nextBytes(myID); - NID myNID = new NID(myID); - _myNodeInfo = new NodeInfo(myNID, session.getMyDestination(), _qPort); + _myID = new byte[NID.HASH_LENGTH]; + ctx.random().nextBytes(_myID); + _myNID = new NID(_myID); + _myNodeInfo = new NodeInfo(_myNID, session.getMyDestination(), _qPort); session.addMuxedSessionListener(this, I2PSession.PROTO_DATAGRAM_RAW, _rPort); session.addMuxedSessionListener(this, I2PSession.PROTO_DATAGRAM, _qPort); @@ -242,7 +246,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { tried.add(nInfo); // this isn't going to work, he will just return our own? - ReplyWaiter waiter = sendFindNode(nInfo, _myNodeInfo); + ReplyWaiter waiter = sendFindNode(nInfo, _myNID); if (waiter == null) continue; synchronized(waiter) { @@ -309,7 +313,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { Set tried = new HashSet(); if (_log.shouldLog(Log.INFO)) - _log.info("Starting getPeers"); + _log.info("Starting getPeers with " + nodes.size() + " to try"); for (int i = 0; i < max; i++) { NodeInfo nInfo; try { @@ -413,7 +417,10 @@ public class KRPC implements I2PSessionMuxedListener, DHT { announce(ih); int rv = 0; long start = _context.clock().now(); - List nodes = _knownNodes.findClosest(new InfoHash(ih), max); + InfoHash iHash = new InfoHash(ih); + List nodes = _knownNodes.findClosest(iHash, max); + if (_log.shouldLog(Log.INFO)) + _log.info("Found " + nodes.size() + " to announce to for " + iHash); for (NodeInfo nInfo : nodes) { if (announce(ih, nInfo, Math.min(maxWait, 60*1000))) rv++; @@ -435,14 +442,18 @@ public class KRPC implements I2PSessionMuxedListener, DHT { * @param maxWait the maximum time to wait (ms) or 0 to return immediately. * @return success */ - public boolean announce(byte[] ih, NodeInfo nInfo, long maxWait) { + private boolean announce(byte[] ih, NodeInfo nInfo, long maxWait) { InfoHash iHash = new InfoHash(ih); - TokenKey tokenKey = new TokenKey(nInfo.getNID(), iHash); - Token token = _incomingTokens.get(tokenKey); + // it isn't clear from BEP 5 if a token is bound to a single infohash? + // for now, just bind to the NID + //TokenKey tokenKey = new TokenKey(nInfo.getNID(), iHash); + Token token = _incomingTokens.get(nInfo.getNID()); if (token == null) { // we have no token, have to do a getPeers first to get a token if (maxWait <= 0) return false; + if (_log.shouldLog(Log.INFO)) + _log.info("No token for announce to " + nInfo + " sending get_peers first"); ReplyWaiter waiter = sendGetPeers(nInfo, iHash); if (waiter == null) return false; @@ -453,15 +464,24 @@ public class KRPC implements I2PSessionMuxedListener, DHT { } catch (InterruptedException ie) {} } int replyType = waiter.getReplyCode(); - if (!(replyType == REPLY_PEERS || replyType == REPLY_NODES)) + if (!(replyType == REPLY_PEERS || replyType == REPLY_NODES)) { + if (_log.shouldLog(Log.INFO)) + _log.info("Get_peers failed to " + nInfo); return false; + } // we should have a token now - token = _incomingTokens.get(tokenKey); - if (token == null) + token = _incomingTokens.get(nInfo.getNID()); + if (token == null) { + if (_log.shouldLog(Log.INFO)) + _log.info("Huh? no token after get_peers succeeded to " + nInfo); return false; + } maxWait -= _context.clock().now() - start; - if (maxWait < 1000) + if (maxWait < 1000) { + if (_log.shouldLog(Log.INFO)) + _log.info("Ran out of time after get_peers succeeded to " + nInfo); return false; + } } // send and wait on rcv msg lock unless maxWait <= 0 @@ -525,6 +545,8 @@ public class KRPC implements I2PSessionMuxedListener, DHT { * @return null on error */ private ReplyWaiter sendPing(NodeInfo nInfo) { + if (_log.shouldLog(Log.INFO)) + _log.info("Sending ping to: " + nInfo); Map map = new HashMap(); map.put("q", "ping"); Map args = new HashMap(); @@ -534,9 +556,12 @@ public class KRPC implements I2PSessionMuxedListener, DHT { /** * @param nInfo who to send it to + * @param tID target ID we are looking for * @return null on error */ - private ReplyWaiter sendFindNode(NodeInfo nInfo, NodeInfo tID) { + private ReplyWaiter sendFindNode(NodeInfo nInfo, NID tID) { + if (_log.shouldLog(Log.INFO)) + _log.info("Sending find node of " + tID + " to: " + nInfo); Map map = new HashMap(); map.put("q", "find_node"); Map args = new HashMap(); @@ -550,12 +575,18 @@ public class KRPC implements I2PSessionMuxedListener, DHT { * @return null on error */ private ReplyWaiter sendGetPeers(NodeInfo nInfo, InfoHash ih) { + if (_log.shouldLog(Log.INFO)) + _log.info("Sending get peers of " + ih + " to: " + nInfo); Map map = new HashMap(); map.put("q", "get_peers"); Map args = new HashMap(); args.put("info_hash", ih.getData()); map.put("a", args); - return sendQuery(nInfo, map, true); + ReplyWaiter rv = sendQuery(nInfo, map, true); + // save the InfoHash so we can get it later + if (rv != null) + rv.setSentObject(ih); + return rv; } /** @@ -563,6 +594,8 @@ public class KRPC implements I2PSessionMuxedListener, DHT { * @return null on error */ private ReplyWaiter sendAnnouncePeer(NodeInfo nInfo, InfoHash ih, Token token) { + if (_log.shouldLog(Log.INFO)) + _log.info("Sending announce of " + ih + " to: " + nInfo); Map map = new HashMap(); map.put("q", "announce_peer"); Map args = new HashMap(); @@ -573,9 +606,6 @@ public class KRPC implements I2PSessionMuxedListener, DHT { map.put("a", args); // an announce need not be signed, we have a token ReplyWaiter rv = sendQuery(nInfo, map, false); - // save the InfoHash so we can get it later - if (rv != null) - rv.setSentObject(ih); return rv; } @@ -587,6 +617,8 @@ public class KRPC implements I2PSessionMuxedListener, DHT { * @return success */ private boolean sendPong(NodeInfo nInfo, MsgID msgID) { + if (_log.shouldLog(Log.INFO)) + _log.info("Sending pong to: " + nInfo); Map map = new HashMap(); Map resps = new HashMap(); map.put("r", resps); @@ -604,6 +636,8 @@ public class KRPC implements I2PSessionMuxedListener, DHT { * @return success */ private boolean sendNodes(NodeInfo nInfo, MsgID msgID, Token token, byte[] ids) { + if (_log.shouldLog(Log.INFO)) + _log.info("Sending nodes to: " + nInfo); Map map = new HashMap(); Map resps = new HashMap(); map.put("r", resps); @@ -615,6 +649,8 @@ public class KRPC implements I2PSessionMuxedListener, DHT { /** @param token non-null */ private boolean sendPeers(NodeInfo nInfo, MsgID msgID, Token token, List peers) { + if (_log.shouldLog(Log.INFO)) + _log.info("Sending peers to: " + nInfo); Map map = new HashMap(); Map resps = new HashMap(); map.put("r", resps); @@ -630,6 +666,8 @@ public class KRPC implements I2PSessionMuxedListener, DHT { * @return success */ private boolean sendError(NodeInfo nInfo, MsgID msgID, int err, String msg) { + if (_log.shouldLog(Log.INFO)) + _log.info("Sending error " + msg + " to: " + nInfo); Map map = new HashMap(); Map resps = new HashMap(); map.put("r", resps); @@ -647,8 +685,8 @@ public class KRPC implements I2PSessionMuxedListener, DHT { private ReplyWaiter sendQuery(NodeInfo nInfo, Map map, boolean repliable) { if (nInfo.equals(_myNodeInfo)) throw new IllegalArgumentException("wtf don't send to ourselves"); - if (_log.shouldLog(Log.INFO)) - _log.info("Sending query to: " + nInfo); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Sending query to: " + nInfo); if (nInfo.getDestination() == null) { NodeInfo newInfo = _knownNodes.get(nInfo.getNID()); if (newInfo != null && newInfo.getDestination() != null) { @@ -666,11 +704,11 @@ public class KRPC implements I2PSessionMuxedListener, DHT { Map args = (Map) map.get("a"); if (args == null) throw new IllegalArgumentException("no args"); - args.put("id", _myNodeInfo.getData()); + args.put("id", _myID); int port = nInfo.getPort(); if (!repliable) port++; - boolean success = sendMessage(nInfo.getDestination(), port, map, true); + boolean success = sendMessage(nInfo.getDestination(), port, map, repliable); if (success) { // save for the caller to get ReplyWaiter rv = new ReplyWaiter(mID, nInfo, null, null); @@ -687,8 +725,8 @@ public class KRPC implements I2PSessionMuxedListener, DHT { private boolean sendResponse(NodeInfo nInfo, MsgID msgID, Map map) { if (nInfo.equals(_myNodeInfo)) throw new IllegalArgumentException("wtf don't send to ourselves"); - if (_log.shouldLog(Log.INFO)) - _log.info("Sending response to: " + nInfo); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Sending response to: " + nInfo); if (nInfo.getDestination() == null) { NodeInfo newInfo = _knownNodes.get(nInfo.getNID()); if (newInfo != null && newInfo.getDestination() != null) { @@ -705,7 +743,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { Map resps = (Map) map.get("r"); if (resps == null) throw new IllegalArgumentException("no resps"); - resps.put("id", _myNodeInfo.getData()); + resps.put("id", _myID); return sendMessage(nInfo.getDestination(), nInfo.getPort() + 1, map, false); } @@ -803,7 +841,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { byte[] msgIDBytes = map.get("t").getBytes(); MsgID mID = new MsgID(msgIDBytes); String type = map.get("y").getString(); - if (type.equals("q") && from != null) { + if (type.equals("q")) { // queries must be repliable String method = map.get("q").getString(); Map args = map.get("a").getMap(); @@ -849,21 +887,31 @@ public class KRPC implements I2PSessionMuxedListener, DHT { /** * Adds sender to our DHT. - * @param dest non-null + * @param dest may be null for announce_peer method only * @throws NPE too */ private void receiveQuery(MsgID msgID, Destination dest, int fromPort, String method, Map args) throws InvalidBEncodingException { + if (dest == null && !method.equals("announce_peer")) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Received non-announce_peer query method on reply port: " + method); + return; + } byte[] nid = args.get("id").getBytes(); - NodeInfo nInfo = new NodeInfo(nid); - nInfo = heardFrom(nInfo); - nInfo.setDestination(dest); -// ninfo.checkport ? + NodeInfo nInfo; + if (dest != null) { + nInfo = new NodeInfo(new NID(nid), dest, fromPort); + nInfo = heardFrom(nInfo); + nInfo.setDestination(dest); + // ninfo.checkport ? + } else { + nInfo = null; + } if (method.equals("ping")) { receivePing(msgID, nInfo); } else if (method.equals("find_node")) { byte[] tid = args.get("target").getBytes(); - NodeInfo tID = new NodeInfo(tid); + NID tID = new NID(tid); receiveFindNode(msgID, nInfo, tID); } else if (method.equals("get_peers")) { byte[] hash = args.get("info_hash").getBytes(); @@ -875,7 +923,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { // this is the "TCP" port, we don't care //int port = args.get("port").getInt(); byte[] token = args.get("token").getBytes(); - receiveAnnouncePeer(msgID, nInfo, ih, token); + receiveAnnouncePeer(msgID, ih, token); } else { if (_log.shouldLog(Log.WARN)) _log.warn("Unknown query method rcvd: " + method); @@ -907,6 +955,9 @@ public class KRPC implements I2PSessionMuxedListener, DHT { NodeInfo nInfo2 = _knownNodes.putIfAbsent(nID, nInfo); if (nInfo2 != null) oldInfo = nInfo2; + } else { + if (oldInfo.getDestination() == null && nInfo.getDestination() != null) + oldInfo.setDestination(nInfo.getDestination()); } if (when > oldInfo.lastSeen()) oldInfo.setLastSeen(when); @@ -924,8 +975,9 @@ public class KRPC implements I2PSessionMuxedListener, DHT { /** * Handle and respond to the query + * @param tID target ID they are looking for */ - private void receiveFindNode(MsgID msgID, NodeInfo nInfo, NodeInfo tID) throws InvalidBEncodingException { + private void receiveFindNode(MsgID msgID, NodeInfo nInfo, NID tID) throws InvalidBEncodingException { if (_log.shouldLog(Log.INFO)) _log.info("Rcvd find_node from: " + nInfo + " for: " + tID); NodeInfo peer = _knownNodes.get(tID); @@ -934,7 +986,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { sendNodes(nInfo, msgID, peer.getData()); } else { // get closest from DHT - List nodes = _knownNodes.findClosest(tID.getNID(), K); + List nodes = _knownNodes.findClosest(tID, K); nodes.remove(nInfo); // him nodes.remove(_myNodeInfo); // me byte[] nodeArray = new byte[nodes.size() * NodeInfo.LENGTH]; @@ -953,7 +1005,9 @@ public class KRPC implements I2PSessionMuxedListener, DHT { _log.info("Rcvd get_peers from: " + nInfo + " for: " + ih); // generate and save random token Token token = new Token(_context); - _outgoingTokens.put(ih, token); + _outgoingTokens.put(token, nInfo.getNID()); + if (_log.shouldLog(Log.INFO)) + _log.info("Stored new OB token: " + token + " for: " + nInfo); List peers = _tracker.getPeers(ih, MAX_WANT); if (peers.isEmpty()) { @@ -979,22 +1033,27 @@ public class KRPC implements I2PSessionMuxedListener, DHT { } /** - * Handle and respond to the query + * Handle and respond to the query. + * We have no node info here, it came on response port, we have to get it from the token */ - private void receiveAnnouncePeer(MsgID msgID, NodeInfo nInfo, InfoHash ih, byte[] token) throws InvalidBEncodingException { - if (_log.shouldLog(Log.INFO)) - _log.info("Rcvd announce from: " + nInfo + " for: " + ih); - // check token - // get desthash from token->dest map - Token oldToken = _outgoingTokens.get(ih); - if (oldToken == null || !DataHelper.eq(oldToken.getData(), token)) { + private void receiveAnnouncePeer(MsgID msgID, InfoHash ih, byte[] tok) throws InvalidBEncodingException { + Token token = new Token(tok); + NID nid = _outgoingTokens.get(token); + if (nid == null) { if (_log.shouldLog(Log.WARN)) - _log.warn("Bad token"); + _log.warn("Unknown token in announce_peer: " + token); + if (_log.shouldLog(Log.INFO)) + _log.info("Current known tokens: " + _outgoingTokens.keySet()); return; } - - //msg ID -> NodeInfo -> Dest -> Hash - //verify with token -> nid or dest or hash ???? + NodeInfo nInfo = _knownNodes.get(nid); + if (nInfo == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Unknown node in announce_peer for: " + nid); + return; + } + if (_log.shouldLog(Log.INFO)) + _log.info("Rcvd announce from: " + nInfo + " for: " + ih); _tracker.announce(ih, nInfo.getHash()); // the reply for an announce is the same as the reply for a ping @@ -1020,9 +1079,10 @@ public class KRPC implements I2PSessionMuxedListener, DHT { InfoHash ih = (InfoHash) waiter.getSentObject(); if (btok != null && ih != null) { byte[] tok = btok.getBytes(); - _incomingTokens.put(new TokenKey(nInfo.getNID(), ih), new Token(_context, tok)); + Token token = new Token(_context, tok); + _incomingTokens.put(nInfo.getNID(), token); if (_log.shouldLog(Log.INFO)) - _log.info("Got token, must be a response to get_peers"); + _log.info("Got token: " + token + ", must be a response to get_peers"); } else { if (_log.shouldLog(Log.INFO)) _log.info("No token and saved infohash, must be a response to find_node"); @@ -1042,7 +1102,8 @@ public class KRPC implements I2PSessionMuxedListener, DHT { waiter.gotReply(REPLY_PEERS, rlist); } else { // a ping response or an announce peer response - receivePong(nInfo); + byte[] nid = response.get("id").getBytes(); + receivePong(nInfo, nid); waiter.gotReply(REPLY_PONG, null); } } @@ -1085,8 +1146,18 @@ public class KRPC implements I2PSessionMuxedListener, DHT { return rv; } - /** does nothing, but node was already added to our DHT */ - private void receivePong(NodeInfo nInfo) { + /** + * If node info was previously created with the dummy NID, + * replace it with the received NID. + */ + private void receivePong(NodeInfo nInfo, byte[] nid) { + if (nInfo.getNID().equals(_fakeNID)) { + NodeInfo newInfo = new NodeInfo(new NID(nid), nInfo.getHash(), nInfo.getPort()); + Destination dest = nInfo.getDestination(); + if (dest != null) + newInfo.setDestination(dest); + heardFrom(newInfo); + } if (_log.shouldLog(Log.INFO)) _log.info("Rcvd pong from: " + nInfo); } @@ -1121,23 +1192,23 @@ public class KRPC implements I2PSessionMuxedListener, DHT { /** * Either wait on this object with a timeout, or use non-null Runnables. - * Any sent data to be rememberd may be stored by setSentObject(). + * Any sent data to be remembered may be stored by setSentObject(). * Reply object may be in getReplyObject(). * @param onReply must be fast, otherwise set to null and wait on this * @param onTimeout must be fast, otherwise set to null and wait on this */ public ReplyWaiter(MsgID mID, NodeInfo nInfo, Runnable onReply, Runnable onTimeout) { - super(nInfo.getData()); + super(nInfo.getNID(), nInfo.getHash(), nInfo.getPort()); + Destination dest = nInfo.getDestination(); + if (dest != null) + setDestination(dest); this.mid = mID; this.onReply = onReply; this.onTimeout = onTimeout; - if (onTimeout != null) - this.event = new Event(); - else - this.event = null; + this.event = new Event(); } - /** only used for announce, to save the Info Hash */ + /** only used for get_peers, to save the Info Hash */ public void setSentObject(Object o) { sentObject = o; } @@ -1174,10 +1245,11 @@ public class KRPC implements I2PSessionMuxedListener, DHT { public void gotReply(int code, Object o) { replyCode = code; replyObject = o; - if (event != null) - event.cancel(); + event.cancel(); _sentQueries.remove(mid); - heardFrom(this); + // if it is fake, heardFrom is called by receivePong() + if (!getNID().equals(_fakeNID)) + heardFrom(this); if (onReply != null) onReply.run(); synchronized(this) { @@ -1266,7 +1338,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { public void timeReached() { long now = _context.clock().now(); - for (Iterator iter = _outgoingTokens.values().iterator(); iter.hasNext(); ) { + for (Iterator iter = _outgoingTokens.keySet().iterator(); iter.hasNext(); ) { Token tok = iter.next(); if (tok.lastSeen() < now - MAX_TOKEN_AGE) iter.remove(); diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java index 9e73ca03b..7c74a506e 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java @@ -15,6 +15,7 @@ import net.i2p.data.SimpleDataStructure; * * Things are a little tricky in KRPC since we exchange Hashes and don't * always have the Destination. + * The conpact info is immutable. The Destination may be added later. * * @since 0.8.4 * @author zzz @@ -45,7 +46,6 @@ public class NodeInfo extends SimpleDataStructure { /** * No Destination yet available - * @deprecated unused * @throws IllegalArgumentException */ public NodeInfo(NID nID, Hash hash, int port) { @@ -138,13 +138,11 @@ public class NodeInfo extends SimpleDataStructure { * @throws IllegalArgumentException if hash of dest doesn't match previous hash */ public void setDestination(Destination dest) throws IllegalArgumentException { + if (this.dest != null) + return; if (!dest.calculateHash().equals(this.hash)) throw new IllegalArgumentException("Hash mismatch, was: " + this.hash + " new: " + dest.calculateHash()); - if (this.dest == null) - this.dest = dest; - else if (!this.dest.equals(dest)) - throw new IllegalArgumentException("Dest mismatch, was: " + this.dest+ " new: " + dest); - // else keep the old to reduce object churn + this.dest = dest; } public int getPort() { diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/Token.java b/apps/i2psnark/java/src/org/klomp/snark/dht/Token.java index c8859155d..46b3e1097 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/Token.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/Token.java @@ -3,6 +3,8 @@ package org.klomp.snark.dht; * GPLv2 */ +import java.util.Date; + import net.i2p.I2PAppContext; import net.i2p.data.ByteArray; import net.i2p.data.DataHelper; @@ -24,6 +26,7 @@ public class Token extends ByteArray { byte[] data = new byte[MY_TOK_LEN]; ctx.random().nextBytes(data); setData(data); + setValid(MY_TOK_LEN); lastSeen = ctx.clock().now(); } @@ -33,7 +36,36 @@ public class Token extends ByteArray { lastSeen = ctx.clock().now(); } + /** incoming - for lookup only, not storage, lastSeen is 0 */ + public Token(byte[] data) { + super(data); + lastSeen = 0; + } + public long lastSeen() { return lastSeen; } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(64); + buf.append("[Token: "); + byte[] bs = getData(); + if (bs.length == 0) { + buf.append("0 bytes"); + } else { + buf.append(bs.length).append(" bytes: 0x"); + // backwards, but the same way BEValue does it + for (int i = 0; i < bs.length; i++) { + int b = bs[i] & 0xff; + if (b < 16) + buf.append('0'); + buf.append(Integer.toHexString(b)); + } + } + if (lastSeen > 0) + buf.append(" created ").append((new Date(lastSeen)).toString()); + buf.append(']'); + return buf.toString(); + } } From 121491a3be3d2cb6e1f42868e657c9d7f271da5a Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 3 Jun 2012 16:05:38 +0000 Subject: [PATCH 06/23] - B32 lookup if required for non-announce queries only - Token timeout tweaks - Most classes package private --- .../src/org/klomp/snark/dht/DHTNodes.java | 2 +- .../src/org/klomp/snark/dht/InfoHash.java | 2 +- .../java/src/org/klomp/snark/dht/KRPC.java | 48 ++++++++++++++++--- .../java/src/org/klomp/snark/dht/MsgID.java | 2 +- .../java/src/org/klomp/snark/dht/NID.java | 2 +- .../src/org/klomp/snark/dht/NodeInfo.java | 2 +- .../java/src/org/klomp/snark/dht/Peer.java | 2 +- .../java/src/org/klomp/snark/dht/Peers.java | 2 +- .../java/src/org/klomp/snark/dht/Token.java | 2 +- .../src/org/klomp/snark/dht/TokenKey.java | 2 +- .../src/org/klomp/snark/dht/Torrents.java | 2 +- 11 files changed, 52 insertions(+), 16 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java b/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java index cd6d0f37c..14c9f9a3f 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java @@ -28,7 +28,7 @@ import net.i2p.util.SimpleTimer; * @since 0.8.4 * @author zzz */ -public class DHTNodes extends ConcurrentHashMap { +class DHTNodes extends ConcurrentHashMap { private final I2PAppContext _context; private long _expireTime; diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/InfoHash.java b/apps/i2psnark/java/src/org/klomp/snark/dht/InfoHash.java index 2b439c6c4..221d79af4 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/InfoHash.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/InfoHash.java @@ -11,7 +11,7 @@ import net.i2p.crypto.SHA1Hash; * @since 0.8.4 * @author zzz */ -public class InfoHash extends SHA1Hash { +class InfoHash extends SHA1Hash { public InfoHash(byte[] data) { super(data); diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java index e0608df4e..c4190bd3b 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java @@ -127,6 +127,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { private static final long MAX_NODEINFO_AGE = 60*60*1000; /** how long since generated do we delete - BEP 5 says 10 minutes */ private static final long MAX_TOKEN_AGE = 60*60*1000; + private static final long MAX_INBOUND_TOKEN_AGE = MAX_TOKEN_AGE - 5*60*1000; /** how long since sent do we wait for a reply */ private static final long MAX_MSGID_AGE = 2*60*1000; /** how long since sent do we wait for a reply */ @@ -679,6 +680,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { // TODO sendQuery with onReply / onTimeout args /** + * Blocking if repliable and we must lookup b32 * @param repliable true for all but announce * @return null on error */ @@ -691,11 +693,19 @@ public class KRPC implements I2PSessionMuxedListener, DHT { NodeInfo newInfo = _knownNodes.get(nInfo.getNID()); if (newInfo != null && newInfo.getDestination() != null) { nInfo = newInfo; - } else { - // lookup b32? + } else if (!repliable) { + // Don't lookup for announce query, we should already have it if (_log.shouldLog(Log.WARN)) - _log.warn("No destination for: " + nInfo); + _log.warn("Dropping non-repliable query, no dest for " + nInfo); return null; + } else { + // Lookup the dest for the hash + // TODO spin off into thread or queue? We really don't want to block here + if (!lookupDest(nInfo)) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Dropping repliable query, no dest for " + nInfo); + return null; + } } } map.put("y", "q"); @@ -734,7 +744,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { } else { // lookup b32? if (_log.shouldLog(Log.WARN)) - _log.warn("No destination for: " + nInfo); + _log.warn("Dropping response, no dest for " + nInfo); return false; } } @@ -763,7 +773,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { } else { // lookup b32? if (_log.shouldLog(Log.WARN)) - _log.warn("No destination for: " + nInfo); + _log.warn("Dropping sendError, no dest for " + nInfo); return false; } } @@ -772,6 +782,32 @@ public class KRPC implements I2PSessionMuxedListener, DHT { return sendMessage(nInfo.getDestination(), nInfo.getPort() + 1, map, false); } + /** + * Get the dest for a NodeInfo lacking it, and store it there. + * Blocking. + * @return success + */ + private boolean lookupDest(NodeInfo nInfo) { + if (_log.shouldLog(Log.INFO)) + _log.info("looking up dest for " + nInfo); + try { + // use a short timeout for now + Destination dest = _session.lookupDest(nInfo.getHash(), 5*1000); + if (dest != null) { + nInfo.setDestination(dest); + if (_log.shouldLog(Log.INFO)) + _log.info("lookup success for " + nInfo); + return true; + } + } catch (I2PSessionException ise) { + if (_log.shouldLog(Log.WARN)) + _log.warn("lookup fail", ise); + } + if (_log.shouldLog(Log.INFO)) + _log.info("lookup fail for " + nInfo); + return false; + } + /** * Lowest-level send message call. * @param repliable true for all but announce @@ -1345,7 +1381,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { } for (Iterator iter = _incomingTokens.values().iterator(); iter.hasNext(); ) { Token tok = iter.next(); - if (tok.lastSeen() < now - MAX_TOKEN_AGE) + if (tok.lastSeen() < now - MAX_INBOUND_TOKEN_AGE) iter.remove(); } // TODO sent queries? diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/MsgID.java b/apps/i2psnark/java/src/org/klomp/snark/dht/MsgID.java index 94b37a690..6d53f7c8f 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/MsgID.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/MsgID.java @@ -12,7 +12,7 @@ import net.i2p.data.ByteArray; * @since 0.8.4 * @author zzz */ -public class MsgID extends ByteArray { +class MsgID extends ByteArray { private static final int MY_TOK_LEN = 8; diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/NID.java b/apps/i2psnark/java/src/org/klomp/snark/dht/NID.java index 3d0d0a496..87407720b 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/NID.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/NID.java @@ -11,7 +11,7 @@ import net.i2p.crypto.SHA1Hash; * @since 0.8.4 * @author zzz */ -public class NID extends SHA1Hash { +class NID extends SHA1Hash { public NID(byte[] data) { super(data); diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java index 7c74a506e..1d82b268a 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java @@ -21,7 +21,7 @@ import net.i2p.data.SimpleDataStructure; * @author zzz */ -public class NodeInfo extends SimpleDataStructure { +class NodeInfo extends SimpleDataStructure { private long lastSeen; private NID nID; diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/Peer.java b/apps/i2psnark/java/src/org/klomp/snark/dht/Peer.java index 8943c06b5..84fc263a7 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/Peer.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/Peer.java @@ -12,7 +12,7 @@ import net.i2p.data.Hash; * @since 0.8.4 * @author zzz */ -public class Peer extends Hash { +class Peer extends Hash { private long lastSeen; diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/Peers.java b/apps/i2psnark/java/src/org/klomp/snark/dht/Peers.java index 2a2452e95..f16d903ec 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/Peers.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/Peers.java @@ -13,7 +13,7 @@ import net.i2p.data.Hash; * @since 0.8.4 * @author zzz */ -public class Peers extends ConcurrentHashMap { +class Peers extends ConcurrentHashMap { public Peers() { super(); diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/Token.java b/apps/i2psnark/java/src/org/klomp/snark/dht/Token.java index 46b3e1097..37a43575d 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/Token.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/Token.java @@ -15,7 +15,7 @@ import net.i2p.data.DataHelper; * @since 0.8.4 * @author zzz */ -public class Token extends ByteArray { +class Token extends ByteArray { private static final int MY_TOK_LEN = 8; private final long lastSeen; diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/TokenKey.java b/apps/i2psnark/java/src/org/klomp/snark/dht/TokenKey.java index d2217a015..996d43351 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/TokenKey.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/TokenKey.java @@ -12,7 +12,7 @@ import net.i2p.data.DataHelper; * @since 0.8.4 * @author zzz */ -public class TokenKey extends SHA1Hash { +class TokenKey extends SHA1Hash { public TokenKey(NID nID, InfoHash ih) { super(DataHelper.xor(nID.getData(), ih.getData())); diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/Torrents.java b/apps/i2psnark/java/src/org/klomp/snark/dht/Torrents.java index c791e7077..304b7c949 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/Torrents.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/Torrents.java @@ -11,7 +11,7 @@ import java.util.concurrent.ConcurrentHashMap; * @since 0.8.4 * @author zzz */ -public class Torrents extends ConcurrentHashMap { +class Torrents extends ConcurrentHashMap { public Torrents() { super(); From d5cb443925e4f91f9058a0d54acc2738a8e9dd2f Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 4 Jun 2012 14:15:38 +0000 Subject: [PATCH 07/23] - Switch back from storing NID to full NodeInfo for outgoing tokens so they don't get expired early - Announce only to the single closest DHT peer - Increase random port range - Decrease max local tracker and DHT size --- .../src/org/klomp/snark/TrackerClient.java | 3 ++- .../src/org/klomp/snark/dht/DHTNodes.java | 2 +- .../src/org/klomp/snark/dht/DHTTracker.java | 2 +- .../java/src/org/klomp/snark/dht/KRPC.java | 26 +++++++++---------- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java index 01b6a829b..ac5c53285 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java +++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java @@ -374,7 +374,8 @@ public class TrackerClient extends I2PAppThread // announce ourselves while the token is still good // FIXME this needs to be in its own thread if (!stop) { - int good = _util.getDHT().announce(snark.getInfoHash(), 8, 5*60*1000); + // announce only to the 1 closest + int good = _util.getDHT().announce(snark.getInfoHash(), 1, 5*60*1000); _util.debug("Sent " + good + " good announces to DHT", Snark.INFO); } diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java b/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java index 14c9f9a3f..0e57d1f26 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java @@ -39,7 +39,7 @@ class DHTNodes extends ConcurrentHashMap { private static final long MAX_EXPIRE_TIME = 60*60*1000; private static final long MIN_EXPIRE_TIME = 5*60*1000; private static final long DELTA_EXPIRE_TIME = 7*60*1000; - private static final int MAX_PEERS = 9999; + private static final int MAX_PEERS = 999; public DHTNodes(I2PAppContext ctx) { super(); diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/DHTTracker.java b/apps/i2psnark/java/src/org/klomp/snark/dht/DHTTracker.java index 712b89e4f..d7e9df93e 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/DHTTracker.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/DHTTracker.java @@ -34,7 +34,7 @@ class DHTTracker { private static final long MAX_EXPIRE_TIME = 95*60*1000; private static final long MIN_EXPIRE_TIME = 5*60*1000; private static final long DELTA_EXPIRE_TIME = 7*60*1000; - private static final int MAX_PEERS = 9999; + private static final int MAX_PEERS = 2000; DHTTracker(I2PAppContext ctx) { _context = ctx; diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java index c4190bd3b..f34a6a1dd 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java @@ -92,7 +92,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { /** index to sent queries awaiting reply */ private final ConcurrentHashMap _sentQueries; /** index to outgoing tokens we generated, sent in reply to a get_peers query */ - private final ConcurrentHashMap _outgoingTokens; + private final ConcurrentHashMap _outgoingTokens; /** index to incoming opaque tokens, received in a peers or nodes reply */ private final ConcurrentHashMap _incomingTokens; @@ -148,8 +148,9 @@ public class KRPC implements I2PSessionMuxedListener, DHT { _incomingTokens = new ConcurrentHashMap(); // Construct my NodeInfo - // ports can really be fixed, just do this for testing - _qPort = 30000 + ctx.random().nextInt(99); + // Pick ports over a big range to marginally increase security + // If we add a search DHT, adjust to stay out of each other's way + _qPort = 2555 + ctx.random().nextInt(61111); _rPort = _qPort + 1; _myID = new byte[NID.HASH_LENGTH]; ctx.random().nextBytes(_myID); @@ -291,6 +292,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { /** * Get peers for a torrent. + * This is an iterative lookup in the DHT. * Blocking! * Caller should run in a thread. * @@ -404,13 +406,16 @@ public class KRPC implements I2PSessionMuxedListener, DHT { } /** - * Announce to the closest DHT peers. + * Announce to the closest peers in the local DHT. + * This is NOT iterative - call getPeers() first to get the closest + * peers into the local DHT. * Blocking unless maxWait <= 0 * Caller should run in a thread. * This also automatically announces ourself to our local tracker. * For best results do a getPeers() first so we have tokens. * * @param ih the Info Hash (torrent) + * @param max maximum number of peers to announce to * @param maxWait the maximum total time to wait (ms) or 0 to do all in parallel and return immediately. * @return the number of successful announces, not counting ourselves. */ @@ -842,6 +847,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { } try { + // TODO I2CP per-packet options boolean success = _session.sendMessage(dest, payload, 0, payload.length, null, null, 60*1000, repliable ? I2PSession.PROTO_DATAGRAM : I2PSession.PROTO_DATAGRAM_RAW, fromPort, toPort); @@ -1041,7 +1047,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { _log.info("Rcvd get_peers from: " + nInfo + " for: " + ih); // generate and save random token Token token = new Token(_context); - _outgoingTokens.put(token, nInfo.getNID()); + _outgoingTokens.put(token, nInfo); if (_log.shouldLog(Log.INFO)) _log.info("Stored new OB token: " + token + " for: " + nInfo); @@ -1074,20 +1080,14 @@ public class KRPC implements I2PSessionMuxedListener, DHT { */ private void receiveAnnouncePeer(MsgID msgID, InfoHash ih, byte[] tok) throws InvalidBEncodingException { Token token = new Token(tok); - NID nid = _outgoingTokens.get(token); - if (nid == null) { + NodeInfo nInfo = _outgoingTokens.get(token); + if (nInfo == null) { if (_log.shouldLog(Log.WARN)) _log.warn("Unknown token in announce_peer: " + token); if (_log.shouldLog(Log.INFO)) _log.info("Current known tokens: " + _outgoingTokens.keySet()); return; } - NodeInfo nInfo = _knownNodes.get(nid); - if (nInfo == null) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Unknown node in announce_peer for: " + nid); - return; - } if (_log.shouldLog(Log.INFO)) _log.info("Rcvd announce from: " + nInfo + " for: " + ih); From 3f40487c996477d47b51be5f99439596eee0f50c Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 4 Jun 2012 22:34:56 +0000 Subject: [PATCH 08/23] - Add persistent local DHT storage - Shutdown now closes tunnel - Delay after sending stop announces at shutdown - Stub out using Hash cache - Implement stop for all cleaners - Log tweaks --- .../src/org/klomp/snark/I2PSnarkUtil.java | 4 +- .../src/org/klomp/snark/SnarkManager.java | 12 ++- .../src/org/klomp/snark/dht/DHTNodes.java | 28 +++++-- .../src/org/klomp/snark/dht/DHTTracker.java | 25 ++++-- .../java/src/org/klomp/snark/dht/KRPC.java | 39 +++++++--- .../src/org/klomp/snark/dht/NodeInfo.java | 61 ++++++++++++++- .../src/org/klomp/snark/dht/PersistDHT.java | 77 +++++++++++++++++++ 7 files changed, 216 insertions(+), 30 deletions(-) create mode 100644 apps/i2psnark/java/src/org/klomp/snark/dht/PersistDHT.java diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java index 425d51aeb..0d71dcd34 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java +++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java @@ -261,7 +261,8 @@ public class I2PSnarkUtil { // FIXME this can cause race NPEs elsewhere _manager = null; _shitlist.clear(); - mgr.destroySocketManager(); + if (mgr != null) + mgr.destroySocketManager(); // this will delete a .torrent file d/l in progress so don't do that... FileUtil.rmdir(_tmpDir, false); // in case the user will d/l a .torrent file next... @@ -405,6 +406,7 @@ public class I2PSnarkUtil { byte[] b = Base32.decode(ip.substring(0, BASE32_HASH_LENGTH)); if (b != null) { Hash h = new Hash(b); + //Hash h = Hash.create(b); if (_log.shouldLog(Log.INFO)) _log.info("Using existing session for lookup of " + ip); try { diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index 9bfb885cf..42927df00 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -1514,15 +1514,23 @@ public class SnarkManager implements Snark.CompleteListener { } } - public class SnarkManagerShutdown extends I2PAppThread { + private class SnarkManagerShutdown extends I2PAppThread { @Override public void run() { Set names = listTorrentFiles(); + int running = 0; for (Iterator iter = names.iterator(); iter.hasNext(); ) { Snark snark = getTorrent((String)iter.next()); - if ( (snark != null) && (!snark.isStopped()) ) + if (snark != null && !snark.isStopped()) { snark.stopTorrent(); + running++; + } } + _snarks.clear(); + if (running > 0) { + try { sleep(1500); } catch (InterruptedException ie) {}; + } + _util.disconnect(); } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java b/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java index 0e57d1f26..e18fca6fe 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java @@ -15,8 +15,7 @@ import net.i2p.I2PAppContext; import net.i2p.crypto.SHA1Hash; import net.i2p.data.DataHelper; import net.i2p.util.Log; -import net.i2p.util.SimpleScheduler; -import net.i2p.util.SimpleTimer; +import net.i2p.util.SimpleTimer2; /** * All the nodes we know about, stored as a mapping from @@ -33,6 +32,7 @@ class DHTNodes extends ConcurrentHashMap { private final I2PAppContext _context; private long _expireTime; private final Log _log; + private volatile boolean _isRunning; /** stagger with other cleaners */ private static final long CLEAN_TIME = 237*1000; @@ -46,7 +46,16 @@ class DHTNodes extends ConcurrentHashMap { _context = ctx; _expireTime = MAX_EXPIRE_TIME; _log = _context.logManager().getLog(DHTNodes.class); - SimpleScheduler.getInstance().addPeriodicEvent(new Cleaner(), CLEAN_TIME); + } + + public void start() { + _isRunning = true; + new Cleaner(); + } + + public void stop() { + clear(); + _isRunning = false; } /** @@ -82,9 +91,15 @@ class DHTNodes extends ConcurrentHashMap { ****/ /** */ - private class Cleaner implements SimpleTimer.TimedEvent { + private class Cleaner extends SimpleTimer2.TimedEvent { + + public Cleaner() { + super(SimpleTimer2.getInstance(), CLEAN_TIME); + } public void timeReached() { + if (!_isRunning) + return; long now = _context.clock().now(); int peerCount = 0; for (Iterator iter = DHTNodes.this.values().iterator(); iter.hasNext(); ) { @@ -100,11 +115,12 @@ class DHTNodes extends ConcurrentHashMap { else _expireTime = Math.min(_expireTime + DELTA_EXPIRE_TIME, MAX_EXPIRE_TIME); - if (_log.shouldLog(Log.INFO)) - _log.info("DHT storage cleaner done, now with " + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("DHT storage cleaner done, now with " + peerCount + " peers, " + DataHelper.formatDuration(_expireTime) + " expiration"); + schedule(CLEAN_TIME); } } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/DHTTracker.java b/apps/i2psnark/java/src/org/klomp/snark/dht/DHTTracker.java index d7e9df93e..97fbcef73 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/DHTTracker.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/DHTTracker.java @@ -12,8 +12,7 @@ import net.i2p.I2PAppContext; import net.i2p.data.DataHelper; import net.i2p.data.Hash; import net.i2p.util.Log; -import net.i2p.util.SimpleScheduler; -import net.i2p.util.SimpleTimer; +import net.i2p.util.SimpleTimer2; /** * The tracker stores peers, i.e. Dest hashes (not nodes). @@ -27,6 +26,7 @@ class DHTTracker { private final Torrents _torrents; private long _expireTime; private final Log _log; + private volatile boolean _isRunning; /** stagger with other cleaners */ private static final long CLEAN_TIME = 199*1000; @@ -41,12 +41,16 @@ class DHTTracker { _torrents = new Torrents(); _expireTime = MAX_EXPIRE_TIME; _log = _context.logManager().getLog(DHTTracker.class); - SimpleScheduler.getInstance().addPeriodicEvent(new Cleaner(), CLEAN_TIME); + } + + public void start() { + _isRunning = true; + new Cleaner(); } void stop() { _torrents.clear(); - // no way to stop the cleaner + _isRunning = false; } void announce(InfoHash ih, Hash hash) { @@ -93,9 +97,15 @@ class DHTTracker { return rv; } - private class Cleaner implements SimpleTimer.TimedEvent { + private class Cleaner extends SimpleTimer2.TimedEvent { + + public Cleaner() { + super(SimpleTimer2.getInstance(), CLEAN_TIME); + } public void timeReached() { + if (!_isRunning) + return; long now = _context.clock().now(); int torrentCount = 0; int peerCount = 0; @@ -122,11 +132,12 @@ class DHTTracker { else _expireTime = Math.min(_expireTime + DELTA_EXPIRE_TIME, MAX_EXPIRE_TIME); - if (_log.shouldLog(Log.INFO)) - _log.info("DHT tracker cleaner done, now with " + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("DHT tracker cleaner done, now with " + torrentCount + " torrents, " + peerCount + " peers, " + DataHelper.formatDuration(_expireTime) + " expiration"); + schedule(CLEAN_TIME); } } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java index f34a6a1dd..2c44deb68 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java @@ -5,6 +5,7 @@ package org.klomp.snark.dht; */ import java.io.ByteArrayInputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -36,8 +37,6 @@ import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.data.SimpleDataStructure; import net.i2p.util.Log; -import net.i2p.util.SimpleScheduler; -import net.i2p.util.SimpleTimer; import net.i2p.util.SimpleTimer2; import org.klomp.snark.bencode.BDecoder; @@ -108,6 +107,8 @@ public class KRPC implements I2PSessionMuxedListener, DHT { private final int _rPort; /** signed dgrams */ private final int _qPort; + private final File _dhtFile; + private volatile boolean _isRunning; /** all-zero NID used for pings */ private static final NID _fakeNID = new NID(new byte[NID.HASH_LENGTH]); @@ -134,6 +135,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { private static final long DEFAULT_QUERY_TIMEOUT = 75*1000; /** stagger with other cleaners */ private static final long CLEAN_TIME = 63*1000; + private static final String DHT_FILE = "i2psnark.dht.dat"; public KRPC (I2PAppContext ctx, I2PSession session) { _context = ctx; @@ -156,11 +158,11 @@ public class KRPC implements I2PSessionMuxedListener, DHT { ctx.random().nextBytes(_myID); _myNID = new NID(_myID); _myNodeInfo = new NodeInfo(_myNID, session.getMyDestination(), _qPort); + _dhtFile = new File(ctx.getConfigDir(), DHT_FILE); session.addMuxedSessionListener(this, I2PSession.PROTO_DATAGRAM_RAW, _rPort); session.addMuxedSessionListener(this, I2PSession.PROTO_DATAGRAM, _qPort); - // can't be stopped - SimpleScheduler.getInstance().addPeriodicEvent(new Cleaner(), CLEAN_TIME); + start(); } ///////////////// Public methods @@ -391,6 +393,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { public void announce(byte[] ih, byte[] peerHash) { InfoHash iHash = new InfoHash(ih); _tracker.announce(iHash, new Hash(peerHash)); +// _tracker.announce(iHash, Hash.create(peerHash)); } /** @@ -506,24 +509,32 @@ public class KRPC implements I2PSessionMuxedListener, DHT { } /** - * Does nothing yet, everything is prestarted. + * Loads the DHT from file. * Can't be restarted after stopping? */ public void start() { + _knownNodes.start(); + _tracker.start(); + PersistDHT.loadDHT(this, _dhtFile); // start the explore thread + _isRunning = true; + // no need to keep ref, it will eventually stop + new Cleaner(); } /** * Stop everything. */ public void stop() { + _isRunning = false; // FIXME stop the explore thread // unregister port listeners _session.removeListener(I2PSession.PROTO_DATAGRAM, _qPort); _session.removeListener(I2PSession.PROTO_DATAGRAM_RAW, _rPort); // clear the DHT and tracker _tracker.stop(); - _knownNodes.clear(); + PersistDHT.saveDHT(_knownNodes, _dhtFile); + _knownNodes.stop(); _sentQueries.clear(); _outgoingTokens.clear(); _incomingTokens.clear(); @@ -1175,6 +1186,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { for (BEValue bev : peers) { byte[] b = bev.getBytes(); Hash h = new Hash(b); + //Hash h = Hash.create(b); rv.add(h); } if (_log.shouldLog(Log.INFO)) @@ -1303,7 +1315,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { if (onTimeout != null) onTimeout.run(); if (_log.shouldLog(Log.INFO)) - _log.warn("timeout waiting for reply from " + this.toString()); + _log.warn("timeout waiting for reply from " + ReplyWaiter.this.toString()); } } } @@ -1370,9 +1382,15 @@ public class KRPC implements I2PSessionMuxedListener, DHT { /** * Cleaner-upper */ - private class Cleaner implements SimpleTimer.TimedEvent { + private class Cleaner extends SimpleTimer2.TimedEvent { + + public Cleaner() { + super(SimpleTimer2.getInstance(), CLEAN_TIME); + } public void timeReached() { + if (!_isRunning) + return; long now = _context.clock().now(); for (Iterator iter = _outgoingTokens.keySet().iterator(); iter.hasNext(); ) { Token tok = iter.next(); @@ -1390,12 +1408,13 @@ public class KRPC implements I2PSessionMuxedListener, DHT { if (ni.lastSeen() < now - MAX_NODEINFO_AGE) iter.remove(); } - if (_log.shouldLog(Log.INFO)) - _log.info("KRPC cleaner done, now with " + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("KRPC cleaner done, now with " + _outgoingTokens.size() + " sent Tokens, " + _incomingTokens.size() + " rcvd Tokens, " + _knownNodes.size() + " known peers, " + _sentQueries.size() + " queries awaiting response"); + schedule(CLEAN_TIME); } } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java index 1d82b268a..e4f52091b 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java @@ -3,6 +3,8 @@ package org.klomp.snark.dht; * From zzzot, modded and relicensed to GPLv2 */ +import net.i2p.data.Base64; +import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.data.Hash; @@ -41,7 +43,7 @@ class NodeInfo extends SimpleDataStructure { this.dest = dest; this.hash = dest.calculateHash(); this.port = port; - initialize(nID, this.hash, port); + initialize(); } /** @@ -53,7 +55,7 @@ class NodeInfo extends SimpleDataStructure { this.nID = nID; this.hash = hash; this.port = port; - initialize(nID, hash, port); + initialize(); } /** @@ -81,6 +83,36 @@ class NodeInfo extends SimpleDataStructure { initialize(d); } + /** + * Form persistent storage string. + * Format: NID:Hash:Destination:port + * First 3 in base 64; Destination may be empty string + * @throws IllegalArgumentException + */ + public NodeInfo(String s) throws DataFormatException { + super(); + String[] parts = s.split(":", 4); + if (parts.length != 4) + throw new DataFormatException("Bad format"); + byte[] nid = Base64.decode(parts[0]); + if (nid == null) + throw new DataFormatException("Bad NID"); + nID = new NID(nid); + byte[] h = Base64.decode(parts[1]); + if (h == null) + throw new DataFormatException("Bad hash"); + hash = new Hash(h); + //hash = Hash.create(h); + if (parts[2].length() > 0) + dest = new Destination(parts[2]); + try { + port = Integer.parseInt(parts[3]); + } catch (NumberFormatException nfe) { + throw new DataFormatException("Bad port", nfe); + } + initialize(); + } + /** * Creates data structures from the compact info * @throws IllegalArgumentException @@ -91,18 +123,22 @@ class NodeInfo extends SimpleDataStructure { byte[] ndata = new byte[NID.HASH_LENGTH]; System.arraycopy(compactInfo, 0, ndata, 0, NID.HASH_LENGTH); this.nID = new NID(ndata); + //3 lines or... byte[] hdata = new byte[Hash.HASH_LENGTH]; System.arraycopy(compactInfo, NID.HASH_LENGTH, hdata, 0, Hash.HASH_LENGTH); this.hash = new Hash(hdata); + //this.hash = Hash.create(compactInfo, NID.HASH_LENGTH); this.port = (int) DataHelper.fromLong(compactInfo, NID.HASH_LENGTH + Hash.HASH_LENGTH, 2); + if (port <= 0 || port >= 65535) + throw new IllegalArgumentException("Bad port"); } /** * Creates 54-byte compact info * @throws IllegalArgumentException */ - private void initialize(NID nID, Hash hash, int port) { - if (port < 0 || port > 65535) + private void initialize() { + if (port <= 0 || port >= 65535) throw new IllegalArgumentException("Bad port"); byte[] compactInfo = new byte[LENGTH]; System.arraycopy(nID.getData(), 0, compactInfo, 0, NID.HASH_LENGTH); @@ -173,7 +209,24 @@ class NodeInfo extends SimpleDataStructure { } } + @Override public String toString() { return "NodeInfo: " + nID + ' ' + hash + " port: " + port; } + + /** + * To persistent storage string. + * Format: NID:Hash:Destination:port + * First 3 in base 64; Destination may be empty string + */ + public String toPersistentString() { + StringBuilder buf = new StringBuilder(650); + buf.append(nID.toBase64()).append(':'); + buf.append(hash.toBase64()).append(':'); + if (dest != null) + buf.append(dest.toBase64()); + buf.append(':').append(port); + return buf.toString(); + } + } diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/PersistDHT.java b/apps/i2psnark/java/src/org/klomp/snark/dht/PersistDHT.java new file mode 100644 index 000000000..dde495a04 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/PersistDHT.java @@ -0,0 +1,77 @@ +package org.klomp.snark.dht; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; + +import net.i2p.I2PAppContext; +import net.i2p.data.DataFormatException; +import net.i2p.util.Log; +import net.i2p.util.SecureFileOutputStream; + +/** + * Retrieve / Store the local DHT in a file + * + */ +abstract class PersistDHT { + + public static synchronized void loadDHT(KRPC krpc, File file) { + Log log = I2PAppContext.getGlobalContext().logManager().getLog(PersistDHT.class); + int count = 0; + FileInputStream in = null; + try { + in = new FileInputStream(file); + BufferedReader br = new BufferedReader(new InputStreamReader(in, "ISO-8859-1")); + String line = null; + while ( (line = br.readLine()) != null) { + if (line.startsWith("#")) + continue; + try { + krpc.addNode(new NodeInfo(line)); + count++; + // TODO limit number? this will flush the router's SDS caches + } catch (IllegalArgumentException iae) { + if (log.shouldLog(Log.WARN)) + log.warn("Error reading DHT entry", iae); + } catch (DataFormatException dfe) { + if (log.shouldLog(Log.WARN)) + log.warn("Error reading DHT entry", dfe); + } + } + } catch (IOException ioe) { + if (log.shouldLog(Log.WARN) && file.exists()) + log.warn("Error reading the DHT File", ioe); + } finally { + if (in != null) try { in.close(); } catch (IOException ioe) {} + } + if (log.shouldLog(Log.INFO)) + log.info("Loaded " + count + " nodes from " + file); + } + + public static synchronized void saveDHT(DHTNodes nodes, File file) { + Log log = I2PAppContext.getGlobalContext().logManager().getLog(PersistDHT.class); + int count = 0; + PrintWriter out = null; + try { + out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(file), "ISO-8859-1"))); + out.println("# DHT nodes, format is NID:Hash:Destination:port"); + for (NodeInfo ni : nodes.values()) { + // DHTNodes shouldn't contain us, if that changes check here + out.println(ni.toPersistentString()); + count++; + } + } catch (IOException ioe) { + if (log.shouldLog(Log.WARN)) + log.warn("Error writing the DHT File", ioe); + } finally { + if (out != null) out.close(); + } + if (log.shouldLog(Log.INFO)) + log.info("Stored " + count + " nodes to " + file); + } +} From 6a1b90f8f83c22d2c6841fbdddbc8ac4bbf50ee5 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 5 Jun 2012 01:03:39 +0000 Subject: [PATCH 09/23] hash caching --- .../java/src/org/klomp/snark/I2PSnarkUtil.java | 4 ++-- .../i2psnark/java/src/org/klomp/snark/dht/KRPC.java | 7 ++++--- .../java/src/org/klomp/snark/dht/NodeInfo.java | 13 ++++++------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java index 0d71dcd34..f44a30bf1 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java +++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java @@ -405,8 +405,8 @@ public class I2PSnarkUtil { if (sess != null) { byte[] b = Base32.decode(ip.substring(0, BASE32_HASH_LENGTH)); if (b != null) { - Hash h = new Hash(b); - //Hash h = Hash.create(b); + //Hash h = new Hash(b); + Hash h = Hash.create(b); if (_log.shouldLog(Log.INFO)) _log.info("Using existing session for lookup of " + ip); try { diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java index 2c44deb68..0ee799411 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java @@ -393,7 +393,8 @@ public class KRPC implements I2PSessionMuxedListener, DHT { public void announce(byte[] ih, byte[] peerHash) { InfoHash iHash = new InfoHash(ih); _tracker.announce(iHash, new Hash(peerHash)); -// _tracker.announce(iHash, Hash.create(peerHash)); + // Do NOT do this, corrupts the Hash cache and the Peer ID + //_tracker.announce(iHash, Hash.create(peerHash)); } /** @@ -1185,8 +1186,8 @@ public class KRPC implements I2PSessionMuxedListener, DHT { List rv = new ArrayList(peers.size()); for (BEValue bev : peers) { byte[] b = bev.getBytes(); - Hash h = new Hash(b); - //Hash h = Hash.create(b); + //Hash h = new Hash(b); + Hash h = Hash.create(b); rv.add(h); } if (_log.shouldLog(Log.INFO)) diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java index e4f52091b..8ff21efc4 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java @@ -101,8 +101,8 @@ class NodeInfo extends SimpleDataStructure { byte[] h = Base64.decode(parts[1]); if (h == null) throw new DataFormatException("Bad hash"); - hash = new Hash(h); - //hash = Hash.create(h); + //hash = new Hash(h); + hash = Hash.create(h); if (parts[2].length() > 0) dest = new Destination(parts[2]); try { @@ -123,11 +123,10 @@ class NodeInfo extends SimpleDataStructure { byte[] ndata = new byte[NID.HASH_LENGTH]; System.arraycopy(compactInfo, 0, ndata, 0, NID.HASH_LENGTH); this.nID = new NID(ndata); - //3 lines or... - byte[] hdata = new byte[Hash.HASH_LENGTH]; - System.arraycopy(compactInfo, NID.HASH_LENGTH, hdata, 0, Hash.HASH_LENGTH); - this.hash = new Hash(hdata); - //this.hash = Hash.create(compactInfo, NID.HASH_LENGTH); + //byte[] hdata = new byte[Hash.HASH_LENGTH]; + //System.arraycopy(compactInfo, NID.HASH_LENGTH, hdata, 0, Hash.HASH_LENGTH); + //this.hash = new Hash(hdata); + this.hash = Hash.create(compactInfo, NID.HASH_LENGTH); this.port = (int) DataHelper.fromLong(compactInfo, NID.HASH_LENGTH + Hash.HASH_LENGTH, 2); if (port <= 0 || port >= 65535) throw new IllegalArgumentException("Bad port"); From 41096c7f238c107a44c6c49977b7ea3225099d2e Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 12 Jun 2012 18:09:42 +0000 Subject: [PATCH 10/23] - Add heardAbout() and call for receive peers - Move last-seen tracking from NodeInfo to NID, add fail tracking - Make NodeInfo fields final - Remove nodes on consecutive failures - Only persist nodes heard from recently - Implement NID verification for security --- .../java/src/org/klomp/snark/dht/KRPC.java | 76 +++++++++---------- .../java/src/org/klomp/snark/dht/NID.java | 22 ++++++ .../src/org/klomp/snark/dht/NodeInfo.java | 65 +++++++--------- .../src/org/klomp/snark/dht/PersistDHT.java | 7 +- 4 files changed, 92 insertions(+), 78 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java index 0ee799411..cadb350e5 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java @@ -124,6 +124,8 @@ public class KRPC implements I2PSessionMuxedListener, DHT { private static final int REPLY_PEERS = 2; private static final int REPLY_NODES = 3; + public static final boolean SECURE_NID = true; + /** how long since last heard from do we delete - BEP 5 says 15 minutes */ private static final long MAX_NODEINFO_AGE = 60*60*1000; /** how long since generated do we delete - BEP 5 says 10 minutes */ @@ -155,7 +157,10 @@ public class KRPC implements I2PSessionMuxedListener, DHT { _qPort = 2555 + ctx.random().nextInt(61111); _rPort = _qPort + 1; _myID = new byte[NID.HASH_LENGTH]; - ctx.random().nextBytes(_myID); + if (SECURE_NID) + System.arraycopy(session.getMyDestination().calculateHash().getData(), 0, _myID, 0, NID.HASH_LENGTH); + else + ctx.random().nextBytes(_myID); _myNID = new NID(_myID); _myNodeInfo = new NodeInfo(_myNID, session.getMyDestination(), _qPort); _dhtFile = new File(ctx.getConfigDir(), DHT_FILE); @@ -167,29 +172,6 @@ public class KRPC implements I2PSessionMuxedListener, DHT { ///////////////// Public methods - /** - * For bootstrapping if loaded from config file. - * @param when when did we hear from them - */ - public void addNode(NodeInfo nInfo, long when) { - heardFrom(nInfo, when); - } - - /** - * NodeInfo heard from - */ - public void addNode(NodeInfo nInfo) { - heardFrom(nInfo); - } - - /** - * For saving in a config file. - * @return the values, not a copy, could change, use an iterator - */ - public Collection getNodes() { - return _knownNodes.values(); - } - /** * @return The UDP query port */ @@ -989,14 +971,6 @@ public class KRPC implements I2PSessionMuxedListener, DHT { * @return old NodeInfo or nInfo if none, use this to reduce object churn */ private NodeInfo heardFrom(NodeInfo nInfo) { - return heardFrom(nInfo, _context.clock().now()); - } - - /** - * Used for initialization - * @return old NodeInfo or nInfo if none, use this to reduce object churn - */ - private NodeInfo heardFrom(NodeInfo nInfo, long when) { // try to keep ourselves out of the DHT if (nInfo.equals(_myNodeInfo)) return _myNodeInfo; @@ -1013,11 +987,39 @@ public class KRPC implements I2PSessionMuxedListener, DHT { if (oldInfo.getDestination() == null && nInfo.getDestination() != null) oldInfo.setDestination(nInfo.getDestination()); } - if (when > oldInfo.lastSeen()) - oldInfo.setLastSeen(when); + oldInfo.getNID().setLastSeen(); return oldInfo; } + /** + * Called for bootstrap or for all nodes in a receiveNodes reply. + * Package private for PersistDHT. + * @return non-null nodeInfo from DB if present, otherwise the nInfo parameter is returned + */ + NodeInfo heardAbout(NodeInfo nInfo) { + // try to keep ourselves out of the DHT + if (nInfo.equals(_myNodeInfo)) + return _myNodeInfo; + NID nID = nInfo.getNID(); + NodeInfo rv = _knownNodes.putIfAbsent(nID, nInfo); + if (rv == null) + rv = nInfo; + return rv; + } + + /** + * Called when a reply times out + */ + private void timeout(NodeInfo nInfo) { + boolean remove = nInfo.getNID().timeout(); + if (remove) { + if (_knownNodes.remove(nInfo) != null) { + if (_log.shouldLog(Log.INFO)) + _log.info("Removed after consecutive timeouts: " + nInfo); + } + } + } + /** * Handle and respond to the query */ @@ -1163,12 +1165,9 @@ public class KRPC implements I2PSessionMuxedListener, DHT { */ private List receiveNodes(NodeInfo nInfo, byte[] ids) throws InvalidBEncodingException { List rv = new ArrayList(ids.length / NodeInfo.LENGTH); - long fakeTime = _context.clock().now() - (MAX_NODEINFO_AGE * 3 / 4); for (int off = 0; off < ids.length; off += NodeInfo.LENGTH) { NodeInfo nInf = new NodeInfo(ids, off); - // anti-churn - // TODO do we need heardAbout too? - nInf = heardFrom(nInf, fakeTime); + nInf = heardAbout(nInf); rv.add(nInf); } if (_log.shouldLog(Log.INFO)) @@ -1315,6 +1314,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { _sentQueries.remove(mid); if (onTimeout != null) onTimeout.run(); + timeout(ReplyWaiter.this); if (_log.shouldLog(Log.INFO)) _log.warn("timeout waiting for reply from " + ReplyWaiter.this.toString()); } diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/NID.java b/apps/i2psnark/java/src/org/klomp/snark/dht/NID.java index 87407720b..0543009da 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/NID.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/NID.java @@ -4,6 +4,7 @@ package org.klomp.snark.dht; */ import net.i2p.crypto.SHA1Hash; +import net.i2p.util.Clock; /** * A 20-byte peer ID, used as a Map key in lots of places. @@ -13,7 +14,28 @@ import net.i2p.crypto.SHA1Hash; */ class NID extends SHA1Hash { + private long lastSeen; + private int fails; + + private static final int MAX_FAILS = 3; + public NID(byte[] data) { super(data); } + + public long lastSeen() { + return lastSeen; + } + + public void setLastSeen() { + lastSeen = Clock.getInstance().now(); + fails = 0; + } + + /** + * @return if more than max timeouts + */ + public boolean timeout() { + return fails++ > MAX_FAILS; + } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java index 8ff21efc4..34e47752e 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java @@ -25,11 +25,10 @@ import net.i2p.data.SimpleDataStructure; class NodeInfo extends SimpleDataStructure { - private long lastSeen; - private NID nID; - private Hash hash; + private final NID nID; + private final Hash hash; private Destination dest; - private int port; + private final int port; public static final int LENGTH = NID.HASH_LENGTH + Hash.HASH_LENGTH + 2; @@ -44,6 +43,7 @@ class NodeInfo extends SimpleDataStructure { this.hash = dest.calculateHash(); this.port = port; initialize(); + verify(); } /** @@ -56,16 +56,7 @@ class NodeInfo extends SimpleDataStructure { this.hash = hash; this.port = port; initialize(); - } - - /** - * No Destination yet available - * @param compactInfo 20 byte node ID, 32 byte destHash, 2 byte port - * @throws IllegalArgumentException - */ - public NodeInfo(byte[] compactInfo) { - super(compactInfo); - initialize(compactInfo); + verify(); } /** @@ -80,11 +71,18 @@ class NodeInfo extends SimpleDataStructure { byte[] d = new byte[LENGTH]; System.arraycopy(compactInfo, offset, d, 0, LENGTH); setData(d); - initialize(d); + byte[] ndata = new byte[NID.HASH_LENGTH]; + System.arraycopy(d, 0, ndata, 0, NID.HASH_LENGTH); + this.nID = new NID(ndata); + this.hash = Hash.create(d, NID.HASH_LENGTH); + this.port = (int) DataHelper.fromLong(d, NID.HASH_LENGTH + Hash.HASH_LENGTH, 2); + if (port <= 0 || port >= 65535) + throw new IllegalArgumentException("Bad port"); + verify(); } /** - * Form persistent storage string. + * Create from persistent storage string. * Format: NID:Hash:Destination:port * First 3 in base 64; Destination may be empty string * @throws IllegalArgumentException @@ -113,24 +111,6 @@ class NodeInfo extends SimpleDataStructure { initialize(); } - /** - * Creates data structures from the compact info - * @throws IllegalArgumentException - */ - private void initialize(byte[] compactInfo) { - if (compactInfo.length != LENGTH) - throw new IllegalArgumentException("Bad compact info length"); - byte[] ndata = new byte[NID.HASH_LENGTH]; - System.arraycopy(compactInfo, 0, ndata, 0, NID.HASH_LENGTH); - this.nID = new NID(ndata); - //byte[] hdata = new byte[Hash.HASH_LENGTH]; - //System.arraycopy(compactInfo, NID.HASH_LENGTH, hdata, 0, Hash.HASH_LENGTH); - //this.hash = new Hash(hdata); - this.hash = Hash.create(compactInfo, NID.HASH_LENGTH); - this.port = (int) DataHelper.fromLong(compactInfo, NID.HASH_LENGTH + Hash.HASH_LENGTH, 2); - if (port <= 0 || port >= 65535) - throw new IllegalArgumentException("Bad port"); - } /** * Creates 54-byte compact info @@ -146,6 +126,17 @@ class NodeInfo extends SimpleDataStructure { setData(compactInfo); } + /** + * Verify the NID matches the Hash + * @throws IllegalArgumentException + */ + private void verify() { + if (!KRPC.SECURE_NID) + return; + if (!DataHelper.eq(nID.getData(), 0, hash.getData(), 0, NID.HASH_LENGTH)) + throw new IllegalArgumentException("NID/Hash mismatch"); + } + public int length() { return LENGTH; } @@ -185,11 +176,7 @@ class NodeInfo extends SimpleDataStructure { } public long lastSeen() { - return lastSeen; - } - - public void setLastSeen(long now) { - lastSeen = now; + return nID.lastSeen(); } @Override diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/PersistDHT.java b/apps/i2psnark/java/src/org/klomp/snark/dht/PersistDHT.java index dde495a04..730137caa 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/PersistDHT.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/PersistDHT.java @@ -20,6 +20,8 @@ import net.i2p.util.SecureFileOutputStream; */ abstract class PersistDHT { + private static final long MAX_AGE = 60*60*1000; + public static synchronized void loadDHT(KRPC krpc, File file) { Log log = I2PAppContext.getGlobalContext().logManager().getLog(PersistDHT.class); int count = 0; @@ -32,7 +34,7 @@ abstract class PersistDHT { if (line.startsWith("#")) continue; try { - krpc.addNode(new NodeInfo(line)); + krpc.heardAbout(new NodeInfo(line)); count++; // TODO limit number? this will flush the router's SDS caches } catch (IllegalArgumentException iae) { @@ -56,11 +58,14 @@ abstract class PersistDHT { public static synchronized void saveDHT(DHTNodes nodes, File file) { Log log = I2PAppContext.getGlobalContext().logManager().getLog(PersistDHT.class); int count = 0; + long maxAge = I2PAppContext.getGlobalContext().clock().now() - MAX_AGE; PrintWriter out = null; try { out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(file), "ISO-8859-1"))); out.println("# DHT nodes, format is NID:Hash:Destination:port"); for (NodeInfo ni : nodes.values()) { + if (ni.lastSeen() < maxAge) + continue; // DHTNodes shouldn't contain us, if that changes check here out.println(ni.toPersistentString()); count++; From d0b967388a92bcc2e509dc8fa9b103c520787357 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 12 Jun 2012 18:30:58 +0000 Subject: [PATCH 11/23] rework DHTNodes to hide the CHM implementation, in prep for real Kad --- .../src/org/klomp/snark/dht/DHTNodes.java | 39 +++++++++++++++++-- .../java/src/org/klomp/snark/dht/KRPC.java | 10 ++--- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java b/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java index e18fca6fe..bca56bc6e 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java @@ -4,6 +4,7 @@ package org.klomp.snark.dht; */ import java.util.ArrayList; +import java.util.Collection; import java.util.Comparator; import java.util.Iterator; import java.util.List; @@ -27,11 +28,12 @@ import net.i2p.util.SimpleTimer2; * @since 0.8.4 * @author zzz */ -class DHTNodes extends ConcurrentHashMap { +class DHTNodes { private final I2PAppContext _context; private long _expireTime; private final Log _log; + private final ConcurrentHashMap _nodeMap; private volatile boolean _isRunning; /** stagger with other cleaners */ @@ -42,10 +44,10 @@ class DHTNodes extends ConcurrentHashMap { private static final int MAX_PEERS = 999; public DHTNodes(I2PAppContext ctx) { - super(); _context = ctx; _expireTime = MAX_EXPIRE_TIME; _log = _context.logManager().getLog(DHTNodes.class); + _nodeMap = new ConcurrentHashMap(); } public void start() { @@ -58,6 +60,37 @@ class DHTNodes extends ConcurrentHashMap { _isRunning = false; } + // begin ConcurrentHashMap methods + + public int size() { + return _nodeMap.size(); + } + + public void clear() { + _nodeMap.clear(); + } + + public NodeInfo get(NID nid) { + return _nodeMap.get(nid); + } + + /** + * @return the old value if present, else nInfo + */ + public NodeInfo putIfAbsent(NodeInfo nInfo) { + return _nodeMap.putIfAbsent(nInfo.getNID(), nInfo); + } + + public NodeInfo remove(NID nid) { + return _nodeMap.remove(nid); + } + + public Collection values() { + return _nodeMap.values(); + } + + // end ConcurrentHashMap methods + /** * Fake DHT * @param sha1 either a InfoHash or a NID @@ -65,7 +98,7 @@ class DHTNodes extends ConcurrentHashMap { List findClosest(SHA1Hash h, int numWant) { // sort the whole thing Set all = new TreeSet(new SHA1Comparator(h)); - all.addAll(keySet()); + all.addAll(_nodeMap.keySet()); int sz = all.size(); int max = Math.min(numWant, sz); diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java index cadb350e5..e8743350e 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java @@ -980,7 +980,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { if (_log.shouldLog(Log.INFO)) _log.info("Adding node: " + nInfo); oldInfo = nInfo; - NodeInfo nInfo2 = _knownNodes.putIfAbsent(nID, nInfo); + NodeInfo nInfo2 = _knownNodes.putIfAbsent(nInfo); if (nInfo2 != null) oldInfo = nInfo2; } else { @@ -1000,8 +1000,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { // try to keep ourselves out of the DHT if (nInfo.equals(_myNodeInfo)) return _myNodeInfo; - NID nID = nInfo.getNID(); - NodeInfo rv = _knownNodes.putIfAbsent(nID, nInfo); + NodeInfo rv = _knownNodes.putIfAbsent(nInfo); if (rv == null) rv = nInfo; return rv; @@ -1011,9 +1010,10 @@ public class KRPC implements I2PSessionMuxedListener, DHT { * Called when a reply times out */ private void timeout(NodeInfo nInfo) { - boolean remove = nInfo.getNID().timeout(); + NID nid = nInfo.getNID(); + boolean remove = nid.timeout(); if (remove) { - if (_knownNodes.remove(nInfo) != null) { + if (_knownNodes.remove(nid) != null) { if (_log.shouldLog(Log.INFO)) _log.info("Removed after consecutive timeouts: " + nInfo); } From 44da37f00915c4cc47e57fe1554d3ddd6ec8bb72 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 12 Jun 2012 19:22:31 +0000 Subject: [PATCH 12/23] - Timeout if can't find b32 - Refactor ReplyWaiter --- .../java/src/org/klomp/snark/dht/KRPC.java | 48 +++++++++---------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java index e8743350e..771ccf155 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java @@ -703,6 +703,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { if (!lookupDest(nInfo)) { if (_log.shouldLog(Log.WARN)) _log.warn("Dropping repliable query, no dest for " + nInfo); + timeout(nInfo); return null; } } @@ -1118,7 +1119,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { * @throws NPE, IllegalArgumentException, and others too */ private void receiveResponse(ReplyWaiter waiter, Map response) throws InvalidBEncodingException { - NodeInfo nInfo = waiter; + NodeInfo nInfo = waiter.getSentTo(); BEValue nodes = response.get("nodes"); BEValue values = response.get("values"); @@ -1229,11 +1230,11 @@ public class KRPC implements I2PSessionMuxedListener, DHT { /** * Callback for replies */ - private class ReplyWaiter extends NodeInfo { + private class ReplyWaiter extends SimpleTimer2.TimedEvent { private final MsgID mid; + private final NodeInfo sentTo; private final Runnable onReply; private final Runnable onTimeout; - private final SimpleTimer2.TimedEvent event; private int replyCode; private Object sentObject; private Object replyObject; @@ -1246,14 +1247,15 @@ public class KRPC implements I2PSessionMuxedListener, DHT { * @param onTimeout must be fast, otherwise set to null and wait on this */ public ReplyWaiter(MsgID mID, NodeInfo nInfo, Runnable onReply, Runnable onTimeout) { - super(nInfo.getNID(), nInfo.getHash(), nInfo.getPort()); - Destination dest = nInfo.getDestination(); - if (dest != null) - setDestination(dest); + super(SimpleTimer2.getInstance(), DEFAULT_QUERY_TIMEOUT); this.mid = mID; + this.sentTo = nInfo; this.onReply = onReply; this.onTimeout = onTimeout; - this.event = new Event(); + } + + public NodeInfo getSentTo() { + return sentTo; } /** only used for get_peers, to save the Info Hash */ @@ -1266,7 +1268,6 @@ public class KRPC implements I2PSessionMuxedListener, DHT { return sentObject; } - /** * Should contain null if getReplyCode is REPLY_PONG. * Should contain List if getReplyCode is REPLY_PEERS. @@ -1291,13 +1292,13 @@ public class KRPC implements I2PSessionMuxedListener, DHT { * Also removes from _sentQueries and calls heardFrom(). */ public void gotReply(int code, Object o) { + cancel(); + _sentQueries.remove(mid); replyCode = code; replyObject = o; - event.cancel(); - _sentQueries.remove(mid); // if it is fake, heardFrom is called by receivePong() - if (!getNID().equals(_fakeNID)) - heardFrom(this); + if (!sentTo.getNID().equals(_fakeNID)) + heardFrom(sentTo); if (onReply != null) onReply.run(); synchronized(this) { @@ -1305,19 +1306,14 @@ public class KRPC implements I2PSessionMuxedListener, DHT { } } - private class Event extends SimpleTimer2.TimedEvent { - public Event() { - super(SimpleTimer2.getInstance(), DEFAULT_QUERY_TIMEOUT); - } - - public void timeReached() { - _sentQueries.remove(mid); - if (onTimeout != null) - onTimeout.run(); - timeout(ReplyWaiter.this); - if (_log.shouldLog(Log.INFO)) - _log.warn("timeout waiting for reply from " + ReplyWaiter.this.toString()); - } + /** timer callback on timeout */ + public void timeReached() { + _sentQueries.remove(mid); + if (onTimeout != null) + onTimeout.run(); + timeout(sentTo); + if (_log.shouldLog(Log.INFO)) + _log.warn("timeout waiting for reply from " + sentTo); } } From c2137a2a80b0373fc090a43dd56129647186974c Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 12 Jun 2012 21:38:25 +0000 Subject: [PATCH 13/23] - Add explore thread - More checks for stopping - Add xor of port to secure NID --- .../java/src/org/klomp/snark/dht/KRPC.java | 70 +++++++++++++++---- .../src/org/klomp/snark/dht/NodeInfo.java | 30 +++++++- 2 files changed, 87 insertions(+), 13 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java index 771ccf155..63a010e0d 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java @@ -36,6 +36,7 @@ import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.data.SimpleDataStructure; +import net.i2p.util.I2PAppThread; import net.i2p.util.Log; import net.i2p.util.SimpleTimer2; @@ -111,7 +112,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { private volatile boolean _isRunning; /** all-zero NID used for pings */ - private static final NID _fakeNID = new NID(new byte[NID.HASH_LENGTH]); + public static final NID FAKE_NID = new NID(new byte[NID.HASH_LENGTH]); /** Max number of nodes to return. BEP 5 says 8 */ private static final int K = 8; @@ -137,6 +138,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { private static final long DEFAULT_QUERY_TIMEOUT = 75*1000; /** stagger with other cleaners */ private static final long CLEAN_TIME = 63*1000; + private static final long EXPLORE_TIME = 877*1000; private static final String DHT_FILE = "i2psnark.dht.dat"; public KRPC (I2PAppContext ctx, I2PSession session) { @@ -156,12 +158,14 @@ public class KRPC implements I2PSessionMuxedListener, DHT { // If we add a search DHT, adjust to stay out of each other's way _qPort = 2555 + ctx.random().nextInt(61111); _rPort = _qPort + 1; - _myID = new byte[NID.HASH_LENGTH]; - if (SECURE_NID) - System.arraycopy(session.getMyDestination().calculateHash().getData(), 0, _myID, 0, NID.HASH_LENGTH); - else + if (SECURE_NID) { + _myNID = NodeInfo.generateNID(session.getMyDestination().calculateHash(), _qPort); + _myID = _myNID.getData(); + } else { + _myID = new byte[NID.HASH_LENGTH]; ctx.random().nextBytes(_myID); - _myNID = new NID(_myID); + _myNID = new NID(_myID); + } _myNodeInfo = new NodeInfo(_myNID, session.getMyDestination(), _qPort); _dhtFile = new File(ctx.getConfigDir(), DHT_FILE); @@ -193,7 +197,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { * If and when the pong is received the node will be inserted in our DHT. */ public void ping(Destination dest, int port) { - NodeInfo nInfo = new NodeInfo(_fakeNID, dest, port); + NodeInfo nInfo = new NodeInfo(dest, port); sendPing(nInfo); } @@ -222,6 +226,8 @@ public class KRPC implements I2PSessionMuxedListener, DHT { if (_log.shouldLog(Log.INFO)) _log.info("Starting explore"); for (int i = 0; i < maxNodes; i++) { + if (!_isRunning) + break; NodeInfo nInfo; try { nInfo = toTry.first(); @@ -302,6 +308,8 @@ public class KRPC implements I2PSessionMuxedListener, DHT { if (_log.shouldLog(Log.INFO)) _log.info("Starting getPeers with " + nodes.size() + " to try"); for (int i = 0; i < max; i++) { + if (!_isRunning) + break; NodeInfo nInfo; try { nInfo = toTry.first(); @@ -414,6 +422,8 @@ public class KRPC implements I2PSessionMuxedListener, DHT { if (_log.shouldLog(Log.INFO)) _log.info("Found " + nodes.size() + " to announce to for " + iHash); for (NodeInfo nInfo : nodes) { + if (!_isRunning) + break; if (announce(ih, nInfo, Math.min(maxWait, 60*1000))) rv++; maxWait -= _context.clock().now() - start; @@ -503,6 +513,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { _isRunning = true; // no need to keep ref, it will eventually stop new Cleaner(); + new Explorer(5*1000); } /** @@ -1200,7 +1211,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { * replace it with the received NID. */ private void receivePong(NodeInfo nInfo, byte[] nid) { - if (nInfo.getNID().equals(_fakeNID)) { + if (nInfo.getNID().equals(FAKE_NID)) { NodeInfo newInfo = new NodeInfo(new NID(nid), nInfo.getHash(), nInfo.getPort()); Destination dest = nInfo.getDestination(); if (dest != null) @@ -1243,8 +1254,8 @@ public class KRPC implements I2PSessionMuxedListener, DHT { * Either wait on this object with a timeout, or use non-null Runnables. * Any sent data to be remembered may be stored by setSentObject(). * Reply object may be in getReplyObject(). - * @param onReply must be fast, otherwise set to null and wait on this - * @param onTimeout must be fast, otherwise set to null and wait on this + * @param onReply must be fast, otherwise set to null and wait on this UNUSED + * @param onTimeout must be fast, otherwise set to null and wait on this UNUSED */ public ReplyWaiter(MsgID mID, NodeInfo nInfo, Runnable onReply, Runnable onTimeout) { super(SimpleTimer2.getInstance(), DEFAULT_QUERY_TIMEOUT); @@ -1294,10 +1305,10 @@ public class KRPC implements I2PSessionMuxedListener, DHT { public void gotReply(int code, Object o) { cancel(); _sentQueries.remove(mid); - replyCode = code; replyObject = o; + replyCode = code; // if it is fake, heardFrom is called by receivePong() - if (!sentTo.getNID().equals(_fakeNID)) + if (!sentTo.getNID().equals(FAKE_NID)) heardFrom(sentTo); if (onReply != null) onReply.run(); @@ -1314,6 +1325,9 @@ public class KRPC implements I2PSessionMuxedListener, DHT { timeout(sentTo); if (_log.shouldLog(Log.INFO)) _log.warn("timeout waiting for reply from " + sentTo); + synchronized(this) { + this.notifyAll(); + } } } @@ -1414,4 +1428,36 @@ public class KRPC implements I2PSessionMuxedListener, DHT { schedule(CLEAN_TIME); } } + + /** + * Fire off explorer thread + */ + private class Explorer extends SimpleTimer2.TimedEvent { + + public Explorer(long delay) { + super(SimpleTimer2.getInstance(), delay); + } + + public void timeReached() { + if (!_isRunning) + return; + (new I2PAppThread(new ExplorerThread(), "DHT Explore", true)).start(); + } + } + + /** + * explorer thread + */ + private class ExplorerThread implements Runnable { + + public void run() { + if (!_isRunning) + return; + explore(8, 60*1000, 1); + // refresh the buckets here too + if (!_isRunning) + return; + new Explorer(EXPLORE_TIME); + } + } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java index 34e47752e..211439f20 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java @@ -32,6 +32,18 @@ class NodeInfo extends SimpleDataStructure { public static final int LENGTH = NID.HASH_LENGTH + Hash.HASH_LENGTH + 2; + /** + * With a fake NID used for pings + */ + public NodeInfo(Destination dest, int port) { + super(); + this.nID = KRPC.FAKE_NID; + this.dest = dest; + this.hash = dest.calculateHash(); + this.port = port; + initialize(); + } + /** * Use this if we have the full destination * @throws IllegalArgumentException @@ -126,6 +138,18 @@ class NodeInfo extends SimpleDataStructure { setData(compactInfo); } + /** + * Generate a secure NID that matches the Hash and port + * @throws IllegalArgumentException + */ + public static NID generateNID(Hash h, int p) { + byte[] n = new byte[NID.HASH_LENGTH]; + System.arraycopy(h.getData(), 0, n, 0, NID.HASH_LENGTH); + n[0] ^= (byte) (p >> 8); + n[1] ^= (byte) p; + return new NID(n); + } + /** * Verify the NID matches the Hash * @throws IllegalArgumentException @@ -133,7 +157,11 @@ class NodeInfo extends SimpleDataStructure { private void verify() { if (!KRPC.SECURE_NID) return; - if (!DataHelper.eq(nID.getData(), 0, hash.getData(), 0, NID.HASH_LENGTH)) + byte[] nb = nID.getData(); + byte[] hb = hash.getData(); + if ((!DataHelper.eq(nb, 2, hb, 2, NID.HASH_LENGTH - 2)) || + ((nb[0] ^ (port >> 8)) & 0xff) != (hb[0] & 0xff) || + ((nb[1] ^ port) & 0xff) != (hb[1] & 0xff)) throw new IllegalArgumentException("NID/Hash mismatch"); } From 8522779df1e252bed2590a0bbaf1195495f0accb Mon Sep 17 00:00:00 2001 From: zzz Date: Fri, 22 Jun 2012 15:12:43 +0000 Subject: [PATCH 14/23] - Switch to real kad with lib from i2p.zzz.kademlia (not checked in yet) - Bootstrap only once in explore thread - Add exploring to explore thread - Don't store default DHT setting in config file, so we can switch default to true later - Add new enforce-protocol streaming config, sorry locks out < 0.7.1 peers - Log tweaks --- .../src/org/klomp/snark/I2PSnarkUtil.java | 2 + .../src/org/klomp/snark/SnarkManager.java | 9 ++- .../src/org/klomp/snark/dht/DHTNodes.java | 62 ++++++++++--------- .../java/src/org/klomp/snark/dht/KRPC.java | 43 ++++++++----- .../java/src/org/klomp/snark/dht/NID.java | 7 ++- .../src/org/klomp/snark/dht/NodeInfo.java | 2 +- 6 files changed, 76 insertions(+), 49 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java index 23d7ed3c7..0b3de3523 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java +++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java @@ -240,6 +240,8 @@ public class I2PSnarkUtil { opts.setProperty("i2p.streaming.maxTotalConnsPerMinute", "8"); if (opts.getProperty("i2p.streaming.maxConnsPerHour") == null) opts.setProperty("i2p.streaming.maxConnsPerHour", "20"); + if (opts.getProperty("i2p.streaming.enforceProtocol") == null) + opts.setProperty("i2p.streaming.enforceProtocol", "true"); _manager = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, opts); _connecting = false; } diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index ed14a83a4..b4344321e 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -289,8 +289,9 @@ public class SnarkManager implements Snark.CompleteListener { _config.setProperty(PROP_STARTUP_DELAY, Integer.toString(DEFAULT_STARTUP_DELAY)); if (!_config.containsKey(PROP_THEME)) _config.setProperty(PROP_THEME, DEFAULT_THEME); - if (!_config.containsKey(PROP_USE_DHT)) - _config.setProperty(PROP_USE_DHT, Boolean.toString(I2PSnarkUtil.DEFAULT_USE_DHT)); + // no, so we can switch default to true later + //if (!_config.containsKey(PROP_USE_DHT)) + // _config.setProperty(PROP_USE_DHT, Boolean.toString(I2PSnarkUtil.DEFAULT_USE_DHT)); updateConfig(); } /** @@ -365,7 +366,9 @@ public class SnarkManager implements Snark.CompleteListener { String useOT = _config.getProperty(PROP_USE_OPENTRACKERS); boolean bOT = useOT == null || Boolean.valueOf(useOT).booleanValue(); _util.setUseOpenTrackers(bOT); - _util.setUseDHT(Boolean.valueOf(_config.getProperty(PROP_USE_DHT)).booleanValue()); + // careful, so we can switch default to true later + _util.setUseDHT(Boolean.valueOf(_config.getProperty(PROP_USE_DHT, + Boolean.toString(I2PSnarkUtil.DEFAULT_USE_DHT))).booleanValue()); getDataDir().mkdirs(); initTrackerMap(); } diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java b/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java index bca56bc6e..4dd704062 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java @@ -15,15 +15,15 @@ import java.util.concurrent.ConcurrentHashMap; import net.i2p.I2PAppContext; import net.i2p.crypto.SHA1Hash; import net.i2p.data.DataHelper; +import net.i2p.kademlia.KBucketSet; import net.i2p.util.Log; import net.i2p.util.SimpleTimer2; /** * All the nodes we know about, stored as a mapping from * node ID to a Destination and Port. - * Also uses the keySet as a subsitute for kbuckets. * - * Swap this out for a real DHT later. + * And a real Kademlia routing table, which stores node IDs only. * * @since 0.8.4 * @author zzz @@ -34,6 +34,7 @@ class DHTNodes { private long _expireTime; private final Log _log; private final ConcurrentHashMap _nodeMap; + private final KBucketSet _kad; private volatile boolean _isRunning; /** stagger with other cleaners */ @@ -43,11 +44,12 @@ class DHTNodes { private static final long DELTA_EXPIRE_TIME = 7*60*1000; private static final int MAX_PEERS = 999; - public DHTNodes(I2PAppContext ctx) { + public DHTNodes(I2PAppContext ctx, NID me) { _context = ctx; _expireTime = MAX_EXPIRE_TIME; _log = _context.logManager().getLog(DHTNodes.class); _nodeMap = new ConcurrentHashMap(); + _kad = new KBucketSet(ctx, me, 8, 1); } public void start() { @@ -67,6 +69,7 @@ class DHTNodes { } public void clear() { + _kad.clear(); _nodeMap.clear(); } @@ -75,13 +78,15 @@ class DHTNodes { } /** - * @return the old value if present, else nInfo + * @return the old value if present, else null */ public NodeInfo putIfAbsent(NodeInfo nInfo) { + _kad.add(nInfo.getNID()); return _nodeMap.putIfAbsent(nInfo.getNID(), nInfo); } public NodeInfo remove(NID nid) { + _kad.remove(nid); return _nodeMap.remove(nid); } @@ -92,36 +97,31 @@ class DHTNodes { // end ConcurrentHashMap methods /** - * Fake DHT + * DHT * @param sha1 either a InfoHash or a NID */ - List findClosest(SHA1Hash h, int numWant) { - // sort the whole thing - Set all = new TreeSet(new SHA1Comparator(h)); - all.addAll(_nodeMap.keySet()); - int sz = all.size(); - int max = Math.min(numWant, sz); - - // return the first ones - List rv = new ArrayList(max); - int count = 0; - for (NID nid : all) { - if (count++ >= max) - break; - NodeInfo nInfo = get(nid); - if (nInfo == null) - continue; - rv.add(nInfo); + public List findClosest(SHA1Hash h, int numWant) { + NID key; + if (h instanceof NID) + key = (NID) h; + else + key = new NID(h.getData()); + List keys = _kad.getClosest(key, numWant); + List rv = new ArrayList(keys.size()); + for (NID nid : keys) { + NodeInfo ninfo = _nodeMap.get(nid); + if (ninfo != null) + rv.add(ninfo); } return rv; } - /**** used CHM methods to be replaced: - public Collection values() {} - public NodeInfo get(NID nID) {} - public NodeInfo putIfAbssent(NID nID, NodeInfo nInfo) {} - public int size() {} - ****/ + /** + * DHT - get random keys to explore + */ + public List getExploreKeys() { + return _kad.getExploreKeys(15*60*1000); + } /** */ private class Cleaner extends SimpleTimer2.TimedEvent { @@ -137,10 +137,12 @@ class DHTNodes { int peerCount = 0; for (Iterator iter = DHTNodes.this.values().iterator(); iter.hasNext(); ) { NodeInfo peer = iter.next(); - if (peer.lastSeen() < now - _expireTime) + if (peer.lastSeen() < now - _expireTime) { iter.remove(); - else + _kad.remove(peer.getNID()); + } else { peerCount++; + } } if (peerCount > MAX_PEERS) diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java index 63a010e0d..a224b1416 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java @@ -110,6 +110,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { private final int _qPort; private final File _dhtFile; private volatile boolean _isRunning; + private volatile boolean _hasBootstrapped; /** all-zero NID used for pings */ public static final NID FAKE_NID = new NID(new byte[NID.HASH_LENGTH]); @@ -147,8 +148,6 @@ public class KRPC implements I2PSessionMuxedListener, DHT { _log = ctx.logManager().getLog(KRPC.class); _tracker = new DHTTracker(ctx); - // in place of a DHT, store everybody we hear from for now - _knownNodes = new DHTNodes(ctx); _sentQueries = new ConcurrentHashMap(); _outgoingTokens = new ConcurrentHashMap(); _incomingTokens = new ConcurrentHashMap(); @@ -168,6 +167,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { } _myNodeInfo = new NodeInfo(_myNID, session.getMyDestination(), _qPort); _dhtFile = new File(ctx.getConfigDir(), DHT_FILE); + _knownNodes = new DHTNodes(ctx, _myNID); session.addMuxedSessionListener(this, I2PSession.PROTO_DATAGRAM_RAW, _rPort); session.addMuxedSessionListener(this, I2PSession.PROTO_DATAGRAM, _qPort); @@ -206,25 +206,24 @@ public class KRPC implements I2PSessionMuxedListener, DHT { * Blocking! * This is almost the same as getPeers() * + * @param target the key we are searching for * @param maxNodes how many to contact * @param maxWait how long to wait for each to reply (not total) must be > 0 * @param parallel how many outstanding at once (unimplemented, always 1) */ - public void explore(int maxNodes, long maxWait, int parallel) { - // Initial set to try, will get added to as we go - NID myNID = _myNodeInfo.getNID(); - List nodes = _knownNodes.findClosest(myNID, maxNodes); + private void explore(NID target, int maxNodes, long maxWait, int parallel) { + List nodes = _knownNodes.findClosest(target, maxNodes); if (nodes.isEmpty()) { if (_log.shouldLog(Log.WARN)) _log.info("DHT is empty, cannot explore"); return; } - SortedSet toTry = new TreeSet(new NodeInfoComparator(myNID)); + SortedSet toTry = new TreeSet(new NodeInfoComparator(target)); toTry.addAll(nodes); Set tried = new HashSet(); if (_log.shouldLog(Log.INFO)) - _log.info("Starting explore"); + _log.info("Starting explore of " + target); for (int i = 0; i < maxNodes; i++) { if (!_isRunning) break; @@ -237,8 +236,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { toTry.remove(nInfo); tried.add(nInfo); - // this isn't going to work, he will just return our own? - ReplyWaiter waiter = sendFindNode(nInfo, _myNID); + ReplyWaiter waiter = sendFindNode(nInfo, target); if (waiter == null) continue; synchronized(waiter) { @@ -266,7 +264,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { } } if (_log.shouldLog(Log.INFO)) - _log.info("Finished explore"); + _log.info("Finished explore of " + target); } /** @@ -1441,7 +1439,10 @@ public class KRPC implements I2PSessionMuxedListener, DHT { public void timeReached() { if (!_isRunning) return; - (new I2PAppThread(new ExplorerThread(), "DHT Explore", true)).start(); + if (_knownNodes.size() > 0) + (new I2PAppThread(new ExplorerThread(), "DHT Explore", true)).start(); + else + schedule(60*1000); } } @@ -1453,10 +1454,24 @@ public class KRPC implements I2PSessionMuxedListener, DHT { public void run() { if (!_isRunning) return; - explore(8, 60*1000, 1); - // refresh the buckets here too + if (!_hasBootstrapped) { + if (_log.shouldLog(Log.INFO)) + _log.info("Bootstrap start size: " + _knownNodes.size()); + explore(_myNID, 8, 60*1000, 1); + if (_log.shouldLog(Log.INFO)) + _log.info("Bootstrap done size: " + _knownNodes.size()); + _hasBootstrapped = true; + } if (!_isRunning) return; + if (_log.shouldLog(Log.INFO)) + _log.info("Explore start size: " + _knownNodes.size()); + List keys = _knownNodes.getExploreKeys(); + for (NID nid : keys) { + explore(nid, 8, 60*1000, 1); + } + if (_log.shouldLog(Log.INFO)) + _log.info("Explore done size: " + _knownNodes.size()); new Explorer(EXPLORE_TIME); } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/NID.java b/apps/i2psnark/java/src/org/klomp/snark/dht/NID.java index 0543009da..f61c857d8 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/NID.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/NID.java @@ -8,17 +8,22 @@ import net.i2p.util.Clock; /** * A 20-byte peer ID, used as a Map key in lots of places. + * Must be public for constructor in KBucketSet.generateRandomKey() * * @since 0.8.4 * @author zzz */ -class NID extends SHA1Hash { +public class NID extends SHA1Hash { private long lastSeen; private int fails; private static final int MAX_FAILS = 3; + public NID() { + super(null); + } + public NID(byte[] data) { super(data); } diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java index 211439f20..f9a919c00 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java @@ -225,7 +225,7 @@ class NodeInfo extends SimpleDataStructure { @Override public String toString() { - return "NodeInfo: " + nID + ' ' + hash + " port: " + port; + return "NodeInfo: " + nID + ' ' + hash + " port: " + port + (dest != null ? " known dest" : " null dest"); } /** From d5a1e0b1c6c5dbf21e33005e0200784d09ce2ce4 Mon Sep 17 00:00:00 2001 From: zzz Date: Fri, 22 Jun 2012 17:39:41 +0000 Subject: [PATCH 15/23] - Add kad lib, from i2p.zzz.kademlia branch (without the history), which is a rewrite of the netdb kad - Drop now-unused SHA1Comparator - Efficiency tweak to NodeInfoComparator --- .../java/src/net/i2p/kademlia/KBucket.java | 75 ++ .../src/net/i2p/kademlia/KBucketImpl.java | 149 +++ .../java/src/net/i2p/kademlia/KBucketSet.java | 854 ++++++++++++++++++ .../net/i2p/kademlia/SelectionCollector.java | 10 + .../src/net/i2p/kademlia/XORComparator.java | 27 + .../klomp/snark/dht/NodeInfoComparator.java | 8 +- .../org/klomp/snark/dht/SHA1Comparator.java | 31 - 7 files changed, 1119 insertions(+), 35 deletions(-) create mode 100644 apps/i2psnark/java/src/net/i2p/kademlia/KBucket.java create mode 100644 apps/i2psnark/java/src/net/i2p/kademlia/KBucketImpl.java create mode 100644 apps/i2psnark/java/src/net/i2p/kademlia/KBucketSet.java create mode 100644 apps/i2psnark/java/src/net/i2p/kademlia/SelectionCollector.java create mode 100644 apps/i2psnark/java/src/net/i2p/kademlia/XORComparator.java delete mode 100644 apps/i2psnark/java/src/org/klomp/snark/dht/SHA1Comparator.java diff --git a/apps/i2psnark/java/src/net/i2p/kademlia/KBucket.java b/apps/i2psnark/java/src/net/i2p/kademlia/KBucket.java new file mode 100644 index 000000000..075547fbd --- /dev/null +++ b/apps/i2psnark/java/src/net/i2p/kademlia/KBucket.java @@ -0,0 +1,75 @@ +package net.i2p.kademlia; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.Set; + +import net.i2p.data.SimpleDataStructure; + +/** + * Group, without inherent ordering, a set of keys a certain distance away from + * a local key, using XOR as the distance metric + * + * Refactored from net.i2p.router.networkdb.kademlia + */ +public interface KBucket { + + /** + * Lowest order high bit for difference keys. + * The lower-bounds distance of this bucket is 2**begin. + * If begin == 0, this is the closest bucket. + */ + public int getRangeBegin(); + + /** + * Highest high bit for the difference keys. + * The upper-bounds distance of this bucket is (2**(end+1)) - 1. + * If begin == end, the bucket cannot be split further. + * If end == (numbits - 1), this is the furthest bucket. + */ + public int getRangeEnd(); + + /** + * Number of keys already contained in this kbucket + */ + public int getKeyCount(); + + /** + * Add the peer to the bucket + * + * @return true if added + */ + public boolean add(T key); + + /** + * Remove the key from the bucket + * @return true if the key existed in the bucket before removing it, else false + */ + public boolean remove(T key); + + /** + * Update the last-changed timestamp to now. + */ + public void setLastChanged(); + + /** + * The last-changed timestamp + */ + public long getLastChanged(); + + /** + * Retrieve all routing table entries stored in the bucket + * @return set of Hash structures + */ + public Set getEntries(); + + public void getEntries(SelectionCollector collector); + + public void clear(); +} diff --git a/apps/i2psnark/java/src/net/i2p/kademlia/KBucketImpl.java b/apps/i2psnark/java/src/net/i2p/kademlia/KBucketImpl.java new file mode 100644 index 000000000..f73a4e071 --- /dev/null +++ b/apps/i2psnark/java/src/net/i2p/kademlia/KBucketImpl.java @@ -0,0 +1,149 @@ +package net.i2p.kademlia; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.util.Collections; +import java.util.Set; + +import net.i2p.I2PAppContext; +import net.i2p.data.SimpleDataStructure; +import net.i2p.util.ConcurrentHashSet; + +/** + * A concurrent implementation using ConcurrentHashSet. + * The max size (K) may be temporarily exceeded due to concurrency, + * a pending split, or the behavior of the supplied trimmer, + * as explained below. + * The creator is responsible for splits. + * + * This class has no knowledge of the DHT base used for XORing, + * and thus there are no validity checks in add/remove. + * + * The begin and end values are immutable. + * All entries in this bucket will have at least one bit different + * from us in the range [begin, end] inclusive. + * Splits must be implemented by creating two new buckets + * and discarding this one. + * + * The keys are kept in a Set and are NOT sorted by last-seen. + * Per-key last-seen-time, failures, etc. must be tracked elsewhere. + * + * If this bucket is full (i.e. begin == end && size == max) + * then add() will call KBucketTrimmer.trim() do + * (possibly) remove older entries, and indicate whether + * to add the new entry. If the trimmer returns true without + * removing entries, this KBucket will exceed the max size. + * + * Refactored from net.i2p.router.networkdb.kademlia + */ +class KBucketImpl implements KBucket { + /** + * set of Hash objects for the peers in the kbucket + */ + private final Set _entries; + /** include if any bits equal or higher to this bit (in big endian order) */ + private final int _begin; + /** include if no bits higher than this bit (inclusive) are set */ + private final int _end; + private final int _max; + private final KBucketSet.KBucketTrimmer _trimmer; + /** when did we last shake things up */ + private long _lastChanged; + private final I2PAppContext _context; + + /** + * All entries in this bucket will have at least one bit different + * from us in the range [begin, end] inclusive. + */ + public KBucketImpl(I2PAppContext context, int begin, int end, int max, KBucketSet.KBucketTrimmer trimmer) { + if (begin > end) + throw new IllegalArgumentException(begin + " > " + end); + _context = context; + _entries = new ConcurrentHashSet(max + 4); + _begin = begin; + _end = end; + _max = max; + _trimmer = trimmer; + } + + public int getRangeBegin() { return _begin; } + + public int getRangeEnd() { return _end; } + + public int getKeyCount() { + return _entries.size(); + } + + /** + * @return an unmodifiable view; not a copy + */ + public Set getEntries() { + return Collections.unmodifiableSet(_entries); + } + + public void getEntries(SelectionCollector collector) { + for (T h : _entries) { + collector.add(h); + } + } + + public void clear() { + _entries.clear(); + } + + /** + * Sets last-changed if rv is true OR if the peer is already present. + * Calls the trimmer if begin == end and we are full. + * If begin != end then add it and caller must do bucket splitting. + * @return true if added + */ + public boolean add(T peer) { + if (_begin != _end || _entries.size() < _max || + _entries.contains(peer) || _trimmer.trim(this, peer)) { + // do this even if already contains, to call setLastChanged() + boolean rv = _entries.add(peer); + setLastChanged(); + return rv; + } + return false; + } + + /** + * @return if removed. Does NOT set lastChanged. + */ + public boolean remove(T peer) { + boolean rv = _entries.remove(peer); + //if (rv) + // setLastChanged(); + return rv; + } + + /** + * Update the last-changed timestamp to now. + */ + public void setLastChanged() { + _lastChanged = _context.clock().now(); + } + + /** + * The last-changed timestamp, which actually indicates last-added or last-seen. + */ + public long getLastChanged() { + return _lastChanged; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(1024); + buf.append(_entries.size()); + buf.append(" entries in (").append(_begin).append(',').append(_end); + buf.append(") : ").append(_entries.toString()); + return buf.toString(); + } +} diff --git a/apps/i2psnark/java/src/net/i2p/kademlia/KBucketSet.java b/apps/i2psnark/java/src/net/i2p/kademlia/KBucketSet.java new file mode 100644 index 000000000..e61c4a6fd --- /dev/null +++ b/apps/i2psnark/java/src/net/i2p/kademlia/KBucketSet.java @@ -0,0 +1,854 @@ +package net.i2p.kademlia; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import net.i2p.I2PAppContext; +import net.i2p.data.DataHelper; +import net.i2p.data.SimpleDataStructure; +import net.i2p.util.Log; + +/** + * In-memory storage of buckets sorted by the XOR metric from the base (us) + * passed in via the constructor. + * This starts with one bucket covering the whole key space, and + * may eventually be split to a max of the number of bits in the data type + * (160 for SHA1Hash or 256 for Hash), + * times 2**(B-1) for Kademlia value B. + * + * Refactored from net.i2p.router.networkdb.kademlia + */ +public class KBucketSet { + private final Log _log; + private final I2PAppContext _context; + private final T _us; + + /** + * The bucket list is locked by _bucketsLock, however the individual + * buckets are not locked. Users may see buckets that have more than + * the maximum k entries, or may have adds and removes silently fail + * when they appear to succeed. + * + * Closest values are in bucket 0, furthest are in the last bucket. + */ + private final List _buckets; + private final Range _rangeCalc; + private final KBucketTrimmer _trimmer; + + /** + * Locked for reading only when traversing all the buckets. + * Locked for writing only when splitting a bucket. + * Adds/removes/gets from individual buckets are not locked. + */ + private final ReentrantReadWriteLock _bucketsLock = new ReentrantReadWriteLock(false); + + private final int KEYSIZE_BITS; + private final int NUM_BUCKETS; + private final int BUCKET_SIZE; + private final int B_VALUE; + private final int B_FACTOR; + + /** + * Use the default trim strategy, which removes a random entry. + * @param us the local identity (typically a SHA1Hash or Hash) + * The class must have a zero-argument constructor. + * @param max the Kademlia value "k", the max per bucket, k >= 4 + * @param b the Kademlia value "b", split buckets an extra 2**(b-1) times, + * b > 0, use 1 for bittorrent, Kademlia paper recommends 5 + */ + public KBucketSet(I2PAppContext context, T us, int max, int b) { + this(context, us, max, b, new RandomTrimmer(context, max)); + } + + /** + * Use the supplied trim strategy. + */ + public KBucketSet(I2PAppContext context, T us, int max, int b, KBucketTrimmer trimmer) { + _us = us; + _context = context; + _log = context.logManager().getLog(KBucketSet.class); + _trimmer = trimmer; + if (max <= 4 || b <= 0 || b > 8) + throw new IllegalArgumentException(); + KEYSIZE_BITS = us.length() * 8; + B_VALUE = b; + B_FACTOR = 1 << (b - 1); + NUM_BUCKETS = KEYSIZE_BITS * B_FACTOR; + BUCKET_SIZE = max; + _buckets = createBuckets(); + _rangeCalc = new Range(us, B_VALUE); + // this verifies the zero-argument constructor + makeKey(new byte[us.length()]); + } + + private void getReadLock() { + _bucketsLock.readLock().lock(); + } + + /** + * Get the lock if we can. Non-blocking. + * @return true if the lock was acquired + */ + private boolean tryReadLock() { + return _bucketsLock.readLock().tryLock(); + } + + private void releaseReadLock() { + _bucketsLock.readLock().unlock(); + } + + /** @return true if the lock was acquired */ + private boolean getWriteLock() { + try { + boolean rv = _bucketsLock.writeLock().tryLock(3000, TimeUnit.MILLISECONDS); + if ((!rv) && _log.shouldLog(Log.WARN)) + _log.warn("no lock, size is: " + _bucketsLock.getQueueLength(), new Exception("rats")); + return rv; + } catch (InterruptedException ie) {} + return false; + } + + private void releaseWriteLock() { + _bucketsLock.writeLock().unlock(); + } + + /** + * @return true if the peer is new to the bucket it goes in, or false if it was + * already in it. Always returns false on an attempt to add ourselves. + * + */ + public boolean add(T peer) { + KBucket bucket; + getReadLock(); + try { + bucket = getBucket(peer); + } finally { releaseReadLock(); } + if (bucket != null) { + if (bucket.add(peer)) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Peer " + peer + " added to bucket " + bucket); + if (shouldSplit(bucket)) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Splitting bucket " + bucket); + split(bucket.getRangeBegin()); + //testAudit(this, _log); + } + return true; + } else { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Peer " + peer + " NOT added to bucket " + bucket); + return false; + } + } else { + if (_log.shouldLog(Log.WARN)) + _log.warn("Failed to add, probably us: " + peer); + return false; + } + } + + /** + * No lock required. + * FIXME will split the closest buckets too far if B > 1 and K < 2**B + * Won't ever really happen and if it does it still works. + */ + private boolean shouldSplit(KBucket b) { + return + b.getRangeBegin() != b.getRangeEnd() && + b.getKeyCount() > BUCKET_SIZE; + } + + /** + * Grabs the write lock. + * Caller must NOT have the read lock. + * The bucket should be splittable (range start != range end). + * @param r the range start of the bucket to be split + */ + private void split(int r) { + if (!getWriteLock()) + return; + try { + locked_split(r); + } finally { releaseWriteLock(); } + } + + /** + * Creates two or more new buckets. The old bucket is replaced and discarded. + * + * Caller must hold write lock + * The bucket should be splittable (range start != range end). + * @param r the range start of the bucket to be split + */ + private void locked_split(int r) { + int b = pickBucket(r); + while (shouldSplit(_buckets.get(b))) { + KBucket b0 = _buckets.get(b); + // Each bucket gets half the keyspace. + // When B_VALUE = 1, or the bucket is larger than B_FACTOR, then + // e.g. 0-159 => 0-158, 159-159 + // When B_VALUE > 1, and the bucket is smaller than B_FACTOR, then + // e.g. 1020-1023 => 1020-1021, 1022-1023 + int s1, e1, s2, e2; + s1 = b0.getRangeBegin(); + e2 = b0.getRangeEnd(); + if (B_FACTOR > 1 && + (s1 & (B_FACTOR - 1)) == 0 && + ((e2 + 1) & (B_FACTOR - 1)) == 0 && + e2 > s1 + B_FACTOR) { + // The bucket is a "whole" kbucket with a range > B_FACTOR, + // so it should be split into two "whole" kbuckets each with + // a range >= B_FACTOR. + // Log split + s2 = e2 + 1 - B_FACTOR; + } else { + // The bucket is the smallest "whole" kbucket with a range == B_FACTOR, + // or B_VALUE > 1 and the bucket has already been split. + // Start or continue splitting down to a depth B_VALUE. + // Linear split + s2 = s1 + ((1 + e2 - s1) / 2); + } + e1 = s2 - 1; + if (_log.shouldLog(Log.INFO)) + _log.info("Splitting (" + s1 + ',' + e2 + ") -> (" + s1 + ',' + e1 + ") (" + s2 + ',' + e2 + ')'); + KBucket b1 = createBucket(s1, e1); + KBucket b2 = createBucket(s2, e2); + for (T key : b0.getEntries()) { + if (getRange(key) < s2) + b1.add(key); + else + b2.add(key); + } + _buckets.set(b, b1); + _buckets.add(b + 1, b2); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Split bucket at idx " + b + + ":\n" + b0 + + "\ninto: " + b1 + + "\nand: " + b2); + //if (_log.shouldLog(Log.DEBUG)) + // _log.debug("State is now: " + toString()); + + if (b2.getKeyCount() > BUCKET_SIZE) { + // should be rare... too hard to call _trimmer from here + // (and definitely not from inside the write lock) + if (_log.shouldLog(Log.INFO)) + _log.info("All went into 2nd bucket after split"); + } + // loop if all the entries went in the first bucket + } + } + + /** + * The current number of entries. + */ + public int size() { + int rv = 0; + getReadLock(); + try { + for (KBucket b : _buckets) { + rv += b.getKeyCount(); + } + } finally { releaseReadLock(); } + return rv; + } + + public boolean remove(T entry) { + KBucket kbucket; + getReadLock(); + try { + kbucket = getBucket(entry); + } finally { releaseReadLock(); } + boolean removed = kbucket.remove(entry); + return removed; + } + + /** @since 0.8.8 */ + public void clear() { + getReadLock(); + try { + for (KBucket b : _buckets) { + b.clear(); + } + } finally { releaseReadLock(); } + _rangeCalc.clear(); + } + + /** + * @return a copy in a new set + */ + public Set getAll() { + Set all = new HashSet(256); + getReadLock(); + try { + for (KBucket b : _buckets) { + all.addAll(b.getEntries()); + } + } finally { releaseReadLock(); } + return all; + } + + /** + * @return a copy in a new set + */ + public Set getAll(Set toIgnore) { + Set all = getAll(); + all.removeAll(toIgnore); + return all; + } + + public void getAll(SelectionCollector collector) { + getReadLock(); + try { + for (KBucket b : _buckets) { + b.getEntries(collector); + } + } finally { releaseReadLock(); } + } + + /** + * The keys closest to us. + * Returned list will never contain us. + * @return non-null, closest first + */ + public List getClosest(int max) { + return getClosest(max, Collections.EMPTY_SET); + } + + /** + * The keys closest to us. + * Returned list will never contain us. + * @return non-null, closest first + */ + public List getClosest(int max, Collection toIgnore) { + List rv = new ArrayList(max); + int count = 0; + getReadLock(); + try { + // start at first (closest) bucket + for (int i = 0; i < _buckets.size() && count < max; i++) { + Set entries = _buckets.get(i).getEntries(); + // add the whole bucket except for ignores, + // extras will be trimmed after sorting + for (T e : entries) { + if (!toIgnore.contains(e)) { + rv.add(e); + count++; + } + } + } + } finally { releaseReadLock(); } + Comparator comp = new XORComparator(_us); + Collections.sort(rv, comp); + int sz = rv.size(); + for (int i = sz - 1; i >= max; i--) { + rv.remove(i); + } + return rv; + } + + /** + * The keys closest to the key. + * Returned list will never contain us. + * @return non-null, closest first + */ + public List getClosest(T key, int max) { + return getClosest(key, max, Collections.EMPTY_SET); + } + + /** + * The keys closest to the key. + * Returned list will never contain us. + * @return non-null, closest first + */ + public List getClosest(T key, int max, Collection toIgnore) { + if (key.equals(_us)) + return getClosest(max, toIgnore); + List rv = new ArrayList(max); + int count = 0; + getReadLock(); + try { + int start = pickBucket(key); + // start at closest bucket, then to the smaller (closer to us) buckets + for (int i = start; i >= 0 && count < max; i--) { + Set entries = _buckets.get(i).getEntries(); + for (T e : entries) { + if (!toIgnore.contains(e)) { + rv.add(e); + count++; + } + } + } + // then the farther from us buckets if necessary + for (int i = start + 1; i < _buckets.size() && count < max; i++) { + Set entries = _buckets.get(i).getEntries(); + for (T e : entries) { + if (!toIgnore.contains(e)) { + rv.add(e); + count++; + } + } + } + } finally { releaseReadLock(); } + Comparator comp = new XORComparator(key); + Collections.sort(rv, comp); + int sz = rv.size(); + for (int i = sz - 1; i >= max; i--) { + rv.remove(i); + } + return rv; + } + + /** + * The bucket number (NOT the range number) that the xor of the key goes in + * Caller must hold read lock + * @return 0 to max-1 or -1 for us + */ + private int pickBucket(T key) { + int range = getRange(key); + if (range < 0) + return -1; + int rv = pickBucket(range); + if (rv >= 0) { + return rv; + } + _log.error("Key does not fit in any bucket?! WTF!\nKey : [" + + DataHelper.toHexString(key.getData()) + "]" + + "\nUs : " + _us + + "\nDelta: [" + + DataHelper.toHexString(DataHelper.xor(_us.getData(), key.getData())) + + "]", new Exception("WTF")); + _log.error(toString()); + throw new IllegalStateException("pickBucket returned " + rv); + //return -1; + } + + /** + * Returned list is a copy of the bucket list, closest first, + * with the actual buckets (not a copy). + * + * Primarily for testing. You shouldn't ever need to get all the buckets. + * Use getClosest() or getAll() instead to get the keys. + * + * @return non-null + */ + List> getBuckets() { + getReadLock(); + try { + return new ArrayList(_buckets); + } finally { releaseReadLock(); } + } + + /** + * The bucket that the xor of the key goes in + * Caller must hold read lock + * @return null if key is us + */ + private KBucket getBucket(T key) { + int bucket = pickBucket(key); + if (bucket < 0) + return null; + return _buckets.get(bucket); + } + + /** + * The bucket number that contains this range number + * Caller must hold read lock or write lock + * @return 0 to max-1 or -1 for us + */ + private int pickBucket(int range) { + // If B is small, a linear search from back to front + // is most efficient since most of the keys are at the end... + // If B is larger, there's a lot of sub-buckets + // of equal size to be checked so a binary search is better + if (B_VALUE <= 3) { + for (int i = _buckets.size() - 1; i >= 0; i--) { + KBucket b = _buckets.get(i); + if (range >= b.getRangeBegin() && range <= b.getRangeEnd()) + return i; + } + return -1; + } else { + KBucket dummy = new DummyBucket(range); + return Collections.binarySearch(_buckets, dummy, new BucketComparator()); + } + } + + private List createBuckets() { + // just an initial size + List buckets = new ArrayList(4 * B_FACTOR); + buckets.add(createBucket(0, NUM_BUCKETS -1)); + return buckets; + } + + private KBucket createBucket(int start, int end) { + if (end - start >= B_FACTOR && + (((end + 1) & B_FACTOR - 1) != 0 || + (start & B_FACTOR - 1) != 0)) + throw new IllegalArgumentException("Sub-bkt crosses K-bkt boundary: " + start + '-' + end); + KBucket bucket = new KBucketImpl(_context, start, end, BUCKET_SIZE, _trimmer); + return bucket; + } + + /** + * The number of bits minus 1 (range number) for the xor of the key. + * Package private for testing only. Others shouldn't need this. + * @return 0 to max-1 or -1 for us + */ + int getRange(T key) { + return _rangeCalc.getRange(key); + } + + /** + * For every bucket that hasn't been updated in this long, + * generate a random key that would be a member of that bucket. + * The returned keys may be searched for to "refresh" the buckets. + * @return non-null, closest first + */ + public List getExploreKeys(long age) { + List rv = new ArrayList(_buckets.size()); + long old = _context.clock().now() - age; + getReadLock(); + try { + for (KBucket b : _buckets) { + if (b.getLastChanged() < old) + rv.add(generateRandomKey(b)); + } + } finally { releaseReadLock(); } + return rv; + } + + /** + * Generate a random key to go within this bucket + * Package private for testing only. Others shouldn't need this. + */ + T generateRandomKey(KBucket bucket) { + int begin = bucket.getRangeBegin(); + int end = bucket.getRangeEnd(); + // number of fixed bits, out of B_VALUE - 1 bits + int fixed = 0; + int bsz = 1 + end - begin; + // compute fixed = B_VALUE - log2(bsz) + // e.g for B=4, B_FACTOR=8, sz 4-> fixed 1, sz 2->fixed 2, sz 1 -> fixed 3 + while (bsz < B_FACTOR) { + fixed++; + bsz <<= 1; + } + int fixedBits = 0; + if (fixed > 0) { + // 0x01, 03, 07, 0f, ... + int mask = (1 << fixed) - 1; + // fixed bits masked from begin + fixedBits = (begin >> (B_VALUE - (fixed + 1))) & mask; + } + int obegin = begin; + int oend = end; + begin >>= (B_VALUE - 1); + end >>= (B_VALUE - 1); + // we need randomness for [0, begin) bits + BigInteger variance; + // 00000000rrrr + if (begin > 0) + variance = new BigInteger(begin - fixed, _context.random()); + else + variance = BigInteger.ZERO; + // we need nonzero randomness for [begin, end] bits + int numNonZero = 1 + end - begin; + if (numNonZero == 1) { + // 00001000rrrr + variance = variance.setBit(begin); + // fixed bits as the 'main' bucket is split + // 00001fffrrrr + if (fixed > 0) + variance = variance.or(BigInteger.valueOf(fixedBits).shiftLeft(begin - fixed)); + } else { + // dont span main bucket boundaries with depth > 1 + if (fixed > 0) + throw new IllegalStateException("WTF " + bucket); + BigInteger nonz; + if (numNonZero <= 62) { + // add one to ensure nonzero + long nz = 1 + _context.random().nextLong((1l << numNonZero) - 1); + nonz = BigInteger.valueOf(nz); + } else { + // loop to ensure nonzero + do { + nonz = new BigInteger(numNonZero, _context.random()); + } while (nonz.equals(BigInteger.ZERO)); + } + // shift left and or-in the nonzero randomness + if (begin > 0) + nonz = nonz.shiftLeft(begin); + // 0000nnnnrrrr + variance = variance.or(nonz); + } + + if (_log.shouldLog(Log.DEBUG)) + _log.debug("SB(" + obegin + ',' + oend + ") KB(" + begin + ',' + end + ") fixed=" + fixed + " fixedBits=" + fixedBits + " numNonZ=" + numNonZero); + byte data[] = variance.toByteArray(); + T key = makeKey(data); + byte[] hash = DataHelper.xor(key.getData(), _us.getData()); + T rv = makeKey(hash); + + // DEBUG + //int range = getRange(rv); + //if (range < obegin || range > oend) { + // throw new IllegalStateException("Generate random key failed range=" + range + " for " + rv + " meant for bucket " + bucket); + //} + + return rv; + } + + /** + * Make a new SimpleDataStrucure from the data + * @param data size <= SDS length, else throws IAE + * Can be 1 bigger if top byte is zero + */ + private T makeKey(byte[] data) { + int len = _us.length(); + int dlen = data.length; + if (dlen > len + 1 || + (dlen == len + 1 && data[0] != 0)) + throw new IllegalArgumentException("bad length " + dlen + " > " + len); + T rv; + try { + rv = (T) _us.getClass().newInstance(); + } catch (Exception e) { + _log.error("fail", e); + throw new RuntimeException(e); + } + if (dlen == len) { + rv.setData(data); + } else { + byte[] ndata = new byte[len]; + if (dlen == len + 1) { + // one bigger + System.arraycopy(data, 1, ndata, 0, len); + } else { + // smaller + System.arraycopy(data, 0, ndata, len - dlen, dlen); + } + rv.setData(ndata); + } + return rv; + } + + private static class Range { + private final int _bValue; + private final BigInteger _bigUs; + private final Map _distanceCache; + + public Range(T us, int bValue) { + _bValue = bValue; + _bigUs = new BigInteger(1, us.getData()); + _distanceCache = new LHM(256); + } + + /** @return 0 to max-1 or -1 for us */ + public int getRange(T key) { + Integer rv; + synchronized (_distanceCache) { + rv = _distanceCache.get(key); + if (rv == null) { + // easy way when _bValue == 1 + //rv = Integer.valueOf(_bigUs.xor(new BigInteger(1, key.getData())).bitLength() - 1); + BigInteger xor = _bigUs.xor(new BigInteger(1, key.getData())); + int range = xor.bitLength() - 1; + if (_bValue > 1) { + int toShift = range + 1 - _bValue; + int highbit = range; + range <<= _bValue - 1; + if (toShift >= 0) { + int extra = xor.clearBit(highbit).shiftRight(toShift).intValue(); + range += extra; + //Log log = I2PAppContext.getGlobalContext().logManager().getLog(KBucketSet.class); + //if (log.shouldLog(Log.DEBUG)) + // log.debug("highbit " + highbit + " toshift " + toShift + " extra " + extra + " new " + range); + } + } + rv = Integer.valueOf(range); + _distanceCache.put(key, rv); + } + } + return rv.intValue(); + } + + public void clear() { + synchronized (_distanceCache) { + _distanceCache.clear(); + } + } + } + + private static class LHM extends LinkedHashMap { + private final int _max; + + public LHM(int max) { + super(max, 0.75f, true); + _max = max; + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > _max; + } + } + + /** + * For Collections.binarySearch. + * getRangeBegin == getRangeEnd. + */ + private static class DummyBucket implements KBucket { + private final int r; + + public DummyBucket(int range) { + r = range; + } + + public int getRangeBegin() { return r; } + public int getRangeEnd() { return r; } + + public int getKeyCount() { + return 0; + } + + public Set getEntries() { + throw new UnsupportedOperationException(); + } + + public void getEntries(SelectionCollector collector) { + throw new UnsupportedOperationException(); + } + + public void clear() {} + + public boolean add(T peer) { + throw new UnsupportedOperationException(); + } + + public boolean remove(T peer) { + return false; + } + + public void setLastChanged() {} + + public long getLastChanged() { + return 0; + } + } + + /** + * For Collections.binarySearch. + * Returns equal for any overlap. + */ + private static class BucketComparator implements Comparator { + public int compare(KBucket l, KBucket r) { + if (l.getRangeEnd() < r.getRangeBegin()) + return -1; + if (l.getRangeBegin() > r.getRangeEnd()) + return 1; + return 0; + } + } + + /** + * Called when a kbucket can no longer be split and is too big + */ + public interface KBucketTrimmer { + /** + * Called from add() just before adding the entry. + * You may call getEntries() and/or remove() from here. + * Do NOT call add(). + * To always discard a newer entry, always return false. + * + * @param kbucket the kbucket that is now too big + * @param justAdded the entry that was just added, causing it to be too big + * @return true to actually add the entry. + */ + public boolean trim(KBucket kbucket, K toAdd); + } + + /** + * Removes a random element. Not resistant to flooding. + */ + public static class RandomTrimmer implements KBucketTrimmer { + protected final I2PAppContext _ctx; + private final int _max; + + public RandomTrimmer(I2PAppContext ctx, int max) { + _ctx = ctx; + _max = max; + } + + public boolean trim(KBucket kbucket, T toAdd) { + List e = new ArrayList(kbucket.getEntries()); + int sz = e.size(); + // concurrency + if (sz < _max) + return true; + T toRemove = e.get(_ctx.random().nextInt(sz)); + return kbucket.remove(toRemove); + } + } + + /** + * Removes a random element, but only if the bucket hasn't changed in 5 minutes. + */ + public static class RandomIfOldTrimmer extends RandomTrimmer { + + public RandomIfOldTrimmer(I2PAppContext ctx, int max) { + super(ctx, max); + } + + public boolean trim(KBucket kbucket, T toAdd) { + if (kbucket.getLastChanged() > _ctx.clock().now() - 5*60*1000) + return false; + return super.trim(kbucket, toAdd); + } + } + + /** + * Removes nothing and always rejects the add. Flood resistant.. + */ + public static class RejectTrimmer implements KBucketTrimmer { + public boolean trim(KBucket kbucket, T toAdd) { + return false; + } + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(1024); + buf.append("Bucket set rooted on: ").append(_us.toString()) + .append(" K= ").append(BUCKET_SIZE) + .append(" B= ").append(B_VALUE) + .append(" with ").append(size()) + .append(" keys in ").append(_buckets.size()).append(" buckets:\n"); + getReadLock(); + try { + int len = _buckets.size(); + for (int i = 0; i < len; i++) { + KBucket b = _buckets.get(i); + buf.append("* Bucket ").append(i).append("/").append(len).append(": "); + buf.append(b.toString()).append("\n"); + } + } finally { releaseReadLock(); } + return buf.toString(); + } +} diff --git a/apps/i2psnark/java/src/net/i2p/kademlia/SelectionCollector.java b/apps/i2psnark/java/src/net/i2p/kademlia/SelectionCollector.java new file mode 100644 index 000000000..8d4b9972a --- /dev/null +++ b/apps/i2psnark/java/src/net/i2p/kademlia/SelectionCollector.java @@ -0,0 +1,10 @@ +package net.i2p.kademlia; + +import net.i2p.data.SimpleDataStructure; + +/** + * Visit kbuckets, gathering matches + */ +public interface SelectionCollector { + public void add(T entry); +} diff --git a/apps/i2psnark/java/src/net/i2p/kademlia/XORComparator.java b/apps/i2psnark/java/src/net/i2p/kademlia/XORComparator.java new file mode 100644 index 000000000..d11823f49 --- /dev/null +++ b/apps/i2psnark/java/src/net/i2p/kademlia/XORComparator.java @@ -0,0 +1,27 @@ +package net.i2p.kademlia; + +import java.util.Comparator; + +import net.i2p.data.DataHelper; +import net.i2p.data.SimpleDataStructure; + +/** + * Help sort Hashes in relation to a base key using the XOR metric + * + */ +class XORComparator implements Comparator { + private final byte[] _base; + + /** + * @param target key to compare distances with + */ + public XORComparator(T target) { + _base = target.getData(); + } + + public int compare(T lhs, T rhs) { + byte lhsDelta[] = DataHelper.xor(lhs.getData(), _base); + byte rhsDelta[] = DataHelper.xor(rhs.getData(), _base); + return DataHelper.compareTo(lhsDelta, rhsDelta); + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfoComparator.java b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfoComparator.java index 66eb57bba..9995dfe57 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfoComparator.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfoComparator.java @@ -16,15 +16,15 @@ import net.i2p.data.DataHelper; * @author zzz */ class NodeInfoComparator implements Comparator { - private final SHA1Hash _base; + private final byte[] _base; public NodeInfoComparator(SHA1Hash h) { - _base = h; + _base = h.getData(); } public int compare(NodeInfo lhs, NodeInfo rhs) { - byte lhsDelta[] = DataHelper.xor(lhs.getNID().getData(), _base.getData()); - byte rhsDelta[] = DataHelper.xor(rhs.getNID().getData(), _base.getData()); + byte lhsDelta[] = DataHelper.xor(lhs.getNID().getData(), _base); + byte rhsDelta[] = DataHelper.xor(rhs.getNID().getData(), _base); return DataHelper.compareTo(lhsDelta, rhsDelta); } diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/SHA1Comparator.java b/apps/i2psnark/java/src/org/klomp/snark/dht/SHA1Comparator.java deleted file mode 100644 index 36c355e49..000000000 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/SHA1Comparator.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.klomp.snark.dht; -/* - * From zzzot, modded and relicensed to GPLv2 - */ - -import java.util.Comparator; - -import net.i2p.crypto.SHA1Hash; -import net.i2p.data.DataHelper; - -/** - * Closest to a InfoHash or NID key. - * Use for InfoHashes and NIDs. - * - * @since 0.8.4 - * @author zzz - */ -class SHA1Comparator implements Comparator { - private final byte[] _base; - - public SHA1Comparator(SHA1Hash h) { - _base = h.getData(); - } - - public int compare(SHA1Hash lhs, SHA1Hash rhs) { - byte lhsDelta[] = DataHelper.xor(lhs.getData(), _base); - byte rhsDelta[] = DataHelper.xor(rhs.getData(), _base); - return DataHelper.compareTo(lhsDelta, rhsDelta); - } - -} From dba3fee4777d3502b8098f8087cbd754b8a3d888 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 25 Jun 2012 18:20:18 +0000 Subject: [PATCH 16/23] - Concurrent PeerCoordinatorSet - final infoHash in Snark --- .../src/org/klomp/snark/PeerAcceptor.java | 5 +-- .../org/klomp/snark/PeerCoordinatorSet.java | 37 ++++++++++--------- .../java/src/org/klomp/snark/Snark.java | 14 ++++--- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java b/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java index e8b123373..ee651ed85 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java @@ -117,9 +117,8 @@ public class PeerAcceptor } } else { // multitorrent capable, so lets see what we can handle - for (Iterator iter = coordinators.iterator(); iter.hasNext(); ) { - PeerCoordinator cur = (PeerCoordinator)iter.next(); - + PeerCoordinator cur = coordinators.get(peerInfoHash); + if (cur != null) { if (DataHelper.eq(cur.getInfoHash(), peerInfoHash)) { if (cur.needPeers()) { diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinatorSet.java b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinatorSet.java index de367c4d4..7b1347bf7 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinatorSet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinatorSet.java @@ -1,9 +1,10 @@ package org.klomp.snark; -import java.util.ArrayList; -import java.util.HashSet; import java.util.Iterator; -import java.util.Set; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import net.i2p.crypto.SHA1Hash; /** * Hmm, any guesses as to what this is? Used by the multitorrent functionality @@ -12,26 +13,28 @@ import java.util.Set; * from it there too) */ public class PeerCoordinatorSet { - private final Set _coordinators; + private final Map _coordinators; public PeerCoordinatorSet() { - _coordinators = new HashSet(); + _coordinators = new ConcurrentHashMap(); } - - public Iterator iterator() { - synchronized (_coordinators) { - return new ArrayList(_coordinators).iterator(); - } + + public Iterator iterator() { + return _coordinators.values().iterator(); } - + public void add(PeerCoordinator coordinator) { - synchronized (_coordinators) { - _coordinators.add(coordinator); - } + _coordinators.put(new SHA1Hash(coordinator.getInfoHash()), coordinator); } + public void remove(PeerCoordinator coordinator) { - synchronized (_coordinators) { - _coordinators.remove(coordinator); - } + _coordinators.remove(new SHA1Hash(coordinator.getInfoHash())); + } + + /** + * @since 0.9.2 + */ + public PeerCoordinator get(byte[] infoHash) { + return _coordinators.get(new SHA1Hash(infoHash)); } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java index 138f2dd2c..9bc0d7cea 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java @@ -252,7 +252,7 @@ public class Snark private boolean stopped; private boolean starting; private byte[] id; - private byte[] infoHash; + private final byte[] infoHash; private String additionalTrackerURL; private final I2PSnarkUtil _util; private final PeerCoordinatorSet _peerCoordinatorSet; @@ -340,6 +340,7 @@ public class Snark meta = null; File f = null; InputStream in = null; + byte[] x_infoHash = null; try { f = new File(torrent); @@ -362,7 +363,7 @@ public class Snark throw new IOException("not found"); } meta = new MetaInfo(in); - infoHash = meta.getInfoHash(); + x_infoHash = meta.getInfoHash(); } catch(IOException ioe) { @@ -403,6 +404,7 @@ public class Snark try { in.close(); } catch (IOException ioe) {} } + infoHash = x_infoHash; // final debug(meta.toString(), INFO); // When the metainfo torrent was created from an existing file/dir @@ -1239,8 +1241,8 @@ public class Snark if (_peerCoordinatorSet == null || uploaders <= 0) return false; int totalUploaders = 0; - for (Iterator iter = _peerCoordinatorSet.iterator(); iter.hasNext(); ) { - PeerCoordinator c = (PeerCoordinator)iter.next(); + for (Iterator iter = _peerCoordinatorSet.iterator(); iter.hasNext(); ) { + PeerCoordinator c = iter.next(); if (!c.halted()) totalUploaders += c.uploaders; } @@ -1253,8 +1255,8 @@ public class Snark if (_peerCoordinatorSet == null) return false; long total = 0; - for (Iterator iter = _peerCoordinatorSet.iterator(); iter.hasNext(); ) { - PeerCoordinator c = (PeerCoordinator)iter.next(); + for (Iterator iter = _peerCoordinatorSet.iterator(); iter.hasNext(); ) { + PeerCoordinator c = iter.next(); if (!c.halted()) total += c.getCurrentUploadRate(); } From 4715dbdbd082d4c6e14db5640bc667f9c7a1d77d Mon Sep 17 00:00:00 2001 From: zzz Date: Fri, 3 Aug 2012 20:40:31 +0000 Subject: [PATCH 17/23] fixup after prop --- apps/i2psnark/java/src/org/klomp/snark/Snark.java | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java index 02b703be0..efb491d31 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java @@ -385,6 +385,7 @@ public class Snark try { in.close(); } catch (IOException ioe) {} } + infoHash = x_infoHash; // final if (_log.shouldLog(Log.INFO)) _log.info(meta.toString()); From 280a708afeebe3863ed7411ee84ff736fbfbd964 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 4 Aug 2012 17:11:11 +0000 Subject: [PATCH 18/23] - Change secure Node ID requirements again - Protect against null DHT races - Add message about restarting tunnel when DHT config changes - Add DHT size to table totals --- .../src/org/klomp/snark/PeerCheckerTask.java | 11 +++++--- .../src/org/klomp/snark/SnarkManager.java | 2 ++ .../src/org/klomp/snark/TrackerClient.java | 24 ++++++++++------- .../java/src/org/klomp/snark/dht/DHT.java | 5 ++++ .../java/src/org/klomp/snark/dht/KRPC.java | 9 ++++++- .../src/org/klomp/snark/dht/NodeInfo.java | 27 ++++++++++++------- .../org/klomp/snark/web/I2PSnarkServlet.java | 9 +++++++ 7 files changed, 63 insertions(+), 24 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java b/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java index ac53f38b6..2bb9feb5f 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java @@ -28,6 +28,8 @@ import net.i2p.I2PAppContext; import net.i2p.data.DataHelper; import net.i2p.util.Log; +import org.klomp.snark.dht.DHT; + /** * TimerTask that checks for good/bad up/downloader. Works together * with the PeerCoordinator to select which Peers get (un)choked. @@ -74,6 +76,7 @@ class PeerCheckerTask implements Runnable List removed = new ArrayList(); int uploadLimit = coordinator.allowedUploaders(); boolean overBWLimit = coordinator.overUpBWLimit(); + DHT dht = _util.getDHT(); for (Peer peer : peerList) { // Remove dying peers @@ -218,8 +221,8 @@ class PeerCheckerTask implements Runnable if (coordinator.getNeededLength() > 0 || !peer.isCompleted()) peer.keepAlive(); // announce them to local tracker (TrackerClient does this too) - if (_util.getDHT() != null && (_runCount % 5) == 0) { - _util.getDHT().announce(coordinator.getInfoHash(), peer.getPeerID().getDestHash()); + if (dht != null && (_runCount % 5) == 0) { + dht.announce(coordinator.getInfoHash(), peer.getPeerID().getDestHash()); } } @@ -267,8 +270,8 @@ class PeerCheckerTask implements Runnable } // announce ourselves to local tracker (TrackerClient does this too) - if (_util.getDHT() != null && (_runCount % 16) == 0) { - _util.getDHT().announce(coordinator.getInfoHash()); + if (dht != null && (_runCount % 16) == 0) { + dht.announce(coordinator.getInfoHash()); } } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index 459486831..25fa0ffa0 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -577,6 +577,8 @@ public class SnarkManager implements Snark.CompleteListener { addMessage(_("Enabled DHT.")); else addMessage(_("Disabled DHT.")); + if (_util.connected()) + addMessage(_("DHT change requires tunnel shutdown and reopen")); _util.setUseDHT(useDHT); changed = true; } diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java index ad26660ee..ccb028134 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java +++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java @@ -43,6 +43,8 @@ import net.i2p.util.I2PAppThread; import net.i2p.util.Log; import net.i2p.util.SimpleTimer2; +import org.klomp.snark.dht.DHT; + /** * Informs metainfo tracker of events and gets new peers for peer * coordinator. @@ -323,8 +325,9 @@ public class TrackerClient implements Runnable { } // Local DHT tracker announce - if (_util.getDHT() != null) - _util.getDHT().announce(snark.getInfoHash()); + DHT dht = _util.getDHT(); + if (dht != null) + dht.announce(snark.getInfoHash()); long uploaded = coordinator.getUploaded(); long downloaded = coordinator.getDownloaded(); @@ -372,9 +375,10 @@ public class TrackerClient implements Runnable { snark.setTrackerSeenPeers(tr.seenPeers); // pass everybody over to our tracker - if (_util.getDHT() != null) { + dht = _util.getDHT(); + if (dht != null) { for (Peer peer : peers) { - _util.getDHT().announce(snark.getInfoHash(), peer.getPeerID().getDestHash()); + dht.announce(snark.getInfoHash(), peer.getPeerID().getDestHash()); } } @@ -458,20 +462,21 @@ public class TrackerClient implements Runnable { // Get peers from DHT // FIXME this needs to be in its own thread - if (_util.getDHT() != null && (meta == null || !meta.isPrivate()) && !stop) { + dht = _util.getDHT(); + if (dht != null && (meta == null || !meta.isPrivate()) && !stop) { int numwant; if (event.equals(STOPPED_EVENT) || !coordinator.needOutboundPeers()) numwant = 1; else numwant = _util.getMaxConnections(); - List hashes = _util.getDHT().getPeers(snark.getInfoHash(), numwant, 2*60*1000); + List hashes = dht.getPeers(snark.getInfoHash(), numwant, 2*60*1000); if (_log.shouldLog(Log.INFO)) _log.info("Got " + hashes + " from DHT"); // announce ourselves while the token is still good // FIXME this needs to be in its own thread if (!stop) { // announce only to the 1 closest - int good = _util.getDHT().announce(snark.getInfoHash(), 1, 5*60*1000); + int good = dht.announce(snark.getInfoHash(), 1, 5*60*1000); if (_log.shouldLog(Log.INFO)) _log.info("Sent " + good + " good announces to DHT"); } @@ -548,8 +553,9 @@ public class TrackerClient implements Runnable { */ private void unannounce() { // Local DHT tracker unannounce - if (_util.getDHT() != null) - _util.getDHT().unannounce(snark.getInfoHash()); + DHT dht = _util.getDHT(); + if (dht != null) + dht.unannounce(snark.getInfoHash()); int i = 0; for (Tracker tr : trackers) { if (_util.connected() && diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/DHT.java b/apps/i2psnark/java/src/org/klomp/snark/dht/DHT.java index 2e401f060..d5642835b 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/DHT.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/DHT.java @@ -89,4 +89,9 @@ public interface DHT { * Stop everything. */ public void stop(); + + /** + * Known nodes, not estimated total network size. + */ + public int size(); } diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java index a224b1416..37724825d 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java @@ -158,7 +158,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { _qPort = 2555 + ctx.random().nextInt(61111); _rPort = _qPort + 1; if (SECURE_NID) { - _myNID = NodeInfo.generateNID(session.getMyDestination().calculateHash(), _qPort); + _myNID = NodeInfo.generateNID(session.getMyDestination().calculateHash(), _qPort, _context.random()); _myID = _myNID.getData(); } else { _myID = new byte[NID.HASH_LENGTH]; @@ -176,6 +176,13 @@ public class KRPC implements I2PSessionMuxedListener, DHT { ///////////////// Public methods + /** + * Known nodes, not estimated total network size. + */ + public int size() { + return _knownNodes.size(); + } + /** * @return The UDP query port */ diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java index f9a919c00..9ed1a2912 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java @@ -9,6 +9,7 @@ import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.data.SimpleDataStructure; +import net.i2p.util.RandomSource; /* * A Node ID, Hash, and port, and an optional Destination. @@ -139,29 +140,35 @@ class NodeInfo extends SimpleDataStructure { } /** - * Generate a secure NID that matches the Hash and port + * Generate a secure NID that matches the Hash and port. + * Rules: First 4 bytes must match Hash. + * Next 2 bytes must match Hash ^ port. + * Remaining bytes may be random. + * * @throws IllegalArgumentException */ - public static NID generateNID(Hash h, int p) { + public static NID generateNID(Hash h, int p, RandomSource random) { byte[] n = new byte[NID.HASH_LENGTH]; - System.arraycopy(h.getData(), 0, n, 0, NID.HASH_LENGTH); - n[0] ^= (byte) (p >> 8); - n[1] ^= (byte) p; + System.arraycopy(h.getData(), 0, n, 0, 6); + n[4] ^= (byte) (p >> 8); + n[5] ^= (byte) p; + random.nextBytes(n, 6, NID.HASH_LENGTH - 6); return new NID(n); } /** - * Verify the NID matches the Hash - * @throws IllegalArgumentException + * Verify the NID matches the Hash. + * See generateNID() for requirements. + * @throws IllegalArgumentException on mismatch */ private void verify() { if (!KRPC.SECURE_NID) return; byte[] nb = nID.getData(); byte[] hb = hash.getData(); - if ((!DataHelper.eq(nb, 2, hb, 2, NID.HASH_LENGTH - 2)) || - ((nb[0] ^ (port >> 8)) & 0xff) != (hb[0] & 0xff) || - ((nb[1] ^ port) & 0xff) != (hb[1] & 0xff)) + if ((!DataHelper.eq(nb, 0, hb, 0, 4)) || + ((nb[4] ^ (port >> 8)) & 0xff) != (hb[4] & 0xff) || + ((nb[5] ^ port) & 0xff) != (hb[5] & 0xff)) throw new IllegalArgumentException("NID/Hash mismatch"); } diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java index 5a7bec05f..3eafa3ecd 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -42,6 +42,7 @@ import org.klomp.snark.SnarkManager; import org.klomp.snark.Storage; import org.klomp.snark.Tracker; import org.klomp.snark.TrackerClient; +import org.klomp.snark.dht.DHT; import org.mortbay.jetty.servlet.DefaultServlet; import org.mortbay.resource.Resource; @@ -470,6 +471,14 @@ public class I2PSnarkServlet extends DefaultServlet { out.write(", "); out.write(DataHelper.formatSize2(stats[5]) + "B, "); out.write(ngettext("1 connected peer", "{0} connected peers", (int) stats[4])); + DHT dht = _manager.util().getDHT(); + if (dht != null) { + int dhts = dht.size(); + if (dhts > 0) { + out.write(", "); + out.write(ngettext("1 DHT peer", "{0} DHT peers", dhts)); + } + } out.write("\n"); if (_manager.util().connected()) { out.write(" " + formatSize(stats[0]) + "\n" + From 67f16b0de42058372619eb37d14186a9272c3257 Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 5 Aug 2012 16:55:39 +0000 Subject: [PATCH 19/23] javadocs --- apps/i2psnark/java/src/net/i2p/kademlia/KBucketSet.java | 1 - apps/i2psnark/java/src/net/i2p/kademlia/package.html | 6 ++++++ apps/i2psnark/java/src/org/klomp/snark/PeerListener.java | 4 ++-- apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java | 2 +- apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java | 2 +- build.xml | 2 +- 6 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 apps/i2psnark/java/src/net/i2p/kademlia/package.html diff --git a/apps/i2psnark/java/src/net/i2p/kademlia/KBucketSet.java b/apps/i2psnark/java/src/net/i2p/kademlia/KBucketSet.java index e61c4a6fd..9b8f8474c 100644 --- a/apps/i2psnark/java/src/net/i2p/kademlia/KBucketSet.java +++ b/apps/i2psnark/java/src/net/i2p/kademlia/KBucketSet.java @@ -778,7 +778,6 @@ public class KBucketSet { * To always discard a newer entry, always return false. * * @param kbucket the kbucket that is now too big - * @param justAdded the entry that was just added, causing it to be too big * @return true to actually add the entry. */ public boolean trim(KBucket kbucket, K toAdd); diff --git a/apps/i2psnark/java/src/net/i2p/kademlia/package.html b/apps/i2psnark/java/src/net/i2p/kademlia/package.html new file mode 100644 index 000000000..fe1a24f43 --- /dev/null +++ b/apps/i2psnark/java/src/net/i2p/kademlia/package.html @@ -0,0 +1,6 @@ +

+This is a major rewrite of KBucket, KBucketSet, and KBucketImpl from net.i2p.router.networkdb.kademlia. +The classes are now generic to support SHA1. SHA256, or other key lengths. +The long-term goal is to prove out this new implementation in i2psnark, +then move it to core, then convert the network database to use it. +

diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java b/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java index ee2de562d..8b272bf81 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java @@ -194,10 +194,10 @@ interface PeerListener * * @param peer the Peer that got the message. * @param port the query port - * @param port the response port + * @param rport the response port * @since 0.8.4 */ - void gotPort(Peer peer, int port, int qport); + void gotPort(Peer peer, int port, int rport); /** * Called when peers are received via PEX diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java b/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java index 4dd704062..8c37600ec 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java @@ -98,7 +98,7 @@ class DHTNodes { /** * DHT - * @param sha1 either a InfoHash or a NID + * @param h either a InfoHash or a NID */ public List findClosest(SHA1Hash h, int numWant) { NID key; diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java index 37724825d..f892233e4 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java @@ -383,7 +383,7 @@ public class KRPC implements I2PSessionMuxedListener, DHT { * Non-blocking. * * @param ih the Info Hash (torrent) - * @param peer the peer's Hash + * @param peerHash the peer's Hash */ public void announce(byte[] ih, byte[] peerHash) { InfoHash iHash = new InfoHash(ih); diff --git a/build.xml b/build.xml index d69a8f95f..b8c35ab6c 100644 --- a/build.xml +++ b/build.xml @@ -422,7 +422,7 @@ - + From ced0129e0313f00a7c8c06445cd027811cc36f02 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 6 Aug 2012 14:04:32 +0000 Subject: [PATCH 20/23] * libjbigi.so for ARMv6 GMP 4.3.2 Compiled on Raspberry Pi with gcc version: gcc (Debian 4.6.3-8+rpi1) 4.6.3 java version "1.6.0_24" OpenJDK Runtime Environment (IcedTea6 1.11.3) (6b24-1.11.3-2+rpi1) OpenJDK Zero VM (build 20.0-b12, mixed mode) Stripped. Had a report that the ARMv5 jbigi worked on the RPi but it didn't for me. See NativeBigInteger for more info. /proc/cpuinfo: Processor : ARMv6-compatible processor rev 7 (v6l) BogoMIPS : 697.95 Features : swp half thumb fastmult vfp edsp java tls CPU implementer : 0x41 CPU architecture: 7 CPU variant : 0x0 CPU part : 0xb76 CPU revision : 7 Hardware : BCM2708 Revision : 0002 --- installer/lib/jbigi/libjbigi-linux-armv6.so | Bin 0 -> 88220 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 installer/lib/jbigi/libjbigi-linux-armv6.so diff --git a/installer/lib/jbigi/libjbigi-linux-armv6.so b/installer/lib/jbigi/libjbigi-linux-armv6.so new file mode 100644 index 0000000000000000000000000000000000000000..1f9eb86cf323f3e2e96b9f276206543a708ac25f GIT binary patch literal 88220 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 QQ|yz-LBL%n1xN|*SN@=TKL7v# literal 0 HcmV?d00001 From 91e61dbd5c191b64e2d6a72bd59202646e306573 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 6 Aug 2012 14:13:15 +0000 Subject: [PATCH 21/23] fix flag links so language selection returns to the same page --- apps/routerconsole/jsp/console.jsp | 40 +++++++++++++++--------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/apps/routerconsole/jsp/console.jsp b/apps/routerconsole/jsp/console.jsp index aa9168caf..b70c4170e 100644 --- a/apps/routerconsole/jsp/console.jsp +++ b/apps/routerconsole/jsp/console.jsp @@ -36,26 +36,26 @@
<% /* English, then alphabetical by English name please */ %> - English - عربية - 中文 - čeština - Dansk - Deutsch - Eesti - Español - Suomi - Français
- ελληνικά - Magyar - Italiano - Nederlands - Polski - Português - Русский - Svenska - Українська - Tiếng Việt + English + عربية + 中文 + čeština + Dansk + Deutsch + Eesti + Español + Suomi + Français
+ ελληνικά + Magyar + Italiano + Nederlands + Polski + Português + Русский + Svenska + Українська + Tiếng Việt

<%=intl._("Welcome to I2P")%>

From 1ab8200c7f2e0cf41014cd26cdea06b4c802376f Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 6 Aug 2012 14:45:37 +0000 Subject: [PATCH 22/23] * Clone System properties before iterating to avoid ConcurrentModificationException (ticket #680) --- .../client/streaming/I2PSocketManagerFactory.java | 14 ++++++++------ .../sam/java/src/net/i2p/sam/SAMStreamSession.java | 3 +-- .../java/src/net/i2p/sam/SAMv3StreamSession.java | 3 +-- core/java/src/net/i2p/I2PAppContext.java | 7 ++++--- core/java/src/net/i2p/client/I2PSessionImpl.java | 2 +- history.txt | 7 +++++++ router/java/src/net/i2p/router/RouterVersion.java | 2 +- 7 files changed, 23 insertions(+), 15 deletions(-) diff --git a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManagerFactory.java b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManagerFactory.java index 113d1e7ff..5c864110a 100644 --- a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManagerFactory.java +++ b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManagerFactory.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.util.Iterator; +import java.util.Map; import java.util.Properties; import net.i2p.I2PAppContext; @@ -36,7 +37,7 @@ public class I2PSocketManagerFactory { * @return the newly created socket manager, or null if there were errors */ public static I2PSocketManager createManager() { - return createManager(getHost(), getPort(), System.getProperties()); + return createManager(getHost(), getPort(), (Properties) System.getProperties().clone()); } /** @@ -59,7 +60,7 @@ public class I2PSocketManagerFactory { * @return the newly created socket manager, or null if there were errors */ public static I2PSocketManager createManager(String host, int port) { - return createManager(host, port, System.getProperties()); + return createManager(host, port, (Properties) System.getProperties().clone()); } /** @@ -95,7 +96,7 @@ public class I2PSocketManagerFactory { * @return the newly created socket manager, or null if there were errors */ public static I2PSocketManager createManager(InputStream myPrivateKeyStream) { - return createManager(myPrivateKeyStream, getHost(), getPort(), System.getProperties()); + return createManager(myPrivateKeyStream, getHost(), getPort(), (Properties) System.getProperties().clone()); } /** @@ -126,10 +127,11 @@ public class I2PSocketManagerFactory { I2PClient client = I2PClientFactory.createClient(); if (opts == null) opts = new Properties(); - for (Iterator iter = System.getProperties().keySet().iterator(); iter.hasNext(); ) { - String name = (String)iter.next(); + Properties syscopy = (Properties) System.getProperties().clone(); + for (Map.Entry e : syscopy.entrySet()) { + String name = (String) e.getKey(); if (!opts.containsKey(name)) - opts.setProperty(name, System.getProperty(name)); + opts.setProperty(name, (String) e.getValue()); } //boolean oldLib = DEFAULT_MANAGER.equals(opts.getProperty(PROP_MANAGER, DEFAULT_MANAGER)); //if (oldLib && false) { diff --git a/apps/sam/java/src/net/i2p/sam/SAMStreamSession.java b/apps/sam/java/src/net/i2p/sam/SAMStreamSession.java index d43f3a576..ab5d9068b 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMStreamSession.java +++ b/apps/sam/java/src/net/i2p/sam/SAMStreamSession.java @@ -124,8 +124,7 @@ public class SAMStreamSession { _log.debug("SAM STREAM session instantiated"); - Properties allprops = new Properties(); - allprops.putAll(System.getProperties()); + Properties allprops = (Properties) System.getProperties().clone(); allprops.putAll(props); String i2cpHost = allprops.getProperty(I2PClient.PROP_TCP_HOST, "127.0.0.1"); diff --git a/apps/sam/java/src/net/i2p/sam/SAMv3StreamSession.java b/apps/sam/java/src/net/i2p/sam/SAMv3StreamSession.java index 699058613..159798f60 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMv3StreamSession.java +++ b/apps/sam/java/src/net/i2p/sam/SAMv3StreamSession.java @@ -85,8 +85,7 @@ public class SAMv3StreamSession extends SAMStreamSession implements SAMv3Handle _log.debug("SAM STREAM session instantiated"); - Properties allprops = new Properties(); - allprops.putAll(System.getProperties()); + Properties allprops = (Properties) System.getProperties().clone(); allprops.putAll(rec.getProps()); String i2cpHost = allprops.getProperty(I2PClient.PROP_TCP_HOST, "127.0.0.1"); diff --git a/core/java/src/net/i2p/I2PAppContext.java b/core/java/src/net/i2p/I2PAppContext.java index 313f6e445..15189f876 100644 --- a/core/java/src/net/i2p/I2PAppContext.java +++ b/core/java/src/net/i2p/I2PAppContext.java @@ -516,7 +516,8 @@ public class I2PAppContext { * @return set of Strings containing the names of defined system properties */ public Set getPropertyNames() { - Set names = new HashSet(System.getProperties().keySet()); + // clone to avoid ConcurrentModificationException + Set names = new HashSet(((Properties) System.getProperties().clone()).keySet()); if (_overrideProps != null) names.addAll(_overrideProps.keySet()); return names; @@ -531,8 +532,8 @@ public class I2PAppContext { * @since 0.8.4 */ public Properties getProperties() { - Properties rv = new Properties(); - rv.putAll(System.getProperties()); + // clone to avoid ConcurrentModificationException + Properties rv = (Properties) System.getProperties().clone(); rv.putAll(_overrideProps); return rv; } diff --git a/core/java/src/net/i2p/client/I2PSessionImpl.java b/core/java/src/net/i2p/client/I2PSessionImpl.java index ed670149f..349f3d678 100644 --- a/core/java/src/net/i2p/client/I2PSessionImpl.java +++ b/core/java/src/net/i2p/client/I2PSessionImpl.java @@ -164,7 +164,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa _log = context.logManager().getLog(getClass()); _closed = true; if (options == null) - options = System.getProperties(); + options = (Properties) System.getProperties().clone(); loadConfig(options); } diff --git a/history.txt b/history.txt index eae3314f0..b2a84d686 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,10 @@ +2012-08-06 zzz + * Clone System properties before iterating to avoid + ConcurrentModificationException (ticket #680) + * Console: Fix flag links on /console to return to same page + * i2psnark: Add support for DHT (disabled by default) + * jbigi: Add ARMv6 libjbigi.so for Raspberry Pi + 2012-08-05 zzz * I2PSessionImpl: One more volatile (ticket #659) * i2ptunnel, I2CP, EepGet: Buffer socket input streams (ticket #666) diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 0725033fa..f6e6df3a5 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -18,7 +18,7 @@ public class RouterVersion { /** deprecated */ public final static String ID = "Monotone"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 3; + public final static long BUILD = 4; /** for example "-test" */ public final static String EXTRA = ""; From 3017e4f51a69ea9b75d2168be31776d56622fd09 Mon Sep 17 00:00:00 2001 From: str4d Date: Tue, 7 Aug 2012 12:18:16 +0000 Subject: [PATCH 23/23] Fixed .mtn-ignore to ignore build dirs properly --- .mtn-ignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.mtn-ignore b/.mtn-ignore index 67d2a1aa3..c42160e3c 100644 --- a/.mtn-ignore +++ b/.mtn-ignore @@ -17,10 +17,10 @@ _jsp\.java$ \.war$ \.zip$ ^\. -^build/ +^build ^pkg-temp/ ~$ -/build/ +/build /classes/ ^debian/copyright override.properties