Browse Source

feat: add app.configureHostResolver (#30576)

Jeremy Rose 3 years ago
parent
commit
dd7aeda6fb

+ 55 - 0
docs/api/app.md

@@ -1061,6 +1061,61 @@ Imports the certificate in pkcs12 format into the platform certificate store.
 `callback` is called with the `result` of import operation, a value of `0`
 indicates success while any other value indicates failure according to Chromium [net_error_list](https://source.chromium.org/chromium/chromium/src/+/master:net/base/net_error_list.h).
 
+### `app.configureHostResolver(options)`
+
+* `options` Object
+  * `enableBuiltInResolver` Boolean (optional) - Whether the built-in host
+    resolver is used in preference to getaddrinfo. When enabled, the built-in
+    resolver will attempt to use the system's DNS settings to do DNS lookups
+    itself. Enabled by default on macOS, disabled by default on Windows and
+    Linux.
+  * `secureDnsMode` String (optional) - Can be "off", "automatic" or "secure".
+    Configures the DNS-over-HTTP mode. When "off", no DoH lookups will be
+    performed. When "automatic", DoH lookups will be peformed first if DoH is
+    available, and insecure DNS lookups will be performed as a fallback. When
+    "secure", only DoH lookups will be performed. Defaults to "automatic".
+  * `secureDnsServers` String[] (optional) - A list of DNS-over-HTTP
+    server templates. See [RFC8484 § 3][] for details on the template format.
+    Most servers support the POST method; the template for such servers is
+    simply a URI. Note that for [some DNS providers][doh-providers], the
+    resolver will automatically upgrade to DoH unless DoH is explicitly
+    disabled, even if there are no DoH servers provided in this list.
+  * `enableAdditionalDnsQueryTypes` Boolean (optional) - Controls whether additional DNS
+    query types, e.g. HTTPS (DNS type 65) will be allowed besides the
+    traditional A and AAAA queries when a request is being made via insecure
+    DNS. Has no effect on Secure DNS which always allows additional types.
+    Defaults to true.
+
+Configures host resolution (DNS and DNS-over-HTTPS). By default, the following
+resolvers will be used, in order:
+
+1. DNS-over-HTTPS, if the [DNS provider supports it][doh-providers], then
+2. the built-in resolver (enabled on macOS only by default), then
+3. the system's resolver (e.g. `getaddrinfo`).
+
+This can be configured to either restrict usage of non-encrypted DNS
+(`secureDnsMode: "secure"`), or disable DNS-over-HTTPS (`secureDnsMode:
+"off"`). It is also possible to enable or disable the built-in resolver.
+
+To disable insecure DNS, you can specify a `secureDnsMode` of `"secure"`. If you do
+so, you should make sure to provide a list of DNS-over-HTTPS servers to use, in
+case the user's DNS configuration does not include a provider that supports
+DoH.
+
+```js
+app.configureHostResolver({
+  secureDnsMode: 'secure',
+  secureDnsServers: [
+    'https://cloudflare-dns.com/dns-query'
+  ]
+})
+```
+
+This API must be called after the `ready` event is emitted.
+
+[doh-providers]: https://source.chromium.org/chromium/chromium/src/+/main:net/dns/public/doh_provider_entry.cc;l=31?q=%22DohProviderEntry::GetList()%22&ss=chromium%2Fchromium%2Fsrc
+[RFC8484 § 3]: https://datatracker.ietf.org/doc/html/rfc8484#section-3
+
 ### `app.disableHardwareAcceleration()`
 
 Disables hardware acceleration for current app.

+ 2 - 2
package.json

@@ -4,7 +4,7 @@
   "repository": "https://github.com/electron/electron",
   "description": "Build cross platform desktop apps with JavaScript, HTML, and CSS",
   "devDependencies": {
-    "@electron/docs-parser": "^0.12.1",
+    "@electron/docs-parser": "^0.12.2",
     "@electron/typescript-definitions": "^8.9.5",
     "@octokit/auth-app": "^2.10.0",
     "@octokit/rest": "^18.0.3",
@@ -141,4 +141,4 @@
       "node script/gen-hunspell-filenames.js"
     ]
   }
-}
+}

+ 128 - 0
shell/browser/api/electron_api_app.cc

@@ -19,6 +19,7 @@
 #include "base/system/sys_info.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/icon_manager.h"
+#include "chrome/common/chrome_features.h"
 #include "chrome/common/chrome_paths.h"
 #include "content/browser/gpu/compositor_util.h"        // nogncheck
 #include "content/browser/gpu/gpu_data_manager_impl.h"  // nogncheck
