2005-07-19 jrandom
* Further preparation for removing I2CP crypto * Added some validation to the DH key agreement (thanks $anon) * Validate tunnel data message expirations (though not really a problem, since tunnels expire) * Minor PRNG threading cleanup
This commit is contained in:
@ -181,4 +181,4 @@ class I2CPMessageProducer {
|
||||
msg.setSessionId(session.getSessionId());
|
||||
session.sendMessage(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -119,49 +119,57 @@ class I2PSessionImpl2 extends I2PSessionImpl {
|
||||
|
||||
private boolean sendBestEffort(Destination dest, byte payload[], SessionKey keyUsed, Set tagsSent)
|
||||
throws I2PSessionException {
|
||||
long begin = _context.clock().now();
|
||||
if (_log.shouldLog(Log.DEBUG)) _log.debug("begin sendBestEffort");
|
||||
SessionKey key = _context.sessionKeyManager().getCurrentKey(dest.getPublicKey());
|
||||
if (_log.shouldLog(Log.DEBUG)) _log.debug("key fetched");
|
||||
if (key == null) key = _context.sessionKeyManager().createSession(dest.getPublicKey());
|
||||
SessionTag tag = _context.sessionKeyManager().consumeNextAvailableTag(dest.getPublicKey(), key);
|
||||
if (_log.shouldLog(Log.DEBUG)) _log.debug("tag consumed");
|
||||
SessionKey key = null;
|
||||
SessionKey newKey = null;
|
||||
SessionTag tag = null;
|
||||
Set sentTags = null;
|
||||
int oldTags = _context.sessionKeyManager().getAvailableTags(dest.getPublicKey(), key);
|
||||
long availTimeLeft = _context.sessionKeyManager().getAvailableTimeLeft(dest.getPublicKey(), key);
|
||||
int oldTags = 0;
|
||||
long begin = _context.clock().now();
|
||||
if (I2CPMessageProducer.END_TO_END_CRYPTO) {
|
||||
if (_log.shouldLog(Log.DEBUG)) _log.debug("begin sendBestEffort");
|
||||
key = _context.sessionKeyManager().getCurrentKey(dest.getPublicKey());
|
||||
if (_log.shouldLog(Log.DEBUG)) _log.debug("key fetched");
|
||||
if (key == null) key = _context.sessionKeyManager().createSession(dest.getPublicKey());
|
||||
tag = _context.sessionKeyManager().consumeNextAvailableTag(dest.getPublicKey(), key);
|
||||
if (_log.shouldLog(Log.DEBUG)) _log.debug("tag consumed");
|
||||
sentTags = null;
|
||||
oldTags = _context.sessionKeyManager().getAvailableTags(dest.getPublicKey(), key);
|
||||
long availTimeLeft = _context.sessionKeyManager().getAvailableTimeLeft(dest.getPublicKey(), key);
|
||||
|
||||
if ( (tagsSent == null) || (tagsSent.size() <= 0) ) {
|
||||
if (oldTags < NUM_TAGS) {
|
||||
sentTags = createNewTags(NUM_TAGS);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("** sendBestEffort only had " + oldTags + " with " + availTimeLeft + ", adding " + NUM_TAGS + ": " + sentTags);
|
||||
} else if (availTimeLeft < 2 * 60 * 1000) {
|
||||
// if we have > 50 tags, but they expire in under 2 minutes, we want more
|
||||
sentTags = createNewTags(NUM_TAGS);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getPrefix() + "Tags expiring in " + availTimeLeft + ", adding " + NUM_TAGS + " new ones: " + sentTags);
|
||||
//_log.error("** sendBestEffort available time left " + availTimeLeft);
|
||||
if ( (tagsSent == null) || (tagsSent.size() <= 0) ) {
|
||||
if (oldTags < NUM_TAGS) {
|
||||
sentTags = createNewTags(NUM_TAGS);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("** sendBestEffort only had " + oldTags + " with " + availTimeLeft + ", adding " + NUM_TAGS + ": " + sentTags);
|
||||
} else if (availTimeLeft < 2 * 60 * 1000) {
|
||||
// if we have > 50 tags, but they expire in under 2 minutes, we want more
|
||||
sentTags = createNewTags(NUM_TAGS);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug(getPrefix() + "Tags expiring in " + availTimeLeft + ", adding " + NUM_TAGS + " new ones: " + sentTags);
|
||||
//_log.error("** sendBestEffort available time left " + availTimeLeft);
|
||||
} else {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("sendBestEffort old tags: " + oldTags + " available time left: " + availTimeLeft);
|
||||
}
|
||||
} else {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("sendBestEffort old tags: " + oldTags + " available time left: " + availTimeLeft);
|
||||
_log.debug("sendBestEffort is sending " + tagsSent.size() + " with " + availTimeLeft
|
||||
+ "ms left, " + oldTags + " tags known and "
|
||||
+ (tag == null ? "no tag" : " a valid tag"));
|
||||
}
|
||||
|
||||
if (false) // rekey
|
||||
newKey = _context.keyGenerator().generateSessionKey();
|
||||
|
||||
if ( (tagsSent != null) && (tagsSent.size() > 0) ) {
|
||||
if (sentTags == null)
|
||||
sentTags = new HashSet();
|
||||
sentTags.addAll(tagsSent);
|
||||
}
|
||||
} else {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("sendBestEffort is sending " + tagsSent.size() + " with " + availTimeLeft
|
||||
+ "ms left, " + oldTags + " tags known and "
|
||||
+ (tag == null ? "no tag" : " a valid tag"));
|
||||
// not using end to end crypto, so don't ever bundle any tags
|
||||
}
|
||||
|
||||
SessionKey newKey = null;
|
||||
if (false) // rekey
|
||||
newKey = _context.keyGenerator().generateSessionKey();
|
||||
|
||||
if ( (tagsSent != null) && (tagsSent.size() > 0) ) {
|
||||
if (sentTags == null)
|
||||
sentTags = new HashSet();
|
||||
sentTags.addAll(tagsSent);
|
||||
}
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG)) _log.debug("before creating nonce");
|
||||
|
||||
long nonce = _context.random().nextInt(Integer.MAX_VALUE);
|
||||
@ -174,10 +182,14 @@ class I2PSessionImpl2 extends I2PSessionImpl {
|
||||
if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Setting key = " + key);
|
||||
|
||||
if (keyUsed != null) {
|
||||
if (newKey != null)
|
||||
keyUsed.setData(newKey.getData());
|
||||
else
|
||||
keyUsed.setData(key.getData());
|
||||
if (I2CPMessageProducer.END_TO_END_CRYPTO) {
|
||||
if (newKey != null)
|
||||
keyUsed.setData(newKey.getData());
|
||||
else
|
||||
keyUsed.setData(key.getData());
|
||||
} else {
|
||||
keyUsed.setData(SessionKey.INVALID_KEY.getData());
|
||||
}
|
||||
}
|
||||
if (tagsSent != null) {
|
||||
if (sentTags != null) {
|
||||
|
@ -18,6 +18,7 @@ import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.I2PException;
|
||||
import net.i2p.data.ByteArray;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.data.SessionKey;
|
||||
@ -157,8 +158,14 @@ public class DHSessionKeyBuilder {
|
||||
// read: Y
|
||||
BigInteger Y = readBigI(in);
|
||||
if (Y == null) return null;
|
||||
builder.setPeerPublicValue(Y);
|
||||
return builder;
|
||||
try {
|
||||
builder.setPeerPublicValue(Y);
|
||||
return builder;
|
||||
} catch (InvalidPublicParameterException ippe) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("Key exchange failed (hostile peer?)", ippe);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static BigInteger readBigI(InputStream in) throws IOException {
|
||||
@ -175,7 +182,7 @@ public class DHSessionKeyBuilder {
|
||||
System.arraycopy(Y, 0, Y2, 1, 256);
|
||||
Y = Y2;
|
||||
}
|
||||
return new NativeBigInteger(Y);
|
||||
return new NativeBigInteger(1, Y);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -269,10 +276,11 @@ public class DHSessionKeyBuilder {
|
||||
* Specify the value given by the peer for use in the session key negotiation
|
||||
*
|
||||
*/
|
||||
public void setPeerPublicValue(BigInteger peerVal) {
|
||||
public void setPeerPublicValue(BigInteger peerVal) throws InvalidPublicParameterException {
|
||||
validatePublic(peerVal);
|
||||
_peerValue = peerVal;
|
||||
}
|
||||
public void setPeerPublicValue(byte val[]) {
|
||||
public void setPeerPublicValue(byte val[]) throws InvalidPublicParameterException {
|
||||
if (val.length != 256)
|
||||
throw new IllegalArgumentException("Peer public value must be exactly 256 bytes");
|
||||
|
||||
@ -284,7 +292,8 @@ public class DHSessionKeyBuilder {
|
||||
System.arraycopy(val, 0, val2, 1, 256);
|
||||
val = val2;
|
||||
}
|
||||
_peerValue = new NativeBigInteger(val);
|
||||
setPeerPublicValue(new NativeBigInteger(1, val));
|
||||
//_peerValue = new NativeBigInteger(val);
|
||||
}
|
||||
|
||||
public BigInteger getPeerPublicValue() {
|
||||
@ -355,8 +364,58 @@ public class DHSessionKeyBuilder {
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* rfc2631:
|
||||
* The following algorithm MAY be used to validate a received public key y.
|
||||
*
|
||||
* 1. Verify that y lies within the interval [2,p-1]. If it does not,
|
||||
* the key is invalid.
|
||||
* 2. Compute y^q mod p. If the result == 1, the key is valid.
|
||||
* Otherwise the key is invalid.
|
||||
*/
|
||||
private static final void validatePublic(BigInteger publicValue) throws InvalidPublicParameterException {
|
||||
int cmp = publicValue.compareTo(NativeBigInteger.ONE);
|
||||
if (cmp <= 0)
|
||||
throw new InvalidPublicParameterException("Public value is below two: " + publicValue.toString());
|
||||
|
||||
cmp = publicValue.compareTo(CryptoConstants.elgp);
|
||||
if (cmp >= 0)
|
||||
throw new InvalidPublicParameterException("Public value is above p-1: " + publicValue.toString());
|
||||
|
||||
// todo:
|
||||
// whatever validation needs to be done to mirror the rfc's part 2 (we don't have a q, so can't do
|
||||
// if (NativeBigInteger.ONE.compareTo(publicValue.modPow(q, CryptoConstants.elgp)) != 0)
|
||||
// throw new InvalidPublicParameterException("Invalid public value with y^q mod p != 1");
|
||||
//
|
||||
}
|
||||
|
||||
/*
|
||||
private static void testValidation() {
|
||||
NativeBigInteger bi = new NativeBigInteger("-3416069082912684797963255430346582466254460710249795973742848334283491150671563023437888953432878859472362439146158925287289114133666004165938814597775594104058593692562989626922979416277152479694258099203456493995467386903611666213773085025718340335205240293383622352894862685806192183268523899615405287022135356656720938278415659792084974076416864813957028335830794117802560169423133816961503981757298122040391506600117301607823659479051969827845787626261515313227076880722069706394405554113103165334903531980102626092646197079218895216346725765704256096661045699444128316078549709132753443706200863682650825635513");
|
||||
try {
|
||||
validatePublic(bi);
|
||||
System.err.println("valid?!");
|
||||
} catch (InvalidPublicParameterException ippe) {
|
||||
System.err.println("Ok, invalid. cool");
|
||||
}
|
||||
|
||||
byte val[] = bi.toByteArray();
|
||||
System.out.println("Len: " + val.length + " first is ok? " + ( (val[0] & 0x80) == 1)
|
||||
+ "\n" + DataHelper.toString(val, 64));
|
||||
NativeBigInteger bi2 = new NativeBigInteger(1, val);
|
||||
try {
|
||||
validatePublic(bi2);
|
||||
System.out.println("valid");
|
||||
} catch (InvalidPublicParameterException ippe) {
|
||||
System.out.println("invalid");
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
public static void main(String args[]) {
|
||||
//if (true) { testValidation(); return; }
|
||||
|
||||
RandomSource.getInstance().nextBoolean(); // warm it up
|
||||
try {
|
||||
Thread.sleep(20 * 1000);
|
||||
@ -365,36 +424,40 @@ public class DHSessionKeyBuilder {
|
||||
I2PAppContext ctx = new I2PAppContext();
|
||||
_log.debug("\n\n\n\nBegin test\n");
|
||||
long negTime = 0;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
long startNeg = Clock.getInstance().now();
|
||||
DHSessionKeyBuilder builder1 = new DHSessionKeyBuilder();
|
||||
DHSessionKeyBuilder builder2 = new DHSessionKeyBuilder();
|
||||
BigInteger pub1 = builder1.getMyPublicValue();
|
||||
builder2.setPeerPublicValue(pub1);
|
||||
BigInteger pub2 = builder2.getMyPublicValue();
|
||||
builder1.setPeerPublicValue(pub2);
|
||||
SessionKey key1 = builder1.getSessionKey();
|
||||
SessionKey key2 = builder2.getSessionKey();
|
||||
long endNeg = Clock.getInstance().now();
|
||||
negTime += endNeg - startNeg;
|
||||
try {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
long startNeg = Clock.getInstance().now();
|
||||
DHSessionKeyBuilder builder1 = new DHSessionKeyBuilder();
|
||||
DHSessionKeyBuilder builder2 = new DHSessionKeyBuilder();
|
||||
BigInteger pub1 = builder1.getMyPublicValue();
|
||||
builder2.setPeerPublicValue(pub1);
|
||||
BigInteger pub2 = builder2.getMyPublicValue();
|
||||
builder1.setPeerPublicValue(pub2);
|
||||
SessionKey key1 = builder1.getSessionKey();
|
||||
SessionKey key2 = builder2.getSessionKey();
|
||||
long endNeg = Clock.getInstance().now();
|
||||
negTime += endNeg - startNeg;
|
||||
|
||||
if (!key1.equals(key2))
|
||||
_log.error("**ERROR: Keys do not match");
|
||||
else
|
||||
_log.debug("**Success: Keys match");
|
||||
if (!key1.equals(key2))
|
||||
_log.error("**ERROR: Keys do not match");
|
||||
else
|
||||
_log.debug("**Success: Keys match");
|
||||
|
||||
byte iv[] = new byte[16];
|
||||
RandomSource.getInstance().nextBytes(iv);
|
||||
String origVal = "1234567890123456"; // 16 bytes max using AESEngine
|
||||
byte enc[] = new byte[16];
|
||||
byte dec[] = new byte[16];
|
||||
ctx.aes().encrypt(origVal.getBytes(), 0, enc, 0, key1, iv, 16);
|
||||
ctx.aes().decrypt(enc, 0, dec, 0, key2, iv, 16);
|
||||
String tranVal = new String(dec);
|
||||
if (origVal.equals(tranVal))
|
||||
_log.debug("**Success: D(E(val)) == val");
|
||||
else
|
||||
_log.error("**ERROR: D(E(val)) != val [val=(" + tranVal + "), origVal=(" + origVal + ")");
|
||||
byte iv[] = new byte[16];
|
||||
RandomSource.getInstance().nextBytes(iv);
|
||||
String origVal = "1234567890123456"; // 16 bytes max using AESEngine
|
||||
byte enc[] = new byte[16];
|
||||
byte dec[] = new byte[16];
|
||||
ctx.aes().encrypt(origVal.getBytes(), 0, enc, 0, key1, iv, 16);
|
||||
ctx.aes().decrypt(enc, 0, dec, 0, key2, iv, 16);
|
||||
String tranVal = new String(dec);
|
||||
if (origVal.equals(tranVal))
|
||||
_log.debug("**Success: D(E(val)) == val");
|
||||
else
|
||||
_log.error("**ERROR: D(E(val)) != val [val=(" + tranVal + "), origVal=(" + origVal + ")");
|
||||
}
|
||||
} catch (InvalidPublicParameterException ippe) {
|
||||
_log.error("Invalid dh", ippe);
|
||||
}
|
||||
_log.debug("Negotiation time for 5 runs: " + negTime + " @ " + negTime / 5l + "ms each");
|
||||
try {
|
||||
@ -451,4 +514,13 @@ public class DHSessionKeyBuilder {
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
public static class InvalidPublicParameterException extends I2PException {
|
||||
public InvalidPublicParameterException() {
|
||||
super();
|
||||
}
|
||||
public InvalidPublicParameterException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
}
|
@ -220,10 +220,10 @@ class TransientSessionKeyManager extends SessionKeyManager {
|
||||
*
|
||||
*/
|
||||
public void tagsDelivered(PublicKey target, SessionKey key, Set sessionTags) {
|
||||
if (_log.shouldLog(Log.WARN)) {
|
||||
if (_log.shouldLog(Log.DEBUG)) {
|
||||
//_log.debug("Tags delivered to set " + set + " on session " + sess);
|
||||
if (sessionTags.size() > 0)
|
||||
_log.warn("Tags delivered: " + sessionTags.size() + " for key: " + key.toBase64() + ": " + sessionTags);
|
||||
_log.debug("Tags delivered: " + sessionTags.size() + " for key: " + key.toBase64() + ": " + sessionTags);
|
||||
}
|
||||
OutboundSession sess = getSession(target);
|
||||
if (sess == null) {
|
||||
@ -286,10 +286,10 @@ class TransientSessionKeyManager extends SessionKeyManager {
|
||||
}
|
||||
}
|
||||
|
||||
if (_log.shouldLog(Log.ERROR)) {
|
||||
_log.error("Multiple tags matching! tagSet: " + tagSet + " and old tagSet: " + old + " tag: " + dupTag);
|
||||
_log.error("Earlier tag set creation: " + old + ": key=" + old.getAssociatedKey().toBase64(), old.getCreatedBy());
|
||||
_log.error("Current tag set creation: " + tagSet + ": key=" + tagSet.getAssociatedKey().toBase64(), tagSet.getCreatedBy());
|
||||
if (_log.shouldLog(Log.WARN)) {
|
||||
_log.warn("Multiple tags matching! tagSet: " + tagSet + " and old tagSet: " + old + " tag: " + dupTag + "/" + dupTag.toBase64());
|
||||
_log.warn("Earlier tag set creation: " + old + ": key=" + old.getAssociatedKey().toBase64(), old.getCreatedBy());
|
||||
_log.warn("Current tag set creation: " + tagSet + ": key=" + tagSet.getAssociatedKey().toBase64(), tagSet.getCreatedBy());
|
||||
}
|
||||
}
|
||||
|
||||
@ -413,7 +413,7 @@ class TransientSessionKeyManager extends SessionKeyManager {
|
||||
long now = _context.clock().now();
|
||||
StringBuffer buf = null;
|
||||
StringBuffer bufSummary = null;
|
||||
if (_log.shouldLog(Log.WARN)) {
|
||||
if (_log.shouldLog(Log.DEBUG)) {
|
||||
buf = new StringBuffer(128);
|
||||
buf.append("Expiring inbound: ");
|
||||
bufSummary = new StringBuffer(1024);
|
||||
@ -438,9 +438,9 @@ class TransientSessionKeyManager extends SessionKeyManager {
|
||||
}
|
||||
_context.statManager().addRateData("crypto.sessionTagsRemaining", remaining, 0);
|
||||
if ( (buf != null) && (removed > 0) )
|
||||
_log.warn(buf.toString());
|
||||
_log.debug(buf.toString());
|
||||
if (bufSummary != null)
|
||||
_log.warn("Cleaning up with remaining: " + bufSummary.toString());
|
||||
_log.debug("Cleaning up with remaining: " + bufSummary.toString());
|
||||
|
||||
//_log.warn("Expiring tags: [" + tagsToDrop + "]");
|
||||
|
||||
|
@ -10,6 +10,7 @@ package net.i2p.data;
|
||||
*/
|
||||
|
||||
import java.io.Serializable;
|
||||
import net.i2p.data.Base64;
|
||||
|
||||
/**
|
||||
* Wrap up an array of bytes so that they can be compared and placed in hashes,
|
||||
@ -82,6 +83,10 @@ public class ByteArray implements Serializable, Comparable {
|
||||
}
|
||||
|
||||
public final String toString() {
|
||||
return DataHelper.toString(getData(), 32);
|
||||
return super.toString() + "/" + DataHelper.toString(getData(), 32) + "." + _valid;
|
||||
}
|
||||
|
||||
public final String toBase64() {
|
||||
return Base64.encode(_data, _offset, _valid);
|
||||
}
|
||||
}
|
@ -27,6 +27,7 @@ public class SessionKey extends DataStructureImpl {
|
||||
private Object _preparedKey;
|
||||
|
||||
public final static int KEYSIZE_BYTES = 32;
|
||||
public static final SessionKey INVALID_KEY = new SessionKey(new byte[KEYSIZE_BYTES]);
|
||||
|
||||
public SessionKey() {
|
||||
this(null);
|
||||
|
@ -31,11 +31,13 @@ public class BufferedStatLog implements StatLog {
|
||||
private String _outFile;
|
||||
|
||||
private static final int BUFFER_SIZE = 1024;
|
||||
private static final boolean DISABLE_LOGGING = false;
|
||||
|
||||
public BufferedStatLog(I2PAppContext ctx) {
|
||||
_context = ctx;
|
||||
_log = ctx.logManager().getLog(BufferedStatLog.class);
|
||||
_events = new StatEvent[BUFFER_SIZE];
|
||||
if (DISABLE_LOGGING) return;
|
||||
for (int i = 0; i < BUFFER_SIZE; i++)
|
||||
_events[i] = new StatEvent();
|
||||
_eventNext = 0;
|
||||
@ -48,6 +50,7 @@ public class BufferedStatLog implements StatLog {
|
||||
}
|
||||
|
||||
public void addData(String scope, String stat, long value, long duration) {
|
||||
if (DISABLE_LOGGING) return;
|
||||
synchronized (_events) {
|
||||
_events[_eventNext].init(scope, stat, value, duration);
|
||||
_eventNext = (_eventNext + 1) % _events.length;
|
||||
|
@ -85,11 +85,15 @@ public final class ByteCache {
|
||||
*
|
||||
*/
|
||||
public final void release(ByteArray entry) {
|
||||
release(entry, true);
|
||||
}
|
||||
public final void release(ByteArray entry, boolean shouldZero) {
|
||||
if (_cache) {
|
||||
if ( (entry == null) || (entry.getData() == null) )
|
||||
return;
|
||||
|
||||
Arrays.fill(entry.getData(), (byte)0x0);
|
||||
if (shouldZero)
|
||||
Arrays.fill(entry.getData(), (byte)0x0);
|
||||
synchronized (_available) {
|
||||
if (_available.size() < _maxCached)
|
||||
_available.add(entry);
|
||||
|
@ -61,9 +61,14 @@ public class PooledRandomSource extends RandomSource {
|
||||
}
|
||||
|
||||
private final RandomSource pickPRNG() {
|
||||
int i = _nextPool % POOL_SIZE;
|
||||
_nextPool = (++_nextPool) % POOL_SIZE;
|
||||
return _pool[i];
|
||||
// how much more explicit can we get?
|
||||
int cur = _nextPool;
|
||||
cur = cur % POOL_SIZE;
|
||||
RandomSource rv = _pool[cur];
|
||||
cur++;
|
||||
cur = cur % POOL_SIZE;
|
||||
_nextPool = cur;
|
||||
return rv;
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user