summaryrefslogtreecommitdiffstats
path: root/netwerk/dns/HTTPSSVC.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--netwerk/dns/HTTPSSVC.cpp523
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