Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

/* Any copyright is dedicated to the Public Domain.
"use strict";
// Regression test for the optimistic-DNS / Happy Eyeballs "got a stale response
// for refresh query" crash (bug 2059597): an answer with a 0 TTL is "born in"
// the cache grace period (SetExpiration puts mGraceStart at now + ttl), so
// deriving nsIDNSAddrRecord.fromStaleCache from the grace status at read time
// misreports a freshly resolved answer -- including a cache-bypassing
// revalidation -- as stale. The flag must instead be captured at serve time:
// true only for a grace-period cache hit, and false for any fresh resolution.
//
// The answer is served by a DoH (TRR) server so it can carry a real TTL of 0,
// and its responses are held back by `delay` so the background renewal that a
// grace hit triggers cannot clear the flag before we read it -- no timing race.
const HOST = "ttl-zero.example.com";
const ADDR = "127.0.0.1";
// Long enough that the renewal a grace hit kicks off cannot complete before we
// read the flag, and that a cache-bypassing lookup must wait for a fresh answer.
const DELAY_MS = 1000;
let trrServer;
function registerA(ttl, delay) {
return trrServer.registerDoHAnswers(HOST, "A", {
answers: [{ name: HOST, ttl, type: "A", flush: false, data: ADDR }],
delay,
});
}
function resolve(flags) {
return new Promise((resolve, reject) => {
let listener = {
onLookupComplete(inRequest, inRecord, inStatus) {
if (!Components.isSuccessCode(inStatus)) {
reject(inStatus);
return;
}
resolve(inRecord.QueryInterface(Ci.nsIDNSAddrRecord));
},
QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]),
};
Services.dns.asyncResolve(
HOST,
Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
flags,
null,
listener,
Services.tm.currentThread,
{}
);
});
}
add_setup(async function setup() {
trr_test_setup();
// TRR_ONLY so every answer comes from the DoH server, keeping native
// resolution (which answers 127.0.0.1 for everything in TRR tests) out of it.
Services.prefs.setIntPref("network.trr.mode", 3);
trrServer = new TRRServer();
await trrServer.start();
Services.prefs.setCharPref(
"network.trr.uri",
`https://foo.example.com:${trrServer.port()}/dns-query`
);
// A single address family keeps the answer unambiguous.
await trrServer.registerDoHAnswers(HOST, "AAAA", { answers: [] });
registerCleanupFunction(async () => {
// Turn TRR off before dropping its URI, so 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();
try {
await trrServer.stop();
} catch (e) {
info("Error stopping server: " + e);
}
});
});
add_task(async function test_stale_cache_lifecycle() {
// 1. First resolution: a fresh TTL-0 answer. It is born in the grace window,
// but as a freshly resolved answer it must not be reported as stale.
await registerA(0, 0);
let fresh = await resolve(Ci.nsIDNSService.RESOLVE_DEFAULT_FLAGS);
Assert.equal(
fresh.fromStaleCache,
false,
"a freshly resolved TTL-0 answer is not stale"
);
// Hold subsequent answers back so the renewal a grace hit starts (and, later,
// the bypass lookup) cannot complete before we read the flag.
await registerA(0, DELAY_MS);
// 2. Second resolution served from the grace-period cache: reported as stale.
let cached = await resolve(Ci.nsIDNSService.RESOLVE_DEFAULT_FLAGS);
Assert.equal(
cached.fromStaleCache,
true,
"a grace-period cache hit is reported as stale"
);
// 3. Cache-bypassing revalidation (what Happy Eyeballs does after a stale
// answer): a fresh answer again, so it must not be reported as stale even
// though its 0 TTL once more lands it in the grace window.
let revalidated = await resolve(Ci.nsIDNSService.RESOLVE_BYPASS_CACHE);
Assert.equal(
revalidated.fromStaleCache,
false,
"a cache-bypassing revalidation of a TTL-0 host is not stale"
);
});