package net.i2p.router.networkdb.kademlia; import java.util.HashSet; import java.util.List; 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; import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.data.i2np.DatabaseSearchReplyMessage; import net.i2p.data.i2np.DatabaseStoreMessage; import net.i2p.data.i2np.I2NPMessage; import net.i2p.router.JobImpl; import net.i2p.router.MessageSelector; import net.i2p.router.ReplyJob; import net.i2p.router.RouterContext; import net.i2p.router.TunnelInfo; import net.i2p.router.util.MaskedIPSet; import net.i2p.util.Log; /** * Send a netDb lookup to a floodfill peer - If it is found, great, * but if they reply back saying they dont know it, queue up a store of the * key to a random floodfill peer again (via FloodfillStoreJob) * */ class FloodfillVerifyStoreJob extends JobImpl { private final Log _log; private final Hash _key; private Hash _target; private final Hash _sentTo; private final FloodfillNetworkDatabaseFacade _facade; private long _expiration; private long _sendTime; private final long _published; private final boolean _isRouterInfo; private MessageWrapper.WrappedMessage _wrappedMessage; private final Set<Hash> _ignore; private final MaskedIPSet _ipSet; private static final int START_DELAY = 18*1000; private static final int START_DELAY_RAND = 9*1000; private static final int VERIFY_TIMEOUT = 20*1000; private static final int MAX_PEERS_TO_TRY = 4; private static final int IP_CLOSE_BYTES = 3; /** * Delay a few seconds, then start the verify * @param sentTo who to give the credit or blame to, can be null */ public FloodfillVerifyStoreJob(RouterContext ctx, Hash key, long published, boolean isRouterInfo, Hash sentTo, FloodfillNetworkDatabaseFacade facade) { super(ctx); facade.verifyStarted(key); _key = key; _published = published; _isRouterInfo = isRouterInfo; _log = ctx.logManager().getLog(getClass()); _sentTo = sentTo; _facade = facade; _ignore = new HashSet<Hash>(MAX_PEERS_TO_TRY); if (sentTo != null) { _ipSet = new MaskedIPSet(ctx, sentTo, IP_CLOSE_BYTES); _ignore.add(_sentTo); } else { _ipSet = new MaskedIPSet(4); } // wait some time before trying to verify the store getTiming().setStartAfter(ctx.clock().now() + START_DELAY + ctx.random().nextInt(START_DELAY_RAND)); getContext().statManager().createRateStat("netDb.floodfillVerifyOK", "How long a floodfill verify takes when it succeeds", "NetworkDatabase", new long[] { 60*60*1000 }); getContext().statManager().createRateStat("netDb.floodfillVerifyFail", "How long a floodfill verify takes when it fails", "NetworkDatabase", new long[] { 60*60*1000 }); getContext().statManager().createRateStat("netDb.floodfillVerifyTimeout", "How long a floodfill verify takes when it times out", "NetworkDatabase", new long[] { 60*60*1000 }); } public String getName() { return "Verify netdb store"; } /** * Query a random floodfill for the leaseset or routerinfo * that we just stored to a (hopefully different) floodfill peer. * * If it fails (after a timeout period), resend the data. * If the queried data is older than what we stored, that counts as a fail. **/ public void runJob() { _target = pickTarget(); if (_target == null) { _facade.verifyFinished(_key); return; } boolean isInboundExploratory; TunnelInfo replyTunnelInfo; if (_isRouterInfo || getContext().keyRing().get(_key) != null) { replyTunnelInfo = getContext().tunnelManager().selectInboundExploratoryTunnel(_target); isInboundExploratory = true; } else { replyTunnelInfo = getContext().tunnelManager().selectInboundTunnel(_key, _target); isInboundExploratory = false; } if (replyTunnelInfo == null) { if (_log.shouldLog(Log.WARN)) _log.warn("No inbound tunnels to get a reply from!"); return; } DatabaseLookupMessage lookup = buildLookup(replyTunnelInfo); // If we are verifying a leaseset, use the destination's own tunnels, // to avoid association by the exploratory tunnel OBEP. // Unless it is an encrypted leaseset. TunnelInfo outTunnel; if (_isRouterInfo || getContext().keyRing().get(_key) != null) outTunnel = getContext().tunnelManager().selectOutboundExploratoryTunnel(_target); else outTunnel = getContext().tunnelManager().selectOutboundTunnel(_key, _target); if (outTunnel == null) { if (_log.shouldLog(Log.WARN)) _log.warn("No outbound tunnels to verify a store"); _facade.verifyFinished(_key); return; } // garlic encrypt to hide contents from the OBEP RouterInfo peer = _facade.lookupRouterInfoLocally(_target); if (peer == null) { if (_log.shouldLog(Log.WARN)) _log.warn("Fail finding target RI"); _facade.verifyFinished(_key); return; } if (DatabaseLookupMessage.supportsEncryptedReplies(peer)) { // register the session with the right SKM MessageWrapper.OneTimeSession sess; if (isInboundExploratory) { sess = MessageWrapper.generateSession(getContext()); } else { sess = MessageWrapper.generateSession(getContext(), _key); if (sess == null) { if (_log.shouldLog(Log.WARN)) _log.warn("No SKM to reply to"); _facade.verifyFinished(_key); return; } } if (_log.shouldLog(Log.INFO)) _log.info("Requesting encrypted reply from " + _target + ' ' + sess.key + ' ' + sess.tag); lookup.setReplySession(sess.key, sess.tag); } Hash fromKey; if (_isRouterInfo) fromKey = null; else fromKey = _key; _wrappedMessage = MessageWrapper.wrap(getContext(), lookup, fromKey, peer); if (_wrappedMessage == null) { if (_log.shouldLog(Log.WARN)) _log.warn("Fail Garlic encrypting"); _facade.verifyFinished(_key); return; } I2NPMessage sent = _wrappedMessage.getMessage(); if (_log.shouldLog(Log.INFO)) _log.info("Starting verify (stored " + _key + " to " + _sentTo + "), asking " + _target); _sendTime = getContext().clock().now(); _expiration = _sendTime + VERIFY_TIMEOUT; getContext().messageRegistry().registerPending(new VerifyReplySelector(), new VerifyReplyJob(getContext()), new VerifyTimeoutJob(getContext())); getContext().tunnelDispatcher().dispatchOutbound(sent, outTunnel.getSendTunnelId(0), _target); } /** * Pick a responsive floodfill close to the key, but not the one we sent to */ private Hash pickTarget() { Hash rkey = getContext().routingKeyGenerator().getRoutingKey(_key); FloodfillPeerSelector sel = (FloodfillPeerSelector)_facade.getPeerSelector(); Certificate keyCert = null; if (!_isRouterInfo) { Destination dest = _facade.lookupDestinationLocally(_key); if (dest != null) { Certificate cert = dest.getCertificate(); if (cert.getCertificateType() == Certificate.CERTIFICATE_TYPE_KEY) keyCert = cert; } } if (keyCert != null) { while (true) { List<Hash> peers = sel.selectFloodfillParticipants(rkey, 1, _ignore, _facade.getKBuckets()); if (peers.isEmpty()) break; Hash peer = peers.get(0); RouterInfo ri = _facade.lookupRouterInfoLocally(peer); if (ri != null && StoreJob.supportsCert(ri, keyCert)) { Set<String> peerIPs = new MaskedIPSet(getContext(), ri, IP_CLOSE_BYTES); if (!_ipSet.containsAny(peerIPs)) { _ipSet.addAll(peerIPs); return peer; } else { if (_log.shouldLog(Log.INFO)) _log.info(getJobId() + ": Skipping verify w/ router too close to the store " + peer); } } else { if (_log.shouldLog(Log.INFO)) _log.info(getJobId() + ": Skipping verify w/ router that doesn't support key certs " + peer); } _ignore.add(peer); } } else { List<Hash> peers = sel.selectFloodfillParticipants(rkey, 1, _ignore, _facade.getKBuckets()); if (!peers.isEmpty()) return peers.get(0); } if (_log.shouldLog(Log.WARN)) _log.warn("No other peers to verify floodfill with, using the one we sent to"); return _sentTo; } /** @return non-null */ private DatabaseLookupMessage buildLookup(TunnelInfo replyTunnelInfo) { // If we are verifying a leaseset, use the destination's own tunnels, // to avoid association by the exploratory tunnel OBEP. // Unless it is an encrypted leaseset. DatabaseLookupMessage m = new DatabaseLookupMessage(getContext(), true); m.setMessageExpiration(getContext().clock().now() + VERIFY_TIMEOUT); m.setReplyTunnel(replyTunnelInfo.getReceiveTunnelId(0)); m.setFrom(replyTunnelInfo.getPeer(0)); m.setSearchKey(_key); m.setSearchType(_isRouterInfo ? DatabaseLookupMessage.Type.RI : DatabaseLookupMessage.Type.LS); return m; } private class VerifyReplySelector implements MessageSelector { public boolean continueMatching() { return false; // only want one match } public long getExpiration() { return _expiration; } public boolean isMatch(I2NPMessage message) { if (message instanceof DatabaseStoreMessage) { DatabaseStoreMessage dsm = (DatabaseStoreMessage)message; return _key.equals(dsm.getKey()); } else if (message instanceof DatabaseSearchReplyMessage) { DatabaseSearchReplyMessage dsrm = (DatabaseSearchReplyMessage)message; return _key.equals(dsrm.getSearchKey()); } return false; } } private class VerifyReplyJob extends JobImpl implements ReplyJob { private I2NPMessage _message; public VerifyReplyJob(RouterContext ctx) { super(ctx); } public String getName() { return "Handle floodfill verification reply"; } public void runJob() { long delay = getContext().clock().now() - _sendTime; if (_wrappedMessage != null) _wrappedMessage.acked(); _facade.verifyFinished(_key); if (_message instanceof DatabaseStoreMessage) { // Verify it's as recent as the one we sent DatabaseStoreMessage dsm = (DatabaseStoreMessage)_message; boolean success = dsm.getEntry().getDate() >= _published; if (success) { // store ok, w00t! getContext().profileManager().dbLookupSuccessful(_target, delay); if (_sentTo != null) getContext().profileManager().dbStoreSuccessful(_sentTo); getContext().statManager().addRateData("netDb.floodfillVerifyOK", delay); if (_log.shouldLog(Log.INFO)) _log.info("Verify success for " + _key); if (_isRouterInfo) _facade.routerInfoPublishSuccessful(); return; } if (_log.shouldLog(Log.WARN)) _log.warn("Verify failed (older) for " + _key); if (_log.shouldLog(Log.INFO)) _log.info("Rcvd older data: " + dsm.getEntry()); } else if (_message instanceof DatabaseSearchReplyMessage) { DatabaseSearchReplyMessage dsrm = (DatabaseSearchReplyMessage) _message; // assume 0 old, all new, 0 invalid, 0 dup getContext().profileManager().dbLookupReply(_target, 0, dsrm.getNumReplies(), 0, 0, delay); if (_log.shouldLog(Log.WARN)) _log.warn("Verify failed (DSRM) for " + _key); // only for RI... LS too dangerous? if (_isRouterInfo) getContext().jobQueue().addJob(new SingleLookupJob(getContext(), dsrm)); } // store failed, boo, hiss! // blame the sent-to peer, but not the verify peer if (_sentTo != null) getContext().profileManager().dbStoreFailed(_sentTo); // Blame the verify peer also. // We must use dbLookupFailed() or dbStoreFailed(), neither of which is exactly correct, // but we have to use one of them to affect the FloodfillPeerSelector ordering. // If we don't do this we get stuck using the same verify peer every time even // though it is the real problem. if (_target != null && !_target.equals(_sentTo)) getContext().profileManager().dbLookupFailed(_target); getContext().statManager().addRateData("netDb.floodfillVerifyFail", delay); resend(); } public void setMessage(I2NPMessage message) { _message = message; } } /** * the netDb store failed to verify, so resend it to a random floodfill peer * Fixme - since we now store closest-to-the-key, this is likely to store to the * very same ff as last time, until the stats get bad enough to switch. * Therefore, pass the failed ff through as a don't-store-to. * Let's also add the one we just tried to verify with, as they could be a pair of no-flooders. * So at least we'll try THREE ffs round-robin if things continue to fail... */ private void resend() { DatabaseEntry ds = _facade.lookupLocally(_key); if (ds != null) { Set<Hash> toSkip = new HashSet<Hash>(2); if (_sentTo != null) toSkip.add(_sentTo); if (_target != null) toSkip.add(_target); _facade.sendStore(_key, ds, null, null, FloodfillNetworkDatabaseFacade.PUBLISH_TIMEOUT, toSkip); } } private class VerifyTimeoutJob extends JobImpl { public VerifyTimeoutJob(RouterContext ctx) { super(ctx); } public String getName() { return "Floodfill verification timeout"; } public void runJob() { if (_wrappedMessage != null) _wrappedMessage.fail(); // Only blame the verify peer getContext().profileManager().dbLookupFailed(_target); //if (_sentTo != null) // getContext().profileManager().dbStoreFailed(_sentTo); getContext().statManager().addRateData("netDb.floodfillVerifyTimeout", getContext().clock().now() - _sendTime); if (_log.shouldLog(Log.WARN)) _log.warn("Verify timed out for: " + _key); if (_ignore.size() < MAX_PEERS_TO_TRY) { // Don't resend, simply rerun FVSJ.this inline and // chose somebody besides _target for verification _ignore.add(_target); FloodfillVerifyStoreJob.this.runJob(); } else { _facade.verifyFinished(_key); resend(); } } } }