Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
- 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 for negative entries, end-to-end through Happy Eyeballs
// a stale negative; the resolver does not refresh it, so the connection can
// only succeed if Happy Eyeballs revalidates it (a cache-bypassing refresh) and
// uses the fresh address.
//
// The other family (AAAA) resolves to an address whose connect is blocked by
// the mock network layer. That keeps A eligible to be served stale (a negative
// is only served for a high-priority query when the other family has a usable
// positive) and it means the connection cannot succeed on AAAA -- only on the
// revalidated A. The blocked address is never actually connected to, so no real
// IPv6 connectivity is required.
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 = "negative-optimistic-dns.example.com";
// Working address (A), where the HTTP/2 server listens. Only reachable after
// the stale A negative is revalidated.
const FRESH_ADDR = "127.0.0.1";
// The other family (AAAA); its connect is blocked so it can't carry the request.
const BLOCKED_ADDR = "::1";
const NEG_TTL = 1; // seconds
const GRACE = 600; // seconds
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
)
);
});
}
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);
Services.prefs.setIntPref("network.trr.mode", 3);
Services.prefs.setIntPref("network.dnsNegativeCacheExpiration", NEG_TTL);
Services.prefs.setIntPref(
"network.dnsNegativeCacheExpirationGracePeriod",
GRACE
);
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",
);
// AAAA is a usable-but-unreachable positive: it keeps the A negative eligible
// to be served stale, and its connect is blocked below.
await trrServer.registerDoHAnswers(HOST, "AAAA", {
answers: [
{ name: HOST, ttl: 55, type: "AAAA", flush: false, data: BLOCKED_ADDR },
],
});
mockController.blockTCPConnect(
mockController.createScriptableNetAddr(BLOCKED_ADDR, originPort)
);
registerCleanupFunction(async () => {
Services.prefs.clearUserPref("network.http.happy_eyeballs_enabled");
Services.prefs.clearUserPref("network.socket.attach_mock_network_layer");
Services.prefs.clearUserPref("network.trr.mode");
Services.prefs.clearUserPref("network.trr.uri");
Services.prefs.clearUserPref("network.dnsNegativeCacheExpiration");
Services.prefs.clearUserPref(
"network.dnsNegativeCacheExpirationGracePeriod"
);
trr_clear_prefs();
mockController.clearBlockedTCPConnect();
try {
await trrServer.stop();
await server.stop();
} catch (e) {
info("Error stopping servers: " + e);
}
});
});
add_task(async function test_stale_negative_revalidated_by_happy_eyeballs() {
// Seed a negative A entry: A resolves to nothing and AAAA's connect is
// blocked, so the request fails and a negative A record is cached.
await trrServer.registerDoHAnswers(HOST, "A", { answers: [] });
let seed = await openChan(true);
Assert.equal(
seed.status,
Cr.NS_ERROR_CONNECTION_REFUSED,
"seeding request fails: A has no address and the AAAA connect is refused"
);
// A now has a working address, reachable only if the stale negative is
// revalidated by Happy Eyeballs.
await trrServer.registerDoHAnswers(HOST, "A", {
answers: [
{ name: HOST, ttl: 55, type: "A", flush: false, data: FRESH_ADDR },
],
});
// Age the negative A entry past its TTL into its grace period.
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(r => setTimeout(r, NEG_TTL * 1000 + 200));
let req = await openChan(false);
Assert.equal(
req.QueryInterface(Ci.nsIHttpChannel).responseStatus,
200,
"request succeeds after Happy Eyeballs revalidates the stale A negative"
);
Assert.equal(
req.QueryInterface(Ci.nsIHttpChannelInternal).remoteAddress,
FRESH_ADDR,
"connected to the revalidated A address"
);
});