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;