/* 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(
"resource://testing-common/httpd.sys.mjs"
);
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);
});