mirror of
https://github.com/nestriness/nestri.git
synced 2026-11-03 22:22:39 +02:00
feat: attach a shell to a running box
The host can now ask a box for a shell on a pseudo-terminal, the way a container runtime attaches to a container. `HostToGuest::Attach` starts the process, and the guest then dials the host on a vsock port of its own (7001) and carries the terminal's bytes over a small framed stream: data both ways, a resize, a hangup, and the exit status last. A screenful of output on the JSON-lines control channel would hold every other message in the box behind it, which is why it is not there. A failure to start is the exception, and is said on the control channel because nothing has connected to the data port yet. A shell is not a launch: it never ends the box, and the box ending ends it. It is started through the same reaper as everything else, so it cannot become the zombie that exists to be prevented. With a terminal on the other end the PTY is ordinary; without one it is put in raw mode so a command run for its output gets its bytes back unrewritten. Closing the stream hangs the shell up, process group and all. The frame codec is a pure bytes-in, frames-out module in nesprotocol with a payload cap and no skipping of unknown tags, since a length read from a guest is read across a trust boundary. No version bump: adding a message does not need one, and a guest that predates it simply never dials. The runtime stage gains strace, lsof and less, so a root shell in a box is not one that cannot trace a process. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
e5447a91e4
commit
d0ec87a139
@@ -11,7 +11,7 @@ use std::future::Future;
|
||||
use std::io;
|
||||
use std::pin::Pin;
|
||||
|
||||
use nesprotocol::lifecycle::{Exec, Exit, Image, Mount, Overlay};
|
||||
use nesprotocol::lifecycle::{AttachId, Exec, Exit, Image, Mount, Overlay, Winsize};
|
||||
|
||||
use std::os::unix::process::CommandExt;
|
||||
|
||||
@@ -51,6 +51,15 @@ pub trait Workload {
|
||||
/// layer, and the two together where it says.
|
||||
fn mount_overlays(&mut self, overlays: &[Overlay]) -> Result<(), Failure>;
|
||||
|
||||
/// Start a process on a terminal and connect it to the host on a data port
|
||||
/// of its own.
|
||||
///
|
||||
/// Returns once the process is started or has failed to be, and no later:
|
||||
/// everything that can fail before there is anything to connect to fails
|
||||
/// here, where it can be sent up the control channel. The connection itself
|
||||
/// is made in the background.
|
||||
fn attach(&mut self, id: AttachId, exec: &Exec, size: Option<Winsize>) -> Result<(), Failure>;
|
||||
|
||||
/// Start the command the descriptor names.
|
||||
///
|
||||
/// Returning the exit as a future, rather than a `wait` method, is what
|
||||
@@ -146,6 +155,32 @@ impl Workload for Process {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn attach(&mut self, id: AttachId, exec: &Exec, size: Option<Winsize>) -> Result<(), Failure> {
|
||||
use nesprotocol::lifecycle::ATTACH_PORT;
|
||||
use tokio_vsock::{VMADDR_CID_HOST, VsockAddr, VsockStream};
|
||||
|
||||
let shell = crate::attach::spawn(&self.waiters, exec, size)?;
|
||||
tracing::info!(attach = %id, pid = shell.pid(), uid = exec.uid, "a shell was started");
|
||||
tokio::spawn(async move {
|
||||
// Dialled once. The host is listening before it asks, so a refused
|
||||
// connection means it stopped wanting the shell, and `shell`
|
||||
// dropping at the end of this task is what hangs it up.
|
||||
let address = VsockAddr::new(VMADDR_CID_HOST, ATTACH_PORT);
|
||||
let stream = match VsockStream::connect(address).await {
|
||||
Ok(stream) => stream,
|
||||
Err(error) => {
|
||||
tracing::warn!(attach = %id, %error, "could not reach the host for a shell");
|
||||
return;
|
||||
}
|
||||
};
|
||||
match crate::attach::serve(shell, &id, stream).await {
|
||||
Ok(()) => tracing::info!(attach = %id, "a shell ended"),
|
||||
Err(error) => tracing::warn!(attach = %id, %error, "a shell's connection failed"),
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mount_overlays(&mut self, overlays: &[Overlay]) -> Result<(), Failure> {
|
||||
for overlay in overlays {
|
||||
mount_overlay(overlay)?;
|
||||
@@ -263,7 +298,7 @@ impl Workload for Process {
|
||||
/// finds the variable unset does not fail loudly -- the compositor here falls
|
||||
/// back to `/tmp` -- so the sockets land somewhere world-writable and shared
|
||||
/// with every other user, and everything reports success. ref(d-0065)
|
||||
fn environment(exec: &Exec, runtime: Option<&str>) -> Vec<(String, String)> {
|
||||
pub(crate) fn environment(exec: &Exec, runtime: Option<&str>) -> Vec<(String, String)> {
|
||||
GRAPHICS
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||
@@ -950,6 +985,8 @@ pub mod double {
|
||||
pub images: Vec<Vec<Image>>,
|
||||
/// Which kind of mount was asked for, in the order it was asked.
|
||||
pub order: Vec<&'static str>,
|
||||
pub attached: Vec<(AttachId, Exec, Option<Winsize>)>,
|
||||
pub attach_failure: Option<Failure>,
|
||||
pub started: Vec<Exec>,
|
||||
pub stops: usize,
|
||||
pub mount_failure: Option<Failure>,
|
||||
@@ -977,6 +1014,8 @@ pub mod double {
|
||||
overlays: Vec::new(),
|
||||
images: Vec::new(),
|
||||
order: Vec::new(),
|
||||
attached: Vec::new(),
|
||||
attach_failure: None,
|
||||
started: Vec::new(),
|
||||
stops: 0,
|
||||
mount_failure: None,
|
||||
@@ -999,6 +1038,19 @@ pub mod double {
|
||||
}
|
||||
}
|
||||
|
||||
fn attach(
|
||||
&mut self,
|
||||
id: AttachId,
|
||||
exec: &Exec,
|
||||
size: Option<Winsize>,
|
||||
) -> Result<(), Failure> {
|
||||
self.attached.push((id, exec.clone(), size));
|
||||
match &self.attach_failure {
|
||||
Some(failure) => Err(failure.clone()),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn mount_images(&mut self, images: &[Image]) -> Result<(), Failure> {
|
||||
self.order.push("images");
|
||||
self.images.push(images.to_vec());
|
||||
|
||||
Reference in New Issue
Block a user