diff options
Diffstat (limited to '')
-rw-r--r-- | netwerk/test/unit/test_trr.js | 2136 |
1 files changed, 2136 insertions, 0 deletions
diff --git a/netwerk/test/unit/test_trr.js b/netwerk/test/unit/test_trr.js new file mode 100644 index 0000000000..fa2d915c20 --- /dev/null +++ b/netwerk/test/unit/test_trr.js @@ -0,0 +1,2136 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const dns = Cc["@mozilla.org/network/dns-service;1"].getService( + Ci.nsIDNSService +); +const { MockRegistrar } = ChromeUtils.import( + "resource://testing-common/MockRegistrar.jsm" +); +const mainThread = Services.tm.currentThread; + +const gDefaultPref = Services.prefs.getDefaultBranch(""); +const gOverride = Cc["@mozilla.org/network/native-dns-override;1"].getService( + Ci.nsINativeDNSResolverOverride +); + +const defaultOriginAttributes = {}; +let h2Port = null; + +async function SetParentalControlEnabled(aEnabled) { + let parentalControlsService = { + parentalControlsEnabled: aEnabled, + QueryInterface: ChromeUtils.generateQI(["nsIParentalControlsService"]), + }; + let cid = MockRegistrar.register( + "@mozilla.org/parental-controls-service;1", + parentalControlsService + ); + dns.reloadParentalControlEnabled(); + MockRegistrar.unregister(cid); +} + +function setup() { + dump("start!\n"); + + let env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + h2Port = env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + + Services.prefs.setBoolPref("network.http.spdy.enabled", true); + Services.prefs.setBoolPref("network.http.spdy.enabled.http2", true); + // the TRR server is on 127.0.0.1 + Services.prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1"); + + // use the h2 server as DOH provider + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh` + ); + // make all native resolve calls "secretly" resolve localhost instead + Services.prefs.setBoolPref("network.dns.native-is-localhost", true); + + // 0 - off, 1 - reserved, 2 - TRR first, 3 - TRR only, 4 - reserved + Services.prefs.setIntPref("network.trr.mode", 2); // TRR first + Services.prefs.setBoolPref("network.trr.wait-for-portal", false); + // By default wait for all responses before notifying the listeners. + Services.prefs.setBoolPref("network.trr.wait-for-A-and-AAAA", true); + // don't confirm that TRR is working, just go! + Services.prefs.setCharPref("network.trr.confirmationNS", "skip"); + // some tests rely on the cache not being cleared on pref change. + // we specifically test that this works + Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. // the foo.example.com domain name. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + SetParentalControlEnabled(false); +} + +setup(); +registerCleanupFunction(async () => { + trr_clear_prefs(); +}); + +// This is an IP that is local, so we don't crash when connecting to it, +// but also connecting to it fails, so we attempt to retry with regular DNS. +const BAD_IP = (() => { + if (mozinfo.os == "linux" || mozinfo.os == "android") { + return "127.9.9.9"; + } + return "0.0.0.0"; +})(); + +class DNSListener { + constructor( + name, + expectedAnswer, + expectedSuccess = true, + delay, + trrServer = "", + expectEarlyFail = false, + flags = 0 + ) { + this.name = name; + this.expectedAnswer = expectedAnswer; + this.expectedSuccess = expectedSuccess; + this.delay = delay; + this.promise = new Promise(resolve => { + this.resolve = resolve; + }); + + let resolverInfo = + trrServer == "" ? null : dns.newTRRResolverInfo(trrServer); + try { + this.request = dns.asyncResolve( + name, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + flags, + resolverInfo, + this, + mainThread, + defaultOriginAttributes + ); + Assert.ok(!expectEarlyFail); + } catch (e) { + Assert.ok(expectEarlyFail); + this.resolve([e]); + } + } + + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.ok( + inRequest == this.request, + "Checking that this is the correct callback" + ); + + // If we don't expect success here, just resolve and the caller will + // decide what to do with the results. + if (!this.expectedSuccess) { + this.resolve([inRequest, inRecord, inStatus]); + return; + } + + Assert.equal(inStatus, Cr.NS_OK, "Checking status"); + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + let answer = inRecord.getNextAddrAsString(); + Assert.equal( + answer, + this.expectedAnswer, + `Checking result for ${this.name}` + ); + + if (this.delay !== undefined) { + Assert.greaterOrEqual( + inRecord.trrFetchDurationNetworkOnly, + this.delay, + `the response should take at least ${this.delay}` + ); + + Assert.greaterOrEqual( + inRecord.trrFetchDuration, + this.delay, + `the response should take at least ${this.delay}` + ); + + if (this.delay == 0) { + // The response timing should be really 0 + Assert.equal( + inRecord.trrFetchDurationNetworkOnly, + 0, + `the response time should be 0` + ); + + Assert.equal( + inRecord.trrFetchDuration, + this.delay, + `the response time should be 0` + ); + } + } + + this.resolve([inRequest, inRecord, inStatus]); + } + + QueryInterface(aIID) { + if (aIID.equals(Ci.nsIDNSListener) || aIID.equals(Ci.nsISupports)) { + return this; + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + } + + // Implement then so we can await this as a promise. + then() { + return this.promise.then.apply(this.promise, arguments); + } +} + +add_task(async function test0_nodeExecute() { + // This test checks that moz-http2.js running in node is working. + // This should always be the first test in this file (except for setup) + // otherwise we may encounter random failures when the http2 server is down. + + await NodeServer.execute("bad_id", `"hello"`) + .then(() => ok(false, "expecting to throw")) + .catch(e => equal(e.message, "Error: could not find id")); +}); + +function makeChan(url, mode) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.setTRRMode(mode); + return chan; +} + +add_task( + { skip_if: () => mozinfo.os == "mac" }, + async function test_trr_flags() { + Services.prefs.setBoolPref("network.trr.fallback-on-zero-response", true); + let httpserv = new HttpServer(); + httpserv.registerPathHandler("/", function handler(metadata, response) { + let content = "ok"; + response.setHeader("Content-Length", `${content.length}`); + response.bodyOutputStream.write(content, content.length); + }); + httpserv.start(-1); + const URL = `http://example.com:${httpserv.identity.primaryPort}/`; + + dns.clearCache(true); + Services.prefs.setCharPref( + "network.trr.uri", + `https://localhost:${h2Port}/doh?responseIP=${BAD_IP}` + ); + + Services.prefs.setIntPref("network.trr.mode", 0); + dns.clearCache(true); + let chan = makeChan(URL, Ci.nsIRequest.TRR_DEFAULT_MODE); + await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve))); + equal(chan.getTRRMode(), Ci.nsIRequest.TRR_DEFAULT_MODE); + dns.clearCache(true); + chan = makeChan(URL, Ci.nsIRequest.TRR_DISABLED_MODE); + await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve))); + equal(chan.getTRRMode(), Ci.nsIRequest.TRR_DISABLED_MODE); + dns.clearCache(true); + chan = makeChan(URL, Ci.nsIRequest.TRR_FIRST_MODE); + await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve))); + equal(chan.getTRRMode(), Ci.nsIRequest.TRR_FIRST_MODE); + dns.clearCache(true); + chan = makeChan( + `http://example.com:${httpserv.identity.primaryPort}/`, + Ci.nsIRequest.TRR_ONLY_MODE + ); + // Should fail as it tries to connect to local but unavailable IP + await new Promise(resolve => + chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE)) + ); + equal(chan.getTRRMode(), Ci.nsIRequest.TRR_ONLY_MODE); + + dns.clearCache(true); + Services.prefs.setCharPref( + "network.trr.uri", + `https://localhost:${h2Port}/doh?responseIP=${BAD_IP}` + ); + Services.prefs.setIntPref("network.trr.mode", 2); + + dns.clearCache(true); + chan = makeChan(URL, Ci.nsIRequest.TRR_DEFAULT_MODE); + // Does get the IP from TRR, but failure means it falls back to DNS. + await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve))); + dns.clearCache(true); + chan = makeChan(URL, Ci.nsIRequest.TRR_DISABLED_MODE); + await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve))); + dns.clearCache(true); + // Does get the IP from TRR, but failure means it falls back to DNS. + chan = makeChan(URL, Ci.nsIRequest.TRR_FIRST_MODE); + await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve))); + dns.clearCache(true); + chan = makeChan(URL, Ci.nsIRequest.TRR_ONLY_MODE); + await new Promise(resolve => + chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE)) + ); + + dns.clearCache(true); + Services.prefs.setCharPref( + "network.trr.uri", + `https://localhost:${h2Port}/doh?responseIP=${BAD_IP}` + ); + Services.prefs.setIntPref("network.trr.mode", 3); + + dns.clearCache(true); + chan = makeChan(URL, Ci.nsIRequest.TRR_DEFAULT_MODE); + await new Promise(resolve => + chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE)) + ); + dns.clearCache(true); + chan = makeChan(URL, Ci.nsIRequest.TRR_DISABLED_MODE); + await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve))); + dns.clearCache(true); + chan = makeChan(URL, Ci.nsIRequest.TRR_FIRST_MODE); + await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve))); + dns.clearCache(true); + chan = makeChan(URL, Ci.nsIRequest.TRR_ONLY_MODE); + await new Promise(resolve => + chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE)) + ); + + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 5); + Services.prefs.setCharPref( + "network.trr.uri", + `https://localhost:${h2Port}/doh?responseIP=1.1.1.1` + ); + + dns.clearCache(true); + chan = makeChan(URL, Ci.nsIRequest.TRR_DEFAULT_MODE); + await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve))); + dns.clearCache(true); + chan = makeChan(URL, Ci.nsIRequest.TRR_DISABLED_MODE); + await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve))); + dns.clearCache(true); + chan = makeChan(URL, Ci.nsIRequest.TRR_FIRST_MODE); + await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve))); + dns.clearCache(true); + chan = makeChan(URL, Ci.nsIRequest.TRR_ONLY_MODE); + await new Promise(resolve => chan.asyncOpen(new ChannelListener(resolve))); + + await new Promise(resolve => httpserv.stop(resolve)); + Services.prefs.clearUserPref("network.trr.fallback-on-zero-response"); + } +); + +// verify basic A record +add_task(async function test1() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2` + ); + + await new DNSListener("bar.example.com", "2.2.2.2"); +}); + +// verify basic A record - without bootstrapping +add_task(async function test1b() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3` + ); + Services.prefs.clearUserPref("network.trr.bootstrapAddress"); + Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + + await new DNSListener("bar.example.com", "3.3.3.3"); + + Services.prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1"); + Services.prefs.clearUserPref("network.dns.localDomains"); +}); + +// verify that the name was put in cache - it works with bad DNS URI +add_task(async function test2() { + // Don't clear the cache. That is what we're checking. + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/404` + ); + + await new DNSListener("bar.example.com", "3.3.3.3"); +}); + +// verify working credentials in DOH request +add_task(async function test3() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=4.4.4.4&auth=true` + ); + Services.prefs.setCharPref("network.trr.credentials", "user:password"); + + await new DNSListener("bar.example.com", "4.4.4.4"); +}); + +// verify failing credentials in DOH request +add_task(async function test4() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=4.4.4.4&auth=true` + ); + Services.prefs.setCharPref("network.trr.credentials", "evil:person"); + + let [, , inStatus] = await new DNSListener( + "wrong.example.com", + undefined, + false + ); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); +}); + +// verify DOH push, part A +add_task(async function test5() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=5.5.5.5&push=true` + ); + + await new DNSListener("first.example.com", "5.5.5.5"); +}); + +add_task(async function test5b() { + // At this point the second host name should've been pushed and we can resolve it using + // cache only. Set back the URI to a path that fails. + // Don't clear the cache, otherwise we lose the pushed record. + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/404` + ); + dump("test5b - resolve push.example.org please\n"); + + await new DNSListener("push.example.org", "2018::2018"); +}); + +// verify AAAA entry +add_task(async function test6() { + Services.prefs.setBoolPref("network.trr.wait-for-A-and-AAAA", true); + + dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.early-AAAA", true); // ignored when wait-for-A-and-AAAA is true + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2020&delayIPv4=100` + ); + await new DNSListener("aaaa.example.com", "2020:2020::2020"); + + dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.early-AAAA", false); // ignored when wait-for-A-and-AAAA is true + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2030&delayIPv4=100` + ); + await new DNSListener("aaaa.example.com", "2020:2020::2030"); + + dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.early-AAAA", true); // ignored when wait-for-A-and-AAAA is true + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2020&delayIPv6=100` + ); + await new DNSListener("aaaa.example.com", "2020:2020::2020"); + + dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.early-AAAA", false); // ignored when wait-for-A-and-AAAA is true + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2030&delayIPv6=100` + ); + await new DNSListener("aaaa.example.com", "2020:2020::2030"); + + dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.early-AAAA", true); // ignored when wait-for-A-and-AAAA is true + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2020` + ); + await new DNSListener("aaaa.example.com", "2020:2020::2020"); + + dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.early-AAAA", false); // ignored when wait-for-A-and-AAAA is true + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2030` + ); + await new DNSListener("aaaa.example.com", "2020:2020::2030"); + + Services.prefs.setBoolPref("network.trr.wait-for-A-and-AAAA", false); + + dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.early-AAAA", true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2020&delayIPv4=100` + ); + await new DNSListener("aaaa.example.com", "2020:2020::2020"); + + dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.early-AAAA", false); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2030&delayIPv4=100` + ); + await new DNSListener("aaaa.example.com", "2020:2020::2030"); + + dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.early-AAAA", true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2020&delayIPv6=100` + ); + await new DNSListener("aaaa.example.com", "2020:2020::2020"); + + dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.early-AAAA", false); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2030&delayIPv6=100` + ); + await new DNSListener("aaaa.example.com", "2020:2020::2030"); + + dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.early-AAAA", true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2020` + ); + await new DNSListener("aaaa.example.com", "2020:2020::2020"); + + dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.early-AAAA", false); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2020:2020::2030` + ); + await new DNSListener("aaaa.example.com", "2020:2020::2030"); + + Services.prefs.clearUserPref("network.trr.early-AAAA"); + Services.prefs.clearUserPref("network.trr.wait-for-A-and-AAAA"); +}); + +// verify RFC1918 address from the server is rejected +add_task(async function test7() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=192.168.0.1` + ); + let [, , inStatus] = await new DNSListener( + "rfc1918.example.com", + undefined, + false + ); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=::ffff:192.168.0.1` + ); + [, , inStatus] = await new DNSListener( + "rfc1918-ipv6.example.com", + undefined, + false + ); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); +}); + +// verify RFC1918 address from the server is fine when told so +add_task(async function test8() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=192.168.0.1` + ); + Services.prefs.setBoolPref("network.trr.allow-rfc1918", true); + await new DNSListener("rfc1918.example.com", "192.168.0.1"); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=::ffff:192.168.0.1` + ); + await new DNSListener("rfc1918-ipv6.example.com", "::ffff:192.168.0.1"); +}); + +// use GET and disable ECS (makes a larger request) +// verify URI template cutoff +add_task(async function test8b() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh{?dns}` + ); + Services.prefs.clearUserPref("network.trr.allow-rfc1918"); + Services.prefs.setBoolPref("network.trr.useGET", true); + Services.prefs.setBoolPref("network.trr.disable-ECS", true); + await new DNSListener("ecs.example.com", "5.5.5.5"); +}); + +// use GET +add_task(async function test9() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh` + ); + Services.prefs.clearUserPref("network.trr.allow-rfc1918"); + Services.prefs.setBoolPref("network.trr.useGET", true); + Services.prefs.setBoolPref("network.trr.disable-ECS", false); + await new DNSListener("get.example.com", "5.5.5.5"); +}); + +// confirmationNS set without confirmed NS yet +add_task(async function test10() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.clearUserPref("network.trr.useGET"); + Services.prefs.clearUserPref("network.trr.disable-ECS"); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=1::ffff` + ); + Services.prefs.setCharPref( + "network.trr.confirmationNS", + "confirm.example.com" + ); + + await new DNSListener("skipConfirmationForMode3.example.com", "1::ffff"); +}); + +// use a slow server and short timeout! +add_task(async function test11() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref("network.trr.confirmationNS", "skip"); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/dns-750ms` + ); + Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 10); + let [, , inStatus] = await new DNSListener( + "test11.example.com", + undefined, + false + ); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); +}); + +// gets an no answers back from DoH. Falls back to regular DNS +add_task(async function test12() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first + Services.prefs.setIntPref("network.trr.request_timeout_ms", 10); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=none` + ); + Services.prefs.clearUserPref("network.trr.request_timeout_ms"); + Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms"); + await new DNSListener("confirm.example.com", "127.0.0.1"); +}); + +// TRR-first gets a 404 back from DOH +add_task(async function test13() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/404` + ); + await new DNSListener("test13.example.com", "127.0.0.1"); +}); + +// Test that MODE_RESERVED4 (previously MODE_SHADOW) is treated as TRR off. +add_task(async function test14() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 4); // MODE_RESERVED4. Interpreted as TRR off. + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/404` + ); + await new DNSListener("test14.example.com", "127.0.0.1"); + + Services.prefs.setIntPref("network.trr.mode", 4); // MODE_RESERVED4. Interpreted as TRR off. + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2` + ); + await new DNSListener("bar.example.com", "127.0.0.1"); +}); + +// Test that MODE_RESERVED4 (previously MODE_SHADOW) is treated as TRR off. +add_task(async function test15() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 4); // MODE_RESERVED4. Interpreted as TRR off. + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/dns-750ms` + ); + Services.prefs.setIntPref("network.trr.request_timeout_ms", 10); + Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 10); + await new DNSListener("test15.example.com", "127.0.0.1"); + + Services.prefs.setIntPref("network.trr.mode", 4); // MODE_RESERVED4. Interpreted as TRR off. + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2` + ); + await new DNSListener("bar.example.com", "127.0.0.1"); +}); + +// TRR-first and timed out TRR +add_task(async function test16() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/dns-750ms` + ); + Services.prefs.setIntPref("network.trr.request_timeout_ms", 10); + Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 10); + await new DNSListener("test16.example.com", "127.0.0.1"); +}); + +// TRR-only and chase CNAME +add_task(async function test17() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/dns-cname` + ); + Services.prefs.clearUserPref("network.trr.request_timeout_ms"); + Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms"); + await new DNSListener("cname.example.com", "99.88.77.66"); +}); + +// TRR-only and a CNAME loop +add_task(async function test18() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=none&cnameloop=true` + ); + let [, , inStatus] = await new DNSListener( + "test18.example.com", + undefined, + false + ); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); +}); + +// Test that MODE_RESERVED1 (previously MODE_PARALLEL) is treated as TRR off. +add_task(async function test19() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 1); // MODE_RESERVED1. Interpreted as TRR off. + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=none&cnameloop=true` + ); + await new DNSListener("test19.example.com", "127.0.0.1"); + + Services.prefs.setIntPref("network.trr.mode", 1); // MODE_RESERVED1. Interpreted as TRR off. + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2` + ); + await new DNSListener("bar.example.com", "127.0.0.1"); +}); + +// TRR-first and a CNAME loop +add_task(async function test20() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=none&cnameloop=true` + ); + await new DNSListener("test20.example.com", "127.0.0.1"); +}); + +// Test that MODE_RESERVED4 (previously MODE_SHADOW) is treated as TRR off. +add_task(async function test21() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 4); // MODE_RESERVED4. Interpreted as TRR off. + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=none&cnameloop=true` + ); + await new DNSListener("test21.example.com", "127.0.0.1"); + + Services.prefs.setIntPref("network.trr.mode", 4); // MODE_RESERVED4. Interpreted as TRR off. + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2` + ); + await new DNSListener("bar.example.com", "127.0.0.1"); +}); + +// verify that basic A record name mismatch gets rejected. +// Gets a response for bar.example.com instead of what it requested +add_task(async function test22() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only to avoid native fallback + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?hostname=bar.example.com` + ); + let [, , inStatus] = await new DNSListener( + "mismatch.example.com", + undefined, + false + ); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); +}); + +// TRR-only, with a CNAME response with a bundled A record for that CNAME! +add_task(async function test23() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/dns-cname-a` + ); + await new DNSListener("cname-a.example.com", "9.8.7.6"); +}); + +// TRR-first check that TRR result is used +add_task(async function test24() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first + Services.prefs.setCharPref("network.trr.excluded-domains", ""); + Services.prefs.setCharPref("network.trr.builtin-excluded-domains", ""); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192` + ); + await new DNSListener("bar.example.com", "192.192.192.192"); +}); + +// TRR-first check that DNS result is used if domain is part of the excluded-domains pref +add_task(async function test24b() { + dns.clearCache(true); + Services.prefs.setCharPref("network.trr.excluded-domains", "bar.example.com"); + await new DNSListener("bar.example.com", "127.0.0.1"); +}); + +// TRR-first check that DNS result is used if domain is part of the excluded-domains pref +add_task(async function test24c() { + dns.clearCache(true); + Services.prefs.setCharPref("network.trr.excluded-domains", "example.com"); + await new DNSListener("bar.example.com", "127.0.0.1"); +}); + +// TRR-first check that DNS result is used if domain is part of the excluded-domains pref that contains things before it. +add_task(async function test24d() { + dns.clearCache(true); + Services.prefs.setCharPref( + "network.trr.excluded-domains", + "foo.test.com, bar.example.com" + ); + await new DNSListener("bar.example.com", "127.0.0.1"); +}); + +// TRR-first check that DNS result is used if domain is part of the excluded-domains pref that contains things after it. +add_task(async function test24e() { + dns.clearCache(true); + Services.prefs.setCharPref( + "network.trr.excluded-domains", + "bar.example.com, foo.test.com" + ); + await new DNSListener("bar.example.com", "127.0.0.1"); +}); + +function observerPromise(topic) { + return new Promise(resolve => { + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + dump(`observe: ${aSubject}, ${aTopic}, ${aData} \n`); + if (aTopic == topic) { + Services.obs.removeObserver(observer, topic); + resolve(aData); + } + }, + }; + Services.obs.addObserver(observer, topic); + }); +} + +// TRR-first check that captivedetect.canonicalURL is resolved via native DNS +add_task(async function test24f() { + dns.clearCache(true); + + const cpServer = new HttpServer(); + cpServer.registerPathHandler("/cp", function handleRawData( + request, + response + ) { + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Cache-Control", "no-cache", false); + response.bodyOutputStream.write("data", 4); + }); + cpServer.start(-1); + cpServer.identity.setPrimary( + "http", + "detectportal.firefox.com", + cpServer.identity.primaryPort + ); + let cpPromise = observerPromise("captive-portal-login"); + + Services.prefs.setCharPref( + "captivedetect.canonicalURL", + `http://detectportal.firefox.com:${cpServer.identity.primaryPort}/cp` + ); + Services.prefs.setBoolPref("network.captive-portal-service.testMode", true); + Services.prefs.setBoolPref("network.captive-portal-service.enabled", true); + + // The captive portal has to have used native DNS, otherwise creating + // a socket to a non-local IP would trigger a crash. + await cpPromise; + // Simply resolving the captive portal domain should still use TRR + await new DNSListener("detectportal.firefox.com", "192.192.192.192"); + + Services.prefs.clearUserPref("network.captive-portal-service.enabled"); + Services.prefs.clearUserPref("network.captive-portal-service.testMode"); + Services.prefs.clearUserPref("captivedetect.canonicalURL"); + + await new Promise(resolve => cpServer.stop(resolve)); +}); + +// TRR-first check that a domain is resolved via native DNS when parental control is enabled. +add_task(async function test24g() { + dns.clearCache(true); + await SetParentalControlEnabled(true); + await new DNSListener("www.example.com", "127.0.0.1"); + await SetParentalControlEnabled(false); +}); + +// TRR-first check that DNS result is used if domain is part of the builtin-excluded-domains pref +add_task(async function test24h() { + dns.clearCache(true); + Services.prefs.setCharPref("network.trr.excluded-domains", ""); + Services.prefs.setCharPref( + "network.trr.builtin-excluded-domains", + "bar.example.com" + ); + await new DNSListener("bar.example.com", "127.0.0.1"); +}); + +// TRR-first check that DNS result is used if domain is part of the builtin-excluded-domains pref +add_task(async function test24i() { + dns.clearCache(true); + Services.prefs.setCharPref( + "network.trr.builtin-excluded-domains", + "example.com" + ); + await new DNSListener("bar.example.com", "127.0.0.1"); +}); + +// TRR-first check that DNS result is used if domain is part of the builtin-excluded-domains pref that contains things before it. +add_task(async function test24j() { + dns.clearCache(true); + Services.prefs.setCharPref( + "network.trr.builtin-excluded-domains", + "foo.test.com, bar.example.com" + ); + await new DNSListener("bar.example.com", "127.0.0.1"); +}); + +// TRR-first check that DNS result is used if domain is part of the builtin-excluded-domains pref that contains things after it. +add_task(async function test24k() { + dns.clearCache(true); + Services.prefs.setCharPref( + "network.trr.builtin-excluded-domains", + "bar.example.com, foo.test.com" + ); + await new DNSListener("bar.example.com", "127.0.0.1"); +}); + +// TRR-only that resolving excluded with TRR-only mode will use the remote +// resolver if it's not in the excluded domains +add_task(async function test25() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref("network.trr.excluded-domains", ""); + Services.prefs.setCharPref("network.trr.builtin-excluded-domains", ""); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192` + ); + + await new DNSListener("excluded", "192.192.192.192", true); +}); + +// TRR-only check that excluded goes directly to native lookup when in the excluded-domains +add_task(async function test25b() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref("network.trr.excluded-domains", "excluded"); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192` + ); + + await new DNSListener("excluded", "127.0.0.1"); +}); + +// TRR-only check that test.local is resolved via native DNS +add_task(async function test25c() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref("network.trr.excluded-domains", "excluded,local"); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192` + ); + + await new DNSListener("test.local", "127.0.0.1"); +}); + +// TRR-only check that .other is resolved via native DNS when the pref is set +add_task(async function test25d() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.excluded-domains", + "excluded,local,other" + ); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192` + ); + + await new DNSListener("domain.other", "127.0.0.1"); +}); + +// TRR-only check that captivedetect.canonicalURL is resolved via native DNS +add_task({ skip_if: () => true }, async function test25e() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192` + ); + + const cpServer = new HttpServer(); + cpServer.registerPathHandler("/cp", function handleRawData( + request, + response + ) { + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Cache-Control", "no-cache", false); + response.bodyOutputStream.write("data", 4); + }); + cpServer.start(-1); + cpServer.identity.setPrimary( + "http", + "detectportal.firefox.com", + cpServer.identity.primaryPort + ); + let cpPromise = observerPromise("captive-portal-login"); + + Services.prefs.setCharPref( + "captivedetect.canonicalURL", + `http://detectportal.firefox.com:${cpServer.identity.primaryPort}/cp` + ); + Services.prefs.setBoolPref("network.captive-portal-service.testMode", true); + Services.prefs.setBoolPref("network.captive-portal-service.enabled", true); + + // The captive portal has to have used native DNS, otherwise creating + // a socket to a non-local IP would trigger a crash. + await cpPromise; + // // Simply resolving the captive portal domain should still use TRR + await new DNSListener("detectportal.firefox.com", "192.192.192.192"); + + Services.prefs.clearUserPref("network.captive-portal-service.enabled"); + Services.prefs.clearUserPref("network.captive-portal-service.testMode"); + Services.prefs.clearUserPref("captivedetect.canonicalURL"); + + await new Promise(resolve => cpServer.stop(resolve)); +}); + +// TRR-only check that a domain is resolved via native DNS when parental control is enabled. +add_task(async function test25f() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + await SetParentalControlEnabled(true); + await new DNSListener("www.example.com", "127.0.0.1"); + await SetParentalControlEnabled(false); +}); + +// TRR-only check that excluded goes directly to native lookup when in the builtin-excluded-domains +add_task(async function test25g() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref("network.trr.excluded-domains", ""); + Services.prefs.setCharPref( + "network.trr.builtin-excluded-domains", + "excluded" + ); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192` + ); + + await new DNSListener("excluded", "127.0.0.1"); +}); + +// TRR-only check that test.local is resolved via native DNS +add_task(async function test25h() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.builtin-excluded-domains", + "excluded,local" + ); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192` + ); + + await new DNSListener("test.local", "127.0.0.1"); +}); + +// TRR-only check that .other is resolved via native DNS when the pref is set +add_task(async function test25i() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.builtin-excluded-domains", + "excluded,local,other" + ); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=192.192.192.192` + ); + + await new DNSListener("domain.other", "127.0.0.1"); +}); + +// Check that none of the requests have set any cookies. +add_task(async function count_cookies() { + let cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager); + Assert.equal(cm.countCookiesFromHost("example.com"), 0); + Assert.equal(cm.countCookiesFromHost("foo.example.com."), 0); +}); + +add_task(async function test_connection_closed() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref("network.trr.excluded-domains", ""); + // We don't need to wait for 30 seconds for the request to fail + Services.prefs.setIntPref("network.trr.request_timeout_mode_trronly_ms", 500); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2` + ); + // bootstrap + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1"); + + await new DNSListener("bar.example.com", "2.2.2.2"); + + // makes the TRR connection shut down. + let [, , inStatus] = await new DNSListener("closeme.com", undefined, false); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + await new DNSListener("bar2.example.com", "2.2.2.2"); +}); + +add_task(async function test_connection_closed_no_bootstrap() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref("network.trr.excluded-domains", "excluded,local"); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3` + ); + Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + Services.prefs.clearUserPref("network.trr.bootstrapAddress"); + + await new DNSListener("bar.example.com", "3.3.3.3"); + + // makes the TRR connection shut down. + let [, , inStatus] = await new DNSListener("closeme.com", undefined, false); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + await new DNSListener("bar2.example.com", "3.3.3.3"); +}); + +add_task(async function test_connection_closed_no_bootstrap_localhost() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref("network.trr.excluded-domains", "excluded"); + Services.prefs.setCharPref( + "network.trr.uri", + `https://localhost:${h2Port}/doh?responseIP=3.3.3.3` + ); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.trr.bootstrapAddress"); + + await new DNSListener("bar.example.com", "3.3.3.3"); + + // makes the TRR connection shut down. + let [, , inStatus] = await new DNSListener("closeme.com", undefined, false); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + await new DNSListener("bar2.example.com", "3.3.3.3"); +}); + +add_task(async function test_connection_closed_no_bootstrap_no_excluded() { + // This test makes sure that even in mode 3 without a bootstrap address + // we are able to restart the TRR connection if it drops - the TRR service + // channel will use regular DNS to resolve the TRR address. + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref("network.trr.excluded-domains", ""); + Services.prefs.setCharPref("network.trr.builtin-excluded-domains", ""); + Services.prefs.setCharPref( + "network.trr.uri", + `https://localhost:${h2Port}/doh?responseIP=3.3.3.3` + ); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.trr.bootstrapAddress"); + + await new DNSListener("bar.example.com", "3.3.3.3"); + + // makes the TRR connection shut down. + let [, , inStatus] = await new DNSListener("closeme.com", undefined, false); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + dns.clearCache(true); + await new DNSListener("bar2.example.com", "3.3.3.3"); +}); + +add_task(async function test_connection_closed_trr_first() { + // This test exists to document what happens when we're in TRR only mode + // and we don't set a bootstrap address. We use DNS to resolve the + // initial URI, but if the connection fails, we don't fallback to DNS + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first + Services.prefs.setCharPref( + "network.trr.uri", + `https://localhost:${h2Port}/doh?responseIP=9.9.9.9` + ); + Services.prefs.setCharPref("network.dns.localDomains", "closeme.com"); + Services.prefs.clearUserPref("network.trr.bootstrapAddress"); + + await new DNSListener("bar.example.com", "9.9.9.9"); + + // makes the TRR connection shut down. Should fallback to DNS + await new DNSListener("closeme.com", "127.0.0.1"); + // TRR should be back up again + await new DNSListener("bar2.example.com", "9.9.9.9"); +}); + +add_task(async function test_clearCacheOnURIChange() { + dns.clearCache(true); + Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", true); + Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first + Services.prefs.setCharPref( + "network.trr.uri", + `https://localhost:${h2Port}/doh?responseIP=7.7.7.7` + ); + + await new DNSListener("bar.example.com", "7.7.7.7"); + + // The TRR cache should be cleared by this pref change. + Services.prefs.setCharPref( + "network.trr.uri", + `https://localhost:${h2Port}/doh?responseIP=8.8.8.8` + ); + + await new DNSListener("bar.example.com", "8.8.8.8"); + Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false); +}); + +add_task(async function test_dnsSuffix() { + async function checkDnsSuffixInMode(mode) { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", mode); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=1.2.3.4&push=true` + ); + await new DNSListener("example.org", "1.2.3.4"); + await new DNSListener("push.example.org", "2018::2018"); + await new DNSListener("test.com", "1.2.3.4"); + + let networkLinkService = { + dnsSuffixList: ["example.org"], + QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]), + }; + Services.obs.notifyObservers( + networkLinkService, + "network:dns-suffix-list-updated" + ); + await new DNSListener("test.com", "1.2.3.4"); + if (Services.prefs.getBoolPref("network.trr.split_horizon_mitigations")) { + await new DNSListener("example.org", "127.0.0.1"); + // Also test that we don't use the pushed entry. + await new DNSListener("push.example.org", "127.0.0.1"); + } else { + await new DNSListener("example.org", "1.2.3.4"); + await new DNSListener("push.example.org", "2018::2018"); + } + + // Attempt to clean up, just in case + networkLinkService.dnsSuffixList = []; + Services.obs.notifyObservers( + networkLinkService, + "network:dns-suffix-list-updated" + ); + } + + Services.prefs.setBoolPref("network.trr.split_horizon_mitigations", true); + await checkDnsSuffixInMode(2); + Services.prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1"); + await checkDnsSuffixInMode(3); + Services.prefs.setBoolPref("network.trr.split_horizon_mitigations", false); + // Test again with mitigations off + await checkDnsSuffixInMode(2); + await checkDnsSuffixInMode(3); + Services.prefs.clearUserPref("network.trr.split_horizon_mitigations"); + Services.prefs.clearUserPref("network.trr.bootstrapAddress"); +}); + +// Test AsyncResoleWithTrrServer. +add_task(async function test_async_resolve_with_trr_server_1() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 0); // TRR-disabled + + await new DNSListener( + "bar_with_trr1.example.com", + "2.2.2.2", + true, + undefined, + `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2` + ); + + // Test request without trr server, it should return a native dns response. + await new DNSListener("bar_with_trr1.example.com", "127.0.0.1"); +}); + +// Test AsyncResoleWithTrrServer. +add_task(async function test_async_resolve_with_trr_server_2() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2` + ); + + await new DNSListener( + "bar_with_trr2.example.com", + "3.3.3.3", + true, + undefined, + `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3` + ); + + // Test request without trr server, it should return a response from trr server defined in the pref. + await new DNSListener("bar_with_trr2.example.com", "2.2.2.2"); +}); + +// Test AsyncResoleWithTrrServer. +add_task(async function test_async_resolve_with_trr_server_3() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2` + ); + + await new DNSListener( + "bar_with_trr3.example.com", + "3.3.3.3", + true, + undefined, + `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3` + ); + + // Test request without trr server, it should return a response from trr server defined in the pref. + await new DNSListener("bar_with_trr3.example.com", "2.2.2.2"); +}); + +// Test AsyncResoleWithTrrServer. +add_task(async function test_async_resolve_with_trr_server_5() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 5); // TRR-user-disabled + + // When dns is resolved in socket process, we can't set |expectEarlyFail| to true. + let inSocketProcess = mozinfo.socketprocess_networking; + await new DNSListener( + "bar_with_trr3.example.com", + undefined, + false, + undefined, + `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3`, + !inSocketProcess + ); + + // Call normal AsyncOpen, it will return result from the native resolver. + await new DNSListener("bar_with_trr3.example.com", "127.0.0.1"); +}); + +// Test AsyncResoleWithTrrServer. +add_task(async function test_async_resolve_with_trr_server_different_cache() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2` + ); + + await new DNSListener("bar_with_trr4.example.com", "2.2.2.2", true); + + // The record will be fetch again. + await new DNSListener( + "bar_with_trr4.example.com", + "3.3.3.3", + true, + undefined, + `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3` + ); +}); + +// Test AsyncResoleWithTrrServer. +add_task(async function test_async_resolve_with_trr_server_different_servers() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2` + ); + + await new DNSListener("bar_with_trr5.example.com", "2.2.2.2", true); + + // The record will be fetch again. + await new DNSListener( + "bar_with_trr5.example.com", + "3.3.3.3", + true, + undefined, + `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3` + ); + + // The record will be fetch again. + await new DNSListener( + "bar_with_trr5.example.com", + "4.4.4.4", + true, + undefined, + `https://foo.example.com:${h2Port}/doh?responseIP=4.4.4.4` + ); +}); + +// Test AsyncResoleWithTrrServer. +// AsyncResoleWithTrrServer will failed. +// There will be no fallback to native and the host will not be blacklisted. +add_task(async function test_async_resolve_with_trr_server_no_blacklist() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2` + ); + + let [, , inStatus] = await new DNSListener( + "bar_with_trr6.example.com", + undefined, + false, + undefined, + `https://foo.example.com:${h2Port}/404` + ); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + + await new DNSListener("bar_with_trr6.example.com", "2.2.2.2", true); +}); + +// verify that DOH push does not work with AsyncResoleWithTrrServer. +add_task(async function test_async_resolve_with_trr_server_no_push() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2` + ); + + await new DNSListener( + "bar_with_trr7.example.com", + "3.3.3.3", + true, + undefined, + `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3&push=true` + ); +}); + +add_task(async function test_async_resolve_with_trr_server_no_push_part_2() { + // AsyncResoleWithTrrServer rejects server pushes and the entry for push.example.org + // shouldn't be neither in the default cache not in AsyncResoleWithTrrServer cache. + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/404` + ); + dump( + "test_async_resolve_with_trr_server_no_push_part_2 - resolve push.example.org will not be in the cache.\n" + ); + + await new DNSListener( + "push.example.org", + "3.3.3.3", + true, + undefined, + `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3&push=true` + ); + + await new DNSListener("push.example.org", "127.0.0.1"); +}); + +// Verify that AsyncResoleWithTrrServer is not block on confirmationNS of the defaut serveer. +add_task(async function test_async_resolve_with_trr_server_confirmation_ns() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 2); // TRR-only + Services.prefs.clearUserPref("network.trr.useGET"); + Services.prefs.clearUserPref("network.trr.disable-ECS"); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=1::ffff` + ); + Services.prefs.setCharPref( + "network.trr.confirmationNS", + "confirm.example.com" + ); + + // AsyncResoleWithTrrServer will succeed + await new DNSListener( + "bar_with_trr8.example.com", + "3.3.3.3", + true, + undefined, + `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3` + ); + + Services.prefs.setCharPref("network.trr.confirmationNS", "skip"); +}); + +// verify TRR timings +add_task(async function test_fetch_time() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2&delayIPv4=20` + ); + + await new DNSListener("bar_time.example.com", "2.2.2.2", true, 20); +}); + +// gets an error from DoH. It will fall back to regular DNS. The TRR timing should be 0. +add_task(async function test_no_fetch_time_on_trr_error() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/404&delayIPv4=20` + ); + + await new DNSListener("bar_time1.example.com", "127.0.0.1", true, 0); +}); + +// check an excluded domain. It should fall back to regular DNS. The TRR timing should be 0. +add_task(async function test_no_fetch_time_for_excluded_domains() { + dns.clearCache(true); + Services.prefs.setCharPref( + "network.trr.excluded-domains", + "bar_time2.example.com" + ); + Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2&delayIPv4=20` + ); + + await new DNSListener("bar_time2.example.com", "127.0.0.1", true, 0); + + Services.prefs.setCharPref("network.trr.excluded-domains", ""); +}); + +// verify RFC1918 address from the server is rejected and the TRR timing will be not set because the response will be from the native resolver. +add_task(async function test_no_fetch_time_for_rfc1918_not_allowed() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=192.168.0.1&delayIPv4=20` + ); + await new DNSListener("rfc1918_time.example.com", "127.0.0.1", true, 0); +}); + +add_task(async function test_content_encoding_gzip() { + dns.clearCache(true); + Services.prefs.setBoolPref( + "network.trr.send_empty_accept-encoding_headers", + false + ); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2` + ); + + await new DNSListener("bar.example.com", "2.2.2.2"); +}); + +add_task(async function test_redirect_get() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); // TRR-only + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?redirect=4.4.4.4{&dns}` + ); + Services.prefs.clearUserPref("network.trr.allow-rfc1918"); + Services.prefs.setBoolPref("network.trr.useGET", true); + Services.prefs.setBoolPref("network.trr.disable-ECS", true); + await new DNSListener("ecs.example.com", "4.4.4.4"); +}); + +// test redirect +add_task(async function test_redirect_post() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setBoolPref("network.trr.useGET", false); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?redirect=4.4.4.4` + ); + + await new DNSListener("bar.example.com", "4.4.4.4"); +}); + +// confirmationNS set without confirmed NS yet +// checks that we properly fall back to DNS is confirmation is not ready yet +add_task(async function test_resolve_not_confirmed() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=7.7.7.7` + ); + Services.prefs.setCharPref( + "network.trr.confirmationNS", + "confirm.example.com" + ); + + await new DNSListener("example.org", "127.0.0.1"); + + // Check that the confirmation eventually completes. + let count = 100; + while (count > 0) { + if (count == 50 || count == 10) { + // At these two points we do a longer timeout to account for a slow + // response on the server side. This is usually a problem on the Android + // because of the increased delay between the emulator and host. + await new Promise(resolve => do_timeout(100 * (100 / count), resolve)); + } + let [, inRecord] = await new DNSListener( + `ip${count}.example.org`, + undefined, + false + ); + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + let responseIP = inRecord.getNextAddrAsString(); + if (responseIP == "7.7.7.7") { + break; + } + count--; + } + + Assert.greater(count, 0, "Finished confirmation before 100 iterations"); + + Services.prefs.setCharPref("network.trr.confirmationNS", "skip"); +}); + +// Tests that we handle FQDN encoding and decoding properly +add_task(async function test_fqdn() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=9.8.7.6` + ); + await new DNSListener("fqdn.example.org.", "9.8.7.6"); +}); + +add_task(async function test_fqdn_get() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setBoolPref("network.trr.useGET", true); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=9.8.7.6` + ); + await new DNSListener("fqdn_get.example.org.", "9.8.7.6"); + + Services.prefs.setBoolPref("network.trr.useGET", false); +}); + +add_task(async function test_detected_uri() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.clearUserPref("network.trr.uri"); + let defaultURI = gDefaultPref.getCharPref("network.trr.uri"); + gDefaultPref.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=3.4.5.6` + ); + await new DNSListener("domainA.example.org.", "3.4.5.6"); + dns.setDetectedTrrURI( + `https://foo.example.com:${h2Port}/doh?responseIP=1.2.3.4` + ); + await new DNSListener("domainB.example.org.", "1.2.3.4"); + gDefaultPref.setCharPref("network.trr.uri", defaultURI); +}); + +add_task(async function test_detected_uri_userSet() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=4.5.6.7` + ); + await new DNSListener("domainA.example.org.", "4.5.6.7"); + // This should be a no-op, since we have a user-set URI + dns.setDetectedTrrURI( + `https://foo.example.com:${h2Port}/doh?responseIP=1.2.3.4` + ); + await new DNSListener("domainB.example.org.", "4.5.6.7"); +}); + +add_task(async function test_detected_uri_link_change() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.clearUserPref("network.trr.uri"); + let defaultURI = gDefaultPref.getCharPref("network.trr.uri"); + gDefaultPref.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=3.4.5.6` + ); + await new DNSListener("domainA.example.org.", "3.4.5.6"); + dns.setDetectedTrrURI( + `https://foo.example.com:${h2Port}/doh?responseIP=1.2.3.4` + ); + await new DNSListener("domainB.example.org.", "1.2.3.4"); + + let networkLinkService = { + platformDNSIndications: 0, + QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]), + }; + + Services.obs.notifyObservers( + networkLinkService, + "network:link-status-changed", + "changed" + ); + + await new DNSListener("domainC.example.org.", "3.4.5.6"); + + gDefaultPref.setCharPref("network.trr.uri", defaultURI); +}); + +add_task(async function test_pref_changes() { + Services.prefs.clearUserPref("network.trr.uri"); + let defaultURI = gDefaultPref.getCharPref("network.trr.uri"); + + async function doThenCheckURI(closure, expectedURI, expectChange = false) { + let uriChanged; + if (expectChange) { + uriChanged = observerPromise("network:trr-uri-changed"); + } + closure(); + if (expectChange) { + await uriChanged; + } + equal(dns.currentTrrURI, expectedURI); + } + + // setting the default value of the pref should be reflected in the URI + await doThenCheckURI(() => { + gDefaultPref.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?default` + ); + }, `https://foo.example.com:${h2Port}/doh?default`); + + // the user set value should be reflected in the URI + await doThenCheckURI(() => { + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?user` + ); + }, `https://foo.example.com:${h2Port}/doh?user`); + + // A user set pref is selected, so it should be chosen instead of the rollout + await doThenCheckURI( + () => { + Services.prefs.setCharPref( + "doh-rollout.uri", + `https://foo.example.com:${h2Port}/doh?rollout` + ); + }, + `https://foo.example.com:${h2Port}/doh?user`, + false + ); + + // There is no user set pref, so we go to the rollout pref + await doThenCheckURI(() => { + Services.prefs.clearUserPref("network.trr.uri"); + }, `https://foo.example.com:${h2Port}/doh?rollout`); + + // When the URI is set by the rollout addon, detection is allowed + await doThenCheckURI(() => { + dns.setDetectedTrrURI(`https://foo.example.com:${h2Port}/doh?detected`); + }, `https://foo.example.com:${h2Port}/doh?detected`); + + // Should switch back to the default provided by the rollout addon + await doThenCheckURI(() => { + let networkLinkService = { + platformDNSIndications: 0, + QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]), + }; + Services.obs.notifyObservers( + networkLinkService, + "network:link-status-changed", + "changed" + ); + }, `https://foo.example.com:${h2Port}/doh?rollout`); + + // Again the user set pref should be chosen + await doThenCheckURI(() => { + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?user` + ); + }, `https://foo.example.com:${h2Port}/doh?user`); + + // Detection should not work with a user set pref + await doThenCheckURI( + () => { + dns.setDetectedTrrURI(`https://foo.example.com:${h2Port}/doh?detected`); + }, + `https://foo.example.com:${h2Port}/doh?user`, + false + ); + + // Should stay the same on network changes + await doThenCheckURI( + () => { + let networkLinkService = { + platformDNSIndications: 0, + QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]), + }; + Services.obs.notifyObservers( + networkLinkService, + "network:link-status-changed", + "changed" + ); + }, + `https://foo.example.com:${h2Port}/doh?user`, + false + ); + + // Restore the pref + gDefaultPref.setCharPref("network.trr.uri", defaultURI); +}); + +add_task(async function test_async_resolve_with_trr_server_bad_port() { + dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2` + ); + + let [, , inStatus] = await new DNSListener( + "only_once.example.com", + undefined, + false, + undefined, + `https://target.example.com:666/404` + ); + Assert.ok( + !Components.isSuccessCode(inStatus), + `${inStatus} should be an error code` + ); + + // // MOZ_LOG=sync,timestamp,nsHostResolver:5 We should not keep resolving only_once.example.com + // // TODO: find a way of automating this + // await new Promise(resolve => {}); +}); + +add_task(async function test_dohrollout_mode() { + Services.prefs.clearUserPref("network.trr.mode"); + Services.prefs.clearUserPref("doh-rollout.mode"); + + equal(dns.currentTrrMode, 0); + + async function doThenCheckMode(trrMode, rolloutMode, expectedMode, message) { + let modeChanged; + if (dns.currentTrrMode != expectedMode) { + modeChanged = observerPromise("network:trr-mode-changed"); + } + + if (trrMode != undefined) { + Services.prefs.setIntPref("network.trr.mode", trrMode); + } + + if (rolloutMode != undefined) { + Services.prefs.setIntPref("doh-rollout.mode", rolloutMode); + } + + if (modeChanged) { + await modeChanged; + } + equal(dns.currentTrrMode, expectedMode, message); + } + + await doThenCheckMode(2, undefined, 2); + await doThenCheckMode(3, undefined, 3); + await doThenCheckMode(5, undefined, 5); + await doThenCheckMode(2, undefined, 2); + await doThenCheckMode(0, undefined, 0); + await doThenCheckMode(1, undefined, 5); + await doThenCheckMode(6, undefined, 5); + + await doThenCheckMode(2, 0, 2); + await doThenCheckMode(2, 1, 2); + await doThenCheckMode(2, 2, 2); + await doThenCheckMode(2, 3, 2); + await doThenCheckMode(2, 5, 2); + await doThenCheckMode(3, 2, 3); + await doThenCheckMode(5, 2, 5); + + Services.prefs.clearUserPref("network.trr.mode"); + Services.prefs.clearUserPref("doh-rollout.mode"); + + await doThenCheckMode(undefined, 2, 2); + await doThenCheckMode(undefined, 3, 3); + + // All modes that are not 0,2,3 are treated as 5 + await doThenCheckMode(undefined, 5, 5); + await doThenCheckMode(undefined, 4, 5); + await doThenCheckMode(undefined, 6, 5); + + await doThenCheckMode(undefined, 2, 2); + await doThenCheckMode(3, undefined, 3); + + Services.prefs.clearUserPref("network.trr.mode"); + equal(dns.currentTrrMode, 2); + Services.prefs.clearUserPref("doh-rollout.mode"); + equal(dns.currentTrrMode, 0); +}); + +add_task(async function test_ipv6_trr_fallback() { + dns.clearCache(true); + let httpserver = new HttpServer(); + httpserver.registerPathHandler("/content", (metadata, response) => { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-Control", "no-cache"); + + const responseBody = "anybody"; + response.bodyOutputStream.write(responseBody, responseBody.length); + }); + httpserver.start(-1); + + Services.prefs.setBoolPref("network.captive-portal-service.testMode", true); + let url = `http://127.0.0.1:666/doom_port_should_not_be_open`; + Services.prefs.setCharPref("network.connectivity-service.IPv6.url", url); + let ncs = Cc[ + "@mozilla.org/network/network-connectivity-service;1" + ].getService(Ci.nsINetworkConnectivityService); + + 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); + }); + } + + let checks = promiseObserverNotification( + "network:connectivity-service:ip-checks-complete" + ); + + ncs.recheckIPConnectivity(); + + await checks; + equal( + ncs.IPv6, + Ci.nsINetworkConnectivityService.NOT_AVAILABLE, + "Check IPv6 support (expect NOT_AVAILABLE)" + ); + + Services.prefs.setIntPref("network.trr.mode", 2); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=4.4.4.4` + ); + const override = Cc["@mozilla.org/network/native-dns-override;1"].getService( + Ci.nsINativeDNSResolverOverride + ); + gOverride.addIPOverride("ipv6.host.com", "1:1::2"); + + await new DNSListener( + "ipv6.host.com", + "1:1::2", + true, + 0, + "", + false, + Ci.nsIDNSService.RESOLVE_DISABLE_IPV4 + ); + + Services.prefs.clearUserPref("network.captive-portal-service.testMode"); + Services.prefs.clearUserPref("network.connectivity-service.IPv6.url"); + + override.clearOverrides(); + await httpserver.stop(); +}); + +add_task(async function test_no_retry_without_doh() { + // See bug 1648147 - if the TRR returns 0.0.0.0 we should not retry with DNS + Services.prefs.setBoolPref("network.trr.fallback-on-zero-response", false); + + async function test(url, ip) { + Services.prefs.setIntPref("network.trr.mode", 2); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=${ip}` + ); + + // Requests to 0.0.0.0 are usually directed to localhost, so let's use a port + // we know isn't being used - 666 (Doom) + let chan = makeChan(url, Ci.nsIRequest.TRR_DEFAULT_MODE); + let statusCounter = { + statusCount: {}, + QueryInterface: ChromeUtils.generateQI([ + "nsIInterfaceRequestor", + "nsIProgressEventSink", + ]), + getInterface(iid) { + return this.QueryInterface(iid); + }, + onProgress(request, progress, progressMax) {}, + onStatus(request, status, statusArg) { + this.statusCount[status] = 1 + (this.statusCount[status] || 0); + }, + }; + chan.notificationCallbacks = statusCounter; + await new Promise(resolve => + chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE)) + ); + equal( + statusCounter.statusCount[0x804b000b], + 1, + "Expecting only one instance of NS_NET_STATUS_RESOLVED_HOST" + ); + equal( + statusCounter.statusCount[0x804b0007], + 1, + "Expecting only one instance of NS_NET_STATUS_CONNECTING_TO" + ); + } + + await test(`http://unknown.ipv4.stuff:666/path`, "0.0.0.0"); + await test(`http://unknown.ipv6.stuff:666/path`, "::"); +}); + +// This test checks that normally when the TRR mode goes from ON -> OFF +// we purge the DNS cache (including TRR), so the entries aren't used on +// networks where they shouldn't. For example - turning on a VPN. +add_task(async function test_purge_trr_cache_on_mode_change() { + Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", true); + + Services.prefs.setIntPref("network.trr.mode", 0); + Services.prefs.setIntPref("doh-rollout.mode", 2); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3` + ); + + await new DNSListener("cached.example.com", "3.3.3.3"); + Services.prefs.clearUserPref("doh-rollout.mode"); + + await new DNSListener("cached.example.com", "127.0.0.1"); + + Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false); +}); |