From 4066e5df02d65483fe814608103ea2a15d542089 Mon Sep 17 00:00:00 2001 From: idk Date: Fri, 12 May 2023 17:26:36 +0000 Subject: [PATCH] Router/Tunnel: xor message IDs in order to prevent cross-context leaks. Adds unique message ID's per context to bloom filter for safer replay protection. The transport and client tunnel managers use a message ID in order to prevent messages from being replayed. Prior to this checkin, the message ID queue used the same IDs in clients and transports. If a message was sent to a transport and a client with the same message ID, the message ID in one would cause a replay to be detected in the other. The result would be that the message reply would come back empty, creating a point of evidence that a client and a transport were hosted on the same router. However, there is no way from the attackers POV to determine with certainty that the message was dropped because the message was replayed, making it very easy to demonstrate a potential information leak using a known router and a known client, but more difficult, to use to deanonymize a known client on an unknown router (i.e. by trying routers from the local NetDB). So what we have here is a situation where an attacker observing router behavior can say that a message was dropped, and that they have reason to believe it is because it contained an ID which was replayed. This constitutes a potential information leak and is resolved by this checkin. --- .../src/net/i2p/router/InNetMessagePool.java | 15 ++- .../net/i2p/router/TunnelPoolSettings.java | 8 +- .../router/transport/TransportManager.java | 5 +- .../tunnel/InboundMessageDistributor.java | 92 ++++++++++++++----- 4 files changed, 91 insertions(+), 29 deletions(-) diff --git a/router/java/src/net/i2p/router/InNetMessagePool.java b/router/java/src/net/i2p/router/InNetMessagePool.java index 9eccfea5db..aed2fe490f 100644 --- a/router/java/src/net/i2p/router/InNetMessagePool.java +++ b/router/java/src/net/i2p/router/InNetMessagePool.java @@ -119,6 +119,10 @@ public synchronized HandlerJobBuilder registerHandlerJobBuilder(int i2npMessageT _handlerJobBuilders[i2npMessageType] = builder; return old; } + + public int add(I2NPMessage messageBody, RouterIdentity fromRouter, Hash fromRouterHash) { + return add(messageBody, fromRouter, fromRouterHash, 0); + } /** * Add a new message to the pool. @@ -134,7 +138,10 @@ public synchronized HandlerJobBuilder registerHandlerJobBuilder(int i2npMessageT * @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) { + public int add(I2NPMessage messageBody, + RouterIdentity fromRouter, + Hash fromRouterHash, + long msgIDBloomXor) { final MessageHistory history = _context.messageHistory(); final boolean doHistory = history.getDoLog(); @@ -158,7 +165,11 @@ public int add(I2NPMessage messageBody, RouterIdentity fromRouter, Hash fromRout // just validate the expiration invalidReason = _context.messageValidator().validateMessage(exp); } else { - invalidReason = _context.messageValidator().validateMessage(messageBody.getUniqueId(), exp); + if (msgIDBloomXor == 0) + invalidReason = _context.messageValidator().validateMessage(messageBody.getUniqueId(), exp); + else + invalidReason = _context.messageValidator().validateMessage(messageBody.getUniqueId() + ^ msgIDBloomXor, exp); } if (invalidReason != null) { diff --git a/router/java/src/net/i2p/router/TunnelPoolSettings.java b/router/java/src/net/i2p/router/TunnelPoolSettings.java index 7a759dcc95..dfc5b2db26 100644 --- a/router/java/src/net/i2p/router/TunnelPoolSettings.java +++ b/router/java/src/net/i2p/router/TunnelPoolSettings.java @@ -85,7 +85,9 @@ public class TunnelPoolSettings { private static final int MIN_PRIORITY = -25; private static final int MAX_PRIORITY = 25; private static final int EXPLORATORY_PRIORITY = 30; - + + private final long _msgIdBloomXor; + /** * Exploratory tunnel */ @@ -116,6 +118,8 @@ public TunnelPoolSettings(Hash dest, boolean isInbound) { _IPRestriction = DEFAULT_IP_RESTRICTION; _unknownOptions = new Properties(); _randomKey = generateRandomKey(); + _msgIdBloomXor = RandomSource.getInstance().nextLong(); + if (_isExploratory && !_isInbound) _priority = EXPLORATORY_PRIORITY; if (!_isExploratory) @@ -286,6 +290,8 @@ public void setAliasOf(Hash h) { */ public Properties getUnknownOptions() { return _unknownOptions; } + public long getMsgIdBloomXor() { return _msgIdBloomXor; } + /** * Defaults in props are NOT honored. * In-JVM client side must promote defaults to the primary map. diff --git a/router/java/src/net/i2p/router/transport/TransportManager.java b/router/java/src/net/i2p/router/transport/TransportManager.java index 68c5558461..d1e1f3334f 100644 --- a/router/java/src/net/i2p/router/transport/TransportManager.java +++ b/router/java/src/net/i2p/router/transport/TransportManager.java @@ -111,6 +111,8 @@ public class TransportManager implements TransportEventListener { private static final long UPNP_REFRESH_TIME = UPnP.LEASE_TIME_SECONDS * 1000L / 3; + private final long _msgIdBloomXor; + public TransportManager(RouterContext context) { _context = context; _log = _context.logManager().getLog(TransportManager.class); @@ -134,6 +136,7 @@ public TransportManager(RouterContext context) { _dhThread = (_enableUDP || enableNTCP2) ? new DHSessionKeyBuilder.PrecalcRunner(context) : null; // always created, even if NTCP2 is not enabled, because ratchet needs it _xdhThread = new X25519KeyFactory(context); + _msgIdBloomXor = _context.random().nextLong(); } /** @@ -965,7 +968,7 @@ public void messageReceived(I2NPMessage message, RouterIdentity fromRouter, Hash if (_log.shouldLog(Log.DEBUG)) _log.debug("I2NPMessage received: " + message.getClass().getSimpleName() /*, new Exception("Where did I come from again?") */ ); try { - _context.inNetMessagePool().add(message, fromRouter, fromRouterHash); + _context.inNetMessagePool().add(message, fromRouter, fromRouterHash, _msgIdBloomXor); //if (_log.shouldLog(Log.DEBUG)) // _log.debug("Added to in pool"); } catch (IllegalArgumentException iae) { diff --git a/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java b/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java index fbd19df81d..ebcaeb9715 100644 --- a/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java +++ b/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java @@ -33,7 +33,8 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver { private final Log _log; private final Hash _client; private final GarlicMessageReceiver _receiver; - + private String _clientNickname; + private final long _msgIdBloomXor; /** * @param client null for router tunnel */ @@ -43,6 +44,23 @@ public InboundMessageDistributor(RouterContext ctx, Hash client) { _log = ctx.logManager().getLog(InboundMessageDistributor.class); _receiver = new GarlicMessageReceiver(ctx, this, client); // all createRateStat in TunnelDispatcher + + if (_client != null) { + TunnelPoolSettings clienttps = _context.tunnelManager().getInboundSettings(_client); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Initializing client (nickname: " + + clienttps.getDestinationNickname() + + " b32: " + _client.toBase32() + + ") InboundMessageDistributor with tunnel pool settings: " + clienttps); + _clientNickname = clienttps.getDestinationNickname(); + _msgIdBloomXor = clienttps.getMsgIdBloomXor(); + } else { + _clientNickname = "NULL/Expl"; + _msgIdBloomXor = 0; + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Initializing null or exploratory InboundMessageDistributor"); + } + } public void distribute(I2NPMessage msg, Hash target) { @@ -51,8 +69,9 @@ public void distribute(I2NPMessage msg, Hash target) { public void distribute(I2NPMessage msg, Hash target, TunnelId tunnel) { if (_log.shouldLog(Log.DEBUG)) - _log.debug("IBMD for " + ((_client != null) ? _client.toBase32() : "null") - + " to " + target + " / " + tunnel + " : " + msg); + _log.debug("IBMD for " + _clientNickname + " (" + + ((_client != null) ? _client.toBase32() : "null") + + ") to " + target + " / " + tunnel + " : " + msg); // allow messages on client tunnels even after client disconnection, as it may // include e.g. test messages, etc. DataMessages will be dropped anyway @@ -99,7 +118,8 @@ public void distribute(I2NPMessage msg, Hash target, TunnelId tunnel) { // We handle this safely, so we don't ask him again. // Todo: if peer was ff and RI is not ff, queue for exploration in netdb (but that isn't part of the facade now) if (_log.shouldLog(Log.WARN)) - _log.warn("Dropping DSM down a tunnel for " + _client.toBase32() + ": " + msg); + _log.warn("Inbound DSM received down a tunnel for " + _clientNickname + + " (" + _client.toBase32() + "): " + msg); // Handle safely by just updating the caps table, after doing basic validation Hash key = dsm.getKey(); if (_context.routerHash().equals(key)) @@ -192,32 +212,38 @@ public void distribute(I2NPMessage msg, Hash target, TunnelId tunnel) { } else { if (_log.shouldLog(Log.INFO)) _log.info("distributing inbound tunnel message into our inNetMessagePool" - + " (for client " + ((_client != null) ? _client.toBase32() : "null") - + " to target=NULL/tunnel=NULL " + msg); - _context.inNetMessagePool().add(msg, null, null); + + " (for client " + _clientNickname + " (" + + ((_client != null) ? _client.toBase32() : "null") + + ") to target=NULL/tunnel=NULL " + msg); + if (_msgIdBloomXor == 0) + _context.inNetMessagePool().add(msg, null, null); + else + _context.inNetMessagePool().add(msg, null, null, _msgIdBloomXor); } } else if (_context.routerHash().equals(target)) { if (type == GarlicMessage.MESSAGE_TYPE) if (_log.shouldLog(Log.WARN)) _log.warn("Dropping inbound garlic message TARGETED TO OUR ROUTER for client " + + _clientNickname + " (" + ((_client != null) ? _client.toBase32() : "null") - + " to " + target + " / " + tunnel); + + ") to " + target + " / " + tunnel); else if (_log.shouldLog(Log.WARN)) _log.warn("Dropping inbound message TARGETED TO OUR ROUTER for client " - + ((_client != null) ? _client.toBase32() : "null") - + " to " + target + " / " + tunnel + " : " + msg); + + _clientNickname + " (" + ((_client != null) ? _client.toBase32() : "null") + + ") to " + target + " / " + tunnel + " : " + msg); return; } else { if (type == GarlicMessage.MESSAGE_TYPE) if (_log.shouldLog(Log.WARN)) _log.warn("Dropping targeted inbound garlic message for client " + + _clientNickname + " (" + ((_client != null) ? _client.toBase32() : "null") - + " to " + target + " / " + tunnel); + + ") to " + target + " / " + tunnel); else if (_log.shouldLog(Log.WARN)) - _log.warn("Dropping targeted inbound message for client " - + ((_client != null) ? _client.toBase32() : "null") + _log.warn("Dropping targeted inbound message for client " + _clientNickname + + " (" + ((_client != null) ? _client.toBase32() : "null") + " to " + target + " / " + tunnel + " : " + msg); return; } @@ -263,9 +289,13 @@ public void handleClove(DeliveryInstructions instructions, I2NPMessage data) { // ... and inject it. ((LeaseSet)dsm.getEntry()).setReceivedBy(_client); if (_log.shouldLog(Log.INFO)) - _log.info("Storing garlic LS down tunnel for: " + dsm.getKey() + " sent to: " + - (_client != null ? _client.toBase32() : "router")); - _context.inNetMessagePool().add(dsm, null, null); + _log.info("Storing garlic LS down tunnel for: " + dsm.getKey() + " sent to: " + + _clientNickname + " (" + + (_client != null ? _client.toBase32() : ") router")); + if (_msgIdBloomXor == 0) + _context.inNetMessagePool().add(dsm, null, null); + else + _context.inNetMessagePool().add(dsm, null, null, _msgIdBloomXor); } else { if (_client != null) { // drop it, since the data we receive shouldn't include router @@ -273,7 +303,8 @@ public void handleClove(DeliveryInstructions instructions, I2NPMessage data) { // open an attack vector) _context.statManager().addRateData("tunnel.dropDangerousClientTunnelMessage", 1, DatabaseStoreMessage.MESSAGE_TYPE); - _log.error("Dropped dangerous message down a tunnel for " + _client.toBase32() + ": " + dsm, new Exception("cause")); + _log.error("Dropped dangerous message down a tunnel for " + _clientNickname + + " ("+ _client.toBase32() + ") : " + dsm, new Exception("cause")); return; } // Case 3: @@ -282,11 +313,14 @@ public void handleClove(DeliveryInstructions instructions, I2NPMessage data) { // We must send to the InNetMessagePool so the message can be matched // and the search marked as successful. // note that encrypted replies to RI lookups is currently disables in ISJ, we won't get here. - // ... and inject it. if (_log.shouldLog(Log.INFO)) - _log.info("Storing garlic RI down tunnel for: " + dsm.getKey()); - _context.inNetMessagePool().add(dsm, null, null); + _log.info("Storing garlic RI down tunnel (" + _clientNickname + + ") for: " + dsm.getKey()); + if (_msgIdBloomXor == 0) + _context.inNetMessagePool().add(dsm, null, null); + else + _context.inNetMessagePool().add(dsm, null, null, _msgIdBloomXor); } } else if (_client != null && type == DatabaseSearchReplyMessage.MESSAGE_TYPE) { // DSRMs show up here now that replies are encrypted @@ -305,7 +339,10 @@ public void handleClove(DeliveryInstructions instructions, I2NPMessage data) { orig = newMsg; } ****/ - _context.inNetMessagePool().add(orig, null, null); + if (_msgIdBloomXor == 0) + _context.inNetMessagePool().add(orig, null, null); + else + _context.inNetMessagePool().add(orig, null, null, _msgIdBloomXor); } else if (type == DataMessage.MESSAGE_TYPE) { // a data message targetting the local router is how we send load tests (real // data messages target destinations) @@ -318,9 +355,14 @@ public void handleClove(DeliveryInstructions instructions, I2NPMessage data) { // as that might open an attack vector _context.statManager().addRateData("tunnel.dropDangerousClientTunnelMessage", 1, data.getType()); - _log.error("Dropped dangerous message down a tunnel for " + _client.toBase32() + ": " + data, new Exception("cause")); + _log.error("Dropped dangerous message received down a tunnel for " + + _clientNickname + " (" + _client.toBase32() + ") : " + + data, new Exception("cause")); } else { - _context.inNetMessagePool().add(data, null, null); + if (_msgIdBloomXor == 0) + _context.inNetMessagePool().add(data, null, null); + else + _context.inNetMessagePool().add(data, null, null, _msgIdBloomXor); } return; @@ -368,8 +410,8 @@ public void handleClove(DeliveryInstructions instructions, I2NPMessage data) { // allow distribute() to evaluate the message. if (_log.shouldLog(Log.INFO)) _log.info("Recursively handling message from targeted clove (for client:" - + ((_client != null) ? _client.toBase32() : "null") + ", msg type: " - + data.getClass().getSimpleName() + "): " + instructions.getRouter() + + _clientNickname + " " + ((_client != null) ? _client.toBase32() : "null") + + ", msg type: " + data.getClass().getSimpleName() + "): " + instructions.getRouter() + ":" + instructions.getTunnelId() + " msg: " + data); distribute(data, instructions.getRouter(), instructions.getTunnelId());