blob: 5bee9457fe7b14d98055d8769dab8b3b03b427c6 [file] [log] [blame]
// Copyright 2020 The Chromium OS 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 "system-proxy/sandboxed_worker.h"
#include <inttypes.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string>
#include <utility>
#include <vector>
#include <base/bind.h>
#include <base/callback_helpers.h>
#include <base/check.h>
#include <base/files/file_util.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <brillo/http/http_transport.h>
#include <chromeos/patchpanel/net_util.h>
#include <google/protobuf/repeated_field.h>
#include "system-proxy/protobuf_util.h"
#include "system-proxy/system_proxy_adaptor.h"
namespace {
constexpr char kSystemProxyWorkerBin[] = "/usr/sbin/system_proxy_worker";
constexpr char kSeccompFilterPath[] =
"/usr/share/policy/system-proxy-worker-seccomp.policy";
constexpr int kMaxWorkerMessageSize = 4096;
// Size of the buffer array used to read data from the worker's stderr.
constexpr int kWorkerBufferSize = 1024;
constexpr char kPrefixDirect[] = "direct://";
constexpr char kPrefixHttp[] = "http://";
} // namespace
namespace system_proxy {
SandboxedWorker::SandboxedWorker(base::WeakPtr<SystemProxyAdaptor> adaptor)
: jail_(minijail_new()), adaptor_(adaptor), pid_(0) {}
bool SandboxedWorker::Start() {
DCHECK(!IsRunning()) << "Worker is already running.";
if (!jail_)
return false;
minijail_namespace_pids(jail_.get());
minijail_namespace_net(jail_.get());
minijail_no_new_privs(jail_.get());
minijail_use_seccomp_filter(jail_.get());
minijail_parse_seccomp_filters(jail_.get(), kSeccompFilterPath);
// Required to forward SIGTERM to the child process.
minijail_forward_signals(jail_.get());
// Resets the signal mask to ensure signals are not unintentionally blocked.
minijail_reset_signal_mask(jail_.get());
// Resets the signal handlers to the default behaviours. This is needed so
// that the child process terminates when receiving the SIGTERM signal.
minijail_reset_signal_handlers(jail_.get());
int child_stdin = -1, child_stdout = -1, child_stderr = -1;
std::vector<char*> args_ptr;
args_ptr.push_back(const_cast<char*>(kSystemProxyWorkerBin));
args_ptr.push_back(nullptr);
// Execute the command.
int res =
minijail_run_pid_pipes(jail_.get(), args_ptr[0], args_ptr.data(), &pid_,
&child_stdin, &child_stdout, &child_stderr);
if (res != 0) {
LOG(ERROR) << "Failed to start sandboxed worker: " << strerror(-res);
return false;
}
// Make sure the pipes never block.
if (!base::SetNonBlocking(child_stdin))
LOG(WARNING) << "Failed to set stdin non-blocking";
if (!base::SetNonBlocking(child_stdout))
LOG(WARNING) << "Failed to set stdout non-blocking";
if (!base::SetNonBlocking(child_stderr))
LOG(WARNING) << "Failed to set stderr non-blocking";
stdin_pipe_.reset(child_stdin);
stdout_pipe_.reset(child_stdout);
stderr_pipe_.reset(child_stderr);
stdout_watcher_ = base::FileDescriptorWatcher::WatchReadable(
stdout_pipe_.get(),
base::BindRepeating(&SandboxedWorker::OnMessageReceived,
base::Unretained(this)));
stderr_watcher_ = base::FileDescriptorWatcher::WatchReadable(
stderr_pipe_.get(), base::BindRepeating(&SandboxedWorker::OnErrorReceived,
base::Unretained(this)));
return true;
}
void SandboxedWorker::SetCredentials(const worker::Credentials& credentials) {
worker::WorkerConfigs configs;
*configs.mutable_credentials() = credentials;
if (!WriteProtobuf(stdin_pipe_.get(), configs)) {
LOG(ERROR) << "Failed to set credentials for worker " << pid_;
}
}
bool SandboxedWorker::SetListeningAddress(uint32_t addr, int port) {
worker::SocketAddress address;
address.set_addr(addr);
address.set_port(port);
worker::WorkerConfigs configs;
*configs.mutable_listening_address() = address;
if (!WriteProtobuf(stdin_pipe_.get(), configs)) {
LOG(ERROR) << "Failed to set local proxy address for worker " << pid_;
return false;
}
local_proxy_host_and_port_ = base::StringPrintf(
"%s:%d", patchpanel::IPv4AddressToString(addr).c_str(), port);
LOG(INFO) << "Set proxy address " << local_proxy_host_and_port_
<< " for worker " << pid_;
return true;
}
bool SandboxedWorker::SetKerberosEnabled(bool enabled,
const std::string& krb5_conf_path,
const std::string& krb5_ccache_path) {
worker::KerberosConfig kerberos_config;
kerberos_config.set_enabled(enabled);
kerberos_config.set_krb5cc_path(krb5_ccache_path);
kerberos_config.set_krb5conf_path(krb5_conf_path);
worker::WorkerConfigs configs;
*configs.mutable_kerberos_config() = kerberos_config;
if (!WriteProtobuf(stdin_pipe_.get(), configs)) {
LOG(ERROR) << "Failed to set kerberos enabled for worker " << pid_;
return false;
}
return true;
}
bool SandboxedWorker::ClearUserCredentials() {
worker::ClearUserCredentials clear_user_credentials;
worker::WorkerConfigs configs;
*configs.mutable_clear_user_credentials() = clear_user_credentials;
if (!WriteProtobuf(stdin_pipe_.get(), configs)) {
LOG(ERROR) << "Failed to send request to clear user credentials for worker "
<< pid_;
return false;
}
return true;
}
bool SandboxedWorker::Stop() {
if (is_being_terminated_)
return true;
LOG(INFO) << "Killing " << pid_;
is_being_terminated_ = true;
if (kill(pid_, SIGTERM) < 0) {
if (errno == ESRCH) {
// No process or group found for pid, assume already terminated.
return true;
}
PLOG(ERROR) << "Failed to terminate process " << pid_;
return false;
}
return true;
}
bool SandboxedWorker::IsRunning() {
return pid_ != 0 && !is_being_terminated_;
}
void SandboxedWorker::OnMessageReceived() {
worker::WorkerRequest request;
if (!ReadProtobuf(stdout_pipe_.get(), &request)) {
LOG(ERROR) << "Failed to read request from worker " << pid_;
// The message is corrupted or the pipe closed, either way stop listening.
stdout_watcher_ = nullptr;
return;
}
if (request.has_log_request()) {
LOG(INFO) << "[worker: " << pid_ << "]" << request.log_request().message();
}
if (request.has_proxy_resolution_request()) {
const worker::ProxyResolutionRequest& proxy_request =
request.proxy_resolution_request();
// This callback will always be called with at least one proxy entry. Even
// if the dbus call itself fails, the proxy server list will contain the
// direct proxy.
adaptor_->GetChromeProxyServersAsync(
proxy_request.target_url(),
base::BindRepeating(&SandboxedWorker::OnProxyResolved,
weak_ptr_factory_.GetWeakPtr(),
proxy_request.target_url()));
}
if (request.has_auth_required_request()) {
const worker::AuthRequiredRequest& auth_request =
request.auth_required_request();
adaptor_->RequestAuthenticationCredentials(
auth_request.protection_space(), auth_request.bad_cached_credentials());
}
}
void SandboxedWorker::SetNetNamespaceLifelineFd(
base::ScopedFD net_namespace_lifeline_fd) {
// Sanity check that only one network namespace is setup for the worker
// process.
DCHECK(!net_namespace_lifeline_fd_.is_valid());
net_namespace_lifeline_fd_ = std::move(net_namespace_lifeline_fd);
}
void SandboxedWorker::OnErrorReceived() {
std::vector<char> buf;
buf.resize(kWorkerBufferSize);
std::string message;
std::string worker_msg = "[worker: " + std::to_string(pid_) + "] ";
ssize_t count = kWorkerBufferSize;
ssize_t total_count = 0;
while (count == kWorkerBufferSize) {
count = HANDLE_EINTR(read(stderr_pipe_.get(), buf.data(), buf.size()));
if (count < 0) {
PLOG(ERROR) << worker_msg << "Failed to read from stdio";
return;
}
if (count == 0) {
if (!message.empty())
break; // Full message was read at the first iteration.
PLOG(INFO) << worker_msg << "Pipe closed";
// Stop watching, otherwise the handler will fire forever.
stderr_watcher_ = nullptr;
}
total_count += count;
if (total_count > kMaxWorkerMessageSize) {
LOG(ERROR) << "Failure to read message from woker: message size exceeds "
"maximum allowed";
stderr_watcher_ = nullptr;
return;
}
message.append(buf.begin(), buf.begin() + count);
}
LOG(ERROR) << worker_msg << message;
}
void SandboxedWorker::OnProxyResolved(
const std::string& target_url,
bool success,
const std::vector<std::string>& proxy_servers) {
worker::ProxyResolutionReply reply;
reply.set_target_url(target_url);
// Only http and direct proxies are supported at the moment.
for (const auto& proxy : proxy_servers) {
if (base::StartsWith(proxy, kPrefixHttp,
base::CompareCase::INSENSITIVE_ASCII) ||
base::StartsWith(proxy, kPrefixDirect,
base::CompareCase::INSENSITIVE_ASCII)) {
// Make sure the local proxy doesn't try to connect to itself.
if (!adaptor_->IsLocalProxy(proxy)) {
reply.add_proxy_servers(proxy);
}
}
}
worker::WorkerConfigs configs;
*configs.mutable_proxy_resolution_reply() = reply;
if (!WriteProtobuf(stdin_pipe_.get(), configs)) {
LOG(ERROR) << "Failed to send proxy resolution reply to worker" << pid_;
}
}
} // namespace system_proxy