propagate from branch 'i2p.i2p' (head 80aed456e1c6e4b17906153c9ee6dc9bc45e0eec)

to branch 'i2p.i2p.zzz.dhtsnark' (head dbf88ff4c1429f26656ad34fe0b9ba94305d726a)
This commit is contained in:
zzz
2012-06-22 15:13:04 +00:00
26 changed files with 2615 additions and 25 deletions

View File

@@ -28,6 +28,9 @@ abstract class ExtensionHandler {
public static final int ID_PEX = 2; public static final int ID_PEX = 2;
/** not ut_pex since the compact format is different */ /** not ut_pex since the compact format is different */
public static final String TYPE_PEX = "i2p_pex"; 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 */ /** 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 MAX_METADATA_SIZE = Storage.MAX_PIECES * 20 * 5 / 4;
private static final int PARALLEL_REQUESTS = 3; private static final int PARALLEL_REQUESTS = 3;
@@ -36,9 +39,10 @@ abstract class ExtensionHandler {
/** /**
* @param metasize -1 if unknown * @param metasize -1 if unknown
* @param pexAndMetadata advertise these capabilities * @param pexAndMetadata advertise these capabilities
* @param dht advertise DHT capability
* @return bencoded outgoing handshake message * @return bencoded outgoing handshake message
*/ */
public static byte[] getHandshake(int metasize, boolean pexAndMetadata) { public static byte[] getHandshake(int metasize, boolean pexAndMetadata, boolean dht) {
Map<String, Object> handshake = new HashMap(); Map<String, Object> handshake = new HashMap();
Map<String, Integer> m = new HashMap(); Map<String, Integer> m = new HashMap();
if (pexAndMetadata) { if (pexAndMetadata) {
@@ -47,6 +51,9 @@ abstract class ExtensionHandler {
if (metasize >= 0) if (metasize >= 0)
handshake.put("metadata_size", Integer.valueOf(metasize)); 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 // include the map even if empty so the far-end doesn't NPE
handshake.put("m", m); handshake.put("m", m);
handshake.put("p", Integer.valueOf(6881)); handshake.put("p", Integer.valueOf(6881));
@@ -65,6 +72,8 @@ abstract class ExtensionHandler {
handleMetadata(peer, listener, bs, log); handleMetadata(peer, listener, bs, log);
else if (id == ID_PEX) else if (id == ID_PEX)
handlePEX(peer, listener, bs, log); handlePEX(peer, listener, bs, log);
else if (id == ID_DHT)
handleDHT(peer, listener, bs, log);
else if (log.shouldLog(Log.INFO)) else if (log.shouldLog(Log.INFO))
log.info("Unknown extension msg " + id + " from " + peer); log.info("Unknown extension msg " + id + " from " + peer);
} }
@@ -87,6 +96,12 @@ abstract class ExtensionHandler {
// peer state calls peer listener calls sendPEX() // 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(); MagnetState state = peer.getMagnetState();
if (msgmap.get(TYPE_METADATA) == null) { if (msgmap.get(TYPE_METADATA) == null) {
@@ -335,6 +350,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<String, BEValue> 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 * added.f and dropped unsupported
* @param pList non-null * @param pList non-null
@@ -362,4 +399,22 @@ abstract class ExtensionHandler {
} }
} }
/**
* Send the DHT port numbers
* @since DHT
*/
public static void sendDHT(Peer peer, int qport, int rport) {
Map<String, Object> 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);
}
}
} }

View File

@@ -37,7 +37,7 @@ import net.i2p.util.SimpleTimer;
import net.i2p.util.Translate; import net.i2p.util.Translate;
import org.klomp.snark.dht.DHT; import org.klomp.snark.dht.DHT;
//import org.klomp.snark.dht.KRPC; import org.klomp.snark.dht.KRPC;
/** /**
* I2P specific helpers for I2PSnark * I2P specific helpers for I2PSnark
@@ -65,6 +65,7 @@ public class I2PSnarkUtil {
private final File _tmpDir; private final File _tmpDir;
private int _startupDelay; private int _startupDelay;
private boolean _shouldUseOT; private boolean _shouldUseOT;
private boolean _shouldUseDHT;
private boolean _areFilesPublic; private boolean _areFilesPublic;
private List<String> _openTrackers; private List<String> _openTrackers;
private DHT _dht; private DHT _dht;
@@ -77,7 +78,7 @@ public class I2PSnarkUtil {
public static final int DEFAULT_MAX_UP_BW = 8; //KBps public static final int DEFAULT_MAX_UP_BW = 8; //KBps
public static final int MAX_CONNECTIONS = 16; // per torrent public static final int MAX_CONNECTIONS = 16; // per torrent
public static final String PROP_MAX_BW = "i2cp.outboundBytesPerSecond"; public static final String PROP_MAX_BW = "i2cp.outboundBytesPerSecond";
//private static final boolean ENABLE_DHT = true; public static final boolean DEFAULT_USE_DHT = false;
public I2PSnarkUtil(I2PAppContext ctx) { public I2PSnarkUtil(I2PAppContext ctx) {
_context = ctx; _context = ctx;
@@ -94,6 +95,7 @@ public class I2PSnarkUtil {
_shouldUseOT = DEFAULT_USE_OPENTRACKERS; _shouldUseOT = DEFAULT_USE_OPENTRACKERS;
// FIXME split if default has more than one // FIXME split if default has more than one
_openTrackers = Collections.singletonList(DEFAULT_OPENTRACKERS); _openTrackers = Collections.singletonList(DEFAULT_OPENTRACKERS);
_shouldUseDHT = DEFAULT_USE_DHT;
// This is used for both announce replies and .torrent file downloads, // This is used for both announce replies and .torrent file downloads,
// so it must be available even if not connected to I2CP. // so it must be available even if not connected to I2CP.
// so much for multiple instances // so much for multiple instances
@@ -238,12 +240,14 @@ public class I2PSnarkUtil {
opts.setProperty("i2p.streaming.maxTotalConnsPerMinute", "8"); opts.setProperty("i2p.streaming.maxTotalConnsPerMinute", "8");
if (opts.getProperty("i2p.streaming.maxConnsPerHour") == null) if (opts.getProperty("i2p.streaming.maxConnsPerHour") == null)
opts.setProperty("i2p.streaming.maxConnsPerHour", "20"); 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); _manager = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, opts);
_connecting = false; _connecting = false;
} }
// FIXME this only instantiates krpc once, left stuck with old manager // FIXME this only instantiates krpc once, left stuck with old manager
//if (ENABLE_DHT && _manager != null && _dht == null) if (_shouldUseDHT && _manager != null && _dht == null)
// _dht = new KRPC(_context, _manager.getSession()); _dht = new KRPC(_context, _manager.getSession());
return (_manager != null); return (_manager != null);
} }
@@ -270,7 +274,11 @@ public class I2PSnarkUtil {
/** /**
* Destroy the destination itself * Destroy the destination itself
*/ */
public void disconnect() { public synchronized void disconnect() {
if (_dht != null) {
_dht.stop();
_dht = null;
}
I2PSocketManager mgr = _manager; I2PSocketManager mgr = _manager;
// FIXME this can cause race NPEs elsewhere // FIXME this can cause race NPEs elsewhere
_manager = null; _manager = null;
@@ -444,7 +452,8 @@ public class I2PSnarkUtil {
if (sess != null) { if (sess != null) {
byte[] b = Base32.decode(ip.substring(0, BASE32_HASH_LENGTH)); byte[] b = Base32.decode(ip.substring(0, BASE32_HASH_LENGTH));
if (b != null) { if (b != null) {
Hash h = new Hash(b); //Hash h = new Hash(b);
Hash h = Hash.create(b);
if (_log.shouldLog(Log.INFO)) if (_log.shouldLog(Log.INFO))
_log.info("Using existing session for lookup of " + ip); _log.info("Using existing session for lookup of " + ip);
try { try {
@@ -519,6 +528,22 @@ public class I2PSnarkUtil {
public boolean shouldUseOpenTrackers() { public boolean shouldUseOpenTrackers() {
return _shouldUseOT; 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 * Like DataHelper.toHexString but ensures no loss of leading zero bytes

View File

@@ -80,7 +80,9 @@ public class Peer implements Comparable
static final long OPTION_FAST = 0x0000000000000004l; static final long OPTION_FAST = 0x0000000000000004l;
static final long OPTION_DHT = 0x0000000000000001l; static final long OPTION_DHT = 0x0000000000000001l;
/** we use a different bit since the compact format is different */ /** 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_I2P_DHT = 0x0000000040000000l;
*/
static final long OPTION_AZMP = 0x1000000000000000l; static final long OPTION_AZMP = 0x1000000000000000l;
private long options; private long options;
@@ -269,15 +271,17 @@ public class Peer implements Comparable
_log.debug("Peer supports extensions, sending reply message"); _log.debug("Peer supports extensions, sending reply message");
int metasize = metainfo != null ? metainfo.getInfoBytes().length : -1; int metasize = metainfo != null ? metainfo.getInfoBytes().length : -1;
boolean pexAndMetadata = metainfo == null || !metainfo.isPrivate(); 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) { // Old DHT PORT message
if (_log.shouldLog(Log.DEBUG)) //if ((options & OPTION_I2P_DHT) != 0 && util.getDHT() != null) {
_log.debug("Peer supports DHT, sending PORT message"); // if (_log.shouldLog(Log.DEBUG))
int port = util.getDHT().getPort(); // _log.debug("Peer supports DHT, sending PORT message");
out.sendPort(port); // int port = util.getDHT().getPort();
} // out.sendPort(port);
//}
// Send our bitmap // Send our bitmap
if (bitfield != null) if (bitfield != null)

View File

@@ -1273,6 +1273,7 @@ class PeerCoordinator implements PeerListener
} }
} else if (id == ExtensionHandler.ID_HANDSHAKE) { } else if (id == ExtensionHandler.ID_HANDSHAKE) {
sendPeers(peer); sendPeers(peer);
sendDHT(peer);
} }
} }
@@ -1301,6 +1302,26 @@ class PeerCoordinator implements PeerListener
} catch (InvalidBEncodingException ibee) {} } 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<String, BEValue> 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 * Sets the storage after transition out of magnet mode
* Snark calls this after we call gotMetaInfo() * Snark calls this after we call gotMetaInfo()
@@ -1318,11 +1339,13 @@ class PeerCoordinator implements PeerListener
/** /**
* PeerListener callback * PeerListener callback
* Tell the DHT to ping it, this will get back the node info * Tell the DHT to ping it, this will get back the node info
* @param rport must be port + 1
* @since 0.8.4 * @since 0.8.4
*/ */
public void gotPort(Peer peer, int port) { public void gotPort(Peer peer, int port, int rport) {
DHT dht = _util.getDHT(); DHT dht = _util.getDHT();
if (dht != null) if (dht != null &&
port > 0 && port < 65535 && rport == port + 1)
dht.ping(peer.getDestination(), port); dht.ping(peer.getDestination(), port);
} }

View File

@@ -190,13 +190,14 @@ interface PeerListener
void gotExtension(Peer peer, int id, byte[] bs); 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 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 * @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 * Called when peers are received via PEX

View File

@@ -526,10 +526,14 @@ class PeerState implements DataLoader
setInteresting(true); setInteresting(true);
} }
/** @since 0.8.4 */ /**
* Unused
* @since 0.8.4
*/
void portMessage(int port) 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) void unknownMessage(int type, byte[] bs)

View File

@@ -90,6 +90,7 @@ public class SnarkManager implements Snark.CompleteListener {
private static final String PROP_USE_OPENTRACKERS = "i2psnark.useOpentrackers"; private static final String PROP_USE_OPENTRACKERS = "i2psnark.useOpentrackers";
public static final String PROP_OPENTRACKERS = "i2psnark.opentrackers"; public static final String PROP_OPENTRACKERS = "i2psnark.opentrackers";
public static final String PROP_PRIVATETRACKERS = "i2psnark.privatetrackers"; public static final String PROP_PRIVATETRACKERS = "i2psnark.privatetrackers";
private static final String PROP_USE_DHT = "i2psnark.enableDHT";
public static final int MIN_UP_BW = 2; public static final int MIN_UP_BW = 2;
public static final int DEFAULT_MAX_UP_BW = 10; public static final int DEFAULT_MAX_UP_BW = 10;
@@ -288,6 +289,9 @@ public class SnarkManager implements Snark.CompleteListener {
_config.setProperty(PROP_STARTUP_DELAY, Integer.toString(DEFAULT_STARTUP_DELAY)); _config.setProperty(PROP_STARTUP_DELAY, Integer.toString(DEFAULT_STARTUP_DELAY));
if (!_config.containsKey(PROP_THEME)) if (!_config.containsKey(PROP_THEME))
_config.setProperty(PROP_THEME, DEFAULT_THEME); _config.setProperty(PROP_THEME, DEFAULT_THEME);
// 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(); updateConfig();
} }
/** /**
@@ -362,6 +366,9 @@ public class SnarkManager implements Snark.CompleteListener {
String useOT = _config.getProperty(PROP_USE_OPENTRACKERS); String useOT = _config.getProperty(PROP_USE_OPENTRACKERS);
boolean bOT = useOT == null || Boolean.valueOf(useOT).booleanValue(); boolean bOT = useOT == null || Boolean.valueOf(useOT).booleanValue();
_util.setUseOpenTrackers(bOT); _util.setUseOpenTrackers(bOT);
// 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(); getDataDir().mkdirs();
initTrackerMap(); initTrackerMap();
} }
@@ -380,7 +387,7 @@ public class SnarkManager implements Snark.CompleteListener {
public void updateConfig(String dataDir, boolean filesPublic, boolean autoStart, String refreshDelay, public void updateConfig(String dataDir, boolean filesPublic, boolean autoStart, String refreshDelay,
String startDelay, String seedPct, String eepHost, String startDelay, String seedPct, String eepHost,
String eepPort, String i2cpHost, String i2cpPort, String i2cpOpts, 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; boolean changed = false;
//if (eepHost != null) { //if (eepHost != null) {
// // unused, we use socket eepget // // unused, we use socket eepget
@@ -564,6 +571,15 @@ public class SnarkManager implements Snark.CompleteListener {
_util.setUseOpenTrackers(useOpenTrackers); _util.setUseOpenTrackers(useOpenTrackers);
changed = true; 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 != null) {
if(!theme.equals(_config.getProperty(PROP_THEME))) { if(!theme.equals(_config.getProperty(PROP_THEME))) {
_config.setProperty(PROP_THEME, theme); _config.setProperty(PROP_THEME, theme);

View File

@@ -464,7 +464,8 @@ public class TrackerClient implements Runnable {
// announce ourselves while the token is still good // announce ourselves while the token is still good
// FIXME this needs to be in its own thread // FIXME this needs to be in its own thread
if (!stop) { 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); _util.debug("Sent " + good + " good announces to DHT", Snark.INFO);
} }

View File

@@ -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(); public int getPort();
/**
* @return The UDP response port
*/
public int getRPort();
/** /**
* Ping. We don't have a NID yet so the node is presumed * Ping. We don't have a NID yet so the node is presumed
* to be absent from our DHT. * to be absent from our DHT.
@@ -79,4 +84,9 @@ public interface DHT {
* @return the number of successful announces, not counting ourselves. * @return the number of successful announces, not counting ourselves.
*/ */
public int announce(byte[] ih, int max, long maxWait); public int announce(byte[] ih, int max, long maxWait);
/**
* Stop everything.
*/
public void stop();
} }

