tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit adcd3fe4f6c53665fa32ef76e5e53224d49f9c75
parent de64d765182f326737b5f35f7d01c7fbe592b759
Author: Kershaw Chang <kershaw@mozilla.com>
Date:   Wed, 26 Nov 2025 09:27:02 +0000

Bug 2001618 - Retry once if H3 backup timer is active when a transaction closes, r=necko-reviewers,valentin

When an HTTP/3 backup timer is still active and the transaction closes with an error, we currently fail to retry properly. This patch maps the terminal error to NS_ERROR_NET_RESET to trigger a single retry in that state. This is a temporary workaround; the correct policy should be implemented in Happy Eyeballs.

Differential Revision: https://phabricator.services.mozilla.com/D273784

Diffstat:
Mnetwerk/protocol/http/nsHttpTransaction.cpp | 8++++++++
Anetwerk/test/unit/test_http3_proxy_ipv6_fallback.js | 200+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnetwerk/test/unit/xpcshell.toml | 7+++++++
3 files changed, 215 insertions(+), 0 deletions(-)

diff --git a/netwerk/protocol/http/nsHttpTransaction.cpp b/netwerk/protocol/http/nsHttpTransaction.cpp @@ -1333,6 +1333,14 @@ void nsHttpTransaction::Close(nsresult reason) { mDNSRequest = nullptr; } + // If an HTTP/3 backup timer is active and this transaction ends in error, + // treat it as NS_ERROR_NET_RESET so the transaction will retry once. + // NOTE: This is a temporary workaround; the proper fix belongs in + // the Happy Eyeballs project. + if (NS_FAILED(reason) && mHttp3BackupTimerCreated && mHttp3BackupTimer) { + reason = NS_ERROR_NET_RESET; + } + MaybeCancelFallbackTimer(); MOZ_ASSERT(OnSocketThread(), "not on socket thread"); diff --git a/netwerk/test/unit/test_http3_proxy_ipv6_fallback.js b/netwerk/test/unit/test_http3_proxy_ipv6_fallback.js @@ -0,0 +1,200 @@ +/* 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" +); + +const { Http3ProxyFilter, NodeHTTP2Server, NodeHTTP2ProxyServer } = + ChromeUtils.importESModule("resource://testing-common/NodeServer.sys.mjs"); + +let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); +let trrServer; +let proxyFilter; +let proxyPort; +let proxyHost; +let serverPort; + +add_setup(async function setup() { + trr_test_setup(); + + if (mozinfo.socketprocess_networking) { + Cc["@mozilla.org/network/protocol;1?name=http"].getService( + Ci.nsIHttpProtocolHandler + ); + Services.dns; // Needed to trigger socket process. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + Services.prefs.setBoolPref("network.http.http3.enable", true); + Services.prefs.setBoolPref( + "network.http.http3.block_loopback_ipv6_addr", + true + ); + Services.prefs.setBoolPref( + "network.http.http3.retry_different_ip_family", + false + ); + Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + Services.prefs.setBoolPref("network.dns.get-ttl", false); + + let proxy = new NodeHTTP2ProxyServer(); + await proxy.startWithoutProxyFilter(); + proxyPort = proxy.port(); + proxyHost = "alt2.example.com"; + + 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(proxyHost, "AAAA", { + answers: [ + { + name: proxyHost, + ttl: 55, + type: "AAAA", + flush: false, + data: "::1", + }, + ], + }); + await trrServer.registerDoHAnswers(proxyHost, "A", { + answers: [ + { + name: proxyHost, + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ], + }); + + await new TRRDNSListener(proxyHost, "::1"); + + let server = new NodeHTTP2Server(); + await server.start(); + serverPort = server.port(); + info(`server port:${server.port()}`); + + // Register multiple endpoints + await server.registerPathHandler("/concurrent1", (req, resp) => { + resp.writeHead(200); + resp.end("response1"); + }); + await server.registerPathHandler("/concurrent2", (req, resp) => { + resp.writeHead(200); + resp.end("response2"); + }); + await server.registerPathHandler("/concurrent3", (req, resp) => { + resp.writeHead(200); + resp.end("response3"); + }); + + proxyFilter = new Http3ProxyFilter( + proxyHost, + proxyPort, + 0, + "/.well-known/masque/udp/{target_host}/{target_port}/", + "" + ); + + registerCleanupFunction(async () => { + trr_clear_prefs(); + Services.prefs.clearUserPref( + "network.http.http3.retry_different_ip_family" + ); + Services.prefs.clearUserPref("network.http.speculative-parallel-limit"); + Services.prefs.clearUserPref("network.http.http3.block_loopback_ipv6_addr"); + Services.prefs.clearUserPref("network.dns.get-ttl"); + Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + if (trrServer) { + await trrServer.stop(); + } + await proxy.stop(); + await server.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]); + } + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +// Test if we fallback to HTTP/2 proxy when IPv6 is blocked. +add_task(async function test_fallback_with_speculative_connection() { + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6); + pps.registerFilter(proxyFilter, 10); + + const promises = []; + for (let i = 1; i <= 3; i++) { + let chan = makeChan( + `https://alt1.example.com:${serverPort}/concurrent${i}` + ); + promises.push(channelOpenPromise(chan, CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL)); + } + + const results = await Promise.all(promises); + + // Verify all requests succeeded with correct responses + for (let i = 0; i < 3; i++) { + const [req, buf] = results[i]; + Assert.equal(req.status, Cr.NS_OK); + Assert.equal(buf, `response${i + 1}`); + } + + pps.unregisterFilter(proxyFilter); +}); + +add_task(async function test_fallback_without_speculative_connection() { + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); + + Services.obs.notifyObservers(null, "net:cancel-all-connections"); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + pps.registerFilter(proxyFilter, 10); + + const promises = []; + for (let i = 1; i <= 3; i++) { + let chan = makeChan( + `https://alt1.example.com:${serverPort}/concurrent${i}` + ); + promises.push(channelOpenPromise(chan, CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL)); + } + + const results = await Promise.all(promises); + + // Verify all requests succeeded with correct responses + for (let i = 0; i < 3; i++) { + const [req, buf] = results[i]; + Assert.equal(req.status, Cr.NS_OK); + Assert.equal(buf, `response${i + 1}`); + } + + pps.unregisterFilter(proxyFilter); +}); diff --git a/netwerk/test/unit/xpcshell.toml b/netwerk/test/unit/xpcshell.toml @@ -940,6 +940,13 @@ skip-if = [ # connection. Disable test on MacOS 10.15. ] +["test_http3_proxy_ipv6_fallback.js"] +run-sequentially = ["true"] # node server exceptions dont replay well +skip-if = [ + "os == 'android'", + "os == 'win' && msix'", # Bug 1808049 +] + ["test_http3_proxy_no_speculative.js"] head = "head_cookies.js head_channels.js head_cache.js head_http3.js http3_proxy_common.js" run-sequentially = ["true"] # node server exceptions dont replay well