Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

  • This test gets skipped with pattern: os == 'win' && os_version == '11.26100' && arch == 'x86_64' && msix OR os == 'win' && os_version == '11.26200' && arch == 'x86_64' && msix
  • Manifest: netwerk/test/unit/xpcshell.toml
/* 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/. */
// Tests that when a server sends a fatal TLS alert on a session resumption
// attempt over an HTTPS-RR-routed connection, Firefox retries on the SAME
// alt-route (with a fresh handshake, no PSK) instead of stripping the route
// and falling back to the bare origin port.
//
// Without route preservation, PrepareConnInfoForRetry strips the alt-route
// (because echConfig is empty) and the retry hits the bare origin port,
// which has no listener in this test rig — surfacing CONNECTION_REFUSED to
// the user as "Unable to connect" instead of recovering with a fresh
// handshake on the alt-port.
//
// Both common alert variants are covered:
// - SSL_ERROR_ILLEGAL_PARAMETER_ALERT (e.g. PSK binder verification failure)
// - SSL_ERROR_DECRYPT_ERROR_ALERT (e.g. server STEK rotation)
// Both map to NS_ERROR_MODULE_SECURITY, so ShouldRestartOnResumptionError
// treats them identically.
//
// Companion: test_retry_illegal_parameter.js / test_retry_decrypt_error.js
// (no HTTPS RR — verify the fix is a no-op when mOrigConnInfo is null).
"use strict";
const { HttpServer } = ChromeUtils.importESModule(
);
const { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
// FaultyServer listens on a fixed port (TLSServer.cpp LISTEN_PORT) and
// matches behavior by SNI; these hostnames trigger the resumption-time
// alerts we want to exercise.
const kAltPort = 8443;
const kIllegalParameterHost = "illegal-parameter-on-resume.example.com";
const kDecryptErrorHost = "decrypt-error-on-resume.example.com";
// URL port used by test channels. Nothing must be listening here so a
// route-stripping fallback fails clearly with CONNECTION_REFUSED.
const kOriginPort = 8765;
// Fresh object per call: xpcshell's add_test mutates the options it receives
// (sets isTask/isSetup), so a shared const would fail on its second use.
const skipIfSystemNSS = () => ({ skip_if: () => AppConstants.MOZ_SYSTEM_NSS });
let httpServer;
let trrServer;
let resumeCallbackCount = 0;
async function registerHTTPSRR(host) {
await trrServer.registerDoHAnswers(
`_${kOriginPort}._https.${host}`,
"HTTPS",
{
answers: [
{
name: `_${kOriginPort}._https.${host}`,
ttl: 55,
type: "HTTPS",
flush: false,
data: {
priority: 1,
name: host,
values: [
{ key: "alpn", value: "h2" },
{ key: "port", value: kAltPort },
],
},
},
],
}
);
await trrServer.registerDoHAnswers(host, "A", {
answers: [
{
name: host,
ttl: 55,
type: "A",
flush: false,
data: "127.0.0.1",
},
],
});
}
// Local makeChan: HTTPS-RR routing is disallowed for system-principal
// channels whose content policy type is not TYPE_DOCUMENT (see
// nsHttpChannel.cpp httpsRRAllowed). The shared head_channels.js makeChan
// doesn't set contentPolicyType, so we need our own.
function makeDocChan(url) {
return NetUtil.newChannel({
uri: url,
loadUsingSystemPrincipal: true,
contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
}).QueryInterface(Ci.nsIHttpChannel);
}
// Opens an HTTPS channel to host:kOriginPort and asserts it succeeded and
// that FaultyServer fired its callback `expectedFaults` times during this
// connection.
async function connectAndAssert(host, expectedFaults, label) {
const before = resumeCallbackCount;
const chan = makeDocChan(`https://${host}:${kOriginPort}/`);
const [, buf] = await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL);
ok(buf, `${host}: ${label}: connection succeeded`);
equal(
resumeCallbackCount - before,
expectedFaults,
`${host}: ${label}: FaultyServer fired ${expectedFaults} time(s)`
);
}
add_setup(skipIfSystemNSS(), async () => {
httpServer = new HttpServer();
httpServer.registerPathHandler("/callback/1", () => {
resumeCallbackCount++;
});
httpServer.start(-1);
registerCleanupFunction(async () => httpServer.stop());
await asyncSetupFaultyServer(httpServer);
trr_test_setup();
for (const [name, value] of [
["network.dns.upgrade_with_https_rr", true],
["network.dns.use_https_rr_as_altsvc", true],
["network.dns.echconfig.enabled", false],
]) {
Services.prefs.setBoolPref(name, value);
registerCleanupFunction(() => Services.prefs.clearUserPref(name));
}
trrServer = new TRRServer();
await trrServer.start();
registerCleanupFunction(async () => trrServer.stop());
Services.prefs.setIntPref("network.trr.mode", 3);
Services.prefs.setCharPref(
"network.trr.uri",
`https://foo.example.com:${trrServer.port()}/dns-query`
);
registerCleanupFunction(() => trr_clear_prefs());
await registerHTTPSRR(kIllegalParameterHost);
await registerHTTPSRR(kDecryptErrorHost);
});
// One round: fresh handshake (populates the session-ticket cache, no fault),
// then a resumption attempt that the server rejects (fault count +1). With
// the fix, the retry stays on the alt-route and succeeds; without it, the
// retry hits the bare origin port (no listener) and CONNECTION_REFUSED
// surfaces as a channel error.
async function testResumptionRetry(host) {
await connectAndAssert(host, 0, "fresh handshake");
// The server's anti-replay window prohibits accepting 0-RTT immediately
// after issuing a ticket.
await sleep(1);
await connectAndAssert(host, 1, "resumption rejected, retry on alt-route");
}
add_task(skipIfSystemNSS(), async function test_illegal_parameter() {
await testResumptionRetry(kIllegalParameterHost);
});
add_task(skipIfSystemNSS(), async function test_decrypt_error() {
await testResumptionRetry(kDecryptErrorHost);
});