blob: b0a228d9026f69a8128a86e9eaab9d666ad76233 [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/virtualization/bin/guest/vshc.h"
#include <fcntl.h>
#include <fuchsia/hardware/pty/llcpp/fidl.h>
#include <fuchsia/virtualization/cpp/fidl.h>
#include <lib/syslog/cpp/macros.h>
#include <poll.h>
#include <unistd.h>
#include <zircon/status.h>
#include <algorithm>
#include <iostream>
#include <google/protobuf/message_lite.h>
#include "src/lib/fsl/socket/socket_drainer.h"
#include "src/lib/fsl/tasks/fd_waiter.h"
#include "src/virtualization/lib/vsh/util.h"
#include "src/virtualization/packages/biscotti_guest/third_party/protos/vsh.pb.h"
namespace fpty = ::llcpp::fuchsia::hardware::pty;
std::pair<int, int> init_tty(void) {
int cols = 80;
int rows = 24;
if (isatty(STDIN_FILENO)) {
fdio_t* io = fdio_unsafe_fd_to_io(STDIN_FILENO);
auto wsz =
fpty::Device::Call::GetWindowSize(zx::unowned_channel(fdio_unsafe_borrow_channel(io)));
if (wsz.status() != ZX_OK || wsz->status != ZX_OK) {
FX_LOGS(WARNING) << "Unable to determine shell geometry, defaulting to 80x24";
} else {
cols = wsz->size.width;
rows = wsz->size.height;
}
// Enable raw mode on tty so that inputs such as ctrl-c are passed on
// faithfully to the client for forwarding to the remote shell
// (instead of closing the client side).
auto result = fpty::Device::Call::ClrSetFeature(
zx::unowned_channel(fdio_unsafe_borrow_channel(io)), 0, fpty::FEATURE_RAW);
if (result.status() != ZX_OK || result->status != ZX_OK) {
FX_LOGS(ERROR) << "Failed to set FEATURE_RAW, some features may not work.";
}
fdio_unsafe_release(io);
}
return {cols, rows};
}
void reset_tty(void) {
if (isatty(STDIN_FILENO)) {
fdio_t* io = fdio_unsafe_fd_to_io(STDIN_FILENO);
auto result = fpty::Device::Call::ClrSetFeature(
zx::unowned_channel(fdio_unsafe_borrow_channel(io)), fpty::FEATURE_RAW, 0);
if (result.status() != ZX_OK || result->status != ZX_OK) {
FX_LOGS(ERROR) << "Failed to reset FEATURE_RAW";
}
fdio_unsafe_release(io);
}
}
namespace {
class ConsoleIn {
public:
ConsoleIn(async::Loop* loop, zx::unowned_socket&& socket)
: loop_(loop), sink_(std::move(socket)), fd_waiter_(loop->dispatcher()) {}
bool Start(void) {
if (fcntl(STDIN_FILENO, F_GETFD) != -1) {
fd_waiter_.Wait(fit::bind_member(this, &ConsoleIn::HandleStdin), STDIN_FILENO, POLLIN);
} else {
FX_LOGS(ERROR) << "Unable to start the async output loop";
return false;
}
return true;
}
void HandleStdin(zx_status_t status, uint32_t events) {
if (status != ZX_OK && status != ZX_ERR_SHOULD_WAIT) {
loop_->Shutdown();
return;
}
vm_tools::vsh::GuestMessage msg_out;
uint8_t buf[vsh::kMaxDataSize];
ssize_t actual = read(STDIN_FILENO, buf, vsh::kMaxDataSize);
msg_out.mutable_data_message()->set_stream(vm_tools::vsh::STDIN_STREAM);
msg_out.mutable_data_message()->set_data(buf, actual);
if (!vsh::SendMessage(*sink_, msg_out)) {
FX_LOGS(ERROR) << "Failed to send stdin";
return;
}
fd_waiter_.Wait(fit::bind_member(this, &ConsoleIn::HandleStdin), STDIN_FILENO, POLLIN);
}
private:
async::Loop* loop_;
zx::unowned_socket sink_;
fsl::FDWaiter fd_waiter_;
};
class ConsoleOut {
public:
ConsoleOut(async::Loop* loop, zx::unowned_socket&& socket)
: loop_(loop),
source_(std::move(socket)),
reading_size_(true),
msg_size_(sizeof(uint32_t)),
bytes_left_(msg_size_) {}
bool Start(void) {
wait_.set_object(source_->get());
wait_.set_trigger(ZX_SOCKET_READABLE);
auto status = wait_.Begin(loop_->dispatcher());
if (status != ZX_OK) {
FX_LOGS(ERROR) << "Unable to start the async input loop";
return false;
}
return true;
}
void HandleTtyOutput(async_dispatcher_t* dispatcher, async::WaitBase* wait, zx_status_t status,
const zx_packet_signal_t* signal) {
if (status != ZX_OK && status != ZX_ERR_SHOULD_WAIT) {
loop_->Shutdown();
loop_->Quit();
return;
}
vm_tools::vsh::HostMessage msg_in;
if (status != ZX_ERR_SHOULD_WAIT && bytes_left_) {
size_t actual;
source_->read(0, buf_ + (msg_size_ - bytes_left_), bytes_left_, &actual);
bytes_left_ -= actual;
}
if (bytes_left_ == 0 && reading_size_) {
uint32_t sz;
std::memcpy(&sz, buf_, sizeof(sz));
// set state for next iteration
reading_size_ = false;
msg_size_ = le32toh(sz);
bytes_left_ = msg_size_;
FX_CHECK(msg_size_ <= vsh::kMaxMessageSize)
<< "Message size of " << msg_size_ << " exceeds kMaxMessageSize";
} else if (bytes_left_ == 0 && !reading_size_) {
FX_CHECK(msg_in.ParseFromArray(buf_, msg_size_)) << "Failed to parse incoming message.";
reading_size_ = true;
msg_size_ = sizeof(uint32_t);
bytes_left_ = msg_size_;
switch (msg_in.msg_case()) {
case vm_tools::vsh::HostMessage::MsgCase::kDataMessage: {
auto data = msg_in.data_message().data();
write(STDOUT_FILENO, data.data(), data.size());
} break;
case vm_tools::vsh::HostMessage::MsgCase::kStatusMessage:
if (msg_in.status_message().status() != vm_tools::vsh::READY) {
loop_->Shutdown();
loop_->Quit();
reset_tty();
if (msg_in.status_message().status() == vm_tools::vsh::EXITED) {
exit(msg_in.status_message().code());
} else {
FX_LOGS(ERROR) << "vsh did not complete successfully.";
exit(-1);
}
}
break;
default:
FX_LOGS(WARNING) << "Unhandled HostMessage received.";
}
}
status = wait->Begin(dispatcher);
}
private:
async::WaitMethod<ConsoleOut, &ConsoleOut::HandleTtyOutput> wait_{this};
async::Loop* loop_;
zx::unowned_socket source_;
uint8_t buf_[vsh::kMaxMessageSize];
bool reading_size_;
size_t msg_size_;
size_t bytes_left_;
};
} // namespace
static bool init_shell(const zx::socket& usock) {
vm_tools::vsh::SetupConnectionRequest conn_req;
vm_tools::vsh::SetupConnectionResponse conn_resp;
// Target can be |vsh::kVmShell| or the empty string for the VM.
// Specifying container name directly here is not supported.
conn_req.set_target("");
// User can be defaulted with empty string. This is chronos for vmshell and
// root otherwise
conn_req.set_user("");
// Blank command for login shell. (other uses deprecated, use argv directly
// instead)
conn_req.set_command("");
conn_req.clear_argv();
auto env = conn_req.mutable_env();
if (auto term_env = getenv("TERM"))
(*env)["TERM"] = std::string(term_env);
(*env)["LXD_DIR"] = "/mnt/stateful/lxd";
(*env)["LXD_CONF"] = "/mnt/stateful/lxd_conf";
(*env)["LXD_UNPRIVILEGED_ONLY"] = "true";
if (!vsh::SendMessage(usock, conn_req)) {
FX_LOGS(ERROR) << "Failed to send connection request";
return false;
}
// No use setting up the async message handling if we haven't even
// connected properly. Block on connection response.
if (!vsh::RecvMessage(usock, &conn_resp)) {
FX_LOGS(ERROR) << "Failed to receive response from vshd, giving up after one try";
return false;
}
if (conn_resp.status() != vm_tools::vsh::READY) {
FX_LOGS(ERROR) << "Server was unable to set up connection properly: "
<< conn_resp.description();
return false;
}
// Connection to server established.
// Initial Configuration Phase.
auto [cols, rows] = init_tty();
vm_tools::vsh::GuestMessage msg_out;
msg_out.mutable_resize_message()->set_cols(cols);
msg_out.mutable_resize_message()->set_rows(rows);
if (!vsh::SendMessage(usock, msg_out)) {
FX_LOGS(ERROR) << "Failed to send window resize message";
return false;
}
return true;
}
void handle_vsh(std::optional<uint32_t> o_env_id, std::optional<uint32_t> o_cid,
std::optional<uint32_t> o_port, async::Loop* loop, sys::ComponentContext* context) {
uint32_t env_id, cid, port = o_port.value_or(vsh::kVshPort);
fuchsia::virtualization::ManagerSyncPtr manager;
context->svc()->Connect(manager.NewRequest());
std::vector<fuchsia::virtualization::EnvironmentInfo> env_infos;
manager->List(&env_infos);
if (env_infos.size() == 0) {
FX_LOGS(ERROR) << "Unable to find any environments.";
return;
}
env_id = o_env_id.value_or(env_infos[0].id);
fuchsia::virtualization::RealmSyncPtr realm;
manager->Connect(env_id, realm.NewRequest());
std::vector<fuchsia::virtualization::InstanceInfo> instances;
realm->ListInstances(&instances);
if (instances.size() == 0) {
FX_LOGS(ERROR) << "Unable to find any instances in environment " << env_id;
return;
}
cid = o_cid.value_or(instances[0].cid);
// Verify the environment and instance specified exist
if (std::find_if(env_infos.begin(), env_infos.end(),
[env_id](auto ei) { return ei.id == env_id; }) == env_infos.end()) {
FX_LOGS(ERROR) << "No existing environment with id " << env_id;
return;
}
if (std::find_if(instances.begin(), instances.end(), [cid](auto in) { return in.cid == cid; }) ==
instances.end()) {
FX_LOGS(ERROR) << "No existing instances in env " << env_id << " with cid " << cid;
return;
}
fuchsia::virtualization::HostVsockEndpointSyncPtr vsock_endpoint;
realm->GetHostVsockEndpoint(vsock_endpoint.NewRequest());
// Open a socket to the guest's vsock port where vshd should be listening
zx::socket socket, remote_socket;
zx_status_t status = zx::socket::create(ZX_SOCKET_STREAM, &socket, &remote_socket);
if (status != ZX_OK) {
FX_LOGS(ERROR) << "Failed to create socket: " << zx_status_get_string(status);
return;
}
vsock_endpoint->Connect(cid, port, std::move(remote_socket), &status);
if (status != ZX_OK) {
FX_LOGS(ERROR) << "Failed to connect: " << zx_status_get_string(status);
return;
}
// Now |socket| is a zircon socket plumbed to a port on the guest's vsock
// interface. The vshd service is hopefully on the other end of this pipe.
// We communicate with the service via protobuf messages.
if (!init_shell(socket)) {
FX_LOGS(ERROR) << "vsh SetupConnection failed.";
return;
}
// Directly inject some helper functions for connecting to container.
// This sleep below is to give bash some time to start after being `exec`d.
// Otherwise the input will be duplicated in the output stream.
usleep(100'000);
vm_tools::vsh::GuestMessage msg_out;
msg_out.mutable_data_message()->set_stream(vm_tools::vsh::STDIN_STREAM);
msg_out.mutable_data_message()->set_data(
"function stretch() { lxc exec stretch -- login -f machina ; } \n\n");
if (!vsh::SendMessage(socket, msg_out)) {
FX_LOGS(WARNING) << "Failed to inject helper function";
}
// Set up the I/O loops
ConsoleIn i(loop, zx::unowned_socket(socket));
ConsoleOut o(loop, zx::unowned_socket(socket));
bool success = true;
if (!i.Start()) {
FX_LOGS(ERROR) << "Problem starting ConsoleIn loop";
success = false;
}
if (!o.Start()) {
FX_LOGS(ERROR) << "Problem starting ConsoleOut loop";
success = false;
}
if (success) {
loop->Run();
}
reset_tty();
}