summaryrefslogtreecommitdiffstats
path: root/netwerk/test/unit/test_httpssvc_retry_with_ech.js
diff options
context:
space:
mode:
Diffstat (limited to 'netwerk/test/unit/test_httpssvc_retry_with_ech.js')
-rw-r--r--netwerk/test/unit/test_httpssvc_retry_with_ech.js511
1 files changed, 511 insertions, 0 deletions
diff --git a/netwerk/test/unit/test_httpssvc_retry_with_ech.js b/netwerk/test/unit/test_httpssvc_retry_with_ech.js
new file mode 100644
index 0000000000..44bed59772
--- /dev/null
+++ b/netwerk/test/unit/test_httpssvc_retry_with_ech.js
@@ -0,0 +1,511 @@
+/* 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 trrServer;
+let h3Port;
+let h3EchConfig;
+
+const certOverrideService = Cc[
+ "@mozilla.org/security/certoverride;1"
+].getService(Ci.nsICertOverrideService);
+
+function checkSecurityInfo(chan, expectPrivateDNS, expectAcceptedECH) {
+ let securityInfo = chan.securityInfo;
+ Assert.equal(
+ securityInfo.isAcceptedEch,
+ expectAcceptedECH,
+ "ECH Status == Expected Status"
+ );
+ Assert.equal(
+ securityInfo.usedPrivateDNS,
+ expectPrivateDNS,
+ "Private DNS Status == Expected Status"
+ );
+}
+
+add_setup(async function setup() {
+ // Allow telemetry probes which may otherwise be disabled for some
+ // applications (e.g. Thunderbird).
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+
+ trr_test_setup();
+
+ Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true);
+ Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true);
+ Services.prefs.setBoolPref("network.dns.echconfig.enabled", true);
+ Services.prefs.setBoolPref("network.dns.http3_echconfig.enabled", true);
+ Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0);
+ Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY);
+
+ await asyncStartTLSTestServer(
+ "EncryptedClientHelloServer",
+ "../../../security/manager/ssl/tests/unit/test_encrypted_client_hello"
+ );
+
+ h3Port = Services.env.get("MOZHTTP3_PORT_ECH");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+
+ h3EchConfig = Services.env.get("MOZHTTP3_ECH");
+ Assert.notEqual(h3EchConfig, null);
+ Assert.notEqual(h3EchConfig, "");
+});
+
+registerCleanupFunction(async () => {
+ trr_clear_prefs();
+ Services.prefs.clearUserPref("network.trr.mode");
+ Services.prefs.clearUserPref("network.trr.uri");
+ Services.prefs.clearUserPref("network.dns.upgrade_with_https_rr");
+ Services.prefs.clearUserPref("network.dns.use_https_rr_as_altsvc");
+ Services.prefs.clearUserPref("network.dns.echconfig.enabled");
+ Services.prefs.clearUserPref("network.dns.http3_echconfig.enabled");
+ Services.prefs.clearUserPref(
+ "network.dns.echconfig.fallback_to_origin_when_all_failed"
+ );
+ Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
+ Services.prefs.clearUserPref("network.dns.port_prefixed_qname_https_rr");
+ Services.prefs.clearUserPref("security.tls.ech.grease_http3");
+ Services.prefs.clearUserPref("security.tls.ech.grease_probability");
+ if (trrServer) {
+ await trrServer.stop();
+ }
+});
+
+function makeChan(url) {
+ let chan = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ return chan;
+}
+
+function channelOpenPromise(chan, flags) {
+ return new Promise(resolve => {
+ function finish(req, buffer) {
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ false
+ );
+ resolve([req, buffer]);
+ }
+ certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ true
+ );
+ let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal);
+ internal.setWaitForHTTPSSVCRecord();
+ chan.asyncOpen(new ChannelListener(finish, null, flags));
+ });
+}
+
+function ActivityObserver() {}
+
+ActivityObserver.prototype = {
+ activites: [],
+ observeConnectionActivity(
+ aHost,
+ aPort,
+ aSSL,
+ aHasECH,
+ aIsHttp3,
+ aActivityType,
+ aActivitySubtype,
+ aTimestamp,
+ aExtraStringData
+ ) {
+ dump(
+ "*** Connection Activity 0x" +
+ aActivityType.toString(16) +
+ " 0x" +
+ aActivitySubtype.toString(16) +
+ " " +
+ aExtraStringData +
+ "\n"
+ );
+ this.activites.push({ host: aHost, subType: aActivitySubtype });
+ },
+};
+
+function checkHttpActivities(activites, expectECH) {
+ let foundDNSAndSocket = false;
+ let foundSettingECH = false;
+ let foundConnectionCreated = false;
+ for (let activity of activites) {
+ switch (activity.subType) {
+ case Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_DNSANDSOCKET_CREATED:
+ case Ci.nsIHttpActivityObserver
+ .ACTIVITY_SUBTYPE_SPECULATIVE_DNSANDSOCKET_CREATED:
+ foundDNSAndSocket = true;
+ break;
+ case Ci.nsIHttpActivityDistributor.ACTIVITY_SUBTYPE_ECH_SET:
+ foundSettingECH = true;
+ break;
+ case Ci.nsIHttpActivityDistributor.ACTIVITY_SUBTYPE_CONNECTION_CREATED:
+ foundConnectionCreated = true;
+ break;
+ default:
+ break;
+ }
+ }
+
+ Assert.equal(foundDNSAndSocket, true, "Should have one DnsAndSock created");
+ Assert.equal(foundSettingECH, expectECH, "Should have echConfig");
+ Assert.equal(
+ foundConnectionCreated,
+ true,
+ "Should have one connection created"
+ );
+}
+
+add_task(async function testConnectWithECH() {
+ const ECH_CONFIG_FIXED =
+ "AEn+DQBFTQAgACCKB1Y5SfrGIyk27W82xPpzWTDs3q72c04xSurDWlb9CgAEAAEAA2QWZWNoLXB1YmxpYy5leGFtcGxlLmNvbQAA";
+ trrServer = new TRRServer();
+ await trrServer.start();
+
+ let observerService = Cc[
+ "@mozilla.org/network/http-activity-distributor;1"
+ ].getService(Ci.nsIHttpActivityDistributor);
+ let observer = new ActivityObserver();
+ observerService.addObserver(observer);
+ observerService.observeConnection = true;
+
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port}/dns-query`
+ );
+
+ // Only the last record is valid to use.
+ await trrServer.registerDoHAnswers("ech-private.example.com", "HTTPS", {
+ answers: [
+ {
+ name: "ech-private.example.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "ech-private.example.com",
+ values: [
+ { key: "alpn", value: "http/1.1" },
+ { key: "port", value: 8443 },
+ {
+ key: "echconfig",
+ value: ECH_CONFIG_FIXED,
+ needBase64Decode: true,
+ },
+ ],
+ },
+ },
+ ],
+ });
+
+ await trrServer.registerDoHAnswers("ech-private.example.com", "A", {
+ answers: [
+ {
+ name: "ech-private.example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("ech-private.example.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ HandshakeTelemetryHelpers.resetHistograms();
+ let chan = makeChan(`https://ech-private.example.com`);
+ await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+ checkSecurityInfo(chan, true, true);
+ // Only check telemetry if network process is disabled.
+ if (!mozinfo.socketprocess_networking) {
+ HandshakeTelemetryHelpers.checkSuccess(["", "_ECH", "_FIRST_TRY"]);
+ HandshakeTelemetryHelpers.checkEmpty(["_CONSERVATIVE", "_ECH_GREASE"]);
+ }
+
+ await trrServer.stop();
+ observerService.removeObserver(observer);
+ observerService.observeConnection = false;
+
+ let filtered = observer.activites.filter(
+ activity => activity.host === "ech-private.example.com"
+ );
+ checkHttpActivities(filtered, true);
+});
+
+add_task(async function testEchRetry() {
+ Services.obs.notifyObservers(null, "net:cancel-all-connections");
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ Services.dns.clearCache(true);
+
+ const ECH_CONFIG_TRUSTED_RETRY =
+ "AEn+DQBFTQAgACCKB1Y5SfrGIyk27W82xPpzWTDs3q72c04xSurDWlb9CgAEAAMAA2QWZWNoLXB1YmxpYy5leGFtcGxlLmNvbQAA";
+ 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`
+ );
+
+ // Only the last record is valid to use.
+ await trrServer.registerDoHAnswers("ech-private.example.com", "HTTPS", {
+ answers: [
+ {
+ name: "ech-private.example.com",
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: "ech-private.example.com",
+ values: [
+ { key: "alpn", value: "http/1.1" },
+ { key: "port", value: 8443 },
+ {
+ key: "echconfig",
+ value: ECH_CONFIG_TRUSTED_RETRY,
+ needBase64Decode: true,
+ },
+ ],
+ },
+ },
+ ],
+ });
+
+ await trrServer.registerDoHAnswers("ech-private.example.com", "A", {
+ answers: [
+ {
+ name: "ech-private.example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("ech-private.example.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ });
+
+ Services.prefs.setBoolPref("network.dns.echconfig.enabled", true);
+
+ HandshakeTelemetryHelpers.resetHistograms();
+ let chan = makeChan(`https://ech-private.example.com`);
+ await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+ checkSecurityInfo(chan, true, true);
+ // Only check telemetry if network process is disabled.
+ if (!mozinfo.socketprocess_networking) {
+ for (let hName of ["SSL_HANDSHAKE_RESULT", "SSL_HANDSHAKE_RESULT_ECH"]) {
+ let h = Services.telemetry.getHistogramById(hName);
+ HandshakeTelemetryHelpers.assertHistogramMap(
+ h.snapshot(),
+ new Map([
+ ["0", 1],
+ ["188", 1],
+ ])
+ );
+ }
+ HandshakeTelemetryHelpers.checkEntry(["_FIRST_TRY"], 188, 1);
+ HandshakeTelemetryHelpers.checkEmpty(["_CONSERVATIVE", "_ECH_GREASE"]);
+ }
+
+ await trrServer.stop();
+});
+
+async function H3ECHTest(
+ echConfig,
+ expectedHistKey,
+ expectedHistEntries,
+ advertiseECH
+) {
+ Services.dns.clearCache(true);
+ Services.obs.notifyObservers(null, "net:cancel-all-connections");
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ resetEchTelemetry();
+ trrServer = new TRRServer();
+ await trrServer.start();
+
+ Services.prefs.setCharPref(
+ "network.trr.uri",
+ `https://foo.example.com:${trrServer.port}/dns-query`
+ );
+ Services.prefs.setBoolPref("network.dns.port_prefixed_qname_https_rr", true);
+
+ let observerService = Cc[
+ "@mozilla.org/network/http-activity-distributor;1"
+ ].getService(Ci.nsIHttpActivityDistributor);
+ Services.obs.notifyObservers(null, "net:cancel-all-connections");
+ let observer = new ActivityObserver();
+ observerService.addObserver(observer);
+ observerService.observeConnection = true;
+ // Clear activities for past connections
+ observer.activites = [];
+
+ let portPrefixedName = `_${h3Port}._https.public.example.com`;
+ let vals = [
+ { key: "alpn", value: "h3-29" },
+ { key: "port", value: h3Port },
+ ];
+ if (advertiseECH) {
+ vals.push({
+ key: "echconfig",
+ value: echConfig,
+ needBase64Decode: true,
+ });
+ }
+ // Only the last record is valid to use.
+
+ await trrServer.registerDoHAnswers(portPrefixedName, "HTTPS", {
+ answers: [
+ {
+ name: portPrefixedName,
+ ttl: 55,
+ type: "HTTPS",
+ flush: false,
+ data: {
+ priority: 1,
+ name: ".",
+ values: vals,
+ },
+ },
+ ],
+ });
+
+ await trrServer.registerDoHAnswers("public.example.com", "A", {
+ answers: [
+ {
+ name: "public.example.com",
+ ttl: 55,
+ type: "A",
+ flush: false,
+ data: "127.0.0.1",
+ },
+ ],
+ });
+
+ await new TRRDNSListener("public.example.com", {
+ type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC,
+ port: h3Port,
+ });
+
+ let chan = makeChan(`https://public.example.com:${h3Port}`);
+ let [req] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
+ req.QueryInterface(Ci.nsIHttpChannel);
+ Assert.equal(req.protocolVersion, "h3-29");
+ checkSecurityInfo(chan, true, advertiseECH);
+
+ await trrServer.stop();
+
+ observerService.removeObserver(observer);
+ observerService.observeConnection = false;
+
+ let filtered = observer.activites.filter(
+ activity => activity.host === "public.example.com"
+ );
+ checkHttpActivities(filtered, advertiseECH);
+ await checkEchTelemetry(expectedHistKey, expectedHistEntries);
+}
+
+function resetEchTelemetry() {
+ Services.telemetry.getKeyedHistogramById("HTTP3_ECH_OUTCOME").clear();
+}
+
+async function checkEchTelemetry(histKey, histEntries) {
+ Services.obs.notifyObservers(null, "net:cancel-all-connections");
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ let values = Services.telemetry
+ .getKeyedHistogramById("HTTP3_ECH_OUTCOME")
+ .snapshot()[histKey];
+ if (!mozinfo.socketprocess_networking) {
+ HandshakeTelemetryHelpers.assertHistogramMap(values, histEntries);
+ }
+}
+
+add_task(async function testH3WithNoEch() {
+ Services.prefs.setBoolPref("security.tls.ech.grease_http3", false);
+ Services.prefs.setIntPref("security.tls.ech.grease_probability", 0);
+ await H3ECHTest(
+ h3EchConfig,
+ "NONE",
+ new Map([
+ ["0", 1],
+ ["1", 0],
+ ]),
+ false
+ );
+});
+
+add_task(async function testH3WithECH() {
+ await H3ECHTest(
+ h3EchConfig,
+ "REAL",
+ new Map([
+ ["0", 1],
+ ["1", 0],
+ ]),
+ true
+ );
+});
+
+add_task(async function testH3WithGreaseEch() {
+ Services.prefs.setBoolPref("security.tls.ech.grease_http3", true);
+ Services.prefs.setIntPref("security.tls.ech.grease_probability", 100);
+ await H3ECHTest(
+ h3EchConfig,
+ "GREASE",
+ new Map([
+ ["0", 1],
+ ["1", 0],
+ ]),
+ false
+ );
+});
+
+add_task(async function testH3WithECHRetry() {
+ Services.dns.clearCache(true);
+ Services.obs.notifyObservers(null, "net:cancel-all-connections");
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ function base64ToArray(base64) {
+ var binary_string = atob(base64);
+ var len = binary_string.length;
+ var bytes = new Uint8Array(len);
+ for (var i = 0; i < len; i++) {
+ bytes[i] = binary_string.charCodeAt(i);
+ }
+ return bytes;
+ }
+
+ let decodedConfig = base64ToArray(h3EchConfig);
+ decodedConfig[6] ^= 0x94;
+ let encoded = btoa(String.fromCharCode.apply(null, decodedConfig));
+ await H3ECHTest(
+ encoded,
+ "REAL",
+ new Map([
+ ["0", 1],
+ ["1", 1],
+ ]),
+ true
+ );
+});