Source code
Revision control
Copy as Markdown
Other Tools
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=4 sw=2 sts=2 et cin: */
/* 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
#include "DNS.h"
#include "DNSUtils.h"
#include "nsCharSeparatedTokenizer.h"
#include "nsContentUtils.h"
#include "nsHttpHandler.h"
#include "nsHttpChannel.h"
#include "nsHostResolver.h"
#include "nsIHttpChannel.h"
#include "nsIHttpChannelInternal.h"
#include "nsIIOService.h"
#include "nsIInputStream.h"
#include "nsIObliviousHttp.h"
#include "nsIOService.h"
#include "nsISupports.h"
#include "nsISupportsUtils.h"
#include "nsITimedChannel.h"
#include "nsIUploadChannel2.h"
#include "nsIURIMutator.h"
#include "nsNetUtil.h"
#include "nsQueryObject.h"
#include "nsStringStream.h"
#include "nsThreadUtils.h"
#include "nsURLHelper.h"
#include "ObliviousHttpChannel.h"
#include "TRR.h"
#include "TRRService.h"
#include "TRRServiceChannel.h"
#include "TRRLoadInfo.h"
#include "mozilla/Base64.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/Logging.h"
#include "mozilla/Maybe.h"
#include "mozilla/Preferences.h"
#include "mozilla/StaticPrefs_network.h"
#include "mozilla/Telemetry.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/Tokenizer.h"
#include "mozilla/UniquePtr.h"
// Put DNSLogging.h at the end to avoid LOG being overwritten by other headers.
#include "DNSLogging.h"
#include "mozilla/glean/GleanMetrics.h"
namespace mozilla {
namespace net {
NS_IMPL_ISUPPORTS(TRR, nsIHttpPushListener, nsIInterfaceRequestor,
nsIStreamListener, nsIRunnable, nsITimerCallback)
// when firing off a normal A or AAAA query
TRR::TRR(AHostResolver* aResolver, nsHostRecord* aRec, enum TrrType aType)
: mozilla::Runnable("TRR"),
mRec(aRec),
mHostResolver(aResolver),
mType(aType),
mOriginSuffix(aRec->originSuffix) {
mHost = aRec->host;
mPB = aRec->pb;
MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess() || XRE_IsSocketProcess(),
"TRR must be in parent or socket process");
}
// when following CNAMEs
TRR::TRR(AHostResolver* aResolver, nsHostRecord* aRec, nsCString& aHost,
enum TrrType& aType, unsigned int aLoopCount, bool aPB)
: mozilla::Runnable("TRR"),
mHost(aHost),
mRec(aRec),
mHostResolver(aResolver),
mType(aType),
mPB(aPB),
mCnameLoop(aLoopCount),
mOriginSuffix(aRec ? aRec->originSuffix : ""_ns) {
MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess() || XRE_IsSocketProcess(),
"TRR must be in parent or socket process");
}
// used on push
TRR::TRR(AHostResolver* aResolver, bool aPB)
: mozilla::Runnable("TRR"), mHostResolver(aResolver), mPB(aPB) {
MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess() || XRE_IsSocketProcess(),
"TRR must be in parent or socket process");
}
// to verify a domain
TRR::TRR(AHostResolver* aResolver, nsACString& aHost, enum TrrType aType,
const nsACString& aOriginSuffix, bool aPB, bool aUseFreshConnection)
: mozilla::Runnable("TRR"),
mHost(aHost),
mRec(nullptr),
mHostResolver(aResolver),
mType(aType),
mPB(aPB),
mOriginSuffix(aOriginSuffix),
mUseFreshConnection(aUseFreshConnection) {
MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess() || XRE_IsSocketProcess(),
"TRR must be in parent or socket process");
}
void TRR::HandleTimeout() {
mTimeout = nullptr;
RecordReason(TRRSkippedReason::TRR_TIMEOUT);
Cancel(NS_ERROR_NET_TIMEOUT_EXTERNAL);
}
NS_IMETHODIMP
TRR::Notify(nsITimer* aTimer) {
if (aTimer == mTimeout) {
HandleTimeout();
} else {
MOZ_CRASH("Unknown timer");
}
return NS_OK;
}
NS_IMETHODIMP
TRR::Run() {
MOZ_ASSERT_IF(XRE_IsParentProcess() && TRRService::Get(),
NS_IsMainThread() || TRRService::Get()->IsOnTRRThread());
MOZ_ASSERT_IF(XRE_IsSocketProcess(), NS_IsMainThread());
if ((TRRService::Get() == nullptr) || NS_FAILED(SendHTTPRequest())) {
RecordReason(TRRSkippedReason::TRR_SEND_FAILED);
FailData(NS_ERROR_FAILURE);
// The dtor will now be run
}
return NS_OK;
}
DNSPacket* TRR::GetOrCreateDNSPacket() {
if (!mPacket) {
mPacket = MakeUnique<DNSPacket>();
}
return mPacket.get();
}
nsresult TRR::CreateQueryURI(nsIURI** aOutURI) {
nsAutoCString uri;
nsCOMPtr<nsIURI> dnsURI;
if (UseDefaultServer()) {
TRRService::Get()->GetURI(uri);
} else {
uri = mRec->mTrrServer;
}
nsresult rv = NS_NewURI(getter_AddRefs(dnsURI), uri);
if (NS_FAILED(rv)) {
RecordReason(TRRSkippedReason::TRR_BAD_URL);
return rv;
}
dnsURI.forget(aOutURI);
return NS_OK;
}
bool TRR::MaybeBlockRequest() {
if (((mType == TRRTYPE_A) || (mType == TRRTYPE_AAAA)) &&
mRec->mEffectiveTRRMode != nsIRequest::TRR_ONLY_MODE) {
// let NS resolves skip the blocklist check
// we also don't check the blocklist for TRR only requests
MOZ_ASSERT(mRec);
// If TRRService isn't enabled anymore for the req, don't do TRR.
if (!TRRService::Get()->Enabled(mRec->mEffectiveTRRMode)) {
RecordReason(TRRSkippedReason::TRR_MODE_NOT_ENABLED);
return true;
}
if (!StaticPrefs::network_trr_strict_native_fallback() &&
UseDefaultServer() &&
TRRService::Get()->IsTemporarilyBlocked(mHost, mOriginSuffix, mPB,
true)) {
if (mType == TRRTYPE_A) {
// count only blocklist for A records to avoid double counts
Telemetry::Accumulate(Telemetry::DNS_TRR_BLACKLISTED3,
TRRService::ProviderKey(), true);
}
RecordReason(TRRSkippedReason::TRR_HOST_BLOCKED_TEMPORARY);
// not really an error but no TRR is issued
return true;
}
if (TRRService::Get()->IsExcludedFromTRR(mHost)) {
RecordReason(TRRSkippedReason::TRR_EXCLUDED);
return true;
}
if (UseDefaultServer() && (mType == TRRTYPE_A)) {
Telemetry::Accumulate(Telemetry::DNS_TRR_BLACKLISTED3,
TRRService::ProviderKey(), false);
}
}
return false;
}
nsresult TRR::SendHTTPRequest() {
// This is essentially the "run" method - created from nsHostResolver
if (mCancelled) {
return NS_ERROR_FAILURE;
}
if ((mType != TRRTYPE_A) && (mType != TRRTYPE_AAAA) &&
(mType != TRRTYPE_NS) && (mType != TRRTYPE_TXT) &&
(mType != TRRTYPE_HTTPSSVC)) {
// limit the calling interface because nsHostResolver has explicit slots for
// these types
return NS_ERROR_FAILURE;
}
if (MaybeBlockRequest()) {
return NS_ERROR_UNKNOWN_HOST;
}
LOG(("TRR::SendHTTPRequest resolve %s type %u\n", mHost.get(), mType));
nsAutoCString body;
bool disableECS = StaticPrefs::network_trr_disable_ECS();
nsresult rv =
GetOrCreateDNSPacket()->EncodeRequest(body, mHost, mType, disableECS);
if (NS_FAILED(rv)) {
HandleEncodeError(rv);
return rv;
}
bool useGet = StaticPrefs::network_trr_useGET();
nsCOMPtr<nsIURI> dnsURI;
rv = CreateQueryURI(getter_AddRefs(dnsURI));
if (NS_FAILED(rv)) {
LOG(("TRR:SendHTTPRequest: NewURI failed!\n"));
return rv;
}
if (useGet) {
/* For GET requests, the outgoing packet needs to be Base64url-encoded and
then appended to the end of the URI. */
nsAutoCString encoded;
rv = Base64URLEncode(body.Length(),
reinterpret_cast<const unsigned char*>(body.get()),
Base64URLEncodePaddingPolicy::Omit, encoded);
NS_ENSURE_SUCCESS(rv, rv);
nsAutoCString query;
rv = dnsURI->GetQuery(query);
if (NS_FAILED(rv)) {
return rv;
}
if (query.IsEmpty()) {
query.Assign("?dns="_ns);
} else {
query.Append("&dns="_ns);
}
query.Append(encoded);
rv = NS_MutateURI(dnsURI).SetQuery(query).Finalize(dnsURI);
LOG(("TRR::SendHTTPRequest GET dns=%s\n", body.get()));
}
nsCOMPtr<nsIChannel> channel;
bool useOHTTP = StaticPrefs::network_trr_use_ohttp();
if (useOHTTP) {
nsCOMPtr<nsIObliviousHttpService> ohttpService(
do_GetService("@mozilla.org/network/oblivious-http-service;1"));
if (!ohttpService) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsIURI> relayURI;
nsTArray<uint8_t> encodedConfig;
rv = ohttpService->GetTRRSettings(getter_AddRefs(relayURI), encodedConfig);
if (NS_FAILED(rv)) {
return rv;
}
if (!relayURI) {
return NS_ERROR_FAILURE;
}
rv = ohttpService->NewChannel(relayURI, dnsURI, encodedConfig,
getter_AddRefs(channel));
} else {
rv = DNSUtils::CreateChannelHelper(dnsURI, getter_AddRefs(channel));
}
if (NS_FAILED(rv) || !channel) {
LOG(("TRR:SendHTTPRequest: NewChannel failed!\n"));
return rv;
}
auto loadFlags = nsIRequest::LOAD_ANONYMOUS | nsIRequest::INHIBIT_CACHING |
nsIRequest::LOAD_BYPASS_CACHE |
nsIChannel::LOAD_BYPASS_URL_CLASSIFIER;
if (mUseFreshConnection) {
// Causes TRRServiceChannel to tell the connection manager
// to clear out any connection with the current conn info.
loadFlags |= nsIRequest::LOAD_FRESH_CONNECTION;
}
channel->SetLoadFlags(loadFlags);
NS_ENSURE_SUCCESS(rv, rv);
rv = channel->SetNotificationCallbacks(this);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(channel);
if (!httpChannel) {
return NS_ERROR_UNEXPECTED;
}
// This connection should not use TRR
rv = httpChannel->SetTRRMode(nsIRequest::TRR_DISABLED_MODE);
NS_ENSURE_SUCCESS(rv, rv);
nsCString contentType(ContentType());
rv = httpChannel->SetRequestHeader("Accept"_ns, contentType, false);
NS_ENSURE_SUCCESS(rv, rv);
nsAutoCString cred;
if (UseDefaultServer()) {
TRRService::Get()->GetCredentials(cred);
}
if (!cred.IsEmpty()) {
rv = httpChannel->SetRequestHeader("Authorization"_ns, cred, false);
NS_ENSURE_SUCCESS(rv, rv);
}
nsCOMPtr<nsIHttpChannelInternal> internalChannel = do_QueryInterface(channel);
if (!internalChannel) {
return NS_ERROR_UNEXPECTED;
}
// setting a small stream window means the h2 stack won't pipeline a window
// update with each HEADERS or reply to a DATA with a WINDOW UPDATE
rv = internalChannel->SetInitialRwin(127 * 1024);
NS_ENSURE_SUCCESS(rv, rv);
rv = internalChannel->SetIsTRRServiceChannel(true);
NS_ENSURE_SUCCESS(rv, rv);
if (UseDefaultServer() && StaticPrefs::network_trr_async_connInfo()) {
RefPtr<nsHttpConnectionInfo> trrConnInfo =
TRRService::Get()->TRRConnectionInfo();
if (trrConnInfo) {
nsAutoCString host;
dnsURI->GetHost(host);
if (host.Equals(trrConnInfo->GetOrigin())) {
internalChannel->SetConnectionInfo(trrConnInfo);
LOG(("TRR::SendHTTPRequest use conn info:%s\n",
trrConnInfo->HashKey().get()));
} else {
MOZ_DIAGNOSTIC_CRASH("host not equal to trrConnInfo origin");
}
} else {
TRRService::Get()->InitTRRConnectionInfo();
}
}
if (useGet) {
rv = httpChannel->SetRequestMethod("GET"_ns);
NS_ENSURE_SUCCESS(rv, rv);
} else {
nsCOMPtr<nsIUploadChannel2> uploadChannel = do_QueryInterface(httpChannel);
if (!uploadChannel) {
return NS_ERROR_UNEXPECTED;
}
uint32_t streamLength = body.Length();
nsCOMPtr<nsIInputStream> uploadStream;
rv =
NS_NewCStringInputStream(getter_AddRefs(uploadStream), std::move(body));
NS_ENSURE_SUCCESS(rv, rv);
rv = uploadChannel->ExplicitSetUploadStream(uploadStream, contentType,
streamLength, "POST"_ns, false);
NS_ENSURE_SUCCESS(rv, rv);
}
rv = SetupTRRServiceChannelInternal(httpChannel, useGet, contentType);
if (NS_FAILED(rv)) {
return rv;
}
rv = httpChannel->AsyncOpen(this);
if (NS_FAILED(rv)) {
return rv;
}
// If the asyncOpen succeeded we can say that we actually attempted to
// use the TRR connection.
if (mRec) {
mRec->mResolverType = ResolverType();
}
NS_NewTimerWithCallback(
getter_AddRefs(mTimeout), this,
mTimeoutMs ? mTimeoutMs : TRRService::Get()->GetRequestTimeout(),
nsITimer::TYPE_ONE_SHOT);
mChannel = channel;
return NS_OK;
}
// static
nsresult TRR::SetupTRRServiceChannelInternal(nsIHttpChannel* aChannel,
bool aUseGet,
const nsACString& aContentType) {
nsCOMPtr<nsIHttpChannel> httpChannel = aChannel;
MOZ_ASSERT(httpChannel);
nsresult rv = NS_OK;
if (!aUseGet) {
rv =
httpChannel->SetRequestHeader("Cache-Control"_ns, "no-store"_ns, false);
NS_ENSURE_SUCCESS(rv, rv);
}
// Sanitize the request by removing the Accept-Language header so we minimize
// the amount of fingerprintable information we send to the server.
if (!StaticPrefs::network_trr_send_accept_language_headers()) {
rv = httpChannel->SetRequestHeader("Accept-Language"_ns, ""_ns, false);
NS_ENSURE_SUCCESS(rv, rv);
}
// Sanitize the request by removing the User-Agent
if (!StaticPrefs::network_trr_send_user_agent_headers()) {
rv = httpChannel->SetRequestHeader("User-Agent"_ns, ""_ns, false);
NS_ENSURE_SUCCESS(rv, rv);
}
if (StaticPrefs::network_trr_send_empty_accept_encoding_headers()) {
rv = httpChannel->SetEmptyRequestHeader("Accept-Encoding"_ns);
NS_ENSURE_SUCCESS(rv, rv);
}
// set the *default* response content type
if (NS_FAILED(httpChannel->SetContentType(aContentType))) {
LOG(("TRR::SetupTRRServiceChannelInternal: couldn't set content-type!\n"));
}
return NS_OK;
}
NS_IMETHODIMP
TRR::GetInterface(const nsIID& iid, void** result) {
if (!iid.Equals(NS_GET_IID(nsIHttpPushListener))) {
return NS_ERROR_NO_INTERFACE;
}
nsCOMPtr<nsIHttpPushListener> copy(this);
*result = copy.forget().take();
return NS_OK;
}
nsresult TRR::DohDecodeQuery(const nsCString& query, nsCString& host,
enum TrrType& type) {
FallibleTArray<uint8_t> binary;
bool found_dns = false;
LOG(("TRR::DohDecodeQuery %s!\n", query.get()));
// extract "dns=" from the query string
nsAutoCString data;
for (const nsACString& token :
nsCCharSeparatedTokenizer(query, '&').ToRange()) {
nsDependentCSubstring dns = Substring(token, 0, 4);
nsAutoCString check(dns);
if (check.Equals("dns=")) {
nsDependentCSubstring q = Substring(token, 4, -1);
data = q;
found_dns = true;
break;
}
}
if (!found_dns) {
LOG(("TRR::DohDecodeQuery no dns= in pushed URI query string\n"));
return NS_ERROR_ILLEGAL_VALUE;
}
nsresult rv =
Base64URLDecode(data, Base64URLDecodePaddingPolicy::Ignore, binary);
NS_ENSURE_SUCCESS(rv, rv);
uint32_t avail = binary.Length();
if (avail < 12) {
return NS_ERROR_FAILURE;
}
// check the query bit and the opcode
if ((binary[2] & 0xf8) != 0) {
return NS_ERROR_FAILURE;
}
uint32_t qdcount = (binary[4] << 8) + binary[5];
if (!qdcount) {
return NS_ERROR_FAILURE;
}
uint32_t index = 12;
uint32_t length = 0;
host.Truncate();
do {
if (avail < (index + 1)) {
return NS_ERROR_UNEXPECTED;
}
length = binary[index];
if (length) {
if (host.Length()) {
host.Append(".");
}
if (avail < (index + 1 + length)) {
return NS_ERROR_UNEXPECTED;
}
host.Append((const char*)(&binary[0]) + index + 1, length);
}
index += 1 + length; // skip length byte + label
} while (length);
LOG(("TRR::DohDecodeQuery host %s\n", host.get()));
if (avail < (index + 2)) {
return NS_ERROR_UNEXPECTED;
}
uint16_t i16 = 0;
i16 += binary[index] << 8;
i16 += binary[index + 1];
type = (enum TrrType)i16;
LOG(("TRR::DohDecodeQuery type %d\n", (int)type));
return NS_OK;
}
nsresult TRR::ReceivePush(nsIHttpChannel* pushed, nsHostRecord* pushedRec) {
if (!mHostResolver) {
return NS_ERROR_UNEXPECTED;
}
LOG(("TRR::ReceivePush: PUSH incoming!\n"));
nsCOMPtr<nsIURI> uri;
pushed->GetURI(getter_AddRefs(uri));
nsAutoCString query;
if (uri) {
uri->GetQuery(query);
}
if (NS_FAILED(DohDecodeQuery(query, mHost, mType)) ||
HostIsIPLiteral(mHost)) { // literal
LOG(("TRR::ReceivePush failed to decode %s\n", mHost.get()));
return NS_ERROR_UNEXPECTED;
}
if ((mType != TRRTYPE_A) && (mType != TRRTYPE_AAAA) &&
(mType != TRRTYPE_TXT) && (mType != TRRTYPE_HTTPSSVC)) {
LOG(("TRR::ReceivePush unknown type %d\n", mType));
return NS_ERROR_UNEXPECTED;
}
if (TRRService::Get()->IsExcludedFromTRR(mHost)) {
return NS_ERROR_FAILURE;
}
uint32_t type = nsIDNSService::RESOLVE_TYPE_DEFAULT;
if (mType == TRRTYPE_TXT) {
type = nsIDNSService::RESOLVE_TYPE_TXT;
} else if (mType == TRRTYPE_HTTPSSVC) {
type = nsIDNSService::RESOLVE_TYPE_HTTPSSVC;
}
RefPtr<nsHostRecord> hostRecord;
nsresult rv;
rv = mHostResolver->GetHostRecord(
mHost, ""_ns, type, pushedRec->flags, pushedRec->af, pushedRec->pb,
pushedRec->originSuffix, getter_AddRefs(hostRecord));
if (NS_FAILED(rv)) {
return rv;
}
// Since we don't ever call nsHostResolver::NameLookup for this record,
// we need to copy the trr mode from the previous record
if (hostRecord->mEffectiveTRRMode == nsIRequest::TRR_DEFAULT_MODE) {
hostRecord->mEffectiveTRRMode =
static_cast<nsIRequest::TRRMode>(pushedRec->mEffectiveTRRMode);
}
rv = mHostResolver->TrrLookup_unlocked(hostRecord, this);
if (NS_FAILED(rv)) {
return rv;
}
rv = pushed->AsyncOpen(this);
if (NS_FAILED(rv)) {
return rv;
}
// OK!
mChannel = pushed;
mRec.swap(hostRecord);