diff --git a/MobileLibrary/Android/PsiphonTunnel/PsiphonTunnel.java b/MobileLibrary/Android/PsiphonTunnel/PsiphonTunnel.java
index 0ab7766a8..9c4115d54 100644
--- a/MobileLibrary/Android/PsiphonTunnel/PsiphonTunnel.java
+++ b/MobileLibrary/Android/PsiphonTunnel/PsiphonTunnel.java
@@ -131,6 +131,11 @@ default public void onActiveAuthorizationIDs(List<String> authorizations) {}
         default public void onTrafficRateLimits(long upstreamBytesPerSecond, long downstreamBytesPerSecond) {}
         default public void onApplicationParameters(Object parameters) {}
         default public void onServerAlert(String reason, String subject, List<String> actionURLs) {}
+        /**
+         * Called when tunnel-core reports connected server region information.
+         * @param region The server region received.
+         */
+        default public void onConnectedServerRegion(String region) {}
         default public void onExiting() {}
     }
 
@@ -1079,6 +1084,9 @@ private void handlePsiphonNotice(String noticeJSON) {
                       enableUdpGwKeepalive();
                     }
                 }
+                // Also report the tunnel's egress region to the host service
+                mHostService.onConnectedServerRegion(
+                        notice.getJSONObject("data").getString("serverRegion"));
             } else if (noticeType.equals("ApplicationParameters")) {
                 mHostService.onApplicationParameters(
                     notice.getJSONObject("data").get("parameters"));
diff --git a/MobileLibrary/Android/SampleApps/TunneledWebView/app/build.gradle b/MobileLibrary/Android/SampleApps/TunneledWebView/app/build.gradle
index 62d433924..4cc11be52 100644
--- a/MobileLibrary/Android/SampleApps/TunneledWebView/app/build.gradle
+++ b/MobileLibrary/Android/SampleApps/TunneledWebView/app/build.gradle
@@ -35,4 +35,11 @@ dependencies {
     implementation 'androidx.appcompat:appcompat:1.0.0'
     // always specify exact library version in your real project to avoid non-deterministic builds
     implementation 'ca.psiphon:psiphontunnel:2.+'
+
+    // For the latest version compile the library from source, see MobileLibrary/Android/README.md
+    // in the Psiphon-Labs/psiphon-tunnel-core repository, copy the ca.psiphon.aar artifact to
+    // the libs folder under the app module and replace the above line
+    // (e.g. replace implementation 'ca.psiphon:psiphontunnel:2.+')
+    // with the following line:
+    // implementation files('libs/ca.psiphon.aar')
 }
diff --git a/MobileLibrary/Android/SampleApps/TunneledWebView/app/src/main/java/ca/psiphon/tunneledwebview/MainActivity.java b/MobileLibrary/Android/SampleApps/TunneledWebView/app/src/main/java/ca/psiphon/tunneledwebview/MainActivity.java
index 8acdfcee0..c65d7a8b7 100644
--- a/MobileLibrary/Android/SampleApps/TunneledWebView/app/src/main/java/ca/psiphon/tunneledwebview/MainActivity.java
+++ b/MobileLibrary/Android/SampleApps/TunneledWebView/app/src/main/java/ca/psiphon/tunneledwebview/MainActivity.java
@@ -8,6 +8,8 @@ Licensed under Creative Commons Zero (CC0).
 import android.content.Context;
 import android.os.Bundle;
 import androidx.appcompat.app.AppCompatActivity;
+
+import android.util.Log;
 import android.webkit.WebSettings;
 import android.webkit.WebView;
 import android.widget.ArrayAdapter;
@@ -61,6 +63,7 @@ Licensed under Creative Commons Zero (CC0).
 public class MainActivity extends AppCompatActivity
         implements PsiphonTunnel.HostService {
 
+    private static final String TAG = "TunneledWebView";
     private ListView mListView;
     private WebView mWebView;
 
@@ -152,6 +155,7 @@ private void logMessage(final String message) {
             public void run() {
                 mLogMessages.add(message);
                 mListView.setSelection(mLogMessages.getCount() - 1);
+                Log.d(TAG, "logMessage: " + message);
             }
         });
     }
@@ -249,6 +253,11 @@ public void onConnected() {
         loadWebView();
     }
 
