diff --git a/history.txt b/history.txt
index 2ab62f009b560d4dafe8a3681bf5b96f48bdcf46..80a39779cec5c24cbf7560fe2aaada82ac9b5b91 100644
--- a/history.txt
+++ b/history.txt
@@ -1,8 +1,11 @@
+2014-04-06 zzz
+ * NetDB: Iterative search improvements
 
 2014-04-05 zzz
  * I2PTunnel:
    - Add server option for unique local address per-client
    - Fix changing outproxy without stopping tunnel (ticket #1164)
+   - Fix add-to-addressbook hostname link in i2ptunnel (ticket #688)
  * NetDB: Skip key cert LS stores and verifies for floodfills that don't support them
 
 2014-04-01 zzz
diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java
index 0725033fa600cfb5180ac82283f5194ee35b34b0..f6e6df3a5c0d10fbf1efc2f7069c342c2fe16f4e 100644
--- a/router/java/src/net/i2p/router/RouterVersion.java
+++ b/router/java/src/net/i2p/router/RouterVersion.java
@@ -18,7 +18,7 @@ public class RouterVersion {
     /** deprecated */
     public final static String ID = "Monotone";
     public final static String VERSION = CoreVersion.VERSION;
-    public final static long BUILD = 3;
+    public final static long BUILD = 4;
 
     /** for example "-test" */
     public final static String EXTRA = "";
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillStoreJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillStoreJob.java
index b9acaca3e5669b17d5a58c2928d2a44e1cb2d01f..5618d256459f57a592b44571a4f339c8bb4aaa98 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillStoreJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillStoreJob.java
@@ -56,12 +56,16 @@ class FloodfillStoreJob extends StoreJob {
     protected void succeed() {
         super.succeed();
 
-        if (_state != null) {
             if (_facade.isVerifyInProgress(_state.getTarget())) {
                 if (_log.shouldLog(Log.INFO))
                     _log.info("Skipping verify, one already in progress for: " + _state.getTarget());
                 return;
             }
+            if (getContext().router().gracefulShutdownInProgress()) {
+                if (_log.shouldLog(Log.INFO))
+                    _log.info("Skipping verify, shutdown in progress for: " + _state.getTarget());
+                return;
+            }
             // Get the time stamp from the data we sent, so the Verify job can meke sure that
             // it finds something stamped with that time or newer.
             DatabaseEntry data = _state.getData();
@@ -81,7 +85,6 @@ class FloodfillStoreJob extends StoreJob {
             } catch (NoSuchElementException nsee) {}
             getContext().jobQueue().addJob(new FloodfillVerifyStoreJob(getContext(), _state.getTarget(),
                                                                        published, isRouterInfo, sentTo, _facade));
-        }
     }
     
     @Override
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeLookupJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeLookupJob.java
index d4c77dbfdf0ff65fde18a1de941b8433939c8361..13d76368877357176c371ba621fb8166b5aa454a 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeLookupJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeLookupJob.java
@@ -3,6 +3,7 @@ package net.i2p.router.networkdb.kademlia;
 import net.i2p.data.Hash;
 import net.i2p.data.RouterInfo;
 import net.i2p.data.i2np.DatabaseSearchReplyMessage;
+import net.i2p.util.Log;
 import net.i2p.router.JobImpl;
 import net.i2p.router.RouterContext;
 
@@ -22,19 +23,26 @@ import net.i2p.router.RouterContext;
  * @since 0.8.9
  */
 class IterativeLookupJob extends JobImpl {
+    private final Log _log;
     private final DatabaseSearchReplyMessage _dsrm;
     private final IterativeSearchJob _search;
 
     public IterativeLookupJob(RouterContext ctx, DatabaseSearchReplyMessage dsrm, IterativeSearchJob search) {
         super(ctx);
+        _log = ctx.logManager().getLog(IterativeLookupJob.class);
         _dsrm = dsrm;
         _search = search;
     }
 
     public void runJob() { 
-                // TODO - dsrm.getFromHash() can't be trusted - check against the list of
-                // those we sent the search to in _search ?
                 Hash from = _dsrm.getFromHash();
+                // dsrm.getFromHash() can't be trusted - check against the list of
+                // those we sent the search to in _search
+                if (!_search.wasQueried(from)) {
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn(_search.getJobId() + ": ILJ DSRM from unqueried peer: " + _dsrm);
+                    return;
+                }
 
                 // Chase the hashes from the reply
                 // 255 max, see comments in SingleLookupJob
@@ -54,15 +62,35 @@ class IterativeLookupJob extends JobImpl {
                         invalidPeers++;
                         continue;
                     }
+                    if (getContext().banlist().isBanlistedForever(peer)) {
+                        oldPeers++;
+                        continue;
+                    }
                     RouterInfo ri = getContext().netDb().lookupRouterInfoLocally(peer);
                     if (ri == null) {
-                        // get the RI from the peer that told us about it
-                        getContext().jobQueue().addJob(new IterativeFollowupJob(getContext(), peer, from, _search));
+                        // Take it on faith that it's ff to speed things up, we don't need the RI
+                        // to query it.
+                        // Zero-hop outbound tunnel will be failed in ISJ.sendQuery()
+                        _search.newPeerToTry(peer);
+                        if (_search.getFromHash() == null) {
+                            // get the RI from the peer that told us about it
+                            // Only if original search used expl. tunnels
+                            getContext().jobQueue().addJob(new SingleSearchJob(getContext(), peer, from));
+                        } else {
+                            // don't look it up as we don't have a good way to do it securely...
+                            // add to expl. queue to look up later? no, probably not safe either
+                        }
                         newPeers++;
                     } else if (ri.getPublished() < getContext().clock().now() - 60*60*1000 ||
                              !FloodfillNetworkDatabaseFacade.isFloodfill(ri)) {
                         // get an updated RI from the (now ff?) peer
-                        getContext().jobQueue().addJob(new IterativeFollowupJob(getContext(), peer, peer, _search));
+                        // Only if original search used expl. tunnels
+                        if (_search.getFromHash() == null) {
+                            getContext().jobQueue().addJob(new IterativeFollowupJob(getContext(), peer, peer, _search));
+                        } else {
+                            // for now, don't believe him, don't call newPeerToTry()
+                            // is IFJ safe if we use the client tunnels?
+                        }
                         oldPeers++;
                     } else {
                         // add it to the sorted queue
@@ -73,6 +101,8 @@ class IterativeLookupJob extends JobImpl {
                         oldPeers++;
                     }
                 }
+                if (_log.shouldLog(Log.INFO))
+                    _log.info(_search.getJobId() + ": ILJ DSRM processed " + newPeers + '/' + oldPeers + '/' + invalidPeers + " new/old/invalid hashes");
                 long timeSent = _search.timeSent(from);
                 // assume 0 dup
                 if (timeSent > 0) {
diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java
index 919250b5dcc604080a3e534446bccd16e11cd3e9..7a2fa777096ae2d97e37b6e3efd13321b91f45de 100644
--- a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java
+++ b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java
@@ -252,7 +252,6 @@ class IterativeSearchJob extends FloodSearchJob {
      *  Send a DLM to the peer
      */
     private void sendQuery(Hash peer) {
-            DatabaseLookupMessage dlm = new DatabaseLookupMessage(getContext(), true);
             TunnelManagerFacade tm = getContext().tunnelManager();
             TunnelInfo outTunnel;
             TunnelInfo replyTunnel;
@@ -281,11 +280,22 @@ class IterativeSearchJob extends FloodSearchJob {
             // if it happens to be closest to itself and we are using zero-hop exploratory tunnels.
             // If we don't, the OutboundMessageDistributor ends up logging erors for
             // not being able to send to the floodfill, if we don't have an older netdb entry.
-            if (outTunnel.getLength() <= 1 && peer.equals(_key)) {
-                failed(peer, false);
-                return;
+            if (outTunnel.getLength() <= 1) {
+                if (peer.equals(_key)) {
+                    failed(peer, false);
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn(getJobId() + ": not doing zero-hop self-lookup of " + peer);
+                    return;
+                }
+                if (_facade.lookupLocallyWithoutValidation(peer) == null) {
+                    failed(peer, false);
+                    if (_log.shouldLog(Log.WARN))
+                        _log.warn(getJobId() + ": not doing zero-hop lookup to unknown " + peer);
+                    return;
+                }
             }
             
+            DatabaseLookupMessage dlm = new DatabaseLookupMessage(getContext(), true);
             dlm.setFrom(replyTunnel.getPeer(0));
             dlm.setMessageExpiration(getContext().clock().now() + SINGLE_SEARCH_MSG_TIME);
             dlm.setReplyTunnel(replyTunnel.getReceiveTunnelId(0));
@@ -386,11 +396,17 @@ class IterativeSearchJob extends FloodSearchJob {
         if (peer.equals(getContext().routerHash()) ||
             peer.equals(_key))
             return;
-        RouterInfo ri = getContext().netDb().lookupRouterInfoLocally(peer);
-        if (!FloodfillNetworkDatabaseFacade.isFloodfill(ri))
+        if (getContext().banlist().isBanlistedForever(peer)) {
+            if (_log.shouldLog(Log.INFO))
+                _log.info(getJobId() + ": banlisted peer from DSRM " + peer);
             return;
-        if (getContext().banlist().isBanlistedForever(peer))
+        }
+        RouterInfo ri = getContext().netDb().lookupRouterInfoLocally(peer);
+        if (ri != null && !FloodfillNetworkDatabaseFacade.isFloodfill(ri)) {
+            if (_log.shouldLog(Log.INFO))
+                _log.info(getJobId() + ": non-ff peer from DSRM " + peer);
             return;
+        }
         synchronized (this) {
             if (_failedPeers.contains(peer) ||
                 _unheardFrom.contains(peer))
@@ -399,10 +415,28 @@ class IterativeSearchJob extends FloodSearchJob {
                 return;  // already in the list
         }
         if (_log.shouldLog(Log.INFO))
-            _log.info(getJobId() + ": new peer from DSRM " + peer);
+            _log.info(getJobId() + ": new peer from DSRM: known? " + (ri != null) + ' ' + peer);
         retry();
     }
 
+    /**
+     *  Hash of the dest this query is from
+     *  @return null for router
+     *  @since 0.9.13
+     */
+    public Hash getFromHash() {
+        return _fromLocalDest;
+    }
+
+    /**
+     *  Did we send a request to this peer?
+     *  @since 0.9.13
+     */
+    public boolean wasQueried(Hash peer) {
+        synchronized (this) {
+            return _unheardFrom.contains(peer) || _failedPeers.contains(peer);
+        }
+    }
 
     /**
      *  When did we send the query to the peer?
diff --git a/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java b/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java
index 9f202a6a2cf529a1806dfbe032fc8041b987cdb0..f769bc9bf9d9b70b4597a2c88cda6f429e197827 100644
--- a/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java
+++ b/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java
@@ -69,6 +69,7 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
             // LS or RI and client or expl., so that we can safely follow references
             // in a reply to a LS lookup over client tunnels.
             // ILJ would also have to follow references via client tunnels
+          /****
             DatabaseSearchReplyMessage orig = (DatabaseSearchReplyMessage) msg;
             if (orig.getNumReplies() > 0) {
                 if (_log.shouldLog(Log.INFO))
@@ -78,6 +79,7 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
                 newMsg.setSearchKey(orig.getSearchKey());
                 msg = newMsg;
             }
+          ****/
         } else if ( (_client != null) && 
                     (type == DatabaseStoreMessage.MESSAGE_TYPE)) {
             DatabaseStoreMessage dsm = (DatabaseStoreMessage) msg;
@@ -164,8 +166,8 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
                     _log.warn("no outbound tunnel to send the client message for " + _client + ": " + msg);
                 return;
             }
-            if (_log.shouldLog(Log.INFO))
-                _log.info("distributing inbound tunnel message back out " + out
+            if (_log.shouldLog(Log.DEBUG))
+                _log.debug("distributing IB tunnel msg type " + type + " back out " + out
                           + " targetting " + target);
             TunnelId outId = out.getSendTunnelId(0);
             if (outId == null) {
@@ -174,8 +176,9 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
                                + " failing to distribute " + msg);
                 return;
             }
-            if (msg.getMessageExpiration() < _context.clock().now() + 10*1000)
-                msg.setMessageExpiration(_context.clock().now() + 10*1000);
+            long exp = _context.clock().now() + 20*1000;
+            if (msg.getMessageExpiration() < exp)
+                msg.setMessageExpiration(exp);
             _context.tunnelDispatcher().dispatchOutbound(msg, outId, tunnel, target);
         }
     }
@@ -250,6 +253,7 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
                     // in a reply to a LS lookup over client tunnels.
                     // ILJ would also have to follow references via client tunnels
                     DatabaseSearchReplyMessage orig = (DatabaseSearchReplyMessage) data;
+                  /****
                     if (orig.getNumReplies() > 0) {
                         if (_log.shouldLog(Log.INFO))
                             _log.info("Removing replies from a garlic DSRM down a tunnel for " + _client + ": " + data);
@@ -258,6 +262,7 @@ class InboundMessageDistributor implements GarlicMessageReceiver.CloveReceiver {
                         newMsg.setSearchKey(orig.getSearchKey());
                         orig = newMsg;
                      }
+                   ****/
                     _context.inNetMessagePool().add(orig, null, null);
                 } else if (type == DataMessage.MESSAGE_TYPE) {
                         // a data message targetting the local router is how we send load tests (real