/* This Source Code Form is subject to the terms of the Mozilla Public * 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/. */ "use strict"; var { setTimeout } = ChromeUtils.importESModule( "resource://gre/modules/Timer.sys.mjs" ); let h2Port; let h3Port; let trrServer; const { TestUtils } = ChromeUtils.importESModule( "resource://testing-common/TestUtils.sys.mjs" ); const certOverrideService = Cc[ "@mozilla.org/security/certoverride;1" ].getService(Ci.nsICertOverrideService); add_setup(async function setup() { h2Port = Services.env.get("MOZHTTP2_PORT"); Assert.notEqual(h2Port, null); Assert.notEqual(h2Port, ""); h3Port = Services.env.get("MOZHTTP3_PORT"); Assert.notEqual(h3Port, null); Assert.notEqual(h3Port, ""); trr_test_setup(); if (mozinfo.socketprocess_networking) { Services.dns; // Needed to trigger socket process. await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched); } Services.prefs.setIntPref("network.trr.mode", 2); // TRR first Services.prefs.setBoolPref("network.http.http3.enable", true); Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6); Services.prefs.setBoolPref( "network.http.http3.block_loopback_ipv6_addr", true ); registerCleanupFunction(async () => { trr_clear_prefs(); Services.prefs.clearUserPref("network.http.http3.block_loopback_ipv6_addr"); if (trrServer) { await trrServer.stop(); } }); }); function makeChan(url) { let chan = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true, contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, }).QueryInterface(Ci.nsIHttpChannel); chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; return chan; } function channelOpenPromise(chan, flags) { // eslint-disable-next-line no-async-promise-executor return new Promise(async resolve => { function finish(req, buffer) { resolve([req, buffer]); certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( false ); } let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal); internal.setWaitForHTTPSSVCRecord(); certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( true ); chan.asyncOpen(new ChannelListener(finish, null, flags)); }); } async function registerDoHAnswers(host, ipv4Answers, ipv6Answers, httpsRecord) { trrServer = new TRRServer(); await trrServer.start(); Services.prefs.setIntPref("network.trr.mode", 3); Services.prefs.setCharPref( "network.trr.uri", `https://foo.example.com:${trrServer.port}/dns-query` ); await trrServer.registerDoHAnswers(host, "HTTPS", { answers: httpsRecord, }); await trrServer.registerDoHAnswers(host, "AAAA", { answers: ipv6Answers, }); await trrServer.registerDoHAnswers(host, "A", { answers: ipv4Answers, }); Services.dns.clearCache(true); } // Test if we retry IPv4 address for Http/3 properly. add_task(async function test_retry_with_ipv4() { let host = "test.http3_retry.com"; let ipv4answers = [ { name: host, ttl: 55, type: "A", flush: false, data: "127.0.0.1", }, ]; // The UDP socket will return connection refused error because we set // "network.http.http3.block_loopback_ipv6_addr" to true. let ipv6answers = [ { name: host, ttl: 55, type: "AAAA", flush: false, data: "::1", }, ]; let httpsRecord = [ { name: host, ttl: 55, type: "HTTPS", flush: false, data: { priority: 1, name: host, values: [ { key: "alpn", value: "h3-29" }, { key: "port", value: h3Port }, ], }, }, ]; await registerDoHAnswers(host, ipv4answers, ipv6answers, httpsRecord); let chan = makeChan(`https://${host}`); let [req] = await channelOpenPromise(chan); Assert.equal(req.protocolVersion, "h3-29"); await trrServer.stop(); }); add_task(async function test_retry_with_ipv4_disabled() { let host = "test.http3_retry_ipv4_blocked.com"; let ipv4answers = [ { name: host, ttl: 55, type: "A", flush: false, data: "127.0.0.1", }, ]; // The UDP socket will return connection refused error because we set // "network.http.http3.block_loopback_ipv6_addr" to true. let ipv6answers = [ { name: host, ttl: 55, type: "AAAA", flush: false, data: "::1", }, ]; let httpsRecord = [ { name: host, ttl: 55, type: "HTTPS", flush: false, data: { priority: 1, name: host, values: [ { key: "alpn", value: "h3-29" }, { key: "port", value: h3Port }, ], }, }, ]; await registerDoHAnswers(host, ipv4answers, ipv6answers, httpsRecord); let chan = makeChan(`https://${host}`); chan.QueryInterface(Ci.nsIHttpChannelInternal); chan.setIPv4Disabled(); await channelOpenPromise(chan, CL_EXPECT_FAILURE); await trrServer.stop(); }); // See bug 1837252. There is no way to observe the outcome of this test, because // the crash in bug 1837252 is only triggered by speculative connection. // The outcome of this test is no crash. add_task(async function test_retry_with_ipv4_failed() { let host = "test.http3_retry_failed.com"; // Return a wrong answer intentionally. let ipv4answers = [ { name: host, ttl: 55, type: "AAAA", flush: false, data: "127.0.0.1", }, ]; // The UDP socket will return connection refused error because we set // "network.http.http3.block_loopback_ipv6_addr" to true. let ipv6answers = [ { name: host, ttl: 55, type: "AAAA", flush: false, data: "::1", }, ]; let httpsRecord = [ { name: host, ttl: 55, type: "HTTPS", flush: false, data: { priority: 1, name: host, values: [ { key: "alpn", value: "h3-29" }, { key: "port", value: h3Port }, ], }, }, ]; await registerDoHAnswers(host, ipv4answers, ipv6answers, httpsRecord); // This speculative connection is used to trigger the mechanism to retry // Http/3 connection with a IPv4 address. // We want to make the connection entry's IP preference known, // so DnsAndConnectSocket::mRetryWithDifferentIPFamily will be set to true // before the second speculative connection. let uri = Services.io.newURI(`https://test.http3_retry_failed.com`); Services.io.speculativeConnect( uri, Services.scriptSecurityManager.getSystemPrincipal(), null, false ); // eslint-disable-next-line mozilla/no-arbitrary-setTimeout await new Promise(resolve => setTimeout(resolve, 3000)); // When this speculative connection is created, the connection entry is // already set to prefer IPv4. Since we provided an invalid A response, // DnsAndConnectSocket::OnLookupComplete is called with an error. // Since DnsAndConnectSocket::mRetryWithDifferentIPFamily is true, we do // retry DNS lookup. During retry, we should not create UDP connection. Services.io.speculativeConnect( uri, Services.scriptSecurityManager.getSystemPrincipal(), null, false ); // eslint-disable-next-line mozilla/no-arbitrary-setTimeout await new Promise(resolve => setTimeout(resolve, 3000)); await trrServer.stop(); });