diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHandler.java
index 82c175e922c3ac896cdfa1c2ee8b2136d2ca7f92..4d2b73bc326954d79bfd82bfb839660338aea1cd 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHandler.java
@@ -271,11 +271,12 @@ public class ConfigNetHandler extends FormHandler {
         if (switchRequired) {
             hiddenSwitch();
         } else if (restartRequired) {
-            if (_context.hasWrapper()) {
+            //if (_context.hasWrapper()) {
                 // Wow this dumps all conns immediately and really isn't nice
                 addFormNotice("Performing a soft restart");
                 _context.router().restart();
-                addFormNotice("Soft restart complete");
+                // restart() returns immediately now
+                //addFormNotice("Soft restart complete");
 
                 // Most of the time we aren't changing addresses, just enabling or disabling
                 // things, so let's try just a new routerInfo and see how that works.
@@ -285,14 +286,12 @@ public class ConfigNetHandler extends FormHandler {
                 // So don't do this...
                 //_context.router().rebuildRouterInfo();
                 //addFormNotice("Router Info rebuilt");
-            } else {
+            //} else {
                 // There's a few changes that don't really require restart (e.g. enabling inbound TCP)
                 // But it would be hard to get right, so just do a restart.
-                addFormError(_("Gracefully restarting I2P to change published router address"));
-                if (_context.hasWrapper())
-                    _context.addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_GRACEFUL_RESTART));
-                _context.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART);
-            }
+                //addFormError(_("Gracefully restarting I2P to change published router address"));
+                //_context.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART);
+            //}
         }
     }
 