@@ -27,13 +28,16 @@
 #include "content/public/browser/child_process_data.h"
 #include "content/public/browser/client_certificate_delegate.h"
 #include "content/public/browser/gpu_data_manager.h"
+#include "content/public/browser/network_service_instance.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/common/content_switches.h"
 #include "media/audio/audio_manager.h"
+#include "net/dns/public/util.h"
 #include "net/ssl/client_cert_identity.h"
 #include "net/ssl/ssl_cert_request_info.h"
 #include "net/ssl/ssl_private_key.h"
 #include "sandbox/policy/switches.h"
+#include "services/network/network_service.h"
 #include "shell/browser/api/electron_api_menu.h"
 #include "shell/browser/api/electron_api_session.h"
 #include "shell/browser/api/electron_api_web_contents.h"
@@ -419,6 +423,27 @@ struct Converter<content::CertificateRequestResultType> {
   }
 };
 
+template <>
+struct Converter<net::SecureDnsMode> {
+  static bool FromV8(v8::Isolate* isolate,
+                     v8::Local<v8::Value> val,
+                     net::SecureDnsMode* out) {
+    std::string s;
+    if (!ConvertFromV8(isolate, val, &s))
+      return false;
+    if (s == "off") {
+      *out = net::SecureDnsMode::kOff;
+      return true;
+    } else if (s == "automatic") {
+      *out = net::SecureDnsMode::kAutomatic;
+      return true;
+    } else if (s == "secure") {
+      *out = net::SecureDnsMode::kSecure;
+      return true;
+    }
+    return false;
+  }
+};
 }  // namespace gin
 
 namespace electron {
@@ -1525,6 +1550,108 @@ v8::Local<v8::Value> App::GetDockAPI(v8::Isolate* isolate) {
 }
 #endif
 
+void ConfigureHostResolver(v8::Isolate* isolate,
+                           const gin_helper::Dictionary& opts) {
+  gin_helper::ErrorThrower thrower(isolate);
+  net::SecureDnsMode secure_dns_mode = net::SecureDnsMode::kOff;
+  std::string default_doh_templates;
+  if (base::FeatureList::IsEnabled(features::kDnsOverHttps)) {
+    if (features::kDnsOverHttpsFallbackParam.Get()) {
+      secure_dns_mode = net::SecureDnsMode::kAutomatic;
+    } else {
+      secure_dns_mode = net::SecureDnsMode::kSecure;
+    }
+    default_doh_templates = features::kDnsOverHttpsTemplatesParam.Get();
+  }
+  std::string server_method;
+  std::vector<net::DnsOverHttpsServerConfig> dns_over_https_servers;
+  absl::optional<std::vector<network::mojom::DnsOverHttpsServerPtr>>
+      servers_mojo;
+  if (!default_doh_templates.empty() &&
+      secure_dns_mode != net::SecureDnsMode::kOff) {
+    for (base::StringPiece server_template :
+         SplitStringPiece(default_doh_templates, " ", base::TRIM_WHITESPACE,
+                          base::SPLIT_WANT_NONEMPTY)) {
+      if (!net::dns_util::IsValidDohTemplate(server_template, &server_method)) {
+        continue;
+      }
+
+      bool use_post = server_method == "POST";
+      dns_over_https_servers.emplace_back(std::string(server_template),
+                                          use_post);
+
+      if (!servers_mojo.has_value()) {
+        servers_mojo = absl::make_optional<
+            std::vector<network::mojom::DnsOverHttpsServerPtr>>();
+      }
+
+      network::mojom::DnsOverHttpsServerPtr server_mojo =
+          network::mojom::DnsOverHttpsServer::New();
+      server_mojo->server_template = std::string(server_template);
+      server_mojo->use_post = use_post;
+      servers_mojo->emplace_back(std::move(server_mojo));
+    }
+  }
+
+  bool enable_built_in_resolver =
+      base::FeatureList::IsEnabled(features::kAsyncDns);
+  bool additional_dns_query_types_enabled = true;
+
+  if (opts.Has("enableBuiltInResolver") &&
+      !opts.Get("enableBuiltInResolver", &enable_built_in_resolver)) {
+    thrower.ThrowTypeError("enableBuiltInResolver must be a boolean");
+    return;
+  }
+
+  if (opts.Has("secureDnsMode") &&
+      !opts.Get("secureDnsMode", &secure_dns_mode)) {
+    thrower.ThrowTypeError(
+        "secureDnsMode must be one of: off, automatic, secure");
+    return;
+  }
+
+  std::vector<std::string> secure_dns_server_strings;
+  if (opts.Has("secureDnsServers")) {
+    if (!opts.Get("secureDnsServers", &secure_dns_server_strings)) {
+      thrower.ThrowTypeError("secureDnsServers must be an array of strings");
+      return;
+    }
+    servers_mojo = absl::nullopt;
+    for (const std::string& server_template : secure_dns_server_strings) {
+      std::string server_method;
+      if (!net::dns_util::IsValidDohTemplate(server_template, &server_method)) {
+        thrower.ThrowTypeError(std::string("not a valid DoH template: ") +
+                               server_template);
+        return;
+      }
+      bool use_post = server_method == "POST";
+      if (!servers_mojo.has_value()) {
+        servers_mojo = absl::make_optional<
+            std::vector<network::mojom::DnsOverHttpsServerPtr>>();
+      }
+
+      network::mojom::DnsOverHttpsServerPtr server_mojo =
+          network::mojom::DnsOverHttpsServer::New();
+      server_mojo->server_template = std::string(server_template);
+      server_mojo->use_post = use_post;
+      servers_mojo->emplace_back(std::move(server_mojo));
+    }
+  }
+
+  if (opts.Has("enableAdditionalDnsQueryTypes") &&
+      !opts.Get("enableAdditionalDnsQueryTypes",
+                &additional_dns_query_types_enabled)) {
+    thrower.ThrowTypeError("enableAdditionalDnsQueryTypes must be a boolean");
+    return;
+  }
+
+  // Configure the stub resolver. This must be done after the system
+  // NetworkContext is created, but before anything has the chance to use it.
+  content::GetNetworkService()->ConfigureStubHostResolver(
+      enable_built_in_resolver, secure_dns_mode, std::move(servers_mojo),
+      additional_dns_query_types_enabled);
+}
+
 // static
 App* App::Get() {
   static base::NoDestructor<App> app;
@@ -1671,6 +1798,7 @@ gin::ObjectTemplateBuilder App::GetObjectTemplateBuilder(v8::Isolate* isolate) {
 #endif
       .SetProperty("userAgentFallback", &App::GetUserAgentFallback,
                    &App::SetUserAgentFallback)
+      .SetMethod("configureHostResolver", &ConfigureHostResolver)
       .SetMethod("enableSandbox", &App::EnableSandbox);
 }
 

+ 50 - 0
shell/browser/net/system_network_context_manager.cc

@@ -7,11 +7,14 @@
 #include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "base/command_line.h"
 #include "base/path_service.h"
+#include "base/strings/string_split.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/net/chrome_mojo_proxy_resolver_factory.h"
+#include "chrome/common/chrome_features.h"
 #include "chrome/common/chrome_paths.h"
 #include "chrome/common/chrome_switches.h"
 #include "components/os_crypt/os_crypt.h"
@@ -21,6 +24,7 @@
 #include "content/public/common/network_service_util.h"
 #include "electron/fuses.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "net/dns/public/util.h"
 #include "net/net_buildflags.h"
 #include "services/cert_verifier/public/mojom/cert_verifier_service_factory.mojom.h"
 #include "services/network/network_service.h"
@@ -234,6 +238,52 @@ void SystemNetworkContextManager::OnNetworkServiceCreated(
       network_context_.BindNewPipeAndPassReceiver(),
       CreateNetworkContextParams());
 
+  net::SecureDnsMode default_secure_dns_mode = net::SecureDnsMode::kOff;
+  std::string default_doh_templates;
+  if (base::FeatureList::IsEnabled(features::kDnsOverHttps)) {
+    if (features::kDnsOverHttpsFallbackParam.Get()) {
+      default_secure_dns_mode = net::SecureDnsMode::kAutomatic;
+    } else {
+      default_secure_dns_mode = net::SecureDnsMode::kSecure;
+    }
+    default_doh_templates = features::kDnsOverHttpsTemplatesParam.Get();
+  }
+  std::string server_method;
+  absl::optional<std::vector<network::mojom::DnsOverHttpsServerPtr>>
+      servers_mojo;
+  if (!default_doh_templates.empty() &&
+      default_secure_dns_mode != net::SecureDnsMode::kOff) {
+    for (base::StringPiece server_template :
+         SplitStringPiece(default_doh_templates, " ", base::TRIM_WHITESPACE,
+                          base::SPLIT_WANT_NONEMPTY)) {
+      if (!net::dns_util::IsValidDohTemplate(server_template, &server_method)) {
+        continue;
+      }
+
+      bool use_post = server_method == "POST";
+
+      if (!servers_mojo.has_value()) {
+        servers_mojo = absl::make_optional<
+            std::vector<network::mojom::DnsOverHttpsServerPtr>>();
+      }
+
+      network::mojom::DnsOverHttpsServerPtr server_mojo =
+          network::mojom::DnsOverHttpsServer::New();
+      server_mojo->server_template = std::string(server_template);
+      server_mojo->use_post = use_post;
+      servers_mojo->emplace_back(std::move(server_mojo));
+    }
+  }
+
+  bool additional_dns_query_types_enabled = true;
+
+  // Configure the stub resolver. This must be done after the system
+  // NetworkContext is created, but before anything has the chance to use it.
+  content::GetNetworkService()->ConfigureStubHostResolver(
+      base::FeatureList::IsEnabled(features::kAsyncDns),
+      default_secure_dns_mode, std::move(servers_mojo),
+      additional_dns_query_types_enabled);
+
   std::string app_name = electron::Browser::Get()->GetName();
 #if defined(OS_MAC)
   KeychainPassword::GetServiceName() = app_name + " Safe Storage";

+ 53 - 1
spec-main/api-app-spec.ts

@@ -6,7 +6,7 @@ import * as net from 'net';
 import * as fs from 'fs';
 import * as path from 'path';
 import { promisify } from 'util';
-import { app, BrowserWindow, Menu, session } from 'electron/main';
+import { app, BrowserWindow, Menu, session, net as electronNet } from 'electron/main';
 import { emittedOnce } from './events-helpers';
 import { closeWindow, closeAllWindows } from './window-helpers';
 import { ifdescribe, ifit } from './spec-helpers';
@@ -1631,6 +1631,58 @@ describe('app module', () => {
       expect(app.isSecureKeyboardEntryEnabled()).to.equal(false);
     });
   });
