diff options
Diffstat (limited to '')
-rw-r--r-- | netwerk/dns/HTTPSSVC.cpp | 523 |
1 files changed, 523 insertions, 0 deletions
diff --git a/netwerk/dns/HTTPSSVC.cpp b/netwerk/dns/HTTPSSVC.cpp new file mode 100644 index 0000000000..efaf19bbf4 --- /dev/null +++ b/netwerk/dns/HTTPSSVC.cpp @@ -0,0 +1,523 @@ +/* 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 "HTTPSSVC.h" +#include "mozilla/net/DNS.h" +#include "nsHttp.h" +#include "nsHttpHandler.h" +#include "nsNetAddr.h" +#include "nsNetUtil.h" +#include "nsIDNSService.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS(SVCBRecord, nsISVCBRecord) + +class SvcParam : public nsISVCParam, + public nsISVCParamAlpn, + public nsISVCParamNoDefaultAlpn, + public nsISVCParamPort, + public nsISVCParamIPv4Hint, + public nsISVCParamEchConfig, + public nsISVCParamIPv6Hint, + public nsISVCParamODoHConfig { + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSISVCPARAM + NS_DECL_NSISVCPARAMALPN + NS_DECL_NSISVCPARAMNODEFAULTALPN + NS_DECL_NSISVCPARAMPORT + NS_DECL_NSISVCPARAMIPV4HINT + NS_DECL_NSISVCPARAMECHCONFIG + NS_DECL_NSISVCPARAMIPV6HINT + NS_DECL_NSISVCPARAMODOHCONFIG + public: + explicit SvcParam(const SvcParamType& value) : mValue(value){}; + + private: + virtual ~SvcParam() = default; + SvcParamType mValue; +}; + +NS_IMPL_ADDREF(SvcParam) +NS_IMPL_RELEASE(SvcParam) +NS_INTERFACE_MAP_BEGIN(SvcParam) + NS_INTERFACE_MAP_ENTRY(nsISVCParam) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISVCParam) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsISVCParamAlpn, mValue.is<SvcParamAlpn>()) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsISVCParamNoDefaultAlpn, + mValue.is<SvcParamNoDefaultAlpn>()) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsISVCParamPort, mValue.is<SvcParamPort>()) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsISVCParamIPv4Hint, + mValue.is<SvcParamIpv4Hint>()) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsISVCParamEchConfig, + mValue.is<SvcParamEchConfig>()) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsISVCParamIPv6Hint, + mValue.is<SvcParamIpv6Hint>()) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsISVCParamODoHConfig, + mValue.is<SvcParamODoHConfig>()) +NS_INTERFACE_MAP_END + +NS_IMETHODIMP +SvcParam::GetType(uint16_t* aType) { + *aType = mValue.match( + [](Nothing&) { return SvcParamKeyMandatory; }, + [](SvcParamAlpn&) { return SvcParamKeyAlpn; }, + [](SvcParamNoDefaultAlpn&) { return SvcParamKeyNoDefaultAlpn; }, + [](SvcParamPort&) { return SvcParamKeyPort; }, + [](SvcParamIpv4Hint&) { return SvcParamKeyIpv4Hint; }, + [](SvcParamEchConfig&) { return SvcParamKeyEchConfig; }, + [](SvcParamIpv6Hint&) { return SvcParamKeyIpv6Hint; }, + [](SvcParamODoHConfig&) { return SvcParamKeyODoHConfig; }); + return NS_OK; +} + +NS_IMETHODIMP +SvcParam::GetAlpn(nsTArray<nsCString>& aAlpn) { + if (!mValue.is<SvcParamAlpn>()) { + MOZ_ASSERT(false, "Unexpected type for variant"); + return NS_ERROR_NOT_AVAILABLE; + } + aAlpn.AppendElements(mValue.as<SvcParamAlpn>().mValue); + return NS_OK; +} + +NS_IMETHODIMP +SvcParam::GetPort(uint16_t* aPort) { + if (!mValue.is<SvcParamPort>()) { + MOZ_ASSERT(false, "Unexpected type for variant"); + return NS_ERROR_NOT_AVAILABLE; + } + *aPort = mValue.as<SvcParamPort>().mValue; + return NS_OK; +} + +NS_IMETHODIMP +SvcParam::GetEchconfig(nsACString& aEchConfig) { + if (!mValue.is<SvcParamEchConfig>()) { + MOZ_ASSERT(false, "Unexpected type for variant"); + return NS_ERROR_NOT_AVAILABLE; + } + aEchConfig = mValue.as<SvcParamEchConfig>().mValue; + return NS_OK; +} + +NS_IMETHODIMP +SvcParam::GetIpv4Hint(nsTArray<RefPtr<nsINetAddr>>& aIpv4Hint) { + if (!mValue.is<SvcParamIpv4Hint>()) { + MOZ_ASSERT(false, "Unexpected type for variant"); + return NS_ERROR_NOT_AVAILABLE; + } + const auto& results = mValue.as<SvcParamIpv4Hint>().mValue; + for (const auto& ip : results) { + if (ip.raw.family != AF_INET) { + return NS_ERROR_UNEXPECTED; + } + RefPtr<nsINetAddr> hint = new nsNetAddr(&ip); + aIpv4Hint.AppendElement(hint); + } + + return NS_OK; +} + +NS_IMETHODIMP +SvcParam::GetIpv6Hint(nsTArray<RefPtr<nsINetAddr>>& aIpv6Hint) { + if (!mValue.is<SvcParamIpv6Hint>()) { + MOZ_ASSERT(false, "Unexpected type for variant"); + return NS_ERROR_NOT_AVAILABLE; + } + const auto& results = mValue.as<SvcParamIpv6Hint>().mValue; + for (const auto& ip : results) { + if (ip.raw.family != AF_INET6) { + return NS_ERROR_UNEXPECTED; + } + RefPtr<nsINetAddr> hint = new nsNetAddr(&ip); + aIpv6Hint.AppendElement(hint); + } + return NS_OK; +} + +NS_IMETHODIMP +SvcParam::GetODoHConfig(nsACString& aODoHConfig) { + if (!mValue.is<SvcParamODoHConfig>()) { + MOZ_ASSERT(false, "Unexpected type for variant"); + return NS_ERROR_NOT_AVAILABLE; + } + aODoHConfig = mValue.as<SvcParamODoHConfig>().mValue; + return NS_OK; +} + +bool SVCB::operator<(const SVCB& aOther) const { + if (gHttpHandler->EchConfigEnabled()) { + if (mHasEchConfig && !aOther.mHasEchConfig) { + return true; + } + if (!mHasEchConfig && aOther.mHasEchConfig) { + return false; + } + } + + return mSvcFieldPriority < aOther.mSvcFieldPriority; +} + +Maybe<uint16_t> SVCB::GetPort() const { + Maybe<uint16_t> port; + for (const auto& value : mSvcFieldValue) { + if (value.mValue.is<SvcParamPort>()) { + port.emplace(value.mValue.as<SvcParamPort>().mValue); + if (NS_FAILED(NS_CheckPortSafety(*port, "https"))) { + *port = 0; + } + return port; + } + } + + return Nothing(); +} + +bool SVCB::NoDefaultAlpn() const { + for (const auto& value : mSvcFieldValue) { + if (value.mValue.is<SvcParamKeyNoDefaultAlpn>()) { + return true; + } + } + + return false; +} + +void SVCB::GetIPHints(CopyableTArray<mozilla::net::NetAddr>& aAddresses) const { + if (mSvcFieldPriority == 0) { + return; + } + + for (const auto& value : mSvcFieldValue) { + if (value.mValue.is<SvcParamIpv4Hint>()) { + aAddresses.AppendElements(value.mValue.as<SvcParamIpv4Hint>().mValue); + } else if (value.mValue.is<SvcParamIpv6Hint>()) { + aAddresses.AppendElements(value.mValue.as<SvcParamIpv6Hint>().mValue); + } + } +} + +class AlpnComparator { + public: + bool Equals(const Tuple<nsCString, SupportedAlpnRank>& aA, + const Tuple<nsCString, SupportedAlpnRank>& aB) const { + return Get<1>(aA) == Get<1>(aB); + } + bool LessThan(const Tuple<nsCString, SupportedAlpnRank>& aA, + const Tuple<nsCString, SupportedAlpnRank>& aB) const { + return Get<1>(aA) > Get<1>(aB); + } +}; + +nsTArray<Tuple<nsCString, SupportedAlpnRank>> SVCB::GetAllAlpn() const { + nsTArray<Tuple<nsCString, SupportedAlpnRank>> alpnList; + for (const auto& value : mSvcFieldValue) { + if (value.mValue.is<SvcParamAlpn>()) { + for (const auto& alpn : value.mValue.as<SvcParamAlpn>().mValue) { + alpnList.AppendElement(MakeTuple(alpn, IsAlpnSupported(alpn))); + } + } + } + alpnList.Sort(AlpnComparator()); + return alpnList; +} + +SVCBRecord::SVCBRecord(const SVCB& data, + Maybe<Tuple<nsCString, SupportedAlpnRank>> aAlpn) + : mData(data), mAlpn(aAlpn) { + mPort = mData.GetPort(); +} + +NS_IMETHODIMP SVCBRecord::GetPriority(uint16_t* aPriority) { + *aPriority = mData.mSvcFieldPriority; + return NS_OK; +} + +NS_IMETHODIMP SVCBRecord::GetName(nsACString& aName) { + aName = mData.mSvcDomainName; + return NS_OK; +} + +Maybe<uint16_t> SVCBRecord::GetPort() { return mPort; } + +Maybe<Tuple<nsCString, SupportedAlpnRank>> SVCBRecord::GetAlpn() { + return mAlpn; +} + +NS_IMETHODIMP SVCBRecord::GetSelectedAlpn(nsACString& aAlpn) { + if (!mAlpn) { + return NS_ERROR_NOT_AVAILABLE; + } + + aAlpn = Get<0>(*mAlpn); + return NS_OK; +} + +NS_IMETHODIMP SVCBRecord::GetEchConfig(nsACString& aEchConfig) { + aEchConfig = mData.mEchConfig; + return NS_OK; +} + +NS_IMETHODIMP SVCBRecord::GetODoHConfig(nsACString& aODoHConfig) { + aODoHConfig = mData.mODoHConfig; + return NS_OK; +} + +NS_IMETHODIMP SVCBRecord::GetValues(nsTArray<RefPtr<nsISVCParam>>& aValues) { + for (const auto& v : mData.mSvcFieldValue) { + RefPtr<nsISVCParam> param = new SvcParam(v.mValue); + aValues.AppendElement(param); + } + return NS_OK; +} + +NS_IMETHODIMP SVCBRecord::GetHasIPHintAddress(bool* aHasIPHintAddress) { + *aHasIPHintAddress = mData.mHasIPHints; + return NS_OK; +} + +static bool CheckRecordIsUsable(const SVCB& aRecord, nsIDNSService* aDNSService, + const nsACString& aHost, + uint32_t& aExcludedCount) { + if (!aHost.IsEmpty()) { + bool excluded = false; + if (NS_SUCCEEDED(aDNSService->IsSVCDomainNameFailed( + aHost, aRecord.mSvcDomainName, &excluded)) && + excluded) { + // Skip if the domain name of this record was failed to connect before. + ++aExcludedCount; + return false; + } + } + + Maybe<uint16_t> port = aRecord.GetPort(); + if (port && *port == 0) { + // Found an unsafe port, skip this record. + return false; + } + + return true; +} + +static bool CheckAlpnIsUsable(SupportedAlpnRank aAlpnType, bool aNoHttp2, + bool aNoHttp3, bool aCheckHttp3ExcludedList, + const nsACString& aTargetName, + uint32_t& aExcludedCount) { + // Skip if this alpn is not supported. + if (aAlpnType == SupportedAlpnRank::NOT_SUPPORTED) { + return false; + } + + // Skip if we don't want to use http2. + if (aNoHttp2 && aAlpnType == SupportedAlpnRank::HTTP_2) { + return false; + } + + if (IsHttp3(aAlpnType)) { + if (aCheckHttp3ExcludedList && gHttpHandler->IsHttp3Excluded(aTargetName)) { + aExcludedCount++; + return false; + } + + if (aNoHttp3) { + return false; + } + } + + return true; +} + +static nsTArray<SVCBWrapper> FlattenRecords(const nsTArray<SVCB>& aRecords) { + nsTArray<SVCBWrapper> result; + for (const auto& record : aRecords) { + nsTArray<Tuple<nsCString, SupportedAlpnRank>> alpnList = + record.GetAllAlpn(); + if (alpnList.IsEmpty()) { + result.AppendElement(SVCBWrapper(record)); + } else { + for (const auto& alpn : alpnList) { + SVCBWrapper wrapper(record); + wrapper.mAlpn = Some(alpn); + result.AppendElement(wrapper); + } + } + } + return result; +} + +already_AddRefed<nsISVCBRecord> +DNSHTTPSSVCRecordBase::GetServiceModeRecordInternal( + bool aNoHttp2, bool aNoHttp3, const nsTArray<SVCB>& aRecords, + bool& aRecordsAllExcluded, bool aCheckHttp3ExcludedList) { + RefPtr<SVCBRecord> selectedRecord; + RefPtr<SVCBRecord> h3RecordWithEchConfig; + uint32_t recordHasNoDefaultAlpnCount = 0; + uint32_t recordExcludedCount = 0; + aRecordsAllExcluded = false; + nsCOMPtr<nsIDNSService> dns = do_GetService(NS_DNSSERVICE_CONTRACTID); + uint32_t h3ExcludedCount = 0; + + nsTArray<SVCBWrapper> records = FlattenRecords(aRecords); + for (const auto& record : records) { + if (record.mRecord.mSvcFieldPriority == 0) { + // In ServiceMode, the SvcPriority should never be 0. + return nullptr; + } + + if (record.mRecord.NoDefaultAlpn()) { + ++recordHasNoDefaultAlpnCount; + } + + if (!CheckRecordIsUsable(record.mRecord, dns, mHost, recordExcludedCount)) { + // Skip if this record is not usable. + continue; + } + + if (record.mAlpn) { + if (!CheckAlpnIsUsable(Get<1>(*(record.mAlpn)), aNoHttp2, aNoHttp3, + aCheckHttp3ExcludedList, + record.mRecord.mSvcDomainName, h3ExcludedCount)) { + continue; + } + + if (IsHttp3(Get<1>(*(record.mAlpn)))) { + // If the selected alpn is h3 and ech for h3 is disabled, we want + // to find out if there is another non-h3 record that has + // echConfig. If yes, we'll use the non-h3 record with echConfig + // to connect. If not, we'll use h3 to connect without echConfig. + if (record.mRecord.mHasEchConfig && + (gHttpHandler->EchConfigEnabled() && + !gHttpHandler->EchConfigEnabled(true))) { + if (!h3RecordWithEchConfig) { + // Save this h3 record for later use. + h3RecordWithEchConfig = + new SVCBRecord(record.mRecord, record.mAlpn); + // Make sure the next record is not h3. + aNoHttp3 = true; + continue; + } + } + } + } + + if (!selectedRecord) { + selectedRecord = new SVCBRecord(record.mRecord, record.mAlpn); + } + } + + if (!selectedRecord && !h3RecordWithEchConfig) { + // If all records indicate "no-default-alpn", we should not use this RRSet. + if (recordHasNoDefaultAlpnCount == records.Length()) { + return nullptr; + } + + if (recordExcludedCount == records.Length()) { + aRecordsAllExcluded = true; + return nullptr; + } + + // If all records are in http3 excluded list, try again without checking the + // excluded list. This is better than returning nothing. + if (h3ExcludedCount == records.Length() && aCheckHttp3ExcludedList) { + return GetServiceModeRecordInternal(aNoHttp2, aNoHttp3, aRecords, + aRecordsAllExcluded, false); + } + } + + if (h3RecordWithEchConfig) { + if (selectedRecord && selectedRecord->mData.mHasEchConfig) { + return selectedRecord.forget(); + } + + return h3RecordWithEchConfig.forget(); + } + + return selectedRecord.forget(); +} + +void DNSHTTPSSVCRecordBase::GetAllRecordsWithEchConfigInternal( + bool aNoHttp2, bool aNoHttp3, const nsTArray<SVCB>& aRecords, + bool* aAllRecordsHaveEchConfig, bool* aAllRecordsInH3ExcludedList, + nsTArray<RefPtr<nsISVCBRecord>>& aResult, bool aCheckHttp3ExcludedList) { + if (aRecords.IsEmpty()) { + return; + } + + *aAllRecordsHaveEchConfig = aRecords[0].mHasEchConfig; + *aAllRecordsInH3ExcludedList = false; + // The first record should have echConfig. + if (!(*aAllRecordsHaveEchConfig)) { + return; + } + + uint32_t h3ExcludedCount = 0; + nsTArray<SVCBWrapper> records = FlattenRecords(aRecords); + for (const auto& record : records) { + if (record.mRecord.mSvcFieldPriority == 0) { + // This should not happen, since GetAllRecordsWithEchConfigInternal() + // should be called only if GetServiceModeRecordInternal() returns a + // non-null record. + MOZ_ASSERT(false); + return; + } + + // Records with echConfig are in front of records without echConfig, so we + // don't have to continue. + *aAllRecordsHaveEchConfig &= record.mRecord.mHasEchConfig; + if (!(*aAllRecordsHaveEchConfig)) { + aResult.Clear(); + return; + } + + Maybe<uint16_t> port = record.mRecord.GetPort(); + if (port && *port == 0) { + // Found an unsafe port, skip this record. + continue; + } + + if (record.mAlpn) { + if (!CheckAlpnIsUsable(Get<1>(*(record.mAlpn)), aNoHttp2, aNoHttp3, + aCheckHttp3ExcludedList, + record.mRecord.mSvcDomainName, h3ExcludedCount)) { + continue; + } + } + + RefPtr<nsISVCBRecord> svcbRecord = + new SVCBRecord(record.mRecord, record.mAlpn); + aResult.AppendElement(svcbRecord); + } + + // If all records are in http3 excluded list, try again without checking the + // excluded list. This is better than returning nothing. + if (h3ExcludedCount == records.Length() && aCheckHttp3ExcludedList) { + GetAllRecordsWithEchConfigInternal( + aNoHttp2, aNoHttp3, aRecords, aAllRecordsHaveEchConfig, + aAllRecordsInH3ExcludedList, aResult, false); + *aAllRecordsInH3ExcludedList = true; + } +} + +bool DNSHTTPSSVCRecordBase::HasIPAddressesInternal( + const nsTArray<SVCB>& aRecords) { + for (const SVCB& record : aRecords) { + if (record.mSvcFieldPriority != 0) { + for (const auto& value : record.mSvcFieldValue) { + if (value.mValue.is<SvcParamIpv4Hint>()) { + return true; + } + if (value.mValue.is<SvcParamIpv6Hint>()) { + return true; + } + } + } + } + + return false; +} + +} // namespace net +} // namespace mozilla |