Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

/* 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/. */
"use strict";
// Regression test for bug 2068870. A host with a single resolved address whose
// address has been reported unusable must still be reachable: the legacy
// nsIDNSAddrRecord::getNextAddr() path resets the blocklist when every address
// is blocklisted, but Happy Eyeballs v3 reads the address list through
// getAddresses(), which has no such recovery and hands the state machine zero
// candidates, surfacing as NS_ERROR_UNKNOWN_HOST.
const { HttpServer } = ChromeUtils.importESModule(
);
const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
Ci.nsINativeDNSResolverOverride
);
const HOST = "single-address.example.com";
// Happy Eyeballs looks up A records with RESOLVE_DISABLE_IPV6, and the address
// family is part of the DNS cache key. The blocklist lives on the cached host
// record, so the lookup that poisons it must use the same family.
const A_RECORD_FLAGS = Ci.nsIDNSService.RESOLVE_DISABLE_IPV6;
let server;
let baseURL;
add_setup(async function () {
Services.prefs.setBoolPref("network.http.happy_eyeballs_enabled", true);
Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0);
server = new HttpServer();
server.registerPathHandler("/ok", (req, resp) => {
resp.setStatusLine(req.httpVersion, 200, "OK");
resp.setHeader("Content-Type", "text/plain", false);
resp.write("ok");
});
server.start(-1);
baseURL = `http://${HOST}:${server.identity.primaryPort}`;
override.addIPOverride(HOST, "127.0.0.1");
registerCleanupFunction(async () => {
override.clearOverrides();
Services.prefs.clearUserPref("network.http.happy_eyeballs_enabled");
Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
Services.dns.clearCache(true);
await new Promise(resolve => server.stop(resolve));
});
});
function fetchOk() {
return new Promise(resolve => {
let chan = NetUtil.newChannel({
uri: `${baseURL}/ok`,
loadUsingSystemPrincipal: true,
});
NetUtil.asyncFetch(chan, (stream, status) => {
if (Components.isSuccessCode(status)) {
// Drain the stream so the connection is returned cleanly.
NetUtil.readInputStreamToString(stream, stream.available());
}
resolve(status);
});
});
}
function asyncResolveARecord() {
return new Promise((resolve, reject) => {
Services.dns.asyncResolve(
HOST,
Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
A_RECORD_FLAGS,
null,
{
onLookupComplete(request, record, status) {
if (!Components.isSuccessCode(status)) {
reject(status);
return;
}
resolve(record.QueryInterface(Ci.nsIDNSAddrRecord));
},
},
Services.tm.currentThread,
{}
);
});
}
// Mark the host's only address unusable, the way a transport failure does.
async function blocklistSoleAddress() {
let record = await asyncResolveARecord();
let addresses = [];
while (record.hasMore()) {
addresses.push(record.getNextAddrAsString());
}
Assert.deepEqual(
addresses,
["127.0.0.1"],
"the test host must resolve to exactly one address"
);
// getNextAddrAsString() left the iterator on the last address returned.
record.rewind();
record.getNextAddrAsString();
record.reportUnusable(server.identity.primaryPort);
}
add_task(async function test_single_address_still_usable_after_unusable() {
Assert.equal(await fetchOk(), Cr.NS_OK, "first request succeeds");
await blocklistSoleAddress();
// Force a new connection attempt; the DNS cache (and its blocklist) is
// deliberately left intact, which is what the reporter observed.
Services.obs.notifyObservers(null, "net:cancel-all-connections");
Services.obs.notifyObservers(null, "net:prune-all-connections");
Assert.equal(
await fetchOk(),
Cr.NS_OK,
"the sole address must still be tried after it was reported unusable"
);
});