1use std::collections::HashMap;
2use std::io::{BufReader, BufWriter, Read, Write};
3use std::net::{Shutdown, TcpListener, TcpStream};
4use std::os::fd::AsRawFd;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::{Arc, Mutex};
7use std::time::Duration;
8
9use anyhow::{bail, Context, Result};
10use crossbeam_channel::Receiver;
11
12use crate::backend::network::FileHandleNetworkAttachment;
13use crate::backend::terminal;
14use crate::backend::*;
15
16use vm_proto::{
17 frame, ChmodRequest, CopyRequest, DiscardRequest, ExecRequest, ForwardRequest, ForwardResponse,
18 FsOkResponse, MkdirRequest, MountRequest, MountResponse, PortMapping, ReadDirRequest,
19 ReadDirResponse, ReadFileRequest, RemoveRequest, RenameRequest, StatRequest, StatResponse,
20 WatchRequest, WriteFileRequest, WriteFileResponse, VSOCK_PORT, VSOCK_PORT_FORWARD,
21};
22
23pub fn command_line(verbose: bool, console: bool) -> String {
36 let base = "root=/dev/vda rw init=/usr/bin/vm-guest mitigations=off printk.time=1";
37 if verbose {
38 format!("console={} {}", CONSOLE_DEVICE, base)
39 } else if console {
40 format!("console={} {} quiet", CONSOLE_DEVICE, base)
41 } else {
42 format!("{} quiet", base)
43 }
44}
45
46#[derive(Debug, Clone)]
49pub struct MountConfig {
50 pub host_path: String,
51 pub guest_path: String,
52 pub read_only: bool,
53}
54
55pub struct VmConfigBuilder {
58 kernel: Option<String>,
59 rootfs: Option<String>,
60 initrd: Option<String>,
61 cpus: usize,
62 memory_mb: u64,
63 console: bool,
64 verbose: bool,
65 network_fd: Option<i32>,
66 nbd_uri: Option<String>,
67 mounts: Vec<MountConfig>,
68 sync_disk: bool,
69}
70
71impl VmConfigBuilder {
72 pub(crate) fn new() -> Self {
73 VmConfigBuilder {
74 kernel: None,
75 rootfs: None,
76 initrd: None,
77 cpus: 2,
78 memory_mb: 2048,
79 console: true,
80 verbose: false,
81 network_fd: None,
82 nbd_uri: None,
83 mounts: Vec::new(),
84 sync_disk: true,
85 }
86 }
87
88 pub fn sync_disk(mut self, enabled: bool) -> Self {
93 self.sync_disk = enabled;
94 self
95 }
96
97 pub fn console(mut self, enabled: bool) -> Self {
101 self.console = enabled;
102 self
103 }
104
105 pub fn verbose(mut self, enabled: bool) -> Self {
108 self.verbose = enabled;
109 self
110 }
111
112 pub fn kernel(mut self, path: impl Into<String>) -> Self {
113 self.kernel = Some(path.into());
114 self
115 }
116
117 pub fn rootfs(mut self, path: impl Into<String>) -> Self {
118 self.rootfs = Some(path.into());
119 self
120 }
121
122 pub fn initrd(mut self, path: impl Into<String>) -> Self {
123 self.initrd = Some(path.into());
124 self
125 }
126
127 pub fn cpus(mut self, n: usize) -> Self {
128 self.cpus = n;
129 self
130 }
131
132 pub fn memory_mb(mut self, mb: u64) -> Self {
133 self.memory_mb = mb;
134 self
135 }
136
137 pub fn network_fd(mut self, fd: i32) -> Self {
139 self.network_fd = Some(fd);
140 self
141 }
142
143 pub fn nbd_uri(mut self, uri: impl Into<String>) -> Self {
145 self.nbd_uri = Some(uri.into());
146 self
147 }
148
149 pub fn mount(mut self, config: MountConfig) -> Self {
151 self.mounts.push(config);
152 self
153 }
154
155 pub fn build(self) -> Result<Sandbox> {
156 let kernel_path = self.kernel.context("kernel path is required")?;
157 let rootfs_path = self.rootfs.context("rootfs path is required")?;
158
159 if !VirtualMachine::supported() {
160 bail!("Virtualization is not supported on this machine");
161 }
162
163 let boot_loader = LinuxBootLoader::new_with_kernel(&kernel_path);
164 if let Some(ref initrd) = self.initrd {
165 boot_loader.set_initrd(initrd);
166 }
167
168 boot_loader.set_command_line(&command_line(self.verbose, self.console));
169
170 let memory_bytes = self.memory_mb * 1024 * 1024;
171 let config = VirtualMachineConfiguration::new(&boot_loader, self.cpus, memory_bytes);
172
173 if self.console || self.verbose {
176 let serial_attachment = if self.console {
177 FileHandleSerialAttachment::new(
178 std::io::stdin().as_raw_fd(),
179 std::io::stdout().as_raw_fd(),
180 )
181 } else {
182 FileHandleSerialAttachment::new_write_only(std::io::stderr().as_raw_fd())
183 };
184 let serial = VirtioConsoleSerialPort::new_with_attachment(&serial_attachment);
185 config.set_serial_ports(&[serial]);
186 }
187
188 let nbd_attachment;
189 let disk_attachment;
190 let block_device = if let Some(ref uri) = self.nbd_uri {
191 nbd_attachment = NbdAttachment::new(uri, 30.0, false)
192 .map_err(|e| anyhow::anyhow!("Failed to create NBD attachment: {}", e))?;
193 VirtioBlockDevice::new(&nbd_attachment)
194 } else {
195 let sync_mode = if self.sync_disk {
196 DiskImageSynchronizationMode::Fsync
197 } else {
198 DiskImageSynchronizationMode::None
199 };
200 disk_attachment = DiskImageAttachment::new_with_options(
201 &rootfs_path,
202 false,
203 DiskImageCachingMode::Cached,
204 sync_mode,
205 )
206 .map_err(|e| anyhow::anyhow!("Failed to create disk attachment: {}", e))?;
207 VirtioBlockDevice::new(&disk_attachment)
208 };
209 config.set_storage_devices(&[&block_device]);
210
211 if let Some(fd) = self.network_fd {
212 let net_attachment = FileHandleNetworkAttachment::new(fd);
213 let net_device = VirtioNetworkDevice::new_with_attachment(&net_attachment);
214 net_device.set_mac_address(&MACAddress::random_local());
215 config.set_network_devices(&[net_device]);
216 }
217
218 let mut fs_devices: Vec<VirtioFileSystemDevice> = Vec::new();
220 let mut mount_requests: Vec<MountRequest> = Vec::new();
221
222 for (i, m) in self.mounts.iter().enumerate() {
223 let tag = format!("mount{}", i);
224 let shared_dir = SharedDirectory::new(&m.host_path, m.read_only);
225 fs_devices.push(VirtioFileSystemDevice::new(&tag, &shared_dir));
226 mount_requests.push(MountRequest {
227 tag,
228 guest_path: m.guest_path.clone(),
229 read_only: m.read_only,
230 });
231 }
232
233 if !fs_devices.is_empty() {
234 config.set_directory_sharing_devices(&fs_devices);
235 }
236
237 let socket_device = VirtioSocketDevice::new();
238 config.set_socket_devices(&[socket_device]);
239
240 config.set_entropy_devices(&[VirtioEntropyDevice::new()]);
241
242 config
243 .validate()
244 .map_err(|e| anyhow::anyhow!("VM configuration invalid: {}", e))?;
245
246 Ok(Sandbox {
247 vm: Arc::new(VirtualMachine::new(&config)),
248 mounts: Mutex::new(mount_requests),
249 })
250 }
251}
252
253pub struct Sandbox {
256 vm: Arc<VirtualMachine>,
257 mounts: Mutex<Vec<MountRequest>>,
258}
259
260impl Sandbox {
261 pub fn builder() -> VmConfigBuilder {
262 VmConfigBuilder::new()
263 }
264
265 pub fn start(&self) -> Result<()> {
266 self.vm
267 .start()
268 .map_err(|e| anyhow::anyhow!("Failed to start VM: {}", e))
269 }
270
271 pub fn stop(&self) -> Result<()> {
272 self.vm
273 .stop()
274 .map_err(|e| anyhow::anyhow!("Failed to stop VM: {}", e))
275 }
276
277 pub fn wait_ready(&self) -> Result<()> {
279 let stream = self.connect_vsock()?;
280 drop(stream);
281 Ok(())
282 }
283
284 pub fn state_channel(&self) -> Receiver<VmState> {
285 self.vm.state_channel()
286 }
287
288 fn send_mount_requests(&self, writer: &mut impl Write, reader: &mut impl Read) -> Result<()> {
291 let mounts = std::mem::take(&mut *self.mounts.lock().unwrap());
292 for req in &mounts {
293 frame::send_json(writer, frame::MOUNT_REQ, &req).context("sending mount request")?;
294 let (_msg_type, payload) = frame::read_frame(reader)
295 .context("reading mount response")?
296 .context("guest closed connection during mount init")?;
297 let resp: MountResponse = match serde_json::from_slice(&payload) {
298 Ok(r) => r,
299 Err(_) => {
300 bail!(
301 "guest does not support directory mounts. \
302 Run `hanzo-vm upgrade` and recreate the checkpoint to enable --mount."
303 );
304 }
305 };
306 if !resp.ok {
307 bail!(
308 "mount failed: {} -> {}: {}",
309 req.tag,
310 req.guest_path,
311 resp.error.unwrap_or_else(|| "unknown error".into())
312 );
313 }
314 }
315 Ok(())
316 }
317
318 pub fn exec(
321 &self,
322 argv: &[impl AsRef<str>],
323 stdout: &mut impl Write,
324 stderr: &mut impl Write,
325 ) -> Result<i32> {
326 self.exec_with_env(argv, &HashMap::new(), stdout, stderr)
327 }
328
329 pub fn exec_with_env(
330 &self,
331 argv: &[impl AsRef<str>],
332 env: &HashMap<String, String>,
333 stdout: &mut impl Write,
334 stderr: &mut impl Write,
335 ) -> Result<i32> {
336 let stream = self.connect_vsock()?;
337 let mut writer = stream.try_clone()?;
338 let mut reader = stream;
339
340 self.send_mount_requests(&mut writer, &mut reader)?;
341
342 let req = ExecRequest {
343 argv: argv.iter().map(|s| s.as_ref().to_string()).collect(),
344 env: env.clone(),
345 tty: None,
346 rows: None,
347 cols: None,
348 cwd: None,
349 };
350 frame::send_json(&mut writer, frame::EXEC_REQ, &req)?;
351
352 let mut exit_code = 0;
353
354 loop {
355 match frame::read_frame(&mut reader).context("reading vsock response")? {
356 Some((frame::STDOUT, payload)) => {
357 stdout.write_all(&payload)?;
358 }
359 Some((frame::STDERR, payload)) => {
360 stderr.write_all(&payload)?;
361 }
362 Some((frame::EXIT, payload)) => {
363 exit_code = frame::parse_exit_code(&payload).unwrap_or(0);
364 break;
365 }
366 Some((frame::ERROR, payload)) => {
367 let msg = String::from_utf8_lossy(&payload);
368 write!(stderr, "guest error: {}", msg)?;
369 exit_code = 1;
370 break;
371 }
372 Some(_) => {} None => break, }
375 }
376
377 Ok(exit_code)
378 }
379
380 pub fn reach(&self) -> Result<()> {
390 self.connect_vsock().map(drop)
391 }
392
393 pub fn attest(&self, bind: &[u8; 64]) -> Result<vm_measure::attest::Status> {
404 let stream = self.connect_vsock()?;
405 let mut writer = stream.try_clone()?;
406 let mut reader = stream;
407 reader.set_read_timeout(Some(Duration::from_secs(5)))?;
408
409 self.send_mount_requests(&mut writer, &mut reader)?;
410
411 frame::write_frame(&mut writer, frame::ATTEST_REQ, bind)?;
412 match frame::read_frame(&mut reader).context("reading attest response")? {
413 Some((frame::ATTEST_RESP, payload)) => {
414 serde_json::from_slice(&payload).context("reading the guest's attestation status")
415 }
416 Some((frame::ERROR, payload)) => bail!("{}", String::from_utf8_lossy(&payload)),
417 Some((other, _)) => bail!("unexpected frame type 0x{other:02x} in attest response"),
418 None => bail!("guest closed connection during attest"),
419 }
420 }
421
422 pub fn read_file(&self, path: &str) -> Result<Vec<u8>> {
423 let stream = self.connect_vsock()?;
424 let mut writer = stream.try_clone()?;
425 let mut reader = stream;
426
427 self.send_mount_requests(&mut writer, &mut reader)?;
428
429 let req = ReadFileRequest {
430 path: path.to_string(),
431 };
432 frame::send_json(&mut writer, frame::READ_FILE_REQ, &req)?;
433
434 match frame::read_frame(&mut reader).context("reading read_file response")? {
435 Some((frame::READ_FILE_RESP, payload)) => Ok(payload),
436 Some((frame::ERROR, payload)) => {
437 bail!("{}", String::from_utf8_lossy(&payload));
438 }
439 Some((other, _)) => {
440 bail!(
441 "unexpected frame type 0x{:02x} in read_file response",
442 other
443 );
444 }
445 None => bail!("guest closed connection during read_file"),
446 }
447 }
448
449 pub fn write_file(&self, path: &str, content: &[u8]) -> Result<()> {
450 let stream = self.connect_vsock()?;
451 let mut writer = stream.try_clone()?;
452 let mut reader = stream;
453
454 self.send_mount_requests(&mut writer, &mut reader)?;
455
456 let req = WriteFileRequest {
457 path: path.to_string(),
458 len: content.len() as u64,
459 };
460 frame::send_json(&mut writer, frame::WRITE_FILE_REQ, &req)?;
461 frame::write_frame(&mut writer, frame::WRITE_FILE_DATA, content)?;
462
463 let (_msg_type, payload) = frame::read_frame(&mut reader)
464 .context("reading write_file response")?
465 .context("guest closed connection during write_file")?;
466
467 let resp: WriteFileResponse =
468 serde_json::from_slice(&payload).context("parsing write_file response")?;
469
470 if !resp.ok {
471 bail!(
472 "write_file failed: {}",
473 resp.error.unwrap_or_else(|| "unknown error".into())
474 );
475 }
476
477 Ok(())
478 }
479
480 fn void_fs_op(&self, req_frame: u8, req: &impl serde::Serialize) -> Result<()> {
482 let stream = self.connect_vsock()?;
483 let mut writer = stream.try_clone()?;
484 let mut reader = stream;
485
486 self.send_mount_requests(&mut writer, &mut reader)?;
487
488 frame::send_json(&mut writer, req_frame, req)?;
489
490 match frame::read_frame(&mut reader).context("reading fs op response")? {
491 Some((frame::FS_OK_RESP, payload)) => {
492 let resp: FsOkResponse =
493 serde_json::from_slice(&payload).context("parsing fs ok response")?;
494 if !resp.ok {
495 bail!("{}", resp.error.unwrap_or_else(|| "unknown error".into()));
496 }
497 Ok(())
498 }
499 Some((frame::ERROR, payload)) => {
500 bail!("{}", String::from_utf8_lossy(&payload));
501 }
502 Some((other, _)) => {
503 bail!("unexpected frame type 0x{:02x}", other);
504 }
505 None => bail!("guest closed connection"),
506 }
507 }
508
509 pub fn mkdir(&self, path: &str, recursive: bool) -> Result<()> {
510 self.void_fs_op(
511 frame::MKDIR_REQ,
512 &MkdirRequest {
513 path: path.to_string(),
514 recursive,
515 },
516 )
517 }
518
519 pub fn download(
521 &self,
522 url: &str,
523 path: &str,
524 extract: bool,
525 strip_components: u32,
526 on_progress: impl Fn(vm_proto::DownloadProgress),
527 ) -> Result<()> {
528 let stream = self.connect_vsock()?;
529 let mut writer = stream.try_clone()?;
530 let mut reader = stream;
531
532 self.send_mount_requests(&mut writer, &mut reader)?;
533
534 let req = vm_proto::DownloadRequest {
535 url: url.to_string(),
536 path: path.to_string(),
537 extract,
538 strip_components,
539 };
540 frame::send_json(&mut writer, frame::DOWNLOAD_REQ, &req)?;
541
542 loop {
544 match frame::read_frame(&mut reader)? {
545 Some((frame::DOWNLOAD_PROGRESS, payload)) => {
546 if let Ok(progress) =
547 serde_json::from_slice::<vm_proto::DownloadProgress>(&payload)
548 {
549 on_progress(progress);
550 }
551 }
552 Some((frame::FS_OK_RESP, payload)) => {
553 let resp: FsOkResponse = serde_json::from_slice(&payload)?;
554 if !resp.ok {
555 bail!("{}", resp.error.unwrap_or_else(|| "download failed".into()));
556 }
557 return Ok(());
558 }
559 Some((frame::ERROR, payload)) => {
560 bail!("{}", String::from_utf8_lossy(&payload));
561 }
562 Some((other, _)) => {
563 bail!("unexpected frame 0x{:02x} during download", other);
564 }
565 None => bail!("connection closed during download"),
566 }
567 }
568 }
569
570 pub fn read_dir(&self, path: &str) -> Result<ReadDirResponse> {
571 let stream = self.connect_vsock()?;
572 let mut writer = stream.try_clone()?;
573 let mut reader = stream;
574
575 self.send_mount_requests(&mut writer, &mut reader)?;
576
577 let req = ReadDirRequest {
578 path: path.to_string(),
579 };
580 frame::send_json(&mut writer, frame::READ_DIR_REQ, &req)?;
581
582 match frame::read_frame(&mut reader).context("reading read_dir response")? {
583 Some((frame::READ_DIR_RESP, payload)) => {
584 Ok(serde_json::from_slice(&payload).context("parsing read_dir response")?)
585 }
586 Some((frame::ERROR, payload)) => {
587 bail!("{}", String::from_utf8_lossy(&payload));
588 }
589 Some((other, _)) => {
590 bail!("unexpected frame type 0x{:02x} in read_dir response", other);
591 }
592 None => bail!("guest closed connection during read_dir"),
593 }
594 }
595
596 pub fn stat(&self, path: &str) -> Result<StatResponse> {
597 let stream = self.connect_vsock()?;
598 let mut writer = stream.try_clone()?;
599 let mut reader = stream;
600
601 self.send_mount_requests(&mut writer, &mut reader)?;
602
603 let req = StatRequest {
604 path: path.to_string(),
605 };
606 frame::send_json(&mut writer, frame::STAT_REQ, &req)?;
607
608 match frame::read_frame(&mut reader).context("reading stat response")? {
609 Some((frame::STAT_RESP, payload)) => {
610 Ok(serde_json::from_slice(&payload).context("parsing stat response")?)
611 }
612 Some((frame::ERROR, payload)) => {
613 bail!("{}", String::from_utf8_lossy(&payload));
614 }
615 Some((other, _)) => {
616 bail!("unexpected frame type 0x{:02x} in stat response", other);
617 }
618 None => bail!("guest closed connection during stat"),
619 }
620 }
621
622 pub fn remove(&self, path: &str, recursive: bool) -> Result<()> {
623 self.void_fs_op(
624 frame::REMOVE_REQ,
625 &RemoveRequest {
626 path: path.to_string(),
627 recursive,
628 },
629 )
630 }
631
632 pub fn discard_overlay(&self, path: &str) -> Result<()> {
635 self.void_fs_op(
636 frame::DISCARD_REQ,
637 &DiscardRequest {
638 path: path.to_string(),
639 },
640 )
641 }
642
643 pub fn rename(&self, old_path: &str, new_path: &str) -> Result<()> {
644 self.void_fs_op(
645 frame::RENAME_REQ,
646 &RenameRequest {
647 old_path: old_path.to_string(),
648 new_path: new_path.to_string(),
649 },
650 )
651 }
652
653 pub fn copy(&self, src: &str, dst: &str, recursive: bool) -> Result<()> {
654 self.void_fs_op(
655 frame::COPY_REQ,
656 &CopyRequest {
657 src: src.to_string(),
658 dst: dst.to_string(),
659 recursive,
660 },
661 )
662 }
663
664 pub fn chmod(&self, path: &str, mode: u32) -> Result<()> {
665 self.void_fs_op(
666 frame::CHMOD_REQ,
667 &ChmodRequest {
668 path: path.to_string(),
669 mode,
670 },
671 )
672 }
673
674 pub fn open_exec(
678 &self,
679 argv: &[impl AsRef<str>],
680 env: &HashMap<String, String>,
681 cwd: Option<&str>,
682 ) -> Result<TcpStream> {
683 let stream = self.connect_vsock()?;
684 let mut writer = stream.try_clone()?;
685 let mut reader = stream.try_clone()?;
686
687 self.send_mount_requests(&mut writer, &mut reader)?;
688
689 let req = ExecRequest {
690 argv: argv.iter().map(|s| s.as_ref().to_string()).collect(),
691 env: env.clone(),
692 tty: None,
693 rows: None,
694 cols: None,
695 cwd: cwd.map(|s| s.to_string()),
696 };
697 frame::send_json(&mut writer, frame::EXEC_REQ, &req)?;
698
699 Ok(stream)
700 }
701
702 pub fn open_shell(
707 &self,
708 argv: &[impl AsRef<str>],
709 env: &HashMap<String, String>,
710 rows: u16,
711 cols: u16,
712 ) -> Result<TcpStream> {
713 self.open_shell_with_cwd(argv, env, rows, cols, None)
714 }
715
716 pub fn open_shell_with_cwd(
717 &self,
718 argv: &[impl AsRef<str>],
719 env: &HashMap<String, String>,
720 rows: u16,
721 cols: u16,
722 cwd: Option<&str>,
723 ) -> Result<TcpStream> {
724 let stream = self.connect_vsock()?;
725 let mut writer = stream.try_clone()?;
726 let mut reader = stream.try_clone()?;
727
728 self.send_mount_requests(&mut writer, &mut reader)?;
729
730 let req = ExecRequest {
731 argv: argv.iter().map(|s| s.as_ref().to_string()).collect(),
732 env: env.clone(),
733 tty: Some(true),
734 rows: Some(rows),
735 cols: Some(cols),
736 cwd: cwd.map(|s| s.to_string()),
737 };
738 frame::send_json(&mut writer, frame::EXEC_REQ, &req)?;
739
740 Ok(stream)
741 }
742
743 pub fn open_watch(&self, path: &str, recursive: bool) -> Result<TcpStream> {
746 let stream = self.connect_vsock()?;
747 let mut writer = stream.try_clone()?;
748 let mut reader = stream.try_clone()?;
749
750 self.send_mount_requests(&mut writer, &mut reader)?;
751
752 let req = WatchRequest {
753 path: path.to_string(),
754 recursive,
755 };
756 frame::send_json(&mut writer, frame::WATCH_REQ, &req)?;
757
758 Ok(stream)
759 }
760
761 pub fn shell(&self, argv: &[impl AsRef<str>], env: &HashMap<String, String>) -> Result<i32> {
766 let stdin_fd = std::io::stdin().as_raw_fd();
767 let (rows, cols) = terminal::terminal_size(stdin_fd);
768
769 let stream = self.connect_vsock()?;
770 let mut writer = stream.try_clone()?;
771 let mut reader = stream;
772
773 self.send_mount_requests(&mut writer, &mut reader)?;
775
776 let req = ExecRequest {
778 argv: argv.iter().map(|s| s.as_ref().to_string()).collect(),
779 env: env.clone(),
780 tty: Some(true),
781 rows: Some(rows),
782 cols: Some(cols),
783 cwd: None,
784 };
785 frame::send_json(&mut writer, frame::EXEC_REQ, &req)?;
786
787 let _raw_guard = terminal::TerminalState::enter_raw_mode(stdin_fd);
789
790 let (relay, shutdown_signal) =
792 terminal::StdinRelay::new(stdin_fd).expect("failed to init stdin relay");
793
794 let exit_code = Arc::new(Mutex::new(0i32));
795
796 let mut vsock_writer = writer.try_clone()?;
798 let stdin_thread = std::thread::spawn(move || {
799 let mut buf = [0u8; 4096];
800 loop {
801 match relay.wait() {
802 terminal::StdinEvent::Ready => {
803 let n = terminal::read_raw(stdin_fd, &mut buf);
804 if n == 0 {
805 break;
806 }
807 if frame::write_frame(&mut vsock_writer, frame::STDIN, &buf[..n]).is_err() {
808 break;
809 }
810 }
811 terminal::StdinEvent::Resize => {
812 let (rows, cols) = terminal::terminal_size(stdin_fd);
813 let payload = frame::resize_payload(rows, cols);
814 if frame::write_frame(&mut vsock_writer, frame::RESIZE, &payload).is_err() {
815 break;
816 }
817 }
818 terminal::StdinEvent::Shutdown => break,
819 }
820 }
821 });
822
823 let exit_code_b = exit_code.clone();
827 let vsock_thread = std::thread::spawn(move || {
828 let mut reader = BufReader::new(reader);
829 let mut stdout = BufWriter::new(std::io::stdout());
830 loop {
831 match frame::read_frame(&mut reader) {
832 Ok(Some((frame::STDOUT, payload))) => {
833 let _ = stdout.write_all(&payload);
834 if reader.buffer().is_empty() {
839 let _ = stdout.flush();
840 }
841 }
842 Ok(Some((frame::EXIT, payload))) => {
843 let _ = stdout.flush();
844 *exit_code_b.lock().unwrap() =
845 frame::parse_exit_code(&payload).unwrap_or(0);
846 break;
847 }
848 Ok(Some((frame::ERROR, payload))) => {
849 let _ = stdout.flush();
850 let msg = String::from_utf8_lossy(&payload);
851 let _ = std::io::stderr()
852 .write_all(format!("guest error: {}\r\n", msg).as_bytes());
853 *exit_code_b.lock().unwrap() = 1;
854 break;
855 }
856 Ok(Some(_)) => {} Ok(None) | Err(_) => break,
858 }
859 }
860 let _ = stdout.flush();
861 shutdown_signal.signal();
862 });
863
864 let _ = vsock_thread.join();
866 let _ = stdin_thread.join();
867
868 let code = *exit_code.lock().unwrap();
871 Ok(code)
872 }
873
874 pub fn start_port_forwarding(&self, forwards: &[PortMapping]) -> Result<PortForwardHandle> {
877 let stop = Arc::new(AtomicBool::new(false));
878 let mut listeners = Vec::new();
879
880 for mapping in forwards {
881 let addr: std::net::SocketAddr = format!("127.0.0.1:{}", mapping.host_port)
882 .parse()
883 .with_context(|| format!("Invalid port {}", mapping.host_port))?;
884 let socket = socket2::Socket::new(socket2::Domain::IPV4, socket2::Type::STREAM, None)
885 .with_context(|| {
886 format!("Failed to create socket for port {}", mapping.host_port)
887 })?;
888 socket.set_reuse_address(true)?;
889 socket
890 .bind(&addr.into())
891 .with_context(|| format!("Failed to bind port {}", mapping.host_port))?;
892 socket
893 .listen(128)
894 .with_context(|| format!("Failed to listen on port {}", mapping.host_port))?;
895 socket.set_nonblocking(true)?;
896 let tcp_listener: TcpListener = socket.into();
897
898 let guest_port = mapping.guest_port;
899 let vm = Arc::clone(&self.vm);
900 let stop_flag = stop.clone();
901
902 eprintln!(
903 "hanzo-vm: forwarding 127.0.0.1:{} -> guest:{}",
904 mapping.host_port, mapping.guest_port
905 );
906
907 let handle = std::thread::spawn(move || {
908 while !stop_flag.load(Ordering::Relaxed) {
909 match tcp_listener.accept() {
910 Ok((tcp_stream, _)) => {
911 let _ = tcp_stream.set_nonblocking(false);
914 let vm = Arc::clone(&vm);
915 std::thread::spawn(move || {
916 if let Err(e) =
917 handle_forward_connection(tcp_stream, &vm, guest_port)
918 {
919 tracing::debug!("port forward error: {}", e);
920 }
921 });
922 }
923 Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
924 std::thread::sleep(Duration::from_millis(50));
925 }
926 Err(e) => {
927 if !stop_flag.load(Ordering::Relaxed) {
928 tracing::debug!("accept error on port forward listener: {}", e);
929 }
930 break;
931 }
932 }
933 }
934 });
935
936 listeners.push(handle);
937 }
938
939 Ok(PortForwardHandle {
940 stop,
941 threads: listeners,
942 })
943 }
944
945 pub fn connect_forward(&self, guest_port: u16) -> Result<TcpStream> {
954 open_forward_stream(&self.vm, guest_port)
955 }
956
957 fn connect_vsock(&self) -> Result<TcpStream> {
958 let state_rx = self.vm.state_channel();
959 for attempt in 1..=1000 {
960 if let Ok(state) = state_rx.try_recv() {
962 match state {
963 VmState::Stopped => {
964 bail!("VM stopped during startup - check boot output above for errors")
965 }
966 VmState::Error => bail!("VM encountered an error during startup"),
967 _ => {}
968 }
969 }
970 match self.vm.connect_to_vsock_port(VSOCK_PORT) {
971 Ok(s) => {
972 let _ = s.set_nodelay(true);
973 return Ok(s);
974 }
975 Err(e) => {
976 if attempt == 1000 {
977 bail!(
978 "Failed to connect to guest after {} attempts: {}",
979 attempt,
980 e
981 );
982 }
983 tracing::debug!("vsock connect attempt {} failed: {}", attempt, e);
984 let interval = if attempt < 100 { 1 } else { 10 };
987 std::thread::sleep(Duration::from_millis(interval));
988 }
989 }
990 }
991 unreachable!()
992 }
993}
994
995pub struct PortForwardHandle {
1000 stop: Arc<AtomicBool>,
1001 threads: Vec<std::thread::JoinHandle<()>>,
1002}
1003
1004impl Drop for PortForwardHandle {
1005 fn drop(&mut self) {
1006 self.stop.store(true, Ordering::Relaxed);
1007 for thread in self.threads.drain(..) {
1008 let _ = thread.join();
1009 }
1010 }
1011}
1012
1013fn handle_forward_connection(
1014 tcp_stream: TcpStream,
1015 vm: &VirtualMachine,
1016 guest_port: u16,
1017) -> Result<()> {
1018 let vsock_stream = open_forward_stream(vm, guest_port)?;
1019 relay(tcp_stream, vsock_stream);
1021 Ok(())
1022}
1023
1024fn open_forward_stream(vm: &VirtualMachine, guest_port: u16) -> Result<TcpStream> {
1028 let mut vsock_stream = vm
1029 .connect_to_vsock_port(VSOCK_PORT_FORWARD)
1030 .map_err(|e| anyhow::anyhow!("vsock connect for port forward: {}", e))?;
1031 let _ = vsock_stream.set_nodelay(true);
1032
1033 let req = ForwardRequest { port: guest_port };
1035 frame::send_json(&mut vsock_stream, frame::FWD_REQ, &req)?;
1036
1037 let (_msg_type, payload) = frame::read_frame(&mut vsock_stream)
1039 .context("reading forward response")?
1040 .context("guest closed connection during forward handshake")?;
1041 let resp: ForwardResponse =
1042 serde_json::from_slice(&payload).context("parsing forward response")?;
1043
1044 if resp.status != "ok" {
1045 bail!(
1046 "guest refused forward: {}",
1047 resp.message.unwrap_or_default()
1048 );
1049 }
1050
1051 Ok(vsock_stream)
1052}
1053
1054fn relay(a: TcpStream, b: TcpStream) {
1055 let mut a_read = a.try_clone().expect("clone tcp stream");
1056 let mut b_write = b.try_clone().expect("clone vsock stream");
1057 let mut b_read = b;
1058 let mut a_write = a;
1059
1060 let t1 = std::thread::spawn(move || {
1061 let _ = std::io::copy(&mut a_read, &mut b_write);
1062 let _ = b_write.shutdown(Shutdown::Write);
1063 });
1064 let t2 = std::thread::spawn(move || {
1065 let _ = std::io::copy(&mut b_read, &mut a_write);
1066 let _ = a_write.shutdown(Shutdown::Write);
1067 });
1068 let _ = t1.join();
1069 let _ = t2.join();
1070}