Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
-
CI results over the last 22 days3,468Runs100%Pass0Failures0Timeouts0Crashes4,319Skips
Scheduled runs skipped per day (up to 59.4%) Oct 6Oct 27 - This test gets skipped with pattern: os == 'android' OR os == 'win'
- This test runs only with pattern: !socketprocess_networking
- 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
// The host resolves to an IPv6 and an IPv4 address, but the IPv6 address is
// unusable: TCP connects to it are refused and UDP sends to it keep returning
// EAGAIN while the socket still polls as writable. A resumed (0-RTT) HTTP/3
// session to the IPv6 address must not wedge requests forever; they should
// complete over another route.
//
// Run with and without Happy Eyeballs.
"use strict";
var { setTimeout, clearTimeout } = ChromeUtils.importESModule(
"resource://gre/modules/Timer.sys.mjs"
);
const { NodeHTTP2Server, 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);
const HOST = "foo.example.com";
const REQUEST_TIMEOUT_MS = 10000;
let h3Server;
let h2Server;
let h3Port;
let h2Port;
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.http3.enable", true);
Services.prefs.setBoolPref("network.http.http3.enable_0rtt", true);
Services.prefs.setBoolPref("network.socket.attach_mock_network_layer", true);
Services.prefs.setBoolPref("network.dns.disableIPv6", false);
Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0);
Services.prefs.setIntPref("network.http.http3.0rtt_timeout", 1000);
// Give the second request time to join the HTTP/3 connection before the
// first request's TCP backup takes it away.
Services.prefs.setIntPref("network.http.http3.backup_timer_delay", 1000);
h3Server = new HTTP3Server();
await h3Server.start(h3ServerPath, h3DBPath);
h3Port = h3Server.port();
h2Server = new NodeHTTP2Server();
await h2Server.start();
h2Port = h2Server.port();
for (let path of ["/", "/first", "/second"]) {
await h2Server.registerPathHandler(path, (_req, resp) => {
resp.writeHead(200, { "content-type": "text/plain" });
resp.end("ok");
});
}
Services.prefs.setCharPref(
"network.http.http3.alt-svc-mapping-for-testing",
`${HOST};h3=:${h3Port}`
);
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.socket.attach_mock_network_layer");
Services.prefs.clearUserPref("network.dns.disableIPv6");
Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
Services.prefs.clearUserPref("network.http.http3.0rtt_timeout");
Services.prefs.clearUserPref("network.http.http3.backup_timer_delay");
Services.prefs.clearUserPref(
"network.http.http3.alt-svc-mapping-for-testing"
);
override.clearOverrides();
clearMocks();
await h3Server.stop();
await h2Server.stop();
});
});
function clearMocks() {
mockController.clearBlockedTCPConnect();
mockController.clearWouldBlockUDPAddr();
}
async function resetState() {
clearMocks();
Services.obs.notifyObservers(null, "net:cancel-all-connections");
Services.obs.notifyObservers(null, "browser:purge-session-history");
let tokenCache = Cc["@mozilla.org/network/ssl-tokens-cache;1"].getService(
Ci.nsISSLTokensCache
);
await tokenCache.asyncClearSSLExternalAndInternalSessionCache();
Services.dns.clearCache(true);
override.clearOverrides();
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 1000));
}
function openChan(path = "/") {
let chan = NetUtil.newChannel({
loadUsingSystemPrincipal: true,
}).QueryInterface(Ci.nsIHttpChannel);
chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
let promise = new Promise(resolve => {
chan.asyncOpen({
onStartRequest() {},
onDataAvailable(_req, stream, _offset, count) {
read_stream(stream, count);
},
onStopRequest(req, status) {
let httpChan = req.QueryInterface(Ci.nsIHttpChannel);
resolve({
status,
responseStatus: Components.isSuccessCode(status)
? httpChan.responseStatus
: 0,
protocol: Components.isSuccessCode(status)
? httpChan.protocolVersion
: "",
});
},
QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
});
});
return { chan, promise };
}
async function openChanWithTimeout(path) {
let { chan, promise } = openChan(path);
let timer;
let timeout = new Promise(resolve => {
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
timer = setTimeout(() => resolve(null), REQUEST_TIMEOUT_MS);
});
let result = await Promise.race([promise, timeout]);
clearTimeout(timer);
if (!result) {
chan.cancel(Cr.NS_BINDING_ABORTED);
await promise;
}
return result;
}
async function primeSessionTicket() {
// Only the IPv4 address is known and TCP is blocked, so this request uses
// HTTP/3 over IPv4 and leaves a session ticket for 0-RTT.
override.addIPOverride(HOST, "127.0.0.1");
mockController.blockTCPConnect(
mockController.createScriptableNetAddr("127.0.0.1", h2Port)
);
let { promise } = openChan();
let r = await promise;
Assert.equal(r.responseStatus, 200, "Priming request should succeed");
Assert.equal(r.protocol, "h3", "Priming request should use HTTP/3");
// Let the NewSessionTicket arrive before tearing the connection down.
// 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, 1000));
mockController.clearBlockedTCPConnect();
Services.dns.clearCache(true);
override.clearOverrides();
}
function breakIPv6() {
override.addIPOverride(HOST, "::1");
override.addIPOverride(HOST, "127.0.0.1");
mockController.blockTCPConnect(
mockController.createScriptableNetAddr("::1", h2Port)
);
mockController.wouldBlockUDPAddrIO(
mockController.createScriptableNetAddr("::1", h3Port)
);
}
async function runScenario(heEnabled) {
Services.prefs.setBoolPref("network.http.happy_eyeballs_enabled", heEnabled);
await resetState();
await primeSessionTicket();
breakIPv6();
// The first request sets up the resumed HTTP/3 connection. The second one
// starts while that connection is already active, so it is dispatched onto
// it directly, like the requests that hung in the original report.
let first = openChanWithTimeout("/first");
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 300));
let second = openChanWithTimeout("/second");
checkResult(await first, "first", heEnabled);
checkResult(await second, "second", heEnabled);
await resetState();
}
function checkResult(r, name, heEnabled) {
Assert.ok(
r,
`The ${name} request should not hang when IPv6 sends would block (HE=${heEnabled})`
);
if (r) {
Assert.ok(
Components.isSuccessCode(r.status),
`The ${name} request should succeed (HE=${heEnabled}), status=0x${r.status.toString(16)}`
);
Assert.equal(r.responseStatus, 200, "Response status should be 200");
}
}
add_task(async function test_ipv6_would_block_with_happy_eyeballs() {
await runScenario(true);
});
add_task(async function test_ipv6_would_block_without_happy_eyeballs() {
await runScenario(false);
});