+    @Override
+    public void onConnectedServerRegion(String region) {
+        logMessage("connected server region: " + region);
+    }
+
     @Override
     public void onHomepage(String url) {
         logMessage("home page: " + url);
diff --git a/MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.h b/MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.h
index 3ff66d45a..7aa5b1d4e 100644
--- a/MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.h
+++ b/MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.h
@@ -299,6 +299,12 @@ WWAN or vice versa or VPN state changed
  */
 - (void)onApplicationParameters:(NSDictionary * _Nonnull)parameters;
 
+
+/*!
+ Called when tunnel-core reports connected server region information
+ @param region The server region received.
+ */
+- (void)onConnectedServerRegion:(NSString * _Nonnull)region;
 @end
 
 /*!
diff --git a/MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.m b/MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.m
index 1b50a806c..bb756fb5b 100644
--- a/MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.m
+++ b/MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.m
@@ -1174,6 +1174,18 @@ - (void)handlePsiphonNotice:(NSString * _Nonnull)noticeJSON {
             });
         }
     }
+    else if ([noticeType isEqualToString:@"ActiveTunnel"]) {
+        id region = [notice valueForKeyPath:@"data.serverRegion"];
+        if (![region isKindOfClass:[NSString class]]) {
+            [self logMessage:[NSString stringWithFormat: @"ActiveTunnel notice missing data.serverRegion: %@", noticeJSON]];
+            return;
+        }
+        if ([self.tunneledAppDelegate respondsToSelector:@selector(onConnectedServerRegion:)]) {
+            dispatch_sync(self->callbackQueue, ^{
+                [self.tunneledAppDelegate onConnectedServerRegion:region];
+            });
+        }
+    }
     else if ([noticeType isEqualToString:@"InternalError"]) {
         internalError = TRUE;
     }
diff --git a/MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/AppDelegate.swift b/MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/AppDelegate.swift
index ebc857d4f..aa907f7bd 100644
--- a/MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/AppDelegate.swift
+++ b/MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/AppDelegate.swift
@@ -365,4 +365,8 @@ extension AppDelegate: TunneledAppDelegate {
             self.httpProxyPort = port
         }
     }
+
+    func onConnectedServerRegion(_ region: String) {
+        NSLog("onConnectedServerRegion(%@)", region)
+    }
 }
diff --git a/psiphon/controller.go b/psiphon/controller.go
index aa6ee302b..f2217cc3d 100755
--- a/psiphon/controller.go
+++ b/psiphon/controller.go
@@ -1001,7 +1001,8 @@ loop:
 			NoticeActiveTunnel(
 				connectedTunnel.dialParams.ServerEntry.GetDiagnosticID(),
 				connectedTunnel.dialParams.TunnelProtocol,
-				connectedTunnel.dialParams.ServerEntry.SupportsSSHAPIRequests())
+				connectedTunnel.dialParams.ServerEntry.SupportsSSHAPIRequests(),
+				connectedTunnel.dialParams.ServerEntry.Region)
 
 			if isFirstTunnel {
 
diff --git a/psiphon/notice.go b/psiphon/notice.go
index 910fb5feb..b41a3edef 100644
--- a/psiphon/notice.go
+++ b/psiphon/notice.go
@@ -678,12 +678,13 @@ func NoticeRequestedTactics(dialParams *DialParameters) {
 }
 
 // NoticeActiveTunnel is a successful connection that is used as an active tunnel for port forwarding
-func NoticeActiveTunnel(diagnosticID, protocol string, isTCS bool) {
+func NoticeActiveTunnel(diagnosticID, protocol string, isTCS bool, serverRegion string) {
 	singletonNoticeLogger.outputNotice(
 		"ActiveTunnel", noticeIsDiagnostic,
 		"diagnosticID", diagnosticID,
 		"protocol", protocol,
-		"isTCS", isTCS)
+		"isTCS", isTCS,
+		"serverRegion", serverRegion)
 }
 
 // NoticeSocksProxyPortInUse is a failure to use the configured LocalSocksProxyPort
diff --git a/psiphon/server/discovery.go b/psiphon/server/discovery.go
index 96cdeb92e..b8e803f54 100644
--- a/psiphon/server/discovery.go
+++ b/psiphon/server/discovery.go
@@ -110,8 +110,6 @@ func (d *Discovery) reload(reloadedTactics bool) error {
 	// Initialize and set underlying discovery component. Replaces old
 	// component if discovery is already initialized.
 
-	oldDiscovery := d.discovery
-
 	discovery := discovery.MakeDiscovery(
 		d.support.PsinetDatabase.GetDiscoveryServers(),
 		discoveryStrategy)
@@ -120,6 +118,7 @@ func (d *Discovery) reload(reloadedTactics bool) error {
 
 	d.Lock()
 
+	oldDiscovery := d.discovery
 	d.discovery = discovery
 	d.currentStrategy = strategy
 
@@ -143,6 +142,8 @@ func (d *Discovery) reload(reloadedTactics bool) error {
 
 // Stop stops discovery and cleans up underlying resources.
 func (d *Discovery) Stop() {
+	d.Lock()
+	defer d.Unlock()
 	d.discovery.Stop()
 }
 
diff --git a/psiphon/server/discovery/discovery.go b/psiphon/server/discovery/discovery.go
index 608dc356b..28b705893 100644
--- a/psiphon/server/discovery/discovery.go
+++ b/psiphon/server/discovery/discovery.go
@@ -167,7 +167,8 @@ func (d *Discovery) Start() {
 			// Note: servers with a discovery date range in the past are not
 			// removed from d.all in case the wall clock has drifted;
 			// otherwise, we risk removing them prematurely.
-			servers, nextUpdate := discoverableServers(d.all, d.clk)
+			var servers []*psinet.DiscoveryServer
+			servers, nextUpdate = discoverableServers(d.all, d.clk)
 
 			// Update the set of discoverable servers.
 			d.strategy.serversChanged(servers)
diff --git a/psiphon/server/discovery/discovery_test.go b/psiphon/server/discovery/discovery_test.go
index 3cb3bdb1a..2f293e7ef 100644
--- a/psiphon/server/discovery/discovery_test.go
+++ b/psiphon/server/discovery/discovery_test.go
@@ -149,9 +149,9 @@ func runDiscoveryTest(tt *discoveryTest, now time.Time) error {
 	discovery.Start()
 
 	for _, check := range tt.checks {
-		time.Sleep(1 * time.Second) // let async code complete
+		time.Sleep(10 * time.Millisecond) // let async code complete
 		clk.SetNow(check.t)
-		time.Sleep(1 * time.Second) // let async code complete
+		time.Sleep(10 * time.Millisecond) // let async code complete
 		discovered := discovery.SelectServers(net.IP{})
 		discoveredIPs := make([]string, len(discovered))
 		for i := range discovered {