I2P Address: [http://git.idk.i2p]

Skip to content
Snippets Groups Projects
Unverified Commit 687858e7 authored by idk's avatar idk
Browse files

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.

patch created by @obscuratus, tested, reviewed and checked in by @obscuratus and @idk
parent 76197ce7
No related branches found
No related tags found
No related merge requests found
......@@ -119,6 +119,10 @@ public class InNetMessagePool implements Service {
_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 class InNetMessagePool implements Service {
* @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 class InNetMessagePool implements Service {
// 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) {
......
......@@ -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 class TunnelPoolSettings {
_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 class TunnelPoolSettings {
*/
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.
......
......@@ -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 class TransportManager implements TransportEventListener {
_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 class TransportManager implements TransportEventListener {
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) {
......
......@@ -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 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
_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 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
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 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
// 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 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
} 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 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
// ... 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 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
// 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 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
// 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 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
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 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
// 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 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
// 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());
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment