From 35be9f49b43347a2c0a5077348e5de7af0b7608b Mon Sep 17 00:00:00 2001 From: Tochukwu Nkemdilim <11903253+toksdotdev@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:41:28 -0400 Subject: [PATCH] fix(krun): air-gap guests by default; opt-in TSI inet hijack when no virtio-net device was configured, configure_vsock auto-enabled TsiFlags::HIJACK_INET so the guest's inet socket calls were transparently bridged to the host through vsock. that silently defeated air-gap setups: the guest had no network interface, yet its tcp/udp traffic still reached the outside world. flipped the default so guests with no virtio-net are air-gapped, and added MachineBuilder::enable_inet_hijack(bool) for callers that still want the TSI fallback. reported in superradcompany/microsandbox#645. BREAKING CHANGE: callers that previously relied on the implicit TSI fallback (no virtio-net configured) must now call .machine(|m| m.enable_inet_hijack(true)) to keep that behavior. --- src/krun/src/api/builder.rs | 1 + src/krun/src/api/builders.rs | 22 +++++++ src/krun/src/api/vm.rs | 107 +++++++++++++++++++++++++++-------- 3 files changed, 106 insertions(+), 24 deletions(-) diff --git a/src/krun/src/api/builder.rs b/src/krun/src/api/builder.rs index 5672ec62d..f774514e7 100644 --- a/src/krun/src/api/builder.rs +++ b/src/krun/src/api/builder.rs @@ -532,6 +532,7 @@ impl VmBuilder { self.exit_observers, exit_evt, exit_code, + self.machine.enable_inet_hijack, )) } } diff --git a/src/krun/src/api/builders.rs b/src/krun/src/api/builders.rs index 76db4e0c4..d6df9ab81 100644 --- a/src/krun/src/api/builders.rs +++ b/src/krun/src/api/builders.rs @@ -44,6 +44,7 @@ pub struct MachineBuilder { pub(crate) nested_virt: bool, pub(crate) split_irqchip: bool, pub(crate) vsock: bool, + pub(crate) enable_inet_hijack: bool, } //-------------------------------------------------------------------------------------------------- @@ -334,6 +335,7 @@ impl MachineBuilder { nested_virt: false, split_irqchip: false, vsock: false, + enable_inet_hijack: false, } } @@ -384,6 +386,26 @@ impl MachineBuilder { self.vsock = enabled; self } + + /// Enable the automatic TSI INET hijack fallback. + /// + /// When set to `true` and no virtio-net device is configured, + /// `TsiFlags::HIJACK_INET` is enabled so the guest's INET socket + /// calls are transparently bridged to the host through vsock. This + /// is useful for guests that need outbound connectivity without the + /// caller setting up a virtio-net backend. + /// + /// Defaults to `false` — guests with no virtio-net are air-gapped + /// by default. Callers must opt in to TSI when they want host + /// network access without an explicit network device. + /// + /// Note: `HIJACK_UNIX` (used by virtio-fs on Linux for AF_UNIX + /// hijack) is gated on `HIJACK_INET` already being set, so leaving + /// INET hijack off also leaves the UNIX hijack auto-enable path off. + pub fn enable_inet_hijack(mut self, enabled: bool) -> Self { + self.enable_inet_hijack = enabled; + self + } } impl Default for MachineBuilder { diff --git a/src/krun/src/api/vm.rs b/src/krun/src/api/vm.rs index fee208e0d..22bf0709f 100644 --- a/src/krun/src/api/vm.rs +++ b/src/krun/src/api/vm.rs @@ -51,6 +51,11 @@ pub struct Vm { exit_evt: EventFd, /// Shared exit code — written by the VMM, readable by exit observers. exit_code: Arc, + /// Opt in to the automatic `TsiFlags::HIJACK_INET` fallback that + /// bridges guest INET sockets to the host via vsock when no + /// virtio-net device is configured. Set via + /// [`MachineBuilder::enable_inet_hijack`](super::builders::MachineBuilder::enable_inet_hijack). + enable_inet_hijack: bool, /// Keeps the libkrunfw library loaded so kernel memory pointers remain valid. _krunfw_library: Option, } @@ -75,6 +80,7 @@ impl Vm { exit_observers: Vec>, exit_evt: EventFd, exit_code: Arc, + enable_inet_hijack: bool, ) -> Self { Self { vmr, @@ -89,6 +95,7 @@ impl Vm { exit_observers, exit_evt, exit_code, + enable_inet_hijack, _krunfw_library: None, } } @@ -259,31 +266,13 @@ impl Vm { /// Configure the vsock device. /// /// The device is only attached when actually needed — either because the - /// caller explicitly requested it (`VmBuilder::vsock(true)`), or because - /// TSI needs it as a transport (no virtio-net → HIJACK_INET; single root - /// virtio-fs on Linux → HIJACK_UNIX). This keeps the per-VM IRQ/MMIO - /// budget free when nothing actually uses vsock. + /// caller explicitly requested it (`MachineBuilder::vsock(true)`), or + /// because the caller opted in to TSI as a transport + /// (`MachineBuilder::enable_inet_hijack(true)` with no virtio-net → + /// HIJACK_INET; single root virtio-fs on Linux → HIJACK_UNIX). This + /// keeps the per-VM IRQ/MMIO budget free when nothing uses vsock. fn configure_vsock(&mut self) -> Result<()> { - use devices::virtio::TsiFlags; - - let mut tsi_flags = TsiFlags::empty(); - - // Enable TSI if no virtio-net configured - #[cfg(feature = "net")] - if self.vmr.net.list.is_empty() { - tsi_flags |= TsiFlags::HIJACK_INET; - } - - #[cfg(not(feature = "net"))] - { - tsi_flags |= TsiFlags::HIJACK_INET; - } - - // Enable TSI for AF_UNIX if single root virtio-fs - #[cfg(not(feature = "tee"))] - { - tsi_flags = self.maybe_enable_hijack_unix(tsi_flags); - } + let tsi_flags = self.compute_tsi_flags(); if !self.vmr.request_vsock && tsi_flags.is_empty() { return Ok(()); @@ -304,6 +293,38 @@ impl Vm { Ok(()) } + /// Decide which `TsiFlags` should be enabled for this VM. + /// + /// Extracted from [`configure_vsock`](Self::configure_vsock) so the + /// flag-selection logic can be exercised by unit tests without + /// touching `VmResources::set_vsock_device`. + fn compute_tsi_flags(&self) -> devices::virtio::TsiFlags { + use devices::virtio::TsiFlags; + + let mut tsi_flags = TsiFlags::empty(); + + // Enable TSI INET hijack as a fallback when no virtio-net is + // configured and the caller opted in via + // `MachineBuilder::enable_inet_hijack(true)`. Default is air-gap. + #[cfg(feature = "net")] + if self.enable_inet_hijack && self.vmr.net.list.is_empty() { + tsi_flags |= TsiFlags::HIJACK_INET; + } + + #[cfg(not(feature = "net"))] + if self.enable_inet_hijack { + tsi_flags |= TsiFlags::HIJACK_INET; + } + + // Enable TSI for AF_UNIX if single root virtio-fs + #[cfg(not(feature = "tee"))] + { + tsi_flags = self.maybe_enable_hijack_unix(tsi_flags); + } + + tsi_flags + } + fn get_exec_path(&self) -> String { self.exec_path .as_ref() @@ -439,6 +460,10 @@ mod tests { use vmm::vmm_config::fs::FsDeviceConfig; fn make_vm() -> Vm { + make_vm_with(false) + } + + fn make_vm_with(enable_inet_hijack: bool) -> Vm { Vm::new( VmResources::default(), Some("debug loglevel=7".to_string()), @@ -452,6 +477,7 @@ mod tests { Vec::new(), EventFd::new(EFD_NONBLOCK).unwrap(), Arc::new(AtomicI32::new(i32::MAX)), + enable_inet_hijack, ) } @@ -500,4 +526,37 @@ mod tests { assert!(!flags.contains(TsiFlags::HIJACK_UNIX)); } + + #[test] + fn compute_tsi_flags_air_gaps_by_default_with_no_net() { + let vm = make_vm(); + let flags = vm.compute_tsi_flags(); + assert!(!flags.contains(TsiFlags::HIJACK_INET)); + } + + #[test] + fn compute_tsi_flags_enables_inet_hijack_when_opted_in() { + let vm = make_vm_with(true); + let flags = vm.compute_tsi_flags(); + assert!(flags.contains(TsiFlags::HIJACK_INET)); + } + + #[cfg(all(not(feature = "tee"), not(target_os = "macos")))] + #[test] + fn compute_tsi_flags_unix_hijack_follows_inet_hijack() { + let mut vm = make_vm(); + vm.vmr.fs.push(FsDeviceConfig { + fs_id: "/dev/root".to_string(), + shared_dir: "/tmp/rootfs".to_string(), + shm_size: None, + allow_root_dir_delete: false, + }); + + let flags = vm.compute_tsi_flags(); + + // `maybe_enable_hijack_unix` gates UNIX hijack on INET hijack + // already being set, so the default (no opt-in) drops both. + assert!(!flags.contains(TsiFlags::HIJACK_INET)); + assert!(!flags.contains(TsiFlags::HIJACK_UNIX)); + } }