Skip to content

Commit bdae869

Browse files
committed
feat: add initial support for syncing files to the enclave
The new `files` key in the manifest specifies the list of file paths that should be synced to the enclave. Only absolute paths without any relative components are accepted. This is a simplified implementation to support passing Kubernetes service account credentials and other ConfigMap and Secret data mounted as pod volumes. It comes with the following limitations: * All files must exist on the host (pod) when `enclaver-run` starts. If a file does not exist when the enclave is launched, the enclave will hang forever waiting for its initial sync. * If the path specified in a manifest is a symlink and one of path components of its target is removed or replaced non-atomically (i.e. removed and recreated slower than the inotify debounce interval of 1 second), the file will stop being synced. * Files are synced sequentially, so the startup time of the enclave may be slow if they are large.
1 parent efcbabc commit bdae869

10 files changed

Lines changed: 902 additions & 22 deletions

File tree

enclaver/Cargo.lock

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

enclaver/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ base64 = "0.13"
3434
bollard = { git = "https://github.com/fussybeaver/bollard.git", branch = "master" }
3535
bytes = "1.6"
3636
cbc = { version = "0.1", features = [ "std", "block-padding" ] }
37+
chrono = "0.4"
3738
circbuf = "0.2"
3839
clap = { version = "4.5.31", features = ["derive"] }
3940
console-subscriber = { version = "0.1.10", optional = true }
@@ -52,6 +53,7 @@ json = "0.12"
5253
lazy_static = "1.5"
5354
log = "0.4"
5455
nix = "0.24"
56+
notify-debouncer-full = "0.5"
5557
pkcs8 = { version = "0.9", features = ["pem"] }
5658
pretty_env_logger = "0.5"
5759
rand = { version = "0.8", features = ["std", "std_rng"] }

