Skip to content

Commit aa8fa91

Browse files
committed
Support for using passt as network egress
1 parent 51b8a44 commit aa8fa91

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
}
@@ -218,12 +217,117 @@ impl VmState {
218217
}
219218

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

538653
let command = cmd_args.remove(0);
539-
let note = "{}".to_string();
654+
let note = ProcessAnnotation {
655+
kind: "cvm".to_string(),
656+
live_for: None,
657+
};
540658
let note = serde_json::to_string(&note)?;
541659
let process_config = ProcessConfig {
542660
id: self.manifest.id.clone(),
@@ -551,8 +669,9 @@ impl VmConfig {
551669
cid: Some(self.cid),
552670
note,
553671
};
672+
processes.push(process_config);
554673

555-
Ok(process_config)
674+
Ok(processes)
556675
}
557676
}
558677

@@ -728,6 +847,22 @@ impl VmWorkDir {
728847
self.workdir.join("qmp.sock")
729848
}
730849

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

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)