Skip to content

Commit

Permalink
Router/Tunnel: xor message IDs in order to prevent cross-context leaks.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
eyedeekay committed May 17, 2023
1 parent c49023a commit 4066e5d
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 29 deletions.
15 changes: 13 additions & 2 deletions router/java/src/net/i2p/router/InNetMessagePool.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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();

Expand All @@ -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) {
Expand Down
8 changes: 7 additions & 1 deletion router/java/src/net/i2p/router/TunnelPoolSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
}

/**
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -263,17 +289,22 @@ 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
// references, as that might get us to talk to them (and therefore
// 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:
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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;

Expand Down Expand Up @@ -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());
Expand Down

0 comments on commit 4066e5d

Please sign in to comment.