diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/AndroidPluginModule.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/AndroidPluginModule.java
index c62aac754f3102e7652bcd6414bf6ed7fc0d2d81..2d22e8f4e9706b43b0129aafd374c6cd44a68404 100644
--- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/AndroidPluginModule.java
+++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/AndroidPluginModule.java
@@ -48,7 +48,7 @@ public class AndroidPluginModule {
 				appContext, locationUtils, reporter, eventBus,
 				torSocketFactory, backoffFactory);
 		DuplexPluginFactory lan = new AndroidLanTcpPluginFactory(ioExecutor,
-				backoffFactory, appContext);
+				scheduler, backoffFactory, appContext);
 		Collection<DuplexPluginFactory> duplex =
 				Arrays.asList(bluetooth, tor, lan);
 		@NotNullByDefault
diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPlugin.java
index c6eba28c7d8f9508e799fe1861f047b1efa4e09f..506bc997f0dbc8b251cb12ecb038a14b51b39509 100644
--- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPlugin.java
+++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPlugin.java
@@ -20,6 +20,7 @@ import java.net.Socket;
 import java.net.UnknownHostException;
 import java.util.Collection;
 import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.logging.Logger;
 
 import javax.annotation.Nullable;
@@ -29,16 +30,37 @@ import static android.content.Context.CONNECTIVITY_SERVICE;
 import static android.content.Context.WIFI_SERVICE;
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.wifi.WifiManager.EXTRA_WIFI_STATE;
 import static android.os.Build.VERSION.SDK_INT;
 import static java.util.Collections.emptyList;
 import static java.util.Collections.singletonList;
