Skip to content

Commit 8a3c4ac

Browse files
committed
ra-rpc: populate Unix peer creds for UDS listeners
1 parent 900cd55 commit 8a3c4ac

2 files changed

Lines changed: 253 additions & 12 deletions

File tree

guest-agent/src/server.rs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ use crate::http_routes;
1010
use crate::rpc_service::{AppState, ExternalRpcHandler, InternalRpcHandler, InternalRpcHandlerV0};
1111
use crate::socket_activation::{ActivatedSockets, ActivatedUnixListener};
1212
use anyhow::{anyhow, Context, Result};
13+
#[cfg(unix)]
14+
use ra_rpc::rocket_helper::UnixPeerCredListener;
1315
use rocket::{
1416
fairing::AdHoc,
1517
figment::Figment,
16-
listener::{Bind, DefaultListener},
18+
listener::{unix::UnixListener, Bind, DefaultListener},
1719
};
1820
use rocket_vsock_listener::VsockListener;
1921
use sd_notify::{notify as sd_notify, NotifyState};
@@ -43,18 +45,20 @@ async fn run_internal_v0(
4345

4446
if let Some(std_listener) = activated_socket {
4547
info!("Using systemd-activated socket for tappd.sock");
46-
let listener = ActivatedUnixListener::new(std_listener)?;
48+
let listener = UnixPeerCredListener::new(ActivatedUnixListener::new(std_listener)?);
4749
sock_ready_tx.send(()).ok();
4850
ignite
4951
.launch_on(listener)
5052
.await
5153
.map_err(|err: rocket::Error| anyhow!(err.to_string()))?;
5254
} else {
53-
let endpoint = DefaultListener::bind_endpoint(&ignite)
55+
let endpoint = UnixListener::bind_endpoint(&ignite)
5456
.map_err(|err| anyhow!("Failed to get endpoint: {err}"))?;
55-
let listener = DefaultListener::bind(&ignite)
56-
.await
57-
.map_err(|err| anyhow!("Failed to bind on {endpoint}: {err}"))?;
57+
let listener = UnixPeerCredListener::new(
58+
<UnixListener as Bind>::bind(&ignite)
59+
.await
60+
.map_err(|err| anyhow!("Failed to bind on {endpoint}: {err}"))?,
61+
);
5862
sock_ready_tx.send(()).ok();
5963
ignite
6064
.launch_on(listener)
@@ -80,18 +84,20 @@ async fn run_internal(
8084

8185
if let Some(std_listener) = activated_socket {
8286
info!("Using systemd-activated socket for dstack.sock");
83-
let listener = ActivatedUnixListener::new(std_listener)?;
87+
let listener = UnixPeerCredListener::new(ActivatedUnixListener::new(std_listener)?);
8488
sock_ready_tx.send(()).ok();
8589
ignite
8690
.launch_on(listener)
8791
.await
8892
.map_err(|err: rocket::Error| anyhow!(err.to_string()))?;
8993
} else {
90-
let endpoint = DefaultListener::bind_endpoint(&ignite)
94+
let endpoint = UnixListener::bind_endpoint(&ignite)
9195
.map_err(|err| anyhow!("Failed to get endpoint: {err}"))?;
92-
let listener = DefaultListener::bind(&ignite)
93-
.await
94-
.map_err(|err| anyhow!("Failed to bind on {endpoint}: {err}"))?;
96+
let listener = UnixPeerCredListener::new(
97+
<UnixListener as Bind>::bind(&ignite)
98+
.await
99+
.map_err(|err| anyhow!("Failed to bind on {endpoint}: {err}"))?,
100+
);
95101
sock_ready_tx.send(()).ok();
96102
ignite
97103
.launch_on(listener)

ra-rpc/src/rocket_helper.rs

Lines changed: 236 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
// SPDX-License-Identifier: Apache-2.0
44

55
use std::convert::Infallible;
6+
use std::fmt;
7+
use std::io;
8+
use std::path::PathBuf;
9+
use std::pin::Pin;
10+
use std::task::{Context as TaskContext, Poll};
611

712
#[cfg(all(feature = "rocket", feature = "openapi"))]
813
use crate::openapi::{OpenApiDoc, RenderedDoc};
@@ -13,6 +18,12 @@ use std::{borrow::Cow, sync::Arc};
1318

1419
use anyhow::{Context, Result};
1520
use ra_tls::traits::CertExt;
21+
#[cfg(unix)]
22+
use rocket::listener::unix::UnixStream;
23+
#[cfg(unix)]
24+
use rocket::listener::{Connection, Listener};
25+
#[cfg(unix)]
26+
use rocket::tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
1627
use rocket::{
1728
data::{ByteUnit, Data, Limits, ToByteUnit},
1829
http::{uri::Origin, ContentType, Method, Status},
@@ -25,7 +36,7 @@ use rocket::{
2536
use rocket_vsock_listener::VsockEndpoint;
2637
use tracing::warn;
2738

28-
use crate::{encode_error, CallContext, RemoteEndpoint, RpcCall};
39+
use crate::{encode_error, CallContext, RemoteEndpoint, RpcCall, UnixPeerCred};
2940

3041
pub struct RpcResponse {
3142
is_json: bool,
@@ -48,6 +59,141 @@ impl<'r> Responder<'r, 'static> for RpcResponse {
4859
}
4960
}
5061

62+
#[cfg(unix)]
63+
#[derive(Debug, Clone)]
64+
struct UnixPeerEndpoint {
65+
path: PathBuf,
66+
peer: Option<UnixPeerCred>,
67+
}
68+
69+
#[cfg(unix)]
70+
impl fmt::Display for UnixPeerEndpoint {
71+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72+
write!(f, "unix:{}", self.path.display())
73+
}
74+
}
75+
76+
#[cfg(unix)]
77+
pub struct UnixPeerCredListener<L> {
78+
inner: L,
79+
}
80+
81+
#[cfg(unix)]
82+
impl<L> UnixPeerCredListener<L> {
83+
pub fn new(inner: L) -> Self {
84+
Self { inner }
85+
}
86+
}
87+
88+
#[cfg(unix)]
89+
pub struct UnixPeerCredConnection {
90+
stream: UnixStream,
91+
endpoint: rocket::listener::Endpoint,
92+
}
93+
94+
#[cfg(unix)]
95+
impl AsyncRead for UnixPeerCredConnection {
96+
fn poll_read(
97+
mut self: Pin<&mut Self>,
98+
cx: &mut TaskContext<'_>,
99+
buf: &mut ReadBuf<'_>,
100+
) -> Poll<io::Result<()>> {
101+
Pin::new(&mut self.stream).poll_read(cx, buf)
102+
}
103+
}
104+
105+
#[cfg(unix)]
106+
impl AsyncWrite for UnixPeerCredConnection {
107+
fn poll_write(
108+
mut self: Pin<&mut Self>,
109+
cx: &mut TaskContext<'_>,
110+
buf: &[u8],
111+
) -> Poll<io::Result<usize>> {
112+
Pin::new(&mut self.stream).poll_write(cx, buf)
113+
}
114+
115+
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<io::Result<()>> {
116+
Pin::new(&mut self.stream).poll_flush(cx)
117+
}
118+
119+
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<io::Result<()>> {
120+
Pin::new(&mut self.stream).poll_shutdown(cx)
121+
}
122+
123+
fn is_write_vectored(&self) -> bool {
124+
self.stream.is_write_vectored()
125+
}
126+
127+
fn poll_write_vectored(
128+
mut self: Pin<&mut Self>,
129+
cx: &mut TaskContext<'_>,
130+
bufs: &[io::IoSlice<'_>],
131+
) -> Poll<io::Result<usize>> {
132+
Pin::new(&mut self.stream).poll_write_vectored(cx, bufs)
133+
}
134+
}
135+
136+
#[cfg(unix)]
137+
impl Connection for UnixPeerCredConnection {
138+
fn endpoint(&self) -> io::Result<rocket::listener::Endpoint> {
139+
Ok(self.endpoint.clone())
140+
}
141+
}
142+
143+
#[cfg(unix)]
144+
impl<L> Listener for UnixPeerCredListener<L>
145+
where
146+
L: Listener<Accept = UnixStream, Connection = UnixStream>,
147+
{
148+
type Accept = UnixStream;
149+
type Connection = UnixPeerCredConnection;
150+
151+
async fn accept(&self) -> io::Result<Self::Accept> {
152+
self.inner.accept().await
153+
}
154+
155+
async fn connect(&self, accept: Self::Accept) -> io::Result<Self::Connection> {
156+
let path = accept
157+
.local_addr()?
158+
.as_pathname()
159+
.map(PathBuf::from)
160+
.or_else(|| {
161+
self.inner
162+
.endpoint()
163+
.ok()
164+
.and_then(|e| e.unix().map(PathBuf::from))
165+
});
166+
167+
let endpoint = match path {
168+
Some(path) => rocket::listener::Endpoint::new(UnixPeerEndpoint {
169+
path,
170+
peer: unix_peer_cred(&accept),
171+
}),
172+
None => accept.local_addr()?.try_into()?,
173+
};
174+
175+
Ok(UnixPeerCredConnection {
176+
stream: accept,
177+
endpoint,
178+
})
179+
}
180+
181+
fn endpoint(&self) -> io::Result<rocket::listener::Endpoint> {
182+
self.inner.endpoint()
183+
}
184+
}
185+
186+
#[cfg(unix)]
187+
fn unix_peer_cred(stream: &UnixStream) -> Option<UnixPeerCred> {
188+
let cred = stream.peer_cred().ok()?;
189+
let pid = cred.pid()?;
190+
Some(UnixPeerCred {
191+
pid: pid as u64,
192+
uid: cred.uid() as u64,
193+
gid: cred.gid() as u64,
194+
})
195+
}
196+
51197
#[derive(Debug, Clone)]
52198
pub struct QuoteVerifier {
53199
pccs_url: Option<String>,
@@ -266,6 +412,27 @@ impl From<Endpoint> for RemoteEndpoint {
266412
Endpoint::Tcp(addr) => RemoteEndpoint::Tcp(addr),
267413
Endpoint::Quic(addr) => RemoteEndpoint::Quic(addr),
268414
Endpoint::Unix(path) => RemoteEndpoint::Unix { path, peer: None },
415+
#[cfg(unix)]
416+
Endpoint::Custom(endpoint) => {
417+
if let Some(endpoint) =
418+
(endpoint.as_ref() as &dyn std::any::Any).downcast_ref::<UnixPeerEndpoint>()
419+
{
420+
RemoteEndpoint::Unix {
421+
path: endpoint.path.clone(),
422+
peer: endpoint.peer.clone(),
423+
}
424+
} else {
425+
let address = endpoint.to_string();
426+
match address.parse::<VsockEndpoint>() {
427+
Ok(addr) => RemoteEndpoint::Vsock {
428+
cid: addr.cid,
429+
port: addr.port,
430+
},
431+
Err(_) => RemoteEndpoint::Other(address),
432+
}
433+
}
434+
}
435+
Endpoint::Tls(inner, _) => RemoteEndpoint::from((*inner).clone()),
269436
_ => {
270437
let address = endpoint.to_string();
271438
match address.parse::<VsockEndpoint>() {
@@ -280,6 +447,74 @@ impl From<Endpoint> for RemoteEndpoint {
280447
}
281448
}
282449

450+
#[cfg(all(test, unix))]
451+
mod tests {
452+
use super::*;
453+
use rocket::listener::unix::UnixListener;
454+
use rocket::tokio;
455+
use std::time::{SystemTime, UNIX_EPOCH};
456+
457+
#[test]
458+
fn custom_unix_endpoint_maps_to_remote_endpoint() {
459+
let endpoint = Endpoint::new(UnixPeerEndpoint {
460+
path: PathBuf::from("/tmp/test.sock"),
461+
peer: Some(UnixPeerCred {
462+
pid: 1,
463+
uid: 2,
464+
gid: 3,
465+
}),
466+
});
467+
468+
let remote = RemoteEndpoint::from(endpoint);
469+
assert_eq!(
470+
remote,
471+
RemoteEndpoint::Unix {
472+
path: PathBuf::from("/tmp/test.sock"),
473+
peer: Some(UnixPeerCred {
474+
pid: 1,
475+
uid: 2,
476+
gid: 3,
477+
}),
478+
}
479+
);
480+
}
481+
482+
#[tokio::test]
483+
async fn unix_peer_cred_listener_populates_peer() {
484+
let unique = SystemTime::now()
485+
.duration_since(UNIX_EPOCH)
486+
.unwrap()
487+
.as_nanos();
488+
let path = std::env::temp_dir().join(format!("ra-rpc-peer-{unique}.sock"));
489+
490+
let listener = UnixListener::bind(&path, false).await.unwrap();
491+
let listener = UnixPeerCredListener::new(listener);
492+
493+
let client = tokio::spawn({
494+
let path = path.clone();
495+
async move { tokio::net::UnixStream::connect(path).await }
496+
});
497+
let accepted = listener.accept().await.unwrap();
498+
let _client = client.await.unwrap().unwrap();
499+
let conn = listener.connect(accepted).await.unwrap();
500+
501+
let remote = RemoteEndpoint::from(conn.endpoint().unwrap());
502+
match remote {
503+
RemoteEndpoint::Unix {
504+
path: got_path,
505+
peer,
506+
} => {
507+
assert_eq!(got_path, path);
508+
let peer = peer.expect("expected unix peer credentials");
509+
assert_eq!(peer.pid, std::process::id() as u64);
510+
}
511+
other => panic!("unexpected remote endpoint: {other:?}"),
512+
}
513+
514+
let _ = std::fs::remove_file(path);
515+
}
516+
}
517+
283518
pub async fn handle_prpc_impl<S, Call: RpcCall<S>>(
284519
args: PrpcHandler<'_, '_, S>,
285520
) -> Result<RpcResponse> {

0 commit comments

Comments
 (0)