Commit 6c3f6bb6 authored by David Van Cleve's avatar David Van Cleve Committed by Commit Bot

Add EsniContent class for surfacing ESNI results

This patch adds a new class, EsniContent, that serves to
aggregate the results of several ESNI (TLS 1.3 Encrypted Server Name
Indication, draft 4) DNS records and surface them to the
connection stack.

An ESNI DNS query response contains a list of ESNI records, each
of which comprises a "key object" and an optional list of
IP addresses associated with the key object. Aggregating the records
should ensure that:

- each key is only stored once (the spec allows keys to be up to ~60K in
size)
- it is fast to determine which IP addresses have associated keys, and
to iterate over these associated keys (for purposes of prioritizing
addresses in connection establishment)

To do this, an EsniContent object stores a set of bytestrings
(the distinct key objects) and a map from each IP address to a set of
handles to the keys associated with that IP address.

R=ericorth

Bug: 1003494
Change-Id: I0ff2478ef6db6bd9fcb4a685444d96076ed26eae
Cq-Do-Not-Cancel-Tryjobs: true
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1862730
Commit-Queue: David Van Cleve <davidvc@chromium.org>
Reviewed-by: default avatarEric Orth <ericorth@chromium.org>
Cr-Commit-Position: refs/heads/master@{#707094}
parent d107e270
......@@ -56,6 +56,7 @@ source_set("dns") {
"dns_socket_pool.cc",
"dns_socket_pool.h",
"dns_transaction.cc",
"esni_content.cc",
"host_cache.cc",
"host_resolver.cc",
"host_resolver_manager.cc",
......@@ -192,6 +193,7 @@ source_set("host_resolver") {
sources += [
"dns_config.h",
"dns_config_overrides.h",
"esni_content.h",
"host_cache.h",
"host_resolver.h",
"mapped_host_resolver.h",
......@@ -382,6 +384,7 @@ source_set("tests") {
"dns_socket_pool_unittest.cc",
"dns_transaction_unittest.cc",
"dns_util_unittest.cc",
"esni_content_unittest.cc",
"host_cache_unittest.cc",
"host_resolver_manager_unittest.cc",
"mapped_host_resolver_unittest.cc",
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/dns/esni_content.h"
namespace net {
EsniContent::EsniContent() = default;
EsniContent::EsniContent(const EsniContent& other) {
MergeFrom(other);
}
EsniContent::EsniContent(EsniContent&& other) = default;
EsniContent& EsniContent::operator=(const EsniContent& other) {
MergeFrom(other);
return *this;
}
EsniContent& EsniContent::operator=(EsniContent&& other) = default;
EsniContent::~EsniContent() = default;
bool operator==(const EsniContent& c1, const EsniContent& c2) {
return c1.keys() == c2.keys() &&
c1.keys_for_addresses() == c2.keys_for_addresses();
}
const std::set<std::string, EsniContent::StringPieceComparator>&
EsniContent::keys() const {
return keys_;
}
const std::map<IPAddress, std::set<base::StringPiece>>&
EsniContent::keys_for_addresses() const {
return keys_for_addresses_;
}
void EsniContent::AddKey(base::StringPiece key) {
if (keys_.find(key) == keys_.end())
keys_.insert(std::string(key));
}
void EsniContent::AddKeyForAddress(const IPAddress& address,
base::StringPiece key) {
auto key_it = keys_.find(key);
if (key_it == keys_.end()) {
bool key_was_added;
std::tie(key_it, key_was_added) = keys_.insert(std::string(key));
DCHECK(key_was_added);
}
keys_for_addresses_[address].insert(base::StringPiece(*key_it));
}
void EsniContent::MergeFrom(const EsniContent& other) {
for (const auto& kv : other.keys_for_addresses()) {
const IPAddress& address = kv.first;
const auto& keys_for_address = kv.second;
for (base::StringPiece key : keys_for_address)
AddKeyForAddress(address, key);
}
for (const std::string& key : other.keys())
AddKey(key);
}
} // namespace net
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef NET_DNS_ESNI_CONTENT_H_
#define NET_DNS_ESNI_CONTENT_H_
#include <map>
#include <set>
#include <string>
#include "base/strings/string_piece.h"
#include "net/base/ip_address.h"
#include "net/base/net_export.h"
namespace net {
// An EsniContent struct represents an aggregation of the
// content of several ESNI (TLS 1.3 Encrypted Server Name Indication,
// draft 4) resource records.
//
// This aggregation contains:
// (1) The ESNI key objects from each of the ESNI records, and
// (2) A collection of IP addresses, each of which is associated
// with one or more of the key objects. (Each key will likely also
// be associated with several destination addresses.)
class NET_EXPORT_PRIVATE EsniContent {
public:
EsniContent();
EsniContent(const EsniContent& other);
EsniContent(EsniContent&& other);
EsniContent& operator=(const EsniContent& other);
EsniContent& operator=(EsniContent&& other);
~EsniContent();
// Key objects (which might be up to ~50K in length) are stored
// in a collection of std::string; use transparent comparison
// to allow checking whether a given base::StringPiece is in
// the collection without making copies.
struct StringPieceComparator {
using is_transparent = int;
bool operator()(const base::StringPiece lhs,
const base::StringPiece rhs) const {
return lhs < rhs;
}
};
const std::set<std::string, StringPieceComparator>& keys() const;
const std::map<IPAddress, std::set<base::StringPiece>>& keys_for_addresses()
const;
// Adds |key| (if it is not already stored) without associating it
// with any particular addresss; if this addition is performed, it
// copies the underlying string.
void AddKey(base::StringPiece key);
// Associates a key with an address, copying the underlying string to
// the internal collection of keys if it is not already stored.
void AddKeyForAddress(const IPAddress& address, base::StringPiece key);
// Merges the contents of |other|:
// 1. unions the collection of stored keys with |other.keys()| and
// 2. unions the stored address-key associations with
// |other.keys_for_addresses()|.
void MergeFrom(const EsniContent& other);
private:
// In order to keep the StringPieces in |keys_for_addresses_| valid,
// |keys_| must be of a collection type guaranteeing stable pointers.
std::set<std::string, StringPieceComparator> keys_;
std::map<IPAddress, std::set<base::StringPiece>> keys_for_addresses_;
};
// Two EsniContent structs are equal if they have the same set of keys, the
// same set of IP addresses, and the same subset of the keys corresponding to
// each IP address.
NET_EXPORT_PRIVATE
bool operator==(const EsniContent& c1, const EsniContent& c2);
} // namespace net
#endif // NET_DNS_ESNI_CONTENT_H_
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/dns/esni_content.h"
#include "base/strings/string_number_conversions.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace net {
namespace {
IPAddress MakeIPAddress() {
// Introduce some (deterministic) variation in the IP addresses
// generated.
static uint8_t next_octet = 0;
next_octet += 4;
return IPAddress(next_octet, next_octet + 1, next_octet + 2, next_octet + 3);
}
// Make sure we can add keys.
TEST(EsniContentTest, AddKey) {
EsniContent c1;
c1.AddKey("a");
EXPECT_THAT(c1.keys(), ::testing::UnorderedElementsAre("a"));
c1.AddKey("a");
EXPECT_THAT(c1.keys(), ::testing::UnorderedElementsAre("a"));
c1.AddKey("b");
EXPECT_THAT(c1.keys(), ::testing::UnorderedElementsAre("a", "b"));
}
// Make sure we can add key-address pairs.
TEST(EsniContentTest, AddKeyForAddress) {
EsniContent c1;
auto address = MakeIPAddress();
c1.AddKeyForAddress(address, "a");
EXPECT_THAT(c1.keys(), ::testing::UnorderedElementsAre("a"));
EXPECT_THAT(c1.keys_for_addresses(),
::testing::UnorderedElementsAre(::testing::Pair(
address, ::testing::UnorderedElementsAre("a"))));
}
TEST(EsniContentTest, AssociateAddressWithExistingKey) {
EsniContent c1;
auto address = MakeIPAddress();
c1.AddKey("a");
c1.AddKeyForAddress(address, "a");
EXPECT_THAT(c1.keys(), ::testing::UnorderedElementsAre("a"));
EXPECT_THAT(c1.keys_for_addresses(),
::testing::UnorderedElementsAre(::testing::Pair(
address, ::testing::UnorderedElementsAre("a"))));
}
// Merging to an empty EsniContent should make the result equal the source of
// the merge.
TEST(EsniContentTest, MergeToEmpty) {
EsniContent c1;
c1.AddKey("c");
IPAddress address = MakeIPAddress();
c1.AddKeyForAddress(address, "a");
c1.AddKeyForAddress(address, "b");
EsniContent empty;
empty.MergeFrom(c1);
EXPECT_EQ(c1, empty);
}
TEST(EsniContentTest, MergeFromEmptyNoOp) {
EsniContent c1, c2;
c1.AddKey("a");
c2.AddKey("a");
EsniContent empty;
c1.MergeFrom(empty);
EXPECT_EQ(c1, c2);
}
// Test that merging multiple keys corresponding to a single address works.
TEST(EsniContentTest, MergeKeysForSingleHost) {
EsniContent c1, c2;
IPAddress address = MakeIPAddress();
c1.AddKeyForAddress(address, "a");
c1.AddKeyForAddress(address, "b");
c2.AddKeyForAddress(address, "b");
c2.AddKeyForAddress(address, "c");
c1.MergeFrom(c2);
EXPECT_THAT(c1.keys(), ::testing::UnorderedElementsAre("a", "b", "c"));
EXPECT_THAT(c1.keys_for_addresses(),
::testing::UnorderedElementsAre(::testing::Pair(
address, ::testing::UnorderedElementsAre("a", "b", "c"))));
}
// Test that merging multiple addresss corresponding to a single key works.
TEST(EsniContentTest, MergeHostsForSingleKey) {
EsniContent c1, c2;
IPAddress address = MakeIPAddress();
IPAddress second_address = MakeIPAddress();
c1.AddKeyForAddress(address, "a");
c2.AddKeyForAddress(second_address, "a");
c1.MergeFrom(c2);
EXPECT_THAT(c1.keys(), ::testing::UnorderedElementsAre("a"));
EXPECT_THAT(
c1.keys_for_addresses(),
::testing::UnorderedElementsAre(
::testing::Pair(address, ::testing::UnorderedElementsAre("a")),
::testing::Pair(second_address,
::testing::UnorderedElementsAre("a"))));
}
// Test merging some more complex instances of the class.
TEST(EsniContentTest, MergeSeveralHostsAndKeys) {
EsniContent c1, c2, expected;
for (int i = 0; i < 50; ++i) {
IPAddress address = MakeIPAddress();
std::string key = base::NumberToString(i);
switch (i % 3) {
case 0:
c1.AddKey(key);
expected.AddKey(key);
break;
case 1:
c2.AddKey(key);
expected.AddKey(key);
break;
}
// Associate each address with a subset of the keys seen so far
{
int j = 0;
for (auto key : c1.keys()) {
if (j % 2) {
c1.AddKeyForAddress(address, key);
expected.AddKeyForAddress(address, key);
}
++j;
}
}
{
int j = 0;
for (auto key : c2.keys()) {
if (j % 3 == 1) {
c2.AddKeyForAddress(address, key);
expected.AddKeyForAddress(address, key);
}
++j;
}
}
}
{
EsniContent merge_dest = c1;
EsniContent merge_src = c2;
merge_dest.MergeFrom(merge_src);
EXPECT_EQ(merge_dest, expected);
}
{
EsniContent merge_dest = c2;
EsniContent merge_src = c1;
merge_dest.MergeFrom(merge_src);
EXPECT_EQ(merge_dest, expected);
}
}
} // namespace
} // namespace net
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment