Net: Plumb CNAME aliases to URLResponseHead to expose to ad-tagging.

The overall project aims to expose CNAME aliases to the
SubresourceFilter to aid in ad tagging and blocking.

A previous CL added an accessor to net::HttpBasicState as well as one
to net::HttpStream.

This change continues the plumbing by adding a field to
net::HttpResponseInfo to store the alias list in the HttpCache and the
code to net::HttpNetworkTransaction and net::HttpCache::Transaction to
save the aliases to this field.

We moreover add the code to network::URLLoader to set the field
`dns_aliases` in network::mojom::URLResponseHead.

Relevant tests:
net:net_unittests
out/Default/net_unittests --gtest_filter=*Dns*Alias*

services/network:tests
out/Default/services_unittests --gtest_filter=*Dns*Alias*

Bug: 1151047
Change-Id: I9adebfcc5fa811ece4d55e70f3d624d495ca6d1c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2569956
Reviewed-by: Matt Menke <[email protected]>
Reviewed-by: Maksim Orlovich <[email protected]>
Commit-Queue: Cammie Smith Barnes <[email protected]>
Cr-Commit-Position: refs/heads/master@{#839172}
diff --git a/net/http/http_cache_lookup_manager_unittest.cc b/net/http/http_cache_lookup_manager_unittest.cc
index 55302912..21162a33 100644
--- a/net/http/http_cache_lookup_manager_unittest.cc
+++ b/net/http/http_cache_lookup_manager_unittest.cc
@@ -63,6 +63,7 @@
       "Last-Modified: Wed, 28 Nov 2007 00:40:09 GMT\n",
       base::Time(),
       "<html><body>Google Blah Blah</body></html>",
+      {},
       TEST_MODE_NORMAL,
       nullptr,
       nullptr,
diff --git a/net/http/http_cache_transaction.cc b/net/http/http_cache_transaction.cc
index 27f9663..bb0b938 100644
--- a/net/http/http_cache_transaction.cc
+++ b/net/http/http_cache_transaction.cc
@@ -1889,6 +1889,7 @@
   response_.unused_since_prefetch = new_response_->unused_since_prefetch;
   response_.restricted_prefetch = new_response_->restricted_prefetch;
   response_.ssl_info = new_response_->ssl_info;
+  response_.dns_aliases = new_response_->dns_aliases;
   if (new_response_->vary_data.is_valid()) {
     response_.vary_data = new_response_->vary_data;
   } else if (response_.vary_data.is_valid()) {
diff --git a/net/http/http_cache_unittest.cc b/net/http/http_cache_unittest.cc
index d20774c..0c0a7c4 100644
--- a/net/http/http_cache_unittest.cc
+++ b/net/http/http_cache_unittest.cc
@@ -381,6 +381,7 @@
     "Cache-Control: max-age=10000\n",
     base::Time(),
     "<html><body>Google Blah Blah</body></html>",
+    {},
     TEST_MODE_SYNC_NET_START,
     &FastTransactionServer::FastNoStoreHandler,
     nullptr,
@@ -580,6 +581,7 @@
     "Content-Length: 10\n",
     base::Time(),
     "rg: 40-49 ",
+    {},
     TEST_MODE_NORMAL,
     &RangeTransactionServer::RangeHandler,
     nullptr,
@@ -13016,4 +13018,78 @@
                   Field(&Entry::value_uint64, Gt(0UL)))));
 }
 
