+_("The eepsite was not reachable, because it uses encryption options that are not supported by your I2P or Java version.")
+
diff --git a/installer/resources/proxy/encp-header.ht b/installer/resources/proxy/encp-header.ht
new file mode 100644
index 0000000000..9d53fb7076
--- /dev/null
+++ b/installer/resources/proxy/encp-header.ht
@@ -0,0 +1,25 @@
+HTTP/1.1 504 Gateway Timeout
+Content-Type: text/html; charset=UTF-8
+Cache-control: no-cache
+Connection: close
+Proxy-Connection: close
+
+
+
+
+
_("Warning: Outproxy Unreachable")
+
+_("The HTTP outproxy was not reachable, because it uses encryption options that are not supported by your I2P or Java version.")
+_("You may want to {0}retry{1} as this will randomly reselect an outproxy from the pool you have defined {2}here{3} (if you have more than one configured).", "", "", "", "")
+_("If you continue to have trouble you may want to edit your outproxy list {0}here{1}.", "", "")
+
+
_("Could not connect to the following destination:")
diff --git a/router/java/src/net/i2p/router/NetworkDatabaseFacade.java b/router/java/src/net/i2p/router/NetworkDatabaseFacade.java
index 2e4ee3c150..fdd1fd30fb 100644
--- a/router/java/src/net/i2p/router/NetworkDatabaseFacade.java
+++ b/router/java/src/net/i2p/router/NetworkDatabaseFacade.java
@@ -14,6 +14,7 @@ import java.util.Collections;
import java.util.Set;
import net.i2p.data.DatabaseEntry;
+import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.data.LeaseSet;
import net.i2p.data.router.RouterInfo;
@@ -51,18 +52,51 @@ public abstract class NetworkDatabaseFacade implements Service {
public abstract LeaseSet lookupLeaseSetLocally(Hash key);
public abstract void lookupRouterInfo(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs);
public abstract RouterInfo lookupRouterInfoLocally(Hash key);
+
+ /**
+ * Lookup using the client's tunnels
+ * Succeeds even if LS validation fails due to unsupported sig type
+ *
+ * @param fromLocalDest use these tunnels for the lookup, or null for exploratory
+ * @since 0.9.16
+ */
+ public abstract void lookupDestination(Hash key, Job onFinishedJob, long timeoutMs, Hash fromLocalDest);
+
+ /**
+ * Lookup locally in netDB and in badDest cache
+ * Succeeds even if LS validation failed due to unsupported sig type
+ *
+ * @since 0.9.16
+ */
+ public abstract Destination lookupDestinationLocally(Hash key);
+
/**
- * return the leaseSet if another leaseSet already existed at that key
+ * @return the leaseSet if another leaseSet already existed at that key
*
* @throws IllegalArgumentException if the data is not valid
*/
public abstract LeaseSet store(Hash key, LeaseSet leaseSet) throws IllegalArgumentException;
+
/**
- * return the routerInfo if another router already existed at that key
+ * @return the routerInfo if another router already existed at that key
*
* @throws IllegalArgumentException if the data is not valid
*/
public abstract RouterInfo store(Hash key, RouterInfo routerInfo) throws IllegalArgumentException;
+
+ /**
+ * @return the old entry if it already existed at that key
+ * @throws IllegalArgumentException if the data is not valid
+ * @since 0.9.16
+ */
+ public DatabaseEntry store(Hash key, DatabaseEntry entry) throws IllegalArgumentException {
+ if (entry.getType() == DatabaseEntry.KEY_TYPE_ROUTERINFO)
+ return store(key, (RouterInfo) entry);
+ if (entry.getType() == DatabaseEntry.KEY_TYPE_LEASESET)
+ return store(key, (LeaseSet) entry);
+ throw new IllegalArgumentException("unknown type");
+ }
+
/**
* @throws IllegalArgumentException if the local router is not valid
*/
@@ -101,4 +135,12 @@ public abstract class NetworkDatabaseFacade implements Service {
* @since IPv6
*/
public boolean floodfillEnabled() { return false; };
+
+ /**
+ * Is it permanently negative cached?
+ *
+ * @param key only for Destinations; for RouterIdentities, see Banlist
+ * @since 0.9.16
+ */
+ public boolean isNegativeCachedForever(Hash key) { return false; }
}
diff --git a/router/java/src/net/i2p/router/PersistentKeyRing.java b/router/java/src/net/i2p/router/PersistentKeyRing.java
index a3e71ee8e2..920eec7d29 100644
--- a/router/java/src/net/i2p/router/PersistentKeyRing.java
+++ b/router/java/src/net/i2p/router/PersistentKeyRing.java
@@ -70,9 +70,8 @@ public class PersistentKeyRing extends KeyRing {
Hash h = e.getKey();
buf.append(h.toBase64().substring(0, 6)).append("…");
buf.append("
");
- LeaseSet ls = _ctx.netDb().lookupLeaseSetLocally(h);
- if (ls != null) {
- Destination dest = ls.getDestination();
+ Destination dest = _ctx.netDb().lookupDestinationLocally(h);
+ if (dest != null) {
if (_ctx.clientManager().isLocal(dest)) {
TunnelPoolSettings in = _ctx.tunnelManager().getInboundSettings(h);
if (in != null && in.getDestinationNickname() != null)
diff --git a/router/java/src/net/i2p/router/client/LookupDestJob.java b/router/java/src/net/i2p/router/client/LookupDestJob.java
index be08388ba2..911365b8e8 100644
--- a/router/java/src/net/i2p/router/client/LookupDestJob.java
+++ b/router/java/src/net/i2p/router/client/LookupDestJob.java
@@ -38,7 +38,11 @@ class LookupDestJob extends JobImpl {
}
/**
- * One of h or name non-null
+ * One of h or name non-null.
+ *
+ * For hash or b32 name, the dest will be returned if the LS can be found,
+ * even if the dest uses unsupported crypto.
+ *
* @param reqID must be >= 0 if name != null
* @param sessID must non-null if reqID >= 0
* @param fromLocalDest use these tunnels for the lookup, or null for exploratory
@@ -88,7 +92,7 @@ class LookupDestJob extends JobImpl {
returnFail();
} else {
DoneJob done = new DoneJob(getContext());
- getContext().netDb().lookupLeaseSet(_hash, done, done, _timeout, _fromLocalDest);
+ getContext().netDb().lookupDestination(_hash, done, _timeout, _fromLocalDest);
}
}
@@ -98,9 +102,9 @@ class LookupDestJob extends JobImpl {
}
public String getName() { return "LeaseSet Lookup Reply to Client"; }
public void runJob() {
- LeaseSet ls = getContext().netDb().lookupLeaseSetLocally(_hash);
- if (ls != null)
- returnDest(ls.getDestination());
+ Destination dest = getContext().netDb().lookupDestinationLocally(_hash);
+ if (dest != null)
+ returnDest(dest);
else
returnFail();
}
diff --git a/router/java/src/net/i2p/router/dummy/DummyNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/dummy/DummyNetworkDatabaseFacade.java
index 6c99bb90b3..460d7a7125 100644
--- a/router/java/src/net/i2p/router/dummy/DummyNetworkDatabaseFacade.java
+++ b/router/java/src/net/i2p/router/dummy/DummyNetworkDatabaseFacade.java
@@ -15,6 +15,7 @@ import java.util.Map;
import java.util.Set;
import net.i2p.data.DatabaseEntry;
+import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.data.LeaseSet;
import net.i2p.data.router.RouterInfo;
@@ -23,8 +24,8 @@ import net.i2p.router.NetworkDatabaseFacade;
import net.i2p.router.RouterContext;
public class DummyNetworkDatabaseFacade extends NetworkDatabaseFacade {
- private Map _routers;
- private RouterContext _context;
+ private final Map _routers;
+ private final RouterContext _context;
public DummyNetworkDatabaseFacade(RouterContext ctx) {
_routers = Collections.synchronizedMap(new HashMap());
@@ -42,6 +43,11 @@ public class DummyNetworkDatabaseFacade extends NetworkDatabaseFacade {
public void lookupLeaseSet(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) {}
public void lookupLeaseSet(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs, Hash fromLocalDest) {}
public LeaseSet lookupLeaseSetLocally(Hash key) { return null; }
+
+ public void lookupDestination(Hash key, Job onFinishedJob, long timeoutMs, Hash fromLocalDest) {}
+
+ public Destination lookupDestinationLocally(Hash key) { return null; }
+
public void lookupRouterInfo(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) {
RouterInfo info = lookupRouterInfoLocally(key);
if (info == null)
@@ -50,13 +56,16 @@ public class DummyNetworkDatabaseFacade extends NetworkDatabaseFacade {
_context.jobQueue().addJob(onFindJob);
}
public RouterInfo lookupRouterInfoLocally(Hash key) { return _routers.get(key); }
+
public void publish(LeaseSet localLeaseSet) {}
public void publish(RouterInfo localRouterInfo) {}
+
public LeaseSet store(Hash key, LeaseSet leaseSet) { return leaseSet; }
public RouterInfo store(Hash key, RouterInfo routerInfo) {
RouterInfo rv = _routers.put(key, routerInfo);
return rv;
}
+
public void unpublish(LeaseSet localLeaseSet) {}
public void fail(Hash dbEntry) {
_routers.remove(dbEntry);
diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
index 7da54172ce..d9f7a0a516 100644
--- a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
+++ b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
@@ -425,12 +425,19 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
getContext().statManager().addRateData("client.leaseSetFailedRemoteTime", lookupTime);
}
- //if (_finished == Result.NONE) {
+
+ int cause;
+ if (getContext().netDb().isNegativeCachedForever(_to.calculateHash())) {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Unable to send to " + _toString + " because the sig type is unsupported");
+ cause = MessageStatusMessage.STATUS_SEND_FAILURE_UNSUPPORTED_ENCRYPTION;
+ } else {
if (_log.shouldLog(Log.WARN))
_log.warn("Unable to send to " + _toString + " because we couldn't find their leaseSet");
- //}
+ cause = MessageStatusMessage.STATUS_SEND_FAILURE_NO_LEASESET;
+ }
- dieFatal(MessageStatusMessage.STATUS_SEND_FAILURE_NO_LEASESET);
+ dieFatal(cause);
}
}
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodOnlyLookupMatchJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodOnlyLookupMatchJob.java
index 0de6c6504f..46f985a917 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodOnlyLookupMatchJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodOnlyLookupMatchJob.java
@@ -2,10 +2,10 @@ package net.i2p.router.networkdb.kademlia;
import net.i2p.data.DatabaseEntry;
import net.i2p.data.LeaseSet;
-import net.i2p.data.router.RouterInfo;
import net.i2p.data.i2np.DatabaseSearchReplyMessage;
import net.i2p.data.i2np.DatabaseStoreMessage;
import net.i2p.data.i2np.I2NPMessage;
+import net.i2p.data.router.RouterInfo;
import net.i2p.router.JobImpl;
import net.i2p.router.ReplyJob;
import net.i2p.router.RouterContext;
@@ -62,6 +62,9 @@ class FloodOnlyLookupMatchJob extends JobImpl implements ReplyJob {
} else {
getContext().netDb().store(dsm.getKey(), (RouterInfo) dsm.getEntry());
}
+ } catch (UnsupportedCryptoException uce) {
+ _search.failed();
+ return;
} catch (IllegalArgumentException iae) {
if (_log.shouldLog(Log.WARN))
_log.warn(_search.getJobId() + ": Received an invalid store reply", iae);
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java
index 22d51249a2..e8a80368c4 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java
@@ -7,11 +7,12 @@ import java.util.Map;
import java.util.Set;
import net.i2p.data.DatabaseEntry;
+import net.i2p.data.Destination;
import net.i2p.data.Hash;
-import net.i2p.data.router.RouterInfo;
import net.i2p.data.TunnelId;
import net.i2p.data.i2np.DatabaseLookupMessage;
import net.i2p.data.i2np.DatabaseStoreMessage;
+import net.i2p.data.router.RouterInfo;
import net.i2p.router.Job;
import net.i2p.router.JobImpl;
import net.i2p.router.OutNetMessage;
@@ -31,7 +32,6 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad
private final Set _verifiesInProgress;
private FloodThrottler _floodThrottler;
private LookupThrottler _lookupThrottler;
- private NegativeLookupCache _negativeCache;
/**
* This is the flood redundancy. Entries are
@@ -65,7 +65,6 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad
_context.statManager().createRateStat("netDb.searchReplyNotValidated", "How many search replies we get that we are NOT able to validate (fetch)", "NetworkDatabase", new long[] { 5*60*1000l, 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l });
_context.statManager().createRateStat("netDb.searchReplyValidationSkipped", "How many search replies we get from unreliable peers that we skip?", "NetworkDatabase", new long[] { 5*60*1000l, 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l });
_context.statManager().createRateStat("netDb.republishQuantity", "How many peers do we need to send a found leaseSet to?", "NetworkDatabase", new long[] { 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l });
- _context.statManager().createRateStat("netDb.negativeCache", "Aborted lookup, already cached", "NetworkDatabase", new long[] { 60*60*1000l });
}
@Override
@@ -73,7 +72,6 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad
super.startup();
_context.jobQueue().addJob(new FloodfillMonitorJob(_context, this));
_lookupThrottler = new LookupThrottler();
- _negativeCache = new NegativeLookupCache();
// refresh old routers
Job rrj = new RefreshRoutersJob(_context, this);
@@ -171,25 +169,6 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad
return _lookupThrottler.shouldThrottle(from, id);
}
- /**
- * Increment in the negative lookup cache
- * @since 0.9.4
- */
- void lookupFailed(Hash key) {
- _negativeCache.lookupFailed(key);
- }
-
- /**
- * Is the key in the negative lookup cache?
- * @since 0.9.4
- */
- boolean isNegativeCached(Hash key) {
- boolean rv = _negativeCache.isCached(key);
- if (rv)
- _context.statManager().addRateData("netDb.negativeCache", 1);
- return rv;
- }
-
/**
* Send to a subset of all floodfill peers.
* We do this to implement Kademlia within the floodfills, i.e.
@@ -301,7 +280,9 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad
}
/**
- * Lookup using exploratory tunnels
+ * Lookup using exploratory tunnels.
+ *
+ * Caller should check negative cache and/or banlist before calling.
*
* Begin a kademlia style search for the key specified, which can take up to timeoutMs and
* will fire the appropriate jobs on success or timeout (or if the kademlia search completes
@@ -315,7 +296,10 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad
}
/**
- * Lookup using the client's tunnels
+ * Lookup using the client's tunnels.
+ *
+ * Caller should check negative cache and/or banlist before calling.
+ *
* @param fromLocalDest use these tunnels for the lookup, or null for exploratory
* @return null always
* @since 0.9.10
@@ -473,6 +457,7 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad
// should we skip the search?
if (_floodfillEnabled ||
_context.jobQueue().getMaxLag() > 500 ||
+ _context.banlist().isBanlistedForever(peer) ||
getKBucketSetSize() > MAX_DB_BEFORE_SKIPPING_SEARCH) {
// don't try to overload ourselves (e.g. failing 3000 router refs at
// once, and then firing off 3000 netDb lookup tasks)
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java
index 85d0bc974d..085c0c921a 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java
@@ -6,6 +6,7 @@ import java.util.Set;
import net.i2p.data.Certificate;
import net.i2p.data.DatabaseEntry;
+import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.data.LeaseSet;
import net.i2p.data.router.RouterInfo;
@@ -173,9 +174,9 @@ class FloodfillVerifyStoreJob extends JobImpl {
FloodfillPeerSelector sel = (FloodfillPeerSelector)_facade.getPeerSelector();
Certificate keyCert = null;
if (!_isRouterInfo) {
- LeaseSet ls = _facade.lookupLeaseSetLocally(_key);
- if (ls != null) {
- Certificate cert = ls.getDestination().getCertificate();
+ Destination dest = _facade.lookupDestinationLocally(_key);
+ if (dest != null) {
+ Certificate cert = dest.getCertificate();
if (cert.getCertificateType() == Certificate.CERTIFICATE_TYPE_KEY)
keyCert = cert;
}
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseStoreMessageJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseStoreMessageJob.java
index 57d1ee57bc..ffb6d77f00 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseStoreMessageJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseStoreMessageJob.java
@@ -51,6 +51,8 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl {
long recvBegin = System.currentTimeMillis();
String invalidMessage = null;
+ // set if invalid store but not his fault
+ boolean dontBlamePeer = false;
boolean wasNew = false;
RouterInfo prevNetDb = null;
Hash key = _message.getKey();
@@ -72,6 +74,7 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl {
if (getContext().clientManager().isLocal(key)) {
//getContext().statManager().addRateData("netDb.storeLocalLeaseSetAttempt", 1, 0);
// throw rather than return, so that we send the ack below (prevent easy attack)
+ dontBlamePeer = true;
throw new IllegalArgumentException("Peer attempted to store local leaseSet: " +
key.toBase64().substring(0, 4));
}
@@ -114,6 +117,9 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl {
//if (!ls.getReceivedAsReply())
// match.setReceivedAsPublished(true);
}
+ } catch (UnsupportedCryptoException uce) {
+ invalidMessage = uce.getMessage();
+ dontBlamePeer = true;
} catch (IllegalArgumentException iae) {
invalidMessage = iae.getMessage();
}
@@ -131,8 +137,10 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl {
if (getContext().routerHash().equals(key)) {
//getContext().statManager().addRateData("netDb.storeLocalRouterInfoAttempt", 1, 0);
// throw rather than return, so that we send the ack below (prevent easy attack)
+ dontBlamePeer = true;
throw new IllegalArgumentException("Peer attempted to store our RouterInfo");
}
+ getContext().profileManager().heardAbout(key);
prevNetDb = getContext().netDb().store(key, ri);
wasNew = ((null == prevNetDb) || (prevNetDb.getPublished() < ri.getPublished()));
// Check new routerinfo address against blocklist
@@ -152,7 +160,9 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl {
_log.warn("New address received, Blocklisting old peer " + key + ' ' + ri);
}
}
- getContext().profileManager().heardAbout(key);
+ } catch (UnsupportedCryptoException uce) {
+ invalidMessage = uce.getMessage();
+ dontBlamePeer = true;
} catch (IllegalArgumentException iae) {
invalidMessage = iae.getMessage();
}
@@ -165,14 +175,16 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl {
long recvEnd = System.currentTimeMillis();
getContext().statManager().addRateData("netDb.storeRecvTime", recvEnd-recvBegin);
- if (_message.getReplyToken() > 0)
+ // ack even if invalid or unsupported
+ // TODO any cases where we shouldn't?
+ if (_message.getReplyToken() > 0)
sendAck();
long ackEnd = System.currentTimeMillis();
if (_from != null)
_fromHash = _from.getHash();
if (_fromHash != null) {
- if (invalidMessage == null) {
+ if (invalidMessage == null || dontBlamePeer) {
getContext().profileManager().dbStoreReceived(_fromHash, wasNew);
getContext().statManager().addRateData("netDb.storeHandled", ackEnd-recvEnd);
} else {
@@ -180,7 +192,7 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl {
if (_log.shouldLog(Log.WARN))
_log.warn("Peer " + _fromHash.toBase64() + " sent bad data: " + invalidMessage);
}
- } else if (invalidMessage != null) {
+ } else if (invalidMessage != null && !dontBlamePeer) {
if (_log.shouldLog(Log.WARN))
_log.warn("Unknown peer sent bad data: " + invalidMessage);
}
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
index ba52e5d611..5507710cdc 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java
@@ -19,14 +19,20 @@ import java.util.Iterator;
import java.util.Map;
import java.util.Set;
+import net.i2p.crypto.SigType;
+import net.i2p.data.Certificate;
import net.i2p.data.DatabaseEntry;
+import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
+import net.i2p.data.Destination;
import net.i2p.data.Hash;
+import net.i2p.data.KeyCertificate;
import net.i2p.data.LeaseSet;
-import net.i2p.data.router.RouterAddress;
-import net.i2p.data.router.RouterInfo;
import net.i2p.data.i2np.DatabaseLookupMessage;
import net.i2p.data.i2np.DatabaseStoreMessage;
+import net.i2p.data.router.RouterAddress;
+import net.i2p.data.router.RouterIdentity;
+import net.i2p.data.router.RouterInfo;
import net.i2p.kademlia.KBucketSet;
import net.i2p.kademlia.RejectTrimmer;
import net.i2p.kademlia.SelectionCollector;
@@ -63,6 +69,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
protected final RouterContext _context;
private final ReseedChecker _reseedChecker;
private volatile long _lastRIPublishTime;
+ private NegativeLookupCache _negativeCache;
/**
* Map of Hash to RepublishLeaseSetJob for leases we'realready managing.
@@ -155,6 +162,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
_reseedChecker = new ReseedChecker(context);
context.statManager().createRateStat("netDb.lookupDeferred", "how many lookups are deferred?", "NetworkDatabase", new long[] { 60*60*1000 });
context.statManager().createRateStat("netDb.exploreKeySet", "how many keys are queued for exploration?", "NetworkDatabase", new long[] { 60*60*1000 });
+ context.statManager().createRateStat("netDb.negativeCache", "Aborted lookup, already cached", "NetworkDatabase", new long[] { 60*60*1000l });
// following are for StoreJob
context.statManager().createRateStat("netDb.storeRouterInfoSent", "How many routerInfo store messages have we sent?", "NetworkDatabase", new long[] { 60*60*1000l });
context.statManager().createRateStat("netDb.storeLeaseSetSent", "How many leaseSet store messages have we sent?", "NetworkDatabase", new long[] { 60*60*1000l });
@@ -223,6 +231,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
//_ds = null;
_exploreKeys.clear(); // hope this doesn't cause an explosion, it shouldn't.
// _exploreKeys = null;
+ _negativeCache.clear();
}
public synchronized void restart() {
@@ -262,6 +271,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
//_ds = new TransientDataStore();
// _exploreKeys = new HashSet(64);
_dbDir = dbDir;
+ _negativeCache = new NegativeLookupCache();
createHandlers();
@@ -480,7 +490,8 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
}
/**
- * Lookup using exploratory tunnels
+ * Lookup using exploratory tunnels.
+ * Use lookupDestination() if you don't need the LS or need it validated.
*/
public void lookupLeaseSet(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) {
lookupLeaseSet(key, onFindJob, onFailedLookupJob, timeoutMs, null);
@@ -488,6 +499,8 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
/**
* Lookup using the client's tunnels
+ * Use lookupDestination() if you don't need the LS or need it validated.
+ *
* @param fromLocalDest use these tunnels for the lookup, or null for exploratory
* @since 0.9.10
*/
@@ -500,6 +513,11 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
_log.debug("leaseSet found locally, firing " + onFindJob);
if (onFindJob != null)
_context.jobQueue().addJob(onFindJob);
+ } else if (isNegativeCached(key)) {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Negative cached, not searching: " + key);
+ if (onFailedLookupJob != null)
+ _context.jobQueue().addJob(onFailedLookupJob);
} else {
if (_log.shouldLog(Log.DEBUG))
_log.debug("leaseSet not found locally, running search");
@@ -509,6 +527,9 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
_log.debug("after lookupLeaseSet");
}
+ /**
+ * Use lookupDestination() if you don't need the LS or need it validated.
+ */
public LeaseSet lookupLeaseSetLocally(Hash key) {
if (!_initialized) return null;
DatabaseEntry ds = _ds.get(key);
@@ -531,6 +552,47 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
return null;
}
}
+
+ /**
+ * Lookup using the client's tunnels
+ * Succeeds even if LS validation and store fails due to unsupported sig type, expired, etc.
+ *
+ * Note that there are not separate success and fail jobs. Caller must call
+ * lookupDestinationLocally() in the job to determine success.
+ *
+ * @param onFinishedJob non-null
+ * @param fromLocalDest use these tunnels for the lookup, or null for exploratory
+ * @since 0.9.16
+ */
+ public void lookupDestination(Hash key, Job onFinishedJob, long timeoutMs, Hash fromLocalDest) {
+ if (!_initialized) return;
+ Destination d = lookupDestinationLocally(key);
+ if (d != null) {
+ _context.jobQueue().addJob(onFinishedJob);
+ } else {
+ search(key, onFinishedJob, onFinishedJob, timeoutMs, true, fromLocalDest);
+ }
+ }
+
+ /**
+ * Lookup locally in netDB and in badDest cache
+ * Succeeds even if LS validation fails due to unsupported sig type, expired, etc.
+ *
+ * @since 0.9.16
+ */
+ public Destination lookupDestinationLocally(Hash key) {
+ if (!_initialized) return null;
+ DatabaseEntry ds = _ds.get(key);
+ if (ds != null) {
+ if (ds.getType() == DatabaseEntry.KEY_TYPE_LEASESET) {
+ LeaseSet ls = (LeaseSet)ds;
+ return ls.getDestination();
+ }
+ } else {
+ return _negativeCache.getBadDest(key);
+ }
+ return null;
+ }
public void lookupRouterInfo(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) {
if (!_initialized) return;
@@ -538,6 +600,9 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
if (ri != null) {
if (onFindJob != null)
_context.jobQueue().addJob(onFindJob);
+ } else if (_context.banlist().isBanlistedForever(key)) {
+ if (onFailedLookupJob != null)
+ _context.jobQueue().addJob(onFailedLookupJob);
} else {
search(key, onFindJob, onFailedLookupJob, timeoutMs, false);
}
@@ -694,9 +759,10 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
* Unlike for RouterInfos, this is only called once, when stored.
* After that, LeaseSet.isCurrent() is used.
*
+ * @throws UnsupportedCryptoException if that's why it failed.
* @return reason why the entry is not valid, or null if it is valid
*/
- private String validate(Hash key, LeaseSet leaseSet) {
+ private String validate(Hash key, LeaseSet leaseSet) throws UnsupportedCryptoException {
if (!key.equals(leaseSet.getDestination().calculateHash())) {
if (_log.shouldLog(Log.WARN))
_log.warn("Invalid store attempt! key does not match leaseSet.destination! key = "
@@ -704,9 +770,11 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
return "Key does not match leaseSet.destination - " + key.toBase64();
}
if (!leaseSet.verifySignature()) {
+ // throws UnsupportedCryptoException
+ processStoreFailure(key, leaseSet);
if (_log.shouldLog(Log.WARN))
- _log.warn("Invalid leaseSet signature! leaseSet = " + leaseSet);
- return "Invalid leaseSet signature on " + leaseSet.getDestination().calculateHash().toBase64();
+ _log.warn("Invalid leaseSet signature! " + leaseSet);
+ return "Invalid leaseSet signature on " + key;
}
long earliest = leaseSet.getEarliestLeaseDate();
long latest = leaseSet.getLatestLeaseDate();
@@ -722,7 +790,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
+ " first exp. " + new Date(earliest)
+ " last exp. " + new Date(latest),
new Exception("Rejecting store"));
- return "Expired leaseSet for " + leaseSet.getDestination().calculateHash().toBase64()
+ return "Expired leaseSet for " + leaseSet.getDestination().calculateHash()
+ " expired " + DataHelper.formatDuration(age) + " ago";
}
if (latest > now + (Router.CLOCK_FUDGE_FACTOR + MAX_LEASE_FUTURE)) {
@@ -739,9 +807,13 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
}
/**
- * Store the leaseSet
+ * Store the leaseSet.
+ *
+ * If the store fails due to unsupported crypto, it will negative cache
+ * the hash until restart.
*
* @throws IllegalArgumentException if the leaseSet is not valid
+ * @throws UnsupportedCryptoException if that's why it failed.
* @return previous entry or null
*/
public LeaseSet store(Hash key, LeaseSet leaseSet) throws IllegalArgumentException {
@@ -798,6 +870,10 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
*
* Call this only on first store, to check the key and signature once
*
+ * If the store fails due to unsupported crypto, it will banlist
+ * the router hash until restart and then throw UnsupportedCrytpoException.
+ *
+ * @throws UnsupportedCryptoException if that's why it failed.
* @return reason why the entry is not valid, or null if it is valid
*/
private String validate(Hash key, RouterInfo routerInfo) throws IllegalArgumentException {
@@ -807,6 +883,8 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
return "Key does not match routerInfo.identity";
}
if (!routerInfo.isValid()) {
+ // throws UnsupportedCryptoException
+ processStoreFailure(key, routerInfo);
if (_log.shouldLog(Log.WARN))
_log.warn("Invalid routerInfo signature! forged router structure! router = " + routerInfo);
return "Invalid routerInfo signature";
@@ -892,15 +970,29 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
}
/**
- * store the routerInfo
+ * Store the routerInfo.
+ *
+ * If the store fails due to unsupported crypto, it will banlist
+ * the router hash until restart and then throw UnsupportedCrytpoException.
*
* @throws IllegalArgumentException if the routerInfo is not valid
+ * @throws UnsupportedCryptoException if that's why it failed.
* @return previous entry or null
*/
public RouterInfo store(Hash key, RouterInfo routerInfo) throws IllegalArgumentException {
return store(key, routerInfo, true);
}
+ /**
+ * Store the routerInfo.
+ *
+ * If the store fails due to unsupported crypto, it will banlist
+ * the router hash until restart and then throw UnsupportedCrytpoException.
+ *
+ * @throws IllegalArgumentException if the routerInfo is not valid
+ * @throws UnsupportedCryptoException if that's why it failed.
+ * @return previous entry or null
+ */
RouterInfo store(Hash key, RouterInfo routerInfo, boolean persist) throws IllegalArgumentException {
if (!_initialized) return null;
@@ -934,6 +1026,59 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
_kb.add(key);
return rv;
}
+
+ /**
+ * If the validate fails, call this
+ * to determine if it was because of unsupported crypto.
+ *
+ * If so, this will banlist-forever the router hash or permanently negative cache the dest hash,
+ * and then throw the exception. Otherwise it does nothing.
+ *
+ * @throws UnsupportedCryptoException if that's why it failed.
+ * @since 0.9.16
+ */
+ private void processStoreFailure(Hash h, DatabaseEntry entry) throws UnsupportedCryptoException {
+ if (entry.getHash().equals(h)) {
+ if (entry.getType() == DatabaseEntry.KEY_TYPE_LEASESET) {
+ LeaseSet ls = (LeaseSet) entry;
+ Destination d = ls.getDestination();
+ Certificate c = d.getCertificate();
+ if (c.getCertificateType() == Certificate.CERTIFICATE_TYPE_KEY) {
+ try {
+ KeyCertificate kc = c.toKeyCertificate();
+ SigType type = kc.getSigType();
+ if (type == null || !type.isAvailable()) {
+ failPermanently(d);
+ String stype = (type != null) ? type.toString() : Integer.toString(kc.getSigTypeCode());
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Unsupported sig type " + stype + " for destination " + h);
+ throw new UnsupportedCryptoException("Sig type " + stype);
+ }
+ } catch (DataFormatException dfe) {}
+ }
+ } else if (entry.getType() == DatabaseEntry.KEY_TYPE_ROUTERINFO) {
+ RouterInfo ri = (RouterInfo) entry;
+ RouterIdentity id = ri.getIdentity();
+ Certificate c = id.getCertificate();
+ if (c.getCertificateType() == Certificate.CERTIFICATE_TYPE_KEY) {
+ try {
+ KeyCertificate kc = c.toKeyCertificate();
+ SigType type = kc.getSigType();
+ if (type == null || !type.isAvailable()) {
+ String stype = (type != null) ? type.toString() : Integer.toString(kc.getSigTypeCode());
+ _context.banlist().banlistRouterForever(h, "Unsupported signature type " + stype);
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Unsupported sig type " + stype + " for router " + h);
+ throw new UnsupportedCryptoException("Sig type " + stype);
+ }
+ } catch (DataFormatException dfe) {}
+ }
+ }
+ }
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Verify fail, cause unknown: " + entry);
+ }
+
/**
* Final remove for a leaseset.
@@ -1005,8 +1150,12 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
* without any match)
*
* Unused - called only by FNDF.searchFull() from FloodSearchJob which is overridden - don't use this.
+ *
+ * @throws UnsupportedOperationException always
*/
SearchJob search(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs, boolean isLease) {
+ throw new UnsupportedOperationException();
+/****
if (!_initialized) return null;
boolean isNew = true;
SearchJob searchJob = null;
@@ -1031,6 +1180,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
_context.statManager().addRateData("netDb.lookupDeferred", deferred, searchJob.getExpiration()-_context.clock().now());
}
return searchJob;
+****/
}
/**
@@ -1102,6 +1252,47 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
_context.jobQueue().addJob(new StoreJob(_context, this, key, ds, onSuccess, onFailure, sendTimeout, toIgnore));
}
+ /**
+ * Increment in the negative lookup cache
+ *
+ * @param key for Destinations or RouterIdentities
+ * @since 0.9.4 moved from FNDF to KNDF in 0.9.16
+ */
+ void lookupFailed(Hash key) {
+ _negativeCache.lookupFailed(key);
+ }
+
+ /**
+ * Is the key in the negative lookup cache?
+ *&
+ * @param key for Destinations or RouterIdentities
+ * @since 0.9.4 moved from FNDF to KNDF in 0.9.16
+ */
+ boolean isNegativeCached(Hash key) {
+ boolean rv = _negativeCache.isCached(key);
+ if (rv)
+ _context.statManager().addRateData("netDb.negativeCache", 1);
+ return rv;
+ }
+
+ /**
+ * Negative cache until restart
+ * @since 0.9.16
+ */
+ void failPermanently(Destination dest) {
+ _negativeCache.failPermanently(dest);
+ }
+
+ /**
+ * Is it permanently negative cached?
+ *
+ * @param key only for Destinations; for RouterIdentities, see Banlist
+ * @since 0.9.16
+ */
+ public boolean isNegativeCachedForever(Hash key) {
+ return _negativeCache.getBadDest(key) != null;
+ }
+
/**
* Debug info, HTML formatted
* @since 0.9.10
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/NegativeLookupCache.java b/router/java/src/net/i2p/router/networkdb/kademlia/NegativeLookupCache.java
index abbb67ed62..1784f9434d 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/NegativeLookupCache.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/NegativeLookupCache.java
@@ -1,6 +1,9 @@
package net.i2p.router.networkdb.kademlia;
+import java.util.Map;
+import net.i2p.data.Destination;
import net.i2p.data.Hash;
+import net.i2p.util.LHMCache;
import net.i2p.util.ObjectCounter;
import net.i2p.util.SimpleScheduler;
import net.i2p.util.SimpleTimer;
@@ -12,11 +15,15 @@ import net.i2p.util.SimpleTimer;
*/
class NegativeLookupCache {
private final ObjectCounter counter;
+ private final Map badDests;
+
private static final int MAX_FAILS = 3;
+ private static final int MAX_BAD_DESTS = 128;
private static final long CLEAN_TIME = 2*60*1000;
public NegativeLookupCache() {
this.counter = new ObjectCounter();
+ this.badDests = new LHMCache(MAX_BAD_DESTS);
SimpleScheduler.getInstance().addPeriodicEvent(new Cleaner(), CLEAN_TIME);
}
@@ -25,7 +32,46 @@ class NegativeLookupCache {
}
public boolean isCached(Hash h) {
- return this.counter.count(h) >= MAX_FAILS;
+ if (counter.count(h) >= MAX_FAILS)
+ return true;
+ synchronized(badDests) {
+ return badDests.get(h) != null;
+ }
+ }
+
+ /**
+ * Negative cache the hash until restart,
+ * but cache the destination.
+ *
+ * @since 0.9.16
+ */
+ public void failPermanently(Destination dest) {
+ Hash h = dest.calculateHash();
+ synchronized(badDests) {
+ badDests.put(h, dest);
+ }
+ }
+
+ /**
+ * Get an unsupported but cached Destination
+ *
+ * @return dest or null if not cached
+ * @since 0.9.16
+ */
+ public Destination getBadDest(Hash h) {
+ synchronized(badDests) {
+ return badDests.get(h);
+ }
+ }
+
+ /**
+ * @since 0.9.16
+ */
+ public void clear() {
+ counter.clear();
+ synchronized(badDests) {
+ badDests.clear();
+ }
}
private class Cleaner implements SimpleTimer.TimedEvent {
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java
index 1c945b1eef..f0edded8b0 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java
@@ -202,22 +202,29 @@ class SearchJob extends JobImpl {
_log.debug(getJobId() + ": Already completed");
return;
}
+ if (_state.isAborted()) {
+ if (_log.shouldLog(Log.INFO))
+ _log.info(getJobId() + ": Search aborted");
+ _state.complete();
+ fail();
+ return;
+ }
if (_log.shouldLog(Log.INFO))
_log.info(getJobId() + ": Searching: " + _state);
if (isLocal()) {
if (_log.shouldLog(Log.INFO))
_log.info(getJobId() + ": Key found locally");
- _state.complete(true);
+ _state.complete();
succeed();
} else if (isExpired()) {
if (_log.shouldLog(Log.INFO))
_log.info(getJobId() + ": Key search expired");
- _state.complete(true);
+ _state.complete();
fail();
} else if (_state.getAttempted().size() > MAX_PEERS_QUERIED) {
if (_log.shouldLog(Log.INFO))
_log.info(getJobId() + ": Too many peers quried");
- _state.complete(true);
+ _state.complete();
fail();
} else {
//_log.debug("Continuing search");
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchState.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchState.java
index 106c8ad4dc..61bd1b645d 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/SearchState.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchState.java
@@ -19,15 +19,16 @@ import net.i2p.router.RouterContext;
*/
class SearchState {
private final RouterContext _context;
- private final HashSet _pendingPeers;
+ private final Set _pendingPeers;
private final Map _pendingPeerTimes;
- private final HashSet _attemptedPeers;
- private final HashSet _failedPeers;
- private final HashSet _successfulPeers;
- private final HashSet _repliedPeers;
+ private final Set _attemptedPeers;
+ private final Set _failedPeers;
+ private final Set _successfulPeers;
+ private final Set _repliedPeers;
private final Hash _searchKey;
private volatile long _completed;
private volatile long _started;
+ private volatile boolean _aborted;
public SearchState(RouterContext context, Hash key) {
_context = context;
@@ -87,10 +88,19 @@ class SearchState {
return new HashSet(_failedPeers);
}
}
+
public boolean completed() { return _completed != -1; }
- public void complete(boolean completed) {
- if (completed)
- _completed = _context.clock().now();
+
+ public void complete() {
+ _completed = _context.clock().now();
+ }
+
+ /** @since 0.9.16 */
+ public boolean isAborted() { return _aborted; }
+
+ /** @since 0.9.16 */
+ public void abort() {
+ _aborted = true;
}
public long getWhenStarted() { return _started; }
@@ -177,6 +187,8 @@ class SearchState {
buf.append(" completed? false ");
else
buf.append(" completed on ").append(new Date(_completed));
+ if (_aborted)
+ buf.append(" (Aborted)");
buf.append("\n\tAttempted: ");
synchronized (_attemptedPeers) {
buf.append(_attemptedPeers.size()).append(' ');
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchUpdateReplyFoundJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchUpdateReplyFoundJob.java
index 9974037164..63cc1c18f8 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/SearchUpdateReplyFoundJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchUpdateReplyFoundJob.java
@@ -81,34 +81,21 @@ class SearchUpdateReplyFoundJob extends JobImpl implements ReplyJob {
if (message instanceof DatabaseStoreMessage) {
long timeToReply = _state.dataFound(_peer);
-
DatabaseStoreMessage msg = (DatabaseStoreMessage)message;
DatabaseEntry entry = msg.getEntry();
- if (entry.getType() == DatabaseEntry.KEY_TYPE_LEASESET) {
- try {
- _facade.store(msg.getKey(), (LeaseSet) entry);
- getContext().profileManager().dbLookupSuccessful(_peer, timeToReply);
- } catch (IllegalArgumentException iae) {
- if (_log.shouldLog(Log.ERROR))
- _log.warn("Peer " + _peer + " sent us an invalid leaseSet: " + iae.getMessage());
- getContext().profileManager().dbLookupReply(_peer, 0, 0, 1, 0, timeToReply);
- }
- } else if (entry.getType() == DatabaseEntry.KEY_TYPE_ROUTERINFO) {
- if (_log.shouldLog(Log.INFO))
- _log.info(getJobId() + ": dbStore received on search containing router "
- + msg.getKey() + " with publishDate of "
- + new Date(entry.getDate()));
- try {
- _facade.store(msg.getKey(), (RouterInfo) entry);
- getContext().profileManager().dbLookupSuccessful(_peer, timeToReply);
- } catch (IllegalArgumentException iae) {
- if (_log.shouldLog(Log.ERROR))
- _log.warn("Peer " + _peer + " sent us an invalid routerInfo: " + iae.getMessage());
- getContext().profileManager().dbLookupReply(_peer, 0, 0, 1, 0, timeToReply);
- }
- } else {
- if (_log.shouldLog(Log.ERROR))
- _log.error(getJobId() + ": Unknown db store type?!@ " + entry.getType());
+ try {
+ _facade.store(msg.getKey(), entry);
+ getContext().profileManager().dbLookupSuccessful(_peer, timeToReply);
+ } catch (UnsupportedCryptoException iae) {
+ // don't blame the peer
+ getContext().profileManager().dbLookupSuccessful(_peer, timeToReply);
+ _state.abort();
+ // searchNext() will call fail()
+ } catch (IllegalArgumentException iae) {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Peer " + _peer + " sent us invalid data: ", iae);
+ // blame the peer
+ getContext().profileManager().dbLookupReply(_peer, 0, 0, 1, 0, timeToReply);
}
} else if (message instanceof DatabaseSearchReplyMessage) {
_job.replyFound((DatabaseSearchReplyMessage)message, _peer);
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/UnsupportedCryptoException.java b/router/java/src/net/i2p/router/networkdb/kademlia/UnsupportedCryptoException.java
new file mode 100644
index 0000000000..0159eb0201
--- /dev/null
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/UnsupportedCryptoException.java
@@ -0,0 +1,18 @@
+package net.i2p.router.networkdb.kademlia;
+
+/**
+ * Signature verification failed because the
+ * sig type is unknown or unavailable.
+ *
+ * @since 0.9.16
+ */
+public class UnsupportedCryptoException extends IllegalArgumentException {
+
+ public UnsupportedCryptoException(String msg) {
+ super(msg);
+ }
+
+ public UnsupportedCryptoException(String msg, Throwable t) {
+ super(msg, t);
+ }
+}
|