View File

@@ -0,0 +1,161 @@
package org.klomp.snark.dht;
/*
* From zzzot, modded and relicensed to GPLv2
*/
import java.util.ArrayList;
import java.util.Collection;
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.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.
*
* And a real Kademlia routing table, which stores node IDs only.
*
* @since 0.8.4
* @author zzz
*/
class DHTNodes {
private final I2PAppContext _context;
private long _expireTime;
private final Log _log;
private final ConcurrentHashMap<NID, NodeInfo> _nodeMap;
private final KBucketSet<NID> _kad;
private volatile boolean _isRunning;
/** 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 = 999;
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() {
_isRunning = true;
new Cleaner();
}
public void stop() {
clear();
_isRunning = false;
}
// begin ConcurrentHashMap methods
public int size() {
return _nodeMap.size();
}
public void clear() {
_kad.clear();
_nodeMap.clear();
}
public NodeInfo get(NID nid) {
return _nodeMap.get(nid);
}
/**
* @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);
}
public Collection<NodeInfo> values() {
return _nodeMap.values();
}
// end ConcurrentHashMap methods
/**
* DHT
* @param sha1 either a InfoHash or a NID
*/
public List<NodeInfo> findClosest(SHA1Hash h, int numWant) {
NID key;
if (h instanceof NID)
key = (NID) h;
else
key = new NID(h.getData());
List<NID> keys = _kad.getClosest(key, numWant);
List<NodeInfo> rv = new ArrayList(keys.size());
for (NID nid : keys) {
NodeInfo ninfo = _nodeMap.get(nid);
if (ninfo != null)
rv.add(ninfo);
}
return rv;
}
/**
* DHT - get random keys to explore
*/
public List<NID> getExploreKeys() {
return _kad.getExploreKeys(15*60*1000);
}
/** */
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<NodeInfo> iter = DHTNodes.this.values().iterator(); iter.hasNext(); ) {
NodeInfo peer = iter.next();
if (peer.lastSeen() < now - _expireTime) {
iter.remove();
_kad.remove(peer.getNID());
} 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.DEBUG))
_log.debug("DHT storage cleaner done, now with " +
peerCount + " peers, " +
DataHelper.formatDuration(_expireTime) + " expiration");
schedule(CLEAN_TIME);
}
}
}