+TEST_F(HttpCacheTest, DnsAliasesNoRevalidation) {
+  MockHttpCache cache;
+  HttpResponseInfo response;
+  ScopedMockTransaction transaction(kSimpleGET_Transaction);
+  transaction.dns_aliases = {"alias1", "alias2"};
+
+  RunTransactionTestWithResponseInfo(cache.http_cache(), transaction,
+                                     &response);
+  EXPECT_FALSE(response.was_cached);
+  EXPECT_THAT(response.dns_aliases, testing::ElementsAre("alias1", "alias2"));
+
+  // The second request should be cached, and the response used without
+  // revalidation. Set the transaction alias list to empty to verify that the
+  // cached aliases are being used.
+  transaction.dns_aliases = {};
+  RunTransactionTestWithResponseInfo(cache.http_cache(), transaction,
+                                     &response);
+  EXPECT_TRUE(response.was_cached);
+  EXPECT_THAT(response.dns_aliases, testing::ElementsAre("alias1", "alias2"));
+}
+
+TEST_F(HttpCacheTest, NoDnsAliasesNoRevalidation) {
+  MockHttpCache cache;
+  HttpResponseInfo response;
+  ScopedMockTransaction transaction(kSimpleGET_Transaction);
+  transaction.dns_aliases = {};
+
+  RunTransactionTestWithResponseInfo(cache.http_cache(), transaction,
+                                     &response);
+  EXPECT_FALSE(response.was_cached);
+  EXPECT_TRUE(response.dns_aliases.empty());
+
+  // The second request should be cached, and the response used without
+  // revalidation. Set the transaction alias list to nonempty to verify that the
+  // cached aliases are being used.
+  transaction.dns_aliases = {"alias"};
+  RunTransactionTestWithResponseInfo(cache.http_cache(), transaction,
+                                     &response);
+  EXPECT_TRUE(response.was_cached);
+  EXPECT_TRUE(response.dns_aliases.empty());
+}
+
+TEST_F(HttpCacheTest, DnsAliasesRevalidation) {
+  MockHttpCache cache;
+  HttpResponseInfo response;
+  ScopedMockTransaction transaction(kTypicalGET_Transaction);
+  transaction.response_headers =
+      "Date: Wed, 28 Nov 2007 09:40:09 GMT\n"
+      "Last-Modified: Wed, 28 Nov 2007 00:40:09 GMT\n"
+      "Cache-Control: max-age=0\n";
+  transaction.dns_aliases = {"alias1", "alias2"};
+
+  RunTransactionTestWithResponseInfo(cache.http_cache(), transaction,
+                                     &response);
+  EXPECT_FALSE(response.was_cached);
+  EXPECT_THAT(response.dns_aliases, testing::ElementsAre("alias1", "alias2"));
+
+  // On the second request, the cache should be revalidated. Change the aliases
+  // to be sure that the new aliases are being used, and have the response be
+  // cached for next time.
+  transaction.response_headers = "Cache-Control: max-age=10000\n";
+  transaction.dns_aliases = {"alias3", "alias4"};
+  RunTransactionTestWithResponseInfo(cache.http_cache(), transaction,
+                                     &response);
+  EXPECT_FALSE(response.was_cached);
+  EXPECT_THAT(response.dns_aliases, testing::ElementsAre("alias3", "alias4"));
+
+  transaction.dns_aliases = {"alias5", "alias6"};
+  RunTransactionTestWithResponseInfo(cache.http_cache(), transaction,
+                                     &response);
+  EXPECT_TRUE(response.was_cached);
+  EXPECT_THAT(response.dns_aliases, testing::ElementsAre("alias3", "alias4"));
+}
+
 }  // namespace net
diff --git a/net/http/http_network_transaction.cc b/net/http/http_network_transaction.cc
index e526512..1a624bf 100644
--- a/net/http/http_network_transaction.cc
+++ b/net/http/http_network_transaction.cc
@@ -565,6 +565,7 @@
   response_.alpn_negotiated_protocol =
       NextProtoToString(stream_request_->negotiated_protocol());
   response_.was_fetched_via_spdy = stream_request_->using_spdy();
+  response_.dns_aliases = stream_->GetDnsAliases();
   SetProxyInfoInReponse(used_proxy_info, &response_);
   OnIOComplete(OK);
 }
diff --git a/net/http/http_network_transaction_unittest.cc b/net/http/http_network_transaction_unittest.cc
index df4fda9..29a5c811 100644
--- a/net/http/http_network_transaction_unittest.cc
+++ b/net/http/http_network_transaction_unittest.cc
@@ -6416,6 +6416,9 @@
   EXPECT_EQ(100, response->headers->GetContentLength());
   EXPECT_TRUE(HttpVersion(1, 1) == response->headers->GetHttpVersion());
 
+  // DNS aliases should be empty when using a proxy.
+  EXPECT_TRUE(response->dns_aliases.empty());
+
   TransportInfo expected_transport;
   expected_transport.type = TransportType::kProxied;
   expected_transport.endpoint = IPEndPoint(IPAddress::IPv4Localhost(), 70);
@@ -6484,6 +6487,9 @@
   ASSERT_TRUE(response->headers);
   EXPECT_EQ("HTTP/1.1 200", response->headers->GetStatusLine());
 
