package net.i2p.router.networkdb.kademlia;
/*
* free (adj.): unencumbered; not under the control of others
* Written by jrandom in 2003 and released into the public domain
* with no warranty of any kind, either expressed or implied.
* It probably won't make your computer catch on fire, or eat
* your children, but it might. Use at your own risk.
*
*/
import java.util.Collection;
import java.util.Date;
import net.i2p.data.DatabaseEntry;
import net.i2p.data.Hash;
import net.i2p.data.LeaseSet;
import net.i2p.data.TunnelId;
import net.i2p.data.router.RouterAddress;
import net.i2p.data.router.RouterIdentity;
import net.i2p.data.router.RouterInfo;
import net.i2p.data.i2np.DatabaseStoreMessage;
import net.i2p.data.i2np.DeliveryStatusMessage;
import net.i2p.data.i2np.TunnelGatewayMessage;
import net.i2p.router.Job;
import net.i2p.router.JobImpl;
import net.i2p.router.OutNetMessage;
import net.i2p.router.RouterContext;
import net.i2p.router.TunnelInfo;
import net.i2p.router.message.SendMessageDirectJob;
import net.i2p.util.Log;
/**
* Receive DatabaseStoreMessage data and store it in the local net db
*
*/
public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl {
private final Log _log;
private final DatabaseStoreMessage _message;
private final RouterIdentity _from;
private Hash _fromHash;
private final FloodfillNetworkDatabaseFacade _facade;
private final static int REPLY_TIMEOUT = 60*1000;
private final static int MESSAGE_PRIORITY = OutNetMessage.PRIORITY_NETDB_REPLY;
/**
* @param receivedMessage must never have reply token set if it came down a tunnel
*/
public HandleFloodfillDatabaseStoreMessageJob(RouterContext ctx, DatabaseStoreMessage receivedMessage,
RouterIdentity from, Hash fromHash,
FloodfillNetworkDatabaseFacade facade) {
super(ctx);
_log = ctx.logManager().getLog(getClass());
_message = receivedMessage;
_from = from;
_fromHash = fromHash;
_facade = facade;
}
public void runJob() {
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Handling database store message");
long recvBegin = System.currentTimeMillis();
String invalidMessage = null;
// set if invalid store but not his fault
boolean dontBlamePeer = false;
boolean wasNew = false;
RouterInfo prevNetDb = null;
Hash key = _message.getKey();
DatabaseEntry entry = _message.getEntry();
if (entry.getType() == DatabaseEntry.KEY_TYPE_LEASESET) {
getContext().statManager().addRateData("netDb.storeLeaseSetHandled", 1);
if (_log.shouldLog(Log.INFO))
_log.info("Handling dbStore of leaseset " + _message);
//_log.info("Handling dbStore of leasset " + key + " with expiration of "
// + new Date(_message.getLeaseSet().getEarliestLeaseDate()));
try {
// Never store a leaseSet for a local dest received from somebody else.
// This generally happens from a FloodfillVerifyStoreJob.
// If it is valid, it shouldn't be newer than what we have - unless
// somebody has our keys...
// This could happen with multihoming - where it's really important to prevent
// storing the other guy's leaseset, it will confuse us badly.
if (getContext().clientManager().isLocal(key)) {
//getContext().statManager().addRateData("netDb.storeLocalLeaseSetAttempt", 1, 0);
// throw rather than return, so that we send the ack below (prevent easy attack)
dontBlamePeer = true;
throw new IllegalArgumentException("Peer attempted to store local leaseSet: " +
key.toBase64().substring(0, 4));
}
LeaseSet ls = (LeaseSet) entry;
//boolean oldrar = ls.getReceivedAsReply();
//boolean oldrap = ls.getReceivedAsPublished();
// If this was received as a response to a query,
// FloodOnlyLookupMatchJob called setReceivedAsReply(),
// and we are seeing this only as a duplicate,
// so we don't set the receivedAsPublished() flag.
// Otherwise, mark it as something we received unsolicited, so we'll answer queries
// for it. This flag must NOT get set on entries that we
// receive in response to our own lookups.
// See ../HDLMJ for more info
if (!ls.getReceivedAsReply())
ls.setReceivedAsPublished(true);
//boolean rap = ls.getReceivedAsPublished();
//if (_log.shouldLog(Log.INFO))
// _log.info("oldrap? " + oldrap + " oldrar? " + oldrar + " newrap? " + rap);
LeaseSet match = getContext().netDb().store(key, ls);
if (match == null) {
wasNew = true;
} else if (match.getEarliestLeaseDate() < ls.getEarliestLeaseDate()) {
wasNew = true;
// If it is in our keyspace and we are talking to it
if (match.getReceivedAsPublished())
ls.setReceivedAsPublished(true);
} else {
wasNew = false;
// The FloodOnlyLookupSelector goes away after the first good reply
// So on the second reply, FloodOnlyMatchJob is not called to set ReceivedAsReply.
// So then we think it's an unsolicited store.
// So we should skip this.
// If the 2nd reply is newer than the first, ReceivedAsPublished will be set incorrectly,
// that will hopefully be rare.
// A more elaborate solution would be a List of recent ReceivedAsReply LeaseSets, with receive time ?
// A real unsolicited store is likely to be new - hopefully...
//if (!ls.getReceivedAsReply())
// match.setReceivedAsPublished(true);
}
} catch (UnsupportedCryptoException uce) {
invalidMessage = uce.getMessage();
dontBlamePeer = true;
} catch (IllegalArgumentException iae) {
invalidMessage = iae.getMessage();
}
} else if (entry.getType() == DatabaseEntry.KEY_TYPE_ROUTERINFO) {
RouterInfo ri = (RouterInfo) entry;
getContext().statManager().addRateData("netDb.storeRouterInfoHandled", 1);
if (_log.shouldLog(Log.INFO))
_log.info("Handling dbStore of router " + key + " with publishDate of "
+ new Date(ri.getPublished()));
try {
// Never store our RouterInfo received from somebody else.
// This generally happens from a FloodfillVerifyStoreJob.
// If it is valid, it shouldn't be newer than what we have - unless
// somebody has our keys...
if (getContext().routerHash().equals(key)) {
//getContext().statManager().addRateData("netDb.storeLocalRouterInfoAttempt", 1, 0);
// This is initiated by PeerTestJob from another peer
// throw rather than return, so that we send the ack below (prevent easy attack)
dontBlamePeer = true;
throw new IllegalArgumentException("Peer attempted to store our RouterInfo");
}
getContext().profileManager().heardAbout(key);
prevNetDb = getContext().netDb().store(key, ri);
wasNew = ((null == prevNetDb) || (prevNetDb.getPublished() < ri.getPublished()));
// Check new routerinfo address against blocklist
if (wasNew) {
if (prevNetDb == null) {
if ((!getContext().banlist().isBanlistedForever(key)) &&
getContext().blocklist().isBlocklisted(ri) &&
_log.shouldLog(Log.WARN))
_log.warn("Blocklisting new peer " + key + ' ' + ri);
} else {
Collection<RouterAddress> oldAddr = prevNetDb.getAddresses();
Collection<RouterAddress> newAddr = ri.getAddresses();
if ((!newAddr.equals(oldAddr)) &&
(!getContext().banlist().isBanlistedForever(key)) &&
getContext().blocklist().isBlocklisted(ri) &&
_log.shouldLog(Log.WARN))
_log.warn("New address received, Blocklisting old peer " + key + ' ' + ri);
}
}
} catch (UnsupportedCryptoException uce) {
invalidMessage = uce.getMessage();
dontBlamePeer = true;
} catch (IllegalArgumentException iae) {
invalidMessage = iae.getMessage();
}
} else {
if (_log.shouldLog(Log.ERROR))
_log.error("Invalid DatabaseStoreMessage data type - " + entry.getType()
+ ": " + _message);
// don't ack or flood
return;
}
long recvEnd = System.currentTimeMillis();
getContext().statManager().addRateData("netDb.storeRecvTime", recvEnd-recvBegin);
// ack even if invalid
// in particular, ack our own RI (from PeerTestJob)
// TODO any cases where we shouldn't?
if (_message.getReplyToken() > 0)
sendAck(key);
long ackEnd = System.currentTimeMillis();
if (_from != null)
_fromHash = _from.getHash();
if (_fromHash != null) {
if (invalidMessage == null || dontBlamePeer) {
getContext().profileManager().dbStoreReceived(_fromHash, wasNew);
getContext().statManager().addRateData("netDb.storeHandled", ackEnd-recvEnd);
} else {
// Should we record in the profile?
if (_log.shouldLog(Log.WARN))
_log.warn("Peer " + _fromHash.toBase64() + " sent bad data: " + invalidMessage);
}
} else if (invalidMessage != null && !dontBlamePeer) {
if (_log.shouldLog(Log.WARN))
_log.warn("Unknown peer sent bad data: " + invalidMessage);
}
// flood it
if (invalidMessage == null &&
getContext().netDb().floodfillEnabled() &&
_message.getReplyToken() > 0) {
if (wasNew) {
// DOS prevention
// Note this does not throttle the ack above
if (_facade.shouldThrottleFlood(key)) {
if (_log.shouldLog(Log.WARN))
_log.warn("Too many recent stores, not flooding key: " + key);
getContext().statManager().addRateData("netDb.floodThrottled", 1);
return;
}
long floodBegin = System.currentTimeMillis();
_facade.flood(_message.getEntry());
// ERR: see comment in HandleDatabaseLookupMessageJob regarding hidden mode
//else if (!_message.getRouterInfo().isHidden())
long floodEnd = System.currentTimeMillis();
getContext().statManager().addRateData("netDb.storeFloodNew", floodEnd-floodBegin, 60*1000);
} else {
// don't flood it *again*
getContext().statManager().addRateData("netDb.storeFloodOld", 1);
}
}
}
private void sendAck(Hash storedKey) {
DeliveryStatusMessage msg = new DeliveryStatusMessage(getContext());
msg.setMessageId(_message.getReplyToken());
// Randomize for a little protection against clock-skew fingerprinting.
// But the "arrival" isn't used for anything, right?
// TODO just set to 0?
// TODO we have no session to garlic wrap this with, needs new message
msg.setArrival(getContext().clock().now() - getContext().random().nextInt(3*1000));
// may be null
TunnelId replyTunnel = _message.getReplyTunnel();
// A store of our own RI, only if we are not FF
DatabaseStoreMessage msg2;
if ((getContext().netDb().floodfillEnabled() && !getContext().router().gracefulShutdownInProgress()) ||
storedKey.equals(getContext().routerHash())) {
// don't send our RI if the store was our RI (from PeerTestJob)
msg2 = null;
} else {
// we aren't ff, send a go-away message
msg2 = new DatabaseStoreMessage(getContext());
RouterInfo me = getContext().router().getRouterInfo();
msg2.setEntry(me);
if (_log.shouldWarn())
_log.warn("Got a store w/ reply token, but we aren't ff: from: " + _from +
" fromHash: " + _fromHash + " msg: " + _message, new Exception());
}
Hash toPeer = _message.getReplyGateway();
boolean toUs = getContext().routerHash().equals(toPeer);
// to reduce connection congestion, send directly if connected already,
// else through an exploratory tunnel.
if (toUs && replyTunnel != null) {
// if we are the gateway, act as if we received it
TunnelGatewayMessage tgm = new TunnelGatewayMessage(getContext());
tgm.setMessage(msg);
tgm.setTunnelId(replyTunnel);
tgm.setMessageExpiration(msg.getMessageExpiration());
getContext().tunnelDispatcher().dispatch(tgm);
if (msg2 != null) {
TunnelGatewayMessage tgm2 = new TunnelGatewayMessage(getContext());
tgm2.setMessage(msg2);
tgm2.setTunnelId(replyTunnel);
tgm2.setMessageExpiration(msg.getMessageExpiration());
getContext().tunnelDispatcher().dispatch(tgm2);
}
} else if (toUs || getContext().commSystem().isEstablished(toPeer)) {
Job send = new SendMessageDirectJob(getContext(), msg, toPeer, REPLY_TIMEOUT, MESSAGE_PRIORITY);
send.runJob();
if (msg2 != null) {
Job send2 = new SendMessageDirectJob(getContext(), msg2, toPeer, REPLY_TIMEOUT, MESSAGE_PRIORITY);
send2.runJob();
}
} else {
// pick tunnel with endpoint closest to toPeer
TunnelInfo outTunnel = getContext().tunnelManager().selectOutboundExploratoryTunnel(toPeer);
if (outTunnel == null) {
if (_log.shouldLog(Log.WARN))
_log.warn("No outbound tunnel could be found");
return;
}
getContext().tunnelDispatcher().dispatchOutbound(msg, outTunnel.getSendTunnelId(0),
replyTunnel, toPeer);
if (msg2 != null)
getContext().tunnelDispatcher().dispatchOutbound(msg2, outTunnel.getSendTunnelId(0),
replyTunnel, toPeer);
}
}
public String getName() { return "Handle Database Store Message"; }
@Override
public void dropped() {
getContext().messageHistory().messageProcessingError(_message.getUniqueId(), _message.getClass().getName(), "Dropped due to overload");
}
}