View File

@@ -0,0 +1,143 @@
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.SimpleTimer2;
/**
* 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;
private volatile boolean _isRunning;
/** 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 = 2000;
DHTTracker(I2PAppContext ctx) {
_context = ctx;
_torrents = new Torrents();
_expireTime = MAX_EXPIRE_TIME;
_log = _context.logManager().getLog(DHTTracker.class);
}
public void start() {
_isRunning = true;
new Cleaner();
}
void stop() {
_torrents.clear();
_isRunning = false;
}
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<Hash> getPeers(InfoHash ih, int max) {
Peers peers = _torrents.get(ih);
if (peers == null)
return Collections.EMPTY_LIST;
int size = peers.size();
List<Hash> rv = new ArrayList(peers.values());
if (max < size) {
Collections.shuffle(rv, _context.random());
rv = rv.subList(0, max);
}
return rv;
}
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;
for (Iterator<Peers> iter = _torrents.values().iterator(); iter.hasNext(); ) {
Peers p = iter.next();
int recent = 0;
for (Iterator<Peer> 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.DEBUG))
_log.debug("DHT tracker cleaner done, now with " +
torrentCount + " torrents, " +
peerCount + " peers, " +
DataHelper.formatDuration(_expireTime) + " expiration");
schedule(CLEAN_TIME);
}
}
}

View File

@@ -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
*/
class InfoHash extends SHA1Hash {
public InfoHash(byte[] data) {
super(data);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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
*/
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);
}
}

