Compare commits

...

2 Commits

Author SHA1 Message Date
idk
4066e5df02 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.
2023-05-17 16:41:38 +00:00
c49023af18 Router/Tunnel: Handling Updates for Inbound Messages.
Eliminate the forwarding of messages in the Inbound Message Distributor.

The Inbound Message Distributor is the end point, and attempting
to handling forwarding instructions opens up too many possibilities
for potentially dangerous scenarios.

Typically, the messages with forwarding instructions are the result
of decrypting garlic messages that yield another I2NP message,
which is recursively handled.

It shouldn't be necessary to forward messages at this point.
The client should handle any decisions regarding forwarding, not
the router.

Normally, I don't clean up the code in a given commit, but there
were some remnants of commented-out code right in the middle
of the section being reworked, and this deprecated code would
have been out of context if not removed.

Signed-off-by: obscuratus <obscuratus@mail.i2p>
2023-04-25 19:08:43 +00:00
4 changed files with 120 additions and 59 deletions

View File

@ -119,6 +119,10 @@ public class InNetMessagePool implements Service {
_handlerJobBuilders[i2npMessageType] = builder; _handlerJobBuilders[i2npMessageType] = builder;
return old; return old;
} }
public int add(I2NPMessage messageBody, RouterIdentity fromRouter, Hash fromRouterHash) {
return add(messageBody, fromRouter, fromRouterHash, 0);
}
/** /**
* Add a new message to the pool. * 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 * @return -1 for some types of errors but not all; 0 otherwise
* (was queue length, long ago) * (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 MessageHistory history = _context.messageHistory();
final boolean doHistory = history.getDoLog(); final boolean doHistory = history.getDoLog();
@ -158,7 +165,11 @@ public class InNetMessagePool implements Service {
// just validate the expiration // just validate the expiration
invalidReason = _context.messageValidator().validateMessage(exp); invalidReason = _context.messageValidator().validateMessage(exp);
} else { } 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) { if (invalidReason != null) {

View File

@ -85,7 +85,9 @@ public class TunnelPoolSettings {
private static final int MIN_PRIORITY = -25; private static final int MIN_PRIORITY = -25;
private static final int MAX_PRIORITY = 25; private static final int MAX_PRIORITY = 25;
private static final int EXPLORATORY_PRIORITY = 30; private static final int EXPLORATORY_PRIORITY = 30;
private final long _msgIdBloomXor;
/** /**
* Exploratory tunnel * Exploratory tunnel
*/ */
@ -116,6 +118,8 @@ public class TunnelPoolSettings {
_IPRestriction = DEFAULT_IP_RESTRICTION; _IPRestriction = DEFAULT_IP_RESTRICTION;
_unknownOptions = new Properties(); _unknownOptions = new Properties();
_randomKey = generateRandomKey(); _randomKey = generateRandomKey();
_msgIdBloomXor = RandomSource.getInstance().nextLong();
if (_isExploratory && !_isInbound) if (_isExploratory && !_isInbound)
_priority = EXPLORATORY_PRIORITY; _priority = EXPLORATORY_PRIORITY;
if (!_isExploratory) if (!_isExploratory)
@ -286,6 +290,8 @@ public class TunnelPoolSettings {
*/ */
public Properties getUnknownOptions() { return _unknownOptions; } public Properties getUnknownOptions() { return _unknownOptions; }
public long getMsgIdBloomXor() { return _msgIdBloomXor; }
/** /**
* Defaults in props are NOT honored. * Defaults in props are NOT honored.
* In-JVM client side must promote defaults to the primary map. * In-JVM client side must promote defaults to the primary map.

View File

@ -111,6 +111,8 @@ public class TransportManager implements TransportEventListener {
private static final long UPNP_REFRESH_TIME = UPnP.LEASE_TIME_SECONDS * 1000L / 3; private static final long UPNP_REFRESH_TIME = UPnP.LEASE_TIME_SECONDS * 1000L / 3;
private final long _msgIdBloomXor;
public TransportManager(RouterContext context) { public TransportManager(RouterContext context) {
_context = context; _context = context;
_log = _context.logManager().getLog(TransportManager.class); _log = _context.logManager().getLog(TransportManager.class);
@ -134,6 +136,7 @@ public class TransportManager implements TransportEventListener {
_dhThread = (_enableUDP || enableNTCP2) ? new DHSessionKeyBuilder.PrecalcRunner(context) : null; _dhThread = (_enableUDP || enableNTCP2) ? new DHSessionKeyBuilder.PrecalcRunner(context) : null;
// always created, even if NTCP2 is not enabled, because ratchet needs it // always created, even if NTCP2 is not enabled, because ratchet needs it
_xdhThread = new X25519KeyFactory(context); _xdhThread = new X25519KeyFactory(context);
_msgIdBloomXor = _context.random().nextLong();
} }
/** /**
@ -965,7 +968,7 @@ public class TransportManager implements TransportEventListener {
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("I2NPMessage received: " + message.getClass().getSimpleName() /*, new Exception("Where did I come from again?") */ ); _log.debug("I2NPMessage received: " + message.getClass().getSimpleName() /*, new Exception("Where did I come from again?") */ );
try { try {
_context.inNetMessagePool().add(message, fromRouter, fromRouterHash); _context.inNetMessagePool().add(message, fromRouter, fromRouterHash, _msgIdBloomXor);
//if (_log.shouldLog(Log.DEBUG)) //if (_log.shouldLog(Log.DEBUG))
// _log.debug("Added to in pool"); // _log.debug("Added to in pool");
} catch (IllegalArgumentException iae) { } catch (IllegalArgumentException iae) {

View File

@ -33,7 +33,8 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
private final Log _log; private final Log _log;
private final Hash _client; private final Hash _client;
private final GarlicMessageReceiver _receiver; private final GarlicMessageReceiver _receiver;
private String _clientNickname;
private final long _msgIdBloomXor;
/** /**
* @param client null for router tunnel * @param client null for router tunnel
*/ */
@ -43,6 +44,23 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
_log = ctx.logManager().getLog(InboundMessageDistributor.class); _log = ctx.logManager().getLog(InboundMessageDistributor.class);
_receiver = new GarlicMessageReceiver(ctx, this, client); _receiver = new GarlicMessageReceiver(ctx, this, client);
// all createRateStat in TunnelDispatcher // 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) { public void distribute(I2NPMessage msg, Hash target) {
@ -51,7 +69,9 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
public void distribute(I2NPMessage msg, Hash target, TunnelId tunnel) { public void distribute(I2NPMessage msg, Hash target, TunnelId tunnel) {
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("IBMD for " + _client + " 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 // allow messages on client tunnels even after client disconnection, as it may
// include e.g. test messages, etc. DataMessages will be dropped anyway // include e.g. test messages, etc. DataMessages will be dropped anyway
@ -98,7 +118,8 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
// We handle this safely, so we don't ask him again. // 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) // 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)) 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 // Handle safely by just updating the caps table, after doing basic validation
Hash key = dsm.getKey(); Hash key = dsm.getKey();
if (_context.routerHash().equals(key)) if (_context.routerHash().equals(key))
@ -176,9 +197,12 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
} // switch } // switch
} // client != null } // client != null
if ( (target == null) || ( (tunnel == null) && (_context.routerHash().equals(target) ) ) ) { if ( (target == null) && (tunnel == null) ) {
// targetting us either implicitly (no target) or explicitly (no tunnel) // Since the InboundMessageDistributor handles messages for the endpoint,
// make sure we don't honor any remote requests directly (garlic instructions, etc) // most messages that arrive here have both target==null and tunnel==null.
// Messages with targeting instructions need careful handling, and will
// typically be dropped because we're the endpoint. Especially when they
// specifically target this router (_context.routerHash().equals(target)).
if (type == GarlicMessage.MESSAGE_TYPE) { if (type == GarlicMessage.MESSAGE_TYPE) {
// in case we're looking for replies to a garlic message (cough load tests cough) // in case we're looking for replies to a garlic message (cough load tests cough)
_context.inNetMessagePool().handleReplies(msg); _context.inNetMessagePool().handleReplies(msg);
@ -187,47 +211,43 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
_receiver.receive((GarlicMessage)msg); _receiver.receive((GarlicMessage)msg);
} else { } else {
if (_log.shouldLog(Log.INFO)) if (_log.shouldLog(Log.INFO))
_log.info("distributing inbound tunnel message into our inNetMessagePool: " + msg); _log.info("distributing inbound tunnel message into our inNetMessagePool"
_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);
} }
/****** latency measuring attack?
} else if (_context.routerHash().equals(target)) { } else if (_context.routerHash().equals(target)) {
// the want to send it to a tunnel, except we are also that tunnel's gateway if (type == GarlicMessage.MESSAGE_TYPE)
// dispatch it directly
if (_log.shouldLog(Log.INFO))
_log.info("distributing inbound tunnel message back out, except we are the gateway");
TunnelGatewayMessage gw = new TunnelGatewayMessage(_context);
gw.setMessage(msg);
gw.setTunnelId(tunnel);
gw.setMessageExpiration(_context.clock().now()+10*1000);
gw.setUniqueId(_context.random().nextLong(I2NPMessage.MAX_ID_VALUE));
_context.tunnelDispatcher().dispatch(gw);
******/
} else {
// ok, they want us to send it remotely, but that'd bust our anonymity,
// so we send it out a tunnel first
// TODO use the OCMOSJ cache to pick OB tunnel we are already using?
TunnelInfo out = _context.tunnelManager().selectOutboundTunnel(_client, target);
if (out == null) {
if (_log.shouldLog(Log.WARN)) if (_log.shouldLog(Log.WARN))
_log.warn("no outbound tunnel to send the client message for " + _client + ": " + msg); _log.warn("Dropping inbound garlic message TARGETED TO OUR ROUTER for client "
return; + _clientNickname + " ("
} + ((_client != null) ? _client.toBase32() : "null")
if (_log.shouldLog(Log.DEBUG)) + ") to " + target + " / " + tunnel);
_log.debug("distributing IB tunnel msg type " + type + " back out " + out else
+ " targetting " + target); if (_log.shouldLog(Log.WARN))
TunnelId outId = out.getSendTunnelId(0); _log.warn("Dropping inbound message TARGETED TO OUR ROUTER for client "
if (outId == null) { + _clientNickname + " (" + ((_client != null) ? _client.toBase32() : "null")
if (_log.shouldLog(Log.ERROR)) + ") to " + target + " / " + tunnel + " : " + msg);
_log.error("strange? outbound tunnel has no outboundId? " + out return;
+ " failing to distribute " + msg); } else {
return; if (type == GarlicMessage.MESSAGE_TYPE)
} if (_log.shouldLog(Log.WARN))
long exp = _context.clock().now() + 20*1000; _log.warn("Dropping targeted inbound garlic message for client "
if (msg.getMessageExpiration() < exp) + _clientNickname + " ("
msg.setMessageExpiration(exp); + ((_client != null) ? _client.toBase32() : "null")
_context.tunnelDispatcher().dispatchOutbound(msg, outId, tunnel, target); + ") to " + target + " / " + tunnel);
else
if (_log.shouldLog(Log.WARN))
_log.warn("Dropping targeted inbound message for client " + _clientNickname
+ " (" + ((_client != null) ? _client.toBase32() : "null")
+ " to " + target + " / " + tunnel + " : " + msg);
return;
} }
} }
/** /**
@ -269,9 +289,13 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
// ... and inject it. // ... and inject it.
((LeaseSet)dsm.getEntry()).setReceivedBy(_client); ((LeaseSet)dsm.getEntry()).setReceivedBy(_client);
if (_log.shouldLog(Log.INFO)) if (_log.shouldLog(Log.INFO))
_log.info("Storing garlic LS down tunnel for: " + dsm.getKey() + " sent to: " + _log.info("Storing garlic LS down tunnel for: " + dsm.getKey() + " sent to: "
(_client != null ? _client.toBase32() : "router")); + _clientNickname + " ("
_context.inNetMessagePool().add(dsm, null, null); + (_client != null ? _client.toBase32() : ") router"));
if (_msgIdBloomXor == 0)
_context.inNetMessagePool().add(dsm, null, null);
else
_context.inNetMessagePool().add(dsm, null, null, _msgIdBloomXor);
} else { } else {
if (_client != null) { if (_client != null) {
// drop it, since the data we receive shouldn't include router // drop it, since the data we receive shouldn't include router
@ -279,7 +303,8 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
// open an attack vector) // open an attack vector)
_context.statManager().addRateData("tunnel.dropDangerousClientTunnelMessage", 1, _context.statManager().addRateData("tunnel.dropDangerousClientTunnelMessage", 1,
DatabaseStoreMessage.MESSAGE_TYPE); 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; return;
} }
// Case 3: // Case 3:
@ -288,11 +313,14 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
// We must send to the InNetMessagePool so the message can be matched // We must send to the InNetMessagePool so the message can be matched
// and the search marked as successful. // and the search marked as successful.
// note that encrypted replies to RI lookups is currently disables in ISJ, we won't get here. // note that encrypted replies to RI lookups is currently disables in ISJ, we won't get here.
// ... and inject it. // ... and inject it.
if (_log.shouldLog(Log.INFO)) if (_log.shouldLog(Log.INFO))
_log.info("Storing garlic RI down tunnel for: " + dsm.getKey()); _log.info("Storing garlic RI down tunnel (" + _clientNickname
_context.inNetMessagePool().add(dsm, null, null); + ") 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) { } else if (_client != null && type == DatabaseSearchReplyMessage.MESSAGE_TYPE) {
// DSRMs show up here now that replies are encrypted // DSRMs show up here now that replies are encrypted
@ -311,7 +339,10 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
orig = newMsg; 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) { } else if (type == DataMessage.MESSAGE_TYPE) {
// a data message targetting the local router is how we send load tests (real // a data message targetting the local router is how we send load tests (real
// data messages target destinations) // data messages target destinations)
@ -324,9 +355,14 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
// as that might open an attack vector // as that might open an attack vector
_context.statManager().addRateData("tunnel.dropDangerousClientTunnelMessage", 1, _context.statManager().addRateData("tunnel.dropDangerousClientTunnelMessage", 1,
data.getType()); 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 { } 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; return;
@ -370,9 +406,14 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
case DeliveryInstructions.DELIVERY_MODE_ROUTER: // fall through case DeliveryInstructions.DELIVERY_MODE_ROUTER: // fall through
case DeliveryInstructions.DELIVERY_MODE_TUNNEL: case DeliveryInstructions.DELIVERY_MODE_TUNNEL:
// Targeted messages are usually dropped, but it is safe to
// allow distribute() to evaluate the message.
if (_log.shouldLog(Log.INFO)) if (_log.shouldLog(Log.INFO))
_log.info("clove targetted " + instructions.getRouter() + ":" + instructions.getTunnelId() _log.info("Recursively handling message from targeted clove (for client:"
+ ", treat recursively to prevent leakage"); + _clientNickname + " " + ((_client != null) ? _client.toBase32() : "null")
+ ", msg type: " + data.getClass().getSimpleName() + "): " + instructions.getRouter()
+ ":" + instructions.getTunnelId() + " msg: " + data);
distribute(data, instructions.getRouter(), instructions.getTunnelId()); distribute(data, instructions.getRouter(), instructions.getTunnelId());
return; return;