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 627024b67167e98ce1151ac41c89b9258aa02b40..e8bda1a293e684f0f302d5be6770bf699425a5e4 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 f7ee2294c73726ad36296251b225c7c1061fb89e..f5e1fac7f82d6e8451d816c8c6ce699e01a97d1d 100644 --- a/apps/i2ptunnel/jsp/editClient.jsp +++ b/apps/i2ptunnel/jsp/editClient.jsp @@ -289,9 +289,9 @@ </div> <div id="portField" class="rowItem"> <label for="reduceTime" accesskey="c"> - Reduce when idle (minutes): + Close when idle (minutes): </label> - <input type="text" id="port" name="reduceTime" size="4" maxlength="4" title="Reduced Tunnel Idle Time" value="<%=editBean.getReduceTime(curTunnel)%>" class="freetext" /> + <input type="text" id="port" name="closeTime" size="4" maxlength="4" title="Reduced Tunnel Idle Time" value="<%=editBean.getCloseTime(curTunnel)%>" class="freetext" /> </div> <div class="subdivider"> 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 e21f9d9ce77305168ae8690bae51cfc370dc1cd4..8b8b2fb16a421325d0a953bae1b85ae059c2c7ad 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("<tr><td colspan=\"3\"><font color=\"red\">PERFORMANCE WARNING - Settings include very long tunnels</font></td></tr>"); + if (in.getQuantity() + in.getBackupQuantity() >= WARN_QUANTITY || + out.getQuantity() + out.getBackupQuantity() >= WARN_QUANTITY) + buf.append("<tr><td colspan=\"3\"><font color=\"red\">PERFORMANCE WARNING - Settings include high tunnel quantities</font></td></tr>"); buf.append("<tr><td></td><td><b>Inbound</b></td><td><b>Outbound</b></td></tr>\n"); @@ -130,15 +135,15 @@ public class ConfigTunnelsHelper extends HelperBase { buf.append("<tr><td>Backup quantity</td>\n"); buf.append("<td><select name=\"").append(index).append(".backupInbound\">\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("</select></td>\n"); buf.append("<td><select name=\"").append(index).append(".backupOutbound\">\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("</select></td>\n"); buf.append("</tr>\n"); diff --git a/core/java/src/net/i2p/client/I2CPMessageProducer.java b/core/java/src/net/i2p/client/I2CPMessageProducer.java index 5b45ee7a362cda72097d00fac8e514f13096b37d..b897d22d0ee2230577e80c6972f52ed77e97efba 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 d4ff7360a73e48ac8b91607f0c790dd115ac18b9..2c8582a4f8272106a9dbf1294ba4d1619050d55e 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 6a90952a52f2bcf1ec5312347f36c768e08da083..56ef88974b88cd3d7201d68cf38131cc8bbaccdd 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 0000000000000000000000000000000000000000..1babd9551c5a179eac030b8dabedd4e836b608e3 --- /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 15045028a88eb9b8ba4f0424ab35fdacbbaa4c7d..294059bdbedab7c164f18b79c02f105900e02fa2 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 d36d26401a496ca09fc3eb96098da7ad7c448253..0dcc8187091d85010525d1070547637285cd9dbd 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;