Garrick Evans | 066dc2c | 2020-12-10 10:43:55 +0900 | [diff] [blame] | 1 | // Copyright 2021 The Chromium OS Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | #include "dns-proxy/controller.h" |
| 6 | |
Jason Jeremy Iman | f608042 | 2021-02-08 03:10:18 +0900 | [diff] [blame] | 7 | #include <sys/capability.h> |
| 8 | #include <sys/prctl.h> |
| 9 | |
Garrick Evans | 066dc2c | 2020-12-10 10:43:55 +0900 | [diff] [blame] | 10 | #include <vector> |
| 11 | |
| 12 | #include <base/bind.h> |
| 13 | #include "base/notreached.h" |
| 14 | #include <base/process/launch.h> |
| 15 | #include <base/threading/thread_task_runner_handle.h> |
| 16 | #include <chromeos/scoped_minijail.h> |
Garrick Evans | 48c84ef | 2021-01-28 11:29:42 +0900 | [diff] [blame] | 17 | #include <shill/dbus-constants.h> |
Garrick Evans | 066dc2c | 2020-12-10 10:43:55 +0900 | [diff] [blame] | 18 | |
| 19 | namespace dns_proxy { |
| 20 | namespace { |
| 21 | |
| 22 | constexpr int kSubprocessRestartDelayMs = 900; |
| 23 | constexpr char kSeccompPolicyPath[] = |
| 24 | "/usr/share/policy/dns-proxy-seccomp.policy"; |
| 25 | |
| 26 | } // namespace |
| 27 | |
| 28 | Controller::Controller(const std::string& progname) : progname_(progname) { |
| 29 | default_proxy_deps_ = std::make_unique<DefaultProxyDeps>(base::Bind( |
| 30 | &Controller::EvalDefaultProxyDeps, weak_factory_.GetWeakPtr())); |
| 31 | } |
| 32 | |
| 33 | Controller::~Controller() { |
| 34 | for (const auto& p : proxies_) |
| 35 | Kill(p); |
Garrick Evans | 9c8797d | 2021-02-17 21:28:10 +0900 | [diff] [blame] | 36 | |
| 37 | if (bus_) |
| 38 | bus_->ShutdownAndBlock(); |
Garrick Evans | 066dc2c | 2020-12-10 10:43:55 +0900 | [diff] [blame] | 39 | } |
| 40 | |
| 41 | int Controller::OnInit() { |
| 42 | LOG(INFO) << "Starting DNS Proxy service"; |
| 43 | |
Jason Jeremy Iman | f608042 | 2021-02-08 03:10:18 +0900 | [diff] [blame] | 44 | // Preserve CAP_NET_BIND_SERVICE so the child processes have the capability. |
| 45 | // Without the ambient set, file capabilities need to be used. |
| 46 | if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_NET_BIND_SERVICE, 0, 0) != |
| 47 | 0) { |
| 48 | LOG(ERROR) << "Failed to add CAP_NET_BIND_SERVICE to the ambient set"; |
| 49 | } |
| 50 | |
Garrick Evans | 066dc2c | 2020-12-10 10:43:55 +0900 | [diff] [blame] | 51 | // Handle subprocess lifecycle. |
| 52 | process_reaper_.Register(this); |
| 53 | |
| 54 | /// Run after Daemon::OnInit() |
| 55 | base::ThreadTaskRunnerHandle::Get()->PostTask( |
| 56 | FROM_HERE, base::Bind(&Controller::Setup, weak_factory_.GetWeakPtr())); |
| 57 | return DBusDaemon::OnInit(); |
| 58 | } |
| 59 | |
| 60 | void Controller::OnShutdown(int*) { |
| 61 | LOG(INFO) << "Stopping DNS Proxy service"; |
| 62 | } |
| 63 | |
| 64 | void Controller::Setup() { |
| 65 | shill_.reset(new shill::Client(bus_)); |
| 66 | shill_->Init(); |
Garrick Evans | 07c86d6 | 2021-02-18 09:31:42 +0900 | [diff] [blame^] | 67 | shill_->RegisterDefaultDeviceChangedHandler(base::BindRepeating( |
| 68 | &Controller::OnDefaultDeviceChanged, weak_factory_.GetWeakPtr())); |
Garrick Evans | 066dc2c | 2020-12-10 10:43:55 +0900 | [diff] [blame] | 69 | |
| 70 | patchpanel_ = patchpanel::Client::New(); |
| 71 | CHECK(patchpanel_) << "Failed to initialize patchpanel client"; |
| 72 | patchpanel_->RegisterOnAvailableCallback(base::BindRepeating( |
| 73 | &Controller::OnPatchpanelReady, weak_factory_.GetWeakPtr())); |
| 74 | |
| 75 | RunProxy(Proxy::Type::kSystem); |
| 76 | } |
| 77 | |
| 78 | void Controller::OnPatchpanelReady(bool success) { |
| 79 | CHECK(success) << "Failed to connect to patchpanel"; |
| 80 | patchpanel_->RegisterNetworkDeviceChangedSignalHandler(base::BindRepeating( |
| 81 | &Controller::OnVirtualDeviceChanged, weak_factory_.GetWeakPtr())); |
| 82 | |
| 83 | // Process the current set of patchpanel devices and launch any required |
| 84 | // proxy processes. |
| 85 | for (const auto& d : patchpanel_->GetDevices()) |
| 86 | VirtualDeviceAdded(d); |
| 87 | } |
| 88 | |
| 89 | void Controller::RunProxy(Proxy::Type type, const std::string& ifname) { |
| 90 | ProxyProc proc(type, ifname); |
| 91 | auto it = proxies_.find(proc); |
| 92 | if (it != proxies_.end()) { |
| 93 | return; |
| 94 | } |
| 95 | |
| 96 | ScopedMinijail jail(minijail_new()); |
| 97 | minijail_namespace_net(jail.get()); |
| 98 | minijail_no_new_privs(jail.get()); |
| 99 | minijail_use_seccomp_filter(jail.get()); |
| 100 | minijail_parse_seccomp_filters(jail.get(), kSeccompPolicyPath); |
| 101 | minijail_forward_signals(jail.get()); |
| 102 | minijail_reset_signal_mask(jail.get()); |
| 103 | minijail_reset_signal_handlers(jail.get()); |
| 104 | minijail_run_as_init(jail.get()); |
| 105 | |
| 106 | std::vector<char*> argv; |
| 107 | const std::string flag_t = "--t=" + std::string(Proxy::TypeToString(type)); |
| 108 | argv.push_back(const_cast<char*>(progname_.c_str())); |
| 109 | argv.push_back(const_cast<char*>(flag_t.c_str())); |
| 110 | if (!ifname.empty()) { |
| 111 | const std::string flag_if = "--i=" + ifname; |
| 112 | argv.push_back(const_cast<char*>(flag_if.c_str())); |
| 113 | } |
| 114 | argv.push_back(nullptr); |
| 115 | |
| 116 | pid_t pid; |
| 117 | if (minijail_run_pid(jail.get(), argv[0], argv.data(), &pid) != 0) { |
| 118 | LOG(DFATAL) << "Failed to launch process for proxy " << proc; |
| 119 | return; |
| 120 | } |
| 121 | proc.pid = pid; |
| 122 | LOG(INFO) << "Launched process for proxy " << proc; |
| 123 | |
| 124 | if (!process_reaper_.WatchForChild( |
| 125 | FROM_HERE, pid, |
| 126 | base::Bind(&Controller::OnProxyExit, weak_factory_.GetWeakPtr(), |
| 127 | pid))) { |
| 128 | LOG(ERROR) << "Failed to watch process for proxy " << proc |
| 129 | << " - did it crash after launch?"; |
| 130 | return; |
| 131 | } |
| 132 | |
| 133 | proxies_.emplace(proc); |
| 134 | } |
| 135 | |
| 136 | void Controller::KillProxy(Proxy::Type type, const std::string& ifname) { |
| 137 | auto it = proxies_.find(ProxyProc(type, ifname)); |
| 138 | if (it != proxies_.end()) { |
| 139 | Kill(*it); |
| 140 | proxies_.erase(it); |
| 141 | } |
| 142 | } |
| 143 | |
| 144 | void Controller::Kill(const ProxyProc& proc) { |
Garrick Evans | 48c84ef | 2021-01-28 11:29:42 +0900 | [diff] [blame] | 145 | EvalProxyExit(proc); |
Garrick Evans | 066dc2c | 2020-12-10 10:43:55 +0900 | [diff] [blame] | 146 | process_reaper_.ForgetChild(proc.pid); |
| 147 | int rc = kill(proc.pid, SIGTERM); |
| 148 | if (rc < 0 && rc != ESRCH) |
| 149 | LOG(ERROR) << "Failed to kill process for proxy " << proc; |
| 150 | } |
| 151 | |
| 152 | void Controller::OnProxyExit(pid_t pid, const siginfo_t& siginfo) { |
| 153 | process_reaper_.ForgetChild(pid); |
| 154 | |
| 155 | // There will only ever be a handful of entries in this map so a linear scan |
| 156 | // will be trivial. |
| 157 | ProxyProc proc; |
| 158 | bool found = false; |
| 159 | for (auto it = proxies_.begin(); it != proxies_.end(); ++it) { |
| 160 | if (it->pid == pid) { |
| 161 | proc = *it; |
| 162 | proxies_.erase(it); |
| 163 | found = true; |
| 164 | break; |
| 165 | } |
| 166 | } |
| 167 | if (!found) { |
| 168 | LOG(ERROR) << "Unexpected process (" << pid << ") exit signal received"; |
| 169 | return; |
| 170 | } |
| 171 | |
Garrick Evans | 48c84ef | 2021-01-28 11:29:42 +0900 | [diff] [blame] | 172 | EvalProxyExit(proc); |
| 173 | |
Garrick Evans | 066dc2c | 2020-12-10 10:43:55 +0900 | [diff] [blame] | 174 | switch (siginfo.si_code) { |
| 175 | case CLD_EXITED: |
| 176 | case CLD_DUMPED: |
| 177 | case CLD_KILLED: |
| 178 | case CLD_TRAPPED: |
| 179 | LOG(ERROR) << "Process for proxy [" << proc |
| 180 | << " was unexpectedly killed (" << siginfo.si_code << ":" |
| 181 | << siginfo.si_status << ") - attempting to restart"; |
| 182 | |
| 183 | base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| 184 | FROM_HERE, |
| 185 | base::Bind(&Controller::RunProxy, weak_factory_.GetWeakPtr(), |
| 186 | proc.opts.type, proc.opts.ifname), |
| 187 | base::TimeDelta::FromMilliseconds(kSubprocessRestartDelayMs)); |
| 188 | break; |
| 189 | |
| 190 | case CLD_STOPPED: |
| 191 | LOG(WARNING) << "Process for proxy " << proc |
| 192 | << " was unexpectedly stopped"; |
| 193 | break; |
| 194 | |
| 195 | case CLD_CONTINUED: |
| 196 | LOG(WARNING) << "Process for proxy " << proc << " has continued"; |
| 197 | break; |
| 198 | |
| 199 | default: |
| 200 | NOTREACHED(); |
| 201 | } |
| 202 | } |
| 203 | |
Garrick Evans | 48c84ef | 2021-01-28 11:29:42 +0900 | [diff] [blame] | 204 | void Controller::EvalProxyExit(const ProxyProc& proc) { |
| 205 | if (proc.opts.type != Proxy::Type::kSystem) |
| 206 | return; |
| 207 | |
| 208 | // Ensure the system proxy address is cleared from shill. |
| 209 | brillo::ErrorPtr error; |
| 210 | if (!shill_->ManagerProperties()->Set(shill::kDNSProxyIPv4AddressProperty, "", |
| 211 | &error)) |
| 212 | LOG(WARNING) << "Failed to clear shill dns-proxy property for " << proc |
| 213 | << ": " << error->GetMessage(); |
| 214 | } |
| 215 | |
Garrick Evans | 066dc2c | 2020-12-10 10:43:55 +0900 | [diff] [blame] | 216 | void Controller::EvalDefaultProxyDeps(bool has_deps) { |
| 217 | has_deps ? RunProxy(Proxy::Type::kDefault) : KillProxy(Proxy::Type::kDefault); |
| 218 | } |
| 219 | |
Garrick Evans | 07c86d6 | 2021-02-18 09:31:42 +0900 | [diff] [blame^] | 220 | void Controller::OnDefaultDeviceChanged( |
| 221 | const shill::Client::Device* const device) { |
| 222 | // If the default service is lost, |device| will be null. In this case, we can |
Garrick Evans | 066dc2c | 2020-12-10 10:43:55 +0900 | [diff] [blame] | 223 | // still safely clear the VPN bit since when it reconnects, if the VPN is |
| 224 | // still up, the bit will be reset here. |
Garrick Evans | 07c86d6 | 2021-02-18 09:31:42 +0900 | [diff] [blame^] | 225 | // Note that all VPN devices now use the kVPN type (so there is no need to |
| 226 | // check kPPP or kTunnel). |
| 227 | default_proxy_deps_->vpn_on(device && device->type == |
| 228 | shill::Client::Device::Type::kVPN); |
Garrick Evans | 066dc2c | 2020-12-10 10:43:55 +0900 | [diff] [blame] | 229 | } |
| 230 | |
| 231 | void Controller::OnVirtualDeviceChanged( |
| 232 | const patchpanel::NetworkDeviceChangedSignal& signal) { |
| 233 | switch (signal.event()) { |
| 234 | case patchpanel::NetworkDeviceChangedSignal::DEVICE_ADDED: |
| 235 | VirtualDeviceAdded(signal.device()); |
| 236 | break; |
| 237 | case patchpanel::NetworkDeviceChangedSignal::DEVICE_REMOVED: |
| 238 | VirtualDeviceRemoved(signal.device()); |
| 239 | break; |
| 240 | default: |
| 241 | NOTREACHED(); |
| 242 | } |
| 243 | } |
| 244 | |
| 245 | void Controller::VirtualDeviceAdded(const patchpanel::NetworkDevice& device) { |
| 246 | switch (device.guest_type()) { |
| 247 | case patchpanel::NetworkDevice::TERMINA_VM: |
| 248 | case patchpanel::NetworkDevice::PLUGIN_VM: |
| 249 | default_proxy_deps_->guest_up(device.ifname()); |
| 250 | break; |
| 251 | default: |
| 252 | break; |
| 253 | } |
| 254 | } |
| 255 | |
| 256 | void Controller::VirtualDeviceRemoved(const patchpanel::NetworkDevice& device) { |
| 257 | switch (device.guest_type()) { |
| 258 | case patchpanel::NetworkDevice::TERMINA_VM: |
| 259 | case patchpanel::NetworkDevice::PLUGIN_VM: |
| 260 | default_proxy_deps_->guest_down(device.ifname()); |
| 261 | break; |
| 262 | default: |
| 263 | break; |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | } // namespace dns_proxy |