blob: 2640ebe0f1bf8dcd227390dc7cdcc936cf47a28f [file] [log] [blame] [edit]
// Copyright 2021 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef DNS_PROXY_RESOLVER_H_
#define DNS_PROXY_RESOLVER_H_
#include <sys/socket.h>
#include <iostream>
#include <map>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <base/containers/span.h>
#include <base/files/file_descriptor_watcher_posix.h>
#include <base/functional/callback.h>
#include <base/memory/weak_ptr.h>
#include <base/time/time.h>
#include <chromeos/net-base/socket.h>
#include <chromeos/patchpanel/dns/dns_response.h>
#include "dns-proxy/ares_client.h"
#include "dns-proxy/doh_curl_client.h"
#include "dns-proxy/metrics.h"
namespace dns_proxy {
// |kDefaultDNSBufSize| holds the default receive buffer size for a DNS message.
// The value is taken as the recommended maximum EDNS buffer size of 1232,
// following default MTU value of 1500.
constexpr size_t kDefaultDNSBufSize = 2048;
// |kMaxDNSBufSize| holds the maximum size of a DNS message which is the maximum
// size of a TCP packet.
constexpr size_t kMaxDNSBufSize = 65536;
// For TCP, DNS has an additional 2-bytes header representing the length
// of the query. A 2-bytes padding length is added to the receiving buffer,
// so it is 4-bytes aligned.
constexpr int kTCPBufferPaddingLength = 2;
// Resolver receives wire-format DNS queries and proxies them to DNS server(s).
// This class supports standard plain-text resolving using c-ares and secure
// DNS / DNS-over-HTTPS (DoH) using CURL.
//
// The resolver supports both plain-text and DoH name resolution. By default,
// standard DNS will be performed using the name servers passed to
// SetNameServers. DNS over HTTPS will be used if secure DNS providers are
// passed to SetDoHProviders. DoH can either be "always on" or "opportunistic".
// In the case of the former, only DNS over HTTPS will be performed and failures
// are final. In the case of latter, if DNS over HTTP fails, it will fall back
// to standard plain-text DNS.
//
// Resolver listens on UDP and TCP port 53.
class Resolver {
public:
// |SocketFd| stores client's socket data.
// This is used to send reply to the client on callback called.
struct SocketFd {
SocketFd(int type, int fd, size_t buf_size = kDefaultDNSBufSize);
// If |buf| is full, resize by doubling the buffer size up to a maximum of
// |kMaxDNSBufSize|. Otherwise, do nothing. Returns the size of |buf|.
size_t try_resize();
// Getter for |msg| and |len| that excludes the additional 2-bytes TCP
// header data containing the DNS query length.
const char* get_message() const;
const size_t get_length() const;
// |type| is either SOCK_STREAM or SOCK_DGRAM.
const int type;
const int fd;
// Store the data and length of the query to retry failing queries.
// For TCP, DNS has an additional 2-bytes header representing the length
// of the query. In order to make the data 4-bytes aligned, a pointer |msg|
// for buffer |buf| is used. |len| denotes the length of |msg|.
char* msg;
size_t len;
// Holds the source address of the client and it's address length.
// At initialization, |socklen| value will be the size of |src|. Upon
// receiving, |socklen| should be updated to be the size of the address
// of |src|.
// For TCP connections, |src| and |socklen| are not used.
struct sockaddr_storage src;
socklen_t socklen;
// Underlying buffer of |msg| with default size of |kDefaultDNSBufSize|.
std::vector<char> buf;
// Number of attempted retry. Query should not be retried when reaching
// a certain threshold.
int num_retries;
// Number of currently running queries.
int num_active_queries;
// Whether DoH should be bypassed for this query.
bool bypass_doh;
// Identifier for the socket. |fd| is not a suitable identifier here as it
// can be used for multiple SocketFds.
const int id;
// Records timings for metrics.
Metrics::QueryTimer timer;
base::WeakPtrFactory<SocketFd> weak_factory{this};
};
// Whether a domain is included or excluded from using DoH.
enum class DomainDoHConfig { kIncluded, kExcluded };
// |ProbeState| is used to store the probe state of a DoH provider or name
// server. For example, when a probe succeeds for a specific name server,
// subsequent probing that refers to the same state will be disabled.
// The same applies for invalidating a name server. If a query resulted in
// a failure, but the query is done prior to the name server being validated,
// the name server will not be invalidated.
struct ProbeState {
ProbeState(const std::string& target, bool doh, bool validated = false);
// |target| is the DoH provider or name server used for the probe.
// |doh| defines whether target is a DoH provider or a name server.
std::string target;
bool doh;
bool validated;
int num_retries;
base::WeakPtrFactory<ProbeState> weak_factory{this};
};
// |ProbeData| stores required data of a DoH or Do53 probe to record metrics
// and logs.
struct ProbeData {
sa_family_t family;
int num_retries;
base::Time start_time;
};
Resolver(base::RepeatingCallback<void(std::ostream& stream)> logger,
base::TimeDelta timeout,
base::TimeDelta retry_delay,
int max_num_retries);
// Provided for testing only.
Resolver(std::unique_ptr<AresClient> ares_client,
std::unique_ptr<DoHCurlClientInterface> curl_client,
std::unique_ptr<net_base::SocketFactory> socket_factory,
bool disable_probe = true,
bool disable_query_validation = true,
std::unique_ptr<Metrics> metrics = nullptr);
virtual ~Resolver() = default;
// Listen on an incoming DNS query on address |addr| for UDP and TCP.
// Listening on default DNS port (53) requires CAP_NET_BIND_SERVICE.
// |ifname| is used as an identifier for the listening sockets. A new listen
// call with the same |ifname| replaces the previous sockets.
virtual bool ListenTCP(struct sockaddr* addr, std::string_view ifname = "");
virtual bool ListenUDP(struct sockaddr* addr, std::string_view ifname = "");
virtual void StopListen(sa_family_t family, std::string_view ifname = "");
// Set standard DNS and DNS-over-HTTPS servers endpoints.
// If DoH servers are not empty, resolving domain will be done with DoH.
// |always_on_doh| flag is used to disallow fallback to standard plain-text
// DNS.
virtual void SetNameServers(const std::vector<std::string>& name_servers);
virtual void SetDoHProviders(const std::vector<std::string>& doh_providers,
bool always_on_doh = false);
// Set interface name to bind to when sending queries. This is necessary for:
// - ARC proxies to use the network it is tied to.
// - All proxies to be able to reach link-local addresses.
// When SetInterface is called with an empty string, queries will be sent
// without binding to any interface. This is the same behavior with
// ClearInterface.
virtual void SetInterface(std::string_view ifname);
virtual void ClearInterface();
// Set DNS-over-HTTPS included and excluded domains. This is used to
// disable DoH (and falls back to plain-text DNS) for certain domains.
void SetDomainDoHConfigs(
const std::vector<std::string>& doh_included_domains,
const std::vector<std::string>& doh_excluded_domains);
// Handle DNS results queried through ares.
// |sock_fd| is the socket data needed to reply to the client. Empty
// |sock_fd| means that the request is already handled.
// |probe_state| stores the current probing state including the name server
// used for the query. Upon failure, if |probe_state| is valid, the name
// server should be invalidated.
// This function passed the first successful result or the last result back
// to the client.
//
// |status| is the ares response status. |resp| is the wire-format response
// of the DNS query given through `Resolve(...)`.
// |resp| and its lifecycle is owned by ares.
void HandleAresResult(base::WeakPtr<SocketFd> sock_fd,
base::WeakPtr<ProbeState> probe_state,
int status,
const base::span<unsigned char>& resp);
// Handle DoH results queried through curl.
// |sock_fd| is the socket data needed to reply to the client. Empty
// |sock_fd| means that the request is already handled.
// |probe_state| stores the current probing state including the DoH provider
// used for the query. Upon failure, if |probe_state| is valid, the DoH
// provider should be invalidated.
// This function passed the first successful result or the last result back
// to the client.
//
// |http_code| is the HTTP status code of the response. |resp| is the
// wire-format response of the DNS query given through `Resolve(...)`.
// |resp| and its lifecycle is owned by DoHCurlClient.
void HandleCurlResult(base::WeakPtr<SocketFd> sock_fd,
base::WeakPtr<ProbeState> probe_state,
const DoHCurlClient::CurlResult& res,
const base::span<unsigned char>& resp);
// Resolve a domain using CURL or Ares using data from |sock_fd|.
// If |fallback| is true, force to use standard plain-text DNS.
void Resolve(base::WeakPtr<SocketFd> sock_fd, bool fallback = false);
// Handle DoH and Do53 probe result from the DoH provider or name server
// provided inside |probe_state|. |probe_state| also defines the current
// probing state, including if it is already successful. If the probe is
// successful, the provider or name server will be validated.
// For Do53, |probe_data| is added for metrics, including IP family.
void HandleDoHProbeResult(base::WeakPtr<ProbeState> probe_state,
const ProbeData& probe_data,
const DoHCurlClient::CurlResult& res,
const base::span<unsigned char>& resp);
void HandleDo53ProbeResult(base::WeakPtr<ProbeState> probe_state,
const ProbeData& probe_data,
int status,
const base::span<unsigned char>& resp);
// Handle DNS query from clients. |type| values will be either SOCK_DGRAM
// or SOCK STREAM, for UDP and TCP respectively.
void OnDNSQuery(int fd, int type);
// Handle DNS query data from clients read through |OnDNSQuery|. Added for
// unit testing.
void HandleDNSQuery(std::unique_ptr<SocketFd> sock_fd);
// Get a SocketFd from |pending_sock_fds_| using the key |fd| and remove it
// from the map. Added for unit testing.
std::unique_ptr<SocketFd> PopPendingSocketFd(int fd);
// Get the domain name being queried from a DNS query.
std::optional<std::string> GetDNSQuestionName(
const base::span<const uint8_t>& query);
// Create a SERVFAIL response from a DNS query.
patchpanel::DnsResponse ConstructServFailResponse(
const base::span<const char>& query);
// Returns whether or not a DNS response has NXDOMAIN rcode. Return false if
// the DNS response is invalid.
bool IsNXDOMAIN(const base::span<const unsigned char>& resp);
// Returns whether or not a DNS query is valid.
bool ValidateQuery(const base::span<const uint8_t>& query);
// Provided for testing only. Enable or disable probing.
void SetProbingEnabled(bool enable_probe);
friend std::ostream& operator<<(std::ostream& stream,
const Resolver& resolver);
// Returns whether or not DoH should be bypassed based on the configuration of
// DoH included and excluded domain lists.
bool BypassDoH(const std::string& domain);
// Returns the configuration of DoH included or excluded for a domain.
std::optional<DomainDoHConfig> GetDomainDoHConfig(const std::string& domain);
protected:
// Wrapper around libc recvfrom, allowing override in fuzzer tests.
virtual ssize_t Receive(int fd,
char* buffer,
size_t buffer_size,
struct sockaddr* src_addr,
socklen_t* addrlen);
private:
// |TCPConnection| is used to track and terminate TCP connections.
struct TCPConnection {
TCPConnection(std::unique_ptr<net_base::Socket> sock,
const base::RepeatingCallback<void(int, int)>& callback);
std::unique_ptr<net_base::Socket> sock;
std::unique_ptr<base::FileDescriptorWatcher::Controller> watcher;
};
// Callback to handle newly opened connections on TCP sockets.
void OnTCPConnection(const std::string& ifname, sa_family_t family);
// Send back data taken from CURL or Ares to the client.
void ReplyDNS(base::WeakPtr<SocketFd> sock_fd,
const base::span<unsigned char>& resp);
// Set either name servers or DoH providers |targets| based on the boolean
// type |doh|.
void SetServers(const std::vector<std::string>& targets, bool doh);
// CommitQuery updates |sock_fd| state based on its DNS query data, appends
// the |sock_fd| to |sock_fds_|, and initiates a DNS request through
// `Resolve(...)`.
void CommitQuery(std::unique_ptr<SocketFd> sock_fd);
// Resolve a domain using CURL or Ares using data from |sock_fd|.
bool ResolveDNS(base::WeakPtr<SocketFd> sock_fd, bool doh);
// Get the active DoH providers / name servers. It will try to return the
// validated DoH providers / name servers unless there are none.
// The behavior is then slightly different for each modes:
// - DoH off: return all name servers.
// - DoH automatic: return empty DoH providers (should behave like DoH off).
// - DoH always on: return all DoH providers.
std::vector<std::string> GetActiveDoHProviders();
std::vector<std::string> GetActiveNameServers();
// Restart probe upon DNS query failure. |probe_state| store the data needed
// for probing, DoH provider or name server. |probe_state| is invalidated
// after this call.
void RestartProbe(base::WeakPtr<ProbeState> probe_state);
// Start a probe to validate a DoH provider or name server defines inside
// |probe_state|. Weak pointer is used here to cancel remaining probes if it
// is no longer interesting.
void Probe(base::WeakPtr<ProbeState> probe_state);
// A logging name for this Resolver to distinguish its logs from lots of other
// Resolvers owner by other Proxy instances.
base::RepeatingCallback<void(std::ostream& stream)> logger_;
std::unique_ptr<net_base::SocketFactory> socket_factory_ =
std::make_unique<net_base::SocketFactory>();
// Disallow DoH fallback to standard plain-text DNS.
bool always_on_doh_;
// Resolve using DoH if true.
bool doh_enabled_;
// Watch |tcp_srcs_| for incoming TCP connections.
std::map<std::pair<std::string, sa_family_t>,
std::unique_ptr<net_base::Socket>>
tcp_srcs_;
std::map<std::pair<std::string, sa_family_t>,
std::unique_ptr<base::FileDescriptorWatcher::Controller>>
tcp_src_watchers_;
// Map of TCP connections keyed by their file descriptor.
std::map<int, std::unique_ptr<TCPConnection>> tcp_connections_;
// Watch queries from |udp_srcs_|.
std::map<std::pair<std::string, sa_family_t>,
std::unique_ptr<net_base::Socket>>
udp_srcs_;
std::map<std::pair<std::string, sa_family_t>,
std::unique_ptr<base::FileDescriptorWatcher::Controller>>
udp_src_watchers_;
// Name servers and DoH providers validated through probes.
std::vector<std::string> validated_name_servers_;
std::vector<std::string> validated_doh_providers_;
// Name servers and DoH providers of the underlying network alongside its
// probe state.
std::map<std::string, std::unique_ptr<ProbeState>> name_servers_;
std::map<std::string, std::unique_ptr<ProbeState>> doh_providers_;
// Map of fully qualified domain name (FQDN) of domains to be included or
// excluded from using DoH.
std::map<std::string, DomainDoHConfig> domain_doh_configs_;
// List of domain suffixes to be included or excluded from using DoH.
// The list is sorted by the number of dots of the domain suffixes.
std::vector<std::pair<std::string, DomainDoHConfig>>
domain_suffix_doh_configs_;
// Whether or not the DoH included and excluded domains configurations are
// set.
bool doh_included_domains_set_ = false;
bool doh_excluded_domains_set_ = false;
// Provided for testing only. Boolean to disable probe.
bool disable_probe_ = false;
// Provided for testing only. Boolean to disable query validation.
bool disable_query_validation_ = false;
// Delay before retrying a failing query.
base::TimeDelta retry_delay_;
// Maximum number of retries before giving up.
int max_num_retries_;
// Metrics must outlive SocketFd as it is called on SocketFd's destructor.
std::unique_ptr<Metrics> metrics_;
// Map of SocketFds keyed by its SocketFd ID.
std::map<int, std::unique_ptr<SocketFd>> sock_fds_;
// Interface name that is bound for DNS queries. If empty, the sockets are not
// bound to any interface.
std::string ifname_;
// Map of incomplete DNS transaction's SocketFds keyed by its fd. This is
// necessary because for TCP, it is possible for the DNS query to be sent
// over multiple TCP segments.
std::map<int, std::unique_ptr<SocketFd>> pending_sock_fds_;
// Ares client to resolve DNS through standard plain-text DNS.
std::unique_ptr<AresClient> ares_client_;
// Curl client to resolve DNS through secure DNS.
std::unique_ptr<DoHCurlClientInterface> curl_client_;
base::WeakPtrFactory<Resolver> weak_factory_{this};
};
} // namespace dns_proxy
#endif // DNS_PROXY_RESOLVER_H_