Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
- This test gets skipped with pattern: os == 'android' OR os == 'win' && 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
"use strict";
// Optimistic DNS: an address served from a stale (past-TTL, grace-period) cache
// entry is raced right away, and Happy Eyeballs revalidates it with its own
// cache-bypassing lookup. Here the stale address refuses the connection, so the
// request can only succeed if that revalidation happens and its address is
// raced too.
//
// Serving a stale entry also makes the resolver renew it in the background, and
// that renewal alone is enough to reach the working address: it updates the
// shared host record, and a lookup that lands after it sees the new address. So
// the revalidated answer is delayed here, which keeps the renewal from
// completing before Happy Eyeballs has consumed the stale answer and failed on
// it. TRR_ONLY mode keeps native resolution, which answers 127.0.0.1 for
// everything in TRR tests, out of the picture as well.
var { setTimeout } = ChromeUtils.importESModule(
"resource://gre/modules/Timer.sys.mjs"
);
const { NodeHTTP2Server } = ChromeUtils.importESModule(
);
const mockController = Cc[
"@mozilla.org/network/mock-network-controller;1"
].getService(Ci.nsIMockNetworkLayerController);
const HOST = "optimistic-dns.example.com";
// The stale answer. Connecting to it is blocked, and refused instantly.
const STALE_ADDR = "127.0.0.2";
// The revalidated answer, where the HTTP/2 server listens.
const FRESH_ADDR = "127.0.0.1";
// Lifetime of the seeded answer, and the wait that ages it into the grace
// period. The revalidated answer outlives the test instead, so that it is never
// itself served stale.
const STALE_TTL_SECONDS = 1;
const STALE_WAIT_MS = STALE_TTL_SECONDS * 1000 + 100;
const FRESH_TTL_SECONDS = 55;
// How long the revalidated answer is held back. Long enough that a lookup
// served from the stale entry cannot observe it, and well short of the TRR
// request timeout in TRR_ONLY mode.
const REVALIDATION_DELAY_MS = 1000;
let trrServer;
let server;
let originURL;
let originPort;
function openChan(expectFailure) {
let chan = NetUtil.newChannel({
uri: originURL,
loadUsingSystemPrincipal: true,
contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
}).QueryInterface(Ci.nsIHttpChannel);
chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
return new Promise(resolve => {
chan.asyncOpen(
new ChannelListener(
req => resolve(req),
null,
(expectFailure ? CL_EXPECT_FAILURE : 0) | CL_ALLOW_UNKNOWN_CL
)
);
});
}
function registerAnswer(addr, ttl, delay) {
return trrServer.registerDoHAnswers(HOST, "A", {
answers: [{ name: HOST, ttl, type: "A", flush: false, data: addr }],
delay,
});
}
add_setup(async function setup() {
trr_test_setup();
Services.prefs.setBoolPref("network.http.happy_eyeballs_enabled", true);
Services.prefs.setBoolPref("network.socket.attach_mock_network_layer", true);
// TRR_ONLY: every answer then comes from the DoH server, which is what lets
// the test control when the revalidated one shows up.
Services.prefs.setIntPref("network.trr.mode", 3);
let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
Ci.nsIX509CertDB
);
addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
server = new NodeHTTP2Server();
await server.start(0, [HOST]);
originPort = server.port();
await server.registerPathHandler("/", (req, resp) => {
resp.writeHead(200, { "Content-Type": "text/plain" });
resp.end("ok");
});
trrServer = new TRRServer();
await trrServer.start();
Services.prefs.setCharPref(
"network.trr.uri",
);
// A single address family keeps the stale answer and its revalidation
// unambiguous.
await trrServer.registerDoHAnswers(HOST, "AAAA", { answers: [] });
mockController.blockTCPConnect(
mockController.createScriptableNetAddr(STALE_ADDR, originPort)
);
registerCleanupFunction(async () => {
// Turn TRR off before dropping its URI, so that nothing looks up the
// default (non-local) resolver on the way out.
Services.prefs.clearUserPref("network.trr.mode");
Services.prefs.clearUserPref("network.trr.uri");
trr_clear_prefs();
Services.prefs.clearUserPref("network.http.happy_eyeballs_enabled");
Services.prefs.clearUserPref("network.socket.attach_mock_network_layer");
mockController.clearBlockedTCPConnect();
try {
await trrServer.stop();
await server.stop();
} catch (e) {
info("Error stopping servers: " + e);
}
});
});
add_task(async function test_stale_answer_revalidated_and_raced() {
// Seed the cache with the address that cannot be connected to, driving the
// request through Happy Eyeballs so the entry is keyed exactly as the one the
// real request below is served from.
await registerAnswer(STALE_ADDR, STALE_TTL_SECONDS, 0);
let seed = await openChan(true);
Assert.equal(
seed.status,
Cr.NS_ERROR_CONNECTION_REFUSED,
"seeding connection to the stale address should be refused"
);
// From now on the name resolves to the working address, but only for a query
// that reaches the resolver, and only after a delay.
await registerAnswer(FRESH_ADDR, FRESH_TTL_SECONDS, REVALIDATION_DELAY_MS);
// Age the seeded entry past its TTL so the next lookup is served from its
// grace period.
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, STALE_WAIT_MS));
let req = await openChan(false);
Assert.equal(
req.QueryInterface(Ci.nsIHttpChannel).responseStatus,
200,
"request should succeed on the revalidated address"
);
Assert.equal(
req.QueryInterface(Ci.nsIHttpChannelInternal).remoteAddress,
FRESH_ADDR,
"should have connected to the revalidated address, not the stale one"
);
});