diff --git a/.prettierignore b/.prettierignore index 94faab9d4795..da211ddbb823 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1424,6 +1424,7 @@ testing/xpcshell/dns-packet/ testing/xpcshell/node_ip/ testing/xpcshell/node-http2/ testing/xpcshell/node-ws/ +testing/xpcshell/odoh-wasm/ third_party/ toolkit/components/certviewer/content/vendor/ toolkit/components/jsoncpp/ diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml index 44a39c8239f5..a96c430d3ab9 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml @@ -12199,6 +12199,42 @@ value: true mirror: always +# Whether to enable odoh. +- name: network.trr.odoh.enabled + type: RelaxedAtomicBool + value: false + mirror: always + +# The uri of Oblivious Proxy. +- name: network.trr.odoh.proxy_uri + type: String + value: "" + mirror: never + +# The host name of Oblivious Target. +- name: network.trr.odoh.target_host + type: String + value: "" + mirror: never + +# The uri path of the odoh uri. +- name: network.trr.odoh.target_path + type: String + value: "" + mirror: never + +# The minimum ttl of the DNS record that contains ODoHConfigs. +- name: network.trr.odoh.min_ttl + type: RelaxedAtomicUint32 + value: 60 + mirror: always + +# The uri indicates where to get ODoHConfigs. +- name: network.trr.odoh.configs_uri + type: String + value: "" + mirror: never + # Whether to add padding in the doh dns queries (rfc 7830) - name: network.trr.padding type: RelaxedAtomicBool diff --git a/netwerk/base/nsLoadGroup.cpp b/netwerk/base/nsLoadGroup.cpp index e3f88fc55467..cd6c89fb5590 100644 --- a/netwerk/base/nsLoadGroup.cpp +++ b/netwerk/base/nsLoadGroup.cpp @@ -1001,6 +1001,23 @@ void nsLoadGroup::TelemetryReportChannel(nsITimedChannel* aTimedChannel, asyncOpen, requestStart); } + if (StaticPrefs::network_trr_odoh_enabled() && !domainLookupStart.IsNull() && + !domainLookupEnd.IsNull()) { + nsCOMPtr dns = do_GetService(NS_DNSSERVICE_CONTRACTID); + bool ODoHActivated = false; + if (dns && NS_SUCCEEDED(dns->GetODoHActivated(&ODoHActivated)) && + ODoHActivated) { + if (aDefaultRequest) { + Telemetry::AccumulateTimeDelta( + Telemetry::HTTP_PAGE_DNS_ODOH_LOOKUP_TIME, domainLookupStart, + domainLookupEnd); + } else { + Telemetry::AccumulateTimeDelta(Telemetry::HTTP_SUB_DNS_ODOH_LOOKUP_TIME, + domainLookupStart, domainLookupEnd); + } + } + } + #undef HTTP_REQUEST_HISTOGRAMS } diff --git a/netwerk/dns/ChildDNSService.cpp b/netwerk/dns/ChildDNSService.cpp index 8ef9f3332934..b171e8047306 100644 --- a/netwerk/dns/ChildDNSService.cpp +++ b/netwerk/dns/ChildDNSService.cpp @@ -386,6 +386,14 @@ ChildDNSService::GetMyHostName(nsACString& result) { return NS_ERROR_NOT_AVAILABLE; } +NS_IMETHODIMP +ChildDNSService::GetODoHActivated(bool* aResult) { + NS_ENSURE_ARG(aResult); + + *aResult = mODoHActivated; + return NS_OK; +} + void ChildDNSService::NotifyRequestDone(DNSRequestSender* aDnsRequest) { // We need the original flags and listener for the pending requests hash. nsIDNSService::DNSFlags originalFlags = @@ -430,6 +438,12 @@ nsresult ChildDNSService::Init() { AddPrefObserver(prefs); } + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->AddObserver(this, "odoh-service-activated", false); + } + return NS_OK; } @@ -475,7 +489,10 @@ ChildDNSService::Observe(nsISupports* subject, const char* topic, if (!strcmp(topic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { // Reread prefs ReadPrefs(NS_ConvertUTF16toUTF8(data).get()); + } else if (!strcmp(topic, "odoh-service-activated")) { + mODoHActivated = u"true"_ns.Equals(data); } + return NS_OK; } diff --git a/netwerk/dns/ChildDNSService.h b/netwerk/dns/ChildDNSService.h index 611f8b871f49..35b2bf30f9dd 100644 --- a/netwerk/dns/ChildDNSService.h +++ b/netwerk/dns/ChildDNSService.h @@ -60,6 +60,8 @@ class ChildDNSService final : public DNSServiceBase, public nsPIDNSService { nsIDNSListener* aListener, nsresult aReason, const OriginAttributes& aOriginAttributes); + bool mODoHActivated = false; + // We need to remember pending dns requests to be able to cancel them. nsClassHashtable>> mPendingRequests; diff --git a/netwerk/dns/DNS.h b/netwerk/dns/DNS.h index a0dbbaa17db7..838e16980183 100644 --- a/netwerk/dns/DNS.h +++ b/netwerk/dns/DNS.h @@ -156,7 +156,39 @@ union NetAddr { nsCString ToString() const; }; -enum class DNSResolverType : uint32_t { Native = 0, TRR }; +#define ODOH_VERSION 0x0001 +static const char kODoHQuery[] = "odoh query"; +static const char hODoHConfigID[] = "odoh key id"; +static const char kODoHResponse[] = "odoh response"; +static const char kODoHKey[] = "odoh key"; +static const char kODoHNonce[] = "odoh nonce"; + +struct ObliviousDoHConfigContents { + uint16_t mKemId{}; + uint16_t mKdfId{}; + uint16_t mAeadId{}; + nsTArray mPublicKey; +}; + +struct ObliviousDoHConfig { + uint16_t mVersion{}; + uint16_t mLength{}; + ObliviousDoHConfigContents mContents; + nsTArray mConfigId; +}; + +enum ObliviousDoHMessageType : uint8_t { + ODOH_QUERY = 1, + ODOH_RESPONSE = 2, +}; + +struct ObliviousDoHMessage { + ObliviousDoHMessageType mType{ODOH_QUERY}; + nsTArray mKeyId; + nsTArray mEncryptedMessage; +}; + +enum class DNSResolverType : uint32_t { Native = 0, TRR, ODoH }; class AddrInfo { NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AddrInfo) @@ -184,7 +216,10 @@ class AddrInfo { size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const; - bool IsTRR() const { return mResolverType == DNSResolverType::TRR; } + bool IsTRROrODoH() const { + return mResolverType == DNSResolverType::TRR || + mResolverType == DNSResolverType::ODoH; + } DNSResolverType ResolverType() const { return mResolverType; } unsigned int TRRType() { return mTRRType; } diff --git a/netwerk/dns/DNSPacket.cpp b/netwerk/dns/DNSPacket.cpp index b41403214aa0..8d470cf00776 100644 --- a/netwerk/dns/DNSPacket.cpp +++ b/netwerk/dns/DNSPacket.cpp @@ -8,6 +8,7 @@ #include "mozilla/EndianUtils.h" #include "mozilla/ScopeExit.h" #include "mozilla/StaticPrefs_network.h" +#include "ODoHService.h" // Put DNSLogging.h at the end to avoid LOG being overwritten by other headers. #include "DNSLogging.h" @@ -1076,5 +1077,552 @@ nsresult DNSPacket::Decode( return rv; } +static SECItem* CreateRawConfig(const ObliviousDoHConfig& aConfig) { + SECItem* item(::SECITEM_AllocItem(nullptr, nullptr, + 8 + aConfig.mContents.mPublicKey.Length())); + if (!item) { + return nullptr; + } + + uint16_t index = 0; + NetworkEndian::writeUint16(&item->data[index], aConfig.mContents.mKemId); + index += 2; + NetworkEndian::writeUint16(&item->data[index], aConfig.mContents.mKdfId); + index += 2; + NetworkEndian::writeUint16(&item->data[index], aConfig.mContents.mAeadId); + index += 2; + uint16_t keyLength = aConfig.mContents.mPublicKey.Length(); + NetworkEndian::writeUint16(&item->data[index], keyLength); + index += 2; + memcpy(&item->data[index], aConfig.mContents.mPublicKey.Elements(), + aConfig.mContents.mPublicKey.Length()); + return item; +} + +static bool CreateConfigId(ObliviousDoHConfig& aConfig) { + SECStatus rv; + CK_HKDF_PARAMS params = {0}; + SECItem paramsi = {siBuffer, (unsigned char*)¶ms, sizeof(params)}; + + UniquePK11SlotInfo slot(PK11_GetInternalSlot()); + if (!slot) { + return false; + } + + UniqueSECItem rawConfig(CreateRawConfig(aConfig)); + if (!rawConfig) { + return false; + } + + UniquePK11SymKey configKey(PK11_ImportDataKey(slot.get(), CKM_HKDF_DATA, + PK11_OriginUnwrap, CKA_DERIVE, + rawConfig.get(), nullptr)); + if (!configKey) { + return false; + } + + params.bExtract = CK_TRUE; + params.bExpand = CK_TRUE; + params.prfHashMechanism = CKM_SHA256; + params.ulSaltType = CKF_HKDF_SALT_NULL; + params.pInfo = (unsigned char*)&hODoHConfigID[0]; + params.ulInfoLen = strlen(hODoHConfigID); + UniquePK11SymKey derived(PK11_DeriveWithFlags( + configKey.get(), CKM_HKDF_DATA, ¶msi, CKM_HKDF_DERIVE, CKA_DERIVE, + SHA256_LENGTH, CKF_SIGN | CKF_VERIFY)); + + rv = PK11_ExtractKeyValue(derived.get()); + if (rv != SECSuccess) { + return false; + } + + SECItem* derivedItem = PK11_GetKeyData(derived.get()); + if (!derivedItem) { + return false; + } + + if (derivedItem->len != SHA256_LENGTH) { + return false; + } + + aConfig.mConfigId.AppendElements(derivedItem->data, derivedItem->len); + return true; +} + +// static +bool ODoHDNSPacket::ParseODoHConfigs(Span aData, + nsTArray& aOut) { + // struct { + // uint16 kem_id; + // uint16 kdf_id; + // uint16 aead_id; + // opaque public_key<1..2^16-1>; + // } ObliviousDoHConfigContents; + // + // struct { + // uint16 version; + // uint16 length; + // select (ObliviousDoHConfig.version) { + // case 0xff03: ObliviousDoHConfigContents contents; + // } + // } ObliviousDoHConfig; + // + // ObliviousDoHConfig ObliviousDoHConfigs<1..2^16-1>; + + Span::const_iterator it = aData.begin(); + uint16_t length = 0; + if (!get16bit(aData, it, length)) { + return false; + } + + if (length != aData.Length() - 2) { + return false; + } + + nsTArray result; + static const int kMinimumConfigContentLength = 12; + while (std::distance(it, aData.cend()) > kMinimumConfigContentLength) { + ObliviousDoHConfig config; + if (!get16bit(aData, it, config.mVersion)) { + return false; + } + + if (!get16bit(aData, it, config.mLength)) { + return false; + } + + if (std::distance(it, aData.cend()) < config.mLength) { + return false; + } + + if (!get16bit(aData, it, config.mContents.mKemId)) { + return false; + } + + if (!get16bit(aData, it, config.mContents.mKdfId)) { + return false; + } + + if (!get16bit(aData, it, config.mContents.mAeadId)) { + return false; + } + + uint16_t keyLength = 0; + if (!get16bit(aData, it, keyLength)) { + return false; + } + + if (!keyLength || std::distance(it, aData.cend()) < keyLength) { + return false; + } + + config.mContents.mPublicKey.AppendElements(Span(it, it + keyLength)); + it += keyLength; + + CreateConfigId(config); + + // Check if the version of the config is supported and validate its content. + if (config.mVersion == ODOH_VERSION && + PK11_HPKE_ValidateParameters( + static_cast(config.mContents.mKemId), + static_cast(config.mContents.mKdfId), + static_cast(config.mContents.mAeadId)) == SECSuccess) { + result.AppendElement(std::move(config)); + } else { + LOG(("ODoHDNSPacket::ParseODoHConfigs got an invalid config")); + } + } + + aOut = std::move(result); + return true; +} + +ODoHDNSPacket::~ODoHDNSPacket() { PK11_HPKE_DestroyContext(mContext, true); } + +nsresult ODoHDNSPacket::EncodeRequest(nsCString& aBody, const nsACString& aHost, + uint16_t aType, bool aDisableECS) { + nsAutoCString queryBody; + nsresult rv = DNSPacket::EncodeRequest(queryBody, aHost, aType, aDisableECS); + if (NS_FAILED(rv)) { + SetDNSPacketStatus(DNSPacketStatus::EncodeError); + return rv; + } + + if (!gODoHService->ODoHConfigs()) { + SetDNSPacketStatus(DNSPacketStatus::KeyNotAvailable); + return NS_ERROR_FAILURE; + } + + if (gODoHService->ODoHConfigs()->IsEmpty()) { + SetDNSPacketStatus(DNSPacketStatus::KeyNotUsable); + return NS_ERROR_FAILURE; + } + + // We only use the first ODoHConfig. + const ObliviousDoHConfig& config = (*gODoHService->ODoHConfigs())[0]; + + ObliviousDoHMessage message; + // The spec didn't recommand padding length for encryption, let's use 0 here. + if (!EncryptDNSQuery(queryBody, 0, config, message)) { + SetDNSPacketStatus(DNSPacketStatus::EncryptError); + return NS_ERROR_FAILURE; + } + + aBody.Truncate(); + aBody += message.mType; + uint16_t keyIdLength = message.mKeyId.Length(); + aBody += static_cast(keyIdLength >> 8); + aBody += static_cast(keyIdLength); + aBody.Append(reinterpret_cast(message.mKeyId.Elements()), + keyIdLength); + uint16_t messageLen = message.mEncryptedMessage.Length(); + aBody += static_cast(messageLen >> 8); + aBody += static_cast(messageLen); + aBody.Append( + reinterpret_cast(message.mEncryptedMessage.Elements()), + messageLen); + + SetDNSPacketStatus(DNSPacketStatus::Success); + return NS_OK; +} + +/* + * def encrypt_query_body(pkR, key_id, Q_plain): + * enc, context = SetupBaseS(pkR, "odoh query") + * aad = 0x01 || len(key_id) || key_id + * ct = context.Seal(aad, Q_plain) + * Q_encrypted = enc || ct + * return Q_encrypted + */ +bool ODoHDNSPacket::EncryptDNSQuery(const nsACString& aQuery, + uint16_t aPaddingLen, + const ObliviousDoHConfig& aConfig, + ObliviousDoHMessage& aOut) { + mContext = PK11_HPKE_NewContext( + static_cast(aConfig.mContents.mKemId), + static_cast(aConfig.mContents.mKdfId), + static_cast(aConfig.mContents.mAeadId), nullptr, nullptr); + if (!mContext) { + LOG(("ODoHDNSPacket::EncryptDNSQuery create context failed")); + return false; + } + + SECKEYPublicKey* pkR; + SECStatus rv = + PK11_HPKE_Deserialize(mContext, aConfig.mContents.mPublicKey.Elements(), + aConfig.mContents.mPublicKey.Length(), &pkR); + if (rv != SECSuccess) { + return false; + } + + UniqueSECItem hpkeInfo( + ::SECITEM_AllocItem(nullptr, nullptr, strlen(kODoHQuery))); + if (!hpkeInfo) { + return false; + } + + memcpy(hpkeInfo->data, kODoHQuery, strlen(kODoHQuery)); + + rv = PK11_HPKE_SetupS(mContext, nullptr, nullptr, pkR, hpkeInfo.get()); + if (rv != SECSuccess) { + LOG(("ODoHDNSPacket::EncryptDNSQuery setupS failed")); + return false; + } + + const SECItem* hpkeEnc = PK11_HPKE_GetEncapPubKey(mContext); + if (!hpkeEnc) { + return false; + } + + // aad = 0x01 || len(key_id) || key_id + UniqueSECItem aad(::SECITEM_AllocItem(nullptr, nullptr, + 1 + 2 + aConfig.mConfigId.Length())); + if (!aad) { + return false; + } + + aad->data[0] = ODOH_QUERY; + NetworkEndian::writeUint16(&aad->data[1], aConfig.mConfigId.Length()); + memcpy(&aad->data[3], aConfig.mConfigId.Elements(), + aConfig.mConfigId.Length()); + + // struct { + // opaque dns_message<1..2^16-1>; + // opaque padding<0..2^16-1>; + // } ObliviousDoHMessagePlaintext; + SECItem* odohPlainText(::SECITEM_AllocItem( + nullptr, nullptr, 2 + aQuery.Length() + 2 + aPaddingLen)); + if (!odohPlainText) { + return false; + } + + mPlainQuery.reset(odohPlainText); + memset(mPlainQuery->data, 0, mPlainQuery->len); + NetworkEndian::writeUint16(&mPlainQuery->data[0], aQuery.Length()); + memcpy(&mPlainQuery->data[2], aQuery.BeginReading(), aQuery.Length()); + NetworkEndian::writeUint16(&mPlainQuery->data[2 + aQuery.Length()], + aPaddingLen); + + SECItem* chCt = nullptr; + rv = PK11_HPKE_Seal(mContext, aad.get(), mPlainQuery.get(), &chCt); + if (rv != SECSuccess) { + LOG(("ODoHDNSPacket::EncryptDNSQuery seal failed")); + return false; + } + + UniqueSECItem ct(chCt); + + aOut.mType = ODOH_QUERY; + aOut.mKeyId.AppendElements(aConfig.mConfigId); + aOut.mEncryptedMessage.AppendElements(Span(hpkeEnc->data, hpkeEnc->len)); + aOut.mEncryptedMessage.AppendElements(Span(ct->data, ct->len)); + + return true; +} + +nsresult ODoHDNSPacket::Decode( + nsCString& aHost, enum TrrType aType, nsCString& aCname, bool aAllowRFC1918, + DOHresp& aResp, TypeRecordResultType& aTypeResult, + nsClassHashtable& aAdditionalRecords, + uint32_t& aTTL) { + // This function could be called multiple times when we are checking CNAME + // records, but we only need to decrypt the response once. + if (!mDecryptedResponseRange) { + if (!DecryptDNSResponse()) { + SetDNSPacketStatus(DNSPacketStatus::DecryptError); + return NS_ERROR_FAILURE; + } + + uint32_t index = 0; + uint16_t responseLength = get16bit(mResponse, index); + index += 2; + + if (mBodySize < (index + responseLength)) { + SetDNSPacketStatus(DNSPacketStatus::DecryptError); + return NS_ERROR_ILLEGAL_VALUE; + } + + DecryptedResponseRange range; + range.mStart = index; + range.mLength = responseLength; + + index += responseLength; + uint16_t paddingLen = get16bit(mResponse, index); + + if (static_cast(4 + responseLength + paddingLen) != + mBodySize) { + SetDNSPacketStatus(DNSPacketStatus::DecryptError); + return NS_ERROR_ILLEGAL_VALUE; + } + + mDecryptedResponseRange.emplace(range); + } + + nsresult rv = DecodeInternal(aHost, aType, aCname, aAllowRFC1918, aResp, + aTypeResult, aAdditionalRecords, aTTL, + &mResponse[mDecryptedResponseRange->mStart], + mDecryptedResponseRange->mLength); + SetDNSPacketStatus(NS_SUCCEEDED(rv) ? DNSPacketStatus::Success + : DNSPacketStatus::DecodeError); + return rv; +} + +static bool CreateObliviousDoHMessage(const unsigned char* aData, + unsigned int aLength, + ObliviousDoHMessage& aOut) { + if (aLength < 5) { + return false; + } + + unsigned int index = 0; + aOut.mType = static_cast(aData[index++]); + + uint16_t keyIdLength = get16bit(aData, index); + index += 2; + if (aLength < (index + keyIdLength)) { + return false; + } + + aOut.mKeyId.AppendElements(Span(aData + index, keyIdLength)); + index += keyIdLength; + + uint16_t messageLen = get16bit(aData, index); + index += 2; + if (aLength < (index + messageLen)) { + return false; + } + + aOut.mEncryptedMessage.AppendElements(Span(aData + index, messageLen)); + return true; +} + +static SECStatus HKDFExtract(SECItem* aSalt, PK11SymKey* aIkm, + UniquePK11SymKey& aOutKey) { + CK_HKDF_PARAMS params = {0}; + SECItem paramsItem = {siBuffer, (unsigned char*)¶ms, sizeof(params)}; + + params.bExtract = CK_TRUE; + params.bExpand = CK_FALSE; + params.prfHashMechanism = CKM_SHA256; + params.ulSaltType = aSalt ? CKF_HKDF_SALT_DATA : CKF_HKDF_SALT_NULL; + params.pSalt = aSalt ? (CK_BYTE_PTR)aSalt->data : nullptr; + params.ulSaltLen = aSalt ? aSalt->len : 0; + + UniquePK11SymKey prk(PK11_Derive(aIkm, CKM_HKDF_DERIVE, ¶msItem, + CKM_HKDF_DERIVE, CKA_DERIVE, 0)); + if (!prk) { + return SECFailure; + } + + aOutKey.swap(prk); + return SECSuccess; +} + +static SECStatus HKDFExpand(PK11SymKey* aPrk, const SECItem* aInfo, int aLen, + bool aKey, UniquePK11SymKey& aOutKey) { + CK_HKDF_PARAMS params = {0}; + SECItem paramsItem = {siBuffer, (unsigned char*)¶ms, sizeof(params)}; + + params.bExtract = CK_FALSE; + params.bExpand = CK_TRUE; + params.prfHashMechanism = CKM_SHA256; + params.ulSaltType = CKF_HKDF_SALT_NULL; + params.pInfo = (CK_BYTE_PTR)aInfo->data; + params.ulInfoLen = aInfo->len; + CK_MECHANISM_TYPE deriveMech = CKM_HKDF_DERIVE; + CK_MECHANISM_TYPE keyMech = aKey ? CKM_AES_GCM : CKM_HKDF_DERIVE; + + UniquePK11SymKey derivedKey( + PK11_Derive(aPrk, deriveMech, ¶msItem, keyMech, CKA_DERIVE, aLen)); + if (!derivedKey) { + return SECFailure; + } + + aOutKey.swap(derivedKey); + return SECSuccess; +} + +/* + * def decrypt_response_body(context, Q_plain, R_encrypted, response_nonce): + * aead_key, aead_nonce = derive_secrets(context, Q_plain, response_nonce) + * aad = 0x02 || len(response_nonce) || response_nonce + * R_plain, error = Open(key, nonce, aad, R_encrypted) + * return R_plain, error + */ +bool ODoHDNSPacket::DecryptDNSResponse() { + ObliviousDoHMessage message; + if (!CreateObliviousDoHMessage(mResponse, mBodySize, message)) { + LOG(("ODoHDNSPacket::DecryptDNSResponse invalid response")); + return false; + } + + if (message.mType != ODOH_RESPONSE) { + return false; + } + + const unsigned int kResponseNonceLen = 16; + // KeyId is actually response_nonce + if (message.mKeyId.Length() != kResponseNonceLen) { + return false; + } + + // def derive_secrets(context, Q_plain, response_nonce): + // secret = context.Export("odoh response", Nk) + // salt = Q_plain || len(response_nonce) || response_nonce + // prk = Extract(salt, secret) + // key = Expand(odoh_prk, "odoh key", Nk) + // nonce = Expand(odoh_prk, "odoh nonce", Nn) + // return key, nonce + const SECItem kODoHResponsetInfoItem = { + siBuffer, (unsigned char*)kODoHResponse, + static_cast(strlen(kODoHResponse))}; + const unsigned int kAes128GcmKeyLen = 16; + const unsigned int kAes128GcmNonceLen = 12; + PK11SymKey* tmp = nullptr; + SECStatus rv = PK11_HPKE_ExportSecret(mContext, &kODoHResponsetInfoItem, + kAes128GcmKeyLen, &tmp); + if (rv != SECSuccess) { + LOG(("ODoHDNSPacket::DecryptDNSResponse export secret failed")); + return false; + } + UniquePK11SymKey odohSecret(tmp); + + SECItem* salt(::SECITEM_AllocItem(nullptr, nullptr, + mPlainQuery->len + 2 + kResponseNonceLen)); + memcpy(salt->data, mPlainQuery->data, mPlainQuery->len); + NetworkEndian::writeUint16(&salt->data[mPlainQuery->len], kResponseNonceLen); + memcpy(salt->data + mPlainQuery->len + 2, message.mKeyId.Elements(), + kResponseNonceLen); + UniqueSECItem st(salt); + UniquePK11SymKey odohPrk; + rv = HKDFExtract(salt, odohSecret.get(), odohPrk); + if (rv != SECSuccess) { + LOG(("ODoHDNSPacket::DecryptDNSResponse extract failed")); + return false; + } + + SECItem keyInfoItem = {siBuffer, (unsigned char*)&kODoHKey[0], + static_cast(strlen(kODoHKey))}; + UniquePK11SymKey key; + rv = HKDFExpand(odohPrk.get(), &keyInfoItem, kAes128GcmKeyLen, true, key); + if (rv != SECSuccess) { + LOG(("ODoHDNSPacket::DecryptDNSResponse expand key failed")); + return false; + } + + SECItem nonceInfoItem = {siBuffer, (unsigned char*)&kODoHNonce[0], + static_cast(strlen(kODoHNonce))}; + UniquePK11SymKey nonce; + rv = HKDFExpand(odohPrk.get(), &nonceInfoItem, kAes128GcmNonceLen, false, + nonce); + if (rv != SECSuccess) { + LOG(("ODoHDNSPacket::DecryptDNSResponse expand nonce failed")); + return false; + } + + rv = PK11_ExtractKeyValue(nonce.get()); + if (rv != SECSuccess) { + return false; + } + + SECItem* derivedItem = PK11_GetKeyData(nonce.get()); + if (!derivedItem) { + return false; + } + + // aad = 0x02 || len(response_nonce) || response_nonce + SECItem* aadItem( + ::SECITEM_AllocItem(nullptr, nullptr, 1 + 2 + kResponseNonceLen)); + aadItem->data[0] = ODOH_RESPONSE; + NetworkEndian::writeUint16(&aadItem->data[1], kResponseNonceLen); + memcpy(&aadItem->data[3], message.mKeyId.Elements(), kResponseNonceLen); + UniqueSECItem aad(aadItem); + + SECItem paramItem; + CK_GCM_PARAMS param; + param.pIv = derivedItem->data; + param.ulIvLen = derivedItem->len; + param.ulIvBits = param.ulIvLen * 8; + param.ulTagBits = 16 * 8; + param.pAAD = (CK_BYTE_PTR)aad->data; + param.ulAADLen = aad->len; + + paramItem.type = siBuffer; + paramItem.data = (unsigned char*)(¶m); + paramItem.len = sizeof(CK_GCM_PARAMS); + + memset(mResponse, 0, mBodySize); + rv = PK11_Decrypt(key.get(), CKM_AES_GCM, ¶mItem, mResponse, &mBodySize, + MAX_SIZE, message.mEncryptedMessage.Elements(), + message.mEncryptedMessage.Length()); + if (rv != SECSuccess) { + LOG(("ODoHDNSPacket::DecryptDNSResponse decrypt failed %d", + PORT_GetError())); + return false; + } + + return true; +} + } // namespace net } // namespace mozilla diff --git a/netwerk/dns/DNSPacket.h b/netwerk/dns/DNSPacket.h index 12df52c1d751..46b85b806600 100644 --- a/netwerk/dns/DNSPacket.h +++ b/netwerk/dns/DNSPacket.h @@ -106,6 +106,39 @@ class DNSPacket { Maybe mOriginHost; }; +class ODoHDNSPacket final : public DNSPacket { + public: + ODoHDNSPacket() = default; + virtual ~ODoHDNSPacket(); + + static bool ParseODoHConfigs(Span aData, + nsTArray& aOut); + + virtual nsresult EncodeRequest(nsCString& aBody, const nsACString& aHost, + uint16_t aType, bool aDisableECS) override; + + virtual nsresult Decode( + nsCString& aHost, enum TrrType aType, nsCString& aCname, + bool aAllowRFC1918, DOHresp& aResp, TypeRecordResultType& aTypeResult, + nsClassHashtable& aAdditionalRecords, + uint32_t& aTTL) override; + + protected: + bool EncryptDNSQuery(const nsACString& aQuery, uint16_t aPaddingLen, + const ObliviousDoHConfig& aConfig, + ObliviousDoHMessage& aOut); + bool DecryptDNSResponse(); + + HpkeContext* mContext = nullptr; + UniqueSECItem mPlainQuery; + // This struct indicates the range of decrypted responses stored in mResponse. + struct DecryptedResponseRange { + uint16_t mStart = 0; + uint16_t mLength = 0; + }; + Maybe mDecryptedResponseRange; +}; + } // namespace net } // namespace mozilla diff --git a/netwerk/dns/DNSUtils.h b/netwerk/dns/DNSUtils.h index f5b53c5d7232..5a89e6c64691 100644 --- a/netwerk/dns/DNSUtils.h +++ b/netwerk/dns/DNSUtils.h @@ -16,11 +16,13 @@ namespace mozilla { namespace net { class NetworkConnectivityService; +class ODoHService; class TRR; class DNSUtils final { private: friend class NetworkConnectivityService; + friend class ODoHService; friend class ObliviousHttpService; friend class TRR; static nsresult CreateChannelHelper(nsIURI* aUri, nsIChannel** aResult); diff --git a/netwerk/dns/ODoH.cpp b/netwerk/dns/ODoH.cpp new file mode 100644 index 000000000000..dd752f74e290 --- /dev/null +++ b/netwerk/dns/ODoH.cpp @@ -0,0 +1,112 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 : */ +/* 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/. */ + +#include "ODoH.h" + +#include "mozilla/Base64.h" +#include "nsIURIMutator.h" +#include "ODoHService.h" +#include "TRRService.h" +// Put DNSLogging.h at the end to avoid LOG being overwritten by other headers. +#include "DNSLogging.h" +#include "nsNetUtil.h" + +namespace mozilla { +namespace net { + +NS_IMETHODIMP +ODoH::Run() { + if (!gODoHService) { + RecordReason(TRRSkippedReason::TRR_SEND_FAILED); + FailData(NS_ERROR_FAILURE); + return NS_OK; + } + + if (!gODoHService->ODoHConfigs()) { + LOG(( + "ODoH::Run ODoHConfigs is not available mTriedDownloadODoHConfigs=%d\n", + mTriedDownloadODoHConfigs)); + // Make this lookup fail if we don't have a valid ODoHConfig and we already + // tried before. + if (NS_SUCCEEDED(gODoHService->UpdateODoHConfig()) && + !mTriedDownloadODoHConfigs) { + gODoHService->AppendPendingODoHRequest(this); + mTriedDownloadODoHConfigs = true; + } else { + RecordReason(TRRSkippedReason::ODOH_UPDATE_KEY_FAILED); + FailData(NS_ERROR_FAILURE); + return NS_OK; + } + return NS_OK; + } + + return TRR::Run(); +} + +DNSPacket* ODoH::GetOrCreateDNSPacket() { + if (!mPacket) { + mPacket = MakeUnique(); + } + + return mPacket.get(); +} + +nsresult ODoH::CreateQueryURI(nsIURI** aOutURI) { + nsAutoCString uri; + nsCOMPtr dnsURI; + gODoHService->GetRequestURI(uri); + + nsresult rv = NS_NewURI(getter_AddRefs(dnsURI), uri); + if (NS_FAILED(rv)) { + return rv; + } + + dnsURI.forget(aOutURI); + return NS_OK; +} + +void ODoH::HandleTimeout() { + // If this request is still in the pending queue, it means we can't get the + // ODoHConfigs within the timeout. + if (gODoHService->RemovePendingODoHRequest(this)) { + RecordReason(TRRSkippedReason::ODOH_KEY_NOT_AVAILABLE); + } + + TRR::HandleTimeout(); +} + +void ODoH::HandleEncodeError(nsresult aStatusCode) { + MOZ_ASSERT(NS_FAILED(aStatusCode)); + + DNSPacketStatus status = mPacket->PacketStatus(); + MOZ_ASSERT(status != DNSPacketStatus::Success); + + if (status == DNSPacketStatus::KeyNotAvailable) { + RecordReason(TRRSkippedReason::ODOH_KEY_NOT_AVAILABLE); + } else if (status == DNSPacketStatus::KeyNotUsable) { + RecordReason(TRRSkippedReason::ODOH_KEY_NOT_USABLE); + } else if (status == DNSPacketStatus::EncryptError) { + RecordReason(TRRSkippedReason::ODOH_ENCRYPTION_FAILED); + } else { + MOZ_ASSERT_UNREACHABLE("Unexpected status code."); + } +} + +void ODoH::HandleDecodeError(nsresult aStatusCode) { + MOZ_ASSERT(NS_FAILED(aStatusCode)); + + DNSPacketStatus status = mPacket->PacketStatus(); + MOZ_ASSERT(status != DNSPacketStatus::Success); + + if (status == DNSPacketStatus::DecryptError) { + RecordReason(TRRSkippedReason::ODOH_DECRYPTION_FAILED); + } + + TRR::HandleDecodeError(aStatusCode); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/dns/ODoH.h b/netwerk/dns/ODoH.h new file mode 100644 index 000000000000..385ac83bd583 --- /dev/null +++ b/netwerk/dns/ODoH.h @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 : */ +/* 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/. */ + +#ifndef mozilla_net_ODoH_h +#define mozilla_net_ODoH_h + +#include "TRR.h" + +namespace mozilla { +namespace net { + +class ODoH final : public TRR { + public: + explicit ODoH(AHostResolver* aResolver, nsHostRecord* aRec, + enum TrrType aType) + : TRR(aResolver, aRec, aType) {} + // when following CNAMEs + explicit ODoH(AHostResolver* aResolver, nsHostRecord* aRec, nsCString& aHost, + enum TrrType& aType, unsigned int aLoopCount, bool aPB) + : TRR(aResolver, aRec, aHost, aType, aLoopCount, aPB) {} + NS_IMETHOD Run() override; + // ODoH should not support push. + NS_IMETHOD GetInterface(const nsIID&, void**) override { + return NS_ERROR_NO_INTERFACE; + } + + protected: + virtual ~ODoH() = default; + virtual DNSPacket* GetOrCreateDNSPacket() override; + virtual nsresult CreateQueryURI(nsIURI** aOutURI) override; + virtual const char* ContentType() const override { + return "application/oblivious-dns-message"; + } + virtual DNSResolverType ResolverType() const override { + return DNSResolverType::ODoH; + } + virtual bool MaybeBlockRequest() override { + // TODO: check excluded list + return false; + }; + virtual void RecordProcessingTime(nsIChannel* aChannel) override { + // TODO: record processing time for ODoH. + } + virtual void ReportStatus(nsresult aStatusCode) override { + // TODO: record status in ODoHService. + } + virtual void HandleTimeout() override; + virtual void HandleEncodeError(nsresult aStatusCode) override; + virtual void HandleDecodeError(nsresult aStatusCode) override; + + bool mTriedDownloadODoHConfigs = false; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/dns/ODoHService.cpp b/netwerk/dns/ODoHService.cpp new file mode 100644 index 000000000000..ce34a8ad892e --- /dev/null +++ b/netwerk/dns/ODoHService.cpp @@ -0,0 +1,521 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "ODoHService.h" + +#include "DNSUtils.h" +#include "mozilla/net/SocketProcessChild.h" +#include "mozilla/Preferences.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_network.h" +#include "nsICancelable.h" +#include "nsIDNSAdditionalInfo.h" +#include "nsIDNSService.h" +#include "nsIDNSByTypeRecord.h" +#include "nsIOService.h" +#include "nsIObserverService.h" +#include "nsNetUtil.h" +#include "ODoH.h" +#include "TRRService.h" +#include "nsURLHelper.h" +// Put DNSLogging.h at the end to avoid LOG being overwritten by other headers. +#include "DNSLogging.h" + +static const char kODoHProxyURIPref[] = "network.trr.odoh.proxy_uri"; +static const char kODoHTargetHostPref[] = "network.trr.odoh.target_host"; +static const char kODoHTargetPathPref[] = "network.trr.odoh.target_path"; +static const char kODoHConfigsUriPref[] = "network.trr.odoh.configs_uri"; + +namespace mozilla { +namespace net { + +ODoHService* gODoHService = nullptr; + +NS_IMPL_ISUPPORTS(ODoHService, nsIDNSListener, nsIObserver, + nsISupportsWeakReference, nsITimerCallback, nsINamed, + nsIStreamLoaderObserver) + +ODoHService::ODoHService() + : mLock("net::ODoHService"), mQueryODoHConfigInProgress(false) { + gODoHService = this; +} + +ODoHService::~ODoHService() { gODoHService = nullptr; } + +bool ODoHService::Init() { + MOZ_ASSERT(NS_IsMainThread(), "wrong thread"); + + nsCOMPtr prefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (!prefBranch) { + return false; + } + + prefBranch->AddObserver(kODoHProxyURIPref, this, true); + prefBranch->AddObserver(kODoHTargetHostPref, this, true); + prefBranch->AddObserver(kODoHTargetPathPref, this, true); + prefBranch->AddObserver(kODoHConfigsUriPref, this, true); + + ReadPrefs(nullptr); + + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->AddObserver(this, "xpcom-shutdown-threads", true); + } + + return true; +} + +bool ODoHService::Enabled() const { + return StaticPrefs::network_trr_odoh_enabled(); +} + +NS_IMETHODIMP +ODoHService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(NS_IsMainThread(), "wrong thread"); + if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { + ReadPrefs(NS_ConvertUTF16toUTF8(aData).get()); + } else if (!strcmp(aTopic, "xpcom-shutdown-threads")) { + if (mTTLTimer) { + mTTLTimer->Cancel(); + mTTLTimer = nullptr; + } + } + + return NS_OK; +} + +nsresult ODoHService::ReadPrefs(const char* aName) { + if (!aName || !strcmp(aName, kODoHConfigsUriPref)) { + OnODohConfigsURIChanged(); + } + if (!aName || !strcmp(aName, kODoHProxyURIPref) || + !strcmp(aName, kODoHTargetHostPref) || + !strcmp(aName, kODoHTargetPathPref)) { + OnODoHPrefsChange(aName == nullptr); + } + + return NS_OK; +} + +void ODoHService::OnODohConfigsURIChanged() { + nsAutoCString uri; + Preferences::GetCString(kODoHConfigsUriPref, uri); + + bool updateConfig = false; + { + MutexAutoLock lock(mLock); + if (!mODoHConfigsUri.Equals(uri)) { + mODoHConfigsUri = uri; + updateConfig = true; + } + } + + if (updateConfig) { + UpdateODoHConfigFromURI(); + } +} + +void ODoHService::OnODoHPrefsChange(bool aInit) { + nsAutoCString proxyURI; + Preferences::GetCString(kODoHProxyURIPref, proxyURI); + nsAutoCString targetHost; + Preferences::GetCString(kODoHTargetHostPref, targetHost); + nsAutoCString targetPath; + Preferences::GetCString(kODoHTargetPathPref, targetPath); + + bool updateODoHConfig = false; + { + MutexAutoLock lock(mLock); + mODoHProxyURI = proxyURI; + // Only update ODoHConfig when the host is really changed. + if (!mODoHTargetHost.Equals(targetHost) && mODoHConfigsUri.IsEmpty()) { + updateODoHConfig = true; + } + mODoHTargetHost = targetHost; + mODoHTargetPath = targetPath; + + BuildODoHRequestURI(); + } + + if (updateODoHConfig) { + // When this function is called from ODoHService::Init(), it's on the same + // call stack as nsDNSService is inited. In this case, we need to dispatch + // UpdateODoHConfigFromHTTPSRR(), since recursively getting DNS service is + // not allowed. + auto task = []() { gODoHService->UpdateODoHConfigFromHTTPSRR(); }; + if (aInit) { + NS_DispatchToMainThread(NS_NewRunnableFunction( + "ODoHService::UpdateODoHConfigFromHTTPSRR", std::move(task))); + } else { + task(); + } + } +} + +static nsresult ExtractHostAndPort(const nsACString& aURI, nsCString& aResult, + int32_t& aOutPort) { + nsCOMPtr uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aURI); + if (NS_FAILED(rv)) { + return rv; + } + + if (!uri->SchemeIs("https")) { + LOG(("ODoHService host uri is not https")); + return NS_ERROR_FAILURE; + } + + rv = uri->GetPort(&aOutPort); + if (NS_FAILED(rv)) { + return rv; + } + + return uri->GetAsciiHost(aResult); +} + +void ODoHService::BuildODoHRequestURI() { + mLock.AssertCurrentThreadOwns(); + + mODoHRequestURI.Truncate(); + if (mODoHTargetHost.IsEmpty() || mODoHTargetPath.IsEmpty()) { + return; + } + + if (mODoHProxyURI.IsEmpty()) { + mODoHRequestURI.Append(mODoHTargetHost); + mODoHRequestURI.AppendLiteral("/"); + mODoHRequestURI.Append(mODoHTargetPath); + } else { + nsAutoCString hostStr; + int32_t port = -1; + if (NS_FAILED(ExtractHostAndPort(mODoHTargetHost, hostStr, port))) { + return; + } + + mODoHRequestURI.Append(mODoHProxyURI); + mODoHRequestURI.AppendLiteral("?targethost="); + mODoHRequestURI.Append(hostStr); + mODoHRequestURI.AppendLiteral("&targetpath=/"); + mODoHRequestURI.Append(mODoHTargetPath); + } +} + +void ODoHService::GetRequestURI(nsACString& aResult) { + MutexAutoLock lock(mLock); + aResult = mODoHRequestURI; +} + +nsresult ODoHService::UpdateODoHConfig() { + LOG(("ODoHService::UpdateODoHConfig")); + if (mQueryODoHConfigInProgress) { + return NS_OK; + } + + if (NS_SUCCEEDED(UpdateODoHConfigFromURI())) { + return NS_OK; + } + + return UpdateODoHConfigFromHTTPSRR(); +} + +nsresult ODoHService::UpdateODoHConfigFromURI() { + LOG(("ODoHService::UpdateODoHConfigFromURI")); + + nsAutoCString configUri; + { + MutexAutoLock lock(mLock); + configUri = mODoHConfigsUri; + } + + if (configUri.IsEmpty() || !StringBeginsWith(configUri, "https://"_ns)) { + LOG(("ODoHService::UpdateODoHConfigFromURI: uri is invalid")); + return UpdateODoHConfigFromHTTPSRR(); + } + + nsCOMPtr target = TRRService::Get()->MainThreadOrTRRThread(); + if (!target) { + return NS_ERROR_UNEXPECTED; + } + + if (!target->IsOnCurrentThread()) { + nsresult rv = target->Dispatch(NS_NewRunnableFunction( + "ODoHService::UpdateODoHConfigFromURI", + []() { gODoHService->UpdateODoHConfigFromURI(); })); + if (NS_SUCCEEDED(rv)) { + // Set mQueryODoHConfigInProgress to true to avoid updating ODoHConfigs + // when waiting the runnable to be executed. + mQueryODoHConfigInProgress = true; + } + return rv; + } + + // In case any error happens, we should reset mQueryODoHConfigInProgress. + auto guard = MakeScopeExit([&]() { mQueryODoHConfigInProgress = false; }); + + nsCOMPtr uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), configUri); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr channel; + rv = DNSUtils::CreateChannelHelper(uri, getter_AddRefs(channel)); + if (NS_FAILED(rv) || !channel) { + LOG(("NewChannel failed!")); + return rv; + } + + rv = channel->SetLoadFlags( + nsIRequest::LOAD_ANONYMOUS | nsIRequest::INHIBIT_CACHING | + nsIRequest::LOAD_BYPASS_CACHE | nsIChannel::LOAD_BYPASS_URL_CLASSIFIER); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr 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); + + nsCOMPtr loader; + rv = NS_NewStreamLoader(getter_AddRefs(loader), this); + if (NS_FAILED(rv)) { + return rv; + } + + rv = httpChannel->AsyncOpen(loader); + if (NS_FAILED(rv)) { + return rv; + } + + // AsyncOpen succeeded, dismiss the guard. + MutexAutoLock lock(mLock); + guard.release(); + mLoader.swap(loader); + return rv; +} + +nsresult ODoHService::UpdateODoHConfigFromHTTPSRR() { + LOG(("ODoHService::UpdateODoHConfigFromHTTPSRR")); + + nsAutoCString uri; + { + MutexAutoLock lock(mLock); + uri = mODoHTargetHost; + } + + nsCOMPtr dns( + do_GetService("@mozilla.org/network/dns-service;1")); + if (!dns) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (!TRRService::Get()) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsAutoCString hostStr; + int32_t port = -1; + nsresult rv = ExtractHostAndPort(uri, hostStr, port); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr tmpOutstanding; + nsCOMPtr target = TRRService::Get()->MainThreadOrTRRThread(); + // We'd like to bypass the DNS cache, since ODoHConfigs will be updated + // manually by ODoHService. + nsIDNSService::DNSFlags flags = + nsIDNSService::RESOLVE_DISABLE_ODOH | nsIDNSService::RESOLVE_BYPASS_CACHE; + nsCOMPtr info; + if (port != -1) { + Unused << dns->NewAdditionalInfo(""_ns, port, getter_AddRefs(info)); + } + rv = dns->AsyncResolveNative(hostStr, nsIDNSService::RESOLVE_TYPE_HTTPSSVC, + flags, info, this, target, OriginAttributes(), + getter_AddRefs(tmpOutstanding)); + LOG(("ODoHService::UpdateODoHConfig [host=%s rv=%" PRIx32 "]", hostStr.get(), + static_cast(rv))); + + if (NS_SUCCEEDED(rv)) { + mQueryODoHConfigInProgress = true; + } + return rv; +} + +void ODoHService::StartTTLTimer(uint32_t aTTL) { + if (mTTLTimer) { + mTTLTimer->Cancel(); + mTTLTimer = nullptr; + } + LOG(("ODoHService::StartTTLTimer ttl=%d(s)", aTTL)); + NS_NewTimerWithCallback(getter_AddRefs(mTTLTimer), this, aTTL * 1000, + nsITimer::TYPE_ONE_SHOT); +} + +NS_IMETHODIMP +ODoHService::Notify(nsITimer* aTimer) { + MOZ_ASSERT(aTimer == mTTLTimer); + UpdateODoHConfig(); + return NS_OK; +} + +NS_IMETHODIMP +ODoHService::GetName(nsACString& aName) { + aName.AssignLiteral("ODoHService"); + return NS_OK; +} + +void ODoHService::ODoHConfigUpdateDone(uint32_t aTTL, + Span aRawConfig) { + MOZ_ASSERT_IF(XRE_IsParentProcess() && TRRService::Get(), + NS_IsMainThread() || TRRService::Get()->IsOnTRRThread()); + MOZ_ASSERT_IF(XRE_IsSocketProcess(), NS_IsMainThread()); + + MutexAutoLock lock(mLock); + mQueryODoHConfigInProgress = false; + mODoHConfigs.reset(); + + nsTArray configs; + if (ODoHDNSPacket::ParseODoHConfigs(aRawConfig, configs)) { + mODoHConfigs.emplace(std::move(configs)); + } + + // Let observers know whether ODoHService is activated or not. + bool hasODoHConfigs = mODoHConfigs && !mODoHConfigs->IsEmpty(); + if (aTTL < StaticPrefs::network_trr_odoh_min_ttl()) { + aTTL = StaticPrefs::network_trr_odoh_min_ttl(); + } + auto task = [hasODoHConfigs, aTTL]() { + MOZ_ASSERT(NS_IsMainThread()); + if (XRE_IsSocketProcess()) { + SocketProcessChild::GetSingleton()->SendODoHServiceActivated( + hasODoHConfigs); + } + + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + + if (observerService) { + observerService->NotifyObservers(nullptr, "odoh-service-activated", + hasODoHConfigs ? u"true" : u"false"); + } + + if (aTTL) { + gODoHService->StartTTLTimer(aTTL); + } + }; + + if (NS_IsMainThread()) { + task(); + } else { + NS_DispatchToMainThread( + NS_NewRunnableFunction("ODoHService::Activated", std::move(task))); + } + + if (!mPendingRequests.IsEmpty()) { + nsTArray> requests = std::move(mPendingRequests); + nsCOMPtr target = + TRRService::Get()->MainThreadOrTRRThread(); + for (auto& query : requests) { + target->Dispatch(query.forget()); + } + } +} + +NS_IMETHODIMP +ODoHService::OnLookupComplete(nsICancelable* aRequest, nsIDNSRecord* aRec, + nsresult aStatus) { + MOZ_ASSERT_IF(XRE_IsParentProcess() && TRRService::Get(), + NS_IsMainThread() || TRRService::Get()->IsOnTRRThread()); + MOZ_ASSERT_IF(XRE_IsSocketProcess(), NS_IsMainThread()); + + nsCOMPtr httpsRecord; + nsCString rawODoHConfig; + auto notifyDone = MakeScopeExit([&]() { + uint32_t ttl = 0; + if (httpsRecord) { + Unused << httpsRecord->GetTtl(&ttl); + } + + ODoHConfigUpdateDone( + ttl, + Span(reinterpret_cast(rawODoHConfig.BeginReading()), + rawODoHConfig.Length())); + }); + + LOG(("ODoHService::OnLookupComplete [aStatus=%" PRIx32 "]", + static_cast(aStatus))); + if (NS_FAILED(aStatus)) { + return NS_OK; + } + + httpsRecord = do_QueryInterface(aRec); + if (!httpsRecord) { + return NS_OK; + } + + nsTArray> svcbRecords; + httpsRecord->GetRecords(svcbRecords); + for (const auto& record : svcbRecords) { + Unused << record->GetODoHConfig(rawODoHConfig); + if (!rawODoHConfig.IsEmpty()) { + break; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +ODoHService::OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext, + nsresult aStatus, uint32_t aLength, + const uint8_t* aContent) { + MOZ_ASSERT_IF(XRE_IsParentProcess() && TRRService::Get(), + NS_IsMainThread() || TRRService::Get()->IsOnTRRThread()); + MOZ_ASSERT_IF(XRE_IsSocketProcess(), NS_IsMainThread()); + LOG(("ODoHService::OnStreamComplete aLength=%d\n", aLength)); + + { + MutexAutoLock lock(mLock); + mLoader = nullptr; + } + ODoHConfigUpdateDone(0, Span(aContent, aLength)); + return NS_OK; +} + +const Maybe>& ODoHService::ODoHConfigs() { + MOZ_ASSERT_IF(XRE_IsParentProcess() && TRRService::Get(), + NS_IsMainThread() || TRRService::Get()->IsOnTRRThread()); + MOZ_ASSERT_IF(XRE_IsSocketProcess(), NS_IsMainThread()); + + return mODoHConfigs; +} + +void ODoHService::AppendPendingODoHRequest(ODoH* aRequest) { + LOG(("ODoHService::AppendPendingODoHQuery\n")); + MOZ_ASSERT_IF(XRE_IsParentProcess() && TRRService::Get(), + NS_IsMainThread() || TRRService::Get()->IsOnTRRThread()); + MOZ_ASSERT_IF(XRE_IsSocketProcess(), NS_IsMainThread()); + + MutexAutoLock lock(mLock); + mPendingRequests.AppendElement(aRequest); +} + +bool ODoHService::RemovePendingODoHRequest(ODoH* aRequest) { + MOZ_ASSERT_IF(XRE_IsParentProcess() && TRRService::Get(), + NS_IsMainThread() || TRRService::Get()->IsOnTRRThread()); + MOZ_ASSERT_IF(XRE_IsSocketProcess(), NS_IsMainThread()); + + MutexAutoLock lock(mLock); + return mPendingRequests.RemoveElement(aRequest); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/dns/ODoHService.h b/netwerk/dns/ODoHService.h new file mode 100644 index 000000000000..4e09ebe3984c --- /dev/null +++ b/netwerk/dns/ODoHService.h @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef ODoHService_h_ +#define ODoHService_h_ + +#include "DNS.h" +#include "mozilla/Atomics.h" +#include "mozilla/Maybe.h" +#include "mozilla/Mutex.h" +#include "nsString.h" +#include "nsIDNSListener.h" +#include "nsIObserver.h" +#include "nsIStreamLoader.h" +#include "nsITimer.h" +#include "nsWeakReference.h" + +namespace mozilla { +namespace net { + +class ODoH; + +class ODoHService : public nsIDNSListener, + public nsIObserver, + public nsSupportsWeakReference, + public nsITimerCallback, + public nsINamed, + public nsIStreamLoaderObserver { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIDNSLISTENER + NS_DECL_NSIOBSERVER + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + NS_DECL_NSISTREAMLOADEROBSERVER + + ODoHService(); + bool Init(); + bool Enabled() const; + + const Maybe>& ODoHConfigs(); + void AppendPendingODoHRequest(ODoH* aRequest); + bool RemovePendingODoHRequest(ODoH* aRequest); + void GetRequestURI(nsACString& aResult); + // Send a DNS query to reterive the ODoHConfig. + nsresult UpdateODoHConfig(); + + private: + virtual ~ODoHService(); + nsresult ReadPrefs(const char* aName); + void OnODoHPrefsChange(bool aInit); + void BuildODoHRequestURI(); + void StartTTLTimer(uint32_t aTTL); + void OnODohConfigsURIChanged(); + void ODoHConfigUpdateDone(uint32_t aTTL, Span aRawConfig); + nsresult UpdateODoHConfigFromHTTPSRR(); + nsresult UpdateODoHConfigFromURI(); + + mozilla::Mutex mLock; + Atomic mQueryODoHConfigInProgress; + nsCString mODoHProxyURI MOZ_GUARDED_BY(mLock); + nsCString mODoHTargetHost MOZ_GUARDED_BY(mLock); + nsCString mODoHTargetPath MOZ_GUARDED_BY(mLock); + nsCString mODoHRequestURI MOZ_GUARDED_BY(mLock); + nsCString mODoHConfigsUri MOZ_GUARDED_BY(mLock); + Maybe> mODoHConfigs MOZ_GUARDED_BY(mLock); + nsTArray> mPendingRequests MOZ_GUARDED_BY(mLock); + // This timer is always touched on main thread to avoid race conditions. + nsCOMPtr mTTLTimer; + nsCOMPtr mLoader MOZ_GUARDED_BY(mLock); +}; + +extern ODoHService* gODoHService; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/dns/TRR.cpp b/netwerk/dns/TRR.cpp index 3bfc892c65b1..c3421ad26a24 100644 --- a/netwerk/dns/TRR.cpp +++ b/netwerk/dns/TRR.cpp @@ -26,6 +26,7 @@ #include "nsStringStream.h" #include "nsThreadUtils.h" #include "nsURLHelper.h" +#include "ODoH.h" #include "ObliviousHttpChannel.h" #include "TRR.h" #include "TRRService.h" @@ -880,7 +881,9 @@ nsresult TRR::FollowCname(nsIChannel* aChannel) { LOG(("TRR::On200Response CNAME %s => %s (%u)\n", mHost.get(), mCname.get(), mCnameLoop)); RefPtr trr = - new TRR(mHostResolver, mRec, mCname, mType, mCnameLoop, mPB); + ResolverType() == DNSResolverType::ODoH + ? new ODoH(mHostResolver, mRec, mCname, mType, mCnameLoop, mPB) + : new TRR(mHostResolver, mRec, mCname, mType, mCnameLoop, mPB); if (!TRRService::Get()) { return NS_ERROR_FAILURE; } diff --git a/netwerk/dns/TRRQuery.cpp b/netwerk/dns/TRRQuery.cpp index defe4353dd8c..0484ccbeb05e 100644 --- a/netwerk/dns/TRRQuery.cpp +++ b/netwerk/dns/TRRQuery.cpp @@ -9,6 +9,7 @@ #include "nsQueryObject.h" #include "TRR.h" #include "TRRService.h" +#include "ODoH.h" // Put DNSLogging.h at the end to avoid LOG being overwritten by other headers. #include "DNSLogging.h" @@ -64,10 +65,15 @@ void TRRQuery::MarkSendingTRR(TRR* trr, enum TrrType rectype, MutexAutoLock&) { } } -void TRRQuery::PrepareQuery(enum TrrType aRecType, +void TRRQuery::PrepareQuery(bool aUseODoH, enum TrrType aRecType, nsTArray>& aRequestsToSend) { LOG(("TRR Resolve %s type %d\n", mRecord->host.get(), (int)aRecType)); - RefPtr trr = new TRR(this, mRecord, aRecType); + RefPtr trr; + if (aUseODoH) { + trr = new ODoH(this, mRecord, aRecType); + } else { + trr = new TRR(this, mRecord, aRecType); + } { MutexAutoLock trrlock(mTrrLock); @@ -99,9 +105,14 @@ bool TRRQuery::SendQueries(nsTArray>& aRequestsToSend) { return madeQuery; } -nsresult TRRQuery::DispatchLookup(TRR* pushedTRR) { +nsresult TRRQuery::DispatchLookup(TRR* pushedTRR, bool aUseODoH) { + if (aUseODoH && pushedTRR) { + MOZ_ASSERT(false, "ODoH should not support push"); + return NS_ERROR_UNKNOWN_HOST; + } + if (!mRecord->IsAddrRecord()) { - return DispatchByTypeLookup(pushedTRR); + return DispatchByTypeLookup(pushedTRR, aUseODoH); } RefPtr addrRec = do_QueryObject(mRecord); @@ -131,20 +142,21 @@ nsresult TRRQuery::DispatchLookup(TRR* pushedTRR) { // same time. nsTArray> requestsToSend; if ((mRecord->af == AF_UNSPEC || mRecord->af == AF_INET6)) { - PrepareQuery(TRRTYPE_AAAA, requestsToSend); + PrepareQuery(aUseODoH, TRRTYPE_AAAA, requestsToSend); } if (mRecord->af == AF_UNSPEC || mRecord->af == AF_INET) { - PrepareQuery(TRRTYPE_A, requestsToSend); + PrepareQuery(aUseODoH, TRRTYPE_A, requestsToSend); } if (SendQueries(requestsToSend)) { + mUsingODoH = aUseODoH; return NS_OK; } return NS_ERROR_UNKNOWN_HOST; } -nsresult TRRQuery::DispatchByTypeLookup(TRR* pushedTRR) { +nsresult TRRQuery::DispatchByTypeLookup(TRR* pushedTRR, bool aUseODoH) { RefPtr typeRec = do_QueryObject(mRecord); MOZ_ASSERT(typeRec); if (!typeRec) { @@ -167,7 +179,12 @@ nsresult TRRQuery::DispatchByTypeLookup(TRR* pushedTRR) { } LOG(("TRR Resolve %s type %d\n", typeRec->host.get(), (int)rectype)); - RefPtr trr = pushedTRR ? pushedTRR : new TRR(this, mRecord, rectype); + RefPtr trr; + if (aUseODoH) { + trr = new ODoH(this, mRecord, rectype); + } else { + trr = pushedTRR ? pushedTRR : new TRR(this, mRecord, rectype); + } if (pushedTRR || NS_SUCCEEDED(TRRService::Get()->DispatchTRRRequest(trr))) { MutexAutoLock trrlock(mTrrLock); @@ -268,7 +285,7 @@ AHostResolver::LookupStatus TRRQuery::CompleteLookup( if (newRRSet->TRRType() == TRRTYPE_A) { LOG(("A lookup failed. Checking if AAAA record exists")); nsTArray> requestsToSend; - PrepareQuery(TRRTYPE_AAAA, requestsToSend); + PrepareQuery(mUsingODoH, TRRTYPE_AAAA, requestsToSend); if (SendQueries(requestsToSend)) { LOG(("Sent AAAA request")); return LOOKUP_OK; @@ -276,7 +293,7 @@ AHostResolver::LookupStatus TRRQuery::CompleteLookup( } else if (newRRSet->TRRType() == TRRTYPE_AAAA) { LOG(("AAAA lookup failed. Checking if A record exists")); nsTArray> requestsToSend; - PrepareQuery(TRRTYPE_A, requestsToSend); + PrepareQuery(mUsingODoH, TRRTYPE_A, requestsToSend); if (SendQueries(requestsToSend)) { LOG(("Sent A request")); return LOOKUP_OK; diff --git a/netwerk/dns/TRRQuery.h b/netwerk/dns/TRRQuery.h index 5c606bcc9a4d..14f08ac7ac39 100644 --- a/netwerk/dns/TRRQuery.h +++ b/netwerk/dns/TRRQuery.h @@ -22,7 +22,7 @@ class TRRQuery : public AHostResolver { mRecord(aHostRecord), mTrrLock("TRRQuery.mTrrLock") {} - nsresult DispatchLookup(TRR* pushedTRR = nullptr); + nsresult DispatchLookup(TRR* pushedTRR = nullptr, bool aUseODoHProxy = false); void Cancel(nsresult aStatus); @@ -69,13 +69,15 @@ class TRRQuery : public AHostResolver { mozilla::TimeDuration Duration() { return mTrrDuration; } private: - nsresult DispatchByTypeLookup(TRR* pushedTRR = nullptr); + nsresult DispatchByTypeLookup(TRR* pushedTRR = nullptr, + bool aUseODoHProxy = false); private: ~TRRQuery() = default; void MarkSendingTRR(TRR* trr, TrrType rectype, MutexAutoLock&); - void PrepareQuery(TrrType aRecType, nsTArray>& aRequestsToSend); + void PrepareQuery(bool aUseODoH, TrrType aRecType, + nsTArray>& aRequestsToSend); bool SendQueries(nsTArray>& aRequestsToSend); RefPtr mHostResolver; @@ -94,6 +96,7 @@ class TRRQuery : public AHostResolver { Atomic mTRRRequestCounter{0}; uint8_t mTRRSuccess = 0; // number of successful TRR responses + bool mUsingODoH = false; bool mCalledCompleteLookup = false; mozilla::TimeDuration mTrrDuration; diff --git a/netwerk/dns/TRRService.cpp b/netwerk/dns/TRRService.cpp index 5cb42f43082d..0fcb413b9217 100644 --- a/netwerk/dns/TRRService.cpp +++ b/netwerk/dns/TRRService.cpp @@ -216,6 +216,11 @@ nsresult TRRService::Init() { sTRRBackgroundThread = thread; } + mODoHService = new ODoHService(); + if (!mODoHService->Init()) { + return NS_ERROR_FAILURE; + } + Preferences::RegisterCallbackAndCall( EventTelemetryPrefChanged, "network.trr.confirmation_telemetry_enabled"_ns); diff --git a/netwerk/dns/TRRService.h b/netwerk/dns/TRRService.h index eb6b87b58f4d..9ca2b7db8ea7 100644 --- a/netwerk/dns/TRRService.h +++ b/netwerk/dns/TRRService.h @@ -11,6 +11,7 @@ #include "nsIObserver.h" #include "nsITimer.h" #include "nsWeakReference.h" +#include "ODoHService.h" #include "TRRServiceBase.h" #include "nsICaptivePortalService.h" #include "nsTHashSet.h" @@ -108,6 +109,7 @@ class TRRService : public TRRServiceBase, friend class TRRServiceChild; friend class TRRServiceParent; + friend class ODoHService; static void AddObserver(nsIObserver* aObserver, nsIObserverService* aObserverService = nullptr); static bool CheckCaptivePortalIsPassed(); @@ -376,6 +378,7 @@ class TRRService : public TRRServiceBase, // This is used to track whether a confirmation was triggered by a URI change, // so we don't trigger another one just because other prefs have changed. bool mConfirmationTriggered{false}; + RefPtr mODoHService; nsCOMPtr mLinkService; }; diff --git a/netwerk/dns/moz.build b/netwerk/dns/moz.build index 17612ff1b396..a83d2720c4bb 100644 --- a/netwerk/dns/moz.build +++ b/netwerk/dns/moz.build @@ -78,6 +78,8 @@ UNIFIED_SOURCES += [ "nsHostRecord.cpp", "nsHostResolver.cpp", "nsIDNService.cpp", + "ODoH.cpp", + "ODoHService.cpp", "punycode.c", "TRR.cpp", "TRRQuery.cpp", diff --git a/netwerk/dns/nsDNSService2.cpp b/netwerk/dns/nsDNSService2.cpp index f115cab4de34..ca7018c2adbc 100644 --- a/netwerk/dns/nsDNSService2.cpp +++ b/netwerk/dns/nsDNSService2.cpp @@ -128,7 +128,9 @@ NS_IMETHODIMP nsDNSRecord::IsTRR(bool* retval) { MutexAutoLock lock(mHostRecord->addr_info_lock); if (mHostRecord->addr_info) { - *retval = mHostRecord->addr_info->IsTRR(); + // TODO: Let the consumers of nsIDNSRecord be unaware of the difference of + // TRR and ODoH. Will let them know the truth when needed. + *retval = mHostRecord->addr_info->IsTRROrODoH(); } else { *retval = false; } @@ -144,7 +146,7 @@ nsDNSRecord::ResolvedInSocketProcess(bool* retval) { NS_IMETHODIMP nsDNSRecord::GetTrrFetchDuration(double* aTime) { MutexAutoLock lock(mHostRecord->addr_info_lock); - if (mHostRecord->addr_info && mHostRecord->addr_info->IsTRR()) { + if (mHostRecord->addr_info && mHostRecord->addr_info->IsTRROrODoH()) { *aTime = mHostRecord->addr_info->GetTrrFetchDuration(); } else { *aTime = 0; @@ -155,7 +157,7 @@ nsDNSRecord::GetTrrFetchDuration(double* aTime) { NS_IMETHODIMP nsDNSRecord::GetTrrFetchDurationNetworkOnly(double* aTime) { MutexAutoLock lock(mHostRecord->addr_info_lock); - if (mHostRecord->addr_info && mHostRecord->addr_info->IsTRR()) { + if (mHostRecord->addr_info && mHostRecord->addr_info->IsTRROrODoH()) { *aTime = mHostRecord->addr_info->GetTrrFetchDurationNetworkOnly(); } else { *aTime = 0; @@ -845,6 +847,7 @@ nsDNSService::Init() { observerService->AddObserver(this, "last-pb-context-exited", false); observerService->AddObserver(this, NS_NETWORK_LINK_TOPIC, false); observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + observerService->AddObserver(this, "odoh-service-activated", false); } RefPtr res; @@ -1300,6 +1303,14 @@ nsDNSService::GetMyHostName(nsACString& result) { return NS_ERROR_FAILURE; } +NS_IMETHODIMP +nsDNSService::GetODoHActivated(bool* aResult) { + NS_ENSURE_ARG(aResult); + + *aResult = mODoHActivated; + return NS_OK; +} + NS_IMETHODIMP nsDNSService::Observe(nsISupports* subject, const char* topic, const char16_t* data) { @@ -1322,6 +1333,8 @@ nsDNSService::Observe(nsISupports* subject, const char* topic, } } else if (!strcmp(topic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { Shutdown(); + } else if (!strcmp(topic, "odoh-service-activated")) { + mODoHActivated = u"true"_ns.Equals(data); } if (flushCache && resolver) { diff --git a/netwerk/dns/nsDNSService2.h b/netwerk/dns/nsDNSService2.h index db8dd3eec6db..cd516289d026 100644 --- a/netwerk/dns/nsDNSService2.h +++ b/netwerk/dns/nsDNSService2.h @@ -128,6 +128,7 @@ class nsDNSService final : public mozilla::net::DNSServiceBase, uint32_t mResCacheExpiration = 0; uint32_t mResCacheGrace = 0; bool mResolverPrefsUpdated = false; + bool mODoHActivated = false; nsClassHashtable> mFailedSVCDomainNames; }; diff --git a/netwerk/dns/nsHostRecord.cpp b/netwerk/dns/nsHostRecord.cpp index 65b1d011502d..cc38dd555781 100644 --- a/netwerk/dns/nsHostRecord.cpp +++ b/netwerk/dns/nsHostRecord.cpp @@ -245,7 +245,7 @@ bool AddrHostRecord::HasUsableResultInternal( bool AddrHostRecord::RemoveOrRefresh(bool aTrrToo) { // no need to flush TRRed names, they're not resolved "locally" MutexAutoLock lock(addr_info_lock); - if (addr_info && !aTrrToo && addr_info->IsTRR()) { + if (addr_info && !aTrrToo && addr_info->IsTRROrODoH()) { return false; } if (LoadNative()) { @@ -284,6 +284,21 @@ void AddrHostRecord::ResolveComplete() { : Telemetry::LABELS_DNS_LOOKUP_DISPOSITION3::osFail); } + if (mResolverType == DNSResolverType::ODoH) { + // XXX(kershaw): Consider adding the failed host name into a blocklist. + if (mTRRSuccess) { + uint32_t millis = static_cast(mTrrDuration.ToMilliseconds()); + Telemetry::Accumulate(Telemetry::DNS_ODOH_LOOKUP_TIME, millis); + } + + if (nsHostResolver::Mode() == nsIDNSService::MODE_TRRFIRST) { + Telemetry::Accumulate(Telemetry::ODOH_SKIP_REASON_ODOH_FIRST, + static_cast(mTRRSkippedReason)); + } + + return; + } + if (mResolverType == DNSResolverType::TRR) { if (mTRRSuccess) { MOZ_DIAGNOSTIC_ASSERT(mTRRSkippedReason == diff --git a/netwerk/dns/nsHostRecord.h b/netwerk/dns/nsHostRecord.h index 0064bb9e4206..94b9967db1d4 100644 --- a/netwerk/dns/nsHostRecord.h +++ b/netwerk/dns/nsHostRecord.h @@ -321,7 +321,7 @@ class AddrHostRecord final : public nsHostRecord { mozilla::TimeDuration mTrrDuration; mozilla::TimeDuration mNativeDuration; - // TRR was used on this record + // TRR or ODoH was used on this record mozilla::Atomic mResolverType{DNSResolverType::Native}; // clang-format off diff --git a/netwerk/dns/nsHostResolver.cpp b/netwerk/dns/nsHostResolver.cpp index ea4eaa6401d1..f994d83e633a 100644 --- a/netwerk/dns/nsHostResolver.cpp +++ b/netwerk/dns/nsHostResolver.cpp @@ -36,6 +36,7 @@ #include "TRR.h" #include "TRRQuery.h" #include "TRRService.h" +#include "ODoHService.h" #include "mozilla/Atomics.h" #include "mozilla/HashFunctions.h" @@ -932,7 +933,9 @@ nsresult nsHostResolver::TrrLookup(nsHostRecord* aRec, MaybeRenewHostRecordLocked(rec, aLock); RefPtr query = new TRRQuery(this, rec); - nsresult rv = query->DispatchLookup(pushedTRR); + bool useODoH = gODoHService->Enabled() && + !((rec->flags & nsIDNSService::RESOLVE_DISABLE_ODOH)); + nsresult rv = query->DispatchLookup(pushedTRR, useODoH); if (NS_FAILED(rv)) { rec->RecordReason(TRRSkippedReason::TRR_DID_NOT_MAKE_QUERY); return rv; @@ -1289,7 +1292,7 @@ void nsHostResolver::PrepareRecordExpirationAddrRecord( unsigned int grace = mDefaultGracePeriod; unsigned int ttl = mDefaultCacheLifetime; - if (sGetTtlEnabled || rec->addr_info->IsTRR()) { + if (sGetTtlEnabled || rec->addr_info->IsTRROrODoH()) { if (rec->addr_info && rec->addr_info->TTL() != AddrInfo::NO_TTL_DATA) { ttl = rec->addr_info->TTL(); } @@ -1601,7 +1604,7 @@ nsHostResolver::LookupStatus nsHostResolver::CompleteLookupLocked( bool hasNativeResult = false; { MutexAutoLock lock(addrRec->addr_info_lock); - if (addrRec->addr_info && !addrRec->addr_info->IsTRR()) { + if (addrRec->addr_info && !addrRec->addr_info->IsTRROrODoH()) { hasNativeResult = true; } } @@ -1896,7 +1899,7 @@ void nsHostResolver::GetDNSCacheEntries(nsTArray* args) { info.hostaddr.AppendElement(buf); } } - info.TRR = addrRec->addr_info->IsTRR(); + info.TRR = addrRec->addr_info->IsTRROrODoH(); } info.originAttributesSuffix = recordEntry.GetKey().originSuffix; diff --git a/netwerk/dns/nsIDNSService.idl b/netwerk/dns/nsIDNSService.idl index c1aecccd8c33..e81877cba832 100644 --- a/netwerk/dns/nsIDNSService.idl +++ b/netwerk/dns/nsIDNSService.idl @@ -88,6 +88,8 @@ interface nsIDNSService : nsISupports RESOLVE_IGNORE_SOCKS_DNS = (1 << 13), // If set, only cached IP hint addresses will be returned from resolve/asyncResolve. RESOLVE_IP_HINT = (1 << 14), + // If set, do not use ODoH for resolving the host name. + RESOLVE_DISABLE_ODOH = (1 << 15), // If set, the DNS service will pass a DNS record to // OnLookupComplete even when there was a resolution error. RESOLVE_WANT_RECORD_ON_ERROR = (1 << 16), @@ -347,6 +349,12 @@ interface nsIDNSService : nsISupports */ readonly attribute ACString TRRDomainKey; + /** + * Returns true when we have valid ODoHConfigs to encrypt/decrypt oblivious + * DNS packets. + */ + readonly attribute boolean ODoHActivated; + /************************************************************************* * Listed below are the various flags that may be OR'd together to form * the aFlags parameter passed to asyncResolve() and resolve(). diff --git a/netwerk/ipc/PSocketProcess.ipdl b/netwerk/ipc/PSocketProcess.ipdl index 6a922ab57421..8347644e3823 100644 --- a/netwerk/ipc/PSocketProcess.ipdl +++ b/netwerk/ipc/PSocketProcess.ipdl @@ -128,6 +128,7 @@ parent: OriginAttributes aOriginAttributes, nsCString aRequestString) returns (bool aAccepted); + async ODoHServiceActivated(bool aActivated); async ExcludeHttp2OrHttp3(HttpConnectionInfoCloneArgs aArgs); diff --git a/netwerk/ipc/SocketProcessParent.cpp b/netwerk/ipc/SocketProcessParent.cpp index dabd2a66ce55..f55ec680af62 100644 --- a/netwerk/ipc/SocketProcessParent.cpp +++ b/netwerk/ipc/SocketProcessParent.cpp @@ -309,6 +309,18 @@ void SocketProcessParent::Destroy(RefPtr&& aParent) { new DeferredDeleteSocketProcessParent(std::move(aParent))); } +mozilla::ipc::IPCResult SocketProcessParent::RecvODoHServiceActivated( + const bool& aActivated) { + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + + if (observerService) { + observerService->NotifyObservers(nullptr, "odoh-service-activated", + aActivated ? u"true" : u"false"); + } + return IPC_OK(); +} + mozilla::ipc::IPCResult SocketProcessParent::RecvExcludeHttp2OrHttp3( const HttpConnectionInfoCloneArgs& aArgs) { RefPtr cinfo = diff --git a/netwerk/ipc/SocketProcessParent.h b/netwerk/ipc/SocketProcessParent.h index fb7840288675..ee11701ed129 100644 --- a/netwerk/ipc/SocketProcessParent.h +++ b/netwerk/ipc/SocketProcessParent.h @@ -98,6 +98,8 @@ class SocketProcessParent final nsIURI* aPushedURL, OriginAttributes&& aOriginAttributes, nsCString&& aRequestString, CachePushCheckResolver&& aResolver); + mozilla::ipc::IPCResult RecvODoHServiceActivated(const bool& aActivated); + mozilla::ipc::IPCResult RecvExcludeHttp2OrHttp3( const HttpConnectionInfoCloneArgs& aArgs); mozilla::ipc::IPCResult RecvOnConsoleMessage(const nsString& aMessage); diff --git a/netwerk/protocol/http/TRRServiceChannel.cpp b/netwerk/protocol/http/TRRServiceChannel.cpp index 3a316d1e45d2..033c0ae26ed6 100644 --- a/netwerk/protocol/http/TRRServiceChannel.cpp +++ b/netwerk/protocol/http/TRRServiceChannel.cpp @@ -1180,15 +1180,18 @@ nsresult TRRServiceChannel::SetupReplacementChannel(nsIURI* aNewURI, encodedChannel->SetApplyConversion(LoadApplyConversion()); } + // mContentTypeHint is empty when this channel is used to download + // ODoHConfigs. if (mContentTypeHint.IsEmpty()) { return NS_OK; } // Make sure we set content-type on the old channel properly. - MOZ_ASSERT(mContentTypeHint.Equals("application/dns-message")); + MOZ_ASSERT(mContentTypeHint.Equals("application/dns-message") || + mContentTypeHint.Equals("application/oblivious-dns-message")); // Apply TRR specific settings. Note that we already know mContentTypeHint is - // "application/dns-message" here. + // "application/dns-message" or "application/oblivious-dns-message" here. return TRR::SetupTRRServiceChannelInternal( httpChannel, mRequestHead.ParsedMethod() == nsHttpRequestHead::kMethod_Get, diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp index beffad131c3b..dbbbcbfbe2fd 100644 --- a/netwerk/protocol/http/nsHttpChannel.cpp +++ b/netwerk/protocol/http/nsHttpChannel.cpp @@ -7029,7 +7029,17 @@ static void RecordOnStartTelemetry(nsresult aStatus, Others = 2, }; - if (TRRService::Get() && TRRService::Get()->IsConfirmed()) { + if (StaticPrefs::network_trr_odoh_enabled()) { + nsCOMPtr dns = do_GetService(NS_DNSSERVICE_CONTRACTID); + if (!dns) { + return; + } + bool ODoHActivated = false; + if (NS_SUCCEEDED(dns->GetODoHActivated(&ODoHActivated)) && ODoHActivated) { + Telemetry::Accumulate(Telemetry::HTTP_CHANNEL_ONSTART_SUCCESS_ODOH, + NS_SUCCEEDED(aStatus)); + } + } else if (TRRService::Get() && TRRService::Get()->IsConfirmed()) { // Note this telemetry probe is not working when DNS resolution is done in // the socket process. HttpOnStartState state = HttpOnStartState::Others; diff --git a/netwerk/socket/nsISocketProvider.idl b/netwerk/socket/nsISocketProvider.idl index 1f19b932f957..01576defd2a4 100644 --- a/netwerk/socket/nsISocketProvider.idl +++ b/netwerk/socket/nsISocketProvider.idl @@ -138,7 +138,7 @@ interface nsISocketProvider : nsISupports /** * If set, indicates that the connection used a privacy-preserving DNS - * transport such as DoH, DoQ or similar. Currently this field is + * transport such as DoH, DoQ, ODOH or similar. Currently this field is * set only when DoH is used via the TRR. */ const unsigned long USED_PRIVATE_DNS = (1 << 12); diff --git a/netwerk/test/unit/test_odoh.js b/netwerk/test/unit/test_odoh.js new file mode 100644 index 000000000000..51f91879147b --- /dev/null +++ b/netwerk/test/unit/test_odoh.js @@ -0,0 +1,295 @@ +/* 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/. */ + +/* + * Most of tests in this file are reused from test_trr.js, except tests listed + * below. Since ODoH doesn't support push and customerized DNS resolver, some + * related tests are skipped. + * + * test_trr_flags + * test_push + * test_clearCacheOnURIChange // TODO: Clear DNS cache when ODoH prefs changed. + * test_dnsSuffix + * test_async_resolve_with_trr_server + * test_content_encoding_gzip + * test_redirect + * test_confirmation + * test_detected_uri + * test_pref_changes + * test_dohrollout_mode + * test_purge_trr_cache_on_mode_change + * test_old_bootstrap_pref + */ + +"use strict"; + +/* import-globals-from trr_common.js */ + +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); + +add_setup(async function setup() { + h2Port = trr_test_setup(); + runningODoHTests = true; + + // This is for skiping the security check for the odoh host. + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + + registerCleanupFunction(() => { + trr_clear_prefs(); + Services.prefs.clearUserPref("network.trr.odoh.enabled"); + Services.prefs.clearUserPref("network.trr.odoh.target_path"); + Services.prefs.clearUserPref("network.trr.odoh.configs_uri"); + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + }); + + if (mozinfo.socketprocess_networking) { + Services.dns; // Needed to trigger socket process. + await TestUtils.waitForCondition(() => Services.io.socketProcessLaunched); + } + + Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY); +}); + +add_task(async function testODoHConfig() { + // use the h2 server as DOH provider + Services.prefs.setCharPref( + "network.trr.uri", + "https://foo.example.com:" + h2Port + "/odohconfig" + ); + + let { inRecord } = await new TRRDNSListener("odoh_host.example.com", { + type: Ci.nsIDNSService.RESOLVE_TYPE_HTTPSSVC, + }); + let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records; + Assert.equal(answer[0].priority, 1); + Assert.equal(answer[0].name, "odoh_host.example.com"); + Assert.equal(answer[0].values.length, 1); + let odohconfig = answer[0].values[0].QueryInterface( + Ci.nsISVCParamODoHConfig + ).ODoHConfig; + Assert.equal(odohconfig.length, 46); +}); + +let testIndex = 0; + +async function ODoHConfigTest(query, ODoHHost, expectedResult = false) { + Services.prefs.setCharPref( + "network.trr.uri", + "https://foo.example.com:" + h2Port + "/odohconfig?" + query + ); + + // Setting the pref "network.trr.odoh.target_host" will trigger the reload of + // the ODoHConfigs. + if (ODoHHost != undefined) { + Services.prefs.setCharPref("network.trr.odoh.target_host", ODoHHost); + } else { + Services.prefs.setCharPref( + "network.trr.odoh.target_host", + `https://odoh_host_${testIndex++}.com` + ); + } + + await topicObserved("odoh-service-activated"); + Assert.equal(Services.dns.ODoHActivated, expectedResult); +} + +add_task(async function testODoHConfig1() { + await ODoHConfigTest("invalid=empty"); +}); + +add_task(async function testODoHConfig2() { + await ODoHConfigTest("invalid=version"); +}); + +add_task(async function testODoHConfig3() { + await ODoHConfigTest("invalid=configLength"); +}); + +add_task(async function testODoHConfig4() { + await ODoHConfigTest("invalid=totalLength"); +}); + +add_task(async function testODoHConfig5() { + await ODoHConfigTest("invalid=kemId"); +}); + +add_task(async function testODoHConfig6() { + // Use a very short TTL. + Services.prefs.setIntPref("network.trr.odoh.min_ttl", 1); + await ODoHConfigTest("invalid=kemId&ttl=1"); + + // This is triggered by the expiration of the TTL. + await topicObserved("odoh-service-activated"); + Assert.ok(!Services.dns.ODoHActivated); + Services.prefs.clearUserPref("network.trr.odoh.min_ttl"); +}); + +add_task(async function testODoHConfig7() { + Services.dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 2); // TRR-first + Services.prefs.setBoolPref("network.trr.odoh.enabled", true); + // At this point, we've queried the ODoHConfig, but there is no usable config + // (kemId is not supported). So, we should see the DNS result is from the + // native resolver. + await new TRRDNSListener("bar.example.com", "127.0.0.1"); +}); + +async function ODoHConfigTestHTTP(configUri, expectedResult) { + // Setting the pref "network.trr.odoh.configs_uri" will trigger the reload of + // the ODoHConfigs. + Services.prefs.setCharPref("network.trr.odoh.configs_uri", configUri); + + await topicObserved("odoh-service-activated"); + Assert.equal(Services.dns.ODoHActivated, expectedResult); +} + +add_task(async function testODoHConfig8() { + Services.dns.clearCache(true); + Services.prefs.setCharPref("network.trr.uri", ""); + + await ODoHConfigTestHTTP( + `https://foo.example.com:${h2Port}/odohconfig?downloadFrom=http`, + true + ); +}); + +add_task(async function testODoHConfig9() { + // Reset the prefs back to the correct values, so we will get valid + // ODoHConfigs when "network.trr.odoh.configs_uri" is cleared below. + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${h2Port}/odohconfig` + ); + Services.prefs.setCharPref( + "network.trr.odoh.target_host", + `https://odoh_host.example.com:${h2Port}` + ); + + // Use a very short TTL. + Services.prefs.setIntPref("network.trr.odoh.min_ttl", 1); + // This will trigger to download ODoHConfigs from HTTPS RR. + Services.prefs.clearUserPref("network.trr.odoh.configs_uri"); + + await topicObserved("odoh-service-activated"); + Assert.ok(Services.dns.ODoHActivated); + + await ODoHConfigTestHTTP( + `https://foo.example.com:${h2Port}/odohconfig?downloadFrom=http`, + true + ); + + // This is triggered by the expiration of the TTL. + await topicObserved("odoh-service-activated"); + Assert.ok(Services.dns.ODoHActivated); + Services.prefs.clearUserPref("network.trr.odoh.min_ttl"); +}); + +add_task(async function testODoHConfig10() { + // We can still get ODoHConfigs from HTTPS RR. + await ODoHConfigTestHTTP("http://invalid_odoh_config.com", true); +}); + +add_task(async function ensureODoHConfig() { + Services.prefs.setBoolPref("network.trr.odoh.enabled", true); + // Make sure we can connect to ODOH target successfully. + Services.prefs.setCharPref( + "network.dns.localDomains", + "odoh_host.example.com" + ); + + // Make sure we have an usable ODoHConfig to use. + await ODoHConfigTestHTTP( + `https://foo.example.com:${h2Port}/odohconfig?downloadFrom=http`, + true + ); +}); + +add_task(test_A_record); + +add_task(test_AAAA_records); + +add_task(test_RFC1918); + +add_task(test_GET_ECS); + +add_task(test_timeout_mode3); + +add_task(test_strict_native_fallback); + +add_task(test_no_answers_fallback); + +add_task(test_404_fallback); + +add_task(test_mode_1_and_4); + +add_task(test_CNAME); + +add_task(test_name_mismatch); + +add_task(test_mode_2); + +add_task(test_excluded_domains); + +add_task(test_captiveportal_canonicalURL); + +add_task(test_parentalcontrols); + +// TRR-first check that DNS result is used if domain is part of the builtin-excluded-domains pref +add_task(test_builtin_excluded_domains); + +add_task(test_excluded_domains_mode3); + +add_task(test25e); + +add_task(test_parentalcontrols_mode3); + +add_task(test_builtin_excluded_domains_mode3); + +add_task(count_cookies); + +add_task(test_connection_closed); + +add_task(test_fetch_time); + +add_task(test_fqdn); + +add_task(test_ipv6_trr_fallback); + +add_task(test_ipv4_trr_fallback); + +add_task(test_no_retry_without_doh); + +add_task(test_connection_reuse_and_cycling).skip(); // Bug 1742743 + +add_task(async function testODoHConfigNotAvailableInMode3() { + Services.dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref("network.trr.uri", ""); + + await ODoHConfigTestHTTP("https://failed_odoh_config.com", false); + + // In mode 3, the DNS lookup should fail. + let { inStatus } = await new TRRDNSListener("test.example.com", { + expectedSuccess: false, + }); + + Assert.equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST); +}); + +add_task(async function testODoHConfigNotAvailableInMode2() { + Services.dns.clearCache(true); + Services.prefs.setIntPref("network.trr.mode", 2); + Services.prefs.setCharPref("network.trr.uri", ""); + + await ODoHConfigTestHTTP("https://failed_odoh_config_1.com", false); + + // In mode 2, we fallback to native. + await new TRRDNSListener("test.example.com", "127.0.0.1"); +}); diff --git a/netwerk/test/unit/test_trr.js b/netwerk/test/unit/test_trr.js index 1fcba44f8604..bed4a78e6d1a 100644 --- a/netwerk/test/unit/test_trr.js +++ b/netwerk/test/unit/test_trr.js @@ -8,6 +8,7 @@ SetParentalControlEnabled(false); function setup() { h2Port = trr_test_setup(); + runningODoHTests = false; } setup(); diff --git a/netwerk/test/unit/test_trr_telemetry.js b/netwerk/test/unit/test_trr_telemetry.js index 69f5d59201ff..8ac8274d2371 100644 --- a/netwerk/test/unit/test_trr_telemetry.js +++ b/netwerk/test/unit/test_trr_telemetry.js @@ -15,6 +15,7 @@ const { TelemetryTestUtils } = ChromeUtils.importESModule( function setup() { h2Port = trr_test_setup(); + runningODoHTests = false; } let TRR_OK = 1; diff --git a/netwerk/test/unit/test_trr_with_proxy.js b/netwerk/test/unit/test_trr_with_proxy.js index 420cc9e7164a..2f95bf2acdfa 100644 --- a/netwerk/test/unit/test_trr_with_proxy.js +++ b/netwerk/test/unit/test_trr_with_proxy.js @@ -21,6 +21,7 @@ const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); function setup() { h2Port = trr_test_setup(); + runningODoHTests = false; SetParentalControlEnabled(false); } diff --git a/netwerk/test/unit/trr_common.js b/netwerk/test/unit/trr_common.js index 62cf4d094db8..779a23249c73 100644 --- a/netwerk/test/unit/trr_common.js +++ b/netwerk/test/unit/trr_common.js @@ -38,6 +38,7 @@ async function SetParentalControlEnabled(aEnabled) { MockRegistrar.unregister(cid); } +let runningODoHTests = false; let runningOHTTPTests = false; let h2Port; @@ -68,6 +69,8 @@ function setModeAndURIForOHTTP(mode, path, domain) { function setModeAndURI(mode, path, domain) { if (runningOHTTPTests) { setModeAndURIForOHTTP(mode, path, domain); + } else if (runningODoHTests) { + setModeAndURIForODoH(mode, path); } else { Services.prefs.setIntPref("network.trr.mode", mode); if (domain) { @@ -194,7 +197,11 @@ async function test_GET_ECS() { info("Verifying resolution via GET with ECS disabled"); Services.dns.clearCache(true); // The template part should be discarded - setModeAndURI(3, "doh{?dns}"); + if (runningODoHTests) { + setModeAndURI(3, "odoh"); + } else { + setModeAndURI(3, "doh{?dns}"); + } Services.prefs.setBoolPref("network.trr.useGET", true); Services.prefs.setBoolPref("network.trr.disable-ECS", true); @@ -202,7 +209,11 @@ async function test_GET_ECS() { info("Verifying resolution via GET with ECS enabled"); Services.dns.clearCache(true); - setModeAndURI(3, "doh"); + if (runningODoHTests) { + setModeAndURI(3, "odoh"); + } else { + setModeAndURI(3, "doh"); + } Services.prefs.setBoolPref("network.trr.disable-ECS", false); await new TRRDNSListener("get.example.com", "5.5.5.5"); @@ -353,6 +364,14 @@ async function test_strict_native_fallback() { info("Now with confirmation failed - should fallback"); Services.dns.clearCache(true); setModeAndURI(2, "doh?responseIP=2.2.2.2&corruptedAnswer=true"); + if (runningODoHTests) { + Services.prefs.setCharPref( + "network.trr.uri", + "https://foo.example.com:" + + h2Port + + "/odohconfig?failConfirmation=true" + ); + } Services.prefs.setCharPref("network.trr.confirmationNS", "example.com"); await TestUtils.waitForCondition( // 3 => CONFIRM_FAILED, 4 => CONFIRM_TRYING_FAILED @@ -371,6 +390,12 @@ async function test_strict_native_fallback() { setModeAndURI(2, "doh?responseIP=2.2.2.2"); if (!mozinfo.socketprocess_networking) { // Only need to reset confirmation state if we messed with it before. + if (runningODoHTests) { + Services.prefs.setCharPref( + "network.trr.uri", + "https://foo.example.com:" + h2Port + "/odohconfig" + ); + } Services.prefs.setCharPref("network.trr.confirmationNS", "skip"); await TestUtils.waitForCondition( // 5 => CONFIRM_DISABLED @@ -471,14 +496,22 @@ async function test_CNAME() { // The dns-cname path alternates between sending us a CNAME pointing to // another domain, and an A record. If we follow the cname correctly, doing // a lookup with this path as the DoH URI should resolve to that A record. - setModeAndURI(3, "dns-cname"); + if (runningODoHTests) { + setModeAndURI(3, "odoh?cname=content"); + } else { + setModeAndURI(3, "dns-cname"); + } await new TRRDNSListener("cname.example.com", "99.88.77.66"); info("Verifying that we bail out when we're thrown into a CNAME loop"); Services.dns.clearCache(true); // First mode 3. - setModeAndURI(3, "doh?responseIP=none&cnameloop=true"); + if (runningODoHTests) { + setModeAndURI(3, "odoh?responseIP=none&cnameloop=true"); + } else { + setModeAndURI(3, "doh?responseIP=none&cnameloop=true"); + } let { inStatus } = await new TRRDNSListener( "test18.example.com", @@ -492,14 +525,22 @@ async function test_CNAME() { // Now mode 2. Services.dns.clearCache(true); - setModeAndURI(2, "doh?responseIP=none&cnameloop=true"); + if (runningODoHTests) { + setModeAndURI(2, "ododoh?responseIP=none&cnameloop=trueoh"); + } else { + setModeAndURI(2, "doh?responseIP=none&cnameloop=true"); + } await new TRRDNSListener("test20.example.com", "127.0.0.1"); // Should fallback info("Check that we correctly handle CNAME bundled with an A record"); Services.dns.clearCache(true); // "dns-cname-a" path causes server to send a CNAME as well as an A record - setModeAndURI(3, "dns-cname-a"); + if (runningODoHTests) { + setModeAndURI(3, "odoh?cname=ARecord"); + } else { + setModeAndURI(3, "dns-cname-a"); + } await new TRRDNSListener("cname-a.example.com", "9.8.7.6"); } diff --git a/netwerk/test/unit/xpcshell.ini b/netwerk/test/unit/xpcshell.ini index e92bf7b45f7d..9bafab1c13ac 100644 --- a/netwerk/test/unit/xpcshell.ini +++ b/netwerk/test/unit/xpcshell.ini @@ -611,6 +611,11 @@ skip-if = !debug (os == "win" && socketprocess_networking) [test_SuperfluousAuth.js] +[test_odoh.js] +head = head_channels.js head_cache.js head_cookies.js head_trr.js head_http3.js trr_common.js +run-sequentially = node server exceptions dont replay well +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure [test_trr_confirmation.js] skip-if = socketprocess_networking # confirmation state isn't passed cross-process diff --git a/python/mozbuild/mozbuild/action/test_archive.py b/python/mozbuild/mozbuild/action/test_archive.py index 06fef60f8d14..720c2f024318 100644 --- a/python/mozbuild/mozbuild/action/test_archive.py +++ b/python/mozbuild/mozbuild/action/test_archive.py @@ -544,6 +544,7 @@ ARCHIVE_FILES = { "node_ip/**", "node-ws/**", "dns-packet/**", + "odoh-wasm/**", "remotexpcshelltests.py", "runxpcshelltests.py", "selftest.py", diff --git a/testing/xpcshell/moz-http2/moz-http2.js b/testing/xpcshell/moz-http2/moz-http2.js index 334ccd8e1c7c..be9a904dcb1c 100644 --- a/testing/xpcshell/moz-http2/moz-http2.js +++ b/testing/xpcshell/moz-http2/moz-http2.js @@ -20,6 +20,7 @@ const ip = require(`${node_http2_root}/../node_ip`); const { fork } = require("child_process"); const path = require("path"); const zlib = require("zlib"); +const odoh = require(`${node_http2_root}/../odoh-wasm/pkg`); // Hook into the decompression code to log the decompressed name-value pairs var compression_module = node_http2_root + "/lib/protocol/compressor"; @@ -1120,6 +1121,174 @@ function handleRequest(req, res) { res.end(""); }); return; + } else if (u.pathname === "/odohconfig") { + let payload = Buffer.from(""); + req.on("data", function receiveData(chunk) { + payload = Buffer.concat([payload, chunk]); + }); + req.on("end", function finishedData() { + let answers = []; + let odohconfig; + if (u.query.invalid) { + if (u.query.invalid === "empty") { + odohconfig = Buffer.from(""); + } else if (u.query.invalid === "version") { + odohconfig = Buffer.from( + "002cff030028002000010001002021c8c16355091b28d521cb196627297955c1b607a3dcf1f136534578460d077d", + "hex" + ); + } else if (u.query.invalid === "configLength") { + odohconfig = Buffer.from( + "002cff040028002000010001002021c8c16355091b28d521cb196627297955c1b607a3dcf1f136534578460d07", + "hex" + ); + } else if (u.query.invalid === "totalLength") { + odohconfig = Buffer.from( + "012cff030028002000010001002021c8c16355091b28d521cb196627297955c1b607a3dcf1f136534578460d077d", + "hex" + ); + } else if (u.query.invalid === "kemId") { + odohconfig = Buffer.from( + "002cff040028002100010001002021c8c16355091b28d521cb196627297955c1b607a3dcf1f136534578460d077d", + "hex" + ); + } + } else { + odohconfig = odoh.get_odoh_config(); + } + + if (u.query.downloadFrom === "http") { + res.writeHead(200); + res.write(odohconfig); + res.end(""); + } else { + var b64encoded = Buffer.from(odohconfig).toString("base64"); + let packet = dnsPacket.decode(payload); + if ( + u.query.failConfirmation == "true" && + packet.questions[0].type == "NS" && + packet.questions[0].name == "example.com" + ) { + res.writeHead(200); + res.write("<12bytes"); + res.end(""); + return; + } + if (packet.questions[0].type == "HTTPS") { + answers.push({ + name: packet.questions[0].name, + type: packet.questions[0].type, + ttl: u.query.ttl ? u.query.ttl : 55, + class: "IN", + flush: false, + data: { + priority: 1, + name: packet.questions[0].name, + values: [ + { + key: "odoh", + value: b64encoded, + needBase64Decode: true, + }, + ], + }, + }); + } + + let buf = dnsPacket.encode({ + type: "response", + id: packet.id, + flags: dnsPacket.RECURSION_DESIRED, + questions: packet.questions, + answers, + }); + + res.setHeader("Content-Type", "application/dns-message"); + res.setHeader("Content-Length", buf.length); + res.writeHead(200); + res.write(buf); + res.end(""); + } + }); + return; + } else if (u.pathname === "/odoh") { + let responseIP = u.query.responseIP; + if (!responseIP) { + responseIP = "5.5.5.5"; + } + + if (u.query.auth) { + if (!handleAuth()) { + return; + } + } + + if (u.query.noResponse) { + return; + } + + let payload = Buffer.from(""); + + function emitResponse(response, requestPayload) { + let decryptedQuery = odoh.decrypt_query(requestPayload); + let packet = dnsPacket.decode(Buffer.from(decryptedQuery.buffer)); + let answer = createDNSAnswer( + response, + packet, + responseIP, + requestPayload + ); + if (!answer) { + return; + } + + let encryptedResponse = odoh.create_response(answer); + writeDNSResponse( + response, + encryptedResponse, + getDelayFromPacket(packet, responseType(packet, responseIP)), + "application/oblivious-dns-message" + ); + } + + if (u.query.dns) { + payload = Buffer.from(u.query.dns, "base64"); + emitResponse(res, payload); + return; + } + + req.on("data", function receiveData(chunk) { + payload = Buffer.concat([payload, chunk]); + }); + req.on("end", function finishedData() { + if (u.query.httpError) { + res.writeHead(404); + res.end("Not Found"); + return; + } + + if (u.query.cname) { + let decryptedQuery = odoh.decrypt_query(payload); + let rContent; + if (u.query.cname === "ARecord") { + rContent = createCNameARecord(); + } else { + rContent = createCNameContent(Buffer.from(decryptedQuery.buffer)); + } + let encryptedResponse = odoh.create_response(rContent); + res.setHeader("Content-Type", "application/oblivious-dns-message"); + res.setHeader("Content-Length", encryptedResponse.length); + res.writeHead(200); + res.write(encryptedResponse); + res.end(""); + return; + } + // parload is empty when we send redirect response. + if (payload.length) { + emitResponse(res, payload); + } + }); + return; } else if (u.pathname === "/httpssvc_as_altsvc") { let payload = Buffer.from(""); req.on("data", function receiveData(chunk) { diff --git a/testing/xpcshell/odoh-wasm/Cargo.toml b/testing/xpcshell/odoh-wasm/Cargo.toml new file mode 100644 index 000000000000..f709e66139d5 --- /dev/null +++ b/testing/xpcshell/odoh-wasm/Cargo.toml @@ -0,0 +1,42 @@ +[workspace] + +[package] +name = "odoh-wasm" +version = "0.1.0" +authors = ["Kershaw Chang "] +edition = "2018" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +wasm-bindgen = "0.2.63" +odoh-rs = "=0.1.10" +hpke = "=0.5.0" +js-sys = "0.3" +hex = "0.4" +futures = "0.3.1" +rand = "=0.7" + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.6", optional = true } + +# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size +# compared to the default allocator's ~10K. It is slower than the default +# allocator, however. +# +# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. +wee_alloc = { version = "0.4.5", optional = true } + +[dev-dependencies] +wasm-bindgen-test = "0.3.13" + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" diff --git a/testing/xpcshell/odoh-wasm/LICENSE_APACHE b/testing/xpcshell/odoh-wasm/LICENSE_APACHE new file mode 100644 index 000000000000..1b5ec8b78e23 --- /dev/null +++ b/testing/xpcshell/odoh-wasm/LICENSE_APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/testing/xpcshell/odoh-wasm/LICENSE_MIT b/testing/xpcshell/odoh-wasm/LICENSE_MIT new file mode 100644 index 000000000000..681a58f76e09 --- /dev/null +++ b/testing/xpcshell/odoh-wasm/LICENSE_MIT @@ -0,0 +1,25 @@ +Copyright (c) 2018 Kershaw Chang + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/testing/xpcshell/odoh-wasm/README.md b/testing/xpcshell/odoh-wasm/README.md new file mode 100644 index 000000000000..d3e413bfec51 --- /dev/null +++ b/testing/xpcshell/odoh-wasm/README.md @@ -0,0 +1,75 @@ +
+ +

wasm-pack-template

+ + A template for kick starting a Rust and WebAssembly project using wasm-pack. + +

+ Build Status +

+ +

+ Tutorial + | + Chat +

+ + Built with 🦀🕸 by The Rust and WebAssembly Working Group +
+ +## About + +[**📚 Read this template tutorial! 📚**][template-docs] + +This template is designed for compiling Rust libraries into WebAssembly and +publishing the resulting package to NPM. + +Be sure to check out [other `wasm-pack` tutorials online][tutorials] for other +templates and usages of `wasm-pack`. + +[tutorials]: https://rustwasm.github.io/docs/wasm-pack/tutorials/index.html +[template-docs]: https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html + +## 🚴 Usage + +### 🐑 Use `cargo generate` to Clone this Template + +[Learn more about `cargo generate` here.](https://github.com/ashleygwilliams/cargo-generate) + +``` +cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project +cd my-project +``` + +### 🛠️ Build with `wasm-pack build` + +``` +wasm-pack build +``` + +### 🛠️ Build a module that can be used for nodejs + +``` +wasm-pack build --target nodejs +``` + +### 🔬 Test in Headless Browsers with `wasm-pack test` + +``` +wasm-pack test --headless --firefox +``` + +### 🎁 Publish to NPM with `wasm-pack publish` + +``` +wasm-pack publish +``` + +## 🔋 Batteries Included + +* [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) for communicating + between WebAssembly and JavaScript. +* [`console_error_panic_hook`](https://github.com/rustwasm/console_error_panic_hook) + for logging panic messages to the developer console. +* [`wee_alloc`](https://github.com/rustwasm/wee_alloc), an allocator optimized + for small code size. diff --git a/testing/xpcshell/odoh-wasm/pkg/README.md b/testing/xpcshell/odoh-wasm/pkg/README.md new file mode 100644 index 000000000000..1e4617a6d293 --- /dev/null +++ b/testing/xpcshell/odoh-wasm/pkg/README.md @@ -0,0 +1,69 @@ +
+ +

