Skip to content

Commit 7f6feab

Browse files
committed
feat: Add systemd socket activation for dstack-guest-agent
Replace systemd-socket-proxyd with native socket activation support. This eliminates the proxy layer and allows the sockets to survive service restarts, maintaining compatibility with containers that mount /var/run/dstack.sock or /var/run/tappd.sock directly. Changes: - Add dstack-guest-agent.socket with both sockets (ordered) - Remove dstack-socket.{socket,service} and tappd-socket.{socket,service} - Add listenfd dependency for receiving sockets from systemd - Add ActivatedUnixListener wrapper implementing rocket's Listener trait - Modify run_internal/run_internal_v0 to use activated sockets when available - Update dstack-guest-agent.service to require the socket unit
1 parent 8e6fae8 commit 7f6feab

10 files changed

Lines changed: 145 additions & 93 deletions

Cargo.lock

Lines changed: 12 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
@@ -105,6 +105,7 @@ tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
105105
safe-write = "0.1.2"
106106
nix = "0.29.0"
107107
sd-notify = "0.4.5"
108+
listenfd = "1.0"
108109
jemallocator = "0.5.4"
109110

110111
# Serialization/Parsing

basefiles/dstack-guest-agent.service

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[Unit]
22
Description=dstack Guest Agent Service
3-
After=network.target tboot.service
3+
Requires=dstack-guest-agent.socket
4+
After=network.target tboot.service dstack-guest-agent.socket
45
Before=docker.service
56

67
[Service]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# SPDX-FileCopyrightText: 2025 Phala Network <dstack@phala.network>
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
# Socket activation for dstack-guest-agent.
6+
# Provides backward compatibility for containers that mount sockets directly.
7+
# Socket order: dstack.sock (index 0), tappd.sock (index 1)
8+
9+
[Unit]
10+
Description=dstack guest agent sockets
11+
12+
[Socket]
13+
ListenStream=/var/run/dstack.sock
14+
ListenStream=/var/run/tappd.sock
15+
SocketMode=0777
16+
17+
[Install]
18+
WantedBy=sockets.target

basefiles/dstack-socket.service

Lines changed: 0 additions & 15 deletions
This file was deleted.

basefiles/dstack-socket.socket

Lines changed: 0 additions & 16 deletions
This file was deleted.

basefiles/tappd-socket.service

Lines changed: 0 additions & 15 deletions
This file was deleted.

basefiles/tappd-socket.socket

Lines changed: 0 additions & 16 deletions
This file was deleted.

guest-agent/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,4 @@ tempfile.workspace = true
5353
rand.workspace = true
5454
or-panic.workspace = true
5555
cc-eventlog.workspace = true
56+
listenfd.workspace = true

guest-agent/src/main.rs

Lines changed: 111 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@
22
//
33
// SPDX-License-Identifier: Apache-2.0
44

5-
use std::{fs::Permissions, future::pending, os::unix::fs::PermissionsExt};
5+
use std::{future::pending, io, os::unix::net::UnixListener as StdUnixListener};
66

77
use anyhow::{anyhow, Context, Result};
88
use clap::Parser;
99
use config::BindAddr;
1010
use guest_api_service::GuestApiHandler;
11+
use listenfd::ListenFd;
1112
use rocket::{
1213
fairing::AdHoc,
1314
figment::Figment,
14-
listener::{Bind, DefaultListener},
15+
listener::{unix::UnixStream, Bind, DefaultListener, Endpoint, Listener},
1516
};
1617
use rocket_vsock_listener::VsockListener;
1718
use rpc_service::{AppState, ExternalRpcHandler, InternalRpcHandler, InternalRpcHandlerV0};
@@ -26,6 +27,59 @@ mod http_routes;
2627
mod models;
2728
mod rpc_service;
2829