View File

@@ -0,0 +1,46 @@
package org.klomp.snark.dht;
/*
* From zzzot, modded and relicensed to GPLv2
*/
import net.i2p.crypto.SHA1Hash;
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
*/
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);
}
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;
}
}

View File

@@ -0,0 +1,246 @@
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;
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.
* The conpact info is immutable. The Destination may be added later.
*
* @since 0.8.4
* @author zzz
*/
class NodeInfo extends SimpleDataStructure {
private final NID nID;
private final Hash hash;
private Destination dest;
private final int port;
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
*/
public NodeInfo(NID nID, Destination dest, int port) {
super();
this.nID = nID;
this.dest = dest;
this.hash = dest.calculateHash();
this.port = port;
initialize();
verify();
}
/**
* No Destination yet available
* @throws IllegalArgumentException
*/
public NodeInfo(NID nID, Hash hash, int port) {
super();
this.nID = nID;
this.hash = hash;
this.port = port;
initialize();
verify();
}
/**
* 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);
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();
}
/**
* Create from 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 54-byte compact info
* @throws IllegalArgumentException
*/
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);
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);
}
/**
* 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
*/
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))
throw new IllegalArgumentException("NID/Hash mismatch");
}
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 (this.dest != null)
return;
if (!dest.calculateHash().equals(this.hash))
throw new IllegalArgumentException("Hash mismatch, was: " + this.hash + " new: " + dest.calculateHash());
this.dest = dest;
}
public int getPort() {
return this.port;
}
public long lastSeen() {
return nID.lastSeen();
}
@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;
}
}
@Override
public String toString() {
return "NodeInfo: " + nID + ' ' + hash + " port: " + port + (dest != null ? " known dest" : " null dest");
}
/**
* 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();
}
}

View File

@@ -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<NodeInfo> {
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);
}
}

View File

@@ -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
*/
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;
}
}