diff --git a/core/java/src/net/i2p/time/Timestamper.java b/core/java/src/net/i2p/time/Timestamper.java
index 39908ca5782fc4d7f70e86eb02057b0aad2774cf..0167c102d89ea52c40389d9e08826e1ad313a3d0 100644
--- a/core/java/src/net/i2p/time/Timestamper.java
+++ b/core/java/src/net/i2p/time/Timestamper.java
@@ -17,7 +17,7 @@ import net.i2p.util.Log;
  * forever.
  */
 public class Timestamper implements Runnable {
-    private I2PAppContext _context;
+    private final I2PAppContext _context;
     private Log _log;
     private final List<String> _servers;
     private List<String> _priorityServers;
@@ -26,7 +26,7 @@ public class Timestamper implements Runnable {
     private int _concurringServers;
     private int _consecutiveFails;
     private volatile boolean _disabled;
-    private boolean _daemon;
+    private final boolean _daemon;
     private boolean _initialized;
     private boolean _wellSynced;
     private volatile boolean _isRunning;
@@ -60,6 +60,10 @@ public class Timestamper implements Runnable {
         // moved here to prevent problems with synchronized statements.
         _servers = new ArrayList(3);
         _listeners = new CopyOnWriteArrayList();
+        _context = ctx;
+        _daemon = daemon;
+        // DO NOT initialize _log here, stack overflow via LogManager init loop
+
         // Don't bother starting a thread if we are disabled.
         // This means we no longer check every 5 minutes to see if we got enabled,
         // so the property must be set at startup.
@@ -69,10 +73,6 @@ public class Timestamper implements Runnable {
             _initialized = true;
             return;
         }
-        _context = ctx;
-        _daemon = daemon;
-        _initialized = false;
-        _wellSynced = false;
         if (lsnr != null)
             _listeners.add(lsnr);
         updateConfig();
@@ -124,6 +124,15 @@ public class Timestamper implements Runnable {
         } catch (InterruptedException ie) {}
     }
     
+    /**
+     *  Update the time immediately.
+     *  @since 0.8.8
+     */
+    public void timestampNow() {
+         if (_initialized && _isRunning && (!_disabled) && _timestamperThread != null)
+             _timestamperThread.interrupt();
+    }
+    
     /** @since 0.8.8 */
     private class Shutdown implements Runnable {
         public void run() {
diff --git a/core/java/src/net/i2p/util/Clock.java b/core/java/src/net/i2p/util/Clock.java
index 26d8ff0e869f55ffc7c30fe46a5228b467f72ef0..767809349bb52ef7883b55e1e6fe69d8154a9b9d 100644
--- a/core/java/src/net/i2p/util/Clock.java
+++ b/core/java/src/net/i2p/util/Clock.java
@@ -1,6 +1,5 @@
 package net.i2p.util;
 
-import java.util.Iterator;
 import java.util.Set;
 import java.util.concurrent.CopyOnWriteArraySet;
 
@@ -21,11 +20,11 @@ import net.i2p.time.Timestamper;
 public class Clock implements Timestamper.UpdateListener {
     protected final I2PAppContext _context;
     private final Timestamper _timestamper;
-    protected final long _startedOn;
+    protected long _startedOn;
     protected boolean _statCreated;
     protected volatile long _offset;
     protected boolean _alreadyChanged;
-    private final Set _listeners;
+    private final Set<ClockUpdateListener> _listeners;
     
     public Clock(I2PAppContext context) {
         _context = context;
@@ -33,6 +32,7 @@ public class Clock implements Timestamper.UpdateListener {
         _timestamper = new Timestamper(context, this);
         _startedOn = System.currentTimeMillis();
     }
+
     public static Clock getInstance() {
         return I2PAppContext.getGlobalContext().clock();
     }
@@ -151,13 +151,17 @@ public class Clock implements Timestamper.UpdateListener {
     }
 
     protected void fireOffsetChanged(long delta) {
-            for (Iterator iter = _listeners.iterator(); iter.hasNext();) {
-                ClockUpdateListener lsnr = (ClockUpdateListener) iter.next();
+            for (ClockUpdateListener lsnr : _listeners) {
                 lsnr.offsetChanged(delta);
             }
     }
 
-    public static interface ClockUpdateListener {
+    public interface ClockUpdateListener {
+
+        /**
+         *  @param delta = (new offset - old offset),
+         *         where each offset = (now() - System.currentTimeMillis())
+         */
         public void offsetChanged(long delta);
     }
 }
diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java
index 74a098aa2da3ae5ce6e770b23e71db5f9dbc723a..194cd1b3db64cdd9376b1e2f0217f57fb357fd8f 100644
--- a/router/java/src/net/i2p/router/Router.java
+++ b/router/java/src/net/i2p/router/Router.java
@@ -58,7 +58,7 @@ import net.i2p.util.SimpleTimer;
  * Main driver for the router.
  *
  */
-public class Router {
+public class Router implements RouterClock.ClockShiftListener {
     private Log _log;
     private RouterContext _context;
     private final Map<String, String> _config;
@@ -70,7 +70,7 @@ public class Router {
     private boolean _higherVersionSeen;
     //private SessionKeyPersistenceHelper _sessionKeyPersistenceHelper;
     private boolean _killVMOnEnd;
-    private boolean _isAlive;
+    private volatile boolean _isAlive;
     private int _gracefulExitCode;
     private I2PThread.OOMEventListener _oomListener;
     private ShutdownHook _shutdownHook;
@@ -351,9 +351,15 @@ public class Router {
     /**
      * True if the router has tried to communicate with another router who is running a higher
      * incompatible protocol version.  
-     *
+     * @deprecated unused
      */
     public boolean getHigherVersionSeen() { return _higherVersionSeen; }
+
+    /**
+     * True if the router has tried to communicate with another router who is running a higher
+     * incompatible protocol version.  
+     * @deprecated unused
+     */
     public void setHigherVersionSeen(boolean seen) { _higherVersionSeen = seen; }
     
     public long getWhenStarted() { return _started; }
@@ -361,7 +367,7 @@ public class Router {
     /** wall clock uptime */
     public long getUptime() { 
         if ( (_context == null) || (_context.clock() == null) ) return 1; // racing on startup
-        return _context.clock().now() - _context.clock().getOffset() - _started;
+        return Math.max(1, _context.clock().now() - _context.clock().getOffset() - _started);
     }
     
     public RouterContext getContext() { return _context; }
@@ -961,7 +967,11 @@ public class Router {
     public static final int EXIT_HARD_RESTART = 4;
     public static final int EXIT_GRACEFUL_RESTART = 5;
     
+    /**
+     *  Shutdown with no chance of cancellation
+     */
     public void shutdown(int exitCode) {
+        ((RouterClock) _context.clock()).removeShiftListener(this);
         _isAlive = false;
         _context.random().saveSeed();
         I2PThread.removeOOMEventListener(_oomListener);
@@ -1199,33 +1209,77 @@ public class Router {
         return true;
     }
     
+    /**
+     *  The clock shift listener.
+     *  Restart the router if we should.
+     *
+     *  @since 0.8.8
+     */
+    public void clockShift(long delta) {
+        if (gracefulShutdownInProgress() || !_isAlive)
+            return;
+        if (delta > -60*1000 && delta < 60*1000)
+            return;
+        if (_context.commSystem().countActivePeers() <= 0)
+            return;
+        if (delta > 0)
+            _log.error("Restarting after large clock shift forward by " + DataHelper.formatDuration(delta));
+        else
+            _log.error("Restarting after large clock shift backward by " + DataHelper.formatDuration(0 - delta));
+        restart();
+    }
+
     /**
      *  A "soft" restart, primarily of the comm system, after
      *  a port change or large step-change in system time.
      *  Does not stop the whole JVM, so it is safe even in the absence
      *  of the wrapper.
-     *  This is not a graceful restart - all peer connections are dropped.
+     *  This is not a graceful restart - all peer connections are dropped immediately.
+     *
+     *  As of 0.8.8, this returns immediately and does the actual restart in a separate thread.
+     *  Poll isAlive() if you need to know when the restart is complete.
      */
-    public void restart() {
+    public synchronized void restart() {
+        if (gracefulShutdownInProgress() || !_isAlive)
+            return;
+        ((RouterClock) _context.clock()).removeShiftListener(this);
         _isAlive = false;
+        Thread t = new Thread(new Restarter(), "Router Restart");
+        t.start();
+    }    
+
+    /**
+     *  @since 0.8.8
+     */
+    private class Restarter implements Runnable {
+        public void run() {
+            _started = _context.clock().now();
+            _log.error("Stopping the router for a restart...");
+            _log.logAlways(Log.WARN, "Stopping the client manager");
+            try { _context.clientManager().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error stopping the client manager", t); }
+            _log.logAlways(Log.WARN, "Stopping the comm system");
+            try { _context.commSystem().restart(); } catch (Throwable t) { _log.log(Log.CRIT, "Error restarting the comm system", t); }
+            _log.logAlways(Log.WARN, "Stopping the tunnel manager");
+            try { _context.tunnelManager().restart(); } catch (Throwable t) { _log.log(Log.CRIT, "Error restarting the tunnel manager", t); }
+
+            //try { _context.peerManager().restart(); } catch (Throwable t) { _log.log(Log.CRIT, "Error restarting the peer manager", t); }
+            //try { _context.netDb().restart(); } catch (Throwable t) { _log.log(Log.CRIT, "Error restarting the networkDb", t); }
+            //try { _context.jobQueue().restart(); } catch (Throwable t) { _log.log(Log.CRIT, "Error restarting the job queue", t); }
         
-        try { _context.commSystem().restart(); } catch (Throwable t) { _log.log(Log.CRIT, "Error restarting the comm system", t); }
-        try { _context.clientManager().restart(); } catch (Throwable t) { _log.log(Log.CRIT, "Error restarting the client manager", t); }
-        try { _context.tunnelManager().restart(); } catch (Throwable t) { _log.log(Log.CRIT, "Error restarting the tunnel manager", t); }
-        try { _context.peerManager().restart(); } catch (Throwable t) { _log.log(Log.CRIT, "Error restarting the peer manager", t); }
-        try { _context.netDb().restart(); } catch (Throwable t) { _log.log(Log.CRIT, "Error restarting the networkDb", t); }
-        
-        //try { _context.jobQueue().restart(); } catch (Throwable t) { _log.log(Log.CRIT, "Error restarting the job queue", t); }
-        
-        _log.log(Log.CRIT, "Restart teardown complete... ");
-        try { Thread.sleep(10*1000); } catch (InterruptedException ie) {}
+            _log.logAlways(Log.WARN, "Router teardown complete, restarting the router...");
+            try { Thread.sleep(10*1000); } catch (InterruptedException ie) {}
         
-        _log.log(Log.CRIT, "Restarting...");
+            _log.logAlways(Log.WARN, "Restarting the comm system");
+            _log.logAlways(Log.WARN, "Restarting the tunnel manager");
+            _log.logAlways(Log.WARN, "Restarting the client manager");
+            try { _context.clientManager().startup(); } catch (Throwable t) { _log.log(Log.CRIT, "Error stopping the client manager", t); }
         
-        _isAlive = true;
-        _started = _context.clock().now();
+            _isAlive = true;
+            rebuildRouterInfo();
         
-        _log.log(Log.CRIT, "Restart complete");
+            _log.logAlways(Log.WARN, "Restart complete");
+            ((RouterClock) _context.clock()).addShiftListener(Router.this);
+        }
     }
     
     public static void main(String args[]) {
diff --git a/router/java/src/net/i2p/router/RouterClock.java b/router/java/src/net/i2p/router/RouterClock.java
index 4fdcd5933b3dbdf394bdad98a65526501bc21eb0..4a5164f1797c13b78065af1fc3d82aaca379c3c0 100644
--- a/router/java/src/net/i2p/router/RouterClock.java
+++ b/router/java/src/net/i2p/router/RouterClock.java
@@ -1,5 +1,9 @@
 package net.i2p.router;
 
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+import net.i2p.data.DataHelper;
 import net.i2p.util.Clock;
 import net.i2p.util.Log;
 
@@ -26,6 +30,7 @@ public class RouterClock extends Clock {
     private static final long MAX_SLEW = 50;
     public static final int DEFAULT_STRATUM = 8;
     private static final int WORST_STRATUM = 16;
+
     /** the max NTP Timestamper delay is 30m right now, make this longer than that */
     private static final long MIN_DELAY_FOR_WORSE_STRATUM = 45*60*1000;
     private volatile long _desiredOffset;
@@ -34,12 +39,21 @@ public class RouterClock extends Clock {
     private long _lastChanged;
     private int _lastStratum;
 
-    private final RouterContext _contextRC;
+    /**
+     *  If the system clock shifts by this much (positive or negative),
+     *  call the callback, we probably need a soft restart.
+     *  @since 0.8.8
+     */
+    private static final long MASSIVE_SHIFT = 75*1000;
+    private final Set<ClockShiftListener> _shiftListeners;
+    private volatile long _lastShiftNanos;
 
     public RouterClock(RouterContext context) {
         super(context);
-        _contextRC = context;
         _lastStratum = WORST_STRATUM;
+        _lastSlewed = System.currentTimeMillis();
+        _shiftListeners = new CopyOnWriteArraySet();
+        _lastShiftNanos = System.nanoTime();
     }
 
     /**
@@ -98,11 +112,11 @@ public class RouterClock extends Clock {
             }
             
             // If so configured, check sanity of proposed clock offset
-            if (_contextRC.getBooleanPropertyDefaultTrue("router.clockOffsetSanityCheck") &&
+            if (_context.getBooleanPropertyDefaultTrue("router.clockOffsetSanityCheck") &&
                 _alreadyChanged) {
 
                 // Try calculating peer clock skew
-                long currentPeerClockSkew = _contextRC.commSystem().getFramedAveragePeerClockSkew(50);
+                long currentPeerClockSkew = ((RouterContext)_context).commSystem().getFramedAveragePeerClockSkew(50);
 
                     // Predict the effect of applying the proposed clock offset
                     long predictedPeerClockSkew = currentPeerClockSkew + delta;
@@ -131,10 +145,10 @@ public class RouterClock extends Clock {
                 getLog().info("Updating target clock offset to " + offsetMs + "ms from " + _offset + "ms, Stratum " + stratum);
             
             if (!_statCreated) {
-                _contextRC.statManager().createRequiredRateStat("clock.skew", "Clock step adjustment (ms)", "Clock", new long[] { 10*60*1000, 3*60*60*1000, 24*60*60*60 });
+                _context.statManager().createRequiredRateStat("clock.skew", "Clock step adjustment (ms)", "Clock", new long[] { 10*60*1000, 3*60*60*1000, 24*60*60*60 });
                 _statCreated = true;
             }
-            _contextRC.statManager().addRateData("clock.skew", delta, 0);
+            _context.statManager().addRateData("clock.skew", delta, 0);
             _desiredOffset = offsetMs;
         } else {
             getLog().log(Log.INFO, "Initializing clock offset to " + offsetMs + "ms, Stratum " + stratum);
@@ -177,7 +191,12 @@ public class RouterClock extends Clock {
         long systemNow = System.currentTimeMillis();
         // copy the global, so two threads don't both increment or decrement _offset
         long offset = _offset;
-        if (systemNow >= _lastSlewed + MAX_SLEW) {
+        long sinceLastSlewed = systemNow - _lastSlewed;
+        if (sinceLastSlewed >= MASSIVE_SHIFT ||
+            sinceLastSlewed <= 0 - MASSIVE_SHIFT) {
+            _lastSlewed = systemNow;
+            notifyMassive(sinceLastSlewed);
+        } else if (sinceLastSlewed >= MAX_SLEW) {
             // copy the global
             long desiredOffset = _desiredOffset;
             if (desiredOffset > offset) {
@@ -196,6 +215,66 @@ public class RouterClock extends Clock {
         return offset + systemNow;
     }
 
+    /*
+     *  A large system clock shift happened. Tell people about it.
+     *
+     *  @since 0.8.8
+     */
+    private void notifyMassive(long shift) {
+        long nowNanos = System.nanoTime();
+        // try to prevent dups, not guaranteed
+        // nanoTime() isn't guaranteed to be monotonic either :(
+        if (nowNanos < _lastShiftNanos + MASSIVE_SHIFT)
+            return;
+        _lastShiftNanos = nowNanos;
+
+        // reset these so the offset can be reset by the timestamper again
+        _startedOn = System.currentTimeMillis();
+        _alreadyChanged = false;
+        getTimestamper().timestampNow();
+
+        if (shift > 0)
+            getLog().log(Log.CRIT, "Large clock shift forward by " + DataHelper.formatDuration(shift));
+        else
+            getLog().log(Log.CRIT, "Large clock shift backward by " + DataHelper.formatDuration(0 - shift));
+
+        for (ClockShiftListener lsnr : _shiftListeners) {
+            lsnr.clockShift(shift);
+        }
+    }
+
+    /*
+     *  Get notified of massive System clock shifts, positive or negative -
+     *  generally a minute or more.
+     *  The adjusted (offset) clock changes by the same amount.
+     *  The offset itself did not change.
+     *  Warning - duplicate notifications may occur.
+     *
+     *  @since 0.8.8
+     */
+    public void addShiftListener(ClockShiftListener lsnr) {
+            _shiftListeners.add(lsnr);
+    }
+
+    /*
+     *  @since 0.8.8
+     */
+    public void removeShiftListener(ClockShiftListener lsnr) {
+            _shiftListeners.remove(lsnr);
+    }
+
+    /*
+     *  @since 0.8.8
+     */
+    public interface ClockShiftListener {
+
+        /**
+         *  @param delta The system clock and adjusted clock just changed by this much,
+         *               in milliseconds (approximately)
+         */
+        public void clockShift(long delta);
+    }
+
     /*
      *  How far we still have to slew, for diagnostics
      *  @since 0.7.12
@@ -204,5 +283,4 @@ public class RouterClock extends Clock {
     public long getDeltaOffset() {
         return _desiredOffset - _offset;
     }
-    
 }
diff --git a/router/java/src/net/i2p/router/client/ClientManager.java b/router/java/src/net/i2p/router/client/ClientManager.java
index 6f4d4414f5c2644e31afeb7fda5c3ae5cf590885..ef061e771b40141424b97401cd958d60ef965346 100644
--- a/router/java/src/net/i2p/router/client/ClientManager.java
+++ b/router/java/src/net/i2p/router/client/ClientManager.java
@@ -91,15 +91,8 @@ class ClientManager {
         // to let the old listener die
         try { Thread.sleep(2*1000); } catch (InterruptedException ie) {}
         
-        int port = ClientManagerFacadeImpl.DEFAULT_PORT;
-        String portStr = _ctx.router().getConfigSetting(ClientManagerFacadeImpl.PROP_CLIENT_PORT);
-        if (portStr != null) {
-            try {
-                port = Integer.parseInt(portStr);
-            } catch (NumberFormatException nfe) {
-                _log.error("Error setting the port: " + portStr + " is not valid", nfe);
-            }
-        } 
+        int port = _ctx.getProperty(ClientManagerFacadeImpl.PROP_CLIENT_PORT,
+                                    ClientManagerFacadeImpl.DEFAULT_PORT);
         startListeners(port);
     }
     
diff --git a/router/java/src/net/i2p/router/startup/BootCommSystemJob.java b/router/java/src/net/i2p/router/startup/BootCommSystemJob.java
index 6e3e31efd12b5d800cc1edb251e6e9153345dbef..400d84efdd556325b481994e1b8fe084c9b1594e 100644
--- a/router/java/src/net/i2p/router/startup/BootCommSystemJob.java
+++ b/router/java/src/net/i2p/router/startup/BootCommSystemJob.java
@@ -11,6 +11,7 @@ package net.i2p.router.startup;
 import net.i2p.router.Job;
 import net.i2p.router.JobImpl;
 import net.i2p.router.RouterContext;
+import net.i2p.router.RouterClock;
 import net.i2p.util.Log;
 
 /** This actually boots almost everything */
@@ -45,6 +46,7 @@ public class BootCommSystemJob extends JobImpl {
         getContext().jobQueue().addJob(new StartAcceptingClientsJob(getContext()));
 
         getContext().jobQueue().addJob(new ReadConfigJob(getContext()));
+        ((RouterClock) getContext().clock()).addShiftListener(getContext().router());
     }
         
     private void startupDb() {
diff --git a/router/java/src/net/i2p/router/transport/UPnP.java b/router/java/src/net/i2p/router/transport/UPnP.java
index fe3ad037766eae862544fb9da5c88a31a73367a1..862f12cbb706a7b3f83475b9e55974f8d318df83 100644
--- a/router/java/src/net/i2p/router/transport/UPnP.java
+++ b/router/java/src/net/i2p/router/transport/UPnP.java
@@ -91,8 +91,20 @@ class UPnP extends ControlPoint implements DeviceChangeListener, EventListener {
 		return super.start();
 	}
 
+	/**
+	 *  WARNING - Blocking up to 2 seconds
+	 */
 	public void terminate() {
+		// this gets spun off in a thread...
 		unregisterPortMappings();
+		// If we stop too early and we've forwarded multiple ports,
+		// the later ones don't get unregistered
+		int i = 0;
+		while (i++ < 20 && !portsForwarded.isEmpty()) {
+			try {
+				Thread.sleep(100);
+			} catch (InterruptedException ie) {}
+		}
 		super.stop();
 		_router = null;
 		_service = null;
@@ -672,7 +684,7 @@ class UPnP extends ControlPoint implements DeviceChangeListener, EventListener {
          */
 	private void unregisterPorts(Set<ForwardPort> portsToForwardNow) {
 	        Thread t = new Thread(new UnregisterPortsThread(portsToForwardNow));
-		t.setName("UPnP Port Opener " + (++__id));
+		t.setName("UPnP Port Closer " + (++__id));
 		t.setDaemon(true);
 		t.start();
 	}