Support tunneled requests in NetworkServiceProxyDelegate

With this change, CustomProxyConfig can specify rules for http://,
https://, ws://, and wss:// requests with a single proxy list.

NetworkServiceProxyDelegate will now support proxy resolution not just
for http:// requests, but for any scheme (except ftp://, which isn't
proxied in //net, either).  The delegate will also modify the CONNECT
request headers to include the headers specified in CustomProxyConfig.

The meaning of CustomProxyConfig is modified to let features specify
proxying behavior precisely through net::ProxyConfig::ProxyRules.  In
particular, the proxy rules now determine which schemes are proxied with
no additional assumptions in NetworkServiceProxyDelegate.

Bug: 915659
Change-Id: I2dd2a95ab96bdf4d14cffa5c2541fbb678cd3f94
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1417455
Commit-Queue: Wojciech Dzier┼╝anowski <wdzierzanowski@opera.com>
Reviewed-by: Tarun Bansal <tbansal@chromium.org>
Reviewed-by: Matt Menke <mmenke@chromium.org>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Reviewed-by: Dominick Ng <dominickn@chromium.org>
Cr-Commit-Position: refs/heads/master@{#640680}
diff --git a/chrome/browser/data_reduction_proxy/data_reduction_proxy_browsertest.cc b/chrome/browser/data_reduction_proxy/data_reduction_proxy_browsertest.cc
index 0a7f640..6577f10 100644
--- a/chrome/browser/data_reduction_proxy/data_reduction_proxy_browsertest.cc
+++ b/chrome/browser/data_reduction_proxy/data_reduction_proxy_browsertest.cc
@@ -7,6 +7,7 @@
 #include "base/bind.h"
 #include "base/strings/strcat.h"
 #include "base/strings/string_util.h"
+#include "base/test/bind_test_util.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
 #include "chrome/browser/metrics/subprocess_metrics_provider.h"
@@ -329,6 +330,57 @@
   EXPECT_EQ(GetBody(), kDummyBody);
 }
 
