Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* 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/. */
// Verifies that when an H3 0-RTT attempt fails with NS_ERROR_NET_RESET,
// ProcessConnectionResult restarts the transaction without 0-RTT and the
// retry succeeds via H3 (full handshake, resumed=false).
//
// blockUDPAddrIO + 0rtt_timeout cause the 0-RTT session to time out, which
// closes the HE stream with NS_ERROR_NET_RESET (mBeforeConnectedError=true).
// ProcessConnectionResult calls FinishAdopted0RTT(true)+Restart().
// Http3Session::Shutdown sets mDontExclude=true (mHad0RttStream) so H3
// stays in the alt-svc map. After clearing the UDP block the retry H3
// completes the full QUIC handshake and returns 200/h3.
"use strict";
var { setTimeout } = ChromeUtils.importESModule(
"resource://gre/modules/Timer.sys.mjs"
);
const { NodeHTTP2Server } = ChromeUtils.importESModule(
);
const { HTTP3Server } = ChromeUtils.importESModule(
);
const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
Ci.nsINativeDNSResolverOverride
);
const mockController = Cc[
"@mozilla.org/network/mock-network-controller;1"
].getService(Ci.nsIMockNetworkLayerController);
let h3Server;
let h2Server;
let h3Port;
add_setup(async function () {
let h3ServerPath = Services.env.get("MOZ_HTTP3_SERVER_PATH");
let h3DBPath = Services.env.get("MOZ_HTTP3_CERT_DB_PATH");
let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
Ci.nsIX509CertDB
);
addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
Services.prefs.setBoolPref("network.http.happy_eyeballs_enabled", true);
Services.prefs.setBoolPref("network.http.http3.enable", true);
Services.prefs.setBoolPref("network.http.http3.enable_0rtt", true);
// NSPR I/O is required for the mock network layer to intercept sendto().
Services.prefs.setBoolPref("network.http.http3.use_nspr_for_io", true);
Services.prefs.setBoolPref("network.socket.attach_mock_network_layer", true);
Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0);
// Use IPv4 only so HE races a single address — keeps the scenario simple.
Services.prefs.setBoolPref("network.dns.disableIPv6", true);
h3Server = new HTTP3Server();
await h3Server.start(h3ServerPath, h3DBPath);
h3Port = h3Server.port();
h2Server = new NodeHTTP2Server();
await h2Server.start();
await h2Server.registerPathHandler("/", (_req, resp) => {
resp.writeHead(200, { "content-type": "text/plain" });
resp.end("ok");
});
override.addIPOverride("foo.example.com", "127.0.0.1");
registerCleanupFunction(async () => {
Services.prefs.clearUserPref("network.http.happy_eyeballs_enabled");
Services.prefs.clearUserPref("network.http.http3.enable");
Services.prefs.clearUserPref("network.http.http3.enable_0rtt");
Services.prefs.clearUserPref("network.http.http3.use_nspr_for_io");
Services.prefs.clearUserPref("network.socket.attach_mock_network_layer");
Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
Services.prefs.clearUserPref("network.dns.disableIPv6");
Services.prefs.clearUserPref(
"network.http.http3.alt-svc-mapping-for-testing"
);
override.clearOverrides();
mockController.clearBlockedUDPAddr();
mockController.clearBlockedTCPConnect();
await h3Server.stop();
await h2Server.stop();
});
});
function openChan(host, port) {
return new Promise(resolve => {
let chan = NetUtil.newChannel({
uri: `https://${host}:${port}/`,
loadUsingSystemPrincipal: true,
}).QueryInterface(Ci.nsIHttpChannel);
chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
chan.asyncOpen(
new ChannelListener(
(req, buf) => {
let resumed = false;
try {
resumed = req.securityInfo.resumed;
} catch (_e) {}
resolve({
status: req.QueryInterface(Ci.nsIHttpChannel).responseStatus,
protocol: req.protocolVersion,
resumed,
buffer: buf,
});
},
null,
CL_ALLOW_UNKNOWN_CL
)
);
});
}
add_task(async function test_he_h3_0rtt_reset_restarts_without_0rtt() {
const host = "foo.example.com";
const h2Port = h2Server.port();
// Alt-svc maps the host to the H3 server; plain TCP connects to the H2
// server on h2Port.
Services.prefs.setCharPref(
"network.http.http3.alt-svc-mapping-for-testing",
`${host};h3=:${h3Port}`
);
// ── First request: full H3 handshake, PSK ticket written to cache ──────
let r1 = await openChan(host, h2Port);
Assert.equal(r1.status, 200, "First request should succeed");
Assert.equal(r1.protocol, "h3", "First request should use H3");
Assert.equal(r1.resumed, false, "First request should not use 0-RTT");
// Give NSS time to persist the session ticket before clearing connections.
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 1000));
Services.obs.notifyObservers(null, "net:cancel-all-connections");
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 500));
// ── Second request: H3 0-RTT times out -> restart -> retry via H3 ──────────
Services.prefs.setIntPref("network.http.http3.0rtt_timeout", 100);
let blockAddr = mockController.createScriptableNetAddr("127.0.0.1", h3Port);
mockController.blockUDPAddrIO(blockAddr);
let blockedTCP = mockController.createScriptableNetAddr("127.0.0.1", h2Port);
mockController.blockTCPConnect(blockedTCP);
let r2Promise = openChan(host, h2Port);
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 150));
mockController.clearBlockedUDPAddr();
let r2 = await r2Promise;
Assert.equal(r2.status, 200, "Restarted request should succeed");
Assert.equal(r2.protocol, "h3", "Retry uses H3 full handshake (no 0-RTT)");
Assert.equal(
r2.resumed,
false,
"No 0-RTT: PSK was consumed by the 0-RTT attempt"
);
mockController.clearBlockedTCPConnect();
Services.prefs.clearUserPref("network.http.http3.0rtt_timeout");
});