enclaver/src/bin/odyn/file_sync.rs

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
use anyhow::{anyhow, Context, Result};
2+
use futures::{Stream, StreamExt};
3+
use log::{error, info};
4+
use std::collections::{HashMap, HashSet};
5+
use std::path::PathBuf;
6+
use std::sync::atomic::{AtomicBool, Ordering};
7+
use std::sync::Arc;
8+
9+
use tokio::fs;
10+
use tokio::io::AsyncWriteExt;
11+
use tokio::sync::{mpsc, oneshot};
12+
use tokio::task::JoinHandle;
13+
use tokio_util::codec::{BytesCodec, FramedRead};
14+
use tokio_vsock::VsockStream;
15+
16+
use crate::config::Configuration;
17+
use enclaver::constants::FILE_SYNC_PORT;
18+
use enclaver::files;
19+
use enclaver::json_transport::JsonTransport;
20+
use enclaver::vsock;
21+
22+
struct FileSyncServer {
23+
files: Arc<HashMap<String, PathBuf>>,
24+
incoming: Box<dyn Stream<Item = VsockStream> + Unpin + Send>,
25+
initial_sync_done: Arc<AtomicBool>,
26+
initial_sync_notifier: oneshot::Sender<()>,
27+
}
28+
29+
impl FileSyncServer {
30+
pub fn new(sender: oneshot::Sender<()>, files: &HashSet<String>) -> Result<Self> {
31+
let incoming = Box::new(vsock::serve(FILE_SYNC_PORT)?);
32+
33+
Ok(Self {
34+
files: Arc::new(
35+
files
36+
.iter()
37+
.map(|file| (file.clone(), PathBuf::from(file)))
38+
.collect::<HashMap<_, _>>(),
39+
),
40+
incoming: incoming,
41+
initial_sync_done: Arc::new(AtomicBool::new(false)),
42+
initial_sync_notifier: sender,
43+
})
44+
}
45+
46+
pub async fn serve(self) -> Result<()> {
47+
for path in self.files.values() {
48+
if let Some(parent) = path.parent() {
49+
fs::create_dir_all(parent).await.context(format!(
50+
"cannot create parent directory for {}",
51+
path.to_string_lossy()
52+
))?;
53+
} else {
54+
return Err(anyhow!(
55+
"cannot find parent directory for {}",
56+
path.to_string_lossy()
57+
));
58+
}
59+
}
60+
61+
info!("Waiting for the host to connect for file sync");
62+
63+
let mut incoming = Box::into_pin(self.incoming);
64+
loop {
65+
let initial_conn = incoming.next().await;
66+
if let Some(_) = initial_conn {
67+
break;
68+
}
69+
}
70+
71+
info!("Accepting file sync connections");
72+
73+
let (event_tx, mut event_rx) = mpsc::channel(self.files.len());
74+
let mut initial_sync_files = self.files.keys().cloned().collect::<HashSet<_>>();
75+
let initial_sync_done = self.initial_sync_done.clone();
76+
77+
let server = tokio::task::spawn(async move {
78+
while let Some(stream) = incoming.next().await {
79+
let initial_sync_done = initial_sync_done.clone();
80+
let files = self.files.clone();
81+
let tx = event_tx.clone();
82+
tokio::task::spawn(async move {
83+
match FileSyncServer::service_conn(stream, files).await {
84+
Ok(file) => {
85+
if !initial_sync_done.load(Ordering::SeqCst) {
86+
let _ = tx.send(file).await;
87+
}
88+
}
89+
Err(err) => error!("{err}"),
90+
}
91+
});
92+
}
93+
});
94+
95+
let initial_sync_done = self.initial_sync_done.clone();
96+
97+
while !initial_sync_done.load(Ordering::SeqCst) {
98+
if let Some(file) = event_rx.recv().await {
99+
initial_sync_files.remove(&file);
100+
if initial_sync_files.is_empty() {
101+
initial_sync_done.store(true, Ordering::SeqCst);
102+
break;
103+
}
104+
}
105+
}
106+
107+
let _ = self.initial_sync_notifier.send(());
108+
server.await?;
109+
110+
Ok(())
111+
}
112+
113+
async fn service_conn(
114+
mut vsock: VsockStream,
115+
files: Arc<HashMap<String, PathBuf>>,
116+
) -> Result<String> {
117+
let file_meta = files::Metadata::recv(&mut vsock).await?;
118+
119+
if let Some(file) = files.get(&file_meta.path) {
120+
info!(
121+
"Syncing file: {} ({} bytes)",
122+
file_meta.path, file_meta.size
123+
);
124+
125+
let now = chrono::Utc::now();
126+
let ts = now.timestamp_millis().to_string();
127+
128+
let mut tmp_file = file.as_os_str().to_os_string();
129+
tmp_file.push(std::ffi::OsString::from(".".to_string() + &ts));
130+
131+
{
132+
let mut file = fs::File::create(&tmp_file).await?;
133+
134+
if file_meta.size > 0 {
135+
let mut reader = FramedRead::new(vsock, BytesCodec::new());
136+
let mut bytes_written: u64 = 0;
137+
138+
while let Some(Ok(chunk)) = reader.next().await {
139+
let bytes_pending = if bytes_written + chunk.len() as u64 > file_meta.size {
140+
bytes_written + chunk.len() as u64 - file_meta.size
141+
} else {
142+
chunk.len() as u64
143+
};
144+
145+
file.write_all(&chunk[..bytes_pending as usize]).await?;
146+
bytes_written += bytes_pending;
147+
}
148+
149+
file.flush().await?;
150+
}
151+
}
152+
153+
fs::rename(&tmp_file, &file).await?;
154+
155+
info!("File sync done: {}", file_meta.path);
156+
} else {
157+
return Err(anyhow!(
158+
"received sync request for unknown file {}",
159+
file_meta.path
160+
));
161+
}
162+
163+
Ok(file_meta.path)
164+
}
165+
}
166+
167+
pub struct FileSyncService {
168+
server: Option<JoinHandle<()>>,
169+
}
170+
171+
impl FileSyncService {
172+
pub async fn start(config: &Configuration) -> Result<Self> {
173+
let mut files = HashSet::new();
174+
175+
if let Some(ref manifest_files) = config.manifest.files {
176+
for file_path in manifest_files {
177+
files.insert(file_path.clone());
178+
}
179+
}
180+
181+
let task = if files.is_empty() {
182+
None
183+
} else {
184+
info!("Starting file sync server");
185+
186+
let (sync_tx, sync_rx) = oneshot::channel();
187+
let server = FileSyncServer::new(sync_tx, &files)?;
188+
189+
let handle = tokio::task::spawn(async move {
190+
if let Err(err) = server.serve().await {
191+
error!("{err}");
192+
}
193+
});
194+
195+
info!("Waiting for initial file sync");
196+
let _ = sync_rx.await;
197+
info!("Initial file sync complete");
198+
199+
Some(handle)
200+
};
201+
202+
Ok(Self { server: task })
203+
}
204+
205+
pub async fn stop(self) {
206+
if let Some(server) = self.server {
207+
server.abort();
208+
_ = server.await;
209+
}
210+
}
211+
}

