/* -*- 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 "nsICaptivePortalService.h" #include "nsIObserverService.h" #include "nsIURIMutator.h" #include "nsNetUtil.h" #include "nsStandardURL.h" #include "TRR.h" #include "TRRService.h" #include "mozilla/Preferences.h" static const char kOpenCaptivePortalLoginEvent[] = "captive-portal-login"; static const char kClearPrivateData[] = "clear-private-data"; static const char kPurge[] = "browser:purge-session-history"; static const char kDisableIpv6Pref[] = "network.dns.disableIPv6"; #define TRR_PREF_PREFIX "network.trr." #define TRR_PREF(x) TRR_PREF_PREFIX x namespace mozilla { namespace net { #undef LOG extern mozilla::LazyLogModule gHostResolverLog; #define LOG(args) MOZ_LOG(gHostResolverLog, mozilla::LogLevel::Debug, args) TRRService *gTRRService = nullptr; NS_IMPL_ISUPPORTS(TRRService, nsIObserver, nsISupportsWeakReference) TRRService::TRRService() : mInitialized(false) , mMode(0) , mTRRBlacklistExpireTime(72 * 3600) , mTRRTimeout(3000) , mLock("trrservice") , mConfirmationNS(NS_LITERAL_CSTRING("example.com")) , mWaitForCaptive(true) , mRfc1918(false) , mCaptiveIsPassed(false) , mUseGET(false) , mClearTRRBLStorage(false) , mConfirmationState(CONFIRM_INIT) , mRetryConfirmInterval(1000) { MOZ_ASSERT(NS_IsMainThread(), "wrong thread"); } nsresult TRRService::Init() { MOZ_ASSERT(NS_IsMainThread(), "wrong thread"); if (mInitialized) { return NS_OK; } mInitialized = true; nsCOMPtr observerService = mozilla::services::GetObserverService(); if (observerService) { observerService->AddObserver(this, NS_CAPTIVE_PORTAL_CONNECTIVITY, true); observerService->AddObserver(this, kOpenCaptivePortalLoginEvent, true); observerService->AddObserver(this, kClearPrivateData, true); observerService->AddObserver(this, kPurge, true); } nsCOMPtr prefBranch; GetPrefBranch(getter_AddRefs(prefBranch)); if (prefBranch) { prefBranch->AddObserver(TRR_PREF_PREFIX, this, true); prefBranch->AddObserver(kDisableIpv6Pref, this, true); } nsCOMPtr captivePortalService = do_GetService(NS_CAPTIVEPORTAL_CID); if (captivePortalService) { int32_t captiveState; MOZ_ALWAYS_SUCCEEDS(captivePortalService->GetState(&captiveState)); if ((captiveState == nsICaptivePortalService::UNLOCKED_PORTAL) || (captiveState == nsICaptivePortalService::NOT_CAPTIVE)) { mCaptiveIsPassed = true; } LOG(("TRRService::Init mCaptiveState=%d mCaptiveIsPassed=%d\n", captiveState, (int)mCaptiveIsPassed)); } ReadPrefs(nullptr); gTRRService = this; LOG(("Initialized TRRService\n")); return NS_OK; } bool TRRService::Enabled() { if (mConfirmationState == CONFIRM_INIT && (!mWaitForCaptive || mCaptiveIsPassed || (mMode == MODE_TRRONLY))) { LOG(("TRRService::Enabled => CONFIRM_TRYING\n")); mConfirmationState = CONFIRM_TRYING; } if (mConfirmationState == CONFIRM_TRYING) { LOG(("TRRService::Enabled MaybeConfirm()\n")); MaybeConfirm(); } if (mConfirmationState != CONFIRM_OK) { LOG(("TRRService::Enabled mConfirmationState=%d mCaptiveIsPassed=%d\n", (int)mConfirmationState, (int)mCaptiveIsPassed)); } return (mConfirmationState == CONFIRM_OK); } void TRRService::GetPrefBranch(nsIPrefBranch **result) { MOZ_ASSERT(NS_IsMainThread(), "wrong thread"); *result = nullptr; CallGetService(NS_PREFSERVICE_CONTRACTID, result); } nsresult TRRService::ReadPrefs(const char *name) { MOZ_ASSERT(NS_IsMainThread(), "wrong thread"); if (!name || !strcmp(name, TRR_PREF("mode"))) { // 0 - off, 1 - parallel, 2 - TRR first, 3 - TRR only, 4 - shadow uint32_t tmp; if (NS_SUCCEEDED(Preferences::GetUint(TRR_PREF("mode"), &tmp))) { mMode = tmp; } } if (!name || !strcmp(name, TRR_PREF("uri"))) { // Base URI, appends "?ct&dns=..." MutexAutoLock lock(mLock); nsAutoCString old(mPrivateURI); Preferences::GetCString(TRR_PREF("uri"), mPrivateURI); nsAutoCString scheme; if (!mPrivateURI.IsEmpty()) { nsCOMPtr ios(do_GetIOService()); if (ios) { ios->ExtractScheme(mPrivateURI, scheme); } } if (!mPrivateURI.IsEmpty() && !scheme.Equals("https")) { LOG(("TRRService TRR URI %s is not https. Not used.\n", mPrivateURI.get())); mPrivateURI.Truncate(); } if (!mPrivateURI.IsEmpty()) { LOG(("TRRService TRR URI %s\n", mPrivateURI.get())); } if (!old.IsEmpty() && !mPrivateURI.Equals(old)) { mClearTRRBLStorage = true; LOG(("TRRService clearing blacklist because of change is uri service\n")); } } if (!name || !strcmp(name, TRR_PREF("credentials"))) { MutexAutoLock lock(mLock); Preferences::GetCString(TRR_PREF("credentials"), mPrivateCred); } if (!name || !strcmp(name, TRR_PREF("confirmationNS"))) { MutexAutoLock lock(mLock); nsAutoCString old(mConfirmationNS); Preferences::GetCString(TRR_PREF("confirmationNS"), mConfirmationNS); if (name && !old.IsEmpty() && !mConfirmationNS.Equals(old) && (mConfirmationState > CONFIRM_TRYING)) { LOG(("TRR::ReadPrefs: restart confirmationNS state\n")); mConfirmationState = CONFIRM_TRYING; } } if (!name || !strcmp(name, TRR_PREF("bootstrapAddress"))) { MutexAutoLock lock(mLock); Preferences::GetCString(TRR_PREF("bootstrapAddress"), mBootstrapAddr); } if (!name || !strcmp(name, TRR_PREF("wait-for-portal"))) { // Wait for captive portal? bool tmp; if (NS_SUCCEEDED(Preferences::GetBool(TRR_PREF("wait-for-portal"), &tmp))) { mWaitForCaptive = tmp; } } if (!name || !strcmp(name, TRR_PREF("allow-rfc1918"))) { bool tmp; if (NS_SUCCEEDED(Preferences::GetBool(TRR_PREF("allow-rfc1918"), &tmp))) { mRfc1918 = tmp; } } if (!name || !strcmp(name, TRR_PREF("useGET"))) { bool tmp; if (NS_SUCCEEDED(Preferences::GetBool(TRR_PREF("useGET"), &tmp))) { mUseGET = tmp; } } if (!name || !strcmp(name, TRR_PREF("blacklist-duration"))) { // prefs is given in number of seconds uint32_t secs; if (NS_SUCCEEDED(Preferences::GetUint(TRR_PREF("blacklist-duration"), &secs))) { mTRRBlacklistExpireTime = secs; } } if (!name || !strcmp(name, TRR_PREF("request-timeout"))) { // number of milliseconds uint32_t ms; if (NS_SUCCEEDED(Preferences::GetUint(TRR_PREF("request-timeout"), &ms))) { mTRRTimeout = ms; } } if (!name || !strcmp(name, TRR_PREF("early-AAAA"))) { bool tmp; if (NS_SUCCEEDED(Preferences::GetBool(TRR_PREF("early-AAAA"), &tmp))) { mEarlyAAAA = tmp; } } if (!name || !strcmp(name, kDisableIpv6Pref)) { bool tmp; if (NS_SUCCEEDED(Preferences::GetBool(kDisableIpv6Pref, &tmp))) { mDisableIPv6 = tmp; } } return NS_OK; } nsresult TRRService::GetURI(nsCString &result) { MutexAutoLock lock(mLock); result = mPrivateURI; return NS_OK; } nsresult TRRService::GetCredentials(nsCString &result) { MutexAutoLock lock(mLock); result = mPrivateCred; return NS_OK; } nsresult TRRService::Start() { MOZ_ASSERT(NS_IsMainThread(), "wrong thread"); if (!mInitialized) { return NS_ERROR_NOT_INITIALIZED; } return NS_OK; } TRRService::~TRRService() { MOZ_ASSERT(NS_IsMainThread(), "wrong thread"); LOG(("Exiting TRRService\n")); gTRRService = nullptr; } NS_IMETHODIMP TRRService::Observe(nsISupports *aSubject, const char * aTopic, const char16_t * aData) { MOZ_ASSERT(NS_IsMainThread(), "wrong thread"); LOG(("TRR::Observe() topic=%s\n", aTopic)); if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { ReadPrefs(NS_ConvertUTF16toUTF8(aData).get()); if (((mConfirmationState == CONFIRM_INIT) && !mBootstrapAddr.IsEmpty() && (mMode == MODE_TRRONLY)) || (mConfirmationState == CONFIRM_FAILED)) { mConfirmationState = CONFIRM_TRYING; MaybeConfirm(); } } else if (!strcmp(aTopic, kOpenCaptivePortalLoginEvent)) { // We are in a captive portal LOG(("TRRservice in captive portal\n")); mCaptiveIsPassed = false; } else if (!strcmp(aTopic, NS_CAPTIVE_PORTAL_CONNECTIVITY)) { nsAutoCString data = NS_ConvertUTF16toUTF8(aData); LOG(("TRRservice captive portal was %s\n", data.get())); if (!mTRRBLStorage) { mTRRBLStorage = DataStorage::Get(DataStorageClass::TRRBlacklist); if (mTRRBLStorage) { bool storageWillPersist = true; if (NS_FAILED(mTRRBLStorage->Init(storageWillPersist))) { mTRRBLStorage = nullptr; } if (mClearTRRBLStorage) { if (mTRRBLStorage) { mTRRBLStorage->Clear(); } mClearTRRBLStorage = false; } } } if (mConfirmationState != CONFIRM_OK) { mConfirmationState = CONFIRM_TRYING; MaybeConfirm(); } else { LOG(("TRRservice CP clear when already up!\n")); } mCaptiveIsPassed = true; } else if (!strcmp(aTopic, kClearPrivateData) || !strcmp(aTopic, kPurge)) { // flush the TRR blacklist, both in-memory and on-disk if (mTRRBLStorage) { mTRRBLStorage->Clear(); } } return NS_OK; } void TRRService::MaybeConfirm() { if (TRR_DISABLED(mMode) || mConfirmer || mConfirmationState != CONFIRM_TRYING) { LOG(("TRRService:MaybeConfirm mode=%d, mConfirmer=%p mConfirmationState=%d\n", (int)mMode, (void *)mConfirmer, (int)mConfirmationState)); return; } nsAutoCString host; { MutexAutoLock lock(mLock); host = mConfirmationNS; } if (host.Equals("skip")) { LOG(("TRRService starting confirmation test %s SKIPPED\n", mPrivateURI.get())); mConfirmationState = CONFIRM_OK; } else { LOG(("TRRService starting confirmation test %s %s\n", mPrivateURI.get(), host.get())); mConfirmer = new TRR(this, host, TRRTYPE_NS, false); NS_DispatchToMainThread(mConfirmer); } } bool TRRService::MaybeBootstrap(const nsACString &aPossible, nsACString &aResult) { MutexAutoLock lock(mLock); if (TRR_DISABLED(mMode) || mBootstrapAddr.IsEmpty()) { return false; } nsCOMPtr url; nsresult rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID) .Apply(NS_MutatorMethod(&nsIStandardURLMutator::Init, nsIStandardURL::URLTYPE_STANDARD, 443, mPrivateURI, nullptr, nullptr, nullptr)) .Finalize(url); if (NS_FAILED(rv)) { LOG(("TRRService::MaybeBootstrap failed to create URI!\n")); return false; } nsAutoCString host; url->GetHost(host); if (!aPossible.Equals(host)) { return false; } LOG(("TRRService::MaybeBootstrap: use %s instead of %s\n", mBootstrapAddr.get(), host.get())); aResult = mBootstrapAddr; return true; } // When running in TRR-only mode, the blacklist is not used and it will also // try resolving the localhost / .local names. bool TRRService::IsTRRBlacklisted(const nsACString &aHost, bool privateBrowsing, bool aParentsToo) // false if domain { if (mMode == MODE_TRRONLY) { return false; // might as well try } // hardcode these so as to not worry about expiration if (StringEndsWith(aHost, NS_LITERAL_CSTRING(".local")) || aHost.Equals(NS_LITERAL_CSTRING("localhost"))) { return true; } if (!Enabled()) { return true; } if (!mTRRBLStorage) { return false; } // Only use the Storage API in the main thread MOZ_ASSERT(NS_IsMainThread(), "wrong thread"); if (mClearTRRBLStorage) { mTRRBLStorage->Clear(); mClearTRRBLStorage = false; return false; // just cleared! } int32_t dot = aHost.FindChar('.'); if ((dot == kNotFound) && aParentsToo) { // Only if a full host name. Domains can be dotless to be able to // blacklist entire TLDs return true; } else if(dot != kNotFound) { // there was a dot, check the parent first dot++; nsDependentCSubstring domain = Substring(aHost, dot, aHost.Length() - dot); nsAutoCString check(domain); // recursively check the domain part of this name if (IsTRRBlacklisted(check, privateBrowsing, false)) { // the domain name of this name is already TRR blacklisted return true; } } MutexAutoLock lock(mLock); // use a unified casing for the hashkey nsAutoCString hashkey(aHost); nsCString val(mTRRBLStorage->Get(hashkey, privateBrowsing ? DataStorage_Private : DataStorage_Persistent)); if (!val.IsEmpty()) { nsresult code; int32_t until = val.ToInteger(&code) + mTRRBlacklistExpireTime; int32_t expire = NowInSeconds(); if (NS_SUCCEEDED(code) && (until > expire)) { LOG(("Host [%s] is TRR blacklisted\n", nsCString(aHost).get())); return true; } // the blacklisted entry has expired RefPtr storage = mTRRBLStorage; nsCOMPtr runnable = NS_NewRunnableFunction("proxyStorageRemove", [storage, hashkey, privateBrowsing]() { storage->Remove(hashkey, privateBrowsing ? DataStorage_Private : DataStorage_Persistent); }); if (!NS_IsMainThread()) { NS_DispatchToMainThread(runnable); } else { runnable->Run(); } } return false; } class ProxyBlacklist : public Runnable { public: ProxyBlacklist(TRRService *service, const nsACString &aHost, bool pb, bool aParentsToo) : mozilla::Runnable("proxyBlackList") , mService(service), mHost(aHost), mPB(pb), mParentsToo(aParentsToo) { } NS_IMETHOD Run() override { mService->TRRBlacklist(mHost, mPB, mParentsToo); mService = nullptr; return NS_OK; } private: RefPtr mService; nsCString mHost; bool mPB; bool mParentsToo; }; void TRRService::TRRBlacklist(const nsACString &aHost, bool privateBrowsing, bool aParentsToo) { if (!mTRRBLStorage) { return; } if (!NS_IsMainThread()) { NS_DispatchToMainThread(new ProxyBlacklist(this, aHost, privateBrowsing, aParentsToo)); return; } LOG(("TRR blacklist %s\n", nsCString(aHost).get())); nsAutoCString hashkey(aHost); nsAutoCString val; val.AppendInt( NowInSeconds() ); // creation time // this overwrites any existing entry { MutexAutoLock lock(mLock); mTRRBLStorage->Put(hashkey, val, privateBrowsing ? DataStorage_Private : DataStorage_Persistent); } if (aParentsToo) { // when given a full host name, verify its domain as well int32_t dot = aHost.FindChar('.'); if (dot != kNotFound) { // this has a domain to be checked dot++; nsDependentCSubstring domain = Substring(aHost, dot, aHost.Length() - dot); nsAutoCString check(domain); if (IsTRRBlacklisted(check, privateBrowsing, false)) { // the domain part is already blacklisted, no need to add this entry return; } // verify 'check' over TRR LOG(("TRR: verify if '%s' resolves as NS\n", check.get())); // check if there's an NS entry for this name RefPtr trr = new TRR(this, check, TRRTYPE_NS, privateBrowsing); NS_DispatchToMainThread(trr); } } } NS_IMETHODIMP TRRService::Notify(nsITimer *aTimer) { if (aTimer == mRetryConfirmTimer) { mRetryConfirmTimer = nullptr; if (mConfirmationState == CONFIRM_FAILED) { LOG(("TRRService retry NS of %s\n", mConfirmationNS.get())); mConfirmationState = CONFIRM_TRYING; MaybeConfirm(); } } else { MOZ_CRASH("Unknown timer"); } return NS_OK; } AHostResolver::LookupStatus TRRService::CompleteLookup(nsHostRecord *rec, nsresult status, AddrInfo *aNewRRSet, bool pb) { // this is an NS check for the TRR blacklist or confirmationNS check MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(!rec); nsAutoPtr newRRSet(aNewRRSet); MOZ_ASSERT(newRRSet && newRRSet->IsTRR() == TRRTYPE_NS); MOZ_ASSERT(!mConfirmer || (mConfirmationState == CONFIRM_TRYING)); if (mConfirmationState == CONFIRM_TRYING) { MOZ_ASSERT(mConfirmer); mConfirmationState = NS_SUCCEEDED(status) ? CONFIRM_OK : CONFIRM_FAILED; LOG(("TRRService finishing confirmation test %s %d %X\n", mPrivateURI.get(), (int)mConfirmationState, (unsigned int)status)); mConfirmer = nullptr; if ((mConfirmationState == CONFIRM_FAILED) && (mMode == MODE_TRRONLY)) { // in TRR-only mode; retry failed confirmations NS_NewTimerWithCallback(getter_AddRefs(mRetryConfirmTimer), this, mRetryConfirmInterval, nsITimer::TYPE_ONE_SHOT); if (mRetryConfirmInterval < 64000) { // double the interval up to this point mRetryConfirmInterval *= 2; } } else { if (mMode != MODE_TRRONLY) { // don't accumulate trronly data here since trronly failures are // handled above by trying again, so counting the successes here would // skew the numbers Telemetry::Accumulate(Telemetry::DNS_TRR_NS_VERFIFIED, (mConfirmationState == CONFIRM_OK)); } mRetryConfirmInterval = 1000; } return LOOKUP_OK; } // when called without a host record, this is a domain name check response. if (NS_SUCCEEDED(status)) { LOG(("TRR verified %s to be fine!\n", newRRSet->mHostName)); } else { LOG(("TRR says %s doesn't resolve as NS!\n", newRRSet->mHostName)); TRRBlacklist(nsCString(newRRSet->mHostName), pb, false); } return LOOKUP_OK; } #undef LOG } // namespace net } // namespace mozilla