| // Copyright 2021 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/dns_config_service_linux.h" |
| |
| #include <netdb.h> |
| #include <netinet/in.h> |
| #include <resolv.h> |
| #include <sys/socket.h> |
| #include <sys/types.h> |
| |
| #include <map> |
| #include <memory> |
| #include <string> |
| #include <type_traits> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/check.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_path_watcher.h" |
| #include "base/location.h" |
| #include "base/logging.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/sequence_checker.h" |
| #include "base/threading/scoped_blocking_call.h" |
| #include "base/time/time.h" |
| #include "net/base/ip_endpoint.h" |
| #include "net/dns/dns_config.h" |
| #include "net/dns/nsswitch_reader.h" |
| #include "net/dns/public/resolv_reader.h" |
| #include "net/dns/serial_worker.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| |
| namespace net { |
| |
| namespace internal { |
| |
| namespace { |
| |
| const base::FilePath::CharType kFilePathHosts[] = |
| FILE_PATH_LITERAL("/etc/hosts"); |
| |
| #ifndef _PATH_RESCONF // Normally defined in <resolv.h> |
| #define _PATH_RESCONF FILE_PATH_LITERAL("/etc/resolv.conf") |
| #endif |
| |
| constexpr base::FilePath::CharType kFilePathResolv[] = _PATH_RESCONF; |
| |
| #ifndef _PATH_NSSWITCH_CONF // Normally defined in <netdb.h> |
| #define _PATH_NSSWITCH_CONF FILE_PATH_LITERAL("/etc/nsswitch.conf") |
| #endif |
| |
| constexpr base::FilePath::CharType kFilePathNsswitch[] = _PATH_NSSWITCH_CONF; |
| |
| absl::optional<DnsConfig> ConvertResStateToDnsConfig( |
| const struct __res_state& res) { |
| absl::optional<std::vector<net::IPEndPoint>> nameservers = |
| GetNameservers(res); |
| DnsConfig dns_config; |
| dns_config.unhandled_options = false; |
| |
| if (!nameservers.has_value()) |
| return absl::nullopt; |
| |
| // Expected to be validated by GetNameservers() |
| DCHECK(res.options & RES_INIT); |
| |
| dns_config.nameservers = std::move(nameservers.value()); |
| dns_config.search.clear(); |
| for (int i = 0; (i < MAXDNSRCH) && res.dnsrch[i]; ++i) { |
| dns_config.search.emplace_back(res.dnsrch[i]); |
| } |
| |
| dns_config.ndots = res.ndots; |
| dns_config.fallback_period = base::Seconds(res.retrans); |
| dns_config.attempts = res.retry; |
| #if defined(RES_ROTATE) |
| dns_config.rotate = res.options & RES_ROTATE; |
| #endif |
| #if !defined(RES_USE_DNSSEC) |
| // Some versions of libresolv don't have support for the DO bit. In this |
| // case, we proceed without it. |
| static const int RES_USE_DNSSEC = 0; |
| #endif |
| |
| // The current implementation assumes these options are set. They normally |
| // cannot be overwritten by /etc/resolv.conf |
| const unsigned kRequiredOptions = RES_RECURSE | RES_DEFNAMES | RES_DNSRCH; |
| if ((res.options & kRequiredOptions) != kRequiredOptions) { |
| dns_config.unhandled_options = true; |
| return dns_config; |
| } |
| |
| const unsigned kUnhandledOptions = RES_USEVC | RES_IGNTC | RES_USE_DNSSEC; |
| if (res.options & kUnhandledOptions) { |
| dns_config.unhandled_options = true; |
| return dns_config; |
| } |
| |
| if (dns_config.nameservers.empty()) |
| return absl::nullopt; |
| |
| // If any name server is 0.0.0.0, assume the configuration is invalid. |
| for (const IPEndPoint& nameserver : dns_config.nameservers) { |
| if (nameserver.address().IsZero()) |
| return absl::nullopt; |
| } |
| return dns_config; |
| } |
| |
| // Helper to add the effective result of `action` to `in_out_parsed_behavior`. |
| // Returns false if `action` results in inconsistent behavior (setting an action |
| // for a status that already has a different action). |
| bool SetActionBehavior(const NsswitchReader::ServiceAction& action, |
| std::map<NsswitchReader::Status, NsswitchReader::Action>& |
| in_out_parsed_behavior) { |
| if (action.negated) { |
| for (NsswitchReader::Status status : |
| {NsswitchReader::Status::kSuccess, NsswitchReader::Status::kNotFound, |
| NsswitchReader::Status::kUnavailable, |
| NsswitchReader::Status::kTryAgain}) { |
| if (status != action.status) { |
| NsswitchReader::ServiceAction effective_action = { |
| /*negated=*/false, status, action.action}; |
| if (!SetActionBehavior(effective_action, in_out_parsed_behavior)) |
| return false; |
| } |
| } |
| } else { |
| if (in_out_parsed_behavior.count(action.status) >= 1 && |
| in_out_parsed_behavior[action.status] != action.action) { |
| return false; |
| } |
| in_out_parsed_behavior[action.status] = action.action; |
| } |
| |
| return true; |
| } |
| |
| // Helper to determine if `actions` match `expected_actions`, meaning `actions` |
| // contains no unknown statuses or actions and for every expectation set in |
| // `expected_actions`, the expected action matches the effective result from |
| // `actions`. |
| bool AreActionsCompatible( |
| const std::vector<NsswitchReader::ServiceAction>& actions, |
| const std::map<NsswitchReader::Status, NsswitchReader::Action> |
| expected_actions) { |
| std::map<NsswitchReader::Status, NsswitchReader::Action> parsed_behavior; |
| |
| for (const NsswitchReader::ServiceAction& action : actions) { |
| if (action.status == NsswitchReader::Status::kUnknown || |
| action.action == NsswitchReader::Action::kUnknown) { |
| return false; |
| } |
| |
| if (!SetActionBehavior(action, parsed_behavior)) |
| return false; |
| } |
| |
| // Default behavior if not configured. |
| if (parsed_behavior.count(NsswitchReader::Status::kSuccess) == 0) |
| parsed_behavior[NsswitchReader::Status::kSuccess] = |
| NsswitchReader::Action::kReturn; |
| if (parsed_behavior.count(NsswitchReader::Status::kNotFound) == 0) |
| parsed_behavior[NsswitchReader::Status::kNotFound] = |
| NsswitchReader::Action::kContinue; |
| if (parsed_behavior.count(NsswitchReader::Status::kUnavailable) == 0) |
| parsed_behavior[NsswitchReader::Status::kUnavailable] = |
| NsswitchReader::Action::kContinue; |
| if (parsed_behavior.count(NsswitchReader::Status::kTryAgain) == 0) |
| parsed_behavior[NsswitchReader::Status::kTryAgain] = |
| NsswitchReader::Action::kContinue; |
| |
| for (const std::pair<const NsswitchReader::Status, NsswitchReader::Action>& |
| expected : expected_actions) { |
| if (parsed_behavior[expected.first] != expected.second) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // These values are emitted in metrics. Entries should not be renumbered and |
| // numeric values should never be reused. (See NsswitchIncompatibleReason in |
| // tools/metrics/histograms/enums.xml.) |
| enum class IncompatibleNsswitchReason { |
| kFilesMissing = 0, |
| kMultipleFiles = 1, |
| kBadFilesActions = 2, |
| kDnsMissing = 3, |
| kBadDnsActions = 4, |
| kBadMdnsMinimalActions = 5, |
| kBadOtherServiceActions = 6, |
| kUnknownService = 7, |
| kIncompatibleService = 8, |
| kMaxValue = kIncompatibleService |
| }; |
| |
| void RecordIncompatibleNsswitchReason( |
| IncompatibleNsswitchReason reason, |
| absl::optional<NsswitchReader::Service> service_token) { |
| UMA_HISTOGRAM_ENUMERATION("Net.DNS.DnsConfig.Nsswitch.IncompatibleReason", |
| reason); |
| if (service_token) { |
| UMA_HISTOGRAM_ENUMERATION("Net.DNS.DnsConfig.Nsswitch.IncompatibleService", |
| service_token.value()); |
| } |
| } |
| |
| bool IsNsswitchConfigCompatible( |
| const std::vector<NsswitchReader::ServiceSpecification>& nsswitch_hosts) { |
| bool files_found = false; |
| for (const NsswitchReader::ServiceSpecification& specification : |
| nsswitch_hosts) { |
| switch (specification.service) { |
| case NsswitchReader::Service::kUnknown: |
| RecordIncompatibleNsswitchReason( |
| IncompatibleNsswitchReason::kUnknownService, specification.service); |
| return false; |
| |
| case NsswitchReader::Service::kFiles: |
| if (files_found) { |
| RecordIncompatibleNsswitchReason( |
| IncompatibleNsswitchReason::kMultipleFiles, |
| specification.service); |
| return false; |
| } |
| files_found = true; |
| // Chrome will use the result on HOSTS hit and otherwise continue to |
| // DNS. `kFiles` entries must match that behavior to be compatible. |
| if (!AreActionsCompatible(specification.actions, |
| {{NsswitchReader::Status::kSuccess, |
| NsswitchReader::Action::kReturn}, |
| {NsswitchReader::Status::kNotFound, |
| NsswitchReader::Action::kContinue}, |
| {NsswitchReader::Status::kUnavailable, |
| NsswitchReader::Action::kContinue}, |
| {NsswitchReader::Status::kTryAgain, |
| NsswitchReader::Action::kContinue}})) { |
| RecordIncompatibleNsswitchReason( |
| IncompatibleNsswitchReason::kBadFilesActions, |
| specification.service); |
| return false; |
| } |
| break; |
| |
| case NsswitchReader::Service::kDns: |
| if (!files_found) { |
| RecordIncompatibleNsswitchReason( |
| IncompatibleNsswitchReason::kFilesMissing, |
| /*service_token=*/absl::nullopt); |
| return false; |
| } |
| // Chrome will always stop if DNS finds a result or will otherwise |
| // fallback to the system resolver (and get whatever behavior is |
| // configured in nsswitch.conf), so the only compatibility requirement |
| // is that `kDns` entries are configured to return on success. |
| if (!AreActionsCompatible(specification.actions, |
| {{NsswitchReader::Status::kSuccess, |
| NsswitchReader::Action::kReturn}})) { |
| RecordIncompatibleNsswitchReason( |
| IncompatibleNsswitchReason::kBadDnsActions, |
| specification.service); |
| return false; |
| } |
| |
| // Ignore any entries after `kDns` because Chrome will fallback to the |
| // system resolver if a result was not found in DNS. |
| return true; |
| |
| case NsswitchReader::Service::kMdns: |
| case NsswitchReader::Service::kMdns4: |
| case NsswitchReader::Service::kMdns6: |
| case NsswitchReader::Service::kResolve: |
| RecordIncompatibleNsswitchReason( |
| IncompatibleNsswitchReason::kIncompatibleService, |
| specification.service); |
| return false; |
| |
| case NsswitchReader::Service::kMdnsMinimal: |
| case NsswitchReader::Service::kMdns4Minimal: |
| case NsswitchReader::Service::kMdns6Minimal: |
| // Always compatible as long as `kUnavailable` is `kContinue` because |
| // the service is expected to always result in `kUnavailable` for any |
| // names Chrome would attempt to resolve (non-*.local names because |
| // Chrome always delegates *.local names to the system resolver). |
| if (!AreActionsCompatible(specification.actions, |
| {{NsswitchReader::Status::kUnavailable, |
| NsswitchReader::Action::kContinue}})) { |
| RecordIncompatibleNsswitchReason( |
| IncompatibleNsswitchReason::kBadMdnsMinimalActions, |
| specification.service); |
| return false; |
| } |
| break; |
| |
| case NsswitchReader::Service::kMyHostname: |
| case NsswitchReader::Service::kNis: |
| // Similar enough to Chrome behavior (or unlikely to matter for Chrome |
| // resolutions) to be considered compatible unless the actions do |
| // something very weird to skip remaining services without a result. |
| if (!AreActionsCompatible(specification.actions, |
| {{NsswitchReader::Status::kNotFound, |
| NsswitchReader::Action::kContinue}, |
| {NsswitchReader::Status::kUnavailable, |
| NsswitchReader::Action::kContinue}, |
| {NsswitchReader::Status::kTryAgain, |
| NsswitchReader::Action::kContinue}})) { |
| RecordIncompatibleNsswitchReason( |
| IncompatibleNsswitchReason::kBadOtherServiceActions, |
| specification.service); |
| return false; |
| } |
| break; |
| } |
| } |
| |
| RecordIncompatibleNsswitchReason(IncompatibleNsswitchReason::kDnsMissing, |
| /*service_token=*/absl::nullopt); |
| return false; |
| } |
| |
| } // namespace |
| |
| class DnsConfigServiceLinux::Watcher : public DnsConfigService::Watcher { |
| public: |
| explicit Watcher(DnsConfigServiceLinux& service) |
| : DnsConfigService::Watcher(service) {} |
| ~Watcher() override = default; |
| |
| Watcher(const Watcher&) = delete; |
| Watcher& operator=(const Watcher&) = delete; |
| |
| bool Watch() override { |
| CheckOnCorrectSequence(); |
| |
| bool success = true; |
| if (!resolv_watcher_.Watch( |
| base::FilePath(kFilePathResolv), |
| base::FilePathWatcher::Type::kNonRecursive, |
| base::BindRepeating(&Watcher::OnResolvFilePathWatcherChange, |
| base::Unretained(this)))) { |
| LOG(ERROR) << "DNS config (resolv.conf) watch failed to start."; |
| success = false; |
| } |
| |
| if (!nsswitch_watcher_.Watch( |
| base::FilePath(kFilePathNsswitch), |
| base::FilePathWatcher::Type::kNonRecursive, |
| base::BindRepeating(&Watcher::OnNsswitchFilePathWatcherChange, |
| base::Unretained(this)))) { |
| LOG(ERROR) << "DNS nsswitch.conf watch failed to start."; |
| success = false; |
| } |
| |
| if (!hosts_watcher_.Watch( |
| base::FilePath(kFilePathHosts), |
| base::FilePathWatcher::Type::kNonRecursive, |
| base::BindRepeating(&Watcher::OnHostsFilePathWatcherChange, |
| base::Unretained(this)))) { |
| LOG(ERROR) << "DNS hosts watch failed to start."; |
| success = false; |
| } |
| return success; |
| } |
| |
| private: |
| void OnResolvFilePathWatcherChange(const base::FilePath& path, bool error) { |
| UMA_HISTOGRAM_BOOLEAN("Net.DNS.DnsConfig.Resolv.FileChange", true); |
| OnConfigChanged(!error); |
| } |
| |
| void OnNsswitchFilePathWatcherChange(const base::FilePath& path, bool error) { |
| UMA_HISTOGRAM_BOOLEAN("Net.DNS.DnsConfig.Nsswitch.FileChange", true); |
| OnConfigChanged(!error); |
| } |
| |
| void OnHostsFilePathWatcherChange(const base::FilePath& path, bool error) { |
| OnHostsChanged(!error); |
| } |
| |
| base::FilePathWatcher resolv_watcher_; |
| base::FilePathWatcher nsswitch_watcher_; |
| base::FilePathWatcher hosts_watcher_; |
| }; |
| |
| // A SerialWorker that uses libresolv to initialize res_state and converts |
| // it to DnsConfig. |
| class DnsConfigServiceLinux::ConfigReader : public SerialWorker { |
| public: |
| explicit ConfigReader(DnsConfigServiceLinux& service, |
| std::unique_ptr<ResolvReader> resolv_reader, |
| std::unique_ptr<NsswitchReader> nsswitch_reader) |
| : service_(&service), |
| work_item_(std::make_unique<WorkItem>(std::move(resolv_reader), |
| std::move(nsswitch_reader))) { |
| // Allow execution on another thread; nothing thread-specific about |
| // constructor. |
| DETACH_FROM_SEQUENCE(sequence_checker_); |
| } |
| |
| ~ConfigReader() override = default; |
| |
| ConfigReader(const ConfigReader&) = delete; |
| ConfigReader& operator=(const ConfigReader&) = delete; |
| |
| std::unique_ptr<SerialWorker::WorkItem> CreateWorkItem() override { |
| // Reuse same `WorkItem` to allow reuse of contained reader objects. |
| DCHECK(work_item_); |
| return std::move(work_item_); |
| } |
| |
| bool OnWorkFinished(std::unique_ptr<SerialWorker::WorkItem> |
| serial_worker_work_item) override { |
| DCHECK(serial_worker_work_item); |
| DCHECK(!work_item_); |
| DCHECK(!IsCancelled()); |
| |
| work_item_.reset(static_cast<WorkItem*>(serial_worker_work_item.release())); |
| if (work_item_->dns_config_.has_value()) { |
| service_->OnConfigRead(std::move(work_item_->dns_config_).value()); |
| return true; |
| } else { |
| LOG(WARNING) << "Failed to read DnsConfig."; |
| return false; |
| } |
| } |
| |
| private: |
| class WorkItem : public SerialWorker::WorkItem { |
| public: |
| WorkItem(std::unique_ptr<ResolvReader> resolv_reader, |
| std::unique_ptr<NsswitchReader> nsswitch_reader) |
| : resolv_reader_(std::move(resolv_reader)), |
| nsswitch_reader_(std::move(nsswitch_reader)) { |
| DCHECK(resolv_reader_); |
| DCHECK(nsswitch_reader_); |
| } |
| |
| void DoWork() override { |
| base::ScopedBlockingCall scoped_blocking_call( |
| FROM_HERE, base::BlockingType::MAY_BLOCK); |
| |
| { |
| std::unique_ptr<ScopedResState> res = resolv_reader_->GetResState(); |
| if (res) { |
| dns_config_ = ConvertResStateToDnsConfig(res->state()); |
| } |
| } |
| |
| UMA_HISTOGRAM_BOOLEAN("Net.DNS.DnsConfig.Resolv.Read", |
| dns_config_.has_value()); |
| if (!dns_config_.has_value()) |
| return; |
| UMA_HISTOGRAM_BOOLEAN("Net.DNS.DnsConfig.Resolv.Valid", |
| dns_config_->IsValid()); |
| UMA_HISTOGRAM_BOOLEAN("Net.DNS.DnsConfig.Resolv.Compatible", |
| !dns_config_->unhandled_options); |
| |
| // Override `fallback_period` value to match default setting on |
| // Windows. |
| dns_config_->fallback_period = kDnsDefaultFallbackPeriod; |
| |
| if (dns_config_ && !dns_config_->unhandled_options) { |
| std::vector<NsswitchReader::ServiceSpecification> nsswitch_hosts = |
| nsswitch_reader_->ReadAndParseHosts(); |
| UMA_HISTOGRAM_COUNTS_100("Net.DNS.DnsConfig.Nsswitch.NumServices", |
| nsswitch_hosts.size()); |
| dns_config_->unhandled_options = |
| !IsNsswitchConfigCompatible(nsswitch_hosts); |
| UMA_HISTOGRAM_BOOLEAN("Net.DNS.DnsConfig.Nsswitch.Compatible", |
| !dns_config_->unhandled_options); |
| } |
| } |
| |
| private: |
| friend class ConfigReader; |
| absl::optional<DnsConfig> dns_config_; |
| std::unique_ptr<ResolvReader> resolv_reader_; |
| std::unique_ptr<NsswitchReader> nsswitch_reader_; |
| }; |
| |
| // Raw pointer to owning DnsConfigService. |
| const raw_ptr<DnsConfigServiceLinux> service_; |
| |
| // Null while the `WorkItem` is running on the `ThreadPool`. |
| std::unique_ptr<WorkItem> work_item_; |
| }; |
| |
| DnsConfigServiceLinux::DnsConfigServiceLinux() |
| : DnsConfigService(kFilePathHosts) { |
| // Allow constructing on one thread and living on another. |
| DETACH_FROM_SEQUENCE(sequence_checker_); |
| } |
| |
| DnsConfigServiceLinux::~DnsConfigServiceLinux() { |
| if (config_reader_) |
| config_reader_->Cancel(); |
| } |
| |
| void DnsConfigServiceLinux::ReadConfigNow() { |
| if (!config_reader_) |
| CreateReader(); |
| config_reader_->WorkNow(); |
| } |
| |
| bool DnsConfigServiceLinux::StartWatching() { |
| CreateReader(); |
| watcher_ = std::make_unique<Watcher>(*this); |
| return watcher_->Watch(); |
| } |
| |
| void DnsConfigServiceLinux::CreateReader() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(!config_reader_); |
| DCHECK(resolv_reader_); |
| DCHECK(nsswitch_reader_); |
| config_reader_ = std::make_unique<ConfigReader>( |
| *this, std::move(resolv_reader_), std::move(nsswitch_reader_)); |
| } |
| |
| } // namespace internal |
| |
| // static |
| std::unique_ptr<DnsConfigService> DnsConfigService::CreateSystemService() { |
| return std::make_unique<internal::DnsConfigServiceLinux>(); |
| } |
| |
| } // namespace net |