diff --git a/api/net/sf/briar/api/transport/batch/BatchTransportCallback.java b/api/net/sf/briar/api/transport/batch/BatchTransportCallback.java
index 33423e3b72afef1750f237d72a07ac2eab12ed9c..7f335c6068daad41a6d834a1c4dea0217cc24bff 100644
--- a/api/net/sf/briar/api/transport/batch/BatchTransportCallback.java
+++ b/api/net/sf/briar/api/transport/batch/BatchTransportCallback.java
@@ -1,7 +1,6 @@
 package net.sf.briar.api.transport.batch;
 
 import net.sf.briar.api.ContactId;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.transport.TransportCallback;
 
 /**
@@ -12,6 +11,5 @@ public interface BatchTransportCallback extends TransportCallback {
 
 	void readerCreated(BatchTransportReader r);
 
-	void writerCreated(ContactId contactId, TransportId t, long connection,
-			BatchTransportWriter w);
+	void writerCreated(ContactId contactId, BatchTransportWriter w);
 }
diff --git a/api/net/sf/briar/api/transport/batch/BatchTransportPlugin.java b/api/net/sf/briar/api/transport/batch/BatchTransportPlugin.java
index 09482f12bf468780631c87402eeaef3501e28ca7..4bd53dd5be9a85074ca61f5d97281e91f516ee59 100644
--- a/api/net/sf/briar/api/transport/batch/BatchTransportPlugin.java
+++ b/api/net/sf/briar/api/transport/batch/BatchTransportPlugin.java
@@ -50,10 +50,10 @@ public interface BatchTransportPlugin {
 	boolean shouldPoll();
 
 	/**
-	 * Returns the desired interval in seconds between calls to the plugin's
-	 * poll() method.
+	 * Returns the desired interval in milliseconds between calls to the
+	 * plugin's poll() method.
 	 */
-	int getPollingInterval();
+	long getPollingInterval();
 
 	/**
 	 * Attempts to establish incoming and/or outgoing connections using the
diff --git a/api/net/sf/briar/api/transport/stream/StreamTransportCallback.java b/api/net/sf/briar/api/transport/stream/StreamTransportCallback.java
index f244160de75b966cce26eeae9bd7a6242aff0a88..2e41f18f1300a38046c6ce5f4026af9f2154236a 100644
--- a/api/net/sf/briar/api/transport/stream/StreamTransportCallback.java
+++ b/api/net/sf/briar/api/transport/stream/StreamTransportCallback.java
@@ -1,7 +1,6 @@
 package net.sf.briar.api.transport.stream;
 
 import net.sf.briar.api.ContactId;
-import net.sf.briar.api.TransportId;
 import net.sf.briar.api.transport.TransportCallback;
 
 /**
@@ -12,6 +11,6 @@ public interface StreamTransportCallback extends TransportCallback {
 
 	void incomingConnectionCreated(StreamTransportConnection c);
 
-	void outgoingConnectionCreated(ContactId contactId, TransportId t,
-			long connection, StreamTransportConnection c);
+	void outgoingConnectionCreated(ContactId contactId,
+			StreamTransportConnection c);
 }
diff --git a/api/net/sf/briar/api/transport/stream/StreamTransportConnection.java b/api/net/sf/briar/api/transport/stream/StreamTransportConnection.java
index c66edb69f656eedd7a4d05b68647e8a02ac286a9..aa3c25c81822aa909504114e285735d6a195f727 100644
--- a/api/net/sf/briar/api/transport/stream/StreamTransportConnection.java
+++ b/api/net/sf/briar/api/transport/stream/StreamTransportConnection.java
@@ -28,5 +28,5 @@ public interface StreamTransportConnection {
 	 * connection is not used, or if an exception is thrown while using the
 	 * connection.
 	 */
-	void close() throws IOException;
+	void dispose() throws IOException;
 }