+IN_PROC_BROWSER_TEST_F(DataReductionProxyBrowsertest,
+                       ProxyNotUsedForWebSocket) {
+  // Expect the WebSocket handshake to be attempted with |test_server|
+  // directly.
+  base::RunLoop web_socket_handshake_loop;
+  net::EmbeddedTestServer test_server;
+  test_server.RegisterRequestHandler(
+      base::BindRepeating(&BasicResponse, kDummyBody));
+  test_server.RegisterRequestMonitor(base::BindLambdaForTesting(
+      [&web_socket_handshake_loop](
+          const net::test_server::HttpRequest& request) {
+        if (request.headers.count("upgrade") > 0u)
+          web_socket_handshake_loop.Quit();
+      }));
+  ASSERT_TRUE(test_server.Start());
+
+  // If the DRP client (erroneously) decides to proxy the WebSocket handshake,
+  // it will attempt to establish a tunnel through |drp_server|.
+  net::EmbeddedTestServer drp_server;
+  drp_server.AddDefaultHandlers(
+      base::FilePath(FILE_PATH_LITERAL("chrome/test/data")));
+  bool tunnel_attempted = false;
+  drp_server.RegisterRequestMonitor(base::BindLambdaForTesting(
+      [&tunnel_attempted, &web_socket_handshake_loop](
+          const net::test_server::HttpRequest& request) {
+        if (request.method == net::test_server::METHOD_CONNECT) {
+          tunnel_attempted = true;
+          web_socket_handshake_loop.Quit();
+        }
+      }));
+  ASSERT_TRUE(drp_server.Start());
+  SetConfig(CreateConfigForServer(drp_server));
+  // A network change forces the config to be fetched.
+  SimulateNetworkChange(network::mojom::ConnectionType::CONNECTION_3G);
+  WaitForConfig();
+
+  ui_test_utils::NavigateToURL(browser(),
+                               GetURLWithMockHost(test_server, "/echo"));
+
+  const std::string url =
+      base::StrCat({"ws://", kMockHost, ":", test_server.base_url().port()});
+  const std::string script = R"((url => {
+    var ws = new WebSocket(url);
+  }))";
+  EXPECT_TRUE(
+      ExecuteScript(browser()->tab_strip_model()->GetActiveWebContents(),
+                    script + "('" + url + "')"));
+  web_socket_handshake_loop.Run();
+  EXPECT_FALSE(tunnel_attempted);
+}
+
 IN_PROC_BROWSER_TEST_F(DataReductionProxyBrowsertest, UMAMetricsRecorded) {
   base::HistogramTester histogram_tester;
 
diff --git a/components/data_reduction_proxy/core/browser/data_reduction_proxy_config.cc b/components/data_reduction_proxy/core/browser/data_reduction_proxy_config.cc
index 3a54223..169d63a 100644
--- a/components/data_reduction_proxy/core/browser/data_reduction_proxy_config.cc
+++ b/components/data_reduction_proxy/core/browser/data_reduction_proxy_config.cc
@@ -664,6 +664,10 @@
       // Hostnames with no dot in them.
       "<local>,"
 
+      // WebSockets
+      "ws://*,"
+      "wss://*,"
+
       // RFC6890 current network (only valid as source address).
       "0.0.0.0/8,"
 
diff --git a/components/data_reduction_proxy/core/browser/data_reduction_proxy_io_data.cc b/components/data_reduction_proxy/core/browser/data_reduction_proxy_io_data.cc
index 4b3a1d1..c34ddde5 100644
--- a/components/data_reduction_proxy/core/browser/data_reduction_proxy_io_data.cc
+++ b/components/data_reduction_proxy/core/browser/data_reduction_proxy_io_data.cc
@@ -389,12 +389,17 @@
 
   // Set an alternate proxy list to be used for media requests which only
   // contains proxies supporting the media resource type.
-  net::ProxyList media_proxies;
+  std::vector<DataReductionProxyServer> media_proxies;
   for (const auto& proxy : proxies_for_http) {
     if (proxy.SupportsResourceType(ResourceTypeProvider::CONTENT_TYPE_MEDIA))
-      media_proxies.AddProxyServer(proxy.proxy_server());
+      media_proxies.push_back(proxy);
   }
-  config->alternate_proxy_list = media_proxies;
+  config->alternate_rules =
+      configurator_
+          ->CreateProxyConfig(true /* probe_url_config */,
+                              config_->GetNetworkPropertiesManager(),
+                              media_proxies)
+          .proxy_rules();
 
   net::EffectiveConnectionType type = GetEffectiveConnectionType();
   if (type > net::EFFECTIVE_CONNECTION_TYPE_OFFLINE) {
diff --git a/components/data_reduction_proxy/core/browser/data_reduction_proxy_io_data_unittest.cc b/components/data_reduction_proxy/core/browser/data_reduction_proxy_io_data_unittest.cc
index ca981e2a..9ebaa95 100644
--- a/components/data_reduction_proxy/core/browser/data_reduction_proxy_io_data_unittest.cc
+++ b/components/data_reduction_proxy/core/browser/data_reduction_proxy_io_data_unittest.cc
@@ -275,7 +275,7 @@
   EXPECT_TRUE(
       client.config->pre_cache_headers.HasHeader(chrome_proxy_ect_header()));
   // Alternate proxy list should be empty because there are no core proxies.
-  EXPECT_TRUE(client.config->alternate_proxy_list.IsEmpty());
+  EXPECT_TRUE(client.config->alternate_rules.empty());
 }
 
 TEST_F(DataReductionProxyIODataTest, TestCustomProxyConfigUpdatedOnECTChange) {
@@ -400,9 +400,12 @@
   io_data.SetCustomProxyConfigClient(std::move(client_ptr_info));
   base::RunLoop().RunUntilIdle();
 
-  net::ProxyList expected_proxy_list;
-  expected_proxy_list.SetSingleProxyServer(core_proxy_server);
-  EXPECT_TRUE(client.config->alternate_proxy_list.Equals(expected_proxy_list));
+  net::ProxyConfig::ProxyRules expected_rules;
+  expected_rules.type =
+      net::ProxyConfig::ProxyRules::Type::PROXY_LIST_PER_SCHEME;
+  expected_rules.proxies_for_http.AddProxyServer(core_proxy_server);
+  expected_rules.proxies_for_http.AddProxyServer(net::ProxyServer::Direct());
+  EXPECT_TRUE(client.config->alternate_rules.Equals(expected_rules));
 }
 
 }  // namespace data_reduction_proxy
