bug 1466462 - TRR: disable EDNS Client Subnet by default r=mcmanus
authorDaniel Stenberg <daniel@haxx.se>
Mon, 13 Aug 2018 15:45:15 +0000
changeset 486333 ff3f57736492ae3884047c9e27521db1ce4f1138
parent 486332 7f54ced0aeebad9503625b4007453a141720df50
child 486334 9a65efe88395ef3d8e79f652a317bafbd9bbe962
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmcmanus
bugs1466462
milestone63.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 1466462 - TRR: disable EDNS Client Subnet by default r=mcmanus Set the "network.trr.disable-ECS" pref to false to disable. MozReview-Commit-ID: GE6L8Vpvuu0 Differential Revision: https://phabricator.services.mozilla.com/D2933
modules/libpref/init/all.js
netwerk/dns/TRR.cpp
netwerk/dns/TRR.h
netwerk/dns/TRRService.cpp
netwerk/dns/TRRService.h
netwerk/test/unit/test_trr.js
testing/xpcshell/moz-http2/moz-http2.js
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -5373,16 +5373,18 @@ pref("network.trr.confirmationNS", "exam
 pref("network.trr.bootstrapAddress", "");
 // TRR blacklist entry expire time (in seconds). Default is one minute.
 // Meant to survive basically a page load.
 pref("network.trr.blacklist-duration", 60);
 // Single TRR request timeout, in milliseconds
 pref("network.trr.request-timeout", 3000);
 // Allow AAAA entries to be used "early", before the A results are in
 pref("network.trr.early-AAAA", false);
+// Explicitly disable ECS (EDNS Client Subnet, RFC 7871)
+pref("network.trr.disable-ECS", true);
 
 pref("captivedetect.canonicalURL", "http://detectportal.firefox.com/success.txt");
 pref("captivedetect.canonicalContent", "success\n");
 pref("captivedetect.maxWaitingTime", 5000);
 pref("captivedetect.pollingTime", 3000);
 pref("captivedetect.maxRetryCount", 5);
 
 #ifdef RELEASE_OR_BETA
--- a/netwerk/dns/TRR.cpp
+++ b/netwerk/dns/TRR.cpp
@@ -53,32 +53,33 @@ TRR::Notify(nsITimer *aTimer)
   }
 
   return NS_OK;
 }
 
 // convert a given host request to a DOH 'body'
 //
 nsresult
-TRR::DohEncode(nsCString &aBody)
+TRR::DohEncode(nsCString &aBody, bool aDisableECS)
 {
   aBody.Truncate();
   // Header
   aBody += '\0';
   aBody += '\0'; // 16 bit id
   aBody += 0x01; // |QR|   Opcode  |AA|TC|RD| Set the RD bit
   aBody += '\0'; // |RA|   Z    |   RCODE   |
   aBody += '\0';
   aBody += 1;    // QDCOUNT (number of entries in the question section)
   aBody += '\0';
   aBody += '\0'; // ANCOUNT
   aBody += '\0';
   aBody += '\0'; // NSCOUNT
-  aBody += '\0';
+
   aBody += '\0'; // ARCOUNT
+  aBody += aDisableECS ? 1 : '\0';   // ARCOUNT low byte for EDNS(0)
 
   // Question
 
   // The input host name should be converted to a sequence of labels, where
   // each label consists of a length octet followed by that number of
   // octets.  The domain name terminates with the zero length octet for the
   // null label of the root.
   // Followed by 16 bit QTYPE and 16 bit QCLASS
@@ -109,16 +110,48 @@ TRR::DohEncode(nsCString &aBody)
     offset += labelLength + 1; // move over label and dot
   } while(true);
 
   aBody += '\0'; // upper 8 bit TYPE
   aBody += static_cast<uint8_t>(mType);
   aBody += '\0'; // upper 8 bit CLASS
   aBody += kDNS_CLASS_IN;  // IN - "the Internet"
 
