Bug 1502025 - Use captive portal endpoints for connectivity checks r=dragana
authorValentin Gosu <valentin.gosu@gmail.com>
Sun, 02 Dec 2018 23:29:27 +0000
changeset 505621 389ecc0ddf76e316c9ec17e8847829f19cb49af6
parent 505620 552ef6767e3b9244a8f31382306f3ffa42bdb2de
child 505622 29385deef9ea018dd480de4f4c9ceaa4b1308c5d
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdragana
bugs1502025
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1502025 - Use captive portal endpoints for connectivity checks r=dragana Differential Revision: https://phabricator.services.mozilla.com/D13334
modules/libpref/init/all.js
netwerk/base/NetworkConnectivityService.cpp
netwerk/base/NetworkConnectivityService.h
netwerk/base/nsINetworkConnectivityService.idl
netwerk/base/nsIOService.cpp
netwerk/test/unit/test_network_connectivity_service.js
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -5460,16 +5460,18 @@ pref("network.captive-portal-service.min
 pref("network.captive-portal-service.maxInterval", 1500000); // 25 minutes
 // Every 10 checks, the delay is increased by a factor of 5
 pref("network.captive-portal-service.backoffFactor", "5.0");
 pref("network.captive-portal-service.enabled", false);
 
 pref("network.connectivity-service.enabled", true);
 pref("network.connectivity-service.DNSv4.domain", "mozilla.org");
 pref("network.connectivity-service.DNSv6.domain", "mozilla.org");
+pref("network.connectivity-service.IPv4.url", "http://detectportal.firefox.com/success.txt?ipv4");
+pref("network.connectivity-service.IPv6.url", "http://detectportal.firefox.com/success.txt?ipv6");
 
 // DNS Trusted Recursive Resolver
 // 0 - default off, 1 - race, 2 TRR first, 3 TRR only, 4 shadow, 5 off by choice
 pref("network.trr.mode", 0);
 // DNS-over-HTTP service to use, must be HTTPS://
 pref("network.trr.uri", "https://mozilla.cloudflare-dns.com/dns-query");
 // credentials to pass to DOH end-point
 pref("network.trr.credentials", "");
--- a/netwerk/base/NetworkConnectivityService.cpp
+++ b/netwerk/base/NetworkConnectivityService.cpp
@@ -2,22 +2,24 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "NetworkConnectivityService.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/Services.h"
 #include "xpcpublic.h"
 #include "nsSocketTransport2.h"
+#include "nsIURIMutator.h"
+#include "nsINetworkLinkService.h"
 
 namespace mozilla {
 namespace net {
 
-NS_IMPL_ISUPPORTS(NetworkConnectivityService, nsINetworkConnectivityService,
-                  nsIObserver, nsIDNSListener)
+NS_IMPL_ISUPPORTS(NetworkConnectivityService, nsIDNSListener, nsIObserver,
+                  nsINetworkConnectivityService, nsIStreamListener)
 
 static StaticRefPtr<NetworkConnectivityService> gConnService;
 
 // static
 already_AddRefed<NetworkConnectivityService>
 NetworkConnectivityService::GetSingleton() {
   if (gConnService) {
     return do_AddRef(gConnService);
@@ -30,75 +32,85 @@ NetworkConnectivityService::GetSingleton
   ClearOnShutdown(&gConnService);
   return do_AddRef(gConnService);
 }
 
 nsresult NetworkConnectivityService::Init() {
   nsCOMPtr<nsIObserverService> observerService =
       mozilla::services::GetObserverService();
   observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+  observerService->AddObserver(this, NS_NETWORK_LINK_TOPIC, false);
   observerService->AddObserver(this, "network:captive-portal-connectivity",
                                false);
 
-  // We need to schedule this for a bit later, to avoid a recursive service
-  // initialization.
-  MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(
-      NewRunnableMethod("NetworkConnectivityService::PerformChecks", this,
-                        &NetworkConnectivityService::PerformChecks)));
   return NS_OK;
 }
 
 NS_IMETHODIMP
-NetworkConnectivityService::GetDNSv4(int32_t *aState) {
+NetworkConnectivityService::GetDNSv4(ConnectivityState *aState) {
   NS_ENSURE_ARG(aState);
   *aState = mDNSv4;
   return NS_OK;
 }
 
 NS_IMETHODIMP
-NetworkConnectivityService::GetDNSv6(int32_t *aState) {
+NetworkConnectivityService::GetDNSv6(ConnectivityState *aState) {
   NS_ENSURE_ARG(aState);
   *aState = mDNSv6;
   return NS_OK;
 }
 
 NS_IMETHODIMP
-NetworkConnectivityService::GetIPv4(int32_t *aState) {
+NetworkConnectivityService::GetIPv4(ConnectivityState *aState) {
   NS_ENSURE_ARG(aState);
-  *aState = nsSocketTransport::HasIPv4Connectivity()
-                ? nsINetworkConnectivityService::OK
-                : nsINetworkConnectivityService::NOT_AVAILABLE;
+  *aState = mIPv4;
   return NS_OK;
 }
 
 NS_IMETHODIMP
-NetworkConnectivityService::GetIPv6(int32_t *aState) {
+NetworkConnectivityService::GetIPv6(ConnectivityState *aState) {
   NS_ENSURE_ARG(aState);
-  *aState = nsSocketTransport::HasIPv6Connectivity()
-                ? nsINetworkConnectivityService::OK
-                : nsINetworkConnectivityService::NOT_AVAILABLE;
+  *aState = mIPv6;
   return NS_OK;
 }
 
-void NetworkConnectivityService::PerformChecks() { RecheckDNS(); }
+void NetworkConnectivityService::PerformChecks() {
+  mDNSv4 = UNKNOWN;
+  mDNSv6 = UNKNOWN;
+
+  mIPv4 = UNKNOWN;
+  mIPv6 = UNKNOWN;
+
+  RecheckDNS();
+  RecheckIPConnectivity();
+}
+
+static inline void NotifyObservers(const char *aTopic)
+{
+  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+  obs->NotifyObservers(nullptr, aTopic, nullptr);
+}
 
 NS_IMETHODIMP
 NetworkConnectivityService::OnLookupComplete(nsICancelable *aRequest,
                                              nsIDNSRecord *aRecord,
                                              nsresult aStatus) {
-  int32_t state = aRecord ? nsINetworkConnectivityService::OK
-                          : nsINetworkConnectivityService::NOT_AVAILABLE;
+  ConnectivityState state = aRecord ? OK : NOT_AVAILABLE;
 
   if (aRequest == mDNSv4Request) {
     mDNSv4 = state;
     mDNSv4Request = nullptr;
   } else if (aRequest == mDNSv6Request) {
     mDNSv6 = state;
     mDNSv6Request = nullptr;
   }
+
+  if (!mDNSv4Request && !mDNSv6Request) {
+    NotifyObservers("network:connectivity-service:dns-checks-complete");
+  }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 NetworkConnectivityService::OnLookupByTypeComplete(nsICancelable *aRequest,
                                                    nsIDNSByTypeRecord *aRes,
                                                    nsresult aStatus) {
   return NS_OK;
@@ -130,34 +142,166 @@ NetworkConnectivityService::RecheckDNS()
   return rv;
 }
 
 NS_IMETHODIMP
 NetworkConnectivityService::Observe(nsISupports *aSubject, const char *aTopic,
                                     const char16_t *aData) {
   if (!strcmp(aTopic, "network:captive-portal-connectivity")) {
     // Captive portal is cleared, so we redo the checks.
-    mDNSv4 = nsINetworkConnectivityService::UNKNOWN;
-    mDNSv6 = nsINetworkConnectivityService::UNKNOWN;
-
     PerformChecks();
   } else if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
     if (mDNSv4Request) {
       mDNSv4Request->Cancel(NS_ERROR_ABORT);
       mDNSv4Request = nullptr;
     }
     if (mDNSv6Request) {
       mDNSv6Request->Cancel(NS_ERROR_ABORT);
       mDNSv6Request = nullptr;
     }
 
     nsCOMPtr<nsIObserverService> observerService =
         mozilla::services::GetObserverService();
     observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
     observerService->RemoveObserver(this,
                                     "network:captive-portal-connectivity");
+    observerService->RemoveObserver(this, NS_NETWORK_LINK_TOPIC);
+  } else if (!strcmp(aTopic, NS_NETWORK_LINK_TOPIC) &&
+             !NS_LITERAL_STRING(NS_NETWORK_LINK_DATA_UNKNOWN).Equals(aData)) {
+    PerformChecks();
   }
 
   return NS_OK;
 }
 
+static inline already_AddRefed<nsIChannel> SetupIPCheckChannel(bool ipv4) {
+  nsresult rv;
+  nsAutoCString url;
+
+  if (ipv4) {
+    rv = Preferences::GetCString("network.connectivity-service.IPv4.url", url);
+  } else {
+    rv = Preferences::GetCString("network.connectivity-service.IPv6.url", url);
+  }
+  NS_ENSURE_SUCCESS(rv, nullptr);
+
+  nsCOMPtr<nsIURI> uri;
+  rv = NS_NewURI(getter_AddRefs(uri), url);
+  NS_ENSURE_SUCCESS(rv, nullptr);
+
+  nsCOMPtr<nsIChannel> channel;
+  rv = NS_NewChannel(
+      getter_AddRefs(channel), uri, nsContentUtils::GetSystemPrincipal(),
+      nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+      nsIContentPolicy::TYPE_OTHER,
+      nullptr,  // aPerformanceStorage
+      nullptr,  // aLoadGroup
+      nullptr,
+      nsIRequest::LOAD_BYPASS_CACHE |    // don't read from the cache
+          nsIRequest::INHIBIT_CACHING |  // don't write the response to cache
+          nsIRequest::LOAD_ANONYMOUS);   // prevent privacy leaks
+
+  NS_ENSURE_SUCCESS(rv, nullptr);
+
+  nsCOMPtr<nsIHttpChannelInternal> internalChan = do_QueryInterface(channel);
+  NS_ENSURE_TRUE(internalChan, nullptr);
+
+  if (ipv4) {
+    internalChan->SetIPv6Disabled();
+  } else {
+    internalChan->SetIPv4Disabled();
+  }
+
+  return channel.forget();
+}
+
+NS_IMETHODIMP
+NetworkConnectivityService::RecheckIPConnectivity() {
+  bool enabled =
+      Preferences::GetBool("network.connectivity-service.enabled", false);
+  if (!enabled) {
+    return NS_OK;
+  }
+
+  if (xpc::AreNonLocalConnectionsDisabled() &&
+      !Preferences::GetBool("network.captive-portal-service.testMode", false)) {
+    return NS_OK;
+  }
+
+  if (mIPv4Channel) {
+    mIPv4Channel->Cancel(NS_ERROR_ABORT);
+    mIPv4Channel = nullptr;
+  }
+  if (mIPv6Channel) {
+    mIPv6Channel->Cancel(NS_ERROR_ABORT);
+    mIPv6Channel = nullptr;
+  }
+
+  nsresult rv;
+  mIPv4Channel = SetupIPCheckChannel(/* ipv4 = */ true);
+  if (mIPv4Channel) {
+    rv = mIPv4Channel->AsyncOpen2(this);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
+  mIPv6Channel = SetupIPCheckChannel(/* ipv4 = */ false);
+  if (mIPv6Channel) {
+    rv = mIPv6Channel->AsyncOpen2(this);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+NetworkConnectivityService::OnStartRequest(nsIRequest *aRequest,
+                                           nsISupports *aContext) {
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+NetworkConnectivityService::OnStopRequest(nsIRequest *aRequest,
+                                          nsISupports *aContext,
+                                          nsresult aStatusCode) {
+  if (aStatusCode == NS_ERROR_ABORT) {
+    return NS_OK;
+  }
+
+  ConnectivityState status = NS_FAILED(aStatusCode) ? NOT_AVAILABLE : OK;
+
+  if (aRequest == mIPv4Channel) {
+    mIPv4 = status;
+    mIPv4Channel = nullptr;
+  } else if (aRequest == mIPv6Channel) {
+#ifdef DEBUG
+    // Verify that the check was performed over IPv6
+    nsCOMPtr<nsIHttpChannelInternal> v6Internal = do_QueryInterface(aRequest);
+    MOZ_ASSERT(v6Internal);
+    nsAutoCString peerAddr;
+    Unused << v6Internal->GetRemoteAddress(peerAddr);
+    MOZ_ASSERT(peerAddr.Contains(':') || NS_FAILED(aStatusCode));
+#endif
+
+    mIPv6 = status;
+    mIPv6Channel = nullptr;
+  } else {
+    MOZ_ASSERT(false, "Unknown request");
+  }
+
+  if (!mIPv6Channel && !mIPv4Channel) {
+    NotifyObservers("network:connectivity-service:ip-checks-complete");
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+NetworkConnectivityService::OnDataAvailable(nsIRequest *aRequest,
+                                            nsISupports *aContext,
+                                            nsIInputStream *aInputStream,
+                                            uint64_t aOffset, uint32_t aCount) {
+  nsAutoCString data;
+  Unused << NS_ReadInputStreamToString(aInputStream, data, aCount);
+  return NS_OK;
+}
+
 }  // namespace net
 }  // namespace mozilla
--- a/netwerk/base/NetworkConnectivityService.h
+++ b/netwerk/base/NetworkConnectivityService.h
@@ -7,39 +7,48 @@
 
 #include "nsINetworkConnectivityService.h"
 
 namespace mozilla {
 namespace net {
 
 class NetworkConnectivityService : public nsINetworkConnectivityService,
                                    public nsIObserver,
-                                   public nsIDNSListener {
+                                   public nsIDNSListener,
+                                   public nsIStreamListener {
  public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSINETWORKCONNECTIVITYSERVICE
   NS_DECL_NSIOBSERVER
   NS_DECL_NSIDNSLISTENER
+  NS_DECL_NSISTREAMLISTENER
+  NS_DECL_NSIREQUESTOBSERVER
 
   nsresult Init();
   static already_AddRefed<NetworkConnectivityService> GetSingleton();
 
  private:
   NetworkConnectivityService() = default;
   virtual ~NetworkConnectivityService() = default;
 
   // Calls all the check methods
   void PerformChecks();
 
   // Will be set to OK if the DNS request returned in IP of this type,
   //                NOT_AVAILABLE if that type of resolution is not available
   //                UNKNOWN if the check wasn't performed
-  int32_t mDNSv4 = nsINetworkConnectivityService::UNKNOWN;
-  int32_t mDNSv6 = nsINetworkConnectivityService::UNKNOWN;
+  ConnectivityState mDNSv4 = nsINetworkConnectivityService::UNKNOWN;
+  ConnectivityState mDNSv6 = nsINetworkConnectivityService::UNKNOWN;
+
+  ConnectivityState mIPv4 = nsINetworkConnectivityService::UNKNOWN;
+  ConnectivityState mIPv6 = nsINetworkConnectivityService::UNKNOWN;
 
   nsCOMPtr<nsICancelable> mDNSv4Request;
   nsCOMPtr<nsICancelable> mDNSv6Request;
+
+  nsCOMPtr<nsIChannel> mIPv4Channel;
+  nsCOMPtr<nsIChannel> mIPv6Channel;
 };
 
 }  // namespace net
 }  // namespace mozilla
 
 #endif  // NetworkConnectivityService_h_
--- a/netwerk/base/nsINetworkConnectivityService.idl
+++ b/netwerk/base/nsINetworkConnectivityService.idl
@@ -9,23 +9,28 @@ interface nsINetworkConnectivityService 
 {
   /**
    * Each tested feature may be in one of 3 states:
    *   UNKNOWN, if a check hasn't been performed.
    *   OK, if the feature was successfully tested
    *   NOT_AVAILABLE, if the feature is blocked by the network.
    *                  Note that the endpoints are guaranteed to support the features.
    */
-  const long UNKNOWN             = 0;
-  const long OK                  = 1;
-  const long NOT_AVAILABLE       = 2;
+  cenum ConnectivityState: 8 {
+    UNKNOWN             = 0,
+    OK                  = 1,
+    NOT_AVAILABLE       = 2
+  };
 
   /* If DNS v4/v6 queries actually work on the current network */
-  readonly attribute long DNSv4;
-  readonly attribute long DNSv6;
+  readonly attribute nsINetworkConnectivityService_ConnectivityState DNSv4;
+  readonly attribute nsINetworkConnectivityService_ConnectivityState DNSv6;
 
   /* If connecting to IPv4/v6 works on the current network */
-  readonly attribute long IPv4;
-  readonly attribute long IPv6;
+  readonly attribute nsINetworkConnectivityService_ConnectivityState IPv4;
+  readonly attribute nsINetworkConnectivityService_ConnectivityState IPv6;
 
   /* Starts the DNS request to check for DNS v4/v6 availability */
   void recheckDNS();
+
+  /* Starts HTTP requests over IPv4 and IPv6, and checks if they work */
+  void recheckIPConnectivity();
 };
--- a/netwerk/base/nsIOService.cpp
+++ b/netwerk/base/nsIOService.cpp
@@ -254,20 +254,16 @@ nsresult nsIOService::Init() {
 
   gIOService = this;
 
   InitializeNetworkLinkService();
   InitializeProtocolProxyService();
 
   SetOffline(false);
 
-  RefPtr<NetworkConnectivityService> ncs =
-      NetworkConnectivityService::GetSingleton();
-  ncs->Init();
-
   return NS_OK;
 }
 
 nsIOService::~nsIOService() {
   if (gIOService) {
     MOZ_ASSERT(gIOService == this);
     gIOService = nullptr;
   }
@@ -280,16 +276,20 @@ nsresult nsIOService::InitializeCaptiveP
   }
 
   mCaptivePortalService = do_GetService(NS_CAPTIVEPORTAL_CID);
   if (mCaptivePortalService) {
     return static_cast<CaptivePortalService *>(mCaptivePortalService.get())
         ->Initialize();
   }
 
+  RefPtr<NetworkConnectivityService> ncs =
+      NetworkConnectivityService::GetSingleton();
+  ncs->Init();
+
   return NS_OK;
 }
 
 nsresult nsIOService::InitializeSocketTransportService() {
   nsresult rv = NS_OK;
 
   if (!mSocketTransportService) {
     mSocketTransportService =
--- a/netwerk/test/unit/test_network_connectivity_service.js
+++ b/netwerk/test/unit/test_network_connectivity_service.js
@@ -3,59 +3,117 @@
  */
 
 "use strict";
 
 ChromeUtils.import("resource://testing-common/httpd.js");
 ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
+/**
+ * Waits for an observer notification to fire.
+ *
+ * @param {String} topic The notification topic.
+ * @returns {Promise} A promise that fulfills when the notification is fired.
+ */
+function promiseObserverNotification(topic, matchFunc) {
+  return new Promise((resolve, reject) => {
+    Services.obs.addObserver(function observe(subject, topic, data) {
+      let matches = typeof matchFunc != 'function' || matchFunc(subject, data);
+      if (!matches) {
+        return;
+      }
+      Services.obs.removeObserver(observe, topic);
+      resolve({subject, data});
+    }, topic);
+  });
+}
+
 registerCleanupFunction(() => {
   Services.prefs.clearUserPref("network.connectivity-service.DNSv4.domain");
   Services.prefs.clearUserPref("network.connectivity-service.DNSv6.domain");
+  Services.prefs.clearUserPref("network.captive-portal-service.testMode");
+  Services.prefs.clearUserPref("network.connectivity-service.IPv4.url");
+  Services.prefs.clearUserPref("network.connectivity-service.IPv6.url");
 });
 
+let httpserver = null;
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+  return "http://localhost:" + httpserver.identity.primaryPort + "/content";
+});
+
+function contentHandler(metadata, response)
+{
+  response.setHeader("Content-Type", "text/plain");
+  response.setHeader("Cache-Control", "no-cache");
+
+  const responseBody = "anybody";
+  response.bodyOutputStream.write(responseBody, responseBody.length);
+}
+
 const DEFAULT_WAIT_TIME = 200; // ms
 
 const kDNSv6Domain = (mozinfo.os == "linux")
                        ? "ip6-localhost"
                        : "localhost";
 
 add_task(async function testDNS() {
   let ncs = Cc["@mozilla.org/network/network-connectivity-service;1"]
               .getService(Ci.nsINetworkConnectivityService);
 
   // Set the endpoints, trigger a DNS recheck, and wait for it to complete.
   Services.prefs.setCharPref("network.connectivity-service.DNSv4.domain", "example.org");
   Services.prefs.setCharPref("network.connectivity-service.DNSv6.domain", kDNSv6Domain);
   ncs.recheckDNS();
-  await new Promise(resolve => do_timeout(DEFAULT_WAIT_TIME, resolve));
+  await promiseObserverNotification("network:connectivity-service:dns-checks-complete");
 
   equal(ncs.DNSv4, Ci.nsINetworkConnectivityService.OK, "Check DNSv4 support (expect OK)");
   equal(ncs.DNSv6, Ci.nsINetworkConnectivityService.OK, "Check DNSv6 support (expect OK)");
 
   // Set the endpoints to non-exitant domains, trigger a DNS recheck, and wait for it to complete.
   Services.prefs.setCharPref("network.connectivity-service.DNSv4.domain", "does-not-exist.example");
   Services.prefs.setCharPref("network.connectivity-service.DNSv6.domain", "does-not-exist.example");
   ncs.recheckDNS();
-  await new Promise(resolve => do_timeout(DEFAULT_WAIT_TIME, resolve));
+  await promiseObserverNotification("network:connectivity-service:dns-checks-complete");
 
   equal(ncs.DNSv4, Ci.nsINetworkConnectivityService.NOT_AVAILABLE, "Check DNSv4 support (expect N/A)");
   equal(ncs.DNSv6, Ci.nsINetworkConnectivityService.NOT_AVAILABLE, "Check DNSv6 support (expect N/A)");
 
   // Set the endpoints back to the proper domains, and simulate a captive portal
   // event.
   Services.prefs.setCharPref("network.connectivity-service.DNSv4.domain", "example.org");
   Services.prefs.setCharPref("network.connectivity-service.DNSv6.domain", kDNSv6Domain);
   Services.obs.notifyObservers(null, "network:captive-portal-connectivity", null);
   // This will cause the state to go to UNKNOWN for a bit, until the check is completed.
   equal(ncs.DNSv4, Ci.nsINetworkConnectivityService.UNKNOWN, "Check DNSv4 support (expect UNKNOWN)");
   equal(ncs.DNSv6, Ci.nsINetworkConnectivityService.UNKNOWN, "Check DNSv6 support (expect UNKNOWN)");
 
-  await new Promise(resolve => do_timeout(DEFAULT_WAIT_TIME, resolve));
+  await promiseObserverNotification("network:connectivity-service:dns-checks-complete");
 
   equal(ncs.DNSv4, Ci.nsINetworkConnectivityService.OK, "Check DNSv4 support (expect OK)");
   equal(ncs.DNSv6, Ci.nsINetworkConnectivityService.OK, "Check DNSv6 support (expect OK)");
 
-  // It's difficult to check when there's no connectivity in automation,
+  httpserver = new HttpServer();
+  httpserver.registerPathHandler("/cps.txt", contentHandler);
+  httpserver.start(-1);
+
+  // Before setting the pref, this status is unknown in automation
+  equal(ncs.IPv4, Ci.nsINetworkConnectivityService.UNKNOWN, "Check IPv4 support (expect UNKNOWN)");
+  equal(ncs.IPv6, Ci.nsINetworkConnectivityService.UNKNOWN, "Check IPv6 support (expect UNKNOWN)");
+
+  Services.prefs.setBoolPref("network.captive-portal-service.testMode", true);
+  Services.prefs.setCharPref("network.connectivity-service.IPv4.url", URL);
+  Services.prefs.setCharPref("network.connectivity-service.IPv6.url", URL);
+  ncs.recheckIPConnectivity();
+  await promiseObserverNotification("network:connectivity-service:ip-checks-complete");
+
   equal(ncs.IPv4, Ci.nsINetworkConnectivityService.OK, "Check IPv4 support (expect OK)");
-  equal(ncs.IPv6, Ci.nsINetworkConnectivityService.OK, "Check IPv6 support (expect OK)");
+  // httpserver doesn't yet support IPv6
+  equal(ncs.IPv6, Ci.nsINetworkConnectivityService.NOT_AVAILABLE, "Check IPv6 support (expect OK[TODO])");
+
+  // check that the CPS status is NOT_AVAILABLE when the endpoint is down.
+  await new Promise(resolve => httpserver.stop(resolve));
+  Services.obs.notifyObservers(null, "network:captive-portal-connectivity", null);
+  await promiseObserverNotification("network:connectivity-service:ip-checks-complete");
+
+  equal(ncs.IPv4, Ci.nsINetworkConnectivityService.NOT_AVAILABLE, "Check IPv4 support (expect NOT_AVAILABLE)");
+  equal(ncs.IPv6, Ci.nsINetworkConnectivityService.NOT_AVAILABLE, "Check IPv6 support (expect NOT_AVAILABLE)");
 });