diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java
index 627024b67..e8bda1a29 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java
@@ -150,6 +150,10 @@ public class EditBean extends IndexBean {
return false;
}
+ public int getCloseTime(int tunnel) {
+ return getProperty(tunnel, "closeIdleTime", 30);
+ }
+
public boolean getNewDest(int tunnel) {
return false;
}
diff --git a/apps/i2ptunnel/jsp/editClient.jsp b/apps/i2ptunnel/jsp/editClient.jsp
index f7ee2294c..f5e1fac7f 100644
--- a/apps/i2ptunnel/jsp/editClient.jsp
+++ b/apps/i2ptunnel/jsp/editClient.jsp
@@ -289,9 +289,9 @@
- Reduce when idle (minutes):
+ Close when idle (minutes):
-
+
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigTunnelsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigTunnelsHelper.java
index e21f9d9ce..8b8b2fb16 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigTunnelsHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigTunnelsHelper.java
@@ -49,7 +49,9 @@ public class ConfigTunnelsHelper extends HelperBase {
private static final int WARN_LENGTH = 4;
private static final int MAX_LENGTH = 4;
- private static final int MAX_QUANTITY = 3;
+ private static final int WARN_QUANTITY = 5;
+ private static final int MAX_QUANTITY = 6;
+ private static final int MAX_BACKUP_QUANTITY = 3;
private static final int MAX_VARIANCE = 2;
private static final int MIN_NEG_VARIANCE = -1;
private void renderForm(StringBuffer buf, int index, String prefix, String name, TunnelPoolSettings in, TunnelPoolSettings out) {
@@ -64,6 +66,9 @@ public class ConfigTunnelsHelper extends HelperBase {
if (in.getLength() + Math.abs(in.getLengthVariance()) >= WARN_LENGTH ||
out.getLength() + Math.abs(out.getLengthVariance()) >= WARN_LENGTH)
buf.append("
PERFORMANCE WARNING - Settings include very long tunnels ");
+ if (in.getQuantity() + in.getBackupQuantity() >= WARN_QUANTITY ||
+ out.getQuantity() + out.getBackupQuantity() >= WARN_QUANTITY)
+ buf.append("PERFORMANCE WARNING - Settings include high tunnel quantities ");
buf.append("Inbound Outbound \n");
@@ -130,15 +135,15 @@ public class ConfigTunnelsHelper extends HelperBase {
buf.append("Backup quantity \n");
buf.append("\n");
now = in.getBackupQuantity();
- renderOptions(buf, 0, MAX_QUANTITY, now, "", "tunnel");
- if (now > MAX_QUANTITY)
+ renderOptions(buf, 0, MAX_BACKUP_QUANTITY, now, "", "tunnel");
+ if (now > MAX_BACKUP_QUANTITY)
renderOptions(buf, now, now, now, "", "tunnel");
buf.append(" \n");
buf.append("\n");
now = out.getBackupQuantity();
- renderOptions(buf, 0, MAX_QUANTITY, now, "", "tunnel");
- if (now > MAX_QUANTITY)
+ renderOptions(buf, 0, MAX_BACKUP_QUANTITY, now, "", "tunnel");
+ if (now > MAX_BACKUP_QUANTITY)
renderOptions(buf, now, now, now, "", "tunnel");
buf.append(" \n");
buf.append(" \n");
diff --git a/core/java/src/net/i2p/client/I2CPMessageProducer.java b/core/java/src/net/i2p/client/I2CPMessageProducer.java
index 5b45ee7a3..b897d22d0 100644
--- a/core/java/src/net/i2p/client/I2CPMessageProducer.java
+++ b/core/java/src/net/i2p/client/I2CPMessageProducer.java
@@ -10,6 +10,7 @@ package net.i2p.client;
*/
import java.util.Date;
+import java.util.Properties;
import java.util.Set;
import net.i2p.I2PAppContext;
@@ -27,6 +28,7 @@ import net.i2p.data.i2cp.CreateLeaseSetMessage;
import net.i2p.data.i2cp.CreateSessionMessage;
import net.i2p.data.i2cp.DestroySessionMessage;
import net.i2p.data.i2cp.MessageId;
+import net.i2p.data.i2cp.ReconfigureSessionMessage;
import net.i2p.data.i2cp.ReportAbuseMessage;
import net.i2p.data.i2cp.SendMessageMessage;
import net.i2p.data.i2cp.SendMessageExpiresMessage;
@@ -188,4 +190,33 @@ class I2CPMessageProducer {
msg.setSessionId(session.getSessionId());
session.sendMessage(msg);
}
+
+ /**
+ * Update number of tunnels
+ *
+ * @param tunnels 0 for original configured number
+ */
+ public void updateTunnels(I2PSessionImpl session, int tunnels) throws I2PSessionException {
+ ReconfigureSessionMessage msg = new ReconfigureSessionMessage();
+ SessionConfig cfg = new SessionConfig(session.getMyDestination());
+ Properties props = session.getOptions();
+ if (tunnels > 0) {
+ Properties newprops = new Properties();
+ newprops.putAll(props);
+ props = newprops;
+ props.setProperty("inbound.quantity", "" + tunnels);
+ props.setProperty("outbound.quantity", "" + tunnels);
+ props.setProperty("inbound.backupQuantity", "0");
+ props.setProperty("outbound.backupQuantity", "0");
+ }
+ cfg.setOptions(props);
+ try {
+ cfg.signSessionConfig(session.getPrivateKey());
+ } catch (DataFormatException dfe) {
+ throw new I2PSessionException("Unable to sign the session config", dfe);
+ }
+ msg.setSessionConfig(cfg);
+ msg.setSessionId(session.getSessionId());
+ session.sendMessage(msg);
+ }
}
diff --git a/core/java/src/net/i2p/client/I2PSessionImpl.java b/core/java/src/net/i2p/client/I2PSessionImpl.java
index d4ff7360a..2c8582a4f 100644
--- a/core/java/src/net/i2p/client/I2PSessionImpl.java
+++ b/core/java/src/net/i2p/client/I2PSessionImpl.java
@@ -110,6 +110,9 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
*/
protected AvailabilityNotifier _availabilityNotifier;
+ private long _lastActivity;
+ private boolean _isReduced;
+
void dateUpdated() {
_dateReceived = true;
synchronized (_dateReceivedLock) {
@@ -290,6 +293,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
_log.info(getPrefix() + "Lease set created with inbound tunnels after "
+ (connected - startConnect)
+ "ms - ready to participate in the network!");
+ startIdleMonitor();
} catch (UnknownHostException uhe) {
_closed = true;
throw new I2PSessionException(getPrefix() + "Invalid session configuration", uhe);
@@ -316,6 +320,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
_log.error("Receive message " + msgId + " had no matches, remaining=" + remaining);
return null;
}
+ updateActivity();
return msg.getPayload().getUnencryptedData();
}
@@ -668,4 +673,34 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
public Destination lookupDest(Hash h) throws I2PSessionException {
return null;
}
+
+ protected void updateActivity() {
+ _lastActivity = _context.clock().now();
+ if (_isReduced) {
+ _isReduced = false;
+ try {
+ _producer.updateTunnels(this, 0);
+ } catch (I2PSessionException ise) {
+ _log.error(getPrefix() + "bork restore from reduced");
+ }
+ }
+ }
+
+ public long lastActivity() {
+ return _lastActivity;
+ }
+
+ public void setReduced() {
+ _isReduced = true;
+ }
+
+ private void startIdleMonitor() {
+ _isReduced = false;
+ boolean reduce = Boolean.valueOf(_options.getProperty("i2cp.reduceOnIdle")).booleanValue();
+ boolean close = Boolean.valueOf(_options.getProperty("i2cp.closeOnIdle")).booleanValue();
+ if (reduce || close) {
+ updateActivity();
+ SimpleScheduler.getInstance().addEvent(new SessionIdleTimer(_context, this, reduce, close), SessionIdleTimer.MINIMUM_TIME);
+ }
+ }
}
diff --git a/core/java/src/net/i2p/client/I2PSessionImpl2.java b/core/java/src/net/i2p/client/I2PSessionImpl2.java
index 6a90952a5..56ef88974 100644
--- a/core/java/src/net/i2p/client/I2PSessionImpl2.java
+++ b/core/java/src/net/i2p/client/I2PSessionImpl2.java
@@ -122,6 +122,7 @@ class I2PSessionImpl2 extends I2PSessionImpl {
throws I2PSessionException {
if (_log.shouldLog(Log.DEBUG)) _log.debug("sending message");
if (isClosed()) throw new I2PSessionException("Already closed");
+ updateActivity();
// Sadly there is no way to send something completely uncompressed in a backward-compatible way,
// so we have to still send it in a gzip format, which adds 23 bytes (2.4% for a 960-byte msg)
diff --git a/core/java/src/net/i2p/client/SessionIdleTimer.java b/core/java/src/net/i2p/client/SessionIdleTimer.java
new file mode 100644
index 000000000..1babd9551
--- /dev/null
+++ b/core/java/src/net/i2p/client/SessionIdleTimer.java
@@ -0,0 +1,106 @@
+package net.i2p.client;
+
+/*
+ * free (adj.): unencumbered; not under the control of others
+ *
+ */
+
+import java.util.Properties;
+
+import net.i2p.I2PAppContext;
+import net.i2p.data.DataHelper;
+import net.i2p.util.Log;
+import net.i2p.util.SimpleScheduler;
+import net.i2p.util.SimpleTimer;
+
+/**
+ * Reduce tunnels or shutdown the session on idle if so configured
+ *
+ * @author zzz
+ */
+public class SessionIdleTimer implements SimpleTimer.TimedEvent {
+ public static final long MINIMUM_TIME = 5*60*1000;
+ private static final long DEFAULT_REDUCE_TIME = 20*60*1000;
+ private static final long DEFAULT_CLOSE_TIME = 30*60*1000;
+ private final static Log _log = new Log(SessionIdleTimer.class);
+ private I2PAppContext _context;
+ private I2PSessionImpl _session;
+ private boolean _reduceEnabled;
+ private int _reduceQuantity;
+ private long _reduceTime;
+ private boolean _shutdownEnabled;
+ private long _shutdownTime;
+ private long _minimumTime;
+
+ /**
+ * reduce, shutdown, or both must be true
+ */
+ public SessionIdleTimer(I2PAppContext context, I2PSessionImpl session, boolean reduce, boolean shutdown) {
+ _context = context;
+ _session = session;
+ _reduceEnabled = reduce;
+ _shutdownEnabled = shutdown;
+ if (! (reduce || shutdown))
+ throw new IllegalArgumentException("At least one must be enabled");
+ Properties props = session.getOptions();
+ _minimumTime = Long.MAX_VALUE;
+ if (reduce) {
+ _reduceQuantity = 1;
+ String p = props.getProperty("i2cp.reduceQuantity");
+ if (p != null) {
+ try {
+ _reduceQuantity = Math.max(Integer.parseInt(p), 1);
+ // also check vs. configured quantities?
+ } catch (NumberFormatException nfe) {}
+ }
+ _reduceTime = DEFAULT_REDUCE_TIME;
+ p = props.getProperty("i2cp.reduceTime");
+ if (p != null) {
+ try {
+ _reduceTime = Math.max(Long.parseLong(p), MINIMUM_TIME);
+ } catch (NumberFormatException nfe) {}
+ }
+ _minimumTime = _reduceTime;
+ }
+ if (shutdown) {
+ _shutdownTime = DEFAULT_CLOSE_TIME;
+ String p = props.getProperty("i2cp.closeTime");
+ if (p != null) {
+ try {
+ _shutdownTime = Math.max(Long.parseLong(p), MINIMUM_TIME);
+ } catch (NumberFormatException nfe) {}
+ }
+ _minimumTime = Math.min(_minimumTime, _shutdownTime);
+ if (reduce && _shutdownTime <= _reduceTime)
+ reduce = false;
+ }
+ }
+
+ public void timeReached() {
+ if (_session.isClosed())
+ return;
+ long now = _context.clock().now();
+ long lastActivity = _session.lastActivity();
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Fire idle timer, last activity: " + DataHelper.formatDuration(now - lastActivity) + " ago ");
+ if (_shutdownEnabled && now - lastActivity >= _shutdownTime) {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Closing on idle " + _session);
+ _session.destroySession();
+ } else if (_reduceEnabled && now - lastActivity >= _reduceTime) {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Reducing quantity on idle " + _session);
+ try {
+ _session.getProducer().updateTunnels(_session, _reduceQuantity);
+ } catch (I2PSessionException ise) {
+ _log.error("bork idle reduction " + ise);
+ }
+ _session.setReduced();
+ if (_shutdownEnabled)
+ SimpleScheduler.getInstance().addEvent(this, _shutdownTime - (now - lastActivity));
+ // else sessionimpl must reschedule??
+ } else {
+ SimpleScheduler.getInstance().addEvent(this, _minimumTime - (now - lastActivity));
+ }
+ }
+}
diff --git a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java
index 15045028a..294059bdb 100644
--- a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java
+++ b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java
@@ -69,6 +69,8 @@ public class I2CPMessageHandler {
return new ReceiveMessageBeginMessage();
case ReceiveMessageEndMessage.MESSAGE_TYPE:
return new ReceiveMessageEndMessage();
+ case ReconfigureSessionMessage.MESSAGE_TYPE:
+ return new ReconfigureSessionMessage();
case ReportAbuseMessage.MESSAGE_TYPE:
return new ReportAbuseMessage();
case RequestLeaseSetMessage.MESSAGE_TYPE:
diff --git a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
index d36d26401..0dcc81870 100644
--- a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
+++ b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java
@@ -8,6 +8,8 @@ package net.i2p.router.client;
*
*/
+import java.util.Properties;
+
import net.i2p.data.Payload;
import net.i2p.data.i2cp.CreateLeaseSetMessage;
import net.i2p.data.i2cp.CreateSessionMessage;
@@ -27,6 +29,7 @@ import net.i2p.data.i2cp.SendMessageExpiresMessage;
import net.i2p.data.i2cp.SessionId;
import net.i2p.data.i2cp.SessionStatusMessage;
import net.i2p.data.i2cp.SetDateMessage;
+import net.i2p.router.ClientTunnelSettings;
import net.i2p.router.RouterContext;
import net.i2p.util.Log;
import net.i2p.util.RandomSource;
@@ -87,6 +90,9 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
case DestLookupMessage.MESSAGE_TYPE:
handleDestLookup(reader, (DestLookupMessage)message);
break;
+ case ReconfigureSessionMessage.MESSAGE_TYPE:
+ handleReconfigureSession(reader, (ReconfigureSessionMessage)message);
+ break;
default:
if (_log.shouldLog(Log.ERROR))
_log.error("Unhandled I2CP type received: " + message.getType());
@@ -138,24 +144,13 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
return;
}
- SessionStatusMessage msg = new SessionStatusMessage();
SessionId sessionId = new SessionId();
sessionId.setSessionId(getNextSessionId());
_runner.setSessionId(sessionId);
- msg.setSessionId(sessionId);
- msg.setStatus(SessionStatusMessage.STATUS_CREATED);
- try {
- if (_log.shouldLog(Log.DEBUG))
- _log.debug("before sending sessionStatusMessage for " + message.getSessionConfig().getDestination().calculateHash().toBase64());
- _runner.doSend(msg);
- if (_log.shouldLog(Log.DEBUG))
- _log.debug("after sending sessionStatusMessage for " + message.getSessionConfig().getDestination().calculateHash().toBase64());
- _runner.sessionEstablished(message.getSessionConfig());
- if (_log.shouldLog(Log.DEBUG))
- _log.debug("after sessionEstablished for " + message.getSessionConfig().getDestination().calculateHash().toBase64());
- } catch (I2CPMessageException ime) {
- _log.error("Error writing out the session status message", ime);
- }
+ sendStatusMessage(SessionStatusMessage.STATUS_CREATED);
+ _runner.sessionEstablished(message.getSessionConfig());
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("after sessionEstablished for " + message.getSessionConfig().getDestination().calculateHash().toBase64());
_context.jobQueue().addJob(new CreateSessionJob(_context, _runner));
}
@@ -249,10 +244,36 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi
*/
private void handleReconfigureSession(I2CPMessageReader reader, ReconfigureSessionMessage message) {
if (_log.shouldLog(Log.INFO))
- _log.info("Updating options - session " + _runner.getSessionId());
+ _log.info("Updating options - old: " + _runner.getConfig() + " new: " + message.getSessionConfig());
+ if (!message.getSessionConfig().getDestination().equals(_runner.getConfig().getDestination())) {
+ _log.error("Dest mismatch");
+ sendStatusMessage(SessionStatusMessage.STATUS_INVALID);
+ _runner.stopRunning();
+ return;
+ }
_runner.getConfig().getOptions().putAll(message.getSessionConfig().getOptions());
+ ClientTunnelSettings settings = new ClientTunnelSettings();
+ Properties props = new Properties();
+ props.putAll(_runner.getConfig().getOptions());
+ settings.readFromProperties(props);
+ _context.tunnelManager().setInboundSettings(_runner.getConfig().getDestination().calculateHash(),
+ settings.getInboundSettings());
+ _context.tunnelManager().setOutboundSettings(_runner.getConfig().getDestination().calculateHash(),
+ settings.getOutboundSettings());
+ sendStatusMessage(SessionStatusMessage.STATUS_UPDATED);
}
+ private void sendStatusMessage(int status) {
+ SessionStatusMessage msg = new SessionStatusMessage();
+ msg.setSessionId(_runner.getSessionId());
+ msg.setStatus(status);
+ try {
+ _runner.doSend(msg);
+ } catch (I2CPMessageException ime) {
+ _log.error("Error writing out the session status message", ime);
+ }
+ }
+
// this *should* be mod 65536, but UnsignedInteger is still b0rked. FIXME
private final static int MAX_SESSION_ID = 32767;