diff --git a/services/network/DEPS b/services/network/DEPS
index 4d8cc1a..d140caf 100644
--- a/services/network/DEPS
+++ b/services/network/DEPS
@@ -19,4 +19,5 @@
   "+services/service_manager/public",
   "+services/service_manager/sandbox",
   "+third_party/boringssl/src/include",
+  "+url",
 ]
diff --git a/services/network/network_context_unittest.cc b/services/network/network_context_unittest.cc
index f025c30..02b87cb 100644
--- a/services/network/network_context_unittest.cc
+++ b/services/network/network_context_unittest.cc
@@ -4752,9 +4752,8 @@
   auto config = mojom::CustomProxyConfig::New();
   config->rules.ParseFromString("http=" +
                                 ConvertToProxyServer(invalid_server).ToURI());
-
-  config->alternate_proxy_list.AddProxyServer(
-      ConvertToProxyServer(proxy_test_server));
+  config->alternate_rules.ParseFromString(
+      "http=" + ConvertToProxyServer(proxy_test_server).ToURI());
   proxy_config_client->OnCustomProxyConfigUpdated(std::move(config));
   scoped_task_environment_.RunUntilIdle();
 
diff --git a/services/network/network_service_proxy_delegate.cc b/services/network/network_service_proxy_delegate.cc
index 5207d9b..d2ba9ff 100644
--- a/services/network/network_service_proxy_delegate.cc
+++ b/services/network/network_service_proxy_delegate.cc
@@ -9,6 +9,7 @@
 #include "net/proxy_resolution/proxy_info.h"
 #include "net/proxy_resolution/proxy_resolution_service.h"
 #include "services/network/url_loader.h"
