From dd7aeda6fbcd72de4029f946a57e6c96d8abfba7 Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Tue, 31 Aug 2021 11:55:30 -0700 Subject: [PATCH] feat: add app.configureHostResolver (#30576) --- docs/api/app.md | 55 ++++++++ package.json | 4 +- shell/browser/api/electron_api_app.cc | 128 ++++++++++++++++++ .../net/system_network_context_manager.cc | 50 +++++++ spec-main/api-app-spec.ts | 54 +++++++- yarn.lock | 8 +- 6 files changed, 292 insertions(+), 7 deletions(-) diff --git a/docs/api/app.md b/docs/api/app.md index f1f6890cd61f..a017c2e86e12 100755 --- a/docs/api/app.md +++ b/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. diff --git a/package.json b/package.json index ce82c17a5864..7965149232f2 100644 --- a/package.json +++ b/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" ] } -} \ No newline at end of file +} diff --git a/shell/browser/api/electron_api_app.cc b/shell/browser/api/electron_api_app.cc index 4d4b6dab501a..55b5f2142d53 100644 --- a/shell/browser/api/electron_api_app.cc +++ b/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 { } }; +template <> +struct Converter { + static bool FromV8(v8::Isolate* isolate, + v8::Local 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 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 dns_over_https_servers; + absl::optional> + 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 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 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 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; @@ -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); } diff --git a/shell/browser/net/system_network_context_manager.cc b/shell/browser/net/system_network_context_manager.cc index f02fb58e7fc3..9daf131a52ed 100644 --- a/shell/browser/net/system_network_context_manager.cc +++ b/shell/browser/net/system_network_context_manager.cc @@ -7,11 +7,14 @@ #include #include #include +#include #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> + 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 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"; diff --git a/spec-main/api-app-spec.ts b/spec-main/api-app-spec.ts index 35cb4a54b0df..18c9ab088b5d 100644 --- a/spec-main/api-app-spec.ts +++ b/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', () => { diff --git a/yarn.lock b/yarn.lock index 2307a9855a11..9cf7591329d4 100644 --- a/yarn.lock +++ b/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"