+
+  describe('configureHostResolver', () => {
+    after(() => {
+      // Returns to the default configuration.
+      app.configureHostResolver({});
+    });
+
+    it('fails on bad arguments', () => {
+      expect(() => {
+        (app.configureHostResolver as any)();
+      }).to.throw();
+      expect(() => {
+        app.configureHostResolver({
+          secureDnsMode: 'notAValidValue' as any
+        });
+      }).to.throw();
+      expect(() => {
+        app.configureHostResolver({
+          secureDnsServers: [123 as any]
+        });
+      }).to.throw();
+    });
+
+    it('affects dns lookup behavior', async () => {
+      // 1. resolve a domain name to check that things are working
+      await expect(new Promise((resolve, reject) => {
+        electronNet.request({
+          method: 'HEAD',
+          url: 'https://www.electronjs.org'
+        }).on('response', resolve)
+          .on('error', reject)
+          .end();
+      })).to.eventually.be.fulfilled();
+      // 2. change the host resolver configuration to something that will
+      // always fail
+      app.configureHostResolver({
+        secureDnsMode: 'secure',
+        secureDnsServers: ['https://127.0.0.1:1234']
+      });
+      // 3. check that resolving domain names now fails
+      await expect(new Promise((resolve, reject) => {
+        electronNet.request({
+          method: 'HEAD',
+          // Needs to be a slightly different domain to above, otherwise the
+          // response will come from the cache.
+          url: 'https://electronjs.org'
+        }).on('response', resolve)
+          .on('error', reject)
+          .end();
+      })).to.eventually.be.rejectedWith(/ERR_NAME_NOT_RESOLVED/);
+    });
+  });
 });
 
 describe('default behavior', () => {

+ 4 - 4
yarn.lock

@@ -18,10 +18,10 @@
     esutils "^2.0.2"
     js-tokens "^4.0.0"
 
-"@electron/docs-parser@^0.12.1":
-  version "0.12.1"
-  resolved "https://registry.yarnpkg.com/@electron/docs-parser/-/docs-parser-0.12.1.tgz#254c324b5953c67cdcce0a8902736778a1788742"
-  integrity sha512-E9/GjNVlFzBM2MNOoLjiKSE0xAMM3KsxvzMKmMeORY7aDbalObFm23XCh8DC8Jn/hfh6BzgVPF3OZO9hKvs5nw==
+"@electron/docs-parser@^0.12.2":
+  version "0.12.2"
+  resolved "https://registry.yarnpkg.com/@electron/docs-parser/-/docs-parser-0.12.2.tgz#42ac92404058411be4155b25320b96192da85ba5"
+  integrity sha512-81l/jlz21VvTOZ21NyY1gd63ZPT/Ny0vY/nu9iYb2FkaGThMvy2xKNHifPcOTDkT+94jK0D8f7eUMDh75FIqCw==
   dependencies:
     "@types/markdown-it" "^10.0.0"
     chai "^4.2.0"