| // 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. | |
| #include "dns-proxy/proxy.h" | |
| #include <linux/rtnetlink.h> | |
| #include <net/if.h> | |
| #include <sys/types.h> | |
| #include <sysexits.h> | |
| #include <unistd.h> | |
| #include <optional> | |
| #include <set> | |
| #include <base/check.h> | |
| #include <base/functional/bind.h> | |
| #include <base/logging.h> | |
| #include <base/strings/string_split.h> | |
| #include <base/strings/string_util.h> | |
| #include <base/task/single_thread_task_runner.h> | |
| #include <base/time/time.h> | |
| #include <chromeos/patchpanel/message_dispatcher.h> | |
| #include <net-base/rtnl_handler.h> | |
| #include <shill/dbus-constants.h> | |
| #include "dns-proxy/ipc.pb.h" | |
| // Using directive is necessary to have the overloaded function for socket data | |
| // structure available. | |
| using patchpanel::operator<<; | |
| namespace dns_proxy { | |
| namespace { | |
| // The DoH provider URLs that come from Chrome may be URI templates instead. | |
| // Per https://datatracker.ietf.org/doc/html/rfc8484#section-4.1 these will | |
| // include the {?dns} parameter template for GET requests. These can be safely | |
| // removed since any compliant server must support both GET and POST requests | |
| // and this services only uses POST. | |
| constexpr char kDNSParamTemplate[] = "{?dns}"; | |
| std::string TrimParamTemplate(const std::string& url) { | |
| const size_t pos = url.find(kDNSParamTemplate); | |
| if (pos == std::string::npos) { | |
| return url; | |
| } | |
| return url.substr(0, pos); | |
| } | |
| Metrics::ProcessType ProcessTypeOf(Proxy::Type t) { | |
| switch (t) { | |
| case Proxy::Type::kSystem: | |
| return Metrics::ProcessType::kProxySystem; | |
| case Proxy::Type::kDefault: | |
| return Metrics::ProcessType::kProxyDefault; | |
| case Proxy::Type::kARC: | |
| return Metrics::ProcessType::kProxyARC; | |
| default: | |
| NOTREACHED(); | |
| } | |
| } | |
| template <typename T> | |
| std::vector<std::string> ToStringVec(const std::vector<T>& addrs) { | |
| std::vector<std::string> ret; | |
| for (const auto& addr : addrs) { | |
| ret.push_back(addr.ToString()); | |
| } | |
| return ret; | |
| } | |
| } // namespace | |
| constexpr base::TimeDelta kShillPropertyAttemptDelay = base::Milliseconds(200); | |
| constexpr base::TimeDelta kRequestTimeout = base::Seconds(10); | |
| constexpr base::TimeDelta kRequestRetryDelay = base::Milliseconds(200); | |
| constexpr char kSystemProxyType[] = "system"; | |
| constexpr char kDefaultProxyType[] = "default"; | |
| constexpr char kARCProxyType[] = "arc"; | |
| constexpr int32_t kRequestMaxRetry = 1; | |
| constexpr uint16_t kDefaultPort = 13568; // port 53 in network order. | |
| // static | |
| const char* Proxy::TypeToString(Type t) { | |
| switch (t) { | |
| case Type::kSystem: | |
| return kSystemProxyType; | |
| case Type::kDefault: | |
| return kDefaultProxyType; | |
| case Type::kARC: | |
| return kARCProxyType; | |
| default: | |
| NOTREACHED(); | |
| } | |
| } | |
| // static | |
| std::optional<Proxy::Type> Proxy::StringToType(const std::string& s) { | |
| if (s == kSystemProxyType) | |
| return Type::kSystem; | |
| if (s == kDefaultProxyType) | |
| return Type::kDefault; | |
| if (s == kARCProxyType) | |
| return Type::kARC; | |
| return std::nullopt; | |
| } | |
| std::ostream& operator<<(std::ostream& stream, Proxy::Type type) { | |
| stream << Proxy::TypeToString(type); | |
| return stream; | |
| } | |
| std::ostream& operator<<(std::ostream& stream, Proxy::Options opts) { | |
| stream << "{" << Proxy::TypeToString(opts.type) << ":" << opts.ifname << "}"; | |
| return stream; | |
| } | |
| std::ostream& operator<<(std::ostream& stream, const Proxy& proxy) { | |
| stream << "{" << Proxy::TypeToString(proxy.opts_.type) << ":"; | |
| if (!proxy.opts_.ifname.empty()) { | |
| stream << proxy.opts_.ifname; | |
| } else if (proxy.device_ && !proxy.device_->ifname.empty()) { | |
| stream << proxy.device_->ifname; | |
| } else { | |
| stream << "_"; | |
| } | |
| return stream << "}"; | |
| } | |
| Proxy::Proxy(const Proxy::Options& opts, int32_t fd) | |
| : opts_(opts), metrics_proc_type_(ProcessTypeOf(opts_.type)) { | |
| doh_config_.set_logger( | |
| base::BindRepeating(&Proxy::LogName, weak_factory_.GetWeakPtr())); | |
| if (opts_.type == Type::kSystem) { | |
| doh_config_.set_metrics(&metrics_); | |
| msg_dispatcher_ = | |
| std::make_unique<patchpanel::MessageDispatcher<ProxyAddrMessage>>( | |
| base::ScopedFD(fd)); | |
| } | |
| addr_listener_ = std::make_unique<net_base::RTNLListener>( | |
| net_base::RTNLHandler::kRequestAddr, | |
| base::BindRepeating(&Proxy::RTNLMessageHandler, | |
| weak_factory_.GetWeakPtr())); | |
| net_base::RTNLHandler::GetInstance()->Start(RTMGRP_IPV6_IFADDR); | |
| } | |
| // This ctor is only used for testing. | |
| Proxy::Proxy(const Options& opts, | |
| std::unique_ptr<patchpanel::Client> patchpanel, | |
| std::unique_ptr<shill::Client> shill, | |
| std::unique_ptr<patchpanel::MessageDispatcher<ProxyAddrMessage>> | |
| msg_dispatcher) | |
| : opts_(opts), | |
| patchpanel_(std::move(patchpanel)), | |
| shill_(std::move(shill)), | |
| metrics_proc_type_(ProcessTypeOf(opts_.type)) { | |
| if (opts_.type == Type::kSystem) { | |
| msg_dispatcher_ = std::move(msg_dispatcher); | |
| } | |
| } | |
| int Proxy::OnInit() { | |
| LOG(INFO) << *this << " Starting DNS proxy"; | |
| /// Run after Daemon::OnInit() | |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( | |
| FROM_HERE, base::BindOnce(&Proxy::Setup, weak_factory_.GetWeakPtr())); | |
| return DBusDaemon::OnInit(); | |
| } | |
| void Proxy::OnShutdown(int* code) { | |
| LOG(INFO) << *this << " Stopping DNS proxy (" << *code << ")"; | |
| addr_listener_.reset(); | |
| if (opts_.type == Type::kSystem) { | |
| ClearShillDNSProxyAddresses(); | |
| ClearIPAddressesInController(); | |
| } | |
| } | |
| void Proxy::Setup() { | |
| if (!session_) { | |
| session_ = std::make_unique<SessionMonitor>(bus_); | |
| } | |
| session_->RegisterSessionStateHandler(base::BindRepeating( | |
| &Proxy::OnSessionStateChanged, weak_factory_.GetWeakPtr())); | |
| if (!patchpanel_) | |
| patchpanel_ = patchpanel::Client::New(bus_); | |
| if (!patchpanel_) { | |
| metrics_.RecordProcessEvent( | |
| metrics_proc_type_, Metrics::ProcessEvent::kPatchpanelNotInitialized); | |
| LOG(ERROR) << *this << " Failed to initialize patchpanel client"; | |
| QuitWithExitCode(EX_UNAVAILABLE); | |
| return; | |
| } | |
| patchpanel_->RegisterOnAvailableCallback( | |
| base::BindOnce(&Proxy::OnPatchpanelReady, weak_factory_.GetWeakPtr())); | |
| patchpanel_->RegisterProcessChangedCallback(base::BindRepeating( | |
| &Proxy::OnPatchpanelReset, weak_factory_.GetWeakPtr())); | |
| } | |
| void Proxy::OnPatchpanelReady(bool success) { | |
| if (!success) { | |
| metrics_.RecordProcessEvent(metrics_proc_type_, | |
| Metrics::ProcessEvent::kPatchpanelNotReady); | |
| LOG(ERROR) << *this << " Failed to connect to patchpanel"; | |
| QuitWithExitCode(EX_UNAVAILABLE); | |
| return; | |
| } | |
| // The default network proxy might actually be carrying Chrome, Crostini or | |
| // if a VPN is on, even ARC traffic, but we attribute this as as "user" | |
| // sourced. | |
| patchpanel::Client::TrafficSource traffic_source; | |
| switch (opts_.type) { | |
| case Type::kSystem: | |
| traffic_source = patchpanel::Client::TrafficSource::kSystem; | |
| break; | |
| case Type::kARC: | |
| traffic_source = patchpanel::Client::TrafficSource::kArc; | |
| break; | |
| default: | |
| traffic_source = patchpanel::Client::TrafficSource::kUser; | |
| } | |
| // Note that using getpid() here requires that this minijail is not creating a | |
| // new PID namespace. | |
| // The default proxy (only) needs to use the VPN, if applicable, the others | |
| // expressly need to avoid it. | |
| // TODO(b/273744897): Use the patchpanel Network id of the shill Device that | |
| // this Proxy is associated to. Until the Network id is available, using the | |
| // shill's Device kInterfaceProperty is consistent with patchpanel's tracking | |
| // of shill's Devices. For multiplexed Cellular interfaces, patchpanel is | |
| // responsible for using the correct multiplexed network interface | |
| // (b/273741099). Callers of ConnectNamespace are expected to use the shill's | |
| // Device kInterfaceProperty. | |
| auto res = patchpanel_->ConnectNamespace( | |
| getpid(), opts_.ifname, /*forward_user_traffic=*/true, | |
| /*route_on_vpn=*/opts_.type == Type::kDefault, traffic_source, | |
| /*static_ipv6=*/true); | |
| if (!res.first.is_valid()) { | |
| metrics_.RecordProcessEvent(metrics_proc_type_, | |
| Metrics::ProcessEvent::kPatchpanelNoNamespace); | |
| LOG(ERROR) << *this << " Failed to establish private network namespace"; | |
| QuitWithExitCode(EX_CANTCREAT); | |
| return; | |
| } | |
| ns_fd_ = std::move(res.first); | |
| ns_ = res.second; | |
| LOG(INFO) << *this << " Successfully connected private network namespace: " | |
| << ns_.host_ifname << " <--> " << ns_.peer_ifname; | |
| // Now it's safe to connect shill. | |
| InitShill(); | |
| // Track single-networked guests' start up and shut down for redirecting | |
| // traffic to the proxy. | |
| if (opts_.type == Type::kDefault) | |
| patchpanel_->RegisterVirtualDeviceEventHandler(base::BindRepeating( | |
| &Proxy::OnVirtualDeviceChanged, weak_factory_.GetWeakPtr())); | |
| } | |
| void Proxy::StartDnsRedirection(const std::string& ifname, | |
| sa_family_t sa_family, | |
| const std::vector<std::string>& nameservers) { | |
| // Request IPv6 DNS redirection rule only if the IPv6 address is available. | |
| if (sa_family == AF_INET6 && !ns_peer_ipv6_address_) { | |
| return; | |
| } | |
| // Reset last created rules. | |
| lifeline_fds_.erase(std::make_pair(ifname, sa_family)); | |
| patchpanel::Client::DnsRedirectionRequestType type; | |
| switch (opts_.type) { | |
| case Type::kSystem: | |
| type = patchpanel::Client::DnsRedirectionRequestType::kExcludeDestination; | |
| break; | |
| case Type::kDefault: | |
| type = patchpanel::Client::DnsRedirectionRequestType::kDefault; | |
| // If |ifname| is empty, request SetDnsRedirectionRule rule for USER. | |
| if (ifname.empty()) { | |
| type = patchpanel::Client::DnsRedirectionRequestType::kUser; | |
| } | |
| break; | |
| case Type::kARC: | |
| type = patchpanel::Client::DnsRedirectionRequestType::kArc; | |
| break; | |
| default: | |
| LOG(DFATAL) << *this << " Unexpected proxy type " << opts_.type; | |
| return; | |
| } | |
| const auto peer_addr = sa_family == AF_INET6 | |
| ? ns_peer_ipv6_address_->ToString() | |
| : ns_.peer_ipv4_address.ToString(); | |
| auto fd = patchpanel_->RedirectDns(type, ifname, peer_addr, nameservers, | |
| ns_.host_ifname); | |
| // Restart the proxy if DNS redirection rules are failed to be set up. This | |
| // is necessary because when DNS proxy is running, /etc/resolv.conf is | |
| // replaced by the IP address of system proxy. This causes non-system traffic | |
| // to be routed incorrectly without the redirection rules. | |
| if (!fd.is_valid()) { | |
| metrics_.RecordProcessEvent(metrics_proc_type_, | |
| Metrics::ProcessEvent::kPatchpanelNoRedirect); | |
| LOG(ERROR) << *this << " Failed to start DNS redirection"; | |
| QuitWithExitCode(EX_CONFIG); | |
| return; | |
| } | |
| lifeline_fds_.emplace(std::make_pair(ifname, sa_family), std::move(fd)); | |
| } | |
| void Proxy::StopDnsRedirection(const std::string& ifname, | |
| sa_family_t sa_family) { | |
| lifeline_fds_.erase(std::make_pair(ifname, sa_family)); | |
| } | |
| void Proxy::OnPatchpanelReset(bool reset) { | |
| if (reset) { | |
| metrics_.RecordProcessEvent(metrics_proc_type_, | |
| Metrics::ProcessEvent::kPatchpanelReset); | |
| LOG(WARNING) << *this << " Patchpanel has been reset"; | |
| return; | |
| } | |
| // If patchpanel crashes, the proxy is useless since the connected virtual | |
| // network is gone. So the best bet is to exit and have the controller restart | |
| // us. Note if this is the system proxy, it will inform shill on shutdown. | |
| metrics_.RecordProcessEvent(metrics_proc_type_, | |
| Metrics::ProcessEvent::kPatchpanelShutdown); | |
| LOG(ERROR) << *this << " Patchpanel has been shutdown - restarting DNS proxy"; | |
| QuitWithExitCode(EX_UNAVAILABLE); | |
| } | |
| void Proxy::InitShill() { | |
| // shill_ should always be null unless a test has injected a client. | |
| if (!shill_) | |
| shill_.reset(new shill::Client(bus_)); | |
| shill_->RegisterOnAvailableCallback( | |
| base::BindOnce(&Proxy::OnShillReady, weak_factory_.GetWeakPtr())); | |
| shill_->RegisterProcessChangedHandler( | |
| base::BindRepeating(&Proxy::OnShillReset, weak_factory_.GetWeakPtr())); | |
| } | |
| void Proxy::OnShillReady(bool success) { | |
| shill_ready_ = success; | |
| if (!shill_ready_) { | |
| metrics_.RecordProcessEvent(metrics_proc_type_, | |
| Metrics::ProcessEvent::kShillNotReady); | |
| LOG(ERROR) << *this << " Failed to connect to shill"; | |
| QuitWithExitCode(EX_UNAVAILABLE); | |
| return; | |
| } | |
| shill_->RegisterDefaultDeviceChangedHandler(base::BindRepeating( | |
| &Proxy::OnDefaultDeviceChanged, weak_factory_.GetWeakPtr())); | |
| shill_->RegisterDeviceChangedHandler( | |
| base::BindRepeating(&Proxy::OnDeviceChanged, weak_factory_.GetWeakPtr())); | |
| if (opts_.type == Proxy::Type::kARC) { | |
| for (const auto& d : shill_->GetDevices()) { | |
| OnDeviceChanged(d.get()); | |
| } | |
| } | |
| } | |
| void Proxy::OnShillReset(bool reset) { | |
| if (reset) { | |
| metrics_.RecordProcessEvent(metrics_proc_type_, | |
| Metrics::ProcessEvent::kShillReset); | |
| LOG(WARNING) << *this << " Shill has been reset"; | |
| // If applicable, restore the address of the system proxy. | |
| if (opts_.type == Type::kSystem && ns_fd_.is_valid()) { | |
| SetShillDNSProxyAddresses(ns_.peer_ipv4_address, ns_peer_ipv6_address_); | |
| // Start DNS redirection rule to exclude traffic with destination not | |
| // equal to the underlying name server. | |
| StartDnsRedirection(/*ifname=*/"", AF_INET); | |
| StartDnsRedirection(/*ifname=*/"", AF_INET6); | |
| } | |
| return; | |
| } | |
| metrics_.RecordProcessEvent(metrics_proc_type_, | |
| Metrics::ProcessEvent::kShillShutdown); | |
| LOG(WARNING) << *this << " Shill has been shutdown"; | |
| shill_ready_ = false; | |
| shill_props_.reset(); | |
| shill_.reset(); | |
| InitShill(); | |
| } | |
| void Proxy::OnSessionStateChanged(bool login) { | |
| if (login) { | |
| LOG(INFO) << *this << " Service enabled by user login"; | |
| Enable(); | |
| return; | |
| } | |
| LOG(INFO) << *this << " Service disabled by user logout"; | |
| Disable(); | |
| } | |
| void Proxy::Enable() { | |
| if (!ns_fd_.is_valid() || !device_) | |
| return; | |
| if (opts_.type == Type::kSystem) { | |
| SetShillDNSProxyAddresses(ns_.peer_ipv4_address, ns_peer_ipv6_address_); | |
| SendIPAddressesToController(ns_.peer_ipv4_address, ns_peer_ipv6_address_); | |
| // Start DNS redirection rule to exclude traffic with destination not equal | |
| // to the underlying name server. | |
| StartDnsRedirection(/*ifname=*/"", AF_INET); | |
| StartDnsRedirection(/*ifname=*/"", AF_INET6); | |
| return; | |
| } | |
| if (opts_.type == Type::kDefault) { | |
| // Start DNS redirection rule for user traffic (cups, chronos, update | |
| // engine, etc). | |
| StartDnsRedirection(/*ifname=*/"", AF_INET, | |
| ToStringVec(doh_config_.ipv4_nameservers())); | |
| StartDnsRedirection(/*ifname=*/"", AF_INET6, | |
| ToStringVec(doh_config_.ipv6_nameservers())); | |
| } | |
| // Process the current set of patchpanel devices and add necessary | |
| // redirection rules. | |
| for (const auto& d : patchpanel_->GetDevices()) { | |
| StartGuestDnsRedirection(d, AF_INET); | |
| StartGuestDnsRedirection(d, AF_INET6); | |
| } | |
| } | |
| void Proxy::Disable() { | |
| if (opts_.type == Type::kSystem && ns_fd_.is_valid()) { | |
| ClearShillDNSProxyAddresses(); | |
| ClearIPAddressesInController(); | |
| } | |
| // Teardown DNS redirection rules. | |
| lifeline_fds_.clear(); | |
| } | |
| void Proxy::Stop() { | |
| doh_config_.clear(); | |
| resolver_.reset(); | |
| device_.reset(); | |
| lifeline_fds_.clear(); | |
| if (opts_.type == Type::kSystem) { | |
| ClearShillDNSProxyAddresses(); | |
| ClearIPAddressesInController(); | |
| } | |
| } | |
| std::unique_ptr<Resolver> Proxy::NewResolver(base::TimeDelta timeout, | |
| base::TimeDelta retry_delay, | |
| int max_num_retries) { | |
| return std::make_unique<Resolver>( | |
| base::BindRepeating(&Proxy::LogName, weak_factory_.GetWeakPtr()), timeout, | |
| retry_delay, max_num_retries); | |
| } | |
| void Proxy::OnDefaultDeviceChanged(const shill::Client::Device* const device) { | |
| // ARC proxies will handle changes to their network in OnDeviceChanged. | |
| if (opts_.type == Proxy::Type::kARC) | |
| return; | |
| // Default service is either not ready yet or has just disconnected. | |
| if (!device) { | |
| // If it disconnected, shutdown the resolver. | |
| if (device_) { | |
| LOG(WARNING) << *this | |
| << " is stopping because there is no default service"; | |
| Stop(); | |
| } | |
| return; | |
| } | |
| shill::Client::Device new_default_device = *device; | |
| // The system proxy should ignore when a VPN is turned on as it must continue | |
| // to work with the underlying physical interface. | |
| if (opts_.type == Proxy::Type::kSystem && | |
| device->type == shill::Client::Device::Type::kVPN) { | |
| if (device_) | |
| return; | |
| // No device means that the system proxy has started up with a VPN as the | |
| // default network; which means we need to dig out the physical network | |
| // device and use that from here forward. | |
| auto dd = shill_->DefaultDevice(/*exclude_vpn=*/true); | |
| if (!dd) { | |
| LOG(ERROR) << *this << " No default non-VPN device found"; | |
| return; | |
| } | |
| new_default_device = *dd.get(); | |
| } | |
| // While this is enforced in shill as well, only enable resolution if the | |
| // service online. | |
| if (new_default_device.state != | |
| shill::Client::Device::ConnectionState::kOnline) { | |
| if (device_) { | |
| LOG(WARNING) << *this << " is stopping because the default device [" | |
| << new_default_device.ifname << "] is offline"; | |
| Stop(); | |
| } | |
| return; | |
| } | |
| if (!device_) | |
| device_ = std::make_unique<shill::Client::Device>(); | |
| // The default network has changed. | |
| if (new_default_device.ifname != device_->ifname) | |
| LOG(INFO) << *this << " is now tracking [" << new_default_device.ifname | |
| << "]"; | |
| *device_.get() = new_default_device; | |
| MaybeCreateResolver(); | |
| UpdateNameServers(); | |
| // For the default proxy, we have to update DNS redirection rule. This allows | |
| // DNS traffic to be redirected to the proxy. | |
| if (opts_.type == Type::kDefault) { | |
| // Start DNS redirection rule for user traffic (cups, chronos, update | |
| // engine, etc). | |
| StartDnsRedirection(/*ifname=*/"", AF_INET, | |
| ToStringVec(doh_config_.ipv4_nameservers())); | |
| StartDnsRedirection(/*ifname=*/"", AF_INET6, | |
| ToStringVec(doh_config_.ipv6_nameservers())); | |
| // Process the current set of patchpanel devices and add necessary | |
| // redirection rules. | |
| for (const auto& d : patchpanel_->GetDevices()) { | |
| StartGuestDnsRedirection(d, AF_INET); | |
| StartGuestDnsRedirection(d, AF_INET6); | |
| } | |
| } | |
| // For the system proxy, we have to tell shill about it. We should start | |
| // receiving DNS traffic on success. But if this fails, we don't have much | |
| // choice but to just crash out and try again. | |
| if (opts_.type == Type::kSystem) { | |
| SetShillDNSProxyAddresses(ns_.peer_ipv4_address, ns_peer_ipv6_address_, | |
| /*die_on_failure=*/true); | |
| SendIPAddressesToController(ns_.peer_ipv4_address, ns_peer_ipv6_address_); | |
| // Start DNS redirection rule to exclude traffic with destination not equal | |
| // to the underlying name server. | |
| StartDnsRedirection(/*ifname=*/"", AF_INET); | |
| StartDnsRedirection(/*ifname=*/"", AF_INET6); | |
| } | |
| } | |
| shill::Client::ManagerPropertyAccessor* Proxy::shill_props() { | |
| if (!shill_props_) { | |
| shill_props_ = shill_->ManagerProperties(); | |
| shill_props_->Watch(shill::kDNSProxyDOHProvidersProperty, | |
| base::BindRepeating(&Proxy::OnDoHProvidersChanged, | |
| weak_factory_.GetWeakPtr())); | |
| } | |
| return shill_props_.get(); | |
| } | |
| void Proxy::OnDeviceChanged(const shill::Client::Device* const device) { | |
| if (!device || (device_ && device_->ifname != device->ifname)) | |
| return; | |
| switch (opts_.type) { | |
| case Type::kDefault: | |
| // We don't need to worry about this here since the default proxy | |
| // always/only tracks the default device and any update will be handled by | |
| // OnDefaultDeviceChanged. | |
| return; | |
| case Type::kSystem: | |
| if (!device_ || device_->network_config == device->network_config) | |
| return; | |
| device_->network_config = device->network_config; | |
| UpdateNameServers(); | |
| return; | |
| case Type::kARC: | |
| // TODO(b/273744897): Change this checks to compare the Network id | |
| // associated with the shill's Device (primary Network) once patchpanel | |
| // Network ids are available and once dnsproxy uses the patchpanel | |
| // Network id. | |
| if (opts_.ifname != device->ifname) | |
| return; | |
| if (device->state != shill::Client::Device::ConnectionState::kOnline) { | |
| if (device_) { | |
| LOG(WARNING) << *this << " is stopping because the device [" | |
| << device->ifname << "] is offline"; | |
| Stop(); | |
| } | |
| return; | |
| } | |
| if (!device_) { | |
| device_ = std::make_unique<shill::Client::Device>(); | |
| } | |
| *device_.get() = *device; | |
| MaybeCreateResolver(); | |
| UpdateNameServers(); | |
| // Process the current set of patchpanel devices and add necessary | |
| // redirection rules. | |
| for (const auto& d : patchpanel_->GetDevices()) { | |
| StartGuestDnsRedirection(d, AF_INET); | |
| StartGuestDnsRedirection(d, AF_INET6); | |
| } | |
| break; | |
| default: | |
| NOTREACHED(); | |
| } | |
| } | |
| void Proxy::MaybeCreateResolver() { | |
| if (resolver_) | |
| return; | |
| resolver_ = | |
| NewResolver(kRequestTimeout, kRequestRetryDelay, kRequestMaxRetry); | |
| doh_config_.set_resolver(resolver_.get()); | |
| // Listen on IPv4 and IPv6. Listening on AF_INET explicitly is not needed | |
| // because net.ipv6.bindv6only sysctl is defaulted to 0 and is not | |
| // explicitly turned on in the codebase. | |
| struct sockaddr_in6 addr = {0}; | |
| addr.sin6_family = AF_INET6; | |
| addr.sin6_port = kDefaultPort; | |
| addr.sin6_addr = | |
| in6addr_any; // Since we're running in the private namespace. | |
| if (!resolver_->ListenUDP(reinterpret_cast<struct sockaddr*>(&addr))) { | |
| metrics_.RecordProcessEvent( | |
| metrics_proc_type_, Metrics::ProcessEvent::kResolverListenUDPFailure); | |
| LOG(ERROR) << *this << " failed to start UDP relay loop"; | |
| QuitWithExitCode(EX_IOERR); | |
| return; | |
| } | |
| if (!resolver_->ListenTCP(reinterpret_cast<struct sockaddr*>(&addr))) { | |
| metrics_.RecordProcessEvent( | |
| metrics_proc_type_, Metrics::ProcessEvent::kResolverListenTCPFailure); | |
| LOG(DFATAL) << *this << " failed to start TCP relay loop"; | |
| } | |
| // Fetch the DoH settings. | |
| brillo::ErrorPtr error; | |
| brillo::VariantDictionary doh_providers; | |
| if (shill_props()->Get(shill::kDNSProxyDOHProvidersProperty, &doh_providers, | |
| &error)) { | |
| OnDoHProvidersChanged(brillo::Any(doh_providers)); | |
| } else { | |
| // Only log this metric in the system proxy to avoid replicating the data. | |
| if (opts_.type == Type::kSystem) { | |
| metrics_.RecordDnsOverHttpsMode(Metrics::DnsOverHttpsMode::kUnknown); | |
| } | |
| LOG(ERROR) << *this << " failed to obtain DoH configuration from shill: " | |
| << error->GetMessage(); | |
| } | |
| } | |
| void Proxy::UpdateNameServers() { | |
| if (!device_) { | |
| LOG(ERROR) << *this << " updating name servers with invalid shill device"; | |
| return; | |
| } | |
| // Use pointer to avoid unnecessary copies. | |
| auto* network_config = &device_->network_config; | |
| // Special case for VPN without nameserver. Fallback to default physical | |
| // network's nameserver(s). | |
| if (device_->type == shill::Client::Device::Type::kVPN && | |
| device_->network_config.dns_servers.empty()) { | |
| auto dd = shill_->DefaultDevice(/*exclude_vpn=*/true); | |
| if (!dd) { | |
| LOG(ERROR) << *this << " no default non-VPN device found"; | |
| return; | |
| } | |
| network_config = &dd->network_config; | |
| } | |
| std::vector<net_base::IPv4Address> ipv4_nameservers; | |
| std::vector<net_base::IPv6Address> ipv6_nameservers; | |
| for (const auto& addr : network_config->dns_servers) { | |
| switch (addr.GetFamily()) { | |
| case net_base::IPFamily::kIPv4: | |
| ipv4_nameservers.push_back(*addr.ToIPv4Address()); | |
| break; | |
| case net_base::IPFamily::kIPv6: | |
| ipv6_nameservers.push_back(*addr.ToIPv6Address()); | |
| break; | |
| } | |
| } | |
| if (ipv4_nameservers.empty() && ipv6_nameservers.empty()) { | |
| LOG(WARNING) << *this << " has empty name servers"; | |
| } | |
| doh_config_.set_nameservers(ipv4_nameservers, ipv6_nameservers); | |
| metrics_.RecordNameservers(doh_config_.ipv4_nameservers().size(), | |
| doh_config_.ipv6_nameservers().size()); | |
| LOG(INFO) << *this << " applied device DNS configuration"; | |
| } | |
| void Proxy::OnDoHProvidersChanged(const brillo::Any& value) { | |
| doh_config_.set_providers(value.Get<brillo::VariantDictionary>()); | |
| } | |
| void Proxy::SetShillDNSProxyAddresses( | |
| const std::optional<net_base::IPv4Address>& ipv4_addr, | |
| const std::optional<net_base::IPv6Address>& ipv6_addr, | |
| bool die_on_failure, | |
| uint8_t num_retries) { | |
| if (opts_.type != Type::kSystem) { | |
| LOG(DFATAL) << *this << " " << __func__ | |
| << " must be called from system proxy only"; | |
| return; | |
| } | |
| if (num_retries == 0) { | |
| metrics_.RecordProcessEvent( | |
| metrics_proc_type_, | |
| Metrics::ProcessEvent::kShillSetProxyAddressRetryExceeded); | |
| LOG(ERROR) << *this << " Maximum number of retries exceeding attempt to" | |
| << " set dns-proxy address property on shill"; | |
| if (die_on_failure) | |
| QuitWithExitCode(EX_UNAVAILABLE); | |
| return; | |
| } | |
| // If doesn't ever come back, there is no point in retrying here; and | |
| // if it does, then initialization process will eventually come back | |
| // into this function and succeed. | |
| if (!shill_ready_) { | |
| LOG(WARNING) << *this | |
| << " No connection to shill - cannot set dns-proxy address " | |
| "property IPv4 [" | |
| << (ipv4_addr ? ipv4_addr->ToString() : "") << "], IPv6 [" | |
| << (ipv6_addr ? ipv6_addr->ToString() : "") << "]"; | |
| return; | |
| } | |
| std::vector<std::string> addrs; | |
| if (ipv4_addr && !doh_config_.ipv4_nameservers().empty()) { | |
| addrs.push_back(ipv4_addr->ToString()); | |
| } | |
| if (ipv6_addr && !doh_config_.ipv6_nameservers().empty()) { | |
| addrs.push_back(ipv6_addr->ToString()); | |
| } | |
| if (addrs.empty()) { | |
| shill_->GetManagerProxy()->ClearDNSProxyAddresses(/*error=*/nullptr); | |
| LOG(INFO) << *this << " Successfully cleared dns-proxy address property"; | |
| return; | |
| } | |
| brillo::ErrorPtr error; | |
| if (shill_->GetManagerProxy()->SetDNSProxyAddresses(addrs, &error)) { | |
| LOG(INFO) << *this << " Successfully set dns-proxy address property [" | |
| << base::JoinString(addrs, ",") << "]"; | |
| return; | |
| } | |
| LOG(ERROR) << *this << " Failed to set dns-proxy address property [" | |
| << base::JoinString(addrs, ",") | |
| << "] on shill: " << error->GetMessage() << ". Retrying..."; | |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( | |
| FROM_HERE, | |
| base::BindOnce(&Proxy::SetShillDNSProxyAddresses, | |
| weak_factory_.GetWeakPtr(), ipv4_addr, ipv6_addr, | |
| die_on_failure, num_retries - 1), | |
| kShillPropertyAttemptDelay); | |
| } | |
| void Proxy::ClearShillDNSProxyAddresses() { | |
| SetShillDNSProxyAddresses(/*ipv4_address=*/std::nullopt, | |
| /*ipv6_address=*/std::nullopt); | |
| } | |
| void Proxy::SendIPAddressesToController( | |
| const net_base::IPv4Address& ipv4_addr, | |
| const std::optional<net_base::IPv6Address>& ipv6_addr) { | |
| if (opts_.type != Type::kSystem) { | |
| LOG(DFATAL) << *this << " Must be called from system proxy only"; | |
| return; | |
| } | |
| ProxyAddrMessage msg; | |
| msg.set_type(ProxyAddrMessage::SET_ADDRS); | |
| if (!doh_config_.ipv4_nameservers().empty()) { | |
| msg.add_addrs(ipv4_addr.ToString()); | |
| } | |
| if (ipv6_addr && !doh_config_.ipv6_nameservers().empty()) { | |
| msg.add_addrs(ipv6_addr->ToString()); | |
| } | |
| // Don't send empty proxy address. | |
| if (msg.addrs().empty()) { | |
| return; | |
| } | |
| SendProtoMessage(msg); | |
| } | |
| void Proxy::ClearIPAddressesInController() { | |
| ProxyAddrMessage msg; | |
| msg.set_type(ProxyAddrMessage::CLEAR_ADDRS); | |
| SendProtoMessage(msg); | |
| } | |
| void Proxy::SendProtoMessage(const ProxyAddrMessage& msg) { | |
| if (msg_dispatcher_->SendMessage(msg)) { | |
| return; | |
| } | |
| LOG(ERROR) << *this << " Failed to set IP addresses to controller"; | |
| // This might be caused by the file descriptor getting invalidated. Quit the | |
| // process to let the controller restart the proxy. Restarting allows a new | |
| // clean state. | |
| Quit(); | |
| } | |
| const std::vector<net_base::IPv4Address>& Proxy::DoHConfig::ipv4_nameservers() { | |
| return ipv4_nameservers_; | |
| } | |
| const std::vector<net_base::IPv6Address>& Proxy::DoHConfig::ipv6_nameservers() { | |
| return ipv6_nameservers_; | |
| } | |
| void Proxy::DoHConfig::set_resolver(Resolver* resolver) { | |
| resolver_ = resolver; | |
| update(); | |
| } | |
| void Proxy::DoHConfig::set_nameservers( | |
| const std::vector<net_base::IPv4Address>& ipv4_nameservers, | |
| const std::vector<net_base::IPv6Address>& ipv6_nameservers) { | |
| ipv4_nameservers_ = ipv4_nameservers; | |
| ipv6_nameservers_ = ipv6_nameservers; | |
| update(); | |
| } | |
| void Proxy::DoHConfig::set_providers( | |
| const brillo::VariantDictionary& providers) { | |
| secure_providers_.clear(); | |
| secure_providers_with_fallback_.clear(); | |
| auto_providers_.clear(); | |
| if (providers.empty()) { | |
| if (metrics_) { | |
| metrics_->RecordDnsOverHttpsMode(Metrics::DnsOverHttpsMode::kOff); | |
| } | |
| LOG(INFO) << *this << " DoH: off"; | |
| update(); | |
| return; | |
| } | |
| for (const auto& [endpoint, value] : providers) { | |
| // We expect that in secure, always-on to find one (or more) endpoints with | |
| // no nameservers. | |
| const auto nameservers = value.TryGet<std::string>(""); | |
| if (nameservers.empty()) { | |
| secure_providers_.insert(TrimParamTemplate(endpoint)); | |
| continue; | |
| } | |
| // On secure DNS automatic mode with fallback, we expect a wildcard | |
| // nameserver ("*"). See also b/333757554. | |
| if (nameservers == shill::kDNSProxyDOHProvidersMatchAnyIPAddress) { | |
| secure_providers_with_fallback_.insert(TrimParamTemplate(endpoint)); | |
| continue; | |
| } | |
| // Remap nameserver -> secure endpoint so we can quickly determine if DoH | |
| // should be attempted when the name servers change. | |
| for (const auto& ns_str : | |
| base::SplitString(nameservers, ",", base::TRIM_WHITESPACE, | |
| base::SPLIT_WANT_NONEMPTY)) { | |
| const auto ns = net_base::IPAddress::CreateFromString(ns_str); | |
| if (ns) { | |
| auto_providers_[*ns] = TrimParamTemplate(endpoint); | |
| } else { | |
| LOG(WARNING) << "Invalid nameserver string: " << ns_str; | |
| } | |
| } | |
| } | |
| // If for some reason, both collections are non-empty, prefer the automatic | |
| // upgrade configuration or the secure DNS with fallback configuration. | |
| if (!secure_providers_with_fallback_.empty() || !auto_providers_.empty()) { | |
| secure_providers_.clear(); | |
| if (metrics_) { | |
| metrics_->RecordDnsOverHttpsMode(Metrics::DnsOverHttpsMode::kAutomatic); | |
| } | |
| LOG(INFO) << *this << " DoH: automatic"; | |
| } | |
| if (!secure_providers_.empty()) { | |
| if (metrics_) { | |
| metrics_->RecordDnsOverHttpsMode(Metrics::DnsOverHttpsMode::kAlwaysOn); | |
| } | |
| LOG(INFO) << *this << " DoH: always-on"; | |
| } | |
| update(); | |
| } | |
| void Proxy::DoHConfig::update() { | |
| if (!resolver_) | |
| return; | |
| std::vector<net_base::IPAddress> nameservers; | |
| for (const auto& ipv4_nameservers : ipv4_nameservers_) { | |
| nameservers.push_back(net_base::IPAddress(ipv4_nameservers)); | |
| } | |
| for (const auto& ipv6_nameservers : ipv6_nameservers_) { | |
| nameservers.push_back(net_base::IPAddress(ipv6_nameservers)); | |
| } | |
| resolver_->SetNameServers(ToStringVec(nameservers)); | |
| std::set<std::string> doh_providers; | |
| bool doh_always_on = false; | |
| if (!secure_providers_.empty()) { | |
| doh_providers = secure_providers_; | |
| doh_always_on = true; | |
| } else if (!secure_providers_with_fallback_.empty()) { | |
| doh_providers = secure_providers_with_fallback_; | |
| } else if (!auto_providers_.empty()) { | |
| for (const auto& ns : nameservers) { | |
| const auto it = auto_providers_.find(ns); | |
| if (it != auto_providers_.end()) { | |
| doh_providers.emplace(it->second); | |
| } | |
| } | |
| } | |
| resolver_->SetDoHProviders( | |
| std::vector(doh_providers.begin(), doh_providers.end()), doh_always_on); | |
| } | |
| void Proxy::DoHConfig::clear() { | |
| resolver_ = nullptr; | |
| secure_providers_.clear(); | |
| secure_providers_with_fallback_.clear(); | |
| auto_providers_.clear(); | |
| } | |
| void Proxy::DoHConfig::set_metrics(Metrics* metrics) { | |
| metrics_ = metrics; | |
| } | |
| void Proxy::DoHConfig::set_logger(Proxy::Logger logger) { | |
| logger_ = std::move(logger); | |
| } | |
| void Proxy::RTNLMessageHandler(const net_base::RTNLMessage& msg) { | |
| // Listen only for global or site-local IPv6 address changes. | |
| if (msg.address_status().scope != RT_SCOPE_UNIVERSE && | |
| msg.address_status().scope != RT_SCOPE_SITE) { | |
| return; | |
| } | |
| // Listen only for the peer interface IPv6 changes. | |
| if (msg.interface_index() != if_nametoindex(ns_.peer_ifname.c_str())) { | |
| return; | |
| } | |
| switch (msg.mode()) { | |
| case net_base::RTNLMessage::kModeAdd: { | |
| const auto ifa_addr = msg.GetAddress(); | |
| if (!ifa_addr || ifa_addr->GetFamily() != net_base::IPFamily::kIPv6) { | |
| LOG(ERROR) << *this << " RTNL message does not have valid IPv6 address"; | |
| return; | |
| } | |
| const auto peer_ipv6_addr = ifa_addr->ToIPv6CIDR()->address(); | |
| if (ns_peer_ipv6_address_ == peer_ipv6_addr) { | |
| return; | |
| } | |
| ns_peer_ipv6_address_ = peer_ipv6_addr; | |
| if (opts_.type == Type::kDefault && device_) { | |
| StartDnsRedirection(/*ifname=*/"", AF_INET6, | |
| ToStringVec(doh_config_.ipv6_nameservers())); | |
| } | |
| for (const auto& d : patchpanel_->GetDevices()) { | |
| StartGuestDnsRedirection(d, AF_INET6); | |
| } | |
| if (opts_.type == Type::kSystem && device_) { | |
| SetShillDNSProxyAddresses(ns_.peer_ipv4_address, ns_peer_ipv6_address_); | |
| SendIPAddressesToController(ns_.peer_ipv4_address, | |
| ns_peer_ipv6_address_); | |
| StartDnsRedirection(/*ifname=*/"", AF_INET6); | |
| } | |
| return; | |
| } | |
| case net_base::RTNLMessage::kModeDelete: | |
| ns_peer_ipv6_address_ = std::nullopt; | |
| if (opts_.type == Type::kDefault) { | |
| StopDnsRedirection(/*ifname=*/"", AF_INET6); | |
| } | |
| for (const auto& d : patchpanel_->GetDevices()) { | |
| StopGuestDnsRedirection(d, AF_INET6); | |
| } | |
| if (opts_.type == Type::kSystem && device_) { | |
| SetShillDNSProxyAddresses(/*ipv4_addr=*/ns_.peer_ipv4_address, | |
| /*ipv6_addr=*/std::nullopt); | |
| SendIPAddressesToController(/*ipv4_addr=*/ns_.peer_ipv4_address, | |
| /*ipv6_addr=*/std::nullopt); | |
| StopDnsRedirection(/*ifname=*/"", AF_INET6); | |
| } | |
| return; | |
| default: | |
| return; | |
| } | |
| } | |
| void Proxy::OnVirtualDeviceChanged( | |
| patchpanel::Client::VirtualDeviceEvent event, | |
| const patchpanel::Client::VirtualDevice& device) { | |
| switch (event) { | |
| case patchpanel::Client::VirtualDeviceEvent::kAdded: | |
| StartGuestDnsRedirection(device, AF_INET); | |
| StartGuestDnsRedirection(device, AF_INET6); | |
| break; | |
| case patchpanel::Client::VirtualDeviceEvent::kRemoved: | |
| StopGuestDnsRedirection(device, AF_INET); | |
| StopGuestDnsRedirection(device, AF_INET6); | |
| break; | |
| default: | |
| NOTREACHED(); | |
| } | |
| } | |
| void Proxy::StartGuestDnsRedirection( | |
| const patchpanel::Client::VirtualDevice& device, sa_family_t sa_family) { | |
| if (!device_ || | |
| base::Contains(lifeline_fds_, std::make_pair(device.ifname, sa_family))) { | |
| return; | |
| } | |
| switch (device.guest_type) { | |
| case patchpanel::Client::GuestType::kTerminaVm: | |
| case patchpanel::Client::GuestType::kParallelsVm: | |
| if (opts_.type == Type::kDefault) { | |
| StartDnsRedirection(device.ifname, sa_family); | |
| } | |
| return; | |
| case patchpanel::Client::GuestType::kArcContainer: | |
| case patchpanel::Client::GuestType::kArcVm: | |
| // b/273741099: For multiplexed Cellular interfaces, patchpanel is | |
| // responsible for advertasing the shill's Device kInterfaceProperty | |
| // instead of the primary multiplexed interface, and consumers of | |
| // patchpanel::VirtualDevice are expected to use the shill's Device | |
| // kInterfaceProperty. | |
| // TODO(b/273744897): Change this checks to compare the Network id | |
| // associated with the shill's Device (primary Network) once patchpanel | |
| // Network ids are available and once dnsproxy uses the patchpanel | |
| // Network id. | |
| if (opts_.type == Type::kARC && opts_.ifname == device.phys_ifname) { | |
| // TODO(b/273744897): Use the patchpanel Network id of the shill Device | |
| // that this Proxy is associated to. Until the Network id is available, | |
| // using the shill's Device kInterfaceProperty is consistent with | |
| // patchpanel's tracking of shill's Devices. For multiplexed Cellular | |
| // interfaces, patchpanel is responsible for using the correct | |
| // multiplexed network interface. Callers of RedirectDNS are expected to | |
| // use the shill's Device kInterfaceProperty. | |
| StartDnsRedirection(device.ifname, sa_family); | |
| } | |
| return; | |
| default: | |
| return; | |
| } | |
| } | |
| void Proxy::StopGuestDnsRedirection( | |
| const patchpanel::Client::VirtualDevice& device, sa_family_t sa_family) { | |
| switch (device.guest_type) { | |
| case patchpanel::Client::GuestType::kTerminaVm: | |
| case patchpanel::Client::GuestType::kParallelsVm: | |
| if (opts_.type == Type::kDefault) { | |
| StopDnsRedirection(device.ifname, sa_family); | |
| } | |
| return; | |
| default: | |
| // For ARC, upon removal of the virtual device, the corresponding proxy | |
| // will also be removed. This will undo the created firewall rules. | |
| // However, if IPv6 is removed, firewall rules created need to be | |
| // removed. | |
| // TODO(b/273744897): Change this checks to compare the Network id | |
| // associated with the shill's Device (primary Network) once patchpanel | |
| // Network ids are available and once dnsproxy uses the patchpanel | |
| // Network id. | |
| if (opts_.type == Type::kARC && opts_.ifname == device.phys_ifname) { | |
| StopDnsRedirection(device.ifname, sa_family); | |
| } | |
| return; | |
| } | |
| } | |
| void Proxy::LogName(std::ostream& stream) const { | |
| stream << *this; | |
| } | |
| } // namespace dns_proxy |