+  // DNS aliases should be empty when using a proxy.
+  EXPECT_TRUE(response->dns_aliases.empty());
+
   TransportInfo expected_transport;
   expected_transport.type = TransportType::kProxied;
   expected_transport.endpoint = IPEndPoint(IPAddress::IPv4Localhost(), 70);
@@ -23401,4 +23407,120 @@
   trans_post_auth_bar.reset();
 }
 
+TEST_F(HttpNetworkTransactionTest, RequestWithDnsAliases) {
+  // Create a request.
+  HttpRequestInfo request;
+  request.method = "GET";
+  request.url = GURL("http://www.example.org/");
+  request.traffic_annotation =
+      net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS);
+
+  // Add a rule with DNS aliases to the host resolver.
+  std::vector<std::string> aliases({"alias1", "alias2", "www.example.org"});
+  auto* rules = session_deps_.host_resolver->rules();
+  rules->AddIPLiteralRuleWithDnsAliases("www.example.org", "127.0.0.1",
+                                        std::move(aliases));
+  session_deps_.host_resolver->set_rules(rules);
+
+  // Create a HttpNetworkSession.
+  std::unique_ptr<HttpNetworkSession> session(CreateSession(&session_deps_));
+
+  // Create a transaction.
+  HttpNetworkTransaction trans(DEFAULT_PRIORITY, session.get());
+
+  // Prepare the expected data to be written and read. The client should send
+  // the request below.
+  MockWrite data_writes[] = {
+      MockWrite("GET / HTTP/1.1\r\n"
+                "Host: www.example.org\r\n"
+                "Connection: keep-alive\r\n\r\n"),
+  };
+
+  // The server should respond with the following.
+  MockRead data_reads[] = {
+      MockRead("HTTP/1.0 200 OK\r\n"),
+      MockRead("Content-Type: text/html; charset=iso-8859-1\r\n"),
+      MockRead("Content-Length: 100\r\n\r\n"),
+      MockRead(SYNCHRONOUS, OK),
+  };
+
+  StaticSocketDataProvider data(data_reads, data_writes);
+  session_deps_.socket_factory->AddSocketDataProvider(&data);
+  TestCompletionCallback callback;
+
+  // Start the transaction.
+  int rv = trans.Start(&request, callback.callback(), NetLogWithSource());
+  EXPECT_THAT(rv, IsError(ERR_IO_PENDING));
+
+  // Wait for completion.
+  rv = callback.WaitForResult();
+  EXPECT_THAT(rv, IsOk());
+
+  // Get the response info.
+  const HttpResponseInfo* response = trans.GetResponseInfo();
+
+  // Verify that the alias list was stored in the response info as expected.
+  ASSERT_TRUE(response);
+  EXPECT_THAT(response->dns_aliases,
+              testing::ElementsAre("alias1", "alias2", "www.example.org"));
+}
+
+TEST_F(HttpNetworkTransactionTest, RequestWithNoAdditionalDnsAliases) {
+  // Create a request.
+  HttpRequestInfo request;
+  request.method = "GET";
+  request.url = GURL("http://www.example.org/");
+  request.traffic_annotation =
+      net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS);
+
+  // Add a rule without DNS aliases to the host resolver. The parameter is an
+  // empty vector.
+  std::vector<std::string> aliases;
+  auto* rules = session_deps_.host_resolver->rules();
+  rules->AddIPLiteralRuleWithDnsAliases("www.example.org", "127.0.0.1",
+                                        std::move(aliases));
+  session_deps_.host_resolver->set_rules(rules);
+
+  // Create a HttpNetworkSession.
+  std::unique_ptr<HttpNetworkSession> session(CreateSession(&session_deps_));
+
+  // Create a transaction.
+  HttpNetworkTransaction trans(DEFAULT_PRIORITY, session.get());
+
+  // Prepare the expected data to be written and read. The client should send
+  // the request below.
+  MockWrite data_writes[] = {
+      MockWrite("GET / HTTP/1.1\r\n"
+                "Host: www.example.org\r\n"
+                "Connection: keep-alive\r\n\r\n"),
+  };
+
+  // The server should respond with the following.
+  MockRead data_reads[] = {
+      MockRead("HTTP/1.0 200 OK\r\n"),
+      MockRead("Content-Type: text/html; charset=iso-8859-1\r\n"),
+      MockRead("Content-Length: 100\r\n\r\n"),
+      MockRead(SYNCHRONOUS, OK),
+  };
+
+  StaticSocketDataProvider data(data_reads, data_writes);
+  session_deps_.socket_factory->AddSocketDataProvider(&data);
+  TestCompletionCallback callback;
+
+  // Start the transaction.
+  int rv = trans.Start(&request, callback.callback(), NetLogWithSource());
+  EXPECT_THAT(rv, IsError(ERR_IO_PENDING));
+
+  // Wait for completion.
+  rv = callback.WaitForResult();
+  EXPECT_THAT(rv, IsOk());
+
+  // Get the response info.
+  const HttpResponseInfo* response = trans.GetResponseInfo();
+
+  // Verify that the alias list was stored in the response info as expected.
+  ASSERT_TRUE(response);
+  EXPECT_THAT(response->dns_aliases, testing::ElementsAre("www.example.org"));
+}
+
 }  // namespace net