+  if (aDisableECS) {
+    // EDNS(0) is RFC 6891, ECS is RFC 7871
+    aBody += '\0'; // NAME       | domain name  | MUST be 0 (root domain)      |
+    aBody += '\0';
+    aBody += 41;   // TYPE       | u_int16_t    | OPT (41)                     |
+    aBody += 16;   // CLASS      | u_int16_t    | requestor's UDP payload size |
+    aBody += '\0'; // advertise 4K (high-byte: 16 | low-byte: 0), ignored by DoH
+    aBody += '\0'; // TTL        | u_int32_t    | extended RCODE and flags     |
+    aBody += '\0';
+    aBody += '\0';
+    aBody += '\0';
+
+    aBody += '\0'; // upper 8 bit RDLEN
+    aBody += 8;    // RDLEN      | u_int16_t    | length of all RDATA          |
+
+    // RDATA      | octet stream | {attribute,value} pairs      |
+    // The RDATA is just the ECS option setting zero subnet prefix
+
+    aBody += '\0'; // upper 8 bit OPTION-CODE ECS
+    aBody += 8;    // OPTION-CODE, 2 octets, for ECS is 8
+
+    aBody += '\0'; // upper 8 bit OPTION-LENGTH
+    aBody += 4;    // OPTION-LENGTH, 2 octets, contains the length of the payload
+                   // after OPTION-LENGTH
+    aBody += '\0'; // upper 8 bit FAMILY
+    aBody += AF_INET; // FAMILY, 2 octets
+
+    aBody += '\0'; // SOURCE PREFIX-LENGTH      |     SCOPE PREFIX-LENGTH       |
+    aBody += '\0';
+
+    // ADDRESS, minimum number of octets == nothing because zero bits
+  }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 TRR::Run()
 {
   MOZ_ASSERT(NS_IsMainThread());
   if ((gTRRService == nullptr) || NS_FAILED(SendHTTPRequest())) {
@@ -158,37 +191,38 @@ TRR::SendHTTPRequest()
 
   nsresult rv;
   nsCOMPtr<nsIIOService> ios(do_GetIOService(&rv));
   NS_ENSURE_SUCCESS(rv, rv);
 
   bool useGet = gTRRService->UseGET();
   nsAutoCString body;
   nsCOMPtr<nsIURI> dnsURI;
+  bool disableECS = gTRRService->DisableECS();
 
   LOG(("TRR::SendHTTPRequest resolve %s type %u\n", mHost.get(), mType));
 
   if (useGet) {
     nsAutoCString tmp;
-    rv = DohEncode(tmp);
+    rv = DohEncode(tmp, disableECS);
     NS_ENSURE_SUCCESS(rv, rv);
 
     /* For GET requests, the outgoing packet needs to be Base64url-encoded and
        then appended to the end of the URI. */
     rv = Base64URLEncode(tmp.Length(), reinterpret_cast<const unsigned char *>(tmp.get()),
                          Base64URLEncodePaddingPolicy::Omit, body);
     NS_ENSURE_SUCCESS(rv, rv);
 
     nsAutoCString uri;
     gTRRService->GetURI(uri);
     uri.Append(NS_LITERAL_CSTRING("?dns="));
     uri.Append(body);
     rv = NS_NewURI(getter_AddRefs(dnsURI), uri);
   } else {
-    rv = DohEncode(body);
+    rv = DohEncode(body, disableECS);
     NS_ENSURE_SUCCESS(rv, rv);
 
     nsAutoCString uri;
     gTRRService->GetURI(uri);
     rv = NS_NewURI(getter_AddRefs(dnsURI), uri);
   }
   if (NS_FAILED(rv)) {
     LOG(("TRR:SendHTTPRequest: NewURI failed!\n"));
--- a/netwerk/dns/TRR.h
+++ b/netwerk/dns/TRR.h
@@ -137,17 +137,17 @@ public:
   enum TrrType Type() { return mType; }
   nsCString mHost;
   RefPtr<nsHostRecord> mRec;
   RefPtr<AHostResolver> mHostResolver;
 
 private:
   ~TRR() = default;
   nsresult SendHTTPRequest();
-  nsresult DohEncode(nsCString &target);
+  nsresult DohEncode(nsCString &target, bool aDisableECS);
   nsresult PassQName(unsigned int &index);
   nsresult GetQname(nsAutoCString &aQname, unsigned int &aIndex);
   nsresult DohDecode(nsCString &aHost);
   nsresult ReturnData();
 
   // FailData() must be called to signal that the asynch TRR resolve is
   // completed. For failed name resolves ("no such host"), the 'error' it
   // passses on in its argument must be NS_ERROR_UNKNOWN_HOST. Other errors
--- a/netwerk/dns/TRRService.cpp
+++ b/netwerk/dns/TRRService.cpp
@@ -38,16 +38,17 @@ TRRService::TRRService()
   , mTRRBlacklistExpireTime(72 * 3600)
   , mTRRTimeout(3000)
   , mLock("trrservice")
   , mConfirmationNS(NS_LITERAL_CSTRING("example.com"))
   , mWaitForCaptive(true)
   , mRfc1918(false)
   , mCaptiveIsPassed(false)
   , mUseGET(false)
+  , mDisableECS(true)
   , mClearTRRBLStorage(false)
   , mConfirmationState(CONFIRM_INIT)
   , mRetryConfirmInterval(1000)
 {
   MOZ_ASSERT(NS_IsMainThread(), "wrong thread");
 }
 
 nsresult
@@ -220,16 +221,22 @@ TRRService::ReadPrefs(const char *name)
     }
   }
   if (!name || !strcmp(name, kDisableIpv6Pref)) {
     bool tmp;
     if (NS_SUCCEEDED(Preferences::GetBool(kDisableIpv6Pref, &tmp))) {
       mDisableIPv6 = tmp;
     }
   }
+  if (!name || !strcmp(name, TRR_PREF("disable-ECS"))) {
+    bool tmp;
+    if (NS_SUCCEEDED(Preferences::GetBool(TRR_PREF("disable-ECS"), &tmp))) {
+      mDisableECS = tmp;
+    }
+  }
 
   return NS_OK;
 }
 
 nsresult
 TRRService::GetURI(nsCString &result)
 {
   MutexAutoLock lock(mLock);
--- a/netwerk/dns/TRRService.h
+++ b/netwerk/dns/TRRService.h
@@ -33,16 +33,17 @@ public:
   nsresult Start();
   bool Enabled();
 
   uint32_t Mode() { return mMode; }
   bool AllowRFC1918() { return mRfc1918; }
   bool UseGET() { return mUseGET; }
   bool EarlyAAAA() { return mEarlyAAAA; }
   bool DisableIPv6() { return mDisableIPv6; }
+  bool DisableECS() { return mDisableECS; }
   nsresult GetURI(nsCString &result);
   nsresult GetCredentials(nsCString &result);
   uint32_t GetRequestTimeout() { return mTRRTimeout; }
 
   LookupStatus CompleteLookup(nsHostRecord *, nsresult, mozilla::net::AddrInfo *, bool pb) override;
   void TRRBlacklist(const nsACString &host, bool privateBrowsing, bool aParentsToo);
   bool IsTRRBlacklisted(const nsACString &host, bool privateBrowsing, bool fullhost);
 
@@ -66,16 +67,17 @@ private:
   nsCString mBootstrapAddr;
 
   Atomic<bool, Relaxed> mWaitForCaptive; // wait for the captive portal to say OK before using TRR
   Atomic<bool, Relaxed> mRfc1918; // okay with local IP addresses in DOH responses?
   Atomic<bool, Relaxed> mCaptiveIsPassed; // set when captive portal check is passed
   Atomic<bool, Relaxed> mUseGET; // do DOH using GET requests (instead of POST)
   Atomic<bool, Relaxed> mEarlyAAAA; // allow use of AAAA results before A is in
   Atomic<bool, Relaxed> mDisableIPv6; // don't even try
+  Atomic<bool, Relaxed> mDisableECS;  // disable EDNS Client Subnet in requests
 
   // TRR Blacklist storage
   RefPtr<DataStorage> mTRRBLStorage;
   Atomic<bool, Relaxed> mClearTRRBLStorage;
 
   enum ConfirmationState {
     CONFIRM_INIT = 0,
     CONFIRM_TRYING = 1,
--- a/netwerk/test/unit/test_trr.js
+++ b/netwerk/test/unit/test_trr.js
@@ -53,16 +53,17 @@ function resetTRRPrefs() {
   prefs.clearUserPref("network.trr.credentials");
   prefs.clearUserPref("network.trr.wait-for-portal");
   prefs.clearUserPref("network.trr.allow-rfc1918");
   prefs.clearUserPref("network.trr.useGET");
   prefs.clearUserPref("network.trr.confirmationNS");
   prefs.clearUserPref("network.trr.bootstrapAddress");
   prefs.clearUserPref("network.trr.blacklist-duration");
   prefs.clearUserPref("network.trr.request-timeout");
+  prefs.clearUserPref("network.trr.disable-ECS");
 }
 
 registerCleanupFunction(() => {
   prefs.clearUserPref("network.http.spdy.enabled");
   prefs.clearUserPref("network.http.spdy.enabled.http2");
   prefs.clearUserPref("network.dns.localDomains");
   prefs.clearUserPref("network.dns.native-is-localhost");
   resetTRRPrefs();
@@ -253,33 +254,47 @@ function test8()
 {
   prefs.setIntPref("network.trr.mode", 3); // TRR-only
   prefs.setCharPref("network.trr.uri", "https://foo.example.com:" + h2Port + "/dns-rfc1918");
   prefs.setBoolPref("network.trr.allow-rfc1918", true);
   test_answer = "192.168.0.1";
   listen = dns.asyncResolve("rfc1918.example.com", 0, listenerFine, mainThread, defaultOriginAttributes);
 }
 
+// use GET and disable ECS (makes a larger request)
+function test8b()
+{
+  prefs.setIntPref("network.trr.mode", 3); // TRR-only
+  prefs.setCharPref("network.trr.uri", "https://foo.example.com:" + h2Port + "/dns-ecs");
+  prefs.clearUserPref("network.trr.allow-rfc1918");
+  prefs.setBoolPref("network.trr.useGET", true);
+  prefs.setBoolPref("network.trr.disable-ECS", true);
+  test_answer = "5.5.5.5";
+  listen = dns.asyncResolve("ecs.example.com", 0, listenerFine, mainThread, defaultOriginAttributes);
+}
+
 // use GET
 function test9()
 {
   prefs.setIntPref("network.trr.mode", 3); // TRR-only
   prefs.setCharPref("network.trr.uri", "https://foo.example.com:" + h2Port + "/dns-get");
   prefs.clearUserPref("network.trr.allow-rfc1918");
   prefs.setBoolPref("network.trr.useGET", true);
+  prefs.setBoolPref("network.trr.disable-ECS", false);
   test_answer = "1.2.3.4";
   listen = dns.asyncResolve("get.example.com", 0, listenerFine, mainThread, defaultOriginAttributes);
 }
 
 // confirmationNS set without confirmed NS yet
 // NOTE: this requires test9 to run before, as the http2 server resets state there
 function test10()
 {
   prefs.setIntPref("network.trr.mode", 3); // TRR-only
   prefs.clearUserPref("network.trr.useGET");
+  prefs.clearUserPref("network.trr.disable-ECS");
   prefs.setCharPref("network.trr.uri", "https://foo.example.com:" + h2Port + "/dns-confirm");
   prefs.setCharPref("network.trr.confirmationNS", "confirm.example.com");
   test_loops = 100; // set for test10b
   try {
     listen = dns.asyncResolve("wrong.example.com", 0, listenerFails,
                               mainThread, defaultOriginAttributes);
   } catch (e) {
     // NS_ERROR_UNKNOWN_HOST exception is expected
@@ -433,16 +448,17 @@ var tests = [ test1,
               test2,
               test3,
               test4,
               test5,
               test5b,
               test6,
               test7,
               test8,
+              test8b,
               test9,
               test10,
               test10b,
               test11,
               test12,
               test13,
               test14,
               test15,
--- a/testing/xpcshell/moz-http2/moz-http2.js
+++ b/testing/xpcshell/moz-http2/moz-http2.js
@@ -599,17 +599,28 @@ function handleRequest(req, res) {
     res.setHeader('Content-Type', 'application/dns-message');
     res.setHeader('Content-Length', content.length);
     res.writeHead(200);
     res.write(content);
     res.end("");
     return;
 
   }
-
+  // for use with test_trr.js, test8b
+  else if (u.path === "/dns-ecs?dns=AAABAAABAAAAAAABA2VjcwdleGFtcGxlA2NvbQAAAQABAAApEAAAAAAAAAgACAAEAAIAAA") {
+    // the query string asks for an A entry for ecs.example.com
+    // ecs.example.com has A entry 5.5.5.5
+    var content= new Buffer("00000100000100010000000003656373076578616D706C6503636F6D0000010001C00C0001000100000037000405050505", "hex");
+    res.setHeader('Content-Type', 'application/dns-message');
+    res.setHeader('Content-Length', content.length);
+    res.writeHead(200);
+    res.write(content);
+    res.end("");
+    return;
+  }
   // for use with test_trr.js
   else if (u.path === "/dns-get?dns=AAABAAABAAAAAAAAA2dldAdleGFtcGxlA2NvbQAAAQAB") {
     // the query string asks for an A entry for get.example.com
     // get.example.com has A entry 1.2.3.4
     var content= new Buffer("00000100000100010000000003676574076578616D706C6503636F6D0000010001C00C0001000100000037000401020304", "hex");
     res.setHeader('Content-Type', 'application/dns-message');
     res.setHeader('Content-Length', content.length);
     res.writeHead(200);