+import static java.util.concurrent.TimeUnit.SECONDS;
 
 @NotNullByDefault
 class AndroidLanTcpPlugin extends LanTcpPlugin {
 
+	// See android.net.wifi.WifiManager
+	private static final String WIFI_AP_STATE_CHANGED_ACTION =
+			"android.net.wifi.WIFI_AP_STATE_CHANGED";
+	private static final int WIFI_AP_STATE_ENABLED = 13;
+
+	private static final byte[] WIFI_AP_ADDRESS_BYTES =
+			{(byte) 192, (byte) 168, 43, 1};
+	private static final InetAddress WIFI_AP_ADDRESS;
+
 	private static final Logger LOG =
 			Logger.getLogger(AndroidLanTcpPlugin.class.getName());
 
+	static {
+		try {
+			WIFI_AP_ADDRESS = InetAddress.getByAddress(WIFI_AP_ADDRESS_BYTES);
+		} catch (UnknownHostException e) {
+			// Should only be thrown if the address has an illegal length
+			throw new AssertionError(e);
+		}
+	}
+
+	private final ScheduledExecutorService scheduler;
 	private final Context appContext;
 	private final ConnectivityManager connectivityManager;
 	@Nullable
@@ -48,10 +70,11 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
 	private volatile BroadcastReceiver networkStateReceiver = null;
 	private volatile SocketFactory socketFactory;
 
-	AndroidLanTcpPlugin(Executor ioExecutor, Backoff backoff,
-			Context appContext, DuplexPluginCallback callback, int maxLatency,
-			int maxIdleTime) {
+	AndroidLanTcpPlugin(Executor ioExecutor, ScheduledExecutorService scheduler,
+			Backoff backoff, Context appContext, DuplexPluginCallback callback,
+			int maxLatency, int maxIdleTime) {
 		super(ioExecutor, backoff, callback, maxLatency, maxIdleTime);
+		this.scheduler = scheduler;
 		this.appContext = appContext;
 		ConnectivityManager connectivityManager = (ConnectivityManager)
 				appContext.getSystemService(CONNECTIVITY_SERVICE);
@@ -59,7 +82,7 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
 		this.connectivityManager = connectivityManager;
 		wifiManager = (WifiManager) appContext.getApplicationContext()
 				.getSystemService(WIFI_SERVICE);
-		socketFactory = getSocketFactory();
+		socketFactory = SocketFactory.getDefault();
 	}
 
 	@Override
@@ -68,7 +91,9 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
 		running = true;
 		// Register to receive network status events
 		networkStateReceiver = new NetworkStateReceiver();
-		IntentFilter filter = new IntentFilter(CONNECTIVITY_ACTION);
+		IntentFilter filter = new IntentFilter();
+		filter.addAction(CONNECTIVITY_ACTION);
+		filter.addAction(WIFI_AP_STATE_CHANGED_ACTION);
 		appContext.registerReceiver(networkStateReceiver, filter);
 	}
 
@@ -87,10 +112,17 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
 
 	@Override
 	protected Collection<InetAddress> getLocalIpAddresses() {
+		// If the device doesn't have wifi, don't open any sockets
 		if (wifiManager == null) return emptyList();
+		// If we're connected to a wifi network, use that network
 		WifiInfo info = wifiManager.getConnectionInfo();
-		if (info == null || info.getIpAddress() == 0) return emptyList();
-		return singletonList(intToInetAddress(info.getIpAddress()));
+		if (info != null && info.getIpAddress() != 0)
+			return singletonList(intToInetAddress(info.getIpAddress()));
+		// If we're running an access point, return its address
+		if (super.getLocalIpAddresses().contains(WIFI_AP_ADDRESS))
+				return singletonList(WIFI_AP_ADDRESS);
+		// No suitable addresses
+		return emptyList();
 	}
 
 	private InetAddress intToInetAddress(int ip) {
@@ -124,9 +156,28 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
 
 		@Override
 		public void onReceive(Context ctx, Intent i) {
-			if (!running || wifiManager == null) return;
-			WifiInfo info = wifiManager.getConnectionInfo();
-			if (info == null || info.getIpAddress() == 0) {
+			if (!running) return;
+			if (isApEnabledEvent(i)) {
+				// The state change may be broadcast before the AP address is
+				// visible, so delay handling the event
+				scheduler.schedule(this::handleConnectivityChange, 1, SECONDS);
+			} else {
+				handleConnectivityChange();
+			}
+		}
+
+		private void handleConnectivityChange() {
+			if (!running) return;
+			Collection<InetAddress> addrs = getLocalIpAddresses();
+			if (addrs.contains(WIFI_AP_ADDRESS)) {
+				LOG.info("Providing wifi hotspot");
+				// There's no corresponding Network object and thus no way
+				// to get a suitable socket factory, so we won't be able to
+				// make outgoing connections on API 21+ if another network
+				// has internet access
+				socketFactory = SocketFactory.getDefault();
+				if (socket == null || socket.isClosed()) bind();
+			} else if (addrs.isEmpty()) {
 				LOG.info("Not connected to wifi");
 				socketFactory = SocketFactory.getDefault();
 				tryToClose(socket);
@@ -136,5 +187,10 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
 				if (socket == null || socket.isClosed()) bind();
 			}
 		}
+
+		private boolean isApEnabledEvent(Intent i) {
+			return WIFI_AP_STATE_CHANGED_ACTION.equals(i.getAction()) &&
+					i.getIntExtra(EXTRA_WIFI_STATE, 0) == WIFI_AP_STATE_ENABLED;
+		}
 	}
 }
diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPluginFactory.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPluginFactory.java
index 311219a66562ef91edad3a81ee7207f828f42e2f..da8149c9a00ed1d29083734514040f9a43aa41e6 100644
--- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPluginFactory.java
+++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPluginFactory.java
@@ -11,6 +11,7 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
 import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
 
 import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
 
 import javax.annotation.concurrent.Immutable;
 
@@ -27,12 +28,15 @@ public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
 	private static final double BACKOFF_BASE = 1.2;
 
 	private final Executor ioExecutor;
+	private final ScheduledExecutorService scheduler;
 	private final BackoffFactory backoffFactory;
 	private final Context appContext;
 
 	public AndroidLanTcpPluginFactory(Executor ioExecutor,
-			BackoffFactory backoffFactory, Context appContext) {
+			ScheduledExecutorService scheduler, BackoffFactory backoffFactory,
+			Context appContext) {
 		this.ioExecutor = ioExecutor;
+		this.scheduler = scheduler;
 		this.backoffFactory = backoffFactory;
 		this.appContext = appContext;
 	}
@@ -51,7 +55,7 @@ public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
 	public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
 		Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
 				MAX_POLLING_INTERVAL, BACKOFF_BASE);
