package net.i2p.router; /* * free (adj.): unencumbered; not under the control of others * Written by jrandom in 2003 and released into the public domain * with no warranty of any kind, either expressed or implied. * It probably won't make your computer catch on fire, or eat * your children, but it might. Use at your own risk. * */ import java.io.Writer; import java.util.ArrayList; import java.util.Date; import java.util.List; import net.i2p.data.Hash; import net.i2p.data.router.RouterIdentity; import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.data.i2np.DatabaseSearchReplyMessage; import net.i2p.data.i2np.DeliveryStatusMessage; import net.i2p.data.i2np.I2NPMessage; import net.i2p.data.i2np.TunnelDataMessage; import net.i2p.data.i2np.TunnelGatewayMessage; import net.i2p.util.I2PThread; import net.i2p.util.Log; /** * Manage a pool of inbound InNetMessages. This pool is filled by the * Network communication system when it receives messages, and various jobs * periodically retrieve them for processing. * * Actually, this doesn't 'pool' anything, since DISPATCH_DIRECT = true. */ public class InNetMessagePool implements Service { private final Log _log; private final RouterContext _context; private final HandlerJobBuilder _handlerJobBuilders[]; /** following 5 unused unless DISPATCH_DIRECT == false */ private final List<I2NPMessage> _pendingDataMessages; private final List<Hash> _pendingDataMessagesFrom; private final List<I2NPMessage> _pendingGatewayMessages; private SharedShortCircuitDataJob _shortCircuitDataJob; private SharedShortCircuitGatewayJob _shortCircuitGatewayJob; private boolean _alive; private boolean _dispatchThreaded; /** Make this >= the max I2NP message type number (currently 24) */ private static final int MAX_I2NP_MESSAGE_TYPE = 31; /** * If set to true, we will have two additional threads - one for dispatching * tunnel data messages, and another for dispatching tunnel gateway messages. * These will not use the JobQueue but will operate sequentially. Otherwise, * if this is set to false, the messages will be queued up in the jobQueue, * using the jobQueue's single thread. * */ public static final String PROP_DISPATCH_THREADED = "router.dispatchThreaded"; public static final boolean DEFAULT_DISPATCH_THREADED = false; /** * If we aren't doing threaded dispatch for tunnel messages, should we * call the actual dispatch() method inline (on the same thread which * called add())? If false, we queue it up in a shared short circuit * job. */ private static final boolean DISPATCH_DIRECT = true; public InNetMessagePool(RouterContext context) { _context = context; _handlerJobBuilders = new HandlerJobBuilder[MAX_I2NP_MESSAGE_TYPE + 1]; if (DISPATCH_DIRECT) { // keep the compiler happy since they are final _pendingDataMessages = null; _pendingDataMessagesFrom = null; _pendingGatewayMessages = null; } else { _pendingDataMessages = new ArrayList<I2NPMessage>(16); _pendingDataMessagesFrom = new ArrayList<Hash>(16); _pendingGatewayMessages = new ArrayList<I2NPMessage>(16); _shortCircuitDataJob = new SharedShortCircuitDataJob(context); _shortCircuitGatewayJob = new SharedShortCircuitGatewayJob(context); } _log = _context.logManager().getLog(InNetMessagePool.class); _context.statManager().createRateStat("inNetPool.dropped", "How often do we drop a message", "InNetPool", new long[] { 60*60*1000l }); _context.statManager().createRateStat("inNetPool.droppedDeliveryStatusDelay", "How long after a delivery status message is created do we receive it back again (for messages that are too slow to be handled)", "InNetPool", new long[] { 60*60*1000l }); _context.statManager().createRateStat("inNetPool.duplicate", "How often do we receive a duplicate message", "InNetPool", new long[] { 60*60*1000l }); //_context.statManager().createRateStat("inNetPool.droppedTunnelCreateStatusMessage", "How often we drop a slow-to-arrive tunnel request response", "InNetPool", new long[] { 60*60*1000l, 24*60*60*1000l }); _context.statManager().createRateStat("inNetPool.droppedDbLookupResponseMessage", "How often we drop a slow-to-arrive db search response", "InNetPool", new long[] { 60*60*1000l }); } /** * @return previous builder for this message type, or null * @throws ArrayIndexOutOfBoundsException if i2npMessageType is greater than MAX_I2NP_MESSAGE_TYPE */ public synchronized HandlerJobBuilder registerHandlerJobBuilder(int i2npMessageType, HandlerJobBuilder builder) { HandlerJobBuilder old = _handlerJobBuilders[i2npMessageType]; _handlerJobBuilders[i2npMessageType] = builder; return old; } /** * @return previous builder for this message type, or null * @throws ArrayIndexOutOfBoundsException if i2npMessageType is greater than MAX_I2NP_MESSAGE_TYPE * @deprecated unused */ @Deprecated public synchronized HandlerJobBuilder unregisterHandlerJobBuilder(int i2npMessageType) { HandlerJobBuilder old = _handlerJobBuilders[i2npMessageType]; _handlerJobBuilders[i2npMessageType] = null; return old; } /** * Add a new message to the pool. * If there is * a HandlerJobBuilder for the inbound message type, the message is loaded * into a job created by that builder and queued up for processing instead * (though if the builder doesn't create a job, it is added to the pool) * * @param messageBody non-null * @param fromRouter may be null * @param fromRouterHash may be null, calculated from fromRouter if null * * @return -1 for some types of errors but not all; 0 otherwise * (was queue length, long ago) */ public int add(I2NPMessage messageBody, RouterIdentity fromRouter, Hash fromRouterHash) { long exp = messageBody.getMessageExpiration(); if (_log.shouldLog(Log.INFO)) _log.info("Rcvd" + " ID " + messageBody.getUniqueId() + " exp. " + new Date(exp) + " type " + messageBody.getClass().getSimpleName()); //if (messageBody instanceof DataMessage) { // _context.statManager().getStatLog().addData(fromRouterHash.toBase64().substring(0,6), "udp.floodDataReceived", 1, 0); // return 0; //} int type = messageBody.getType(); String invalidReason = null; if (type == TunnelDataMessage.MESSAGE_TYPE) { // the IV validator is sufficient for dup detection on tunnel messages, so // just validate the expiration invalidReason = _context.messageValidator().validateMessage(exp); } else { invalidReason = _context.messageValidator().validateMessage(messageBody.getUniqueId(), exp); } if (invalidReason != null) { int level = Log.WARN; //if (messageBody instanceof TunnelCreateMessage) // level = Log.INFO; if (_log.shouldLog(level)) _log.log(level, "Dropping message [" + messageBody.getUniqueId() + " expiring on " + exp + "]: " + messageBody.getClass().getSimpleName() + ": " + invalidReason + ": " + messageBody); _context.statManager().addRateData("inNetPool.dropped", 1); // FIXME not necessarily a duplicate, could be expired too long ago / too far in future _context.statManager().addRateData("inNetPool.duplicate", 1); _context.messageHistory().droppedOtherMessage(messageBody, (fromRouter != null ? fromRouter.calculateHash() : fromRouterHash)); _context.messageHistory().messageProcessingError(messageBody.getUniqueId(), messageBody.getClass().getSimpleName(), "Duplicate/expired"); return -1; } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("Message received [" + messageBody.getUniqueId() + " expiring on " + exp + "] is NOT a duplicate or exipired"); } boolean jobFound = false; boolean allowMatches = true; if (type == TunnelGatewayMessage.MESSAGE_TYPE) { shortCircuitTunnelGateway(messageBody); allowMatches = false; } else if (type == TunnelDataMessage.MESSAGE_TYPE) { shortCircuitTunnelData(messageBody, fromRouterHash); allowMatches = false; } else { // why don't we allow type 0? There used to be a message of type 0 long ago... if ( (type > 0) && (type < _handlerJobBuilders.length) ) { HandlerJobBuilder builder = _handlerJobBuilders[type]; if (_log.shouldLog(Log.DEBUG)) _log.debug("Add msg to the pool - builder: " + builder + " type: " + messageBody.getClass().getSimpleName()); if (builder != null) { Job job = builder.createJob(messageBody, fromRouter, fromRouterHash); if (job != null) { _context.jobQueue().addJob(job); jobFound = true; } else { // ok, we may not have *found* a job, per se, but we could have, the // job may have just executed inline jobFound = true; } } } } if (allowMatches) { int replies = handleReplies(messageBody); if (replies <= 0) { // not handled as a reply if (!jobFound) { // was not handled via HandlerJobBuilder _context.messageHistory().droppedOtherMessage(messageBody, (fromRouter != null ? fromRouter.calculateHash() : fromRouterHash)); if (type == DeliveryStatusMessage.MESSAGE_TYPE) { // Avoid logging side effect from a horrible UDP EstablishmentManager hack // We could set up a separate stat for it but don't bother for now long arr = ((DeliveryStatusMessage)messageBody).getArrival(); if (arr > 10) { long timeSinceSent = _context.clock().now() - arr; if (_log.shouldLog(Log.WARN)) _log.warn("Dropping unhandled delivery status message created " + timeSinceSent + "ms ago: " + messageBody); _context.statManager().addRateData("inNetPool.droppedDeliveryStatusDelay", timeSinceSent); } //} else if (type == TunnelCreateStatusMessage.MESSAGE_TYPE) { // if (_log.shouldLog(Log.INFO)) // _log.info("Dropping slow tunnel create request response: " + messageBody); // _context.statManager().addRateData("inNetPool.droppedTunnelCreateStatusMessage", 1, 0); } else if (type == DatabaseSearchReplyMessage.MESSAGE_TYPE) { if (_log.shouldLog(Log.INFO)) _log.info("Dropping slow db lookup response: " + messageBody); _context.statManager().addRateData("inNetPool.droppedDbLookupResponseMessage", 1); } else if (type == DatabaseLookupMessage.MESSAGE_TYPE) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Dropping netDb lookup due to throttling"); } else { if (_log.shouldLog(Log.WARN)) _log.warn("Message expiring on " + messageBody.getMessageExpiration() + " was not handled by a HandlerJobBuilder - DROPPING: " + messageBody, new Exception("f00!")); _context.statManager().addRateData("inNetPool.dropped", 1); } } else { String mtype = messageBody.getClass().getName(); _context.messageHistory().receiveMessage(mtype, messageBody.getUniqueId(), messageBody.getMessageExpiration(), fromRouterHash, true); return 0; // no queue } } } String mtype = messageBody.getClass().getName(); _context.messageHistory().receiveMessage(mtype, messageBody.getUniqueId(), messageBody.getMessageExpiration(), fromRouterHash, true); return 0; // no queue } public int handleReplies(I2NPMessage messageBody) { List<OutNetMessage> origMessages = _context.messageRegistry().getOriginalMessages(messageBody); int sz = origMessages.size(); if (sz <= 0) return 0; if (_log.shouldLog(Log.DEBUG)) { _log.debug("Original messages for inbound message: " + sz); if (sz > 1) _log.debug("Orig: " + origMessages + " \nthe above are replies for: " + messageBody); } for (int i = 0; i < sz; i++) { OutNetMessage omsg = origMessages.get(i); ReplyJob job = omsg.getOnReplyJob(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Original message [" + i + "] " + omsg.getReplySelector() + " : " + omsg + ": reply job: " + job); if (job != null) { job.setMessage(messageBody); _context.jobQueue().addJob(job); } } return sz; } // the following short circuits the tunnel dispatching - i'm not sure whether // we'll want to run the dispatching in jobs or whether it shuold go inline with // others and/or on other threads (e.g. transport threads). lets try 'em both. private void shortCircuitTunnelGateway(I2NPMessage messageBody) { if (DISPATCH_DIRECT) { doShortCircuitTunnelGateway(messageBody); } else { synchronized (_pendingGatewayMessages) { _pendingGatewayMessages.add(messageBody); _pendingGatewayMessages.notifyAll(); } if (!_dispatchThreaded) _context.jobQueue().addJob(_shortCircuitGatewayJob); } } private void doShortCircuitTunnelGateway(I2NPMessage messageBody) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Shortcut dispatch tunnelGateway message " + messageBody); _context.tunnelDispatcher().dispatch((TunnelGatewayMessage)messageBody); } private void shortCircuitTunnelData(I2NPMessage messageBody, Hash from) { if (DISPATCH_DIRECT) { doShortCircuitTunnelData(messageBody, from); } else { synchronized (_pendingDataMessages) { _pendingDataMessages.add(messageBody); _pendingDataMessagesFrom.add(from); _pendingDataMessages.notifyAll(); //_context.jobQueue().addJob(new ShortCircuitDataJob(_context, messageBody, from)); } if (!_dispatchThreaded) _context.jobQueue().addJob(_shortCircuitDataJob); } } private void doShortCircuitTunnelData(I2NPMessage messageBody, Hash from) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Shortcut dispatch tunnelData message " + messageBody); _context.tunnelDispatcher().dispatch((TunnelDataMessage)messageBody, from); } public void renderStatusHTML(Writer out) {} /** does nothing since we aren't threaded */ public synchronized void restart() { shutdown(); try { Thread.sleep(100); } catch (InterruptedException ie) {} startup(); } /** does nothing since we aren't threaded */ public synchronized void shutdown() { _alive = false; if (!DISPATCH_DIRECT) { synchronized (_pendingDataMessages) { _pendingDataMessages.clear(); _pendingDataMessagesFrom.clear(); _pendingDataMessages.notifyAll(); } } } /** does nothing since we aren't threaded */ public synchronized void startup() { _alive = true; _dispatchThreaded = DEFAULT_DISPATCH_THREADED; String threadedStr = _context.getProperty(PROP_DISPATCH_THREADED); if (threadedStr != null) { _dispatchThreaded = Boolean.parseBoolean(threadedStr); } if (_dispatchThreaded) { _context.statManager().createRateStat("pool.dispatchDataTime", "How long a tunnel dispatch takes", "Tunnels", new long[] { 10*60*1000l, 60*60*1000l, 24*60*60*1000l }); _context.statManager().createRateStat("pool.dispatchGatewayTime", "How long a tunnel gateway dispatch takes", "Tunnels", new long[] { 10*60*1000l, 60*60*1000l, 24*60*60*1000l }); I2PThread data = new I2PThread(new TunnelDataDispatcher(), "Tunnel data dispatcher"); data.setDaemon(true); data.start(); I2PThread gw = new I2PThread(new TunnelGatewayDispatcher(), "Tunnel gateway dispatcher"); gw.setDaemon(true); gw.start(); } } /** unused unless DISPATCH_DIRECT == false */ private class SharedShortCircuitDataJob extends JobImpl { public SharedShortCircuitDataJob(RouterContext ctx) { super(ctx); } public String getName() { return "Dispatch tunnel participant message"; } public void runJob() { int remaining = 0; I2NPMessage msg = null; Hash from = null; synchronized (_pendingDataMessages) { if (!_pendingDataMessages.isEmpty()) { msg = _pendingDataMessages.remove(0); from = _pendingDataMessagesFrom.remove(0); } remaining = _pendingDataMessages.size(); } if (msg != null) doShortCircuitTunnelData(msg, from); if (remaining > 0) getContext().jobQueue().addJob(SharedShortCircuitDataJob.this); } } /** unused unless DISPATCH_DIRECT == false */ private class SharedShortCircuitGatewayJob extends JobImpl { public SharedShortCircuitGatewayJob(RouterContext ctx) { super(ctx); } public String getName() { return "Dispatch tunnel gateway message"; } public void runJob() { I2NPMessage msg = null; int remaining = 0; synchronized (_pendingGatewayMessages) { if (!_pendingGatewayMessages.isEmpty()) msg = _pendingGatewayMessages.remove(0); remaining = _pendingGatewayMessages.size(); } if (msg != null) doShortCircuitTunnelGateway(msg); if (remaining > 0) getContext().jobQueue().addJob(SharedShortCircuitGatewayJob.this); } } /** unused unless router.dispatchThreaded=true */ private class TunnelGatewayDispatcher implements Runnable { public void run() { while (_alive) { I2NPMessage msg = null; try { synchronized (_pendingGatewayMessages) { if (_pendingGatewayMessages.isEmpty()) _pendingGatewayMessages.wait(); else msg = _pendingGatewayMessages.remove(0); } if (msg != null) { long before = _context.clock().now(); doShortCircuitTunnelGateway(msg); long elapsed = _context.clock().now() - before; _context.statManager().addRateData("pool.dispatchGatewayTime", elapsed); } } catch (InterruptedException ie) { } catch (OutOfMemoryError oome) { throw oome; } catch (RuntimeException e) { if (_log.shouldLog(Log.CRIT)) _log.log(Log.CRIT, "Error in the tunnel gateway dispatcher", e); } } } } /** unused unless router.dispatchThreaded=true */ private class TunnelDataDispatcher implements Runnable { public void run() { while (_alive) { I2NPMessage msg = null; Hash from = null; try { synchronized (_pendingDataMessages) { if (_pendingDataMessages.isEmpty()) { _pendingDataMessages.wait(); } else { msg = _pendingDataMessages.remove(0); from = _pendingDataMessagesFrom.remove(0); } } if (msg != null) { long before = _context.clock().now(); doShortCircuitTunnelData(msg, from); long elapsed = _context.clock().now() - before; _context.statManager().addRateData("pool.dispatchDataTime", elapsed); } } catch (InterruptedException ie) { } catch (OutOfMemoryError oome) { throw oome; } catch (RuntimeException e) { if (_log.shouldLog(Log.CRIT)) _log.log(Log.CRIT, "Error in the tunnel data dispatcher", e); } } } } }