diff --git a/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothTransportConnection.java b/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothTransportConnection.java
index 50cd9db82cc1ff059c6e5e05e867a8c4923a53a5..d349650735cbd6540378c97777b9e28e013eb8b2 100644
--- a/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothTransportConnection.java
+++ b/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothTransportConnection.java
@@ -3,8 +3,11 @@ package org.briarproject.plugins.droidtooth;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.briarproject.api.plugins.Plugin;
+import org.briarproject.api.plugins.TransportConnectionReader;
+import org.briarproject.api.plugins.TransportConnectionWriter;
 import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
 
 import android.bluetooth.BluetoothSocket;
@@ -13,30 +16,69 @@ class DroidtoothTransportConnection implements DuplexTransportConnection {
 
 	private final Plugin plugin;
 	private final BluetoothSocket socket;
+	private final Reader reader;
+	private final Writer writer;
+	private final AtomicBoolean halfClosed, closed;
 
 	DroidtoothTransportConnection(Plugin plugin, BluetoothSocket socket) {
 		this.plugin = plugin;
 		this.socket = socket;
+		reader = new Reader();
+		writer = new Writer();
+		halfClosed = new AtomicBoolean(false);
+		closed = new AtomicBoolean(false);
 	}
 
-	public int getMaxFrameLength() {
-		return plugin.getMaxFrameLength();
+	public TransportConnectionReader getReader() {
+		return reader;
 	}
 
-	public long getMaxLatency() {
-		return plugin.getMaxLatency();
+	public TransportConnectionWriter getWriter() {
+		return writer;
 	}
 
-	public InputStream getInputStream() throws IOException {
-		return socket.getInputStream();
-	}
+	private class Reader implements TransportConnectionReader {
+
+		public int getMaxFrameLength() {
+			return plugin.getMaxFrameLength();
+		}
+
+		public long getMaxLatency() {
+			return plugin.getMaxLatency();
+		}
 
-	public OutputStream getOutputStream() throws IOException {
-		return socket.getOutputStream();
+		public InputStream getInputStream() throws IOException {
+			return socket.getInputStream();
+		}
+
+		public void dispose(boolean exception, boolean recognised)
+				throws IOException {
+			if(halfClosed.getAndSet(true) || exception)
+				if(!closed.getAndSet(true)) socket.close();
+		}
 	}
 
-	public void dispose(boolean exception, boolean recognised)
-			throws IOException {
-		socket.close();
+	private class Writer implements TransportConnectionWriter {
+
+		public int getMaxFrameLength() {
+			return plugin.getMaxFrameLength();
+		}
+
+		public long getMaxLatency() {
+			return plugin.getMaxLatency();
+		}
+
+		public long getCapacity() {
+			return Long.MAX_VALUE;
+		}
+
+		public OutputStream getOutputStream() throws IOException {
+			return socket.getOutputStream();
+		}
+
+		public void dispose(boolean exception) throws IOException {
+			if(halfClosed.getAndSet(true) || exception)
+				if(!closed.getAndSet(true)) socket.close();
+		}
 	}
 }
diff --git a/briar-android/src/org/briarproject/plugins/tor/TorTransportConnection.java b/briar-android/src/org/briarproject/plugins/tor/TorTransportConnection.java
index 14c490d1877f20fda3954724e9630998bee9494d..ab0969eae633fd8270cc35bcb33bc8e361884ebf 100644
--- a/briar-android/src/org/briarproject/plugins/tor/TorTransportConnection.java
+++ b/briar-android/src/org/briarproject/plugins/tor/TorTransportConnection.java
@@ -4,38 +4,80 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.Socket;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.briarproject.api.plugins.Plugin;
+import org.briarproject.api.plugins.TransportConnectionReader;
+import org.briarproject.api.plugins.TransportConnectionWriter;
 import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
 
 class TorTransportConnection implements DuplexTransportConnection {
 
 	private final Plugin plugin;
 	private final Socket socket;
+	private final Reader reader;
+	private final Writer writer;
+	private final AtomicBoolean halfClosed, closed;
 
 	TorTransportConnection(Plugin plugin, Socket socket) {
 		this.plugin = plugin;
 		this.socket = socket;
+		reader = new Reader();
+		writer = new Writer();
+		halfClosed = new AtomicBoolean(false);
+		closed = new AtomicBoolean(false);
 	}
 
-	public int getMaxFrameLength() {
-		return plugin.getMaxFrameLength();
+	public TransportConnectionReader getReader() {
+		return reader;
 	}
 
-	public long getMaxLatency() {
-		return plugin.getMaxLatency();
+	public TransportConnectionWriter getWriter() {
+		return writer;
 	}
 
-	public InputStream getInputStream() throws IOException {
-		return socket.getInputStream();
-	}
+	private class Reader implements TransportConnectionReader {
+
+		public int getMaxFrameLength() {
+			return plugin.getMaxFrameLength();
+		}
+
+		public long getMaxLatency() {
+			return plugin.getMaxLatency();
+		}
 
-	public OutputStream getOutputStream() throws IOException {
-		return socket.getOutputStream();
+		public InputStream getInputStream() throws IOException {
+			return socket.getInputStream();
+		}
+
+		public void dispose(boolean exception, boolean recognised)
+				throws IOException {
+			if(halfClosed.getAndSet(true) || exception)
+				if(!closed.getAndSet(true)) socket.close();
+		}
 	}
 
-	public void dispose(boolean exception, boolean recognised)
-			throws IOException {
-		socket.close();
+	private class Writer implements TransportConnectionWriter {
+
+		public int getMaxFrameLength() {
+			return plugin.getMaxFrameLength();
+		}
+
+		public long getMaxLatency() {
+			return plugin.getMaxLatency();
+		}
+
+		public long getCapacity() {
+			return Long.MAX_VALUE;
+		}
+
+		public OutputStream getOutputStream() throws IOException {
+			return socket.getOutputStream();
+		}
+
+		public void dispose(boolean exception) throws IOException {
+			if(halfClosed.getAndSet(true) || exception)
+				if(!closed.getAndSet(true)) socket.close();
+		}
 	}
 }
diff --git a/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java b/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java
index b946f57606f956cc90abfab69169fdaa27a41877..ba9c86224b04d83b6ae67d1da2ed50b7f9f90b83 100644
--- a/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java
+++ b/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java
@@ -72,13 +72,10 @@ public interface CryptoComponent {
 
 	/**
 	 * Derives a frame key from the given temporary secret and stream number.
-	 * @param alice indicates whether the key is for a connection initiated by
+	 * @param alice indicates whether the key is for a stream initiated by
 	 * Alice or Bob.
-	 * @param initiator indicates whether the key is for the initiator's or the
-	 * responder's side of the connection.
 	 */
-	SecretKey deriveFrameKey(byte[] secret, long streamNumber, boolean alice,
-			boolean initiator);
+	SecretKey deriveFrameKey(byte[] secret, long streamNumber, boolean alice);
 
 	/** Returns a cipher for encrypting and authenticating frames. */
 	AuthenticatedCipher getFrameCipher();
diff --git a/briar-api/src/org/briarproject/api/messaging/MessagingSession.java b/briar-api/src/org/briarproject/api/messaging/MessagingSession.java
new file mode 100644
index 0000000000000000000000000000000000000000..786922c8165b25d5196e6e0ae571104bf3437a19
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/messaging/MessagingSession.java
@@ -0,0 +1,19 @@
+package org.briarproject.api.messaging;
+
+import java.io.IOException;
+
+public interface MessagingSession {
+
+	/**
+	 * Runs the session. This method returns when there are no more packets to
+	 * send or when the {@link #interrupt()} method has been called.
+	 */
+	void run() throws IOException;
+
+	/**
+	 * Interrupts the session, causing the {@link #run()} method to return at
+	 * the next opportunity or throw an {@link java.io.IOException IOException}
+	 * if it cannot return cleanly.
+	 */
+	void interrupt();
+}
diff --git a/briar-api/src/org/briarproject/api/messaging/MessagingSessionFactory.java b/briar-api/src/org/briarproject/api/messaging/MessagingSessionFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..a014fe92f9d6d34149dbac84504ef31c7ad46248
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/messaging/MessagingSessionFactory.java
@@ -0,0 +1,14 @@
+package org.briarproject.api.messaging;
+
+import org.briarproject.api.plugins.TransportConnectionReader;
+import org.briarproject.api.plugins.TransportConnectionWriter;
+import org.briarproject.api.transport.StreamContext;
+
+public interface MessagingSessionFactory {
+
+	MessagingSession createIncomingSession(StreamContext ctx,
+			TransportConnectionReader r);
+
+	MessagingSession createOutgoingSession(StreamContext ctx,
+			TransportConnectionWriter w, boolean duplex);
+}
diff --git a/briar-api/src/org/briarproject/api/messaging/duplex/DuplexConnectionFactory.java b/briar-api/src/org/briarproject/api/messaging/duplex/DuplexConnectionFactory.java
deleted file mode 100644
index 977df2176ffef7733c817167ef8d10e57b9e0d39..0000000000000000000000000000000000000000
--- a/briar-api/src/org/briarproject/api/messaging/duplex/DuplexConnectionFactory.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.briarproject.api.messaging.duplex;
-
-import org.briarproject.api.ContactId;
-import org.briarproject.api.TransportId;
-import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
-import org.briarproject.api.transport.StreamContext;
-
-public interface DuplexConnectionFactory {
-
-	void createIncomingConnection(StreamContext ctx,
-			DuplexTransportConnection d);
-
-	void createOutgoingConnection(ContactId c, TransportId t,
-			DuplexTransportConnection d);
-}
diff --git a/briar-api/src/org/briarproject/api/messaging/simplex/SimplexConnectionFactory.java b/briar-api/src/org/briarproject/api/messaging/simplex/SimplexConnectionFactory.java
deleted file mode 100644
index 61f75a084a3300bce0584f252cddb08c8d31f354..0000000000000000000000000000000000000000
--- a/briar-api/src/org/briarproject/api/messaging/simplex/SimplexConnectionFactory.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package org.briarproject.api.messaging.simplex;
-
-import org.briarproject.api.ContactId;
-import org.briarproject.api.TransportId;
-import org.briarproject.api.plugins.simplex.SimplexTransportReader;
-import org.briarproject.api.plugins.simplex.SimplexTransportWriter;
-import org.briarproject.api.transport.StreamContext;
-
-public interface SimplexConnectionFactory {
-
-	void createIncomingConnection(StreamContext ctx,
-			SimplexTransportReader r);
-
-	void createOutgoingConnection(ContactId c, TransportId t,
-			SimplexTransportWriter w);
-}
diff --git a/briar-api/src/org/briarproject/api/plugins/TransportConnectionReader.java b/briar-api/src/org/briarproject/api/plugins/TransportConnectionReader.java
new file mode 100644
index 0000000000000000000000000000000000000000..71bf0cc03f6efaa44967211cde8be39fc4e91778
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/plugins/TransportConnectionReader.java
@@ -0,0 +1,32 @@
+package org.briarproject.api.plugins;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * An interface for reading data from a transport connection. The reader is not
+ * responsible for decrypting or authenticating the data.
+ */
+public interface TransportConnectionReader {
+
+	/** Returns the maximum frame length of the transport in bytes. */
+	int getMaxFrameLength();
+
+	/** Returns the maximum latency of the transport in milliseconds. */
+	long getMaxLatency();
+
+	/** Returns an input stream for reading from the transport connection. */
+	InputStream getInputStream() throws IOException;
+
+	/**
+	 * Marks this side of the transport connection closed. If the transport is
+	 * simplex, the connection is closed. If the transport is duplex, the
+	 * connection is closed if <tt>exception</tt> is true or the other side of
+	 * the connection has been marked as closed.
+	 * @param exception true if the connection is being closed because of an
+	 * exception. This may affect how resources are disposed of.
+	 * @param recognised true if the pseudo-random tag was recognised. This may
+	 * affect how resources are disposed of.
+	 */
+	void dispose(boolean exception, boolean recognised) throws IOException;
+}
diff --git a/briar-api/src/org/briarproject/api/plugins/TransportConnectionWriter.java b/briar-api/src/org/briarproject/api/plugins/TransportConnectionWriter.java
new file mode 100644
index 0000000000000000000000000000000000000000..554e5de8a110e3d24c249cf896287bdd1e5336d4
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/plugins/TransportConnectionWriter.java
@@ -0,0 +1,33 @@
+package org.briarproject.api.plugins;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An interface for writing data to a transport connection. The writer is not
+ * responsible for authenticating or encrypting the data.
+ */
+public interface TransportConnectionWriter {
+
+	/** Returns the maximum frame length of the transport in bytes. */
+	int getMaxFrameLength();
+
+	/** Returns the maximum latency of the transport in milliseconds. */
+	long getMaxLatency();
+
+	/** Returns the capacity of the transport connection in bytes. */
+	long getCapacity();
+
+	/** Returns an output stream for writing to the transport connection. */
+	OutputStream getOutputStream() throws IOException;
+
+	/**
+	 * Marks this side of the transport connection closed. If the transport is
+	 * simplex, the connection is closed. If the transport is duplex, the
+	 * connection is closed if <tt>exception</tt> is true or the other side of
+	 * the connection has been marked as closed.
+	 * @param exception true if the connection is being closed because of an
+	 * exception. This may affect how resources are disposed of.
+	 */
+	void dispose(boolean exception) throws IOException;
+}
diff --git a/briar-api/src/org/briarproject/api/plugins/duplex/DuplexTransportConnection.java b/briar-api/src/org/briarproject/api/plugins/duplex/DuplexTransportConnection.java
index 7193c0849ae8f57a2fe04b701bc4d2c567e0b35d..8440fe4add593c36820d53262e8aa712f8260ce7 100644
--- a/briar-api/src/org/briarproject/api/plugins/duplex/DuplexTransportConnection.java
+++ b/briar-api/src/org/briarproject/api/plugins/duplex/DuplexTransportConnection.java
@@ -1,8 +1,7 @@
 package org.briarproject.api.plugins.duplex;
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
+import org.briarproject.api.plugins.TransportConnectionReader;
+import org.briarproject.api.plugins.TransportConnectionWriter;
 
 /**
  * An interface for reading and writing data over a duplex transport. The
@@ -11,23 +10,11 @@ import java.io.OutputStream;
  */
 public interface DuplexTransportConnection {
 
-	/** Returns the maximum frame length of the transport in bytes. */
-	int getMaxFrameLength();
+	/** Returns a {@link org.briarproject.api.plugins.TransportConnectionReader
+	 * TransportConnectionReader} for reading from the connection. */
+	TransportConnectionReader getReader();
 
-	/** Returns the maximum latency of the transport in milliseconds. */
-	long getMaxLatency();
-
-	/** Returns an input stream for reading from the connection. */
-	InputStream getInputStream() throws IOException;
-
-	/** Returns an output stream for writing to the connection. */
-	OutputStream getOutputStream() throws IOException;
-
-	/**
-	 * Closes the connection and disposes of any associated resources. The
-	 * first argument indicates whether the connection is being closed because
-	 * of an exception and the second argument indicates whether the connection
-	 * was recognised, which may affect how resources are disposed of.
-	 */
-	void dispose(boolean exception, boolean recognised) throws IOException;
+	/** Returns a {@link org.briarproject.api.plugins.TransportConnectionWriter
+	 * TransportConnectionWriter} for writing to the connection. */
+	TransportConnectionWriter getWriter();
 }
diff --git a/briar-api/src/org/briarproject/api/plugins/simplex/SimplexPlugin.java b/briar-api/src/org/briarproject/api/plugins/simplex/SimplexPlugin.java
index c6334becc9765c7b11ba0affcf730c659e79a066..9e4a1defd8e64fe5a46b550f1d39f3676f30e64d 100644
--- a/briar-api/src/org/briarproject/api/plugins/simplex/SimplexPlugin.java
+++ b/briar-api/src/org/briarproject/api/plugins/simplex/SimplexPlugin.java
@@ -2,6 +2,8 @@ package org.briarproject.api.plugins.simplex;
 
 import org.briarproject.api.ContactId;
 import org.briarproject.api.plugins.Plugin;
+import org.briarproject.api.plugins.TransportConnectionReader;
+import org.briarproject.api.plugins.TransportConnectionWriter;
 
 /** An interface for transport plugins that support simplex communication. */
 public interface SimplexPlugin extends Plugin {
@@ -11,12 +13,12 @@ public interface SimplexPlugin extends Plugin {
 	 * current transport and configuration properties. Returns null if a reader
 	 * could not be created.
 	 */
-	SimplexTransportReader createReader(ContactId c);
+	TransportConnectionReader createReader(ContactId c);
 
 	/**
 	 * Attempts to create and return a writer for the given contact using the
 	 * current transport and configuration properties. Returns null if a writer
 	 * could not be created.
 	 */
-	SimplexTransportWriter createWriter(ContactId c);
+	TransportConnectionWriter createWriter(ContactId c);
 }
diff --git a/briar-api/src/org/briarproject/api/plugins/simplex/SimplexPluginCallback.java b/briar-api/src/org/briarproject/api/plugins/simplex/SimplexPluginCallback.java
index d4cf816118e5e038c3dfdb71ddc4b8d4136ee228..1f597c70d21cf9f6d4f7f10790c9e2065786c569 100644
--- a/briar-api/src/org/briarproject/api/plugins/simplex/SimplexPluginCallback.java
+++ b/briar-api/src/org/briarproject/api/plugins/simplex/SimplexPluginCallback.java
@@ -2,6 +2,8 @@ package org.briarproject.api.plugins.simplex;
 
 import org.briarproject.api.ContactId;
 import org.briarproject.api.plugins.PluginCallback;
+import org.briarproject.api.plugins.TransportConnectionReader;
+import org.briarproject.api.plugins.TransportConnectionWriter;
 
 /**
  * An interface for handling readers and writers created by a simplex transport
@@ -9,7 +11,7 @@ import org.briarproject.api.plugins.PluginCallback;
  */
 public interface SimplexPluginCallback extends PluginCallback {
 
-	void readerCreated(SimplexTransportReader r);
+	void readerCreated(TransportConnectionReader r);
 
-	void writerCreated(ContactId c, SimplexTransportWriter w);
+	void writerCreated(ContactId c, TransportConnectionWriter w);
 }
diff --git a/briar-api/src/org/briarproject/api/plugins/simplex/SimplexTransportReader.java b/briar-api/src/org/briarproject/api/plugins/simplex/SimplexTransportReader.java
deleted file mode 100644
index 387a7a03bf69cc48b7109afe9a450881695f01f5..0000000000000000000000000000000000000000
--- a/briar-api/src/org/briarproject/api/plugins/simplex/SimplexTransportReader.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package org.briarproject.api.plugins.simplex;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * An interface for reading data from a simplex transport. The reader is not
- * responsible for decrypting or authenticating the data before returning it.
- */
-public interface SimplexTransportReader {
-
-	/** Returns the maximum frame length of the transport in bytes. */
-	int getMaxFrameLength();
-
-	/** Returns an input stream for reading from the transport. */
-	InputStream getInputStream() throws IOException;
-
-	/**
-	 * Closes the reader and disposes of any associated resources. The first
-	 * argument indicates whether the reader is being closed because of an
-	 * exception and the second argument indicates whether the connection was
-	 * recognised, which may affect how resources are disposed of.
-	 */
-	void dispose(boolean exception, boolean recognised) throws IOException;
-}
diff --git a/briar-api/src/org/briarproject/api/plugins/simplex/SimplexTransportWriter.java b/briar-api/src/org/briarproject/api/plugins/simplex/SimplexTransportWriter.java
deleted file mode 100644
index 0b50a7898126c235ed784e7499a83705dd0ce73d..0000000000000000000000000000000000000000
--- a/briar-api/src/org/briarproject/api/plugins/simplex/SimplexTransportWriter.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package org.briarproject.api.plugins.simplex;
-
-import java.io.IOException;
-import java.io.OutputStream;
-
-/**
- * An interface for writing data to a simplex transport. The writer is not
- * responsible for authenticating or encrypting the data before writing it.
- */
-public interface SimplexTransportWriter {
-
-	/** Returns the capacity of the transport in bytes. */
-	long getCapacity();
-
-	/** Returns the maximum frame length of the transport in bytes. */
-	int getMaxFrameLength();
-
-	/** Returns the maximum latency of the transport in milliseconds. */
-	long getMaxLatency();
-
-	/** Returns an output stream for writing to the transport. */
-	OutputStream getOutputStream() throws IOException;
-
-	/**
-	 * Closes the writer and disposes of any associated resources. The
-	 * argument indicates whether the writer is being closed because of an
-	 * exception, which may affect how resources are disposed of.
-	 */
-	void dispose(boolean exception) throws IOException;
-}
diff --git a/briar-api/src/org/briarproject/api/transport/ConnectionDispatcher.java b/briar-api/src/org/briarproject/api/transport/ConnectionDispatcher.java
index 27f41ce3fad0057d2dd13c09f149bfa2ce34d2de..6b2058c38f459394f434876e72605154300c26cb 100644
--- a/briar-api/src/org/briarproject/api/transport/ConnectionDispatcher.java
+++ b/briar-api/src/org/briarproject/api/transport/ConnectionDispatcher.java
@@ -2,18 +2,18 @@ package org.briarproject.api.transport;
 
 import org.briarproject.api.ContactId;
 import org.briarproject.api.TransportId;
+import org.briarproject.api.plugins.TransportConnectionReader;
+import org.briarproject.api.plugins.TransportConnectionWriter;
 import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
-import org.briarproject.api.plugins.simplex.SimplexTransportReader;
-import org.briarproject.api.plugins.simplex.SimplexTransportWriter;
 
 public interface ConnectionDispatcher {
 
-	void dispatchIncomingConnection(TransportId t, SimplexTransportReader r);
+	void dispatchIncomingConnection(TransportId t, TransportConnectionReader r);
 
 	void dispatchIncomingConnection(TransportId t, DuplexTransportConnection d);
 
 	void dispatchOutgoingConnection(ContactId c, TransportId t,
-			SimplexTransportWriter w);
+			TransportConnectionWriter w);
 
 	void dispatchOutgoingConnection(ContactId c, TransportId t,
 			DuplexTransportConnection d);
diff --git a/briar-api/src/org/briarproject/api/transport/StreamReaderFactory.java b/briar-api/src/org/briarproject/api/transport/StreamReaderFactory.java
index 8c43c30eb539c8c90c769feb05b6a17e808f8f61..14459ec60a0d7e1c2e1f3e286fdaea0dd8554ea9 100644
--- a/briar-api/src/org/briarproject/api/transport/StreamReaderFactory.java
+++ b/briar-api/src/org/briarproject/api/transport/StreamReaderFactory.java
@@ -6,7 +6,7 @@ public interface StreamReaderFactory {
 
 	/** Creates a {@link StreamReader} for a transport connection. */
 	StreamReader createStreamReader(InputStream in, int maxFrameLength,
-			StreamContext ctx, boolean incoming, boolean initiator);
+			StreamContext ctx);
 
 	/** Creates a {@link StreamReader} for an invitation connection. */
 	StreamReader createInvitationStreamReader(InputStream in,
diff --git a/briar-api/src/org/briarproject/api/transport/StreamWriter.java b/briar-api/src/org/briarproject/api/transport/StreamWriter.java
index e5f50ecce8bbc229470a11367c7e7aef0052863b..2684742dbdf5889fcb382c8cadbaade65b2eb7f0 100644
--- a/briar-api/src/org/briarproject/api/transport/StreamWriter.java
+++ b/briar-api/src/org/briarproject/api/transport/StreamWriter.java
@@ -10,10 +10,4 @@ public interface StreamWriter {
 	 * be written.
 	 */
 	OutputStream getOutputStream();
-
-	/**
-	 * Returns the maximum number of bytes that can be written to the output
-	 * stream.
-	 */
-	long getRemainingCapacity();
 }
diff --git a/briar-api/src/org/briarproject/api/transport/StreamWriterFactory.java b/briar-api/src/org/briarproject/api/transport/StreamWriterFactory.java
index f3b8e1c2f86e5afdb364c42168797c4d6cce2b6d..f038a75221d52f0798ab9176ed353868d281d624 100644
--- a/briar-api/src/org/briarproject/api/transport/StreamWriterFactory.java
+++ b/briar-api/src/org/briarproject/api/transport/StreamWriterFactory.java
@@ -6,8 +6,7 @@ public interface StreamWriterFactory {
 
 	/** Creates a {@link StreamWriter} for a transport connection. */
 	StreamWriter createStreamWriter(OutputStream out, int maxFrameLength,
-			long capacity, StreamContext ctx, boolean incoming,
-			boolean initiator);
+			StreamContext ctx);
 
 	/** Creates a {@link StreamWriter} for an invitation connection. */
 	StreamWriter createInvitationStreamWriter(OutputStream out,
diff --git a/briar-api/src/org/briarproject/api/transport/TagRecogniser.java b/briar-api/src/org/briarproject/api/transport/TagRecogniser.java
index a6823a2e932a85f45bd6a79e1a32b4e8a9b22f2d..b73d096979d6f5a153e66c6a205c0355cdfec81f 100644
--- a/briar-api/src/org/briarproject/api/transport/TagRecogniser.java
+++ b/briar-api/src/org/briarproject/api/transport/TagRecogniser.java
@@ -4,12 +4,13 @@ import org.briarproject.api.ContactId;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.db.DbException;
 
-/** Maintains the table of expected tags for recognising incoming streams. */
+/** Keeps track of expected tags and uses them to recognise incoming streams. */
 public interface TagRecogniser {
 
 	/**
-	 * Returns a {@link StreamContext} for reading from the stream with the
-	 * given tag if the tag was expected, or null if the tag was unexpected.
+	 * Looks up the given tag and returns a {@link StreamContext} for reading
+	 * from the stream if the tag was expected, or null if the tag was
+	 * unexpected.
 	 */
 	StreamContext recogniseTag(TransportId t, byte[] tag) throws DbException;
 
diff --git a/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java b/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java
index dc4dbba95cef4dfd8486d6c24ed70a59d049620b..d616a50b72854df08239c071de91ed33687005be 100644
--- a/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java
+++ b/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java
@@ -76,14 +76,10 @@ class CryptoComponentImpl implements CryptoComponent {
 	// Labels for key derivation
 	private static final byte[] A_TAG = { 'A', '_', 'T', 'A', 'G', '\0' };
 	private static final byte[] B_TAG = { 'B', '_', 'T', 'A', 'G', '\0' };
-	private static final byte[] A_FRAME_A =
-		{ 'A', '_', 'F', 'R', 'A', 'M', 'E', '_', 'A', '\0' };
-	private static final byte[] A_FRAME_B =
-		{ 'A', '_', 'F', 'R', 'A', 'M', 'E', '_', 'B', '\0' };
-	private static final byte[] B_FRAME_A =
-		{ 'B', '_', 'F', 'R', 'A', 'M', 'E', '_', 'A', '\0' };
-	private static final byte[] B_FRAME_B =
-		{ 'B', '_', 'F', 'R', 'A', 'M', 'E', '_', 'B', '\0' };
+	private static final byte[] A_FRAME =
+		{ 'A', '_', 'F', 'R', 'A', 'M', 'E', '\0' };
+	private static final byte[] B_FRAME =
+		{ 'B', '_', 'F', 'R', 'A', 'M', 'E', '\0' };
 	// Blank secret for argument validation
 	private static final byte[] BLANK_SECRET = new byte[CIPHER_KEY_BYTES];
 
@@ -288,20 +284,15 @@ class CryptoComponentImpl implements CryptoComponent {
 	}
 
 	public SecretKey deriveFrameKey(byte[] secret, long streamNumber,
-			boolean alice, boolean initiator) {
+			boolean alice) {
 		if(secret.length != CIPHER_KEY_BYTES)
 			throw new IllegalArgumentException();
 		if(Arrays.equals(secret, BLANK_SECRET))
 			throw new IllegalArgumentException();
 		if(streamNumber < 0 || streamNumber > MAX_32_BIT_UNSIGNED)
 			throw new IllegalArgumentException();
-		if(alice) {
-			if(initiator) return deriveKey(secret, A_FRAME_A, streamNumber);
-			else return deriveKey(secret, A_FRAME_B, streamNumber);
-		} else {
-			if(initiator) return deriveKey(secret, B_FRAME_A, streamNumber);
-			else return deriveKey(secret, B_FRAME_B, streamNumber);
-		}
+		if(alice) return deriveKey(secret, A_FRAME, streamNumber);
+		else return deriveKey(secret, B_FRAME, streamNumber);
 	}
 
 	private SecretKey deriveKey(byte[] secret, byte[] label, long context) {
diff --git a/briar-core/src/org/briarproject/invitation/AliceConnector.java b/briar-core/src/org/briarproject/invitation/AliceConnector.java
index 455e1a6bda1dfd6bc09bb67a0836fb19f390f75d..955dc158872d56f80bcfa806f6dc674678342087 100644
--- a/briar-core/src/org/briarproject/invitation/AliceConnector.java
+++ b/briar-core/src/org/briarproject/invitation/AliceConnector.java
@@ -75,8 +75,8 @@ class AliceConnector extends Connector {
 		Writer w;
 		byte[] secret;
 		try {
-			in = conn.getInputStream();
-			out = conn.getOutputStream();
+			in = conn.getReader().getInputStream();
+			out = conn.getWriter().getOutputStream();
 			r = readerFactory.createReader(in);
 			w = writerFactory.createWriter(out);
 			// Alice goes first
@@ -130,7 +130,7 @@ class AliceConnector extends Connector {
 		// Confirmation succeeded - upgrade to a secure connection
 		if(LOG.isLoggable(INFO))
 			LOG.info(pluginName + " confirmation succeeded");
-		int maxFrameLength = conn.getMaxFrameLength();
+		int maxFrameLength = conn.getReader().getMaxFrameLength();
 		StreamReader streamReader =
 				streamReaderFactory.createInvitationStreamReader(in,
 						maxFrameLength, secret, false);
diff --git a/briar-core/src/org/briarproject/invitation/BobConnector.java b/briar-core/src/org/briarproject/invitation/BobConnector.java
index 584f5b81f972282a6f62c5304425eb11b26fb24b..4da2407eb3fce591147c8da2910dfd03ef497e08 100644
--- a/briar-core/src/org/briarproject/invitation/BobConnector.java
+++ b/briar-core/src/org/briarproject/invitation/BobConnector.java
@@ -69,8 +69,8 @@ class BobConnector extends Connector {
 		Writer w;
 		byte[] secret;
 		try {
-			in = conn.getInputStream();
-			out = conn.getOutputStream();
+			in = conn.getReader().getInputStream();
+			out = conn.getWriter().getOutputStream();
 			r = readerFactory.createReader(in);
 			w = writerFactory.createWriter(out);
 			// Alice goes first
@@ -130,7 +130,7 @@ class BobConnector extends Connector {
 		// Confirmation succeeded - upgrade to a secure connection
 		if(LOG.isLoggable(INFO))
 			LOG.info(pluginName + " confirmation succeeded");
-		int maxFrameLength = conn.getMaxFrameLength();
+		int maxFrameLength = conn.getReader().getMaxFrameLength();
 		StreamReader streamReader =
 				streamReaderFactory.createInvitationStreamReader(in,
 						maxFrameLength, secret, true);
diff --git a/briar-core/src/org/briarproject/invitation/Connector.java b/briar-core/src/org/briarproject/invitation/Connector.java
index 36b84860f532b7a30a68bd07a97425006468cc07..32347a130a0173f238c835ba5ed55836d1bdad8c 100644
--- a/briar-core/src/org/briarproject/invitation/Connector.java
+++ b/briar-core/src/org/briarproject/invitation/Connector.java
@@ -311,7 +311,8 @@ abstract class Connector extends Thread {
 			boolean exception) {
 		try {
 			LOG.info("Closing connection");
-			conn.dispose(exception, true);
+			conn.getReader().dispose(exception, true);
+			conn.getWriter().dispose(exception);
 		} catch(IOException e) {
 			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 		}
diff --git a/briar-core/src/org/briarproject/messaging/simplex/IncomingSimplexConnection.java b/briar-core/src/org/briarproject/messaging/IncomingSession.java
similarity index 62%
rename from briar-core/src/org/briarproject/messaging/simplex/IncomingSimplexConnection.java
rename to briar-core/src/org/briarproject/messaging/IncomingSession.java
index 72162747c2c5ac167a27d0f107d78dc290d174c1..37547d3cd9ce774fa16b0dd19a0dba4f26b1fd70 100644
--- a/briar-core/src/org/briarproject/messaging/simplex/IncomingSimplexConnection.java
+++ b/briar-core/src/org/briarproject/messaging/IncomingSession.java
@@ -1,4 +1,4 @@
-package org.briarproject.messaging.simplex;
+package org.briarproject.messaging;
 
 import static java.util.logging.Level.WARNING;
 
@@ -10,12 +10,12 @@ import java.util.logging.Logger;
 
 import org.briarproject.api.ContactId;
 import org.briarproject.api.FormatException;
-import org.briarproject.api.TransportId;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.messaging.Ack;
 import org.briarproject.api.messaging.Message;
 import org.briarproject.api.messaging.MessageVerifier;
+import org.briarproject.api.messaging.MessagingSession;
 import org.briarproject.api.messaging.PacketReader;
 import org.briarproject.api.messaging.PacketReaderFactory;
 import org.briarproject.api.messaging.RetentionAck;
@@ -25,103 +25,91 @@ import org.briarproject.api.messaging.SubscriptionUpdate;
 import org.briarproject.api.messaging.TransportAck;
 import org.briarproject.api.messaging.TransportUpdate;
 import org.briarproject.api.messaging.UnverifiedMessage;
-import org.briarproject.api.plugins.simplex.SimplexTransportReader;
-import org.briarproject.api.transport.ConnectionRegistry;
+import org.briarproject.api.plugins.TransportConnectionReader;
 import org.briarproject.api.transport.StreamContext;
 import org.briarproject.api.transport.StreamReader;
 import org.briarproject.api.transport.StreamReaderFactory;
-import org.briarproject.util.ByteUtils;
 
-class IncomingSimplexConnection {
+/**
+ * An incoming {@link org.briarproject.api.messaging.MessagingSession
+ * MessagingSession}.
+ */
+class IncomingSession implements MessagingSession {
 
 	private static final Logger LOG =
-			Logger.getLogger(IncomingSimplexConnection.class.getName());
+			Logger.getLogger(IncomingSession.class.getName());
 
+	private final DatabaseComponent db;
 	private final Executor dbExecutor, cryptoExecutor;
 	private final MessageVerifier messageVerifier;
-	private final DatabaseComponent db;
-	private final ConnectionRegistry connRegistry;
-	private final StreamReaderFactory connReaderFactory;
+	private final StreamReaderFactory streamReaderFactory;
 	private final PacketReaderFactory packetReaderFactory;
 	private final StreamContext ctx;
-	private final SimplexTransportReader transport;
+	private final TransportConnectionReader transportReader;
 	private final ContactId contactId;
-	private final TransportId transportId;
 
-	IncomingSimplexConnection(Executor dbExecutor, Executor cryptoExecutor,
-			MessageVerifier messageVerifier, DatabaseComponent db,
-			ConnectionRegistry connRegistry,
-			StreamReaderFactory connReaderFactory,
+	private volatile boolean interrupted = false;
+
+	IncomingSession(DatabaseComponent db, Executor dbExecutor,
+			Executor cryptoExecutor, MessageVerifier messageVerifier,
+			StreamReaderFactory streamReaderFactory,
 			PacketReaderFactory packetReaderFactory, StreamContext ctx,
-			SimplexTransportReader transport) {
+			TransportConnectionReader transportReader) {
+		this.db = db;
 		this.dbExecutor = dbExecutor;
 		this.cryptoExecutor = cryptoExecutor;
 		this.messageVerifier = messageVerifier;
-		this.db = db;
-		this.connRegistry = connRegistry;
-		this.connReaderFactory = connReaderFactory;
+		this.streamReaderFactory = streamReaderFactory;
 		this.packetReaderFactory = packetReaderFactory;
 		this.ctx = ctx;
-		this.transport = transport;
+		this.transportReader = transportReader;
 		contactId = ctx.getContactId();
-		transportId = ctx.getTransportId();
 	}
 
-	void read() {
-		connRegistry.registerConnection(contactId, transportId);
-		try {
-			InputStream in = transport.getInputStream();
-			int maxFrameLength = transport.getMaxFrameLength();
-			StreamReader conn = connReaderFactory.createStreamReader(in,
-					maxFrameLength, ctx, true, true);
-			in = conn.getInputStream();
-			PacketReader reader = packetReaderFactory.createPacketReader(in);
-			// Read packets until EOF
-			while(!reader.eof()) {
-				if(reader.hasAck()) {
-					Ack a = reader.readAck();
-					dbExecutor.execute(new ReceiveAck(a));
-				} else if(reader.hasMessage()) {
-					UnverifiedMessage m = reader.readMessage();
-					cryptoExecutor.execute(new VerifyMessage(m));
-				} else if(reader.hasRetentionAck()) {
-					RetentionAck a = reader.readRetentionAck();
-					dbExecutor.execute(new ReceiveRetentionAck(a));
-				} else if(reader.hasRetentionUpdate()) {
-					RetentionUpdate u = reader.readRetentionUpdate();
-					dbExecutor.execute(new ReceiveRetentionUpdate(u));
-				} else if(reader.hasSubscriptionAck()) {
-					SubscriptionAck a = reader.readSubscriptionAck();
-					dbExecutor.execute(new ReceiveSubscriptionAck(a));
-				} else if(reader.hasSubscriptionUpdate()) {
-					SubscriptionUpdate u = reader.readSubscriptionUpdate();
-					dbExecutor.execute(new ReceiveSubscriptionUpdate(u));
-				} else if(reader.hasTransportAck()) {
-					TransportAck a = reader.readTransportAck();
-					dbExecutor.execute(new ReceiveTransportAck(a));
-				} else if(reader.hasTransportUpdate()) {
-					TransportUpdate u = reader.readTransportUpdate();
-					dbExecutor.execute(new ReceiveTransportUpdate(u));
-				} else {
-					throw new FormatException();
-				}
+	public void run() throws IOException {
+		InputStream in = transportReader.getInputStream();
+		int maxFrameLength = transportReader.getMaxFrameLength();
+		StreamReader streamReader = streamReaderFactory.createStreamReader(in,
+				maxFrameLength, ctx);
+		in = streamReader.getInputStream();
+		PacketReader packetReader = packetReaderFactory.createPacketReader(in);
+		// Read packets until interrupted or EOF
+		while(!interrupted && !packetReader.eof()) {
+			if(packetReader.hasAck()) {
+				Ack a = packetReader.readAck();
+				dbExecutor.execute(new ReceiveAck(a));
+			} else if(packetReader.hasMessage()) {
+				UnverifiedMessage m = packetReader.readMessage();
+				cryptoExecutor.execute(new VerifyMessage(m));
+			} else if(packetReader.hasRetentionAck()) {
+				RetentionAck a = packetReader.readRetentionAck();
+				dbExecutor.execute(new ReceiveRetentionAck(a));
+			} else if(packetReader.hasRetentionUpdate()) {
+				RetentionUpdate u = packetReader.readRetentionUpdate();
+				dbExecutor.execute(new ReceiveRetentionUpdate(u));
+			} else if(packetReader.hasSubscriptionAck()) {
+				SubscriptionAck a = packetReader.readSubscriptionAck();
+				dbExecutor.execute(new ReceiveSubscriptionAck(a));
+			} else if(packetReader.hasSubscriptionUpdate()) {
+				SubscriptionUpdate u = packetReader.readSubscriptionUpdate();
+				dbExecutor.execute(new ReceiveSubscriptionUpdate(u));
+			} else if(packetReader.hasTransportAck()) {
+				TransportAck a = packetReader.readTransportAck();
+				dbExecutor.execute(new ReceiveTransportAck(a));
+			} else if(packetReader.hasTransportUpdate()) {
+				TransportUpdate u = packetReader.readTransportUpdate();
+				dbExecutor.execute(new ReceiveTransportUpdate(u));
+			} else {
+				throw new FormatException();
 			}
-			dispose(false, true);
-		} catch(IOException e) {
-			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			dispose(true, true);
-		} finally {
-			connRegistry.unregisterConnection(contactId, transportId);
 		}
+		in.close();
 	}
 
-	private void dispose(boolean exception, boolean recognised) {
-		ByteUtils.erase(ctx.getSecret());
-		try {
-			transport.dispose(exception, recognised);
-		} catch(IOException e) {
-			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-		}
+	public void interrupt() {
+		// This won't interrupt a blocking read, but the read will throw an
+		// exception when the transport connection is closed
+		interrupted = true;
 	}
 
 	private class ReceiveAck implements Runnable {
diff --git a/briar-core/src/org/briarproject/messaging/MessagingModule.java b/briar-core/src/org/briarproject/messaging/MessagingModule.java
index c2dde22025cbce2e348f5a84d1e0725ef581d51c..5986266b8dd50b2062b0499e2b31f18d04833926 100644
--- a/briar-core/src/org/briarproject/messaging/MessagingModule.java
+++ b/briar-core/src/org/briarproject/messaging/MessagingModule.java
@@ -1,5 +1,7 @@
 package org.briarproject.messaging;
 
+import javax.inject.Singleton;
+
 import org.briarproject.api.Author;
 import org.briarproject.api.AuthorFactory;
 import org.briarproject.api.crypto.CryptoComponent;
@@ -9,6 +11,7 @@ import org.briarproject.api.messaging.MessageFactory;
 import org.briarproject.api.messaging.MessageVerifier;
 import org.briarproject.api.messaging.PacketReaderFactory;
 import org.briarproject.api.messaging.PacketWriterFactory;
+import org.briarproject.api.messaging.MessagingSessionFactory;
 import org.briarproject.api.messaging.SubscriptionUpdate;
 import org.briarproject.api.messaging.UnverifiedMessage;
 import org.briarproject.api.serial.StructReader;
@@ -18,6 +21,7 @@ import com.google.inject.Provides;
 
 public class MessagingModule extends AbstractModule {
 
+	@Override
 	protected void configure() {
 		bind(AuthorFactory.class).to(AuthorFactoryImpl.class);
 		bind(GroupFactory.class).to(GroupFactoryImpl.class);
@@ -25,6 +29,8 @@ public class MessagingModule extends AbstractModule {
 		bind(MessageVerifier.class).to(MessageVerifierImpl.class);
 		bind(PacketReaderFactory.class).to(PacketReaderFactoryImpl.class);
 		bind(PacketWriterFactory.class).to(PacketWriterFactoryImpl.class);
+		bind(MessagingSessionFactory.class).to(
+				MessagingSessionFactoryImpl.class).in(Singleton.class);
 	}
 
 	@Provides
diff --git a/briar-core/src/org/briarproject/messaging/MessagingSessionFactoryImpl.java b/briar-core/src/org/briarproject/messaging/MessagingSessionFactoryImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..f8b3792a41af7bd8dae2b530a15cd832090146b4
--- /dev/null
+++ b/briar-core/src/org/briarproject/messaging/MessagingSessionFactoryImpl.java
@@ -0,0 +1,67 @@
+package org.briarproject.messaging;
+
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+
+import org.briarproject.api.crypto.CryptoExecutor;
+import org.briarproject.api.db.DatabaseComponent;
+import org.briarproject.api.db.DatabaseExecutor;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.messaging.MessageVerifier;
+import org.briarproject.api.messaging.MessagingSession;
+import org.briarproject.api.messaging.PacketReaderFactory;
+import org.briarproject.api.messaging.PacketWriterFactory;
+import org.briarproject.api.messaging.MessagingSessionFactory;
+import org.briarproject.api.plugins.TransportConnectionReader;
+import org.briarproject.api.plugins.TransportConnectionWriter;
+import org.briarproject.api.transport.StreamContext;
+import org.briarproject.api.transport.StreamReaderFactory;
+import org.briarproject.api.transport.StreamWriterFactory;
+
+class MessagingSessionFactoryImpl implements MessagingSessionFactory {
+
+	private final DatabaseComponent db;
+	private final Executor dbExecutor, cryptoExecutor;
+	private final MessageVerifier messageVerifier;
+	private final EventBus eventBus;
+	private final StreamReaderFactory streamReaderFactory;
+	private final StreamWriterFactory streamWriterFactory;
+	private final PacketReaderFactory packetReaderFactory;
+	private final PacketWriterFactory packetWriterFactory;
+
+	@Inject
+	MessagingSessionFactoryImpl(DatabaseComponent db,
+			@DatabaseExecutor Executor dbExecutor,
+			@CryptoExecutor Executor cryptoExecutor,
+			MessageVerifier messageVerifier, EventBus eventBus,
+			StreamReaderFactory streamReaderFactory,
+			StreamWriterFactory streamWriterFactory,
+			PacketReaderFactory packetReaderFactory,
+			PacketWriterFactory packetWriterFactory) {
+		this.db = db;
+		this.dbExecutor = dbExecutor;
+		this.cryptoExecutor = cryptoExecutor;
+		this.messageVerifier = messageVerifier;
+		this.eventBus = eventBus;
+		this.streamReaderFactory = streamReaderFactory;
+		this.streamWriterFactory = streamWriterFactory;
+		this.packetReaderFactory = packetReaderFactory;
+		this.packetWriterFactory = packetWriterFactory;
+	}
+
+	public MessagingSession createIncomingSession(StreamContext ctx,
+			TransportConnectionReader r) {
+		return new IncomingSession(db, dbExecutor, cryptoExecutor,
+				messageVerifier, streamReaderFactory, packetReaderFactory,
+				ctx, r);
+	}
+
+	public MessagingSession createOutgoingSession(StreamContext ctx,
+			TransportConnectionWriter w, boolean duplex) {
+		if(duplex) return new ReactiveOutgoingSession(db, dbExecutor, eventBus,
+				streamWriterFactory, packetWriterFactory, ctx, w);
+		else return new SinglePassOutgoingSession(db, dbExecutor,
+				streamWriterFactory, packetWriterFactory, ctx, w);
+	}
+}
diff --git a/briar-core/src/org/briarproject/messaging/ReactiveOutgoingSession.java b/briar-core/src/org/briarproject/messaging/ReactiveOutgoingSession.java
new file mode 100644
index 0000000000000000000000000000000000000000..c3eae51056073fa352e5d6df0b37c4cdd5fb20ff
--- /dev/null
+++ b/briar-core/src/org/briarproject/messaging/ReactiveOutgoingSession.java
@@ -0,0 +1,518 @@
+package org.briarproject.messaging;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.messaging.MessagingConstants.MAX_PACKET_LENGTH;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.logging.Logger;
+
+import org.briarproject.api.ContactId;
+import org.briarproject.api.db.DatabaseComponent;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.event.ContactRemovedEvent;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.event.EventListener;
+import org.briarproject.api.event.LocalSubscriptionsUpdatedEvent;
+import org.briarproject.api.event.LocalTransportsUpdatedEvent;
+import org.briarproject.api.event.MessageAddedEvent;
+import org.briarproject.api.event.MessageExpiredEvent;
+import org.briarproject.api.event.MessageRequestedEvent;
+import org.briarproject.api.event.MessageToAckEvent;
+import org.briarproject.api.event.MessageToRequestEvent;
+import org.briarproject.api.event.RemoteRetentionTimeUpdatedEvent;
+import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent;
+import org.briarproject.api.event.RemoteTransportsUpdatedEvent;
+import org.briarproject.api.event.TransportRemovedEvent;
+import org.briarproject.api.messaging.Ack;
+import org.briarproject.api.messaging.MessagingSession;
+import org.briarproject.api.messaging.Offer;
+import org.briarproject.api.messaging.PacketWriter;
+import org.briarproject.api.messaging.PacketWriterFactory;
+import org.briarproject.api.messaging.Request;
+import org.briarproject.api.messaging.RetentionAck;
+import org.briarproject.api.messaging.RetentionUpdate;
+import org.briarproject.api.messaging.SubscriptionAck;
+import org.briarproject.api.messaging.SubscriptionUpdate;
+import org.briarproject.api.messaging.TransportAck;
+import org.briarproject.api.messaging.TransportUpdate;
+import org.briarproject.api.plugins.TransportConnectionWriter;
+import org.briarproject.api.transport.StreamContext;
+import org.briarproject.api.transport.StreamWriter;
+import org.briarproject.api.transport.StreamWriterFactory;
+
+/**
+ * An outgoing {@link org.briarproject.api.messaging.MessagingSession
+ * MessagingSession} that keeps its output stream open and reacts to events
+ * that make packets available to send.
+ */
+class ReactiveOutgoingSession implements MessagingSession, EventListener {
+
+	private static final Logger LOG =
+			Logger.getLogger(ReactiveOutgoingSession.class.getName());
+
+	private static final ThrowingRunnable<IOException> CLOSE =
+			new ThrowingRunnable<IOException>() {
+		public void run() {}
+	};
+
+	private final DatabaseComponent db;
+	private final Executor dbExecutor;
+	private final EventBus eventBus;
+	private final StreamWriterFactory streamWriterFactory;
+	private final PacketWriterFactory packetWriterFactory;
+	private final StreamContext ctx;
+	private final TransportConnectionWriter transportWriter;
+	private final ContactId contactId;
+	private final long maxLatency;
+	private final BlockingQueue<ThrowingRunnable<IOException>> writerTasks;
+
+	private volatile PacketWriter packetWriter = null;
+	private volatile boolean interrupted = false;
+
+	ReactiveOutgoingSession(DatabaseComponent db, Executor dbExecutor,
+			EventBus eventBus, StreamWriterFactory streamWriterFactory,
+			PacketWriterFactory packetWriterFactory, StreamContext ctx,
+			TransportConnectionWriter transportWriter) {
+		this.db = db;
+		this.dbExecutor = dbExecutor;
+		this.eventBus = eventBus;
+		this.streamWriterFactory = streamWriterFactory;
+		this.packetWriterFactory = packetWriterFactory;
+		this.ctx = ctx;
+		this.transportWriter = transportWriter;
+		contactId = ctx.getContactId();
+		maxLatency = transportWriter.getMaxLatency();
+		writerTasks = new LinkedBlockingQueue<ThrowingRunnable<IOException>>();
+	}
+
+	public void run() throws IOException {
+		eventBus.addListener(this);
+		try {
+			OutputStream out = transportWriter.getOutputStream();
+			int maxFrameLength = transportWriter.getMaxFrameLength();
+			StreamWriter streamWriter = streamWriterFactory.createStreamWriter(
+					out, maxFrameLength, ctx);
+			out = streamWriter.getOutputStream();
+			packetWriter = packetWriterFactory.createPacketWriter(out, true);
+			// Start a query for each type of packet, in order of urgency
+			dbExecutor.execute(new GenerateTransportAcks());
+			dbExecutor.execute(new GenerateTransportUpdates());
+			dbExecutor.execute(new GenerateSubscriptionAck());
+			dbExecutor.execute(new GenerateSubscriptionUpdate());
+			dbExecutor.execute(new GenerateRetentionAck());
+			dbExecutor.execute(new GenerateRetentionUpdate());
+			dbExecutor.execute(new GenerateAck());
+			dbExecutor.execute(new GenerateBatch());
+			dbExecutor.execute(new GenerateOffer());
+			dbExecutor.execute(new GenerateRequest());
+			// Write packets until interrupted
+			try {
+				while(!interrupted) {
+					ThrowingRunnable<IOException> task = writerTasks.take();
+					if(task == CLOSE) break;
+					task.run();
+				}
+				out.flush();
+				out.close();
+			} catch(InterruptedException e) {
+				LOG.info("Interrupted while waiting for a packet to write");
+				Thread.currentThread().interrupt();
+			}
+		} finally {
+			eventBus.removeListener(this);
+		}
+	}
+
+	public void interrupt() {
+		interrupted = true;
+		writerTasks.add(CLOSE);
+	}
+
+	public void eventOccurred(Event e) {
+		if(e instanceof ContactRemovedEvent) {
+			ContactRemovedEvent c = (ContactRemovedEvent) e;
+			if(contactId.equals(c.getContactId())) {
+				LOG.info("Contact removed, closing");
+				interrupt();
+			}
+		} else if(e instanceof MessageAddedEvent) {
+			dbExecutor.execute(new GenerateOffer());
+		} else if(e instanceof MessageExpiredEvent) {
+			dbExecutor.execute(new GenerateRetentionUpdate());
+		} else if(e instanceof LocalSubscriptionsUpdatedEvent) {
+			LocalSubscriptionsUpdatedEvent l =
+					(LocalSubscriptionsUpdatedEvent) e;
+			if(l.getAffectedContacts().contains(contactId)) {
+				dbExecutor.execute(new GenerateSubscriptionUpdate());
+				dbExecutor.execute(new GenerateOffer());
+			}
+		} else if(e instanceof LocalTransportsUpdatedEvent) {
+			dbExecutor.execute(new GenerateTransportUpdates());
+		} else if(e instanceof MessageRequestedEvent) {
+			if(((MessageRequestedEvent) e).getContactId().equals(contactId))
+				dbExecutor.execute(new GenerateBatch());
+		} else if(e instanceof MessageToAckEvent) {
+			if(((MessageToAckEvent) e).getContactId().equals(contactId))
+				dbExecutor.execute(new GenerateAck());
+		} else if(e instanceof MessageToRequestEvent) {
+			if(((MessageToRequestEvent) e).getContactId().equals(contactId))
+				dbExecutor.execute(new GenerateRequest());
+		} else if(e instanceof RemoteRetentionTimeUpdatedEvent) {
+			dbExecutor.execute(new GenerateRetentionAck());
+		} else if(e instanceof RemoteSubscriptionsUpdatedEvent) {
+			dbExecutor.execute(new GenerateSubscriptionAck());
+			dbExecutor.execute(new GenerateOffer());
+		} else if(e instanceof RemoteTransportsUpdatedEvent) {
+			dbExecutor.execute(new GenerateTransportAcks());
+		} else if(e instanceof TransportRemovedEvent) {
+			TransportRemovedEvent t = (TransportRemovedEvent) e;
+			if(ctx.getTransportId().equals(t.getTransportId())) {
+				LOG.info("Transport removed, closing");
+				interrupt();
+			}
+		}
+	}
+
+	// This task runs on the database thread
+	private class GenerateAck implements Runnable {
+
+		public void run() {
+			int maxMessages = packetWriter.getMaxMessagesForAck(Long.MAX_VALUE);
+			try {
+				Ack a = db.generateAck(contactId, maxMessages);
+				if(LOG.isLoggable(INFO))
+					LOG.info("Generated ack: " + (a != null));
+				if(a != null) writerTasks.add(new WriteAck(a));
+			} catch(DbException e) {
+				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				interrupt();
+			}
+		}
+	}
+
+	// This task runs on the writer thread
+	private class WriteAck implements ThrowingRunnable<IOException> {
+
+		private final Ack ack;
+
+		private WriteAck(Ack ack) {
+			this.ack = ack;
+		}
+
+		public void run() throws IOException {
+			packetWriter.writeAck(ack);
+			LOG.info("Sent ack");
+			dbExecutor.execute(new GenerateAck());
+		}
+	}
+
+	// This task runs on the database thread
+	private class GenerateBatch implements Runnable {
+
+		public void run() {
+			try {
+				Collection<byte[]> b = db.generateRequestedBatch(contactId,
+						MAX_PACKET_LENGTH, maxLatency);
+				if(LOG.isLoggable(INFO))
+					LOG.info("Generated batch: " + (b != null));
+				if(b != null) writerTasks.add(new WriteBatch(b));
+			} catch(DbException e) {
+				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				interrupt();
+			}
+		}
+	}
+
+	// This task runs on the writer thread
+	private class WriteBatch implements ThrowingRunnable<IOException> {
+
+		private final Collection<byte[]> batch;
+
+		private WriteBatch(Collection<byte[]> batch) {
+			this.batch = batch;
+		}
+
+		public void run() throws IOException {
+			for(byte[] raw : batch) packetWriter.writeMessage(raw);
+			LOG.info("Sent batch");
+			dbExecutor.execute(new GenerateBatch());
+		}
+	}
+
+	// This task runs on the database thread
+	private class GenerateOffer implements Runnable {
+
+		public void run() {
+			int maxMessages = packetWriter.getMaxMessagesForOffer(
+					Long.MAX_VALUE);
+			try {
+				Offer o = db.generateOffer(contactId, maxMessages, maxLatency);
+				if(LOG.isLoggable(INFO))
+					LOG.info("Generated offer: " + (o != null));
+				if(o != null) writerTasks.add(new WriteOffer(o));
+			} catch(DbException e) {
+				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				interrupt();
+			}
+		}
+	}
+
+	// This task runs on the writer thread
+	private class WriteOffer implements ThrowingRunnable<IOException> {
+
+		private final Offer offer;
+
+		private WriteOffer(Offer offer) {
+			this.offer = offer;
+		}
+
+		public void run() throws IOException {
+			packetWriter.writeOffer(offer);
+			LOG.info("Sent offer");
+			dbExecutor.execute(new GenerateOffer());
+		}
+	}
+
+	// This task runs on the database thread
+	private class GenerateRequest implements Runnable {
+
+		public void run() {
+			int maxMessages = packetWriter.getMaxMessagesForRequest(
+					Long.MAX_VALUE);
+			try {
+				Request r = db.generateRequest(contactId, maxMessages);
+				if(LOG.isLoggable(INFO))
+					LOG.info("Generated request: " + (r != null));
+				if(r != null) writerTasks.add(new WriteRequest(r));
+			} catch(DbException e) {
+				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				interrupt();
+			}
+		}
+	}
+
+	// This task runs on the writer thread
+	private class WriteRequest implements ThrowingRunnable<IOException> {
+
+		private final Request request;
+
+		private WriteRequest(Request request) {
+			this.request = request;
+		}
+
+		public void run() throws IOException {
+			packetWriter.writeRequest(request);
+			LOG.info("Sent request");
+			dbExecutor.execute(new GenerateRequest());
+		}
+	}
+
+	// This task runs on the database thread
+	private class GenerateRetentionAck implements Runnable {
+
+		public void run() {
+			try {
+				RetentionAck a = db.generateRetentionAck(contactId);
+				if(LOG.isLoggable(INFO))
+					LOG.info("Generated retention ack: " + (a != null));
+				if(a != null) writerTasks.add(new WriteRetentionAck(a));
+			} catch(DbException e) {
+				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				interrupt();
+			}
+		}
+	}
+
+	// This tasks runs on the writer thread
+	private class WriteRetentionAck implements ThrowingRunnable<IOException> {
+
+		private final RetentionAck ack;
+
+		private WriteRetentionAck(RetentionAck ack) {
+			this.ack = ack;
+		}
+
+
+		public void run() throws IOException {
+			packetWriter.writeRetentionAck(ack);
+			LOG.info("Sent retention ack");
+			dbExecutor.execute(new GenerateRetentionAck());
+		}
+	}
+
+	// This task runs on the database thread
+	private class GenerateRetentionUpdate implements Runnable {
+
+		public void run() {
+			try {
+				RetentionUpdate u =
+						db.generateRetentionUpdate(contactId, maxLatency);
+				if(LOG.isLoggable(INFO))
+					LOG.info("Generated retention update: " + (u != null));
+				if(u != null) writerTasks.add(new WriteRetentionUpdate(u));
+			} catch(DbException e) {
+				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				interrupt();
+			}
+		}
+	}
+
+	// This task runs on the writer thread
+	private class WriteRetentionUpdate
+	implements ThrowingRunnable<IOException> {
+
+		private final RetentionUpdate update;
+
+		private WriteRetentionUpdate(RetentionUpdate update) {
+			this.update = update;
+		}
+
+		public void run() throws IOException {
+			packetWriter.writeRetentionUpdate(update);
+			LOG.info("Sent retention update");
+			dbExecutor.execute(new GenerateRetentionUpdate());
+		}
+	}
+
+	// This task runs on the database thread
+	private class GenerateSubscriptionAck implements Runnable {
+
+		public void run() {
+			try {
+				SubscriptionAck a = db.generateSubscriptionAck(contactId);
+				if(LOG.isLoggable(INFO))
+					LOG.info("Generated subscription ack: " + (a != null));
+				if(a != null) writerTasks.add(new WriteSubscriptionAck(a));
+			} catch(DbException e) {
+				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				interrupt();
+			}
+		}
+	}
+
+	// This tasks runs on the writer thread
+	private class WriteSubscriptionAck
+	implements ThrowingRunnable<IOException> {
+
+		private final SubscriptionAck ack;
+
+		private WriteSubscriptionAck(SubscriptionAck ack) {
+			this.ack = ack;
+		}
+
+		public void run() throws IOException {
+			packetWriter.writeSubscriptionAck(ack);
+			LOG.info("Sent subscription ack");
+			dbExecutor.execute(new GenerateSubscriptionAck());
+		}
+	}
+
+	// This task runs on the database thread
+	private class GenerateSubscriptionUpdate implements Runnable {
+
+		public void run() {
+			try {
+				SubscriptionUpdate u =
+						db.generateSubscriptionUpdate(contactId, maxLatency);
+				if(LOG.isLoggable(INFO))
+					LOG.info("Generated subscription update: " + (u != null));
+				if(u != null) writerTasks.add(new WriteSubscriptionUpdate(u));
+			} catch(DbException e) {
+				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				interrupt();
+			}
+		}
+	}
+
+	// This task runs on the writer thread
+	private class WriteSubscriptionUpdate
+	implements ThrowingRunnable<IOException> {
+
+		private final SubscriptionUpdate update;
+
+		private WriteSubscriptionUpdate(SubscriptionUpdate update) {
+			this.update = update;
+		}
+
+		public void run() throws IOException {
+			packetWriter.writeSubscriptionUpdate(update);
+			LOG.info("Sent subscription update");
+			dbExecutor.execute(new GenerateSubscriptionUpdate());
+		}
+	}
+
+	// This task runs on the database thread
+	private class GenerateTransportAcks implements Runnable {
+
+		public void run() {
+			try {
+				Collection<TransportAck> acks =
+						db.generateTransportAcks(contactId);
+				if(LOG.isLoggable(INFO))
+					LOG.info("Generated transport acks: " + (acks != null));
+				if(acks != null) writerTasks.add(new WriteTransportAcks(acks));
+			} catch(DbException e) {
+				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				interrupt();
+			}
+		}
+	}
+
+	// This tasks runs on the writer thread
+	private class WriteTransportAcks implements ThrowingRunnable<IOException> {
+
+		private final Collection<TransportAck> acks;
+
+		private WriteTransportAcks(Collection<TransportAck> acks) {
+			this.acks = acks;
+		}
+
+		public void run() throws IOException {
+			for(TransportAck a : acks) packetWriter.writeTransportAck(a);
+			LOG.info("Sent transport acks");
+			dbExecutor.execute(new GenerateTransportAcks());
+		}
+	}
+
+	// This task runs on the database thread
+	private class GenerateTransportUpdates implements Runnable {
+
+		public void run() {
+			try {
+				Collection<TransportUpdate> t =
+						db.generateTransportUpdates(contactId, maxLatency);
+				if(LOG.isLoggable(INFO))
+					LOG.info("Generated transport updates: " + (t != null));
+				if(t != null) writerTasks.add(new WriteTransportUpdates(t));
+			} catch(DbException e) {
+				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				interrupt();
+			}
+		}
+	}
+
+	// This task runs on the writer thread
+	private class WriteTransportUpdates
+	implements ThrowingRunnable<IOException> {
+
+		private final Collection<TransportUpdate> updates;
+
+		private WriteTransportUpdates(Collection<TransportUpdate> updates) {
+			this.updates = updates;
+		}
+
+		public void run() throws IOException {
+			for(TransportUpdate u : updates)
+				packetWriter.writeTransportUpdate(u);
+			LOG.info("Sent transport updates");
+			dbExecutor.execute(new GenerateTransportUpdates());
+		}
+	}
+}
diff --git a/briar-core/src/org/briarproject/messaging/SinglePassOutgoingSession.java b/briar-core/src/org/briarproject/messaging/SinglePassOutgoingSession.java
new file mode 100644
index 0000000000000000000000000000000000000000..c09829a92044d11ca1b3254a5fcdb513f55f187c
--- /dev/null
+++ b/briar-core/src/org/briarproject/messaging/SinglePassOutgoingSession.java
@@ -0,0 +1,395 @@
+package org.briarproject.messaging;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.messaging.MessagingConstants.MAX_PACKET_LENGTH;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Logger;
+
+import org.briarproject.api.ContactId;
+import org.briarproject.api.db.DatabaseComponent;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.messaging.Ack;
+import org.briarproject.api.messaging.MessagingSession;
+import org.briarproject.api.messaging.PacketWriter;
+import org.briarproject.api.messaging.PacketWriterFactory;
+import org.briarproject.api.messaging.RetentionAck;
+import org.briarproject.api.messaging.RetentionUpdate;
+import org.briarproject.api.messaging.SubscriptionAck;
+import org.briarproject.api.messaging.SubscriptionUpdate;
+import org.briarproject.api.messaging.TransportAck;
+import org.briarproject.api.messaging.TransportUpdate;
+import org.briarproject.api.plugins.TransportConnectionWriter;
+import org.briarproject.api.transport.StreamContext;
+import org.briarproject.api.transport.StreamWriter;
+import org.briarproject.api.transport.StreamWriterFactory;
+
+/**
+ * An outgoing {@link org.briarproject.api.messaging.MessagingSession
+ * MessagingSession} that closes its output stream when no more packets are
+ * available to send.
+ */
+class SinglePassOutgoingSession implements MessagingSession {
+
+	private static final Logger LOG =
+			Logger.getLogger(SinglePassOutgoingSession.class.getName());
+
+	private static final ThrowingRunnable<IOException> CLOSE =
+			new ThrowingRunnable<IOException>() {
+		public void run() {}
+	};
+
+	private final DatabaseComponent db;
+	private final Executor dbExecutor;
+	private final StreamWriterFactory streamWriterFactory;
+	private final PacketWriterFactory packetWriterFactory;
+	private final StreamContext ctx;
+	private final TransportConnectionWriter transportWriter;
+	private final ContactId contactId;
+	private final long maxLatency;
+	private final AtomicInteger outstandingQueries;
+	private final BlockingQueue<ThrowingRunnable<IOException>> writerTasks;
+
+	private volatile StreamWriter streamWriter = null;
+	private volatile PacketWriter packetWriter = null;
+	private volatile boolean interrupted = false;
+
+	SinglePassOutgoingSession(DatabaseComponent db, Executor dbExecutor,
+			StreamWriterFactory streamWriterFactory,
+			PacketWriterFactory packetWriterFactory, StreamContext ctx,
+			TransportConnectionWriter transportWriter) {
+		this.db = db;
+		this.dbExecutor = dbExecutor;
+		this.streamWriterFactory = streamWriterFactory;
+		this.packetWriterFactory = packetWriterFactory;
+		this.ctx = ctx;
+		this.transportWriter = transportWriter;
+		contactId = ctx.getContactId();
+		maxLatency = transportWriter.getMaxLatency();
+		outstandingQueries = new AtomicInteger(8); // One per type of packet
+		writerTasks = new LinkedBlockingQueue<ThrowingRunnable<IOException>>();
+	}
+
+	public void run() throws IOException {
+		OutputStream out = transportWriter.getOutputStream();
+		int maxFrameLength = transportWriter.getMaxFrameLength();
+		streamWriter = streamWriterFactory.createStreamWriter(out,
+				maxFrameLength, ctx);
+		out = streamWriter.getOutputStream();
+		packetWriter = packetWriterFactory.createPacketWriter(out, false);
+		// Start a query for each type of packet, in order of urgency
+		dbExecutor.execute(new GenerateTransportAcks());
+		dbExecutor.execute(new GenerateTransportUpdates());
+		dbExecutor.execute(new GenerateSubscriptionAck());
+		dbExecutor.execute(new GenerateSubscriptionUpdate());
+		dbExecutor.execute(new GenerateRetentionAck());
+		dbExecutor.execute(new GenerateRetentionUpdate());
+		dbExecutor.execute(new GenerateAck());
+		dbExecutor.execute(new GenerateBatch());
+		// Write packets until interrupted or there are no more packets to write
+		try {
+			while(!interrupted) {
+				ThrowingRunnable<IOException> task = writerTasks.take();
+				if(task == CLOSE) break;
+				task.run();
+			}
+			out.flush();
+			out.close();
+		} catch(InterruptedException e) {
+			LOG.info("Interrupted while waiting for a packet to write");
+			Thread.currentThread().interrupt();
+		}
+	}
+
+	public void interrupt() {
+		interrupted = true;
+		writerTasks.add(CLOSE);
+	}
+
+	private void decrementOutstandingQueries() {
+		if(outstandingQueries.decrementAndGet() == 0) writerTasks.add(CLOSE);
+	}
+
+	// This task runs on the database thread
+	private class GenerateAck implements Runnable {
+
+		public void run() {
+			int maxMessages = packetWriter.getMaxMessagesForAck(Long.MAX_VALUE);
+			try {
+				Ack a = db.generateAck(contactId, maxMessages);
+				if(LOG.isLoggable(INFO))
+					LOG.info("Generated ack: " + (a != null));
+				if(a == null) decrementOutstandingQueries();
+				else writerTasks.add(new WriteAck(a));
+			} catch(DbException e) {
+				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				interrupt();
+			}
+		}
+	}
+
+	// This task runs on the writer thread
+	private class WriteAck implements ThrowingRunnable<IOException> {
+
+		private final Ack ack;
+
+		private WriteAck(Ack ack) {
+			this.ack = ack;
+		}
+
+		public void run() throws IOException {
+			packetWriter.writeAck(ack);
+			LOG.info("Sent ack");
+			dbExecutor.execute(new GenerateAck());
+		}
+	}
+
+	// This task runs on the database thread
+	private class GenerateBatch implements Runnable {
+
+		public void run() {
+			try {
+				Collection<byte[]> b = db.generateBatch(contactId,
+						MAX_PACKET_LENGTH, maxLatency);
+				if(LOG.isLoggable(INFO))
+					LOG.info("Generated batch: " + (b != null));
+				if(b == null) decrementOutstandingQueries();
+				else writerTasks.add(new WriteBatch(b));
+			} catch(DbException e) {
+				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				interrupt();
+			}
+		}
+	}
+
+	// This task runs on the writer thread
+	private class WriteBatch implements ThrowingRunnable<IOException> {
+
+		private final Collection<byte[]> batch;
+
+		private WriteBatch(Collection<byte[]> batch) {
+			this.batch = batch;
+		}
+
+		public void run() throws IOException {
+			for(byte[] raw : batch) packetWriter.writeMessage(raw);
+			LOG.info("Sent batch");
+			dbExecutor.execute(new GenerateBatch());
+		}
+	}
+
+	// This task runs on the database thread
+	private class GenerateRetentionAck implements Runnable {
+
+		public void run() {
+			try {
+				RetentionAck a = db.generateRetentionAck(contactId);
+				if(LOG.isLoggable(INFO))
+					LOG.info("Generated retention ack: " + (a != null));
+				if(a == null) decrementOutstandingQueries();
+				else writerTasks.add(new WriteRetentionAck(a));
+			} catch(DbException e) {
+				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				interrupt();
+			}
+		}
+	}
+
+	// This tasks runs on the writer thread
+	private class WriteRetentionAck implements ThrowingRunnable<IOException> {
+
+		private final RetentionAck ack;
+
+		private WriteRetentionAck(RetentionAck ack) {
+			this.ack = ack;
+		}
+
+
+		public void run() throws IOException {
+			packetWriter.writeRetentionAck(ack);
+			LOG.info("Sent retention ack");
+			dbExecutor.execute(new GenerateRetentionAck());
+		}
+	}
+
+	// This task runs on the database thread
+	private class GenerateRetentionUpdate implements Runnable {
+
+		public void run() {
+			try {
+				RetentionUpdate u =
+						db.generateRetentionUpdate(contactId, maxLatency);
+				if(LOG.isLoggable(INFO))
+					LOG.info("Generated retention update: " + (u != null));
+				if(u == null) decrementOutstandingQueries();
+				else writerTasks.add(new WriteRetentionUpdate(u));
+			} catch(DbException e) {
+				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				interrupt();
+			}
+		}
+	}
+
+	// This task runs on the writer thread
+	private class WriteRetentionUpdate
+	implements ThrowingRunnable<IOException> {
+
+		private final RetentionUpdate update;
+
+		private WriteRetentionUpdate(RetentionUpdate update) {
+			this.update = update;
+		}
+
+		public void run() throws IOException {
+			packetWriter.writeRetentionUpdate(update);
+			LOG.info("Sent retention update");
+			dbExecutor.execute(new GenerateRetentionUpdate());
+		}
+	}
+
+	// This task runs on the database thread
+	private class GenerateSubscriptionAck implements Runnable {
+
+		public void run() {
+			try {
+				SubscriptionAck a = db.generateSubscriptionAck(contactId);
+				if(LOG.isLoggable(INFO))
+					LOG.info("Generated subscription ack: " + (a != null));
+				if(a == null) decrementOutstandingQueries();
+				else writerTasks.add(new WriteSubscriptionAck(a));
+			} catch(DbException e) {
+				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				interrupt();
+			}
+		}
+	}
+
+	// This tasks runs on the writer thread
+	private class WriteSubscriptionAck
+	implements ThrowingRunnable<IOException> {
+
+		private final SubscriptionAck ack;
+
+		private WriteSubscriptionAck(SubscriptionAck ack) {
+			this.ack = ack;
+		}
+
+		public void run() throws IOException {
+			packetWriter.writeSubscriptionAck(ack);
+			LOG.info("Sent subscription ack");
+			dbExecutor.execute(new GenerateSubscriptionAck());
+		}
+	}
+
+	// This task runs on the database thread
+	private class GenerateSubscriptionUpdate implements Runnable {
+
+		public void run() {
+			try {
+				SubscriptionUpdate u =
+						db.generateSubscriptionUpdate(contactId, maxLatency);
+				if(LOG.isLoggable(INFO))
+					LOG.info("Generated subscription update: " + (u != null));
+				if(u == null) decrementOutstandingQueries();
+				else writerTasks.add(new WriteSubscriptionUpdate(u));
+			} catch(DbException e) {
+				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				interrupt();
+			}
+		}
+	}
+
+	// This task runs on the writer thread
+	private class WriteSubscriptionUpdate
+	implements ThrowingRunnable<IOException> {
+
+		private final SubscriptionUpdate update;
+
+		private WriteSubscriptionUpdate(SubscriptionUpdate update) {
+			this.update = update;
+		}
+
+		public void run() throws IOException {
+			packetWriter.writeSubscriptionUpdate(update);
+			LOG.info("Sent subscription update");
+			dbExecutor.execute(new GenerateSubscriptionUpdate());
+		}
+	}
+
+	// This task runs on the database thread
+	private class GenerateTransportAcks implements Runnable {
+
+		public void run() {
+			try {
+				Collection<TransportAck> acks =
+						db.generateTransportAcks(contactId);
+				if(LOG.isLoggable(INFO))
+					LOG.info("Generated transport acks: " + (acks != null));
+				if(acks == null) decrementOutstandingQueries();
+				else writerTasks.add(new WriteTransportAcks(acks));
+			} catch(DbException e) {
+				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				interrupt();
+			}
+		}
+	}
+
+	// This tasks runs on the writer thread
+	private class WriteTransportAcks implements ThrowingRunnable<IOException> {
+
+		private final Collection<TransportAck> acks;
+
+		private WriteTransportAcks(Collection<TransportAck> acks) {
+			this.acks = acks;
+		}
+
+		public void run() throws IOException {
+			for(TransportAck a : acks) packetWriter.writeTransportAck(a);
+			LOG.info("Sent transport acks");
+			dbExecutor.execute(new GenerateTransportAcks());
+		}
+	}
+
+	// This task runs on the database thread
+	private class GenerateTransportUpdates implements Runnable {
+
+		public void run() {
+			try {
+				Collection<TransportUpdate> t =
+						db.generateTransportUpdates(contactId, maxLatency);
+				if(LOG.isLoggable(INFO))
+					LOG.info("Generated transport updates: " + (t != null));
+				if(t == null) decrementOutstandingQueries();
+				else writerTasks.add(new WriteTransportUpdates(t));
+			} catch(DbException e) {
+				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				interrupt();
+			}
+		}
+	}
+
+	// This task runs on the writer thread
+	private class WriteTransportUpdates
+	implements ThrowingRunnable<IOException> {
+
+		private final Collection<TransportUpdate> updates;
+
+		private WriteTransportUpdates(Collection<TransportUpdate> updates) {
+			this.updates = updates;
+		}
+
+		public void run() throws IOException {
+			for(TransportUpdate u : updates)
+				packetWriter.writeTransportUpdate(u);
+			LOG.info("Sent transport updates");
+			dbExecutor.execute(new GenerateTransportUpdates());
+		}
+	}
+}
diff --git a/briar-core/src/org/briarproject/messaging/ThrowingRunnable.java b/briar-core/src/org/briarproject/messaging/ThrowingRunnable.java
new file mode 100644
index 0000000000000000000000000000000000000000..334581daac6631e0e3245299d2762cf359fecf60
--- /dev/null
+++ b/briar-core/src/org/briarproject/messaging/ThrowingRunnable.java
@@ -0,0 +1,6 @@
+package org.briarproject.messaging;
+
+interface ThrowingRunnable<T extends Throwable> {
+
+	public void run() throws T;
+}
diff --git a/briar-core/src/org/briarproject/messaging/duplex/DuplexConnection.java b/briar-core/src/org/briarproject/messaging/duplex/DuplexConnection.java
deleted file mode 100644
index 0ba89f29fd93a0b649a9586b507c10bbe71af0da..0000000000000000000000000000000000000000
--- a/briar-core/src/org/briarproject/messaging/duplex/DuplexConnection.java
+++ /dev/null
@@ -1,871 +0,0 @@
-package org.briarproject.messaging.duplex;
-
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.api.messaging.MessagingConstants.MAX_PACKET_LENGTH;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.security.GeneralSecurityException;
-import java.util.Collection;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.Executor;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.logging.Logger;
-
-import org.briarproject.api.ContactId;
-import org.briarproject.api.FormatException;
-import org.briarproject.api.TransportId;
-import org.briarproject.api.db.DatabaseComponent;
-import org.briarproject.api.db.DbException;
-import org.briarproject.api.event.ContactRemovedEvent;
-import org.briarproject.api.event.Event;
-import org.briarproject.api.event.EventBus;
-import org.briarproject.api.event.EventListener;
-import org.briarproject.api.event.LocalSubscriptionsUpdatedEvent;
-import org.briarproject.api.event.LocalTransportsUpdatedEvent;
-import org.briarproject.api.event.MessageAddedEvent;
-import org.briarproject.api.event.MessageExpiredEvent;
-import org.briarproject.api.event.MessageRequestedEvent;
-import org.briarproject.api.event.MessageToAckEvent;
-import org.briarproject.api.event.MessageToRequestEvent;
-import org.briarproject.api.event.RemoteRetentionTimeUpdatedEvent;
-import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent;
-import org.briarproject.api.event.RemoteTransportsUpdatedEvent;
-import org.briarproject.api.messaging.Ack;
-import org.briarproject.api.messaging.Message;
-import org.briarproject.api.messaging.MessageVerifier;
-import org.briarproject.api.messaging.Offer;
-import org.briarproject.api.messaging.PacketReader;
-import org.briarproject.api.messaging.PacketReaderFactory;
-import org.briarproject.api.messaging.PacketWriter;
-import org.briarproject.api.messaging.PacketWriterFactory;
-import org.briarproject.api.messaging.Request;
-import org.briarproject.api.messaging.RetentionAck;
-import org.briarproject.api.messaging.RetentionUpdate;
-import org.briarproject.api.messaging.SubscriptionAck;
-import org.briarproject.api.messaging.SubscriptionUpdate;
-import org.briarproject.api.messaging.TransportAck;
-import org.briarproject.api.messaging.TransportUpdate;
-import org.briarproject.api.messaging.UnverifiedMessage;
-import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
-import org.briarproject.api.transport.ConnectionRegistry;
-import org.briarproject.api.transport.StreamContext;
-import org.briarproject.api.transport.StreamReader;
-import org.briarproject.api.transport.StreamReaderFactory;
-import org.briarproject.api.transport.StreamWriter;
-import org.briarproject.api.transport.StreamWriterFactory;
-import org.briarproject.util.ByteUtils;
-
-abstract class DuplexConnection implements EventListener {
-
-	private static final Logger LOG =
-			Logger.getLogger(DuplexConnection.class.getName());
-
-	private static final Runnable CLOSE = new Runnable() {
-		public void run() {}
-	};
-
-	private static final Runnable DIE = new Runnable() {
-		public void run() {}
-	};
-
-	protected final DatabaseComponent db;
-	protected final EventBus eventBus;
-	protected final ConnectionRegistry connRegistry;
-	protected final StreamReaderFactory connReaderFactory;
-	protected final StreamWriterFactory connWriterFactory;
-	protected final PacketReaderFactory packetReaderFactory;
-	protected final PacketWriterFactory packetWriterFactory;
-	protected final StreamContext ctx;
-	protected final DuplexTransportConnection transport;
-	protected final ContactId contactId;
-	protected final TransportId transportId;
-
-	private final Executor dbExecutor, cryptoExecutor;
-	private final MessageVerifier messageVerifier;
-	private final long maxLatency;
-	private final AtomicBoolean disposed;
-	private final BlockingQueue<Runnable> writerTasks;
-
-	private volatile PacketWriter writer = null;
-
-	DuplexConnection(Executor dbExecutor, Executor cryptoExecutor,
-			MessageVerifier messageVerifier, DatabaseComponent db,
-			EventBus eventBus, ConnectionRegistry connRegistry,
-			StreamReaderFactory connReaderFactory,
-			StreamWriterFactory connWriterFactory,
-			PacketReaderFactory packetReaderFactory,
-			PacketWriterFactory packetWriterFactory, StreamContext ctx,
-			DuplexTransportConnection transport) {
-		this.dbExecutor = dbExecutor;
-		this.cryptoExecutor = cryptoExecutor;
-		this.messageVerifier = messageVerifier;
-		this.db = db;
-		this.eventBus = eventBus;
-		this.connRegistry = connRegistry;
-		this.connReaderFactory = connReaderFactory;
-		this.connWriterFactory = connWriterFactory;
-		this.packetReaderFactory = packetReaderFactory;
-		this.packetWriterFactory = packetWriterFactory;
-		this.ctx = ctx;
-		this.transport = transport;
-		contactId = ctx.getContactId();
-		transportId = ctx.getTransportId();
-		maxLatency = transport.getMaxLatency();
-		disposed = new AtomicBoolean(false);
-		writerTasks = new LinkedBlockingQueue<Runnable>();
-	}
-
-	protected abstract StreamReader createStreamReader() throws IOException;
-
-	protected abstract StreamWriter createStreamWriter() throws IOException;
-
-	public void eventOccurred(Event e) {
-		if(e instanceof ContactRemovedEvent) {
-			ContactRemovedEvent c = (ContactRemovedEvent) e;
-			if(contactId.equals(c.getContactId())) writerTasks.add(CLOSE);
-		} else if(e instanceof MessageAddedEvent) {
-			dbExecutor.execute(new GenerateOffer());
-		} else if(e instanceof MessageExpiredEvent) {
-			dbExecutor.execute(new GenerateRetentionUpdate());
-		} else if(e instanceof LocalSubscriptionsUpdatedEvent) {
-			LocalSubscriptionsUpdatedEvent l =
-					(LocalSubscriptionsUpdatedEvent) e;
-			if(l.getAffectedContacts().contains(contactId)) {
-				dbExecutor.execute(new GenerateSubscriptionUpdate());
-				dbExecutor.execute(new GenerateOffer());
-			}
-		} else if(e instanceof LocalTransportsUpdatedEvent) {
-			dbExecutor.execute(new GenerateTransportUpdates());
-		} else if(e instanceof MessageRequestedEvent) {
-			if(((MessageRequestedEvent) e).getContactId().equals(contactId))
-				dbExecutor.execute(new GenerateBatch());
-		} else if(e instanceof MessageToAckEvent) {
-			if(((MessageToAckEvent) e).getContactId().equals(contactId))
-				dbExecutor.execute(new GenerateAck());
-		} else if(e instanceof MessageToRequestEvent) {
-			if(((MessageToRequestEvent) e).getContactId().equals(contactId))
-				dbExecutor.execute(new GenerateRequest());
-		} else if(e instanceof RemoteRetentionTimeUpdatedEvent) {
-			dbExecutor.execute(new GenerateRetentionAck());
-		} else if(e instanceof RemoteSubscriptionsUpdatedEvent) {
-			dbExecutor.execute(new GenerateSubscriptionAck());
-			dbExecutor.execute(new GenerateOffer());
-		} else if(e instanceof RemoteTransportsUpdatedEvent) {
-			dbExecutor.execute(new GenerateTransportAcks());
-		}
-	}
-
-	void read() {
-		try {
-			InputStream in = createStreamReader().getInputStream();
-			PacketReader reader = packetReaderFactory.createPacketReader(in);
-			LOG.info("Starting to read");
-			while(!reader.eof()) {
-				if(reader.hasAck()) {
-					Ack a = reader.readAck();
-					LOG.info("Received ack");
-					dbExecutor.execute(new ReceiveAck(a));
-				} else if(reader.hasMessage()) {
-					UnverifiedMessage m = reader.readMessage();
-					LOG.info("Received message");
-					cryptoExecutor.execute(new VerifyMessage(m));
-				} else if(reader.hasOffer()) {
-					Offer o = reader.readOffer();
-					LOG.info("Received offer");
-					dbExecutor.execute(new ReceiveOffer(o));
-				} else if(reader.hasRequest()) {
-					Request r = reader.readRequest();
-					LOG.info("Received request");
-					dbExecutor.execute(new ReceiveRequest(r));
-				} else if(reader.hasRetentionAck()) {
-					RetentionAck a = reader.readRetentionAck();
-					LOG.info("Received retention ack");
-					dbExecutor.execute(new ReceiveRetentionAck(a));
-				} else if(reader.hasRetentionUpdate()) {
-					RetentionUpdate u = reader.readRetentionUpdate();
-					LOG.info("Received retention update");
-					dbExecutor.execute(new ReceiveRetentionUpdate(u));
-				} else if(reader.hasSubscriptionAck()) {
-					SubscriptionAck a = reader.readSubscriptionAck();
-					LOG.info("Received subscription ack");
-					dbExecutor.execute(new ReceiveSubscriptionAck(a));
-				} else if(reader.hasSubscriptionUpdate()) {
-					SubscriptionUpdate u = reader.readSubscriptionUpdate();
-					LOG.info("Received subscription update");
-					dbExecutor.execute(new ReceiveSubscriptionUpdate(u));
-				} else if(reader.hasTransportAck()) {
-					TransportAck a = reader.readTransportAck();
-					LOG.info("Received transport ack");
-					dbExecutor.execute(new ReceiveTransportAck(a));
-				} else if(reader.hasTransportUpdate()) {
-					TransportUpdate u = reader.readTransportUpdate();
-					LOG.info("Received transport update");
-					dbExecutor.execute(new ReceiveTransportUpdate(u));
-				} else {
-					throw new FormatException();
-				}
-			}
-			LOG.info("Finished reading");
-			writerTasks.add(CLOSE);
-		} catch(IOException e) {
-			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			writerTasks.add(DIE);
-		}
-	}
-
-	void write() {
-		connRegistry.registerConnection(contactId, transportId);
-		eventBus.addListener(this);
-		try {
-			OutputStream out = createStreamWriter().getOutputStream();
-			writer = packetWriterFactory.createPacketWriter(out, true);
-			LOG.info("Starting to write");
-			// Ensure the tag is sent
-			out.flush();
-			// Send the initial packets
-			dbExecutor.execute(new GenerateTransportAcks());
-			dbExecutor.execute(new GenerateTransportUpdates());
-			dbExecutor.execute(new GenerateSubscriptionAck());
-			dbExecutor.execute(new GenerateSubscriptionUpdate());
-			dbExecutor.execute(new GenerateRetentionAck());
-			dbExecutor.execute(new GenerateRetentionUpdate());
-			dbExecutor.execute(new GenerateAck());
-			dbExecutor.execute(new GenerateBatch());
-			dbExecutor.execute(new GenerateOffer());
-			dbExecutor.execute(new GenerateRequest());
-			// Main loop
-			Runnable task = null;
-			while(true) {
-				LOG.info("Waiting for something to write");
-				task = writerTasks.take();
-				if(task == CLOSE || task == DIE) break;
-				task.run();
-			}
-			LOG.info("Finished writing");
-			if(task == CLOSE) {
-				writer.flush();
-				writer.close();
-				dispose(false, true);
-			} else {
-				dispose(true, true);
-			}
-		} catch(InterruptedException e) {
-			LOG.warning("Interrupted while waiting for task");
-			Thread.currentThread().interrupt();
-			dispose(true, true);
-		} catch(IOException e) {
-			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			dispose(true, true);
-		}
-		eventBus.removeListener(this);
-		connRegistry.unregisterConnection(contactId, transportId);
-	}
-
-	private void dispose(boolean exception, boolean recognised) {
-		if(disposed.getAndSet(true)) return;
-		if(LOG.isLoggable(INFO))
-			LOG.info("Disposing: " + exception + ", " + recognised);
-		ByteUtils.erase(ctx.getSecret());
-		try {
-			transport.dispose(exception, recognised);
-		} catch(IOException e) {
-			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-		}
-	}
-
-	// This task runs on the database thread
-	private class ReceiveAck implements Runnable {
-
-		private final Ack ack;
-
-		private ReceiveAck(Ack ack) {
-			this.ack = ack;
-		}
-
-		public void run() {
-			try {
-				db.receiveAck(contactId, ack);
-				LOG.info("DB received ack");
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-	}
-
-	// This task runs on a crypto thread
-	private class VerifyMessage implements Runnable {
-
-		private final UnverifiedMessage message;
-
-		private VerifyMessage(UnverifiedMessage message) {
-			this.message = message;
-		}
-
-		public void run() {
-			try {
-				Message m = messageVerifier.verifyMessage(message);
-				LOG.info("Verified message");
-				dbExecutor.execute(new ReceiveMessage(m));
-			} catch(GeneralSecurityException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-	}
-
-	// This task runs on the database thread
-	private class ReceiveMessage implements Runnable {
-
-		private final Message message;
-
-		private ReceiveMessage(Message message) {
-			this.message = message;
-		}
-
-		public void run() {
-			try {
-				db.receiveMessage(contactId, message);
-				LOG.info("DB received message");
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-	}
-
-	// This task runs on the database thread
-	private class ReceiveOffer implements Runnable {
-
-		private final Offer offer;
-
-		private ReceiveOffer(Offer offer) {
-			this.offer = offer;
-		}
-
-		public void run() {
-			try {
-				db.receiveOffer(contactId, offer);
-				LOG.info("DB received offer");
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-	}
-
-	// This task runs on the database thread
-	private class ReceiveRequest implements Runnable {
-
-		private final Request request;
-
-		private ReceiveRequest(Request request) {
-			this.request = request;
-		}
-
-		public void run() {
-			try {
-				db.receiveRequest(contactId, request);
-				LOG.info("DB received request");
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-	}
-
-	// This task runs on the database thread
-	private class ReceiveRetentionAck implements Runnable {
-
-		private final RetentionAck ack;
-
-		private ReceiveRetentionAck(RetentionAck ack) {
-			this.ack = ack;
-		}
-
-		public void run() {
-			try {
-				db.receiveRetentionAck(contactId, ack);
-				LOG.info("DB received retention ack");
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-	}
-
-	// This task runs on the database thread
-	private class ReceiveRetentionUpdate implements Runnable {
-
-		private final RetentionUpdate update;
-
-		private ReceiveRetentionUpdate(RetentionUpdate update) {
-			this.update = update;
-		}
-
-		public void run() {
-			try {
-				db.receiveRetentionUpdate(contactId, update);
-				LOG.info("DB received retention update");
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-	}
-
-	// This task runs on the database thread
-	private class ReceiveSubscriptionAck implements Runnable {
-
-		private final SubscriptionAck ack;
-
-		private ReceiveSubscriptionAck(SubscriptionAck ack) {
-			this.ack = ack;
-		}
-
-		public void run() {
-			try {
-				db.receiveSubscriptionAck(contactId, ack);
-				LOG.info("DB received subscription ack");
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-	}
-
-	// This task runs on the database thread
-	private class ReceiveSubscriptionUpdate implements Runnable {
-
-		private final SubscriptionUpdate update;
-
-		private ReceiveSubscriptionUpdate(SubscriptionUpdate update) {
-			this.update = update;
-		}
-
-		public void run() {
-			try {
-				db.receiveSubscriptionUpdate(contactId, update);
-				LOG.info("DB received subscription update");
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-	}
-
-	// This task runs on the database thread
-	private class ReceiveTransportAck implements Runnable {
-
-		private final TransportAck ack;
-
-		private ReceiveTransportAck(TransportAck ack) {
-			this.ack = ack;
-		}
-
-		public void run() {
-			try {
-				db.receiveTransportAck(contactId, ack);
-				LOG.info("DB received transport ack");
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-	}
-
-	// This task runs on the database thread
-	private class ReceiveTransportUpdate implements Runnable {
-
-		private final TransportUpdate update;
-
-		private ReceiveTransportUpdate(TransportUpdate update) {
-			this.update = update;
-		}
-
-		public void run() {
-			try {
-				db.receiveTransportUpdate(contactId, update);
-				LOG.info("DB received transport update");
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-	}
-
-	// This task runs on the database thread
-	private class GenerateAck implements Runnable {
-
-		public void run() {
-			assert writer != null;
-			int maxMessages = writer.getMaxMessagesForAck(Long.MAX_VALUE);
-			try {
-				Ack a = db.generateAck(contactId, maxMessages);
-				if(LOG.isLoggable(INFO))
-					LOG.info("Generated ack: " + (a != null));
-				if(a != null) writerTasks.add(new WriteAck(a));
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-	}
-
-	// This task runs on the writer thread
-	private class WriteAck implements Runnable {
-
-		private final Ack ack;
-
-		private WriteAck(Ack ack) {
-			this.ack = ack;
-		}
-
-		public void run() {
-			assert writer != null;
-			try {
-				writer.writeAck(ack);
-				LOG.info("Sent ack");
-				dbExecutor.execute(new GenerateAck());
-			} catch(IOException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				dispose(true, true);
-			}
-		}
-	}
-
-	// This task runs on the database thread
-	private class GenerateBatch implements Runnable {
-
-		public void run() {
-			assert writer != null;
-			try {
-				Collection<byte[]> b = db.generateRequestedBatch(contactId,
-						MAX_PACKET_LENGTH, maxLatency);
-				if(LOG.isLoggable(INFO))
-					LOG.info("Generated batch: " + (b != null));
-				if(b != null) writerTasks.add(new WriteBatch(b));
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-	}
-
-	// This task runs on the writer thread
-	private class WriteBatch implements Runnable {
-
-		private final Collection<byte[]> batch;
-
-		private WriteBatch(Collection<byte[]> batch) {
-			this.batch = batch;
-		}
-
-		public void run() {
-			assert writer != null;
-			try {
-				for(byte[] raw : batch) writer.writeMessage(raw);
-				LOG.info("Sent batch");
-				dbExecutor.execute(new GenerateBatch());
-			} catch(IOException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				dispose(true, true);
-			}
-		}
-	}
-
-	// This task runs on the database thread
-	private class GenerateOffer implements Runnable {
-
-		public void run() {
-			assert writer != null;
-			int maxMessages = writer.getMaxMessagesForOffer(Long.MAX_VALUE);
-			try {
-				Offer o = db.generateOffer(contactId, maxMessages, maxLatency);
-				if(LOG.isLoggable(INFO))
-					LOG.info("Generated offer: " + (o != null));
-				if(o != null) writerTasks.add(new WriteOffer(o));
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-	}
-
-	// This task runs on the writer thread
-	private class WriteOffer implements Runnable {
-
-		private final Offer offer;
-
-		private WriteOffer(Offer offer) {
-			this.offer = offer;
-		}
-
-		public void run() {
-			assert writer != null;
-			try {
-				writer.writeOffer(offer);
-				LOG.info("Sent offer");
-				dbExecutor.execute(new GenerateOffer());
-			} catch(IOException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				dispose(true, true);
-			}
-		}
-	}
-
-	// This task runs on the database thread
-	private class GenerateRequest implements Runnable {
-
-		public void run() {
-			assert writer != null;
-			int maxMessages = writer.getMaxMessagesForRequest(Long.MAX_VALUE);
-			try {
-				Request r = db.generateRequest(contactId, maxMessages);
-				if(LOG.isLoggable(INFO))
-					LOG.info("Generated request: " + (r != null));
-				if(r != null) writerTasks.add(new WriteRequest(r));
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-	}
-
-	// This task runs on the writer thread
-	private class WriteRequest implements Runnable {
-
-		private final Request request;
-
-		private WriteRequest(Request request) {
-			this.request = request;
-		}
-
-		public void run() {
-			assert writer != null;
-			try {
-				writer.writeRequest(request);
-				LOG.info("Sent request");
-				dbExecutor.execute(new GenerateRequest());
-			} catch(IOException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				dispose(true, true);
-			}
-		}
-	}
-
-	// This task runs on the database thread
-	private class GenerateRetentionAck implements Runnable {
-
-		public void run() {
-			try {
-				RetentionAck a = db.generateRetentionAck(contactId);
-				if(LOG.isLoggable(INFO))
-					LOG.info("Generated retention ack: " + (a != null));
-				if(a != null) writerTasks.add(new WriteRetentionAck(a));
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-	}
-
-	// This tasks runs on the writer thread
-	private class WriteRetentionAck implements Runnable {
-
-		private final RetentionAck ack;
-
-		private WriteRetentionAck(RetentionAck ack) {
-			this.ack = ack;
-		}
-
-		public void run() {
-			assert writer != null;
-			try {
-				writer.writeRetentionAck(ack);
-				LOG.info("Sent retention ack");
-				dbExecutor.execute(new GenerateRetentionAck());
-			} catch(IOException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				dispose(true, true);
-			}
-		}
-	}
-
-	// This task runs on the database thread
-	private class GenerateRetentionUpdate implements Runnable {
-
-		public void run() {
-			try {
-				RetentionUpdate u =
-						db.generateRetentionUpdate(contactId, maxLatency);
-				if(LOG.isLoggable(INFO))
-					LOG.info("Generated retention update: " + (u != null));
-				if(u != null) writerTasks.add(new WriteRetentionUpdate(u));
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-	}
-
-	// This task runs on the writer thread
-	private class WriteRetentionUpdate implements Runnable {
-
-		private final RetentionUpdate update;
-
-		private WriteRetentionUpdate(RetentionUpdate update) {
-			this.update = update;
-		}
-
-		public void run() {
-			assert writer != null;
-			try {
-				writer.writeRetentionUpdate(update);
-				LOG.info("Sent retention update");
-				dbExecutor.execute(new GenerateRetentionUpdate());
-			} catch(IOException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				dispose(true, true);
-			}
-		}
-	}
-
-	// This task runs on the database thread
-	private class GenerateSubscriptionAck implements Runnable {
-
-		public void run() {
-			try {
-				SubscriptionAck a = db.generateSubscriptionAck(contactId);
-				if(LOG.isLoggable(INFO))
-					LOG.info("Generated subscription ack: " + (a != null));
-				if(a != null) writerTasks.add(new WriteSubscriptionAck(a));
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-	}
-
-	// This tasks runs on the writer thread
-	private class WriteSubscriptionAck implements Runnable {
-
-		private final SubscriptionAck ack;
-
-		private WriteSubscriptionAck(SubscriptionAck ack) {
-			this.ack = ack;
-		}
-
-		public void run() {
-			assert writer != null;
-			try {
-				writer.writeSubscriptionAck(ack);
-				LOG.info("Sent subscription ack");
-				dbExecutor.execute(new GenerateSubscriptionAck());
-			} catch(IOException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				dispose(true, true);
-			}
-		}
-	}
-
-	// This task runs on the database thread
-	private class GenerateSubscriptionUpdate implements Runnable {
-
-		public void run() {
-			try {
-				SubscriptionUpdate u =
-						db.generateSubscriptionUpdate(contactId, maxLatency);
-				if(LOG.isLoggable(INFO))
-					LOG.info("Generated subscription update: " + (u != null));
-				if(u != null) writerTasks.add(new WriteSubscriptionUpdate(u));
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-	}
-
-	// This task runs on the writer thread
-	private class WriteSubscriptionUpdate implements Runnable {
-
-		private final SubscriptionUpdate update;
-
-		private WriteSubscriptionUpdate(SubscriptionUpdate update) {
-			this.update = update;
-		}
-
-		public void run() {
-			assert writer != null;
-			try {
-				writer.writeSubscriptionUpdate(update);
-				LOG.info("Sent subscription update");
-				dbExecutor.execute(new GenerateSubscriptionUpdate());
-			} catch(IOException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				dispose(true, true);
-			}
-		}
-	}
-
-	// This task runs on the database thread
-	private class GenerateTransportAcks implements Runnable {
-
-		public void run() {
-			try {
-				Collection<TransportAck> acks =
-						db.generateTransportAcks(contactId);
-				if(LOG.isLoggable(INFO))
-					LOG.info("Generated transport acks: " + (acks != null));
-				if(acks != null) writerTasks.add(new WriteTransportAcks(acks));
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-	}
-
-	// This tasks runs on the writer thread
-	private class WriteTransportAcks implements Runnable {
-
-		private final Collection<TransportAck> acks;
-
-		private WriteTransportAcks(Collection<TransportAck> acks) {
-			this.acks = acks;
-		}
-
-		public void run() {
-			assert writer != null;
-			try {
-				for(TransportAck a : acks) writer.writeTransportAck(a);
-				LOG.info("Sent transport acks");
-				dbExecutor.execute(new GenerateTransportAcks());
-			} catch(IOException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				dispose(true, true);
-			}
-		}
-	}
-
-	// This task runs on the database thread
-	private class GenerateTransportUpdates implements Runnable {
-
-		public void run() {
-			try {
-				Collection<TransportUpdate> t =
-						db.generateTransportUpdates(contactId, maxLatency);
-				if(LOG.isLoggable(INFO))
-					LOG.info("Generated transport updates: " + (t != null));
-				if(t != null) writerTasks.add(new WriteTransportUpdates(t));
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
-		}
-	}
-
-	// This task runs on the writer thread
-	private class WriteTransportUpdates implements Runnable {
-
-		private final Collection<TransportUpdate> updates;
-
-		private WriteTransportUpdates(Collection<TransportUpdate> updates) {
-			this.updates = updates;
-		}
-
-		public void run() {
-			assert writer != null;
-			try {
-				for(TransportUpdate u : updates) writer.writeTransportUpdate(u);
-				LOG.info("Sent transport updates");
-				dbExecutor.execute(new GenerateTransportUpdates());
-			} catch(IOException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				dispose(true, true);
-			}
-		}
-	}
-}
diff --git a/briar-core/src/org/briarproject/messaging/duplex/DuplexConnectionFactoryImpl.java b/briar-core/src/org/briarproject/messaging/duplex/DuplexConnectionFactoryImpl.java
deleted file mode 100644
index f8affa8dde6ddb479ac883c32e65488dc3797952..0000000000000000000000000000000000000000
--- a/briar-core/src/org/briarproject/messaging/duplex/DuplexConnectionFactoryImpl.java
+++ /dev/null
@@ -1,108 +0,0 @@
-package org.briarproject.messaging.duplex;
-
-import java.util.concurrent.Executor;
-import java.util.logging.Logger;
-
-import javax.inject.Inject;
-
-import org.briarproject.api.ContactId;
-import org.briarproject.api.TransportId;
-import org.briarproject.api.crypto.CryptoExecutor;
-import org.briarproject.api.crypto.KeyManager;
-import org.briarproject.api.db.DatabaseComponent;
-import org.briarproject.api.db.DatabaseExecutor;
-import org.briarproject.api.event.EventBus;
-import org.briarproject.api.messaging.MessageVerifier;
-import org.briarproject.api.messaging.PacketReaderFactory;
-import org.briarproject.api.messaging.PacketWriterFactory;
-import org.briarproject.api.messaging.duplex.DuplexConnectionFactory;
-import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
-import org.briarproject.api.transport.ConnectionRegistry;
-import org.briarproject.api.transport.StreamContext;
-import org.briarproject.api.transport.StreamReaderFactory;
-import org.briarproject.api.transport.StreamWriterFactory;
-
-class DuplexConnectionFactoryImpl implements DuplexConnectionFactory {
-
-	private static final Logger LOG =
-			Logger.getLogger(DuplexConnectionFactoryImpl.class.getName());
-
-	private final Executor dbExecutor, cryptoExecutor;
-	private final MessageVerifier messageVerifier;
-	private final DatabaseComponent db;
-	private final EventBus eventBus;
-	private final KeyManager keyManager;
-	private final ConnectionRegistry connRegistry;
-	private final StreamReaderFactory connReaderFactory;
-	private final StreamWriterFactory connWriterFactory;
-	private final PacketReaderFactory packetReaderFactory;
-	private final PacketWriterFactory packetWriterFactory;
-
-	@Inject
-	DuplexConnectionFactoryImpl(@DatabaseExecutor Executor dbExecutor,
-			@CryptoExecutor Executor cryptoExecutor,
-			MessageVerifier messageVerifier, DatabaseComponent db,
-			EventBus eventBus, KeyManager keyManager,
-			ConnectionRegistry connRegistry,
-			StreamReaderFactory connReaderFactory,
-			StreamWriterFactory connWriterFactory,
-			PacketReaderFactory packetReaderFactory,
-			PacketWriterFactory packetWriterFactory) {
-		this.dbExecutor = dbExecutor;
-		this.cryptoExecutor = cryptoExecutor;
-		this.messageVerifier = messageVerifier;
-		this.db = db;
-		this.eventBus = eventBus;
-		this.keyManager = keyManager;
-		this.connRegistry = connRegistry;
-		this.connReaderFactory = connReaderFactory;
-		this.connWriterFactory = connWriterFactory;
-		this.packetReaderFactory = packetReaderFactory;
-		this.packetWriterFactory = packetWriterFactory;
-	}
-
-	public void createIncomingConnection(StreamContext ctx,
-			DuplexTransportConnection transport) {
-		final DuplexConnection conn = new IncomingDuplexConnection(dbExecutor,
-				cryptoExecutor, messageVerifier, db, eventBus, connRegistry,
-				connReaderFactory, connWriterFactory, packetReaderFactory,
-				packetWriterFactory, ctx, transport);
-		Runnable write = new Runnable() {
-			public void run() {
-				conn.write();
-			}
-		};
-		new Thread(write, "DuplexConnectionWriter").start();
-		Runnable read = new Runnable() {
-			public void run() {
-				conn.read();
-			}
-		};
-		new Thread(read, "DuplexConnectionReader").start();
-	}
-
-	public void createOutgoingConnection(ContactId c, TransportId t,
-			DuplexTransportConnection transport) {
-		StreamContext ctx = keyManager.getStreamContext(c, t);
-		if(ctx == null) {
-			LOG.warning("Could not create outgoing stream context");
-			return;
-		}
-		final DuplexConnection conn = new OutgoingDuplexConnection(dbExecutor,
-				cryptoExecutor, messageVerifier, db, eventBus, connRegistry,
-				connReaderFactory, connWriterFactory, packetReaderFactory,
-				packetWriterFactory, ctx, transport);
-		Runnable write = new Runnable() {
-			public void run() {
-				conn.write();
-			}
-		};
-		new Thread(write, "DuplexConnectionWriter").start();
-		Runnable read = new Runnable() {
-			public void run() {
-				conn.read();
-			}
-		};
-		new Thread(read, "DuplexConnectionReader").start();
-	}
-}
diff --git a/briar-core/src/org/briarproject/messaging/duplex/DuplexMessagingModule.java b/briar-core/src/org/briarproject/messaging/duplex/DuplexMessagingModule.java
deleted file mode 100644
index 7f3996f346a5c6cfbcf6a3a1c41a9c410eb81aed..0000000000000000000000000000000000000000
--- a/briar-core/src/org/briarproject/messaging/duplex/DuplexMessagingModule.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.briarproject.messaging.duplex;
-
-import javax.inject.Singleton;
-
-import org.briarproject.api.messaging.duplex.DuplexConnectionFactory;
-
-import com.google.inject.AbstractModule;
-
-public class DuplexMessagingModule extends AbstractModule {
-
-	protected void configure() {
-		bind(DuplexConnectionFactory.class).to(
-				DuplexConnectionFactoryImpl.class).in(Singleton.class);
-	}
-}
diff --git a/briar-core/src/org/briarproject/messaging/duplex/IncomingDuplexConnection.java b/briar-core/src/org/briarproject/messaging/duplex/IncomingDuplexConnection.java
deleted file mode 100644
index 52e4e931810f29948de4e5eb495660b75e28c6c4..0000000000000000000000000000000000000000
--- a/briar-core/src/org/briarproject/messaging/duplex/IncomingDuplexConnection.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package org.briarproject.messaging.duplex;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.concurrent.Executor;
-
-import org.briarproject.api.db.DatabaseComponent;
-import org.briarproject.api.event.EventBus;
-import org.briarproject.api.messaging.MessageVerifier;
-import org.briarproject.api.messaging.PacketReaderFactory;
-import org.briarproject.api.messaging.PacketWriterFactory;
-import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
-import org.briarproject.api.transport.ConnectionRegistry;
-import org.briarproject.api.transport.StreamContext;
-import org.briarproject.api.transport.StreamReader;
-import org.briarproject.api.transport.StreamReaderFactory;
-import org.briarproject.api.transport.StreamWriter;
-import org.briarproject.api.transport.StreamWriterFactory;
-
-class IncomingDuplexConnection extends DuplexConnection {
-
-	IncomingDuplexConnection(Executor dbExecutor, Executor cryptoExecutor,
-			MessageVerifier messageVerifier, DatabaseComponent db,
-			EventBus eventBus, ConnectionRegistry connRegistry,
-			StreamReaderFactory connReaderFactory,
-			StreamWriterFactory connWriterFactory,
-			PacketReaderFactory packetReaderFactory,
-			PacketWriterFactory packetWriterFactory,
-			StreamContext ctx, DuplexTransportConnection transport) {
-		super(dbExecutor, cryptoExecutor, messageVerifier, db, eventBus,
-				connRegistry, connReaderFactory, connWriterFactory,
-				packetReaderFactory, packetWriterFactory, ctx, transport);
-	}
-
-	@Override
-	protected StreamReader createStreamReader() throws IOException {
-		InputStream in = transport.getInputStream();
-		int maxFrameLength = transport.getMaxFrameLength();
-		return connReaderFactory.createStreamReader(in, maxFrameLength,
-				ctx, true, true);
-	}
-
-	@Override
-	protected StreamWriter createStreamWriter() throws IOException {
-		OutputStream out = transport.getOutputStream();
-		int maxFrameLength = transport.getMaxFrameLength();
-		return connWriterFactory.createStreamWriter(out, maxFrameLength,
-				Long.MAX_VALUE, ctx, true, false);
-	}
-}
diff --git a/briar-core/src/org/briarproject/messaging/duplex/OutgoingDuplexConnection.java b/briar-core/src/org/briarproject/messaging/duplex/OutgoingDuplexConnection.java
deleted file mode 100644
index 7eab0226d9e8cabeccc18752f156f72190ff9a97..0000000000000000000000000000000000000000
--- a/briar-core/src/org/briarproject/messaging/duplex/OutgoingDuplexConnection.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package org.briarproject.messaging.duplex;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.concurrent.Executor;
-
-import org.briarproject.api.db.DatabaseComponent;
-import org.briarproject.api.event.EventBus;
-import org.briarproject.api.messaging.MessageVerifier;
-import org.briarproject.api.messaging.PacketReaderFactory;
-import org.briarproject.api.messaging.PacketWriterFactory;
-import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
-import org.briarproject.api.transport.ConnectionRegistry;
-import org.briarproject.api.transport.StreamContext;
-import org.briarproject.api.transport.StreamReader;
-import org.briarproject.api.transport.StreamReaderFactory;
-import org.briarproject.api.transport.StreamWriter;
-import org.briarproject.api.transport.StreamWriterFactory;
-
-class OutgoingDuplexConnection extends DuplexConnection {
-
-	OutgoingDuplexConnection(Executor dbExecutor, Executor cryptoExecutor,
-			MessageVerifier messageVerifier, DatabaseComponent db,
-			EventBus eventBus, ConnectionRegistry connRegistry,
-			StreamReaderFactory connReaderFactory,
-			StreamWriterFactory connWriterFactory,
-			PacketReaderFactory packetReaderFactory,
-			PacketWriterFactory packetWriterFactory, StreamContext ctx,
-			DuplexTransportConnection transport) {
-		super(dbExecutor, cryptoExecutor, messageVerifier, db, eventBus,
-				connRegistry, connReaderFactory, connWriterFactory,
-				packetReaderFactory, packetWriterFactory, ctx, transport);
-	}
-
-	@Override
-	protected StreamReader createStreamReader() throws IOException {
-		InputStream in = transport.getInputStream();
-		int maxFrameLength = transport.getMaxFrameLength();
-		return connReaderFactory.createStreamReader(in, maxFrameLength,
-				ctx, false, false);
-	}
-
-	@Override
-	protected StreamWriter createStreamWriter() throws IOException {
-		OutputStream out = transport.getOutputStream();
-		int maxFrameLength = transport.getMaxFrameLength();
-		return connWriterFactory.createStreamWriter(out, maxFrameLength,
-				Long.MAX_VALUE, ctx, false, true);
-	}
-}
diff --git a/briar-core/src/org/briarproject/messaging/simplex/OutgoingSimplexConnection.java b/briar-core/src/org/briarproject/messaging/simplex/OutgoingSimplexConnection.java
deleted file mode 100644
index 94fd128ff7aab12e6409709a87d77b0ef8e7f5ed..0000000000000000000000000000000000000000
--- a/briar-core/src/org/briarproject/messaging/simplex/OutgoingSimplexConnection.java
+++ /dev/null
@@ -1,187 +0,0 @@
-package org.briarproject.messaging.simplex;
-
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.api.messaging.MessagingConstants.MAX_PACKET_LENGTH;
-
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.Collection;
-import java.util.logging.Logger;
-
-import org.briarproject.api.ContactId;
-import org.briarproject.api.TransportId;
-import org.briarproject.api.db.DatabaseComponent;
-import org.briarproject.api.db.DbException;
-import org.briarproject.api.messaging.Ack;
-import org.briarproject.api.messaging.PacketWriter;
-import org.briarproject.api.messaging.PacketWriterFactory;
-import org.briarproject.api.messaging.RetentionAck;
-import org.briarproject.api.messaging.RetentionUpdate;
-import org.briarproject.api.messaging.SubscriptionAck;
-import org.briarproject.api.messaging.SubscriptionUpdate;
-import org.briarproject.api.messaging.TransportAck;
-import org.briarproject.api.messaging.TransportUpdate;
-import org.briarproject.api.plugins.simplex.SimplexTransportWriter;
-import org.briarproject.api.transport.ConnectionRegistry;
-import org.briarproject.api.transport.StreamContext;
-import org.briarproject.api.transport.StreamWriter;
-import org.briarproject.api.transport.StreamWriterFactory;
-import org.briarproject.util.ByteUtils;
-
-class OutgoingSimplexConnection {
-
-	private static final Logger LOG =
-			Logger.getLogger(OutgoingSimplexConnection.class.getName());
-
-	private final DatabaseComponent db;
-	private final ConnectionRegistry connRegistry;
-	private final StreamWriterFactory connWriterFactory;
-	private final PacketWriterFactory packetWriterFactory;
-	private final StreamContext ctx;
-	private final SimplexTransportWriter transport;
-	private final ContactId contactId;
-	private final TransportId transportId;
-	private final long maxLatency;
-
-	OutgoingSimplexConnection(DatabaseComponent db,
-			ConnectionRegistry connRegistry,
-			StreamWriterFactory connWriterFactory,
-			PacketWriterFactory packetWriterFactory, StreamContext ctx,
-			SimplexTransportWriter transport) {
-		this.db = db;
-		this.connRegistry = connRegistry;
-		this.connWriterFactory = connWriterFactory;
-		this.packetWriterFactory = packetWriterFactory;
-		this.ctx = ctx;
-		this.transport = transport;
-		contactId = ctx.getContactId();
-		transportId = ctx.getTransportId();
-		maxLatency = transport.getMaxLatency();
-	}
-
-	void write() {
-		connRegistry.registerConnection(contactId, transportId);
-		try {
-			OutputStream out = transport.getOutputStream();
-			long capacity = transport.getCapacity();
-			int maxFrameLength = transport.getMaxFrameLength();
-			StreamWriter conn = connWriterFactory.createStreamWriter(
-					out, maxFrameLength, capacity, ctx, false, true);
-			out = conn.getOutputStream();
-			if(conn.getRemainingCapacity() < MAX_PACKET_LENGTH)
-				throw new EOFException();
-			PacketWriter writer = packetWriterFactory.createPacketWriter(out,
-					false);
-			// Send the initial packets: updates and acks
-			boolean hasSpace = writeTransportAcks(conn, writer);
-			if(hasSpace) hasSpace = writeTransportUpdates(conn, writer);
-			if(hasSpace) hasSpace = writeSubscriptionAck(conn, writer);
-			if(hasSpace) hasSpace = writeSubscriptionUpdate(conn, writer);
-			if(hasSpace) hasSpace = writeRetentionAck(conn, writer);
-			if(hasSpace) hasSpace = writeRetentionUpdate(conn, writer);
-			// Write acks until you can't write acks no more
-			capacity = conn.getRemainingCapacity();
-			int maxMessages = writer.getMaxMessagesForAck(capacity);
-			Ack a = db.generateAck(contactId, maxMessages);
-			while(a != null) {
-				writer.writeAck(a);
-				capacity = conn.getRemainingCapacity();
-				maxMessages = writer.getMaxMessagesForAck(capacity);
-				a = db.generateAck(contactId, maxMessages);
-			}
-			// Write messages until you can't write messages no more
-			capacity = conn.getRemainingCapacity();
-			int maxLength = (int) Math.min(capacity, MAX_PACKET_LENGTH);
-			Collection<byte[]> batch = db.generateBatch(contactId, maxLength,
-					maxLatency);
-			while(batch != null) {
-				for(byte[] raw : batch) writer.writeMessage(raw);
-				capacity = conn.getRemainingCapacity();
-				maxLength = (int) Math.min(capacity, MAX_PACKET_LENGTH);
-				batch = db.generateBatch(contactId, maxLength, maxLatency);
-			}
-			writer.flush();
-			writer.close();
-			dispose(false);
-		} catch(DbException e) {
-			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			dispose(true);
-		} catch(IOException e) {
-			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			dispose(true);
-		}
-		connRegistry.unregisterConnection(contactId, transportId);
-	}
-
-	private boolean writeTransportAcks(StreamWriter conn,
-			PacketWriter writer) throws DbException, IOException {
-		assert conn.getRemainingCapacity() >= MAX_PACKET_LENGTH;
-		Collection<TransportAck> acks = db.generateTransportAcks(contactId);
-		if(acks == null) return true;
-		for(TransportAck a : acks) {
-			writer.writeTransportAck(a);
-			if(conn.getRemainingCapacity() < MAX_PACKET_LENGTH) return false;
-		}
-		return true;
-	}
-
-	private boolean writeTransportUpdates(StreamWriter conn,
-			PacketWriter writer) throws DbException, IOException {
-		assert conn.getRemainingCapacity() >= MAX_PACKET_LENGTH;
-		Collection<TransportUpdate> updates =
-				db.generateTransportUpdates(contactId, maxLatency);
-		if(updates == null) return true;
-		for(TransportUpdate u : updates) {
-			writer.writeTransportUpdate(u);
-			if(conn.getRemainingCapacity() < MAX_PACKET_LENGTH) return false;
-		}
-		return true;
-	}
-
-	private boolean writeSubscriptionAck(StreamWriter conn,
-			PacketWriter writer) throws DbException, IOException {
-		assert conn.getRemainingCapacity() >= MAX_PACKET_LENGTH;
-		SubscriptionAck a = db.generateSubscriptionAck(contactId);
-		if(a == null) return true;
-		writer.writeSubscriptionAck(a);
-		return conn.getRemainingCapacity() >= MAX_PACKET_LENGTH;
-	}
-
-	private boolean writeSubscriptionUpdate(StreamWriter conn,
-			PacketWriter writer) throws DbException, IOException {
-		assert conn.getRemainingCapacity() >= MAX_PACKET_LENGTH;
-		SubscriptionUpdate u =
-				db.generateSubscriptionUpdate(contactId, maxLatency);
-		if(u == null) return true;
-		writer.writeSubscriptionUpdate(u);
-		return conn.getRemainingCapacity() >= MAX_PACKET_LENGTH;
-	}
-
-	private boolean writeRetentionAck(StreamWriter conn,
-			PacketWriter writer) throws DbException, IOException {
-		assert conn.getRemainingCapacity() >= MAX_PACKET_LENGTH;
-		RetentionAck a = db.generateRetentionAck(contactId);
-		if(a == null) return true;
-		writer.writeRetentionAck(a);
-		return conn.getRemainingCapacity() >= MAX_PACKET_LENGTH;
-	}
-
-	private boolean writeRetentionUpdate(StreamWriter conn,
-			PacketWriter writer) throws DbException, IOException {
-		assert conn.getRemainingCapacity() >= MAX_PACKET_LENGTH;
-		RetentionUpdate u = db.generateRetentionUpdate(contactId, maxLatency);
-		if(u == null) return true;
-		writer.writeRetentionUpdate(u);
-		return conn.getRemainingCapacity() >= MAX_PACKET_LENGTH;
-	}
-
-	private void dispose(boolean exception) {
-		ByteUtils.erase(ctx.getSecret());
-		try {
-			transport.dispose(exception);
-		} catch(IOException e) {
-			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-		}
-	}
-}
diff --git a/briar-core/src/org/briarproject/messaging/simplex/SimplexConnectionFactoryImpl.java b/briar-core/src/org/briarproject/messaging/simplex/SimplexConnectionFactoryImpl.java
deleted file mode 100644
index f202d061eba2b9715943a3cb8c6fd251617fe49b..0000000000000000000000000000000000000000
--- a/briar-core/src/org/briarproject/messaging/simplex/SimplexConnectionFactoryImpl.java
+++ /dev/null
@@ -1,90 +0,0 @@
-package org.briarproject.messaging.simplex;
-
-import java.util.concurrent.Executor;
-import java.util.logging.Logger;
-
-import javax.inject.Inject;
-
-import org.briarproject.api.ContactId;
-import org.briarproject.api.TransportId;
-import org.briarproject.api.crypto.CryptoExecutor;
-import org.briarproject.api.crypto.KeyManager;
-import org.briarproject.api.db.DatabaseComponent;
-import org.briarproject.api.db.DatabaseExecutor;
-import org.briarproject.api.messaging.MessageVerifier;
-import org.briarproject.api.messaging.PacketReaderFactory;
-import org.briarproject.api.messaging.PacketWriterFactory;
-import org.briarproject.api.messaging.simplex.SimplexConnectionFactory;
-import org.briarproject.api.plugins.simplex.SimplexTransportReader;
-import org.briarproject.api.plugins.simplex.SimplexTransportWriter;
-import org.briarproject.api.transport.ConnectionRegistry;
-import org.briarproject.api.transport.StreamContext;
-import org.briarproject.api.transport.StreamReaderFactory;
-import org.briarproject.api.transport.StreamWriterFactory;
-
-class SimplexConnectionFactoryImpl implements SimplexConnectionFactory {
-
-	private static final Logger LOG =
-			Logger.getLogger(SimplexConnectionFactoryImpl.class.getName());
-
-	private final Executor dbExecutor, cryptoExecutor;
-	private final MessageVerifier messageVerifier;
-	private final DatabaseComponent db;
-	private final KeyManager keyManager;
-	private final ConnectionRegistry connRegistry;
-	private final StreamReaderFactory connReaderFactory;
-	private final StreamWriterFactory connWriterFactory;
-	private final PacketReaderFactory packetReaderFactory;
-	private final PacketWriterFactory packetWriterFactory;
-
-	@Inject
-	SimplexConnectionFactoryImpl(@DatabaseExecutor Executor dbExecutor,
-			@CryptoExecutor Executor cryptoExecutor,
-			MessageVerifier messageVerifier, DatabaseComponent db,
-			KeyManager keyManager, ConnectionRegistry connRegistry,
-			StreamReaderFactory connReaderFactory,
-			StreamWriterFactory connWriterFactory,
-			PacketReaderFactory packetReaderFactory,
-			PacketWriterFactory packetWriterFactory) {
-		this.dbExecutor = dbExecutor;
-		this.cryptoExecutor = cryptoExecutor;
-		this.messageVerifier = messageVerifier;
-		this.db = db;
-		this.keyManager = keyManager;
-		this.connRegistry = connRegistry;
-		this.connReaderFactory = connReaderFactory;
-		this.connWriterFactory = connWriterFactory;
-		this.packetReaderFactory = packetReaderFactory;
-		this.packetWriterFactory = packetWriterFactory;
-	}
-
-	public void createIncomingConnection(StreamContext ctx,
-			SimplexTransportReader r) {
-		final IncomingSimplexConnection conn = new IncomingSimplexConnection(
-				dbExecutor, cryptoExecutor, messageVerifier, db, connRegistry,
-				connReaderFactory, packetReaderFactory, ctx, r);
-		Runnable read = new Runnable() {
-			public void run() {
-				conn.read();
-			}
-		};
-		new Thread(read, "SimplexConnectionReader").start();
-	}
-
-	public void createOutgoingConnection(ContactId c, TransportId t,
-			SimplexTransportWriter w) {
-		StreamContext ctx = keyManager.getStreamContext(c, t);
-		if(ctx == null) {
-			LOG.warning("Could not create outgoing connection context");
-			return;
-		}		
-		final OutgoingSimplexConnection conn = new OutgoingSimplexConnection(db,
-				connRegistry, connWriterFactory, packetWriterFactory, ctx, w);
-		Runnable write = new Runnable() {
-			public void run() {
-				conn.write();
-			}
-		};
-		new Thread(write, "SimplexConnectionWriter").start();
-	}
-}
diff --git a/briar-core/src/org/briarproject/messaging/simplex/SimplexMessagingModule.java b/briar-core/src/org/briarproject/messaging/simplex/SimplexMessagingModule.java
deleted file mode 100644
index 3bf9ffe85ef8441dca6e1456b8cea6571081bdaa..0000000000000000000000000000000000000000
--- a/briar-core/src/org/briarproject/messaging/simplex/SimplexMessagingModule.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.briarproject.messaging.simplex;
-
-import javax.inject.Singleton;
-
-import org.briarproject.api.messaging.simplex.SimplexConnectionFactory;
-
-import com.google.inject.AbstractModule;
-
-public class SimplexMessagingModule extends AbstractModule {
-
-	protected void configure() {
-		bind(SimplexConnectionFactory.class).to(
-				SimplexConnectionFactoryImpl.class).in(Singleton.class);
-	}
-}
diff --git a/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java b/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java
index 1e14f9c72c0cbd00ecf6c283ee3af63df753d33c..745d5df05348ff8bac1827366b3f7e5141de5bf7 100644
--- a/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java
+++ b/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java
@@ -27,6 +27,8 @@ import org.briarproject.api.lifecycle.IoExecutor;
 import org.briarproject.api.plugins.Plugin;
 import org.briarproject.api.plugins.PluginCallback;
 import org.briarproject.api.plugins.PluginManager;
+import org.briarproject.api.plugins.TransportConnectionReader;
+import org.briarproject.api.plugins.TransportConnectionWriter;
 import org.briarproject.api.plugins.duplex.DuplexPlugin;
 import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
 import org.briarproject.api.plugins.duplex.DuplexPluginConfig;
@@ -36,8 +38,6 @@ import org.briarproject.api.plugins.simplex.SimplexPlugin;
 import org.briarproject.api.plugins.simplex.SimplexPluginCallback;
 import org.briarproject.api.plugins.simplex.SimplexPluginConfig;
 import org.briarproject.api.plugins.simplex.SimplexPluginFactory;
-import org.briarproject.api.plugins.simplex.SimplexTransportReader;
-import org.briarproject.api.plugins.simplex.SimplexTransportWriter;
 import org.briarproject.api.system.Clock;
 import org.briarproject.api.transport.ConnectionDispatcher;
 import org.briarproject.api.ui.UiCallback;
@@ -377,11 +377,11 @@ class PluginManagerImpl implements PluginManager {
 			super(id);
 		}
 
-		public void readerCreated(SimplexTransportReader r) {
+		public void readerCreated(TransportConnectionReader r) {
 			dispatcher.dispatchIncomingConnection(id, r);
 		}
 
-		public void writerCreated(ContactId c, SimplexTransportWriter w) {
+		public void writerCreated(ContactId c, TransportConnectionWriter w) {
 			dispatcher.dispatchOutgoingConnection(c, id, w);
 		}
 	}
diff --git a/briar-core/src/org/briarproject/plugins/PollerImpl.java b/briar-core/src/org/briarproject/plugins/PollerImpl.java
index a92cdf4cd85c57ede04f0b9ed3dd0c998382d2fb..b560bfdd51bb7d0de4823b1bc78a83f06053fb5e 100644
--- a/briar-core/src/org/briarproject/plugins/PollerImpl.java
+++ b/briar-core/src/org/briarproject/plugins/PollerImpl.java
@@ -20,14 +20,14 @@ class PollerImpl implements Poller {
 			Logger.getLogger(PollerImpl.class.getName());
 
 	private final Executor ioExecutor;
-	private final ConnectionRegistry connRegistry;
+	private final ConnectionRegistry connectionRegistry;
 	private final Timer timer;
 
 	@Inject
-	PollerImpl(@IoExecutor Executor ioExecutor, ConnectionRegistry connRegistry,
-			Timer timer) {
+	PollerImpl(@IoExecutor Executor ioExecutor,
+			ConnectionRegistry connectionRegistry, Timer timer) {
 		this.ioExecutor = ioExecutor;
-		this.connRegistry = connRegistry;
+		this.connectionRegistry = connectionRegistry;
 		this.timer = timer;
 	}
 
@@ -53,7 +53,7 @@ class PollerImpl implements Poller {
 			public void run() {
 				if(LOG.isLoggable(INFO))
 					LOG.info("Polling " + p.getClass().getSimpleName());
-				p.poll(connRegistry.getConnectedContacts(p.getId()));
+				p.poll(connectionRegistry.getConnectedContacts(p.getId()));
 			}
 		});
 	}
diff --git a/briar-core/src/org/briarproject/plugins/file/FilePlugin.java b/briar-core/src/org/briarproject/plugins/file/FilePlugin.java
index 03020373660ff631dd65106b376e82428b6d2be6..1af4215ba6afb3fdf547cebc033cb07dde57e157 100644
--- a/briar-core/src/org/briarproject/plugins/file/FilePlugin.java
+++ b/briar-core/src/org/briarproject/plugins/file/FilePlugin.java
@@ -14,10 +14,10 @@ import java.util.concurrent.Executor;
 import java.util.logging.Logger;
 
 import org.briarproject.api.ContactId;
+import org.briarproject.api.plugins.TransportConnectionReader;
+import org.briarproject.api.plugins.TransportConnectionWriter;
 import org.briarproject.api.plugins.simplex.SimplexPlugin;
 import org.briarproject.api.plugins.simplex.SimplexPluginCallback;
-import org.briarproject.api.plugins.simplex.SimplexTransportReader;
-import org.briarproject.api.plugins.simplex.SimplexTransportWriter;
 import org.briarproject.api.system.FileUtils;
 
 public abstract class FilePlugin implements SimplexPlugin {
@@ -60,11 +60,11 @@ public abstract class FilePlugin implements SimplexPlugin {
 		return running;
 	}
 
-	public SimplexTransportReader createReader(ContactId c) {
+	public TransportConnectionReader createReader(ContactId c) {
 		return null;
 	}
 
-	public SimplexTransportWriter createWriter(ContactId c) {
+	public TransportConnectionWriter createWriter(ContactId c) {
 		if(!running) return null;
 		return createWriter(createConnectionFilename());
 	}
@@ -81,7 +81,7 @@ public abstract class FilePlugin implements SimplexPlugin {
 		return filename.toLowerCase(Locale.US).matches("[a-z]{8}\\.dat");
 	}
 
-	private SimplexTransportWriter createWriter(String filename) {
+	private TransportConnectionWriter createWriter(String filename) {
 		if(!running) return null;
 		File dir = chooseOutputDirectory();
 		if(dir == null || !dir.exists() || !dir.isDirectory()) return null;
diff --git a/briar-core/src/org/briarproject/plugins/file/FileTransportReader.java b/briar-core/src/org/briarproject/plugins/file/FileTransportReader.java
index 0a97e56732473129dc43f427f6927b3f83c67e7d..316773ec01270fc0b45282edd62a6b40785985e5 100644
--- a/briar-core/src/org/briarproject/plugins/file/FileTransportReader.java
+++ b/briar-core/src/org/briarproject/plugins/file/FileTransportReader.java
@@ -7,9 +7,9 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.util.logging.Logger;
 
-import org.briarproject.api.plugins.simplex.SimplexTransportReader;
+import org.briarproject.api.plugins.TransportConnectionReader;
 
-class FileTransportReader implements SimplexTransportReader {
+class FileTransportReader implements TransportConnectionReader {
 
 	private static final Logger LOG =
 			Logger.getLogger(FileTransportReader.class.getName());
@@ -28,6 +28,10 @@ class FileTransportReader implements SimplexTransportReader {
 		return plugin.getMaxFrameLength();
 	}
 
+	public long getMaxLatency() {
+		return plugin.getMaxLatency();
+	}
+
 	public InputStream getInputStream() {
 		return in;
 	}
diff --git a/briar-core/src/org/briarproject/plugins/file/FileTransportWriter.java b/briar-core/src/org/briarproject/plugins/file/FileTransportWriter.java
index d56dfb72691387680510a56ab929dd23fe020271..2ca55593fc6cdb63b0789a4e9dac14a233b335d4 100644
--- a/briar-core/src/org/briarproject/plugins/file/FileTransportWriter.java
+++ b/briar-core/src/org/briarproject/plugins/file/FileTransportWriter.java
@@ -7,9 +7,9 @@ import java.io.IOException;
 import java.io.OutputStream;
 import java.util.logging.Logger;
 
-import org.briarproject.api.plugins.simplex.SimplexTransportWriter;
+import org.briarproject.api.plugins.TransportConnectionWriter;
 
-class FileTransportWriter implements SimplexTransportWriter {
+class FileTransportWriter implements TransportConnectionWriter {
 
 	private static final Logger LOG =
 			Logger.getLogger(FileTransportWriter.class.getName());
@@ -27,10 +27,6 @@ class FileTransportWriter implements SimplexTransportWriter {
 		this.plugin = plugin;
 	}
 
-	public long getCapacity() {
-		return capacity;
-	}
-
 	public int getMaxFrameLength() {
 		return plugin.getMaxFrameLength();
 	}
@@ -39,6 +35,10 @@ class FileTransportWriter implements SimplexTransportWriter {
 		return plugin.getMaxLatency();
 	}
 
+	public long getCapacity() {
+		return capacity;
+	}
+
 	public OutputStream getOutputStream() {
 		return out;
 	}
diff --git a/briar-core/src/org/briarproject/plugins/tcp/TcpTransportConnection.java b/briar-core/src/org/briarproject/plugins/tcp/TcpTransportConnection.java
index 6612f95fe593ce8e6446575eabbf922363a9e048..83506a211682a611ddf4a6918079336e4530eaa4 100644
--- a/briar-core/src/org/briarproject/plugins/tcp/TcpTransportConnection.java
+++ b/briar-core/src/org/briarproject/plugins/tcp/TcpTransportConnection.java
@@ -4,38 +4,80 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.Socket;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.briarproject.api.plugins.Plugin;
+import org.briarproject.api.plugins.TransportConnectionReader;
+import org.briarproject.api.plugins.TransportConnectionWriter;
 import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
 
 class TcpTransportConnection implements DuplexTransportConnection {
 
 	private final Plugin plugin;
 	private final Socket socket;
+	private final Reader reader;
+	private final Writer writer;
+	private final AtomicBoolean halfClosed, closed;
 
 	TcpTransportConnection(Plugin plugin, Socket socket) {
 		this.plugin = plugin;
 		this.socket = socket;
+		reader = new Reader();
+		writer = new Writer();
+		halfClosed = new AtomicBoolean(false);
+		closed = new AtomicBoolean(false);
 	}
 
-	public int getMaxFrameLength() {
-		return plugin.getMaxFrameLength();
+	public TransportConnectionReader getReader() {
+		return reader;
 	}
 
-	public long getMaxLatency() {
-		return plugin.getMaxLatency();
+	public TransportConnectionWriter getWriter() {
+		return writer;
 	}
 
-	public InputStream getInputStream() throws IOException {
-		return socket.getInputStream();
-	}
+	private class Reader implements TransportConnectionReader {
+
+		public int getMaxFrameLength() {
+			return plugin.getMaxFrameLength();
+		}
+
+		public long getMaxLatency() {
+			return plugin.getMaxLatency();
+		}
 
-	public OutputStream getOutputStream() throws IOException {
-		return socket.getOutputStream();
+		public InputStream getInputStream() throws IOException {
+			return socket.getInputStream();
+		}
+
+		public void dispose(boolean exception, boolean recognised)
+				throws IOException {
+			if(halfClosed.getAndSet(true) || exception)
+				if(!closed.getAndSet(true)) socket.close();
+		}
 	}
 
-	public void dispose(boolean exception, boolean recognised)
-			throws IOException {
-		socket.close();
+	private class Writer implements TransportConnectionWriter {
+
+		public int getMaxFrameLength() {
+			return plugin.getMaxFrameLength();
+		}
+
+		public long getMaxLatency() {
+			return plugin.getMaxLatency();
+		}
+
+		public long getCapacity() {
+			return Long.MAX_VALUE;
+		}
+
+		public OutputStream getOutputStream() throws IOException {
+			return socket.getOutputStream();
+		}
+
+		public void dispose(boolean exception) throws IOException {
+			if(halfClosed.getAndSet(true) || exception)
+				if(!closed.getAndSet(true)) socket.close();
+		}
 	}
 }
diff --git a/briar-core/src/org/briarproject/transport/ConnectionDispatcherImpl.java b/briar-core/src/org/briarproject/transport/ConnectionDispatcherImpl.java
index adda06a5602036dd9483de27d06d7ca9fa8061eb..abae50bff7017eca7e6b81494855b4ddc9c2bddf 100644
--- a/briar-core/src/org/briarproject/transport/ConnectionDispatcherImpl.java
+++ b/briar-core/src/org/briarproject/transport/ConnectionDispatcherImpl.java
@@ -13,14 +13,16 @@ import javax.inject.Inject;
 
 import org.briarproject.api.ContactId;
 import org.briarproject.api.TransportId;
+import org.briarproject.api.crypto.KeyManager;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.lifecycle.IoExecutor;
-import org.briarproject.api.messaging.duplex.DuplexConnectionFactory;
-import org.briarproject.api.messaging.simplex.SimplexConnectionFactory;
+import org.briarproject.api.messaging.MessagingSession;
+import org.briarproject.api.messaging.MessagingSessionFactory;
+import org.briarproject.api.plugins.TransportConnectionReader;
+import org.briarproject.api.plugins.TransportConnectionWriter;
 import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
-import org.briarproject.api.plugins.simplex.SimplexTransportReader;
-import org.briarproject.api.plugins.simplex.SimplexTransportWriter;
 import org.briarproject.api.transport.ConnectionDispatcher;
+import org.briarproject.api.transport.ConnectionRegistry;
 import org.briarproject.api.transport.StreamContext;
 import org.briarproject.api.transport.TagRecogniser;
 
@@ -30,132 +32,280 @@ class ConnectionDispatcherImpl implements ConnectionDispatcher {
 			Logger.getLogger(ConnectionDispatcherImpl.class.getName());
 
 	private final Executor ioExecutor;
+	private final KeyManager keyManager;
 	private final TagRecogniser tagRecogniser;
-	private final SimplexConnectionFactory simplexConnFactory;
-	private final DuplexConnectionFactory duplexConnFactory;
+	private final MessagingSessionFactory messagingSessionFactory;
+	private final ConnectionRegistry connectionRegistry;
 
 	@Inject
 	ConnectionDispatcherImpl(@IoExecutor Executor ioExecutor,
-			TagRecogniser tagRecogniser,
-			SimplexConnectionFactory simplexConnFactory,
-			DuplexConnectionFactory duplexConnFactory) {
+			KeyManager keyManager, TagRecogniser tagRecogniser,
+			MessagingSessionFactory messagingSessionFactory,
+			ConnectionRegistry connectionRegistry) {
 		this.ioExecutor = ioExecutor;
+		this.keyManager = keyManager;
 		this.tagRecogniser = tagRecogniser;
-		this.simplexConnFactory = simplexConnFactory;
-		this.duplexConnFactory = duplexConnFactory;
+		this.messagingSessionFactory = messagingSessionFactory;
+		this.connectionRegistry = connectionRegistry;
 	}
 
 	public void dispatchIncomingConnection(TransportId t,
-			SimplexTransportReader r) {
-		ioExecutor.execute(new DispatchSimplexConnection(t, r));
+			TransportConnectionReader r) {
+		ioExecutor.execute(new DispatchIncomingSimplexConnection(t, r));
 	}
 
 	public void dispatchIncomingConnection(TransportId t,
 			DuplexTransportConnection d) {
-		ioExecutor.execute(new DispatchDuplexConnection(t, d));
+		ioExecutor.execute(new DispatchIncomingDuplexConnection(t, d));
 	}
 
 	public void dispatchOutgoingConnection(ContactId c, TransportId t,
-			SimplexTransportWriter w) {
-		simplexConnFactory.createOutgoingConnection(c, t, w);
+			TransportConnectionWriter w) {
+		ioExecutor.execute(new DispatchOutgoingSimplexConnection(c, t, w));
 	}
 
 	public void dispatchOutgoingConnection(ContactId c, TransportId t,
 			DuplexTransportConnection d) {
-		duplexConnFactory.createOutgoingConnection(c, t, d);
+		ioExecutor.execute(new DispatchOutgoingDuplexConnection(c, t, d));
 	}
 
-	private byte[] readTag(InputStream in) throws IOException {
-		byte[] b = new byte[TAG_LENGTH];
-		int offset = 0;
-		while(offset < b.length) {
-			int read = in.read(b, offset, b.length - offset);
-			if(read == -1) throw new EOFException();
-			offset += read;
+	private StreamContext readAndRecogniseTag(TransportId t,
+			TransportConnectionReader r) {
+		// Read the tag
+		byte[] tag = new byte[TAG_LENGTH];
+		try {
+			InputStream in = r.getInputStream();
+			int offset = 0;
+			while(offset < tag.length) {
+				int read = in.read(tag, offset, tag.length - offset);
+				if(read == -1) throw new EOFException();
+				offset += read;
+			}
+		} catch(IOException e) {
+			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			dispose(r, true, false);
+			return null;
+		}
+		// Recognise the tag
+		StreamContext ctx = null;
+		try {
+			ctx = tagRecogniser.recogniseTag(t, tag);
+		} catch(DbException e) {
+			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			dispose(r, true, false);
+			return null;
+		}
+		if(ctx == null) dispose(r, false, false);
+		return ctx;
+	}
+
+	private void runAndDispose(StreamContext ctx, TransportConnectionReader r) {
+		MessagingSession in =
+				messagingSessionFactory.createIncomingSession(ctx, r);
+		ContactId contactId = ctx.getContactId();
+		TransportId transportId = ctx.getTransportId();
+		connectionRegistry.registerConnection(contactId, transportId);
+		try {
+			in.run();
+		} catch(IOException e) {
+			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			dispose(r, true, true);
+			return;
+		} finally {
+			connectionRegistry.unregisterConnection(contactId, transportId);
+		}
+		dispose(r, false, true);
+	}
+
+	private void dispose(TransportConnectionReader r, boolean exception,
+			boolean recognised) {
+		try {
+			r.dispose(exception, recognised);
+		} catch(IOException e) {
+			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		}
+	}
+
+	private void runAndDispose(StreamContext ctx, TransportConnectionWriter w,
+			boolean duplex) {
+		MessagingSession out =
+				messagingSessionFactory.createOutgoingSession(ctx, w, duplex);
+		ContactId contactId = ctx.getContactId();
+		TransportId transportId = ctx.getTransportId();
+		connectionRegistry.registerConnection(contactId, transportId);
+		try {
+			out.run();
+		} catch(IOException e) {
+			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			dispose(w, true);
+			return;
+		} finally {
+			connectionRegistry.unregisterConnection(contactId, transportId);
+		}
+		dispose(w, false);
+	}
+
+	private void dispose(TransportConnectionWriter w, boolean exception) {
+		try {
+			w.dispose(exception);
+		} catch(IOException e) {
+			if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 		}
-		return b;
 	}
 
-	private class DispatchSimplexConnection implements Runnable {
+	private class DispatchIncomingSimplexConnection implements Runnable {
 
 		private final TransportId transportId;
-		private final SimplexTransportReader transport;
+		private final TransportConnectionReader reader;
 
-		private DispatchSimplexConnection(TransportId transportId,
-				SimplexTransportReader transport) {
+		private DispatchIncomingSimplexConnection(TransportId transportId,
+				TransportConnectionReader reader) {
 			this.transportId = transportId;
-			this.transport = transport;
+			this.reader = reader;
 		}
 
 		public void run() {
-			try {
-				byte[] tag = readTag(transport.getInputStream());
-				StreamContext ctx = tagRecogniser.recogniseTag(transportId,
-						tag);
-				if(ctx == null) {
-					transport.dispose(false, false);
-				} else {
-					simplexConnFactory.createIncomingConnection(ctx,
-							transport);
-				}
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				try {
-					transport.dispose(true, false);
-				} catch(IOException e1) {
-					if(LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e1.toString(), e1);
-				}
-			} catch(IOException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				try {
-					transport.dispose(true, false);
-				} catch(IOException e1) {
-					if(LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e1.toString(), e1);
-				}
+			// Read and recognise the tag
+			StreamContext ctx = readAndRecogniseTag(transportId, reader);
+			if(ctx == null) return;
+			// Run the incoming session
+			runAndDispose(ctx, reader);
+		}
+	}
+
+	private class DispatchOutgoingSimplexConnection implements Runnable {
+
+		private final ContactId contactId;
+		private final TransportId transportId;
+		private final TransportConnectionWriter writer;
+
+		private DispatchOutgoingSimplexConnection(ContactId contactId,
+				TransportId transportId, TransportConnectionWriter writer) {
+			this.contactId = contactId;
+			this.transportId = transportId;
+			this.writer = writer;
+		}
+
+		public void run() {
+			// Allocate a stream context
+			StreamContext ctx = keyManager.getStreamContext(contactId,
+					transportId);
+			if(ctx == null) {
+				dispose(writer, false);
+				return;
 			}
+			// Run the outgoing session
+			runAndDispose(ctx, writer, false);
 		}
 	}
 
-	private class DispatchDuplexConnection implements Runnable {
+	private class DispatchIncomingDuplexConnection implements Runnable {
 
 		private final TransportId transportId;
-		private final DuplexTransportConnection transport;
+		private final TransportConnectionReader reader;
+		private final TransportConnectionWriter writer;
 
-		private DispatchDuplexConnection(TransportId transportId,
+		private DispatchIncomingDuplexConnection(TransportId transportId,
 				DuplexTransportConnection transport) {
 			this.transportId = transportId;
-			this.transport = transport;
+			reader = transport.getReader();
+			writer = transport.getWriter();
+		}
+
+		public void run() {
+			// Read and recognise the tag
+			StreamContext ctx = readAndRecogniseTag(transportId, reader);
+			if(ctx == null) return;
+			// Start the outgoing session on another thread
+			ioExecutor.execute(new DispatchIncomingDuplexConnectionSide2(
+					ctx.getContactId(), transportId, writer));
+			// Run the incoming session
+			runAndDispose(ctx, reader);
+		}
+	}
+
+	private class DispatchIncomingDuplexConnectionSide2 implements Runnable {
+
+		private final ContactId contactId;
+		private final TransportId transportId;
+		private final TransportConnectionWriter writer;
+
+		private DispatchIncomingDuplexConnectionSide2(ContactId contactId,
+				TransportId transportId, TransportConnectionWriter writer) {
+			this.contactId = contactId;
+			this.transportId = transportId;
+			this.writer = writer;
 		}
 
 		public void run() {
-			byte[] tag;
-			try {
-				tag = readTag(transport.getInputStream());
-			} catch(IOException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				dispose(true, false);
+			// Allocate a stream context
+			StreamContext ctx = keyManager.getStreamContext(contactId,
+					transportId);
+			if(ctx == null) {
+				dispose(writer, false);
 				return;
 			}
-			StreamContext ctx = null;
-			try {
-				ctx = tagRecogniser.recogniseTag(transportId, tag);
-			} catch(DbException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				dispose(true, false);
+			// Run the outgoing session
+			runAndDispose(ctx, writer, true);
+		}
+	}
+
+	private class DispatchOutgoingDuplexConnection implements Runnable {
+
+		private final ContactId contactId;
+		private final TransportId transportId;
+		private final TransportConnectionReader reader;
+		private final TransportConnectionWriter writer;
+
+		private DispatchOutgoingDuplexConnection(ContactId contactId,
+				TransportId transportId, DuplexTransportConnection transport) {
+			this.contactId = contactId;
+			this.transportId = transportId;
+			reader = transport.getReader();
+			writer = transport.getWriter();
+		}
+
+		public void run() {
+			// Allocate a stream context
+			StreamContext ctx = keyManager.getStreamContext(contactId,
+					transportId);
+			if(ctx == null) {
+				dispose(writer, false);
 				return;
 			}
-			if(ctx == null) dispose(false, false);
-			else duplexConnFactory.createIncomingConnection(ctx, transport);
+			// Start the incoming session on another thread
+			ioExecutor.execute(new DispatchOutgoingDuplexConnectionSide2(
+					contactId, transportId, reader));
+			// Run the outgoing session
+			runAndDispose(ctx, writer, true);
 		}
+	}
+
+	private class DispatchOutgoingDuplexConnectionSide2 implements Runnable {
 
-		private void dispose(boolean exception, boolean recognised) {
-			try {
-				transport.dispose(exception, recognised);
-			} catch(IOException e) {
-				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		private final ContactId contactId;
+		private final TransportId transportId;
+		private final TransportConnectionReader reader;
+
+		private DispatchOutgoingDuplexConnectionSide2(ContactId contactId,
+				TransportId transportId, TransportConnectionReader reader) {
+			this.contactId = contactId;
+			this.transportId = transportId;
+			this.reader = reader;
+		}
+
+		public void run() {
+			// Read and recognise the tag
+			StreamContext ctx = readAndRecogniseTag(transportId, reader);
+			if(ctx == null) return;
+			// Check that the stream comes from the expected contact
+			if(!ctx.getContactId().equals(contactId)) {
+				LOG.warning("Wrong contact ID for duplex connection");
+				dispose(reader, true, true);
+				return;
 			}
+			// Run the incoming session
+			runAndDispose(ctx, reader);
 		}
 	}
 }
\ No newline at end of file
diff --git a/briar-core/src/org/briarproject/transport/FrameWriter.java b/briar-core/src/org/briarproject/transport/FrameWriter.java
index 167ab3430bfc0943f146c928a97cb9edc2c26196..4f29b7999a08fa1c7964fc65140124232f0b9028 100644
--- a/briar-core/src/org/briarproject/transport/FrameWriter.java
+++ b/briar-core/src/org/briarproject/transport/FrameWriter.java
@@ -8,9 +8,6 @@ interface FrameWriter {
 	void writeFrame(byte[] frame, int payloadLength, boolean finalFrame)
 			throws IOException;
 
-	/** Flushes the stack. */
+	/** Flushes the stream. */
 	void flush() throws IOException;
-
-	/** Returns the maximum number of bytes that can be written. */
-	long getRemainingCapacity();
 }
diff --git a/briar-core/src/org/briarproject/transport/OutgoingEncryptionLayer.java b/briar-core/src/org/briarproject/transport/OutgoingEncryptionLayer.java
index a2c0036772af4452243119cea77dc78a1fba6f5a..0d6d6ace977950a38d774f6652d005b282d16eef 100644
--- a/briar-core/src/org/briarproject/transport/OutgoingEncryptionLayer.java
+++ b/briar-core/src/org/briarproject/transport/OutgoingEncryptionLayer.java
@@ -19,22 +19,18 @@ class OutgoingEncryptionLayer implements FrameWriter {
 	private final AuthenticatedCipher frameCipher;
 	private final SecretKey frameKey;
 	private final byte[] tag, iv, aad, ciphertext;
-	private final int frameLength, maxPayloadLength;
+	private final int frameLength;
 
-	private long capacity, frameNumber;
+	private long frameNumber;
 	private boolean writeTag;
 
-	/** Constructor for the initiator's side of a connection. */
-	OutgoingEncryptionLayer(OutputStream out, long capacity,
-			AuthenticatedCipher frameCipher, SecretKey frameKey,
-			int frameLength, byte[] tag) {
+	OutgoingEncryptionLayer(OutputStream out, AuthenticatedCipher frameCipher,
+			SecretKey frameKey, int frameLength, byte[] tag) {
 		this.out = out;
-		this.capacity = capacity;
 		this.frameCipher = frameCipher;
 		this.frameKey = frameKey;
 		this.frameLength = frameLength;
 		this.tag = tag;
-		maxPayloadLength = frameLength - HEADER_LENGTH - MAC_LENGTH;
 		iv = new byte[IV_LENGTH];
 		aad = new byte[AAD_LENGTH];
 		ciphertext = new byte[frameLength];
@@ -42,24 +38,6 @@ class OutgoingEncryptionLayer implements FrameWriter {
 		writeTag = true;
 	}
 
-	/** Constructor for the responder's side of a connection. */
-	OutgoingEncryptionLayer(OutputStream out, long capacity,
-			AuthenticatedCipher frameCipher, SecretKey frameKey,
-			int frameLength) {
-		this.out = out;
-		this.capacity = capacity;
-		this.frameCipher = frameCipher;
-		this.frameKey = frameKey;
-		this.frameLength = frameLength;
-		tag = null;
-		maxPayloadLength = frameLength - HEADER_LENGTH - MAC_LENGTH;
-		iv = new byte[IV_LENGTH];
-		aad = new byte[AAD_LENGTH];
-		ciphertext = new byte[frameLength];
-		frameNumber = 0;
-		writeTag = false;
-	}
-
 	public void writeFrame(byte[] frame, int payloadLength, boolean finalFrame)
 			throws IOException {
 		if(frameNumber > MAX_32_BIT_UNSIGNED) throw new IllegalStateException();
@@ -71,7 +49,6 @@ class OutgoingEncryptionLayer implements FrameWriter {
 				frameKey.erase();
 				throw e;
 			}
-			capacity -= tag.length;
 			writeTag = false;
 		}
 		// Encode the header
@@ -107,7 +84,6 @@ class OutgoingEncryptionLayer implements FrameWriter {
 			frameKey.erase();
 			throw e;
 		}
-		capacity -= ciphertextLength;
 		frameNumber++;
 	}
 
@@ -120,33 +96,8 @@ class OutgoingEncryptionLayer implements FrameWriter {
 				frameKey.erase();
 				throw e;
 			}
-			capacity -= tag.length;
 			writeTag = false;
 		}
 		out.flush();
 	}
-
-	public long getRemainingCapacity() {
-		// How many frame numbers can we use?
-		long frameNumbers = MAX_32_BIT_UNSIGNED - frameNumber + 1;
-		// How many full frames do we have space for?
-		long bytes = writeTag ? capacity - tag.length : capacity;
-		long fullFrames = bytes / frameLength;
-		// Are we limited by frame numbers or space?
-		if(frameNumbers > fullFrames) {
-			// Can we send a partial frame after the full frames?
-			int partialFrame = (int) (bytes - fullFrames * frameLength);
-			if(partialFrame > HEADER_LENGTH + MAC_LENGTH) {
-				// Send full frames and a partial frame, limited by space
-				int partialPayload = partialFrame - HEADER_LENGTH - MAC_LENGTH;
-				return maxPayloadLength * fullFrames + partialPayload;
-			} else {
-				// Send full frames only, limited by space
-				return maxPayloadLength * fullFrames;
-			}
-		} else {
-			// Send full frames only, limited by frame numbers
-			return maxPayloadLength * frameNumbers;
-		}
-	}
 }
\ No newline at end of file
diff --git a/briar-core/src/org/briarproject/transport/StreamReaderFactoryImpl.java b/briar-core/src/org/briarproject/transport/StreamReaderFactoryImpl.java
index d2997b36635c4548306c0e80a95e45bd27cc92de..d71bfc50ac24c8cc72009a9df6689abee11ddfaa 100644
--- a/briar-core/src/org/briarproject/transport/StreamReaderFactoryImpl.java
+++ b/briar-core/src/org/briarproject/transport/StreamReaderFactoryImpl.java
@@ -20,24 +20,21 @@ class StreamReaderFactoryImpl implements StreamReaderFactory {
 	}
 
 	public StreamReader createStreamReader(InputStream in,
-			int maxFrameLength, StreamContext ctx, boolean incoming,
-			boolean initiator) {
+			int maxFrameLength, StreamContext ctx) {
 		byte[] secret = ctx.getSecret();
 		long streamNumber = ctx.getStreamNumber();
-		boolean weAreAlice = ctx.getAlice();
-		boolean initiatorIsAlice = incoming ? !weAreAlice : weAreAlice;
-		SecretKey frameKey = crypto.deriveFrameKey(secret, streamNumber,
-				initiatorIsAlice, initiator);
-		FrameReader encryption = new IncomingEncryptionLayer(in,
+		boolean alice = !ctx.getAlice();
+		SecretKey frameKey = crypto.deriveFrameKey(secret, streamNumber, alice);
+		FrameReader frameReader = new IncomingEncryptionLayer(in,
 				crypto.getFrameCipher(), frameKey, maxFrameLength);
-		return new StreamReaderImpl(encryption, maxFrameLength);
+		return new StreamReaderImpl(frameReader, maxFrameLength);
 	}
 
 	public StreamReader createInvitationStreamReader(InputStream in,
 			int maxFrameLength, byte[] secret, boolean alice) {
-		SecretKey frameKey = crypto.deriveFrameKey(secret, 0, true, alice);
-		FrameReader encryption = new IncomingEncryptionLayer(in,
+		SecretKey frameKey = crypto.deriveFrameKey(secret, 0, alice);
+		FrameReader frameReader = new IncomingEncryptionLayer(in,
 				crypto.getFrameCipher(), frameKey, maxFrameLength);
-		return new StreamReaderImpl(encryption, maxFrameLength);
+		return new StreamReaderImpl(frameReader, maxFrameLength);
 	}
 }
diff --git a/briar-core/src/org/briarproject/transport/StreamWriterFactoryImpl.java b/briar-core/src/org/briarproject/transport/StreamWriterFactoryImpl.java
index 1935fea1be241f80218264eb3ae7bcbc2fe92fe8..80185d74b04e9db0a85f4ed6340d8877e0a90b9e 100644
--- a/briar-core/src/org/briarproject/transport/StreamWriterFactoryImpl.java
+++ b/briar-core/src/org/briarproject/transport/StreamWriterFactoryImpl.java
@@ -22,35 +22,29 @@ class StreamWriterFactoryImpl implements StreamWriterFactory {
 	}
 
 	public StreamWriter createStreamWriter(OutputStream out,
-			int maxFrameLength, long capacity, StreamContext ctx,
-			boolean incoming, boolean initiator) {
+			int maxFrameLength, StreamContext ctx) {
 		byte[] secret = ctx.getSecret();
 		long streamNumber = ctx.getStreamNumber();
-		boolean weAreAlice = ctx.getAlice();
-		boolean initiatorIsAlice = incoming ? !weAreAlice : weAreAlice;
-		SecretKey frameKey = crypto.deriveFrameKey(secret, streamNumber,
-				initiatorIsAlice, initiator);
-		FrameWriter encryption;
-		if(initiator) {
-			byte[] tag = new byte[TAG_LENGTH];
-			SecretKey tagKey = crypto.deriveTagKey(secret, initiatorIsAlice);
-			crypto.encodeTag(tag, tagKey, streamNumber);
-			tagKey.erase();
-			encryption = new OutgoingEncryptionLayer(out, capacity,
-					crypto.getFrameCipher(), frameKey, maxFrameLength, tag);
-		} else {
-			encryption = new OutgoingEncryptionLayer(out, capacity,
-					crypto.getFrameCipher(), frameKey, maxFrameLength);
-		}
-		return new StreamWriterImpl(encryption, maxFrameLength);
+		boolean alice = ctx.getAlice();
+		byte[] tag = new byte[TAG_LENGTH];
+		SecretKey tagKey = crypto.deriveTagKey(secret, alice);
+		crypto.encodeTag(tag, tagKey, streamNumber);
+		tagKey.erase();
+		SecretKey frameKey = crypto.deriveFrameKey(secret, streamNumber, alice);
+		FrameWriter frameWriter = new OutgoingEncryptionLayer(out,
+				crypto.getFrameCipher(), frameKey, maxFrameLength, tag);
+		return new StreamWriterImpl(frameWriter, maxFrameLength);
 	}
 
 	public StreamWriter createInvitationStreamWriter(OutputStream out,
 			int maxFrameLength, byte[] secret, boolean alice) {
-		SecretKey frameKey = crypto.deriveFrameKey(secret, 0, true, alice);
-		FrameWriter encryption = new OutgoingEncryptionLayer(out,
-				Long.MAX_VALUE, crypto.getFrameCipher(), frameKey,
-				maxFrameLength);
-		return new StreamWriterImpl(encryption, maxFrameLength);
+		byte[] tag = new byte[TAG_LENGTH];
+		SecretKey tagKey = crypto.deriveTagKey(secret, alice);
+		crypto.encodeTag(tag, tagKey, 0);
+		tagKey.erase();
+		SecretKey frameKey = crypto.deriveFrameKey(secret, 0, alice);
+		FrameWriter frameWriter = new OutgoingEncryptionLayer(out,
+				crypto.getFrameCipher(), frameKey, maxFrameLength, tag);
+		return new StreamWriterImpl(frameWriter, maxFrameLength);
 	}
 }
\ No newline at end of file
diff --git a/briar-core/src/org/briarproject/transport/StreamWriterImpl.java b/briar-core/src/org/briarproject/transport/StreamWriterImpl.java
index 82de19e1e0012fc6ad0ae396c21e19986f34bee3..7a65a12199a7133a79928c768d38f0d3d7be0e68 100644
--- a/briar-core/src/org/briarproject/transport/StreamWriterImpl.java
+++ b/briar-core/src/org/briarproject/transport/StreamWriterImpl.java
@@ -33,10 +33,6 @@ class StreamWriterImpl extends OutputStream implements StreamWriter {
 		return this;
 	}
 
-	public long getRemainingCapacity() {
-		return out.getRemainingCapacity();
-	}
-
 	@Override
 	public void close() throws IOException {
 		writeFrame(true);
diff --git a/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothTransportConnection.java b/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothTransportConnection.java
index 4e99e439c56dde2036ec979e345533e387a0be83..8a2c7967257c223d5e34c08444eab33663ab3afd 100644
--- a/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothTransportConnection.java
+++ b/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothTransportConnection.java
@@ -3,40 +3,82 @@ package org.briarproject.plugins.bluetooth;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import javax.microedition.io.StreamConnection;
 
 import org.briarproject.api.plugins.Plugin;
+import org.briarproject.api.plugins.TransportConnectionReader;
+import org.briarproject.api.plugins.TransportConnectionWriter;
 import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
 
 class BluetoothTransportConnection implements DuplexTransportConnection {
 
 	private final Plugin plugin;
 	private final StreamConnection stream;
+	private final Reader reader;
+	private final Writer writer;
+	private final AtomicBoolean halfClosed, closed;
 
 	BluetoothTransportConnection(Plugin plugin, StreamConnection stream) {
 		this.plugin = plugin;
 		this.stream = stream;
+		reader = new Reader();
+		writer = new Writer();
+		halfClosed = new AtomicBoolean(false);
+		closed = new AtomicBoolean(false);
 	}
 
-	public int getMaxFrameLength() {
-		return plugin.getMaxFrameLength();
+	public TransportConnectionReader getReader() {
+		return reader;
 	}
 
-	public long getMaxLatency() {
-		return plugin.getMaxLatency();
+	public TransportConnectionWriter getWriter() {
+		return writer;
 	}
 
-	public InputStream getInputStream() throws IOException {
-		return stream.openInputStream();
-	}
+	private class Reader implements TransportConnectionReader {
+
+		public int getMaxFrameLength() {
+			return plugin.getMaxFrameLength();
+		}
+
+		public long getMaxLatency() {
+			return plugin.getMaxLatency();
+		}
 
-	public OutputStream getOutputStream() throws IOException {
-		return stream.openOutputStream();
+		public InputStream getInputStream() throws IOException {
+			return stream.openInputStream();
+		}
+
+		public void dispose(boolean exception, boolean recognised)
+				throws IOException {
+			if(halfClosed.getAndSet(true) || exception)
+				if(!closed.getAndSet(true)) stream.close();
+		}
 	}
 
-	public void dispose(boolean exception, boolean recognised)
-			throws IOException {
-		stream.close();
+	private class Writer implements TransportConnectionWriter {
+
+		public int getMaxFrameLength() {
+			return plugin.getMaxFrameLength();
+		}
+
+		public long getMaxLatency() {
+			return plugin.getMaxLatency();
+		}
+
+		public long getCapacity() {
+			return Long.MAX_VALUE;
+		}
+
+		public OutputStream getOutputStream() throws IOException {
+			return stream.openOutputStream();
+		}
+
+		public void dispose(boolean exception) throws IOException {
+			if(halfClosed.getAndSet(true) || exception)
+				if(!closed.getAndSet(true)) stream.close();
+		}
 	}
 }
diff --git a/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java b/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java
index 22d45eb0254f99298522c1c702c17f47bf49f446..873fd5e395ccd905b67370534c441db13489a316 100644
--- a/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java
+++ b/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java
@@ -14,12 +14,15 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Logger;
 
 import org.briarproject.api.ContactId;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.TransportProperties;
 import org.briarproject.api.crypto.PseudoRandom;
+import org.briarproject.api.plugins.TransportConnectionReader;
+import org.briarproject.api.plugins.TransportConnectionWriter;
 import org.briarproject.api.plugins.duplex.DuplexPlugin;
 import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
 import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
@@ -102,6 +105,7 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback {
 		return running;
 	}
 
+	// FIXME: Don't poll this plugin
 	public boolean shouldPoll() {
 		return true;
 	}
@@ -164,7 +168,7 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback {
 		}
 	}
 
-	private boolean resetModem() {
+	boolean resetModem() {
 		if(!running) return false;
 		for(String portName : serialPortList.getPortNames()) {
 			if(LOG.isLoggable(INFO))
@@ -227,25 +231,21 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback {
 	private class ModemTransportConnection
 	implements DuplexTransportConnection {
 
-		private final CountDownLatch finished = new CountDownLatch(1);
+		private final AtomicBoolean halfClosed = new AtomicBoolean(false);
+		private final AtomicBoolean closed = new AtomicBoolean(false);
+		private final CountDownLatch disposalFinished = new CountDownLatch(1);
+		private final Reader reader = new Reader();
+		private final Writer writer = new Writer();
 
-		public int getMaxFrameLength() {
-			return maxFrameLength;
+		public TransportConnectionReader getReader() {
+			return reader;
 		}
 
-		public long getMaxLatency() {
-			return maxLatency;
+		public TransportConnectionWriter getWriter() {
+			return writer;
 		}
 
-		public InputStream getInputStream() throws IOException {
-			return modem.getInputStream();
-		}
-
-		public OutputStream getOutputStream() throws IOException {
-			return modem.getOutputStream();
-		}
-
-		public void dispose(boolean exception, boolean recognised) {
+		private void hangUp(boolean exception) {
 			LOG.info("Call disconnected");
 			try {
 				modem.hangUp();
@@ -254,11 +254,55 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback {
 				exception = true;
 			}
 			if(exception) resetModem();
-			finished.countDown();
+			disposalFinished.countDown();
 		}
 
 		private void waitForDisposal() throws InterruptedException {
-			finished.await();
+			disposalFinished.await();
+		}
+
+		private class Reader implements TransportConnectionReader {
+
+			public int getMaxFrameLength() {
+				return maxFrameLength;
+			}
+
+			public long getMaxLatency() {
+				return maxLatency;
+			}
+
+			public InputStream getInputStream() throws IOException {
+				return modem.getInputStream();
+			}
+
+			public void dispose(boolean exception, boolean recognised) {
+				if(halfClosed.getAndSet(true) || exception)
+					if(!closed.getAndSet(true)) hangUp(exception);
+			}
+		}
+
+		private class Writer implements TransportConnectionWriter {
+
+			public int getMaxFrameLength() {
+				return maxFrameLength;
+			}
+
+			public long getMaxLatency() {
+				return maxLatency;
+			}
+
+			public long getCapacity() {
+				return Long.MAX_VALUE;
+			}
+
+			public OutputStream getOutputStream() throws IOException {
+				return modem.getOutputStream();
+			}
+
+			public void dispose(boolean exception) {
+				if(halfClosed.getAndSet(true) || exception)
+					if(!closed.getAndSet(true)) hangUp(exception);
+			}
 		}
 	}
 }
diff --git a/briar-tests/build.xml b/briar-tests/build.xml
index e126d2dceb96441e923f8ad36bfa1af844c0db75..88c573a8b1c528153ebfb572afb7d03fc257bcef 100644
--- a/briar-tests/build.xml
+++ b/briar-tests/build.xml
@@ -111,8 +111,8 @@
 			<test name='org.briarproject.messaging.ConstantsTest'/>
 			<test name='org.briarproject.messaging.ConsumersTest'/>
 			<test name='org.briarproject.messaging.PacketReaderImplTest'/>
-			<test name='org.briarproject.messaging.simplex.OutgoingSimplexConnectionTest'/>
-			<test name='org.briarproject.messaging.simplex.SimplexMessagingIntegrationTest'/>
+			<test name='org.briarproject.messaging.SimplexMessagingIntegrationTest'/>
+			<test name='org.briarproject.messaging.SinglePassOutgoingSessionTest'/>
 			<test name='org.briarproject.plugins.PluginManagerImplTest'/>
 			<test name='org.briarproject.plugins.file.LinuxRemovableDriveFinderTest'/>
 			<test name='org.briarproject.plugins.file.MacRemovableDriveFinderTest'/>
diff --git a/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java b/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java
index 9e1b190adfb1baba0d647bf762ee4dfb3d8e9b43..8cbc75befd6f6970ffc181ea48a36c46aabdd8e7 100644
--- a/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java
+++ b/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java
@@ -45,8 +45,6 @@ import org.briarproject.crypto.CryptoModule;
 import org.briarproject.db.DatabaseModule;
 import org.briarproject.event.EventModule;
 import org.briarproject.messaging.MessagingModule;
-import org.briarproject.messaging.duplex.DuplexMessagingModule;
-import org.briarproject.messaging.simplex.SimplexMessagingModule;
 import org.briarproject.reliability.ReliabilityModule;
 import org.briarproject.serial.SerialModule;
 import org.briarproject.transport.TransportModule;
@@ -81,7 +79,6 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 				new TestLifecycleModule(), new TestSystemModule(),
 				new TestUiModule(), new CryptoModule(), new DatabaseModule(),
 				new EventModule(), new MessagingModule(),
-				new DuplexMessagingModule(), new SimplexMessagingModule(),
 				new ReliabilityModule(), new SerialModule(),
 				new TransportModule());
 		streamReaderFactory = i.getInstance(StreamReaderFactory.class);
@@ -125,29 +122,29 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		StreamContext ctx = new StreamContext(contactId, transportId,
 				secret.clone(), 0, true);
-		StreamWriter conn = streamWriterFactory.createStreamWriter(
-				out, MAX_FRAME_LENGTH, Long.MAX_VALUE, ctx, false, true);
-		OutputStream out1 = conn.getOutputStream();
-		PacketWriter writer = packetWriterFactory.createPacketWriter(out1,
+		StreamWriter streamWriter = streamWriterFactory.createStreamWriter(out,
+				MAX_FRAME_LENGTH, ctx);
+		OutputStream out1 = streamWriter.getOutputStream();
+		PacketWriter packetWriter = packetWriterFactory.createPacketWriter(out1,
 				false);
 
-		writer.writeAck(new Ack(messageIds));
+		packetWriter.writeAck(new Ack(messageIds));
 
-		writer.writeMessage(message.getSerialised());
-		writer.writeMessage(message1.getSerialised());
+		packetWriter.writeMessage(message.getSerialised());
+		packetWriter.writeMessage(message1.getSerialised());
 
-		writer.writeOffer(new Offer(messageIds));
+		packetWriter.writeOffer(new Offer(messageIds));
 
-		writer.writeRequest(new Request(messageIds));
+		packetWriter.writeRequest(new Request(messageIds));
 
 		SubscriptionUpdate su = new SubscriptionUpdate(Arrays.asList(group), 1);
-		writer.writeSubscriptionUpdate(su);
+		packetWriter.writeSubscriptionUpdate(su);
 
 		TransportUpdate tu = new TransportUpdate(transportId,
 				transportProperties, 1);
-		writer.writeTransportUpdate(tu);
+		packetWriter.writeTransportUpdate(tu);
 
-		writer.flush();
+		packetWriter.flush();
 		return out.toByteArray();
 	}
 
@@ -158,44 +155,44 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 		// FIXME: Check that the expected tag was received
 		StreamContext ctx = new StreamContext(contactId, transportId,
 				secret.clone(), 0, false);
-		StreamReader conn = streamReaderFactory.createStreamReader(
-				in, MAX_FRAME_LENGTH, ctx, true, true);
-		InputStream in1 = conn.getInputStream();
-		PacketReader reader = packetReaderFactory.createPacketReader(in1);
+		StreamReader streamReader = streamReaderFactory.createStreamReader(in,
+				MAX_FRAME_LENGTH, ctx);
+		InputStream in1 = streamReader.getInputStream();
+		PacketReader packetReader = packetReaderFactory.createPacketReader(in1);
 
 		// Read the ack
-		assertTrue(reader.hasAck());
-		Ack a = reader.readAck();
+		assertTrue(packetReader.hasAck());
+		Ack a = packetReader.readAck();
 		assertEquals(messageIds, a.getMessageIds());
 
 		// Read and verify the messages
-		assertTrue(reader.hasMessage());
-		UnverifiedMessage m = reader.readMessage();
+		assertTrue(packetReader.hasMessage());
+		UnverifiedMessage m = packetReader.readMessage();
 		checkMessageEquality(message, messageVerifier.verifyMessage(m));
-		assertTrue(reader.hasMessage());
-		m = reader.readMessage();
+		assertTrue(packetReader.hasMessage());
+		m = packetReader.readMessage();
 		checkMessageEquality(message1, messageVerifier.verifyMessage(m));
-		assertFalse(reader.hasMessage());
+		assertFalse(packetReader.hasMessage());
 
 		// Read the offer
-		assertTrue(reader.hasOffer());
-		Offer o = reader.readOffer();
+		assertTrue(packetReader.hasOffer());
+		Offer o = packetReader.readOffer();
 		assertEquals(messageIds, o.getMessageIds());
 
 		// Read the request
-		assertTrue(reader.hasRequest());
-		Request req = reader.readRequest();
+		assertTrue(packetReader.hasRequest());
+		Request req = packetReader.readRequest();
 		assertEquals(messageIds, req.getMessageIds());
 
 		// Read the subscription update
-		assertTrue(reader.hasSubscriptionUpdate());
-		SubscriptionUpdate su = reader.readSubscriptionUpdate();
+		assertTrue(packetReader.hasSubscriptionUpdate());
+		SubscriptionUpdate su = packetReader.readSubscriptionUpdate();
 		assertEquals(Arrays.asList(group), su.getGroups());
 		assertEquals(1, su.getVersion());
 
 		// Read the transport update
-		assertTrue(reader.hasTransportUpdate());
-		TransportUpdate tu = reader.readTransportUpdate();
+		assertTrue(packetReader.hasTransportUpdate());
+		TransportUpdate tu = packetReader.readTransportUpdate();
 		assertEquals(transportId, tu.getId());
 		assertEquals(transportProperties, tu.getProperties());
 		assertEquals(1, tu.getVersion());
diff --git a/briar-tests/src/org/briarproject/crypto/KeyDerivationTest.java b/briar-tests/src/org/briarproject/crypto/KeyDerivationTest.java
index 4b27522e84c17a756ce52e1a6e0f261f277758ac..54c4a3c1560d6363bbe277a740a358ae811aff30 100644
--- a/briar-tests/src/org/briarproject/crypto/KeyDerivationTest.java
+++ b/briar-tests/src/org/briarproject/crypto/KeyDerivationTest.java
@@ -25,10 +25,8 @@ public class KeyDerivationTest extends BriarTestCase {
 	@Test
 	public void testKeysAreDistinct() {
 		List<SecretKey> keys = new ArrayList<SecretKey>();
-		keys.add(crypto.deriveFrameKey(secret, 0, false, false));
-		keys.add(crypto.deriveFrameKey(secret, 0, false, true));
-		keys.add(crypto.deriveFrameKey(secret, 0, true, false));
-		keys.add(crypto.deriveFrameKey(secret, 0, true, true));
+		keys.add(crypto.deriveFrameKey(secret, 0, true));
+		keys.add(crypto.deriveFrameKey(secret, 0, false));
 		keys.add(crypto.deriveTagKey(secret, true));
 		keys.add(crypto.deriveTagKey(secret, false));
 		for(int i = 0; i < 4; i++) {
diff --git a/briar-tests/src/org/briarproject/messaging/ConstantsTest.java b/briar-tests/src/org/briarproject/messaging/ConstantsTest.java
index 75e36a80fd08134fcc3000a5eb7d583911488591..1aee065e1f325972074c82e0915b4a8e2a899d20 100644
--- a/briar-tests/src/org/briarproject/messaging/ConstantsTest.java
+++ b/briar-tests/src/org/briarproject/messaging/ConstantsTest.java
@@ -46,8 +46,6 @@ import org.briarproject.api.messaging.TransportUpdate;
 import org.briarproject.crypto.CryptoModule;
 import org.briarproject.db.DatabaseModule;
 import org.briarproject.event.EventModule;
-import org.briarproject.messaging.duplex.DuplexMessagingModule;
-import org.briarproject.messaging.simplex.SimplexMessagingModule;
 import org.briarproject.serial.SerialModule;
 import org.briarproject.transport.TransportModule;
 import org.junit.Test;
@@ -67,8 +65,7 @@ public class ConstantsTest extends BriarTestCase {
 		Injector i = Guice.createInjector(new TestDatabaseModule(),
 				new TestLifecycleModule(), new TestSystemModule(),
 				new CryptoModule(), new DatabaseModule(), new EventModule(),
-				new MessagingModule(), new DuplexMessagingModule(),
-				new SimplexMessagingModule(), new SerialModule(),
+				new MessagingModule(), new SerialModule(),
 				new TransportModule());
 		crypto = i.getInstance(CryptoComponent.class);
 		groupFactory = i.getInstance(GroupFactory.class);
diff --git a/briar-tests/src/org/briarproject/messaging/simplex/SimplexMessagingIntegrationTest.java b/briar-tests/src/org/briarproject/messaging/SimplexMessagingIntegrationTest.java
similarity index 84%
rename from briar-tests/src/org/briarproject/messaging/simplex/SimplexMessagingIntegrationTest.java
rename to briar-tests/src/org/briarproject/messaging/SimplexMessagingIntegrationTest.java
index e26b4be88458e1ed464fc54d3090c5dddfab28c7..834eba129f1fab9435a762b584749e9652ebc475 100644
--- a/briar-tests/src/org/briarproject/messaging/simplex/SimplexMessagingIntegrationTest.java
+++ b/briar-tests/src/org/briarproject/messaging/SimplexMessagingIntegrationTest.java
@@ -1,4 +1,4 @@
-package org.briarproject.messaging.simplex;
+package org.briarproject.messaging;
 
 import static org.briarproject.api.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
 import static org.briarproject.api.messaging.MessagingConstants.GROUP_SALT_LENGTH;
@@ -30,9 +30,9 @@ import org.briarproject.api.messaging.GroupFactory;
 import org.briarproject.api.messaging.Message;
 import org.briarproject.api.messaging.MessageFactory;
 import org.briarproject.api.messaging.MessageVerifier;
+import org.briarproject.api.messaging.MessagingSession;
 import org.briarproject.api.messaging.PacketReaderFactory;
 import org.briarproject.api.messaging.PacketWriterFactory;
-import org.briarproject.api.transport.ConnectionRegistry;
 import org.briarproject.api.transport.Endpoint;
 import org.briarproject.api.transport.StreamContext;
 import org.briarproject.api.transport.StreamReaderFactory;
@@ -41,8 +41,6 @@ import org.briarproject.api.transport.TagRecogniser;
 import org.briarproject.crypto.CryptoModule;
 import org.briarproject.db.DatabaseModule;
 import org.briarproject.event.EventModule;
-import org.briarproject.messaging.MessagingModule;
-import org.briarproject.messaging.duplex.DuplexMessagingModule;
 import org.briarproject.plugins.ImmediateExecutor;
 import org.briarproject.serial.SerialModule;
 import org.briarproject.transport.TransportModule;
@@ -88,8 +86,7 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
 		return Guice.createInjector(new TestDatabaseModule(dir),
 				new TestLifecycleModule(), new TestSystemModule(),
 				new CryptoModule(), new DatabaseModule(), new EventModule(),
-				new MessagingModule(), new DuplexMessagingModule(),
-				new SimplexMessagingModule(), new SerialModule(),
+				new MessagingModule(), new SerialModule(),
 				new TransportModule());
 	}
 
@@ -140,29 +137,26 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
 		Message message = messageFactory.createAnonymousMessage(null, group,
 				contentType, timestamp, body);
 		db.addLocalMessage(message);
-		// Create an outgoing simplex connection
+		// Create an outgoing messaging session
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		ConnectionRegistry connRegistry =
-				alice.getInstance(ConnectionRegistry.class);
-		StreamWriterFactory connWriterFactory =
+		StreamWriterFactory streamWriterFactory =
 				alice.getInstance(StreamWriterFactory.class);
 		PacketWriterFactory packetWriterFactory =
 				alice.getInstance(PacketWriterFactory.class);
-		TestSimplexTransportWriter transport = new TestSimplexTransportWriter(
-				out, Long.MAX_VALUE, Long.MAX_VALUE);
+		TestTransportConnectionWriter transport =
+				new TestTransportConnectionWriter(out);
 		StreamContext ctx = km.getStreamContext(contactId, transportId);
 		assertNotNull(ctx);
-		OutgoingSimplexConnection simplex = new OutgoingSimplexConnection(db,
-				connRegistry, connWriterFactory, packetWriterFactory, ctx,
-				transport);
+		MessagingSession session = new SinglePassOutgoingSession(db,
+				new ImmediateExecutor(), streamWriterFactory,
+				packetWriterFactory, ctx, transport);
 		// Write whatever needs to be written
-		simplex.write();
-		assertTrue(transport.getDisposed());
-		assertFalse(transport.getException());
+		session.run();
+		transport.dispose(false);
 		// Clean up
 		km.stop();
 		db.close();
-		// Return the contents of the simplex connection
+		// Return the contents of the stream
 		return out.toByteArray();
 	}
 
@@ -204,28 +198,24 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
 		assertEquals(tag.length, read);
 		StreamContext ctx = rec.recogniseTag(transportId, tag);
 		assertNotNull(ctx);
-		// Create an incoming simplex connection
+		// Create an incoming messaging session
 		MessageVerifier messageVerifier =
 				bob.getInstance(MessageVerifier.class);
-		ConnectionRegistry connRegistry =
-				bob.getInstance(ConnectionRegistry.class);
-		StreamReaderFactory connWriterFactory =
+		StreamReaderFactory streamReaderFactory =
 				bob.getInstance(StreamReaderFactory.class);
-		PacketReaderFactory packetWriterFactory =
+		PacketReaderFactory packetReaderFactory =
 				bob.getInstance(PacketReaderFactory.class);
-		TestSimplexTransportReader transport =
-				new TestSimplexTransportReader(in);
-		IncomingSimplexConnection simplex = new IncomingSimplexConnection(
+		TestTransportConnectionReader transport =
+				new TestTransportConnectionReader(in);
+		MessagingSession session = new IncomingSession(db,
 				new ImmediateExecutor(), new ImmediateExecutor(),
-				messageVerifier, db, connRegistry, connWriterFactory,
-				packetWriterFactory, ctx, transport);
+				messageVerifier, streamReaderFactory, packetReaderFactory,
+				ctx, transport);
 		// No messages should have been added yet
 		assertFalse(listener.messageAdded);
 		// Read whatever needs to be read
-		simplex.read();
-		assertTrue(transport.getDisposed());
-		assertFalse(transport.getException());
-		assertTrue(transport.getRecognised());
+		session.run();
+		transport.dispose(false, true);
 		// The private message from Alice should have been added
 		assertTrue(listener.messageAdded);
 		// Clean up
diff --git a/briar-tests/src/org/briarproject/messaging/simplex/OutgoingSimplexConnectionTest.java b/briar-tests/src/org/briarproject/messaging/SinglePassOutgoingSessionTest.java
similarity index 70%
rename from briar-tests/src/org/briarproject/messaging/simplex/OutgoingSimplexConnectionTest.java
rename to briar-tests/src/org/briarproject/messaging/SinglePassOutgoingSessionTest.java
index 3de41423c4f04d3135a61882f8b42ff1abc2e759..27f03296f1439519023432c34bbaa5de448aeae3 100644
--- a/briar-tests/src/org/briarproject/messaging/simplex/OutgoingSimplexConnectionTest.java
+++ b/briar-tests/src/org/briarproject/messaging/SinglePassOutgoingSessionTest.java
@@ -1,9 +1,7 @@
-package org.briarproject.messaging.simplex;
+package org.briarproject.messaging;
 
-import static org.briarproject.api.messaging.MessagingConstants.MAX_PACKET_LENGTH;
 import static org.briarproject.api.transport.TransportConstants.HEADER_LENGTH;
 import static org.briarproject.api.transport.TransportConstants.MAC_LENGTH;
-import static org.briarproject.api.transport.TransportConstants.MIN_STREAM_LENGTH;
 import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH;
 
 import java.io.ByteArrayOutputStream;
@@ -23,14 +21,12 @@ import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DatabaseExecutor;
 import org.briarproject.api.messaging.Ack;
 import org.briarproject.api.messaging.MessageId;
+import org.briarproject.api.messaging.MessagingSession;
 import org.briarproject.api.messaging.PacketWriterFactory;
-import org.briarproject.api.transport.ConnectionRegistry;
 import org.briarproject.api.transport.StreamContext;
 import org.briarproject.api.transport.StreamWriterFactory;
 import org.briarproject.crypto.CryptoModule;
 import org.briarproject.event.EventModule;
-import org.briarproject.messaging.MessagingModule;
-import org.briarproject.messaging.duplex.DuplexMessagingModule;
 import org.briarproject.serial.SerialModule;
 import org.briarproject.transport.TransportModule;
 import org.jmock.Expectations;
@@ -42,39 +38,37 @@ import com.google.inject.Guice;
 import com.google.inject.Injector;
 import com.google.inject.Module;
 
-public class OutgoingSimplexConnectionTest extends BriarTestCase {
+public class SinglePassOutgoingSessionTest extends BriarTestCase {
 
 	// FIXME: This is an integration test, not a unit test
 
 	private final Mockery context;
 	private final DatabaseComponent db;
-	private final ConnectionRegistry connRegistry;
-	private final StreamWriterFactory connWriterFactory;
+	private final Executor dbExecutor;
+	private final StreamWriterFactory streamWriterFactory;
 	private final PacketWriterFactory packetWriterFactory;
 	private final ContactId contactId;
 	private final MessageId messageId;
 	private final TransportId transportId;
 	private final byte[] secret;
 
-	public OutgoingSimplexConnectionTest() {
+	public SinglePassOutgoingSessionTest() {
 		context = new Mockery();
 		db = context.mock(DatabaseComponent.class);
+		dbExecutor = Executors.newSingleThreadExecutor();
 		Module testModule = new AbstractModule() {
 			@Override
 			public void configure() {
 				bind(DatabaseComponent.class).toInstance(db);
 				bind(Executor.class).annotatedWith(
-						DatabaseExecutor.class).toInstance(
-								Executors.newCachedThreadPool());
+						DatabaseExecutor.class).toInstance(dbExecutor);
 			}
 		};
 		Injector i = Guice.createInjector(testModule,
 				new TestLifecycleModule(), new TestSystemModule(),
 				new CryptoModule(), new EventModule(), new MessagingModule(),
-				new DuplexMessagingModule(), new SimplexMessagingModule(),
 				new SerialModule(), new TransportModule());
-		connRegistry = i.getInstance(ConnectionRegistry.class);
-		connWriterFactory = i.getInstance(StreamWriterFactory.class);
+		streamWriterFactory = i.getInstance(StreamWriterFactory.class);
 		packetWriterFactory = i.getInstance(PacketWriterFactory.class);
 		contactId = new ContactId(234);
 		messageId = new MessageId(TestUtils.getRandomId());
@@ -83,34 +77,15 @@ public class OutgoingSimplexConnectionTest extends BriarTestCase {
 		new Random().nextBytes(secret);
 	}
 
-	@Test
-	public void testConnectionTooShort() throws Exception {
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		TestSimplexTransportWriter transport = new TestSimplexTransportWriter(
-				out, MAX_PACKET_LENGTH, Long.MAX_VALUE);
-		StreamContext ctx = new StreamContext(contactId, transportId,
-				secret, 0, true);
-		OutgoingSimplexConnection connection = new OutgoingSimplexConnection(db,
-				connRegistry, connWriterFactory, packetWriterFactory, ctx,
-				transport);
-		connection.write();
-		// Nothing should have been written
-		assertEquals(0, out.size());
-		// The transport should have been disposed with exception == true
-		assertTrue(transport.getDisposed());
-		assertTrue(transport.getException());
-	}
-
 	@Test
 	public void testNothingToSend() throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		TestSimplexTransportWriter transport = new TestSimplexTransportWriter(
-				out, MIN_STREAM_LENGTH, Long.MAX_VALUE);
+		TestTransportConnectionWriter writer =
+				new TestTransportConnectionWriter(out);
 		StreamContext ctx = new StreamContext(contactId, transportId,
 				secret, 0, true);
-		OutgoingSimplexConnection connection = new OutgoingSimplexConnection(db,
-				connRegistry, connWriterFactory, packetWriterFactory, ctx,
-				transport);
+		MessagingSession session = new SinglePassOutgoingSession(db, dbExecutor,
+				streamWriterFactory, packetWriterFactory, ctx, writer);
 		context.checking(new Expectations() {{
 			// No transport acks to send
 			oneOf(db).generateTransportAcks(contactId);
@@ -141,25 +116,22 @@ public class OutgoingSimplexConnectionTest extends BriarTestCase {
 					with(any(long.class)));
 			will(returnValue(null));
 		}});
-		connection.write();
+		session.run();
 		// Only the tag and an empty final frame should have been written
 		assertEquals(TAG_LENGTH + HEADER_LENGTH + MAC_LENGTH, out.size());
-		// The transport should have been disposed with exception == false
-		assertTrue(transport.getDisposed());
-		assertFalse(transport.getException());
 		context.assertIsSatisfied();
 	}
 
 	@Test
 	public void testSomethingToSend() throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		TestSimplexTransportWriter transport = new TestSimplexTransportWriter(
-				out, MIN_STREAM_LENGTH, Long.MAX_VALUE);
+		TestTransportConnectionWriter writer =
+				new TestTransportConnectionWriter(out);
 		StreamContext ctx = new StreamContext(contactId, transportId,
 				secret, 0, true);
-		OutgoingSimplexConnection connection = new OutgoingSimplexConnection(db,
-				connRegistry, connWriterFactory, packetWriterFactory, ctx,
-				transport);
+		MessagingSession session = new SinglePassOutgoingSession(db, dbExecutor,
+				streamWriterFactory, packetWriterFactory,
+				ctx, writer);
 		final byte[] raw = new byte[1234];
 		context.checking(new Expectations() {{
 			// No transport acks to send
@@ -198,13 +170,10 @@ public class OutgoingSimplexConnectionTest extends BriarTestCase {
 					with(any(long.class)));
 			will(returnValue(null));
 		}});
-		connection.write();
+		session.run();
 		// Something should have been written
 		int overhead = TAG_LENGTH + HEADER_LENGTH + MAC_LENGTH;
 		assertTrue(out.size() > overhead + UniqueId.LENGTH + raw.length);
-		// The transport should have been disposed with exception == false
-		assertTrue(transport.getDisposed());
-		assertFalse(transport.getException());
 		context.assertIsSatisfied();
 	}
 }
diff --git a/briar-tests/src/org/briarproject/messaging/TestTransportConnectionReader.java b/briar-tests/src/org/briarproject/messaging/TestTransportConnectionReader.java
new file mode 100644
index 0000000000000000000000000000000000000000..246c0920d0989eea47159c666e5fd897f2640bd3
--- /dev/null
+++ b/briar-tests/src/org/briarproject/messaging/TestTransportConnectionReader.java
@@ -0,0 +1,35 @@
+package org.briarproject.messaging;
+
+import static org.briarproject.api.transport.TransportConstants.MAX_FRAME_LENGTH;
+
+import java.io.InputStream;
+
+import org.briarproject.api.plugins.TransportConnectionReader;
+
+class TestTransportConnectionReader implements TransportConnectionReader {
+
+	private final InputStream in;
+
+	private boolean disposed = false;
+
+	TestTransportConnectionReader(InputStream in) {
+		this.in = in;
+	}
+
+	public int getMaxFrameLength() {
+		return MAX_FRAME_LENGTH;
+	}
+
+	public long getMaxLatency() {
+		return Long.MAX_VALUE;
+	}
+
+	public InputStream getInputStream() {
+		return in;
+	}
+
+	public void dispose(boolean exception, boolean recognised) {
+		assert !disposed;
+		disposed = true;
+	}
+}
\ No newline at end of file
diff --git a/briar-tests/src/org/briarproject/messaging/TestTransportConnectionWriter.java b/briar-tests/src/org/briarproject/messaging/TestTransportConnectionWriter.java
new file mode 100644
index 0000000000000000000000000000000000000000..ab4e4077ecee1e2a4c77dc5cb5ccfa6f91315d20
--- /dev/null
+++ b/briar-tests/src/org/briarproject/messaging/TestTransportConnectionWriter.java
@@ -0,0 +1,40 @@
+package org.briarproject.messaging;
+
+import static org.briarproject.api.transport.TransportConstants.MAX_FRAME_LENGTH;
+
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+
+import org.briarproject.api.plugins.TransportConnectionWriter;
+
+class TestTransportConnectionWriter implements TransportConnectionWriter {
+
+	private final ByteArrayOutputStream out;
+
+	private boolean disposed = false;
+
+	TestTransportConnectionWriter(ByteArrayOutputStream out) {
+		this.out = out;
+	}
+
+	public long getCapacity() {
+		return Long.MAX_VALUE;
+	}
+
+	public int getMaxFrameLength() {
+		return MAX_FRAME_LENGTH;
+	}
+
+	public long getMaxLatency() {
+		return Long.MAX_VALUE;
+	}
+
+	public OutputStream getOutputStream() {
+		return out;
+	}
+
+	public void dispose(boolean exception) {
+		assert !disposed;
+		disposed = true;
+	}
+}
\ No newline at end of file
diff --git a/briar-tests/src/org/briarproject/messaging/simplex/TestSimplexTransportReader.java b/briar-tests/src/org/briarproject/messaging/simplex/TestSimplexTransportReader.java
deleted file mode 100644
index 22829b8b85cb728ebbc1b8cddc9612a83436af63..0000000000000000000000000000000000000000
--- a/briar-tests/src/org/briarproject/messaging/simplex/TestSimplexTransportReader.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package org.briarproject.messaging.simplex;
-
-import static org.briarproject.api.transport.TransportConstants.MAX_FRAME_LENGTH;
-
-import java.io.InputStream;
-
-import org.briarproject.api.plugins.simplex.SimplexTransportReader;
-
-class TestSimplexTransportReader implements SimplexTransportReader {
-
-	private final InputStream in;
-
-	private boolean disposed = false, exception = false, recognised = false;
-
-	TestSimplexTransportReader(InputStream in) {
-		this.in = in;
-	}
-
-	public int getMaxFrameLength() {
-		return MAX_FRAME_LENGTH;
-	}
-
-	public InputStream getInputStream() {
-		return in;
-	}
-
-	public void dispose(boolean exception, boolean recognised) {
-		assert !disposed;
-		disposed = true;
-		this.exception = exception;
-		this.recognised = recognised;
-	}
-
-	boolean getDisposed() {
-		return disposed;
-	}
-
-	boolean getException() {
-		return exception;
-	}
-
-	boolean getRecognised() {
-		return recognised;
-	}
-}
\ No newline at end of file
diff --git a/briar-tests/src/org/briarproject/messaging/simplex/TestSimplexTransportWriter.java b/briar-tests/src/org/briarproject/messaging/simplex/TestSimplexTransportWriter.java
deleted file mode 100644
index 39e4a76810813326d5b53886012619688376dfe7..0000000000000000000000000000000000000000
--- a/briar-tests/src/org/briarproject/messaging/simplex/TestSimplexTransportWriter.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package org.briarproject.messaging.simplex;
-
-import static org.briarproject.api.transport.TransportConstants.MAX_FRAME_LENGTH;
-
-import java.io.ByteArrayOutputStream;
-import java.io.OutputStream;
-
-import org.briarproject.api.plugins.simplex.SimplexTransportWriter;
-
-class TestSimplexTransportWriter implements SimplexTransportWriter {
-
-	private final ByteArrayOutputStream out;
-	private final long capacity, maxLatency;
-
-	private boolean disposed = false, exception = false;
-
-	TestSimplexTransportWriter(ByteArrayOutputStream out, long capacity,
-			long maxLatency) {
-		this.out = out;
-		this.capacity = capacity;
-		this.maxLatency = maxLatency;
-	}
-
-	public long getCapacity() {
-		return capacity;
-	}
-
-	public int getMaxFrameLength() {
-		return MAX_FRAME_LENGTH;
-	}
-
-	public long getMaxLatency() {
-		return maxLatency;
-	}
-
-	public OutputStream getOutputStream() {
-		return out;
-	}
-
-	public void dispose(boolean exception) {
-		assert !disposed;
-		disposed = true;
-		this.exception = exception;
-	}
-
-	boolean getDisposed() {
-		return disposed;
-	}
-
-	boolean getException() {
-		return exception;
-	}
-}
\ No newline at end of file
diff --git a/briar-tests/src/org/briarproject/plugins/DuplexTest.java b/briar-tests/src/org/briarproject/plugins/DuplexTest.java
index 20de33cbe6baf2e7af9f255e0aa2da5cc55b580b..e818e3bb25092fcc56d4d863e45e046e5a9eabe5 100644
--- a/briar-tests/src/org/briarproject/plugins/DuplexTest.java
+++ b/briar-tests/src/org/briarproject/plugins/DuplexTest.java
@@ -7,6 +7,8 @@ import java.util.Scanner;
 
 import org.briarproject.api.ContactId;
 import org.briarproject.api.crypto.PseudoRandom;
+import org.briarproject.api.plugins.TransportConnectionReader;
+import org.briarproject.api.plugins.TransportConnectionWriter;
 import org.briarproject.api.plugins.duplex.DuplexPlugin;
 import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
 
@@ -21,12 +23,14 @@ abstract class DuplexTest {
 
 	protected void sendChallengeReceiveResponse(DuplexTransportConnection d) {
 		assert plugin != null;
+		TransportConnectionReader r = d.getReader();
+		TransportConnectionWriter w = d.getWriter();
 		try {
-			PrintStream out = new PrintStream(d.getOutputStream());
+			PrintStream out = new PrintStream(w.getOutputStream());
 			out.println(CHALLENGE);
 			out.flush();
 			System.out.println("Sent challenge: " + CHALLENGE);
-			Scanner in = new Scanner(d.getInputStream());
+			Scanner in = new Scanner(r.getInputStream());
 			if(in.hasNextLine()) {
 				String response = in.nextLine();
 				System.out.println("Received response: " + response);
@@ -38,11 +42,13 @@ abstract class DuplexTest {
 			} else {
 				System.out.println("No response");
 			}
-			d.dispose(false, true);
+			r.dispose(false, true);
+			w.dispose(false);
 		} catch(IOException e) {
 			e.printStackTrace();
 			try {
-				d.dispose(true, true);
+				r.dispose(true, true);
+				w.dispose(true);
 			} catch(IOException e1) {
 				e1.printStackTrace();
 			}
@@ -51,13 +57,16 @@ abstract class DuplexTest {
 
 	protected void receiveChallengeSendResponse(DuplexTransportConnection d) {
 		assert plugin != null;
+		TransportConnectionReader r = d.getReader();
+		TransportConnectionWriter w = d.getWriter();
 		try {
-			Scanner in = new Scanner(d.getInputStream());
+			Scanner in = new Scanner(r.getInputStream());
 			if(in.hasNextLine()) {
 				String challenge = in.nextLine();
 				System.out.println("Received challenge: " + challenge);
 				if(CHALLENGE.equals(challenge)) {
-					PrintStream out = new PrintStream(d.getOutputStream());
+
+					PrintStream out = new PrintStream(w.getOutputStream());
 					out.println(RESPONSE);
 					out.flush();
 					System.out.println("Sent response: " + RESPONSE);
@@ -67,11 +76,13 @@ abstract class DuplexTest {
 			} else {
 				System.out.println("No challenge");
 			}
-			d.dispose(false, true);
+			r.dispose(false, true);
+			w.dispose(false);
 		} catch(IOException e) {
 			e.printStackTrace();
 			try {
-				d.dispose(true, true);
+				r.dispose(true, true);
+				w.dispose(true);
 			} catch(IOException e1) {
 				e1.printStackTrace();
 			}
diff --git a/briar-tests/src/org/briarproject/plugins/file/RemovableDrivePluginTest.java b/briar-tests/src/org/briarproject/plugins/file/RemovableDrivePluginTest.java
index b799262185d751e8cc068677e41ce8d6244d2a94..b8e4ee88b24ac4fe00e2a7babdb3c973d32f465e 100644
--- a/briar-tests/src/org/briarproject/plugins/file/RemovableDrivePluginTest.java
+++ b/briar-tests/src/org/briarproject/plugins/file/RemovableDrivePluginTest.java
@@ -15,8 +15,8 @@ import org.briarproject.BriarTestCase;
 import org.briarproject.TestFileUtils;
 import org.briarproject.TestUtils;
 import org.briarproject.api.ContactId;
+import org.briarproject.api.plugins.TransportConnectionWriter;
 import org.briarproject.api.plugins.simplex.SimplexPluginCallback;
-import org.briarproject.api.plugins.simplex.SimplexTransportWriter;
 import org.briarproject.api.system.FileUtils;
 import org.briarproject.plugins.ImmediateExecutor;
 import org.briarproject.plugins.file.RemovableDriveMonitor.Callback;
@@ -32,6 +32,7 @@ public class RemovableDrivePluginTest extends BriarTestCase {
 	private final ContactId contactId = new ContactId(234);
 	private final FileUtils fileUtils = new TestFileUtils();
 
+	@Override
 	@Before
 	public void setUp() {
 		testDir.mkdirs();
@@ -253,7 +254,7 @@ public class RemovableDrivePluginTest extends BriarTestCase {
 				fileUtils, callback, finder, monitor, MAX_FRAME_LENGTH, 0);
 		plugin.start();
 
-		SimplexTransportWriter writer = plugin.createWriter(contactId);
+		TransportConnectionWriter writer = plugin.createWriter(contactId);
 		assertNotNull(writer);
 		// The output file should exist and should be empty
 		File[] files = drive1.listFiles();
@@ -352,6 +353,7 @@ public class RemovableDrivePluginTest extends BriarTestCase {
 		context.assertIsSatisfied();
 	}
 
+	@Override
 	@After
 	public void tearDown() {
 		TestUtils.deleteTestDirectory(testDir);
diff --git a/briar-tests/src/org/briarproject/plugins/modem/ModemPluginTest.java b/briar-tests/src/org/briarproject/plugins/modem/ModemPluginTest.java
index d008330a28e3e77cd04c19099b521fea5975c35c..1abf9d24402d61938abb3b3df38420968679fafd 100644
--- a/briar-tests/src/org/briarproject/plugins/modem/ModemPluginTest.java
+++ b/briar-tests/src/org/briarproject/plugins/modem/ModemPluginTest.java
@@ -278,7 +278,8 @@ public class ModemPluginTest extends BriarTestCase {
 		public Object invoke(Invocation invocation) throws Throwable {
 			DuplexTransportConnection conn =
 					(DuplexTransportConnection) invocation.getParameter(1);
-			conn.dispose(false, true);
+			conn.getReader().dispose(false, true);
+			conn.getWriter().dispose(false);
 			invoked.countDown();
 			return null;
 		}
diff --git a/briar-tests/src/org/briarproject/plugins/tcp/LanTcpPluginTest.java b/briar-tests/src/org/briarproject/plugins/tcp/LanTcpPluginTest.java
index fcbb30c52bbcc228481eb7d9d2ea1049f6c51f46..f3a321bbcc0242b84c137c9e2cce7512c795c9a8 100644
--- a/briar-tests/src/org/briarproject/plugins/tcp/LanTcpPluginTest.java
+++ b/briar-tests/src/org/briarproject/plugins/tcp/LanTcpPluginTest.java
@@ -148,7 +148,8 @@ public class LanTcpPluginTest extends BriarTestCase {
 		assertTrue(latch.await(5, SECONDS));
 		assertFalse(error.get());
 		// Clean up
-		d.dispose(false, true);
+		d.getReader().dispose(false, true);
+		d.getWriter().dispose(false);
 		ss.close();
 		plugin.stop();
 	}
diff --git a/briar-tests/src/org/briarproject/transport/OutgoingEncryptionLayerTest.java b/briar-tests/src/org/briarproject/transport/OutgoingEncryptionLayerTest.java
index d3a7746a1fa14370e42581ce8249d9d891a09344..93f0a53f9b83e4b81676dbf76a878e6ae5d0bdc0 100644
--- a/briar-tests/src/org/briarproject/transport/OutgoingEncryptionLayerTest.java
+++ b/briar-tests/src/org/briarproject/transport/OutgoingEncryptionLayerTest.java
@@ -25,8 +25,6 @@ public class OutgoingEncryptionLayerTest extends BriarTestCase {
 	// FIXME: This is an integration test, not a unit test
 
 	private static final int FRAME_LENGTH = 1024;
-	private static final int MAX_PAYLOAD_LENGTH =
-			FRAME_LENGTH - HEADER_LENGTH - MAC_LENGTH;
 
 	private final CryptoComponent crypto;
 	private final AuthenticatedCipher frameCipher;
@@ -56,7 +54,7 @@ public class OutgoingEncryptionLayerTest extends BriarTestCase {
 		// Check that the actual tag and ciphertext match what's expected
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out,
-				10 * FRAME_LENGTH, frameCipher, frameKey, FRAME_LENGTH, tag);
+				frameCipher, frameKey, FRAME_LENGTH, tag);
 		o.writeFrame(new byte[FRAME_LENGTH - MAC_LENGTH], payloadLength, false);
 		byte[] actual = out.toByteArray();
 		assertEquals(TAG_LENGTH + FRAME_LENGTH, actual.length);
@@ -67,93 +65,14 @@ public class OutgoingEncryptionLayerTest extends BriarTestCase {
 	}
 
 	@Test
-	public void testInitiatorClosesConnectionWithoutWriting() throws Exception {
+	public void testCloseConnectionWithoutWriting() throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		// Initiator's constructor
 		OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out,
-				10 * FRAME_LENGTH, frameCipher, crypto.generateSecretKey(),
-				FRAME_LENGTH, tag);
+				frameCipher, crypto.generateSecretKey(), FRAME_LENGTH, tag);
 		// Write an empty final frame without having written any other frames
 		o.writeFrame(new byte[FRAME_LENGTH - MAC_LENGTH], 0, true);
 		// The tag and the empty frame should be written to the output stream
 		assertEquals(TAG_LENGTH + HEADER_LENGTH + MAC_LENGTH, out.size());
 	}
-
-	@Test
-	public void testResponderClosesConnectionWithoutWriting() throws Exception {
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		// Responder's constructor
-		OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out,
-				10 * FRAME_LENGTH, frameCipher, crypto.generateSecretKey(),
-				FRAME_LENGTH);
-		// Write an empty final frame without having written any other frames
-		o.writeFrame(new byte[FRAME_LENGTH - MAC_LENGTH], 0, true);
-		// An empty final frame should be written to the output stream
-		assertEquals(HEADER_LENGTH + MAC_LENGTH, out.size());
-	}
-
-	@Test
-	public void testRemainingCapacityWithTag() throws Exception {
-		int MAX_PAYLOAD_LENGTH = FRAME_LENGTH - HEADER_LENGTH - MAC_LENGTH;
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		// Initiator's constructor
-		OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out,
-				10 * FRAME_LENGTH, frameCipher, crypto.generateSecretKey(),
-				FRAME_LENGTH, tag);
-		// There should be space for nine full frames and one partial frame
-		byte[] frame = new byte[FRAME_LENGTH - MAC_LENGTH];
-		assertEquals(10 * MAX_PAYLOAD_LENGTH - TAG_LENGTH,
-				o.getRemainingCapacity());
-		// Write nine frames, each containing a partial payload
-		for(int i = 0; i < 9; i++) {
-			o.writeFrame(frame, 123, false);
-			assertEquals((9 - i) * MAX_PAYLOAD_LENGTH - TAG_LENGTH,
-					o.getRemainingCapacity());
-		}
-		// Write the final frame, which will not be padded
-		o.writeFrame(frame, 123, true);
-		int finalFrameLength = HEADER_LENGTH + 123 + MAC_LENGTH;
-		assertEquals(MAX_PAYLOAD_LENGTH - TAG_LENGTH - finalFrameLength,
-				o.getRemainingCapacity());
-	}
-
-	@Test
-	public void testRemainingCapacityWithoutTag() throws Exception {
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		// Responder's constructor
-		OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out,
-				10 * FRAME_LENGTH, frameCipher, crypto.generateSecretKey(),
-				FRAME_LENGTH);
-		// There should be space for ten full frames
-		assertEquals(10 * MAX_PAYLOAD_LENGTH, o.getRemainingCapacity());
-		// Write nine frames, each containing a partial payload
-		byte[] frame = new byte[FRAME_LENGTH - MAC_LENGTH];
-		for(int i = 0; i < 9; i++) {
-			o.writeFrame(frame, 123, false);
-			assertEquals((9 - i) * MAX_PAYLOAD_LENGTH,
-					o.getRemainingCapacity());
-		}
-		// Write the final frame, which will not be padded
-		o.writeFrame(frame, 123, true);
-		int finalFrameLength = HEADER_LENGTH + 123 + MAC_LENGTH;
-		assertEquals(MAX_PAYLOAD_LENGTH - finalFrameLength,
-				o.getRemainingCapacity());
-	}
-
-	@Test
-	public void testRemainingCapacityLimitedByFrameNumbers() throws Exception {
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		// The connection has plenty of space so we're limited by frame numbers
-		OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out,
-				Long.MAX_VALUE, frameCipher, crypto.generateSecretKey(),
-				FRAME_LENGTH);
-		// There should be enough frame numbers for 2^32 frames
-		assertEquals((1L << 32) * MAX_PAYLOAD_LENGTH, o.getRemainingCapacity());
-		// Write a frame containing a partial payload
-		byte[] frame = new byte[FRAME_LENGTH - MAC_LENGTH];
-		o.writeFrame(frame, 123, false);
-		// There should be enough frame numbers for 2^32 - 1 frames
-		assertEquals(((1L << 32) - 1) * MAX_PAYLOAD_LENGTH,
-				o.getRemainingCapacity());
-	}
 }
diff --git a/briar-tests/src/org/briarproject/transport/TransportIntegrationTest.java b/briar-tests/src/org/briarproject/transport/TransportIntegrationTest.java
index 742e2074128fd4602ecb54fbf03ffa7b9b25363e..19153db75a48546db0f4f12a6c827568ffd7511c 100644
--- a/briar-tests/src/org/briarproject/transport/TransportIntegrationTest.java
+++ b/briar-tests/src/org/briarproject/transport/TransportIntegrationTest.java
@@ -1,12 +1,11 @@
 package org.briarproject.transport;
 
-import static org.briarproject.api.messaging.MessagingConstants.MAX_PACKET_LENGTH;
-import static org.briarproject.api.transport.TransportConstants.MAX_FRAME_LENGTH;
-import static org.briarproject.api.transport.TransportConstants.MIN_STREAM_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH;
 import static org.junit.Assert.assertArrayEquals;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.Random;
@@ -14,13 +13,9 @@ import java.util.Random;
 import org.briarproject.BriarTestCase;
 import org.briarproject.TestLifecycleModule;
 import org.briarproject.TestSystemModule;
-import org.briarproject.api.ContactId;
-import org.briarproject.api.TransportId;
 import org.briarproject.api.crypto.AuthenticatedCipher;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.crypto.SecretKey;
-import org.briarproject.api.transport.StreamContext;
-import org.briarproject.api.transport.StreamWriter;
 import org.briarproject.api.transport.StreamWriterFactory;
 import org.briarproject.crypto.CryptoModule;
 import org.junit.Test;
@@ -35,13 +30,10 @@ public class TransportIntegrationTest extends BriarTestCase {
 	private final int FRAME_LENGTH = 2048;
 
 	private final CryptoComponent crypto;
-	private final StreamWriterFactory streamWriterFactory;
-	private final ContactId contactId;
-	private final TransportId transportId;
 	private final AuthenticatedCipher frameCipher;
 	private final Random random;
 	private final byte[] secret;
-	private final SecretKey frameKey;
+	private final SecretKey tagKey, frameKey;
 
 	public TransportIntegrationTest() {
 		Module testModule = new AbstractModule() {
@@ -54,15 +46,13 @@ public class TransportIntegrationTest extends BriarTestCase {
 		Injector i = Guice.createInjector(testModule, new CryptoModule(),
 				new TestLifecycleModule(), new TestSystemModule());
 		crypto = i.getInstance(CryptoComponent.class);
-		streamWriterFactory = i.getInstance(StreamWriterFactory.class);
-		contactId = new ContactId(234);
-		transportId = new TransportId("id");
 		frameCipher = crypto.getFrameCipher();
 		random = new Random();
 		// Since we're sending frames to ourselves, we only need outgoing keys
 		secret = new byte[32];
 		random.nextBytes(secret);
-		frameKey = crypto.deriveFrameKey(secret, 0, true, true);
+		tagKey = crypto.deriveTagKey(secret, true);
+		frameKey = crypto.deriveFrameKey(secret, 0, true);
 	}
 
 	@Test
@@ -76,6 +66,9 @@ public class TransportIntegrationTest extends BriarTestCase {
 	}
 
 	private void testWriteAndRead(boolean initiator) throws Exception {
+		// Encode the tag
+		byte[] tag = new byte[TAG_LENGTH];
+		crypto.encodeTag(tag, tagKey, 0);
 		// Generate two random frames
 		byte[] frame = new byte[1234];
 		random.nextBytes(frame);
@@ -83,87 +76,46 @@ public class TransportIntegrationTest extends BriarTestCase {
 		random.nextBytes(frame1);
 		// Copy the frame key - the copy will be erased
 		SecretKey frameCopy = frameKey.copy();
-		// Write the frames
+		// Write the tag and the frames
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		FrameWriter encryptionOut = new OutgoingEncryptionLayer(out,
-				Long.MAX_VALUE, frameCipher, frameCopy, FRAME_LENGTH);
-		StreamWriterImpl writer = new StreamWriterImpl(encryptionOut,
+		FrameWriter frameWriter = new OutgoingEncryptionLayer(out,
+				frameCipher, frameCopy, FRAME_LENGTH, tag);
+		StreamWriterImpl streamWriter = new StreamWriterImpl(frameWriter,
 				FRAME_LENGTH);
-		OutputStream out1 = writer.getOutputStream();
+		OutputStream out1 = streamWriter.getOutputStream();
 		out1.write(frame);
 		out1.flush();
 		out1.write(frame1);
 		out1.flush();
 		byte[] output = out.toByteArray();
-		assertEquals(FRAME_LENGTH * 2, output.length);
-		// Read the tag and the frames back
+		assertEquals(TAG_LENGTH + FRAME_LENGTH * 2, output.length);
+		// Read the tag back
 		ByteArrayInputStream in = new ByteArrayInputStream(output);
-		FrameReader encryptionIn = new IncomingEncryptionLayer(in, frameCipher,
+		byte[] recoveredTag = new byte[tag.length];
+		read(in, recoveredTag);
+		assertArrayEquals(tag, recoveredTag);
+		// Read the frames back
+		FrameReader frameReader = new IncomingEncryptionLayer(in, frameCipher,
 				frameKey, FRAME_LENGTH);
-		StreamReaderImpl reader = new StreamReaderImpl(encryptionIn,
+		StreamReaderImpl streamReader = new StreamReaderImpl(frameReader,
 				FRAME_LENGTH);
-		InputStream in1 = reader.getInputStream();
-		byte[] recovered = new byte[frame.length];
+		InputStream in1 = streamReader.getInputStream();
+		byte[] recoveredFrame = new byte[frame.length];
+		read(in1, recoveredFrame);
+		assertArrayEquals(frame, recoveredFrame);
+		byte[] recoveredFrame1 = new byte[frame1.length];
+		read(in1, recoveredFrame1);
+		assertArrayEquals(frame1, recoveredFrame1);
+		streamWriter.close();
+		streamReader.close();
+	}
+
+	private void read(InputStream in, byte[] dest) throws IOException {
 		int offset = 0;
-		while(offset < recovered.length) {
-			int read = in1.read(recovered, offset, recovered.length - offset);
-			if(read == -1) break;
-			offset += read;
-		}
-		assertEquals(recovered.length, offset);
-		assertArrayEquals(frame, recovered);
-		byte[] recovered1 = new byte[frame1.length];
-		offset = 0;
-		while(offset < recovered1.length) {
-			int read = in1.read(recovered1, offset, recovered1.length - offset);
+		while(offset < dest.length) {
+			int read = in.read(dest, offset, dest.length - offset);
 			if(read == -1) break;
 			offset += read;
 		}
-		assertEquals(recovered1.length, offset);
-		assertArrayEquals(frame1, recovered1);
-		writer.close();
-		reader.close();
-	}
-
-	@Test
-	public void testOverheadWithTag() throws Exception {
-		ByteArrayOutputStream out =
-				new ByteArrayOutputStream(MIN_STREAM_LENGTH);
-		StreamContext ctx = new StreamContext(contactId, transportId,
-				secret, 0, true);
-		StreamWriter w = streamWriterFactory.createStreamWriter(out,
-				MAX_FRAME_LENGTH, MIN_STREAM_LENGTH, ctx, false, true);
-		// Check that the connection writer thinks there's room for a packet
-		long capacity = w.getRemainingCapacity();
-		assertTrue(capacity > MAX_PACKET_LENGTH);
-		assertTrue(capacity < MIN_STREAM_LENGTH);
-		// Check that there really is room for a packet
-		byte[] payload = new byte[MAX_PACKET_LENGTH];
-		w.getOutputStream().write(payload);
-		w.getOutputStream().close();
-		long used = out.size();
-		assertTrue(used > MAX_PACKET_LENGTH);
-		assertTrue(used <= MIN_STREAM_LENGTH);
-	}
-
-	@Test
-	public void testOverheadWithoutTag() throws Exception {
-		ByteArrayOutputStream out =
-				new ByteArrayOutputStream(MIN_STREAM_LENGTH);
-		StreamContext ctx = new StreamContext(contactId, transportId,
-				secret, 0, true);
-		StreamWriter w = streamWriterFactory.createStreamWriter(out,
-				MAX_FRAME_LENGTH, MIN_STREAM_LENGTH, ctx, false, false);
-		// Check that the connection writer thinks there's room for a packet
-		long capacity = w.getRemainingCapacity();
-		assertTrue(capacity > MAX_PACKET_LENGTH);
-		assertTrue(capacity < MIN_STREAM_LENGTH);
-		// Check that there really is room for a packet
-		byte[] payload = new byte[MAX_PACKET_LENGTH];
-		w.getOutputStream().write(payload);
-		w.getOutputStream().close();
-		long used = out.size();
-		assertTrue(used > MAX_PACKET_LENGTH);
-		assertTrue(used <= MIN_STREAM_LENGTH);
 	}
 }