diff --git a/net/http/http_response_info.cc b/net/http/http_response_info.cc
index 711667b..78950484 100644
--- a/net/http/http_response_info.cc
+++ b/net/http/http_response_info.cc
@@ -114,6 +114,9 @@
   // restricted in some way.
   RESPONSE_INFO_RESTRICTED_PREFETCH = 1 << 26,
 
+  // This bit is set if the response has a nonempty `dns_aliases` entry.
+  RESPONSE_INFO_HAS_DNS_ALIASES = 1 << 27,
+
   // TODO(darin): Add other bits to indicate alternate request methods.
   // For now, we don't support storing those.
 };
@@ -368,6 +371,20 @@
         base::checked_cast<uint16_t>(peer_signature_algorithm);
   }
 
+  // Read DNS aliases.
+  if (flags & RESPONSE_INFO_HAS_DNS_ALIASES) {
+    int num_aliases;
+    if (!iter.ReadInt(&num_aliases))
+      return false;
+
+    std::string alias;
+    for (int i = 0; i < num_aliases; i++) {
+      if (!iter.ReadString(&alias))
+        return false;
+      dns_aliases.push_back(alias);
+    }
+  }
+
   return true;
 }
 
@@ -409,6 +426,8 @@
     flags |= RESPONSE_INFO_PKP_BYPASSED;
   if (!stale_revalidate_timeout.is_null())
     flags |= RESPONSE_INFO_HAS_STALENESS;
+  if (!dns_aliases.empty())
+    flags |= RESPONSE_INFO_HAS_DNS_ALIASES;
 
   pickle->WriteInt(flags);
   pickle->WriteInt64(request_time.ToInternalValue());
@@ -457,6 +476,12 @@
 
   if (ssl_info.is_valid() && ssl_info.peer_signature_algorithm != 0)
     pickle->WriteInt(ssl_info.peer_signature_algorithm);
+
+  if (!dns_aliases.empty()) {
+    pickle->WriteInt(dns_aliases.size());
+    for (const auto& alias : dns_aliases)
+      pickle->WriteString(alias);
+  }
 }
 
 bool HttpResponseInfo::DidUseQuic() const {
diff --git a/net/http/http_response_info.h b/net/http/http_response_info.h
index a056f4d..5ef989d 100644
--- a/net/http/http_response_info.h
+++ b/net/http/http_response_info.h
@@ -241,6 +241,11 @@
   // The "Vary" header data for this response.
   HttpVaryData vary_data;
 
+  // Any DNS aliases for the remote endpoint. The alias chain order is
+  // preserved in reverse, from canonical name (i.e. address record name)
+  // through to query name.
+  std::vector<std::string> dns_aliases;
+
   static std::string ConnectionInfoToString(ConnectionInfo connection_info);
 };
 
diff --git a/net/http/http_response_info_unittest.cc b/net/http/http_response_info_unittest.cc
index af3eb0c..43c8770a 100644
--- a/net/http/http_response_info_unittest.cc
+++ b/net/http/http_response_info_unittest.cc
@@ -12,6 +12,7 @@
 #include "net/test/cert_test_util.h"
 #include "net/test/ct_test_util.h"
 #include "net/test/test_data_directory.h"
+#include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace net {
@@ -225,6 +226,23 @@
       restored_ssl3_response_info.InitFromPickle(ssl3_pickle, &truncated));
 }
 
