Skip to content

Commit 5ef09a9

Browse files
committed
refactor(guest-agent): isolate simulator into standalone binary
1 parent e7e5eef commit 5ef09a9

15 files changed

Lines changed: 656 additions & 413 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ cargo build --release -p dstack-vmm
4848
cargo build --release -p dstack-kms
4949
cargo build --release -p dstack-gateway
5050
cargo build --release -p dstack-guest-agent
51+
cargo build --release -p dstack-guest-agent-simulator
5152

5253
# Check code
5354
cargo check --all-features

Cargo.lock

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ members = [
2424
"iohash",
2525
"guest-agent",
2626
"guest-agent/rpc",
27+
"guest-agent-simulator",
2728
"vmm",
2829
"vmm/rpc",
2930
"gateway",

guest-agent-simulator/Cargo.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# SPDX-FileCopyrightText: © 2025 Phala Network <dstack@phala.network>
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
[package]
6+
name = "dstack-guest-agent-simulator"
7+
version.workspace = true
8+
authors.workspace = true
9+
edition.workspace = true
10+
license.workspace = true
11+
12+
[[bin]]
13+
name = "dstack-simulator"
14+
path = "src/main.rs"
15+
16+
[dependencies]
17+
anyhow.workspace = true
18+
clap.workspace = true
19+
serde.workspace = true
20+
tracing.workspace = true
21+
tracing-subscriber.workspace = true
22+
rocket.workspace = true
23+
ra-rpc = { workspace = true, features = ["rocket"] }
24+
ra-tls = { workspace = true, features = ["quote"] }
25+
dstack-guest-agent = { path = "../guest-agent" }
26+
dstack-guest-agent-rpc.workspace = true

guest-agent-simulator/dstack.toml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# SPDX-FileCopyrightText: © 2025 Phala Network <dstack@phala.network>
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
[default]
6+
workers = 8
7+
max_blocking = 64
8+
ident = "dstack Simulator"
9+
temp_dir = "/tmp"
10+
keep_alive = 10
11+
log_level = "debug"
12+
13+
[default.core]
14+
keys_file = "appkeys.json"
15+
compose_file = "app-compose.json"
16+
sys_config_file = "sys-config.json"
17+
18+
[default.core.simulator]
19+
attestation_file = "attestation.bin"
20+
21+
[internal-v0]
22+
address = "unix:./tappd.sock"
23+
reuse = true
24+
25+
[internal]
26+
address = "unix:./dstack.sock"
27+
reuse = true
28+
29+
[external]
30+
address = "unix:./external.sock"
31+
reuse = true
32+
33+
[guest-api]
34+
address = "unix:./guest.sock"
35+
reuse = true

guest-agent-simulator/src/main.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// SPDX-FileCopyrightText: © 2025 Phala Network <dstack@phala.network>
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
use std::sync::Arc;
6+
7+
use anyhow::{bail, Context, Result};
8+
use clap::Parser;
9+
use dstack_guest_agent::{
10+
backend::{
11+
load_versioned_attestation, simulated_attest_response, simulated_info_attestation,
12+
simulated_quote_response, PlatformBackend,
13+
},
14+
config::{self, Config},
15+
AppState, run_server,
16+
};
17+
use dstack_guest_agent_rpc::{AttestResponse, GetQuoteResponse};
18+
use ra_rpc::Attestation;
19+
use ra_tls::attestation::VersionedAttestation;
20+
use serde::Deserialize;
21+
use tracing::warn;
22+
23+
const DEFAULT_CONFIG: &str = include_str!("../dstack.toml");
24+
25+
#[derive(Parser)]
26+
#[command(author, version, about = "dstack guest agent simulator", long_version = dstack_guest_agent::app_version())]
27+
struct Args {
28+
/// Path to the configuration file
29+
#[arg(short, long)]
30+
config: Option<String>,
31+
32+
/// Enable systemd watchdog
33+
#[arg(short, long)]
34+
watchdog: bool,
35+
}
36+
37+
#[derive(Debug, Clone, Deserialize)]
38+
struct SimulatorSettings {
39+
attestation_file: String,
40+
}
41+
42+
#[derive(Debug, Clone, Deserialize)]
43+
struct SimulatorCoreConfig {
44+
#[serde(flatten)]
45+
core: Config,
46+
simulator: SimulatorSettings,
47+
}
48+
49+
struct SimulatorPlatform {
50+
attestation: VersionedAttestation,
51+
}
52+
53+
impl SimulatorPlatform {
54+
fn new(attestation: VersionedAttestation) -> Self {
55+
Self { attestation }
56+
}
57+
}
58+
59+
impl PlatformBackend for SimulatorPlatform {
60+
fn attestation_for_info(&self) -> Result<Option<Attestation>> {
61+
Ok(Some(simulated_info_attestation(&self.attestation)))
62+
}
63+
64+
fn attestation_override(&self) -> Result<Option<VersionedAttestation>> {
65+
Ok(Some(self.attestation.clone()))
66+
}
67+
68+
fn quote_response(&self, report_data: [u8; 64], vm_config: &str) -> Result<GetQuoteResponse> {
69+
simulated_quote_response(&self.attestation, report_data, vm_config)
70+
}
71+
72+
fn attest_response(&self, _report_data: [u8; 64]) -> Result<AttestResponse> {
73+
Ok(simulated_attest_response(&self.attestation))
74+
}
75+
76+
fn emit_event(&self, event: &str, _payload: &[u8]) -> Result<()> {
77+
bail!("runtime event emission is unavailable in simulator mode: {event}")
78+
}
79+
}
80+
81+
#[rocket::main]
82+
async fn main() -> Result<()> {
83+
{
84+
use tracing_subscriber::{fmt, EnvFilter};
85+
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
86+
fmt().with_env_filter(filter).with_ansi(false).init();
87+
}
88+
let args = Args::parse();
89+
let figment = config::load_config_figment_with_default(DEFAULT_CONFIG, args.config.as_deref());
90+
let sim_config: SimulatorCoreConfig = figment
91+
.focus("core")
92+
.extract()
93+
.context("Failed to extract simulator core config")?;
94+
warn!(attestation_file = %sim_config.simulator.attestation_file, "starting dstack guest-agent simulator");
95+
let attestation = load_versioned_attestation(&sim_config.simulator.attestation_file)?;
96+
let state = AppState::new(sim_config.core, Arc::new(SimulatorPlatform::new(attestation)))
97+
.await
98+
.context("Failed to create simulator app state")?;
99+
run_server(state, figment, args.watchdog).await
100+
}
101+
102+
103+
#[cfg(test)]
104+
mod tests {
105+
use super::*;
106+
107+
fn load_fixture_platform() -> SimulatorPlatform {
108+
let fixture = load_versioned_attestation(
109+
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../guest-agent/fixtures/attestation.bin"),
110+
)
111+
.expect("fixture attestation should load");
112+
SimulatorPlatform::new(fixture)
113+
}
114+
115+
#[test]
116+
fn simulator_rejects_runtime_event_emission() {
117+
let platform = load_fixture_platform();
118+
let err = platform.emit_event("test.event", b"payload").unwrap_err();
119+
assert!(err.to_string().contains("unavailable in simulator mode"));
120+
}
121+
122+
#[test]
123+
fn simulator_provides_attestation_override() {
124+
let platform = load_fixture_platform();
125+
assert!(platform.attestation_override().unwrap().is_some());
126+
assert!(platform.attestation_for_info().unwrap().is_some());
127+
}
128+
}

guest-agent/dstack.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@ compose_file = "/dstack/.host-shared/app-compose.json"
1616
sys_config_file = "/dstack/.host-shared/.sys-config.json"
1717
data_disks = ["/"]
1818

19-
[default.core.simulator]
20-
enabled = false
21-
attestation_file = "attestation.bin"
2219

2320
[internal-v0]
2421
address = "unix:/var/run/dstack/tappd.sock"

guest-agent/src/backend.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// SPDX-FileCopyrightText: © 2024-2025 Phala Network <dstack@phala.network>
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
use std::path::Path;
6+
7+
use anyhow::{Context, Result};
8+
use dstack_attest::emit_runtime_event;
9+
use dstack_guest_agent_rpc::{AttestResponse, GetQuoteResponse};
10+
use fs_err as fs;
11+
use ra_rpc::Attestation;
12+
use ra_tls::attestation::{VersionedAttestation, TDX_QUOTE_REPORT_DATA_RANGE};
13+
14+
pub trait PlatformBackend: Send + Sync {
15+
fn attestation_for_info(&self) -> Result<Option<Attestation>>;
16+
fn attestation_override(&self) -> Result<Option<VersionedAttestation>>;
17+
fn quote_response(&self, report_data: [u8; 64], vm_config: &str) -> Result<GetQuoteResponse>;
18+
fn attest_response(&self, report_data: [u8; 64]) -> Result<AttestResponse>;
19+
fn emit_event(&self, event: &str, payload: &[u8]) -> Result<()>;
20+
}
21+
22+
#[derive(Debug, Default)]
23+
pub struct RealPlatform;
24+
25+
impl PlatformBackend for RealPlatform {
26+
fn attestation_for_info(&self) -> Result<Option<Attestation>> {
27+
Ok(Attestation::local().ok())
28+
}
29+
30+
fn attestation_override(&self) -> Result<Option<VersionedAttestation>> {
31+
Ok(None)
32+
}
33+
34+
fn quote_response(&self, report_data: [u8; 64], vm_config: &str) -> Result<GetQuoteResponse> {
35+
let attestation = Attestation::quote(&report_data).context("Failed to get quote")?;
36+
let tdx_quote = attestation.get_tdx_quote_bytes();
37+
let tdx_event_log = attestation.get_tdx_event_log_string();
38+
Ok(GetQuoteResponse {
39+
quote: tdx_quote.unwrap_or_default(),
40+
event_log: tdx_event_log.unwrap_or_default(),
41+
report_data: report_data.to_vec(),
42+
vm_config: vm_config.to_string(),
43+
})
44+
}
45+
46+
fn attest_response(&self, report_data: [u8; 64]) -> Result<AttestResponse> {
47+
let attestation = Attestation::quote(&report_data).context("Failed to get attestation")?;
48+
Ok(AttestResponse {
49+
attestation: attestation.into_versioned().to_scale(),
50+
})
51+
}
52+
53+
fn emit_event(&self, event: &str, payload: &[u8]) -> Result<()> {
54+
emit_runtime_event(event, payload)
55+
}
56+
}
57+
58+
pub fn load_versioned_attestation(path: impl AsRef<Path>) -> Result<VersionedAttestation> {
59+
let path = path.as_ref();
60+
let attestation_bytes = fs::read(path).with_context(|| {
61+
format!(
62+
"Failed to read simulator attestation file: {}",
63+
path.display()
64+
)
65+
})?;
66+
VersionedAttestation::from_scale(&attestation_bytes)
67+
.context("Failed to decode simulator attestation")
68+
}
69+
70+
pub fn simulated_quote_response(
71+
attestation: &VersionedAttestation,
72+
report_data: [u8; 64],
73+
vm_config: &str,
74+
) -> Result<GetQuoteResponse> {
75+
let VersionedAttestation::V0 { attestation } = attestation.clone();
76+
let mut attestation = attestation;
77+
let Some(quote) = attestation.tdx_quote_mut() else {
78+
return Err(anyhow::anyhow!("Quote not found"));
79+
};
80+
81+
quote.quote[TDX_QUOTE_REPORT_DATA_RANGE].copy_from_slice(&report_data);
82+
Ok(GetQuoteResponse {
83+
quote: quote.quote.to_vec(),
84+
event_log: serde_json::to_string(&quote.event_log)
85+
.context("Failed to serialize event log")?,
86+
report_data: report_data.to_vec(),
87+
vm_config: vm_config.to_string(),
88+
})
89+
}
90+
91+
pub fn simulated_attest_response(attestation: &VersionedAttestation) -> AttestResponse {
92+
AttestResponse {
93+
attestation: attestation.to_scale(),
94+
}
95+
}
96+
97+
pub fn simulated_info_attestation(attestation: &VersionedAttestation) -> Attestation {
98+
attestation.clone().into_inner()
99+
}

guest-agent/src/config.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ use serde::{de::Error, Deserialize};
1313
pub const DEFAULT_CONFIG: &str = include_str!("../dstack.toml");
1414

1515
pub fn load_config_figment(config_file: Option<&str>) -> Figment {
16-
load_config("dstack", DEFAULT_CONFIG, config_file, true)
16+
load_config_figment_with_default(DEFAULT_CONFIG, config_file)
17+
}
18+
19+
pub fn load_config_figment_with_default(default_config: &str, config_file: Option<&str>) -> Figment {
20+
load_config("dstack", default_config, config_file, true)
1721
}
1822

1923
#[derive(Debug, Clone, Copy, Deserialize)]
@@ -43,8 +47,6 @@ pub struct Config {
4347
pub sys_config_file: PathBuf,
4448
#[serde(default)]
4549
pub pccs_url: Option<String>,
46-
pub simulator: Simulator,
47-
// List of disks to be shown in the dashboard
4850
pub data_disks: HashSet<PathBuf>,
4951
}
5052

@@ -67,9 +69,3 @@ where
6769
raw: content,
6870
})
6971
}
70-
71-
#[derive(Debug, Clone, Deserialize)]
72-
pub struct Simulator {
73-
pub enabled: bool,
74-
pub attestation_file: String,
75-
}

guest-agent/src/lib.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// SPDX-FileCopyrightText: © 2024-2025 Phala Network <dstack@phala.network>
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
pub const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
6+
pub const GIT_REV: &str = git_version::git_version!(
7+
args = ["--abbrev=20", "--always", "--dirty=-modified"],
8+
prefix = "git:",
9+
fallback = "unknown"
10+
);
11+
12+
pub mod backend;
13+
pub mod config;
14+
mod guest_api_service;
15+
mod http_routes;
16+
mod models;
17+
pub mod rpc_service;
18+
mod server;
19+
mod socket_activation;
20+
21+
pub use rpc_service::AppState;
22+
pub use server::{app_version, run as run_server};

0 commit comments

Comments
 (0)