Merge branch '487-ls2' into 'master'

NetDB, I2CP: Fix tunnels going yellow for several minutes (Gitlab #487)

Closes #487

See merge request i2p-hackers/i2p.i2p!211
This commit is contained in:
zzz
2024-08-16 11:15:15 +00:00
12 changed files with 155 additions and 36 deletions
@@ -105,6 +105,7 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
private long _offlineExpiration;
private Signature _offlineSignature;
protected SigningPublicKey _transientSigningPublicKey;
private long _lastLS2SignTime;
// subsession stuff
// registered subsessions
@@ -1185,6 +1186,21 @@ public abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2
SessionId getSessionId() { return _sessionId; }
void setSessionId(SessionId id) { _sessionId = id; }
/**
* The published timestamp of the last LS2 we signed
*
* @return 0 if never
* @since 0.9.64
*/
long getLastLS2SignTime() { return _lastLS2SignTime; };
/**
* The published timestamp of the last LS2 we signed
*
* @since 0.9.64
*/
void setLastLS2SignTime(long now) { _lastLS2SignTime = now; };
/** configure the listener */
public void setSessionListener(I2PSessionListener lsnr) { _sessionListener = lsnr; }
@@ -130,19 +130,25 @@ class RequestLeaseSetMessageHandler extends HandlerImpl {
boolean isLS2 = requiresLS2(session);
LeaseSet leaseSet;
if (isLS2) {
LeaseSet2 ls2;
if (_ls2Type == DatabaseEntry.KEY_TYPE_LS2) {
leaseSet = new LeaseSet2();
ls2 = new LeaseSet2();
} else if (_ls2Type == DatabaseEntry.KEY_TYPE_ENCRYPTED_LS2) {
leaseSet = new EncryptedLeaseSet();
ls2 = new EncryptedLeaseSet();
} else if (_ls2Type == DatabaseEntry.KEY_TYPE_META_LS2) {
leaseSet = new MetaLeaseSet();
ls2= new MetaLeaseSet();
} else {
session.propogateError("Unsupported LS2 type", new Exception());
session.destroySession();
return;
}
if (Boolean.parseBoolean(session.getOptions().getProperty("i2cp.dontPublishLeaseSet")))
((LeaseSet2)leaseSet).setUnpublished();
ls2.setUnpublished();
// ensure 1-second resolution timestamp is higher than last one
long now = Math.max(_context.clock().now(), session.getLastLS2SignTime() + 1000);
ls2.setPublished(now);
session.setLastLS2SignTime(now);
leaseSet = ls2;
} else {
leaseSet = new LeaseSet();
}
@@ -42,19 +42,25 @@ class RequestVariableLeaseSetMessageHandler extends RequestLeaseSetMessageHandle
boolean isLS2 = requiresLS2(session);
LeaseSet leaseSet;
if (isLS2) {
LeaseSet2 ls2;
if (_ls2Type == DatabaseEntry.KEY_TYPE_LS2) {
leaseSet = new LeaseSet2();
ls2 = new LeaseSet2();
} else if (_ls2Type == DatabaseEntry.KEY_TYPE_ENCRYPTED_LS2) {
leaseSet = new EncryptedLeaseSet();
ls2 = new EncryptedLeaseSet();
} else if (_ls2Type == DatabaseEntry.KEY_TYPE_META_LS2) {
leaseSet = new MetaLeaseSet();
ls2 = new MetaLeaseSet();
} else {
session.propogateError("Unsupported LS2 type", new Exception());
session.destroySession();
return;
}
if (Boolean.parseBoolean(session.getOptions().getProperty("i2cp.dontPublishLeaseSet")))
((LeaseSet2)leaseSet).setUnpublished();
ls2.setUnpublished();
// ensure 1-second resolution timestamp is higher than last one
long now = Math.max(_context.clock().now(), session.getLastLS2SignTime() + 1000);
ls2.setPublished(now);
session.setLastLS2SignTime(now);
leaseSet = ls2;
} else {
leaseSet = new LeaseSet();
}
@@ -310,7 +310,7 @@ public class EncryptedLeaseSet extends LeaseSet2 {
DataHelper.writeLong(out, 2, _signingKey.getType().getCode());
_signingKey.writeBytes(out);
if (_published <= 0)
_published = Clock.getInstance().now();
setPublished(Clock.getInstance().now());
DataHelper.writeLong(out, 4, _published / 1000);
DataHelper.writeLong(out, 2, (_expires - _published) / 1000);
DataHelper.writeLong(out, 2, _flags);
+3
View File
@@ -238,6 +238,9 @@ public class LeaseSet extends DatabaseEntry {
* determine which LeaseSet was published more recently (later earliestLeaseSetDate
* means it was published later)
*
* Warning - do not use this for version comparison for LeaseSet2.
* Use LeaseSet2.getPublished() instead.
*
* @return earliest end date of any lease in the set, or -1 if there are no leases
*/
public long getEarliestLeaseDate() {
+16 -2
View File
@@ -66,6 +66,9 @@ public class LeaseSet2 extends LeaseSet {
* Published timestamp, as received.
* Different than getDate() or getEarliestLeaseDate(), which are the earliest lease expiration.
*
* Use this for LS2 version comparison, NOT getEarliestLeaseDate(), because
* that will return -1 for EncryptedLS and MetaLS.
*
* @return in ms, with 1 second resolution
* @since 0.9.39
*/
@@ -73,6 +76,18 @@ public class LeaseSet2 extends LeaseSet {
return _published;
}
/**
* Set published timestamp.
* Will be rounded to nearest second.
* If not called, will be set on write.
*
* @since 0.9.64
*/
public void setPublished(long now) {
// we round it here, so comparisons during verifies aren't wrong
_published = ((now + 500) / 1000) * 1000;
}
/**
* Published expiration, as received.
* May be different than getLatestLeaseDate(), which is the latest lease expiration.
@@ -524,8 +539,7 @@ public class LeaseSet2 extends LeaseSet {
protected void writeHeader(OutputStream out) throws DataFormatException, IOException {
_destination.writeBytes(out);
if (_published <= 0) {
// we round it here, so comparisons during verifies aren't wrong
_published = ((Clock.getInstance().now() + 500) / 1000) * 1000;
setPublished(Clock.getInstance().now());
}
long pub1k = _published / 1000;
DataHelper.writeLong(out, 4, pub1k);
@@ -22,6 +22,17 @@ public interface DataStore {
public DatabaseEntry get(Hash key, boolean persist);
public boolean put(Hash key, DatabaseEntry data);
public boolean put(Hash key, DatabaseEntry data, boolean persist);
/*
* Unconditionally store, bypass all newer/older checks
*
* @return success
* @param key non-null
* @param data non-null
* @since 0.9.64
*/
public boolean forcePut(Hash key, DatabaseEntry data);
public DatabaseEntry remove(Hash key);
public DatabaseEntry remove(Hash key, boolean persist);
public Set<Hash> getKeys();
@@ -61,15 +61,18 @@ class ExpireLeasesJob extends JobImpl {
*
*/
private List<Hash> selectKeysToExpire() {
RouterContext ctx = getContext();
List<Hash> toExpire = new ArrayList<Hash>(128);
for (Map.Entry<Hash, DatabaseEntry> entry : _facade.getDataStore().getMapEntries()) {
DatabaseEntry obj = entry.getValue();
if (obj.isLeaseSet()) {
LeaseSet ls = (LeaseSet)obj;
if (!ls.isCurrent(Router.CLOCK_FUDGE_FACTOR))
toExpire.add(entry.getKey());
else if (_log.shouldLog(Log.DEBUG))
_log.debug("Lease " + ls.getDestination().calculateHash() + " is current, no need to expire");
if (!ls.isCurrent(Router.CLOCK_FUDGE_FACTOR)) {
Hash h = entry.getKey();
toExpire.add(h);
if (ctx.clientManager().isLocal(h))
_log.logAlways(Log.WARN, "Expired local leaseset " + h.toBase32());
}
}
}
return toExpire;
@@ -146,22 +146,11 @@ class HandleFloodfillDatabaseStoreMessageJob extends JobImpl {
LeaseSet match = _facade.store(key, ls);
if (match == null) {
wasNew = true;
} else if (match.getEarliestLeaseDate() < ls.getEarliestLeaseDate()) {
} else if (KademliaNetworkDatabaseFacade.isNewer(ls, match)) {
wasNew = true;
// If it is in our keyspace and we are talking to it
//if (match.getReceivedAsPublished())
// ls.setReceivedAsPublished(true);
} else if (type != DatabaseEntry.KEY_TYPE_LEASESET &&
match.getType() != DatabaseEntry.KEY_TYPE_LEASESET) {
LeaseSet2 ls2 = (LeaseSet2) ls;
LeaseSet2 match2 = (LeaseSet2) match;
if (match2.getPublished() < ls2.getPublished()) {
wasNew = true;
//if (match.getReceivedAsPublished())
// ls.setReceivedAsPublished(true);
} else {
wasNew = false;
}
} else {
wasNew = false;
// The FloodOnlyLookupSelector goes away after the first good reply
@@ -881,6 +881,8 @@ public abstract class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacad
private static final long PUBLISH_DELAY = 3*1000;
/**
* Stores in local netdb, and publishes to floodfill if client manager says to
*
* @throws IllegalArgumentException if the leaseSet is not valid
*/
public void publish(LeaseSet localLeaseSet) throws IllegalArgumentException {
@@ -891,7 +893,8 @@ public abstract class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacad
}
Hash h = localLeaseSet.getHash();
try {
store(h, localLeaseSet);
// force overwrite of previous entry
store(h, localLeaseSet, true);
} catch (IllegalArgumentException iae) {
_log.error("locally published leaseSet is not valid?", iae);
throw iae;
@@ -1078,7 +1081,7 @@ public abstract class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacad
}
return null;
}
/**
* Store the leaseSet.
*
@@ -1090,12 +1093,28 @@ public abstract class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacad
* @return previous entry or null
*/
public LeaseSet store(Hash key, LeaseSet leaseSet) throws IllegalArgumentException {
return store(key, leaseSet, false);
}
/**
* Store the leaseSet.
*
* If the store fails due to unsupported crypto, it will negative cache
* the hash until restart.
*
* @param force always store even if not newer
* @throws IllegalArgumentException if the leaseSet is not valid
* @throws UnsupportedCryptoException if that's why it failed.
* @return previous entry or null
* @since 0.9.64
*/
public LeaseSet store(Hash key, LeaseSet leaseSet, boolean force) throws IllegalArgumentException {
if (!_initialized) return null;
LeaseSet rv;
try {
rv = (LeaseSet)_ds.get(key);
if (rv != null && rv.getEarliestLeaseDate() >= leaseSet.getEarliestLeaseDate()) {
if (rv != null && !force && !isNewer(leaseSet, rv)) {
if (_log.shouldDebug())
_log.debug("Not storing older " + key);
// if it hasn't changed, no need to do anything
@@ -1117,7 +1136,7 @@ public abstract class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacad
// spoof / hash collision detection
// todo allow non-exp to overwrite exp
if (rv != null) {
if (rv != null && !force) {
Destination d1 = leaseSet.getDestination();
Destination d2 = rv.getDestination();
if (d1 != null && d2 != null && !d1.equals(d2))
@@ -1159,9 +1178,10 @@ public abstract class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacad
if (err != null)
throw new IllegalArgumentException("Invalid store attempt - " + err);
if (_log.shouldDebug())
_log.debug("Storing LS to the data store...");
_ds.put(key, leaseSet);
if (force)
_ds.forcePut(key, leaseSet);
else
_ds.put(key, leaseSet);
if (encls != null) {
// we now have decrypted it, store it as well
@@ -1205,6 +1225,24 @@ public abstract class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacad
return rv;
}
/**
* Utility to determine if a is newer than b.
* Uses publish date if a and b are both LS2, else earliest lease date.
*
* @param a non-null
* @param b non-null
* @return if a is newer than b
* @since 0.9.64
*/
public static boolean isNewer(LeaseSet a, LeaseSet b) {
if (a.getType() != DatabaseEntry.KEY_TYPE_LEASESET &&
b.getType() != DatabaseEntry.KEY_TYPE_LEASESET) {
return ((LeaseSet2) a).getPublished() > ((LeaseSet2) b).getPublished();
} else {
return a.getEarliestLeaseDate() > b.getEarliestLeaseDate();
}
}
private static final int MIN_ROUTERS = 90;
@@ -164,6 +164,24 @@ public class PersistentDataStore extends TransientDataStore {
_writer.queue(key, data);
return rv;
}
/*
* Unconditionally store, bypass all newer/older checks.
* Persists for RI only.
*
* @param persist if false, call super only, don't access disk
* @return success
* @param key non-null
* @param data non-null
* @since 0.9.64
*/
@Override
public boolean forcePut(Hash key, DatabaseEntry data) {
boolean rv = super.forcePut(key, data);
if (rv && data.getType() == DatabaseEntry.KEY_TYPE_ROUTERINFO)
_writer.queue(key, data);
return rv;
}
/** How many files to write every 10 minutes. Doesn't make sense to limit it,
* they just back up in the queue hogging memory.
@@ -329,10 +347,11 @@ public class PersistentDataStore extends TransientDataStore {
if (fos != null) try { fos.close(); } catch (IOException ioe) {}
}
}
private long getPublishDate(DatabaseEntry data) {
private static long getPublishDate(DatabaseEntry data) {
return data.getDate();
}
/**
* This was mostly for manual reseeding, i.e. the user manually
* copies RI files to the directory. Nobody does this,
@@ -190,6 +190,20 @@ class TransientDataStore implements DataStore {
}
return rv;
}
/*
* Unconditionally store, bypass all newer/older checks
*
* @return success
* @param key non-null
* @param data non-null
* @since 0.9.64
*/
@Override
public boolean forcePut(Hash key, DatabaseEntry data) {
_data.put(key, data);
return true;
}
@Override
public String toString() {