+#include "url/url_constants.h"
 
 namespace network {
 namespace {
@@ -67,9 +68,42 @@
   return false;
 }
 
-// Whether the custom proxy can proxy |url|.
-bool IsURLValidForProxy(const GURL& url) {
-  return url.SchemeIs(url::kHttpScheme) && !net::IsLocalhost(url);
+// Returns true if there is a possibility that |proxy_rules->Apply()| can
+// choose |target_proxy|. This does not consider the bypass rules; it only
+// scans the possible set of proxy server.
+bool RulesContainsProxy(const net::ProxyConfig::ProxyRules& proxy_rules,
+                        const net::ProxyServer& target_proxy) {
+  switch (proxy_rules.type) {
+    case net::ProxyConfig::ProxyRules::Type::EMPTY:
+      return false;
+
+    case net::ProxyConfig::ProxyRules::Type::PROXY_LIST:
+      return CheckProxyList(proxy_rules.single_proxies, target_proxy);
+
+    case net::ProxyConfig::ProxyRules::Type::PROXY_LIST_PER_SCHEME:
+      return CheckProxyList(proxy_rules.proxies_for_http, target_proxy) ||
+             CheckProxyList(proxy_rules.proxies_for_https, target_proxy);
+  }
+
+  NOTREACHED();
+  return false;
+}
+
+bool IsValidCustomProxyConfig(const mojom::CustomProxyConfig& config) {
+  switch (config.rules.type) {
+    case net::ProxyConfig::ProxyRules::Type::EMPTY:
+      return true;
+
+    case net::ProxyConfig::ProxyRules::Type::PROXY_LIST:
+      return !config.rules.single_proxies.IsEmpty();
+
+    case net::ProxyConfig::ProxyRules::Type::PROXY_LIST_PER_SCHEME:
+      return !config.rules.proxies_for_http.IsEmpty() ||
+             !config.rules.proxies_for_https.IsEmpty();
+  }
+
+  NOTREACHED();
+  return false;
 }
 
 // Merges headers from |in| to |out|. If the header already exists in |out| they
@@ -106,14 +140,21 @@
   if (!MayProxyURL(request->url()))
     return;
 
-  MergeRequestHeaders(headers, proxy_config_->pre_cache_headers);
+  // For other schemes, the headers can be added to the CONNECT request when
+  // establishing the secure tunnel instead, see OnBeforeHttp1TunnelRequest().
+  const bool scheme_is_http = request->url().SchemeIs(url::kHttpScheme);
+  if (scheme_is_http)
+    MergeRequestHeaders(headers, proxy_config_->pre_cache_headers);
 
   auto* url_loader = URLLoader::ForRequest(*request);
   if (url_loader) {
     if (url_loader->custom_proxy_use_alternate_proxy_list()) {
       should_use_alternate_proxy_list_cache_.Put(request->url().spec(), true);
     }
-    MergeRequestHeaders(headers, url_loader->custom_proxy_pre_cache_headers());
+    if (scheme_is_http) {
+      MergeRequestHeaders(headers,
+                          url_loader->custom_proxy_pre_cache_headers());
+    }
   }
 }
 
@@ -121,7 +162,13 @@
     net::URLRequest* request,
     const net::ProxyInfo& proxy_info,
     net::HttpRequestHeaders* headers) {
+  // For other schemes, the headers can be added to the CONNECT request when
+  // establishing the secure tunnel instead, see OnBeforeHttp1TunnelRequest().
+  if (!request->url().SchemeIs(url::kHttpScheme))
+    return;
+
   auto* url_loader = URLLoader::ForRequest(*request);
+
   if (IsInProxyConfig(proxy_info.proxy_server())) {
     MergeRequestHeaders(headers, proxy_config_->post_cache_headers);
 
@@ -150,7 +197,7 @@
     const std::string& method,
     const net::ProxyRetryInfoMap& proxy_retry_info,
     net::ProxyInfo* result) {
-  if (!EligibleForProxy(*result, url, method))
+  if (!EligibleForProxy(*result, method))
     return;
 
   net::ProxyInfo proxy_info;
@@ -167,7 +214,10 @@
 
 void NetworkServiceProxyDelegate::OnBeforeHttp1TunnelRequest(
     const net::ProxyServer& proxy_server,
-    net::HttpRequestHeaders* extra_headers) {}
+    net::HttpRequestHeaders* extra_headers) {
+  if (IsInProxyConfig(proxy_server))
+    MergeRequestHeaders(extra_headers, proxy_config_->connect_tunnel_headers);
+}
 
 net::Error NetworkServiceProxyDelegate::OnHttp1TunnelHeadersReceived(
     const net::ProxyServer& proxy_server,
@@ -177,8 +227,7 @@
 
 void NetworkServiceProxyDelegate::OnCustomProxyConfigUpdated(
     mojom::CustomProxyConfigPtr proxy_config) {
-  DCHECK(proxy_config->rules.empty() ||
-         !proxy_config->rules.proxies_for_http.IsEmpty());
+  DCHECK(IsValidCustomProxyConfig(*proxy_config));
   if (proxy_config_) {
     previous_proxy_configs_.push_front(std::move(proxy_config_));
     if (previous_proxy_configs_.size() > kMaxPreviousConfigs)
@@ -220,11 +269,11 @@
   if (!proxy_server.is_valid() || proxy_server.is_direct())
     return false;
 
-  if (CheckProxyList(proxy_config_->rules.proxies_for_http, proxy_server))
+  if (RulesContainsProxy(proxy_config_->rules, proxy_server))
     return true;
 
   for (const auto& config : previous_proxy_configs_) {
-    if (CheckProxyList(config->rules.proxies_for_http, proxy_server))
+    if (RulesContainsProxy(config->rules, proxy_server))
       return true;
   }
 
@@ -232,13 +281,10 @@
 }
 
 bool NetworkServiceProxyDelegate::MayProxyURL(const GURL& url) const {
-  return IsURLValidForProxy(url) && !proxy_config_->rules.empty();
+  return !proxy_config_->rules.empty();
 }
 
 bool NetworkServiceProxyDelegate::MayHaveProxiedURL(const GURL& url) const {
-  if (!IsURLValidForProxy(url))
-    return false;
-
   if (!proxy_config_->rules.empty())
     return true;
 
@@ -252,23 +298,18 @@
 
 bool NetworkServiceProxyDelegate::EligibleForProxy(
     const net::ProxyInfo& proxy_info,
-    const GURL& url,
     const std::string& method) const {
   return proxy_info.is_direct() && proxy_info.proxy_list().size() == 1 &&
-         MayProxyURL(url) &&
          (proxy_config_->allow_non_idempotent_methods ||
           net::HttpUtil::IsMethodIdempotent(method));
 }
 
 net::ProxyConfig::ProxyRules NetworkServiceProxyDelegate::GetProxyRulesForURL(
     const GURL& url) const {
-  net::ProxyConfig::ProxyRules rules = proxy_config_->rules;
   const auto iter = should_use_alternate_proxy_list_cache_.Peek(url.spec());
-  if (iter == should_use_alternate_proxy_list_cache_.end())
-    return rules;
-
-  rules.proxies_for_http = proxy_config_->alternate_proxy_list;
-  return rules;
+  return iter != should_use_alternate_proxy_list_cache_.end()
+             ? proxy_config_->alternate_rules
+             : proxy_config_->rules;
 }
 
 }  // namespace network
diff --git a/services/network/network_service_proxy_delegate.h b/services/network/network_service_proxy_delegate.h
index 66ae8a3..a14abdc0 100644
--- a/services/network/network_service_proxy_delegate.h
+++ b/services/network/network_service_proxy_delegate.h
@@ -70,9 +70,9 @@
   // or a previous config.
   bool MayHaveProxiedURL(const GURL& url) const;
 
-  // Whether the |url| with current |proxy_info| is eligible to be proxied.
+  // Whether the HTTP |method| with current |proxy_info| is eligible to be
+  // proxied.
   bool EligibleForProxy(const net::ProxyInfo& proxy_info,
-                        const GURL& url,
                         const std::string& method) const;
 
   // Get the proxy rules that apply to |url|.
diff --git a/services/network/network_service_proxy_delegate_unittest.cc b/services/network/network_service_proxy_delegate_unittest.cc
index 81ba245..57e1421 100644
--- a/services/network/network_service_proxy_delegate_unittest.cc
+++ b/services/network/network_service_proxy_delegate_unittest.cc
@@ -3,8 +3,12 @@
 // found in the LICENSE file.
 
 #include "services/network/network_service_proxy_delegate.h"
+
+#include <string>
+
 #include "base/test/scoped_task_environment.h"
 #include "net/url_request/url_request_test_util.h"
+#include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace network {
@@ -14,9 +18,19 @@
 constexpr char kLocalhost[] = "http://localhost";
 constexpr char kHttpsUrl[] = "https://example.com";
 constexpr char kWebsocketUrl[] = "ws://example.com";
+constexpr char kBypassUrl[] = "http://bypass.com";
 
 }  // namespace
 
+MATCHER_P2(Contain,
+           expected_name,
+           expected_value,
+           std::string("headers ") + (negation ? "don't " : "") + "contain '" +
+               expected_name + ": " + expected_value + "'") {
+  std::string value;
+  return arg.GetHeader(expected_name, &value) && value == expected_value;
+}
+
 class NetworkServiceProxyDelegateTest : public testing::Test {
  public:
   NetworkServiceProxyDelegateTest() {}
@@ -70,23 +84,7 @@
   auto request = CreateRequest(GURL(kHttpUrl));
   delegate->OnBeforeStartTransaction(request.get(), &headers);
 
-  std::string value;
-  EXPECT_TRUE(headers.GetHeader("foo", &value));
-  EXPECT_EQ(value, "bar");
-}
-
-TEST_F(NetworkServiceProxyDelegateTest,
-       DoesNotAddHeadersBeforeCacheForLocalhost) {
-  auto config = mojom::CustomProxyConfig::New();
-  config->rules.ParseFromString("http=proxy");
-  config->pre_cache_headers.SetHeader("foo", "bar");
-  auto delegate = CreateDelegate(std::move(config));
-
-  net::HttpRequestHeaders headers;
-  auto request = CreateRequest(GURL(kLocalhost));
-  delegate->OnBeforeStartTransaction(request.get(), &headers);
-
-  EXPECT_TRUE(headers.IsEmpty());
+  EXPECT_THAT(headers, Contain("foo", "bar"));
 }
 
 TEST_F(NetworkServiceProxyDelegateTest,
@@ -115,6 +113,20 @@
   EXPECT_TRUE(headers.IsEmpty());
 }
 
+TEST_F(NetworkServiceProxyDelegateTest,
+       DoesNotAddHeadersBeforeCacheForWebSocket) {
+  auto config = mojom::CustomProxyConfig::New();
+  config->rules.ParseFromString("http=proxy");
+  config->pre_cache_headers.SetHeader("foo", "bar");
+  auto delegate = CreateDelegate(std::move(config));
+
+  net::HttpRequestHeaders headers;
+  auto request = CreateRequest(GURL(kWebsocketUrl));
+  delegate->OnBeforeStartTransaction(request.get(), &headers);
+
+  EXPECT_TRUE(headers.IsEmpty());
+}
+
 TEST_F(NetworkServiceProxyDelegateTest, AddsHeadersAfterCache) {
   auto config = mojom::CustomProxyConfig::New();
   config->rules.ParseFromString("http=proxy");
@@ -127,9 +139,7 @@
   info.UsePacString("PROXY proxy");
   delegate->OnBeforeSendHeaders(request.get(), info, &headers);
 
-  std::string value;
-  EXPECT_TRUE(headers.GetHeader("foo", &value));
-  EXPECT_EQ(value, "bar");
+  EXPECT_THAT(headers, Contain("foo", "bar"));
 }
 
 TEST_F(NetworkServiceProxyDelegateTest,
@@ -163,6 +173,40 @@
   EXPECT_TRUE(headers.IsEmpty());
 }
 
+TEST_F(NetworkServiceProxyDelegateTest, DoesNotAddHeadersAfterCacheForHttps) {
+  auto config = mojom::CustomProxyConfig::New();
+  config->rules.ParseFromString("http=proxy");
+  config->post_cache_headers.SetHeader("foo", "bar");
+  auto delegate = CreateDelegate(std::move(config));
+
+  net::HttpRequestHeaders headers;
+  auto request = CreateRequest(GURL(kHttpsUrl));
+  net::ProxyInfo info;
+  info.UsePacString("PROXY proxy");
+  delegate->OnBeforeSendHeaders(request.get(), info, &headers);
+
+  EXPECT_TRUE(headers.IsEmpty());
+}
+
+TEST_F(NetworkServiceProxyDelegateTest, DoesNotAddHeadersIfProxyIsBypassed) {
+  auto config = mojom::CustomProxyConfig::New();
+  config->rules.ParseFromString("http=proxy");
+  config->rules.bypass_rules.AddRuleFromString(GURL(kBypassUrl).host());
+  config->pre_cache_headers.SetHeader("pre", "cache");
+  config->post_cache_headers.SetHeader("post", "cache");
+  auto delegate = CreateDelegate(std::move(config));
+
+  net::HttpRequestHeaders headers;
+  auto request = CreateRequest(GURL(kBypassUrl));
+  delegate->OnBeforeStartTransaction(request.get(), &headers);
+
+  net::ProxyInfo info;
+  info.UseDirect();
+  delegate->OnBeforeSendHeaders(request.get(), info, &headers);
+
+  EXPECT_TRUE(headers.IsEmpty());
+}
+
 TEST_F(NetworkServiceProxyDelegateTest,
        RemovesPreCacheHeadersWhenProxyNotInConfig) {
   auto config = mojom::CustomProxyConfig::New();
@@ -194,9 +238,7 @@
   info.UseDirect();
   delegate->OnBeforeSendHeaders(request.get(), info, &headers);
 
-  std::string value;
-  EXPECT_TRUE(headers.GetHeader("foo", &value));
-  EXPECT_EQ(value, "value");
+  EXPECT_THAT(headers, Contain("foo", "value"));
 }
 
 TEST_F(NetworkServiceProxyDelegateTest, KeepsPreCacheHeadersWhenProxyInConfig) {
@@ -212,9 +254,7 @@
   info.UsePacString("PROXY proxy");
   delegate->OnBeforeSendHeaders(request.get(), info, &headers);
 
-  std::string value;
-  EXPECT_TRUE(headers.GetHeader("foo", &value));
-  EXPECT_EQ(value, "bar");
+  EXPECT_THAT(headers, Contain("foo", "bar"));
 }
 
 TEST_F(NetworkServiceProxyDelegateTest, KeepsHeadersWhenConfigUpdated) {
@@ -234,9 +274,7 @@
   info.UsePacString("PROXY proxy");
   delegate->OnBeforeSendHeaders(request.get(), info, &headers);
 
-  std::string value;
-  EXPECT_TRUE(headers.GetHeader("foo", &value));
-  EXPECT_EQ(value, "bar");
+  EXPECT_THAT(headers, Contain("foo", "bar"));
 }
 
 TEST_F(NetworkServiceProxyDelegateTest,
@@ -260,6 +298,23 @@
   EXPECT_TRUE(headers.IsEmpty());
 }
 
+TEST_F(NetworkServiceProxyDelegateTest, AddsHeadersToTunnelRequest) {
+  auto config = mojom::CustomProxyConfig::New();
+  config->rules.ParseFromString("https://proxy");
+  config->pre_cache_headers.SetHeader("pre_cache", "foo");
+  config->post_cache_headers.SetHeader("post_cache", "bar");
+  config->connect_tunnel_headers.SetHeader("connect", "baz");
+  auto delegate = CreateDelegate(std::move(config));
+
+  net::HttpRequestHeaders headers;
+  auto proxy_server = net::ProxyServer::FromPacString("HTTPS proxy");
+  delegate->OnBeforeHttp1TunnelRequest(proxy_server, &headers);
+
+  EXPECT_FALSE(headers.HasHeader("pre_cache"));
+  EXPECT_FALSE(headers.HasHeader("post_cache"));
+  EXPECT_THAT(headers, Contain("connect", "baz"));
+}
+
 TEST_F(NetworkServiceProxyDelegateTest, OnResolveProxySuccessHttpProxy) {
   auto config = mojom::CustomProxyConfig::New();
   config->rules.ParseFromString("http=foo");
@@ -296,6 +351,52 @@
             net::ProxyServer::FromPacString("QUIC foo"));
 }
 
+TEST_F(NetworkServiceProxyDelegateTest, OnResolveProxySuccessHttpsUrl) {
+  auto config = mojom::CustomProxyConfig::New();
+  config->rules.ParseFromString("https://foo");
+  auto delegate = CreateDelegate(std::move(config));
+
+  net::ProxyInfo result;
+  result.UseDirect();
+  delegate->OnResolveProxy(GURL(kHttpsUrl), "GET", net::ProxyRetryInfoMap(),
+                           &result);
+
+  net::ProxyList expected_proxy_list;
+  expected_proxy_list.AddProxyServer(
+      net::ProxyServer::FromPacString("HTTPS foo"));
+  EXPECT_TRUE(result.proxy_list().Equals(expected_proxy_list));
+}
+
+TEST_F(NetworkServiceProxyDelegateTest, OnResolveProxySuccessWebSocketUrl) {
+  auto config = mojom::CustomProxyConfig::New();
+  config->rules.ParseFromString("https://foo");
+  auto delegate = CreateDelegate(std::move(config));
+
+  net::ProxyInfo result;
+  result.UseDirect();
+  delegate->OnResolveProxy(GURL(kWebsocketUrl), "GET", net::ProxyRetryInfoMap(),
+                           &result);
+
+  net::ProxyList expected_proxy_list;
+  expected_proxy_list.AddProxyServer(
+      net::ProxyServer::FromPacString("HTTPS foo"));
+  EXPECT_TRUE(result.proxy_list().Equals(expected_proxy_list));
+}
+
+TEST_F(NetworkServiceProxyDelegateTest, OnResolveProxyNoRuleForHttpsUrl) {
+  auto config = mojom::CustomProxyConfig::New();
+  config->rules.ParseFromString("http=foo");
+  auto delegate = CreateDelegate(std::move(config));
+
+  net::ProxyInfo result;
+  result.UseDirect();
+  delegate->OnResolveProxy(GURL(kHttpsUrl), "GET", net::ProxyRetryInfoMap(),
+                           &result);
+
+  EXPECT_TRUE(result.is_direct());
+  EXPECT_FALSE(result.alternative_proxy().is_valid());
+}
+
 TEST_F(NetworkServiceProxyDelegateTest, OnResolveProxyLocalhost) {
   auto config = mojom::CustomProxyConfig::New();
   config->rules.ParseFromString("http=foo");
@@ -354,9 +455,12 @@
   EXPECT_TRUE(result.proxy_list().Equals(expected_proxy_list));
 }
 
-TEST_F(NetworkServiceProxyDelegateTest, OnResolveProxyWebsocketScheme) {
+TEST_F(NetworkServiceProxyDelegateTest,
+       OnResolveProxyBypassForWebSocketScheme) {
   auto config = mojom::CustomProxyConfig::New();
   config->rules.ParseFromString("http=foo");
+  config->rules.bypass_rules.AddRuleFromString(GURL(kWebsocketUrl).scheme() +
+                                               "://*");
   auto delegate = CreateDelegate(std::move(config));
 
   net::ProxyInfo result;
diff --git a/services/network/public/mojom/network_context.mojom b/services/network/public/mojom/network_context.mojom
index 7e0994c..b4db742 100644
--- a/services/network/public/mojom/network_context.mojom
+++ b/services/network/public/mojom/network_context.mojom
@@ -45,15 +45,15 @@
 // Config for setting a custom proxy config that will be used if a request
 // matches the proxy rules and would otherwise be direct. This config allows
 // headers to be set on requests to the proxies from the config before and/or
-// after the caching layer. Currently only supports proxying http requests.
+// after the caching layer.
 struct CustomProxyConfig {
-  // The custom proxy rules to use. Right now this is limited to proxies for
-  // http requests.
+  // The custom proxy rules to use. Note that ftp:// requests are not
+  // supported.
   ProxyRules rules;
 
-  // List of proxies that will be used if
+  // The proxy rules that will be used if
   // ResourceRequest::custom_proxy_use_alternate_proxy_list is set.
-  ProxyList alternate_proxy_list;
+  ProxyRules alternate_rules;
 
   // Whether the custom proxy config should apply to requests using
   // non-idempotent methods. Can be true if the proxy is known to handle this
@@ -67,17 +67,22 @@
   // |custom_proxy_pre_cache_headers| and |custom_proxy_post_cache_headers|
   // fields in ResourceRequest.
   //
-  // Headers that will be set before the cache for http requests. If the request
-  // does not use a custom proxy, these headers will be removed before sending
-  // to the network. If a request already has one of these headers set, it may
-  // be overwritten if a custom proxy is used, or removed if a custom proxy is
-  // not used.
+  // Headers that will be set before the cache for http:// requests. If the
+  // request does not use a custom proxy, these headers will be removed before
+  // sending to the network. If a request already has one of these headers set,
+  // it may be overwritten if a custom proxy is used, or removed if a custom
+  // proxy is not used.
   HttpRequestHeaders pre_cache_headers;
 
-  // Headers that will be set after the cache for requests that are issued
-  // through a custom proxy. Headers here will overwrite matching headers on the
-  // request if a custom proxy is used.
+  // Headers that will be set after the cache for http:// requests that are
+  // issued through a custom proxy. Headers here will overwrite matching
+  // headers on the request if a custom proxy is used.
   HttpRequestHeaders post_cache_headers;
+
+  // For tunneled requests (https://, ws://, wss://), these headers are added
+  // to the CONNECT request. Headers here will overwrite matching headers on
+  // the CONNECT request if a custom proxy is used.
+  HttpRequestHeaders connect_tunnel_headers;
 };
 
 // Client to update the custom proxy config.
diff --git a/services/network/public/mojom/url_loader.mojom b/services/network/public/mojom/url_loader.mojom
index 1d46b702..39d6ca6 100644
--- a/services/network/public/mojom/url_loader.mojom
+++ b/services/network/public/mojom/url_loader.mojom
@@ -283,9 +283,10 @@
   // The profile ID of network conditions to throttle the network request.
   mojo_base.mojom.UnguessableToken? throttling_profile_id;
 
-  // Headers that will be added pre and post cache if the network context uses
-  // the custom proxy for this request. The custom proxy is used for requests
-  // that match the custom proxy config, and would otherwise be made direct.
+  // Headers that will be added pre and post cache for http:// requests if the
+  // network context uses the custom proxy for this request. The custom proxy
+  // is used for requests that match the custom proxy config, and would
+  // otherwise be made direct.
   HttpRequestHeaders custom_proxy_pre_cache_headers;
   HttpRequestHeaders custom_proxy_post_cache_headers;