| // 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(); |
| } |