30+
/// Socket indices for systemd socket activation.
31+
/// Order matches ListenStream declarations in dstack-guest-agent.socket.
32+
mod socket_index {
33+
pub const DSTACK: usize = 0;
34+
pub const TAPPD: usize = 1;
35+
}
36+
37+
/// Systemd-activated sockets passed via LISTEN_FDS.
38+
struct ActivatedSockets {
39+
dstack: Option<StdUnixListener>,
40+
tappd: Option<StdUnixListener>,
41+
}
42+
43+
impl ActivatedSockets {
44+
fn from_env() -> Self {
45+
let mut listenfd = ListenFd::from_env();
46+
Self {
47+
dstack: listenfd.take_unix_listener(socket_index::DSTACK).ok().flatten(),
48+
tappd: listenfd.take_unix_listener(socket_index::TAPPD).ok().flatten(),
49+
}
50+
}
51+
}
52+
53+
/// Wrapper for systemd-activated Unix socket that implements rocket's Listener trait.
54+
struct ActivatedUnixListener {
55+
listener: tokio::net::UnixListener,
56+
}
57+
58+
impl ActivatedUnixListener {
59+
fn new(std_listener: StdUnixListener) -> io::Result<Self> {
60+
std_listener.set_nonblocking(true)?;
61+
let listener = tokio::net::UnixListener::from_std(std_listener)?;
62+
Ok(Self { listener })
63+
}
64+
}
65+
66+
impl Listener for ActivatedUnixListener {
67+
type Accept = UnixStream;
68+
type Connection = UnixStream;
69+
70+
async fn accept(&self) -> io::Result<Self::Accept> {
71+
Ok(self.listener.accept().await?.0)
72+
}
73+
74+
async fn connect(&self, accept: Self::Accept) -> io::Result<Self::Connection> {
75+
Ok(accept)
76+
}
77+
78+
fn endpoint(&self) -> io::Result<Endpoint> {
79+
self.listener.local_addr()?.try_into()
80+
}
81+
}
82+
2983
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
3084
const GIT_REV: &str = git_version::git_version!(
3185
args = ["--abbrev=20", "--always", "--dirty=-modified"],
@@ -52,6 +106,7 @@ struct Args {
52106
async fn run_internal_v0(
53107
state: AppState,
54108
figment: Figment,
109+
activated_socket: Option<StdUnixListener>,
55110
sock_ready_tx: oneshot::Sender<()>,
56111
) -> Result<()> {
57112
let rocket = rocket::custom(figment)
@@ -64,26 +119,36 @@ async fn run_internal_v0(
64119
.ignite()
65120
.await
66121
.map_err(|err| anyhow!("Failed to ignite rocket: {err}"))?;
67-
let endpoint = DefaultListener::bind_endpoint(&ignite)
68-
.map_err(|err| anyhow!("Failed to get endpoint: {err}"))?;
69-
let listener = DefaultListener::bind(&ignite)
70-
.await
71-
.map_err(|err| anyhow!("Failed to bind on {endpoint}: {err}"))?;
72-
if let Some(path) = endpoint.unix() {
73-
// Allow any user to connect to the socket
74-
fs_err::set_permissions(path, Permissions::from_mode(0o777))?;
122+
123+
if let Some(std_listener) = activated_socket {
124+
// Use systemd-activated socket
125+
info!("Using systemd-activated socket for tappd.sock");
126+
let listener = ActivatedUnixListener::new(std_listener)?;
127+
sock_ready_tx.send(()).ok();
128+
ignite
129+
.launch_on(listener)
130+
.await
131+
.map_err(|err: rocket::Error| anyhow!(err.to_string()))?;
132+
} else {
133+
// Fall back to binding our own socket
134+
let endpoint = DefaultListener::bind_endpoint(&ignite)
135+
.map_err(|err| anyhow!("Failed to get endpoint: {err}"))?;
136+
let listener = DefaultListener::bind(&ignite)
137+
.await
138+
.map_err(|err| anyhow!("Failed to bind on {endpoint}: {err}"))?;
139+
sock_ready_tx.send(()).ok();
140+
ignite
141+
.launch_on(listener)
142+
.await
143+
.map_err(|err| anyhow!(err.to_string()))?;
75144
}
76-
sock_ready_tx.send(()).ok();
77-
ignite
78-
.launch_on(listener)
79-
.await
80-
.map_err(|err| anyhow!(err.to_string()))?;
81145
Ok(())
82146
}
83147

84148
async fn run_internal(
85149
state: AppState,
86150
figment: Figment,
151+
activated_socket: Option<StdUnixListener>,
87152
sock_ready_tx: oneshot::Sender<()>,
88153
) -> Result<()> {
89154
let rocket = rocket::custom(figment)
@@ -93,20 +158,29 @@ async fn run_internal(
93158
.ignite()
94159
.await
95160
.map_err(|err| anyhow!("Failed to ignite rocket: {err}"))?;
96-
let endpoint = DefaultListener::bind_endpoint(&ignite)
97-
.map_err(|err| anyhow!("Failed to get endpoint: {err}"))?;
98-
let listener = DefaultListener::bind(&ignite)
99-
.await
100-
.map_err(|err| anyhow!("Failed to bind on {endpoint}: {err}"))?;
101-
if let Some(path) = endpoint.unix() {
102-
// Allow any user to connect to the socket
103-
fs_err::set_permissions(path, Permissions::from_mode(0o777))?;
161+
162+
if let Some(std_listener) = activated_socket {
163+
// Use systemd-activated socket
164+
info!("Using systemd-activated socket for dstack.sock");
165+
let listener = ActivatedUnixListener::new(std_listener)?;
166+
sock_ready_tx.send(()).ok();
167+
ignite
168+
.launch_on(listener)
169+
.await
170+
.map_err(|err: rocket::Error| anyhow!(err.to_string()))?;
171+
} else {
172+
// Fall back to binding our own socket
173+
let endpoint = DefaultListener::bind_endpoint(&ignite)
174+
.map_err(|err| anyhow!("Failed to get endpoint: {err}"))?;
175+
let listener = DefaultListener::bind(&ignite)
176+
.await
177+
.map_err(|err| anyhow!("Failed to bind on {endpoint}: {err}"))?;
178+
sock_ready_tx.send(()).ok();
179+
ignite
180+
.launch_on(listener)
181+
.await
182+
.map_err(|err| anyhow!(err.to_string()))?;
104183
}
105-
sock_ready_tx.send(()).ok();
106-
ignite
107-
.launch_on(listener)
108-
.await
109-
.map_err(|err| anyhow!(err.to_string()))?;
110184
Ok(())
111185
}
112186

@@ -219,11 +293,18 @@ async fn main() -> Result<()> {
219293
.extract()
220294
.context("Failed to extract bind address")?;
221295
let guest_api_figment = figment.select("guest-api");
296+
297+
// Get systemd-activated sockets if available
298+
let activated = ActivatedSockets::from_env();
299+
if activated.dstack.is_some() || activated.tappd.is_some() {
300+
info!("Systemd socket activation detected");
301+
}
302+
222303
let (tappd_ready_tx, tappd_ready_rx) = oneshot::channel();
223304
let (sock_ready_tx, sock_ready_rx) = oneshot::channel();
224305
tokio::select!(
225-
res = run_internal_v0(state.clone(), internal_v0_figment, tappd_ready_tx) => res?,
226-
res = run_internal(state.clone(), internal_figment, sock_ready_tx) => res?,
306+
res = run_internal_v0(state.clone(), internal_v0_figment, activated.tappd, tappd_ready_tx) => res?,
307+
res = run_internal(state.clone(), internal_figment, activated.dstack, sock_ready_tx) => res?,
227308
res = run_external(state.clone(), external_figment) => res?,
228309
res = run_guest_api(state.clone(), guest_api_figment) => res?,
229310
_ = async {

0 commit comments

Comments
 (0)