-		return new AndroidLanTcpPlugin(ioExecutor, backoff, appContext,
-				callback, MAX_LATENCY, MAX_IDLE_TIME);
+		return new AndroidLanTcpPlugin(ioExecutor, scheduler, backoff,
+				appContext, callback, MAX_LATENCY, MAX_IDLE_TIME);
 	}
 }
diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java
index 7ccaa9a2797a4a96b92429996553be31f5f3e252..73b2a39b39657d4ddc4b336c9ca87d1cd69901be 100644
--- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java
+++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java
@@ -16,6 +16,7 @@ import android.os.PowerManager;
 import net.freehaven.tor.control.EventHandler;
 import net.freehaven.tor.control.TorControlConnection;
 
+import org.briarproject.bramble.PoliteExecutor;
 import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.data.BdfList;
 import org.briarproject.bramble.api.event.Event;
@@ -63,8 +64,6 @@ import java.util.concurrent.Future;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
 import java.util.logging.Logger;
 import java.util.regex.Pattern;
 import java.util.zip.ZipInputStream;
@@ -111,7 +110,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 	private static final Logger LOG =
 			Logger.getLogger(TorPlugin.class.getName());
 
-	private final Executor ioExecutor;
+	private final Executor ioExecutor, connectionStatusExecutor;
 	private final ScheduledExecutorService scheduler;
 	private final Context appContext;
 	private final LocationUtils locationUtils;
@@ -125,7 +124,6 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 	private final File torDirectory, torFile, geoIpFile, configFile;
 	private final File doneFile, cookieFile;
 	private final PowerManager.WakeLock wakeLock;
-	private final Lock connectionStatusLock;
 	private final AtomicReference<Future<?>> connectivityCheck =
 			new AtomicReference<>();
 	private final AtomicBoolean used = new AtomicBoolean(false);
@@ -167,7 +165,9 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 		// This tag will prevent Huawei's powermanager from killing us.
 		wakeLock = pm.newWakeLock(PARTIAL_WAKE_LOCK, "LocationManagerService");
 		wakeLock.setReferenceCounted(false);
-		connectionStatusLock = new ReentrantLock();
+		// Don't execute more than one connection status check at a time
+		connectionStatusExecutor = new PoliteExecutor("TorPlugin",
+				ioExecutor, 1);
 	}
 
 	@Override
