Skip to content

Commit 7643a32

Browse files
authored
Merge pull request #270 from Dstack-TEE/passt
Support for using passt as network egress
2 parents ea7a9bc + aa8fa91 commit 7643a32

File tree

5 files changed

+232
-25
lines changed

5 files changed

+232
-25
lines changed

supervisor/client/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ impl SupervisorClient {
110110

111111
// Async API
112112
impl SupervisorClient {
113-
pub async fn deploy(&self, config: ProcessConfig) -> Result<()> {
113+
pub async fn deploy(&self, config: &ProcessConfig) -> Result<()> {
114114
self.http_request("POST", "/deploy", config).await
115115
}
116116

vmm/src/app.rs

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::config::{Config, Protocol};
1+
use crate::config::{Config, ProcessAnnotation, Protocol};
22

33
use anyhow::{bail, Context, Result};
44
use bon::Builder;
@@ -174,7 +174,6 @@ impl App {
174174
manifest,
175175
image,
176176
cid,
177-
networking: self.config.networking.clone(),
178177
workdir: vm_work_dir.path().to_path_buf(),
179178
gateway_enabled: app_compose.gateway_enabled(),
180179
};
@@ -213,6 +212,11 @@ impl App {
213212
vm_state.config.clone()
214213
};
215214
if !is_running {
215+
// Try to stop passt if already running
216+
if self.config.cvm.networking.is_passt() {
217+
self.supervisor.stop(&format!("passt-{}", id)).await.ok();
218+
}
219+
216220
let work_dir = self.work_dir(id);
217221
for path in [work_dir.serial_pty(), work_dir.qmp_socket()] {
218222
if path.symlink_metadata().is_ok() {
@@ -221,11 +225,13 @@ impl App {
221225
}
222226

223227
let devices = self.try_allocate_gpus(&vm_config.manifest)?;
224-
let process_config = vm_config.config_qemu(&work_dir, &self.config.cvm, &devices)?;
225-
self.supervisor
226-
.deploy(process_config)
227-
.await
228-
.with_context(|| format!("Failed to start VM {id}"))?;
228+
let processes = vm_config.config_qemu(&work_dir, &self.config.cvm, &devices)?;
229+
for process in processes {
230+
self.supervisor
231+
.deploy(&process)
232+
.await
233+
.with_context(|| format!("Failed to start process {}", process.id))?;
234+
}
229235

230236
let mut state = self.lock();
231237
let vm_state = state.get_mut(id).context("VM not found")?;
@@ -259,6 +265,16 @@ impl App {
259265
self.supervisor.stop(id).await?;
260266
}
261267
self.supervisor.remove(id).await?;
268+
if self.config.cvm.networking.is_passt() {
269+
let passt_id = format!("passt-{}", id);
270+
let info = self.supervisor.info(&passt_id).await.ok().flatten();
271+
if let Some(info) = info {
272+
if info.state.status.is_running() {
273+
self.supervisor.stop(&passt_id).await?;
274+
}
275+
self.supervisor.remove(&passt_id).await?;
276+
}
277+
}
262278
}
263279

264280
{
@@ -276,9 +292,14 @@ impl App {
276292
pub async fn reload_vms(&self) -> Result<()> {
277293
let vm_path = self.vm_dir();
278294
let running_vms = self.supervisor.list().await.context("Failed to list VMs")?;
295+
let running_vms: Vec<(ProcessAnnotation, _)> = running_vms
296+
.into_iter()
297+
.map(|p| (serde_json::from_str(&p.config.note).unwrap_or_default(), p))
298+
.collect();
279299
let occupied_cids = running_vms
280300
.iter()
281-
.flat_map(|p| p.config.cid.map(|cid| (p.config.id.clone(), cid)))
301+
.filter(|(note, _)| note.is_cvm())
302+
.flat_map(|(_, p)| p.config.cid.map(|cid| (p.config.id.clone(), cid)))
282303
.collect::<HashMap<_, _>>();
283304
{
284305
let mut state = self.lock();

vmm/src/app/qemu.rs

Lines changed: 141 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! QEMU related code
22
use crate::{
33
app::Manifest,
4-
config::{CvmConfig, GatewayConfig, Networking},
4+
config::{CvmConfig, GatewayConfig, Networking, PasstNetworking, ProcessAnnotation, Protocol},
55
};
66
use std::{collections::HashMap, os::unix::fs::PermissionsExt};
77
use std::{
@@ -54,7 +54,6 @@ pub struct VmConfig {
5454
pub manifest: Manifest,
5555
pub image: Image,
5656
pub cid: u32,
57-
pub networking: Networking,
5857
pub workdir: PathBuf,
5958
pub gateway_enabled: bool,
6059
}
@@ -220,12 +219,117 @@ impl VmState {
220219
}
221220

222221
impl VmConfig {
222+
fn config_passt(&self, workdir: &VmWorkDir, netcfg: &PasstNetworking) -> Result<ProcessConfig> {
223+
let PasstNetworking {
224+
passt_exec,
225+
interface,
226+
address,
227+
netmask,
228+
gateway,
229+
dns,
230+
map_host_loopback,
231+
map_guest_addr,
232+
no_map_gw,
233+
ipv4_only,
234+
} = netcfg;
235+
236+
let passt_socket = workdir.passt_socket();
237+
if passt_socket.exists() {
238+
fs_err::remove_file(&passt_socket).context("Failed to remove passt socket")?;
239+
}
240+
let passt_exec = if passt_exec.is_empty() {
241+
"passt"
242+
} else {
243+
passt_exec
244+
};
245+
246+
let passt_log = workdir.passt_log();
247+
248+
let mut passt_cmd = Command::new(passt_exec);
249+
passt_cmd.arg("--socket").arg(&passt_socket);
250+
passt_cmd.arg("--log-file").arg(&passt_log);
251+
252+
if !interface.is_empty() {
253+
passt_cmd.arg("--interface").arg(interface);
254+
}
255+
if !address.is_empty() {
256+
passt_cmd.arg("--address").arg(address);
257+
}
258+
if !netmask.is_empty() {
259+
passt_cmd.arg("--netmask").arg(netmask);
260+
}
261+
if !gateway.is_empty() {
262+
passt_cmd.arg("--gateway").arg(gateway);
263+
}
264+
for dns in dns {
265+
passt_cmd.arg("--dns").arg(dns);
266+
}
267+
if !map_host_loopback.is_empty() {
268+
passt_cmd.arg("--map-host-loopback").arg(map_host_loopback);
269+
}
270+
if !map_guest_addr.is_empty() {
271+
passt_cmd.arg("--map-guest-addr").arg(map_guest_addr);
272+
}
273+
if *no_map_gw {
274+
passt_cmd.arg("--no-map-gw");
275+
}
276+
if *ipv4_only {
277+
passt_cmd.arg("--ipv4-only");
278+
}
279+
// Group port mappings by protocol
280+
let mut tcp_ports = Vec::new();
281+
let mut udp_ports = Vec::new();
282+
283+
for pm in &self.manifest.port_map {
284+
let port_spec = format!("{}/{}:{}", pm.address, pm.from, pm.to);
285+
match pm.protocol {
286+
Protocol::Tcp => tcp_ports.push(port_spec),
287+
Protocol::Udp => udp_ports.push(port_spec),
288+
}
289+
}
290+
// Add TCP port forwarding if any
291+
if !tcp_ports.is_empty() {
292+
passt_cmd.arg("--tcp-ports").arg(tcp_ports.join(","));
293+
}
294+
// Add UDP port forwarding if any
295+
if !udp_ports.is_empty() {
296+
passt_cmd.arg("--udp-ports").arg(udp_ports.join(","));
297+
}
298+
passt_cmd.arg("-f").arg("-1");
299+
300+
let args = passt_cmd
301+
.get_args()
302+
.map(|arg| arg.to_string_lossy().to_string())
303+
.collect::<Vec<_>>();
304+
let stdout_path = workdir.passt_stdout();
305+
let stderr_path = workdir.passt_stderr();
306+
let note = ProcessAnnotation {
307+
kind: "passt".to_string(),
308+
live_for: Some(self.manifest.id.clone()),
309+
};
310+
let note = serde_json::to_string(&note)?;
311+
let process_config = ProcessConfig {
312+
id: format!("passt-{}", self.manifest.id),
313+
args,
314+
name: format!("passt-{}", self.manifest.name),
315+
command: passt_exec.to_string(),
316+
env: Default::default(),
317+
cwd: workdir.to_string_lossy().to_string(),
318+
stdout: stdout_path.to_string_lossy().to_string(),
319+
stderr: stderr_path.to_string_lossy().to_string(),
320+
pidfile: Default::default(),
321+
cid: None,
322+
note,
323+
};
324+
Ok(process_config)
325+
}
326+
223327
pub fn config_qemu(
224328
&self,
225329
workdir: impl AsRef<Path>,
226330
cfg: &CvmConfig,
227331
gpus: &GpuConfig,
228-
) -> Result<ProcessConfig> {
332+
) -> Result<Vec<ProcessConfig>> {
229333
let workdir = VmWorkDir::new(workdir);
230334
let serial_file = workdir.serial_file();
231335
let serial_pty = workdir.serial_pty();
@@ -304,12 +408,13 @@ impl VmConfig {
304408
}
305409
}
306410
}
411+
let mut processes = vec![];
307412
command
308413
.arg("-drive")
309414
.arg(format!("file={},if=none,id=hd1", hda_path.display()))
310415
.arg("-device")
311416
.arg("virtio-blk-pci,drive=hd1");
312-
let netdev = match &self.networking {
417+
let netdev = match &cfg.networking {
313418
Networking::User(netcfg) => {
314419
let mut netdev = format!(
315420
"user,id=net0,net={},dhcpstart={},restrict={}",
@@ -328,6 +433,16 @@ impl VmConfig {
328433
}
329434
netdev
330435
}
436+
Networking::Passt(netcfg) => {
437+
processes.push(
438+
self.config_passt(&workdir, netcfg)
439+
.context("Failed to configure passt")?,
440+
);
441+
format!(
442+
"stream,id=net0,server=off,addr.type=unix,addr.path={}",
443+
workdir.passt_socket().display()
444+
)
445+
}
331446
Networking::Custom(netcfg) => netcfg.netdev.clone(),
332447
};
333448
command.arg("-netdev").arg(netdev);
@@ -538,7 +653,10 @@ impl VmConfig {
538653
}
539654

540655
let command = cmd_args.remove(0);
541-
let note = "{}".to_string();
656+
let note = ProcessAnnotation {
657+
kind: "cvm".to_string(),
658+
live_for: None,
659+
};
542660
let note = serde_json::to_string(&note)?;
543661
let process_config = ProcessConfig {
544662
id: self.manifest.id.clone(),
@@ -553,8 +671,9 @@ impl VmConfig {
553671
cid: Some(self.cid),
554672
note,
555673
};
674+
processes.push(process_config);
556675

557-
Ok(process_config)
676+
Ok(processes)
558677
}
559678
}
560679

@@ -730,6 +849,22 @@ impl VmWorkDir {
730849
self.workdir.join("qmp.sock")
731850
}
732851

852+
pub fn passt_socket(&self) -> PathBuf {
853+
self.workdir.join("passt.sock")
854+
}
855+
856+
pub fn passt_stdout(&self) -> PathBuf {
857+
self.workdir.join("passt.stdout")
858+
}
859+
860+
pub fn passt_stderr(&self) -> PathBuf {
861+
self.workdir.join("passt.stderr")
862+
}
863+
864+
pub fn passt_log(&self) -> PathBuf {
865+
self.workdir.join("passt.log")
866+
}
867+
733868
pub fn path(&self) -> &Path {
734869
&self.workdir
735870
}

vmm/src/config.rs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ pub struct CvmConfig {
130130
pub qemu_pci_hole64_size: u64,
131131
/// QEMU hotplug_off
132132
pub qemu_hotplug_off: bool,
133+
134+
/// Networking configuration
135+
pub networking: Networking,
133136
}
134137

135138
#[derive(Debug, Clone, Deserialize)]
@@ -210,9 +213,6 @@ pub struct Config {
210213
/// Gateway configuration
211214
pub gateway: GatewayConfig,
212215

213-
/// Networking configuration
214-
pub networking: Networking,
215-
216216
/// Authentication configuration
217217
pub auth: AuthConfig,
218218

@@ -226,6 +226,23 @@ pub struct Config {
226226
pub key_provider: KeyProviderConfig,
227227
}
228228

229+
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
230+
pub struct ProcessAnnotation {
231+
#[serde(default)]
232+
pub kind: String,
233+
#[serde(default)]
234+
pub live_for: Option<String>,
235+
}
236+
237+
impl ProcessAnnotation {
238+
pub fn is_cvm(&self) -> bool {
239+
if self.live_for.is_some() {
240+
return false;
241+
}
242+
self.kind.is_empty() || self.kind == "cvm"
243+
}
244+
}
245+
229246
impl Config {
230247
pub fn abs_path(self) -> Result<Self> {
231248
Ok(Self {
@@ -240,16 +257,37 @@ impl Config {
240257
#[serde(tag = "mode", rename_all = "lowercase")]
241258
pub enum Networking {
242259
User(UserNetworking),
260+
Passt(PasstNetworking),
243261
Custom(CustomNetworking),
244262
}
245263

264+
impl Networking {
265+
pub fn is_passt(&self) -> bool {
266+
matches!(self, Networking::Passt(_))
267+
}
268+
}
269+
246270
#[derive(Debug, Clone, Deserialize, Serialize)]
247271
pub struct UserNetworking {
248272
pub net: String,
249273
pub dhcp_start: String,
250274
pub restrict: bool,
251275
}
252276

277+
#[derive(Debug, Clone, Deserialize, Serialize)]
278+
pub struct PasstNetworking {
279+
pub passt_exec: String,
280+
pub interface: String,
281+
pub address: String,
282+
pub netmask: String,
283+
pub gateway: String,
284+
pub dns: Vec<String>,
285+
pub map_host_loopback: String,
286+
pub map_guest_addr: String,
287+
pub no_map_gw: bool,
288+
pub ipv4_only: bool,
289+
}
290+
253291
#[derive(Debug, Clone, Deserialize, Serialize)]
254292
pub struct CustomNetworking {
255293
pub netdev: String,

0 commit comments

Comments
 (0)