tor-browser

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

commit b36b73270863f3b8ddaffdb2961ba1ca307591de
parent b812274f7bcab51a916d8765a0fc054cdb3b00ef
Author: Valentin Gosu <valentin.gosu@gmail.com>
Date:   Tue,  7 Oct 2025 12:28:44 +0000

Bug 1988988 - Add extension API support for masque proxy r=necko-reviewers,kershaw,robwu

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

Diffstat:
Mtoolkit/components/extensions/ProxyChannelFilter.sys.mjs | 45+++++++++++++++++++++++++++++++++++++++++++--
Atoolkit/components/extensions/test/xpcshell/test_ext_proxy_http3.js | 144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/components/extensions/test/xpcshell/test_proxy_info_results.js | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mtoolkit/components/extensions/test/xpcshell/xpcshell-remote.toml | 3+++
4 files changed, 298 insertions(+), 7 deletions(-)

diff --git a/toolkit/components/extensions/ProxyChannelFilter.sys.mjs b/toolkit/components/extensions/ProxyChannelFilter.sys.mjs @@ -35,6 +35,7 @@ const PROXY_TYPES = Object.freeze({ HTTP: "http", SOCKS: "socks", // SOCKS5 SOCKS4: "socks4", + MASQUE: "masque", }); const ProxyInfoData = { @@ -46,6 +47,7 @@ const ProxyInfoData = { "type", "host", "port", + "pathTemplate", "username", "password", "proxyDNS", @@ -102,6 +104,23 @@ const ProxyInfoData = { proxyData.port = port; }, + pathTemplate(proxyData) { + let { pathTemplate } = proxyData; + if (proxyData.type !== PROXY_TYPES.MASQUE) { + if (pathTemplate !== undefined) { + throw new ExtensionError( + `ProxyInfoData: pathTemplate can only be used for "masque" proxies` + ); + } + return; + } + if (typeof pathTemplate !== "string" || !pathTemplate) { + throw new ExtensionError( + `ProxyInfoData: Invalid proxy path template: "${pathTemplate}"` + ); + } + }, + username(proxyData) { let { username } = proxyData; if (username !== undefined && typeof username !== "string") { @@ -109,6 +128,11 @@ const ProxyInfoData = { `ProxyInfoData: Invalid proxy server username: "${username}"` ); } + if (username !== undefined && proxyData.type === PROXY_TYPES.MASQUE) { + throw new ExtensionError( + `ProxyInfoData: Username not expected for "masque" proxy info` + ); + } }, password(proxyData) { @@ -118,6 +142,11 @@ const ProxyInfoData = { `ProxyInfoData: Invalid proxy server password: "${password}"` ); } + if (password !== undefined && proxyData.type === PROXY_TYPES.MASQUE) { + throw new ExtensionError( + `ProxyInfoData: Password not expected for "masque" proxy info` + ); + } }, proxyDNS(proxyData) { @@ -162,9 +191,9 @@ const ProxyInfoData = { `ProxyInfoData: Invalid proxy server authorization header: "${proxyAuthorizationHeader}"` ); } - if (type !== "https" && type !== "http") { + if (type !== "https" && type !== "http" && type !== "masque") { throw new ExtensionError( - `ProxyInfoData: ProxyAuthorizationHeader requires type "https" or "http"` + `ProxyInfoData: ProxyAuthorizationHeader requires type "https" or "http" or "masque"` ); } }, @@ -198,6 +227,7 @@ const ProxyInfoData = { type, host, port, + pathTemplate, username, password, proxyDNS, @@ -229,6 +259,17 @@ const ProxyInfoData = { failoverTimeout ? failoverTimeout : PROXY_TIMEOUT_SEC, failoverProxy ); + } else if (type == PROXY_TYPES.MASQUE) { + proxyInfo = lazy.ProxyService.newMASQUEProxyInfo( + host, + port, + pathTemplate, + proxyAuthorizationHeader, + connectionIsolationKey, + proxyDNS ? TRANSPARENT_PROXY_RESOLVES_HOST : 0, + failoverTimeout ? failoverTimeout : PROXY_TIMEOUT_SEC, + failoverProxy + ); } else { proxyInfo = lazy.ProxyService.newProxyInfo( type, diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_proxy_http3.js b/toolkit/components/extensions/test/xpcshell/test_ext_proxy_http3.js @@ -0,0 +1,144 @@ +"use strict"; + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "43" +); + +let { promiseShutdownManager, promiseStartupManager } = AddonTestUtils; + +function readFile(file) { + let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(file, -1, 0, 0); + let data = NetUtil.readInputStreamToString(fstream, fstream.available()); + fstream.close(); + return data; +} + +function addCertFromFile(certdb, filename, trustString) { + let certFile = do_get_file(filename, false); + let pem = readFile(certFile) + .replace(/-----BEGIN CERTIFICATE-----/, "") + .replace(/-----END CERTIFICATE-----/, "") + .replace(/[\r\n]/g, ""); + certdb.addCertFromBase64(pem, trustString); +} + +let proxyHost; +let proxyPort; + +/** + * Sets up HTTP3 proxy configuration for extension tests + */ +add_setup(async function setup() { + Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + Services.prefs.setBoolPref("network.dns.disableIPv6", true); + Services.prefs.setIntPref("network.webtransport.datagram_size", 1500); + Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile( + certdb, + "../../../../../netwerk/test/unit/proxy-ca.pem", + "CTu,u,u" + ); + addCertFromFile( + certdb, + "../../../../../netwerk/test/unit/http2-ca.pem", + "CTu,u,u" + ); + + proxyHost = "foo.example.com"; + proxyPort = Services.env.get("MOZHTTP3_PORT_MASQUE"); + + // A dummy request to make sure AltSvcCache::mStorage is ready. + // It doesn't really matter if it succeeds or fails. + try { + await fetch("https://localhost"); + } catch (e) {} + + // In case the prefs have a different value by default. + Services.prefs.setBoolPref( + "extensions.webextensions.early_background_wakeup_on_request", + false + ); + + registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + }); +}); + +add_task(async function test_connect_udp() { + await promiseStartupManager(); + + let h3Port = Services.env.get("MOZHTTP3_PORT"); + ok(h3Port, `MOZHTTP3_PORT should be set: ${h3Port}`); + + function background(proxyInfo) { + browser.proxy.onRequest.addListener( + details => { + browser.test.log(`onRequest: ${JSON.stringify(details)}`); + browser.test.sendMessage("onRequest", details.url); + return proxyInfo; + }, + { urls: ["<all_urls>"] } + ); + } + + let proxyInfo = { + host: proxyHost, + port: proxyPort, + type: "masque", + pathTemplate: "/.well-known/masque/udp/{target_host}/{target_port}/", + connectionIsolationKey: "masque-udp", + }; + + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + permissions: ["proxy", "<all_urls>"], + }, + background: `(${background})(${JSON.stringify(proxyInfo)})`, + }); + + await extension.startup(); + + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + `alt1.example.com;h3=:${h3Port}` + ); + + // Test the HTTP3 connection through the extension proxy + let response = await ExtensionTestUtils.fetch( + `https://alt1.example.com:${h3Port}/no_body`, + `https://alt1.example.com:${h3Port}/` + ); + Assert.equal(response, "Hello World", "HTTP3 proxy request succeeded"); + + equal( + await extension.awaitMessage("onRequest"), + `https://alt1.example.com:${h3Port}/no_body`, + "Observed proxy.onRequest for speculative document request" + ); + equal( + await extension.awaitMessage("onRequest"), + `https://alt1.example.com:${h3Port}/no_body`, + "Observed proxy.onRequest for document request" + ); + equal( + await extension.awaitMessage("onRequest"), + `https://alt1.example.com:${h3Port}/`, + "Observed proxy.onRequest for fetch() call" + ); + + await extension.unload(); + await promiseShutdownManager(); +}); diff --git a/toolkit/components/extensions/test/xpcshell/test_proxy_info_results.js b/toolkit/components/extensions/test/xpcshell/test_proxy_info_results.js @@ -194,7 +194,7 @@ add_task(async function test_proxyInfo_results() { ], expected: { error: - 'ProxyInfoData: ProxyAuthorizationHeader requires type "https" or "http"', + 'ProxyInfoData: ProxyAuthorizationHeader requires type "https" or "http" or "masque"', }, }, { @@ -304,7 +304,7 @@ add_task(async function test_proxyInfo_results() { expected: { proxyInfo: { host: "foo.bar", - port: "3128", + port: 3128, type: "http", }, }, @@ -342,7 +342,7 @@ add_task(async function test_proxyInfo_results() { expected: { proxyInfo: { host: "foo.bar", - port: "3128", + port: 3128, type: "https", }, }, @@ -441,7 +441,7 @@ add_task(async function test_proxyInfo_results() { expected: { proxyInfo: { host: "foo.bar", - port: "3128", + port: 3128, type: "https", proxyAuthorizationHeader: "test", connectionIsolationKey: "key", @@ -461,11 +461,114 @@ add_task(async function test_proxyInfo_results() { expected: { proxyInfo: { host: "foo.bar", - port: "3128", + port: 3128, + type: "http", + proxyAuthorizationHeader: "test", + connectionIsolationKey: "key", + }, + }, + }, + { + proxy: [ + { type: "http", + host: "foo.bar", + port: 8080, + username: "mungosantamaria", + password: "pass123", + proxyDNS: true, + failoverTimeout: 3, + pathTemplate: "/.well-known/masque/udp/{target_host}/{target_port}/", + }, + ], + expected: { + error: `ProxyInfoData: pathTemplate can only be used for "masque" proxies`, + }, + }, + { + proxy: [ + { + host: "foo.bar", + port: 3128, + pathTemplate: "/.well-known/masque/udp/{target_host}/{target_port}/", + type: "masque", + }, + ], + expected: { + proxyInfo: { + host: "foo.bar", + port: 3128, + type: "masque", + pathTemplate: "/.well-known/masque/udp/{target_host}/{target_port}/", + }, + }, + }, + { + proxy: [ + { + host: "foo.bar", + port: 3128, + proxyAuthorizationHeader: "test", + connectionIsolationKey: "key", + pathTemplate: "/.well-known/masque/udp/{target_host}/{target_port}/", + type: "masque", + }, + ], + expected: { + proxyInfo: { + host: "foo.bar", + port: 3128, + type: "masque", + proxyAuthorizationHeader: "test", + connectionIsolationKey: "key", + pathTemplate: "/.well-known/masque/udp/{target_host}/{target_port}/", + }, + }, + }, + { + proxy: [ + { + type: "masque", + host: "foo.bar", + port: 3128, + proxyAuthorizationHeader: "test", + connectionIsolationKey: "key", + }, + ], + expected: { + error: `ProxyInfoData: Invalid proxy path template: "undefined"`, + }, + }, + { + proxy: [ + { + type: "masque", + host: "foo.bar", + port: 3128, + proxyAuthorizationHeader: "test", + connectionIsolationKey: "key", + pathTemplate: "/.well-known/masque/udp/{target_host}/{target_port}/", + username: "mungosantamaria", + }, + ], + expected: { + error: `ProxyInfoData: Username not expected for "masque" proxy info`, + }, + }, + { + proxy: [ + { + host: "foo.bar", + port: 3128, proxyAuthorizationHeader: "test", connectionIsolationKey: "key", + pathTemplate: "/.well-known/masque/udp/{target_host}/{target_port}/", + type: "masque", + password: "pass123", }, + ], + expected: { + error: `ProxyInfoData: Password not expected for "masque" proxy info`, }, }, ]; diff --git a/toolkit/components/extensions/test/xpcshell/xpcshell-remote.toml b/toolkit/components/extensions/test/xpcshell/xpcshell-remote.toml @@ -34,6 +34,9 @@ skip-if = ["tsan"] ["test_ext_ipcBlob.js"] skip-if = ["os == 'android' && processor == 'x86_64'"] +["test_ext_proxy_http3.js"] +support-files = ["!/netwerk/test/unit/proxy-ca.pem", "!/netwerk/test/unit/http2-ca.pem"] + ["test_extension_process_alive.js"] ["test_process_crash_telemetry.js"]