diff --git a/core/java/src/net/i2p/client/naming/BlockfileNamingService.java b/core/java/src/net/i2p/client/naming/BlockfileNamingService.java
index ced910183b4c0778a3a2d7f1688f44ae38c74d6a..fdd035c2e9f4c81fad0c5747a1fe9b979de24b36 100644
--- a/core/java/src/net/i2p/client/naming/BlockfileNamingService.java
+++ b/core/java/src/net/i2p/client/naming/BlockfileNamingService.java
@@ -33,6 +33,7 @@ import net.i2p.data.Hash;
 import net.i2p.util.Log;
 import net.i2p.util.SecureFileOutputStream;
 
+import net.metanotion.io.RAIFile;
 import net.metanotion.io.Serializer;
 import net.metanotion.io.block.BlockFile;
 import net.metanotion.io.data.UTF8StringBytes;
@@ -76,10 +77,11 @@ import net.metanotion.util.skiplist.SkipList;
 public class BlockfileNamingService extends DummyNamingService {
 
     private final BlockFile _bf;
-    private final RandomAccessFile _raf;
+    private final RAIFile _raf;
     private final List<String> _lists;
     private final List<InvalidEntry> _invalid;
     private volatile boolean _isClosed;
+    private final boolean _readOnly;
 
     private static final Serializer _infoSerializer = new PropertiesSerializer();
     private static final Serializer _stringSerializer = new UTF8StringBytes();
@@ -87,6 +89,7 @@ public class BlockfileNamingService extends DummyNamingService {
 
     private static final String HOSTS_DB = "hostsdb.blockfile";
     private static final String FALLBACK_LIST = "hosts.txt";
+    private static final String PROP_FORCE = "i2p.naming.blockfile.writeInAppContext";
 
     private static final String INFO_SKIPLIST = "%%__INFO__%%";
     private static final String PROP_INFO = "info";
@@ -100,6 +103,13 @@ public class BlockfileNamingService extends DummyNamingService {
     private static final String PROP_SOURCE = "s";
     
     /**
+     *  Opens the database at hostsdb.blockfile or creates a new
+     *  one and imports entries from hosts.txt, userhosts.txt, and privatehosts.txt.
+     *
+     *  If not in router context, the database will be opened read-only
+     *  unless the property i2p.naming.blockfile.writeInAppContext is true.
+     *  Not designed for simultaneous access by multiple processes.
+     *
      *  @throws RuntimeException on fatal error
      */
     public BlockfileNamingService(I2PAppContext context) {
@@ -107,14 +117,31 @@ public class BlockfileNamingService extends DummyNamingService {
         _lists = new ArrayList();
         _invalid = new ArrayList();
         BlockFile bf = null;
-        RandomAccessFile raf = null;
+        RAIFile raf = null;
+        boolean readOnly = false;
         File f = new File(_context.getRouterDir(), HOSTS_DB);
         if (f.exists()) {
             try {
                 // closing a BlockFile does not close the underlying file,
                 // so we must create and retain a RAF so we may close it later
-                raf = new RandomAccessFile(f, "rw");
+
+                // *** Open readonly if not in router context (unless forced)
+                readOnly = (!f.canWrite()) ||
+                           ((!context.isRouterContext()) && (!context.getBooleanProperty(PROP_FORCE)));
+                raf = new RAIFile(f, true, !readOnly);
                 bf = initExisting(raf);
+                if (readOnly && context.isRouterContext())
+                    _log.logAlways(Log.WARN, "Read-only hosts database in router context");
+                if (bf.wasMounted()) {
+                    if (context.isRouterContext())
+                        _log.logAlways(Log.WARN, "The hosts database was not closed cleanly or is still open by another process");
+                    else
+                        _log.logAlways(Log.WARN, "The hosts database is possibly in use by another process, perhaps the router? " +
+                                       "The database is not designed for simultaneous access by multiple processes.\n" +
+                                       "If you are using clients outside the router JVM, consider using the hosts.txt " +
+                                       "naming service with " +
+                                       "i2p.naming.impl=net.i2p.client.naming.HostsTxtNamingService");
+                }
             } catch (IOException ioe) {
                 if (raf != null) {
                     try { raf.close(); } catch (IOException e) {}
@@ -131,7 +158,7 @@ public class BlockfileNamingService extends DummyNamingService {
             try {
                 // closing a BlockFile does not close the underlying file,
                 // so we must create and retain a RAF so we may close it later
-                raf = new RandomAccessFile(f, "rw");
+                raf = new RAIFile(f, true, true);
                 SecureFileOutputStream.setPerms(f);
                 bf = init(raf);
             } catch (IOException ioe) {
@@ -141,9 +168,11 @@ public class BlockfileNamingService extends DummyNamingService {
                 _log.log(Log.CRIT, "Failed to initialize database", ioe);
                 throw new RuntimeException(ioe);
             }
+            readOnly = false;
         }
         _bf = bf;
         _raf = raf;
+        _readOnly = readOnly;
         _context.addShutdownTask(new Shutdown());
     }
 
@@ -152,7 +181,7 @@ public class BlockfileNamingService extends DummyNamingService {
      *  privatehosts.txt, userhosts.txt, and hosts.txt,
      *  creating a skiplist in the database for each.
      */
-    private BlockFile init(RandomAccessFile f) throws IOException {
+    private BlockFile init(RAIFile f) throws IOException {
         long start = _context.clock().now();
         try {
             BlockFile rv = new BlockFile(f, true);
@@ -221,7 +250,7 @@ public class BlockfileNamingService extends DummyNamingService {
     /**
      *  Read the info block of an existing database.
      */
-    private BlockFile initExisting(RandomAccessFile raf) throws IOException {
+    private BlockFile initExisting(RAIFile raf) throws IOException {
         long start = _context.clock().now();
         try {
             BlockFile bf = new BlockFile(raf, false);
@@ -427,6 +456,10 @@ public class BlockfileNamingService extends DummyNamingService {
     }
 
     private boolean put(String hostname, Destination d, Properties options, boolean checkExisting) {
+        if (_readOnly) {
+            _log.error("Add entry failed, read-only hosts database");
+            return false;
+        }
         String key = hostname.toLowerCase();
         String listname = FALLBACK_LIST;
         Properties props = new Properties();
@@ -475,6 +508,10 @@ public class BlockfileNamingService extends DummyNamingService {
      */
     @Override
     public boolean remove(String hostname, Properties options) {
+        if (_readOnly) {
+            _log.error("Remove entry failed, read-only hosts database");
+            return false;
+        }
         String key = hostname.toLowerCase();
         String listname = FALLBACK_LIST;
         if (options != null) {
@@ -657,7 +694,7 @@ public class BlockfileNamingService extends DummyNamingService {
                      de != null &&
                      de.dest != null &&
                      de.dest.getPublicKey() != null;
-        if (!rv)
+        if ((!rv) && (!_readOnly))
             _invalid.add(new InvalidEntry(key, listname));
         return rv;
     }
@@ -731,7 +768,11 @@ public class BlockfileNamingService extends DummyNamingService {
             try {
                 _bf.close();
             } catch (IOException ioe) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Error closing", ioe);
             } catch (RuntimeException e) {
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("Error closing", e);
             }
             try {
                 _raf.close();
@@ -870,8 +911,16 @@ public class BlockfileNamingService extends DummyNamingService {
         }
     }
 
+    /**
+     *  BlockfileNamingService [force]
+     *  force = force writable
+     */
     public static void main(String[] args) {
-        BlockfileNamingService bns = new BlockfileNamingService(I2PAppContext.getGlobalContext());
+        Properties ctxProps = new Properties();
+        if (args.length > 0 && args[0].equals("force"))
+            ctxProps.setProperty(PROP_FORCE, "true");
+        I2PAppContext ctx = new I2PAppContext(ctxProps);
+        BlockfileNamingService bns = new BlockfileNamingService(ctx);
         List<String> names = null;
         Properties props = new Properties();
         try {
diff --git a/core/java/src/net/metanotion/io/RAIFile.java b/core/java/src/net/metanotion/io/RAIFile.java
index 0988896e62bcdaddfa7bd3edff971f76a7de0210..43ef2f2b87580a96d43fc05d680a9ef920d507b1 100644
--- a/core/java/src/net/metanotion/io/RAIFile.java
+++ b/core/java/src/net/metanotion/io/RAIFile.java
@@ -38,13 +38,17 @@ import java.io.RandomAccessFile;
 public class RAIFile implements RandomAccessInterface, DataInput, DataOutput {
 	private File f;
 	private RandomAccessFile delegate;
-	private boolean r=false, w=false;
+	private final boolean r, w;
 
 	public RAIFile(RandomAccessFile file) throws FileNotFoundException {
 		this.f = null;
 		this.delegate = file;
+		this.r = true;
+		// fake, we don't really know
+		this.w = true;
 	}
 
+	/** @param read must be true */
 	public RAIFile(File file, boolean read, boolean write) throws FileNotFoundException {
 		this.f = file;
 		this.r = read;
@@ -55,6 +59,15 @@ public class RAIFile implements RandomAccessInterface, DataInput, DataOutput {
 		this.delegate = new RandomAccessFile(file, mode);
 	}
 
+	/**
+	 *  I2P is the file writable?
+	 *  Only valid if the File constructor was used, not the RAF constructor
+	 *  @since 0.8.8
+	 */
+	public boolean canWrite() {
+		return this.w;
+	}
+
 	public long getFilePointer()		throws IOException { return delegate.getFilePointer(); }
 	public long length()				throws IOException { return delegate.length(); }
 	public int read()					throws IOException { return delegate.read(); }
diff --git a/core/java/src/net/metanotion/io/RandomAccessInterface.java b/core/java/src/net/metanotion/io/RandomAccessInterface.java
index 953d006985482a1028e34d18e8a2365dc7d5b98b..fa382621c8332325e8162c1dca08727ee6dc03c4 100644
--- a/core/java/src/net/metanotion/io/RandomAccessInterface.java
+++ b/core/java/src/net/metanotion/io/RandomAccessInterface.java
@@ -39,6 +39,13 @@ public interface RandomAccessInterface {
 	public void seek(long pos) throws IOException;
 	public void setLength(long newLength) throws IOException;
 
+	/**
+	 *  I2P is the file writable?
+	 *  Only valid if the File constructor was used, not the RAF constructor
+	 *  @since 0.8.8
+	 */
+	public boolean canWrite();
+
 	// Closeable Methods
 	public void close() throws IOException;
 
diff --git a/core/java/src/net/metanotion/io/block/BlockFile.java b/core/java/src/net/metanotion/io/block/BlockFile.java
index 74b8f723ce82b2f8f15e062cf72818a6b73e4e50..eee03a0da4dfd2cdd7e81d8b3578ec5912520d38 100644
--- a/core/java/src/net/metanotion/io/block/BlockFile.java
+++ b/core/java/src/net/metanotion/io/block/BlockFile.java
@@ -90,6 +90,9 @@ public class BlockFile {
 	private int mounted = 0;
 	public int spanSize = 16;
 
+	/** I2P was the file locked when we opened it? */
+	private final boolean _wasMounted;
+
 	private BSkipList metaIndex;
 	/** cached list of free pages, only valid if freListStart > 0 */
 	private FreeListBlock flb;
@@ -258,11 +261,19 @@ public class BlockFile {
 		return curPage;
 	}
 
+	/** Use this constructor with a readonly RAI for a readonly blockfile */
 	public BlockFile(RandomAccessInterface rai) throws IOException { this(rai, false); }
+
+	/** RAF must be writable */
 	public BlockFile(RandomAccessFile raf) throws IOException { this(new RAIFile(raf), false); }
+
+	/** RAF must be writable */
 	public BlockFile(RandomAccessFile raf, boolean init) throws IOException { this(new RAIFile(raf), init); }
+
+	/** File must be writable */
 	public BlockFile(File f, boolean init) throws IOException { this(new RAIFile(f, true, true), init); }
 
+	/** Use this constructor with a readonly RAI and init = false for a readonly blockfile */
 	public BlockFile(RandomAccessInterface rai, boolean init) throws IOException {
 		if(rai==null) { throw new NullPointerException(); }
 		
@@ -283,16 +294,26 @@ public class BlockFile {
 				throw new IOException("Bad magic number");
 			}
 		}
-		if (mounted != 0)
+		_wasMounted = mounted != 0;
+		if (_wasMounted)
 			log.warn("Warning - file was not previously closed");
 		if(fileLen != file.length())
 			throw new IOException("Expected file length " + fileLen +
 		                              " but actually " + file.length());
-		mount();
+		if (rai.canWrite())
+			mount();
 
 		metaIndex = new BSkipList(spanSize, this, METAINDEX_PAGE, new StringBytes(), new IntBytes());
 	}
 
+	/**
+	 *  I2P was the file locked when we opened it?
+	 *  @since 0.8.8
+	 */
+	public boolean wasMounted() {
+		return _wasMounted;
+	}
+
 	/**
 	 *  Go to any page but the superblock.
 	 *  Page 1 is the superblock, must use file.seek(0) to get there.
@@ -454,8 +475,10 @@ public class BlockFile {
 		}
 
 		// Unmount.
-		file.seek(BlockFile.OFFSET_MOUNTED);
-		file.writeShort(0);
+		if (file.canWrite()) {
+			file.seek(BlockFile.OFFSET_MOUNTED);
+			file.writeShort(0);
+		}
 	}
 
 	public void bfck(boolean fix) {
diff --git a/core/java/src/net/metanotion/io/block/index/BSkipList.java b/core/java/src/net/metanotion/io/block/index/BSkipList.java
index c55b850d950953a3f43456ee62a206a856cf9ca5..9313441f5371a8733cc63085eee82f20d8cbd79a 100644
--- a/core/java/src/net/metanotion/io/block/index/BSkipList.java
+++ b/core/java/src/net/metanotion/io/block/index/BSkipList.java
@@ -98,7 +98,8 @@ public class BSkipList extends SkipList {
 		}
 		if (BlockFile.log.shouldLog(Log.DEBUG))
 			BlockFile.log.debug("Loaded " + this + " cached " + levelHash.size() + " levels and " + spanHash.size() + " spans with " + total + " entries");
-		if (levelCount != levelHash.size() || spans != spanHash.size() || size != total) {
+		if (bf.file.canWrite() &&
+		    (levelCount != levelHash.size() || spans != spanHash.size() || size != total)) {
 			if (BlockFile.log.shouldLog(Log.WARN))
 				BlockFile.log.warn("On-disk counts were " + levelCount + " / " + spans + " / " +  size + ", correcting");
 			size = total;
@@ -117,6 +118,8 @@ public class BSkipList extends SkipList {
 
 	@Override
 	public void flush() {
+                if (!bf.file.canWrite())
+                    return;
 		if (isClosed) {
 			BlockFile.log.error("Already closed!! " + this, new Exception());
 			return;