View File

@@ -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
*/
class Peers extends ConcurrentHashMap<Hash, Peer> {
public Peers() {
super();
}
}

View File

@@ -0,0 +1,82 @@
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 {
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;
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.heardAbout(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;
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++;
}
} 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);
}
}

View File

@@ -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<SHA1Hash> {
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);
}
}

View File

@@ -0,0 +1,71 @@
package org.klomp.snark.dht;
/*
* GPLv2
*/
import java.util.Date;
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
*/
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);
setValid(MY_TOK_LEN);
lastSeen = ctx.clock().now();
}
/** incoming - save the token (arbitrary length) */
public Token(I2PAppContext ctx, byte[] data) {
super(data);
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();
}
}

View File

@@ -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
*/
class TokenKey extends SHA1Hash {
public TokenKey(NID nID, InfoHash ih) {
super(DataHelper.xor(nID.getData(), ih.getData()));
}
}

View File

@@ -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
*/
class Torrents extends ConcurrentHashMap<InfoHash, Peers> {
public Torrents() {
super();
}
}

View File

@@ -691,11 +691,12 @@ public class I2PSnarkServlet extends DefaultServlet {
String refreshDel = req.getParameter("refreshDelay"); String refreshDel = req.getParameter("refreshDelay");
String startupDel = req.getParameter("startupDelay"); String startupDel = req.getParameter("startupDelay");
boolean useOpenTrackers = req.getParameter("useOpenTrackers") != null; boolean useOpenTrackers = req.getParameter("useOpenTrackers") != null;
boolean useDHT = req.getParameter("useDHT") != null;
//String openTrackers = req.getParameter("openTrackers"); //String openTrackers = req.getParameter("openTrackers");
String theme = req.getParameter("theme"); String theme = req.getParameter("theme");
_manager.updateConfig(dataDir, filesPublic, autoStart, refreshDel, startupDel, _manager.updateConfig(dataDir, filesPublic, autoStart, refreshDel, startupDel,
seedPct, eepHost, eepPort, i2cpHost, i2cpPort, i2cpOpts, seedPct, eepHost, eepPort, i2cpHost, i2cpPort, i2cpOpts,
upLimit, upBW, useOpenTrackers, theme); upLimit, upBW, useOpenTrackers, useDHT, theme);
} else if ("Save2".equals(action)) { } else if ("Save2".equals(action)) {
String taction = req.getParameter("taction"); String taction = req.getParameter("taction");
if (taction != null) if (taction != null)
@@ -1457,6 +1458,7 @@ public class I2PSnarkServlet extends DefaultServlet {
boolean autoStart = _manager.shouldAutoStart(); boolean autoStart = _manager.shouldAutoStart();
boolean useOpenTrackers = _manager.util().shouldUseOpenTrackers(); boolean useOpenTrackers = _manager.util().shouldUseOpenTrackers();
//String openTrackers = _manager.util().getOpenTrackerString(); //String openTrackers = _manager.util().getOpenTrackerString();
boolean useDHT = _manager.util().shouldUseDHT();
//int seedPct = 0; //int seedPct = 0;
out.write("<form action=\"/i2psnark/configure\" method=\"POST\">\n" + out.write("<form action=\"/i2psnark/configure\" method=\"POST\">\n" +
@@ -1570,6 +1572,14 @@ public class I2PSnarkServlet extends DefaultServlet {
+ (useOpenTrackers ? "checked " : "") + (useOpenTrackers ? "checked " : "")
+ "title=\""); + "title=\"");
out.write(_("If checked, announce torrents to open trackers as well as the tracker listed in the torrent file")); out.write(_("If checked, announce torrents to open trackers as well as the tracker listed in the torrent file"));
out.write("\" ></td></tr>\n" +
"<tr><td>");
out.write(_("Enable DHT") + " (**BETA**)");
out.write(": <td><input type=\"checkbox\" class=\"optbox\" name=\"useDHT\" value=\"true\" "
+ (useDHT ? "checked " : "")
+ "title=\"");
out.write(_("If checked, use DHT"));
out.write("\" ></td></tr>\n"); out.write("\" ></td></tr>\n");
// "<tr><td>"); // "<tr><td>");

View File

@@ -258,5 +258,16 @@ public interface I2PSession {
public static final int PROTO_ANY = 0; public static final int PROTO_ANY = 0;
public static final int PROTO_UNSPECIFIED = 0; public static final int PROTO_UNSPECIFIED = 0;
public static final int PROTO_STREAMING = 6; 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; public static final int PROTO_DATAGRAM = 17;
/**
* A raw (unsigned) datagram
* @since 0.9.1
*/
public static final int PROTO_DATAGRAM_RAW = 18;
} }