+// Test that `dns_aliases` is preserved.
+TEST_F(HttpResponseInfoTest, DnsAliases) {
+  response_info_.dns_aliases = {"alias1", "alias2", "alias3"};
+  net::HttpResponseInfo restored_response_info;
+  PickleAndRestore(response_info_, &restored_response_info);
+  EXPECT_THAT(restored_response_info.dns_aliases,
+              testing::ElementsAre("alias1", "alias2", "alias3"));
+}
+
+// Test that an empty `dns_aliases` is preserved and doesn't throw an error.
+TEST_F(HttpResponseInfoTest, EmptyDnsAliases) {
+  response_info_.dns_aliases = {};
+  net::HttpResponseInfo restored_response_info;
+  PickleAndRestore(response_info_, &restored_response_info);
+  EXPECT_TRUE(restored_response_info.dns_aliases.empty());
+}
+
 }  // namespace
 
 }  // namespace net
diff --git a/net/http/http_transaction_test_util.cc b/net/http/http_transaction_test_util.cc
index 01f0271e8..47d8b84 100644
--- a/net/http/http_transaction_test_util.cc
+++ b/net/http/http_transaction_test_util.cc
@@ -63,6 +63,7 @@
     "Cache-Control: max-age=10000\n",
     base::Time(),
     "<html><body>Google Blah Blah</body></html>",
+    {},
     TEST_MODE_NORMAL,
     nullptr,
     nullptr,
@@ -84,6 +85,7 @@
     "",
     base::Time(),
     "<html><body>Google Blah Blah</body></html>",
+    {},
     TEST_MODE_NORMAL,
     nullptr,
     nullptr,
@@ -106,6 +108,7 @@
     "Last-Modified: Wed, 28 Nov 2007 00:40:09 GMT\n",
     base::Time(),
     "<html><body>Google Blah Blah</body></html>",
+    {},
     TEST_MODE_NORMAL,
     nullptr,
     nullptr,
@@ -128,6 +131,7 @@
     "Etag: \"foopy\"\n",
     base::Time(),
     "<html><body>Google Blah Blah</body></html>",
+    {},
     TEST_MODE_NORMAL,
     nullptr,
     nullptr,
@@ -149,6 +153,7 @@
     "Cache-Control: max-age=10000\n",
     base::Time(),
     "<html><body>Google Blah Blah</body></html>",
+    {},
     TEST_MODE_NORMAL,
     nullptr,
     nullptr,
@@ -515,6 +520,7 @@
   response_.ssl_info.cert = t->cert;
   response_.ssl_info.cert_status = t->cert_status;
   response_.ssl_info.connection_status = t->ssl_connection_status;
+  response_.dns_aliases = t->dns_aliases;
   data_ = resp_data;
   content_length_ = response_.headers->GetContentLength();
 
diff --git a/net/http/http_transaction_test_util.h b/net/http/http_transaction_test_util.h
index aa7be5c3..4c4f7af 100644
--- a/net/http/http_transaction_test_util.h
+++ b/net/http/http_transaction_test_util.h
@@ -83,6 +83,10 @@
   // If |response_time| is unspecified, the current time will be used.
   base::Time response_time;
   const char* data;
+  // Any aliases for the requested URL, as read from DNS records. The alias
+  // chain order is preserved in reverse, from canonical name (i.e. address
+  // record name) through to query name.
+  std::vector<std::string> dns_aliases;
   int test_mode;
   MockTransactionHandler handler;
   MockTransactionReadHandler read_handler;
diff --git a/net/quic/quic_network_transaction_unittest.cc b/net/quic/quic_network_transaction_unittest.cc
index 9ed12bc..1eb2de4 100644
--- a/net/quic/quic_network_transaction_unittest.cc
+++ b/net/quic/quic_network_transaction_unittest.cc
@@ -956,6 +956,9 @@
     CheckResponseData(&trans, expected);
     if (used_proxy) {
       EXPECT_TRUE(trans.GetResponseInfo()->proxy_server.is_quic());
+
+      // DNS aliases should be empty when using a proxy.
+      EXPECT_TRUE(trans.GetResponseInfo()->dns_aliases.empty());
     } else {
       EXPECT_TRUE(trans.GetResponseInfo()->proxy_server.is_direct());
     }
@@ -7819,6 +7822,9 @@
   CheckResponseData(&trans, "0123456789");
   EXPECT_TRUE(trans.GetResponseInfo()->proxy_server.is_quic());
 
