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