@@ -697,54 +697,44 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 	}
 
 	private void updateConnectionStatus() {
-		ioExecutor.execute(() -> {
+		connectionStatusExecutor.execute(() -> {
 			if (!running) return;
-			try {
-				connectionStatusLock.lock();
-				updateConnectionStatusLocked();
-			} finally {
-				connectionStatusLock.unlock();
-			}
-		});
-	}
+			Object o = appContext.getSystemService(CONNECTIVITY_SERVICE);
+			ConnectivityManager cm = (ConnectivityManager) o;
+			NetworkInfo net = cm.getActiveNetworkInfo();
+			boolean online = net != null && net.isConnected();
+			boolean wifi = online && net.getType() == TYPE_WIFI;
+			String country = locationUtils.getCurrentCountry();
+			boolean blocked = TorNetworkMetadata.isTorProbablyBlocked(
+					country);
+			Settings s = callback.getSettings();
+			int network = s.getInt(PREF_TOR_NETWORK, PREF_TOR_NETWORK_ALWAYS);
 
-	// Locking: connectionStatusLock
-	private void updateConnectionStatusLocked() {
-		Object o = appContext.getSystemService(CONNECTIVITY_SERVICE);
-		ConnectivityManager cm = (ConnectivityManager) o;
-		NetworkInfo net = cm.getActiveNetworkInfo();
-		boolean online = net != null && net.isConnected();
-		boolean wifi = online && net.getType() == TYPE_WIFI;
-		String country = locationUtils.getCurrentCountry();
-		boolean blocked = TorNetworkMetadata.isTorProbablyBlocked(
-				country);
-		Settings s = callback.getSettings();
-		int network = s.getInt(PREF_TOR_NETWORK, PREF_TOR_NETWORK_ALWAYS);
-
-		if (LOG.isLoggable(INFO)) {
-			LOG.info("Online: " + online + ", wifi: " + wifi);
-			if ("".equals(country)) LOG.info("Country code unknown");
-			else LOG.info("Country code: " + country);
-		}
+			if (LOG.isLoggable(INFO)) {
+				LOG.info("Online: " + online + ", wifi: " + wifi);
+				if ("".equals(country)) LOG.info("Country code unknown");
+				else LOG.info("Country code: " + country);
+			}
 
-		try {
-			if (!online) {
-				LOG.info("Disabling network, device is offline");
-				enableNetwork(false);
-			} else if (blocked) {
-				LOG.info("Disabling network, country is blocked");
-				enableNetwork(false);
-			} else if (network == PREF_TOR_NETWORK_NEVER
-					|| (network == PREF_TOR_NETWORK_WIFI && !wifi)) {
-				LOG.info("Disabling network due to data setting");
-				enableNetwork(false);
-			} else {
-				LOG.info("Enabling network");
-				enableNetwork(true);
+			try {
+				if (!online) {
+					LOG.info("Disabling network, device is offline");
+					enableNetwork(false);
+				} else if (blocked) {
+					LOG.info("Disabling network, country is blocked");
+					enableNetwork(false);
+				} else if (network == PREF_TOR_NETWORK_NEVER
+						|| (network == PREF_TOR_NETWORK_WIFI && !wifi)) {
+					LOG.info("Disabling network due to data setting");
+					enableNetwork(false);
+				} else {
+					LOG.info("Enabling network");
+					enableNetwork(true);
+				}
+			} catch (IOException e) {
+				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 			}
-		} catch (IOException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-		}
+		});
 	}
 
 	private void scheduleConnectionStatusUpdate() {
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java
index 8b861164df2dd4144bccbea99d0953793e085d8e..78b3cab7c52886d5fef16274b46c3a3d1c1047fc 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java
@@ -1,5 +1,6 @@
 package org.briarproject.bramble.plugin.tcp;
 
+import org.briarproject.bramble.PoliteExecutor;
 import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.data.BdfList;
 import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
@@ -47,7 +48,7 @@ abstract class TcpPlugin implements DuplexPlugin {
 	private static final Logger LOG =
 			Logger.getLogger(TcpPlugin.class.getName());
 
-	protected final Executor ioExecutor;
+	protected final Executor ioExecutor, bindExecutor;
 	protected final Backoff backoff;
 	protected final DuplexPluginCallback callback;
 	protected final int maxLatency, maxIdleTime, socketTimeout;
@@ -90,6 +91,8 @@ abstract class TcpPlugin implements DuplexPlugin {
 		if (maxIdleTime > Integer.MAX_VALUE / 2)
 			socketTimeout = Integer.MAX_VALUE;
 		else socketTimeout = maxIdleTime * 2;
+		// Don't execute more than one bind operation at a time
+		bindExecutor = new PoliteExecutor("TcpPlugin", ioExecutor, 1);
 	}
 
 	@Override
@@ -110,8 +113,9 @@ abstract class TcpPlugin implements DuplexPlugin {
 	}
 
 	protected void bind() {
-		ioExecutor.execute(() -> {
+		bindExecutor.execute(() -> {
 			if (!running) return;
+			if (socket != null && !socket.isClosed()) return;
 			ServerSocket ss = null;
 			for (InetSocketAddress addr : getLocalSocketAddresses()) {
 				try {