+  // DNS aliases should be empty when using a proxy.
+  EXPECT_TRUE(trans.GetResponseInfo()->dns_aliases.empty());
+
   // Causes MockSSLClientSocket to disconnect, which causes the underlying QUIC
   // proxy socket to disconnect.
   NetworkChangeNotifier::NotifyObserversOfIPAddressChangeForTests();
diff --git a/net/url_request/url_request_job_unittest.cc b/net/url_request/url_request_job_unittest.cc
index d4b985c..51942c4 100644
--- a/net/url_request/url_request_job_unittest.cc
+++ b/net/url_request/url_request_job_unittest.cc
@@ -87,6 +87,7 @@
   transaction->response_headers = response_headers;
   transaction->response_time = base::Time();
   transaction->data = "hello";
+  transaction->dns_aliases = {};
   transaction->test_mode = TEST_MODE_NORMAL;
   transaction->handler = nullptr;
   transaction->read_handler = nullptr;
@@ -113,6 +114,7 @@
     "Content-Length: 30\n",  // Intentionally wrong.
     base::Time(),
     "hello",
+    {},
     TEST_MODE_NORMAL,
     nullptr,
     nullptr,
@@ -134,6 +136,7 @@
     "Content-Length: +30\n",  // Invalid
     base::Time(),
     "hello",
+    {},
     TEST_MODE_NORMAL,
     nullptr,
     nullptr,
@@ -156,6 +159,7 @@
     "Content-Length: 30\n",  // Intentionally wrong.
     base::Time(),
     "",
+    {},
     TEST_MODE_NORMAL,
     &GZipServer,
     nullptr,
@@ -178,6 +182,7 @@
     "Content-Encoding: gzip\n",
     base::Time(),
     "",
+    {},
     TEST_MODE_SLOW_READ,
     &GZipHelloServer,
     nullptr,
@@ -201,6 +206,7 @@
     "Content-Length: 5\n",
     base::Time(),
     "hello",
+    {},
     TEST_MODE_NORMAL,
     nullptr,
     nullptr,
@@ -222,6 +228,7 @@
     "Content-Encoding: gzip\n",
     base::Time(),
     "",
+    {},
     TEST_MODE_NORMAL,
     nullptr,
     nullptr,
@@ -244,6 +251,7 @@
     "Content-Length: 21\n",
     base::Time(),
     "not a valid gzip body",
+    {},
     TEST_MODE_NORMAL,
     nullptr,
     nullptr,
@@ -267,6 +275,7 @@
     "Content-Length: 230\n",  // Intentionally wrong.
     base::Time(),
     "",
+    {},
     TEST_MODE_SLOW_READ,
     &BrotliHelloServer,
     nullptr,
diff --git a/net/url_request/url_request_unittest.cc b/net/url_request/url_request_unittest.cc
index 1560b089..a45042e5 100644
--- a/net/url_request/url_request_unittest.cc
+++ b/net/url_request/url_request_unittest.cc
@@ -101,6 +101,7 @@
 #include "net/http/http_request_headers.h"
 #include "net/http/http_response_headers.h"
 #include "net/http/http_server_properties.h"
+#include "net/http/http_transaction_test_util.h"
 #include "net/http/http_util.h"
 #include "net/log/file_net_log_observer.h"
 #include "net/log/net_log_event_type.h"
@@ -12390,4 +12391,54 @@
   EXPECT_EQ("/auth-basic", r->auth_challenge_info()->path);
 }
 
