1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 
6 #include "ODoHService.h"
7 
8 #include "DNSUtils.h"
9 #include "mozilla/net/SocketProcessChild.h"
10 #include "mozilla/Preferences.h"
11 #include "mozilla/ScopeExit.h"
12 #include "mozilla/StaticPrefs_network.h"
13 #include "nsIDNSService.h"
14 #include "nsIDNSByTypeRecord.h"
15 #include "nsIOService.h"
16 #include "nsIObserverService.h"
17 #include "nsNetUtil.h"
18 #include "ODoH.h"
19 #include "TRRService.h"
20 #include "nsURLHelper.h"
21 // Put DNSLogging.h at the end to avoid LOG being overwritten by other headers.
22 #include "DNSLogging.h"
23 
24 static const char kODoHProxyURIPref[] = "network.trr.odoh.proxy_uri";
25 static const char kODoHTargetHostPref[] = "network.trr.odoh.target_host";
26 static const char kODoHTargetPathPref[] = "network.trr.odoh.target_path";
27 static const char kODoHConfigsUriPref[] = "network.trr.odoh.configs_uri";
28 
29 namespace mozilla {
30 namespace net {
31 
32 ODoHService* gODoHService = nullptr;
33 
NS_IMPL_ISUPPORTS(ODoHService,nsIDNSListener,nsIObserver,nsISupportsWeakReference,nsITimerCallback,nsIStreamLoaderObserver)34 NS_IMPL_ISUPPORTS(ODoHService, nsIDNSListener, nsIObserver,
35                   nsISupportsWeakReference, nsITimerCallback,
36                   nsIStreamLoaderObserver)
37 
38 ODoHService::ODoHService()
39     : mLock("net::ODoHService"), mQueryODoHConfigInProgress(false) {
40   gODoHService = this;
41 }
42 
~ODoHService()43 ODoHService::~ODoHService() { gODoHService = nullptr; }
44 
Init()45 bool ODoHService::Init() {
46   MOZ_ASSERT(NS_IsMainThread(), "wrong thread");
47 
48   nsCOMPtr<nsIPrefBranch> prefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID));
49   if (!prefBranch) {
50     return false;
51   }
52 
53   prefBranch->AddObserver(kODoHProxyURIPref, this, true);
54   prefBranch->AddObserver(kODoHTargetHostPref, this, true);
55   prefBranch->AddObserver(kODoHTargetPathPref, this, true);
56   prefBranch->AddObserver(kODoHConfigsUriPref, this, true);
57 
58   ReadPrefs(nullptr);
59 
60   nsCOMPtr<nsIObserverService> observerService =
61       mozilla::services::GetObserverService();
62   if (observerService) {
63     observerService->AddObserver(this, "xpcom-shutdown-threads", true);
64   }
65 
66   return true;
67 }
68 
Enabled() const69 bool ODoHService::Enabled() const {
70   return StaticPrefs::network_trr_odoh_enabled();
71 }
72 
73 NS_IMETHODIMP
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * aData)74 ODoHService::Observe(nsISupports* aSubject, const char* aTopic,
75                      const char16_t* aData) {
76   MOZ_ASSERT(NS_IsMainThread(), "wrong thread");
77   if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
78     ReadPrefs(NS_ConvertUTF16toUTF8(aData).get());
79   } else if (!strcmp(aTopic, "xpcom-shutdown-threads")) {
80     if (mTTLTimer) {
81       mTTLTimer->Cancel();
82       mTTLTimer = nullptr;
83     }
84   }
85 
86   return NS_OK;
87 }
88 
ReadPrefs(const char * aName)89 nsresult ODoHService::ReadPrefs(const char* aName) {
90   if (!aName || !strcmp(aName, kODoHConfigsUriPref)) {
91     OnODohConfigsURIChanged();
92   }
93   if (!aName || !strcmp(aName, kODoHProxyURIPref) ||
94       !strcmp(aName, kODoHTargetHostPref) ||
95       !strcmp(aName, kODoHTargetPathPref)) {
96     OnODoHPrefsChange(aName == nullptr);
97   }
98 
99   return NS_OK;
100 }
101 
OnODohConfigsURIChanged()102 void ODoHService::OnODohConfigsURIChanged() {
103   nsAutoCString uri;
104   Preferences::GetCString(kODoHConfigsUriPref, uri);
105 
106   bool updateConfig = false;
107   {
108     MutexAutoLock lock(mLock);
109     if (!mODoHConfigsUri.Equals(uri)) {
110       mODoHConfigsUri = uri;
111       updateConfig = true;
112     }
113   }
114 
115   if (updateConfig) {
116     UpdateODoHConfigFromURI();
117   }
118 }
119 
OnODoHPrefsChange(bool aInit)120 void ODoHService::OnODoHPrefsChange(bool aInit) {
121   nsAutoCString proxyURI;
122   Preferences::GetCString(kODoHProxyURIPref, proxyURI);
123   nsAutoCString targetHost;
124   Preferences::GetCString(kODoHTargetHostPref, targetHost);
125   nsAutoCString targetPath;
126   Preferences::GetCString(kODoHTargetPathPref, targetPath);
127 
128   bool updateODoHConfig = false;
129   {
130     MutexAutoLock lock(mLock);
131     mODoHProxyURI = proxyURI;
132     // Only update ODoHConfig when the host is really changed.
133     if (!mODoHTargetHost.Equals(targetHost) && mODoHConfigsUri.IsEmpty()) {
134       updateODoHConfig = true;
135     }
136     mODoHTargetHost = targetHost;
137     mODoHTargetPath = targetPath;
138 
139     BuildODoHRequestURI();
140   }
141 
142   if (updateODoHConfig) {
143     // When this function is called from ODoHService::Init(), it's on the same
144     // call stack as nsDNSService is inited. In this case, we need to dispatch
145     // UpdateODoHConfigFromHTTPSRR(), since recursively getting DNS service is
146     // not allowed.
147     auto task = []() { gODoHService->UpdateODoHConfigFromHTTPSRR(); };
148     if (aInit) {
149       NS_DispatchToMainThread(NS_NewRunnableFunction(
150           "ODoHService::UpdateODoHConfigFromHTTPSRR", std::move(task)));
151     } else {
152       task();
153     }
154   }
155 }
156 
ExtractHost(const nsACString & aURI,nsCString & aResult)157 static nsresult ExtractHost(const nsACString& aURI, nsCString& aResult) {
158   nsCOMPtr<nsIURI> uri;
159   nsresult rv = NS_NewURI(getter_AddRefs(uri), aURI);
160   if (NS_FAILED(rv)) {
161     return rv;
162   }
163 
164   if (!uri->SchemeIs("https")) {
165     LOG(("ODoHService host uri is not https"));
166     return NS_ERROR_FAILURE;
167   }
168 
169   return uri->GetAsciiHost(aResult);
170 }
171 
BuildODoHRequestURI()172 void ODoHService::BuildODoHRequestURI() {
173   mLock.AssertCurrentThreadOwns();
174 
175   mODoHRequestURI.Truncate();
176   if (mODoHTargetHost.IsEmpty() || mODoHTargetPath.IsEmpty()) {
177     return;
178   }
179 
180   if (mODoHProxyURI.IsEmpty()) {
181     mODoHRequestURI.Append(mODoHTargetHost);
182     mODoHRequestURI.AppendLiteral("/");
183     mODoHRequestURI.Append(mODoHTargetPath);
184   } else {
185     nsAutoCString hostStr;
186     if (NS_FAILED(ExtractHost(mODoHTargetHost, hostStr))) {
187       return;
188     }
189 
190     mODoHRequestURI.Append(mODoHProxyURI);
191     mODoHRequestURI.AppendLiteral("?targethost=");
192     mODoHRequestURI.Append(hostStr);
193     mODoHRequestURI.AppendLiteral("&targetpath=/");
194     mODoHRequestURI.Append(mODoHTargetPath);
195   }
196 }
197 
GetRequestURI(nsACString & aResult)198 void ODoHService::GetRequestURI(nsACString& aResult) {
199   MutexAutoLock lock(mLock);
200   aResult = mODoHRequestURI;
201 }
202 
UpdateODoHConfig()203 nsresult ODoHService::UpdateODoHConfig() {
204   LOG(("ODoHService::UpdateODoHConfig"));
205   if (mQueryODoHConfigInProgress) {
206     return NS_OK;
207   }
208 
209   if (NS_SUCCEEDED(UpdateODoHConfigFromURI())) {
210     return NS_OK;
211   }
212 
213   return UpdateODoHConfigFromHTTPSRR();
214 }
215 
UpdateODoHConfigFromURI()216 nsresult ODoHService::UpdateODoHConfigFromURI() {
217   LOG(("ODoHService::UpdateODoHConfigFromURI"));
218 
219   nsAutoCString configUri;
220   {
221     MutexAutoLock lock(mLock);
222     configUri = mODoHConfigsUri;
223   }
224 
225   if (configUri.IsEmpty() || !StringBeginsWith(configUri, "https://"_ns)) {
226     LOG(("ODoHService::UpdateODoHConfigFromURI: uri is invalid"));
227     return UpdateODoHConfigFromHTTPSRR();
228   }
229 
230   nsCOMPtr<nsIEventTarget> target = gTRRService->MainThreadOrTRRThread();
231   if (!target) {
232     return NS_ERROR_UNEXPECTED;
233   }
234 
235   if (!target->IsOnCurrentThread()) {
236     nsresult rv = target->Dispatch(NS_NewRunnableFunction(
237         "ODoHService::UpdateODoHConfigFromURI",
238         []() { gODoHService->UpdateODoHConfigFromURI(); }));
239     if (NS_SUCCEEDED(rv)) {
240       // Set mQueryODoHConfigInProgress to true to avoid updating ODoHConfigs
241       // when waiting the runnable to be executed.
242       mQueryODoHConfigInProgress = true;
243     }
244     return rv;
245   }
246 
247   // In case any error happens, we should reset mQueryODoHConfigInProgress.
248   auto guard = MakeScopeExit([&]() { mQueryODoHConfigInProgress = false; });
249 
250   nsCOMPtr<nsIURI> uri;
251   nsresult rv = NS_NewURI(getter_AddRefs(uri), configUri);
252   if (NS_FAILED(rv)) {
253     return rv;
254   }
255 
256   nsCOMPtr<nsIChannel> channel;
257   rv = DNSUtils::CreateChannelHelper(uri, getter_AddRefs(channel));
258   if (NS_FAILED(rv) || !channel) {
259     LOG(("NewChannel failed!"));
260     return rv;
261   }
262 
263   channel->SetLoadFlags(
264       nsIRequest::LOAD_ANONYMOUS | nsIRequest::INHIBIT_CACHING |
265       nsIRequest::LOAD_BYPASS_CACHE | nsIChannel::LOAD_BYPASS_URL_CLASSIFIER);
266   NS_ENSURE_SUCCESS(rv, rv);
267 
268   nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(channel);
269   if (!httpChannel) {
270     return NS_ERROR_UNEXPECTED;
271   }
272 
273   // This connection should not use TRR
274   rv = httpChannel->SetTRRMode(nsIRequest::TRR_DISABLED_MODE);
275   NS_ENSURE_SUCCESS(rv, rv);
276 
277   nsCOMPtr<nsIStreamLoader> loader;
278   rv = NS_NewStreamLoader(getter_AddRefs(loader), this);
279   if (NS_FAILED(rv)) {
280     return rv;
281   }
282 
283   rv = httpChannel->AsyncOpen(loader);
284   if (NS_FAILED(rv)) {
285     return rv;
286   }
287 
288   // AsyncOpen succeeded, dismiss the guard.
289   guard.release();
290   mLoader.swap(loader);
291   return rv;
292 }
293 
UpdateODoHConfigFromHTTPSRR()294 nsresult ODoHService::UpdateODoHConfigFromHTTPSRR() {
295   LOG(("ODoHService::UpdateODoHConfigFromHTTPSRR"));
296 
297   nsAutoCString uri;
298   {
299     MutexAutoLock lock(mLock);
300     uri = mODoHTargetHost;
301   }
302 
303   nsCOMPtr<nsIDNSService> dns(
304       do_GetService("@mozilla.org/network/dns-service;1"));
305   if (!dns) {
306     return NS_ERROR_NOT_AVAILABLE;
307   }
308 
309   if (!gTRRService) {
310     return NS_ERROR_NOT_AVAILABLE;
311   }
312 
313   nsAutoCString hostStr;
314   nsresult rv = ExtractHost(uri, hostStr);
315   if (NS_FAILED(rv)) {
316     return rv;
317   }
318 
319   nsCOMPtr<nsICancelable> tmpOutstanding;
320   nsCOMPtr<nsIEventTarget> target = gTRRService->MainThreadOrTRRThread();
321   // We'd like to bypass the DNS cache, since ODoHConfigs will be updated
322   // manually by ODoHService.
323   uint32_t flags =
324       nsIDNSService::RESOLVE_DISABLE_ODOH | nsIDNSService::RESOLVE_BYPASS_CACHE;
325   rv = dns->AsyncResolveNative(hostStr, nsIDNSService::RESOLVE_TYPE_HTTPSSVC,
326                                flags, nullptr, this, target, OriginAttributes(),
327                                getter_AddRefs(tmpOutstanding));
328   LOG(("ODoHService::UpdateODoHConfig [host=%s rv=%" PRIx32 "]", hostStr.get(),
329        static_cast<uint32_t>(rv)));
330 
331   if (NS_SUCCEEDED(rv)) {
332     mQueryODoHConfigInProgress = true;
333   }
334   return rv;
335 }
336 
StartTTLTimer(uint32_t aTTL)337 void ODoHService::StartTTLTimer(uint32_t aTTL) {
338   if (mTTLTimer) {
339     mTTLTimer->Cancel();
340     mTTLTimer = nullptr;
341   }
342   LOG(("ODoHService::StartTTLTimer ttl=%d(s)", aTTL));
343   NS_NewTimerWithCallback(getter_AddRefs(mTTLTimer), this, aTTL * 1000,
344                           nsITimer::TYPE_ONE_SHOT);
345 }
346 
347 NS_IMETHODIMP
Notify(nsITimer * aTimer)348 ODoHService::Notify(nsITimer* aTimer) {
349   MOZ_ASSERT(aTimer == mTTLTimer);
350   UpdateODoHConfig();
351   return NS_OK;
352 }
353 
ODoHConfigUpdateDone(uint32_t aTTL,Span<const uint8_t> aRawConfig)354 void ODoHService::ODoHConfigUpdateDone(uint32_t aTTL,
355                                        Span<const uint8_t> aRawConfig) {
356   MOZ_ASSERT_IF(XRE_IsParentProcess() && gTRRService,
357                 NS_IsMainThread() || gTRRService->IsOnTRRThread());
358   MOZ_ASSERT_IF(XRE_IsSocketProcess(), NS_IsMainThread());
359 
360   mQueryODoHConfigInProgress = false;
361   mODoHConfigs.reset();
362 
363   nsTArray<ObliviousDoHConfig> configs;
364   if (ODoHDNSPacket::ParseODoHConfigs(aRawConfig, configs)) {
365     mODoHConfigs.emplace(std::move(configs));
366   }
367 
368   // Let observers know whether ODoHService is activated or not.
369   bool hasODoHConfigs = mODoHConfigs && !mODoHConfigs->IsEmpty();
370   if (aTTL < StaticPrefs::network_trr_odoh_min_ttl()) {
371     aTTL = StaticPrefs::network_trr_odoh_min_ttl();
372   }
373   auto task = [hasODoHConfigs, aTTL]() {
374     MOZ_ASSERT(NS_IsMainThread());
375     if (XRE_IsSocketProcess()) {
376       SocketProcessChild::GetSingleton()->SendODoHServiceActivated(
377           hasODoHConfigs);
378     }
379 
380     nsCOMPtr<nsIObserverService> observerService =
381         mozilla::services::GetObserverService();
382 
383     if (observerService) {
384       observerService->NotifyObservers(nullptr, "odoh-service-activated",
385                                        hasODoHConfigs ? u"true" : u"false");
386     }
387 
388     if (aTTL) {
389       gODoHService->StartTTLTimer(aTTL);
390     }
391   };
392 
393   if (NS_IsMainThread()) {
394     task();
395   } else {
396     NS_DispatchToMainThread(
397         NS_NewRunnableFunction("ODoHService::Activated", std::move(task)));
398   }
399 
400   if (!mPendingRequests.IsEmpty()) {
401     nsTArray<RefPtr<ODoH>> requests = std::move(mPendingRequests);
402     nsCOMPtr<nsIEventTarget> target = gTRRService->MainThreadOrTRRThread();
403     for (auto& query : requests) {
404       target->Dispatch(query.forget());
405     }
406   }
407 }
408 
409 NS_IMETHODIMP
OnLookupComplete(nsICancelable * aRequest,nsIDNSRecord * aRec,nsresult aStatus)410 ODoHService::OnLookupComplete(nsICancelable* aRequest, nsIDNSRecord* aRec,
411                               nsresult aStatus) {
412   MOZ_ASSERT_IF(XRE_IsParentProcess() && gTRRService,
413                 NS_IsMainThread() || gTRRService->IsOnTRRThread());
414   MOZ_ASSERT_IF(XRE_IsSocketProcess(), NS_IsMainThread());
415 
416   nsCOMPtr<nsIDNSHTTPSSVCRecord> httpsRecord;
417   nsCString rawODoHConfig;
418   auto notifyDone = MakeScopeExit([&]() {
419     uint32_t ttl = 0;
420     if (httpsRecord) {
421       Unused << httpsRecord->GetTtl(&ttl);
422     }
423 
424     ODoHConfigUpdateDone(
425         ttl,
426         Span(reinterpret_cast<const uint8_t*>(rawODoHConfig.BeginReading()),
427              rawODoHConfig.Length()));
428   });
429 
430   LOG(("ODoHService::OnLookupComplete [aStatus=%" PRIx32 "]",
431        static_cast<uint32_t>(aStatus)));
432   if (NS_FAILED(aStatus)) {
433     return NS_OK;
434   }
435 
436   httpsRecord = do_QueryInterface(aRec);
437   if (!httpsRecord) {
438     return NS_OK;
439   }
440 
441   nsTArray<RefPtr<nsISVCBRecord>> svcbRecords;
442   httpsRecord->GetRecords(svcbRecords);
443   for (const auto& record : svcbRecords) {
444     Unused << record->GetODoHConfig(rawODoHConfig);
445     if (!rawODoHConfig.IsEmpty()) {
446       break;
447     }
448   }
449 
450   return NS_OK;
451 }
452 
453 NS_IMETHODIMP
OnStreamComplete(nsIStreamLoader * aLoader,nsISupports * aContext,nsresult aStatus,uint32_t aLength,const uint8_t * aContent)454 ODoHService::OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext,
455                               nsresult aStatus, uint32_t aLength,
456                               const uint8_t* aContent) {
457   MOZ_ASSERT_IF(XRE_IsParentProcess() && gTRRService,
458                 NS_IsMainThread() || gTRRService->IsOnTRRThread());
459   MOZ_ASSERT_IF(XRE_IsSocketProcess(), NS_IsMainThread());
460   LOG(("ODoHService::OnStreamComplete aLength=%d\n", aLength));
461 
462   {
463     MutexAutoLock lock(mLock);
464     mLoader = nullptr;
465   }
466   ODoHConfigUpdateDone(0, Span(aContent, aLength));
467   return NS_OK;
468 }
469 
ODoHConfigs()470 const Maybe<nsTArray<ObliviousDoHConfig>>& ODoHService::ODoHConfigs() {
471   MOZ_ASSERT_IF(XRE_IsParentProcess() && gTRRService,
472                 NS_IsMainThread() || gTRRService->IsOnTRRThread());
473   MOZ_ASSERT_IF(XRE_IsSocketProcess(), NS_IsMainThread());
474 
475   return mODoHConfigs;
476 }
477 
AppendPendingODoHRequest(ODoH * aRequest)478 void ODoHService::AppendPendingODoHRequest(ODoH* aRequest) {
479   LOG(("ODoHService::AppendPendingODoHQuery\n"));
480   MOZ_ASSERT_IF(XRE_IsParentProcess() && gTRRService,
481                 NS_IsMainThread() || gTRRService->IsOnTRRThread());
482   MOZ_ASSERT_IF(XRE_IsSocketProcess(), NS_IsMainThread());
483 
484   mPendingRequests.AppendElement(aRequest);
485 }
486 
RemovePendingODoHRequest(ODoH * aRequest)487 bool ODoHService::RemovePendingODoHRequest(ODoH* aRequest) {
488   MOZ_ASSERT_IF(XRE_IsParentProcess() && gTRRService,
489                 NS_IsMainThread() || gTRRService->IsOnTRRThread());
490   MOZ_ASSERT_IF(XRE_IsSocketProcess(), NS_IsMainThread());
491 
492   return mPendingRequests.RemoveElement(aRequest);
493 }
494 
495 }  // namespace net
496 }  // namespace mozilla
497