wasm-pack-template

+ + A template for kick starting a Rust and WebAssembly project using wasm-pack. + +

+ Build Status +

+ +

+ Tutorial + | + Chat +

+ + Built with 🦀🕸 by The Rust and WebAssembly Working Group +
+ +## About + +[**📚 Read this template tutorial! 📚**][template-docs] + +This template is designed for compiling Rust libraries into WebAssembly and +publishing the resulting package to NPM. + +Be sure to check out [other `wasm-pack` tutorials online][tutorials] for other +templates and usages of `wasm-pack`. + +[tutorials]: https://rustwasm.github.io/docs/wasm-pack/tutorials/index.html +[template-docs]: https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html + +## 🚴 Usage + +### 🐑 Use `cargo generate` to Clone this Template + +[Learn more about `cargo generate` here.](https://github.com/ashleygwilliams/cargo-generate) + +``` +cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project +cd my-project +``` + +### 🛠️ Build with `wasm-pack build` + +``` +wasm-pack build +``` + +### 🔬 Test in Headless Browsers with `wasm-pack test` + +``` +wasm-pack test --headless --firefox +``` + +### 🎁 Publish to NPM with `wasm-pack publish` + +``` +wasm-pack publish +``` + +## 🔋 Batteries Included + +* [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) for communicating + between WebAssembly and JavaScript. +* [`console_error_panic_hook`](https://github.com/rustwasm/console_error_panic_hook) + for logging panic messages to the developer console. +* [`wee_alloc`](https://github.com/rustwasm/wee_alloc), an allocator optimized + for small code size. diff --git a/testing/xpcshell/odoh-wasm/pkg/odoh_wasm.d.ts b/testing/xpcshell/odoh-wasm/pkg/odoh_wasm.d.ts new file mode 100644 index 000000000000..1f90ef659192 --- /dev/null +++ b/testing/xpcshell/odoh-wasm/pkg/odoh_wasm.d.ts @@ -0,0 +1,16 @@ +/* tslint:disable */ +/* eslint-disable */ +/** +* @returns {Uint8Array} +*/ +export function get_odoh_config(): Uint8Array; +/** +* @param {Uint8Array} odoh_encrypted_query_msg +* @returns {Uint8Array} +*/ +export function decrypt_query(odoh_encrypted_query_msg: Uint8Array): Uint8Array; +/** +* @param {Uint8Array} response +* @returns {Uint8Array} +*/ +export function create_response(response: Uint8Array): Uint8Array; diff --git a/testing/xpcshell/odoh-wasm/pkg/odoh_wasm.js b/testing/xpcshell/odoh-wasm/pkg/odoh_wasm.js new file mode 100644 index 000000000000..14b97d74365b --- /dev/null +++ b/testing/xpcshell/odoh-wasm/pkg/odoh_wasm.js @@ -0,0 +1,132 @@ +let imports = {}; +imports['__wbindgen_placeholder__'] = module.exports; +let wasm; +const { TextDecoder } = require(`util`); + +const heap = new Array(32).fill(undefined); + +heap.push(undefined, null, true, false); + +function getObject(idx) { return heap[idx]; } + +let heap_next = heap.length; + +function dropObject(idx) { + if (idx < 36) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + +cachedTextDecoder.decode(); + +let cachegetUint8Memory0 = null; +function getUint8Memory0() { + if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) { + cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer); + } + return cachegetUint8Memory0; +} + +function getStringFromWasm0(ptr, len) { + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} +/** +* @returns {Uint8Array} +*/ +module.exports.get_odoh_config = function() { + var ret = wasm.get_odoh_config(); + return takeObject(ret); +}; + +let WASM_VECTOR_LEN = 0; + +function passArray8ToWasm0(arg, malloc) { + const ptr = malloc(arg.length * 1); + getUint8Memory0().set(arg, ptr / 1); + WASM_VECTOR_LEN = arg.length; + return ptr; +} +/** +* @param {Uint8Array} odoh_encrypted_query_msg +* @returns {Uint8Array} +*/ +module.exports.decrypt_query = function(odoh_encrypted_query_msg) { + var ptr0 = passArray8ToWasm0(odoh_encrypted_query_msg, wasm.__wbindgen_malloc); + var len0 = WASM_VECTOR_LEN; + var ret = wasm.decrypt_query(ptr0, len0); + return takeObject(ret); +}; + +/** +* @param {Uint8Array} response +* @returns {Uint8Array} +*/ +module.exports.create_response = function(response) { + var ptr0 = passArray8ToWasm0(response, wasm.__wbindgen_malloc); + var len0 = WASM_VECTOR_LEN; + var ret = wasm.create_response(ptr0, len0); + return takeObject(ret); +}; + +module.exports.__wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); +}; + +module.exports.__wbg_log_b3f203d9e6882397 = function(arg0, arg1) { + console.log(getStringFromWasm0(arg0, arg1)); +}; + +module.exports.__wbg_buffer_eb2155f17856c20b = function(arg0) { + var ret = getObject(arg0).buffer; + return addHeapObject(ret); +}; + +module.exports.__wbg_newwithbyteoffsetandlength_7d07f77c6d0d8e26 = function(arg0, arg1, arg2) { + var ret = new Uint8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0); + return addHeapObject(ret); +}; + +module.exports.__wbg_new_ff8b26f7b2d7e2fb = function(arg0) { + var ret = new Uint8Array(getObject(arg0)); + return addHeapObject(ret); +}; + +module.exports.__wbg_newwithlength_a49b32b2030b93c3 = function(arg0) { + var ret = new Uint8Array(arg0 >>> 0); + return addHeapObject(ret); +}; + +module.exports.__wbindgen_throw = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); +}; + +module.exports.__wbindgen_memory = function() { + var ret = wasm.memory; + return addHeapObject(ret); +}; + +const path = require('path').join(__dirname, 'odoh_wasm_bg.wasm'); +const bytes = require('fs').readFileSync(path); + +const wasmModule = new WebAssembly.Module(bytes); +const wasmInstance = new WebAssembly.Instance(wasmModule, imports); +wasm = wasmInstance.exports; +module.exports.__wasm = wasm; + diff --git a/testing/xpcshell/odoh-wasm/pkg/odoh_wasm_bg.wasm b/testing/xpcshell/odoh-wasm/pkg/odoh_wasm_bg.wasm new file mode 100644 index 000000000000..ddca009ed480 Binary files /dev/null and b/testing/xpcshell/odoh-wasm/pkg/odoh_wasm_bg.wasm differ diff --git a/testing/xpcshell/odoh-wasm/pkg/odoh_wasm_bg.wasm.d.ts b/testing/xpcshell/odoh-wasm/pkg/odoh_wasm_bg.wasm.d.ts new file mode 100644 index 000000000000..e3a939f2e0fd --- /dev/null +++ b/testing/xpcshell/odoh-wasm/pkg/odoh_wasm_bg.wasm.d.ts @@ -0,0 +1,7 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export function get_odoh_config(): number; +export function decrypt_query(a: number, b: number): number; +export function create_response(a: number, b: number): number; +export function __wbindgen_malloc(a: number): number; diff --git a/testing/xpcshell/odoh-wasm/pkg/package.json b/testing/xpcshell/odoh-wasm/pkg/package.json new file mode 100644 index 000000000000..e6db000676d2 --- /dev/null +++ b/testing/xpcshell/odoh-wasm/pkg/package.json @@ -0,0 +1,15 @@ +{ + "name": "odoh-wasm", + "collaborators": [ + "Kershaw Chang " + ], + "version": "0.1.0", + "files": [ + "odoh_wasm_bg.wasm", + "odoh_wasm.js", + "odoh_wasm_bg.js", + "odoh_wasm.d.ts" + ], + "main": "odoh_wasm.js", + "types": "odoh_wasm.d.ts" +} \ No newline at end of file diff --git a/testing/xpcshell/odoh-wasm/src/lib.rs b/testing/xpcshell/odoh-wasm/src/lib.rs new file mode 100644 index 000000000000..8f94d2b567c3 --- /dev/null +++ b/testing/xpcshell/odoh-wasm/src/lib.rs @@ -0,0 +1,158 @@ +use hpke::{ + kem::X25519HkdfSha256, + Kem as KemTrait, Serializable, +}; + +use odoh_rs::protocol::{ + create_response_msg, parse_received_query, + ObliviousDoHConfigContents, ObliviousDoHKeyPair, + ObliviousDoHQueryBody, +}; + +use futures::executor; +use hex; +use wasm_bindgen::prelude::*; + +pub type Kem = X25519HkdfSha256; + +// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global +// allocator. +#[cfg(feature = "wee_alloc")] +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +pub const ODOH_VERSION: u16 = 0x0001; +const KEM_ID: u16 = 0x0020; +const KDF_ID: u16 = 0x0001; +const AEAD_ID: u16 = 0x0001; + +// random bytes, should be 32 bytes for X25519 keys +pub const IKM: &str = "871389a8727130974e3eb3ee528d440a871389a8727130974e3eb3ee528d440a"; + +#[wasm_bindgen] +extern "C" { + // Use `js_namespace` here to bind `console.log(..)` instead of just + // `log(..)` + #[wasm_bindgen(js_namespace = console)] + fn log(s: &str); + + // The `console.log` is quite polymorphic, so we can bind it with multiple + // signatures. Note that we need to use `js_name` to ensure we always call + // `log` in JS. + #[wasm_bindgen(js_namespace = console, js_name = log)] + fn log_u32(a: u32); + + // Multiple arguments too! + #[wasm_bindgen(js_namespace = console, js_name = log)] + fn log_many(a: &str, b: &str); +} + +macro_rules! console_log { + // Note that this is using the `log` function imported above during + // `bare_bones` + ($($t:tt)*) => (log(&format_args!($($t)*).to_string())) +} + +fn generate_key_pair() -> ObliviousDoHKeyPair { + let ikm_bytes = hex::decode(IKM).unwrap(); + let (secret_key, public_key) = Kem::derive_keypair(&ikm_bytes); + let public_key_bytes = public_key.to_bytes().to_vec(); + let odoh_public_key = ObliviousDoHConfigContents { + kem_id: KEM_ID, + kdf_id: KDF_ID, + aead_id: AEAD_ID, + public_key: public_key_bytes, + }; + ObliviousDoHKeyPair { + private_key: secret_key, + public_key: odoh_public_key, + } +} + +#[wasm_bindgen] +pub fn get_odoh_config() -> js_sys::Uint8Array { + let key_pair = generate_key_pair(); + let public_key_bytes = key_pair.public_key.public_key; + let length_bytes = (public_key_bytes.len() as u16).to_be_bytes(); + let odoh_config_length = 12 + public_key_bytes.len(); + let version = ODOH_VERSION; + let odoh_contents_length = 8 + public_key_bytes.len(); + let kem_id = KEM_ID; // DHKEM(X25519, HKDF-SHA256) + let kdf_id = KDF_ID; // KDF(SHA-256) + let aead_id = AEAD_ID; // AEAD(AES-GCM-128) + let mut result = vec![]; + result.extend(&((odoh_config_length as u16).to_be_bytes())); + result.extend(&((version as u16).to_be_bytes())); + result.extend(&((odoh_contents_length as u16).to_be_bytes())); + result.extend(&((kem_id as u16).to_be_bytes())); + result.extend(&((kdf_id as u16).to_be_bytes())); + result.extend(&((aead_id as u16).to_be_bytes())); + result.extend(&length_bytes); + result.extend(&public_key_bytes); + return js_sys::Uint8Array::from(&result[..]); +} + +static mut QUERY_BODY: Option = None; +static mut SERVER_SECRET: Option> = None; + +#[wasm_bindgen] +pub fn decrypt_query( + odoh_encrypted_query_msg: &[u8], +) -> js_sys::Uint8Array { + let mut result = vec![]; + unsafe { + let key_pair = generate_key_pair(); + let parsed_res = + executor::block_on(parse_received_query(&key_pair, &odoh_encrypted_query_msg)); + let (parsed_query, secret) = match parsed_res { + Ok(t) => (t.0, t.1), + Err(_) => { + console_log!("parse_received_query failed!"); + return js_sys::Uint8Array::new_with_length(0) + }, + }; + + result.extend(&parsed_query.dns_msg); + + QUERY_BODY = Some(parsed_query); + SERVER_SECRET = Some(secret); + } + + return js_sys::Uint8Array::from(&result[..]); +} + +#[wasm_bindgen] +pub fn create_response( + response: &[u8], +) -> js_sys::Uint8Array { + unsafe { + if let Some(body) = &QUERY_BODY { + if let Some(secret) = &SERVER_SECRET { + // random bytes + let nonce = vec![0x1b, 0xff, 0xfd, 0xff, 0x1a, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xe]; + let result = executor::block_on(create_response_msg( + &secret, + &response, + None, + Some(nonce), + &body, + )); + let generated_response = match result { + Ok(r) => r, + Err(_) => { + console_log!("create_response_msg failed!"); + return js_sys::Uint8Array::new_with_length(0); + } + }; + + QUERY_BODY = None; + SERVER_SECRET = None; + return js_sys::Uint8Array::from(&generated_response[..]); + } + } + } + + console_log!("create_response_msg failed!"); + return js_sys::Uint8Array::new_with_length(0); +} diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json index a7682bf7879d..b4be974dbff7 100644 --- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -4174,6 +4174,15 @@ "bug_numbers": [1682552], "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"] }, + "HTTP_CHANNEL_ONSTART_SUCCESS_ODOH": { + "record_in_processes": ["main"], + "products": ["firefox"], + "expires_in_version": "never", + "kind": "boolean", + "description": "Successfully started HTTP channels when ODoH is enabled", + "bug_numbers": [1689987], + "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"] + }, "HTTP_CONNECTION_ENTRY_CACHE_HIT_1": { "record_in_processes": ["main", "socket"], "products": ["firefox", "fennec"], @@ -17894,6 +17903,50 @@ "kind": "boolean", "description": "Whether an HTTP request gets upgraded to HTTPS because HTTPS RR is presented" }, + "DNS_ODOH_LOOKUP_TIME": { + "record_in_processes": ["main", "socket"], + "products": ["firefox"], + "expires_in_version": "never", + "kind": "exponential", + "high": 60000, + "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"], + "bug_numbers": [1689987], + "n_buckets": 50, + "description": "Time for a completed ODoH resolution (msec)" + }, + "ODOH_SKIP_REASON_ODOH_FIRST": { + "record_in_processes": ["main", "socket"], + "products": ["firefox"], + "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"], + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 50, + "bug_numbers": [1689987], + "releaseChannelCollection": "opt-out", + "description": "When in ODoH-first mode, it lists the reason we may have skipped ODoH" + }, + "HTTP_PAGE_DNS_ODOH_LOOKUP_TIME": { + "record_in_processes": ["main", "content"], + "products": ["firefox"], + "expires_in_version": "never", + "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"], + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "description": "HTTP page channel: ODoH lookup time (ms)", + "bug_numbers": [1689987] + }, + "HTTP_SUB_DNS_ODOH_LOOKUP_TIME": { + "record_in_processes": ["main", "content"], + "products": ["firefox"], + "expires_in_version": "never", + "kind": "exponential", + "high": 30000, + "n_buckets": 50, + "bug_numbers": [1689987], + "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"], + "description": "HTTP subitem channel: ODoH lookup time (ms)" + }, "HTTP_PRELOAD_IMAGE_STARTREQUEST_DELAY": { "record_in_processes": ["content"], "products": ["firefox"], diff --git a/tools/rewriting/ThirdPartyPaths.txt b/tools/rewriting/ThirdPartyPaths.txt index 8f581671d7df..3f2bc8da5fbb 100644 --- a/tools/rewriting/ThirdPartyPaths.txt +++ b/tools/rewriting/ThirdPartyPaths.txt @@ -171,6 +171,7 @@ testing/xpcshell/dns-packet/ testing/xpcshell/node_ip/ testing/xpcshell/node-http2/ testing/xpcshell/node-ws/ +testing/xpcshell/odoh-wasm/ third_party/ toolkit/components/certviewer/content/vendor/ toolkit/components/jsoncpp/