+class URLRequestDnsAliasTest : public TestWithTaskEnvironment {
+ protected:
+  URLRequestDnsAliasTest() : context_(true) {
+    context_.set_network_delegate(&network_delegate_);
+    context_.set_host_resolver(&host_resolver_);
+    context_.Init();
+  }
+
+  void SetUp() override { ASSERT_TRUE(test_server_.Start()); }
+
+  TestNetworkDelegate network_delegate_;  // Must outlive URLRequest.
+  MockHostResolver host_resolver_;
+  TestURLRequestContext context_;
+  TestDelegate test_delegate_;
+  EmbeddedTestServer test_server_;
+};
+
+TEST_F(URLRequestDnsAliasTest, WithDnsAliases) {
+  GURL url(test_server_.GetURL("www.example.test", "/echo"));
+  std::vector<std::string> aliases({"alias1", "alias2", "host"});
+  host_resolver_.rules()->AddIPLiteralRuleWithDnsAliases(
+      "www.example.test", "127.0.0.1", std::move(aliases));
+
+  std::unique_ptr<URLRequest> request(context_.CreateRequest(
+      url, DEFAULT_PRIORITY, &test_delegate_, TRAFFIC_ANNOTATION_FOR_TESTS));
+
+  request->Start();
+
+  test_delegate_.RunUntilComplete();
+  EXPECT_THAT(test_delegate_.request_status(), IsOk());
+  EXPECT_THAT(request->response_info().dns_aliases,
+              testing::ElementsAre("alias1", "alias2", "host"));
+}
+
+TEST_F(URLRequestDnsAliasTest, NoAdditionalDnsAliases) {
+  GURL url(test_server_.GetURL("www.example.test", "/echo"));
+  host_resolver_.rules()->AddIPLiteralRuleWithDnsAliases(
+      "www.example.test", "127.0.0.1", {} /* dns_aliases */);
+
+  std::unique_ptr<URLRequest> request(context_.CreateRequest(
+      url, DEFAULT_PRIORITY, &test_delegate_, TRAFFIC_ANNOTATION_FOR_TESTS));
+
+  request->Start();
+
+  test_delegate_.RunUntilComplete();
+  EXPECT_THAT(test_delegate_.request_status(), IsOk());
+  EXPECT_THAT(request->response_info().dns_aliases,
+              testing::ElementsAre("www.example.test"));
+}
+
 }  // namespace net
diff --git a/services/network/url_loader.cc b/services/network/url_loader.cc
index 3f5b58a7..2f80592 100644
--- a/services/network/url_loader.cc
+++ b/services/network/url_loader.cc
@@ -145,6 +145,7 @@
   response->auth_challenge_info = request->auth_challenge_info();
   response->has_range_requested = request->extra_request_headers().HasHeader(
       net::HttpRequestHeaders::kRange);
+  response->dns_aliases = request->response_info().dns_aliases;
 }
 
 // A subclass of net::UploadBytesElementReader which owns
diff --git a/services/network/url_loader_unittest.cc b/services/network/url_loader_unittest.cc
index 817637f8..8baa288 100644
--- a/services/network/url_loader_unittest.cc
+++ b/services/network/url_loader_unittest.cc
@@ -141,6 +141,10 @@
 
 constexpr char kInsecureHost[] = "othersite.test";
 
+constexpr char kHostnameWithAliases[] = "www.example.test";
+
+constexpr char kHostnameWithoutAliases[] = "www.other.test";
+
 static ResourceRequest CreateResourceRequest(const char* method,
                                              const GURL& url) {
   ResourceRequest request;
@@ -531,6 +535,8 @@
     // the loopback address and will let us access |test_server_|.
     scoped_refptr<net::RuleBasedHostResolverProc> mock_resolver_proc =
         base::MakeRefCounted<net::RuleBasedHostResolverProc>(nullptr);
+    mock_resolver_proc->AddIPLiteralRuleWithDnsAliases(
+        kHostnameWithAliases, "127.0.0.1", {"alias1", "alias2", "host"});
     mock_resolver_proc->AddRule("*", "127.0.0.1");
     mock_host_resolver_ = std::make_unique<net::ScopedDefaultHostResolverProc>(
         mock_resolver_proc.get());
@@ -6320,4 +6326,22 @@
   EXPECT_FALSE(socket_data.socket());
 }
 
+TEST_F(URLLoaderTest, WithDnsAliases) {
+  GURL url(test_server_.GetURL(kHostnameWithAliases, "/echo"));
+
+  EXPECT_EQ(net::OK, Load(url));
+  ASSERT_TRUE(client_.response_head());
+  EXPECT_THAT(client_.response_head()->dns_aliases,
+              testing::ElementsAre("alias1", "alias2", "host"));
+}
+
+TEST_F(URLLoaderTest, NoAdditionalDnsAliases) {
+  GURL url(test_server_.GetURL(kHostnameWithoutAliases, "/echo"));
+
+  EXPECT_EQ(net::OK, Load(url));
+  ASSERT_TRUE(client_.response_head());
+  EXPECT_THAT(client_.response_head()->dns_aliases,
+              testing::ElementsAre(kHostnameWithoutAliases));
+}
+
 }  // namespace network