diff --git a/api/net/sf/briar/api/transport/stream/StreamTransportPlugin.java b/api/net/sf/briar/api/transport/stream/StreamTransportPlugin.java
index 970d58b2c4c0e1b0be3d3df210b0e91d8f5a4e7b..ff2c78bb6a25977d48f012a3022df9fdbd766437 100644
--- a/api/net/sf/briar/api/transport/stream/StreamTransportPlugin.java
+++ b/api/net/sf/briar/api/transport/stream/StreamTransportPlugin.java
@@ -49,10 +49,10 @@ public interface StreamTransportPlugin {
 	boolean shouldPoll();
 
 	/**
-	 * Returns the desired interval in seconds between calls to the plugin's
-	 * poll() method.
+	 * Returns the desired interval in milliseconds between calls to the
+	 * plugin's poll() method.
 	 */
-	int getPollingInterval();
+	long getPollingInterval();
 
 	/**
 	 * Attempts to establish connections using the current transport and
diff --git a/components/net/sf/briar/plugins/file/FilePlugin.java b/components/net/sf/briar/plugins/file/FilePlugin.java
index 1ca4e7fd87cf9765f1d35520f16ccfc354bf3056..ca0a5c7e2785b4ec175e4dd484941ad1a547b99c 100644
--- a/components/net/sf/briar/plugins/file/FilePlugin.java
+++ b/components/net/sf/briar/plugins/file/FilePlugin.java
@@ -74,18 +74,6 @@ abstract class FilePlugin implements BatchTransportPlugin {
 		this.config = config;
 	}
 
-	public boolean shouldPoll() {
-		return false;
-	}
-
-	public int getPollingInterval() {
-		return 0;
-	}
-
-	public void poll() {
-		throw new UnsupportedOperationException();
-	}
-
 	public BatchTransportReader createReader(ContactId c) {
 		return null;
 	}
diff --git a/components/net/sf/briar/plugins/file/RemovableDrivePlugin.java b/components/net/sf/briar/plugins/file/RemovableDrivePlugin.java
index e3df83ed704921908396aac37cd925918e561200..8810933439df485d5ff1043198f991f3d7422187 100644
--- a/components/net/sf/briar/plugins/file/RemovableDrivePlugin.java
+++ b/components/net/sf/briar/plugins/file/RemovableDrivePlugin.java
@@ -48,6 +48,18 @@ implements RemovableDriveMonitor.Callback {
 		monitor.stop();
 	}
 
+	public boolean shouldPoll() {
+		return false;
+	}
+
+	public long getPollingInterval() {
+		return 0L;
+	}
+
+	public void poll() {
+		throw new UnsupportedOperationException();
+	}
+
 	@Override
 	protected File chooseOutputDirectory() {
 		try {
diff --git a/components/net/sf/briar/plugins/socket/SimpleSocketPlugin.java b/components/net/sf/briar/plugins/socket/SimpleSocketPlugin.java
new file mode 100644
index 0000000000000000000000000000000000000000..2734a484a340fa33711ede54bfc4ca3793f00164
--- /dev/null
+++ b/components/net/sf/briar/plugins/socket/SimpleSocketPlugin.java
@@ -0,0 +1,79 @@
+package net.sf.briar.plugins.socket;
+
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+import net.sf.briar.api.ContactId;
+import net.sf.briar.api.TransportId;
+
+public class SimpleSocketPlugin extends SocketPlugin {
+
+	public static final int TRANSPORT_ID = 1;
+
+	private static final TransportId id = new TransportId(TRANSPORT_ID);
+
+	private final long pollingInterval;
+
+	SimpleSocketPlugin(Executor executor, long pollingInterval) {
+		super(executor);
+		this.pollingInterval = pollingInterval;
+	}
+	
+	public TransportId getId() {
+		return id;
+	}
+
+	public boolean shouldPoll() {
+		return true;
+	}
+
+	public long getPollingInterval() {
+		return pollingInterval;
+	}
+
+	@Override
+	protected SocketAddress getLocalSocketAddress() {
+		Map<String, String> properties;
+		synchronized(this) {
+			properties = localProperties;
+		}
+		if(properties == null) return null;
+		return createSocketAddress(properties);
+	}
+
+	@Override
+	protected SocketAddress getSocketAddress(ContactId c) {
+		Map<String, String> properties;
+		synchronized(this) {
+			properties = remoteProperties.get(c);
+		}
+		if(properties == null) return null;
+		return createSocketAddress(properties);
+	}
+
+	private SocketAddress createSocketAddress(Map<String, String> properties) {
+		String host = properties.get("host");
+		String portString = properties.get("port");
+		if(host == null || portString == null) return null;
+		int port;
+		try {
+			port = Integer.valueOf(portString);
+		} catch(NumberFormatException e) {
+			return null;
+		}
+		return InetSocketAddress.createUnresolved(host, port);
+	}
+
+	@Override
+	protected Socket createClientSocket() {
+		return new Socket();
+	}
+
+	@Override
+	protected Socket createServerSocket() {
+		return new Socket();
+	}
+}
diff --git a/components/net/sf/briar/plugins/socket/SocketPlugin.java b/components/net/sf/briar/plugins/socket/SocketPlugin.java
new file mode 100644
index 0000000000000000000000000000000000000000..b24fb6556dca406b4eb57b9c00d5ef19fc7e8454
--- /dev/null
+++ b/components/net/sf/briar/plugins/socket/SocketPlugin.java
@@ -0,0 +1,122 @@
+package net.sf.briar.plugins.socket;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+import net.sf.briar.api.ContactId;
+import net.sf.briar.api.transport.InvalidConfigException;
+import net.sf.briar.api.transport.InvalidTransportException;
+import net.sf.briar.api.transport.stream.StreamTransportCallback;
+import net.sf.briar.api.transport.stream.StreamTransportConnection;
+import net.sf.briar.api.transport.stream.StreamTransportPlugin;
+
+abstract class SocketPlugin implements StreamTransportPlugin {
+
+	private final Executor executor;
+
+	protected Map<String, String> localProperties = null;
+	protected Map<ContactId, Map<String, String>> remoteProperties = null;
+	protected Map<String, String> config = null;
+	protected StreamTransportCallback callback = null;
+
+	private volatile boolean started = false;
+
+	protected abstract SocketAddress getLocalSocketAddress();
+	protected abstract SocketAddress getSocketAddress(ContactId c);
+	protected abstract Socket createClientSocket();
+	protected abstract Socket createServerSocket();
+
+	SocketPlugin(Executor executor) {
+		this.executor = executor;
+	}
+
+	public synchronized void start(Map<String, String> localProperties,
+			Map<ContactId, Map<String, String>> remoteProperties,
+			Map<String, String> config, StreamTransportCallback callback)
+	throws InvalidTransportException, InvalidConfigException {
+		if(started) throw new IllegalStateException();
+		started = true;
+		this.localProperties = localProperties;
+		this.remoteProperties = remoteProperties;
+		this.config = config;
+		this.callback = callback;
+		executor.execute(createBinder());
+	}
+
+	protected Runnable createBinder() {
+		return new Runnable() {
+			public void run() {
+				SocketAddress addr = getLocalSocketAddress();
+				if(addr == null) return;
+				Socket s = createServerSocket();
+				try {
+					s.bind(addr);
+				} catch(IOException e) {
+					return;
+				}
+			}
+		};
+	}
+
+	public synchronized void stop() {
+		if(!started) throw new IllegalStateException();
+		started = false;
+	}
+
+	public synchronized void setLocalProperties(Map<String, String> properties)
+	throws InvalidTransportException {
+		if(!started) throw new IllegalStateException();
+		localProperties = properties;
+	}
+
+	public synchronized void setRemoteProperties(ContactId c,
+			Map<String, String> properties)
+	throws InvalidTransportException {
+		if(!started) throw new IllegalStateException();
+		remoteProperties.put(c, properties);
+	}
+
+	public synchronized void setConfig(Map<String, String> config)
+	throws InvalidConfigException {
+		if(!started) throw new IllegalStateException();
+		this.config = config;
+	}
+
+	public synchronized void poll() {
+		if(!shouldPoll()) throw new UnsupportedOperationException();
+		if(!started) throw new IllegalStateException();
+		for(ContactId c : remoteProperties.keySet()) {
+			executor.execute(createConnector(c));
+		}
+	}
+
+	protected Runnable createConnector(final ContactId c) {
+		return new Runnable() {
+			public void run() {
+				StreamTransportConnection conn = createAndConnectSocket(c);
+				if(conn != null) callback.outgoingConnectionCreated(c, conn);
+			}
+		};
+	}
+
+	public StreamTransportConnection createConnection(ContactId c) {
+		if(!started) throw new IllegalStateException();
+		return createAndConnectSocket(c);
+	}
+
+	private StreamTransportConnection createAndConnectSocket(ContactId c) {
+		if(!started) return null;
+		SocketAddress addr = getSocketAddress(c);
+		if(addr == null) return null;
+		Socket s = createClientSocket();
+		try {
+			s.connect(addr);
+		} catch(IOException e) {
+			return null;
+		}
+		return new SocketTransportConnection(s);
+	}
+}
diff --git a/components/net/sf/briar/plugins/socket/SocketTransportConnection.java b/components/net/sf/briar/plugins/socket/SocketTransportConnection.java
new file mode 100644
index 0000000000000000000000000000000000000000..92488fefff7c132fe208b971cdf18d6a553a7bde
--- /dev/null
+++ b/components/net/sf/briar/plugins/socket/SocketTransportConnection.java
@@ -0,0 +1,38 @@
+package net.sf.briar.plugins.socket;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+
+import net.sf.briar.api.transport.stream.StreamTransportConnection;
+
+class SocketTransportConnection implements StreamTransportConnection {
+
+	private final Socket socket;
+
+	private boolean streamInUse = false;
+
+	SocketTransportConnection(Socket socket) {
+		this.socket = socket;
+	}
+
+	public InputStream getInputStream() throws IOException {
+		streamInUse = true;
+		return socket.getInputStream();
+	}
+
+	public OutputStream getOutputStream() throws IOException {
+		streamInUse = true;
+		return socket.getOutputStream();
+	}
+
+	public void finish() throws IOException {
+		// FIXME: Tell the plugin?
+		streamInUse = false;
+	}
+
+	public void dispose() throws IOException {
+		if(streamInUse) socket.close();
+	}
+}