diff --git a/history.txt b/history.txt index dfc453cf7..9c758e2a3 100644 --- a/history.txt +++ b/history.txt @@ -1,4 +1,25 @@ +2020-03-31 zzz + * NetDB: + - Add support for ratchet replies (proposal 154) + - Add support for ElG lookups and stores from ECIES-only destinations + - Variable timeout for MessageWrapper-generated tags + * Ratchet: + - Variable timeout for tagsets + - Expire tags too far behind current one + - Remove ID and DI from ACKREQ block + - Add timeout job in OCMOSJ + - Prep for next key support + - Add support for acks and callbacks + * Tunnels: + - Refactor TestJob to use MessageWrapper + - Add support for ratchet + 2020-03-24 zzz + * Blockfile: Add b32 to export output + * Graphs: Fix rrd4j deprecation warnings + * Profiles: + - Don't decay during first 90 minutes of uptime + - Change decay from .75 twice a day to .84 four times a day * Tunnels: Make new tunnel selection round-robin 2020-03-20 zzz diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index f6e6df3a5..4e28a2c8d 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -18,7 +18,7 @@ public class RouterVersion { /** deprecated */ public final static String ID = "Monotone"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 4; + public final static long BUILD = 5; /** for example "-test" */ public final static String EXTRA = ""; diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java b/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java index 830ac5519..185ac33d4 100644 --- a/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java +++ b/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java @@ -232,7 +232,7 @@ class OutboundClientMessageJobHelper { LeaseSetKeys lsk = ctx.keyManager().getKeys(from); I2NPMessage msg; if (lsk == null || lsk.isSupported(EncType.ELGAMAL_2048)) { - msg = wrapDSM(ctx, skm, dsm); + msg = wrapDSM(ctx, skm, dsm, expiration); if (msg == null) { if (log.shouldLog(Log.WARN)) log.warn("Failed to wrap ack clove"); @@ -279,12 +279,14 @@ class OutboundClientMessageJobHelper { * @return null on error * @since 0.9.12 */ - private static GarlicMessage wrapDSM(RouterContext ctx, SessionKeyManager skm, DeliveryStatusMessage dsm) { + private static GarlicMessage wrapDSM(RouterContext ctx, SessionKeyManager skm, + DeliveryStatusMessage dsm, long expiration) { // garlic route that DeliveryStatusMessage to ourselves so the endpoints and gateways // can't tell its a test. to simplify this, we encrypt it with a random key and tag, // remembering that key+tag so that we can decrypt it later. this means we can do the // garlic encryption without any ElGamal (yay) - MessageWrapper.OneTimeSession sess = MessageWrapper.generateSession(ctx, skm); + long fromNow = expiration - ctx.clock().now(); + MessageWrapper.OneTimeSession sess = MessageWrapper.generateSession(ctx, skm, fromNow, true); GarlicMessage msg = MessageWrapper.wrap(ctx, dsm, sess); return msg; } diff --git a/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java b/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java index d8482a0a8..6d4ffdfb3 100644 --- a/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java +++ b/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java @@ -18,6 +18,7 @@ import net.i2p.data.LeaseSet; import net.i2p.data.router.RouterIdentity; import net.i2p.data.router.RouterInfo; import net.i2p.data.SessionKey; +import net.i2p.data.SessionTag; import net.i2p.data.TunnelId; import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.data.i2np.DatabaseSearchReplyMessage; @@ -29,6 +30,7 @@ import net.i2p.router.JobImpl; import net.i2p.router.OutNetMessage; import net.i2p.router.Router; import net.i2p.router.RouterContext; +import net.i2p.router.crypto.ratchet.RatchetSessionTag; import net.i2p.router.networkdb.kademlia.MessageWrapper; import net.i2p.router.message.SendMessageDirectJob; import net.i2p.util.Log; @@ -313,11 +315,19 @@ public class HandleDatabaseLookupMessageJob extends JobImpl { SessionKey replyKey = _message.getReplyKey(); if (replyKey != null) { // encrypt the reply - if (_log.shouldLog(Log.INFO)) - _log.info("Sending encrypted reply to " + toPeer + ' ' + replyKey + ' ' + _message.getReplyTag()); - message = MessageWrapper.wrap(getContext(), message, replyKey, _message.getReplyTag()); + SessionTag tag = _message.getReplyTag(); + if (tag != null) { + if (_log.shouldLog(Log.INFO)) + _log.info("Sending AES reply to " + toPeer + ' ' + replyKey + ' ' + tag); + message = MessageWrapper.wrap(getContext(), message, replyKey, tag); + } else { + RatchetSessionTag rtag = _message.getRatchetReplyTag(); + if (_log.shouldLog(Log.INFO)) + _log.info("Sending AEAD reply to " + toPeer + ' ' + replyKey + ' ' + rtag); + message = MessageWrapper.wrap(getContext(), message, replyKey, rtag); + } if (message == null) { - _log.error("Encryption error"); + _log.error("DLM reply encryption error"); return; } _replyKeyConsumed = true; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/ExploreJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/ExploreJob.java index a9438d9cf..2fda8a946 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/ExploreJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/ExploreJob.java @@ -141,7 +141,7 @@ class ExploreJob extends SearchJob { // request encrypted reply? if (DatabaseLookupMessage.supportsEncryptedReplies(peer)) { MessageWrapper.OneTimeSession sess; - sess = MessageWrapper.generateSession(getContext()); + sess = MessageWrapper.generateSession(getContext(), MAX_EXPLORE_TIME); if (_log.shouldLog(Log.INFO)) _log.info(getJobId() + ": Requesting encrypted reply from " + peer.getIdentity().calculateHash() + ' ' + sess.key + ' ' + sess.tag); 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 daf20e4a6..a48622bd4 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java @@ -146,16 +146,23 @@ class FloodfillVerifyStoreJob extends JobImpl { _facade.verifyFinished(_key); return; } + boolean supportsElGamal = true; + boolean supportsRatchet = false; if (DatabaseLookupMessage.supportsEncryptedReplies(peer)) { // register the session with the right SKM MessageWrapper.OneTimeSession sess; if (isInboundExploratory) { - sess = MessageWrapper.generateSession(getContext()); + sess = MessageWrapper.generateSession(getContext(), VERIFY_TIMEOUT); } else { LeaseSetKeys lsk = getContext().keyManager().getKeys(_client); - if (lsk == null || lsk.isSupported(EncType.ELGAMAL_2048)) { + supportsRatchet = lsk != null && + lsk.isSupported(EncType.ECIES_X25519) && + DatabaseLookupMessage.supportsRatchetReplies(peer); + supportsElGamal = lsk != null && + lsk.isSupported(EncType.ELGAMAL_2048); + if (supportsElGamal || supportsRatchet) { // garlic encrypt - sess = MessageWrapper.generateSession(getContext(), _client); + sess = MessageWrapper.generateSession(getContext(), _client, VERIFY_TIMEOUT, !supportsRatchet); if (sess == null) { if (_log.shouldLog(Log.WARN)) _log.warn("No SKM to reply to"); @@ -163,7 +170,7 @@ class FloodfillVerifyStoreJob extends JobImpl { return; } } else { - // We don't yet have any way to request/get a ECIES-tagged reply, + // We don't have a compatible way to get a reply, // skip it for now. if (_log.shouldWarn()) _log.warn("Skipping store verify for ECIES client " + _client.toBase32()); @@ -171,23 +178,41 @@ class FloodfillVerifyStoreJob extends JobImpl { return; } } - if (_log.shouldLog(Log.INFO)) - _log.info(getJobId() + ": Requesting encrypted reply from " + _target + ' ' + sess.key + ' ' + sess.tag); - lookup.setReplySession(sess.key, sess.tag); + if (sess.tag != null) { + if (_log.shouldInfo()) + _log.info(getJobId() + ": Requesting AES reply from " + peer + ' ' + sess.key + ' ' + sess.tag); + lookup.setReplySession(sess.key, sess.tag); + } else { + if (_log.shouldInfo()) + _log.info(getJobId() + ": Requesting AEAD reply from " + peer + ' ' + sess.key + ' ' + sess.rtag); + lookup.setReplySession(sess.key, sess.rtag); + } } Hash fromKey; - if (_isRouterInfo) - fromKey = null; - else - fromKey = _client; - _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; + if (supportsElGamal) { + if (_isRouterInfo) + fromKey = null; + else + fromKey = _client; + _wrappedMessage = MessageWrapper.wrap(getContext(), lookup, fromKey, peer); + if (_wrappedMessage == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Fail Garlic encrypting"); + _facade.verifyFinished(_key); + return; + } + sent = _wrappedMessage.getMessage(); + } else { + // force full ElG for ECIES fromkey + sent = MessageWrapper.wrap(getContext(), lookup, peer); + if (sent == 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(getJobId() + ": Starting verify (stored " + _key + " to " + _sentTo + "), asking " + _target); diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java index 3d862adf6..563a83adc 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java @@ -336,19 +336,27 @@ public class IterativeSearchJob extends FloodSearchJob { TunnelInfo replyTunnel; boolean isClientReplyTunnel; boolean isDirect; + boolean supportsRatchet = false; + boolean supportsElGamal = true; if (_fromLocalDest != null) { outTunnel = tm.selectOutboundTunnel(_fromLocalDest, peer); if (outTunnel == null) outTunnel = tm.selectOutboundExploratoryTunnel(peer); LeaseSetKeys lsk = getContext().keyManager().getKeys(_fromLocalDest); - if (lsk == null || lsk.isSupported(EncType.ELGAMAL_2048)) { + supportsRatchet = lsk != null && + lsk.isSupported(EncType.ECIES_X25519) && + DatabaseLookupMessage.supportsRatchetReplies(ri); + supportsElGamal = !supportsRatchet && + lsk != null && + lsk.isSupported(EncType.ELGAMAL_2048); + if (supportsElGamal || supportsRatchet) { // garlic encrypt to dest SKM replyTunnel = tm.selectInboundTunnel(_fromLocalDest, peer); isClientReplyTunnel = replyTunnel != null; if (!isClientReplyTunnel) replyTunnel = tm.selectInboundExploratoryTunnel(peer); } else { - // We don't yet have any way to request/get a ECIES-tagged reply, + // We don't have a way to request/get a ECIES-tagged reply, // so send it to the router SKM isClientReplyTunnel = false; replyTunnel = tm.selectInboundExploratoryTunnel(peer); @@ -443,18 +451,24 @@ public class IterativeSearchJob extends FloodSearchJob { _log.warn(getJobId() + ": Can't do encrypted lookup to " + peer + " with EncType " + type); return; } - if (true) { - MessageWrapper.OneTimeSession sess; - if (isClientReplyTunnel) - sess = MessageWrapper.generateSession(getContext(), _fromLocalDest); - else - sess = MessageWrapper.generateSession(getContext()); - if (sess != null) { - if (_log.shouldLog(Log.INFO)) - _log.info(getJobId() + ": Requesting encrypted reply from " + peer + ' ' + sess.key + ' ' + sess.tag); + + MessageWrapper.OneTimeSession sess; + if (isClientReplyTunnel) + sess = MessageWrapper.generateSession(getContext(), _fromLocalDest, SINGLE_SEARCH_MSG_TIME, !supportsRatchet); + else + sess = MessageWrapper.generateSession(getContext(), SINGLE_SEARCH_MSG_TIME); + if (sess != null) { + if (sess.tag != null) { + if (_log.shouldInfo()) + _log.info(getJobId() + ": Requesting AES reply from " + peer + ' ' + sess.key + ' ' + sess.tag); dlm.setReplySession(sess.key, sess.tag); - } // else client went away, but send it anyway - } + } else { + if (_log.shouldInfo()) + _log.info(getJobId() + ": Requesting AEAD reply from " + peer + ' ' + sess.key + ' ' + sess.rtag); + dlm.setReplySession(sess.key, sess.rtag); + } + } // else client went away, but send it anyway + outMsg = MessageWrapper.wrap(getContext(), dlm, ri); // ElG can take a while so do a final check before we send it, // a response may have come in. diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java b/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java index 3b6f168b0..d7dca37e1 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java @@ -9,13 +9,17 @@ import net.i2p.crypto.TagSetHandle; import net.i2p.data.Certificate; import net.i2p.data.Hash; import net.i2p.data.PublicKey; -import net.i2p.data.router.RouterInfo; import net.i2p.data.SessionKey; import net.i2p.data.SessionTag; import net.i2p.data.i2np.DeliveryInstructions; import net.i2p.data.i2np.GarlicMessage; import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.router.RouterInfo; import net.i2p.router.RouterContext; +import net.i2p.router.crypto.TransientSessionKeyManager; +import net.i2p.router.crypto.ratchet.MuxedSKM; +import net.i2p.router.crypto.ratchet.RatchetSKM; +import net.i2p.router.crypto.ratchet.RatchetSessionTag; import net.i2p.router.message.GarlicMessageBuilder; import net.i2p.router.message.PayloadGarlicConfig; import net.i2p.router.util.RemovableSingletonSet; @@ -63,7 +67,7 @@ public class MessageWrapper { if (skm == null) return null; SessionKey sentKey = new SessionKey(); - Set sentTags = new HashSet(); + Set sentTags = new HashSet(NETDB_TAGS_TO_DELIVER); GarlicMessage msg = GarlicMessageBuilder.buildMessage(ctx, payload, sentKey, sentTags, NETDB_TAGS_TO_DELIVER, NETDB_LOW_THRESHOLD, skm); if (msg == null) @@ -150,11 +154,25 @@ public class MessageWrapper { * @since 0.9.7 */ public static class OneTimeSession { + /** ElG or ratchet */ public final SessionKey key; + /** non-null for ElG */ public final SessionTag tag; + /** + * non-null for ratchet + * @since 0.9.46 + */ + public final RatchetSessionTag rtag; public OneTimeSession(SessionKey key, SessionTag tag) { this.key = key; this.tag = tag; + rtag = null; + } + + /** @since 0.9.46 */ + public OneTimeSession(SessionKey key, RatchetSessionTag tag) { + this.key = key; rtag = tag; + this.tag = null; } } @@ -164,10 +182,11 @@ public class MessageWrapper { * The recipient can then send us an AES-encrypted message, * avoiding ElGamal. * + * @param expiration time from now * @since 0.9.7 */ - public static OneTimeSession generateSession(RouterContext ctx) { - return generateSession(ctx, ctx.sessionKeyManager()); + public static OneTimeSession generateSession(RouterContext ctx, long expiration) { + return generateSession(ctx, ctx.sessionKeyManager(), expiration, true); } /** @@ -176,14 +195,16 @@ public class MessageWrapper { * The recipient can then send us an AES-encrypted message, * avoiding ElGamal. * + * @param expiration time from now * @return null if we can't find the SKM for the localDest * @since 0.9.9 */ - public static OneTimeSession generateSession(RouterContext ctx, Hash localDest) { + public static OneTimeSession generateSession(RouterContext ctx, Hash localDest, + long expiration, boolean forceElG) { SessionKeyManager skm = ctx.clientManager().getClientSessionKeyManager(localDest); if (skm == null) return null; - return generateSession(ctx, skm); + return generateSession(ctx, skm, expiration, forceElG); } /** @@ -192,29 +213,49 @@ public class MessageWrapper { * The recipient can then send us an AES-encrypted message, * avoiding ElGamal. * + * @param expiration time from now * @return non-null * @since 0.9.9 */ - public static OneTimeSession generateSession(RouterContext ctx, SessionKeyManager skm) { + public static OneTimeSession generateSession(RouterContext ctx, SessionKeyManager skm, + long expiration, boolean forceElG) { SessionKey key = ctx.keyGenerator().generateSessionKey(); - SessionTag tag = new SessionTag(true); - Set tags = new RemovableSingletonSet(tag); - skm.tagsReceived(key, tags, 2*60*1000); + if (forceElG || (skm instanceof TransientSessionKeyManager)) { + SessionTag tag = new SessionTag(true); + Set tags = new RemovableSingletonSet(tag); + skm.tagsReceived(key, tags, expiration); + return new OneTimeSession(key, tag); + } + // ratchet + RatchetSKM rskm; + if (skm instanceof RatchetSKM) { + rskm = (RatchetSKM) skm; + } else if (skm instanceof MuxedSKM) { + rskm = ((MuxedSKM) skm).getECSKM(); + } else { + throw new IllegalStateException("skm not a ratchet " + skm); + } + RatchetSessionTag tag = new RatchetSessionTag(ctx.random().nextLong()); + rskm.tagsReceived(key, tag, expiration); return new OneTimeSession(key, tag); } /** * Garlic wrap a message from nobody, destined for an unknown router, * to hide the contents from the IBGW. - * Uses a supplied one-time session key tag for AES encryption, - * avoiding ElGamal. + * Uses a supplied one-time session key tag for AES or AEAD encryption, + * avoiding ElGamal or X25519. + * + * Used by OCMJH for DSM. * * @param session non-null * @return null on encrypt failure * @since 0.9.12 */ public static GarlicMessage wrap(RouterContext ctx, I2NPMessage m, OneTimeSession session) { - return wrap(ctx, m, session.key, session.tag); + if (session.tag != null) + return wrap(ctx, m, session.key, session.tag); + return wrap(ctx, m, session.key, session.rtag); } /** @@ -223,6 +264,8 @@ public class MessageWrapper { * Uses a supplied session key and session tag for AES encryption, * avoiding ElGamal. * + * Used by above and for DLM replies in HDLMJ. + * * @param encryptKey non-null * @param encryptTag non-null * @return null on encrypt failure @@ -233,9 +276,30 @@ public class MessageWrapper { ctx.random().nextLong(I2NPMessage.MAX_ID_VALUE), m.getMessageExpiration(), DeliveryInstructions.LOCAL, m); - GarlicMessage msg = GarlicMessageBuilder.buildMessage(ctx, payload, null, null, encryptKey, encryptTag); return msg; } + + /** + * Garlic wrap a message from nobody, destined for an unknown router, + * to hide the contents from the IBGW. + * Uses a supplied session key and session tag for ratchet encryption, + * avoiding full ECIES. + * + * Used by above and for DLM replies in HDLMJ. + * + * @param encryptKey non-null + * @param encryptTag non-null + * @return null on encrypt failure + * @since 0.9.46 + */ + public static GarlicMessage wrap(RouterContext ctx, I2NPMessage m, SessionKey encryptKey, RatchetSessionTag encryptTag) { + PayloadGarlicConfig payload = new PayloadGarlicConfig(Certificate.NULL_CERT, + ctx.random().nextLong(I2NPMessage.MAX_ID_VALUE), + m.getMessageExpiration(), + DeliveryInstructions.LOCAL, m); + GarlicMessage msg = GarlicMessageBuilder.buildMessage(ctx, payload, encryptKey, encryptTag); + return msg; + } } diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java index f4d756eec..61fe6d659 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java @@ -14,6 +14,7 @@ import java.util.Set; import net.i2p.crypto.EncType; import net.i2p.crypto.SigType; +import net.i2p.data.Base64; import net.i2p.data.Certificate; import net.i2p.data.DatabaseEntry; import net.i2p.data.DataFormatException; @@ -297,7 +298,8 @@ abstract class StoreJob extends JobImpl { Hash rkey = getContext().routingKeyGenerator().getRoutingKey(key); KBucketSet ks = _facade.getKBuckets(); if (ks == null) return new ArrayList(); - return ((FloodfillPeerSelector)_peerSelector).selectFloodfillParticipants(rkey, numClosest, alreadyChecked, ks); + List rv = ((FloodfillPeerSelector)_peerSelector).selectFloodfillParticipants(rkey, numClosest, alreadyChecked, ks); + return rv; } /** limit expiration for direct sends */ @@ -496,9 +498,19 @@ abstract class StoreJob extends JobImpl { } sent = wm.getMessage(); _state.addPending(to, wm); + } else if (lsk.isSupported(EncType.ECIES_X25519)) { + // force full ElG for ECIES-only + sent = MessageWrapper.wrap(getContext(), msg, peer); + if (sent == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Fail garlic encrypting from: " + client); + fail(); + return; + } + _state.addPending(to); } else { - // We don't yet have any way to request/get a ECIES-tagged reply, - // so send it unencrypted. + // Above are the only two enc types for now, won't get here. + // Send it unencrypted. sent = msg; _state.addPending(to); } diff --git a/router/java/src/net/i2p/router/tunnel/pool/TestJob.java b/router/java/src/net/i2p/router/tunnel/pool/TestJob.java index a1da7f98d..0195e8959 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/TestJob.java +++ b/router/java/src/net/i2p/router/tunnel/pool/TestJob.java @@ -1,12 +1,9 @@ package net.i2p.router.tunnel.pool; -import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import net.i2p.crypto.SessionKeyManager; -import net.i2p.data.Certificate; -import net.i2p.data.SessionKey; import net.i2p.data.SessionTag; -import net.i2p.data.i2np.DeliveryInstructions; import net.i2p.data.i2np.DeliveryStatusMessage; import net.i2p.data.i2np.GarlicMessage; import net.i2p.data.i2np.I2NPMessage; @@ -16,9 +13,7 @@ import net.i2p.router.OutNetMessage; import net.i2p.router.ReplyJob; import net.i2p.router.RouterContext; import net.i2p.router.TunnelInfo; -import net.i2p.router.message.GarlicMessageBuilder; -import net.i2p.router.message.PayloadGarlicConfig; -import net.i2p.router.util.RemovableSingletonSet; +import net.i2p.router.networkdb.kademlia.MessageWrapper; import net.i2p.stat.Rate; import net.i2p.stat.RateStat; import net.i2p.util.Log; @@ -26,6 +21,9 @@ import net.i2p.util.Log; /** * Repeatedly test a single tunnel for its entire lifetime, * or until the pool is shut down or removed from the client manager. + * + * Tunnel testing is disabled by default now, except for hidden mode, + * see TunnelPoolManager.buildComplete() */ class TestJob extends JobImpl { private final Log _log; @@ -37,6 +35,8 @@ class TestJob extends JobImpl { private PooledTunnelCreatorConfig _otherTunnel; /** save this so we can tell the SKM to kill it if the test fails */ private SessionTag _encryptTag; + private static final AtomicInteger __id = new AtomicInteger(); + private int _id; /** base to randomize the test delay on */ private static final int TEST_DELAY = 40*1000; @@ -60,15 +60,16 @@ class TestJob extends JobImpl { public void runJob() { if (_pool == null || !_pool.isAlive()) return; - long lag = getContext().jobQueue().getMaxLag(); + final RouterContext ctx = getContext(); + long lag = ctx.jobQueue().getMaxLag(); if (lag > 3000) { if (_log.shouldLog(Log.WARN)) _log.warn("Deferring test of " + _cfg + " due to job lag = " + lag); - getContext().statManager().addRateData("tunnel.testAborted", _cfg.getLength(), 0); + ctx.statManager().addRateData("tunnel.testAborted", _cfg.getLength()); scheduleRetest(); return; } - if (getContext().router().gracefulShutdownInProgress()) + if (ctx.router().gracefulShutdownInProgress()) return; // don't reschedule _found = false; // note: testing with exploratory tunnels always, even if the tested tunnel @@ -80,11 +81,11 @@ class TestJob extends JobImpl { if (_cfg.isInbound()) { _replyTunnel = _cfg; // TODO if testing is re-enabled, pick closest to far end - _outTunnel = getContext().tunnelManager().selectOutboundTunnel(); + _outTunnel = ctx.tunnelManager().selectOutboundTunnel(); _otherTunnel = (PooledTunnelCreatorConfig) _outTunnel; } else { // TODO if testing is re-enabled, pick closest to far end - _replyTunnel = getContext().tunnelManager().selectInboundTunnel(); + _replyTunnel = ctx.tunnelManager().selectInboundTunnel(); _outTunnel = _cfg; _otherTunnel = (PooledTunnelCreatorConfig) _replyTunnel; } @@ -92,61 +93,59 @@ class TestJob extends JobImpl { if ( (_replyTunnel == null) || (_outTunnel == null) ) { if (_log.shouldLog(Log.WARN)) _log.warn("Insufficient tunnels to test " + _cfg + " with: " + _replyTunnel + " / " + _outTunnel); - getContext().statManager().addRateData("tunnel.testAborted", _cfg.getLength(), 0); + ctx.statManager().addRateData("tunnel.testAborted", _cfg.getLength()); scheduleRetest(); } else { int testPeriod = getTestPeriod(); - long testExpiration = getContext().clock().now() + testPeriod; - DeliveryStatusMessage m = new DeliveryStatusMessage(getContext()); - m.setArrival(getContext().clock().now()); + long now = ctx.clock().now(); + long testExpiration = now + testPeriod; + DeliveryStatusMessage m = new DeliveryStatusMessage(ctx); + m.setArrival(now); m.setMessageExpiration(testExpiration); - m.setMessageId(getContext().random().nextLong(I2NPMessage.MAX_ID_VALUE)); - ReplySelector sel = new ReplySelector(getContext(), m.getMessageId(), testExpiration); - OnTestReply onReply = new OnTestReply(getContext()); - OnTestTimeout onTimeout = new OnTestTimeout(getContext()); - OutNetMessage msg = getContext().messageRegistry().registerPending(sel, onReply, onTimeout); + m.setMessageId(ctx.random().nextLong(I2NPMessage.MAX_ID_VALUE)); + ReplySelector sel = new ReplySelector(m.getMessageId(), testExpiration); + OnTestReply onReply = new OnTestReply(); + OnTestTimeout onTimeout = new OnTestTimeout(); + OutNetMessage msg = ctx.messageRegistry().registerPending(sel, onReply, onTimeout); onReply.setSentMessage(msg); - sendTest(m); + sendTest(m, testPeriod); } } - private void sendTest(I2NPMessage m) { + private void sendTest(I2NPMessage m, int testPeriod) { // garlic route that DeliveryStatusMessage to ourselves so the endpoints and gateways // can't tell its a test. to simplify this, we encrypt it with a random key and tag, // remembering that key+tag so that we can decrypt it later. this means we can do the // garlic encryption without any ElGamal (yay) - PayloadGarlicConfig payload = new PayloadGarlicConfig(Certificate.NULL_CERT, - getContext().random().nextLong(I2NPMessage.MAX_ID_VALUE), - m.getMessageExpiration(), - DeliveryInstructions.LOCAL, m); - payload.setRecipient(getContext().router().getRouterInfo()); - - SessionKey encryptKey = getContext().keyGenerator().generateSessionKey(); - SessionTag encryptTag = new SessionTag(true); - _encryptTag = encryptTag; - Set sentTags = null; - GarlicMessage msg = GarlicMessageBuilder.buildMessage(getContext(), payload, sentTags, - getContext().keyManager().getPublicKey(), - encryptKey, encryptTag); - + final RouterContext ctx = getContext(); + MessageWrapper.OneTimeSession sess; + if (_cfg.isInbound() && !_pool.getSettings().isExploratory()) { + // to client. false means don't force AES + sess = MessageWrapper.generateSession(ctx, _pool.getSettings().getDestination(), testPeriod, false); + } else { + // to router. AES. + sess = MessageWrapper.generateSession(ctx, testPeriod); + } + if (sess == null) { + scheduleRetest(); + return; + } + // null for ratchet + _encryptTag = sess.tag; + GarlicMessage msg; + if (sess.tag != null) // AES + msg = MessageWrapper.wrap(ctx, m, sess.key, sess.tag); + else // ratchet + msg = MessageWrapper.wrap(ctx, m, sess.key, sess.rtag); if (msg == null) { // overloaded / unknown peers / etc scheduleRetest(); return; } - Set encryptTags = new RemovableSingletonSet(encryptTag); - // Register the single tag with the appropriate SKM - if (_cfg.isInbound() && !_pool.getSettings().isExploratory()) { - SessionKeyManager skm = getContext().clientManager().getClientSessionKeyManager(_pool.getSettings().getDestination()); - if (skm != null) - skm.tagsReceived(encryptKey, encryptTags); - } else { - getContext().sessionKeyManager().tagsReceived(encryptKey, encryptTags); - } - + _id = __id.getAndIncrement(); if (_log.shouldLog(Log.DEBUG)) - _log.debug("Sending garlic test of " + _outTunnel + " / " + _replyTunnel); - getContext().tunnelDispatcher().dispatchOutbound(msg, _outTunnel.getSendTunnelId(0), + _log.debug("Sending garlic test #" + _id + " of " + _outTunnel + " / " + _replyTunnel); + ctx.tunnelDispatcher().dispatchOutbound(msg, _outTunnel.getSendTunnelId(0), _replyTunnel.getReceiveTunnelId(0), _replyTunnel.getPeer(0)); } @@ -154,8 +153,8 @@ class TestJob extends JobImpl { public void testSuccessful(int ms) { if (_pool == null || !_pool.isAlive()) return; - getContext().statManager().addRateData("tunnel.testSuccessLength", _cfg.getLength(), 0); - getContext().statManager().addRateData("tunnel.testSuccessTime", ms, 0); + getContext().statManager().addRateData("tunnel.testSuccessLength", _cfg.getLength()); + getContext().statManager().addRateData("tunnel.testSuccessTime", ms); _outTunnel.incrementVerifiedBytesTransferred(1024); // reply tunnel is marked in the inboundEndpointProcessor @@ -170,7 +169,7 @@ class TestJob extends JobImpl { _otherTunnel.testJobSuccessful(ms); if (_log.shouldLog(Log.DEBUG)) - _log.debug("Tunnel test successful in " + ms + "ms: " + _cfg); + _log.debug("Tunnel test #" + _id + " successful in " + ms + "ms: " + _cfg); scheduleRetest(); } @@ -193,7 +192,7 @@ class TestJob extends JobImpl { else getContext().statManager().addRateData("tunnel.testFailedTime", timeToFail, timeToFail); if (_log.shouldLog(Log.WARN)) - _log.warn("Tunnel test failed in " + timeToFail + "ms: " + _cfg); + _log.warn("Tunnel test #" + _id + " failed in " + timeToFail + "ms: " + _cfg); boolean keepGoing = _cfg.tunnelFailed(); // blame the expl. tunnel too if (_otherTunnel.getLength() > 1) @@ -234,6 +233,7 @@ class TestJob extends JobImpl { } private void scheduleRetest() { scheduleRetest(false); } + private void scheduleRetest(boolean asap) { if (_pool == null || !_pool.isAlive()) return; @@ -248,18 +248,16 @@ class TestJob extends JobImpl { } private class ReplySelector implements MessageSelector { - private final RouterContext _context; private final long _id; private final long _expiration; - public ReplySelector(RouterContext ctx, long id, long expiration) { - _context = ctx; + public ReplySelector(long id, long expiration) { _id = id; _expiration = expiration; _found = false; } - public boolean continueMatching() { return !_found && _context.clock().now() < _expiration; } + public boolean continueMatching() { return !_found && getContext().clock().now() < _expiration; } public long getExpiration() { return _expiration; } @@ -286,7 +284,7 @@ class TestJob extends JobImpl { private long _successTime; private OutNetMessage _sentMessage; - public OnTestReply(RouterContext ctx) { super(ctx); } + public OnTestReply() { super(TestJob.this.getContext()); } public String getName() { return "Tunnel test success"; } @@ -322,22 +320,23 @@ class TestJob extends JobImpl { private class OnTestTimeout extends JobImpl { private final long _started; - public OnTestTimeout(RouterContext ctx) { - super(ctx); - _started = ctx.clock().now(); + public OnTestTimeout() { + super(TestJob.this.getContext()); + _started = getContext().clock().now(); } public String getName() { return "Tunnel test timeout"; } public void runJob() { if (_log.shouldLog(Log.WARN)) - _log.warn("Timeout: found? " + _found); + _log.warn("Tunnel test #" + _id + " timeout: found? " + _found); if (!_found) { // don't clog up the SKM with old one-tag tagsets if (_cfg.isInbound() && !_pool.getSettings().isExploratory()) { SessionKeyManager skm = getContext().clientManager().getClientSessionKeyManager(_pool.getSettings().getDestination()); - if (skm != null) + if (skm != null && _encryptTag != null) skm.consumeTag(_encryptTag); + // else null tag for ratchet, let it expire } else { getContext().sessionKeyManager().consumeTag(_encryptTag); } diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java index 6bd713a35..b00595137 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java +++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java @@ -563,12 +563,8 @@ public class TunnelPoolManager implements TunnelManagerFacade { _context.router().isHidden() || _context.router().getRouterInfo().getAddressCount() <= 0)) { Hash client = cfg.getDestination(); - LeaseSetKeys lsk = client != null ? _context.keyManager().getKeys(client) : null; - if (lsk == null || lsk.isSupported(EncType.ELGAMAL_2048)) { - TunnelPool pool = cfg.getTunnelPool(); - _context.jobQueue().addJob(new TestJob(_context, cfg, pool)); - } - // else we don't yet have any way to request/get a ECIES-tagged reply, + TunnelPool pool = cfg.getTunnelPool(); + _context.jobQueue().addJob(new TestJob(_context, cfg, pool)); } }