enclaver/src/bin/odyn/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod config;
55
pub mod console;
66
pub mod egress;
77
pub mod enclave;
8+
pub mod file_sync;
89
pub mod ingress;
910
pub mod kms_proxy;
1011
pub mod launcher;
@@ -24,6 +25,7 @@ use api::ApiService;
2425
use config::Configuration;
2526
use console::{AppLog, AppStatus};
2627
use egress::EgressService;
28+
use file_sync::FileSyncService;
2729
use ingress::IngressService;
2830
use kms_proxy::KmsProxyService;
2931

@@ -62,13 +64,15 @@ async fn launch(args: &CliArgs) -> Result<launcher::ExitStatus> {
6264
let kms_proxy = KmsProxyService::start(config.clone(), nsm.clone()).await?;
6365
let api = ApiService::start(&config, nsm.clone()).await?;
6466
let env = sync_environment(&config).await?;
67+
let file_sync = FileSyncService::start(&config).await?;
6568

6669
let creds = launcher::Credentials { uid: 0, gid: 0 };
6770

6871
info!("Starting {:?}", args.entrypoint);
6972
let exit_status = launcher::start_child(args.entrypoint.clone(), creds, env).await??;
7073
info!("Entrypoint {}", exit_status);
7174

75+
file_sync.stop().await;
7276
api.stop().await;
7377
kms_proxy.stop().await;
7478
ingress.stop().await;

enclaver/src/build.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,20 @@ impl EnclaveArtifactBuilder {
8181

8282
self.analyze_manifest(&manifest);
8383

84+
if let Some(ref files) = &manifest.files {
85+
for path in files.iter() {
86+
if PathBuf::from(path).components().any(|component| {
87+
component == std::path::Component::ParentDir
88+
|| component == std::path::Component::CurDir
89+
}) {
90+
return Err(anyhow!(
91+
"File path with relative components in manifest: {}",
92+
path,
93+
));
94+
}
95+
}
96+
}
97+
8498
let resolved_sources = self.resolve_sources(&manifest).await?;
8599

86100
let amended_img = self

enclaver/src/constants.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub const STATUS_PORT: u32 = 17000;
1414
pub const APP_LOG_PORT: u32 = 17001;
1515
pub const HTTP_EGRESS_VSOCK_PORT: u32 = 17002;
1616
pub const ENV_SYNC_PORT: u32 = 17003;
17+
pub const FILE_SYNC_PORT: u32 = 17004;
1718

1819
// Default TCP Port that the egress proxy listens on inside the enclave, if not
1920
// specified in the manifest.

0 commit comments

Comments
 (0)