2004-09-06 jrandom

* Address a race condition in the key management code that would manifest
      itself as a corrupt router identity.
    * Properly clear old transport addresses from being displayed on the old
      console after soft restarts.
    * Properly refuse to load the client applications more than once in the
      same JVM.
    * Added support for a graceful restart (a graceful shutdown followed by a
      full JVM restart - useful for restarting client apps).
    * More defensive programming, HTML cleanup, logging
    * wrapper.config cleanup of duplicate lines
This commit is contained in:
jrandom
2004-09-06 05:20:40 +00:00
committed by zzz
parent db339d40de
commit 0eedc1b128
13 changed files with 113 additions and 49 deletions

View File

@ -190,8 +190,8 @@ public class I2PTunnelRunner extends I2PThread implements I2PSocket.SocketErrorL
} catch (IOException ex) { } catch (IOException ex) {
if (!finished) if (!finished)
_log.error("Error forwarding", ex); _log.error("Error forwarding", ex);
else //else
_log.warn("You may ignore this", ex); // _log.warn("You may ignore this", ex);
} finally { } finally {
try { try {
out.close(); out.close();

View File

@ -37,6 +37,9 @@ public class ConfigServiceHandler extends FormHandler {
} else if ("Cancel graceful shutdown".equals(_action)) { } else if ("Cancel graceful shutdown".equals(_action)) {
_context.router().cancelGracefulShutdown(); _context.router().cancelGracefulShutdown();
addFormNotice("Graceful shutdown cancelled"); addFormNotice("Graceful shutdown cancelled");
} else if ("Graceful restart".equals(_action)) {
_context.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART);
addFormNotice("Graceful restart requested");
} else if ("Hard restart".equals(_action)) { } else if ("Hard restart".equals(_action)) {
_context.router().shutdown(Router.EXIT_HARD_RESTART); _context.router().shutdown(Router.EXIT_HARD_RESTART);
addFormNotice("Hard restart requested"); addFormNotice("Hard restart requested");

View File

@ -25,36 +25,47 @@
System.setProperty("net.i2p.router.web.ConfigServiceHandler.nonce", new java.util.Random().nextLong()+""); %> System.setProperty("net.i2p.router.web.ConfigServiceHandler.nonce", new java.util.Random().nextLong()+""); %>
<input type="hidden" name="nonce" value="<%=System.getProperty("net.i2p.router.web.ConfigServiceHandler.nonce")%>" /> <input type="hidden" name="nonce" value="<%=System.getProperty("net.i2p.router.web.ConfigServiceHandler.nonce")%>" />
<h4>Shutdown the router</h4> <h4>Shutdown the router</h4>
Graceful shutdown lets the router satisfy the agreements it has already made <p>Graceful shutdown lets the router satisfy the agreements it has already made
before shutting down, but may take a few minutes. If you need to kill the before shutting down, but may take a few minutes. If you need to kill the
router immediately, that option is available as well.<br /> router immediately, that option is available as well.</p>
<input type="submit" name="action" value="Shutdown gracefully" /> <input type="submit" name="action" value="Shutdown gracefully" />
<input type="submit" name="action" value="Shutdown immediately" /> <input type="submit" name="action" value="Shutdown immediately" />
<input type="submit" name="action" value="Cancel graceful shutdown" /> <input type="submit" name="action" value="Cancel graceful shutdown" />
<p>If you want the router to restart itself after shutting down, you can choose one of
the following. This is useful in some situations - for example, if you changed
some settings that client applications only read at startup, such as the routerconsole password
or the interface it listens on. A graceful restart will take a few minutes (but your peers
will appreciate your patience), while a hard restart does so immediately. After tearing down
the router, it will wait 1 minute before starting back up again.</p>
<input type="submit" name="action" value="Graceful restart" />
<input type="submit" name="action" value="Hard restart" /> <input type="submit" name="action" value="Hard restart" />
<% if ( (System.getProperty("os.name") != null) && (System.getProperty("os.name").startsWith("Win")) ) { %> <% if ( (System.getProperty("os.name") != null) && (System.getProperty("os.name").startsWith("Win")) ) { %>
<h4>Systray integration</h4> <h4>Systray integration</h4>
On the windows platform, there is a small application to sit in the system <p>On the windows platform, there is a small application to sit in the system
tray, allowing you to view the router's status (later on, I2P client applications tray, allowing you to view the router's status (later on, I2P client applications
will be able to integrate their own functionality into the system tray as well). will be able to integrate their own functionality into the system tray as well).
If you are on windows, you can either enable or disable that icon here. <br /> If you are on windows, you can either enable or disable that icon here.</p>
<input type="submit" name="action" value="Show systray icon" /> <input type="submit" name="action" value="Show systray icon" />
<input type="submit" name="action" value="Hide systray icon" /> <input type="submit" name="action" value="Hide systray icon" />
<h4>Run on startup</h4> <h4>Run on startup</h4>
You can control whether I2P is run on startup or not by selecting one of the <p>You can control whether I2P is run on startup or not by selecting one of the
following options - I2P will install (or remove) a service accordingly. You can following options - I2P will install (or remove) a service accordingly. You can
also run the <code>install_i2p_service_winnt.bat</code> (or also run the <code>install_i2p_service_winnt.bat</code> (or
<code>uninstall_i2p_service_winnt.bat</code>) from the command line, if you prefer.<br /> <code>uninstall_i2p_service_winnt.bat</code>) from the command line, if you prefer.</p>
<input type="submit" name="action" value="Run I2P on startup" /> <input type="submit" name="action" value="Run I2P on startup" />
<input type="submit" name="action" value="Don't run I2P on startup" /><br /> <input type="submit" name="action" value="Don't run I2P on startup" /><br />
<b>Note:</b> If you are running I2P as service right now, removing it will shut <p><b>Note:</b> If you are running I2P as service right now, removing it will shut
down your router immediately. You may want to consider shutting down gracefully, as down your router immediately. You may want to consider shutting down gracefully, as
above, then running uninstall_i2p_service_winnt.bat. above, then running uninstall_i2p_service_winnt.bat.</p>
<% } %> <% } %>
<h4>Debugging</h4> <h4>Debugging</h4>
At times, it may be helpful to debug I2P by getting a thread dump. To do so, <p>At times, it may be helpful to debug I2P by getting a thread dump. To do so,
please select the following option and review the thread dumped to please select the following option and review the thread dumped to
<a href="logs.jsp#servicelogs">wrapper.log</a>.<br /> <a href="logs.jsp#servicelogs">wrapper.log</a>.</p>
<input type="submit" name="action" value="Dump threads" /> <input type="submit" name="action" value="Dump threads" />
</form> </form>
</div> </div>

View File

@ -221,6 +221,7 @@ public class RouterInfo extends DataStructureImpl {
if (bytes == null) throw new DataFormatException("Not enough data to sign"); if (bytes == null) throw new DataFormatException("Not enough data to sign");
// now sign with the key // now sign with the key
Signature sig = DSAEngine.getInstance().sign(bytes, key); Signature sig = DSAEngine.getInstance().sign(bytes, key);
if (sig == null) throw new DataFormatException("Not enough data to sign, or other signature failure");
setSignature(sig); setSignature(sig);
//_log.debug("Signed " + SHA256Generator.getInstance().calculateHash(bytes).toBase64() + " with " + key); //_log.debug("Signed " + SHA256Generator.getInstance().calculateHash(bytes).toBase64() + " with " + key);
//_log.debug("verify ok? " + DSAEngine.getInstance().verifySignature(sig, bytes, getIdentity().getSigningPublicKey())); //_log.debug("verify ok? " + DSAEngine.getInstance().verifySignature(sig, bytes, getIdentity().getSigningPublicKey()));

View File

@ -1,4 +1,16 @@
$Id: history.txt,v 1.6 2004/09/04 00:41:42 jrandom Exp $ $Id: history.txt,v 1.7 2004/09/04 16:54:09 jrandom Exp $
2004-09-06 jrandom
* Address a race condition in the key management code that would manifest
itself as a corrupt router identity.
* Properly clear old transport addresses from being displayed on the old
console after soft restarts.
* Properly refuse to load the client applications more than once in the
same JVM.
* Added support for a graceful restart (a graceful shutdown followed by a
full JVM restart - useful for restarting client apps).
* More defensive programming, HTML cleanup, logging
* wrapper.config cleanup of duplicate lines
2004-09-04 jrandom 2004-09-04 jrandom
* Added some basic guards to prevent multiple instances from running. * Added some basic guards to prevent multiple instances from running.

View File

@ -96,10 +96,7 @@ wrapper.on_exit.4=RESTART
# the router may take a few seconds to save state, etc # the router may take a few seconds to save state, etc
wrapper.jvm_exit.timeout=60 wrapper.jvm_exit.timeout=60
# the router may take a few seconds to save state, etc # give the OS 60s to clear all the old sockets / etc before restarting
wrapper.jvm_exit.timeout=30
# give the OS 30s to clear all the old sockets / etc before restarting
wrapper.restart.delay=60 wrapper.restart.delay=60
# use the wrapper's internal timer thread. otherwise this would # use the wrapper's internal timer thread. otherwise this would

View File

@ -24,6 +24,7 @@ import net.i2p.data.PrivateKey;
import net.i2p.data.PublicKey; import net.i2p.data.PublicKey;
import net.i2p.data.SigningPrivateKey; import net.i2p.data.SigningPrivateKey;
import net.i2p.data.SigningPublicKey; import net.i2p.data.SigningPublicKey;
import net.i2p.util.Clock;
import net.i2p.util.Log; import net.i2p.util.Log;
/** /**
@ -38,8 +39,7 @@ public class KeyManager {
private SigningPrivateKey _signingPrivateKey; private SigningPrivateKey _signingPrivateKey;
private SigningPublicKey _signingPublicKey; private SigningPublicKey _signingPublicKey;
private Map _leaseSetKeys; // Destination --> LeaseSetKeys private Map _leaseSetKeys; // Destination --> LeaseSetKeys
private boolean _alreadyReadFromDisk; private SynchronizeKeysJob _synchronizeJob;
private boolean _pendingWrite;
public final static String PROP_KEYDIR = "router.keyBackupDir"; public final static String PROP_KEYDIR = "router.keyBackupDir";
public final static String DEFAULT_KEYDIR = "keyBackup"; public final static String DEFAULT_KEYDIR = "keyBackup";
@ -47,43 +47,49 @@ public class KeyManager {
private final static String KEYFILE_PUBLIC_ENC = "publicEncryption.key"; private final static String KEYFILE_PUBLIC_ENC = "publicEncryption.key";
private final static String KEYFILE_PRIVATE_SIGNING = "privateSigning.key"; private final static String KEYFILE_PRIVATE_SIGNING = "privateSigning.key";
private final static String KEYFILE_PUBLIC_SIGNING = "publicSigning.key"; private final static String KEYFILE_PUBLIC_SIGNING = "publicSigning.key";
private final static long DELAY = 30*1000; private final static long DELAY = 5*60*1000;
public KeyManager(RouterContext context) { public KeyManager(RouterContext context) {
_context = context; _context = context;
_log = _context.logManager().getLog(KeyManager.class); _log = _context.logManager().getLog(KeyManager.class);
_synchronizeJob = new SynchronizeKeysJob();
setPrivateKey(null); setPrivateKey(null);
setPublicKey(null); setPublicKey(null);
setSigningPrivateKey(null); setSigningPrivateKey(null);
setSigningPublicKey(null); setSigningPublicKey(null);
_leaseSetKeys = new HashMap(); _leaseSetKeys = new HashMap();
_alreadyReadFromDisk = false; }
_pendingWrite = false;
_context.jobQueue().addJob(new SynchronizeKeysJob()); public void startup() {
queueWrite();
} }
/** Configure the router's private key */ /** Configure the router's private key */
public void setPrivateKey(PrivateKey key) { public void setPrivateKey(PrivateKey key) {
_privateKey = key; _privateKey = key;
_pendingWrite = true; if (key != null)
queueWrite();
} }
public PrivateKey getPrivateKey() { return _privateKey; } public PrivateKey getPrivateKey() { return _privateKey; }
/** Configure the router's public key */ /** Configure the router's public key */
public void setPublicKey(PublicKey key) { public void setPublicKey(PublicKey key) {
_publicKey = key; _publicKey = key;
_pendingWrite = true; if (key != null)
queueWrite();
} }
public PublicKey getPublicKey() { return _publicKey; } public PublicKey getPublicKey() { return _publicKey; }
/** Configure the router's signing private key */ /** Configure the router's signing private key */
public void setSigningPrivateKey(SigningPrivateKey key) { public void setSigningPrivateKey(SigningPrivateKey key) {
_signingPrivateKey = key; _signingPrivateKey = key;
_pendingWrite = true; if (key != null)
queueWrite();
} }
public SigningPrivateKey getSigningPrivateKey() { return _signingPrivateKey; } public SigningPrivateKey getSigningPrivateKey() { return _signingPrivateKey; }
/** Configure the router's signing public key */ /** Configure the router's signing public key */
public void setSigningPublicKey(SigningPublicKey key) { public void setSigningPublicKey(SigningPublicKey key) {
_signingPublicKey = key; _signingPublicKey = key;
_pendingWrite = true; if (key != null)
queueWrite();
} }
public SigningPublicKey getSigningPublicKey() { return _signingPublicKey; } public SigningPublicKey getSigningPublicKey() { return _signingPublicKey; }
@ -93,24 +99,27 @@ public class KeyManager {
synchronized (_leaseSetKeys) { synchronized (_leaseSetKeys) {
_leaseSetKeys.put(dest, keys); _leaseSetKeys.put(dest, keys);
} }
_pendingWrite = true; if (dest != null)
queueWrite();
} }
/** private void queueWrite() {
* True if we've never read the data from disk or if we've Clock cl = _context.clock();
* updated data in memory. JobQueue q = _context.jobQueue();
*/ if ( (cl == null) || (q == null) ) return;
private boolean needsSync() { _synchronizeJob.getTiming().setStartAfter(cl.now());
return !(_alreadyReadFromDisk && !_pendingWrite); q.addJob(_synchronizeJob);
} }
public LeaseSetKeys unregisterKeys(Destination dest) { public LeaseSetKeys unregisterKeys(Destination dest) {
if (_log.shouldLog(Log.INFO))
_log.info("Unregistering keys for destination " + dest.calculateHash().toBase64()); _log.info("Unregistering keys for destination " + dest.calculateHash().toBase64());
LeaseSetKeys rv = null; LeaseSetKeys rv = null;
synchronized (_leaseSetKeys) { synchronized (_leaseSetKeys) {
rv = (LeaseSetKeys)_leaseSetKeys.remove(dest); rv = (LeaseSetKeys)_leaseSetKeys.remove(dest);
} }
_pendingWrite = true; if (dest != null)
queueWrite();
return rv; return rv;
} }
@ -133,26 +142,27 @@ public class KeyManager {
super(KeyManager.this._context); super(KeyManager.this._context);
} }
public void runJob() { public void runJob() {
String keyDir = KeyManager.this._context.getProperty(PROP_KEYDIR); String keyDir = getContext().getProperty(PROP_KEYDIR);
if (keyDir == null) if (keyDir == null)
keyDir = DEFAULT_KEYDIR; keyDir = DEFAULT_KEYDIR;
File dir = new File(keyDir); File dir = new File(keyDir);
if (!dir.exists()) if (!dir.exists())
dir.mkdirs(); dir.mkdirs();
if (dir.exists() && dir.isDirectory() && dir.canRead() && dir.canWrite()) if (dir.exists() && dir.isDirectory() && dir.canRead() && dir.canWrite()) {
syncKeys(dir); syncKeys(dir);
} else {
_log.log(Log.CRIT, "Unable to synchronize keys in " + keyDir + " - permissions problem?");
}
getTiming().setStartAfter(KeyManager.this._context.clock().now()+DELAY); getTiming().setStartAfter(KeyManager.this._context.clock().now()+DELAY);
KeyManager.this._context.jobQueue().addJob(this); KeyManager.this._context.jobQueue().addJob(this);
} }
private void syncKeys(File keyDir) { private void syncKeys(File keyDir) {
if (!needsSync()) return;
syncPrivateKey(keyDir); syncPrivateKey(keyDir);
syncPublicKey(keyDir); syncPublicKey(keyDir);
syncSigningKey(keyDir); syncSigningKey(keyDir);
syncVerificationKey(keyDir); syncVerificationKey(keyDir);
_alreadyReadFromDisk = true;
} }
private void syncPrivateKey(File keyDir) { private void syncPrivateKey(File keyDir) {

View File

@ -55,6 +55,7 @@ public class Router {
private SessionKeyPersistenceHelper _sessionKeyPersistenceHelper; private SessionKeyPersistenceHelper _sessionKeyPersistenceHelper;
private boolean _killVMOnEnd; private boolean _killVMOnEnd;
private boolean _isAlive; private boolean _isAlive;
private int _gracefulExitCode;
private I2PThread.OOMEventListener _oomListener; private I2PThread.OOMEventListener _oomListener;
private ShutdownHook _shutdownHook; private ShutdownHook _shutdownHook;
private I2PThread _gracefulShutdownDetector; private I2PThread _gracefulShutdownDetector;
@ -168,6 +169,8 @@ public class Router {
Runtime.getRuntime().addShutdownHook(_shutdownHook); Runtime.getRuntime().addShutdownHook(_shutdownHook);
I2PThread.addOOMEventListener(_oomListener); I2PThread.addOOMEventListener(_oomListener);
_context.keyManager().startup();
readConfig(); readConfig();
setupHandlers(); setupHandlers();
@ -543,6 +546,7 @@ public class Router {
public static final int EXIT_HARD = 3; public static final int EXIT_HARD = 3;
public static final int EXIT_OOM = 10; public static final int EXIT_OOM = 10;
public static final int EXIT_HARD_RESTART = 4; public static final int EXIT_HARD_RESTART = 4;
public static final int EXIT_GRACEFUL_RESTART = 5;
public void shutdown(int exitCode) { public void shutdown(int exitCode) {
_isAlive = false; _isAlive = false;
@ -580,6 +584,10 @@ public class Router {
* *
*/ */
public void shutdownGracefully() { public void shutdownGracefully() {
shutdownGracefully(EXIT_GRACEFUL);
}
public void shutdownGracefully(int exitCode) {
_gracefulExitCode = exitCode;
_config.setProperty(PROP_SHUTDOWN_IN_PROGRESS, "true"); _config.setProperty(PROP_SHUTDOWN_IN_PROGRESS, "true");
synchronized (_gracefulShutdownDetector) { synchronized (_gracefulShutdownDetector) {
_gracefulShutdownDetector.notifyAll(); _gracefulShutdownDetector.notifyAll();
@ -615,7 +623,7 @@ public class Router {
if (_context.tunnelManager().getParticipatingCount() <= 0) { if (_context.tunnelManager().getParticipatingCount() <= 0) {
if (_log.shouldLog(Log.CRIT)) if (_log.shouldLog(Log.CRIT))
_log.log(Log.CRIT, "Graceful shutdown progress - no more tunnels, safe to die"); _log.log(Log.CRIT, "Graceful shutdown progress - no more tunnels, safe to die");
shutdown(EXIT_GRACEFUL); shutdown(_gracefulExitCode);
return; return;
} else { } else {
try { try {

View File

@ -222,8 +222,13 @@ public class OutboundClientMessageJob extends JobImpl {
public String getName() { return "Short circuit search"; } public String getName() { return "Short circuit search"; }
public void runJob() { public void runJob() {
LeaseSet ls = getContext().netDb().lookupLeaseSetLocally(_ls.getDestination().calculateHash()); LeaseSet ls = getContext().netDb().lookupLeaseSetLocally(_ls.getDestination().calculateHash());
if (ls == null) if (ls == null) {
try {
getContext().netDb().store(_ls.getDestination().calculateHash(), _ls); getContext().netDb().store(_ls.getDestination().calculateHash(), _ls);
} catch (IllegalArgumentException iae) {
// ignore - it expired anyway
}
}
} }
} }

View File

@ -13,6 +13,7 @@ import java.util.Properties;
import net.i2p.data.DataFormatException; import net.i2p.data.DataFormatException;
import net.i2p.data.RouterInfo; import net.i2p.data.RouterInfo;
import net.i2p.data.SigningPrivateKey;
import net.i2p.router.JobImpl; import net.i2p.router.JobImpl;
import net.i2p.router.RouterContext; import net.i2p.router.RouterContext;
import net.i2p.util.Log; import net.i2p.util.Log;
@ -41,7 +42,13 @@ public class PublishLocalRouterInfoJob extends JobImpl {
ri.setPublished(getContext().clock().now()); ri.setPublished(getContext().clock().now());
ri.setOptions(stats); ri.setOptions(stats);
ri.setAddresses(getContext().commSystem().createAddresses()); ri.setAddresses(getContext().commSystem().createAddresses());
ri.sign(getContext().keyManager().getSigningPrivateKey()); SigningPrivateKey key = getContext().keyManager().getSigningPrivateKey();
if (key == null) {
_log.log(Log.CRIT, "Internal error - signing private key not known? rescheduling publish for 30s");
requeue(30*1000);
return;
}
ri.sign(key);
getContext().router().setRouterInfo(ri); getContext().router().setRouterInfo(ri);
if (_log.shouldLog(Log.INFO)) if (_log.shouldLog(Log.INFO))
_log.info("Newly updated routerInfo is published with " + stats.size() _log.info("Newly updated routerInfo is published with " + stats.size()

View File

@ -490,11 +490,15 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade {
public void publish(RouterInfo localRouterInfo) { public void publish(RouterInfo localRouterInfo) {
if (!_initialized) return; if (!_initialized) return;
Hash h = localRouterInfo.getIdentity().getHash(); Hash h = localRouterInfo.getIdentity().getHash();
try {
store(h, localRouterInfo); store(h, localRouterInfo);
synchronized (_explicitSendKeys) { synchronized (_explicitSendKeys) {
_explicitSendKeys.add(h); _explicitSendKeys.add(h);
} }
writeMyInfo(localRouterInfo); writeMyInfo(localRouterInfo);
} catch (IllegalArgumentException iae) {
_log.error("Local routerInfo was invalid? "+ iae.getMessage(), iae);
}
} }
/** /**

View File

@ -26,12 +26,17 @@ class LoadClientAppsJob extends JobImpl {
private static final String PROP_CLIENT_CONFIG_FILENAME = "router.clientConfigFile"; private static final String PROP_CLIENT_CONFIG_FILENAME = "router.clientConfigFile";
private static final String DEFAULT_CLIENT_CONFIG_FILENAME = "clients.config"; private static final String DEFAULT_CLIENT_CONFIG_FILENAME = "clients.config";
private static boolean _loaded = false;
public LoadClientAppsJob(RouterContext ctx) { public LoadClientAppsJob(RouterContext ctx) {
super(ctx); super(ctx);
_log = ctx.logManager().getLog(LoadClientAppsJob.class); _log = ctx.logManager().getLog(LoadClientAppsJob.class);
} }
public void runJob() { public void runJob() {
synchronized (LoadClientAppsJob.class) {
if (_loaded) return;
_loaded = true;
}
Properties clientApps = getClientApps(); Properties clientApps = getClientApps();
int i = 0; int i = 0;
while (true) { while (true) {

View File

@ -112,6 +112,7 @@ public class TransportManager implements TransportEventListener {
((Transport)_transports.get(i)).stopListening(); ((Transport)_transports.get(i)).stopListening();
} }
_transports.clear(); _transports.clear();
_addresses.clear();
} }
private boolean isSupported(Set addresses, Transport t) { private boolean isSupported(Set addresses, Transport t) {