diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java index 8f1c240c35af0f167004437568433d3d63f93e9d..dfa9d7c210dc95fddaa2ad4b3718e803ede9e5ed 100644 --- a/router/java/src/net/i2p/router/RouterContext.java +++ b/router/java/src/net/i2p/router/RouterContext.java @@ -134,8 +134,8 @@ public class RouterContext extends I2PAppContext { _shitlist = new Shitlist(this); _blocklist = new Blocklist(this); _messageValidator = new MessageValidator(this); - //_throttle = new RouterThrottleImpl(this); - _throttle = new RouterDoSThrottle(this); + _throttle = new RouterThrottleImpl(this); + //_throttle = new RouterDoSThrottle(this); _integrationCalc = new IntegrationCalculator(this); _speedCalc = new SpeedCalculator(this); _capacityCalc = new CapacityCalculator(this); diff --git a/router/java/src/net/i2p/router/RouterDoSThrottle.java b/router/java/src/net/i2p/router/RouterDoSThrottle.java index 79471627a24996782fc018d6d0a5616812c7a887..5f49206bb5787e1617c238d3183c5658e2db7827 100644 --- a/router/java/src/net/i2p/router/RouterDoSThrottle.java +++ b/router/java/src/net/i2p/router/RouterDoSThrottle.java @@ -6,6 +6,7 @@ import net.i2p.data.Hash; * Minor extention of the router throttle to handle some DoS events and * throttle accordingly. * + * @deprecated unused */ class RouterDoSThrottle extends RouterThrottleImpl { public RouterDoSThrottle(RouterContext context) { diff --git a/router/java/src/net/i2p/router/RouterThrottleImpl.java b/router/java/src/net/i2p/router/RouterThrottleImpl.java index 750acb0730ec2ec3f3eb4ae15155e2d1591449d0..289f929a8ed9dcce027a04d0a67f0dccb82d2d22 100644 --- a/router/java/src/net/i2p/router/RouterThrottleImpl.java +++ b/router/java/src/net/i2p/router/RouterThrottleImpl.java @@ -72,6 +72,7 @@ class RouterThrottleImpl implements RouterThrottle { } } + /** @deprecated unused, function moved to netdb */ public boolean acceptNetDbLookupRequest(Hash key) { long lag = _context.jobQueue().getMaxLag(); if (lag > JOB_LAG_LIMIT) { diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodThrottler.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodThrottler.java index 303e8fc0ecce7881f84b87c7645363dfa99919bf..96b0f508d707be006e9e73face3c212003c0a33b 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodThrottler.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodThrottler.java @@ -7,6 +7,9 @@ import net.i2p.util.SimpleTimer; /** * Count how often we have recently flooded a key + * This offers basic DOS protection but is not a complete solution. + * + * @since 0.7.11 */ class FloodThrottler { private ObjectCounter<Hash> counter; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseLookupMessageHandler.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseLookupMessageHandler.java index 50e5b7f80c6bcb449f4baa6eef995a8830312829..8ef121d0605e8a4260335827cb8db82484f01265 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseLookupMessageHandler.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseLookupMessageHandler.java @@ -23,12 +23,15 @@ import net.i2p.util.Log; */ public class FloodfillDatabaseLookupMessageHandler implements HandlerJobBuilder { private RouterContext _context; + private FloodfillNetworkDatabaseFacade _facade; private Log _log; - public FloodfillDatabaseLookupMessageHandler(RouterContext context) { + + public FloodfillDatabaseLookupMessageHandler(RouterContext context, FloodfillNetworkDatabaseFacade facade) { _context = context; + _facade = facade; _log = context.logManager().getLog(FloodfillDatabaseLookupMessageHandler.class); - _context.statManager().createRateStat("netDb.lookupsReceived", "How many netDb lookups have we received?", "NetworkDatabase", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); - _context.statManager().createRateStat("netDb.lookupsDropped", "How many netDb lookups did we drop due to throttling?", "NetworkDatabase", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); + _context.statManager().createRateStat("netDb.lookupsReceived", "How many netDb lookups have we received?", "NetworkDatabase", new long[] { 60*60*1000l }); + _context.statManager().createRateStat("netDb.lookupsDropped", "How many netDb lookups did we drop due to throttling?", "NetworkDatabase", new long[] { 60*60*1000l }); // following are for ../HDLMJ _context.statManager().createRateStat("netDb.lookupsHandled", "How many netDb lookups have we handled?", "NetworkDatabase", new long[] { 60*60*1000l }); _context.statManager().createRateStat("netDb.lookupsMatched", "How many netDb lookups did we have the data for?", "NetworkDatabase", new long[] { 60*60*1000l }); @@ -42,18 +45,19 @@ public class FloodfillDatabaseLookupMessageHandler implements HandlerJobBuilder public Job createJob(I2NPMessage receivedMessage, RouterIdentity from, Hash fromHash) { _context.statManager().addRateData("netDb.lookupsReceived", 1, 0); - if (true || _context.throttle().acceptNetDbLookupRequest(((DatabaseLookupMessage)receivedMessage).getSearchKey())) { - Job j = new HandleFloodfillDatabaseLookupMessageJob(_context, (DatabaseLookupMessage)receivedMessage, from, fromHash); - if (false) { - // might as well inline it, all the heavy lifting is queued up in later jobs, if necessary - j.runJob(); - return null; - } else { + DatabaseLookupMessage dlm = (DatabaseLookupMessage)receivedMessage; + if (!_facade.shouldThrottleLookup(dlm.getFrom(), dlm.getReplyTunnel())) { + Job j = new HandleFloodfillDatabaseLookupMessageJob(_context, dlm, from, fromHash); + //if (false) { + // // might as well inline it, all the heavy lifting is queued up in later jobs, if necessary + // j.runJob(); + // return null; + //} else { return j; - } + //} } else { - if (_log.shouldLog(Log.INFO)) - _log.info("Dropping lookup request as throttled"); + if (_log.shouldLog(Log.WARN)) + _log.warn("Dropping lookup request for " + dlm.getSearchKey() + " (throttled), reply was to: " + dlm.getFrom() + " tunnel: " + dlm.getReplyTunnel()); _context.statManager().addRateData("netDb.lookupsDropped", 1, 1); return null; } 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 09e59c12e50af58470622b0069d442507f10fca1..8993da43342b163a39de5d43e40d8bc6062bae18 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java @@ -12,6 +12,7 @@ import net.i2p.data.DataStructure; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; import net.i2p.data.RouterInfo; +import net.i2p.data.TunnelId; import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.data.i2np.DatabaseSearchReplyMessage; import net.i2p.data.i2np.DatabaseStoreMessage; @@ -38,6 +39,7 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad private static String _alwaysQuery; private final Set<Hash> _verifiesInProgress; private FloodThrottler _floodThrottler; + private LookupThrottler _lookupThrottler; public FloodfillNetworkDatabaseFacade(RouterContext context) { super(context); @@ -63,11 +65,12 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad public void startup() { super.startup(); _context.jobQueue().addJob(new FloodfillMonitorJob(_context, this)); + _lookupThrottler = new LookupThrottler(); } @Override protected void createHandlers() { - _context.inNetMessagePool().registerHandlerJobBuilder(DatabaseLookupMessage.MESSAGE_TYPE, new FloodfillDatabaseLookupMessageHandler(_context)); + _context.inNetMessagePool().registerHandlerJobBuilder(DatabaseLookupMessage.MESSAGE_TYPE, new FloodfillDatabaseLookupMessageHandler(_context, this)); _context.inNetMessagePool().registerHandlerJobBuilder(DatabaseStoreMessage.MESSAGE_TYPE, new FloodfillDatabaseStoreMessageHandler(_context, this)); } @@ -103,6 +106,22 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad } } + /** + * Increments and tests. + * @since 0.7.11 + */ + boolean shouldThrottleFlood(Hash key) { + return _floodThrottler != null && _floodThrottler.shouldThrottle(key); + } + + /** + * Increments and tests. + * @since 0.7.11 + */ + boolean shouldThrottleLookup(Hash from, TunnelId id) { + return _lookupThrottler.shouldThrottle(from, id); + } + private static final int MAX_TO_FLOOD = 7; /** @@ -116,13 +135,6 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad key = ((LeaseSet)ds).getDestination().calculateHash(); else key = ((RouterInfo)ds).getIdentity().calculateHash(); - // DOS prevention - if (_floodThrottler != null && _floodThrottler.shouldThrottle(key)) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Too many recent stores, not flooding key: " + key); - _context.statManager().addRateData("netDb.floodThrottled", 1, 0); - return; - } Hash rkey = _context.routingKeyGenerator().getRoutingKey(key); FloodfillPeerSelector sel = (FloodfillPeerSelector)getPeerSelector(); List peers = sel.selectFloodfillParticipants(rkey, MAX_TO_FLOOD, getKBuckets()); 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 d57f3bf97d61693600c7f0bb032a157fa7845915..452d8a60b9cd73273a41157eb984e13f4b2a4f0f 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseStoreMessageJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseStoreMessageJob.java @@ -54,9 +54,9 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl { String invalidMessage = null; boolean wasNew = false; RouterInfo prevNetDb = null; + Hash key = _message.getKey(); if (_message.getValueType() == DatabaseStoreMessage.KEY_TYPE_LEASESET) { getContext().statManager().addRateData("netDb.storeLeaseSetHandled", 1, 0); - Hash key = _message.getKey(); if (_log.shouldLog(Log.INFO)) _log.info("Handling dbStore of leaseset " + _message); //_log.info("Handling dbStore of leasset " + key + " with expiration of " @@ -92,7 +92,6 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl { } } else if (_message.getValueType() == DatabaseStoreMessage.KEY_TYPE_ROUTERINFO) { getContext().statManager().addRateData("netDb.storeRouterInfoHandled", 1, 0); - Hash key = _message.getKey(); if (_log.shouldLog(Log.INFO)) _log.info("Handling dbStore of router " + key + " with publishDate of " + new Date(_message.getRouterInfo().getPublished())); @@ -163,6 +162,14 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl { FloodfillNetworkDatabaseFacade.floodfillEnabled(getContext()) && _message.getReplyToken() > 0) { if (wasNew) { + // DOS prevention + // Note this does not throttle the ack above + if (_facade.shouldThrottleFlood(key)) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Too many recent stores, not flooding key: " + key); + getContext().statManager().addRateData("netDb.floodThrottled", 1, 0); + return; + } long floodBegin = System.currentTimeMillis(); if (_message.getValueType() == DatabaseStoreMessage.KEY_TYPE_LEASESET) _facade.flood(_message.getLeaseSet()); diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/LookupThrottler.java b/router/java/src/net/i2p/router/networkdb/kademlia/LookupThrottler.java new file mode 100644 index 0000000000000000000000000000000000000000..b4cef3621e5db85747462ed1281e38be05f0a3d1 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/LookupThrottler.java @@ -0,0 +1,70 @@ +package net.i2p.router.networkdb.kademlia; + +import net.i2p.data.Hash; +import net.i2p.data.TunnelId; +import net.i2p.util.ObjectCounter; +import net.i2p.util.SimpleScheduler; +import net.i2p.util.SimpleTimer; + +/** + * Count how often we have recently received a lookup request with + * the reply specified to go to a peer/TunnelId pair. + * This offers basic DOS protection but is not a complete solution. + * The reply peer/tunnel could be spoofed, for example. + * And a requestor could have up to 6 reply tunnels. + * + * @since 0.7.11 + */ +class LookupThrottler { + private ObjectCounter<ReplyTunnel> counter; + /** the id of this is -1 */ + private static final TunnelId DUMMY_ID = new TunnelId(); + /** this seems like plenty */ + private static final int MAX_LOOKUPS = 30; + private static final long CLEAN_TIME = 60*1000; + + LookupThrottler() { + this.counter = new ObjectCounter(); + SimpleScheduler.getInstance().addPeriodicEvent(new Cleaner(), CLEAN_TIME); + } + + /** + * increments before checking + * @param key non-null + * @param id null if for direct lookups + */ + boolean shouldThrottle(Hash key, TunnelId id) { + return this.counter.increment(new ReplyTunnel(key, id)) > MAX_LOOKUPS; + } + + private class Cleaner implements SimpleTimer.TimedEvent { + public void timeReached() { + LookupThrottler.this.counter.clear(); + } + } + + /** yes, we could have a two-level lookup, or just do h.tostring() + id.tostring() */ + private static class ReplyTunnel { + public Hash h; + public TunnelId id; + + ReplyTunnel(Hash h, TunnelId id) { + this.h = h; + if (id != null) + this.id = id; + else + this.id = DUMMY_ID; + } + + @Override + public boolean equals(Object obj) { + return this.h.equals(((ReplyTunnel)obj).h) && + this.id.equals(((ReplyTunnel)obj).id); + } + + @Override + public int hashCode() { + return this.h.hashCode() + this.id.hashCode(); + } + } +}