| // 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/controller.h" | |
| #include <sys/capability.h> | |
| #include <sys/prctl.h> | |
| #include <sysexits.h> | |
| #include <set> | |
| #include <utility> | |
| #include <vector> | |
| #include <base/check.h> | |
| #include <base/files/file_path.h> | |
| #include <base/files/file_util.h> | |
| #include <base/functional/bind.h> | |
| #include <base/logging.h> | |
| #include <base/notreached.h> | |
| #include <base/process/launch.h> | |
| #include <base/task/single_thread_task_runner.h> | |
| #include <base/time/time.h> | |
| #include <chromeos/scoped_minijail.h> | |
| #include <chromeos/patchpanel/message_dispatcher.h> | |
| #include <shill/dbus-constants.h> | |
| #include "dns-proxy/ipc.pb.h" | |
| #include "dns-proxy/proxy.h" | |
| namespace dns_proxy { | |
| namespace { | |
| constexpr base::TimeDelta kSubprocessRestartDelay = base::Milliseconds(900); | |
| constexpr base::TimeDelta kSubprocessMaxWaitTime = base::Seconds(3); | |
| constexpr base::TimeDelta kSubprocessWaitSleepTime = base::Milliseconds(100); | |
| constexpr char kSeccompPolicyPath[] = | |
| "/usr/share/policy/dns-proxy-seccomp.policy"; | |
| constexpr char kResolvConfRunPath[] = "/run/dns-proxy/resolv.conf"; | |
| // Loops until all child processes are stopped or there is an error. This | |
| // function is safe to call even if |pids| contains an already stopped children | |
| // as long as waitpid is not previously called for the pid. | |
| bool WaitForChildren(std::set<pid_t> pids) { | |
| base::TimeTicks deadline = base::TimeTicks::Now() + kSubprocessMaxWaitTime; | |
| while (base::TimeTicks::Now() < deadline) { | |
| int status; | |
| pid_t pid = HANDLE_EINTR(waitpid(0, &status, WNOHANG)); | |
| if (pid == -1) { | |
| if (errno == ECHILD) { | |
| return true; | |
| } | |
| PLOG(ERROR) << "Unable to find child processes"; | |
| return false; | |
| } | |
| if (pid == 0) { | |
| base::PlatformThread::Sleep(kSubprocessWaitSleepTime); | |
| continue; | |
| } | |
| // Log child process exit status. | |
| if (WIFEXITED(status)) { | |
| LOG(INFO) << "Process " << pid << " exited with status " | |
| << WEXITSTATUS(status); | |
| } else if (WIFSIGNALED(status)) { | |
| LOG(INFO) << "Process " << pid << " killed by signal " << WTERMSIG(status) | |
| << (WCOREDUMP(status) ? " (core dumped)" : ""); | |
| } else { | |
| LOG(WARNING) << "Unknown exit status " << status << " for process " | |
| << pid; | |
| } | |
| // Wait until all child processes exit. | |
| pids.erase(pid); | |
| if (pids.empty()) { | |
| return true; | |
| } | |
| } | |
| LOG(WARNING) << "Reached maximum wait time before all child processes exit"; | |
| return false; | |
| } | |
| } // namespace | |
| Controller::Controller(const std::string& progname) | |
| : progname_(progname), resolv_conf_(new ResolvConf()) {} | |
| // This ctor is only used for testing. | |
| Controller::Controller(std::unique_ptr<ResolvConf> resolv_conf) | |
| : resolv_conf_(std::move(resolv_conf)) {} | |
| int Controller::OnInit() { | |
| LOG(INFO) << "Starting DNS Proxy service"; | |
| // Set run path for resolv.conf. | |
| resolv_conf_->set_path(base::FilePath(kResolvConfRunPath)); | |
| // Preserve CAP_NET_BIND_SERVICE so the child processes have the capability. | |
| // Without the ambient set, file capabilities need to be used. | |
| if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_NET_BIND_SERVICE, 0, 0) != | |
| 0) { | |
| metrics_.RecordProcessEvent(Metrics::ProcessType::kController, | |
| Metrics::ProcessEvent::kCapNetBindServiceError); | |
| LOG(ERROR) << "Failed to add CAP_NET_BIND_SERVICE to the ambient set"; | |
| } | |
| // Handle subprocess lifecycle. | |
| process_reaper_.Register(this); | |
| /// Run after Daemon::OnInit() | |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( | |
| FROM_HERE, | |
| base::BindOnce(&Controller::Setup, weak_factory_.GetWeakPtr())); | |
| return DBusDaemon::OnInit(); | |
| } | |
| void Controller::OnShutdown(int* code) { | |
| LOG(INFO) << "Stopping DNS Proxy service (" << *code << ")"; | |
| std::set<pid_t> pids = {}; | |
| for (const auto& p : proxies_) { | |
| pids.emplace(p.pid); | |
| Kill(p); | |
| } | |
| if (!WaitForChildren(pids)) { | |
| LOG(WARNING) << "Failed to wait for all child processes to stop"; | |
| } else { | |
| LOG(INFO) << "Stopped all child processes properly"; | |
| } | |
| is_shutdown_ = true; | |
| } | |
| void Controller::Setup() { | |
| features_ = ChromeFeaturesServiceClient::New(bus_); | |
| if (features_) { | |
| features_->IsDNSProxyEnabled(base::BindOnce(&Controller::OnFeatureEnabled, | |
| weak_factory_.GetWeakPtr())); | |
| } else { | |
| LOG(ERROR) << "Failed to initialize Chrome features client - " | |
| << "service will be enabled by default"; | |
| feature_enabled_.emplace(true); | |
| } | |
| patchpanel_ = patchpanel::Client::New(bus_); | |
| if (!patchpanel_) { | |
| metrics_.RecordProcessEvent( | |
| Metrics::ProcessType::kController, | |
| Metrics::ProcessEvent::kPatchpanelNotInitialized); | |
| LOG(ERROR) << "Failed to initialize patchpanel client"; | |
| QuitWithExitCode(EX_UNAVAILABLE); | |
| return; | |
| } | |
| patchpanel_->RegisterOnAvailableCallback(base::BindOnce( | |
| &Controller::OnPatchpanelReady, weak_factory_.GetWeakPtr())); | |
| patchpanel_->RegisterProcessChangedCallback(base::BindRepeating( | |
| &Controller::OnPatchpanelReset, weak_factory_.GetWeakPtr())); | |
| shill_.reset(new shill::Client(bus_)); | |
| shill_->RegisterProcessChangedHandler(base::BindRepeating( | |
| &Controller::OnShillReset, weak_factory_.GetWeakPtr())); | |
| shill_->RegisterOnAvailableCallback( | |
| base::BindOnce(&Controller::OnShillReady, weak_factory_.GetWeakPtr())); | |
| RunProxy(Proxy::Type::kSystem); | |
| RunProxy(Proxy::Type::kDefault); | |
| } | |
| void Controller::OnFeatureEnabled(std::optional<bool> enabled) { | |
| // Avoid starting child processes when the controller is shut down. | |
| if (is_shutdown_) { | |
| return; | |
| } | |
| if (!enabled.has_value()) { | |
| LOG(ERROR) << "Failed to read feature flag - " | |
| << "service will be enabled by default"; | |
| feature_enabled_.emplace(true); | |
| } else { | |
| feature_enabled_.emplace(enabled.value()); | |
| LOG(INFO) << "Service " | |
| << (feature_enabled_.value() ? "enabled" : "disabled") | |
| << " by feature flag"; | |
| } | |
| } | |
| void Controller::OnPatchpanelReady(bool success) { | |
| if (!success) { | |
| metrics_.RecordProcessEvent(Metrics::ProcessType::kController, | |
| Metrics::ProcessEvent::kPatchpanelNotReady); | |
| LOG(ERROR) << "Failed to connect to patchpanel"; | |
| QuitWithExitCode(EX_UNAVAILABLE); | |
| return; | |
| } | |
| patchpanel_->RegisterVirtualDeviceEventHandler(base::BindRepeating( | |
| &Controller::OnVirtualDeviceChanged, weak_factory_.GetWeakPtr())); | |
| // Process the current set of patchpanel devices and launch any required | |
| // proxy processes. | |
| for (const auto& d : patchpanel_->GetDevices()) | |
| VirtualDeviceAdded(d); | |
| } | |
| void Controller::OnPatchpanelReset(bool reset) { | |
| if (reset) { | |
| LOG(WARNING) << "Patchpanel has been reset"; | |
| return; | |
| } | |
| // If patchpanel crashes, the proxies will be restarted, so just create a new | |
| // client and continue on. | |
| metrics_.RecordProcessEvent(Metrics::ProcessType::kController, | |
| Metrics::ProcessEvent::kPatchpanelShutdown); | |
| LOG(ERROR) << "Patchpanel has been shutdown - reconnecting..."; | |
| } | |
| void Controller::OnShillReady(bool success) { | |
| shill_ready_ = success; | |
| if (!shill_ready_) { | |
| metrics_.RecordProcessEvent(Metrics::ProcessType::kController, | |
| Metrics::ProcessEvent::kShillNotReady); | |
| LOG(DFATAL) << "Failed to connect to shill"; | |
| return; | |
| } | |
| shill_->RegisterDefaultDeviceChangedHandler(base::BindRepeating( | |
| &Controller::OnDefaultDeviceChanged, weak_factory_.GetWeakPtr())); | |
| } | |
| void Controller::OnShillReset(bool reset) { | |
| if (reset) { | |
| LOG(WARNING) << "Shill has been reset"; | |
| return; | |
| } | |
| LOG(WARNING) << "Shill has been shutdown"; | |
| shill_ready_ = false; | |
| // Listen for it to come back. | |
| shill_->RegisterOnAvailableCallback( | |
| base::BindOnce(&Controller::OnShillReady, weak_factory_.GetWeakPtr())); | |
| } | |
| void Controller::RunProxy(Proxy::Type type, const std::string& ifname) { | |
| if (!feature_enabled_.has_value()) { | |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( | |
| FROM_HERE, base::BindOnce(&Controller::RunProxy, | |
| weak_factory_.GetWeakPtr(), type, ifname)); | |
| return; | |
| } | |
| if (!feature_enabled_.value()) { | |
| return; | |
| } | |
| ProxyProc proc(type, ifname); | |
| const auto& it = restarts_.find(proc); | |
| if (it != restarts_.end() && !it->second.is_valid()) { | |
| LOG(ERROR) << "Not running blocked proxy " << proc; | |
| return; | |
| } | |
| if (proxies_.find(proc) != proxies_.end()) { | |
| return; | |
| } | |
| ScopedMinijail jail(minijail_new()); | |
| minijail_namespace_net(jail.get()); | |
| minijail_no_new_privs(jail.get()); | |
| minijail_use_seccomp_filter(jail.get()); | |
| minijail_parse_seccomp_filters(jail.get(), kSeccompPolicyPath); | |
| minijail_forward_signals(jail.get()); | |
| minijail_reset_signal_mask(jail.get()); | |
| minijail_reset_signal_handlers(jail.get()); | |
| minijail_run_as_init(jail.get()); | |
| // Create FDs to communicate to the proxy. | |
| base::ScopedFD controller_fd, proxy_fd; | |
| if (type == Proxy::Type::kSystem) { | |
| int control[2]; | |
| if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, control) != 0) { | |
| PLOG(ERROR) << "Failed to start system proxy. socketpair failed"; | |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( | |
| FROM_HERE, base::BindOnce(&Controller::RunProxy, | |
| weak_factory_.GetWeakPtr(), type, ifname)); | |
| return; | |
| } | |
| controller_fd.reset(control[0]); | |
| proxy_fd.reset(control[1]); | |
| } | |
| std::vector<char*> argv; | |
| const std::string flag_t = "--t=" + std::string(Proxy::TypeToString(type)); | |
| argv.push_back(const_cast<char*>(progname_.c_str())); | |
| argv.push_back(const_cast<char*>(flag_t.c_str())); | |
| std::string flag_i = "--i="; | |
| if (!ifname.empty()) { | |
| flag_i += ifname; | |
| argv.push_back(const_cast<char*>(flag_i.c_str())); | |
| } | |
| if (type == Proxy::Type::kSystem && controller_fd.is_valid() && | |
| proxy_fd.is_valid()) { | |
| const std::string flag_fd = "--fd=" + std::to_string(proxy_fd.get()); | |
| argv.push_back(const_cast<char*>(flag_fd.c_str())); | |
| } | |
| argv.push_back(nullptr); | |
| pid_t pid; | |
| if (minijail_run_pid(jail.get(), argv[0], argv.data(), &pid) != 0) { | |
| metrics_.RecordProcessEvent(Metrics::ProcessType::kController, | |
| Metrics::ProcessEvent::kProxyLaunchFailure); | |
| LOG(DFATAL) << "Failed to launch process for proxy " << proc; | |
| return; | |
| } | |
| proc.pid = pid; | |
| metrics_.RecordProcessEvent(Metrics::ProcessType::kController, | |
| Metrics::ProcessEvent::kProxyLaunchSuccess); | |
| LOG(INFO) << "Launched process for proxy " << proc; | |
| if (!process_reaper_.WatchForChild( | |
| FROM_HERE, pid, | |
| base::BindOnce(&Controller::OnProxyExit, weak_factory_.GetWeakPtr(), | |
| pid))) { | |
| LOG(ERROR) << "Failed to watch process for proxy " << proc | |
| << " - did it crash after launch?"; | |
| return; | |
| } | |
| if (type == Proxy::Type::kSystem) { | |
| msg_dispatcher_ = | |
| std::make_unique<patchpanel::MessageDispatcher<ProxyAddrMessage>>( | |
| std::move(controller_fd)); | |
| msg_dispatcher_->RegisterFailureHandler(base::BindRepeating( | |
| &Controller::OnProxyAddrMessageFailure, weak_factory_.GetWeakPtr())); | |
| msg_dispatcher_->RegisterMessageHandler(base::BindRepeating( | |
| &Controller::OnProxyAddrMessage, weak_factory_.GetWeakPtr())); | |
| } | |
| proxies_.emplace(proc); | |
| } | |
| void Controller::OnProxyAddrMessageFailure() { | |
| msg_dispatcher_.reset(); | |
| KillProxy(Proxy::Type::kSystem, /*ifname=*/"", /*forget=*/false); | |
| } | |
| void Controller::OnProxyAddrMessage(const ProxyAddrMessage& msg) { | |
| switch (msg.type()) { | |
| case ProxyAddrMessage::SET_ADDRS: | |
| resolv_conf_->SetDNSProxyAddresses( | |
| std::vector<std::string>(msg.addrs().begin(), msg.addrs().end())); | |
| break; | |
| case ProxyAddrMessage::CLEAR_ADDRS: | |
| resolv_conf_->SetDNSProxyAddresses({}); | |
| break; | |
| default: | |
| NOTREACHED(); | |
| } | |
| } | |
| void Controller::KillProxy(Proxy::Type type, | |
| const std::string& ifname, | |
| bool forget) { | |
| auto it = proxies_.find(ProxyProc(type, ifname)); | |
| if (it == proxies_.end()) { | |
| return; | |
| } | |
| Kill(*it, forget); | |
| if (!forget) { | |
| return; | |
| } | |
| proxies_.erase(it); | |
| restarts_.erase(*it); | |
| } | |
| void Controller::Kill(const ProxyProc& proc, bool forget) { | |
| EvalProxyExit(proc); | |
| if (forget) { | |
| process_reaper_.ForgetChild(proc.pid); | |
| } | |
| int rc = kill(proc.pid, SIGTERM); | |
| if (rc < 0 && rc != ESRCH) { | |
| metrics_.RecordProcessEvent(Metrics::ProcessType::kController, | |
| Metrics::ProcessEvent::kProxyKillFailure); | |
| LOG(ERROR) << "Failed to kill process for proxy " << proc; | |
| } | |
| } | |
| void Controller::OnProxyExit(pid_t pid, const siginfo_t& siginfo) { | |
| process_reaper_.ForgetChild(pid); | |
| // There will only ever be a handful of entries in this map so a linear scan | |
| // will be trivial. | |
| ProxyProc proc; | |
| bool found = false; | |
| for (auto it = proxies_.begin(); it != proxies_.end(); ++it) { | |
| if (it->pid == pid) { | |
| proc = *it; | |
| proxies_.erase(it); | |
| found = true; | |
| break; | |
| } | |
| } | |
| if (!found) { | |
| metrics_.RecordProcessEvent(Metrics::ProcessType::kController, | |
| Metrics::ProcessEvent::kProxyMissing); | |
| LOG(ERROR) << "Unexpected process (" << pid << ") exit signal received"; | |
| return; | |
| } | |
| EvalProxyExit(proc); | |
| switch (siginfo.si_code) { | |
| case CLD_EXITED: | |
| case CLD_DUMPED: | |
| case CLD_KILLED: | |
| case CLD_TRAPPED: | |
| metrics_.RecordProcessEvent(Metrics::ProcessType::kController, | |
| Metrics::ProcessEvent::kProxyKilled); | |
| LOG(ERROR) << "Process for proxy [" << proc | |
| << " was unexpectedly killed (" << siginfo.si_code << ":" | |
| << siginfo.si_status << ") - " | |
| << (RestartProxy(proc) ? "attempting to restart" | |
| : "restart attempts exceeded"); | |
| break; | |
| case CLD_STOPPED: | |
| metrics_.RecordProcessEvent(Metrics::ProcessType::kController, | |
| Metrics::ProcessEvent::kProxyStopped); | |
| LOG(WARNING) << "Process for proxy " << proc | |
| << " was unexpectedly stopped"; | |
| break; | |
| case CLD_CONTINUED: | |
| metrics_.RecordProcessEvent(Metrics::ProcessType::kController, | |
| Metrics::ProcessEvent::kProxyContinued); | |
| LOG(WARNING) << "Process for proxy " << proc << " has continued"; | |
| break; | |
| default: | |
| NOTREACHED(); | |
| } | |
| } | |
| bool Controller::RestartProxy(const ProxyProc& proc) { | |
| auto it = restarts_.find(proc); | |
| if (it == restarts_.end()) { | |
| // First time the process has been restarted. | |
| restarts_.emplace(proc, ProxyRestarts()); | |
| } else if (!it->second.try_next()) { | |
| return false; | |
| } | |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( | |
| FROM_HERE, | |
| base::BindOnce(&Controller::RunProxy, weak_factory_.GetWeakPtr(), | |
| proc.opts.type, proc.opts.ifname), | |
| kSubprocessRestartDelay); | |
| return true; | |
| } | |
| void Controller::EvalProxyExit(const ProxyProc& proc) { | |
| if (proc.opts.type != Proxy::Type::kSystem) | |
| return; | |
| // Ensure the system proxy address is cleared from shill. | |
| if (!shill_ready_) { | |
| LOG(WARNING) << "Cannot clear shill dns-property for " << proc | |
| << " - shill is not connected"; | |
| return; | |
| } | |
| shill_->GetManagerProxy()->ClearDNSProxyAddresses(nullptr /* error */); | |
| resolv_conf_->SetDNSProxyAddresses({}); | |
| // Cleanup fd between the proxy and controller. | |
| msg_dispatcher_.reset(); | |
| } | |
| void Controller::OnVirtualDeviceChanged( | |
| patchpanel::Client::VirtualDeviceEvent event, | |
| const patchpanel::Client::VirtualDevice& device) { | |
| switch (event) { | |
| case patchpanel::Client::VirtualDeviceEvent::kAdded: | |
| VirtualDeviceAdded(device); | |
| break; | |
| case patchpanel::Client::VirtualDeviceEvent::kRemoved: | |
| VirtualDeviceRemoved(device); | |
| break; | |
| default: | |
| NOTREACHED(); | |
| } | |
| } | |
| void Controller::VirtualDeviceAdded( | |
| const patchpanel::Client::VirtualDevice& device) { | |
| if (patchpanel::Client::IsArcGuest(device.guest_type)) { | |
| RunProxy(Proxy::Type::kARC, device.phys_ifname); | |
| } | |
| } | |
| void Controller::VirtualDeviceRemoved( | |
| const patchpanel::Client::VirtualDevice& device) { | |
| if (patchpanel::Client::IsArcGuest(device.guest_type)) { | |
| KillProxy(Proxy::Type::kARC, device.phys_ifname); | |
| } | |
| } | |
| void Controller::OnDefaultDeviceChanged( | |
| const shill::Client::Device* const device) { | |
| // Default service is either not ready yet or has just disconnected. | |
| if (!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) << "No default non-VPN device found"; | |
| return; | |
| } | |
| network_config = &dd->network_config; | |
| } | |
| resolv_conf_->SetDNSFromLists(network_config->dns_servers, | |
| network_config->dns_search_domains); | |
| } | |
| } // namespace dns_proxy |