diff --git a/libs/weupnp-0.1.1.jar b/libs/weupnp-0.1.1.jar
new file mode 100644
index 0000000000000000000000000000000000000000..2626a76c0284e8ca9e42bf5ce111ba1afb7527ab
Binary files /dev/null and b/libs/weupnp-0.1.1.jar differ
diff --git a/src/net/sf/briar/HelloWorldModule.java b/src/net/sf/briar/HelloWorldModule.java
index 64a3d8fcf23f49682445f87c0a3af7ca3a11c18d..a0fa22c7f89a6b65cc1874c32ff2d50b61de3f39 100644
--- a/src/net/sf/briar/HelloWorldModule.java
+++ b/src/net/sf/briar/HelloWorldModule.java
@@ -1,13 +1,10 @@
 package net.sf.briar;
 
-import static android.content.Context.MODE_PRIVATE;
-
 import java.io.File;
 
 import net.sf.briar.api.crypto.Password;
 import net.sf.briar.api.db.DatabaseConfig;
 import net.sf.briar.api.ui.UiCallback;
-import android.content.Context;
 
 import com.google.inject.AbstractModule;
 
@@ -16,7 +13,7 @@ public class HelloWorldModule extends AbstractModule {
 	private final DatabaseConfig config;
 	private final UiCallback callback;
 
-	public HelloWorldModule(final Context appContext) {
+	public HelloWorldModule(final File dir) {
 		final Password password = new Password() {
 
 			public char[] getPassword() {
@@ -26,7 +23,7 @@ public class HelloWorldModule extends AbstractModule {
 		config = new DatabaseConfig() {
 
 			public File getDataDirectory() {
-				return appContext.getDir("db", MODE_PRIVATE);
+				return dir;
 			}
 
 			public Password getPassword() {
diff --git a/src/net/sf/briar/HelloWorldService.java b/src/net/sf/briar/HelloWorldService.java
index 2eec307890beedfa8c3fc8844a5d883b5bb1ba49..6dbbb30790aa9c003cd122fe9d814384ca4344b9 100644
--- a/src/net/sf/briar/HelloWorldService.java
+++ b/src/net/sf/briar/HelloWorldService.java
@@ -3,6 +3,7 @@ package net.sf.briar;
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 
+import java.io.File;
 import java.io.IOException;
 import java.util.logging.Logger;
 
@@ -55,8 +56,8 @@ public class HelloWorldService extends Service implements Runnable {
 	}
 
 	public void run() {
-		Injector i = Guice.createInjector(
-				new HelloWorldModule(getApplicationContext()),
+		File dir = getApplicationContext().getDir("db", MODE_PRIVATE);
+		Injector i = Guice.createInjector(new HelloWorldModule(dir),
 				new AndroidModule(), new ClockModule(), new CryptoModule(),
 				new DatabaseModule(), new LifecycleModule(),
 				new PluginsModule(), new ProtocolModule(),
@@ -77,7 +78,7 @@ public class HelloWorldService extends Service implements Runnable {
 				LOG.info(pluginsStarted + " plugins started");
 			// ...sleep...
 			try {
-				Thread.sleep(1000);
+				Thread.sleep(30 * 1000);
 			} catch(InterruptedException ignored) {}
 			// ...and stop
 			if(LOG.isLoggable(INFO)) LOG.info("Shutting down");
diff --git a/src/net/sf/briar/plugins/tcp/MappingResult.java b/src/net/sf/briar/plugins/tcp/MappingResult.java
new file mode 100644
index 0000000000000000000000000000000000000000..f558a8d731f45d162900485e9ba8253a5a896872
--- /dev/null
+++ b/src/net/sf/briar/plugins/tcp/MappingResult.java
@@ -0,0 +1,32 @@
+package net.sf.briar.plugins.tcp;
+
+import java.net.InetAddress;
+
+class MappingResult {
+
+	private final InetAddress internal, external;
+	private final boolean succeeded;
+
+	MappingResult(InetAddress internal, InetAddress external,
+			boolean succeeded) {
+		this.internal = internal;
+		this.external = external;
+		this.succeeded = succeeded;
+	}
+
+	InetAddress getInternal() {
+		return internal;
+	}
+
+	InetAddress getExternal() {
+		return external;
+	}
+
+	boolean getSucceeded() {
+		return succeeded;
+	}
+
+	boolean isUsable() {
+		return internal != null && external != null && succeeded;
+	}
+}
diff --git a/src/net/sf/briar/plugins/tcp/PortMapper.java b/src/net/sf/briar/plugins/tcp/PortMapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..5bd226329df1e9496bf87d35e97cfc2a9376a0a6
--- /dev/null
+++ b/src/net/sf/briar/plugins/tcp/PortMapper.java
@@ -0,0 +1,10 @@
+package net.sf.briar.plugins.tcp;
+
+interface PortMapper {
+
+	void start();
+
+	void stop();
+
+	MappingResult map(int port);
+}
diff --git a/src/net/sf/briar/plugins/tcp/PortMapperImpl.java b/src/net/sf/briar/plugins/tcp/PortMapperImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..e990cb2e38eb1e90c3301dcac142400821b089ec
--- /dev/null
+++ b/src/net/sf/briar/plugins/tcp/PortMapperImpl.java
@@ -0,0 +1,82 @@
+package net.sf.briar.plugins.tcp;
+
+import static java.util.logging.Level.WARNING;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.Collection;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.logging.Logger;
+
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.wetorrent.upnp.GatewayDevice;
+import org.wetorrent.upnp.GatewayDiscover;
+import org.xml.sax.SAXException;
+
+class PortMapperImpl implements PortMapper {
+
+	private static final Logger LOG =
+			Logger.getLogger(PortMapperImpl.class.getName());
+
+	private final CountDownLatch started = new CountDownLatch(1);
+	private final Collection<Integer> ports =
+			new CopyOnWriteArrayList<Integer>();
+
+	private volatile GatewayDevice gateway = null;
+
+	public void start() {
+		GatewayDiscover d = new GatewayDiscover();
+		try {
+			d.discover();
+		} catch(IOException e) {
+			if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
+		} catch(SAXException e) {
+			if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
+		} catch(ParserConfigurationException e) {
+			if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
+		}
+		gateway = d.getValidGateway();
+		started.countDown();
+	}
+
+	public void stop() {
+		if(gateway == null) return;
+		try {
+			for(Integer port: ports) gateway.deletePortMapping(port, "TCP");
+		} catch(IOException e) {
+			if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
+		} catch(SAXException e) {
+			if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
+		}
+	}
+
+	public MappingResult map(int port) {
+		try {
+			started.await();
+		} catch(InterruptedException e) {
+			if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
+			Thread.currentThread().interrupt();
+			return null;
+		}
+		if(gateway == null) return null;
+		InetAddress internal = gateway.getLocalAddress();
+		if(internal == null) return null;
+		boolean succeeded = false;
+		InetAddress external = null;
+		try {
+			succeeded = gateway.addPortMapping(port, port,
+					internal.getHostAddress(), "TCP", "TCP");
+			String externalString = gateway.getExternalIPAddress();
+			if(externalString != null)
+				external = InetAddress.getByName(externalString);
+		} catch(IOException e) {
+			if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
+		} catch(SAXException e) {
+			if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
+		}
+		if(succeeded) ports.add(port);
+		return new MappingResult(internal, external, succeeded);
+	}
+}
diff --git a/src/net/sf/briar/plugins/tcp/TcpPlugin.java b/src/net/sf/briar/plugins/tcp/TcpPlugin.java
index 603cba44e1a171da32005aad5d89679b79842393..f476aad47b60139d81e53d3453390385f82b523a 100644
--- a/src/net/sf/briar/plugins/tcp/TcpPlugin.java
+++ b/src/net/sf/briar/plugins/tcp/TcpPlugin.java
@@ -91,10 +91,11 @@ abstract class TcpPlugin implements DuplexPlugin {
 			socket = ss;
 		}
 		if(LOG.isLoggable(INFO)) {
-			LOG.info("Listening on " + ss.getInetAddress().getHostAddress()
-					+ ":" + ss.getLocalPort());
+			String addr = ss.getInetAddress().getHostAddress();
+			int port = ss.getLocalPort();
+			LOG.info("Listening on " + addr + " " + port);
 		}
-		setLocalSocketAddress(ss.getLocalSocketAddress());
+		setLocalSocketAddress((InetSocketAddress) ss.getLocalSocketAddress());
 		acceptContactConnections(ss);
 	}
 
@@ -106,12 +107,11 @@ abstract class TcpPlugin implements DuplexPlugin {
 		}
 	}
 
-	private void setLocalSocketAddress(SocketAddress s) {
-		InetSocketAddress i = (InetSocketAddress) s;
-		InetAddress addr = i.getAddress();
+	protected void setLocalSocketAddress(InetSocketAddress a) {
+		InetAddress addr = a.getAddress();
 		TransportProperties p = new TransportProperties();
 		p.put("address", addr.getHostAddress());
-		p.put("port", String.valueOf(i.getPort()));
+		p.put("port", String.valueOf(a.getPort()));
 		callback.mergeLocalProperties(p);
 	}
 
diff --git a/src/net/sf/briar/plugins/tcp/WanTcpPlugin.java b/src/net/sf/briar/plugins/tcp/WanTcpPlugin.java
index 69bd3074e0deb8584a71e812f11d8e2b2e926e89..9b2cd234fa938d2ca74e47d024c97dfe30590438 100644
--- a/src/net/sf/briar/plugins/tcp/WanTcpPlugin.java
+++ b/src/net/sf/briar/plugins/tcp/WanTcpPlugin.java
@@ -2,6 +2,7 @@ package net.sf.briar.plugins.tcp;
 
 import static java.util.logging.Level.WARNING;
 
+import java.io.IOException;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.NetworkInterface;
@@ -33,27 +34,54 @@ class WanTcpPlugin extends TcpPlugin {
 	private static final Logger LOG =
 			Logger.getLogger(WanTcpPlugin.class.getName());
 
+	private final PortMapper portMapper;
+
+	private volatile MappingResult mappingResult;
+
 	WanTcpPlugin(@PluginExecutor Executor pluginExecutor,
-			DuplexPluginCallback callback, long pollingInterval) {
+			DuplexPluginCallback callback, long pollingInterval,
+			PortMapper portMapper) {
 		super(pluginExecutor, callback, pollingInterval);
+		this.portMapper = portMapper;
 	}
 
 	public TransportId getId() {
 		return ID;
 	}
 
+	@Override
+	public void start() throws IOException {
+		super.start();
+		pluginExecutor.execute(new Runnable() {
+			public void run() {
+				portMapper.start();
+			}
+		});
+	}
+
+	@Override
+	public void stop() throws IOException {
+		super.stop();
+		pluginExecutor.execute(new Runnable() {
+			public void run() {
+				portMapper.stop();
+			}
+		});
+	}
+
 	@Override
 	protected List<SocketAddress> getLocalSocketAddresses() {
 		List<SocketAddress> addrs = new ArrayList<SocketAddress>();
-		// Prefer a previously used address and port if available
+		// Prefer a previously used external address and port if available
 		TransportProperties p = callback.getLocalProperties();
 		String addrString = p.get("address");
 		String portString = p.get("port");
 		InetAddress addr = null;
+		int port = 0;
 		if(addrString != null && portString != null) {
 			try {
 				addr = InetAddress.getByName(addrString);
-				int port = Integer.valueOf(portString);
+				port = Integer.valueOf(portString);
 				addrs.add(new InetSocketAddress(addr, port));
 				addrs.add(new InetSocketAddress(addr, 0));
 			} catch(NumberFormatException e) {
@@ -79,9 +107,31 @@ class WanTcpPlugin extends TcpPlugin {
 				if(!link && !site) addrs.add(new InetSocketAddress(a, 0));
 			}
 		}
+		// Accept interfaces that can be port-mapped
+		if(port == 0) port = chooseEphemeralPort();
+		mappingResult = portMapper.map(port);
+		if(mappingResult != null && mappingResult.isUsable())
+			addrs.add(new InetSocketAddress(mappingResult.getInternal(), port));
 		return addrs;
 	}
 
+	private int chooseEphemeralPort() {
+		return 32768 + (int) (Math.random() * 32768);
+	}
+
+	@Override
+	protected void setLocalSocketAddress(InetSocketAddress a) {
+		InetAddress addr = a.getAddress();
+		if(mappingResult != null && mappingResult.isUsable()) {
+			if(addr.equals(mappingResult.getInternal()))
+				addr = mappingResult.getExternal();
+		}
+		TransportProperties p = new TransportProperties();
+		p.put("address", addr.getHostAddress());
+		p.put("port", String.valueOf(a.getPort()));
+		callback.mergeLocalProperties(p);
+	}
+
 	public boolean supportsInvitations() {
 		return false;
 	}
diff --git a/src/net/sf/briar/plugins/tcp/WanTcpPluginFactory.java b/src/net/sf/briar/plugins/tcp/WanTcpPluginFactory.java
index e4ed87284fb30e8bed42e7305e44852fae56ff68..f976e22cf531e5a00d370d3227f47d67a0b08857 100644
--- a/src/net/sf/briar/plugins/tcp/WanTcpPluginFactory.java
+++ b/src/net/sf/briar/plugins/tcp/WanTcpPluginFactory.java
@@ -16,6 +16,7 @@ public class WanTcpPluginFactory implements DuplexPluginFactory {
 	public DuplexPlugin createPlugin(@PluginExecutor Executor pluginExecutor,
 			AndroidExecutor androidExecutor, Context appContext,
 			DuplexPluginCallback callback) {
-		return new WanTcpPlugin(pluginExecutor, callback, POLLING_INTERVAL);
+		return new WanTcpPlugin(pluginExecutor, callback, POLLING_INTERVAL,
+				new PortMapperImpl());
 	}
 }