blob: a8758b55e7531eba9e310e0c73be9c1982db9d9d [file] [log] [blame]
// Copyright 2019 The Fuchsia 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 "src/developer/sshd-host/service.h"
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <fuchsia/boot/cpp/fidl.h>
#include <fuchsia/component/cpp/fidl.h>
#include <fuchsia/process/cpp/fidl.h>
#include <lib/async/dispatcher.h>
#include <lib/fdio/fd.h>
#include <lib/fit/defer.h>
#include <lib/fit/function.h>
#include <lib/sys/cpp/service_directory.h>
#include <lib/syslog/cpp/macros.h>
#include <netdb.h>
#include <netinet/in.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <zircon/errors.h>
#include <zircon/processargs.h>
#include <zircon/types.h>
#include <memory>
#include <vector>
#include <fbl/unique_fd.h>
#include "src/developer/sshd-host/constants.h"
#include "src/lib/fxl/strings/string_printf.h"
namespace sshd_host {
zx_status_t provision_authorized_keys_from_bootloader_file(
const std::shared_ptr<sys::ServiceDirectory>& service_directory) {
fuchsia::boot::ItemsSyncPtr boot_items;
if (zx_status_t status = service_directory->Connect(boot_items.NewRequest()); status != ZX_OK) {
FX_PLOGS(ERROR, status)
<< "Provisioning keys from boot item: failed to connect to boot items service";
return status;
}
zx::vmo vmo;
if (zx_status_t status =
boot_items->GetBootloaderFile(std::string(kAuthorizedKeysBootloaderFileName.data(),
kAuthorizedKeysBootloaderFileName.size()),
&vmo);
status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Provisioning keys from boot item: GetBootloaderFile failed";
return status;
}
if (!vmo.is_valid()) {
FX_LOGS(INFO) << "Provisioning keys from boot item: bootloader file not found: "
<< kAuthorizedKeysBootloaderFileName;
return ZX_ERR_NOT_FOUND;
}
uint64_t size;
if (zx_status_t status = vmo.get_property(ZX_PROP_VMO_CONTENT_SIZE, &size, sizeof(size));
status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Provisioning keys from boot item: unable to get file size";
return status;
}
std::unique_ptr buffer = std::make_unique<uint8_t[]>(size);
if (zx_status_t status = vmo.read(buffer.get(), 0, size); status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Provisioning keys from boot item: failed to read file";
return status;
}
if (mkdir(kSshDirectory, 0700) && errno != EEXIST) {
FX_LOGS(ERROR) << "Provisioning keys from boot item: failed to create directory: "
<< kSshDirectory << " Error: " << strerror(errno);
return ZX_ERR_IO;
}
fbl::unique_fd kfd(open(kAuthorizedKeysPath, O_CREAT | O_EXCL | O_WRONLY, S_IRUSR | S_IWUSR));
if (!kfd) {
FX_LOGS(ERROR) << "Provisioning keys from boot item: open failed: " << kAuthorizedKeysPath
<< " error: " << strerror(errno);
return errno == EEXIST ? ZX_ERR_ALREADY_EXISTS : ZX_ERR_IO;
}
if (write(kfd.get(), buffer.get(), size) != static_cast<ssize_t>(size)) {
FX_LOGS(ERROR) << "Provisioning keys from boot item: write failed: " << strerror(errno);
return ZX_ERR_IO;
}
fsync(kfd.get());
if (close(kfd.release())) {
FX_LOGS(ERROR) << "Provisioning keys from boot item: close failed: " << strerror(errno);
return ZX_ERR_IO;
}
FX_LOGS(INFO) << "Provisioning keys from boot item: authorized_keys provisioned";
return ZX_OK;
}
// static
Service::Socket Service::MakeSocket(async_dispatcher_t* dispatcher, IpVersion ip_version,
uint16_t port) {
const int family = static_cast<int>(ip_version);
fbl::unique_fd sock(socket(family, SOCK_STREAM, IPPROTO_TCP));
if (!sock.is_valid()) {
FX_LOGS(FATAL) << "Failed to create socket: " << strerror(errno);
}
sockaddr_storage addr;
switch (ip_version) {
case IpVersion::V4:
*reinterpret_cast<struct sockaddr_in*>(&addr) = sockaddr_in{
.sin_family = AF_INET,
.sin_port = htons(port),
.sin_addr = in_addr{INADDR_ANY},
};
break;
case IpVersion::V6:
*reinterpret_cast<struct sockaddr_in6*>(&addr) = sockaddr_in6{
.sin6_family = AF_INET6,
.sin6_port = htons(port),
.sin6_addr = in6addr_any,
};
// Disable dual-stack mode for the socket.
constexpr const int kEnable = 1;
setsockopt(sock.get(), IPPROTO_IPV6, IPV6_V6ONLY, &kEnable, sizeof(kEnable));
break;
}
if (bind(sock.get(), reinterpret_cast<const sockaddr*>(&addr), sizeof addr) < 0) {
FX_LOGS(FATAL) << "Failed to bind to " << port << ": " << strerror(errno);
}
FX_SLOG(INFO, "listen() for inbound SSH connections", KV("port", (int)port));
if (listen(sock.get(), 10) < 0) {
FX_LOGS(FATAL) << "Failed to listen: " << strerror(errno);
}
return Socket{.fd = std::move(sock), .waiter = fsl::FDWaiter(dispatcher)};
}
Service::Service(async_dispatcher_t* dispatcher,
std::shared_ptr<sys::ServiceDirectory> service_directory, uint16_t port)
: dispatcher_(dispatcher),
service_directory_(std::move(service_directory)),
port_(port),
v4_socket_(MakeSocket(dispatcher_, IpVersion::V4, port_)),
v6_socket_(MakeSocket(dispatcher_, IpVersion::V6, port_)) {
Wait(std::nullopt);
}
Service::~Service() = default;
void Service::Wait(std::optional<IpVersion> ip_version) {
FX_SLOG(DEBUG, "Waiting for next connection");
auto do_wait = [this](Service::Socket* sock, IpVersion ip_version) {
sock->waiter.Wait(
[this, sock, ip_version](zx_status_t status, uint32_t /*events*/) {
if (status != ZX_OK) {
FX_PLOGS(FATAL, status) << "Failed to wait on socket";
}
struct sockaddr_storage peer_addr {};
socklen_t peer_addr_len = sizeof(peer_addr);
fbl::unique_fd conn(accept(sock->fd.get(), reinterpret_cast<struct sockaddr*>(&peer_addr),
&peer_addr_len));
if (!conn.is_valid()) {
if (errno == EPIPE) {
FX_LOGS(FATAL) << "The netstack died. Terminating.";
} else {
FX_LOGS(ERROR) << "Failed to accept: " << strerror(errno);
// Wait for another connection.
Wait(ip_version);
}
return;
}
std::string peer_name = "unknown";
char host[NI_MAXHOST];
char port[NI_MAXSERV];
if (int res =
getnameinfo(reinterpret_cast<struct sockaddr*>(&peer_addr), peer_addr_len, host,
sizeof(host), port, sizeof(port), NI_NUMERICHOST | NI_NUMERICSERV);
res == 0) {
switch (ip_version) {
case IpVersion::V4:
peer_name = fxl::StringPrintf("%s:%s", host, port);
break;
case IpVersion::V6:
peer_name = fxl::StringPrintf("[%s]:%s", host, port);
break;
}
} else {
FX_LOGS(WARNING)
<< "Error from getnameinfo(.., NI_NUMERICHOST | NI_NUMERICSERV) for peer address: "
<< gai_strerror(res);
}
FX_SLOG(INFO, "Accepted connection", KV("remote", peer_name.c_str()));
Launch(std::move(conn));
Wait(ip_version);
},
sock->fd.get(), POLLIN);
};
if (ip_version) {
switch (*ip_version) {
case IpVersion::V4:
return do_wait(&v4_socket_, IpVersion::V4);
case IpVersion::V6:
return do_wait(&v6_socket_, IpVersion::V6);
}
} else {
do_wait(&v4_socket_, IpVersion::V4);
do_wait(&v6_socket_, IpVersion::V6);
}
}
void Service::Launch(fbl::unique_fd conn) {
std::string child_name = fxl::StringPrintf("sshd-%lu", next_child_num_++);
fuchsia::component::RealmSyncPtr realm;
if (zx_status_t status = service_directory_->Connect(realm.NewRequest()); status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Failed to connect to realm service";
return;
}
fuchsia::component::ControllerSyncPtr controller;
{
fuchsia::component::decl::CollectionRef collection{
.name = std::string(kShellCollection),
};
fuchsia::component::decl::Child decl;
decl.set_name(child_name);
decl.set_url("#meta/sshd.cm");
decl.set_startup(fuchsia::component::decl::StartupMode::LAZY);
fuchsia::component::CreateChildArgs args;
args.set_controller(controller.NewRequest());
fuchsia::component::Realm_CreateChild_Result result;
if (zx_status_t status =
realm->CreateChild(collection, std::move(decl), std::move(args), &result);
status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Failed to create sshd child";
return;
}
if (result.is_err()) {
FX_LOGS(ERROR) << "Failed to create sshd child: " << static_cast<uint32_t>(result.err());
return;
}
}
fuchsia::component::ExecutionControllerPtr& execution_controller = controllers_.emplace_back();
auto remove_controller_on_error = fit::defer([this]() { controllers_.pop_back(); });
fit::callback<void(zx_status_t)> on_stop_cb = [this, ptr = execution_controller.get(),
realm = std::move(realm),
child_name](zx_status_t status) {
if (status != ZX_OK) {
FX_PLOGS(WARNING, status) << "sshd component stopped with error";
}
// Remove the controller.
auto i = std::find_if(controllers_.begin(), controllers_.end(),
[ptr](const fuchsia::component::ExecutionControllerPtr& controller) {
return controller.get() == ptr;
});
if (i != controllers_.end()) {
controllers_.erase(i);
};
// Destroy the component.
fuchsia::component::decl::ChildRef child_ref = {
.name = child_name,
.collection = std::string(kShellCollection),
};
fuchsia::component::Realm_DestroyChild_Result result;
if (zx_status_t status = realm->DestroyChild(std::move(child_ref), &result); status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Failed to destroy sshd child";
return;
}
if (result.is_err()) {
FX_LOGS(ERROR) << "Failed to destroy sshd child: " << static_cast<uint32_t>(result.err());
return;
}
};
execution_controller.events().OnStop =
[on_stop_cb = on_stop_cb.share()](fuchsia::component::StoppedPayload payload) mutable {
zx_status_t status = ZX_OK;
if (payload.has_status()) {
status = payload.status();
}
on_stop_cb(status);
};
execution_controller.set_error_handler(
[on_stop_cb = std::move(on_stop_cb)](zx_status_t status) mutable { on_stop_cb(status); });
// Pass the connection fd as stdin and stdout handles to the sshd component.
fuchsia::component::StartChildArgs start_args;
for (int fd : {STDIN_FILENO, STDOUT_FILENO}) {
zx::handle conn_handle;
if (zx_status_t status = fdio_fd_clone(conn.get(), conn_handle.reset_and_get_address());
status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Failed to clone connection file descriptor " << conn.get();
return;
}
start_args.mutable_numbered_handles()->push_back(
fuchsia::process::HandleInfo{.handle = std::move(conn_handle), .id = PA_HND(PA_FD, fd)});
}
fuchsia::component::Controller_Start_Result result;
if (zx_status_t status = controller->Start(std::move(start_args),
execution_controller.NewRequest(dispatcher_), &result);
status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Failed to start sshd child";
return;
}
if (result.is_err()) {
FX_LOGS(ERROR) << "Failed to start sshd child: " << static_cast<uint32_t>(result.err());
return;
}
remove_controller_on_error.cancel();
}
} // namespace sshd_host